Files
proxmox/packages/economics-toolkit/src/gas-realtime.ts
defiQUG dbd517b279 Sync workspace: config, docs, scripts, CI, operator rules, and submodule pointers.
- Update dbis_core, cross-chain-pmm-lps, explorer-monorepo, metamask-integration, pr-workspace/chains
- Omit embedded publish git dirs and empty placeholders from index

Made-with: Cursor
2026-04-12 06:12:20 -07:00

194 lines
6.0 KiB
TypeScript

/**
* Live fee data from JSON-RPC (eth_feeData) + optional USD via CoinGecko public API.
*/
import { JsonRpcProvider, formatEther } from 'ethers';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
export interface GasNetworkRow {
chainId: number;
name: string;
defaultRpc: string;
nativeSymbol: string;
coingeckoId: string | null;
nativeDecimals: number;
}
export interface FeeDataSnapshot {
gasPrice: bigint | null;
maxFeePerGas: bigint | null;
maxPriorityFeePerGas: bigint | null;
}
/** Conservative wei/gas for budgeting: legacy gasPrice, else EIP-1559 maxFeePerGas. */
export function effectiveGasPriceWei(fd: FeeDataSnapshot): bigint {
if (fd.gasPrice != null && fd.gasPrice > 0n) return fd.gasPrice;
if (fd.maxFeePerGas != null && fd.maxFeePerGas > 0n) return fd.maxFeePerGas;
return 0n;
}
export function txCostWei(gasUnits: bigint, pricePerGasWei: bigint): bigint {
return gasUnits * pricePerGasWei;
}
function configPath(): string {
const here = dirname(fileURLToPath(import.meta.url));
return join(here, '../config/gas-networks.json');
}
export function loadGasNetworks(): GasNetworkRow[] {
const raw = readFileSync(configPath(), 'utf8');
const j = JSON.parse(raw) as { networks: GasNetworkRow[] };
return j.networks;
}
/** Resolve RPC: ECONOMICS_GAS_RPC_<chainId> > RPC_URL_138 for 138 > defaultRpc */
export function resolveRpcUrl(net: GasNetworkRow): string {
const envKey = `ECONOMICS_GAS_RPC_${net.chainId}`;
const fromEnv = process.env[envKey];
if (fromEnv && fromEnv.startsWith('http')) return fromEnv.trim();
if (net.chainId === 138) {
const u = process.env.RPC_URL_138;
if (u && u.startsWith('http')) return u.trim();
}
return net.defaultRpc;
}
export async function fetchFeeData(rpcUrl: string, timeoutMs = 15000): Promise<FeeDataSnapshot> {
const provider = new JsonRpcProvider(rpcUrl);
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const fd = await provider.getFeeData();
return {
gasPrice: fd.gasPrice ?? null,
maxFeePerGas: fd.maxFeePerGas ?? null,
maxPriorityFeePerGas: fd.maxPriorityFeePerGas ?? null,
};
} finally {
clearTimeout(t);
}
}
const CG = 'https://api.coingecko.com/api/v3/simple/price';
/** USD price for one coingecko id (public API; rate-limited). */
export async function fetchUsdPrice(coingeckoId: string, timeoutMs = 10000): Promise<number | null> {
const u = `${CG}?ids=${encodeURIComponent(coingeckoId)}&vs_currencies=usd`;
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(u, { signal: ctrl.signal, headers: { accept: 'application/json' } });
if (!res.ok) return null;
const j = (await res.json()) as Record<string, { usd?: number }>;
const v = j[coingeckoId]?.usd;
return typeof v === 'number' && v > 0 ? v : null;
} catch {
return null;
} finally {
clearTimeout(t);
}
}
export interface NetworkGasQuote {
chainId: number;
name: string;
rpcUrl: string;
nativeSymbol: string;
feeData: FeeDataSnapshot;
effectiveGasPriceWei: string;
gasUnits: string;
txCostWei: string;
txCostNative: string;
nativeDecimals: number;
usdPerNative: number | null;
txCostUsd: number | null;
/** If notionalUsdt set: txCostUsd / notionalUsdt * 100 */
gasPctOfNotional: number | null;
error?: string;
}
export async function quoteNetworkGas(params: {
net: GasNetworkRow;
gasUnits: bigint;
notionalUsdt?: number;
skipUsd?: boolean;
}): Promise<NetworkGasQuote> {
const rpcUrl = resolveRpcUrl(params.net);
let feeData: FeeDataSnapshot;
try {
feeData = await fetchFeeData(rpcUrl);
} catch (e) {
return {
chainId: params.net.chainId,
name: params.net.name,
rpcUrl,
nativeSymbol: params.net.nativeSymbol,
feeData: { gasPrice: null, maxFeePerGas: null, maxPriorityFeePerGas: null },
effectiveGasPriceWei: '0',
gasUnits: params.gasUnits.toString(),
txCostWei: '0',
txCostNative: '0',
nativeDecimals: params.net.nativeDecimals,
usdPerNative: null,
txCostUsd: null,
gasPctOfNotional: null,
error: e instanceof Error ? e.message : String(e),
};
}
const eff = effectiveGasPriceWei(feeData);
const costWei = txCostWei(params.gasUnits, eff);
const costNative = formatEther(costWei);
let usdPerNative: number | null = null;
let txCostUsd: number | null = null;
let gasPctOfNotional: number | null = null;
const skip = params.skipUsd || process.env.ECONOMICS_GAS_SKIP_USD === '1';
if (!skip && params.net.coingeckoId) {
usdPerNative = await fetchUsdPrice(params.net.coingeckoId);
if (usdPerNative != null) {
const nativeFloat = parseFloat(costNative);
if (!Number.isNaN(nativeFloat)) {
txCostUsd = nativeFloat * usdPerNative;
if (params.notionalUsdt != null && params.notionalUsdt > 0 && txCostUsd != null) {
gasPctOfNotional = (txCostUsd / params.notionalUsdt) * 100;
}
}
}
}
return {
chainId: params.net.chainId,
name: params.net.name,
rpcUrl,
nativeSymbol: params.net.nativeSymbol,
feeData,
effectiveGasPriceWei: eff.toString(),
gasUnits: params.gasUnits.toString(),
txCostWei: costWei.toString(),
txCostNative: costNative,
nativeDecimals: params.net.nativeDecimals,
usdPerNative,
txCostUsd,
gasPctOfNotional,
};
}
export async function quoteAllConfiguredGas(params: {
chainIds?: number[];
gasUnits: bigint;
notionalUsdt?: number;
skipUsd?: boolean;
}): Promise<NetworkGasQuote[]> {
const all = loadGasNetworks();
const want = params.chainIds?.length
? new Set(params.chainIds)
: null;
const nets = want ? all.filter((n) => want.has(n.chainId)) : all;
const results = await Promise.all(nets.map((net) => quoteNetworkGas({ net, gasUnits: params.gasUnits, notionalUsdt: params.notionalUsdt, skipUsd: params.skipUsd })));
return results.sort((a, b) => a.chainId - b.chainId);
}