@oracle-agent/oracle 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CONTRIBUTING.md +98 -0
  2. package/LICENSE +202 -0
  3. package/README.md +384 -0
  4. package/SECURITY.md +89 -0
  5. package/SETUP.md +235 -0
  6. package/artifacts/inscription/oracle-was-here-preview.png +0 -0
  7. package/artifacts/inscription/oracle-was-here.svg +1 -0
  8. package/artifacts/specialist-packs/oracle-full-crypto.json +273 -0
  9. package/bin/desk-server.mjs +438 -0
  10. package/bin/oracle-data-mcp.mjs +603 -0
  11. package/bin/oracle-init.mjs +423 -0
  12. package/bin/oracle-public-server.mjs +36 -0
  13. package/bin/oracle-route.mjs +254 -0
  14. package/bin/oracle-scan.mjs +192 -0
  15. package/docs/adding-a-chain.md +229 -0
  16. package/docs/architecture.md +135 -0
  17. package/docs/profiles.md +132 -0
  18. package/examples/add-a-chain.mjs +65 -0
  19. package/examples/research-a-token.mjs +70 -0
  20. package/package.json +90 -0
  21. package/profiles/_template/SOUL.md +47 -0
  22. package/profiles/_template/profile.json +22 -0
  23. package/profiles/bitcoin-agent/SOUL.md +31 -0
  24. package/profiles/bitcoin-agent/profile.json +32 -0
  25. package/profiles/hyperliquid-agent/SOUL.md +34 -0
  26. package/profiles/hyperliquid-agent/profile.json +37 -0
  27. package/profiles/oracle/SOUL.md +65 -0
  28. package/profiles/oracle/profile.json +35 -0
  29. package/profiles/polymarket-agent/SOUL.md +35 -0
  30. package/profiles/polymarket-agent/profile.json +34 -0
  31. package/profiles/profile.schema.json +90 -0
  32. package/profiles/protocol-builder/SOUL.md +50 -0
  33. package/profiles/protocol-builder/profile.json +37 -0
  34. package/profiles/robinhood-agent/SOUL.md +45 -0
  35. package/profiles/robinhood-agent/profile.json +39 -0
  36. package/profiles/solana-agent/SOUL.md +37 -0
  37. package/profiles/solana-agent/profile.json +37 -0
  38. package/profiles/stable-agent/SOUL.md +43 -0
  39. package/profiles/stable-agent/profile.json +37 -0
  40. package/public/oracle-console/app.js +272 -0
  41. package/public/oracle-console/bitcoin-wallets.js +206 -0
  42. package/public/oracle-console/index.html +91 -0
  43. package/public/oracle-console/styles.css +239 -0
  44. package/public/oracle-splash/index.html +931 -0
  45. package/scripts/build-inscription.py +230 -0
  46. package/scripts/check-test-count.mjs +105 -0
  47. package/scripts/e2e-hl-markets.mjs +21 -0
  48. package/scripts/e2e-hl-perps.mjs +48 -0
  49. package/scripts/e2e-hypercore-staking.mjs +128 -0
  50. package/scripts/e2e-solana-bitcoin.mjs +183 -0
  51. package/scripts/public-api-scan.mjs +23 -0
  52. package/scripts/secret-scan.mjs +181 -0
  53. package/scripts/verify-v3-venues.mjs +192 -0
  54. package/skills/oracle-best-execution/SKILL.md +127 -0
  55. package/skills/oracle-bitcoin/SKILL.md +53 -0
  56. package/skills/oracle-chain-graphs-telegram-cards/SKILL.md +59 -0
  57. package/skills/oracle-circuit-breaker/SKILL.md +51 -0
  58. package/skills/oracle-contract-research/SKILL.md +55 -0
  59. package/skills/oracle-desk/SKILL.md +58 -0
  60. package/skills/oracle-dex-launch/SKILL.md +38 -0
  61. package/skills/oracle-grants/SKILL.md +69 -0
  62. package/skills/oracle-hypercore-staking/SKILL.md +57 -0
  63. package/skills/oracle-hyperliquid/SKILL.md +56 -0
  64. package/skills/oracle-meme-token-sniper/SKILL.md +73 -0
  65. package/skills/oracle-nft-gacha-launch/SKILL.md +48 -0
  66. package/skills/oracle-nft-mint-gas-war/SKILL.md +63 -0
  67. package/skills/oracle-polymarket/SKILL.md +60 -0
  68. package/skills/oracle-protocol-builder/SKILL.md +38 -0
  69. package/skills/oracle-protocol-security/SKILL.md +60 -0
  70. package/skills/oracle-public-product/SKILL.md +44 -0
  71. package/skills/oracle-receipts/SKILL.md +52 -0
  72. package/skills/oracle-rfq-tokenized-assets/SKILL.md +69 -0
  73. package/skills/oracle-smart-wallet-scanner/SKILL.md +49 -0
  74. package/skills/oracle-solana/SKILL.md +65 -0
  75. package/skills/oracle-solana-nft/SKILL.md +54 -0
  76. package/skills/oracle-token-research/SKILL.md +67 -0
  77. package/src/agent-auth.mjs +191 -0
  78. package/src/approval-guard.mjs +282 -0
  79. package/src/attestation-secret.mjs +88 -0
  80. package/src/audit-log.mjs +196 -0
  81. package/src/auto-slippage.mjs +378 -0
  82. package/src/capability-posture.mjs +125 -0
  83. package/src/chains.mjs +62 -0
  84. package/src/data/catalog.mjs +495 -0
  85. package/src/data/desk-data.mjs +623 -0
  86. package/src/data/http.mjs +200 -0
  87. package/src/data/provider-endpoint.mjs +94 -0
  88. package/src/data/providers/aerodrome.mjs +244 -0
  89. package/src/data/providers/balancer.mjs +208 -0
  90. package/src/data/providers/bitcoin-esplora.mjs +230 -0
  91. package/src/data/providers/bitcoin-meta.mjs +378 -0
  92. package/src/data/providers/blockscout.mjs +14 -0
  93. package/src/data/providers/bridges.mjs +241 -0
  94. package/src/data/providers/cowswap.mjs +501 -0
  95. package/src/data/providers/curve.mjs +200 -0
  96. package/src/data/providers/defillama.mjs +88 -0
  97. package/src/data/providers/dexscreener.mjs +43 -0
  98. package/src/data/providers/evm-rpc.mjs +203 -0
  99. package/src/data/providers/geckoterminal.mjs +34 -0
  100. package/src/data/providers/gmx.mjs +495 -0
  101. package/src/data/providers/hl-info.mjs +102 -0
  102. package/src/data/providers/hl-markets.mjs +210 -0
  103. package/src/data/providers/hl-perps.mjs +382 -0
  104. package/src/data/providers/hl-staking.mjs +352 -0
  105. package/src/data/providers/hl-ws.mjs +119 -0
  106. package/src/data/providers/hyperevm-dex.mjs +49 -0
  107. package/src/data/providers/jupiter.mjs +182 -0
  108. package/src/data/providers/lifi.mjs +150 -0
  109. package/src/data/providers/magiceden-sol.mjs +355 -0
  110. package/src/data/providers/morpho.mjs +173 -0
  111. package/src/data/providers/odos.mjs +155 -0
  112. package/src/data/providers/oneinch.mjs +173 -0
  113. package/src/data/providers/opensea-multichain.mjs +136 -0
  114. package/src/data/providers/opensea-nft.mjs +99 -0
  115. package/src/data/providers/paraswap.mjs +117 -0
  116. package/src/data/providers/pendle.mjs +187 -0
  117. package/src/data/providers/poly-public.mjs +96 -0
  118. package/src/data/providers/poly-ws.mjs +103 -0
  119. package/src/data/providers/rh-agent.mjs +59 -0
  120. package/src/data/providers/satflow.mjs +336 -0
  121. package/src/data/providers/solana-rpc.mjs +186 -0
  122. package/src/data/providers/uniswap-v3.mjs +303 -0
  123. package/src/data/providers/zerox.mjs +166 -0
  124. package/src/data/public-api-scan.mjs +61 -0
  125. package/src/data/quote-placeholder.mjs +31 -0
  126. package/src/exact-integer.mjs +72 -0
  127. package/src/exec-policy.mjs +444 -0
  128. package/src/flags.mjs +15 -0
  129. package/src/fresh-window.mjs +76 -0
  130. package/src/gmx-attestation.mjs +175 -0
  131. package/src/index.mjs +50 -0
  132. package/src/nft-gas-war-guard.mjs +139 -0
  133. package/src/onboarding/agent-keys.mjs +157 -0
  134. package/src/onboarding/index.mjs +18 -0
  135. package/src/onboarding/tiers.mjs +139 -0
  136. package/src/oracle-env.mjs +38 -0
  137. package/src/protocol-execution.mjs +84 -0
  138. package/src/public-api/buzz-integration.mjs +256 -0
  139. package/src/public-api/connect-agent.mjs +397 -0
  140. package/src/public-api/grants.mjs +142 -0
  141. package/src/public-api/http.mjs +374 -0
  142. package/src/public-control/aa-adapter.mjs +402 -0
  143. package/src/public-control/build-registry.mjs +227 -0
  144. package/src/public-control/bundler-client.mjs +372 -0
  145. package/src/public-control/grant-indexer.mjs +296 -0
  146. package/src/public-control/policy-render.mjs +69 -0
  147. package/src/public-control/policy-schema.mjs +318 -0
  148. package/src/public-control/runtime-config.mjs +265 -0
  149. package/src/public-control/session-key-model.mjs +374 -0
  150. package/src/public-control/session-orchestrator.mjs +412 -0
  151. package/src/route-attestation.mjs +132 -0
  152. package/src/router/best-execution.mjs +221 -0
  153. package/src/router/index.mjs +185 -0
  154. package/src/router/prepare-bridge.mjs +288 -0
  155. package/src/router/prepare-route.mjs +341 -0
  156. package/src/router/proposal.mjs +311 -0
  157. package/src/router/risk-classifier.mjs +119 -0
  158. package/src/router/route-sources.mjs +292 -0
  159. package/src/scanner/chains.config.mjs +381 -0
  160. package/src/scanner/contract.mjs +270 -0
  161. package/src/scanner/evm-scanner.mjs +394 -0
  162. package/src/scanner/index.mjs +9 -0
  163. package/src/scanner/v2-venue.mjs +335 -0
  164. package/src/scanner/v3-venue.mjs +290 -0
  165. package/src/scopes.mjs +44 -0
  166. package/src/sell-simulation.mjs +167 -0
  167. package/src/token-transfer-guard.mjs +188 -0
  168. package/src/vault-attestation.mjs +145 -0
  169. package/src/venues.mjs +206 -0
@@ -0,0 +1,182 @@
1
+ // Jupiter Swap API lane for Solana. Prepare-only: returns a serialized
2
+ // transaction for the user's Solana wallet to sign. No local signer, no broadcast.
3
+
4
+ import { httpJson } from "../http.mjs";
5
+ import { solanaPubkey } from "./solana-rpc.mjs";
6
+
7
+ export const JUPITER_SWAP_API = "https://lite-api.jup.ag/swap/v1";
8
+ export const SOL_MINT = "So11111111111111111111111111111111111111112";
9
+ export const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
10
+ export const JUPITER_MAX_SLIPPAGE_BPS = 100;
11
+ const DEFAULT_QUOTE_TTL_MS = 20_000;
12
+
13
+ function baseUrl(opts = {}) {
14
+ return String(opts.baseUrl || process.env.JUPITER_SWAP_API_URL || JUPITER_SWAP_API).replace(/\/$/, "");
15
+ }
16
+
17
+ function amountString(value, label = "amount") {
18
+ try {
19
+ const n = BigInt(String(value));
20
+ if (n <= 0n) throw new Error("nonpositive");
21
+ return n.toString();
22
+ } catch {
23
+ throw new Error(`jupiter: ${label} must be a positive integer string`);
24
+ }
25
+ }
26
+
27
+ function slippageBps(value) {
28
+ const n = value == null ? 50 : Number(value);
29
+ if (!Number.isInteger(n) || n < 0) throw new Error("jupiter: slippageBps must be a non-negative integer");
30
+ if (n > JUPITER_MAX_SLIPPAGE_BPS) throw new Error("jupiter: slippage exceeds hard 100 bps cap");
31
+ return n;
32
+ }
33
+
34
+ function boolDefault(value, fallback) {
35
+ return value == null ? fallback : Boolean(value);
36
+ }
37
+
38
+ function base64Tx(value) {
39
+ const text = String(value || "").trim();
40
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(text) || text.length % 4 !== 0) throw new Error("jupiter: swapTransaction must be base64");
41
+ return text;
42
+ }
43
+
44
+ function solanaGuard(quote = {}, { quotedAtMs = Date.now(), ttlMs = DEFAULT_QUOTE_TTL_MS } = {}) {
45
+ const inputMint = solanaPubkey(quote.inputMint, "inputMint");
46
+ const outputMint = solanaPubkey(quote.outputMint, "outputMint");
47
+ const inAmount = amountString(quote.inAmount, "inAmount");
48
+ const outAmount = amountString(quote.outAmount, "outAmount");
49
+ const otherAmountThreshold = amountString(quote.otherAmountThreshold ?? quote.outAmount, "otherAmountThreshold");
50
+ const bps = slippageBps(quote.slippageBps ?? 50);
51
+ return {
52
+ mode: "solana-jupiter-quote",
53
+ provider: "jupiter",
54
+ venue: "jupiter-swap-v1",
55
+ inputMint,
56
+ outputMint,
57
+ inAmount,
58
+ outAmount,
59
+ otherAmountThreshold,
60
+ slippageBps: bps,
61
+ contextSlot: quote.contextSlot ?? null,
62
+ quotedAtMs,
63
+ expiresAtMs: quotedAtMs + Math.min(Math.max(Number(ttlMs) || DEFAULT_QUOTE_TTL_MS, 1_000), 30_000),
64
+ };
65
+ }
66
+
67
+ export async function jupiterHealth(opts = {}) {
68
+ try {
69
+ const q = await jupiterQuote({ inputMint: SOL_MINT, outputMint: USDC_MINT, amount: "1000000", slippageBps: 50 }, opts);
70
+ return { ok: true, provider: "jupiter", outAmount: q.quote.outAmount, exec: false };
71
+ } catch (error) {
72
+ return { ok: false, provider: "jupiter", error: String(error?.message || error), exec: false };
73
+ }
74
+ }
75
+
76
+ export async function jupiterQuote(args = {}, opts = {}) {
77
+ const inputMint = solanaPubkey(args.inputMint || args.fromMint || SOL_MINT, "inputMint");
78
+ const outputMint = solanaPubkey(args.outputMint || args.toMint || USDC_MINT, "outputMint");
79
+ const amount = amountString(args.amount || args.inAmount, "amount");
80
+ const bps = slippageBps(args.slippageBps ?? args.maxSlippageBps ?? 50);
81
+ const url = new URL(`${baseUrl(opts)}/quote`);
82
+ url.searchParams.set("inputMint", inputMint);
83
+ url.searchParams.set("outputMint", outputMint);
84
+ url.searchParams.set("amount", amount);
85
+ url.searchParams.set("slippageBps", String(bps));
86
+ url.searchParams.set("swapMode", args.swapMode || "ExactIn");
87
+ if (args.onlyDirectRoutes != null) url.searchParams.set("onlyDirectRoutes", String(Boolean(args.onlyDirectRoutes)));
88
+ if (args.platformFeeBps != null) url.searchParams.set("platformFeeBps", String(args.platformFeeBps));
89
+ const quote = await httpJson(url.toString(), {
90
+ fetchImpl: opts.fetchImpl,
91
+ timeoutMs: opts.timeoutMs ?? 12_000,
92
+ });
93
+ const guard = solanaGuard({ ...quote, slippageBps: bps }, opts);
94
+ return {
95
+ provider: "jupiter",
96
+ chain: "solana-mainnet-beta",
97
+ quoteReady: true,
98
+ executionReady: false,
99
+ inputMint,
100
+ outputMint,
101
+ amount,
102
+ quote,
103
+ solanaGuard: guard,
104
+ };
105
+ }
106
+
107
+ function compactBody(body) {
108
+ return Object.fromEntries(Object.entries(body).filter(([, v]) => v !== undefined));
109
+ }
110
+
111
+ export async function jupiterPrepareSwap(args = {}, opts = {}) {
112
+ const userPublicKey = solanaPubkey(args.userPublicKey || args.owner || args.wallet, "userPublicKey");
113
+ const quoteEnvelope = args.quoteResponse
114
+ ? { quote: args.quoteResponse, solanaGuard: solanaGuard(args.quoteResponse, opts) }
115
+ : await jupiterQuote(args, opts);
116
+ const quoteResponse = quoteEnvelope.quote;
117
+ const guard = quoteEnvelope.solanaGuard || solanaGuard(quoteResponse, opts);
118
+
119
+ // dynamicSlippage lets Jupiter widen the on-chain slippage floor at build
120
+ // time, which silently voids the hard bps ceiling the quote path enforced —
121
+ // the returned guard would still advertise the capped number. Refuse it
122
+ // rather than return a transaction whose real tolerance exceeds the cap.
123
+ if (boolDefault(args.dynamicSlippage, false) === true) {
124
+ throw new Error(
125
+ "jupiter: dynamicSlippage would override the enforced slippage ceiling — pass an explicit slippageBps instead"
126
+ );
127
+ }
128
+
129
+ const response = await httpJson(`${baseUrl(opts)}/swap`, {
130
+ method: "POST",
131
+ body: compactBody({
132
+ quoteResponse,
133
+ userPublicKey,
134
+ wrapAndUnwrapSol: boolDefault(args.wrapAndUnwrapSol, true),
135
+ dynamicComputeUnitLimit: boolDefault(args.dynamicComputeUnitLimit, true),
136
+ dynamicSlippage: false,
137
+ prioritizationFeeLamports: args.prioritizationFeeLamports,
138
+ }),
139
+ fetchImpl: opts.fetchImpl,
140
+ timeoutMs: opts.timeoutMs ?? 15_000,
141
+ });
142
+ const swapTransaction = base64Tx(response?.swapTransaction);
143
+
144
+ // Bind the returned transaction to the quote it claims to implement. A
145
+ // compromised or impersonated endpoint can return any bytes while Oracle
146
+ // stamps the optimistic quote on top; the user signs the bytes, not the
147
+ // guard. Jupiter echoes the quote it built from — require it to match.
148
+ const echoed = response?.quoteResponse ?? response?.quote ?? null;
149
+ if (echoed) {
150
+ for (const field of ["inAmount", "outAmount", "otherAmountThreshold", "inputMint", "outputMint"]) {
151
+ const want = quoteResponse?.[field];
152
+ const got = echoed?.[field];
153
+ if (want != null && got != null && String(want) !== String(got)) {
154
+ throw new Error(
155
+ `jupiter: swap response ${field} (${got}) does not match the quoted ${field} (${want}) — refusing to prepare`
156
+ );
157
+ }
158
+ }
159
+ }
160
+
161
+ return {
162
+ provider: "jupiter",
163
+ chain: "solana-mainnet-beta",
164
+ prepareReady: true,
165
+ executionReady: false,
166
+ signingReady: false,
167
+ broadcastReady: false,
168
+ requiresUserSignature: true,
169
+ // The prepared bytes are NOT verified against the quote beyond the fields
170
+ // Jupiter echoes. Simulate before signing.
171
+ verifiedAgainstQuote: Boolean(echoed),
172
+ userPublicKey,
173
+ quote: quoteResponse,
174
+ solanaGuard: guard,
175
+ swapTransaction,
176
+ swapTransactionEncoding: "base64",
177
+ lastValidBlockHeight: response?.lastValidBlockHeight ?? null,
178
+ prioritizationFeeLamports: response?.prioritizationFeeLamports ?? null,
179
+ computeUnitLimit: response?.computeUnitLimit ?? null,
180
+ raw: response,
181
+ };
182
+ }
@@ -0,0 +1,150 @@
1
+ // LI.FI public quote API — no API key required for basic quotes.
2
+
3
+ import { httpJson } from "../http.mjs";
4
+ import { attachAutoSlippage, bindAutoSlippageGuardToCall, resolveAutoSlippage } from "../../auto-slippage.mjs";
5
+ import { QUOTE_PLACEHOLDER_ADDRESS, quoteAddress } from "../quote-placeholder.mjs";
6
+
7
+ export const LIFI_API = "https://li.quest/v1";
8
+
9
+ function base(opts = {}) {
10
+ return (opts.baseUrl || process.env.LIFI_API_URL || LIFI_API).replace(/\/$/, "");
11
+ }
12
+
13
+ const NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
14
+ const ZERO = "0x0000000000000000000000000000000000000000";
15
+
16
+ function isNativeToken(value) {
17
+ const token = String(value || NATIVE).toLowerCase();
18
+ return token === NATIVE.toLowerCase() || token === ZERO;
19
+ }
20
+
21
+ function address(value, label) {
22
+ const text = String(value || "").trim();
23
+ if (!/^0x[0-9a-fA-F]{40}$/.test(text)) throw new Error(`lifi: ${label} must be a 20-byte address`);
24
+ return text.toLowerCase();
25
+ }
26
+
27
+ export async function lifiHealth(opts = {}) {
28
+ // cheap chains list
29
+ const chains = await httpJson(`${base(opts)}/chains`, {
30
+ fetchImpl: opts.fetchImpl,
31
+ timeoutMs: opts.timeoutMs ?? 12_000,
32
+ });
33
+ const n = Array.isArray(chains) ? chains.length : chains?.chains?.length || 0;
34
+ return { ok: n > 0, chainCount: n };
35
+ }
36
+
37
+ /**
38
+ * @param {object} q
39
+ * @param {number|string} q.fromChain
40
+ * @param {number|string} q.toChain
41
+ * @param {string} q.fromToken
42
+ * @param {string} q.toToken
43
+ * @param {string} q.fromAmount wei/base units
44
+ * @param {string} [q.fromAddress]
45
+ */
46
+ export async function lifiQuote(q = {}, opts = {}) {
47
+ const {
48
+ fromChain,
49
+ toChain = fromChain,
50
+ fromToken = NATIVE,
51
+ toToken,
52
+ fromAmount,
53
+ fromAddress = QUOTE_PLACEHOLDER_ADDRESS,
54
+ } = q;
55
+ if (fromChain == null || !toToken || !fromAmount) {
56
+ throw new Error("lifiQuote requires fromChain, toToken, fromAmount");
57
+ }
58
+ const params = new URLSearchParams({
59
+ fromChain: String(fromChain),
60
+ toChain: String(toChain),
61
+ fromToken,
62
+ toToken,
63
+ fromAmount: String(fromAmount),
64
+ fromAddress,
65
+ });
66
+ const selected = resolveAutoSlippage({
67
+ liquidityUsd: q.liquidityUsd,
68
+ priceChange5m: q.priceChange5m,
69
+ crossChain: Number(toChain) !== Number(fromChain),
70
+ requestedCapBps: q.maxSlippageBps ?? q.slippageBps,
71
+ });
72
+ if (!selected.executable) {
73
+ throw new Error(`lifi: route requires ${selected.requiredBps} bps above cap ${selected.capBps}`);
74
+ }
75
+ // LI.FI expects a decimal fraction: 0.005 = 50 bps.
76
+ params.set("slippage", String(selected.selectedBps / 10_000));
77
+ const result = await httpJson(`${base(opts)}/quote?${params}`, {
78
+ fetchImpl: opts.fetchImpl,
79
+ timeoutMs: opts.timeoutMs ?? 20_000,
80
+ });
81
+ const amountOut = result?.estimate?.toAmount || result?.toAmount;
82
+ if (!amountOut) return { ...result, autoSlippagePolicy: selected };
83
+ const venue = result?.transactionRequest?.to || result?.estimate?.approvalAddress || "lifi";
84
+ const guarded = attachAutoSlippage(result, {
85
+ chainId: Number(fromChain),
86
+ venue,
87
+ amountOut,
88
+ liquidityUsd: q.liquidityUsd,
89
+ priceChange5m: q.priceChange5m,
90
+ crossChain: Number(toChain) !== Number(fromChain),
91
+ requestedCapBps: q.maxSlippageBps ?? q.slippageBps,
92
+ });
93
+ const providerMin = result?.estimate?.toAmountMin || result?.toAmountMin;
94
+ if (providerMin != null && BigInt(providerMin) < BigInt(guarded.amountOutMinimum)) {
95
+ throw new Error("lifi: provider transaction is under-protected versus bounded auto-slippage floor");
96
+ }
97
+ return guarded;
98
+ }
99
+
100
+ export async function lifiPrepare(q = {}, opts = {}) {
101
+ const quote = await lifiQuote(q, opts);
102
+ const tx = quote?.transactionRequest;
103
+ if (!tx?.to || !tx?.data) throw new Error("lifi: quote did not return an executable transactionRequest");
104
+ const chainId = Number(q.fromChain);
105
+ if (tx.chainId != null && Number(tx.chainId) !== chainId) throw new Error("lifi: transaction chain mismatch");
106
+ if (q.fromAddress && tx.from && String(tx.from).toLowerCase() !== String(q.fromAddress).toLowerCase()) {
107
+ throw new Error("lifi: transaction sender mismatch");
108
+ }
109
+ let requiresApproval = null;
110
+ if (!isNativeToken(q.fromToken)) {
111
+ const spender = quote?.estimate?.approvalAddress || quote?.approvalAddress;
112
+ if (!spender) throw new Error("lifi: ERC20 route missing approvalAddress");
113
+ requiresApproval = {
114
+ token: address(q.fromToken, "approval token"),
115
+ spender: address(spender, "approval spender"),
116
+ amount: String(q.fromAmount),
117
+ };
118
+ }
119
+ return {
120
+ provider: "lifi",
121
+ calldataReady: true,
122
+ // NOT executable authority. The bytes are assembled but nothing is signed:
123
+ // the user's wallet is still the only thing that can authorize this. The
124
+ // previous `executionReady: true` read as "cleared to execute" and trained
125
+ // agents to treat a quote as permission.
126
+ requiresUserSignature: true,
127
+ signingReady: false,
128
+ broadcastReady: false,
129
+ chainId,
130
+ quote,
131
+ autoSlippage: quote.autoSlippage,
132
+ requiresApproval,
133
+ transaction: {
134
+ chainId,
135
+ from: tx.from || q.fromAddress,
136
+ to: tx.to,
137
+ data: tx.data,
138
+ value: tx.value ?? "0x0",
139
+ ...(tx.gasLimit != null ? { gasLimit: tx.gasLimit } : {}),
140
+ slippageGuard: bindAutoSlippageGuardToCall(quote.autoSlippage, { chainId, venue: tx.to, data: tx.data }),
141
+ },
142
+ };
143
+ }
144
+
145
+ export async function lifiChains(opts = {}) {
146
+ return httpJson(`${base(opts)}/chains`, {
147
+ fetchImpl: opts.fetchImpl,
148
+ timeoutMs: opts.timeoutMs ?? 12_000,
149
+ });
150
+ }
@@ -0,0 +1,355 @@
1
+ // Magic Eden Solana NFT lane. Read + prepare only.
2
+ //
3
+ // Reads (collection stats, listings, activities) are keyless on the public
4
+ // mainnet API. The instruction builders (buy_now, list, mint) require a Magic
5
+ // Eden API key and return an UNSIGNED transaction the user's wallet signs.
6
+ // Nothing here signs or broadcasts.
7
+
8
+ import { httpJson } from "../http.mjs";
9
+ import { solanaPubkey } from "./solana-rpc.mjs";
10
+ import { resolveProviderEndpoint, credentialedHeaders } from "../provider-endpoint.mjs";
11
+
12
+ export const MAGICEDEN_SOL_API = "https://api-mainnet.magiceden.dev/v2";
13
+ export const LAMPORTS_PER_SOL = 1_000_000_000;
14
+
15
+ function endpoint(opts = {}) {
16
+ return resolveProviderEndpoint({
17
+ provider: "magiceden",
18
+ defaultUrl: MAGICEDEN_SOL_API,
19
+ hosts: ["api-mainnet.magiceden.dev", "api-devnet.magiceden.dev"],
20
+ url: opts.baseUrl || process.env.MAGICEDEN_API_URL,
21
+ });
22
+ }
23
+
24
+ function base(opts = {}) {
25
+ return endpoint(opts).baseUrl;
26
+ }
27
+
28
+ function apiKey(opts = {}) {
29
+ return opts.apiKey || process.env.MAGICEDEN_API_KEY || "";
30
+ }
31
+
32
+ function headers(opts = {}) {
33
+ // Bearer token goes only to a pinned Magic Eden host.
34
+ const key = apiKey(opts);
35
+ return credentialedHeaders(
36
+ { accept: "application/json" },
37
+ { Authorization: key ? `Bearer ${key}` : "" },
38
+ endpoint(opts).trusted
39
+ );
40
+ }
41
+
42
+ function symbolArg(value) {
43
+ const symbol = String(value || "").trim();
44
+ if (!/^[a-z0-9_]{1,64}$/i.test(symbol)) throw new Error("magiceden: symbol must be a collection slug");
45
+ return symbol;
46
+ }
47
+
48
+ /**
49
+ * Number() accepts "1e2", "0x10", " 12 " and Infinity. A price that parses
50
+ * differently from how it reads is a spend-control bug: the UI shows one value
51
+ * and the wallet is asked for another. Require a plain decimal.
52
+ */
53
+ function positiveNumber(value, label) {
54
+ if (typeof value === "number") {
55
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`magiceden: ${label} must be a positive number`);
56
+ return value;
57
+ }
58
+ const text = String(value ?? "").trim();
59
+ if (!/^\d+(\.\d+)?$/.test(text)) {
60
+ throw new Error(`magiceden: ${label} must be a plain decimal number (no hex, exponent, or sign)`);
61
+ }
62
+ const n = Number(text);
63
+ if (!Number.isFinite(n) || n <= 0) throw new Error(`magiceden: ${label} must be a positive number`);
64
+ return n;
65
+ }
66
+
67
+ /** Pull the launchpad's quoted mint price (SOL) out of an API response. */
68
+ function mintPriceSol(raw) {
69
+ const candidates = [
70
+ raw?.price,
71
+ raw?.mintPrice,
72
+ raw?.priceSol,
73
+ raw?.v0?.price,
74
+ raw?.launchStageInfo?.price,
75
+ raw?.stage?.price,
76
+ ];
77
+ for (const c of candidates) {
78
+ if (c == null) continue;
79
+ const n = Number(c);
80
+ if (Number.isFinite(n) && n >= 0) return n;
81
+ }
82
+ // A free mint legitimately quotes zero.
83
+ if (raw?.isFree === true) return 0;
84
+ return null;
85
+ }
86
+
87
+ function lamports(sol) {
88
+ return Math.round(positiveNumber(sol, "priceSol") * LAMPORTS_PER_SOL);
89
+ }
90
+
91
+ export async function magicEdenSolHealth(opts = {}) {
92
+ try {
93
+ const stats = await magicEdenSolStats({ symbol: "mad_lads" }, opts);
94
+ return {
95
+ ok: true,
96
+ provider: "magiceden-sol",
97
+ keyed: Boolean(apiKey(opts)),
98
+ readTier: "keyless",
99
+ prepareTier: apiKey(opts) ? "keyed" : "requires MAGICEDEN_API_KEY",
100
+ floorPriceSol: stats.floorPriceSol,
101
+ exec: false,
102
+ };
103
+ } catch (error) {
104
+ return {
105
+ ok: false,
106
+ provider: "magiceden-sol",
107
+ keyed: Boolean(apiKey(opts)),
108
+ error: String(error?.message || error).slice(0, 160),
109
+ exec: false,
110
+ };
111
+ }
112
+ }
113
+
114
+ export async function magicEdenSolStats(args = {}, opts = {}) {
115
+ const symbol = symbolArg(args.symbol || args.collection);
116
+ const raw = await httpJson(`${base(opts)}/collections/${encodeURIComponent(symbol)}/stats`, {
117
+ headers: headers(opts),
118
+ fetchImpl: opts.fetchImpl,
119
+ timeoutMs: opts.timeoutMs ?? 12_000,
120
+ });
121
+ const floorLamports = Number(raw?.floorPrice ?? 0);
122
+ return {
123
+ provider: "magiceden-sol",
124
+ chain: "solana-mainnet-beta",
125
+ symbol,
126
+ floorPriceLamports: floorLamports || null,
127
+ floorPriceSol: floorLamports ? floorLamports / LAMPORTS_PER_SOL : null,
128
+ listedCount: raw?.listedCount ?? null,
129
+ volume24hrSol: raw?.volume24hr != null ? Number(raw.volume24hr) / LAMPORTS_PER_SOL : null,
130
+ avgPrice24hrSol: raw?.avgPrice24hr != null ? Number(raw.avgPrice24hr) / LAMPORTS_PER_SOL : null,
131
+ exec: false,
132
+ raw,
133
+ };
134
+ }
135
+
136
+ function normalizeListing(item = {}) {
137
+ const priceSol = Number(item.price ?? 0);
138
+ return {
139
+ tokenMint: item.tokenMint ?? null,
140
+ tokenATA: item.tokenAddress ?? null,
141
+ seller: item.seller ?? null,
142
+ sellerReferral: item.sellerReferral ?? null,
143
+ auctionHouse: item.auctionHouse ?? null,
144
+ pdaAddress: item.pdaAddress ?? null,
145
+ priceSol: priceSol || null,
146
+ priceLamports: priceSol ? Math.round(priceSol * LAMPORTS_PER_SOL) : null,
147
+ tokenSize: item.tokenSize ?? 1,
148
+ };
149
+ }
150
+
151
+ export async function magicEdenSolListings(args = {}, opts = {}) {
152
+ const symbol = symbolArg(args.symbol || args.collection);
153
+ const limit = Math.min(Math.max(Number(args.limit ?? 20) || 20, 1), 100);
154
+ const offset = Math.max(Number(args.offset ?? 0) || 0, 0);
155
+ const url = new URL(`${base(opts)}/collections/${encodeURIComponent(symbol)}/listings`);
156
+ url.searchParams.set("offset", String(offset));
157
+ url.searchParams.set("limit", String(limit));
158
+ if (args.sort) url.searchParams.set("sort", String(args.sort));
159
+ const raw = await httpJson(url.toString(), {
160
+ headers: headers(opts),
161
+ fetchImpl: opts.fetchImpl,
162
+ timeoutMs: opts.timeoutMs ?? 12_000,
163
+ });
164
+ const listings = (Array.isArray(raw) ? raw : []).map(normalizeListing);
165
+ return {
166
+ provider: "magiceden-sol",
167
+ chain: "solana-mainnet-beta",
168
+ symbol,
169
+ count: listings.length,
170
+ listings,
171
+ exec: false,
172
+ };
173
+ }
174
+
175
+ export async function magicEdenSolTokenListing(args = {}, opts = {}) {
176
+ const mint = solanaPubkey(args.tokenMint || args.mint, "tokenMint");
177
+ const raw = await httpJson(`${base(opts)}/tokens/${encodeURIComponent(mint)}/listings`, {
178
+ headers: headers(opts),
179
+ fetchImpl: opts.fetchImpl,
180
+ timeoutMs: opts.timeoutMs ?? 12_000,
181
+ });
182
+ const listings = (Array.isArray(raw) ? raw : []).map(normalizeListing);
183
+ return { provider: "magiceden-sol", chain: "solana-mainnet-beta", tokenMint: mint, listings, exec: false };
184
+ }
185
+
186
+ /**
187
+ * Build an UNSIGNED buy_now transaction for a listed Solana NFT.
188
+ * Requires MAGICEDEN_API_KEY. Returns base64 tx for the user's wallet.
189
+ */
190
+ export async function magicEdenSolPrepareBuy(args = {}, opts = {}) {
191
+ if (!apiKey(opts)) throw new Error("magiceden: MAGICEDEN_API_KEY required to build a buy instruction");
192
+ const buyer = solanaPubkey(args.buyer || args.userPublicKey, "buyer");
193
+ const seller = solanaPubkey(args.seller, "seller");
194
+ const tokenMint = solanaPubkey(args.tokenMint, "tokenMint");
195
+ const tokenATA = solanaPubkey(args.tokenATA || args.tokenAddress, "tokenATA");
196
+ const auctionHouse = solanaPubkey(args.auctionHouse, "auctionHouse");
197
+ const priceSol = positiveNumber(args.priceSol ?? args.price, "priceSol");
198
+ const maxPriceSol = args.maxPriceSol == null ? priceSol : positiveNumber(args.maxPriceSol, "maxPriceSol");
199
+ if (priceSol > maxPriceSol) {
200
+ throw new Error(`magiceden: listing price ${priceSol} SOL exceeds maxPriceSol cap ${maxPriceSol} SOL`);
201
+ }
202
+ const url = new URL(`${base(opts)}/instructions/buy_now`);
203
+ url.searchParams.set("buyer", buyer);
204
+ url.searchParams.set("seller", seller);
205
+ url.searchParams.set("auctionHouseAddress", auctionHouse);
206
+ url.searchParams.set("tokenMint", tokenMint);
207
+ url.searchParams.set("tokenATA", tokenATA);
208
+ url.searchParams.set("price", String(priceSol));
209
+ if (args.sellerReferral) url.searchParams.set("sellerReferral", String(args.sellerReferral));
210
+ if (args.buyerReferral) url.searchParams.set("buyerReferral", String(args.buyerReferral));
211
+ const raw = await httpJson(url.toString(), {
212
+ headers: headers(opts),
213
+ fetchImpl: opts.fetchImpl,
214
+ timeoutMs: opts.timeoutMs ?? 15_000,
215
+ });
216
+ const data = raw?.v0?.tx?.data ?? raw?.tx?.data ?? null;
217
+ // Accept ONLY the documented unsigned instruction field. A pre-signed payload
218
+ // (txSigned) from a compromised or impersonated API must never be laundered
219
+ // into a "prepare" result — the user would be signing bytes Oracle never
220
+ // validated against the price cap.
221
+ if (!Array.isArray(data)) {
222
+ if (raw?.txSigned) throw new Error("magiceden: refused a pre-signed transaction payload from the API");
223
+ throw new Error("magiceden: buy_now returned no unsigned transaction payload");
224
+ }
225
+ const transaction = Buffer.from(data).toString("base64");
226
+ return {
227
+ provider: "magiceden-sol",
228
+ chain: "solana-mainnet-beta",
229
+ kind: "nft-buy",
230
+ prepareReady: true,
231
+ executionReady: false,
232
+ signingReady: false,
233
+ broadcastReady: false,
234
+ requiresUserSignature: true,
235
+ buyer,
236
+ seller,
237
+ tokenMint,
238
+ priceSol,
239
+ maxPriceSol,
240
+ lamports: lamports(priceSol),
241
+ transaction,
242
+ transactionEncoding: "base64",
243
+ raw,
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Build an UNSIGNED list instruction so a holder can list their own NFT.
249
+ * Requires MAGICEDEN_API_KEY. User wallet signs; Oracle never broadcasts.
250
+ */
251
+ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
252
+ if (!apiKey(opts)) throw new Error("magiceden: MAGICEDEN_API_KEY required to build a list instruction");
253
+ const seller = solanaPubkey(args.seller || args.userPublicKey, "seller");
254
+ const tokenMint = solanaPubkey(args.tokenMint, "tokenMint");
255
+ const tokenATA = solanaPubkey(args.tokenATA || args.tokenAddress, "tokenATA");
256
+ const auctionHouse = solanaPubkey(args.auctionHouse, "auctionHouse");
257
+ const priceSol = positiveNumber(args.priceSol ?? args.price, "priceSol");
258
+ const url = new URL(`${base(opts)}/instructions/sell`);
259
+ url.searchParams.set("seller", seller);
260
+ url.searchParams.set("auctionHouseAddress", auctionHouse);
261
+ url.searchParams.set("tokenMint", tokenMint);
262
+ url.searchParams.set("tokenAccount", tokenATA);
263
+ url.searchParams.set("price", String(priceSol));
264
+ const raw = await httpJson(url.toString(), {
265
+ headers: headers(opts),
266
+ fetchImpl: opts.fetchImpl,
267
+ timeoutMs: opts.timeoutMs ?? 15_000,
268
+ });
269
+ const data = raw?.v0?.tx?.data ?? raw?.tx?.data ?? null;
270
+ const transaction = Array.isArray(data) ? Buffer.from(data).toString("base64") : null;
271
+ if (!transaction) throw new Error("magiceden: sell returned no transaction payload");
272
+ return {
273
+ provider: "magiceden-sol",
274
+ chain: "solana-mainnet-beta",
275
+ kind: "nft-list",
276
+ prepareReady: true,
277
+ executionReady: false,
278
+ signingReady: false,
279
+ broadcastReady: false,
280
+ requiresUserSignature: true,
281
+ seller,
282
+ tokenMint,
283
+ priceSol,
284
+ transaction,
285
+ transactionEncoding: "base64",
286
+ raw,
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Launchpad mint instruction. Requires MAGICEDEN_API_KEY. Unsigned only.
292
+ */
293
+ export async function magicEdenSolPrepareMint(args = {}, opts = {}) {
294
+ if (!apiKey(opts)) throw new Error("magiceden: MAGICEDEN_API_KEY required to build a mint instruction");
295
+ const buyer = solanaPubkey(args.buyer || args.userPublicKey, "buyer");
296
+ const symbol = symbolArg(args.symbol || args.collection);
297
+ // A mint debits the buyer, so the cap is mandatory. Recording it without
298
+ // enforcing it is worse than having none: the caller believes a ceiling held.
299
+ if (args.maxPriceSol == null) {
300
+ throw new Error("magiceden: maxPriceSol is required to prepare a mint — an uncapped mint has no spend ceiling");
301
+ }
302
+ const maxPriceSol = positiveNumber(args.maxPriceSol, "maxPriceSol");
303
+ const nftAmount = args.nftAmount == null ? 1 : positiveNumber(args.nftAmount, "nftAmount");
304
+ if (!Number.isInteger(nftAmount)) throw new Error("magiceden: nftAmount must be a whole number");
305
+
306
+ const url = new URL(`${base(opts)}/instructions/mint`);
307
+ url.searchParams.set("symbol", symbol);
308
+ url.searchParams.set("wallet", buyer);
309
+ url.searchParams.set("nftAmount", String(nftAmount));
310
+ const raw = await httpJson(url.toString(), {
311
+ headers: headers(opts),
312
+ fetchImpl: opts.fetchImpl,
313
+ timeoutMs: opts.timeoutMs ?? 15_000,
314
+ });
315
+
316
+ // Enforce the cap against the price the launchpad actually quotes. If the
317
+ // price cannot be determined, fail closed rather than return an unbounded
318
+ // mint the caller thinks is capped.
319
+ const quoted = mintPriceSol(raw);
320
+ if (quoted == null) {
321
+ throw new Error("magiceden: mint price not present in the API response — refusing to prepare an unverifiable spend");
322
+ }
323
+ const totalSol = quoted * nftAmount;
324
+ if (totalSol > maxPriceSol) {
325
+ throw new Error(
326
+ `magiceden: mint cost ${totalSol} SOL exceeds maxPriceSol cap ${maxPriceSol} SOL`
327
+ );
328
+ }
329
+
330
+ const data = raw?.v0?.tx?.data ?? raw?.tx?.data ?? null;
331
+ if (!Array.isArray(data)) {
332
+ if (raw?.txSigned) throw new Error("magiceden: refused a pre-signed transaction payload from the API");
333
+ throw new Error("magiceden: mint returned no unsigned transaction payload");
334
+ }
335
+ const transaction = Buffer.from(data).toString("base64");
336
+ return {
337
+ provider: "magiceden-sol",
338
+ chain: "solana-mainnet-beta",
339
+ kind: "nft-mint",
340
+ prepareReady: true,
341
+ executionReady: false,
342
+ signingReady: false,
343
+ broadcastReady: false,
344
+ requiresUserSignature: true,
345
+ buyer,
346
+ symbol,
347
+ nftAmount,
348
+ priceSol: quoted,
349
+ totalSol,
350
+ maxPriceSol,
351
+ transaction,
352
+ transactionEncoding: "base64",
353
+ raw,
354
+ };
355
+ }