import { Hash, Hex } from 'ox' import { BaseError, decodeEventLog, encodeFunctionData, erc20Abi, ExecutionRevertedError, InsufficientFundsError, IntrinsicGasTooHighError, IntrinsicGasTooLowError, NonceMaxValueError, NonceTooLowError, type Address, type LocalAccount, TipAboveFeeCapError, TransactionReceiptNotFoundError, TransactionTypeNotSupportedError, } from 'viem' import { getBalance, getBlock, getTransactionCount, getTransactionReceipt, prepareTransactionRequest, readContract, sendRawTransaction, } from 'viem/actions' import * as Viem from '../Viem.js' import type * as Chain from './Chain.js' import type * as Provider from './Provider.js' /** Read-only destination subsidy inventory available during action creation. */ export type Inventory = { /** Account that supplies destination USDC and native gas. */ account: Address /** Verifies token and gas capacity without accessing signing material. */ check(parameters: Inventory.check.Parameters): Promise /** Operational maximum applied in addition to organization policy. */ policy: { maxAmount: string } } /** Contracts for checking destination subsidy inventory. */ export declare namespace Inventory { /** Contracts for one inventory check. */ namespace check { /** Inputs used to reserve worst-case destination inventory. */ type Parameters = { /** Destination token quantity promised by Tempo. */ amount: bigint /** Destination EVM chain. */ chain: Chain.Chain /** Final transfer recipient. */ recipient: Address /** Inventory already promised by active transfers. */ reservations: Settler.prepare.Reservations /** Destination ERC-20 token. */ token: Address } /** Native gas amount reserved for the promised transfer. */ type Result = { nativeAmount: string } } } /** Signed destination-chain subsidy persisted before broadcast. */ export type Transaction = { /** Destination token quantity supplied by Tempo. */ amount: string /** Destination-chain signer account. */ account: Address /** Destination chain CAIP-2 id. */ chainId: string /** Hash of the signed transaction. */ hash: Hex.Hex /** EOA nonce reserved for this transaction. */ nonce: number /** Worst-case native gas amount reserved for this transaction. */ nativeAmount?: string | undefined /** Signed EVM transaction bytes. */ transaction: Hex.Hex } /** Reads a persisted destination subsidy transaction from private route state. */ export function read(value: unknown): Transaction | undefined { if (typeof value !== 'object' || value === null) return undefined const transaction = value as Record if ( typeof transaction['account'] !== 'string' || !/^0x[0-9a-fA-F]{40}$/.test(transaction['account']) || typeof transaction['amount'] !== 'string' || !/^\d+$/.test(transaction['amount']) || typeof transaction['chainId'] !== 'string' || !/^eip155:\d+$/.test(transaction['chainId']) || typeof transaction['hash'] !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(transaction['hash']) || typeof transaction['nonce'] !== 'number' || !Number.isSafeInteger(transaction['nonce']) || transaction['nonce'] < 0 || (transaction['nativeAmount'] !== undefined && (typeof transaction['nativeAmount'] !== 'string' || !/^\d+$/.test(transaction['nativeAmount']))) || typeof transaction['transaction'] !== 'string' || !/^0x[0-9a-fA-F]+$/.test(transaction['transaction']) ) return undefined return transaction as Transaction } /** Prepares and submits ERC-20 destination subsidy transactions. */ export type Settler = { /** Account that supplies destination USDC and native gas. */ account: Address /** Broadcasts one previously persisted signed transaction. */ broadcast(parameters: { chain: Chain.Chain; transaction: Transaction }): Promise /** Reads the next nonce visible at the destination RPC. */ pendingNonce(parameters: { chain: Chain.Chain }): Promise /** Operational maximum applied in addition to the scoped API-key policy. */ policy: { maxAmount: string } /** Prepares one signed ERC-20 transfer without broadcasting it. */ prepare(parameters: Settler.prepare.Parameters): Promise /** Reads and verifies one persisted subsidy transaction. */ status(parameters: Settler.status.Parameters): Promise } /** Contracts for destination subsidy settlement operations. */ export declare namespace Settler { /** Contracts for preparing one destination subsidy transaction. */ namespace prepare { /** Inputs used to construct the signed ERC-20 transfer. */ type Parameters = { /** Destination token quantity supplied by Tempo. */ amount: bigint /** Destination EVM chain. */ chain: Chain.Chain /** EOA nonce durably reserved for this transfer. */ nonce: number /** Final transfer recipient. */ recipient: Address /** Inventory already promised by unsettled transactions. */ reservations: Settler.prepare.Reservations /** Destination ERC-20 token. */ token: Address } /** Inventory reserved while serializing transactions for one signer. */ type Reservations = { /** Native gas amount promised by unsettled transactions. */ nativeAmount: bigint /** Token amount promised by unsettled transactions. */ tokenAmount: bigint } } /** Contracts for reading a destination subsidy receipt. */ namespace status { /** Persisted transaction and expected transfer terms. */ type Parameters = { /** Destination EVM chain. */ chain: Chain.Chain /** Final transfer recipient. */ recipient: Address /** Persisted subsidy transaction. */ transaction: Transaction /** Destination ERC-20 token. */ token: Address } /** Receipt state used by transfer reconciliation. */ type ReturnType = { broadcast: boolean; type: 'pending' } | { type: 'reverted' | 'success' } } } /** Returns the normalized 1:1 destination amount for two USD stablecoins. */ export function requiredAmount(options: requiredAmount.Options): bigint { if ( options.destinationToken.currency !== 'USD' || options.sourceToken.currency !== options.destinationToken.currency ) throw new UnsupportedTokenError() const amount = BigInt(options.sourceAmount) const difference = options.destinationToken.decimals - options.sourceToken.decimals if (difference >= 0) return amount * 10n ** BigInt(difference) const divisor = 10n ** BigInt(-difference) if (amount % divisor !== 0n) throw new UnsupportedAmountError() return amount / divisor } export declare namespace requiredAmount { /** Source amount and stablecoin metadata used for 1:1 normalization. */ type Options = { /** Token delivered on the destination chain. */ destinationToken: Provider.TokenRef /** Source token quantity in base units. */ sourceAmount: string /** Token spent on the source chain. */ sourceToken: Provider.TokenRef } } /** Creates a read-only EVM destination subsidy inventory checker. */ export function createInventory(options: createInventory.Options): Inventory { const getClient = createGetClient(options.fetch) return { account: options.account, async check(parameters) { try { const request = await checkInventory(getClient, options.account, parameters) return { nativeAmount: request.nativeAmount.toString() } } catch (cause) { if (cause instanceof BalanceUnavailableError || cause instanceof UnsupportedChainError) throw cause throw new InventoryUpstreamError(cause) } }, policy: options.policy, } } export declare namespace createInventory { /** Dependencies for one destination subsidy inventory checker. */ type Options = { /** EVM account funded with destination USDC and native gas. */ account: Address /** Fetch implementation used for destination RPC requests. */ fetch: typeof globalThis.fetch /** Operational maximum destination deficit in decimal USD. */ policy: { maxAmount: string } } } /** Creates an EVM destination subsidy settler. */ export function create(options: create.Options): Settler { const getClient = createGetClient(options.fetch) return { account: options.account.address, async broadcast(parameters) { const client = getClient(parameters.chain) const hash = await sendRawTransaction(client, { serializedTransaction: parameters.transaction.transaction, }).catch(async (cause) => { if (!isNonceTooLowBroadcastError(cause)) throw cause const receipt = await getTransactionReceipt(client, { hash: parameters.transaction.hash, }).catch((receiptCause) => { if (receiptCause instanceof TransactionReceiptNotFoundError) return undefined throw receiptCause }) if (!receipt) throw cause return parameters.transaction.hash }) if (!Hex.isEqual(hash, parameters.transaction.hash)) throw new TransactionHashMismatchError() }, async pendingNonce(parameters) { return getTransactionCount(getClient(parameters.chain), { address: options.account.address, blockTag: 'pending', }) }, policy: options.policy, async prepare(parameters) { const { nativeAmount, request } = await checkInventory(getClient, options.account, parameters) const transaction = await options.account.signTransaction({ ...request, chainId: Number(parameters.chain.id.slice('eip155:'.length)), } as never) return { account: options.account.address, amount: parameters.amount.toString(), chainId: parameters.chain.id, hash: Hash.keccak256(transaction), nativeAmount: nativeAmount.toString(), nonce: parameters.nonce, transaction, } }, async status(parameters) { const client = getClient(parameters.chain) const receipt = await getTransactionReceipt(client, { hash: parameters.transaction.hash, }).catch((cause) => { if (cause instanceof TransactionReceiptNotFoundError) return undefined throw cause }) if (!receipt) return { broadcast: true, type: 'pending' } const [canonical, finalized] = await Promise.all([ getBlock(client, { blockNumber: receipt.blockNumber, includeTransactions: false }), getBlock(client, { blockTag: 'finalized', includeTransactions: false }), ]) if (canonical.hash !== receipt.blockHash || receipt.blockNumber > finalized.number) return { broadcast: false, type: 'pending' } if (receipt.status !== 'success') return { type: 'reverted' } const matched = receipt.logs.some((log) => { if (log.address.toLowerCase() !== parameters.token.toLowerCase()) return false try { const event = decodeEventLog({ abi: erc20Abi, data: log.data, topics: log.topics }) return ( event.eventName === 'Transfer' && event.args.from.toLowerCase() === parameters.transaction.account.toLowerCase() && event.args.to.toLowerCase() === parameters.recipient.toLowerCase() && event.args.value === BigInt(parameters.transaction.amount) ) } catch { return false } }) return { type: matched ? 'success' : 'reverted' } }, } } function createGetClient(fetch: typeof globalThis.fetch) { return (chain: Chain.Chain) => { if (chain.kind !== 'evm' || chain.rpcUrls.length === 0) throw new UnsupportedChainError() return Viem.createEvmClient({ chainId: Number(chain.id.slice('eip155:'.length)), fetch, urls: chain.rpcUrls, }) } } async function checkInventory( getClient: ReturnType, account: Address | LocalAccount, parameters: Inventory.check.Parameters & { nonce?: number | undefined }, ) { const client = getClient(parameters.chain) const address = typeof account === 'string' ? account : account.address const tokenBalance = await readContract(client, { abi: erc20Abi, address: parameters.token, args: [address], functionName: 'balanceOf', }) if (tokenBalance < parameters.amount + parameters.reservations.tokenAmount) throw new BalanceUnavailableError() const request = await prepareTransactionRequest(client, { account, data: encodeFunctionData({ abi: erc20Abi, args: [parameters.recipient, parameters.amount], functionName: 'transfer', }), ...(parameters.nonce === undefined ? {} : { nonce: parameters.nonce }), to: parameters.token, value: 0n, } as never) const gasPrice = request.maxFeePerGas ?? request.gasPrice if (request.gas === undefined || gasPrice === undefined) throw new BalanceUnavailableError() const nativeAmount = request.gas * gasPrice if ((await getBalance(client, { address })) < nativeAmount + parameters.reservations.nativeAmount) throw new BalanceUnavailableError() return { nativeAmount, request } } export declare namespace create { /** Dependencies for one destination subsidy settler. */ type Options = { /** EVM account funded with destination USDC and native gas. */ account: LocalAccount /** Fetch implementation used for destination RPC requests. */ fetch: typeof globalThis.fetch /** Operational maximum destination deficit in decimal USD. */ policy: { maxAmount: string } } } /** Returns whether preparing a fresh transaction cannot recover. */ export function isPermanentPreparationError(cause: unknown): boolean { if (!(cause instanceof Error)) return false if ( cause instanceof BaseError && cause.walk( (error) => error instanceof ExecutionRevertedError || error instanceof InsufficientFundsError || error instanceof IntrinsicGasTooHighError || error instanceof IntrinsicGasTooLowError || error instanceof NonceMaxValueError || error instanceof TransactionTypeNotSupportedError, ) ) return true const message = `${cause.message} ${'details' in cause ? String(cause.details) : ''}` return [ /execution reverted/i, /insufficient funds/i, /intrinsic gas too (?:high|low)/i, /nonce has max value/i, /transaction type not supported/i, ].some((pattern) => pattern.test(message)) } /** Returns whether rebroadcasting the same signed transaction cannot recover. */ export function isPermanentBroadcastError(cause: unknown): boolean { if (isPermanentPreparationError(cause)) return true if (!(cause instanceof Error)) return false if (cause instanceof BaseError && cause.walk((error) => error instanceof TipAboveFeeCapError)) return true const message = `${cause.message} ${'details' in cause ? String(cause.details) : ''}` return /max priority fee per gas higher than max fee per gas|tip (?:above|higher than) fee cap/i.test( message, ) } function isNonceTooLowBroadcastError(cause: unknown): boolean { if (!(cause instanceof Error)) return false if (cause instanceof BaseError && cause.walk((error) => error instanceof NonceTooLowError)) return true const message = `${cause.message} ${'details' in cause ? String(cause.details) : ''}` return /nonce too low/i.test(message) } /** The subsidy account lacks destination token inventory or native gas. */ export class BalanceUnavailableError extends Error { override name = 'Routes.TransferSubsidy.BalanceUnavailableError' } /** A destination RPC prevented inventory verification. */ export class InventoryUpstreamError extends Error { override name = 'Routes.TransferSubsidy.InventoryUpstreamError' constructor(cause: unknown) { super('Destination subsidy inventory could not be verified.', { cause }) } } /** The route cannot preserve its denomination across the selected tokens. */ export class UnsupportedTokenError extends Error { override name = 'Routes.TransferSubsidy.UnsupportedTokenError' } /** The source amount cannot be represented exactly in destination base units. */ export class UnsupportedAmountError extends Error { override name = 'Routes.TransferSubsidy.UnsupportedAmountError' } /** The destination chain cannot execute an EVM subsidy transfer. */ export class UnsupportedChainError extends Error { override name = 'Routes.TransferSubsidy.UnsupportedChainError' } /** The RPC returned a hash that differs from the persisted transaction. */ export class TransactionHashMismatchError extends Error { override name = 'Routes.TransferSubsidy.TransactionHashMismatchError' }