Some checks failed
deploy / deploy (push) Failing after 35s
Proof: Dashboard portfolio metrics now include DB-tracked USDC balances valued from live BTC/EUR and BTC/USDC reference prices, with regression coverage for the observed USDC inventory case. Assumptions: USDC is cash-equivalent for valuation when a fresh BTC/USDC reference event is available; live trading safety remains governed by pair config and price route checks. Still fake: Portfolio valuation still does not provide fee-complete realized PnL or generalized valuation for every imported non-stable asset.
3704 lines
116 KiB
JavaScript
3704 lines
116 KiB
JavaScript
import { Pool } from 'pg';
|
|
|
|
import { deriveIntentRequestOutcomeRecords } from '../core/intent-request-outcomes.mjs';
|
|
import { buildCashEquivalentValuationAssets } from '../core/portfolio-metrics.mjs';
|
|
import { deriveQuoteOutcomeRecords } from '../core/quote-outcomes.mjs';
|
|
import {
|
|
CURRENT_NBTC_ASSET_ID,
|
|
CURRENT_USDC_ASSET_ID,
|
|
ONE_CLICK_TOKENS_URL,
|
|
buildBtcUsdcPriceRoute,
|
|
buildSeedAssets,
|
|
buildSeedPairs,
|
|
buildSeedPriceRoute,
|
|
buildSeedStrategyConfig,
|
|
hashJson,
|
|
normalizeOneClickTokenResponse,
|
|
normalizePairMode,
|
|
pairCanMake,
|
|
pairCanObserve,
|
|
pairCanTake,
|
|
} from '../core/trading-config.mjs';
|
|
|
|
const TABLES = [
|
|
'raw_near_intents_quotes',
|
|
'swap_demand_events',
|
|
'market_price_events',
|
|
'intent_inventory_snapshots',
|
|
'liquidity_actions',
|
|
'funding_observations',
|
|
'ops_alerts',
|
|
'environment_status_events',
|
|
'trade_decisions',
|
|
'execute_trade_commands',
|
|
'trade_execution_results',
|
|
'intent_request_preflights',
|
|
'intent_request_submission_results',
|
|
];
|
|
|
|
const PORTFOLIO_METRICS_TABLE = 'portfolio_metrics_snapshots';
|
|
const QUOTE_OUTCOMES_TABLE = 'quote_outcome_attributions';
|
|
const INTENT_REQUEST_OUTCOMES_TABLE = 'intent_request_outcomes';
|
|
const SUPPORTED_ASSET_IMPORT_RUNS_TABLE = 'supported_asset_import_runs';
|
|
const TRADING_ASSETS_TABLE = 'trading_assets';
|
|
const TRADING_PAIRS_TABLE = 'trading_pairs';
|
|
const PAIR_STRATEGY_CONFIGS_TABLE = 'pair_strategy_configs';
|
|
const PAIR_PRICE_ROUTES_TABLE = 'pair_price_routes';
|
|
const PAIR_CONFIG_AUDIT_LOG_TABLE = 'pair_config_audit_log';
|
|
const CREDITED_LIQUIDITY_STATUSES = ['CREDITED', 'COMPLETED', 'FINALIZED', 'SETTLED'];
|
|
const COMPLETED_WITHDRAWAL_STATUSES = ['COMPLETED', 'FINALIZED', 'SETTLED'];
|
|
const REFRESHABLE_INTENT_REQUEST_OUTCOME_STATUSES = [
|
|
'draft',
|
|
'submitted',
|
|
'accepted_by_relay',
|
|
'awaiting_settlement',
|
|
];
|
|
|
|
export function createPostgresPool({ connectionString }) {
|
|
return new Pool({
|
|
connectionString,
|
|
});
|
|
}
|
|
|
|
async function withTransaction(pool, operation) {
|
|
if (typeof pool.connect !== 'function') return operation(pool);
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
const result = await operation(client);
|
|
await client.query('COMMIT');
|
|
return result;
|
|
} catch (error) {
|
|
try {
|
|
await client.query('ROLLBACK');
|
|
} catch {
|
|
// Preserve the original transaction failure.
|
|
}
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
export async function ensureHistorySchema(pool) {
|
|
await ensureTradingConfigSchema(pool);
|
|
|
|
for (const table of TABLES) {
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS ${table} (
|
|
event_id TEXT PRIMARY KEY,
|
|
topic TEXT NOT NULL,
|
|
venue TEXT NOT NULL,
|
|
source TEXT,
|
|
event_type TEXT NOT NULL,
|
|
observed_at TIMESTAMPTZ,
|
|
ingested_at TIMESTAMPTZ NOT NULL,
|
|
quote_id TEXT,
|
|
pair TEXT,
|
|
decision_key TEXT,
|
|
payload JSONB NOT NULL,
|
|
raw JSONB
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${table}_quote_id_idx
|
|
ON ${table} (quote_id)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${table}_decision_key_idx
|
|
ON ${table} (decision_key)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${table}_ingested_at_idx
|
|
ON ${table} (ingested_at DESC)
|
|
`);
|
|
}
|
|
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'funding_observations_tx_hash_idx',
|
|
table: 'funding_observations',
|
|
expression: "(payload->>'tx_hash')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'funding_observations_handle_idx',
|
|
table: 'funding_observations',
|
|
expression: "(payload->>'funding_handle')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'funding_observations_asset_id_idx',
|
|
table: 'funding_observations',
|
|
expression: "(payload->>'asset_id')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'funding_observations_chain_idx',
|
|
table: 'funding_observations',
|
|
expression: "(payload->>'chain')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'funding_observations_status_idx',
|
|
table: 'funding_observations',
|
|
expression: "(payload->>'status')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'ops_alerts_alert_code_idx',
|
|
table: 'ops_alerts',
|
|
expression: "(payload->>'alert_code')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'ops_alerts_status_idx',
|
|
table: 'ops_alerts',
|
|
expression: "(payload->>'status')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'ops_alerts_asset_id_idx',
|
|
table: 'ops_alerts',
|
|
expression: "(payload->>'asset_id')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'environment_status_events_key_idx',
|
|
table: 'environment_status_events',
|
|
expression: "(payload->>'environment_key')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'environment_status_events_status_idx',
|
|
table: 'environment_status_events',
|
|
expression: "(payload->>'status')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'environment_status_events_fingerprint_idx',
|
|
table: 'environment_status_events',
|
|
expression: "(payload->>'status_fingerprint')",
|
|
});
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS ${PORTFOLIO_METRICS_TABLE} (
|
|
metric_id TEXT PRIMARY KEY,
|
|
computed_at TIMESTAMPTZ NOT NULL,
|
|
baseline_anchor_at TIMESTAMPTZ,
|
|
baseline_status TEXT NOT NULL,
|
|
payload JSONB NOT NULL
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${PORTFOLIO_METRICS_TABLE}_computed_at_idx
|
|
ON ${PORTFOLIO_METRICS_TABLE} (computed_at DESC)
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS notification_deliveries (
|
|
notification_key TEXT PRIMARY KEY,
|
|
notification_type TEXT NOT NULL,
|
|
source_kind TEXT NOT NULL,
|
|
source_id TEXT,
|
|
status TEXT NOT NULL,
|
|
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
first_attempt_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_attempt_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
delivered_at TIMESTAMPTZ,
|
|
payload JSONB NOT NULL,
|
|
response JSONB,
|
|
error JSONB
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS notification_deliveries_status_idx
|
|
ON notification_deliveries (status, last_attempt_at DESC)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS notification_deliveries_type_idx
|
|
ON notification_deliveries (notification_type, last_attempt_at DESC)
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS ${QUOTE_OUTCOMES_TABLE} (
|
|
quote_id TEXT PRIMARY KEY,
|
|
decision_id TEXT,
|
|
command_id TEXT,
|
|
execution_result_status TEXT NOT NULL,
|
|
execution_result_code TEXT,
|
|
submitted_at TIMESTAMPTZ,
|
|
command_at TIMESTAMPTZ,
|
|
outcome_status TEXT NOT NULL,
|
|
outcome_observed_at TIMESTAMPTZ,
|
|
outcome_source TEXT NOT NULL,
|
|
attribution_status TEXT NOT NULL,
|
|
attribution_method TEXT,
|
|
attributed_inventory_delta JSONB,
|
|
computed_at TIMESTAMPTZ NOT NULL,
|
|
payload JSONB NOT NULL
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${QUOTE_OUTCOMES_TABLE}_outcome_observed_at_idx
|
|
ON ${QUOTE_OUTCOMES_TABLE} (outcome_observed_at DESC)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${QUOTE_OUTCOMES_TABLE}_outcome_status_idx
|
|
ON ${QUOTE_OUTCOMES_TABLE} (outcome_status)
|
|
`);
|
|
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'intent_request_preflights_request_id_idx',
|
|
table: 'intent_request_preflights',
|
|
expression: "(payload->>'request_id')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'intent_request_preflights_idempotency_key_idx',
|
|
table: 'intent_request_preflights',
|
|
expression: "(payload->>'idempotency_key')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'intent_request_submission_results_request_id_idx',
|
|
table: 'intent_request_submission_results',
|
|
expression: "(payload->>'request_id')",
|
|
});
|
|
await ensureExpressionIndex(pool, {
|
|
name: 'intent_request_submission_results_idempotency_key_idx',
|
|
table: 'intent_request_submission_results',
|
|
expression: "(payload->>'idempotency_key')",
|
|
});
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS ${INTENT_REQUEST_OUTCOMES_TABLE} (
|
|
request_id TEXT PRIMARY KEY,
|
|
idempotency_key TEXT NOT NULL,
|
|
submission_id TEXT,
|
|
intent_hash TEXT,
|
|
submission_status TEXT,
|
|
relay_status TEXT,
|
|
submitted_at TIMESTAMPTZ,
|
|
outcome_status TEXT NOT NULL,
|
|
outcome_observed_at TIMESTAMPTZ,
|
|
outcome_source TEXT NOT NULL,
|
|
outcome_reason TEXT NOT NULL,
|
|
attribution_status TEXT NOT NULL,
|
|
attribution_method TEXT,
|
|
attributed_inventory_delta JSONB,
|
|
computed_at TIMESTAMPTZ NOT NULL,
|
|
payload JSONB NOT NULL
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${INTENT_REQUEST_OUTCOMES_TABLE}_outcome_observed_at_idx
|
|
ON ${INTENT_REQUEST_OUTCOMES_TABLE} (outcome_observed_at DESC)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${INTENT_REQUEST_OUTCOMES_TABLE}_outcome_status_idx
|
|
ON ${INTENT_REQUEST_OUTCOMES_TABLE} (outcome_status)
|
|
`);
|
|
}
|
|
|
|
export async function ensureTradingConfigSchema(pool) {
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS ${SUPPORTED_ASSET_IMPORT_RUNS_TABLE} (
|
|
run_id TEXT PRIMARY KEY,
|
|
source_url TEXT NOT NULL,
|
|
fetched_at TIMESTAMPTZ NOT NULL,
|
|
status TEXT NOT NULL,
|
|
token_count INTEGER NOT NULL DEFAULT 0,
|
|
added_count INTEGER NOT NULL DEFAULT 0,
|
|
updated_count INTEGER NOT NULL DEFAULT 0,
|
|
unchanged_count INTEGER NOT NULL DEFAULT 0,
|
|
retired_count INTEGER NOT NULL DEFAULT 0,
|
|
raw_response_hash TEXT,
|
|
error TEXT,
|
|
raw_response JSONB
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${SUPPORTED_ASSET_IMPORT_RUNS_TABLE}_fetched_at_idx
|
|
ON ${SUPPORTED_ASSET_IMPORT_RUNS_TABLE} (fetched_at DESC)
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS ${TRADING_ASSETS_TABLE} (
|
|
asset_id TEXT PRIMARY KEY,
|
|
venue TEXT NOT NULL,
|
|
symbol TEXT NOT NULL,
|
|
label TEXT NOT NULL,
|
|
decimals INTEGER NOT NULL,
|
|
blockchain TEXT,
|
|
chain TEXT,
|
|
contract_address TEXT,
|
|
latest_price TEXT,
|
|
price_updated_at TIMESTAMPTZ,
|
|
supported BOOLEAN NOT NULL DEFAULT false,
|
|
retired_at TIMESTAMPTZ,
|
|
enabled_for_inventory BOOLEAN NOT NULL DEFAULT false,
|
|
role TEXT,
|
|
withdraw_address TEXT,
|
|
raw_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_supported_at TIMESTAMPTZ,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${TRADING_ASSETS_TABLE}_supported_idx
|
|
ON ${TRADING_ASSETS_TABLE} (supported, updated_at DESC)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${TRADING_ASSETS_TABLE}_inventory_idx
|
|
ON ${TRADING_ASSETS_TABLE} (enabled_for_inventory, asset_id)
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS ${TRADING_PAIRS_TABLE} (
|
|
pair_id TEXT PRIMARY KEY,
|
|
venue TEXT NOT NULL,
|
|
asset_in TEXT NOT NULL REFERENCES ${TRADING_ASSETS_TABLE}(asset_id),
|
|
asset_out TEXT NOT NULL REFERENCES ${TRADING_ASSETS_TABLE}(asset_id),
|
|
mode TEXT NOT NULL,
|
|
enabled BOOLEAN NOT NULL DEFAULT false,
|
|
status TEXT NOT NULL DEFAULT 'disabled',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (venue, asset_in, asset_out)
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${TRADING_PAIRS_TABLE}_enabled_idx
|
|
ON ${TRADING_PAIRS_TABLE} (enabled, status, updated_at DESC)
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS ${PAIR_STRATEGY_CONFIGS_TABLE} (
|
|
config_id TEXT PRIMARY KEY,
|
|
pair_id TEXT NOT NULL REFERENCES ${TRADING_PAIRS_TABLE}(pair_id),
|
|
version INTEGER NOT NULL,
|
|
active BOOLEAN NOT NULL DEFAULT false,
|
|
edge_bps INTEGER NOT NULL,
|
|
max_notional NUMERIC NOT NULL,
|
|
min_notional NUMERIC NOT NULL DEFAULT 0,
|
|
slippage_bps INTEGER NOT NULL DEFAULT 0,
|
|
min_deadline_ms INTEGER NOT NULL,
|
|
price_max_age_ms INTEGER NOT NULL,
|
|
inventory_max_age_ms INTEGER NOT NULL,
|
|
request_default_notional NUMERIC,
|
|
request_max_notional NUMERIC,
|
|
request_max_slippage_bps INTEGER,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_by TEXT NOT NULL,
|
|
reason TEXT,
|
|
UNIQUE (pair_id, version)
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE UNIQUE INDEX IF NOT EXISTS ${PAIR_STRATEGY_CONFIGS_TABLE}_active_one_idx
|
|
ON ${PAIR_STRATEGY_CONFIGS_TABLE} (pair_id)
|
|
WHERE active
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS ${PAIR_PRICE_ROUTES_TABLE} (
|
|
route_id TEXT PRIMARY KEY,
|
|
pair_id TEXT NOT NULL REFERENCES ${TRADING_PAIRS_TABLE}(pair_id),
|
|
source TEXT NOT NULL,
|
|
base_asset_id TEXT NOT NULL REFERENCES ${TRADING_ASSETS_TABLE}(asset_id),
|
|
quote_asset_id TEXT NOT NULL REFERENCES ${TRADING_ASSETS_TABLE}(asset_id),
|
|
route_config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
max_age_ms INTEGER NOT NULL,
|
|
enabled BOOLEAN NOT NULL DEFAULT false,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${PAIR_PRICE_ROUTES_TABLE}_pair_enabled_idx
|
|
ON ${PAIR_PRICE_ROUTES_TABLE} (pair_id, enabled)
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS ${PAIR_CONFIG_AUDIT_LOG_TABLE} (
|
|
audit_id TEXT PRIMARY KEY,
|
|
entity_type TEXT NOT NULL,
|
|
entity_id TEXT NOT NULL,
|
|
action TEXT NOT NULL,
|
|
old_value JSONB,
|
|
new_value JSONB,
|
|
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
changed_by TEXT NOT NULL,
|
|
reason TEXT
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${PAIR_CONFIG_AUDIT_LOG_TABLE}_entity_idx
|
|
ON ${PAIR_CONFIG_AUDIT_LOG_TABLE} (entity_type, entity_id, changed_at DESC)
|
|
`);
|
|
}
|
|
|
|
export async function seedTradingConfig(pool, {
|
|
now = new Date().toISOString(),
|
|
changedBy = 'repo_seed',
|
|
} = {}) {
|
|
await ensureTradingConfigSchema(pool);
|
|
|
|
for (const asset of buildSeedAssets()) {
|
|
await upsertSeedAsset(pool, { asset, now });
|
|
}
|
|
|
|
for (const pair of buildSeedPairs()) {
|
|
await upsertSeedPair(pool, { pair, now, preserveRuntimeState: true });
|
|
await upsertSeedStrategyConfig(pool, {
|
|
config: buildSeedStrategyConfig(pair.pairId, { createdBy: changedBy }),
|
|
});
|
|
await upsertSeedPriceRoute(pool, {
|
|
route: buildSeedPriceRoute(pair.pairId),
|
|
now,
|
|
});
|
|
}
|
|
|
|
await seedKnownEnabledPairRuntimeConfig(pool, { now });
|
|
|
|
return loadTradingConfig(pool);
|
|
}
|
|
|
|
export async function importSupportedAssets(pool, {
|
|
sourceUrl = ONE_CLICK_TOKENS_URL,
|
|
fetchedAt = new Date().toISOString(),
|
|
fetchImpl = globalThis.fetch,
|
|
response = null,
|
|
} = {}) {
|
|
await ensureTradingConfigSchema(pool);
|
|
|
|
let rawResponse = response;
|
|
let rawResponseHash = null;
|
|
let runId = null;
|
|
|
|
try {
|
|
if (rawResponse == null) {
|
|
if (typeof fetchImpl !== 'function') throw new Error('fetch implementation is unavailable');
|
|
const fetchResponse = await fetchImpl(sourceUrl);
|
|
const text = await fetchResponse.text();
|
|
if (!fetchResponse.ok) throw new Error(`HTTP ${fetchResponse.status}: ${text.slice(0, 200)}`);
|
|
rawResponse = text ? JSON.parse(text) : null;
|
|
}
|
|
|
|
const normalizedTokens = normalizeOneClickTokenResponse(rawResponse);
|
|
rawResponseHash = hashJson(rawResponse);
|
|
runId = `asset-import:${Date.parse(fetchedAt)}:${rawResponseHash.slice(0, 16)}`;
|
|
|
|
const existing = await loadTradingAssetsById(pool);
|
|
let addedCount = 0;
|
|
let updatedCount = 0;
|
|
let unchangedCount = 0;
|
|
|
|
for (const token of normalizedTokens) {
|
|
const previous = existing.get(token.assetId) || null;
|
|
if (!previous) addedCount += 1;
|
|
else if (importedAssetChanged(previous, token)) updatedCount += 1;
|
|
else unchangedCount += 1;
|
|
|
|
await upsertImportedAsset(pool, {
|
|
asset: token,
|
|
fetchedAt,
|
|
});
|
|
}
|
|
|
|
const importedAssetIds = normalizedTokens.map((token) => token.assetId);
|
|
const retiredResult = await pool.query(
|
|
`
|
|
UPDATE ${TRADING_ASSETS_TABLE}
|
|
SET
|
|
supported = false,
|
|
retired_at = COALESCE(retired_at, $1),
|
|
updated_at = $1
|
|
WHERE venue = 'near-intents'
|
|
AND supported = true
|
|
AND NOT (asset_id = ANY($2::text[]))
|
|
RETURNING asset_id
|
|
`,
|
|
[fetchedAt, importedAssetIds],
|
|
);
|
|
|
|
const summary = {
|
|
run_id: runId,
|
|
source_url: sourceUrl,
|
|
fetched_at: fetchedAt,
|
|
status: 'success',
|
|
token_count: normalizedTokens.length,
|
|
added_count: addedCount,
|
|
updated_count: updatedCount,
|
|
unchanged_count: unchangedCount,
|
|
retired_count: retiredResult.rowCount,
|
|
raw_response_hash: rawResponseHash,
|
|
error: null,
|
|
raw_response: rawResponse,
|
|
};
|
|
await insertAssetImportRun(pool, summary);
|
|
return publicAssetImportRunSummary(summary);
|
|
} catch (error) {
|
|
const fallbackHash = rawResponse == null ? null : hashJson(rawResponse);
|
|
runId ||= `asset-import:${Date.parse(fetchedAt)}:${fallbackHash?.slice(0, 16) || 'failed'}`;
|
|
const summary = {
|
|
run_id: runId,
|
|
source_url: sourceUrl,
|
|
fetched_at: fetchedAt,
|
|
status: 'failed',
|
|
token_count: 0,
|
|
added_count: 0,
|
|
updated_count: 0,
|
|
unchanged_count: 0,
|
|
retired_count: 0,
|
|
raw_response_hash: fallbackHash,
|
|
error: error.message,
|
|
raw_response: rawResponse,
|
|
};
|
|
await insertAssetImportRun(pool, summary);
|
|
throw Object.assign(error, { importRun: publicAssetImportRunSummary(summary) });
|
|
}
|
|
}
|
|
|
|
export async function loadTradingConfig(pool) {
|
|
await ensureTradingConfigSchema(pool);
|
|
|
|
const [
|
|
assetResult,
|
|
pairResult,
|
|
strategyResult,
|
|
routeResult,
|
|
latestImportResult,
|
|
] = await Promise.all([
|
|
pool.query(`
|
|
SELECT *
|
|
FROM ${TRADING_ASSETS_TABLE}
|
|
ORDER BY symbol ASC, asset_id ASC
|
|
`),
|
|
pool.query(`
|
|
SELECT *
|
|
FROM ${TRADING_PAIRS_TABLE}
|
|
ORDER BY created_at ASC, pair_id ASC
|
|
`),
|
|
pool.query(`
|
|
SELECT *
|
|
FROM ${PAIR_STRATEGY_CONFIGS_TABLE}
|
|
WHERE active = true
|
|
ORDER BY pair_id ASC, version DESC
|
|
`),
|
|
pool.query(`
|
|
SELECT *
|
|
FROM ${PAIR_PRICE_ROUTES_TABLE}
|
|
WHERE enabled = true
|
|
ORDER BY pair_id ASC, created_at DESC
|
|
`),
|
|
pool.query(`
|
|
SELECT *
|
|
FROM ${SUPPORTED_ASSET_IMPORT_RUNS_TABLE}
|
|
ORDER BY fetched_at DESC
|
|
LIMIT 1
|
|
`),
|
|
]);
|
|
|
|
return buildTradingConfigSnapshot({
|
|
assetRows: assetResult.rows,
|
|
pairRows: pairResult.rows,
|
|
strategyRows: strategyResult.rows,
|
|
routeRows: routeResult.rows,
|
|
latestImportRun: latestImportResult.rows[0] || null,
|
|
});
|
|
}
|
|
|
|
export function createTradingConfigStore({
|
|
pool,
|
|
cacheTtlMs = 5_000,
|
|
logger = null,
|
|
} = {}) {
|
|
if (!pool) throw new Error('pool is required');
|
|
let cached = null;
|
|
let cachedAtMs = 0;
|
|
let lastError = null;
|
|
|
|
async function refresh({ force = false } = {}) {
|
|
const nowMs = Date.now();
|
|
if (!force && cached && nowMs - cachedAtMs <= cacheTtlMs) return cached;
|
|
|
|
try {
|
|
cached = await loadTradingConfig(pool);
|
|
cachedAtMs = nowMs;
|
|
lastError = null;
|
|
return cached;
|
|
} catch (error) {
|
|
lastError = error;
|
|
logger?.error?.('trading_config_load_failed', {
|
|
details: { error: error.message },
|
|
});
|
|
cached = buildFailClosedTradingConfig(error);
|
|
cachedAtMs = nowMs;
|
|
return cached;
|
|
}
|
|
}
|
|
|
|
return {
|
|
getConfig: refresh,
|
|
async forceRefresh() {
|
|
return refresh({ force: true });
|
|
},
|
|
getCachedConfig() {
|
|
return cached || buildFailClosedTradingConfig(lastError || new Error('config not loaded'));
|
|
},
|
|
getState() {
|
|
return summarizeTradingConfigSnapshot(
|
|
cached || buildFailClosedTradingConfig(lastError || new Error('config not loaded')),
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function loadAssetCatalogSummary(pool, { limit = 250 } = {}) {
|
|
await ensureTradingConfigSchema(pool);
|
|
const boundedLimit = Math.max(1, Number(limit) || 50);
|
|
const [latestImportResult, countResult, assetResult] = await Promise.all([
|
|
pool.query(`
|
|
SELECT *
|
|
FROM ${SUPPORTED_ASSET_IMPORT_RUNS_TABLE}
|
|
ORDER BY fetched_at DESC
|
|
LIMIT 1
|
|
`),
|
|
pool.query(`
|
|
SELECT
|
|
COUNT(*)::INT AS known_count,
|
|
COUNT(*) FILTER (WHERE supported)::INT AS supported_count,
|
|
COUNT(*) FILTER (WHERE retired_at IS NOT NULL OR supported = false)::INT AS retired_count,
|
|
COUNT(*) FILTER (WHERE enabled_for_inventory)::INT AS inventory_enabled_count
|
|
FROM ${TRADING_ASSETS_TABLE}
|
|
`),
|
|
pool.query(`
|
|
SELECT
|
|
asset_id,
|
|
venue,
|
|
symbol,
|
|
label,
|
|
decimals,
|
|
blockchain,
|
|
chain,
|
|
contract_address,
|
|
latest_price,
|
|
price_updated_at,
|
|
supported,
|
|
retired_at,
|
|
enabled_for_inventory,
|
|
role,
|
|
withdraw_address,
|
|
raw_payload <> '{}'::jsonb AS raw_payload_available,
|
|
updated_at
|
|
FROM ${TRADING_ASSETS_TABLE}
|
|
ORDER BY symbol ASC, asset_id ASC
|
|
LIMIT $1
|
|
`, [boundedLimit]),
|
|
]);
|
|
const counts = countResult.rows[0] || {};
|
|
return {
|
|
latest_import: normalizeAssetImportRunRow(latestImportResult.rows[0] || null),
|
|
counts: {
|
|
known: Number(counts.known_count || 0),
|
|
supported: Number(counts.supported_count || 0),
|
|
retired: Number(counts.retired_count || 0),
|
|
inventory_enabled: Number(counts.inventory_enabled_count || 0),
|
|
},
|
|
items: assetResult.rows.map(normalizeAssetCatalogSummaryRow),
|
|
};
|
|
}
|
|
|
|
export async function loadPairConfigSummary(pool) {
|
|
const snapshot = await loadTradingConfig(pool);
|
|
return {
|
|
ok: snapshot.ok,
|
|
block_reason: snapshot.blockReason,
|
|
loaded_at: snapshot.loadedAt,
|
|
pairs: snapshot.pairs,
|
|
};
|
|
}
|
|
|
|
export async function createPairStrategyConfigVersion(pool, {
|
|
pairId = null,
|
|
pair = null,
|
|
edgeBps = null,
|
|
maxNotional = null,
|
|
minNotional = null,
|
|
slippageBps = null,
|
|
minDeadlineMs = null,
|
|
priceMaxAgeMs = null,
|
|
inventoryMaxAgeMs = null,
|
|
requestDefaultNotional = undefined,
|
|
requestMaxNotional = undefined,
|
|
requestMaxSlippageBps = undefined,
|
|
changedBy = 'operator',
|
|
reason = 'operator config update',
|
|
} = {}) {
|
|
await ensureTradingConfigSchema(pool);
|
|
const resolvedPairId = pairId || pair;
|
|
if (!resolvedPairId) throw new Error('pair_id is required');
|
|
|
|
return withTransaction(pool, async (client) => {
|
|
const activeResult = await client.query(
|
|
`
|
|
SELECT *
|
|
FROM ${PAIR_STRATEGY_CONFIGS_TABLE}
|
|
WHERE pair_id = $1 AND active = true
|
|
ORDER BY version DESC
|
|
LIMIT 1
|
|
`,
|
|
[resolvedPairId],
|
|
);
|
|
const active = activeResult.rows[0];
|
|
if (!active) throw new Error(`active strategy config missing for pair ${resolvedPairId}`);
|
|
|
|
const nextVersion = Number(active.version || 0) + 1;
|
|
const nextEdgeBps = edgeBps == null ? Number(active.edge_bps) : Number(edgeBps);
|
|
if (!Number.isInteger(nextEdgeBps) || nextEdgeBps <= 0) {
|
|
throw new Error('edge_bps must be a positive integer');
|
|
}
|
|
const nextMaxNotional = positiveNumberStringOrDefault(maxNotional, active.max_notional, 'max_notional');
|
|
|
|
const nextConfig = {
|
|
configId: `${resolvedPairId}:v${nextVersion}`,
|
|
pairId: resolvedPairId,
|
|
version: nextVersion,
|
|
edgeBps: nextEdgeBps,
|
|
maxNotional: nextMaxNotional,
|
|
minNotional: minNotional == null ? String(active.min_notional) : String(minNotional),
|
|
slippageBps: slippageBps == null ? Number(active.slippage_bps) : Number(slippageBps),
|
|
minDeadlineMs: minDeadlineMs == null ? Number(active.min_deadline_ms) : Number(minDeadlineMs),
|
|
priceMaxAgeMs: priceMaxAgeMs == null ? Number(active.price_max_age_ms) : Number(priceMaxAgeMs),
|
|
inventoryMaxAgeMs:
|
|
inventoryMaxAgeMs == null ? Number(active.inventory_max_age_ms) : Number(inventoryMaxAgeMs),
|
|
requestDefaultNotional:
|
|
requestDefaultNotional === undefined
|
|
? active.request_default_notional == null ? null : String(active.request_default_notional)
|
|
: nullablePositiveNumberString(requestDefaultNotional, 'request_default_notional'),
|
|
requestMaxNotional:
|
|
requestMaxNotional === undefined
|
|
? active.request_max_notional == null ? null : String(active.request_max_notional)
|
|
: nullablePositiveNumberString(requestMaxNotional, 'request_max_notional'),
|
|
requestMaxSlippageBps:
|
|
requestMaxSlippageBps === undefined
|
|
? active.request_max_slippage_bps == null ? null : Number(active.request_max_slippage_bps)
|
|
: nullableNonNegativeInteger(requestMaxSlippageBps, 'request_max_slippage_bps'),
|
|
createdBy: changedBy,
|
|
reason,
|
|
};
|
|
|
|
await client.query(
|
|
`UPDATE ${PAIR_STRATEGY_CONFIGS_TABLE} SET active = false WHERE pair_id = $1 AND active = true`,
|
|
[resolvedPairId],
|
|
);
|
|
await insertPairStrategyConfig(client, { config: nextConfig, active: true });
|
|
await insertConfigAuditLog(client, {
|
|
entityType: 'pair_strategy_config',
|
|
entityId: resolvedPairId,
|
|
action: 'version_created',
|
|
oldValue: normalizeStrategyConfigRow(active),
|
|
newValue: nextConfig,
|
|
changedBy,
|
|
reason,
|
|
});
|
|
|
|
return normalizeStrategyConfigRow({
|
|
...active,
|
|
config_id: nextConfig.configId,
|
|
pair_id: nextConfig.pairId,
|
|
version: nextConfig.version,
|
|
active: true,
|
|
edge_bps: nextConfig.edgeBps,
|
|
max_notional: nextConfig.maxNotional,
|
|
min_notional: nextConfig.minNotional,
|
|
slippage_bps: nextConfig.slippageBps,
|
|
min_deadline_ms: nextConfig.minDeadlineMs,
|
|
price_max_age_ms: nextConfig.priceMaxAgeMs,
|
|
inventory_max_age_ms: nextConfig.inventoryMaxAgeMs,
|
|
request_default_notional: nextConfig.requestDefaultNotional,
|
|
request_max_notional: nextConfig.requestMaxNotional,
|
|
request_max_slippage_bps: nextConfig.requestMaxSlippageBps,
|
|
created_by: changedBy,
|
|
reason,
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function enableObserveOnlyPair(pool, {
|
|
venue = 'near-intents',
|
|
assetIn,
|
|
assetOut,
|
|
changedBy = 'operator',
|
|
reason = 'operator enabled observe-only pair',
|
|
} = {}) {
|
|
await ensureTradingConfigSchema(pool);
|
|
if (!assetIn || !assetOut) throw new Error('asset_in and asset_out are required');
|
|
const pairId = `${assetIn}->${assetOut}`;
|
|
const existingResult = await pool.query(
|
|
`
|
|
SELECT *
|
|
FROM ${TRADING_PAIRS_TABLE}
|
|
WHERE pair_id = $1
|
|
LIMIT 1
|
|
`,
|
|
[pairId],
|
|
);
|
|
const existingPair = existingResult.rows[0] ? normalizeTradingPairRow(existingResult.rows[0]) : null;
|
|
if (existingPair?.enabled && existingPair.status !== 'disabled') {
|
|
return existingPair;
|
|
}
|
|
const pair = {
|
|
pairId,
|
|
venue,
|
|
assetIn,
|
|
assetOut,
|
|
mode: 'observe_only',
|
|
enabled: true,
|
|
status: 'observe_only',
|
|
};
|
|
await upsertSeedPair(pool, { pair, now: new Date().toISOString() });
|
|
await insertConfigAuditLog(pool, {
|
|
entityType: 'trading_pair',
|
|
entityId: pairId,
|
|
action: 'observe_only_enabled',
|
|
oldValue: null,
|
|
newValue: pair,
|
|
changedBy,
|
|
reason,
|
|
});
|
|
return pair;
|
|
}
|
|
|
|
export async function setTradingPairMode(pool, {
|
|
venue = 'near-intents',
|
|
pairId = null,
|
|
pair = null,
|
|
assetIn = null,
|
|
assetOut = null,
|
|
mode = 'observe_only',
|
|
edgeBps = null,
|
|
maxNotional = null,
|
|
minNotional = null,
|
|
slippageBps = null,
|
|
minDeadlineMs = null,
|
|
priceMaxAgeMs = null,
|
|
inventoryMaxAgeMs = null,
|
|
requestDefaultNotional = undefined,
|
|
requestMaxNotional = undefined,
|
|
requestMaxSlippageBps = undefined,
|
|
changedBy = 'operator',
|
|
reason = 'operator pair mode update',
|
|
} = {}) {
|
|
await ensureTradingConfigSchema(pool);
|
|
const normalizedMode = normalizePairMode(mode);
|
|
const resolvedPairId = pairId || pair || (assetIn && assetOut ? `${assetIn}->${assetOut}` : null);
|
|
if (!resolvedPairId) throw new Error('pair_id or asset_in/asset_out is required');
|
|
|
|
return withTransaction(pool, async (client) => {
|
|
const existingResult = await client.query(
|
|
`
|
|
SELECT *
|
|
FROM ${TRADING_PAIRS_TABLE}
|
|
WHERE pair_id = $1
|
|
LIMIT 1
|
|
`,
|
|
[resolvedPairId],
|
|
);
|
|
const existingPair = existingResult.rows[0] ? normalizeTradingPairRow(existingResult.rows[0]) : null;
|
|
const [pairAssetIn, pairAssetOut] = splitPairId(resolvedPairId);
|
|
const resolvedAssetIn = assetIn || existingPair?.assetIn || pairAssetIn;
|
|
const resolvedAssetOut = assetOut || existingPair?.assetOut || pairAssetOut;
|
|
if (!resolvedAssetIn || !resolvedAssetOut) throw new Error('asset_in and asset_out are required');
|
|
|
|
const assets = await loadTradingAssetsById(client);
|
|
if (!assets.has(resolvedAssetIn)) throw new Error(`asset_in is not registered: ${resolvedAssetIn}`);
|
|
if (!assets.has(resolvedAssetOut)) throw new Error(`asset_out is not registered: ${resolvedAssetOut}`);
|
|
|
|
const nextPair = {
|
|
pairId: resolvedPairId,
|
|
venue: existingPair?.venue || venue,
|
|
assetIn: resolvedAssetIn,
|
|
assetOut: resolvedAssetOut,
|
|
mode: normalizedMode,
|
|
enabled: true,
|
|
status: normalizedMode,
|
|
};
|
|
await upsertSeedPair(client, { pair: nextPair, now: new Date().toISOString() });
|
|
|
|
let strategyConfig = null;
|
|
if (pairCanMake(nextPair) || pairCanTake(nextPair)) {
|
|
await enableInventoryForAssets(client, {
|
|
assetIds: [resolvedAssetIn, resolvedAssetOut],
|
|
now: new Date().toISOString(),
|
|
});
|
|
|
|
const activeConfigResult = await client.query(
|
|
`
|
|
SELECT *
|
|
FROM ${PAIR_STRATEGY_CONFIGS_TABLE}
|
|
WHERE pair_id = $1 AND active = true
|
|
ORDER BY version DESC
|
|
LIMIT 1
|
|
`,
|
|
[resolvedPairId],
|
|
);
|
|
strategyConfig = activeConfigResult.rows[0]
|
|
? normalizeStrategyConfigRow(activeConfigResult.rows[0])
|
|
: null;
|
|
|
|
if (!strategyConfig) {
|
|
const nextConfig = buildInitialPairStrategyConfig(resolvedPairId, {
|
|
edgeBps,
|
|
maxNotional,
|
|
minNotional,
|
|
slippageBps,
|
|
minDeadlineMs,
|
|
priceMaxAgeMs,
|
|
inventoryMaxAgeMs,
|
|
requestDefaultNotional,
|
|
requestMaxNotional,
|
|
requestMaxSlippageBps,
|
|
changedBy,
|
|
reason,
|
|
});
|
|
await insertPairStrategyConfig(client, { config: nextConfig, active: true });
|
|
strategyConfig = normalizeStrategyConfigRow({
|
|
config_id: nextConfig.configId,
|
|
pair_id: nextConfig.pairId,
|
|
version: nextConfig.version,
|
|
active: true,
|
|
edge_bps: nextConfig.edgeBps,
|
|
max_notional: nextConfig.maxNotional,
|
|
min_notional: nextConfig.minNotional,
|
|
slippage_bps: nextConfig.slippageBps,
|
|
min_deadline_ms: nextConfig.minDeadlineMs,
|
|
price_max_age_ms: nextConfig.priceMaxAgeMs,
|
|
inventory_max_age_ms: nextConfig.inventoryMaxAgeMs,
|
|
request_default_notional: nextConfig.requestDefaultNotional,
|
|
request_max_notional: nextConfig.requestMaxNotional,
|
|
request_max_slippage_bps: nextConfig.requestMaxSlippageBps,
|
|
created_by: changedBy,
|
|
reason,
|
|
});
|
|
await insertConfigAuditLog(client, {
|
|
entityType: 'pair_strategy_config',
|
|
entityId: resolvedPairId,
|
|
action: 'initial_version_created',
|
|
oldValue: null,
|
|
newValue: strategyConfig,
|
|
changedBy,
|
|
reason,
|
|
});
|
|
}
|
|
|
|
const knownRoute = buildKnownPriceRouteForPair(nextPair);
|
|
if (knownRoute) {
|
|
await upsertSeedPriceRoute(client, {
|
|
route: knownRoute,
|
|
now: new Date().toISOString(),
|
|
});
|
|
}
|
|
}
|
|
|
|
await insertConfigAuditLog(client, {
|
|
entityType: 'trading_pair',
|
|
entityId: resolvedPairId,
|
|
action: 'mode_set',
|
|
oldValue: existingPair,
|
|
newValue: nextPair,
|
|
changedBy,
|
|
reason,
|
|
});
|
|
return {
|
|
...nextPair,
|
|
strategyConfig,
|
|
};
|
|
});
|
|
}
|
|
|
|
export async function pauseTradingPair(pool, {
|
|
pairId = null,
|
|
pair = null,
|
|
changedBy = 'operator',
|
|
reason = 'operator paused pair',
|
|
} = {}) {
|
|
await ensureTradingConfigSchema(pool);
|
|
const resolvedPairId = pairId || pair;
|
|
if (!resolvedPairId) throw new Error('pair_id is required');
|
|
|
|
return withTransaction(pool, async (client) => {
|
|
const existingResult = await client.query(
|
|
`
|
|
SELECT *
|
|
FROM ${TRADING_PAIRS_TABLE}
|
|
WHERE pair_id = $1
|
|
LIMIT 1
|
|
`,
|
|
[resolvedPairId],
|
|
);
|
|
const existingPair = existingResult.rows[0] ? normalizeTradingPairRow(existingResult.rows[0]) : null;
|
|
if (!existingPair) throw new Error(`trading pair not found: ${resolvedPairId}`);
|
|
|
|
const nextPair = {
|
|
pairId: existingPair.pairId,
|
|
venue: existingPair.venue,
|
|
assetIn: existingPair.assetIn,
|
|
assetOut: existingPair.assetOut,
|
|
mode: existingPair.mode,
|
|
enabled: false,
|
|
status: 'disabled',
|
|
};
|
|
await upsertSeedPair(client, { pair: nextPair, now: new Date().toISOString() });
|
|
await insertConfigAuditLog(client, {
|
|
entityType: 'trading_pair',
|
|
entityId: resolvedPairId,
|
|
action: 'paused',
|
|
oldValue: existingPair,
|
|
newValue: nextPair,
|
|
changedBy,
|
|
reason,
|
|
});
|
|
return nextPair;
|
|
});
|
|
}
|
|
|
|
function buildTradingConfigSnapshot({
|
|
assetRows,
|
|
pairRows,
|
|
strategyRows,
|
|
routeRows,
|
|
latestImportRun,
|
|
}) {
|
|
const loadedAt = new Date().toISOString();
|
|
const assets = assetRows.map(normalizeTradingAssetRow);
|
|
const assetRegistry = new Map(assets.map((asset) => [asset.assetId, asset]));
|
|
const strategyByPairId = new Map(
|
|
strategyRows.map((row) => [row.pair_id, normalizeStrategyConfigRow(row)]),
|
|
);
|
|
const routeByPairId = new Map();
|
|
for (const row of routeRows) {
|
|
if (!routeByPairId.has(row.pair_id)) routeByPairId.set(row.pair_id, normalizePriceRouteRow(row));
|
|
}
|
|
|
|
const pairs = pairRows.map((row) => {
|
|
const pair = normalizeTradingPairRow(row);
|
|
const assetIn = assetRegistry.get(pair.assetIn) || null;
|
|
const assetOut = assetRegistry.get(pair.assetOut) || null;
|
|
const strategyConfig = strategyByPairId.get(pair.pairId) || null;
|
|
const priceRoute = routeByPairId.get(pair.pairId) || null;
|
|
const blockReasons = [];
|
|
if (!assetIn) blockReasons.push('asset_in_missing');
|
|
if (!assetOut) blockReasons.push('asset_out_missing');
|
|
if (assetIn && !Number.isInteger(assetIn.decimals)) blockReasons.push('asset_in_decimals_missing');
|
|
if (assetOut && !Number.isInteger(assetOut.decimals)) blockReasons.push('asset_out_decimals_missing');
|
|
if ((pairCanMake(pair) || pairCanTake(pair)) && !strategyConfig) {
|
|
blockReasons.push('pair_strategy_config_missing');
|
|
}
|
|
if ((pairCanMake(pair) || pairCanTake(pair)) && !priceRoute) {
|
|
blockReasons.push('price_route_missing');
|
|
}
|
|
if (strategyConfig && (!Number.isInteger(strategyConfig.edgeBps) || strategyConfig.edgeBps <= 0)) {
|
|
blockReasons.push('edge_bps_invalid');
|
|
}
|
|
if (strategyConfig && !(Number(strategyConfig.maxNotional) > 0)) {
|
|
blockReasons.push('max_notional_invalid');
|
|
}
|
|
|
|
const observeEnabled = pairCanObserve(pair);
|
|
const makerEnabled = pairCanMake(pair);
|
|
const takerEnabled = pairCanTake(pair);
|
|
const canTrade = (makerEnabled || takerEnabled) && blockReasons.length === 0;
|
|
return {
|
|
...pair,
|
|
key: pair.pairId,
|
|
assetIn,
|
|
assetOut,
|
|
asset_in: pair.assetIn,
|
|
asset_out: pair.assetOut,
|
|
asset_in_symbol: assetIn?.symbol || pair.assetIn,
|
|
asset_out_symbol: assetOut?.symbol || pair.assetOut,
|
|
strategyConfig,
|
|
priceRoute,
|
|
observeEnabled,
|
|
makerEnabled,
|
|
takerEnabled,
|
|
canTrade,
|
|
blockReason: blockReasons[0] || null,
|
|
blockReasons,
|
|
};
|
|
});
|
|
|
|
const observedPairs = pairs.filter((pair) => pair.observeEnabled);
|
|
const enabledPairKeys = new Set(observedPairs.map((pair) => pair.key));
|
|
const makerPairKeys = new Set(pairs.filter((pair) => pair.makerEnabled).map((pair) => pair.key));
|
|
const takerPairKeys = new Set(pairs.filter((pair) => pair.takerEnabled).map((pair) => pair.key));
|
|
const pairByKey = new Map(pairs.map((pair) => [pair.key, pair]));
|
|
const pairById = new Map(pairs.map((pair) => [pair.pairId, pair]));
|
|
const pairStrategyByPairKey = new Map(
|
|
pairs
|
|
.filter((pair) => pair.strategyConfig)
|
|
.map((pair) => [pair.key, pair.strategyConfig]),
|
|
);
|
|
const pairPriceRouteByPairKey = new Map(
|
|
pairs
|
|
.filter((pair) => pair.priceRoute)
|
|
.map((pair) => [pair.key, pair.priceRoute]),
|
|
);
|
|
const trackedAssets = assets.filter((asset) => asset.enabledForInventory);
|
|
const currentBtc = assetRegistry.get('nep141:nbtc.bridge.near') || trackedAssets.find((asset) => asset.symbol === 'BTC') || null;
|
|
const legacyBtc = assetRegistry.get('nep141:btc.omft.near') || null;
|
|
const currentEure = assetRegistry.get('nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near')
|
|
|| trackedAssets.find((asset) => asset.symbol === 'EURe')
|
|
|| null;
|
|
const preferredActivePair = pairByKey.get('nep141:nbtc.bridge.near->nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near')
|
|
|| observedPairs[0]
|
|
|| null;
|
|
const defaultTakerPair = pairByKey.get('nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near->nep141:nbtc.bridge.near')
|
|
|| pairs.find((pair) => pair.takerEnabled && pair.canTrade)
|
|
|| null;
|
|
const activeAssetIds = preferredActivePair?.assetIn && preferredActivePair?.assetOut
|
|
? [preferredActivePair.assetIn.assetId, preferredActivePair.assetOut.assetId]
|
|
: [];
|
|
const blockReason = observedPairs.length === 0
|
|
? 'no_enabled_pairs'
|
|
: trackedAssets.length === 0
|
|
? 'no_inventory_tracked_assets'
|
|
: null;
|
|
|
|
return {
|
|
ok: blockReason == null,
|
|
source: 'postgres',
|
|
loadedAt,
|
|
blockReason,
|
|
latestImportRun: normalizeAssetImportRunRow(latestImportRun),
|
|
assets,
|
|
assetRegistry,
|
|
trackedAssets,
|
|
trackedAssetIds: trackedAssets.map((asset) => asset.assetId),
|
|
tradingBtc: currentBtc,
|
|
tradingBtcAssets: [currentBtc, legacyBtc].filter(Boolean),
|
|
tradingEure: currentEure,
|
|
activePair: preferredActivePair?.key || null,
|
|
activeAssetIds,
|
|
pairs,
|
|
observedPairs,
|
|
enabledPairKeys,
|
|
makerPairKeys,
|
|
takerPairKeys,
|
|
pairByKey,
|
|
pairById,
|
|
pairStrategyByPairKey,
|
|
pairPriceRouteByPairKey,
|
|
defaultTakerPair,
|
|
tradingConfigLoaded: true,
|
|
requireDbTradingConfig: true,
|
|
};
|
|
}
|
|
|
|
function buildFailClosedTradingConfig(error) {
|
|
return {
|
|
ok: false,
|
|
source: 'postgres',
|
|
loadedAt: new Date().toISOString(),
|
|
blockReason: 'trading_config_unavailable',
|
|
error: error?.message || 'trading config unavailable',
|
|
latestImportRun: null,
|
|
assets: [],
|
|
assetRegistry: new Map(),
|
|
trackedAssets: [],
|
|
trackedAssetIds: [],
|
|
tradingBtc: null,
|
|
tradingBtcAssets: [],
|
|
tradingEure: null,
|
|
activePair: null,
|
|
activeAssetIds: [],
|
|
pairs: [],
|
|
observedPairs: [],
|
|
enabledPairKeys: new Set(),
|
|
makerPairKeys: new Set(),
|
|
takerPairKeys: new Set(),
|
|
pairByKey: new Map(),
|
|
pairById: new Map(),
|
|
pairStrategyByPairKey: new Map(),
|
|
pairPriceRouteByPairKey: new Map(),
|
|
defaultTakerPair: null,
|
|
tradingConfigLoaded: false,
|
|
requireDbTradingConfig: true,
|
|
};
|
|
}
|
|
|
|
export function summarizeTradingConfigSnapshot(snapshot) {
|
|
return {
|
|
ok: snapshot.ok,
|
|
source: snapshot.source,
|
|
loaded_at: snapshot.loadedAt,
|
|
block_reason: snapshot.blockReason || null,
|
|
error: snapshot.error || null,
|
|
latest_import: snapshot.latestImportRun || null,
|
|
asset_count: snapshot.assets?.length || 0,
|
|
tracked_asset_count: snapshot.trackedAssets?.length || 0,
|
|
enabled_pair_count: snapshot.observedPairs?.length || 0,
|
|
active_pair: snapshot.activePair || null,
|
|
pairs: (snapshot.pairs || []).map((pair) => ({
|
|
pair_id: pair.pairId,
|
|
pair: pair.key,
|
|
mode: pair.mode,
|
|
status: pair.status,
|
|
enabled: pair.enabled,
|
|
can_trade: pair.canTrade,
|
|
block_reason: pair.blockReason,
|
|
strategy_config_id: pair.strategyConfig?.configId || null,
|
|
strategy_config_version: pair.strategyConfig?.version || null,
|
|
edge_bps: pair.strategyConfig?.edgeBps ?? null,
|
|
max_notional: pair.strategyConfig?.maxNotional ?? null,
|
|
price_route_id: pair.priceRoute?.routeId || null,
|
|
})),
|
|
};
|
|
}
|
|
|
|
function normalizeTradingAssetRow(row) {
|
|
return {
|
|
assetId: row.asset_id,
|
|
asset_id: row.asset_id,
|
|
venue: row.venue,
|
|
symbol: row.symbol,
|
|
label: row.label || row.symbol,
|
|
decimals: Number(row.decimals),
|
|
blockchain: row.blockchain || null,
|
|
chain: row.chain || row.blockchain || null,
|
|
contractAddress: row.contract_address || null,
|
|
contract_address: row.contract_address || null,
|
|
latestPrice: row.latest_price == null ? null : String(row.latest_price),
|
|
latest_price: row.latest_price == null ? null : String(row.latest_price),
|
|
priceUpdatedAt: toIsoTimestamp(row.price_updated_at),
|
|
price_updated_at: toIsoTimestamp(row.price_updated_at),
|
|
supported: row.supported === true,
|
|
retiredAt: toIsoTimestamp(row.retired_at),
|
|
retired_at: toIsoTimestamp(row.retired_at),
|
|
enabledForInventory: row.enabled_for_inventory === true,
|
|
enabled_for_inventory: row.enabled_for_inventory === true,
|
|
role: row.role || null,
|
|
withdrawAddress: row.withdraw_address || '',
|
|
withdraw_address: row.withdraw_address || '',
|
|
rawPayload: row.raw_payload || {},
|
|
raw_payload: row.raw_payload || {},
|
|
updated_at: toIsoTimestamp(row.updated_at),
|
|
};
|
|
}
|
|
|
|
function normalizeAssetCatalogSummaryRow(row) {
|
|
return {
|
|
assetId: row.asset_id,
|
|
asset_id: row.asset_id,
|
|
venue: row.venue,
|
|
symbol: row.symbol,
|
|
label: row.label || row.symbol,
|
|
decimals: Number(row.decimals),
|
|
blockchain: row.blockchain || null,
|
|
chain: row.chain || row.blockchain || null,
|
|
contractAddress: row.contract_address || null,
|
|
contract_address: row.contract_address || null,
|
|
latestPrice: row.latest_price == null ? null : String(row.latest_price),
|
|
latest_price: row.latest_price == null ? null : String(row.latest_price),
|
|
priceUpdatedAt: toIsoTimestamp(row.price_updated_at),
|
|
price_updated_at: toIsoTimestamp(row.price_updated_at),
|
|
supported: row.supported === true,
|
|
retiredAt: toIsoTimestamp(row.retired_at),
|
|
retired_at: toIsoTimestamp(row.retired_at),
|
|
enabledForInventory: row.enabled_for_inventory === true,
|
|
enabled_for_inventory: row.enabled_for_inventory === true,
|
|
role: row.role || null,
|
|
withdrawAddress: row.withdraw_address || '',
|
|
withdraw_address: row.withdraw_address || '',
|
|
rawPayloadAvailable: row.raw_payload_available === true,
|
|
raw_payload_available: row.raw_payload_available === true,
|
|
updated_at: toIsoTimestamp(row.updated_at),
|
|
};
|
|
}
|
|
|
|
function normalizeTradingPairRow(row) {
|
|
return {
|
|
pairId: row.pair_id,
|
|
pair_id: row.pair_id,
|
|
venue: row.venue,
|
|
assetIn: row.asset_in,
|
|
assetOut: row.asset_out,
|
|
mode: row.mode,
|
|
enabled: row.enabled === true,
|
|
status: row.status,
|
|
created_at: toIsoTimestamp(row.created_at),
|
|
updated_at: toIsoTimestamp(row.updated_at),
|
|
};
|
|
}
|
|
|
|
function splitPairId(pairId) {
|
|
const parts = String(pairId || '').split('->');
|
|
if (parts.length !== 2 || !parts[0] || !parts[1]) return [null, null];
|
|
return parts;
|
|
}
|
|
|
|
function buildInitialPairStrategyConfig(pairId, {
|
|
edgeBps = null,
|
|
maxNotional = null,
|
|
minNotional = null,
|
|
slippageBps = null,
|
|
minDeadlineMs = null,
|
|
priceMaxAgeMs = null,
|
|
inventoryMaxAgeMs = null,
|
|
requestDefaultNotional = undefined,
|
|
requestMaxNotional = undefined,
|
|
requestMaxSlippageBps = undefined,
|
|
changedBy = 'operator',
|
|
reason = 'operator pair strategy config initialization',
|
|
} = {}) {
|
|
const baseConfig = buildSeedStrategyConfig(pairId, {
|
|
createdBy: changedBy,
|
|
reason,
|
|
});
|
|
|
|
return {
|
|
...baseConfig,
|
|
edgeBps: positiveIntegerOrDefault(edgeBps, baseConfig.edgeBps, 'edge_bps'),
|
|
maxNotional: positiveNumberStringOrDefault(maxNotional, baseConfig.maxNotional, 'max_notional'),
|
|
minNotional: nonNegativeNumberStringOrDefault(minNotional, baseConfig.minNotional, 'min_notional'),
|
|
slippageBps: nonNegativeIntegerOrDefault(slippageBps, baseConfig.slippageBps, 'slippage_bps'),
|
|
minDeadlineMs: positiveIntegerOrDefault(minDeadlineMs, baseConfig.minDeadlineMs, 'min_deadline_ms'),
|
|
priceMaxAgeMs: positiveIntegerOrDefault(priceMaxAgeMs, baseConfig.priceMaxAgeMs, 'price_max_age_ms'),
|
|
inventoryMaxAgeMs:
|
|
positiveIntegerOrDefault(inventoryMaxAgeMs, baseConfig.inventoryMaxAgeMs, 'inventory_max_age_ms'),
|
|
requestDefaultNotional:
|
|
nullablePositiveNumberStringOrDefault(
|
|
requestDefaultNotional,
|
|
baseConfig.requestDefaultNotional,
|
|
'request_default_notional',
|
|
),
|
|
requestMaxNotional:
|
|
nullablePositiveNumberStringOrDefault(
|
|
requestMaxNotional,
|
|
baseConfig.requestMaxNotional,
|
|
'request_max_notional',
|
|
),
|
|
requestMaxSlippageBps:
|
|
nullableNonNegativeIntegerOrDefault(
|
|
requestMaxSlippageBps,
|
|
baseConfig.requestMaxSlippageBps,
|
|
'request_max_slippage_bps',
|
|
),
|
|
};
|
|
}
|
|
|
|
function hasConfigOverride(value) {
|
|
return value != null && String(value).trim() !== '';
|
|
}
|
|
|
|
function positiveIntegerOrDefault(value, fallback, field) {
|
|
if (!hasConfigOverride(value)) return Number(fallback);
|
|
const next = Number(value);
|
|
if (!Number.isInteger(next) || next <= 0) throw new Error(`${field} must be a positive integer`);
|
|
return next;
|
|
}
|
|
|
|
function nonNegativeIntegerOrDefault(value, fallback, field) {
|
|
if (!hasConfigOverride(value)) return Number(fallback);
|
|
const next = Number(value);
|
|
if (!Number.isInteger(next) || next < 0) throw new Error(`${field} must be a non-negative integer`);
|
|
return next;
|
|
}
|
|
|
|
function positiveNumberStringOrDefault(value, fallback, field) {
|
|
if (!hasConfigOverride(value)) return String(fallback);
|
|
const next = String(value).trim();
|
|
if (!(Number(next) > 0)) throw new Error(`${field} must be greater than zero`);
|
|
return next;
|
|
}
|
|
|
|
function nonNegativeNumberStringOrDefault(value, fallback, field) {
|
|
if (!hasConfigOverride(value)) return String(fallback);
|
|
const next = String(value).trim();
|
|
if (!(Number(next) >= 0)) throw new Error(`${field} must be zero or greater`);
|
|
return next;
|
|
}
|
|
|
|
function nullablePositiveNumberStringOrDefault(value, fallback, field) {
|
|
if (value === undefined) return fallback == null ? null : positiveNumberStringOrDefault(fallback, '1', field);
|
|
return nullablePositiveNumberString(value, field);
|
|
}
|
|
|
|
function nullablePositiveNumberString(value, field) {
|
|
if (!hasConfigOverride(value)) return null;
|
|
return positiveNumberStringOrDefault(value, '1', field);
|
|
}
|
|
|
|
function nullableNonNegativeIntegerOrDefault(value, fallback, field) {
|
|
if (value === undefined) return fallback == null ? null : nonNegativeIntegerOrDefault(fallback, 0, field);
|
|
return nullableNonNegativeInteger(value, field);
|
|
}
|
|
|
|
function nullableNonNegativeInteger(value, field) {
|
|
if (!hasConfigOverride(value)) return null;
|
|
return nonNegativeIntegerOrDefault(value, 0, field);
|
|
}
|
|
|
|
function normalizeStrategyConfigRow(row) {
|
|
if (!row) return null;
|
|
return {
|
|
configId: row.config_id,
|
|
config_id: row.config_id,
|
|
pairId: row.pair_id,
|
|
pair_id: row.pair_id,
|
|
version: Number(row.version),
|
|
active: row.active === true,
|
|
edgeBps: Number(row.edge_bps),
|
|
edge_bps: Number(row.edge_bps),
|
|
maxNotional: String(row.max_notional),
|
|
max_notional: String(row.max_notional),
|
|
minNotional: String(row.min_notional ?? '0'),
|
|
min_notional: String(row.min_notional ?? '0'),
|
|
slippageBps: Number(row.slippage_bps ?? 0),
|
|
slippage_bps: Number(row.slippage_bps ?? 0),
|
|
minDeadlineMs: Number(row.min_deadline_ms),
|
|
min_deadline_ms: Number(row.min_deadline_ms),
|
|
priceMaxAgeMs: Number(row.price_max_age_ms),
|
|
price_max_age_ms: Number(row.price_max_age_ms),
|
|
inventoryMaxAgeMs: Number(row.inventory_max_age_ms),
|
|
inventory_max_age_ms: Number(row.inventory_max_age_ms),
|
|
requestDefaultNotional:
|
|
row.request_default_notional == null ? null : String(row.request_default_notional),
|
|
request_default_notional:
|
|
row.request_default_notional == null ? null : String(row.request_default_notional),
|
|
requestMaxNotional:
|
|
row.request_max_notional == null ? null : String(row.request_max_notional),
|
|
request_max_notional:
|
|
row.request_max_notional == null ? null : String(row.request_max_notional),
|
|
requestMaxSlippageBps:
|
|
row.request_max_slippage_bps == null ? null : Number(row.request_max_slippage_bps),
|
|
request_max_slippage_bps:
|
|
row.request_max_slippage_bps == null ? null : Number(row.request_max_slippage_bps),
|
|
created_at: toIsoTimestamp(row.created_at),
|
|
created_by: row.created_by || null,
|
|
reason: row.reason || null,
|
|
};
|
|
}
|
|
|
|
function normalizePriceRouteRow(row) {
|
|
if (!row) return null;
|
|
return {
|
|
routeId: row.route_id,
|
|
route_id: row.route_id,
|
|
pairId: row.pair_id,
|
|
pair_id: row.pair_id,
|
|
source: row.source,
|
|
baseAssetId: row.base_asset_id,
|
|
base_asset_id: row.base_asset_id,
|
|
quoteAssetId: row.quote_asset_id,
|
|
quote_asset_id: row.quote_asset_id,
|
|
routeConfig: row.route_config || {},
|
|
route_config: row.route_config || {},
|
|
maxAgeMs: Number(row.max_age_ms),
|
|
max_age_ms: Number(row.max_age_ms),
|
|
enabled: row.enabled === true,
|
|
};
|
|
}
|
|
|
|
function normalizeAssetImportRunRow(row) {
|
|
if (!row) return null;
|
|
return {
|
|
run_id: row.run_id,
|
|
source_url: row.source_url,
|
|
fetched_at: toIsoTimestamp(row.fetched_at),
|
|
status: row.status,
|
|
token_count: Number(row.token_count || 0),
|
|
added_count: Number(row.added_count || 0),
|
|
updated_count: Number(row.updated_count || 0),
|
|
unchanged_count: Number(row.unchanged_count || 0),
|
|
retired_count: Number(row.retired_count || 0),
|
|
raw_response_hash: row.raw_response_hash || null,
|
|
error: row.error || null,
|
|
};
|
|
}
|
|
|
|
function publicAssetImportRunSummary(run) {
|
|
const { raw_response: _rawResponse, ...publicRun } = run;
|
|
return publicRun;
|
|
}
|
|
|
|
async function seedKnownEnabledPairRuntimeConfig(pool, { now }) {
|
|
const pairResult = await pool.query(`SELECT * FROM ${TRADING_PAIRS_TABLE}`);
|
|
for (const row of pairResult.rows) {
|
|
const pair = normalizeTradingPairRow(row);
|
|
if (!pairCanMake(pair) && !pairCanTake(pair)) continue;
|
|
|
|
await enableInventoryForAssets(pool, {
|
|
assetIds: [pair.assetIn, pair.assetOut],
|
|
now,
|
|
});
|
|
|
|
const route = buildKnownPriceRouteForPair(pair);
|
|
if (route) await upsertSeedPriceRoute(pool, { route, now });
|
|
}
|
|
}
|
|
|
|
function buildKnownPriceRouteForPair(pair) {
|
|
const assets = new Set([pair?.assetIn, pair?.assetOut]);
|
|
if (assets.has(CURRENT_NBTC_ASSET_ID) && assets.has(CURRENT_USDC_ASSET_ID)) {
|
|
return buildBtcUsdcPriceRoute(pair.pairId);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function enableInventoryForAssets(pool, { assetIds, now }) {
|
|
const uniqueAssetIds = [...new Set(assetIds.filter(Boolean))];
|
|
if (!uniqueAssetIds.length) return;
|
|
|
|
await pool.query(
|
|
`
|
|
UPDATE ${TRADING_ASSETS_TABLE}
|
|
SET enabled_for_inventory = true,
|
|
updated_at = $2
|
|
WHERE asset_id = ANY($1::text[])
|
|
`,
|
|
[uniqueAssetIds, now],
|
|
);
|
|
}
|
|
|
|
async function upsertSeedAsset(pool, { asset, now }) {
|
|
await pool.query(
|
|
`
|
|
INSERT INTO ${TRADING_ASSETS_TABLE} (
|
|
asset_id,
|
|
venue,
|
|
symbol,
|
|
label,
|
|
decimals,
|
|
blockchain,
|
|
chain,
|
|
contract_address,
|
|
latest_price,
|
|
price_updated_at,
|
|
supported,
|
|
retired_at,
|
|
enabled_for_inventory,
|
|
role,
|
|
withdraw_address,
|
|
raw_payload,
|
|
last_supported_at,
|
|
updated_at
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,NULL,$12,$13,$14,$15::jsonb,$16,$16)
|
|
ON CONFLICT (asset_id) DO UPDATE SET
|
|
venue = EXCLUDED.venue,
|
|
symbol = EXCLUDED.symbol,
|
|
label = EXCLUDED.label,
|
|
decimals = EXCLUDED.decimals,
|
|
blockchain = EXCLUDED.blockchain,
|
|
chain = EXCLUDED.chain,
|
|
contract_address = COALESCE(${TRADING_ASSETS_TABLE}.contract_address, EXCLUDED.contract_address),
|
|
supported = ${TRADING_ASSETS_TABLE}.supported OR EXCLUDED.supported,
|
|
enabled_for_inventory = ${TRADING_ASSETS_TABLE}.enabled_for_inventory OR EXCLUDED.enabled_for_inventory,
|
|
role = COALESCE(${TRADING_ASSETS_TABLE}.role, EXCLUDED.role),
|
|
withdraw_address = COALESCE(NULLIF(${TRADING_ASSETS_TABLE}.withdraw_address, ''), EXCLUDED.withdraw_address),
|
|
raw_payload = CASE
|
|
WHEN ${TRADING_ASSETS_TABLE}.raw_payload = '{}'::jsonb THEN EXCLUDED.raw_payload
|
|
ELSE ${TRADING_ASSETS_TABLE}.raw_payload
|
|
END,
|
|
last_supported_at = COALESCE(${TRADING_ASSETS_TABLE}.last_supported_at, EXCLUDED.last_supported_at),
|
|
updated_at = EXCLUDED.updated_at
|
|
`,
|
|
[
|
|
asset.assetId,
|
|
asset.venue,
|
|
asset.symbol,
|
|
asset.label,
|
|
asset.decimals,
|
|
asset.blockchain,
|
|
asset.chain || asset.blockchain,
|
|
asset.contractAddress,
|
|
asset.latestPrice,
|
|
asset.priceUpdatedAt,
|
|
asset.supported,
|
|
asset.enabledForInventory,
|
|
asset.role,
|
|
asset.withdrawAddress || '',
|
|
JSON.stringify(asset.rawPayload || {}),
|
|
now,
|
|
],
|
|
);
|
|
}
|
|
|
|
async function upsertSeedPair(pool, { pair, now, preserveRuntimeState = false }) {
|
|
const conflictUpdate = preserveRuntimeState
|
|
? `
|
|
venue = EXCLUDED.venue,
|
|
asset_in = EXCLUDED.asset_in,
|
|
asset_out = EXCLUDED.asset_out,
|
|
mode = ${TRADING_PAIRS_TABLE}.mode,
|
|
enabled = ${TRADING_PAIRS_TABLE}.enabled,
|
|
status = ${TRADING_PAIRS_TABLE}.status,
|
|
updated_at = EXCLUDED.updated_at
|
|
`
|
|
: `
|
|
venue = EXCLUDED.venue,
|
|
asset_in = EXCLUDED.asset_in,
|
|
asset_out = EXCLUDED.asset_out,
|
|
mode = EXCLUDED.mode,
|
|
enabled = EXCLUDED.enabled,
|
|
status = EXCLUDED.status,
|
|
updated_at = EXCLUDED.updated_at
|
|
`;
|
|
await pool.query(
|
|
`
|
|
INSERT INTO ${TRADING_PAIRS_TABLE} (
|
|
pair_id,
|
|
venue,
|
|
asset_in,
|
|
asset_out,
|
|
mode,
|
|
enabled,
|
|
status,
|
|
created_at,
|
|
updated_at
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$8)
|
|
ON CONFLICT (pair_id) DO UPDATE SET
|
|
${conflictUpdate}
|
|
`,
|
|
[
|
|
pair.pairId,
|
|
pair.venue,
|
|
pair.assetIn,
|
|
pair.assetOut,
|
|
pair.mode,
|
|
pair.enabled,
|
|
pair.status,
|
|
now,
|
|
],
|
|
);
|
|
}
|
|
|
|
async function upsertSeedStrategyConfig(pool, { config }) {
|
|
await insertPairStrategyConfig(pool, { config, active: config.active !== false });
|
|
}
|
|
|
|
async function insertPairStrategyConfig(pool, { config, active = true }) {
|
|
await pool.query(
|
|
`
|
|
INSERT INTO ${PAIR_STRATEGY_CONFIGS_TABLE} (
|
|
config_id,
|
|
pair_id,
|
|
version,
|
|
active,
|
|
edge_bps,
|
|
max_notional,
|
|
min_notional,
|
|
slippage_bps,
|
|
min_deadline_ms,
|
|
price_max_age_ms,
|
|
inventory_max_age_ms,
|
|
request_default_notional,
|
|
request_max_notional,
|
|
request_max_slippage_bps,
|
|
created_by,
|
|
reason
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
|
|
ON CONFLICT (config_id) DO NOTHING
|
|
`,
|
|
[
|
|
config.configId,
|
|
config.pairId,
|
|
config.version,
|
|
active,
|
|
config.edgeBps,
|
|
config.maxNotional,
|
|
config.minNotional,
|
|
config.slippageBps,
|
|
config.minDeadlineMs,
|
|
config.priceMaxAgeMs,
|
|
config.inventoryMaxAgeMs,
|
|
config.requestDefaultNotional,
|
|
config.requestMaxNotional,
|
|
config.requestMaxSlippageBps,
|
|
config.createdBy,
|
|
config.reason,
|
|
],
|
|
);
|
|
}
|
|
|
|
async function upsertSeedPriceRoute(pool, { route, now }) {
|
|
await pool.query(
|
|
`
|
|
INSERT INTO ${PAIR_PRICE_ROUTES_TABLE} (
|
|
route_id,
|
|
pair_id,
|
|
source,
|
|
base_asset_id,
|
|
quote_asset_id,
|
|
route_config,
|
|
max_age_ms,
|
|
enabled,
|
|
created_at,
|
|
updated_at
|
|
) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9,$9)
|
|
ON CONFLICT (route_id) DO UPDATE SET
|
|
source = EXCLUDED.source,
|
|
base_asset_id = EXCLUDED.base_asset_id,
|
|
quote_asset_id = EXCLUDED.quote_asset_id,
|
|
route_config = EXCLUDED.route_config,
|
|
max_age_ms = EXCLUDED.max_age_ms,
|
|
enabled = ${PAIR_PRICE_ROUTES_TABLE}.enabled,
|
|
updated_at = EXCLUDED.updated_at
|
|
`,
|
|
[
|
|
route.routeId,
|
|
route.pairId,
|
|
route.source,
|
|
route.baseAssetId,
|
|
route.quoteAssetId,
|
|
JSON.stringify(route.routeConfig || {}),
|
|
route.maxAgeMs,
|
|
route.enabled,
|
|
now,
|
|
],
|
|
);
|
|
}
|
|
|
|
async function loadTradingAssetsById(pool) {
|
|
const result = await pool.query(`SELECT * FROM ${TRADING_ASSETS_TABLE}`);
|
|
return new Map(result.rows.map((row) => [row.asset_id, normalizeTradingAssetRow(row)]));
|
|
}
|
|
|
|
async function upsertImportedAsset(pool, { asset, fetchedAt }) {
|
|
await pool.query(
|
|
`
|
|
INSERT INTO ${TRADING_ASSETS_TABLE} (
|
|
asset_id,
|
|
venue,
|
|
symbol,
|
|
label,
|
|
decimals,
|
|
blockchain,
|
|
chain,
|
|
contract_address,
|
|
latest_price,
|
|
price_updated_at,
|
|
supported,
|
|
retired_at,
|
|
enabled_for_inventory,
|
|
raw_payload,
|
|
last_supported_at,
|
|
updated_at
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,true,NULL,false,$11::jsonb,$12,$12)
|
|
ON CONFLICT (asset_id) DO UPDATE SET
|
|
venue = EXCLUDED.venue,
|
|
symbol = EXCLUDED.symbol,
|
|
label = EXCLUDED.label,
|
|
decimals = EXCLUDED.decimals,
|
|
blockchain = EXCLUDED.blockchain,
|
|
chain = EXCLUDED.chain,
|
|
contract_address = EXCLUDED.contract_address,
|
|
latest_price = EXCLUDED.latest_price,
|
|
price_updated_at = EXCLUDED.price_updated_at,
|
|
supported = true,
|
|
retired_at = NULL,
|
|
enabled_for_inventory = ${TRADING_ASSETS_TABLE}.enabled_for_inventory,
|
|
raw_payload = EXCLUDED.raw_payload,
|
|
last_supported_at = EXCLUDED.last_supported_at,
|
|
updated_at = EXCLUDED.updated_at
|
|
`,
|
|
[
|
|
asset.assetId,
|
|
asset.venue,
|
|
asset.symbol,
|
|
asset.label,
|
|
asset.decimals,
|
|
asset.blockchain,
|
|
asset.chain || asset.blockchain,
|
|
asset.contractAddress,
|
|
asset.latestPrice,
|
|
asset.priceUpdatedAt,
|
|
JSON.stringify(asset.rawPayload || {}),
|
|
fetchedAt,
|
|
],
|
|
);
|
|
}
|
|
|
|
async function insertAssetImportRun(pool, run) {
|
|
await pool.query(
|
|
`
|
|
INSERT INTO ${SUPPORTED_ASSET_IMPORT_RUNS_TABLE} (
|
|
run_id,
|
|
source_url,
|
|
fetched_at,
|
|
status,
|
|
token_count,
|
|
added_count,
|
|
updated_count,
|
|
unchanged_count,
|
|
retired_count,
|
|
raw_response_hash,
|
|
error,
|
|
raw_response
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12::jsonb)
|
|
ON CONFLICT (run_id) DO UPDATE SET
|
|
status = EXCLUDED.status,
|
|
token_count = EXCLUDED.token_count,
|
|
added_count = EXCLUDED.added_count,
|
|
updated_count = EXCLUDED.updated_count,
|
|
unchanged_count = EXCLUDED.unchanged_count,
|
|
retired_count = EXCLUDED.retired_count,
|
|
raw_response_hash = EXCLUDED.raw_response_hash,
|
|
error = EXCLUDED.error,
|
|
raw_response = EXCLUDED.raw_response
|
|
`,
|
|
[
|
|
run.run_id,
|
|
run.source_url,
|
|
run.fetched_at,
|
|
run.status,
|
|
run.token_count,
|
|
run.added_count,
|
|
run.updated_count,
|
|
run.unchanged_count,
|
|
run.retired_count,
|
|
run.raw_response_hash,
|
|
run.error,
|
|
run.raw_response == null ? null : JSON.stringify(run.raw_response),
|
|
],
|
|
);
|
|
}
|
|
|
|
async function insertConfigAuditLog(pool, {
|
|
entityType,
|
|
entityId,
|
|
action,
|
|
oldValue = null,
|
|
newValue = null,
|
|
changedBy,
|
|
reason,
|
|
}) {
|
|
await pool.query(
|
|
`
|
|
INSERT INTO ${PAIR_CONFIG_AUDIT_LOG_TABLE} (
|
|
audit_id,
|
|
entity_type,
|
|
entity_id,
|
|
action,
|
|
old_value,
|
|
new_value,
|
|
changed_by,
|
|
reason
|
|
) VALUES ($1,$2,$3,$4,$5::jsonb,$6::jsonb,$7,$8)
|
|
`,
|
|
[
|
|
`audit:${Date.now()}:${Math.random().toString(16).slice(2)}`,
|
|
entityType,
|
|
entityId,
|
|
action,
|
|
oldValue == null ? null : JSON.stringify(oldValue),
|
|
newValue == null ? null : JSON.stringify(newValue),
|
|
changedBy,
|
|
reason,
|
|
],
|
|
);
|
|
}
|
|
|
|
function importedAssetChanged(previous, next) {
|
|
return (
|
|
previous.symbol !== next.symbol
|
|
|| previous.label !== next.label
|
|
|| previous.decimals !== next.decimals
|
|
|| previous.blockchain !== next.blockchain
|
|
|| previous.contractAddress !== next.contractAddress
|
|
|| previous.latestPrice !== next.latestPrice
|
|
|| previous.priceUpdatedAt !== next.priceUpdatedAt
|
|
|| previous.supported !== true
|
|
|| previous.retiredAt != null
|
|
);
|
|
}
|
|
|
|
export async function insertHistoryEvent(pool, { table, topic, event, record }) {
|
|
await pool.query(
|
|
`
|
|
INSERT INTO ${table} (
|
|
event_id,
|
|
topic,
|
|
venue,
|
|
source,
|
|
event_type,
|
|
observed_at,
|
|
ingested_at,
|
|
quote_id,
|
|
pair,
|
|
decision_key,
|
|
payload,
|
|
raw
|
|
) VALUES (
|
|
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12::jsonb
|
|
)
|
|
ON CONFLICT (event_id) DO NOTHING
|
|
`,
|
|
[
|
|
event.event_id,
|
|
topic,
|
|
event.venue,
|
|
event.source,
|
|
event.event_type,
|
|
event.observed_at,
|
|
event.ingested_at,
|
|
record.quote_id,
|
|
record.pair,
|
|
record.decision_key,
|
|
JSON.stringify(event.payload),
|
|
event.raw ? JSON.stringify(event.raw) : null,
|
|
],
|
|
);
|
|
}
|
|
|
|
export async function insertEnvironmentStatusChange(pool, { topic, event, record }) {
|
|
const fingerprint = event.payload?.status_fingerprint || null;
|
|
const environmentKey = event.payload?.environment_key || record.decision_key || null;
|
|
const result = await pool.query(
|
|
`
|
|
WITH latest AS (
|
|
SELECT payload->>'status_fingerprint' AS status_fingerprint
|
|
FROM environment_status_events
|
|
WHERE decision_key = $10
|
|
ORDER BY COALESCE(observed_at, ingested_at) DESC, ingested_at DESC
|
|
LIMIT 1
|
|
)
|
|
INSERT INTO environment_status_events (
|
|
event_id,
|
|
topic,
|
|
venue,
|
|
source,
|
|
event_type,
|
|
observed_at,
|
|
ingested_at,
|
|
quote_id,
|
|
pair,
|
|
decision_key,
|
|
payload,
|
|
raw
|
|
)
|
|
SELECT $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12::jsonb
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM latest WHERE status_fingerprint = $13
|
|
)
|
|
ON CONFLICT (event_id) DO NOTHING
|
|
RETURNING event_id
|
|
`,
|
|
[
|
|
event.event_id,
|
|
topic,
|
|
event.venue,
|
|
event.source,
|
|
event.event_type,
|
|
event.observed_at,
|
|
event.ingested_at,
|
|
record.quote_id,
|
|
record.pair,
|
|
environmentKey,
|
|
JSON.stringify(event.payload),
|
|
event.raw ? JSON.stringify(event.raw) : null,
|
|
fingerprint,
|
|
],
|
|
);
|
|
|
|
return {
|
|
inserted: result.rowCount > 0,
|
|
status_fingerprint: fingerprint,
|
|
environment_key: environmentKey,
|
|
};
|
|
}
|
|
|
|
export async function loadPortfolioMetricInputs(pool, {
|
|
btcAsset = null,
|
|
btcAssets = null,
|
|
eureAsset = null,
|
|
trackedAssets = [],
|
|
} = {}) {
|
|
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
|
|
const [currentInventory, currentPrice, currentPriceEvents, commandAggregate, resultAggregate] = await Promise.all([
|
|
loadLatestEventPayload(pool, 'intent_inventory_snapshots'),
|
|
loadLatestEventPayload(
|
|
pool,
|
|
'market_price_events',
|
|
"WHERE COALESCE(payload->>'eure_per_btc', '') <> '' ORDER BY COALESCE(observed_at, ingested_at) DESC LIMIT 1",
|
|
),
|
|
loadLatestMarketPricePayloadsByRoute(pool),
|
|
pool.query(`
|
|
SELECT
|
|
MIN(ingested_at) AS first_command_at,
|
|
COUNT(*)::INT AS command_count
|
|
FROM execute_trade_commands
|
|
`),
|
|
pool.query(`
|
|
SELECT COUNT(*)::INT AS result_count
|
|
FROM trade_execution_results
|
|
`),
|
|
]);
|
|
|
|
const firstCommandAt = commandAggregate.rows[0]?.first_command_at || null;
|
|
const commandCount = Number(commandAggregate.rows[0]?.command_count || 0);
|
|
const resultCount = Number(resultAggregate.rows[0]?.result_count || 0);
|
|
const valuationAssets = buildCashEquivalentValuationAssets({
|
|
trackedAssets,
|
|
btcAsset: effectiveBtcAssets[0],
|
|
btcAssets: effectiveBtcAssets,
|
|
eureAsset,
|
|
priceEvents: currentPriceEvents.map((entry) => entry.payload),
|
|
});
|
|
|
|
if (!firstCommandAt) {
|
|
return {
|
|
currentInventory,
|
|
currentPrice,
|
|
currentPriceEvents,
|
|
valuationAssets,
|
|
baseline: null,
|
|
commandCount,
|
|
resultCount,
|
|
};
|
|
}
|
|
|
|
const baselineInventory = await loadLatestEventPayload(
|
|
pool,
|
|
'intent_inventory_snapshots',
|
|
'WHERE ingested_at <= $1 ORDER BY ingested_at DESC LIMIT 1',
|
|
[firstCommandAt],
|
|
);
|
|
const baselinePrice = await loadNearestPricePayload(pool, baselineInventory?.ingested_at || firstCommandAt);
|
|
const externalFlows = (
|
|
baselineInventory
|
|
&& baselinePrice
|
|
&& effectiveBtcAssets.length
|
|
&& eureAsset?.assetId
|
|
)
|
|
? await loadExternalAssetFlowsSince(pool, {
|
|
since: firstCommandAt,
|
|
btcAsset: effectiveBtcAssets[0],
|
|
btcAssets: effectiveBtcAssets,
|
|
eureAsset,
|
|
valuationAssets,
|
|
})
|
|
: [];
|
|
|
|
return {
|
|
currentInventory,
|
|
currentPrice,
|
|
currentPriceEvents,
|
|
valuationAssets,
|
|
baseline: baselineInventory && baselinePrice ? {
|
|
anchor: 'latest_inventory_before_first_command',
|
|
command_at: new Date(firstCommandAt).toISOString(),
|
|
inventory: baselineInventory.payload,
|
|
price: baselinePrice.payload,
|
|
} : null,
|
|
externalFlows,
|
|
commandCount,
|
|
resultCount,
|
|
};
|
|
}
|
|
|
|
async function loadLatestMarketPricePayloadsByRoute(pool) {
|
|
const result = await pool.query(`
|
|
SELECT DISTINCT ON (payload->>'price_route_id')
|
|
observed_at,
|
|
ingested_at,
|
|
payload
|
|
FROM market_price_events
|
|
WHERE COALESCE(payload->>'price_route_id', '') <> ''
|
|
ORDER BY payload->>'price_route_id', COALESCE(observed_at, ingested_at) DESC
|
|
`);
|
|
|
|
return result.rows.map((row) => ({
|
|
observed_at: toIsoTimestamp(row.observed_at),
|
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
|
payload: {
|
|
...(row.payload || {}),
|
|
observed_at: row.payload?.observed_at || toIsoTimestamp(row.observed_at),
|
|
ingested_at: row.payload?.ingested_at || toIsoTimestamp(row.ingested_at),
|
|
},
|
|
}));
|
|
}
|
|
|
|
export async function upsertPortfolioMetric(pool, {
|
|
metricId,
|
|
computedAt,
|
|
baselineAnchorAt = null,
|
|
baselineStatus,
|
|
payload,
|
|
}) {
|
|
await pool.query(
|
|
`
|
|
INSERT INTO ${PORTFOLIO_METRICS_TABLE} (
|
|
metric_id,
|
|
computed_at,
|
|
baseline_anchor_at,
|
|
baseline_status,
|
|
payload
|
|
) VALUES ($1, $2, $3, $4, $5::jsonb)
|
|
ON CONFLICT (metric_id) DO UPDATE SET
|
|
computed_at = EXCLUDED.computed_at,
|
|
baseline_anchor_at = EXCLUDED.baseline_anchor_at,
|
|
baseline_status = EXCLUDED.baseline_status,
|
|
payload = EXCLUDED.payload
|
|
`,
|
|
[
|
|
metricId,
|
|
computedAt,
|
|
baselineAnchorAt,
|
|
baselineStatus,
|
|
JSON.stringify(payload),
|
|
],
|
|
);
|
|
}
|
|
|
|
export async function loadLatestPortfolioMetric(pool) {
|
|
const result = await pool.query(`
|
|
SELECT metric_id, computed_at, baseline_anchor_at, baseline_status, payload
|
|
FROM ${PORTFOLIO_METRICS_TABLE}
|
|
ORDER BY computed_at DESC
|
|
LIMIT 1
|
|
`);
|
|
if (!result.rows[0]) return null;
|
|
return normalizePortfolioMetricRow(result.rows[0]);
|
|
}
|
|
|
|
export async function claimNotificationDelivery(pool, {
|
|
notificationKey,
|
|
notificationType,
|
|
sourceKind,
|
|
sourceId = null,
|
|
payload = {},
|
|
}) {
|
|
if (!notificationKey) throw new Error('notificationKey is required');
|
|
if (!notificationType) throw new Error('notificationType is required');
|
|
if (!sourceKind) throw new Error('sourceKind is required');
|
|
|
|
const result = await pool.query(
|
|
`
|
|
INSERT INTO notification_deliveries (
|
|
notification_key,
|
|
notification_type,
|
|
source_kind,
|
|
source_id,
|
|
status,
|
|
attempt_count,
|
|
payload,
|
|
response,
|
|
error
|
|
) VALUES ($1, $2, $3, $4, 'pending', 1, $5::jsonb, NULL, NULL)
|
|
ON CONFLICT (notification_key) DO UPDATE SET
|
|
status = 'pending',
|
|
attempt_count = notification_deliveries.attempt_count + 1,
|
|
last_attempt_at = NOW(),
|
|
payload = EXCLUDED.payload,
|
|
response = NULL,
|
|
error = NULL
|
|
WHERE notification_deliveries.status <> 'delivered'
|
|
RETURNING notification_key
|
|
`,
|
|
[
|
|
notificationKey,
|
|
notificationType,
|
|
sourceKind,
|
|
sourceId,
|
|
JSON.stringify(payload || {}),
|
|
],
|
|
);
|
|
return result.rowCount > 0;
|
|
}
|
|
|
|
export async function finishNotificationDelivery(pool, {
|
|
notificationKey,
|
|
ok,
|
|
response = null,
|
|
error = null,
|
|
}) {
|
|
if (!notificationKey) throw new Error('notificationKey is required');
|
|
await pool.query(
|
|
`
|
|
UPDATE notification_deliveries
|
|
SET
|
|
status = $2,
|
|
delivered_at = CASE WHEN $2 = 'delivered' THEN NOW() ELSE delivered_at END,
|
|
last_attempt_at = NOW(),
|
|
response = $3::jsonb,
|
|
error = $4::jsonb
|
|
WHERE notification_key = $1
|
|
`,
|
|
[
|
|
notificationKey,
|
|
ok ? 'delivered' : 'failed',
|
|
response ? JSON.stringify(response) : null,
|
|
error ? JSON.stringify(error) : null,
|
|
],
|
|
);
|
|
}
|
|
|
|
export async function refreshQuoteOutcomes(pool, {
|
|
btcAsset = null,
|
|
eureAsset = null,
|
|
now = Date.now(),
|
|
submissionLimit = 1000,
|
|
inventoryLimit = 5000,
|
|
} = {}) {
|
|
if (!btcAsset?.assetId || !eureAsset?.assetId) return [];
|
|
|
|
const safeSubmissionLimit = Math.max(1, Number(submissionLimit) || 1000);
|
|
const safeInventoryLimit = Math.max(1, Number(inventoryLimit) || 5000);
|
|
const submissionsResult = await pool.query(
|
|
`
|
|
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
|
FROM (
|
|
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
|
FROM trade_execution_results
|
|
WHERE payload->>'status' = 'submitted'
|
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
|
LIMIT $1
|
|
) recent_submissions
|
|
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
|
`,
|
|
[safeSubmissionLimit],
|
|
);
|
|
if (!submissionsResult.rows.length) return [];
|
|
|
|
const quoteIds = [...new Set(submissionsResult.rows.map((row) => row.quote_id).filter(Boolean))];
|
|
if (!quoteIds.length) return [];
|
|
|
|
const [
|
|
commandsResult,
|
|
decisionsResult,
|
|
inventoryResult,
|
|
] = await Promise.all([
|
|
pool.query(`
|
|
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
|
FROM execute_trade_commands
|
|
WHERE quote_id = ANY($1::text[])
|
|
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
|
`, [quoteIds]),
|
|
pool.query(`
|
|
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
|
FROM trade_decisions
|
|
WHERE quote_id = ANY($1::text[])
|
|
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
|
`, [quoteIds]),
|
|
pool.query(`
|
|
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
|
FROM (
|
|
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
|
FROM intent_inventory_snapshots
|
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
|
LIMIT $1
|
|
) recent_inventory_snapshots
|
|
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
|
`, [safeInventoryLimit]),
|
|
]);
|
|
|
|
const records = deriveQuoteOutcomeRecords({
|
|
submissions: submissionsResult.rows,
|
|
commands: commandsResult.rows,
|
|
decisions: decisionsResult.rows,
|
|
inventorySnapshots: inventoryResult.rows,
|
|
btcAsset,
|
|
eureAsset,
|
|
now,
|
|
});
|
|
|
|
if (!records.length) return [];
|
|
|
|
const computedAt = new Date(
|
|
typeof now === 'number' ? now : Date.parse(now),
|
|
).toISOString();
|
|
for (const record of records) {
|
|
await upsertQuoteOutcome(pool, {
|
|
...record,
|
|
computedAt,
|
|
});
|
|
}
|
|
|
|
return records;
|
|
}
|
|
|
|
export async function upsertQuoteOutcome(pool, {
|
|
quote_id,
|
|
decision_id = null,
|
|
command_id = null,
|
|
execution_result_status,
|
|
execution_result_code = null,
|
|
submitted_at = null,
|
|
command_at = null,
|
|
outcome_status,
|
|
outcome_observed_at = null,
|
|
outcome_source,
|
|
attribution_status,
|
|
attribution_method = null,
|
|
attributed_inventory_delta = null,
|
|
computedAt,
|
|
payload,
|
|
}) {
|
|
await pool.query(
|
|
`
|
|
INSERT INTO ${QUOTE_OUTCOMES_TABLE} (
|
|
quote_id,
|
|
decision_id,
|
|
command_id,
|
|
execution_result_status,
|
|
execution_result_code,
|
|
submitted_at,
|
|
command_at,
|
|
outcome_status,
|
|
outcome_observed_at,
|
|
outcome_source,
|
|
attribution_status,
|
|
attribution_method,
|
|
attributed_inventory_delta,
|
|
computed_at,
|
|
payload
|
|
) VALUES (
|
|
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13::jsonb,$14,$15::jsonb
|
|
)
|
|
ON CONFLICT (quote_id) DO UPDATE SET
|
|
decision_id = EXCLUDED.decision_id,
|
|
command_id = EXCLUDED.command_id,
|
|
execution_result_status = EXCLUDED.execution_result_status,
|
|
execution_result_code = EXCLUDED.execution_result_code,
|
|
submitted_at = EXCLUDED.submitted_at,
|
|
command_at = EXCLUDED.command_at,
|
|
outcome_status = EXCLUDED.outcome_status,
|
|
outcome_observed_at = EXCLUDED.outcome_observed_at,
|
|
outcome_source = EXCLUDED.outcome_source,
|
|
attribution_status = EXCLUDED.attribution_status,
|
|
attribution_method = EXCLUDED.attribution_method,
|
|
attributed_inventory_delta = EXCLUDED.attributed_inventory_delta,
|
|
computed_at = EXCLUDED.computed_at,
|
|
payload = EXCLUDED.payload
|
|
`,
|
|
[
|
|
quote_id,
|
|
decision_id,
|
|
command_id,
|
|
execution_result_status,
|
|
execution_result_code,
|
|
submitted_at,
|
|
command_at,
|
|
outcome_status,
|
|
outcome_observed_at,
|
|
outcome_source,
|
|
attribution_status,
|
|
attribution_method,
|
|
attributed_inventory_delta ? JSON.stringify(attributed_inventory_delta) : null,
|
|
computedAt,
|
|
JSON.stringify(payload || {}),
|
|
],
|
|
);
|
|
}
|
|
|
|
export async function loadRecentQuoteOutcomes(pool, { limit = 200 } = {}) {
|
|
const result = await pool.query(
|
|
`
|
|
SELECT
|
|
quote_id,
|
|
decision_id,
|
|
command_id,
|
|
execution_result_status,
|
|
execution_result_code,
|
|
submitted_at,
|
|
command_at,
|
|
outcome_status,
|
|
outcome_observed_at,
|
|
outcome_source,
|
|
attribution_status,
|
|
attribution_method,
|
|
attributed_inventory_delta,
|
|
computed_at,
|
|
payload
|
|
FROM ${QUOTE_OUTCOMES_TABLE}
|
|
ORDER BY
|
|
CASE outcome_status
|
|
WHEN 'completed' THEN 0
|
|
WHEN 'not_filled' THEN 1
|
|
ELSE 2
|
|
END,
|
|
COALESCE(outcome_observed_at, submitted_at, computed_at) DESC
|
|
LIMIT $1
|
|
`,
|
|
[Math.max(1, Number(limit) || 200)],
|
|
);
|
|
|
|
return result.rows.map(normalizeQuoteOutcomeRow);
|
|
}
|
|
|
|
export async function loadIntentRequestPreflightByIdOrKey(pool, {
|
|
requestId = null,
|
|
idempotencyKey = null,
|
|
} = {}) {
|
|
const result = await pool.query(
|
|
`
|
|
SELECT observed_at, ingested_at, payload
|
|
FROM intent_request_preflights
|
|
WHERE ($1::text IS NOT NULL AND payload->>'request_id' = $1)
|
|
OR ($2::text IS NOT NULL AND payload->>'idempotency_key' = $2)
|
|
ORDER BY
|
|
COALESCE(observed_at, ingested_at) DESC,
|
|
CASE payload->>'status'
|
|
WHEN 'accepted_by_relay' THEN 0
|
|
WHEN 'failed' THEN 1
|
|
WHEN 'blocked' THEN 2
|
|
WHEN 'submit_requested' THEN 3
|
|
ELSE 4
|
|
END
|
|
LIMIT 1
|
|
`,
|
|
[requestId, idempotencyKey],
|
|
);
|
|
|
|
return normalizeEventPayloadRow(result.rows[0])?.payload || null;
|
|
}
|
|
|
|
export async function loadLatestIntentRequestSubmission(pool, { requestId } = {}) {
|
|
if (!requestId) return null;
|
|
const result = await pool.query(
|
|
`
|
|
SELECT observed_at, ingested_at, payload
|
|
FROM intent_request_submission_results
|
|
WHERE payload->>'request_id' = $1
|
|
ORDER BY
|
|
COALESCE(observed_at, ingested_at) DESC,
|
|
CASE payload->>'status'
|
|
WHEN 'accepted_by_relay' THEN 0
|
|
WHEN 'failed' THEN 1
|
|
WHEN 'blocked' THEN 2
|
|
WHEN 'submit_requested' THEN 3
|
|
ELSE 4
|
|
END
|
|
LIMIT 1
|
|
`,
|
|
[requestId],
|
|
);
|
|
|
|
return normalizeEventPayloadRow(result.rows[0])?.payload || null;
|
|
}
|
|
|
|
export async function loadIntentRequestSubmissionsForStatusRefresh(pool, {
|
|
limit = 20,
|
|
} = {}) {
|
|
const result = await pool.query(
|
|
`
|
|
WITH latest_submissions AS (
|
|
SELECT DISTINCT ON (payload->>'request_id')
|
|
observed_at, ingested_at, payload
|
|
FROM intent_request_submission_results
|
|
WHERE COALESCE(payload->>'request_id', '') <> ''
|
|
ORDER BY
|
|
payload->>'request_id',
|
|
COALESCE(observed_at, ingested_at) DESC,
|
|
CASE payload->>'status'
|
|
WHEN 'accepted_by_relay' THEN 0
|
|
WHEN 'failed' THEN 1
|
|
WHEN 'blocked' THEN 2
|
|
WHEN 'submit_requested' THEN 3
|
|
ELSE 4
|
|
END
|
|
)
|
|
SELECT observed_at, ingested_at, payload
|
|
FROM latest_submissions
|
|
WHERE payload->>'status' = 'accepted_by_relay'
|
|
AND COALESCE(payload->>'intent_hash', '') <> ''
|
|
AND COALESCE(payload->>'relay_status', '') NOT IN ('SETTLED', 'NOT_FOUND_OR_NOT_VALID')
|
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
|
LIMIT $1
|
|
`,
|
|
[Math.max(1, Number(limit) || 20)],
|
|
);
|
|
|
|
return result.rows
|
|
.map((row) => normalizeIntentRequestSubmissionPayload(normalizeEventPayloadRow(row)?.payload || null))
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export async function refreshIntentRequestOutcomes(pool, {
|
|
btcAsset = null,
|
|
eureAsset = null,
|
|
now = Date.now(),
|
|
preflightLimit = 100,
|
|
submissionLimit = 500,
|
|
inventoryLimit = 5000,
|
|
} = {}) {
|
|
if (!btcAsset?.assetId || !eureAsset?.assetId) return [];
|
|
|
|
const safePreflightLimit = Math.max(1, Number(preflightLimit) || 100);
|
|
const safeSubmissionLimit = Math.max(1, Number(submissionLimit) || 500);
|
|
const safeInventoryLimit = Math.max(1, Number(inventoryLimit) || 5000);
|
|
const preflightResult = await loadIntentRequestPreflightRefreshCandidates(pool, {
|
|
limit: safePreflightLimit,
|
|
});
|
|
if (!preflightResult.rows.length) return [];
|
|
|
|
const requestIds = [
|
|
...new Set(preflightResult.rows
|
|
.map((row) => row.payload?.request_id)
|
|
.filter(Boolean)),
|
|
];
|
|
if (!requestIds.length) return [];
|
|
|
|
const submissions = await loadIntentRequestSubmissionsForRefresh(pool, {
|
|
requestIds,
|
|
limit: safeSubmissionLimit,
|
|
});
|
|
const inventorySnapshots = await loadIntentInventorySnapshotsForRequestRefresh(pool, {
|
|
submissions: submissions.rows,
|
|
limit: safeInventoryLimit,
|
|
});
|
|
|
|
const records = deriveIntentRequestOutcomeRecords({
|
|
preflights: preflightResult.rows,
|
|
submissions: submissions.rows,
|
|
inventorySnapshots: inventorySnapshots.rows,
|
|
btcAsset,
|
|
eureAsset,
|
|
now,
|
|
});
|
|
|
|
if (!records.length) return [];
|
|
|
|
const computedAt = new Date(
|
|
typeof now === 'number' ? now : Date.parse(now),
|
|
).toISOString();
|
|
for (const record of records) {
|
|
await upsertIntentRequestOutcome(pool, {
|
|
...record,
|
|
computedAt,
|
|
});
|
|
}
|
|
return records;
|
|
}
|
|
|
|
async function loadIntentRequestPreflightRefreshCandidates(pool, { limit }) {
|
|
return pool.query(
|
|
`
|
|
WITH latest_preflights AS (
|
|
SELECT DISTINCT ON (payload->>'request_id')
|
|
event_id,
|
|
observed_at,
|
|
ingested_at,
|
|
payload
|
|
FROM intent_request_preflights
|
|
WHERE COALESCE(payload->>'request_id', '') <> ''
|
|
ORDER BY payload->>'request_id', COALESCE(observed_at, ingested_at) DESC
|
|
), latest_submissions AS (
|
|
SELECT DISTINCT ON (payload->>'request_id')
|
|
payload->>'request_id' AS request_id,
|
|
observed_at,
|
|
ingested_at,
|
|
payload
|
|
FROM intent_request_submission_results
|
|
WHERE COALESCE(payload->>'request_id', '') <> ''
|
|
ORDER BY
|
|
payload->>'request_id',
|
|
COALESCE(observed_at, ingested_at) DESC,
|
|
CASE payload->>'status'
|
|
WHEN 'accepted_by_relay' THEN 0
|
|
WHEN 'failed' THEN 1
|
|
WHEN 'blocked' THEN 2
|
|
WHEN 'submit_requested' THEN 3
|
|
ELSE 4
|
|
END
|
|
), refresh_candidates AS (
|
|
SELECT
|
|
p.event_id,
|
|
p.observed_at,
|
|
p.ingested_at,
|
|
p.payload,
|
|
COALESCE(
|
|
s.observed_at,
|
|
s.ingested_at,
|
|
p.observed_at,
|
|
p.ingested_at
|
|
) AS refresh_sort_at
|
|
FROM latest_preflights p
|
|
LEFT JOIN latest_submissions s
|
|
ON s.request_id = p.payload->>'request_id'
|
|
LEFT JOIN ${INTENT_REQUEST_OUTCOMES_TABLE} o
|
|
ON o.request_id = p.payload->>'request_id'
|
|
WHERE o.request_id IS NULL
|
|
OR o.outcome_status = ANY($1::text[])
|
|
OR COALESCE(
|
|
s.observed_at,
|
|
s.ingested_at,
|
|
p.observed_at,
|
|
p.ingested_at
|
|
) > o.computed_at
|
|
ORDER BY refresh_sort_at DESC
|
|
LIMIT $2
|
|
)
|
|
SELECT event_id, observed_at, ingested_at, payload
|
|
FROM refresh_candidates
|
|
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
|
`,
|
|
[REFRESHABLE_INTENT_REQUEST_OUTCOME_STATUSES, limit],
|
|
);
|
|
}
|
|
|
|
async function loadIntentRequestSubmissionsForRefresh(pool, { requestIds, limit }) {
|
|
return pool.query(
|
|
`
|
|
SELECT event_id, observed_at, ingested_at, payload
|
|
FROM intent_request_submission_results
|
|
WHERE payload->>'request_id' = ANY($1::text[])
|
|
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
|
LIMIT $2
|
|
`,
|
|
[requestIds, limit],
|
|
);
|
|
}
|
|
|
|
async function loadIntentInventorySnapshotsForRequestRefresh(pool, {
|
|
submissions,
|
|
limit,
|
|
}) {
|
|
const anchorAt = earliestIntentRequestInventoryAnchor({
|
|
submissions,
|
|
});
|
|
if (!anchorAt) return { rows: [] };
|
|
|
|
return pool.query(
|
|
`
|
|
WITH previous_snapshot AS (
|
|
SELECT event_id, observed_at, ingested_at, quote_id, payload, COALESCE(observed_at, ingested_at) AS sort_at
|
|
FROM intent_inventory_snapshots
|
|
WHERE COALESCE(observed_at, ingested_at) < $1
|
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
|
LIMIT 1
|
|
), following_snapshots AS (
|
|
SELECT event_id, observed_at, ingested_at, quote_id, payload, COALESCE(observed_at, ingested_at) AS sort_at
|
|
FROM intent_inventory_snapshots
|
|
WHERE COALESCE(observed_at, ingested_at) >= $1
|
|
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
|
LIMIT $2
|
|
), bounded_snapshots AS (
|
|
SELECT event_id, observed_at, ingested_at, quote_id, payload, sort_at
|
|
FROM previous_snapshot
|
|
UNION ALL
|
|
SELECT event_id, observed_at, ingested_at, quote_id, payload, sort_at
|
|
FROM following_snapshots
|
|
)
|
|
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
|
FROM bounded_snapshots
|
|
ORDER BY sort_at ASC
|
|
`,
|
|
[anchorAt, limit],
|
|
);
|
|
}
|
|
|
|
function earliestIntentRequestInventoryAnchor({ submissions = [] } = {}) {
|
|
const anchors = submissions
|
|
.map((row) => row.payload?.submitted_at || row.observed_at || row.ingested_at)
|
|
.map((value) => {
|
|
const timestamp = timestampValue(value);
|
|
return Number.isFinite(timestamp) ? timestamp : null;
|
|
})
|
|
.filter((value) => value != null)
|
|
.sort((left, right) => left - right);
|
|
|
|
return anchors.length ? new Date(anchors[0]).toISOString() : null;
|
|
}
|
|
|
|
export async function upsertIntentRequestOutcome(pool, {
|
|
request_id,
|
|
idempotency_key,
|
|
submission_id = null,
|
|
intent_hash = null,
|
|
submission_status = null,
|
|
relay_status = null,
|
|
submitted_at = null,
|
|
outcome_status,
|
|
outcome_observed_at = null,
|
|
outcome_source,
|
|
outcome_reason,
|
|
attribution_status,
|
|
attribution_method = null,
|
|
attributed_inventory_delta = null,
|
|
computedAt,
|
|
payload,
|
|
}) {
|
|
await pool.query(
|
|
`
|
|
INSERT INTO ${INTENT_REQUEST_OUTCOMES_TABLE} (
|
|
request_id,
|
|
idempotency_key,
|
|
submission_id,
|
|
intent_hash,
|
|
submission_status,
|
|
relay_status,
|
|
submitted_at,
|
|
outcome_status,
|
|
outcome_observed_at,
|
|
outcome_source,
|
|
outcome_reason,
|
|
attribution_status,
|
|
attribution_method,
|
|
attributed_inventory_delta,
|
|
computed_at,
|
|
payload
|
|
) VALUES (
|
|
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::jsonb,$15,$16::jsonb
|
|
)
|
|
ON CONFLICT (request_id) DO UPDATE SET
|
|
idempotency_key = EXCLUDED.idempotency_key,
|
|
submission_id = EXCLUDED.submission_id,
|
|
intent_hash = EXCLUDED.intent_hash,
|
|
submission_status = EXCLUDED.submission_status,
|
|
relay_status = EXCLUDED.relay_status,
|
|
submitted_at = EXCLUDED.submitted_at,
|
|
outcome_status = EXCLUDED.outcome_status,
|
|
outcome_observed_at = EXCLUDED.outcome_observed_at,
|
|
outcome_source = EXCLUDED.outcome_source,
|
|
outcome_reason = EXCLUDED.outcome_reason,
|
|
attribution_status = EXCLUDED.attribution_status,
|
|
attribution_method = EXCLUDED.attribution_method,
|
|
attributed_inventory_delta = EXCLUDED.attributed_inventory_delta,
|
|
computed_at = EXCLUDED.computed_at,
|
|
payload = EXCLUDED.payload
|
|
`,
|
|
[
|
|
request_id,
|
|
idempotency_key,
|
|
submission_id,
|
|
intent_hash,
|
|
submission_status,
|
|
relay_status,
|
|
submitted_at,
|
|
outcome_status,
|
|
outcome_observed_at,
|
|
outcome_source,
|
|
outcome_reason,
|
|
attribution_status,
|
|
attribution_method,
|
|
attributed_inventory_delta ? JSON.stringify(attributed_inventory_delta) : null,
|
|
computedAt,
|
|
JSON.stringify(payload || {}),
|
|
],
|
|
);
|
|
}
|
|
|
|
export async function loadRecentIntentRequests(pool, {
|
|
limit = 20,
|
|
btcAsset = null,
|
|
eureAsset = null,
|
|
now = Date.now(),
|
|
refreshOutcomes = true,
|
|
} = {}) {
|
|
if (refreshOutcomes && btcAsset?.assetId && eureAsset?.assetId) {
|
|
await refreshIntentRequestOutcomes(pool, { btcAsset, eureAsset, now }).catch(() => []);
|
|
}
|
|
|
|
const result = await pool.query(
|
|
`
|
|
WITH latest_preflights AS (
|
|
SELECT DISTINCT ON (payload->>'request_id')
|
|
observed_at AS preflight_observed_at,
|
|
ingested_at AS preflight_ingested_at,
|
|
payload AS preflight_payload
|
|
FROM intent_request_preflights
|
|
WHERE COALESCE(payload->>'request_id', '') <> ''
|
|
ORDER BY payload->>'request_id', COALESCE(observed_at, ingested_at) DESC
|
|
), latest_submissions AS (
|
|
SELECT DISTINCT ON (payload->>'request_id')
|
|
observed_at AS submission_observed_at,
|
|
ingested_at AS submission_ingested_at,
|
|
payload AS submission_payload
|
|
FROM intent_request_submission_results
|
|
WHERE COALESCE(payload->>'request_id', '') <> ''
|
|
ORDER BY
|
|
payload->>'request_id',
|
|
COALESCE(observed_at, ingested_at) DESC,
|
|
CASE payload->>'status'
|
|
WHEN 'accepted_by_relay' THEN 0
|
|
WHEN 'failed' THEN 1
|
|
WHEN 'blocked' THEN 2
|
|
WHEN 'submit_requested' THEN 3
|
|
ELSE 4
|
|
END
|
|
)
|
|
SELECT
|
|
p.preflight_observed_at,
|
|
p.preflight_ingested_at,
|
|
p.preflight_payload,
|
|
s.submission_observed_at,
|
|
s.submission_ingested_at,
|
|
s.submission_payload,
|
|
o.outcome_observed_at,
|
|
o.computed_at AS outcome_computed_at,
|
|
o.payload AS outcome_payload
|
|
FROM latest_preflights p
|
|
LEFT JOIN latest_submissions s
|
|
ON s.submission_payload->>'request_id' = p.preflight_payload->>'request_id'
|
|
LEFT JOIN ${INTENT_REQUEST_OUTCOMES_TABLE} o
|
|
ON o.request_id = p.preflight_payload->>'request_id'
|
|
ORDER BY COALESCE(
|
|
o.outcome_observed_at,
|
|
s.submission_observed_at,
|
|
s.submission_ingested_at,
|
|
p.preflight_observed_at,
|
|
p.preflight_ingested_at
|
|
) DESC
|
|
LIMIT $1
|
|
`,
|
|
[Math.max(1, Number(limit) || 20)],
|
|
);
|
|
|
|
return result.rows.map(normalizeIntentRequestRow);
|
|
}
|
|
|
|
|
|
export async function loadLatestInventorySnapshot(pool) {
|
|
const latest = await loadLatestEventPayload(pool, 'intent_inventory_snapshots');
|
|
if (!latest) return null;
|
|
return {
|
|
ingested_at: latest.ingested_at,
|
|
payload: latest.payload,
|
|
};
|
|
}
|
|
|
|
export async function loadLatestMarketPrice(pool) {
|
|
const latest = await loadLatestEventPayload(pool, 'market_price_events');
|
|
if (!latest) return null;
|
|
return {
|
|
ingested_at: latest.ingested_at,
|
|
payload: latest.payload,
|
|
};
|
|
}
|
|
|
|
export async function loadRecentQuotes(pool, { limit = 10 } = {}) {
|
|
const result = await pool.query(
|
|
`
|
|
SELECT observed_at, ingested_at, payload
|
|
FROM swap_demand_events
|
|
ORDER BY ingested_at DESC
|
|
LIMIT $1
|
|
`,
|
|
[limit],
|
|
);
|
|
|
|
return result.rows.map(normalizeRecentQuoteRow);
|
|
}
|
|
|
|
export async function loadSubmissionSummary(pool) {
|
|
const result = await pool.query(`
|
|
SELECT
|
|
COUNT(*)::INT AS total,
|
|
MAX(COALESCE(observed_at, ingested_at)) AS last_submission_at
|
|
FROM trade_execution_results
|
|
WHERE payload->>'status' = 'submitted'
|
|
`);
|
|
|
|
return {
|
|
total: Number(result.rows[0]?.total || 0),
|
|
last_submission_at: toIsoTimestamp(result.rows[0]?.last_submission_at),
|
|
};
|
|
}
|
|
|
|
export async function loadSubmissionPage(pool, { page = 1, pageSize = 20 } = {}) {
|
|
const normalizedPage = Math.max(1, Number(page) || 1);
|
|
const normalizedPageSize = Math.max(1, Number(pageSize) || 20);
|
|
const offset = (normalizedPage - 1) * normalizedPageSize;
|
|
|
|
const [countResult, rowsResult] = await Promise.all([
|
|
pool.query(`
|
|
SELECT COUNT(*)::INT AS total
|
|
FROM trade_execution_results
|
|
WHERE payload->>'status' = 'submitted'
|
|
`),
|
|
pool.query(
|
|
`
|
|
SELECT
|
|
r.observed_at AS result_observed_at,
|
|
r.ingested_at AS result_ingested_at,
|
|
r.payload AS result_payload,
|
|
c.payload AS command_payload,
|
|
d.payload AS decision_payload,
|
|
o.payload AS outcome_payload
|
|
FROM trade_execution_results r
|
|
LEFT JOIN execute_trade_commands c
|
|
ON c.decision_key = r.decision_key
|
|
LEFT JOIN trade_decisions d
|
|
ON d.decision_key = COALESCE(c.payload->>'decision_id', r.payload->>'decision_id')
|
|
LEFT JOIN ${QUOTE_OUTCOMES_TABLE} o
|
|
ON o.quote_id = r.quote_id
|
|
WHERE r.payload->>'status' = 'submitted'
|
|
ORDER BY COALESCE(r.observed_at, r.ingested_at) DESC
|
|
LIMIT $1
|
|
OFFSET $2
|
|
`,
|
|
[normalizedPageSize, offset],
|
|
),
|
|
]);
|
|
|
|
const total = Number(countResult.rows[0]?.total || 0);
|
|
|
|
return {
|
|
page: normalizedPage,
|
|
page_size: normalizedPageSize,
|
|
total,
|
|
total_pages: total > 0 ? Math.ceil(total / normalizedPageSize) : 1,
|
|
items: rowsResult.rows.map(normalizeSubmissionRow),
|
|
};
|
|
}
|
|
|
|
export async function loadRecentExecutionResults(pool, { limit = 20 } = {}) {
|
|
const result = await pool.query(
|
|
`
|
|
SELECT
|
|
r.observed_at AS result_observed_at,
|
|
r.ingested_at AS result_ingested_at,
|
|
r.payload AS result_payload,
|
|
c.ingested_at AS command_ingested_at,
|
|
c.payload AS command_payload,
|
|
d.payload AS decision_payload,
|
|
o.payload AS outcome_payload
|
|
FROM trade_execution_results r
|
|
LEFT JOIN execute_trade_commands c
|
|
ON c.decision_key = r.decision_key
|
|
LEFT JOIN trade_decisions d
|
|
ON d.decision_key = COALESCE(c.payload->>'decision_id', r.payload->>'decision_id')
|
|
LEFT JOIN ${QUOTE_OUTCOMES_TABLE} o
|
|
ON o.quote_id = r.quote_id
|
|
ORDER BY COALESCE(r.observed_at, r.ingested_at) DESC
|
|
LIMIT $1
|
|
`,
|
|
[limit],
|
|
);
|
|
|
|
return result.rows.map((row) => normalizeExecutionResultRow(row));
|
|
}
|
|
|
|
export async function loadRecentExecuteTradeCommands(pool, { limit = 20 } = {}) {
|
|
const result = await pool.query(
|
|
`
|
|
SELECT observed_at, ingested_at, payload
|
|
FROM execute_trade_commands
|
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
|
LIMIT $1
|
|
`,
|
|
[limit],
|
|
);
|
|
|
|
return result.rows.map((row) => normalizeExecuteTradeCommandRow(row));
|
|
}
|
|
|
|
export async function loadCurrentFundingObservations(pool) {
|
|
const result = await pool.query(`
|
|
SELECT DISTINCT ON (decision_key)
|
|
observed_at,
|
|
ingested_at,
|
|
payload
|
|
FROM funding_observations
|
|
WHERE decision_key IS NOT NULL
|
|
ORDER BY decision_key, ingested_at DESC
|
|
`);
|
|
|
|
return result.rows
|
|
.map((row) => ({
|
|
observed_at: toIsoTimestamp(row.observed_at),
|
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
|
payload: row.payload,
|
|
}))
|
|
.sort((left, right) => (
|
|
Date.parse(
|
|
right.payload?.last_seen_at
|
|
|| right.observed_at
|
|
|| right.ingested_at
|
|
|| '',
|
|
) - Date.parse(
|
|
left.payload?.last_seen_at
|
|
|| left.observed_at
|
|
|| left.ingested_at
|
|
|| '',
|
|
)
|
|
));
|
|
}
|
|
|
|
export async function loadRecentDepositStatuses(pool, { limit = 20 } = {}) {
|
|
const normalizedLimit = Math.max(1, Number(limit) || 20);
|
|
const fetchLimit = Math.max(normalizedLimit * 50, 500);
|
|
const result = await pool.query(
|
|
`
|
|
SELECT observed_at, ingested_at, payload
|
|
FROM liquidity_actions
|
|
WHERE payload->>'action_type' = 'deposit_status_observed'
|
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
|
LIMIT $1
|
|
`,
|
|
[fetchLimit],
|
|
);
|
|
|
|
return normalizeDepositStatusRows(result.rows)
|
|
.sort((left, right) => (
|
|
timestampValue(right.observed_at || right.ingested_at)
|
|
- timestampValue(left.observed_at || left.ingested_at)
|
|
))
|
|
.slice(0, normalizedLimit);
|
|
}
|
|
|
|
export async function loadRecentAlertTransitions(pool, { limit = 20 } = {}) {
|
|
const result = await pool.query(
|
|
`
|
|
SELECT observed_at, ingested_at, payload
|
|
FROM ops_alerts
|
|
ORDER BY ingested_at DESC
|
|
LIMIT $1
|
|
`,
|
|
[limit],
|
|
);
|
|
|
|
return result.rows.map((row) => ({
|
|
observed_at: toIsoTimestamp(row.observed_at),
|
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
|
payload: row.payload,
|
|
}));
|
|
}
|
|
|
|
export async function loadRecentEnvironmentStatuses(pool, { limit = 20 } = {}) {
|
|
const result = await pool.query(
|
|
`
|
|
SELECT observed_at, ingested_at, payload
|
|
FROM environment_status_events
|
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
|
LIMIT $1
|
|
`,
|
|
[Math.max(1, Number(limit) || 20)],
|
|
);
|
|
|
|
return result.rows.map((row) => ({
|
|
observed_at: toIsoTimestamp(row.observed_at),
|
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
|
payload: row.payload,
|
|
}));
|
|
}
|
|
|
|
export async function loadRecentTradeDecisions(pool, { limit = 20 } = {}) {
|
|
const result = await pool.query(
|
|
`
|
|
SELECT observed_at, ingested_at, payload
|
|
FROM trade_decisions
|
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
|
LIMIT $1
|
|
`,
|
|
[limit],
|
|
);
|
|
|
|
return result.rows.map((row) => ({
|
|
observed_at: toIsoTimestamp(row.observed_at),
|
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
|
payload: row.payload,
|
|
}));
|
|
}
|
|
|
|
async function loadLatestEventPayload(pool, table, clause = 'ORDER BY ingested_at DESC LIMIT 1', params = []) {
|
|
const result = await pool.query(
|
|
`
|
|
SELECT ingested_at, payload
|
|
FROM ${table}
|
|
${clause}
|
|
`,
|
|
params,
|
|
);
|
|
if (!result.rows[0]) return null;
|
|
return {
|
|
ingested_at: result.rows[0].ingested_at ? new Date(result.rows[0].ingested_at).toISOString() : null,
|
|
payload: result.rows[0].payload,
|
|
};
|
|
}
|
|
|
|
async function loadNearestPricePayload(pool, anchorAt) {
|
|
const before = await loadLatestEventPayload(
|
|
pool,
|
|
'market_price_events',
|
|
'WHERE ingested_at <= $1 ORDER BY ingested_at DESC LIMIT 1',
|
|
[anchorAt],
|
|
);
|
|
if (before) return before;
|
|
return loadLatestEventPayload(
|
|
pool,
|
|
'market_price_events',
|
|
'WHERE ingested_at >= $1 ORDER BY ingested_at ASC LIMIT 1',
|
|
[anchorAt],
|
|
);
|
|
}
|
|
|
|
async function loadExternalAssetFlowsSince(pool, {
|
|
since,
|
|
btcAsset,
|
|
btcAssets = null,
|
|
eureAsset,
|
|
valuationAssets = [],
|
|
} = {}) {
|
|
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
|
|
const [depositRows, withdrawalRows] = await Promise.all([
|
|
loadCreditedDepositRowsSince(pool, since),
|
|
loadCompletedWithdrawalRowsSince(pool, since),
|
|
]);
|
|
|
|
const flows = [];
|
|
|
|
for (const row of depositRows) {
|
|
flows.push(await normalizeExternalFlowRow(pool, {
|
|
row,
|
|
kind: 'deposit',
|
|
sign: 1n,
|
|
btcAsset: effectiveBtcAssets[0],
|
|
btcAssets: effectiveBtcAssets,
|
|
eureAsset,
|
|
valuationAssets,
|
|
}));
|
|
}
|
|
|
|
for (const row of withdrawalRows) {
|
|
flows.push(await normalizeExternalFlowRow(pool, {
|
|
row,
|
|
kind: 'withdrawal',
|
|
sign: -1n,
|
|
btcAsset: effectiveBtcAssets[0],
|
|
btcAssets: effectiveBtcAssets,
|
|
eureAsset,
|
|
valuationAssets,
|
|
}));
|
|
}
|
|
|
|
return flows
|
|
.filter(Boolean)
|
|
.sort((left, right) => timestampValue(left.effective_at) - timestampValue(right.effective_at));
|
|
}
|
|
|
|
async function loadCreditedDepositRowsSince(pool, since) {
|
|
const result = await pool.query(
|
|
`
|
|
SELECT observed_at, ingested_at, payload
|
|
FROM liquidity_actions
|
|
WHERE payload->>'action_type' = 'deposit_status_observed'
|
|
AND UPPER(payload->>'status') = ANY($1)
|
|
AND COALESCE(payload->'details'->>'tx_hash', '') <> ''
|
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
|
`,
|
|
[CREDITED_LIQUIDITY_STATUSES],
|
|
);
|
|
return normalizeDepositStatusRows(result.rows)
|
|
.filter((row) => timestampValue(row.observed_at || row.ingested_at) > timestampValue(since));
|
|
}
|
|
|
|
async function loadCompletedWithdrawalRowsSince(pool, since) {
|
|
const result = await pool.query(
|
|
`
|
|
SELECT DISTINCT ON (
|
|
payload->'details'->>'withdrawal_hash',
|
|
payload->>'chain',
|
|
payload->>'asset_id'
|
|
)
|
|
observed_at,
|
|
ingested_at,
|
|
payload
|
|
FROM liquidity_actions
|
|
WHERE ingested_at > $1
|
|
AND payload->>'action_type' = 'withdrawal_status_changed'
|
|
AND UPPER(payload->>'status') = ANY($2)
|
|
AND COALESCE(payload->'details'->>'withdrawal_hash', '') <> ''
|
|
ORDER BY
|
|
payload->'details'->>'withdrawal_hash',
|
|
payload->>'chain',
|
|
payload->>'asset_id',
|
|
ingested_at DESC
|
|
`,
|
|
[since, COMPLETED_WITHDRAWAL_STATUSES],
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
async function normalizeExternalFlowRow(pool, {
|
|
row,
|
|
kind,
|
|
sign,
|
|
btcAsset,
|
|
btcAssets = null,
|
|
eureAsset,
|
|
valuationAssets = [],
|
|
} = {}) {
|
|
const payload = row?.payload || {};
|
|
const details = payload.details || {};
|
|
const assetId = payload.asset_id || details.asset_id || null;
|
|
if (!assetId) return null;
|
|
|
|
const amount = String(details.amount || '0');
|
|
const effectiveAt = toIsoTimestamp(details.created_at || row.observed_at || row.ingested_at);
|
|
const signedUnits = (sign * BigInt(amount)).toString();
|
|
let referencePriceAtFlowTime = null;
|
|
let referencePriceEurePerUnitAtFlowTime = null;
|
|
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
|
|
const flowBtcAsset = effectiveBtcAssets.find((asset) => asset.assetId === assetId);
|
|
const valuationAsset = valuationAssets.find((asset) => asset.assetId === assetId);
|
|
|
|
if (flowBtcAsset) {
|
|
const nearestPrice = await loadNearestPricePayload(pool, effectiveAt);
|
|
referencePriceAtFlowTime = nearestPrice?.payload?.eure_per_btc || null;
|
|
} else if (valuationAsset) {
|
|
referencePriceEurePerUnitAtFlowTime = valuationAsset.currentUnitValueEure || null;
|
|
} else if (assetId !== eureAsset?.assetId) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
flow_id:
|
|
kind === 'deposit'
|
|
? `deposit:${details.tx_hash || effectiveAt || Math.random().toString(16).slice(2)}`
|
|
: `withdrawal:${details.withdrawal_hash || effectiveAt || Math.random().toString(16).slice(2)}`,
|
|
kind,
|
|
asset_id: assetId,
|
|
effective_at: effectiveAt,
|
|
signed_units: signedUnits,
|
|
tx_hash: details.tx_hash || null,
|
|
withdrawal_hash: details.withdrawal_hash || null,
|
|
reference_price_eure_per_btc_at_flow_time: referencePriceAtFlowTime,
|
|
reference_price_eure_per_unit_at_flow_time: referencePriceEurePerUnitAtFlowTime,
|
|
};
|
|
}
|
|
|
|
function normalizeBtcAssets({ btcAsset = null, btcAssets = null } = {}) {
|
|
const assets = btcAssets?.length ? btcAssets : [btcAsset];
|
|
return [...new Map(
|
|
assets
|
|
.filter((asset) => asset?.assetId)
|
|
.map((asset) => [asset.assetId, asset]),
|
|
).values()];
|
|
}
|
|
|
|
function normalizePortfolioMetricRow(row) {
|
|
return {
|
|
metric_id: row.metric_id,
|
|
computed_at: row.computed_at ? new Date(row.computed_at).toISOString() : null,
|
|
baseline_anchor_at: row.baseline_anchor_at ? new Date(row.baseline_anchor_at).toISOString() : null,
|
|
baseline_status: row.baseline_status,
|
|
payload: row.payload,
|
|
};
|
|
}
|
|
|
|
function normalizeQuoteOutcomeRow(row) {
|
|
const payload = row.payload || {};
|
|
return {
|
|
quote_id: row.quote_id || payload.quote_id || null,
|
|
decision_id: row.decision_id || payload.decision_id || null,
|
|
command_id: row.command_id || payload.command_id || null,
|
|
pair: payload.pair || null,
|
|
direction: payload.direction || null,
|
|
request_kind: payload.request_kind || null,
|
|
gross_edge_pct: payload.gross_edge_pct || null,
|
|
eure_notional: payload.eure_notional || null,
|
|
execution_result_status: row.execution_result_status || payload.execution_result_status || null,
|
|
execution_result_code: row.execution_result_code || payload.execution_result_code || null,
|
|
submitted_at: toIsoTimestamp(row.submitted_at || payload.submitted_at),
|
|
command_at: toIsoTimestamp(row.command_at || payload.command_at),
|
|
outcome_status: row.outcome_status || payload.outcome_status || null,
|
|
outcome_observed_at: toIsoTimestamp(row.outcome_observed_at || payload.outcome_observed_at),
|
|
outcome_source: row.outcome_source || payload.outcome_source || null,
|
|
outcome_reason: payload.outcome_reason || null,
|
|
attribution_status: row.attribution_status || payload.attribution_status || null,
|
|
attribution_method: row.attribution_method || payload.attribution_method || null,
|
|
attributed_inventory_delta:
|
|
row.attributed_inventory_delta
|
|
|| payload.attributed_inventory_delta
|
|
|| null,
|
|
computed_at: toIsoTimestamp(row.computed_at),
|
|
evidence: payload.evidence || null,
|
|
};
|
|
}
|
|
|
|
function normalizeEventPayloadRow(row) {
|
|
if (!row) return null;
|
|
return {
|
|
observed_at: toIsoTimestamp(row.observed_at),
|
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
|
payload: row.payload || {},
|
|
};
|
|
}
|
|
|
|
function normalizeIntentRequestSubmissionPayload(payload = null) {
|
|
if (!payload) return null;
|
|
return {
|
|
request_id: payload.request_id || null,
|
|
idempotency_key: payload.idempotency_key || null,
|
|
submission_id: payload.submission_id || null,
|
|
status: payload.status || null,
|
|
result_code: payload.result_code || null,
|
|
result_text: payload.result_text || null,
|
|
quote_hash: payload.quote_hash || null,
|
|
intent_hash: payload.intent_hash || null,
|
|
destination_amount_units: payload.destination_amount_units || null,
|
|
nonce: payload.nonce || null,
|
|
submitted_at: toIsoTimestamp(payload.submitted_at),
|
|
relay_status: payload.relay_status || null,
|
|
relay_status_response: payload.relay_status_response || null,
|
|
status_checked_at: toIsoTimestamp(payload.status_checked_at),
|
|
};
|
|
}
|
|
|
|
export function normalizeIntentRequestRow(row) {
|
|
const preflight = row.preflight_payload || {};
|
|
const submission = row.submission_payload || null;
|
|
const outcome = row.outcome_payload || null;
|
|
const state = outcome?.outcome_status
|
|
|| mapSubmissionStatusToRequestState(submission?.status)
|
|
|| preflight.state
|
|
|| 'unknown';
|
|
const reasonCode = outcome?.outcome_reason
|
|
|| submission?.result_code
|
|
|| preflight.reason_code
|
|
|| 'reason_unknown';
|
|
const reasonText = outcome?.reason_text
|
|
|| (outcome?.outcome_reason ? humanizeIntentRequestReason(outcome.outcome_reason) : null)
|
|
|| submission?.result_text
|
|
|| preflight.reason_text
|
|
|| reasonCode.replaceAll('_', ' ');
|
|
|
|
return {
|
|
request_id: preflight.request_id || null,
|
|
idempotency_key: preflight.idempotency_key || null,
|
|
submission_id: submission?.submission_id || outcome?.submission_id || null,
|
|
intent_hash: submission?.intent_hash || outcome?.intent_hash || null,
|
|
quote_hash: submission?.quote_hash || preflight.selected_quote?.quote_hash || null,
|
|
created_at: toIsoTimestamp(preflight.created_at || row.preflight_observed_at || row.preflight_ingested_at),
|
|
submitted_at: toIsoTimestamp(submission?.submitted_at || outcome?.submitted_at || row.submission_observed_at || row.submission_ingested_at),
|
|
resolved_at: isTerminalIntentRequestState(state)
|
|
? toIsoTimestamp(outcome?.outcome_observed_at || row.outcome_observed_at || submission?.submitted_at)
|
|
: null,
|
|
state,
|
|
state_label: labelIntentRequestState(state),
|
|
reason_code: reasonCode,
|
|
reason_text: reasonText,
|
|
source_asset_id: preflight.source_asset_id || null,
|
|
source_symbol: preflight.source_symbol || null,
|
|
source_decimals: preflight.source_decimals ?? null,
|
|
destination_asset_id: preflight.destination_asset_id || null,
|
|
destination_symbol: preflight.destination_symbol || null,
|
|
destination_decimals: preflight.destination_decimals ?? null,
|
|
source_amount_units: preflight.source_amount_units || null,
|
|
expected_destination_amount_units: preflight.expected_destination_amount_units || null,
|
|
min_destination_amount_units: preflight.min_destination_amount_units || null,
|
|
quoted_destination_amount_units:
|
|
submission?.destination_amount_units
|
|
|| preflight.quoted_destination_amount_units
|
|
|| preflight.selected_quote?.amount_out
|
|
|| null,
|
|
slippage_bps: preflight.slippage_bps ?? null,
|
|
deadline_at: preflight.deadline_at || null,
|
|
signer_account_id: preflight.signer_account_id || null,
|
|
signer_public_key: preflight.signer_public_key || null,
|
|
verifier_contract: preflight.verifier_contract || null,
|
|
nonce: submission?.nonce || null,
|
|
nonce_policy: preflight.nonce_policy || null,
|
|
live_submit_capable: preflight.live_submit_capable === true && !submission,
|
|
solver_quote_count: preflight.solver_quote_count || 0,
|
|
selected_quote: preflight.selected_quote || null,
|
|
submission_status: submission?.status || outcome?.submission_status || null,
|
|
relay_status: submission?.relay_status || outcome?.evidence?.relay_status || null,
|
|
relay_response: submission?.relay_response || null,
|
|
relay_status_response: submission?.relay_status_response || null,
|
|
outcome_status: outcome?.outcome_status || null,
|
|
outcome_source: outcome?.outcome_source || null,
|
|
attribution_status: outcome?.attribution_status || null,
|
|
attribution_method: outcome?.attribution_method || null,
|
|
attributed_inventory_delta: outcome?.attributed_inventory_delta || null,
|
|
has_settlement_evidence: Boolean(
|
|
outcome?.attributed_inventory_delta
|
|
&& ['heuristic_match', 'linked_settlement'].includes(outcome?.attribution_status),
|
|
),
|
|
lifecycle: {
|
|
preflight,
|
|
submission,
|
|
outcome,
|
|
},
|
|
};
|
|
}
|
|
|
|
function humanizeIntentRequestReason(reason) {
|
|
const normalized = String(reason || '').trim();
|
|
const labels = {
|
|
accepted_by_relay_without_settlement:
|
|
'Relay accepted the signed request; waiting for durable EURe decrease and BTC increase evidence.',
|
|
relay_settled_but_inventory_delta_missing:
|
|
'Relay reported settlement, but no matching durable inventory movement is linked yet.',
|
|
deadline_elapsed_without_settlement:
|
|
'Deadline and grace window elapsed without matching EURe decrease and BTC increase evidence.',
|
|
matched_inventory_delta:
|
|
'Matched durable EURe decrease and BTC increase evidence.',
|
|
ambiguous_inventory_delta_match:
|
|
'More than one inventory movement could match this request; no completion is assigned.',
|
|
relay_not_found_or_not_valid:
|
|
'Relay reported the intent as not found or not valid.',
|
|
relay_settled_without_expected_inventory_delta:
|
|
'Relay reports settlement, but durable inventory does not show the expected EURe decrease and BTC increase.',
|
|
solver_quote_unanswered:
|
|
'The relay returned no solver quotes for this request.',
|
|
};
|
|
return labels[normalized] || normalized.replaceAll('_', ' ');
|
|
}
|
|
|
|
function mapSubmissionStatusToRequestState(status) {
|
|
if (status === 'accepted_by_relay') return 'awaiting_settlement';
|
|
if (status === 'submit_requested') return 'submitted';
|
|
if (status === 'blocked') return 'blocked';
|
|
if (status === 'failed') return 'failed';
|
|
return null;
|
|
}
|
|
|
|
function labelIntentRequestState(state) {
|
|
const labels = {
|
|
draft: 'Draft',
|
|
blocked: 'Blocked',
|
|
submitted: 'Submitted',
|
|
accepted_by_relay: 'Accepted by relay',
|
|
awaiting_settlement: 'Awaiting settlement',
|
|
failed: 'Failed',
|
|
not_filled: 'Not filled',
|
|
completed: 'Completed',
|
|
};
|
|
return labels[state] || state || 'Unknown';
|
|
}
|
|
|
|
function isTerminalIntentRequestState(state) {
|
|
return ['blocked', 'failed', 'not_filled', 'completed'].includes(state);
|
|
}
|
|
|
|
|
|
function normalizeRecentQuoteRow(row) {
|
|
const payload = row.payload || {};
|
|
return {
|
|
quote_id: payload.quote_id || null,
|
|
pair: payload.pair || buildPair(payload.asset_in, payload.asset_out),
|
|
asset_in: payload.asset_in || null,
|
|
asset_out: payload.asset_out || null,
|
|
request_kind: payload.request_kind || null,
|
|
amount_in: payload.amount_in ?? null,
|
|
amount_out: payload.amount_out ?? null,
|
|
min_deadline_ms: payload.min_deadline_ms ?? null,
|
|
observed_at: toIsoTimestamp(row.observed_at),
|
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
|
};
|
|
}
|
|
|
|
function normalizeSubmissionRow(row) {
|
|
const resultPayload = row.result_payload || {};
|
|
const commandPayload = row.command_payload || {};
|
|
const decisionPayload = row.decision_payload || {};
|
|
const outcomePayload = row.outcome_payload || {};
|
|
|
|
return {
|
|
command_id: resultPayload.command_id || commandPayload.command_id || null,
|
|
decision_id:
|
|
commandPayload.decision_id
|
|
|| resultPayload.decision_id
|
|
|| decisionPayload.decision_id
|
|
|| null,
|
|
execution_key: resultPayload.execution_key || commandPayload.execution_key || null,
|
|
quote_id: resultPayload.quote_id || commandPayload.quote_id || decisionPayload.quote_id || null,
|
|
pair: resultPayload.pair || commandPayload.pair || decisionPayload.pair || null,
|
|
pair_id: commandPayload.pair_id || decisionPayload.pair_id || resultPayload.pair_id || null,
|
|
pair_config_id:
|
|
commandPayload.pair_config_id
|
|
|| decisionPayload.pair_config_id
|
|
|| resultPayload.pair_config_id
|
|
|| null,
|
|
pair_config_version:
|
|
commandPayload.pair_config_version
|
|
|| decisionPayload.pair_config_version
|
|
|| resultPayload.pair_config_version
|
|
|| null,
|
|
edge_bps: commandPayload.edge_bps || decisionPayload.edge_bps || resultPayload.edge_bps || null,
|
|
observed_at: toIsoTimestamp(row.result_observed_at || row.result_ingested_at),
|
|
ingested_at: toIsoTimestamp(row.result_ingested_at),
|
|
status: resultPayload.status || null,
|
|
result_code: resultPayload.result_code || null,
|
|
outcome_status: outcomePayload.outcome_status || null,
|
|
outcome_reason: outcomePayload.outcome_reason || null,
|
|
attribution_status: outcomePayload.attribution_status || null,
|
|
attributed_inventory_delta: outcomePayload.attributed_inventory_delta || null,
|
|
request_kind: commandPayload.request_kind || decisionPayload.request_kind || null,
|
|
asset_in: commandPayload.asset_in || null,
|
|
asset_out: commandPayload.asset_out || null,
|
|
amount_in: resolveTradeAmount(commandPayload, 'amount_in'),
|
|
amount_out: resolveTradeAmount(commandPayload, 'amount_out'),
|
|
quoted_amount_in: commandPayload.amount_in || null,
|
|
quoted_amount_out: commandPayload.amount_out || null,
|
|
gross_edge_pct: decisionPayload.gross_edge_pct || null,
|
|
decision_reason: decisionPayload.decision_reason || null,
|
|
direction: decisionPayload.direction || null,
|
|
};
|
|
}
|
|
|
|
function normalizeExecuteTradeCommandRow(row) {
|
|
const payload = row.payload || {};
|
|
return {
|
|
command_id: payload.command_id || null,
|
|
decision_id: payload.decision_id || null,
|
|
execution_key: payload.execution_key || null,
|
|
quote_id: payload.quote_id || null,
|
|
pair: payload.pair || null,
|
|
pair_id: payload.pair_id || null,
|
|
pair_config_id: payload.pair_config_id || null,
|
|
pair_config_version: payload.pair_config_version || null,
|
|
edge_bps: payload.edge_bps || null,
|
|
direction: payload.direction || null,
|
|
request_kind: payload.request_kind || null,
|
|
asset_in: payload.asset_in || null,
|
|
asset_out: payload.asset_out || null,
|
|
amount_in: resolveTradeAmount(payload, 'amount_in'),
|
|
amount_out: resolveTradeAmount(payload, 'amount_out'),
|
|
observed_at: toIsoTimestamp(row.observed_at || row.ingested_at),
|
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
|
};
|
|
}
|
|
|
|
function normalizeExecutionResultRow(row) {
|
|
const resultPayload = row.result_payload || {};
|
|
const commandPayload = row.command_payload || {};
|
|
const decisionPayload = row.decision_payload || {};
|
|
const outcomePayload = row.outcome_payload || {};
|
|
|
|
return {
|
|
command_id: resultPayload.command_id || commandPayload.command_id || null,
|
|
decision_id:
|
|
commandPayload.decision_id
|
|
|| resultPayload.decision_id
|
|
|| decisionPayload.decision_id
|
|
|| null,
|
|
execution_key: resultPayload.execution_key || commandPayload.execution_key || null,
|
|
quote_id: resultPayload.quote_id || commandPayload.quote_id || decisionPayload.quote_id || null,
|
|
pair: resultPayload.pair || commandPayload.pair || decisionPayload.pair || null,
|
|
pair_id: commandPayload.pair_id || decisionPayload.pair_id || resultPayload.pair_id || null,
|
|
pair_config_id:
|
|
commandPayload.pair_config_id
|
|
|| decisionPayload.pair_config_id
|
|
|| resultPayload.pair_config_id
|
|
|| null,
|
|
pair_config_version:
|
|
commandPayload.pair_config_version
|
|
|| decisionPayload.pair_config_version
|
|
|| resultPayload.pair_config_version
|
|
|| null,
|
|
edge_bps: commandPayload.edge_bps || decisionPayload.edge_bps || resultPayload.edge_bps || null,
|
|
command_at: toIsoTimestamp(row.command_ingested_at),
|
|
result_at: toIsoTimestamp(row.result_observed_at || row.result_ingested_at),
|
|
status: resultPayload.status || null,
|
|
result_code: resultPayload.result_code || null,
|
|
outcome_status:
|
|
outcomePayload.outcome_status
|
|
|| resultPayload.outcome_status
|
|
|| resultPayload.venue_outcome_status
|
|
|| resultPayload.trade_outcome_status
|
|
|| null,
|
|
outcome_reason:
|
|
outcomePayload.outcome_reason
|
|
|| resultPayload.outcome_reason
|
|
|| resultPayload.venue_outcome_reason
|
|
|| resultPayload.trade_outcome_reason
|
|
|| null,
|
|
outcome_source: outcomePayload.outcome_source || null,
|
|
attribution_status: outcomePayload.attribution_status || null,
|
|
attribution_method: outcomePayload.attribution_method || null,
|
|
attributed_inventory_delta: outcomePayload.attributed_inventory_delta || null,
|
|
outcome_payload: outcomePayload.quote_id ? outcomePayload : null,
|
|
venue_response: resultPayload.venue_response || null,
|
|
error_message: resultPayload.error?.message || null,
|
|
note: resultPayload.note || null,
|
|
};
|
|
}
|
|
|
|
function resolveTradeAmount(commandPayload, field) {
|
|
const quoteOutputField = commandPayload?.quote_output?.[field];
|
|
const proposedField = commandPayload?.[`proposed_${field}`];
|
|
return quoteOutputField || proposedField || commandPayload?.[field] || null;
|
|
}
|
|
|
|
function buildPair(assetIn, assetOut) {
|
|
if (!assetIn || !assetOut) return null;
|
|
return `${assetIn}->${assetOut}`;
|
|
}
|
|
|
|
function toIsoTimestamp(value) {
|
|
if (!value) return null;
|
|
const date = new Date(value);
|
|
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
}
|
|
|
|
function timestampValue(value) {
|
|
const parsed = Date.parse(value || '');
|
|
return Number.isFinite(parsed) ? parsed : -Infinity;
|
|
}
|
|
|
|
function buildDepositStatusRowKey(payload) {
|
|
const details = payload?.details || {};
|
|
return [
|
|
details.tx_hash || 'no-tx',
|
|
payload?.chain || details.chain || 'no-chain',
|
|
payload?.asset_id || details.asset_id || 'no-asset',
|
|
details.address || 'no-address',
|
|
details.amount || 'no-amount',
|
|
].join('|');
|
|
}
|
|
|
|
function normalizeDepositStatusRows(rows = []) {
|
|
const byKey = new Map();
|
|
|
|
for (const row of rows) {
|
|
const key = buildDepositStatusRowKey(row.payload);
|
|
const effectiveAt = depositStatusEffectiveAt(row);
|
|
const latestSortAt = timestampValue(row.observed_at || row.ingested_at);
|
|
const existing = byKey.get(key);
|
|
|
|
if (!existing) {
|
|
byKey.set(key, {
|
|
latest: row,
|
|
latestSortAt,
|
|
firstEffectiveAt: effectiveAt,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (latestSortAt > existing.latestSortAt) {
|
|
existing.latest = row;
|
|
existing.latestSortAt = latestSortAt;
|
|
}
|
|
const candidateEffectiveTs = timestampValue(effectiveAt);
|
|
const existingEffectiveTs = timestampValue(existing.firstEffectiveAt);
|
|
if (
|
|
Number.isFinite(candidateEffectiveTs)
|
|
&& (!Number.isFinite(existingEffectiveTs) || candidateEffectiveTs < existingEffectiveTs)
|
|
) {
|
|
existing.firstEffectiveAt = effectiveAt;
|
|
}
|
|
}
|
|
|
|
return [...byKey.values()].map(({ latest, firstEffectiveAt }) => {
|
|
const effectiveAt = firstEffectiveAt || toIsoTimestamp(latest.observed_at || latest.ingested_at);
|
|
return {
|
|
observed_at: effectiveAt,
|
|
ingested_at: toIsoTimestamp(latest.ingested_at),
|
|
payload: withDepositCreatedAt(latest.payload, effectiveAt),
|
|
};
|
|
});
|
|
}
|
|
|
|
function depositStatusEffectiveAt(row) {
|
|
return toIsoTimestamp(
|
|
row?.payload?.details?.created_at
|
|
|| row?.observed_at
|
|
|| row?.ingested_at,
|
|
);
|
|
}
|
|
|
|
function withDepositCreatedAt(payload, effectiveAt) {
|
|
if (!effectiveAt) return payload;
|
|
const details = payload?.details || {};
|
|
return {
|
|
...payload,
|
|
details: {
|
|
...details,
|
|
created_at: details.created_at || effectiveAt,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function ensureExpressionIndex(pool, { name, table, expression }) {
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS ${name}
|
|
ON ${table} (${expression})
|
|
`);
|
|
}
|