/** @module-tag localnet */ import type { RpcRequest } from 'ox' import { SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' import { parseUnits, type Address, type BaseError } from 'viem' import { fillTransaction, sendTransactionSync } from 'viem/actions' import { Actions, Addresses, Capabilities, Tick, Transaction, VirtualAddress } from 'viem/tempo' import { tempo, tempoModerato } from 'viem/tempo/chains' import * as Relay from '../../test/Relay.js' import * as Runtime from '../../test/runtime.js' type Server = Relay.Server const userAccount = Relay.accounts[9]! const feePayerAccount = Relay.accounts[0]! const recipient = Relay.accounts[7]! /** * Tokens the relay handler probes for fee-token resolution. The default * `resolveTokens` uses the Tempo API for mainnet and testnet, so localnet * tests inject this list explicitly. */ const localnetTokens = [ { address: '0x20c0000000000000000000000000000000000000', decimals: 6, name: 'pathUSD', symbol: 'pathUSD', }, { address: '0x20c0000000000000000000000000000000000001', decimals: 6, name: 'alphaUSD', symbol: 'alphaUSD', }, { address: '0x20c0000000000000000000000000000000000002', decimals: 6, name: 'betaUSD', symbol: 'betaUSD', }, { address: '0x20c0000000000000000000000000000000000003', decimals: 6, name: 'thetaUSD', symbol: 'thetaUSD', }, ] as const /** Case-insensitive lookup into balanceDiffs keyed by address. */ function findDiffs( balanceDiffs: Capabilities.FillTransactionCapabilities['balanceDiffs'], address: string, ) { return Object.entries(balanceDiffs ?? {}).find( ([addr]) => addr.toLowerCase() === address.toLowerCase(), )?.[1] } /** Extracts relay virtual-address metadata while viem's public type catches up. */ function virtualAddresses(capabilities: Capabilities.FillTransactionCapabilities | undefined) { return ( capabilities as | (Capabilities.FillTransactionCapabilities & { virtualAddresses?: Record | undefined }) | undefined )?.virtualAddresses } /** Client used only to build typed calldata (`Actions.token.*.call`). */ const caller = Relay.getClient() /** A simple transfer call for tests that just need a valid transaction. */ const transferCall = () => Actions.token.transfer.call(caller, { token: Relay.addresses.alphaUsd, to: recipient.address, amount: 1n, }) beforeAll(async () => { // Faucet-fund the accounts that pay fees or create fresh tokens. The faucet is // permissionless, so no genesis privileges are required. Left unfunded on // purpose: accounts[5]/[10] (balance-sensitive fee-resolution and // insufficient-balance tests) and accounts[7] (recipient only). await Promise.all( [0, 2, 3, 4, 6, 8, 9].map((index) => Actions.faucet.fundSync(Relay.getClient(), { account: Relay.accounts[index]!, timeout: 60_000, }), ), ) // userAccount prefers alphaUsd (a faucet-funded genesis token) as its fee token. await Actions.fee.setUserToken(Relay.getClient(), { account: userAccount, token: Relay.addresses.alphaUsd, }) }) describe.skipIf(Runtime.get().mode !== 'localnet')('default', () => { let client: ReturnType let server: Server beforeAll(async () => { server = await Relay.createServer((await Relay.relay({})).listener) client = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) test('default: returns filled transaction with capabilities', async () => { const { transaction } = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) expect(transaction.gas).toBeDefined() expect(transaction.nonce).toBeDefined() }) test('behavior: proxies other methods to RPC node', async () => { const chainId = await client.request({ method: 'eth_chainId' }) expect(Number(chainId)).toMatchInlineSnapshot(`${Relay.chain.id}`) }) test('behavior: surfaces upstream RPC errors as JSON-RPC errors', async () => { // grantRoles reverts with Unauthorized when caller is not an admin // (eth_call defaults `from` to the zero address). We expect the relay to // forward the revert as a structured JSON-RPC error response (HTTP 200 // with `error.code`/`error.data`), not a 500. const call = Actions.token.grantRoles.call(caller, { token: Relay.addresses.alphaUsd, role: 'issuer', to: recipient.address, }) const response = await fetch(server.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: call.to, data: call.data }, 'latest'], }), }) expect(response.status).toBe(200) const body = (await response.json()) as { id: number jsonrpc: string error: { code: number; message: string; data?: string } } // Drop `message` from the snapshot — it embeds the upstream RPC URL/port // and viem version, which are nondeterministic. const { message: _message, ...errorRest } = body.error expect({ ...body, error: errorRest }).toMatchInlineSnapshot(` { "error": { "code": 3, "data": "0x82b42900", }, "id": 1, "jsonrpc": "2.0", } `) }) test('behavior: returns actionable error for eth_signRawTransaction without feePayer', async () => { const response = await fetch(server.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_signRawTransaction', params: ['0x00'], }), }) expect(response.status).toBe(200) const body = (await response.json()) as { id: number jsonrpc: string error: { code: number; message: string } } expect(body.error.code).toBe(-32601) expect(body.error.message).toContain('fee payer') expect(body.error.message).toContain('Handler.relay()') }) test('behavior: handles JSON-RPC batch requests', async () => { const response = await fetch(server.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify([ { jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] }, { jsonrpc: '2.0', id: 2, method: 'eth_chainId', params: [] }, ]), }) expect(response.status).toBe(200) const body = (await response.json()) as { id: number; result: string }[] expect(Array.isArray(body)).toBe(true) expect(body).toHaveLength(2) expect(body[0]!.id).toBe(1) expect(body[1]!.id).toBe(2) expect(Number(body[0]!.result)).toBe(Relay.chain.id) expect(Number(body[1]!.result)).toBe(Relay.chain.id) }) }) describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: with feePayer', () => { let server: Server let client: ReturnType let requests: RpcRequest.RpcRequest[] = [] beforeAll(async () => { server = await Relay.createServer( ( await Relay.relay({ feePayer: { account: feePayerAccount, name: 'Test Sponsor', url: 'https://test.com', }, onRequest: async (request) => { requests.push(request) }, }) ).listener, ) client = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) afterEach(() => { requests = [] }) test('default: returns sponsored tx with feePayerSignature', async () => { const { transaction } = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) expect(transaction.feePayerSignature).toBeDefined() expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(` [ "eth_fillTransaction", ] `) }) test('behavior: returns sponsor capabilities', async () => { const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) const meta = result.capabilities expect(meta?.['sponsored']).toBe(true) expect(meta?.['sponsor']).toMatchInlineSnapshot(` { "address": "${feePayerAccount.address}", "name": "Test Sponsor", "url": "https://test.com", } `) }) test('behavior: sponsored tx can be signed and broadcast', async () => { const { transaction } = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) const signed = await userAccount.signTransaction(transaction as never) const receipt = (await Relay.getClient().request({ method: 'eth_sendRawTransactionSync' as never, params: [signed], })) as { feePayer?: string | undefined } expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase()) }) test('behavior: missing from returns error capability when errors capability is enabled', async () => { const result = await fillTransaction(client, { calls: [transferCall()], capabilities: { errors: true }, }) expect(result.capabilities).toMatchInlineSnapshot(` { "error": { "errorName": "unknown", "message": "unknown account", }, "sponsored": false, } `) }) }) describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: with feePayer.feeToken', () => { let server: Server let client: ReturnType const sponsorFeeToken = '0x20c0000000000000000000000000000000000000' as const // pathUSD beforeAll(async () => { server = await Relay.createServer( ( await Relay.relay({ feePayer: { account: feePayerAccount, feeToken: sponsorFeeToken, }, tokens: localnetTokens, }) ).listener, ) client = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) test('default: sponsor.feeToken is used when request omits feeToken', async () => { const { transaction } = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken) expect(transaction.feePayerSignature).toBeDefined() }) test('behavior: sponsor.feeToken overrides request feeToken on sponsored fills', async () => { const { transaction } = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], feeToken: Relay.addresses.alphaUsd as Address, }) expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken) expect(transaction.feePayerSignature).toBeDefined() }) test('behavior: request feeToken wins when sponsorship is opted out via feePayer:false', async () => { const { transaction } = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], feePayer: false as never, feeToken: Relay.addresses.alphaUsd as Address, }) expect(transaction.feeToken?.toLowerCase()).toBe(Relay.addresses.alphaUsd.toLowerCase()) expect(transaction.feePayerSignature).toBeUndefined() }) test('behavior: broadcast tx receipt records the sponsor.feeToken', async () => { // Fill via relay (where override kicks in), then sign + broadcast manually. // viem's `sendTransactionSync` with a hydrated account does local prep and // would skip the relay's `eth_fillTransaction`, bypassing the override. const { transaction } = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], feeToken: Relay.addresses.alphaUsd as Address, }) expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken) const signed = await userAccount.signTransaction(transaction as never) const receipt = (await Relay.getClient().request({ method: 'eth_sendRawTransactionSync' as never, params: [signed], })) as { feePayer?: string | undefined; feeToken?: string | undefined } expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase()) expect(receipt.feeToken?.toLowerCase()).toBe(sponsorFeeToken) }) test('behavior: raw sponsor signing restores sponsor.feeToken when envelope omits feeToken', async () => { const rpc = Relay.getClient({ account: userAccount }) const { transaction } = await fillTransaction(rpc, { account: userAccount.address, calls: [transferCall()], feePayer: true as never, }) const partial = await userAccount.signTransaction({ ...transaction, feePayer: true } as never) expect([null, undefined]).toContain( Transaction.deserialize(partial as `0x76${string}`).feeToken, ) const response = await fetch(server.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_signRawTransaction', params: [partial], }), }) const body = (await response.json()) as { result: `0x76${string}` } const feeToken = Transaction.deserialize(body.result).feeToken as Address | undefined expect(feeToken?.toLowerCase()).toBe(sponsorFeeToken) }) test('behavior: raw sponsor signing restores token-list default before validation', async () => { let feeToken_validated: Address | undefined const customServer = await Relay.createServer( ( await Relay.relay({ feePayer: { account: feePayerAccount, validate: (request) => { feeToken_validated = request.feeToken as Address | undefined return true }, }, tokens: localnetTokens, }) ).listener, ) const rpc = Relay.getClient({ account: userAccount }) const { transaction } = await fillTransaction(rpc, { account: userAccount.address, calls: [transferCall()], feePayer: true as never, }) const partial = await userAccount.signTransaction({ ...transaction, feePayer: true } as never) try { const response = await fetch(customServer.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_signRawTransaction', params: [partial], }), }) const body = (await response.json()) as { result: `0x76${string}` } const feeToken = Transaction.deserialize(body.result).feeToken as Address | undefined expect(feeToken_validated?.toLowerCase()).toBe(sponsorFeeToken) expect(feeToken?.toLowerCase()).toBe(sponsorFeeToken) } finally { customServer.close() } }) }) describe.skipIf(Runtime.get().mode !== 'localnet')( 'behavior: with app-provided feePayer URL', () => { let appServer: Server let walletServer: Server let client: ReturnType beforeAll(async () => { // App relay: has a fee payer account and signs transactions. appServer = await Relay.createServer( ( await Relay.relay({ feePayer: { account: feePayerAccount, name: 'App Sponsor', url: 'https://app.example.com', }, }) ).listener, ) // Wallet relay: no fee payer configured — proxies to app relay. walletServer = await Relay.createServer( ( await Relay.relay({ internal_allowUnsafeUrls: true, }) ).listener, ) client = Relay.getClient({ url: walletServer.url }) }) afterAll(() => { appServer.close() walletServer.close() }) test('default: proxies fill to app relay and returns sponsored tx', async () => { const { transaction } = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], feePayer: appServer.url as never, }) expect(transaction.feePayerSignature).toBeDefined() expect(transaction.gas).toBeDefined() }) test('behavior: relays sponsor metadata from app relay', async () => { const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], feePayer: appServer.url as never, }) expect(result.capabilities?.['sponsored']).toBe(true) expect(result.capabilities?.['sponsor']).toMatchInlineSnapshot(` { "address": "${feePayerAccount.address}", "name": "App Sponsor", "url": "https://app.example.com", } `) }) test('behavior: sponsored tx from app relay can be signed and broadcast', async () => { const { transaction } = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], feePayer: appServer.url as never, }) const signed = await userAccount.signTransaction(transaction as never) const receipt = (await Relay.getClient().request({ method: 'eth_sendRawTransactionSync' as never, params: [signed], })) as { feePayer?: string | undefined } expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase()) }) }, ) describe.skipIf(Runtime.get().mode !== 'localnet')( 'behavior: with app-provided feePayer URL + autoSwap', () => { let appServer: Server let walletServer: Server let client: ReturnType beforeAll(async () => { // App relay sponsors fees AND has `features: 'all'` so it can recover // from InsufficientBalance via autoSwap. appServer = await Relay.createServer( ( await Relay.relay({ features: 'all', feePayer: { account: feePayerAccount, name: 'App Sponsor', url: 'https://app.example.com', }, }) ).listener, ) // Wallet relay forwards to the app relay; also has features:'all' so its // own fill() can detect upstream `capabilities.error` as InsufficientBalance. walletServer = await Relay.createServer( ( await Relay.relay({ features: 'all', internal_allowUnsafeUrls: true, }) ).listener, ) client = Relay.getClient({ url: walletServer.url }) }) afterAll(() => { appServer.close() walletServer.close() }) test('behavior: autoSwap recovers when external feePayer surfaces InsufficientBalance', async () => { const sender = Relay.accounts[6]! // Token pair + DEX liquidity. Use alphaUsd as the quote token so the // relay can swap alphaUsd → base to cover the deficit. const rpc = Relay.getClient({ account: Relay.accounts[0]! }) const { token: base } = await Actions.token.createSync(rpc, { name: 'External Swap Base', symbol: 'EXTBASE', currency: 'USD', quoteToken: Relay.addresses.alphaUsd, }) await sendTransactionSync(rpc, { calls: [ Actions.token.grantRoles.call(caller, { token: base, role: 'issuer', to: rpc.account!.address, }), Actions.token.mint.call(caller, { token: base, to: rpc.account!.address, amount: parseUnits('10000', 6), }), Actions.token.approve.call(caller, { token: base, spender: Addresses.stablecoinDex, amount: parseUnits('10000', 6), }), Actions.token.approve.call(caller, { token: Relay.addresses.alphaUsd, spender: Addresses.stablecoinDex, amount: parseUnits('10000', 6), }), ], }) await Actions.dex.createPairSync(rpc, { base }) await Actions.dex.placeSync(rpc, { token: base, amount: parseUnits('500', 6), type: 'sell', tick: Tick.fromPrice('1.001'), }) // Sender has faucet alphaUsd (fee + swap source) but NO base tokens. await Actions.fee.setUserToken(Relay.getClient({ account: sender }), { token: Relay.addresses.alphaUsd, }) // Sender attempts to transfer base via the wallet relay, which forwards // to the app relay. The app relay returns 200 with capabilities.error = // InsufficientBalance and a stub tx; the wallet relay must convert that // into a synthetic throw so its own fill() autoSwap branch can recover. const transferAmount = parseUnits('5', 6) const result = await fillTransaction(client, { account: sender.address, ...Actions.token.transfer.call(caller, { token: base, to: Relay.accounts[7]!.address, amount: transferAmount, }), feePayer: appServer.url as never, }) const { transaction, capabilities } = result // Tx is filled with the swap calls prepended (approve + buy + transfer). expect(transaction.calls).toHaveLength(3) expect(transaction.feePayerSignature).toBeDefined() // autoSwap metadata is surfaced. expect(capabilities?.['autoSwap']?.slippage).toBe(0.05) expect(capabilities?.['autoSwap']?.maxIn.symbol).toBe('AlphaUSD') expect(capabilities?.['autoSwap']?.minOut.symbol).toBe('EXTBASE') expect(capabilities?.['autoSwap']?.minOut.formatted).toBe('5') }) }, ) describe.skipIf(Runtime.get().mode !== 'localnet')( 'behavior: app-provided feePayer URL bypasses wallet validate', () => { let appServer: Server let walletServer: Server let client: ReturnType beforeAll(async () => { // App relay is the authoritative sponsor — it has its own fee payer // account and signs sponsored transactions. appServer = await Relay.createServer( ( await Relay.relay({ feePayer: { account: feePayerAccount, name: 'App Sponsor', url: 'https://app.example.com', }, }) ).listener, ) // Wallet relay has its own fee payer with a `validate` that ALWAYS // rejects. This guards the wallet's own fee payer; it must NOT gate // sponsorship when the dapp supplies its own external feePayer URL. walletServer = await Relay.createServer( ( await Relay.relay({ features: 'all', internal_allowUnsafeUrls: true, feePayer: { account: feePayerAccount, name: 'Wallet Sponsor', validate: () => false, }, }) ).listener, ) client = Relay.getClient({ url: walletServer.url }) }) afterAll(() => { appServer.close() walletServer.close() }) test('behavior: external feePayer URL is sponsored even when wallet validate rejects', async () => { const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], feePayer: appServer.url as never, }) expect(result.transaction.feePayerSignature).toBeDefined() expect(result.transaction.maxFeePerGas).toBeDefined() expect(result.transaction.maxFeePerGas).not.toBe(0n) expect(result.capabilities?.['sponsored']).toBe(true) expect(result.capabilities?.['sponsor']?.name).toBe('App Sponsor') }) }, ) describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: chainId path parameter', () => { let server: Server let client: ReturnType beforeAll(async () => { server = await Relay.createServer((await Relay.relay({})).listener) client = Relay.getClient({ url: `${server.url}/${Relay.chain.id}` }) }) afterAll(() => { server.close() }) test('default: proxies RPC methods via /:chainId path', async () => { const chainId = await client.request({ method: 'eth_chainId' }) expect(Number(chainId)).toMatchInlineSnapshot(`${Relay.chain.id}`) }) test('behavior: fills transaction via /:chainId path', async () => { const { transaction } = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) expect(transaction.gas).toBeDefined() expect(transaction.nonce).toBeDefined() }) test('behavior: handles batch requests via /:chainId path', async () => { const response = await fetch(`${server.url}/${Relay.chain.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify([ { jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] }, { jsonrpc: '2.0', id: 2, method: 'eth_chainId', params: [] }, ]), }) expect(response.status).toBe(200) const body = (await response.json()) as { id: number; result: string }[] expect(body).toHaveLength(2) expect(Number(body[0]!.result)).toBe(Relay.chain.id) expect(Number(body[1]!.result)).toBe(Relay.chain.id) }) }) describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: capabilities', () => { let server: Server let client: ReturnType beforeAll(async () => { server = await Relay.createServer( ( await Relay.relay({ features: 'all', }) ).listener, ) client = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) test('default: returns fee and sponsored info', async () => { const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) const meta = result.capabilities expect(meta?.['fee']).toBeDefined() expect(meta?.['fee']?.decimals).toBe(6) expect(meta?.['fee']?.symbol).toBe('AlphaUSD') expect(meta?.['sponsored']).toBe(false) }) test('behavior: token transfer produces balance diffs', async () => { const sender = Relay.accounts[6]! const recipient = Relay.accounts[7]! const token = Relay.addresses.alphaUsd // Sender is faucet-funded with alphaUsd (enough for transfer + fee). // Set fee token so relay doesn't need pathUSD balance. await Actions.fee.setUserToken(Relay.getClient(), { account: sender, token }) const { data, to: callTo } = Actions.token.transfer.call(caller, { token, to: recipient.address, amount: 100n, }) const result = await fillTransaction(client, { account: sender.address, to: callTo, data, }) const meta = result.capabilities const senderDiffs = findDiffs(meta?.['balanceDiffs'], sender.address)! const tokenDiff = senderDiffs.find((d) => d.address.toLowerCase() === token.toLowerCase())! expect(tokenDiff.decimals).toBe(6) expect(tokenDiff.direction).toBe('outgoing') expect(tokenDiff.formatted).toBe('0.0001') expect(tokenDiff.symbol).toBe('AlphaUSD') expect(tokenDiff.value).toBe('0x64') }) test('behavior: resolves direct virtual-address targets', async () => { const virtualAddress = VirtualAddress.from({ masterId: '0xffffffff', userTag: '0x000000000001', }) const result = await fillTransaction(client, { account: userAccount.address, to: virtualAddress, }) expect(virtualAddresses(result.capabilities)).toMatchInlineSnapshot(` { "0xfffffffffdfdfdfdfdfdfdfdfdfd000000000001": null, } `) }) test('behavior: resolves TIP-20 memo transfer virtual-address recipients', async () => { const virtualAddress = VirtualAddress.from({ masterId: '0xfffffffe', userTag: '0x000000000002', }) const result = await fillTransaction(client, { account: userAccount.address, calls: [ Actions.token.transfer.call(caller, { amount: 1n, memo: '0x01', to: virtualAddress, token: Relay.addresses.alphaUsd, }), ], capabilities: { errors: true }, }) expect(virtualAddresses(result.capabilities)).toMatchInlineSnapshot(` { "0xfffffffefdfdfdfdfdfdfdfdfdfd000000000002": null, } `) }) test('behavior: approve + dex swap + transfer produces balance diffs', async () => { const sender = Relay.accounts[8]! const recipient = Relay.accounts[7]! // Set up token pair + DEX liquidity. const rpc = Relay.getClient({ account: Relay.accounts[0]! }) const { token: quote } = await Actions.token.createSync(rpc, { name: 'Test Quote', symbol: 'TQUOTE', currency: 'USD', }) const { token: base } = await Actions.token.createSync(rpc, { name: 'Test Base', symbol: 'TBASE', currency: 'USD', quoteToken: quote, }) await sendTransactionSync(rpc, { calls: [ Actions.token.grantRoles.call(caller, { token: base, role: 'issuer', to: rpc.account!.address, }), Actions.token.grantRoles.call(caller, { token: quote, role: 'issuer', to: rpc.account!.address, }), Actions.token.mint.call(caller, { token: base, to: rpc.account!.address, amount: parseUnits('10000', 6), }), Actions.token.mint.call(caller, { token: quote, to: rpc.account!.address, amount: parseUnits('10000', 6), }), Actions.token.approve.call(caller, { token: base, spender: Addresses.stablecoinDex, amount: parseUnits('10000', 6), }), Actions.token.approve.call(caller, { token: quote, spender: Addresses.stablecoinDex, amount: parseUnits('10000', 6), }), ], }) await Actions.dex.createPairSync(rpc, { base }) await Actions.dex.placeSync(rpc, { token: base, amount: parseUnits('500', 6), type: 'sell', tick: Tick.fromPrice('1.001'), }) // Fund sender with quote tokens; alphaUsd fees come from the faucet. await Actions.token.mintSync(rpc, { token: quote, amount: parseUnits('1000', 6), to: sender.address, }) await Actions.fee.setUserToken(Relay.getClient({ account: sender }), { token: Relay.addresses.alphaUsd, }) const buyAmount = parseUnits('10', 6) const result = await fillTransaction(client, { account: sender.address, calls: [ Actions.token.approve.call(caller, { token: quote, spender: Addresses.stablecoinDex, amount: parseUnits('100', 6), }), Actions.dex.buy.call({ tokenIn: quote, tokenOut: base, amountOut: buyAmount, maxAmountIn: parseUnits('100', 6), }), Actions.token.transfer.call(caller, { token: base, to: recipient.address, amount: buyAmount, }), ], }) const meta = result.capabilities const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)! expect(diffs).toHaveLength(1) const quoteDiff = diffs[0]! expect(quoteDiff.address.toLowerCase()).toBe(quote.toLowerCase()) expect(quoteDiff.direction).toBe('outgoing') expect(quoteDiff.symbol).toBe('TQUOTE') expect(quoteDiff.name).toBe('Test Quote') expect(quoteDiff.decimals).toBe(6) // No base diff — bought and immediately transferred out (net zero). expect(diffs.find((d) => d.address.toLowerCase() === base.toLowerCase())).toBeUndefined() }) test('behavior: approval covered by transfer is suppressed', async () => { const sender = Relay.accounts[6]! const recipient = Relay.accounts[7]! const token = Relay.addresses.alphaUsd // approve(100) + transfer(100) to same spender → approval fully covered. const result = await fillTransaction(client, { account: sender.address, calls: [ Actions.token.approve.call(caller, { token, spender: recipient.address, amount: 100n, }), Actions.token.transfer.call(caller, { token, to: recipient.address, amount: 100n, }), ], }) const meta = result.capabilities const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)! const tokenDiff = diffs.find((d) => d.address.toLowerCase() === token.toLowerCase())! // Only the transfer shows — approval is fully covered. expect(tokenDiff.value).toBe('0x64') expect(tokenDiff.direction).toBe('outgoing') }) test('behavior: uncovered approval shows as outgoing', async () => { const sender = Relay.accounts[6]! const spender = Relay.accounts[7]! const token = Relay.addresses.alphaUsd // approve(200) + transfer(50) to same spender → 150 uncovered approval. const result = await fillTransaction(client, { account: sender.address, calls: [ Actions.token.approve.call(caller, { token, spender: spender.address, amount: 200n, }), Actions.token.transfer.call(caller, { token, to: spender.address, amount: 50n, }), ], }) const meta = result.capabilities const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)! const tokenDiff = diffs.find((d) => d.address.toLowerCase() === token.toLowerCase())! // transfer(50) + uncovered approval(150) = 200 outgoing. expect(tokenDiff.value).toBe('0xc8') expect(tokenDiff.direction).toBe('outgoing') }) }) describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: AMM resolution', () => { let server: Server let client: ReturnType beforeAll(async () => { server = await Relay.createServer( ( await Relay.relay({ features: 'all', }) ).listener, ) client = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) test('behavior: prepends swap calls on InsufficientBalance', async () => { const sender = Relay.accounts[4]! // Set up token pair + DEX liquidity. // Use alphaUsd as the quote token so the relay can swap alphaUsd → base. const rpc = Relay.getClient({ account: Relay.accounts[0]! }) const { token: base } = await Actions.token.createSync(rpc, { name: 'Swap Base', symbol: 'SWBASE', currency: 'USD', quoteToken: Relay.addresses.alphaUsd, }) await sendTransactionSync(rpc, { calls: [ Actions.token.grantRoles.call(caller, { token: base, role: 'issuer', to: rpc.account!.address, }), Actions.token.mint.call(caller, { token: base, to: rpc.account!.address, amount: parseUnits('10000', 6), }), Actions.token.approve.call(caller, { token: base, spender: Addresses.stablecoinDex, amount: parseUnits('10000', 6), }), Actions.token.approve.call(caller, { token: Relay.addresses.alphaUsd, spender: Addresses.stablecoinDex, amount: parseUnits('10000', 6), }), ], }) await Actions.dex.createPairSync(rpc, { base }) await Actions.dex.placeSync(rpc, { token: base, amount: parseUnits('500', 6), type: 'sell', tick: Tick.fromPrice('1.001'), }) // Sender has faucet alphaUsd (fee token) but NO base tokens. await Actions.fee.setUserToken(Relay.getClient({ account: sender }), { token: Relay.addresses.alphaUsd, }) // Sender tries to transfer base tokens they don't have. // Relay should detect InsufficientBalance, swap alphaUsd → base via DEX, and retry. const transferAmount = parseUnits('5', 6) const result = await fillTransaction(client, { account: sender.address, ...Actions.token.transfer.call(caller, { token: base, to: Relay.accounts[7]!.address, amount: transferAmount, }), }) // Should succeed — relay auto-swapped quote → base. const { transaction, capabilities } = result expect(transaction.gas).toBeDefined() expect(transaction.nonce).toBeDefined() expect(transaction.feeToken).toBe(Relay.addresses.alphaUsd) expect(transaction.calls).toHaveLength(3) // approve + swap + transfer const m = capabilities expect(m?.['sponsored']).toBe(false) expect(m?.['fee']?.decimals).toBe(6) expect(m?.['fee']?.symbol).toBe('AlphaUSD') // Balance diffs exclude swap tokens — only the user's transfer shows. const diffs = findDiffs(m?.['balanceDiffs'], sender.address)! expect(diffs).toHaveLength(1) expect(diffs[0]!.direction).toBe('outgoing') expect(diffs[0]!.formatted).toBe('5') expect(diffs[0]!.symbol).toBe('SWBASE') expect(diffs[0]!.address.toLowerCase()).toBe(base.toLowerCase()) // autoSwap reports the injected AMM swap. expect(m?.['autoSwap']?.slippage).toBe(0.05) expect(m?.['autoSwap']?.maxIn.formatted).toBe('5.25') expect(m?.['autoSwap']?.maxIn.symbol).toBe('AlphaUSD') expect(m?.['autoSwap']?.maxIn.token.toLowerCase()).toBe(Relay.addresses.alphaUsd.toLowerCase()) expect(m?.['autoSwap']?.minOut.formatted).toBe('5') expect(m?.['autoSwap']?.minOut.symbol).toBe('SWBASE') expect(m?.['autoSwap']?.minOut.token.toLowerCase()).toBe(base.toLowerCase()) }) test('behavior: custom slippage is applied to autoSwap', async () => { const sender = Relay.accounts[2]! // Set up token pair + DEX liquidity. const rpc = Relay.getClient({ account: Relay.accounts[0]! }) const { token: base } = await Actions.token.createSync(rpc, { name: 'Slippage Base', symbol: 'SLPBASE', currency: 'USD', quoteToken: Relay.addresses.alphaUsd, }) await sendTransactionSync(rpc, { calls: [ Actions.token.grantRoles.call(caller, { token: base, role: 'issuer', to: rpc.account!.address, }), Actions.token.mint.call(caller, { token: base, to: rpc.account!.address, amount: parseUnits('10000', 6), }), Actions.token.approve.call(caller, { token: base, spender: Addresses.stablecoinDex, amount: parseUnits('10000', 6), }), Actions.token.approve.call(caller, { token: Relay.addresses.alphaUsd, spender: Addresses.stablecoinDex, amount: parseUnits('10000', 6), }), ], }) await Actions.dex.createPairSync(rpc, { base }) await Actions.dex.placeSync(rpc, { token: base, amount: parseUnits('500', 6), type: 'sell', tick: Tick.fromPrice('1.001'), }) // Sender has faucet alphaUsd but NO base tokens. await Actions.fee.setUserToken(Relay.getClient({ account: sender }), { token: Relay.addresses.alphaUsd, }) // Create relay with custom 2% slippage. const customServer = await Relay.createServer( ( await Relay.relay({ features: 'all', autoSwap: { slippage: 0.02 }, }) ).listener, ) const customClient = Relay.getClient({ url: customServer.url }) const result = await fillTransaction(customClient, { account: sender.address, ...Actions.token.transfer.call(caller, { token: base, to: Relay.accounts[7]!.address, amount: parseUnits('10', 6), }), }) customServer.close() const m = result.capabilities expect(m?.['autoSwap']?.slippage).toBe(0.02) // 10 + 2% = 10.2 expect(m?.['autoSwap']?.maxIn.formatted).toBe('10.2') expect(m?.['autoSwap']?.minOut.formatted).toBe('10') }) test('behavior: autoSwap disabled throws InsufficientBalance instead of swapping', async () => { const sender = Relay.accounts[3]! // Set up token pair + DEX liquidity. const rpc = Relay.getClient({ account: Relay.accounts[0]! }) const { token: base } = await Actions.token.createSync(rpc, { name: 'No Swap Base', symbol: 'NSWBASE', currency: 'USD', quoteToken: Relay.addresses.alphaUsd, }) await sendTransactionSync(rpc, { calls: [ Actions.token.grantRoles.call(caller, { token: base, role: 'issuer', to: rpc.account!.address, }), Actions.token.mint.call(caller, { token: base, to: rpc.account!.address, amount: parseUnits('10000', 6), }), Actions.token.approve.call(caller, { token: base, spender: Addresses.stablecoinDex, amount: parseUnits('10000', 6), }), ], }) await Actions.dex.createPairSync(rpc, { base }) await Actions.dex.placeSync(rpc, { token: base, amount: parseUnits('500', 6), type: 'sell', tick: Tick.fromPrice('1.001'), }) // Sender has faucet alphaUsd but NO base tokens. await Actions.fee.setUserToken(Relay.getClient({ account: sender }), { token: Relay.addresses.alphaUsd, }) // Create relay with autoSwap disabled. const customServer = await Relay.createServer( ( await Relay.relay({ features: 'all', autoSwap: false, }) ).listener, ) const customClient = Relay.getClient({ url: customServer.url }) // Should return error capability instead of auto-swapping. const result = await fillTransaction(customClient, { account: sender.address, calls: [ Actions.token.transfer.call(caller, { token: base, to: Relay.accounts[7]!.address, amount: parseUnits('5', 6), }), ], capabilities: { errors: true }, }) const error = result.capabilities?.['error'] expect({ ...error, data: undefined }).toMatchInlineSnapshot(` { "abiItem": { "inputs": [ { "name": "available", "type": "uint256", }, { "name": "required", "type": "uint256", }, { "name": "token", "type": "address", }, ], "name": "InsufficientBalance", "type": "error", }, "data": undefined, "errorName": "InsufficientBalance", "message": "Insufficient balance. Required: 5000000, available: 0.", } `) customServer.close() }) }) describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: conditional sponsoring', () => { let server: Server let client: ReturnType beforeAll(async () => { // accounts[3] is faucet-funded with alphaUsd so transfers succeed. await Actions.fee.setUserToken(Relay.getClient(), { account: Relay.accounts[3]!, token: Relay.addresses.alphaUsd, }) server = await Relay.createServer( ( await Relay.relay({ feePayer: { account: feePayerAccount, name: 'Test Sponsor', url: 'https://test.com', validate: (request) => request.from?.toLowerCase() !== Relay.accounts[3]!.address.toLowerCase(), }, }) ).listener, ) client = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) test('behavior: approved tx is sponsored and can be broadcast', async () => { const { transaction } = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) expect(transaction.feePayerSignature).toBeDefined() const signed = await userAccount.signTransaction(transaction as never) const receipt = (await Relay.getClient().request({ method: 'eth_sendRawTransactionSync' as never, params: [signed], })) as { feePayer?: string | undefined } expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase()) }) test('behavior: rejected tx is not sponsored and can be self-paid', async () => { const sender = Relay.accounts[3]! const result = await fillTransaction(client, { account: sender.address, calls: [transferCall()], }) expect(result.transaction.feePayerSignature).toBeUndefined() const meta = result.capabilities expect(meta?.['sponsored']).toBe(false) expect(meta?.['sponsor']).toBeUndefined() const serialized = (await Transaction.serialize(result.transaction as never)) as `0x76${string}` const envelope = TxEnvelopeTempo.deserialize(serialized) const signature = await sender.sign({ hash: TxEnvelopeTempo.getSignPayload(envelope), }) const signed = TxEnvelopeTempo.serialize(envelope, { signature: SignatureEnvelope.from(signature), }) const receipt = (await Relay.getClient().request({ method: 'eth_sendRawTransactionSync' as never, params: [signed], })) as { feePayer?: string | undefined } // Sender pays their own fee — no external fee payer. expect(receipt.feePayer).not.toBe(feePayerAccount.address.toLowerCase()) }) }) describe.skipIf(Runtime.get().mode !== 'localnet')( 'behavior: path A — guaranteed sponsorship (no validate)', () => { let server: Server let client: ReturnType let requests: RpcRequest.RpcRequest[] = [] beforeAll(async () => { server = await Relay.createServer( ( await Relay.relay({ features: 'all', tokens: localnetTokens, feePayer: { account: feePayerAccount, name: 'Path A Sponsor', }, onRequest: async (request) => { requests.push(request) }, }) ).listener, ) client = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) afterEach(() => { requests = [] }) test('behavior: skips fee token resolution and sponsors', async () => { const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) expect(result.transaction.feePayerSignature).toBeDefined() expect(result.capabilities?.['sponsored']).toBe(true) expect(result.capabilities?.['sponsor']?.name).toBe('Path A Sponsor') // Only one fill request — no fee token resolution round-trip. expect(requests.filter((r) => r.method === 'eth_fillTransaction')).toHaveLength(1) }) test('behavior: returns fee even when no feeToken in request', async () => { const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) expect(result.capabilities?.['fee']).toBeDefined() expect(result.capabilities?.['fee']?.decimals).toBeTypeOf('number') expect(result.capabilities?.['fee']?.symbol).toBeTypeOf('string') expect(result.capabilities?.['fee']?.formatted).toBeTypeOf('string') }) test('behavior: simulate and sign run concurrently with fill', async () => { const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) // All capabilities are present despite parallel execution. expect(result.transaction.feePayerSignature).toBeDefined() expect(result.capabilities?.['sponsored']).toBe(true) }) test('behavior: defaults feeToken to chain default when caller omits it', async () => { // Without the default, the broadcast envelope has no feeToken and // the chain falls back to the sender's account token, which often // lacks FeeAMM liquidity. const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) expect(result.transaction.feeToken?.toLowerCase()).toBe( localnetTokens[0]!.address.toLowerCase(), ) }) test('behavior: preserves caller-supplied feeToken', async () => { const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], feeToken: Relay.addresses.alphaUsd as Address, }) expect(result.transaction.feeToken?.toLowerCase()).toBe( Relay.addresses.alphaUsd.toLowerCase(), ) }) }, ) describe.skipIf(Runtime.get().mode !== 'localnet')( 'behavior: path B — conditional sponsorship (validate)', () => { let server: Server let client: ReturnType let requests: RpcRequest.RpcRequest[] = [] // Reject accounts[3], approve everyone else. const rejectedSender = Relay.accounts[3]! beforeAll(async () => { // rejectedSender is faucet-funded with alphaUsd so it can self-pay. await Actions.fee.setUserToken(Relay.getClient(), { account: rejectedSender, token: Relay.addresses.alphaUsd, }) server = await Relay.createServer( ( await Relay.relay({ features: 'all', feePayer: { account: feePayerAccount, name: 'Path B Sponsor', validate: (request) => request.from?.toLowerCase() !== rejectedSender.address.toLowerCase(), }, onRequest: async (request) => { requests.push(request) }, }) ).listener, ) client = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) afterEach(() => { requests = [] }) test('behavior: approved sender gets sponsorship with parallel fills', async () => { const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) expect(result.transaction.feePayerSignature).toBeDefined() expect(result.capabilities?.['sponsored']).toBe(true) expect(result.capabilities?.['sponsor']?.name).toBe('Path B Sponsor') }) test('behavior: rejected sender gets unsponsored tx from parallel fill', async () => { const result = await fillTransaction(client, { account: rejectedSender.address, calls: [transferCall()], }) expect(result.transaction.feePayerSignature).toBeUndefined() expect(result.capabilities?.['sponsored']).toBe(false) expect(result.capabilities?.['sponsor']).toBeUndefined() }) test('behavior: rejected tx can be signed and broadcast by sender', async () => { const result = await fillTransaction(client, { account: rejectedSender.address, calls: [transferCall()], }) const serialized = (await Transaction.serialize( result.transaction as never, )) as `0x76${string}` const envelope = TxEnvelopeTempo.deserialize(serialized) const signature = await rejectedSender.sign({ hash: TxEnvelopeTempo.getSignPayload(envelope), }) const signed = TxEnvelopeTempo.serialize(envelope, { signature: SignatureEnvelope.from(signature), }) const receipt = (await Relay.getClient().request({ method: 'eth_sendRawTransactionSync' as never, params: [signed], })) as { feePayer?: string | undefined } expect(receipt.feePayer).not.toBe(feePayerAccount.address.toLowerCase()) }) }, ) describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: path C — no sponsorship', () => { let server: Server let client: ReturnType let requests: RpcRequest.RpcRequest[] = [] beforeAll(async () => { server = await Relay.createServer( ( await Relay.relay({ features: 'all', onRequest: async (request) => { requests.push(request) }, }) ).listener, ) client = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) afterEach(() => { requests = [] }) test('behavior: resolves fee token and fills without sponsorship', async () => { const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) expect(result.transaction.feePayerSignature).toBeUndefined() expect(result.capabilities?.['sponsored']).toBe(false) expect(result.capabilities?.['fee']).toBeDefined() // Single fill — no speculative second fill. expect(requests.filter((r) => r.method === 'eth_fillTransaction')).toHaveLength(1) }) test('behavior: simulate and autoSwap metadata resolve concurrently', async () => { const result = await fillTransaction(client, { account: userAccount.address, calls: [transferCall()], }) // Capabilities are populated despite parallel execution. expect(result.transaction.gas).toBeDefined() expect(result.transaction.nonce).toBeDefined() expect(result.capabilities?.['fee']).toBeDefined() expect(result.capabilities?.['sponsored']).toBe(false) }) }) describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: fee token resolution', () => { const feeTokenAccount = Relay.accounts[0]! const preferredToken = Relay.addresses.alphaUsd let server: Server let client: ReturnType beforeAll(async () => { // feeTokenAccount is faucet-funded with alphaUsd so the balance check passes. // Set on-chain fee token preference. await Actions.fee.setUserToken(Relay.getClient(), { account: feeTokenAccount, token: preferredToken, }) server = await Relay.createServer( ( await Relay.relay({ features: 'all', tokens: localnetTokens, }) ).listener, ) client = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) test('behavior: uses explicitly provided feeToken', async () => { const feeToken = '0x20c0000000000000000000000000000000000001' const { transaction } = await fillTransaction(client, { account: feeTokenAccount.address, calls: [transferCall()], feeToken, }) expect(transaction.feeToken).toBe(feeToken) }) test('behavior: resolves to onchain user token when it has balance', async () => { const { transaction } = await fillTransaction(client, { account: feeTokenAccount.address, calls: [transferCall()], }) expect(transaction.feeToken).toBe(preferredToken) }) test('behavior: resolves to highest-balance token from token list', async () => { // Create two fresh TIP-20s and mint different amounts to an account that is // NOT faucet-funded, so only these two balances influence resolution // (a faucet-funded account would carry genesis balances that dominate). // The relay's tokenlist is restricted to just these two so the // highest-balance resolver picks between them deterministically. const freshAccount = Relay.accounts[5]! const rpc = Relay.getClient({ account: Relay.accounts[0]! }) const { token: lowUsd } = await Actions.token.createSync(rpc, { name: 'LowUSD', symbol: 'LowUSD', currency: 'USD', quoteToken: Relay.addresses.alphaUsd, }) const { token: highUsd } = await Actions.token.createSync(rpc, { name: 'HighUSD', symbol: 'HighUSD', currency: 'USD', quoteToken: Relay.addresses.alphaUsd, }) await sendTransactionSync(rpc, { calls: [ Actions.token.grantRoles.call(caller, { token: lowUsd, role: 'issuer', to: rpc.account!.address, }), Actions.token.grantRoles.call(caller, { token: highUsd, role: 'issuer', to: rpc.account!.address, }), Actions.token.mint.call(caller, { token: lowUsd, amount: parseUnits('100', 6), to: freshAccount.address, }), Actions.token.mint.call(caller, { token: highUsd, amount: parseUnits('500', 6), to: freshAccount.address, }), ], }) const customServer = await Relay.createServer( ( await Relay.relay({ features: 'all', tokens: [ { address: lowUsd, decimals: 6, name: 'LowUSD', symbol: 'LowUSD' }, { address: highUsd, decimals: 6, name: 'HighUSD', symbol: 'HighUSD' }, ], }) ).listener, ) const customClient = Relay.getClient({ url: customServer.url }) const { transaction } = await fillTransaction(customClient, { account: freshAccount.address, calls: [ Actions.token.transfer.call(caller, { token: highUsd, to: recipient.address, amount: 1n }), ], }) customServer.close() // highUsd has the higher balance (500 > 100). expect(transaction.feeToken?.toLowerCase()).toBe(highUsd.toLowerCase()) }) test('behavior: falls back to pathUSD when no preference or balances', async () => { const freshAccount = Relay.accounts[10]! const { transaction } = await fillTransaction(client, { account: freshAccount.address, calls: [ Actions.token.transfer.call(caller, { token: Relay.addresses.alphaUsd, to: freshAccount.address, amount: 0n, }), ], }) expect(transaction.feeToken).toBeUndefined() }) }) describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: error capabilities', () => { let server: Server let client: ReturnType beforeAll(async () => { server = await Relay.createServer( ( await Relay.relay({ features: 'all', getClient: Relay.readClient('https://rpc.moderato.tempo.xyz', tempoModerato.id), }) ).listener, ) client = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) // Skipped: this asserts an inline snapshot of the sender's live moderato // `balanceDiffs`, which depends on that account's balances on the hosted // moderato testnet at capture time — not reproducible in the disposable // localnet harness. The sibling error/JSON-RPC error tests below cover the // same relay error-capability behavior without a live-balance snapshot. test.skip('behavior: returns requireFunds on InsufficientBalance when errors capability is enabled', async () => { const sender = Relay.accounts[10]! const result = await fillTransaction(client, { account: sender.address, calls: [ Actions.token.transfer.call(caller, { token: Relay.addresses.alphaUsd, to: recipient.address, amount: parseUnits('100', 6), }), ], capabilities: { errors: true }, }) expect(result.capabilities).toMatchInlineSnapshot(` { "balanceDiffs": { "0x0eB552e73e6f8E0922749e0fB08af2a71ECb2b7F": [ { "address": "0x20c0000000000000000000000000000000000001", "decimals": 6, "direction": "outgoing", "formatted": "100", "name": "AlphaUSD", "recipients": [ "0xAF4311d557fBC876059e39306ec1f3343753df29", ], "symbol": "AlphaUSD", "value": "0x5f5e100", }, ], }, "error": { "abiItem": { "inputs": [ { "name": "available", "type": "uint256", }, { "name": "required", "type": "uint256", }, { "name": "token", "type": "address", }, ], "name": "InsufficientBalance", "type": "error", }, "data": "0x832f98b500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000020c0000000000000000000000000000000000001", "errorName": "InsufficientBalance", "message": "Insufficient balance. Required: 100000000, available: 0.", }, "requireFunds": { "amount": "0x5f5e100", "decimals": 6, "formatted": "100", "symbol": "AlphaUSD", "token": "0x20C0000000000000000000000000000000000001", }, "sponsored": false, } `) }) test('behavior: returns error capability on generic revert when errors capability is enabled', async () => { const sender = Relay.accounts[10]! const result = await fillTransaction(client, { account: sender.address, calls: [ Actions.token.grantRoles.call(caller, { token: Relay.addresses.alphaUsd, role: 'issuer', to: sender.address, }), ], capabilities: { errors: true }, }) expect(result.capabilities).toMatchInlineSnapshot(` { "error": { "abiItem": { "inputs": [], "name": "Unauthorized", "type": "error", }, "data": "0x82b42900", "errorName": "Unauthorized", "message": "Unauthorized.", }, "sponsored": false, } `) }) test('default: throws JSON-RPC error on InsufficientBalance', async () => { const sender = Relay.accounts[10]! const error = await fillTransaction(client, { account: sender.address, calls: [ Actions.token.transfer.call(caller, { token: Relay.addresses.alphaUsd, to: recipient.address, amount: parseUnits('100', 6), }), ], }).then( () => undefined, (e: BaseError) => e, ) expect(error).toBeDefined() // Walk the viem error chain to the underlying RPC error. The default path // surfaces the chain revert as a JSON-RPC error (`code: 3`) with the // ABI-encoded `InsufficientBalance(uint256, uint256, address)` selector. const rpc = error?.walk(isRpcError) as { data?: string } | undefined expect(rpc?.data?.startsWith('0x832f98b5')).toBe(true) }) test('default: throws JSON-RPC error on generic revert', async () => { const sender = Relay.accounts[10]! const error = await fillTransaction(client, { account: sender.address, calls: [ Actions.token.grantRoles.call(caller, { token: Relay.addresses.alphaUsd, role: 'issuer', to: sender.address, }), ], }).then( () => undefined, (e: BaseError) => e, ) expect(error).toBeDefined() const rpc = error?.walk(isRpcError) as { data?: string } | undefined // ABI-encoded `Unauthorized()`. expect(rpc?.data).toBe('0x82b42900') }) test('behavior: explicit `errors: false` matches default JSON-RPC error behavior', async () => { const sender = Relay.accounts[10]! const error = await fillTransaction(client, { account: sender.address, calls: [ Actions.token.transfer.call(caller, { token: Relay.addresses.alphaUsd, to: recipient.address, amount: parseUnits('100', 6), }), ], capabilities: { errors: false } as never, }).then( () => undefined, (e: BaseError) => e, ) expect(error).toBeDefined() const rpc = error?.walk(isRpcError) as { data?: string } | undefined expect(rpc?.data?.startsWith('0x832f98b5')).toBe(true) }) }) function isRpcError(e: unknown): boolean { return typeof e === 'object' && e !== null && 'code' in e && (e as { code: unknown }).code === 3 } // Skipped: this drives a real mainnet fill against the hosted presto RPC using // a hard-coded sender that must hold live USDC.e (and no PathUSD) for autoSwap // to trigger — external state that isn't reproducible in the localnet harness. describe.skip('behavior: mainnet autoSwap with USDC.e → PathUSD', () => { let server: Server let mainnetClient: ReturnType const mainnetUsdce = '0x20c000000000000000000000b9537d11c60e8b50' as const const mainnetPathUsd = '0x20c0000000000000000000000000000000000000' as const const mainnetSender = '0xb472f3ca15f34Db22d43FA503043F1e6541AC085' as const beforeAll(async () => { server = await Relay.createServer( ( await Relay.relay({ features: 'all', getClient: Relay.readClient('https://rpc.presto.tempo.xyz', tempo.id), }) ).listener, ) mainnetClient = Relay.getClient({ url: server.url }) }) afterAll(() => { server.close() }) test('behavior: auto-swaps USDC.e → PathUSD when sender has USDC.e but no PathUSD', async () => { const result = await fillTransaction(mainnetClient, { account: mainnetSender, calls: [ Actions.token.transfer.call(caller, { token: mainnetPathUsd, to: recipient.address, amount: parseUnits('0.5', 6), }), ], }) // Expected: relay detects InsufficientBalance for PathUSD, auto-swaps // USDC.e → PathUSD via DEX, and returns a filled transaction with swap calls. const { transaction, capabilities } = result expect(transaction.gas).toBeDefined() expect(transaction.nonce).toBeDefined() expect(transaction.calls!.length).toBeGreaterThanOrEqual(3) // approve + swap + transfer expect(capabilities?.['autoSwap']).toBeDefined() expect(capabilities?.['autoSwap']?.maxIn.token.toLowerCase()).toBe(mainnetUsdce.toLowerCase()) expect(capabilities?.['autoSwap']?.minOut.token.toLowerCase()).toBe( mainnetPathUsd.toLowerCase(), ) expect(capabilities?.['sponsored']).toBe(false) // Should NOT have requireFunds — autoSwap should handle it. expect((capabilities as Record)?.['requireFunds']).toBeUndefined() }) })