@bsv/overlay-express
Version:
BSV Blockchain Overlay Express
124 lines (110 loc) • 4.2 kB
text/typescript
import { ArcadeProvider } from '../ArcadeProvider.js'
import { ChaintracksProvider } from '../ChaintracksProvider.js'
import { ProviderChainBroadcaster } from '../ProviderChainBroadcaster.js'
describe('Overlay transaction providers', () => {
const tx = {
id: jest.fn(() => '11'.repeat(32)),
toHexEF: jest.fn(() => 'efhex'),
toHex: jest.fn(() => 'rawhex')
} as any
beforeEach(() => {
jest.clearAllMocks()
})
it('tries the next broadcaster after a transient provider failure', async () => {
const first = {
broadcast: jest.fn(async () => ({
status: 'error' as const,
code: '503',
description: 'backpressure',
more: { terminal: false }
}))
}
const second = {
broadcast: jest.fn(async () => ({
status: 'success' as const,
txid: tx.id(),
message: 'accepted'
}))
}
const chain = new ProviderChainBroadcaster([
{ name: 'Arcade', broadcaster: first },
{ name: 'ARC', broadcaster: second }
])
const response = await chain.broadcast(tx)
expect(response.status).toBe('success')
expect(first.broadcast).toHaveBeenCalledTimes(1)
expect(second.broadcast).toHaveBeenCalledTimes(1)
})
it('stops on terminal double-spend failures', async () => {
const first = {
broadcast: jest.fn(async () => ({
status: 'error' as const,
code: 'DOUBLE_SPEND_ATTEMPTED',
description: 'double spend',
more: { terminal: true, competingTxs: ['22'.repeat(32)] }
}))
}
const second = {
broadcast: jest.fn(async () => ({
status: 'success' as const,
txid: tx.id(),
message: 'accepted'
}))
}
const chain = new ProviderChainBroadcaster([
{ name: 'Arcade', broadcaster: first },
{ name: 'ARC', broadcaster: second }
])
const response = await chain.broadcast(tx)
expect(response.status).toBe('error')
if (response.status === 'error') {
expect(response.code).toBe('DOUBLE_SPEND_ATTEMPTED')
}
expect(first.broadcast).toHaveBeenCalledTimes(1)
expect(second.broadcast).not.toHaveBeenCalled()
})
it('classifies Arcade double-spend status as a broadcast failure', async () => {
const requests: Array<{ url: string, init?: RequestInit }> = []
const fetcher = jest.fn(async (url: string, init?: RequestInit) => {
requests.push({ url, init })
return new Response(JSON.stringify({
txid: tx.id(),
txStatus: 'DOUBLE_SPEND_ATTEMPTED',
competingTxs: ['22'.repeat(32)]
}), { status: 202 })
}) as any
const arcade = new ArcadeProvider('https://arcade.example/', {
apiKey: 'secret',
callbackUrl: 'https://overlay.example/arc-ingest',
callbackToken: 'callback',
fetch: fetcher
})
const response = await arcade.broadcast(tx)
expect(response.status).toBe('error')
if (response.status === 'error') {
expect(response.code).toBe('DOUBLE_SPEND_ATTEMPTED')
}
expect(requests[0].url).toBe('https://arcade.example/tx')
expect((requests[0].init?.headers as Record<string, string>).Authorization).toBe('Bearer secret')
})
it('validates merkle roots through go-chaintracks headers', async () => {
const fetcher = jest.fn(async (url: string) => {
if (url.endsWith('/height')) {
return new Response(JSON.stringify({ height: 900000 }), { status: 200 })
}
if (url.endsWith('/header/height/900000')) {
return new Response(JSON.stringify({
height: 900000,
hash: 'aa'.repeat(32),
merkleRoot: 'bb'.repeat(32)
}), { status: 200 })
}
return new Response('', { status: 404 })
}) as any
const chaintracks = new ChaintracksProvider('https://arcade.example', { fetch: fetcher })
await expect(chaintracks.currentHeight()).resolves.toBe(900000)
await expect(chaintracks.isValidRootForHeight('bb'.repeat(32), 900000)).resolves.toBe(true)
await expect(chaintracks.isValidRootForHeight('cc'.repeat(32), 900000)).resolves.toBe(false)
expect(chaintracks.reorgStreamUrl()).toBe('https://arcade.example/chaintracks/v2/reorg/stream')
})
})