mobile: add SEP-24 buy (on-ramp) screen - #522
Conversation
Adds a mobile buy screen that lets users bring fiat into the wallet through a SEP-24 anchor. lib/sep24.ts ports the deposit-side protocol logic from frontend/wallet/lib/sep24.ts (anchor TOML discovery, SEP-10 challenge validation/signing, interactive deposit request, status polling) as pure, injectable-signer functions with no browser-only APIs. app/buy.tsx drives the flow: enter anchor domain/amount/asset, kick off the interactive deposit, launch the returned URL with expo-web-browser's openAuthSessionAsync, and poll for the resulting transaction status once the user returns to the app. Adds expo-web-browser as a dependency (installed via `expo install` for SDK-compatible versioning).
|
@BigManly4 is attempting to deploy a commit to the miracle656's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@BigManly4 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Miracle656
left a comment
There was a problem hiding this comment.
Good structure here. Making the SEP-10 signer injectable rather than reaching for browser storage — the same pattern as executeBulkPayout(rows, submitBatch) — is the right call for a port, and the module doc is explicit that no browser-only APIs are used. discoverAnchorInfo reading stellar.toml and the status polling loop both look right.
I want to focus on the SEP-10 challenge validation, because you've built most of it and it's worth finishing properly.
What you got right
signSep10Challenge already rejects:
- unparseable XDR →
MALFORMED - no
manage_dataoperation →MISSING_MANAGE_DATA - a
manage_datakey not ending in" auth"→INVALID_HOME_DOMAIN - missing
timeBounds→MALFORMED maxTimein the past →EXPIRED
That's more than most implementations bother with, and the typed Sep10ErrorCode union makes the failures actionable.
🚨 The missing check: sequence number must be 0
SEP-0010 requires the challenge transaction to have sequence number 0. That is the control that makes a challenge impossible to submit to the network — a transaction with sequence 0 can never be valid on-chain.
Without it, a malicious or compromised anchor can hand you a genuine transaction with a real sequence number — say a payment operation draining the account — and as long as it also carries a manage_data op ending in " auth" and valid timeBounds, signSep10Challenge will happily sign it and hand back a broadcastable envelope.
if (tx.sequence !== '0') {
throw new Sep10ChallengeError(
`SEP-10 challenge must have sequence number 0, got ${tx.sequence}`,
'MALFORMED',
);
}Please add that. It's two lines and it's the difference between a validated challenge and a blind signing oracle.
Also worth adding: verify the source account
SEP-0010 also says the challenge's source account must be the anchor's SIGNING_KEY, as published in its stellar.toml. You already fetch the TOML in discoverAnchorInfo, so the value is in hand:
if (tx.source !== anchorInfo.signingKey) {
throw new Sep10ChallengeError(
'Challenge source account does not match the anchor SIGNING_KEY',
'MALFORMED',
);
}This stops a network-level attacker who can intercept the WEB_AUTH_ENDPOINT response from substituting their own challenge.
Duplicate module with #523
Your #523 creates frontend/mobile/lib/sep24.ts too — a different 230-line version. They can't both land.
The important part: #523's copy has no challenge validation at all. Its getSep10Jwt fetches the challenge and passes it straight to the signer, and it takes the network passphrase from the anchor's own response (network_passphrase ?? networkPassphrase), letting the anchor choose which network you sign for.
So please consolidate on this PR's version, and move #523's unique piece — initiateWithdraw — into it. Concretely:
- Keep this
lib/sep24.tsas the single module. - Add
initiateWithdrawhere (it's a near-mirror ofinitiateDeposit). - Strip
lib/sep24.tsout of #523 entirely so it only containsapp/withdraw.tsxandlib/sweepContractBalance.ts.
I've left the same note on #523.
Smaller things
app.json — #508 removes it in favour of app.config.ts. Your 2+/1- change will need to move there. Don't act on this yet; I'm also settling the bundle identifier across #508/#512/#514 and will confirm the canonical values first.
app/buy.tsx collides with #517, which adds a 34-line placeholder buy.tsx. Yours is the real implementation — I've asked #517 to drop its stubs.
The stub signer. FEE_PAYER_ADDRESS from env with no real signing is fine as a staged port, and you documented it. Worth a TODO referencing the issue that will wire up real signing, so it can't quietly ship in that state — a buy flow that authenticates with a stub is a thing someone will eventually assume works.
No tests. signSep10Challenge is a pure function over an XDR string — it's the single most test-worthy thing in this PR, and each rejection path is a two-line test. Once a runner lands (#524/#525 add jest.config.js), please cover at minimum: sequence ≠ 0, expired, missing manage_data, wrong source. Those tests are what stop the validation quietly regressing later.
Reconcile with main and wire the SEP-10 path the library already implements: - Drop app.json; main uses app.config.ts, which already registers the veil scheme this flow returns through. - buy.tsx never called getSep10Jwt, so deposits went out unauthenticated and any anchor requiring SEP-10 would reject them. It now authenticates whenever the anchor advertises WEB_AUTH_ENDPOINT, signing the challenge with the device signer via signSep10Challenge, and passes the JWT to initiateDeposit. - The account came from EXPO_PUBLIC_FEE_PAYER_ADDRESS, defaulting to '', so out of the box the screen posted an empty account. It now resolves through walletStore.getWalletAddress() with the env value as a fallback. tsc clean; jest 10 suites / 174 tests; expo lint clean.
|
Merging. Two things I fixed before merging: SEP-10 was implemented but never called. The account was empty by default. Also dropped the On the acceptance criterion — buy flow launches the anchor interactive URL and returns to app — Verified: Worth a follow-up: the SEP-10 signer uses the G-account secret from Note I've closed your #521 (vault) separately — that one reported |
lib/sep24.ts landed with Miracle656#522, so take main's version (which has the SEP-10 challenge validator this branch lacks) and add initiateWithdraw plus the withdraw-only fields on Sep24TransactionStatus on top. Replace the two stubs in withdraw.tsx: - The SEP-10 signer returned the challenge XDR unsigned, so every anchor would reject the token exchange. It now signs with the device key through signSep10Challenge, matching buy.tsx. - The sweep signer returned `pending-sweep-<amount>-<ts>` — a fabricated hash for a transfer that never happened, which the screen then displayed as "sweep tx: …". It now throws with an explanation. The existing catch already treats a failed sweep as non-fatal and keeps polling the withdrawal, so the honest failure degrades exactly where the fabricated success used to lie. - Account resolves through walletStore.getWalletAddress() with the env value as a fallback, rather than EXPO_PUBLIC_FEE_PAYER_ADDRESS defaulting to ''. Also drop app.json (main uses app.config.ts) and an unused Alert import. tsc clean; jest 10 suites / 174 tests; expo lint clean.
Summary
Adds the mobile on-ramp (buy) flow described in the issue: a screen that
initiates a SEP-24 interactive deposit with an anchor, launches the
anchor's hosted flow, and returns cleanly to the app.
frontend/mobile/lib/sep24.ts— ports the deposit-side protocol logicfrom
frontend/wallet/lib/sep24.ts: anchorstellar.tomldiscovery(
discoverAnchorInfo), SEP-10 challenge validation/signing(
signSep10Challenge,Sep10ChallengeError), the SEP-10 web-authexchange (
getSep10Jwt), the/transactions/deposit/interactiverequest (
initiateDeposit), and transaction status polling(
getTransactionStatus,isSep24Complete). It is pure/testable and hasno browser-only APIs (no
window,document,localStorage,sessionStorage, WebAuthn) — the web version signs the SEP-10 challengewith a browser passkey stored in
localStorage; this port instead takesan injectable
Sep10ChallengeSignerasync function parameter, the samepattern as
executeBulkPayout(rows, submitBatch)infrontend/mobile/lib/bulkPayout.ts. Mobile has no wallet-signing infraported yet, so this is left as an injectable seam rather than invented;
the buy screen itself only needs the unauthenticated deposit path
(no JWT), matching how
frontend/wallet/app/buy/page.tsxcallsinitiateDepositwithout a JWT for its generic SEP-24 flow.frontend/mobile/app/buy.tsx— a self-contained React Native screen(plain
react-nativeprimitives +StyleSheet.create, dark themematching the existing
swap.tsx/bulk-payout.tsxscreens: background#0B0B0F, card#1e293b, accent#6366f1, muted text#9BA1A6/#94a3b8,error
#f87171). Flow:initiateDepositto get the interactive URL + transaction id.expo-web-browser'sWebBrowser.openAuthSessionAsync, using anexpo-linkingredirect URLback into the app; when the browser session closes (completed or
dismissed) control returns to the screen.
getTransactionStatusevery 5s,stopping once the anchor reports the transaction as complete/error/
refunded/expired (
isSep24Complete); also exposes a manual"Check status" button for an immediate refresh.
with a retry affordance.
Adds
expo-web-browseras a dependency, installed vianpx expo install expo-web-browserso the version (~57.0.2) and theapp.jsonconfig-plugin entry are correct for this project's Expo SDK57 toolchain.
What was verified
cd frontend/mobile && npm install && npm run typecheckpasses cleanlywith no errors.
frontend/wallet/app/buy/page.tsxandfrontend/wallet/lib/sep24.tsin full and cross-checked the portedlogic against them (TOML regex parsing, SEP-10 challenge validation
rules, deposit request/response shape, status polling cadence and
terminal states).
frontend/mobile/app/swap.tsx,frontend/mobile/app/bulk-payout.tsx,and
frontend/mobile/lib/bulkPayout.tsfor styling, stubbed/injectablesigning, and state-machine shape.
environment, so the screen was not visually rendered — typecheck and
manual code review are the extent of verification here.
What's stubbed
getSep10Jwt,signSep10Challenge) areported and exported but not wired into the buy screen, since mobile has
no wallet signing infra yet; the screen uses the unauthenticated deposit
path, same as the web app's generic SEP-24 flow.
EXPO_PUBLIC_FEE_PAYER_ADDRESS, matching the existing stub used inswap.tsx, pending real wallet address wiring.closes #478