Skip to content

fix(subscriptions): do not silently swallow cancel transaction build or signing errors (#16) - #98

Open
ghzhost wants to merge 1 commit into
StellarSend:mainfrom
ghzhost:fix/cancel-subscription-error-handling-16
Open

fix(subscriptions): do not silently swallow cancel transaction build or signing errors (#16)#98
ghzhost wants to merge 1 commit into
StellarSend:mainfrom
ghzhost:fix/cancel-subscription-error-handling-16

Conversation

@ghzhost

@ghzhost ghzhost commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Fixes #16 by ensuring useCancelSubscription does not silently swallow errors during cancellation transaction building or user signing.

Context & Root Cause

Previously, useCancelSubscription wrapped subscriptionApi.buildCancelTransaction(subscriptionId) and signTransaction(built.xdr) in a broad try/catch block:

try {
  const built = await subscriptionApi.buildCancelTransaction(subscriptionId)
  if (built?.xdr) {
    signedXdr = await signTransaction(built.xdr)
  }
} catch {
  signedXdr = undefined
}
return subscriptionApi.cancel(subscriptionId, signedXdr)

This conflated the legitimate case where no signature is required (backend returns { xdr: undefined }) with actual failure modes:

  1. When buildCancelTransaction threw an error (backend 500, network error, invalid subscription state), the error was caught and subscriptionApi.cancel(subscriptionId, undefined) was executed anyway, potentially marking the subscription cancelled in the database while leaving the on-chain standing order active.
  2. When the user rejected the Freighter wallet signature prompt (signTransaction rejecting), the error was caught and cancellation proceeded without the required authorization against the user's explicit decision.

Fix

  • Removed the broad try/catch swallowing around buildCancelTransaction and signTransaction.
  • Maintained the legitimate no-signature path: if built?.xdr is falsy/undefined, signedXdr remains undefined and subscriptionApi.cancel(subscriptionId, undefined) is called.
  • If built?.xdr exists, signTransaction is invoked and its signed XDR is passed to subscriptionApi.cancel.
  • If either buildCancelTransaction or signTransaction rejects/throws, the mutation properly throws and aborts without calling cancel.

Test Plan

  • Added comprehensive unit tests in src/hooks/useSubscriptions.test.tsx:
    • Verified happy path without signature requirement (built.xdr === undefined).
    • Verified happy path with signature requirement (built.xdr === '...'), ensuring signedXdr is passed to subscriptionApi.cancel.
    • Verified that build endpoint errors (network/500) reject the mutation and prevent cancel from being called.
    • Verified that user signing rejections (Freighter decline) reject the mutation and prevent cancel from being called.
  • Ran full test suite (NODE_ENV=development npx vitest run) -> 27 test files passed, 117 tests passed.
  • Built production bundle (npm run build) -> TypeScript check and Vite build passed cleanly.

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.

useCancelSubscription treats every buildCancelTransaction failure as "no signature required" and cancels anyway

1 participant