import { erc20Abi, ExecutionRevertedError, FeeCapTooLowError, NonceTooHighError, NonceTooLowError, TipAboveFeeCapError, } from 'viem' import { base } from 'viem/chains' import * as Anvil from '../../../test/Anvil.js' import * as TestRoutes from '../../../test/Routes.js' import * as Chain from './Chain.js' import * as TransferSubsidy from './TransferSubsidy.js' describe('requiredAmount', () => { test('normalizes USD stablecoins across decimal precisions', () => { const snapshot = TestRoutes.transferSnapshot() expect([ TransferSubsidy.requiredAmount({ destinationToken: { ...snapshot.destinationToken, decimals: 18 }, sourceAmount: '1000000', sourceToken: snapshot.sourceToken, }), TransferSubsidy.requiredAmount({ destinationToken: { ...snapshot.destinationToken, decimals: 3 }, sourceAmount: '1000000', sourceToken: snapshot.sourceToken, }), ]).toMatchInlineSnapshot(` [ 1000000000000000000n, 1000n, ] `) }) test('rejects a different denomination', () => { const snapshot = TestRoutes.transferSnapshot() expect(() => TransferSubsidy.requiredAmount({ destinationToken: { ...snapshot.destinationToken, currency: 'EUR' }, sourceAmount: '1000000', sourceToken: snapshot.sourceToken, }), ).toThrowErrorMatchingInlineSnapshot(`[Routes.TransferSubsidy.UnsupportedTokenError]`) }) }) describe('read', () => { test('accepts only complete persisted signed transactions', () => { const transaction = { account: `0x${'11'.repeat(20)}`, amount: '3001', chainId: 'eip155:8453', hash: `0x${'22'.repeat(32)}`, nonce: 7, transaction: `0x${'33'.repeat(100)}`, } expect([TransferSubsidy.read(transaction), TransferSubsidy.read({ ...transaction, nonce: -1 })]) .toMatchInlineSnapshot(` [ { "account": "0x1111111111111111111111111111111111111111", "amount": "3001", "chainId": "eip155:8453", "hash": "0x2222222222222222222222222222222222222222222222222222222222222222", "nonce": 7, "transaction": "0x33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", }, undefined, ] `) }) }) describe('isPermanentPreparationError', () => { test('separates rejected transactions from errors that a retry can recover', () => { expect([ TransferSubsidy.isPermanentPreparationError(new ExecutionRevertedError()), TransferSubsidy.isPermanentPreparationError(new FeeCapTooLowError()), TransferSubsidy.isPermanentPreparationError(new NonceTooHighError()), TransferSubsidy.isPermanentPreparationError(new NonceTooLowError()), TransferSubsidy.isPermanentPreparationError(new TipAboveFeeCapError()), TransferSubsidy.isPermanentPreparationError(new Error('tip higher than fee cap')), ]).toEqual([true, false, false, false, false, false]) }) }) describe('isPermanentBroadcastError', () => { test('rejects immutable signed transactions with invalid fee caps', () => { expect([ TransferSubsidy.isPermanentBroadcastError(new TipAboveFeeCapError()), TransferSubsidy.isPermanentBroadcastError(new Error('tip higher than fee cap')), TransferSubsidy.isPermanentBroadcastError(new FeeCapTooLowError()), ]).toEqual([true, true, false]) }) }) describe.skipIf(!Anvil.available)('create', () => { test('signs, broadcasts, and verifies a Base USDC subsidy', async () => { const anvil = await Anvil.start({ chain: base, forkUrl: base.rpcUrls.default.http[0]! }) try { const account = Anvil.getAccount(3) const recipient = Anvil.getAccount(4).address const token = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' await Anvil.setErc20Balance(anvil.client, { address: account.address, amount: 1_000_000n, slot: 9n, token, }) const chain = Chain.from({ id: 'eip155:8453', name: 'Base', rpcUrls: [anvil.rpcUrl], slug: 'base', }) const settler = TransferSubsidy.create({ account, fetch: globalThis.fetch, policy: { maxAmount: '5' }, }) const nonce = await settler.pendingNonce({ chain }) await expect( settler.prepare({ amount: 1_000_001n, chain, nonce, recipient, reservations: { nativeAmount: 0n, tokenAmount: 0n }, token, }), ).rejects.toThrow(TransferSubsidy.BalanceUnavailableError) const transaction = await settler.prepare({ amount: 3_001n, chain, nonce, recipient, reservations: { nativeAmount: 0n, tokenAmount: 0n }, token, }) await settler.broadcast({ chain, transaction }) await expect(settler.status({ chain, recipient, token, transaction })).resolves.toEqual({ broadcast: false, type: 'pending', }) await anvil.client.mine({ blocks: 64 }) await expect(settler.status({ chain, recipient, token, transaction })).resolves.toEqual({ type: 'success', }) await expect( anvil.client.readContract({ abi: erc20Abi, address: token, args: [recipient], functionName: 'balanceOf', }), ).resolves.toBe(3_001n) const rotated = TransferSubsidy.create({ account: Anvil.getAccount(5), fetch: globalThis.fetch, policy: { maxAmount: '5' }, }) await expect(rotated.status({ chain, recipient, token, transaction })).resolves.toEqual({ type: 'success', }) } finally { await anvil.stop() } }) })