Skip to content

Commit 5c04712

Browse files
grunchclaude
andcommitted
fix(add-bond-invoice): only treat wait timeout as a successful submission
Previously any error from wait_for_dm was reported as "invoice submitted", so a subscribe/sign/transport failure where the reply never went out was misreported as success. Introduce a distinguishable WaitForDmTimeout error and only show the success message on that timeout (the genuine no-reply happy path), propagating every other error. Also drop the unnecessary Option wrapper around the payload, which is always Some by the time it is used (invalid invoices return early). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a29e90f commit 5c04712

3 files changed

Lines changed: 41 additions & 16 deletions

File tree

src/cli/add_bond_invoice.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::parser::common::{
22
create_emoji_field_row, create_field_value_header, create_standard_table,
33
};
4-
use crate::util::{print_dm_events, send_dm, wait_for_dm};
4+
use crate::util::{print_dm_events, send_dm, wait_for_dm, WaitForDmTimeout};
55
use crate::{cli::Context, db::Order, lightning::is_valid_invoice};
66
use anyhow::Result;
77
use lnurl::lightning_address::LightningAddress;
@@ -53,10 +53,10 @@ pub async fn execute_add_bond_invoice(order_id: &Uuid, invoice: &str, ctx: &Cont
5353
// Parse invoice (Lightning address or BOLT11) and build payload
5454
let ln_addr = LightningAddress::from_str(invoice);
5555
let payload = if ln_addr.is_ok() {
56-
Some(Payload::PaymentRequest(None, invoice.to_string(), None))
56+
Payload::PaymentRequest(None, invoice.to_string(), None)
5757
} else {
5858
match is_valid_invoice(invoice) {
59-
Ok(i) => Some(Payload::PaymentRequest(None, i.to_string(), None)),
59+
Ok(i) => Payload::PaymentRequest(None, i.to_string(), None),
6060
Err(e) => {
6161
return Err(anyhow::anyhow!("Invalid invoice: {}", e));
6262
}
@@ -71,7 +71,7 @@ pub async fn execute_add_bond_invoice(order_id: &Uuid, invoice: &str, ctx: &Cont
7171
Some(request_id),
7272
None,
7373
Action::AddBondInvoice,
74-
payload,
74+
Some(payload),
7575
);
7676

7777
// Serialize the message
@@ -91,18 +91,21 @@ pub async fn execute_add_bond_invoice(order_id: &Uuid, invoice: &str, ctx: &Cont
9191
);
9292

9393
// Wait for a possible reply. On success Mostro pays the invoice from its
94-
// wallet without acknowledging over Nostr, so a timeout here is the happy
94+
// wallet without acknowledging over Nostr, so a *timeout* here is the happy
9595
// path; Mostro only answers with `cant-do` on failure (late reply, wrong
96-
// sender, bad invoice, etc.).
96+
// sender, bad invoice, etc.). Any other error (subscribe/sign/transport)
97+
// means the reply may never have been sent — surface it instead of
98+
// misreporting it as success.
9799
match wait_for_dm(ctx, Some(&order_trade_keys), sent_message).await {
98100
Ok(recv_event) => {
99101
print_dm_events(recv_event, request_id, ctx, Some(&order_trade_keys)).await?;
100102
}
101-
Err(_) => {
103+
Err(e) if e.downcast_ref::<WaitForDmTimeout>().is_some() => {
102104
println!("✅ Bond payout invoice submitted to Mostro.");
103105
println!("💡 Mostro will pay it from its wallet; no further confirmation is sent.");
104106
println!("💡 Run `get-dm` to check for a `cant-do` response in case of an error.");
105107
}
108+
Err(e) => return Err(e),
106109
}
107110

108111
Ok(())

src/util/messaging.rs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,25 @@ pub async fn send_plain_text_dm(
256256
.await
257257
}
258258

259+
/// Distinguishable error returned by [`wait_for_dm`] when no reply arrives
260+
/// within [`FETCH_EVENTS_TIMEOUT`].
261+
///
262+
/// Most callers `?`-propagate it like any other error, but flows where "no
263+
/// reply" is the happy path (e.g. `add-bond-invoice`, where Mostro pays the
264+
/// invoice without acking over Nostr) can detect it via
265+
/// `downcast_ref::<WaitForDmTimeout>()` and avoid misreporting genuine
266+
/// subscribe/send/transport failures as success.
267+
#[derive(Debug)]
268+
pub struct WaitForDmTimeout;
269+
270+
impl std::fmt::Display for WaitForDmTimeout {
271+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272+
write!(f, "Timeout waiting for DM or gift wrap event")
273+
}
274+
}
275+
276+
impl std::error::Error for WaitForDmTimeout {}
277+
259278
pub async fn wait_for_dm<F>(
260279
ctx: &crate::cli::Context,
261280
order_trade_keys: Option<&Keys>,
@@ -278,23 +297,25 @@ where
278297
sent_message.await?;
279298

280299
// Wait for the DM or gift wrap event
281-
let event = tokio::time::timeout(super::events::FETCH_EVENTS_TIMEOUT, async move {
300+
let waited = tokio::time::timeout(super::events::FETCH_EVENTS_TIMEOUT, async move {
282301
loop {
283302
match notifications.recv().await {
284-
Ok(notification) => match notification {
285-
RelayPoolNotification::Event { event, .. } => {
286-
return Ok(*event);
287-
}
288-
_ => continue,
289-
},
303+
Ok(RelayPoolNotification::Event { event, .. }) => return Ok(*event),
304+
Ok(_) => continue,
290305
Err(e) => {
291306
return Err(anyhow::anyhow!("Error receiving notification: {:?}", e));
292307
}
293308
}
294309
}
295310
})
296-
.await?
297-
.map_err(|_| anyhow::anyhow!("Timeout waiting for DM or gift wrap event"))?;
311+
.await;
312+
313+
// Keep a genuine timeout (the only "no reply" outcome) distinguishable from
314+
// a notification-channel error so callers can treat them differently.
315+
let event = match waited {
316+
Ok(inner) => inner?,
317+
Err(_elapsed) => return Err(WaitForDmTimeout.into()),
318+
};
298319

299320
let mut events = Events::default();
300321
events.insert(event);

src/util/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub use events::{
1212
pub use messaging::{
1313
derive_shared_key_hex, derive_shared_keys, keys_from_shared_hex, print_dm_events,
1414
send_admin_chat_message_via_shared_key, send_dm, send_plain_text_dm, wait_for_dm,
15+
WaitForDmTimeout,
1516
};
1617
pub use misc::{get_mcli_path, uppercase_first};
1718
pub use net::connect_nostr;

0 commit comments

Comments
 (0)