summarizeSendOutcome function

SendOutcome summarizeSendOutcome(
  1. List<TxSubmitResult> results, {
  2. required bool isTwoStepTex,
  3. required Map<String, DeliveryState?> delivery,
})

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);
}