refactor: remove deprecated payment URI classes and unify URI handling with `ERC681URI` implementation (#3423)
What changed, and why it matters
This commit is a code cleanup: it removes several old, near-duplicate payment-URI classes for Ethereum-compatible chains (Polygon, Base, Arbitrum, BSC) and makes every EVM chain use a single shared ERC-681 URI builder. It also adds a parser to read those URIs back. The change is mostly a refactor with no obvious security bug, but it touches code that formats crypto payment amounts and addresses, so a small risk of accidental parsing/formatting mistakes remains.
Treat as a routine refactor. Reviewers should verify that ERC681URI.fromUri handles malformed or malicious URIs gracefully (e.g., missing address, invalid hex, oversized amounts, negative values) without crashing or producing incorrect payment amounts. Consider adding unit tests for edge cases in amount parsing and for non-mainnet chain IDs.
Security signals we found
EVM payment URI generation now centralized in ERC681URI, reducing duplicated address/amount serialization logic
New ERC681URI.fromUri parses untrusted URIs and converts amount strings to BigInt via double parsing and formatFixed
_formatAmountForERC20 uses double.parse(amount) * 1e18 then BigInt.from, which can introduce floating-point rounding for token amounts
_formatAmountForNative uses double.parse and stringifies as scientific notation, potentially losing precision
_normalizeToIntegerWei accepts scientific notation, plain integers, and decimal ETH amounts and shifts by 18 decimals
_getTargetAddress uses a non-null assertion (!) on RegExp.firstMatch(path)!.group(0)!, which will throw if the path lacks a valid 0x address
No input validation or bounds checks on chainId or amount length before BigInt.parse/formatFixed
Evidence from the diff
The patch deletes BaseURI, ArbitrumURI, BSCURI, and PolygonURI from cw_core/lib/payment_uris.dart and changes EVMChainWalletAddresses.getPaymentUri and ExchangeTradeViewModel to produce ERC681URI for all EVM chains. ERC681URI.toString() encodes native transfers as ethereum:
@Changed components
cw_core/lib/payment_uris.dartcw_evm/lib/evm_chain_wallet_addresses.dartlib/view_model/exchange/exchange_trade_view_model.dartInspect captured patch +106 / −186
diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart
index 2703d5e2..43d452f8 100644
--- a/cw_core/lib/payment_uris.dart
+++ b/cw_core/lib/payment_uris.dart
@@ -1,4 +1,4 @@
-import 'package:cw_core/format_fixed.dart';
+import "package:cw_core/format_fixed.dart";
abstract class PaymentURI {
PaymentURI({required this.amount, required this.address});
@@ -19,7 +19,7 @@ class MoneroURI extends PaymentURI {
@override
String toString() {
- var base = 'monero:$address';
+ var base = "monero:$address";
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
@@ -34,7 +34,7 @@ class HavenURI extends PaymentURI {
@override
String toString() {
- var base = 'haven:$address';
+ var base = "haven:$address";
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
@@ -45,7 +45,7 @@ class HavenURI extends PaymentURI {
}
class BitcoinURI extends PaymentURI {
- BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
+ BitcoinURI({required super.amount, required super.address, this.pjUri = ""});
final String pjUri;
@@ -53,13 +53,16 @@ class BitcoinURI extends PaymentURI {
String toString() {
final qp = <String, String>{};
- if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
+ if (amount.isNotEmpty) {
+ qp["amount"] = amount.replaceAll(",", ".");
+ }
+
if (pjUri.isNotEmpty && !address.startsWith("sp")) {
- qp['pjos'] = '0';
- qp['pj'] = pjUri;
+ qp["pjos"] = "0";
+ qp["pj"] = pjUri;
}
- return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
+ return Uri(scheme: "bitcoin", path: address, queryParameters: qp).toString();
}
}
@@ -68,7 +71,7 @@ class LitecoinURI extends PaymentURI {
@override
String toString() {
- var base = 'litecoin:$address';
+ var base = "litecoin:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -83,52 +86,7 @@ class EthereumURI extends PaymentURI {
@override
String toString() {
- var base = 'ethereum:$address';
-
- if (amount.isNotEmpty) {
- base += '?amount=${amount.replaceAll(',', '.')}';
- }
-
- return base;
- }
-}
-
-class BaseURI extends PaymentURI {
- BaseURI({required super.amount, required super.address});
-
- @override
- String toString() {
- var base = 'base:$address';
-
- if (amount.isNotEmpty) {
- base += '?amount=${amount.replaceAll(',', '.')}';
- }
-
- return base;
- }
-}
-
-class ArbitrumURI extends PaymentURI {
- ArbitrumURI({required super.amount, required super.address});
-
- @override
- String toString() {
- var base = 'arbitrum:$address';
-
- if (amount.isNotEmpty) {
- base += '?amount=${amount.replaceAll(',', '.')}';
- }
-
- return base;
- }
-}
-
-class BSCURI extends PaymentURI {
- BSCURI({required super.amount, required super.address});
-
- @override
- String toString() {
- var base = 'bsc:$address';
+ var base = "ethereum:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -158,22 +116,7 @@ class NanoURI extends PaymentURI {
@override
String toString() {
- var base = 'nano:$address';
- if (amount.isNotEmpty) {
- base += '?amount=${amount.replaceAll(',', '.')}';
- }
-
- return base;
- }
-}
-
-class PolygonURI extends PaymentURI {
- PolygonURI({required super.amount, required super.address});
-
- @override
- String toString() {
- var base = 'polygon:$address';
-
+ var base = "nano:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
@@ -189,14 +132,14 @@ class SolanaURI extends PaymentURI {
@override
String toString() {
- var base = 'solana:$address';
+ var base = "solana:$address";
final params = <String>[];
if (amount.isNotEmpty) {
params.add('amount=${amount.replaceAll(',', '.')}');
}
if (contractAddress != null && contractAddress!.isNotEmpty) {
- params.add('spl-token=$contractAddress');
+ params.add("spl-token=$contractAddress");
}
if (params.isNotEmpty) {
base += '?${params.join('&')}';
@@ -213,14 +156,14 @@ class TronURI extends PaymentURI {
@override
String toString() {
- var base = 'tron:$address';
+ var base = "tron:$address";
final params = <String>[];
if (amount.isNotEmpty) {
params.add('amount=${amount.replaceAll(',', '.')}');
}
if (contractAddress != null && contractAddress!.isNotEmpty) {
- params.add('token=$contractAddress');
+ params.add("token=$contractAddress");
}
if (params.isNotEmpty) {
base += '?${params.join('&')}';
@@ -235,7 +178,7 @@ class WowneroURI extends PaymentURI {
@override
String toString() {
- var base = 'wownero:$address';
+ var base = "wownero:$address";
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
@@ -246,12 +189,11 @@ class WowneroURI extends PaymentURI {
}
class ZanoURI extends PaymentURI {
- ZanoURI({required String amount, required String address})
- : super(amount: amount, address: address);
+ ZanoURI({required super.amount, required super.address});
@override
String toString() {
- var base = 'zano:' + address;
+ var base = "zano:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -262,12 +204,11 @@ class ZanoURI extends PaymentURI {
}
class DecredURI extends PaymentURI {
- DecredURI({required String amount, required String address})
- : super(amount: amount, address: address);
+ DecredURI({required super.amount, required super.address});
@override
String toString() {
- var base = 'decred:' + address;
+ var base = "decred:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -278,12 +219,11 @@ class DecredURI extends PaymentURI {
}
class DogeURI extends PaymentURI {
- DogeURI({required String amount, required String address})
- : super(amount: amount, address: address);
+ DogeURI({required super.amount, required super.address});
@override
String toString() {
- var base = 'doge:' + address;
+ var base = "doge:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -298,7 +238,7 @@ class ZcashURI extends PaymentURI {
@override
String toString() {
- var base = 'zcash:$address';
+ var base = "zcash:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -309,9 +249,6 @@ class ZcashURI extends PaymentURI {
}
class ERC681URI extends PaymentURI {
- final int chainId;
- final String? contractAddress;
-
ERC681URI({
required this.chainId,
required super.address,
@@ -319,37 +256,63 @@ class ERC681URI extends PaymentURI {
required this.contractAddress,
});
+ factory ERC681URI.fromUri(Uri uri) {
+ final (isContract, targetAddress) = _getTargetAddress(uri.path);
+ final chainId = _getChainID(uri.path);
+
+ final address = isContract ? uri.queryParameters["address"] ?? "" : targetAddress;
+ final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"];
+
+ var formatedAmount = "";
+
+ if (amountParam != null) {
+ final normalized = _normalizeToIntegerWei(amountParam);
+ formatedAmount = formatFixed(BigInt.parse(normalized), 18);
+ } else {
+ formatedAmount = uri.queryParameters["amount"] ?? "";
+ }
+
+ return ERC681URI(
+ chainId: chainId,
+ address: address,
+ amount: formatedAmount,
+ contractAddress: isContract ? targetAddress : null,
+ );
+ }
+
+ final int chainId;
+ final String? contractAddress;
+
@override
String toString() {
- var uri = 'ethereum:';
+ var uri = "ethereum:";
final targetAddress = contractAddress ?? address;
uri += targetAddress;
if (chainId != 1) {
- uri += '@$chainId';
+ uri += "@$chainId";
}
if (contractAddress != null) {
- uri += '/transfer';
+ uri += "/transfer";
}
final params = <String, String>{};
if (contractAddress != null) {
- params['address'] = address;
+ params["address"] = address;
if (amount.isNotEmpty) {
- params['uint256'] = _formatAmountForERC20(amount);
+ params["uint256"] = _formatAmountForERC20(amount);
}
} else {
if (amount.isNotEmpty) {
- params['value'] = _formatAmountForNative(amount);
+ params["value"] = _formatAmountForNative(amount);
}
}
if (params.isNotEmpty) {
- uri += '?';
- uri += params.entries.map((e) => '${e.key}=${e.value}').join('&');
+ uri += "?${params.entries.map((e) => "${e.key}=${e.value}").join("&")}";
}
return uri;
@@ -359,12 +322,12 @@ class ERC681URI extends PaymentURI {
String _formatAmountForERC20(String amount) {
try {
// Convert decimal amount to BigInt (assuming 18 decimals)
- final amountDouble = double.parse(amount.replaceAll(',', '.'));
+ final amountDouble = double.parse(amount.replaceAll(",", "."));
final amountBigInt = BigInt.from(amountDouble * 1e18);
return amountBigInt.toString();
} catch (e) {
// Fallback to original amount if parsing fails
- return amount.replaceAll(',', '.');
+ return amount.replaceAll(",", ".");
}
}
@@ -372,50 +335,23 @@ class ERC681URI extends PaymentURI {
String _formatAmountForNative(String amount) {
try {
// Convert decimal amount to double for scientific notation
- final amountDouble = double.parse(amount.replaceAll(',', '.'));
+ final amountDouble = double.parse(amount.replaceAll(",", "."));
// Use scientific notation as recommended by ERC-681
- return '${amountDouble}e18';
+ return "${amountDouble}e18";
} catch (e) {
// Fallback to original amount if parsing fails
- return amount.replaceAll(',', '.');
- }
- }
-
- factory ERC681URI.fromUri(Uri uri) {
- final (isContract, targetAddress) = _getTargetAddress(uri.path);
- final chainId = _getChainID(uri.path);
-
- final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress;
- final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"];
-
- var formatedAmount = "";
-
- if (amountParam != null) {
- final normalized = _normalizeToIntegerWei(amountParam);
- formatedAmount = formatFixed(BigInt.parse(normalized), 18);
- } else {
- formatedAmount = uri.queryParameters["amount"] ?? "";
+ return amount.replaceAll(",", ".");
}
-
- return ERC681URI(
- chainId: chainId,
- address: address,
- amount: formatedAmount,
- contractAddress: isContract ? targetAddress : null,
- );
}
- static int _getChainID(String path) {
- return int.parse(RegExp(
- r'@\d*',
- ).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
- "1");
- }
+ static int _getChainID(String path) => int.parse(
+ RegExp(r"@\d*").firstMatch(path)?.group(0)?.replaceAll("@", "") ?? "1",
+ );
static (bool, String) _getTargetAddress(String path) {
final targetAddress =
- RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!;
+ RegExp(r"^(0x)?[0-9a-f]{40}", caseSensitive: false).firstMatch(path)!.group(0)!;
return (path.contains("/"), targetAddress);
}
@@ -426,25 +362,27 @@ class ERC681URI extends PaymentURI {
/// - Scientific notation: "0.123e18", "1e6" → expanded to integer
/// - Decimal ETH: "0.123456" → shifted by 18 decimals
static String _normalizeToIntegerWei(String input) {
- final raw = input.replaceAll(',', '.').trim();
+ final raw = input.replaceAll(",", ".").trim();
// First we check if it's already a plain integer (basically just a number with no dot, no exponent)
try {
- final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) &&
- !raw.contains('.') &&
- !raw.toLowerCase().contains('e');
- if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), '');
+ final isPlainInteger = RegExp(r"^[+-]?\d+$").hasMatch(raw) &&
+ !raw.contains(".") &&
+ !raw.toLowerCase().contains("e");
+ if (isPlainInteger) {
+ return raw.replaceFirst(RegExp(r"^\+"), "");
+ }
// Then we check if it's a scientific notation
- final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$');
+ final sci = RegExp(r"^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$");
if (sci.hasMatch(raw)) {
- final mantissaStr = raw.toLowerCase().split('e')[0];
- final exp = int.parse(raw.toLowerCase().split('e')[1]);
+ final mantissaStr = raw.toLowerCase().split("e")[0];
+ final exp = int.parse(raw.toLowerCase().split("e")[1]);
return _expandDecimal(mantissaStr, exp);
}
// Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount
- if (raw.contains('.')) {
+ if (raw.contains(".")) {
return _expandDecimal(raw, 18);
}
return raw;
@@ -462,34 +400,34 @@ class ERC681URI extends PaymentURI {
/// _expandDecimal('1.2', 3) -> '1200'
static String _expandDecimal(String decimalStr, int expShift) {
var s = decimalStr.trim();
- var sign = '';
- if (s.startsWith('-') || s.startsWith('+')) {
- sign = s[0] == '-' ? '-' : '';
+ var sign = "";
+ if (s.startsWith("-") || s.startsWith("+")) {
+ sign = s[0] == "-" ? "-" : "";
s = s.substring(1);
}
// First we split the integer and fractional parts
- final parts = s.split('.');
- final intPart = parts[0].isEmpty ? '0' : parts[0];
- final fracPart = parts.length > 1 ? parts[1] : '';
- final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), '');
+ final parts = s.split(".");
+ final intPart = parts[0].isEmpty ? "0" : parts[0];
+ final fracPart = parts.length > 1 ? parts[1] : "";
+ final digits = (intPart + fracPart).replaceFirst(RegExp(r"^0+"), "");
final fracLen = fracPart.length;
// Then we calculate the effective shift = desired shift minus existing fractional digits
final shift = expShift - fracLen;
if (shift >= 0) {
- final head = digits.isEmpty ? '0' : digits;
- final zeros = List.filled(shift, '0').join();
+ final head = digits.isEmpty ? "0" : digits;
+ final zeros = List.filled(shift, "0").join();
final res = head + zeros;
- return sign + (res.isEmpty ? '0' : res);
+ return sign + (res.isEmpty ? "0" : res);
} else {
// Need to insert a decimal point within digits; return integer by truncating
final cut = digits.length + shift;
if (cut <= 0) {
- return '0';
+ return "0";
}
final res = digits.substring(0, cut);
- return sign + (res.isEmpty ? '0' : res);
+ return sign + (res.isEmpty ? "0" : res);
}
}
}
diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart
index f7cef751..fad6eb68 100644
--- a/cw_evm/lib/evm_chain_wallet_addresses.dart
+++ b/cw_evm/lib/evm_chain_wallet_addresses.dart
@@ -1,18 +1,15 @@
-import 'dart:developer';
+import "dart:developer";
-import 'package:cw_core/payment_uris.dart';
-import 'package:cw_core/wallet_addresses.dart';
-import 'package:cw_core/wallet_info.dart';
-import 'package:mobx/mobx.dart';
+import "package:cw_core/payment_uris.dart";
+import "package:cw_core/wallet_addresses.dart";
+import "package:mobx/mobx.dart";
-part 'evm_chain_wallet_addresses.g.dart';
+part "evm_chain_wallet_addresses.g.dart";
class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses;
abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
- EVMChainWalletAddressesBase(WalletInfo walletInfo, this._selectedChainId)
- : address = '',
- super(walletInfo);
+ EVMChainWalletAddressesBase(super.walletInfo, this._selectedChainId) : address = "";
@override
@observable
@@ -33,7 +30,7 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
Future<void> updateAddressesInBox() async {
try {
addressesMap.clear();
- addressesMap[address] = '';
+ addressesMap[address] = "";
await saveAddressesInBox();
} catch (e) {
log(e.toString());
@@ -41,20 +38,10 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
}
@override
- PaymentURI getPaymentUri(String amount) {
- switch (_selectedChainId) {
- case 1:
- return EthereumURI(amount: amount, address: address);
- case 137:
- return PolygonURI(amount: amount, address: address);
- case 8453:
- return BaseURI(amount: amount, address: address);
- case 42161:
- return ArbitrumURI(amount: amount, address: address);
- case 56:
- return BSCURI(amount: amount, address: address);
- default:
- return EthereumURI(amount: amount, address: address);
- }
- }
+ PaymentURI getPaymentUri(String amount) => ERC681URI(
+ address: address,
+ amount: amount,
+ chainId: _selectedChainId,
+ contractAddress: null,
+ );
}
diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart
index 6e039e27..5ca718eb 100644
--- a/lib/view_model/exchange/exchange_trade_view_model.dart
+++ b/lib/view_model/exchange/exchange_trade_view_model.dart
@@ -442,16 +442,11 @@ abstract class ExchangeTradeViewModelBase with Store {
case WalletType.dogecoin:
return DogeURI(address: inputAddress, amount: amount);
case WalletType.ethereum:
- return _createERC681URI(fromCurrency, inputAddress, amount);
- // TODO: Expand ERC681URI support to Polygon(modify decoding flow for QRs, pay anything, and deep link handling)
case WalletType.polygon:
- return PolygonURI(amount: amount, address: inputAddress);
case WalletType.base:
- return BaseURI(amount: amount, address: inputAddress);
case WalletType.arbitrum:
- return ArbitrumURI(amount: amount, address: inputAddress);
case WalletType.bsc:
- return BSCURI(amount: amount, address: inputAddress);
+ return _createERC681URI(fromCurrency, inputAddress, amount);
case WalletType.solana:
return SolanaURI(amount: amount, address: inputAddress);
case WalletType.tron:
Why this scored 14/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.