unrip/test/trading-config.test.mjs
philipp 2ffa4b17f1
Some checks failed
deploy / deploy (push) Failing after 34s
Move trading config into Postgres
Proof: npm test passed 159/159; npm run operator-dashboard:build passed; repo-local Postgres importer smoke test imported 163 live 1Click tokens with only 3 inventory-enabled seed assets and nBTC/EURe pairs at 49 bps.

Assumptions: Forgejo main push is the repo deployment path; production has existing repo-managed POSTGRES_URL/POSTGRES_PASSWORD/NEAR_INTENTS_API_KEY secrets; startup seed may create initial current nBTC/EURe config but must preserve DB runtime pair flags after creation.

Still fake: no live funds movement was attempted; imported supported assets remain catalog-only unless explicitly enabled in DB; production rollout evidence still depends on the Forgejo deploy job completing after this push.
2026-05-12 21:34:58 +02:00

458 lines
16 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,
loadTradingConfig,
seedTradingConfig,
} 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('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('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+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 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 };
}