nyxora 1.6.13 → 1.7.1

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 (69) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +2 -2
  3. package/bin/nyxora.mjs +8 -0
  4. package/package.json +1 -2
  5. package/packages/core/package.json +1 -1
  6. package/packages/core/src/agent/limitOrderManager.ts +2 -2
  7. package/packages/core/src/agent/reasoning.ts +36 -14
  8. package/packages/core/src/config/parser.ts +99 -9
  9. package/packages/core/src/gateway/cli.ts +28 -2
  10. package/packages/core/src/gateway/setup.ts +45 -10
  11. package/packages/core/src/gateway/telegram.ts +53 -12
  12. package/packages/core/src/system/skills/searchWeb.ts +187 -21
  13. package/packages/core/src/utils/formatter.ts +12 -5
  14. package/packages/core/src/web3/config.ts +7 -1
  15. package/packages/core/src/web3/skills/bridgeToken.ts +4 -3
  16. package/packages/core/src/web3/skills/checkAddress.ts +2 -2
  17. package/packages/core/src/web3/skills/checkPortfolio.ts +55 -39
  18. package/packages/core/src/web3/skills/checkSecurity.ts +3 -2
  19. package/packages/core/src/web3/skills/customTx.ts +2 -2
  20. package/packages/core/src/web3/skills/getBalance.ts +3 -3
  21. package/packages/core/src/web3/skills/marketAnalysis.ts +2 -2
  22. package/packages/core/src/web3/skills/mintNft.ts +2 -2
  23. package/packages/core/src/web3/skills/swapToken.ts +4 -3
  24. package/packages/core/src/web3/skills/transfer.ts +2 -2
  25. package/packages/core/src/web3/utils/tokens.ts +8 -0
  26. package/packages/dashboard/dist/assets/{index-24OeXn-k.css → index-BSk4CLkG.css} +1 -1
  27. package/packages/dashboard/dist/assets/{index-BuYfTEKE.js → index-Dc3Tu0Te.js} +21 -21
  28. package/packages/dashboard/dist/index.html +2 -2
  29. package/packages/dashboard/package.json +1 -1
  30. package/packages/mcp-server/package.json +1 -1
  31. package/packages/policy/package.json +1 -1
  32. package/packages/signer/package.json +1 -1
  33. package/launcher.js +0 -48
  34. package/packages/core/src/agent/reasoning.d.ts.map +0 -1
  35. package/packages/core/src/config/parser.d.ts.map +0 -1
  36. package/packages/core/src/gateway/cli.d.ts.map +0 -1
  37. package/packages/core/src/memory/logger.d.ts.map +0 -1
  38. package/packages/core/src/utils/safeLogger.js +0 -59
  39. package/packages/core/src/web3/config.d.ts.map +0 -1
  40. package/packages/core/src/web3/skills/getBalance.d.ts.map +0 -1
  41. package/packages/dashboard/public/favicon.svg +0 -10
  42. package/packages/dashboard/public/icons.svg +0 -24
  43. package/packages/dashboard/src/App.css +0 -184
  44. package/packages/dashboard/src/App.tsx +0 -588
  45. package/packages/dashboard/src/BalanceWidget.tsx +0 -65
  46. package/packages/dashboard/src/MarketWidget.tsx +0 -73
  47. package/packages/dashboard/src/NetworkSelector.tsx +0 -64
  48. package/packages/dashboard/src/NyxoraLogo.tsx +0 -25
  49. package/packages/dashboard/src/OsSkills.tsx +0 -352
  50. package/packages/dashboard/src/Overview.tsx +0 -157
  51. package/packages/dashboard/src/PendingTransactions.tsx +0 -75
  52. package/packages/dashboard/src/Settings.tsx +0 -338
  53. package/packages/dashboard/src/Skills.tsx +0 -200
  54. package/packages/dashboard/src/SwapWidget.tsx +0 -141
  55. package/packages/dashboard/src/TransactionWidget.tsx +0 -95
  56. package/packages/dashboard/src/assets/hero.png +0 -0
  57. package/packages/dashboard/src/assets/react.svg +0 -1
  58. package/packages/dashboard/src/assets/vite.svg +0 -1
  59. package/packages/dashboard/src/components/PillSelect.tsx +0 -65
  60. package/packages/dashboard/src/index.css +0 -807
  61. package/packages/dashboard/src/main.tsx +0 -10
  62. package/packages/dashboard/src/overview.css +0 -304
  63. package/packages/dashboard/src/utils/api.ts +0 -31
  64. package/packages/mcp-server/tsconfig.tsbuildinfo +0 -1
  65. package/test-address.ts +0 -11
  66. package/test-all-chains.ts +0 -19
  67. package/test-db.ts +0 -3
  68. package/test-portfolio.ts +0 -14
  69. package/tsconfig.tsbuildinfo +0 -1
@@ -1,43 +1,209 @@
1
- import { search, SafeSearchType } from 'duck-duck-scrape';
1
+ import { loadConfig, loadApiKeys } from '../../config/parser';
2
2
 
3
- export async function searchWeb(query: string): Promise<string> {
4
- try {
5
- const searchResults = await search(query, {
6
- safeSearch: SafeSearchType.MODERATE
7
- });
3
+ interface SearchQueryResult {
4
+ title: string;
5
+ url: string;
6
+ content: string;
7
+ }
8
+
9
+ const SEARXNG_INSTANCES = [
10
+ 'https://search.mdosch.de',
11
+ 'https://searx.tiekoetter.com',
12
+ 'https://paulgo.io',
13
+ 'https://searx.be',
14
+ 'https://searx.fmac.network'
15
+ ];
16
+
17
+ async function searchTavily(query: string, apiKey: string, depth: number = 1): Promise<SearchQueryResult[]> {
18
+ const searchDepth = depth > 1 ? 'advanced' : 'basic';
19
+ const maxResults = depth > 1 ? 15 : 8;
20
+ const res = await fetch('https://api.tavily.com/search', {
21
+ method: 'POST',
22
+ headers: { 'Content-Type': 'application/json' },
23
+ body: JSON.stringify({ api_key: apiKey, query, search_depth: searchDepth, max_results: maxResults })
24
+ });
25
+
26
+ if (!res.ok) {
27
+ const status = res.status;
28
+ throw new Error(`[Tavily Error] Status: ${status}`);
29
+ }
30
+
31
+ const json = await res.json();
32
+ if (!json.results) return [];
33
+
34
+ return json.results.map((r: any) => ({
35
+ title: r.title,
36
+ url: r.url,
37
+ content: r.content
38
+ }));
39
+ }
8
40
 
9
- if (!searchResults.results || searchResults.results.length === 0) {
10
- return "No results found for your query.";
41
+ async function searchBrave(query: string, apiKey: string, depth: number = 1): Promise<SearchQueryResult[]> {
42
+ const q = encodeURIComponent(query);
43
+ const count = depth > 1 ? 15 : 8;
44
+ const res = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${q}&count=${count}`, {
45
+ headers: {
46
+ 'Accept': 'application/json',
47
+ 'X-Subscription-Token': apiKey
11
48
  }
49
+ });
50
+
51
+ if (!res.ok) {
52
+ const status = res.status;
53
+ throw new Error(`[Brave Error] Status: ${status}`);
54
+ }
55
+
56
+ const json = await res.json();
57
+ if (!json.web || !json.web.results) return [];
58
+
59
+ return json.web.results.map((r: any) => ({
60
+ title: r.title,
61
+ url: r.url,
62
+ content: r.description || r.title
63
+ }));
64
+ }
12
65
 
13
- // Limit to top 8 results
14
- const topResults = searchResults.results.slice(0, 8);
15
-
16
- let responseText = `Search Results for "${query}":\n\n`;
17
-
18
- topResults.forEach((result, index) => {
19
- responseText += `${index + 1}. ${result.title}\n`;
20
- responseText += `URL: ${result.url}\n`;
21
- responseText += `Snippet: ${result.description}\n\n`;
22
- });
66
+ async function searchSearxng(query: string, depth: number = 1): Promise<SearchQueryResult[]> {
67
+ const q = encodeURIComponent(query);
68
+ const maxResults = depth > 1 ? 15 : 8;
69
+ for (const url of SEARXNG_INSTANCES) {
70
+ try {
71
+ const res = await fetch(`${url}/search?q=${q}&format=json`, {
72
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' },
73
+ signal: AbortSignal.timeout(4000)
74
+ });
75
+
76
+ if (res.ok) {
77
+ const json = await res.json();
78
+ if (!json.results || json.results.length === 0) continue;
79
+
80
+ return json.results.slice(0, maxResults).map((r: any) => ({
81
+ title: r.title || 'No title',
82
+ url: r.url || '#',
83
+ content: r.content || r.snippet || r.description || 'No description available'
84
+ }));
85
+ }
86
+ } catch (e) {
87
+ continue;
88
+ }
89
+ }
90
+ throw new Error('[SearXNG Error] All decentralized instances failed.');
91
+ }
92
+
93
+ const searchCache = new Map<string, {data: SearchQueryResult[], timestamp: number}>();
94
+
95
+ export async function searchWeb(query: string, depth: number = 1): Promise<string> {
96
+ // Auto-inject current year for time-sensitive queries
97
+ const lowerQuery = query.toLowerCase();
98
+ const currentYear = new Date().getFullYear().toString();
99
+ let finalQuery = query;
100
+
101
+ if ((lowerQuery.includes('hari ini') || lowerQuery.includes('sekarang') || lowerQuery.includes('today') || lowerQuery.includes('saat ini') || lowerQuery.includes('terbaru')) && !lowerQuery.includes(currentYear)) {
102
+ finalQuery = `${query} ${currentYear}`;
103
+ console.log(`[WebSearch] Auto-injected current year: "${finalQuery}"`);
104
+ }
23
105
 
106
+ const cacheKey = `${finalQuery.trim().toLowerCase()}_depth_${depth}`;
107
+ const cached = searchCache.get(cacheKey);
108
+ if (cached && (Date.now() - cached.timestamp < 300000)) {
109
+ console.log(`[WebSearch] Returning cached results for: "${finalQuery}" (Depth: ${depth})`);
110
+ let responseText = `Search Results for "${query}" (Cached):\n\n`;
111
+ cached.data.forEach((r, index) => {
112
+ responseText += `${index + 1}. ${r.title}\n`;
113
+ responseText += `URL: ${r.url}\n`;
114
+ responseText += `Snippet: ${r.content}\n\n`;
115
+ });
24
116
  return responseText.trim();
25
- } catch (error: any) {
26
- return `Failed to search the web: ${error.message}`;
27
117
  }
118
+
119
+ const config = loadConfig();
120
+ const provider = config.web_search?.provider || 'mesh';
121
+ const vaultKeys = await loadApiKeys();
122
+ const creds = Object.keys(vaultKeys).length > 0 ? vaultKeys : (config.credentials || {});
123
+
124
+ let results: SearchQueryResult[] = [];
125
+
126
+ try {
127
+ if (provider === 'tavily' && creds.tavily_key) {
128
+ try {
129
+ results = await searchTavily(finalQuery, creds.tavily_key, depth);
130
+ } catch (e: any) {
131
+ if (e.message.includes('401') || e.message.includes('429')) {
132
+ console.warn('[WebSearch] Primary provider (Tavily) failed with 429/401. Switching to backup provider (Brave Search)...');
133
+ if (creds.brave_key) {
134
+ try {
135
+ results = await searchBrave(finalQuery, creds.brave_key, depth);
136
+ } catch (e2: any) {
137
+ console.warn('[WebSearch] Backup provider (Brave) failed. Falling back to SearXNG Mesh...');
138
+ results = await searchSearxng(finalQuery, depth);
139
+ }
140
+ } else {
141
+ console.warn('[WebSearch] No backup provider found. Falling back to SearXNG Mesh...');
142
+ results = await searchSearxng(finalQuery, depth);
143
+ }
144
+ } else {
145
+ throw e;
146
+ }
147
+ }
148
+ } else if (provider === 'brave' && creds.brave_key) {
149
+ try {
150
+ results = await searchBrave(finalQuery, creds.brave_key, depth);
151
+ } catch (e: any) {
152
+ if (e.message.includes('403') || e.message.includes('429')) {
153
+ console.warn('[WebSearch] Primary provider (Brave) failed with 429/403. Switching to backup provider (Tavily)...');
154
+ if (creds.tavily_key) {
155
+ try {
156
+ results = await searchTavily(finalQuery, creds.tavily_key, depth);
157
+ } catch (e2: any) {
158
+ console.warn('[WebSearch] Backup provider (Tavily) failed. Falling back to SearXNG Mesh...');
159
+ results = await searchSearxng(finalQuery, depth);
160
+ }
161
+ } else {
162
+ console.warn('[WebSearch] No backup provider found. Falling back to SearXNG Mesh...');
163
+ results = await searchSearxng(finalQuery, depth);
164
+ }
165
+ } else {
166
+ throw e;
167
+ }
168
+ }
169
+ } else {
170
+ results = await searchSearxng(finalQuery, depth);
171
+ }
172
+ } catch (e: any) {
173
+ return `Failed to search the web: ${e.message}`;
174
+ }
175
+
176
+ if (results.length > 0) {
177
+ searchCache.set(cacheKey, { data: results, timestamp: Date.now() });
178
+ } else {
179
+ return `Search Results for "${query}": No results found.`;
180
+ }
181
+
182
+ let responseText = `Search Results for "${query}":\n\n`;
183
+ results.forEach((r, index) => {
184
+ responseText += `${index + 1}. ${r.title}\n`;
185
+ responseText += `URL: ${r.url}\n`;
186
+ responseText += `Snippet: ${r.content}\n\n`;
187
+ });
188
+
189
+ return responseText.trim();
28
190
  }
29
191
 
30
192
  export const searchWebToolDefinition = {
31
193
  type: "function",
32
194
  function: {
33
195
  name: "search_web",
34
- description: "Searches the internet for information using a search engine. Returns top titles, snippets, and URLs. Use this to find current events, documentation, or general facts.",
196
+ description: "Searches the internet for information using a search engine. Returns top titles, snippets, and URLs. Use this to find current events, documentation, or general facts. If the user asks for deep/comprehensive research, pass depth: 2 or 3.",
35
197
  parameters: {
36
198
  type: "object",
37
199
  properties: {
38
200
  query: {
39
201
  type: "string",
40
202
  description: "The search query to look up.",
203
+ },
204
+ depth: {
205
+ type: "number",
206
+ description: "Depth of the search (1 for basic, 2 or 3 for deep comprehensive research). Default is 1.",
41
207
  }
42
208
  },
43
209
  required: ["query"],
@@ -1,6 +1,6 @@
1
1
  import { PendingTransaction } from '../agent/transactionManager';
2
2
 
3
- export function formatTransactionSuccess(tx: PendingTransaction, rawResult: string): string {
3
+ export function formatTransactionSuccess(tx: PendingTransaction, rawResult: string, isIndonesian: boolean = false): string {
4
4
  let txHash = 'N/A';
5
5
 
6
6
  try {
@@ -15,13 +15,20 @@ export function formatTransactionSuccess(tx: PendingTransaction, rawResult: stri
15
15
 
16
16
  const chainFormatted = tx.chainName.charAt(0).toUpperCase() + tx.chainName.slice(1);
17
17
 
18
+ let actionText = '';
18
19
  if (tx.type === 'swap') {
19
- return `Alright, I have completed the swap from ${tx.details.amount} ${tx.details.fromToken.toUpperCase()} to ${tx.details.toToken.toUpperCase()}.\n\nChain: ${chainFormatted}\nTx Hash:\n${txHash}`;
20
+ actionText = isIndonesian ? `Swap ${tx.details.amount} ${tx.details.fromToken.toUpperCase()} ke ${tx.details.toToken.toUpperCase()}` : `Swapped ${tx.details.amount} ${tx.details.fromToken.toUpperCase()} to ${tx.details.toToken.toUpperCase()}`;
20
21
  } else if (tx.type === 'transfer') {
21
- return `Alright, I have completed the transfer of ${tx.details.amountEth} tokens to ${tx.details.toAddress}.\n\nChain: ${chainFormatted}\nTx Hash:\n${txHash}`;
22
+ actionText = isIndonesian ? `Transfer ${tx.details.amountEth} token ke <code>${tx.details.toAddress}</code>` : `Transferred ${tx.details.amountEth} tokens to <code>${tx.details.toAddress}</code>`;
23
+ } else {
24
+ actionText = isIndonesian ? 'Aksi Berhasil' : 'Action Successful';
25
+ }
26
+
27
+ if (isIndonesian) {
28
+ return `**Nama Chain:** ${chainFormatted}\n**Status:** Sukses (${actionText})\n**Tx Hash:** <code>${txHash}</code>`;
29
+ } else {
30
+ return `**Chain Name:** ${chainFormatted}\n**Status:** Success (${actionText})\n**Tx Hash:** <code>${txHash}</code>`;
22
31
  }
23
-
24
- return `Transaction successful!\n\nChain: ${chainFormatted}\nTx Hash:\n${txHash}`;
25
32
  }
26
33
 
27
34
  export function formatTransactionError(tx: PendingTransaction, errorMsg: string): string {
@@ -1,5 +1,5 @@
1
1
  import { createPublicClient, http, fallback, PublicClient, Transport } from 'viem';
2
- import { mainnet, base, bsc, arbitrum, optimism, sepolia } from 'viem/chains';
2
+ import { mainnet, base, bsc, arbitrum, optimism, sepolia, polygon } from 'viem/chains';
3
3
  import { loadConfig } from '../config/parser';
4
4
 
5
5
  export const supportedChains = {
@@ -9,8 +9,10 @@ export const supportedChains = {
9
9
  arbitrum: arbitrum,
10
10
  optimism: optimism,
11
11
  sepolia: sepolia,
12
+ polygon: polygon,
12
13
  };
13
14
 
15
+ export const SUPPORTED_CHAIN_NAMES = Object.keys(supportedChains);
14
16
  export type ChainName = keyof typeof supportedChains;
15
17
 
16
18
  export function getPublicClient(chainName: ChainName): PublicClient {
@@ -53,6 +55,10 @@ export function getPublicClient(chainName: ChainName): PublicClient {
53
55
  } else if (chainName === 'sepolia') {
54
56
  transports.push(http('https://ethereum-sepolia-rpc.publicnode.com', { timeout: 5000 }));
55
57
  transports.push(http('https://rpc.sepolia.org', { timeout: 5000 }));
58
+ } else if (chainName === 'polygon') {
59
+ transports.push(http('https://polygon-rpc.publicnode.com', { timeout: 5000 }));
60
+ transports.push(http('https://polygon.llamarpc.com', { timeout: 5000 }));
61
+ transports.push(http('https://polygon-rpc.com', { timeout: 5000 }));
56
62
  }
57
63
  }
58
64
 
@@ -1,5 +1,5 @@
1
1
  import { parseUnits, formatUnits } from 'viem';
2
- import { getPublicClient, getAddress, ChainName } from '../config';
2
+ import { getPublicClient, getAddress, ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
3
3
  import { txManager } from '../../agent/transactionManager';
4
4
  import { resolveToken, ERC20_ABI } from '../utils/tokens';
5
5
 
@@ -10,6 +10,7 @@ const CHAIN_IDS: Record<ChainName, number> = {
10
10
  arbitrum: 42161,
11
11
  optimism: 10,
12
12
  sepolia: 11155111,
13
+ polygon: 137,
13
14
  };
14
15
 
15
16
  async function getLifiQuote(fromChainId: number, toChainId: number, fromToken: string, toToken: string, amountWei: string, userAddress: string, slippage: number) {
@@ -210,12 +211,12 @@ export const bridgeTokenToolDefinition = {
210
211
  properties: {
211
212
  fromChainName: {
212
213
  type: "string",
213
- enum: ["ethereum", "base", "bsc", "arbitrum", "optimism", "sepolia"],
214
+ enum: SUPPORTED_CHAIN_NAMES,
214
215
  description: "The source blockchain network",
215
216
  },
216
217
  toChainName: {
217
218
  type: "string",
218
- enum: ["ethereum", "base", "bsc", "arbitrum", "optimism", "sepolia"],
219
+ enum: SUPPORTED_CHAIN_NAMES,
219
220
  description: "The destination blockchain network",
220
221
  },
221
222
  fromToken: {
@@ -1,5 +1,5 @@
1
1
  import { isAddress } from 'viem';
2
- import { getPublicClient, ChainName } from '../config';
2
+ import { getPublicClient, ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
3
3
 
4
4
  export async function checkAddress(chainName: ChainName, address: string): Promise<string> {
5
5
  try {
@@ -40,7 +40,7 @@ export const checkAddressToolDefinition = {
40
40
  properties: {
41
41
  chainName: {
42
42
  type: "string",
43
- enum: ["ethereum", "base", "bsc", "arbitrum", "optimism", "sepolia"],
43
+ enum: SUPPORTED_CHAIN_NAMES,
44
44
  description: "The name of the blockchain to check the address on."
45
45
  },
46
46
  address: {
@@ -1,5 +1,5 @@
1
1
  import { formatEther, formatUnits } from 'viem';
2
- import { getPublicClient, ChainName } from '../config';
2
+ import { getPublicClient, ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
3
3
  import { TOKEN_MAP, ERC20_ABI } from '../utils/tokens';
4
4
 
5
5
  const portfolioCache: Record<string, { data: string, timestamp: number }> = {};
@@ -60,51 +60,67 @@ export async function checkPortfolio(chainName: ChainName, address?: `0x${string
60
60
  let report = `📊 **Portfolio for ${targetAddress} on ${chainName.toUpperCase()}**\n\n`;
61
61
  let totalUsdValue = 0;
62
62
 
63
- // We will do Promise.all for balances
64
- const balancePromises = tokensToScan.map(async (t) => {
65
- let balanceNum = 0;
63
+ // We will do True On-Chain Multicall with Chunking (max 30 tokens / 60 calls per batch)
64
+ const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
65
+ const MULTICALL3_ABI = [{
66
+ inputs: [{ name: 'addr', type: 'address' }],
67
+ name: 'getEthBalance',
68
+ outputs: [{ name: 'balance', type: 'uint256' }],
69
+ stateMutability: 'view',
70
+ type: 'function',
71
+ }] as const;
72
+
73
+ const contracts: any[] = [];
74
+ for (const t of tokensToScan) {
66
75
  if (t.isNative) {
67
- const bal = await client.getBalance({ address: targetAddress as `0x${string}` });
68
- balanceNum = parseFloat(formatEther(bal));
76
+ contracts.push({ address: MULTICALL3_ADDRESS, abi: MULTICALL3_ABI, functionName: 'getEthBalance', args: [targetAddress as `0x${string}`] });
69
77
  } else {
70
- try {
71
- const [balanceWei, decimals] = await Promise.all([
72
- // @ts-ignore
73
- client.readContract({
74
- address: t.address,
75
- abi: ERC20_ABI,
76
- functionName: 'balanceOf',
77
- args: [targetAddress as `0x${string}`],
78
- }) as Promise<bigint>,
79
- // @ts-ignore
80
- client.readContract({
81
- address: t.address,
82
- abi: ERC20_ABI,
83
- functionName: 'decimals',
84
- }) as Promise<number>
85
- ]);
86
- balanceNum = parseFloat(formatUnits(balanceWei, decimals));
87
- } catch (e) {
88
- balanceNum = 0;
89
- }
78
+ contracts.push({ address: t.address, abi: ERC20_ABI, functionName: 'balanceOf', args: [targetAddress as `0x${string}`] });
79
+ contracts.push({ address: t.address, abi: ERC20_ABI, functionName: 'decimals' });
90
80
  }
91
- return { ...t, balanceNum };
92
- });
93
-
94
- const timeoutPromise = new Promise<any[]>((_, reject) =>
95
- setTimeout(() => reject(new Error('RPC request timed out')), 3000)
96
- );
81
+ }
97
82
 
98
- let balances: any[];
83
+ const CHUNK_SIZE = 60; // 30 tokens (2 calls per token)
84
+ const multicallResults: any[] = [];
85
+
99
86
  try {
100
- balances = await Promise.race([
101
- Promise.all(balancePromises),
102
- timeoutPromise
103
- ]);
87
+ // Create a timeout promise for 5 seconds (more tolerant for large portfolios)
88
+ const timeoutPromise = new Promise<never>((_, reject) =>
89
+ setTimeout(() => reject(new Error('RPC request timed out')), 5000)
90
+ );
91
+
92
+ const executionPromise = (async () => {
93
+ for (let i = 0; i < contracts.length; i += CHUNK_SIZE) {
94
+ const chunk = contracts.slice(i, i + CHUNK_SIZE);
95
+ const res = await client.multicall({ contracts: chunk, allowFailure: true } as any);
96
+ multicallResults.push(...res);
97
+ }
98
+ })();
99
+
100
+ await Promise.race([executionPromise, timeoutPromise]);
104
101
  } catch (e: any) {
105
- return `⚠️ **${chainName.toUpperCase()} Network is experiencing high latency.**\nThe public RPC failed to respond within 3 seconds. Please try again later.`;
102
+ return `⚠️ **${chainName.toUpperCase()} Network is experiencing high latency.**\nThe public RPC failed to respond. Please try again later.`;
106
103
  }
107
104
 
105
+ // Map results back to tokens
106
+ let resultIndex = 0;
107
+ const balances = tokensToScan.map((t) => {
108
+ let balanceNum = 0;
109
+ if (t.isNative) {
110
+ const balResult = multicallResults[resultIndex++];
111
+ if (balResult?.status === 'success' && balResult.result) {
112
+ balanceNum = parseFloat(formatEther(balResult.result as bigint));
113
+ }
114
+ } else {
115
+ const balResult = multicallResults[resultIndex++];
116
+ const decResult = multicallResults[resultIndex++];
117
+ if (balResult?.status === 'success' && balResult.result !== undefined && decResult?.status === 'success' && decResult.result !== undefined) {
118
+ balanceNum = parseFloat(formatUnits(balResult.result as bigint, Number(decResult.result)));
119
+ }
120
+ }
121
+ return { ...t, balanceNum };
122
+ });
123
+
108
124
  const nonZeroBalances = balances.filter(b => b.balanceNum > 0);
109
125
 
110
126
  if (nonZeroBalances.length === 0) {
@@ -165,7 +181,7 @@ export const checkPortfolioToolDefinition = {
165
181
  properties: {
166
182
  chainName: {
167
183
  type: "string",
168
- enum: ["ethereum", "base", "bsc", "arbitrum", "optimism", "sepolia"],
184
+ enum: SUPPORTED_CHAIN_NAMES,
169
185
  description: "The blockchain network",
170
186
  },
171
187
  address: {
@@ -1,4 +1,4 @@
1
- import { ChainName } from '../config';
1
+ import { ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
2
2
 
3
3
  const CHAIN_IDS: Record<ChainName, number> = {
4
4
  ethereum: 1,
@@ -7,6 +7,7 @@ const CHAIN_IDS: Record<ChainName, number> = {
7
7
  arbitrum: 42161,
8
8
  optimism: 10,
9
9
  sepolia: 11155111,
10
+ polygon: 137,
10
11
  };
11
12
 
12
13
  export async function checkTokenSecurity(chainName: ChainName, contractAddress: string): Promise<string> {
@@ -57,7 +58,7 @@ export const checkSecurityToolDefinition = {
57
58
  properties: {
58
59
  chainName: {
59
60
  type: "string",
60
- enum: ["ethereum", "base", "bsc", "arbitrum", "optimism", "sepolia"],
61
+ enum: SUPPORTED_CHAIN_NAMES,
61
62
  description: "The blockchain network",
62
63
  },
63
64
  contractAddress: {
@@ -1,5 +1,5 @@
1
1
  import { parseEther } from 'viem';
2
- import { getPublicClient, getAddress, ChainName } from '../config';
2
+ import { getPublicClient, getAddress, ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
3
3
  import { txManager } from '../../agent/transactionManager';
4
4
 
5
5
  export async function prepareCustomTx(
@@ -91,7 +91,7 @@ export const customTxToolDefinition = {
91
91
  properties: {
92
92
  chainName: {
93
93
  type: "string",
94
- enum: ["ethereum", "base", "bsc", "arbitrum", "optimism", "sepolia"],
94
+ enum: SUPPORTED_CHAIN_NAMES,
95
95
  description: "The blockchain network",
96
96
  },
97
97
  toAddress: {
@@ -1,5 +1,5 @@
1
1
  import { formatEther, formatUnits } from 'viem';
2
- import { getPublicClient, ChainName } from '../config';
2
+ import { getPublicClient, ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
3
3
  import { ERC20_ABI, resolveToken } from '../utils/tokens';
4
4
  import { saveTokenToWhitelist } from '../../utils/userWhitelistManager';
5
5
 
@@ -72,8 +72,8 @@ export const getBalanceToolDefinition = {
72
72
  properties: {
73
73
  chainName: {
74
74
  type: "string",
75
- enum: ["ethereum", "base", "bsc", "arbitrum", "optimism", "sepolia"],
76
- description: "The name of the blockchain to check."
75
+ enum: SUPPORTED_CHAIN_NAMES,
76
+ description: "The blockchain network"
77
77
  },
78
78
  address: {
79
79
  type: "string",
@@ -1,4 +1,4 @@
1
- import { ChainName } from '../config';
1
+ import { ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
2
2
  import { resolveToken } from '../utils/tokens';
3
3
 
4
4
  export async function analyzeMarket(chainName: ChainName, tokenAddressOrSymbol: string): Promise<string> {
@@ -85,7 +85,7 @@ export const marketAnalysisToolDefinition = {
85
85
  properties: {
86
86
  chainName: {
87
87
  type: "string",
88
- enum: ["ethereum", "base", "bsc", "arbitrum", "optimism", "sepolia"],
88
+ enum: SUPPORTED_CHAIN_NAMES,
89
89
  description: "The blockchain network",
90
90
  },
91
91
  tokenAddressOrSymbol: {
@@ -1,5 +1,5 @@
1
1
  import { parseAbi, parseEther } from 'viem';
2
- import { getPublicClient, getAddress, ChainName } from '../config';
2
+ import { getPublicClient, getAddress, ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
3
3
  import { txManager } from '../../agent/transactionManager';
4
4
 
5
5
  export async function prepareMintNft(
@@ -120,7 +120,7 @@ export const mintNftToolDefinition = {
120
120
  properties: {
121
121
  chainName: {
122
122
  type: "string",
123
- enum: ["ethereum", "base", "bsc", "arbitrum", "optimism", "sepolia"],
123
+ enum: SUPPORTED_CHAIN_NAMES,
124
124
  description: "The blockchain network",
125
125
  },
126
126
  contractAddress: {
@@ -1,17 +1,18 @@
1
1
  import { parseUnits, formatUnits } from 'viem';
2
- import { getPublicClient, getAddress, ChainName } from '../config';
2
+ import { getPublicClient, getAddress, ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
3
3
  import { txManager } from '../../agent/transactionManager';
4
4
  import { resolveToken, ERC20_ABI } from '../utils/tokens';
5
5
  import { saveTokenToWhitelist } from '../../utils/userWhitelistManager';
6
6
  import crypto from 'crypto';
7
7
 
8
- const CHAIN_IDS: Record<ChainName, number> = {
8
+ const CHAIN_IDS: Record<string, number> = {
9
9
  ethereum: 1,
10
10
  base: 8453,
11
11
  bsc: 56,
12
12
  arbitrum: 42161,
13
13
  optimism: 10,
14
14
  sepolia: 11155111,
15
+ polygon: 137,
15
16
  };
16
17
 
17
18
  async function getLifiQuote(fromChainId: number, toChainId: number, fromToken: string, toToken: string, amountWei: string, userAddress: string, slippage: number) {
@@ -231,7 +232,7 @@ export const swapTokenToolDefinition = {
231
232
  properties: {
232
233
  chainName: {
233
234
  type: "string",
234
- enum: ["ethereum", "base", "bsc", "arbitrum", "optimism", "sepolia"],
235
+ enum: SUPPORTED_CHAIN_NAMES,
235
236
  description: "The blockchain network",
236
237
  },
237
238
  fromToken: {
@@ -1,5 +1,5 @@
1
1
  import { parseEther, parseUnits } from 'viem';
2
- import { getPublicClient, getAddress, ChainName } from '../config';
2
+ import { getPublicClient, getAddress, ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
3
3
  import { txManager } from '../../agent/transactionManager';
4
4
  import { resolveToken, ERC20_ABI } from '../utils/tokens';
5
5
 
@@ -120,7 +120,7 @@ export const transferToolDefinition = {
120
120
  properties: {
121
121
  chainName: {
122
122
  type: "string",
123
- enum: ["ethereum", "base", "bsc", "arbitrum", "optimism", "sepolia"],
123
+ enum: SUPPORTED_CHAIN_NAMES,
124
124
  description: "The name of the blockchain to execute the transfer on."
125
125
  },
126
126
  toAddress: {
@@ -91,6 +91,14 @@ export const TOKEN_MAP: Record<ChainName, Record<string, `0x${string}`>> = {
91
91
  WETH: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14",
92
92
  USDC: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", // Circle Faucet Sepolia USDC
93
93
  USDT: "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0" // Common Sepolia USDT
94
+ },
95
+ polygon: {
96
+ MATIC: "0x0000000000000000000000000000000000000000",
97
+ POL: "0x0000000000000000000000000000000000000000",
98
+ WMATIC: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
99
+ WPOL: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
100
+ USDC: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
101
+ USDT: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
94
102
  }
95
103
  };
96
104