@@ -11,6 +11,7 @@ use bitcoin::secp256k1::PublicKey;
11
11
use lightning:: ln:: channelmanager:: { PaymentId , RecipientOnionFields , Retry } ;
12
12
use lightning:: ln:: msgs:: SocketAddress ;
13
13
use lightning:: ln:: { ChannelId , PaymentHash , PaymentPreimage } ;
14
+ use lightning:: offers:: offer:: { self , Offer } ;
14
15
use lightning:: onion_message:: messenger:: Destination ;
15
16
use lightning:: onion_message:: packet:: OnionMessageContents ;
16
17
use lightning:: routing:: gossip:: NodeId ;
@@ -19,6 +20,8 @@ use lightning::sign::{EntropySource, KeysManager};
19
20
use lightning:: util:: config:: { ChannelHandshakeConfig , ChannelHandshakeLimits , UserConfig } ;
20
21
use lightning:: util:: persist:: KVStore ;
21
22
use lightning:: util:: ser:: { Writeable , Writer } ;
23
+ use lightning_invoice:: payment:: payment_parameters_from_invoice;
24
+ use lightning_invoice:: payment:: payment_parameters_from_zero_amount_invoice;
22
25
use lightning_invoice:: { utils, Bolt11Invoice , Currency } ;
23
26
use lightning_persister:: fs_store:: FilesystemStore ;
24
27
use std:: env;
@@ -72,7 +75,7 @@ pub(crate) fn poll_for_user_input(
72
75
) ;
73
76
println ! ( "LDK logs are available at <your-supplied-ldk-data-dir-path>/.ldk/logs" ) ;
74
77
println ! ( "Local Node ID is {}." , channel_manager. get_our_node_id( ) ) ;
75
- loop {
78
+ ' read_command : loop {
76
79
print ! ( "> " ) ;
77
80
io:: stdout ( ) . flush ( ) . unwrap ( ) ; // Without flushing, the `>` doesn't print
78
81
let mut line = String :: new ( ) ;
@@ -160,20 +163,90 @@ pub(crate) fn poll_for_user_input(
160
163
continue ;
161
164
}
162
165
163
- let invoice = match Bolt11Invoice :: from_str ( invoice_str. unwrap ( ) ) {
164
- Ok ( inv) => inv,
165
- Err ( e) => {
166
- println ! ( "ERROR: invalid invoice: {:?}" , e) ;
166
+ let mut user_provided_amt: Option < u64 > = None ;
167
+ if let Some ( amt_msat_str) = words. next ( ) {
168
+ match amt_msat_str. parse ( ) {
169
+ Ok ( amt) => user_provided_amt = Some ( amt) ,
170
+ Err ( e) => {
171
+ println ! ( "ERROR: couldn't parse amount_msat: {}" , e) ;
172
+ continue ;
173
+ }
174
+ } ;
175
+ }
176
+
177
+ if let Ok ( offer) = Offer :: from_str ( invoice_str. unwrap ( ) ) {
178
+ let offer_hash = Sha256 :: hash ( invoice_str. unwrap ( ) . as_bytes ( ) ) ;
179
+ let payment_id = PaymentId ( * offer_hash. as_ref ( ) ) ;
180
+
181
+ let amt_msat = match ( offer. amount ( ) , user_provided_amt) {
182
+ ( Some ( offer:: Amount :: Bitcoin { amount_msats } ) , _) => * amount_msats,
183
+ ( _, Some ( amt) ) => amt,
184
+ ( amt, _) => {
185
+ println ! ( "ERROR: Cannot process non-Bitcoin-denominated offer value {:?}" , amt) ;
186
+ continue ;
187
+ }
188
+ } ;
189
+ if user_provided_amt. is_some ( ) && user_provided_amt != Some ( amt_msat) {
190
+ println ! ( "Amount didn't match offer of {}msat" , amt_msat) ;
167
191
continue ;
168
192
}
169
- } ;
170
193
171
- send_payment (
172
- & channel_manager,
173
- & invoice,
174
- & mut outbound_payments. lock ( ) . unwrap ( ) ,
175
- Arc :: clone ( & fs_store) ,
176
- ) ;
194
+ while user_provided_amt. is_none ( ) {
195
+ print ! ( "Paying offer for {} msat. Continue (Y/N)? >" , amt_msat) ;
196
+ io:: stdout ( ) . flush ( ) . unwrap ( ) ;
197
+
198
+ if let Err ( e) = io:: stdin ( ) . read_line ( & mut line) {
199
+ println ! ( "ERROR: {}" , e) ;
200
+ break ' read_command;
201
+ }
202
+
203
+ if line. len ( ) == 0 {
204
+ // We hit EOF / Ctrl-D
205
+ break ' read_command;
206
+ }
207
+
208
+ if line. starts_with ( "Y" ) {
209
+ break ;
210
+ }
211
+ if line. starts_with ( "N" ) {
212
+ continue ' read_command;
213
+ }
214
+ }
215
+
216
+ outbound_payments. lock ( ) . unwrap ( ) . payments . insert (
217
+ payment_id,
218
+ PaymentInfo {
219
+ preimage : None ,
220
+ secret : None ,
221
+ status : HTLCStatus :: Pending ,
222
+ amt_msat : MillisatAmount ( Some ( amt_msat) ) ,
223
+ } ,
224
+ ) ;
225
+ fs_store
226
+ . write ( "" , "" , OUTBOUND_PAYMENTS_FNAME , & outbound_payments. encode ( ) )
227
+ . unwrap ( ) ;
228
+
229
+ let retry = Retry :: Timeout ( Duration :: from_secs ( 10 ) ) ;
230
+ let amt = Some ( amt_msat) ;
231
+ let pay = channel_manager
232
+ . pay_for_offer ( & offer, None , amt, None , payment_id, retry, None ) ;
233
+ if pay. is_err ( ) {
234
+ println ! ( "ERROR: Failed to pay: {:?}" , pay) ;
235
+ }
236
+ } else {
237
+ match Bolt11Invoice :: from_str ( invoice_str. unwrap ( ) ) {
238
+ Ok ( invoice) => send_payment (
239
+ & channel_manager,
240
+ & invoice,
241
+ user_provided_amt,
242
+ & mut outbound_payments. lock ( ) . unwrap ( ) ,
243
+ Arc :: clone ( & fs_store) ,
244
+ ) ,
245
+ Err ( e) => {
246
+ println ! ( "ERROR: invalid invoice: {:?}" , e) ;
247
+ }
248
+ }
249
+ }
177
250
}
178
251
"keysend" => {
179
252
let dest_pubkey = match words. next ( ) {
@@ -212,6 +285,34 @@ pub(crate) fn poll_for_user_input(
212
285
Arc :: clone ( & fs_store) ,
213
286
) ;
214
287
}
288
+ "getoffer" => {
289
+ let offer_builder = channel_manager. create_offer_builder ( String :: new ( ) ) ;
290
+ if let Err ( e) = offer_builder {
291
+ println ! ( "ERROR: Failed to initiate offer building: {:?}" , e) ;
292
+ continue ;
293
+ }
294
+
295
+ let amt_str = words. next ( ) ;
296
+ let offer = if amt_str. is_some ( ) {
297
+ let amt_msat: Result < u64 , _ > = amt_str. unwrap ( ) . parse ( ) ;
298
+ if amt_msat. is_err ( ) {
299
+ println ! ( "ERROR: getoffer provided payment amount was not a number" ) ;
300
+ continue ;
301
+ }
302
+ offer_builder. unwrap ( ) . amount_msats ( amt_msat. unwrap ( ) ) . build ( )
303
+ } else {
304
+ offer_builder. unwrap ( ) . build ( )
305
+ } ;
306
+
307
+ if offer. is_err ( ) {
308
+ println ! ( "ERROR: Failed to build offer: {:?}" , offer. unwrap_err( ) ) ;
309
+ } else {
310
+ // Note that unlike BOLT11 invoice creation we don't bother to add a
311
+ // pending inbound payment here, as offers can be reused and don't
312
+ // correspond with individual payments.
313
+ println ! ( "{}" , offer. unwrap( ) ) ;
314
+ }
315
+ }
215
316
"getinvoice" => {
216
317
let amt_str = words. next ( ) ;
217
318
if amt_str. is_none ( ) {
@@ -480,11 +581,12 @@ fn help() {
480
581
println ! ( " disconnectpeer <peer_pubkey>" ) ;
481
582
println ! ( " listpeers" ) ;
482
583
println ! ( "\n Payments:" ) ;
483
- println ! ( " sendpayment <invoice> " ) ;
584
+ println ! ( " sendpayment <invoice|offer> [<amount_msat>] " ) ;
484
585
println ! ( " keysend <dest_pubkey> <amt_msats>" ) ;
485
586
println ! ( " listpayments" ) ;
486
587
println ! ( "\n Invoices:" ) ;
487
588
println ! ( " getinvoice <amt_msats> <expiry_secs>" ) ;
589
+ println ! ( " getoffer [<amt_msats>]" ) ;
488
590
println ! ( "\n Other:" ) ;
489
591
println ! ( " signmessage <message>" ) ;
490
592
println ! (
@@ -686,12 +788,41 @@ fn open_channel(
686
788
}
687
789
688
790
fn send_payment (
689
- channel_manager : & ChannelManager , invoice : & Bolt11Invoice ,
791
+ channel_manager : & ChannelManager , invoice : & Bolt11Invoice , required_amount_msat : Option < u64 > ,
690
792
outbound_payments : & mut OutboundPaymentInfoStorage , fs_store : Arc < FilesystemStore > ,
691
793
) {
692
794
let payment_id = PaymentId ( ( * invoice. payment_hash ( ) ) . to_byte_array ( ) ) ;
693
- let payment_hash = PaymentHash ( ( * invoice. payment_hash ( ) ) . to_byte_array ( ) ) ;
694
795
let payment_secret = Some ( * invoice. payment_secret ( ) ) ;
796
+ let zero_amt_invoice =
797
+ invoice. amount_milli_satoshis ( ) . is_none ( ) || invoice. amount_milli_satoshis ( ) == Some ( 0 ) ;
798
+ let pay_params_opt = if zero_amt_invoice {
799
+ if let Some ( amt_msat) = required_amount_msat {
800
+ payment_parameters_from_zero_amount_invoice ( invoice, amt_msat)
801
+ } else {
802
+ println ! ( "Need an amount for the given 0-value invoice" ) ;
803
+ print ! ( "> " ) ;
804
+ return ;
805
+ }
806
+ } else {
807
+ if required_amount_msat. is_some ( ) && invoice. amount_milli_satoshis ( ) != required_amount_msat
808
+ {
809
+ println ! (
810
+ "Amount didn't match invoice value of {}msat" ,
811
+ invoice. amount_milli_satoshis( ) . unwrap_or( 0 )
812
+ ) ;
813
+ print ! ( "> " ) ;
814
+ return ;
815
+ }
816
+ payment_parameters_from_invoice ( invoice)
817
+ } ;
818
+ let ( payment_hash, recipient_onion, route_params) = match pay_params_opt {
819
+ Ok ( res) => res,
820
+ Err ( e) => {
821
+ println ! ( "Failed to parse invoice" ) ;
822
+ print ! ( "> " ) ;
823
+ return ;
824
+ }
825
+ } ;
695
826
outbound_payments. payments . insert (
696
827
payment_id,
697
828
PaymentInfo {
@@ -703,42 +834,6 @@ fn send_payment(
703
834
) ;
704
835
fs_store. write ( "" , "" , OUTBOUND_PAYMENTS_FNAME , & outbound_payments. encode ( ) ) . unwrap ( ) ;
705
836
706
- let mut recipient_onion = RecipientOnionFields :: secret_only ( * invoice. payment_secret ( ) ) ;
707
- recipient_onion. payment_metadata = invoice. payment_metadata ( ) . map ( |v| v. clone ( ) ) ;
708
- let mut payment_params = match PaymentParameters :: from_node_id (
709
- invoice. recover_payee_pub_key ( ) ,
710
- invoice. min_final_cltv_expiry_delta ( ) as u32 ,
711
- )
712
- . with_expiry_time (
713
- invoice. duration_since_epoch ( ) . as_secs ( ) . saturating_add ( invoice. expiry_time ( ) . as_secs ( ) ) ,
714
- )
715
- . with_route_hints ( invoice. route_hints ( ) )
716
- {
717
- Err ( e) => {
718
- println ! ( "ERROR: Could not process invoice to prepare payment parameters, {:?}, invoice: {:?}" , e, invoice) ;
719
- return ;
720
- }
721
- Ok ( p) => p,
722
- } ;
723
- if let Some ( features) = invoice. features ( ) {
724
- payment_params = match payment_params. with_bolt11_features ( features. clone ( ) ) {
725
- Err ( e) => {
726
- println ! ( "ERROR: Could not build BOLT11 payment parameters! {:?}" , e) ;
727
- return ;
728
- }
729
- Ok ( p) => p,
730
- } ;
731
- }
732
- let invoice_amount = match invoice. amount_milli_satoshis ( ) {
733
- None => {
734
- println ! ( "ERROR: An invoice with an amount is expected; {:?}" , invoice) ;
735
- return ;
736
- }
737
- Some ( a) => a,
738
- } ;
739
- let route_params =
740
- RouteParameters :: from_payment_params_and_value ( payment_params, invoice_amount) ;
741
-
742
837
match channel_manager. send_payment (
743
838
payment_hash,
744
839
recipient_onion,
0 commit comments