lightning_probing/
lib.rs

1use anyhow::{bail, Context};
2use constants::FailureReason;
3use lnd_grpc_rust::{
4    lnrpc::{self, payment::PaymentStatus, FeatureBit, PaymentFailureReason},
5    routerrpc, LndClient,
6};
7use log::info;
8use std::result::Result::Ok;
9mod constants;
10mod utils;
11use utils::{filter_channels_from_pubkeys, generate_secret_for_probes};
12
13use crate::utils::{get_node_features, get_node_info, print_in_flight_payment};
14
15const TLV_ONION_REQ: i32 = FeatureBit::TlvOnionReq as i32;
16const DEFAULT_TIMEOUT_SECONDS: i32 = 60;
17const ZERO_AMOUNT: i64 = 0;
18
19pub struct ProbeDestination {
20    pub client: LndClient,
21    pub probe_amount_sat: Option<i64>,
22    pub destination_pubkey: Option<String>,
23    pub timeout_seconds: Option<i32>,
24    pub fee_limit_sat: i64,
25    pub payment_request: Option<String>,
26    pub outgoing_pubkeys: Option<Vec<String>>,
27    pub last_hop_pubkey: Option<String>,
28    pub max_paths: Option<u32>,
29}
30
31#[derive(Debug)]
32pub struct ProbeResult {
33    pub payment: lnrpc::Payment,
34    pub is_probe_success: bool,
35    pub failure_reason: PaymentFailureReason,
36}
37
38pub async fn probe_destination(mut args: ProbeDestination) -> anyhow::Result<ProbeResult> {
39    if args.payment_request.is_none() && args.destination_pubkey.is_none() {
40        bail!("ExpectedEitherPaymentRequestOrDestinationPubkey");
41    }
42
43    if args.payment_request.is_some() && args.destination_pubkey.is_some() {
44        bail!("ExpectedPaymentRequestOrDestinationPubkeyAndNotBoth");
45    }
46
47    let mut destination: String = "".to_string();
48    let mut features: Vec<i32> = vec![TLV_ONION_REQ];
49    let mut amount: i64 = args.probe_amount_sat.unwrap_or_default();
50    let mut outgoing_channel_ids: Vec<u64> = vec![];
51
52    if args.outgoing_pubkeys.is_some() {
53        let res =
54            filter_channels_from_pubkeys(&mut args.client, args.outgoing_pubkeys.unwrap()).await?;
55
56        outgoing_channel_ids = res.channels.into_iter().map(|n| n.chan_id).collect();
57    }
58
59    if args.destination_pubkey.is_some() {
60        destination = args.destination_pubkey.clone().unwrap();
61
62        let node_info = get_node_info(&mut args.client, args.destination_pubkey.unwrap())
63            .await
64            .context("failed to get nodeinfo")?;
65
66        let node_features = node_info.node.context("failed to get node info")?.features;
67
68        features = get_node_features(node_features);
69    }
70
71    if let Some(payment_request_string) = args.payment_request {
72        let request = lnrpc::PayReqString {
73            pay_req: payment_request_string,
74        };
75
76        let decoded_payment_request = args
77            .client
78            .lightning()
79            .decode_pay_req(request)
80            .await
81            .context("FailedToDecodePaymentRequest")?;
82
83        let inner = decoded_payment_request.into_inner();
84
85        if inner.num_satoshis == ZERO_AMOUNT || inner.num_msat == ZERO_AMOUNT {
86            bail!("Can't probe with 0 amount invoices")
87        }
88
89        amount = inner.num_satoshis;
90        features = get_node_features(inner.features);
91
92        destination = inner.destination;
93    }
94
95    let hash = generate_secret_for_probes();
96
97    let request = routerrpc::SendPaymentRequest {
98        amt: amount,
99        dest: hex::decode(destination).context("Failed to decode hex pubkey")?,
100        dest_features: features,
101        payment_hash: hash,
102        timeout_seconds: args.timeout_seconds.unwrap_or(DEFAULT_TIMEOUT_SECONDS),
103        fee_limit_sat: args.fee_limit_sat,
104        max_parts: args.max_paths.unwrap_or_default(),
105        outgoing_chan_ids: outgoing_channel_ids,
106
107        ..Default::default()
108    };
109
110    let res = args.client.router().send_payment_v2(request).await;
111
112    let mut response = match res {
113        Ok(response) => response.into_inner(),
114        Err(e) => bail!(
115            "Failed to get streaming response from send payment: {:?}",
116            e
117        ),
118    };
119
120    while let Some(payment) = response.message().await.context("Failed to get payment")? {
121        if let Some(status) = PaymentStatus::from_i32(payment.status) {
122            let failure_reason = FailureReason::from(payment.failure_reason);
123            if status == PaymentStatus::InFlight {
124                let details = print_in_flight_payment(payment.clone())?;
125                if let Some(last_detail) = details.last() {
126                    info!("payment inflight {:?}", last_detail);
127                }
128            }
129
130            if status == PaymentStatus::Failed {
131                let is_probe_success = failure_reason == FailureReason::IncorrectPaymentDetails;
132                return Ok(ProbeResult {
133                    failure_reason: payment.failure_reason(),
134                    is_probe_success,
135                    payment,
136                });
137            }
138        }
139    }
140
141    bail!("An Unexpected error occoured")
142}