features/wallet/send/send_state library

Classes

RecipientEmpty
Field is blank — the neutral first state (no message, no premature warning).
RecipientInvalid
The address doesn't parse for this network — not sendable.
RecipientShielded
A shielded recipient: PRIVATE and memo-capable ("Shielded · private").
RecipientStatus
The LIVE classification of the recipient field — the §5.1 privacy axis (shielded/private vs transparent/public) plus the validity the form gates the Review action on. A sealed family so the screen renders each state exhaustively (no default blind spot). Computed SYNCHRONOUSLY from WalletSession.validateRecipient (local, no network — feedback even fully offline) on every keystroke; carries NO money state (design invariant 1).
RecipientTransparent
A transparent / transparent-only recipient: PUBLIC on-chain and memo-incapable ("Transparent · public"). Honest and allowed — the §5.1 de-shield disclosure warns at Review; the send is never blocked for it.
RecipientWrongNetwork
Well-formed but for a DIFFERENT network (e.g. a testnet address pasted into a mainnet wallet) — a distinct, renderable state (not "malformed").
SendAlreadySubmitted
The one-shot token was already consumed (a double-tap / re-entry). The notes are NEVER broadcast twice (§6.3) — honest "already submitted", not an error.
SendAmountFault
Host-side amount parse failure (before any bridge call) — see ZecAmountFault.
SendCategoricalFault
A categorical fault with no payload — rendered from reason.
SendFlowIndeterminate
The flow ENTERED the spend and then lost the answer — the sign+broadcast ran and something threw after it (a host authorizer's own bookkeeping, an SDK error raised past the point of no return). Money MAY have moved and this layer cannot tell.
SendFlowNothingCreated
The flow ended with NOTHING signed and nothing queued — structurally, not by inference: the spend closure was never entered.
SendFlowOutcome
What became of ONE send flow, addressed by flowId — the authoritative answer, published by SendController and consumed by whoever owns that flow's host report.
SendFlowQueued
The flow durably queued an intent offline. No transaction, no txid — but the core's queuedSendId IS the join key onto the parked-send surfaces.
SendFlowSent
The flow reached a send. outcome is the honest reduction; txids are the ids it minted (see sendTxids); isTwoStepTex is the proposal's shape signal, carried because the reducer's own two-step residual cannot be re-derived downstream (core #307). singleRecipientZat is the SENT proposal's one-recipient total (FR-46) — copied from the proposal whose id went to send, never a re-proposed or displayed one; null for two recipients.
SendForm
The editable send form. fault is an inline, honest message from a failed prepare/queue (a bad address, not enough funds, …) — null on first entry.
SendFormFault
A FIXABLE form fault surfaced inline so the user corrects the input on the SAME form (never a full-screen failure that discards what they typed). Reads only the typed WalletApiError.kind (or a host-side parse fault) — never an error payload — so nothing sensitive (address/amount) leaks (§5.4).
SendInsufficientFunds
Not enough spendable value. Carries the audited note-selector's figures (all zatoshis) so the host can say "you have X spendable, need Z, Y still arriving". Never logged (§5.4) — the static message is.
SendKept
The wallet KEPT the signed payment but has not promised to send it on its own: for some transaction that did not go out, the core's per-transaction delivery reading was not DeliveryState.retryPending — a held state, no state at all, or a reading that could not be taken. Distinct from SendSavedForRetry on purpose (stage S8 obligation, row 10): "saved — we'll finish sending" is said ONLY when the core reports that it will retry; this arm says "saved" and points at Activity, where the live delivery state is rendered. Money-safe either way — the bytes are persisted.
SendOutcome
The honest result of a send — a broadcast failure is NOT a lost payment (each tx is persisted and re-sent by the resubmission machinery), so the variants distinguish "went out", "saved for retry", "already done", and "couldn't build" rather than success/error.
SendOutcomeUnknown
The flow ENTERED its spend and then lost the answer (S7 U1): the closure ran — a transaction may be signed and broadcast, or an intent durably queued — and something past that point threw or declined (the host's own authorizer code, an untyped throw). Neither "sent" nor "nothing was sent" is true, so the screen says neither: it points at where the truth is (Activity for a send, the pending payments for a queue) and offers no retry. Deliberately carries NO error — nothing here renders a throw's text.
SendOverCeiling
The amount exceeds the HOST's send ceiling (walletSendCeilingZatProvider — e.g. an alpha roll-out cap). Carries the ceiling so the inline copy can state the limit; refused BEFORE any compose/propose bridge call. Distinct from ZecAmountFault.outOfRange (the protocol supply bound): this is host POLICY, and the copy must say so honestly rather than imply an invalid amount.
SendPreparing
propose in flight — deterministic + local (note-selection/fee), no network. Transient; a spinner.
SendQueued
Terminal result of an offline queueSend — the intent is durably stored and will send on the next online sync.
SendQueuing
queueSend in flight (durably persist the intent). Transient; a spinner.
SendRequestExpired
The host's request this flow was opened with can no longer spend (stage S8 deadline, R05): the entry's mount grace ran out before a send screen appeared, the host was told "no transaction", and that answer is final for the request. Nothing to pay, nothing to retry — the screen states the fact and the user starts again from the host.
SendReview
The confirm screen: show the EXACT numbers + the §5.1 de-shield disclosure, then SendController.confirm signs and broadcasts. Holds the display proposal (its opaque token is consumed by id on confirm) and the recipient the user typed, echoed back so they verify WHO they're paying.
SendSavedForRetry
Some (or none) of the txs reached the network. Each is PERSISTED and will be re-broadcast on the next sync — money-safe, the user already confirmed the numbers. The honest "saved, will finish sending when you're back online".
SendSent
Terminal result of a send — see SendOutcome for the honest variants.
SendServerSilentFault
The server will not say which network it is on and the wallet's grace for such a server has run out — RW-SYNC-003, GRACE-1 (§4p). Carries the SDK's reason (blocks / the device clock / never confirmed) and, for the blocks sentence, the count it names. Its OWN class, not a SendFaultReason: the copy needs the payload, and it must never fold into SendFaultReason.networkUpgradeUnsupported — nothing was upgraded, an update fixes nothing, and the next step is "switch servers" (or "check the device's date and time"). Never logged; the static message is.
SendSignFailed
Build/sign failed — no money moved. "Couldn't complete this payment; nothing was sent." The user re-proposes (the consumed token can't be re-sent).
SendState
The send flow's states (inc-2d-ui), a sealed family so the screen renders each phase with an exhaustive switch (no default blind spot). The flow is form → (propose) → review → (send) → result, with an offline-first queue branch. Rendering layer ONLY — every transition runs through SendController; no money state is stored here (design invariant 1; Rust is the single source of truth). The DTOs it carries (SendProposal) are display projections — the opaque proposal token stays Rust-side.
SendSubmitting
send in flight (sign in a blocking proving task + persist + broadcast). Transient; a spinner. Holds the proposal so a UI can keep showing the figures.
SendSucceeded
Every tx broadcast successfully.
SendTexInMotion
A two-step TEX (ZIP-320) send where SOME but not all legs were accepted — typically tx0 (the unshield to a wallet-controlled one-time address) out but the forwarding tail (tx1) not; since core #307 also the reverse shape, where tx0 read "already known" (the wallet's own background re-broadcast raced the send and landed it first) and tx1 was accepted — either way the endpoint knows tx0. The funds are IN MOTION on a one-time address the wallet controls — money has LEFT the shielded pool but NOT confirmed at the recipient. This is NOT an ordinary "each tx independently saved-for-retry" partial: the two legs are SEQUENCED (tx1 spends tx0's output, so it can only mine after tx0). The honest terminal copy says ONLY the permanently-true things — "on its way" + "don't send it again" + "recover from your wallet if it doesn't complete" — and MUST NOT promise auto-completion: tx1 can expire (~40 blocks, the common mobile case) into a strand that only the shipped recovery (the wallet-screen recover-now / sweep) resolves (spec §3.2i-2 UX-honesty (i); the durable parked/recoverable surface owns the later in-motion→stranded truth and re-notifies). Distinguished from SendSavedForRetry purely by SendProposal.isTwoStepTex (the SSOT shape signal, 2e-2b-v-5a) — only the interactive SEND path produces it; shield (t→z) and move-to-transparent (z→own-t) have no TEX destination, so they never do.

Enums

SendFaultReason
The payload-free form-fault categories (the honest message axis; no codes).

Functions

classifyProposeFailure(Object error) → SendFormFault
Map a propose/encodePaymentUri failure to a SendFormFault. Reads the typed WalletApiError.kind ONLY (never a payload — §5.4); a non-FRB error is the generic SendFaultReason.couldNotPrepare. Pure + total, so it is unit-tested at its boundary without a device.
classifyQueueFailure(Object error) → SendFormFault
Map a queueSend failure to a SendFormFault. Same kind-only discipline. queuedSendsFull is the one queue-specific kind; everything else mirrors classifyProposeFailure's categorical mapping (compose-time errors are identical, since queue composes the same URI).
classifyRecipient(WalletSession session, String raw) → RecipientStatus
Classify the recipient field's current text against the wallet's own network. PURE over the (sync, local) WalletSession.validateRecipient seam — reads only memoCapable or the typed WalletApiError.kind (NEVER an address payload — §5.4), so it is unit-tested at its boundary behind a fake. A blank field is RecipientEmpty (no bridge call); any non-FRB error degrades to RecipientInvalid (honest, never a raw code — invariant 6).
classifySendFailure(Object error) → SendState
Map a send (sign+broadcast) failure to the next SendState. Distinct from the form mappers: a send failure is terminal-ish, so it routes to either the result screen (already-submitted / sign-failed) or back to the form (stale ⇒ re-propose, busy ⇒ retry). Reads kind only (§5.4).
deliveryStatesFor(WalletSession session, List<TxSubmitResult> results) → Future<Map<String, DeliveryState?>>
The core's delivery reading for every transaction in results that did NOT come back accepted, keyed by txid — the summarizeSendOutcome input that makes "saved for retry" a reported fact (stage S8 obligation, row 10) rather than a fallback. A read that fails typed is recorded as null: the reducer then lands on SendKept (saved, no promise) — under-promising is the honest direction when the wallet's answer could not be taken.
landedDeliveryStates(WalletSession session, List<TxSubmitResult> results) → Future<Map<String, DeliveryState?>>
deliveryStatesFor for a spend whose results already LANDED (R13 §4.2): any throw — not only a typed one — reads as "no delivery state", so the landed outcome still shows ("saved", no promise), never "outcome unknown". Unknown means only that the answer was lost; here it was not.
parkedAuthorizeErrorPrecedesPersistence(Object error) → bool
The parked "Send now" twin, for authorizeParkedSend (Wallet::try_sign_queued_intent → drain_multi: create — the wallet-db commit — then mark_sent_multi, then read_raw_tx) (R13 §4.1).
queueErrorPrecedesPersistence(Object error) → bool
The queue twin of sendErrorPrecedesPersistence, for queueSend (Wallet::queue_send_uri → queue_send → intent_store::enqueue): WalletBusy and WatchOnly are the entry gates, and QueuedSendsFull is the cap checked inside the enqueue transaction BEFORE its insert. Every SQLite-classified kind is left out: this layer cannot tell a fault before the insert from one at the commit.
sendErrorPrecedesPersistence(Object error) → bool
Whether error, thrown by the SDK's own send AFTER the spend closure was entered, is a kind the core raises only BEFORE any transaction is persisted (S7 U1 fold) — so classifySendFailure's "nothing was sent" / back-to-form routing is still the truth. Anything else lands on SendOutcomeUnknown.
sendTxids(List<TxSubmitResult> results) → List<String>
The transaction ids in results, in order — every arm that HAS one, not only the accepted ones. A tx that got no verdict (grpcFailure), was rejected from the mempool (submitFailure), or was never attempted still EXISTS: it is signed, persisted, and re-broadcast by the resubmission machinery, so a host must be able to cite it. The forward-compat TxSubmitResult.unknown arm carries no id and contributes none — which is why a caller checks for emptiness rather than assuming one id per result. Pure + total.
singleStepSendErrorPrecedesPersistence(Object error) → bool
The SINGLE-STEP twin of sendErrorPrecedesPersistence, for the shield and move-to-transparent send (R13 §4.1). Same path (Wallet::send_by_id → sign_proposal_upstream → create_signed_core → broadcast_persisted), but never a two-step: a shield is its own arm and a move pays the wallet's own t-address, never a TEX — so mark_sent_multi is unreachable, and the engine writes the transaction ONCE, in one transaction, on full success.
summarizeSendOutcome(List<TxSubmitResult> results, {required bool isTwoStepTex, required Map<String, DeliveryState?> delivery}) → SendOutcome
Reduce the per-tx TxSubmitResult list to an honest SendOutcome. A tx is "out" only on TxSubmitResult_Success; every other arm (grpc/submit failure, not-attempted) means persisted-but-unsent, and whether the wallet will retry it is the CORE's to say, not this reducer's to assume.
walletSendReportFor(SendFlowOutcome flow, {String? correlationId}) → WalletSendReport
The ONE translation from a flow's terminal fact to the host's report (FR-26). Pure + total, so it is unit-tested at its boundary with no widget tree and no device.