- 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
194 lines
6.0 KiB
TypeScript
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);
|
|
}
|