|
| 1 | +// This file is Copyright its original authors, visible in version control |
| 2 | +// history. |
| 3 | +// |
| 4 | +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE |
| 5 | +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
| 6 | +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. |
| 7 | +// You may not use this file except in accordance with one or both of these |
| 8 | +// licenses. |
| 9 | + |
| 10 | +use std::time::Duration; |
| 11 | + |
| 12 | +use ldk_server_client::client::{EventStream, LdkServerClient}; |
| 13 | +use ldk_server_client::error::LdkServerError; |
| 14 | +use ldk_server_client::error::LdkServerErrorCode::InternalError; |
| 15 | +use ldk_server_client::ldk_server_grpc::api::unified_send_response; |
| 16 | +use ldk_server_client::ldk_server_grpc::api::{ |
| 17 | + GetPaymentDetailsRequest, GetPaymentDetailsResponse, UnifiedSendRequest, UnifiedSendResponse, |
| 18 | +}; |
| 19 | +use ldk_server_client::ldk_server_grpc::events::event_envelope::Event; |
| 20 | + |
| 21 | +use crate::{handle_error, handle_response_result, print_response, sanitize_for_terminal}; |
| 22 | + |
| 23 | +const EXIT_PAYMENT_FAILED: i32 = 2; |
| 24 | +const EXIT_PAYMENT_TIMEOUT: i32 = 3; |
| 25 | + |
| 26 | +fn lightning_payment_id(response: &UnifiedSendResponse) -> Option<&str> { |
| 27 | + match response.payment_result.as_ref() { |
| 28 | + Some(unified_send_response::PaymentResult::Bolt11PaymentId(id)) |
| 29 | + | Some(unified_send_response::PaymentResult::Bolt12PaymentId(id)) => Some(id.as_str()), |
| 30 | + _ => None, |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +fn exit_with_payment(code: i32, message: &str, payment: &GetPaymentDetailsResponse) -> ! { |
| 35 | + eprintln!("{}", sanitize_for_terminal(message.to_string())); |
| 36 | + print_response(payment); |
| 37 | + std::process::exit(code); |
| 38 | +} |
| 39 | + |
| 40 | +/// Subscribe before paying, then wait for `PaymentSuccessful` / `PaymentFailed`. |
| 41 | +/// |
| 42 | +/// The event stream is live-only, so the subscription is opened before `unified_send`. |
| 43 | +/// A dropped stream is an error; it is not replayed. `timeout` of `None` waits until |
| 44 | +/// the payment finishes. On-chain results are printed immediately. |
| 45 | +/// Exit codes: 0 succeeded, 2 failed, 3 timed out. |
| 46 | +pub(crate) async fn pay_and_wait( |
| 47 | + client: &LdkServerClient, request: UnifiedSendRequest, timeout: Option<Duration>, |
| 48 | +) { |
| 49 | + let mut events = client.subscribe_events().await.map_err(handle_error).unwrap(); |
| 50 | + |
| 51 | + let response = client.unified_send(request).await.map_err(handle_error).unwrap(); |
| 52 | + |
| 53 | + let Some(payment_id) = lightning_payment_id(&response).map(str::to_string) else { |
| 54 | + handle_response_result::<_, UnifiedSendResponse>(Ok(response)); |
| 55 | + return; |
| 56 | + }; |
| 57 | + |
| 58 | + match timeout { |
| 59 | + Some(timeout) => eprintln!( |
| 60 | + "Payment initiated: {payment_id} (waiting up to {}s for terminal state)", |
| 61 | + timeout.as_secs() |
| 62 | + ), |
| 63 | + None => eprintln!("Payment initiated: {payment_id} (waiting for terminal state)"), |
| 64 | + } |
| 65 | + |
| 66 | + match wait_for_terminal_event(client, &mut events, &payment_id, timeout).await { |
| 67 | + Ok(payment) => print_response(&payment), |
| 68 | + Err(WaitError::Failed(payment)) => { |
| 69 | + exit_with_payment( |
| 70 | + EXIT_PAYMENT_FAILED, |
| 71 | + &format!("Payment {payment_id} failed"), |
| 72 | + &payment, |
| 73 | + ); |
| 74 | + }, |
| 75 | + Err(WaitError::TimedOut(payment)) => { |
| 76 | + let secs = timeout.map(|timeout| timeout.as_secs()).unwrap_or(0); |
| 77 | + eprintln!( |
| 78 | + "{}", |
| 79 | + sanitize_for_terminal(format!( |
| 80 | + "Timed out after {secs}s waiting for payment {payment_id}" |
| 81 | + )) |
| 82 | + ); |
| 83 | + if let Some(payment) = payment { |
| 84 | + print_response(&payment); |
| 85 | + } |
| 86 | + std::process::exit(EXIT_PAYMENT_TIMEOUT); |
| 87 | + }, |
| 88 | + Err(WaitError::Transport(e)) => handle_error(e), |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +enum WaitError { |
| 93 | + Failed(GetPaymentDetailsResponse), |
| 94 | + TimedOut(Option<GetPaymentDetailsResponse>), |
| 95 | + Transport(LdkServerError), |
| 96 | +} |
| 97 | + |
| 98 | +async fn wait_for_terminal_event( |
| 99 | + client: &LdkServerClient, events: &mut EventStream, payment_id: &str, timeout: Option<Duration>, |
| 100 | +) -> Result<GetPaymentDetailsResponse, WaitError> { |
| 101 | + let deadline = timeout.map(|timeout| tokio::time::Instant::now() + timeout); |
| 102 | + |
| 103 | + match next_matching_event(events, payment_id, deadline).await? { |
| 104 | + Some(Ok(payment)) => Ok(payment), |
| 105 | + Some(Err(WaitError::TimedOut(_))) => { |
| 106 | + Err(WaitError::TimedOut(fetch_latest(client, payment_id).await)) |
| 107 | + }, |
| 108 | + Some(Err(error)) => Err(error), |
| 109 | + None => Err(WaitError::Transport(LdkServerError::new( |
| 110 | + InternalError, |
| 111 | + format!("event stream ended before payment {payment_id} reached a terminal state"), |
| 112 | + ))), |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +/// `Ok(None)` means the stream ended without a matching terminal event. |
| 117 | +async fn next_matching_event( |
| 118 | + events: &mut EventStream, payment_id: &str, deadline: Option<tokio::time::Instant>, |
| 119 | +) -> Result<Option<Result<GetPaymentDetailsResponse, WaitError>>, WaitError> { |
| 120 | + loop { |
| 121 | + let next = events.next_message(); |
| 122 | + let message = match deadline { |
| 123 | + Some(deadline) => { |
| 124 | + let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); |
| 125 | + if remaining.is_zero() { |
| 126 | + return Ok(Some(Err(WaitError::TimedOut(None)))); |
| 127 | + } |
| 128 | + match tokio::time::timeout(remaining, next).await { |
| 129 | + Ok(message) => message, |
| 130 | + Err(_) => return Ok(Some(Err(WaitError::TimedOut(None)))), |
| 131 | + } |
| 132 | + }, |
| 133 | + None => next.await, |
| 134 | + }; |
| 135 | + |
| 136 | + match message { |
| 137 | + Some(Ok(envelope)) => match envelope.event { |
| 138 | + Some(Event::PaymentSuccessful(event)) if event.payment_id == payment_id => { |
| 139 | + return Ok(Some(Ok(GetPaymentDetailsResponse { payment: event.payment }))); |
| 140 | + }, |
| 141 | + Some(Event::PaymentFailed(event)) if event.payment_id == payment_id => { |
| 142 | + return Ok(Some(Err(WaitError::Failed(GetPaymentDetailsResponse { |
| 143 | + payment: event.payment, |
| 144 | + })))); |
| 145 | + }, |
| 146 | + _ => {}, |
| 147 | + }, |
| 148 | + Some(Err(e)) => return Err(WaitError::Transport(e)), |
| 149 | + None => return Ok(None), |
| 150 | + } |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +async fn fetch_latest( |
| 155 | + client: &LdkServerClient, payment_id: &str, |
| 156 | +) -> Option<GetPaymentDetailsResponse> { |
| 157 | + match client |
| 158 | + .get_payment_details(GetPaymentDetailsRequest { payment_id: payment_id.to_string() }) |
| 159 | + .await |
| 160 | + { |
| 161 | + Ok(details) if details.payment.is_some() => Some(details), |
| 162 | + Ok(_) => { |
| 163 | + eprintln!("Payment {payment_id} was not found. It may not be persisted yet."); |
| 164 | + None |
| 165 | + }, |
| 166 | + Err(e) => { |
| 167 | + eprintln!("Warning: failed to fetch payment {payment_id}: {}", e.message); |
| 168 | + None |
| 169 | + }, |
| 170 | + } |
| 171 | +} |
0 commit comments