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
actionat 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 retryactioninside 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
actioncompleted — Dart'sfinallydiscards 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 —
actionthrows 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 andactionrefuses 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
-
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