import { Interface, JsonRpcProvider, Wallet, formatEther } from 'ethers'; import { readFileSync } from 'fs'; const SWAP_ABI = [ 'function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut) external returns (uint256)', ]; export interface ExecutorAllowlist { chainId: number; allowedTo: string[]; /** Max msg.value in wei (string). */ maxValueWei: string; /** Optional max fee per gas in gwei (omit = no cap). */ maxFeePerGasGwei?: number; } export function loadAllowlist(path: string): ExecutorAllowlist { const raw = readFileSync(path, 'utf8'); const j = JSON.parse(raw) as ExecutorAllowlist; if (!j.chainId || !Array.isArray(j.allowedTo)) { throw new Error('Invalid allowlist: need chainId and allowedTo[]'); } return { chainId: j.chainId, allowedTo: j.allowedTo.map((a: string) => a.toLowerCase()), maxValueWei: j.maxValueWei ?? '0', maxFeePerGasGwei: j.maxFeePerGasGwei, }; } function normalizeAddr(a: string): string { return a.trim().toLowerCase(); } /** Calldata for DODOPMMIntegration.swapExactIn; send `to` = integration address. */ export function encodeSwapExactInCalldata( pool: string, tokenIn: string, amountIn: bigint, minAmountOut: bigint ): string { const iface = new Interface(SWAP_ABI); return iface.encodeFunctionData('swapExactIn', [pool, tokenIn, amountIn, minAmountOut]); } export interface SimulationResult { ok: boolean; error?: string; gasEstimate?: bigint; } export async function simulateCall(params: { rpcUrl: string; from: string; to: string; data: string; valueWei?: bigint; }): Promise { const provider = new JsonRpcProvider(params.rpcUrl); const tx = { from: params.from, to: params.to, data: params.data, value: params.valueWei ?? 0n, }; try { await provider.call(tx); } catch (e) { const msg = e instanceof Error ? e.message : String(e); return { ok: false, error: msg }; } try { const gasEstimate = await provider.estimateGas(tx); return { ok: true, gasEstimate }; } catch { return { ok: true, gasEstimate: undefined }; } } export async function enforceAllowlistAndSimulate(params: { rpcUrl: string; allowlistPath: string; from: string; to: string; data: string; valueWei?: bigint; }): Promise { let list: ExecutorAllowlist; try { list = loadAllowlist(params.allowlistPath); } catch (e) { return { ok: false, allowlistOk: false, allowlistError: e instanceof Error ? e.message : String(e), }; } const provider = new JsonRpcProvider(params.rpcUrl); const net = await provider.getNetwork(); if (Number(net.chainId) !== list.chainId) { return { ok: false, allowlistOk: false, allowlistError: `chainId mismatch: rpc=${net.chainId} allowlist=${list.chainId}`, }; } if (!list.allowedTo.includes(normalizeAddr(params.to))) { return { ok: false, allowlistOk: false, allowlistError: `to not in allowlist: ${params.to}`, }; } const maxVal = BigInt(list.maxValueWei); const v = params.valueWei ?? 0n; if (v > maxVal) { return { ok: false, allowlistOk: false, allowlistError: `value ${v} exceeds maxValueWei ${maxVal}`, }; } const sim = await simulateCall(params); return { ...sim, allowlistOk: true }; } export async function broadcastIfApply(params: { rpcUrl: string; privateKey: string; allowlistPath: string; to: string; data: string; valueWei?: bigint; }): Promise<{ hash?: string; error?: string }> { const list = loadAllowlist(params.allowlistPath); const provider = new JsonRpcProvider(params.rpcUrl); const net = await provider.getNetwork(); if (Number(net.chainId) !== list.chainId) { return { error: `chainId mismatch: ${net.chainId}` }; } if (!list.allowedTo.includes(normalizeAddr(params.to))) { return { error: 'to not allowlisted' }; } const maxVal = BigInt(list.maxValueWei); const v = params.valueWei ?? 0n; if (v > maxVal) { return { error: 'value exceeds allowlist' }; } const wallet = new Wallet(params.privateKey, provider); const tx: Parameters[0] = { to: params.to, data: params.data, value: v, }; if (list.maxFeePerGasGwei !== undefined) { tx.maxFeePerGas = BigInt(Math.floor(list.maxFeePerGasGwei * 1e9)); } try { const sent = await wallet.sendTransaction(tx); return { hash: sent.hash }; } catch (e) { return { error: e instanceof Error ? e.message : String(e) }; } } /** Format gas cost in ETH given estimate and effective gas price (wei). */ export function formatGasCostEth(gasEstimate: bigint, gasPriceWei: bigint): string { try { const wei = gasEstimate * gasPriceWei; return formatEther(wei); } catch { return 'n/a'; } }