nyxora 1.6.12 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/package.json +1 -2
- package/packages/core/package.json +1 -1
- package/packages/core/src/agent/reasoning.ts +26 -6
- package/packages/core/src/gateway/setup.ts +1 -1
- package/packages/core/src/gateway/telegram.ts +50 -11
- package/packages/core/src/utils/formatter.ts +12 -5
- package/packages/core/src/web3/skills/checkPortfolio.ts +53 -37
- package/packages/dashboard/dist/assets/{index-BuYfTEKE.js → index-DQtaOlOl.js} +8 -8
- package/packages/dashboard/dist/index.html +1 -1
- package/packages/dashboard/package.json +1 -1
- package/packages/dashboard/src/App.tsx +3 -6
- 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/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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.7.0]
|
|
9
|
+
|
|
10
|
+
### Bug Fixes & Optimizations
|
|
11
|
+
- **Time Sync Hallucination**: Fixed a critical issue where the AI hallucinates the current date and time. Nyxora now dynamically injects the host OS's exact `new Date().toLocaleString()` into the system prompt upon every execution.
|
|
12
|
+
- **Aggressive UI Auto-Scroll**: Resolved a severe React rendering bug in the dashboard where the 2-second history polling forced the chat window to aggressively scroll to the bottom. Auto-scroll is now strictly isolated to new message arrivals (`messages.length`).
|
|
13
|
+
- **Orphaned OS Skills**: Re-wired the `search_web` (Internet Search) and `analyze_document` (PDF/DOCX Extractor) skills back into the core reasoning engine. These skills were previously orphaned and inaccessible to the AI despite being active in the dashboard.
|
|
14
|
+
- **Multicall3 Portfolio Engine**: Fully replaced parallel `client.getBalance` and ERC20 fetching with a hyper-efficient `Multicall3` architecture. Balances are now chunked (max 30 tokens per batch) to guarantee zero rate-limits and payload errors on public RPCs.
|
|
15
|
+
- **ChatGPT-Level NLP Persona**: Upgraded the AI's core reasoning engine to natively understand unstructured text, slang, and informal contexts. Rigidly enforced Markdown Table generation for all financial data.
|
|
16
|
+
- **Telegram HTML Parser**: Implemented a custom `formatToTelegramHTML` function. Nyxora now escapes dangerous characters (`<`, `>`, `&`) and automatically wraps AI-generated Markdown tables into `<pre>` monospaced blocks, completely eliminating the "Bad Request" rendering crash on Telegram.
|
|
17
|
+
- **Dynamic Tx Formatter (Tap-to-Copy)**: The post-transaction approval message is now bilingual (auto-detecting English/Indonesian from chat history). Transaction Hashes and wallet addresses are wrapped in `<code>` tags for seamless tap-to-copy UX on mobile devices.
|
|
18
|
+
- **CLI Setup Typography**: Updated outdated CLI prompts that falsely referenced legacy `AES-256-GCM` encryption. The CLI now correctly informs the user that Private Keys are securely locked inside the OS Native Keyring Vault.
|
|
19
|
+
|
|
8
20
|
## [1.6.7]
|
|
9
21
|
|
|
10
22
|
### UI/UX
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](#️-advanced-security-threat-model)
|
|
9
9
|
[](#️-advanced-security-threat-model)
|
|
10
10
|
|
|
11
|
-
Nyxora is a **secure, non-custodial runtime infrastructure for autonomous onchain agents** built with a robust Monorepo architecture (Node.js & React). Designed for autonomous workflows with a premium
|
|
11
|
+
Nyxora is a **secure, non-custodial runtime infrastructure for autonomous onchain agents** built with a robust Monorepo architecture (Node.js & React). Designed for autonomous workflows with a premium Utility-Centric dark-themed UI and strict client-side key isolation.
|
|
12
12
|
|
|
13
13
|
**Nyxora now natively supports the Model Context Protocol (MCP)**. You can transform your external AI agents (like Claude Desktop and Cursor) into secure Web3 actors that execute swaps and fetch balances using Nyxora's secure signer vault. [View the MCP Integration Guide](https://nyxoraAI.github.io/Nyxora/guide/mcp-integration)
|
|
14
14
|
|
|
@@ -25,7 +25,7 @@ It operates under an institutional-grade **Cryptographically Bound Human-in-the-
|
|
|
25
25
|
* **Plugin Sandbox VM**: Execute community-built external skills securely inside an airtight Node.js `vm` chamber with zero access to your file system or terminal processes.
|
|
26
26
|
|
|
27
27
|
### 🌐 Web3 Skills (On-Chain)
|
|
28
|
-
* **
|
|
28
|
+
* **Security Scanner**: Nyxora can scan smart contracts via GoPlus Labs to detect Honeypots, Hidden Taxes, and malicious proxy upgrades before you buy.
|
|
29
29
|
* **Automated Take Profit (TP) & Cut Loss (CL)**: The trader's holy grail. Set natural language rules (e.g., "Sell my PEPE if price drops below $0.001"). Nyxora runs a background cron monitor and executes the swap while you sleep.
|
|
30
30
|
* **Cross-Chain Hybrid Market Scanner**: Real-time asset tracking combining CoinGecko global data with DexScreener on-chain metrics across Ethereum, Base, Solana, BSC, and more.
|
|
31
31
|
* **"Lean Degen" Auto-Whitelist**: Automatically intercepts Contract Addresses (CAs) whenever you check balances or swap tokens, saving them to your localized `user_whitelist.json` for future tracking.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nyxora",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"packages/*"
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
"duck-duck-scrape": "^2.2.7",
|
|
30
30
|
"express": "^5.2.1",
|
|
31
31
|
"express-rate-limit": "^7.5.0",
|
|
32
|
-
"googleapis": "^173.0.0",
|
|
33
32
|
"helmet": "^8.0.0",
|
|
34
33
|
"isolated-vm": "^6.1.2",
|
|
35
34
|
"jsonwebtoken": "^9.0.2",
|
|
@@ -21,10 +21,12 @@ import { getMyAddressToolDefinition, getMyAddress } from '../web3/skills/getMyAd
|
|
|
21
21
|
import { createLimitOrderToolDefinition, listLimitOrdersToolDefinition, cancelLimitOrderToolDefinition, limitOrderManager } from './limitOrderManager';
|
|
22
22
|
import { updateProfileToolDefinition, updateProfile } from './updateProfile';
|
|
23
23
|
import { updateSecurityPolicyToolDefinition, updateSecurityPolicy } from '../system/skills/updateSecurityPolicy';
|
|
24
|
+
import { analyzeDocumentToolDefinition, analyzeDocument } from '../system/skills/analyzeDocument';
|
|
24
25
|
import { readLocalFileToolDefinition, readLocalFile } from '../system/skills/readFile';
|
|
25
26
|
import { writeLocalFileToolDefinition, writeLocalFile } from '../system/skills/writeFile';
|
|
26
27
|
import { runTerminalCommandToolDefinition, runTerminalCommand } from '../system/skills/executeShell';
|
|
27
28
|
import { browseWebsiteToolDefinition, browseWebsite } from '../system/skills/browseWeb';
|
|
29
|
+
import { searchWebToolDefinition, searchWeb } from '../system/skills/searchWeb';
|
|
28
30
|
import { installExternalSkillToolDefinition, installExternalSkill } from '../system/skills/installSkill';
|
|
29
31
|
import {
|
|
30
32
|
readGmailInbox,
|
|
@@ -151,12 +153,20 @@ async function executeWithRetry(
|
|
|
151
153
|
|
|
152
154
|
function getSystemPrompt() {
|
|
153
155
|
const config = loadConfig();
|
|
156
|
+
const currentDateTime = new Date().toLocaleString('en-US', { timeZoneName: 'short' });
|
|
154
157
|
let basePrompt = `You are an autonomous Web3 agent operating on EVM chains.
|
|
155
|
-
You are equipped with a native wallet.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
CRITICAL RULE:
|
|
159
|
-
CRITICAL RULE:
|
|
158
|
+
You are equipped with a native wallet.
|
|
159
|
+
The current real-world date and time is: ${currentDateTime}. Use this for any time-related questions.
|
|
160
|
+
|
|
161
|
+
CRITICAL RULE 1: ADVANCED NLP & PERSONA. You must act as a highly intelligent, adaptive, and intuitive assistant (similar to ChatGPT or Gemini). You must seamlessly understand the user's language structure, including slang, shorthand, informal context, and mixed languages. However, you must maintain a professional and highly accurate Web3 operational standard.
|
|
162
|
+
CRITICAL RULE 2: LANGUAGE MATCHING. You must always reply in the exact same language that the user uses to talk to you. If the user speaks Indonesian, reply in Indonesian. If they speak English, reply in English.
|
|
163
|
+
CRITICAL RULE 3: FORMATTING & CONCISENESS.
|
|
164
|
+
- Your responses MUST be concise and to the point. Do not add unnecessary fluff or overly long explanations unless explicitly asked.
|
|
165
|
+
- When displaying numbers or monetary values, separate thousands with commas (e.g., $1,000,000) for readability.
|
|
166
|
+
- When displaying a list of assets, tokens, portfolio, or transaction history, YOU MUST USE MARKDOWN TABLES. Do not use bullet points for financial data.
|
|
167
|
+
CRITICAL RULE 4: When the user asks to check "my balance", "saldo saya", or anything about their own wallet generally, ALWAYS use the check_portfolio tool to show all assets on the chain that have a USD value greater than 0. LEAVE THE ADDRESS PARAMETER EMPTY. Do NOT use get_balance unless the user explicitly asks for the balance of ONE specific token.
|
|
168
|
+
CRITICAL RULE 5: If the user doesn't specify a chain, default to: ${config.agent.default_chain}. If the user mentions a specific chain (e.g., "on BNB", "di Base"), you MUST override the default and execute the tool on that specific chain.
|
|
169
|
+
CRITICAL RULE 6: If you use the default chain because the user forgot to specify one, you MUST politely confirm which chain you checked in your response (e.g., "I checked your balance on the ${config.agent.default_chain} network..."). Do not issue scary warnings.`;
|
|
160
170
|
|
|
161
171
|
// Read IDENTITY.md for core AI persona
|
|
162
172
|
try {
|
|
@@ -246,10 +256,12 @@ export async function processUserInput(input: string, role: 'user' | 'system' =
|
|
|
246
256
|
cancelLimitOrderToolDefinition as any,
|
|
247
257
|
updateProfileToolDefinition as any,
|
|
248
258
|
updateSecurityPolicyToolDefinition as any,
|
|
259
|
+
analyzeDocumentToolDefinition as any,
|
|
249
260
|
readLocalFileToolDefinition as any,
|
|
250
261
|
writeLocalFileToolDefinition as any,
|
|
251
262
|
runTerminalCommandToolDefinition as any,
|
|
252
263
|
browseWebsiteToolDefinition as any,
|
|
264
|
+
searchWebToolDefinition as any,
|
|
253
265
|
installExternalSkillToolDefinition as any,
|
|
254
266
|
readGmailInboxToolDefinition as any,
|
|
255
267
|
listCalendarEventsToolDefinition as any,
|
|
@@ -382,7 +394,11 @@ export async function processUserInput(input: string, role: 'user' | 'system' =
|
|
|
382
394
|
break;
|
|
383
395
|
}
|
|
384
396
|
case 'update_security_policy': {
|
|
385
|
-
result = updateSecurityPolicy(args.
|
|
397
|
+
result = await updateSecurityPolicy(args.policy, args.action || 'add');
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
case 'analyze_document': {
|
|
401
|
+
result = await analyzeDocument(args.filePath);
|
|
386
402
|
break;
|
|
387
403
|
}
|
|
388
404
|
case 'read_local_file': {
|
|
@@ -409,6 +425,10 @@ export async function processUserInput(input: string, role: 'user' | 'system' =
|
|
|
409
425
|
result = await browseWebsite(args.url);
|
|
410
426
|
break;
|
|
411
427
|
}
|
|
428
|
+
case 'search_web': {
|
|
429
|
+
result = await searchWeb(args.query);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
412
432
|
case 'install_external_skill': {
|
|
413
433
|
result = await installExternalSkill(args.url);
|
|
414
434
|
break;
|
|
@@ -242,7 +242,7 @@ Provider: ${config.llm.provider}`;
|
|
|
242
242
|
let privateKey = '';
|
|
243
243
|
if (walletSetupType === 'manual') {
|
|
244
244
|
privateKey = (await password({
|
|
245
|
-
message: 'Enter Wallet Private Key (0x...)\n (Will be
|
|
245
|
+
message: 'Enter Wallet Private Key (0x...)\n (Will be securely locked in your OS Native Keyring Vault):',
|
|
246
246
|
})) as string;
|
|
247
247
|
if (isCancel(privateKey)) return process.exit(0);
|
|
248
248
|
} else if (walletSetupType === 'generate') {
|
|
@@ -10,6 +10,31 @@ import { executeCustomTx } from '../web3/skills/customTx';
|
|
|
10
10
|
import { formatTransactionSuccess, formatTransactionError } from '../utils/formatter';
|
|
11
11
|
import pc from 'picocolors';
|
|
12
12
|
|
|
13
|
+
export function formatToTelegramHTML(text: string): string {
|
|
14
|
+
if (!text) return "";
|
|
15
|
+
let html = text
|
|
16
|
+
.replace(/&/g, '&')
|
|
17
|
+
.replace(/</g, '<')
|
|
18
|
+
.replace(/>/g, '>');
|
|
19
|
+
|
|
20
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
|
21
|
+
// Match italic * avoiding bullet points at the start of a line
|
|
22
|
+
html = html.replace(/(?<!^|\n)\*(?!\s)(.*?)(?<!\s)\*/g, '<i>$1</i>');
|
|
23
|
+
html = html.replace(/_(.*?)_/g, '<i>$1</i>');
|
|
24
|
+
|
|
25
|
+
// Convert code blocks and inline code
|
|
26
|
+
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
|
27
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
28
|
+
|
|
29
|
+
// Transform Markdown Tables to <pre> monospaced blocks so they don't break on mobile
|
|
30
|
+
const tableRegex = /(?:\|.*\|(?:\n|$))+/g;
|
|
31
|
+
html = html.replace(tableRegex, (match) => {
|
|
32
|
+
return `<pre>${match.trim()}</pre>\n`;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return html;
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
export function startTelegramBot() {
|
|
14
39
|
const config = loadConfig();
|
|
15
40
|
const token = config.integrations?.telegram?.bot_token;
|
|
@@ -104,10 +129,10 @@ export function startTelegramBot() {
|
|
|
104
129
|
const onProgress = async (progressText: string) => {
|
|
105
130
|
try {
|
|
106
131
|
if (!progressMsgId) {
|
|
107
|
-
const sent = await ctx.reply(progressText, { parse_mode: '
|
|
132
|
+
const sent = await ctx.reply(`<i>${progressText.replace(/_/g, '')}</i>`, { parse_mode: 'HTML' });
|
|
108
133
|
progressMsgId = sent.message_id;
|
|
109
134
|
} else {
|
|
110
|
-
await ctx.telegram.editMessageText(ctx.chat.id, progressMsgId, undefined, progressText, { parse_mode: '
|
|
135
|
+
await ctx.telegram.editMessageText(ctx.chat.id, progressMsgId, undefined, `<i>${progressText.replace(/_/g, '')}</i>`, { parse_mode: 'HTML' });
|
|
111
136
|
}
|
|
112
137
|
} catch (e) {}
|
|
113
138
|
};
|
|
@@ -122,17 +147,20 @@ export function startTelegramBot() {
|
|
|
122
147
|
if (pendingTxs.length > 0) {
|
|
123
148
|
const latestTx = pendingTxs[pendingTxs.length - 1];
|
|
124
149
|
if (Date.now() - latestTx.createdAt < 120000) {
|
|
125
|
-
await ctx.reply(response,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
150
|
+
await ctx.reply(formatToTelegramHTML(response), {
|
|
151
|
+
parse_mode: 'HTML',
|
|
152
|
+
...Markup.inlineKeyboard([
|
|
153
|
+
[
|
|
154
|
+
Markup.button.callback('✅ Approve', `approve_${latestTx.id}`),
|
|
155
|
+
Markup.button.callback('❌ Reject', `reject_${latestTx.id}`)
|
|
156
|
+
]
|
|
157
|
+
])
|
|
158
|
+
});
|
|
131
159
|
return;
|
|
132
160
|
}
|
|
133
161
|
}
|
|
134
162
|
|
|
135
|
-
await ctx.reply(response);
|
|
163
|
+
await ctx.reply(formatToTelegramHTML(response), { parse_mode: 'HTML' });
|
|
136
164
|
} catch (error: any) {
|
|
137
165
|
console.error('[Telegram] Error processing message:', error);
|
|
138
166
|
await ctx.reply('❌ Sorry, I encountered an error while processing your message.');
|
|
@@ -170,8 +198,19 @@ export function startTelegramBot() {
|
|
|
170
198
|
}
|
|
171
199
|
|
|
172
200
|
txManager.updateStatus(txId, 'executed', result);
|
|
173
|
-
|
|
174
|
-
|
|
201
|
+
|
|
202
|
+
// Pass session history to formatTransactionSuccess to detect language
|
|
203
|
+
const sessionId = ctx.chat?.id.toString() || 'default';
|
|
204
|
+
const history = logger.getHistory(sessionId);
|
|
205
|
+
let isIndonesian = false;
|
|
206
|
+
if (history.length > 0) {
|
|
207
|
+
const lastMsg = history[history.length - 1].content.toLowerCase();
|
|
208
|
+
const idWords = ['saya', 'kamu', 'aku', 'apa', 'bagaimana', 'kenapa', 'bisa', 'tolong', 'ke', 'di', 'dari', 'yang', 'ini', 'itu', 'buat', 'cek', 'saldo'];
|
|
209
|
+
if (idWords.some(w => lastMsg.includes(w))) isIndonesian = true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const prettyMsg = formatTransactionSuccess(tx, result, isIndonesian);
|
|
213
|
+
await ctx.reply(formatToTelegramHTML(`✅ **Transaction processed:**\n\n${prettyMsg}`), { parse_mode: 'HTML' });
|
|
175
214
|
|
|
176
215
|
logger.addEntry({ role: 'assistant', content: `✅ Transaction processed:\n\n${prettyMsg}` });
|
|
177
216
|
logger.addEntry({ role: 'tool', name: tx.type === 'swap' ? 'swap_token' : 'transfer_native', content: result });
|
|
@@ -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 {
|
|
@@ -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) {
|