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 });