Skip to content

Conversation

@dieguezguille
Copy link
Member

@dieguezguille dieguezguille commented Jan 23, 2026


Open with Devin

Summary by CodeRabbit

  • New Features

    • New chain logo component and unified asset logo component with symbol-based images and initials fallback.
  • Improvements

    • App-wide switch to portfolio-backed asset sourcing for more accurate balances and listings.
    • Prefetching and query enhancements for tokens/chains to reduce loading states and improve responsiveness.
    • Replaced several static chain images with the new dynamic chain logo rendering.

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

@changeset-bot
Copy link

changeset-bot bot commented Jan 23, 2026

⚠️ No Changeset found

Latest commit: 9c537a7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

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.

Walkthrough

Replaces static URI-based asset logos with a symbol-driven AssetLogo (which fetches token logos), adds ChainLogo, removes useAccountAssets in favor of usePortfolio, adds LiFi query options/token balance queries, and exposes persistOptions from the query client. Many components updated to the new APIs and props.

Changes

Cohort / File(s) Summary
Core shared components
src/components/shared/AssetLogo.tsx, src/components/shared/ChainLogo.tsx, src/components/add-funds/TokenLogo.tsx
AssetLogo rewritten to accept symbol and fetch token logoURI (falls back to initials); new ChainLogo added; TokenLogo.tsx removed.
Query client & persistence
src/utils/queryClient.ts, src/app/_layout.tsx
Added lifiChainsOptions, lifiTokensOptions, tokenBalancesOptions; exported persistOptions (used in layout). Dehydrate options updated to exclude "lifi".
Portfolio & hooks
src/utils/usePortfolio.ts, src/utils/useAccountAssets.ts
useAccountAssets deleted; usePortfolio added/expanded with ProtocolAsset/ExternalAsset/PortfolioAsset types, merged assets, sorting options, and pending flags.
Asset logos & LiFi helpers
src/utils/assetLogos.ts, src/utils/lifi.ts
Added getTokenLogoURI(tokens, symbol) helper; refactored assetLogos export; lifi prefetch and defensive exa token handling added.
Global call-site updates (AssetLogo symbol)
src/components/... (many files, e.g. activity/*, add-funds/*, loans/*, pay-mode/*, send-funds/*, shared/*, swaps/*, home/*)
Replaced assetLogos/logoURI-based AssetLogo usages with AssetLogo symbol={...} across numerous components; removed many assetLogos imports and adjusted types/props (e.g., removed logoURI/external props).
Chain logo replacements
src/components/*/... (e.g. TransactionDetails.tsx, AddressDialog.tsx, CopyAddressSheet.tsx, TokenInput.tsx)
Replaced local OptimismImage or chain SVGs with ChainLogo usage; removed chainLogoUri prop from TokenInput and similar call-sites.
Selector / swaps changes
src/components/swaps/SelectorModal.tsx, src/components/swaps/Swaps.tsx, src/components/swaps/TokenInput.tsx
Swaps/selector moved from useAccountAssetsusePortfolio; TokenListItem/TokenInput updated to accept/match portfolio assets; AssetLogo usage switched to symbol.
Pay/send flow updates
src/components/pay-mode/*, src/components/send-funds/*
Pay.tsx now uses usePortfolio and manual selection state; ReviewSheet props trimmed (removed external/logoURI); AssetLogo symbol usage applied across pay/send UI.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • cruzdanilo
  • franm91
  • nfmelendez
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title '♻️ refactor lifi usage' accurately describes the main objective of the changeset, which comprehensively refactors how LiFi usage is implemented across the application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch lifi

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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @dieguezguille, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the application's approach to handling cryptocurrency assets and blockchain networks. By introducing dedicated AssetLogo and ChainLogo components, the system now dynamically retrieves and displays visual identifiers for tokens and chains, moving away from static imports. The integration with the LIFI SDK has been deepened to efficiently fetch and cache comprehensive token and chain information. Furthermore, asset management logic has been consolidated into a single usePortfolio hook, which now intelligently combines and sorts both protocol-specific and external assets, enhancing data consistency and simplifying asset-related operations across the application.

Highlights

  • Centralized Asset and Chain Logo Management: Introduced dedicated AssetLogo and ChainLogo components to dynamically fetch and display logos for tokens and blockchain networks, replacing numerous direct image imports and the TokenLogo component.
  • Enhanced LIFI SDK Integration: Deepened integration with the LIFI SDK for fetching and caching comprehensive token and chain data, including prefetching mechanisms and dynamic lookup of token logos based on available LIFI data.
  • Consolidated Asset Management Logic: Replaced the useAccountAssets hook with a new usePortfolio hook, centralizing the logic for managing both protocol-specific and external assets, improving data consistency and simplifying asset-related operations across the application.
  • Codebase Streamlining: Removed redundant files such as src/assets/images/optimism.svg and src/components/add-funds/TokenLogo.tsx, contributing to a cleaner and more maintainable codebase.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View issue and 5 additional flags in Devin Review.

Open in Devin Review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant and well-executed refactoring of how asset and chain data, particularly logos, are handled throughout the application. By replacing the hardcoded assetLogos map and component-specific logic with new generic AssetLogo and ChainLogo components that fetch data from LI.FI, you've greatly improved maintainability and scalability. The introduction of the usePortfolio hook to replace useAccountAssets and the centralization of LI.FI queries in queryClient.ts are excellent changes that streamline data fetching and caching. Overall, this is a high-quality refactoring. I've included one suggestion to address a React anti-pattern that was present in the code.

Comment on lines 489 to 495
if (!selectedAsset.address && assets[0]) {
const { type } = assets[0];
setSelectedAsset({
address: type === "external" ? parse(Address, accountAssets[0].address) : parse(Address, accountAssets[0].market),
address: type === "external" ? parse(Address, assets[0].address) : parse(Address, assets[0].market),
external: type === "external",
});
}

Choose a reason for hiding this comment

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

high

Calling a state setter (setSelectedAsset) directly within the component's render body is a React anti-pattern that can lead to unexpected re-renders and bugs. This logic should be moved into a useEffect hook to ensure it only runs after the component has rendered and when its dependencies change.

  useEffect(() => {
    if (!selectedAsset.address && assets[0]) {
      const { type } = assets[0];
      setSelectedAsset({
        address: type === "external" ? parse(Address, assets[0].address) : parse(Address, assets[0].market),
        external: type === "external",
      });
    }
  }, [assets, selectedAsset.address]);

@sentry
Copy link

sentry bot commented Jan 23, 2026

Codecov Report

❌ Patch coverage is 52.29358% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.37%. Comparing base (ca189ac) to head (9c537a7).

Files with missing lines Patch % Lines
src/utils/queryClient.ts 31.57% 26 Missing ⚠️
src/utils/lifi.ts 0.00% 11 Missing ⚠️
src/utils/usePortfolio.ts 65.51% 10 Missing ⚠️
src/components/shared/ChainLogo.tsx 75.00% 2 Missing ⚠️
src/components/home/CardLimits.tsx 0.00% 1 Missing ⚠️
src/components/shared/AssetLogo.tsx 85.71% 1 Missing ⚠️
src/components/shared/AssetSelector.tsx 75.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           optimize     #681      +/-   ##
============================================
- Coverage     61.68%   61.37%   -0.32%     
============================================
  Files           169      170       +1     
  Lines          5304     5364      +60     
  Branches       1498     1517      +19     
============================================
+ Hits           3272     3292      +20     
- Misses         1859     1900      +41     
+ Partials        173      172       -1     
Flag Coverage Δ
e2e 59.58% <52.29%> (-0.26%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/swaps/SelectorModal.tsx (1)

33-38: Lift usePortfolio out of TokenListItem and normalize address matching.

TokenListItem now depends on app state (usePortfolio), which violates the “dumb component” boundary and can duplicate portfolio computation per row. You already have assets in TokenSelectModal, so pass the matching asset down. While refactoring, normalize address casing to avoid mismatches.

♻️ Proposed refactor
 import usePortfolio from "../../utils/usePortfolio";
 import AssetLogo from "../shared/AssetLogo";
@@
 import type { Token } from "@lifi/sdk";
 
+type PortfolioAssetItem = ReturnType<typeof usePortfolio>["assets"][number];
+
+function findMatchingAsset(assets: PortfolioAssetItem[], token: Token) {
+  const tokenAddress = token.address.toLowerCase();
+  return assets.find((asset) => {
+    const assetAddress = asset.type === "protocol" ? asset.asset : asset.address;
+    return assetAddress?.toLowerCase() === tokenAddress;
+  });
+}
+
 function TokenListItem({
   token,
   isSelected,
   onPress,
   language,
+  matchingAsset,
 }: {
   isSelected: boolean;
   language: string;
   onPress: () => void;
   token: Token;
+  matchingAsset?: PortfolioAssetItem;
 }) {
-  const { assets } = usePortfolio();
-  const matchingAsset = assets.find(
-    (asset) =>
-      (asset.type === "protocol" && asset.asset === token.address) ||
-      (asset.type === "external" && asset.address === token.address),
-  );
   return (
@@
   const filteredTokens = useMemo(() => {
@@
-    const matchesAsset = (token: Token) =>
-      assets.find(
-        (asset) =>
-          (asset.type === "protocol" && asset.asset === token.address) ||
-          (asset.type === "external" && asset.address === token.address),
-      );
+    function matchesAsset(token: Token) {
+      return findMatchingAsset(assets, token);
+    }
@@
-                renderItem={({ item }) => (
-                  <TokenListItem
-                    token={item}
-                    isSelected={selectedToken?.address === item.address}
-                    onPress={() => {
-                      onSelect(item);
-                      setSearchQuery("");
-                    }}
-                    language={language}
-                  />
-                )}
+                renderItem={({ item }) => {
+                  const matchingAsset = findMatchingAsset(assets, item);
+                  return (
+                    <TokenListItem
+                      token={item}
+                      isSelected={selectedToken?.address === item.address}
+                      onPress={() => {
+                        onSelect(item);
+                        setSearchQuery("");
+                      }}
+                      language={language}
+                      matchingAsset={matchingAsset}
+                    />
+                  );
+                }}
As per coding guidelines, keep `src/components/**` UI components stateless and fed via props.

Also applies to: 108-134

🤖 Fix all issues with AI agents
In `@src/components/pay-mode/Pay.tsx`:
- Around line 489-495: The render currently calls setSelectedAsset directly
(selectedAsset, setSelectedAsset, assets, parse, Address), move this
initialization out of render: either initialize selectedAsset lazily in useState
with an initializer that inspects assets[0] and returns the parsed
address/external flag, or wrap the current logic in a useEffect that runs when
assets or selectedAsset.address change (guard: if !selectedAsset.address &&
assets[0]) and calls setSelectedAsset with the same parsed Address and external
boolean; ensure you import/useEffect and keep the parse(Address, ...) logic
unchanged.

In `@src/components/shared/PaymentScheduleSheet.tsx`:
- Line 70: The AssetLogo is being fed a defensive empty-string fallback
(AssetLogo symbol={symbol ?? ""}) even though the parent conditional guarantees
market and thus symbol exist; remove the unnecessary ?? "" fallback and pass
symbol directly (AssetLogo symbol={symbol}) so TypeScript can narrow symbol to
string within the conditional in PaymentScheduleSheet, or alternatively
explicitly assert/convert only if needed (e.g., ensure symbol is typed as string
in the block) to avoid rendering an empty placeholder.

In `@src/utils/lifi.ts`:
- Around line 118-124: The catch block that swallows failures from
getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B") should
log/report the error before returning the fallback tokens; change the anonymous
catch to catch (err) and call the project’s telemetry/logger (e.g., logger.error
or captureException) with context including chain.id and the EXA address, then
return tokens; apply the same change to the other getToken usage around the EXA
fetch at the 187-192 location.

In `@src/utils/queryClient.ts`:
- Around line 42-45: Replace the hardcoded EXA token address string passed to
getToken with a named constant (e.g., EXA_TOKEN_ADDRESS) defined near the top of
the module; update the Promise.all call to use EXA_TOKEN_ADDRESS instead of the
literal string and keep the same .catch fallback, ensuring references to
getToken(chain.id, EXA_TOKEN_ADDRESS) and the exa variable remain unchanged so
the intent is self-documenting.

In `@src/utils/usePortfolio.ts`:
- Around line 117-128: The comparator in the assets sorting (inside
usePortfolio's assets useMemo) can be unstable when both items resolve to "USDC"
because it returns -1 for aSymbol === "USDC" before checking bSymbol; update the
comparator to handle the tie explicitly by adding a tiebreaker when both aSymbol
and bSymbol are "USDC" (for example compare by usdValue, then by a.type vs
b.type or by symbol) so the sort is deterministic and stable.
- Line 130: The current isPending only returns isExternalPending from the token
balances query; update the hook to also include the pending state from
useReadPreviewerExactly (e.g., previewerPending or similar) and return a
combined flag (isPending = isExternalPending || previewerPending) so consumers
know if any portfolio data source is still loading; compute the combined boolean
before the final return and replace the existing isPending with this combined
value, referencing useReadPreviewerExactly and isExternalPending to locate the
sources to merge.
- Around line 90-100: The usdValue calculation in the useMemo for protocolAssets
currently divides BigInt values before Number conversion, losing fractional
precision; update the expression in the protocolAssets mapping (usdValue) to
convert the BigInt product from withdrawLimit(markets, market.market) *
market.usdPrice to a Number first and then divide by (10 ** market.decimals) and
by 1e18 to preserve decimals—adjust the arithmetic ordering in the usdValue
assignment inside the useMemo so it follows the pattern used elsewhere (e.g.,
AssetSelector) by performing Number(...) on the BigInt product before the
floating-point divisions.

Comment on lines +118 to +124
const tokens = response.tokens[chain.id] ?? [];
try {
const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B");
return [exa, ...tokens];
} catch {
return tokens;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid silent catch blocks for EXA token fetch failures.

Consider reporting the error before returning the fallback list so failures are visible in telemetry.

♻️ Suggested tweak
-  } catch {
-    return tokens;
-  }
+  } catch (error) {
+    reportError(error);
+    return tokens;
+  }
...
-  } catch {
-    return allowTokens;
-  }
+  } catch (error) {
+    reportError(error);
+    return allowTokens;
+  }

Also applies to: 187-192

🤖 Prompt for AI Agents
In `@src/utils/lifi.ts` around lines 118 - 124, The catch block that swallows
failures from getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B")
should log/report the error before returning the fallback tokens; change the
anonymous catch to catch (err) and call the project’s telemetry/logger (e.g.,
logger.error or captureException) with context including chain.id and the EXA
address, then return tokens; apply the same change to the other getToken usage
around the EXA fetch at the 187-192 location.

Comment on lines 42 to 45
const [{ tokens }, exa] = await Promise.all([
getTokens({ chainTypes: [ChainType.EVM] }),
getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch(() => undefined),
]);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Extract the hardcoded EXA token address to a named constant.

The EXA token address is a magic string. Per coding guidelines, avoid cryptic values and prefer self-documenting code.

♻️ Suggested refactor
+const EXA_TOKEN_ADDRESS = "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B";
+
 export const lifiTokensOptions = queryOptions({
   queryKey: ["lifi", "tokens"],
   staleTime: Infinity,
   gcTime: Infinity,
   enabled: !chain.testnet && chain.id !== anvil.id,
   queryFn: async () => {
     try {
       const [{ tokens }, exa] = await Promise.all([
         getTokens({ chainTypes: [ChainType.EVM] }),
-        getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch(() => undefined),
+        getToken(chain.id, EXA_TOKEN_ADDRESS).catch(() => undefined),
       ]);
📝 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 [{ tokens }, exa] = await Promise.all([
getTokens({ chainTypes: [ChainType.EVM] }),
getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch(() => undefined),
]);
const EXA_TOKEN_ADDRESS = "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B";
export const lifiTokensOptions = queryOptions({
queryKey: ["lifi", "tokens"],
staleTime: Infinity,
gcTime: Infinity,
enabled: !chain.testnet && chain.id !== anvil.id,
queryFn: async () => {
try {
const [{ tokens }, exa] = await Promise.all([
getTokens({ chainTypes: [ChainType.EVM] }),
getToken(chain.id, EXA_TOKEN_ADDRESS).catch(() => undefined),
]);
🤖 Prompt for AI Agents
In `@src/utils/queryClient.ts` around lines 42 - 45, Replace the hardcoded EXA
token address string passed to getToken with a named constant (e.g.,
EXA_TOKEN_ADDRESS) defined near the top of the module; update the Promise.all
call to use EXA_TOKEN_ADDRESS instead of the literal string and keep the same
.catch fallback, ensuring references to getToken(chain.id, EXA_TOKEN_ADDRESS)
and the exa variable remain unchanged so the intent is self-documenting.

Comment on lines +90 to +100
const protocolAssets = useMemo<ProtocolAsset[]>(() => {
if (!markets) return [];
return markets
.filter(({ floatingDepositAssets }) => floatingDepositAssets > 0n)
.map((market) => ({
...market,
usdValue:
Number((withdrawLimit(markets, market.market) * market.usdPrice) / BigInt(10 ** market.decimals)) / 1e18,
type: "protocol" as const,
}));
}, [markets]);
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find Market type definition and usdPrice field
rg -nA10 'interface Market|type Market' --type=ts --type=tsx | head -50

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Check withdrawLimit function definition
rg -nB2 -A8 'function withdrawLimit|const withdrawLimit' --type=ts --type=tsx | head -50

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Find other USD value calculations to see if there's a pattern
rg -nC3 'usdPrice.*decimals|usdValue' --type=ts --type=tsx | head -100

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Find Market type definition - search all files
rg -nA10 'interface Market|type Market' | head -60

Repository: exactly/exa

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Find withdrawLimit function
rg -nB2 -A8 'function withdrawLimit|const withdrawLimit' | head -60

Repository: exactly/exa

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Find Market.ts or similar type definition file
fd -e ts -e tsx | grep -i market | head -20

Repository: exactly/exa

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Check the exact file and surrounding context in usePortfolio.ts
cat -n src/utils/usePortfolio.ts | sed -n '85,105p'

Repository: exactly/exa

Length of output: 850


🏁 Script executed:

#!/bin/bash
# Search for Market type/interface definition - try broader search
rg 'Market' src/ -A 5 | grep -A 20 'interface Market\|type Market' | head -40

Repository: exactly/exa

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Look at imports at top of usePortfolio.ts to find Market source
head -30 src/utils/usePortfolio.ts

Repository: exactly/exa

Length of output: 905


🏁 Script executed:

#!/bin/bash
# Find withdrawLimit definition
rg 'withdrawLimit' src/ -B 3 -A 10 | head -80

Repository: exactly/exa

Length of output: 5117


🏁 Script executed:

#!/bin/bash
# Search for type definitions in likely files
fd Market -e ts | head -20

Repository: exactly/exa

Length of output: 37


Fix BigInt division order to preserve decimal precision in USD value calculation.

The usdValue calculation divides BigInt values before converting to Number, which truncates and loses fractional parts. Change line 97 from:

Number((withdrawLimit(markets, market.market) * market.usdPrice) / BigInt(10 ** market.decimals)) / 1e18

to:

Number(withdrawLimit(markets, market.market) * market.usdPrice) / (10 ** market.decimals) / 1e18

This matches the established pattern in the codebase (see AssetSelector.tsx) and preserves decimal precision for accurate USD calculations.

🤖 Prompt for AI Agents
In `@src/utils/usePortfolio.ts` around lines 90 - 100, The usdValue calculation in
the useMemo for protocolAssets currently divides BigInt values before Number
conversion, losing fractional precision; update the expression in the
protocolAssets mapping (usdValue) to convert the BigInt product from
withdrawLimit(markets, market.market) * market.usdPrice to a Number first and
then divide by (10 ** market.decimals) and by 1e18 to preserve decimals—adjust
the arithmetic ordering in the usdValue assignment inside the useMemo so it
follows the pattern used elsewhere (e.g., AssetSelector) by performing
Number(...) on the BigInt product before the floating-point divisions.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/swaps/SelectorModal.tsx (1)

10-48: Remove usePortfolio hook from TokenListItem and pass matchingAsset as a prop.

TokenListItem is a dumb UI component and should not call hooks. Every list item currently subscribes to the portfolio independently, creating redundant subscriptions. The parent TokenSelectModal already has assets and computes the matching asset—pass it down instead.

Refactor approach

Move the asset lookup logic to the parent component. Define a helper function to find the matching asset, then pass the result as a prop to TokenListItem:

+type PortfolioAsset = ReturnType<typeof usePortfolio>["assets"][number];

 function TokenListItem({
   token,
   isSelected,
   onPress,
   language,
+  matchingAsset,
 }: {
   isSelected: boolean;
   language: string;
   onPress: () => void;
   token: Token;
+  matchingAsset?: PortfolioAsset;
 }) {
-  const { assets } = usePortfolio();
-  const matchingAsset = assets.find(
-    (asset) =>
-      (asset.type === "protocol" && asset.asset === token.address) ||
-      (asset.type === "external" && asset.address === token.address),
-  );
   return (

In the parent TokenSelectModal, define the lookup function once and use it when rendering:

 const { assets } = usePortfolio();
+const findMatchingAsset = (token: Token) =>
+  assets.find(
+    (asset) =>
+      (asset.type === "protocol" && asset.asset === token.address) ||
+      (asset.type === "external" && asset.address === token.address),
+  );

 // In FlatList renderItem:
 <TokenListItem
   token={item}
   isSelected={selectedToken?.address === item.address}
+  matchingAsset={findMatchingAsset(item)}
   onPress={() => {
     onSelect(item);
     setSearchQuery("");
   }}
   language={language}
 />
🤖 Fix all issues with AI agents
In `@src/components/loans/LoanSummary.tsx`:
- Line 76: The AssetLogo is being passed an empty string when symbol is
undefined which yields a blank placeholder; update LoanSummary to avoid this by
either conditionally rendering AssetLogo only when symbol is defined (check
symbol before rendering the <AssetLogo ... /> element) or provide a meaningful
fallback prop (e.g., pass "??" or another placeholder instead of ""), ensuring
you update the AssetLogo invocation and any surrounding JSX in the LoanSummary
component so the UI shows a fallback or skeleton while market data loads.

In `@src/components/shared/AssetSelector.tsx`:
- Around line 35-39: The empty-state ("No available assets") can flash because
assets depends on markets but the code only checks usePortfolio's isPending;
update the conditional around assets length to also verify that markets have
finished loading (e.g., ensure markets is defined/non-null or markets.length is
determined from useReadPreviewerExactly) before showing the empty state.
Concretely, in AssetSelector adjust the if (assets.length === 0) branch to also
gate on markets (from useReadPreviewerExactly) or a markets-loading flag so you
only render the empty-state when markets are resolved in addition to isPending
being false.

In `@src/components/shared/ChainLogo.tsx`:
- Around line 22-33: The layout container in ChainLogo.tsx currently uses View;
replace it with the appropriate Tamagui stack (XStack or YStack) to follow
project layout conventions: import XStack/YStack, swap the <View ...> wrapper
around the <Text> with XStack or YStack, keep the visual props (width, height,
borderRadius, backgroundColor) and convert layout props to the stack equivalents
(center alignment/justify), and ensure the inner Text usage
({name.slice(0,2).toUpperCase()}) remains unchanged.

In `@src/components/shared/WeightedRate.tsx`:
- Around line 48-50: Replace the hardcoded spacing in the depositMarkets.map
render for XStack: change the marginRight prop on XStack (inside
depositMarkets.map) from the numeric conditional marginRight={index <
array.length - 1 ? -6 : 0} to use design tokens, e.g. marginRight={index <
array.length - 1 ? "$-2_5" : "$0"}, so XStack/AssetLogo follow the tokenized
spacing standard.

In `@src/utils/usePortfolio.ts`:
- Around line 15-38: Replace inconsistent plain string address typings with the
Hex type from `@exactly/common/validation`: import Hex and update
ProtocolAsset.market (currently `0x${string}`) as well as ProtocolAsset.asset
and ExternalAsset.address (currently string) to use Hex so all asset address
fields share the same strongly-typed address type; keep other fields and union
type PortfolioAsset unchanged.
- Around line 117-128: The sorting logic in the assets useMemo repeats the
"symbol.slice(3)" normalization; extract a helper (e.g., normalizeSymbol(symbol:
string, isProtocol: boolean)) and use it inside the assets comparator to produce
a normalized symbol for both protocol and external assets before the USDC check;
update references in useMemo (assets), anywhere else duplicating the slice
(e.g., Amount.tsx and other occurrences) to call normalizeSymbol to keep
behavior consistent and reduce duplication.
♻️ Duplicate comments (1)
src/components/pay-mode/Pay.tsx (1)

489-493: Move setSelectedAsset out of render to avoid re-render loops.

Calling a state setter during render can cause unstable re-renders and unexpected behavior. Move this to a useEffect driven by assets and selectedAsset.address.

🐛 Proposed fix
-  if (!selectedAsset.address && assets[0]) {
-    const { type } = assets[0];
-    setSelectedAsset({
-      address: type === "external" ? parse(Address, assets[0].address) : parse(Address, assets[0].market),
-      external: type === "external",
-    });
-  }
+  useEffect(() => {
+    if (!selectedAsset.address && assets[0]) {
+      const { type } = assets[0];
+      setSelectedAsset({
+        address: type === "external" ? parse(Address, assets[0].address) : parse(Address, assets[0].market),
+        external: type === "external",
+      });
+    }
+  }, [assets, selectedAsset.address]);

Comment on lines +48 to +50
{depositMarkets.map(({ market, symbol }, index, array) => (
<XStack key={market} marginRight={index < array.length - 1 ? -6 : 0} zIndex={array.length - index}>
<AssetLogo symbol={symbol} width={16} height={16} />
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

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file in question
fd "WeightedRate.tsx" --type f

Repository: exactly/exa

Length of output: 93


🏁 Script executed:

# Check the tamagui.config.ts for available spacing tokens
fd "tamagui.config.ts" --type f

Repository: exactly/exa

Length of output: 72


🏁 Script executed:

# Read the WeightedRate.tsx file to see the exact code at lines 48-50
cat -n src/components/shared/WeightedRate.tsx | sed -n '40,55p'

Repository: exactly/exa

Length of output: 700


🏁 Script executed:

# Read the tamagui.config.ts to check available spacing tokens
cat -n tamagui.config.ts

Repository: exactly/exa

Length of output: 29509


Use a design token instead of hardcoded spacing value.

marginRight={index < array.length - 1 ? -6 : 0} uses a hardcoded numeric value. Replace with a token reference like marginRight={index < array.length - 1 ? "$-2_5" : "$0"} to comply with the design token requirement.

🤖 Prompt for AI Agents
In `@src/components/shared/WeightedRate.tsx` around lines 48 - 50, Replace the
hardcoded spacing in the depositMarkets.map render for XStack: change the
marginRight prop on XStack (inside depositMarkets.map) from the numeric
conditional marginRight={index < array.length - 1 ? -6 : 0} to use design
tokens, e.g. marginRight={index < array.length - 1 ? "$-2_5" : "$0"}, so
XStack/AssetLogo follow the tokenized spacing standard.

Comment on lines 15 to 38
export type ProtocolAsset = {
asset: string;
assetName: string;
decimals: number;
floatingDepositAssets: bigint;
market: `0x${string}`;
symbol: string;
type: "protocol";
usdPrice: bigint;
usdValue: number;
};

export type ExternalAsset = {
address: string;
amount?: bigint;
decimals: number;
name: string;
priceUSD: string;
symbol: string;
type: "external";
usdValue: number;
};

export type PortfolioAsset = ExternalAsset | ProtocolAsset;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider consistent address typing across asset types.

ProtocolAsset.market uses the template literal type `0x${string}` while ProtocolAsset.asset and ExternalAsset.address use plain string. For consistency and type safety, consider using the Hex type from @exactly/common/validation for all address fields.

🤖 Prompt for AI Agents
In `@src/utils/usePortfolio.ts` around lines 15 - 38, Replace inconsistent plain
string address typings with the Hex type from `@exactly/common/validation`: import
Hex and update ProtocolAsset.market (currently `0x${string}`) as well as
ProtocolAsset.asset and ExternalAsset.address (currently string) to use Hex so
all asset address fields share the same strongly-typed address type; keep other
fields and union type PortfolioAsset unchanged.

Comment on lines 117 to 128
const assets = useMemo<PortfolioAsset[]>(() => {
const combined = [...protocolAssets, ...externalAssets];
return combined.sort((a, b) => {
if (options?.sortBy === "usdcFirst") {
const aSymbol = a.type === "protocol" ? a.symbol.slice(3) : a.symbol;
const bSymbol = b.type === "protocol" ? b.symbol.slice(3) : b.symbol;
if (aSymbol === "USDC") return -1;
if (bSymbol === "USDC") return 1;
}
return b.usdValue - a.usdValue;
});
}, [protocolAssets, externalAssets, options?.sortBy]);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting symbol normalization to reduce code duplication.

The symbol.slice(3) pattern for stripping the "exa" prefix appears multiple times in the codebase (lines 63, 121-122, and in Amount.tsx line 143). Consider extracting this to a helper function for consistency and maintainability.

♻️ Optional: Extract symbol normalization helper
// In a shared utils file
export function normalizeSymbol(symbol: string, isProtocol: boolean): string {
  const normalized = isProtocol ? symbol.slice(3) : symbol;
  return normalized === "WETH" ? "ETH" : normalized;
}

Then use in sorting:

-        const aSymbol = a.type === "protocol" ? a.symbol.slice(3) : a.symbol;
-        const bSymbol = b.type === "protocol" ? b.symbol.slice(3) : b.symbol;
+        const aSymbol = normalizeSymbol(a.symbol, a.type === "protocol");
+        const bSymbol = normalizeSymbol(b.symbol, b.type === "protocol");
🤖 Prompt for AI Agents
In `@src/utils/usePortfolio.ts` around lines 117 - 128, The sorting logic in the
assets useMemo repeats the "symbol.slice(3)" normalization; extract a helper
(e.g., normalizeSymbol(symbol: string, isProtocol: boolean)) and use it inside
the assets comparator to produce a normalized symbol for both protocol and
external assets before the USDC check; update references in useMemo (assets),
anywhere else duplicating the slice (e.g., Amount.tsx and other occurrences) to
call normalizeSymbol to keep behavior consistent and reduce duplication.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View issues and 11 additional flags in Devin Review.

Open in Devin Review

});
}, [protocolAssets, externalAssets, options?.sortBy]);

return { portfolio, averageRate, assets, protocolAssets, externalAssets, isPending: isExternalPending || isMarketsPending };

Choose a reason for hiding this comment

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

🟡 isPending always true on testnet/anvil causing infinite skeleton display

When running on testnet or anvil, the isPending value from usePortfolio is always true, causing UI components to show skeleton loaders indefinitely when the user has no protocol deposits.

Click to expand

Root Cause

The tokenBalancesOptions query has enabled: !!account && !chain.testnet && chain.id !== anvil.id (src/utils/queryClient.ts:62). On testnet/anvil, this is false, so the query never runs.

In TanStack Query v5, when a query is disabled and has no cached data, isPending is true (status is 'pending').

Impact

In usePortfolio.ts:130:

return { ..., isPending: isExternalPending || isMarketsPending };

isExternalPending is always true on testnet/anvil, making isPending always true.

In AssetSelector.tsx:38-45:

if (assets.length === 0) {
  if (isPending || !markets) {
    return <AssetSkeleton />; // Shows skeleton forever on testnet
  }
  return <Text>No available assets.</Text>;
}

When a user has no protocol deposits on testnet/anvil, they see an infinite loading skeleton instead of "No available assets."

Recommendation: Consider using isFetching instead of isPending for the external assets query, or handle the testnet/anvil case explicitly:

const { data: tokenBalances, isFetching: isExternalFetching } = useQuery(tokenBalancesOptions(resolvedAccount));
// ...
return { ..., isPending: isExternalFetching || isMarketsPending };
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/shared/Success.tsx (1)

104-118: Consider tightening the outer condition to require currency to be defined.

The guard on line 117 correctly prevents AssetLogo from receiving an undefined symbol. However, the outer condition on line 104 (currency !== "USDC") still evaluates to true when currency is undefined, causing the block to render with "with [amount]" but empty currency text and no logo.

If rendering this block only makes sense when a non-USDC currency is actually provided, consider adjusting the outer condition:

Proposed fix
-                {currency !== "USDC" && (
+                {currency && currency !== "USDC" && (
                   <XStack gap="$s2" alignItems="center">
                     ...
-                    {currency && <AssetLogo height={22} width={22} symbol={currency} />}
+                    <AssetLogo height={22} width={22} symbol={currency} />
                   </XStack>
                 )}

This makes the inner guard redundant (since currency is guaranteed to be defined) and prevents incomplete UI when no currency is provided.

src/components/home/AssetList.tsx (1)

139-146: Guard priceUSD before parseUnits to prevent runtime errors.

parseUnits expects a string; if priceUSD is missing or undefined from the LiFi SDK data, this will throw. A defensive guard is warranted here, as similar code in src/components/send-funds/Amount.tsx already uses this pattern.

🛠️ Proposed fix
-    usdPrice: parseUnits(priceUSD, 18),
+    usdPrice: parseUnits(priceUSD ?? "0", 18),
🤖 Fix all issues with AI agents
In `@src/components/add-funds/SupportedAssetsSheet.tsx`:
- Line 19: The filter callback in supportedAssets uses an abbreviated parameter
name `s`; rename it to a descriptive name like `symbol` for readability and
consistency (update the arrow function inside Object.keys(assetLogos).filter to
use `symbol` and adjust its references so the check excludes "USDC.e" and "DAI"
correctly). Ensure the variable `supportedAssets` and any downstream uses
continue to work with the renamed parameter.

In `@src/utils/queryClient.ts`:
- Around line 45-47: The catch callback for the getToken call uses an
implicitly-typed error parameter causing the lint rule; change the catch handler
to accept error: unknown, then narrow it before passing to reportError (e.g.,
coerce to Error safely or extract message) and return undefined as before.
Update the catch on the getToken(...) call that assigns to const exa and ensure
reportError is invoked with a proper Error or string derived from the unknown
(or wrap unknown in new Error(String(error))) so the code satisfies
`@typescript-eslint/use-unknown-in-catch-callback-variable`.
♻️ Duplicate comments (5)
src/utils/lifi.ts (2)

118-124: Do not swallow token fetch errors.

The EXA token fetch failure is silently ignored, which hides telemetry. Please report the error before falling back.

♻️ Suggested fix
-  } catch {
-    return tokens;
-  }
+  } catch (error) {
+    reportError(error);
+    return tokens;
+  }

187-192: Do not swallow allowlist token fetch errors.

Same concern as above; log the failure before returning the fallback list.

♻️ Suggested fix
-  } catch {
-    return allowTokens;
-  }
+  } catch (error) {
+    reportError(error);
+    return allowTokens;
+  }
src/utils/usePortfolio.ts (1)

90-98: Avoid BigInt division before Number conversion in usdValue.

Dividing BigInt before converting truncates decimals and under-reports value. Convert to Number first, then divide by decimals.

How does JavaScript BigInt division handle fractional results, and why does it truncate decimals?
🐛 Proposed fix
-        usdValue:
-          Number((withdrawLimit(markets, market.market) * market.usdPrice) / BigInt(10 ** market.decimals)) / 1e18,
+        usdValue:
+          Number(withdrawLimit(markets, market.market) * market.usdPrice) / (10 ** market.decimals) / 1e18,

Based on learnings, this preserves decimal precision for display.

src/components/shared/WeightedRate.tsx (1)

48-51: Replace hardcoded overlap spacing with a design token.

marginRight={index < array.length - 1 ? -6 : 0} should use a spacing token to comply with design-token-only styling.

♻️ Proposed fix
-            <XStack key={market} marginRight={index < array.length - 1 ? -6 : 0} zIndex={array.length - index}>
+            <XStack
+              key={market}
+              marginRight={index < array.length - 1 ? "$-2_5" : "$0"}
+              zIndex={array.length - index}
+            >

As per coding guidelines, use design tokens for spacing.

src/components/loans/LoanSummary.tsx (1)

76-76: Avoid rendering AssetLogo with an empty symbol.

Passing "" yields a blank fallback. Render only when symbol exists (or provide a real fallback).

♻️ Suggested fix
-            <AssetLogo height={16} symbol={symbol ?? ""} width={16} />
+            {symbol ? <AssetLogo height={16} symbol={symbol} width={16} /> : null}

Comment on lines 45 to 47
const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch((error) => {
reportError(error);
return undefined;
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

Fix lint error: use unknown in catch callback.

CI fails with @typescript-eslint/use-unknown-in-catch-callback-variable on this callback.

🐛 Proposed fix
-      const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch((error) => {
+      const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch((error: unknown) => {
         reportError(error);
         return undefined;
       });
📝 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 exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch((error) => {
reportError(error);
return undefined;
const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch((error: unknown) => {
reportError(error);
return undefined;
});
🧰 Tools
🪛 GitHub Actions: test

[error] 45-45: eslint: Prefer the safe 'unknown' in a catch callback variable. @typescript-eslint/use-unknown-in-catch-callback-variable. Step: 'nx run @exactly/mobile:"test:eslint"', command: 'eslint ./src --max-warnings=0'

🤖 Prompt for AI Agents
In `@src/utils/queryClient.ts` around lines 45 - 47, The catch callback for the
getToken call uses an implicitly-typed error parameter causing the lint rule;
change the catch handler to accept error: unknown, then narrow it before passing
to reportError (e.g., coerce to Error safely or extract message) and return
undefined as before. Update the catch on the getToken(...) call that assigns to
const exa and ensure reportError is invoked with a proper Error or string
derived from the unknown (or wrap unknown in new Error(String(error))) so the
code satisfies `@typescript-eslint/use-unknown-in-catch-callback-variable`.

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: 5

🤖 Fix all issues with AI agents
In `@src/components/add-funds/AddCrypto.tsx`:
- Around line 145-148: The XStack mapping over supportedAssets uses a hardcoded
negative marginRight (-12) which violates the token-only spacing rule; update
the XStack rendered in the supportedAssets.map (the XStack element that wraps
AssetLogo) to use a Tamagui space token instead of the literal -12 (e.g.,
replace the conditional marginRight value with the appropriate negative space
token from your theme/spacing tokens), ensuring the token is referenced directly
(not a computed pixel literal) and preserving the conditional logic for the last
item.

In `@src/components/add-funds/Bridge.tsx`:
- Around line 700-711: The View wrapping ChainLogo currently uses a hardcoded
borderColor="white"; replace that with the design token
borderColor="$borderNeutralSoft" to match the existing border usage pattern in
this component (see other Asset/Chain logo wrappers using $borderNeutralSoft) so
the View containing ChainLogo and the AssetLogo remain consistent with theme
tokens and not use $background which is empty.

In `@src/components/loans/AmountSelector.tsx`:
- Around line 84-90: selectedMarket can be undefined causing AssetLogo to
receive an empty string and render an empty badge; update AmountSelector to
guard or provide a clear placeholder: compute a symbol before rendering (use
selectedMarket?.symbol.slice(3), map "WETH" -> "ETH") and if selectedMarket is
undefined render a small skeleton/placeholder component or pass a meaningful
default symbol (e.g., "UNKNOWN" or "—") to the AssetLogo prop so AssetLogo never
receives an empty string; adjust the render around AssetLogo in AmountSelector
accordingly.

In `@src/components/shared/AssetLogo.tsx`:
- Around line 26-45: AssetLogo uses a computed numeric fontSize (fontSize={width
* 0.4}) which violates design-token rules; replace that numeric size with a
Tamagui token by mapping the incoming width to an appropriate token (e.g.,
small/medium/large tokens) and pass that token string into the Text component's
fontSize prop instead; update the Text in AssetLogo to use the chosen token (and
add a small helper or inline conditional mapping based on the width prop) so the
component uses token-based sizing while leaving getTokenLogoURI, useQuery, and
StyledImage unchanged.

In `@src/components/shared/CopyAddressSheet.tsx`:
- Line 18: The component CopyAddressSheet currently derives supportedAssets from
Object.keys(assetLogos) which creates an indirect dependency on assetLogos;
extract a dedicated constant array (e.g., SUPPORTED_ASSETS) listing the allowed
symbols (excluding "USDC.e" and "DAI") and replace the derived supportedAssets
with this constant; update any references to supportedAssets in CopyAddressSheet
to use SUPPORTED_ASSETS and remove the unused dependency on assetLogos in this
file (or keep assetLogos import only if still needed elsewhere).
♻️ Duplicate comments (7)
src/utils/lifi.ts (2)

119-124: Avoid silent catch blocks for EXA token fetch failures.

The catch block swallows failures from getToken() without reporting. Consider logging the error before returning the fallback to maintain visibility in telemetry.

♻️ Suggested fix
   try {
     const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B");
     return [exa, ...tokens];
-  } catch {
+  } catch (error) {
+    reportError(error);
     return tokens;
   }

187-192: Same silent catch pattern here.

Apply consistent error reporting for the EXA token fetch failure.

♻️ Suggested fix
   try {
     const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B");
     return [exa, ...allowTokens];
-  } catch {
+  } catch (error) {
+    reportError(error);
     return allowTokens;
   }
src/utils/queryClient.ts (1)

35-54: Extract the hardcoded EXA token address to a named constant.

The EXA token address appears multiple times in the codebase. Per coding guidelines, avoid magic strings and prefer self-documenting code.

♻️ Suggested refactor
+const EXA_TOKEN_ADDRESS = "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B";
+
 export const lifiTokensOptions = queryOptions({
   // ...
   queryFn: async () => {
     try {
       // ...
       if (chain.id !== optimism.id) return allTokens;
-      const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch((error: unknown) => {
+      const exa = await getToken(chain.id, EXA_TOKEN_ADDRESS).catch((error: unknown) => {
         reportError(error);
       });
       return exa ? [exa, ...allTokens] : allTokens;
     } catch (error) {
       // ...
     }
   },
 });

This constant could also be reused in src/utils/lifi.ts where the same address appears in getAllTokens and getAllowTokens.

src/utils/usePortfolio.ts (2)

94-98: Fix usdValue precision loss from BigInt division.

Line 97 divides in BigInt space before conversion, truncating decimals. Convert to Number first, then divide to preserve precision.

🐛 Proposed fix
-        usdValue:
-          Number((withdrawLimit(markets, market.market) * market.usdPrice) / BigInt(10 ** market.decimals)) / 1e18,
+        usdValue:
+          Number(withdrawLimit(markets, market.market) * market.usdPrice) / (10 ** market.decimals) / 1e18,

Based on learnings, prefer Number(bigintAmount) / 10 ** decimals to avoid truncation.


52-52: isPending can stay true when tokenBalances query is disabled.

On testnet/anvil the query is disabled, but TanStack Query reports isPending: true, so Line 136 can keep the UI in a perpetual loading state. Consider deriving pending from isFetching only when the query is enabled.

🧩 Suggested adjustment
-  const { data: tokenBalances, isPending: isExternalPending } = useQuery(tokenBalancesOptions(resolvedAccount));
+  const tokenBalancesQuery = tokenBalancesOptions(resolvedAccount);
+  const { data: tokenBalances, isFetching: isExternalFetching } = useQuery(tokenBalancesQuery);
+  const isExternalPending = tokenBalancesQuery.enabled ? isExternalFetching : false;

Also applies to: 136-136

src/components/shared/WeightedRate.tsx (1)

48-52: LGTM on symbol-based AssetLogo migration.

The mapping correctly passes symbol to AssetLogo for each deposit market.

Note: The hardcoded marginRight={-6} spacing value should use a design token instead (e.g., "$-2_5"), but this was already flagged in a previous review.

src/components/loans/LoanSummary.tsx (1)

76-76: Consider handling undefined symbol more gracefully.

When symbol is undefined, passing an empty string results in a blank placeholder. Since isMarketFetching is now tracked, this is less likely to occur, but conditional rendering would be safer.

Comment on lines +145 to +148
{supportedAssets.map((symbol, index) => (
<XStack key={symbol} marginRight={index < supportedAssets.length - 1 ? -12 : 0} zIndex={index}>
<AssetLogo symbol={symbol} width={32} height={32} />
</XStack>
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

Replace hardcoded negative spacing with a token-derived value.

marginRight={... ? -12 : 0} violates the token-only spacing rule. Use a Tamagui space token value instead.

🛠️ Suggested fix using Tamagui tokens
-import { ScrollView, XStack, YStack } from "tamagui";
+import { ScrollView, XStack, YStack, useToken } from "tamagui";
@@
 export default function AddCrypto() {
   const router = useRouter();
   const fontScale = PixelRatio.getFontScale();
+  const overlapSpacing = -useToken("space", "s3");
   const { address } = useAccount();
@@
-                {supportedAssets.map((symbol, index) => (
-                  <XStack key={symbol} marginRight={index < supportedAssets.length - 1 ? -12 : 0} zIndex={index}>
+                {supportedAssets.map((symbol, index) => (
+                  <XStack key={symbol} marginRight={index < supportedAssets.length - 1 ? overlapSpacing : 0} zIndex={index}>
                     <AssetLogo symbol={symbol} width={32} height={32} />
                   </XStack>
                 ))}

As per coding guidelines, use design tokens for spacing values.

🤖 Prompt for AI Agents
In `@src/components/add-funds/AddCrypto.tsx` around lines 145 - 148, The XStack
mapping over supportedAssets uses a hardcoded negative marginRight (-12) which
violates the token-only spacing rule; update the XStack rendered in the
supportedAssets.map (the XStack element that wraps AssetLogo) to use a Tamagui
space token instead of the literal -12 (e.g., replace the conditional
marginRight value with the appropriate negative space token from your
theme/spacing tokens), ensuring the token is referenced directly (not a computed
pixel literal) and preserving the conditional logic for the last item.

Comment on lines +700 to 711
<AssetLogo symbol={destinationToken.symbol} width={40} height={40} />
<View
position="absolute"
bottom={0}
right={0}
width={20}
height={20}
borderWidth={1}
borderColor="white"
borderRadius={10}
borderRadius="$r_0"
overflow="hidden"
>
<OptimismImage width="100%" height="100%" />
<ChainLogo size={20} />
</View>
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate the tamagui config file
find . -name "tamagui.config.*" -type f 2>/dev/null | head -20

Repository: exactly/exa

Length of output: 74


🏁 Script executed:

#!/bin/bash
# Get basic info about the Bridge.tsx file to understand context
wc -l src/components/add-funds/Bridge.tsx
head -30 src/components/add-funds/Bridge.tsx

Repository: exactly/exa

Length of output: 1174


🏁 Script executed:

#!/bin/bash
# Examine the tamagui config to see color tokens
cat tamagui.config.ts | head -200

Repository: exactly/exa

Length of output: 6719


🏁 Script executed:

#!/bin/bash
# Look at the specific lines in Bridge.tsx to understand context
sed -n '690,720p' src/components/add-funds/Bridge.tsx

Repository: exactly/exa

Length of output: 1731


🏁 Script executed:

#!/bin/bash
# Check if there are more color tokens defined, particularly looking for background or neutral tokens
grep -n "background\|neutral\|border" tamagui.config.ts | head -50

Repository: exactly/exa

Length of output: 2863


🏁 Script executed:

#!/bin/bash
# Get the complete color tokens section to see if there's a white or neutral color
sed -n '/color: {/,/},/p' tamagui.config.ts | tail -100

Repository: exactly/exa

Length of output: 3341


🏁 Script executed:

#!/bin/bash
# Also check if there's any design token documentation or if $background is actually defined
grep -n "\$background\|background:" tamagui.config.ts

Repository: exactly/exa

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Check the exact hex value for grayscaleLight1 to confirm it's white-ish
grep -n "grayscaleLight1:" tamagui.config.ts

Repository: exactly/exa

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Let's see the full tamagui config around line 262-268 to understand the theme tokens better
sed -n '260,275p' tamagui.config.ts

Repository: exactly/exa

Length of output: 927


🏁 Script executed:

#!/bin/bash
# Verify all available color tokens in the light theme
sed -n '260,275p' tamagui.config.ts

Repository: exactly/exa

Length of output: 927


🏁 Script executed:

#!/bin/bash
# Check for any other white-like or neutral border tokens
grep -n "uiNeutral\|borderNeutral" tamagui.config.ts | head -20

Repository: exactly/exa

Length of output: 1276


🏁 Script executed:

#!/bin/bash
# Search for other uses of borderColor in Bridge.tsx to see pattern
grep -n "borderColor" src/components/add-funds/Bridge.tsx

Repository: exactly/exa

Length of output: 374


🏁 Script executed:

#!/bin/bash
# Search for other badge/overlay patterns with borders in the codebase
rg "borderColor.*white|borderColor.*\$ui|borderColor.*\$background" --type=tsx -C2 | head -50

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Get more context about the light/dark theme tokens and what's available
sed -n '260,380p' tamagui.config.ts | grep -E "border|inverse"

Repository: exactly/exa

Length of output: 853


Hardcoded color violates design token guidelines; proposed fix is incorrect.

Line 706 uses borderColor="white" instead of a design token. However, the proposed fix using $background is incorrect—that token is defined as an empty string in the config. Looking at similar border usage patterns in this file (lines 614, 628, 672, 918), the appropriate token is $borderNeutralSoft, which maps to the light grayscale border color.

Proposed fix
                              <View
                                position="absolute"
                                bottom={0}
                                right={0}
                                borderWidth={1}
-                               borderColor="white"
+                               borderColor="$borderNeutralSoft"
                                borderRadius="$r_0"
                                overflow="hidden"
                              >
📝 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
<AssetLogo symbol={destinationToken.symbol} width={40} height={40} />
<View
position="absolute"
bottom={0}
right={0}
width={20}
height={20}
borderWidth={1}
borderColor="white"
borderRadius={10}
borderRadius="$r_0"
overflow="hidden"
>
<OptimismImage width="100%" height="100%" />
<ChainLogo size={20} />
</View>
<AssetLogo symbol={destinationToken.symbol} width={40} height={40} />
<View
position="absolute"
bottom={0}
right={0}
borderWidth={1}
borderColor="$borderNeutralSoft"
borderRadius="$r_0"
overflow="hidden"
>
<ChainLogo size={20} />
</View>
🤖 Prompt for AI Agents
In `@src/components/add-funds/Bridge.tsx` around lines 700 - 711, The View
wrapping ChainLogo currently uses a hardcoded borderColor="white"; replace that
with the design token borderColor="$borderNeutralSoft" to match the existing
border usage pattern in this component (see other Asset/Chain logo wrappers
using $borderNeutralSoft) so the View containing ChainLogo and the AssetLogo
remain consistent with theme tokens and not use $background which is empty.

Comment on lines 84 to 90
<AssetLogo
source={{
uri: assetLogos[
selectedMarket?.symbol.slice(3) === "WETH"
? "ETH"
: (selectedMarket?.symbol.slice(3) as keyof typeof assetLogos)
],
}}
symbol={
selectedMarket?.symbol.slice(3) === "WETH" ? "ETH" : (selectedMarket?.symbol.slice(3) ?? "")
}
width={32}
height={32}
/>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider handling the undefined market edge case for AssetLogo.

When selectedMarket is undefined, this passes an empty string to symbol, causing AssetLogo to render a fallback badge with empty initials. While this is a brief state during initial load, consider either showing a skeleton or a placeholder symbol.

♻️ Optional improvement
 <AssetLogo
-  symbol={
-    selectedMarket?.symbol.slice(3) === "WETH" ? "ETH" : (selectedMarket?.symbol.slice(3) ?? "")
-  }
+  symbol={
+    selectedMarket
+      ? selectedMarket.symbol.slice(3) === "WETH"
+        ? "ETH"
+        : selectedMarket.symbol.slice(3)
+      : "?"
+  }
   width={32}
   height={32}
 />
📝 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
<AssetLogo
source={{
uri: assetLogos[
selectedMarket?.symbol.slice(3) === "WETH"
? "ETH"
: (selectedMarket?.symbol.slice(3) as keyof typeof assetLogos)
],
}}
symbol={
selectedMarket?.symbol.slice(3) === "WETH" ? "ETH" : (selectedMarket?.symbol.slice(3) ?? "")
}
width={32}
height={32}
/>
<AssetLogo
symbol={
selectedMarket
? selectedMarket.symbol.slice(3) === "WETH"
? "ETH"
: selectedMarket.symbol.slice(3)
: "?"
}
width={32}
height={32}
/>
🤖 Prompt for AI Agents
In `@src/components/loans/AmountSelector.tsx` around lines 84 - 90, selectedMarket
can be undefined causing AssetLogo to receive an empty string and render an
empty badge; update AmountSelector to guard or provide a clear placeholder:
compute a symbol before rendering (use selectedMarket?.symbol.slice(3), map
"WETH" -> "ETH") and if selectedMarket is undefined render a small
skeleton/placeholder component or pass a meaningful default symbol (e.g.,
"UNKNOWN" or "—") to the AssetLogo prop so AssetLogo never receives an empty
string; adjust the render around AssetLogo in AmountSelector accordingly.

Comment on lines +26 to +45
export default function AssetLogo({ height, symbol, width }: { height: number; symbol: string; width: number }) {
const { data: tokens = [] } = useQuery(lifiTokensOptions);
const uri = getTokenLogoURI(tokens, symbol);
if (!uri) {
return (
<View
width={width}
height={height}
borderRadius="$r_0"
backgroundColor="$backgroundStrong"
alignItems="center"
justifyContent="center"
>
<Text fontSize={width * 0.4} fontWeight="bold" color="$uiNeutralSecondary">
{symbol.slice(0, 2).toUpperCase()}
</Text>
</View>
);
}
return <StyledImage source={{ uri }} width={width} height={height} />;
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

Use Tamagui font-size tokens instead of a computed numeric size.

fontSize={width * 0.4} introduces a hardcoded numeric style. Please switch to a token-based size (optionally mapped from width) to comply with the design-token rule.

🛠️ Suggested fix using token-based sizing
 export default function AssetLogo({ height, symbol, width }: { height: number; symbol: string; width: number }) {
+  const fontSize = width >= 40 ? "$7" : width >= 32 ? "$6" : width >= 24 ? "$5" : "$4";
   const { data: tokens = [] } = useQuery(lifiTokensOptions);
   const uri = getTokenLogoURI(tokens, symbol);
   if (!uri) {
     return (
       <View
         width={width}
         height={height}
         borderRadius="$r_0"
         backgroundColor="$backgroundStrong"
         alignItems="center"
         justifyContent="center"
       >
-        <Text fontSize={width * 0.4} fontWeight="bold" color="$uiNeutralSecondary">
+        <Text fontSize={fontSize} fontWeight="bold" color="$uiNeutralSecondary">
           {symbol.slice(0, 2).toUpperCase()}
         </Text>
       </View>
     );
   }
   return <StyledImage source={{ uri }} width={width} height={height} />;
 }

As per coding guidelines, keep font sizing on design tokens.

🤖 Prompt for AI Agents
In `@src/components/shared/AssetLogo.tsx` around lines 26 - 45, AssetLogo uses a
computed numeric fontSize (fontSize={width * 0.4}) which violates design-token
rules; replace that numeric size with a Tamagui token by mapping the incoming
width to an appropriate token (e.g., small/medium/large tokens) and pass that
token string into the Text component's fontSize prop instead; update the Text in
AssetLogo to use the chosen token (and add a small helper or inline conditional
mapping based on the width prop) so the component uses token-based sizing while
leaving getTokenLogoURI, useQuery, and StyledImage unchanged.

const supportedAssets = Object.entries(assetLogos)
.filter(([symbol]) => symbol !== "USDC.e" && symbol !== "DAI")
.map(([symbol, image]) => ({ symbol, image }));
const supportedAssets = Object.keys(assetLogos).filter((s) => s !== "USDC.e" && s !== "DAI");
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting supported assets to a constant array.

The assetLogos import is now only used to derive symbol keys, not for the actual URIs. This creates an indirect dependency where changing assetLogos affects supported assets even though the logo URIs are no longer consumed here.

Proposed refactor
-import assetLogos from "../../utils/assetLogos";
+// TODO consider moving supported assets to a shared constant
+const supportedAssets = ["USDC", "WETH", "wstETH", "OP", "WBTC", "cbBTC", "AERO"];
-
-const supportedAssets = Object.keys(assetLogos).filter((s) => s !== "USDC.e" && s !== "DAI");
📝 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 supportedAssets = Object.keys(assetLogos).filter((s) => s !== "USDC.e" && s !== "DAI");
// TODO consider moving supported assets to a shared constant
const supportedAssets = ["USDC", "WETH", "wstETH", "OP", "WBTC", "cbBTC", "AERO"];
🤖 Prompt for AI Agents
In `@src/components/shared/CopyAddressSheet.tsx` at line 18, The component
CopyAddressSheet currently derives supportedAssets from Object.keys(assetLogos)
which creates an indirect dependency on assetLogos; extract a dedicated constant
array (e.g., SUPPORTED_ASSETS) listing the allowed symbols (excluding "USDC.e"
and "DAI") and replace the derived supportedAssets with this constant; update
any references to supportedAssets in CopyAddressSheet to use SUPPORTED_ASSETS
and remove the unused dependency on assetLogos in this file (or keep assetLogos
import only if still needed elsewhere).

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.

2 participants