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.
- package/dist/packages/core/src/agent/llmProvider.js +10 -3
- package/dist/packages/core/src/gateway/server.js +1 -1
- package/dist/packages/core/src/memory/logger.js +6 -6
- package/dist/packages/core/src/utils/httpClient.js +22 -5
- package/dist/packages/core/src/web3/aggregator/defiRouter.js +4 -2
- package/dist/packages/core/src/web3/aggregator/providerRegistry.js +8 -1
- package/dist/packages/core/src/web3/aggregator/providers/KyberSwapProvider.js +8 -4
- package/dist/packages/core/src/web3/aggregator/providers/LifiProvider.js +14 -2
- package/dist/packages/core/src/web3/aggregator/providers/OneInchProvider.js +3 -1
- package/dist/packages/core/src/web3/aggregator/providers/OpBridgeProvider.js +10 -2
- package/dist/packages/core/src/web3/aggregator/providers/OpenOceanProvider.js +63 -7
- package/dist/packages/core/src/web3/aggregator/providers/RelayProvider.js +20 -9
- package/dist/packages/core/src/web3/aggregator/providers/ZeroXProvider.js +40 -12
- package/dist/packages/core/src/web3/aggregator/routeSelector.js +25 -14
- package/dist/packages/core/src/web3/skills/bridgeToken.js +47 -3
- package/dist/packages/core/src/web3/skills/swapToken.js +47 -4
- package/dist/packages/core/src/web3/utils/tokens.js +12 -12
- package/dist/packages/core/src/web3/utils/vaultClient.js +6 -0
- package/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/packages/core/src/agent/llmProvider.ts +14 -3
- package/packages/core/src/gateway/server.ts +1 -1
- package/packages/core/src/memory/logger.ts +6 -6
- package/packages/core/src/utils/httpClient.ts +22 -5
- package/packages/core/src/web3/aggregator/defiRouter.ts +6 -2
- package/packages/core/src/web3/aggregator/providerRegistry.ts +7 -1
- package/packages/core/src/web3/aggregator/providers/KyberSwapProvider.ts +9 -4
- package/packages/core/src/web3/aggregator/providers/LifiProvider.ts +16 -2
- package/packages/core/src/web3/aggregator/providers/OneInchProvider.ts +3 -2
- package/packages/core/src/web3/aggregator/providers/OpBridgeProvider.ts +11 -2
- package/packages/core/src/web3/aggregator/providers/OpenOceanProvider.ts +66 -7
- package/packages/core/src/web3/aggregator/providers/RelayProvider.ts +25 -10
- package/packages/core/src/web3/aggregator/providers/ZeroXProvider.ts +45 -12
- package/packages/core/src/web3/aggregator/routeSelector.ts +29 -16
- package/packages/core/src/web3/aggregator/types.ts +3 -0
- package/packages/core/src/web3/skills/bridgeToken.ts +19 -4
- package/packages/core/src/web3/skills/swapToken.ts +17 -5
- package/packages/core/src/web3/utils/tokens.ts +12 -12
- package/packages/core/src/web3/utils/vaultClient.ts +7 -0
- package/packages/dashboard/dist/assets/{index-C_WmWSch.js → index-DlwR7UtR.js} +1 -1
- package/packages/dashboard/dist/index.html +1 -1
- package/packages/dashboard/package.json +1 -1
- package/packages/mcp-server/package.json +1 -1
- package/packages/policy/package.json +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
//
|
|
37
|
-
|
|
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
|
|
27
|
+
return false;
|
|
20
28
|
}
|
|
21
29
|
supports(request) {
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
11
|
-
sepolia:
|
|
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:
|
|
76
|
-
executable:
|
|
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: '
|
|
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/
|
|
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
|
|
51
|
-
|
|
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: {
|
|
54
|
-
|
|
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:
|
|
63
|
-
toChainId:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
61
|
-
|
|
62
|
-
|
|
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
|
}
|