walletSendReportFor function
The ONE translation from a flow's terminal fact to the host's report (FR-26). Pure + total, so it is unit-tested at its boundary with no widget tree and no device.
Lives HERE, beside the other reducers, and not in the public report file: the report DTOs are the host's vocabulary and must not depend on this package's internal flow states — the dependency runs internal → public.
Three honesty rules are enforced here rather than trusted to callers:
- The txid list, not the outcome variant, decides whether a transaction exists. An outcome that says "saved for retry" over an EMPTY result set created nothing to cite, and reporting that as a transaction hands the host a payment record it can never reconcile.
- The motion axis is three-valued. A two-step (ZIP-320) send whose legs
ALL came back unaccepted can be the core #307 already-known race — the
wallet's own background pass landed them first and the funds ARE in motion
— and this layer cannot tell. Saying
falsethere invites a host to offer "pay another way" over a payment already on its way. - The recipient amount rides a TAGGED report only (FR-46): set when
correlationIdis non-null, whatever the motion — an untagged push gets nothing new.
Implementation
WalletSendReport walletSendReportFor(
SendFlowOutcome flow, {
String? correlationId,
}) {
switch (flow) {
case SendFlowNothingCreated():
return WalletSendNoTransaction(correlationId: correlationId);
case SendFlowIndeterminate():
return WalletSendUnclassified(correlationId: correlationId);
case SendFlowQueued(:final queuedSendId):
return WalletSendQueuedOffline(
queuedSendId: queuedSendId,
correlationId: correlationId,
);
case SendFlowSent(
:final outcome,
:final txids,
:final isTwoStepTex,
:final singleRecipientZat,
):
switch (outcome) {
case SendAlreadySubmitted():
return WalletSendAlreadySubmitted(correlationId: correlationId);
case SendSignFailed():
return WalletSendNoTransaction(correlationId: correlationId);
case SendSucceeded():
case SendSavedForRetry():
case SendKept():
case SendTexInMotion():
if (txids.isEmpty) {
return WalletSendUnclassified(correlationId: correlationId);
}
final broadcast = switch (outcome) {
SendSucceeded(:final txCount) => txCount,
SendSavedForRetry(:final broadcast) => broadcast,
SendKept(:final broadcast) => broadcast,
SendTexInMotion(:final broadcast) => broadcast,
_ => 0,
};
return WalletSendTransactionCreated(
txids: txids,
broadcastCount: broadcast,
motion: switch (outcome) {
SendTexInMotion() => WalletSendMotion.inMotion,
// The #307 residual: a two-step with NOTHING accepted is either
// "nothing went out" or "the background pass already landed both".
// Indistinguishable here, so say so.
_ when isTwoStepTex && broadcast == 0 =>
WalletSendMotion.indeterminate,
_ => WalletSendMotion.notInMotion,
},
// FR-46: TAGGED reports only — the tag is what makes the host's
// join honest; an untagged push gets nothing new (ADR-0558's
// one-sided-tag rule).
recipientAmountZat: correlationId == null
? null
: singleRecipientZat,
correlationId: correlationId,
);
}
}
}