Files
proxmox/scripts/verify/plan-token-aggregation-liquidity-gap-funding.mjs
defiQUG 09e8c08023
Some checks failed
Deploy to Phoenix / deploy (push) Has been skipped
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Has been skipped
Deploy to Phoenix / cloudflare (push) Has been skipped
Deploy to Phoenix / validate (push) Failing after 3s
feat(non-evm): operator binding config, planner hints, and validation
- Add config/non-evm-operator-binding.json (public hints only; no secrets).
- Extend .env.master.example with XRPL/Tron/Solana/TRONGRID overrides.
- Wire solana-gru-bridge-lineup refs; refresh non-evm lane stubs from binding.
- Teach liquidity-gap planner to read binding; validate JSON in validate-config-files.sh.
- Document handoff in CWUSDC_NON_MANUAL_PROVIDER_TASKS; cross-link GRU spec.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 00:08:09 -07:00

991 lines
44 KiB
JavaScript

#!/usr/bin/env node
/**
* Read-only funding planner for token-aggregation adoption-readiness liquidity gaps.
*
* It does not broadcast transactions. It checks the deployer wallet's native and ERC-20 balances
* for every current liquidityMissingDetails row and classifies each row as:
* - fundable_token_balance_present
* - gas_gated
* - token_balance_gated
* - pool_binding_gated
*/
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
const repoRoot = resolve(new URL("../..", import.meta.url).pathname);
const readinessPath = resolve(repoRoot, "reports/status/token-aggregation-adoption-readiness-live-20260509.json");
const nonEvmHealthPath = resolve(repoRoot, "reports/status/non-evm-network-health-latest.json");
const nonEvmLaneStatusPath = resolve(repoRoot, "reports/status/non-evm-lane-status-latest.json");
const operatorNonEvmBindingPath = resolve(repoRoot, "config/non-evm-operator-binding.json");
function readOperatorNonEvmBinding() {
try {
return JSON.parse(readFileSync(operatorNonEvmBindingPath, "utf8"));
} catch {
return {};
}
}
const jsonOut = resolve(repoRoot, "reports/status/token-aggregation-liquidity-gap-funding-plan-latest.json");
const mdOut = resolve(repoRoot, "reports/status/token-aggregation-liquidity-gap-funding-plan-latest.md");
const deployer = (process.env.DEPLOYER_ADDRESS || process.env.DEPLOYER || "0x4A666F96fC8764181194447A7dFdb7d471b301C8").trim();
const envFiles = [resolve(repoRoot, ".env"), resolve(repoRoot, "smom-dbis-138/.env")];
const stabilityCycles = Number(process.env.TOKEN_AGGREGATION_STABILITY_CYCLES || "30");
const gasSafetyBps = BigInt(process.env.TOKEN_AGGREGATION_GAS_SAFETY_BPS || "15000");
const coffeeMoneyUsdAvailable = Number(process.env.DEPLOYER_COFFEE_MONEY_USD || "48");
const coffeeMoneyLiquidityUsdPerRow = Number(process.env.COFFEE_MONEY_LIQUIDITY_USD_PER_ROW || "1");
const bridgeCapableChains = new Set([1, 10, 25, 56, 100, 137, 42161, 42220, 43114, 8453]);
const protocolinkCandidateChains = new Set([1, 10, 56, 100, 137, 42161, 42220, 43114, 8453]);
const officialQuoteAssets = new Set([
"1:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"1:0xdac17f958d2ee523a2206206994597c13d831ec7",
"10:0x0b2c639c533813f4aa9d7837caf62653d097ff85",
"10:0x94b008aa00579c1307b0ef2c499ad98a8ce58e58",
"25:0xc21223249ca28397b4b6541dffaecc539bff0c59",
"25:0x66e428c3f67a68878562e79a0234c1f83c208770",
"56:0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d",
"56:0x55d398326f99059ff775485246999027b3197955",
"100:0xddafbb505ad214d7b80b1f830fccc89b60fb7a83",
"100:0x4ecaba5870353805a9f068101a40e0f32ed605c6",
"137:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
"137:0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
"8453:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
"42161:0xaf88d065e77c8cc2239327c5edb3a432268e5831",
"42161:0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9",
"42220:0x765de816845861e75a25fca122bb6898b8b1282a",
"42220:0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e",
"43114:0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e",
"43114:0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7",
]);
const ethereumSourceTokens = [
{ symbol: "USDC", address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", decimals: 6, role: "official_quote_capital" },
{ symbol: "USDT", address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", decimals: 6, role: "official_quote_capital" },
{ symbol: "LINK", address: "0x514910771AF9Ca656af840dff83E8264EcF986CA", decimals: 18, role: "route_quote_before_use" },
{ symbol: "WETH", address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18, role: "wrapped_native_gas_source" },
{ symbol: "XAUt", address: "0x68749665FF8D2d112Fa859AA293F07A622782F38", decimals: 6, role: "route_quote_before_use" },
{ symbol: "cWUSDC", address: "0x66a3c2fa3e467aa586e90912f977e648589cabaf", decimals: 6, role: "evidence_or_pair_side_not_native_gas" },
];
const rpcByChain = {
1: process.env.ETHEREUM_MAINNET_RPC || process.env.RPC_URL_1 || "https://ethereum.publicnode.com",
10: process.env.OPTIMISM_MAINNET_RPC || process.env.OPTIMISM_RPC_URL || process.env.RPC_URL_10 || "https://optimism.publicnode.com",
25: process.env.CRONOS_RPC_URL || process.env.CRONOS_MAINNET_RPC || process.env.RPC_URL_25 || "https://cronos-evm-rpc.publicnode.com",
56: process.env.BSC_RPC_URL || process.env.BSC_MAINNET_RPC || process.env.RPC_URL_56 || "https://bsc-rpc.publicnode.com",
100: process.env.GNOSIS_MAINNET_RPC || process.env.GNOSIS_RPC_URL || process.env.GNOSIS_RPC || process.env.RPC_URL_100 || "https://gnosis.publicnode.com",
137: process.env.POLYGON_MAINNET_RPC || process.env.POLYGON_RPC_URL || process.env.RPC_URL_137 || "https://polygon-bor-rpc.publicnode.com",
138: process.env.RPC_URL_138_PUBLIC || process.env.RPC_URL_138 || process.env.CHAIN138_RPC_URL || "http://192.168.11.221:8545",
1111: process.env.WEMIX_MAINNET_RPC || process.env.WEMIX_RPC || process.env.RPC_URL_1111 || "https://api.wemix.com",
8453: process.env.BASE_MAINNET_RPC || process.env.BASE_RPC_URL || process.env.RPC_URL_8453 || "https://base-rpc.publicnode.com",
42161: process.env.ARBITRUM_MAINNET_RPC || process.env.ARBITRUM_RPC_URL || process.env.RPC_URL_42161 || "https://arbitrum-one-rpc.publicnode.com",
42220: process.env.CELO_MAINNET_RPC || process.env.CELO_RPC_URL || process.env.CELO_RPC || process.env.RPC_URL_42220 || "https://celo-rpc.publicnode.com",
43114: process.env.AVALANCHE_RPC_URL || process.env.AVALANCHE_MAINNET_RPC || process.env.RPC_URL_43114 || "https://avalanche-c-chain-rpc.publicnode.com",
651940: process.env.CHAIN_651940_RPC_URL || process.env.ALL_MAINNET_RPC || "https://mainnet-rpc.alltra.global",
};
const nativeSymbolsByChain = {
1: "ETH",
10: "ETH",
25: "CRO",
56: "BNB",
100: "xDAI",
137: "POL",
138: "DBIS",
1111: "WEMIX",
8453: "ETH",
42161: "ETH",
42220: "CELO",
43114: "AVAX",
651940: "ALL",
};
const gasPriceCache = new Map();
function padAddress(address) {
return String(address).replace(/^0x/i, "").padStart(64, "0");
}
async function rpcCall(rpcUrl, method, params) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 12_000);
try {
const response = await fetch(rpcUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", method, params, id: 1 }),
signal: controller.signal,
});
const json = await response.json();
if (json.error) return { ok: false, error: json.error.message || JSON.stringify(json.error) };
return { ok: true, result: json.result };
} catch (error) {
return { ok: false, error: error.message };
} finally {
clearTimeout(timeout);
}
}
function bigintFromHex(hex) {
if (!hex || hex === "0x") return 0n;
return BigInt(hex);
}
function decimalUnits(raw, decimals) {
const scale = 10n ** BigInt(decimals);
const whole = raw / scale;
const frac = raw % scale;
const fracText = frac.toString().padStart(decimals, "0").replace(/0+$/, "");
return fracText ? `${whole}.${fracText}` : whole.toString();
}
async function erc20Balance(rpcUrl, token, holder) {
const [balance, decimals] = await Promise.all([
rpcCall(rpcUrl, "eth_call", [{ to: token, data: `0x70a08231${padAddress(holder)}` }, "latest"]),
rpcCall(rpcUrl, "eth_call", [{ to: token, data: "0x313ce567" }, "latest"]),
]);
const raw = balance.ok ? bigintFromHex(balance.result) : 0n;
const dec = decimals.ok ? Number(bigintFromHex(decimals.result)) : 18;
return {
ok: balance.ok,
raw: raw.toString(),
units: decimalUnits(raw, Number.isFinite(dec) ? dec : 18),
decimals: Number.isFinite(dec) ? dec : 18,
error: balance.ok ? undefined : balance.error,
};
}
async function nativeBalance(rpcUrl, holder) {
const balance = await rpcCall(rpcUrl, "eth_getBalance", [holder, "latest"]);
const raw = balance.ok ? bigintFromHex(balance.result) : 0n;
return {
ok: balance.ok,
raw: raw.toString(),
units: decimalUnits(raw, 18),
error: balance.ok ? undefined : balance.error,
};
}
async function buildEthereumSourceInventory() {
const rpcUrl = rpcByChain[1];
const native = await nativeBalance(rpcUrl, deployer);
const tokens = await Promise.all(ethereumSourceTokens.map(async (token) => {
const balance = await erc20Balance(rpcUrl, token.address, deployer);
return {
...token,
balance: balance.units,
balanceRaw: balance.raw,
balanceStatus: BigInt(balance.raw || "0") > 0n ? "present" : "zero",
error: balance.error,
};
}));
return {
chainId: 1,
network: "Ethereum Mainnet",
deployer,
native: {
symbol: "ETH",
balance: native.units,
balanceRaw: native.raw,
role: "mainnet_transaction_gas_do_not_fully_drain",
},
tokens,
interpretation: [
"Ethereum portfolio value is not the same as immediately spendable cross-chain gas.",
"Keep enough ETH for Mainnet approvals, swaps, and liquidity/stability events.",
"Use USDC/USDT first as official quote capital; use LINK/XAUt only after a live route quote proves acceptable output.",
"Treat cWUSDC as pair-side/evidence inventory unless a real public route converts it into the exact official token needed.",
],
};
}
async function gasPrice(rpcUrl, chainId) {
if (!rpcUrl) return { ok: false, raw: "0", units: "0", error: "missing_rpc" };
if (gasPriceCache.has(chainId)) return gasPriceCache.get(chainId);
const result = await rpcCall(rpcUrl, "eth_gasPrice", []);
const raw = result.ok ? bigintFromHex(result.result) : 0n;
const payload = {
ok: result.ok,
raw: raw.toString(),
gwei: decimalUnits(raw, 9),
error: result.ok ? undefined : result.error,
};
gasPriceCache.set(chainId, payload);
return payload;
}
function table(headers, rows) {
return [
`| ${headers.join(" | ")} |`,
`| ${headers.map(() => "---").join(" | ")} |`,
...rows.map((row) => `| ${row.map((cell) => String(cell ?? "").replace(/\|/g, "\\|")).join(" | ")} |`),
].join("\n");
}
function readJsonIfExists(path, fallback = null) {
try {
return JSON.parse(readFileSync(path, "utf8"));
} catch {
return fallback;
}
}
function readEnvValue(...keys) {
for (const key of keys) {
if (process.env[key]) return process.env[key].trim();
}
for (const file of envFiles) {
let text = "";
try {
text = readFileSync(file, "utf8");
} catch {
continue;
}
for (const key of keys) {
const match = text.match(new RegExp(`^${key}=([^\\n#]*)`, "m"));
if (match?.[1]) return match[1].trim().replace(/^['"]|['"]$/g, "");
}
}
return "";
}
const base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
function base58Encode(bytes) {
let value = BigInt(`0x${Buffer.from(bytes).toString("hex") || "0"}`);
let output = "";
while (value > 0n) {
const remainder = Number(value % 58n);
output = `${base58Alphabet[remainder]}${output}`;
value /= 58n;
}
let leadingZeroes = 0;
for (const byte of bytes) {
if (byte !== 0) break;
leadingZeroes += 1;
}
return `${"1".repeat(leadingZeroes)}${output || ""}`;
}
function base58Decode(text) {
let value = 0n;
for (const char of text) {
const index = base58Alphabet.indexOf(char);
if (index < 0) throw new Error(`invalid_base58_char_${char}`);
value = value * 58n + BigInt(index);
}
let hex = value.toString(16);
if (hex.length % 2) hex = `0${hex}`;
const decoded = hex ? Buffer.from(hex, "hex") : Buffer.alloc(0);
const leading = [...text].findIndex((char) => char !== "1");
const zeroCount = leading < 0 ? text.length : leading;
return Buffer.concat([Buffer.alloc(zeroCount), decoded]);
}
function solanaWalletFromConfig() {
const explicit = readEnvValue("SOLANA_DEPLOYER_ADDRESS", "SOLANA_WALLET_ADDRESS", "SOLANA_PUBLIC_KEY");
if (explicit) return { address: explicit, source: "env_public_key" };
const keypairPath = readEnvValue("SOLANA_KEYPAIR_PATH");
if (keypairPath) {
try {
const keypair = JSON.parse(readFileSync(keypairPath, "utf8"));
if (Array.isArray(keypair) && keypair.length >= 64) {
return { address: base58Encode(Buffer.from(keypair.slice(32, 64))), source: "SOLANA_KEYPAIR_PATH_public_key" };
}
} catch {
// Fall through to private-key decode if present.
}
}
const privateKey = readEnvValue("PRIVATE_KEY_SOLANA_DEPLOYER", "SOLANA_PRIVATE_KEY");
if (privateKey) {
try {
const decoded = base58Decode(privateKey);
if (decoded.length >= 64) return { address: base58Encode(decoded.subarray(32, 64)), source: "solana_private_key_public_half" };
} catch {
// Keep address gated if the value is a seed-only key.
}
}
return { address: "", source: "missing" };
}
function tronWalletFromConfig(binding) {
const override = binding?.tron?.addressOverride?.trim();
if (override) return { address: override, source: "config_non_evm_operator_binding_tron_override" };
const explicit = readEnvValue("TRON_DEPLOYER_ADDRESS", "TRON_WALLET_ADDRESS", "TRON_PUBLIC_ADDRESS", "TRON_ACCOUNT_ADDRESS");
if (explicit) return { address: explicit, source: "env_tron_address" };
const ethAddress = deployer.replace(/^0x/i, "");
if (/^[0-9a-fA-F]{40}$/.test(ethAddress)) {
const payload = Buffer.from(`41${ethAddress}`, "hex");
const checksum = createHash("sha256").update(createHash("sha256").update(payload).digest()).digest().subarray(0, 4);
return { address: base58Encode(Buffer.concat([payload, checksum])), source: "derived_from_evm_deployer_address" };
}
return { address: "", source: "missing" };
}
function xrplWalletFromConfig(binding) {
const explicit = readEnvValue("XRPL_DEPLOYER_ADDRESS", "XRP_DEPLOYER_ADDRESS", "XRPL_WALLET_ADDRESS", "XRP_WALLET_ADDRESS", "XRPL_ACCOUNT");
if (explicit?.trim()) return { address: explicit.trim(), source: "env_xrpl_address" };
const cfg = binding?.xrpl?.canonicalAccount?.trim();
if (cfg) return { address: cfg, source: "config_non_evm_operator_binding" };
return { address: "", source: "missing" };
}
function requiredFundingFor(network, binding) {
const m = binding?.minimumFundingTargets;
if (network === "Solana" && m?.solana?.minSolOperationalHint) {
return `${m.solana.minSolOperationalHint} SOL (repo hint; operator confirms venue targets)`;
}
if (network === "Tron" && m?.tron?.minTrxOperationalHint) {
return `${m.tron.minTrxOperationalHint} TRX (repo hint; operator confirms energy/inventory)`;
}
if (network === "XRPL" && m?.xrpl?.minXrpReserveHint) {
return `${m.xrpl.minXrpReserveHint} XRP (repo reserve hint; align trustlines/issuer policy)`;
}
return "TBD";
}
async function solanaNativeBalance(address) {
if (!address) return { ok: false, units: "address_required", raw: "0", error: "missing_solana_address" };
const rpcUrl = readEnvValue("SOLANA_RPC_URL") || "https://solana-rpc.publicnode.com";
const result = await rpcCall(rpcUrl, "getBalance", [address]);
const lamports = result.ok ? BigInt(result.result?.value ?? 0) : 0n;
return {
ok: result.ok,
raw: lamports.toString(),
units: decimalUnits(lamports, 9),
error: result.ok ? undefined : result.error,
};
}
async function tronNativeBalance(address) {
if (!address) return { ok: false, units: "address_required", raw: "0", error: "missing_tron_address" };
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 12_000);
try {
const headers = { "content-type": "application/json" };
const apiKey = readEnvValue("TRONGRID_API_KEY");
if (apiKey) headers["TRON-PRO-API-KEY"] = apiKey;
const response = await fetch("https://api.trongrid.io/wallet/getaccount", {
method: "POST",
headers,
body: JSON.stringify({ address, visible: true }),
signal: controller.signal,
});
const json = await response.json();
const sun = BigInt(json.balance ?? 0);
return { ok: response.ok, raw: sun.toString(), units: decimalUnits(sun, 6), error: response.ok ? undefined : JSON.stringify(json) };
} catch (error) {
return { ok: false, raw: "0", units: "0", error: error.message };
} finally {
clearTimeout(timeout);
}
}
async function xrplNativeBalance(address) {
if (!address) return { ok: false, units: "address_required", raw: "0", error: "missing_xrpl_address" };
const result = await rpcCall(readEnvValue("XRPL_RPC_URL") || "https://xrplcluster.com", "account_info", [{ account: address, ledger_index: "validated" }]);
const drops = result.ok ? BigInt(result.result?.account_data?.Balance ?? 0) : 0n;
return {
ok: result.ok,
raw: drops.toString(),
units: decimalUnits(drops, 6),
error: result.ok ? undefined : result.error,
};
}
function networkHealth(health, network) {
return (health?.checks ?? []).find((row) => row.network === network) ?? null;
}
function classifyFundingPath(detail, token, native) {
const chainId = Number(detail.chainId);
const addressKey = `${chainId}:${String(detail.address ?? "").toLowerCase()}`;
const hasToken = BigInt(token.raw || "0") > 0n;
const hasGas = BigInt(native.raw || "0") > 0n;
const isOfficialQuoteAsset = officialQuoteAssets.has(addressKey);
const canBridge = bridgeCapableChains.has(chainId);
const canProtocolink = protocolinkCandidateChains.has(chainId);
if (hasToken && detail.category === "configured_or_indexed_pools_zero_tvl") {
return {
fundingPath: "seed_existing_visible_pool_from_deployer_balance",
fundingPathStatus: hasGas ? "executable_after_operator_approval" : "native_gas_topup_required",
assetClass: isOfficialQuoteAsset ? "official_quote_asset" : "repo_or_wrapped_asset",
protocolinkUse: "not_required_for_seed; optional for pre-seed rebalance quote",
};
}
if (hasToken && detail.category === "no_visible_pool_binding") {
return {
fundingPath: "create_or_bind_pool_then_seed_from_deployer_balance",
fundingPathStatus: hasGas ? "pool_binding_required_before_funding" : "native_gas_and_pool_binding_required",
assetClass: isOfficialQuoteAsset ? "official_quote_asset" : "repo_or_wrapped_asset",
protocolinkUse: "not_required_until pool exists; optional to rebalance paired side",
};
}
if (isOfficialQuoteAsset) {
return {
fundingPath: canProtocolink ? "protocolink_swap_candidate_for_non_mintable_quote_asset" : "external_quote_asset_required",
fundingPathStatus: canProtocolink ? "requires_live_route_quote_source_asset_and_min_out" : "external_funding_required",
assetClass: "official_quote_asset",
protocolinkUse: canProtocolink
? "use only after live quote proves deployer-held source asset converts into this exact token"
: "unsupported_by_current_protocolink_candidate_set",
};
}
if (canBridge) {
return {
fundingPath: "bridge_or_destination_mint_repo_asset_then_seed",
fundingPathStatus: hasGas ? "bridge_or_mint_route_required" : "native_gas_topup_then_bridge_or_mint",
assetClass: "repo_or_wrapped_asset",
protocolinkUse: "optional only if a public swap route beats bridge/mint for the needed asset",
};
}
return {
fundingPath: "manual_asset_source_required",
fundingPathStatus: hasGas ? "asset_source_required" : "native_gas_and_asset_source_required",
assetClass: "unknown_or_unclassified_asset",
protocolinkUse: "route support not classified",
};
}
function gasUnitsForFundingPath(fundingPath) {
const policy = {
seed_existing_visible_pool_from_deployer_balance: {
oneTimeGasUnits: 650_000,
stabilityGasUnitsPerCycle: 260_000,
rationale: "approve plus add/sync/validation transaction budget for already visible pools",
},
create_or_bind_pool_then_seed_from_deployer_balance: {
oneTimeGasUnits: 1_350_000,
stabilityGasUnitsPerCycle: 320_000,
rationale: "factory create/bind plus seed transaction budget",
},
bridge_or_destination_mint_repo_asset_then_seed: {
oneTimeGasUnits: 1_150_000,
stabilityGasUnitsPerCycle: 300_000,
rationale: "bridge-or-mint plus destination seed transaction budget",
},
protocolink_swap_candidate_for_non_mintable_quote_asset: {
oneTimeGasUnits: 900_000,
stabilityGasUnitsPerCycle: 360_000,
rationale: "Protocolink route execution plus seed/rebalance budget after live quote",
},
external_quote_asset_required: {
oneTimeGasUnits: 450_000,
stabilityGasUnitsPerCycle: 260_000,
rationale: "post-funding seed/rebalance budget; asset funding is out of band",
},
manual_asset_source_required: {
oneTimeGasUnits: 650_000,
stabilityGasUnitsPerCycle: 260_000,
rationale: "manual source then seed/rebalance budget",
},
};
return policy[fundingPath] ?? {
oneTimeGasUnits: 650_000,
stabilityGasUnitsPerCycle: 260_000,
rationale: "default seed/rebalance budget",
};
}
function buildGasPlan({ chainId, native, gas, fundingPath }) {
const units = gasUnitsForFundingPath(fundingPath);
const gasPriceWei = BigInt(gas.raw || "0");
const oneTimeRaw = BigInt(units.oneTimeGasUnits) * gasPriceWei;
const stabilityRaw = BigInt(units.stabilityGasUnitsPerCycle) * BigInt(stabilityCycles) * gasPriceWei;
const subtotalRaw = oneTimeRaw + stabilityRaw;
const requiredRaw = (subtotalRaw * gasSafetyBps + 9_999n) / 10_000n;
const nativeRaw = BigInt(native.raw || "0");
const shortfallRaw = requiredRaw > nativeRaw ? requiredRaw - nativeRaw : 0n;
const surplusRaw = nativeRaw > requiredRaw ? nativeRaw - requiredRaw : 0n;
return {
nativeSymbol: nativeSymbolsByChain[chainId] ?? "native",
gasPriceWei: gas.raw,
gasPriceGwei: gas.gwei,
oneTimeGasUnits: units.oneTimeGasUnits,
stabilityCycles,
stabilityGasUnitsPerCycle: units.stabilityGasUnitsPerCycle,
safetyBps: Number(gasSafetyBps),
requiredNativeRaw: requiredRaw.toString(),
requiredNative: decimalUnits(requiredRaw, 18),
oneTimeNative: decimalUnits(oneTimeRaw, 18),
stabilityNative: decimalUnits(stabilityRaw, 18),
nativeBalanceRaw: native.raw,
nativeBalance: native.units,
shortfallNativeRaw: shortfallRaw.toString(),
shortfallNative: decimalUnits(shortfallRaw, 18),
surplusNativeRaw: surplusRaw.toString(),
surplusNative: decimalUnits(surplusRaw, 18),
status: shortfallRaw === 0n ? "gas_budget_satisfied" : "gas_budget_shortfall",
rationale: units.rationale,
gasPriceError: gas.error,
};
}
async function buildNonEvmFundingRequirements() {
const health = readJsonIfExists(nonEvmHealthPath, null);
const laneStatus = readJsonIfExists(nonEvmLaneStatusPath, null);
const lanes = laneStatus?.lanes ?? {};
const binding = readOperatorNonEvmBinding();
const solanaWallet = solanaWalletFromConfig();
const tronWallet = tronWalletFromConfig(binding);
const xrplWallet = xrplWalletFromConfig(binding);
const tronCanon = binding?.tron?.canonicalDeployerConfirmed === true;
const [solanaBalance, tronBalance, xrplBalance] = await Promise.all([
solanaNativeBalance(solanaWallet.address),
tronNativeBalance(tronWallet.address),
xrplNativeBalance(xrplWallet.address),
]);
const requirements = [
{
network: "Solana",
target: "mainnet-beta",
includedInFundingScope: true,
walletAddress: solanaWallet.address || "missing",
walletSource: solanaWallet.source,
currentBalanceStatus: solanaBalance.ok ? `${solanaBalance.units} SOL` : solanaBalance.units,
currentBalanceRaw: solanaBalance.raw,
nativeGasAsset: "SOL",
bridgeOrWrappedAsset: lanes.solana?.destinationAsset?.symbol ?? "cWAUSDT",
requiredFunding: requiredFundingFor("Solana", binding),
status: solanaWallet.address ? "spl_mint_inventory_and_minimum_funding_targets_required" : "wallet_and_spl_mint_inventory_required",
networkHealth: networkHealth(health, "Solana"),
requirements: [
solanaWallet.address ? "Canonical Solana deployer public key is bound for native SOL checks." : "Bind canonical Solana custody wallet/public key for funding checks.",
"Populate SPL mint addresses in config/solana-gru-bridge-lineup.json.",
"Check SOL gas/rent balance and SPL token balances for each promoted cW* mint.",
"Set minimum pool/rent/execution funding targets per Solana venue before declaring positive liquidity.",
],
},
{
network: "Tron",
target: "mainnet",
includedInFundingScope: true,
walletAddress: tronWallet.address || "missing",
walletSource: tronWallet.source,
currentBalanceStatus: tronBalance.ok ? `${tronBalance.units} TRX` : tronBalance.units,
currentBalanceRaw: tronBalance.raw,
nativeGasAsset: "TRX",
bridgeOrWrappedAsset: "TronAdapter relay inventory",
requiredFunding: requiredFundingFor("Tron", binding),
status:
!tronWallet.address
? "native_tron_wallet_and_asset_inventory_required"
: tronWallet.source === "derived_from_evm_deployer_address" && !tronCanon
? "derived_tron_wallet_needs_operator_confirmation_and_asset_inventory"
: "native_tron_wallet_and_asset_inventory_required",
networkHealth: networkHealth(health, "Tron"),
requirements: [
tronCanon
? "Operator confirmed EVM-derived Tron deployer as canonical (config/non-evm-operator-binding.json tron.canonicalDeployerConfirmed)."
: tronWallet.source === "derived_from_evm_deployer_address"
? "Confirm whether the EVM deployer-derived Tron address is the canonical native Tron deployer (set tron.canonicalDeployerConfirmed when true)."
: "Bind canonical Tron custody wallet address.",
"Check TRX energy/bandwidth funding and any native TRC-20 inventory needed for relay settlement.",
"Promote or document native Tron-side contracts/assets before treating Tron as liquidity-ready.",
"Close Chain 138 TronAdapter source/publication evidence separately from native Tron funding.",
],
},
{
network: "XRPL",
target: "mainnet",
includedInFundingScope: true,
walletAddress: xrplWallet.address || "missing",
walletSource: xrplWallet.source,
currentBalanceStatus: xrplBalance.ok ? `${xrplBalance.units} XRP` : xrplBalance.units,
currentBalanceRaw: xrplBalance.raw,
nativeGasAsset: "XRP",
bridgeOrWrappedAsset: lanes.xrpl?.wrappedAsset?.address ? `wXRP ${lanes.xrpl.wrappedAsset.address}` : "wXRP",
requiredFunding: requiredFundingFor("XRPL", binding),
status: xrplWallet.address ? "xrpl_reserve_trustline_and_bridge_inventory_required" : "xrpl_wallet_reserve_and_bridge_inventory_required",
networkHealth: networkHealth(health, "XRPL"),
requirements: [
xrplWallet.address ? "Canonical XRPL account is bound for native XRP checks." : "Bind canonical XRPL account and optional destination tag policy.",
binding?.xrpl?.destinationTagPolicy && binding.xrpl.destinationTagPolicy !== "none"
? `Destination tag policy (from config): ${binding.xrpl.destinationTagPolicy}.`
: null,
"Check XRP reserve, transfer-fee cushion, and any trustline/issuer requirements.",
"Check Chain 138 wXRP inventory and MintBurnController authorization readiness.",
"Close Chain 138 XRPLAdapter/wXRP/MintBurnController source-publication evidence separately from XRPL funding.",
].filter(Boolean),
},
{
network: "Other non-EVM majors",
target: "BTC/SOL/XRP/ADA/XLM/HBAR/SUI/TON class expansion",
includedInFundingScope: true,
walletAddress: "per-network wallet not bound",
walletSource: "missing",
currentBalanceStatus: "not_supported_by_current_balance_planner",
nativeGasAsset: "varies",
bridgeOrWrappedAsset: "not bound",
requiredFunding: "TBD",
status: "adapter_wallet_asset_and_venue_requirements_not_yet_bound",
networkHealth: null,
requirements: [
"Create per-network custody wallet and balance checker.",
"Bind asset IDs/mints/trustlines/program IDs in repo config.",
"Define minimum native gas/rent/reserve and liquidity targets per network.",
"Add lane evidence before including the network in tracker-facing liquidity claims.",
],
},
];
return {
requirements,
bindingEcho: {
configPath: "config/non-evm-operator-binding.json",
xrplAccountBound: Boolean(xrplWallet.address),
tronCanonicalDeployerConfirmed: tronCanon,
tronWalletSource: tronWallet.source,
solanaWalletSource: solanaWallet.source,
},
};
}
const readiness = JSON.parse(readFileSync(readinessPath, "utf8"));
const details = readiness.blockerInventory?.liquidityMissingDetails ?? [];
const rows = [];
for (const detail of details) {
const chainId = Number(detail.chainId);
const rpcUrl = rpcByChain[chainId];
const native = rpcUrl ? await nativeBalance(rpcUrl, deployer) : { ok: false, raw: "0", units: "0", error: "missing_rpc" };
const token = rpcUrl && detail.address?.startsWith("0x")
? await erc20Balance(rpcUrl, detail.address, deployer)
: { ok: false, raw: "0", units: "0", decimals: 18, error: "missing_token_or_rpc" };
const hasGas = BigInt(native.raw || "0") > 0n;
const hasToken = BigInt(token.raw || "0") > 0n;
const funding = classifyFundingPath(detail, token, native);
const gas = await gasPrice(rpcUrl, chainId);
const gasPlan = buildGasPlan({ chainId, native, gas, fundingPath: funding.fundingPath });
let status = "token_balance_gated";
if (detail.category === "no_visible_pool_binding") status = hasToken ? "pool_binding_gated" : "pool_binding_and_token_balance_gated";
if (detail.category === "configured_or_indexed_pools_zero_tvl" && hasToken) status = "fundable_token_balance_present";
if (!hasGas) status = `${status}+gas_gated`;
rows.push({
chainId,
symbol: detail.symbol,
address: detail.address,
category: detail.category,
poolCount: detail.poolCount,
zeroTvlPoolCount: detail.zeroTvlPoolCount,
nativeBalance: native.units,
tokenBalance: token.units,
tokenBalanceRaw: token.raw,
status,
...funding,
gasPlan,
rpcError: native.error || token.error,
});
}
const gasBudgetRows = rows.map((row) => ({
chainId: row.chainId,
symbol: row.symbol,
nativeSymbol: row.gasPlan.nativeSymbol,
fundingPath: row.fundingPath,
requiredNative: row.gasPlan.requiredNative,
nativeBalance: row.gasPlan.nativeBalance,
shortfallNative: row.gasPlan.shortfallNative,
status: row.gasPlan.status,
}));
const chainGasBudgetMap = new Map();
for (const row of rows) {
const existing = chainGasBudgetMap.get(row.chainId) ?? {
chainId: row.chainId,
nativeSymbol: row.gasPlan.nativeSymbol,
gasPriceGwei: row.gasPlan.gasPriceGwei,
requiredNativeRaw: 0n,
nativeBalanceRaw: BigInt(row.gasPlan.nativeBalanceRaw || "0"),
rows: 0,
symbols: [],
};
existing.requiredNativeRaw += BigInt(row.gasPlan.requiredNativeRaw || "0");
existing.rows += 1;
existing.symbols.push(row.symbol);
chainGasBudgetMap.set(row.chainId, existing);
}
const chainGasBudgetRows = [...chainGasBudgetMap.values()]
.sort((a, b) => Number(a.chainId) - Number(b.chainId))
.map((row) => {
const shortfallRaw = row.requiredNativeRaw > row.nativeBalanceRaw ? row.requiredNativeRaw - row.nativeBalanceRaw : 0n;
const surplusRaw = row.nativeBalanceRaw > row.requiredNativeRaw ? row.nativeBalanceRaw - row.requiredNativeRaw : 0n;
return {
chainId: row.chainId,
nativeSymbol: row.nativeSymbol,
gasPriceGwei: row.gasPriceGwei,
rows: row.rows,
symbols: [...new Set(row.symbols)].join(", "),
requiredNativeRaw: row.requiredNativeRaw.toString(),
requiredNative: decimalUnits(row.requiredNativeRaw, 18),
nativeBalanceRaw: row.nativeBalanceRaw.toString(),
nativeBalance: decimalUnits(row.nativeBalanceRaw, 18),
shortfallNativeRaw: shortfallRaw.toString(),
shortfallNative: decimalUnits(shortfallRaw, 18),
surplusNativeRaw: surplusRaw.toString(),
surplusNative: decimalUnits(surplusRaw, 18),
status: shortfallRaw === 0n ? "chain_gas_budget_satisfied" : "chain_gas_budget_shortfall",
};
});
const etherscanStability = {
purpose: "off_chain_indexing_stability_for_token_trackers",
boundary: "Etherscan/token trackers index public on-chain facts; gas only funds the transactions that create and refresh those facts.",
requiredOnChainFacts: [
"Verified token contract and correct metadata/logoURI publication path.",
"Visible/indexable pool contract for each promoted token pair.",
"Positive, non-dust liquidity on the visible pool.",
"Recent real swap or liquidity-change events when tracker freshness is required.",
"Official quote-token evidence when claiming cW*/USDC or c*/USDC peg support.",
],
gasBudgetRole: [
"Create or bind missing pools.",
"Approve and seed liquidity.",
"Execute Protocolink/bridge/mint/swap actions when needed.",
"Run recurring stability/rebalance transactions so public indexers observe fresh state.",
],
cannotBeSolvedByGasAlone: [
"A missing verified-source listing.",
"A token logo or page-info package that is not published at the expected endpoint.",
"A pool that exists only in internal config but is not visible/indexable on the public chain.",
"A c* balance that has not been bridged or swapped into the exact official quote asset required by the tracker claim.",
],
readinessStatus: chainGasBudgetRows.some((row) => row.status === "chain_gas_budget_shortfall")
? "on_chain_stability_transactions_gas_shortfall"
: "on_chain_stability_transactions_gas_budget_satisfied",
};
const coffeeMoneyExecutableRows = rows.filter((row) => [
"seed_existing_visible_pool_from_deployer_balance",
"create_or_bind_pool_then_seed_from_deployer_balance",
].includes(row.fundingPath));
const coffeeMoneyGasShortfallChains = chainGasBudgetRows.filter((row) => row.status === "chain_gas_budget_shortfall");
const coffeeMoneyPlan = {
purpose: "start_visible_indexable_liquidity_with_coffee_money",
operatorObservedUsdAvailable: coffeeMoneyUsdAvailable,
liquidityDustUsdPerRow: coffeeMoneyLiquidityUsdPerRow,
immediatelyUsefulRows: coffeeMoneyExecutableRows.length,
estimatedLiquidityDustUsd: Number((coffeeMoneyExecutableRows.length * coffeeMoneyLiquidityUsdPerRow).toFixed(2)),
gasShortfallChains: coffeeMoneyGasShortfallChains.map((row) => ({
chainId: row.chainId,
nativeSymbol: row.nativeSymbol,
shortfallNative: row.shortfallNative,
symbols: row.symbols,
})),
assessment: coffeeMoneyUsdAvailable >= 35
? "enough_to_start_coffee_money_liquidity_if_routed_into_missing_native_gas"
: "not_enough_for_full_coffee_money_set",
recommendedOrder: [
"Top up native gas on shortfall chains first: Optimism, BSC, Polygon, Arbitrum.",
"Seed existing visible pools that already have deployer token balance.",
"Create or bind missing visible pools for rows that already have deployer token balance.",
"Run tiny real swaps/liquidity events so Etherscan/tracker indexers see fresh public facts.",
"Leave Protocolink-only official quote-asset rows for last unless a live quote proves conversion from current deployer assets.",
],
boundary: "This starts indexable public liquidity evidence; it does not create deep market depth or a large 1:1 peg reserve.",
};
const counts = rows.reduce((acc, row) => {
acc[row.status] = (acc[row.status] || 0) + 1;
return acc;
}, {});
const nev = await buildNonEvmFundingRequirements();
const nonEvmFundingRequirements = nev.requirements;
const ethereumSourceInventory = await buildEthereumSourceInventory();
const payload = {
generatedAt: new Date().toISOString(),
mode: "read_only_no_broadcast",
deployer,
sourceReadiness: "reports/status/token-aggregation-adoption-readiness-live-20260509.json",
summary: {
rows: rows.length,
nonEvmFundingRequirementRows: nonEvmFundingRequirements.length,
fundableTokenBalancePresent: rows.filter((row) => row.status.startsWith("fundable_token_balance_present")).length,
poolBindingGated: rows.filter((row) => row.status.includes("pool_binding")).length,
gasGated: rows.filter((row) => row.status.includes("gas_gated")).length,
protocolinkSwapCandidates: rows.filter((row) => row.fundingPath === "protocolink_swap_candidate_for_non_mintable_quote_asset").length,
bridgeOrMintCandidates: rows.filter((row) => row.fundingPath === "bridge_or_destination_mint_repo_asset_then_seed").length,
poolCreateOrBindFirst: rows.filter((row) => row.fundingPath === "create_or_bind_pool_then_seed_from_deployer_balance").length,
seedExistingVisiblePoolNow: rows.filter((row) => row.fundingPath === "seed_existing_visible_pool_from_deployer_balance").length,
gasBudgetSatisfied: rows.filter((row) => row.gasPlan.status === "gas_budget_satisfied").length,
gasBudgetShortfall: rows.filter((row) => row.gasPlan.status === "gas_budget_shortfall").length,
chainGasBudgetSatisfied: chainGasBudgetRows.filter((row) => row.status === "chain_gas_budget_satisfied").length,
chainGasBudgetShortfall: chainGasBudgetRows.filter((row) => row.status === "chain_gas_budget_shortfall").length,
gasPolicy: {
stabilityCycles,
gasSafetyBps: Number(gasSafetyBps),
},
statusCounts: counts,
},
rows,
gasBudgetRows,
chainGasBudgetRows,
etherscanStability,
coffeeMoneyPlan,
ethereumSourceInventory,
nonEvmFundingRequirements,
operatorNonEvmBindingEcho: nev.bindingEcho,
};
const md = [
"# Token-Aggregation Liquidity Gap Funding Plan",
"",
`- Generated: \`${payload.generatedAt}\``,
`- Mode: \`${payload.mode}\``,
`- Deployer: \`${deployer}\``,
"",
table(["Metric", "Count"], Object.entries(payload.summary).map(([key, value]) => [key, typeof value === "object" ? JSON.stringify(value) : value])),
"",
"## Rows",
"",
table(
["Chain", "Symbol", "Category", "Pools", "Native", "Token balance", "Status", "Funding path", "Gas shortfall"],
rows.map((row) => [row.chainId, row.symbol, row.category, row.poolCount, row.nativeBalance, row.tokenBalance, row.status, row.fundingPath, `${row.gasPlan.shortfallNative} ${row.gasPlan.nativeSymbol}`]),
),
"",
"### Chain-Level Gas Budget",
"",
"This aggregates all planned row actions by network because the same deployer native balance pays every deployment, seed, swap, bridge, and stability transaction on that chain.",
"",
table(
["Chain", "Symbols", "Native", "Rows", "Gas price gwei", "Required", "Balance", "Shortfall", "Status"],
chainGasBudgetRows.map((row) => [
row.chainId,
row.symbols,
row.nativeSymbol,
row.rows,
row.gasPriceGwei,
`${row.requiredNative} ${row.nativeSymbol}`,
`${row.nativeBalance} ${row.nativeSymbol}`,
`${row.shortfallNative} ${row.nativeSymbol}`,
row.status,
]),
),
"",
"## Gas Budget",
"",
`Gas is budgeted for one deployment/seed action plus \`${stabilityCycles}\` continual stability cycles, with a \`${Number(gasSafetyBps) / 100}%\` safety multiplier. Etherscan/token-tracker stability itself is off-chain indexing; gas only funds the on-chain facts that Etherscan can index.`,
"",
table(
["Chain", "Symbol", "Native", "Gas price gwei", "One-time gas", "Stability gas/cycle", "Required", "Balance", "Shortfall", "Status"],
rows.map((row) => [
row.chainId,
row.symbol,
row.gasPlan.nativeSymbol,
row.gasPlan.gasPriceGwei,
row.gasPlan.oneTimeGasUnits,
row.gasPlan.stabilityGasUnitsPerCycle,
`${row.gasPlan.requiredNative} ${row.gasPlan.nativeSymbol}`,
`${row.gasPlan.nativeBalance} ${row.gasPlan.nativeSymbol}`,
`${row.gasPlan.shortfallNative} ${row.gasPlan.nativeSymbol}`,
row.gasPlan.status,
]),
),
"",
"## Etherscan Stability Boundary",
"",
`- Purpose: \`${etherscanStability.purpose}\``,
`- Status: \`${etherscanStability.readinessStatus}\``,
`- Boundary: ${etherscanStability.boundary}`,
"",
"Required on-chain facts for Etherscan/tracker stability:",
"",
...etherscanStability.requiredOnChainFacts.map((item) => `- ${item}`),
"",
"Gas budget role:",
"",
...etherscanStability.gasBudgetRole.map((item) => `- ${item}`),
"",
"Cannot be solved by gas alone:",
"",
...etherscanStability.cannotBeSolvedByGasAlone.map((item) => `- ${item}`),
"",
"## Coffee-Money Start Plan",
"",
`- Operator-observed deployer value available: \`$${coffeeMoneyPlan.operatorObservedUsdAvailable}\``,
`- Assessment: \`${coffeeMoneyPlan.assessment}\``,
`- Immediately useful rows: \`${coffeeMoneyPlan.immediatelyUsefulRows}\``,
`- Planning dust liquidity: \`$${coffeeMoneyPlan.liquidityDustUsdPerRow}\` per row`,
`- Estimated dust liquidity: \`$${coffeeMoneyPlan.estimatedLiquidityDustUsd}\``,
`- Boundary: ${coffeeMoneyPlan.boundary}`,
"",
"Native gas shortfall chains to fill first:",
"",
table(
["Chain", "Symbols", "Shortfall"],
coffeeMoneyPlan.gasShortfallChains.map((row) => [row.chainId, row.symbols, `${row.shortfallNative} ${row.nativeSymbol}`]),
),
"",
"Recommended order:",
"",
...coffeeMoneyPlan.recommendedOrder.map((item) => `- ${item}`),
"",
"### Ethereum Source Inventory",
"",
`- Native ETH: \`${ethereumSourceInventory.native.balance}\``,
"",
table(
["Token", "Balance", "Role", "Status"],
ethereumSourceInventory.tokens.map((token) => [token.symbol, token.balance, token.role, token.balanceStatus]),
),
"",
"Interpretation:",
"",
...ethereumSourceInventory.interpretation.map((item) => `- ${item}`),
"",
"## Funding Path Interpretation",
"",
"- `seed_existing_visible_pool_from_deployer_balance`: token and gas are present; only operator approval and pool-specific seeding rules remain.",
"- `create_or_bind_pool_then_seed_from_deployer_balance`: token and gas are present, but no visible/indexable pool binding exists yet.",
"- `bridge_or_destination_mint_repo_asset_then_seed`: repo-controlled c*/cW* inventory can be moved or minted once the bridge/mint path and destination gas are ready.",
"- `protocolink_swap_candidate_for_non_mintable_quote_asset`: the needed asset is an official/non-mintable quote asset; Protocolink can help only after a live quote proves a deployer-held source asset converts into the exact target token with acceptable minOut.",
"- `external_quote_asset_required`: neither bridge nor Protocolink coverage is classified for that exact non-mintable quote asset.",
"",
"## Non-EVM Funding Requirements",
"",
"These networks are now part of funding scope. The planner resolves non-EVM deployer wallets where the repo can prove them, checks native gas balances where possible, and applies `minimumFundingTargets` hints from `config/non-evm-operator-binding.json` (operator must still confirm live venue targets).",
"",
table(
["Field", "Value"],
Object.entries(payload.operatorNonEvmBindingEcho || {}).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)]),
),
"",
table(
["Network", "Target", "Wallet", "Source", "Native gas", "Current balance", "Required funding", "Status"],
payload.nonEvmFundingRequirements.map((row) => [
row.network,
row.target,
row.walletAddress,
row.walletSource,
row.nativeGasAsset,
row.currentBalanceStatus,
row.requiredFunding,
row.status,
]),
),
"",
"### Non-EVM Requirement Details",
"",
...payload.nonEvmFundingRequirements.flatMap((row) => [
`#### ${row.network}`,
"",
...row.requirements.map((requirement) => `- ${requirement}`),
"",
]),
"",
"## Execution Boundary",
"",
"This planner is read-only. It proves whether the deployer currently holds token and gas inventory for each liquidity gap. It does not create pools, add liquidity, approve tokens, bridge assets, or broadcast transactions.",
].join("\n");
mkdirSync(resolve(repoRoot, "reports/status"), { recursive: true });
writeFileSync(jsonOut, `${JSON.stringify(payload, null, 2)}\n`);
writeFileSync(mdOut, `${md}\n`);
console.log(jsonOut);