import * as NodeBuffer from 'node:buffer' import { AbiEvent, AbiParameters, Hex } from 'ox' import * as TestApp from '../../../test/App.js' import * as TestRelay from '../../../test/Relay.js' import * as TestRoutes from '../../../test/Routes.js' import * as Metrics from '../../Metrics.js' import * as RoutesCatalog from '../../db/tables/routesCatalog.js' import * as RoutesDepositAddresses from '../../db/tables/routesDepositAddresses.js' import * as RoutesDeposits from '../../db/tables/routesDeposits.js' import * as Webhooks from '../Webhooks.js' import * as Chain from './Chain.js' import * as DepositAddress from './DepositAddress.js' import * as Evidence from './Evidence.js' import * as Reconciliation from './Reconciliation.js' import * as SourceObservation from './SourceObservation.js' import * as Relay from './providers/relay.js' const db = TestApp.database() describe('createTracker', () => { test('bounds dust work, advances after failures, revisits old rows, and accepts late attribution', async () => { const route = TestRoutes.transferSnapshot() const address = await DepositAddress.create(db, { deliveryStrategy: 'provider', environment: 'production', now: new Date('2026-01-01T00:00:00.000Z'), orgId: 'org_unattributed', providerOutputToken: route.destinationToken, snapshot: { ...TestRoutes.depositAddressSnapshot(), address: `0x${'44'.repeat(20)}`, destinationChain: route.destinationChain, destinationToken: route.destinationToken, refundAddress: `0x${'55'.repeat(20)}`, sourceChain: route.sourceChain, sourceToken: route.sourceToken, }, }) const seed = async (index: number, now: Date) => { const observed = await SourceObservation.observe(db, { addressId: address.id, amount: index === 59 ? '1000000' : '1', chainId: route.sourceChain.id, now, recipient: address.address, sender: address.refundAddress, tokenAddress: route.sourceToken.address, transactionHash: Hex.fromNumber(index + 1, { size: 32 }), transferIndex: 0, }) if (observed.type === 'ignored') throw new Error('Expected a source deposit.') return observed.record } const deposits = [] for (let index = 0; index < 60; index++) deposits.push(await seed(index, new Date('2026-01-01T00:01:00.000Z'))) deposits.sort((left, right) => (left.id < right.id ? -1 : 1)) const reads: string[] = [] const deliveries: Webhooks.QueueReference[] = [] const counts: Parameters[] = [] let failedHash: string | undefined let attributedHash: string | undefined let now = new Date('2026-01-01T00:02:00.000Z') const destinationHash = `0x${'ff'.repeat(32)}` const server = await TestRelay.createServer(async (request, response) => { response.setHeader('content-type', 'application/json') if (request.method === 'GET') { response.end( JSON.stringify({ requests: attributedHash ? [ { createdAt: '2026-01-01T00:02:00.000Z', data: { inTxs: [], outTxs: [{ txHash: destinationHash }] }, depositAddress: { depositTxHash: attributedHash }, id: 'late-request', status: 'success', updatedAt: '2026-01-01T00:03:00.000Z', }, ] : [], }), ) return } const chunks: Uint8Array[] = [] for await (const chunk of request) chunks.push(chunk) const body = JSON.parse(NodeBuffer.Buffer.concat(chunks).toString()) as { id: number method: string params?: string[] | undefined } const hash = body.params?.[0] if (body.method === 'eth_getTransactionReceipt') reads.push(hash!) if (body.method === 'eth_getTransactionReceipt' && hash === failedHash) { response.end( JSON.stringify({ error: { code: -32000, message: 'Receipt unavailable' }, id: body.id, jsonrpc: '2.0', }), ) return } const destination = hash === destinationHash const amount = destination || hash === Hex.fromNumber(60, { size: 32 }) ? 1_000_000n : 1n const result = body.method === 'eth_blockNumber' ? '0x10' : { blockNumber: '0x10', logs: [ { address: destination ? route.destinationToken.address : route.sourceToken.address, data: Hex.fromNumber(amount, { size: 32 }), logIndex: '0x0', topics: [ AbiEvent.getSelector('event Transfer(address,address,uint256)'), AbiParameters.encode([{ type: 'address' }], [address.refundAddress as Hex.Hex]), AbiParameters.encode( [{ type: 'address' }], [(destination ? address.recipient : address.address) as Hex.Hex], ), ], }, ], status: '0x1', } response.end(JSON.stringify({ id: body.id, jsonrpc: '2.0', result })) }) try { await RoutesCatalog.publish(db, { chainTokens: [], chains: [route.sourceChain, route.destinationChain].map((chain) => ({ aliases: [], id: chain.id as Chain.Id, name: chain.name, parentChainId: null, rpcUrls: [server.url], slug: chain.name.toLowerCase(), })), routes: [], tokens: [], }) await Webhooks.createSubscription(db, { chainId: 4217, destination: { type: 'url', url: 'https://example.com/webhooks' }, environment: 'production', eventType: 'routes:deposit.updated', filters: {}, owner: { orgId: address.orgId, type: 'api_key' }, }) const tracker = () => Reconciliation.createTracker({ db, dispatch: async () => undefined, dispatchDepositUpdates: async (references) => { deliveries.push(...references) return references.length }, metrics: Metrics.from({ count: (...parameters) => { counts.push(parameters) }, flush() {}, gauge() {}, histogram() {}, }), now: () => now, providers: [Relay.relay({ apiKey: 'test-key', baseUrl: server.url })], verifyTransfers: Evidence.createVerifier({ db }), }) const reconcile = () => tracker().reconcile({ addressId: address.id, trigger: 'poll', type: 'routes:deposit-address:reconcile', }) expect(await reconcile()).toMatchObject({ type: 'completed', updated: 25 }) expect(reads.sort()).toEqual( deposits .slice(0, 25) .map((deposit) => deposit.sourceTransactionHash) .sort(), ) expect(deliveries).toHaveLength(25) const first = await RoutesDeposits.get(db, deposits[0]!.id) const newcomer = await seed(60, new Date('2026-01-01T00:02:01.000Z')) now = new Date('2026-01-01T00:03:00.000Z') reads.length = 0 failedHash = deposits[25]!.sourceTransactionHash await expect(reconcile()).rejects.toThrow('Unattributed deposit reconciliation failed.') expect(new Set(reads)).toEqual( new Set(deposits.slice(25, 50).map((deposit) => deposit.sourceTransactionHash)), ) expect(counts).toContainEqual([ 'routes_deposit_reconciliation_count', 1, { outcome: 'failed', provider: 'relay', trigger: 'poll' }, ]) failedHash = undefined reads.length = 0 expect(await reconcile()).toMatchObject({ type: 'completed', updated: 10 }) expect(reads.sort()).toEqual( deposits .slice(50) .map((deposit) => deposit.sourceTransactionHash) .sort(), ) expect((await RoutesDeposits.get(db, newcomer.id))?.status).toBe('detected') reads.length = 0 expect(await reconcile()).toMatchObject({ type: 'completed', updated: 0 }) expect(reads.sort()).toEqual( deposits .slice(0, 25) .map((deposit) => deposit.sourceTransactionHash) .sort(), ) expect(await RoutesDeposits.get(db, deposits[0]!.id)).toEqual(first) expect(deliveries).toHaveLength(59) expect(await reconcile()).toMatchObject({ type: 'completed', updated: 1 }) expect(await reconcile()).toMatchObject({ type: 'completed', updated: 1 }) expect((await RoutesDeposits.get(db, newcomer.id))?.status).toBe('bridging') attributedHash = Hex.fromNumber(60, { size: 32 }) await reconcile() const legitimate = deposits.find( (deposit) => deposit.sourceTransactionHash === attributedHash, )! expect(await RoutesDeposits.get(db, legitimate.id)).toMatchObject({ providerRequestId: 'late-request', snapshot: { destinationAmount: { baseUnits: '1000000' } }, status: 'completed', }) expect( await RoutesDeposits.listByAddress( db, { environment: 'production', orgId: address.orgId }, address.id, { limit: 100 }, ), ).toHaveLength(61) expect(await RoutesDepositAddresses.get(db, address.id)).toMatchObject({ pollLeaseUntil: null, }) } finally { await server.closeAsync() } }) })