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
292 lines
10 KiB
JavaScript
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}`);
|