Skip to content

Conversation

@gomesalexandre
Copy link
Contributor

@gomesalexandre gomesalexandre commented Jan 14, 2026

Description

This PR consolidates the following PRs into a single review-friendly PR targeting develop:

All above PRs will be closed in favor of this consolidated PR.

What's included:

Fixes:

  • Fix transaction modal showing skipped approval steps that shouldn't be visible
  • Fix "My Positions" showing empty after deposit/withdraw (now waits for data refresh)
  • Fix Action Center notifications staying stuck on "Pending" after transaction completes
  • Fix balance not updating for second-class chains (Monad, Tron, etc.) after yield transactions
  • Fix filters not working on asset symbol pages (e.g., /yields/asset/GHO)
  • Fix stale quote shown when re-entering deposit/withdraw modal after completing a transaction
  • Fix claim flow by properly passing passthrough and manageActionType from pendingActions
  • Fix APY in a few places for Cosmos SDK staking (ShapeShift DAO validator and Cosmostation)
  • Fix success step not showing when staking from earn CTA in account/asset page
  • Fix duplicate close buttons on success step

Features:

  • Add new earn tab in swapper under flag for one-click yield aping
  • Add metric clarity tooltips for Available to Earn, APY help text, Potential Earnings
  • Add recommendation strip above tabs showing top 3 opportunities based on holdings
  • Add type filter alongside network/provider with URL persistence
  • Add new one-click enter modal in account/asset pages
  • Add new flag for "Yield page" to soft toggle yield without full yields page
  • Create YieldForm, YieldHero, and YieldManager components for cleaner yield detail page

UX/UI Improvements:

  • Better information architecture of /yields route for mobile
  • Improve mobile layouts throughout
  • Make yield feature less ai-spewy and more consistent with design system
  • Honor various enter terminologies depending on yield type (stake/deposit/supply)
  • Use proper translation interpolation instead of string concatenation
  • Improve desktop layout with wider container and better spacing
  • Simplify YieldStats to 3-column grid without card wrapper
  • YieldPositionCard now hides when no balance
  • Render asset cards when there is a single yield for that asset
  • Prefer asset symbols for simplicity; leverage right-side APY for easy comparison

Cleanup:

  • Remove multi-validator support - only use default validator (ShapeShift DAO for Cosmos)
  • Delete unused components: ValidatorBreakdown, YieldEnterExit, YieldValidatorSelectModal (~1300 lines removed)
  • Remove unused translations
  • Perform code extraction/unification

Issue (if applicable)

Risk

Low - all changes are scoped to the yield feature which is behind a feature flag. No changes to transaction logic beyond fixing the claim flow passthrough.

What protocols, transaction types, wallets or contract interactions might be affected by this PR?

Yield.xyz integrations, yield enter/exit/claim actions on all supported yield protocols.

Testing

Engineering

  • Ensure yield-specific page looks and performs well (stake/unstake/claim) in web and mobile viewports
  • Ensure multi-validator support is gone (only default validator used)
  • Navigate to /yields and apply network/provider/type filters - verify persistence
  • Click on an asset (e.g., USDC) - filters should persist in URL
  • Test deposit transaction and verify:
    • Only non-skipped steps shown in modal
    • "My Positions" shows balance immediately after completion
    • Action Center shows "Complete" not "Pending"
    • For Monad/Tron, native balance updates after tx
  • Test earn tab with USDT and another asset
  • Test subsequent deposits - should request approval again, not show stale quote
  • Ensure USDT deposits are gucci (approval needed, reset + approval needed, no approval needed)
  • Ensure ShapeShift DAO is still "preferred" for Cosmos SDK staking
  • Things look better on mobile now!

Operations

  • 🏁 My feature is behind a flag and doesn't require operations testing (yet)

Screenshots (if applicable)

Yield detail page refactor:
https://jam.dev/c/2ff6b47e-d2dc-4147-a0d9-52c10a703069

Mobile improvements:
mobile1
mobile2
mobile3
mobile4

Type filter & recommendations:
https://jam.dev/c/8bae16b6-87fe-41a8-a480-f918a0b70679

Earn tab:
https://jam.dev/c/c9e81000-60cb-4430-95d0-89a51225629f
https://jam.dev/c/4fabf2af-06af-488f-8e5b-e93f513c278b

One-click enter flow - Mobile:
https://jam.dev/c/1ff8f48c-ca5e-4b79-992c-cc5bf9109ffb

One-click enter flow - Desktop:
https://jam.dev/c/1f93ce52-a66f-410c-a0ae-c92eb47d3ba9

USDT reset + approval flows:
https://jam.dev/c/2f4fe73b-095f-45f0-8e5d-7f11b654b9cf
https://jam.dev/c/c8a11f4e-aabb-4485-bab9-63a9276193c9

Initial fixes:
https://jam.dev/c/9f77b0f6-7185-4383-ac98-6dcd8f32517e
https://jam.dev/c/71010588-6804-4cf5-8974-b31cd602e5cd

Success step & duplicate close button fixes:

Summary by CodeRabbit

  • New Features

    • Earn tab with end-to-end enter/exit flows, input & confirmation screens, and success UI
    • Full-screen Yield Manager and Enter modal, transaction steps list, YieldHero and animated asset flow visuals
  • Improvements

    • Terminology updated from “Deposit/Withdraw” to “Enter/Exit”
    • Recommended-yields strip, enhanced provider/validator selection, filters, and improved mobile/responsive layouts
    • Expanded translations and user-facing copy updates
  • Chores

    • New feature flags to control Yields page and Earn tab (dev/prod toggles)

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

gomesalexandre and others added 30 commits January 11, 2026 16:22
- Remove unnecessary filteredYields dependency in YieldAssetDetails useMemo hooks
- Add validator breakdown utility functions
- Enhance yield action modal with additional transaction handling
- Refactor yield filters component for better code organization
- Update yields list component layout
- Improve yield filters hook logic
- Add yield transaction flow hooks
- Add missing translation key

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
- Add APPROVAL type to TX_TYPE_TO_LABEL (button now shows "Approve" not "Approval")
- Filter SKIPPED transactions in displaySteps preview
- Use first CREATED transaction for button text
- Harmonize button/row labels: SUPPLY/ENTER -> "Deposit", EXIT -> "Withdraw"
- Fix useCallback missing dependencies in useYieldTransactionFlow
- Add icon background color for yield detail hero
- Add chainName to action transaction metadata

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Change invalidateQueries to refetchQueries with await
- Ensures position data is fresh before modal closes
- Fixes "My Positions" showing empty after deposit/withdraw

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Since dispatchNotification is called after waitForActionCompletion,
the transaction is already confirmed. Dispatch with Complete status
directly instead of relying on Unchained subscriber (which doesn't
work for chains without Unchained support like Monad).

Co-Authored-By: Claude Opus 4.5 <[email protected]>
After yield transaction completion, trigger portfolioApi.getAccount
refetch for second class chains (Monad, Tron, etc.) that don't have
Unchained support. This ensures wallet balances update immediately.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Extract sortedRows from table to use as dependency
- TanStack Table's table object reference is stable, so useMemo
  was not re-running when filtered data changed
- Using sortedRows as dependency triggers proper re-memoization

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add searchString prop to YieldItem
- Pass current filter params when navigating to asset detail page
- Filters persist across navigation (e.g., network=ethereum)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Set staleTime: Infinity to prevent refetching during modal lifecycle
- Set gcTime: 0 to garbage collect cached quote when modal closes
- Ensures fresh quote on re-entry, no stale data from previous tx

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Remove quote queries from cache when modal closes
- Ensures fresh quote fetch on subsequent modal opens
- Remove YIELD_POLISH.md

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Create YieldEnterModal component with Trust Wallet-inspired design
- Big centered input with fiat/crypto toggle
- Asset icon, percent buttons (25%, 50%, 75%, Max)
- Debounced quote fetching (500ms) for pre-loading transactions
- Auto-select default validator (ShapeShift DAO for Cosmos)
- Stats section with APY, validator/provider info, and min deposit
- Modify YieldAssetSection to open modal instead of navigate
- Add translation key for "Stake {asset}"

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add TransactionStepsList component for shared step display
- Extract useConfetti hook to eliminate duplication
- Export poll/waitForActionCompletion from useYieldTransactionFlow
- Update YieldOpportunityCard with horizontal layout
- Use Card variant='dashboard' in YieldAssetSection
- Add tx hash links in success view

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- YieldFilters: Use MenuOptionGroup/MenuItemOption for proper theme styling (no more blue.500)
- ValidatorBreakdown: Remove useColorModeValue, use direct color tokens
- YieldEnterModal: Extract hover style constants outside component
- YieldEnterExit: Extract style constants outside component, fix inline button handlers
- YieldActionModal: Use STATUS_LABEL_KEYS map to avoid nested ternaries
- useYieldTransactionFlow: Rename loadingMessage to statusLabel for consistency

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- YieldValidatorSelectModal: gray.300, gray.500 -> text.subtle
- YieldActionModal: whiteAlpha.50 -> background.surface.raised.base

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add SHAPESHIFT_VALIDATOR constant with fallback APR data
- normalizeCosmosValidators ensures ShapeShift is always first
- Only ShapeShift is marked as preferred for Cosmos SDK
- Add COSMOS_NETWORK_FALLBACK_APR (15%) for validators missing APR
- Remove redundant sortValidators call in modal

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Remove redundant ASSET column from YieldActivePositions table
- Remove "Your Balance" label (already obvious from context)
- Move ensureValidatorApr to utils for reuse
- Fix React key warning in ReactTable (destructure key from spread props)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…hide section without wallet

- Extract TransactionStepStatus from anonymous IIFE in YieldActionModal
- Reuse existing poll utility from lib/poll instead of custom implementation
- Hide YieldAssetSection when wallet is not connected
- Reorder TransactionStep type fields for cleaner grouping
- Remove unnecessary curly braces in useYieldValidators

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…saction step

- Reverted poll changes to use original inline implementation
- Export waitForActionCompletion for YieldEnterModal usage
- Fix enterButtonText to use getTransactionButtonText for proper labels
  (shows "Approve" instead of "Stake USDT" when approval is first step)
- Add originalTitle and type to TransactionStep in YieldEnterModal
- Ensure consistent use of loadingMessage property

Co-Authored-By: Claude Opus 4.5 <[email protected]>
… model

- Replace for-loop auto-execution with click-per-step model matching useYieldTransactionFlow
- Add activeStepIndex, rawTransactions, currentActionId state for multi-step tracking
- Extract executeSingleTransaction callback for single transaction execution
- Export filterExecutableTransactions from hook for code reuse
- Fix button text and steps display between transaction steps

[skip ci]

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Adds waitForTransactionConfirmation helper to poll backend until the
submitted transaction is confirmed, preventing nonce conflicts when
users quickly transition between Approve and Deposit transactions.

This resolves "replacement transaction underpriced" errors by ensuring
the on-chain nonce counter has incremented before constructing the next
transaction.

[skip ci]

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Co-Authored-By: Claude Opus 4.5 <[email protected]>
gomesalexandre and others added 13 commits January 12, 2026 23:27
Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add responsive padding to Container components for breathing room on mobile
- Fix YieldFilters to stack vertically on mobile with full-width controls
- Fix ViewToggle to be full-width on mobile with equal flex buttons
- Fix YieldAssetDetails filters layout for mobile responsiveness
- Hide YieldOpportunityStats when wallet not connected (stale data bug)
- Add isConnected guard to prevent showing stale portfolio balances
- Add "myPositions" translation key for plural form in list tab

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Create YieldForm component for enter/exit/claim modal
- Create YieldHero component with centered title and stacked icon design
- Create YieldManager dialog wrapper for yield actions
- Fix claim flow by passing passthrough and manageActionType from pendingActions
- Use proper translation interpolation for modal titles
- Add missing translations (stakeSymbol, unstakeSymbol, depositSymbol, etc.)
- Remove multi-validator support - only use default validator (ShapeShift DAO)
- Delete unused ValidatorBreakdown, YieldEnterExit, YieldValidatorSelectModal
- Improve desktop layout with wider container
- Simplify YieldStats and YieldPositionCard

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@gomesalexandre gomesalexandre requested a review from a team as a code owner January 14, 2026 18:32
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

📝 Walkthrough

Walkthrough

Adds a gated "Earn" feature: new feature flags, routes and navigation, Earn UI (input/confirm), yield selection components, transaction flow enhancements (USDT reset, polling), many new Yields UI components and Redux slice, extensive translations and several Yields refactors/removals.

Changes

Cohort / File(s) Summary
Feature flags & config
\.env`, `.env.development``, src/config.ts, ``src/state/slices/preferencesSlice/preferencesSlice.ts``
Added VITE_FEATURE_YIELDS_PAGE and VITE_FEATURE_EARN_TAB entries, validators, and initialized feature flags in preferences slice.
Routing & navigation
src/Routes/RoutesCommon.tsx, src/components/Layout/Header/Header.tsx, src/components/Layout/Header/NavBar/NavigationDropdown.tsx, src/pages/Trade/tabs/*Tab.tsx, src/components/MultiHopTrade/types.ts, src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputHeader.tsx
Added /earn routes (asset-specific + confirm), Earn tab wiring, TradeInputTab.Earn enum, and header/dropdown active-route logic.
Earn flow pages & components
src/pages/Trade/tabs/EarnTab.tsx, src/components/MultiHopTrade/components/Earn/EarnInput.tsx, src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx, src/components/MultiHopTrade/components/Earn/types.ts
New EarnTab page, EarnInput (selection/quotes), EarnConfirm (multi-step confirm), and route constants/types.
Yield selector & footer
src/components/MultiHopTrade/components/Earn/components/YieldSelector.tsx, src/components/MultiHopTrade/components/Earn/components/EarnFooter.tsx
Added YieldSelector modal and EarnFooter with APY/earnings/validator/provider UI and CTA logic.
Transaction flow & utils
src/pages/Yields/hooks/useYieldTransactionFlow.ts, src/lib/yieldxyz/utils.ts, src/lib/yieldxyz/constants.ts
Enhanced useYieldTransactionFlow with USDT allowance/reset, polling helpers, displaySteps; added getTransactionButtonText, ensureValidatorApr, and Cosmos validator constants.
Yields UI — new components
src/pages/Yields/components/* (e.g., YieldEnterModal, YieldForm, YieldManager, YieldHero, YieldAssetFlow, YieldSuccess, TransactionStepsList)
Added multiple components for enter/exit/claim flows, success UI, steps list, and a confetti hook.
Yields UI — refactors & mobile
src/pages/Yields/* (e.g., YieldDetail, YieldAssetDetails, YieldItem, YieldStats, YieldPositionCard, YieldActivePositions, YieldAssetSection, YieldOpportunityCard, YieldFilters, YieldOpportunityStats, YieldTable, YieldsList)
Large refactor: mobile detection, type filtering, responsive layouts, route-driven modals, removal of color-mode branching, recommended strip, and UI simplifications.
Removed legacy yield modules
src/pages/Yields/components/ValidatorBreakdown.tsx, src/pages/Yields/components/YieldEnterExit.tsx, src/pages/Yields/components/YieldValidatorSelectModal.tsx
Deleted legacy components replaced by new YieldForm/EnterModal/Manager architecture.
Redux: tradeEarnInput slice
src/state/slices/tradeEarnInputSlice/*, src/state/reducer.ts, src/state/store.ts, src/state/slices/tradeEarnInputSlice/selectors.ts
Added tradeEarnInput slice, selectors (incl. selectSelectedYieldId, selectEarnHasUserEnteredAmount), reducer registration, and clear-state handling.
Types & queries
src/state/slices/actionSlice/types.ts, src/lib/yieldxyz/types.ts, src/react-queries/queries/yieldxyz/useYieldValidators.ts
Added optional chainName to action metadata, cooldownPeriod to YieldMechanics, and Cosmos validator normalization (includes SHAPESHIFT_VALIDATOR).
Translations
src/assets/translations/en/main.json
Extensive enter/exit terminology changes, new navBar.earn, earn section keys, explainers, and success/flow strings.
Misc / tests / dev
src/components/ReactTable/ReactTable.tsx, src/test/mocks/store.ts, vite.config.mts
ReactTable key/prop handling changes, updated mock store (feature flags + tradeEarnInput), added allowedHosts in Vite dev server.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as EarnInput/EarnConfirm
    participant Selector as YieldSelector
    participant API as YieldXYZ API
    participant Wallet
    participant TxFlow as useYieldTransactionFlow
    participant Success as YieldSuccess

    User->>UI: open Earn, pick asset & yield, enter amount
    UI->>Selector: request provider/yields
    Selector->>API: fetch providers & yields
    API-->>Selector: returns providers & yields
    Selector-->>UI: selected yield
    UI->>API: request quote & validators
    API-->>UI: returns quote & validators
    User->>UI: confirm -> EarnConfirm
    EarnConfirm->>Wallet: request sign / send tx
    Wallet-->>TxFlow: tx submitted (may include allowance reset)
    TxFlow->>API: poll action/tx status
    API-->>TxFlow: status updates
    TxFlow-->>EarnConfirm: final confirmation
    EarnConfirm->>Success: render success UI
    Success-->>User: show result (confetti, view position)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • shapeshift/web#11578 — Prior Earn/Yield integration and earlier yield UX that this PR builds on and refactors.
  • shapeshift/web#10461 — Also modifies src/config.ts to add VITE_ validators; related to feature-flag additions.
  • shapeshift/web#10783 — Introduced modal registration/stacking utilities referenced by refactored modal usage in this PR.

Suggested labels

high risk

Suggested reviewers

  • premiumjibles

Poem

🐰 I hopped in code to make yields bloom,
Tabs and modals now brighten the room,
Validators chosen, confetti takes flight,
Quotes signed, steps traced — transactions alight,
Hop in, stake up, and watch earnings zoom! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'feat: big batch of yield improvements' is generic and vague, lacking specific detail about the primary changes. Consider using a more descriptive title that highlights the main feature (e.g., 'feat: add Earn tab and consolidate yield improvements') or the most impactful change.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The pull request addresses all primary objectives from linked issues: Earn tab feature, bug fixes for staking, balance updates, APY display, UX improvements with tooltips, mobile layout refinements, and filter enhancements.
Out of Scope Changes check ✅ Passed Changes remain within scope of yield feature improvements. ReactTable refactoring and minor vite config changes are supporting updates for existing functionality and do not diverge from stated objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

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

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

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

⚠️ Outside diff range comments (1)
src/pages/Yields/components/YieldsList.tsx (1)

146-149: Fix TS type usage: React.ChangeEvent is referenced without importing React.

Proposed fix
-import { memo, useCallback, useMemo, useState } from 'react'
+import { memo, useCallback, useMemo, useState } from 'react'
+import type { ChangeEvent } from 'react'
@@
-  const handleSearchChange = useCallback(
-    (e: React.ChangeEvent<HTMLInputElement>) => setSearchQuery(e.target.value),
+  const handleSearchChange = useCallback(
+    (e: ChangeEvent<HTMLInputElement>) => setSearchQuery(e.target.value),
     [],
   )
🤖 Fix all issues with AI agents
In `@src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx`:
- Around line 186-200: The SharedConfirm component type includes an unused prop
isLoading (in SharedConfirmProps) that should be removed; update the
SharedConfirmProps type to delete the isLoading field and then remove the
isLoading={...} prop from all SharedConfirm usages in EarnConfirm (the three
call sites that pass isLoading), ensuring you only delete the prop from the JSX
and not other props or logic—leave SharedConfirm, EarnConfirm, and related
handlers (e.g., handleBack) unchanged.

In `@src/components/MultiHopTrade/components/Earn/types.ts`:
- Around line 1-4: EarnRoutePaths currently defines Confirm as an absolute path
which mismatches the routing pattern used elsewhere; change the enum value of
EarnRoutePaths.Confirm from an absolute path to the relative segment "confirm"
so it aligns with EarnRoutePaths.Input and the Route usage in EarnTab.tsx (Route
path='confirm' element={earnConfirmElement}). Ensure only EarnRoutePaths.Confirm
is updated to the relative string while leaving EarnRoutePaths.Input as '/earn'.

In `@src/pages/Yields/components/YieldEnterModal.tsx`:
- Around line 312-316: The effect currently closes the modal immediately when
step === ModalStep.Success (useEffect watching [step, handleModalClose]), which
prevents the successContent (rendered when isSuccess is true) from ever being
seen; either remove this useEffect or change it to delay calling
handleModalClose (e.g., setTimeout) so users can view successContent, or if
immediate close is intended, remove the now-dead successContent/isSuccess
rendering blocks and any related success UI code; update references to
ModalStep.Success, handleModalClose, isSuccess, and successContent accordingly.

In `@src/pages/Yields/components/YieldForm.tsx`:
- Around line 99-105: The TSX type reference React.ChangeEvent in the
CryptoAmountInputProps type is used without importing React; fix by importing
the ChangeEvent type (or React) from 'react' and update the type signature to
use the imported symbol (e.g., ChangeEvent<HTMLInputElement>) so the compiler
can resolve React.ChangeEvent; ensure the file imports from 'react' and adjust
the onChange prop in CryptoAmountInputProps accordingly.

In `@src/pages/Yields/components/YieldItem.tsx`:
- Around line 219-222: GradientApy is a Chakra Text-based component and the prop
itemProp='medium' is a typo; replace the invalid prop with fontWeight='medium'
on the GradientApy element (look for the GradientApy usage that currently passes
itemProp, e.g., the instance rendering {apyFormatted}) so it uses a valid
TextProps property.

In `@src/pages/Yields/components/YieldsList.tsx`:
- Around line 268-314: The recommendedYields calculation currently falls back to
precision = assets[assetId]?.precision ?? 18 which can miscompute human-readable
balances; update the eligibleYields logic inside the useMemo (recommendedYields)
to skip assets when assets[assetId] or assets[assetId].precision is undefined
(use continue) instead of defaulting to 18 before calling
fromBaseUnit(baseBalance, precision), so the minimum-deposit check uses only
assets with known precision and avoids incorrect eligibility decisions.

In `@src/pages/Yields/hooks/useYieldTransactionFlow.ts`:
- Around line 86-103: In waitForTransactionConfirmation the poll completeness
predicate currently treats a missing tx as confirmed because tx?.status !==
TransactionStatus.Created is true when tx is undefined; update the predicate to
only return true when tx exists and its status is not Created (e.g., Boolean(tx
&& tx.status !== TransactionStatus.Created)). Also update the error/completion
checker (the third callback) to handle tx === undefined explicitly and return a
descriptive Error('Transaction not found') (or keep as not-found handling you
prefer) before checking tx.status === TransactionStatus.Failed, so missing
transactions don't prematurely stop polling as "confirmed."
- Around line 278-331: The allowance query is currently enabled for any 'enter'
action with an approvalSpender when the feature flag is on, but it should only
run for USDT on Ethereum mainnet; update the useAllowance call (and its
isDisabled logic) to also require isUsdtOnEthereumMainnet(inputTokenAssetId,
yieldChainId) before enabling polling (keep other guards: action==='enter',
isUsdtApprovalResetEnabled and approvalSpender), so change the isDisabled
expression in the useAllowance invocation to include
!isUsdtOnEthereumMainnet(inputTokenAssetId, yieldChainId).

In `@src/pages/Yields/YieldDetail.tsx`:
- Around line 91-100: Rename the returned object key `usd` to a clearer name
like `userCurrency` (or `displayCurrency`) in the useMemo block that computes
totalUsd (the block using totalUsd.times(userCurrencyToUsdRate)), so the value
reflects it's in the user's selected currency; then update all call sites that
access this memoized result (replace references to `.usd` with `.userCurrency`
or the chosen name). Ensure you update the return object in YieldDetail and any
destructurings or property accesses elsewhere to use the new key.

In `@src/state/slices/tradeEarnInputSlice/selectors.ts`:
- Around line 38-41: Remove the redundant selector export
selectEarnHasUserEnteredAmount from selectors.ts and replace its usages with the
base selector selectHasUserEnteredAmount: delete the createSelector block for
selectEarnHasUserEnteredAmount, update any imports (notably in EarnInput.tsx) to
import selectHasUserEnteredAmount instead of selectEarnHasUserEnteredAmount, and
update the selector reference in EarnInput.tsx to use selectHasUserEnteredAmount
so the shared implementation is used rather than a duplicate wrapper.
🧹 Nitpick comments (19)
src/pages/Yields/components/YieldViewHelpers.tsx (1)

22-37: Responsive layout looks good.

The simplified ButtonGroup with responsive width and flex properties correctly handles mobile (full-width buttons) and desktop (inline auto-width) layouts.

Optional: The aria-labels on lines 24 and 31 are hardcoded English strings. For consistency with ListHeader which uses translate() for all text, consider using translation keys here as well to support i18n for screen reader users.

♻️ Optional: Use translation keys for aria-labels
 <ButtonGroup size='md' isAttached variant='outline' width={{ base: 'full', md: 'auto' }}>
   <IconButton
-    aria-label='Grid View'
+    aria-label={translate('yieldXYZ.gridView')}
     icon={gridIcon}
     onClick={handleSetGridView}
     isActive={isGridActive}
     flex={{ base: 1, md: 'none' }}
   />
   <IconButton
-    aria-label='List View'
+    aria-label={translate('yieldXYZ.listView')}
     icon={listIcon}
     onClick={handleSetListView}
     isActive={isListActive}
     flex={{ base: 1, md: 'none' }}
   />
 </ButtonGroup>

This would require adding translation keys and passing translate from useTranslate().

src/pages/Yields/components/YieldOpportunityStats.tsx (1)

218-271: Consider extracting repeated tooltip pattern.

The info tooltip pattern is repeated three times with the same structure. Consider extracting into a small helper component for reduced duplication.

♻️ Optional: Extract InfoTooltip helper
const InfoTooltip = memo(function InfoTooltip({ 
  label, 
  color = 'purple.300' 
}: { 
  label: string
  color?: string 
}) {
  return (
    <Tooltip label={label} hasArrow>
      <Box as='span' cursor='help'>
        <Icon as={FaInfoCircle} boxSize={3} color={color} />
      </Box>
    </Tooltip>
  )
})

Usage:

<InfoTooltip label={translate('yieldXYZ.availableToEarnTooltip')} />
src/pages/Yields/components/YieldHero.tsx (2)

55-56: Consider reading validator directly without useMemo.

The searchParams object from useSearchParams should be stable when unchanged, but searchParams.get('validator') is a simple string lookup. You could simplify to:

const validator = searchParams.get('validator')

This is a minor optimization since the memoization overhead may exceed the getter cost.


227-240: Redundant opacity when isDisabled is set.

Chakra's Button component already applies reduced opacity when isDisabled={true}. The manual opacity prop may conflict with or override the built-in disabled styling.

Suggested simplification
           <Button
             leftIcon={exitIcon}
             variant='outline'
             size='lg'
             height={14}
             borderRadius='xl'
             onClick={handleExit}
             flex={1}
             fontWeight='bold'
             isDisabled={!hasExitBalance}
-            opacity={!hasExitBalance ? 0.6 : 1}
           >
             {exitLabel}
           </Button>
src/components/MultiHopTrade/components/Earn/components/EarnFooter.tsx (2)

76-129: Consider extracting icon elements to avoid recreating them on each call.

The getYieldExplainers function creates new Icon and InfoIcon JSX elements on every invocation. Since these are static and don't depend on props, consider defining them as constants outside the function.

♻️ Suggested refactor
+const swapIcon = <Icon as={MdSwapHoriz} color='text.subtle' />
+const giftIcon = <Icon as={FaGift} color='text.subtle' />
+const infoIcon = <InfoIcon color='text.subtle' />
+
 const getYieldExplainers = (selectedYield: AugmentedYieldDto): ExplainerItem[] => {
   const yieldType = selectedYield.mechanics.type
   const outputTokenSymbol = selectedYield.outputToken?.symbol

   switch (yieldType) {
     case 'liquid-staking':
       return [
         {
-          icon: <Icon as={MdSwapHoriz} color='text.subtle' />,
+          icon: swapIcon,
           textKey: outputTokenSymbol
             ? 'earn.explainers.liquidStakingReceive'
             : 'earn.explainers.liquidStakingTrade',
         },
         {
-          icon: <Icon as={FaGift} color='text.subtle' />,
+          icon: giftIcon,
           textKey: 'earn.explainers.rewardsSchedule',
         },
         {
-          icon: <InfoIcon color='text.subtle' />,
+          icon: infoIcon,
           textKey: 'earn.explainers.liquidStakingWithdraw',
         },
       ]
     // ... apply similar changes to other cases

284-293: Potential empty string rendering for yield type.

When selectedYield.mechanics.type is an empty string, this condition is truthy but the text will render an empty string after the replace(). Consider adding a more explicit check.

♻️ Suggested fix
-              {selectedYield.mechanics.type && (
+              {selectedYield.mechanics.type ? (
                 <Flex justify='space-between' align='center' mt={3}>
                   <Text fontSize='sm' color='text.subtle'>
                     {translate('earn.yieldType')}
                   </Text>
                   <Text fontSize='sm' fontWeight='medium' textTransform='capitalize'>
                     {selectedYield.mechanics.type.replace(/-/g, ' ')}
                   </Text>
                 </Flex>
-              )}
+              ) : null}
src/react-queries/queries/yieldxyz/useYieldValidators.ts (1)

33-36: Consider applying ensureValidatorApr consistently.

When existingShapeshift?.rewardRate?.total is truthy (line 15-16), the shapeshiftValidator bypasses ensureValidatorApr. While this should be fine since it has a total, for defensive consistency you might consider applying the same normalization. That said, the current logic is functionally correct.

♻️ Optional: Apply ensureValidatorApr uniformly
 const normalizeCosmosValidators = (validators: ValidatorDto[]): ValidatorDto[] => {
   const existingShapeshift = validators.find(v => v.address === SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS)

-  const shapeshiftValidator: ValidatorDto = existingShapeshift?.rewardRate?.total
-    ? { ...existingShapeshift, preferred: true }
-    : { ...SHAPESHIFT_VALIDATOR, ...existingShapeshift, preferred: true }
+  const baseShapeshiftValidator: ValidatorDto = existingShapeshift?.rewardRate?.total
+    ? existingShapeshift
+    : { ...SHAPESHIFT_VALIDATOR, ...existingShapeshift }
+
+  const shapeshiftValidator = ensureValidatorApr({ ...baseShapeshiftValidator, preferred: true })

   const otherValidators = validators
     .filter(v => v.address !== SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS)
     .map(v => ensureValidatorApr({ ...v, preferred: false }))

   return [shapeshiftValidator, ...otherValidators]
 }
src/pages/Yields/components/YieldManager.tsx (1)

17-29: Consider narrowing the action type more defensively.

The type assertion on line 23 casts arbitrary URL input to a union type. If a user navigates directly with an invalid action parameter, TypeScript won't catch it at runtime, though line 69 provides a sensible fallback to 'enter'.

This is a minor concern since the fallback handles the edge case gracefully.

♻️ Optional: Add runtime validation
+ const VALID_ACTIONS = ['enter', 'exit', 'claim'] as const
+ type YieldAction = (typeof VALID_ACTIONS)[number]
+ 
+ const rawAction = searchParams.get('action')
+ const action: YieldAction | undefined = rawAction && VALID_ACTIONS.includes(rawAction as YieldAction)
+   ? (rawAction as YieldAction)
+   : undefined
- const action = searchParams.get('action') as 'enter' | 'exit' | 'claim' | undefined
src/pages/Trade/tabs/EarnTab.tsx (1)

82-84: Consider removing unnecessary useMemo.

The title computation is a simple translation lookup that doesn't benefit from memoization. The translate function call is lightweight and useMemo adds overhead here.

Suggested simplification
-  const title = useMemo(() => {
-    return translate('navBar.earn')
-  }, [translate])
+  const title = translate('navBar.earn')
src/components/MultiHopTrade/components/Earn/components/YieldSelector.tsx (1)

110-116: Consider using a local fallback asset instead of external URL.

The hardcoded CoinGecko fallback URL (https://assets.coingecko.com/...) creates an external dependency that could fail if the service is unavailable or changes the URL structure. Consider using a local placeholder asset from the app's assets.

Also applies to: 225-231

src/pages/Yields/YieldAssetDetails.tsx (2)

45-45: Consider using Chakra's theme breakpoint instead of hardcoded pixel value.

The hardcoded '(max-width: 768px)' could diverge from Chakra's md breakpoint if the theme is customized. Consider using useBreakpointValue for consistency with the design system.

Alternative using useBreakpointValue
-import {
-  ...
-  useMediaQuery,
-} from '@chakra-ui/react'
+import {
+  ...
+  useBreakpointValue,
+} from '@chakra-ui/react'

-  const [isMobile] = useMediaQuery('(max-width: 768px)')
+  const isMobile = useBreakpointValue({ base: true, md: false }, { ssr: false })

440-463: Mobile filter hiding could use responsive display object.

The display={isMobile ? 'none' : 'flex'} pattern works, but Chakra's responsive object syntax (display={{ base: 'none', md: 'flex' }}) would be more idiomatic and wouldn't require the useMediaQuery hook if that's the only usage.

src/components/MultiHopTrade/components/Earn/EarnInput.tsx (1)

133-140: URL navigation triggers on every input change.

This effect navigates to a new URL on every change to sellAmountCryptoBaseUnit, which includes each keystroke in the amount input. This could cause excessive history entries and performance overhead.

Consider debouncing this navigation or only navigating on blur/submit.

Proposed debounced navigation
+  const debouncedSellAmountCryptoBaseUnit = useDebounce(sellAmountCryptoBaseUnit, 500)
+
   useEffect(() => {
     if (!sellAsset.assetId || !selectedYieldId) return

     const encodedYieldId = encodeURIComponent(selectedYieldId)
-    const baseUnit = sellAmountCryptoBaseUnit ?? '0'
+    const baseUnit = debouncedSellAmountCryptoBaseUnit ?? '0'

     navigate(`/earn/${sellAsset.assetId}/${encodedYieldId}/${baseUnit}`, { replace: true })
-  }, [sellAsset.assetId, selectedYieldId, sellAmountCryptoBaseUnit, navigate])
+  }, [sellAsset.assetId, selectedYieldId, debouncedSellAmountCryptoBaseUnit, navigate])
src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx (1)

35-36: The empty object fallback is safe due to the isOpen guard, but the type assertion is unnecessary complexity.

The yieldItem: selectedYield ?? defaultYieldItem pattern with isOpen: Boolean(selectedYield) ensures the hook never executes when the empty object is passed, since the query is only enabled when isOpen is true (which correlates with selectedYield being defined). The hook also guards against an incomplete yieldItem with checks like if (!yieldItem || !userAddress || !yieldChainId) return null in txArguments.

However, this pattern introduces unnecessary complexity. Since the hook already has proper guards and the isOpen flag controls execution, consider whether the hook can accept yieldItem as optional (AugmentedYieldDto | undefined) to eliminate the type assertion entirely.

src/pages/Yields/components/YieldForm.tsx (1)

402-407: Remove the no-op useEffect.

It currently does nothing and can be dropped to reduce noise.

src/pages/Yields/components/YieldAssetSection.tsx (1)

44-129: Modal flow looks good; small state simplification is possible.

isEnterModalOpen could be derived from selectedYield !== null (or vice-versa) to avoid two sources of truth, but current logic is fine.

src/pages/Yields/components/YieldActionModal.tsx (3)

176-195: Variable shadowing: step shadows the ModalStep state variable.

On line 179, the local const step = transactionSteps[activeStepIndex] shadows the step variable from useYieldTransactionFlow (line 61). While this doesn't cause a bug since the outer step is of type ModalStep and the inner is a transaction step object, it can cause confusion during maintenance.

♻️ Suggested fix: rename the local variable
     if (activeStepIndex >= 0 && transactionSteps[activeStepIndex]) {
-      const step = transactionSteps[activeStepIndex]
-      return getTransactionButtonText(step.type, step.originalTitle)
+      const currentStep = transactionSteps[activeStepIndex]
+      return getTransactionButtonText(currentStep.type, currentStep.originalTitle)
     }

208-208: Consider memoizing assetFlowDirection.

Per coding guidelines, conditional values should be wrapped in useMemo. While this is a simple ternary, wrapping it maintains consistency with the rest of the component.

♻️ Suggested fix
-  const assetFlowDirection = action === 'exit' ? 'exit' : 'enter'
+  const assetFlowDirection = useMemo(
+    () => (action === 'exit' ? 'exit' : 'enter'),
+    [action],
+  )

340-343: Unreachable null branch: vaultMetadata is always truthy.

The vaultMetadata computed on lines 102-111 always returns an object (the final fallback is { name: 'Vault', logoURI: yieldItem.metadata.logoURI }). The ternary's null branch is unreachable.

♻️ Suggested simplification
   const successProviderInfo = useMemo(
-    () => (vaultMetadata ? { name: vaultMetadata.name, logoURI: vaultMetadata.logoURI } : null),
+    () => ({ name: vaultMetadata.name, logoURI: vaultMetadata.logoURI }),
     [vaultMetadata],
   )

Comment on lines +54 to +70
const getYieldTypeName = (type: string): string => {
const typeMap: Record<string, string> = {
'native-staking': 'Native Staking',
'pooled-staking': 'Pooled Staking',
'liquid-staking': 'Liquid Staking',
staking: 'Staking',
lending: 'Lending',
vault: 'Vault',
restaking: 'Restaking',
}
if (typeMap[type]) return typeMap[type]
return type
.split('-')
.filter((word): word is string => Boolean(word))
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded English strings should use translation keys.

The getYieldTypeName function contains hardcoded English display names. Per coding guidelines, all user-facing text must use translation keys for i18n support.

Suggested approach
-const getYieldTypeName = (type: string): string => {
-  const typeMap: Record<string, string> = {
-    'native-staking': 'Native Staking',
-    'pooled-staking': 'Pooled Staking',
-    'liquid-staking': 'Liquid Staking',
-    staking: 'Staking',
-    lending: 'Lending',
-    vault: 'Vault',
-    restaking: 'Restaking',
-  }
-  if (typeMap[type]) return typeMap[type]
-  return type
-    .split('-')
-    .filter((word): word is string => Boolean(word))
-    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
-    .join(' ')
-}
+const getYieldTypeName = (type: string, translate: (key: string) => string): string => {
+  const typeKey = `yieldXYZ.types.${type.replace(/-/g, '_')}`
+  const translated = translate(typeKey)
+  if (translated !== typeKey) return translated
+  return type
+    .split('-')
+    .filter((word): word is string => Boolean(word))
+    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
+    .join(' ')
+}

Then add corresponding translation keys in main.json.

Comment on lines +186 to +200
if (!selectedYield) {
return (
<SharedConfirm
bodyContent={
<VStack spacing={4} p={6} flex={1} justify='center'>
<Text>{translate('earn.selectYieldOpportunity')}</Text>
<Button onClick={handleBack}>{translate('common.goBack')}</Button>
</VStack>
}
footerContent={null}
isLoading={false}
onBack={handleBack}
headerTranslation='earn.confirmEarn'
/>
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find SharedConfirm component definition
fd -t f SharedConfirm.tsx

Repository: shapeshift/web

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Search for SharedConfirm type/interface definition across the codebase
rg 'type SharedConfirmProps|interface SharedConfirmProps|const SharedConfirm.*=|function SharedConfirm' -A 10 -t tsx -t ts

Repository: shapeshift/web

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Read the SharedConfirm component to see its props interface
cat -n src/components/MultiHopTrade/components/SharedConfirm/SharedConfirm.tsx

Repository: shapeshift/web

Length of output: 2309


🏁 Script executed:

#!/bin/bash
# Check the specific lines in EarnConfirm.tsx where isLoading is allegedly passed
sed -n '186,223p' src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx | cat -n
sed -n '340,350p' src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx | cat -n

Repository: shapeshift/web

Length of output: 1701


Remove unused isLoading prop from SharedConfirm.

The isLoading prop is defined in SharedConfirmProps but is not destructured or used in the component implementation. Remove it from the type definition on line 15 of SharedConfirm.tsx and the three call sites in EarnConfirm.tsx (lines 196, 219, 344).

🤖 Prompt for AI Agents
In `@src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx` around lines
186 - 200, The SharedConfirm component type includes an unused prop isLoading
(in SharedConfirmProps) that should be removed; update the SharedConfirmProps
type to delete the isLoading field and then remove the isLoading={...} prop from
all SharedConfirm usages in EarnConfirm (the three call sites that pass
isLoading), ensuring you only delete the prop from the JSX and not other props
or logic—leave SharedConfirm, EarnConfirm, and related handlers (e.g.,
handleBack) unchanged.

Comment on lines +1 to +4
export enum EarnRoutePaths {
Input = '/earn',
Confirm = '/earn/confirm',
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine route path enum definitions

echo "=== Searching for TradeRoutePaths ==="
rg -n "enum TradeRoutePaths" --type=ts -A 5

echo ""
echo "=== Searching for LimitOrderRoutePaths ==="
rg -n "enum LimitOrderRoutePaths" --type=ts -A 5

echo ""
echo "=== Searching for FiatRampRoutePaths ==="
rg -n "enum FiatRampRoutePaths" --type=ts -A 5

echo ""
echo "=== Searching for EarnRoutePaths usage in codebase ==="
rg "EarnRoutePaths\." --type=ts -B 2 -A 2 | head -50

Repository: shapeshift/web

Length of output: 4507


🏁 Script executed:

#!/bin/bash
# Check routing configuration for Earn component

echo "=== Searching for Earn route definitions in routing config ==="
rg -n "EarnRoutePaths\|/earn" --type=ts src/components/MultiHopTrade -B 2 -A 2 | grep -E "(Route|path|navigate)" | head -50

echo ""
echo "=== Check how MultiHopTrade routes are structured ==="
fd ".*Route.*" --type ts src/components/MultiHopTrade | head -20

echo ""
echo "=== Examine MultiHopTrade routing structure ==="
find src/components/MultiHopTrade -name "*Route*" -o -name "*routes*" | head -20

Repository: shapeshift/web

Length of output: 476


🏁 Script executed:

#!/bin/bash
# Search for route registration and usage patterns

echo "=== Search for route registration in MultiHopTrade ==="
rg "Route|path=" src/components/MultiHopTrade --type-list | head -5
fd . src/components/MultiHopTrade -name "*.tsx" -o -name "*.ts" | xargs rg -l "Route.*EarnRoutePaths|EarnRoutePaths.*Route" 2>/dev/null | head -10

echo ""
echo "=== Check MultiHopTrade main file ==="
cat -n src/components/MultiHopTrade/MultiHopTrade.tsx | head -100

echo ""
echo "=== Look for route configuration patterns ==="
rg "Routes\|<Route" src/components/MultiHopTrade/*.tsx 2>/dev/null | head -20

Repository: shapeshift/web

Length of output: 5667


🏁 Script executed:

#!/bin/bash
# Search for route configuration

echo "=== Continue reading MultiHopTrade.tsx to see route setup ==="
cat -n src/components/MultiHopTrade/MultiHopTrade.tsx | tail -150 | head -100

echo ""
echo "=== Search for Route elements with paths ==="
rg "path=" src/components/MultiHopTrade/MultiHopTrade.tsx -A 1 -B 1

echo ""
echo "=== Check Earn routing setup ==="
rg "EarnRoutePaths|earn" src/components/MultiHopTrade/MultiHopTrade.tsx -B 2 -A 2

Repository: shapeshift/web

Length of output: 5501


🏁 Script executed:

#!/bin/bash
# Search for Earn route configuration

echo "=== Search for Earn routes setup ==="
rg "Earn" src/components/MultiHopTrade/MultiHopTrade.tsx

echo ""
echo "=== Check RoutesCommon for Earn route definitions ==="
rg "earn" src/Routes/RoutesCommon.tsx -B 3 -A 3

echo ""
echo "=== Look for where Earn component routes are defined ==="
fd "Earn" src/components/MultiHopTrade --type f | xargs cat | grep -n "Route\|path" | head -30

Repository: shapeshift/web

Length of output: 1019


🏁 Script executed:

#!/bin/bash
# Look for the actual route definitions for Earn

echo "=== Search for where Earn routes are defined in MultiHopTrade.tsx ==="
wc -l src/components/MultiHopTrade/MultiHopTrade.tsx

echo ""
echo "=== Look at the full Routes section ==="
rg "Routes" src/components/MultiHopTrade/MultiHopTrade.tsx -A 50 | head -100

echo ""
echo "=== Check if there's a separate Earn routing component ==="
find src/components/MultiHopTrade/components/Earn -type f -name "*.tsx" -o -name "*.ts" | xargs ls -la

Repository: shapeshift/web

Length of output: 4930


🏁 Script executed:

#!/bin/bash
# Search for Earn route nesting

echo "=== Get the full MultiHopTrade.tsx file to see Routes setup ==="
cat src/components/MultiHopTrade/MultiHopTrade.tsx | tail -80

echo ""
echo "=== Search for Earn component integration ==="
rg "Earn" src/components/MultiHopTrade/MultiHopTrade.tsx -B 5 -A 5

echo ""
echo "=== Look for nested route setup in Earn ==="
fd . src/components/MultiHopTrade/components/Earn -type f | xargs rg "Route" -l

Repository: shapeshift/web

Length of output: 25309


🏁 Script executed:

#!/bin/bash
# Check how Earn routes are actually used

echo "=== Search for Route definitions with EarnRoutePaths ==="
rg "path.*earn|EarnRoutePaths" src/components/MultiHopTrade/components/Earn -B 3 -A 3

echo ""
echo "=== Check EarnTab.tsx for routing ==="
rg "EarnRoutePaths\|Route\|Routes" src/pages/Trade/tabs/EarnTab.tsx -B 2 -A 2

echo ""
echo "=== Look for where Earn routes might be nested ==="
rg "Routes" src/pages/Trade/tabs/EarnTab.tsx -B 5 -A 15

Repository: shapeshift/web

Length of output: 6065


Use relative path for EarnRoutePaths.Confirm to match the established pattern.

The Route definition in EarnTab.tsx uses relative paths (<Route path='confirm' element={earnConfirmElement} />), but EarnRoutePaths defines absolute paths. This diverges from the pattern established by TradeRoutePaths and LimitOrderRoutePaths, which use absolute paths only for Input routes and relative paths for sub-routes like Confirm. Update EarnRoutePaths.Confirm = 'confirm' to align with the consistent pattern.

🤖 Prompt for AI Agents
In `@src/components/MultiHopTrade/components/Earn/types.ts` around lines 1 - 4,
EarnRoutePaths currently defines Confirm as an absolute path which mismatches
the routing pattern used elsewhere; change the enum value of
EarnRoutePaths.Confirm from an absolute path to the relative segment "confirm"
so it aligns with EarnRoutePaths.Input and the Route usage in EarnTab.tsx (Route
path='confirm' element={earnConfirmElement}). Ensure only EarnRoutePaths.Confirm
is updated to the relative string while leaving EarnRoutePaths.Input as '/earn'.

Comment on lines +237 to +241
if (isFiat) {
const crypto = bnOrZero(values.value)
.div(marketData?.price ?? 1)
.toFixed()
setCryptoAmount(crypto)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Division by zero possible when price is 0.

marketData?.price ?? 1 only falls back to 1 when price is null or undefined. If price is 0, the nullish coalescing operator returns 0, causing division by zero.

Proposed fix
       if (isFiat) {
         const crypto = bnOrZero(values.value)
-          .div(marketData?.price ?? 1)
+          .div(marketData?.price || 1)
           .toFixed()
         setCryptoAmount(crypto)

Comment on lines +268 to +314
const recommendedYields = useMemo(() => {
if (!isConnected || !yields?.byInputAssetId || !userCurrencyBalances || !assetBalancesBaseUnit)
return []

const recommendations: {
yield: AugmentedYieldDto
balanceFiat: ReturnType<typeof bnOrZero>
potentialEarnings: ReturnType<typeof bnOrZero>
}[] = []

for (const [assetId, balanceFiat] of Object.entries(userCurrencyBalances)) {
const yieldsForAsset = yields.byInputAssetId[assetId]
if (!yieldsForAsset?.length) continue

const balance = bnOrZero(balanceFiat)
if (balance.lte(0)) continue

// Filter to only include yields where user meets the minimum requirement
const eligibleYields = yieldsForAsset.filter(y => {
const minDeposit = bnOrZero(y.mechanics?.entryLimits?.minimum)
if (minDeposit.lte(0)) return true

// minDeposit is human readable (e.g. 32), so convert base balance to human
const precision = assets[assetId]?.precision ?? 18
const baseBalance = bnOrZero(assetBalancesBaseUnit[assetId])
const balanceHuman = bnOrZero(fromBaseUnit(baseBalance, precision))
return balanceHuman.gte(minDeposit)
})

if (!eligibleYields.length) continue

const bestYield = eligibleYields.reduce((best, current) =>
current.rewardRate.total > best.rewardRate.total ? current : best,
)

recommendations.push({
yield: bestYield,
balanceFiat: balance,
potentialEarnings: balance.times(bestYield.rewardRate.total),
})
}

return recommendations
.sort((a, b) => b.potentialEarnings.minus(a.potentialEarnings).toNumber())
.slice(0, 3)
}, [isConnected, yields?.byInputAssetId, userCurrencyBalances, assetBalancesBaseUnit, assets])

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find fromBaseUnit definition and check its expectations
rg -n "export const fromBaseUnit|function fromBaseUnit|fromBaseUnit.*=" src packages --type ts --type tsx -B2 -A5 | head -100

Repository: shapeshift/web

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Get callsites to understand typical usage patterns
rg -n "fromBaseUnit\(" src packages --type ts --type tsx -C2 | head -150

Repository: shapeshift/web

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Check what assets are used in yields context and how precision varies
rg -n "precision\s*\?" src/pages/Yields --type ts --type tsx -B3 -A3

Repository: shapeshift/web

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Look at how assets are loaded/defined in the Yields context
rg -n "useAppSelector.*assets|const assets.*=" src/pages/Yields --type ts --type tsx -B2 -A5 | head -100

Repository: shapeshift/web

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Find fromBaseUnit definition
rg "export const fromBaseUnit|function fromBaseUnit" -B2 -A8 src packages

Repository: shapeshift/web

Length of output: 1616


🏁 Script executed:

#!/bin/bash
# Find fromBaseUnit usage patterns
rg "fromBaseUnit\(" -B1 -A1 src packages | head -200

Repository: shapeshift/web

Length of output: 15055


🏁 Script executed:

#!/bin/bash
# Check the specific file mentioned in the review
head -350 src/pages/Yields/components/YieldsList.tsx | tail -100

Repository: shapeshift/web

Length of output: 3510


🏁 Script executed:

#!/bin/bash
# Look for asset precision patterns and how missing assets are handled
rg "assets\[.*\].*precision|precision.*\?" src/pages/Yields -B2 -A2

Repository: shapeshift/web

Length of output: 561


🏁 Script executed:

#!/bin/bash
# Check how assets object is populated in yields context
rg "const assets.*=" src/pages/Yields -B5 -A5 | head -80

Repository: shapeshift/web

Length of output: 923


🏁 Script executed:

#!/bin/bash
# Check if there are any patterns in the codebase where precision 18 is used as fallback
rg "precision.*\?\?\s*18|precision.*18\s*\?" src | head -50

Repository: shapeshift/web

Length of output: 753


🏁 Script executed:

#!/bin/bash
# See if there are tests showing the impact of wrong precision
rg -n "fromBaseUnit.*18" packages/utils/src/baseUnits/ -B3 -A3

Repository: shapeshift/web

Length of output: 1126


🏁 Script executed:

#!/bin/bash
# Check selectAssets to understand what data it contains and if it can have gaps
rg -n "selectAssets" src/state/slices -B5 -A10 | head -100

Repository: shapeshift/web

Length of output: 8433


🏁 Script executed:

#!/bin/bash
# Look at asset precision in the codebase to see common values
rg "\.precision" src -o | sort | uniq -c | sort -rn | head -20

Repository: shapeshift/web

Length of output: 1992


🏁 Script executed:

#!/bin/bash
# Check if there are any tests for precision fallback scenarios
rg "precision.*fallback|fallback.*precision" --type-list

Repository: shapeshift/web

Length of output: 447


🏁 Script executed:

#!/bin/bash
# Check the common-selectors pattern to see how they handle missing precision
rg -A10 "if (precision === undefined)" src/state/slices/common-selectors.ts

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Look for how yields typically handle missing asset data
rg "assets\[.*\]" src/pages/Yields -B2 -A2 | head -100

Repository: shapeshift/web

Length of output: 561


🏁 Script executed:

#!/bin/bash
# Check if selectAssets can return incomplete data
rg -A20 "export const selectAssets" src/state/slices/assetsSlice/selectors.ts

Repository: shapeshift/web

Length of output: 2202


🏁 Script executed:

#!/bin/bash
# Look at how portfolio selectors safely handle missing assets
rg -B5 -A10 "if (asset)" src/state/slices/portfolioSlice/selectors.ts | head -80

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check selectAssets definition to see if it can have missing entries
rg -B5 -A10 "selectAssetsById" src/state/slices/assetsSlice/selectors.ts | head -50

Repository: shapeshift/web

Length of output: 1726


🏁 Script executed:

#!/bin/bash
# Look at other places where precision ?? 18 is used to understand if this pattern is widespread
rg "precision.*\?\?\s*18|precision.*??\s*18" -B3 -A3 src pages

Repository: shapeshift/web

Length of output: 18852


🏁 Script executed:

#!/bin/bash
# Check if assetBalancesBaseUnit can have keys that aren't in assets object
rg -B10 -A5 "assetBalancesBaseUnit" src/pages/Yields/components/YieldsList.tsx

Repository: shapeshift/web

Length of output: 2304


Skip yield eligibility check when asset metadata is missing instead of defaulting precision to 18.

Using precision ?? 18 as a fallback can incorrectly mark yields as eligible or ineligible because fromBaseUnit(baseBalance, precision) depends critically on the correct precision value. If an asset is missing from the asset registry but present in the user's balance, defaulting to 18 assumes it's an EVM token—which may not be true. This produces incorrect human-readable balance calculations, breaking the minimum deposit eligibility check. Instead, skip the asset with continue when precision is unknown.

🤖 Prompt for AI Agents
In `@src/pages/Yields/components/YieldsList.tsx` around lines 268 - 314, The
recommendedYields calculation currently falls back to precision =
assets[assetId]?.precision ?? 18 which can miscompute human-readable balances;
update the eligibleYields logic inside the useMemo (recommendedYields) to skip
assets when assets[assetId] or assets[assetId].precision is undefined (use
continue) instead of defaulting to 18 before calling fromBaseUnit(baseBalance,
precision), so the minimum-deposit check uses only assets with known precision
and avoids incorrect eligibility decisions.

Comment on lines +86 to +103
export const waitForTransactionConfirmation = (
actionId: string,
transactionId: string,
): Promise<ActionDto> =>
poll(
() => fetchAction(actionId),
action => {
const tx = action.transactions.find(t => t.id === transactionId)
return tx?.status !== TransactionStatus.Created
},
action => {
if (action.status === YieldActionStatus.Failed) return new Error('Action failed')
if (action.status === YieldActionStatus.Canceled) return new Error('Action was canceled')
const tx = action.transactions.find(t => t.id === transactionId)
if (tx?.status === TransactionStatus.Failed) return new Error('Transaction failed')
return undefined
},
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix polling completion: treat “tx not found” as not-confirmed (or an error), not complete.

Right now tx?.status !== TransactionStatus.Created evaluates to true when tx is undefined, so polling can “confirm” a non-existent transaction.

Proposed fix
 export const waitForTransactionConfirmation = (
   actionId: string,
   transactionId: string,
 ): Promise<ActionDto> =>
   poll(
     () => fetchAction(actionId),
     action => {
       const tx = action.transactions.find(t => t.id === transactionId)
-      return tx?.status !== TransactionStatus.Created
+      return Boolean(tx && tx.status !== TransactionStatus.Created)
     },
     action => {
       if (action.status === YieldActionStatus.Failed) return new Error('Action failed')
       if (action.status === YieldActionStatus.Canceled) return new Error('Action was canceled')
       const tx = action.transactions.find(t => t.id === transactionId)
       if (tx?.status === TransactionStatus.Failed) return new Error('Transaction failed')
       return undefined
     },
   )
🤖 Prompt for AI Agents
In `@src/pages/Yields/hooks/useYieldTransactionFlow.ts` around lines 86 - 103, In
waitForTransactionConfirmation the poll completeness predicate currently treats
a missing tx as confirmed because tx?.status !== TransactionStatus.Created is
true when tx is undefined; update the predicate to only return true when tx
exists and its status is not Created (e.g., Boolean(tx && tx.status !==
TransactionStatus.Created)). Also update the error/completion checker (the third
callback) to handle tx === undefined explicitly and return a descriptive
Error('Transaction not found') (or keep as not-found handling you prefer) before
checking tx.status === TransactionStatus.Failed, so missing transactions don't
prematurely stop polling as "confirmed."

Comment on lines +278 to +331
// USDT reset logic - only for enter action on USDT/ETH
const approvalSpender = useMemo(() => {
if (action !== 'enter') return null
if (!quoteData?.transactions) return null
const createdTransactions = quoteData.transactions.filter(
tx => tx.status === TransactionStatus.Created,
)
const approvalTx = createdTransactions.find(isApprovalTransaction)
if (!approvalTx) return null
return getSpenderFromApprovalTx(approvalTx)
}, [action, quoteData?.transactions])

const allowanceQuery = useAllowance({
assetId: inputTokenAssetId,
spender: approvalSpender ?? undefined,
from: userAddress || undefined,
isDisabled: !approvalSpender || !isUsdtApprovalResetEnabled || action !== 'enter',
isRefetchEnabled: true,
})

const isUsdtResetRequired = useMemo(() => {
if (action !== 'enter') return false
if (!isUsdtApprovalResetEnabled) return false
if (!isUsdtOnEthereumMainnet(inputTokenAssetId, yieldChainId)) return false
if (!approvalSpender) return false
if (!allowanceQuery.data) return false
return bnOrZero(allowanceQuery.data).gt(0)
}, [
action,
isUsdtApprovalResetEnabled,
inputTokenAssetId,
yieldChainId,
approvalSpender,
allowanceQuery.data,
])

// Check if we're waiting for USDT allowance check before we can determine reset requirement
const isAllowanceCheckPending = useMemo(() => {
if (action !== 'enter') return false
if (!isUsdtApprovalResetEnabled) return false
if (!isUsdtOnEthereumMainnet(inputTokenAssetId, yieldChainId)) return false
if (!approvalSpender) return false
// If we have an approval spender for USDT but allowance data hasn't loaded yet
return allowanceQuery.data === undefined && !allowanceQuery.isError
}, [
action,
isUsdtApprovalResetEnabled,
inputTokenAssetId,
yieldChainId,
approvalSpender,
allowanceQuery.data,
allowanceQuery.isError,
])

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Narrow allowance polling to the actual reset scenario (USDT on Ethereum mainnet).

useAllowance currently runs for any enter with an approval spender when the feature flag is on, even though the reset logic only applies to USDT on Ethereum mainnet.

Proposed fix
   const allowanceQuery = useAllowance({
     assetId: inputTokenAssetId,
     spender: approvalSpender ?? undefined,
     from: userAddress || undefined,
-    isDisabled: !approvalSpender || !isUsdtApprovalResetEnabled || action !== 'enter',
+    isDisabled:
+      !approvalSpender ||
+      !isUsdtApprovalResetEnabled ||
+      action !== 'enter' ||
+      !isUsdtOnEthereumMainnet(inputTokenAssetId, yieldChainId),
     isRefetchEnabled: true,
   })
🤖 Prompt for AI Agents
In `@src/pages/Yields/hooks/useYieldTransactionFlow.ts` around lines 278 - 331,
The allowance query is currently enabled for any 'enter' action with an
approvalSpender when the feature flag is on, but it should only run for USDT on
Ethereum mainnet; update the useAllowance call (and its isDisabled logic) to
also require isUsdtOnEthereumMainnet(inputTokenAssetId, yieldChainId) before
enabling polling (keep other guards: action==='enter',
isUsdtApprovalResetEnabled and approvalSpender), so change the isDisabled
expression in the useAllowance invocation to include
!isUsdtOnEthereumMainnet(inputTokenAssetId, yieldChainId).

Comment on lines +91 to +100
const totalUsd = [activeBalance, enteringBalance, exitingBalance, withdrawableBalance].reduce(
(sum, b) => sum.plus(bnOrZero(b?.aggregatedAmountUsd)),
bnOrZero(0),
)

return {
usd: totalUsd.times(userCurrencyToUsdRate).toFixed(),
crypto: totalCrypto.toFixed(),
}
}, [balances, selectedValidatorAddress, userCurrencyToUsdRate])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the definition of selectUserCurrencyToUsdRate and examine its implementation
rg -n 'selectUserCurrencyToUsdRate' --type=ts --type=tsx -B2 -A10 | head -100

Repository: shapeshift/web

Length of output: 85


🏁 Script executed:

# Also search for where this selector is defined
rg -n 'export.*selectUserCurrencyToUsdRate|const selectUserCurrencyToUsdRate' --type=ts --type=tsx -A15

Repository: shapeshift/web

Length of output: 85


🏁 Script executed:

# Check the actual YieldDetail.tsx file around the questionable code
cat -n src/pages/Yields/YieldDetail.tsx | sed -n '85,105p'

Repository: shapeshift/web

Length of output: 832


🏁 Script executed:

# Search for selectUserCurrencyToUsdRate definition without file type issues
rg -n 'selectUserCurrencyToUsdRate' -A10 | head -80

Repository: shapeshift/web

Length of output: 6867


🏁 Script executed:

# Find where this selector is defined
rg -n 'const selectUserCurrencyToUsdRate|export.*selectUserCurrencyToUsdRate' -A15 | head -100

Repository: shapeshift/web

Length of output: 1488


🏁 Script executed:

# Look for the selector in the slices directory (common Redux pattern)
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "selectUserCurrencyToUsdRate" | head -10

Repository: shapeshift/web

Length of output: 653


🏁 Script executed:

# Verify the fiatMarketData structure and confirm this pattern is consistent
rg -n 'fiatMarketData\[' --type=ts -A2 | head -40

Repository: shapeshift/web

Length of output: 1332


Rename the usd key to reflect user currency denomination.

The value at line 97 is correctly calculated in user currency (USD * rate = user currency amount), but the return key usd is misleading. Rename it to userCurrency or displayCurrency to accurately reflect the denomination of the returned value.

🤖 Prompt for AI Agents
In `@src/pages/Yields/YieldDetail.tsx` around lines 91 - 100, Rename the returned
object key `usd` to a clearer name like `userCurrency` (or `displayCurrency`) in
the useMemo block that computes totalUsd (the block using
totalUsd.times(userCurrencyToUsdRate)), so the value reflects it's in the user's
selected currency; then update all call sites that access this memoized result
(replace references to `.usd` with `.userCurrency` or the chosen name). Ensure
you update the return object in YieldDetail and any destructurings or property
accesses elsewhere to use the new key.

Comment on lines +38 to +41
export const selectEarnHasUserEnteredAmount = createSelector(
selectInputSellAmountCryptoPrecision,
sellAmountCryptoPrecision => bnOrZero(sellAmountCryptoPrecision).gt(0),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's see the structure of the selectors file
cat -n src/state/slices/tradeEarnInputSlice/selectors.ts

Repository: shapeshift/web

Length of output: 1748


🏁 Script executed:

#!/bin/bash
# Find and examine the createTradeInputBaseSelectors file
find . -name 'createTradeInputBaseSelectors*' -type f | head -5

Repository: shapeshift/web

Length of output: 131


🏁 Script executed:

#!/bin/bash
# Read the createTradeInputBaseSelectors file to see selectHasUserEnteredAmount implementation
cat -n ./src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts | head -100

Repository: shapeshift/web

Length of output: 4332


🏁 Script executed:

#!/bin/bash
# Continue reading to find selectHasUserEnteredAmount
cat -n ./src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts | sed -n '100,250p'

Repository: shapeshift/web

Length of output: 6324


🏁 Script executed:

#!/bin/bash
# Now let's check the usage of both selectors in the codebase
echo "=== selectEarnHasUserEnteredAmount usage ==="
rg -n 'selectEarnHasUserEnteredAmount' --type ts --type tsx -g '!*.test.*' -A 2

echo ""
echo "=== selectHasUserEnteredAmount usage in Earn/Yields context ==="
rg -n 'selectHasUserEnteredAmount' src/pages/Yields src/pages/Earn --type ts --type tsx 2>/dev/null || echo "No matches in Earn/Yields pages"

echo ""
echo "=== selectHasUserEnteredAmount usage overall ==="
rg -n 'selectHasUserEnteredAmount' --type ts --type tsx -g '!*.test.*' | head -20

Repository: shapeshift/web

Length of output: 322


🏁 Script executed:

#!/bin/bash
# Try with different approach - search for both selectors without type restrictions
echo "=== selectEarnHasUserEnteredAmount usage ==="
rg 'selectEarnHasUserEnteredAmount' -g '!*.test.ts' -g '!*.test.tsx' -A 2

echo ""
echo "=== selectHasUserEnteredAmount usage in tradeEarnInputSlice ==="
rg 'selectHasUserEnteredAmount' src/state/slices/tradeEarnInputSlice/ -g '!*.test.*' -A 2

echo ""
echo "=== selectHasUserEnteredAmount in other trade slices ==="
rg 'selectHasUserEnteredAmount' src/state/slices/tradeInputSlice/ -g '!*.test.*' -A 2

Repository: shapeshift/web

Length of output: 1688


🏁 Script executed:

#!/bin/bash
# Check if selectHasUserEnteredAmount is actually exported/used from tradeEarnInputSlice
echo "=== Check tradeEarnInputSlice/selectors.ts exports ==="
cat src/state/slices/tradeEarnInputSlice/selectors.ts | grep -A 30 'export const'

echo ""
echo "=== Check if selectHasUserEnteredAmount is ever used from tradeEarnInputSlice ==="
rg 'from.*tradeEarnInputSlice.*selectHasUserEnteredAmount' -g '!*.test.*'

echo ""
echo "=== Check if selectHasUserEnteredAmount is used in tradeInputSlice context ==="
rg 'selectHasUserEnteredAmount' src/components/MultiHopTrade/ -B 3 -A 3 | head -40

Repository: shapeshift/web

Length of output: 5982


🏁 Script executed:

#!/bin/bash
# Check the pattern - are there other Earn-specific selectors or is this unique?
echo "=== All exported selectors from tradeEarnInputSlice ==="
cat src/state/slices/tradeEarnInputSlice/selectors.ts | grep 'export const select'

echo ""
echo "=== Check if EarnInput.tsx uses any of the base selectHasUserEnteredAmount ==="
rg 'selectHasUserEnteredAmount' src/components/MultiHopTrade/components/Earn/

Repository: shapeshift/web

Length of output: 308


🏁 Script executed:

#!/bin/bash
# Let's check if there's a pattern of domain-specific selector wrappers in other slices
echo "=== Check TradeInputSlice for similar pattern ==="
cat src/state/slices/tradeInputSlice/selectors.ts | grep -A 3 'selectHasUserEnteredAmount'

echo ""
echo "=== Check LimitOrderSlice for similar pattern ==="
find . -path ./node_modules -prune -o -name '*limitOrder*selectors*' -type f -print | head -5

Repository: shapeshift/web

Length of output: 295


🏁 Script executed:

#!/bin/bash
# Let's verify if selectHasUserEnteredAmount from the destructured export is actually exported
echo "=== Verify exports from tradeEarnInputSlice/selectors.ts ==="
node -e "
const content = require('fs').readFileSync('src/state/slices/tradeEarnInputSlice/selectors.ts', 'utf-8');
const hasDirectExport = content.includes('export const selectHasUserEnteredAmount');
const hasDestructuredExport = content.match(/export const \{[\s\S]*selectHasUserEnteredAmount[\s\S]*\}/);
console.log('Direct export of selectHasUserEnteredAmount:', !!hasDirectExport);
console.log('Destructured export includes selectHasUserEnteredAmount:', !!hasDestructuredExport);
"

echo ""
echo "=== Check if selectHasUserEnteredAmount is imported from tradeEarnInputSlice anywhere ==="
rg 'from.*tradeEarnInputSlice.*selectHasUserEnteredAmount|import.*selectHasUserEnteredAmount.*from.*tradeEarnInputSlice'

Repository: shapeshift/web

Length of output: 322


Remove duplicate selector and use the base selectHasUserEnteredAmount.

selectEarnHasUserEnteredAmount duplicates selectHasUserEnteredAmount from the base selectors without adding any distinct logic or behavior—both perform an identical bnOrZero(sellAmountCryptoPrecision).gt(0) check. Use the imported selectHasUserEnteredAmount in EarnInput.tsx instead of maintaining a separate wrapper selector.

🤖 Prompt for AI Agents
In `@src/state/slices/tradeEarnInputSlice/selectors.ts` around lines 38 - 41,
Remove the redundant selector export selectEarnHasUserEnteredAmount from
selectors.ts and replace its usages with the base selector
selectHasUserEnteredAmount: delete the createSelector block for
selectEarnHasUserEnteredAmount, update any imports (notably in EarnInput.tsx) to
import selectHasUserEnteredAmount instead of selectEarnHasUserEnteredAmount, and
update the selector reference in EarnInput.tsx to use selectHasUserEnteredAmount
so the shared implementation is used rather than a duplicate wrapper.

gomesalexandre and others added 3 commits January 14, 2026 22:26
Removed the useEffect that auto-closed the modal on success, allowing
users to see the success screen with transaction details before dismissing.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The YieldSuccess component already has its own Close button, so the
additional DialogFooter with Close button was redundant.

Fixes: #11650

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Cards/mobile: show Available (green) when user has wallet balance but no staked position
- List view: show both Balance and Available when user has both
- Earn button now shows individual yields instead of filtered groups
- Pass correct position balance vs wallet balance to YieldItem

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/pages/Yields/components/YieldsList.tsx`:
- Around line 331-338: The eligibility check in yieldsForAsset.filter (used to
compute eligibleYields) uses a hardcoded precision fallback
(assets[assetId]?.precision ?? 18) which can produce incorrect comparisons;
mirror the fix used in recommendedYields by replacing that hardcoded fallback
with the same precision resolution logic (use the shared DEFAULT/utility
precision or the same assets lookup used in recommendedYields), then compute
baseBalance/fromBaseUnit with that resolved precision before comparing to
bnOrZero(y.mechanics?.entryLimits?.minimum) so the minimum deposit check uses
the correct precision.
♻️ Duplicate comments (2)
src/pages/Yields/components/YieldsList.tsx (1)

268-314: Skip yield eligibility check when asset metadata is missing instead of defaulting precision to 18.

Using precision ?? 18 as a fallback can incorrectly mark yields as eligible or ineligible because fromBaseUnit(baseBalance, precision) depends critically on the correct precision value. If an asset is missing from the asset registry but present in the user's balance, defaulting to 18 assumes it's an EVM token—which may not be true (e.g., USDC has 6 decimals). This produces incorrect human-readable balance calculations, breaking the minimum deposit eligibility check.

🐛 Proposed fix
       const eligibleYields = yieldsForAsset.filter(y => {
         const minDeposit = bnOrZero(y.mechanics?.entryLimits?.minimum)
         if (minDeposit.lte(0)) return true

-        // minDeposit is human readable (e.g. 32), so convert base balance to human
-        const precision = assets[assetId]?.precision ?? 18
+        const asset = assets[assetId]
+        if (!asset) return false // Skip if asset metadata unknown
+
+        const precision = asset.precision
         const baseBalance = bnOrZero(assetBalancesBaseUnit[assetId])
         const balanceHuman = bnOrZero(fromBaseUnit(baseBalance, precision))
         return balanceHuman.gte(minDeposit)
       })
src/pages/Yields/components/YieldItem.tsx (1)

358-361: Invalid prop itemProp on GradientApy component.

GradientApy extends Chakra's Text component. The prop itemProp='medium' appears to be a typo—it should likely be fontWeight='medium' based on usage elsewhere in the file.

🐛 Proposed fix
-                <GradientApy itemProp='medium' justifyContent='flex-end'>
+                <GradientApy fontWeight='medium' justifyContent='flex-end'>
🧹 Nitpick comments (1)
src/pages/Yields/components/YieldsList.tsx (1)

63-63: Consider using a breakpoint token instead of hardcoded pixel value.

The hardcoded 768px breakpoint may drift from Chakra's theme breakpoints. Consider using the theme's md breakpoint for consistency with the design system.

Suggested improvement
-  const [isMobile] = useMediaQuery('(max-width: 768px)')
+  const [isMobile] = useMediaQuery('(max-width: 48em)', { ssr: false })

Or better, use Chakra's useBreakpointValue for theme-aware responsive logic:

const isMobile = useBreakpointValue({ base: true, md: false }) ?? false
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c0db870 and ce363eb.

📒 Files selected for processing (2)
  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Never assume a library is available - always check imports/package.json first
Prefer composition over inheritance
Write self-documenting code with clear variable and function names
Keep functions small and focused on a single responsibility
Avoid deep nesting - use early returns instead
Prefer procedural and easy to understand code
Never expose, log, or commit secrets, API keys, or credentials
Validate all inputs, especially user inputs
Handle errors gracefully with meaningful messages
Don't silently catch and ignore exceptions
Log errors appropriately for debugging
Provide fallback behavior when possible
Use appropriate data structures for the task
Never add code comments unless explicitly requested
When modifying code, do not add comments that reference previous implementations or explain what changed. Comments should only describe the current logic and functionality.
Use meaningful names for branches, variables, and functions
Always run yarn lint --fix and yarn type-check after making changes
Avoid let variable assignments - prefer const with inline IIFE switch statements or extract to functions for conditional logic

Files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Avoid useEffect where practical - use it only when necessary and following best practices
Avoid 'any' types - use specific type annotations instead
For default values with user overrides, use computed values (useMemo) instead of useEffect - pattern: userSelected ?? smartDefault ?? fallback
When function parameters are unused due to interface requirements, refactor the interface or implementation to remove them rather than prefixing with underscore
Sanitize data before displaying to prevent XSS
Memoize aggressively - wrap component variables in useMemo and callbacks in useCallback where possible
For static JSX icon elements (e.g., <TbCopy />) that don't depend on state/props, define them as constants outside the component to avoid re-renders instead of using useMemo
Account for light/dark mode using useColorModeValue hook
Account for responsive mobile designs in all UI components
When applying styles, use the existing standards and conventions of the codebase
Use Chakra UI components and conventions
All copy/text must use translation keys - never hardcode strings
Use the translation hook: useTranslate() from react-polyglot
Use useFeatureFlag('FlagName') hook to access feature flag values in components
Prefer type over interface for type definitions
Use strict typing - avoid any
Use Nominal types for domain identifiers (e.g., WalletId, AccountId)
Import types from @shapeshiftoss/caip for chain/account/asset IDs
Use useAppSelector for Redux state
Use useAppDispatch for Redux actions
Memoize expensive computations with useMemo
Memoize callbacks with useCallback

**/*.{ts,tsx}: Use Result<T, E> pattern for error handling in swappers and APIs; ALWAYS use Ok() and Err() from @sniptt/monads; AVOID throwing within swapper API implementations
ALWAYS use custom error classes from @shapeshiftoss/errors with meaningful error codes for internationalization and relevant details in error objects
ALWAYS wrap async op...

Files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/error-handling.mdc)

**/*.{tsx,jsx}: ALWAYS wrap React components in error boundaries and provide user-friendly fallback components with error logging
ALWAYS use useErrorToast hook for displaying errors with translated error messages and handle different error types appropriately

Use PascalCase for React component names and match the component name to the file name

Files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/naming-conventions.mdc)

**/*.{js,jsx,ts,tsx}: Use camelCase for variables, functions, and methods with descriptive names that explain the purpose
Use verb prefixes for functions that perform actions (e.g., fetch, validate, execute, update, calculate)
Use UPPER_SNAKE_CASE for constants and configuration values with descriptive names
Use handle prefix for event handlers with descriptive names in camelCase
Use descriptive boolean variable names with is, has, can, should prefixes
Use named exports for components, functions, and utilities instead of default exports
Use descriptive import names and avoid renaming imports unless necessary
Avoid non-descriptive variable names like data, item, obj, and single-letter variable names except in loops
Avoid abbreviations in names unless they are widely understood
Avoid generic function names like fn, func, or callback

Files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/react-best-practices.mdc)

**/*.{jsx,tsx}: ALWAYS use useMemo for expensive computations, object/array creations, and filtered data
ALWAYS use useMemo for derived values and computed properties
ALWAYS use useMemo for conditional values and simple transformations
ALWAYS use useCallback for event handlers and functions passed as props
ALWAYS use useCallback for any function that could be passed as a prop or dependency
ALWAYS include all dependencies in useEffect, useMemo, useCallback dependency arrays
NEVER use // eslint-disable-next-line react-hooks/exhaustive-deps unless absolutely necessary, and ALWAYS explain why dependencies are excluded if using eslint disable
ALWAYS use named exports for components; NEVER use default exports for components
KEEP component files under 200 lines when possible; BREAK DOWN large components into smaller, reusable pieces
EXTRACT complex logic into custom hooks
ALWAYS wrap components in error boundaries for production
ALWAYS handle async errors properly in async operations
ALWAYS provide user-friendly error messages in error handling
ALWAYS use virtualization for lists with 100+ items
ALWAYS implement proper key props for list items
ALWAYS lazy load heavy components using React.lazy for code splitting
ALWAYS use Suspense wrapper for lazy loaded components
USE local state for component-level state; LIFT state up when needed across multiple components; USE Context for avoiding prop drilling; USE Redux only for global state shared across multiple places
Wrap components receiving props with memo for performance optimization

Files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/react-best-practices.mdc)

Ensure TypeScript types are explicit and proper; avoid use of any type

Files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
🧠 Learnings (31)
📓 Common learnings
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10231
File: src/components/AssetSearch/components/AssetList.tsx:2-2
Timestamp: 2025-08-08T15:00:49.887Z
Learning: Project shapeshift/web: NeOMakinG prefers avoiding minor a11y/UI nitpicks (e.g., adding aria-hidden to decorative icons in empty states like src/components/AssetSearch/components/AssetList.tsx) within feature PRs; defer such suggestions to a follow-up instead of blocking the PR.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10569
File: src/plugins/walletConnectToDapps/components/WalletConnectSigningModal/WalletConnectModalSigningFooter.tsx:121-129
Timestamp: 2025-09-17T22:40:30.149Z
Learning: gomesalexandre maintains strict scope discipline even for style/UI PRs in shapeshift/web, declining functionally correct UX improvements (like keeping Cancel button enabled during gas simulation loading) when they fall outside the PR's stated styling objectives, demonstrating his consistent pattern of deferring valid but tangential improvements to separate efforts.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10418
File: src/plugins/walletConnectToDapps/components/header/WalletConnectToDappsHeaderButton.tsx:0-0
Timestamp: 2025-09-08T22:00:48.005Z
Learning: gomesalexandre dismissed an aria-label accessibility suggestion with "meh" in PR `#10418` for WalletConnectToDappsHeaderButton.tsx, consistent with the team's pattern of deferring minor a11y improvements to follow-up PRs rather than expanding feature PR scope.
Learnt from: CR
Repo: shapeshift/web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T21:20:04.979Z
Learning: To add a new feature flag: (1) Add to `FeatureFlags` type in `src/state/slices/preferencesSlice/preferencesSlice.ts`, (2) Add environment variable validation in `src/config.ts`, (3) Add to initial state in `preferencesSlice.ts`, (4) Add to test mock in `src/test/mocks/store.ts`, (5) Set appropriate values in `.env`, `.env.development`, and `.env.production`
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10206
File: src/config.ts:127-128
Timestamp: 2025-08-07T11:20:44.614Z
Learning: gomesalexandre prefers required environment variables without default values in the config file (src/config.ts). They want explicit configuration and fail-fast behavior when environment variables are missing, rather than having fallback defaults.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10461
File: src/plugins/walletConnectToDapps/components/modals/ContractInteractionBreakdown.tsx:0-0
Timestamp: 2025-09-13T16:45:18.813Z
Learning: gomesalexandre prefers aggressively deleting unused/obsolete code files ("ramboing") rather than fixing technical issues in code that won't be used, demonstrating his preference for keeping codebases clean and PR scope focused.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10458
File: src/plugins/walletConnectToDapps/types.ts:7-7
Timestamp: 2025-09-10T15:34:29.604Z
Learning: gomesalexandre is comfortable relying on transitive dependencies (like abitype through ethers/viem) rather than explicitly declaring them in package.json, preferring to avoid package.json bloat when the transitive dependency approach works reliably in practice.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10503
File: .env:56-56
Timestamp: 2025-09-16T13:17:02.938Z
Learning: gomesalexandre prefers to enable feature flags globally in the base .env file when the intent is to activate features everywhere, even when there are known issues like crashes, demonstrating his preference for intentional global feature rollouts over cautious per-environment enablement.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10249
File: src/pages/ThorChainLP/components/ReusableLpStatus/TransactionRow.tsx:447-503
Timestamp: 2025-08-13T17:07:10.763Z
Learning: gomesalexandre prefers relying on TypeScript's type system for validation rather than adding defensive runtime null checks when types are properly defined. They favor a TypeScript-first approach over defensive programming with runtime validations.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10276
File: src/hooks/useActionCenterSubscribers/useThorchainLpDepositActionSubscriber.tsx:61-66
Timestamp: 2025-08-14T17:51:47.556Z
Learning: gomesalexandre is not concerned about structured logging and prefers to keep console.error usage as-is rather than implementing structured logging patterns, even when project guidelines suggest otherwise.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10413
File: src/components/Modals/FiatRamps/fiatRampProviders/onramper/utils.ts:29-55
Timestamp: 2025-09-02T14:26:19.028Z
Learning: gomesalexandre prefers to keep preparatory/reference code simple until it's actively consumed, rather than implementing comprehensive error handling, validation, and robustness improvements upfront. They prefer to add these improvements when the code is actually being used in production.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10783
File: src/context/ModalStackProvider/useModalRegistration.ts:30-41
Timestamp: 2025-10-16T11:14:40.657Z
Learning: gomesalexandre prefers to add lint rules (like typescript-eslint/strict-boolean-expressions for truthiness checks on numbers) to catch common issues project-wide rather than relying on code review to catch them.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10276
File: src/pages/ThorChainLP/components/ReusableLpStatus/TransactionRow.tsx:396-402
Timestamp: 2025-08-14T17:55:57.490Z
Learning: gomesalexandre is comfortable with functions/variables that return undefined or true (tri-state) when only the truthy case matters, preferring to rely on JavaScript's truthy/falsy behavior rather than explicitly returning boolean values.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10206
File: src/lib/moralis.ts:47-85
Timestamp: 2025-08-07T11:22:16.983Z
Learning: gomesalexandre prefers console.error over structured logging for Moralis API integration debugging, as they find it more conventional and prefer to examine XHR requests directly rather than rely on structured logs for troubleshooting.
📚 Learning: 2025-11-24T21:20:04.979Z
Learnt from: CR
Repo: shapeshift/web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T21:20:04.979Z
Learning: Applies to **/*.{ts,tsx} : Account for responsive mobile designs in all UI components

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-08-08T15:00:49.887Z
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10231
File: src/components/AssetSearch/components/AssetList.tsx:2-2
Timestamp: 2025-08-08T15:00:49.887Z
Learning: Project shapeshift/web: NeOMakinG prefers avoiding minor a11y/UI nitpicks (e.g., adding aria-hidden to decorative icons in empty states like src/components/AssetSearch/components/AssetList.tsx) within feature PRs; defer such suggestions to a follow-up instead of blocking the PR.

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-08-08T14:59:40.422Z
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10231
File: src/pages/Explore/ExploreCategory.tsx:231-238
Timestamp: 2025-08-08T14:59:40.422Z
Learning: In src/pages/Explore/ExploreCategory.tsx, for the PageHeader filter trigger, NeOMakinG considers changing a clickable Chakra Icon to IconButton too nitpicky for this PR and prefers to keep the current Icon-based trigger; such minor a11y/UI nitpicks should be deferred to a follow-up if needed.

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
📚 Learning: 2025-09-08T15:53:09.362Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10442
File: src/components/TradeAssetSearch/components/GroupedAssetList/GroupedAssetList.tsx:34-35
Timestamp: 2025-09-08T15:53:09.362Z
Learning: In DefaultAssetList.tsx, the GroupedAssetList component already receives the activeChainId prop correctly on line ~58, contrary to automated analysis that may flag it as missing.

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-09-16T09:32:21.333Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10490
File: src/components/Layout/Header/NavBar/NavigationDropdown.tsx:96-103
Timestamp: 2025-09-16T09:32:21.333Z
Learning: In the shapeshift/web codebase, gomesalexandre confirms that using `item.icon && <Icon as={item.icon} boxSize={4} />` for Chakra UI MenuItem.icon prop is valid and acceptable - the boolean short-circuit pattern works fine with Chakra components and doesn't need to be changed to explicit undefined.

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
📚 Learning: 2025-11-24T21:20:44.637Z
Learnt from: CR
Repo: shapeshift/web PR: 0
File: .cursor/rules/react-best-practices.mdc:0-0
Timestamp: 2025-11-24T21:20:44.637Z
Learning: Applies to **/*.{jsx,tsx} : ALWAYS implement proper key props for list items

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
📚 Learning: 2025-11-24T21:20:04.979Z
Learnt from: CR
Repo: shapeshift/web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T21:20:04.979Z
Learning: Applies to **/*.{ts,tsx} : Use Chakra UI components and conventions

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-10-07T03:44:27.350Z
Learnt from: 0xApotheosis
Repo: shapeshift/web PR: 10760
File: src/components/ManageHiddenAssets/ManageHiddenAssetsList.tsx:78-84
Timestamp: 2025-10-07T03:44:27.350Z
Learning: In the ShapeShift web codebase, the following are stable references and do not need to be included in useCallback/useMemo dependency arrays:
- `navigate` from `useBrowserRouter()` hook
- Modal control objects (like `walletDrawer`) from `useModal()` hook (including their `isOpen`, `close`, and `open` methods)
- These are backed by stable context providers

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-11-24T21:20:04.979Z
Learnt from: CR
Repo: shapeshift/web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T21:20:04.979Z
Learning: Applies to **/*.{ts,tsx} : For static JSX icon elements (e.g., `<TbCopy />`) that don't depend on state/props, define them as constants outside the component to avoid re-renders instead of using useMemo

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-08-14T17:54:32.563Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10276
File: src/pages/ThorChainLP/components/ReusableLpStatus/ReusableLpStatus.tsx:97-108
Timestamp: 2025-08-14T17:54:32.563Z
Learning: In ReusableLpStatus component (src/pages/ThorChainLP/components/ReusableLpStatus/ReusableLpStatus.tsx), the txAssets dependency is stable from first render because poolAsset, baseAsset, actionSide, and action are all defined first render, making the current txAssetsStatuses initialization pattern safe without needing useEffect synchronization.

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-11-24T21:20:04.979Z
Learnt from: CR
Repo: shapeshift/web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T21:20:04.979Z
Learning: Applies to **/*.{ts,tsx} : Import types from `shapeshiftoss/caip` for chain/account/asset IDs

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-12-27T16:02:52.792Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11536
File: src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx:252-265
Timestamp: 2025-12-27T16:02:52.792Z
Learning: When reviewing bug fixes, especially in shapeshift/web, prefer minimal changes that fix correctness over introducing broader refactors or quality-of-life improvements (e.g., extracting duplicated logic) unless such improvements are essential to the fix. Apply this guideline broadly to TSX files and related components, not just the specific location, to keep changes focused and maintainable.

Applied to files:

  • src/pages/Yields/components/YieldItem.tsx
  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-07-30T20:57:48.176Z
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10148
File: src/components/MarketTableVirtualized/MarketsTableVirtualized.tsx:88-91
Timestamp: 2025-07-30T20:57:48.176Z
Learning: In src/components/MarketTableVirtualized/MarketsTableVirtualized.tsx, NeOMakinG prefers keeping the hardcoded overscan value (40) over dynamic calculation based on viewport height, prioritizing code simplicity over precision when the current approach works effectively.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-08-06T09:47:29.865Z
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10191
File: src/pages/Explore/Explore.tsx:174-178
Timestamp: 2025-08-06T09:47:29.865Z
Learning: In src/pages/Explore/Explore.tsx, NeOMakinG prefers keeping complex height calculations inline rather than extracting them to useMemo, prioritizing perceived performance/execution speed over code readability, even when the calculation would only run when the dependency (isSearching) changes.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-08-17T21:53:03.806Z
Learnt from: 0xApotheosis
Repo: shapeshift/web PR: 10290
File: scripts/generateAssetData/color-map.json:41-47
Timestamp: 2025-08-17T21:53:03.806Z
Learning: In the ShapeShift web codebase, native assets (using CAIP-19 slip44 namespace like eip155:1/slip44:60, bip122:.../slip44:..., cosmos:.../slip44:...) are manually hardcoded and not generated via the automated asset generation script. Only ERC20/BEP20 tokens go through the asset generation process. The validation scripts should only validate generated assets, not manually added native assets.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-10-21T17:11:18.087Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10871
File: src/components/Modals/Send/hooks/useSendDetails/useSendDetails.tsx:426-428
Timestamp: 2025-10-21T17:11:18.087Z
Learning: In src/components/Modals/Send/hooks/useSendDetails/useSendDetails.tsx, within the handleInputChange function, use .toFixed() without arguments (not .toString()) when converting BigNumber amounts for input field synchronization. This avoids exponential notation in the input while preserving precision for presentational components like <Amount.Crypto /> and <Amount.Fiat /> to format appropriately.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-08-10T21:09:25.643Z
Learnt from: premiumjibles
Repo: shapeshift/web PR: 10215
File: src/components/MultiHopTrade/hooks/useGetTradeRateInput.ts:65-67
Timestamp: 2025-08-10T21:09:25.643Z
Learning: In the MultiHopTrade components, `selectInputBuyAsset` and `selectInputSellAsset` selectors from `tradeInputSlice` always return defined values because they have default values in the initial state (BTC for buyAsset, ETH for sellAsset, with fallback to defaultAsset). Null checks for these assets are unnecessary when using these selectors.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-11-03T22:31:30.786Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10985
File: packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts:0-0
Timestamp: 2025-11-03T22:31:30.786Z
Learning: In packages/swapper/src/swappers/PortalsSwapper, the rate and quote files intentionally use different approaches for calculating buyAmountBeforeSlippageCryptoBaseUnit: getPortalsTradeRate.tsx uses minOutputAmount / (1 - buffer) for conservative estimates, while getPortalsTradeQuote.ts uses outputAmount / (1 - buffer) for final quote display. This difference is validated by on-chain simulation testing and is intentional.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-07-24T11:07:20.536Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10073
File: src/features/defi/providers/fox-farming/components/FoxFarmingManager/Claim/Claim.tsx:77-84
Timestamp: 2025-07-24T11:07:20.536Z
Learning: In fox farming components, the `opportunity?.rewardsCryptoBaseUnit?.amounts` property has a well-defined type signature that is always an array (never undefined), but can be empty: `readonly [] | readonly [string, string, string] | readonly [string, string] | readonly [string]`. Using optional chaining on the `amounts` property itself is unnecessary since it's always defined, though accessing `amounts[0]` on an empty array returns undefined which bnOrZero() handles safely.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-12-26T15:45:47.558Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11515
File: scripts/generateAssetData/generateRelatedAssetIndex/generateRelatedAssetIndex.ts:100-122
Timestamp: 2025-12-26T15:45:47.558Z
Learning: In scripts/generateAssetData/generateRelatedAssetIndex/generateRelatedAssetIndex.ts, gomesalexandre is comfortable with the fetchBridgedCategoryMappings function lacking try-catch error handling for CoinGecko API calls. He prefers letting the script crash on API failures rather than adding defensive error handling, consistent with his fail-fast approach for asset generation scripts.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-10-15T15:57:39.956Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10810
File: src/plugins/walletConnectToDapps/utils/tenderly/index.ts:212-0
Timestamp: 2025-10-15T15:57:39.956Z
Learning: gomesalexandre uses discriminated union patterns (e.g., `isEIP1559 ? { max_fee_per_gas, max_priority_fee_per_gas } : { gas_price }`) in WalletConnect flows without additional validation guards, trusting that the runtime data structure ensures mutual exclusivity between EIP-1559 and legacy gas pricing fields.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-08-15T07:51:16.374Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10278
File: src/components/AssetHeader/hooks/useQuickBuy.ts:97-99
Timestamp: 2025-08-15T07:51:16.374Z
Learning: The selectPortfolioUserCurrencyBalanceByAssetId selector in src/state/slices/portfolioSlice/selectors.ts expects a filter object with an assetId property, not a raw AssetId string. The selector signature is (state: ReduxState, filter) where filter should have an assetId property. This pattern is consistent across portfolio selectors that use selectAssetIdParamFromFilter. Passing a filter object like { assetId: someAssetId } is the correct usage pattern.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-08-22T12:59:01.702Z
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10323
File: src/components/Layout/Header/ActionCenter/components/RewardDistributionActionCard.tsx:37-53
Timestamp: 2025-08-22T12:59:01.702Z
Learning: In RewardDistributionActionCard component (src/components/Layout/Header/ActionCenter/components/RewardDistributionActionCard.tsx), NeOMakinG confirmed that runeAsset is expected to always be defined when the component renders, so defensive guards against undefined runeAsset are not needed.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-11-24T21:20:04.979Z
Learnt from: CR
Repo: shapeshift/web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T21:20:04.979Z
Learning: Applies to **/*.{ts,tsx} : Account for light/dark mode using `useColorModeValue` hook

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-10-17T07:51:58.374Z
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10783
File: src/context/WalletProvider/NewWalletViews/NewWalletViewsSwitch.tsx:344-349
Timestamp: 2025-10-17T07:51:58.374Z
Learning: In the shapeshift/web codebase, Chakra UI's ModalContent component supports the containerProps prop. When using the useModalRegistration hook from ModalStackProvider, spreading {...modalContentProps} directly onto ModalContent is correct and properly applies z-index and pointer-events through containerProps.sx. Do not suggest extracting sx from modalContentProps.containerProps.sx.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-09-12T12:04:59.556Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10461
File: src/plugins/walletConnectToDapps/components/WalletConnectSigningModal/content/SendTransactionContent.tsx:0-0
Timestamp: 2025-09-12T12:04:59.556Z
Learning: The ShapeShift codebase's fromBaseUnit function correctly handles hex strings (like WalletConnect transaction.value) without manual conversion because bnOrZero -> bn -> new BigNumber() automatically detects and parses hex strings starting with "0x". gomesalexandre confirmed this with concrete evidence showing hex value 0x176d1c49189db correctly converts to 0.000412118294825435 ETH.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-08-11T09:46:41.060Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10219
File: src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx:167-172
Timestamp: 2025-08-11T09:46:41.060Z
Learning: In the shapeshift/web repository, the display cache logic for trade quotes (using `selectUserAvailableTradeQuotes` and `selectUserUnavailableTradeQuotes`) is intentionally kept the same between `TradeInput.tsx` and `TradeQuotes.tsx` components. The `hasQuotes` computation in `TradeInput.tsx` uses these display cache selectors by design, matching the pattern used in `TradeQuotes.tsx`.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-09-04T17:29:59.479Z
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10380
File: src/components/TradeAssetSearch/hooks/useGetPopularAssetsQuery.tsx:28-33
Timestamp: 2025-09-04T17:29:59.479Z
Learning: In shapeshift/web, the useGetPopularAssetsQuery function in src/components/TradeAssetSearch/hooks/useGetPopularAssetsQuery.tsx intentionally uses primaryAssets[assetId] instead of falling back to assets[assetId]. The design distributes primary assets across chains by iterating through their related assets and adding the primary asset to each related asset's chain. This ensures primary assets appear in all chains where they have related assets, supporting the grouped asset system.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-11-24T21:20:44.637Z
Learnt from: CR
Repo: shapeshift/web PR: 0
File: .cursor/rules/react-best-practices.mdc:0-0
Timestamp: 2025-11-24T21:20:44.637Z
Learning: Applies to **/*.{jsx,tsx} : ALWAYS use virtualization for lists with 100+ items

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
📚 Learning: 2025-11-05T23:37:30.632Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10912
File: src/context/WalletProvider/NewWalletViews/NewWalletViewsSwitch.tsx:290-290
Timestamp: 2025-11-05T23:37:30.632Z
Learning: In the ShapeShift web codebase, `isMobile` imported from '@/lib/globals' is a module-level constant (defined as `export const isMobile = Boolean(window?.isShapeShiftMobile)`) that is evaluated once at module load time. It is a stable reference that does not need to be included in useCallback/useMemo/useEffect dependency arrays.

Applied to files:

  • src/pages/Yields/components/YieldsList.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Call / Static
🔇 Additional comments (12)
src/pages/Yields/components/YieldsList.tsx (5)

56-56: LGTM!

Clean extraction of tabSelectedSx as a constant outside the component, avoiding unnecessary re-renders as per coding guidelines.


175-182: LGTM!

Clean derivation of unique yield types with proper capitalization and formatting. The Set-based deduplication is efficient.


607-691: LGTM!

The grid element rendering logic correctly handles both single yields and groups, with proper mobile variant switching and all necessary dependencies in the useMemo array.


820-854: LGTM!

The recommended strip element is well-structured with proper conditional rendering based on connection state and data availability. Mobile variant is correctly applied.


856-876: LGTM!

The content element correctly handles the empty state check for both isMyOpportunities and regular view, with proper mobile-aware view mode fallback.

src/pages/Yields/components/YieldItem.tsx (7)

47-55: LGTM!

Clean type definitions with proper optional props. The variant union type correctly includes the new 'mobile' option.


75-107: LGTM!

The stats computation correctly handles both single and group yields with proper memoization. The dependency array includes all necessary values.


124-135: LGTM!

The click handler correctly differentiates between single and group yields, with proper URL encoding for asset symbols and search string preservation.


299-366: LGTM on mobile variant structure!

The mobile variant layout is well-structured with a compact 3-column grid for balance/TVL/APY. The conditional APY label (maxApy vs apy) correctly reflects single vs group context.


368-429: LGTM!

The row variant correctly implements responsive column visibility with proper display breakpoints and handles both single and group provider rendering appropriately.


431-504: LGTM!

The card variant maintains visual consistency with proper hover effects, gradient APY styling, and flexible stat positioning. The cursor correctly reflects whether entering is allowed.


506-524: LGTM!

The mobile skeleton variant provides appropriate loading state UI with the correct structure matching the mobile variant layout.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +331 to +338
const eligibleYields = yieldsForAsset.filter(y => {
const minDeposit = bnOrZero(y.mechanics?.entryLimits?.minimum)
if (minDeposit.gt(0)) {
const precision = assets[assetId]?.precision ?? 18
const baseBalance = bnOrZero(assetBalancesBaseUnit[assetId])
const balanceHuman = bnOrZero(fromBaseUnit(baseBalance, precision))
if (balanceHuman.lt(minDeposit)) return false
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same precision fallback issue in availableYields.

This mirrors the issue in recommendedYields. When precision is unknown, the eligibility check will be incorrect.

🐛 Proposed fix
       const eligibleYields = yieldsForAsset.filter(y => {
         const minDeposit = bnOrZero(y.mechanics?.entryLimits?.minimum)
         if (minDeposit.gt(0)) {
-          const precision = assets[assetId]?.precision ?? 18
+          const asset = assets[assetId]
+          if (!asset) return false
+          const precision = asset.precision
           const baseBalance = bnOrZero(assetBalancesBaseUnit[assetId])
           const balanceHuman = bnOrZero(fromBaseUnit(baseBalance, precision))
           if (balanceHuman.lt(minDeposit)) return false
         }
📝 Committable suggestion

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

Suggested change
const eligibleYields = yieldsForAsset.filter(y => {
const minDeposit = bnOrZero(y.mechanics?.entryLimits?.minimum)
if (minDeposit.gt(0)) {
const precision = assets[assetId]?.precision ?? 18
const baseBalance = bnOrZero(assetBalancesBaseUnit[assetId])
const balanceHuman = bnOrZero(fromBaseUnit(baseBalance, precision))
if (balanceHuman.lt(minDeposit)) return false
}
const eligibleYields = yieldsForAsset.filter(y => {
const minDeposit = bnOrZero(y.mechanics?.entryLimits?.minimum)
if (minDeposit.gt(0)) {
const asset = assets[assetId]
if (!asset) return false
const precision = asset.precision
const baseBalance = bnOrZero(assetBalancesBaseUnit[assetId])
const balanceHuman = bnOrZero(fromBaseUnit(baseBalance, precision))
if (balanceHuman.lt(minDeposit)) return false
}
🤖 Prompt for AI Agents
In `@src/pages/Yields/components/YieldsList.tsx` around lines 331 - 338, The
eligibility check in yieldsForAsset.filter (used to compute eligibleYields) uses
a hardcoded precision fallback (assets[assetId]?.precision ?? 18) which can
produce incorrect comparisons; mirror the fix used in recommendedYields by
replacing that hardcoded fallback with the same precision resolution logic (use
the shared DEFAULT/utility precision or the same assets lookup used in
recommendedYields), then compute baseBalance/fromBaseUnit with that resolved
precision before comparing to bnOrZero(y.mechanics?.entryLimits?.minimum) so the
minimum deposit check uses the correct precision.

Comment on lines +1028 to +1046
{!isMobile && (
<>
<YieldFilters
networks={networks}
selectedNetwork={selectedNetwork}
onSelectNetwork={handleNetworkChange}
providers={providers}
selectedProvider={selectedProvider}
onSelectProvider={handleProviderChange}
types={types}
selectedType={selectedType}
onSelectType={handleTypeChange}
sortOption={sortOption}
onSortChange={handleSortChange}
mb={0}
/>
<ViewToggle viewMode={viewMode} setViewMode={setViewMode} />
</>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the full context of the YieldsList component
cat -n src/pages/Yields/components/YieldsList.tsx | head -100

Repository: shapeshift/web

Length of output: 4544


🏁 Script executed:

# Check file size and then examine the section in question
wc -l src/pages/Yields/components/YieldsList.tsx

Repository: shapeshift/web

Length of output: 105


🏁 Script executed:

# Look at the specific lines and surrounding context (1000-1060)
sed -n '1000,1060p' src/pages/Yields/components/YieldsList.tsx

Repository: shapeshift/web

Length of output: 2014


🏁 Script executed:

# Search for mobile filter implementations in the Yields directory
fd . src/pages/Yields/components -type f -name "*.tsx" | xargs grep -l "isMobile\|mobile.*filter" -i

Repository: shapeshift/web

Length of output: 230


🏁 Script executed:

# Check if there's a filter modal or drawer implementation
rg "filter.*modal\|modal.*filter\|filter.*drawer\|drawer.*filter" -i src/pages/Yields

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

# Let me search correctly for files in Yields components
find src/pages/Yields/components -name "*.tsx" -o -name "*.ts"

Repository: shapeshift/web

Length of output: 997


🏁 Script executed:

# Check the entire section around the isMobile filters to see if there's a mobile alternative
sed -n '1000,1056p' src/pages/Yields/components/YieldsList.tsx

Repository: shapeshift/web

Length of output: 2014


🏁 Script executed:

# Search for any mobile filter modal or drawer implementation
rg "Modal\|Drawer\|Popover" src/pages/Yields/components -A 2 -B 2

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

# Check if YieldFilters component has mobile-specific logic
cat -n src/pages/Yields/components/YieldFilters.tsx 2>/dev/null | head -50

Repository: shapeshift/web

Length of output: 1586


🏁 Script executed:

# Let's search for any mobile filter implementations in the entire Yields directory
rg "isMobile.*filter\|mobile.*filter" src/pages/Yields -i

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

# Check git history for the isMobile filters section
git log -p --follow -S "!isMobile && (" -- src/pages/Yields/components/YieldsList.tsx | head -200

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

# Look at useYieldFilters hook to understand how filters work
cat -n src/pages/Yields/hooks/useYieldFilters.tsx 2>/dev/null | head -100

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

# Check if there's URL params logic that maintains filters on mobile
rg "searchParams\|setSearchParams" src/pages/Yields/components/YieldsList.tsx -B 2 -A 2

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

# Compare with Explore component mentioned in learnings
cat -n src/pages/Explore/ExploreCategory.tsx | grep -A 10 -B 10 "isMobile\|mobile" | head -50

Repository: shapeshift/web

Length of output: 1069


🏁 Script executed:

# Let me check the full YieldsList component more thoroughly for any mobile filter logic
sed -n '1,1056p' src/pages/Yields/components/YieldsList.tsx | tail -200

Repository: shapeshift/web

Length of output: 6354


🏁 Script executed:

# Check if there's a mobile filters implementation elsewhere or commented out
rg "mobile\|isMobile" src/pages/Yields/components/YieldsList.tsx -B 3 -A 3

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

# Look for any drawer or modal patterns in the entire Yields directory
rg "Drawer\|Modal\|Popover" src/pages/Yields --files-with-matches

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

# Check how filters persist in the URL and if they work on mobile
sed -n '60,100p' src/pages/Yields/components/YieldsList.tsx

Repository: shapeshift/web

Length of output: 1576


🏁 Script executed:

# Check if there's filter functionality in related components
cat -n src/pages/Yields/components/YieldViewHelpers.tsx | head -100

Repository: shapeshift/web

Length of output: 3285


🏁 Script executed:

# Let me check if there's any Jira ticket or issue reference in the code
rg "TODO|FIXME|XXX.*mobile.*filter|XXX.*filter.*mobile" src/pages/Yields

Repository: shapeshift/web

Length of output: 324


🏁 Script executed:

# Check the useYieldFilters hook implementation to confirm it uses setSearchParams
find src -name "useYieldFilters*" -type f

Repository: shapeshift/web

Length of output: 99


🏁 Script executed:

# Look at the complete filter change handlers to confirm they work via URL params
fd useYieldFilters -type f -name "*.tsx" -o -name "*.ts" | head -5

Repository: shapeshift/web

Length of output: 228


🏁 Script executed:

# Search for any alternate filter implementation or drawer pattern in Yields
rg "Modal|Drawer" src/pages/Yields/components/YieldFilters.tsx -A 5

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

# Check the useYieldFilters hook implementation
cat -n src/pages/Yields/hooks/useYieldFilters.ts

Repository: shapeshift/web

Length of output: 3572


🏁 Script executed:

# Verify that filter handlers use setSearchParams
rg "setSearchParams\|useSearchParams" src/pages/Yields/hooks/useYieldFilters.ts -B 2 -A 2

Repository: shapeshift/web

Length of output: 40


🏁 Script executed:

# Check if YieldFilters component itself has any mobile awareness
grep -n "isMobile\|mobile" src/pages/Yields/components/YieldFilters.tsx

Repository: shapeshift/web

Length of output: 40


Add mobile-friendly filters or justify the design decision.

Mobile users cannot access any filters (network, provider, type) or change view modes—these are completely hidden. The coding guidelines require accounting for responsive mobile designs in all UI components. Either implement a condensed filter UI, filter modal/drawer, or filter menu for mobile, or document why mobile filtering is intentionally unsupported. Currently, filters work via URL parameters only on mobile, not through the UI.

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

Labels

None yet

Projects

None yet

3 participants