diff --git a/lib/pages/receive_view/addresses/address_card.dart b/lib/pages/receive_view/addresses/address_card.dart index db14fd979..59074d979 100644 --- a/lib/pages/receive_view/addresses/address_card.dart +++ b/lib/pages/receive_view/addresses/address_card.dart @@ -304,7 +304,7 @@ class _AddressCardState extends ConsumerState { key: _qrKey, child: QR( data: AddressUtils.buildUriString( - widget.coin, + widget.coin.uriScheme, address.value, {}, ), diff --git a/lib/pages/receive_view/addresses/address_details_view.dart b/lib/pages/receive_view/addresses/address_details_view.dart index 2b0e09187..2006e900b 100644 --- a/lib/pages/receive_view/addresses/address_details_view.dart +++ b/lib/pages/receive_view/addresses/address_details_view.dart @@ -97,7 +97,7 @@ class _AddressDetailsViewState extends ConsumerState { key: _qrKey, child: QR( data: AddressUtils.buildUriString( - ref.watch(pWalletCoin(widget.walletId)), + ref.watch(pWalletCoin(widget.walletId)).uriScheme, address.value, {}, ), @@ -289,7 +289,7 @@ class _AddressDetailsViewState extends ConsumerState { key: _qrKey, child: QR( data: AddressUtils.buildUriString( - coin, + coin.uriScheme, address.value, {}, ), diff --git a/lib/pages/receive_view/addresses/address_qr_popup.dart b/lib/pages/receive_view/addresses/address_qr_popup.dart index 5a8bc1592..b4446f543 100644 --- a/lib/pages/receive_view/addresses/address_qr_popup.dart +++ b/lib/pages/receive_view/addresses/address_qr_popup.dart @@ -142,7 +142,7 @@ class _AddressQrPopupState extends State { key: _qrKey, child: QR( data: AddressUtils.buildUriString( - widget.coin, + widget.coin.uriScheme, widget.addressString, {}, ), 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 7a7497d0e..f9049a538 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 @@ -170,7 +170,7 @@ class _GenerateUriQrCodeViewState extends State { } final uriString = AddressUtils.buildUriString( - widget.coin, + widget.coin.uriScheme, receivingAddress, queryParams, ); @@ -263,7 +263,7 @@ class _GenerateUriQrCodeViewState extends State { } _uriString = AddressUtils.buildUriString( - widget.coin, + widget.coin.uriScheme, receivingAddress, {}, ); diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 2beaab5f2..a40262cdc 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -577,7 +577,7 @@ class _ReceiveViewState extends ConsumerState { children: [ QR( data: AddressUtils.buildUriString( - coin, + coin.uriScheme, address, {}, ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index a792641ba..a96a01b8f 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -448,7 +448,7 @@ class _DesktopReceiveState extends ConsumerState { Center( child: QR( data: AddressUtils.buildUriString( - coin, + coin.uriScheme, address, {}, ), 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 9f6f545f6..c9c887fee 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 @@ -841,6 +841,8 @@ class _DesktopSendState extends ConsumerState { ref.read(pSendAmount.notifier).state = amount; } + // Trigger validation after pasting. + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = sendToController.text.isNotEmpty; }); @@ -856,6 +858,7 @@ class _DesktopSendState extends ConsumerState { sendToController.text = content; _address = content; + // Trigger validation after pasting. _setValidAddressProviders(_address); setState(() { _addressToggleFlag = sendToController.text.isNotEmpty; diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 788bde4d5..7b00c242a 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -10,11 +10,19 @@ import 'dart:convert'; -import 'logger.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; class AddressUtils { - static final Set recognizedParams = {'amount', 'label', 'message'}; + static final Set recognizedParams = { + 'amount', + 'label', + 'message', + 'tx_amount', // For Monero/Wownero. + 'tx_payment_id', + 'recipient_name', + 'tx_description', + // TODO [prio=med]: Add more recognized params for other coins. + }; static String condenseAddress(String address) { return '${address.substring(0, 5)}...${address.substring(address.length - 5)}'; @@ -178,43 +186,144 @@ class AddressUtils { .where((entry) => recognizedParams.contains(entry.key.toLowerCase()))); } - /// Parses a URI string. + /// Parses a URI string and returns a map with parsed components. static Map parseUri(String uri) { final Map result = {}; try { final u = Uri.parse(uri); if (u.hasScheme) { result["scheme"] = u.scheme.toLowerCase(); - result["address"] = u.path; - result.addAll(u.queryParameters); + + // Handle different URI formats. + if (result["scheme"] == "bitcoin" || + result["scheme"] == "bitcoincash") { + result["address"] = u.path; + } else if (result["scheme"] == "monero") { + // Monero addresses can contain '?' which Uri.parse interprets as query start. + final addressEnd = + uri.indexOf('?', 7); // 7 is the length of "monero:". + if (addressEnd != -1) { + result["address"] = uri.substring(7, addressEnd); + } else { + result["address"] = uri.substring(7); + } + } else { + // Default case, treat path as address. + result["address"] = u.path; + } + + // Parse query parameters. + result.addAll(_parseQueryParameters(u.queryParameters)); + + // Handle Monero-specific fragment (tx_description). + if (u.fragment.isNotEmpty && result["scheme"] == "monero") { + result["tx_description"] = Uri.decodeComponent(u.fragment); + } } } catch (e) { - Logging.instance - .log("Exception caught in parseUri($uri): $e", level: LogLevel.Error); + print("Exception caught in parseUri($uri): $e"); } return result; } - /// builds a uri string with the given address and query parameters if any + /// Helper method to parse and normalize query parameters. + static Map _parseQueryParameters(Map params) { + final Map result = {}; + params.forEach((key, value) { + final lowerKey = key.toLowerCase(); + if (recognizedParams.contains(lowerKey)) { + switch (lowerKey) { + case 'amount': + case 'tx_amount': + result['amount'] = _normalizeAmount(value); + break; + case 'label': + case 'recipient_name': + result['label'] = Uri.decodeComponent(value); + break; + case 'message': + case 'tx_description': + result['message'] = Uri.decodeComponent(value); + break; + case 'tx_payment_id': + result['tx_payment_id'] = Uri.decodeComponent(value); + break; + default: + result[lowerKey] = Uri.decodeComponent(value); + } + } else { + // Include unrecognized parameters as-is. + result[key] = Uri.decodeComponent(value); + } + }); + return result; + } + + /// Normalizes amount value to a standard format. + static String _normalizeAmount(String amount) { + // Remove any non-numeric characters except for '.' + final sanitized = amount.replaceAll(RegExp(r'[^\d.]'), ''); + // Ensure only one decimal point + final parts = sanitized.split('.'); + if (parts.length > 2) { + return '${parts[0]}.${parts.sublist(1).join()}'; + } + return sanitized; + } + + /// Centralized method to handle various cryptocurrency URIs and return a common object. + static PaymentUriData parsePaymentUri(String uri) { + final Map parsedData = parseUri(uri); + + // 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); + + 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, + ); + } + + /// Builds a uri string with the given address and query parameters (if any) static String buildUriString( - CryptoCurrency coin, + String scheme, String address, Map params, ) { - // TODO: other sanitation as well ? - String sanitizedAddress = address; - if (coin is Bitcoincash || coin is Ecash) { - final prefix = "${coin.uriScheme}:"; - if (address.startsWith(prefix)) { - sanitizedAddress = address.replaceFirst(prefix, ""); - } - } // Filter unrecognized parameters. final filteredParams = filterParams(params); - String uriString = "${coin.uriScheme}:$sanitizedAddress"; - if (filteredParams.isNotEmpty) { - uriString += Uri(queryParameters: filteredParams).toString(); + String uriString = "$scheme:$address"; + + if (scheme.toLowerCase() == "monero") { + // Handle Monero-specific formatting. + if (filteredParams.containsKey("tx_description")) { + final description = filteredParams.remove("tx_description")!; + if (filteredParams.isNotEmpty) { + uriString += Uri(queryParameters: filteredParams).toString(); + } + uriString += "#${Uri.encodeComponent(description)}"; + } else if (filteredParams.isNotEmpty) { + uriString += Uri(queryParameters: filteredParams).toString(); + } + } else { + // General case for other cryptocurrencies. + if (filteredParams.isNotEmpty) { + uriString += Uri(queryParameters: filteredParams).toString(); + } } + return uriString; } @@ -224,10 +333,7 @@ class AddressUtils { try { result = Map.from(jsonDecode(data) as Map); } catch (e) { - Logging.instance.log( - "Exception caught in parseQRSeedData($data): $e", - level: LogLevel.Error, - ); + print("Exception caught in parseQRSeedData($data): $e"); } return result; } @@ -236,4 +342,41 @@ class AddressUtils { static String encodeQRSeedData(List words) { return jsonEncode({"mnemonic": words}); } + + /// Method to get CryptoCurrency based on URI scheme. + static CryptoCurrency _getCryptoCurrencyByScheme(String scheme) { + switch (scheme) { + case 'bitcoin': + return Bitcoin(CryptoCurrencyNetwork.main); + case 'bitcoincash': + return Bitcoincash(CryptoCurrencyNetwork.main); + case 'ethereum': + return Ethereum(CryptoCurrencyNetwork.main); + case 'monero': + return Monero(CryptoCurrencyNetwork.main); + // Add more cases as needed for other coins + default: + throw UnsupportedError('Unsupported URI scheme: $scheme'); + } + } +} + +class PaymentUriData { + final CryptoCurrency coin; + final String address; + final String? amount; + final String? label; + final String? message; + final String? paymentId; // Specific to Monero. + final Map additionalParams; + + PaymentUriData({ + required this.coin, + required this.address, + this.amount, + this.label, + this.message, + this.paymentId, + required this.additionalParams, + }); }