UNPKG

@bsv/overlay-express

Version:
109 lines (98 loc) 3.04 kB
import { BroadcastFailure, Broadcaster, BroadcastResponse, Transaction } from '@bsv/sdk' export interface NamedBroadcaster { name: string broadcaster: Broadcaster } const TERMINAL_CODES = new Set([ '400', '460', '463', '464', 'DOUBLE_SPEND_ATTEMPTED', 'REJECTED', 'INVALID', 'MALFORMED', 'MINED_IN_STALE_BLOCK' ]) function isTerminalBroadcastFailure (failure: BroadcastFailure): boolean { const code = failure.code.toUpperCase() const description = failure.description.toUpperCase() if (TERMINAL_CODES.has(code)) return true if (code.includes('ORPHAN') || description.includes('ORPHAN')) return true if ( typeof failure.more === 'object' && failure.more !== null && 'terminal' in failure.more && failure.more.terminal === true ) { return true } return false } /** * Tries transaction propagation providers in priority order. * * Transient provider failures fall through to the next provider. Terminal * validation states, including double spends, stop immediately so a conflicting * transaction is not retried against a second propagation surface. */ export class ProviderChainBroadcaster implements Broadcaster { constructor (private readonly providers: NamedBroadcaster[]) { if (providers.length === 0) { throw new TypeError('ProviderChainBroadcaster requires at least one provider') } } async broadcast (tx: Transaction): Promise<BroadcastResponse | BroadcastFailure> { const failures: Array<{ provider: string, failure: BroadcastFailure }> = [] for (const provider of this.providers) { let response: BroadcastResponse | BroadcastFailure try { response = await provider.broadcaster.broadcast(tx) } catch (error: unknown) { response = { status: 'error', code: '500', description: error instanceof Error ? error.message : 'Internal Server Error', more: { provider: provider.name, terminal: false } } } if (response.status === 'success') { return { ...response, message: `[${provider.name}] ${response.message}` } } failures.push({ provider: provider.name, failure: response }) if (isTerminalBroadcastFailure(response)) { return annotateFailure(response, provider.name, failures) } } const last = failures[failures.length - 1] return annotateFailure(last.failure, last.provider, failures) } } function annotateFailure ( failure: BroadcastFailure, provider: string, failures: Array<{ provider: string, failure: BroadcastFailure }> ): BroadcastFailure { return { ...failure, more: { ...(typeof failure.more === 'object' && failure.more !== null ? failure.more : {}), provider, providerFailures: failures.map(item => ({ provider: item.provider, code: item.failure.code, description: item.failure.description, txid: item.failure.txid, more: item.failure.more })) } } }