/** * LiFi status polling: domain-specific tick (DONE / FAILED / errors / indexer lag) * composed with the shared {@link pollLoop} engine — no EVM receipt rules here. */ import { sdkLogger } from '@portal-hq/utils' import type { LifiStatusRawResponse, LifiStatusRequest, LifiStatusResponse, } from '../../../types' import { pollLoop, PollLoopTimeoutError, type PollTickResult, } from '../../internal/pollLoop' import type { IPortalLifiTradingApi } from './PortalLifiTradingApi' const LOG_PREFIX = '[Lifi.pollStatus]' /** * Whitelist of LiFi / backend messages that indicate the transfer indexer is not ready yet. * Avoid broad substrings (e.g. "400") so unrelated client or validation errors fail fast via * the consecutive-error path instead of spinning until timeout. */ const INDEXER_LAG_MESSAGE_SNIPPETS = [ 'not an evm transaction', 'no transfer information found', 'transfer not found', ] as const export function isLikelyLifiIndexerNotReadyMessage(message: string): boolean { const m = message.toLowerCase() return INDEXER_LAG_MESSAGE_SNIPPETS.some((s) => m.includes(s)) } export interface PollLifiUntilTerminalParams { getStatus: IPortalLifiTradingApi['getStatus'] request: Pick onUpdate?: (raw: LifiStatusRawResponse) => boolean | void everyMs: number initialDelayMs: number timeoutMs: number maxConsecutiveErrors: number backoff?: { factor: number; maxIntervalMs: number } } /** * Poll LiFi transfer status until DONE, FAILED, callback requests stop, timeout, or too many errors. */ export async function pollLifiStatusUntilTerminal( params: PollLifiUntilTerminalParams, ): Promise { let consecutiveHardErrors = 0 try { return await pollLoop({ tick: async (): Promise> => { return lifiStatusPollTick(params, { get: () => consecutiveHardErrors, set: (n: number) => { consecutiveHardErrors = n }, }) }, intervalMs: params.everyMs, initialDelayMs: params.initialDelayMs, backoff: params.backoff, timeoutMs: params.timeoutMs, }) } catch (error) { if (error instanceof PollLoopTimeoutError) { throw new Error( `${LOG_PREFIX} timed out after ${params.timeoutMs}ms waiting for LiFi terminal status`, ) } throw error } } async function lifiStatusPollTick( params: PollLifiUntilTerminalParams, counter: { get: () => number; set: (n: number) => void }, ): Promise> { try { const res: LifiStatusResponse = await params.getStatus(params.request) if (res.error) { sdkLogger.warn(`${LOG_PREFIX} response error field`, res.error) return bumpHardError(counter, params.maxConsecutiveErrors) } counter.set(0) const raw = res.data?.rawResponse if (!raw) { return { kind: 'continue' } } const { status } = raw if (status === 'DONE') { return { kind: 'stop', value: raw } } if (status === 'FAILED') { const detail = raw.substatusMessage ?? raw.substatus ?? 'LiFi transfer FAILED' return { kind: 'throw', error: new Error(`${LOG_PREFIX} ${detail}`), } } if (params.onUpdate) { const carryOn = params.onUpdate(raw) if (carryOn === false) { return { kind: 'stop', value: raw } } } return { kind: 'continue' } } catch (error) { const message = error instanceof Error ? error.message : String(error) if (isLikelyLifiIndexerNotReadyMessage(message)) { sdkLogger.debug(`${LOG_PREFIX} indexer not ready, will retry`, message) counter.set(0) return { kind: 'continue' } } sdkLogger.warn(`${LOG_PREFIX} getStatus error`, error) return bumpHardError(counter, params.maxConsecutiveErrors) } } function bumpHardError( counter: { get: () => number; set: (n: number) => void }, max: number, ): PollTickResult { const next = counter.get() + 1 counter.set(next) if (next >= max) { return { kind: 'throw', error: new Error( `${LOG_PREFIX} too many consecutive getStatus failures (${max})`, ), } } return { kind: 'continue' } }