@oracle-agent/oracle 0.5.0 → 0.7.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 (93) hide show
  1. package/README.md +10 -1
  2. package/bin/oracle-data-mcp.mjs +32 -0
  3. package/docs/cli.md +28 -0
  4. package/package.json +3 -3
  5. package/profiles/oracle/SOUL.md +10 -4
  6. package/public/oracle-splash/assets/hero/orb.avif +0 -0
  7. package/public/oracle-splash/assets/hero/orb.webm +0 -0
  8. package/public/oracle-splash/assets/llms/claude.svg +1 -0
  9. package/public/oracle-splash/assets/llms/codex.svg +1 -0
  10. package/public/oracle-splash/assets/llms/cursor-icon.svg +12 -0
  11. package/public/oracle-splash/assets/llms/cursor.svg +17 -0
  12. package/public/oracle-splash/assets/llms/deepseek.svg +1 -0
  13. package/public/oracle-splash/assets/llms/gemini.svg +1 -0
  14. package/public/oracle-splash/assets/llms/grok.svg +8 -0
  15. package/public/oracle-splash/assets/llms/hermes.png +0 -0
  16. package/public/oracle-splash/assets/llms/kimi-icon.svg +1 -0
  17. package/public/oracle-splash/assets/llms/kimi.svg +1 -0
  18. package/public/oracle-splash/assets/llms/manus.svg +18 -0
  19. package/public/oracle-splash/assets/llms/openclaw.svg +22 -0
  20. package/public/oracle-splash/assets/llms/perplexity.svg +1 -0
  21. package/public/oracle-splash/assets/llms/qwen.svg +1 -0
  22. package/public/oracle-splash/assets/llms/windsurf.svg +1 -0
  23. package/public/oracle-splash/assets/protocols/abcdex.png +0 -0
  24. package/public/oracle-splash/assets/protocols/across.png +0 -0
  25. package/public/oracle-splash/assets/protocols/aerodrome.png +0 -0
  26. package/public/oracle-splash/assets/protocols/balancer.png +0 -0
  27. package/public/oracle-splash/assets/protocols/cowswap.png +0 -0
  28. package/public/oracle-splash/assets/protocols/curve.png +0 -0
  29. package/public/oracle-splash/assets/protocols/dreamcash.png +0 -0
  30. package/public/oracle-splash/assets/protocols/felix.png +0 -0
  31. package/public/oracle-splash/assets/protocols/gmx.png +0 -0
  32. package/public/oracle-splash/assets/protocols/hyena.png +0 -0
  33. package/public/oracle-splash/assets/protocols/hyperbeat.png +0 -0
  34. package/public/oracle-splash/assets/protocols/hyperliquid.png +0 -0
  35. package/public/oracle-splash/assets/protocols/hyperswap.png +0 -0
  36. package/public/oracle-splash/assets/protocols/jupiter.png +0 -0
  37. package/public/oracle-splash/assets/protocols/kinetiq.png +0 -0
  38. package/public/oracle-splash/assets/protocols/lfj.png +0 -0
  39. package/public/oracle-splash/assets/protocols/lifi.png +0 -0
  40. package/public/oracle-splash/assets/protocols/magic-eden.png +0 -0
  41. package/public/oracle-splash/assets/protocols/morpho.png +0 -0
  42. package/public/oracle-splash/assets/protocols/odos.png +0 -0
  43. package/public/oracle-splash/assets/protocols/oneinch.png +0 -0
  44. package/public/oracle-splash/assets/protocols/opensea.png +0 -0
  45. package/public/oracle-splash/assets/protocols/pancakeswap.png +0 -0
  46. package/public/oracle-splash/assets/protocols/paragon.png +0 -0
  47. package/public/oracle-splash/assets/protocols/paraswap.png +0 -0
  48. package/public/oracle-splash/assets/protocols/pendle.png +0 -0
  49. package/public/oracle-splash/assets/protocols/polymarket.png +0 -0
  50. package/public/oracle-splash/assets/protocols/quickswap.png +0 -0
  51. package/public/oracle-splash/assets/protocols/relay.png +0 -0
  52. package/public/oracle-splash/assets/protocols/stargate.png +0 -0
  53. package/public/oracle-splash/assets/protocols/tradexyz.png +0 -0
  54. package/public/oracle-splash/assets/protocols/uniswap.png +0 -0
  55. package/public/oracle-splash/assets/protocols/velodrome.png +0 -0
  56. package/public/oracle-splash/assets/protocols/ventuals.png +0 -0
  57. package/public/oracle-splash/assets/wordmarks/cowswap.svg +1 -1
  58. package/public/oracle-splash/assets/wordmarks/curve.png +0 -0
  59. package/public/oracle-splash/assets/wordmarks/gmx.svg +1 -1
  60. package/public/oracle-splash/assets/wordmarks/hyena.svg +7 -0
  61. package/public/oracle-splash/assets/wordmarks/kinetiq.svg +1 -0
  62. package/public/oracle-splash/assets/wordmarks/lfj.webp +0 -0
  63. package/public/oracle-splash/assets/wordmarks/magic-eden.svg +1 -1
  64. package/public/oracle-splash/assets/wordmarks/morpho.svg +1 -1
  65. package/public/oracle-splash/assets/wordmarks/odos.svg +1 -1
  66. package/public/oracle-splash/assets/wordmarks/opensea.svg +1 -1
  67. package/public/oracle-splash/assets/wordmarks/outcome.svg +16 -0
  68. package/public/oracle-splash/assets/wordmarks/paragon.svg +9 -0
  69. package/public/oracle-splash/assets/wordmarks/paraswap.svg +1 -1
  70. package/public/oracle-splash/assets/wordmarks/xyz.svg +12 -0
  71. package/public/oracle-splash/index.html +156 -184
  72. package/skills/oracle-chat/SKILL.md +61 -0
  73. package/skills/oracle-chat/chain.SKILL.md +31 -0
  74. package/skills/oracle-chat/setup.SKILL.md +37 -0
  75. package/skins/oracle.yaml +52 -0
  76. package/src/cli/chain-catalog.mjs +215 -0
  77. package/src/cli/chain-state.mjs +74 -0
  78. package/src/cli/commands/chain.mjs +143 -0
  79. package/src/cli/commands/chat.mjs +268 -0
  80. package/src/cli/commands/model.mjs +37 -0
  81. package/src/cli/commands/setup.mjs +283 -0
  82. package/src/cli/first-run.mjs +3 -0
  83. package/src/cli/kernel.mjs +42 -3
  84. package/src/cli/messaging-platforms.mjs +209 -0
  85. package/src/cli/oracle-harness.py +148 -0
  86. package/src/cli/paths.mjs +5 -0
  87. package/src/cli/setup-state.mjs +168 -0
  88. package/src/data/catalog.mjs +11 -0
  89. package/src/data/desk-data.mjs +6 -0
  90. package/src/data/providers/rfq.mjs +15 -0
  91. package/src/index.mjs +7 -0
  92. package/src/rfq/intent.mjs +180 -0
  93. package/src/rfq/sources.mjs +181 -0
@@ -0,0 +1,180 @@
1
+ import { createHash } from "node:crypto";
2
+ import { getAddress, isAddress } from "ethers";
3
+
4
+ export const NATIVE_TOKEN = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
5
+ export const DEFAULT_SAME_CHAIN_SOURCES = Object.freeze(["lifi", "paraswap", "0x", "cow", "uniswap-v3"]);
6
+ export const DEFAULT_CROSS_CHAIN_SOURCES = Object.freeze(["lifi"]);
7
+ const HEX32 = /^0x[0-9a-fA-F]{64}$/;
8
+ const INT = /^\d+$/;
9
+
10
+ function fail(label, message = "invalid") {
11
+ throw new Error(`rfq: ${label} ${message}`);
12
+ }
13
+
14
+ function chainId(value, label) {
15
+ const n = Number(value);
16
+ if (!Number.isSafeInteger(n) || n <= 0) fail(label, "must be a positive chainId");
17
+ return n;
18
+ }
19
+
20
+ function amount(value, label) {
21
+ const s = String(value ?? "").trim();
22
+ if (!INT.test(s) || BigInt(s) <= 0n) fail(label, "must be a positive integer string");
23
+ return s;
24
+ }
25
+
26
+ function optionalAmount(value, label) {
27
+ if (value == null || value === "") return null;
28
+ return amount(value, label);
29
+ }
30
+
31
+ function token(value, label) {
32
+ const s = String(value ?? "").trim();
33
+ if (s.toLowerCase() === NATIVE_TOKEN.toLowerCase()) return NATIVE_TOKEN;
34
+ if (!isAddress(s)) fail(label, "must be an EVM address or native token sentinel");
35
+ return getAddress(s);
36
+ }
37
+
38
+ function address(value, label) {
39
+ const s = String(value ?? "").trim();
40
+ if (!isAddress(s)) fail(label, "must be an EVM address");
41
+ return getAddress(s);
42
+ }
43
+
44
+ function optionalAddress(value, label) {
45
+ if (value == null || value === "") return null;
46
+ return address(value, label);
47
+ }
48
+
49
+ function stringList(value, label, defaults) {
50
+ if (value === undefined) return [...defaults];
51
+ if (!Array.isArray(value)) fail(label, "must be an array");
52
+ return value.map((x) => String(x).trim()).filter(Boolean);
53
+ }
54
+
55
+ function deadline(value, { nowMs, maxDeadlineMs }, label) {
56
+ const n = Number(value);
57
+ if (!Number.isFinite(n)) fail(label, "must be a finite millisecond timestamp");
58
+ if (n <= nowMs) fail(label, "expired");
59
+ if (n > nowMs + maxDeadlineMs) fail(label, "exceeds maxDeadlineMs");
60
+ return Math.floor(n);
61
+ }
62
+
63
+ function plain(value) {
64
+ if (Array.isArray(value)) return value.map(plain);
65
+ if (value && typeof value === "object") {
66
+ const out = {};
67
+ for (const key of Object.keys(value).sort()) {
68
+ if (value[key] !== undefined) out[key] = plain(value[key]);
69
+ }
70
+ return out;
71
+ }
72
+ return value;
73
+ }
74
+
75
+ export function canonicalJson(value) {
76
+ return JSON.stringify(plain(value));
77
+ }
78
+
79
+ export function sha256Hex(value) {
80
+ return `0x${createHash("sha256").update(canonicalJson(value)).digest("hex")}`;
81
+ }
82
+
83
+ function withoutHash(value, keys) {
84
+ const out = { ...value };
85
+ for (const key of keys) delete out[key];
86
+ return out;
87
+ }
88
+
89
+ export function hashRfqIntent(intent) {
90
+ return sha256Hex(withoutHash(intent, ["intentHash"]));
91
+ }
92
+
93
+ export function hashFirmQuote(quote) {
94
+ return sha256Hex(withoutHash(quote, ["firmQuoteHash"]));
95
+ }
96
+
97
+ export function normalizeRfqIntent(input = {}, opts = {}) {
98
+ const nowMs = Number(opts.nowMs ?? Date.now());
99
+ const maxDeadlineMs = Number(opts.maxDeadlineMs ?? 30 * 60 * 1000);
100
+ if (!Number.isFinite(nowMs) || !Number.isFinite(maxDeadlineMs) || maxDeadlineMs <= 0) fail("time", "is invalid");
101
+ const fromChainId = chainId(input.fromChainId ?? input.fromChain, "fromChainId");
102
+ const toChainId = chainId(input.toChainId ?? input.toChain, "toChainId");
103
+ const sameChain = fromChainId === toChainId;
104
+ const defaults = sameChain ? DEFAULT_SAME_CHAIN_SOURCES : DEFAULT_CROSS_CHAIN_SOURCES;
105
+ const intent = {
106
+ kind: "rfq-intent",
107
+ version: 1,
108
+ fromChainId,
109
+ toChainId,
110
+ sellToken: token(input.sellToken ?? input.tokenIn ?? input.fromToken, "sellToken"),
111
+ buyToken: token(input.buyToken ?? input.tokenOut ?? input.toToken, "buyToken"),
112
+ sellAmount: amount(input.sellAmount ?? input.amountIn ?? input.fromAmount, "sellAmount"),
113
+ receiver: address(input.receiver ?? input.recipient ?? input.to, "receiver"),
114
+ deadlineMs: deadline(input.deadlineMs ?? input.deadline ?? input.expiresAtMs, { nowMs, maxDeadlineMs }, "deadline"),
115
+ minBuyAmount: optionalAmount(input.minBuyAmount ?? input.minOut ?? input.amountOutMin, "minBuyAmount"),
116
+ allowedSources: stringList(input.allowedSources, "allowedSources", defaults),
117
+ allowedRouters: stringList(input.allowedRouters, "allowedRouters", []),
118
+ slippageBps: Number.isFinite(Number(input.slippageBps)) ? Number(input.slippageBps) : null,
119
+ partialFill: input.partialFill === true,
120
+ metadata: input.metadata && typeof input.metadata === "object" && !Array.isArray(input.metadata) ? plain(input.metadata) : {},
121
+ };
122
+ if (intent.slippageBps != null && (!Number.isInteger(intent.slippageBps) || intent.slippageBps < 0 || intent.slippageBps > 10_000)) fail("slippageBps", "must be 0 to 10000");
123
+ intent.intentId = input.intentId ? String(input.intentId) : sha256Hex({ seed: "rfq-intent", intent }).slice(0, 34);
124
+ intent.intentHash = hashRfqIntent(intent);
125
+ return Object.freeze(intent);
126
+ }
127
+
128
+ function artifact(value) {
129
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail("artifact", "required");
130
+ if (value.typedDataHash != null && !HEX32.test(String(value.typedDataHash))) fail("artifact.typedDataHash", "must be bytes32");
131
+ if (value.calldataHash != null && !HEX32.test(String(value.calldataHash))) fail("artifact.calldataHash", "must be bytes32");
132
+ if (value.to != null && !isAddress(String(value.to))) fail("artifact.to", "must be an address");
133
+ if (value.data != null && !/^0x(?:[0-9a-fA-F]{2})*$/.test(String(value.data))) fail("artifact.data", "must be hex");
134
+ return plain(value);
135
+ }
136
+
137
+ export function normalizeFirmQuote(intent, quote = {}, opts = {}) {
138
+ if (!intent || intent.kind !== "rfq-intent") fail("intent", "required");
139
+ const nowMs = Number(opts.nowMs ?? Date.now());
140
+ const expiryMs = Number(quote.expiryMs ?? quote.expiresAtMs ?? quote.validUntilMs);
141
+ if (!Number.isFinite(expiryMs)) fail("quote expiry", "required");
142
+ if (expiryMs <= nowMs) fail("quote", "expired");
143
+ if (expiryMs > intent.deadlineMs) fail("quote expiry", "exceeds intent deadline");
144
+ const source = String(quote.source ?? quote.provider ?? "").trim();
145
+ if (!source) fail("source", "required");
146
+ if (!intent.allowedSources.includes(source)) fail("source", "not allowed by intent");
147
+ const amountOut = amount(quote.amountOut ?? quote.buyAmount, "amountOut");
148
+ const minBuyAmount = amount(quote.minBuyAmount ?? quote.minOut ?? amountOut, "minBuyAmount");
149
+ if (BigInt(minBuyAmount) > BigInt(amountOut)) fail("minBuyAmount", "exceeds amountOut");
150
+ const artifactValue = artifact(quote.artifact ?? quote.executableArtifact ?? quote.transaction ?? quote.typedData);
151
+ const quotedAtMs = Number(quote.quotedAtMs ?? quote.timestampMs ?? opts.nowMs ?? Date.now());
152
+ if (!Number.isFinite(quotedAtMs)) fail("quotedAtMs", "must be finite");
153
+ const router = optionalAddress(quote.router ?? quote.settlementRouter ?? artifactValue.to, "router");
154
+ const out = {
155
+ kind: "rfq-firm-quote",
156
+ provider: "rfq",
157
+ surface: "rfq",
158
+ version: 1,
159
+ quoteId: String(quote.quoteId ?? quote.id ?? "").trim(),
160
+ source,
161
+ intentHash: intent.intentHash,
162
+ fromChainId: intent.fromChainId,
163
+ toChainId: intent.toChainId,
164
+ sellToken: intent.sellToken,
165
+ buyToken: intent.buyToken,
166
+ sellAmount: intent.sellAmount,
167
+ receiver: intent.receiver,
168
+ router,
169
+ amountOut,
170
+ minBuyAmount,
171
+ quotedAtMs: Math.floor(quotedAtMs),
172
+ expiryMs: Math.floor(expiryMs),
173
+ artifact: artifactValue,
174
+ maker: quote.maker ? String(quote.maker) : null,
175
+ metadata: quote.metadata && typeof quote.metadata === "object" && !Array.isArray(quote.metadata) ? plain(quote.metadata) : {},
176
+ };
177
+ if (!out.quoteId) out.quoteId = sha256Hex({ seed: "rfq-quote", out }).slice(0, 34);
178
+ out.firmQuoteHash = hashFirmQuote(out);
179
+ return Object.freeze(out);
180
+ }
@@ -0,0 +1,181 @@
1
+ import { lifiQuote } from "../data/providers/lifi.mjs";
2
+ import { paraswapPrice } from "../data/providers/paraswap.mjs";
3
+ import { zeroxQuote } from "../data/providers/zerox.mjs";
4
+ import { cowQuote } from "../data/providers/cowswap.mjs";
5
+ import { uniV3QuoteExactIn, UNI_V3_CHAINS } from "../data/providers/uniswap-v3.mjs";
6
+ import { normalizeFirmQuote } from "./intent.mjs";
7
+
8
+ function allowed(intent, source) {
9
+ return Array.isArray(intent?.allowedSources) && intent.allowedSources.includes(source);
10
+ }
11
+
12
+ function envValue(env, key) {
13
+ return String(env?.[key] ?? "").trim();
14
+ }
15
+
16
+ function candidate(source, run) {
17
+ return Object.freeze({ source, run });
18
+ }
19
+
20
+ export function sourceCandidates(intent, opts = {}) {
21
+ if (!intent || intent.kind !== "rfq-intent") throw new Error("rfq: intent required");
22
+ const env = opts.env ?? process.env;
23
+ const providers = opts.providers ?? {};
24
+ const uniChains = opts.supportedUniV3Chains ?? UNI_V3_CHAINS;
25
+ const sameChain = Number(intent.fromChainId) === Number(intent.toChainId);
26
+ const out = [];
27
+ if (allowed(intent, "lifi")) {
28
+ out.push(candidate("lifi", async () => {
29
+ const fn = providers.lifiQuote ?? lifiQuote;
30
+ const q = await fn({
31
+ fromChain: intent.fromChainId,
32
+ toChain: intent.toChainId,
33
+ fromToken: intent.sellToken,
34
+ toToken: intent.buyToken,
35
+ fromAmount: intent.sellAmount,
36
+ fromAddress: intent.receiver,
37
+ }, opts);
38
+ const est = q?.estimate ?? q?.[0]?.estimate ?? q;
39
+ return {
40
+ quoteId: q?.id ?? q?.routeId ?? null,
41
+ amountOut: est?.toAmount ?? q?.toAmount,
42
+ minBuyAmount: est?.toAmountMin ?? q?.toAmountMin ?? est?.toAmount ?? q?.toAmount,
43
+ expiryMs: Date.now() + Number(opts.defaultQuoteTtlMs ?? 20_000),
44
+ artifact: { type: "lifi-route", calldataHash: q?.transactionRequest?.data ? await hashMaybe(opts, q.transactionRequest.data) : undefined, route: q },
45
+ metadata: { tool: q?.tool ?? q?.toolDetails?.name ?? null },
46
+ };
47
+ }));
48
+ }
49
+ if (!sameChain) return out;
50
+ if (allowed(intent, "paraswap")) {
51
+ out.push(candidate("paraswap", async () => {
52
+ const fn = providers.paraswapPrice ?? paraswapPrice;
53
+ const q = await fn({
54
+ chainId: intent.fromChainId,
55
+ srcToken: intent.sellToken,
56
+ destToken: intent.buyToken,
57
+ amount: intent.sellAmount,
58
+ srcDecimals: opts.decimalsIn,
59
+ destDecimals: opts.decimalsOut,
60
+ }, opts);
61
+ const pr = q?.priceRoute ?? q;
62
+ return {
63
+ quoteId: pr?.id ?? null,
64
+ amountOut: pr?.destAmount,
65
+ minBuyAmount: q?.amountOutMinimum ?? pr?.destAmount,
66
+ expiryMs: Date.now() + Number(opts.defaultQuoteTtlMs ?? 20_000),
67
+ artifact: { type: "paraswap-price-route", route: pr },
68
+ metadata: { contractAddress: pr?.contractAddress ?? null },
69
+ };
70
+ }));
71
+ }
72
+ if (allowed(intent, "0x") && (envValue(env, "ZEROX_API_KEY") || envValue(env, "0X_API_KEY") || providers.zeroxQuote)) {
73
+ out.push(candidate("0x", async () => {
74
+ const fn = providers.zeroxQuote ?? zeroxQuote;
75
+ const q = await fn({ chainId: intent.fromChainId, sellToken: intent.sellToken, buyToken: intent.buyToken, sellAmount: intent.sellAmount, taker: intent.receiver }, opts);
76
+ return {
77
+ quoteId: q?.id ?? null,
78
+ amountOut: q?.buyAmount ?? q?.grossBuyAmount,
79
+ minBuyAmount: q?.minBuyAmount ?? q?.buyAmount,
80
+ expiryMs: Date.now() + Number(opts.defaultQuoteTtlMs ?? 20_000),
81
+ artifact: { type: "0x-transaction", to: q?.transaction?.to, data: q?.transaction?.data, value: q?.transaction?.value ?? "0" },
82
+ metadata: { route: q?.route ?? null },
83
+ };
84
+ }));
85
+ }
86
+ if (allowed(intent, "cow")) {
87
+ out.push(candidate("cow", async () => {
88
+ const fn = providers.cowQuote ?? cowQuote;
89
+ const q = await fn({ chainId: intent.fromChainId, sellToken: intent.sellToken, buyToken: intent.buyToken, sellAmountBeforeFee: intent.sellAmount, from: intent.receiver, signingScheme: "eip712" }, opts);
90
+ const quote = q?.quote ?? q;
91
+ return {
92
+ quoteId: q?.id ?? null,
93
+ amountOut: quote?.buyAmount,
94
+ minBuyAmount: quote?.buyAmount,
95
+ expiryMs: Math.min(Number(quote?.validTo ?? 0) * 1000 || Date.now() + Number(opts.defaultQuoteTtlMs ?? 20_000), intent.deadlineMs),
96
+ artifact: { type: "cow-order", order: quote },
97
+ metadata: { solver: true },
98
+ };
99
+ }));
100
+ }
101
+ if (allowed(intent, "uniswap-v3") && uniChains[Number(intent.fromChainId)]) {
102
+ out.push(candidate("uniswap-v3", async () => {
103
+ const fn = providers.uniV3QuoteExactIn ?? uniV3QuoteExactIn;
104
+ const q = await fn({ chainId: intent.fromChainId, tokenIn: intent.sellToken, tokenOut: intent.buyToken, amountIn: intent.sellAmount }, opts);
105
+ return {
106
+ quoteId: q?.id ?? null,
107
+ amountOut: q?.amountOut,
108
+ minBuyAmount: q?.minOut ?? q?.amountOutMinimum ?? q?.amountOut,
109
+ expiryMs: Date.now() + Number(opts.defaultQuoteTtlMs ?? 20_000),
110
+ artifact: { type: "uniswap-v3-quote", quoter: q?.quoter, fee: q?.fee },
111
+ metadata: { onchain: true },
112
+ };
113
+ }));
114
+ }
115
+ return out;
116
+ }
117
+
118
+ async function hashMaybe(opts, value) {
119
+ const crypto = await import("node:crypto");
120
+ return `0x${crypto.createHash("sha256").update(String(value)).digest("hex")}`;
121
+ }
122
+
123
+ async function runWithTimeout(c, timeoutMs) {
124
+ return Promise.race([
125
+ Promise.resolve().then(c.run),
126
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`${c.source}: timeout`)), timeoutMs)),
127
+ ]);
128
+ }
129
+
130
+ export async function executeRfqCandidates(intent, candidates, opts = {}) {
131
+ const timeoutMs = Number(opts.timeoutMs ?? 12_000);
132
+ const settled = await Promise.allSettled(candidates.map((c) => runWithTimeout(c, timeoutMs)));
133
+ const quotes = [];
134
+ const failed = [];
135
+ for (let i = 0; i < settled.length; i++) {
136
+ const c = candidates[i];
137
+ const r = settled[i];
138
+ if (r.status === "rejected") {
139
+ failed.push({ source: c.source, error: String(r.reason?.message || r.reason) });
140
+ continue;
141
+ }
142
+ try {
143
+ quotes.push(normalizeFirmQuote(intent, { ...r.value, source: c.source }, opts));
144
+ } catch (e) {
145
+ failed.push({ source: c.source, error: String(e?.message || e) });
146
+ }
147
+ }
148
+ return { quotes, failed };
149
+ }
150
+
151
+ export function rankRfqQuotes(quotes = [], opts = {}) {
152
+ const nowMs = Number(opts.nowMs ?? Date.now());
153
+ const usable = quotes.filter((q) => Number(q.expiryMs ?? 0) > nowMs);
154
+ usable.sort((a, b) => {
155
+ const floor = BigInt(b.minBuyAmount ?? 0) - BigInt(a.minBuyAmount ?? 0);
156
+ if (floor !== 0n) return floor > 0n ? 1 : -1;
157
+ const gross = BigInt(b.amountOut ?? 0) - BigInt(a.amountOut ?? 0);
158
+ return gross > 0n ? 1 : gross < 0n ? -1 : 0;
159
+ });
160
+ const ranked = usable.map((q) => ({ ...q, scoreBasis: "minBuyAmount" }));
161
+ const warnings = [];
162
+ if (!ranked.length) warnings.push("no RFQ source returned a usable firm quote");
163
+ if (quotes.length !== ranked.length) warnings.push("expired RFQ quotes were excluded from ranking");
164
+ return { best: ranked[0] ?? null, quotes: ranked, failed: [], warnings };
165
+ }
166
+
167
+ export async function requestRfqQuotes(intent, opts = {}) {
168
+ const candidates = sourceCandidates(intent, opts);
169
+ const { quotes, failed } = await executeRfqCandidates(intent, candidates, opts);
170
+ const ranked = rankRfqQuotes(quotes, opts);
171
+ return {
172
+ kind: "rfq-result",
173
+ intent,
174
+ best: ranked.best,
175
+ quotes: ranked.quotes,
176
+ failed,
177
+ warnings: [...ranked.warnings],
178
+ sourcesTried: candidates.length,
179
+ sourcesAnswered: quotes.length,
180
+ };
181
+ }