From 4bf15be22ebb3c6b491792e590cc2b9961c293ca Mon Sep 17 00:00:00 2001 From: philipp Date: Wed, 13 May 2026 15:43:37 +0200 Subject: [PATCH] Keep funding addresses refreshing without token list Proof: supported_tokens bridge RPC failures no longer abort liquidity-manager deposit address refresh; regression tests cover the non-fatal warning path. Assumptions: deposit handles remain chain-level NEAR Intents bridge data and Gnosis assets share the Gnosis handle when the bridge deposit_address RPC succeeds. Still fake: USDC deposits are not proven credited yet; supported_tokens is still unavailable upstream until the bridge RPC responds successfully. --- src/apps/liquidity-manager.mjs | 18 ++++----- src/core/liquidity-state.mjs | 1 + src/core/liquidity-supported-tokens.mjs | 34 ++++++++++++++++ src/core/service-snapshot-summary.mjs | 1 + test/liquidity-state.test.mjs | 1 + test/liquidity-supported-tokens.test.mjs | 50 ++++++++++++++++++++++++ 6 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 src/core/liquidity-supported-tokens.mjs create mode 100644 test/liquidity-supported-tokens.test.mjs diff --git a/src/apps/liquidity-manager.mjs b/src/apps/liquidity-manager.mjs index 79403e0..54d2876 100644 --- a/src/apps/liquidity-manager.mjs +++ b/src/apps/liquidity-manager.mjs @@ -18,6 +18,7 @@ import { } from '../core/funding-observations.mjs'; import { createJsonStateStore } from '../core/json-state-store.mjs'; import { normalizeLiquidityState } from '../core/liquidity-state.mjs'; +import { refreshSupportedTokens } from '../core/liquidity-supported-tokens.mjs'; import { buildBridgeWithdrawalPlan } from '../core/liquidity-withdrawals.mjs'; import { createLogger, serializeError } from '../core/log.mjs'; import { assertFundingObservationEvent, assertLiquidityActionEvent } from '../core/schemas.mjs'; @@ -133,8 +134,12 @@ async function refresh() { if (state.paused) return; try { - const supported = await bridgeClient.supportedTokens({ chains }); - state.supported_tokens = mapSupportedTokens(supported?.tokens || []); + await refreshSupportedTokens({ + bridgeClient, + chains, + state, + logger, + }); for (const chain of chains) { await refreshChain(chain, state); @@ -816,15 +821,6 @@ async function shutdown() { process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); -function mapSupportedTokens(tokens) { - return Object.fromEntries( - tokens.map((token) => [ - `${token.near_token_id}:${token.defuse_asset_identifier}`, - token, - ]), - ); -} - function mapDepositAssetId(deposit, chain) { return bridgeDepositAssetId(deposit, { assetRegistry: runtimeConfig.assetRegistry, diff --git a/src/core/liquidity-state.mjs b/src/core/liquidity-state.mjs index d6bcfee..d913870 100644 --- a/src/core/liquidity-state.mjs +++ b/src/core/liquidity-state.mjs @@ -3,6 +3,7 @@ export function normalizeLiquidityState(state, { withdrawalsFrozen }) { state.deposits ||= {}; state.tracked_withdrawals ||= {}; state.supported_tokens ||= {}; + state.supported_tokens_error ??= null; state.funding_observations ||= {}; state.funding_observations_by_handle ||= {}; state.funding_visibility_by_asset ||= {}; diff --git a/src/core/liquidity-supported-tokens.mjs b/src/core/liquidity-supported-tokens.mjs new file mode 100644 index 0000000..9555567 --- /dev/null +++ b/src/core/liquidity-supported-tokens.mjs @@ -0,0 +1,34 @@ +import { serializeError } from './log.mjs'; + +export function mapSupportedTokens(tokens = []) { + return Object.fromEntries( + tokens.map((token) => [ + `${token.near_token_id}:${token.defuse_asset_identifier}`, + token, + ]), + ); +} + +export async function refreshSupportedTokens({ + bridgeClient, + chains, + state, + logger = null, +}) { + try { + const supported = await bridgeClient.supportedTokens({ chains }); + state.supported_tokens = mapSupportedTokens(supported?.tokens || []); + state.supported_tokens_error = null; + return { ok: true, token_count: Object.keys(state.supported_tokens).length }; + } catch (error) { + const serialized = serializeError(error); + state.supported_tokens_error = serialized; + logger?.warn?.('supported_tokens_refresh_failed', { + details: { + chains, + error: serialized, + }, + }); + return { ok: false, error: serialized }; + } +} diff --git a/src/core/service-snapshot-summary.mjs b/src/core/service-snapshot-summary.mjs index c297fee..236f0d8 100644 --- a/src/core/service-snapshot-summary.mjs +++ b/src/core/service-snapshot-summary.mjs @@ -62,6 +62,7 @@ export function summarizeServiceState(service, state) { 'funding_observer_paused', 'withdrawals_frozen', 'deposit_addresses', + 'supported_tokens_error', 'withdrawal_defaults', 'latest_funding_observation_at', 'last_refresh_at', diff --git a/test/liquidity-state.test.mjs b/test/liquidity-state.test.mjs index 92ec75c..9db3838 100644 --- a/test/liquidity-state.test.mjs +++ b/test/liquidity-state.test.mjs @@ -16,6 +16,7 @@ test('normalizeLiquidityState hydrates missing nested maps from persisted partia assert.deepEqual(state.deposits, {}); assert.deepEqual(state.tracked_withdrawals, {}); assert.deepEqual(state.supported_tokens, {}); + assert.equal(state.supported_tokens_error, null); assert.deepEqual(state.funding_observations, {}); assert.deepEqual(state.funding_observations_by_handle, {}); assert.deepEqual(state.funding_visibility_by_asset, {}); diff --git a/test/liquidity-supported-tokens.test.mjs b/test/liquidity-supported-tokens.test.mjs new file mode 100644 index 0000000..0ff30aa --- /dev/null +++ b/test/liquidity-supported-tokens.test.mjs @@ -0,0 +1,50 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + mapSupportedTokens, + refreshSupportedTokens, +} from '../src/core/liquidity-supported-tokens.mjs'; + +test('mapSupportedTokens indexes bridge tokens by near and intents identifiers', () => { + const mapped = mapSupportedTokens([ + { + near_token_id: 'token.near', + defuse_asset_identifier: 'nep141:token.near', + decimals: 18, + }, + ]); + + assert.deepEqual(Object.keys(mapped), ['token.near:nep141:token.near']); + assert.equal(mapped['token.near:nep141:token.near'].decimals, 18); +}); + +test('refreshSupportedTokens records warning state without throwing', async () => { + const state = { + supported_tokens: { + previous: { near_token_id: 'previous' }, + }, + }; + const warnings = []; + + const result = await refreshSupportedTokens({ + bridgeClient: { + async supportedTokens() { + throw new Error('Bridge RPC supported_tokens failed'); + }, + }, + chains: ['btc', 'gnosis'], + state, + logger: { + warn(event, fields) { + warnings.push({ event, fields }); + }, + }, + }); + + assert.equal(result.ok, false); + assert.equal(state.supported_tokens.previous.near_token_id, 'previous'); + assert.equal(state.supported_tokens_error.message, 'Bridge RPC supported_tokens failed'); + assert.equal(warnings[0].event, 'supported_tokens_refresh_failed'); + assert.deepEqual(warnings[0].fields.details.chains, ['btc', 'gnosis']); +});