prepare method
Future<void>
prepare({
- required String address,
- required String amountText,
- String? memo,
- WalletMachineMemo? machineMemo,
Compose the ZIP-321 URI from the form and propose (the FIRST half). On success → SendReview with the confirmable numbers; on a typed failure → back to SendForm with an honest inline fault. The amount is parsed host-side first (integer-exact, never via double).
Implementation
Future<void> prepare({
required String address,
required String amountText,
String? memo,
WalletMachineMemo? machineMemo,
}) async {
if (_inFlight) return; // re-entrancy: ignore taps while a step runs
final session = _requireSession();
if (session == null) return;
final parsed = parseZecAmount(amountText);
if (parsed is ZecAmountInvalid) {
_set(SendForm(fault: SendAmountFault(parsed.fault)));
return;
}
final zat = (parsed as ZecAmountValid).zat;
// The host's policy ceiling (e.g. an alpha cap) — refused BEFORE any
// bridge call, like the parse gate above (`null` = no ceiling).
final ceiling = ref.read(walletSendCeilingZatProvider);
if (ceiling != null && zat > ceiling) {
_set(SendForm(fault: SendOverCeiling(ceiling)));
return;
}
final recipient = address.trim();
// Synchronous transient transition BEFORE the first await — the double-tap
// interlock (a second prepare sees `_inFlight` and no-ops). Deliberately
// NON-const: the post-await guards match on INSTANCE IDENTITY (#330) — a
// canonicalized const would alias a different build cycle's transient, so
// a session flip + fresh prepare could let the DEAD cycle's continuation
// write its stale proposal over the live one.
// ignore: prefer_const_constructors
final preparing = SendPreparing();
_set(preparing);
final String uri;
try {
// Compose through the port (the bridge crossing lives in the adapter); a
// bad address / un-sendable memo throws a typed error here, synchronously.
uri = session.composePaymentUri(
recipient: recipient,
amountZat: zat,
memoText: memo,
// FR-28: the host's opaque bytes ride EVERY compose, not just the
// first. The form re-composes from its own fields on each Review tap,
// which is exactly where the bytes used to be dropped silently.
memoBytes: machineMemo?.bytes,
);
} catch (error) {
_set(SendForm(fault: classifyProposeFailure(error)));
return;
}
try {
// The same honest-degradation timeout the shield prepare uses (
// wrap review): propose is a LOCAL, no-network call, so a hang means
// the FFI boundary is WEDGED — and with the entry resets now in-flight
// no-ops (re-attachment, review F1) a wedged Preparing would otherwise
// be a permanently stuck money screen. TimeoutException classifies to
// couldNotPrepareTransient: a wallet still scanning, not bad input.
final proposal = await session
.propose(uri)
.timeout(walletFfiWedgeTimeout);
if (_disposed || !identical(state, preparing)) return;
_set(
SendReview(
proposal: proposal,
recipient: recipient,
// FR-28: the purpose travels to the REVIEW so the disclosure and the
// host's authorization prompt read the same sentence from one place.
machineMemoPurpose: machineMemo?.purpose,
),
);
} catch (error) {
if (_disposed || !identical(state, preparing)) return;
_set(SendForm(fault: classifyProposeFailure(error)));
}
}