feat(spark): stable-sats receive auto-convert and send-flow fixes#3764
Open
esaugomez31 wants to merge 12 commits intofix--spark-settings-and-onboarding-uifrom
Open
feat(spark): stable-sats receive auto-convert and send-flow fixes#3764esaugomez31 wants to merge 12 commits intofix--spark-settings-and-onboarding-uifrom
esaugomez31 wants to merge 12 commits intofix--spark-settings-and-onboarding-uifrom
Conversation
This was referenced Apr 25, 2026
This was referenced Apr 25, 2026
Collaborator
Author
1d89661 to
8e3715b
Compare
6ac3787 to
ece95dc
Compare
8e3715b to
c930670
Compare
This was referenced Apr 29, 2026
c930670 to
8646588
Compare
ece95dc to
a918f3a
Compare
…onvert fee to settlement currency
a918f3a to
fffa4ca
Compare
8646588 to
5bf699b
Compare
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.

Spark: stable-sats receive auto-convert and send-flow fixes
What this PR does
Completes the USD wallet experience for self-custodial Spark. Users can now receive payments as stable sats (incoming BTC is automatically converted to USDB right after a Lightning receive) and send from a USD wallet through the Spark SDK's built-in USDB→BTC swap. It also fixes several UX bugs on the confirmation and transaction-detail screens that misrepresented Spark transactions, and introduces a structured SDK-error pipeline so failures show clear, translated messages instead of raw SDK strings or "Something went wrong".
Receive side
Added a new auto-convert module (
app/self-custodial/auto-convert/) with an executor, a persistent pending-conversion storage, a listener hook, and a mount component wired intoapp.tsx. It watches for completed Lightning receives and, when the active wallet is USD, converts the incoming BTC to USDB using the exact received amount. The storage makes the queue durable across app restarts so a conversion that fails mid-flight is retried on next launch. A post-convert sync is triggered so the new USDB balance shows up immediately rather than waiting for the next wallet poll, and the older balance-stale heuristic in the wallet provider has been removed in favor of this deterministic path.Smaller receive polish: the
lightning:URI prefix is stripped from the displayed invoice, the receive toggle icon hides at opacity 0 when the wallet is locked (keeps the layout from shifting), and a newuseReceiveAssetModehook centralizes the decision of whether to receive BTC or USDB.Convert flow (BTC ↔ USDB)
Reworked the convert bridge to support exact-input amounts and to surface the fee as a DisplayAmount so the UI can render it in the user's display currency cleanly. The
useNonCustodialConversionLimitshook now returns a typed min/max shape that the UI uses to validate before submitting. Existing tests were updated and new ones added for the convert bridge, stable-balance bridge, limits bridge, and token-balance bridge.Send side — three bugs fixed
Sending from a USD wallet over Lightning was routing wrong because the code was passing
tokenIdentifier: "usdb-token-id"to the SDK, which is for the destination asset. For a USD→BTC Lightning send the SDK expectsconversionOptions: ToBitcoin({ fromTokenIdentifier })instead. The lightning payment-details now passesconversionOptionsfor USD wallets and nothing for BTC wallets;tokenIdentifieris no longer used on this path.The fee returned by the SDK is always in sats, but the confirmation screen was summing it into the settlement amount without conversion — so a BTC fee got added to a USD balance, producing wrong totals and false "amount exceeds balance" warnings. The screen now converts the fee to the settlement currency before adding it, and the send-helpers always return the fee as a BTC money amount so the conversion boundary is explicit.
During a successful send, the SDK briefly reports a zero balance while the settlement broadcasts, which flagged the amount as exceeding balance mid-flight and blocked the confirm button. Added a
skipBalanceCheck = isSendingMax || hasAttemptedSendguard so balance validation is bypassed once the user has committed to sending.Structured SDK errors
A new
classifySdkErrorfunction maps all twelveSdkErrortags (SparkError,InsufficientFunds,NetworkError,ChainServiceError,MaxDepositClaimFeeExceeded,InvalidInput,InvalidUuid,LnurlError,MissingUtxo,StorageError,Signer,Generic) to a small stable set ofSelfCustodialErrorCodevalues (InsufficientFunds,BelowMinimum,NetworkError,InvalidInput,Generic). Wrapper tags likeSparkErrorandGenericare further refined by a case-insensitive.includes()check on the inner string for hints like "insufficient", "minimum", "network", "timeout", with the more specific hints evaluated first. TheTAG_TO_CODEmap is typed as an exhaustiveRecord<SdkErrorTags, …>so any future SDK tag will fail to compile until it's mapped.A new
useTranslateSdkErrorhook translates those codes into user-facing strings via a newSelfCustodialErrori18n namespace (insufficientFunds, belowMinimum, networkError, invalidInput, generic), added in all 28 locales with the appropriate diacritics. The confirmation screen uses the hook for any error message coming back from the send mutation, falling back to the generic "something went wrong" string only when the code isn't recognized. The old "Send failed: …" raw string fallback is gone.UI polish
The confirmation screen header was showing "Destination - " with an empty label for Spark sends because
transactionType()had no case forpaymentType === "spark". Added the case; it now readsLL.common.spark().The transaction detail screen was labeling self-custodial Spark transactions as "Lightning" because the self-custodial→GraphQL fragment mapper routes Spark through
SettlementViaLn. ThetypeDisplayhelper is now exported and takes an optionalselfCustodialPaymentType; when the active wallet is self-custodial and the transaction is Spark, the detail screen correctly shows "Spark". Custodial transactions are unchanged.Breaking notes
None for consumers. The self-custodial
createGetFee,createGetFeeOnchain, and related send-helpers lost theircurrencyparameter — the fee is now always returned in sats and the UI reconverts. All in-tree callers were updated in the same commits. The custodial send path is untouched.Tests
Full suite passes: 326 suites, 3403 tests. New specs cover the classifier (every tag branch plus inner-string refinement), the translator hook, the conversionOptions path on both
prepareSendand the send mutations, the auto-convert executor and its storage, the auto-convert listener hook and mount component, the receive-asset-mode hook, and thetypeDisplaySpark override. Existing payment-details, bridge, and mapper specs were updated for the new signatures and removed-mock surface.