import { AbiEvent, AbiFunction, Address, Hash, Hex } from 'ox' import { BlockNotFoundError, parseEventLogs, publicActions, TransactionReceiptNotFoundError, } from 'viem' import * as z from 'zod/mini' 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 FundingProvider from '../Provider.js' const approve = AbiFunction.from('function approve(address spender, uint256 amount)') const defaultFetch: FundingProvider.Fetch = (input, init) => globalThis.fetch(input, init) const destinationChainId = 'eip155:4217' const destinationEid = 30_410 const destinationPool = '0x8c76e2f6c5ceda9aa7772e7eff30280226c44392' const destinationToken = '0x20C000000000000000000000b9537d11c60E8b50' const layerZeroEndpoint = '0x1a44076050125825900e736c501f859c50fE728c' 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 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)', ) const quoteTtlMs = 60_000 const maximumLogBlockRange = 100_000n const sourceEids = { 'eip155:1': 30_101, 'eip155:8453': 30_184, } as const 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 Tempo 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 funding route.')) } type Deployment = { destinationPool: Address.Address pool: Address.Address sourceEid: number 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 funding provider. */ export function stargate(options: stargate.Options = {}) { const fetch = options.fetch ?? defaultFetch const now = options.now ?? (() => new Date()) return FundingProvider.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, recipient: zeroAddress, }) const [limit, feeDetails, receipt] = await rpcClient({ fetch, signal, urls: route.source.chain.rpcUrls, }).readContract({ abi: [quoteOft], address: deployment.pool, args: [sendParam], functionName: 'quoteOFT', }) if (!isExactQuote({ 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 }, signal) { if ( method !== 'transaction' || mode !== 'exactSource' || !Address.validate(recipient, { strict: false }) || !Address.validate(sender, { strict: false }) ) throw new FundingProvider.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 FundingProvider.ProviderUnavailableError() const sendParam = createSendParam({ amount: candidate.sourceAmount.amount, recipient }) const rpc = rpcClient({ fetch, signal, urls: candidate.route.source.chain.rpcUrls }) const [[limit, feeDetails, receipt], messagingFee, destinationBlock] = await Promise.all([ rpc.readContract({ abi: [quoteOft], address: deployment.pool, args: [sendParam], functionName: 'quoteOFT', }), rpc.readContract({ abi: [quoteSend], account: sender as Address.Address, address: deployment.pool, args: [sendParam, false], functionName: 'quoteSend', }), rpcClient({ fetch, signal, urls: candidate.route.destination.chain.rpcUrls, }).getBlock({ blockTag: 'finalized', includeTransactions: false }), ]) if (!isExactQuote({ amount: candidate.sourceAmount.amount, feeDetails, limit, receipt })) throw new FundingProvider.ProviderUnavailableError() if (messagingFee.lzTokenFee !== 0n) throw new FundingProvider.ProviderPayloadError() const action = { calls: [ { data: AbiFunction.encodeData(approve, [ deployment.pool, BigInt(candidate.sourceAmount.amount), ]), to: deployment.sourceToken, value: '0x0', }, { data: AbiFunction.encodeData(sendToken, [sendParam, messagingFee, sender]), to: deployment.pool, value: Hex.fromNumber(messagingFee.nativeFee), }, ], type: 'evm:calls' as const, } validateAction({ action, configuration: candidate.configuration, destinationTokenAddress: candidate.destinationToken.address, recipient, sender, sourceAmount: candidate.sourceAmount.amount, sourceChainId: candidate.sourceChain.id, sourceTokenAddress: candidate.sourceToken.address, }) const sampledAt = now() return { action, destinationAmountMin: receipt.amountReceivedLD.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: nativeToken(candidate.sourceChain.id), }, ], providerState: { destinationBlockNumber: destinationBlock.number.toString(), routeConfiguration: candidate.configuration, }, quality: quality(), sampledAt: sampledAt.toISOString(), } }, validateTransferAction({ action, candidate, recipient, sender }) { return { nativeValues: validateAction({ action, configuration: candidate.configuration, destinationTokenAddress: candidate.destinationToken.address, recipient, sender, sourceAmount: candidate.sourceAmount.amount, sourceChainId: candidate.sourceChain.id, sourceTokenAddress: candidate.sourceToken.address, }), } }, async verifySourceTransaction( { destinationChain, providerState, routeConfiguration, sourceChain, transactionHash, transfer, }, signal, ) { if (transfer.method !== 'transaction' || transfer.mode !== 'exactSource' || !transfer.sender) return { type: 'invalid' } const configuration = providerState?.['routeConfiguration'] ?? routeConfiguration const evidence = await verifySourceTransaction({ configuration, destinationTokenAddress: transfer.destinationToken.address, fetch, recipient: transfer.recipient, sender: transfer.sender, signal, sourceAmount: transfer.sourceAmount.baseUnits, sourceChain, sourceTokenAddress: transfer.sourceToken.address, transactionHash, validAfter: transfer.quote.sampledAt, }) if (evidence.type === 'invalid') return evidence if (evidence.type !== 'verified') return { type: 'pending' } const destinationBlockNumber = typeof providerState?.['destinationBlockNumber'] === 'string' && /^\d+$/.test(providerState['destinationBlockNumber']) ? providerState['destinationBlockNumber'] : await blockNumberBefore({ chain: destinationChain, fetch, signal, timestamp: transfer.quote.sampledAt, }) return { 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?: FundingProvider.Fetch | undefined /** Clock override for deterministic tests. */ now?: (() => Date) | undefined } } /** Validates one direct Stargate action and returns its exact native-value allowance. */ export function validateAction(options: validateAction.Options): readonly Action.NativeValue[] { try { const action = Action.schema.EvmCalls.parse(options.action) const deployment = resolveDeployment({ configuration: options.configuration, destinationChainId, destinationTokenAddress: options.destinationTokenAddress, sourceChainId: options.sourceChainId, sourceTokenAddress: options.sourceTokenAddress, }) if (!deployment || action.calls.length !== 2) throw new FundingProvider.ProviderPayloadError() const [approval, execution] = action.calls if ( !Address.isEqual(approval!.to as Address.Address, deployment.sourceToken) || BigInt(approval!.value) !== 0n ) throw new FundingProvider.ProviderPayloadError() const [spender, approvalAmount] = AbiFunction.decodeData(approve, approval!.data as Hex.Hex) if ( !Address.isEqual(spender, deployment.pool) || approvalAmount !== BigInt(options.sourceAmount) ) throw new FundingProvider.ProviderPayloadError() validateSend({ data: execution!.data as Hex.Hex, nativeValue: BigInt(execution!.value), pool: deployment.pool, recipient: options.recipient, sender: options.sender, sourceAmount: options.sourceAmount, to: execution!.to as Address.Address, }) return [{ to: execution!.to, value: execution!.value }] } catch (cause) { if (cause instanceof FundingProvider.ProviderPayloadError) throw cause throw new FundingProvider.ProviderPayloadError() } } export declare namespace validateAction { /** 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 /** Curated Tempo destination token. */ destinationTokenAddress: string /** Tempo 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, destinationTokenAddress: options.destinationTokenAddress, sourceChainId: options.sourceChain.id, sourceTokenAddress: options.sourceTokenAddress, }) if ( !deployment || options.sourceChain.kind !== 'evm' || !Hash.validate(options.transactionHash) || !Address.validate(options.recipient, { strict: false }) || !Address.validate(options.sender, { strict: false }) ) return { type: 'invalid' } const rpc = rpcClient({ fetch: options.fetch ?? defaultFetch, signal: options.signal, urls: 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 sourceEvents = parseEventLogs({ abi: [oftSent], logs: receipt.logs, strict: true }).filter( ({ address, args }) => Address.isEqual(address, deployment.pool) && args.dstEid === destinationEid && Address.isEqual(args.fromAddress, options.sender as Address.Address) && args.amountSentLD === amount && args.amountReceivedLD === amount, ) 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) return { type: 'invalid' } const guid = sourceEvents[0]!.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, layerZeroEndpoint) && 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)) === 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.recipient)) && Hex.toBigInt(Hex.slice(packet, 148, 156)) === amount ) }, ) if (packets.length !== 1) return { type: 'invalid' } return { 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 = { /** Stargate configuration for the selected route. */ configuration: unknown /** Curated Tempo destination token. */ destinationTokenAddress: string /** Fetch implementation override for chain RPC requests. */ fetch?: FundingProvider.Fetch | undefined /** Tempo beneficiary encoded in the Stargate send. */ recipient: string /** 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: 'reorged' } | { guid: Hex.Hex; sourceBlock: Block; sourceEid: number; type: 'verified' } } /** Finds and verifies the finalized Stargate delivery directly on Tempo. */ 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 || options.destinationChain.kind !== 'evm' || !/^\d+$/.test(options.destinationBlockNumber) || !Hash.validate(options.guid) || !Address.validate(options.recipient, { strict: false }) ) return { type: 'invalid' } const rpc = rpcClient({ fetch: options.fetch ?? defaultFetch, urls: options.destinationChain.rpcUrls, }) const [chainId, finalized] = await Promise.all([ rpc.getChainId(), rpc.getBlock({ blockTag: 'finalized', 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 <= finalized.number; fromBlock += maximumLogBlockRange ) { const toBlock = fromBlock + maximumLogBlockRange - 1n const logs = await rpc.getLogs({ address: deployment.destinationPool, args: { guid: options.guid, toAddress: options.recipient as Address.Address }, event: oftReceived, fromBlock, strict: true, toBlock: toBlock > finalized.number ? finalized.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 > finalized.number) return { type: 'invalid' } const transfers = parseEventLogs({ abi: [transferEvent], logs: receipt.logs, strict: true, }).filter( ({ address, args }) => Address.isEqual(address, destinationToken as Address.Address) && Address.isEqual(args.from, zeroAddress) && 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 /** Tempo block captured before the source transaction was broadcast. */ destinationBlockNumber: string /** Tempo chain and RPC endpoints. */ destinationChain: Chain.Chain /** Curated Tempo destination token. */ destinationTokenAddress: string /** Fetch implementation override for Tempo RPC requests. */ fetch?: FundingProvider.Fetch | undefined /** GUID emitted by the verified source Stargate pool. */ guid: Hex.Hex /** Tempo beneficiary. */ recipient: string /** 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 FundingProvider.ProviderConfigurationError() return Viem.createEvmClient(options).extend(publicActions) } declare namespace rpcClient { type Options = { fetch: FundingProvider.Fetch signal?: AbortSignal | undefined urls: readonly string[] } } async function blockNumberBefore(options: blockNumberBefore.Options) { if (options.chain.kind !== 'evm') throw new FundingProvider.ProviderConfigurationError() const timestamp = Date.parse(options.timestamp) if (!Number.isFinite(timestamp)) throw new FundingProvider.ProviderPayloadError() const rpc = rpcClient({ fetch: options.fetch, signal: options.signal, urls: 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 FundingProvider.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: FundingProvider.Fetch /** 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: destinationEid, extraOptions: '0x', minAmountLD: amount, oftCmd: '0x', to: recipientBytes32(options.recipient), } } declare namespace createSendParam { type Options = { amount: string recipient: string } } function isExactQuote(options: isExactQuote.Options) { const amount = BigInt(options.amount) return ( amount >= options.limit.minAmountLD && amount <= options.limit.maxAmountLD && options.feeDetails.every((fee) => fee.feeAmountLD >= 0n) && options.receipt.amountSentLD === amount && options.receipt.amountReceivedLD === amount ) } declare namespace isExactQuote { type Options = { amount: string feeDetails: readonly { feeAmountLD: bigint }[] limit: { maxAmountLD: bigint; minAmountLD: bigint } receipt: { amountReceivedLD: bigint; amountSentLD: bigint } } } function nativeToken(sourceChainId: string) { 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 recipientBytes32(recipient: string) { if (!Address.validate(recipient, { strict: false })) throw new FundingProvider.ProviderConfigurationError() return Hex.padLeft(recipient as Hex.Hex, 32) } function validateSend(options: validateSend.Options) { const [sendParam, messagingFee, refundAddress] = AbiFunction.decodeData(sendToken, options.data) const amount = BigInt(options.sourceAmount) if ( !Address.isEqual(options.to, options.pool) || sendParam.amountLD !== amount || sendParam.composeMsg !== '0x' || sendParam.dstEid !== destinationEid || sendParam.extraOptions !== '0x' || sendParam.minAmountLD !== amount || sendParam.oftCmd !== '0x' || !Hex.isEqual(sendParam.to, recipientBytes32(options.recipient)) || messagingFee.lzTokenFee !== 0n || messagingFee.nativeFee !== options.nativeValue || !Address.isEqual(refundAddress, options.sender as Address.Address) ) throw new FundingProvider.ProviderPayloadError() } declare namespace validateSend { type Options = { data: Hex.Hex nativeValue: bigint pool: Address.Address recipient: string sender: string sourceAmount: string to: Address.Address } } function resolveDeployment(options: resolveDeployment.Options): Deployment | undefined { const parsed = schema.Configuration.safeParse(options.configuration) const sourceEid = parsed.success && (parsed.data.sourceEid ?? sourceEids[options.sourceChainId as keyof typeof sourceEids]) if ( !parsed.success || !sourceEid || options.destinationChainId !== destinationChainId || !options.sourceChainId.startsWith('eip155:') || !Address.validate(options.destinationTokenAddress, { strict: false }) || !Address.validate(options.sourceTokenAddress, { strict: false }) || !Address.isEqual(options.destinationTokenAddress as Address.Address, destinationToken) ) return undefined return { destinationPool: Address.checksum(parsed.data.destinationPoolAddress ?? destinationPool), pool: Address.checksum(parsed.data.poolAddress), sourceEid, sourceToken: Address.checksum(options.sourceTokenAddress), } } declare namespace resolveDeployment { type Options = { configuration: unknown destinationChainId: string destinationTokenAddress: string sourceChainId: string sourceTokenAddress: string } } function unavailable(options: unavailable.Options) { return FundingProvider.unavailableResult({ detail: 'stargate:oft-v2', message: options.message, now: options.now, source: 'onchain', }) } declare namespace unavailable { type Options = { message: string now: Date } }