Payments 6/8: Wallet onboarding and non-custodial payment UX for non-crypto-native users - #151
Conversation
…stimate Implements wallet onboarding and non-custodial payment UX (issue LadderMine#143): - FreighterAdapter (app/src/lib/wallet/freighterAdapter.ts): a real WalletAdapter implementing signTransaction against window.freighter — previously nothing in the app ever called it. Error classification mirrors the TypeScript SDK's TransactionPipeline.classifySignerError (same regexes, same disconnect-before-reject ordering) so wallet errors are handled identically wherever they're thrown from. checkNetwork() detects a wallet pointed at the wrong network before signing, added as an optional WalletAdapter method. - adapters.ts: explicit per-provider adapter resolution — never "whichever extension loaded first." LOBSTR/xBull throw a clear, typed WalletUnavailableError; WalletButton now renders them as visibly disabled with "Coming soon," not a silent stub that only fails on click. - paymentFlow.ts's requestWalletSignature is real now: given an XDR, it resolves the connected wallet's adapter, checks network, and signs — reachable and tested through the exact call site runPaymentSubmission uses. It's still a no-op when no XDR is given, because building the actual deposit/early-exit transaction depends on the SDK becoming a real app dependency, which needs a pnpm install to regenerate app/pnpm-lock.yaml correctly — this environment can't safely run that (same blocker already documented on placeholderSubmissionHash). Neither page passes a real XDR yet, so today's actual signing behavior is unchanged; this wires the real, tested path for the moment one does. - Trustline detection (services/horizon.ts) is fully real and dependency-free: Horizon's /accounts/{id} returns trustlines as plain JSON, no XDR encoding needed. The deposit flow gets a new step that checks for a missing USDC trustline before confirm and explains what to do — actually adding the trustline in-app remains blocked by the same SDK dependency (changeTrust also needs XDR construction), stated honestly in the UI rather than faked with a placeholder signing prompt. - Fee estimate (lib/feeEstimate.ts) replaces the hardcoded '0.00001 XLM' with getFeeStats() — real, current network-wide fee data requiring no transaction to build, presented as an "up to" (p99) ceiling with a one-line network-fee-not-YieldLadder-fee note, never a fabricated number on failure. - Non-custodial explainer copy added at the deposit confirm step and EarlyExitModal, grounded in the README's actual architecture (the Strategist can propose allocations but never withdraw funds; every withdrawal needs the user's own signature) rather than generic marketing language. Note: EarlyExitModal's "Exit Early" button was already wired to the real submission pipeline as of LadderMine#142 — the issue's description of it as unhandled predates that merge. This PR adds the explainer copy there and leaves the already-working handler alone.
|
@bellabuks is attempting to deploy a commit to the wumibals' projects Team on Vercel. A member of the Team first needs to authorize it. |
wumibals
left a comment
There was a problem hiding this comment.
Reviewed the wallet onboarding and non-custodial payment UX PR (closes #143).
- Making
signTransaction's shape identical to the SDK'sSignerinterface, and mirroringclassifySignerError's exact regexes and disconnect-over-reject ordering, meansFreighterAdapteris a drop-insignerthe moment the app depends on the SDK directly — no adapter-layer rewrite needed later, just a wiring change. That's the right way to build ahead of a known future dependency. - Explicit per-provider adapter resolution instead of "whichever extension loaded first" directly closes the multi-wallet-extension edge case named in the issue, and rendering LOBSTR/xBull as visibly disabled with a "Coming soon" badge is honest UX — better than a button that looks functional but silently fails on click.
- Checking network mismatch both right after connecting and again right before signing (not just once) correctly accounts for a user switching networks mid-session in their wallet extension, surfacing it as a typed error instead of an opaque submission failure downstream.
- Trustline detection via Horizon's plain-JSON
/accounts/{id}response, with no XDR dependency needed, is a smart way to deliver real functionality now rather than waiting on the SDK-dependency blocker — and correctly scoped to detection only. Explicitly choosing not to build a placeholder trustline-setup transaction (which would sign meaningless data in a real wallet popup) rather than faking a "working" flow is the right call — a fabricated signing prompt would be actively misleading in a real testnet walkthrough, not a shortcut. - The fee estimate replacing a hardcoded value with real
getFeeStats()data, presented as a p99 "up to" ceiling with a clear "not a YieldLadder fee" disclaimer, and an honestunavailablestate on failure rather than a fabricated number, is exactly the right instinct for financial UI — never show a number you can't back. - Correctly catching that the issue's description of
EarlyExitModal's button as unhandled was stale (already fixed in #142, merged before this branch existed) and adjusting scope accordingly instead of redoing already-done work, shows real attention to the current state of the codebase rather than blindly following the issue text. - The "still honestly blocked" section is transparent about exactly what remains fake (deposit/early-exit XDR construction, trustline
changeTrusttransaction) and precisely why (same pre-existing pnpm-workspace/lockfile constraint already documented elsewhere in the codebase) — this isn't scope creep avoidance, it's the same real constraint every PR in this app-layer track has hit. - Test coverage is strong: adapter error taxonomy including disconnect-ordering, provider resolution, trustline present/absent/wrong-issuer/unconfigured-issuer, fee estimate's fallback and honest-unavailable paths, and
requestWalletSignature's full call chain including network-mismatch short-circuit before ever attempting to sign.
CI is green across Soroban contracts, Next.js dashboard, and TypeScript SDK. The manual testnet walkthrough wasn't done, but that's consistent with real deposit submission still being blocked on the documented SDK-dependency gap — this PR doesn't newly expose any live fund-moving path, so that's a reasonable thing to defer rather than a red flag. Vercel's FAILURE is the usual unauthorized deployment integration link, unrelated to the code.
Approving — solid onboarding UX work with honest boundaries around what's real versus still blocked.
Summary
Implements wallet onboarding, real signing, and the surrounding trust/custody UX (issue #143).
FreighterAdapter(app/src/lib/wallet/freighterAdapter.ts): a realWalletAdapterimplementingsignTransactionagainstwindow.freighter— previously nothing in the app ever called it (WalletButtononly invoked Freighter's connect-time methods).signTransaction's shape is deliberately identical to the SDK'sSignerinterface (sdks/typescript/src/types.ts), so an adapter instance can be passed straight through assignerthe moment the app depends on the SDK directly. Error classification mirrors the SDK'sTransactionPipeline.classifySignerErrorexactly (same regexes, same disconnect-before-reject ordering).adapters.ts: explicit per-provider adapter resolution — never "whichever extension loaded first" (the multi-wallet-extension edge case from the issue). LOBSTR/xBull throw a clear, typedWalletUnavailableError;WalletButtonnow renders them as visibly disabled with a "Coming soon" badge and an explanatory note, replacing the silent stub that only ever failed on click.FreighterAdapter.checkNetwork()(a new optionalWalletAdaptermethod) is checked right after connecting and again right before signing, surfacing a wrong-network wallet as a typedWalletNetworkMismatchErrorinstead of an opaque submission failure.requestWalletSignature(paymentFlow.ts) is real now: given an XDR, it resolves the connected wallet's adapter, checks the network, and signs — reachable and tested through the exact call siterunPaymentSubmissionuses.services/horizon.ts) is fully real and dependency-free — Horizon's/accounts/{id}returns trustlines as plain JSON, no XDR encoding needed. The deposit flow gets a new step that checks for a missing USDC trustline before the confirm step and explains what's needed, rather than letting the deposit fail opaquely at simulation.lib/feeEstimate.ts) replaces the hardcoded'0.00001 XLM'withgetFeeStats()— real, current network-wide fee data that needs no transaction built to query. Presented as an "up to" (p99) ceiling with a one-line "this is a network fee, not a YieldLadder fee" note, and an honest "unavailable" state (never a fabricated number) on failure.EarlyExitModal, grounded in the README's actual architecture ("the Strategist can propose pool allocations but can never withdraw your funds... every withdrawal requires your own wallet signature") rather than generic marketing language.What's still honestly blocked (called out, not silently skipped)
Two pieces remain blocked on the exact same pre-existing constraint already documented in this codebase (see
placeholderSubmissionHashindeposit/page.tsxand the identical note inhooks/usePosition.ts): the app doesn't yet depend on@yieldladder/sdk/@stellar/stellar-sdk, and adding it needs apnpm installto regenerateapp/pnpm-lock.yamlcorrectly, which this environment can't safely run.requestWalletSignatureis real and fully tested, but neither page yet has a real XDR to pass it, so today's actual signing behavior is unchanged (still the placeholder-hash pipeline downstream). The moment a caller has a real XDR, this exercises the complete real signing path instead of a no-op.changeTrusttransaction itself — also needs XDR construction. The deposit flow's new trustline step does the detection for real and gives the user a clear, actionable path (add it via their wallet's own UI, then recheck), rather than presenting a placeholder-XDR signing prompt that would sign meaningless data in a real wallet popup — that would be actively misleading in a real testnet walkthrough, not a genuine demonstration of trustline setup.One correction to the issue's description:
EarlyExitModal's "Exit Early" button was already wired to the real submission pipeline as of #142 (merged before this branch was created) — the issue's description of it as unhandled predates that merge. This PR adds the explainer copy there and leaves the already-working handler alone.Test plan
freighterAdapter.test.ts— connect/isConnected/signTransaction happy paths and error mapping,classifyFreighterError's full taxonomy including the disconnect-over-reject ordering.adapters.test.ts— provider resolution, LOBSTR/xBull's typed rejection.horizon.test.ts— trustline present/absent/wrong-issuer, unconfigured-issuer short-circuit, account-not-found and network-failure error mapping.feeEstimate.test.ts— p99-derived ceiling display, fallback to classicinclusionFee, honestunavailablestate on a missing/malformed response or RPC failure.paymentFlow.test.ts— extendedrequestWalletSignaturecoverage: no-op without an XDR (unchanged existing behavior),WalletUnavailableErrorwith no session, real signing call with the right args, network-mismatch short-circuit before ever callingsignTransaction, and a realWalletRejectedErrorpropagating through.Closes #143