Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use PaymentParameters creation utility and support BOLT12 #129

Merged
merged 4 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 146 additions & 51 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -72,7 +75,7 @@ pub(crate) fn poll_for_user_input(
);
println!("LDK logs are available at <your-supplied-ldk-data-dir-path>/.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();
Expand Down Expand Up @@ -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<u64> = 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() {
Expand Down Expand Up @@ -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<u64, _> = 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() {
Expand Down Expand Up @@ -480,11 +581,12 @@ fn help() {
println!(" disconnectpeer <peer_pubkey>");
println!(" listpeers");
println!("\n Payments:");
println!(" sendpayment <invoice>");
println!(" sendpayment <invoice|offer> [<amount_msat>]");
println!(" keysend <dest_pubkey> <amt_msats>");
println!(" listpayments");
println!("\n Invoices:");
println!(" getinvoice <amt_msats> <expiry_secs>");
println!(" getoffer [<amt_msats>]");
println!("\n Other:");
println!(" signmessage <message>");
println!(
Expand Down Expand Up @@ -686,12 +788,41 @@ fn open_channel(
}

fn send_payment(
channel_manager: &ChannelManager, invoice: &Bolt11Invoice,
channel_manager: &ChannelManager, invoice: &Bolt11Invoice, required_amount_msat: Option<u64>,
outbound_payments: &mut OutboundPaymentInfoStorage, fs_store: Arc<FilesystemStore>,
) {
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 {
Expand All @@ -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,
Expand Down
29 changes: 23 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -172,11 +173,11 @@ pub(crate) type BumpTxEventHandler = BumpTransactionEventHandler<
>;

async fn handle_ldk_events(
channel_manager: &Arc<ChannelManager>, bitcoind_client: &BitcoindClient,
channel_manager: Arc<ChannelManager>, bitcoind_client: &BitcoindClient,
network_graph: &NetworkGraph, keys_manager: &KeysManager,
bump_tx_event_handler: &BumpTxEventHandler,
bump_tx_event_handler: &BumpTxEventHandler, peer_manager: Arc<PeerManager>,
inbound_payments: Arc<Mutex<InboundPaymentInfoStorage>>,
outbound_payments: Arc<Mutex<OutboundPaymentInfoStorage>>, fs_store: &Arc<FilesystemStore>,
outbound_payments: Arc<Mutex<OutboundPaymentInfoStorage>>, fs_store: Arc<FilesystemStore>,
network: Network, event: Event,
) {
match event {
Expand Down Expand Up @@ -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;
}
}
}
}
});
}
}
}

Expand Down Expand Up @@ -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);
Expand All @@ -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,
)
Expand Down
Loading