From 9c58b38f9cbe828acaeb313837a1ddd8309edb62 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 2 Feb 2024 20:57:52 +0000 Subject: [PATCH] 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..7044482 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-amount'd 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!(