Skip to content

Commit dc19458

Browse files
authored
Merge pull request #2300 from SatoshiPortal/2202-qr-scanner-fails-on-non-standard-bip21-donation-uris
fix: handle non-standard BIP21 URIs in QR scanner
2 parents 2383889 + 28ddb62 commit dc19458

34 files changed

Lines changed: 171 additions & 5 deletions
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import 'package:bb_mobile/core/utils/payment_request.dart';
2+
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
3+
import 'package:bb_mobile/main.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
6+
Future<void> main({bool isInitialized = false}) async {
7+
TestWidgetsFlutterBinding.ensureInitialized();
8+
if (!isInitialized) await Bull.init();
9+
10+
const btcAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq';
11+
12+
const lnurlStr =
13+
'lnurl1dp68gurn8ghj7um9wfmxjcm99e3k7mf0v9cxj0m385ekvcenxc6r2c35xvukxefcv5'
14+
'mkvv34x5ekzd3ev56nyd3hxqurzepexejxxepnxscrvwfnv9nxzcn9xq6xyefhvgcxxcmyxy'
15+
'mnserxfq5fns';
16+
17+
const bolt11Invoice =
18+
'lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq'
19+
'dq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2aw'
20+
'hz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp';
21+
22+
group('PaymentRequest.parse', () {
23+
test('BIP21 with LNURL lightning param, label, and message', () async {
24+
final input =
25+
'bitcoin:$btcAddress?lightning=$lnurlStr&label=Donation&message=Thanks';
26+
final result = await PaymentRequest.parse(input);
27+
expect(result, isA<Bip21PaymentRequest>());
28+
final bip21 = result as Bip21PaymentRequest;
29+
expect(bip21.address.toLowerCase(), btcAddress);
30+
expect(bip21.lightning, lnurlStr);
31+
expect(bip21.label, 'Donation');
32+
expect(bip21.message, 'Thanks');
33+
expect(bip21.network, Network.bitcoinMainnet);
34+
});
35+
36+
test('BIP21 with Bolt11 lightning param', () async {
37+
final input = 'bitcoin:$btcAddress?lightning=$bolt11Invoice';
38+
final result = await PaymentRequest.parse(input);
39+
expect(result, isA<Bip21PaymentRequest>());
40+
final bip21 = result as Bip21PaymentRequest;
41+
expect(bip21.address.toLowerCase(), btcAddress);
42+
expect(bip21.lightning, bolt11Invoice);
43+
expect(bip21.network, Network.bitcoinMainnet);
44+
});
45+
46+
test('HTTPS URL with percent-encoded LNAddress in lightning param', () async {
47+
const input =
48+
'https://admin.bullbitcoin.com/abc'
49+
'?lightning=ishi%40walletofsatoshi.com&label=pleasefundme';
50+
final result = await PaymentRequest.parse(input);
51+
expect(result, isA<LnAddressPaymentRequest>());
52+
expect(
53+
(result as LnAddressPaymentRequest).address,
54+
'ishi@walletofsatoshi.com',
55+
);
56+
});
57+
});
58+
}

lib/core/utils/payment_request.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,25 @@ sealed class PaymentRequest with _$PaymentRequest {
7878
if (result != null) return result;
7979
}
8080

81+
final re = RegExp(r'lnurl[0-9a-z]+', caseSensitive: false);
82+
final m = re.firstMatch(trimmed);
83+
84+
if (m != null) {
85+
final result = await _tryParseLnAddress(m.group(0)!);
86+
if (result != null) return result;
87+
}
88+
89+
final lnAddressRe = RegExp(
90+
r'[a-zA-Z0-9._%+\-]+(?:@|%40)[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}',
91+
);
92+
final lnAddressMatch = lnAddressRe.firstMatch(trimmed);
93+
94+
if (lnAddressMatch != null) {
95+
final decoded = Uri.decodeComponent(lnAddressMatch.group(0)!);
96+
final result = await _tryParseLnAddress(decoded);
97+
if (result != null) return result;
98+
}
99+
81100
if (trimmed.toLowerCase().startsWith('lnbc') ||
82101
trimmed.toLowerCase().startsWith('lntb') ||
83102
trimmed.toLowerCase().startsWith('lightning:')) {

lib/features/send/presentation/bloc/send_cubit.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@ class SendCubit extends Cubit<SendState> {
236236
emit(
237237
state.copyWith(
238238
loadingBestWallet: false,
239-
invalidBitcoinStringException: InvalidBitcoinStringException(),
239+
invalidBitcoinStringException:
240+
state.scannedRawPaymentRequest.isNotEmpty
241+
? UnsupportedQrFormatException()
242+
: InvalidBitcoinStringException(),
240243
),
241244
);
242245
return;

lib/features/send/presentation/bloc/send_state.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,8 @@ class AmountlessInvoiceException extends SwapCreationException {
472472
}
473473

474474
class HardwareWalletSwapException extends SwapCreationException {
475-
HardwareWalletSwapException() : super('Hardware wallets cannot be used for swaps');
475+
HardwareWalletSwapException()
476+
: super('Hardware wallets cannot be used for swaps');
476477
}
477478

478479
class ExpiredInvoiceException extends SwapCreationException {
@@ -491,6 +492,8 @@ class InvalidBitcoinStringException extends BullException {
491492
]);
492493
}
493494

495+
class UnsupportedQrFormatException extends InvalidBitcoinStringException {}
496+
494497
/// Exception for swap limit violations.
495498
/// Stored in SendState with min/max limit values for localized error messages.
496499
/// UI displays context-specific messages using sendErrorAmountBelowMinimum,

lib/features/send/ui/screens/full_screen_scanner_page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ class _FullScreenScannerState extends State<FullScreenScannerPage> {
3636
} catch (e) {
3737
data = (qr, null);
3838
widget.onScannedPaymentRequest(data);
39+
if (mounted) context.pop();
3940
}
40-
setState(() {});
4141
}
4242

4343
@override

lib/features/send/ui/screens/send_screen.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,9 @@ class AddressErrorSection extends StatelessWidget {
261261
}
262262
if (invalidAddress != null) {
263263
return BBText(
264-
context.loc.sendErrorInvalidAddressOrInvoice,
264+
invalidAddress is UnsupportedQrFormatException
265+
? context.loc.sendErrorUnsupportedQrCodeFormat
266+
: context.loc.sendErrorInvalidAddressOrInvoice,
265267
style: context.font.bodyMedium,
266268
color: context.appColors.error,
267269
textAlign: .center,

localization/app_ar.arb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2345,6 +2345,7 @@
23452345
"@sendErrorInvalidAddressOrInvoice": {
23462346
"description": "Error when payment request is invalid"
23472347
},
2348+
"sendErrorUnsupportedQrCodeFormat": "تنسيق رمز QR غير مدعوم",
23482349
"transactionLabelAmountReceived": "المبلغ المستلم",
23492350
"@transactionLabelAmountReceived": {
23502351
"description": "Label for received amount"

localization/app_as.arb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4165,6 +4165,7 @@
41654165
"@sendErrorInvalidAddressOrInvoice": {
41664166
"description": "Error when payment request is invalid"
41674167
},
4168+
"sendErrorUnsupportedQrCodeFormat": "অসমৰ্থিত QR ক'ড ফৰ্মেট",
41684169
"sendErrorBuildFailed": "নিৰ্মাণ বিফল হৈছে",
41694170
"@sendErrorBuildFailed": {
41704171
"description": "Error title when transaction build fails"

localization/app_bg.arb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2125,6 +2125,7 @@
21252125
"@sendErrorInvalidAddressOrInvoice": {
21262126
"description": "Error when payment request is invalid"
21272127
},
2128+
"sendErrorUnsupportedQrCodeFormat": "Неподдържан формат на QR код",
21282129
"transactionLabelAmountReceived": "Получена сума",
21292130
"@transactionLabelAmountReceived": {
21302131
"description": "Label for received amount"

localization/app_bn.arb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2460,6 +2460,7 @@
24602460
"@sendErrorInvalidAddressOrInvoice": {
24612461
"description": "Error when payment request is invalid"
24622462
},
2463+
"sendErrorUnsupportedQrCodeFormat": "অসমর্থিত QR কোড ফরম্যাট",
24632464
"transactionLabelAmountReceived": "প্রাপ্ত পরিমাণ",
24642465
"@transactionLabelAmountReceived": {
24652466
"description": "Label for received amount"

0 commit comments

Comments
 (0)