Skip to content

feat: harden Stellar payment, trustline, and path-payment safety (#948 #949 #950 #951) - #1019

Merged
llinsss merged 1 commit into
DogStark:mainfrom
maztah1:feat/stellar-payment-safety-948-951
Aug 28, 2026
Merged

llinsss merged 1 commit into
DogStark:mainfrom
maztah1:feat/stellar-payment-safety-948-951

Conversation

@maztah1

@maztah1 maztah1 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #948
Closes #949
Closes #950
Closes #951

Hardens the Stellar money-movement paths against four classes of unsafe state that can lose or strand funds. Each fix is a small, isolated guard with focused unit tests; no behavioural change on the happy path.

Hardens the Stellar money-movement paths against four classes of unsafe state that can lose or strand funds. Each fix is a small, isolated guard with focused unit tests; no behavioural change on the happy path.

Issue Area Guard added
#948 blockchainService.sendPayment Reject malformed destinations and enforce SEP-0029 memo-required accounts before building a payment
#949 blockchainService signing Refresh account sequence immediately before signing; rebuild once on tx_bad_seq
#950 trustlineService / TrustlineScreen Explain reserve impact; block trustline removal with a balance or open-offer liabilities
#951 stellarPathPaymentService Bind a reviewed quote to its min destination amount, deadline, path, and assets; reject drift/expiry at submit

#948 — Validate Stellar destination and memo requirements

src/services/blockchainService.ts

  • validateStellarDestination(address) — uses the SDK's StrKey checks to accept classic G... (ed25519) and multiplexed M... addresses, trims input, and throws BlockchainServiceError('INVALID_DESTINATION') for anything else so a mistyped address can never reach transaction building.
  • destinationRequiresMemo(pk, loadAccount?) — implements SEP-0029: returns true when the destination account publishes a config.memo_required data entry. Muxed addresses are exempt (they carry their own routing id). Unfunded accounts return false; the account loader is injectable for tests.
  • assertDestinationAndMemo(pk, memo?) — combined guard now called at the top of sendPayment; throws MEMO_REQUIRED when a memo is missing/blank for an opted-in account.

#949 — Protect signing against stale account sequence numbers

src/services/blockchainService.ts

  • isBadSequenceError(error) — recognises Horizon's tx_bad_seq / txBadSeq rejection across raw Horizon error shapes, error.code, and the wrapped BlockchainServiceError message.
  • signAndSubmitWithFreshSequence(secret, buildOps, opts?) — loads the source account (and therefore its sequence) immediately before signing, and rebuilds once with a fresh sequence if Horizon still returns tx_bad_seq. This is idempotent: a tx_bad_seq transaction is provably not in the ledger, so re-applying the same operation cannot double-spend. sendPayment now routes through this helper.

#950 — Trustline reserve and liability checks

src/services/trustlineService.ts, src/screens/TrustlineScreen.tsx, src/models/Trustline.ts

  • describeReserveImpact(state, 'add' | 'remove') — returns the exact ±0.5 XLM reserve delta, the projected locked/available balance, a sufficient flag, and a human-readable summary for the confirmation dialog.
  • canAffordNewTrustline(state) — boundary-correct check that the account can cover the additional reserve.
  • assertTrustlineRemovable(line) — blocks removal with NON_ZERO_BALANCE when a balance remains, and with HAS_LIABILITIES when selling_liabilities / buying_liabilities (open offers) would be stranded. Wired into removeTrustline and the screen's remove flow.
  • parseBalance now surfaces sellingLiabilities / buyingLiabilities on TrustlineAsset so the UI can guard before hitting the network.
  • TrustlineScreen now shows the reserve-impact summary in both the add and remove dialogs and blocks the add action when the account is short.

#951 — Path-payment slippage and expiry

src/services/stellarPathPaymentService.ts

  • computeMinDestinationAmount(amount, slippageBps = 50) — 7-dp floor on delivered amount; validates amount and a 0–10000 bps integer tolerance.
  • assertQuoteNotExpired(quote, now?) — rejects missing/malformed expiry (QUOTE_NO_EXPIRY) and past deadlines (QUOTE_EXPIRED).
  • bindReviewedQuote(quote, slippageBps?) — freezes the reviewed quote into an immutable QuoteBinding (min amount, deadline, path, path count, both assets).
  • assertQuoteMatchesBinding(binding, freshQuote, now?) — rejects ASSET_DRIFT, PATH_DRIFT, SLIPPAGE_EXCEEDED, and expiry when a re-quote no longer matches what the user approved.
  • submitPathPayment accepts an optional reviewedQuote (+ freshQuote, slippageBps); when supplied it validates before submitting and forwards minDestinationAmount + deadline to the backend. The wire payload is unchanged when these are omitted.

Note: issue #951 references blockchainIntegration.ts, but the path-payment quote type and submission live in stellarPathPaymentService.ts; the guard was added there.

Tests

  • src/services/__tests__/blockchainService.validation.test.ts — destination validation (valid/empty/malformed), SEP-0029 memo detection (opted-in, not, unfunded, offline/timeout propagation), assertDestinationAndMemo success/blank-memo/no-memo-required/invalid-before-lookup, isBadSequenceError across all shapes.
  • src/services/__tests__/stellarPathPaymentService.slippage.test.ts — min-amount math and validation, expiry, quote binding, and drift detection (slippage, asset, path, hop count, expiry).
  • src/services/__tests__/trustlineService.reserve.test.ts — reserve delta and boundary (exactly-enough) for add/remove, non-negative projected reserve, removal blocked on balance and on buy/sell liabilities (snake_case and camelCase).

Acceptance criteria notes

  • Characterisation-first: the new guards were driven by the failing/edge tests above.
  • iOS/Android: logic-only changes in shared TS services plus dialog copy in TrustlineScreen; no native or platform-specific code.
  • Paths covered: malformed input, offline/timeout (loader rejection propagates), retry (tx_bad_seq rebuild), and cancellation (guards run before submission) are exercised.
  • No sensitive data in logs: guards throw typed errors with asset codes / amounts only — no secret keys, tokens, or PII added to logs or fixtures. Test fixtures use randomly generated keypairs and synthetic amounts.
  • Idempotency: the sequence-number rebuild re-applies the same operation only after a provably-unincluded tx_bad_seq.

Closes DogStark#948: validate Stellar destination addresses (StrKey ed25519/muxed)
and enforce SEP-0029 memo-required accounts before building a payment.

Closes DogStark#949: refresh the source account sequence number immediately before
signing and rebuild once on tx_bad_seq so concurrent devices or queued
payments cannot submit stale transactions.

Closes DogStark#950: explain trustline reserve impact and block trustline removal
when a balance or buying/selling liabilities would be stranded.

Closes DogStark#951: bind a reviewed path-payment quote to its min destination
amount, deadline, routing path, and assets, and reject any drifted or
expired quote at submission time.

Adds focused unit tests for every new guard.
@drips-wave

drips-wave Bot commented Aug 27, 2026

Copy link
Copy Markdown

@maztah1 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! 🚀

Learn more about application limits

@llinsss
llinsss merged commit ddbe9d1 into DogStark:main Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants