@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.
- package/README.md +15 -10
- package/bin/oracle-data-mcp.mjs +9 -0
- package/docs/architecture.md +3 -1
- package/docs/profiles.md +3 -6
- package/docs/public-surface.md +133 -0
- package/package.json +3 -2
- package/public/oracle-splash/assets/cli-chain-hyperliquid.jpg +0 -0
- package/public/oracle-splash/brand/aave.webp +0 -0
- package/public/oracle-splash/brand/aavev2.webp +0 -0
- package/public/oracle-splash/brand/aborean.webp +0 -0
- package/public/oracle-splash/brand/across.webp +0 -0
- package/public/oracle-splash/brand/aerodrome.webp +0 -0
- package/public/oracle-splash/brand/afi.webp +0 -0
- package/public/oracle-splash/brand/arcus.webp +0 -0
- package/public/oracle-splash/brand/aster.webp +0 -0
- package/public/oracle-splash/brand/avant.webp +0 -0
- package/public/oracle-splash/brand/balancer.webp +0 -0
- package/public/oracle-splash/brand/beefy.webp +0 -0
- package/public/oracle-splash/brand/benqi.webp +0 -0
- package/public/oracle-splash/brand/bestinslot.ico +0 -0
- package/public/oracle-splash/brand/bitfi.webp +0 -0
- package/public/oracle-splash/brand/bonkswap.webp +0 -0
- package/public/oracle-splash/brand/bouncebit.webp +0 -0
- package/public/oracle-splash/brand/camelot.webp +0 -0
- package/public/oracle-splash/brand/compound.webp +0 -0
- package/public/oracle-splash/brand/concrete.webp +0 -0
- package/public/oracle-splash/brand/cow.webp +0 -0
- package/public/oracle-splash/brand/curve.webp +0 -0
- package/public/oracle-splash/brand/derive.webp +0 -0
- package/public/oracle-splash/brand/dolomite.webp +0 -0
- package/public/oracle-splash/brand/eigenlayer.webp +0 -0
- package/public/oracle-splash/brand/ekubo.webp +0 -0
- package/public/oracle-splash/brand/ethena.webp +0 -0
- package/public/oracle-splash/brand/etherfi.webp +0 -0
- package/public/oracle-splash/brand/etherfiborrow.webp +0 -0
- package/public/oracle-splash/brand/felix.webp +0 -0
- package/public/oracle-splash/brand/fluid.webp +0 -0
- package/public/oracle-splash/brand/fluxbeam.webp +0 -0
- package/public/oracle-splash/brand/folks.webp +0 -0
- package/public/oracle-splash/brand/fortyacres.webp +0 -0
- package/public/oracle-splash/brand/gainsnetwork.webp +0 -0
- package/public/oracle-splash/brand/gassss.webp +0 -0
- package/public/oracle-splash/brand/gmx.webp +0 -0
- package/public/oracle-splash/brand/harmonix.webp +0 -0
- package/public/oracle-splash/brand/hlorderbook.webp +0 -0
- package/public/oracle-splash/brand/hlp.webp +0 -0
- package/public/oracle-splash/brand/hop.webp +0 -0
- package/public/oracle-splash/brand/hyperbeat.webp +0 -0
- package/public/oracle-splash/brand/hyperlend.webp +0 -0
- package/public/oracle-splash/brand/hyperliquid.webp +0 -0
- package/public/oracle-splash/brand/hyperliquidhpl.webp +0 -0
- package/public/oracle-splash/brand/hyperliquidspot.webp +0 -0
- package/public/oracle-splash/brand/hyperswap.webp +0 -0
- package/public/oracle-splash/brand/hypha.webp +0 -0
- package/public/oracle-splash/brand/joedex.webp +0 -0
- package/public/oracle-splash/brand/jupiter.webp +0 -0
- package/public/oracle-splash/brand/kelp.webp +0 -0
- package/public/oracle-splash/brand/kinetiq.webp +0 -0
- package/public/oracle-splash/brand/kmhype.webp +0 -0
- package/public/oracle-splash/brand/kona.webp +0 -0
- package/public/oracle-splash/brand/konalend.webp +0 -0
- package/public/oracle-splash/brand/lfj.webp +0 -0
- package/public/oracle-splash/brand/lido.webp +0 -0
- package/public/oracle-splash/brand/lifi.webp +0 -0
- package/public/oracle-splash/brand/lighter.webp +0 -0
- package/public/oracle-splash/brand/lista.webp +0 -0
- package/public/oracle-splash/brand/loopedhype.webp +0 -0
- package/public/oracle-splash/brand/lorenzo.webp +0 -0
- package/public/oracle-splash/brand/magic-eden.webp +0 -0
- package/public/oracle-splash/brand/magiceden.webp +0 -0
- package/public/oracle-splash/brand/mempool.png +0 -0
- package/public/oracle-splash/brand/meteora.webp +0 -0
- package/public/oracle-splash/brand/morpho.webp +0 -0
- package/public/oracle-splash/brand/myriad.webp +0 -0
- package/public/oracle-splash/brand/noxa.webp +0 -0
- package/public/oracle-splash/brand/odos.webp +0 -0
- package/public/oracle-splash/brand/oneinch.webp +0 -0
- package/public/oracle-splash/brand/openbook.webp +0 -0
- package/public/oracle-splash/brand/opensea.webp +0 -0
- package/public/oracle-splash/brand/orca.webp +0 -0
- package/public/oracle-splash/brand/ordinals.png +0 -0
- package/public/oracle-splash/brand/ostium.webp +0 -0
- package/public/oracle-splash/brand/pancakeswap.webp +0 -0
- package/public/oracle-splash/brand/pendle.webp +0 -0
- package/public/oracle-splash/brand/perena.webp +0 -0
- package/public/oracle-splash/brand/pharaoh.webp +0 -0
- package/public/oracle-splash/brand/phoenix.webp +0 -0
- package/public/oracle-splash/brand/polymarket.webp +0 -0
- package/public/oracle-splash/brand/polynomial.webp +0 -0
- package/public/oracle-splash/brand/projectx.webp +0 -0
- package/public/oracle-splash/brand/pumpfun.webp +0 -0
- package/public/oracle-splash/brand/quickswap.webp +0 -0
- package/public/oracle-splash/brand/rain.webp +0 -0
- package/public/oracle-splash/brand/raydium.webp +0 -0
- package/public/oracle-splash/brand/relay.webp +0 -0
- package/public/oracle-splash/brand/rocketpool.webp +0 -0
- package/public/oracle-splash/brand/saber.webp +0 -0
- package/public/oracle-splash/brand/sakuraswap.webp +0 -0
- package/public/oracle-splash/brand/sanctum.webp +0 -0
- package/public/oracle-splash/brand/satflow.png +0 -0
- package/public/oracle-splash/brand/sierra.webp +0 -0
- package/public/oracle-splash/brand/sky.webp +0 -0
- package/public/oracle-splash/brand/solv.webp +0 -0
- package/public/oracle-splash/brand/spark.webp +0 -0
- package/public/oracle-splash/brand/sparksavings.webp +0 -0
- package/public/oracle-splash/brand/sphere.webp +0 -0
- package/public/oracle-splash/brand/ssv.webp +0 -0
- package/public/oracle-splash/brand/stabble.webp +0 -0
- package/public/oracle-splash/brand/stableswap.webp +0 -0
- package/public/oracle-splash/brand/stakewise.webp +0 -0
- package/public/oracle-splash/brand/stargate.webp +0 -0
- package/public/oracle-splash/brand/sthype.webp +0 -0
- package/public/oracle-splash/brand/sushiswap.webp +0 -0
- package/public/oracle-splash/brand/unisat.png +0 -0
- package/public/oracle-splash/brand/uniswap.webp +0 -0
- package/public/oracle-splash/brand/uniswapv2.webp +0 -0
- package/public/oracle-splash/brand/uniswapv4.webp +0 -0
- package/public/oracle-splash/brand/unit.webp +0 -0
- package/public/oracle-splash/brand/velodrome.webp +0 -0
- package/public/oracle-splash/brand/velora.webp +0 -0
- package/public/oracle-splash/brand/venus.webp +0 -0
- package/public/oracle-splash/brand/wbeth.webp +0 -0
- package/public/oracle-splash/brand/xsy.webp +0 -0
- package/public/oracle-splash/brand/yieldyak.webp +0 -0
- package/public/oracle-splash/brand/zerobase.webp +0 -0
- package/public/oracle-splash/brand/zerox.webp +0 -0
- package/public/oracle-splash/index.html +337 -197
- package/scripts/check-doc-drift.mjs +11 -0
- package/scripts/e2e-solana-bitcoin.mjs +1 -1
- package/scripts/secret-scan.mjs +1 -1
- package/src/action-receipts.mjs +108 -11
- package/src/auth/oauth.mjs +672 -0
- package/src/cli/commands/auth.mjs +193 -0
- package/src/cli/commands/bootstrap.mjs +2 -2
- package/src/cli/commands/chat.mjs +97 -75
- package/src/cli/commands/data.mjs +3 -1
- package/src/cli/commands/doctor.mjs +11 -1
- package/src/cli/commands/model.mjs +79 -23
- package/src/cli/kernel.mjs +4 -3
- package/src/cli/model-config.mjs +70 -0
- package/src/data/catalog.mjs +38 -7
- package/src/data/desk-data.mjs +12 -0
- package/src/data/providers/evm-rpc.mjs +19 -16
- package/src/data/providers/jupiter-venues.mjs +119 -0
- package/src/data/providers/jupiter.mjs +132 -0
- package/src/data/providers/magiceden-sol.mjs +13 -0
- package/src/data/providers/morpho.mjs +6 -2
- package/src/data/providers/pendle.mjs +3 -2
- package/src/data/providers/satflow.mjs +4 -1
- package/src/data/providers/signed-material-guard.mjs +108 -0
- package/src/data/providers/uniswap-v3.mjs +34 -5
- package/src/data/providers/uniswap-v4.mjs +414 -0
- package/src/prepare-envelope.mjs +22 -4
- package/src/router/prepare-route.mjs +17 -0
- package/src/router/route-sources.mjs +35 -0
- package/src/tui/app.mjs +18 -7
- package/src/tui/backend.mjs +145 -0
- package/src/tui/standalone-client.mjs +666 -0
- package/src/cards.mjs +0 -369
- 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 };
|
package/src/data/catalog.mjs
CHANGED
|
@@ -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:
|
|
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:
|
|
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",
|
package/src/data/desk-data.mjs
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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",
|