prepare method
Propose the shield (the FIRST half) — DETERMINISTIC, LOCAL (no keys/proofs/
network). On a shieldable balance → ShieldReady with the confirmable
numbers; on null → ShieldNothingToShield (below the threshold, honest
no-op); on a typed failure → ShieldUnavailable.
Implementation
Future<void> prepare() async {
if (_inFlight) return; // re-entrancy: ignore taps while a step runs
final session = _requireSession();
if (session == null) return;
// Synchronous transient transition BEFORE the first await — the double-tap
// interlock (a second prepare sees `_inFlight` and no-ops). NON-const: the
// post-await guards match on INSTANCE IDENTITY (#330) — a canonicalized
// const would alias a different build cycle's transient.
// ignore: prefer_const_constructors
final preparing = ShieldPreparing();
_set(preparing);
try {
// The SAME honest-degradation timeout the receive-address providers use
// (`walletFfiWedgeTimeout`): `proposeShield` is a LOCAL, no-network DB
// read on the blocking pool, so a hang means the FFI boundary is WEDGED — not a
// slow network. Without this the sheet would spin on "Preparing…" forever on a
// money surface with no escape. A `TimeoutException` is not a `WalletApiError`,
// so it routes through the catch to `ShieldUnavailable(couldNotPrepare)` (with a
// "Try again"), exactly like the receive flow's wedged-load handling.
final proposal = await session.proposeShield().timeout(
walletFfiWedgeTimeout,
);
if (_disposed || !identical(state, preparing)) return;
_set(
proposal == null
? const ShieldNothingToShield()
: ShieldReady(proposal),
);
} catch (error) {
if (_disposed || !identical(state, preparing)) return;
_set(classifyShieldPrepareFailure(error));
}
}