summarizeSendOutcome function
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.
delivery is the core's per-transaction delivery reading for the txids that
did not come back accepted (see deliveryStatesFor) — stage S8
obligation, row 10: SendSavedForRetry is produced ONLY when every such
transaction reads DeliveryState.retryPending (or is already
DeliveryState.accepted / DeliveryState.confirmed — the wallet's own
background pass landed it between the send and this read, the core #307
race shape, now readable); when EVERY one already reads accepted or
confirmed the payment is out and the outcome is SendSucceeded. Anything
else — a held DeliveryState.persisted, null (no reading, or the read
failed), a txid the map does not know, a result arm that carries no txid —
is SendKept: saved, no promise. An empty list (a proposal always mints
≥1 tx — defensive) has nothing the core could retry, so it reads SendKept
0/0, never a promise over nothing.
isTwoStepTex is the proposal's SSOT shape signal (SendProposal.isTwoStepTex,
2e-2b-v-5a). A two-step TEX with ANY leg accepted (but not all) is
SendTexInMotion, NOT SendSavedForRetry — the legs are sequenced, and an
accepted leg means the endpoint knows tx0 (an honest endpoint can only accept
tx1 if it does), so the funds are in motion on a wallet-controlled one-time
address and the copy must not over-promise auto-completion. A zero-broadcast
two-step put nothing in motion, so it takes the delivery-decided arms above.
Pure + total — unit-tested at its boundary without a device.
Implementation
SendOutcome summarizeSendOutcome(
List<TxSubmitResult> results, {
required bool isTwoStepTex,
required Map<String, DeliveryState?> delivery,
}) {
final total = results.length;
if (total == 0) return const SendKept(broadcast: 0, total: 0);
final broadcast = results.whereType<TxSubmitResult_Success>().length;
if (broadcast == total) return SendSucceeded(total);
if (isTwoStepTex && broadcast > 0) {
return SendTexInMotion(broadcast: broadcast, total: total);
}
var allOut = true;
for (final result in results) {
if (result is TxSubmitResult_Success) continue;
final txid = _txidOf(result);
final state = txid == null ? null : delivery[txid];
switch (state) {
case DeliveryState.accepted:
case DeliveryState.confirmed:
break;
case DeliveryState.retryPending:
allOut = false;
case DeliveryState.persisted:
case DeliveryState.unknown:
case null:
return SendKept(broadcast: broadcast, total: total);
}
}
if (allOut) return SendSucceeded(total);
return SendSavedForRetry(broadcast: broadcast, total: total);
}