Skip to content

fix: handle wallet rejection and unsubscribe in signAndSendTx#515

Open
jim-counter wants to merge 4 commits intomainfrom
fix/sign-and-send-tx-rejection
Open

fix: handle wallet rejection and unsubscribe in signAndSendTx#515
jim-counter wants to merge 4 commits intomainfrom
fix/sign-and-send-tx-rejection

Conversation

@jim-counter
Copy link
Member

@jim-counter jim-counter commented Feb 13, 2026

Summary

Fix signAndSendTx to properly handle wallet signing rejection and clean up subscriptions.

Problem

When using signAndSendTx with browser wallet extensions (SubWallet, Talisman, Polkadot.js), if the user rejects the signing request, the returned promise hangs forever. This is because:

  1. tx.signAndSend() returns a Promise<() => void> that rejects when the wallet denies signing, but this rejection was never caught
  2. The status callback only fires after the transaction is submitted — it never fires if signing is denied
  3. The subscription was never unsubscribed, leaking the websocket listener

Fix

  • Catch wallet rejection: Attach .then(fn, safeReject) to the outer promise so wallet denials propagate to the caller
  • Settled guard: Prevent redundant processing when the status callback fires multiple times (isInBlock then isFinalized)
  • Race-safe unsubscribe: Handle both timing paths — if the callback settles before .then() assigns unsub, unsubscribe immediately when the outer promise resolves; if .then() fires first, store unsub for the settlement handler
  • Synchronous throw: Wrap signAndSend in try/catch for wallet extensions that throw synchronously

Tests

Added 6 unit tests covering:

  • Wallet rejection rejects the returned promise
  • Slow path: outer resolves before callback — unsub called on settlement
  • Fast path: callback settles before outer resolves — unsub called when outer resolves
  • Synchronous throw from signAndSend rejects the promise
  • Settled guard: isFinalized after isInBlock is a safe no-op
  • Transaction failure (isDropped) rejects and unsubs

Note

detectTxSuccess has a pre-existing bug where return true inside forEach returns from the callback, not the function — so success is always false. This is unrelated to this PR and does not affect transaction outcomes (the tx still succeeds/fails correctly).

Test plan

  • npx jest __test__/signAndSendTx.test.ts — all 6 tests pass
  • npx lerna run build — all 17 packages build cleanly
  • Manual test: Consensus→EVM transfer, reject in SubWallet — spinner stops, error shown
  • Manual test: Consensus→EVM transfer, reject in Talisman — spinner stops, error shown

When using signAndSendTx with browser wallet extensions (SubWallet,
Talisman, Polkadot.js), rejecting the signing request caused the
returned promise to hang forever. This happened because:

1. tx.signAndSend() returns a Promise that rejects on wallet denial,
   but this rejection was never caught
2. The status callback only fires after submission — never on signing denial
3. No unsubscribe cleanup happened when the promise settled

Fix:
- Catch the outer promise rejection via .then(onFulfilled, onRejected)
- Wrap signAndSend in try/catch for synchronous throws from some extensions
- Store and call the unsubscribe function on settlement to clean up
  websocket listeners
- Use settled guard to prevent double-resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jim-counter and others added 3 commits February 13, 2026 18:17
Remove duck-type check and double cast on the outerPromise. The return
type of signAndSend with a callback is well-defined as Promise<() => void>
in @polkadot/api@^15, so the defensive type gymnastics are unnecessary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bugbot correctly identified that if the status callback settles the
promise before outerPromise.then() assigns unsub, cleanup never runs.

Fix: .then() now checks if already settled and unsubs immediately (fast
path). If not yet settled, it stores unsub for safeResolve/safeReject
to call later (slow path). Both paths are now covered.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests cover:
- Wallet rejection propagates to caller
- Slow path: unsub assigned before settlement, cleaned up by tryUnsub
- Fast path: callback settles first, unsub called when outer resolves
- Synchronous throw from signAndSend
- Settled guard prevents issues on isFinalized after isInBlock
- Transaction failure (isDropped) rejects and unsubs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jim-counter
Copy link
Member Author

bugbot run

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@jim-counter
Copy link
Member Author

bugbot run

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

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.

1 participant