WalletSendAuthorizer class abstract interface

The seam itself. The package invokes authorizeSpend around every money-committing bridge call, exactly once per user-confirmed action:

  • interactive send / shield / move-to-transparent (session.send),
  • the manual ephemeral sweep (session.sweepEphemeralFunds),
  • the offline queue (session.queueSend), and
  • a swap's OutOfZec execute (session.swapExecute).

A host implementation typically: prompts the user (its own UI — it owns a navigator; the package passes no BuildContext), unlocks its per-send signing credential, runs action, and re-locks in a finally so the credential can never outlive the one call it authorized:

class HostSendAuthorizer implements WalletSendAuthorizer {
  @override
  Future<T> authorizeSpend<T>(
      WalletSpendIntent intent, Future<T> Function() action) async {
    final passphrase = await promptForPassphrase(intent);
    if (passphrase == null) throw const WalletSpendAuthorizationDenied();
    await stageSpendCredential(passphrase);
    try {
      return await action();
    } finally {
      // Swallow re-lock failures: a `finally` that throws DISCARDS the
      // completed action's result (Dart semantics) — the spend RAN, and
      // an escaping cleanup error would make the flow report "nothing
      // was sent" over moved money. But do NOT ignore it: a credential
      // that silently outlives its one call re-arms the SDK's DEFERRED
      // signing paths (parked-send drain, a TEX second leg) until the
      // next stage/clear or process death — alarm, log (code only),
      // and retry the clear.
      try {
        await clearSpendCredential();
      } catch (_) {
        // alarm + schedule a clear retry — never rethrow
      }
    }
  }
}

CONTRACT (the type system enforces most of it — T is opaque, so the only ways out are running action or throwing):

  • Run action at most once, and return ITS result / let ITS error propagate untouched — the controllers' typed-fault classification depends on seeing the SDK's own errors. Never retry action inside the authorizer: for proposal-consuming kinds the SDK's one-shot token backstops a double run, but a WalletSpendKind.queuedSend has NO token — a second run queues a SECOND send, a double pay at drain.
  • Throw WalletSpendAuthorizationDenied to decline — but ONLY when your own UI already communicated the denial (its doc has the precondition; the package shows nothing for it). Any OTHER throw is treated as a real failure and classified like a signing error. That includes a throw from YOUR cleanup after action completed — Dart's finally discards the completed result, so the flow would report a failure over money that MOVED. Cleanup must swallow its own errors (see the example).
  • ALWAYS complete (resolve or throw): the flow holds its transient "Submitting…" state until you do, so a prompt must be cancelable. The package deliberately applies NO timeout here — authorization legitimately takes as long as the user takes. NOTE: screen re-entry now RE-ATTACHES to an in-flight flow instead of resetting it, so a prompt that never completes wedges that flow until a session change — there is no package-side escape by design; completion is yours to guarantee.
  • Expect CONCURRENT invocations from independent flows — a running ephemeral sweep's bracket stays open across its whole multi-tx run while a send confirm can start its own. Every interleaving is money-safe SDK-side (one spend sub-seed; one-shot proposal tokens), but a custody model with a SINGLE staging slot must serialize the brackets internally (an async mutex around this method) or an early clear kills the other flow's slot into a spurious typed failure.
  • IDENTITY SWITCHES: deny/dismiss any outstanding prompt when the host flips the wallet session (a duress/decoy or account switch). As the backstop, an approval that lands after the flip is FENCED — action throws WalletSpendSessionChanged instead of spending from the dead identity's wallet, and the package shows nothing (see that type's doc). Note the fence covers the not-yet-run action only: a spend whose action was ALREADY in flight when the flip happened completes on the old session — its result screen is deliberately discarded, and the old identity's own surfaces carry the truth (activity for a send/shield/ move, the parked list for a queued send, in-flight for a two-step; an OutOfZec swap's tracking state is session-local — the deposit itself stays bounded by its quote deadline).
  • The prompt is MODAL over the send screen (S13). While it is up the user must not be able to reach the screen underneath: the screen's "Still sending — your payment keeps going if you leave" is true only once the action has run. If the screen is removed while the prompt is still up (a host pop/go/replace), the flow is ABANDONED and action refuses before spending — so the "no transaction" the host was told stays true. A non-modal prompt would let a user leave a spend they were still deciding on, and then have their approval refused.

DEFERRED-SIGNING CAVEAT: at the QUEUE-TIME WalletSpendKind.queuedSend the authorization moment is the COMMIT moment, not the signing moment — the SDK's background drain signs it later, and this hook cannot carry a per-send credential across to that drain.

FR-23-b (#361) LIFTS the consequence: the package's parked-sends surface now offers "Send now", which calls session.authorizeParkedSend INSIDE this bracket, so a per-spend-credential host CAN drain its offline queue — one row per prompt (the row's own FR-17 binding rides the seed pull, so one staged credential serves exactly one row; a batch affordance loops brackets). Serve that call and walletOfflineQueueSupportedProvider = true becomes honest; leave it false while you don't, so the package never advertises a queue that cannot drain.

WalletSpendKind.swapDeposit is NOT deferred (FR-23-a): the deposit signs INSIDE this bracket, at execute — so a per-spend-credential (host-custody) wallet CAN swap. The credential just needs to stay staged for the duration of the action() (the bridge call that signs), exactly like an interactive send.

Implementers

Properties

hashCode → int
The hash code for this object.
no setterinherited
runtimeType → Type
A representation of the runtime type of the object.
no setterinherited

Methods

authorizeSpend<T>(WalletSpendIntent intent, Future<T> action()) → Future<T>
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
toString() → String
A string representation of this object.
inherited

Operators

operator ==(Object other) → bool
The equality operator.
inherited