@@ -347,7 +347,8 @@ impl ConfigurableAmountPaymentInstructions {
347
347
debug_assert ! ( inner. onchain_amt. is_none( ) ) ;
348
348
debug_assert ! ( inner. pop_callback. is_none( ) ) ;
349
349
debug_assert ! ( inner. hrn_proof. is_none( ) ) ;
350
- let bolt11 = resolver. resolve_lnurl ( callback, amount, expected_desc_hash) . await ?;
350
+ let bolt11 =
351
+ resolver. resolve_lnurl_to_invoice ( callback, amount, expected_desc_hash) . await ?;
351
352
if bolt11. amount_milli_satoshis ( ) != Some ( amount. milli_sats ( ) ) {
352
353
return Err ( "LNURL resolution resulted in a BOLT 11 invoice with the wrong amount" ) ;
353
354
}
@@ -428,6 +429,8 @@ pub enum ParseError {
428
429
InvalidBolt12 ( Bolt12ParseError ) ,
429
430
/// An invalid on-chain address was encountered
430
431
InvalidOnChain ( address:: ParseError ) ,
432
+ /// An invalid lnurl was encountered
433
+ InvalidLnurl ( & ' static str ) ,
431
434
/// The payment instructions encoded instructions for a network other than the one specified.
432
435
WrongNetwork ,
433
436
/// Different parts of the payment instructions were inconsistent.
@@ -944,6 +947,47 @@ impl PaymentInstructions {
944
947
) )
945
948
} ,
946
949
}
950
+ } else if let Some ( idx) = instructions. to_lowercase ( ) . rfind ( "lnurl" ) {
951
+ let lnurl_str = & instructions[ idx..] ;
952
+ if let Ok ( ( _, data) ) = bitcoin:: bech32:: decode ( lnurl_str) {
953
+ let url = String :: from_utf8 ( data)
954
+ . map_err ( |_| ParseError :: InvalidLnurl ( "Not utf-8 encoded string" ) ) ?;
955
+ let resolution = hrn_resolver. resolve_lnurl ( & url) . await ;
956
+ let resolution = resolution. map_err ( ParseError :: HrnResolutionError ) ?;
957
+ match resolution {
958
+ HrnResolution :: DNSSEC { .. } => Err ( ParseError :: HrnResolutionError (
959
+ "Unexpected return when resolving lnurl" ,
960
+ ) ) ,
961
+ HrnResolution :: LNURLPay {
962
+ min_value,
963
+ max_value,
964
+ expected_description_hash,
965
+ recipient_description,
966
+ callback,
967
+ } => {
968
+ let inner = PaymentInstructionsImpl {
969
+ description : recipient_description,
970
+ methods : Vec :: new ( ) ,
971
+ lnurl : Some ( (
972
+ callback,
973
+ expected_description_hash,
974
+ min_value,
975
+ max_value,
976
+ ) ) ,
977
+ onchain_amt : None ,
978
+ ln_amt : None ,
979
+ pop_callback : None ,
980
+ hrn : None ,
981
+ hrn_proof : None ,
982
+ } ;
983
+ Ok ( PaymentInstructions :: ConfigurableAmount (
984
+ ConfigurableAmountPaymentInstructions { inner } ,
985
+ ) )
986
+ } ,
987
+ }
988
+ } else {
989
+ parse_resolved_instructions ( instructions, network, supports_pops, None , None )
990
+ }
947
991
} else {
948
992
parse_resolved_instructions ( instructions, network, supports_pops, None , None )
949
993
}
@@ -966,6 +1010,13 @@ mod tests {
966
1010
const SAMPLE_OFFER : & str = "lno1qgs0v8hw8d368q9yw7sx8tejk2aujlyll8cp7tzzyh5h8xyppqqqqqqgqvqcdgq2qenxzatrv46pvggrv64u366d5c0rr2xjc3fq6vw2hh6ce3f9p7z4v4ee0u7avfynjw9q" ;
967
1011
const SAMPLE_BIP21 : & str = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz" ;
968
1012
1013
+ #[ cfg( feature = "http" ) ]
1014
+ const SAMPLE_LNURL : & str = "LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK" ;
1015
+ #[ cfg( feature = "http" ) ]
1016
+ const SAMPLE_LNURL_LN_PREFIX : & str = "lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK" ;
1017
+ #[ cfg( feature = "http" ) ]
1018
+ const SAMPLE_LNURL_FALLBACK : & str = "https://service.com/giftcard/redeem?id=123&lightning=LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK" ;
1019
+
969
1020
const SAMPLE_BIP21_WITH_INVOICE : & str = "bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&lightning=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6" ;
970
1021
#[ cfg( not( feature = "std" ) ) ]
971
1022
const SAMPLE_BIP21_WITH_INVOICE_ADDR : & str = "bc1qylh3u67j673h6y6alv70m0pl2yz53tzhvxgg7u" ;
@@ -1277,4 +1328,33 @@ mod tests {
1277
1328
Err ( ParseError :: InstructionsExpired ) ,
1278
1329
) ;
1279
1330
}
1331
+
1332
+ #[ cfg( feature = "http" ) ]
1333
+ async fn test_lnurl ( str : & str ) {
1334
+ let parsed = PaymentInstructions :: parse (
1335
+ str,
1336
+ Network :: Signet ,
1337
+ & http_resolver:: HTTPHrnResolver ,
1338
+ false ,
1339
+ )
1340
+ . await
1341
+ . unwrap ( ) ;
1342
+
1343
+ let parsed = match parsed {
1344
+ PaymentInstructions :: ConfigurableAmount ( parsed) => parsed,
1345
+ _ => panic ! ( ) ,
1346
+ } ;
1347
+
1348
+ assert_eq ! ( parsed. methods( ) . count( ) , 1 ) ;
1349
+ assert_eq ! ( parsed. min_amt( ) , Some ( Amount :: from_milli_sats( 1000 ) . unwrap( ) ) ) ;
1350
+ assert_eq ! ( parsed. max_amt( ) , Some ( Amount :: from_milli_sats( 11000000000 ) . unwrap( ) ) ) ;
1351
+ }
1352
+
1353
+ #[ cfg( feature = "http" ) ]
1354
+ #[ tokio:: test]
1355
+ async fn parse_lnurl ( ) {
1356
+ test_lnurl ( SAMPLE_LNURL ) . await ;
1357
+ test_lnurl ( SAMPLE_LNURL_LN_PREFIX ) . await ;
1358
+ test_lnurl ( SAMPLE_LNURL_FALLBACK ) . await ;
1359
+ }
1280
1360
}
0 commit comments