@oracle-agent/oracle 0.9.8 → 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 (160) hide show
  1. package/README.md +15 -10
  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 +3 -2
  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 +337 -197
  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/auth/oauth.mjs +672 -0
  133. package/src/cli/commands/auth.mjs +193 -0
  134. package/src/cli/commands/bootstrap.mjs +2 -2
  135. package/src/cli/commands/chat.mjs +97 -75
  136. package/src/cli/commands/data.mjs +3 -1
  137. package/src/cli/commands/doctor.mjs +11 -1
  138. package/src/cli/commands/model.mjs +79 -23
  139. package/src/cli/kernel.mjs +4 -3
  140. package/src/cli/model-config.mjs +70 -0
  141. package/src/data/catalog.mjs +38 -7
  142. package/src/data/desk-data.mjs +12 -0
  143. package/src/data/providers/evm-rpc.mjs +19 -16
  144. package/src/data/providers/jupiter-venues.mjs +119 -0
  145. package/src/data/providers/jupiter.mjs +132 -0
  146. package/src/data/providers/magiceden-sol.mjs +13 -0
  147. package/src/data/providers/morpho.mjs +6 -2
  148. package/src/data/providers/pendle.mjs +3 -2
  149. package/src/data/providers/satflow.mjs +4 -1
  150. package/src/data/providers/signed-material-guard.mjs +108 -0
  151. package/src/data/providers/uniswap-v3.mjs +34 -5
  152. package/src/data/providers/uniswap-v4.mjs +414 -0
  153. package/src/prepare-envelope.mjs +22 -4
  154. package/src/router/prepare-route.mjs +17 -0
  155. package/src/router/route-sources.mjs +35 -0
  156. package/src/tui/app.mjs +18 -7
  157. package/src/tui/backend.mjs +145 -0
  158. package/src/tui/standalone-client.mjs +666 -0
  159. package/src/cards.mjs +0 -369
  160. package/src/cli/__pycache__/oracle-harness.cpython-311.pyc +0 -0
@@ -0,0 +1,70 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { ensureDir, oracleConfigDir } from "./paths.mjs";
5
+
6
+ const BACKENDS = new Set(["auto", "standalone", "hermes"]);
7
+ const PROVIDERS = new Set([
8
+ "openrouter", "openai", "xai", "deepseek", "gemini", "custom",
9
+ "anthropic-oauth", "openai-codex", "xai-oauth",
10
+ ]);
11
+
12
+ export function modelConfigPath() {
13
+ return path.join(oracleConfigDir(), "model.json");
14
+ }
15
+
16
+ function optionalString(value) {
17
+ const text = String(value || "").trim();
18
+ return text || undefined;
19
+ }
20
+
21
+ function normalize(config = {}) {
22
+ const backend = optionalString(config.backend)?.toLowerCase() || "auto";
23
+ const provider = optionalString(config.provider)?.toLowerCase();
24
+ const model = optionalString(config.model);
25
+ const baseUrl = optionalString(config.baseUrl)?.replace(/\/+$/, "");
26
+ const apiKeyEnv = optionalString(config.apiKeyEnv);
27
+ const contextLength = Number.parseInt(String(config.contextLength || ""), 10);
28
+ const reasoningEffort = optionalString(config.reasoningEffort);
29
+
30
+ if (!BACKENDS.has(backend)) throw new Error(`unsupported backend: ${backend}`);
31
+ if (provider && !PROVIDERS.has(provider)) throw new Error(`unsupported provider: ${provider}`);
32
+ if (baseUrl && provider !== "custom") throw new Error("baseUrl is only supported for the custom provider");
33
+ if (apiKeyEnv && !/^[A-Z][A-Z0-9_]*$/.test(apiKeyEnv)) throw new Error("api key env must be an uppercase environment variable name");
34
+ if (provider === "custom" && !baseUrl) throw new Error("custom provider requires baseUrl");
35
+ if (backend === "standalone" && !model) throw new Error("standalone backend requires a model");
36
+
37
+ return {
38
+ backend,
39
+ ...(provider ? { provider } : {}),
40
+ ...(model ? { model } : {}),
41
+ ...(baseUrl ? { baseUrl } : {}),
42
+ ...(apiKeyEnv ? { apiKeyEnv } : {}),
43
+ ...(Number.isFinite(contextLength) && contextLength > 0 ? { contextLength } : {}),
44
+ ...(reasoningEffort ? { reasoningEffort } : {}),
45
+ };
46
+ }
47
+
48
+ export function readModelConfig() {
49
+ const file = modelConfigPath();
50
+ if (!fs.existsSync(file)) return null;
51
+ try {
52
+ return normalize(JSON.parse(fs.readFileSync(file, "utf8")));
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ export function writeModelConfig(config) {
59
+ const normalized = normalize(config);
60
+ const file = modelConfigPath();
61
+ ensureDir(path.dirname(file));
62
+ if (process.platform !== "win32") fs.chmodSync(path.dirname(file), 0o700);
63
+ const temp = `${file}.${process.pid}.tmp`;
64
+ fs.writeFileSync(temp, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
65
+ fs.renameSync(temp, file);
66
+ if (process.platform !== "win32") fs.chmodSync(file, 0o600);
67
+ return normalized;
68
+ }
69
+
70
+ export default { modelConfigPath, readModelConfig, writeModelConfig };
@@ -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
@@ -9,6 +9,7 @@ import { httpJson } from "../http.mjs";
9
9
  import { solanaPubkey } from "./solana-rpc.mjs";
10
10
  import { resolveProviderEndpoint, credentialedHeaders } from "../provider-endpoint.mjs";
11
11
  import { stampPrepared } from "../../prepare-envelope.mjs";
12
+ import { assertNoSignedMaterial } from "./signed-material-guard.mjs";
12
13
 
13
14
  export const MAGICEDEN_SOL_API = "https://api-mainnet.magiceden.dev/v2";
14
15
  export const LAMPORTS_PER_SOL = 1_000_000_000;
@@ -242,6 +243,10 @@ export async function magicEdenSolPrepareBuy(args = {}, opts = {}) {
242
243
  fetchImpl: opts.fetchImpl,
243
244
  timeoutMs: opts.timeoutMs ?? 15_000,
244
245
  });
246
+ // Refuse pre-signed material by PRESENCE, before selecting unsigned bytes.
247
+ // A mixed payload (valid unsigned bytes plus a signed sibling) previously
248
+ // skipped this check entirely and bound the signed field into the envelope.
249
+ assertNoSignedMaterial("magiceden", raw);
245
250
  const data = raw?.v0?.tx?.data ?? raw?.tx?.data ?? null;
246
251
  // Accept ONLY the documented unsigned instruction field. A pre-signed payload
247
252
  // (txSigned) from a compromised or impersonated API must never be laundered
@@ -299,6 +304,10 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
299
304
  fetchImpl: opts.fetchImpl,
300
305
  timeoutMs: opts.timeoutMs ?? 15_000,
301
306
  });
307
+ // Refuse pre-signed material by PRESENCE, before selecting unsigned bytes.
308
+ // A mixed payload (valid unsigned bytes plus a signed sibling) previously
309
+ // skipped this check entirely and bound the signed field into the envelope.
310
+ assertNoSignedMaterial("magiceden", raw);
302
311
  const data = raw?.v0?.tx?.data ?? raw?.tx?.data ?? null;
303
312
  if (!Array.isArray(data)) {
304
313
  if (raw?.txSigned) throw new Error("magiceden: refused a pre-signed transaction payload from the API");
@@ -365,6 +374,10 @@ export async function magicEdenSolPrepareMint(args = {}, opts = {}) {
365
374
  );
366
375
  }
367
376
 
377
+ // Refuse pre-signed material by PRESENCE, before selecting unsigned bytes.
378
+ // A mixed payload (valid unsigned bytes plus a signed sibling) previously
379
+ // skipped this check entirely and bound the signed field into the envelope.
380
+ assertNoSignedMaterial("magiceden", raw);
368
381
  const data = raw?.v0?.tx?.data ?? raw?.tx?.data ?? null;
369
382
  if (!Array.isArray(data)) {
370
383
  if (raw?.txSigned) throw new Error("magiceden: refused a pre-signed transaction payload from the API");
@@ -5,6 +5,7 @@
5
5
  import { Interface, getAddress, isAddress, keccak256, toUtf8Bytes } from "ethers";
6
6
  import { httpJson } from "../http.mjs";
7
7
  import { rpcCall as defaultRpcCall } from "./evm-rpc.mjs";
8
+ import { stampPrepared } from "../../prepare-envelope.mjs";
8
9
 
9
10
  export const MORPHO_API = "https://api.morpho.org/graphql";
10
11
 
@@ -146,11 +147,14 @@ export async function morphoPrepareVault(args = {}, opts = {}) {
146
147
  };
147
148
  vaultGuard.guardHash = keccak256(toUtf8Bytes(canonicalJson(vaultGuard)));
148
149
 
149
- return {
150
+ return stampPrepared({
150
151
  provider: "morpho",
151
152
  protocol: "erc4626-vault",
152
153
  vaultReady: true,
153
154
  executionReady: false,
155
+ signingReady: false,
156
+ broadcastReady: false,
157
+ requiresUserSignature: true,
154
158
  chainId,
155
159
  action,
156
160
  vault,
@@ -169,5 +173,5 @@ export async function morphoPrepareVault(args = {}, opts = {}) {
169
173
  value: "0x0",
170
174
  madVault: vaultGuard,
171
175
  },
172
- };
176
+ }, { provider: "morpho", kind: "erc4626-vault" });
173
177
  }
@@ -2,6 +2,7 @@
2
2
  import { getAddress } from "ethers";
3
3
  import { httpJson } from "../http.mjs";
4
4
  import { attachAutoSlippage, bindAutoSlippageGuardToCall, resolveAutoSlippage } from "../../auto-slippage.mjs";
5
+ import { stampPrepared } from "../../prepare-envelope.mjs";
5
6
 
6
7
  export const PENDLE_API = "https://api-v2.pendle.finance/core";
7
8
 
@@ -154,7 +155,7 @@ export async function pendlePrepare(q = {}, opts = {}) {
154
155
  priceChange5m: q.priceChange5m,
155
156
  requestedCapBps: q.maxSlippageBps ?? q.slippageBps,
156
157
  });
157
- return {
158
+ return stampPrepared({
158
159
  provider: "pendle",
159
160
  calldataReady: true,
160
161
  // NOT executable authority. The bytes are assembled but nothing is signed:
@@ -177,7 +178,7 @@ export async function pendlePrepare(q = {}, opts = {}) {
177
178
  value: tx.value ?? "0x0",
178
179
  slippageGuard: bindAutoSlippageGuardToCall(quote.autoSlippage, { chainId, venue: tx.to, data: tx.data }),
179
180
  },
180
- };
181
+ }, { provider: "pendle", kind: "pendle-swap" });
181
182
  }
182
183
 
183
184
  export async function pendleQuote(q = {}, opts = {}) {
@@ -8,6 +8,7 @@
8
8
  import { btcAddress } from "./bitcoin-esplora.mjs";
9
9
  import { resolveProviderEndpoint, credentialedHeaders } from "../provider-endpoint.mjs";
10
10
  import { stampPrepared } from "../../prepare-envelope.mjs";
11
+ import { assertNoSignedMaterial } from "./signed-material-guard.mjs";
11
12
 
12
13
  export const SATFLOW_BASE = "https://api.satflow.com/v1";
13
14
 
@@ -295,6 +296,8 @@ export async function satflowPreparePurchase(args = {}, opts = {}) {
295
296
  throw new Error(`satflow: purchase price ${settled} sats exceeds maxSats cap ${maxSats} — PSBT withheld`);
296
297
  }
297
298
 
299
+ assertNoSignedMaterial("satflow", data);
300
+
298
301
  return stampPrepared({
299
302
  provider: "satflow",
300
303
  kind: "purchase-intent",
@@ -312,7 +315,6 @@ export async function satflowPreparePurchase(args = {}, opts = {}) {
312
315
  data?.unsignedPSBTBase64 ||
313
316
  data?.unsignedPsbtBase64 ||
314
317
  data?.unsigned_psbt ||
315
- data?.psbt ||
316
318
  null,
317
319
  note: "Sign PSBT in a Bitcoin wallet. Broadcast only through the user's wallet or an explicitly armed Bitcoin execution plane.",
318
320
  });
@@ -325,6 +327,7 @@ export async function satflowPrepareList(args = {}, opts = {}) {
325
327
  throw new Error("satflow: list requires inscription_id or runes_output");
326
328
  }
327
329
  const data = await sfFetch("/intent/sell", { method: "POST", body, opts });
330
+ assertNoSignedMaterial("satflow", data);
328
331
  return stampPrepared({
329
332
  provider: "satflow",
330
333
  kind: "list-intent",