import { Address as core_Address, Hash, Hex, Value } from 'ox' import { type Address, BaseError, ContractFunctionRevertedError, type LocalAccount, TransactionReceiptNotFoundError, } from 'viem' import { getTransactionReceipt, prepareTransactionRequest, sendRawTransaction, signTransaction, } from 'viem/actions' import { Actions, Addresses } from 'viem/tempo' import * as Fees from '../Fees.js' import type * as Viem from '../Viem.js' import type * as Provider from './Provider.js' const slippageBps = 100n const slippageDenominator = 10_000n /** Maximum subsidized deposit value expressed as a decimal USD amount. */ export type Policy = { /** Maximum source deposit value that Tempo will subsidize. */ maxAmount: string } /** Returns whether one USD stablecoin deposit is within the subsidy policy. */ export function supports(policy: Policy, options: supports.Options): boolean { if ( options.destinationToken.currency !== 'USD' || options.sourceToken.currency !== options.destinationToken.currency ) return false const maximum = (() => { try { return Value.from(policy.maxAmount, options.sourceToken.decimals) } catch { return 0n } })() return maximum > 0n && BigInt(options.amount) <= maximum } export declare namespace supports { /** Route and source amount evaluated against a subsidy policy. */ type Options = { /** Source amount in base units. */ amount: string /** Token that the recipient receives on Tempo. */ destinationToken: Provider.TokenRef /** Token deposited on the source chain. */ sourceToken: Provider.TokenRef } } /** Prepares and submits durable pathUSD-funded subsidy transactions. */ export type Settler = { /** Tempo account that supplies pathUSD. */ account: Address /** Broadcasts one previously persisted signed transaction. */ broadcast(parameters: Settler.broadcast.Parameters): Promise /** Maximum subsidized deposit policy enforced at creation and settlement. */ policy: Policy /** Prepares one signed transaction without broadcasting it. */ prepare(parameters: Settler.prepare.Parameters): Promise /** Reads the receipt state of one persisted subsidy transaction. */ status(parameters: Settler.status.Parameters): Promise } /** Contracts for subsidy settlement operations. */ export declare namespace Settler { /** Contracts for broadcasting one signed subsidy transaction. */ namespace broadcast { /** Persisted transaction passed to the broadcaster. */ type Parameters = { /** Expected transaction hash. */ hash: Hex.Hex /** Signed Tempo transaction bytes. */ transaction: Hex.Hex } } /** Contracts for preparing one subsidy transaction. */ namespace prepare { /** Deposit deficit used to construct a subsidy transaction. */ type Parameters = { /** Destination-token deficit in base units. */ amount: bigint /** Deposit id hashed into the destination transfer memo. */ depositId: string /** Tempo account that receives the subsidy. */ recipient: Address /** Destination token address supplied by Tempo. */ token: Address } /** Signed transaction prepared for durable persistence. */ type ReturnType = { /** Hash of the signed transaction. */ hash: Hex.Hex /** Signed Tempo transaction bytes. */ transaction: Hex.Hex } } /** Contracts for reading subsidy transaction state. */ namespace status { /** Persisted transaction inspected by receipt hash. */ type Parameters = { /** Persisted transaction hash. */ hash: Hex.Hex } /** Receipt state used by reconciliation. */ type ReturnType = | { /** No verified receipt exists yet. */ type: 'pending' } | { /** Tempo gas cost persisted for subsidy accounting, in fee-token base units. */ tempoGasPaid: string /** Verified settlement execution result. */ type: 'reverted' | 'success' } } } /** Creates a subsidy settler backed by a Tempo account that holds pathUSD. */ export function create(options: create.Options): Settler { const client = options.getClient() return { account: options.account.address, async broadcast(parameters) { const hash = await sendRawTransaction(client, { serializedTransaction: parameters.transaction, }) if (!Hex.isEqual(hash, parameters.hash)) throw new TransactionHashMismatchError() }, policy: options.policy, async prepare(parameters) { const destinationToken = parameters.token const direct = core_Address.isEqual(destinationToken, Addresses.pathUsd) const memo = Hash.keccak256(Hex.fromString(parameters.depositId)) const nonceKey = (BigInt(memo) >> 1n) + 1n const maximumAmount = await (async () => { if (direct) return parameters.amount const quotedAmount = await Actions.dex .getBuyQuote(client, { amountOut: parameters.amount, tokenIn: Addresses.pathUsd, tokenOut: destinationToken, }) .catch((cause) => { if (hasRevert(cause, ['InsufficientLiquidity', 'PairDoesNotExist'])) throw new LiquidityUnavailableError() throw cause }) if (quotedAmount === 0n) throw new LiquidityUnavailableError() return ( (quotedAmount * (slippageDenominator + slippageBps) + slippageDenominator - 1n) / slippageDenominator ) })() const calls = direct ? [ Actions.token.transfer.call(client, { amount: parameters.amount, memo, to: parameters.recipient, token: destinationToken, }), ] : [ Actions.token.approve.call(client, { amount: maximumAmount, spender: Addresses.stablecoinDex, token: Addresses.pathUsd, }), Actions.dex.buy.call({ amountOut: parameters.amount, maxAmountIn: maximumAmount, tokenIn: Addresses.pathUsd, tokenOut: destinationToken, }), Actions.token.transfer.call(client, { amount: parameters.amount, memo, to: parameters.recipient, token: destinationToken, }), ] const request = await prepareTransactionRequest(client, { account: options.account, calls, // A stable per-deposit nonce lets durable retries rebroadcast without paying twice. nonceKey, }).catch((cause) => { if (hasRevert(cause, ['InsufficientBalance'])) throw new BalanceUnavailableError() throw cause }) const transaction = await signTransaction(client, { ...request, account: options.account, }) return { hash: Hash.keccak256(transaction), transaction } }, async status(parameters) { const receipt = await getTransactionReceipt(client, parameters).catch((cause) => { if (cause instanceof TransactionReceiptNotFoundError) return undefined throw cause }) if (!receipt) return { type: 'pending' } return { tempoGasPaid: Fees.fromGas(receipt.gasUsed, receipt.effectiveGasPrice).toString(), type: receipt.status === 'success' ? 'success' : 'reverted', } }, } } export declare namespace create { /** Dependencies and policy for one subsidy settler. */ type Options = { /** Tempo account that signs subsidies and holds pathUSD. */ account: LocalAccount /** Trusted Tempo RPC client resolver. */ getClient: Viem.GetClient /** Maximum subsidized deposit policy. */ policy: Policy } } function hasRevert(cause: unknown, names: readonly string[]): boolean { if (!(cause instanceof BaseError)) return false const decoded = cause.walk( (error) => error instanceof ContractFunctionRevertedError && Boolean(error.data?.errorName && names.includes(error.data.errorName)), ) return Boolean(decoded) || names.some((name) => cause.details.includes(`${name}(`)) } /** The subsidy account lacks enough pathUSD. */ export class BalanceUnavailableError extends Error { override name = 'Funding.Subsidy.BalanceUnavailableError' } /** The Tempo DEX cannot quote the required destination amount. */ export class LiquidityUnavailableError extends Error { override name = 'Funding.Subsidy.LiquidityUnavailableError' } /** The RPC returned a hash that differs from the persisted transaction. */ export class TransactionHashMismatchError extends Error { override name = 'Funding.Subsidy.TransactionHashMismatchError' }