fix(send): treat a lost send reply as "outcome unknown", not a plain failure, to prevent a double-send
Resilience audit, highest-value finding: a z_sendmany (full node) or lite broadcast whose REPLY is
lost — a transport timeout / connection cut AFTER the request already left us — was reported as an
ordinary "Transaction failed". The user then retries and can broadcast the same payment twice, because
the daemon may already hold the operation (z_sendmany returns an opid and runs async).
Classify the ambiguous case and surface it distinctly, without touching the send itself:
- New pure helpers in rpc/rpc_failure.h: sendReplyMayBeLost() (an RpcTransportError that was requestSent
with no HTTP status and a non-OK curl code — reply-timeout / got-nothing / our-own-abort; definitive
rejections and any HTTP status are excluded), liteSendErrorMayBeInFlight() (a transport-style gRPC text
heuristic, vetoing connect-phase and server-rejection text), and a readable-English marker tag.
- app_network.cpp: the z_sendmany catch records the flag; the failure branch (non-background only) tags
the callback text and forces a wallet re-read so a committed send shows in History. onDisconnected()
tags already-pending send callbacks the same way (they exist only after an opid was returned).
- Lite UserSend (app.cpp): a transport-style broadcast error is tagged identically.
- Send tab: a tagged outcome shows a non-success warning pointing at History and clears the compose form
so any resend must be retyped deliberately; it is NOT presented as a retryable failure.
The actual rpc_->call("z_sendmany", {from, recipients, 1, fee}), the lite send/broadcast call, and all
amounts/fees/recipients/memo bytes are byte-identical; only result-handling and presentation change.
Pure classifier/marker regression tests added (testSendMaybeInFlight).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/text_format.h"
|
||||
#include "../../util/amount_format.h" // toSats / canAfford — integer-satoshi affordability check
|
||||
#include "../../rpc/rpc_failure.h" // isSendMaybeInFlight — lost-reply (outcome unknown) send tag
|
||||
#include "../notifications.h"
|
||||
#include "../layout.h"
|
||||
#include "contact_picker.h"
|
||||
@@ -955,6 +956,18 @@ void RenderSendConfirmPopup(App* app) {
|
||||
s_amount = 0.0;
|
||||
s_memo[0] = '\0';
|
||||
s_send_max = false;
|
||||
} else if (rpc::isSendMaybeInFlight(result)) {
|
||||
// Outcome unknown: the reply was lost after the request left. Do NOT present a
|
||||
// plain failure (it invites a retry/double-send). Warn, point at History, and
|
||||
// clear the form so a resend has to be retyped deliberately.
|
||||
s_tx_status = std::string(TR("send_maybe_sent")) + "\n\n" + rpc::stripSendMaybeInFlight(result);
|
||||
s_result_txid.clear();
|
||||
s_status_success = false;
|
||||
Notifications::instance().warning(TR("send_maybe_sent"), 15.0f);
|
||||
s_to_address[0] = '\0';
|
||||
s_amount = 0.0;
|
||||
s_memo[0] = '\0';
|
||||
s_send_max = false;
|
||||
} else {
|
||||
// Show a friendly cause in the toast; keep the raw backend text below it in
|
||||
// the overlay (copyable) so the detail isn't lost for diagnostics.
|
||||
|
||||
Reference in New Issue
Block a user