unrip/test/trading-config.test.mjs
philipp da2202c455
Some checks failed
deploy / deploy (push) Failing after 39s
Reduce dashboard bootstrap timeout risk
Proof: npm test passed 173/173; npm run operator-dashboard:build passed; regression tests cover omitting raw asset payloads from bootstrap rows and using an explicit 45s bootstrap timeout.

Assumptions: The timeout observed by the operator is the browser-side 15000ms fetch limit on /api/bootstrap; trimming asset rows and extending only the bootstrap timeout keeps the dashboard available while preserving visible asset and pair config.

Still fake: raw imported token payloads remain stored in Postgres but are not exposed through a per-asset dashboard detail view in this fix.
2026-05-13 13:00:00 +02:00

576 lines
20 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { evaluateTradeOpportunity } from '../src/core/strategy.mjs';
import {
CURRENT_EURE_ASSET_ID,
CURRENT_NBTC_ASSET_ID,
LEGACY_OMFT_BTC_ASSET_ID,
normalizeOneClickToken,
} from '../src/core/trading-config.mjs';
import {
createPairStrategyConfigVersion,
enableObserveOnlyPair,
importSupportedAssets,
loadAssetCatalogSummary,
loadTradingConfig,
pauseTradingPair,
seedTradingConfig,
setTradingPairMode,
} from '../src/lib/postgres.mjs';
test('1Click token normalizer preserves live asset fields', () => {
const token = normalizeOneClickToken({
assetId: CURRENT_NBTC_ASSET_ID,
decimals: 8,
blockchain: 'near',
symbol: 'BTC',
price: 80293,
priceUpdatedAt: '2026-05-12T16:25:00.425Z',
contractAddress: 'nbtc.bridge.near',
});
assert.equal(token.assetId, CURRENT_NBTC_ASSET_ID);
assert.equal(token.decimals, 8);
assert.equal(token.symbol, 'BTC');
assert.equal(token.latestPrice, '80293');
assert.equal(token.priceUpdatedAt, '2026-05-12T16:25:00.425Z');
});
test('supported token import is idempotent, does not enable inventory, and retires missing assets', async () => {
const pool = createMemoryPool();
const first = await importSupportedAssets(pool, {
fetchedAt: '2026-05-12T16:30:00.000Z',
response: [
token(CURRENT_NBTC_ASSET_ID, 'BTC', 8),
token(CURRENT_EURE_ASSET_ID, 'EURe', 18),
],
});
assert.equal(first.added_count, 2);
assert.equal(pool.assets.get(CURRENT_NBTC_ASSET_ID).enabled_for_inventory, false);
assert.equal(Object.hasOwn(first, 'raw_response'), false);
const second = await importSupportedAssets(pool, {
fetchedAt: '2026-05-12T16:31:00.000Z',
response: [
token(CURRENT_NBTC_ASSET_ID, 'BTC', 8),
token(CURRENT_EURE_ASSET_ID, 'EURe', 18),
],
});
assert.equal(second.added_count, 0);
assert.equal(second.unchanged_count, 2);
const third = await importSupportedAssets(pool, {
fetchedAt: '2026-05-12T16:32:00.000Z',
response: [
{ ...token(CURRENT_NBTC_ASSET_ID, 'BTC', 8), price: 81000 },
],
});
assert.equal(third.updated_count, 1);
assert.equal(third.retired_count, 1);
assert.equal(pool.assets.get(CURRENT_EURE_ASSET_ID).supported, false);
assert.equal(pool.assets.get(CURRENT_EURE_ASSET_ID).retired_at, '2026-05-12T16:32:00.000Z');
});
test('asset catalog summary omits raw token payloads from bootstrap rows', async () => {
const pool = createMemoryPool();
await importSupportedAssets(pool, {
fetchedAt: '2026-05-12T16:30:00.000Z',
response: [{
...token(CURRENT_NBTC_ASSET_ID, 'BTC', 8),
routes: Array.from({ length: 100 }, (_, index) => ({ route: index, detail: 'not for bootstrap' })),
}],
});
const summary = await loadAssetCatalogSummary(pool, { limit: 250 });
const [asset] = summary.items;
assert.equal(summary.counts.known, 1);
assert.equal(asset.asset_id, CURRENT_NBTC_ASSET_ID);
assert.equal(asset.raw_payload_available, true);
assert.equal(Object.hasOwn(asset, 'raw_payload'), false);
assert.equal(Object.hasOwn(asset, 'rawPayload'), false);
});
test('seeded DB config preserves current nBTC/EURe pair, 49 bps edge, and legacy BTC tracking', async () => {
const pool = createMemoryPool();
await seedTradingConfig(pool, { now: '2026-05-12T16:35:00.000Z' });
await seedTradingConfig(pool, { now: '2026-05-12T16:36:00.000Z' });
const snapshot = await loadTradingConfig(pool);
assert.equal(snapshot.ok, true);
assert.equal(snapshot.activePair, `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`);
assert.equal(snapshot.pairs.length, 2);
assert.equal(snapshot.pairByKey.get(snapshot.activePair).strategyConfig.edgeBps, 49);
assert.equal(snapshot.trackedAssetIds.includes(LEGACY_OMFT_BTC_ASSET_ID), true);
assert.equal([...snapshot.makerPairKeys].some((pair) => pair.includes(LEGACY_OMFT_BTC_ASSET_ID)), false);
});
test('missing DB pair config fails closed', async () => {
const snapshot = await loadTradingConfig(createMemoryPool());
assert.equal(snapshot.ok, false);
assert.equal(snapshot.blockReason, 'no_enabled_pairs');
assert.equal(snapshot.enabledPairKeys.size, 0);
});
test('edge update creates a new active strategy version', async () => {
const pool = createMemoryPool();
await seedTradingConfig(pool);
const next = await createPairStrategyConfigVersion(pool, {
pairId: `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`,
edgeBps: 75,
changedBy: 'test',
reason: 'test edge update',
});
const snapshot = await loadTradingConfig(pool);
const versions = [...pool.strategyConfigs.values()]
.filter((row) => row.pair_id === `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`);
assert.equal(next.version, 2);
assert.equal(snapshot.pairByKey.get(`${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`).strategyConfig.edgeBps, 75);
assert.equal(versions.find((row) => row.version === 1).active, false);
});
test('observe-only enable does not downgrade an active trading pair', async () => {
const pool = createMemoryPool();
await seedTradingConfig(pool);
const pairId = `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`;
const pair = await enableObserveOnlyPair(pool, {
assetIn: CURRENT_NBTC_ASSET_ID,
assetOut: CURRENT_EURE_ASSET_ID,
changedBy: 'test',
reason: 'avoid downgrade',
});
const snapshot = await loadTradingConfig(pool);
assert.equal(pair.pairId, pairId);
assert.equal(pair.mode, 'both');
assert.equal(snapshot.pairByKey.get(pairId).makerEnabled, true);
assert.equal(snapshot.pairByKey.get(pairId).takerEnabled, true);
});
test('observe-only enable creates a non-trading tracked pair', async () => {
const pool = createMemoryPool();
await seedTradingConfig(pool);
const pair = await enableObserveOnlyPair(pool, {
assetIn: LEGACY_OMFT_BTC_ASSET_ID,
assetOut: CURRENT_EURE_ASSET_ID,
changedBy: 'test',
reason: 'watch legacy route',
});
const snapshot = await loadTradingConfig(pool);
assert.equal(pair.mode, 'observe_only');
assert.equal(snapshot.pairByKey.get(`${LEGACY_OMFT_BTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`).observeEnabled, true);
assert.equal(snapshot.pairByKey.get(`${LEGACY_OMFT_BTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`).makerEnabled, false);
assert.equal(snapshot.pairByKey.get(`${LEGACY_OMFT_BTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`).takerEnabled, false);
});
test('repo seed does not re-enable pair runtime flags already stored in DB', async () => {
const pool = createMemoryPool();
await seedTradingConfig(pool);
const pairId = `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`;
const routeId = `${pairId}:btc-eur-reference`;
Object.assign(pool.pairs.get(pairId), {
mode: 'observe_only',
enabled: false,
status: 'disabled',
});
Object.assign(pool.routes.get(routeId), {
enabled: false,
});
await seedTradingConfig(pool);
const snapshot = await loadTradingConfig(pool);
const pair = snapshot.pairByKey.get(pairId);
assert.equal(pair.enabled, false);
assert.equal(pair.mode, 'observe_only');
assert.equal(pair.status, 'disabled');
assert.equal(pool.routes.get(routeId).enabled, false);
assert.equal(pair.priceRoute, null);
assert.equal(pair.makerEnabled, false);
assert.equal(pair.takerEnabled, false);
});
test('pair mode updates activate a directed pair without inventing a price route', async () => {
const pool = createMemoryPool();
await seedTradingConfig(pool);
const pairId = `${LEGACY_OMFT_BTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`;
await enableObserveOnlyPair(pool, {
assetIn: LEGACY_OMFT_BTC_ASSET_ID,
assetOut: CURRENT_EURE_ASSET_ID,
changedBy: 'test',
reason: 'watch legacy route',
});
const updated = await setTradingPairMode(pool, {
pairId,
mode: 'maker',
changedBy: 'test',
reason: 'operator activation test',
});
const snapshot = await loadTradingConfig(pool);
const pair = snapshot.pairByKey.get(pairId);
assert.equal(updated.mode, 'maker');
assert.equal(pair.enabled, true);
assert.equal(pair.makerEnabled, true);
assert.equal(pair.canTrade, false);
assert.equal(pair.blockReason, 'pair_strategy_config_missing');
});
test('pair pause disables trading without deleting strategy config or seed restoring it', async () => {
const pool = createMemoryPool();
await seedTradingConfig(pool);
const pairId = `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`;
const paused = await pauseTradingPair(pool, {
pairId,
changedBy: 'test',
reason: 'operator pause test',
});
await seedTradingConfig(pool);
const snapshot = await loadTradingConfig(pool);
const pair = snapshot.pairByKey.get(pairId);
assert.equal(paused.status, 'disabled');
assert.equal(pair.enabled, false);
assert.equal(pair.mode, 'both');
assert.equal(pair.status, 'disabled');
assert.equal(pair.strategyConfig.edgeBps, 49);
assert.equal(pair.makerEnabled, false);
assert.equal(pair.takerEnabled, false);
});
test('pair mode activation requires both assets to be registered', async () => {
const pool = createMemoryPool();
await seedTradingConfig(pool);
await assert.rejects(
setTradingPairMode(pool, {
assetIn: CURRENT_NBTC_ASSET_ID,
assetOut: 'nep141:not-imported.near',
mode: 'maker',
}),
/asset_out is not registered/,
);
});
test('strategy uses DB pair config for current pair and persists config version', async () => {
const pool = createMemoryPool();
const snapshot = await seedTradingConfig(pool);
const result = evaluateTradeOpportunity({
demandEvent: {
payload: {
quote_id: 'quote-db-1',
pair: `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`,
asset_in: CURRENT_NBTC_ASSET_ID,
asset_out: CURRENT_EURE_ASSET_ID,
request_kind: 'exact_in',
amount_in: '5000',
min_deadline_ms: '60000',
},
},
priceEvent: priceEvent(),
inventoryEvent: inventoryEvent(),
config: snapshot,
armed: true,
now: Date.parse('2026-05-12T16:35:05.000Z'),
});
assert.equal(result.decision.decision, 'actionable');
assert.equal(result.decision.edge_bps, '49');
assert.equal(result.decision.pair_config_version, '1');
assert.equal(result.command.quote_output.amount_out, '4975500000000000000');
assert.equal(result.command.pair_config_id, `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}:v1`);
});
function token(assetId, symbol, decimals) {
return {
assetId,
decimals,
blockchain: symbol === 'EURe' ? 'gnosis' : 'near',
symbol,
price: symbol === 'EURe' ? 1.17 : 80293,
priceUpdatedAt: '2026-05-12T16:25:00.425Z',
contractAddress: assetId.replace(/^nep141:/, ''),
};
}
function priceEvent() {
return {
ingested_at: '2026-05-12T16:35:00.000Z',
payload: {
price_id: 'price-db-1',
pair: `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`,
eur_per_btc: '100000.00000000',
eure_per_btc: '100000.00000000',
btc_per_eur: '0.000010000000',
btc_per_eure: '0.000010000000',
source_used: 'kraken',
},
};
}
function inventoryEvent() {
return {
ingested_at: '2026-05-12T16:35:00.000Z',
payload: {
inventory_id: 'inventory-db-1',
spendable: {
[CURRENT_NBTC_ASSET_ID]: '1000000',
[CURRENT_EURE_ASSET_ID]: '10000000000000000000',
[LEGACY_OMFT_BTC_ASSET_ID]: '0',
},
pending_inbound: {
[CURRENT_NBTC_ASSET_ID]: '0',
[CURRENT_EURE_ASSET_ID]: '0',
[LEGACY_OMFT_BTC_ASSET_ID]: '0',
},
},
};
}
function createMemoryPool() {
return {
assets: new Map(),
pairs: new Map(),
strategyConfigs: new Map(),
routes: new Map(),
importRuns: new Map(),
audit: [],
async query(sql, params = []) {
if (/CREATE TABLE|CREATE (UNIQUE )?INDEX/i.test(sql)) return { rows: [], rowCount: 0 };
if (/SELECT \* FROM trading_assets\s*$/i.test(sql)) return rows(this.assets);
if (/SELECT\s+asset_id,[\s\S]+raw_payload_available[\s\S]+FROM trading_assets/i.test(sql)) {
return selectAssetCatalogRows(this, params);
}
if (/SELECT \*\s+FROM trading_assets\s+ORDER BY/i.test(sql)) return rows(this.assets);
if (/INSERT INTO trading_assets/i.test(sql)) return insertAsset(this, params);
if (/UPDATE trading_assets/i.test(sql)) return retireAssets(this, params);
if (/INSERT INTO supported_asset_import_runs/i.test(sql)) return insertImportRun(this, params);
if (/SELECT \*\s+FROM supported_asset_import_runs/i.test(sql)) {
return { rows: [...this.importRuns.values()].slice(-1), rowCount: this.importRuns.size ? 1 : 0 };
}
if (/COUNT\(\*\)::INT AS known_count/i.test(sql)) {
const assets = [...this.assets.values()];
return {
rows: [{
known_count: assets.length,
supported_count: assets.filter((asset) => asset.supported).length,
retired_count: assets.filter((asset) => asset.retired_at || !asset.supported).length,
inventory_enabled_count: assets.filter((asset) => asset.enabled_for_inventory).length,
}],
rowCount: 1,
};
}
if (/INSERT INTO trading_pairs/i.test(sql)) return insertPair(this, params, sql);
if (/SELECT \*\s+FROM trading_pairs\s+WHERE pair_id = \$1/i.test(sql)) {
const row = this.pairs.get(params[0]);
return { rows: row ? [row] : [], rowCount: row ? 1 : 0 };
}
if (/SELECT \*\s+FROM trading_pairs/i.test(sql)) return rows(this.pairs);
if (/INSERT INTO pair_strategy_configs/i.test(sql)) return insertStrategyConfig(this, params);
if (/SELECT \*\s+FROM pair_strategy_configs\s+WHERE active = true/i.test(sql)) {
return { rows: [...this.strategyConfigs.values()].filter((row) => row.active), rowCount: 0 };
}
if (/SELECT \*\s+FROM pair_strategy_configs\s+WHERE pair_id = \$1 AND active = true/i.test(sql)) {
const active = [...this.strategyConfigs.values()]
.filter((row) => row.pair_id === params[0] && row.active)
.sort((left, right) => right.version - left.version)[0];
return { rows: active ? [active] : [], rowCount: active ? 1 : 0 };
}
if (/UPDATE pair_strategy_configs SET active = false/i.test(sql)) {
let count = 0;
for (const row of this.strategyConfigs.values()) {
if (row.pair_id === params[0] && row.active) {
row.active = false;
count += 1;
}
}
return { rows: [], rowCount: count };
}
if (/INSERT INTO pair_price_routes/i.test(sql)) return insertRoute(this, params, sql);
if (/SELECT \*\s+FROM pair_price_routes/i.test(sql)) {
return { rows: [...this.routes.values()].filter((row) => row.enabled), rowCount: 0 };
}
if (/INSERT INTO pair_config_audit_log/i.test(sql)) {
this.audit.push(params);
return { rows: [], rowCount: 1 };
}
throw new Error(`unhandled SQL in memory pool: ${sql}`);
},
};
}
function rows(map) {
return { rows: [...map.values()], rowCount: map.size };
}
function selectAssetCatalogRows(pool, params) {
const limit = Math.max(1, Number(params[0]) || 50);
const selected = [...pool.assets.values()]
.sort((left, right) => String(left.symbol).localeCompare(String(right.symbol))
|| String(left.asset_id).localeCompare(String(right.asset_id)))
.slice(0, limit)
.map((row) => ({
asset_id: row.asset_id,
venue: row.venue,
symbol: row.symbol,
label: row.label,
decimals: row.decimals,
blockchain: row.blockchain,
chain: row.chain,
contract_address: row.contract_address,
latest_price: row.latest_price,
price_updated_at: row.price_updated_at,
supported: row.supported,
retired_at: row.retired_at,
enabled_for_inventory: row.enabled_for_inventory,
role: row.role,
withdraw_address: row.withdraw_address,
raw_payload_available: Object.keys(row.raw_payload || {}).length > 0,
updated_at: row.updated_at,
}));
return { rows: selected, rowCount: selected.length };
}
function insertAsset(pool, params) {
const seed = params.length === 16;
const [
assetId,
venue,
symbol,
label,
decimals,
blockchain,
chain,
contractAddress,
latestPrice,
priceUpdatedAt,
] = params;
const previous = pool.assets.get(assetId);
const row = {
...(previous || {}),
asset_id: assetId,
venue,
symbol,
label,
decimals,
blockchain,
chain,
contract_address: contractAddress,
latest_price: latestPrice,
price_updated_at: priceUpdatedAt,
supported: seed ? (previous?.supported || params[10]) : true,
retired_at: null,
enabled_for_inventory: seed ? true : previous?.enabled_for_inventory === true,
role: seed ? params[12] : previous?.role || null,
withdraw_address: seed ? params[13] : previous?.withdraw_address || '',
raw_payload: JSON.parse(seed ? params[14] : params[10]),
last_supported_at: seed ? params[15] : params[11],
updated_at: seed ? params[15] : params[11],
};
pool.assets.set(assetId, row);
return { rows: [], rowCount: previous ? 0 : 1 };
}
function retireAssets(pool, params) {
const [retiredAt, importedIds] = params;
let count = 0;
for (const row of pool.assets.values()) {
if (row.venue === 'near-intents' && row.supported && !importedIds.includes(row.asset_id)) {
row.supported = false;
row.retired_at ||= retiredAt;
row.updated_at = retiredAt;
count += 1;
}
}
return { rows: [], rowCount: count };
}
function insertImportRun(pool, params) {
const row = {
run_id: params[0],
source_url: params[1],
fetched_at: params[2],
status: params[3],
token_count: params[4],
added_count: params[5],
updated_count: params[6],
unchanged_count: params[7],
retired_count: params[8],
raw_response_hash: params[9],
error: params[10],
raw_response: params[11] == null ? null : JSON.parse(params[11]),
};
pool.importRuns.set(row.run_id, row);
return { rows: [], rowCount: 1 };
}
function insertPair(pool, params, sql = '') {
const previous = pool.pairs.get(params[0]);
const row = {
pair_id: params[0],
venue: params[1],
asset_in: params[2],
asset_out: params[3],
mode: /mode = trading_pairs\.mode/i.test(sql) && previous ? previous.mode : params[4],
enabled: /enabled = trading_pairs\.enabled/i.test(sql) && previous ? previous.enabled : params[5],
status: /status = trading_pairs\.status/i.test(sql) && previous ? previous.status : params[6],
created_at: params[7],
updated_at: params[7],
};
pool.pairs.set(row.pair_id, row);
return { rows: [], rowCount: 1 };
}
function insertStrategyConfig(pool, params) {
const configId = params[0];
if (pool.strategyConfigs.has(configId)) return { rows: [], rowCount: 0 };
const row = {
config_id: configId,
pair_id: params[1],
version: params[2],
active: params[3],
edge_bps: params[4],
max_notional: params[5],
min_notional: params[6],
slippage_bps: params[7],
min_deadline_ms: params[8],
price_max_age_ms: params[9],
inventory_max_age_ms: params[10],
request_default_notional: params[11],
request_max_notional: params[12],
request_max_slippage_bps: params[13],
created_by: params[14],
reason: params[15],
created_at: '2026-05-12T16:35:00.000Z',
};
pool.strategyConfigs.set(configId, row);
return { rows: [], rowCount: 1 };
}
function insertRoute(pool, params, sql = '') {
const previous = pool.routes.get(params[0]);
const row = {
route_id: params[0],
pair_id: params[1],
source: params[2],
base_asset_id: params[3],
quote_asset_id: params[4],
route_config: JSON.parse(params[5]),
max_age_ms: params[6],
enabled: /enabled = pair_price_routes\.enabled/i.test(sql) && previous ? previous.enabled : params[7],
created_at: params[8],
updated_at: params[8],
};
pool.routes.set(row.route_id, row);
return { rows: [], rowCount: 1 };
}