@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,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// oracle-route -- compare swap and bridge routes across every available source.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// oracle-route swap <chain> <tokenIn> <tokenOut> [amountIn]
|
|
6
|
+
// oracle-route bridge <fromChain> <toChain> <token> [amountIn]
|
|
7
|
+
//
|
|
8
|
+
// <chain> is an id (8453) or key (base). --json for machine output.
|
|
9
|
+
|
|
10
|
+
import { bestSwapRoute, bestBridgeRoute } from "../src/router/index.mjs";
|
|
11
|
+
import { prepareBestRoute } from "../src/router/prepare-route.mjs";
|
|
12
|
+
import { prepareBestBridgeRoute } from "../src/router/prepare-bridge.mjs";
|
|
13
|
+
import { registerBuiltinScanners } from "../src/scanner/chains.config.mjs";
|
|
14
|
+
import { getScanner, listScanners } from "../src/scanner/contract.mjs";
|
|
15
|
+
|
|
16
|
+
registerBuiltinScanners();
|
|
17
|
+
|
|
18
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
19
|
+
const JSON_OUT = rest.includes("--json");
|
|
20
|
+
const args = rest.filter((a) => a !== "--json");
|
|
21
|
+
const NATIVE = "0x0000000000000000000000000000000000000000";
|
|
22
|
+
|
|
23
|
+
function chainId(ref) {
|
|
24
|
+
if (!ref) die("missing <chain>");
|
|
25
|
+
const n = Number(ref);
|
|
26
|
+
if (getScanner(n)) return n;
|
|
27
|
+
const s = listScanners().find((x) => x.key === String(ref).toLowerCase());
|
|
28
|
+
if (s) return s.chainId;
|
|
29
|
+
die(`unknown chain "${ref}". Known: ${listScanners().map((x) => x.key).join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function die(msg) {
|
|
33
|
+
console.error(`error: ${msg}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fmt(raw, decimals) {
|
|
38
|
+
const n = Number(raw) / 10 ** decimals;
|
|
39
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 6 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function render(r, decimals) {
|
|
43
|
+
console.log(`sources: ${r.sourcesAnswered}/${r.sourcesTried} answered`);
|
|
44
|
+
console.log(`ranked on: ${r.rankedOn}\n`);
|
|
45
|
+
|
|
46
|
+
if (!r.routes.length) {
|
|
47
|
+
console.log("no source returned a usable route.");
|
|
48
|
+
for (const f of r.failed) console.log(` ${f.source}: ${f.error}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const w = Math.max(...r.routes.map((x) => x.source.length), 6);
|
|
53
|
+
console.log(` ${"source".padEnd(w)} ${"net out".padStart(14)} ${"gross".padStart(14)} cost`);
|
|
54
|
+
for (const [i, x] of r.routes.entries()) {
|
|
55
|
+
const mark = i === 0 ? "*" : " ";
|
|
56
|
+
const cost =
|
|
57
|
+
x.gasUsd == null
|
|
58
|
+
? "gas unknown"
|
|
59
|
+
: x.gasUsd === 0
|
|
60
|
+
? "gasless (solver)"
|
|
61
|
+
: `$${Number(x.gasUsd + (x.feeUsd ?? 0)).toFixed(2)}`;
|
|
62
|
+
const dur = x.meta?.durationSeconds != null ? ` ~${x.meta.durationSeconds}s` : "";
|
|
63
|
+
console.log(
|
|
64
|
+
`${mark} ${x.source.padEnd(w)} ${fmt(x.netOut, decimals).padStart(14)} ` +
|
|
65
|
+
`${fmt(x.grossOut, decimals).padStart(14)} ${cost}${dur}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const f of r.failed) console.log(` ${f.source.padEnd(w)} FAILED: ${String(f.error).slice(0, 60)}`);
|
|
70
|
+
|
|
71
|
+
console.log(`\n${r.note}`);
|
|
72
|
+
for (const wn of r.warnings) console.log(`\nWARNING: ${wn}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
switch (cmd) {
|
|
77
|
+
case "swap": {
|
|
78
|
+
const cid = chainId(args[0]);
|
|
79
|
+
if (!args[1] || !args[2]) die("usage: oracle-route swap <chain> <tokenIn> <tokenOut> [amountIn]");
|
|
80
|
+
const amountIn = args[3] || (10n ** 18n).toString();
|
|
81
|
+
const scanner = getScanner(cid);
|
|
82
|
+
|
|
83
|
+
// Decimals matter twice: ParaSwap needs them to quote at all, and the ranker
|
|
84
|
+
// needs the destination's to convert gas into output units.
|
|
85
|
+
const [dIn, dOut] = await Promise.all([
|
|
86
|
+
scanner.resolveToken(args[1]).then((t) => t.decimals).catch(() => null),
|
|
87
|
+
scanner.resolveToken(args[2]).then((t) => t.decimals).catch(() => null),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const r = await bestSwapRoute({
|
|
91
|
+
chainId: cid,
|
|
92
|
+
tokenIn: args[1],
|
|
93
|
+
tokenOut: args[2],
|
|
94
|
+
amountIn,
|
|
95
|
+
taker: args[4] || "0x000000000000000000000000000000000000dEaD",
|
|
96
|
+
decimalsIn: dIn,
|
|
97
|
+
decimalsOut: dOut,
|
|
98
|
+
});
|
|
99
|
+
if (JSON_OUT) process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
|
|
100
|
+
else render(r, dOut ?? 18);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case "bridge": {
|
|
105
|
+
const from = chainId(args[0]);
|
|
106
|
+
const to = chainId(args[1]);
|
|
107
|
+
const token = args[2] || NATIVE;
|
|
108
|
+
const amountIn = args[3] || (10n ** 18n).toString();
|
|
109
|
+
|
|
110
|
+
const r = await bestBridgeRoute({
|
|
111
|
+
fromChainId: from,
|
|
112
|
+
toChainId: to,
|
|
113
|
+
tokenIn: token,
|
|
114
|
+
tokenOut: token,
|
|
115
|
+
amountIn,
|
|
116
|
+
taker: args[4] || "0x000000000000000000000000000000000000dEaD",
|
|
117
|
+
decimalsOut: 18,
|
|
118
|
+
});
|
|
119
|
+
if (JSON_OUT) process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
|
|
120
|
+
else render(r, 18);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case "prepare": {
|
|
125
|
+
const cid = chainId(args[0]);
|
|
126
|
+
if (!args[1] || !args[2] || !args[3]) {
|
|
127
|
+
die("usage: oracle-route prepare <chain> <tokenIn> <tokenOut> <taker> [amountIn] [--source=X]");
|
|
128
|
+
}
|
|
129
|
+
// Argument order puts taker BEFORE amount deliberately: preparing without a
|
|
130
|
+
// real taker is the one mistake that cannot be recovered from downstream.
|
|
131
|
+
const taker = args[3];
|
|
132
|
+
const amountIn = args[4] || (10n ** 18n).toString();
|
|
133
|
+
const srcArg = rest.find((a) => a.startsWith("--source="));
|
|
134
|
+
const scanner = getScanner(cid);
|
|
135
|
+
|
|
136
|
+
const [dIn, dOut] = await Promise.all([
|
|
137
|
+
scanner.resolveToken(args[1]).then((t) => t.decimals).catch(() => null),
|
|
138
|
+
scanner.resolveToken(args[2]).then((t) => t.decimals).catch(() => null),
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const r = await prepareBestRoute({
|
|
142
|
+
chainId: cid,
|
|
143
|
+
tokenIn: args[1],
|
|
144
|
+
tokenOut: args[2],
|
|
145
|
+
amountIn,
|
|
146
|
+
taker,
|
|
147
|
+
decimalsIn: dIn,
|
|
148
|
+
decimalsOut: dOut,
|
|
149
|
+
source: srcArg ? srcArg.split("=")[1] : undefined,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (JSON_OUT) {
|
|
153
|
+
process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!r.ok) {
|
|
158
|
+
console.log(`could not prepare: ${r.reason}`);
|
|
159
|
+
if (r.failureKind) console.log(`kind: ${r.failureKind}`);
|
|
160
|
+
if (r.alternative) console.log(`try: --source=${r.alternative}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(`source: ${r.chosen.source}${r.chosen.wasWinner ? " (winner)" : " (override -- NOT the winner)"}`);
|
|
165
|
+
console.log(`artifact: ${r.artifactKind}`);
|
|
166
|
+
console.log(`destination: ${r.destination}`);
|
|
167
|
+
if (r.minOut) console.log(`minOut: ${r.minOut}`);
|
|
168
|
+
if (r.driftBps != null) console.log(`drift: ${(r.driftBps / 100).toFixed(3)}% vs comparison`);
|
|
169
|
+
if (r.requiresApproval) {
|
|
170
|
+
console.log(`\napproval required first:`);
|
|
171
|
+
console.log(` token ${r.requiresApproval.token}`);
|
|
172
|
+
console.log(` spender ${r.requiresApproval.spender}`);
|
|
173
|
+
if (r.requiresApproval.note) console.log(` note ${r.requiresApproval.note}`);
|
|
174
|
+
}
|
|
175
|
+
if (r.runnersUp?.length) {
|
|
176
|
+
console.log(`\nrunners-up: ${r.runnersUp.map((x) => x.source).join(", ")}`);
|
|
177
|
+
}
|
|
178
|
+
for (const w of r.warnings) console.log(`\nWARNING: ${w}`);
|
|
179
|
+
console.log(`\n${r.note}`);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
case "prepare-bridge": {
|
|
184
|
+
const from = chainId(args[0]);
|
|
185
|
+
const to = chainId(args[1]);
|
|
186
|
+
if (!args[2]) die("usage: oracle-route prepare-bridge <fromChain> <toChain> <taker> [token] [amountIn] [--source=X]");
|
|
187
|
+
const taker = args[2];
|
|
188
|
+
const token = args[3] || NATIVE;
|
|
189
|
+
const amountIn = args[4] || (10n ** 17n).toString();
|
|
190
|
+
const srcArg = rest.find((a) => a.startsWith("--source="));
|
|
191
|
+
|
|
192
|
+
const r = await prepareBestBridgeRoute({
|
|
193
|
+
fromChainId: from,
|
|
194
|
+
toChainId: to,
|
|
195
|
+
tokenIn: token,
|
|
196
|
+
tokenOut: token,
|
|
197
|
+
amountIn,
|
|
198
|
+
taker,
|
|
199
|
+
decimalsOut: 18,
|
|
200
|
+
source: srcArg ? srcArg.split("=")[1] : undefined,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (JSON_OUT) {
|
|
204
|
+
process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
if (!r.ok) {
|
|
208
|
+
console.log(`could not prepare: ${r.reason}`);
|
|
209
|
+
if (r.failureKind) console.log(`kind: ${r.failureKind}`);
|
|
210
|
+
if (r.alternative) console.log(`try: --source=${r.alternative}`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log(`source: ${r.chosen.source}${r.chosen.wasWinner ? " (winner)" : " (override -- NOT the winner)"}`);
|
|
215
|
+
console.log(`artifact: ${r.artifactKind}`);
|
|
216
|
+
console.log(`route: chain ${r.fromChainId} -> chain ${r.toChainId}`);
|
|
217
|
+
console.log(`signs: ${r.transactionCount} transaction(s) on chain ${r.fromChainId}`);
|
|
218
|
+
console.log(`destination: ${r.destination}`);
|
|
219
|
+
if (r.minOut) console.log(`minOut: ${r.minOut}`);
|
|
220
|
+
if (r.durationSeconds != null) console.log(`eta: ~${r.durationSeconds}s to credit`);
|
|
221
|
+
if (r.driftBps != null) console.log(`drift: ${(r.driftBps / 100).toFixed(3)}% vs comparison`);
|
|
222
|
+
if (r.requiresApproval) {
|
|
223
|
+
console.log(`\napproval required first:`);
|
|
224
|
+
console.log(` token ${r.requiresApproval.token}`);
|
|
225
|
+
console.log(` spender ${r.requiresApproval.spender}`);
|
|
226
|
+
}
|
|
227
|
+
r.transactions.forEach((t, i) => {
|
|
228
|
+
console.log(`\ntx ${i + 1}/${r.transactionCount}: chain ${t.chainId} -> ${t.to}`);
|
|
229
|
+
});
|
|
230
|
+
if (r.runnersUp?.length) console.log(`\nrunners-up: ${r.runnersUp.map((x) => x.source).join(", ")}`);
|
|
231
|
+
for (const w of r.warnings) console.log(`\nWARNING: ${w}`);
|
|
232
|
+
console.log(`\n${r.note}`);
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
default:
|
|
237
|
+
console.log(
|
|
238
|
+
[
|
|
239
|
+
"oracle-route -- best-execution routing",
|
|
240
|
+
"",
|
|
241
|
+
" oracle-route swap <chain> <tokenIn> <tokenOut> [amountIn] [taker]",
|
|
242
|
+
" oracle-route bridge <fromChain> <toChain> [token] [amountIn] [taker]",
|
|
243
|
+
" oracle-route prepare <chain> <tokenIn> <tokenOut> <taker> [amountIn] [--source=X]",
|
|
244
|
+
" oracle-route prepare-bridge <fromChain> <toChain> <taker> [token] [amt] [--source=X]",
|
|
245
|
+
"",
|
|
246
|
+
" Ranks by NET output (gross minus gas and fees), not headline quote.",
|
|
247
|
+
" A route quoting more but costing more gas can lose. --json for detail.",
|
|
248
|
+
].join("\n"),
|
|
249
|
+
);
|
|
250
|
+
process.exit(cmd ? 1 : 0);
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
die(String(err?.message || err));
|
|
254
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// oracle-scan -- exercise the chain-scanner framework from the shell.
|
|
3
|
+
//
|
|
4
|
+
// Useful for two things: checking a chain is actually reachable with your RPC
|
|
5
|
+
// config, and researching a token without wiring up an agent first.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// oracle-scan chains coverage matrix for every chain
|
|
9
|
+
// oracle-scan head <chain> current block
|
|
10
|
+
// oracle-scan token <chain> <address> on-chain ERC-20 identity
|
|
11
|
+
// oracle-scan pools <chain> <address> tradeable pools by liquidity
|
|
12
|
+
// oracle-scan risk <chain> <address> structural risk checks
|
|
13
|
+
//
|
|
14
|
+
// <chain> is a chain id (8453) or key (base).
|
|
15
|
+
|
|
16
|
+
import { registerBuiltinScanners } from "../src/scanner/chains.config.mjs";
|
|
17
|
+
import { getScanner, listScanners, scannerCoverage } from "../src/scanner/contract.mjs";
|
|
18
|
+
|
|
19
|
+
registerBuiltinScanners();
|
|
20
|
+
|
|
21
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
22
|
+
const JSON_OUT = rest.includes("--json");
|
|
23
|
+
const args = rest.filter((a) => a !== "--json");
|
|
24
|
+
|
|
25
|
+
function resolve(ref) {
|
|
26
|
+
if (!ref) die("missing <chain>");
|
|
27
|
+
const byId = getScanner(Number(ref));
|
|
28
|
+
if (byId) return byId;
|
|
29
|
+
const byKey = listScanners().find((s) => s.key === String(ref).toLowerCase());
|
|
30
|
+
if (byKey) return byKey;
|
|
31
|
+
die(
|
|
32
|
+
`unknown chain "${ref}". Known: ${listScanners().map((s) => `${s.key}(${s.chainId})`).join(", ")}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function die(msg) {
|
|
37
|
+
console.error(`error: ${msg}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function out(obj) {
|
|
42
|
+
process.stdout.write(`${JSON.stringify(obj, null, 2)}\n`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A missing RPC is the most common reason a scan fails. Say so precisely instead of
|
|
46
|
+
// surfacing a generic fetch error.
|
|
47
|
+
function rpcHint(scanner, err) {
|
|
48
|
+
const msg = String(err?.message || err);
|
|
49
|
+
if (/rpc|fetch|ECONN|timeout|provider/i.test(msg)) {
|
|
50
|
+
return (
|
|
51
|
+
`${msg}\n\nThis chain reads its RPC from: ${scanner.rpcEnv.join(" or ")}\n` +
|
|
52
|
+
`Set one, e.g.: export ${scanner.rpcEnv[0]}=https://...`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return msg;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
switch (cmd) {
|
|
60
|
+
case "chains": {
|
|
61
|
+
const cov = scannerCoverage();
|
|
62
|
+
if (JSON_OUT) {
|
|
63
|
+
out(cov);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
console.log(`${cov.chainCount} chains registered\n`);
|
|
67
|
+
const rows = Object.entries(cov.chains).map(([id, c]) => ({
|
|
68
|
+
chain: `${c.key} (${id})`,
|
|
69
|
+
caps: c.supported.length,
|
|
70
|
+
routing: c.failClosed ? "fail-closed (no verified venue)" : `${c.venueCount} venue(s)`,
|
|
71
|
+
}));
|
|
72
|
+
const w = Math.max(...rows.map((r) => r.chain.length));
|
|
73
|
+
for (const r of rows) {
|
|
74
|
+
console.log(` ${r.chain.padEnd(w)} ${String(r.caps).padStart(2)} caps ${r.routing}`);
|
|
75
|
+
}
|
|
76
|
+
console.log(
|
|
77
|
+
"\nEvery chain is read/research capable. Routing value requires a verified\n" +
|
|
78
|
+
"venue -- see CONTRIBUTING.md for the verification rule.",
|
|
79
|
+
);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case "head": {
|
|
84
|
+
const s = resolve(args[0]);
|
|
85
|
+
out(await s.blockNumber());
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case "token": {
|
|
90
|
+
const s = resolve(args[0]);
|
|
91
|
+
if (!args[1]) die("usage: oracle-scan token <chain> <address>");
|
|
92
|
+
out(await s.resolveToken(args[1]));
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case "pools": {
|
|
97
|
+
const s = resolve(args[0]);
|
|
98
|
+
if (!args[1]) die("usage: oracle-scan pools <chain> <address>");
|
|
99
|
+
const r = await s.resolvePools(args[1]);
|
|
100
|
+
if (JSON_OUT) {
|
|
101
|
+
out(r);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
console.log(`evidence: ${r.evidence}`);
|
|
105
|
+
if (r.reason) console.log(`reason: ${r.reason}`);
|
|
106
|
+
for (const p of r.pools.slice(0, 10)) {
|
|
107
|
+
const liq = p.liquidityUsd == null ? "?" : `$${Math.round(p.liquidityUsd).toLocaleString()}`;
|
|
108
|
+
console.log(` ${p.dex.padEnd(12)} ${liq.padStart(14)} ${p.pair}`);
|
|
109
|
+
}
|
|
110
|
+
if (!r.pools.length) console.log(" (no pools with liquidity found)");
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case "risk": {
|
|
115
|
+
const s = resolve(args[0]);
|
|
116
|
+
if (!args[1]) die("usage: oracle-scan risk <chain> <address>");
|
|
117
|
+
const r = await s.scoreRisk(args[1]);
|
|
118
|
+
if (JSON_OUT) {
|
|
119
|
+
out(r);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
console.log(`verdict: ${r.verdict}\n`);
|
|
123
|
+
for (const c of r.checks) {
|
|
124
|
+
console.log(` ${c.result.padEnd(8)} ${c.check.padEnd(16)} ${c.detail ?? ""}`);
|
|
125
|
+
}
|
|
126
|
+
console.log(`\n${r.note}`);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case "quote": {
|
|
131
|
+
const s = resolve(args[0]);
|
|
132
|
+
if (!args[1] || !args[2]) die("usage: oracle-scan quote <chain> <tokenIn> <tokenOut> [amountIn]");
|
|
133
|
+
if (!s.supports("quote")) {
|
|
134
|
+
die(
|
|
135
|
+
`${s.key} has no verified router, so quoting stays fail-closed.\n` +
|
|
136
|
+
"Add a venue with recorded provenance -- see docs/adding-a-chain.md.",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const amountIn = args[3] || (10n ** 18n).toString();
|
|
140
|
+
out(await s.quote({ tokenIn: args[1], tokenOut: args[2], amountIn }));
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case "sell": {
|
|
145
|
+
// The single most valuable check on a low-cap venue: can you get OUT?
|
|
146
|
+
const s = resolve(args[0]);
|
|
147
|
+
if (!args[1]) die("usage: oracle-scan sell <chain> <token> [amountIn]");
|
|
148
|
+
if (!s.supports("sellSimulation")) {
|
|
149
|
+
die(
|
|
150
|
+
`${s.key} has no verified router, so sell simulation is unavailable.\n` +
|
|
151
|
+
"Without it, treat every token on this chain as unproven for exit.",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const amountIn = args[2] || (10n ** 16n).toString();
|
|
155
|
+
const r = await s.sellSimulation({ token: args[1], amountIn });
|
|
156
|
+
if (JSON_OUT) {
|
|
157
|
+
out(r);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
console.log(`verdict: ${r.verdict}`);
|
|
161
|
+
console.log(`reason: ${r.reason ?? ""}`);
|
|
162
|
+
if (r.retentionBps != null) {
|
|
163
|
+
console.log(`retention: ${(r.retentionBps / 100).toFixed(2)}%`);
|
|
164
|
+
}
|
|
165
|
+
if (r.leg) console.log(`failed at: ${r.leg} leg`);
|
|
166
|
+
if (r.note) console.log(`\n${r.note}`);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
default:
|
|
171
|
+
console.log(
|
|
172
|
+
[
|
|
173
|
+
"oracle-scan -- chain scanner CLI",
|
|
174
|
+
"",
|
|
175
|
+
" oracle-scan chains coverage matrix",
|
|
176
|
+
" oracle-scan head <chain> current block",
|
|
177
|
+
" oracle-scan token <chain> <address> ERC-20 identity",
|
|
178
|
+
" oracle-scan pools <chain> <address> pools by liquidity",
|
|
179
|
+
" oracle-scan risk <chain> <address> structural risk checks",
|
|
180
|
+
" oracle-scan quote <chain> <tIn> <tOut> [amt] live exact-input quote",
|
|
181
|
+
" oracle-scan sell <chain> <token> [amt] round-trip: can you exit?",
|
|
182
|
+
"",
|
|
183
|
+
" <chain> is an id (8453) or key (base). --json for machine output.",
|
|
184
|
+
" quote/sell need a chain with a verified router (see: oracle-scan chains).",
|
|
185
|
+
].join("\n"),
|
|
186
|
+
);
|
|
187
|
+
process.exit(cmd ? 1 : 0);
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
const s = args[0] ? (getScanner(Number(args[0])) || listScanners().find((x) => x.key === args[0])) : null;
|
|
191
|
+
die(s ? rpcHint(s, err) : String(err?.message || err));
|
|
192
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Adding a chain
|
|
2
|
+
|
|
3
|
+
Oracle treats a chain as **configuration**. You do not write an integration.
|
|
4
|
+
|
|
5
|
+
## The short version
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { registerCustomChain } from "@oracle-agent/oracle/scanner";
|
|
9
|
+
|
|
10
|
+
registerCustomChain({
|
|
11
|
+
key: "mychain",
|
|
12
|
+
chainId: 7777,
|
|
13
|
+
name: "My Chain",
|
|
14
|
+
rpcEnv: ["MYCHAIN_RPC_URL"],
|
|
15
|
+
nativeCurrency: { symbol: "MYC", decimals: 18 },
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That chain now has block reads, native and ERC-20 balances, on-chain token
|
|
20
|
+
resolution, log scanning, and structural risk checks. No code was written.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
export MYCHAIN_RPC_URL=https://rpc.mychain.example
|
|
24
|
+
oracle-scan head mychain
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Config fields
|
|
28
|
+
|
|
29
|
+
| Field | Required | Notes |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `key` | yes | lowercase slug, used on the CLI |
|
|
32
|
+
| `chainId` | yes | positive integer |
|
|
33
|
+
| `name` | yes | display name |
|
|
34
|
+
| `rpcEnv` | yes | env var names, `UPPER_SNAKE`. Never a literal URL |
|
|
35
|
+
| `nativeCurrency` | no | `{ symbol, decimals }` |
|
|
36
|
+
| `explorer` | no | base URL |
|
|
37
|
+
| `dexscreenerSlug` | no | enables pool discovery — see below |
|
|
38
|
+
| `venues` | no | verified routers/quoters — required for routing value |
|
|
39
|
+
|
|
40
|
+
**RPC URLs live in the environment, never in config.** A repo carrying an endpoint
|
|
41
|
+
is both a leak and a rate-limit problem for whoever else uses that key.
|
|
42
|
+
|
|
43
|
+
## Why `dexscreenerSlug` matters
|
|
44
|
+
|
|
45
|
+
Pool discovery goes through market data rather than walking a factory. A factory
|
|
46
|
+
walk finds pools that *exist*; a trader needs pools with *liquidity*.
|
|
47
|
+
|
|
48
|
+
The slug is DexScreener's own chain identifier — not the chain id, and not always
|
|
49
|
+
the obvious name. If you don't know it, **leave it out**. Pool discovery then
|
|
50
|
+
reports `UNAVAILABLE`, which is honest. A wrong slug silently returns another
|
|
51
|
+
chain's pools, which is worse than no answer.
|
|
52
|
+
|
|
53
|
+
## Adding venues (routing value)
|
|
54
|
+
|
|
55
|
+
A chain with no verified venue is **fail-closed**: read and research work, routing
|
|
56
|
+
value does not. That is a safe default, not a gap.
|
|
57
|
+
|
|
58
|
+
To enable routing you must record *provenance*:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
venues: [
|
|
62
|
+
{
|
|
63
|
+
kind: "router",
|
|
64
|
+
address: "0x...",
|
|
65
|
+
verified: {
|
|
66
|
+
method: "eth_getCode + protocol /chains API",
|
|
67
|
+
source: "https://li.quest/v1/chains",
|
|
68
|
+
date: "2026-07-30",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Validation **refuses** a venue without `verified`. This is not bureaucracy: the
|
|
75
|
+
destination allowlist is the highest-leverage control in the system, and
|
|
76
|
+
allowlisting a spoofed router makes every other guard irrelevant.
|
|
77
|
+
|
|
78
|
+
### How to actually verify
|
|
79
|
+
|
|
80
|
+
1. **Bytecode exists** — `eth_getCode` must return real code. `0x` means nothing is
|
|
81
|
+
deployed at that address on that chain.
|
|
82
|
+
2. **The protocol's own API or docs** names that address, for that chain, today.
|
|
83
|
+
Query the source (e.g. LI.FI's `/v1/chains` returns each chain's official
|
|
84
|
+
`diamondAddress`) rather than trusting a blog post or a model's recollection.
|
|
85
|
+
3. **Constructor-bound addresses match** — read `factory()`, `WETH9()` back off the
|
|
86
|
+
deployed contract and confirm they're what you expect.
|
|
87
|
+
4. **Per chain, separately.** An address verified on Arbitrum is not verified on
|
|
88
|
+
Ethereum, even when the canonical deployment shares an address across chains.
|
|
89
|
+
|
|
90
|
+
Record how and when. The audit trail has to outlive your session.
|
|
91
|
+
|
|
92
|
+
## Capabilities
|
|
93
|
+
|
|
94
|
+
Ten capabilities exist. The generic EVM scanner provides seven out of the box:
|
|
95
|
+
|
|
96
|
+
| Capability | Generic? | Why |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `blockNumber` | yes | standard RPC |
|
|
99
|
+
| `nativeBalance` | yes | standard RPC |
|
|
100
|
+
| `tokenBalance` | yes | ERC-20 is ERC-20 |
|
|
101
|
+
| `resolveToken` | yes | reads metadata off the contract |
|
|
102
|
+
| `resolvePools` | yes* | needs `dexscreenerSlug` |
|
|
103
|
+
| `scanBlocks` | yes | chunked `eth_getLogs` |
|
|
104
|
+
| `scoreRisk` | yes | structural checks |
|
|
105
|
+
| `quote` | venue | needs a verified router |
|
|
106
|
+
| `sellSimulation` | venue | needs a verified router |
|
|
107
|
+
| `prepareUnsignedTx` | venue | needs a verified router |
|
|
108
|
+
|
|
109
|
+
Calling an unimplemented capability **throws**, naming what is supported. It never
|
|
110
|
+
returns `undefined` that a caller could mistake for "no result."
|
|
111
|
+
|
|
112
|
+
## Enabling quote / sell / prepare
|
|
113
|
+
|
|
114
|
+
Add a verified router plus two fields:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
{
|
|
118
|
+
key: "base",
|
|
119
|
+
chainId: 8453,
|
|
120
|
+
// ...
|
|
121
|
+
venueKind: "uniswap-v2",
|
|
122
|
+
wrappedNative: "0x4200000000000000000000000000000000000006",
|
|
123
|
+
venues: [{ kind: "router", address: "0x...", verified: { /* ... */ } }],
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`src/scanner/chains.config.mjs` ships **Base** wired this way as the reference. It
|
|
128
|
+
gets all 10 capabilities; every other built-in stays at 7 and fail-closed until
|
|
129
|
+
someone verifies a venue for it.
|
|
130
|
+
|
|
131
|
+
What you get:
|
|
132
|
+
|
|
133
|
+
- **`quote`** — live exact-input pricing via `getAmountsOut`
|
|
134
|
+
- **`sellSimulation`** — a state-free round trip (buy then sell back) that catches
|
|
135
|
+
the honeypot signature: buy leg quotes fine, sell leg fails. This is the single
|
|
136
|
+
most valuable check on a low-cap venue, because most losses are not bad entries
|
|
137
|
+
but tokens that cannot be exited
|
|
138
|
+
- **`prepareUnsignedTx`** — an **unsigned** swap with a `minOut` computed from the
|
|
139
|
+
live quote, a deadline, and an explicit `approve` step listed separately so
|
|
140
|
+
"approved" and "swapped" are never collapsed into one claim
|
|
141
|
+
|
|
142
|
+
`scoreRisk` also upgrades automatically: with a router it reports real sellability
|
|
143
|
+
instead of `UNKNOWN`.
|
|
144
|
+
|
|
145
|
+
### The slippage ceiling is hard
|
|
146
|
+
|
|
147
|
+
A supplied `slippageBps` is a **maximum**, capped at 100 bps. Above that,
|
|
148
|
+
`prepareUnsignedTx` throws rather than widening:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
slippageBps 250 exceeds the 100 bps ceiling. Block and requote or split
|
|
152
|
+
the order -- do not widen the guard to force a fill.
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A cap that yields under pressure is decoration.
|
|
156
|
+
|
|
157
|
+
### V3 / V4 / custom quoters
|
|
158
|
+
|
|
159
|
+
Two adapters ship:
|
|
160
|
+
|
|
161
|
+
| `venueKind` | Requires | Prices via |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `uniswap-v2` | `router` | `getAmountsOut` on the router |
|
|
164
|
+
| `uniswap-v3` | `quoter` **and** `router` | `quoteExactInputSingle` on QuoterV2 |
|
|
165
|
+
|
|
166
|
+
V3 is **not** V2 with different addresses: it has no `getAmountsOut`, prices through
|
|
167
|
+
a separate Quoter contract, and needs an explicit fee tier per hop. Encoding a V3
|
|
168
|
+
swap with V2 assumptions yields a transaction that reverts, or worse routes through
|
|
169
|
+
the wrong pool. That is why they are sibling adapters.
|
|
170
|
+
|
|
171
|
+
The V3 adapter searches all four fee tiers (100 / 500 / 3000 / 10000) and keeps the
|
|
172
|
+
best. This is necessary, not a nicety — liquidity concentrates in one tier and which
|
|
173
|
+
tier varies by pair and chain. On Arbitrum WETH→USDC the 500 tier returns ~2.4% more
|
|
174
|
+
than the 100 tier; defaulting to 0.3% because it is the common choice silently
|
|
175
|
+
misprices stable and exotic pairs alike.
|
|
176
|
+
|
|
177
|
+
A V3 chain needs **both** a quoter and a router. A router alone stays fail-closed:
|
|
178
|
+
it could encode a swap, but with no priced expectation to guard against — exactly
|
|
179
|
+
the state that produces an unbounded fill.
|
|
180
|
+
|
|
181
|
+
**V4 and custom-quoter venues still need their own adapter.** Some forks require an
|
|
182
|
+
off-chain signed quote you cannot produce locally. Write a sibling module rather
|
|
183
|
+
than bending an existing one; silently mis-encoding a swap is far worse than
|
|
184
|
+
declaring the capability unsupported.
|
|
185
|
+
|
|
186
|
+
### Verifying a venue: run the prober
|
|
187
|
+
|
|
188
|
+
`scripts/verify-v3-venues.mjs` probes candidate addresses on every chain and prints
|
|
189
|
+
a pass/fail with the live price it got:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
node scripts/verify-v3-venues.mjs
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Codesize is not verification.** The canonical mainnet QuoterV2 address returns
|
|
196
|
+
2109 bytes of bytecode on Base — it is *some* contract, just not a working quoter
|
|
197
|
+
for that chain. A codesize check waves it through; the functional probe caught it
|
|
198
|
+
and pointed at Base's real quoter (8273 bytes, prices WETH→USDC correctly).
|
|
199
|
+
|
|
200
|
+
The rule: **a contract that correctly prices a pair you can sanity-check IS the
|
|
201
|
+
thing you think it is.** Anything less is a guess with an address attached.
|
|
202
|
+
|
|
203
|
+
## Honest evidence
|
|
204
|
+
|
|
205
|
+
Every finding carries a label. Respect the distinction:
|
|
206
|
+
|
|
207
|
+
- `LIVE` — read this call
|
|
208
|
+
- `CACHED` — recent, within stated TTL
|
|
209
|
+
- `STALE` — older than TTL; usable only with the caveat stated
|
|
210
|
+
- `UNKNOWN` — we tried and could not determine it
|
|
211
|
+
- `UNAVAILABLE` — this chain/provider cannot answer at all
|
|
212
|
+
|
|
213
|
+
`UNKNOWN` and `UNAVAILABLE` are different facts. Collapsing them is how people end
|
|
214
|
+
up sizing against stale reserves.
|
|
215
|
+
|
|
216
|
+
Risk verdicts are coarse on purpose — `PASS` / `CAUTION` / `FAIL` / `UNKNOWN`. A
|
|
217
|
+
score like "risk 62/100" invites trading a number nobody understands.
|
|
218
|
+
|
|
219
|
+
**`UNKNOWN` is not `PASS`.** The generic scanner returns `UNKNOWN` for sellability
|
|
220
|
+
because it cannot prove exitability without a verified router. A token is not
|
|
221
|
+
tradeable until a round-trip sell simulation succeeds.
|
|
222
|
+
|
|
223
|
+
## Contributing a chain upstream
|
|
224
|
+
|
|
225
|
+
Append to `CHAIN_CONFIGS` in `src/scanner/chains.config.mjs` and open a PR. Tests
|
|
226
|
+
already assert every built-in config validates and that none embeds an RPC URL, so
|
|
227
|
+
CI will catch a malformed entry.
|
|
228
|
+
|
|
229
|
+
If you add venues, put the verification method and date in the PR body.
|