@@ -11,6 +11,7 @@ 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 ;
1516use ldk_node:: lightning_types:: features:: Bolt11InvoiceFeatures ;
1617use ldk_server_grpc:: api:: { DecodeInvoiceRequest , DecodeInvoiceResponse } ;
@@ -20,12 +21,40 @@ use 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+ /// Only the `kind` field is populated for a BOLT12 invoice.
51+ fn decode_bolt12_invoice ( invoice : & str ) -> Option < DecodeInvoiceResponse > {
52+ let bytes = Vec :: < u8 > :: from_hex ( invoice) . ok ( ) ?;
53+ Bolt12Invoice :: try_from ( bytes) . ok ( ) ?;
54+ Some ( DecodeInvoiceResponse { kind : INVOICE_KIND_BOLT12 . to_string ( ) , ..Default :: default ( ) } )
55+ }
2856
57+ fn decode_bolt11_invoice ( invoice : & Bolt11Invoice ) -> DecodeInvoiceResponse {
2958 let destination = invoice. get_payee_pub_key ( ) . to_string ( ) ;
3059 let payment_hash = invoice. payment_hash ( ) . 0 . to_lower_hex_string ( ) ;
3160 let amount_msat = invoice. amount_milli_satoshis ( ) ;
@@ -85,7 +114,7 @@ pub(crate) async fn handle_decode_invoice_request(
85114
86115 let is_expired = invoice. is_expired ( ) ;
87116
88- Ok ( DecodeInvoiceResponse {
117+ DecodeInvoiceResponse {
89118 destination,
90119 payment_hash,
91120 amount_msat,
@@ -101,5 +130,81 @@ pub(crate) async fn handle_decode_invoice_request(
101130 currency,
102131 payment_metadata,
103132 is_expired,
104- } )
133+ kind : INVOICE_KIND_BOLT11 . to_string ( ) ,
134+ }
135+ }
136+
137+ #[ cfg( test) ]
138+ mod tests {
139+ use ldk_node:: lightning:: bitcoin:: secp256k1:: { Keypair , PublicKey , Secp256k1 , SecretKey } ;
140+ use ldk_node:: lightning:: blinded_path:: payment:: { BlindedPayInfo , BlindedPaymentPath } ;
141+ use ldk_node:: lightning:: blinded_path:: BlindedHop ;
142+ use ldk_node:: lightning:: offers:: invoice:: UnsignedBolt12Invoice ;
143+ use ldk_node:: lightning:: offers:: refund:: RefundBuilder ;
144+ use ldk_node:: lightning:: types:: features:: BlindedHopFeatures ;
145+ use ldk_node:: lightning:: types:: payment:: PaymentHash ;
146+ use ldk_node:: lightning:: util:: ser:: Writeable ;
147+
148+ use super :: * ;
149+
150+ fn pubkey ( byte : u8 ) -> PublicKey {
151+ let secp = Secp256k1 :: new ( ) ;
152+ PublicKey :: from_secret_key ( & secp, & SecretKey :: from_slice ( & [ byte; 32 ] ) . unwrap ( ) )
153+ }
154+
155+ /// Builds a signed BOLT12 invoice and returns it hex-encoded, matching how a BOLT12
156+ /// invoice would be supplied to `DecodeInvoice`.
157+ fn sample_bolt12_invoice_hex ( ) -> String {
158+ let secp = Secp256k1 :: new ( ) ;
159+ let keys = Keypair :: from_secret_key ( & secp, & SecretKey :: from_slice ( & [ 43 ; 32 ] ) . unwrap ( ) ) ;
160+
161+ let payment_paths = vec ! [ BlindedPaymentPath :: from_blinded_path_and_payinfo(
162+ pubkey( 40 ) ,
163+ pubkey( 41 ) ,
164+ vec![
165+ BlindedHop { blinded_node_id: pubkey( 43 ) , encrypted_payload: vec![ 0 ; 43 ] } ,
166+ BlindedHop { blinded_node_id: pubkey( 44 ) , encrypted_payload: vec![ 0 ; 44 ] } ,
167+ ] ,
168+ BlindedPayInfo {
169+ fee_base_msat: 1 ,
170+ fee_proportional_millionths: 1_000 ,
171+ cltv_expiry_delta: 42 ,
172+ htlc_minimum_msat: 100 ,
173+ htlc_maximum_msat: 1_000_000_000_000 ,
174+ features: BlindedHopFeatures :: empty( ) ,
175+ } ,
176+ ) ] ;
177+
178+ let refund = RefundBuilder :: new ( vec ! [ 1 ; 32 ] , pubkey ( 42 ) , 1_000 ) . unwrap ( ) . build ( ) . unwrap ( ) ;
179+ let invoice = refund
180+ . respond_with ( payment_paths, PaymentHash ( [ 42 ; 32 ] ) , keys. public_key ( ) )
181+ . unwrap ( )
182+ . build ( )
183+ . unwrap ( )
184+ . sign ( |message : & UnsignedBolt12Invoice | {
185+ Ok :: < _ , ( ) > ( secp. sign_schnorr_no_aux_rand ( message. as_ref ( ) . as_digest ( ) , & keys) )
186+ } )
187+ . unwrap ( ) ;
188+
189+ let mut buffer = Vec :: new ( ) ;
190+ invoice. write ( & mut buffer) . unwrap ( ) ;
191+ buffer. to_lower_hex_string ( )
192+ }
193+
194+ #[ test]
195+ fn rejects_unparseable_input ( ) {
196+ assert ! ( decode_invoice( "not an invoice" ) . is_err( ) ) ;
197+ }
198+
199+ #[ test]
200+ fn rejects_hex_that_is_not_a_bolt12_invoice ( ) {
201+ // Valid hex, but not a BOLT12 invoice TLV stream.
202+ assert ! ( decode_invoice( "00010203" ) . is_err( ) ) ;
203+ }
204+
205+ #[ test]
206+ fn decodes_bolt12_invoice_with_bolt12_kind ( ) {
207+ let response = decode_invoice ( & sample_bolt12_invoice_hex ( ) ) . unwrap ( ) ;
208+ assert_eq ! ( response. kind, INVOICE_KIND_BOLT12 ) ;
209+ }
105210}
0 commit comments