import * as TestApp from '../../../test/App.js' import * as TestFunding from '../../../test/Funding.js' import * as Transfer from './Transfer.js' const db = TestApp.database() const now = new Date('2026-01-01T00:00:00.000Z') beforeEach(() => vi.useFakeTimers({ now, toFake: ['Date'] })) afterEach(() => vi.useRealTimers()) function createInput(overrides: Partial = {}): Transfer.create.Input { return { apiKeyId: 'key_test', environment: 'production', orgId: 'org_test', snapshot: TestFunding.transferSnapshot(), ...overrides, } } describe('canTransition', () => { test('encodes the public state chart', () => { const table = Object.fromEntries( Transfer.statuses.map((from) => [ from, Transfer.statuses.filter((to) => Transfer.canTransition(from, to)), ]), ) expect(table).toMatchInlineSnapshot(` { "action-required": [ "processing", "refunding", "completed", "refunded", ], "awaiting-source": [ "processing", "action-required", "expired", ], "completed": [], "expired": [], "processing": [ "awaiting-source", "refunding", "completed", "action-required", ], "refunded": [], "refunding": [ "refunded", "action-required", ], } `) }) }) describe('isTerminal', () => { test('only chain-verified and expiry outcomes are terminal', () => { expect(Transfer.statuses.filter(Transfer.isTerminal)).toMatchInlineSnapshot(` [ "completed", "refunded", "expired", ] `) }) }) describe('generateId', () => { test('embeds the timestamp so lexical order is chronological', () => { const earlier = Transfer.generateId(new Date('2026-01-01T00:00:00.000Z')) const later = Transfer.generateId(new Date('2026-01-01T00:00:01.000Z')) expect(earlier).toMatch(/^ftr_\d{15}_[0-9A-Za-z]{24}$/) expect(earlier < later).toBe(true) }) }) describe('create', () => { test('creates the row and its version-1 event atomically', async () => { const record = await Transfer.create(db, createInput()) const { id, snapshot: _, ...rest } = record expect(id).toMatch(/^ftr_/) expect(rest).toMatchInlineSnapshot(` { "apiKeyId": "key_test", "createdAt": "2026-01-01T00:00:00.000Z", "environment": "production", "method": "transaction", "mode": "exactSource", "orgId": "org_test", "projectId": null, "providerId": "relay", "providerState": null, "quoteExpiresAt": "2026-01-01T00:01:00.000Z", "status": "awaiting-source", "statusReason": null, "updatedAt": "2026-01-01T00:00:00.000Z", "version": 1, } `) const events = await db.kysely .selectFrom('funding_transfer_events') .select(['status', 'transferId', 'version']) .where('transferId', '=', id) .execute() expect(events).toEqual([{ status: 'awaiting-source', transferId: id, version: 1 }]) }) test('normalizes quote expiry and strips executable actions before persistence', async () => { const snapshot = { ...TestFunding.transferSnapshot({ quote: { expiresAt: '2026-01-01T00:01:00Z', sampledAt: '2026-01-01T00:00:00.000Z' }, }), action: { type: 'evm:calls' }, } const record = await Transfer.create(db, createInput({ snapshot })) expect({ action: 'action' in record.snapshot, expiresAt: record.snapshot.quote.expiresAt, quoteExpiresAt: record.quoteExpiresAt, }).toEqual({ action: false, expiresAt: '2026-01-01T00:01:00.000Z', quoteExpiresAt: '2026-01-01T00:01:00.000Z', }) }) }) describe('registerSourceTransaction', () => { const transactionHash = `0x${'a2'.repeat(32)}` test('atomically assigns source evidence and starts reconciliation', async () => { const record = await Transfer.create( db, createInput({ providerState: { routeConfiguration: { destinationPoolAddress: `0x${'cc'.repeat(20)}`, poolAddress: `0x${'dd'.repeat(20)}`, sourceEid: 30_184, }, }, snapshot: TestFunding.transferSnapshot({ provider: { id: 'stargate', name: 'Stargate' } }), }), ) vi.setSystemTime(new Date('2026-01-01T00:00:10.000Z')) const result = await Transfer.registerSourceTransaction(db, { chainId: 'eip155:8453', id: record.id, providerState: { stargate: { guid: `0x${'bb'.repeat(32)}`, sourceEid: 30_184 } }, transactionHash: `0x${'a1'.repeat(32)}`, }) expect(result.type).toBe('registered') if (result.type !== 'registered') return expect({ providerState: result.record.providerState, sourceTransactionHashes: result.record.snapshot.sourceTransactionHashes, status: result.record.status, version: result.record.version, }).toMatchInlineSnapshot(` { "providerState": { "routeConfiguration": { "destinationPoolAddress": "0xcccccccccccccccccccccccccccccccccccccccc", "poolAddress": "0xdddddddddddddddddddddddddddddddddddddddd", "sourceEid": 30184, }, "stargate": { "guid": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "sourceEid": 30184, }, }, "sourceTransactionHashes": [ "0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", ], "status": "processing", "version": 2, } `) }) test('replays the same assignment and rejects reuse by another transfer', async () => { const first = await Transfer.create(db, createInput()) const second = await Transfer.create(db, createInput()) const options = { chainId: 'eip155:8453', id: first.id, providerState: { stargate: { guid: `0x${'bb'.repeat(32)}`, sourceEid: 30_184 } }, transactionHash, } await Transfer.registerSourceTransaction(db, options) const replayed = await Transfer.registerSourceTransaction(db, options) const conflict = await Transfer.registerSourceTransaction(db, { ...options, id: second.id }) expect({ conflict: conflict.type, replayed: replayed.type, replayedVersion: replayed.type === 'replayed' ? replayed.record.version : undefined, }).toEqual({ conflict: 'conflict', replayed: 'replayed', replayedVersion: 2 }) }) }) describe('reopenSourceTransaction', () => { test('removes invalidated source evidence and accepts a replacement', async () => { const record = await Transfer.create(db, createInput()) const transactionHash = `0x${'a4'.repeat(32)}` await Transfer.registerSourceTransaction(db, { chainId: 'eip155:8453', id: record.id, providerState: { stargate: { guid: `0x${'bb'.repeat(32)}`, sourceEid: 30_184 } }, transactionHash, }) const reopened = await Transfer.reopenSourceTransaction(db, { chainId: 'eip155:8453', id: record.id, providerState: { routeConfiguration: { poolAddress: `0x${'cc'.repeat(20)}` } }, transactionHash: transactionHash.toUpperCase(), }) expect(reopened.type).toBe('reopened') if (reopened.type !== 'reopened') return expect({ providerState: reopened.record.providerState, sourceTransactionHashes: reopened.record.snapshot.sourceTransactionHashes, status: reopened.record.status, version: reopened.record.version, }).toEqual({ providerState: { routeConfiguration: { poolAddress: `0x${'cc'.repeat(20)}` } }, sourceTransactionHashes: undefined, status: 'awaiting-source', version: 3, }) const replacement = await Transfer.registerSourceTransaction(db, { chainId: 'eip155:8453', id: record.id, providerState: { stargate: { guid: `0x${'dd'.repeat(32)}`, sourceEid: 30_184 } }, transactionHash: `0x${'a5'.repeat(32)}`, }) expect(replacement.type).toBe('registered') }) }) describe('complete', () => { test('records verified destination evidence and completes once', async () => { const record = await Transfer.create(db, createInput()) await Transfer.registerSourceTransaction(db, { chainId: 'eip155:8453', id: record.id, providerState: { stargate: { guid: `0x${'bb'.repeat(32)}`, sourceEid: 30_184 } }, transactionHash: `0x${'a3'.repeat(32)}`, }) const options = { chainId: 'eip155:4217', destinationAmount: TestFunding.transferSnapshot().destinationAmount!, id: record.id, transactionHash: `0x${'cc'.repeat(32)}`, } const completed = await Transfer.complete(db, options) const replayed = await Transfer.complete(db, options) expect({ completed: completed.type, destinationTransactionHashes: completed.type === 'completed' ? completed.record.snapshot.destinationTransactionHashes : undefined, replayed: replayed.type, status: completed.type === 'completed' ? completed.record.status : undefined, version: completed.type === 'completed' ? completed.record.version : undefined, }).toMatchInlineSnapshot(` { "completed": "completed", "destinationTransactionHashes": [ "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", ], "replayed": "replayed", "status": "completed", "version": 3, } `) }) }) describe('transition', () => { test('applies a guarded status change and appends an event', async () => { const record = await Transfer.create(db, createInput()) vi.setSystemTime(new Date('2026-01-01T00:00:10.000Z')) const result = await Transfer.transition(db, { expectedVersion: 1, id: record.id, status: 'processing', }) expect(result.type).toBe('applied') if (result.type !== 'applied') return expect({ status: result.record.status, updatedAt: result.record.updatedAt, version: result.record.version, }).toEqual({ status: 'processing', updatedAt: '2026-01-01T00:00:10.000Z', version: 2 }) const events = await db.kysely .selectFrom('funding_transfer_events') .select(['status', 'version']) .where('transferId', '=', record.id) .orderBy('version', 'asc') .execute() expect(events).toEqual([ { status: 'awaiting-source', version: 1 }, { status: 'processing', version: 2 }, ]) }) test('competing transitions converge on one version', async () => { const record = await Transfer.create(db, createInput()) const first = await Transfer.transition(db, { expectedVersion: 1, id: record.id, status: 'processing', }) const second = await Transfer.transition(db, { expectedVersion: 1, id: record.id, status: 'expired', }) expect([first.type, second.type]).toEqual(['applied', 'stale']) if (second.type !== 'stale') return expect(second.current.version).toBe(2) }) test('rejects transitions the lifecycle does not allow', async () => { const record = await Transfer.create(db, createInput()) await Transfer.transition(db, { expectedVersion: 1, id: record.id, status: 'expired' }) const result = await Transfer.transition(db, { expectedVersion: 2, id: record.id, status: 'processing', }) expect(result.type).toBe('invalid') }) test('carries a customer-safe status reason', async () => { const record = await Transfer.create(db, createInput()) await Transfer.transition(db, { expectedVersion: 1, id: record.id, status: 'processing' }) const result = await Transfer.transition(db, { expectedVersion: 2, id: record.id, status: 'action-required', statusReason: { code: 'destination_amount_below_minimum', message: 'The verified destination amount did not satisfy the quoted minimum.', }, }) expect(result.type).toBe('applied') if (result.type !== 'applied') return expect(result.record.statusReason).toMatchInlineSnapshot(` { "code": "destination_amount_below_minimum", "message": "The verified destination amount did not satisfy the quoted minimum.", } `) }) test('preserves the reason for snapshot updates and clears it on lifecycle changes', async () => { const record = await Transfer.create(db, createInput()) await Transfer.transition(db, { expectedVersion: 1, id: record.id, status: 'processing' }) await Transfer.transition(db, { expectedVersion: 2, id: record.id, status: 'action-required', statusReason: { code: 'destination_amount_below_minimum', message: 'The verified destination amount did not satisfy the quoted minimum.', }, }) const snapshot = await Transfer.transition(db, { expectedVersion: 3, id: record.id, snapshot: TestFunding.transferSnapshot({ sourceTransactionHashes: [`0x${'aa'.repeat(32)}`], }), }) expect(snapshot.type).toBe('applied') if (snapshot.type !== 'applied') return expect(snapshot.record.statusReason?.code).toBe('destination_amount_below_minimum') const processing = await Transfer.transition(db, { expectedVersion: 4, id: record.id, status: 'processing', }) expect(processing.type).toBe('applied') if (processing.type !== 'applied') return expect(processing.record.statusReason).toBeNull() }) test('updates the snapshot without a status change', async () => { const record = await Transfer.create(db, createInput()) const result = await Transfer.transition(db, { expectedVersion: 1, id: record.id, snapshot: TestFunding.transferSnapshot({ sourceTransactionHashes: [`0x${'aa'.repeat(32)}`], }), }) expect(result.type).toBe('applied') if (result.type !== 'applied') return expect({ hashes: result.record.snapshot.sourceTransactionHashes, status: result.record.status, version: result.record.version, }).toEqual({ hashes: [`0x${'aa'.repeat(32)}`], status: 'awaiting-source', version: 2, }) }) test('strips executable actions from replacement snapshots', async () => { const record = await Transfer.create(db, createInput()) const snapshot = { ...TestFunding.transferSnapshot(), action: { type: 'evm:calls' }, } const result = await Transfer.transition(db, { expectedVersion: 1, id: record.id, snapshot, }) expect(result.type).toBe('applied') if (result.type !== 'applied') return expect('action' in result.record.snapshot).toBe(false) }) test('reports a missing transfer', async () => { const result = await Transfer.transition(db, { expectedVersion: 1, id: 'ftr_000000000000000_000000000000000000000000', status: 'processing', }) expect(result.type).toBe('not_found') }) }) describe('toPublic', () => { test('serializes the snapshot with lifecycle fields and no ownership columns', async () => { const record = await Transfer.create(db, createInput({ projectId: 'prj_test' })) const { id, ...rest } = Transfer.toPublic(record) expect(id).toMatch(/^ftr_/) expect(rest).toMatchInlineSnapshot(` { "createdAt": "2026-01-01T00:00:00.000Z", "destinationAmount": { "baseUnits": "979295", "currency": "USD", "decimals": 6, "formatted": "0.979295", }, "destinationAmountMin": { "baseUnits": "939242", "currency": "USD", "decimals": 6, "formatted": "0.939242", }, "destinationChain": { "addressFormat": "hex", "id": "eip155:4217", "kind": "evm", "name": "Tempo", }, "destinationToken": { "address": "0x20c000000000000000000000b9537d11c60e8b50", "currency": "USD", "decimals": 6, "name": "Bridged USDC (Stargate)", "standard": "TIP-20", "symbol": "USDC.e", "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50", "verified": true, }, "fees": [], "method": "transaction", "mode": "exactSource", "provider": { "id": "relay", "name": "Relay", }, "quote": { "expiresAt": "2026-01-01T00:01:00.000Z", "sampledAt": "2026-01-01T00:00:00.000Z", }, "recipient": "0x1111111111111111111111111111111111111111", "sender": "0x2222222222222222222222222222222222222222", "sourceAmount": { "baseUnits": "1000000", "currency": "USD", "decimals": 6, "formatted": "1", }, "sourceChain": { "addressFormat": "hex", "id": "eip155:8453", "kind": "evm", "name": "Base", }, "sourceToken": { "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "currency": "USD", "decimals": 6, "name": "USD Coin", "standard": "ERC-20", "symbol": "USDC", "tokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "verified": true, }, "status": "awaiting-source", "updatedAt": "2026-01-01T00:00:00.000Z", "version": 1, } `) }) })