import { AbiFunction, Hex } from 'ox' import { base } from 'viem/chains' import { tempoMainnet } from 'viem/tempo/chains' import * as Anvil from '../../../../test/Anvil.js' import type * as Action from '../Action.js' import type * as Catalog from '../Catalog.js' import * as FundingChain from '../Chain.js' import * as FundingProvider from '../Provider.js' import * as Stargate from './stargate.js' const amount = 1_000_000n const destinationPool = '0x8c76e2f6c5ceda9aa7772e7eff30280226c44392' const destinationToken = '0x20C000000000000000000000b9537d11c60E8b50' 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 sendToken = AbiFunction.from( 'function sendToken((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, (uint72 ticketId, bytes passengerBytes) ticket)', ) 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), verifySourceTransaction: expect.any(Function), }) }) }) describe('validateAction', () => { test('accepts the exact approval and Taxi send calls', () => { expect(validate(action())).toEqual([{ to: pool, value: Hex.fromNumber(nativeFee) }]) }) 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(FundingProvider.ProviderPayloadError) expect(() => validate(action(), { sourceTokenAddress: otherAddress })).toThrowError( FundingProvider.ProviderPayloadError, ) expect(() => validate(action(), { destinationTokenAddress: otherAddress })).toThrowError( FundingProvider.ProviderPayloadError, ) }) test('rejects malformed selectors and mismatched native value', () => { const malformed = action() malformed.calls[1]!.data = '0xdeadbeef' expect(() => validate(malformed)).toThrowError(FundingProvider.ProviderPayloadError) const mismatched = action() mismatched.calls[1]!.value = Hex.fromNumber(nativeFee + 1n) expect(() => validate(mismatched)).toThrowError(FundingProvider.ProviderPayloadError) }) test('rejects missing or mismatched route configuration', () => { expect(() => Stargate.validateAction({ action: action(), destinationTokenAddress: destinationToken, recipient, sender, sourceAmount: amount.toString(), sourceChainId: 'eip155:8453', sourceTokenAddress: sourceToken, }), ).toThrowError(FundingProvider.ProviderPayloadError) expect(() => validate(action(), { configuration: { destinationPoolAddress: destinationPool, poolAddress: otherAddress, sourceEid: 30_184, }, }), ).toThrowError(FundingProvider.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: FundingChain.Chain let destination: Anvil.start.ReturnType let sourceChain: FundingChain.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 = FundingChain.from({ id: 'eip155:4217', name: 'Tempo', rpcUrls: [destination.rpcUrl], slug: 'tempo', }) sourceChain = FundingChain.from({ id: 'eip155:8453', name: 'Base', rpcUrls: [source.rpcUrl], slug: 'base', }) }) afterAll(async () => { await Promise.all([destination?.stop(), source?.stop()]) }) 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, destinationTokenAddress: destinationToken, recipient: bridgedRecipient, sender: bridgedSender, sourceAmount: bridgedAmount, sourceChain, sourceTokenAddress: sourceToken, transactionHash: sourceTransactionHash, validAfter, }), ).resolves.toEqual({ 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, 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, 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 () => { const receipt = await destination.client.getTransactionReceipt({ hash: destinationTransactionHash, }) await expect( Stargate.verifyDestinationDelivery({ configuration, destinationAmount: bridgedAmount, destinationBlockNumber: receipt.blockNumber.toString(), destinationChain, destinationTokenAddress: destinationToken, guid, recipient: bridgedRecipient, sourceChainId: 'eip155:8453', sourceTokenAddress: sourceToken, }), ).resolves.toEqual({ transactionHash: destinationTransactionHash, type: 'verified' }) }) }) 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(sendToken, [ 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, }, 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 } }