From 4e77025ecc327345860323fe4ee32444cc482562 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Fri, 21 Jul 2023 09:48:47 +0200 Subject: [PATCH 1/9] add break and do not check all the prefixes, only until it finds first --- lib/dart_lnurl.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/dart_lnurl.dart b/lib/dart_lnurl.dart index f870907..c33e467 100644 --- a/lib/dart_lnurl.dart +++ b/lib/dart_lnurl.dart @@ -18,11 +18,12 @@ Uri decodeUri(String encodedUrl) { /// The URL doesn't have to be encoded at all as per LUD-17: Protocol schemes and raw (non bech32-encoded) URLs. /// https://github.com/lnurl/luds/blob/luds/17.md /// Handle non bech32-encoded LNURL - final lud17prefixes = ['lnurlw', 'lnurlc', 'lnurlp', 'keyauth']; + final lud17prefixes = ['lnurlw', 'lnurlp', 'keyauth', 'lnurlc']; decodedUri = Uri.parse(encodedUrl); for (final prefix in lud17prefixes) { if (decodedUri.scheme.contains(prefix)) { decodedUri = decodedUri.replace(scheme: prefix); + break; } } if (lud17prefixes.contains(decodedUri.scheme)) { From 3f8ba4eb2ef66c1f58fa358a55df6d6c72374498 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Fri, 21 Jul 2023 17:15:12 +0200 Subject: [PATCH 2/9] add isbech32 to check the encoding of lnurl, add findLnUrlNonBech32 to hangle the nonbech32 lnurls, code refactoring --- lib/dart_lnurl.dart | 22 +++------------------- lib/src/lnurl.dart | 46 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/lib/dart_lnurl.dart b/lib/dart_lnurl.dart index c33e467..dc622d1 100644 --- a/lib/dart_lnurl.dart +++ b/lib/dart_lnurl.dart @@ -14,25 +14,9 @@ export 'src/bech32.dart'; Uri decodeUri(String encodedUrl) { Uri decodedUri; - - /// The URL doesn't have to be encoded at all as per LUD-17: Protocol schemes and raw (non bech32-encoded) URLs. - /// https://github.com/lnurl/luds/blob/luds/17.md - /// Handle non bech32-encoded LNURL - final lud17prefixes = ['lnurlw', 'lnurlp', 'keyauth', 'lnurlc']; - decodedUri = Uri.parse(encodedUrl); - for (final prefix in lud17prefixes) { - if (decodedUri.scheme.contains(prefix)) { - decodedUri = decodedUri.replace(scheme: prefix); - break; - } - } - if (lud17prefixes.contains(decodedUri.scheme)) { - /// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.' - decodedUri = decodedUri.replace( - scheme: decodedUri.host.endsWith('onion') || - decodedUri.host.endsWith('onion.') - ? 'http' - : 'https'); + if (!isbech32(encodedUrl)) { + final String lnUrl = findLnUrlNonBech32(encodedUrl); + decodedUri = Uri.parse(lnUrl); } else { /// Try to parse the input as a lnUrl. Will throw an error if it fails. final lnUrl = findLnUrl(encodedUrl); diff --git a/lib/src/lnurl.dart b/lib/src/lnurl.dart index 98ace54..f49362a 100644 --- a/lib/src/lnurl.dart +++ b/lib/src/lnurl.dart @@ -1,3 +1,5 @@ +import 'package:bech32/bech32.dart'; + /// Parse and return a given lnurl string if it's valid. Will remove /// `lightning:` from the beginning of it if present. String findLnUrl(String input) { @@ -11,3 +13,47 @@ String findLnUrl(String input) { throw ArgumentError('Not a valid lnurl string'); } } + +String findLnUrlNonBech32(String input) { + /// The URL doesn't have to be encoded at all as per LUD-17: Protocol schemes and raw (non bech32-encoded) URLs. + /// https://github.com/lnurl/luds/blob/luds/17.md + /// Handle non bech32-encoded LNURL + + final lud17prefixes = ['lnurlw', 'lnurlp', 'keyauth', 'lnurlc']; + Uri decodedUri = Uri.parse(input); + for (final prefix in lud17prefixes) { + if (decodedUri.scheme.contains(prefix)) { + decodedUri = decodedUri.replace(scheme: prefix); + break; + } + } + if (lud17prefixes.contains(decodedUri.scheme)) { + /// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.' + decodedUri = decodedUri.replace( + scheme: decodedUri.host.endsWith('onion') || + decodedUri.host.endsWith('onion.') + ? 'http' + : 'https'); + } + return decodedUri.toString(); +} + +bool isbech32(String input) { + String bech32 = ''; + final res = new RegExp( + r',*?((lnurl)([0-9]{1,}[a-z0-9]+){1})', + ).allMatches(input.toLowerCase()); + if (res.length == 1) { + try { + bech32 = Bech32Codec() + .decode(res.first.group(0)!.toLowerCase(), res.first.group(0)!.length) + .toString(); + } catch (e) { + throw ArgumentError('Error when decoding bech32 lnurl'); + } + if (!bech32.isEmpty) { + return true; + } + } + return false; +} From ce641a45153cfdc47400049d5d9b30a6d2d40602 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Sun, 23 Jul 2023 17:16:20 +0200 Subject: [PATCH 3/9] code refactoring --- lib/src/lnurl.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/src/lnurl.dart b/lib/src/lnurl.dart index f49362a..3cf8c5d 100644 --- a/lib/src/lnurl.dart +++ b/lib/src/lnurl.dart @@ -39,19 +39,23 @@ String findLnUrlNonBech32(String input) { } bool isbech32(String input) { - String bech32 = ''; - final res = new RegExp( + final match = new RegExp( r',*?((lnurl)([0-9]{1,}[a-z0-9]+){1})', ).allMatches(input.toLowerCase()); - if (res.length == 1) { + if (match.length == 1) { + print(match.first.group(0)); + // get the lnurl string from the regex + String lnurlBech32 = match.first.group(0)!; + String lnurlDecoded = ''; try { - bech32 = Bech32Codec() - .decode(res.first.group(0)!.toLowerCase(), res.first.group(0)!.length) + // try to decode lowercased bech32 and store it to + lnurlDecoded = Bech32Codec() + .decode(lnurlBech32.toLowerCase(), lnurlBech32.length) .toString(); } catch (e) { throw ArgumentError('Error when decoding bech32 lnurl'); } - if (!bech32.isEmpty) { + if (!lnurlDecoded.isNotEmpty) { return true; } } From 47fd30f84e14ef4511ab4a558e37b7354e522ad6 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Mon, 24 Jul 2023 14:19:35 +0200 Subject: [PATCH 4/9] isBech32 - do not decode, use only regex to determine if the url is bech32 or not --- lib/dart_lnurl.dart | 3 +++ lib/src/lnurl.dart | 21 +-------------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/lib/dart_lnurl.dart b/lib/dart_lnurl.dart index dc622d1..7f78e15 100644 --- a/lib/dart_lnurl.dart +++ b/lib/dart_lnurl.dart @@ -14,10 +14,13 @@ export 'src/bech32.dart'; Uri decodeUri(String encodedUrl) { Uri decodedUri; + //Handle the cases when Uri doesn't have to be bech32 encoded, as per LUD-17 if (!isbech32(encodedUrl)) { + /// Non-Bech32 encoded URL final String lnUrl = findLnUrlNonBech32(encodedUrl); decodedUri = Uri.parse(lnUrl); } else { + /// Bech32 encoded URL /// Try to parse the input as a lnUrl. Will throw an error if it fails. final lnUrl = findLnUrl(encodedUrl); diff --git a/lib/src/lnurl.dart b/lib/src/lnurl.dart index 3cf8c5d..6b7a09b 100644 --- a/lib/src/lnurl.dart +++ b/lib/src/lnurl.dart @@ -1,5 +1,3 @@ -import 'package:bech32/bech32.dart'; - /// Parse and return a given lnurl string if it's valid. Will remove /// `lightning:` from the beginning of it if present. String findLnUrl(String input) { @@ -42,22 +40,5 @@ bool isbech32(String input) { final match = new RegExp( r',*?((lnurl)([0-9]{1,}[a-z0-9]+){1})', ).allMatches(input.toLowerCase()); - if (match.length == 1) { - print(match.first.group(0)); - // get the lnurl string from the regex - String lnurlBech32 = match.first.group(0)!; - String lnurlDecoded = ''; - try { - // try to decode lowercased bech32 and store it to - lnurlDecoded = Bech32Codec() - .decode(lnurlBech32.toLowerCase(), lnurlBech32.length) - .toString(); - } catch (e) { - throw ArgumentError('Error when decoding bech32 lnurl'); - } - if (!lnurlDecoded.isNotEmpty) { - return true; - } - } - return false; + return match.length == 1 ? true : false; } From 33c4d2f167e6bed445158542e593b68b177ca53c Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Mon, 24 Jul 2023 15:01:47 +0200 Subject: [PATCH 5/9] code refactoring, handle both encoded and non-encoded url smoothly --- README.md | 8 ++-- lib/dart_lnurl.dart | 36 +++++++++--------- test/dart_lnurl_test.dart | 80 ++++++++++++++++++++++++++++----------- 3 files changed, 80 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index cbcd657..4d2570f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # dart_lnurl [![pub package](https://img.shields.io/badge/pub-0.0.1-blueviolet.svg)](https://pub.dev/packages/dart_lnurl) -A Dart implementation of lnurl to decode bech32 lnurl strings. Currently supports the following tags: +A Dart implementation of lnurl to decode and bech32 lnurl strings. Currently supports the following tags: * withdrawRequest * payRequest * channelRequest @@ -10,7 +10,7 @@ A Dart implementation of lnurl to decode bech32 lnurl strings. Currently support ## Features * ✅ Decode a bech32-encoded lnurl string. * ✅ Handles LUD-17 non-bech32 lnurl string (lnurlw, lnurlp, lnurlc, keyauth). -* ✅ Make GET request to the decoded ln service and return the response. +* ✅ Make GET request to the ln service and return the response. @@ -18,8 +18,8 @@ Learn more about the lnurl spec here: https://github.com/btcontract/lnurl-rfc # API Reference -`Future getParams(String encodedUrl)` -Use this to parse an encoded bech32 lnurl string, call the decoded URI, and return the parsed response from the lnurl service. The `encodedUrl` can either have `lightning:` in it or not. +`Future getParams(String url)` +Use this to parse a lnurl string, call the (decoded if needed) URI, and return the parsed response from the lnurl service. The bech32-encoded `url` can either have `lightning:` in it or not and non-bech32 `url` can either have `lnurlw:`,`lnurlp:`,`lnurlc:` or `keyauth:`. `String decryptSuccessActionAesPayload({LNURLPaySuccessAction successAction, String preimage})` diff --git a/lib/dart_lnurl.dart b/lib/dart_lnurl.dart index 7f78e15..cecf655 100644 --- a/lib/dart_lnurl.dart +++ b/lib/dart_lnurl.dart @@ -12,23 +12,23 @@ export 'src/types.dart'; export 'src/success_action.dart'; export 'src/bech32.dart'; -Uri decodeUri(String encodedUrl) { - Uri decodedUri; +Uri parseLnurl(String input) { + Uri parsedUrl; //Handle the cases when Uri doesn't have to be bech32 encoded, as per LUD-17 - if (!isbech32(encodedUrl)) { + if (!isbech32(input)) { /// Non-Bech32 encoded URL - final String lnUrl = findLnUrlNonBech32(encodedUrl); - decodedUri = Uri.parse(lnUrl); + final String lnUrl = findLnUrlNonBech32(input); + parsedUrl = Uri.parse(lnUrl); } else { /// Bech32 encoded URL /// Try to parse the input as a lnUrl. Will throw an error if it fails. - final lnUrl = findLnUrl(encodedUrl); + final lnUrl = findLnUrl(input); /// Decode the lnurl using bech32 final bech32 = Bech32Codec().decode(lnUrl, lnUrl.length); - decodedUri = Uri.parse(utf8.decode(fromWords(bech32.data))); + parsedUrl = Uri.parse(utf8.decode(fromWords(bech32.data))); } - return decodedUri; + return parsedUrl; } /// Get params from a lnurl string. Possible types are: @@ -39,11 +39,11 @@ Uri decodeUri(String encodedUrl) { /// * `LNURLPayParams` /// /// Throws [ArgumentError] if the provided input is not a valid lnurl. -Future getParams(String encodedUrl) async { - final decodedUri = decodeUri(encodedUrl); +Future getParams(String url) async { + final parsedUrl = parseLnurl(url); try { /// Call the lnurl to get a response - final res = await http.get(decodedUri); + final res = await http.get(parsedUrl); /// If there's an error then throw it if (res.statusCode >= 300) { @@ -58,8 +58,8 @@ Future getParams(String encodedUrl) async { error: LNURLErrorResponse.fromJson({ ...parsedJson, ...{ - 'domain': decodedUri.host, - 'url': decodedUri.toString(), + 'domain': parsedUrl.host, + 'url': parsedUrl.toString(), } }), ); @@ -101,8 +101,8 @@ Future getParams(String encodedUrl) async { error: LNURLErrorResponse.fromJson({ ...parsedJson, ...{ - 'domain': decodedUri.host, - 'url': decodedUri.toString(), + 'domain': parsedUrl.host, + 'url': parsedUrl.toString(), } }), ); @@ -114,9 +114,9 @@ Future getParams(String encodedUrl) async { return LNURLParseResult( error: LNURLErrorResponse.fromJson({ 'status': 'ERROR', - 'reason': '${decodedUri.toString()} returned error: ${e.toString()}', - 'url': decodedUri.toString(), - 'domain': decodedUri.host, + 'reason': '${parsedUrl.toString()} returned error: ${e.toString()}', + 'url': parsedUrl.toString(), + 'domain': parsedUrl.host, }), ); } diff --git a/test/dart_lnurl_test.dart b/test/dart_lnurl_test.dart index a5818c0..cd583ea 100644 --- a/test/dart_lnurl_test.dart +++ b/test/dart_lnurl_test.dart @@ -8,7 +8,7 @@ void main() { test('should handle bolt card lnurlw:// ', () async { final url = 'lnurlw://lnbits.btcslovnik.cz/boltcards/api/v1/scan/wpyeilzhasqu8rgsmfqbv9?p=D13EFAAEC499E07F611B279BA3EE982C&c=DF6C74D375DF8300'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -17,7 +17,7 @@ void main() { test('should handle onion bolt card lnurlw:// ', () async { final url = 'lnurlw://lnbits.btcslovnikxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/boltcards/api/v1/scan/wpyeilzhasqu8rgsmfqbv9?p=D13EFAAEC499E07F611B279BA3EE982C&c=DF6C74D375DF8300'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -27,7 +27,7 @@ void main() { () async { final url = 'enlnurlw://lnbits.btcslovnik.cz/boltcards/api/v1/scan/wpyeilzhasqu8rgsmfqbv9?p=D13EFAAEC499E07F611B279BA3EE982C&c=DF6C74D375DF8300'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -38,7 +38,7 @@ void main() { () async { final url = 'enlnurlw://lnbits.btcslovnikxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/boltcards/api/v1/scan/wpyeilzhasqu8rgsmfqbv9?p=D13EFAAEC499E07F611B279BA3EE982C&c=DF6C74D375DF8300'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -47,21 +47,21 @@ void main() { test('should handle static lnurlw://', () async { final url = 'lnurlw://lnbits.cz/lnurlw/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect(res, Uri.parse('https://lnbits.cz/lnurlw/357')); }); test('should handle static lnurlw:// with additional non-related prefix', () async { final url = 'enlnurlw://lnbits.cz/lnurlw/357'; - final res = await decodeUri(url); + final res = await parseLnurl(url); //expect(res.payParams?.tag, 'payRequest'); expect(res, Uri.parse('https://lnbits.cz/lnurlw/357')); }); test('should handle static onion lnurlw://', () async { final url = 'lnurlw://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlw/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -72,7 +72,7 @@ void main() { () async { final url = 'enlnurlw://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlw/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -81,19 +81,19 @@ void main() { test('should handle lnurlp://', () async { final url = 'lnurlp://lnbits.cz/lnurlp/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect(res, Uri.parse('https://lnbits.cz/lnurlp/357')); }); test('should handle lnurlp:// with additional non-related prefix', () async { final url = 'enlnurlp://lnbits.cz/lnurlp/357'; - final res = await decodeUri(url); + final res = await parseLnurl(url); //expect(res.payParams?.tag, 'payRequest'); expect(res, Uri.parse('https://lnbits.cz/lnurlp/357')); }); test('should handle onion lnurlp://', () async { final url = 'lnurlp://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlp/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -103,7 +103,7 @@ void main() { () async { final url = 'enlnurlp://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlp/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -112,19 +112,19 @@ void main() { test('should handle lnurlc://', () async { final url = 'lnurlc://lnbits.cz/lnurlc/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect(res, Uri.parse('https://lnbits.cz/lnurlc/357')); }); test('should handle lnurlc:// with additional non-related prefix', () async { final url = 'enlnurlc://lnbits.cz/lnurlc/357'; - final res = await decodeUri(url); + final res = await parseLnurl(url); //expect(res.payParams?.tag, 'payRequest'); expect(res, Uri.parse('https://lnbits.cz/lnurlc/357')); }); test('should handle onion lnurlc://', () async { final url = 'lnurlc://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlc/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -134,7 +134,7 @@ void main() { () async { final url = 'enlnurlc://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlc/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -143,19 +143,19 @@ void main() { test('should handle keyauth://', () async { final url = 'keyauth://lnbits.cz/keyauth/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect(res, Uri.parse('https://lnbits.cz/keyauth/357')); }); test('should handle keyauth:// with additional non-related prefix', () async { final url = 'enkeyauth://lnbits.cz/keyauth/357'; - final res = await decodeUri(url); + final res = await parseLnurl(url); //expect(res.payParams?.tag, 'payRequest'); expect(res, Uri.parse('https://lnbits.cz/keyauth/357')); }); test('should handle onion keyauth://', () async { final url = 'keyauth://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/keyauth/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -165,7 +165,7 @@ void main() { () async { final url = 'enkeyauth://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/keyauth/357'; - final res = decodeUri(url); + final res = parseLnurl(url); expect( res, Uri.parse( @@ -210,10 +210,46 @@ void main() { expect(decrypted, plainText); }); - test('should decode lnurl-pay', () async { + test('should parse lnurl-pay', () async { final url = 'lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTNZD9NHXATW9EU8J730D3H82UNV94CXZ7FLWDJHXUMFDAHR6C3NXGCNGCEE8YEX2CFHXCCRYCNXXUENVEFHXQCR2EF5XS6R2D35XUERSEFC8YCKGDF5XAJNGCTX8PJRYVP4XDJNVD3CX3NRVEFCX4SSWRA86F'; - final res = decodeUri(url); + final res = parseLnurl(url); expect(res, isNotNull); }); + + test('should parse lnurl without lightning:', () { + final lnurl = + 'lnurl1dp68gurn8ghj7mrww4exctt5dahkccn00qhxget8wfjk2um0veax2un09e3k7mf0w5lhz0t9xcekzv34vgcx2vfkvcurxwphvgcrwefjvgcnqwrpxqmkxven89skgvp3vs6nwvpjvy6njdfsx5ekgephvcurxdf5xcerwvecvyunsf32lqq'; + expect( + parseLnurl(lnurl), + Uri.parse( + 'https://lnurl-toolbox.degreesofzero.com/u?q=e63a25b0e16f8387b07e2b108a07c339ad01d5702a595053dd7f835462738a98')); + }); + + test('should parse lnurl with lightning:', () { + final lnurl = + 'lightning:lnurl1dp68gurn8ghj7mrww4exctt5dahkccn00qhxget8wfjk2um0veax2un09e3k7mf0w5lhz0t9xcekzv34vgcx2vfkvcurxwphvgcrwefjvgcnqwrpxqmkxven89skgvp3vs6nwvpjvy6njdfsx5ekgephvcurxdf5xcerwvecvyunsf32lqq'; + expect( + parseLnurl(lnurl), + Uri.parse( + 'https://lnurl-toolbox.degreesofzero.com/u?q=e63a25b0e16f8387b07e2b108a07c339ad01d5702a595053dd7f835462738a98')); + }); + + test('should parse LNURL without lightning:', () { + final lnurl = + 'lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTT5DAHKCCN00QHXGET8WFJK2UM0VEAX2UN09E3K7MF0W5LHZ0T9XCEKZV34VGCX2VFKVCURXWPHVGCRWEFJVGCNQWRPXQMKXVEN89SKGVP3VS6NWVPJVY6NJDFSX5EKGEPHVCURXDF5XCERWVECVYUNSF32LQQ'; + expect( + parseLnurl(lnurl), + Uri.parse( + 'https://lnurl-toolbox.degreesofzero.com/u?q=e63a25b0e16f8387b07e2b108a07c339ad01d5702a595053dd7f835462738a98')); + }); + + test('should parse LNURL with lightning:', () { + final lnurl = + 'lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTT5DAHKCCN00QHXGET8WFJK2UM0VEAX2UN09E3K7MF0W5LHZ0T9XCEKZV34VGCX2VFKVCURXWPHVGCRWEFJVGCNQWRPXQMKXVEN89SKGVP3VS6NWVPJVY6NJDFSX5EKGEPHVCURXDF5XCERWVECVYUNSF32LQQ'; + expect( + parseLnurl(lnurl), + Uri.parse( + 'https://lnurl-toolbox.degreesofzero.com/u?q=e63a25b0e16f8387b07e2b108a07c339ad01d5702a595053dd7f835462738a98')); + }); } From b1082fdcd9fb3043be1e342ba7a790c67a63f98b Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Mon, 24 Jul 2023 15:03:06 +0200 Subject: [PATCH 6/9] minor readme text fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d2570f..ca711a7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # dart_lnurl [![pub package](https://img.shields.io/badge/pub-0.0.1-blueviolet.svg)](https://pub.dev/packages/dart_lnurl) -A Dart implementation of lnurl to decode and bech32 lnurl strings. Currently supports the following tags: +A Dart implementation of lnurl to decode bech32 and parse non-bech32 lnurl strings. Currently supports the following tags: * withdrawRequest * payRequest * channelRequest From 7044973929148c80272797683b4b9d99f1095e4a Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Tue, 25 Jul 2023 09:44:14 +0200 Subject: [PATCH 7/9] decodedUri to parsedUri --- lib/dart_lnurl.dart | 26 +++++++++++++------------- lib/src/lnurl.dart | 16 ++++++++-------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/dart_lnurl.dart b/lib/dart_lnurl.dart index cecf655..b6dfc29 100644 --- a/lib/dart_lnurl.dart +++ b/lib/dart_lnurl.dart @@ -13,12 +13,12 @@ export 'src/success_action.dart'; export 'src/bech32.dart'; Uri parseLnurl(String input) { - Uri parsedUrl; + Uri parsedUri; //Handle the cases when Uri doesn't have to be bech32 encoded, as per LUD-17 if (!isbech32(input)) { /// Non-Bech32 encoded URL final String lnUrl = findLnUrlNonBech32(input); - parsedUrl = Uri.parse(lnUrl); + parsedUri = Uri.parse(lnUrl); } else { /// Bech32 encoded URL /// Try to parse the input as a lnUrl. Will throw an error if it fails. @@ -26,9 +26,9 @@ Uri parseLnurl(String input) { /// Decode the lnurl using bech32 final bech32 = Bech32Codec().decode(lnUrl, lnUrl.length); - parsedUrl = Uri.parse(utf8.decode(fromWords(bech32.data))); + parsedUri = Uri.parse(utf8.decode(fromWords(bech32.data))); } - return parsedUrl; + return parsedUri; } /// Get params from a lnurl string. Possible types are: @@ -40,10 +40,10 @@ Uri parseLnurl(String input) { /// /// Throws [ArgumentError] if the provided input is not a valid lnurl. Future getParams(String url) async { - final parsedUrl = parseLnurl(url); + final parsedUri = parseLnurl(url); try { /// Call the lnurl to get a response - final res = await http.get(parsedUrl); + final res = await http.get(parsedUri); /// If there's an error then throw it if (res.statusCode >= 300) { @@ -58,8 +58,8 @@ Future getParams(String url) async { error: LNURLErrorResponse.fromJson({ ...parsedJson, ...{ - 'domain': parsedUrl.host, - 'url': parsedUrl.toString(), + 'domain': parsedUri.host, + 'url': parsedUri.toString(), } }), ); @@ -101,8 +101,8 @@ Future getParams(String url) async { error: LNURLErrorResponse.fromJson({ ...parsedJson, ...{ - 'domain': parsedUrl.host, - 'url': parsedUrl.toString(), + 'domain': parsedUri.host, + 'url': parsedUri.toString(), } }), ); @@ -114,9 +114,9 @@ Future getParams(String url) async { return LNURLParseResult( error: LNURLErrorResponse.fromJson({ 'status': 'ERROR', - 'reason': '${parsedUrl.toString()} returned error: ${e.toString()}', - 'url': parsedUrl.toString(), - 'domain': parsedUrl.host, + 'reason': '${parsedUri.toString()} returned error: ${e.toString()}', + 'url': parsedUri.toString(), + 'domain': parsedUri.host, }), ); } diff --git a/lib/src/lnurl.dart b/lib/src/lnurl.dart index 6b7a09b..7920507 100644 --- a/lib/src/lnurl.dart +++ b/lib/src/lnurl.dart @@ -18,22 +18,22 @@ String findLnUrlNonBech32(String input) { /// Handle non bech32-encoded LNURL final lud17prefixes = ['lnurlw', 'lnurlp', 'keyauth', 'lnurlc']; - Uri decodedUri = Uri.parse(input); + Uri parsedUri = Uri.parse(input); for (final prefix in lud17prefixes) { - if (decodedUri.scheme.contains(prefix)) { - decodedUri = decodedUri.replace(scheme: prefix); + if (parsedUri.scheme.contains(prefix)) { + parsedUri = parsedUri.replace(scheme: prefix); break; } } - if (lud17prefixes.contains(decodedUri.scheme)) { + if (lud17prefixes.contains(parsedUri.scheme)) { /// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.' - decodedUri = decodedUri.replace( - scheme: decodedUri.host.endsWith('onion') || - decodedUri.host.endsWith('onion.') + parsedUri = parsedUri.replace( + scheme: parsedUri.host.endsWith('onion') || + parsedUri.host.endsWith('onion.') ? 'http' : 'https'); } - return decodedUri.toString(); + return parsedUri.toString(); } bool isbech32(String input) { From fa038dcf44e7581b36b4218f9f1746abb282ef01 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Tue, 25 Jul 2023 09:47:09 +0200 Subject: [PATCH 8/9] minor text fixes --- lib/dart_lnurl.dart | 4 ++-- test/dart_lnurl_test.dart | 50 +++++++++++++++++++-------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/dart_lnurl.dart b/lib/dart_lnurl.dart index b6dfc29..77e77d5 100644 --- a/lib/dart_lnurl.dart +++ b/lib/dart_lnurl.dart @@ -12,7 +12,7 @@ export 'src/types.dart'; export 'src/success_action.dart'; export 'src/bech32.dart'; -Uri parseLnurl(String input) { +Uri parseLnUri(String input) { Uri parsedUri; //Handle the cases when Uri doesn't have to be bech32 encoded, as per LUD-17 if (!isbech32(input)) { @@ -40,7 +40,7 @@ Uri parseLnurl(String input) { /// /// Throws [ArgumentError] if the provided input is not a valid lnurl. Future getParams(String url) async { - final parsedUri = parseLnurl(url); + final parsedUri = parseLnUri(url); try { /// Call the lnurl to get a response final res = await http.get(parsedUri); diff --git a/test/dart_lnurl_test.dart b/test/dart_lnurl_test.dart index cd583ea..a185367 100644 --- a/test/dart_lnurl_test.dart +++ b/test/dart_lnurl_test.dart @@ -8,7 +8,7 @@ void main() { test('should handle bolt card lnurlw:// ', () async { final url = 'lnurlw://lnbits.btcslovnik.cz/boltcards/api/v1/scan/wpyeilzhasqu8rgsmfqbv9?p=D13EFAAEC499E07F611B279BA3EE982C&c=DF6C74D375DF8300'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -17,7 +17,7 @@ void main() { test('should handle onion bolt card lnurlw:// ', () async { final url = 'lnurlw://lnbits.btcslovnikxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/boltcards/api/v1/scan/wpyeilzhasqu8rgsmfqbv9?p=D13EFAAEC499E07F611B279BA3EE982C&c=DF6C74D375DF8300'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -27,7 +27,7 @@ void main() { () async { final url = 'enlnurlw://lnbits.btcslovnik.cz/boltcards/api/v1/scan/wpyeilzhasqu8rgsmfqbv9?p=D13EFAAEC499E07F611B279BA3EE982C&c=DF6C74D375DF8300'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -38,7 +38,7 @@ void main() { () async { final url = 'enlnurlw://lnbits.btcslovnikxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/boltcards/api/v1/scan/wpyeilzhasqu8rgsmfqbv9?p=D13EFAAEC499E07F611B279BA3EE982C&c=DF6C74D375DF8300'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -47,21 +47,21 @@ void main() { test('should handle static lnurlw://', () async { final url = 'lnurlw://lnbits.cz/lnurlw/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect(res, Uri.parse('https://lnbits.cz/lnurlw/357')); }); test('should handle static lnurlw:// with additional non-related prefix', () async { final url = 'enlnurlw://lnbits.cz/lnurlw/357'; - final res = await parseLnurl(url); + final res = await parseLnUri(url); //expect(res.payParams?.tag, 'payRequest'); expect(res, Uri.parse('https://lnbits.cz/lnurlw/357')); }); test('should handle static onion lnurlw://', () async { final url = 'lnurlw://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlw/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -72,7 +72,7 @@ void main() { () async { final url = 'enlnurlw://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlw/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -81,19 +81,19 @@ void main() { test('should handle lnurlp://', () async { final url = 'lnurlp://lnbits.cz/lnurlp/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect(res, Uri.parse('https://lnbits.cz/lnurlp/357')); }); test('should handle lnurlp:// with additional non-related prefix', () async { final url = 'enlnurlp://lnbits.cz/lnurlp/357'; - final res = await parseLnurl(url); + final res = await parseLnUri(url); //expect(res.payParams?.tag, 'payRequest'); expect(res, Uri.parse('https://lnbits.cz/lnurlp/357')); }); test('should handle onion lnurlp://', () async { final url = 'lnurlp://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlp/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -103,7 +103,7 @@ void main() { () async { final url = 'enlnurlp://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlp/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -112,19 +112,19 @@ void main() { test('should handle lnurlc://', () async { final url = 'lnurlc://lnbits.cz/lnurlc/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect(res, Uri.parse('https://lnbits.cz/lnurlc/357')); }); test('should handle lnurlc:// with additional non-related prefix', () async { final url = 'enlnurlc://lnbits.cz/lnurlc/357'; - final res = await parseLnurl(url); + final res = await parseLnUri(url); //expect(res.payParams?.tag, 'payRequest'); expect(res, Uri.parse('https://lnbits.cz/lnurlc/357')); }); test('should handle onion lnurlc://', () async { final url = 'lnurlc://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlc/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -134,7 +134,7 @@ void main() { () async { final url = 'enlnurlc://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/lnurlc/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -143,19 +143,19 @@ void main() { test('should handle keyauth://', () async { final url = 'keyauth://lnbits.cz/keyauth/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect(res, Uri.parse('https://lnbits.cz/keyauth/357')); }); test('should handle keyauth:// with additional non-related prefix', () async { final url = 'enkeyauth://lnbits.cz/keyauth/357'; - final res = await parseLnurl(url); + final res = await parseLnUri(url); //expect(res.payParams?.tag, 'payRequest'); expect(res, Uri.parse('https://lnbits.cz/keyauth/357')); }); test('should handle onion keyauth://', () async { final url = 'keyauth://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/keyauth/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -165,7 +165,7 @@ void main() { () async { final url = 'enkeyauth://lnbitsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/keyauth/357'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect( res, Uri.parse( @@ -213,7 +213,7 @@ void main() { test('should parse lnurl-pay', () async { final url = 'lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTNZD9NHXATW9EU8J730D3H82UNV94CXZ7FLWDJHXUMFDAHR6C3NXGCNGCEE8YEX2CFHXCCRYCNXXUENVEFHXQCR2EF5XS6R2D35XUERSEFC8YCKGDF5XAJNGCTX8PJRYVP4XDJNVD3CX3NRVEFCX4SSWRA86F'; - final res = parseLnurl(url); + final res = parseLnUri(url); expect(res, isNotNull); }); @@ -221,7 +221,7 @@ void main() { final lnurl = 'lnurl1dp68gurn8ghj7mrww4exctt5dahkccn00qhxget8wfjk2um0veax2un09e3k7mf0w5lhz0t9xcekzv34vgcx2vfkvcurxwphvgcrwefjvgcnqwrpxqmkxven89skgvp3vs6nwvpjvy6njdfsx5ekgephvcurxdf5xcerwvecvyunsf32lqq'; expect( - parseLnurl(lnurl), + parseLnUri(lnurl), Uri.parse( 'https://lnurl-toolbox.degreesofzero.com/u?q=e63a25b0e16f8387b07e2b108a07c339ad01d5702a595053dd7f835462738a98')); }); @@ -230,7 +230,7 @@ void main() { final lnurl = 'lightning:lnurl1dp68gurn8ghj7mrww4exctt5dahkccn00qhxget8wfjk2um0veax2un09e3k7mf0w5lhz0t9xcekzv34vgcx2vfkvcurxwphvgcrwefjvgcnqwrpxqmkxven89skgvp3vs6nwvpjvy6njdfsx5ekgephvcurxdf5xcerwvecvyunsf32lqq'; expect( - parseLnurl(lnurl), + parseLnUri(lnurl), Uri.parse( 'https://lnurl-toolbox.degreesofzero.com/u?q=e63a25b0e16f8387b07e2b108a07c339ad01d5702a595053dd7f835462738a98')); }); @@ -239,7 +239,7 @@ void main() { final lnurl = 'lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTT5DAHKCCN00QHXGET8WFJK2UM0VEAX2UN09E3K7MF0W5LHZ0T9XCEKZV34VGCX2VFKVCURXWPHVGCRWEFJVGCNQWRPXQMKXVEN89SKGVP3VS6NWVPJVY6NJDFSX5EKGEPHVCURXDF5XCERWVECVYUNSF32LQQ'; expect( - parseLnurl(lnurl), + parseLnUri(lnurl), Uri.parse( 'https://lnurl-toolbox.degreesofzero.com/u?q=e63a25b0e16f8387b07e2b108a07c339ad01d5702a595053dd7f835462738a98')); }); @@ -248,7 +248,7 @@ void main() { final lnurl = 'lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTT5DAHKCCN00QHXGET8WFJK2UM0VEAX2UN09E3K7MF0W5LHZ0T9XCEKZV34VGCX2VFKVCURXWPHVGCRWEFJVGCNQWRPXQMKXVEN89SKGVP3VS6NWVPJVY6NJDFSX5EKGEPHVCURXDF5XCERWVECVYUNSF32LQQ'; expect( - parseLnurl(lnurl), + parseLnUri(lnurl), Uri.parse( 'https://lnurl-toolbox.degreesofzero.com/u?q=e63a25b0e16f8387b07e2b108a07c339ad01d5702a595053dd7f835462738a98')); }); From 8e23bb9415902b5bf89e5eb38d4b7d6575b5db8f Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Thu, 27 Jul 2023 00:33:01 +0200 Subject: [PATCH 9/9] add tests for lnurlw static link, onion, minor code changes --- lib/dart_lnurl.dart | 11 ++++--- test/dart_lnurl_test.dart | 65 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/lib/dart_lnurl.dart b/lib/dart_lnurl.dart index 77e77d5..492f343 100644 --- a/lib/dart_lnurl.dart +++ b/lib/dart_lnurl.dart @@ -15,11 +15,8 @@ export 'src/bech32.dart'; Uri parseLnUri(String input) { Uri parsedUri; //Handle the cases when Uri doesn't have to be bech32 encoded, as per LUD-17 - if (!isbech32(input)) { - /// Non-Bech32 encoded URL - final String lnUrl = findLnUrlNonBech32(input); - parsedUri = Uri.parse(lnUrl); - } else { + + if (isbech32(input)) { /// Bech32 encoded URL /// Try to parse the input as a lnUrl. Will throw an error if it fails. final lnUrl = findLnUrl(input); @@ -27,6 +24,10 @@ Uri parseLnUri(String input) { /// Decode the lnurl using bech32 final bech32 = Bech32Codec().decode(lnUrl, lnUrl.length); parsedUri = Uri.parse(utf8.decode(fromWords(bech32.data))); + } else { + /// Non-Bech32 encoded URL + final String lnUrl = findLnUrlNonBech32(input); + parsedUri = Uri.parse(lnUrl); } return parsedUri; } diff --git a/test/dart_lnurl_test.dart b/test/dart_lnurl_test.dart index a185367..4612cf2 100644 --- a/test/dart_lnurl_test.dart +++ b/test/dart_lnurl_test.dart @@ -50,6 +50,71 @@ void main() { final res = parseLnUri(url); expect(res, Uri.parse('https://lnbits.cz/lnurlw/357')); }); + test('should handle static lnurlw with lightning:', () async { + final url = + 'lightning:LNURL1DP68GURN8GHJ7MRWVF5HGUEWVF6XXURVV96XY7FWVDAZ7AMFW35XGUNPWUHKZURF9AMRZTMVDE6HYMP0GF8Y5V63FFX9WKT5FDTHXD3CVF9XG42DW9VSN09NNG'; + final res = parseLnUri(url); + expect( + res, + Uri.parse( + 'https://lnbits.btcplatby.cz/withdraw/api/v1/lnurl/BNJ3QJLWYtKWs68bJdUMqY')); + }); + + test( + 'should handle static lnurlw with lightning: and with additional non-related prefix', + () async { + final url = + 'enlightning:LNURL1DP68GURN8GHJ7MRWVF5HGUEWVF6XXURVV96XY7FWVDAZ7AMFW35XGUNPWUHKZURF9AMRZTMVDE6HYMP0GF8Y5V63FFX9WKT5FDTHXD3CVF9XG42DW9VSN09NNG'; + final res = parseLnUri(url); + expect( + res, + Uri.parse( + 'https://lnbits.btcplatby.cz/withdraw/api/v1/lnurl/BNJ3QJLWYtKWs68bJdUMqY')); + }); + + test('should handle static onion lnurlw with lightning:', () async { + final url = + 'lightning:LNURL1DP68GUP69UHKCMNZD968XTNZW33HQMRPW338J7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC9EHKU6T0DCHHW6T5DPJ8YCTH9ASHQ6F0WCCJ7MRWW4EXCT6ZFE9RX522F3T4JAZT2AENVWRZFFJ92NT3TYXRL2DG'; + final res = parseLnUri(url); + expect( + res, + Uri.parse( + 'http://lnbits.btcplatbyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/withdraw/api/v1/lnurl/BNJ3QJLWYtKWs68bJdUMqY')); + }); + + test( + 'should handle static onion lnurlw with lightning: and with additional non-related prefix', + () async { + final url = + 'enlightning:LNURL1DP68GUP69UHKCMNZD968XTNZW33HQMRPW338J7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC9EHKU6T0DCHHW6T5DPJ8YCTH9ASHQ6F0WCCJ7MRWW4EXCT6ZFE9RX522F3T4JAZT2AENVWRZFFJ92NT3TYXRL2DG'; + final res = parseLnUri(url); + expect( + res, + Uri.parse( + 'http://lnbits.btcplatbyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/withdraw/api/v1/lnurl/BNJ3QJLWYtKWs68bJdUMqY')); + }); + + test('should handle static onion lowercase lnurlw with lightning:', () async { + final url = + 'lightning:lnurl1dp68gup69uhkcmnzd968xtnzw33hqmrpw338j7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc9ehku6t0dchhw6t5dpj8ycth9ashq6f0wccj7mrww4exct6zfe9rx522f3t4jazt2aenvwrzffj92nt3tyxrl2dg'; + final res = parseLnUri(url); + expect( + res, + Uri.parse( + 'http://lnbits.btcplatbyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/withdraw/api/v1/lnurl/BNJ3QJLWYtKWs68bJdUMqY')); + }); + + test( + 'should handle static onion lowercase lnurlw with lightning and other non-related prefix:', + () async { + final url = + 'enlightning:lnurl1dp68gup69uhkcmnzd968xtnzw33hqmrpw338j7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc9ehku6t0dchhw6t5dpj8ycth9ashq6f0wccj7mrww4exct6zfe9rx522f3t4jazt2aenvwrzffj92nt3tyxrl2dg'; + final res = parseLnUri(url); + expect( + res, + Uri.parse( + 'http://lnbits.btcplatbyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion/withdraw/api/v1/lnurl/BNJ3QJLWYtKWs68bJdUMqY')); + }); test('should handle static lnurlw:// with additional non-related prefix', () async {