feat(stellar): validated network profile, pre-sign review, and idempotent submission - #1003
Merged
llinsss merged 3 commits intoAug 28, 2026
Merged
Conversation
…tent submission Closes DogStark#943 Closes DogStark#945 Closes DogStark#946 Closes DogStark#947 DogStark#943 — Validate Stellar network and asset configuration as one unit - Adds src/config/stellarNetwork.ts: one immutable, validated profile pairing network, Horizon URL and passphrase, resolved together and frozen. - Fixes a real hazard: blockchainService hard-coded STELLAR_NETWORK='TESTNET' while stellarPathPaymentService derived its signing passphrase from config.env. With APP_ENV=production those disagree, so transactions were signed for PUBLIC and submitted to testnet Horizon. - Rejects public-outside-production, testnet-in-production, cross-network Horizon hosts, non-https Horizon and mismatched passphrases, reporting all problems at once. Resolution throws rather than defaulting. DogStark#945 — Require transaction simulation and review before signing - Adds src/services/transactionSimulation.ts, which decodes the envelope that will actually be signed rather than trusting the server-supplied quote, and reports destination, asset, amount, fee, memo, network and per-operation effects. - compareQuoteToSimulation flags any field where the displayed quote and the real envelope disagree; a blocking discrepancy prevents signing. - Implements stale-quote expiry from PathPaymentQuote.expiresAt, which already existed and was never read. An absent or unparseable expiry counts as expired. - PaymentScreen re-evaluates the gate at the moment of signing, adds a live countdown, a refresh action, and accessibility roles/labels/state including a single spoken review summary. DogStark#946 — Prevent duplicate Stellar submissions across taps and retries - Adds src/services/stellarTransactionRegistry.ts keyed by the envelope's deterministic hash: concurrent taps share one request, a confirmed transaction is never re-sent, and an ambiguous timeout is reconciled against Horizon instead of rebuilt with a fresh sequence number. - A test-first check caught a real bug in the first implementation: the in-flight guard was registered after an await, so every tap in the same tick slipped past it. Registration now happens before the first await. - An unreachable Horizon leaves a record pending, never failed — an outage is not evidence a payment failed. - sendPayment and storeDataOnStellar now route through submitStellarTransactionOnce. PaymentScreen adds a synchronous ref lock, since `submitting` state can lose a same-frame double tap. DogStark#947 — Persist and reconcile pending Stellar transactions - Records are persisted before the network call and reconciled on launch via App.tsx, so a termination mid-flight is recoverable. - Only non-secret data is stored: a signed envelope holds the source public key and a signature, the same bytes the network sees. Secrets stay in secure storage. Storage is capped, unresolved records first. Also fixes two pre-existing blockers, without which none of the above could be verified: - jest.config.js: MSW and its ESM dependency graph were never transformed, so jest.setup.js threw and every suite in the repo failed before running a single test. On main, `npx jest` runs 0 tests across 181 failing suites. - src/utils/sanitize.ts: a literal */ inside a JSDoc code span closed the comment early, breaking `tsc` project-wide. Verification: 71 new tests across 4 suites, all passing. Against main with only the jest fix applied, the failure set is byte-identical (88 suites / 406 tests, all pre-existing); passing tests go 2067 -> 2138. Typecheck errors unchanged at 17, none in files this PR adds or rewrites. eslint clean on all changed files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@JoyAdah 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! 🚀 |
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.
Description
Four assigned Stellar payment issues. They share one spine — the network profile from #943 is what #945 checks against and what #946/#947 submit to — so they are proposed together.
Linked Issue
Closes #943
Closes #945
Closes #946
Closes #947
Neither is cosmetic — without them nothing in this repo can be verified at all.
1. No test in the repo currently runs.
jest.setup.jsloadssrc/__mocks__/server.ts→msw/node, whose dependency graph ships ESM.transformIgnorePatternsallowlisted onlyotplib/expo-*, and the transform pattern^.+\.[jt]sx?$doesn't match.mjs— so Babel never touched it and every suite threwCannot use import statement outside a modulebefore executing a single test.Fixed by widening
transformIgnorePatternsand extending the transform to.mjs/.cjs.2.
tscwas broken project-wide.src/utils/sanitize.tshad a literal*/inside a JSDoc code span listing SQL metacharacters, which closed the block comment early — everything after it parsed as code. Fixed by rewording the doc line; no logic touched.#943 — Validate network and asset configuration as one unit
The bug
Network selection was made independently in two places:
services/blockchainService.tsconst STELLAR_NETWORK: string = 'TESTNET'— a hard-coded literalservices/stellarPathPaymentService.tsconfig.env === 'production' ? Networks.PUBLIC : Networks.TESTNETWith
APP_ENV=productionthose disagree:signTransactionXdrsigned with the public passphrase while every Horizon call went to testnet. A passphrase is part of what a Stellar signature commits to, so this is the exact "mixing testnet/mainnet Horizon and passphrase" failure the issue describes.blockchainServicealso computed a passphrase intoconst _networkPassphraseand threw it away.The fix
src/config/stellarNetwork.tsresolves network, Horizon URL and passphrase together, validates them as a unit, and freezes the result. Both services now read it; there are no remaining ad-hoc derivations.Rejected combinations: public network outside production (without explicit opt-in), testnet in production, a Horizon host belonging to the other network, non-https Horizon, malformed URL, and a passphrase that doesn't match. Every problem is reported at once rather than one per run, and resolution throws rather than defaulting — a wrong-network default is the irreversible mistake here.
#945 — Simulation and review before signing
The bug
The confirm screen rendered fields from the server-supplied
PathPaymentQuote, then signedpreparedPayment.transactionXdr. Two different objects, never cross-checked — a wrong or tampered XDR would be approved against a reassuring summary. Andquote.expiresAtalready existed but was never read, so a user could leave the screen open and sign at a long-stale rate.The fix
src/services/transactionSimulation.tsdecodes the envelope that will actually be signed and reports destination, asset, amount, fee, memo, network and per-operation effects.compareQuoteToSimulationflags disagreement between the quote and the envelope; ablockingdiscrepancy prevents signing.evaluateQuoteFreshnessimplements stale-quote expiry — an absent or unparseable expiry counts as expired, because refusing to sign is the safe direction.PaymentScreengains a live countdown, a "Refresh quote" action once lapsed, and re-evaluates the gate at the moment of signing rather than trusting the last render, since the ticker may not have fired since the quote lapsed.Accessibility: the confirm button carries
accessibilityRole,accessibilityLabel,accessibilityState(disabled/busy) and anaccessibilityHintthat explains why it's disabled. Discrepancies render asaccessibilityRole="alert". The expiry countdown usesaccessibilityLiveRegion="polite"only when actually expiring, to avoid a once-a-second announcement. The review block is a singleaccessiblenode with one composed sentence, so a screen-reader user gets the whole review rather than a stream of fragments.#946 — Prevent duplicate submissions
Two distinct hazards, handled differently:
A bug the test-first work caught
The acceptance criteria ask for behaviour to be characterised by test before implementation. That's what found this: my first implementation did
An async function runs synchronously only up to its first
await, so three taps queued in the same tick all passed theinFlightcheck before any of them registered — the guard did nothing for exactly the case it existed for.collapses concurrent submissions of the same envelope into one network callfailed, and registration now happens before the first await.An unreachable Horizon leaves a record
pending, neverfailed— an outage is not evidence a payment failed, and reporting failure would invite a duplicate rebuild.sendPaymentandstoreDataOnStellarnow route throughsubmitStellarTransactionOnce.PaymentScreenalso holds a synchronoususeReflock, becausesubmittingis React state and a second tap in the same frame can beat the re-render.#947 — Persist and reconcile
Records are persisted before the network call, so a termination mid-flight leaves a durable trace;
App.tsxreconciles on launch. Reconciliation never throws — a failure must not block startup.Only non-secret data is stored. A signed envelope contains the source public key and a signature: the same bytes the network sees. Secret keys stay in secure storage and never reach the registry. A test asserts no strkey-shaped secret ever appears in the persisted blob. Storage is capped at 50 records, unresolved first, so trimming can't discard live work.
Test Plan
71 new tests across 4 suites, all passing.
Because the Jest fix changes what runs at all, the honest comparison is against
mainwith only that fix applied, so the source changes are isolated:mainmain+ Jest fix onlyThe failing set is identical — all 406 are pre-existing and unrelated, merely revealed by the setup fix. This branch adds 4 suites and 71 tests and introduces no new failure.
eslintclean on all changed files — 0 errors.transactionSimulation.test.tscallsjest.unmock('@stellar/stellar-sdk')and builds real XDR envelopes; a mocked decoder would prove nothing about the review path.Checklist
docs/STELLAR_PAYMENTS.md; thesendPayment/storeDataOnStellarreturn-type change is documented there.Not verified — needs a reviewer with devices
PaymentScreenchanges are visible, so this needs a pass on both platforms before merge.API change
sendPaymentandstoreDataOnStellarnow resolve toSubmitResult({ hash, status, deduplicated, ledger?, resultCode? }) rather than a raw HorizonSubmitTransactionResponse. Neither had callers outsideblockchainService.ts. The rawsubmitStellarTransactionprimitive is unchanged, and its existing circuit-breaker tests still pass untouched.