diff --git a/src/handlers/market.ts b/src/handlers/market.ts index 788f7c9..32012c8 100644 --- a/src/handlers/market.ts +++ b/src/handlers/market.ts @@ -6,7 +6,7 @@ import { type OrderbookIncrements, type OrderbookLevel, } from '../output/orderbook.js'; -import { ToolExecutionError } from '../utils/errors.js'; +import { ToolExecutionError, formatError } from '../utils/errors.js'; import { fmtProductIds } from '../utils/formatting.js'; import { resolveMarketData } from '../utils/orderBuilder.js'; import { getMarkets } from '../utils/resolveMarket.js'; @@ -17,13 +17,83 @@ import type { CommandResult } from './types.js'; // get_all_markets // --------------------------------------------------------------------------- +type MarketListKind = 'perp' | 'spot'; + +function resolveMarketListLabels( + productId: number, + kind: MarketListKind, + meta: { marketName: string; symbol: string } | undefined, + engineSymbol: string | undefined, +): { name: string; symbol: string } { + const metaSym = meta?.symbol?.trim(); + const metaName = meta?.marketName?.trim(); + const engSym = engineSymbol?.trim(); + + const symbol = metaSym || engSym || '-'; + const name = metaName || metaSym || engSym || `${kind} ${productId}`; + return { name, symbol }; +} + +/** + * Normalizes engine `getSymbols` payloads: some responses use a Record map, + * others may expose an array — avoids `symbols.map is not a function`. + */ +function engineSymbolMapFromResponse(symbolsRaw: unknown): Map { + const map = new Map(); + try { + const entries: unknown[] = Array.isArray(symbolsRaw) + ? symbolsRaw + : symbolsRaw && typeof symbolsRaw === 'object' + ? Object.values(symbolsRaw as Record) + : []; + for (const entry of entries) { + if (!entry || typeof entry !== 'object') continue; + const e = entry as { productId?: unknown; symbol?: unknown }; + const productId = e.productId; + const sym = typeof e.symbol === 'string' ? e.symbol.trim() : ''; + if (typeof productId === 'number' && sym) { + map.set(productId, sym); + } + } + } catch { + // ignore malformed payloads + } + return map; +} + export async function getAllMarkets(ctx: NadoContext): Promise { try { - const [data, metadata] = await Promise.all([ + const [marketsRes, metaRes, symsRes] = await Promise.allSettled([ ctx.client.market.getAllMarkets(), - getMarkets(ctx.dataEnv, ctx.chainEnv).catch(() => []), + getMarkets(ctx.dataEnv, ctx.chainEnv), + ctx.client.context.engineClient.getSymbols({}), ]); - const nameById = new Map(metadata.map((m) => [m.productId, m])); + + if (marketsRes.status === 'rejected') { + throw marketsRes.reason; + } + const data = [...marketsRes.value].sort( + (a, b) => a.productId - b.productId, + ); + + const metadata = metaRes.status === 'fulfilled' ? metaRes.value : []; + if (metaRes.status === 'rejected') { + console.warn( + `[nado] Product metadata unavailable: ${formatError(metaRes.reason)}. Name/Symbol may be incomplete.`, + ); + } + + const engineById = + symsRes.status === 'fulfilled' + ? engineSymbolMapFromResponse(symsRes.value.symbols) + : new Map(); + if (symsRes.status === 'rejected') { + console.warn( + `[nado] Engine symbols unavailable: ${formatError(symsRes.reason)}. Symbol column may rely on metadata only.`, + ); + } + + const metaById = new Map(metadata.map((m) => [m.productId, m])); return { data, @@ -36,12 +106,19 @@ export async function getAllMarkets(ctx: NadoContext): Promise { 'Size Increment', ], rows: data.map((m) => { - const meta = nameById.get(m.productId); + const kind: MarketListKind = + m.type === ProductEngineType.PERP ? 'perp' : 'spot'; + const { name, symbol } = resolveMarketListLabels( + m.productId, + kind, + metaById.get(m.productId), + engineById.get(m.productId), + ); return [ String(m.productId), - meta?.marketName ?? '-', - meta?.symbol ?? '-', - m.type === ProductEngineType.PERP ? 'perp' : 'spot', + name, + symbol, + kind, m.priceIncrement.toFixed(), m.sizeIncrement.toFixed(), ];