Skip to content

feat(src): Earn deposit flow - #2974

Closed
aristidesstaffieri wants to merge 47 commits into
masterfrom
feat/earn-deposit
Closed

aristidesstaffieri wants to merge 47 commits into
masterfrom
feat/earn-deposit

Conversation

@aristidesstaffieri

@aristidesstaffieri aristidesstaffieri commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds an Earn deposit flow to the extension. From a new Earn tile on the Home action row, the user picks a supported token, sees the Blend Fixed pool's supply APY, enters an amount, reviews projected earnings, and submits a submit invocation against the pool contract. Gated to networks with an allowlisted pool — pubnet and testnet only.

Deposits use Blend request_type 2 (SupplyCollateral), not 0, so the position lands in collateral_tokens and the user's borrowing power stays open. Review reads before/after from total_tokens for that reason.

Scope: deposit only. No Positions tab and no withdraw — the Home tile is the only entry point and Home's tabs stay Tokens | Collectibles. If the account doesn't hold the selected token, the flow offers buy / swap / transfer remedies inline, including a swap-within-earn branch that reuses the Swap views inside Earn's sheet stack. Depends on freighter-backend-v2's /protocols/blend/* endpoints.

What's in this PR

  • @shared/helpers/soroban/blend.ts — request/submit op encoding. UDT ScMaps are hand-built because nativeToScVal sorts keys with localeCompare, which is not byte order.
  • @shared/api/{helpers,types}/blend.ts — client for earn-options, pools, and supplied-tokens. null USD/APY means unavailable and is never coalesced to 0.
  • @shared/constants/blend.ts — pool allowlist per network plus isEarnSupportedNetwork, which gates the tile and the route.
  • popup/views/Earn/ + popup/constants/earn.ts — step machine modelled on Send (all visited steps stay mounted). Sheets — pool details, insufficient-balance, network fee, review, swap — are owned by their host step, not steps themselves.
  • popup/components/earn/ — the five screens and their sheets, CTA-state and projected-earnings helpers, pool-stat formatting, asset-icon resolution.
  • popup/helpers/blendDeposit.ts — build + simulate. Auth comes back as sorobanCredentialsSourceAccount, so no authorizeEntry round-trip is needed, including on hardware wallets.
  • popup/components/{amount,swap}/ — extracted percentageAmount helpers and pinned the Swap footer/receive side so the reused views fit a fixed-height sheet. No behavior change for Send or standalone Swap.
  • background/messageListener/handlers/{get,dismiss}EarnIntro.ts — persists the one-time intro interstitial.
  • popup/metrics/earn.ts + constants/metricsNames.ts — funnel instrumentation. No amounts or fiat on any earn event; asset codes, pool_id, and apy only.
  • locales/{en,pt}/translation.json + translationParity.test.ts — new strings and a test that fails on missing or empty parity.

Notes for the reviewer

  • XLM is not held back from a deposit. A submit resource fee is ~546k stroops (~5,000× the inclusion fee), so a naive Max on XLM simulates into insufficient balance; the amount screen surfaces that as a fee shortfall rather than silently reserving.
  • Pool description prose is keyed by pool ID, not generic — the Fixed pool's admin key is verifiably burned, which is what the copy asserts.

Test plan

  • Unit tests pass — yarn jest blend earn Earn percentageAmount SwapAmount.layout translationParity metricsNames (14 suites, 139 tests)
  • Manual deposit against mainnet Fixed pool CAJJZSGM…BXBD
  • CI green
  • Manual pass on a hardware wallet

Followups

  • Positions tab / withdraw.

Example Flow

Screen.Recording.2026-08-24.at.9.16.37.AM.mov
Screen.Recording.2026-08-24.at.9.32.29.AM.mov

@aristidesstaffieri aristidesstaffieri self-assigned this Aug 20, 2026
@aristidesstaffieri aristidesstaffieri changed the title Feat/earn deposit feat(src): Earn deposit flow Aug 20, 2026
  First piece of the Earn feature: the pure encoding layer for depositing
  into a Blend v2 pool. No UI yet.

  Adds `buildBlendRequestScVal` and `buildBlendSubmitOp` for constructing
  `pool.submit(from, spender, to, requests)`, plus the per-network Fixed
  Pool allowlist mirroring freighter-backend-v2's `configs/earn-pools.json`.

  Deposits use request_type 2 (SupplyCollateral), following Blend's own
  integration guidance. Note this lands the position in `collateral_tokens`
  rather than `supplied_tokens`, so consumers should read `total_tokens`.

  The Request struct's ScMap is hand-built rather than going through
  `nativeToScVal`, which sorts map keys with `String.localeCompare`. Locale
  collation ignores `_`, so it is not byte order in general — it would
  invert `r_two` vs `reactivity`. The three keys here happen to agree, but
  relying on that is a trap for the next struct.

  Because from/spender/to all equal the transaction source account,
  simulation emits source-account credentials and the envelope signature
  covers the auth — no `authorizeEntry` round-trip is needed, which is what
  lets hardware wallets use the ordinary signing path.

  Also adds BLEND_DEPOSIT_XLM_FEE_BUFFER: a Blend submit's resource fee
  measures ~546,395 stroops (0.0546 XLM) against the live pool, roughly
  5,000x the inclusion fee that `getAvailableBalance` accounts for. Without
  the buffer, a Max deposit of XLM simulates into insufficient balance.

  Verified against the live mainnet and testnet Fixed pools; the golden XDR
  fixture is cross-checked byte-for-byte against `nativeToScVal` with an
  explicit type spec and matches `scRequestVec` in wallet-backend's
  integration test infrastructure.
  Wires up everything below the Earn UI: the backend client, the route and
  step machine, persistent first-run state, and the Home tile that opens it.
  Clicking Earn now shows the interstitial on first run and lands on an
  empty token picker.
  The first Earn screen with real data behind it: pool-supported assets
  split into what the account holds and what it does not, each with the
  pool's headline rate, and a sheet offering the ways to acquire a token
  the account has none of.
  Amount entry, pool card with current rate, percentage buttons, and the
  pool details sheet. The CTA builds and simulates the real Blend deposit
  via blendDeposit.ts.

  Max holds back an XLM buffer: getAvailableBalance nets out only the
  inclusion fee, but a Blend submit is dominated by its resource fee
  (~0.0546 XLM), so the raw balance would fail simulation. The fee check
  runs after the CTA gate so an unaffordable XLM deposit reads as
  insufficient funds rather than a missing-fee problem.

  Moves getAmountFontSizeClass/buildFiatLineText out of swap/ into
  amount/helpers — they are generic and Earn needs them too.

  The CTA stops at a successful simulation: stepping into DEPOSIT_CONFIRM
  would hit SendingTransaction, which submits on mount, and the review
  sheet does not exist yet.
  Completes the deposit path — the amount CTA simulates, opens a review
  sheet, and Confirm signs and submits.

  useSimulateEarnDeposit returns the same State<SimulateTxData> shape Send
  and Swap produce, so the shared FeesPane works unchanged; the Blockaid
  scan runs on the prepared transaction, not the pre-assembly build.
  projectEarnings uses simple interest, reproducing the design's worked
  example exactly ($500 at 16.94% -> $84.70/yr).

  Uses dedicated submit/terminal components rather than useSubmitTxData and
  SendingTransaction: the former registers the destination as a recent
  address (here, a pool contract) and signs via the classic thunks; the
  latter hardcodes a Send-shaped summary.

  A failed submission returns to the amount screen with a banner, with the
  terminal blanked for that frame so it never flashes.
  Tapping a token the account holds none of now opens a swap inside the Earn
  flow, pinned to that token, returning to the picker with a toast and the
  new balance. Replaces the interim hand-off to the Swap route.

  EarnSwap is a sibling of the Swap route, owning its own sub-steps so the
  picker stays mounted underneath. Shared additions (all optional, existing
  behaviour preserved): onDone/onClose/onDismissError on TransactionConfirm,
  onDismiss on SubmitFail, isDestinationLocked on SwapAmount, and the
  quote-expiry recovery extracted into useSwapSubmitQuoteExpiry so a stale
  quote does not dead-end a user mid-deposit.

  resolveSwapDestination reads the canonical off the SAC's name() for
  zero-balance tokens, which the earn catalog identifies only by contract
  address — verified against the live contracts.

  Sets ?swap=true while the branch is active so useIsSwap classifies it
  correctly without needing to change.
  The build's i18next scanner had been writing new keys into both locale
  files with empty values, and that output rode along in the four preceding
  Earn commits. i18next returns "" for an empty value (a missing key falls
  back to the key text), so every Earn label rendered blank.

  Fills en and pt for all 47 keys, and adds an `i18n empty values` guard over
  both bundles — the class fix, since the scanner adds blank entries on every
  build and nothing currently fails when it does.

  Also wires the two declared-but-unemitted Earn metrics and adds
  e2e-tests/earnDeposit.test.ts (7 cases). The spec is typechecked but not
  executed: every spec in the suite, including swap.test.ts, fails at import
  on Node v22.12.
    Presents swap-within-earn as a bottom sheet over the still-visible token
    picker, per Figma C3G0a4Gd6RQyplRBppGDsL section 9453:29848, and fixes the
    defects that walking the flow against that design turned up.

    The swap can no longer be a step: the radix sheet portals to the document
    body, so a `display: none` step wrapper cannot hide it. STEPS.SWAP is gone
    and the branch is driven by swapTarget with CHOOSE_TOKEN staying the active
    step underneath; ?swap=true keys off the same state. SlideupModal is unusable
    here because it self-measures via scrollHeight, so .EarnSwapSheet takes an
    explicit 90% height and re-bases the reused Swap Views off 100dvh. Its rules
    are scoped by data-slot because our shadcn Sheet forwards className to the
    overlay as well, and it deliberately has no overflow: hidden — the slide-in
    transform makes it the containing block for the review SlideupModal nested
    inside it, which clipping would cut off. Sheet chrome (56px header, 12px to
    the first card, 24px under the CTA, 40px seam notch) is measured off frame
    9459:47811.
    Every screen in the Earn flow that draws an asset icon was handing AssetIcon
    an empty `assetIcons` map, which it reads as "Method 1 lookup still in
    flight" and answers with a loader — before it consults the `icon` prop at all.
    So those icons never resolved; they sat as blank circles. XLM hid it, because
    its logo is bundled and short-circuits the lookup, and until the swap branch
    worked XLM was the only token reachable as a deposit.
      Brings the deposit screen, its pool card and the wallet-address screen to
      spec, measured against the frames rather than eyeballed. The APY ribbon is
      Colors/Green/10 on Green/4 with 16px top corners, where green-08 on green-01
      at 8px read as muted; the card is 16px with the pool name on Text/Primary
      instead of inheriting Secondary; the pool details sheet gains its Backstop
      row and 16px accepted-token icons; and an over-limit amount turns red rather
      than adding a message row the design does not have — the CTA already reads
      "Insufficient funds".

      Three behavioural fixes alongside it. The swap's receive side is pinned
      again, chosen on the picker before it, so the pill is a label rather than a
      dropdown that silently does nothing. The fiat toggle no longer takes focus
      from the amount input, which was flashing the disabled CTA white on every
      press. And an account holding no XLM no longer sees it named after its
      contract address: the catalog reports native with a null symbol *and* a null
      name, so the code now comes from recognising its SAC.

      Shared components keep their existing behaviour by default — AmountCard's
      invalidAmountStyle and isAssetLocked, SwapAmount's isDestinationLocked — and
      the wallet-address screen's titled chrome is gated on ?flow=earn so its five
      other callers are untouched. The Backstop row reads "--" until a backend
      serves backstop_usd: the v2 backend drops the field its own upstream already
      provides.
@github-actions

github-actions Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

PR Preview build is ready: https://github.com/stellar/freighter/releases/tag/untagged-bc12e6d8bfa6cc2e6720
Backend: sandbox (aristidesstaffieri). SDF collaborators only — install instructions in the release description.

  All 7 tests in earnDeposit.test.ts failed on every PR run. With
  maxFailures: 1, that aborted the whole e2e job — the last CI run reported
  4 failed / 62 passed / 182 did not run. Three separate bugs, all in the
  test code; the feature code was fine.

  Stub scope. stubBlendEarn registered its three routes with page.route,
  but the Blend endpoints are backend-v2: the popup only messages the
  background, and the service worker makes the fetch (blend.ts ->
  fetchBackendV2 -> callBackendV2). page.route never sees those, so every
  request went to the real INDEXER_V2_URL — http://indexer.invalid on CI —
  and the picker rendered its error state. Moved to context.route, the
  convention stubCollectibles already documents. Also registered via
  loginToTestAccount's stubOverrides so the routes exist before the popup
  navigates.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds the Blend-backed Earn deposit flow, including token selection, simulation, review, signing, submission, funding remedies, metrics, and network gating.

Changes:

  • Adds Blend API types, contract encoding, pool allowlists, and deposit simulation.
  • Adds the complete Earn UI, embedded swap path, intro persistence, and analytics.
  • Extends shared amount, transaction, localization, and test infrastructure.

Reviewed changes

Copilot reviewed 104 out of 106 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
@shared/api/helpers/blend.ts Adds Blend API clients and mappings.
@shared/api/helpers/__tests__/blend.test.ts Tests Blend API clients.
@shared/api/internal.ts Adds Earn intro messaging APIs.
@shared/api/types/blend.ts Defines Blend response models.
@shared/api/types/message-request.ts Adds Earn intro messages.
@shared/api/types/types.ts Extends background response data.
@shared/constants/blend.ts Adds pool allowlists and gating.
@shared/constants/sac.ts Adds known SAC addresses.
@shared/constants/services.ts Registers Earn service types.
@shared/helpers/soroban/blend.ts Encodes Blend submit operations.
@shared/helpers/soroban/__tests__/blend.test.ts Tests Blend XDR encoding.
extension/e2e-tests/README.md Documents service-worker request stubbing.
extension/e2e-tests/earnDeposit.test.ts Covers the Earn flow end-to-end.
extension/e2e-tests/helpers/stubs.ts Adds Blend and simulation stubs.
extension/src/background/messageListener/popupMessageListener.ts Registers intro handlers.
extension/src/background/messageListener/handlers/getEarnIntroSeen.ts Reads intro state.
extension/src/background/messageListener/handlers/dismissEarnIntro.ts Persists intro dismissal.
extension/src/background/messageListener/__tests__/getEarnIntroSeen.test.ts Tests intro reads.
extension/src/background/messageListener/__tests__/dismissEarnIntro.test.ts Tests intro persistence.
extension/src/constants/localStorageTypes.ts Adds the intro storage key.
extension/src/helpers/metrics.ts Adds the Earn metric flow.
extension/src/popup/App.tsx Registers the Earn reducer.
extension/src/popup/Router.tsx Registers Earn routes.
extension/src/popup/__testHelpers__/index.tsx Registers Earn in test stores.
extension/src/popup/assets/blend-logo.svg Adds the Blend logo.
extension/src/popup/components/BalanceRow/index.tsx Supports custom row metadata.
extension/src/popup/components/SubviewHeader/index.tsx Supports left-side actions.
extension/src/popup/components/account/AccountHeader/index.tsx Adds the Earn action tile.
extension/src/popup/components/account/AccountHeader/styles.scss Supports variable action counts.
extension/src/popup/components/amount/AmountCard/index.tsx Adds locked and invalid variants.
extension/src/popup/components/amount/AmountCard/styles.scss Styles new card variants.
extension/src/popup/components/amount/AmountCard/__tests__/index.test.tsx Tests invalid amount styling.
extension/src/popup/components/amount/constants.ts Centralizes default amounts.
extension/src/popup/components/amount/helpers/amountDisplay.ts Shares amount display helpers.
extension/src/popup/components/amount/helpers/percentageAmount.ts Shares percentage calculations.
extension/src/popup/components/amount/helpers/__tests__/percentageAmount.test.ts Tests percentage calculations.
extension/src/popup/components/earn/EarnAmount/index.tsx Implements deposit entry and simulation.
extension/src/popup/components/earn/EarnAmount/styles.scss Styles deposit entry and fee sheets.
extension/src/popup/components/earn/EarnAmount/PoolCard.tsx Displays the selected pool.
extension/src/popup/components/earn/EarnAmount/NetworkFeeSheet.tsx Provides XLM funding remedies.
extension/src/popup/components/earn/EarnAmount/hooks/useGetEarnAmountData.tsx Loads balances and prices.
extension/src/popup/components/earn/EarnAmount/hooks/useSimulateEarnDeposit.tsx Simulates and scans deposits.
extension/src/popup/components/earn/EarnAmount/helpers/earnCtaState.ts Computes CTA and fee states.
extension/src/popup/components/earn/EarnAmount/helpers/__tests__/earnCtaState.test.ts Tests CTA and fee logic.
extension/src/popup/components/earn/EarnIntro/index.tsx Adds the first-run intro.
extension/src/popup/components/earn/EarnIntro/styles.scss Styles the intro.
extension/src/popup/components/earn/EarnIntro/hooks/useEarnIntroSeen.ts Manages persisted intro state.
extension/src/popup/components/earn/EarnReview/index.tsx Adds deposit review and hardware signing.
extension/src/popup/components/earn/EarnReview/styles.scss Styles review panes.
extension/src/popup/components/earn/EarnReview/helpers/projectEarnings.ts Calculates projected earnings.
extension/src/popup/components/earn/EarnReview/helpers/__tests__/projectEarnings.test.ts Tests earnings projections.
extension/src/popup/components/earn/EarnReview/__tests__/EarnReview.hardwareWallet.test.tsx Tests hardware signing.
extension/src/popup/components/earn/EarnSubmit/index.tsx Adds deposit progress and success UI.
extension/src/popup/components/earn/EarnSubmit/styles.scss Styles submission states.
extension/src/popup/components/earn/EarnSubmit/hooks/useSubmitEarnTxData.tsx Signs, submits, and refreshes deposits.
extension/src/popup/components/earn/EarnSubmit/__tests__/EarnSubmit.test.tsx Tests software and hardware submission.
extension/src/popup/components/earn/EarnSwap/index.tsx Embeds Swap within Earn.
extension/src/popup/components/earn/EarnSwap/styles.scss Adapts Swap to a sheet.
extension/src/popup/components/earn/EarnTokenPicker/index.tsx Adds supported-token selection.
extension/src/popup/components/earn/EarnTokenPicker/styles.scss Styles picker and remedy sheets.
extension/src/popup/components/earn/EarnTokenPicker/NotEnoughTokenSheet.tsx Adds funding remedies.
extension/src/popup/components/earn/EarnTokenPicker/hooks/useGetEarnTokensData.tsx Resolves pools, balances, and icons.
extension/src/popup/components/earn/EarnTokenPicker/helpers/resolveSwapDestination.ts Resolves embedded-swap destinations.
extension/src/popup/components/earn/EarnTokenPicker/helpers/getNotEnoughVariant.ts Selects available remedies.
extension/src/popup/components/earn/EarnTokenPicker/helpers/__tests__/getNotEnoughVariant.test.ts Tests remedy selection.
extension/src/popup/components/earn/PoolDetailsSheet/index.tsx Displays pool statistics.
extension/src/popup/components/earn/PoolDetailsSheet/styles.scss Styles pool details.
extension/src/popup/components/earn/PoolDetailsSheet/poolDescriptions.ts Adds pool-specific descriptions.
extension/src/popup/components/earn/PoolDetailsSheet/hooks/usePoolReserveIcons.tsx Resolves reserve icons.
extension/src/popup/components/earn/PoolIcon/index.tsx Renders protocol branding.
extension/src/popup/components/earn/PoolIcon/styles.scss Styles protocol branding.
extension/src/popup/components/earn/StatRow/index.tsx Adds reusable Earn stat rows.
extension/src/popup/components/earn/StatRow/styles.scss Styles stat rows.
extension/src/popup/components/earn/helpers/earnAssetIcons.ts Resolves catalog asset identities.
extension/src/popup/components/earn/helpers/formatPoolStats.ts Formats pool statistics.
extension/src/popup/components/earn/helpers/__tests__/earnAssetIcons.test.ts Tests catalog asset identity.
extension/src/popup/components/earn/helpers/__tests__/formatPoolStats.test.ts Tests pool formatting.
extension/src/popup/components/InternalTransaction/SubmitTransaction/index.tsx Supports embedded transaction exits.
extension/src/popup/components/InternalTransaction/SubmitFail/index.tsx Supports custom error dismissal.
extension/src/popup/components/send/SendAmount/index.tsx Uses shared amount helpers.
extension/src/popup/components/swap/SwapAsset/index.tsx Supports custom picker back icons.
extension/src/popup/components/swap/SwapAmount/index.tsx Supports locked and sheet layouts.
extension/src/popup/components/swap/SwapAmount/styles.scss Styles embedded Swap variants.
extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.layout.test.tsx Tests locked destination layout.
extension/src/popup/components/swap/SwapAmount/helpers/__tests__/swapAmountHelpers.test.ts Updates shared helper tests.
extension/src/popup/components/swap/hooks/useSwapSubmitQuoteExpiry.ts Shares quote-expiry recovery.
extension/src/popup/constants/earn.ts Defines Earn steps and query state.
extension/src/popup/constants/metricsNames.ts Adds Earn metric names.
extension/src/popup/constants/routes.ts Adds the Earn route.
extension/src/popup/constants/__tests__/metricsNames.test.ts Tests Earn metric names.
extension/src/popup/ducks/earn.ts Adds Earn workflow state.
extension/src/popup/helpers/blendDeposit.ts Builds and simulates deposits.
extension/src/popup/helpers/searchAsset.ts Reuses canonical SAC constants.
extension/src/popup/locales/en/translation.json Adds English Earn strings.
extension/src/popup/locales/pt/translation.json Adds Portuguese Earn strings.
extension/src/popup/locales/__tests__/translationParity.test.ts Enforces translation parity.
extension/src/popup/metrics/earn.ts Adds Earn funnel emitters.
extension/src/popup/metrics/earn.test.ts Tests Earn event payloads.
extension/src/popup/metrics/views.ts Excludes the Earn container view.
extension/src/popup/metrics/views.test.ts Tests container-route exclusions.
extension/src/popup/views/Earn/index.tsx Orchestrates the Earn step machine.
extension/src/popup/views/Earn/styles.scss Adds step transitions.
extension/src/popup/views/Earn/__tests__/Earn.submitFailure.test.tsx Tests failure recovery and retries.
extension/src/popup/views/Swap/index.tsx Uses shared quote-expiry recovery.
extension/src/popup/views/ViewPublicKey/index.tsx Adds Earn-specific receive chrome.
extension/src/popup/views/ViewPublicKey/styles.scss Styles the Earn receive header.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread extension/src/popup/Router.tsx Resolved
  The existing-position fetch was fired without being awaited, so review
  could open on the simulation alone and then flip Position, Monthly and
  Yearly under the user when the slower lookup landed. Await both, with the
  position leg resolving to "0" on failure so it still cannot block a
  deposit.
  Closing the "Depositing" screen navigates back to the account view rather
  than closing the popup, so the submit hook's continuation kept running and
  emitted earn.deposit_completed after earn.deposit_abandoned. The deposit was
  never abandoned — only the screen was.

  Worse than the double count was the asymmetry: a post-close success still
  emitted from the hook's closure, while a post-close failure emitted nothing,
  because earn.deposit_failed lived in the Earn view's effect and that view had
  unmounted.

  - useSubmitEarnTxData now emits both outcomes, from the closure that outlives
    the screen. The Earn view keeps the failure UI work but skips its emit while
    DEPOSIT_CONFIRM is active, so it still owns the failures that never reach
    that step (a device-rejected signature at review) without double counting.
  - Rename earn.deposit_abandoned to earn.deposit_processing_dismissed: a UX
    signal, not an outcome. The outcome is lost only when the popup itself
    closes.
  - Ignore a submitStatus left behind by a previous flow. A late rejection wrote
    ERROR back after closeEarnFlow had reset the submission, and re-entering Earn
    read it as its own failure — emitting against an empty asset and dropping the
    fresh flow onto the amount screen.
  - Extract getFailureReasonCode, now shared by both emitters, and stop it
    throwing: the soroban submit thunk rejects with the parsed response body in
    errorMessage, which scrubStrKeys called .replace on. Inside the hook's
    try/catch that swallowed the failure entirely.
  The pool details sheet listed every reserve the catalog reports under
  "Accepted tokens", including reserves whose `enabled` flag is false.
  Blend's `require_action_allowed` panics with `ReserveDisabled` (#1223)
  on Supply, SupplyCollateral, and Borrow into such a reserve — only
  Withdraw and Repay stay open — so the row could claim a token was
  depositable when it was not.

  The backend already applies the same filter one endpoint over:
  `deriveEarnOptions` skips disabled reserves, so the token picker never
  offers them. The pools catalog deliberately reports every reserve with
  its flag and leaves the decision to the client, which left the sheet as
  the only place able to contradict the picker on the same screen.

  Filter through a shared `getAcceptedReserves`, used by both the icon row
  and the icon-resolution hook so the two cannot drift and no icon is
  fetched for a token that is never drawn. An all-disabled pool renders
  "--", the same unavailable marker the USD and rate rows use.

  No currently served pool is affected — all three mainnet Fixed Pool
  reserves are enabled — but a pool admin can flip the flag at any time.

  Covered by a unit test on the helper and by the e2e pool-details test,
  whose Blend stub now carries a disabled reserve alongside the enabled
  ones.
  The submit-failure branch in `useSubmitEarnTxData` emitted
  `earn.deposit_failed` and then fell through to the shared tail, which
  dispatches `FETCH_DATA_SUCCESS`. Both paths reached `RequestState.SUCCESS`,
  so the hook's own state reported a rejected deposit as a successful one.
  Its sign-failure sibling a few lines above already has the right shape:
  track, `FETCH_DATA_ERROR`, return.

  Nothing was visibly broken, because two guards outside the hook kept the
  wrong value off screen. `EarnSubmit` ands the hook's state together with
  redux `submitStatus`, which the submit thunk independently sets to ERROR,
  and that conjunct also gates the `earn_success` screen view. The Earn view
  goes further and unvisits DEPOSIT_CONFIRM on a failure, so the screen
  unmounts before it could render anything. The defect is in the signal, not
  the UI: anything that later trusts the hook directly — a refactor, a second
  consumer, a test — silently gets "the deposit succeeded".

  Give the branch its early return and flatten the now-redundant `else`, so
  the success tail is only reachable from a fulfilled submission and this
  class of fall-through cannot recur. Behaviour on screen is unchanged;
  `isLoading` is `!isSuccess` either way.

  Cover it at the hook level rather than through the component: both states
  render "Depositing", so the DOM cannot tell them apart, which is why the
  existing failure test — asserting only on the emitted metric — stayed
  green. Extract the store fixture out of `renderSubmit` so the new
  `renderHook` cases share it, and assert both outcomes so a future change
  cannot break success the same way.
  useSimulateEarnDeposit scanned the prepared deposit and wrote the result
  onto its SimulateTxData payload, but nothing read it: EarnReview pulled
  only transactionXdr off simulationState.data, so Earn was the one
  signing surface that computed a verdict and dropped it. If Blockaid ever
  flagged the Blend pool contract, the review said nothing.

  Consume scanResult in EarnReview through the same helpers the other
  review screens use — useShouldTreatTxAsUnableToScan,
  useBlockaidOverrideState, getTransactionSecurityLevel. Only the
  transaction verdict applies here, since Earn's reserves come from the
  backend allowlist and there is no counterparty token to scan, so this
  reads the level directly rather than merging several.

  A flagged deposit now renders BlockaidBanner above the review body,
  opens the BlockAidScanExpanded reasons sheet, and demotes confirmation:
  Cancel takes over the Confirm slot (destructive when malicious) and a
  "Confirm anyway" text button appends below the row, keeping the fee gear
  in place. The sheet renders after the hardware-sign check so confirming
  from it with a device connected still swaps in HardwareSign.

  The action row is built locally rather than reusing ReviewTx's
  ActionButtons, which hardcodes the Send/Swap CTA copy and takes memo
  props this flow has none of.
@aristidesstaffieri

Copy link
Copy Markdown
Contributor Author

scanTx result is computed but never read

Nit / question — no security impact, not a merge blocker.
TL;DR: The Earn deposit flow runs a Blockaid scan on the prepared transaction and stores the verdict on the simulation payload, but nothing downstream ever reads it — the review sheet doesn't surface it and the Confirm button doesn't consider it. Flagging it in case wiring up the warning UI was intended and got left behind; if it was deliberate (the scan and simulate endpoints share a host, so the verdict adds little here, and the deposit target is a pinned constant rather than anything caller-supplied), then the round-trip itself is probably worth dropping.
Detailed explanation (for agents)

great catch, it seems like this actually needs design input so I will look into that and get back to this thread.

@CassioMG update: this is now implemented according to the new design layout in 92e5f56

@aristidesstaffieri

Copy link
Copy Markdown
Contributor Author

earn.max_amount_selected fires for every percentage shortcut, not just Max

Suggestion (analytics correctness) — wrong-by-name event data from the moment the flow ships; cheap to fix now, awkward to fix once dashboards are built on it. Not a merge blocker.

TL;DR: All four percentage shortcuts on the deposit amount screen — 25%, 50%, 75% and Max — emit the same event named "max amount selected". The event does carry a percent property, so the underlying data is recoverable, but anything keyed on the event name alone (dashboards, funnels, alerting) will count a 25% tap as Max usage. The Send flow shipped this exact behaviour, got called out for it in review, and was subsequently fixed to emit only on the 100% tap — this is the same pattern going in the other direction.

Detailed explanation (for agents)

this is addressed in c60e904

@aristidesstaffieri

Copy link
Copy Markdown
Contributor Author

Earn submit hook reports status: "success" when the submission fails

Bug (latent) — the hook's own state is wrong on every failed deposit, but a second check in the consuming component currently keeps the wrong value off the screen. Not a merge blocker.

TL;DR: When the on-chain submission is rejected, the deposit hook records the attempt as a success anyway — it tracks the failure metric, then falls through and reports success regardless. Nothing is visibly broken today, because the screen decides what to render by also consulting the Redux submission status, which is correctly set to error. The risk is that the hook's own signal is wrong: anyone who later trusts it directly — a refactor, a new consumer, a test exercising the hook in isolation — silently gets "the deposit succeeded" for a deposit that failed.

Detailed explanation (for agents)

this is addressed in 55b1057

  Add `earn_deposit` as an Amplitude boolean flag, defaulting to off, and
  require it alongside the existing network check before the Earn action
  renders in the home action row. The default keeps the tile hidden when
  the flag fetch fails or no deployment key is configured.

  The /earn route itself stays registered — only the entry point is gated.

  - remoteConfig: new flag, default, and earnDepositSelector
  - AccountHeader: flag && isEarnSupportedNetwork gate on the tile
  - stubBlendEarn: serve earn_deposit "on" from the Experiment vardata
    route so the Earn e2e specs still reach the flow
  - backfill earn_deposit in hand-built RemoteConfigState test fixtures
  The codebase already exports NO_FIAT_VALUE from popup/helpers/formatters
  for a USD figure that cannot be determined, with docs distinguishing it
  from "$0.00" (unknown vs. known zero). The earn flow was hardcoding the
  same "--" glyph in five places while documenting that same distinction in
  its own comments.

  Swap the literals for the shared constant in formatCompactUsd, formatRate,
  formatProjection, the review screen's USD line, the token picker's APY
  badge, and the pool sheet's empty accepted-tokens state.

  No behavior change — the rendered output is identical, and the tests still
  assert the literal "--" so a change to the constant's value would be caught.
@CassioMG CassioMG mentioned this pull request Aug 26, 2026
1 task
CassioMG and others added 5 commits November 3, 2026 13:52
Brings the Earn deposit branch up to date with master: 235 commits,
including the Varial rebrand, light and dark theming, and the
stellar-sdk v17 upgrade.

Conflicts. Each keeps both sides unless noted:
- Message types, storage keys, handlers and the internal API: the Earn
  intro messages sit beside master's rebrand-announcement claim.
- remoteConfig: earn_deposit joins master's use_token_price_history, and
  master's use_balances_v2 default (now false) is kept.
- AccountHeader: rebuilt on master's Varial Home header. Earn is a fourth
  action tile, gated as before (flag plus an allowlisted network), and the
  row takes auto columns so three or four tiles share it.
- AmountCard: the locked-pill restructure, with master's
  isNativeAssetPair check moved into the shared pill.
- SubmitFail, SubviewHeader, Swap, ViewPublicKey: onDismiss, leftButton
  and the quote-expiry hook beside master's telemetry move, isFloating,
  swap-source and showToast changes.
- searchAsset: master's derived native SAC (getNativeContractId) replaces
  the SACS lookup.
- Screen views: earn stays a non-emitting container beside master's
  redirect routes.
- Translations: a three-way key merge. The 60 Earn keys are added and
  master's changes kept.

Fixes for what master changed underneath the branch:
- stellar-sdk v17: XDR struct fields and union arms are properties
  (rootInvocation, signature), and the Blend encoding tests read them
  through expectUnionVariant. The golden request XDR is unchanged.
- SlideupModal dropped hasBackdrop. Its backdrop always covers the
  viewport now.
- isNativeBalance moved to @shared/helpers/assetIdentity, and the
  asset-identity lint rule replaces three "native" comparisons with
  isNativeAssetId.
- Reset wallet erases hasSeenEarnIntro, like Discover's welcome flag.
- The picker's scroll-fade mask stop is `black`, the way master writes
  mask stops, so the palette guard passes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The branch predates theming, and two of its surfaces were drawn for the
dark frame only.

- The glow behind the intro and picker was an SVG with the dark frame's
  green baked in. On the light page it rendered as a grey smudge. It is
  now a CSS disc (EarnGlow) with a token per theme: green-04 in dark, the
  nearest token to the original, and green-06 in light. The design has no
  light frame for it, and green-06 keeps it about as visible on white as
  the dark glow is on black. filter: blur() takes the Gaussian's standard
  deviation, so the falloff is still the asset's 100px. Dropping the asset
  also removes a filter id the two mounted steps would have shared.
- The swap sheet hosts the Swap screens, whose View.Content inset now
  paints the page colour (base-00). That drew a black slab on the gray-01
  sheet in dark mode and a white one in light. The insets take the sheet
  colour, as master does inside SlideupModal.

Everything else already takes its colours from SDS tokens. Checked screen
by screen in both schemes against the built extension.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every Earn spec failed before the merge too: CI went red once
earn_deposit went back to defaulting off (7ac8fb8).

- The flag stub was a context.route, and neither that nor a page.route
  sees the test page's own Experiment request. It reached the real
  endpoint, 401'd on the test key and left the flag off, so the Earn tile
  never rendered. stubEarnDepositFlag wraps window.fetch in an init script
  registered before login instead, which is how stubSacViaInitScript stubs
  the popup's early fetches.
- The custom-network spec drove the header's account and network
  dropdowns, which master's Home restyle removed. It goes through Settings
  -> Network now, selects the new network there, and backs out to Home.

All seven pass against the built extension.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Small catch-ups with patterns master settled after the branch was cut:

- Every SlideupModal names its dialog with its visible title. The four
  Earn sheets now do too: "Review deposit" (like "Review send" and
  "Review swap"), the pool's name, the fee sheet's and the not-enough
  sheet's headings. No new strings.
- The swap-success toast goes through showToast. It used a stable id,
  and master documents that a stable id can stop a toast rendering once
  an earlier one with that id has been dismissed.
- Two Earn rules used the dead --font-weight-medium, which resolves to
  nothing, so they rendered at the inherited weight. They now use
  --sds-fw-medium, matching their Text/*/500 specs. Master's own uses of
  that namespace are left to its typography fix.
- Drops a comment orphaned when DEFAULT_AMOUNT moved out of SwapAmount,
  and corrects sac.ts, whose doc still said the asset search needed the
  native SAC before master derived it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two more catch-ups with master:

- The picker's and amount screen's onboarding reroutes called
  window.close(). In sidebar mode that is rebound to "navigate home", so
  the panel stayed open and re-ran the hand-off. Master routes every such
  call through closeWindow(), which uses the native close there.
- The pool card's chevron disc is Default/Background/Primary, the page
  colour. Varial pages paint that as base-00, not gray-01, the same move
  master made for its page-coloured discs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CassioMG
CassioMG marked this pull request as draft November 4, 2026 00:12
CassioMG and others added 2 commits November 3, 2026 16:26
earn_deposit now defaults to true, and Amplitude turns it off rather than
on. It is read through killSwitchSelector, master's pattern for default-on
flags: the tile stays hidden until the flags settle, so it can't flash in
before Amplitude switches it off. A failed flag fetch still settles, so an
unreachable Amplitude leaves Earn on.

stubEarnDepositFlag stays, so the Earn specs don't depend on the default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Earn now targets the v2.1 deployment's Fixed pools:

- mainnet "Fixed v2.1": CBAKAZUEJBAFA2DCGBRS2WKJT3FNIMJBHTFL76AY7XMAVEINQNNIQWAO
- testnet "Testnet v2.1": CB3447A446DY3USGWPIDXTNQHF5EX24XDQA2EN6TAZP62JTPBTNOA52Q

Both run the same pool wasm as the v2 pools they replace, with the same
reserves, so the request encoding and submit call are unchanged. Each
sits on its network's v2.1 backstop, and a simulated deposit of the
request Earn builds succeeds on both.

The unit tests read BLEND_FIXED_POOL_IDS instead of a copied literal,
as the e2e stubs already did. The e2e deposit envelope targets the new
mainnet pool, and the stubs carry its live name. The freighter-backend-v2
allowlist (configs/earn-pools.json) has to list the same IDs, or the
picker discards every offer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CassioMG

CassioMG commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Re-opening this under my account so PR Previews build against my sandbox. Same branch and commits; the new PR links back here: #3062

@CassioMG CassioMG closed this Nov 5, 2026
@CassioMG CassioMG mentioned this pull request Nov 5, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants