Cw 1118 silent payment fixes (#2373)
* fix: hive box error * fix: address validator * fix: guarantee save on sp scanned * fix: too many saves * feat: network based address validation * fix: sp testnet * feat: ltc validator * fix: use general sp regex on address getter and remove print
rafael_xmr committed
Jul 11, 2025 at 18:19 UTC
8207eed466029a241d3d333e19b15a0583bf2733
6 files changed
+71
-48
cw_bitcoin/lib/electrum_wallet.dart
+3
-1
@@ -405,8 +405,11 @@ abstract class ElectrumWalletBase
405
406
// Add new TX record
407
transactionHistory.addMany(message);
408
+
409
// Update balance record
410
balance[currency]!.confirmed += tx.amount;
411
+
412
+ await save();
413
}
414
415
await updateAllUnspents();
@@ -544,7 +547,6 @@ abstract class ElectrumWalletBase
547
}
548
549
node!.isElectrs = false;
547
- node!.save();
550
return node!.isElectrs!;
551
}
552
lib/core/address_validator.dart
+32
-9
@@ -5,27 +5,30 @@ import 'package:cake_wallet/solana/solana.dart';
5
import 'package:cake_wallet/zano/zano.dart';
6
import 'package:cw_core/crypto_currency.dart';
7
import 'package:cw_core/erc20_token.dart';
8
+
9
const BEFORE_REGEX = '(^|\\s)';
10
const AFTER_REGEX = '(\$|\\s)';
11
12
class AddressValidator extends TextValidator {
12
- AddressValidator({required CryptoCurrency type})
13
+ AddressValidator({required CryptoCurrency type, bool isTestnet = false})
14
: super(
15
errorMessage: S.current.error_text_address,
16
useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc
17
? (String txt) => BitcoinAddressUtils.validateAddress(
18
address: txt,
19
network: type == CryptoCurrency.btc
19
- ? BitcoinNetwork.mainnet
20
+ ? isTestnet
21
+ ? BitcoinNetwork.testnet
22
+ : BitcoinNetwork.mainnet
23
: LitecoinNetwork.mainnet,
24
)
22
- : type == CryptoCurrency.zano
25
+ : type == CryptoCurrency.zano
26
? zano?.validateAddress
27
: null,
25
- pattern: getPattern(type),
28
+ pattern: getPattern(type, isTestnet: isTestnet),
29
length: getLength(type));
30
28
- static String getPattern(CryptoCurrency type) {
31
+ static String getPattern(CryptoCurrency type, {required bool isTestnet}) {
32
var pattern = "";
33
if (type is Erc20Token) {
34
pattern = '0x[0-9a-zA-Z]+';
@@ -37,10 +40,28 @@ class AddressValidator extends TextValidator {
40
pattern = '[0-9a-zA-Z]{59}|[0-9a-zA-Z]{92}|[0-9a-zA-Z]{104}'
41
'|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}';
42
case CryptoCurrency.btc:
40
- pattern =
41
- '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';
43
+ if (isTestnet) {
44
+ pattern = '(^|\s)([mnL][a-km-zA-HJ-NP-Z1-9]{25,34})'
45
+ '|([23M][a-km-zA-HJ-NP-Z1-9]{25,34})'
46
+ '|(tb1q[ac-hj-np-z02-9]{25,39})'
47
+ '|(tb1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))'
48
+ '|(tb1q[ac-hj-np-z02-9]{40,80})'
49
+ '|(${silentPaymentAddressPatternTestnet})(\$|\s)';
50
+ } else {
51
+ pattern = '(^|\s)(1[a-km-zA-HJ-NP-Z1-9]{25,34})'
52
+ '|(3[a-km-zA-HJ-NP-Z1-9]{25,34})'
53
+ '|(bc1q[ac-hj-np-z02-9]{25,39})'
54
+ '|(bc1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))'
55
+ '|(bc1q[ac-hj-np-z02-9]{40,80})'
56
+ '|(${silentPaymentAddressPatternMainnet})(\$|\s)';
57
+ }
58
case CryptoCurrency.ltc:
43
- pattern = '${P2wpkhAddress.regex.pattern}|${MwebAddress.regex.pattern}';
59
+ pattern = '(^|\s)(L[a-km-zA-HJ-NP-Z1-9]{25,34})'
60
+ '|([3M][a-km-zA-HJ-NP-Z1-9]{25,34})'
61
+ '|(ltc1q[ac-hj-np-z02-9]{25,39})'
62
+ '|(ltc1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))'
63
+ '|(ltc1q[ac-hj-np-z02-9]{40,80})'
64
+ '|(${MwebAddress.regex.pattern})(\$|\s)';
65
case CryptoCurrency.nano:
66
pattern = '[0-9a-zA-Z_]+';
67
case CryptoCurrency.banano:
@@ -281,9 +302,11 @@ class AddressValidator extends TextValidator {
302
}
303
}
304
284
- static String get silentPaymentAddressPattern => SilentPaymentAddress.regex.pattern;
305
+ static String get silentPaymentAddressPatternMainnet => 'sp1[0-9a-zA-Z]{113}';
306
+ static String get silentPaymentAddressPatternTestnet => '(tsp|sprt)1[0-9a-zA-Z]{113}';
307
static String get mWebAddressPattern => MwebAddress.regex.pattern;
308
309
+ // NOTE: not needed to check for network here as it's a general address catcher, validation is separate
310
static String? getAddressFromStringPattern(CryptoCurrency type) {
311
String? pattern = null;
312
lib/src/screens/send/send_page.dart
+3
-3
@@ -736,8 +736,7 @@ class SendPage extends BasePage {
736
currentTheme: currentTheme,
737
footerType: FooterType.singleActionButton,
738
titleText: S.of(context).proceed_on_device,
739
- contentImage:
740
- 'assets/images/hardware_wallet/ledger_nano_x.png',
739
+ contentImage: 'assets/images/hardware_wallet/ledger_nano_x.png',
740
contentImageColor: Theme.of(context).colorScheme.onSurface,
741
content: S.of(context).proceed_on_device_description,
742
singleActionButtonText: S.of(context).cancel,
@@ -810,7 +809,8 @@ class SendPage extends BasePage {
809
bool isRegularElectrumAddress(String address) {
810
final supportedTypes = [CryptoCurrency.btc, CryptoCurrency.ltc, CryptoCurrency.bch];
811
final excludedPatterns = [
813
- RegExp(AddressValidator.silentPaymentAddressPattern),
812
+ RegExp(AddressValidator.silentPaymentAddressPatternMainnet),
813
+ RegExp(AddressValidator.silentPaymentAddressPatternTestnet),
814
RegExp(AddressValidator.mWebAddressPattern)
815
];
816
lib/view_model/send/send_template_view_model.dart
+6
-4
@@ -46,7 +46,8 @@ abstract class SendTemplateViewModelBase with Store {
46
AmountValidator get amountValidator =>
47
AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
48
49
- AddressValidator get addressValidator => AddressValidator(type: _wallet.currency);
49
+ AddressValidator get addressValidator =>
50
+ AddressValidator(type: _wallet.currency, isTestnet: _wallet.isTestnet);
51
52
TemplateValidator get templateValidator => TemplateValidator();
53
@@ -102,7 +103,8 @@ abstract class SendTemplateViewModelBase with Store {
103
@computed
104
List<CryptoCurrency> get walletCurrencies => _wallet.balance.keys.toList();
105
105
- bool get hasMultipleTokens => isEVMCompatibleChain(_wallet.type) ||
106
- _wallet.type == WalletType.solana ||
107
- _wallet.type == WalletType.tron;
106
+ bool get hasMultipleTokens =>
107
+ isEVMCompatibleChain(_wallet.type) ||
108
+ _wallet.type == WalletType.solana ||
109
+ _wallet.type == WalletType.tron;
110
}
lib/view_model/send/send_view_model.dart
+5
-4
@@ -215,7 +215,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
215
216
Validator<String> get allAmountValidator => AllAmountValidator();
217
218
- Validator<String> get addressValidator => AddressValidator(type: selectedCryptoCurrency);
218
+ Validator<String> get addressValidator =>
219
+ AddressValidator(type: selectedCryptoCurrency, isTestnet: wallet.isTestnet);
220
221
Validator<String> get textValidator => TextValidator();
222
@@ -777,12 +778,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
778
return S.current.solana_no_associated_token_account_exception;
779
}
780
780
- if (errorMessage.contains('insufficient funds for rent') &&
781
- errorMessage.contains('Transaction simulation failed') &&
781
+ if (errorMessage.contains('insufficient funds for rent') &&
782
+ errorMessage.contains('Transaction simulation failed') &&
783
errorMessage.contains('account_index')) {
784
final accountIndexMatch = RegExp(r'account_index: (\d+)').firstMatch(errorMessage);
785
if (accountIndexMatch != null) {
785
- return int.parse(accountIndexMatch.group(1)!) == 0
786
+ return int.parse(accountIndexMatch.group(1)!) == 0
787
? S.current.insufficientFundsForRentError
788
: S.current.insufficientFundsForRentErrorReceiver;
789
}
test/core/address_validator_test.dart
+22
-27
@@ -24,17 +24,14 @@ void main() {
24
test('returns correct pattern for Monero', () {
25
final pattern = AddressValidator.getPattern(CryptoCurrency.xmr);
26
expect(pattern, isNotEmpty);
27
- expect(pattern,
28
- contains('4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}'));
27
+ expect(pattern, contains('4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}'));
28
});
29
30
test('returns correct pattern for Litecoin', () {
31
final pattern = AddressValidator.getPattern(CryptoCurrency.ltc);
32
expect(pattern, isNotEmpty);
34
- expect(
35
- pattern,
36
- contains(
37
- '(bc|tb|ltc)1q[ac-hj-np-z02-9]{25,39}|(ltc|t)mweb1q[ac-hj-np-z02-9]{90,120}'));
33
+ expect(pattern,
34
+ contains('(bc|tb|ltc)1q[ac-hj-np-z02-9]{25,39}|(ltc|t)mweb1q[ac-hj-np-z02-9]{90,120}'));
35
});
36
37
test('returns empty string for unknown currency', () {
@@ -67,29 +64,25 @@ void main() {
64
65
group('getAddressFromStringPattern', () {
66
test('returns correct pattern for Bitcoin', () {
70
- final pattern =
71
- AddressValidator.getAddressFromStringPattern(CryptoCurrency.btc);
67
+ final pattern = AddressValidator.getAddressFromStringPattern(CryptoCurrency.btc);
68
expect(pattern, isNotNull);
69
expect(pattern, contains('(bc|tb)1q'));
70
});
71
72
test('returns correct pattern for Ethereum', () {
77
- final pattern =
78
- AddressValidator.getAddressFromStringPattern(CryptoCurrency.eth);
73
+ final pattern = AddressValidator.getAddressFromStringPattern(CryptoCurrency.eth);
74
expect(pattern, isNotNull);
75
expect(pattern, contains('0x[0-9a-zA-Z]+'));
76
});
77
78
test('returns correct pattern for Monero', () {
84
- final pattern =
85
- AddressValidator.getAddressFromStringPattern(CryptoCurrency.xmr);
79
+ final pattern = AddressValidator.getAddressFromStringPattern(CryptoCurrency.xmr);
80
expect(pattern, isNotNull);
81
expect(pattern, contains('(4[0-9a-zA-Z]{94})'));
82
});
83
84
test('returns null for unsupported currency', () {
91
- final pattern =
92
- AddressValidator.getAddressFromStringPattern(CryptoCurrency.dash);
85
+ final pattern = AddressValidator.getAddressFromStringPattern(CryptoCurrency.dash);
86
expect(pattern, isNull);
87
});
88
});
@@ -97,8 +90,7 @@ void main() {
90
group('validation', () {
91
test('validates valid Bitcoin address', () {
92
final validator = AddressValidator(type: CryptoCurrency.btc);
100
- expect(validator.isValid('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq'),
101
- isTrue);
93
+ expect(validator.isValid('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq'), isTrue);
94
expect(validator.isValid('3AD1Btx1MzYGmdpNpeujCfuvU5SsU2LX88'), isTrue);
95
expect(validator.isValid('1HARAhFcvz8ZQp5MhnLFeUynC4bkha3Hv8'), isTrue);
96
});
@@ -106,8 +98,7 @@ void main() {
98
test('rejects invalid Bitcoin address', () {
99
final validator = AddressValidator(type: CryptoCurrency.btc);
100
expect(validator.isValid('invalid_address'), isFalse);
109
- expect(validator.isValid('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043CakeWallet'),
110
- isFalse);
101
+ expect(validator.isValid('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043CakeWallet'), isFalse);
102
});
103
104
test('validates valid Ethereum address', () {
@@ -119,8 +110,8 @@ void main() {
110
test('rejects invalid Ethereum address', () {
111
final validator = AddressValidator(type: CryptoCurrency.eth);
112
expect(validator.isValid('invalid_address'), isFalse);
122
- expect(validator.isValid('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc'),
123
- isFalse); // Too short
113
+ expect(
114
+ validator.isValid('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc'), isFalse); // Too short
115
expect(validator.isValid('C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'),
116
isFalse); // Missing 0x prefix
117
});
@@ -144,8 +135,7 @@ void main() {
135
136
test('validates valid Litecoin address', () {
137
final validator = AddressValidator(type: CryptoCurrency.ltc);
147
- expect(validator.isValid('ltc1qzvxlvlk8wsmue0np20eh3d3qxsusx9jstf8qw8'),
148
- isTrue);
138
+ expect(validator.isValid('ltc1qzvxlvlk8wsmue0np20eh3d3qxsusx9jstf8qw8'), isTrue);
139
expect(
140
validator.isValid(
141
'ltcmweb1qqt9hqch2d0vfdsvt4tf27gullem2tcd57xxrvta9xwvfmwdkn4927q6d8sq6ftw7lkqdkr5g36eqn7w06edgq8tz7gy0nv5d4lhajctkzuath23a'),
@@ -155,15 +145,20 @@ void main() {
145
test('rejects invalid Litecoin address', () {
146
final validator = AddressValidator(type: CryptoCurrency.ltc);
147
expect(validator.isValid('invalid_address'), isFalse);
158
- expect(
159
- validator.isValid('ltc1qzvxlvlk8wsmue0np20eh3d3qxsusxCakeWallet'),
160
- isFalse);
148
+ expect(validator.isValid('ltc1qzvxlvlk8wsmue0np20eh3d3qxsusxCakeWallet'), isFalse);
149
+ });
150
+ });
151
+
152
+ group('silentPaymentAddressPatternMainnet', () {
153
+ test('returns a non-empty pattern', () {
154
+ final pattern = AddressValidator.silentPaymentAddressPatternMainnet;
155
+ expect(pattern, isNotEmpty);
156
});
157
});
158
164
- group('silentPaymentAddressPattern', () {
159
+ group('silentPaymentAddressPatternTestnet', () {
160
test('returns a non-empty pattern', () {
166
- final pattern = AddressValidator.silentPaymentAddressPattern;
161
+ final pattern = AddressValidator.silentPaymentAddressPatternTestnet;
162
expect(pattern, isNotEmpty);
163
});
164
});