@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.
- package/README.md +21 -12
- 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 +1 -1
- 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 +236 -169
- 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/cli/commands/data.mjs +3 -1
- package/src/cli/commands/doctor.mjs +11 -1
- 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/action-receipts.mjs
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
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
|
|
49
|
-
const childPath = `${path}.${
|
|
50
|
-
if (isSecretKey(
|
|
51
|
-
if (value[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
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
|