Skip to content

fix(wallet-toolbox): keep a grant for the session when its token cannot be funded - #1105

Draft
Calgooon wants to merge 1 commit into
bsv-blockchain:mainfrom
Calgooon:fix/keep-unfunded-permission-grants
Draft

Calgooon wants to merge 1 commit into
bsv-blockchain:mainfrom
Calgooon:fix/keep-unfunded-permission-grants

Conversation

@Calgooon

@Calgooon Calgooon commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Draft: opened for review of the approach. Local checks are below; hosted CI has not run on this head yet.

Program and scope

  • Tracker or issue: An empty wallet cannot keep a granted permission: the request hangs and the app times out; a first connect costs about 2,000 sats of permission tokens bsv-desktop#115 (an empty wallet cannot keep a granted permission)
  • Program gate(s) advanced: a first connect from a wallet with no spendable coins
  • Why this change is needed: a permission token costs a little to mint. When the wallet has no coins, the mint fails with WERR_INSUFFICIENT_FUNDS and the user's decision is dropped with it. In grantPersistentPermission (src/WalletPermissionsManager.ts 1103 to 1148 at 2.14.5, 2fd579cb), the throw from await mint (1140) skips cachePermission and markRecentGrant (1146 to 1147), so the next identical call prompts again. A grouped or counterparty grant goes further and rejects the call waiting on it (1314, 1463), because persistPermissionGrant (1369) throws. The error itself is raised promptly (storage/methods/generateChange.ts 199); the cost is the repeated prompt.
  • Explicitly out of scope: a durable or deferred token (minting later once funds arrive), any change to the funded path, the prompt UI, and spending authorizations.
  • Exact head SHA reviewed: 7968438

What was seen

BSV Desktop 3.1.1 (bundling wallet-toolbox 2.14.5) on a fresh wallet, connecting to a BRC-100 web app that signs in with BRC-103:

  • 0 sats: six grants and six timeouts. Each connect brought the grouped prompt and then a protocol prompt (auth message signature, this app and counterparty); each was granted, and on the next attempt the wallet asked for the same permissions again. The app never connected.
  • 5,000 sats: the same wallet connected after one grouped grant, one protocol grant and one retry. Activity showed two permission transactions totaling 1,996 sats, and a returning session connected with no prompt.

The change

  • On WERR_INSUFFICIENT_FUNDS only, the grant is kept in memory (sessionGrants) for the life of the manager, until its own expiry, and is checked alongside the existing grant caches in hasRecentOrPendingGrantCore.
  • Grouped and counterparty grants resume their waiting call in that case (settleFailedGrant).
  • The grant call still throws the funding error, because no token was written. Nothing is persisted, so the next session asks again, and a funded wallet mints the token as it does today.
  • Spending authorizations are never kept without a token, and every other mint failure still re-opens the prompt (the existing "a failed mint re-opens the window" test is unchanged and passes).

The principle behind it: a grant is the user's decision, and the token is the wallet's record of it. A wallet that cannot afford the record should still honor the decision for as long as it is running, rather than ask again.

Impact

  • No public package source or manifest changed
  • Public package source or manifest changed; affected packages are listed below
  • Infrastructure source, dependency, image, or deployment configuration changed
  • Public API, exports, types, runtime targets, or browser/mobile behavior changed
  • Security-sensitive boundary changed
  • Documentation or examples changed

No exports or types change. Behavior changes only after a mint fails for lack of funds: the waiting grouped or counterparty call resolves instead of rejecting, and the same permission is not asked for again in this session. The boundary is the permission check itself. A kept grant is the one the user approved, keyed exactly as the token would be and limited by its expiry.

Affected packages/services and intended patch versions: @bsv/wallet-toolbox, -client and -mobile, joining the pending 2.15.0 notes (CHANGELOG and governance/package-release-notes.json updated; docs/reference/package-api-migrations.md regenerated).

Verification

  • Local commands and results (Node 24.11.1, pnpm 10.33.2, pnpm install --frozen-lockfile --ignore-scripts):
    • New src/__tests/WalletPermissionsManager.unfundedGrant.test.ts, RED on main (cf6f1c08): 3 of 4 fail (a second prompt, Expected: 1 Received: 2; the grouped waiter rejected instead of resolved with WERR_INSUFFICIENT_FUNDS; no session store). The expiry case is a boundary check and passes before and after. GREEN: 4 of 4.
    • jest WalletPermissionsManager: 22 suites, 289 tests passed.
    • Full wallet-toolbox jest (the package test patterns) before building: 296 suites passed, 2 skipped; 6 suites failed only because they load the built out/. After pnpm build those 6 suites pass (74 tests).
    • pnpm typecheck (wallet-toolbox), pnpm lint, pnpm format:check, pnpm health:check: pass.
  • Hosted CI run: pending on this draft
  • Conformance evidence: no BRC-100 interface or wire contract changes
  • Coverage delta: new paths covered by the new suite; hosted figure pending
  • Lint/typecheck delta: zero warnings
  • Browser/mobile/packed-consumer evidence: not run locally
  • Performance or bundle-size delta: one in-memory map; nothing added to the funded path
  • I self-reviewed the complete diff for correctness, security,
    compatibility, public API, artifacts, dependencies, docs, and operations
  • All applicable checks are terminal and successful on the exact head; any
    scope-based skip is expected and validated by the merge gate

Security and dependencies

  • No dependency or lockfile change
  • Changelog, runtime relevance, peer compatibility, transitive graph, and
    audit results were reviewed
  • CodeQL/negative tests cover any changed trust boundary
  • The exact-head CodeQL analysis has no new alert
  • The exact-head repository quality gate reports zero new Sonar findings
    (including accepted or false-positive issue states) and zero unreviewed hotspots;
    Sonar's aggregate Quality Gate passed verdict alone is not merge evidence
  • No new override, advisory dismissal, quality suppression, or skipped test
  • Any temporary exception is registered with owner, evidence, review date,
    and removal condition
  • Workflow permissions and lifecycle-script behavior remain least privilege

Notes for review

  • In BSV Desktop the storage call crosses Electron IPC, and today only the error's message survives that hop, so this check would not see the code there. bsv-blockchain/bsv-desktop has a companion draft that keeps the error class across the bridge (fix(storage): keep the toolbox error class across the IPC bridge bsv-desktop#127).
  • I would welcome a different shape if you prefer one, for example a durable local grant, or minting the token later once funds arrive. This PR is the smallest change I could find that keeps today's funded behavior and the "failed mint re-prompts" rule intact.

🤖 Generated with Claude Code

…ot be funded

A permission token costs a little to mint. In a wallet with no spendable
coins the mint fails with WERR_INSUFFICIENT_FUNDS and the user's decision
was dropped with it: grantPersistentPermission skipped cachePermission and
markRecentGrant, so the next identical call prompted again, and a grouped
or counterparty grant rejected the call that was waiting on it.

- On WERR_INSUFFICIENT_FUNDS only, the grant is kept in memory for the life
  of the manager (until its expiry) and checked with the other grant caches.
- Grouped and counterparty grants resume their waiting call in that case.
- The grant call still throws the funding error; nothing is persisted, so
  the next session asks again and a funded wallet mints as before.
- Spending authorizations are never kept without a token, and every other
  mint failure still re-opens the prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
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