mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-03-12 09:27:01 +00:00
update uri parsing code to support monero URIs
bitcoin URIs now also validate properly
This commit is contained in:
parent
dd11214b3c
commit
b73628d939
8 changed files with 178 additions and 32 deletions
|
@ -304,7 +304,7 @@ class _AddressCardState extends ConsumerState<AddressCard> {
|
|||
key: _qrKey,
|
||||
child: QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
widget.coin,
|
||||
widget.coin.uriScheme,
|
||||
address.value,
|
||||
{},
|
||||
),
|
||||
|
|
|
@ -97,7 +97,7 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
|
|||
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<AddressDetailsView> {
|
|||
key: _qrKey,
|
||||
child: QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
coin,
|
||||
coin.uriScheme,
|
||||
address.value,
|
||||
{},
|
||||
),
|
||||
|
|
|
@ -142,7 +142,7 @@ class _AddressQrPopupState extends State<AddressQrPopup> {
|
|||
key: _qrKey,
|
||||
child: QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
widget.coin,
|
||||
widget.coin.uriScheme,
|
||||
widget.addressString,
|
||||
{},
|
||||
),
|
||||
|
|
|
@ -170,7 +170,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
|
|||
}
|
||||
|
||||
final uriString = AddressUtils.buildUriString(
|
||||
widget.coin,
|
||||
widget.coin.uriScheme,
|
||||
receivingAddress,
|
||||
queryParams,
|
||||
);
|
||||
|
@ -263,7 +263,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
|
|||
}
|
||||
|
||||
_uriString = AddressUtils.buildUriString(
|
||||
widget.coin,
|
||||
widget.coin.uriScheme,
|
||||
receivingAddress,
|
||||
{},
|
||||
);
|
||||
|
|
|
@ -577,7 +577,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
|||
children: [
|
||||
QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
coin,
|
||||
coin.uriScheme,
|
||||
address,
|
||||
{},
|
||||
),
|
||||
|
|
|
@ -448,7 +448,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
Center(
|
||||
child: QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
coin,
|
||||
coin.uriScheme,
|
||||
address,
|
||||
{},
|
||||
),
|
||||
|
|
|
@ -841,6 +841,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
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<DesktopSend> {
|
|||
sendToController.text = content;
|
||||
_address = content;
|
||||
|
||||
// Trigger validation after pasting.
|
||||
_setValidAddressProviders(_address);
|
||||
setState(() {
|
||||
_addressToggleFlag = sendToController.text.isNotEmpty;
|
||||
|
|
|
@ -10,11 +10,19 @@
|
|||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'logger.dart';
|
||||
import '../wallets/crypto_currency/crypto_currency.dart';
|
||||
|
||||
class AddressUtils {
|
||||
static final Set<String> recognizedParams = {'amount', 'label', 'message'};
|
||||
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.
|
||||
};
|
||||
|
||||
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<String, String> parseUri(String uri) {
|
||||
final Map<String, String> 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<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)
|
||||
static String buildUriString(
|
||||
CryptoCurrency coin,
|
||||
String scheme,
|
||||
String address,
|
||||
Map<String, String> 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<String, dynamic>.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<String> 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<String, String> additionalParams;
|
||||
|
||||
PaymentUriData({
|
||||
required this.coin,
|
||||
required this.address,
|
||||
this.amount,
|
||||
this.label,
|
||||
this.message,
|
||||
this.paymentId,
|
||||
required this.additionalParams,
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue