Skip to content

feat(token-select): sort by positive balance by default when wallet connected#475

Merged
gabitoesmiapodo merged 6 commits intodevelopfrom
feat/466
Apr 21, 2026
Merged

feat(token-select): sort by positive balance by default when wallet connected#475
gabitoesmiapodo merged 6 commits intodevelopfrom
feat/466

Conversation

@gabitoesmiapodo
Copy link
Copy Markdown
Collaborator

Summary

Closes #466

The token selector now surfaces tokens with a positive balance at the top of the list (ordered by USD value descending) whenever a wallet is connected, without the consumer needing to set showBalance={true}. Previously, the sort-by-USD logic in useTokens was gated on withBalance, conflating two concerns: fetching/sorting balances and rendering the balance column per row. This PR decouples those concerns and extends sorting to LI.FI-unsupported chains via a multicall fallback.

Changes

  • useTokens: introduce sortByBalance param independent of the balance-column withBalance flag; fetch and apply USD-value sort whenever a wallet is connected and sortByBalance is true
  • Multicall path: fix address normalisation for on-chain balance fetching on LI.FI-unsupported chains
  • TokenSelect, TokenInput, TokenDropdown: default sortByBalance to true when wallet is connected; showBalance still controls only the balance column
  • useWalletStatus: expose web3Status directly to eliminate a redundant hook call in consumers
  • Tests: expanded useTokens.test.ts coverage asserting positive-balance tokens sort before zero-balance tokens; updated useWalletStatus and useWeb3Status tests accordingly

Acceptance criteria

  • Connecting a wallet shows tokens with positive balances at the top, highest USD value first, on any LI.FI-supported chain -- without showBalance={true}
  • Disconnected wallet: list renders in source order, no balance requests fired
  • showBalance controls only whether the balance column is rendered per row
  • New sortByBalance prop is documented in JSDoc on TokenSelect, TokenInput, TokenDropdown
  • Existing call sites keep working unchanged
  • Unit test asserts positive-balance tokens come before zero-balance tokens after updateTokensBalances
  • (Stretch) Sort-by-balance works on LI.FI-unsupported chains via multicall fallback

Test plan

Automated tests

  • src/hooks/useTokens.test.ts
  • src/hooks/useWalletStatus.test.ts
  • src/hooks/useWeb3Status.test.ts
  • src/components/sharedComponents/WalletStatusVerifier.test.tsx
  • src/components/sharedComponents/SignButton.test.tsx
  • src/components/sharedComponents/TransactionButton.test.tsx
pnpm test

Manual verification

  1. Connect a wallet on a supported chain (e.g. mainnet or Sepolia)
  2. Open the token selector -- tokens with positive balance should appear first, ordered by USD value descending
  3. Disconnect wallet -- list should render in source order with no balance requests

Breaking changes

None.

Checklist

  • Self-reviewed my own diff
  • Tests added or updated
  • Docs updated (if applicable)
  • No unrelated changes bundled in

Screenshots

None.

…onnected

- Add sortByBalance prop to TokenSelect / TokenInput / TokenDropdown;
  defaults to isWalletConnected so held tokens surface without any
  consumer-side change
- Decouple balance fetch/sort from showBalance so the balance column
  and the sort are independent concerns; showBalance only controls
  the per-row balance column
- Add updateTokensWithRawBalances export for on-chain multicall path
- Add multicall fallback in useTokens: when the active chain is not
  covered by LI.FI (e.g. Sepolia), fetch ERC-20 balances via multicall
  and native balance via getBalance, then sort with the same sortFn
- Add tests asserting positive-balance tokens precede zero-balance
  tokens and that source order is preserved within each group

Closes #466
…ble hook call

WalletStatusVerifier was calling useWeb3Status() directly in addition to
calling it internally via useWalletStatus(), executing the hook body twice
per render. Exposing web3Status from useWalletStatus eliminates the redundant
call.

Also fixes || to ?? for chain ID fallback (prevents a falsy 0 chainId from
silently falling through) and adds chainId={sepolia.id} to the TransactionButton
in NativeToken — without it the inner button checked against appChainId instead
of the Sepolia chain already verified by the parent WalletStatusVerifier.
…refactor

- Lowercase token addresses when keying the on-chain balances map and when
  reading in updateTokensWithRawBalances, preventing silent zero-balance
  fallback if token lists mix address casing
- Complete web3Status exposure from useWalletStatus (was missing from the
  previous commit): add web3Status to WalletStatus interface and return,
  remove the redundant useWeb3Status() call from WalletStatusVerifier
- Assert result.current.web3Status in useWalletStatus tests
- Update all useWalletStatus mock shapes across test files to carry web3Status
- Document multicall fallback path and absent priceUSD in useTokens JSDoc
- Add inline comment on NativeToken chainId prop
- Move threeTokens fixture inside its describe block
Copilot AI review requested due to automatic review settings April 21, 2026 19:13
@gabitoesmiapodo gabitoesmiapodo self-assigned this Apr 21, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
components.dappbooster Ready Ready Preview, Comment Apr 21, 2026 7:47pm
demo.dappbooster Ready Ready Preview, Comment Apr 21, 2026 7:47pm
docs.dappbooster Ready Ready Preview, Comment Apr 21, 2026 7:47pm

Request Review

Copy link
Copy Markdown
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 improves the token selection UX by prioritizing tokens the connected wallet actually holds, sorting positive-balance tokens to the top (USD-value desc when LI.FI price data is available) and adding an on-chain multicall fallback for chains not supported by LI.FI. It also simplifies wallet-status consumption by returning web3Status directly from useWalletStatus, with related test updates.

Changes:

  • Add balance-aware sorting behavior to token selection flows (defaulting to enabled when a wallet is connected) while keeping showBalance focused on rendering the balance column.
  • Implement on-chain multicall balance fetching fallback for LI.FI-unsupported chains and adjust balance/value rendering to handle missing price data.
  • Expose web3Status via useWalletStatus and update consumers/tests accordingly.

Reviewed changes

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

Show a summary per file
File Description
src/hooks/useTokens.ts Adds multicall fallback balance fetching and raw-balance sorting for unsupported chains.
src/hooks/useTokens.test.ts Adds/expands unit tests for positive-balance-first sorting and raw-balance fallback behavior.
src/components/sharedComponents/TokenSelect/index.tsx Introduces sortByBalance prop and enables balance fetching by default when connected.
src/components/sharedComponents/TokenSelect/List/TokenBalance.tsx Shows “N/A” when priceUSD is unavailable (e.g., multicall fallback).
src/components/sharedComponents/TokenInput/index.tsx Passes through and documents sortByBalance.
src/components/sharedComponents/TokenDropdown.tsx Documents sortByBalance in component JSDoc.
src/hooks/useWalletStatus.ts Returns web3Status from useWalletStatus to avoid redundant hook calls.
src/components/sharedComponents/WalletStatusVerifier.tsx Uses web3Status from useWalletStatus instead of calling useWeb3Status directly.
src/hooks/useWalletStatus.test.ts Adds assertions for the newly exposed web3Status.
src/hooks/useWeb3Status.test.ts Updates connected-status verifier test to rely on useWalletStatus().web3Status.
src/components/sharedComponents/WalletStatusVerifier.test.tsx Updates mocks to include web3Status in useWalletStatus.
src/components/sharedComponents/TransactionButton.test.tsx Updates useWalletStatus mocks to satisfy new return shape (currently via unsafe casts).
src/components/sharedComponents/SignButton.test.tsx Updates useWalletStatus mocks to satisfy new return shape (currently via unsafe casts).
src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx Makes demo’s Sepolia chainId explicit when using TransactionButton.

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

Comment on lines 148 to 152
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
targetChainId: 1,
switchChain: mockSwitchChain,
web3Status: undefined as unknown as ReturnType<typeof useWalletStatus>['web3Status'],
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The useWalletStatus mock returns web3Status as undefined via a cast. Since useWalletStatus now guarantees a real Web3Status object, this mock can hide runtime issues if the component starts using web3Status. Prefer returning a minimal but valid Web3Status stub instead of casting undefined.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in 9f5b773 as part of the shared mockWeb3Status stub rollout in this file.

Comment on lines 104 to 108
>['targetChain'],
targetChainId: 10,
switchChain: mockSwitchChain,
web3Status: undefined as unknown as ReturnType<typeof useWalletStatus>['web3Status'],
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The useWalletStatus mock returns web3Status as undefined via a cast. Since useWalletStatus now guarantees a real Web3Status object, this mock can hide runtime issues if the component starts using web3Status. Prefer returning a minimal but valid Web3Status stub instead of casting undefined.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in d8c5ae2 as part of the shared mockWeb3Status stub rollout in this file.

const { isLoadingBalances, tokensByChainId } = useTokens({
chainId,
withBalance: showBalance,
withBalance: showBalance || resolvedSortByBalance,
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

sortByBalance is documented as a separate sorting toggle, but the current wiring only uses it to decide withBalance; useTokens will still sort whenever withBalance is true. This means sortByBalance={false} does not actually disable sorting when showBalance={true} (balances will be fetched and updateTokensBalances will sort). Consider passing an explicit sortByBalance flag into useTokens/updateTokensBalances (or conditionally applying the sort in TokenSelect) so consumers can independently choose to show the balance column without enabling sorting (and vice versa).

Suggested change
withBalance: showBalance || resolvedSortByBalance,
withBalance: showBalance || resolvedSortByBalance,
sortByBalance: resolvedSortByBalance,

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 0897b15. Added a sortByBalance parameter (default true) to useTokens, updateTokensBalances, and updateTokensWithRawBalances; the sort step is now skipped when false. TokenSelect passes sortByBalance: resolvedSortByBalance, so sortByBalance={false} with showBalance={true} now shows balances without reordering.

Comment on lines 61 to 65
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
targetChainId: 1,
switchChain: mockSwitchChain,
web3Status: undefined as unknown as ReturnType<typeof useWalletStatus>['web3Status'],
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The useWalletStatus mock returns web3Status as undefined via a cast. Since useWalletStatus now guarantees a real Web3Status object, this mock can hide runtime issues if the component starts using web3Status. Prefer returning a minimal but valid Web3Status stub (as done in WalletStatusVerifier tests) instead of casting undefined.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 9f5b773. Replaced the undefined as unknown cast with a shared mockWeb3Status stub mirroring WalletStatusVerifier.test.tsx.

Comment on lines 124 to 128
>['targetChain'],
targetChainId: 10,
switchChain: mockSwitchChain,
web3Status: undefined as unknown as ReturnType<typeof useWalletStatus>['web3Status'],
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The useWalletStatus mock returns web3Status as undefined via a cast. Since useWalletStatus now guarantees a real Web3Status object, this mock can hide runtime issues if the component starts using web3Status. Prefer returning a minimal but valid Web3Status stub instead of casting undefined.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in 9f5b773 as part of the shared mockWeb3Status stub rollout in this file.

Comment on lines 63 to 67
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
targetChainId: 1,
switchChain: mockSwitchChain,
web3Status: undefined as unknown as ReturnType<typeof useWalletStatus>['web3Status'],
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The useWalletStatus mock returns web3Status as undefined via a cast. Since useWalletStatus now guarantees a real Web3Status object, this mock can hide runtime issues if the component starts using web3Status. Prefer returning a minimal but valid Web3Status stub instead of casting undefined.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in d8c5ae2. Replaced the undefined as unknown cast with a shared mockWeb3Status stub mirroring WalletStatusVerifier.test.tsx.

Comment on lines 80 to 84
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
targetChainId: 1,
switchChain: mockSwitchChain,
web3Status: undefined as unknown as ReturnType<typeof useWalletStatus>['web3Status'],
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The useWalletStatus mock returns web3Status as undefined via a cast. Since useWalletStatus now guarantees a real Web3Status object, this mock can hide runtime issues if the component starts using web3Status. Prefer returning a minimal but valid Web3Status stub instead of casting undefined.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in d8c5ae2 as part of the shared mockWeb3Status stub rollout in this file.

Comment on lines 122 to 126
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
targetChainId: 1,
switchChain: mockSwitchChain,
web3Status: undefined as unknown as ReturnType<typeof useWalletStatus>['web3Status'],
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The useWalletStatus mock returns web3Status as undefined via a cast. Since useWalletStatus now guarantees a real Web3Status object, this mock can hide runtime issues if the component starts using web3Status. Prefer returning a minimal but valid Web3Status stub instead of casting undefined.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in d8c5ae2 as part of the shared mockWeb3Status stub rollout in this file.

Comment on lines 78 to 82
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
targetChainId: 1,
switchChain: mockSwitchChain,
web3Status: undefined as unknown as ReturnType<typeof useWalletStatus>['web3Status'],
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The useWalletStatus mock returns web3Status as undefined via a cast. Since useWalletStatus now guarantees a real Web3Status object, this mock can hide runtime issues if the component starts using web3Status. Prefer returning a minimal but valid Web3Status stub instead of casting undefined.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in 9f5b773 as part of the shared mockWeb3Status stub rollout in this file.

Comment on lines 104 to 108
>['targetChain'],
targetChainId: 10,
switchChain: mockSwitchChain,
web3Status: undefined as unknown as ReturnType<typeof useWalletStatus>['web3Status'],
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The useWalletStatus mock returns web3Status as undefined via a cast. Since useWalletStatus now guarantees a real Web3Status object, this mock can hide runtime issues if the component starts using web3Status. Prefer returning a minimal but valid Web3Status stub instead of casting undefined.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in 9f5b773 as part of the shared mockWeb3Status stub rollout in this file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sort token selector by positive balance first

2 participants