Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Use PaymentParameters creation utility and support BOLT12 by TheBlueMatt · Pull Request #129 · lightningdevkit/ldk-sample · GitHub
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Use PaymentParameters creation utility and support BOLT12 by TheBlueMatt · Pull Request #129 · lightningdevkit/ldk-sample · GitHub
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Use PaymentParameters creation utility and support BOLT12 by TheBlueMatt · Pull Request #129 · lightningdevkit/ldk-sample · GitHub
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Use PaymentParameters creation utility and support BOLT12 by TheBlueMatt · Pull Request #129 · lightningdevkit/ldk-sample · GitHub
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Use PaymentParameters creation utility and support BOLT12 by TheBlueMatt · Pull Request #129 · lightningdevkit/ldk-sample · GitHub
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Use PaymentParameters creation utility and support BOLT12 by TheBlueMatt · Pull Request #129 · lightningdevkit/ldk-sample · GitHub
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Use PaymentParameters creation utility and support BOLT12 by TheBlueMatt · Pull Request #129 · lightningdevkit/ldk-sample · GitHub
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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