From f0eb724681f30b24cfd437f966f1b6ecc8a1061e Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 2 Feb 2024 20:04:18 +0000 Subject: [PATCH 1/4] Use `payment_parameters_from_invoice` rather than building by hand --- src/cli.rs | 47 ++++++++++------------------------------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index e348742..5c544c2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -19,6 +19,7 @@ use lightning::sign::{EntropySource, KeysManager}; use lightning::util::config::{ChannelHandshakeConfig, ChannelHandshakeLimits, UserConfig}; use lightning::util::persist::KVStore; use lightning::util::ser::{Writeable, Writer}; +use lightning_invoice::payment::payment_parameters_from_invoice; use lightning_invoice::{utils, Bolt11Invoice, Currency}; use lightning_persister::fs_store::FilesystemStore; use std::env; @@ -690,8 +691,16 @@ fn send_payment( outbound_payments: &mut OutboundPaymentInfoStorage, fs_store: Arc, ) { let payment_id = PaymentId((*invoice.payment_hash()).to_byte_array()); - let payment_hash = PaymentHash((*invoice.payment_hash()).to_byte_array()); let payment_secret = Some(*invoice.payment_secret()); + let (payment_hash, recipient_onion, route_params) = + match payment_parameters_from_invoice(invoice) { + Ok(res) => res, + Err(e) => { + println!("Failed to parse invoice"); + print!("> "); + return; + } + }; outbound_payments.payments.insert( payment_id, PaymentInfo { @@ -703,42 +712,6 @@ fn send_payment( ); fs_store.write("", "", OUTBOUND_PAYMENTS_FNAME, &outbound_payments.encode()).unwrap(); - let mut recipient_onion = RecipientOnionFields::secret_only(*invoice.payment_secret()); - recipient_onion.payment_metadata = invoice.payment_metadata().map(|v| v.clone()); - let mut payment_params = match PaymentParameters::from_node_id( - invoice.recover_payee_pub_key(), - invoice.min_final_cltv_expiry_delta() as u32, - ) - .with_expiry_time( - invoice.duration_since_epoch().as_secs().saturating_add(invoice.expiry_time().as_secs()), - ) - .with_route_hints(invoice.route_hints()) - { - Err(e) => { - println!("ERROR: Could not process invoice to prepare payment parameters, {:?}, invoice: {:?}", e, invoice); - return; - } - Ok(p) => p, - }; - if let Some(features) = invoice.features() { - payment_params = match payment_params.with_bolt11_features(features.clone()) { - Err(e) => { - println!("ERROR: Could not build BOLT11 payment parameters! {:?}", e); - return; - } - Ok(p) => p, - }; - } - let invoice_amount = match invoice.amount_milli_satoshis() { - None => { - println!("ERROR: An invoice with an amount is expected; {:?}", invoice); - return; - } - Some(a) => a, - }; - let route_params = - RouteParameters::from_payment_params_and_value(payment_params, invoice_amount); - match channel_manager.send_payment( payment_hash, recipient_onion, From d6f04e056b7065b05ca6361814c8c0ffc7283257 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 2 Feb 2024 20:56:55 +0000 Subject: [PATCH 2/4] Handle `Event::ConnectionNeeded` for onion message sending --- src/main.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 73a6ddf..6b4dbdd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,6 +51,7 @@ use std::fs; use std::fs::File; use std::io; use std::io::Write; +use std::net::ToSocketAddrs; use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; @@ -172,11 +173,11 @@ pub(crate) type BumpTxEventHandler = BumpTransactionEventHandler< >; async fn handle_ldk_events( - channel_manager: &Arc, bitcoind_client: &BitcoindClient, + channel_manager: Arc, bitcoind_client: &BitcoindClient, network_graph: &NetworkGraph, keys_manager: &KeysManager, - bump_tx_event_handler: &BumpTxEventHandler, + bump_tx_event_handler: &BumpTxEventHandler, peer_manager: Arc, inbound_payments: Arc>, - outbound_payments: Arc>, fs_store: &Arc, + outbound_payments: Arc>, fs_store: Arc, network: Network, event: Event, ) { match event { @@ -512,7 +513,20 @@ async fn handle_ldk_events( } Event::HTLCIntercepted { .. } => {} Event::BumpTransaction(event) => bump_tx_event_handler.handle_event(&event), - Event::ConnectionNeeded { .. } => {} + Event::ConnectionNeeded { node_id, addresses } => { + tokio::spawn(async move { + for address in addresses { + if let Ok(sockaddrs) = address.to_socket_addrs() { + for addr in sockaddrs { + let pm = Arc::clone(&peer_manager); + if cli::connect_peer_if_necessary(node_id, addr, pm).await.is_ok() { + return; + } + } + } + } + }); + } } } @@ -886,6 +900,7 @@ async fn start_ldk() { let inbound_payments_event_listener = Arc::clone(&inbound_payments); let outbound_payments_event_listener = Arc::clone(&outbound_payments); let fs_store_event_listener = Arc::clone(&fs_store); + let peer_manager_event_listener = Arc::clone(&peer_manager); let network = args.network; let event_handler = move |event: Event| { let channel_manager_event_listener = Arc::clone(&channel_manager_event_listener); @@ -896,16 +911,18 @@ async fn start_ldk() { let inbound_payments_event_listener = Arc::clone(&inbound_payments_event_listener); let outbound_payments_event_listener = Arc::clone(&outbound_payments_event_listener); let fs_store_event_listener = Arc::clone(&fs_store_event_listener); + let peer_manager_event_listener = Arc::clone(&peer_manager_event_listener); async move { handle_ldk_events( - &channel_manager_event_listener, + channel_manager_event_listener, &bitcoind_client_event_listener, &network_graph_event_listener, &keys_manager_event_listener, &bump_tx_event_handler, + peer_manager_event_listener, inbound_payments_event_listener, outbound_payments_event_listener, - &fs_store_event_listener, + fs_store_event_listener, network, event, ) From a81a739005b900649bb74f7397c040ddd27c0c11 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 2 Feb 2024 20:57:52 +0000 Subject: [PATCH 3/4] Add BOLT12 Offer generation and payment support --- src/cli.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 97 insertions(+), 14 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 5c544c2..72cc484 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,6 +11,7 @@ use bitcoin::secp256k1::PublicKey; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry}; use lightning::ln::msgs::SocketAddress; use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage}; +use lightning::offers::offer::{self, Offer}; use lightning::onion_message::messenger::Destination; use lightning::onion_message::packet::OnionMessageContents; use lightning::routing::gossip::NodeId; @@ -73,7 +74,7 @@ pub(crate) fn poll_for_user_input( ); println!("LDK logs are available at /.ldk/logs"); println!("Local Node ID is {}.", channel_manager.get_our_node_id()); - loop { + 'read_command: loop { print!("> "); io::stdout().flush().unwrap(); // Without flushing, the `>` doesn't print let mut line = String::new(); @@ -161,20 +162,73 @@ pub(crate) fn poll_for_user_input( continue; } - let invoice = match Bolt11Invoice::from_str(invoice_str.unwrap()) { - Ok(inv) => inv, - Err(e) => { - println!("ERROR: invalid invoice: {:?}", e); - continue; + if let Ok(offer) = Offer::from_str(invoice_str.unwrap()) { + let offer_hash = Sha256::hash(invoice_str.unwrap().as_bytes()); + let payment_id = PaymentId(*offer_hash.as_ref()); + + let amt_msat = + match offer.amount() { + Some(offer::Amount::Bitcoin { amount_msats }) => *amount_msats, + amt => { + println!("ERROR: Cannot process non-Bitcoin-denominated offer value {:?}", amt); + continue; + } + }; + + loop { + print!("Paying offer for {} msat. Continue (Y/N)? >", amt_msat); + io::stdout().flush().unwrap(); + + if let Err(e) = io::stdin().read_line(&mut line) { + println!("ERROR: {}", e); + break 'read_command; + } + + if line.len() == 0 { + // We hit EOF / Ctrl-D + break 'read_command; + } + + if line.starts_with("Y") { + break; + } + if line.starts_with("N") { + continue 'read_command; + } } - }; - send_payment( - &channel_manager, - &invoice, - &mut outbound_payments.lock().unwrap(), - Arc::clone(&fs_store), - ); + outbound_payments.lock().unwrap().payments.insert( + payment_id, + PaymentInfo { + preimage: None, + secret: None, + status: HTLCStatus::Pending, + amt_msat: MillisatAmount(Some(amt_msat)), + }, + ); + fs_store + .write("", "", OUTBOUND_PAYMENTS_FNAME, &outbound_payments.encode()) + .unwrap(); + + let retry = Retry::Timeout(Duration::from_secs(10)); + let pay = channel_manager + .pay_for_offer(&offer, None, None, None, payment_id, retry, None); + if pay.is_err() { + println!("ERROR: Failed to pay: {:?}", pay); + } + } else { + match Bolt11Invoice::from_str(invoice_str.unwrap()) { + Ok(invoice) => send_payment( + &channel_manager, + &invoice, + &mut outbound_payments.lock().unwrap(), + Arc::clone(&fs_store), + ), + Err(e) => { + println!("ERROR: invalid invoice: {:?}", e); + } + } + } } "keysend" => { let dest_pubkey = match words.next() { @@ -213,6 +267,34 @@ pub(crate) fn poll_for_user_input( Arc::clone(&fs_store), ); } + "getoffer" => { + let offer_builder = channel_manager.create_offer_builder(String::new()); + if let Err(e) = offer_builder { + println!("ERROR: Failed to initiate offer building: {:?}", e); + continue; + } + + let amt_str = words.next(); + let offer = if amt_str.is_some() { + let amt_msat: Result = amt_str.unwrap().parse(); + if amt_msat.is_err() { + println!("ERROR: getoffer provided payment amount was not a number"); + continue; + } + offer_builder.unwrap().amount_msats(amt_msat.unwrap()).build() + } else { + offer_builder.unwrap().build() + }; + + if offer.is_err() { + println!("ERROR: Failed to build offer: {:?}", offer.unwrap_err()); + } else { + // Note that unlike BOLT11 invoice creation we don't bother to add a + // pending inbound payment here, as offers can be reused and don't + // correspond with individual payments. + println!("{}", offer.unwrap()); + } + } "getinvoice" => { let amt_str = words.next(); if amt_str.is_none() { @@ -481,11 +563,12 @@ fn help() { println!(" disconnectpeer "); println!(" listpeers"); println!("\n Payments:"); - println!(" sendpayment "); + println!(" sendpayment "); println!(" keysend "); println!(" listpayments"); println!("\n Invoices:"); println!(" getinvoice "); + println!(" getoffer []"); println!("\n Other:"); println!(" signmessage "); println!( From a9273ad140da7c28fb801f0301a31e3f016c913f Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 6 Feb 2024 22:43:24 +0000 Subject: [PATCH 4/4] Support paying 0-amount offers and invoices --- src/cli.rs | 81 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 72cc484..a70d772 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,6 +21,7 @@ use lightning::util::config::{ChannelHandshakeConfig, ChannelHandshakeLimits, Us use lightning::util::persist::KVStore; use lightning::util::ser::{Writeable, Writer}; use lightning_invoice::payment::payment_parameters_from_invoice; +use lightning_invoice::payment::payment_parameters_from_zero_amount_invoice; use lightning_invoice::{utils, Bolt11Invoice, Currency}; use lightning_persister::fs_store::FilesystemStore; use std::env; @@ -162,20 +163,35 @@ pub(crate) fn poll_for_user_input( continue; } + let mut user_provided_amt: Option = None; + if let Some(amt_msat_str) = words.next() { + match amt_msat_str.parse() { + Ok(amt) => user_provided_amt = Some(amt), + Err(e) => { + println!("ERROR: couldn't parse amount_msat: {}", e); + continue; + } + }; + } + if let Ok(offer) = Offer::from_str(invoice_str.unwrap()) { let offer_hash = Sha256::hash(invoice_str.unwrap().as_bytes()); let payment_id = PaymentId(*offer_hash.as_ref()); - let amt_msat = - match offer.amount() { - Some(offer::Amount::Bitcoin { amount_msats }) => *amount_msats, - amt => { - println!("ERROR: Cannot process non-Bitcoin-denominated offer value {:?}", amt); - continue; - } - }; + let amt_msat = match (offer.amount(), user_provided_amt) { + (Some(offer::Amount::Bitcoin { amount_msats }), _) => *amount_msats, + (_, Some(amt)) => amt, + (amt, _) => { + println!("ERROR: Cannot process non-Bitcoin-denominated offer value {:?}", amt); + continue; + } + }; + if user_provided_amt.is_some() && user_provided_amt != Some(amt_msat) { + println!("Amount didn't match offer of {}msat", amt_msat); + continue; + } - loop { + while user_provided_amt.is_none() { print!("Paying offer for {} msat. Continue (Y/N)? >", amt_msat); io::stdout().flush().unwrap(); @@ -211,8 +227,9 @@ pub(crate) fn poll_for_user_input( .unwrap(); let retry = Retry::Timeout(Duration::from_secs(10)); + let amt = Some(amt_msat); let pay = channel_manager - .pay_for_offer(&offer, None, None, None, payment_id, retry, None); + .pay_for_offer(&offer, None, amt, None, payment_id, retry, None); if pay.is_err() { println!("ERROR: Failed to pay: {:?}", pay); } @@ -221,6 +238,7 @@ pub(crate) fn poll_for_user_input( Ok(invoice) => send_payment( &channel_manager, &invoice, + user_provided_amt, &mut outbound_payments.lock().unwrap(), Arc::clone(&fs_store), ), @@ -563,7 +581,7 @@ fn help() { println!(" disconnectpeer "); println!(" listpeers"); println!("\n Payments:"); - println!(" sendpayment "); + println!(" sendpayment []"); println!(" keysend "); println!(" listpayments"); println!("\n Invoices:"); @@ -770,20 +788,41 @@ fn open_channel( } fn send_payment( - channel_manager: &ChannelManager, invoice: &Bolt11Invoice, + channel_manager: &ChannelManager, invoice: &Bolt11Invoice, required_amount_msat: Option, outbound_payments: &mut OutboundPaymentInfoStorage, fs_store: Arc, ) { let payment_id = PaymentId((*invoice.payment_hash()).to_byte_array()); let payment_secret = Some(*invoice.payment_secret()); - let (payment_hash, recipient_onion, route_params) = - match payment_parameters_from_invoice(invoice) { - Ok(res) => res, - Err(e) => { - println!("Failed to parse invoice"); - print!("> "); - return; - } - }; + let zero_amt_invoice = + invoice.amount_milli_satoshis().is_none() || invoice.amount_milli_satoshis() == Some(0); + let pay_params_opt = if zero_amt_invoice { + if let Some(amt_msat) = required_amount_msat { + payment_parameters_from_zero_amount_invoice(invoice, amt_msat) + } else { + println!("Need an amount for the given 0-value invoice"); + print!("> "); + return; + } + } else { + if required_amount_msat.is_some() && invoice.amount_milli_satoshis() != required_amount_msat + { + println!( + "Amount didn't match invoice value of {}msat", + invoice.amount_milli_satoshis().unwrap_or(0) + ); + print!("> "); + return; + } + payment_parameters_from_invoice(invoice) + }; + let (payment_hash, recipient_onion, route_params) = match pay_params_opt { + Ok(res) => res, + Err(e) => { + println!("Failed to parse invoice"); + print!("> "); + return; + } + }; outbound_payments.payments.insert( payment_id, PaymentInfo {