import { AbiFunction, Address, Hex } from 'ox' import { base } from 'viem/chains' import { tempoMainnet } from 'viem/tempo/chains' import * as Anvil from '../../../../test/Anvil.js' import * as TestRoutes from '../../../../test/Routes.js' import type * as Action from '../Action.js' import type * as Catalog from '../Catalog.js' import * as RoutesChain from '../Chain.js' import * as RoutesProvider from '../Provider.js' import * as Stargate from './stargate.js' const amount = 1_000_000n const destinationPool = '0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392' const destinationToken = '0x20C000000000000000000000b9537d11c60E8b50' const feePaymentToken = '0x20c0000000000000000000000000000000000000' const feePaymentTokenChecksum = '0x20C0000000000000000000000000000000000000' const feeToken = '0x0cEb237E109eE22374a567c6b09F373C73FA4cBb' const nativeFee = 10_000n const otherAddress = '0x9999999999999999999999999999999999999999' const pool = '0x27a16dc786820B16E5c9028b75B99F6f604b5d26' const recipient = '0x1111111111111111111111111111111111111111' const sender = '0x2222222222222222222222222222222222222222' const sourceToken = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' const sourceTransactionHash = '0xfd71359adf5095f2ab512a19e0a2df6c8cc0db14ac8a52286d61287daabdccd4' const validAfter = '2020-01-01T00:00:00.000Z' const destinationTransactionHash = '0x3dfaba0a78b8f4bc09e55e05211c8120729e7b6879efe32613883befbca666da' const guid = '0x5549d117c8b23f0ecdf05c2604ed989cf0034f378707bd81319e93834f2f85f2' const approve = AbiFunction.from('function approve(address spender, uint256 amount)') const send = AbiFunction.from( 'function send((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, (uint256 nativeFee, uint256 lzTokenFee) fee, address refundAddress) payable returns ((bytes32 guid, uint64 nonce, (uint256 nativeFee, uint256 lzTokenFee) fee) msgReceipt, (uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', ) const wrap = AbiFunction.from('function wrap(address token, address to, uint256 amount)') type RpcRequest = { /** JSON-RPC method name. */ method?: string /** JSON-RPC positional parameters. */ params?: readonly unknown[] } describe('stargate', () => { test('creates a credential-free quote and transfer provider', () => { expect(Stargate.stargate()).toMatchObject({ getQuote: expect.any(Function), id: 'stargate', name: 'Stargate', prepareTransfer: expect.any(Function), verifyDestinationDelivery: expect.any(Function), verifySourceTransaction: expect.any(Function), }) }) }) describe('validateAction', () => { test('accepts the exact approval and Taxi send calls', () => { expect(validate(action())).toEqual({ nativeValues: [{ to: pool, value: Hex.fromNumber(nativeFee) }], }) }) test('accepts Tempo sends to EVM destinations', () => { const outbound = alternateAction() expect( Stargate.validateAction({ action: outbound, configuration: { destinationPoolAddress: pool, poolAddress: destinationPool, }, destinationAmountMin: (amount - 101n).toString(), destinationChainId: 'eip155:8453', destinationTokenAddress: sourceToken, recipient, sender, sourceAmount: amount.toString(), sourceChainId: 'eip155:4217', sourceTokenAddress: destinationToken, }), ).toEqual({ tokenApprovals: [ { amount: nativeFee.toString(), spender: feeToken, token: feePaymentTokenChecksum }, { amount: nativeFee.toString(), spender: destinationPool, token: feeToken }, ], }) }) test('rejects malformed Tempo alternate-fee calls', () => { const invalid = [ (() => { const candidate = alternateAction() candidate.calls[1]!.data = AbiFunction.encodeData(approve, [feeToken, nativeFee + 1n]) return candidate })(), (() => { const candidate = alternateAction() candidate.calls[2]!.data = AbiFunction.encodeData(wrap, [ feePaymentTokenChecksum, sender, nativeFee + 1n, ]) return candidate })(), (() => { const candidate = alternateAction() candidate.calls[3]!.data = AbiFunction.encodeData(approve, [ destinationPool, nativeFee + 1n, ]) return candidate })(), (() => { const candidate = alternateAction() candidate.calls[4]!.value = Hex.fromNumber(nativeFee) return candidate })(), { ...alternateAction(), calls: alternateAction().calls.slice(0, 4) }, ] for (const candidate of invalid) expect(() => Stargate.validateAction({ action: candidate, configuration: { destinationPoolAddress: pool, poolAddress: destinationPool, }, destinationAmountMin: (amount - 101n).toString(), destinationChainId: 'eip155:8453', destinationTokenAddress: sourceToken, recipient, sender, sourceAmount: amount.toString(), sourceChainId: 'eip155:4217', sourceTokenAddress: destinationToken, }), ).toThrowError(RoutesProvider.ProviderPayloadError) }) test('rejects calls outside the curated Stargate route', () => { const invalid = [ action({ approvalAmount: amount + 1n }), action({ approvalSpender: otherAddress }), action({ composeMsg: '0x01' }), action({ destinationEid: 1 }), action({ executionTarget: otherAddress }), action({ extraOptions: '0x01' }), action({ lzTokenFee: 1n }), action({ minimumAmount: amount - 1n }), action({ oftCmd: '0x01' }), action({ recipient: otherAddress }), action({ refundAddress: otherAddress }), action({ sourceAmount: amount + 1n }), ] for (const candidate of invalid) expect(() => validate(candidate)).toThrowError(RoutesProvider.ProviderPayloadError) expect(() => validate(action(), { sourceTokenAddress: otherAddress })).toThrowError( RoutesProvider.ProviderPayloadError, ) expect(() => validate(action(), { destinationTokenAddress: otherAddress })).toThrowError( RoutesProvider.ProviderPayloadError, ) }) test('rejects malformed selectors and mismatched native value', () => { const malformed = action() malformed.calls[1]!.data = '0xdeadbeef' expect(() => validate(malformed)).toThrowError(RoutesProvider.ProviderPayloadError) const mismatched = action() mismatched.calls[1]!.value = Hex.fromNumber(nativeFee + 1n) expect(() => validate(mismatched)).toThrowError(RoutesProvider.ProviderPayloadError) }) test('rejects missing or mismatched route configuration', () => { expect(() => Stargate.validateAction({ action: action(), destinationAmountMin: amount.toString(), destinationChainId: 'eip155:4217', destinationTokenAddress: destinationToken, recipient, sender, sourceAmount: amount.toString(), sourceChainId: 'eip155:8453', sourceTokenAddress: sourceToken, }), ).toThrowError(RoutesProvider.ProviderPayloadError) expect(() => validate(action(), { configuration: { destinationPoolAddress: destinationPool, poolAddress: otherAddress, sourceEid: 30_184, }, }), ).toThrowError(RoutesProvider.ProviderPayloadError) }) }) describe.skipIf(!Anvil.available)('source and destination evidence', () => { const bridgedAmount = '200000' const bridgedRecipient = '0x7212623e4cff9ad010d28aa065fad2bf58578783' const bridgedSender = '0x52b650a62e384f68880d0a1dc2b84dfed47d5660' const configuration = { destinationPoolAddress: destinationPool, poolAddress: pool, sourceEid: 30_184, } let destinationChain: RoutesChain.Chain let destination: Anvil.start.ReturnType let sourceChain: RoutesChain.Chain let source: Anvil.start.ReturnType beforeAll(async () => { ;[destination, source] = await Promise.all([ Anvil.start({ chain: tempoMainnet, forkUrl: tempoMainnet.rpcUrls.default.http[0]! }), Anvil.start({ chain: base, forkUrl: base.rpcUrls.default.http[0]! }), ]) destinationChain = RoutesChain.from({ id: 'eip155:4217', name: 'Tempo', rpcUrls: [destination.rpcUrl], slug: 'tempo', }) sourceChain = RoutesChain.from({ id: 'eip155:8453', name: 'Base', rpcUrls: [source.rpcUrl], slug: 'base', }) }) afterAll(async () => { await Promise.all([destination?.stop(), source?.stop()]) }) test('quotes the deployed Tempo pool for an outbound transfer', async () => { const provider = Stargate.stargate({ resolveRpcUrls: (chain) => chain.id === destinationChain.id ? [destination.rpcUrl] : chain.rpcUrls, }) const [original] = RoutesProvider.getQuoteCandidates({ catalog: TestRoutes.snapshot, destinationChain: 'base', destinationToken: 'USDC', provider: 'stargate', providers: [provider], sourceAmount: amount.toString(), sourceAmountUnits: 'baseUnits', sourceChain: 'tempo', sourceToken: 'USDC.e', }) if (!original) throw new Error('Missing outbound Stargate candidate.') const candidate = { ...original, route: { ...original.route, destination: { ...original.route.destination, chain: sourceChain }, source: { ...original.route.source, chain: RoutesChain.from({ aliases: destinationChain.aliases, id: destinationChain.id, name: destinationChain.name, rpcUrls: ['https://catalog.invalid'], slug: destinationChain.slug, }), }, }, } const result = await provider.getQuote( { candidate, cache: { bypass: false }, destinationChain: candidate.destinationChain, destinationToken: candidate.destinationToken, request: { destinationChain: 'base', destinationToken: 'USDC', provider: 'stargate', sourceAmount: amount.toString(), sourceAmountUnits: 'baseUnits', sourceChain: 'tempo', sourceToken: 'USDC.e', }, route: candidate.route, sourceAmount: candidate.sourceAmount, sourceChain: candidate.sourceChain, sourceToken: candidate.sourceToken, }, new AbortController().signal, ) expect(result.status).toBe('available') if (result.status !== 'available' || !result.destinationAmount) return expect(result).toMatchObject({ destinationAmountMin: result.destinationAmount, quality: { liquiditySource: 'onchain', sourceDetail: 'stargate:oft-v2:taxi', tier: 'liquid', }, status: 'available', }) expect(BigInt(result.destinationAmount)).toBeGreaterThan(0n) expect(BigInt(result.destinationAmount)).toBeLessThan(amount) }) test('keeps native value for a conventional source endpoint', async () => { const provider = Stargate.stargate() const [original] = RoutesProvider.getQuoteCandidates({ catalog: TestRoutes.snapshot, destinationChain: 'tempo', destinationToken: 'USDC.e', provider: 'stargate', providers: [provider], sourceAmount: amount.toString(), sourceAmountUnits: 'baseUnits', sourceChain: 'base', sourceToken: 'USDC', }) if (!original) throw new Error('Missing inbound Stargate candidate.') const candidate = { ...original, route: { ...original.route, destination: { ...original.route.destination, chain: destinationChain }, source: { ...original.route.source, chain: sourceChain }, }, } const result = await provider.prepareTransfer!( { candidate, method: 'transaction', mode: 'exactSource', recipient, sender, }, new AbortController().signal, ) expect(result.action.type).toBe('evm:calls') if (result.action.type !== 'evm:calls') return expect(result.action.calls).toHaveLength(2) const [, messagingFee] = AbiFunction.decodeData(send, result.action.calls[1]!.data as Hex.Hex) expect(BigInt(result.action.calls[1]!.value)).toBe(messagingFee.nativeFee) expect(result.fees).toMatchObject([ { amount: messagingFee.nativeFee.toString(), side: 'source', token: { standard: 'native', symbol: 'ETH' }, }, ]) }) test('encodes caller slippage in an outbound Tempo send', async () => { const provider = Stargate.stargate() const [original] = RoutesProvider.getQuoteCandidates({ catalog: TestRoutes.snapshot, destinationChain: 'base', destinationToken: 'USDC', provider: 'stargate', providers: [provider], sourceAmount: amount.toString(), sourceAmountUnits: 'baseUnits', sourceChain: 'tempo', sourceToken: 'USDC.e', }) if (!original) throw new Error('Missing outbound Stargate candidate.') const candidate = { ...original, route: { ...original.route, destination: { ...original.route.destination, chain: sourceChain }, source: { ...original.route.source, chain: destinationChain }, }, } const result = await provider.prepareTransfer!( { candidate, method: 'transaction', mode: 'exactSource', recipient, sender, slippageBps: 100, }, new AbortController().signal, ) const expectedMinimum = (BigInt(result.destinationAmount) * 9_900n) / 10_000n expect(result.destinationAmountMin).toBe(expectedMinimum.toString()) expect(result.action.type).toBe('evm:calls') if (result.action.type !== 'evm:calls') return expect(result.fees).toMatchObject([ { amount: expect.stringMatching(/^\d+$/), side: 'source', token: { address: feePaymentToken, standard: 'TIP-20', symbol: 'pathUSD', }, }, ]) expect(result.action.calls).toHaveLength(5) expect(result.action.calls.every((call) => BigInt(call.value) === 0n)).toBe(true) const [paymentSpender, paymentAmount] = AbiFunction.decodeData( approve, result.action.calls[1]!.data as Hex.Hex, ) expect(Address.isEqual(paymentSpender, feeToken)).toBe(true) const [paymentToken, receiver, wrappedAmount] = AbiFunction.decodeData( wrap, result.action.calls[2]!.data as Hex.Hex, ) expect(Address.isEqual(paymentToken, feePaymentTokenChecksum)).toBe(true) expect(Address.isEqual(receiver, sender)).toBe(true) expect(wrappedAmount).toBe(paymentAmount) const [feeSpender, feeAmount] = AbiFunction.decodeData( approve, result.action.calls[3]!.data as Hex.Hex, ) expect(Address.isEqual(feeSpender, destinationPool)).toBe(true) expect(feeAmount).toBe(paymentAmount) const [sendParam] = AbiFunction.decodeData(send, result.action.calls[4]!.data as Hex.Hex) expect(sendParam.minAmountLD).toBe(expectedMinimum) }) test('verifies a direct Base Stargate send and its LayerZero GUID', async () => { const receipt = await source.client.getTransactionReceipt({ hash: sourceTransactionHash }) await expect( Stargate.verifySourceTransaction({ configuration, destinationAmountMin: bridgedAmount, destinationChain, destinationTokenAddress: destinationToken, recipient: bridgedRecipient, sender: bridgedSender, sourceAmount: bridgedAmount, sourceChain, sourceTokenAddress: sourceToken, transactionHash: sourceTransactionHash, validAfter, }), ).resolves.toEqual({ destinationAmount: bridgedAmount, guid, sourceBlock: { hash: receipt.blockHash, number: receipt.blockNumber }, sourceEid: 30_184, type: 'verified', }) }) test('keeps missing transactions pending and rejects mismatched terms', async () => { const options = { configuration, destinationAmountMin: bridgedAmount, destinationChain, destinationTokenAddress: destinationToken, recipient: bridgedRecipient, sender: bridgedSender, sourceAmount: bridgedAmount, sourceChain, sourceTokenAddress: sourceToken, transactionHash: sourceTransactionHash, validAfter, } await expect( Stargate.verifySourceTransaction({ ...options, transactionHash: `0x${'99'.repeat(32)}`, }), ).resolves.toEqual({ type: 'pending' }) await expect( Stargate.verifySourceTransaction({ ...options, recipient: otherAddress }), ).resolves.toEqual({ type: 'invalid' }) await expect( Stargate.verifySourceTransaction({ ...options, validAfter: '2099-01-01T00:00:00.000Z' }), ).resolves.toEqual({ type: 'invalid' }) }) test('rejects reverted and malformed source transactions', async () => { const account = Anvil.getAccount(1) const reverted = await source.client.sendTransactionSync({ account, data: '0xdeadbeef', gas: 100_000n, to: pool, }) const malformed = await source.client.sendTransactionSync({ account, data: '0xdeadbeef', gas: 100_000n, to: account.address, }) const options = { configuration, destinationAmountMin: bridgedAmount, destinationChain, destinationTokenAddress: destinationToken, recipient: bridgedRecipient, sender: account.address, sourceAmount: bridgedAmount, sourceChain, sourceTokenAddress: sourceToken, validAfter, } await expect( Stargate.verifySourceTransaction({ ...options, transactionHash: reverted.transactionHash, }), ).resolves.toEqual({ type: 'invalid' }) await expect( Stargate.verifySourceTransaction({ ...options, transactionHash: malformed.transactionHash, }), ).resolves.toEqual({ type: 'invalid' }) }) test('discovers and verifies the exact Tempo USDC.e delivery', async () => { let blockTag: unknown const receipt = await destination.client.getTransactionReceipt({ hash: destinationTransactionHash, }) await expect( Stargate.verifyDestinationDelivery({ configuration, destinationAmount: bridgedAmount, destinationBlockNumber: receipt.blockNumber.toString(), destinationChain, destinationTokenAddress: destinationToken, fetch: async (input, init) => { if (typeof init?.body !== 'string') throw new Error('Expected a JSON request body.') const request = JSON.parse(init.body) as RpcRequest if (request.method === 'eth_getBlockByNumber') blockTag = request.params?.[0] return globalThis.fetch(input, init) }, guid, recipient: bridgedRecipient, sourceChainId: 'eip155:8453', sourceTokenAddress: sourceToken, }), ).resolves.toEqual({ transactionHash: destinationTransactionHash, type: 'verified' }) expect(blockTag).toBe('latest') }) test('verifies a fee-bearing Tempo send to Base', async () => { const transactionHash = '0x9664a5b8dff87c44181abb174b295fd91b2eedae5cff6dacfbd83ab3d8350e88' const receipt = await destination.client.getTransactionReceipt({ hash: transactionHash }) await expect( Stargate.verifySourceTransaction({ configuration: { destinationPoolAddress: pool, poolAddress: destinationPool, }, destinationAmountMin: '4999999', destinationChain: sourceChain, destinationTokenAddress: sourceToken, recipient: '0x10a3097afe2c7ebc8f1c621bc9bb371c4dff76a8', sender: '0x85b0b03ed6fc421ca55aaa3a55723a70701908b8', sourceAmount: '5000501', sourceChain: destinationChain, sourceTokenAddress: destinationToken, transactionHash, validAfter: '2026-09-01T03:00:00.000Z', }), ).resolves.toEqual({ destinationAmount: '5000000', guid: '0xc456bbb98940eee646c7f9e12d6cd211fe28c510e335f10c2381532358abdd0c', sourceBlock: { hash: receipt.blockHash, number: receipt.blockNumber }, sourceEid: 30_410, type: 'verified', }) }) test('discovers and verifies the Base USDC delivery', async () => { let blockTag: unknown const transactionHash = '0xdf95be7fd90f6530144053d3e8ee2c73537c826eea1bf8ef2dbbc3d6c8f30712' const receipt = await source.client.getTransactionReceipt({ hash: transactionHash }) await expect( Stargate.verifyDestinationDelivery({ configuration: { destinationPoolAddress: pool, poolAddress: destinationPool, }, destinationAmount: '5000000', destinationBlockNumber: receipt.blockNumber.toString(), destinationChain: sourceChain, destinationTokenAddress: sourceToken, fetch: async (input, init) => { if (typeof init?.body !== 'string') throw new Error('Expected a JSON request body.') const request = JSON.parse(init.body) as RpcRequest if (request.method === 'eth_getBlockByNumber') blockTag = request.params?.[0] return globalThis.fetch(input, init) }, guid: '0xc456bbb98940eee646c7f9e12d6cd211fe28c510e335f10c2381532358abdd0c', recipient: '0x10a3097afe2c7ebc8f1c621bc9bb371c4dff76a8', sourceChainId: 'eip155:4217', sourceTokenAddress: destinationToken, }), ).resolves.toEqual({ transactionHash, type: 'verified' }) expect(blockTag).toBe('finalized') }) }) function alternateAction(): Action.EvmCalls { const sendParam = { amountLD: amount, composeMsg: '0x', dstEid: 30_184, extraOptions: '0x', minAmountLD: amount - 101n, oftCmd: '0x', to: Hex.padLeft(recipient, 32), } as const return { calls: [ { data: AbiFunction.encodeData(approve, [destinationPool, amount]), to: destinationToken, value: '0x0', }, { data: AbiFunction.encodeData(approve, [feeToken, nativeFee]), to: feePaymentTokenChecksum, value: '0x0', }, { data: AbiFunction.encodeData(wrap, [feePaymentTokenChecksum, sender, nativeFee]), to: feeToken, value: '0x0', }, { data: AbiFunction.encodeData(approve, [destinationPool, nativeFee]), to: feeToken, value: '0x0', }, { data: AbiFunction.encodeData(send, [sendParam, { lzTokenFee: 0n, nativeFee }, sender]), to: destinationPool, value: '0x0', }, ], type: 'evm:calls', } } function action(options: action.Options = {}): Action.EvmCalls { const executionFee = options.nativeFee ?? nativeFee const sendParam = { amountLD: options.sourceAmount ?? amount, composeMsg: options.composeMsg ?? '0x', dstEid: options.destinationEid ?? 30_410, extraOptions: options.extraOptions ?? '0x', minAmountLD: options.minimumAmount ?? amount, oftCmd: options.oftCmd ?? '0x', to: Hex.padLeft(options.recipient ?? recipient, 32), } as const return { calls: [ { data: AbiFunction.encodeData(approve, [ options.approvalSpender ?? pool, options.approvalAmount ?? amount, ]), to: sourceToken, value: '0x0', }, { data: AbiFunction.encodeData(send, [ sendParam, { lzTokenFee: options.lzTokenFee ?? 0n, nativeFee: executionFee }, options.refundAddress ?? sender, ]), to: options.executionTarget ?? pool, value: Hex.fromNumber(executionFee), }, ], type: 'evm:calls', } } declare namespace action { type Options = { approvalAmount?: bigint | undefined approvalSpender?: `0x${string}` | undefined composeMsg?: Hex.Hex | undefined destinationEid?: number | undefined executionTarget?: `0x${string}` | undefined extraOptions?: Hex.Hex | undefined lzTokenFee?: bigint | undefined minimumAmount?: bigint | undefined nativeFee?: bigint | undefined oftCmd?: Hex.Hex | undefined recipient?: Hex.Hex | undefined refundAddress?: `0x${string}` | undefined sourceAmount?: bigint | undefined } } function validate(candidate: Action.EvmCalls, options: validate.Options = {}) { return Stargate.validateAction({ action: candidate, configuration: options.configuration ?? { destinationPoolAddress: destinationPool, poolAddress: pool, sourceEid: 30_184, }, destinationAmountMin: amount.toString(), destinationChainId: 'eip155:4217', destinationTokenAddress: options.destinationTokenAddress ?? destinationToken, recipient, sender, sourceAmount: amount.toString(), sourceChainId: 'eip155:8453', sourceTokenAddress: options.sourceTokenAddress ?? sourceToken, }) } declare namespace validate { type Options = { configuration?: Catalog.Configuration | undefined destinationTokenAddress?: string | undefined sourceTokenAddress?: string | undefined } }