Apply ALL Mainnet vault assignments and canary preflight
Some checks failed
Deploy to Phoenix / validate (push) Successful in 1m11s
Deploy to Phoenix / deploy (push) Successful in 44s
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Has been cancelled
Deploy to Phoenix / cloudflare (push) Has been cancelled
phoenix-deploy Deployed to atomic-swap-dapp-live
Some checks failed
Deploy to Phoenix / validate (push) Successful in 1m11s
Deploy to Phoenix / deploy (push) Successful in 44s
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Has been cancelled
Deploy to Phoenix / cloudflare (push) Has been cancelled
phoenix-deploy Deployed to atomic-swap-dapp-live
This commit is contained in:
File diff suppressed because it is too large
Load Diff
20
config/all-mainnet-vault-assignments.json
Normal file
20
config/all-mainnet-vault-assignments.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"description": "Operational vault assignments generated from smom-dbis-138/.env public addresses. No private material is stored here.",
|
||||
"defaultByRole": {
|
||||
"treasury_reserve": "0x74eccf9affb0e0938c2168ebdf7ef63a26964483",
|
||||
"bridge_liquidity": "0x31884f84555210FFB36a19D2471b8eBc7372d0A8",
|
||||
"protocol_adapter": "0xb9E29cFa1f89d369671E640d0BB3aD94Cab43965",
|
||||
"emergency_withdraw": "0xb9E29cFa1f89d369671E640d0BB3aD94Cab43965",
|
||||
"single_sided_inventory": "0x31884f84555210FFB36a19D2471b8eBc7372d0A8"
|
||||
},
|
||||
"byChain": {
|
||||
"651940": {
|
||||
"treasury_reserve": "0x74eccf9affb0e0938c2168ebdf7ef63a26964483",
|
||||
"bridge_liquidity": "0x31884f84555210FFB36a19D2471b8eBc7372d0A8",
|
||||
"protocol_adapter": "0xb9E29cFa1f89d369671E640d0BB3aD94Cab43965",
|
||||
"emergency_withdraw": "0xb9E29cFa1f89d369671E640d0BB3aD94Cab43965",
|
||||
"single_sided_inventory": "0x31884f84555210FFB36a19D2471b8eBc7372d0A8"
|
||||
}
|
||||
},
|
||||
"byPoolId": {}
|
||||
}
|
||||
@@ -48,6 +48,7 @@
|
||||
"pool-matrix:validate": "node scripts/validation/validate-pool-creation-matrix.mjs",
|
||||
"all-mainnet:readiness": "node scripts/status/generate-all-mainnet-readiness.mjs",
|
||||
"all-mainnet:pool-balances": "node scripts/status/check-all-mainnet-required-pool-balances.mjs",
|
||||
"all-mainnet:canary-preflight": "node scripts/status/preflight-all-mainnet-canaries.mjs",
|
||||
"all-mainnet:apply-vaults": "node scripts/status/apply-all-mainnet-vault-assignments.mjs",
|
||||
"all-mainnet:record-canaries": "node scripts/status/record-all-mainnet-canary-evidence.mjs"
|
||||
},
|
||||
|
||||
230
scripts/status/preflight-all-mainnet-canaries.mjs
Normal file
230
scripts/status/preflight-all-mainnet-canaries.mjs
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Read-only canary readiness preflight for ALL Mainnet spend pools.
|
||||
*
|
||||
* This does not approve tokens or execute swaps. It checks the operator
|
||||
* wallet inventory, allowances, and quote/query surfaces for rows already
|
||||
* promoted to live_read.
|
||||
*/
|
||||
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
const repoRoot = resolve(new URL("../..", import.meta.url).pathname);
|
||||
const matrixPath = resolve(repoRoot, "config/all-mainnet-pool-creation-matrix.json");
|
||||
const outDir = resolve(repoRoot, "reports/status");
|
||||
const reportPath = resolve(outDir, "all-mainnet-canary-preflight-latest.json");
|
||||
|
||||
const rpcByChain = {
|
||||
1: process.env.ETHEREUM_MAINNET_RPC || process.env.RPC_URL_1 || "https://ethereum.publicnode.com",
|
||||
10: process.env.OPTIMISM_RPC || process.env.RPC_URL_10 || "https://optimism.publicnode.com",
|
||||
25: process.env.CRONOS_RPC || process.env.RPC_URL_25 || "https://cronos-evm-rpc.publicnode.com",
|
||||
56: process.env.BSC_RPC || process.env.RPC_URL_56 || "https://bsc-rpc.publicnode.com",
|
||||
100: process.env.GNOSIS_RPC || process.env.RPC_URL_100 || "https://gnosis.publicnode.com",
|
||||
137: process.env.POLYGON_RPC || process.env.RPC_URL_137 || "https://polygon-bor-rpc.publicnode.com",
|
||||
8453: process.env.BASE_RPC || process.env.RPC_URL_8453 || "https://base-rpc.publicnode.com",
|
||||
42161: process.env.ARBITRUM_RPC || process.env.RPC_URL_42161 || "https://arbitrum-one-rpc.publicnode.com",
|
||||
42220: process.env.CELO_RPC || process.env.RPC_URL_42220 || "https://celo-rpc.publicnode.com",
|
||||
43114: process.env.AVALANCHE_RPC || process.env.RPC_URL_43114 || "https://avalanche-c-chain-rpc.publicnode.com",
|
||||
651940: process.env.ALL_MAINNET_RPC || process.env.CHAIN_651940_RPC_URL || "https://mainnet-rpc.alltra.global",
|
||||
};
|
||||
|
||||
const erc20Abi = [
|
||||
"function balanceOf(address) view returns (uint256)",
|
||||
"function allowance(address,address) view returns (uint256)",
|
||||
"function decimals() view returns (uint8)",
|
||||
];
|
||||
const uniV2RouterAbi = [
|
||||
"function getAmountsOut(uint256,address[]) view returns (uint256[])",
|
||||
];
|
||||
const dodoDvmAbi = [
|
||||
"function querySellBase(address,uint256) view returns (uint256)",
|
||||
"function querySellQuote(address,uint256) view returns (uint256)",
|
||||
];
|
||||
|
||||
function operatorAddress() {
|
||||
if (process.env.DEPLOYER_ADDRESS && ethers.isAddress(process.env.DEPLOYER_ADDRESS)) {
|
||||
return ethers.getAddress(process.env.DEPLOYER_ADDRESS);
|
||||
}
|
||||
if (process.env.SIGNER_ADDRESS && ethers.isAddress(process.env.SIGNER_ADDRESS)) {
|
||||
return ethers.getAddress(process.env.SIGNER_ADDRESS);
|
||||
}
|
||||
if (process.env.PRIVATE_KEY) {
|
||||
return new ethers.Wallet(process.env.PRIVATE_KEY).address;
|
||||
}
|
||||
throw new Error("Set DEPLOYER_ADDRESS, SIGNER_ADDRESS, or PRIVATE_KEY for canary preflight.");
|
||||
}
|
||||
|
||||
function smallQuoteAmount(decimals) {
|
||||
const capped = Math.min(Number(decimals), 6);
|
||||
return 10n ** BigInt(capped);
|
||||
}
|
||||
|
||||
function normalizedAddress(address) {
|
||||
return ethers.getAddress(String(address).toLowerCase());
|
||||
}
|
||||
|
||||
async function callOrError(fn) {
|
||||
try {
|
||||
return { ok: true, value: await fn() };
|
||||
} catch (error) {
|
||||
return { ok: false, error: error.shortMessage || error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function tokenSnapshot(provider, tokenAddress, owner, spender) {
|
||||
const token = new ethers.Contract(normalizedAddress(tokenAddress), erc20Abi, provider);
|
||||
const allowanceSpender = spender ? normalizedAddress(spender) : null;
|
||||
const [balance, decimals, allowance] = await Promise.all([
|
||||
callOrError(() => token.balanceOf(owner)),
|
||||
callOrError(() => token.decimals()),
|
||||
allowanceSpender ? callOrError(() => token.allowance(owner, allowanceSpender)) : Promise.resolve({ ok: false, error: "spender_not_configured" }),
|
||||
]);
|
||||
|
||||
return {
|
||||
address: tokenAddress,
|
||||
balanceRaw: balance.ok ? balance.value.toString() : null,
|
||||
decimals: decimals.ok ? Number(decimals.value) : null,
|
||||
allowanceRaw: allowance.ok ? allowance.value.toString() : null,
|
||||
errors: [
|
||||
...(balance.ok ? [] : [`balance:${balance.error}`]),
|
||||
...(decimals.ok ? [] : [`decimals:${decimals.error}`]),
|
||||
...(spender && !allowance.ok ? [`allowance:${allowance.error}`] : []),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function quoteUniswap(provider, row, base, quote) {
|
||||
if (!row.routerAddress) return { supported: false, errors: ["missing_router_address"] };
|
||||
const router = new ethers.Contract(normalizedAddress(row.routerAddress), uniV2RouterAbi, provider);
|
||||
const baseAmount = smallQuoteAmount(base.decimals ?? 18);
|
||||
const quoteAmount = smallQuoteAmount(quote.decimals ?? 18);
|
||||
const baseToken = normalizedAddress(row.baseToken.address);
|
||||
const quoteToken = normalizedAddress(row.quoteToken.address);
|
||||
const [baseToQuote, quoteToBase] = await Promise.all([
|
||||
callOrError(() => router.getAmountsOut(baseAmount, [baseToken, quoteToken])),
|
||||
callOrError(() => router.getAmountsOut(quoteAmount, [quoteToken, baseToken])),
|
||||
]);
|
||||
|
||||
return {
|
||||
supported: true,
|
||||
routerAddress: row.routerAddress,
|
||||
baseToQuote: baseToQuote.ok ? baseToQuote.value.map((v) => v.toString()) : null,
|
||||
quoteToBase: quoteToBase.ok ? quoteToBase.value.map((v) => v.toString()) : null,
|
||||
errors: [
|
||||
...(baseToQuote.ok ? [] : [`base_to_quote:${baseToQuote.error}`]),
|
||||
...(quoteToBase.ok ? [] : [`quote_to_base:${quoteToBase.error}`]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function quoteDodo(provider, row, base, quote, operator) {
|
||||
const pool = new ethers.Contract(normalizedAddress(row.poolAddress), dodoDvmAbi, provider);
|
||||
const baseAmount = smallQuoteAmount(base.decimals ?? 18);
|
||||
const quoteAmount = smallQuoteAmount(quote.decimals ?? 18);
|
||||
const [sellBase, sellQuote] = await Promise.all([
|
||||
callOrError(() => pool.querySellBase(operator, baseAmount)),
|
||||
callOrError(() => pool.querySellQuote(operator, quoteAmount)),
|
||||
]);
|
||||
|
||||
return {
|
||||
supported: sellBase.ok || sellQuote.ok,
|
||||
poolAddress: row.poolAddress,
|
||||
sellBaseOutRaw: sellBase.ok ? sellBase.value.toString() : null,
|
||||
sellQuoteOutRaw: sellQuote.ok ? sellQuote.value.toString() : null,
|
||||
errors: [
|
||||
...(sellBase.ok ? [] : [`query_sell_base:${sellBase.error}`]),
|
||||
...(sellQuote.ok ? [] : [`query_sell_quote:${sellQuote.error}`]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const matrix = JSON.parse(readFileSync(matrixPath, "utf8"));
|
||||
const generatedAt = new Date().toISOString();
|
||||
const operator = operatorAddress();
|
||||
const rows = matrix.rows.filter((row) => row.requiredForSpend === true && row.status === "live_read");
|
||||
const results = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const rpcUrl = rpcByChain[row.chainId];
|
||||
const result = {
|
||||
poolId: row.poolId,
|
||||
chainId: row.chainId,
|
||||
network: row.network,
|
||||
protocol: row.protocol,
|
||||
poolAddress: row.poolAddress,
|
||||
routerAddress: row.routerAddress,
|
||||
operator,
|
||||
baseToken: row.baseToken,
|
||||
quoteToken: row.quoteToken,
|
||||
base: null,
|
||||
quote: null,
|
||||
quoteCheck: null,
|
||||
canaryPreflight: "blocked",
|
||||
blockers: [],
|
||||
};
|
||||
|
||||
if (!rpcUrl) {
|
||||
result.blockers.push("missing_rpc");
|
||||
results.push(result);
|
||||
continue;
|
||||
}
|
||||
if (!row.poolAddress || !row.baseToken?.address || !row.quoteToken?.address) {
|
||||
if (!row.poolAddress) result.blockers.push("missing_pool_address");
|
||||
if (!row.baseToken?.address) result.blockers.push("missing_base_token_address");
|
||||
if (!row.quoteToken?.address) result.blockers.push("missing_quote_token_address");
|
||||
results.push(result);
|
||||
continue;
|
||||
}
|
||||
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl, row.chainId, { staticNetwork: true });
|
||||
const spender = row.routerAddress || row.poolAddress;
|
||||
const [base, quote] = await Promise.all([
|
||||
tokenSnapshot(provider, row.baseToken.address, operator, spender),
|
||||
tokenSnapshot(provider, row.quoteToken.address, operator, spender),
|
||||
]);
|
||||
result.base = base;
|
||||
result.quote = quote;
|
||||
|
||||
if (base.errors.length > 0) result.blockers.push(...base.errors.map((error) => `base_${error}`));
|
||||
if (quote.errors.length > 0) result.blockers.push(...quote.errors.map((error) => `quote_${error}`));
|
||||
if (BigInt(base.balanceRaw || "0") === 0n) result.blockers.push(`missing_operator_inventory:${row.baseToken.symbol}`);
|
||||
if (BigInt(quote.balanceRaw || "0") === 0n) result.blockers.push(`missing_operator_inventory:${row.quoteToken.symbol}`);
|
||||
|
||||
if (row.protocol === "uniswap_v2") {
|
||||
result.quoteCheck = await quoteUniswap(provider, row, base, quote);
|
||||
} else if (row.protocol === "dodo_pmm") {
|
||||
result.quoteCheck = await quoteDodo(provider, row, base, quote, operator);
|
||||
} else {
|
||||
result.quoteCheck = { supported: false, errors: [`unsupported_protocol:${row.protocol}`] };
|
||||
}
|
||||
|
||||
if (result.quoteCheck?.errors?.length > 0) {
|
||||
result.blockers.push(...result.quoteCheck.errors.map((error) => `quote_${error}`));
|
||||
}
|
||||
if (!result.quoteCheck?.supported) {
|
||||
result.blockers.push("quote_surface_unverified");
|
||||
}
|
||||
|
||||
result.canaryPreflight = result.blockers.length === 0 ? "ready" : "blocked";
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
const statusCounts = results.reduce((counts, result) => {
|
||||
counts[result.canaryPreflight] = (counts[result.canaryPreflight] || 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
|
||||
const report = {
|
||||
generatedAt,
|
||||
sourceMatrix: "config/all-mainnet-pool-creation-matrix.json",
|
||||
operator,
|
||||
liveReadRequiredRows: rows.length,
|
||||
statusCounts,
|
||||
results,
|
||||
};
|
||||
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log(`[OK] ALL Mainnet canary preflight written: ${statusCounts.ready || 0} ready, ${statusCounts.blocked || 0} blocked.`);
|
||||
Reference in New Issue
Block a user