Repository navigation
Conversation
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.
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.
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.
Ports two Earn screens from Freighter-Mobile: "Blend intro (first time user)" (13701:332277) and "Choose an asset" (13717:332943), plus its empty state (13701:332629). The mocks are 402x874 phone frames against a 360x600 popup, so horizontal measures carry over unchanged — both use 24px gutters — while the vertical slack the phone frame has to spare is what gives.
Deposits use Blend `request_type` 2 (SupplyCollateral) rather than 0 (Supply) so the position lands in the pool's `collateral_tokens` instead of `supply_tokens`, leaving the account's borrowing power open. That reasoning only lived in the PR description; the enum's own docstring covers the naming but not why the deposit flow picks 2. Record it where the choice is actually made.
All four percentage shortcuts on the deposit amount screen — 25%, 50%,
75% and Max — emitted `earn.max_amount_selected`. The `percent` property
kept the data recoverable, but anything keyed on the event name alone
(dashboards, funnels, alerting) would count a 25% tap as Max usage.
Send and Swap already gate this: both emit the shared
`payment.max_amount_selected` only when `pct === 100`, matching mobile
(RFC #2883, D5), and Swap's telemetry test asserts a partial tap emits
nothing. Earn was the only amount screen that widened the trigger.
Gate inside `trackEarnPercentAmountSelected` rather than at the call
site, so the decision stays in the metrics module and `EarnAmount`'s
handler is untouched. Keep the earn-scoped name — action events carry no
`flow` property, so reusing `payment.max_amount_selected` would make
Earn max taps indistinguishable from Send's — and keep `percent` (now
always 100) for payload symmetry with mobile. If Earn later wants
partial-shortcut usage, that belongs in a separately named event.
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.
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.
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]>
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]>
4 tasks done
Contributor
|
PR Preview build is ready: https://github.com/stellar/freighter/releases/tag/untagged-2bd986a0bf2c13a2cd31 |
Brings in master's 9 commits since the last merge: balances and prices wait for the remote flags, StellarExpert asset search moves to backend v2, wallet-sdk-km 6.0.0, copy contract address on AssetDetail, and the 5.52.0 version bump. Conflict: - AssetDetail.test.tsx takes master's version. This branch's only change to it was the previous merge hook's line re-wrap, and master since added the copy-address tests in the same place. Follow-on change: - Master's test Wrapper now preloads remoteConfig as loaded, with kill switches off, so balance fetches don't wait forever. It predates earn_deposit, so earn_deposit: false joins that list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
Adds an Earn flow to the extension. From a new Earn tile on Home, users pick a supported token, see its supply APY in Blend's Fixed pool, enter an amount, review projected earnings, and deposit, on mainnet and testnet only. The deposit goes to Blend's v2.1 Fixed pool. This is deposit only, with no positions view or withdraw yet. It continues #2974, re-opened under my account so PR Previews build against my sandbox. Since #2974 it has master merged in, light and dark theme support, and the move to the v2.1 pools.
Example Flow (old videos from previous PR as reference)
Screen.Recording.2026-08-24.at.9.16.37.AM.mov
Screen.Recording.2026-08-24.at.9.32.29.AM.mov
Implementation details (for agents)
What changed
From #2974:
@shared/helpers/soroban/blend.ts: request andsubmitencoding. The struct'sScMapis built by hand becausenativeToScValsorts keys withlocaleCompare, which is not byte order.@shared/api/{helpers,types}/blend.ts: clients for earn-options, pools and positions. AnullUSD or APY means unavailable and is never coalesced to0.@shared/constants/blend.ts: the pool per network, plusisEarnSupportedNetwork, which gates the tile and the route.popup/views/Earn/andpopup/constants/earn.ts: a step machine modelled on Send, where every visited step stays mounted. Each sheet (pool details, not enough, network fee, review, swap) belongs to its host step.popup/components/earn/: the screens and their sheets, CTA-state and projected-earnings helpers, pool-stat formatting, asset-icon resolution.popup/helpers/blendDeposit.ts: build and simulate. Auth comes back with source-account credentials, so there is noauthorizeEntryround trip, including on hardware wallets.popup/components/{amount,swap}/: shared percentage helpers, and the Swap screens adapted to run inside Earn's sheet. No behaviour change for Send or the standalone Swap.background/messageListener/handlers/{get,dismiss}EarnIntro.ts: persists the one-time intro.popup/metrics/earn.ts: funnel events with asset codes,pool_idandapy, and no amounts or fiat.Since #2974:
Master merged in (Varial rebrand, theming, stellar-sdk v17). The Earn tile is a fourth action tile in the new Home header. The code moved to the v17 XDR accessors, the asset-identity predicates, and master's sheet and toast conventions (
ariaLabel,showToast,closeWindow). Reset wallet erases the Earn intro flag. The on-chain request encoding is byte-identical: the golden XDR test is unchanged.Light and dark themes. The glow behind the intro and picker is now a CSS disc with a token per theme (
EarnGlow) instead of an SVG baked for dark. The swap sheet's insets take the sheet colour. Everything else was already token-based.Feature flag.
earn_depositdefaults on and is read fail-closed (killSwitchSelector), so Amplitude can turn Earn off without the tile flashing first.Blend v2.1 pools in
@shared/constants/blend.ts:CBAKAZUEJBAFA2DCGBRS2WKJT3FNIMJBHTFL76AY7XMAVEINQNNIQWAOCB3447A446DY3USGWPIDXTNQHF5EX24XDQA2EN6TAZP62JTPBTNOA52QBoth run the same pool wasm as the v2 pools, with the same reserves.
e2e. The
earn_depositflag is stubbed through an init script (stubEarnDepositFlag), because routes never see the popup's flag request. The custom-network spec goes through Settings → Network.The merge commit's pre-commit hook re-wrapped 9 files that master had committed unformatted: CONTRIBUTING.md, the PR template, 3 workflows and 4 tests. The change is formatting only.
Notes for the reviewer
request_type2 (SupplyCollateral), not 0, so the position lands in collateral and the user's borrowing power stays open.Depends on
/protocols/blend/*endpoints, withconfigs/earn-pools.jsonlisting the two v2.1 pool IDs. The extension discards offers from any other pool.Verification (at
ba8e499)yarn test:ci: 312 suites and 2,812 tests pass.yarn build:extensionpasses.yarn test:e2e: the Earn specs pass (7/7). The full suite passed (272, 0 failed) at8e55e875, the commit before the pool change.Before merge
signing.*events, and the swap inside Earn reports only part of the swap funnel.Follow-ups
Example flow (recorded on #2974, before the Varial redesign)
Screen.Recording.2026-08-24.at.9.16.37.AM.mov
Screen.Recording.2026-08-24.at.9.32.29.AM.mov
🤖 Generated with Claude Code