Conversation
🦋 Changeset detectedLatest commit: 0305718 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
WalkthroughReplaces URI-based asset logos with a symbol-driven AssetLogo and adds ChainLogo; migrates many data hooks from useAccountAssets → usePortfolio; defers LiFi SDK initialization to runtime via ensureConfig and exposes lifi query options; introduces native ETH send path and updates query persistence wiring to use persistOptions. Changes
Sequence Diagram(s)sequenceDiagram
participant App as App/_layout
participant QC as QueryClient
participant Lifi as src/utils/lifi.ts
participant LiFiSDK as LiFi SDK
participant UI as UI Components
App->>QC: Initialize PersistQueryClientProvider(persistOptions)
UI->>QC: useQuery(lifiTokensOptions / tokenBalancesOptions)
QC->>Lifi: invoke ensureConfig() when needed
Lifi->>LiFiSDK: createLifiConfig / set integrator/context
QC->>LiFiSDK: fetch tokens / chains / balances
LiFiSDK-->>QC: return tokens / chains / balances
QC-->>UI: provide token/logo/chain data (AssetLogo / ChainLogo)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
Summary of ChangesHello @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 Highlights
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
src/components/pay-mode/Pay.tsx
Outdated
| 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", | ||
| }); | ||
| } |
There was a problem hiding this comment.
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]);
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #681 +/- ##
==========================================
+ Coverage 58.70% 60.56% +1.85%
==========================================
Files 169 170 +1
Lines 5236 5315 +79
Branches 1461 1491 +30
==========================================
+ Hits 3074 3219 +145
+ Misses 2001 1935 -66
Partials 161 161
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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: LiftusePortfolioout ofTokenListItemand normalize address matching.
TokenListItemnow depends on app state (usePortfolio), which violates the “dumb component” boundary and can duplicate portfolio computation per row. You already haveassetsinTokenSelectModal, so pass the matching asset down. While refactoring, normalize address casing to avoid mismatches.As per coding guidelines, keep `src/components/**` UI components stateless and fed via props.♻️ 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} + /> + ); + }}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.
src/utils/queryClient.ts
Outdated
| const [{ tokens }, exa] = await Promise.all([ | ||
| getTokens({ chainTypes: [ChainType.EVM] }), | ||
| getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch(() => undefined), | ||
| ]); |
There was a problem hiding this comment.
🧹 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.
| 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.
| 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]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find Market type definition and usdPrice field
rg -nA10 'interface Market|type Market' --type=ts --type=tsx | head -50Repository: 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 -50Repository: 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 -100Repository: exactly/exa
Length of output: 82
🏁 Script executed:
#!/bin/bash
# Find Market type definition - search all files
rg -nA10 'interface Market|type Market' | head -60Repository: exactly/exa
Length of output: 37
🏁 Script executed:
#!/bin/bash
# Find withdrawLimit function
rg -nB2 -A8 'function withdrawLimit|const withdrawLimit' | head -60Repository: 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 -20Repository: 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 -40Repository: 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.tsRepository: exactly/exa
Length of output: 905
🏁 Script executed:
#!/bin/bash
# Find withdrawLimit definition
rg 'withdrawLimit' src/ -B 3 -A 10 | head -80Repository: exactly/exa
Length of output: 5117
🏁 Script executed:
#!/bin/bash
# Search for type definitions in likely files
fd Market -e ts | head -20Repository: 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.
There was a problem hiding this comment.
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: RemoveusePortfoliohook fromTokenListItemand passmatchingAssetas 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
TokenSelectModalalready 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: MovesetSelectedAssetout 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
useEffectdriven byassetsandselectedAsset.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]);
| {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} /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate and examine the file in question
fd "WeightedRate.tsx" --type fRepository: exactly/exa
Length of output: 93
🏁 Script executed:
# Check the tamagui.config.ts for available spacing tokens
fd "tamagui.config.ts" --type fRepository: 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.tsRepository: 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.
| 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; |
There was a problem hiding this comment.
🧹 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.
| 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]); |
There was a problem hiding this comment.
🧹 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.
src/utils/usePortfolio.ts
Outdated
| }); | ||
| }, [protocolAssets, externalAssets, options?.sortBy]); | ||
|
|
||
| return { portfolio, averageRate, assets, protocolAssets, externalAssets, isPending: isExternalPending || isMarketsPending }; |
There was a problem hiding this comment.
🟡 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 };Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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 requirecurrencyto be defined.The guard on line 117 correctly prevents
AssetLogofrom receiving an undefined symbol. However, the outer condition on line 104 (currency !== "USDC") still evaluates totruewhencurrencyisundefined, 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
currencyis guaranteed to be defined) and prevents incomplete UI when no currency is provided.src/components/home/AssetList.tsx (1)
139-146: GuardpriceUSDbefore parseUnits to prevent runtime errors.
parseUnitsexpects a string; ifpriceUSDis missing or undefined from the LiFi SDK data, this will throw. A defensive guard is warranted here, as similar code insrc/components/send-funds/Amount.tsxalready 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 whensymbolexists (or provide a real fallback).♻️ Suggested fix
- <AssetLogo height={16} symbol={symbol ?? ""} width={16} /> + {symbol ? <AssetLogo height={16} symbol={symbol} width={16} /> : null}
There was a problem hiding this comment.
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.tswhere the same address appears ingetAllTokensandgetAllowTokens.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 ** decimalsto avoid truncation.
52-52:isPendingcan 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 fromisFetchingonly 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
symboltoAssetLogofor 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
symbolisundefined, passing an empty string results in a blank placeholder. SinceisMarketFetchingis now tracked, this is less likely to occur, but conditional rendering would be safer.
| {supportedAssets.map((symbol, index) => ( | ||
| <XStack key={symbol} marginRight={index < supportedAssets.length - 1 ? -12 : 0} zIndex={index}> | ||
| <AssetLogo symbol={symbol} width={32} height={32} /> | ||
| </XStack> |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, locate the tamagui config file
find . -name "tamagui.config.*" -type f 2>/dev/null | head -20Repository: 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.tsxRepository: exactly/exa
Length of output: 1174
🏁 Script executed:
#!/bin/bash
# Examine the tamagui config to see color tokens
cat tamagui.config.ts | head -200Repository: 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.tsxRepository: 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 -50Repository: 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 -100Repository: 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.tsRepository: 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.tsRepository: 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.tsRepository: 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.tsRepository: 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 -20Repository: 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.tsxRepository: 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 -50Repository: 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.
| <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.
| <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} | ||
| /> |
There was a problem hiding this comment.
🧹 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.
| <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.
src/components/shared/AssetLogo.tsx
Outdated
| 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} />; |
There was a problem hiding this comment.
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.
src/utils/queryClient.ts
Outdated
| try { | ||
| const { tokens } = await getTokens({ chainTypes: [ChainType.EVM] }); | ||
| const allTokens = Object.values(tokens).flat(); | ||
| if (chain.id !== optimism.id) return allTokens; | ||
| const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch((error: unknown) => { | ||
| reportError(error); | ||
| }); | ||
| return exa ? [exa, ...allTokens] : allTokens; | ||
| } catch (error) { | ||
| reportError(error); | ||
| return [] as Token[]; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
src/utils/queryClient.ts
Outdated
| queryFn: async () => { | ||
| try { | ||
| const { tokens } = await getTokens({ chainTypes: [ChainType.EVM] }); | ||
| const allTokens = Object.values(tokens).flat(); | ||
| if (chain.id !== optimism.id) return allTokens; | ||
| const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch((error: unknown) => { | ||
| reportError(error); | ||
| }); | ||
| return exa ? [exa, ...allTokens] : allTokens; | ||
| } catch (error) { | ||
| reportError(error); | ||
| return [] as Token[]; | ||
| } | ||
| }, |
There was a problem hiding this comment.
🔴 LiFi SDK queries may execute before SDK configuration is initialized
The lifiTokensOptions and lifiChainsOptions queries in queryClient.ts directly call LiFi SDK functions (getTokens, getChains, getTokenBalancesByChain) without ensuring the SDK is configured first.
Click to expand
How the bug occurs
- When the app loads,
AssetLogocomponent renders and callsuseQuery(lifiTokensOptions)atsrc/components/shared/AssetLogo.tsx:27 - The query's
queryFnatsrc/utils/queryClient.ts:40-53directly callsgetTokens({ chainTypes: [ChainType.EVM] }) - The
ensureConfig()function insrc/utils/lifi.ts:30-44that sets up the SDK (API key, integrator, RPC URLs) is only called when specific functions likegetRoute,getBridgeSources, etc. are invoked - On production builds (
!chain.testnet && chain.id !== anvil.id), the queries run beforeensureConfig()is ever called
Impact
The LiFi SDK configuration includes:
- API key:
4bdb54aa-4f28-4c61-992a-a2fdc87b0a0b.251e33ad-ef5e-40cb-9b0f-52d634b99e8f - Integrator identification
- Custom RPC URLs with Alchemy API key
Without this configuration, API calls may be rate-limited or fail, causing missing token logos, chain logos, or failed balance fetches on app startup.
Recommendation: Call ensureConfig() at the start of the query functions, or move the LiFi SDK configuration initialization to run at module load time (similar to the original pattern in _layout.tsx). Alternatively, add ensureConfig() calls inside the query functions:
queryFn: async () => {
ensureConfig(); // Add this line
try {
const { tokens } = await getTokens({ chainTypes: [ChainType.EVM] });
// ...
}
}Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/send-funds/ReviewSheet.tsx (1)
70-74: Avoid passing an empty symbol to AssetLogo.
Whensymbolis undefined,symbol ?? ""produces a blank placeholder and an empty symbol next to the amount. Prefer a readable fallback (or makesymbolrequired).🛠️ Suggested fix
- <AssetLogo height={40} symbol={symbol ?? ""} width={40} /> + <AssetLogo height={40} symbol={symbol ?? "??"} width={40} /> ... - {amount} {symbol} + {amount} {symbol ?? "??"}
|
@coderabbitai review the latest force-pushed code |
|
✅ Actions performedReview triggered.
|
d8c3ce3 to
6595518
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@src/components/loans/Review.tsx`:
- Line 236: The AssetLogo is being rendered with symbol ?? "" which yields a
blank badge when symbol is missing; update all AssetLogo usages (component
AssetLogo) to avoid empty initials by either conditionally rendering AssetLogo
only when symbol is present or passing a visible placeholder (e.g., a default
char like "?" or a derived fallback such as the first letter of
assetName/market) so the badge is not blank; ensure you apply this change to
every occurrence noted (the AssetLogo instances at lines referenced) and keep
the prop name symbol as the source of truth for the fallback logic.
In `@src/components/pay-mode/Pay.tsx`:
- Line 733: The AssetLogo is being rendered with symbol possibly undefined which
passes an empty string and produces an empty placeholder; update the rendering
in Pay.tsx to guard against empty/undefined symbols by using a meaningful
fallback or conditional rendering: check the symbol value before passing it to
AssetLogo (refer to the AssetLogo component and the local variable symbol) and
either supply a non-empty fallback string (e.g., a default token ticker) or
skip/render an alternate element when symbol is falsy so AssetLogo never
receives an empty string.
- Line 747: In the Portfolio balance section in Pay.tsx the AssetLogo is being
given symbol ?? "" which passes an empty string when symbol is undefined; change
it to pass symbol only when present (e.g. pass symbol || undefined or omit the
symbol prop entirely when falsy) or conditionally render <AssetLogo> only if
symbol is defined so AssetLogo never receives an empty string; update the JSX
around AssetLogo and ensure the prop name remains symbol.
In `@src/components/send-funds/ReviewSheet.tsx`:
- Around line 69-71: ReviewSheet passes symbol ?? "" into AssetLogo which
renders a blank placeholder when symbol is missing; change the prop to supply a
visible fallback (e.g., symbol || "?" or a computed initial like (symbol?.[0] ??
"?" ) or undefined if AssetLogo handles its own fallback) so users don’t see an
empty box during loading. Update the AssetLogo prop usage in ReviewSheet.tsx
(the AssetLogo component call and the symbol variable) to pass a non-empty
fallback token or undefined to trigger AssetLogo’s built-in placeholder
behavior.
In `@src/utils/lifi.ts`:
- Around line 29-44: The ensureConfig function embeds a hardcoded LiFi API key
and risks concurrent calls to createLifiConfig; move the API key out of source
into an environment variable (read process.env.LIFI_API_KEY or similar) and
replace the literal in the createLifiConfig call, and prevent race conditions by
adding a single-flight guard around ensureConfig (e.g., a module-level promise
or mutex) so simultaneous callers await the same in-flight initialization
instead of invoking createLifiConfig multiple times; keep the existing
configured flag but set/resolve the in-flight promise appropriately inside
ensureConfig and reference createLifiConfig, configured, and ensureConfig when
applying the changes.
In `@src/utils/queryClient.ts`:
- Around line 70-92: The tokenBalancesOptions function includes a redundant
runtime guard inside queryFn ("if (!account) return []") because enabled:
!!account already prevents queryFn from running when account is undefined;
remove that guard and inside queryFn use the non-null account (e.g., account! or
otherwise narrow its type) when calling getTokenBalancesByChain so TypeScript is
satisfied, leaving the rest of the try/catch and data fetching logic in
tokenBalancesOptions/queryFn unchanged.
| let configured = false; | ||
| function ensureConfig() { | ||
| if (configured || chain.testnet || chain.id === anvil.id) return; | ||
| createLifiConfig({ | ||
| integrator: "exa_app", | ||
| apiKey: "4bdb54aa-4f28-4c61-992a-a2fdc87b0a0b.251e33ad-ef5e-40cb-9b0f-52d634b99e8f", | ||
| providers: [EVM({ getWalletClient: () => Promise.resolve(publicClient) })], | ||
| rpcUrls: { | ||
| [optimism.id]: [`${optimism.rpcUrls.alchemy?.http[0]}/${alchemyAPIKey}`], | ||
| [chain.id]: [publicClient.transport.alchemyRpcUrl], | ||
| }, | ||
| }); | ||
| configured = true; | ||
| queryClient.prefetchQuery(lifiTokensOptions).catch(reportError); | ||
| queryClient.prefetchQuery(lifiChainsOptions).catch(reportError); | ||
| } |
There was a problem hiding this comment.
Hardcoded API key should be moved to an environment variable.
The LiFi API key is exposed in source code, which was flagged by static analysis. Even if this is a non-secret public key, storing credentials in environment variables is a security best practice and allows rotation without code changes.
Additionally, there's a potential race condition: if multiple async functions call ensureConfig() concurrently before configured = true is set, createLifiConfig could be called multiple times.
♻️ Suggested approach
+import lifiApiKey from "@exactly/common/lifiApiKey"; // or use process.env
+
+let configPromise: Promise<void> | undefined;
+
-let configured = false;
-function ensureConfig() {
- if (configured || chain.testnet || chain.id === anvil.id) return;
- createLifiConfig({
- integrator: "exa_app",
- apiKey: "4bdb54aa-4f28-4c61-992a-a2fdc87b0a0b.251e33ad-ef5e-40cb-9b0f-52d634b99e8f",
+function ensureConfig() {
+ if (chain.testnet || chain.id === anvil.id) return Promise.resolve();
+ if (!configPromise) {
+ configPromise = Promise.resolve().then(() => {
+ createLifiConfig({
+ integrator: "exa_app",
+ apiKey: lifiApiKey,
providers: [EVM({ getWalletClient: () => Promise.resolve(publicClient) })],
rpcUrls: {
[optimism.id]: [`${optimism.rpcUrls.alchemy?.http[0]}/${alchemyAPIKey}`],
[chain.id]: [publicClient.transport.alchemyRpcUrl],
},
});
- configured = true;
- queryClient.prefetchQuery(lifiTokensOptions).catch(reportError);
- queryClient.prefetchQuery(lifiChainsOptions).catch(reportError);
+ queryClient.prefetchQuery(lifiTokensOptions).catch(reportError);
+ queryClient.prefetchQuery(lifiChainsOptions).catch(reportError);
+ });
+ }
+ return configPromise;
}🧰 Tools
🪛 Gitleaks (8.30.0)
[high] 34-34: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🤖 Prompt for AI Agents
In `@src/utils/lifi.ts` around lines 29 - 44, The ensureConfig function embeds a
hardcoded LiFi API key and risks concurrent calls to createLifiConfig; move the
API key out of source into an environment variable (read
process.env.LIFI_API_KEY or similar) and replace the literal in the
createLifiConfig call, and prevent race conditions by adding a single-flight
guard around ensureConfig (e.g., a module-level promise or mutex) so
simultaneous callers await the same in-flight initialization instead of invoking
createLifiConfig multiple times; keep the existing configured flag but
set/resolve the in-flight promise appropriately inside ensureConfig and
reference createLifiConfig, configured, and ensureConfig when applying the
changes.
There was a problem hiding this comment.
Actionable comments posted: 3
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: Guard the non‑USDC block against undefined currency.
Whencurrencyis undefined,currency !== "USDC"is true and the UI renders “undefined”. Tighten the condition to avoid that.🛠️ Proposed fix
- {currency !== "USDC" && ( + {currency && currency !== "USDC" && ( <XStack gap="$s2" alignItems="center"> <Text headline primary color="$uiNeutralPrimary"> {t("with")} </Text> <Text title2 primary color="$uiNeutralPrimary"> {amount.toLocaleString(language, { maximumFractionDigits: selectedAsset && selectedAsset === marketUSDCAddress ? 2 : 8, })} </Text> <Text title2 primary color="$uiNeutralPrimary"> {currency} </Text> - {currency && <AssetLogo height={22} width={22} symbol={currency} />} + <AssetLogo height={22} width={22} symbol={currency} /> </XStack> )}src/components/shared/Pending.tsx (1)
72-98: Guard the non‑USDC block against undefined currency.
currency !== "USDC"is true whencurrencyis undefined, which renders “undefined” in the UI. Tighten the condition.🛠️ Proposed fix
- {currency !== "USDC" && ( + {currency && currency !== "USDC" && ( <XStack gap="$s2" alignItems="center"> <Text headline primary color="$uiNeutralPrimary"> {t("with")} </Text> <Text title2 primary color="$uiNeutralPrimary"> {amount.toLocaleString(language, { maximumFractionDigits: selectedAsset && selectedAsset === marketUSDCAddress ? 2 : 8, })} </Text> <Text title2 primary color="$uiNeutralPrimary"> {currency} </Text> - {currency && <AssetLogo height={22} width={22} symbol={currency} />} + <AssetLogo height={22} width={22} symbol={currency} /> </XStack> )}
🤖 Fix all issues with AI agents
In `@src/components/shared/AssetSelector.tsx`:
- Around line 35-39: The component duplicates a call to useReadPreviewerExactly
to get markets even though usePortfolio already fetches the same data; update
the usePortfolio hook to return the markets value it obtains (add a markets
field to the hook's return type when invoked without extra args) and then remove
the separate useReadPreviewerExactly call from AssetSelector.tsx (replace uses
of local markets with the markets exposed from usePortfolio), ensuring the hook
still calls useReadPreviewerExactly({ address: previewerAddress, args: [account
?? zeroAddress] }) internally so consumers like AssetSelector can reuse it.
In `@src/components/shared/ChainLogo.tsx`:
- Around line 19-31: The ChainLogo fallback uses a numeric computed fontSize
(fontSize={size * 0.4}); change this to use Tamagui font tokens by mapping the
incoming size prop to an appropriate token and passing that token to the Text
component instead. Locate the ChainLogo component (render branch where
!data?.logoURI and the Text that renders {name.slice(0,2).toUpperCase()}) and
replace the numeric fontSize with a token lookup (e.g., derive a token via a
small helper or conditional map inside ChainLogo like getFontTokenForSize(size)
or a sizeToToken object) so that Text receives a string token (e.g., "$label" or
similar project tokens) instead of a raw number, preserving responsiveness and
design tokens.
In `@src/utils/assetLogos.ts`:
- Around line 11-16: The getTokenLogoURI function currently maps ETH→WETH for
lookup in tokens but does not map WETH→ETH for the static assetLogos fallback,
so when symbol is "WETH" and tokens are empty you miss the ETH logo; update
getTokenLogoURI to compute a resolvedSymbol (e.g., const resolvedSymbol = symbol
=== "WETH" ? "ETH" : symbol) and use that for the assetLogos fallback
(assetLogos[resolvedSymbol as keyof typeof assetLogos]) while keeping the
tokens.find lookup logic unchanged (still use search for token symbol matching).
| if (!data?.logoURI) { | ||
| const name = data?.name ?? chain.name; | ||
| return ( | ||
| <YStack | ||
| width={size} | ||
| height={size} | ||
| borderRadius="$r_0" | ||
| backgroundColor="$backgroundStrong" | ||
| alignItems="center" | ||
| justifyContent="center" | ||
| > | ||
| <Text fontSize={size * 0.4} fontWeight="bold" color="$uiNeutralSecondary"> | ||
| {name.slice(0, 2).toUpperCase()} |
There was a problem hiding this comment.
Use tokenized font size for the fallback initials.
fontSize={size * 0.4} hardcodes a computed numeric value; map size to a Tamagui font token instead.
♻️ Proposed refactor
export default function ChainLogo({ chainId, size }: { chainId?: number; size: number }) {
const targetChainId = chainId ?? chain.id;
+ const fontSize = size >= 40 ? "$7" : size >= 32 ? "$6" : size >= 24 ? "$5" : "$4";
const { data } = useQuery({
...lifiChainsOptions,
select: (chains) => chains.find((c) => c.id === targetChainId),
});
@@
- <Text fontSize={size * 0.4} fontWeight="bold" color="$uiNeutralSecondary">
+ <Text fontSize={fontSize} fontWeight="bold" color="$uiNeutralSecondary">
{name.slice(0, 2).toUpperCase()}
</Text>As per coding guidelines, keep font sizing on design tokens.
📝 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.
| if (!data?.logoURI) { | |
| const name = data?.name ?? chain.name; | |
| return ( | |
| <YStack | |
| width={size} | |
| height={size} | |
| borderRadius="$r_0" | |
| backgroundColor="$backgroundStrong" | |
| alignItems="center" | |
| justifyContent="center" | |
| > | |
| <Text fontSize={size * 0.4} fontWeight="bold" color="$uiNeutralSecondary"> | |
| {name.slice(0, 2).toUpperCase()} | |
| export default function ChainLogo({ chainId, size }: { chainId?: number; size: number }) { | |
| const targetChainId = chainId ?? chain.id; | |
| const fontSize = size >= 40 ? "$7" : size >= 32 ? "$6" : size >= 24 ? "$5" : "$4"; | |
| const { data } = useQuery({ | |
| ...lifiChainsOptions, | |
| select: (chains) => chains.find((c) => c.id === targetChainId), | |
| }); | |
| if (!data?.logoURI) { | |
| const name = data?.name ?? chain.name; | |
| return ( | |
| <YStack | |
| width={size} | |
| height={size} | |
| borderRadius="$r_0" | |
| backgroundColor="$backgroundStrong" | |
| alignItems="center" | |
| justifyContent="center" | |
| > | |
| <Text fontSize={fontSize} fontWeight="bold" color="$uiNeutralSecondary"> | |
| {name.slice(0, 2).toUpperCase()} | |
| </Text> | |
| </YStack> | |
| ); | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@src/components/shared/ChainLogo.tsx` around lines 19 - 31, The ChainLogo
fallback uses a numeric computed fontSize (fontSize={size * 0.4}); change this
to use Tamagui font tokens by mapping the incoming size prop to an appropriate
token and passing that token to the Text component instead. Locate the ChainLogo
component (render branch where !data?.logoURI and the Text that renders
{name.slice(0,2).toUpperCase()}) and replace the numeric fontSize with a token
lookup (e.g., derive a token via a small helper or conditional map inside
ChainLogo like getFontTokenForSize(size) or a sizeToToken object) so that Text
receives a string token (e.g., "$label" or similar project tokens) instead of a
raw number, preserving responsiveness and design tokens.
| const invalidReceiver = !receiver || receiver === zeroAddress; | ||
| const invalidAsset = !withdrawAsset || withdrawAsset === zeroAddress; | ||
| const invalidAsset = !withdrawAsset; |
There was a problem hiding this comment.
🟡 send-funds amount screen treats zeroAddress asset as valid even when it is not a resolvable market or external token
Amount no longer flags 0x000...000 as an invalid asset (invalidAsset = !withdrawAsset), so the screen can proceed with an asset that resolves to neither a protocol market nor an external token balance.
Actual behavior: when asset param is the zero address but useAsset() cannot resolve it to market or externalAsset, the UI renders the amount-entry flow but available is 0n and sendReady stays false, effectively trapping the user on a non-functional screen (validation fails: “Amount cannot be greater than available”).
Expected behavior: either (a) treat zeroAddress as invalid unless it maps to a known native/external token (for ETH transfers), or (b) explicitly verify that the asset is resolvable before allowing the flow.
Click to expand
- Asset validity check now only checks parse success:
const invalidAsset = !withdrawAsset;src/components/send-funds/Amount.tsx:209-210
- But
useAsset(withdrawAsset ?? zeroAddress)will returnmarket === undefinedandexternal === nullwhen the address doesn’t match a market or any token balance, leavingavailable = 0n. sendReadyrequires either aproposeSimulation(market withdraw) or an external transfer simulation / native transfer. When neither exists, the send button can never be enabled.
Recommendation: Gate the flow on resolvability instead of parse success. For example, treat asset as valid if market is truthy OR external is truthy OR (native transfer) external?.address parses and equals zeroAddress. Otherwise show the “Invalid asset address” screen.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/components/send-funds/Amount.tsx`:
- Around line 209-210: The asset validation currently only checks presence
(invalidAsset) but not the zero-address; update the invalidAsset logic to mirror
invalidReceiver by checking both that withdrawAsset exists and is not the
zeroAddress (i.e., change the invalidAsset condition to include withdrawAsset
=== zeroAddress) so that withdrawAsset, invalidAsset, invalidReceiver and
zeroAddress are used consistently in Amount.tsx.
In `@src/components/shared/AssetLogo.tsx`:
- Around line 29-31: The comparison token.chainId === chain.id should normalize
types like other comparisons in the codebase; update the filter in AssetLogo.tsx
(where tokens, chainTokens, chain.id, getTokenLogoURI, lifiTokensOptions are
used) to cast chain.id to a number (e.g. Number(chain.id) or +chain.id) before
comparing so the expression becomes token.chainId === Number(chain.id), matching
the pattern in src/utils/lifi.ts and satisfying
`@typescript-eslint/no-unsafe-enum-comparison`.
| const invalidReceiver = !receiver || receiver === zeroAddress; | ||
| const invalidAsset = !withdrawAsset || withdrawAsset === zeroAddress; | ||
| const invalidAsset = !withdrawAsset; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/components/send-funds/Amount.tsx | head -220 | tail -30Repository: exactly/exa
Length of output: 1533
🏁 Script executed:
wc -l src/components/send-funds/Amount.tsxRepository: exactly/exa
Length of output: 95
🏁 Script executed:
rg -n "invalidAsset\|withdrawAsset\|zeroAddress" src/components/send-funds/Amount.tsx -B 3 -A 3Repository: exactly/exa
Length of output: 37
🏁 Script executed:
rg -n "const invalidReceiver\|const invalidAsset" src/components/send-funds/Amount.tsx -B 5 -A 10Repository: exactly/exa
Length of output: 37
🏁 Script executed:
rg -n "withdrawAsset" src/components/send-funds/Amount.tsx -B 2 -A 2Repository: exactly/exa
Length of output: 1184
🏁 Script executed:
head -100 src/components/send-funds/Amount.tsxRepository: exactly/exa
Length of output: 4371
🏁 Script executed:
sed -n '1,150p' src/components/send-funds/Amount.tsxRepository: exactly/exa
Length of output: 6141
🏁 Script executed:
find . -path "*/common/validation*" -type f | head -20Repository: exactly/exa
Length of output: 77
🏁 Script executed:
rg -n "export.*Address" --type ts --type tsx | grep -i validation | head -20Repository: exactly/exa
Length of output: 82
🏁 Script executed:
cat ./common/validation.tsRepository: exactly/exa
Length of output: 1398
🏁 Script executed:
grep -n "Address" ./common/validation.ts | head -30Repository: exactly/exa
Length of output: 438
Add zero-address validation to invalidAsset check to match invalidReceiver.
The invalidAsset check only validates presence (!withdrawAsset), but invalidReceiver explicitly validates both presence and zero-address (!receiver || receiver === zeroAddress). Since the Address schema from valibot accepts the zero address as a valid Ethereum address format, a user navigating with asset=0x0000...0000 in the URL will pass validation and the component will proceed instead of showing the error state. Update line 210 to:
const invalidAsset = !withdrawAsset || withdrawAsset === zeroAddress;🤖 Prompt for AI Agents
In `@src/components/send-funds/Amount.tsx` around lines 209 - 210, The asset
validation currently only checks presence (invalidAsset) but not the
zero-address; update the invalidAsset logic to mirror invalidReceiver by
checking both that withdrawAsset exists and is not the zeroAddress (i.e., change
the invalidAsset condition to include withdrawAsset === zeroAddress) so that
withdrawAsset, invalidAsset, invalidReceiver and zeroAddress are used
consistently in Amount.tsx.
| const { data: tokens = [] } = useQuery(lifiTokensOptions); | ||
| const chainTokens = tokens.filter((token) => token.chainId === chain.id); | ||
| const uri = symbol ? getTokenLogoURI(chainTokens, symbol) : undefined; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/components/shared/AssetLogo.tsxRepository: exactly/exa
Length of output: 1931
🏁 Script executed:
rg -n "chainId|chain\.id" src/components/shared/AssetLogo.tsx -A 2 -B 2Repository: exactly/exa
Length of output: 408
🏁 Script executed:
# Check the types of chainId in the codebase to understand if it's an enum
fd -e ts -e tsx | xargs rg "type.*chainId|enum.*[Cc]hain|interface.*[Cc]hain" | head -20Repository: exactly/exa
Length of output: 37
🏁 Script executed:
# Find the chain module definition
fd -e ts -e tsx -e js "chain" src | grep -v node_modules | head -20Repository: exactly/exa
Length of output: 90
🏁 Script executed:
# Check lifiTokensOptions definition
rg -n "lifiTokensOptions" src/utils/lifi.ts -A 5 -B 2Repository: exactly/exa
Length of output: 1086
🏁 Script executed:
# Look for token type definitions in lifi
rg -n "type.*[Tt]oken|interface.*[Tt]oken" src/utils/lifi.ts -A 3Repository: exactly/exa
Length of output: 210
🏁 Script executed:
# Get more context around the Token type and chain definition
rg -n "type Token|import.*Token" src/utils/lifi.ts -A 2 -B 2Repository: exactly/exa
Length of output: 219
🏁 Script executed:
# Check the chain module to see what type chain.id is
rg -n "export.*chain|const chain|interface.*chain" --type ts --type tsx | grep -i "generated/chain\|chain\.id" | head -10Repository: exactly/exa
Length of output: 82
🏁 Script executed:
# Look at line 78 in lifi.ts to see the pattern used there
sed -n '75,82p' src/utils/lifi.tsRepository: exactly/exa
Length of output: 566
🏁 Script executed:
# Check the exact type definition from `@lifi/sdk` to understand Token.chainId
rg -n "Token.*type\|Token.*interface" --type-list | head -5Repository: exactly/exa
Length of output: 444
🏁 Script executed:
# Let's see if there's a local type definition or if it's just re-exported
rg -n "chainId" src/utils/lifi.ts -B 3 -A 1Repository: exactly/exa
Length of output: 2142
🏁 Script executed:
# Check what chain object is and its id property type
rg -n "^import.*chain\|^const chain\|^export.*chain" src -A 3 | grep -A 3 "@exactly/common/generated/chain"Repository: exactly/exa
Length of output: 37
🏁 Script executed:
# Look for any TypeScript error or ESLint configuration related to unsafe-enum-comparison
rg -n "unsafe-enum-comparison" src --type ts --type tsxRepository: exactly/exa
Length of output: 82
🏁 Script executed:
# Check the chain import - it's from `@exactly/common/generated/chain`
# Try to find any type info about chain.id
rg -n "chain\." src/components/shared/AssetLogo.tsxRepository: exactly/exa
Length of output: 133
🏁 Script executed:
# Compare: line 78 of lifi.ts uses a type cast, let's see the exact pattern
sed -n '78p' src/utils/lifi.tsRepository: exactly/exa
Length of output: 146
🏁 Script executed:
# Check if there's a type definition or enum for chain or chainId
fd "chain" --type f | grep -E "(types|generated)" | head -10Repository: exactly/exa
Length of output: 37
🏁 Script executed:
# Try to understand the Token type from `@lifi/sdk`
node -e "console.log(require('@lifi/sdk'))" 2>&1 | head -50 || echo "SDK not installed"Repository: exactly/exa
Length of output: 891
🏁 Script executed:
# Check if there are ESLint rules configured that would catch this
fd "eslint" --type f | grep -E "(config|rc)" | head -5Repository: exactly/exa
Length of output: 182
🏁 Script executed:
# Look for any .eslintrc or eslint config
cat .eslintrc* 2>/dev/null || cat eslint.config.* 2>/dev/null || echo "No explicit config found"Repository: exactly/exa
Length of output: 1030
🏁 Script executed:
# Check package.json for ESLint configuration or the `@typescript-eslint/no-unsafe-enum-comparison` rule
rg -n "no-unsafe-enum-comparison|@typescript-eslint" package.jsonRepository: exactly/exa
Length of output: 37
🏁 Script executed:
# Check the base config from `@exactly/common`
cat common/eslint/base.mjs 2>/dev/null | head -100Repository: exactly/exa
Length of output: 4854
🏁 Script executed:
# Look for `@typescript-eslint` configuration
rg -n "no-unsafe-enum-comparison" common/ -rRepository: exactly/exa
Length of output: 468
🏁 Script executed:
# Check type definitions - maybe in tsconfig or type files
fd "types" -type d src | head -5Repository: exactly/exa
Length of output: 225
🏁 Script executed:
# Look for the chain type definition more carefully
rg -n "from.*@exactly/common/generated/chain" src -A 2 -B 2Repository: exactly/exa
Length of output: 22076
🏁 Script executed:
# Search for any explicit no-unsafe-enum-comparison rule in eslint configs
rg -n "no-unsafe-enum-comparison" . --type ts --type jsRepository: exactly/exa
Length of output: 37
🏁 Script executed:
# Check if strictTypeChecked includes this rule by searching TypeScript-ESLint docs or checking common includes
rg -n "strictTypeChecked\|stylisticTypeChecked" common/eslintRepository: exactly/exa
Length of output: 37
🏁 Script executed:
# Look for other instances of chainId comparisons to see if there's a pattern
rg -n "chainId\s*===" src -A 1 -B 1 | head -30Repository: exactly/exa
Length of output: 381
🏁 Script executed:
# Check if there are more chainId comparisons and see which pattern is used
rg -n "chainId.*as number|chainId\s*===|Number(.*chainId)" src -B 1 -A 1 | head -50Repository: exactly/exa
Length of output: 1192
🏁 Script executed:
# Look at how the team handles this issue - check all chainId type casting patterns
rg -n "\(.*chainId.*as\|Number.*chainId" src -B 2 -A 2Repository: exactly/exa
Length of output: 37
🏁 Script executed:
# Check the exact line 42 to see the fontSize usage
sed -n '40,45p' src/components/shared/AssetLogo.tsxRepository: exactly/exa
Length of output: 268
🏁 Script executed:
# See what tamagui design tokens are available for font sizes
rg -n "fontSize|fontSizes" src/tamagui.config.ts -A 5 -B 2 | head -40Repository: exactly/exa
Length of output: 116
🏁 Script executed:
# Check if there's a pattern in the codebase for how fontSize should be specified
rg -n "fontSize=\\\"\$" src/components -l | head -5Repository: exactly/exa
Length of output: 142
🏁 Script executed:
# Find the tamagui config file
fd tamagui.configRepository: exactly/exa
Length of output: 72
🏁 Script executed:
# Look for how other components use fontSize with design tokens
rg -n "fontSize=" src/components -A 1 | head -30Repository: exactly/exa
Length of output: 1955
🏁 Script executed:
# Check the import statements and see if there's a design tokens config
rg -n "tamagui" src/components/shared/AssetLogo.tsxRepository: exactly/exa
Length of output: 96
🏁 Script executed:
# Check if strictTypeChecked enables the unsafe-enum-comparison rule
# First, let's look at what rules are included in the base config more completely
cat common/eslint/base.mjs | grep -A 200 "rules:"Repository: exactly/exa
Length of output: 5130
🏁 Script executed:
# Search for any CI/lint configuration that might show if this is a real pipeline failure
fd "github\|gitlab\|azure\|circleci" -type f | head -10Repository: exactly/exa
Length of output: 225
🏁 Script executed:
# Let's check if there are any TypeScript enum definitions for chain or chainId
rg -n "enum.*[Cc]hain|type.*ChainId" --type ts --type tsx common/ 2>/dev/null | head -20Repository: exactly/exa
Length of output: 37
🏁 Script executed:
# Check strictTypeChecked to understand what rules it includes
rg -n "strictTypeChecked" . --type js -A 2 -B 2Repository: exactly/exa
Length of output: 37
🏁 Script executed:
# Verify the Token type definition from `@lifi/sdk` to understand chainId property type
# Let's check the package.json to see the version
rg -n "@lifi/sdk" package.jsonRepository: exactly/exa
Length of output: 159
🏁 Script executed:
# Check the TypeScript-ESLint strictTypeChecked configuration to confirm no-unsafe-enum-comparison is enabled
# Look at what configs are imported
head -20 common/eslint/base.mjs | grep -E "import|ts\."Repository: exactly/exa
Length of output: 827
🏁 Script executed:
# Search web documentation or check if this specific rule would apply
# Let's verify by looking at how Token is used elsewhere - what's the actual type?
rg -n "Token\[" src/utils/lifi.ts -B 3 -A 3 | head -20Repository: exactly/exa
Length of output: 660
🏁 Script executed:
# One more check - see if there are any build/lint scripts that fail with this issue
fd "Makefile\|package.json" -type f | xargs rg -l "lint\|eslint" | head -3Repository: exactly/exa
Length of output: 225
Match chainId comparison pattern used elsewhere in the codebase to pass @typescript-eslint/no-unsafe-enum-comparison.
Line 30 is missing type normalization that similar comparisons have in src/utils/lifi.ts (line 78). Apply the cast to align with the pattern:
🛠️ Proposed fix
- const chainTokens = tokens.filter((token) => token.chainId === chain.id);
+ const chainTokens = tokens.filter((token) => (token.chainId as number) === chain.id);📝 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.
| const { data: tokens = [] } = useQuery(lifiTokensOptions); | |
| const chainTokens = tokens.filter((token) => token.chainId === chain.id); | |
| const uri = symbol ? getTokenLogoURI(chainTokens, symbol) : undefined; | |
| const { data: tokens = [] } = useQuery(lifiTokensOptions); | |
| const chainTokens = tokens.filter((token) => (token.chainId as number) === chain.id); | |
| const uri = symbol ? getTokenLogoURI(chainTokens, symbol) : undefined; |
🧰 Tools
🪛 GitHub Actions: test
[error] 30-30: ESLint: The two values in this comparison do not have a shared enum type (@typescript-eslint/no-unsafe-enum-comparison).
🤖 Prompt for AI Agents
In `@src/components/shared/AssetLogo.tsx` around lines 29 - 31, The comparison
token.chainId === chain.id should normalize types like other comparisons in the
codebase; update the filter in AssetLogo.tsx (where tokens, chainTokens,
chain.id, getTokenLogoURI, lifiTokensOptions are used) to cast chain.id to a
number (e.g. Number(chain.id) or +chain.id) before comparing so the expression
becomes token.chainId === Number(chain.id), matching the pattern in
src/utils/lifi.ts and satisfying `@typescript-eslint/no-unsafe-enum-comparison`.
There was a problem hiding this comment.
🔴 Pay component can return undefined when maturity is missing
Pay early-returns with return; when maturity is falsy (src/components/pay-mode/Pay.tsx:526). In React, returning undefined from a component render triggers a runtime error (“Nothing was returned from render”).
Actual vs expected:
- Actual:
if (!maturity) return; - Expected: return
null(or render an error state / navigate away).
Impact:
- Navigating to the Pay screen without a valid
maturityparam can crash the screen.
Click to expand
Relevant code:
if (!maturity) return;(src/components/pay-mode/Pay.tsx:526)
(Refers to line 526)
Recommendation: Change to if (!maturity) return null; (or render a fallback UI / route away).
Was this helpful? React with 👍 or 👎 to provide feedback.
src/utils/usePortfolio.ts
Outdated
| const externalAssets = useMemo<ExternalAsset[]>(() => { | ||
| const balances = tokenBalances ?? []; | ||
| if (balances.length === 0) return []; | ||
|
|
||
| return balances.map((token) => ({ | ||
| ...token, | ||
| usdValue: (Number(token.priceUSD) * Number(token.amount ?? 0n)) / 10 ** token.decimals, | ||
| type: "external" as const, | ||
| })); | ||
| }, [tokenBalances]); |
There was a problem hiding this comment.
🔴 usePortfolio no longer filters external assets that overlap protocol markets (double counting + duplicate UI entries)
useAccountAssets previously filtered LiFi balances to exclude tokens that correspond to protocol markets, preventing duplicates and double-counting. The new usePortfolio implementation no longer performs this filter, so the same underlying asset can appear both as a protocol asset and as an external asset.
Actual vs expected:
- Actual:
externalAssetsis built from alltokenBalanceswith no exclusion (usePortfolio.ts:103-112). - Expected: External assets should exclude tokens that map to protocol markets (as before), or there should be a deterministic de-duplication rule.
Impact:
- Portfolio totals can be inflated because
totalBalanceUSDsums both protocol and external USD values (usePortfolio.ts:127-130). - Asset selectors/lists can show the same asset twice (one
protocol, oneexternal), changing behavior vs the prior hook.
Click to expand
Previous behavior (from removed hook) filtered external balances:
return balances.filter(({ address }) => markets && !markets.some(({ market }) => address.toLowerCase() === market.toLowerCase()))
New behavior does not filter at all:
return balances.map((token) => ({ ...token, ... }))(src/utils/usePortfolio.ts:103-112)
Recommendation: When building externalAssets, filter out tokens that overlap protocol markets (e.g., by comparing token.address to each market.asset and/or market.market, depending on what LiFi returns). Then totalBalanceUSD and asset lists will not double count.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/components/swaps/TokenInput.tsx (1)
174-179: Inline styles should use Tamagui's styled system props.The
styleprop with object literals violates the coding guidelines. All styling should go through Tamagui's styled system.♻️ Suggested refactor
Consider using Tamagui props or creating a styled Input variant:
<Input value={value} onChangeText={handleAmountChange} onFocus={onFocus} disabled={disabled} cursor={disabled ? undefined : "pointer"} placeholder={token ? formatUnits(amount, token.decimals) : String(amount)} color={ isDanger ? "$uiErrorSecondary" : isActive ? "$uiNeutralPrimary" : "$uiNeutralPlaceholder" } - style={{ - fontFamily: "BDOGrotesk-Regular", - fontSize: 28, - fontWeight: "bold", - letterSpacing: -0.2, - }} + fontFamily="$body" + fontSize={28} + fontWeight="bold" + letterSpacing={-0.2} textAlign="left" inputMode="decimal" borderColor="transparent" numberOfLines={1} flex={1} width="100%" />Note: Verify that
$bodyor appropriate font token exists in your Tamagui config.As per coding guidelines: "Do not use inline styles with the React Native
styleprop using object literals. All styling must go through Tamagui's styled system props."src/components/loans/AmountSelector.tsx (1)
106-106: Inline style object violates coding guidelines.The
styleprop with object literal should be replaced with Tamagui styled system props.♻️ Suggested refactor
<Input height="auto" inputMode="decimal" onChangeText={handleAmountChange} placeholder="0" onFocus={() => { setFocused(true); }} onBlur={() => { setFocused(false); }} value={value} color={highAmount ? "$interactiveBaseErrorDefault" : "$uiNeutralPrimary"} alignSelf="center" borderWidth={0} - style={{ fontSize: 34, fontWeight: 400, letterSpacing: -0.2 }} + fontSize={34} + fontWeight="400" + letterSpacing={-0.2} cursor="pointer" textAlign="center" backgroundColor="$backgroundSoft" borderBottomLeftRadius={0} borderBottomRightRadius={0} flex={1} />As per coding guidelines: "Do not use inline styles with the React Native
styleprop using object literals."src/components/pay-mode/Pay.tsx (1)
509-510: Consider guarding against undefinedsymbolin currency display contexts.
symbolcan beundefinedwhen bothrepayMarketandexternalAssetare not yet loaded. WhileAssetLogohandlesundefinedgracefully with a "—" fallback, the text displays at lines 735 and 820 will renderundefinedas empty or show inconsistent UI.♻️ Suggested defensive fallback
const symbol = - repayMarket?.symbol.slice(3) === "WETH" ? "ETH" : (repayMarket?.symbol.slice(3) ?? externalAsset?.symbol); + repayMarket?.symbol.slice(3) === "WETH" ? "ETH" : (repayMarket?.symbol.slice(3) ?? externalAsset?.symbol ?? "");Or conditionally render the "Pay with" section only when
symbolis defined.
🤖 Fix all issues with AI agents
In `@src/components/shared/CopyAddressSheet.tsx`:
- Around line 78-82: The XStack inside the supportedAssets.map is using a
hardcoded overlap marginRight={-12}; replace that hardcoded value with the
appropriate spacing token from tamagui.config.ts (import the token and use it in
the marginRight prop for the XStack rendered in the supportedAssets map) so
AssetLogo items overlap using the design token instead of -12; update the
conditional that sets marginRight for the last item to use the token (or 0) and
remove the literal -12.
| {supportedAssets.map((symbol, index) => ( | ||
| <XStack key={symbol} marginRight={index < supportedAssets.length - 1 ? -12 : 0} zIndex={index}> | ||
| <AssetLogo symbol={symbol} width={32} height={32} /> | ||
| </XStack> | ||
| ))} |
There was a problem hiding this comment.
Use design tokens instead of hardcoded overlap spacing.
marginRight={-12} should be replaced with a spacing token from tamagui.config.ts to comply with the design-token-only rule.
As per coding guidelines, all styling must use predefined design tokens from tamagui.config.ts and avoid hardcoded values.
🤖 Prompt for AI Agents
In `@src/components/shared/CopyAddressSheet.tsx` around lines 78 - 82, The XStack
inside the supportedAssets.map is using a hardcoded overlap marginRight={-12};
replace that hardcoded value with the appropriate spacing token from
tamagui.config.ts (import the token and use it in the marginRight prop for the
XStack rendered in the supportedAssets map) so AssetLogo items overlap using the
design token instead of -12; update the conditional that sets marginRight for
the last item to use the token (or 0) and remove the literal -12.
src/utils/usePortfolio.ts
Outdated
| const marketAddresses = new Set(markets.map(({ market }) => market.toLowerCase())); | ||
| return balances | ||
| .filter(({ address }) => !marketAddresses.has(address.toLowerCase())) |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/components/shared/PaymentScheduleSheet.tsx`:
- Around line 61-70: The symbol computation inside the map is redundant since it
only depends on market.symbol; move the derivation (e.g., const symbol =
market.symbol?.slice(3) === "WETH" ? "ETH" : market.symbol?.slice(3)) out of the
Array.from({ length: loan.installments }).map callback in the
PaymentScheduleSheet component so the map only computes per-iteration values
(like maturity) and renders using the precomputed symbol; keep existing
identifiers (loan.installments, MATURITY_INTERVAL, market.symbol, symbol)
intact.
| const externalAssets = useMemo<ExternalAsset[]>(() => { | ||
| const balances = tokenBalances ?? []; | ||
| if (balances.length === 0 || !markets) return []; | ||
|
|
||
| const marketAddresses = new Set(markets.map(({ market }) => market.toLowerCase())); | ||
| return balances | ||
| .filter(({ address }) => !marketAddresses.has(address.toLowerCase())) | ||
| .map((token) => ({ | ||
| ...token, | ||
| usdValue: (Number(token.priceUSD) * Number(token.amount ?? 0n)) / 10 ** token.decimals, | ||
| type: "external" as const, | ||
| })); | ||
| }, [tokenBalances, markets]); |
There was a problem hiding this comment.
🟡 external assets are hidden until markets load because usePortfolio requires markets to compute externalAssets
usePortfolio returns [] for externalAssets whenever markets is not yet available, even if LiFi token balances have already loaded.
Actual behavior: during initial load / slow previewer response, any UI that relies on externalAssets (and derived values like assets and totalBalanceUSD) will temporarily show no external assets and under-report the portfolio total.
Expected behavior: external token balances should be shown as soon as they are available; protocol market data should only be needed for filtering out protocol market addresses (and can be applied later) rather than gating the entire list.
Click to expand
In usePortfolio, external balances are immediately discarded if markets is falsy:
const balances = tokenBalances ?? [];
if (balances.length === 0 || !markets) return [];This makes externalAssets depend on markets, even though balances are fetched independently. See src/utils/usePortfolio.ts:103-110.
Recommendation: Do not gate externalAssets on markets. If markets is unavailable, return external balances without filtering, and re-filter once markets load (or filter with an empty set). Update totalBalanceUSD/assets accordingly.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const marketAddresses = new Set(markets?.map(({ market }) => market.toLowerCase()) ?? []); | ||
| return balances | ||
| .filter(({ address }) => !marketAddresses.has(address.toLowerCase())) | ||
| .map((token) => ({ | ||
| ...token, | ||
| usdValue: (Number(token.priceUSD) * Number(token.amount ?? 0n)) / 10 ** token.decimals, | ||
| type: "external" as const, | ||
| })); |
There was a problem hiding this comment.
🔴 external assets filtering compares token address to market address, causing protocol assets to appear as external duplicates
usePortfolio builds externalAssets from LiFi token balances and tries to filter out assets already represented by protocol markets. However it filters using the market contract address (market.market) instead of the underlying ERC-20 asset address (market.asset).
Actual vs expected:
- Expected: tokens that correspond to Exactly markets (e.g. USDC token address, WETH token address) should not be treated as “external assets”, because they’re already represented by protocol positions.
- Actual: the filter doesn’t exclude them (token addresses almost never equal market contract addresses), so the same underlying asset can appear both as a protocol asset and as an external asset.
Impact:
- portfolio totals can be inflated (double-counting) via
totalBalanceUSDaggregation. - UI lists (portfolio, pay, swaps token selection) can show duplicates and potentially pick the wrong default asset.
Click to expand
The problematic logic:
const marketAddresses = new Set(markets?.map(({ market }) => market.toLowerCase()) ?? []);
return balances
.filter(({ address }) => !marketAddresses.has(address.toLowerCase()))This compares token.address (ERC-20) to market.market (market contract).
Recommendation: Filter external balances by the protocol market’s underlying asset address (e.g. markets?.map(({ asset }) => asset.toLowerCase())) rather than the market contract address. If you also need to exclude market contract addresses for some reason, keep both sets and exclude either.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/components/pay-mode/Pay.tsx`:
- Around line 68-69: The usePortfolio hook is being called without the current
account, causing it to re-run useAccount(); update the call to pass the existing
account variable into usePortfolio (instead of undefined) so it uses that
account and avoids redundant work—locate the usePortfolio call near the assets
declaration and change its first argument from undefined to account (while
leaving the options { sortBy: "usdcFirst" } intact); no other changes to
useAsset or marketUSDCAddress/ exaUSDC are required.
| const { assets } = usePortfolio(undefined, { sortBy: "usdcFirst" }); | ||
| const { market: exaUSDC } = useAsset(marketUSDCAddress); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Pass account into usePortfolio to avoid redundant hook work.
You already have account, so pass it explicitly to prevent usePortfolio from invoking useAccount() again and to make the dependency clear.
♻️ Proposed change
- const { assets } = usePortfolio(undefined, { sortBy: "usdcFirst" });
+ const { assets } = usePortfolio(account, { sortBy: "usdcFirst" });🤖 Prompt for AI Agents
In `@src/components/pay-mode/Pay.tsx` around lines 68 - 69, The usePortfolio hook
is being called without the current account, causing it to re-run useAccount();
update the call to pass the existing account variable into usePortfolio (instead
of undefined) so it uses that account and avoids redundant work—locate the
usePortfolio call near the assets declaration and change its first argument from
undefined to account (while leaving the options { sortBy: "usdcFirst" } intact);
no other changes to useAsset or marketUSDCAddress/ exaUSDC are required.
closes #639, closes #645
Summary by CodeRabbit
New Features
Bug Fixes
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.