Skip to content

Commit a81a739

Browse files
committed
Add BOLT12 Offer generation and payment support
1 parent d6f04e0 commit a81a739

File tree

1 file changed

+97
-14
lines changed

1 file changed

+97
-14
lines changed

src/cli.rs

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use bitcoin::secp256k1::PublicKey;
1111
use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry};
1212
use lightning::ln::msgs::SocketAddress;
1313
use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage};
14+
use lightning::offers::offer::{self, Offer};
1415
use lightning::onion_message::messenger::Destination;
1516
use lightning::onion_message::packet::OnionMessageContents;
1617
use lightning::routing::gossip::NodeId;
@@ -73,7 +74,7 @@ pub(crate) fn poll_for_user_input(
7374
);
7475
println!("LDK logs are available at <your-supplied-ldk-data-dir-path>/.ldk/logs");
7576
println!("Local Node ID is {}.", channel_manager.get_our_node_id());
76-
loop {
77+
'read_command: loop {
7778
print!("> ");
7879
io::stdout().flush().unwrap(); // Without flushing, the `>` doesn't print
7980
let mut line = String::new();
@@ -161,20 +162,73 @@ pub(crate) fn poll_for_user_input(
161162
continue;
162163
}
163164

164-
let invoice = match Bolt11Invoice::from_str(invoice_str.unwrap()) {
165-
Ok(inv) => inv,
166-
Err(e) => {
167-
println!("ERROR: invalid invoice: {:?}", e);
168-
continue;
165+
if let Ok(offer) = Offer::from_str(invoice_str.unwrap()) {
166+
let offer_hash = Sha256::hash(invoice_str.unwrap().as_bytes());
167+
let payment_id = PaymentId(*offer_hash.as_ref());
168+
169+
let amt_msat =
170+
match offer.amount() {
171+
Some(offer::Amount::Bitcoin { amount_msats }) => *amount_msats,
172+
amt => {
173+
println!("ERROR: Cannot process non-Bitcoin-denominated offer value {:?}", amt);
174+
continue;
175+
}
176+
};
177+
178+
loop {
179+
print!("Paying offer for {} msat. Continue (Y/N)? >", amt_msat);
180+
io::stdout().flush().unwrap();
181+
182+
if let Err(e) = io::stdin().read_line(&mut line) {
183+
println!("ERROR: {}", e);
184+
break 'read_command;
185+
}
186+
187+
if line.len() == 0 {
188+
// We hit EOF / Ctrl-D
189+
break 'read_command;
190+
}
191+
192+
if line.starts_with("Y") {
193+
break;
194+
}
195+
if line.starts_with("N") {
196+
continue 'read_command;
197+
}
169198
}
170-
};
171199

172-
send_payment(
173-
&channel_manager,
174-
&invoice,
175-
&mut outbound_payments.lock().unwrap(),
176-
Arc::clone(&fs_store),
177-
);
200+
outbound_payments.lock().unwrap().payments.insert(
201+
payment_id,
202+
PaymentInfo {
203+
preimage: None,
204+
secret: None,
205+
status: HTLCStatus::Pending,
206+
amt_msat: MillisatAmount(Some(amt_msat)),
207+
},
208+
);
209+
fs_store
210+
.write("", "", OUTBOUND_PAYMENTS_FNAME, &outbound_payments.encode())
211+
.unwrap();
212+
213+
let retry = Retry::Timeout(Duration::from_secs(10));
214+
let pay = channel_manager
215+
.pay_for_offer(&offer, None, None, None, payment_id, retry, None);
216+
if pay.is_err() {
217+
println!("ERROR: Failed to pay: {:?}", pay);
218+
}
219+
} else {
220+
match Bolt11Invoice::from_str(invoice_str.unwrap()) {
221+
Ok(invoice) => send_payment(
222+
&channel_manager,
223+
&invoice,
224+
&mut outbound_payments.lock().unwrap(),
225+
Arc::clone(&fs_store),
226+
),
227+
Err(e) => {
228+
println!("ERROR: invalid invoice: {:?}", e);
229+
}
230+
}
231+
}
178232
}
179233
"keysend" => {
180234
let dest_pubkey = match words.next() {
@@ -213,6 +267,34 @@ pub(crate) fn poll_for_user_input(
213267
Arc::clone(&fs_store),
214268
);
215269
}
270+
"getoffer" => {
271+
let offer_builder = channel_manager.create_offer_builder(String::new());
272+
if let Err(e) = offer_builder {
273+
println!("ERROR: Failed to initiate offer building: {:?}", e);
274+
continue;
275+
}
276+
277+
let amt_str = words.next();
278+
let offer = if amt_str.is_some() {
279+
let amt_msat: Result<u64, _> = amt_str.unwrap().parse();
280+
if amt_msat.is_err() {
281+
println!("ERROR: getoffer provided payment amount was not a number");
282+
continue;
283+
}
284+
offer_builder.unwrap().amount_msats(amt_msat.unwrap()).build()
285+
} else {
286+
offer_builder.unwrap().build()
287+
};
288+
289+
if offer.is_err() {
290+
println!("ERROR: Failed to build offer: {:?}", offer.unwrap_err());
291+
} else {
292+
// Note that unlike BOLT11 invoice creation we don't bother to add a
293+
// pending inbound payment here, as offers can be reused and don't
294+
// correspond with individual payments.
295+
println!("{}", offer.unwrap());
296+
}
297+
}
216298
"getinvoice" => {
217299
let amt_str = words.next();
218300
if amt_str.is_none() {
@@ -481,11 +563,12 @@ fn help() {
481563
println!(" disconnectpeer <peer_pubkey>");
482564
println!(" listpeers");
483565
println!("\n Payments:");
484-
println!(" sendpayment <invoice>");
566+
println!(" sendpayment <invoice|offer>");
485567
println!(" keysend <dest_pubkey> <amt_msats>");
486568
println!(" listpayments");
487569
println!("\n Invoices:");
488570
println!(" getinvoice <amt_msats> <expiry_secs>");
571+
println!(" getoffer [<amt_msats>]");
489572
println!("\n Other:");
490573
println!(" signmessage <message>");
491574
println!(

0 commit comments

Comments
 (0)