nyxora 1.6.13 → 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 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
  [![Execution: Cryptographic Approval](https://img.shields.io/badge/Execution-Cryptographic--Approval-orange.svg)](#️-advanced-security-threat-model)
9
9
  [![Privacy: Local-Only Keys](https://img.shields.io/badge/Privacy-Local--Only--Keys-success.svg)](#️-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 Glassmorphism UI dashboard and strict client-side key isolation.
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
- * **Anti-Rugpull & Security Scanner**: Nyxora can scan smart contracts via GoPlus Labs to detect Honeypots, Hidden Taxes, and malicious proxy upgrades before you buy.
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.6.13",
3
+ "version": "1.7.0",
4
4
  "license": "MIT",
5
5
  "workspaces": [
6
6
  "packages/*"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nyxora-agent-core",
3
- "version": "1.6.7",
3
+ "version": "1.7.0",
4
4
  "private": true,
5
5
  "main": "src/gateway/server.ts",
6
6
  "dependencies": {
@@ -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
- CRITICAL RULE: 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.
157
- CRITICAL RULE: 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 (e.g., "what is my ETH balance?").
158
- CRITICAL RULE: 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.
159
- CRITICAL RULE: 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.`;
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.rule, args.action);
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 AES-256-GCM encrypted. See documentation for import guides):',
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, '&amp;')
17
+ .replace(/</g, '&lt;')
18
+ .replace(/>/g, '&gt;');
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: 'Markdown' });
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: 'Markdown' });
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, Markup.inlineKeyboard([
126
- [
127
- Markup.button.callback('✅ Approve', `approve_${latestTx.id}`),
128
- Markup.button.callback('❌ Reject', `reject_${latestTx.id}`)
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
- const prettyMsg = formatTransactionSuccess(tx, result);
174
- await ctx.reply(`✅ Transaction processed:\n\n${prettyMsg}`);
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
- 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 {
@@ -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) {