classifyProposeFailure function

SendFormFault classifyProposeFailure(
  1. Object error
)

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.

Implementation

SendFormFault classifyProposeFailure(Object error) {
  if (error is WalletApiError) {
    return switch (error.kind) {
      WalletErrorKind_InsufficientFunds(
        :final availableZat,
        :final requiredZat,
        :final pendingIncomingZat,
      ) =>
        SendInsufficientFunds(
          availableZat: availableZat,
          requiredZat: requiredZat,
          pendingIncomingZat: pendingIncomingZat,
        ),
      WalletErrorKind_AddressInvalid() => const SendCategoricalFault(
        SendFaultReason.addressInvalid,
      ),
      WalletErrorKind_MemoRequiresShieldedRecipient() =>
        const SendCategoricalFault(SendFaultReason.memoToTransparent),
      WalletErrorKind_MemoTooLong() => const SendCategoricalFault(
        SendFaultReason.memoTooLong,
      ),
      WalletErrorKind_ReservedMemoNotSendable() ||
      WalletErrorKind_MemoInvalid() => const SendCategoricalFault(
        SendFaultReason.memoNotSendable,
      ),
      // Its OWN arm, not folded into memoNotSendable: a both-memos refusal is
      // a caller bug and its copy must not blame the user's memo.
      WalletErrorKind_MemoConflict() => const SendCategoricalFault(
        SendFaultReason.memoConflict,
      ),
      WalletErrorKind_AmountOutOfRange() => const SendCategoricalFault(
        SendFaultReason.amountOutOfRange,
      ),
      WalletErrorKind_NetworkMismatch() => const SendCategoricalFault(
        SendFaultReason.networkMismatch,
      ),
      // A WATCH-ONLY wallet refuses every spend at the SDK entry (RW-VIEW-001).
      // Defense-in-depth (security-N2): the chrome hides Send and the send
      // screen re-gates on the watch-only kind, so this is unreachable on the
      // normal path — but a deep-link / race that DID reach propose gets the
      // honest "this wallet can't send", never the generic couldNotPrepare.
      WalletErrorKind_WatchOnly() => const SendCategoricalFault(
        SendFaultReason.watchOnly,
      ),
      WalletErrorKind_PaymentUriInvalid() => const SendCategoricalFault(
        SendFaultReason.uriInvalid,
      ),
      // A not-yet-anchorable wallet (TTL/sync) is the offline-first fork's
      // trigger — the host offers "queue to send later".
      WalletErrorKind_ProposalStale() => const SendCategoricalFault(
        SendFaultReason.notSyncedYet,
      ),
      // Ironwood/NU6.3: the network runs consensus rules this version cannot
      // build for. Its OWN arm — folding it into notSyncedYet would offer
      // "queue to send later" over a send that cannot go on this version at
      // all, which is worse than an error: it invites the user to wait.
      WalletErrorKind_NetworkUpgradeUnsupported() => const SendCategoricalFault(
        SendFaultReason.networkUpgradeUnsupported,
      ),
      // GRACE-1 (§4p G-1/G-6): the server will not say which network it is on
      // and the grace ran out — its OWN fault, with the SDK's reason, so the
      // copy says "switch servers" (or "check the device's date and time"),
      // never the update-the-app sentence above.
      WalletErrorKind_ConsensusGraceExpired(
        :final by,
        :final blocksSinceLastCurrent,
      ) =>
        SendServerSilentFault(
          by: by,
          blocksSinceLastCurrent: blocksSinceLastCurrent,
        ),
      // And "never checked" is the not-synced-yet story (§4p item 2): the first
      // completed pass resolves it, and the queue is a fine answer meanwhile.
      WalletErrorKind_ConsensusNotEvaluated() => const SendCategoricalFault(
        SendFaultReason.notSyncedYet,
      ),
      // storeBusy joins the lifecycle-busy arm (W-swap-4-a-4): both are honest
      // "try again in a moment" transients — the store one means a write lost
      // its race to a sync commit even past the SDK's in-Rust bounded retry;
      // nothing was written, so retrying IS the remedy (never the recovery
      // journey a storeCorrupt would earn).
      WalletErrorKind_WalletBusy() ||
      WalletErrorKind_InvalidState() ||
      WalletErrorKind_StoreBusy() => const SendCategoricalFault(
        SendFaultReason.walletBusy,
      ),
      // Out of disk persisting the compose (#373): the honest "free up space",
      // not a generic "couldn't prepare" that retries into a deterministic
      // re-fail on a tight disk. Nothing was written — funds untouched.
      WalletErrorKind_DiskFull() => const SendCategoricalFault(
        SendFaultReason.storageFull,
      ),
      // A zero-valued transparent output is structurally unreachable from the form
      // (the host blocks a non-positive amount before compose). Map it EXPLICITLY
      // to the honest generic rather than silently to the wildcard — a future
      // QR/URI path that composed one would still get a true "couldn't prepare",
      // never a wrong "larger than total supply". (A dedicated reason can land
      // with the QR path.)
      WalletErrorKind_ZeroValuedTransparentOutput() =>
        const SendCategoricalFault(SendFaultReason.couldNotPrepare),
      // INC-018 (b), phase-2 P2-2: the retryable class the SDK now types apart —
      // its OWN arm, so the copy says "try again in a moment" and never sends the
      // user to correct details that are correct. Its
      // sibling `proposeFailed` stays on the generic arm below: that class is
      // deterministic on the input, and "check the details" is its honest copy.
      WalletErrorKind_ProposeTransient() => const SendCategoricalFault(
        SendFaultReason.couldNotPrepareTransient,
      ),
      // sendAmountRequired never reaches here (the form always sets an amount);
      // proposeFailed / anything else → generic.
      _ => const SendCategoricalFault(SendFaultReason.couldNotPrepare),
    };
  }
  // The controller's own bound ran out (`walletFfiWedgeTimeout`). Propose is
  // local work that queues behind the scan's db lock, so on a wallet catching
  // up this is the WAIT, not the input (the Seeker walk saw a 12–14 s
  // first address behind the same lock). "Check the details" would send the
  // user to correct details that are correct — the INC-018 (b) mistake.
  if (error is TimeoutException) {
    return const SendCategoricalFault(SendFaultReason.couldNotPrepareTransient);
  }
  return const SendCategoricalFault(SendFaultReason.couldNotPrepare);
}