/* * This file is part of Stack Wallet. * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. * Generated by Cypher Stack on 2023-05-26 * */ import 'dart:convert'; import '../app_config.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; import 'logger.dart'; class AddressUtils { 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)}'; } /// Return only recognized parameters. 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) { final Map result = {}; try { final u = Uri.parse(uri); if (u.hasScheme) { result["scheme"] = u.scheme.toLowerCase(); // 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) { print("Exception caught in parseUri($uri): $e"); } return result; } /// 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. /// /// 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'); // Filter out unrecognized parameters. final filteredParams = _filterParams(parsedData); 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) static String buildUriString( String scheme, String address, Map params, ) { // Filter unrecognized parameters. final filteredParams = _filterParams(params); 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; } /// returns empty if bad data static Map decodeQRSeedData(String data) { Map result = {}; try { result = Map.from(jsonDecode(data) as Map); } catch (e) { print("Exception caught in parseQRSeedData($data): $e"); } return result; } /// encode mnemonic words to qrcode formatted string static String encodeQRSeedData(List words) { return jsonEncode({"mnemonic": words}); } /// Method to get CryptoCurrency based on URI 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 { return null; // throw UnsupportedError('Unsupported URI scheme: $scheme'); } } /// Formats an address string to remove any unnecessary prefixes or suffixes. String formatAddress(String epicAddress) { // strip http:// or https:// prefixes if the address contains an @ symbol (and is thus an epicbox address) if ((epicAddress.startsWith("http://") || epicAddress.startsWith("https://")) && epicAddress.contains("@")) { epicAddress = epicAddress.replaceAll("http://", ""); epicAddress = epicAddress.replaceAll("https://", ""); } // strip mailto: prefix if (epicAddress.startsWith("mailto:")) { epicAddress = epicAddress.replaceAll("mailto:", ""); } // strip / suffix if the address contains an @ symbol (and is thus an epicbox address) if (epicAddress.endsWith("/") && epicAddress.contains("@")) { epicAddress = epicAddress.substring(0, epicAddress.length - 1); } return epicAddress; } } class PaymentUriData { 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.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" " }"; }