features/wallet/wallet_providers
library
Constants
-
walletActivityPageSize
→ const int
-
The activity-list page size (one screenful + headroom). MUST stay ≤ the Rust
MAX_TX_PAGE_SIZE ceiling (500) — the SDK clamps independently, so a future bump
past it would silently cap rather than error. No magic numbers (gate 7). The
accumulating activity list (keyset pagination) lives in walletActivityProvider
(see wallet_activity_controller.dart); the wallet screen calls its refresh()
on the same balance-changing edges as walletSnapshotProvider.
-
walletAddressDeriveTimeout
→ const Duration
-
The address-DERIVE load bound (#386, the E2E-2 device measurement),
used ONLY by walletReceiveAddressReadProvider /
walletTransparentAddressReadProvider: the FIRST derive under launch
catch-up queues behind the contended engine lock for 25–45 s on real
hardware, so the shared 15 s bound FALSE-FIRED on exactly the pass it was
calibrated against ("well past the ~3 s cold-launch derive" — an
idle-wallet figure). The contract is "never false-fires on an honest slow
path, always fires on a genuine wedge"; the derive's latency itself is the
warm-path (engine lock contention), out of scope beyond honest
surfacing.
-
walletFfiWedgeTimeout
→ const Duration
-
The general local-FFI load bound (no-magic-numbers, gate 7), consumed by
NINE call sites across the package (send/shield preflights, activity
pages, rescan estimate, parked moves, auto-shield). A wedged FFI call
exceeding this surfaces the honest error state rather than hanging the
surface. Deliberately NOT raised by #386: the two address DERIVES got
their own measured bound below (walletAddressDeriveTimeout) — retuning
every money-path wedge detector 4× off one receive-screen measurement was
the review's converged blast-radius finding.
-
walletReceiveAddressTimeout
→ const Duration
-
The pre-name of walletFfiWedgeTimeout, kept as an alias because this
file is barrel-exported (host compat). The bound was never receive-specific
— it was the de-facto package-wide FFI wedge detector under a
receive-screen name (the s198_tail naming finding).
Properties
-
incomingFundsEventsProvider
→ NotifierProvider<IncomingFundsNotifier, AsyncValue<IncomingFundsEvent>>
-
The live incoming-funds event stream (spec §3.3 / ADR-0536 — the FR-1
"funds arrived" hook), lifecycle-gated and reconnecting exactly like
syncStatusProvider (the two orthogonal recovery mechanisms documented
there). State = the LATEST event; loading until the first
replay lands.
final
-
isWatchOnlyProvider
→ Provider<bool>
-
Is the active wallet WATCH-ONLY (#397 §3.7 D4/D5)? — the UI chrome key: a
"Watch-only" badge, hidden Send/Shield/Swap affordances, and the Security
screen's export-instead-of-backup arm read this. CHROME-ONLY by design: the
SDK's typed
WalletErrorKind.watchOnly refusals stand regardless, so this
provider is allowed to fail SAFE — false (the full-spend chrome). A
spurious false only shows an affordance the SDK then refuses honestly
(never a wrong spend); a spurious true only hides an affordance.
final
-
syncStatusProvider
→ NotifierProvider<SyncStatusNotifier, AsyncValue<SyncStatus>>
-
The live, lifecycle-gated sync status (spec §3.3; flutter-patterns
§ Stream Lifecycle; the contract in
core/lifecycle/app_lifecycle_provider).
final
-
walletAutoShieldSupportedProvider
→ Provider<bool>
-
HOST SEAM (#383 R2): whether the host's custody model supports the
AUTO-SHIELD policy loop (#328). The loop's spends are
origin: automatic —
no user is present at a prompt — so a per-spend-credential host (every
automatic spend is policy-denied) should override this to false: the
auto-shield toggle is then HIDDEN in the Transparent-funds sheet and the
loop never arms — an honest absence instead of a switch that reads ON but
can never run (the walletOfflineQueueSupportedProvider honesty rule).
UX honesty, not a safety boundary: even when true on such custody, every
attempt still routes through the authorizer and a denial stops the loop
for the session with the funds honestly visible.
final
-
walletCustodyDisclosureProvider
→ FutureProvider<CustodyDisclosure>
-
The PRODUCTION per-tier custody disclosure (FR-14 H1) for the provisioned
wallet — the honest "are my keys hardware-backed / what does delete do"
answer the Security screen renders BEFORE a delete-wallet and as
a custody badge. A one-shot
autoDispose FutureProvider so re-opening the
screen re-probes the live tier. Reads ONLY the measured vault tier (no seed,
no unseal, NO key material). Errors (no provisioner / a wedged keychain) are
surfaced honestly by the screen, never silently treated as "protected" —
and the retry pin is what makes "honestly" PROMPT: without it
the container default silently re-probed a failing keychain ~10× (~38 s of
spinner) before the probe-error card could land on the screen the user
reads BEFORE a delete-wallet. Re-opening the screen is the natural retry.
final
-
walletEffectiveServerHostProvider
→ Provider<String?>
-
The HOST the sync sheet's Server row shows: the session's effective server
when a session exists (the connection's own truth — the no-drift rule
kept by reading the dial itself), else the host-config seam
walletEndpointHostProvider (the pre-session value, and a session-only
host's override). Host only, never the URL.
final
-
walletEndpointHostProvider
→ Provider<String?>
-
HOST SEAM: the lightwalletd server HOST name shown on the sync sheet's
Connection section ("Server: zec.rocks") — the user should be
able to see which host the wallet talks to.
null (the default) hides the
row. walletOnboardingOverrides wires it automatically from the
WalletConfig.endpointUrl; a session-only host overrides it alongside its
session. HOST only — never a full URL (a URL can carry userinfo/params that
don't belong on screen).
final
-
walletHostTransportProvider
→ Provider<WalletHostTransport?>
-
HOST SEAM: the host's OWN transport claim for wallet traffic (maintainer
A host that routes ALL its traffic through its own privacy layer
(xray/vless, a VPN, its own Tor) overrides this so the wallet's network
indicator tells the truth — the SDK's
TorState can only see its own
built-in Tor and would honestly read "Tor off" under a host tunnel. null
(the default) derives the indicator from the SDK's TorState. The claim is
the HOST's responsibility: protection: true earns the protected (green)
treatment, so only pass it for a transport that actually hides the user's
network identity from the server (§3.3 — never claim a protection that is
not running).
final
-
walletIdentityProvider
→ Provider<Object?>
-
The wallet IDENTITY the money surfaces key their last-known values on
(#381 (c) — the converged HIGH). Riverpod retains an async
provider's previous value through a rebuild (
copyWithPrevious), which is
exactly right for a transient re-read or the rescan's same-wallet session
swap — and exactly WRONG across a wallet identity change, where it painted
the DELETED wallet's balance, as-of stamp, Send gate, and parked/in-flight
amounts onto the NEXT wallet's first frames (persistently under a busy
first read), and on a duress/decoy multi-identity host flashed the OWNER's
balance on the DECOY's surface — the same leak class the settings store
got its identity fence for (A4/#348). The four money read providers below
drop their retained value whenever THIS value changes.
final
-
walletInFlightSendsProvider
→ NotifierProvider<WalletInFlightSendsViewNotifier, AsyncValue<List<InFlightSend>>>
-
The in-flight-sends VIEW — identity-fenced (#381 (c)) like the parked
surface.
final
-
walletInFlightSendsReadProvider
→ FutureProviderFamily<List<InFlightSend>, Object?>
-
The IN-FLIGHT two-step (TEX) sends (#309) — first leg broadcast, send not yet complete;
money in motion through a wallet-controlled one-time address. Drives the DURABLE
wallet-screen "on its way — don't send it again" cue that survives the dismissible
post-send result screen across the double-pay temptation window. Invalidated on the
SAME edges as walletParkedSendsProvider (the
_WalletActive sync listener + resume)
so the cue appears after an interactive partial / a queued drain and CLEARS when the
chain completes, strands (→ the recoverable surface), or requeues the send (every tx
expired unmined → back to the parked surface). Returns EMPTY when no session exists. Additive
CAUTIONARY info like the recoverable subset (the same tx0 is independently visible as
a pending tx in the activity list, so a read failure hides no money — unlike parked,
where the surface is the ONLY witness). The render does NOT self-hide on a failure
(#308a, S2 §3.5d): it says it could not check and is retrying, because a vanished cue
reads as "nothing is mid-flight". The container default retry stays a DELIBERATE
KEEP: the retrying state carries hasError, so the line stands through
the retries and a transient blip heals on its own. Readers that only want the
rows (.value ?? [], the rescan sheet's advisory) stay advisory — the core's
fence refuses a rescan with money in motion. AMOUNT-only (§5.4 — the one-time
address never crosses the bridge).
The identity-scoped READER behind walletInFlightSendsProvider —
invalidate THIS; watch the view.
final
-
walletInFlightSwapsProvider
→ NotifierProvider<WalletInFlightSwapsViewNotifier, AsyncValue<List<SwapRecord>>>
-
The in-flight-swaps VIEW — identity-fenced (#381 (c)): a live swap is
money in motion, so it must never carry across a wallet identity change.
final
-
walletInFlightSwapsReadProvider
→ FutureProviderFamily<List<SwapRecord>, Object?>
-
The durable IN-FLIGHT SWAPS surface (W-swap-5, #366) — every swap whose
order was registered at execute and has neither been dismissed nor
self-lapsed. THE kill→relaunch re-attach: after a process restart the
wallet screen lists these and "view swap" re-opens live tracking by the
record's id — without this, an armed OutOfZec deposit was invisible
everywhere while the one-swap-in-flight guard refused new swaps (the
hardware photo). Invalidated on the SAME edges as the send surfaces (the
_WalletActive sync listener + resume) AND on a swap execute landing / a
record dismiss. Returns EMPTY when no session exists. Like the PARKED
surface, a read failure PROPAGATES to AsyncError: mid-flight this list
is the ONLY wallet-side witness of the swap (an OutOfZec deposit is
excluded from the parked/in-flight send surfaces by design; an IntoZec
swap has no activity trace until delivery), so the surface shows an
honest "couldn't load" rather than a silent hide. Deliberately NOT gated
on swapEnabledProvider: a local read is not swap traffic (§3.5) — the
user keeps sight of money in motion even with swap disabled or killed.
§5.4: the record id is render-never-log.
RETRY PINNED OFF: same shape and CORRECTED rationale as
walletParkedSendsReadProvider — the section's skipLoadingOnReload
already surfaced the error arm on the first failure; the pin kills the
wasted background retry cycling behind it and settles the state, at the
cost of the error line standing until the shared sync-edge/resume/dismiss
invalidation cadence (this reader's own retry) next fires.
The identity-scoped READER behind walletInFlightSwapsProvider —
invalidate THIS; watch the view.
final
-
walletOfflineQueueSupportedProvider
→ Provider<bool>
-
HOST SEAM: whether the host's custody model supports the OFFLINE send
queue (#327). Queuing persists the intent NOW and SIGNS later — so a custody
model whose signing credential exists only inside an authorized window
(per-send passphrase/biometric) historically could not serve it at all, and
the send form must not offer "save for later" it cannot honour: override to
false and the affordance is HIDDEN (an honest absence beats a queue that
faults at drain). UX honesty, not a safety boundary — if a queue does slip
through on such custody, the drain fails typed, the send stays visibly
parked ("saved & pending") and cancellable; no funds move.
final
-
walletParkedSendsProvider
→ NotifierProvider<WalletParkedSendsViewNotifier, AsyncValue<List<ParkedSend>>>
-
The parked-sends VIEW — identity-fenced (#381 (c)): a parked AMOUNT is
money display, so it must never carry across a wallet identity change.
final
-
walletParkedSendsReadProvider
→ FutureProviderFamily<List<ParkedSend>, Object?>
-
The queued sends PARKED by the multi-step gate (2e-2b-v-3/v-4) — TEX (ZIP-320)
sends sitting in the queue with NO on-chain transaction, so they appear NOWHERE
in the activity list. A one-shot
FutureProvider, invalidated on the SAME
balance-changing edges as walletSnapshotProvider (the _WalletActive sync
listener + the resume path) AND explicitly after a cancel, so the "saved &
pending" surface stays current. Returns an EMPTY list when no session exists.
UNLIKE the recoverable subset (purely additive info that may swallow to empty),
a read FAILURE here PROPAGATES to AsyncError: a parked send is money the user
is waiting on, so the surface shows an honest "couldn't load" rather than a
silent-hide (the exact failure 2e-2b-v's visibility surfaces exist to foreclose).
AMOUNT-ONLY — the one-time recipient address never crosses the bridge (§5.4).
LIVE end-to-end since gate-removal (2e-2b-v-5a): both the interactive send and
queueSend paths can park a TEX here (empty only when none are parked).
RETRY PINNED OFF (audit; rationale CORRECTED by the review probe):
the section's skipLoadingOnReload already fell through to the honest
error arm after the FIRST failure even under the container default (a
retrying state carries hasError), so the pin does not change what a
cold-load failure SHOWS — it kills the up-to-ten wasted background FFI
retries cycling behind that line and settles the state honestly. The REAL
trade: a one-shot transient blip now leaves the error line standing until
the next invalidation edge (sync-edge/cancel/resume — this reader's own
retry cadence, typically ≤ ~95 s foreground) instead of self-healing in
~450 ms; accepted for a single honest attempt + posture consistency with
the other pinned money readers.
The identity-scoped READER behind walletParkedSendsProvider —
invalidate THIS; watch the view.
final
-
walletReceiveAddressProvider
→ NotifierProvider<WalletReceiveAddressViewNotifier, AsyncValue<String>>
-
The receive-address VIEW — see walletReceiveAddressReadProvider.
final
-
walletReceiveAddressReadProvider
→ FutureProviderFamily<String, Object?>
-
The wallet's current receive address (the unified address for account 0) —
what the user shares to receive ZEC. Only meaningful when a session exists
(the surface is reachable only from a provisioned wallet). NOT key material
— the address is public by design; §5.4 NEVER-LOG still applies
(display/copy, never log).
final
-
walletRecoverableEphemeralFundsProvider
→ NotifierProvider<WalletRecoverableEphemeralFundsViewNotifier, AsyncValue<List<RecoverableEphemeralFunds>>>
-
The recoverable one-time-address funds VIEW — identity-fenced (#381 (c)).
final
-
walletRecoverableEphemeralFundsReadProvider
→ FutureProviderFamily<List<RecoverableEphemeralFunds>, Object?>
-
The wallet's RECOVERABLE one-time-address (ephemeral) funds (2e-2b) — the
SUBSET of the transparent balance sitting on a wallet-controlled single-use
address (an expired TEX forward OR an exchange return). A one-shot
FutureProvider, invalidated on the SYNC balance-changing edges
(spendable/reachedTip in the _WalletActive listener) AND the resume path, so a
newly-surfaced amount appears without a manual pull. NOTE: this is a SUBSET of
walletSnapshotProvider's edges — the user-action completions that also move
transparent funds (shield / move-to-transparent / swap / send) invalidate the
snapshot but NOT (yet) this provider, so the recoverable list can lag the
snapshot after such an action until the next sync edge. That lag is HARMLESS by
construction: the _BalanceCard render CLAMPS the displayed amount to
transparentZat, so a stale-high value can never claim more than the
transparent line it annotates (the subset invariant holds at the render).
Extending invalidation to those action-completion edges stays an optional
refinement (the lag is harmless by the clamp above); the LOAD-BEARING edges are
the sync/resume ones, where an on-chain strand actually surfaces (tx0 mines /
tx1 expires). Returns an EMPTY list when no session exists: the row is purely additive
INFORMATION, so its absence must never raise an error on the money surface
(unlike the snapshot, which the screen depends on). AMOUNT-ONLY — the one-time
address is wallet-internal and never crosses the bridge (§5.4 never-render).
LIVE since gate-removal (2e-2b-v-5a): a TEX two-step can now strand, so this
surface is load-bearing — empty only on a wallet that has stranded nothing.
DELIBERATE KEEP of the container default retry: the render
self-hides on failure (.value ?? []), so a silent heal is strictly better
than surfacing a transient blip on a purely-additive row.
The identity-scoped READER behind
walletRecoverableEphemeralFundsProvider — invalidate THIS; watch the view.
final
-
walletSendAuthorizerProvider
→ Provider<WalletSendAuthorizer>
-
HOST SEAM: authorization around EVERY money-committing bridge call (#327 —
security review F1). The controllers route each signing/committing
action (interactive send, shield, move-to-transparent, ephemeral sweep,
offline queue, swap OutOfZec execute) through
WalletSendAuthorizer.authorizeSpend exactly once per user-confirmed
action. The default pass-through is correct for sealed-keychain custody; a
host with per-send credentials overrides this with its
prompt → unlock → sign → re-lock cycle and throws
WalletSpendAuthorizationDenied on cancel (the flow silently restores its
pre-confirm state — the proposal token stays unconsumed, no bridge call is
made). See WalletSendAuthorizer for the full contract, including the
deferred-signing caveat that pairs this seam with
walletOfflineQueueSupportedProvider.
final
-
walletSendCeilingZatProvider
→ Provider<int?>
-
HOST SEAM: an optional send-amount ceiling in zatoshis — e.g. an alpha
roll-out cap ("sends above 1 ZEC are disabled for now").
null (the
default) means NO ceiling. Enforced at the send form's chokepoints
(SendController.prepare and SendController.queueOffline) BEFORE any
compose/propose bridge call, surfacing as an honest inline
SendOverCeiling fault with the limit in the copy. POLICY, not safety:
the SDK's own range/funds validation is independent of this. Scope is the
interactive send + offline queue only — self-transfers (shield /
move-to-transparent) are wallet-internal and deliberately not covered.
The SWAP deposit IS bounded by this ceiling (FR-23, maintainer 2026-07-10): an
OutOfZec deposit whose ZEC side exceeds the cap is refused at quote-review
with the same over-ceiling fault, so every alpha money-OUT path stays under
one cap. (IntoZec moves no wallet funds — the user sends the deposit
externally — so it is unaffected.)
final
-
walletSessionProvider
→ Provider<WalletSession?>
-
The wallet session source — the money-safety gate's single home. A
deposit-capable WalletSession is exposed to the wallet surface ONLY when
onboarding is OnboardingActive, i.e. AFTER the recovery-phrase backup is
confirmed-and-persisted. Every other phase (loading / welcome / generating /
awaiting-backup / confirming / failed / unavailable — including a
provisioned-but-unconfirmed wallet resumed after a crash) yields
null, and
the wallet surface renders its honest not-set-up state: never invite a
deposit into a wallet whose seed the user has not backed up.
final
-
walletSnapshotProvider
→ NotifierProvider<WalletSnapshotViewNotifier, AsyncValue<WalletState>>
-
The cold-snapshot VIEW every surface watches — identity-fenced (#381
(c), see the fence note above): last-known
.value retention survives
re-reads and the rescan's same-wallet session swap, and can NEVER carry
a deleted/switched-away wallet's balance, as-of stamp, or Send gate
onto the next wallet's frames. To force a re-read, invalidate
walletSnapshotReadProvider — invalidating this view is a no-op.
final
-
walletSnapshotReadProvider
→ FutureProviderFamily<WalletState, Object?>
-
The on-resume cold snapshot (spec §3.3) — balance, Tor, tip, balance
age,
seq. A one-shot read; SyncStatusNotifier invalidates it on a
real resume so a backgrounded UI re-reads cold state before trusting
live events again. Only watched when a session exists. The identity-
scoped READER behind walletSnapshotProvider — invalidate THIS to
force a re-read; watch the view.
final
-
walletSyncedTipProvider
→ NotifierProvider<WalletSyncedTipNotifier, WalletSyncedTip>
-
The session's verdict on the last fully-synced tip — TRI-STATE (#317, the
wrap-review design). The balance header's "(as of block N
, time)"
resolves through this (balanceAsOf):
final
-
walletSyncPolicyProvider
→ Provider<bool>
-
HOST SEAM (#383 R1): whether the package may RUN the background sync loop.
Default
true (sync just runs — real wallets have no Start button). A host
that gates syncing behind its own setting (a data-saver/privacy toggle, an
org policy) overrides this to false: the package then never issues
startSync — and the sync badge/sheet HONESTLY render "sync off — turn it
on in settings" instead of a stalled/connecting story that never resolves
(the honest-off posture; same honesty family as
walletOfflineQueueSupportedProvider). Reactive: flipping to true
starts the loop on the spot; flipping to false stops it (scan progress
is durable — resuming loses nothing). POLICY, not a privacy boundary: the
wallet still opens, balances show the last synced state, and Receive
still works (local derivation).
final
-
walletSyncServersProvider
→ FutureProvider<List<SyncServer>>
-
The sync servers the host OFFERS (the picker, P3-13) — read from the
session, re-read whenever the session identity changes (a switch swaps the
session). Empty when no session or nothing offered.
final
-
walletSyncServerStatusProvider
→ FutureProvider<SyncServerStatus?>
-
Which server the wallet dials, and why (the picker, P3-13) — the
CONNECTION's own truth, read from the session and re-read when it swaps.
null with no session. The Server row and the picker render from this.
final
-
walletTransparentAddressProvider
→ NotifierProvider<WalletTransparentAddressViewNotifier, AsyncValue<String>>
-
The transparent-address VIEW — see walletTransparentAddressReadProvider.
final
-
walletTransparentAddressReadProvider
→ FutureProviderFamily<String, Object?>
-
The wallet's TRANSPARENT receive address (Recv-2 / ADR-0528) — the
external-scope P2PKH t-address the receive screen shows when the user
toggles to "Transparent". The walletReceiveAddressReadProvider pair's
exact mirror (#385): session-lifetime cached (this is the SLOW derive the
device walk measured at multiple seconds per visit), identity-fenced
(the same duress-leak sibling), same honest-degradation timeout. NOT key
material (PUBLIC by design; §5.4 NEVER-LOG still applies).
final
Functions
-
walletNoSilentRetry(int retryCount, Object error)
→ Duration?
-
The shared no-silent-retry pin (#386 probe-6d, WIDENED by the audit):
riverpod 3's container default retries ANY non-
Error exception up to 10
times with ~38 s of cumulative backoff — and FrbException implements Exception, so every typed FFI failure rides it. On the pinned surfaces
that swallow turned an honest one-shot failure into minutes of loading,
each retry discarding the in-flight call: the address derives (#386 — ~10
timeout+backoff cycles of "Preparing your address…", each re-queuing a
fresh FFI call behind the same contended engine lock, with the rebuild
additionally deferred on a PAUSED element — the E2E-2 device symptom); the
parked/in-flight-swaps money witnesses (whose documented contract is
read-failure → an honest "couldn't load", not a section silent-hidden
through the retry window); the custody probe; the token list. No silent
retry: failure → visible AsyncError → the surface's OWN retry affordance
(a Try-again button, a screen re-open, the sync-edge/resume invalidation
cadence) IS the retry, exactly the designed contract. Public: a host
building custom readers over the SDK wants the same posture. The
deliberate KEEPS of the container default are documented at
walletSnapshotReadProvider, walletRecoverableEphemeralFundsReadProvider
and walletInFlightSendsReadProvider.