- 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>
991 lines
44 KiB
JavaScript
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);
|