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: