import { describe, expect, it } from 'vitest'; import { reduce, type EngineConfig, type InvoiceSnapshot, type Observation, type OptionSnapshot, } from '../src/statemachine'; const CFG: EngineConfig = { confirmationThreshold: { base: 30, tron: 19 } }; /** 100 USDT in base units. */ const USDT_100 = 100_000000n; const EXPIRES = 1_000_000; // epoch ms const T0 = 0; function option(overrides: Partial = {}): OptionSnapshot { return { id: 'opt-tron', chain: 'tron', asset: 'USDT', requiredAmount: USDT_100, createdAt: T0, ...overrides, }; } function invoice(overrides: Partial = {}): InvoiceSnapshot { return { id: 'inv-1', status: 'created', toleranceBp: 100, expiresAt: EXPIRES, paidViaOptionId: null, overpaidAmount: null, options: [option()], payments: [], ...overrides, }; } function payment(overrides: Partial> = {}): Observation { return { kind: 'payment', txid: 'tx-1', optionId: 'opt-tron', amount: USDT_100, confirmations: 0, inclusion: 'mempool', at: T0 + 1000, ...overrides, }; } const tick = (at: number): Observation => ({ kind: 'tick', at }); describe('created → detected', () => { it('moves to detected on a mempool observation before expiry and emits invoice.detected', () => { const { invoice: inv, events } = reduce(invoice(), payment(), CFG); expect(inv.status).toBe('detected'); expect(events).toEqual([ { type: 'invoice.detected', invoiceId: 'inv-1', optionId: 'opt-tron', txid: 'tx-1' }, ]); }); it('records the payment row on detection', () => { const { invoice: inv } = reduce(invoice(), payment(), CFG); expect(inv.payments).toHaveLength(1); expect(inv.payments[0]).toMatchObject({ txid: 'tx-1', amount: USDT_100, status: 'mempool' }); }); it('jumps created → confirming when the first observation is already in a block, emitting both events', () => { const { invoice: inv, events } = reduce( invoice(), payment({ inclusion: 'included', confirmations: 3 }), CFG, ); expect(inv.status).toBe('confirming'); expect(events.map((e) => e.type)).toEqual(['invoice.detected', 'invoice.confirming']); }); }); describe('detected → confirming', () => { it('moves to confirming when the tx is included below threshold', () => { let state = reduce(invoice(), payment(), CFG).invoice; const { invoice: inv, events } = reduce(state, payment({ inclusion: 'included', confirmations: 1 }), CFG); expect(inv.status).toBe('confirming'); expect(events).toEqual([ { type: 'invoice.confirming', invoiceId: 'inv-1', optionId: 'opt-tron', txid: 'tx-1', confirmations: 1 }, ]); expect(inv.payments).toHaveLength(1); // same txid updated, not duplicated }); it('stays confirming at threshold − 1 confirmations with no events', () => { let state = reduce(invoice(), payment({ inclusion: 'included', confirmations: 1 }), CFG).invoice; const { invoice: inv, events } = reduce(state, payment({ inclusion: 'included', confirmations: 18 }), CFG); expect(inv.status).toBe('confirming'); expect(events).toEqual([]); }); }); describe('confirming → paid', () => { it('pays at exactly the confirmation threshold with the full amount', () => { let state = reduce(invoice(), payment({ inclusion: 'included', confirmations: 1 }), CFG).invoice; const { invoice: inv, events } = reduce(state, payment({ inclusion: 'included', confirmations: 19 }), CFG); expect(inv.status).toBe('paid'); expect(inv.paidViaOptionId).toBe('opt-tron'); expect(events).toEqual([ { type: 'invoice.paid', invoiceId: 'inv-1', optionId: 'opt-tron', amountConfirmed: USDT_100 }, ]); }); it('respects the per-chain threshold (base = 30)', () => { const inv0 = invoice({ options: [option({ id: 'opt-base', chain: 'base', asset: 'USDC' })] }); const p = payment({ optionId: 'opt-base', inclusion: 'included' }); let state = reduce(inv0, { ...p, confirmations: 29 } as Observation, CFG).invoice; expect(state.status).toBe('confirming'); state = reduce(state, { ...p, confirmations: 30 } as Observation, CFG).invoice; expect(state.status).toBe('paid'); }); }); describe('tolerance edges', () => { it('pays at exactly the tolerance floor (1% of 100 USDT → 99 USDT)', () => { const { invoice: inv } = reduce( invoice(), payment({ amount: 99_000000n, inclusion: 'included', confirmations: 19 }), CFG, ); expect(inv.status).toBe('paid'); }); it('does NOT pay one base unit below the floor', () => { const { invoice: inv, events } = reduce( invoice(), payment({ amount: 98_999999n, inclusion: 'included', confirmations: 19 }), CFG, ); expect(inv.status).toBe('confirming'); expect(events.some((e) => e.type === 'invoice.paid')).toBe(false); }); it('rounds a fractional floor UP (required 33.333333, 1% → floor 33.000000)', () => { const inv0 = invoice({ options: [option({ requiredAmount: 33_333333n })] }); const below = reduce(inv0, payment({ amount: 32_999999n, inclusion: 'included', confirmations: 19 }), CFG); expect(below.invoice.status).toBe('confirming'); const at = reduce(inv0, payment({ amount: 33_000000n, inclusion: 'included', confirmations: 19 }), CFG); expect(at.invoice.status).toBe('paid'); }); it('zero tolerance requires the exact amount', () => { const inv0 = invoice({ toleranceBp: 0 }); const short = reduce(inv0, payment({ amount: USDT_100 - 1n, inclusion: 'included', confirmations: 19 }), CFG); expect(short.invoice.status).toBe('confirming'); const exact = reduce(inv0, payment({ amount: USDT_100, inclusion: 'included', confirmations: 19 }), CFG); expect(exact.invoice.status).toBe('paid'); }); }); describe('overpayment', () => { it('marks paid and records the surplus, emitting invoice.paid then invoice.overpaid', () => { const { invoice: inv, events } = reduce( invoice(), payment({ amount: 105_000000n, inclusion: 'included', confirmations: 19 }), CFG, ); expect(inv.status).toBe('paid'); expect(inv.overpaidAmount).toBe(5_000000n); expect(events.map((e) => e.type)).toEqual(['invoice.paid', 'invoice.overpaid']); const over = events.find((e) => e.type === 'invoice.overpaid'); expect(over).toMatchObject({ surplus: 5_000000n }); }); it('an exact payment produces no overpaid event and no surplus', () => { const { invoice: inv, events } = reduce( invoice(), payment({ inclusion: 'included', confirmations: 19 }), CFG, ); expect(inv.overpaidAmount).toBeNull(); expect(events.map((e) => e.type)).toEqual(['invoice.paid']); }); }); describe('double-tx accumulation', () => { it('two confirmed payments summing past the floor pay the invoice', () => { let state = reduce( invoice(), payment({ txid: 'tx-a', amount: 60_000000n, inclusion: 'included', confirmations: 19 }), CFG, ).invoice; expect(state.status).toBe('confirming'); // 60 < 99 floor const { invoice: inv, events } = reduce( state, payment({ txid: 'tx-b', amount: 40_000000n, inclusion: 'included', confirmations: 19 }), CFG, ); expect(inv.status).toBe('paid'); expect(events.map((e) => e.type)).toContain('invoice.paid'); expect(inv.overpaidAmount).toBeNull(); // 100 exactly }); it('unconfirmed second payment does not count toward the floor', () => { let state = reduce( invoice(), payment({ txid: 'tx-a', amount: 60_000000n, inclusion: 'included', confirmations: 19 }), CFG, ).invoice; state = reduce( state, payment({ txid: 'tx-b', amount: 40_000000n, inclusion: 'included', confirmations: 2 }), CFG, ).invoice; expect(state.status).toBe('confirming'); // …until it confirms const { invoice: inv } = reduce( state, payment({ txid: 'tx-b', amount: 40_000000n, inclusion: 'included', confirmations: 19 }), CFG, ); expect(inv.status).toBe('paid'); }); }); describe('expiry', () => { it('created → expired when expires_at passes with zero payments', () => { const { invoice: inv, events } = reduce(invoice(), tick(EXPIRES), CFG); expect(inv.status).toBe('expired'); expect(events).toEqual([{ type: 'invoice.expired', invoiceId: 'inv-1' }]); }); it('a tick before expires_at changes nothing', () => { const { invoice: inv, events } = reduce(invoice(), tick(EXPIRES - 1), CFG); expect(inv.status).toBe('created'); expect(events).toEqual([]); }); it('expiry race: payment observed at the same instant as expiry wins', () => { const { invoice: inv, events } = reduce(invoice(), payment({ at: EXPIRES }), CFG); expect(inv.status).toBe('detected'); // not expired expect(events.map((e) => e.type)).toEqual(['invoice.detected']); }); it('a pending payment blocks expiry on later ticks, then pays even after expires_at', () => { let state = reduce(invoice(), payment({ at: EXPIRES }), CFG).invoice; state = reduce(state, tick(EXPIRES + 60_000), CFG).invoice; expect(state.status).toBe('detected'); // still waiting on the pending tx const { invoice: inv } = reduce( state, payment({ inclusion: 'included', confirmations: 19, at: EXPIRES + 120_000 }), CFG, ); expect(inv.status).toBe('paid'); }); it('an invoice with no options (payer never selected an asset) simply expires', () => { const { invoice: inv } = reduce(invoice({ options: [] }), tick(EXPIRES + 1), CFG); expect(inv.status).toBe('expired'); }); }); describe('underpaid', () => { it('confirmed-but-short amount at expiry → underpaid with the received amount', () => { let state = reduce( invoice(), payment({ amount: 50_000000n, inclusion: 'included', confirmations: 19 }), CFG, ).invoice; const { invoice: inv, events } = reduce(state, tick(EXPIRES), CFG); expect(inv.status).toBe('underpaid'); expect(events).toEqual([ { type: 'invoice.underpaid', invoiceId: 'inv-1', amountConfirmed: 50_000000n }, ]); }); it('does not resolve underpaid while a short payment is still confirming', () => { let state = reduce( invoice(), payment({ amount: 50_000000n, inclusion: 'included', confirmations: 2 }), CFG, ).invoice; const { invoice: inv } = reduce(state, tick(EXPIRES + 1), CFG); expect(inv.status).toBe('confirming'); // settles to underpaid only once the tx confirms }); it('all payments orphaned at expiry → expired, not underpaid', () => { let state = reduce( invoice(), payment({ inclusion: 'included', confirmations: 5 }), CFG, ).invoice; state = reduce(state, payment({ inclusion: 'orphaned', confirmations: 0 }), CFG).invoice; const { invoice: inv } = reduce(state, tick(EXPIRES), CFG); expect(inv.status).toBe('expired'); }); it('underpaid is terminal: a later confirmed tx is recorded as payment.late', () => { let state = reduce( invoice(), payment({ amount: 50_000000n, inclusion: 'included', confirmations: 19 }), CFG, ).invoice; state = reduce(state, tick(EXPIRES), CFG).invoice; const { invoice: inv, events } = reduce( state, payment({ txid: 'tx-late', amount: 50_000000n, inclusion: 'included', confirmations: 19, at: EXPIRES + 1 }), CFG, ); expect(inv.status).toBe('underpaid'); expect(events).toEqual([ { type: 'payment.late', invoiceId: 'inv-1', optionId: 'opt-tron', txid: 'tx-late', amount: 50_000000n }, ]); }); }); describe('late payments', () => { it('funds arriving after expiry are recorded, emit payment.late, and the invoice stays expired', () => { let state = reduce(invoice(), tick(EXPIRES), CFG).invoice; const { invoice: inv, events } = reduce(state, payment({ at: EXPIRES + 5000 }), CFG); expect(inv.status).toBe('expired'); expect(inv.payments).toHaveLength(1); // never silently dropped expect(events).toEqual([ { type: 'payment.late', invoiceId: 'inv-1', optionId: 'opt-tron', txid: 'tx-1', amount: USDT_100 }, ]); }); it('a late tx confirming later does not emit a second late event', () => { let state = reduce(invoice(), tick(EXPIRES), CFG).invoice; state = reduce(state, payment({ at: EXPIRES + 5000 }), CFG).invoice; const { invoice: inv, events } = reduce( state, payment({ inclusion: 'included', confirmations: 19, at: EXPIRES + 60_000 }), CFG, ); expect(inv.status).toBe('expired'); expect(events).toEqual([]); }); }); describe('reorg / orphan handling', () => { it('orphaning the only pending tx rolls the invoice back silently (before paid)', () => { let state = reduce(invoice(), payment({ inclusion: 'included', confirmations: 5 }), CFG).invoice; expect(state.status).toBe('confirming'); const { invoice: inv, events } = reduce(state, payment({ inclusion: 'orphaned', confirmations: 0 }), CFG); expect(inv.status).toBe('created'); expect(events).toEqual([]); }); it('rolls back a PAID invoice when the winning tx is orphaned and the floor no longer holds', () => { let state = reduce(invoice(), payment({ inclusion: 'included', confirmations: 19 }), CFG).invoice; expect(state.status).toBe('paid'); const { invoice: inv, events } = reduce(state, payment({ inclusion: 'orphaned', confirmations: 0 }), CFG); expect(inv.status).toBe('created'); expect(inv.paidViaOptionId).toBeNull(); expect(events).toEqual([{ type: 'invoice.reverted', invoiceId: 'inv-1', reason: 'reorg' }]); }); it('stays paid when remaining confirmed payments still satisfy the floor, recomputing surplus', () => { let state = reduce( invoice(), payment({ txid: 'tx-a', amount: 105_000000n, inclusion: 'included', confirmations: 19 }), CFG, ).invoice; state = reduce( state, payment({ txid: 'tx-b', amount: 40_000000n, inclusion: 'included', confirmations: 19, at: T0 + 2000 }), CFG, ).invoice; expect(state.overpaidAmount).toBe(5_000000n); // surplus from tx-a alone (paid before tx-b) const { invoice: inv, events } = reduce( state, payment({ txid: 'tx-b', amount: 40_000000n, inclusion: 'orphaned', confirmations: 0 }), CFG, ); expect(inv.status).toBe('paid'); expect(inv.overpaidAmount).toBe(5_000000n); expect(events).toEqual([]); }); it('a reverted invoice pays again if the tx is re-included after the reorg', () => { let state = reduce(invoice(), payment({ inclusion: 'included', confirmations: 19 }), CFG).invoice; state = reduce(state, payment({ inclusion: 'orphaned', confirmations: 0 }), CFG).invoice; expect(state.status).toBe('created'); const { invoice: inv, events } = reduce(state, payment({ inclusion: 'included', confirmations: 19 }), CFG); expect(inv.status).toBe('paid'); expect(events.map((e) => e.type)).toEqual(['invoice.paid']); }); it('reorg rollback past expiry resolves to expired once nothing is pending', () => { let state = reduce(invoice(), payment({ inclusion: 'included', confirmations: 19 }), CFG).invoice; expect(state.status).toBe('paid'); const { invoice: inv } = reduce( state, payment({ inclusion: 'orphaned', confirmations: 0, at: EXPIRES + 1 }), CFG, ); expect(inv.status).toBe('expired'); }); }); describe('multi-asset options (Section 6b)', () => { const twoOptions = () => invoice({ options: [ option({ id: 'opt-tron', createdAt: T0 }), option({ id: 'opt-base', chain: 'base', asset: 'USDC', requiredAmount: 100_100000n, createdAt: T0 + 500 }), ], }); it('pays via whichever option satisfies its own floor first', () => { const { invoice: inv } = reduce( twoOptions(), payment({ optionId: 'opt-base', inclusion: 'included', confirmations: 30, amount: 100_100000n }), CFG, ); expect(inv.status).toBe('paid'); expect(inv.paidViaOptionId).toBe('opt-base'); }); it('each option applies tolerance to its OWN pegged amount', () => { // base option requires 100.10 → floor 99.099 USDC; 99.0 confirmed is NOT enough there const { invoice: inv } = reduce( twoOptions(), payment({ optionId: 'opt-base', inclusion: 'included', confirmations: 30, amount: 99_000000n }), CFG, ); expect(inv.status).toBe('confirming'); }); it('first-option-wins when both satisfy in the same evaluation: earlier createdAt takes it', () => { let state = reduce( twoOptions(), payment({ optionId: 'opt-base', txid: 'tx-base', inclusion: 'included', confirmations: 2, amount: 100_100000n }), CFG, ).invoice; state = reduce( state, payment({ optionId: 'opt-tron', txid: 'tx-tron', inclusion: 'included', confirmations: 2, amount: USDT_100 }), CFG, ).invoice; // both cross their thresholds in one sweep (e.g. one watcher pass updates both) state = reduce( state, payment({ optionId: 'opt-base', txid: 'tx-base', inclusion: 'included', confirmations: 30, amount: 100_100000n }), CFG, ).invoice; // base satisfied first in real order — but simulate simultaneous satisfaction: const simultaneous = reduce( state, payment({ optionId: 'opt-tron', txid: 'tx-tron', inclusion: 'included', confirmations: 19, amount: USDT_100 }), CFG, ); // base already won on the prior observation expect(state.status).toBe('paid'); expect(state.paidViaOptionId).toBe('opt-base'); // and the tron confirmation arriving after is a late payment on the losing option expect(simultaneous.events).toEqual([]); // update to an existing tx: no duplicate late event }); it('earlier-created option wins a true tie within a single evaluation', () => { // Both options already have confirmed totals ≥ floor before any evaluation ran: // construct the aggregate directly, then drive one evaluation with a tick. const inv0: InvoiceSnapshot = { ...twoOptions(), payments: [ { txid: 'tx-base', optionId: 'opt-base', amount: 100_100000n, confirmations: 30, status: 'confirmed' }, { txid: 'tx-tron', optionId: 'opt-tron', amount: USDT_100, confirmations: 19, status: 'confirmed' }, ], status: 'confirming', }; const { invoice: inv } = reduce(inv0, tick(T0 + 5000), CFG); expect(inv.status).toBe('paid'); expect(inv.paidViaOptionId).toBe('opt-tron'); // created earlier }); it('funds landing on an abandoned option after the invoice is paid → payment.late, status unchanged', () => { let state = reduce( twoOptions(), payment({ optionId: 'opt-base', txid: 'tx-base', inclusion: 'included', confirmations: 30, amount: 100_100000n }), CFG, ).invoice; expect(state.status).toBe('paid'); const { invoice: inv, events } = reduce( state, payment({ optionId: 'opt-tron', txid: 'tx-tron', amount: USDT_100, at: T0 + 9000 }), CFG, ); expect(inv.status).toBe('paid'); expect(inv.paidViaOptionId).toBe('opt-base'); expect(inv.payments).toHaveLength(2); // visible to the merchant, never dropped expect(events).toEqual([ { type: 'payment.late', invoiceId: 'inv-1', optionId: 'opt-tron', txid: 'tx-tron', amount: USDT_100 }, ]); }); it('funds observed on a second option BEFORE payment completes stay recorded when the first option wins', () => { let state = reduce( twoOptions(), payment({ optionId: 'opt-tron', txid: 'tx-tron', amount: 20_000000n, inclusion: 'included', confirmations: 19 }), CFG, ).invoice; const { invoice: inv } = reduce( state, payment({ optionId: 'opt-base', txid: 'tx-base', inclusion: 'included', confirmations: 30, amount: 100_100000n }), CFG, ); expect(inv.status).toBe('paid'); expect(inv.paidViaOptionId).toBe('opt-base'); expect(inv.payments.map((p) => p.txid).sort()).toEqual(['tx-base', 'tx-tron']); }); }); describe('robustness', () => { it('rejects observations for an option the invoice does not have', () => { expect(() => reduce(invoice(), payment({ optionId: 'nope' }), CFG)).toThrow(/unknown option/); }); it('is idempotent: re-applying an identical observation changes nothing and emits nothing', () => { const first = reduce(invoice(), payment(), CFG); const second = reduce(first.invoice, payment(), CFG); expect(second.invoice).toEqual(first.invoice); expect(second.events).toEqual([]); }); it('a tick on a paid invoice does nothing', () => { let state = reduce(invoice(), payment({ inclusion: 'included', confirmations: 19 }), CFG).invoice; const { invoice: inv, events } = reduce(state, tick(EXPIRES + 999_999), CFG); expect(inv.status).toBe('paid'); expect(events).toEqual([]); }); it('does not mutate the input snapshot', () => { const inv0 = invoice(); reduce(inv0, payment(), CFG); expect(inv0.status).toBe('created'); expect(inv0.payments).toHaveLength(0); }); });