diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 2a3401029..5938cc00b 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -405,8 +405,11 @@ abstract class ElectrumWalletBase // Add new TX record transactionHistory.addMany(message); + // Update balance record balance[currency]!.confirmed += tx.amount; + + await save(); } await updateAllUnspents(); @@ -544,7 +547,6 @@ abstract class ElectrumWalletBase } node!.isElectrs = false; - node!.save(); return node!.isElectrs!; } diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index c79ac0980..ab2464069 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -5,27 +5,30 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; + const BEFORE_REGEX = '(^|\\s)'; const AFTER_REGEX = '(\$|\\s)'; class AddressValidator extends TextValidator { - AddressValidator({required CryptoCurrency type}) + AddressValidator({required CryptoCurrency type, bool isTestnet = false}) : super( errorMessage: S.current.error_text_address, useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc ? (String txt) => BitcoinAddressUtils.validateAddress( address: txt, network: type == CryptoCurrency.btc - ? BitcoinNetwork.mainnet + ? isTestnet + ? BitcoinNetwork.testnet + : BitcoinNetwork.mainnet : LitecoinNetwork.mainnet, ) - : type == CryptoCurrency.zano + : type == CryptoCurrency.zano ? zano?.validateAddress : null, - pattern: getPattern(type), + pattern: getPattern(type, isTestnet: isTestnet), length: getLength(type)); - static String getPattern(CryptoCurrency type) { + static String getPattern(CryptoCurrency type, {required bool isTestnet}) { var pattern = ""; if (type is Erc20Token) { pattern = '0x[0-9a-zA-Z]+'; @@ -37,10 +40,28 @@ class AddressValidator extends TextValidator { pattern = '[0-9a-zA-Z]{59}|[0-9a-zA-Z]{92}|[0-9a-zA-Z]{104}' '|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}'; case CryptoCurrency.btc: - pattern = - '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; + if (isTestnet) { + pattern = '(^|\s)([mnL][a-km-zA-HJ-NP-Z1-9]{25,34})' + '|([23M][a-km-zA-HJ-NP-Z1-9]{25,34})' + '|(tb1q[ac-hj-np-z02-9]{25,39})' + '|(tb1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))' + '|(tb1q[ac-hj-np-z02-9]{40,80})' + '|(${silentPaymentAddressPatternTestnet})(\$|\s)'; + } else { + pattern = '(^|\s)(1[a-km-zA-HJ-NP-Z1-9]{25,34})' + '|(3[a-km-zA-HJ-NP-Z1-9]{25,34})' + '|(bc1q[ac-hj-np-z02-9]{25,39})' + '|(bc1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))' + '|(bc1q[ac-hj-np-z02-9]{40,80})' + '|(${silentPaymentAddressPatternMainnet})(\$|\s)'; + } case CryptoCurrency.ltc: - pattern = '${P2wpkhAddress.regex.pattern}|${MwebAddress.regex.pattern}'; + pattern = '(^|\s)(L[a-km-zA-HJ-NP-Z1-9]{25,34})' + '|([3M][a-km-zA-HJ-NP-Z1-9]{25,34})' + '|(ltc1q[ac-hj-np-z02-9]{25,39})' + '|(ltc1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))' + '|(ltc1q[ac-hj-np-z02-9]{40,80})' + '|(${MwebAddress.regex.pattern})(\$|\s)'; case CryptoCurrency.nano: pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.banano: @@ -281,9 +302,11 @@ class AddressValidator extends TextValidator { } } - static String get silentPaymentAddressPattern => SilentPaymentAddress.regex.pattern; + static String get silentPaymentAddressPatternMainnet => 'sp1[0-9a-zA-Z]{113}'; + static String get silentPaymentAddressPatternTestnet => '(tsp|sprt)1[0-9a-zA-Z]{113}'; static String get mWebAddressPattern => MwebAddress.regex.pattern; + // NOTE: not needed to check for network here as it's a general address catcher, validation is separate static String? getAddressFromStringPattern(CryptoCurrency type) { String? pattern = null; diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index d8ed4fe5e..74a10fe04 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -736,8 +736,7 @@ class SendPage extends BasePage { currentTheme: currentTheme, footerType: FooterType.singleActionButton, titleText: S.of(context).proceed_on_device, - contentImage: - 'assets/images/hardware_wallet/ledger_nano_x.png', + contentImage: 'assets/images/hardware_wallet/ledger_nano_x.png', contentImageColor: Theme.of(context).colorScheme.onSurface, content: S.of(context).proceed_on_device_description, singleActionButtonText: S.of(context).cancel, @@ -810,7 +809,8 @@ class SendPage extends BasePage { bool isRegularElectrumAddress(String address) { final supportedTypes = [CryptoCurrency.btc, CryptoCurrency.ltc, CryptoCurrency.bch]; final excludedPatterns = [ - RegExp(AddressValidator.silentPaymentAddressPattern), + RegExp(AddressValidator.silentPaymentAddressPatternMainnet), + RegExp(AddressValidator.silentPaymentAddressPatternTestnet), RegExp(AddressValidator.mWebAddressPattern) ]; diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index dcf0e1497..106da72ff 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -46,7 +46,8 @@ abstract class SendTemplateViewModelBase with Store { AmountValidator get amountValidator => AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type)); - AddressValidator get addressValidator => AddressValidator(type: _wallet.currency); + AddressValidator get addressValidator => + AddressValidator(type: _wallet.currency, isTestnet: _wallet.isTestnet); TemplateValidator get templateValidator => TemplateValidator(); @@ -102,7 +103,8 @@ abstract class SendTemplateViewModelBase with Store { @computed List get walletCurrencies => _wallet.balance.keys.toList(); - bool get hasMultipleTokens => isEVMCompatibleChain(_wallet.type) || - _wallet.type == WalletType.solana || - _wallet.type == WalletType.tron; + bool get hasMultipleTokens => + isEVMCompatibleChain(_wallet.type) || + _wallet.type == WalletType.solana || + _wallet.type == WalletType.tron; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 7800ceca6..a767065ec 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -215,7 +215,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor Validator get allAmountValidator => AllAmountValidator(); - Validator get addressValidator => AddressValidator(type: selectedCryptoCurrency); + Validator get addressValidator => + AddressValidator(type: selectedCryptoCurrency, isTestnet: wallet.isTestnet); Validator get textValidator => TextValidator(); @@ -777,12 +778,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return S.current.solana_no_associated_token_account_exception; } - if (errorMessage.contains('insufficient funds for rent') && - errorMessage.contains('Transaction simulation failed') && + if (errorMessage.contains('insufficient funds for rent') && + errorMessage.contains('Transaction simulation failed') && errorMessage.contains('account_index')) { final accountIndexMatch = RegExp(r'account_index: (\d+)').firstMatch(errorMessage); if (accountIndexMatch != null) { - return int.parse(accountIndexMatch.group(1)!) == 0 + return int.parse(accountIndexMatch.group(1)!) == 0 ? S.current.insufficientFundsForRentError : S.current.insufficientFundsForRentErrorReceiver; } diff --git a/test/core/address_validator_test.dart b/test/core/address_validator_test.dart index 08dd5a9e2..f8545999e 100644 --- a/test/core/address_validator_test.dart +++ b/test/core/address_validator_test.dart @@ -24,17 +24,14 @@ void main() { test('returns correct pattern for Monero', () { final pattern = AddressValidator.getPattern(CryptoCurrency.xmr); expect(pattern, isNotEmpty); - expect(pattern, - contains('4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}')); + expect(pattern, contains('4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}')); }); test('returns correct pattern for Litecoin', () { final pattern = AddressValidator.getPattern(CryptoCurrency.ltc); expect(pattern, isNotEmpty); - expect( - pattern, - contains( - '(bc|tb|ltc)1q[ac-hj-np-z02-9]{25,39}|(ltc|t)mweb1q[ac-hj-np-z02-9]{90,120}')); + expect(pattern, + contains('(bc|tb|ltc)1q[ac-hj-np-z02-9]{25,39}|(ltc|t)mweb1q[ac-hj-np-z02-9]{90,120}')); }); test('returns empty string for unknown currency', () { @@ -67,29 +64,25 @@ void main() { group('getAddressFromStringPattern', () { test('returns correct pattern for Bitcoin', () { - final pattern = - AddressValidator.getAddressFromStringPattern(CryptoCurrency.btc); + final pattern = AddressValidator.getAddressFromStringPattern(CryptoCurrency.btc); expect(pattern, isNotNull); expect(pattern, contains('(bc|tb)1q')); }); test('returns correct pattern for Ethereum', () { - final pattern = - AddressValidator.getAddressFromStringPattern(CryptoCurrency.eth); + final pattern = AddressValidator.getAddressFromStringPattern(CryptoCurrency.eth); expect(pattern, isNotNull); expect(pattern, contains('0x[0-9a-zA-Z]+')); }); test('returns correct pattern for Monero', () { - final pattern = - AddressValidator.getAddressFromStringPattern(CryptoCurrency.xmr); + final pattern = AddressValidator.getAddressFromStringPattern(CryptoCurrency.xmr); expect(pattern, isNotNull); expect(pattern, contains('(4[0-9a-zA-Z]{94})')); }); test('returns null for unsupported currency', () { - final pattern = - AddressValidator.getAddressFromStringPattern(CryptoCurrency.dash); + final pattern = AddressValidator.getAddressFromStringPattern(CryptoCurrency.dash); expect(pattern, isNull); }); }); @@ -97,8 +90,7 @@ void main() { group('validation', () { test('validates valid Bitcoin address', () { final validator = AddressValidator(type: CryptoCurrency.btc); - expect(validator.isValid('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq'), - isTrue); + expect(validator.isValid('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq'), isTrue); expect(validator.isValid('3AD1Btx1MzYGmdpNpeujCfuvU5SsU2LX88'), isTrue); expect(validator.isValid('1HARAhFcvz8ZQp5MhnLFeUynC4bkha3Hv8'), isTrue); }); @@ -106,8 +98,7 @@ void main() { test('rejects invalid Bitcoin address', () { final validator = AddressValidator(type: CryptoCurrency.btc); expect(validator.isValid('invalid_address'), isFalse); - expect(validator.isValid('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043CakeWallet'), - isFalse); + expect(validator.isValid('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043CakeWallet'), isFalse); }); test('validates valid Ethereum address', () { @@ -119,8 +110,8 @@ void main() { test('rejects invalid Ethereum address', () { final validator = AddressValidator(type: CryptoCurrency.eth); expect(validator.isValid('invalid_address'), isFalse); - expect(validator.isValid('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc'), - isFalse); // Too short + expect( + validator.isValid('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc'), isFalse); // Too short expect(validator.isValid('C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), isFalse); // Missing 0x prefix }); @@ -144,8 +135,7 @@ void main() { test('validates valid Litecoin address', () { final validator = AddressValidator(type: CryptoCurrency.ltc); - expect(validator.isValid('ltc1qzvxlvlk8wsmue0np20eh3d3qxsusx9jstf8qw8'), - isTrue); + expect(validator.isValid('ltc1qzvxlvlk8wsmue0np20eh3d3qxsusx9jstf8qw8'), isTrue); expect( validator.isValid( 'ltcmweb1qqt9hqch2d0vfdsvt4tf27gullem2tcd57xxrvta9xwvfmwdkn4927q6d8sq6ftw7lkqdkr5g36eqn7w06edgq8tz7gy0nv5d4lhajctkzuath23a'), @@ -155,15 +145,20 @@ void main() { test('rejects invalid Litecoin address', () { final validator = AddressValidator(type: CryptoCurrency.ltc); expect(validator.isValid('invalid_address'), isFalse); - expect( - validator.isValid('ltc1qzvxlvlk8wsmue0np20eh3d3qxsusxCakeWallet'), - isFalse); + expect(validator.isValid('ltc1qzvxlvlk8wsmue0np20eh3d3qxsusxCakeWallet'), isFalse); }); }); - group('silentPaymentAddressPattern', () { + group('silentPaymentAddressPatternMainnet', () { test('returns a non-empty pattern', () { - final pattern = AddressValidator.silentPaymentAddressPattern; + final pattern = AddressValidator.silentPaymentAddressPatternMainnet; + expect(pattern, isNotEmpty); + }); + }); + + group('silentPaymentAddressPatternTestnet', () { + test('returns a non-empty pattern', () { + final pattern = AddressValidator.silentPaymentAddressPatternTestnet; expect(pattern, isNotEmpty); }); });