diff --git a/src/cli.rs b/src/cli.rs index e348742..a70d772 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; @@ -19,6 +20,8 @@ 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::payment::payment_parameters_from_zero_amount_invoice; use lightning_invoice::{utils, Bolt11Invoice, Currency}; use lightning_persister::fs_store::FilesystemStore; use std::env; @@ -72,7 +75,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(); @@ -160,20 +163,90 @@ 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); + 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(), 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; } - }; - send_payment( - &channel_manager, - &invoice, - &mut outbound_payments.lock().unwrap(), - Arc::clone(&fs_store), - ); + while user_provided_amt.is_none() { + 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; + } + } + + 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 amt = Some(amt_msat); + let pay = channel_manager + .pay_for_offer(&offer, None, amt, 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, + user_provided_amt, + &mut outbound_payments.lock().unwrap(), + Arc::clone(&fs_store), + ), + Err(e) => { + println!("ERROR: invalid invoice: {:?}", e); + } + } + } } "keysend" => { let dest_pubkey = match words.next() { @@ -212,6 +285,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() { @@ -480,11 +581,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!( @@ -686,12 +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_hash = PaymentHash((*invoice.payment_hash()).to_byte_array()); let payment_secret = Some(*invoice.payment_secret()); + 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 { @@ -703,42 +834,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, 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, )