@oracle-agent/oracle 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +98 -0
- package/LICENSE +202 -0
- package/README.md +384 -0
- package/SECURITY.md +89 -0
- package/SETUP.md +235 -0
- package/artifacts/inscription/oracle-was-here-preview.png +0 -0
- package/artifacts/inscription/oracle-was-here.svg +1 -0
- package/artifacts/specialist-packs/oracle-full-crypto.json +273 -0
- package/bin/desk-server.mjs +438 -0
- package/bin/oracle-data-mcp.mjs +603 -0
- package/bin/oracle-init.mjs +423 -0
- package/bin/oracle-public-server.mjs +36 -0
- package/bin/oracle-route.mjs +254 -0
- package/bin/oracle-scan.mjs +192 -0
- package/docs/adding-a-chain.md +229 -0
- package/docs/architecture.md +135 -0
- package/docs/profiles.md +132 -0
- package/examples/add-a-chain.mjs +65 -0
- package/examples/research-a-token.mjs +70 -0
- package/package.json +90 -0
- package/profiles/_template/SOUL.md +47 -0
- package/profiles/_template/profile.json +22 -0
- package/profiles/bitcoin-agent/SOUL.md +31 -0
- package/profiles/bitcoin-agent/profile.json +32 -0
- package/profiles/hyperliquid-agent/SOUL.md +34 -0
- package/profiles/hyperliquid-agent/profile.json +37 -0
- package/profiles/oracle/SOUL.md +65 -0
- package/profiles/oracle/profile.json +35 -0
- package/profiles/polymarket-agent/SOUL.md +35 -0
- package/profiles/polymarket-agent/profile.json +34 -0
- package/profiles/profile.schema.json +90 -0
- package/profiles/protocol-builder/SOUL.md +50 -0
- package/profiles/protocol-builder/profile.json +37 -0
- package/profiles/robinhood-agent/SOUL.md +45 -0
- package/profiles/robinhood-agent/profile.json +39 -0
- package/profiles/solana-agent/SOUL.md +37 -0
- package/profiles/solana-agent/profile.json +37 -0
- package/profiles/stable-agent/SOUL.md +43 -0
- package/profiles/stable-agent/profile.json +37 -0
- package/public/oracle-console/app.js +272 -0
- package/public/oracle-console/bitcoin-wallets.js +206 -0
- package/public/oracle-console/index.html +91 -0
- package/public/oracle-console/styles.css +239 -0
- package/public/oracle-splash/index.html +931 -0
- package/scripts/build-inscription.py +230 -0
- package/scripts/check-test-count.mjs +105 -0
- package/scripts/e2e-hl-markets.mjs +21 -0
- package/scripts/e2e-hl-perps.mjs +48 -0
- package/scripts/e2e-hypercore-staking.mjs +128 -0
- package/scripts/e2e-solana-bitcoin.mjs +183 -0
- package/scripts/public-api-scan.mjs +23 -0
- package/scripts/secret-scan.mjs +181 -0
- package/scripts/verify-v3-venues.mjs +192 -0
- package/skills/oracle-best-execution/SKILL.md +127 -0
- package/skills/oracle-bitcoin/SKILL.md +53 -0
- package/skills/oracle-chain-graphs-telegram-cards/SKILL.md +59 -0
- package/skills/oracle-circuit-breaker/SKILL.md +51 -0
- package/skills/oracle-contract-research/SKILL.md +55 -0
- package/skills/oracle-desk/SKILL.md +58 -0
- package/skills/oracle-dex-launch/SKILL.md +38 -0
- package/skills/oracle-grants/SKILL.md +69 -0
- package/skills/oracle-hypercore-staking/SKILL.md +57 -0
- package/skills/oracle-hyperliquid/SKILL.md +56 -0
- package/skills/oracle-meme-token-sniper/SKILL.md +73 -0
- package/skills/oracle-nft-gacha-launch/SKILL.md +48 -0
- package/skills/oracle-nft-mint-gas-war/SKILL.md +63 -0
- package/skills/oracle-polymarket/SKILL.md +60 -0
- package/skills/oracle-protocol-builder/SKILL.md +38 -0
- package/skills/oracle-protocol-security/SKILL.md +60 -0
- package/skills/oracle-public-product/SKILL.md +44 -0
- package/skills/oracle-receipts/SKILL.md +52 -0
- package/skills/oracle-rfq-tokenized-assets/SKILL.md +69 -0
- package/skills/oracle-smart-wallet-scanner/SKILL.md +49 -0
- package/skills/oracle-solana/SKILL.md +65 -0
- package/skills/oracle-solana-nft/SKILL.md +54 -0
- package/skills/oracle-token-research/SKILL.md +67 -0
- package/src/agent-auth.mjs +191 -0
- package/src/approval-guard.mjs +282 -0
- package/src/attestation-secret.mjs +88 -0
- package/src/audit-log.mjs +196 -0
- package/src/auto-slippage.mjs +378 -0
- package/src/capability-posture.mjs +125 -0
- package/src/chains.mjs +62 -0
- package/src/data/catalog.mjs +495 -0
- package/src/data/desk-data.mjs +623 -0
- package/src/data/http.mjs +200 -0
- package/src/data/provider-endpoint.mjs +94 -0
- package/src/data/providers/aerodrome.mjs +244 -0
- package/src/data/providers/balancer.mjs +208 -0
- package/src/data/providers/bitcoin-esplora.mjs +230 -0
- package/src/data/providers/bitcoin-meta.mjs +378 -0
- package/src/data/providers/blockscout.mjs +14 -0
- package/src/data/providers/bridges.mjs +241 -0
- package/src/data/providers/cowswap.mjs +501 -0
- package/src/data/providers/curve.mjs +200 -0
- package/src/data/providers/defillama.mjs +88 -0
- package/src/data/providers/dexscreener.mjs +43 -0
- package/src/data/providers/evm-rpc.mjs +203 -0
- package/src/data/providers/geckoterminal.mjs +34 -0
- package/src/data/providers/gmx.mjs +495 -0
- package/src/data/providers/hl-info.mjs +102 -0
- package/src/data/providers/hl-markets.mjs +210 -0
- package/src/data/providers/hl-perps.mjs +382 -0
- package/src/data/providers/hl-staking.mjs +352 -0
- package/src/data/providers/hl-ws.mjs +119 -0
- package/src/data/providers/hyperevm-dex.mjs +49 -0
- package/src/data/providers/jupiter.mjs +182 -0
- package/src/data/providers/lifi.mjs +150 -0
- package/src/data/providers/magiceden-sol.mjs +355 -0
- package/src/data/providers/morpho.mjs +173 -0
- package/src/data/providers/odos.mjs +155 -0
- package/src/data/providers/oneinch.mjs +173 -0
- package/src/data/providers/opensea-multichain.mjs +136 -0
- package/src/data/providers/opensea-nft.mjs +99 -0
- package/src/data/providers/paraswap.mjs +117 -0
- package/src/data/providers/pendle.mjs +187 -0
- package/src/data/providers/poly-public.mjs +96 -0
- package/src/data/providers/poly-ws.mjs +103 -0
- package/src/data/providers/rh-agent.mjs +59 -0
- package/src/data/providers/satflow.mjs +336 -0
- package/src/data/providers/solana-rpc.mjs +186 -0
- package/src/data/providers/uniswap-v3.mjs +303 -0
- package/src/data/providers/zerox.mjs +166 -0
- package/src/data/public-api-scan.mjs +61 -0
- package/src/data/quote-placeholder.mjs +31 -0
- package/src/exact-integer.mjs +72 -0
- package/src/exec-policy.mjs +444 -0
- package/src/flags.mjs +15 -0
- package/src/fresh-window.mjs +76 -0
- package/src/gmx-attestation.mjs +175 -0
- package/src/index.mjs +50 -0
- package/src/nft-gas-war-guard.mjs +139 -0
- package/src/onboarding/agent-keys.mjs +157 -0
- package/src/onboarding/index.mjs +18 -0
- package/src/onboarding/tiers.mjs +139 -0
- package/src/oracle-env.mjs +38 -0
- package/src/protocol-execution.mjs +84 -0
- package/src/public-api/buzz-integration.mjs +256 -0
- package/src/public-api/connect-agent.mjs +397 -0
- package/src/public-api/grants.mjs +142 -0
- package/src/public-api/http.mjs +374 -0
- package/src/public-control/aa-adapter.mjs +402 -0
- package/src/public-control/build-registry.mjs +227 -0
- package/src/public-control/bundler-client.mjs +372 -0
- package/src/public-control/grant-indexer.mjs +296 -0
- package/src/public-control/policy-render.mjs +69 -0
- package/src/public-control/policy-schema.mjs +318 -0
- package/src/public-control/runtime-config.mjs +265 -0
- package/src/public-control/session-key-model.mjs +374 -0
- package/src/public-control/session-orchestrator.mjs +412 -0
- package/src/route-attestation.mjs +132 -0
- package/src/router/best-execution.mjs +221 -0
- package/src/router/index.mjs +185 -0
- package/src/router/prepare-bridge.mjs +288 -0
- package/src/router/prepare-route.mjs +341 -0
- package/src/router/proposal.mjs +311 -0
- package/src/router/risk-classifier.mjs +119 -0
- package/src/router/route-sources.mjs +292 -0
- package/src/scanner/chains.config.mjs +381 -0
- package/src/scanner/contract.mjs +270 -0
- package/src/scanner/evm-scanner.mjs +394 -0
- package/src/scanner/index.mjs +9 -0
- package/src/scanner/v2-venue.mjs +335 -0
- package/src/scanner/v3-venue.mjs +290 -0
- package/src/scopes.mjs +44 -0
- package/src/sell-simulation.mjs +167 -0
- package/src/token-transfer-guard.mjs +188 -0
- package/src/vault-attestation.mjs +145 -0
- package/src/venues.mjs +206 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Oracle end-to-end LIVE lane probe: Solana + Bitcoin.
|
|
2
|
+
// Read/quote/prepare/simulate only. No signing. No broadcast. No keys required.
|
|
3
|
+
//
|
|
4
|
+
// Run: node scripts/e2e-solana-bitcoin.mjs
|
|
5
|
+
// Exit 0 = every mandatory lane answered live. Optional keyed lanes are reported, not failed.
|
|
6
|
+
|
|
7
|
+
import { data as desk } from "../src/data/desk-data.mjs";
|
|
8
|
+
import { jupiterQuote, jupiterPrepareSwap, SOL_MINT, USDC_MINT } from "../src/data/providers/jupiter.mjs";
|
|
9
|
+
import { solanaSimulateTransaction } from "../src/data/providers/solana-rpc.mjs";
|
|
10
|
+
import { magicEdenSolStats, magicEdenSolListings, magicEdenSolPrepareBuy } from "../src/data/providers/magiceden-sol.mjs";
|
|
11
|
+
|
|
12
|
+
const PROBE_WALLET = process.env.ORACLE_E2E_SOL_PUBKEY || "11111111111111111111111111111111";
|
|
13
|
+
const PROBE_BTC = process.env.ORACLE_E2E_BTC_ADDRESS || "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq";
|
|
14
|
+
|
|
15
|
+
const results = [];
|
|
16
|
+
function record(lane, status, detail) {
|
|
17
|
+
results.push({ lane, status, detail });
|
|
18
|
+
const tag = status === "ok" ? "OK " : status === "keyed" ? "KEY " : "FAIL";
|
|
19
|
+
console.log(`${tag} ${lane} :: ${detail}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(lane, fn, { optional = false } = {}) {
|
|
23
|
+
try {
|
|
24
|
+
const detail = await fn();
|
|
25
|
+
record(lane, "ok", detail);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const message = String(error?.message || error).slice(0, 140);
|
|
29
|
+
record(lane, optional ? "keyed" : "fail", message);
|
|
30
|
+
return optional;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log("== SOLANA ==");
|
|
35
|
+
|
|
36
|
+
await run("solana.rpc.health", async () => {
|
|
37
|
+
const health = await desk.solana.health();
|
|
38
|
+
if (!health?.ok) throw new Error(`rpc unhealthy ${JSON.stringify(health).slice(0, 80)}`);
|
|
39
|
+
return `cluster ok slot lane live`;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await run("solana.rpc.balance", async () => {
|
|
43
|
+
const bal = await desk.solana.balance(PROBE_WALLET);
|
|
44
|
+
return `lamports=${bal?.lamports ?? bal?.value ?? "n/a"}`;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await run("solana.rpc.tokenAccounts", async () => {
|
|
48
|
+
const accounts = await desk.solana.tokenAccounts({ owner: PROBE_WALLET });
|
|
49
|
+
return `accounts=${accounts?.accounts?.length ?? 0}`;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
let jupQuote = null;
|
|
53
|
+
await run("solana.jupiter.quote(SOL->USDC)", async () => {
|
|
54
|
+
jupQuote = await jupiterQuote({ inputMint: SOL_MINT, outputMint: USDC_MINT, amount: "10000000", slippageBps: 50 });
|
|
55
|
+
const g = jupQuote.solanaGuard;
|
|
56
|
+
if (jupQuote.executionReady !== false) throw new Error("quote must not be executionReady");
|
|
57
|
+
if (g.slippageBps > 100) throw new Error("slippage cap breached");
|
|
58
|
+
return `out=${jupQuote.quote.outAmount} bps=${g.slippageBps} expires=${g.expiresAtMs - g.quotedAtMs}ms execReady=false`;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await run("solana.jupiter.slippageCap", async () => {
|
|
62
|
+
try {
|
|
63
|
+
await jupiterQuote({ inputMint: SOL_MINT, outputMint: USDC_MINT, amount: "10000000", slippageBps: 500 });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return `rejected 500bps: ${String(error.message).slice(0, 60)}`;
|
|
66
|
+
}
|
|
67
|
+
throw new Error("500 bps was NOT rejected");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let jupPrepared = null;
|
|
71
|
+
await run("solana.jupiter.prepareSwap", async () => {
|
|
72
|
+
jupPrepared = await jupiterPrepareSwap({
|
|
73
|
+
quoteResponse: jupQuote.quote,
|
|
74
|
+
userPublicKey: PROBE_WALLET,
|
|
75
|
+
});
|
|
76
|
+
if (jupPrepared.signingReady !== false || jupPrepared.broadcastReady !== false) throw new Error("prepare leaked signing/broadcast readiness");
|
|
77
|
+
if (!jupPrepared.requiresUserSignature) throw new Error("prepare must require user signature");
|
|
78
|
+
return `tx=${jupPrepared.swapTransaction.length}b64 requiresUserSig=true signingReady=false broadcastReady=false`;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await run("solana.rpc.simulate(preparedSwap)", async () => {
|
|
82
|
+
const sim = await solanaSimulateTransaction({ transaction: jupPrepared.swapTransaction, sigVerify: false });
|
|
83
|
+
const err = sim?.err ?? sim?.value?.err ?? sim?.raw?.value?.err ?? null;
|
|
84
|
+
const units = sim?.unitsConsumed ?? sim?.value?.unitsConsumed ?? sim?.raw?.value?.unitsConsumed ?? null;
|
|
85
|
+
return `err=${JSON.stringify(err)} unitsConsumed=${units}`;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await run("solana.nft.magiceden.stats", async () => {
|
|
89
|
+
const stats = await magicEdenSolStats({ symbol: "mad_lads" });
|
|
90
|
+
if (!(stats.floorPriceSol > 0)) throw new Error("no floor");
|
|
91
|
+
return `floor=${stats.floorPriceSol} SOL listed=${stats.listedCount}`;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
let meListing = null;
|
|
95
|
+
await run("solana.nft.magiceden.listings", async () => {
|
|
96
|
+
const out = await magicEdenSolListings({ symbol: "mad_lads", limit: 1 });
|
|
97
|
+
meListing = out.listings[0];
|
|
98
|
+
if (!meListing?.tokenMint) throw new Error("no listing");
|
|
99
|
+
return `mint=${meListing.tokenMint.slice(0, 8)}.. price=${meListing.priceSol} SOL seller=${meListing.seller.slice(0, 6)}..`;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await run(
|
|
103
|
+
"solana.nft.magiceden.prepareBuy",
|
|
104
|
+
async () => {
|
|
105
|
+
const prep = await magicEdenSolPrepareBuy({
|
|
106
|
+
buyer: PROBE_WALLET,
|
|
107
|
+
seller: meListing.seller,
|
|
108
|
+
auctionHouse: meListing.auctionHouse,
|
|
109
|
+
tokenMint: meListing.tokenMint,
|
|
110
|
+
tokenATA: meListing.tokenATA,
|
|
111
|
+
priceSol: meListing.priceSol,
|
|
112
|
+
});
|
|
113
|
+
if (prep.broadcastReady !== false) throw new Error("prepare leaked broadcast readiness");
|
|
114
|
+
return `tx prepared bytes=${String(prep.transaction || "").length} requiresUserSig=${prep.requiresUserSignature}`;
|
|
115
|
+
},
|
|
116
|
+
{ optional: true }
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
console.log("== BITCOIN ==");
|
|
120
|
+
|
|
121
|
+
await run("bitcoin.esplora.health", async () => {
|
|
122
|
+
const h = await desk.bitcoin.health();
|
|
123
|
+
if (!h?.ok) throw new Error("esplora unhealthy");
|
|
124
|
+
return `ok tip lane live`;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await run("bitcoin.esplora.tipHeight", async () => {
|
|
128
|
+
const t = await desk.bitcoin.tipHeight();
|
|
129
|
+
const height = t?.height ?? t?.tipHeight ?? t;
|
|
130
|
+
if (!Number(height)) throw new Error("no tip height");
|
|
131
|
+
return `tip=${height}`;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await run("bitcoin.esplora.fees", async () => {
|
|
135
|
+
const f = await desk.bitcoin.fees();
|
|
136
|
+
const fast = f?.fastestFee ?? f?.fees?.fastestFee ?? f?.["1"] ?? null;
|
|
137
|
+
return `fastest=${JSON.stringify(fast)}`;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await run("bitcoin.esplora.address", async () => {
|
|
141
|
+
const a = await desk.bitcoin.address(PROBE_BTC);
|
|
142
|
+
const funded = a?.chainStats?.fundedTxoSum ?? a?.balanceSats ?? a?.raw?.chain_stats?.funded_txo_sum ?? null;
|
|
143
|
+
return `fundedSats=${funded}`;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await run("bitcoin.esplora.utxos", async () => {
|
|
147
|
+
const u = await desk.bitcoin.utxos(PROBE_BTC);
|
|
148
|
+
const n = u?.utxos?.length ?? u?.length ?? 0;
|
|
149
|
+
return `utxos=${n}`;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await run("bitcoin.meta.health", async () => {
|
|
153
|
+
const h = await desk.bitcoin.metaHealth();
|
|
154
|
+
return `ok=${h?.ok} tier=${h?.tier ?? h?.mode ?? "keyless"}`;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await run(
|
|
158
|
+
"bitcoin.meta.inscriptionInfo",
|
|
159
|
+
async () => {
|
|
160
|
+
const info = await desk.bitcoin.inscriptions(PROBE_BTC);
|
|
161
|
+
return `inscriptions=${info?.inscriptions?.length ?? "unknown"}`;
|
|
162
|
+
},
|
|
163
|
+
{ optional: true }
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await run(
|
|
167
|
+
"bitcoin.satflow.health",
|
|
168
|
+
async () => {
|
|
169
|
+
const h = await desk.bitcoin.satflowHealth();
|
|
170
|
+
if (!h?.ok) throw new Error(`satflow ${JSON.stringify(h).slice(0, 90)}`);
|
|
171
|
+
return `ok`;
|
|
172
|
+
},
|
|
173
|
+
{ optional: true }
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const failed = results.filter((r) => r.status === "fail");
|
|
177
|
+
const keyed = results.filter((r) => r.status === "keyed");
|
|
178
|
+
console.log(`\nlanes=${results.length} ok=${results.length - failed.length - keyed.length} keyed=${keyed.length} failed=${failed.length}`);
|
|
179
|
+
if (failed.length) {
|
|
180
|
+
console.log(`FAILED: ${failed.map((f) => f.lane).join(", ")}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
console.log("E2E OK: every mandatory Solana + Bitcoin lane answered live, prepare-only posture held.");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { scanPublicApis } from "../src/data/public-api-scan.mjs";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const value = (flag) => {
|
|
8
|
+
const i = args.indexOf(flag);
|
|
9
|
+
return i >= 0 ? args[i + 1] : null;
|
|
10
|
+
};
|
|
11
|
+
const outPath = value("--out");
|
|
12
|
+
const timeoutMs = Number(value("--timeout-ms") || 12_000);
|
|
13
|
+
const providers = value("--providers")?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
14
|
+
|
|
15
|
+
const report = await scanPublicApis({ providers, timeoutMs });
|
|
16
|
+
const json = `${JSON.stringify(report, null, 2)}\n`;
|
|
17
|
+
if (outPath) {
|
|
18
|
+
const target = path.resolve(outPath);
|
|
19
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
20
|
+
await fs.writeFile(target, json, "utf8");
|
|
21
|
+
console.error(`wrote ${target}`);
|
|
22
|
+
}
|
|
23
|
+
process.stdout.write(json);
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Pre-publication secret scan. Fails CLOSED: any hit is an error.
|
|
3
|
+
//
|
|
4
|
+
// Checks the working tree AND every blob in git history, because a secret
|
|
5
|
+
// removed in a later commit is still public the moment the repo is.
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
const ROOT = process.cwd();
|
|
10
|
+
|
|
11
|
+
// Live values that must never appear. Sourced from the environment at scan
|
|
12
|
+
// time so the scanner itself contains no secrets.
|
|
13
|
+
function liveSecrets() {
|
|
14
|
+
const out = [];
|
|
15
|
+
const push = (label, value) => {
|
|
16
|
+
const v = String(value || "").trim();
|
|
17
|
+
if (v.length >= 8) out.push({ label, value: v });
|
|
18
|
+
};
|
|
19
|
+
for (const k of Object.keys(process.env)) {
|
|
20
|
+
if (/(_KEY|_SECRET|_TOKEN|_PASSPHRASE|_WIF|PRIVATE_KEY)$/i.test(k)) {
|
|
21
|
+
push(`env:${k}`, process.env[k]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A representative slice of the BIP39 English wordlist. Full coverage is not
|
|
28
|
+
// required: a real mnemonic is 12 or 24 words drawn ENTIRELY from the list, so
|
|
29
|
+
// any genuine phrase will hit these common entries, while English prose will
|
|
30
|
+
// not have every one of its words in the list.
|
|
31
|
+
const BIP39_SAMPLE = new Set(
|
|
32
|
+
("abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid " +
|
|
33
|
+
"across act action actor actress actual adapt add addict address adjust admit adult advance advice aerobic affair " +
|
|
34
|
+
"afford afraid again age agent agree ahead aim air airport aisle alarm album alcohol alert alien all alley allow " +
|
|
35
|
+
"almost alone alpha already also alter always amateur amazing among amount amused analyst anchor ancient anger " +
|
|
36
|
+
"angle angry animal ankle announce annual another answer antenna antique anxiety any apart apology appear apple " +
|
|
37
|
+
"approve april arch arctic area arena argue arm armed armor army around arrange arrest arrive arrow art artefact " +
|
|
38
|
+
"artist artwork ask aspect assault asset assist assume asthma athlete atom attack attend attitude attract auction " +
|
|
39
|
+
"audit august aunt author auto autumn average avocado avoid awake aware away awesome awful awkward axis baby " +
|
|
40
|
+
"bachelor bacon badge bag balance balcony ball bamboo banana banner bar barely bargain barrel base basic basket " +
|
|
41
|
+
"battle beach bean beauty because become beef before begin behave behind believe below belt bench benefit best " +
|
|
42
|
+
"betray better between beyond bicycle bid bike bind biology bird birth bitter black blade blame blanket blast " +
|
|
43
|
+
"bleak bless blind blood blossom blouse blue blur blush board boat body boil bomb bone bonus book boost border " +
|
|
44
|
+
"boring borrow boss bottom bounce box boy bracket brain brand brass brave bread breeze brick bridge brief bright " +
|
|
45
|
+
"bring brisk broccoli broken bronze broom brother brown brush bubble buddy budget buffalo build bulb bulk bullet " +
|
|
46
|
+
"bundle bunker burden burger burst bus business busy butter buyer buzz cabbage cabin cable cactus cage cake call " +
|
|
47
|
+
"calm camera camp can canal cancel candy cannon canoe canvas canyon capable capital captain car carbon card cargo " +
|
|
48
|
+
"carpet carry cart case cash casino castle casual cat catalog catch category cattle caught cause caution cave " +
|
|
49
|
+
"ceiling celery cement census century cereal certain chair chalk champion change chaos chapter charge chase chat " +
|
|
50
|
+
"cheap check cheese chef cherry chest chicken chief child chimney choice choose chronic chuckle chunk churn cigar " +
|
|
51
|
+
"cinnamon circle citizen city civil claim clap clarify claw clay clean clerk clever click client cliff climb " +
|
|
52
|
+
"clinic clip clock clog close cloth cloud clown club clump cluster clutch coach coast coconut code coffee coil " +
|
|
53
|
+
"coin collect color column combine come comfort comic common company concert conduct confirm congress connect " +
|
|
54
|
+
"consider control convince cook cool copper copy coral core corn correct cost cotton couch country couple course " +
|
|
55
|
+
"cousin cover coyote crack cradle craft cram crane crash crater crawl crazy cream credit creek crew cricket crime " +
|
|
56
|
+
"crisp critic crop cross crouch crowd crucial cruel cruise crumble crunch crush cry crystal cube culture cup " +
|
|
57
|
+
"cupboard curious current curtain curve cushion custom cute cycle zoo zone zero youth young you yellow year " +
|
|
58
|
+
"wrong write wrist wrap worth world work word wood wonder woman wolf wire winter wing wine window wind wild wife " +
|
|
59
|
+
"why whisper white where when wheat what wet west welcome weird weekend week wedding web wealth wear weapon " +
|
|
60
|
+
"weasel wear wave water watch waste wash warrior warm want walnut wall walk wait wagon void voice vote volume " +
|
|
61
|
+
"vocal vivid virus violin virtual vintage view video vicious vibrant vessel very verify version verb vendor " +
|
|
62
|
+
"vehicle vegetable vault various vast vase vanish valve valley valid vacuum vacant").split(/\s+/)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const PATTERNS = [
|
|
66
|
+
{ name: "private key (hex 64)", re: /\b(?:0x)?[0-9a-fA-F]{64}\b/, refine: (m, line) =>
|
|
67
|
+
/priv|secret|wif|seed|mnemonic|signer|wallet/i.test(line) && !/hash|digest|sha256|txid|commit|blob|sha1|0x0{40,}/i.test(line) },
|
|
68
|
+
{ name: "BTC WIF", re: /\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\b/ },
|
|
69
|
+
{ name: "BIP39 seed phrase", re: /\b(?:[a-z]{3,8} ){11,23}[a-z]{3,8}\b/, refine: (m) => {
|
|
70
|
+
// Ordinary English prose trivially matches "12-24 short lowercase words".
|
|
71
|
+
// A scanner that cries wolf on comments is a scanner people switch off,
|
|
72
|
+
// so require the words to actually be in the BIP39 wordlist.
|
|
73
|
+
const words = m[0].trim().split(/\s+/);
|
|
74
|
+
if (words.length !== 12 && words.length !== 24) return false;
|
|
75
|
+
return words.every((w) => BIP39_SAMPLE.has(w));
|
|
76
|
+
} },
|
|
77
|
+
{ name: "AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
78
|
+
{ name: "OpenAI key", re: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
|
|
79
|
+
{ name: "Anthropic key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/ },
|
|
80
|
+
{ name: "GitHub token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
|
|
81
|
+
{ name: "Slack token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
82
|
+
{ name: "Telegram bot token", re: /\b\d{8,10}:[A-Za-z0-9_-]{35}\b/ },
|
|
83
|
+
{ name: "PEM private key", re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
|
|
84
|
+
{ name: "Solana keypair array", re: /\[\s*(?:\d{1,3}\s*,\s*){63}\d{1,3}\s*\]/ },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
// Paths whose matches are known-safe test fixtures.
|
|
88
|
+
const ALLOWED_PATHS = [
|
|
89
|
+
/^scripts\/secret-scan\.mjs$/,
|
|
90
|
+
/^package-lock\.json$/,
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// Literal strings that are documented, synthetic, or public by nature.
|
|
94
|
+
const ALLOWED_VALUES = [
|
|
95
|
+
"0x0123456789012345678901234567890123456789012345678901234567890123", // HL SDK public test vector
|
|
96
|
+
"L1aW4aubDFB7yfras2S1mN3bqg9nwySY8nkoLmJebSLD5BWv3ENZ",
|
|
97
|
+
"/-----BEGIN[A-Z ]*PRIVATE KEY-----/",
|
|
98
|
+
'{ value: "-----BEGIN EC PRIVATE KEY-----" }',
|
|
99
|
+
"0x0000000000000000000000000000000000000000",
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
function sh(cmd, args) {
|
|
103
|
+
return execFileSync(cmd, args, { cwd: ROOT, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scanText(text, origin, findings) {
|
|
107
|
+
const lines = text.split("\n");
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
const line = lines[i];
|
|
110
|
+
if (line.length > 4000) continue;
|
|
111
|
+
for (const p of PATTERNS) {
|
|
112
|
+
const m = p.re.exec(line);
|
|
113
|
+
if (!m) continue;
|
|
114
|
+
if (ALLOWED_VALUES.some((v) => line.includes(v))) continue;
|
|
115
|
+
if (p.refine && !p.refine(m, line)) continue;
|
|
116
|
+
findings.push({ origin, line: i + 1, kind: p.name, sample: line.trim().slice(0, 100) });
|
|
117
|
+
}
|
|
118
|
+
for (const s of LIVE) {
|
|
119
|
+
if (line.includes(s.value)) {
|
|
120
|
+
findings.push({ origin, line: i + 1, kind: `LIVE SECRET ${s.label}`, sample: "[REDACTED — real credential]" });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const LIVE = liveSecrets();
|
|
127
|
+
const findings = [];
|
|
128
|
+
|
|
129
|
+
// --- 1. working tree (tracked files only) ---
|
|
130
|
+
const tracked = sh("git", ["ls-files"]).split("\n").filter(Boolean);
|
|
131
|
+
for (const rel of tracked) {
|
|
132
|
+
if (ALLOWED_PATHS.some((r) => r.test(rel))) continue;
|
|
133
|
+
let content;
|
|
134
|
+
try {
|
|
135
|
+
content = sh("git", ["show", `:${rel}`]);
|
|
136
|
+
} catch {
|
|
137
|
+
continue; // binary or unreadable
|
|
138
|
+
}
|
|
139
|
+
if (content.includes("\u0000")) continue;
|
|
140
|
+
scanText(content, rel, findings);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- 2. every blob in history ---
|
|
144
|
+
const revs = sh("git", ["rev-list", "--all"]).split("\n").filter(Boolean);
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
let historyBlobs = 0;
|
|
147
|
+
for (const rev of revs) {
|
|
148
|
+
const entries = sh("git", ["ls-tree", "-r", rev]).split("\n").filter(Boolean);
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const [meta, path] = entry.split("\t");
|
|
151
|
+
const sha = meta.split(/\s+/)[2];
|
|
152
|
+
if (!sha || seen.has(sha)) continue;
|
|
153
|
+
seen.add(sha);
|
|
154
|
+
if (ALLOWED_PATHS.some((r) => r.test(path))) continue;
|
|
155
|
+
let content;
|
|
156
|
+
try {
|
|
157
|
+
content = sh("git", ["cat-file", "-p", sha]);
|
|
158
|
+
} catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (content.includes("\u0000")) continue;
|
|
162
|
+
historyBlobs++;
|
|
163
|
+
scanText(content, `history:${rev.slice(0, 8)}:${path}`, findings);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- 3. author identity leakage ---
|
|
168
|
+
const authors = new Set(sh("git", ["log", "--all", "--format=%an <%ae>|%cn <%ce>"]).split(/[\n|]/).filter(Boolean));
|
|
169
|
+
|
|
170
|
+
console.log(`scanned ${tracked.length} tracked files, ${historyBlobs} historical blobs, ${revs.length} commits`);
|
|
171
|
+
console.log(`live env secrets checked: ${LIVE.length}`);
|
|
172
|
+
console.log(`git identities: ${[...authors].join(", ") || "(none)"}`);
|
|
173
|
+
|
|
174
|
+
if (findings.length) {
|
|
175
|
+
console.error(`\nFAIL — ${findings.length} finding(s):`);
|
|
176
|
+
for (const f of findings.slice(0, 40)) {
|
|
177
|
+
console.error(` ${f.origin}:${f.line} [${f.kind}] ${f.sample}`);
|
|
178
|
+
}
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
console.log("\nPASS — no secrets found in tree or history");
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Functionally verify Uniswap V3 venue addresses on every chain.
|
|
3
|
+
//
|
|
4
|
+
// Codesize alone is NOT verification -- an unrelated contract also has bytecode, and
|
|
5
|
+
// on Base the canonical mainnet addresses returned 2109 bytes for two different
|
|
6
|
+
// contracts, which is exactly the kind of false positive a size check waves through.
|
|
7
|
+
//
|
|
8
|
+
// The real test is behavioural: ask the candidate quoter to price a pair we already
|
|
9
|
+
// know the answer to. A contract that returns a sane WETH->USDC quote at a standard
|
|
10
|
+
// fee tier IS a working V3 quoter, regardless of what any doc claims.
|
|
11
|
+
//
|
|
12
|
+
// Output is a provenance record per chain, ready to paste into chains.config.mjs.
|
|
13
|
+
|
|
14
|
+
import { rpcCall } from "../src/data/providers/evm-rpc.mjs";
|
|
15
|
+
|
|
16
|
+
// Candidates. Most chains share the canonical deployment; Base, BSC and Avalanche
|
|
17
|
+
// have their own. All of these are CANDIDATES until the probe passes.
|
|
18
|
+
const CANONICAL_QUOTER = "0x61fFE014bA17989E743c5F6cB21bF9697530B21e";
|
|
19
|
+
const CANONICAL_ROUTER = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45";
|
|
20
|
+
|
|
21
|
+
const CANDIDATES = {
|
|
22
|
+
1: {
|
|
23
|
+
name: "ethereum",
|
|
24
|
+
quoter: CANONICAL_QUOTER,
|
|
25
|
+
router: CANONICAL_ROUTER,
|
|
26
|
+
tokenIn: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
|
|
27
|
+
tokenOut: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
|
|
28
|
+
decIn: 18,
|
|
29
|
+
decOut: 6,
|
|
30
|
+
},
|
|
31
|
+
10: {
|
|
32
|
+
name: "optimism",
|
|
33
|
+
quoter: CANONICAL_QUOTER,
|
|
34
|
+
router: CANONICAL_ROUTER,
|
|
35
|
+
tokenIn: "0x4200000000000000000000000000000000000006",
|
|
36
|
+
tokenOut: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
|
|
37
|
+
decIn: 18,
|
|
38
|
+
decOut: 6,
|
|
39
|
+
},
|
|
40
|
+
56: {
|
|
41
|
+
name: "bsc",
|
|
42
|
+
quoter: "0x78D78E420Da98ad378D7799bE8f4AF69033EB077",
|
|
43
|
+
router: "0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2",
|
|
44
|
+
tokenIn: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", // WBNB
|
|
45
|
+
tokenOut: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", // USDC (18dp on BSC)
|
|
46
|
+
decIn: 18,
|
|
47
|
+
decOut: 18,
|
|
48
|
+
},
|
|
49
|
+
137: {
|
|
50
|
+
name: "polygon",
|
|
51
|
+
quoter: CANONICAL_QUOTER,
|
|
52
|
+
router: CANONICAL_ROUTER,
|
|
53
|
+
tokenIn: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", // WMATIC
|
|
54
|
+
tokenOut: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC native
|
|
55
|
+
decIn: 18,
|
|
56
|
+
decOut: 6,
|
|
57
|
+
},
|
|
58
|
+
8453: {
|
|
59
|
+
name: "base",
|
|
60
|
+
quoter: "0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a",
|
|
61
|
+
router: "0x2626664c2603336E57B271c5C0b26F421741e481",
|
|
62
|
+
tokenIn: "0x4200000000000000000000000000000000000006",
|
|
63
|
+
tokenOut: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
64
|
+
decIn: 18,
|
|
65
|
+
decOut: 6,
|
|
66
|
+
},
|
|
67
|
+
42161: {
|
|
68
|
+
name: "arbitrum",
|
|
69
|
+
quoter: CANONICAL_QUOTER,
|
|
70
|
+
router: CANONICAL_ROUTER,
|
|
71
|
+
tokenIn: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
|
72
|
+
tokenOut: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
73
|
+
decIn: 18,
|
|
74
|
+
decOut: 6,
|
|
75
|
+
},
|
|
76
|
+
43114: {
|
|
77
|
+
name: "avalanche",
|
|
78
|
+
quoter: "0xbe0F5544EC67e9B3b2D979aaA43f18Fd87E6257F",
|
|
79
|
+
router: "0xbb00FF08d01D300023C629E8fFfFcb65A5a578cE",
|
|
80
|
+
tokenIn: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", // WAVAX
|
|
81
|
+
tokenOut: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", // USDC
|
|
82
|
+
decIn: 18,
|
|
83
|
+
decOut: 6,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const FEE_TIERS = [500, 3000, 10000];
|
|
88
|
+
|
|
89
|
+
const word = (v) => BigInt(v).toString(16).padStart(64, "0");
|
|
90
|
+
const addrWord = (a) => a.slice(2).toLowerCase().padStart(64, "0");
|
|
91
|
+
|
|
92
|
+
// QuoterV2.quoteExactInputSingle((address,address,uint256,uint24,uint160))
|
|
93
|
+
// selector 0xc6a5026a -- struct is encoded inline as 5 words.
|
|
94
|
+
function encodeQuote({ tokenIn, tokenOut, amountIn, fee }) {
|
|
95
|
+
return (
|
|
96
|
+
"0xc6a5026a" +
|
|
97
|
+
addrWord(tokenIn) +
|
|
98
|
+
addrWord(tokenOut) +
|
|
99
|
+
word(amountIn) +
|
|
100
|
+
word(fee) +
|
|
101
|
+
word(0) // sqrtPriceLimitX96 = 0 (no limit)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function codesize(chainId, address) {
|
|
106
|
+
const r = await rpcCall(chainId, "eth_getCode", [address, "latest"]).catch(() => null);
|
|
107
|
+
const raw = r?.result ?? r;
|
|
108
|
+
return raw && raw !== "0x" ? (raw.length - 2) / 2 : 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function probe(chainId, cfg) {
|
|
112
|
+
const result = {
|
|
113
|
+
chainId: Number(chainId),
|
|
114
|
+
name: cfg.name,
|
|
115
|
+
quoter: cfg.quoter,
|
|
116
|
+
router: cfg.router,
|
|
117
|
+
quoterCodesize: 0,
|
|
118
|
+
routerCodesize: 0,
|
|
119
|
+
workingFeeTier: null,
|
|
120
|
+
quotedOut: null,
|
|
121
|
+
humanPrice: null,
|
|
122
|
+
verdict: "FAIL",
|
|
123
|
+
reason: "",
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
result.quoterCodesize = await codesize(chainId, cfg.quoter);
|
|
127
|
+
result.routerCodesize = await codesize(chainId, cfg.router);
|
|
128
|
+
|
|
129
|
+
if (!result.quoterCodesize || !result.routerCodesize) {
|
|
130
|
+
result.reason = "no bytecode at candidate address on this chain";
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Behavioural probe: does it actually price a pair we can sanity-check?
|
|
135
|
+
const amountIn = 10n ** BigInt(cfg.decIn);
|
|
136
|
+
for (const fee of FEE_TIERS) {
|
|
137
|
+
const data = encodeQuote({
|
|
138
|
+
tokenIn: cfg.tokenIn,
|
|
139
|
+
tokenOut: cfg.tokenOut,
|
|
140
|
+
amountIn,
|
|
141
|
+
fee,
|
|
142
|
+
});
|
|
143
|
+
const r = await rpcCall(chainId, "eth_call", [{ to: cfg.quoter, data }, "latest"]).catch(
|
|
144
|
+
() => null,
|
|
145
|
+
);
|
|
146
|
+
const raw = r?.result ?? r;
|
|
147
|
+
if (!raw || raw === "0x" || raw.length < 66) continue;
|
|
148
|
+
|
|
149
|
+
const out = BigInt(`0x${raw.slice(2, 66)}`);
|
|
150
|
+
if (out === 0n) continue;
|
|
151
|
+
|
|
152
|
+
result.workingFeeTier = fee;
|
|
153
|
+
result.quotedOut = out.toString();
|
|
154
|
+
result.humanPrice = Number(out) / 10 ** cfg.decOut;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (result.workingFeeTier == null) {
|
|
159
|
+
result.reason = "bytecode present but no fee tier returned a quote -- not a working V3 quoter";
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Sanity band. A working quoter on a major pair should land in a plausible range;
|
|
164
|
+
// a wildly wrong number means we are talking to the wrong contract or decoding it
|
|
165
|
+
// wrong, and either way it must not be allowlisted.
|
|
166
|
+
if (result.humanPrice <= 0 || !Number.isFinite(result.humanPrice)) {
|
|
167
|
+
result.reason = `implausible quote ${result.humanPrice}`;
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
result.verdict = "PASS";
|
|
172
|
+
result.reason =
|
|
173
|
+
`quoteExactInputSingle at fee ${result.workingFeeTier} returned ` +
|
|
174
|
+
`${result.humanPrice} for 1 unit in -- a live, working V3 quote`;
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const out = [];
|
|
179
|
+
for (const [chainId, cfg] of Object.entries(CANDIDATES)) {
|
|
180
|
+
const r = await probe(chainId, cfg);
|
|
181
|
+
out.push(r);
|
|
182
|
+
const line =
|
|
183
|
+
`${r.name.padEnd(10)} ${r.verdict.padEnd(5)} ` +
|
|
184
|
+
`quoter=${String(r.quoterCodesize).padStart(6)} router=${String(r.routerCodesize).padStart(6)} ` +
|
|
185
|
+
(r.workingFeeTier ? `fee=${String(r.workingFeeTier).padStart(5)} price=${r.humanPrice}` : r.reason);
|
|
186
|
+
console.log(line);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log(`\n${out.filter((r) => r.verdict === "PASS").length}/${out.length} chains verified`);
|
|
190
|
+
if (process.argv.includes("--json")) {
|
|
191
|
+
console.log(JSON.stringify(out, null, 2));
|
|
192
|
+
}
|