2023-05-26 21:21:16 +00:00
|
|
|
/*
|
|
|
|
* 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
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2022-08-26 08:11:35 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
|
2024-05-23 00:37:06 +00:00
|
|
|
import '../wallets/crypto_currency/crypto_currency.dart';
|
2022-08-26 08:11:35 +00:00
|
|
|
|
|
|
|
class AddressUtils {
|
2024-08-27 21:41:30 +00:00
|
|
|
static final Set<String> 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.
|
|
|
|
};
|
2024-07-11 23:56:54 +00:00
|
|
|
|
2022-08-26 08:11:35 +00:00
|
|
|
static String condenseAddress(String address) {
|
|
|
|
return '${address.substring(0, 5)}...${address.substring(address.length - 5)}';
|
|
|
|
}
|
|
|
|
|
2024-05-15 21:20:45 +00:00
|
|
|
// 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);
|
|
|
|
// // }
|
|
|
|
// }
|
2022-08-26 08:11:35 +00:00
|
|
|
|
2024-07-11 23:56:54 +00:00
|
|
|
/// Return only recognized parameters.
|
|
|
|
static Map<String, String> filterParams(Map<String, String> params) {
|
|
|
|
return Map.fromEntries(params.entries
|
|
|
|
.where((entry) => recognizedParams.contains(entry.key.toLowerCase())));
|
|
|
|
}
|
|
|
|
|
2024-08-27 21:41:30 +00:00
|
|
|
/// Parses a URI string and returns a map with parsed components.
|
2022-08-26 08:11:35 +00:00
|
|
|
static Map<String, String> parseUri(String uri) {
|
2024-05-15 21:20:45 +00:00
|
|
|
final Map<String, String> result = {};
|
2022-08-26 08:11:35 +00:00
|
|
|
try {
|
|
|
|
final u = Uri.parse(uri);
|
|
|
|
if (u.hasScheme) {
|
|
|
|
result["scheme"] = u.scheme.toLowerCase();
|
2024-08-27 21:41:30 +00:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2024-08-27 21:41:30 +00:00
|
|
|
print("Exception caught in parseUri($uri): $e");
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2024-08-27 21:41:30 +00:00
|
|
|
/// Helper method to parse and normalize query parameters.
|
|
|
|
static Map<String, String> _parseQueryParameters(Map<String, String> params) {
|
|
|
|
final Map<String, String> 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<String, String> 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)
|
2022-08-26 08:11:35 +00:00
|
|
|
static String buildUriString(
|
2024-08-27 21:41:30 +00:00
|
|
|
String scheme,
|
2022-08-26 08:11:35 +00:00
|
|
|
String address,
|
|
|
|
Map<String, String> params,
|
|
|
|
) {
|
2024-07-11 23:56:54 +00:00
|
|
|
// Filter unrecognized parameters.
|
|
|
|
final filteredParams = filterParams(params);
|
2024-08-27 21:41:30 +00:00
|
|
|
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();
|
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
2024-08-27 21:41:30 +00:00
|
|
|
|
2022-08-26 08:11:35 +00:00
|
|
|
return uriString;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// returns empty if bad data
|
|
|
|
static Map<String, dynamic> decodeQRSeedData(String data) {
|
|
|
|
Map<String, dynamic> result = {};
|
|
|
|
try {
|
|
|
|
result = Map<String, dynamic>.from(jsonDecode(data) as Map);
|
|
|
|
} catch (e) {
|
2024-08-27 21:41:30 +00:00
|
|
|
print("Exception caught in parseQRSeedData($data): $e");
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// encode mnemonic words to qrcode formatted string
|
|
|
|
static String encodeQRSeedData(List<String> words) {
|
|
|
|
return jsonEncode({"mnemonic": words});
|
|
|
|
}
|
2024-08-27 21:41:30 +00:00
|
|
|
|
|
|
|
/// 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<String, String> additionalParams;
|
|
|
|
|
|
|
|
PaymentUriData({
|
|
|
|
required this.coin,
|
|
|
|
required this.address,
|
|
|
|
this.amount,
|
|
|
|
this.label,
|
|
|
|
this.message,
|
|
|
|
this.paymentId,
|
|
|
|
required this.additionalParams,
|
|
|
|
});
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|