import { AbiEvent, AbiFunction, Address, Hash, Hex } from 'ox' import { BlockNotFoundError, parseEventLogs, publicActions, TransactionReceiptNotFoundError, } from 'viem' import { Addresses } from 'viem/tempo' import * as z from 'zod/mini' import * as Schema from '../../Schema.js' import * as Viem from '../../Viem.js' import * as Action from '../Action.js' import type * as Catalog from '../Catalog.js' import * as Chain from '../Chain.js' import * as RoutesProvider from '../Provider.js' import type * as Transfer from '../Transfer.js' const approve = AbiFunction.from('function approve(address spender, uint256 amount)') const approvalEvent = AbiEvent.from( 'event Approval(address indexed owner, address indexed spender, uint256 value)', ) const defaultFetch: RoutesProvider.Fetch = (input, init) => globalThis.fetch(input, init) const assets = { 'eip155:1': '0xA0b86991c6218b36c1d19d4a2e9eb0cE3606eB48', 'eip155:4217': '0x20C000000000000000000000b9537d11c60E8b50', 'eip155:8453': '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', } as const const defaultDestinationPool = '0x8c76e2f6c5ceda9aa7772e7eff30280226c44392' const deployments = { 'eip155:1': { eid: 30_101, endpoint: '0x1a44076050125825900e736c501f859c50fE728c', }, 'eip155:4217': { eid: 30_410, endpoint: '0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C', }, 'eip155:8453': { eid: 30_184, endpoint: '0x1a44076050125825900e736c501f859c50fE728c', }, } as const const oftReceived = AbiEvent.from( 'event OFTReceived(bytes32 indexed guid, uint32 srcEid, address indexed toAddress, uint256 amountReceivedLD)', ) const oftSent = AbiEvent.from( 'event OFTSent(bytes32 indexed guid, uint32 dstEid, address indexed fromAddress, uint256 amountSentLD, uint256 amountReceivedLD)', ) const packetSent = AbiEvent.from( 'event PacketSent(bytes encodedPayload, bytes options, address sendLibrary)', ) const quoteOft = AbiFunction.from( 'function quoteOFT((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) view returns ((uint256 minAmountLD, uint256 maxAmountLD) limit, (int256 feeAmountLD, string description)[] oftFeeDetails, (uint256 amountSentLD, uint256 amountReceivedLD) receipt)', ) const quoteSend = AbiFunction.from( 'function quoteSend((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) view returns ((uint256 nativeFee, uint256 lzTokenFee) fee)', ) 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 endpointNativeToken = AbiFunction.from('function nativeToken() view returns (address)') const wrap = AbiFunction.from('function wrap(address token, address to, uint256 amount)') const quoteTtlMs = 60_000 const maximumLogBlockRange = 10_000n const transferEvent = AbiEvent.from( 'event Transfer(address indexed from, address indexed to, uint256 value)', ) const zeroAddress = '0x0000000000000000000000000000000000000000' /** Stargate provider schemas. */ export namespace schema { /** Configuration captured from one selected Stargate route. */ export const Configuration = z .strictObject({ destinationPoolAddress: z.optional( z .string() .check( z.regex(/^0x[0-9a-fA-F]{40}$/), z.describe('Stargate pool contract on the destination chain.'), ), ), poolAddress: z .string() .check( z.regex(/^0x[0-9a-fA-F]{40}$/), z.describe('Stargate pool contract on the source chain.'), ), sourceEid: z.optional( z .number() .check(z.int(), z.positive(), z.describe('LayerZero endpoint ID for the source chain.')), ), }) .check(z.describe('Stargate configuration for one provider route.')) } const transferProviderState = z.object({ destinationBlockNumber: z.string().check(z.regex(/^\d+$/)), routeConfiguration: schema.Configuration, stargate: z.object({ guid: Schema.Hash, sourceBlock: z.optional( z.object({ hash: Schema.Hash, number: z.string().check(z.regex(/^\d+$/)), }), ), sourceEid: z.number().check(z.int(), z.positive()), }), }) type Deployment = { alternateFee?: { paymentToken: Address.Address token: Address.Address } destinationEid: number destinationPool?: Address.Address | undefined pool: Address.Address sourceEid: number sourceEndpoint: Address.Address sourceToken: Address.Address } type SendParam = { amountLD: bigint composeMsg: Hex.Hex dstEid: number extraOptions: Hex.Hex minAmountLD: bigint oftCmd: Hex.Hex to: Hex.Hex } /** Creates a direct Stargate V2 OFT route provider. */ export function stargate(options: stargate.Options = {}) { const fetch = options.fetch ?? defaultFetch const now = options.now ?? (() => new Date()) const resolveRpcUrls = options.resolveRpcUrls ?? ((chain: Chain.Chain) => chain.rpcUrls) return RoutesProvider.from({ id: 'stargate', name: 'Stargate', async getQuote({ candidate, route }, signal) { const deployment = resolveDeployment({ configuration: candidate.configuration, destinationChainId: route.destination.chain.id, destinationTokenAddress: route.destination.address, sourceChainId: route.source.chain.id, sourceTokenAddress: route.source.address, }) const sampledAt = now() if (!deployment) return unavailable({ message: 'ROUTE_NOT_CONFIGURED', now: sampledAt }) const sendParam = createSendParam({ amount: candidate.sourceAmount.amount, destinationEid: deployment.destinationEid, minimumAmount: '0', recipient: quoteRecipient(), recipientChainId: route.destination.chain.id, }) const [limit, feeDetails, receipt] = await rpcClient({ chainId: Number(Chain.eip155Id(route.source.chain)), fetch, signal, urls: resolveRpcUrls(route.source.chain), }).readContract({ abi: [quoteOft], address: deployment.pool, args: [sendParam], functionName: 'quoteOFT', }) if (!isValidQuote({ amount: candidate.sourceAmount.amount, feeDetails, limit, receipt })) return unavailable({ message: 'NO_EXACT_ROUTE', now: sampledAt }) return { destinationAmountMin: receipt.amountReceivedLD.toString(), destinationAmount: receipt.amountReceivedLD.toString(), quality: quality(), sampledAt: sampledAt.toISOString(), status: 'available', } }, async prepareTransfer({ candidate, method, mode, recipient, sender, slippageBps }, signal) { if ( method !== 'transaction' || mode !== 'exactSource' || !validRecipient(candidate.destinationChain.id, recipient) || !Address.validate(sender, { strict: false }) ) throw new RoutesProvider.ProviderConfigurationError() const deployment = resolveDeployment({ configuration: candidate.configuration, destinationChainId: candidate.destinationChain.id, destinationTokenAddress: candidate.destinationToken.address, sourceChainId: candidate.sourceChain.id, sourceTokenAddress: candidate.sourceToken.address, }) if (!deployment) throw new RoutesProvider.ProviderUnavailableError() const rpc = rpcClient({ chainId: Number(Chain.eip155Id(candidate.route.source.chain)), fetch, signal, urls: resolveRpcUrls(candidate.route.source.chain), }) const quoteParam = createSendParam({ amount: candidate.sourceAmount.amount, destinationEid: deployment.destinationEid, minimumAmount: '0', recipient, recipientChainId: candidate.destinationChain.id, }) const [[limit, feeDetails, receipt], destinationBlock] = await Promise.all([ rpc.readContract({ abi: [quoteOft], address: deployment.pool, args: [quoteParam], functionName: 'quoteOFT', }), rpcClient({ chainId: Number(Chain.eip155Id(candidate.route.destination.chain)), fetch, signal, urls: resolveRpcUrls(candidate.route.destination.chain), }).getBlock({ blockTag: 'finalized', includeTransactions: false }), ]) if (!isValidQuote({ amount: candidate.sourceAmount.amount, feeDetails, limit, receipt })) throw new RoutesProvider.ProviderUnavailableError() const quotedMinimum = (receipt.amountReceivedLD * BigInt(10_000 - (slippageBps ?? 0))) / 10_000n // Route amount schemas require a positive minimum, including at 100% slippage. const destinationAmountMin = quotedMinimum > 0n ? quotedMinimum : 1n const sendParam = createSendParam({ amount: candidate.sourceAmount.amount, destinationEid: deployment.destinationEid, minimumAmount: destinationAmountMin.toString(), recipient, recipientChainId: candidate.destinationChain.id, }) const [endpointFeeToken, messagingFee] = await Promise.all([ rpc.readContract({ abi: [endpointNativeToken], address: deployment.sourceEndpoint, functionName: 'nativeToken', }), rpc.readContract({ abi: [quoteSend], account: sender as Address.Address, address: deployment.pool, args: [sendParam, false], functionName: 'quoteSend', }), ]) if (messagingFee.lzTokenFee !== 0n) throw new RoutesProvider.ProviderPayloadError() if ( (deployment.alternateFee && !Address.isEqual(endpointFeeToken, deployment.alternateFee.token)) || (!deployment.alternateFee && !Address.isEqual(endpointFeeToken, zeroAddress)) ) throw new RoutesProvider.ProviderUnavailableError() const action = { calls: [ { data: AbiFunction.encodeData(approve, [ deployment.pool, BigInt(candidate.sourceAmount.amount), ]), to: deployment.sourceToken, value: '0x0', }, // EndpointV2Alt charges an ERC-20 balance, so mint its fee token before the pool pulls it. ...(deployment.alternateFee && messagingFee.nativeFee > 0n ? [ { data: AbiFunction.encodeData(approve, [ deployment.alternateFee.token, messagingFee.nativeFee, ]), to: deployment.alternateFee.paymentToken, value: '0x0' as const, }, { data: AbiFunction.encodeData(wrap, [ deployment.alternateFee.paymentToken, sender, messagingFee.nativeFee, ]), to: deployment.alternateFee.token, value: '0x0' as const, }, { data: AbiFunction.encodeData(approve, [deployment.pool, messagingFee.nativeFee]), to: deployment.alternateFee.token, value: '0x0' as const, }, ] : []), { data: AbiFunction.encodeData(send, [sendParam, messagingFee, sender]), to: deployment.pool, value: deployment.alternateFee ? '0x0' : Hex.fromNumber(messagingFee.nativeFee), }, ], type: 'evm:calls' as const, } validateAction({ action, configuration: candidate.configuration, destinationAmountMin: destinationAmountMin.toString(), destinationChainId: candidate.destinationChain.id, destinationTokenAddress: candidate.destinationToken.address, recipient, sender, sourceAmount: candidate.sourceAmount.amount, sourceChainId: candidate.sourceChain.id, sourceTokenAddress: candidate.sourceToken.address, }) const sampledAt = now() return { action, destinationAmountMin: destinationAmountMin.toString(), destinationAmount: receipt.amountReceivedLD.toString(), expiresAt: new Date(sampledAt.getTime() + quoteTtlMs).toISOString(), fees: messagingFee.nativeFee === 0n ? [] : [ { amount: messagingFee.nativeFee.toString(), side: 'source' as const, token: feeToken(candidate.sourceChain.id), }, ], providerState: { destinationBlockNumber: destinationBlock.number.toString(), routeConfiguration: candidate.configuration, }, quality: quality(), sampledAt: sampledAt.toISOString(), } }, validateTransferAction({ action, candidate, destinationAmountMin, recipient, sender }) { return validateAction({ action, configuration: candidate.configuration, destinationAmountMin, destinationChainId: candidate.destinationChain.id, destinationTokenAddress: candidate.destinationToken.address, recipient, sender, sourceAmount: candidate.sourceAmount.amount, sourceChainId: candidate.sourceChain.id, sourceTokenAddress: candidate.sourceToken.address, }) }, async verifyDestinationDelivery( { destinationChain, providerState, sourceChain, transfer }, signal, ) { const state = transferProviderState.safeParse(providerState) if (!state.success || !transfer.destinationAmount) return { type: 'invalid' } return verifyDestinationDelivery({ configuration: state.data.routeConfiguration, destinationAmount: transfer.destinationAmount.baseUnits, destinationBlockNumber: state.data.destinationBlockNumber, destinationChain, destinationTokenAddress: transfer.destinationToken.address, fetch, guid: state.data.stargate.guid, recipient: transfer.recipient, rpcUrls: resolveRpcUrls(destinationChain), signal, sourceChainId: sourceChain.id, sourceTokenAddress: transfer.sourceToken.address, }) }, async verifySourceTransaction( { destinationChain, providerState, routeConfiguration, sourceChain, transactionHash, transfer, }, signal, ) { if ( transfer.method !== 'transaction' || transfer.mode !== 'exactSource' || (!transfer.destinationAmountMin && !transfer.destinationAmount) || !transfer.sender ) return { type: 'invalid' } const parsedState = providerState?.['stargate'] === undefined ? undefined : transferProviderState.safeParse(providerState) if (parsedState && !parsedState.success) return { type: 'invalid' } const state = parsedState?.data const configuration = state?.routeConfiguration ?? providerState?.['routeConfiguration'] ?? routeConfiguration const evidence = await verifySourceTransaction({ alternateFeeAmount: quotedAlternateFeeAmount({ sourceChainId: sourceChain.id, transfer, }), configuration, destinationAmountMin: (transfer.destinationAmountMin ?? transfer.destinationAmount!) .baseUnits, destinationChain, destinationTokenAddress: transfer.destinationToken.address, fetch, recipient: transfer.recipient, rpcUrls: resolveRpcUrls(sourceChain), sender: transfer.sender, signal, sourceAmount: transfer.sourceAmount.baseUnits, sourceBlock: state?.stargate.sourceBlock ? { hash: state.stargate.sourceBlock.hash, number: BigInt(state.stargate.sourceBlock.number), } : undefined, sourceChain, sourceTokenAddress: transfer.sourceToken.address, transactionHash, validAfter: transfer.quote.sampledAt, }) if (evidence.type === 'invalid' || evidence.type === 'related') return evidence if (evidence.type === 'reorged') { if (!state) return { type: 'pending' } const { stargate: _stargate, ...providerState } = state return { providerState, type: 'reorged' } } if (evidence.type !== 'verified') return evidence if ( state && (!Hex.isEqual(evidence.guid, state.stargate.guid) || evidence.sourceEid !== state.stargate.sourceEid || evidence.destinationAmount !== transfer.destinationAmount?.baseUnits) ) return { type: 'invalid' } const destinationBlockNumber = state?.destinationBlockNumber ?? (typeof providerState?.['destinationBlockNumber'] === 'string' && /^\d+$/.test(providerState['destinationBlockNumber']) ? providerState['destinationBlockNumber'] : await blockNumberBefore({ chain: destinationChain, fetch, rpcUrls: resolveRpcUrls(destinationChain), signal, timestamp: transfer.quote.sampledAt, })) return { destinationAmount: evidence.destinationAmount, providerState: { ...providerState, destinationBlockNumber, routeConfiguration: configuration, stargate: { guid: evidence.guid, sourceBlock: { hash: evidence.sourceBlock.hash, number: evidence.sourceBlock.number.toString(), }, sourceEid: evidence.sourceEid, }, }, type: 'verified', } }, }) } export declare namespace stargate { /** Stargate provider options. */ type Options = { /** Fetch implementation override for chain RPC requests. */ fetch?: RoutesProvider.Fetch | undefined /** Clock override for deterministic tests. */ now?: (() => Date) | undefined /** Resolves preferred RPC endpoints for each route chain. */ resolveRpcUrls?: ((chain: Chain.Chain) => readonly string[]) | undefined } } /** Validates one direct Stargate action and returns its exact execution allowances. */ export function validateAction(options: validateAction.Options): validateAction.Result { try { const action = Action.schema.EvmCalls.parse(options.action) const deployment = resolveDeployment({ configuration: options.configuration, destinationChainId: options.destinationChainId, destinationTokenAddress: options.destinationTokenAddress, sourceChainId: options.sourceChainId, sourceTokenAddress: options.sourceTokenAddress, }) if (!deployment || ![2, 5].includes(action.calls.length)) throw new RoutesProvider.ProviderPayloadError() const approval = action.calls[0]! const execution = action.calls.at(-1)! validateApproval({ amount: BigInt(options.sourceAmount), call: approval, spender: deployment.pool, token: deployment.sourceToken, }) const messagingFee = validateSend({ data: execution!.data as Hex.Hex, destinationAmountMin: options.destinationAmountMin, destinationEid: deployment.destinationEid, nativeFee: deployment.alternateFee ? undefined : BigInt(execution.value), pool: deployment.pool, recipient: options.recipient, recipientChainId: options.destinationChainId, sender: options.sender, sourceAmount: options.sourceAmount, to: execution!.to as Address.Address, }) if (!deployment.alternateFee) { if (action.calls.length !== 2) throw new RoutesProvider.ProviderPayloadError() return { nativeValues: [{ to: execution.to, value: execution.value }] } } if (BigInt(execution.value) !== 0n) throw new RoutesProvider.ProviderPayloadError() if (messagingFee.nativeFee === 0n) { if (action.calls.length !== 2) throw new RoutesProvider.ProviderPayloadError() return {} } if (action.calls.length !== 5) throw new RoutesProvider.ProviderPayloadError() const [paymentApproval, wrapping, feeApproval] = action.calls.slice(1, 4) validateApproval({ amount: messagingFee.nativeFee, call: paymentApproval!, spender: deployment.alternateFee.token, token: deployment.alternateFee.paymentToken, }) const [paymentToken, receiver, amount] = AbiFunction.decodeData(wrap, wrapping!.data as Hex.Hex) if ( !Address.isEqual(wrapping!.to as Address.Address, deployment.alternateFee.token) || BigInt(wrapping!.value) !== 0n || !Address.isEqual(paymentToken, deployment.alternateFee.paymentToken) || !Address.isEqual(receiver, options.sender as Address.Address) || amount !== messagingFee.nativeFee ) throw new RoutesProvider.ProviderPayloadError() validateApproval({ amount: messagingFee.nativeFee, call: feeApproval!, spender: deployment.pool, token: deployment.alternateFee.token, }) return { tokenApprovals: [ { amount: messagingFee.nativeFee.toString(), spender: deployment.alternateFee.token, token: deployment.alternateFee.paymentToken, }, { amount: messagingFee.nativeFee.toString(), spender: deployment.pool, token: deployment.alternateFee.token, }, ], } } catch (cause) { if (cause instanceof RoutesProvider.ProviderPayloadError) throw cause throw new RoutesProvider.ProviderPayloadError() } } export declare namespace validateAction { /** Provider-specific constraints established by Stargate action validation. */ type Result = { /** Exact native values independently validated against provider calldata. */ nativeValues?: readonly Action.NativeValue[] | undefined /** Exact additional token approvals independently validated against provider calldata. */ tokenApprovals?: readonly Action.TokenApproval[] | undefined } /** Expected route terms for one Stargate action. */ type Options = { /** Prepared Stargate action. */ action: Action.EvmCalls /** Stargate configuration for the selected route. */ configuration?: Catalog.Configuration | undefined /** Minimum destination amount encoded in the send call. */ destinationAmountMin: string /** Destination chain CAIP-2 id. */ destinationChainId: string /** Curated destination token. */ destinationTokenAddress: string /** Destination beneficiary. */ recipient: string /** Source account and LayerZero refund recipient. */ sender: string /** Exact source amount in base units. */ sourceAmount: string /** Source chain CAIP-2 id. */ sourceChainId: string /** Curated source token. */ sourceTokenAddress: string } } /** Verifies a submitted Stargate source transaction against stored transfer terms. */ export async function verifySourceTransaction( options: verifySourceTransaction.Options, ): Promise { const deployment = resolveDeployment({ configuration: options.configuration, destinationChainId: options.destinationChain.id, destinationTokenAddress: options.destinationTokenAddress, sourceChainId: options.sourceChain.id, sourceTokenAddress: options.sourceTokenAddress, }) if ( !deployment || options.sourceChain.kind !== 'evm' || !Hash.validate(options.transactionHash) || !validRecipient(options.destinationChain.id, options.recipient) || !Address.validate(options.sender, { strict: false }) ) return { type: 'invalid' } const rpc = rpcClient({ chainId: Number(Chain.eip155Id(options.sourceChain)), fetch: options.fetch ?? defaultFetch, signal: options.signal, urls: options.rpcUrls ?? options.sourceChain.rpcUrls, }) const hash = options.transactionHash as Hex.Hex const [chainId, receipt] = await Promise.all([ rpc.getChainId(), rpc.getTransactionReceipt({ hash }).catch((cause) => { if (cause instanceof TransactionReceiptNotFoundError) return undefined throw cause }), ]) if (chainId !== Number(Chain.eip155Id(options.sourceChain))) return { type: 'invalid' } const block = !receipt || receipt.blockNumber === null ? undefined : await rpc .getBlock({ blockNumber: receipt.blockNumber, includeTransactions: false }) .catch((cause) => { if (cause instanceof BlockNotFoundError) return undefined throw cause }) if ( !receipt || receipt.blockNumber === null || !block || !Hex.isEqual(block.hash, receipt.blockHash) ) { if (!options.sourceBlock) return { type: 'pending' } const finalized = await rpc.getBlock({ blockTag: 'finalized', includeTransactions: false }) if (finalized.number < options.sourceBlock.number) return { type: 'pending' } const canonical = await rpc .getBlock({ blockNumber: options.sourceBlock.number, includeTransactions: false }) .catch((cause) => { if (cause instanceof BlockNotFoundError) return undefined throw cause }) if (!canonical || Hex.isEqual(canonical.hash, options.sourceBlock.hash)) return { type: 'pending' } return { type: 'reorged' } } if (receipt.status !== 'success') return { type: 'invalid' } const validAfter = Math.floor(Date.parse(options.validAfter) / 1_000) if (!Number.isSafeInteger(validAfter) || block.timestamp < BigInt(validAfter)) return { type: 'invalid' } const amount = BigInt(options.sourceAmount) const destinationAmountMin = BigInt(options.destinationAmountMin) const sourceEvents = parseEventLogs({ abi: [oftSent], logs: receipt.logs, strict: true }).filter( ({ address, args }) => Address.isEqual(address, deployment.pool) && args.dstEid === deployment.destinationEid && Address.isEqual(args.fromAddress, options.sender as Address.Address) && args.amountSentLD === amount && args.amountReceivedLD >= destinationAmountMin, ) const transfers = parseEventLogs({ abi: [transferEvent], logs: receipt.logs, strict: true, }).filter( ({ address, args }) => Address.isEqual(address, deployment.sourceToken) && Address.isEqual(args.from, options.sender as Address.Address) && Address.isEqual(args.to, deployment.pool) && args.value === amount, ) if (sourceEvents.length !== 1 || transfers.length !== 1) { const approvals = parseEventLogs({ abi: [approvalEvent], logs: receipt.logs, strict: true, }) const sourceApprovals = approvals.filter( ({ address, args }) => Address.isEqual(address, deployment.sourceToken) && Address.isEqual(args.owner, options.sender as Address.Address) && Address.isEqual(args.spender, deployment.pool) && args.value === amount, ) if (sourceApprovals.length === 1) return { type: 'related' } if (!deployment.alternateFee || options.alternateFeeAmount === undefined) return { type: 'invalid' } const alternateFee = deployment.alternateFee const alternateFeeAmount = BigInt(options.alternateFeeAmount) const paymentApprovals = approvals.filter( ({ address, args }) => Address.isEqual(address, alternateFee.paymentToken) && Address.isEqual(args.owner, options.sender as Address.Address) && Address.isEqual(args.spender, alternateFee.token) && args.value === alternateFeeAmount, ) if (paymentApprovals.length === 1) return { type: 'related' } const feeApprovals = approvals.filter( ({ address, args }) => Address.isEqual(address, alternateFee.token) && Address.isEqual(args.owner, options.sender as Address.Address) && Address.isEqual(args.spender, deployment.pool) && args.value === alternateFeeAmount, ) if (feeApprovals.length === 1) return { type: 'related' } const relatedTransfers = parseEventLogs({ abi: [transferEvent], logs: receipt.logs, strict: true, }) const paymentTransfers = relatedTransfers.filter( ({ address, args }) => Address.isEqual(address, alternateFee.paymentToken) && Address.isEqual(args.from, options.sender as Address.Address) && Address.isEqual(args.to, alternateFee.token) && args.value === alternateFeeAmount, ) const wrappedTransfers = relatedTransfers.filter( ({ address, args }) => Address.isEqual(address, alternateFee.token) && Address.isEqual(args.from, zeroAddress) && Address.isEqual(args.to, options.sender as Address.Address) && args.value === alternateFeeAmount, ) return paymentTransfers.length === 1 && wrappedTransfers.length === 1 ? { type: 'related' } : { type: 'invalid' } } const sourceEvent = sourceEvents[0]! const destinationAmount = sourceEvent.args.amountReceivedLD const guid = sourceEvent.args.guid // PacketV1's 113-byte header is followed by Stargate's type, asset id, recipient, and amount. const packets = parseEventLogs({ abi: [packetSent], logs: receipt.logs, strict: true }).filter( ({ address, args }) => { const packet = args.encodedPayload return ( Address.isEqual(address, deployment.sourceEndpoint) && Hex.size(packet) === 156 && Hex.toNumber(Hex.slice(packet, 0, 1)) === 1 && Hex.toNumber(Hex.slice(packet, 9, 13)) === deployment.sourceEid && Hex.toNumber(Hex.slice(packet, 45, 49)) === deployment.destinationEid && Hex.isEqual(Hex.slice(packet, 81, 113), guid) && Hex.toNumber(Hex.slice(packet, 113, 114)) === 1 && Hex.isEqual( Hex.slice(packet, 116, 148), recipientBytes32(options.destinationChain.id, options.recipient), ) && Hex.toBigInt(Hex.slice(packet, 148, 156)) === destinationAmount ) }, ) if (packets.length !== 1) return { type: 'invalid' } return { destinationAmount: destinationAmount.toString(), guid, sourceBlock: { hash: block.hash, number: block.number }, sourceEid: deployment.sourceEid, type: 'verified', } } export declare namespace verifySourceTransaction { /** Canonical source block that originally contained the transaction. */ type Block = { /** Source block hash. */ hash: Hex.Hex /** Source block number. */ number: bigint } /** Stored terms and chain access needed to verify one source transaction. */ type Options = { /** Exact alternate fee quoted in the payment token's base units. */ alternateFeeAmount?: string | undefined /** Stargate configuration for the selected route. */ configuration: unknown /** Minimum destination amount committed by the provider action. */ destinationAmountMin: string /** Destination chain and address format. */ destinationChain: Chain.Chain /** Curated destination token. */ destinationTokenAddress: string /** Fetch implementation override for chain RPC requests. */ fetch?: RoutesProvider.Fetch | undefined /** Destination beneficiary encoded in the Stargate send. */ recipient: string /** Preferred source-chain RPC endpoints. */ rpcUrls?: readonly string[] | undefined /** Source account and LayerZero refund recipient. */ sender: string /** Optional request cancellation signal. */ signal?: AbortSignal | undefined /** Exact source amount in base units. */ sourceAmount: string /** Canonical block captured during initial verification. */ sourceBlock?: Block | undefined /** Source chain and RPC endpoints. */ sourceChain: Chain.Chain /** Curated source token. */ sourceTokenAddress: string /** Submitted EVM transaction hash. */ transactionHash: string /** Earliest accepted source block timestamp. */ validAfter: string } /** Source transaction verification outcome. */ type Result = | { type: 'invalid' } | { type: 'pending' } | { type: 'related' } | { type: 'reorged' } | { destinationAmount: string guid: Hex.Hex sourceBlock: Block sourceEid: number type: 'verified' } } function quotedAlternateFeeAmount(options: quotedAlternateFeeAmount.Options) { if (options.sourceChainId !== 'eip155:4217') return undefined const fees = options.transfer.fees.filter( (fee) => fee.side === 'source' && fee.type === 'provider' && Address.validate(fee.token.address, { strict: false }) && Address.isEqual(fee.token.address as Address.Address, Addresses.pathUsd), ) return fees.length === 1 ? fees[0]!.amount.baseUnits : undefined } declare namespace quotedAlternateFeeAmount { type Options = { sourceChainId: string transfer: Transfer.Snapshot } } /** Finds and verifies the included Stargate delivery directly on the destination chain. */ export async function verifyDestinationDelivery( options: verifyDestinationDelivery.Options, ): Promise { const deployment = resolveDeployment({ configuration: options.configuration, destinationChainId: options.destinationChain.id, destinationTokenAddress: options.destinationTokenAddress, sourceChainId: options.sourceChainId, sourceTokenAddress: options.sourceTokenAddress, }) if ( !deployment || !deployment.destinationPool || options.destinationChain.kind !== 'evm' || !/^\d+$/.test(options.destinationBlockNumber) || !Hash.validate(options.guid) || !Address.validate(options.recipient, { strict: false }) ) return { type: 'invalid' } const destinationPool = deployment.destinationPool const rpc = rpcClient({ chainId: Number(Chain.eip155Id(options.destinationChain)), fetch: options.fetch ?? defaultFetch, signal: options.signal, urls: options.rpcUrls ?? options.destinationChain.rpcUrls, }) // Tempo completion favors latency over reorg protection; other destinations retain finality. const blockTag = options.destinationChain.id === 'eip155:4217' ? 'latest' : 'finalized' const [chainId, head] = await Promise.all([ rpc.getChainId(), rpc.getBlock({ blockTag, includeTransactions: false }), ]) if (chainId !== Number(Chain.eip155Id(options.destinationChain))) return { type: 'invalid' } const amount = BigInt(options.destinationAmount) const discovered = await (async () => { for ( let fromBlock = BigInt(options.destinationBlockNumber); fromBlock <= head.number; fromBlock += maximumLogBlockRange ) { const toBlock = fromBlock + maximumLogBlockRange - 1n const logs = await rpc.getLogs({ address: destinationPool, args: { guid: options.guid, toAddress: options.recipient as Address.Address }, event: oftReceived, fromBlock, strict: true, toBlock: toBlock > head.number ? head.number : toBlock, }) const matches = logs.filter( ({ args, transactionHash }) => transactionHash && args.srcEid === deployment.sourceEid && args.amountReceivedLD === amount, ) if (logs.length !== matches.length || matches.length > 1) return { type: 'invalid' as const } if (matches[0]?.transactionHash) return { transactionHash: matches[0].transactionHash, type: 'found' as const } } return { type: 'pending' as const } })() if (discovered.type !== 'found') return discovered const { transactionHash } = discovered const receipt = await rpc.getTransactionReceipt({ hash: transactionHash }).catch((cause) => { if (cause instanceof TransactionReceiptNotFoundError) return undefined throw cause }) if (!receipt || receipt.blockNumber === null) return { type: 'pending' } if (receipt.status !== 'success' || receipt.blockNumber > head.number) return { type: 'invalid' } const transfers = parseEventLogs({ abi: [transferEvent], logs: receipt.logs, strict: true, }).filter( ({ address, args }) => Address.isEqual(address, options.destinationTokenAddress as Address.Address) && Address.isEqual( args.from, options.destinationChain.id === 'eip155:4217' ? zeroAddress : destinationPool, ) && Address.isEqual(args.to, options.recipient as Address.Address) && args.value === amount, ) if (transfers.length !== 1) return { type: 'invalid' } return { transactionHash, type: 'verified' } } export declare namespace verifyDestinationDelivery { /** Stored terms and chain access needed to discover and verify one delivery. */ type Options = { /** Stargate configuration for the selected route. */ configuration: unknown /** Exact destination amount in base units. */ destinationAmount: string /** Destination block captured before the source transaction was broadcast. */ destinationBlockNumber: string /** Destination chain and RPC endpoints. */ destinationChain: Chain.Chain /** Curated destination token. */ destinationTokenAddress: string /** Fetch implementation override for destination RPC requests. */ fetch?: RoutesProvider.Fetch | undefined /** GUID emitted by the verified source Stargate pool. */ guid: Hex.Hex /** Destination beneficiary. */ recipient: string /** Preferred destination-chain RPC endpoints. */ rpcUrls?: readonly string[] | undefined /** Optional request cancellation signal. */ signal?: AbortSignal | undefined /** Source chain CAIP-2 id. */ sourceChainId: string /** Curated source token. */ sourceTokenAddress: string } /** Destination delivery verification outcome. */ type Result = | { type: 'invalid' } | { type: 'pending' } | { transactionHash: Hex.Hex; type: 'verified' } } function rpcClient(options: rpcClient.Options) { if (options.urls.length === 0) throw new RoutesProvider.ProviderConfigurationError() return Viem.createEvmClient(options).extend(publicActions) } declare namespace rpcClient { type Options = { chainId: number fetch: RoutesProvider.Fetch signal?: AbortSignal | undefined urls: readonly string[] } } async function blockNumberBefore(options: blockNumberBefore.Options) { if (options.chain.kind !== 'evm') throw new RoutesProvider.ProviderConfigurationError() const timestamp = Date.parse(options.timestamp) if (!Number.isFinite(timestamp)) throw new RoutesProvider.ProviderPayloadError() const rpc = rpcClient({ chainId: Number(Chain.eip155Id(options.chain)), fetch: options.fetch, signal: options.signal, urls: options.rpcUrls ?? options.chain.rpcUrls, }) const [chainId, finalized] = await Promise.all([ rpc.getChainId(), rpc.getBlock({ blockTag: 'finalized', includeTransactions: false }), ]) if (chainId !== Number(Chain.eip155Id(options.chain))) throw new RoutesProvider.ProviderConfigurationError() const target = BigInt(Math.floor(timestamp / 1_000) - 1) let lower = 0n let upper = finalized.number while (lower < upper) { const middle = (lower + upper + 1n) / 2n const block = await rpc.getBlock({ blockNumber: middle, includeTransactions: false }) if (block.timestamp <= target) lower = middle else upper = middle - 1n } return lower.toString() } declare namespace blockNumberBefore { type Options = { /** Destination chain whose block history is searched. */ chain: Chain.Chain /** Fetch implementation used for RPC requests. */ fetch: RoutesProvider.Fetch /** Preferred chain RPC endpoints. */ rpcUrls?: readonly string[] | undefined /** Optional request cancellation signal. */ signal?: AbortSignal | undefined /** Exclusive ISO 8601 timestamp upper bound. */ timestamp: string } } function createSendParam(options: createSendParam.Options): SendParam { const amount = BigInt(options.amount) return { amountLD: amount, composeMsg: '0x', dstEid: options.destinationEid, extraOptions: '0x', minAmountLD: BigInt(options.minimumAmount), oftCmd: '0x', to: recipientBytes32(options.recipientChainId, options.recipient), } } declare namespace createSendParam { type Options = { amount: string destinationEid: number minimumAmount: string recipient: string recipientChainId: string } } function isValidQuote(options: isValidQuote.Options) { const amount = BigInt(options.amount) const fees = options.feeDetails.reduce((total, fee) => total + fee.feeAmountLD, 0n) return ( amount >= options.limit.minAmountLD && amount <= options.limit.maxAmountLD && options.receipt.amountSentLD === amount && options.receipt.amountReceivedLD > 0n && options.receipt.amountReceivedLD === amount + fees ) } declare namespace isValidQuote { type Options = { amount: string feeDetails: readonly { feeAmountLD: bigint }[] limit: { maxAmountLD: bigint; minAmountLD: bigint } receipt: { amountReceivedLD: bigint; amountSentLD: bigint } } } function feeToken(sourceChainId: string) { if (sourceChainId === 'eip155:4217') return { address: Addresses.pathUsd, currency: 'USD', decimals: 6, name: 'pathUSD', standard: 'TIP-20', symbol: 'pathUSD', tokenKey: `${sourceChainId}/erc20:${Addresses.pathUsd.toLowerCase()}`, verified: true, } return { address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', currency: 'ETH', decimals: 18, name: 'Ether', standard: 'native', symbol: 'ETH', tokenKey: `${sourceChainId}/slip44:60`, verified: true, } } function quality() { return { liquiditySource: 'onchain', sourceDetail: 'stargate:oft-v2:taxi', tier: 'liquid' as const, } } function quoteRecipient() { return zeroAddress } function recipientBytes32(chainId: string, recipient: string) { if (chainId.startsWith('eip155:') && Address.validate(recipient, { strict: false })) return Hex.padLeft(recipient as Hex.Hex, 32) throw new RoutesProvider.ProviderConfigurationError() } function validRecipient(chainId: string, recipient: string) { try { recipientBytes32(chainId, recipient) return true } catch { return false } } function validateApproval(options: validateApproval.Options) { if ( !Address.isEqual(options.call.to as Address.Address, options.token) || BigInt(options.call.value) !== 0n ) throw new RoutesProvider.ProviderPayloadError() const [spender, amount] = AbiFunction.decodeData(approve, options.call.data as Hex.Hex) if (!Address.isEqual(spender, options.spender) || amount !== options.amount) throw new RoutesProvider.ProviderPayloadError() } declare namespace validateApproval { type Options = { amount: bigint call: Action.Call spender: Address.Address token: Address.Address } } function validateSend(options: validateSend.Options) { const [sendParam, messagingFee, refundAddress] = AbiFunction.decodeData(send, options.data) const amount = BigInt(options.sourceAmount) if ( !Address.isEqual(options.to, options.pool) || sendParam.amountLD !== amount || sendParam.composeMsg !== '0x' || sendParam.dstEid !== options.destinationEid || sendParam.extraOptions !== '0x' || sendParam.minAmountLD !== BigInt(options.destinationAmountMin) || sendParam.oftCmd !== '0x' || !Hex.isEqual(sendParam.to, recipientBytes32(options.recipientChainId, options.recipient)) || messagingFee.lzTokenFee !== 0n || (options.nativeFee !== undefined && messagingFee.nativeFee !== options.nativeFee) || !Address.isEqual(refundAddress, options.sender as Address.Address) ) throw new RoutesProvider.ProviderPayloadError() return messagingFee } declare namespace validateSend { type Options = { data: Hex.Hex destinationAmountMin: string destinationEid: number nativeFee?: bigint | undefined pool: Address.Address recipient: string recipientChainId: string sender: string sourceAmount: string to: Address.Address } } function resolveDeployment(options: resolveDeployment.Options): Deployment | undefined { const parsed = schema.Configuration.safeParse(options.configuration) const source = deployments[options.sourceChainId as keyof typeof deployments] const destination = deployments[options.destinationChainId as keyof typeof deployments] const sourceEid = parsed.success && (parsed.data.sourceEid ?? source?.eid) if ( !parsed.success || !destination || !source || !sourceEid || !options.sourceChainId.startsWith('eip155:') || !('endpoint' in source) || !matchesAsset(options.destinationChainId, options.destinationTokenAddress) || !matchesAsset(options.sourceChainId, options.sourceTokenAddress) || options.sourceChainId === options.destinationChainId ) return undefined const destinationPoolAddress = parsed.data.destinationPoolAddress ?? (options.destinationChainId === 'eip155:4217' ? defaultDestinationPool : undefined) return { ...(options.sourceChainId === 'eip155:4217' ? { alternateFee: { paymentToken: Address.checksum(Addresses.pathUsd), token: '0x0cEb237E109eE22374a567c6b09F373C73FA4cBb' as const, }, } : {}), destinationEid: destination.eid, destinationPool: destinationPoolAddress ? Address.checksum(destinationPoolAddress) : undefined, pool: Address.checksum(parsed.data.poolAddress), sourceEid, sourceEndpoint: Address.checksum(source.endpoint), sourceToken: Address.checksum(options.sourceTokenAddress), } } function matchesAsset(chainId: string, address: string) { const asset = assets[chainId as keyof typeof assets] return ( asset !== undefined && Address.validate(address, { strict: false }) && Address.isEqual(address as Address.Address, asset) ) } declare namespace resolveDeployment { type Options = { configuration: unknown destinationChainId: string destinationTokenAddress: string sourceChainId: string sourceTokenAddress: string } } function unavailable(options: unavailable.Options) { return RoutesProvider.unavailableResult({ detail: 'stargate:oft-v2', message: options.message, now: options.now, source: 'onchain', }) } declare namespace unavailable { type Options = { message: string now: Date } }