@bsv/overlay-express
Version:
BSV Blockchain Overlay Express
109 lines (98 loc) • 3.04 kB
text/typescript
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
}))
}
}
}