@@ -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,56 @@ impl PaymentInstructions {
944947 ) )
945948 } ,
946949 }
950+ } else if let Some ( idx) = instructions. to_lowercase ( ) . rfind ( "lnurl" ) {
951+ let 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+ let decode = bitcoin:: bech32:: decode ( lnurl_str) . or_else ( |e| {
955+ if let Some ( idx) = lnurl_str. find ( "&" ) {
956+ bitcoin:: bech32:: decode ( & lnurl_str[ ..idx] )
957+ } else {
958+ Err ( e)
959+ }
960+ } ) ;
961+ if let Ok ( ( _, data) ) = decode {
962+ let url = String :: from_utf8 ( data)
963+ . map_err ( |_| ParseError :: InvalidLnurl ( "Not utf-8 encoded string" ) ) ?;
964+ let resolution = hrn_resolver. resolve_lnurl ( & url) . await ;
965+ let resolution = resolution. map_err ( ParseError :: HrnResolutionError ) ?;
966+ match resolution {
967+ HrnResolution :: DNSSEC { .. } => Err ( ParseError :: HrnResolutionError (
968+ "Unexpected return when resolving lnurl" ,
969+ ) ) ,
970+ HrnResolution :: LNURLPay {
971+ min_value,
972+ max_value,
973+ expected_description_hash,
974+ recipient_description,
975+ callback,
976+ } => {
977+ let inner = PaymentInstructionsImpl {
978+ description : recipient_description,
979+ methods : Vec :: new ( ) ,
980+ lnurl : Some ( (
981+ callback,
982+ expected_description_hash,
983+ min_value,
984+ max_value,
985+ ) ) ,
986+ onchain_amt : None ,
987+ ln_amt : None ,
988+ pop_callback : None ,
989+ hrn : None ,
990+ hrn_proof : None ,
991+ } ;
992+ Ok ( PaymentInstructions :: ConfigurableAmount (
993+ ConfigurableAmountPaymentInstructions { inner } ,
994+ ) )
995+ } ,
996+ }
997+ } else {
998+ parse_resolved_instructions ( instructions, network, supports_pops, None , None )
999+ }
9471000 } else {
9481001 parse_resolved_instructions ( instructions, network, supports_pops, None , None )
9491002 }
@@ -966,6 +1019,15 @@ mod tests {
9661019 const SAMPLE_OFFER : & str = "lno1qgs0v8hw8d368q9yw7sx8tejk2aujlyll8cp7tzzyh5h8xyppqqqqqqgqvqcdgq2qenxzatrv46pvggrv64u366d5c0rr2xjc3fq6vw2hh6ce3f9p7z4v4ee0u7avfynjw9q" ;
9671020 const SAMPLE_BIP21 : & str = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz" ;
9681021
1022+ #[ cfg( feature = "http" ) ]
1023+ const SAMPLE_LNURL : & str = "LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK" ;
1024+ #[ cfg( feature = "http" ) ]
1025+ const SAMPLE_LNURL_LN_PREFIX : & str = "lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK" ;
1026+ #[ cfg( feature = "http" ) ]
1027+ const SAMPLE_LNURL_FALLBACK : & str = "https://service.com/giftcard/redeem?id=123&lightning=LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK" ;
1028+ #[ cfg( feature = "http" ) ]
1029+ const SAMPLE_LNURL_FALLBACK_WITH_EXTRAS : & str = "https://service.com/giftcard/redeem?id=123&lightning=LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK&extra=my_extra_param" ;
1030+
9691031 const SAMPLE_BIP21_WITH_INVOICE : & str = "bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&lightning=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6" ;
9701032 #[ cfg( not( feature = "std" ) ) ]
9711033 const SAMPLE_BIP21_WITH_INVOICE_ADDR : & str = "bc1qylh3u67j673h6y6alv70m0pl2yz53tzhvxgg7u" ;
@@ -1277,4 +1339,34 @@ mod tests {
12771339 Err ( ParseError :: InstructionsExpired ) ,
12781340 ) ;
12791341 }
1342+
1343+ #[ cfg( feature = "http" ) ]
1344+ async fn test_lnurl ( str : & str ) {
1345+ let parsed = PaymentInstructions :: parse (
1346+ str,
1347+ Network :: Signet ,
1348+ & http_resolver:: HTTPHrnResolver ,
1349+ false ,
1350+ )
1351+ . await
1352+ . unwrap ( ) ;
1353+
1354+ let parsed = match parsed {
1355+ PaymentInstructions :: ConfigurableAmount ( parsed) => parsed,
1356+ _ => panic ! ( ) ,
1357+ } ;
1358+
1359+ assert_eq ! ( parsed. methods( ) . count( ) , 1 ) ;
1360+ assert_eq ! ( parsed. min_amt( ) , Some ( Amount :: from_milli_sats( 1000 ) . unwrap( ) ) ) ;
1361+ assert_eq ! ( parsed. max_amt( ) , Some ( Amount :: from_milli_sats( 11000000000 ) . unwrap( ) ) ) ;
1362+ }
1363+
1364+ #[ cfg( feature = "http" ) ]
1365+ #[ tokio:: test]
1366+ async fn parse_lnurl ( ) {
1367+ test_lnurl ( SAMPLE_LNURL ) . await ;
1368+ test_lnurl ( SAMPLE_LNURL_LN_PREFIX ) . await ;
1369+ test_lnurl ( SAMPLE_LNURL_FALLBACK ) . await ;
1370+ test_lnurl ( SAMPLE_LNURL_FALLBACK_WITH_EXTRAS ) . await ;
1371+ }
12801372}
0 commit comments