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.
- package/CHANGELOG.md +28 -0
- package/README.md +2 -2
- package/bin/nyxora.mjs +8 -0
- package/package.json +1 -2
- package/packages/core/package.json +1 -1
- package/packages/core/src/agent/limitOrderManager.ts +2 -2
- package/packages/core/src/agent/reasoning.ts +36 -14
- package/packages/core/src/config/parser.ts +99 -9
- package/packages/core/src/gateway/cli.ts +28 -2
- package/packages/core/src/gateway/setup.ts +45 -10
- package/packages/core/src/gateway/telegram.ts +53 -12
- package/packages/core/src/system/skills/searchWeb.ts +187 -21
- package/packages/core/src/utils/formatter.ts +12 -5
- package/packages/core/src/web3/config.ts +7 -1
- package/packages/core/src/web3/skills/bridgeToken.ts +4 -3
- package/packages/core/src/web3/skills/checkAddress.ts +2 -2
- package/packages/core/src/web3/skills/checkPortfolio.ts +55 -39
- package/packages/core/src/web3/skills/checkSecurity.ts +3 -2
- package/packages/core/src/web3/skills/customTx.ts +2 -2
- package/packages/core/src/web3/skills/getBalance.ts +3 -3
- package/packages/core/src/web3/skills/marketAnalysis.ts +2 -2
- package/packages/core/src/web3/skills/mintNft.ts +2 -2
- package/packages/core/src/web3/skills/swapToken.ts +4 -3
- package/packages/core/src/web3/skills/transfer.ts +2 -2
- package/packages/core/src/web3/utils/tokens.ts +8 -0
- package/packages/dashboard/dist/assets/{index-24OeXn-k.css → index-BSk4CLkG.css} +1 -1
- package/packages/dashboard/dist/assets/{index-BuYfTEKE.js → index-Dc3Tu0Te.js} +21 -21
- package/packages/dashboard/dist/index.html +2 -2
- 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
- package/launcher.js +0 -48
- package/packages/core/src/agent/reasoning.d.ts.map +0 -1
- package/packages/core/src/config/parser.d.ts.map +0 -1
- package/packages/core/src/gateway/cli.d.ts.map +0 -1
- package/packages/core/src/memory/logger.d.ts.map +0 -1
- package/packages/core/src/utils/safeLogger.js +0 -59
- package/packages/core/src/web3/config.d.ts.map +0 -1
- package/packages/core/src/web3/skills/getBalance.d.ts.map +0 -1
- package/packages/dashboard/public/favicon.svg +0 -10
- package/packages/dashboard/public/icons.svg +0 -24
- package/packages/dashboard/src/App.css +0 -184
- package/packages/dashboard/src/App.tsx +0 -588
- package/packages/dashboard/src/BalanceWidget.tsx +0 -65
- package/packages/dashboard/src/MarketWidget.tsx +0 -73
- package/packages/dashboard/src/NetworkSelector.tsx +0 -64
- package/packages/dashboard/src/NyxoraLogo.tsx +0 -25
- package/packages/dashboard/src/OsSkills.tsx +0 -352
- package/packages/dashboard/src/Overview.tsx +0 -157
- package/packages/dashboard/src/PendingTransactions.tsx +0 -75
- package/packages/dashboard/src/Settings.tsx +0 -338
- package/packages/dashboard/src/Skills.tsx +0 -200
- package/packages/dashboard/src/SwapWidget.tsx +0 -141
- package/packages/dashboard/src/TransactionWidget.tsx +0 -95
- package/packages/dashboard/src/assets/hero.png +0 -0
- package/packages/dashboard/src/assets/react.svg +0 -1
- package/packages/dashboard/src/assets/vite.svg +0 -1
- package/packages/dashboard/src/components/PillSelect.tsx +0 -65
- package/packages/dashboard/src/index.css +0 -807
- package/packages/dashboard/src/main.tsx +0 -10
- package/packages/dashboard/src/overview.css +0 -304
- package/packages/dashboard/src/utils/api.ts +0 -31
- package/packages/mcp-server/tsconfig.tsbuildinfo +0 -1
- package/test-address.ts +0 -11
- package/test-all-chains.ts +0 -19
- package/test-db.ts +0 -3
- package/test-portfolio.ts +0 -14
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,43 +1,209 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadConfig, loadApiKeys } from '../../config/parser';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
214
|
+
enum: SUPPORTED_CHAIN_NAMES,
|
|
214
215
|
description: "The source blockchain network",
|
|
215
216
|
},
|
|
216
217
|
toChainName: {
|
|
217
218
|
type: "string",
|
|
218
|
-
enum:
|
|
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:
|
|
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
|
|
64
|
-
const
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const timeoutPromise = new Promise<any[]>((_, reject) =>
|
|
95
|
-
setTimeout(() => reject(new Error('RPC request timed out')), 3000)
|
|
96
|
-
);
|
|
81
|
+
}
|
|
97
82
|
|
|
98
|
-
|
|
83
|
+
const CHUNK_SIZE = 60; // 30 tokens (2 calls per token)
|
|
84
|
+
const multicallResults: any[] = [];
|
|
85
|
+
|
|
99
86
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
76
|
-
description: "The
|
|
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:
|
|
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:
|
|
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<
|
|
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:
|
|
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:
|
|
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
|
|