@oracle-agent/oracle 0.10.0 → 0.11.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.
Files changed (148) hide show
  1. package/README.md +21 -12
  2. package/bin/oracle-data-mcp.mjs +9 -0
  3. package/docs/architecture.md +3 -1
  4. package/docs/profiles.md +3 -6
  5. package/docs/public-surface.md +133 -0
  6. package/package.json +1 -1
  7. package/public/oracle-splash/assets/cli-chain-hyperliquid.jpg +0 -0
  8. package/public/oracle-splash/brand/aave.webp +0 -0
  9. package/public/oracle-splash/brand/aavev2.webp +0 -0
  10. package/public/oracle-splash/brand/aborean.webp +0 -0
  11. package/public/oracle-splash/brand/across.webp +0 -0
  12. package/public/oracle-splash/brand/aerodrome.webp +0 -0
  13. package/public/oracle-splash/brand/afi.webp +0 -0
  14. package/public/oracle-splash/brand/arcus.webp +0 -0
  15. package/public/oracle-splash/brand/aster.webp +0 -0
  16. package/public/oracle-splash/brand/avant.webp +0 -0
  17. package/public/oracle-splash/brand/balancer.webp +0 -0
  18. package/public/oracle-splash/brand/beefy.webp +0 -0
  19. package/public/oracle-splash/brand/benqi.webp +0 -0
  20. package/public/oracle-splash/brand/bestinslot.ico +0 -0
  21. package/public/oracle-splash/brand/bitfi.webp +0 -0
  22. package/public/oracle-splash/brand/bonkswap.webp +0 -0
  23. package/public/oracle-splash/brand/bouncebit.webp +0 -0
  24. package/public/oracle-splash/brand/camelot.webp +0 -0
  25. package/public/oracle-splash/brand/compound.webp +0 -0
  26. package/public/oracle-splash/brand/concrete.webp +0 -0
  27. package/public/oracle-splash/brand/cow.webp +0 -0
  28. package/public/oracle-splash/brand/curve.webp +0 -0
  29. package/public/oracle-splash/brand/derive.webp +0 -0
  30. package/public/oracle-splash/brand/dolomite.webp +0 -0
  31. package/public/oracle-splash/brand/eigenlayer.webp +0 -0
  32. package/public/oracle-splash/brand/ekubo.webp +0 -0
  33. package/public/oracle-splash/brand/ethena.webp +0 -0
  34. package/public/oracle-splash/brand/etherfi.webp +0 -0
  35. package/public/oracle-splash/brand/etherfiborrow.webp +0 -0
  36. package/public/oracle-splash/brand/felix.webp +0 -0
  37. package/public/oracle-splash/brand/fluid.webp +0 -0
  38. package/public/oracle-splash/brand/fluxbeam.webp +0 -0
  39. package/public/oracle-splash/brand/folks.webp +0 -0
  40. package/public/oracle-splash/brand/fortyacres.webp +0 -0
  41. package/public/oracle-splash/brand/gainsnetwork.webp +0 -0
  42. package/public/oracle-splash/brand/gassss.webp +0 -0
  43. package/public/oracle-splash/brand/gmx.webp +0 -0
  44. package/public/oracle-splash/brand/harmonix.webp +0 -0
  45. package/public/oracle-splash/brand/hlorderbook.webp +0 -0
  46. package/public/oracle-splash/brand/hlp.webp +0 -0
  47. package/public/oracle-splash/brand/hop.webp +0 -0
  48. package/public/oracle-splash/brand/hyperbeat.webp +0 -0
  49. package/public/oracle-splash/brand/hyperlend.webp +0 -0
  50. package/public/oracle-splash/brand/hyperliquid.webp +0 -0
  51. package/public/oracle-splash/brand/hyperliquidhpl.webp +0 -0
  52. package/public/oracle-splash/brand/hyperliquidspot.webp +0 -0
  53. package/public/oracle-splash/brand/hyperswap.webp +0 -0
  54. package/public/oracle-splash/brand/hypha.webp +0 -0
  55. package/public/oracle-splash/brand/joedex.webp +0 -0
  56. package/public/oracle-splash/brand/jupiter.webp +0 -0
  57. package/public/oracle-splash/brand/kelp.webp +0 -0
  58. package/public/oracle-splash/brand/kinetiq.webp +0 -0
  59. package/public/oracle-splash/brand/kmhype.webp +0 -0
  60. package/public/oracle-splash/brand/kona.webp +0 -0
  61. package/public/oracle-splash/brand/konalend.webp +0 -0
  62. package/public/oracle-splash/brand/lfj.webp +0 -0
  63. package/public/oracle-splash/brand/lido.webp +0 -0
  64. package/public/oracle-splash/brand/lifi.webp +0 -0
  65. package/public/oracle-splash/brand/lighter.webp +0 -0
  66. package/public/oracle-splash/brand/lista.webp +0 -0
  67. package/public/oracle-splash/brand/loopedhype.webp +0 -0
  68. package/public/oracle-splash/brand/lorenzo.webp +0 -0
  69. package/public/oracle-splash/brand/magic-eden.webp +0 -0
  70. package/public/oracle-splash/brand/magiceden.webp +0 -0
  71. package/public/oracle-splash/brand/mempool.png +0 -0
  72. package/public/oracle-splash/brand/meteora.webp +0 -0
  73. package/public/oracle-splash/brand/morpho.webp +0 -0
  74. package/public/oracle-splash/brand/myriad.webp +0 -0
  75. package/public/oracle-splash/brand/noxa.webp +0 -0
  76. package/public/oracle-splash/brand/odos.webp +0 -0
  77. package/public/oracle-splash/brand/oneinch.webp +0 -0
  78. package/public/oracle-splash/brand/openbook.webp +0 -0
  79. package/public/oracle-splash/brand/opensea.webp +0 -0
  80. package/public/oracle-splash/brand/orca.webp +0 -0
  81. package/public/oracle-splash/brand/ordinals.png +0 -0
  82. package/public/oracle-splash/brand/ostium.webp +0 -0
  83. package/public/oracle-splash/brand/pancakeswap.webp +0 -0
  84. package/public/oracle-splash/brand/pendle.webp +0 -0
  85. package/public/oracle-splash/brand/perena.webp +0 -0
  86. package/public/oracle-splash/brand/pharaoh.webp +0 -0
  87. package/public/oracle-splash/brand/phoenix.webp +0 -0
  88. package/public/oracle-splash/brand/polymarket.webp +0 -0
  89. package/public/oracle-splash/brand/polynomial.webp +0 -0
  90. package/public/oracle-splash/brand/projectx.webp +0 -0
  91. package/public/oracle-splash/brand/pumpfun.webp +0 -0
  92. package/public/oracle-splash/brand/quickswap.webp +0 -0
  93. package/public/oracle-splash/brand/rain.webp +0 -0
  94. package/public/oracle-splash/brand/raydium.webp +0 -0
  95. package/public/oracle-splash/brand/relay.webp +0 -0
  96. package/public/oracle-splash/brand/rocketpool.webp +0 -0
  97. package/public/oracle-splash/brand/saber.webp +0 -0
  98. package/public/oracle-splash/brand/sakuraswap.webp +0 -0
  99. package/public/oracle-splash/brand/sanctum.webp +0 -0
  100. package/public/oracle-splash/brand/satflow.png +0 -0
  101. package/public/oracle-splash/brand/sierra.webp +0 -0
  102. package/public/oracle-splash/brand/sky.webp +0 -0
  103. package/public/oracle-splash/brand/solv.webp +0 -0
  104. package/public/oracle-splash/brand/spark.webp +0 -0
  105. package/public/oracle-splash/brand/sparksavings.webp +0 -0
  106. package/public/oracle-splash/brand/sphere.webp +0 -0
  107. package/public/oracle-splash/brand/ssv.webp +0 -0
  108. package/public/oracle-splash/brand/stabble.webp +0 -0
  109. package/public/oracle-splash/brand/stableswap.webp +0 -0
  110. package/public/oracle-splash/brand/stakewise.webp +0 -0
  111. package/public/oracle-splash/brand/stargate.webp +0 -0
  112. package/public/oracle-splash/brand/sthype.webp +0 -0
  113. package/public/oracle-splash/brand/sushiswap.webp +0 -0
  114. package/public/oracle-splash/brand/unisat.png +0 -0
  115. package/public/oracle-splash/brand/uniswap.webp +0 -0
  116. package/public/oracle-splash/brand/uniswapv2.webp +0 -0
  117. package/public/oracle-splash/brand/uniswapv4.webp +0 -0
  118. package/public/oracle-splash/brand/unit.webp +0 -0
  119. package/public/oracle-splash/brand/velodrome.webp +0 -0
  120. package/public/oracle-splash/brand/velora.webp +0 -0
  121. package/public/oracle-splash/brand/venus.webp +0 -0
  122. package/public/oracle-splash/brand/wbeth.webp +0 -0
  123. package/public/oracle-splash/brand/xsy.webp +0 -0
  124. package/public/oracle-splash/brand/yieldyak.webp +0 -0
  125. package/public/oracle-splash/brand/zerobase.webp +0 -0
  126. package/public/oracle-splash/brand/zerox.webp +0 -0
  127. package/public/oracle-splash/index.html +236 -169
  128. package/scripts/check-doc-drift.mjs +11 -0
  129. package/scripts/e2e-solana-bitcoin.mjs +1 -1
  130. package/scripts/secret-scan.mjs +1 -1
  131. package/src/action-receipts.mjs +108 -11
  132. package/src/cli/commands/data.mjs +3 -1
  133. package/src/cli/commands/doctor.mjs +11 -1
  134. package/src/data/catalog.mjs +38 -7
  135. package/src/data/desk-data.mjs +12 -0
  136. package/src/data/providers/evm-rpc.mjs +19 -16
  137. package/src/data/providers/jupiter-venues.mjs +119 -0
  138. package/src/data/providers/jupiter.mjs +132 -0
  139. package/src/data/providers/magiceden-sol.mjs +13 -0
  140. package/src/data/providers/morpho.mjs +6 -2
  141. package/src/data/providers/pendle.mjs +3 -2
  142. package/src/data/providers/satflow.mjs +4 -1
  143. package/src/data/providers/signed-material-guard.mjs +108 -0
  144. package/src/data/providers/uniswap-v3.mjs +34 -5
  145. package/src/data/providers/uniswap-v4.mjs +414 -0
  146. package/src/prepare-envelope.mjs +22 -4
  147. package/src/router/prepare-route.mjs +17 -0
  148. package/src/router/route-sources.mjs +35 -0
@@ -4,17 +4,89 @@
4
4
  // sign, broadcast, or fetch. It only records facts supplied by its caller.
5
5
 
6
6
  import { createHash } from "node:crypto";
7
+ import { wordlists } from "ethers";
7
8
 
8
9
  export const ACTION_RECEIPT_VERSION = 1;
9
10
 
11
+ // Key-name matching was exact-ish and only covered five words, so every wallet
12
+ // export alias (secretKey, seedPhrase, wif, keyMaterial, privKey, xprv, seed,
13
+ // keystore) sailed through. There was also NO value-shape rule, so a raw key or
14
+ // a BIP-39 phrase pasted into a benign field like `memo` was recorded verbatim.
15
+ //
16
+ // Both concerns were already solved in src/address-book.mjs; this module simply
17
+ // did not use the hardened list. Found by the opus lane of the 2026-08-03
18
+ // four-model review, and verified to PERSIST: unknown top-level props are
19
+ // dropped by the schema whitelist, but a secret nested inside a RETAINED field
20
+ // (intent, route, decodedAction) survives into the frozen receipt.
10
21
  const SECRET_KEYS = new Set([
11
22
  "privatekey",
23
+ "privkey",
24
+ "secretkey",
25
+ "seedphrase",
26
+ "seed",
12
27
  "mnemonic",
28
+ "passphrase",
29
+ "password",
30
+ "keystore",
31
+ "keymaterial",
32
+ "wif",
33
+ "xprv",
34
+ "xpriv",
13
35
  "bearer",
14
36
  "authorization",
15
37
  "signature",
38
+ "apikey",
39
+ "credential",
40
+ ]);
41
+
42
+ // Unambiguous key shapes: WIF, extended private key, BIP-39 word run. These are
43
+ // refused wherever they appear.
44
+ //
45
+ // A bare 64-hex string is deliberately NOT in this list. A raw EVM private key
46
+ // and a bytes32 tx hash are byte-identical, and receipts legitimately carry
47
+ // txHash / orderHash / salt / merkleRoot / nonce. Blanket-refusing 64-hex would
48
+ // break real receipts — an over-tight filter is its own bug. It is instead
49
+ // gated on the field name below.
50
+ const KEY_SHAPED_VALUE = [
51
+ /\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\b/,
52
+ /\bxprv[0-9A-Za-z]{50,}\b/,
53
+ ];
54
+
55
+ const BIP39_WORD_COUNTS = new Set([12, 15, 18, 21, 24]);
56
+
57
+ function isBip39EnglishPhrase(value) {
58
+ const words = String(value).trim().toLowerCase().split(/\s+/).filter(Boolean);
59
+ return BIP39_WORD_COUNTS.has(words.length) && words.every((word) => wordlists.en.getWordIndex(word) >= 0);
60
+ }
61
+
62
+ const BARE_32_BYTE_HEX = /^\s*(?:0x)?[0-9a-fA-F]{64}\s*$/;
63
+
64
+ // Field names whose whole value is EXPECTED to be a 32-byte digest.
65
+ //
66
+ // The trailing `(?:e?s)?` matters: `publicValue` scans array leaves using the
67
+ // PARENT key, so a batched route's `orderHashes` / `proofs` / `commitments`
68
+ // arrives here in its plural form (English pluralizes `hash` as `hashes`).
69
+ // Without it a digest LIST is refused while the exact same digest in singular
70
+ // form is accepted — an over-tight filter that breaks real receipts and blocks
71
+ // no secret (key-SHAPED values are refused by KEY_SHAPED_VALUE / BIP-39
72
+ // regardless of field name).
73
+ const DIGEST_FIELD = /(hash|digest|root|salt|nonce|proof|commitment|sig|id)(?:e?s)?$/i;
74
+ const PROTOCOL_HEX_FIELDS = new Set([
75
+ "calldata",
76
+ "data",
77
+ "publickey",
78
+ "pubkey",
79
+ "message",
80
+ "payload",
81
+ "input",
82
+ "extradata",
16
83
  ]);
17
84
 
85
+ function allows32ByteHex(key) {
86
+ const normalized = normalizedKey(key);
87
+ return PROTOCOL_HEX_FIELDS.has(normalized) || DIGEST_FIELD.test(normalized);
88
+ }
89
+
18
90
  export class ActionReceiptSecretError extends Error {
19
91
  constructor(path) {
20
92
  super(`action receipt refused: secret-bearing field at ${path}`);
@@ -32,23 +104,45 @@ function isSecretKey(key) {
32
104
  return [...SECRET_KEYS].some((secret) => normalized.includes(secret));
33
105
  }
34
106
 
35
- function publicValue(value, path = "$") {
36
- if (value == null || typeof value === "string" || typeof value === "boolean") return value;
107
+ /**
108
+ * Refuse a key-material-shaped VALUE, so a secret pasted into a benign field
109
+ * name (memo, note, label) cannot be recorded verbatim.
110
+ *
111
+ * Bare 64-hex is only refused when the field name is NOT digest-shaped: a raw
112
+ * EVM private key and a bytes32 hash are byte-identical, and receipts carry
113
+ * txHash / orderHash / salt / nonce legitimately.
114
+ */
115
+ function assertNoSecretValue(key, value, path) {
116
+ if (typeof value !== "string") return;
117
+ if (KEY_SHAPED_VALUE.some((re) => re.test(value)) || isBip39EnglishPhrase(value)) {
118
+ throw new ActionReceiptSecretError(path);
119
+ }
120
+ if (BARE_32_BYTE_HEX.test(value) && !allows32ByteHex(key)) {
121
+ throw new ActionReceiptSecretError(path);
122
+ }
123
+ }
124
+
125
+ function publicValue(value, path = "$", key = "") {
126
+ if (value == null || typeof value === "boolean") return value;
127
+ if (typeof value === "string") {
128
+ assertNoSecretValue(key, value, path);
129
+ return value;
130
+ }
37
131
  if (typeof value === "number") {
38
132
  if (!Number.isFinite(value)) throw new TypeError(`action receipt: non-finite number at ${path}`);
39
133
  return value;
40
134
  }
41
135
  if (typeof value === "bigint") return value.toString();
42
- if (Array.isArray(value)) return value.map((item, index) => publicValue(item, `${path}[${index}]`));
136
+ if (Array.isArray(value)) return value.map((item, index) => publicValue(item, `${path}[${index}]`, key));
43
137
  if (typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) {
44
138
  throw new TypeError(`action receipt: JSON-like value required at ${path}`);
45
139
  }
46
140
 
47
141
  const out = {};
48
- for (const key of Object.keys(value).sort()) {
49
- const childPath = `${path}.${key}`;
50
- if (isSecretKey(key)) throw new ActionReceiptSecretError(childPath);
51
- if (value[key] !== undefined) out[key] = publicValue(value[key], childPath);
142
+ for (const childKey of Object.keys(value).sort()) {
143
+ const childPath = `${path}.${childKey}`;
144
+ if (isSecretKey(childKey)) throw new ActionReceiptSecretError(childPath);
145
+ if (value[childKey] !== undefined) out[childKey] = publicValue(value[childKey], childPath, childKey);
52
146
  }
53
147
  return out;
54
148
  }
@@ -58,12 +152,15 @@ export function assertNoReceiptSecrets(value) {
58
152
  return publicValue(value);
59
153
  }
60
154
 
155
+ function canonicalPublicJson(value) {
156
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
157
+ if (Array.isArray(value)) return `[${value.map(canonicalPublicJson).join(",")}]`;
158
+ return `{${Object.keys(value).map((key) => `${JSON.stringify(key)}:${canonicalPublicJson(value[key])}`).join(",")}}`;
159
+ }
160
+
61
161
  /** Canonical JSON with recursively sorted object keys. */
62
162
  export function canonicalReceiptJson(value) {
63
- const safe = publicValue(value);
64
- if (safe === null || typeof safe !== "object") return JSON.stringify(safe);
65
- if (Array.isArray(safe)) return `[${safe.map(canonicalReceiptJson).join(",")}]`;
66
- return `{${Object.keys(safe).map((key) => `${JSON.stringify(key)}:${canonicalReceiptJson(safe[key])}`).join(",")}}`;
163
+ return canonicalPublicJson(publicValue(value));
67
164
  }
68
165
 
69
166
  /** Hash exactly the public receipt fields, excluding the self-referential id. */
@@ -53,7 +53,9 @@ export default {
53
53
  const bin = path.join(ctx.root, "bin", "desk-server.mjs");
54
54
  return spawnChild(process.execPath, [bin], { stdio: "inherit", env }, "desk-server");
55
55
  }
56
- if (verb === "health") return dataFetch("/");
56
+ // The server serves /health; / intentionally 404s with a route list. This
57
+ // probed / and so reported the package's own running server as down.
58
+ if (verb === "health") return dataFetch("/health");
57
59
  if (verb === "catalog") return dataFetch("/data/catalog");
58
60
  if (verb === "call") {
59
61
  const provider = ctx.argv[1];
@@ -21,7 +21,9 @@ function loadExecEnv(p) {
21
21
 
22
22
  async function probeDataServer(url) {
23
23
  try {
24
- const res = await fetch(url.replace(/\/$/, "") + "/");
24
+ // The server serves /health; / intentionally 404s with a route list, so
25
+ // probing / reported a healthy server as down.
26
+ const res = await fetch(url.replace(/\/$/, "") + "/health");
25
27
  return res.ok;
26
28
  } catch {
27
29
  return false;
@@ -47,9 +49,17 @@ export default {
47
49
 
48
50
  const execEnvPath = ctx.paths.oracleExecEnvPath();
49
51
  const envFile = loadExecEnv(execEnvPath);
52
+ // `oracle init --apply` writes ORACLE_ROUTE_ATTESTATION_SECRET (see
53
+ // bin/oracle-init.mjs), but doctor checked only ORACLE_ATTESTATION_SECRET
54
+ // and ORACLE_STAMP_HMAC_SECRET — so it told the user to rerun an init step
55
+ // that could never satisfy the check.
50
56
  const att =
57
+ process.env.ORACLE_ROUTE_ATTESTATION_SECRET ||
58
+ process.env.MAD_ROUTE_ATTESTATION_SECRET ||
51
59
  process.env.ORACLE_ATTESTATION_SECRET ||
52
60
  process.env.ORACLE_STAMP_HMAC_SECRET ||
61
+ envFile.ORACLE_ROUTE_ATTESTATION_SECRET ||
62
+ envFile.MAD_ROUTE_ATTESTATION_SECRET ||
53
63
  envFile.ORACLE_ATTESTATION_SECRET ||
54
64
  envFile.ORACLE_STAMP_HMAC_SECRET;
55
65
  push("attestation_secret", att ? "ok" : "warn", att ? "present" : "missing",
@@ -1,8 +1,13 @@
1
1
  // Provider catalog — DATA plane registry. Wallet keys never live here.
2
2
 
3
+ import { UNI_V3_CHAINS } from "./providers/uniswap-v3.mjs";
4
+ import { UNI_V4_CHAINS } from "./providers/uniswap-v4.mjs";
5
+
3
6
  /** @typedef {{ id: string, venue: string, chainIds: number[], auth: 'none'|'apiKey'|'agentKey', ops: string[], baseEnv?: string[], description?: string }} ProviderMeta */
4
7
 
5
8
  const REGISTRY = new Map();
9
+ const UNI_V3_CHAIN_IDS = Object.keys(UNI_V3_CHAINS).map(Number);
10
+ const UNI_V4_CHAIN_IDS = Object.keys(UNI_V4_CHAINS).map(Number);
6
11
  const BUILTIN_PROVIDER_IDS = new Set();
7
12
  let BUILTINS_LOCKED = false;
8
13
 
@@ -211,9 +216,9 @@ registerProvider({
211
216
  venue: "solana-dex",
212
217
  chainIds: [],
213
218
  auth: "none",
214
- ops: ["health", "quote", "prepare"],
219
+ ops: ["health", "venues", "quote", "prepare"],
215
220
  execution: "prepare",
216
- baseEnv: ["JUPITER_SWAP_API_URL"],
221
+ baseEnv: ["JUPITER_SWAP_API_URL", "SOLANA_RPC_URL"],
217
222
  description: "Jupiter Swap API v1 quote + unsigned user-signature swap transaction preparation",
218
223
  });
219
224
 
@@ -304,7 +309,7 @@ registerProvider({
304
309
  registerProvider({
305
310
  id: "uniswap-v3",
306
311
  venue: "dex",
307
- chainIds: [1, 8453, 42161],
312
+ chainIds: UNI_V3_CHAIN_IDS,
308
313
  auth: "none",
309
314
  ops: ["health", "quote", "prepare", "quotePath", "ethUsdc", "chains"],
310
315
  execution: "prepare",
@@ -312,8 +317,34 @@ registerProvider({
312
317
  "ETH_RPC_URL",
313
318
  "BASE_RPC_URL",
314
319
  "ARBITRUM_RPC_URL",
320
+ "OPTIMISM_RPC_URL",
321
+ "BSC_RPC_URL",
322
+ "POLYGON_RPC_URL",
323
+ "AVALANCHE_RPC_URL",
324
+ "RH_CHAIN_RPC",
325
+ ],
326
+ description: "Uniswap V3 QuoterV2 exact-in quotes via eth_call across official deployments plus Robinhood Chain, no key",
327
+ });
328
+
329
+ registerProvider({
330
+ id: "uniswap-v4",
331
+ venue: "dex",
332
+ chainIds: UNI_V4_CHAIN_IDS,
333
+ auth: "none",
334
+ ops: ["health", "quote", "chains"],
335
+ execution: "read-only",
336
+ baseEnv: [
337
+ "ETH_RPC_URL",
338
+ "BASE_RPC_URL",
339
+ "ARBITRUM_RPC_URL",
340
+ "OPTIMISM_RPC_URL",
341
+ "BSC_RPC_URL",
342
+ "POLYGON_RPC_URL",
343
+ "AVALANCHE_RPC_URL",
344
+ "RH_CHAIN_RPC",
315
345
  ],
316
- description: "Uniswap V3 QuoterV2 exact-in quotes via eth_call (ETH/Base/Arb, no key)",
346
+ description:
347
+ "Uniswap V4 Quoter exact-in quotes via eth_call across official deployments plus Robinhood Chain. Quote-only until Universal Router prepare is classified.",
317
348
  });
318
349
 
319
350
  registerProvider({
@@ -503,7 +534,7 @@ registerProvider({
503
534
  registerProvider({
504
535
  id: "odos",
505
536
  venue: "dex",
506
- chainIds: [1, 10, 56, 137, 8453, 42161, 43114],
537
+ chainIds: [1, 10, 56, 137, 8453, 42161, 43114, 4663],
507
538
  auth: "none",
508
539
  ops: ["health", "chains", "quote", "prepare"],
509
540
  // Tier is the honest signal here: the API returns HTTP 410 Gone as of
@@ -532,7 +563,7 @@ registerProvider({
532
563
  registerProvider({
533
564
  id: "paraswap",
534
565
  venue: "dex",
535
- chainIds: [1, 10, 56, 137, 8453, 42161, 43114],
566
+ chainIds: [1, 10, 56, 137, 8453, 42161, 43114, 4663],
536
567
  auth: "none",
537
568
  ops: ["health", "tokens", "price", "quote", "prepare"],
538
569
  execution: "prepare",
@@ -543,7 +574,7 @@ registerProvider({
543
574
  registerProvider({
544
575
  id: "rfq",
545
576
  venue: "intent-rfq",
546
- chainIds: [1, 10, 56, 137, 8453, 42161, 43114],
577
+ chainIds: [1, 10, 56, 137, 8453, 42161, 43114, 4663],
547
578
  auth: "none",
548
579
  ops: ["health", "intent", "quote"],
549
580
  execution: "prepare",
@@ -15,6 +15,7 @@ import * as llama from "./providers/defillama.mjs";
15
15
  import * as lifi from "./providers/lifi.mjs";
16
16
  import * as dxs from "./providers/dexscreener.mjs";
17
17
  import * as uni from "./providers/uniswap-v3.mjs";
18
+ import * as univ4 from "./providers/uniswap-v4.mjs";
18
19
  import * as aerodrome from "./providers/aerodrome.mjs";
19
20
  import * as zerox from "./providers/zerox.mjs";
20
21
  import * as bridges from "./providers/bridges.mjs";
@@ -144,6 +145,7 @@ const OPS = {
144
145
  },
145
146
  jupiter: {
146
147
  health: (o) => jupiter.jupiterHealth(o),
148
+ venues: (o, a = {}) => jupiter.jupiterVenues(a, o),
147
149
  quote: (o, a = {}) => jupiter.jupiterQuote(a, o),
148
150
  prepare: (o, a = {}) => jupiter.jupiterPrepareSwap(a, o),
149
151
  },
@@ -176,6 +178,11 @@ const OPS = {
176
178
  ethUsdc: (o, a = {}) => uni.uniV3EthUsdc(a, o),
177
179
  chains: () => uni.uniV3Chains(),
178
180
  },
181
+ "uniswap-v4": {
182
+ health: (o, a = {}) => univ4.uniV4Health({ ...o, chainId: a.chainId ?? 8453 }),
183
+ quote: (o, a = {}) => univ4.uniV4QuoteExactIn(a, o),
184
+ chains: () => univ4.uniV4Chains(),
185
+ },
179
186
  aerodrome: {
180
187
  health: (o, a = {}) => aerodrome.aerodromeHealth({ ...o, chainId: a.chainId ?? 8453 }),
181
188
  quote: (o, a = {}) => aerodrome.aerodromeQuoteExactIn(a, o),
@@ -665,6 +672,11 @@ export const data = {
665
672
  ethUsdc: (a, o) => dataCall("uniswap-v3", "ethUsdc", a || {}, o),
666
673
  chains: (o) => dataCall("uniswap-v3", "chains", {}, o),
667
674
  },
675
+ univ4: {
676
+ health: (a, o) => dataCall("uniswap-v4", "health", a || {}, o),
677
+ quote: (a, o) => dataCall("uniswap-v4", "quote", a || {}, o),
678
+ chains: (o) => dataCall("uniswap-v4", "chains", {}, o),
679
+ },
668
680
  bridges: {
669
681
  across: (a, o) => dataCall("across", "suggestedFees", a || {}, o),
670
682
  hop: (a, o) => dataCall("hop", "quote", a || {}, o),
@@ -61,25 +61,28 @@ export function resolveRpcUrl(chainId, { rpcUrl, env = process.env } = {}) {
61
61
  * @param {any[]} [params]
62
62
  */
63
63
 
64
- /** Prepare-only: write/settle JSON-RPC methods are hard-refused even on deep import. */
65
- const EVM_WRITE_RPC = new Set([
66
- "eth_sendTransaction",
67
- "eth_sendRawTransaction",
68
- "eth_sign",
69
- "eth_signTransaction",
70
- "eth_signTypedData",
71
- "eth_signTypedData_v4",
72
- "personal_sign",
73
- ]);
64
+ // `rpcCall` is the raw public transport exposed by the data facade. It must be
65
+ // an allowlist, not a partial write denylist: every unrecognized method is
66
+ // unsafe to forward because vendors add write namespaces (`wallet_send*`,
67
+ // `pm_sendUserOperation`, `mev_sendBundle`) faster than a denylist stays fresh.
68
+ //
69
+ // Canonical JSON-RPC method names are ASCII lowercase + underscores. Reject
70
+ // whitespace, separator tricks, and Unicode homoglyphs rather than normalizing
71
+ // them into a different network request.
72
+ const EVM_READ_RPC = /^(?:eth_(?:call|blocknumber|chainid|get[a-z0-9]+|feehistory|gasprice|maxpriorityfeepergas|syncing|protocolversion|estimategas|createaccesslist)|net_(?:version|listening|peercount)|web3_(?:clientversion|sha3))$/;
73
+
74
+ function readRpcMethod(method) {
75
+ const raw = String(method || "");
76
+ const probe = raw.toLowerCase();
77
+ if (raw !== raw.trim() || !/^[a-zA-Z0-9_]+$/.test(raw) || !EVM_READ_RPC.test(probe)) {
78
+ throw new Error(`evm-rpc: ${raw} refused — @oracle-agent/oracle is prepare-only (read RPC only; no broadcast/sign)`);
79
+ }
80
+ return probe;
81
+ }
74
82
 
75
83
  export async function rpcCall(chainId, method, params = [], opts = {}) {
76
84
  const methodName = String(method || "");
77
- if (EVM_WRITE_RPC.has(methodName) || /send|sign/i.test(methodName) && !/^eth_get|^eth_call|^eth_block|^eth_chain|^eth_fee|^eth_gas|^eth_max|^eth_sync|^net_|^web3_/.test(methodName)) {
78
- // allowlist is positive for common reads; anything send/sign-shaped dies.
79
- if (EVM_WRITE_RPC.has(methodName) || methodName.startsWith("eth_send") || methodName.includes("sign")) {
80
- throw new Error(`evm-rpc: ${methodName} refused — @oracle-agent/oracle is prepare-only (no broadcast/sign)`);
81
- }
82
- }
85
+ readRpcMethod(methodName);
83
86
  const url = resolveRpcUrl(chainId, opts);
84
87
  if (!url) {
85
88
  throw new Error(
@@ -0,0 +1,119 @@
1
+ // Solana venue labels routed through the Jupiter aggregator provider.
2
+ //
3
+ // Preserved as source data on 2026-08-03: this roster previously existed only
4
+ // as markup in the splash page, so deleting that markup silently deleted the
5
+ // evidence behind the README/site claim. Data belongs in src/, not in a
6
+ // presentation layer that a redesign can drop.
7
+ //
8
+ // 101 program labels resolving to 98 unique venue brands: Aldrin/Aldrin V2,
9
+ // Orca V1/Orca V2, and SolFi/SolFi V2 are version variants of one venue.
10
+
11
+ export const JUPITER_VENUE_LABELS = Object.freeze([
12
+ "1DEX",
13
+ "Aldrin",
14
+ "Aldrin V2",
15
+ "AlphaQ",
16
+ "Aquifer",
17
+ "BinaryFi",
18
+ "BisonFi",
19
+ "BisonFi Predict",
20
+ "Bonkswap",
21
+ "Boop.fun",
22
+ "Byreal",
23
+ "Carrot",
24
+ "Crema",
25
+ "Cropper",
26
+ "customizable",
27
+ "DefiTuna",
28
+ "Denali",
29
+ "Deriverse",
30
+ "DexLab",
31
+ "Dynamic Bonding Curve",
32
+ "Flint",
33
+ "Flux",
34
+ "FluxBeam",
35
+ "GatorSwap",
36
+ "Gavel",
37
+ "GoonFi V2",
38
+ "GooseFX GAMMA",
39
+ "Guacswap",
40
+ "Hadron",
41
+ "Heaven",
42
+ "Helium Network",
43
+ "Huma",
44
+ "HumidiFi",
45
+ "Hylo Exchange",
46
+ "Hylo Stability Pool",
47
+ "Invariant",
48
+ "Jupiter Lend Earn",
49
+ "JupiterRfqV2",
50
+ "JupLend AMM",
51
+ "Kipseli",
52
+ "LemmingsFi",
53
+ "Manifest",
54
+ "Mercurial",
55
+ "MetaDAO",
56
+ "Meteora",
57
+ "Meteora DAMM v2",
58
+ "Meteora DLMM",
59
+ "Metric",
60
+ "Moonit",
61
+ "more chains",
62
+ "M Swap",
63
+ "Obric V2",
64
+ "Obsidian",
65
+ "Omnipair",
66
+ "OpenBook V2",
67
+ "Orca V1",
68
+ "Orca V2",
69
+ "PancakeSwap",
70
+ "Penguin",
71
+ "Perena",
72
+ "Perena Star",
73
+ "Perps",
74
+ "Phoenix",
75
+ "protocol builder",
76
+ "Pump.fun",
77
+ "Pump.fun Amm",
78
+ "Quantum",
79
+ "Raydium",
80
+ "Raydium CLMM",
81
+ "Raydium CP",
82
+ "Raydium Launchlab",
83
+ "Riptide",
84
+ "RunnerRodeo",
85
+ "Saber",
86
+ "Saber (Decimals)",
87
+ "Sanctum",
88
+ "Sanctum Infinity",
89
+ "Saros",
90
+ "Saros DLMM",
91
+ "Scale Amm",
92
+ "Scale Vmm",
93
+ "Scorch",
94
+ "Solayer",
95
+ "SolFi",
96
+ "SolFi V2",
97
+ "Stabble CLMM",
98
+ "Stabble Stable Swap",
99
+ "Stabble Weighted Swap",
100
+ "StepN",
101
+ "TaurusFi",
102
+ "TesseraV",
103
+ "Token Swap",
104
+ "Trends",
105
+ "VaultLiquidUnstake",
106
+ "Virtuals",
107
+ "Voltr",
108
+ "WhaleStreet",
109
+ "Whirlpool",
110
+ "Woofi",
111
+ "XOrca",
112
+ "ZeroFi"
113
+ ]);
114
+
115
+ /** Unique venue brands, collapsing version variants. */
116
+ export function uniqueVenueBrands() {
117
+ const norm = (s) => s.toLowerCase().replace(/\s+v[0-9]+$/, "").replace(/[^a-z0-9]/g, "");
118
+ return new Set(JUPITER_VENUE_LABELS.map(norm));
119
+ }
@@ -4,6 +4,7 @@
4
4
  import { httpJson } from "../http.mjs";
5
5
  import { solanaPubkey } from "./solana-rpc.mjs";
6
6
  import { stampPrepared } from "../../prepare-envelope.mjs";
7
+ import { assertNoSignedMaterial } from "./signed-material-guard.mjs";
7
8
 
8
9
  export const JUPITER_SWAP_API = "https://lite-api.jup.ag/swap/v1";
9
10
  export const SOL_MINT = "So11111111111111111111111111111111111111112";
@@ -65,6 +66,131 @@ function solanaGuard(quote = {}, { quotedAtMs = Date.now(), ttlMs = DEFAULT_QUOT
65
66
  };
66
67
  }
67
68
 
69
+ // Known BPF loaders. A program is only real if it is BOTH executable AND owned
70
+ // by one of these. `executable: true` alone is claimable by any account, so
71
+ // checking it in isolation is not proof.
72
+ const SOLANA_LOADERS = new Set([
73
+ "BPFLoaderUpgradeab1e11111111111111111111111",
74
+ "BPFLoader2111111111111111111111111111111111",
75
+ "BPFLoader1111111111111111111111111111111111",
76
+ ]);
77
+
78
+ const B58_ALPHA = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
79
+
80
+ function isSolanaPubkey(s) {
81
+ if (typeof s !== "string" || !s.length) return false;
82
+ let num = 0n;
83
+ for (const ch of s) {
84
+ const v = B58_ALPHA.indexOf(ch);
85
+ if (v < 0) return false;
86
+ num = num * 58n + BigInt(v);
87
+ }
88
+ let hex = num.toString(16);
89
+ if (hex.length % 2) hex = "0" + hex;
90
+ let bytes = hex.length / 2;
91
+ for (const ch of s) {
92
+ if (ch === "1") bytes++;
93
+ else break;
94
+ }
95
+ return bytes === 32;
96
+ }
97
+
98
+ export async function jupiterVenues(args = {}, opts = {}) {
99
+ const rpcUrl = args.rpc || opts.solanaRpc || process.env.SOLANA_RPC || "https://api.mainnet-beta.solana.com";
100
+ const fetchImpl = opts.fetchImpl || fetch;
101
+
102
+ const res = await fetchImpl(`${JUPITER_SWAP_API}/program-id-to-label`, {
103
+ signal: AbortSignal.timeout(Number(opts.timeoutMs) || 15_000),
104
+ });
105
+ if (!res.ok) throw new Error(`jupiter venue map failed: HTTP ${res.status}`);
106
+ const labelMap = await res.json();
107
+
108
+ const entries = Object.entries(labelMap);
109
+ const wellFormed = entries.filter(([id]) => isSolanaPubkey(id));
110
+ const verified = [];
111
+ const unresolved = entries
112
+ .filter(([id]) => !isSolanaPubkey(id))
113
+ .map(([id, label]) => ({ programId: id, label, reason: "malformed program id" }));
114
+
115
+ if (args.verify === false) {
116
+ return {
117
+ source: `${JUPITER_SWAP_API}/program-id-to-label`,
118
+ verified: wellFormed.map(([programId, label]) => ({ programId, label })),
119
+ unresolved,
120
+ verifiedOnChain: false,
121
+ totals: { fromRouterMap: entries.length, verified: wellFormed.length },
122
+ };
123
+ }
124
+
125
+ const call = async (body) => {
126
+ const r = await fetchImpl(rpcUrl, {
127
+ method: "POST",
128
+ headers: { "content-type": "application/json" },
129
+ body: JSON.stringify(body),
130
+ signal: AbortSignal.timeout(Number(opts.timeoutMs) || 20_000),
131
+ });
132
+ const j = await r.json();
133
+ if (j.error) throw new Error(j.error.message);
134
+ return j.result;
135
+ };
136
+
137
+ for (let i = 0; i < wellFormed.length; i += 90) {
138
+ const chunk = wellFormed.slice(i, i + 90);
139
+ let out = null;
140
+ for (let attempt = 0; attempt < 3 && !out; attempt++) {
141
+ try {
142
+ out = await call({
143
+ jsonrpc: "2.0",
144
+ id: 1,
145
+ method: "getMultipleAccounts",
146
+ params: [chunk.map(([id]) => id), { encoding: "base64", dataSlice: { offset: 0, length: 0 } }],
147
+ });
148
+ } catch {
149
+ await new Promise((r) => setTimeout(r, 2000 * (attempt + 1)));
150
+ }
151
+ }
152
+ if (!out) {
153
+ for (const [programId, label] of chunk) {
154
+ unresolved.push({ programId, label, reason: "rpc batch failed after 3 attempts" });
155
+ }
156
+ continue;
157
+ }
158
+ out.value.forEach((acc, k) => {
159
+ const [programId, label] = chunk[k];
160
+ if (acc && acc.executable === true && SOLANA_LOADERS.has(acc.owner)) {
161
+ verified.push({ programId, label, owner: acc.owner });
162
+ } else {
163
+ unresolved.push({
164
+ programId,
165
+ label,
166
+ reason: !acc ? "account not found" : acc.executable !== true ? "not executable" : `unknown owner ${acc.owner}`,
167
+ });
168
+ }
169
+ });
170
+ if (i + 90 < wellFormed.length) await new Promise((r) => setTimeout(r, 700));
171
+ }
172
+
173
+ const brands = new Set(
174
+ verified.map((v) => v.label.replace(/\s+(CLMM|CP|V\d+|Launchlab|DLMM|Pools?)$/i, "").trim())
175
+ );
176
+
177
+ return {
178
+ source: `${JUPITER_SWAP_API}/program-id-to-label`,
179
+ rpc: rpcUrl,
180
+ verifiedOnChain: true,
181
+ verifiedAt: new Date().toISOString(),
182
+ totals: {
183
+ fromRouterMap: entries.length,
184
+ verified: verified.length,
185
+ unresolved: unresolved.length,
186
+ distinctLabels: new Set(verified.map((v) => v.label)).size,
187
+ distinctBrands: brands.size,
188
+ },
189
+ verified: verified.sort((a, b) => a.label.localeCompare(b.label)),
190
+ unresolved,
191
+ };
192
+ }
193
+
68
194
  export async function jupiterHealth(opts = {}) {
69
195
  try {
70
196
  const q = await jupiterQuote({ inputMint: SOL_MINT, outputMint: USDC_MINT, amount: "1000000", slippageBps: 50 }, opts);
@@ -142,6 +268,12 @@ export async function jupiterPrepareSwap(args = {}, opts = {}) {
142
268
  });
143
269
  const swapTransaction = base64Tx(response?.swapTransaction);
144
270
 
271
+ // Refuse pre-signed material by PRESENCE. The primary `swapTransaction` field
272
+ // stays unsigned, but the full upstream body was echoed as `raw` and
273
+ // hash-bound into the envelope, so a hostile endpoint could smuggle a signed
274
+ // sibling that downstream tooling might select instead.
275
+ assertNoSignedMaterial("jupiter", response);
276
+
145
277
  // Bind the returned transaction to the quote it claims to implement. A
146
278
  // compromised or impersonated endpoint can return any bytes while Oracle
147
279
  // stamps the optimistic quote on top; the user signs the bytes, not the