Skip to content

feat: include layer2 balances#255

Merged
dadamu merged 2 commits intomainfrom
feat/include-layer2-balances
Dec 30, 2025
Merged

feat: include layer2 balances#255
dadamu merged 2 commits intomainfrom
feat/include-layer2-balances

Conversation

@dadamu
Copy link
Contributor

@dadamu dadamu commented Dec 30, 2025

Pull Request Checklist

Before Submission


Description


Additional Notes


By submitting this pull request, I confirm that:

  • My contribution is made under the Business Source License
  • I grant Forbole Technology Limited a perpetual, worldwide, non-exclusive license to use my contribution under the
    project's
    license terms
  • I have the authority to make this contribution and license it appropriately

Summary by CodeRabbit

  • New Features

    • Integrated real-time token pricing into balance and portfolio displays.
    • Enhanced token logo visibility with improved fallback support.
  • Bug Fixes

    • Fixed missing token prices that previously showed zero values.
    • Corrected balance and portfolio calculations to reflect actual market values.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings December 30, 2025 10:17
@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Note

.coderabbit.yml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'labels', 'include_paths', 'exclude_paths', 'filters', 'review', 'pull_request', 'limits', 'commands', 'messages'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

This PR refactors ERC20 and Kaspa balance retrieval hooks to support batch operations across multiple chains and addresses, introduces token metadata fetching from Zealous Swap API (including prices and logo URLs), and updates six UI components to use dynamic pricing and fallback token images instead of hardcoded values and placeholders.

Changes

Cohort / File(s) Summary
New Token Metadata API Integration
hooks/evm/useZealousSwapMetadata.ts
New hook file providing SWR-based fetching from Zealous Swap API. Exports useZealousSwapTokensMetadata() for raw metadata, useErc20Image(chainId, contractAddress) for logo URLs with fallback support, and useErc20Price(chainId, contractAddress) for token pricing. Includes chain validation (Kasplex mainnet only) and error/loading state propagation.
Hook Refactoring—Balance Retrieval
hooks/evm/useErc20Balance.ts, hooks/evm/useEvmKasBalance.ts
Shifted from per-token/per-chain single-call patterns to batch-based aggregate hooks. Removed decimals parameter from signatures. Introduced useErc20Balances(), useErc20BalancesByAddress(evmAddress) and useEvmKasBalances(), useEvmKasBalancesByAddress(evmAddress) for multi-chain/multi-token retrieval. Updated SWR keys and fetchers to support address-driven batch operations with per-item error handling.
Hook Integration—Portfolio Calculations
hooks/kasplex/useTotalBalanceByAccount.ts, hooks/usePortfolioPerformance.ts
Integrated EVM balance and pricing hooks to compute totalErc20Usd and totalEvmKasUsd by aggregating ERC20 and EVM Kaspa balances with current prices. Added evmAddress derivation from account.evmPublicKey. Updated balance summations to include multi-chain EVM Kaspa amounts alongside on-chain Kaspa; introduces TODO for ERC20 last-day price data.
Component Updates—Asset Display
components/dashboard/Erc20Asset.tsx, components/erc20-asset/Erc20Info.tsx
Integrated useErc20Image and useErc20Price to fetch dynamic logo URLs and token prices. Replaced hardcoded zero-price and static image fallbacks with real data. Updated balance display to use raw balance (no parsing). Token image now prefers asset.image with logoUrl fallback.
Component Updates—Asset Selection & Sending
components/screens/asset-selector/Erc20SelectItem.tsx, components/send/evm/erc20-send/ConfirmStep.tsx, components/send/evm/erc20-send/DetailsStep.tsx
Unified changes: simplified useErc20Balance calls by removing decimals parameter; integrated useErc20Image and useErc20Price for dynamic logo and pricing. Replaced kasIcon static fallback with logoUrl. Updated balance formatting to use raw balance values. Adjusted fiat/currency calculations to use fetched prices instead of hardcoded zeros.

Sequence Diagrams

sequenceDiagram
    participant UI as UI Component
    participant SWR as SWR Hook
    participant MetaAPI as Zealous Swap API
    participant Cache as Local Cache
    
    rect rgb(200, 230, 250)
    Note over UI,Cache: Token Metadata & Pricing Flow
    UI->>SWR: useErc20Price(chainId, contractAddress)
    SWR->>Cache: Check cached metadata
    alt Cache hit & fresh
        Cache-->>SWR: Return cached data
    else Cache miss or stale
        SWR->>MetaAPI: Fetch tokens metadata
        MetaAPI-->>SWR: TokenMetadata[]
        SWR->>Cache: Store (60s refresh interval)
    end
    SWR->>SWR: Find token by address, extract price
    SWR-->>UI: { price, error, isLoading }
    end
Loading
sequenceDiagram
    participant UI as UI Component
    participant Hook as Batch Balance Hook
    participant SWR as SWR Multi-Chain
    participant Chain1 as Chain (Mainnet)
    participant Chain2 as Chain (Testnet)
    
    rect rgb(220, 250, 220)
    Note over UI,Chain2: Multi-Chain Balance Aggregation
    UI->>Hook: useEvmKasBalances() or useErc20Balances()
    Hook->>Hook: Resolve current evmAddress
    Hook->>SWR: Fetch with key: balances:<networkId>-<address>
    SWR->>Chain1: Query balance for address
    Chain1-->>SWR: rawBalance, ether-formatted balance
    SWR->>Chain2: Query balance for address
    Chain2-->>SWR: rawBalance, ether-formatted balance
    SWR->>SWR: Aggregate results into map {chainId → balance}
    SWR-->>Hook: { data: {[chainId]: balance}, error, isLoading }
    Hook-->>UI: Requested balance or aggregated map
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

The changes span 10 files with heterogeneous modifications: new SWR-based metadata API integration requiring validation of chain-gating and error handling; substantial refactoring of balance hooks introducing batch operations and multi-chain aggregation with updated call signatures affecting multiple consumers; six UI components with similar but distinct integration patterns (image fallbacks, price calculations, balance formatting). The logic density is moderate-to-high around hook refactoring and SWR key management, and consistent verification of signature changes across all call sites is required.

Poem

🐰 Hops through chains with batches bright,
Metadata flows from Zealous Swap's light,
No more zeros hardcoded in sight,
Images and prices, aggregated right,
Multi-chain balances—a hoppy delight! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: include layer2 balances' directly and clearly summarizes the main objective of the changeset, which integrates Layer 2 ERC-20 balances, prices, and images across multiple components and hooks.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Layer 2 (EVM-based) balance integration to the portfolio performance calculations. It refactors EVM balance hooks to fetch balances from multiple chains concurrently, integrates with the ZealousSwap API for ERC20 token pricing and images, and updates UI components to display Layer 2 assets with proper pricing information.

Key Changes:

  • Added Layer 2 (EVM KAS and ERC20) balances to portfolio performance and total balance calculations
  • Integrated ZealousSwap API for ERC20 token metadata including prices and logos
  • Refactored balance hooks to support batch fetching across multiple chains and tokens

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
hooks/usePortfolioPerformance.ts Integrates EVM KAS balances from Layer 2 chains into portfolio performance calculations
hooks/kasplex/useTotalBalanceByAccount.ts Adds ERC20 and EVM KAS USD values to total account balance
hooks/evm/useZealousSwapMetadata.ts New hook providing ERC20 token metadata (prices, images) from ZealousSwap API
hooks/evm/useEvmKasBalance.ts Refactored to fetch KAS balances from all supported chains concurrently
hooks/evm/useErc20Balance.ts Refactored to batch-fetch all ERC20 token balances in a single hook call
components/send/evm/erc20-send/DetailsStep.tsx Updated to use new balance hook signature and display token prices/images
components/send/evm/erc20-send/ConfirmStep.tsx Integrated ERC20 pricing for transaction confirmation display
components/screens/asset-selector/Erc20SelectItem.tsx Updated to use refactored balance hook and display token logos
components/erc20-asset/Erc20Info.tsx Added ERC20 price and image display using ZealousSwap metadata
components/dashboard/Erc20Asset.tsx Integrated ERC20 pricing and images for dashboard asset display

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

asset.chainId,
);
const { data } = useErc20Balance(asset.address, asset.chainId);
const balance = data?.balance ?? "0";
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The balance type has changed from string to number (based on the useErc20Balance refactoring), but the fallback value is still a string "0". This type inconsistency could cause issues. The balance should be typed as number and the fallback should be numeric 0.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +78
} catch (error) {
console.error(`Error fetching balance for chain ${chainId}:`, error);
balances[chainId] = {
rawBalance: 0n,
balance: "0",
};
}
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling returns an object with rawBalance set to 0n (bigint) and balance as "0" (string) when an error occurs. However, there's no indication to the caller that an error occurred - the error is only logged to console. Consider returning an error state or throwing to let the caller handle the error appropriately, or at minimum include an error flag in the returned object.

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +110
return useSWR(key, evmAddress ? fetcher : null, {
refreshInterval: 5_000,
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refreshInterval is set to 5 seconds (5_000ms), which means the hook will make network requests to fetch all ERC20 balances every 5 seconds. If there are many tokens in the asset list, this could result in a large number of concurrent RPC calls every 5 seconds, potentially causing performance issues or hitting rate limits. Consider increasing the refresh interval or implementing a more efficient balance update strategy.

Copilot uses AI. Check for mistakes.
@@ -1,51 +1,120 @@
import useSWR from "swr";
import { createPublicClient, http, erc20Abi } from "viem";
import { createPublicClient, http, erc20Abi, Hex, Address } from "viem";
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import Hex.

Suggested change
import { createPublicClient, http, erc20Abi, Hex, Address } from "viem";
import { createPublicClient, http, erc20Abi, Address } from "viem";

Copilot uses AI. Check for mistakes.
import useKaspaBalance from "@/hooks/wallet/useKaspaBalance";
import { useErc20Prices } from "@/hooks/evm/useZealousSwapMetadata";
import {
useEvmKasBalances,
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import useEvmKasBalances.

Suggested change
useEvmKasBalances,

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
hooks/evm/useZealousSwapMetadata.ts (1)

25-31: Consider adding dedupingInterval to SWR config.

The SWR configuration includes refreshInterval and revalidation flags but lacks dedupingInterval. Adding it can prevent redundant requests when multiple components mount simultaneously and request the same data.

🔎 Suggested improvement
 export function useZealousSwapTokensMetadata() {
   return useSWR<ZealousSwapApiResponse>(ZEALOUS_SWAP_API_URL, fetcher, {
     refreshInterval: 60_000, // Refresh every minute
     revalidateOnFocus: false,
     revalidateOnReconnect: true,
+    dedupingInterval: 60_000, // Dedupe requests within 1 minute
   });
 }
hooks/evm/useEvmKasBalance.ts (1)

64-78: Consider structured error tracking for balance fetch failures.

Lines 64-78 catch per-chain balance fetch errors, log them to console, and replace with 0 balance. While this prevents one chain's failure from blocking others, using console.error means errors won't be tracked in production monitoring systems like Sentry.

Consider using structured error tracking:

🔎 Suggested improvement
       } catch (error) {
-        console.error(`Error fetching balance for chain ${chainId}:`, error);
+        captureException(error, {
+          tags: { chainId, component: 'useEvmKasBalance' },
+          extra: { evmAddress },
+        });
         balances[chainId] = {
           rawBalance: 0n,
           balance: "0",
         };
       }

Don't forget to import captureException from @sentry/react at the top of the file.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b522491 and 3aab50f.

📒 Files selected for processing (10)
  • components/dashboard/Erc20Asset.tsx
  • components/erc20-asset/Erc20Info.tsx
  • components/screens/asset-selector/Erc20SelectItem.tsx
  • components/send/evm/erc20-send/ConfirmStep.tsx
  • components/send/evm/erc20-send/DetailsStep.tsx
  • hooks/evm/useErc20Balance.ts
  • hooks/evm/useEvmKasBalance.ts
  • hooks/evm/useZealousSwapMetadata.ts
  • hooks/kasplex/useTotalBalanceByAccount.ts
  • hooks/usePortfolioPerformance.ts
🧰 Additional context used
🧬 Code graph analysis (7)
hooks/usePortfolioPerformance.ts (4)
hooks/wallet/useWalletManager.ts (1)
  • useWalletManager (3-5)
hooks/useKaspaPrice.ts (1)
  • useKaspaPrice (3-5)
hooks/wallet/useKaspaBalance.ts (1)
  • useKaspaBalance (3-6)
hooks/evm/useEvmKasBalance.ts (1)
  • useEvmKasBalancesByAddress (41-95)
hooks/kasplex/useTotalBalanceByAccount.ts (3)
hooks/evm/useErc20Balance.ts (1)
  • useErc20BalancesByAddress (44-115)
hooks/evm/useZealousSwapMetadata.ts (1)
  • useErc20Prices (85-93)
hooks/evm/useEvmKasBalance.ts (1)
  • useEvmKasBalancesByAddress (41-95)
hooks/evm/useEvmKasBalance.ts (3)
hooks/useSettings.ts (1)
  • useSettings (3-8)
lib/layer2.ts (2)
  • MAINNET_SUPPORTED_EVM_L2_CHAINS (56-56)
  • TESTNET_SUPPORTED_EVM_L2_CHAINS (55-55)
hooks/evm/useEvmAddress.ts (1)
  • useEvmAddress (4-12)
components/send/evm/erc20-send/ConfirmStep.tsx (1)
hooks/evm/useZealousSwapMetadata.ts (1)
  • useErc20Price (59-83)
components/erc20-asset/Erc20Info.tsx (4)
components/dashboard/Erc20Asset.tsx (1)
  • Erc20Asset (17-63)
contexts/EvmAssets.tsx (1)
  • Erc20Asset (12-18)
hooks/evm/useZealousSwapMetadata.ts (2)
  • useErc20Price (59-83)
  • useErc20Image (33-57)
hooks/useCurrencyValue.ts (1)
  • useCurrencyValue (14-31)
hooks/evm/useErc20Balance.ts (3)
hooks/evm/useErc20Assets.ts (1)
  • useErc20Assets (7-55)
lib/layer2.ts (1)
  • ALL_SUPPORTED_EVM_L2_CHAINS (58-61)
hooks/evm/useEvmAddress.ts (1)
  • useEvmAddress (4-12)
components/send/evm/erc20-send/DetailsStep.tsx (5)
hooks/evm/useErc20Balance.ts (1)
  • useErc20Balance (8-42)
hooks/evm/useEvmKasBalance.ts (1)
  • useEvmKasBalance (17-39)
hooks/evm/useZealousSwapMetadata.ts (2)
  • useErc20Image (33-57)
  • useErc20Price (59-83)
hooks/useCurrencyValue.ts (1)
  • useCurrencyValue (14-31)
lib/utils.ts (1)
  • formatToken (61-69)
🪛 Biome (2.1.2)
components/dashboard/Erc20Asset.tsx

[error] 17-17: Shouldn't redeclare 'Erc20Asset'. Consider to delete it or rename it.

'Erc20Asset' is defined here:

(lint/suspicious/noRedeclare)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🔇 Additional comments (12)
components/send/evm/erc20-send/ConfirmStep.tsx (1)

29-29: LGTM! Dynamic ERC20 pricing now integrated.

The replacement of the hardcoded tokenPrice = 0 with the dynamic price from useErc20Price correctly enables real-time fiat amount calculations for ERC20 transfers on Kasplex.

Also applies to: 73-73, 76-76

components/erc20-asset/Erc20Info.tsx (1)

8-17: LGTM! Dynamic price and image metadata integrated correctly.

The component now fetches real-time token prices and logo URLs from the Zealous Swap API, replacing hardcoded placeholders. The price is correctly fed into useCurrencyValue for fiat conversion, and the logo fallback mechanism is properly implemented.

components/dashboard/Erc20Asset.tsx (1)

20-31: LGTM! Dynamic pricing and image fallback correctly implemented.

The balance retrieval, price calculation, and image fallback logic are correctly updated to use the new hooks. The formatToken call on line 31 correctly passes a number (data?.balance ?? 0), and the total balance calculation properly multiplies price by balance.

Note: The static analysis warning about redeclaring Erc20Asset on line 17 is a false positive—TypeScript allows the same identifier in different namespaces (type vs value).

Also applies to: 39-39

components/send/evm/erc20-send/DetailsStep.tsx (1)

27-30: LGTM! Balance, price, and image handling correctly refactored.

The updated balance retrieval (removing the decimals parameter), dynamic price fetching, and logo fallback are all correctly implemented. Balance is consistently treated as a number, aligning with the formatToken signature requirements.

Also applies to: 51-56, 81-81, 91-92

hooks/evm/useEvmKasBalance.ts (1)

84-94: LGTM! Multi-chain balance aggregation correctly implemented.

The new architecture fetches balances across all supported chains in parallel, gracefully handles per-chain errors, and maintains backward compatibility through the existing useEvmKasBalance(chainId) interface.

Optional: Consider adding dedupingInterval: 10000 to the SWR config to match the refreshInterval and prevent duplicate requests during the same window.

hooks/kasplex/useTotalBalanceByAccount.ts (1)

6-12: LGTM! Layer2 balance aggregation correctly integrated.

The hook now properly aggregates ERC20 token balances and EVM KAS balances across supported chains into the total USD balance. The implementation includes proper error handling (skipping items with errors, handling missing prices) and correctly converts string balances to numbers for calculations.

Also applies to: 23-55

hooks/usePortfolioPerformance.ts (3)

9-10: LGTM!

The new imports correctly support the Layer2 balance integration.


31-32: USD calculations correctly use aggregated balance.

The calculations properly incorporate both on-chain and Layer2 balances. However, ensure the precision issues from lines 25-30 are addressed to maintain accuracy in these USD valuations.


17-23: Publickey to address derivation is correct and the TODO is appropriately marked.

The publicKeyToAddress import from viem/utils is valid in viem 2.37.13, and the EVM address derivation correctly handles the case where evmPublicKey may not exist. The TODO for ERC20 token performance calculation is appropriate for tracking future work.

hooks/evm/useErc20Balance.ts (3)

1-6: LGTM!

The imports correctly support the refactored batch-based balance fetching approach.


117-120: LGTM!

Clean wrapper pattern that delegates to useErc20BalancesByAddress with the current user's EVM address.


8-42: The review comment's claim about a breaking change is incorrect. The useErc20Balance function signature is (tokenAddress: string, chainId: string) with only 2 parameters, and all existing call sites in the codebase (3 usages: Erc20Asset.tsx, DetailsStep.tsx, Erc20SelectItem.tsx) already use this signature. There is no decimals parameter in the function's signature—decimals is fetched internally via useErc20Balances() from the asset data and included in the returned balance objects. The refactored design is correct.

The type cast on line 35 (balanceData as { error: string }).error could be made more explicit by defining a proper error type for better type safety.

Likely an incorrect or invalid review comment.


export default function Erc20Asset({ asset }: { asset: Erc20Asset }) {
const navigate = useNavigate();
const [settings] = useSettings();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing import for useSettings.

Line 19 uses useSettings() but the import statement is missing. This will cause a compilation error.

🔎 Proposed fix

Add the missing import at the top of the file:

 import { Erc20Asset } from "@/contexts/EvmAssets";
 import useErc20Balance from "@/hooks/evm/useErc20Balance";
+import { useSettings } from "@/hooks/useSettings";
 import Layer2AssetImage from "../Layer2AssetImage";

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In components/dashboard/Erc20Asset.tsx around line 19, the hook useSettings is
used but not imported; add an import for useSettings at the top of the file
(e.g. import { useSettings } from '<module-where-useSettings-is-exported>';),
placing it with the other React/hooks imports so the file compiles.

Comment on lines +16 to +18
const { data } = useErc20Balance(asset.address, asset.chainId);
const balance = data?.balance ?? "0";
const { logoUrl } = useErc20Image(asset.chainId, asset.address);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the balance type returned by useErc20Balance hook and its usage across components

echo "=== Checking useErc20Balance return type definition ==="
fd -e ts -e tsx | xargs rg -l "useErc20Balance" | head -5
echo ""

echo "=== Finding useErc20Balance hook definition ==="
fd -e ts -e tsx -path "*/hooks/*" | xargs rg "useErc20Balance" -l

echo ""
echo "=== Checking the hook implementation ==="
rg -A 15 "export.*function useErc20Balance|export.*const useErc20Balance" --type=ts --type=tsx

echo ""
echo "=== Checking balance usage in Erc20SelectItem.tsx ==="
rg -n -A 3 -B 3 "balance" components/screens/asset-selector/Erc20SelectItem.tsx

echo ""
echo "=== Checking formatToken function signature ==="
rg -B 2 -A 8 "function formatToken|const formatToken|export.*formatToken" --type=ts --type=tsx | head -20

echo ""
echo "=== Checking balance type in other files ==="
rg -n "useErc20Balance" --type=ts --type=tsx -A 2 | head -30

Repository: forbole/kastle

Length of output: 1899


🏁 Script executed:

#!/bin/bash

echo "=== Checking useErc20Balance hook implementation ==="
cat hooks/evm/useErc20Balance.ts

echo ""
echo "=== Searching for formatToken function ==="
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "function formatToken\|const formatToken\|export.*formatToken" 2>/dev/null

echo ""
echo "=== Getting formatToken definition ==="
find . -name "*.ts" -o -name "*.tsx" | xargs grep -B 2 -A 10 "function formatToken\|const formatToken.*=" 2>/dev/null | head -30

Repository: forbole/kastle

Length of output: 4683


Fix critical type mismatch with balance variable.

The useErc20Balance hook returns data.balance as a number (parsed via parseFloat(formatUnits(...))), but line 17 defaults it to the string "0", creating a number | string union type. This causes a type error on line 55 when passing balance to formatToken(number), which expects only a number. Additionally, line 28 compares balance !== "0" (string comparison), which is inconsistent if balance is a number.

Fix by using a numeric default: const balance = data?.balance ?? 0;

🤖 Prompt for AI Agents
In components/screens/asset-selector/Erc20SelectItem.tsx around lines 16 to 18,
the balance variable is defaulting to a string "0" while useErc20Balance returns
a number, producing a number|string union and causing type errors when passed to
functions expecting a number; change the default to a numeric 0 and update any
comparisons to use numeric checks (e.g., balance !== 0) so balance is
consistently typed as a number throughout the file.

</div>
</div>
{!isLedger && <span>{formatToken(parseFloat(balance))}</span>}
{!isLedger && <span>{formatToken(balance)}</span>}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix type mismatch in formatToken call.

The balance variable is typed as string (line 17: data?.balance ?? "0"), but formatToken expects a number as its first parameter. This will cause a TypeScript error or unexpected runtime behavior.

🔎 Proposed fix
-          {!isLedger && <span>{formatToken(balance)}</span>}
+          {!isLedger && <span>{formatToken(Number(balance))}</span>}

Or better yet, fix line 17 to treat balance as a number:

-  const balance = data?.balance ?? "0";
+  const balance = data?.balance ?? 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{!isLedger && <span>{formatToken(balance)}</span>}
{!isLedger && <span>{formatToken(Number(balance))}</span>}
🤖 Prompt for AI Agents
components/screens/asset-selector/Erc20SelectItem.tsx around line 55:
formatToken is being passed balance which is currently a string (set at line 17
as data?.balance ?? "0"), causing a type mismatch; change the balance
initialization to produce a number (e.g. parseFloat(data?.balance ?? "0") or
Number(data?.balance ?? 0)) so formatToken receives a numeric first argument,
and update any related typings to reflect balance: number.

Comment on lines +44 to +115
export function useErc20BalancesByAddress(evmAddress?: Address) {
const { assets } = useErc20Assets();

// Use all tokens from useErc20Tokens
const tokensToFetch =
assets?.map((asset) => {
return {
tokenAddress: asset.address,
decimals: asset.decimals,
chainId: asset.chainId,
};
}) ?? [];

export default function useErc20Balance(
tokenAddress: string,
decimals: number,
chainId: string,
) {
const evmAddress = useEvmAddress();
const fetcher = async () => {
if (!evmAddress) {
return {};
if (!evmAddress || tokensToFetch.length === 0) {
return [];
}

const chain = ALL_SUPPORTED_EVM_L2_CHAINS.find(
(c) => numberToHex(c.id) === chainId,
const balancePromises = tokensToFetch.map(
async ({ tokenAddress, decimals, chainId }) => {
const chain = ALL_SUPPORTED_EVM_L2_CHAINS.find(
(c) => numberToHex(c.id) === chainId,
);
if (!chain) {
return {
error: `Unsupported chain ID: ${chainId}`,
};
}

const client = createPublicClient({
chain,
transport: http(),
});

try {
const rawBalance = await client.readContract({
address: tokenAddress as `0x${string}`,
abi: erc20Abi,
functionName: "balanceOf",
args: [evmAddress],
});
return {
tokenAddress,
chainId,
decimals,
rawBalance,
balance: parseFloat(formatUnits(rawBalance, decimals)),
};
} catch (error) {
console.error(`Error fetching balance for ${tokenAddress}:`, error);
return {
error: `Failed to fetch balance: ${(error as Error).message}`,
};
}
},
);
if (!chain) {
throw new Error(`Unsupported chain ID: ${chainId}`);
}

const client = createPublicClient({
chain,
transport: http(),
});

const rawBalance = await client.readContract({
address: tokenAddress as `0x${string}`,
abi: erc20Abi,
functionName: "balanceOf",
args: [evmAddress],
});
return {
rawBalance,
balance: formatUnits(rawBalance, decimals),
};
return Promise.all(balancePromises);
};

return useSWR(
evmAddress ? `erc20Balance:${tokenAddress}:${evmAddress}-${chainId}` : null,
evmAddress ? fetcher : null,
{
refreshInterval: 10000, // Refresh every 10 seconds
revalidateOnFocus: false,
revalidateOnReconnect: false,
},
);
const key =
evmAddress && tokensToFetch && tokensToFetch.length > 0
? `erc-20-balances:${evmAddress}`
: null;

return useSWR(key, evmAddress ? fetcher : null, {
refreshInterval: 5_000,
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 2_000,
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Improve type safety and error handling in batch fetching.

The batch balance fetching implementation has several concerns:

  1. Mixed result types: Success and error objects are mixed in the same array (lines 68-70, 94-96) without a discriminated union type. This makes it difficult for consumers to safely handle results.

  2. No explicit return type: The function lacks an explicit return type annotation, making the API contract unclear.

  3. Type safety issue: The error objects { error: string } and success objects have different shapes but no discriminator field.

🔎 Suggested improvements

Define a discriminated union type:

type BalanceResult = 
  | { 
      success: true;
      tokenAddress: string;
      chainId: string;
      decimals: number;
      rawBalance: bigint;
      balance: number;
    }
  | {
      success: false;
      tokenAddress?: string;
      chainId?: string;
      error: string;
    };

Update the function signature:

-export function useErc20BalancesByAddress(evmAddress?: Address) {
+export function useErc20BalancesByAddress(evmAddress?: Address): SWRResponse<BalanceResult[], Error> {

Update error returns to include the discriminator:

 return {
+  success: false,
+  tokenAddress,
+  chainId,
   error: `Failed to fetch balance: ${(error as Error).message}`,
 };

And success returns:

 return {
+  success: true,
   tokenAddress,
   chainId,
   decimals,
   rawBalance,
   balance: parseFloat(formatUnits(rawBalance, decimals)),
 };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
hooks/evm/useErc20Balance.ts lines 44-115: the batch fetch mixes heterogeneous
success and error objects without a discriminator and lacks an explicit return
type, making consumers unsafe; define a discriminated union (e.g. BalanceResult
with success: true {tokenAddress, chainId, decimals, rawBalance, balance} and
success: false {tokenAddress?, chainId?, error}), annotate the hook and fetcher
to return Promise<BalanceResult[]>, update every early/exception return to
produce the failure shape (success: false, error, optional tokenAddress/chainId)
and every successful return to produce the success shape (success: true plus
fields), and ensure Promise.all is typed to BalanceResult[] so callers can
safely narrow on success.

Comment on lines +33 to +57
export function useErc20Image(chainId: Hex, contractAddress: Address) {
const { data, error, isLoading } = useZealousSwapTokensMetadata();

const numericChainId = hexToNumber(chainId);
const isKasplex = numericChainId === kasplexMainnet.id;

if (!isKasplex) {
return { logoUrl: undefined, error: "Unsupported chain", isLoading: false };
}

const normalizedAddress = contractAddress.toLowerCase() as Address;
const token = data?.tokens.find(
(t) => t.address.toLowerCase() === normalizedAddress,
);

const logoUrl = token
? `${ZEALOUS_SWAP_IMAGE_URL}${token.logoURI}`
: undefined;

return {
logoUrl,
error,
isLoading,
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent error handling for unsupported chains.

useErc20Image returns { error: "Unsupported chain" } for non-Kasplex chains (line 40), while useErc20Price returns { error: undefined } for the same scenario (lines 66-70). This inconsistency may confuse consumers of these hooks.

Consider standardizing the error handling approach—either both hooks should return an error message for unsupported chains, or neither should.

🔎 Suggested fix for consistency

Option 1: Both return undefined error (indicating "not an error, just not supported"):

   if (!isKasplex) {
-    return { logoUrl: undefined, error: "Unsupported chain", isLoading: false };
+    return { logoUrl: undefined, error: undefined, isLoading: false };
   }

Option 2: Both return an error message:

   if (!isKasplex) {
     return {
       price: 0,
-      error: undefined,
+      error: "Unsupported chain",
       isLoading: false,
     };
   }

Also applies to: 59-83

🤖 Prompt for AI Agents
hooks/evm/useZealousSwapMetadata.ts lines 33-57 (and similarly lines 59-83): the
hooks currently handle unsupported chains inconsistently (one returns error:
"Unsupported chain", the other error: undefined) — standardize to return error:
undefined for unsupported chains to match useErc20Price behavior; update the
early-return branches for non-Kasplex chains to return { logoUrl: undefined,
error: undefined, isLoading: false } (and for the other hook return its
corresponding value shape with error: undefined) so consumers see a consistent
"not supported" non-error state across both hooks.

Comment on lines +25 to +30
const kaspaBalance =
balance +
Object.values(evmKasBalances ?? {}).reduce(
(acc, { balance }) => acc + parseFloat(balance),
0,
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider precision loss and add error handling for balance parsing.

The balance aggregation has potential issues:

  1. Floating-point precision: Using parseFloat on line 28 can introduce precision errors when dealing with cryptocurrency amounts. Consider using BigInt or decimal libraries for accurate financial calculations.

  2. No error handling: If balance is not a valid number string, parseFloat will return NaN, which will propagate through calculations silently.

  3. Silent failures: Failed balance fetches in evmKasBalances would be included in the aggregation without indication.

🔎 Suggested improvements
 const kaspaBalance =
   balance +
   Object.values(evmKasBalances ?? {}).reduce(
-    (acc, { balance }) => acc + parseFloat(balance),
+    (acc, { balance }) => {
+      const parsed = parseFloat(balance);
+      if (isNaN(parsed)) {
+        console.error(`Invalid balance value: ${balance}`);
+        return acc;
+      }
+      return acc + parsed;
+    },
     0,
   );

Consider using a decimal library like decimal.js or bignumber.js for precise financial calculations.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const kaspaBalance =
balance +
Object.values(evmKasBalances ?? {}).reduce(
(acc, { balance }) => acc + parseFloat(balance),
0,
);
const kaspaBalance =
balance +
Object.values(evmKasBalances ?? {}).reduce(
(acc, { balance }) => {
const parsed = parseFloat(balance);
if (isNaN(parsed)) {
console.error(`Invalid balance value: ${balance}`);
return acc;
}
return acc + parsed;
},
0,
);
🤖 Prompt for AI Agents
In hooks/usePortfolioPerformance.ts around lines 25 to 30, the aggregation uses
parseFloat which can cause floating-point precision errors and silently
propagate NaN for invalid balance strings; replace the aggregation to use a
fixed-precision library (e.g., decimal.js or bignumber.js) to sum values,
validate each evmKasBalances entry before including it (skip or treat invalid
strings as zero), and surface or log malformed entries so failures aren’t
silent; ensure the final kaspaBalance is returned/converted in the expected type
consistently.

@dadamu dadamu changed the title Feat/include layer2 balances feat: include layer2 balances Dec 30, 2025
@dadamu dadamu merged commit 3926175 into main Dec 30, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant