Conversation
WalkthroughDerives and exposes USDC price from portfolio data on HomeScreen, threads Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant HomeScreen
participant Portfolio
participant Buy
participant PreviewBuy
participant useRelayBuy
participant QuoteService
User->>HomeScreen: open app
HomeScreen->>Portfolio: fetch wallet balances
Portfolio-->>HomeScreen: balances + per-chain price info
HomeScreen->>HomeScreen: determine max stable balance & usdcPrice
HomeScreen->>Buy: render with usdcPrice
User->>Buy: start buy flow
Buy->>PreviewBuy: open preview (passes usdcPrice)
PreviewBuy->>useRelayBuy: getBestOffer(usdAmount, usdcPrice)
Note over useRelayBuy: normalize native token -> 0x0\nconvert USD to USDC using usdcPrice
useRelayBuy->>QuoteService: request quote (normalized addresses, usdc amount)
QuoteService-->>useRelayBuy: return offer
useRelayBuy-->>PreviewBuy: offer
PreviewBuy-->>Buy: display quote
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. 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 |
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 (1)
src/apps/pulse/utils/utils.tsx (1)
218-263: Stable balance per chain is overwritten instead of accumulatedIn
getStableCurrencyBalanceOnEachChain, each matching stablecontractdoes:balanceMap[chainId] = { balance: price * balance, price: asset.price ? asset.price : undefined, };This overwrites any previous value for that
chainId. If a user has:
- Multiple stable contracts for the same token on a chain, or
- Multiple stable tokens counted in
STABLE_CURRENCIESfor the same chain,you’ll only retain the last one processed, so
balanceunder-reports total stable value and can cause the wrongmaxStableCoinBalancechain to be selected.I’d suggest accumulating instead:
- const price = asset.price ?? 0; - const balance = contract.balance ?? 0; - balanceMap[chainId] = { - balance: price * balance, - price: asset.price ? asset.price : undefined, - }; + const price = asset.price ?? 0; + const balance = contract.balance ?? 0; + const prev = balanceMap[chainId] || { balance: 0, price: undefined }; + const usdValue = price * balance; + balanceMap[chainId] = { + balance: prev.balance + usdValue, + // Prefer a non‑zero price if available, otherwise keep previous + price: price || prev.price, + };That keeps your “USD balance per chain” semantics while avoiding silent under-counting.
🧹 Nitpick comments (6)
src/apps/pulse/utils/parseSearchData.ts (4)
17-40: OP native ETH normalization helper looks good; consider minor robustness tweaksThe normalization logic is clear and correctly scoped to Optimism native ETH. To make it a bit more defensive against API quirks, you could safely loosen the symbol check to be case-insensitive:
- if ( - contract.toLowerCase() === OP_NATIVE_MOBULA.toLowerCase() && - symbol === 'ETH' && - chainId === OPTIMISM_CHAIN_ID - ) { + if ( + contract.toLowerCase() === OP_NATIVE_MOBULA.toLowerCase() && + symbol.toUpperCase() === 'ETH' && + chainId === OPTIMISM_CHAIN_ID + ) {This avoids breakage if Mobula ever returns
eth/Ethinstead of strictlyETH.
67-88: UsenormalizedContractwhen checkingisWrappedNativeTokenfor consistencyYou correctly normalize the contract before storing it in the
Assetobject, but the wrapped-native filter still uses the originalcontractAddress. Using the normalized value would make the semantics consistent across all uses:- const contractAddress = contracts[i]; - const normalizedContract = normalizeContractAddress( - contractAddress, - asset.symbol, - chainId - ); + const contractAddress = contracts[i]; + const normalizedContract = normalizeContractAddress( + contractAddress, + asset.symbol, + chainId + ); // Filter out wrapped native tokens (WETH, WBNB, WPOL, etc.) from search results - if (!isWrappedNativeToken(contractAddress, chainId)) { + if (!isWrappedNativeToken(normalizedContract, chainId)) {Same pattern can be applied in
parseTokenDataandparseFreshAndTrendingTokensbelow.
100-125: Mirror normalization usage inparseTokenDataAs above, consider using
normalizedContractfor the wrapped-native filter to keep behavior aligned with theAsset.contractvalue:- if (MOBULA_CHAIN_NAMES.includes(blockchains[i])) { - const chainId = chainNameToChainIdTokensData(blockchains[i]); - const contractAddress = contracts[i]; - const normalizedContract = normalizeContractAddress( - contractAddress, - asset.symbol, - chainId - ); - - // Filter out wrapped native tokens (WETH, WBNB, WPOL, etc.) from search results - if (!isWrappedNativeToken(contractAddress, chainId)) { + if (MOBULA_CHAIN_NAMES.includes(blockchains[i])) { + const chainId = chainNameToChainIdTokensData(blockchains[i]); + const contractAddress = contracts[i]; + const normalizedContract = normalizeContractAddress( + contractAddress, + asset.symbol, + chainId + ); + + // Filter out wrapped native tokens (WETH, WBNB, WPOL, etc.) from search results + if (!isWrappedNativeToken(normalizedContract, chainId)) { result.push({ ... contract: normalizedContract,Functionally this should be equivalent today, but it future-proofs behavior if the wrapped-native detection ever starts treating the zero address specially.
152-184:parseFreshAndTrendingTokensnormalization fits well; align wrapped-native check as wellThe extraction of
symboland use ofnormalizeContractAddressbefore constructing theAssetlooks good. For consistency with the other helpers, you could also usenormalizedContractfor the wrapped-native filter:- const contractAddress = j.leftColumn?.line1?.copyLink || ''; - const symbol = j.leftColumn?.line1?.text2 || ''; - const normalizedContract = normalizeContractAddress( - contractAddress, - symbol, - +chainId - ); + const contractAddress = j.leftColumn?.line1?.copyLink || ''; + const symbol = j.leftColumn?.line1?.text2 || ''; + const normalizedContract = normalizeContractAddress( + contractAddress, + symbol, + +chainId + ); // Filter out wrapped native tokens (WETH, WBNB, WPOL, etc.) from search results - if (!isWrappedNativeToken(contractAddress, +chainId)) { + if (!isWrappedNativeToken(normalizedContract, +chainId)) { res.push({ chain: getChainName(+chainId), contract: normalizedContract, ... symbol,Nice reuse of the helper and keeping the Optimism-native handling localized to this utility.
src/apps/pulse/components/App/HomeScreen.tsx (1)
110-115: USDC price derivation and propagation look consistent, minor cleanups possibleThe usdcPrice wiring from portfolio →
getStableCurrencyBalanceOnEachChain→HomeScreen→PreviewBuy/Buyis coherent:
- You pick the chain with the maximum stable “balance” and read
pricefrom that same entry.- That
usdcPriceis then the one used for both the initial quote (Buy) and preview/refresh (PreviewBuy).Two small follow‑ups you might consider:
maxStableCoinBalance’sprice?: numberfield is never set or read; either populate it when callingsetMaxStableCoinBalanceor drop it from the type to keep state shape minimal.if (usdcPriceForMaxChain) setUsdcPrice(usdcPriceForMaxChain);will ignore legitimate0values and leave a stale prior price; if you ever expect 0/very-low prices from the API, using an explicit!= nullcheck would avoid that subtlety.Functionally this looks fine; these are mostly cleanliness and edge‑case considerations.
Also applies to: 159-160, 212-242, 960-979, 1124-1143
src/apps/pulse/components/Buy/Buy.tsx (1)
58-82: usdcPrice propagation into Relay Buy quotes and button state looks correct
- Adding
usdcPricetoBuyProps, threading it intogetBestOffer, and including it infetchBuyOffer’s dependencies ensures Relay quotes are computed using the portfolio‑derived USDC price.- Passing
useRelayBuy={USE_RELAY_BUY}intoBuyButtoncleanly gates the “Enable Trading” behavior to the Intent SDK path only.Behavior-wise this matches the PR goals; the dependency/refresh behavior around
usdcPriceis acceptable given you already have an explicit/auto refresh mechanism.Also applies to: 100-103, 280-337, 781-805
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
src/apps/pulse/components/App/HomeScreen.tsx(5 hunks)src/apps/pulse/components/Buy/Buy.tsx(5 hunks)src/apps/pulse/components/Buy/BuyButton.tsx(7 hunks)src/apps/pulse/components/Buy/PreviewBuy.tsx(4 hunks)src/apps/pulse/hooks/useRelayBuy.ts(4 hunks)src/apps/pulse/utils/parseSearchData.ts(7 hunks)src/apps/pulse/utils/utils.tsx(2 hunks)src/hooks/useRemoteConfig.ts(1 hunks)src/services/firebase.ts(2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.
Applied to files:
src/apps/pulse/components/Buy/PreviewBuy.tsxsrc/apps/pulse/hooks/useRelayBuy.tssrc/apps/pulse/components/App/HomeScreen.tsxsrc/apps/pulse/components/Buy/BuyButton.tsxsrc/apps/pulse/utils/utils.tsx
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.
Applied to files:
src/apps/pulse/components/Buy/PreviewBuy.tsxsrc/apps/pulse/hooks/useRelayBuy.tssrc/apps/pulse/components/App/HomeScreen.tsx
🧬 Code graph analysis (4)
src/hooks/useRemoteConfig.ts (1)
src/apps/pulse/hooks/useRelayBuy.ts (1)
useRelayBuy(47-799)
src/apps/pulse/utils/parseSearchData.ts (2)
src/utils/blockchain.ts (2)
isWrappedNativeToken(514-521)getChainName(269-288)src/apps/pulse/utils/constants.ts (1)
getChainName(51-70)
src/apps/pulse/components/Buy/BuyButton.tsx (2)
src/apps/pulse/hooks/useRelayBuy.ts (2)
useRelayBuy(47-799)BuyOffer(30-36)src/apps/pulse/types/tokens.ts (1)
PayingToken(12-21)
src/apps/pulse/utils/utils.tsx (1)
src/apps/pulse/constants/tokens.ts (1)
STABLE_CURRENCIES(13-15)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: lint
- GitHub Check: unit-tests
🔇 Additional comments (4)
src/services/firebase.ts (1)
34-36: Relay Buy now defaults to enabled in both config and error fallback pathsSetting
remoteConfig.defaultConfig.USE_RELAY_BUYtotrueand returningtruefromgetUseRelayBuyFlagon errors means Relay Buy will be enabled whenever remote config is unreachable or unset. This is a behavioral flip from the previous default-off posture; please confirm this is intended for all environments (including staging/production) and aligned with your rollout plan.Also applies to: 55-63
src/hooks/useRemoteConfig.ts (1)
35-37: Initial Relay Buy state now optimistictruebefore Remote Config resolves
useRelayBuynow starts astrue, and is only overridden once Remote Config (or URL override) loads. Together with the Firebase default and error fallback, Relay Buy will be effectively “on by default” even during startup or transient config errors. Please verify that this optimistic behavior is desired for all environments and won’t surprise users who previously saw Intent SDK first.src/apps/pulse/components/Buy/PreviewBuy.tsx (1)
55-71: Preview refresh correctly uses usdcPrice; execution still depends on useRelayBuy’s defaultWithin PreviewBuy:
- The new
usdcPriceprop is cleanly added and passed intogetBestOfferinsiderefreshPreviewBuyData, and included in that callback’s dependency list.- Auto‑refresh will thus recompute Relay offers using the latest portfolio‑derived USDC price.
However, note that the confirm path calls
executeBuyDirectly(), which delegates toexecuteBuyinuseRelayBuywithout forwardingusdcPrice. As a result, the execution quote may use the default price configured insideuseRelayBuyrather than the value you used for the preview. See the separate comment onuseRelayBuy.executeBuyfor a concrete suggestion.Also applies to: 73-89, 497-553, 613-631
src/apps/pulse/components/Buy/BuyButton.tsx (1)
1-17: Buy button behavior cleanly separates Relay Buy from Intent SDKThis refactor looks solid:
- The new
useRelayBuyprop is correctly threaded into bothgetButtonTextandisDisabled.- “Enable Trading on …” is now only shown when
!useRelayBuyand modules aren’t installed, so Relay Buy never displays the Intent‑SDK‑specific enablement path.- For Relay Buy,
getButtonTextsensibly prefersexpressIntentResponse.tokenAmountToReceive(fromBuyOffer) when available, and falls back to the price‑based estimate otherwise.- The widened
expressIntentResponseunion (ExpressIntentResponse | BuyOffer | null | { error: string }) is guarded by property checks ('tokenAmountToReceive' in …,.bids?.length,.error) so each flow reads only the fields it owns.Overall this achieves the “Enable Trading only when relayBuy is false” objective without introducing obvious regressions in the disabled state logic.
Also applies to: 24-41, 59-107, 109-126, 128-142, 144-163, 178-189
| import { isWrappedNativeToken } from '../../../utils/blockchain'; | ||
|
|
||
| // Optimism uses this special address for native ETH in Mobula API | ||
| const OP_NATIVE_MOBULA = '0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000'; |
There was a problem hiding this comment.
And for other natives they use 0xeeee no?
There was a problem hiding this comment.
Yes this fix is only for op
| toChainId | ||
| ); | ||
| const normalizedToTokenAddress = isNativeToken(toTokenAddress) | ||
| ? '0x0000000000000000000000000000000000000000' |
There was a problem hiding this comment.
So why are we not wrapping anymore?
There was a problem hiding this comment.
So for wrapped token it has to convert the input to wrapped but on wallets balance that won't be possible to have that many wrapped token as the native. On sell it can be wrapped token since it's the destination token but for buy you can't enforce this
Deploying x with
|
| Latest commit: |
fbd155f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://b76cebc2.x-e62.pages.dev |
| Branch Preview URL: | https://op-mobula-search.x-e62.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/apps/pulse/hooks/useRelayBuy.ts (1)
149-176: USD→USDC conversion logic is correct; consider guarding usdcPrice ≤ 0 explicitlyThe native token normalization and USD→USDC conversion (
usdAmount / usdcPricethenparseUnits(..., 6)) look correct for Relay’s EXact‑input USDC model. The only edge case is ifusdcPriceis0or non‑finite, wheretoFixedwill throw and surface as “Invalid amount.” If you want clearer diagnostics and a safer failure mode, you could add an explicit guard:- const usdAmount = parseFloat(fromAmount); - if (Number.isNaN(usdAmount) || usdAmount <= 0) { - throw new Error('Invalid amount'); - } - - // Convert USD to USDC amount using actual USDC price - const usdcAmount = usdAmount / usdcPrice; + const usdAmount = parseFloat(fromAmount); + if (Number.isNaN(usdAmount) || usdAmount <= 0) { + throw new Error('Invalid amount'); + } + if (!Number.isFinite(usdcPrice) || usdcPrice <= 0) { + throw new Error('Invalid USDC price'); + } + + // Convert USD to USDC amount using actual USDC price + const usdcAmount = usdAmount / usdcPrice;This is defensive rather than strictly required, since
usdcPriceshould already be > 0.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/apps/pulse/components/Buy/PreviewBuy.tsx(5 hunks)src/apps/pulse/hooks/useRelayBuy.ts(6 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.
Applied to files:
src/apps/pulse/components/Buy/PreviewBuy.tsxsrc/apps/pulse/hooks/useRelayBuy.ts
📚 Learning: 2025-11-21T13:10:33.401Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.401Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.
Applied to files:
src/apps/pulse/components/Buy/PreviewBuy.tsx
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.
Applied to files:
src/apps/pulse/components/Buy/PreviewBuy.tsxsrc/apps/pulse/hooks/useRelayBuy.ts
📚 Learning: 2025-05-23T14:44:33.911Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 315
File: src/apps/the-exchange/utils/wrappedTokens.ts:6-20
Timestamp: 2025-05-23T14:44:33.911Z
Learning: XDAI (Gnosis Chain) is intentionally excluded from the WRAPPED_NATIVE_TOKEN_ADDRESSES mapping in the exchange app's wrappedTokens utility.
Applied to files:
src/apps/pulse/hooks/useRelayBuy.ts
🧬 Code graph analysis (1)
src/apps/pulse/hooks/useRelayBuy.ts (1)
src/services/tokensData.ts (1)
Token(19-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: lint
- GitHub Check: unit-tests
- GitHub Check: build
🔇 Additional comments (5)
src/apps/pulse/components/Buy/PreviewBuy.tsx (3)
55-71: PropagatingusdcPriceinto PreviewBuy props looks consistent
usdcPriceis optional, correctly threaded fromPreviewBuyPropsinto the component, and only used in Relay Buy paths, so existing non‑Relay flows remain unaffected.Also applies to: 73-88
388-395: executeBuy now receives portfolio and usdcPrice, aligning execution with previewPassing both
userPortfolioandusdcPriceintoexecuteBuyensures the executed Relay Buy uses the same USDC pricing context and balance validation as the preview, resolving the earlier quote mismatch between preview and execution.
535-542: IncludingusdcPricein quote refresh and dependencies avoids stale pricingForwarding
usdcPriceintogetBestOfferand adding it torefreshPreviewBuyData’s deps means preview quotes will re‑compute when USDC price changes, and the 15s auto‑refresh will track the latest price rather than a captured value.Also applies to: 614-632
src/apps/pulse/hooks/useRelayBuy.ts (2)
24-45: usdcPrice threading into BuyParams and hook state is clean and backwards‑compatibleAdding
usdcPrice?: numbertoBuyParamsand keeping it optional (with a default of1.0ingetBestOffer) lets newer flows be price‑aware without breaking existing callers that don’t pass a value.
608-614: executeBuy now forwards usdcPrice into getBestOffer, keeping execution aligned with previewExtending
executeBuy’s signature and passingusdcPricethrough togetBestOfferensures the execution quote uses the same USDC price as the one used for preview/refresh, avoiding subtle discrepancies while keeping older call sites functional via theusdcPricedefault.Also applies to: 631-638
Description
How Has This Been Tested?
Screenshots (if appropriate):
Types of changes
Summary by CodeRabbit
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.