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]

Konstantin Ullrich committed Jul 20, 2026 at 23:23 UTC b336da4a9a44a8dd9f4bba51b73dc96440548f9e
3 files changed +106 -186
cw_core/lib/payment_uris.dart
+92 -154
@@ -1,4 +1,4 @@
1 -import 'package:cw_core/format_fixed.dart';
1 +import "package:cw_core/format_fixed.dart";
2
3 abstract class PaymentURI {
4 PaymentURI({required this.amount, required this.address});
@@ -19,7 +19,7 @@ class MoneroURI extends PaymentURI {
19
20 @override
21 String toString() {
22 - var base = 'monero:$address';
22 + var base = "monero:$address";
23
24 if (amount.isNotEmpty) {
25 base += '?tx_amount=${amount.replaceAll(',', '.')}';
@@ -34,7 +34,7 @@ class HavenURI extends PaymentURI {
34
35 @override
36 String toString() {
37 - var base = 'haven:$address';
37 + var base = "haven:$address";
38
39 if (amount.isNotEmpty) {
40 base += '?tx_amount=${amount.replaceAll(',', '.')}';
@@ -45,7 +45,7 @@ class HavenURI extends PaymentURI {
45 }
46
47 class BitcoinURI extends PaymentURI {
48 - BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
48 + BitcoinURI({required super.amount, required super.address, this.pjUri = ""});
49
50 final String pjUri;
51
@@ -53,13 +53,16 @@ class BitcoinURI extends PaymentURI {
53 String toString() {
54 final qp = <String, String>{};
55
56 - if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
56 + if (amount.isNotEmpty) {
57 + qp["amount"] = amount.replaceAll(",", ".");
58 + }
59 +
60 if (pjUri.isNotEmpty && !address.startsWith("sp")) {
58 - qp['pjos'] = '0';
59 - qp['pj'] = pjUri;
61 + qp["pjos"] = "0";
62 + qp["pj"] = pjUri;
63 }
64
62 - return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
65 + return Uri(scheme: "bitcoin", path: address, queryParameters: qp).toString();
66 }
67 }
68
@@ -68,7 +71,7 @@ class LitecoinURI extends PaymentURI {
71
72 @override
73 String toString() {
71 - var base = 'litecoin:$address';
74 + var base = "litecoin:$address";
75
76 if (amount.isNotEmpty) {
77 base += '?amount=${amount.replaceAll(',', '.')}';
@@ -83,52 +86,7 @@ class EthereumURI extends PaymentURI {
86
87 @override
88 String toString() {
86 - var base = 'ethereum:$address';
87 -
88 - if (amount.isNotEmpty) {
89 - base += '?amount=${amount.replaceAll(',', '.')}';
90 - }
91 -
92 - return base;
93 - }
94 -}
95 -
96 -class BaseURI extends PaymentURI {
97 - BaseURI({required super.amount, required super.address});
98 -
99 - @override
100 - String toString() {
101 - var base = 'base:$address';
102 -
103 - if (amount.isNotEmpty) {
104 - base += '?amount=${amount.replaceAll(',', '.')}';
105 - }
106 -
107 - return base;
108 - }
109 -}
110 -
111 -class ArbitrumURI extends PaymentURI {
112 - ArbitrumURI({required super.amount, required super.address});
113 -
114 - @override
115 - String toString() {
116 - var base = 'arbitrum:$address';
117 -
118 - if (amount.isNotEmpty) {
119 - base += '?amount=${amount.replaceAll(',', '.')}';
120 - }
121 -
122 - return base;
123 - }
124 -}
125 -
126 -class BSCURI extends PaymentURI {
127 - BSCURI({required super.amount, required super.address});
128 -
129 - @override
130 - String toString() {
131 - var base = 'bsc:$address';
89 + var base = "ethereum:$address";
90
91 if (amount.isNotEmpty) {
92 base += '?amount=${amount.replaceAll(',', '.')}';
@@ -158,22 +116,7 @@ class NanoURI extends PaymentURI {
116
117 @override
118 String toString() {
161 - var base = 'nano:$address';
162 - if (amount.isNotEmpty) {
163 - base += '?amount=${amount.replaceAll(',', '.')}';
164 - }
165 -
166 - return base;
167 - }
168 -}
169 -
170 -class PolygonURI extends PaymentURI {
171 - PolygonURI({required super.amount, required super.address});
172 -
173 - @override
174 - String toString() {
175 - var base = 'polygon:$address';
176 -
119 + var base = "nano:$address";
120 if (amount.isNotEmpty) {
121 base += '?amount=${amount.replaceAll(',', '.')}';
122 }
@@ -189,14 +132,14 @@ class SolanaURI extends PaymentURI {
132
133 @override
134 String toString() {
192 - var base = 'solana:$address';
135 + var base = "solana:$address";
136 final params = <String>[];
137
138 if (amount.isNotEmpty) {
139 params.add('amount=${amount.replaceAll(',', '.')}');
140 }
141 if (contractAddress != null && contractAddress!.isNotEmpty) {
199 - params.add('spl-token=$contractAddress');
142 + params.add("spl-token=$contractAddress");
143 }
144 if (params.isNotEmpty) {
145 base += '?${params.join('&')}';
@@ -213,14 +156,14 @@ class TronURI extends PaymentURI {
156
157 @override
158 String toString() {
216 - var base = 'tron:$address';
159 + var base = "tron:$address";
160 final params = <String>[];
161
162 if (amount.isNotEmpty) {
163 params.add('amount=${amount.replaceAll(',', '.')}');
164 }
165 if (contractAddress != null && contractAddress!.isNotEmpty) {
223 - params.add('token=$contractAddress');
166 + params.add("token=$contractAddress");
167 }
168 if (params.isNotEmpty) {
169 base += '?${params.join('&')}';
@@ -235,7 +178,7 @@ class WowneroURI extends PaymentURI {
178
179 @override
180 String toString() {
238 - var base = 'wownero:$address';
181 + var base = "wownero:$address";
182
183 if (amount.isNotEmpty) {
184 base += '?tx_amount=${amount.replaceAll(',', '.')}';
@@ -246,12 +189,11 @@ class WowneroURI extends PaymentURI {
189 }
190
191 class ZanoURI extends PaymentURI {
249 - ZanoURI({required String amount, required String address})
250 - : super(amount: amount, address: address);
192 + ZanoURI({required super.amount, required super.address});
193
194 @override
195 String toString() {
254 - var base = 'zano:' + address;
196 + var base = "zano:$address";
197
198 if (amount.isNotEmpty) {
199 base += '?amount=${amount.replaceAll(',', '.')}';
@@ -262,12 +204,11 @@ class ZanoURI extends PaymentURI {
204 }
205
206 class DecredURI extends PaymentURI {
265 - DecredURI({required String amount, required String address})
266 - : super(amount: amount, address: address);
207 + DecredURI({required super.amount, required super.address});
208
209 @override
210 String toString() {
270 - var base = 'decred:' + address;
211 + var base = "decred:$address";
212
213 if (amount.isNotEmpty) {
214 base += '?amount=${amount.replaceAll(',', '.')}';
@@ -278,12 +219,11 @@ class DecredURI extends PaymentURI {
219 }
220
221 class DogeURI extends PaymentURI {
281 - DogeURI({required String amount, required String address})
282 - : super(amount: amount, address: address);
222 + DogeURI({required super.amount, required super.address});
223
224 @override
225 String toString() {
286 - var base = 'doge:' + address;
226 + var base = "doge:$address";
227
228 if (amount.isNotEmpty) {
229 base += '?amount=${amount.replaceAll(',', '.')}';
@@ -298,7 +238,7 @@ class ZcashURI extends PaymentURI {
238
239 @override
240 String toString() {
301 - var base = 'zcash:$address';
241 + var base = "zcash:$address";
242
243 if (amount.isNotEmpty) {
244 base += '?amount=${amount.replaceAll(',', '.')}';
@@ -309,9 +249,6 @@ class ZcashURI extends PaymentURI {
249 }
250
251 class ERC681URI extends PaymentURI {
312 - final int chainId;
313 - final String? contractAddress;
314 -
252 ERC681URI({
253 required this.chainId,
254 required super.address,
@@ -319,37 +256,63 @@ class ERC681URI extends PaymentURI {
256 required this.contractAddress,
257 });
258
259 + factory ERC681URI.fromUri(Uri uri) {
260 + final (isContract, targetAddress) = _getTargetAddress(uri.path);
261 + final chainId = _getChainID(uri.path);
262 +
263 + final address = isContract ? uri.queryParameters["address"] ?? "" : targetAddress;
264 + final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"];
265 +
266 + var formatedAmount = "";
267 +
268 + if (amountParam != null) {
269 + final normalized = _normalizeToIntegerWei(amountParam);
270 + formatedAmount = formatFixed(BigInt.parse(normalized), 18);
271 + } else {
272 + formatedAmount = uri.queryParameters["amount"] ?? "";
273 + }
274 +
275 + return ERC681URI(
276 + chainId: chainId,
277 + address: address,
278 + amount: formatedAmount,
279 + contractAddress: isContract ? targetAddress : null,
280 + );
281 + }
282 +
283 + final int chainId;
284 + final String? contractAddress;
285 +
286 @override
287 String toString() {
324 - var uri = 'ethereum:';
288 + var uri = "ethereum:";
289
290 final targetAddress = contractAddress ?? address;
291 uri += targetAddress;
292
293 if (chainId != 1) {
330 - uri += '@$chainId';
294 + uri += "@$chainId";
295 }
296
297 if (contractAddress != null) {
334 - uri += '/transfer';
298 + uri += "/transfer";
299 }
300
301 final params = <String, String>{};
302
303 if (contractAddress != null) {
340 - params['address'] = address;
304 + params["address"] = address;
305 if (amount.isNotEmpty) {
342 - params['uint256'] = _formatAmountForERC20(amount);
306 + params["uint256"] = _formatAmountForERC20(amount);
307 }
308 } else {
309 if (amount.isNotEmpty) {
346 - params['value'] = _formatAmountForNative(amount);
310 + params["value"] = _formatAmountForNative(amount);
311 }
312 }
313
314 if (params.isNotEmpty) {
351 - uri += '?';
352 - uri += params.entries.map((e) => '${e.key}=${e.value}').join('&');
315 + uri += "?${params.entries.map((e) => "${e.key}=${e.value}").join("&")}";
316 }
317
318 return uri;
@@ -359,12 +322,12 @@ class ERC681URI extends PaymentURI {
322 String _formatAmountForERC20(String amount) {
323 try {
324 // Convert decimal amount to BigInt (assuming 18 decimals)
362 - final amountDouble = double.parse(amount.replaceAll(',', '.'));
325 + final amountDouble = double.parse(amount.replaceAll(",", "."));
326 final amountBigInt = BigInt.from(amountDouble * 1e18);
327 return amountBigInt.toString();
328 } catch (e) {
329 // Fallback to original amount if parsing fails
367 - return amount.replaceAll(',', '.');
330 + return amount.replaceAll(",", ".");
331 }
332 }
333
@@ -372,50 +335,23 @@ class ERC681URI extends PaymentURI {
335 String _formatAmountForNative(String amount) {
336 try {
337 // Convert decimal amount to double for scientific notation
375 - final amountDouble = double.parse(amount.replaceAll(',', '.'));
338 + final amountDouble = double.parse(amount.replaceAll(",", "."));
339
340 // Use scientific notation as recommended by ERC-681
378 - return '${amountDouble}e18';
341 + return "${amountDouble}e18";
342 } catch (e) {
343 // Fallback to original amount if parsing fails
381 - return amount.replaceAll(',', '.');
382 - }
383 - }
384 -
385 - factory ERC681URI.fromUri(Uri uri) {
386 - final (isContract, targetAddress) = _getTargetAddress(uri.path);
387 - final chainId = _getChainID(uri.path);
388 -
389 - final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress;
390 - final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"];
391 -
392 - var formatedAmount = "";
393 -
394 - if (amountParam != null) {
395 - final normalized = _normalizeToIntegerWei(amountParam);
396 - formatedAmount = formatFixed(BigInt.parse(normalized), 18);
397 - } else {
398 - formatedAmount = uri.queryParameters["amount"] ?? "";
344 + return amount.replaceAll(",", ".");
345 }
400 -
401 - return ERC681URI(
402 - chainId: chainId,
403 - address: address,
404 - amount: formatedAmount,
405 - contractAddress: isContract ? targetAddress : null,
406 - );
346 }
347
409 - static int _getChainID(String path) {
410 - return int.parse(RegExp(
411 - r'@\d*',
412 - ).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
413 - "1");
414 - }
348 + static int _getChainID(String path) => int.parse(
349 + RegExp(r"@\d*").firstMatch(path)?.group(0)?.replaceAll("@", "") ?? "1",
350 + );
351
352 static (bool, String) _getTargetAddress(String path) {
353 final targetAddress =
418 - RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!;
354 + RegExp(r"^(0x)?[0-9a-f]{40}", caseSensitive: false).firstMatch(path)!.group(0)!;
355 return (path.contains("/"), targetAddress);
356 }
357
@@ -426,25 +362,27 @@ class ERC681URI extends PaymentURI {
362 /// - Scientific notation: "0.123e18", "1e6" → expanded to integer
363 /// - Decimal ETH: "0.123456" → shifted by 18 decimals
364 static String _normalizeToIntegerWei(String input) {
429 - final raw = input.replaceAll(',', '.').trim();
365 + final raw = input.replaceAll(",", ".").trim();
366
367 // First we check if it's already a plain integer (basically just a number with no dot, no exponent)
368 try {
433 - final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) &&
434 - !raw.contains('.') &&
435 - !raw.toLowerCase().contains('e');
436 - if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), '');
369 + final isPlainInteger = RegExp(r"^[+-]?\d+$").hasMatch(raw) &&
370 + !raw.contains(".") &&
371 + !raw.toLowerCase().contains("e");
372 + if (isPlainInteger) {
373 + return raw.replaceFirst(RegExp(r"^\+"), "");
374 + }
375
376 // Then we check if it's a scientific notation
439 - final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$');
377 + final sci = RegExp(r"^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$");
378 if (sci.hasMatch(raw)) {
441 - final mantissaStr = raw.toLowerCase().split('e')[0];
442 - final exp = int.parse(raw.toLowerCase().split('e')[1]);
379 + final mantissaStr = raw.toLowerCase().split("e")[0];
380 + final exp = int.parse(raw.toLowerCase().split("e")[1]);
381 return _expandDecimal(mantissaStr, exp);
382 }
383
384 // Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount
447 - if (raw.contains('.')) {
385 + if (raw.contains(".")) {
386 return _expandDecimal(raw, 18);
387 }
388 return raw;
@@ -462,34 +400,34 @@ class ERC681URI extends PaymentURI {
400 /// _expandDecimal('1.2', 3) -> '1200'
401 static String _expandDecimal(String decimalStr, int expShift) {
402 var s = decimalStr.trim();
465 - var sign = '';
466 - if (s.startsWith('-') || s.startsWith('+')) {
467 - sign = s[0] == '-' ? '-' : '';
403 + var sign = "";
404 + if (s.startsWith("-") || s.startsWith("+")) {
405 + sign = s[0] == "-" ? "-" : "";
406 s = s.substring(1);
407 }
408
409 // First we split the integer and fractional parts
472 - final parts = s.split('.');
473 - final intPart = parts[0].isEmpty ? '0' : parts[0];
474 - final fracPart = parts.length > 1 ? parts[1] : '';
475 - final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), '');
410 + final parts = s.split(".");
411 + final intPart = parts[0].isEmpty ? "0" : parts[0];
412 + final fracPart = parts.length > 1 ? parts[1] : "";
413 + final digits = (intPart + fracPart).replaceFirst(RegExp(r"^0+"), "");
414 final fracLen = fracPart.length;
415
416 // Then we calculate the effective shift = desired shift minus existing fractional digits
417 final shift = expShift - fracLen;
418 if (shift >= 0) {
481 - final head = digits.isEmpty ? '0' : digits;
482 - final zeros = List.filled(shift, '0').join();
419 + final head = digits.isEmpty ? "0" : digits;
420 + final zeros = List.filled(shift, "0").join();
421 final res = head + zeros;
484 - return sign + (res.isEmpty ? '0' : res);
422 + return sign + (res.isEmpty ? "0" : res);
423 } else {
424 // Need to insert a decimal point within digits; return integer by truncating
425 final cut = digits.length + shift;
426 if (cut <= 0) {
489 - return '0';
427 + return "0";
428 }
429 final res = digits.substring(0, cut);
492 - return sign + (res.isEmpty ? '0' : res);
430 + return sign + (res.isEmpty ? "0" : res);
431 }
432 }
433 }
cw_evm/lib/evm_chain_wallet_addresses.dart
+13 -26
@@ -1,18 +1,15 @@
1 -import 'dart:developer';
1 +import "dart:developer";
2
3 -import 'package:cw_core/payment_uris.dart';
4 -import 'package:cw_core/wallet_addresses.dart';
5 -import 'package:cw_core/wallet_info.dart';
6 -import 'package:mobx/mobx.dart';
3 +import "package:cw_core/payment_uris.dart";
4 +import "package:cw_core/wallet_addresses.dart";
5 +import "package:mobx/mobx.dart";
6
8 -part 'evm_chain_wallet_addresses.g.dart';
7 +part "evm_chain_wallet_addresses.g.dart";
8
9 class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses;
10
11 abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
13 - EVMChainWalletAddressesBase(WalletInfo walletInfo, this._selectedChainId)
14 - : address = '',
15 - super(walletInfo);
12 + EVMChainWalletAddressesBase(super.walletInfo, this._selectedChainId) : address = "";
13
14 @override
15 @observable
@@ -33,7 +30,7 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
30 Future<void> updateAddressesInBox() async {
31 try {
32 addressesMap.clear();
36 - addressesMap[address] = '';
33 + addressesMap[address] = "";
34 await saveAddressesInBox();
35 } catch (e) {
36 log(e.toString());
@@ -41,20 +38,10 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
38 }
39
40 @override
44 - PaymentURI getPaymentUri(String amount) {
45 - switch (_selectedChainId) {
46 - case 1:
47 - return EthereumURI(amount: amount, address: address);
48 - case 137:
49 - return PolygonURI(amount: amount, address: address);
50 - case 8453:
51 - return BaseURI(amount: amount, address: address);
52 - case 42161:
53 - return ArbitrumURI(amount: amount, address: address);
54 - case 56:
55 - return BSCURI(amount: amount, address: address);
56 - default:
57 - return EthereumURI(amount: amount, address: address);
58 - }
59 - }
41 + PaymentURI getPaymentUri(String amount) => ERC681URI(
42 + address: address,
43 + amount: amount,
44 + chainId: _selectedChainId,
45 + contractAddress: null,
46 + );
47 }
lib/view_model/exchange/exchange_trade_view_model.dart
+1 -6
@@ -442,16 +442,11 @@ abstract class ExchangeTradeViewModelBase with Store {
442 case WalletType.dogecoin:
443 return DogeURI(address: inputAddress, amount: amount);
444 case WalletType.ethereum:
445 - return _createERC681URI(fromCurrency, inputAddress, amount);
446 - // TODO: Expand ERC681URI support to Polygon(modify decoding flow for QRs, pay anything, and deep link handling)
445 case WalletType.polygon:
448 - return PolygonURI(amount: amount, address: inputAddress);
446 case WalletType.base:
450 - return BaseURI(amount: amount, address: inputAddress);
447 case WalletType.arbitrum:
452 - return ArbitrumURI(amount: amount, address: inputAddress);
448 case WalletType.bsc:
454 - return BSCURI(amount: amount, address: inputAddress);
449 + return _createERC681URI(fromCurrency, inputAddress, amount);
450 case WalletType.solana:
451 return SolanaURI(amount: amount, address: inputAddress);
452 case WalletType.tron: