nyxora 26.6.26 → 26.6.27

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 (45) hide show
  1. package/dist/packages/core/src/agent/llmProvider.js +10 -3
  2. package/dist/packages/core/src/gateway/server.js +1 -1
  3. package/dist/packages/core/src/memory/logger.js +6 -6
  4. package/dist/packages/core/src/utils/httpClient.js +22 -5
  5. package/dist/packages/core/src/web3/aggregator/defiRouter.js +4 -2
  6. package/dist/packages/core/src/web3/aggregator/providerRegistry.js +8 -1
  7. package/dist/packages/core/src/web3/aggregator/providers/KyberSwapProvider.js +8 -4
  8. package/dist/packages/core/src/web3/aggregator/providers/LifiProvider.js +14 -2
  9. package/dist/packages/core/src/web3/aggregator/providers/OneInchProvider.js +3 -1
  10. package/dist/packages/core/src/web3/aggregator/providers/OpBridgeProvider.js +10 -2
  11. package/dist/packages/core/src/web3/aggregator/providers/OpenOceanProvider.js +63 -7
  12. package/dist/packages/core/src/web3/aggregator/providers/RelayProvider.js +20 -9
  13. package/dist/packages/core/src/web3/aggregator/providers/ZeroXProvider.js +40 -12
  14. package/dist/packages/core/src/web3/aggregator/routeSelector.js +25 -14
  15. package/dist/packages/core/src/web3/skills/bridgeToken.js +47 -3
  16. package/dist/packages/core/src/web3/skills/swapToken.js +47 -4
  17. package/dist/packages/core/src/web3/utils/tokens.js +12 -12
  18. package/dist/packages/core/src/web3/utils/vaultClient.js +6 -0
  19. package/package.json +1 -1
  20. package/packages/core/package.json +1 -1
  21. package/packages/core/src/agent/llmProvider.ts +14 -3
  22. package/packages/core/src/gateway/server.ts +1 -1
  23. package/packages/core/src/memory/logger.ts +6 -6
  24. package/packages/core/src/utils/httpClient.ts +22 -5
  25. package/packages/core/src/web3/aggregator/defiRouter.ts +6 -2
  26. package/packages/core/src/web3/aggregator/providerRegistry.ts +7 -1
  27. package/packages/core/src/web3/aggregator/providers/KyberSwapProvider.ts +9 -4
  28. package/packages/core/src/web3/aggregator/providers/LifiProvider.ts +16 -2
  29. package/packages/core/src/web3/aggregator/providers/OneInchProvider.ts +3 -2
  30. package/packages/core/src/web3/aggregator/providers/OpBridgeProvider.ts +11 -2
  31. package/packages/core/src/web3/aggregator/providers/OpenOceanProvider.ts +66 -7
  32. package/packages/core/src/web3/aggregator/providers/RelayProvider.ts +25 -10
  33. package/packages/core/src/web3/aggregator/providers/ZeroXProvider.ts +45 -12
  34. package/packages/core/src/web3/aggregator/routeSelector.ts +29 -16
  35. package/packages/core/src/web3/aggregator/types.ts +3 -0
  36. package/packages/core/src/web3/skills/bridgeToken.ts +19 -4
  37. package/packages/core/src/web3/skills/swapToken.ts +17 -5
  38. package/packages/core/src/web3/utils/tokens.ts +12 -12
  39. package/packages/core/src/web3/utils/vaultClient.ts +7 -0
  40. package/packages/dashboard/dist/assets/{index-C_WmWSch.js → index-DlwR7UtR.js} +1 -1
  41. package/packages/dashboard/dist/index.html +1 -1
  42. package/packages/dashboard/package.json +1 -1
  43. package/packages/mcp-server/package.json +1 -1
  44. package/packages/policy/package.json +1 -1
  45. package/packages/signer/package.json +1 -1
@@ -12,7 +12,8 @@ class OpenAIAdapter {
12
12
  message: {
13
13
  content: response.choices[0].message.content,
14
14
  tool_calls: response.choices[0].message.tool_calls
15
- }
15
+ },
16
+ usage: response.usage ? { total_tokens: response.usage.total_tokens } : undefined
16
17
  };
17
18
  }
18
19
  }
@@ -124,7 +125,8 @@ class AnthropicAdapter {
124
125
  message: {
125
126
  content: contentStr,
126
127
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined
127
- }
128
+ },
129
+ usage: response.usage ? { total_tokens: response.usage.input_tokens + response.usage.output_tokens } : undefined
128
130
  };
129
131
  }
130
132
  }
@@ -254,11 +256,16 @@ class GeminiAdapter {
254
256
  }
255
257
  }
256
258
  }
259
+ let totalTokens = 0;
260
+ if (data.usageMetadata && data.usageMetadata.totalTokenCount) {
261
+ totalTokens = data.usageMetadata.totalTokenCount;
262
+ }
257
263
  return {
258
264
  message: {
259
265
  content: contentStr,
260
266
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined
261
- }
267
+ },
268
+ usage: totalTokens > 0 ? { total_tokens: totalTokens } : undefined
262
269
  };
263
270
  }
264
271
  }
@@ -183,7 +183,7 @@ app.post('/api/upload-google-credentials', (req, res) => {
183
183
  app.get('/api/history', (req, res) => {
184
184
  try {
185
185
  const sessionId = req.query.session_id;
186
- const history = reasoning_1.logger.getHistory(sessionId);
186
+ const history = reasoning_1.logger.getHistory(sessionId, 1000);
187
187
  // Filter out internal system prompt for the frontend
188
188
  const cleanHistory = history.filter((msg) => msg.role !== 'system');
189
189
  res.json(cleanHistory);
@@ -150,19 +150,19 @@ class Logger {
150
150
  renameSession(sessionId, newTitle) {
151
151
  this.db.prepare('UPDATE sessions SET title = ? WHERE id = ?').run(newTitle, sessionId);
152
152
  }
153
- getHistory(sessionId) {
153
+ getHistory(sessionId, limit = 40) {
154
154
  let rows;
155
155
  // Phase 2: Sliding Window Algorithm (LLM Context Limit)
156
- // Fetch only the last 40 messages, then order them chronologically
156
+ // Fetch only the last X messages, then order them chronologically
157
157
  if (sessionId) {
158
158
  rows = this.db.prepare(`
159
159
  SELECT * FROM (
160
160
  SELECT role, content, name, tool_call_id, tool_calls, session_id, id
161
161
  FROM messages
162
162
  WHERE session_id = ?
163
- ORDER BY id DESC LIMIT 40
163
+ ORDER BY id DESC LIMIT ?
164
164
  ) ORDER BY id ASC
165
- `).all(sessionId);
165
+ `).all(sessionId, limit);
166
166
  }
167
167
  else {
168
168
  rows = this.db.prepare(`
@@ -170,9 +170,9 @@ class Logger {
170
170
  SELECT role, content, name, tool_call_id, tool_calls, session_id, id
171
171
  FROM messages
172
172
  WHERE session_id IS NULL
173
- ORDER BY id DESC LIMIT 40
173
+ ORDER BY id DESC LIMIT ?
174
174
  ) ORDER BY id ASC
175
- `).all();
175
+ `).all(limit);
176
176
  }
177
177
  return rows.map((row) => {
178
178
  const entry = {
@@ -26,16 +26,29 @@ async function safeFetch(url, options = {}) {
26
26
  for (let attempt = 0; attempt <= retries; attempt++) {
27
27
  const controller = new AbortController();
28
28
  const id = setTimeout(() => controller.abort(), timeoutMs);
29
+ const externalSignal = fetchOptions.signal;
30
+ const onExternalAbort = () => controller.abort();
31
+ if (externalSignal) {
32
+ if (externalSignal.aborted)
33
+ controller.abort();
34
+ else
35
+ externalSignal.addEventListener('abort', onExternalAbort);
36
+ }
37
+ const cleanup = () => {
38
+ clearTimeout(id);
39
+ if (externalSignal)
40
+ externalSignal.removeEventListener('abort', onExternalAbort);
41
+ };
29
42
  try {
30
43
  const res = await fetch(url, {
31
44
  ...fetchOptions,
32
45
  headers: finalHeaders,
33
46
  signal: controller.signal
34
47
  });
35
- clearTimeout(id);
48
+ cleanup();
36
49
  // Handle Rate Limits specifically with a forced retry if attempts remain
37
50
  if (res.status === 429) {
38
- if (attempt < retries) {
51
+ if (attempt < retries && !controller.signal.aborted) {
39
52
  // Coingecko and others often need longer backoff, exponentially increase
40
53
  await delay(retryDelayMs * (attempt + 1) * 2);
41
54
  continue;
@@ -44,7 +57,7 @@ async function safeFetch(url, options = {}) {
44
57
  }
45
58
  // Handle Server Errors with retry
46
59
  if (res.status >= 500) {
47
- if (attempt < retries) {
60
+ if (attempt < retries && !controller.signal.aborted) {
48
61
  await delay(retryDelayMs * (attempt + 1));
49
62
  continue;
50
63
  }
@@ -53,8 +66,12 @@ async function safeFetch(url, options = {}) {
53
66
  return res;
54
67
  }
55
68
  catch (error) {
56
- clearTimeout(id);
69
+ cleanup();
57
70
  lastError = error;
71
+ // Do not retry if the external aggregator specifically aborted us
72
+ if (externalSignal && externalSignal.aborted) {
73
+ throw new HttpError(499, 'Client Closed Request', '');
74
+ }
58
75
  // AbortError is a timeout
59
76
  if (error.name === 'AbortError') {
60
77
  if (attempt < retries) {
@@ -64,7 +81,7 @@ async function safeFetch(url, options = {}) {
64
81
  throw new HttpError(408, 'Request Timeout', '');
65
82
  }
66
83
  // Network errors (like fetch failed)
67
- if (attempt < retries) {
84
+ if (attempt < retries && !controller.signal.aborted) {
68
85
  await delay(retryDelayMs * (attempt + 1));
69
86
  continue;
70
87
  }
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.routeTransaction = routeTransaction;
4
4
  const routeSelector_1 = require("./routeSelector");
5
- async function routeTransaction(fromChain, toChain, fromToken, toToken, amountInWei, userAddress, slippageTolerance = "auto") {
5
+ async function routeTransaction(fromChain, toChain, fromToken, toToken, amountInWei, amountFormatted, userAddress, slippageTolerance = "auto", providerName) {
6
6
  fromChain = String(fromChain || "");
7
7
  toChain = String(toChain || "");
8
8
  if (!fromChain || !toChain) {
@@ -35,8 +35,10 @@ async function routeTransaction(fromChain, toChain, fromToken, toToken, amountIn
35
35
  fromToken,
36
36
  toToken,
37
37
  amountInWei,
38
+ amountFormatted,
38
39
  userAddress,
39
- slippageTolerance
40
+ slippageTolerance,
41
+ preferredProvider: providerName && providerName !== "auto" ? providerName : undefined
40
42
  };
41
43
  console.log(`[DeFi Router] Routing transaction via Extensible Provider Runtime...`);
42
44
  return await (0, routeSelector_1.fetchBestRoute)(request, "best_output");
@@ -91,7 +91,14 @@ class AggregatorRegistry {
91
91
  fs_1.default.mkdirSync(providersDir, { recursive: true });
92
92
  return;
93
93
  }
94
- const files = fs_1.default.readdirSync(providersDir).filter(f => f.endsWith('.ts') || f.endsWith('.js'));
94
+ const isCompiled = __dirname.includes('dist') || __dirname.includes('build') || process.env.NODE_ENV === 'production';
95
+ const files = fs_1.default.readdirSync(providersDir).filter(f => {
96
+ if (f.endsWith('.d.ts'))
97
+ return false;
98
+ if (isCompiled)
99
+ return f.endsWith('.js');
100
+ return f.endsWith('.ts') || f.endsWith('.js');
101
+ });
95
102
  for (const file of files) {
96
103
  const fullPath = path_1.default.join(providersDir, file);
97
104
  try {
@@ -38,7 +38,7 @@ class KyberSwapProvider {
38
38
  const slipParam = request.slippageTolerance === "auto" ? 50 : (request.slippageTolerance * 100);
39
39
  // Phase 1: Route
40
40
  const routeUrl = `https://aggregator-api.kyberswap.com/${chainName}/api/v1/routes?tokenIn=${request.fromToken}&tokenOut=${request.toToken}&amountIn=${request.amountInWei}`;
41
- const routeRes = await (0, httpClient_1.safeFetch)(routeUrl, { signal: context.abortSignal });
41
+ const routeRes = await (0, httpClient_1.safeFetch)(routeUrl, { signal: context.abortSignal, retries: 0 });
42
42
  if (!routeRes.ok)
43
43
  throw new Error(`KyberSwap Route Error: ${await routeRes.text()}`);
44
44
  const routeData = await routeRes.json();
@@ -55,13 +55,17 @@ class KyberSwapProvider {
55
55
  method: 'POST',
56
56
  headers: { 'Content-Type': 'application/json' },
57
57
  body: JSON.stringify(buildPayload),
58
- signal: context.abortSignal
58
+ signal: context.abortSignal,
59
+ retries: 0
59
60
  });
60
61
  if (!buildRes.ok)
61
62
  throw new Error(`KyberSwap Build Error: ${await buildRes.text()}`);
62
63
  const buildData = await buildRes.json();
63
- const isNative = request.fromToken.toLowerCase() === '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ||
64
- request.fromToken === '0x0000000000000000000000000000000000000000';
64
+ // KyberSwap returns calldata in 'data' field, and router address in 'routerAddress'
65
+ if (!buildData.data || !buildData.data.routerAddress || !buildData.data.data) {
66
+ throw new Error(`KyberSwap build response missing required fields (routerAddress or data). Got: ${JSON.stringify(Object.keys(buildData.data || {}))}`);
67
+ }
68
+ const isNative = request.fromToken.toLowerCase() === '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
65
69
  return {
66
70
  provider: this.manifest.name,
67
71
  routeId: `kyber-${crypto_1.default.randomUUID()}`,
@@ -44,17 +44,29 @@ class LifiProvider {
44
44
  async getQuote(request, context) {
45
45
  const key = context.apiKeys['lifi_key'];
46
46
  const slipParam = request.slippageTolerance === "auto" ? 0.005 : (request.slippageTolerance / 100);
47
- const url = `https://li.quest/v1/quote?fromChain=${request.fromChain}&toChain=${request.toChain}&fromToken=${request.fromToken}&toToken=${request.toToken}&fromAmount=${request.amountInWei}&fromAddress=${request.userAddress}&slippage=${slipParam}`;
47
+ const fromChainId = CHAIN_IDS[request.fromChain];
48
+ const toChainId = CHAIN_IDS[request.toChain];
49
+ const url = `https://li.quest/v1/quote?fromChain=${fromChainId}&toChain=${toChainId}&fromToken=${request.fromToken}&toToken=${request.toToken}&fromAmount=${request.amountInWei}&fromAddress=${request.userAddress}&slippage=${slipParam}`;
48
50
  const headers = {};
49
51
  if (key)
50
52
  headers['x-lifi-api-key'] = key;
51
53
  const res = await (0, httpClient_1.safeFetch)(url, {
52
54
  headers,
53
- signal: context.abortSignal
55
+ signal: context.abortSignal,
56
+ retries: 0
54
57
  });
55
58
  if (!res.ok)
56
59
  throw new Error(`LI.FI API Error: ${await res.text()}`);
57
60
  const data = await res.json();
61
+ // LiFi can return HTTP 200 with an 'errors' array if a tool fails
62
+ if (data.errors && data.errors.length > 0) {
63
+ const err = data.errors[0];
64
+ throw new Error(`LI.FI route error (${err.code || 'UNKNOWN'}): ${err.message}`);
65
+ }
66
+ // transactionRequest may be null if LiFi found no executable route
67
+ if (!data.transactionRequest || !data.transactionRequest.to) {
68
+ throw new Error(`[LifiProvider] No valid transactionRequest returned — no executable route found`);
69
+ }
58
70
  return {
59
71
  provider: this.manifest.name,
60
72
  routeId: `lifi-${crypto_1.default.randomUUID()}`,
@@ -51,10 +51,12 @@ class OneInchProvider {
51
51
  }
52
52
  const chainId = CHAIN_IDS[request.fromChain];
53
53
  const slippage = request.slippageTolerance === "auto" ? "0.5" : request.slippageTolerance.toString();
54
+ // Re-added &disableEstimate=true to prevent 1inch API from returning 400 Bad Request on unapproved tokens
54
55
  const url = `https://api.1inch.dev/swap/v6.0/${chainId}/swap?src=${request.fromToken}&dst=${request.toToken}&amount=${request.amountInWei}&from=${request.userAddress}&slippage=${slippage}&disableEstimate=true`;
55
56
  const res = await (0, httpClient_1.safeFetch)(url, {
56
57
  headers: { 'Authorization': `Bearer ${key}` },
57
- signal: context.abortSignal
58
+ signal: context.abortSignal,
59
+ retries: 0
58
60
  });
59
61
  if (!res.ok) {
60
62
  throw new Error(`1inch API Error: ${await res.text()}`);
@@ -33,8 +33,16 @@ class OpBridgeProvider {
33
33
  return isNative;
34
34
  }
35
35
  async getQuote(request, context) {
36
- // Official OP Stack L1 Standard Bridge on Sepolia
37
- const bridgeAddress = '0xFBb0621E0B23b5478B630BD55a5f21f67730B0F1';
36
+ // Each OP Stack L2 has its OWN L1StandardBridgeProxy on Sepolia L1
37
+ // Using the wrong address sends ETH to the wrong contract = loss of funds
38
+ const BRIDGE_ADDRESSES = {
39
+ optimism_sepolia: '0xFBb0621E0B23b5478B630BD55a5f21f67730B0F1', // Confirmed: OP Sepolia L1StandardBridgeProxy
40
+ base_sepolia: '0xfd0Bf71F60660E2f608ed56e1659C450eB113120', // Confirmed: Base Sepolia L1StandardBridgeProxy
41
+ };
42
+ const bridgeAddress = BRIDGE_ADDRESSES[request.toChain];
43
+ if (!bridgeAddress) {
44
+ throw new Error(`[OpBridgeProvider] No bridge address configured for destination chain: ${request.toChain}`);
45
+ }
38
46
  // ABI for depositETH
39
47
  const depositEthAbi = (0, viem_1.parseAbi)([
40
48
  'function depositETH(uint32 _minGasLimit, bytes _extraData) payable'
@@ -1,6 +1,14 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.OpenOceanProvider = void 0;
7
+ const httpClient_1 = require("../../../utils/httpClient");
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ const CHAIN_IDS = {
10
+ ethereum: 1, base: 8453, bsc: 56, arbitrum: 42161, optimism: 10, polygon: 137
11
+ };
4
12
  class OpenOceanProvider {
5
13
  manifest = {
6
14
  id: 'openocean',
@@ -8,7 +16,7 @@ class OpenOceanProvider {
8
16
  version: '1.0.0',
9
17
  networks: ['mainnet'],
10
18
  capabilities: ['swap'],
11
- allowedDomains: ['openocean.finance'],
19
+ allowedDomains: ['open-api.openocean.finance'],
12
20
  permissions: {
13
21
  network: true,
14
22
  walletAccess: 'none',
@@ -16,17 +24,65 @@ class OpenOceanProvider {
16
24
  }
17
25
  };
18
26
  isCrossChainSupported() {
19
- return true;
27
+ return false;
20
28
  }
21
29
  supports(request) {
22
- // Mock implementation for MVP
23
- return false;
30
+ if (request.fromChain !== request.toChain)
31
+ return false;
32
+ if (!CHAIN_IDS[request.fromChain])
33
+ return false;
34
+ return true;
24
35
  }
25
36
  async getQuote(request, context) {
26
- throw new Error('OpenOcean Not Implemented in Provider Architecture yet');
37
+ const chainId = CHAIN_IDS[request.fromChain];
38
+ const slippage = request.slippageTolerance === "auto" ? "1" : request.slippageTolerance.toString();
39
+ const inTokenAddress = request.fromToken;
40
+ const outTokenAddress = request.toToken;
41
+ // OpenOcean v3 API requires the human-readable amount (e.g., '1' for 1 USDC), not wei.
42
+ if (!request.amountFormatted) {
43
+ throw new Error("OpenOceanProvider requires 'amountFormatted' (decimal string) in QuoteRequest.");
44
+ }
45
+ const amount = request.amountFormatted;
46
+ const account = request.userAddress;
47
+ const url = `https://open-api.openocean.finance/v3/${chainId}/swap_quote?inTokenAddress=${inTokenAddress}&outTokenAddress=${outTokenAddress}&amount=${amount}&slippage=${slippage}&account=${account}&gasPrice=5`;
48
+ const res = await (0, httpClient_1.safeFetch)(url, {
49
+ signal: context.abortSignal,
50
+ retries: 0
51
+ });
52
+ if (!res.ok) {
53
+ throw new Error(`OpenOcean API Error: ${await res.text()}`);
54
+ }
55
+ const json = await res.json();
56
+ if (json.code !== 200 || !json.data) {
57
+ throw new Error(`OpenOcean Route Error: ${json.error || 'Unknown error'}`);
58
+ }
59
+ const data = json.data;
60
+ return {
61
+ provider: this.manifest.name,
62
+ routeId: `openocean-${crypto_1.default.randomUUID()}`,
63
+ fromChainId: chainId,
64
+ toChainId: chainId,
65
+ inputAmount: BigInt(amount),
66
+ outputAmount: BigInt(data.outAmount),
67
+ executable: true,
68
+ expiresAt: Date.now() + 60000,
69
+ approvalAddress: data.to,
70
+ execution: {
71
+ target: data.to,
72
+ calldata: data.data,
73
+ value: BigInt(data.value || 0)
74
+ },
75
+ raw: json
76
+ };
27
77
  }
28
- async isHealthy() {
29
- return { ok: false, checkedAt: Date.now(), reason: 'Not Implemented' };
78
+ async isHealthy(context) {
79
+ try {
80
+ const res = await (0, httpClient_1.safeFetch)('https://open-api.openocean.finance/v3/1/tokenList?limit=1', { signal: context.abortSignal });
81
+ return { ok: res.ok, checkedAt: Date.now() };
82
+ }
83
+ catch (e) {
84
+ return { ok: false, checkedAt: Date.now(), reason: e.message };
85
+ }
30
86
  }
31
87
  }
32
88
  exports.OpenOceanProvider = OpenOceanProvider;
@@ -6,9 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.RelayProvider = void 0;
7
7
  const httpClient_1 = require("../../../utils/httpClient");
8
8
  const crypto_1 = __importDefault(require("crypto"));
9
+ // Relay.link expects chain IDs as integers (number), not strings
9
10
  const CHAIN_IDS = {
10
- ethereum: '1', base: '8453', bsc: '56', arbitrum: '42161', optimism: '10', polygon: '137',
11
- sepolia: '11155111', base_sepolia: '84532'
11
+ ethereum: 1, base: 8453, bsc: 56, arbitrum: 42161, optimism: 10, polygon: 137,
12
+ sepolia: 11155111, base_sepolia: 84532
12
13
  };
13
14
  class RelayProvider {
14
15
  manifest = {
@@ -36,16 +37,16 @@ class RelayProvider {
36
37
  async getQuote(request, context) {
37
38
  const isTestnet = request.fromChain.includes('sepolia') || request.toChain.includes('sepolia');
38
39
  const baseUrl = isTestnet ? 'https://api.testnets.relay.link' : 'https://api.relay.link';
39
- const originChainId = CHAIN_IDS[request.fromChain];
40
- const destChainId = CHAIN_IDS[request.toChain];
40
+ const originChainId = CHAIN_IDS[request.fromChain]; // number, as required by Relay API
41
+ const destChainId = CHAIN_IDS[request.toChain]; // number, as required by Relay API
41
42
  const originCurrency = request.fromToken.toLowerCase() === '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
42
43
  ? '0x0000000000000000000000000000000000000000' : request.fromToken;
43
44
  const destCurrency = request.toToken.toLowerCase() === '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
44
45
  ? '0x0000000000000000000000000000000000000000' : request.toToken;
45
46
  const payload = {
46
47
  user: request.userAddress,
47
- originChainId,
48
- destinationChainId: destChainId,
48
+ originChainId, // now a number, not string
49
+ destinationChainId: destChainId, // now a number, not string
49
50
  originCurrency,
50
51
  destinationCurrency: destCurrency,
51
52
  recipient: request.userAddress,
@@ -58,7 +59,8 @@ class RelayProvider {
58
59
  method: 'POST',
59
60
  headers: { 'Content-Type': 'application/json' },
60
61
  body: JSON.stringify(payload),
61
- signal: context.abortSignal
62
+ signal: context.abortSignal,
63
+ retries: 0
62
64
  });
63
65
  if (!res.ok)
64
66
  throw new Error(`Relay API Error: ${await res.text()}`);
@@ -66,14 +68,23 @@ class RelayProvider {
66
68
  const txPayload = data.steps?.[0]?.items?.[0]?.data;
67
69
  if (!txPayload)
68
70
  throw new Error("Missing Relay transaction payload");
71
+ // Relay /quote/v2 may structure outputAmount differently; provide fallbacks
72
+ const outputAmount = BigInt(data.details?.currencyOut?.amount ||
73
+ data.details?.currencyOut?.minimumAmount ||
74
+ 0);
75
+ if (outputAmount === 0n) {
76
+ throw new Error("Relay API returned 0 output amount or invalid route details.");
77
+ }
78
+ // Determine executability from whether we have a valid tx payload, not an undocumented 'executable' field
79
+ const isExecutable = !!txPayload && !!txPayload.to && !!txPayload.data;
69
80
  return {
70
81
  provider: this.manifest.name,
71
82
  routeId: `relay-${crypto_1.default.randomUUID()}`,
72
83
  fromChainId: Number(originChainId),
73
84
  toChainId: Number(destChainId),
74
85
  inputAmount: BigInt(request.amountInWei),
75
- outputAmount: BigInt(data.details?.currencyOut?.amount || 0),
76
- executable: true,
86
+ outputAmount: outputAmount,
87
+ executable: isExecutable,
77
88
  expiresAt: Date.now() + 60000,
78
89
  execution: {
79
90
  target: txPayload.to,
@@ -13,7 +13,7 @@ class ZeroXProvider {
13
13
  manifest = {
14
14
  id: '0x',
15
15
  name: '0x API',
16
- version: '1.0.0',
16
+ version: '2.0.0', // Updated to v2
17
17
  networks: ['mainnet'],
18
18
  capabilities: ['swap'],
19
19
  requiredApiKeys: [
@@ -23,9 +23,10 @@ class ZeroXProvider {
23
23
  envKey: 'ZEROX_API_KEY',
24
24
  required: true,
25
25
  secret: true,
26
- docsUrl: 'https://0x.org/docs/0x-swap-api/introduction'
26
+ docsUrl: 'https://0x.org/docs/introduction/getting-started'
27
27
  }
28
28
  ],
29
+ // v2 uses a single unified endpoint, no longer chain-specific subdomains
29
30
  allowedDomains: ['api.0x.org'],
30
31
  permissions: {
31
32
  network: true,
@@ -47,28 +48,55 @@ class ZeroXProvider {
47
48
  const key = context.apiKeys['zero_x_key'];
48
49
  if (!key)
49
50
  throw new Error(`[ZeroXProvider] Missing required API key 'zero_x_key'`);
50
- const slipParam = request.slippageTolerance === "auto" ? "0.005" : (request.slippageTolerance / 100).toString();
51
- const url = `https://api.0x.org/swap/v1/quote?sellToken=${request.fromToken}&buyToken=${request.toToken}&sellAmount=${request.amountInWei}&takerAddress=${request.userAddress}&slippagePercentage=${slipParam}`;
51
+ const chainId = CHAIN_IDS[request.fromChain];
52
+ // v2: slippage is in BASIS POINTS (bps), NOT percentage decimal
53
+ // 0.5% = 50 bps
54
+ const slipBps = request.slippageTolerance === "auto"
55
+ ? "50"
56
+ : Math.round(request.slippageTolerance * 100).toString();
57
+ // v2: unified URL, chain differentiated by chainId param
58
+ // Using /allowance-holder/quote as it's simpler (no EIP-712 signing required)
59
+ const params = new URLSearchParams({
60
+ chainId: String(chainId),
61
+ sellToken: request.fromToken,
62
+ buyToken: request.toToken,
63
+ sellAmount: request.amountInWei,
64
+ taker: request.userAddress, // v2: 'taker' replaces v1's 'takerAddress'
65
+ slippageBps: slipBps, // v2: 'slippageBps' replaces v1's 'slippagePercentage'
66
+ });
67
+ const url = `https://api.0x.org/swap/allowance-holder/quote?${params.toString()}`;
52
68
  const res = await (0, httpClient_1.safeFetch)(url, {
53
- headers: { '0x-api-key': key },
54
- signal: context.abortSignal
69
+ headers: {
70
+ '0x-api-key': key,
71
+ '0x-version': 'v2' // v2: mandatory version header
72
+ },
73
+ signal: context.abortSignal,
74
+ retries: 0
55
75
  });
56
76
  if (!res.ok)
57
- throw new Error(`0x API Error: ${await res.text()}`);
77
+ throw new Error(`0x API v2 Error: ${await res.text()}`);
58
78
  const data = await res.json();
79
+ if (data.liquidityAvailable === false) {
80
+ throw new Error(`[ZeroXProvider] No liquidity available for this pair`);
81
+ }
82
+ // v2: transaction fields are nested under data.transaction, NOT root level
83
+ if (!data.transaction || !data.transaction.to || !data.transaction.data) {
84
+ throw new Error(`[ZeroXProvider] Missing transaction payload in v2 response`);
85
+ }
59
86
  return {
60
87
  provider: this.manifest.name,
61
88
  routeId: `0x-${crypto_1.default.randomUUID()}`,
62
- fromChainId: CHAIN_IDS[request.fromChain],
63
- toChainId: CHAIN_IDS[request.toChain],
89
+ fromChainId: chainId,
90
+ toChainId: chainId,
64
91
  inputAmount: BigInt(request.amountInWei),
65
92
  outputAmount: BigInt(data.buyAmount),
66
93
  executable: true,
67
94
  expiresAt: Date.now() + 60000,
95
+ approvalAddress: data.issues?.allowance?.spender || data.transaction.to,
68
96
  execution: {
69
- target: data.to,
70
- calldata: data.data,
71
- value: BigInt(data.value || 0)
97
+ target: data.transaction.to, // FIXED: was data.to in v1
98
+ calldata: data.transaction.data, // FIXED: was data.data in v1
99
+ value: BigInt(data.transaction.value || 0) // FIXED: was data.value in v1
72
100
  },
73
101
  raw: data
74
102
  };
@@ -8,8 +8,9 @@ const providerRegistry_1 = require("./providerRegistry");
8
8
  const quoteValidator_1 = require("./quoteValidator");
9
9
  const routeScorer_1 = require("./routeScorer");
10
10
  const defiConfigManager_1 = require("../../config/defiConfigManager");
11
+ const providerHealthService_1 = require("./providerHealthService");
11
12
  const crypto_1 = __importDefault(require("crypto"));
12
- const PROVIDER_TIMEOUT_MS = 4000;
13
+ // Dynamic timeout implemented inside fetchBestRoute
13
14
  function withTimeout(promise, ms, signal) {
14
15
  return new Promise((resolve, reject) => {
15
16
  const timer = setTimeout(() => reject(new Error('Provider timeout')), ms);
@@ -33,7 +34,10 @@ function withTimeout(promise, ms, signal) {
33
34
  }
34
35
  async function fetchBestRoute(request, preference = "best_output") {
35
36
  // 1. Resolve eligible providers
36
- const eligibleProviders = providerRegistry_1.aggregatorRegistry.resolveEligibleProviders(request);
37
+ let eligibleProviders = providerRegistry_1.aggregatorRegistry.resolveEligibleProviders(request);
38
+ if (request.preferredProvider && request.preferredProvider !== "auto") {
39
+ eligibleProviders = eligibleProviders.filter(p => p.manifest.id === request.preferredProvider);
40
+ }
37
41
  if (eligibleProviders.length === 0) {
38
42
  throw new Error('[RouteSelector] No eligible providers found for this route.');
39
43
  }
@@ -45,8 +49,10 @@ async function fetchBestRoute(request, preference = "best_output") {
45
49
  abortSignal: controller.signal,
46
50
  apiKeys: keys
47
51
  };
48
- // 3. Parallel fetch with timeout
49
- const promises = eligibleProviders.map(provider => withTimeout(provider.getQuote(request, context), PROVIDER_TIMEOUT_MS, controller.signal)
52
+ // 3. Parallel fetch with adaptive timeout
53
+ // Same-chain swaps are fast, but cross-chain bridges (Relay, LiFi) need more time to calculate routes
54
+ const timeoutMs = request.fromChain === request.toChain ? 4000 : 8000;
55
+ const promises = eligibleProviders.map(provider => withTimeout(provider.getQuote(request, context), timeoutMs, controller.signal)
50
56
  .then(quote => {
51
57
  // Validate immediately after receiving
52
58
  const error = quoteValidator_1.quoteValidator.validate(quote, request);
@@ -56,21 +62,26 @@ async function fetchBestRoute(request, preference = "best_output") {
56
62
  return quote;
57
63
  }));
58
64
  const settled = await Promise.allSettled(promises);
59
- // 4. Collect fulfilled quotes
60
- const quotes = settled
61
- .filter((result) => result.status === 'fulfilled')
62
- .map(result => result.value);
65
+ // 4. Collect fulfilled quotes and record health
66
+ const quotes = [];
67
+ settled.forEach((result, i) => {
68
+ const providerId = eligibleProviders[i].manifest.id;
69
+ if (result.status === 'fulfilled') {
70
+ quotes.push(result.value);
71
+ providerHealthService_1.healthService.recordSuccess(providerId);
72
+ }
73
+ else {
74
+ console.warn(`[RouteSelector] Provider ${eligibleProviders[i].manifest.name} failed:`, result.reason);
75
+ providerHealthService_1.healthService.recordFailure(providerId, result.reason?.message || "Unknown error");
76
+ }
77
+ });
63
78
  if (quotes.length === 0) {
64
- // Log reasons for failure
65
- settled.forEach((res, i) => {
66
- if (res.status === 'rejected') {
67
- console.warn(`[RouteSelector] Provider ${eligibleProviders[i].manifest.name} failed:`, res.reason);
68
- }
69
- });
70
79
  throw new Error('[RouteSelector] All providers failed or timed out. No route found.');
71
80
  }
72
81
  // 5. Score and select best
73
82
  const bestQuote = routeScorer_1.routeScorer.selectBest(quotes, request, preference);
83
+ // Abort any slow providers still running in background
84
+ controller.abort();
74
85
  if (!bestQuote) {
75
86
  throw new Error('[RouteSelector] Failed to score quotes. No route found.');
76
87
  }