Skip to content

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
feat--spark-stable-receive-and-send-fixes
Open

feat(spark): stable-sats receive auto-convert and send-flow fixes#3764
esaugomez31 wants to merge 12 commits intofix--spark-settings-and-onboarding-uifrom
feat--spark-stable-receive-and-send-fixes

Conversation

@esaugomez31
Copy link
Copy Markdown
Collaborator

@esaugomez31 esaugomez31 commented Apr 25, 2026

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 into app.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 new useReceiveAssetMode hook 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 useNonCustodialConversionLimits hook 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

  1. 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 expects conversionOptions: ToBitcoin({ fromTokenIdentifier }) instead. The lightning payment-details now passes conversionOptions for USD wallets and nothing for BTC wallets; tokenIdentifier is no longer used on this path.

  2. 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.

  3. 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 || hasAttemptedSend guard so balance validation is bypassed once the user has committed to sending.

Structured SDK errors

A new classifySdkError function maps all twelve SdkError tags (SparkError, InsufficientFunds, NetworkError, ChainServiceError, MaxDepositClaimFeeExceeded, InvalidInput, InvalidUuid, LnurlError, MissingUtxo, StorageError, Signer, Generic) to a small stable set of SelfCustodialErrorCode values (InsufficientFunds, BelowMinimum, NetworkError, InvalidInput, Generic). Wrapper tags like SparkError and Generic are 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. The TAG_TO_CODE map is typed as an exhaustive Record<SdkErrorTags, …> so any future SDK tag will fail to compile until it's mapped.

A new useTranslateSdkError hook translates those codes into user-facing strings via a new SelfCustodialError i18n 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 for paymentType === "spark". Added the case; it now reads LL.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. The typeDisplay helper is now exported and takes an optional selfCustodialPaymentType; 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 their currency parameter — 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 prepareSend and the send mutations, the auto-convert executor and its storage, the auto-convert listener hook and mount component, the receive-asset-mode hook, and the typeDisplay Spark override. Existing payment-details, bridge, and mapper specs were updated for the new signatures and removed-mock surface.

@esaugomez31 esaugomez31 marked this pull request as ready for review April 25, 2026 02:01
@esaugomez31 esaugomez31 requested review from Copilot and removed request for Copilot April 25, 2026 02:01
@esaugomez31 esaugomez31 self-assigned this Apr 25, 2026
@esaugomez31 esaugomez31 force-pushed the feat--spark-stable-receive-and-send-fixes branch from 1d89661 to 8e3715b Compare April 26, 2026 18:14
@esaugomez31 esaugomez31 force-pushed the fix--spark-settings-and-onboarding-ui branch from 6ac3787 to ece95dc Compare April 28, 2026 16:27
@esaugomez31 esaugomez31 force-pushed the feat--spark-stable-receive-and-send-fixes branch from 8e3715b to c930670 Compare April 28, 2026 16:27
@grimen grimen changed the title Spark: stable-sats receive auto-convert and send-flow fixes feat(spark): stable-sats receive auto-convert and send-flow fixes Apr 28, 2026
@esaugomez31 esaugomez31 force-pushed the feat--spark-stable-receive-and-send-fixes branch from c930670 to 8646588 Compare May 6, 2026 02:05
@esaugomez31 esaugomez31 force-pushed the fix--spark-settings-and-onboarding-ui branch from ece95dc to a918f3a Compare May 6, 2026 02:05
@esaugomez31 esaugomez31 force-pushed the fix--spark-settings-and-onboarding-ui branch from a918f3a to fffa4ca Compare May 7, 2026 15:54
@esaugomez31 esaugomez31 force-pushed the feat--spark-stable-receive-and-send-fixes branch from 8646588 to 5bf699b Compare May 7, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant