refactor: remove deprecated payment URI classes and unify URI handling with ERC681URI implementation (#3423)

* refactor: remove deprecated payment URI classes and unify URI handling with `ERC681URI` implementation

* refactor: apply lint [skip ci]
This commit is contained in:
Konstantin Ullrich
2026-07-21 00:23:20 +03:00
committed by GitHub
parent 41e2cba322
commit b336da4a9a
3 changed files with 133 additions and 213 deletions
+119 -181
View File
@@ -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 (pjUri.isNotEmpty && !address.startsWith("sp")) {
qp['pjos'] = '0';
qp['pj'] = pjUri;
if (amount.isNotEmpty) {
qp["amount"] = amount.replaceAll(",", ".");
}
return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
if (pjUri.isNotEmpty && !address.startsWith("sp")) {
qp["pjos"] = "0";
qp["pj"] = pjUri;
}
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,74 +256,11 @@ class ERC681URI extends PaymentURI {
required this.contractAddress,
});
@override
String toString() {
var uri = 'ethereum:';
final targetAddress = contractAddress ?? address;
uri += targetAddress;
if (chainId != 1) {
uri += '@$chainId';
}
if (contractAddress != null) {
uri += '/transfer';
}
final params = <String, String>{};
if (contractAddress != null) {
params['address'] = address;
if (amount.isNotEmpty) {
params['uint256'] = _formatAmountForERC20(amount);
}
} else {
if (amount.isNotEmpty) {
params['value'] = _formatAmountForNative(amount);
}
}
if (params.isNotEmpty) {
uri += '?';
uri += params.entries.map((e) => '${e.key}=${e.value}').join('&');
}
return uri;
}
/// Formats amount for ERC-20 token transfers (in atomic units)
String _formatAmountForERC20(String amount) {
try {
// Convert decimal amount to BigInt (assuming 18 decimals)
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(',', '.');
}
}
/// Formats amount for native ETH payments (in wei using scientific notation)
String _formatAmountForNative(String amount) {
try {
// Convert decimal amount to double for scientific notation
final amountDouble = double.parse(amount.replaceAll(',', '.'));
// Use scientific notation as recommended by ERC-681
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 address = isContract ? uri.queryParameters["address"] ?? "" : targetAddress;
final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"];
var formatedAmount = "";
@@ -406,16 +280,78 @@ class ERC681URI extends PaymentURI {
);
}
static int _getChainID(String path) {
return int.parse(RegExp(
r'@\d*',
).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
"1");
final int chainId;
final String? contractAddress;
@override
String toString() {
var uri = "ethereum:";
final targetAddress = contractAddress ?? address;
uri += targetAddress;
if (chainId != 1) {
uri += "@$chainId";
}
if (contractAddress != null) {
uri += "/transfer";
}
final params = <String, String>{};
if (contractAddress != null) {
params["address"] = address;
if (amount.isNotEmpty) {
params["uint256"] = _formatAmountForERC20(amount);
}
} else {
if (amount.isNotEmpty) {
params["value"] = _formatAmountForNative(amount);
}
}
if (params.isNotEmpty) {
uri += "?${params.entries.map((e) => "${e.key}=${e.value}").join("&")}";
}
return uri;
}
/// Formats amount for ERC-20 token transfers (in atomic units)
String _formatAmountForERC20(String amount) {
try {
// Convert decimal amount to BigInt (assuming 18 decimals)
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(",", ".");
}
}
/// Formats amount for native ETH payments (in wei using scientific notation)
String _formatAmountForNative(String amount) {
try {
// Convert decimal amount to double for scientific notation
final amountDouble = double.parse(amount.replaceAll(",", "."));
// Use scientific notation as recommended by ERC-681
return "${amountDouble}e18";
} catch (e) {
// Fallback to original amount if parsing fails
return amount.replaceAll(",", ".");
}
}
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);
}
}
}
+13 -26
View File
@@ -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,
);
}
@@ -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: