@@ -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,55 @@ impl PaymentInstructions {
944
947
) )
945
948
} ,
946
949
}
950
+ } else if let Some ( idx) = instructions. to_lowercase ( ) . rfind ( "lnurl" ) {
951
+ let mut lnurl_str = & instructions[ idx..] ;
952
+ // first try to decode as a bech32-encoded lnurl, if that fails, try to drop a
953
+ // trailing `&` and decode again, this could a http query param
954
+ if let Some ( idx) = lnurl_str. find ( '&' ) {
955
+ lnurl_str = & lnurl_str[ ..idx] ;
956
+ }
957
+ if let Some ( idx) = lnurl_str. find ( '#' ) {
958
+ lnurl_str = & lnurl_str[ ..idx] ;
959
+ }
960
+ if let Ok ( ( _, data) ) = bitcoin:: bech32:: decode ( lnurl_str) {
961
+ let url = String :: from_utf8 ( data)
962
+ . map_err ( |_| ParseError :: InvalidLnurl ( "Not utf-8 encoded string" ) ) ?;
963
+ let resolution = hrn_resolver. resolve_lnurl ( & url) . await ;
964
+ let resolution = resolution. map_err ( ParseError :: HrnResolutionError ) ?;
965
+ match resolution {
966
+ HrnResolution :: DNSSEC { .. } => Err ( ParseError :: HrnResolutionError (
967
+ "Unexpected return when resolving lnurl" ,
968
+ ) ) ,
969
+ HrnResolution :: LNURLPay {
970
+ min_value,
971
+ max_value,
972
+ expected_description_hash,
973
+ recipient_description,
974
+ callback,
975
+ } => {
976
+ let inner = PaymentInstructionsImpl {
977
+ description : recipient_description,
978
+ methods : Vec :: new ( ) ,
979
+ lnurl : Some ( (
980
+ callback,
981
+ expected_description_hash,
982
+ min_value,
983
+ max_value,
984
+ ) ) ,
985
+ onchain_amt : None ,
986
+ ln_amt : None ,
987
+ pop_callback : None ,
988
+ hrn : None ,
989
+ hrn_proof : None ,
990
+ } ;
991
+ Ok ( PaymentInstructions :: ConfigurableAmount (
992
+ ConfigurableAmountPaymentInstructions { inner } ,
993
+ ) )
994
+ } ,
995
+ }
996
+ } else {
997
+ parse_resolved_instructions ( instructions, network, supports_pops, None , None )
998
+ }
947
999
} else {
948
1000
parse_resolved_instructions ( instructions, network, supports_pops, None , None )
949
1001
}
@@ -966,6 +1018,19 @@ mod tests {
966
1018
const SAMPLE_OFFER : & str = "lno1qgs0v8hw8d368q9yw7sx8tejk2aujlyll8cp7tzzyh5h8xyppqqqqqqgqvqcdgq2qenxzatrv46pvggrv64u366d5c0rr2xjc3fq6vw2hh6ce3f9p7z4v4ee0u7avfynjw9q" ;
967
1019
const SAMPLE_BIP21 : & str = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz" ;
968
1020
1021
+ #[ cfg( feature = "http" ) ]
1022
+ const SAMPLE_LNURL : & str = "LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK" ;
1023
+ #[ cfg( feature = "http" ) ]
1024
+ const SAMPLE_LNURL_LN_PREFIX : & str = "lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK" ;
1025
+ #[ cfg( feature = "http" ) ]
1026
+ const SAMPLE_LNURL_FALLBACK : & str = "https://service.com/giftcard/redeem?id=123&lightning=LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK" ;
1027
+ #[ cfg( feature = "http" ) ]
1028
+ const SAMPLE_LNURL_FALLBACK_WITH_AND : & str = "https://service.com/giftcard/redeem?id=123&lightning=LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK&extra=my_extra_param" ;
1029
+ #[ cfg( feature = "http" ) ]
1030
+ const SAMPLE_LNURL_FALLBACK_WITH_HASHTAG : & str = "https://service.com/giftcard/redeem?id=123&lightning=LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK#extra=my_extra_param" ;
1031
+ #[ cfg( feature = "http" ) ]
1032
+ const SAMPLE_LNURL_FALLBACK_WITH_BOTH : & str = "https://service.com/giftcard/redeem?id=123&lightning=LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK&extra=my_extra_param#extra2=another_extra_param" ;
1033
+
969
1034
const SAMPLE_BIP21_WITH_INVOICE : & str = "bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&lightning=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6" ;
970
1035
#[ cfg( not( feature = "std" ) ) ]
971
1036
const SAMPLE_BIP21_WITH_INVOICE_ADDR : & str = "bc1qylh3u67j673h6y6alv70m0pl2yz53tzhvxgg7u" ;
@@ -1277,4 +1342,36 @@ mod tests {
1277
1342
Err ( ParseError :: InstructionsExpired ) ,
1278
1343
) ;
1279
1344
}
1345
+
1346
+ #[ cfg( feature = "http" ) ]
1347
+ async fn test_lnurl ( str : & str ) {
1348
+ let parsed = PaymentInstructions :: parse (
1349
+ str,
1350
+ Network :: Signet ,
1351
+ & http_resolver:: HTTPHrnResolver ,
1352
+ false ,
1353
+ )
1354
+ . await
1355
+ . unwrap ( ) ;
1356
+
1357
+ let parsed = match parsed {
1358
+ PaymentInstructions :: ConfigurableAmount ( parsed) => parsed,
1359
+ _ => panic ! ( ) ,
1360
+ } ;
1361
+
1362
+ assert_eq ! ( parsed. methods( ) . count( ) , 1 ) ;
1363
+ assert_eq ! ( parsed. min_amt( ) , Some ( Amount :: from_milli_sats( 1000 ) . unwrap( ) ) ) ;
1364
+ assert_eq ! ( parsed. max_amt( ) , Some ( Amount :: from_milli_sats( 11000000000 ) . unwrap( ) ) ) ;
1365
+ }
1366
+
1367
+ #[ cfg( feature = "http" ) ]
1368
+ #[ tokio:: test]
1369
+ async fn parse_lnurl ( ) {
1370
+ test_lnurl ( SAMPLE_LNURL ) . await ;
1371
+ test_lnurl ( SAMPLE_LNURL_LN_PREFIX ) . await ;
1372
+ test_lnurl ( SAMPLE_LNURL_FALLBACK ) . await ;
1373
+ test_lnurl ( SAMPLE_LNURL_FALLBACK_WITH_AND ) . await ;
1374
+ test_lnurl ( SAMPLE_LNURL_FALLBACK_WITH_HASHTAG ) . await ;
1375
+ test_lnurl ( SAMPLE_LNURL_FALLBACK_WITH_BOTH ) . await ;
1376
+ }
1280
1377
}
0 commit comments