features/wallet/wallet_providers library

Classes

IncomingFundsNotifier
SyncStatusNotifier
WalletInFlightSendsViewNotifier
See walletInFlightSendsProvider.
WalletInFlightSwapsViewNotifier
See walletInFlightSwapsProvider.
WalletParkedSendsViewNotifier
See walletParkedSendsProvider.
WalletReceiveAddressViewNotifier
See walletReceiveAddressProvider.
WalletRecoverableEphemeralFundsViewNotifier
See walletRecoverableEphemeralFundsProvider.
WalletSnapshotViewNotifier
See walletSnapshotProvider.
WalletSyncedTip
See walletSyncedTipProvider.
WalletSyncedTipInvalidated
Positive evidence the last-synced claim is unsafe — suppress the header's height entirely (latch AND stamp) until the next completed pass on a CURRENT server (THE LATCH RULE: a behind server cannot clear this).
WalletSyncedTipLatched
A tip this session proved reached (THE LATCH RULE on walletSyncedTipProvider), with the moment it was observed.
WalletSyncedTipNotifier
WalletSyncedTipUnset
No verdict this session — the persisted stamp may render.
WalletTransparentAddressViewNotifier
See walletTransparentAddressProvider.

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.