AUTHORIZE a parked send NOW (FR-23-b) — sign a send the user already committed, at this
user-present moment, instead of waiting for a background pass to sign it. Pass BOTH the id
AND the createdAt from the `ParkedSend` the user acted on.
The account birthday height — the scan floor a rescan rebuilds from and
the lowest useful rescanFrom target. Clamp a rescan date-picker's
DEFAULT with this instead of a blind "one year ago": map the default
date to a height with estimateBirthday and take the max of the two —
on any wallet younger than a year the blind default silently costs
hours of scanning. Do NOT max-clamp a date the USER picked: an
explicitly earlier pick is the post-restore recovery path (this height
may itself be a too-recent restore estimate, and rescan exists
precisely to go below it), so clamping it up to this floor silently
no-ops that recovery. null before the account is provisioned (first
sync hasn't run). A height, never money.
WalletErrorKind.invalidState if the handle is closed.
CANCEL a parked (queued) send the host listed via listParkedSends — the user-facing escape
hatch to discard a TEX send waiting for the multi-step path (which would otherwise AUTO-BROADCAST
once that path is enabled — see the `ParkedSend` DOUBLE-PAY
CAUTION; cancel is the SAFE counter-affordance, never a re-send). Pass BOTH the id AND the
createdAt from the `ParkedSend` the user is cancelling. Returns true if it was removed,
false if it was already gone / began sending (post-multi-step-enable) / its rowid was reused
(idempotent). On false, re-read listParkedSends — but do NOT present it as definitively
"cancelled" nor invite a re-send: a send that began sending has LEFT the parked surface, so the
payment may be IN-FLIGHT (direct the user to the activity list; a re-send risks a DOUBLE-PAY).
IRREVERSIBLE + UNCONFIRMED — it discards the committed intent immediately; the host MUST confirm
before calling (the confirm dialog is v-4b). MONEY-SAFE: only a still-Queued intent is deleted
(no note reserved, nothing signed, no tx on-chain — cancel cannot strand funds), and the createdAt
identity pin makes a reused id cancel NOTHING. §5.4: ids/timestamps only — no amount or address
crosses. Throws WalletErrorKind.invalidState on a closed handle.
#390 — "Check older swap addresses": widen the range the wallet watches so the
normal sync surfaces older swap deposits/refunds a SEED-ONLY RESTORE left unchecked
(a wallet with a long swap history restored from its phrase). It does NOT itself
find funds — it WIDENS, then any older swap money appears in the balance over the
next minutes of sync. Returns a counts-only SwapAddressCheckReport — render
"checking older swap addresses as your wallet syncs; anything found appears in your
balance", NEVER "found X". Rerunnable (each accepted run goes deeper).
Close the wallet: stop and join the sync loop, QUIESCE any straggling
scan-batch section (a bounded wait — v-5c finding #2), then release the
single-writer lock and close storage on a background thread — all
before this future resolves, so a subsequent open at the same
dbDir never observes a half-closed DB. BOUNDED residual: a wedged
straggler past the quiesce budget can keep the lock alive briefly
after this resolves (integrity still holds; the lock frees the moment
it ends — an immediate re-open should tolerate a transient
WalletAlreadyOpen with a short retry). The chain is the source of
truth, so closing loses nothing. Idempotent: a second call is a no-op.
The wallet's default receive address — a Unified Address with Orchard +
Sapling receivers (no transparent receiver), encoded for display/QR.
Deterministic per seed.
The wallet's transparent RECEIVE address (Recv-2 / ADR-0528): the account's
canonical external-scope (m/44'/coin'/0'/0/0) P2PKH t-address, surfaced ALONGSIDE
the shielded UA behind the receive screen's address-type toggle (default shielded).
PUBLIC + reused-address-linkable (visible on-chain) — the host labels it as such and
keeps the shielded UA the recommended default. Deterministic per seed; the spending
key never leaves Rust (§4.1). Returns the bare t-address string (no librustzcash type
crosses the bridge). A wallet whose stored UFVK predates transparent-inputs (pre-GA)
surfaces the typed key-derivation error, never a panic.
The wallet's DELIVERY OBLIGATION for one transaction it created — the
same value a history row carries on TxSummary.delivery, for a caller
that holds a txidHex (the one send returned in its
TxSubmitResults, passed back verbatim). Read it right after a send
whose result was not all-success: null or anything but
DeliveryState.retryPending means the wallet is NOT promising to send
it on a later sync, so do not say so. null for a transaction this
wallet did not create, one that has expired (its TxStatus says so) or
one it holds no row for. Reads the aux connection off the engine lock,
so it never blocks sync. Throws WalletErrorKind.txidInvalid for a
txidHex that is not 64 hex characters; WalletErrorKind.invalidState
on a closed handle.
How many connections each arm served, by outcome, since this wallet
opened — the numbers to show beside the transport state (the network
panel's "N private, M direct"). One connection is one dial; the count
is exactly what the SDK's own wallet.dial log line records, so the
two never disagree. In memory only: a reopen starts at zero. Readable
in every lifecycle phase, cheap, no I/O. Never logged by the SDK — it
crosses only on this call.
WalletErrorKind.invalidState if the handle is closed.
Remove one in-flight swap record. USER-INTENT only since #367: call from the
terminal card's Done (the outcome rendered full-screen = seen) or the home
row's explicit Remove — a terminal OBSERVATION pins via recordSwapOutcome
instead, so an away-at-terminal outcome is never erased unseen.
Display-only state: this never touches the deposit outbox, the in-flight
guard, or detection (money in motion keeps moving; only the home row goes).
Idempotent — returns false for an already-absent id, so a raced
double-dismiss is harmless. A never-dismissed record self-lapses out of
listInFlightSwaps at SwapRecord.expiresAt.
Construct the NEAR Intents swap provider over THIS wallet's own transport
and turn the swap on-ramp ON (spec §3.5; the swap-near feature). The
composition entry behind the swapQuote/swapExecute/watchSwapStatus
surface — until it succeeds, WalletHandle.swap is conceptually null
(kill layer 2) and those three throw SwapErrorKind.swapDisabled.
How close the wallet is to the one-time-address (ZIP-320 ephemeral) gap-limit ceiling — a
`ReservationPressure` (COUNTS-only). ⚠ outstanding
is a LOWER BOUND (#315 slice-2 proof discovery): the underlying engine read is BLIND to slots
held by sends CREATED then never confirmed (their output row exists from create-persist), so
it can read 0 while the window is actually full — treat it as "at least N in use", never as
proof the window is clear. Surface it as a proactive hint; the real gate is the engine's
TexSendLimitReached at send time, and the #315 reclaim (reclaimEphemeralSlots) gates on
its own wide reservation read, never on this gauge. An INDICATOR for display, NEVER a money
gate. 0 outstanding in the common case (no TEX send has reserved an address). Throws
WalletErrorKind.invalidState on a closed handle.
THE ONE SANCTIONED UFVK EGRESS (#397, spec §3.7 D1 / ADR-0538; §5.4
HARD-D re-cast: "never leaves the device SILENTLY"). Returns the
wallet's full viewing key in the standard unified encoding.
Is this a WATCH-ONLY wallet (#397 §3.7 D4/D5)? SYNC + lock-free (the
kind is immutable after open). The reference UI keys its chrome on
this — the watch-only badge, hidden Send/Shield/Swap affordances, the
no-backup security screen; the typed WalletErrorKind.watchOnly
refusals stand regardless, so a host ignoring it only ever sees
honest errors.
The IN-FLIGHT two-step (TEX/ZIP-320) sends: first leg signed + broadcast, send not yet
complete — the user's money is IN MOTION through a wallet-controlled one-time address. The
host renders a DURABLE "on its way — don't send it again" cue from this list (see
`InFlightSend` for the lifecycle + the DOUBLE-PAY
CAUTION): the post-send result screen's caution is dismissible, and without this surface an
in-flight two-step reads as an ordinary pending send exactly when a worried user is most
tempted to re-pay. DISJOINT from listParkedSends (parked = queued, nothing signed yet;
in-flight = signed + broadcast) — a send is in at most one. Read-only PULL, cheap + LOCAL
(a SQLite read; no network, no money movement); call it on the same edges as
listParkedSends (sync ticks / resume / after a send completes). Empty when nothing is
mid-flight — the common case. Amount-only (§5.4; no recipient/txid/address). Throws
WalletErrorKind.invalidState on a closed handle. HOST CONTRACT on a THROW: the call
fails LOUD on a corrupt store row (fail-closed — a broken row must never silently hide a
possibly-in-motion send); prefer SURFACING that failure over hiding the cue, because a
swallowed error removes the double-pay caution for EVERY in-flight send at once (a host
may fall back to hiding only because the pending tx stays visible in transactions).
The durable IN-FLIGHT SWAP 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 home screen lists these and
re-opens live tracking by feeding SwapRecord.id to watchSwapStatus — without
this list an armed OutOfZec deposit was invisible everywhere while the
one-swap-in-flight guard refused new swaps. NOT gated on swap being enabled or
the §3.5 kill state (a local read is not swap traffic — records stay readable so
the user never loses sight of money in motion). Read-only, cheap + LOCAL (a
SQLite read); call it on the same edges as listParkedSends (sync ticks /
resume / after an execute). Empty when nothing is in flight — the common case.
§5.4: SwapRecord.id is render-never-log. Throws WalletApiError only for a
closed handle / store fault.
EVERY queued send sitting in Queued that has NOT completed yet — a one-time-address (TEX)
two-step awaiting its drain pass / ceiling-parked, or (since #331) a plain single-step send
waiting in the offline queue; the DTO kind says which (presentation-only). They have NO
on-chain transaction yet (INVISIBLE in the activity / transaction list), so this surfaces
them "saved & pending" instead of silence — across an app relaunch it is the ONLY place the
committed spend exists (hidden, a queued send is a DOUBLE-PAY window). Each is amount + id +
kind + paused (`ParkedSend`); the recipient address NEVER
crosses the bridge. A read-only PULL the host calls on demand; cause-agnostic copy (never
"blocked"/"stuck"). A HEALTHY row auto-broadcasts once it can drain — present it per the
`ParkedSend` DOUBLE-PAY CAUTION (never invite a re-send) and EARMARK its amount over the
balance (it stays in spendable). A paused row does NOT auto-broadcast (#315 — the wallet
gave up auto-retrying it; it waits for retryParkedSend or cancelParkedSend and MUST be
phrased as paused, never "will send when ready"). To DISCARD either, pass its
(id, createdAt) to cancelParkedSend (the SDK cancel — the SAFE counter-affordance, landed v-4a,
kind-agnostic; the host UI button/confirm is v-4b). Throws WalletErrorKind.invalidState on a
closed handle.
FR-27 — the MACHINE-MEMO bytes of one transaction, scoped to the
prefixes registered on WalletConfig.machineMemoPrefixes. Returns them
in output order; txidHex is the txidHex a history row gave you,
passed back verbatim.
Mint the NEXT public diversified receive address (FR-8 / Recv-4, ADR-0537): a fresh
unlinkable Unified Address for a contact/invoice that still credits the one wallet
account. Payments to it are detected by the normal shielded scan — including after a
seed-only restore — with no extra host work. Same receiver set as currentAddress
(Orchard + Sapling, no transparent). Every call returns a NEW address from the
never-recycle counter, which survives restore: each seed-only restore starts
minting in a fresh RANDOM band (~2048 mints wide), so no prior life's addresses
are re-issued — deterministic for lives up to ~2048 mints, ≤ ~2^-18 per-restore
band-overlap chance beyond (attribution/linkage caveat, never a fund
risk). Local + offline + cheap (no network, no sync
requirement). Deterministic per index — keep MintedDiversifiedAddress.diversifierIndex
as the durable attribution key, and treat it as WALLET-INTERNAL (never share it with
a counterparty — it is a sequential mint ordinal). Throws
WalletErrorKind.keyDerivation before any account is provisioned (exotic —
accounts import eagerly at create), WalletErrorKind.invalidState on a closed
handle. §5.4: render/store the fields, never log them.
Dial choice under THIS wallet's Tor policy and ask the server who it
is (one GetLightdInfo, 15 s budget). Refuses typed —
WalletErrorKind.syncServerUnreachable (could not dial, timed out, or
the server refused), WalletErrorKind.networkMismatch (the server is
on another Zcash network), WalletErrorKind.syncServerNotOffered —
and NEVER switches. Use it for the picker's "Check server" step; a
switch probes again on its own. default probes endpointUrl.
Prepare a send from a ZIP-321 payment URI — propose (the FIRST half of the
propose→confirm→send flow, inc-2d-ffi). Runs the audited note-selection + fee +
change over the wallet DB and returns a SendProposal: the EXACT numbers
(total/fee/change, per-recipient pool + amount, the §5.1 de-shield disclosure) the
user confirms before signing. DETERMINISTIC — no spending keys, no proofs, no
network, no DB writes; NOTHING is sent. The opaque proposal token is retained
Rust-side; send consumes it by proposalId.
Propose shielding the wallet's detected transparent funds into its shielded pool —
the Recv-3 companion to the transparent receive address (exchange withdrawals and
swap-in deliveries land TRANSPARENT, §1.7; this is the privacy-positive "move them
shielded" action). Wraps the audited propose_shielding (Rule Zero), gated at the
0.001-ZEC shielding threshold:
Queue a send for OFFLINE-first delivery (inc-2d-ffi). Durably persists the send INTENT
(the ZIP-321 URI) and returns its opaque id IMMEDIATELY — NO network, NO signing, NO
money movement. The §6.2 "queued is a normal state" path: tap send with zero
connectivity and it survives a process kill, then proposes→signs→broadcasts on the next
online sync pass (the resubmission machinery) — proposing at SEND time, not queue time,
is exactly what stops a long-queued send from ever carrying a stale anchor.
RECLAIM a one-time-address (TEX) send window bricked by LEAKED reservations — sends that
reserved a one-time address but never confirmed, which the wallet cannot free on its own
(#315). Self-mints a small amount from your SHIELDED balance to the highest provably-abandoned
one-time address; mining it reopens the WHOLE window at once. MONEY-MOVING + EXPLICIT: gate it
behind the #327 authorizer with an honest-cost disclosure (the mint + a later recovery are TWO
transactions, ~4 network fees; the minted principal returns to your wallet).
Pin an OBSERVED terminal outcome into an in-flight swap record (#367): call
when the tracking stream reports a TERMINAL status (success / refunded /
failed) so the home row renders the truth even if the user was away at
observation time — deleting on observation (the pre-#367 contract) erased
outcomes the user never saw, durably if the process died in the gap.
FIRST-WINS (a terminal is forever; a later conflicting pin no-ops) and
idempotent — returns false for an absent / lapsed / already-pinned id.
Display-only state; never touches the deposit outbox, the in-flight guard,
or detection. §5.4: swapId is render-never-log.
Funds recoverable on wallet-controlled one-time (ephemeral) transparent addresses — a TEX
transfer whose forwarding step expired, or an exchange that returned a deposit to that
single-use address. A read-only PULL the host calls on demand (e.g. to render a "recover" row
on the balance). Each entry is amount + welded reorg-finality
(`RecoverableEphemeralFunds`); the one-time
address never crosses the bridge. PRESENTATION: the amount is a SUBSET of the displayed
balance — render it as part of the balance, NEVER as additional funds. Live now that TEX
two-step sends run in production (gate-removal, 2e-2b-v-5); empty on a wallet that has made no
TEX send (no one-time address has ever held a return).
Rescan from an EARLIER birthday to recover funds an over-high restore
birthday skipped (ADR-0534 — the in-app fix for the ADR-0533 "balance reads
zero, sync says done" gap). Rebuilds ONLY the data DB at fromHeight — NO
recovery-phrase re-entry, and NO key material crosses this bridge (the only
argument is a bare block height). A SEALED-KEYCHAIN wallet re-imports from
the seed that stays sealed in the keychain. A HOST-CUSTODIED
(SeedPersistence.none) wallet opened through openWithHostSeed re-imports
through its registered seed port at the FIRST sync after the rescan — the
same pull its first-ever sync made, native-to-native, so the host must have
the seed STAGED for that sync (until it runs, the rebuilt wallet honestly
reads "account not yet provisioned", balance zero, history empty); a port
answering the WRONG seed is refused WalletErrorKind.seedMismatch before a
note is derived, and the next sync asks again. A host-custodied wallet with
NO seed port registered is a typed up-front WalletErrorKind.seedRequired
(nothing can serve the re-import). Pass null to scan the FULL history
(floors to Sapling activation, a slower but money-SAFE scan, NEVER ~tip —
except a freshly-generated wallet, whose durable creation stamp re-floors
null at ~creation: money-equivalent and hours faster; pass an explicit
height to go deeper). Pair with estimateBirthday for a date-picker UX.
RETRY a paused parked send the host listed via listParkedSends — resume the SAME queued
intent after the wallet gave up auto-retrying it (see `ParkedSend::paused`
(crate::api::state::ParkedSend)). The wallet caps auto-retries of a one-time-address send
because every retry permanently uses up one of a small number of address slots; when the
user believes conditions changed (back online, the recipient service reachable again),
THIS is the resume path — NOT cancel + re-enter, which re-pays the retry budget on a fresh
send and MUST NOT be offered while the paused row exists (double-pay + cap evasion). Pass
BOTH the id AND the createdAt from the `ParkedSend`. Returns true if the send was
re-armed (it attempts again on the next background pass — not instantly); false if it
was already gone / began sending / its rowid was reused / it had nothing to reset — an
untouched (never-retried) row is refused, never silently refilled (idempotent — re-read
listParkedSends). MONEY-SAFE: this only re-arms the existing queued intent's retry
budget — nothing is signed or sent by the call itself. §5.4: ids/timestamps only. Throws
WalletErrorKind.invalidState on a closed handle.
Reveal this wallet's BIP39 recovery phrase — word by word, in index order —
so the user can back it up. The ONE sanctioned outbound key-material crossing
(spec §3.3). The words ARE the wallet's whole secret: show them ONCE on a
secure screen the host marks FLAG_SECURE, never log / persist / screenshot /
transmit them, and prompt the user to write them down offline. Returns the
words as a Dart List<String> (24 for a generated wallet; a restored phrase
reveals its own length) — Dart memory cannot be zeroized (the documented §10
exposure); the Rust side holds the words in zeroizing buffers and wipes its
own copies when this call returns.
Sign + broadcast a prepared proposal — the SECOND half of the send flow (inc-2d-ffi).
proposalId is the opaque one-shot token from the SendProposalpropose returned;
pass back ONLY that id — the host cannot mutate what gets signed (the retained proposal
is the sole source of truth, so a tampered id is just a registry miss, never a
wrong-amount sign). The transient spending key is derived, used to sign inside a
blocking proving task, and zeroized — it NEVER crosses this bridge. Each resulting tx
is PERSISTED before broadcast (§6.3), then broadcast over a FRESH per-tx Tor circuit
(§2.3) after a §5.3 jitter.
The on-resume cold snapshot (spec §3.3): everything a UI needs to
re-render after a background gap — balance, sync status, Tor state,
chain tip, and a monotonic seq that also stamps every live stream
item (so a stale stream event can never beat a fresher snapshot in a UI
race: keep the larger seq). Cheap; safe to call on resume before
re-subscribing to the live streams.
Start the background sync loop — required for the live watchSyncStatus
stream (and balance progress) to advance; a freshly opened wallet does
NOT auto-sync. The engine runs over the configured endpoint/Tor with
retry/backoff + a stuck-sync watchdog, so a fault surfaces as a
SyncStatus.stalled EVENT on the stream, never a thrown error or a dead
stream. Idempotent (a second call while running is a no-op).
Stop the background sync loop and await clean teardown. Idempotent;
durable progress is preserved (the chain is the source of truth), so
stopping loses nothing. Prompt even mid-sync (cooperative cancel ≤ one
batch scan). Use it to pause sync WITHOUT closing the wallet; close
stops sync as part of its own teardown. A no-op on an already-closed
handle (nothing to stop).
#390 — the render-only coverage read for the "Check older swap addresses" sheet:
how far the wallet has already checked, for the coverage line and the
Check/Check-deeper affordance. COUNTS ONLY — no address or index crosses. Read-only,
side-effect-free; safe to poll while the sheet is open. Throws
WalletErrorKind.invalidState on a closed handle.
Execute a quote — register the swap intent with the provider and (for OutOfZec)
queue the §4.4 ZEC deposit send (spec §2.6/§3.3). Pass back the SwapQuoteswapQuote returned; this is LEAST-AUTHORITY-equivalent to the send token even
though it carries the whole DTO: the SDK PEEKS its own durable record by quote.id,
compares the DTO's terms and binding to it field-by-field — a difference is
SwapErrorKind.quoteTermsDiffer with nothing consumed — then claims the record
single-flight; EVERY execution term comes from that record, never this DTO
(ADR-0555), so a tampered depositAddress/amount cannot redirect funds.
Returns the SDK-minted SwapId as an opaque String (quote.id itself, never the
provider's deposit address) — do NOT parse it; pass it to watchSwapStatus as-is.
Idempotent against a double-tap / crash-retry: ONE deposit per quote, ever (§5.4: nothing logged).
The dynamic source-asset list for the IntoZec picker (spec §3.3b D5/L6) — LAZY: the host
calls this when the user opens the IntoZec form (NOT on launch, so an idle user emits no
token traffic). The SDK fetches the provider's /v0/tokens on its OWN circuit (never the
sync circuit), filters to real quotable foreign assets (the ZEC asset itself + any
$0/null-price entry dropped), and returns the picker rows + a freshness flag. On a fetch
fault it serves the LAST good list with SwapTokenList.fresh = false (the host shows a
"couldn't refresh, showing cached" banner — honest degradation), never a blank picker; an
empty fresh list is the honest "no assets available right now." The host maps each
(chain, symbol) to a BUNDLED icon (never CDN-fetched). No token field is logged (§5.4).
Request a bounds-checked swap quote (spec §2.6/§3.3) — the FIRST step of the
on-ramp. request names the direction, the user's EXACT side (the anchor every
returned number is bounds-checked against before anything is signed), the
slippage tolerance, and the direction-specific destination/refund fields. The
returned SwapQuote carries the provider deposit address, the min-out floor, the
ZEC side in zatoshis, and the §2.6 privacy disclosure the host MUST render —
funds NEVER move here. No amount/address is logged (§5.4).
MANUALLY recover funds stranded on wallet-controlled one-time (ephemeral) transparent
addresses into the wallet's own SHIELDED balance — the user-discoverable recovery for the funds
recoverableEphemeralFunds surfaces, PLUS late exchange returns / a 2nd deposit / aggregate dust
the automatic surface cannot see (it raw-enumerates each one-time address's on-chain UTXOs).
MONEY-MOVING: it signs + broadcasts one consolidating transaction per funded address (each over
its own circuit, never co-spent — so a recovery never links the one-time-address set on-chain).
Probe, then switch the wallet onto choice and REMEMBER it (the row
survives restarts and rescans; default forgets it). The session is
swapped in place: the sync loop is stopped and joined, the choice
written, the wallet rebuilt over the SAME database — no rescan, no
re-download; funds, history, queued sends and the sync verdict are
untouched. Afterwards call startSync, and re-subscribe the live
streams the swap ends — the old session's watchSyncStatus stream
ENDS (the reference UI swaps its whole session, as after a rescan).
watchTorState and watchIncomingFunds are the exceptions: their
channels are CARRIED into the rebuilt session, so those subscriptions
keep delivering and start reporting the NEW session at once. A
transport chip rendered from watchTorState must not go silent here,
because the rebuilt session is exactly where the private path can
change. The swap service is re-initialised: call enableNearSwap
again if you use it.
Sync for at most budgetMs milliseconds, then report how far the
wallet got — for a host that holds the process only briefly (a
background wake, a pull-to-refresh). ONE pass runs; when the budget
runs out it stops the way stopSync stops the loop (progress kept,
nothing half-written) and the call returns a report with
finished: false, never an error. The budget is always yours: the SDK
has no default and schedules nothing.
The sync servers this host OFFERS (WalletConfig.syncServers, as
validated) — WITHOUT their keys: authValue is always null on this
list (the host supplied it and holds it); authHeader says whether an
entry is gated. The picker, P3-13.
WalletErrorKind.invalidState if the handle is closed.
Which server the wallet dials, which remembered choice produced it,
and whether a fallback is in force. Render the HOST of effectiveUrl
on the sync sheet's Server row — the connection's own truth, so the
display and the dial can never drift. Readable in every lifecycle
phase. WalletErrorKind.invalidState if the handle is closed.
The wallet's transaction history (FR-1 — the activity-history list), newest
first and paginated by an OPAQUE keyset cursor. Pending (unmined) rows sort
ABOVE confirmed ones; after = None is the first page, after = Some(token)
the page after the cursor (the `HistoryPage::next_cursor` from the previous
call — passed back VERBATIM; never parse it); limit caps the page. The keyset
cursor (full (height, tx_index, txid) sort key) means a page boundary never
drops a same-height row nor strands the confirmed history behind a full page of
pending txs. Each row is a typed TxSummary (txid hex in block-explorer
order, SIGNED net amount, status, fee, memo flag) — no key material, amounts
stay typed. Reads librustzcash's audited v_transactions view off the engine
lock, so it never blocks sync.
Stream incoming-funds events (ADR-0536 — the FR-1 "funds arrived" hook):
ONE replay event first (arrivals already in history strictly above
sinceCursor; a null cursor gets a count-0 baseline that hands you a
cursor to persist), then a live event per scan batch that detected
arrivals, and a memoRefresh nudge when a later enhancement pass changes how
existing rows read — either decrypted tx data landed, or a transaction's chain
status did. Treat memoRefresh as "re-pull", never as "new memo data exists".
The live SwapStatus stream for one in-flight swap (spec §3.3/§7) — the swap
parallel of watchSyncStatus. swapId is the opaque id swapExecute returned.
The stream emits the CURRENT status immediately, then polls at a bounded
exponential cadence (5→60s; §7), coalescing onto each change. It NEVER completes
on a transient provider fault (a stall is retried, never a dead stream) — it
completes only on a TERMINAL status (success/refunded/failed), when the host
cancels the subscription, or when the wallet is closed/swap is hard-killed (§3.5).
The live SyncStatus stream (spec §3.3) — the host's honest sync
indicator. The stream emits the CURRENT status immediately on subscribe
(so re-subscribing after an AppLifecycleState.paused gap re-renders at
once), then every change coalesced latest-wins (a burst never floods the
UI). It NEVER completes on a transient fault — a stall is a
SyncStatus.stalled EVENT, not an error or EOF; it completes only when
the host cancels the subscription or the wallet is closed (a final status
is drained first). Call startSync for it to advance past idle.
The live TorState stream — the transport state as it changes, so a
host renders the private path's truth without polling snapshot. The
stream emits the CURRENT state immediately on subscribe, then every
change coalesced latest-wins: the moment a preferred wallet leaves the
private path (TorState.fellBack — the switch a user should be told
about), and the moment a ready path has carried nothing for a minute
(TorState.unanswered). It NEVER completes on a transient fault; it
completes only when the host cancels the subscription, the wallet is
closed, or rescanFrom rebuilds the session (which ends every live
stream — re-subscribe after a rescan). A switchSyncServer does NOT
end it: the stream is carried across the swap and reports the new
session. Readable in every lifecycle phase, like snapshot's tor.
Create a BRAND-NEW wallet — a fresh 24-word seed is generated in Rust
(OsRng), sealed per config.seedPersistence, and used entirely
Rust-side; the seed NEVER crosses this bridge. The wallet is then
opened and returned ready to sync.
Create a WATCH-ONLY wallet from an exported viewing key (#397, spec
§3.7 D2 / ADR-0538 — the consumer half of exportUfvk). Full read
path (sync, balance, history, receive addresses); every spend-class
surface — send, shield, swap, revealMnemonic, rescan — throws the
STRUCTURAL WalletErrorKind.watchOnly (nothing to retry: this wallet
holds no spending keys, by design). Reopen later with the plain
open; a pre-#397 binary refuses the store typed instead of
half-opening it.
Provision a NEW host-custodied-seed wallet at config.dbDir (FR-15), pulling
the seed from the C-ABI seed port the HOST registered (in its own native
library) via zec_wallet_register_seed_port. The seed crosses
native-to-native from the host lib into this one and NEVER through Dart — use
this INSTEAD of createGenerated/restore for the keys-in-host integration
(Relim ADR-0031): there is no mnemonic and no Dart-side seed. The SDK PINS
config.seedPersistence to none (a host-custodied seed is never sealed at
rest) — any other value is enforced to none, not an error.
The PRODUCTION custody disclosure (FR-14 H1) for the wallet at
config.dbDir — render an HONEST pre-wipe confirmation (a hardware-held
key deleted vs best-effort removal) and an ambient custody badge from it.
An ACTIVE probe of the measured vault tier; reads NO seed and unseals
nothing. A Self-less static so it works before/independent of an open
handle. VaultAbsent (headless desktop) ⇒ tier: "none",
eraseAssurance: bestEffort.
Open an EXISTING host-custodied-seed wallet at config.dbDir (FR-15),
attaching the registered C-ABI seed port so signing + fresh-refund-address
derivation can pull the seed on demand. For a wallet whose account is ALREADY
imported (the steady state), balance, history, and the receive address need
NO seed and never call the port; the host stages narrowly and clears per the
FR-12 contract (take-once for a send). This only transports the pull
native-to-native, never through Dart.
Restore an existing wallet from its BIP39 recovery phrase — the ONE
sanctioned INBOUND key-material crossing (spec §3.3; the outbound
counterpart is revealMnemonic). mnemonicWords are
the recovery words in order (a List<String>, the symmetric counterpart to
revealMnemonic — never a single joined string in Dart). Pass each word
LOWERCASED (and trimmed): the audited BIP39 validator does NOT case-fold, so
a soft keyboard's autocapitalized first word would otherwise reject a
CORRECT backup. A mis-cased/unknown word is a typed WalletErrorKind.invalidMnemonic
carrying its INDEX (never silently a different wallet); inter-word and
surrounding whitespace IS tolerated. They are
BIP39-validated (checksum + word list) and turned into a sealed seed
ENTIRELY Rust-side, then the wallet is provisioned at config.dbDir and
opened. The words live in Dart memory only as long as the restore screen
holds them — Dart memory cannot be zeroized (the documented §10 inbound
exposure, identical framing to the outbound revealMnemonic). Once Rust
owns the words (as a SeedSource), the seed + phrase ride zeroizing
buffers and wipe on drop; the brief inbound word-list copy itself is
un-wiped (FRB cannot marshal a zeroizing type) — the same minimal,
documented residue as the outbound reveal, minimised not eliminated.
The duress force-sever (FR-53): make the wallet at config.dbDir
unrecoverable within deadlineMs, EVEN WHILE something still holds it
open (a handle never closed, a straggler past close, a rescan or
server switch mid-rebuild, an open in flight, another process) — the
case where wipe refuses with WalletErrorKind.walletOpen.
Cheap probe: is a COMPLETED, openable wallet provisioned at
config.dbDir? Drives the host's boot fork — false → offer create,
true → open it (then gate on backup confirmation). It is LOCK-FREE
and does NOT open the wallet: it reads only the on-disk provisioning
marker, so it never contends the single-writer lock the subsequent open
needs (an open-and-catch-notFound would, and then break that open).
Delete the wallet at config.dbDir (FR-14) — the host's "delete wallet" /
panic-wipe primitive. Deletes the keychain wrap key FIRST (so on any crash
point this key store can no longer open the on-disk seals or the DB
ciphertext; how strong that is is custodyDisclosure's eraseAssurance,
ADR-0571), then removes the data directory. The seed NEVER crosses this bridge — only the
config does.
Force-complete a wipe whose keychain custody item is ALREADY GONE — the
escape hatch for the rare states where a plain wipe returns
WalletErrorKind.keystoreInconsistent (a manually-deleted item, a
pre-namespacing bare item, an iOS ThisDeviceOnly item that didn't migrate
while dbDir did). It SKIPS the verify-real-sever guard, so the caller MUST
be certain config matches the wallet's create/open config — a wrong config
under force would delete the wrong files. Use ONLY after a plain wipe
returned keystoreInconsistent on a wallet you are sure is yours.