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
This commit is contained in:
rafael_xmr
2025-07-12 00:19:35 +03:00
committed by GitHub
parent 4009f64ee9
commit 8207eed466
6 changed files with 71 additions and 48 deletions
+3 -1
View File
@@ -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!;
}
+32 -9
View File
@@ -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;
+3 -3
View File
@@ -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)
];
@@ -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<CryptoCurrency> 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;
}
+5 -4
View File
@@ -215,7 +215,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
Validator<String> get allAmountValidator => AllAmountValidator();
Validator<String> get addressValidator => AddressValidator(type: selectedCryptoCurrency);
Validator<String> get addressValidator =>
AddressValidator(type: selectedCryptoCurrency, isTestnet: wallet.isTestnet);
Validator<String> 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;
}
+22 -27
View File
@@ -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);
});
});