unrip/src/core/trading-config.mjs
philipp c94d651fc9
Some checks failed
deploy / deploy (push) Failing after 38s
Normalize Gnosis bridge chain for USDC
Proof: Gnosis NEAR Intents assets now persist bridge chain eth:100, repo seed corrects legacy USDC chain rows, and tests prove USDC is not inventory-enabled until a pair activation enables it.

Assumptions: NEAR Intents bridge deposit_address uses eth:100 for Gnosis funding handles while dashboard blockchain labels remain gnosis.

Still fake: USDC deposit credit and live solver fills are not proven by this change; supported_tokens RPC remains upstream-unavailable.
2026-05-13 15:47:23 +02:00

285 lines
8.5 KiB
JavaScript

import crypto from 'node:crypto';
import { pairKey } from './assets.mjs';
export const NEAR_INTENTS_VENUE = 'near-intents';
export const ONE_CLICK_TOKENS_URL = 'https://1click.chaindefuser.com/v0/tokens';
export const CURRENT_NBTC_ASSET_ID = 'nep141:nbtc.bridge.near';
export const LEGACY_OMFT_BTC_ASSET_ID = 'nep141:btc.omft.near';
export const CURRENT_EURE_ASSET_ID =
'nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near';
export const CURRENT_USDC_ASSET_ID =
'nep141:gnosis-0x2a22f9c3b484c3629090feed35f17ff8f88f76f0.omft.near';
export const CURRENT_PAIR_KEY = pairKey(CURRENT_NBTC_ASSET_ID, CURRENT_EURE_ASSET_ID);
export const CURRENT_REVERSE_PAIR_KEY = pairKey(CURRENT_EURE_ASSET_ID, CURRENT_NBTC_ASSET_ID);
export const CURRENT_EDGE_BPS = 49;
export const CURRENT_STRATEGY_MAX_NOTIONAL = '150';
export const CURRENT_REQUEST_DEFAULT_NOTIONAL_EURE = '5';
export const CURRENT_REQUEST_MAX_NOTIONAL_EURE = '5';
export const CURRENT_SLIPPAGE_BPS = 200;
export const CURRENT_MIN_DEADLINE_MS = 60_000;
export const CURRENT_PRICE_MAX_AGE_MS = 30_000;
export const CURRENT_INVENTORY_MAX_AGE_MS = 30_000;
export const PAIR_MODES = new Set(['observe_only', 'maker', 'taker', 'both']);
export const PAIR_STATUSES = new Set(['disabled', 'observe_only', 'maker', 'taker', 'both']);
export function normalizeOneClickToken(token) {
if (!isRecord(token)) throw new Error('token record must be an object');
const assetId = stringField(token.assetId ?? token.asset_id ?? token.defuseAssetId, 'assetId');
const symbol = stringField(token.symbol, 'symbol');
const decimals = integerField(token.decimals, 'decimals');
const blockchain = stringField(token.blockchain ?? token.chain, 'blockchain');
const chain = normalizeBridgeChain({ assetId, blockchain });
const contractAddress = optionalString(token.contractAddress ?? token.contract_address);
const latestPrice = token.price == null ? null : String(token.price);
const priceUpdatedAt = optionalTimestamp(token.priceUpdatedAt ?? token.price_updated_at);
return {
assetId,
venue: NEAR_INTENTS_VENUE,
symbol,
label: symbol,
decimals,
blockchain,
chain,
contractAddress,
latestPrice,
priceUpdatedAt,
supported: true,
rawPayload: token,
};
}
export function normalizeOneClickTokenResponse(response) {
const tokens = Array.isArray(response)
? response
: Array.isArray(response?.tokens)
? response.tokens
: Array.isArray(response?.result)
? response.result
: null;
if (!tokens) throw new Error('supported token response must be an array');
return tokens.map((token) => normalizeOneClickToken(token));
}
export function hashJson(value) {
return crypto.createHash('sha256')
.update(JSON.stringify(value))
.digest('hex');
}
export function buildSeedAssets() {
return [
{
assetId: CURRENT_NBTC_ASSET_ID,
venue: NEAR_INTENTS_VENUE,
symbol: 'BTC',
label: 'BTC / nBTC reserve',
decimals: 8,
blockchain: 'near',
chain: 'btc:mainnet',
contractAddress: 'nbtc.bridge.near',
latestPrice: null,
priceUpdatedAt: null,
supported: true,
enabledForInventory: true,
role: 'trading',
rawPayload: { source: 'repo_seed', assetId: CURRENT_NBTC_ASSET_ID },
},
{
assetId: LEGACY_OMFT_BTC_ASSET_ID,
venue: NEAR_INTENTS_VENUE,
symbol: 'BTC',
label: 'BTC / legacy OMFT',
decimals: 8,
blockchain: 'btc',
chain: 'btc:mainnet',
contractAddress: null,
latestPrice: null,
priceUpdatedAt: null,
supported: true,
enabledForInventory: true,
role: 'legacy',
rawPayload: { source: 'repo_seed', assetId: LEGACY_OMFT_BTC_ASSET_ID },
},
{
assetId: CURRENT_EURE_ASSET_ID,
venue: NEAR_INTENTS_VENUE,
symbol: 'EURe',
label: 'EURe',
decimals: 18,
blockchain: 'gnosis',
chain: 'eth:100',
contractAddress: '0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430',
latestPrice: null,
priceUpdatedAt: null,
supported: true,
enabledForInventory: true,
role: 'trading',
withdrawAddress: '0x6C40267e03A97B2132e7a7d3159C88534eBEfdFb',
rawPayload: { source: 'repo_seed', assetId: CURRENT_EURE_ASSET_ID },
},
{
assetId: CURRENT_USDC_ASSET_ID,
venue: NEAR_INTENTS_VENUE,
symbol: 'USDC',
label: 'USDC',
decimals: 6,
blockchain: 'gnosis',
chain: 'eth:100',
contractAddress: '0x2a22f9c3b484c3629090feed35f17ff8f88f76f0',
latestPrice: null,
priceUpdatedAt: null,
supported: true,
enabledForInventory: false,
role: null,
rawPayload: { source: 'repo_seed', assetId: CURRENT_USDC_ASSET_ID },
},
];
}
export function buildSeedPairs() {
return [
{
pairId: CURRENT_PAIR_KEY,
venue: NEAR_INTENTS_VENUE,
assetIn: CURRENT_NBTC_ASSET_ID,
assetOut: CURRENT_EURE_ASSET_ID,
mode: 'both',
enabled: true,
status: 'both',
},
{
pairId: CURRENT_REVERSE_PAIR_KEY,
venue: NEAR_INTENTS_VENUE,
assetIn: CURRENT_EURE_ASSET_ID,
assetOut: CURRENT_NBTC_ASSET_ID,
mode: 'both',
enabled: true,
status: 'both',
},
];
}
export function buildSeedStrategyConfig(pairId, {
version = 1,
active = true,
createdBy = 'repo_seed',
reason = 'seed current nBTC/EURe production config',
} = {}) {
return {
configId: `${pairId}:v${version}`,
pairId,
version,
active,
edgeBps: CURRENT_EDGE_BPS,
maxNotional: CURRENT_STRATEGY_MAX_NOTIONAL,
minNotional: '0',
slippageBps: CURRENT_SLIPPAGE_BPS,
minDeadlineMs: CURRENT_MIN_DEADLINE_MS,
priceMaxAgeMs: CURRENT_PRICE_MAX_AGE_MS,
inventoryMaxAgeMs: CURRENT_INVENTORY_MAX_AGE_MS,
requestDefaultNotional: CURRENT_REQUEST_DEFAULT_NOTIONAL_EURE,
requestMaxNotional: CURRENT_REQUEST_MAX_NOTIONAL_EURE,
requestMaxSlippageBps: CURRENT_SLIPPAGE_BPS,
createdBy,
reason,
};
}
export function buildSeedPriceRoute(pairId) {
return {
routeId: `${pairId}:btc-eur-reference`,
pairId,
source: 'btc_eur_reference',
baseAssetId: CURRENT_NBTC_ASSET_ID,
quoteAssetId: CURRENT_EURE_ASSET_ID,
routeConfig: {
reference_pair: 'BTC/EUR',
eure_per_eur_assumption: '1',
},
maxAgeMs: CURRENT_PRICE_MAX_AGE_MS,
enabled: true,
};
}
export function buildBtcUsdcPriceRoute(pairId) {
return {
routeId: `${pairId}:btc-usdc-reference`,
pairId,
source: 'btc_usdc_reference',
baseAssetId: CURRENT_NBTC_ASSET_ID,
quoteAssetId: CURRENT_USDC_ASSET_ID,
routeConfig: {
reference_pair: 'BTC/USDC',
kraken_pair: 'XBTUSDC',
coingecko_id: 'bitcoin',
coingecko_vs_currency: 'usd',
usdc_per_usd_assumption: '1',
},
maxAgeMs: CURRENT_PRICE_MAX_AGE_MS,
enabled: true,
};
}
export function pairCanObserve(pair) {
return Boolean(pair?.enabled) && PAIR_MODES.has(pair.mode) && pair.status !== 'disabled';
}
export function pairCanMake(pair) {
return pairCanObserve(pair) && ['maker', 'both'].includes(pair.mode);
}
export function pairCanTake(pair) {
return pairCanObserve(pair) && ['taker', 'both'].includes(pair.mode);
}
export function normalizePairMode(mode) {
const normalized = String(mode || '').trim().toLowerCase();
if (!PAIR_MODES.has(normalized)) throw new Error(`unsupported pair mode: ${mode}`);
return normalized;
}
function stringField(value, field) {
const normalized = optionalString(value);
if (!normalized) throw new Error(`${field} is required`);
return normalized;
}
function optionalString(value) {
const normalized = String(value ?? '').trim();
return normalized || null;
}
function normalizeBridgeChain({ assetId, blockchain }) {
const normalizedBlockchain = String(blockchain || '').trim();
const normalizedAssetId = String(assetId || '').trim();
if (normalizedBlockchain === 'gnosis' || normalizedAssetId.startsWith('nep141:gnosis-')) {
return 'eth:100';
}
return normalizedBlockchain;
}
function integerField(value, field) {
const number = Number(value);
if (!Number.isInteger(number) || number < 0) {
throw new Error(`${field} must be a non-negative integer`);
}
return number;
}
function optionalTimestamp(value) {
if (value == null || value === '') return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) throw new Error('priceUpdatedAt must be a timestamp');
return date.toISOString();
}
function isRecord(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}