From 4594801cf3581c316f49d94037c87ed74f6a2b6f Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 18 Nov 2024 14:14:25 -0600 Subject: [PATCH] Ensure plain addresses are parsed from qr codes. Use uri parsing everywhere with a couple small tweaks. --- .../new_contact_address_entry_form.dart | 122 +++++---- lib/pages/buy_view/buy_form.dart | 112 ++++----- .../exchange_step_views/step_2_view.dart | 165 ++++++------ .../generate_receiving_uri_qr_code_view.dart | 22 +- lib/pages/send_view/frost_ms/recipient.dart | 134 +++++----- lib/pages/send_view/send_view.dart | 18 +- lib/pages/send_view/token_send_view.dart | 33 +-- .../wallet_view/sub_widgets/desktop_send.dart | 116 +++------ .../sub_widgets/desktop_token_send.dart | 40 +-- lib/utilities/address_utils.dart | 236 +++++------------- pubspec.lock | 8 +- test/address_utils_test.dart | 49 ++-- 12 files changed, 425 insertions(+), 630 deletions(-) diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index ba57db69b..d84cee567 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -66,6 +66,62 @@ class _NewContactAddressEntryFormState List coins = []; + void _onQrTapped() async { + try { + // ref + // .read(shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + final qrResult = await widget.barcodeScanner.scan(); + + // Future.delayed( + // const Duration(seconds: 2), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + + final paymentData = AddressUtils.parsePaymentUri( + qrResult.rawContent, + logging: Logging.instance, + ); + + if (paymentData != null) { + addressController.text = paymentData.address; + ref.read(addressEntryDataProvider(widget.id)).address = + addressController.text.isEmpty ? null : addressController.text; + + addressLabelController.text = + paymentData.label ?? addressLabelController.text; + ref.read(addressEntryDataProvider(widget.id)).addressLabel = + addressLabelController.text.isEmpty + ? null + : addressLabelController.text; + + // now check for non standard encoded basic address + } else if (ref.read(addressEntryDataProvider(widget.id)).coin != null) { + if (ref.read(addressEntryDataProvider(widget.id)).coin!.validateAddress( + qrResult.rawContent, + )) { + addressController.text = qrResult.rawContent; + ref.read(addressEntryDataProvider(widget.id)).address = + qrResult.rawContent; + } + } + } on PlatformException catch (e, s) { + // ref + // .read(shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true; + Logging.instance.log( + "Failed to get camera permissions to scan address qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + } + @override void initState() { addressLabelController = TextEditingController() @@ -404,71 +460,7 @@ class _NewContactAddressEntryFormState null) TextFieldIconButton( key: const Key("addAddressBookEntryScanQrButtonKey"), - onTap: () async { - try { - // ref - // .read(shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - final qrResult = await widget.barcodeScanner.scan(); - - // Future.delayed( - // const Duration(seconds: 2), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - - final results = - AddressUtils.parseUri(qrResult.rawContent); - if (results.isNotEmpty) { - addressController.text = results["address"] ?? ""; - ref - .read(addressEntryDataProvider(widget.id)) - .address = - addressController.text.isEmpty - ? null - : addressController.text; - - addressLabelController.text = results["label"] ?? - addressLabelController.text; - ref - .read(addressEntryDataProvider(widget.id)) - .addressLabel = - addressLabelController.text.isEmpty - ? null - : addressLabelController.text; - - // now check for non standard encoded basic address - } else if (ref - .read(addressEntryDataProvider(widget.id)) - .coin != - null) { - if (ref - .read(addressEntryDataProvider(widget.id)) - .coin! - .validateAddress( - qrResult.rawContent, - )) { - addressController.text = qrResult.rawContent; - ref - .read(addressEntryDataProvider(widget.id)) - .address = qrResult.rawContent; - } - } - } on PlatformException catch (e, s) { - // ref - // .read(shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true; - Logging.instance.log( - "Failed to get camera permissions to scan address qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, + onTap: _onQrTapped, child: const QrCodeIcon(), ), const SizedBox( diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index 26ab7f254..f47831547 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -713,6 +713,60 @@ class _BuyFormState extends ConsumerState { } } + void _onQrTapped() async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } + + final qrResult = await scanner.scan(); + + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); + + final paymentData = AddressUtils.parsePaymentUri( + qrResult.rawContent, + logging: Logging.instance, + ); + + Logging.instance.log( + "qrResult parsed: $paymentData", + level: LogLevel.Info, + ); + + if (paymentData != null) { + // auto fill address + _address = paymentData.address; + _receiveAddressController.text = _address!; + + setState(() { + _addressToggleFlag = _receiveAddressController.text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else { + _address = qrResult.rawContent; + _receiveAddressController.text = _address ?? ""; + + setState(() { + _addressToggleFlag = _receiveAddressController.text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); + } + } + @override void initState() { _receiveAddressController = TextEditingController(); @@ -1375,63 +1429,7 @@ class _BuyFormState extends ConsumerState { !isDesktop) TextFieldIconButton( key: const Key("buyViewScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); - } - - final qrResult = await scanner.scan(); - - Logging.instance.log( - "qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info, - ); - - final results = AddressUtils.parseUri( - qrResult.rawContent, - ); - - Logging.instance.log( - "qrResult parsed: $results", - level: LogLevel.Info, - ); - - if (results.isNotEmpty) { - // auto fill address - _address = results["address"] ?? ""; - _receiveAddressController.text = _address!; - - setState(() { - _addressToggleFlag = - _receiveAddressController - .text.isNotEmpty; - }); - - // now check for non standard encoded basic address - } else { - _address = qrResult.rawContent; - _receiveAddressController.text = - _address ?? ""; - - setState(() { - _addressToggleFlag = - _receiveAddressController - .text.isNotEmpty; - }); - } - } on PlatformException catch (e, s) { - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning, - ); - } - }, + onTap: _onQrTapped, child: const QrCodeIcon(), ), ], diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index 461dc2935..028ce0ac4 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -70,6 +70,78 @@ class _Step2ViewState extends ConsumerState { bool enableNext = false; + void _onRefundQrTapped() async { + try { + final qrResult = await scanner.scan(); + + final paymentData = AddressUtils.parsePaymentUri( + qrResult.rawContent, + logging: Logging.instance, + ); + + if (paymentData != null) { + // auto fill address + _refundController.text = paymentData.address; + model.refundAddress = _refundController.text; + + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); + } else { + _refundController.text = qrResult.rawContent; + model.refundAddress = _refundController.text; + + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); + } + } + + void _onToQrTapped() async { + try { + final qrResult = await scanner.scan(); + + final paymentData = AddressUtils.parsePaymentUri( + qrResult.rawContent, + logging: Logging.instance, + ); + + if (paymentData != null) { + // auto fill address + _toController.text = paymentData.address; + model.recipientAddress = _toController.text; + + setState(() { + enableNext = _toController.text.isNotEmpty && + (_refundController.text.isNotEmpty || + !ref.read(efExchangeProvider).supportsRefundAddress); + }); + } else { + _toController.text = qrResult.rawContent; + model.recipientAddress = _toController.text; + + setState(() { + enableNext = _toController.text.isNotEmpty && + (_refundController.text.isNotEmpty || + !!ref.read(efExchangeProvider).supportsRefundAddress); + }); + } + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); + } + } + @override void initState() { model = widget.model; @@ -137,7 +209,7 @@ class _Step2ViewState extends ConsumerState { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 75)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -405,50 +477,7 @@ class _Step2ViewState extends ConsumerState { key: const Key( "sendViewScanQrButtonKey", ), - onTap: () async { - try { - final qrResult = - await scanner.scan(); - - final results = - AddressUtils.parseUri( - qrResult.rawContent, - ); - if (results.isNotEmpty) { - // auto fill address - _toController.text = - results["address"] ?? ""; - model.recipientAddress = - _toController.text; - - setState(() { - enableNext = _toController - .text.isNotEmpty && - (_refundController.text - .isNotEmpty || - !supportsRefund); - }); - } else { - _toController.text = - qrResult.rawContent; - model.recipientAddress = - _toController.text; - - setState(() { - enableNext = _toController - .text.isNotEmpty && - (_refundController.text - .isNotEmpty || - !supportsRefund); - }); - } - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning, - ); - } - }, + onTap: _onToQrTapped, child: const QrCodeIcon(), ), ], @@ -685,51 +714,7 @@ class _Step2ViewState extends ConsumerState { key: const Key( "sendViewScanQrButtonKey", ), - onTap: () async { - try { - final qrResult = - await scanner.scan(); - - final results = - AddressUtils.parseUri( - qrResult.rawContent, - ); - if (results.isNotEmpty) { - // auto fill address - _refundController.text = - results["address"] ?? - ""; - model.refundAddress = - _refundController.text; - - setState(() { - enableNext = _toController - .text - .isNotEmpty && - _refundController - .text.isNotEmpty; - }); - } else { - _refundController.text = - qrResult.rawContent; - model.refundAddress = - _refundController.text; - - setState(() { - enableNext = _toController - .text - .isNotEmpty && - _refundController - .text.isNotEmpty; - }); - } - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning, - ); - } - }, + onTap: _onRefundQrTapped, child: const QrCodeIcon(), ), ], diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 581001893..dfe85d934 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -97,7 +97,7 @@ class _GenerateUriQrCodeViewState extends State { initialDirectory: dir.path, ); - if (path != null) { + if (path != null && mounted) { final file = File(path); if (file.existsSync()) { unawaited( @@ -109,13 +109,15 @@ class _GenerateUriQrCodeViewState extends State { ); } else { await file.writeAsBytes(pngBytes); - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "$path saved!", - context: context, - ), - ); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "$path saved!", + context: context, + ), + ); + } } } } else { @@ -167,7 +169,7 @@ class _GenerateUriQrCodeViewState extends State { final Map queryParams = {}; if (amountString.isNotEmpty) { - queryParams["amount"] = amountString; + queryParams["amount"] = amount.toString(); } if (noteString.isNotEmpty) { queryParams["message"] = noteString; @@ -311,7 +313,7 @@ class _GenerateUriQrCodeViewState extends State { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 70)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart index 6d9d9b7c4..ce38153e7 100644 --- a/lib/pages/send_view/frost_ms/recipient.dart +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -120,6 +120,69 @@ class _RecipientState extends ConsumerState { } } + void _onQrTapped() async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75, + ), + ); + } + + final qrResult = await ref.read(pBarcodeScanner).scan(); + + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); + + final paymentData = AddressUtils.parsePaymentUri( + qrResult.rawContent, + logging: Logging.instance, + ); + + Logging.instance.log( + "qrResult parsed: $paymentData", + level: LogLevel.Info, + ); + + if (paymentData != null && + paymentData.coin?.uriScheme == widget.coin.uriScheme) { + // auto fill address + + addressController.text = paymentData.address.trim(); + + // autofill amount field + if (paymentData.amount != null) { + final Amount amount = Decimal.parse(paymentData.amount!).toAmount( + fractionDigits: widget.coin.fractionDigits, + ); + amountController.text = + ref.read(pAmountFormatter(widget.coin)).format( + amount, + withUnitName: false, + ); + } + } else { + addressController.text = qrResult.rawContent.trim(); + } + + setState(() { + _addressIsEmpty = addressController.text.isEmpty; + }); + + _updateRecipientData(); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while " + "trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); + } + } + @override void initState() { addressController = TextEditingController(); @@ -289,76 +352,7 @@ class _RecipientState extends ConsumerState { key: const Key( "sendViewScanQrButtonKey", ), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75, - ), - ); - } - - final qrResult = - await ref.read(pBarcodeScanner).scan(); - - Logging.instance.log( - "qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info, - ); - - /// TODO: deal with address utils - final results = - AddressUtils.parseUri(qrResult.rawContent); - - Logging.instance.log( - "qrResult parsed: $results", - level: LogLevel.Info, - ); - - if (results.isNotEmpty && - results["scheme"] == - widget.coin.uriScheme) { - // auto fill address - - addressController.text = - (results["address"] ?? "").trim(); - - // autofill amount field - if (results["amount"] != null) { - final Amount amount = - Decimal.parse(results["amount"]!) - .toAmount( - fractionDigits: - widget.coin.fractionDigits, - ); - amountController.text = ref - .read(pAmountFormatter(widget.coin)) - .format( - amount, - withUnitName: false, - ); - } - } else { - addressController.text = - qrResult.rawContent.trim(); - } - - setState(() { - _addressIsEmpty = - addressController.text.isEmpty; - }); - - _updateRecipientData(); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while " - "trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning, - ); - } - }, + onTap: _onQrTapped, child: const QrCodeIcon(), ), ], diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 584d868d2..1dd9b68ef 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -11,10 +11,10 @@ import 'dart:async'; import 'dart:io'; +import 'package:cs_monero/cs_monero.dart' as lib_monero; import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:cs_monero/cs_monero.dart' as lib_monero; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:tuple/tuple.dart'; @@ -163,9 +163,13 @@ class _SendViewState extends ConsumerState { level: LogLevel.Info, ); - final paymentData = AddressUtils.parsePaymentUri(qrResult.rawContent); + final paymentData = AddressUtils.parsePaymentUri( + qrResult.rawContent, + logging: Logging.instance, + ); - if (paymentData.coin.uriScheme == coin.uriScheme) { + if (paymentData != null && + paymentData.coin?.uriScheme == coin.uriScheme) { // auto fill address _address = paymentData.address.trim(); sendToController.text = _address!; @@ -195,12 +199,8 @@ class _SendViewState extends ConsumerState { }); // now check for non standard encoded basic address - } else if (ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(qrResult.rawContent)) { - _address = qrResult.rawContent.trim(); + } else { + _address = qrResult.rawContent.split("\n").first.trim(); sendToController.text = _address ?? ""; _setValidAddressProviders(_address); diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 7f82cc0e5..3ad00fe40 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -163,25 +163,30 @@ class _TokenSendViewState extends ConsumerState { level: LogLevel.Info, ); - final results = AddressUtils.parseUri(qrResult.rawContent); + final paymentData = AddressUtils.parsePaymentUri( + qrResult.rawContent, + logging: Logging.instance, + ); - Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info); + Logging.instance + .log("qrResult parsed: $paymentData", level: LogLevel.Info); - if (results.isNotEmpty && results["scheme"] == coin.uriScheme) { + if (paymentData != null && + paymentData.coin?.uriScheme == coin.uriScheme) { // auto fill address - _address = (results["address"] ?? "").trim(); + _address = paymentData.address.trim(); sendToController.text = _address!; // autofill notes field - if (results["message"] != null) { - noteController.text = results["message"]!; - } else if (results["label"] != null) { - noteController.text = results["label"]!; + if (paymentData.message != null) { + noteController.text = paymentData.message!; + } else if (paymentData.label != null) { + noteController.text = paymentData.label!; } // autofill amount field - if (results["amount"] != null) { - final Amount amount = Decimal.parse(results["amount"]!).toAmount( + if (paymentData.amount != null) { + final Amount amount = Decimal.parse(paymentData.amount!).toAmount( fractionDigits: tokenContract.decimals, ); cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( @@ -198,12 +203,8 @@ class _TokenSendViewState extends ConsumerState { }); // now check for non standard encoded basic address - } else if (ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(qrResult.rawContent)) { - _address = qrResult.rawContent.trim(); + } else { + _address = qrResult.rawContent.split("\n").first.trim(); sendToController.text = _address ?? ""; _updatePreviewButtonState(_address, _amountToSend); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 80dea6795..7fb764cfa 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -10,11 +10,11 @@ import 'dart:async'; +import 'package:cs_monero/cs_monero.dart' as lib_monero; import 'package:decimal/decimal.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:cs_monero/cs_monero.dart' as lib_monero; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -145,7 +145,7 @@ class _DesktopSendState extends ConsumerState { Future scanWebcam() async { try { - await showDialog( + await showDialog( context: context, builder: (context) { return QrCodeScannerDialog( @@ -153,16 +153,20 @@ class _DesktopSendState extends ConsumerState { try { _processQrCodeData(qrCodeData); } catch (e, s) { - Logging.instance.log("Error processing QR code data: $e\n$s", - level: LogLevel.Error); + Logging.instance.log( + "Error processing QR code data: $e\n$s", + level: LogLevel.Error, + ); } }, ); }, ); } catch (e, s) { - Logging.instance.log("Error opening QR code scanner dialog: $e\n$s", - level: LogLevel.Error); + Logging.instance.log( + "Error opening QR code scanner dialog: $e\n$s", + level: LogLevel.Error, + ); } } @@ -655,84 +659,15 @@ class _DesktopSendState extends ConsumerState { // return null; // } - Future scanQr() async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 75)); - } - - final qrResult = await scanner.scan(); - - Logging.instance.log( - "qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info, - ); - - final results = AddressUtils.parseUri(qrResult.rawContent); - - Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info); - - if (results.isNotEmpty && results["scheme"] == coin.uriScheme) { - // auto fill address - _address = results["address"] ?? ""; - sendToController.text = _address!; - - // autofill notes field - if (results["message"] != null) { - _note = results["message"]!; - } else if (results["label"] != null) { - _note = results["label"]!; - } - - // autofill amount field - if (results["amount"] != null) { - final amount = Decimal.parse(results["amount"]!).toAmount( - fractionDigits: coin.fractionDigits, - ); - cryptoAmountController.text = ref - .read(pAmountFormatter(coin)) - .format(amount, withUnitName: false); - ref.read(pSendAmount.notifier).state = amount; - } - - setState(() { - _addressToggleFlag = sendToController.text.isNotEmpty; - }); - - // now check for non standard encoded basic address - } else if (ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(qrResult.rawContent)) { - _address = qrResult.rawContent; - sendToController.text = _address ?? ""; - - _setValidAddressProviders(_address); - setState(() { - _addressToggleFlag = sendToController.text.isNotEmpty; - }); - } - } on PlatformException catch (e, s) { - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning, - ); - } catch (e, s) { - Logging.instance.log( - "Failed to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning, - ); - } - } - void _processQrCodeData(String qrCodeData) { try { - final paymentData = AddressUtils.parsePaymentUri(qrCodeData); - if (paymentData.coin.uriScheme == coin.uriScheme) { + final paymentData = AddressUtils.parsePaymentUri( + qrCodeData, + logging: Logging.instance, + ); + + if (paymentData != null && + paymentData.coin?.uriScheme == coin.uriScheme) { // Auto fill address. _address = paymentData.address.trim(); sendToController.text = _address!; @@ -756,6 +691,14 @@ class _DesktopSendState extends ConsumerState { _note = paymentData.label; } + _setValidAddressProviders(_address); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } else { + _address = qrCodeData.split("\n").first.trim(); + sendToController.text = _address ?? ""; + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = sendToController.text.isNotEmpty; @@ -807,8 +750,12 @@ class _DesktopSendState extends ConsumerState { } try { - final paymentData = AddressUtils.parsePaymentUri(content); - if (paymentData.coin.uriScheme == coin.uriScheme) { + final paymentData = AddressUtils.parsePaymentUri( + content, + logging: Logging.instance, + ); + if (paymentData != null && + paymentData.coin?.uriScheme == coin.uriScheme) { // auto fill address _address = paymentData.address; sendToController.text = _address!; @@ -837,6 +784,7 @@ class _DesktopSendState extends ConsumerState { _addressToggleFlag = sendToController.text.isNotEmpty; }); } else { + content = content.split("\n").first.trim(); if (coin is Epiccash) { content = AddressUtils().formatAddress(content); } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index 490465490..e05a9f46a 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -14,14 +14,12 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../../../models/isar/models/contact_entry.dart'; import '../../../../models/paynym/paynym_account_lite.dart'; import '../../../../models/send_view_auto_fill_data.dart'; import '../../../../pages/send_view/confirm_transaction_view.dart'; import '../../../../pages/send_view/sub_widgets/building_transaction_dialog.dart'; -import '../../../desktop_home_view.dart'; -import 'address_book_address_chooser/address_book_address_chooser.dart'; -import 'desktop_fee_dropdown.dart'; import '../../../../providers/providers.dart'; import '../../../../providers/ui/fee_rate_type_state_provider.dart'; import '../../../../providers/ui/preview_tx_button_state_provider.dart'; @@ -51,6 +49,9 @@ import '../../../../widgets/icon_widgets/clipboard_icon.dart'; import '../../../../widgets/icon_widgets/x_icon.dart'; import '../../../../widgets/stack_text_field.dart'; import '../../../../widgets/textfield_icon_button.dart'; +import '../../../desktop_home_view.dart'; +import 'address_book_address_chooser/address_book_address_chooser.dart'; +import 'desktop_fee_dropdown.dart'; // const _kCryptoAmountRegex = r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$'; @@ -480,25 +481,30 @@ class _DesktopTokenSendState extends ConsumerState { level: LogLevel.Info, ); - final results = AddressUtils.parseUri(qrResult.rawContent); + final paymentData = AddressUtils.parsePaymentUri( + qrResult.rawContent, + logging: Logging.instance, + ); - Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info); + Logging.instance + .log("qrResult parsed: $paymentData", level: LogLevel.Info); - if (results.isNotEmpty && results["scheme"] == coin.uriScheme) { + if (paymentData != null && + paymentData.coin?.uriScheme == coin.uriScheme) { // auto fill address - _address = results["address"] ?? ""; + _address = paymentData.address.trim(); sendToController.text = _address!; // autofill notes field - if (results["message"] != null) { - _note = results["message"]!; - } else if (results["label"] != null) { - _note = results["label"]!; + if (paymentData.message != null) { + _note = paymentData.message!; + } else if (paymentData.label != null) { + _note = paymentData.label!; } // autofill amount field - if (results["amount"] != null) { - final amount = Decimal.parse(results["amount"]!).toAmount( + if (paymentData.amount != null) { + final Amount amount = Decimal.parse(paymentData.amount!).toAmount( fractionDigits: ref.read(pCurrentTokenWallet)!.tokenContract.decimals, ); @@ -516,12 +522,8 @@ class _DesktopTokenSendState extends ConsumerState { }); // now check for non standard encoded basic address - } else if (ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(qrResult.rawContent)) { - _address = qrResult.rawContent; + } else { + _address = qrResult.rawContent.split("\n").first.trim(); sendToController.text = _address ?? ""; _updatePreviewButtonState(_address, _amountToSend); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 869aa5895..2e6b241b8 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -12,6 +12,7 @@ import 'dart:convert'; import '../app_config.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; +import 'logger.dart'; class AddressUtils { static final Set recognizedParams = { @@ -29,166 +30,16 @@ class AddressUtils { return '${address.substring(0, 5)}...${address.substring(address.length - 5)}'; } - // static bool validateAddress(String address, Coin coin) { - // //This calls the validate address for each crypto coin, validateAddress is - // //only used in 2 places, so I just replaced the old functionality here - // switch (coin) { - // case Coin.bitcoin: - // return Bitcoin(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.bitcoinFrost: - // return BitcoinFrost(CryptoCurrencyNetwork.main) - // .validateAddress(address); - // case Coin.litecoin: - // return Litecoin(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.bitcoincash: - // return Bitcoincash(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.dogecoin: - // return Dogecoin(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.epicCash: - // return Epiccash(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.ethereum: - // return Ethereum(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.firo: - // return Firo(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.eCash: - // return Ecash(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.monero: - // return Monero(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.wownero: - // return Wownero(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.namecoin: - // return Namecoin(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.particl: - // return Particl(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.peercoin: - // return Peercoin(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.solana: - // return Solana(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.stellar: - // return Stellar(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.nano: - // return Nano(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.banano: - // return Banano(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.tezos: - // return Tezos(CryptoCurrencyNetwork.main).validateAddress(address); - // case Coin.bitcoinTestNet: - // return Bitcoin(CryptoCurrencyNetwork.test).validateAddress(address); - // case Coin.bitcoinFrostTestNet: - // return BitcoinFrost(CryptoCurrencyNetwork.test) - // .validateAddress(address); - // case Coin.litecoinTestNet: - // return Litecoin(CryptoCurrencyNetwork.test).validateAddress(address); - // case Coin.bitcoincashTestnet: - // return Bitcoincash(CryptoCurrencyNetwork.test).validateAddress(address); - // case Coin.firoTestNet: - // return Firo(CryptoCurrencyNetwork.test).validateAddress(address); - // case Coin.dogecoinTestNet: - // return Dogecoin(CryptoCurrencyNetwork.test).validateAddress(address); - // case Coin.peercoinTestNet: - // return Peercoin(CryptoCurrencyNetwork.test).validateAddress(address); - // case Coin.stellarTestnet: - // return Stellar(CryptoCurrencyNetwork.test).validateAddress(address); - // } - // // throw Exception("moved"); - // // switch (coin) { - // // case Coin.bitcoin: - // // return Address.validateAddress(address, bitcoin); - // // case Coin.litecoin: - // // return Address.validateAddress(address, litecoin); - // // case Coin.bitcoincash: - // // try { - // // // 0 for bitcoincash: address scheme, 1 for legacy address - // // final format = bitbox.Address.detectFormat(address); - // // - // // if (coin == Coin.bitcoincashTestnet) { - // // return true; - // // } - // // - // // if (format == bitbox.Address.formatCashAddr) { - // // String addr = address; - // // if (addr.contains(":")) { - // // addr = addr.split(":").last; - // // } - // // - // // return addr.startsWith("q"); - // // } else { - // // return address.startsWith("1"); - // // } - // // } catch (e) { - // // return false; - // // } - // // case Coin.dogecoin: - // // return Address.validateAddress(address, dogecoin); - // // case Coin.epicCash: - // // return validateSendAddress(address) == "1"; - // // case Coin.ethereum: - // // return true; //TODO - validate ETH address - // // case Coin.firo: - // // return Address.validateAddress(address, firoNetwork); - // // case Coin.eCash: - // // return Address.validateAddress(address, eCashNetwork); - // // case Coin.monero: - // // return RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || - // // RegExp("[a-zA-Z0-9]{106}").hasMatch(address); - // // case Coin.wownero: - // // return RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || - // // RegExp("[a-zA-Z0-9]{106}").hasMatch(address); - // // case Coin.namecoin: - // // return Address.validateAddress(address, namecoin, namecoin.bech32!); - // // case Coin.particl: - // // return Address.validateAddress(address, particl); - // // case Coin.stellar: - // // return RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address); - // // case Coin.nano: - // // return NanoAccounts.isValid(NanoAccountType.NANO, address); - // // case Coin.banano: - // // return NanoAccounts.isValid(NanoAccountType.BANANO, address); - // // case Coin.tezos: - // // return RegExp(r"^tz[1-9A-HJ-NP-Za-km-z]{34}$").hasMatch(address); - // // case Coin.bitcoinTestNet: - // // return Address.validateAddress(address, testnet); - // // case Coin.litecoinTestNet: - // // return Address.validateAddress(address, litecointestnet); - // // case Coin.bitcoincashTestnet: - // // try { - // // // 0 for bitcoincash: address scheme, 1 for legacy address - // // final format = bitbox.Address.detectFormat(address); - // // - // // if (coin == Coin.bitcoincashTestnet) { - // // return true; - // // } - // // - // // if (format == bitbox.Address.formatCashAddr) { - // // String addr = address; - // // if (addr.contains(":")) { - // // addr = addr.split(":").last; - // // } - // // - // // return addr.startsWith("q"); - // // } else { - // // return address.startsWith("1"); - // // } - // // } catch (e) { - // // return false; - // // } - // // case Coin.firoTestNet: - // // return Address.validateAddress(address, firoTestNetwork); - // // case Coin.dogecoinTestNet: - // // return Address.validateAddress(address, dogecointestnet); - // // case Coin.stellarTestnet: - // // return RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address); - // // } - // } - /// Return only recognized parameters. - static Map filterParams(Map params) { - return Map.fromEntries(params.entries - .where((entry) => recognizedParams.contains(entry.key.toLowerCase()))); + static Map _filterParams(Map params) { + return Map.fromEntries( + params.entries + .where((entry) => recognizedParams.contains(entry.key.toLowerCase())), + ); } /// Parses a URI string and returns a map with parsed components. - static Map parseUri(String uri) { + static Map _parseUri(String uri) { final Map result = {}; try { final u = Uri.parse(uri); @@ -273,28 +124,36 @@ class AddressUtils { } /// Centralized method to handle various cryptocurrency URIs and return a common object. - static PaymentUriData parsePaymentUri(String uri) { - final Map parsedData = parseUri(uri); + /// + /// Returns null on failure to parse + static PaymentUriData? parsePaymentUri( + String uri, { + Logging? logging, + }) { + try { + final Map parsedData = _parseUri(uri); - // Normalize the URI scheme. - final String scheme = parsedData['scheme'] ?? ''; - parsedData.remove('scheme'); + // Normalize the URI scheme. + final String scheme = parsedData['scheme'] ?? ''; + parsedData.remove('scheme'); - // Determine the coin type based on the URI scheme. - final CryptoCurrency coin = _getCryptoCurrencyByScheme(scheme); + // Filter out unrecognized parameters. + final filteredParams = _filterParams(parsedData); - // Filter out unrecognized parameters. - final filteredParams = filterParams(parsedData); - - return PaymentUriData( - coin: coin, - address: parsedData['address'] ?? '', - amount: filteredParams['amount'] ?? filteredParams['tx_amount'], - label: filteredParams['label'] ?? filteredParams['recipient_name'], - message: filteredParams['message'] ?? filteredParams['tx_description'], - paymentId: filteredParams['tx_payment_id'], // Specific to Monero - additionalParams: filteredParams, - ); + return PaymentUriData( + scheme: scheme, + address: parsedData['address']!.trim(), + amount: filteredParams['amount'] ?? filteredParams['tx_amount'], + label: filteredParams['label'] ?? filteredParams['recipient_name'], + message: filteredParams['message'] ?? filteredParams['tx_description'], + paymentId: filteredParams['tx_payment_id'], + // Specific to Monero + additionalParams: filteredParams, + ); + } catch (e, s) { + logging?.log("$e\n$s", level: LogLevel.Error); + return null; + } } /// Builds a uri string with the given address and query parameters (if any) @@ -304,7 +163,7 @@ class AddressUtils { Map params, ) { // Filter unrecognized parameters. - final filteredParams = filterParams(params); + final filteredParams = _filterParams(params); String uriString = "$scheme:$address"; if (scheme.toLowerCase() == "monero") { @@ -345,11 +204,12 @@ class AddressUtils { } /// Method to get CryptoCurrency based on URI scheme. - static CryptoCurrency _getCryptoCurrencyByScheme(String scheme) { + static CryptoCurrency? _getCryptoCurrencyByScheme(String scheme) { if (AppConfig.coins.map((e) => e.uriScheme).toSet().contains(scheme)) { return AppConfig.coins.firstWhere((e) => e.uriScheme == scheme); } else { - throw UnsupportedError('Unsupported URI scheme: $scheme'); + return null; + // throw UnsupportedError('Unsupported URI scheme: $scheme'); } } @@ -375,21 +235,37 @@ class AddressUtils { } class PaymentUriData { - final CryptoCurrency coin; final String address; + final String? scheme; final String? amount; final String? label; final String? message; final String? paymentId; // Specific to Monero. final Map additionalParams; + CryptoCurrency? get coin => AddressUtils._getCryptoCurrencyByScheme( + scheme ?? "", // empty will just return null + ); + PaymentUriData({ - required this.coin, required this.address, + this.scheme, this.amount, this.label, this.message, this.paymentId, required this.additionalParams, }); + + @override + String toString() => "PaymentUriData { " + "coin: $coin, " + "address: $address, " + "amount: $amount, " + "scheme: $scheme, " + "label: $label, " + "message: $message, " + "paymentId: $paymentId, " + "additionalParams: $additionalParams" + " }"; } diff --git a/pubspec.lock b/pubspec.lock index aed695685..7cee10d78 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1809,18 +1809,18 @@ packages: dependency: "direct main" description: name: sqlite3 - sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb + sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295 url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.3" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "7ae52b23366e5295005022e62fa093f64bfe190810223ea0ebf733a4cd140bce" + sha256: "1e62698dc1ab396152ccaf3b3990d826244e9f3c8c39b51805f209adcd6dbea3" url: "https://pub.dev" source: hosted - version: "0.5.26" + version: "0.5.22" stack_trace: dependency: transitive description: diff --git a/test/address_utils_test.dart b/test/address_utils_test.dart index 1b0c69a2c..599380c31 100644 --- a/test/address_utils_test.dart +++ b/test/address_utils_test.dart @@ -12,52 +12,49 @@ void main() { test("parse a valid uri string A", () { const uri = "dogecoin:$firoAddress?amount=50&label=eggs"; - final result = AddressUtils.parseUri(uri); - expect(result, { - "scheme": "dogecoin", - "address": firoAddress, - "amount": "50", - "label": "eggs", - }); + final result = AddressUtils.parsePaymentUri(uri); + expect(result, isNotNull); + expect(result!.scheme, "dogecoin"); + expect(result.address, firoAddress); + expect(result.amount, "50"); + expect(result.label, "eggs"); }); test("parse a valid uri string B", () { const uri = "firo:$firoAddress?amount=50&message=eggs+are+good"; - final result = AddressUtils.parseUri(uri); - expect(result, { - "scheme": "firo", - "address": firoAddress, - "amount": "50", - "message": "eggs are good", - }); + final result = AddressUtils.parsePaymentUri(uri); + expect(result, isNotNull); + expect(result!.scheme, "firo"); + expect(result.address, firoAddress); + expect(result.amount, "50"); + expect(result.message, "eggs are good"); }); test("parse a valid uri string C", () { const uri = "bitcoin:$firoAddress?amount=50.1&message=eggs%20are%20good%21"; - final result = AddressUtils.parseUri(uri); - expect(result, { - "scheme": "bitcoin", - "address": firoAddress, - "amount": "50.1", - "message": "eggs are good!", - }); + final result = AddressUtils.parsePaymentUri(uri); + expect(result, isNotNull); + expect(result!.scheme, "bitcoin"); + expect(result.address, firoAddress); + expect(result.amount, "50.1"); + expect(result.message, "eggs are good!"); }); test("parse an invalid uri string", () { const uri = "firo$firoAddress?amount=50&label=eggs"; - final result = AddressUtils.parseUri(uri); - expect(result, {}); + final result = AddressUtils.parsePaymentUri(uri); + expect(result, isNull); }); test("parse an invalid string", () { const uri = "$firoAddress?amount=50&label=eggs"; - final result = AddressUtils.parseUri(uri); - expect(result, {}); + final result = AddressUtils.parsePaymentUri(uri); + expect(result, isNull); }); test("parse an invalid uri string", () { const uri = "::: 8 \\ %23"; - expect(AddressUtils.parseUri(uri), {}); + expect(AddressUtils.parsePaymentUri(uri), isNull); }); test("encode a list of (mnemonic) words/strings as a json object", () {