Files
proxmox/scripts/status/check-remaining-deployer-balances.mjs
defiQUG ccab738ae2
All checks were successful
Deploy to Phoenix / validate (push) Successful in 1m21s
Deploy to Phoenix / deploy (push) Successful in 48s
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Successful in 1m20s
phoenix-deploy Deployed to cloudflare-sync
Deploy to Phoenix / cloudflare (push) Successful in 40s
Advance official DODO migration routing evidence
2026-04-30 03:48:53 -07:00

292 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* Read-only deployer balance report for the remaining official-routing blockers.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { resolve } from "node:path";
import { Contract, JsonRpcProvider, Wallet, formatEther, formatUnits, parseUnits } from "ethers";
const repoRoot = resolve(new URL("../..", import.meta.url).pathname);
const matrixPath = resolve(repoRoot, "config/all-mainnet-pool-creation-matrix.json");
const tasksPath = resolve(repoRoot, "reports/status/all-mainnet-remaining-official-routing-tasks-latest.json");
const discoveryPath = resolve(repoRoot, "reports/status/all-mainnet-official-dodo-discovery-latest.json");
const outJson = resolve(repoRoot, "reports/status/all-mainnet-remaining-deployer-balances-latest.json");
const outMd = resolve(repoRoot, "reports/status/all-mainnet-remaining-deployer-balances-latest.md");
const ERC20_ABI = [
"function balanceOf(address) view returns (uint256)",
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
];
const chainRpcCandidates = {
10: ["OPTIMISM_MAINNET_RPC", "OPTIMISM_RPC_URL", "OPTIMISM_RPC"],
25: ["CRONOS_RPC_URL", "CRONOS_RPC", "CRONOS_MAINNET_RPC"],
100: ["GNOSIS_MAINNET_RPC", "GNOSIS_RPC_URL", "GNOSIS_RPC"],
8453: ["BASE_MAINNET_RPC", "BASE_RPC_URL", "BASE_RPC"],
42161: ["ARBITRUM_MAINNET_RPC", "ARBITRUM_RPC_URL", "ARBITRUM_RPC"],
};
function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function loadEnvFile(path, env) {
if (!existsSync(path)) return;
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
const index = trimmed.indexOf("=");
const key = trimmed.slice(0, index).replace(/^export\s+/, "").trim();
let value = trimmed.slice(index + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
value = value.replace(/\s+#.*$/, "");
value = value.replace(/\$\{([^}:]+)(:-([^}]*))?\}/g, (_, name, _fallback, fallbackValue) => (
env[name] ?? process.env[name] ?? fallbackValue ?? ""
));
if (!value || value === "0x" || value.includes("${")) continue;
env[key] ??= value;
}
}
function loadEnv() {
const env = { ...process.env };
loadEnvFile(resolve(repoRoot, ".env"), env);
loadEnvFile(resolve(repoRoot, "smom-dbis-138/.env"), env);
loadEnvFile(resolve(homedir(), ".secure-secrets/private-keys.env"), env);
if (!env.PRIVATE_KEY && env.DEPLOYER_PRIVATE_KEY) env.PRIVATE_KEY = env.DEPLOYER_PRIVATE_KEY;
return env;
}
function rpcForChain(chainId, env) {
for (const key of chainRpcCandidates[chainId] || []) {
if (env[key]) return { key, url: env[key] };
}
return { key: null, url: null };
}
function table(headers, rows) {
return [
`| ${headers.join(" | ")} |`,
`| ${headers.map(() => "---").join(" | ")} |`,
...rows.map((row) => `| ${row.map((cell) => String(cell ?? "").replace(/\|/g, "\\|")).join(" | ")} |`),
].join("\n");
}
function addToken(tokensByChain, chainId, token) {
if (!token?.address) return;
const key = String(chainId);
tokensByChain[key] ??= new Map();
tokensByChain[key].set(token.address.toLowerCase(), {
symbol: token.symbol,
address: token.address,
});
}
function pair(row) {
return `${row.baseToken?.symbol || "?"}/${row.quoteToken?.symbol || "?"}`;
}
function minDodoSeedRaw(decimals) {
// DODO V2 DVM rejected 1_000/2_000 raw but accepted 10_000 raw on 6-decimal quote rows.
return parseUnits("0.01", Number(decimals));
}
const env = loadEnv();
const operator = env.DEPLOYER_ADDRESS || env.SIGNER_ADDRESS || (env.PRIVATE_KEY ? new Wallet(env.PRIVATE_KEY).address : null);
if (!operator) throw new Error("Set DEPLOYER_ADDRESS, SIGNER_ADDRESS, or PRIVATE_KEY.");
const matrix = readJson(matrixPath);
const tasks = existsSync(tasksPath) ? readJson(tasksPath) : { unsupportedRoutingTasks: [], zeroPoolTasks: [] };
const discovery = existsSync(discoveryPath) ? readJson(discoveryPath) : { rows: [] };
const unsupportedRows = matrix.rows.filter((row) => (
row.requiredForSpend === true &&
row.protocol === "dodo_pmm" &&
[25, 100].includes(Number(row.chainId))
));
const zeroRows = discovery.rows.filter((row) => (
(row.poolEvidence || []).some((pool) => pool.seeded === false)
));
const tokensByChain = {};
for (const row of [...unsupportedRows, ...zeroRows.map((zero) => matrix.rows.find((row) => row.poolId === zero.poolId)).filter(Boolean)]) {
addToken(tokensByChain, row.chainId, row.baseToken);
addToken(tokensByChain, row.chainId, row.quoteToken);
}
const chainReports = [];
for (const chainId of Object.keys(tokensByChain).map(Number).sort((a, b) => a - b)) {
const rpc = rpcForChain(chainId, env);
const chainReport = {
chainId,
rpcEnvKey: rpc.key,
nativeBalanceWei: null,
nativeBalance: null,
tokenBalances: [],
errors: [],
};
if (!rpc.url) {
chainReport.errors.push("missing_rpc_url");
chainReports.push(chainReport);
continue;
}
const provider = new JsonRpcProvider(rpc.url, chainId, { staticNetwork: true });
try {
const native = await provider.getBalance(operator);
chainReport.nativeBalanceWei = native.toString();
chainReport.nativeBalance = formatEther(native);
} catch (error) {
chainReport.errors.push(`native_balance:${error.shortMessage || error.message}`);
}
for (const token of tokensByChain[String(chainId)].values()) {
const entry = {
symbol: token.symbol,
address: token.address,
balanceRaw: null,
decimals: null,
balance: null,
minDodoSeedRaw: null,
minDodoSeed: null,
shortfallToMinDodoSeedRaw: null,
shortfallToMinDodoSeed: null,
error: null,
};
try {
const contract = new Contract(token.address, ERC20_ABI, provider);
const [decimals, balance] = await Promise.all([
contract.decimals(),
contract.balanceOf(operator),
]);
const minSeed = minDodoSeedRaw(decimals);
const shortfall = balance >= minSeed ? 0n : minSeed - balance;
entry.decimals = Number(decimals);
entry.balanceRaw = balance.toString();
entry.balance = formatUnits(balance, decimals);
entry.minDodoSeedRaw = minSeed.toString();
entry.minDodoSeed = formatUnits(minSeed, decimals);
entry.shortfallToMinDodoSeedRaw = shortfall.toString();
entry.shortfallToMinDodoSeed = formatUnits(shortfall, decimals);
} catch (error) {
entry.error = error.shortMessage || error.message;
}
chainReport.tokenBalances.push(entry);
}
chainReports.push(chainReport);
}
const unsupportedTaskRows = unsupportedRows.map((row) => {
const task = (tasks.unsupportedRoutingTasks || []).find((item) => item.poolId === row.poolId);
return {
poolId: row.poolId,
chainId: row.chainId,
network: row.network,
pair: pair(row),
currentReplacementPool: row.poolAddress,
targetSupportProtocol: task?.targetSupportProtocol || "pending_official_route_profile",
minimumSpendPlan: task?.targetSupportProtocol === "oneinch_aggregator"
? "No pool seed required; minimum spend is gas plus a tiny route canary after nonzero official 1inch quote is proven."
: "Needs official protocol selection first; no safe spend amount can be calculated until factory/router is confirmed.",
};
});
const zeroTaskRows = zeroRows.map((zero) => {
const matrixRow = matrix.rows.find((row) => row.poolId === zero.poolId);
const evidence = (zero.poolEvidence || []).find((pool) => pool.seeded === false);
const chainReport = chainReports.find((chain) => chain.chainId === zero.chainId);
const quoteBalance = chainReport?.tokenBalances.find((token) => token.symbol === matrixRow?.quoteToken?.symbol);
return {
poolId: zero.poolId,
chainId: zero.chainId,
network: zero.network,
pair: zero.pair,
officialPool: evidence?.poolAddress || null,
baseReserveRaw: evidence?.baseReserveRaw || null,
quoteReserveRaw: evidence?.quoteReserveRaw || null,
totalSupplyRaw: evidence?.totalSupplyRaw || null,
quoteSymbol: matrixRow?.quoteToken?.symbol || null,
deployerQuoteBalance: quoteBalance?.balance || null,
minimumQuoteNeeded: quoteBalance?.minDodoSeed || "0.01",
quoteShortfall: quoteBalance?.shortfallToMinDodoSeed || null,
minimumSpendPlan: "Top up quote to at least 0.01 token units, then seed equal base/quote through DODOAtomicSeeder.",
};
});
const report = {
generatedAt: new Date().toISOString(),
mode: "read_only_deployer_balance_inventory",
operator,
summary: {
unsupportedDodoRows: unsupportedTaskRows.length,
zeroOrUnusableOfficialPools: zeroTaskRows.length,
chainsChecked: chainReports.length,
},
chainReports,
unsupportedTaskRows,
zeroTaskRows,
};
const md = [
"# ALL Mainnet Remaining Deployer Balances",
"",
`- Generated: \`${report.generatedAt}\``,
`- Operator: \`${operator}\``,
"",
table(["Metric", "Count"], Object.entries(report.summary)),
"",
"## Chain Balances",
"",
table(
["Chain", "Native", "Token", "Balance", "Min DODO Seed", "Shortfall"],
chainReports.flatMap((chain) => chain.tokenBalances.map((token) => [
chain.chainId,
chain.nativeBalance,
token.symbol,
token.balance ?? token.error,
token.minDodoSeed,
token.shortfallToMinDodoSeed,
])),
),
"",
"## Unsupported Rows",
"",
table(
["Pool", "Chain", "Pair", "Target", "Minimum Spend Plan"],
unsupportedTaskRows.map((row) => [
row.poolId,
row.chainId,
row.pair,
row.targetSupportProtocol,
row.minimumSpendPlan,
]),
),
"",
"## Zero / Unusable Official Pools",
"",
table(
["Pool", "Chain", "Pair", "Official Pool", "Quote Balance", "Quote Shortfall", "Minimum Spend Plan"],
zeroTaskRows.map((row) => [
row.poolId,
row.chainId,
row.pair,
row.officialPool,
row.deployerQuoteBalance,
row.quoteShortfall,
row.minimumSpendPlan,
]),
),
"",
].join("\n");
mkdirSync(resolve(repoRoot, "reports/status"), { recursive: true });
writeFileSync(outJson, `${JSON.stringify(report, null, 2)}\n`);
writeFileSync(outMd, `${md}\n`);
console.log(`[OK] Remaining deployer balance report written: ${outJson}`);