Repository Public
1// Gas reserve of an agent vault, per network. The vault pays its own network fees in the native coin of its network; the2// reserve is the amount of that coin the engine never counts as tradable and without which an agent may not go LIVE.3//4// RULE (same on every network, sized from recent gas prices, never from a constant per chain):5// one order = ORDER_GAS gas (a routed swap plus its exact-amount approval, with margin) at the recent gas price6// (highest base fee of the last 20 blocks + the median tip), never counted above the signer policy's fee7// ceiling for that network (the signer refuses any order whose fee could exceed it);8// reserve = RESERVE_ORDERS orders at that cost, and at least FLOOR_ORDERS orders at the policy ceiling itself, so a9// fee spike right after funding still leaves room to sell and to withdraw.10// On Ethereum mainnet one order costs dollars: an agent with small orders there is eaten by fees. feeShare() measures it.11import { hexToBigInt } from 'viem';12import type { NetworkDef } from '@spw/core';13import { rpcAny } from '../chains/rpc.ts';1415export const ORDER_GAS = 450_000n, RESERVE_ORDERS = 8n, FLOOR_ORDERS = 2n;16/** share of an order's value above which network fees are flagged as eating the agent */17export const FEE_WARN_SHARE = 0.01;1819export interface GasReserve { /** wei per gas used for the estimate */ gasPrice: bigint; /** native base units for one order */ perOrder: bigint; reserve: bigint }20/** Pure: the reserve for a recent gas price and the signer policy's fee ceiling (native base units) on that network. */21export function reserveFromGas(gasPrice: bigint, feeCeiling: bigint): GasReserve {22 if (gasPrice < 0n || feeCeiling <= 0n) throw new Error('gas reserve needs a gas price and a fee ceiling');23 const raw = ORDER_GAS * gasPrice, perOrder = raw > feeCeiling ? feeCeiling : raw;24 const byPrice = RESERVE_ORDERS * perOrder, floor = FLOOR_ORDERS * feeCeiling;25 return { gasPrice, perOrder, reserve: byPrice > floor ? byPrice : floor };26}27/** Network fee of one order as a share of the order's value (both in USD). */28export const feeShare = (perOrder: bigint, nativeDecimals: number, nativeUsd: number, orderUsd: number): number => (Number(perOrder) / 10 ** nativeDecimals) * nativeUsd / orderUsd;2930/** Recent gas price: highest base fee of the last 20 blocks plus the median of their median tips. */31export async function recentGasPrice(n: NetworkDef): Promise<bigint> {32 try {33 const h = await rpcAny<{ baseFeePerGas: `0x${string}`[]; reward?: `0x${string}`[][] }>(n.rpc, 'eth_feeHistory', ['0x14', 'latest', [50]]);34 const base = h.baseFeePerGas.map((x) => hexToBigInt(x)), tips = (h.reward ?? []).map((r) => hexToBigInt(r[0] ?? '0x0')).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));35 if (!base.length) throw new Error('empty fee history');36 return base.reduce((m, x) => (x > m ? x : m), 0n) + (tips[Math.floor(tips.length / 2)] ?? 0n);37 } catch {38 return hexToBigInt(await rpcAny<`0x${string}`>(n.rpc, 'eth_gasPrice', [])); // a node without fee history: its own suggestion39 }40}4142const cache = new Map<string, { at: number; v: GasReserve }>();43/** The reserve of a network right now (cached one minute). Throws when no gas price can be read or the policy sets no ceiling: no reserve, no LIVE. */44export async function gasReserve(n: NetworkDef, feeCeiling: string | undefined, now = Date.now()): Promise<GasReserve> {45 if (feeCeiling === undefined) throw new Error(`the signer policy sets no fee ceiling on ${n.name}`);46 const key = `${n.id}:${feeCeiling}`, c = cache.get(key);47 if (c && now - c.at < 60_000) return c.v;48 const v = reserveFromGas(await recentGasPrice(n), BigInt(feeCeiling));49 cache.set(key, { at: now, v });50 return v;51}52