From e4156ba28270a4cf5657976f91a5048fe5b10245 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 17 Apr 2024 16:35:11 -0300 Subject: [PATCH] feat: change scanning to subscription model, sync improvements --- cw_bitcoin/lib/bitcoin_address_record.dart | 3 +- cw_bitcoin/lib/bitcoin_unspent.dart | 49 +- cw_bitcoin/lib/bitcoin_wallet.dart | 10 - cw_bitcoin/lib/electrum.dart | 38 +- cw_bitcoin/lib/electrum_balance.dart | 2 +- cw_bitcoin/lib/electrum_transaction_info.dart | 7 +- cw_bitcoin/lib/electrum_wallet.dart | 746 ++++++++++-------- cw_bitcoin/lib/electrum_wallet_addresses.dart | 17 +- cw_bitcoin/lib/exceptions.dart | 6 +- .../lib/pending_bitcoin_transaction.dart | 4 + cw_core/lib/exceptions.dart | 7 +- cw_core/lib/sync_status.dart | 23 +- cw_core/lib/unspent_coins_info.dart | 8 +- lib/bitcoin/cw_bitcoin.dart | 8 +- lib/core/sync_status_title.dart | 4 + lib/entities/default_settings_migration.dart | 12 +- .../screens/receive/widgets/address_cell.dart | 2 +- lib/src/screens/send/send_page.dart | 11 +- .../unspent_coins_list_page.dart | 1 + .../widgets/unspent_coins_list_item.dart | 28 +- lib/view_model/send/send_view_model.dart | 6 + .../unspent_coins/unspent_coins_item.dart | 6 +- .../unspent_coins_list_view_model.dart | 42 +- .../wallet_address_list_view_model.dart | 7 +- res/values/strings_ar.arb | 2 + res/values/strings_bg.arb | 2 + res/values/strings_cs.arb | 2 + res/values/strings_de.arb | 2 + res/values/strings_en.arb | 2 + res/values/strings_es.arb | 2 + res/values/strings_fr.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 2 + res/values/strings_hr.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 2 + res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 4 +- res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + tool/configure.dart | 1 + 51 files changed, 696 insertions(+), 406 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 220298e55..bf36e6fb9 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -90,7 +90,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { String? scriptHash; - String updateScriptHash(BasedUtxoNetwork network) { + String getScriptHash(BasedUtxoNetwork network) { + if (scriptHash != null) return scriptHash!; scriptHash = sh.scriptHash(address, network: network); return scriptHash!; } diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index b2c1d90c4..3691a7a22 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -2,18 +2,16 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_core/unspent_transaction_output.dart'; class BitcoinUnspent extends Unspent { - BitcoinUnspent(BaseBitcoinAddressRecord addressRecord, String hash, int value, int vout, - {this.silentPaymentTweak}) + BitcoinUnspent(BaseBitcoinAddressRecord addressRecord, String hash, int value, int vout) : bitcoinAddressRecord = addressRecord, super(addressRecord.address, hash, value, vout, null); - factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord address, Map json) => + factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord? address, Map json) => BitcoinUnspent( - address, + address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()), json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int, - silentPaymentTweak: json['silent_payment_tweak'] as String?, ); Map toJson() { @@ -22,11 +20,48 @@ class BitcoinUnspent extends Unspent { 'tx_hash': hash, 'value': value, 'tx_pos': vout, - 'silent_payment_tweak': silentPaymentTweak, }; return json; } final BaseBitcoinAddressRecord bitcoinAddressRecord; - String? silentPaymentTweak; +} + +class BitcoinSilentPaymentsUnspent extends BitcoinUnspent { + BitcoinSilentPaymentsUnspent( + BitcoinSilentPaymentAddressRecord addressRecord, + String hash, + int value, + int vout, { + required this.silentPaymentTweak, + required this.silentPaymentLabel, + }) : super(addressRecord, hash, value, vout); + + @override + factory BitcoinSilentPaymentsUnspent.fromJSON( + BitcoinSilentPaymentAddressRecord? address, Map json) => + BitcoinSilentPaymentsUnspent( + address ?? BitcoinSilentPaymentAddressRecord.fromJSON(json['address_record'].toString()), + json['tx_hash'] as String, + json['value'] as int, + json['tx_pos'] as int, + silentPaymentTweak: json['silent_payment_tweak'] as String?, + silentPaymentLabel: json['silent_payment_label'] as String?, + ); + + @override + Map toJson() { + final json = { + 'address_record': bitcoinAddressRecord.toJSON(), + 'tx_hash': hash, + 'value': value, + 'tx_pos': vout, + 'silent_payment_tweak': silentPaymentTweak, + 'silent_payment_label': silentPaymentLabel, + }; + return json; + } + + String? silentPaymentTweak; + String? silentPaymentLabel; } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index e0218eff5..a31925817 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -64,19 +64,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ), ); - hasSilentPaymentsScanning = true; - autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); - - reaction((_) => walletAddresses.addressPageType, (BitcoinAddressType addressPageType) { - final prev = hasSilentPaymentsScanning; - hasSilentPaymentsScanning = addressPageType == SilentPaymentsAddresType.p2sp; - if (prev != hasSilentPaymentsScanning) { - startSync(); - } - }); } static Future create({ diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 6c532a7ce..e9383dda5 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -36,14 +36,15 @@ class ElectrumClient { _errors = {}, unterminatedString = ''; - static const connectionTimeout = Duration(seconds: 300); - static const aliveTimerDuration = Duration(seconds: 300); + static const connectionTimeout = Duration(seconds: 10); + static const aliveTimerDuration = Duration(seconds: 10); bool get isConnected => _isConnected; Socket? socket; void Function(bool)? onConnectionStatusChange; int _id; final Map _tasks; + Map get tasks => _tasks; final Map _errors; bool _isConnected; Timer? _aliveTimer; @@ -277,11 +278,18 @@ class ElectrumClient { Future> getHeader({required int height}) async => await call(method: 'blockchain.block.get_header', params: [height]) as Map; - Future> getTweaks({required int height, required int count}) async => - await callWithTimeout( - method: 'blockchain.block.tweaks', - params: [height, count], - timeout: 10000) as Map; + BehaviorSubject? tweaksSubscribe({required int height}) { + _id += 1; + return subscribe( + id: 'blockchain.tweaks.subscribe', + method: 'blockchain.tweaks.subscribe', + params: [height], + ); + } + + Future> getTweaks({required int height}) async => + await callWithTimeout(method: 'blockchain.tweaks.get', params: [height], timeout: 10000) + as Map; Future estimatefee({required int p}) => call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) { @@ -325,9 +333,6 @@ class ElectrumClient { }); Future> feeRates({BasedUtxoNetwork? network}) async { - if (network == BitcoinNetwork.testnet) { - return [1, 1, 1]; - } try { final topDoubleString = await estimatefee(p: 1); final middleDoubleString = await estimatefee(p: 5); @@ -349,7 +354,7 @@ class ElectrumClient { // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" // } Future getCurrentBlockChainTip() => - call(method: 'blockchain.headers.subscribe').then((result) { + callWithTimeout(method: 'blockchain.headers.subscribe').then((result) { if (result is Map) { return result["height"] as int; } @@ -357,6 +362,12 @@ class ElectrumClient { return null; }); + BehaviorSubject? chainTipSubscribe() { + _id += 1; + return subscribe( + id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe'); + } + BehaviorSubject? chainTipUpdate() { _id += 1; return subscribe( @@ -456,6 +467,11 @@ class ElectrumClient { _tasks[id]?.subject?.add(params.last); break; + case 'blockchain.tweaks.subscribe': + case 'blockchain.headers.subscribe': + final params = request['params'] as List; + _tasks[method]?.subject?.add(params.last); + break; default: break; } diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 45de7de6d..15d6843d8 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -23,7 +23,7 @@ class ElectrumBalance extends Balance { } int confirmed; - final int unconfirmed; + int unconfirmed; final int frozen; @override diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index d98d7a5b4..d06cfe9de 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -18,7 +18,7 @@ class ElectrumTransactionBundle { } class ElectrumTransactionInfo extends TransactionInfo { - List? unspents; + List? unspents; ElectrumTransactionInfo(this.type, {required String id, @@ -178,9 +178,8 @@ class ElectrumTransactionInfo extends TransactionInfo { outputAddresses.isEmpty ? [] : outputAddresses.map((e) => e.toString()).toList(), to: data['to'] as String?, unspents: unspents - .map((unspent) => BitcoinUnspent.fromJSON( - BitcoinSilentPaymentAddressRecord.fromJSON(unspent['address_record'].toString()), - unspent as Map)) + .map((unspent) => + BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map)) .toList(), ); } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 0ec535ab3..b2962f1e0 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -91,7 +91,13 @@ abstract class ElectrumWalletBase transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); reaction((_) => syncStatus, (SyncStatus syncStatus) { - silentPaymentsScanningActive = syncStatus is SyncingSyncStatus; + if (syncStatus is! AttemptingSyncStatus) + silentPaymentsScanningActive = syncStatus is SyncingSyncStatus; + + if (syncStatus is NotConnectedSyncStatus) { + // Needs to re-subscribe to all scripthashes when reconnected + _scripthashesUpdateSubject = {}; + } }); } @@ -122,14 +128,7 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - List get scriptHashes => walletAddresses.allAddresses - .map((addr) => scriptHash(addr.address, network: network)) - .toList(); - - List get publicScriptHashes => walletAddresses.allAddresses - .where((addr) => !addr.isHidden) - .map((addr) => scriptHash(addr.address, network: network)) - .toList(); + Set get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet(); String get xpub => hd.base58!; @@ -142,8 +141,8 @@ abstract class ElectrumWalletBase @override bool? isTestnet; - @observable - bool hasSilentPaymentsScanning = false; + bool get hasSilentPaymentsScanning => type == WalletType.bitcoin; + @observable bool nodeSupportsSilentPayments = true; @observable @@ -151,13 +150,14 @@ abstract class ElectrumWalletBase @action Future setSilentPaymentsScanning(bool active) async { + syncStatus = AttemptingSyncStatus(); silentPaymentsScanningActive = active; if (active) { await _setInitialHeight(); - if ((currentChainTip ?? 0) > walletInfo.restoreHeight) { - _setListeners(walletInfo.restoreHeight, chainTip: currentChainTip); + if (await currentChainTip > walletInfo.restoreHeight) { + _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip); } } else { _isolate?.then((runningIsolate) => runningIsolate.kill(priority: Isolate.immediate)); @@ -174,7 +174,11 @@ abstract class ElectrumWalletBase } @observable - int? currentChainTip; + int? _currentChainTip; + + @computed + Future get currentChainTip async => + _currentChainTip ?? await electrumClient.getCurrentBlockChainTip() ?? 0; @override BitcoinWalletKeys get keys => @@ -183,7 +187,9 @@ abstract class ElectrumWalletBase String _password; List unspentCoins; List _feeRates; + // ignore: prefer_final_fields Map?> _scripthashesUpdateSubject; + // ignore: prefer_final_fields BehaviorSubject? _chainTipUpdateSubject; bool _isTransactionUpdating; Future? _isolate; @@ -201,8 +207,8 @@ abstract class ElectrumWalletBase } @action - Future _setListeners(int height, {int? chainTip, bool? doSingleScan}) async { - final currentChainTip = chainTip ?? await electrumClient.getCurrentBlockChainTip() ?? 0; + Future _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async { + final chainTip = chainTipParam ?? await currentChainTip; syncStatus = AttemptingSyncStatus(); if (_isolate != null) { @@ -218,7 +224,7 @@ abstract class ElectrumWalletBase silentAddress: walletAddresses.silentAddress!, network: network, height: height, - chainTip: currentChainTip, + chainTip: chainTip, electrumClient: ElectrumClient(), transactionHistoryIds: transactionHistory.transactions.keys.toList(), node: ScanNode(node!.uri, node!.useSSL), @@ -237,12 +243,33 @@ abstract class ElectrumWalletBase final tx = map.value; if (tx.unspents != null) { - tx.unspents!.forEach((unspent) => walletAddresses.addSilentAddresses( - [unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord])); - final existingTxInfo = transactionHistory.transactions[txid]; final txAlreadyExisted = existingTxInfo != null; + void updateSilentAddressRecord(BitcoinSilentPaymentsUnspent unspent) { + final silentAddress = walletAddresses.silentAddress!; + final silentPaymentAddress = SilentPaymentAddress( + version: silentAddress.version, + B_scan: silentAddress.B_scan, + B_spend: unspent.silentPaymentLabel != null + ? silentAddress.B_spend.tweakAdd( + BigintUtils.fromBytes( + BytesUtils.fromHexString(unspent.silentPaymentLabel!)), + ) + : silentAddress.B_spend, + hrp: silentAddress.hrp, + ); + + final addressRecord = walletAddresses.silentAddresses.firstWhereOrNull( + (address) => address.address == silentPaymentAddress.toString()); + addressRecord?.txCount += 1; + addressRecord?.balance += unspent.value; + + walletAddresses.addSilentAddresses( + [unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord], + ); + } + // Updating tx after re-scanned if (txAlreadyExisted) { existingTxInfo.amount = tx.amount; @@ -258,37 +285,39 @@ abstract class ElectrumWalletBase .toList(); if (newUnspents.isNotEmpty) { + newUnspents.forEach(updateSilentAddressRecord); + existingTxInfo.unspents ??= []; existingTxInfo.unspents!.addAll(newUnspents); - final amount = newUnspents.length > 1 + final newAmount = newUnspents.length > 1 ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) : newUnspents[0].value; if (existingTxInfo.direction == TransactionDirection.incoming) { - existingTxInfo.amount += amount; - } else { - existingTxInfo.amount = amount; - existingTxInfo.direction = TransactionDirection.incoming; + existingTxInfo.amount += newAmount; } + + // Updates existing TX transactionHistory.addOne(existingTxInfo); + // Update balance record + balance[currency]!.confirmed += newAmount; } } else { - final addressRecord = - walletAddresses.silentAddresses.firstWhere((addr) => addr.address == tx.to); - addressRecord.txCount += 1; + // else: First time seeing this TX after scanning + tx.unspents!.forEach(updateSilentAddressRecord); + // Add new TX record transactionHistory.addMany(message); + // Update balance record + balance[currency]!.confirmed += tx.amount; } - await transactionHistory.save(); - await updateUnspent(); - await updateBalance(); + await updateAllUnspents(); } } } - // check if is a SyncStatus type since "is SyncStatus" doesn't work here if (message is SyncResponse) { if (message.syncStatus is UnsupportedSyncStatus) { nodeSupportsSilentPayments = false; @@ -296,7 +325,6 @@ abstract class ElectrumWalletBase syncStatus = message.syncStatus; walletInfo.restoreHeight = message.height; - await walletInfo.save(); } } } @@ -305,32 +333,25 @@ abstract class ElectrumWalletBase @override Future startSync() async { try { - syncStatus = AttemptingSyncStatus(); + syncStatus = SyncronizingSyncStatus(); if (hasSilentPaymentsScanning) { - try { - await _setInitialHeight(); - } catch (_) {} + await _setInitialHeight(); } - if (silentPaymentsScanningActive) { - if ((currentChainTip ?? 0) > walletInfo.restoreHeight) { - _setListeners(walletInfo.restoreHeight, chainTip: currentChainTip); - } + await _subscribeForUpdates(); + + int finished = 0; + void checkFinishedAllUpdates(void _) { + finished++; + if (finished == 2) syncStatus = SyncedSyncStatus(); } + // Always await first as it discovers new addresses in the process await updateTransactions(); - _subscribeForUpdates(); - await updateUnspent(); - await updateBalance(); - _feeRates = await electrumClient.feeRates(network: network); - Timer.periodic( - const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates()); - - if (!silentPaymentsScanningActive || walletInfo.restoreHeight == currentChainTip) { - syncStatus = SyncedSyncStatus(); - } + updateAllUnspents().then(checkFinishedAllUpdates); + updateBalance().then(checkFinishedAllUpdates); } catch (e, stacktrace) { print(stacktrace); print(e.toString()); @@ -338,29 +359,39 @@ abstract class ElectrumWalletBase } } + @action + Future updateFeeRates() async { + final feeRates = await electrumClient.feeRates(network: network); + if (feeRates != [0, 0, 0]) { + _feeRates = feeRates; + } + } + Node? node; @action @override Future connectToNode({required Node node}) async { + final differentNode = this.node?.uri != node.uri || this.node?.useSSL != node.useSSL; this.node = node; try { syncStatus = ConnectingSyncStatus(); - if (!electrumClient.isConnected) { - await electrumClient.close(); - } + electrumClient.onConnectionStatusChange = null; + await electrumClient.close(); - electrumClient.onConnectionStatusChange = (bool isConnected) async { - if (isConnected) { - syncStatus = ConnectedSyncStatus(); - } else if (isConnected == false) { - syncStatus = LostConnectionSyncStatus(); - } - }; + await Timer(Duration(seconds: differentNode ? 0 : 10), () async { + electrumClient.onConnectionStatusChange = (bool isConnected) async { + if (isConnected && syncStatus is! SyncedSyncStatus) { + syncStatus = ConnectedSyncStatus(); + } else if (!isConnected) { + syncStatus = LostConnectionSyncStatus(); + } + }; - await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); + await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); + }); } catch (e) { print(e.toString()); syncStatus = FailedSyncStatus(); @@ -436,8 +467,15 @@ abstract class ElectrumWalletBase for (int i = 0; i < unspentCoins.length; i++) { final utx = unspentCoins[i]; - if (utx.isSending) { - spendsCPFP = utx.confirmations == 0; + if (utx.isSending && !utx.isFrozen) { + if (hasSilentPayment) { + // Check inputs for shared secret derivation + if (utx.bitcoinAddressRecord.type == SegwitAddresType.p2wsh) { + throw BitcoinTransactionSilentPaymentsNotSupported(); + } + } + + if (!spendsCPFP) spendsCPFP = utx.confirmations == 0; allInputsAmount += utx.value; @@ -447,11 +485,10 @@ abstract class ElectrumWalletBase bool? isSilentPayment = false; if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { + final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; privkey = walletAddresses.silentAddress!.b_spend.tweakAdd( BigintUtils.fromBytes( - BytesUtils.fromHexString( - (utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord).silentPaymentTweak!, - ), + BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!), ), ); spendsSilentPayment = true; @@ -464,7 +501,11 @@ abstract class ElectrumWalletBase ); } - inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr)); + inputPrivKeyInfos.add(ECPrivateInfo( + privkey, + address.type == SegwitAddresType.p2tr, + tweak: !isSilentPayment, + )); inputPubKeys.add(privkey.getPublic()); vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); @@ -520,6 +561,10 @@ abstract class ElectrumWalletBase // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change int amount = allInputsAmount - fee; + if (amount <= 0) { + throw BitcoinTransactionWrongBalanceException(); + } + // Attempting to send less than the dust limit if (_isBelowDust(amount)) { throw BitcoinTransactionNoDustException(); @@ -580,11 +625,10 @@ abstract class ElectrumWalletBase bool? isSilentPayment = false; if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { + final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; privkey = walletAddresses.silentAddress!.b_spend.tweakAdd( BigintUtils.fromBytes( - BytesUtils.fromHexString( - (utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord).silentPaymentTweak!, - ), + BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!), ), ); spendsSilentPayment = true; @@ -597,7 +641,11 @@ abstract class ElectrumWalletBase ); } - inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr)); + inputPrivKeyInfos.add(ECPrivateInfo( + privkey, + address.type == SegwitAddresType.p2tr, + tweak: !isSilentPayment, + )); inputPubKeys.add(privkey.getPublic()); vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); @@ -637,6 +685,16 @@ abstract class ElectrumWalletBase int amountLeftForChangeAndFee = allInputsAmount - credentialsAmount; if (amountLeftForChangeAndFee <= 0) { + if (!spendingAllCoins) { + return estimateTxForAmount( + credentialsAmount, + outputs, + feeRate, + inputsCount: utxos.length + 1, + memo: memo, + hasSilentPayment: hasSilentPayment, + ); + } throw BitcoinTransactionWrongBalanceException(); } @@ -684,6 +742,7 @@ abstract class ElectrumWalletBase // Still has inputs to spend before failing if (!spendingAllCoins) { + outputs.removeLast(); return estimateTxForAmount( credentialsAmount, outputs, @@ -730,6 +789,7 @@ abstract class ElectrumWalletBase outputs.removeLast(); } + outputs.removeLast(); return estimateTxForAmount( credentialsAmount, outputs, @@ -1025,7 +1085,8 @@ abstract class ElectrumWalletBase Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - Future updateUnspent() async { + @action + Future updateAllUnspents() async { List updatedUnspentCoins = []; if (hasSilentPaymentsScanning) { @@ -1037,20 +1098,9 @@ abstract class ElectrumWalletBase }); } - final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); - - await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient - .getListUnspentWithAddress(address.address, network) - .then((unspent) => Future.forEach>(unspent, (unspent) async { - try { - final coin = BitcoinUnspent.fromJSON(address, unspent); - final tx = await fetchTransactionInfo( - hash: coin.hash, height: 0, myAddresses: addressesSet); - coin.isChange = tx?.direction == TransactionDirection.outgoing; - coin.confirmations = tx?.confirmations; - updatedUnspentCoins.add(coin); - } catch (_) {} - })))); + await Future.wait(walletAddresses.allAddresses.map((address) async { + updatedUnspentCoins.addAll(await fetchUnspent(address)); + })); unspentCoins = updatedUnspentCoins; @@ -1072,6 +1122,7 @@ abstract class ElectrumWalletBase coin.isFrozen = coinInfo.isFrozen; coin.isSending = coinInfo.isSending; coin.note = coinInfo.note; + coin.bitcoinAddressRecord.balance += coinInfo.value; } else { _addCoinInfo(coin); } @@ -1081,6 +1132,55 @@ abstract class ElectrumWalletBase await _refreshUnspentCoinsInfo(); } + @action + Future updateUnspents(BitcoinAddressRecord address) async { + final newUnspentCoins = await fetchUnspent(address); + + if (newUnspentCoins.isNotEmpty) { + unspentCoins.addAll(newUnspentCoins); + + newUnspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + coin.bitcoinAddressRecord.balance += coinInfo.value; + } else { + _addCoinInfo(coin); + } + }); + } + } + + @action + Future> fetchUnspent(BitcoinAddressRecord address) async { + final unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); + + List updatedUnspentCoins = []; + + await Future.wait(unspents.map((unspent) async { + try { + final coin = BitcoinUnspent.fromJSON(address, unspent); + final tx = await fetchTransactionInfo(hash: coin.hash, height: 0); + coin.isChange = address.isHidden; + coin.confirmations = tx?.confirmations; + + updatedUnspentCoins.add(coin); + } catch (_) {} + })); + + return updatedUnspentCoins; + } + @action Future _addCoinInfo(BitcoinUnspent coin) async { final newInfo = UnspentCoinsInfo( @@ -1093,6 +1193,7 @@ abstract class ElectrumWalletBase value: coin.value, vout: coin.vout, isChange: coin.isChange, + isSilentPayment: coin is BitcoinSilentPaymentsUnspent, ); await unspentCoinsInfo.add(newInfo); @@ -1309,8 +1410,8 @@ abstract class ElectrumWalletBase time = status["block_time"] as int?; final height = status["block_height"] as int? ?? 0; - confirmations = - height > 0 ? (await electrumClient.getCurrentBlockChainTip())! - height + 1 : 0; + final tip = await currentChainTip; + if (tip > 0) confirmations = height > 0 ? tip - height + 1 : 0; } else { final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); @@ -1335,18 +1436,15 @@ abstract class ElectrumWalletBase } Future fetchTransactionInfo( - {required String hash, - required int height, - required Set myAddresses, - bool? retryOnFailure}) async { + {required String hash, required int height, bool? retryOnFailure}) async { try { return ElectrumTransactionInfo.fromElectrumBundle( await getTransactionExpanded(hash: hash), walletInfo.type, network, - addresses: myAddresses, height: height); + addresses: addressesSet, height: height); } catch (e) { if (e is FormatException && retryOnFailure == true) { await Future.delayed(const Duration(seconds: 2)); - return fetchTransactionInfo(hash: hash, height: height, myAddresses: myAddresses); + return fetchTransactionInfo(hash: hash, height: height); } return null; } @@ -1356,11 +1454,15 @@ abstract class ElectrumWalletBase Future> fetchTransactions() async { try { final Map historiesWithDetails = {}; - final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); - currentChainTip ??= await electrumClient.getCurrentBlockChainTip() ?? 0; - await Future.wait(ADDRESS_TYPES.map( - (type) => fetchTransactionsForAddressType(addressesSet, historiesWithDetails, type))); + if (type == WalletType.bitcoin) { + await Future.wait(ADDRESS_TYPES + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); + } else if (type == WalletType.bitcoinCash) { + await fetchTransactionsForAddressType(historiesWithDetails, P2pkhAddressType.p2pkh); + } else if (type == WalletType.litecoin) { + await fetchTransactionsForAddressType(historiesWithDetails, SegwitAddresType.p2wpkh); + } return historiesWithDetails; } catch (e) { @@ -1370,7 +1472,6 @@ abstract class ElectrumWalletBase } Future fetchTransactionsForAddressType( - Set addressesSet, Map historiesWithDetails, BitcoinAddressType type, ) async { @@ -1379,39 +1480,50 @@ abstract class ElectrumWalletBase final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false); await Future.wait(addressesByType.map((addressRecord) async { - final history = await _fetchAddressHistory(addressRecord, addressesSet, currentChainTip!); + final history = await _fetchAddressHistory(addressRecord, await currentChainTip); if (history.isNotEmpty) { addressRecord.txCount = history.length; historiesWithDetails.addAll(history); final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses; - final isLastUsedAddress = history.isNotEmpty && matchedAddresses.last == addressRecord; + final isUsedAddressUnderGap = matchedAddresses.toList().indexOf(addressRecord) >= + matchedAddresses.length - + (addressRecord.isHidden + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); - if (isLastUsedAddress) { - // The last address by gap limit is used, discover new addresses for the same address type + if (isUsedAddressUnderGap) { + final prevLength = walletAddresses.allAddresses.length; + + // Discover new addresses for the same address type until the gap limit is respected await walletAddresses.discoverAddresses( matchedAddresses.toList(), addressRecord.isHidden, - (address, addressesSet) => _fetchAddressHistory(address, addressesSet, currentChainTip!) - .then((history) => history.isNotEmpty ? address.address : null), + (address) async { + await _subscribeForUpdates(); + return _fetchAddressHistory(address, await currentChainTip) + .then((history) => history.isNotEmpty ? address.address : null); + }, type: type, ); - // Continue until the last address by this address type is not used yet - await fetchTransactionsForAddressType(addressesSet, historiesWithDetails, type); + final newLength = walletAddresses.allAddresses.length; + + if (newLength > prevLength) { + await fetchTransactionsForAddressType(historiesWithDetails, type); + } } } })); } Future> _fetchAddressHistory( - BitcoinAddressRecord addressRecord, Set addressesSet, int currentHeight) async { + BitcoinAddressRecord addressRecord, int? currentHeight) async { try { final Map historiesWithDetails = {}; - final history = await electrumClient - .getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network)); + final history = await electrumClient.getHistory(addressRecord.getScriptHash(network)); if (history.isNotEmpty) { addressRecord.setAsUsed(); @@ -1425,14 +1537,13 @@ abstract class ElectrumWalletBase if (height > 0) { storedTx.height = height; // the tx's block itself is the first confirmation so add 1 - storedTx.confirmations = currentHeight - height + 1; + if (currentHeight != null) storedTx.confirmations = currentHeight - height + 1; storedTx.isPending = storedTx.confirmations == 0; } historiesWithDetails[txid] = storedTx; } else { - final tx = await fetchTransactionInfo( - hash: txid, height: height, myAddresses: addressesSet, retryOnFailure: true); + final tx = await fetchTransactionInfo(hash: txid, height: height, retryOnFailure: true); if (tx != null) { historiesWithDetails[txid] = tx; @@ -1462,9 +1573,9 @@ abstract class ElectrumWalletBase return; } - transactionHistory.transactions.values.forEach((tx) { - if (tx.unspents != null && currentChainTip != null) { - tx.confirmations = currentChainTip! - tx.height + 1; + transactionHistory.transactions.values.forEach((tx) async { + if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height > 0) { + tx.confirmations = await currentChainTip - tx.height + 1; } }); @@ -1479,15 +1590,24 @@ abstract class ElectrumWalletBase } } - void _subscribeForUpdates() async { - scriptHashes.forEach((sh) async { + Future _subscribeForUpdates() async { + final unsubscribedScriptHashes = walletAddresses.allAddresses.where( + (address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)), + ); + + await Future.wait(unsubscribedScriptHashes.map((address) async { + final sh = address.getScriptHash(network); await _scripthashesUpdateSubject[sh]?.close(); - _scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh); + _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh]?.listen((event) async { try { - await updateUnspent(); - await updateBalance(); - await updateTransactions(); + await updateUnspents(address); + + final newBalance = await _fetchBalance(sh); + balance[currency]?.confirmed += newBalance.confirmed; + balance[currency]?.unconfirmed += newBalance.unconfirmed; + + await _fetchAddressHistory(address, await currentChainTip); } catch (e, s) { print(e.toString()); _onError?.call(FlutterErrorDetails( @@ -1497,24 +1617,7 @@ abstract class ElectrumWalletBase )); } }); - }); - - await _chainTipUpdateSubject?.close(); - _chainTipUpdateSubject = electrumClient.chainTipUpdate(); - _chainTipUpdateSubject?.listen((_) async { - try { - final currentHeight = await electrumClient.getCurrentBlockChainTip(); - if (currentHeight != null) walletInfo.restoreHeight = currentHeight; - _setListeners(walletInfo.restoreHeight, chainTip: currentHeight); - } catch (e, s) { - print(e.toString()); - _onError?.call(FlutterErrorDetails( - exception: e, - stack: s, - library: this.runtimeType.toString(), - )); - } - }); + })); } Future _fetchBalances() async { @@ -1534,17 +1637,15 @@ abstract class ElectrumWalletBase if (hasSilentPaymentsScanning) { // Add values from unspent coins that are not fetched by the address list // i.e. scanned silent payments - unspentCoinsInfo.values.forEach((info) { - unspentCoins.forEach((element) { - if (element.hash == info.hash && - element.bitcoinAddressRecord.address == info.address && - element.value == info.value) { - if (info.isFrozen) totalFrozen += element.value; - if (element.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { - totalConfirmed += element.value; + transactionHistory.transactions.values.forEach((tx) { + if (tx.unspents != null) { + tx.unspents!.forEach((unspent) { + if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { + if (unspent.isFrozen) totalFrozen += unspent.value; + totalConfirmed += unspent.value; } - } - }); + }); + } }); } @@ -1567,6 +1668,13 @@ abstract class ElectrumWalletBase confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen); } + Future _fetchBalance(String sh) async { + final balance = await electrumClient.getBalance(sh); + final confirmed = balance['confirmed'] as int? ?? 0; + final unconfirmed = balance['unconfirmed'] as int? ?? 0; + return ElectrumBalance(confirmed: confirmed, unconfirmed: unconfirmed, frozen: 0); + } + Future updateBalance() async { balance[currency] = await _fetchBalances(); await save(); @@ -1597,10 +1705,19 @@ abstract class ElectrumWalletBase } Future _setInitialHeight() async { - currentChainTip = await electrumClient.getCurrentBlockChainTip(); - if (currentChainTip != null && walletInfo.restoreHeight == 0) { - walletInfo.restoreHeight = currentChainTip!; - } + if (_chainTipUpdateSubject != null) return; + + _chainTipUpdateSubject = await electrumClient.chainTipSubscribe(); + _chainTipUpdateSubject?.listen((e) async { + final event = e as Map; + final height = int.parse(event['height'].toString()); + + _currentChainTip = height; + + if (_currentChainTip != null && _currentChainTip! > 0 && walletInfo.restoreHeight == 0) { + walletInfo.restoreHeight = _currentChainTip!; + } + }); } static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) { @@ -1679,8 +1796,6 @@ class SyncResponse { } Future startRefresh(ScanData scanData) async { - var cachedBlockchainHeight = scanData.chainTip; - Future getElectrumConnection() async { final electrumClient = scanData.electrumClient; if (!electrumClient.isConnected) { @@ -1690,11 +1805,6 @@ Future startRefresh(ScanData scanData) async { return electrumClient; } - Future getUpdatedNodeHeight() async { - final electrumClient = await getElectrumConnection(); - return await electrumClient.getCurrentBlockChainTip() ?? cachedBlockchainHeight; - } - var lastKnownBlockHeight = 0; var initialSyncHeight = 0; @@ -1714,37 +1824,163 @@ Future startRefresh(ScanData scanData) async { return; } - // Run this until no more blocks left to scan txs. At first this was recursive - // i.e. re-calling the startRefresh function but this was easier for the above values to retain - // their initial values - while (true) { - lastKnownBlockHeight = syncHeight; + BehaviorSubject? tweaksSubscription = null; - SyncingSyncStatus syncingStatus; - if (scanData.isSingleScan) { - syncingStatus = SyncingSyncStatus(1, 0); - } else { - syncingStatus = SyncingSyncStatus.fromHeightValues( - await getUpdatedNodeHeight(), initialSyncHeight, syncHeight); - } + lastKnownBlockHeight = syncHeight; - scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); + SyncingSyncStatus syncingStatus; + if (scanData.isSingleScan) { + syncingStatus = SyncingSyncStatus(1, 0); + } else { + syncingStatus = + SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + } - if (syncingStatus.blocksLeft <= 0 || (scanData.isSingleScan && scanData.height != syncHeight)) { - scanData.sendPort.send(SyncResponse(await getUpdatedNodeHeight(), SyncedSyncStatus())); - return; - } + scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); - try { - final electrumClient = await getElectrumConnection(); + if (syncingStatus.blocksLeft <= 0 || (scanData.isSingleScan && scanData.height != syncHeight)) { + scanData.sendPort.send(SyncResponse(scanData.chainTip, SyncedSyncStatus())); + return; + } - // TODO: hardcoded values, if timed out decrease amount of blocks per request? - final scanningBlockCount = - scanData.isSingleScan ? 1 : (scanData.network == BitcoinNetwork.testnet ? 25 : 10); + try { + final electrumClient = await getElectrumConnection(); - Map? tweaks; + if (tweaksSubscription == null) { try { - tweaks = await electrumClient.getTweaks(height: syncHeight, count: scanningBlockCount); + tweaksSubscription = await electrumClient.tweaksSubscribe(height: syncHeight); + + tweaksSubscription?.listen((t) { + final tweaks = t as Map; + + if (tweaks.isEmpty) { + syncHeight += 1; + scanData.sendPort.send( + SyncResponse( + syncHeight, + SyncingSyncStatus.fromHeightValues( + currentChainTip, + initialSyncHeight, + syncHeight, + ), + ), + ); + + return; + } + + final blockHeight = tweaks.keys.first.toString(); + + try { + final blockTweaks = tweaks[blockHeight] as Map; + + for (var j = 0; j < blockTweaks.keys.length; j++) { + final txid = blockTweaks.keys.elementAt(j); + final details = blockTweaks[txid] as Map; + final outputPubkeys = (details["output_pubkeys"] as Map); + final tweak = details["tweak"].toString(); + + try { + // scanOutputs called from rust here + final addToWallet = scanOutputs( + outputPubkeys.values.map((o) => o[0].toString()).toList(), + tweak, + Receiver( + scanData.silentAddress.b_scan.toHex(), + scanData.silentAddress.B_spend.toHex(), + scanData.network == BitcoinNetwork.testnet, + scanData.labelIndexes, + scanData.labelIndexes.length, + ), + ); + + if (addToWallet.isEmpty) { + // no results tx, continue to next tx + continue; + } + + // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) + final txInfo = ElectrumTransactionInfo( + WalletType.bitcoin, + id: txid, + height: syncHeight, + amount: 0, + fee: 0, + direction: TransactionDirection.incoming, + isPending: false, + date: DateTime.now(), + confirmations: scanData.chainTip - int.parse(blockHeight) + 1, + unspents: [], + ); + + addToWallet.forEach((label, value) { + (value as Map).forEach((output, tweak) { + final t_k = tweak.toString(); + + final receivingOutputAddress = ECPublic.fromHex(output) + .toTaprootAddress(tweak: false) + .toAddress(scanData.network); + + final receivedAddressRecord = BitcoinSilentPaymentAddressRecord( + receivingOutputAddress, + index: 0, + isHidden: false, + isUsed: true, + network: scanData.network, + silentPaymentTweak: t_k, + type: SegwitAddresType.p2tr, + txCount: 1, + ); + + int? amount; + int? pos; + outputPubkeys.entries.firstWhere((k) { + final isMatchingOutput = k.value[0] == output; + if (isMatchingOutput) { + amount = int.parse(k.value[1].toString()); + pos = int.parse(k.key.toString()); + return true; + } + return false; + }); + + final unspent = BitcoinSilentPaymentsUnspent( + receivedAddressRecord, + txid, + amount!, + pos!, + silentPaymentTweak: t_k, + silentPaymentLabel: label == "None" ? null : label, + ); + + txInfo.unspents!.add(unspent); + txInfo.amount += unspent.value; + }); + }); + + scanData.sendPort.send({txInfo.id: txInfo}); + } catch (_) {} + } + } catch (_) {} + + syncHeight += 1; + scanData.sendPort.send( + SyncResponse( + syncHeight, + SyncingSyncStatus.fromHeightValues( + currentChainTip, + initialSyncHeight, + syncHeight, + ), + ), + ); + + if (int.parse(blockHeight) >= scanData.chainTip) { + scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); + } + + return; + }); } catch (e) { if (e is RequestFailedTimeoutException) { return scanData.sendPort.send( @@ -1752,134 +1988,18 @@ Future startRefresh(ScanData scanData) async { ); } } - - if (tweaks == null) { - return scanData.sendPort.send( - SyncResponse(syncHeight, UnsupportedSyncStatus()), - ); - } - - if (tweaks.isEmpty) { - syncHeight += scanningBlockCount; - continue; - } - - final blockHeights = tweaks.keys; - for (var i = 0; i < blockHeights.length; i++) { - try { - final blockHeight = blockHeights.elementAt(i).toString(); - final blockTweaks = tweaks[blockHeight] as Map; - - for (var j = 0; j < blockTweaks.keys.length; j++) { - final txid = blockTweaks.keys.elementAt(j); - final details = blockTweaks[txid] as Map; - final outputPubkeys = (details["output_pubkeys"] as Map); - final tweak = details["tweak"].toString(); - - try { - final addToWallet = scanOutputs( - outputPubkeys.values.map((o) => o[0].toString()).toList(), - tweak, - Receiver( - scanData.silentAddress.b_scan.toHex(), - scanData.silentAddress.B_spend.toHex(), - scanData.network == BitcoinNetwork.testnet, - scanData.labelIndexes, - scanData.labelIndexes.length, - ), - ); - - if (addToWallet.isEmpty) { - // no results tx, continue to next tx - continue; - } - - addToWallet.forEach((label, value) async { - (value as Map).forEach((output, tweak) async { - final t_k = tweak.toString(); - - final receivingOutputAddress = ECPublic.fromHex(output) - .toTaprootAddress(tweak: false) - .toAddress(scanData.network); - - final receivedAddressRecord = BitcoinSilentPaymentAddressRecord( - receivingOutputAddress, - index: 0, - isHidden: false, - isUsed: true, - network: scanData.network, - silentPaymentTweak: t_k, - type: SegwitAddresType.p2tr, - txCount: 1, - ); - - int? amount; - int? pos; - outputPubkeys.entries.firstWhere((k) { - final matches = k.value[0] == output; - if (matches) { - amount = int.parse(k.value[1].toString()); - pos = int.parse(k.key.toString()); - return true; - } - return false; - }); - - final json = { - 'address_record': receivedAddressRecord.toJSON(), - 'tx_hash': txid, - 'value': amount!, - 'tx_pos': pos!, - 'silent_payment_tweak': t_k, - }; - - final tx = BitcoinUnspent.fromJSON(receivedAddressRecord, json); - - final silentPaymentAddress = SilentPaymentAddress( - version: scanData.silentAddress.version, - B_scan: scanData.silentAddress.B_scan, - B_spend: label == "None" - ? scanData.silentAddress.B_spend - : scanData.silentAddress.B_spend - .tweakAdd(BigintUtils.fromBytes(BytesUtils.fromHexString(label))), - hrp: scanData.silentAddress.hrp, - ); - - final txInfo = ElectrumTransactionInfo( - WalletType.bitcoin, - id: tx.hash, - height: syncHeight, - amount: amount!, - fee: 0, - direction: TransactionDirection.incoming, - isPending: false, - date: DateTime.now(), - confirmations: await getUpdatedNodeHeight() - int.parse(blockHeight) + 1, - to: silentPaymentAddress.toString(), - unspents: [tx], - ); - - scanData.sendPort.send({txInfo.id: txInfo}); - }); - }); - } catch (_) {} - } - } catch (e, s) { - print([e, s]); - } - - // Finished scanning block, add 1 to height and continue to next block in loop - syncHeight += 1; - scanData.sendPort.send(SyncResponse(syncHeight, - SyncingSyncStatus.fromHeightValues(currentChainTip, initialSyncHeight, syncHeight))); - } - } catch (e, stacktrace) { - print(stacktrace); - print(e.toString()); - - scanData.sendPort.send(SyncResponse(syncHeight, NotConnectedSyncStatus())); - break; } + + if (tweaksSubscription == null) { + return scanData.sendPort.send( + SyncResponse(syncHeight, UnsupportedSyncStatus()), + ); + } + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + + scanData.sendPort.send(SyncResponse(syncHeight, NotConnectedSyncStatus())); } } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index e9099ddee..160c4ed48 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -57,7 +57,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privKey!), hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp'); - if (silentAddresses.length == 0) + if (silentAddresses.length == 0) { silentAddresses.add(BitcoinSilentPaymentAddressRecord( silentAddress.toString(), index: 0, @@ -67,6 +67,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { network: network, type: SilentPaymentsAddresType.p2sp, )); + silentAddresses.add(BitcoinSilentPaymentAddressRecord( + silentAddress!.toLabeledSilentPaymentAddress(0).toString(), + index: 0, + isHidden: true, + name: "", + silentPaymentTweak: BytesUtils.toHexString(silentAddress!.generateLabel(0)), + network: network, + type: SilentPaymentsAddresType.p2sp, + )); + } } updateAddressesByMatch(); @@ -446,7 +456,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action Future discoverAddresses(List addressList, bool isHidden, - Future Function(BitcoinAddressRecord, Set) getAddressHistory, + Future Function(BitcoinAddressRecord) getAddressHistory, {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { if (!isHidden) { _validateSideHdAddresses(addressList.toList()); @@ -456,8 +466,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { startIndex: addressList.length, isHidden: isHidden, type: type); addAddresses(newAddresses); - final addressesWithHistory = await Future.wait(newAddresses - .map((addr) => getAddressHistory(addr, _addresses.map((e) => e.address).toSet()))); + final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory)); final isLastAddressUsed = addressesWithHistory.last == addressList.last.address; if (isLastAddressUsed) { diff --git a/cw_bitcoin/lib/exceptions.dart b/cw_bitcoin/lib/exceptions.dart index 4b03eb922..830f56c24 100644 --- a/cw_bitcoin/lib/exceptions.dart +++ b/cw_bitcoin/lib/exceptions.dart @@ -2,7 +2,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/exceptions.dart'; class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException { - BitcoinTransactionWrongBalanceException() : super(CryptoCurrency.btc); + BitcoinTransactionWrongBalanceException({super.amount}) : super(CryptoCurrency.btc); } class BitcoinTransactionNoInputsException extends TransactionNoInputsException {} @@ -25,3 +25,7 @@ class BitcoinTransactionCommitFailedDustOutputSendAll extends TransactionCommitFailedDustOutputSendAll {} class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailedVoutNegative {} + +class BitcoinTransactionCommitFailedBIP68Final extends TransactionCommitFailedBIP68Final {} + +class BitcoinTransactionSilentPaymentsNotSupported extends TransactionInputNotSupported {} diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 529ac61da..020919e43 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -73,6 +73,10 @@ class PendingBitcoinTransaction with PendingTransaction { if (error.contains("bad-txns-vout-negative")) { throw BitcoinTransactionCommitFailedVoutNegative(); } + + if (error.contains("non-BIP68-final")) { + throw BitcoinTransactionCommitFailedBIP68Final(); + } } throw BitcoinTransactionCommitFailed(); } diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index 848ac40e6..eeb43bc38 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -1,9 +1,10 @@ import 'package:cw_core/crypto_currency.dart'; class TransactionWrongBalanceException implements Exception { - TransactionWrongBalanceException(this.currency); + TransactionWrongBalanceException(this.currency, {this.amount}); final CryptoCurrency currency; + final int? amount; } class TransactionNoInputsException implements Exception {} @@ -28,3 +29,7 @@ class TransactionCommitFailedDustOutput implements Exception {} class TransactionCommitFailedDustOutputSendAll implements Exception {} class TransactionCommitFailedVoutNegative implements Exception {} + +class TransactionCommitFailedBIP68Final implements Exception {} + +class TransactionInputNotSupported implements Exception {} diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index dd2d9ca67..40c2109d1 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -31,6 +31,11 @@ class SyncedSyncStatus extends SyncStatus { double progress() => 1.0; } +class SyncronizingSyncStatus extends SyncStatus { + @override + double progress() => 0.0; +} + class NotConnectedSyncStatus extends SyncStatus { const NotConnectedSyncStatus(); @@ -43,10 +48,7 @@ class AttemptingSyncStatus extends SyncStatus { double progress() => 0.0; } -class FailedSyncStatus extends SyncStatus { - @override - double progress() => 1.0; -} +class FailedSyncStatus extends NotConnectedSyncStatus {} class ConnectingSyncStatus extends SyncStatus { @override @@ -58,21 +60,14 @@ class ConnectedSyncStatus extends SyncStatus { double progress() => 0.0; } -class UnsupportedSyncStatus extends SyncStatus { - @override - double progress() => 1.0; -} +class UnsupportedSyncStatus extends NotConnectedSyncStatus {} -class TimedOutSyncStatus extends SyncStatus { - @override - double progress() => 1.0; +class TimedOutSyncStatus extends NotConnectedSyncStatus { @override String toString() => 'Timed out'; } -class LostConnectionSyncStatus extends SyncStatus { - @override - double progress() => 1.0; +class LostConnectionSyncStatus extends NotConnectedSyncStatus { @override String toString() => 'Reconnecting'; } diff --git a/cw_core/lib/unspent_coins_info.dart b/cw_core/lib/unspent_coins_info.dart index 25abd3e48..ed09e17e0 100644 --- a/cw_core/lib/unspent_coins_info.dart +++ b/cw_core/lib/unspent_coins_info.dart @@ -16,7 +16,8 @@ class UnspentCoinsInfo extends HiveObject { required this.value, this.keyImage = null, this.isChange = false, - this.accountIndex = 0 + this.accountIndex = 0, + this.isSilentPayment = false, }); static const typeId = UNSPENT_COINS_INFO_TYPE_ID; @@ -49,13 +50,16 @@ class UnspentCoinsInfo extends HiveObject { @HiveField(8, defaultValue: null) String? keyImage; - + @HiveField(9, defaultValue: false) bool isChange; @HiveField(10, defaultValue: 0) int accountIndex; + @HiveField(11, defaultValue: false) + bool? isSilentPayment; + String get note => noteRaw ?? ''; set note(String value) => noteRaw = value; diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 8ee3e1687..e66da479b 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -187,7 +187,7 @@ class CWBitcoin extends Bitcoin { Future updateUnspents(Object wallet) async { final bitcoinWallet = wallet as ElectrumWallet; - await bitcoinWallet.updateUnspent(); + await bitcoinWallet.updateAllUnspents(); } WalletService createBitcoinWalletService( @@ -386,4 +386,10 @@ class CWBitcoin extends Bitcoin { final bitcoinWallet = wallet as ElectrumWallet; bitcoinWallet.walletAddresses.deleteSilentPaymentAddress(address); } + + @override + Future updateFeeRates(Object wallet) async { + final bitcoinWallet = wallet as ElectrumWallet; + await bitcoinWallet.updateFeeRates(); + } } diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index c743caf55..c43196337 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -44,5 +44,9 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_timed_out; } + if (syncStatus is SyncronizingSyncStatus) { + return S.current.sync_status_syncronizing; + } + return ''; } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 6033c24a6..792fd6f89 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -818,14 +818,16 @@ Future checkCurrentNodes( } if (currentBitcoinElectrumServer == null) { - final cakeWalletElectrum = Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin); + final cakeWalletElectrum = + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: true); await nodeSource.add(cakeWalletElectrum); await sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletElectrum.key as int); } if (currentLitecoinElectrumServer == null) { - final cakeWalletElectrum = Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin); + final cakeWalletElectrum = + Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin, useSSL: true); await nodeSource.add(cakeWalletElectrum); await sharedPreferences.setInt( PreferencesKey.currentLitecoinElectrumSererIdKey, cakeWalletElectrum.key as int); @@ -860,7 +862,8 @@ Future checkCurrentNodes( } if (currentBitcoinCashNodeServer == null) { - final node = Node(uri: cakeWalletBitcoinCashDefaultNodeUri, type: WalletType.bitcoinCash); + final node = + Node(uri: cakeWalletBitcoinCashDefaultNodeUri, type: WalletType.bitcoinCash, useSSL: true); await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, node.key as int); } @@ -888,7 +891,8 @@ Future resetBitcoinElectrumServer( .firstWhereOrNull((node) => node.uriRaw.toString() == cakeWalletBitcoinElectrumUri); if (cakeWalletNode == null) { - cakeWalletNode = Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin); + cakeWalletNode = + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: true); await nodeSource.add(cakeWalletNode); } diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index fd34b00ac..850c08209 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -146,7 +146,7 @@ class AddressCell extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ Text( - '${S.of(context).balance}: $txCount', + '${S.of(context).balance}: $balance', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 970bb31f2..38fe18443 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; @@ -400,6 +401,10 @@ class SendPage extends BasePage { return; } + if (sendViewModel.isElectrumWallet) { + bitcoin!.updateFeeRates(sendViewModel.wallet); + } + reaction((_) => sendViewModel.state, (ExecutionState state) { if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -456,10 +461,12 @@ class SendPage extends BasePage { sendViewModel.selectedCryptoCurrency.toString()); final waitMessage = sendViewModel.walletType == WalletType.solana - ? '. ${S.of(_dialogContext).waitFewSecondForTxUpdate}' : ''; + ? '. ${S.of(_dialogContext).waitFewSecondForTxUpdate}' + : ''; final newContactMessage = newContactAddress != null - ? '\n${S.of(_dialogContext).add_contact_to_address_book}' : ''; + ? '\n${S.of(_dialogContext).add_contact_to_address_book}' + : ''; final alertContent = "$successMessage$waitMessage$newContactMessage"; diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart index 70ae7ce3f..ee6d6dc73 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -57,6 +57,7 @@ class UnspentCoinsListFormState extends State { isSending: item.isSending, isFrozen: item.isFrozen, isChange: item.isChange, + isSilentPayment: item.isSilentPayment, onCheckBoxTap: item.isFrozen ? null : () async { diff --git a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart index e16026073..60a23c99b 100644 --- a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart +++ b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart @@ -12,6 +12,7 @@ class UnspentCoinsListItem extends StatelessWidget { required this.isSending, required this.isFrozen, required this.isChange, + required this.isSilentPayment, this.onCheckBoxTap, }); @@ -21,18 +22,16 @@ class UnspentCoinsListItem extends StatelessWidget { final bool isSending; final bool isFrozen; final bool isChange; + final bool isSilentPayment; final Function()? onCheckBoxTap; @override Widget build(BuildContext context) { final unselectedItemColor = Theme.of(context).cardColor; final selectedItemColor = Theme.of(context).primaryColor; - final itemColor = isSending - ? selectedItemColor - : unselectedItemColor; - final amountColor = isSending - ? Colors.white - : Theme.of(context).extension()!.buttonTextColor; + final itemColor = isSending ? selectedItemColor : unselectedItemColor; + final amountColor = + isSending ? Colors.white : Theme.of(context).extension()!.buttonTextColor; final addressColor = isSending ? Colors.white.withOpacity(0.5) : Theme.of(context).extension()!.buttonSecondaryTextColor; @@ -121,6 +120,23 @@ class UnspentCoinsListItem extends StatelessWidget { ), ), ), + if (isSilentPayment) + Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), + alignment: Alignment.center, + child: Text( + S.of(context).silent_payments, + style: TextStyle( + color: itemColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), + ), ], ), ), diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 038301db4..0bea0c59b 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -567,9 +567,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (error is TransactionCommitFailedVoutNegative) { return S.current.tx_rejected_vout_negative; } + if (error is TransactionCommitFailedBIP68Final) { + return S.current.tx_rejected_bip68_final; + } if (error is TransactionNoDustOnChangeException) { return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max); } + if (error is TransactionInputNotSupported) { + return S.current.tx_invalid_input; + } } return errorMessage; diff --git a/lib/view_model/unspent_coins/unspent_coins_item.dart b/lib/view_model/unspent_coins/unspent_coins_item.dart index bb5c4dd7b..4ca5a10a2 100644 --- a/lib/view_model/unspent_coins/unspent_coins_item.dart +++ b/lib/view_model/unspent_coins/unspent_coins_item.dart @@ -15,7 +15,8 @@ abstract class UnspentCoinsItemBase with Store { required this.isChange, required this.amountRaw, required this.vout, - required this.keyImage + required this.keyImage, + required this.isSilentPayment, }); @observable @@ -47,4 +48,7 @@ abstract class UnspentCoinsItemBase with Store { @observable String? keyImage; + + @observable + bool isSilentPayment; } diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 3b90aff41..bb04cfe5c 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -86,22 +86,32 @@ abstract class UnspentCoinsListViewModelBase with Store { @action void _updateUnspentCoinsInfo() { _items.clear(); - _items.addAll(_getUnspents().map((elem) { - final info = - getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); - return UnspentCoinsItem( - address: elem.address, - amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}', - hash: elem.hash, - isFrozen: info.isFrozen, - note: info.note, - isSending: info.isSending, - amountRaw: elem.value, - vout: elem.vout, - keyImage: elem.keyImage, - isChange: elem.isChange, - ); - })); + List unspents = []; + _getUnspents().forEach((elem) { + try { + final info = + getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); + + unspents.add(UnspentCoinsItem( + address: elem.address, + amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}', + hash: elem.hash, + isFrozen: info.isFrozen, + note: info.note, + isSending: info.isSending, + amountRaw: elem.value, + vout: elem.vout, + keyImage: elem.keyImage, + isChange: elem.isChange, + isSilentPayment: info.isSilentPayment ?? false, + )); + } catch (e, s) { + print(s); + print(e.toString()); + } + }); + + _items.addAll(unspents); } } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 94dcd609d..f1c7d64d2 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -316,8 +316,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (isElectrumWallet) { if (bitcoin!.hasSelectedSilentPayments(wallet)) { final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) { - // Silent Payments index 0 is change per BIP - final isPrimary = address.index == 1; + final isPrimary = address.index == 0; return WalletAddressListItem( id: address.index, @@ -335,11 +334,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final receivedAddressItems = bitcoin!.getSilentPaymentReceivedAddresses(wallet).map((address) { - final isPrimary = address.index == 0; - return WalletAddressListItem( id: address.index, - isPrimary: isPrimary, + isPrimary: false, name: address.name, address: address.address, txCount: address.txCount, diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index d7d68dfc9..1d1e2916a 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -745,8 +745,10 @@ "trusted": "موثوق به", "tx_commit_exception_no_dust_on_change": "يتم رفض المعاملة مع هذا المبلغ. باستخدام هذه العملات المعدنية ، يمكنك إرسال ${min} دون تغيير أو ${max} الذي يعيد التغيير.", "tx_commit_failed": "فشل ارتكاب المعاملة. يرجى الاتصال بالدعم.", + "tx_invalid_input": "أنت تستخدم نوع الإدخال الخاطئ لهذا النوع من الدفع", "tx_no_dust_exception": "يتم رفض المعاملة عن طريق إرسال مبلغ صغير جدًا. يرجى محاولة زيادة المبلغ.", "tx_not_enough_inputs_exception": "لا يكفي المدخلات المتاحة. الرجاء تحديد المزيد تحت التحكم في العملة", + "tx_rejected_bip68_final": "تحتوي المعاملة على مدخلات غير مؤكدة وفشلت في استبدال الرسوم.", "tx_rejected_dust_change": "المعاملة التي يتم رفضها بموجب قواعد الشبكة ، ومبلغ التغيير المنخفض (الغبار). حاول إرسال كل أو تقليل المبلغ.", "tx_rejected_dust_output": "المعاملة التي يتم رفضها بموجب قواعد الشبكة ، وكمية الإخراج المنخفض (الغبار). يرجى زيادة المبلغ.", "tx_rejected_dust_output_send_all": "المعاملة التي يتم رفضها بموجب قواعد الشبكة ، وكمية الإخراج المنخفض (الغبار). يرجى التحقق من رصيد العملات المعدنية المحددة تحت التحكم في العملة.", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 2bc6caa4a..261d4fbc8 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -745,8 +745,10 @@ "trusted": "Надежден", "tx_commit_exception_no_dust_on_change": "Сделката се отхвърля с тази сума. С тези монети можете да изпратите ${min} без промяна или ${max}, която връща промяна.", "tx_commit_failed": "Компетацията на транзакцията не успя. Моля, свържете се с поддръжката.", + "tx_invalid_input": "Използвате грешен тип вход за този тип плащане", "tx_no_dust_exception": "Сделката се отхвърля чрез изпращане на сума твърде малка. Моля, опитайте да увеличите сумата.", "tx_not_enough_inputs_exception": "Няма достатъчно налични входове. Моля, изберете повече под контрол на монети", + "tx_rejected_bip68_final": "Сделката има непотвърдени входове и не успя да се замени с такса.", "tx_rejected_dust_change": "Транзакция, отхвърлена от мрежови правила, ниска сума на промяна (прах). Опитайте да изпратите всички или да намалите сумата.", "tx_rejected_dust_output": "Транзакция, отхвърлена от мрежови правила, ниска стойност на изхода (прах). Моля, увеличете сумата.", "tx_rejected_dust_output_send_all": "Транзакция, отхвърлена от мрежови правила, ниска стойност на изхода (прах). Моля, проверете баланса на монетите, избрани под контрол на монети.", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 1ce8d1fcd..b368947c0 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -745,8 +745,10 @@ "trusted": "Důvěřovat", "tx_commit_exception_no_dust_on_change": "Transakce je zamítnuta s touto částkou. S těmito mincemi můžete odeslat ${min} bez změny nebo ${max}, které se vrátí změna.", "tx_commit_failed": "Transakce COMPORT selhala. Kontaktujte prosím podporu.", + "tx_invalid_input": "Pro tento typ platby používáte nesprávný typ vstupu", "tx_no_dust_exception": "Transakce je zamítnuta odesláním příliš malé. Zkuste prosím zvýšit částku.", "tx_not_enough_inputs_exception": "Není k dispozici dostatek vstupů. Vyberte prosím více pod kontrolou mincí", + "tx_rejected_bip68_final": "Transakce má nepotvrzené vstupy a nepodařilo se nahradit poplatkem.", "tx_rejected_dust_change": "Transakce zamítnuta podle síťových pravidel, množství nízké změny (prach). Zkuste odeslat vše nebo snížit částku.", "tx_rejected_dust_output": "Transakce zamítnuta síťovými pravidly, nízkým množstvím výstupu (prach). Zvyšte prosím částku.", "tx_rejected_dust_output_send_all": "Transakce zamítnuta síťovými pravidly, nízkým množstvím výstupu (prach). Zkontrolujte prosím zůstatek mincí vybraných pod kontrolou mincí.", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index b508e691e..05b6636f5 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -746,8 +746,10 @@ "trusted": "Vertrauenswürdige", "tx_commit_exception_no_dust_on_change": "Die Transaktion wird diesen Betrag abgelehnt. Mit diesen Münzen können Sie ${min} ohne Veränderung oder ${max} senden, die Änderungen zurückgeben.", "tx_commit_failed": "Transaktionsausschüsse ist fehlgeschlagen. Bitte wenden Sie sich an Support.", + "tx_invalid_input": "Sie verwenden den falschen Eingangstyp für diese Art von Zahlung", "tx_no_dust_exception": "Die Transaktion wird abgelehnt, indem eine Menge zu klein gesendet wird. Bitte versuchen Sie, die Menge zu erhöhen.", "tx_not_enough_inputs_exception": "Nicht genügend Eingänge verfügbar. Bitte wählen Sie mehr unter Münzkontrolle aus", + "tx_rejected_bip68_final": "Die Transaktion hat unbestätigte Inputs und konnte nicht durch Gebühr ersetzt werden.", "tx_rejected_dust_change": "Transaktion abgelehnt durch Netzwerkregeln, niedriger Änderungsbetrag (Staub). Versuchen Sie, alle zu senden oder die Menge zu reduzieren.", "tx_rejected_dust_output": "Transaktion durch Netzwerkregeln, niedriger Ausgangsmenge (Staub) abgelehnt. Bitte erhöhen Sie den Betrag.", "tx_rejected_dust_output_send_all": "Transaktion durch Netzwerkregeln, niedriger Ausgangsmenge (Staub) abgelehnt. Bitte überprüfen Sie den Gleichgewicht der unter Münzkontrolle ausgewählten Münzen.", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 8110181bb..763558325 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -745,8 +745,10 @@ "trusted": "Trusted", "tx_commit_exception_no_dust_on_change": "The transaction is rejected with this amount. With these coins you can send ${min} without change or ${max} that returns change.", "tx_commit_failed": "Transaction commit failed. Please contact support.", + "tx_invalid_input": "You are using the wrong input type for this type of payment", "tx_no_dust_exception": "The transaction is rejected by sending an amount too small. Please try increasing the amount.", "tx_not_enough_inputs_exception": "Not enough inputs available. Please select more under Coin Control", + "tx_rejected_bip68_final": "Transaction has unconfirmed inputs and failed to replace by fee.", "tx_rejected_dust_change": "Transaction rejected by network rules, low change amount (dust). Try sending ALL or reducing the amount.", "tx_rejected_dust_output": "Transaction rejected by network rules, low output amount (dust). Please increase the amount.", "tx_rejected_dust_output_send_all": "Transaction rejected by network rules, low output amount (dust). Please check the balance of coins selected under Coin Control.", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index b29eef59c..71b7ad423 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -746,8 +746,10 @@ "trusted": "de confianza", "tx_commit_exception_no_dust_on_change": "La transacción se rechaza con esta cantidad. Con estas monedas puede enviar ${min} sin cambios o ${max} que devuelve el cambio.", "tx_commit_failed": "La confirmación de transacción falló. Póngase en contacto con el soporte.", + "tx_invalid_input": "Está utilizando el tipo de entrada incorrecto para este tipo de pago", "tx_no_dust_exception": "La transacción se rechaza enviando una cantidad demasiado pequeña. Intente aumentar la cantidad.", "tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Seleccione más bajo control de monedas", + "tx_rejected_bip68_final": "La transacción tiene entradas no confirmadas y no ha podido reemplazar por tarifa.", "tx_rejected_dust_change": "Transacción rechazada por reglas de red, bajo cambio de cambio (polvo). Intente enviar todo o reducir la cantidad.", "tx_rejected_dust_output": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Aumente la cantidad.", "tx_rejected_dust_output_send_all": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Verifique el saldo de monedas seleccionadas bajo control de monedas.", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index f2b30a8e4..a03411602 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -745,8 +745,10 @@ "trusted": "de confiance", "tx_commit_exception_no_dust_on_change": "La transaction est rejetée avec ce montant. Avec ces pièces, vous pouvez envoyer ${min} sans changement ou ${max} qui renvoie le changement.", "tx_commit_failed": "La validation de la transaction a échoué. Veuillez contacter l'assistance.", + "tx_invalid_input": "Vous utilisez le mauvais type d'entrée pour ce type de paiement", "tx_no_dust_exception": "La transaction est rejetée en envoyant un montant trop faible. Veuillez essayer d'augmenter le montant.", "tx_not_enough_inputs_exception": "Pas assez d'entrées disponibles. Veuillez sélectionner plus sous Control Control", + "tx_rejected_bip68_final": "La transaction a des entrées non confirmées et n'a pas réussi à remplacer par les frais.", "tx_rejected_dust_change": "Transaction rejetée par les règles du réseau, montant de faible variation (poussière). Essayez d'envoyer tout ou de réduire le montant.", "tx_rejected_dust_output": "Transaction rejetée par les règles du réseau, faible quantité de sortie (poussière). Veuillez augmenter le montant.", "tx_rejected_dust_output_send_all": "Transaction rejetée par les règles du réseau, faible quantité de sortie (poussière). Veuillez vérifier le solde des pièces sélectionnées sous le contrôle des pièces de monnaie.", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 09365a462..36e0f8a3d 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -747,8 +747,10 @@ "trusted": "Amintacce", "tx_commit_exception_no_dust_on_change": "An ƙi ma'amala da wannan adadin. Tare da waɗannan tsabar kudi Zaka iya aika ${min}, ba tare da canji ba ko ${max} wanda ya dawo canzawa.", "tx_commit_failed": "Ma'amala ya kasa. Da fatan za a tuntuɓi goyan baya.", + "tx_invalid_input": "Kuna amfani da nau'in shigar da ba daidai ba don wannan nau'in biyan kuɗi", "tx_no_dust_exception": "An ƙi ma'amala ta hanyar aika adadin ƙarami. Da fatan za a gwada ƙara adadin.", "tx_not_enough_inputs_exception": "Bai isa ba hanyoyin da ake samu. Da fatan za selectiari a karkashin Kwarewar Coin", + "tx_rejected_bip68_final": "Ma'amala tana da abubuwan da basu dace ba kuma sun kasa maye gurbin ta.", "tx_rejected_dust_change": "Ma'amala ta ƙi ta dokokin cibiyar sadarwa, ƙarancin canji (ƙura). Gwada aikawa da duka ko rage adadin.", "tx_rejected_dust_output": "Ma'adar da aka ƙi ta dokokin cibiyar sadarwa, ƙananan fitarwa (ƙura). Da fatan za a ƙara adadin.", "tx_rejected_dust_output_send_all": "Ma'adar da aka ƙi ta dokokin cibiyar sadarwa, ƙananan fitarwa (ƙura). Da fatan za a duba daidaiton tsabar kudi a ƙarƙashin ikon tsabar kudin.", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 4472b0aae..3bfe67695 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -747,8 +747,10 @@ "trusted": "भरोसा", "tx_commit_exception_no_dust_on_change": "लेनदेन को इस राशि से खारिज कर दिया जाता है। इन सिक्कों के साथ आप चेंज या ${min} के बिना ${max} को भेज सकते हैं जो परिवर्तन लौटाता है।", "tx_commit_failed": "लेन -देन प्रतिबद्ध विफल। कृपया संपर्क समर्थन करें।", + "tx_invalid_input": "आप इस प्रकार के भुगतान के लिए गलत इनपुट प्रकार का उपयोग कर रहे हैं", "tx_no_dust_exception": "लेनदेन को बहुत छोटी राशि भेजकर अस्वीकार कर दिया जाता है। कृपया राशि बढ़ाने का प्रयास करें।", "tx_not_enough_inputs_exception": "पर्याप्त इनपुट उपलब्ध नहीं है। कृपया सिक्का नियंत्रण के तहत अधिक चुनें", + "tx_rejected_bip68_final": "लेन -देन में अपुष्ट इनपुट हैं और शुल्क द्वारा प्रतिस्थापित करने में विफल रहे हैं।", "tx_rejected_dust_change": "नेटवर्क नियमों, कम परिवर्तन राशि (धूल) द्वारा खारिज किए गए लेनदेन। सभी भेजने या राशि को कम करने का प्रयास करें।", "tx_rejected_dust_output": "नेटवर्क नियमों, कम आउटपुट राशि (धूल) द्वारा खारिज किए गए लेनदेन। कृपया राशि बढ़ाएं।", "tx_rejected_dust_output_send_all": "नेटवर्क नियमों, कम आउटपुट राशि (धूल) द्वारा खारिज किए गए लेनदेन। कृपया सिक्का नियंत्रण के तहत चुने गए सिक्कों के संतुलन की जाँच करें।", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index fd305cf50..ad52db1fa 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -745,8 +745,10 @@ "trusted": "vjerovao", "tx_commit_exception_no_dust_on_change": "Transakcija se odbija s tim iznosom. Pomoću ovih kovanica možete poslati ${min} bez promjene ili ${max} koja vraća promjenu.", "tx_commit_failed": "Obveza transakcije nije uspjela. Molimo kontaktirajte podršku.", + "tx_invalid_input": "Koristite pogrešnu vrstu ulaza za ovu vrstu plaćanja", "tx_no_dust_exception": "Transakcija se odbija slanjem iznosa premalo. Pokušajte povećati iznos.", "tx_not_enough_inputs_exception": "Nema dovoljno unosa. Molimo odaberite više pod kontrolom novčića", + "tx_rejected_bip68_final": "Transakcija ima nepotvrđene unose i nije zamijenila naknadom.", "tx_rejected_dust_change": "Transakcija odbijena mrežnim pravilima, niska količina promjene (prašina). Pokušajte poslati sve ili smanjiti iznos.", "tx_rejected_dust_output": "Transakcija odbijena mrežnim pravilima, niska količina izlaza (prašina). Molimo povećajte iznos.", "tx_rejected_dust_output_send_all": "Transakcija odbijena mrežnim pravilima, niska količina izlaza (prašina). Molimo provjerite ravnotežu kovanica odabranih pod kontrolom novčića.", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index dd5acfed7..2db131a1a 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -748,8 +748,10 @@ "trusted": "Dipercayai", "tx_commit_exception_no_dust_on_change": "Transaksi ditolak dengan jumlah ini. Dengan koin ini Anda dapat mengirim ${min} tanpa perubahan atau ${max} yang mengembalikan perubahan.", "tx_commit_failed": "Transaksi Gagal. Silakan hubungi Dukungan.", + "tx_invalid_input": "Anda menggunakan jenis input yang salah untuk jenis pembayaran ini", "tx_no_dust_exception": "Transaksi ditolak dengan mengirimkan jumlah yang terlalu kecil. Silakan coba tingkatkan jumlahnya.", "tx_not_enough_inputs_exception": "Tidak cukup input yang tersedia. Pilih lebih banyak lagi di bawah Kontrol Koin", + "tx_rejected_bip68_final": "Transaksi memiliki input yang belum dikonfirmasi dan gagal mengganti dengan biaya.", "tx_rejected_dust_change": "Transaksi ditolak oleh aturan jaringan, jumlah perubahan rendah (debu). Coba kirim semua atau mengurangi jumlahnya.", "tx_rejected_dust_output": "Transaksi ditolak oleh aturan jaringan, jumlah output rendah (debu). Harap tingkatkan jumlahnya.", "tx_rejected_dust_output_send_all": "Transaksi ditolak oleh aturan jaringan, jumlah output rendah (debu). Silakan periksa saldo koin yang dipilih di bawah kontrol koin.", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 9d88e979d..e66bceff5 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -747,8 +747,10 @@ "trusted": "di fiducia", "tx_commit_exception_no_dust_on_change": "La transazione viene respinta con questo importo. Con queste monete è possibile inviare ${min} senza modifiche o ${max} che restituisce il cambiamento.", "tx_commit_failed": "Commit di transazione non riuscita. Si prega di contattare il supporto.", + "tx_invalid_input": "Stai usando il tipo di input sbagliato per questo tipo di pagamento", "tx_no_dust_exception": "La transazione viene respinta inviando un importo troppo piccolo. Per favore, prova ad aumentare l'importo.", "tx_not_enough_inputs_exception": "Input non sufficienti disponibili. Seleziona di più sotto il controllo delle monete", + "tx_rejected_bip68_final": "La transazione ha input non confermati e non è stata sostituita per tassa.", "tx_rejected_dust_change": "Transazione respinta dalle regole di rete, quantità bassa variazione (polvere). Prova a inviare tutto o ridurre l'importo.", "tx_rejected_dust_output": "Transazione respinta dalle regole di rete, bassa quantità di output (polvere). Si prega di aumentare l'importo.", "tx_rejected_dust_output_send_all": "Transazione respinta dalle regole di rete, bassa quantità di output (polvere). Si prega di controllare il saldo delle monete selezionate sotto controllo delle monete.", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index d11c46851..192ea09be 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -746,8 +746,10 @@ "trusted": "信頼できる", "tx_commit_exception_no_dust_on_change": "この金額ではトランザクションは拒否されます。 これらのコインを使用すると、おつりなしの ${min} またはおつりを返す ${max} を送信できます。", "tx_commit_failed": "トランザクションコミットは失敗しました。サポートに連絡してください。", + "tx_invalid_input": "このタイプの支払いに間違った入力タイプを使用しています", "tx_no_dust_exception": "トランザクションは、小さすぎる金額を送信することにより拒否されます。量を増やしてみてください。", "tx_not_enough_inputs_exception": "利用可能な入力が十分ではありません。コイン制御下でもっと選択してください", + "tx_rejected_bip68_final": "トランザクションには未確認の入力があり、料金で交換できませんでした。", "tx_rejected_dust_change": "ネットワークルール、低い変更量(ほこり)によって拒否されたトランザクション。すべてを送信するか、金額を減らしてみてください。", "tx_rejected_dust_output": "ネットワークルール、低出力量(ダスト)によって拒否されたトランザクション。金額を増やしてください。", "tx_rejected_dust_output_send_all": "ネットワークルール、低出力量(ダスト)によって拒否されたトランザクション。コイン管理下で選択されたコインのバランスを確認してください。", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index f0ab69a8d..ef8424c16 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -746,8 +746,10 @@ "trusted": "신뢰할 수 있는", "tx_commit_exception_no_dust_on_change": "이 금액으로 거래가 거부되었습니다. 이 코인을 사용하면 거스름돈 없이 ${min}를 보내거나 거스름돈을 반환하는 ${max}를 보낼 수 있습니다.", "tx_commit_failed": "거래 커밋이 실패했습니다. 지원에 연락하십시오.", + "tx_invalid_input": "이 유형의 지불에 잘못 입력 유형을 사용하고 있습니다.", "tx_no_dust_exception": "너무 작은 금액을 보내면 거래가 거부됩니다. 금액을 늘리십시오.", "tx_not_enough_inputs_exception": "사용 가능한 입력이 충분하지 않습니다. 코인 컨트롤에서 더 많은 것을 선택하십시오", + "tx_rejected_bip68_final": "거래는 확인되지 않은 입력을 받았으며 수수료로 교체하지 못했습니다.", "tx_rejected_dust_change": "네트워크 규칙, 낮은 변경 금액 (먼지)에 의해 거부 된 거래. 전부를 보내거나 금액을 줄이십시오.", "tx_rejected_dust_output": "네트워크 규칙, 낮은 출력 금액 (먼지)에 의해 거부 된 거래. 금액을 늘리십시오.", "tx_rejected_dust_output_send_all": "네트워크 규칙, 낮은 출력 금액 (먼지)에 의해 거부 된 거래. 동전 제어에서 선택한 동전의 균형을 확인하십시오.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index fcc482bae..b64f2ebd1 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -745,8 +745,10 @@ "trusted": "ယုံတယ်။", "tx_commit_exception_no_dust_on_change": "အဆိုပါငွေပေးငွေယူကဒီပမာဏနှင့်အတူပယ်ချခံရသည်။ ဤဒင်္ဂါးပြားများနှင့်အတူပြောင်းလဲမှုကိုပြန်လည်ပြောင်းလဲခြင်းသို့မဟုတ် ${min} မပါဘဲ ${max} ပေးပို့နိုင်သည်။", "tx_commit_failed": "ငွေပေးငွေယူကျူးလွန်မှုပျက်ကွက်။ ကျေးဇူးပြုပြီးပံ့ပိုးမှုဆက်သွယ်ပါ။", + "tx_invalid_input": "သင်သည်ဤငွေပေးချေမှုအမျိုးအစားအတွက်မှားယွင်းသော input type ကိုအသုံးပြုနေသည်", "tx_no_dust_exception": "ငွေပမာဏကိုသေးငယ်လွန်းသောငွေပမာဏကိုပေးပို့ခြင်းဖြင့်ပယ်ဖျက်ခြင်းကိုငြင်းပယ်သည်။ ကျေးဇူးပြုပြီးငွေပမာဏကိုတိုးမြှင့်ကြိုးစားပါ။", "tx_not_enough_inputs_exception": "အလုံအလောက်သွင်းအားစုများမလုံလောက်။ ကျေးဇူးပြုပြီးဒင်္ဂါးပြားထိန်းချုပ်မှုအောက်တွင်ပိုမိုရွေးချယ်ပါ", + "tx_rejected_bip68_final": "ငွေပေးငွေယူသည်အတည်မပြုရသေးသောသွင်းအားစုများရှိပြီးအခကြေးငွေဖြင့်အစားထိုးရန်ပျက်ကွက်ခဲ့သည်။", "tx_rejected_dust_change": "Network စည်းမျဉ်းစည်းကမ်းများဖြင့်ပယ်ဖျက်ခြင်းသည် Network စည်းမျဉ်းစည်းကမ်းများဖြင့်ငြင်းပယ်ခြင်း, အားလုံးပေးပို့ခြင်းသို့မဟုတ်ငွေပမာဏကိုလျှော့ချကြိုးစားပါ။", "tx_rejected_dust_output": "Network စည်းမျဉ်းစည်းကမ်းများဖြင့် ပယ်ချ. ငွေပေးချေမှုသည် output output (ဖုန်မှုန့်) ဖြင့်ပယ်ချခဲ့သည်။ ကျေးဇူးပြုပြီးငွေပမာဏကိုတိုးမြှင့်ပေးပါ။", "tx_rejected_dust_output_send_all": "Network စည်းမျဉ်းစည်းကမ်းများဖြင့် ပယ်ချ. ငွေပေးချေမှုသည် output output (ဖုန်မှုန့်) ဖြင့်ပယ်ချခဲ့သည်။ ဒင်္ဂါးပြားထိန်းချုပ်မှုအောက်တွင်ရွေးချယ်ထားသောဒင်္ဂါးများ၏လက်ကျန်ငွေကိုစစ်ဆေးပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index c0220bd80..b49bdc4da 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -745,8 +745,10 @@ "trusted": "vertrouwd", "tx_commit_exception_no_dust_on_change": "De transactie wordt afgewezen met dit bedrag. Met deze munten kunt u ${min} verzenden zonder verandering of ${max} die wijziging retourneert.", "tx_commit_failed": "Transactiebewissing is mislukt. Neem contact op met de ondersteuning.", + "tx_invalid_input": "U gebruikt het verkeerde invoertype voor dit type betaling", "tx_no_dust_exception": "De transactie wordt afgewezen door een te klein bedrag te verzenden. Probeer het bedrag te verhogen.", "tx_not_enough_inputs_exception": "Niet genoeg ingangen beschikbaar. Selecteer meer onder muntenbesturing", + "tx_rejected_bip68_final": "Transactie heeft onbevestigde ingangen en niet vervangen door vergoeding.", "tx_rejected_dust_change": "Transactie afgewezen door netwerkregels, laag wijzigingsbedrag (stof). Probeer alles te verzenden of het bedrag te verminderen.", "tx_rejected_dust_output": "Transactie afgewezen door netwerkregels, laag outputbedrag (stof). Verhoog het bedrag.", "tx_rejected_dust_output_send_all": "Transactie afgewezen door netwerkregels, laag outputbedrag (stof). Controleer het saldo van munten die zijn geselecteerd onder muntcontrole.", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index d92a193e7..dee8d0fdd 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -745,8 +745,10 @@ "trusted": "Zaufany", "tx_commit_exception_no_dust_on_change": "Transakcja jest odrzucana z tą kwotą. Za pomocą tych monet możesz wysłać ${min} bez zmiany lub ${max}, które zwraca zmianę.", "tx_commit_failed": "Zatwierdzenie transakcji nie powiodło się. Skontaktuj się z obsługą.", + "tx_invalid_input": "Używasz niewłaściwego typu wejściowego dla tego rodzaju płatności", "tx_no_dust_exception": "Transakcja jest odrzucana przez wysyłanie zbyt małej ilości. Spróbuj zwiększyć kwotę.", "tx_not_enough_inputs_exception": "Za mało dostępnych danych wejściowych. Wybierz więcej pod kontrolą monet", + "tx_rejected_bip68_final": "Transakcja niepotwierdza wejściów i nie zastąpiła opłaty.", "tx_rejected_dust_change": "Transakcja odrzucona według reguł sieciowych, niska ilość zmiany (kurz). Spróbuj wysłać całość lub zmniejszyć kwotę.", "tx_rejected_dust_output": "Transakcja odrzucona według reguł sieciowych, niskiej ilości wyjściowej (pyłu). Zwiększ kwotę.", "tx_rejected_dust_output_send_all": "Transakcja odrzucona według reguł sieciowych, niskiej ilości wyjściowej (pyłu). Sprawdź saldo monet wybranych pod kontrolą monet.", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index cf23b4b11..0bf4e4422 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -747,8 +747,10 @@ "trusted": "confiável", "tx_commit_exception_no_dust_on_change": "A transação é rejeitada com esse valor. Com essas moedas, você pode enviar ${min} sem alteração ou ${max} que retorna alterações.", "tx_commit_failed": "A confirmação da transação falhou. Entre em contato com o suporte.", + "tx_invalid_input": "Você está usando o tipo de entrada errado para este tipo de pagamento", "tx_no_dust_exception": "A transação é rejeitada enviando uma quantia pequena demais. Por favor, tente aumentar o valor.", "tx_not_enough_inputs_exception": "Não há entradas disponíveis. Selecione mais sob controle de moedas", + "tx_rejected_bip68_final": "A transação tem entradas não confirmadas e não substituiu por taxa.", "tx_rejected_dust_change": "Transação rejeitada pelas regras de rede, baixa quantidade de troco (poeira). Tente enviar tudo ou reduzir o valor.", "tx_rejected_dust_output": "Transação rejeitada por regras de rede, baixa quantidade de saída (poeira). Por favor, aumente o valor.", "tx_rejected_dust_output_send_all": "Transação rejeitada por regras de rede, baixa quantidade de saída (poeira). Por favor, verifique o saldo de moedas selecionadas sob controle de moedas.", @@ -760,7 +762,7 @@ "unconfirmed": "Saldo não confirmado", "understand": "Entendo", "unmatched_currencies": "A moeda da sua carteira atual não corresponde à do QR digitalizado", - "unspent_change": "Mudar", + "unspent_change": "Troco", "unspent_coins_details_title": "Detalhes de moedas não gastas", "unspent_coins_title": "Moedas não gastas", "unsupported_asset": "Não oferecemos suporte a esta ação para este recurso. Crie ou mude para uma carteira de um tipo de ativo compatível.", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index cccd72590..006ec2a7e 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -746,8 +746,10 @@ "trusted": "доверенный", "tx_commit_exception_no_dust_on_change": "Транзакция отклоняется с этой суммой. С этими монетами вы можете отправлять ${min} без изменения или ${max}, которые возвращают изменение.", "tx_commit_failed": "Комплект транзакции не удался. Пожалуйста, свяжитесь с поддержкой.", + "tx_invalid_input": "Вы используете неправильный тип ввода для этого типа оплаты", "tx_no_dust_exception": "Транзакция отклоняется путем отправки слишком маленькой суммы. Пожалуйста, попробуйте увеличить сумму.", "tx_not_enough_inputs_exception": "Недостаточно входов доступны. Пожалуйста, выберите больше под контролем монет", + "tx_rejected_bip68_final": "Транзакция имеет неподтвержденные входные данные и не смогли заменить на плату.", "tx_rejected_dust_change": "Транзакция отклоняется в соответствии с правилами сети, низкой суммой изменений (пыль). Попробуйте отправить все или уменьшить сумму.", "tx_rejected_dust_output": "Транзакция отклоняется в соответствии с правилами сети, низкой выходной суммой (пыль). Пожалуйста, увеличьте сумму.", "tx_rejected_dust_output_send_all": "Транзакция отклоняется в соответствии с правилами сети, низкой выходной суммой (пыль). Пожалуйста, проверьте баланс монет, выбранных под контролем монет.", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index dbf461990..a5d755205 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -745,8 +745,10 @@ "trusted": "มั่นคง", "tx_commit_exception_no_dust_on_change": "ธุรกรรมถูกปฏิเสธด้วยจำนวนเงินนี้ ด้วยเหรียญเหล่านี้คุณสามารถส่ง ${min} โดยไม่ต้องเปลี่ยนแปลงหรือ ${max} ที่ส่งคืนการเปลี่ยนแปลง", "tx_commit_failed": "การทำธุรกรรมล้มเหลว กรุณาติดต่อฝ่ายสนับสนุน", + "tx_invalid_input": "คุณกำลังใช้ประเภทอินพุตที่ไม่ถูกต้องสำหรับการชำระเงินประเภทนี้", "tx_no_dust_exception": "การทำธุรกรรมถูกปฏิเสธโดยการส่งจำนวนน้อยเกินไป โปรดลองเพิ่มจำนวนเงิน", "tx_not_enough_inputs_exception": "มีอินพุตไม่เพียงพอ โปรดเลือกเพิ่มเติมภายใต้การควบคุมเหรียญ", + "tx_rejected_bip68_final": "การทำธุรกรรมมีอินพุตที่ไม่ได้รับการยืนยันและไม่สามารถแทนที่ด้วยค่าธรรมเนียม", "tx_rejected_dust_change": "ธุรกรรมถูกปฏิเสธโดยกฎเครือข่ายจำนวนการเปลี่ยนแปลงต่ำ (ฝุ่น) ลองส่งทั้งหมดหรือลดจำนวนเงิน", "tx_rejected_dust_output": "การทำธุรกรรมถูกปฏิเสธโดยกฎเครือข่ายจำนวนเอาต์พุตต่ำ (ฝุ่น) โปรดเพิ่มจำนวนเงิน", "tx_rejected_dust_output_send_all": "การทำธุรกรรมถูกปฏิเสธโดยกฎเครือข่ายจำนวนเอาต์พุตต่ำ (ฝุ่น) โปรดตรวจสอบยอดคงเหลือของเหรียญที่เลือกภายใต้การควบคุมเหรียญ", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 904942bff..49ea9d842 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -745,8 +745,10 @@ "trusted": "Pinagkakatiwalaan", "tx_commit_exception_no_dust_on_change": "Ang transaksyon ay tinanggihan sa halagang ito. Sa mga barya na ito maaari kang magpadala ng ${min} nang walang pagbabago o ${max} na nagbabalik ng pagbabago.", "tx_commit_failed": "Nabigo ang transaksyon sa transaksyon. Mangyaring makipag -ugnay sa suporta.", + "tx_invalid_input": "Gumagamit ka ng maling uri ng pag -input para sa ganitong uri ng pagbabayad", "tx_no_dust_exception": "Ang transaksyon ay tinanggihan sa pamamagitan ng pagpapadala ng isang maliit na maliit. Mangyaring subukang dagdagan ang halaga.", "tx_not_enough_inputs_exception": "Hindi sapat na magagamit ang mga input. Mangyaring pumili ng higit pa sa ilalim ng control ng barya", + "tx_rejected_bip68_final": "Ang transaksyon ay hindi nakumpirma na mga input at nabigo na palitan ng bayad.", "tx_rejected_dust_change": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng pagbabago (alikabok). Subukang ipadala ang lahat o bawasan ang halaga.", "tx_rejected_dust_output": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng output (alikabok). Mangyaring dagdagan ang halaga.", "tx_rejected_dust_output_send_all": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng output (alikabok). Mangyaring suriin ang balanse ng mga barya na napili sa ilalim ng kontrol ng barya.", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index d4cb23770..27e281cb1 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -745,8 +745,10 @@ "trusted": "Güvenilir", "tx_commit_exception_no_dust_on_change": "İşlem bu miktarla reddedilir. Bu madeni paralarla değişiklik yapmadan ${min} veya değişikliği döndüren ${max} gönderebilirsiniz.", "tx_commit_failed": "İşlem taahhüdü başarısız oldu. Lütfen Destek ile iletişime geçin.", + "tx_invalid_input": "Bu tür ödeme için yanlış giriş türünü kullanıyorsunuz", "tx_no_dust_exception": "İşlem, çok küçük bir miktar gönderilerek reddedilir. Lütfen miktarı artırmayı deneyin.", "tx_not_enough_inputs_exception": "Yeterli giriş yok. Lütfen madeni para kontrolü altında daha fazlasını seçin", + "tx_rejected_bip68_final": "İşlemin doğrulanmamış girdileri var ve ücrete göre değiştirilemedi.", "tx_rejected_dust_change": "Ağ kurallarına göre reddedilen işlem, düşük değişim miktarı (toz). Tümünü göndermeyi veya miktarı azaltmayı deneyin.", "tx_rejected_dust_output": "Ağ kurallarına göre reddedilen işlem, düşük çıktı miktarı (toz). Lütfen miktarı artırın.", "tx_rejected_dust_output_send_all": "Ağ kurallarına göre reddedilen işlem, düşük çıktı miktarı (toz). Lütfen madeni para kontrolü altında seçilen madeni para dengesini kontrol edin.", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 830027c91..ad41e030f 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -746,8 +746,10 @@ "trusted": "довіряють", "tx_commit_exception_no_dust_on_change": "Транзакція відхилена цією сумою. За допомогою цих монет ви можете надіслати ${min} без змін або ${max}, що повертає зміни.", "tx_commit_failed": "Транзакційна комісія не вдалося. Будь ласка, зв'яжіться з підтримкою.", + "tx_invalid_input": "Ви використовуєте неправильний тип введення для цього типу оплати", "tx_no_dust_exception": "Угода відхиляється, відправивши суму занадто мала. Будь ласка, спробуйте збільшити суму.", "tx_not_enough_inputs_exception": "Недостатньо доступних входів. Виберіть більше під контролем монети", + "tx_rejected_bip68_final": "Трансакція має непідтверджені входи і не замінила плату.", "tx_rejected_dust_change": "Транзакція відхилена за допомогою мережевих правил, низька кількість змін (пил). Спробуйте надіслати все або зменшити суму.", "tx_rejected_dust_output": "Транзакція відхилена за допомогою мережевих правил, низька кількість вихідної кількості (пил). Будь ласка, збільшуйте суму.", "tx_rejected_dust_output_send_all": "Транзакція відхилена за допомогою мережевих правил, низька кількість вихідної кількості (пил). Будь ласка, перевірте баланс монет, вибраних під контролем монет.", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 97e8ba458..12bd1ccfe 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -747,8 +747,10 @@ "trusted": "قابل اعتماد", "tx_commit_exception_no_dust_on_change": "اس رقم سے لین دین کو مسترد کردیا گیا ہے۔ ان سککوں کے ذریعہ آپ بغیر کسی تبدیلی کے ${min} یا ${max} بھیج سکتے ہیں جو لوٹتے ہیں۔", "tx_commit_failed": "ٹرانزیکشن کمٹ ناکام ہوگیا۔ براہ کرم سپورٹ سے رابطہ کریں۔", + "tx_invalid_input": "آپ اس قسم کی ادائیگی کے لئے غلط ان پٹ کی قسم استعمال کررہے ہیں", "tx_no_dust_exception": "لین دین کو بہت چھوٹی رقم بھیج کر مسترد کردیا جاتا ہے۔ براہ کرم رقم میں اضافہ کرنے کی کوشش کریں۔", "tx_not_enough_inputs_exception": "کافی ان پٹ دستیاب نہیں ہے۔ براہ کرم سکے کے کنٹرول میں مزید منتخب کریں", + "tx_rejected_bip68_final": "لین دین میں غیر مصدقہ آدانوں کی ہے اور وہ فیس کے ذریعہ تبدیل کرنے میں ناکام رہا ہے۔", "tx_rejected_dust_change": "نیٹ ورک کے قواعد ، کم تبدیلی کی رقم (دھول) کے ذریعہ لین دین کو مسترد کردیا گیا۔ سب کو بھیجنے یا رقم کو کم کرنے کی کوشش کریں۔", "tx_rejected_dust_output": "لین دین کو نیٹ ورک کے قواعد ، کم آؤٹ پٹ رقم (دھول) کے ذریعہ مسترد کردیا گیا۔ براہ کرم رقم میں اضافہ کریں۔", "tx_rejected_dust_output_send_all": "لین دین کو نیٹ ورک کے قواعد ، کم آؤٹ پٹ رقم (دھول) کے ذریعہ مسترد کردیا گیا۔ براہ کرم سکے کے کنٹرول میں منتخب کردہ سکے کا توازن چیک کریں۔", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 6b268a8c1..20e415b26 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -746,8 +746,10 @@ "trusted": "A ti fọkàn ẹ̀ tán", "tx_commit_exception_no_dust_on_change": "Iṣowo naa ti kọ pẹlu iye yii. Pẹlu awọn owó wọnyi o le firanṣẹ ${min} laisi ayipada tabi ${max} ni iyipada iyipada.", "tx_commit_failed": "Idunadura iṣowo kuna. Jọwọ kan si atilẹyin.", + "tx_invalid_input": "O nlo iru titẹ nkan ti ko tọ fun iru isanwo yii", "tx_no_dust_exception": "Iṣowo naa ni kọ nipa fifiranṣẹ iye ti o kere ju. Jọwọ gbiyanju pọ si iye naa.", "tx_not_enough_inputs_exception": "Ko to awọn titẹsi to. Jọwọ yan diẹ sii labẹ iṣakoso owo", + "tx_rejected_bip68_final": "Iṣowo ni awọn igbewọle gbangba ati kuna lati rọpo nipasẹ owo.", "tx_rejected_dust_change": "Idunadura kọ nipasẹ awọn ofin nẹtiwọọki, iye iyipada kekere (eruku). Gbiyanju lati firanṣẹ gbogbo rẹ tabi dinku iye.", "tx_rejected_dust_output": "Idunadura kọ nipasẹ awọn ofin nẹtiwọọki, iye ti o wuwe kekere (eruku). Jọwọ mu iye naa pọ si.", "tx_rejected_dust_output_send_all": "Idunadura kọ nipasẹ awọn ofin nẹtiwọọki, iye ti o wuwe kekere (eruku). Jọwọ ṣayẹwo dọgbadọgba ti awọn owo ti a yan labẹ iṣakoso owo.", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index c847fc8b9..c38ddbe76 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -745,8 +745,10 @@ "trusted": "值得信赖", "tx_commit_exception_no_dust_on_change": "交易被此金额拒绝。使用这些硬币,您可以发送${min}无需更改或返回${max}的变化。", "tx_commit_failed": "交易承诺失败。请联系支持。", + "tx_invalid_input": "您正在使用错误的输入类型进行此类付款", "tx_no_dust_exception": "通过发送太小的金额来拒绝交易。请尝试增加金额。", "tx_not_enough_inputs_exception": "没有足够的输入。请在硬币控制下选择更多", + "tx_rejected_bip68_final": "交易未确认投入,未能取代费用。", "tx_rejected_dust_change": "交易被网络规则拒绝,较低的变化数量(灰尘)。尝试发送全部或减少金额。", "tx_rejected_dust_output": "交易被网络规则,低输出量(灰尘)拒绝。请增加金额。", "tx_rejected_dust_output_send_all": "交易被网络规则,低输出量(灰尘)拒绝。请检查在硬币控制下选择的硬币的余额。", diff --git a/tool/configure.dart b/tool/configure.dart index 70a74123c..4ac467811 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -173,6 +173,7 @@ abstract class Bitcoin { Future rescan(Object wallet, {required int height, bool? doSingleScan}); bool getNodeIsCakeElectrs(Object wallet); void deleteSilentPaymentAddress(Object wallet, String address); + Future updateFeeRates(Object wallet); } """;