@@ -347,7 +347,8 @@ impl ConfigurableAmountPaymentInstructions {
347347 debug_assert ! ( inner. onchain_amt. is_none( ) ) ;
348348 debug_assert ! ( inner. pop_callback. is_none( ) ) ;
349349 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 ?;
351352 if bolt11. amount_milli_satoshis ( ) != Some ( amount. milli_sats ( ) ) {
352353 return Err ( "LNURL resolution resulted in a BOLT 11 invoice with the wrong amount" ) ;
353354 }
@@ -428,6 +429,8 @@ pub enum ParseError {
428429 InvalidBolt12 ( Bolt12ParseError ) ,
429430 /// An invalid on-chain address was encountered
430431 InvalidOnChain ( address:: ParseError ) ,
432+ /// An invalid lnurl was encountered
433+ InvalidLnurl ( & ' static str ) ,
431434 /// The payment instructions encoded instructions for a network other than the one specified.
432435 WrongNetwork ,
433436 /// Different parts of the payment instructions were inconsistent.
@@ -944,6 +947,55 @@ impl PaymentInstructions {
944947 ) )
945948 } ,
946949 }
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+ }
947999 } else {
9481000 parse_resolved_instructions ( instructions, network, supports_pops, None , None )
9491001 }
@@ -966,6 +1018,19 @@ mod tests {
9661018 const SAMPLE_OFFER : & str = "lno1qgs0v8hw8d368q9yw7sx8tejk2aujlyll8cp7tzzyh5h8xyppqqqqqqgqvqcdgq2qenxzatrv46pvggrv64u366d5c0rr2xjc3fq6vw2hh6ce3f9p7z4v4ee0u7avfynjw9q" ;
9671019 const SAMPLE_BIP21 : & str = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz" ;
9681020
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+
9691034 const SAMPLE_BIP21_WITH_INVOICE : & str = "bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&lightning=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6" ;
9701035 #[ cfg( not( feature = "std" ) ) ]
9711036 const SAMPLE_BIP21_WITH_INVOICE_ADDR : & str = "bc1qylh3u67j673h6y6alv70m0pl2yz53tzhvxgg7u" ;
@@ -1277,4 +1342,36 @@ mod tests {
12771342 Err ( ParseError :: InstructionsExpired ) ,
12781343 ) ;
12791344 }
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+ }
12801377}
0 commit comments