@@ -11,21 +11,68 @@ use std::str::FromStr;
1111use std:: sync:: Arc ;
1212
1313use hex:: prelude:: * ;
14+ use ldk_node:: lightning:: offers:: invoice:: Bolt12Invoice ;
1415use ldk_node:: lightning_invoice:: Bolt11Invoice ;
15- use ldk_node:: lightning_types:: features:: Bolt11InvoiceFeatures ;
16+ use ldk_node:: lightning_types:: features:: { Bolt11InvoiceFeatures , Bolt12InvoiceFeatures } ;
1617use ldk_server_grpc:: api:: { DecodeInvoiceRequest , DecodeInvoiceResponse } ;
1718use ldk_server_grpc:: types:: { Bolt11HopHint , Bolt11RouteHint } ;
1819
1920use crate :: api:: decode_features;
2021use crate :: api:: error:: LdkServerError ;
2122use crate :: service:: Context ;
2223
24+ const INVOICE_KIND_BOLT11 : & str = "bolt11" ;
25+ const INVOICE_KIND_BOLT12 : & str = "bolt12" ;
26+
2327pub ( crate ) async fn handle_decode_invoice_request (
2428 _context : Arc < Context > , request : DecodeInvoiceRequest ,
2529) -> Result < DecodeInvoiceResponse , LdkServerError > {
26- let invoice = Bolt11Invoice :: from_str ( request. invoice . as_str ( ) )
27- . map_err ( |_| ldk_node:: NodeError :: InvalidInvoice ) ?;
30+ decode_invoice ( request. invoice . as_str ( ) )
31+ }
32+
33+ /// Decodes either a BOLT11 invoice string or a hex-encoded BOLT12 invoice.
34+ fn decode_invoice ( invoice : & str ) -> Result < DecodeInvoiceResponse , LdkServerError > {
35+ if let Ok ( bolt11_invoice) = Bolt11Invoice :: from_str ( invoice) {
36+ return Ok ( decode_bolt11_invoice ( & bolt11_invoice) ) ;
37+ }
38+
39+ if let Some ( response) = decode_bolt12_invoice ( invoice) {
40+ return Ok ( response) ;
41+ }
42+
43+ Err ( ldk_node:: NodeError :: InvalidInvoice . into ( ) )
44+ }
45+
46+ /// Attempts to decode `invoice` as a hex-encoded BOLT12 invoice.
47+ ///
48+ /// Unlike offers and BOLT11 invoices, a BOLT12 invoice has no human-readable string
49+ /// encoding — it is exchanged as raw bytes — so the input is expected to be hex-encoded.
50+ /// Fields that do not apply to BOLT12 invoices (e.g. `payment_secret`, `route_hints`) are
51+ /// left at their default empty values.
52+ fn decode_bolt12_invoice ( invoice : & str ) -> Option < DecodeInvoiceResponse > {
53+ let bytes = Vec :: < u8 > :: from_hex ( invoice) . ok ( ) ?;
54+ let invoice = Bolt12Invoice :: try_from ( bytes) . ok ( ) ?;
2855
56+ let features = decode_features ( invoice. invoice_features ( ) . le_flags ( ) , |bytes| {
57+ Bolt12InvoiceFeatures :: from_le_bytes ( bytes) . to_string ( )
58+ } ) ;
59+
60+ Some ( DecodeInvoiceResponse {
61+ destination : invoice. signing_pubkey ( ) . to_string ( ) ,
62+ payment_hash : invoice. payment_hash ( ) . 0 . to_lower_hex_string ( ) ,
63+ amount_msat : Some ( invoice. amount_msats ( ) ) ,
64+ timestamp : invoice. created_at ( ) . as_secs ( ) ,
65+ expiry : invoice. relative_expiry ( ) . as_secs ( ) ,
66+ description : invoice. description ( ) . map ( |d| d. to_string ( ) ) ,
67+ fallback_address : invoice. fallbacks ( ) . into_iter ( ) . next ( ) . map ( |a| a. to_string ( ) ) ,
68+ features,
69+ is_expired : invoice. is_expired ( ) ,
70+ kind : INVOICE_KIND_BOLT12 . to_string ( ) ,
71+ ..Default :: default ( )
72+ } )
73+ }
74+
75+ fn decode_bolt11_invoice ( invoice : & Bolt11Invoice ) -> DecodeInvoiceResponse {
2976 let destination = invoice. get_payee_pub_key ( ) . to_string ( ) ;
3077 let payment_hash = invoice. payment_hash ( ) . 0 . to_lower_hex_string ( ) ;
3178 let amount_msat = invoice. amount_milli_satoshis ( ) ;
@@ -85,7 +132,7 @@ pub(crate) async fn handle_decode_invoice_request(
85132
86133 let is_expired = invoice. is_expired ( ) ;
87134
88- Ok ( DecodeInvoiceResponse {
135+ DecodeInvoiceResponse {
89136 destination,
90137 payment_hash,
91138 amount_msat,
@@ -101,5 +148,94 @@ pub(crate) async fn handle_decode_invoice_request(
101148 currency,
102149 payment_metadata,
103150 is_expired,
104- } )
151+ kind : INVOICE_KIND_BOLT11 . to_string ( ) ,
152+ }
153+ }
154+
155+ #[ cfg( test) ]
156+ mod tests {
157+ use ldk_node:: lightning:: bitcoin:: secp256k1:: { Keypair , PublicKey , Secp256k1 , SecretKey } ;
158+ use ldk_node:: lightning:: blinded_path:: payment:: { BlindedPayInfo , BlindedPaymentPath } ;
159+ use ldk_node:: lightning:: blinded_path:: BlindedHop ;
160+ use ldk_node:: lightning:: offers:: invoice:: UnsignedBolt12Invoice ;
161+ use ldk_node:: lightning:: offers:: refund:: RefundBuilder ;
162+ use ldk_node:: lightning:: types:: features:: BlindedHopFeatures ;
163+ use ldk_node:: lightning:: types:: payment:: PaymentHash ;
164+ use ldk_node:: lightning:: util:: ser:: Writeable ;
165+
166+ use super :: * ;
167+
168+ fn pubkey ( byte : u8 ) -> PublicKey {
169+ let secp = Secp256k1 :: new ( ) ;
170+ PublicKey :: from_secret_key ( & secp, & SecretKey :: from_slice ( & [ byte; 32 ] ) . unwrap ( ) )
171+ }
172+
173+ /// The keypair the sample BOLT12 invoice is signed with; its public key is the
174+ /// invoice's `signing_pubkey`.
175+ fn signing_keypair ( ) -> Keypair {
176+ let secp = Secp256k1 :: new ( ) ;
177+ Keypair :: from_secret_key ( & secp, & SecretKey :: from_slice ( & [ 43 ; 32 ] ) . unwrap ( ) )
178+ }
179+
180+ /// Builds a signed BOLT12 invoice and returns it hex-encoded, matching how a BOLT12
181+ /// invoice would be supplied to `DecodeInvoice`.
182+ fn sample_bolt12_invoice_hex ( ) -> String {
183+ let secp = Secp256k1 :: new ( ) ;
184+ let keys = signing_keypair ( ) ;
185+
186+ let payment_paths = vec ! [ BlindedPaymentPath :: from_blinded_path_and_payinfo(
187+ pubkey( 40 ) ,
188+ pubkey( 41 ) ,
189+ vec![
190+ BlindedHop { blinded_node_id: pubkey( 43 ) , encrypted_payload: vec![ 0 ; 43 ] } ,
191+ BlindedHop { blinded_node_id: pubkey( 44 ) , encrypted_payload: vec![ 0 ; 44 ] } ,
192+ ] ,
193+ BlindedPayInfo {
194+ fee_base_msat: 1 ,
195+ fee_proportional_millionths: 1_000 ,
196+ cltv_expiry_delta: 42 ,
197+ htlc_minimum_msat: 100 ,
198+ htlc_maximum_msat: 1_000_000_000_000 ,
199+ features: BlindedHopFeatures :: empty( ) ,
200+ } ,
201+ ) ] ;
202+
203+ let refund = RefundBuilder :: new ( vec ! [ 1 ; 32 ] , pubkey ( 42 ) , 1_000 ) . unwrap ( ) . build ( ) . unwrap ( ) ;
204+ let invoice = refund
205+ . respond_with ( payment_paths, PaymentHash ( [ 42 ; 32 ] ) , keys. public_key ( ) )
206+ . unwrap ( )
207+ . relative_expiry ( 3600 )
208+ . build ( )
209+ . unwrap ( )
210+ . sign ( |message : & UnsignedBolt12Invoice | {
211+ Ok :: < _ , ( ) > ( secp. sign_schnorr_no_aux_rand ( message. as_ref ( ) . as_digest ( ) , & keys) )
212+ } )
213+ . unwrap ( ) ;
214+
215+ let mut buffer = Vec :: new ( ) ;
216+ invoice. write ( & mut buffer) . unwrap ( ) ;
217+ buffer. to_lower_hex_string ( )
218+ }
219+
220+ #[ test]
221+ fn rejects_unparseable_input ( ) {
222+ assert ! ( decode_invoice( "not an invoice" ) . is_err( ) ) ;
223+ }
224+
225+ #[ test]
226+ fn rejects_hex_that_is_not_a_bolt12_invoice ( ) {
227+ // Valid hex, but not a BOLT12 invoice TLV stream.
228+ assert ! ( decode_invoice( "00010203" ) . is_err( ) ) ;
229+ }
230+
231+ #[ test]
232+ fn decodes_bolt12_invoice_and_populates_fields ( ) {
233+ let response = decode_invoice ( & sample_bolt12_invoice_hex ( ) ) . unwrap ( ) ;
234+ assert_eq ! ( response. kind, INVOICE_KIND_BOLT12 ) ;
235+ assert_eq ! ( response. destination, signing_keypair( ) . public_key( ) . to_string( ) ) ;
236+ assert_eq ! ( response. payment_hash, "2a" . repeat( 32 ) ) ;
237+ assert_eq ! ( response. amount_msat, Some ( 1_000 ) ) ;
238+ assert_eq ! ( response. expiry, 3600 ) ;
239+ assert ! ( !response. is_expired) ;
240+ }
105241}
0 commit comments