diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 095183426..052d18ef0 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -173,6 +173,7 @@ class ElectrumClient { if (isJSONStringCorrect(unterminatedString)) { final response = json.decode(unterminatedString); + _handleResponse(response); // unterminatedString = null; unterminatedString = ''; } @@ -340,6 +341,80 @@ class ElectrumClient { return historyMap; } + Future>>> getBatchUnspent( + List scriptHashes, { + int timeout = 10000, + }) async { + final paramsList = scriptHashes.map((h) => [h]).toList(growable: false); + + final batchResults = await callBatchWithTimeout( + method: 'blockchain.scripthash.listunspent', + paramsList: paramsList, + timeout: timeout, + ); + + final unspentMap = >>{}; + + for (int i = 0; i < scriptHashes.length; i++) { + final sh = scriptHashes[i]; + + if (i >= batchResults.length) { + unspentMap[sh] = const []; + continue; + } + + final result = batchResults[i]; + + if (result is List) { + unspentMap[sh] = result + .whereType>() + .map((m) => m.map((k, v) => MapEntry(k.toString(), v))) + .cast>() + .toList(); + } else { + unspentMap[sh] = const []; + } + } + + return unspentMap; + } + + Future>> getBatchBalance( + List scriptHashes, { + int timeout = 10000, + }) async { + final paramsList = scriptHashes.map((h) => [h]).toList(growable: false); + + final batchResults = await callBatchWithTimeout( + method: 'blockchain.scripthash.get_balance', + paramsList: paramsList, + timeout: timeout, + ); + + final balanceMap = >{}; + + for (int i = 0; i < scriptHashes.length; i++) { + final sh = scriptHashes[i]; + + if (i >= batchResults.length) { + balanceMap[sh] = {}; + continue; + } + + final result = batchResults[i]; + + if (result is Map) { + balanceMap[sh] = result; + } else if (result is Map) { + balanceMap[sh] = Map.from(result); + } else { + balanceMap[sh] = {}; + } + } + + return balanceMap; + } + Future>> getBatchTransactionVerbose( List hashes, { int timeout = 10000, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index ec5492116..1b10235ea 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/lightning/lightning_wallet.dart'; import 'package:cw_core/hardware/hardware_wallet_service.dart'; import 'package:cw_core/root_dir.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; @@ -76,7 +77,7 @@ abstract class ElectrumWalletBase ElectrumBalance? initialBalance, CryptoCurrency? currency, bool? alwaysScan, - this.useLightning = true, + bool useLightning = true, }) : _masterHD = getMasterHD(seedBytes, network, walletInfo.hardwareWalletType), accountHD = getAccountHDWallet( currency, network, seedBytes, xpub, derivationInfo, walletInfo.hardwareWalletType), @@ -102,6 +103,7 @@ abstract class ElectrumWalletBase this.unspentCoinsInfo = unspentCoinsInfo, this.isTestnet = !network.isMainnet, this._mnemonic = mnemonic, + _useLightning = useLightning, super(walletInfo, derivationInfo) { this.electrumClient = electrumClient ?? electrum.ElectrumClient(); this.walletInfo = walletInfo; @@ -305,8 +307,13 @@ abstract class ElectrumWalletBase @observable bool? alwaysScan; + @computed + bool get useLightning => _useLightning && LightningWallet.isAvailable; + + set useLightning(bool val) => _useLightning = val && LightningWallet.isAvailable; + @observable - bool useLightning; + bool _useLightning; final Bip32Slip10Secp256k1? _masterHD; final Bip32Slip10Secp256k1 accountHD; @@ -360,6 +367,8 @@ abstract class ElectrumWalletBase String get xpub => accountHD.publicKey.toExtended; + bool get shouldUseBatchFetching => useBatchForHistory && _isBatchSupported == true; + @override String? get seed => _mnemonic; @@ -701,7 +710,7 @@ abstract class ElectrumWalletBase } await subscribeForUpdates(); - await _checkIfBatchSupported(); + await checkIfBatchSupported(); await updateTransactions(); await updateAllUnspents(); @@ -1786,18 +1795,21 @@ abstract class ElectrumWalletBase } // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating - walletAddresses.allAddresses - .where((element) => element.type != SegwitAddresType.mweb) - .forEach((addr) { - if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; - }); - final addressFutures = walletAddresses.allAddresses + final targetAddresses = walletAddresses.allAddresses .where((element) => element.type != SegwitAddresType.mweb) - .map((address) => fetchUnspent(address)) .toList(); - final results = await Future.wait(addressFutures); + for (final addr in targetAddresses) { + if (addr is! BitcoinSilentPaymentAddressRecord) { + addr.balance = 0; + } + } + + final results = shouldUseBatchFetching + ? await _fetchUnspentsBatch(targetAddresses) + : await _fetchUnspentsRegular(targetAddresses); + final failedCount = results.where((result) => result == null).length; if (failedCount == 0) { @@ -1829,6 +1841,74 @@ abstract class ElectrumWalletBase await _refreshUnspentCoinsInfo(); } + Future?>> _fetchUnspentsRegular( + List addresses, + ) async { + final addressFutures = addresses.map((address) => fetchUnspent(address)).toList(); + return Future.wait(addressFutures); + } + + + Future?>> _fetchUnspentsBatch( + List addresses, + ) async { + final byScriptHash = { + for (final address in addresses) address.getScriptHash(network): address, + }; + + final scriptHashes = byScriptHash.keys.toList(); + + try { + final unspentByScriptHash = + await _processChunksToMap>>( + items: scriptHashes, + chunkSize: addressHistoryChunkSize, + processChunk: _getListUnspentBatch, + ); + + final txHashes = {}; + final coinsByScriptHash = >{}; + + for (final entry in unspentByScriptHash.entries) { + final addressRecord = byScriptHash[entry.key]; + if (addressRecord == null) continue; + + final coins = []; + + for (final unspent in entry.value) { + final coin = BitcoinUnspent.fromJSON(addressRecord, unspent); + coin.isChange = addressRecord.isHidden; + coins.add(coin); + txHashes.add(coin.hash); + } + + coinsByScriptHash[entry.key] = coins; + } + + final txInfoByHash = await fetchTransactionInfoBatch( + hashes: txHashes.toList(), + retryOnFailure: true, + retryDelay: const Duration(seconds: 1), + ); + + for (final coins in coinsByScriptHash.values) { + for (final coin in coins) { + final tx = txInfoByHash[coin.hash]; + coin.confirmations = tx?.confirmations; + coin.isPegOut = tx?.isHogEx; + } + } + + return addresses.map((address) { + final scriptHash = address.getScriptHash(network); + return coinsByScriptHash[scriptHash] ?? []; + }).toList(); + } catch (e) { + printV('fetchUnspentsBatch failed: $e'); + return List?>.filled(addresses.length, null); + } + } + List handleFailedUtxoFetch({ required int failedCount, required List previousUnspentCoins, @@ -2388,25 +2468,30 @@ abstract class ElectrumWalletBase @override Future> fetchTransactions() async { try { - final Map historiesWithDetails = {}; - final shouldUseBatchForHistory = useBatchForHistory && _isBatchSupported == true; + final Map historiesWithDetails = {};; - printV('[BATCH_TEST] Fetching transactions with batch: $shouldUseBatchForHistory'); + printV('[BATCH_TEST] Fetching transactions with batch: $shouldUseBatchFetching'); if (type == WalletType.bitcoin) { - await Future.wait(BITCOIN_ADDRESS_TYPES.map((type) => shouldUseBatchForHistory + await Future.wait(BITCOIN_ADDRESS_TYPES.map((type) => shouldUseBatchFetching ? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type) : fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.bitcoinCash) { await Future.wait(BITCOIN_CASH_ADDRESS_TYPES - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); + .map((type) => shouldUseBatchFetching + ? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type) + : fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.litecoin) { await Future.wait(LITECOIN_ADDRESS_TYPES .where((type) => type != SegwitAddresType.mweb) - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); + .map((type) => shouldUseBatchFetching + ? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type) + : fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.dogecoin) { await Future.wait(DOGECOIN_ADDRESS_TYPES - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); + .map((type) => shouldUseBatchFetching + ? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type) + : fetchTransactionsForAddressType(historiesWithDetails, type))); } transactionHistory.transactions.values.forEach((tx) async { @@ -2450,13 +2535,13 @@ abstract class ElectrumWalletBase historiesWithDetails.addAll(history); final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses; - final isUsedAddressUnderGap = matchedAddresses.toList().indexOf(addressRecord) >= + final isUsedAddressAboveGap = matchedAddresses.toList().indexOf(addressRecord) >= matchedAddresses.length - (addressRecord.isHidden ? ElectrumWalletAddressesBase.defaultChangeAddressesCount : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); - if (isUsedAddressUnderGap) { + if (isUsedAddressAboveGap) { final prevLength = walletAddresses.allAddresses.length; // Discover new addresses for the same address type until the gap limit is respected @@ -2531,19 +2616,7 @@ abstract class ElectrumWalletBase // Got a new transaction fetched, add it to the transaction history // instead of waiting all to finish, and next time it will be faster - if (this is LitecoinWallet) { - // if we have a peg out transaction with the same value - // that matches this received transaction, mark it as being from a peg out: - for (final tx2 in transactionHistory.transactions.values) { - final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs(); - // this isn't a perfect matching algorithm since we don't have the right input/output information from these transaction models (the addresses are in different formats), but this should be more than good enough for now as it's extremely unlikely a user receives the EXACT same amount from 2 different sources and one of them is a peg out and the other isn't WITHIN 5 blocks of each other - if (tx2.additionalInfo["isPegOut"] == true && - tx2.amount == tx.amount && - heightDiff <= 5) { - tx.additionalInfo["fromPegOut"] = true; - } - } - } + _applyLitecoinPegOutTag(tx); transactionHistory.addOne(tx); await transactionHistory.save(); } @@ -2565,65 +2638,128 @@ abstract class ElectrumWalletBase } Future fetchTransactionsForAddressTypeBatch( - Map historiesWithDetails, BitcoinAddressType type) async { + Map historiesWithDetails, + BitcoinAddressType type) async { final addressesByType = - walletAddresses.allAddresses.where((addr) => addr.type == type).toList(); - final hiddenAddresses = addressesByType.where((addr) => addr.isHidden).toList(); + walletAddresses.allAddresses.where((addr) => addr.type == type).toList(); final receiveAddresses = addressesByType.where((addr) => !addr.isHidden).toList(); + final hiddenAddresses = addressesByType.where((addr) => addr.isHidden).toList(); + walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address)); await walletAddresses.saveAddressesInBox(); - final tip = await getCurrentChainTip(); - - final addressHistory = await _processChunksToMap( - items: addressesByType, - chunkSize: addressHistoryChunkSize, - processChunk: (chunk) => _fetchBatchAddressHistory(chunk, tip, addressHistoryChunkSize), + await fetchTransactionsForAddressesBranchBatch( + historiesWithDetails, + type, + receiveAddresses, + isHidden: false, + isLegacyDerivation: false, ); - if (addressHistory.isNotEmpty) historiesWithDetails.addAll(addressHistory); + await fetchTransactionsForAddressesBranchBatch( + historiesWithDetails, + type, + hiddenAddresses, + isHidden: true, + isLegacyDerivation: false, + ); - for (final addressRecord in addressesByType) { - final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses; + await fetchTransactionsForAddressesBranchBatch( + historiesWithDetails, + type, + receiveAddresses, + isHidden: false, + isLegacyDerivation: true, + ); - final isUsedAddressUnderGap = - matchedAddresses.indexOf(addressRecord) >= - matchedAddresses.length - ElectrumWalletAddressesBase.gap; - - if (isUsedAddressUnderGap && addressRecord.isUsed) { - final prevLength = walletAddresses.allAddresses.length; + await fetchTransactionsForAddressesBranchBatch( + historiesWithDetails, + type, + hiddenAddresses, + isHidden: true, + isLegacyDerivation: true, + ); + } - await walletAddresses.discoverAddressesBatch( - matchedAddresses, - addressRecord.isHidden, - (newAddresses) async { - await _fetchBatchAddressHistory( - newAddresses, - tip, - discoveryHistoryChunkSize, - ); + Future fetchTransactionsForAddressesBranchBatch( + Map historiesWithDetails, + BitcoinAddressType type, + List branchAddresses, { + required bool isHidden, + required bool isLegacyDerivation, + }) async { + if (branchAddresses.isEmpty) return; - return newAddresses - .where((addressRecord) => addressRecord.isUsed) - .map((addressRecord) => addressRecord.address) - .toSet(); - }, - type: type, + final tip = await getCurrentChainTip(); + final currentBranch = [...branchAddresses]; + + final initialHistory = + await _processChunksToMap( + items: currentBranch, + chunkSize: addressHistoryChunkSize, + processChunk: (chunk) => _fetchBatchAddressHistory( + chunk, + tip, + addressHistoryChunkSize, + ), + ); + + if (initialHistory.isNotEmpty) { + historiesWithDetails.addAll(initialHistory); + } + + final gapLimit = isHidden + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount; + + final highestUsedIndex = _highestUsedIndex(currentBranch); + final shouldDiscover = + highestUsedIndex >= 0 && highestUsedIndex >= currentBranch.length - gapLimit; + + if (!shouldDiscover) return; + + + final newAddresses = await walletAddresses.discoverAddressesBatch( + currentBranch, + isHidden, + (newAddresses) async { + final newHistory = await _fetchBatchAddressHistory( + newAddresses, + tip, + discoveryHistoryChunkSize, ); - final newLength = walletAddresses.allAddresses.length; - - if (newLength > prevLength) { - await fetchTransactionsForAddressTypeBatch( - historiesWithDetails, - type); - return; + if (newHistory.isNotEmpty) { + historiesWithDetails.addAll(newHistory); } + + return newAddresses + .where((addressRecord) => addressRecord.isUsed) + .map((addressRecord) => addressRecord.address) + .toSet(); + }, + type: type, + isLegacyDerivation: isLegacyDerivation, + ); + + if (newAddresses.isNotEmpty) { + currentBranch.addAll(newAddresses); + + if (isHidden) { + walletAddresses.hiddenAddresses.addAll(newAddresses.map((e) => e.address)); + await walletAddresses.saveAddressesInBox(); } } } + int _highestUsedIndex(List addresses) { + for (int i = addresses.length - 1; i >= 0; i--) { + if (addresses[i].isUsed) return i; + } + return -1; + } + Future> _fetchBatchAddressHistory( List addressRecords, int? currentHeight, @@ -2755,17 +2891,7 @@ abstract class ElectrumWalletBase historiesWithDetails[tx.id] = tx; - // Litecoin peg-out tagging - if (this is LitecoinWallet) { - for (final tx2 in transactionHistory.transactions.values) { - final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs(); - if (tx2.additionalInfo["isPegOut"] == true && - tx2.amount == tx.amount && - heightDiff <= 5) { - tx.additionalInfo["fromPegOut"] = true; - } - } - } + _applyLitecoinPegOutTag(tx); transactionHistory.addOne(tx); didUpdateHistory = true; @@ -2812,6 +2938,22 @@ abstract class ElectrumWalletBase ); } + Future>>> _getListUnspentBatch( + List scriptHashes) { + return electrumClient.getBatchUnspent( + scriptHashes, + timeout: transactionBatchTimeoutMs, + ); + } + + Future>> _getBalanceBatch( + List scriptHashes) { + return electrumClient.getBatchBalance( + scriptHashes, + timeout: transactionBatchTimeoutMs, + ); + } + Future> fetchTransactionInfoBatch({ required List hashes, Map? heightsByHash, @@ -2899,8 +3041,16 @@ abstract class ElectrumWalletBase final inputTxIdsByHash = _collectInputTxIdsByHash(originalByHash); - final inputVerboseByTxId = await _fetchInputTransactionVerboseBatch( - inputTxIdsByHash); + final allInputTxids = {}; + for (final txids in inputTxIdsByHash.values) { + allInputTxids.addAll(txids); + } + + final inputTxIds = allInputTxids.toList(growable: false); + + final inputVerboseByTxId = inputTxIds.isEmpty + ? >{} + : await _fetchTransactionVerboseBatch(inputTxIds); final parsedInputTxById = _parseTransactions(inputVerboseByTxId); @@ -2992,65 +3142,6 @@ abstract class ElectrumWalletBase return inputTxIdsByHash; } - - Future>> _fetchInputTransactionVerboseBatch( - Map> inputTxidsByHash) async { - final allInputTxids = {}; - for (final txids in inputTxidsByHash.values) { - allInputTxids.addAll(txids); - } - - final inputTxIds = allInputTxids.toList(growable: false); - - final verboseTransactionByHash = - await _processChunksToMap>( - items: inputTxIds, - chunkSize: inputTransactionChunkSize, - processChunk: _getTransactionVerboseBatch, - onChunkError: (chunk, error) { - if (error is electrum.RequestFailedTimeoutException) { - printV( - 'fetchInputTransactionVerboseBatch timeout for ${chunk.length} txs: ${error.method}', - ); - } else { - printV( - 'fetchInputTransactionVerboseBatch failed for ${chunk.length} txs: $error,', - ); - } - }, - ); - - final emptyHex = []; - for (final txId in inputTxIds) { - final vTx = verboseTransactionByHash[txId]; - if (vTx == null || vTx.isEmpty || vTx['hex'] == null) { - emptyHex.add(txId); - } - } - - final hexByHash = await _processChunksToMap( - items: emptyHex, - chunkSize: inputTransactionChunkSize, - processChunk: _getTransactionHexBatch, - ); - - for (final txId in inputTxIds) { - final verbose = verboseTransactionByHash[txId] ?? {}; - if ((verbose['hex'] as String?) == null) { - final hex = hexByHash[txId]; - if (hex != null && hex.isNotEmpty) { - verboseTransactionByHash[txId] = { - ...verbose, - 'hex': hex, - }; - } - } - } - - return verboseTransactionByHash; - } - - Future> _buildTransactionBundlesBatch({ required List unique, required Map? heightsByHash, @@ -3283,18 +3374,64 @@ abstract class ElectrumWalletBase })); } + Future>> fetchBalancesBatch( + List addresses, + ) async { + final scriptHashes = addresses.map((address) => address.getScriptHash(network)).toList(); + + if (scriptHashes.isEmpty) { + return >[]; + } + + try { + final balancesByScriptHash = + await _processChunksToMap>( + items: scriptHashes, + chunkSize: addressHistoryChunkSize, + processChunk: _getBalanceBatch, + ); + + final balances = scriptHashes + .map((scriptHash) => balancesByScriptHash[scriptHash] ?? {}) + .toList(); + + final hasMissingBalance = balances.any((balance) => balance['confirmed'] == null); + if (hasMissingBalance) { + printV('fetchBalancesBatch returned missing balances, falling back to regular flow'); + return fetchBalancesRegular(addresses); + } + + return balances; + } catch (e) { + printV('fetchBalancesBatch failed, falling back to regular flow: $e'); + return fetchBalancesRegular(addresses); + } + } + + Future>> fetchBalancesRegular( + List addresses, + ) async { + final balanceFutures = >>[]; + + for (final address in addresses) { + final sh = address.getScriptHash(network); + balanceFutures.add(electrumClient.getBalance(sh)); + } + + return Future.wait(balanceFutures); + } + Future fetchBalances() async { final addresses = walletAddresses.allAddresses .where((address) => address.address.isNotEmpty) .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) .toList(); - final balanceFutures = >>[]; - for (var i = 0; i < addresses.length; i++) { - final addressRecord = addresses[i]; - final sh = addressRecord.getScriptHash(network); - final balanceFuture = electrumClient.getBalance(sh); - balanceFutures.add(balanceFuture); - } + + final balances = shouldUseBatchFetching + ? await fetchBalancesBatch(addresses) + : await fetchBalancesRegular(addresses); + + printV('Fetched balances for ${addresses.length} addresses. Batch fetching: $shouldUseBatchFetching'); var totalFrozen = 0; var totalConfirmed = 0; @@ -3330,8 +3467,6 @@ abstract class ElectrumWalletBase }); }); - final balances = await Future.wait(balanceFutures); - if (balances.isNotEmpty && balances.first['confirmed'] == null) { // if we got null balance responses from the server, set our connection status to lost and return our last known balance: printV("got null balance responses from the server, setting connection status to lost"); @@ -3413,7 +3548,23 @@ abstract class ElectrumWalletBase return base64Encode(decodedSig); } - Future _checkIfBatchSupported() async { + void _applyLitecoinPegOutTag(ElectrumTransactionInfo tx) { + if (this is! LitecoinWallet) return; + + // if we have a peg out transaction with the same value + // that matches this received transaction, mark it as being from a peg out: + for (final tx2 in transactionHistory.transactions.values) { + final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs(); + // this isn't a perfect matching algorithm since we don't have the right input/output information from these transaction models (the addresses are in different formats), but this should be more than good enough for now as it's extremely unlikely a user receives the EXACT same amount from 2 different sources and one of them is a peg out and the other isn't WITHIN 5 blocks of each other + if (tx2.additionalInfo["isPegOut"] == true && + tx2.amount == tx.amount && + heightDiff <= 5) { + tx.additionalInfo["fromPegOut"] = true; + } + } + } + + Future checkIfBatchSupported() async { if (_isBatchSupported != null) { printV('[BATCH_TEST] Already checked: $_isBatchSupported'); @@ -3433,12 +3584,23 @@ abstract class ElectrumWalletBase printV('[BATCH_TEST] Start: hashes=${hashes.length}, timeout=${batchTestTimeoutMs}ms'); - await electrumClient.callBatchWithTimeout( + final result = await electrumClient.callBatchWithTimeout( method: 'blockchain.scripthash.get_history', paramsList: paramsList, timeout: batchTestTimeoutMs, ); + final hasError = result.any((item) => + item is Map && + item.containsKey('error') && + item['error'] != null); + + if (hasError) { + _isBatchSupported = false; + printV('[BATCH_TEST] Result: supported=false (server returned error)'); + return; + } + _isBatchSupported = true; printV('[BATCH_TEST] Result: supported=true'); } on electrum.RequestFailedTimeoutException catch (e) { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index d3763386d..e692816b6 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -715,38 +715,42 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future discoverAddressesBatch( + Future> discoverAddressesBatch( List addressList, bool isHidden, Future> Function(List) getUsedAddresses, { BitcoinAddressType type = SegwitAddresType.p2wpkh, + required bool isLegacyDerivation, }) async { final newAddresses = await _createNewAddresses( gap, startIndex: addressList.length, isHidden: isHidden, type: type, + isLegacyDerivation: isLegacyDerivation, ); addAddresses(newAddresses); final usedAddresses = await getUsedAddresses(newAddresses); - final isLastAddressUsed = - newAddresses.isNotEmpty && usedAddresses.contains(newAddresses.last.address); - if (!isLastAddressUsed) { - return false; + final hasUsedAddressInGap = newAddresses.any( + (addressRecord) => usedAddresses.contains(addressRecord.address)); + + if (!hasUsedAddressInGap) { + return newAddresses; } final updatedAddressList = [...addressList, ...newAddresses]; - await discoverAddressesBatch( + final moreNewAddresses = await discoverAddressesBatch( updatedAddressList, isHidden, getUsedAddresses, type: type, + isLegacyDerivation: isLegacyDerivation, ); - return true; + return [...newAddresses, ...moreNewAddresses]; } @@ -818,6 +822,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { this._addresses.clear(); this._addresses.addAll(addressesSet); updateAddressesByMatch(); + updateReceiveAddresses(); + updateChangeAddresses(); } @action diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 3cee6c9da..3a363aa4a 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -381,6 +381,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } catch (e) { printV("failed to subscribe for updates: $e"); } + await checkIfBatchSupported(); updateFeeRates(); _feeRatesTimer?.cancel(); _feeRatesTimer = diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 818286a2d..7550fc626 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -92,15 +92,25 @@ class Node { login = map['login'] as String?, label = map['label'] as String?, password = map['password'] as String?, - isPow = (map["isPow"] != null && map['isPow'] != 0) as bool? ?? false, - useSSL = (map['useSSL'] != 0) as bool?, + isPow = _getBoolFromDB(map['isPow']), + useSSL = _getBoolFromDB(map['useSSL']), typeRaw = (map["typeRaw"] ?? 0) as int, - trusted = (map['trusted'] != 0) as bool? ?? false, + trusted = _getBoolFromDB(map['trusted']), socksProxyAddress = map['socksProxyAddress'] as String?, - isEnabledForAutoSwitching = (map['isEnabledForAutoSwitching'] != 0) as bool? ?? false, - isOfficial = (map['isOfficial'] != 0) as bool? ?? false, - isBuiltin = (map['isBuiltin'] != 0) as bool? ?? false, - isDefault = (map['isDefault'] != 0) as bool? ?? false; + isEnabledForAutoSwitching = _getBoolFromDB(map['isEnabledForAutoSwitching']), + isOfficial = _getBoolFromDB(map['isOfficial']), + isBuiltin = _getBoolFromDB(map['isBuiltin']), + isDefault = _getBoolFromDB(map['isDefault']); + + static bool _getBoolFromDB(value, {bool? defaultValue}) { + if (value is bool) { + return value; + } else if (value is int) { + return value == 1; + } else { + return defaultValue ?? false; + } + } Map toMap() { return { @@ -110,15 +120,15 @@ class Node { 'login': login, "label": label, 'password': password, - "isPow": isPow, - 'useSSL': useSSL, + "isPow": isPow ? 1 : 0, + 'useSSL': (useSSL??false) ? 1 : 0, "typeRaw": typeRaw, - 'trusted': trusted, + 'trusted': trusted ? 1 : 0, 'socksProxyAddress': socksProxyAddress, - 'isEnabledForAutoSwitching': isEnabledForAutoSwitching, - "isOfficial": isOfficial, - "isBuiltin": isBuiltin, - "isDefault": isDefault + 'isEnabledForAutoSwitching': isEnabledForAutoSwitching ? 1 : 0, + "isOfficial": isOfficial ? 1 : 0, + "isBuiltin": isBuiltin ? 1 : 0, + "isDefault": isDefault ? 1 : 0 }; } @@ -127,11 +137,11 @@ class Node { } static Future deleteAll() async { - return await db!.delete(tableName, where: "isPow = ?", whereArgs: [false]); + return await db!.delete(tableName, where: "isPow = ?", whereArgs: [0]); } static Future deleteAllPow() async { - return await db!.delete(tableName, where: "isPow = ?", whereArgs: [true]); + return await db!.delete(tableName, where: "isPow = ?", whereArgs: [1]); } Future save() async { @@ -173,35 +183,35 @@ class Node { static Future> getAll() async { - return selectList("isPow = ?", [false]); + return selectList("isPow = ?", [0]); } static Future> getAllBuiltin() async { - return selectList("isPow = ? AND isBuiltin = ?", [false, true]); + return selectList("isPow = ? AND isBuiltin = ?", [0, 1]); } static Future> getAllPowBuiltin() async { - return selectList("isPow = ? AND isBuiltin = ?", [true, true]); + return selectList("isPow = ? AND isBuiltin = ?", [1, 1]); } static Future> getAllForWalletType(WalletType type) async { - return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), false]); + return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), 0]); } static Future getDefaultForWalletType(WalletType type) async { - return (await selectList("typeRaw = ? AND isPow = ? AND isDefault = ?", [serializeToInt(type), false, true])).firstOrNull; + return (await selectList("typeRaw = ? AND isPow = ? AND isDefault = ?", [serializeToInt(type), 0, 1])).firstOrNull; } static Future getDefaultPowForWalletType(WalletType type) async { - return (await selectList("typeRaw = ? AND isPow = ? AND isDefault = ?", [serializeToInt(type), true, true])).firstOrNull; + return (await selectList("typeRaw = ? AND isPow = ? AND isDefault = ?", [serializeToInt(type), 1, 1])).firstOrNull; } static Future> getAllForWalletTypePow(WalletType type) async { - return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), true]); + return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), 1]); } static Future> getAllPow() async { - return selectList("isPow = ?", [true]); + return selectList("isPow = ?", [1]); } static Future get(int id) async { @@ -475,7 +485,7 @@ class Node { }, ), ); - + final data = jsonDecode(response.body); if (response.statusCode != 200 || data["error"] != null || diff --git a/lib/new-ui/pages/receive_page.dart b/lib/new-ui/pages/receive_page.dart index 764393e3b..9ad2d7d2f 100644 --- a/lib/new-ui/pages/receive_page.dart +++ b/lib/new-ui/pages/receive_page.dart @@ -342,7 +342,7 @@ class _NewReceivePageState extends State { setState(() { _addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull( (item) => (item is WalletAddressListItem && item.address == newAddress.address)) - as WalletAddressListItem; + as WalletAddressListItem?; }); } } diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index 1fd28fc9b..c3f5da957 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -972,7 +972,7 @@ class _NewSendPageState extends State { onNext: (PaymentFlowResult newResult) { final selectedChainId = newResult.chainId; final isCompatible = - selectedChainId == evm!.getSelectedChainId(widget.sendViewModel.wallet); + selectedChainId == evm?.getSelectedChainId(widget.sendViewModel.wallet); if (isCompatible) { widget.sendViewModel.setSelectedCryptoCurrency( diff --git a/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart b/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart index 3400ef82f..271f38898 100644 --- a/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart +++ b/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart @@ -263,7 +263,7 @@ class _SwapAddressSelectionModalRowState extends State \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 6adcddda4..c10fdd318 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -208,7 +208,7 @@ "coin_control": "التحكم في العملات (اختياري)", "coin_control_desc": "تصفية المخرجات لهذه المعاملة. ستنعكس التغييرات التي تجريها هنا على المعاملة الحالية فقط.", "coin_control_load_failed": "فشل تحميل العملات غير المصروفة", - "cold_or_recover_wallet": "أضف محفظة للعرض فقط من Cupcake أو محفظة باردة أو استعد محفظة ورقية", + "cold_or_recover_wallet": "أضف محفظة باردة معزولة عن الهواء أو استرد محفظة ورقية.", "collection_address": "عنوان الاستلام", "collection_description": "وصف المجموعة", "collection_name": "اسم المجموعة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 07ac4cb2b..ad912b030 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -208,7 +208,7 @@ "coin_control": "Контрол на монетите (по избор)", "coin_control_desc": "Филтрирайте изходите за тази транзакция. Промените, които направите тук, ще се отразят само на текущата транзакция.", "coin_control_load_failed": "Неуспешно зареждане на неизхарчени монети", - "cold_or_recover_wallet": "Добавете портфейл само за четене от Cupcake или от студен портфейл, или възстановете хартиен портфейл", + "cold_or_recover_wallet": "Добавете cold wallet с въздушна изолация или възстановете хартиен портфейл.", "collection_address": "Адрес за събиране", "collection_description": "Описание на колекцията", "collection_name": "Име на колекцията", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 17311f762..ff731539e 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -208,7 +208,7 @@ "coin_control": "Kontrola mincí (nepovinné)", "coin_control_desc": "Filtrujte výstupy pro tuto transakci. Změny, které zde provedete, se projeví pouze u aktuální transakce.", "coin_control_load_failed": "Nepodařilo se načíst neutracené mince", - "cold_or_recover_wallet": "Přidejte peněženku pouze pro čtení z Cupcake nebo z cold peněženky, nebo obnovte papírovou peněženku", + "cold_or_recover_wallet": "Přidejte air-gapped cold wallet nebo obnovte papírovou peněženku.", "collection_address": "Sběrná adresa", "collection_description": "Popis kolekce", "collection_name": "Název kolekce", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index da7855e06..42c892ae8 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -208,7 +208,7 @@ "coin_control": "Coin Control (optional)", "coin_control_desc": "Filtern Sie die Outputs für diese Transaktion. Die hier vorgenommenen Änderungen wirken sich nur auf die aktuelle Transaktion aus.", "coin_control_load_failed": "Nicht ausgegebene Coins konnten nicht geladen werden", - "cold_or_recover_wallet": "Fügen Sie eine schreibgeschützte Wallet von Cupcake oder eine Cold Wallet hinzu oder stellen Sie eine Paper Wallet wieder her.", + "cold_or_recover_wallet": "Fügen Sie eine air-gapped Cold Wallet hinzu oder stellen Sie eine Paper Wallet wieder her.", "collection_address": "Sammeladresse", "collection_description": "Sammlungsbeschreibung", "collection_name": "Sammlungsname", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index deb88f4c1..6e73b1683 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -210,7 +210,7 @@ "coin_control": "Coin Control (optional)", "coin_control_desc": "Filter outputs for this transaction. The changes you make here will only reflect on the current transaction.", "coin_control_load_failed": "Failed to load unspent coins", - "cold_or_recover_wallet": "Add a read-only wallet from Cupcake or a cold wallet or recover a paper wallet", + "cold_or_recover_wallet": "Add an air-gapped cold wallet or recover a paper wallet.", "collection_address": "Collection Address", "collection_description": "Collection Description", "collection_name": "Collection Name", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 7093d823e..5754f2000 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -208,7 +208,7 @@ "coin_control": "Control de monedas (opcional)", "coin_control_desc": "Filtra las salidas de esta transacción. Los cambios que realices aquí solo se reflejarán en la transacción actual.", "coin_control_load_failed": "No se pudieron cargar las monedas sin gastar", - "cold_or_recover_wallet": "Añadir una billetera de solo lectura desde Cupcake, una billetera fría o recuperar una billetera de papel", + "cold_or_recover_wallet": "Agrega una cold wallet air-gapped o recupera una paper wallet.", "collection_address": "Dirección de cobro", "collection_description": "Descripción de la colección", "collection_name": "Nombre de la colección", diff --git a/res/values/strings_fa.arb b/res/values/strings_fa.arb index f75cf9c4a..f39bf4f73 100644 --- a/res/values/strings_fa.arb +++ b/res/values/strings_fa.arb @@ -206,7 +206,7 @@ "coin_control": "کنترل کوین (اختیاری)", "coin_control_desc": "خروجی‌های این تراکنش را فیلتر کنید. تغییراتی که اینجا اعمال می‌کنید فقط روی تراکنش فعلی اثر می‌گذارد.", "coin_control_load_failed": "بارگیری کوین‌های خرج‌نشده ناموفق بود", - "cold_or_recover_wallet": "افزودن یک کیف پول فقط‌خواندنی از Cupcake یا یک کیف پول سرد یا بازیابی یک کیف پول کاغذی", + "cold_or_recover_wallet": "یک کیف پول سرد ایزوله‌شده از هوا اضافه کنید یا یک کیف پول کاغذی را بازیابی کنید.", "collection_address": "آدرس دریافت", "collection_description": "توضیحات مجموعه", "collection_name": "نام مجموعه", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 1fe1f36e2..72f8ec0b1 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -208,7 +208,7 @@ "coin_control": "Contrôle des pièces (optionnel)", "coin_control_desc": "Filtrer les sorties pour cette transaction. Les modifications que vous apportez ici ne s’appliqueront qu’à la transaction en cours.", "coin_control_load_failed": "Échec du chargement des sorties non dépensées", - "cold_or_recover_wallet": "Ajoutez un portefeuille en lecture seule depuis Cupcake, un portefeuille froid ou récupérez un portefeuille papier", + "cold_or_recover_wallet": "Ajoutez un cold wallet air-gapped ou récupérez un paper wallet.", "collection_address": "Adresse de collecte", "collection_description": "Description de la collection", "collection_name": "Nom de la collection", diff --git a/res/values/strings_gn.arb b/res/values/strings_gn.arb index 70a0def4c..e2ea93646 100644 --- a/res/values/strings_gn.arb +++ b/res/values/strings_gn.arb @@ -170,7 +170,7 @@ "coin_control": "Jehecha moneda (poravokuaá)", "coin_control_desc": "Embogua hag̃ua umi outputs ko transacción-pe guarã. Umi ñemoambue rejapóva ko’ápe ojehechaukáta pe transacción ko’ág̃aguáre añoite.", "coin_control_load_failed": "Ndojekargai umi moneda ndojeporúiva .", - "cold_or_recover_wallet": "Emoĩ peteĩ billetera moñe’ẽ hag̃uánte Cupcake-gui, peteĩ billetera ro’ysã, térã eguerujey peteĩ billetera kuatiapegua", + "cold_or_recover_wallet": "Embojoapy peteĩ cold wallet air-gapped térã emboykeréi peteĩ paper wallet.", "color": "Sa'y", "color_theme": "Téma sa'y", "commit_transaction_amount_fee": "Emoneĩ ñembohasa\nMboheta: ${amount}\nHepyme’ẽ: ${fee}", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index e7d46a284..d6dab4f5f 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -208,7 +208,7 @@ "coin_control": "Sarrafa tsabar kuɗi (na zaɓi)", "coin_control_desc": "Tace fitarwa (outputs) don wannan ma'amala. Canje-canjen da kuke yi a nan za su shafi ma'amalar yanzu kawai.", "coin_control_load_failed": "An kasa loda tsabar kuɗin da ba a kashe ba", - "cold_or_recover_wallet": "Ƙara walat mai karantawa-kawai daga Cupcake ko walat mai sanyi, ko dawo da walat na takarda", + "cold_or_recover_wallet": "Ƙara cold wallet mai air-gapped ko dawo da paper wallet.", "collection_address": "Adireshin karɓa", "collection_description": "Bayanin Tarin", "collection_name": "Sunan tarin", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 232c96684..4073dc5ac 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -208,7 +208,7 @@ "coin_control": "कॉइन नियंत्रण (वैकल्पिक)", "coin_control_desc": "इस लेनदेन के लिए आउटपुट फ़िल्टर करें। यहां किए गए बदलाव केवल वर्तमान लेनदेन में ही दिखाई देंगे।", "coin_control_load_failed": "अनखर्चित सिक्के लोड करने में विफल", - "cold_or_recover_wallet": "Cupcake या किसी कोल्ड वॉलेट से एक रीड-ओनली वॉलेट जोड़ें, या किसी पेपर वॉलेट को रिकवर करें", + "cold_or_recover_wallet": "एक एयर-गैप्ड कोल्ड वॉलेट जोड़ें या पेपर वॉलेट रिकवर करें।", "collection_address": "कलेक्शन पता", "collection_description": "संग्रह का विवरण", "collection_name": "संग्रह का नाम", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 10f8e27ad..cf1da0fbb 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -208,7 +208,7 @@ "coin_control": "Kontrola kovanica (nije obavezno)", "coin_control_desc": "Filtrirajte izlaze za ovu transakciju. Promjene koje ovdje napravite odrazit će se samo na trenutnu transakciju.", "coin_control_load_failed": "Učitavanje nepotrošenih kovanica nije uspjelo", - "cold_or_recover_wallet": "Dodajte novčanik samo za čitanje iz Cupcakea ili hladni novčanik ili oporavite papirnati novčanik", + "cold_or_recover_wallet": "Dodajte air-gapped cold wallet ili oporavite paper wallet.", "collection_address": "Adresa za prikupljanje", "collection_description": "Opis kolekcije", "collection_name": "Naziv kolekcije", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 2c1da86a7..191444e9d 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -208,7 +208,7 @@ "coin_control": "Մետաղադրամների կառավարում (ըստ ցանկության)", "coin_control_desc": "Զտեք ելքերը այս գործարքի համար։ Այստեղ կատարած փոփոխությունները կանդրադառնան միայն ընթացիկ գործարքի վրա։", "coin_control_load_failed": "Չհաջողվեց բեռնել չծախսված մետաղադրամները", - "cold_or_recover_wallet": "Ավելացնել միայն դիտման դրամապանակ Cupcake-ից կամ սառը դրամապանակից, կամ վերականգնել թղթային դրամապանակը", + "cold_or_recover_wallet": "Ավելացրեք air-gapped cold wallet կամ վերականգնեք paper wallet:", "collection_address": "Հավաքագրման հասցե", "collection_description": "Հավաքածուի նկարագրություն", "collection_name": "Հավաքածուի անունը", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 7a36f5332..4ade8cd41 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -208,7 +208,7 @@ "coin_control": "Kontrol koin (opsional)", "coin_control_desc": "Filter output untuk transaksi ini. Perubahan yang Anda buat di sini hanya akan berlaku pada transaksi saat ini.", "coin_control_load_failed": "Gagal memuat koin yang belum dibelanjakan", - "cold_or_recover_wallet": "Tambahkan dompet hanya-baca dari Cupcake atau dompet dingin, atau pulihkan dompet kertas", + "cold_or_recover_wallet": "Tambahkan cold wallet air-gapped atau pulihkan paper wallet.", "collection_address": "Alamat Penagihan", "collection_description": "Deskripsi Koleksi", "collection_name": "Nama Koleksi", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index e0e1c0c69..96906a864 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -208,7 +208,7 @@ "coin_control": "Controllo delle monete (opzionale)", "coin_control_desc": "Filtra gli output per questa transazione. Le modifiche apportate qui avranno effetto solo sulla transazione corrente.", "coin_control_load_failed": "Impossibile caricare le monete non spese", - "cold_or_recover_wallet": "Aggiungi un portafoglio di sola lettura da Cupcake o un portafoglio cold, oppure recupera un portafoglio cartaceo", + "cold_or_recover_wallet": "Aggiungi un cold wallet air-gapped o recupera un paper wallet.", "collection_address": "Indirizzo di raccolta", "collection_description": "Descrizione della collezione", "collection_name": "Nome della collezione", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 8b0ce5d48..3bd828e31 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -208,7 +208,7 @@ "coin_control": "コインコントロール(任意)", "coin_control_desc": "このトランザクションの出力を絞り込みます。ここで行った変更は、現在のトランザクションにのみ反映されます。", "coin_control_load_failed": "未使用のコインの読み込みに失敗しました", - "cold_or_recover_wallet": "Cupcakeまたはコールドウォレットから読み取り専用ウォレットを追加するか、ペーパーウォレットを復元する", + "cold_or_recover_wallet": "エアギャップのコールドウォレットを追加するか、ペーパーウォレットを復元します。", "collection_address": "コレクションアドレス", "collection_description": "コレクションの説明", "collection_name": "コレクション名", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 7c83c1b4b..2cbc34b80 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -208,7 +208,7 @@ "coin_control": "코인 컨트롤(선택 사항)", "coin_control_desc": "이 거래의 출력(UTXO)을 필터링합니다. 여기에서 변경한 내용은 현재 거래에만 반영됩니다.", "coin_control_load_failed": "미사용 코인을 불러오지 못했습니다", - "cold_or_recover_wallet": "Cupcake 또는 콜드 월렛에서 읽기 전용 지갑을 추가하거나 페이퍼 월렛을 복구합니다", + "cold_or_recover_wallet": "에어갭 콜드 월렛을 추가하거나 페이퍼 월렛을 복구하세요.", "collection_address": "컬렉션 주소", "collection_description": "컬렉션 설명", "collection_name": "컬렉션 이름", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 33287b745..bb37d0ad0 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -208,7 +208,7 @@ "coin_control": "Coin control (ရွေးချယ်နိုင်သည်)", "coin_control_desc": "ဤငွေလွှဲပြောင်းမှုအတွက် အထွက်များကို စစ်ထုတ်ပါ။ ဤနေရာတွင် သင်ပြုလုပ်သော အပြောင်းအလဲများသည် လက်ရှိ ငွေလွှဲပြောင်းမှုတွင်သာ သက်ရောက်ပါမည်။", "coin_control_load_failed": "မသုံးရသေးသော ကွိုင်များကို ဖွင့်၍မရပါ။", - "cold_or_recover_wallet": "Cupcake သို့မဟုတ် cold wallet မှ read-only wallet ကို ထည့်ပါ သို့မဟုတ် paper wallet ကို ပြန်လည်ရယူပါ", + "cold_or_recover_wallet": "Air-gapped cold wallet ထည့်ပါ သို့မဟုတ် paper wallet ကို ပြန်လည်ရယူပါ။", "collection_address": "စုဆောင်းရန် လိပ်စာ", "collection_description": "စုစည်းမှု ဖော်ပြချက်", "collection_name": "စုစည်းမှုအမည်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index f69dcc085..903399cb7 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -206,7 +206,7 @@ "coin_control": "Coin Control (optioneel)", "coin_control_desc": "Filter de outputs voor deze transactie. De wijzigingen die je hier aanbrengt, worden alleen toegepast op de huidige transactie.", "coin_control_load_failed": "Kan niet-uitgegeven munten niet laden", - "cold_or_recover_wallet": "Voeg een alleen-lezen wallet toe van Cupcake of een koude wallet of herstel een papieren wallet", + "cold_or_recover_wallet": "Voeg een air-gapped cold wallet toe of herstel een paper wallet.", "collection_address": "Verzameladres", "collection_description": "Verzamelingsbeschrijving", "collection_name": "Verzamelnaam", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 4df026b99..dec73dc17 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -208,7 +208,7 @@ "coin_control": "Kontrola monet (opcjonalna)", "coin_control_desc": "Filtruj wyjścia dla tej transakcji. Zmiany, które tu wprowadzisz, będą dotyczyć tylko bieżącej transakcji.", "coin_control_load_failed": "Nie udało się wczytać niewydanych monet", - "cold_or_recover_wallet": "Dodaj portfel tylko do odczytu z Cupcake albo zimny portfel lub odzyskaj portfel papierowy", + "cold_or_recover_wallet": "Dodaj air-gapped cold wallet lub odzyskaj paper wallet.", "collection_address": "Adres odbioru", "collection_description": "Opis kolekcji", "collection_name": "Nazwa kolekcji", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 3b2dc1bb7..5fd2359a0 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -208,7 +208,7 @@ "coin_control": "Controle de moedas (opcional)", "coin_control_desc": "Filtre as saídas para esta transação. As alterações que você fizer aqui só serão refletidas na transação atual.", "coin_control_load_failed": "Falha ao carregar moedas não gastas", - "cold_or_recover_wallet": "Adicione uma carteira somente leitura do Cupcake ou de uma carteira fria, ou recupere uma carteira de papel", + "cold_or_recover_wallet": "Adicione uma cold wallet air-gapped ou recupere uma paper wallet.", "collection_address": "Endereço de coleta", "collection_description": "Descrição da coleção", "collection_name": "Nome da coleção", diff --git a/res/values/strings_pt_BR.arb b/res/values/strings_pt_BR.arb index 1e178453e..c411aca23 100644 --- a/res/values/strings_pt_BR.arb +++ b/res/values/strings_pt_BR.arb @@ -207,7 +207,7 @@ "coin_control": "Controle de moedas (opcional)", "coin_control_desc": "Filtrar saídas para esta transação. As alterações feitas aqui refletirão apenas na transação atual.", "coin_control_load_failed": "Falha ao carregar moedas não gastas", - "cold_or_recover_wallet": "Adicionar uma carteira somente leitura do Cupcake, uma carteira fria ou recuperar uma carteira de papel", + "cold_or_recover_wallet": "Adicione uma cold wallet air-gapped ou recupere uma paper wallet.", "collection_address": "Endereço de coleta", "collection_description": "Descrição da coleção", "collection_name": "Nome da coleção", @@ -1371,4 +1371,4 @@ "zcash_card_missing_funds": "Fundos ausentes?", "zcash_card_scan": "Escanear", "zcash_card_warning": "Não feche o app até que o procedimento seja concluído; caso contrário, este processo precisará ser reiniciado do zero." -} \ No newline at end of file +} diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 1b8ec615d..0d62647f7 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -208,7 +208,7 @@ "coin_control": "Контроль монет (необязательно)", "coin_control_desc": "Отфильтруйте выходы для этой транзакции. Изменения, которые вы внесёте здесь, отразятся только на текущей транзакции.", "coin_control_load_failed": "Не удалось загрузить неизрасходованные монеты", - "cold_or_recover_wallet": "Добавить кошелёк только для чтения из Cupcake, холодный кошелёк или восстановить бумажный кошелёк", + "cold_or_recover_wallet": "Добавьте изолированный холодный кошелёк или восстановите бумажный кошелёк.", "collection_address": "Адрес для сбора", "collection_description": "Описание коллекции", "collection_name": "Название коллекции", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 71061321a..1fbf7d673 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -208,7 +208,7 @@ "coin_control": "การควบคุมเหรียญ (ไม่บังคับ)", "coin_control_desc": "กรองเอาต์พุตสำหรับธุรกรรมนี้ การเปลี่ยนแปลงที่คุณทำที่นี่จะมีผลกับธุรกรรมปัจจุบันเท่านั้น", "coin_control_load_failed": "ไม่สามารถโหลดเหรียญที่ยังไม่ได้ใช้จ่าย", - "cold_or_recover_wallet": "เพิ่มกระเป๋าเงินแบบอ่านอย่างเดียวจาก Cupcake หรือเพิ่มกระเป๋าเงินแบบ Cold Wallet หรือกู้คืนกระเป๋าเงินกระดาษ", + "cold_or_recover_wallet": "เพิ่ม cold wallet แบบ air-gapped หรือกู้คืน paper wallet", "collection_address": "ที่อยู่สำหรับรับเงิน", "collection_description": "คำอธิบายคอลเลกชัน", "collection_name": "ชื่อคอลเลกชัน", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 8d2ff2e92..afaad1d6d 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -208,7 +208,7 @@ "coin_control": "Kontrol ng coin (opsyonal)", "coin_control_desc": "I-filter ang mga output para sa transaksyong ito. Ang mga pagbabagong gagawin mo rito ay makikita lamang sa kasalukuyang transaksyon.", "coin_control_load_failed": "Nabigong i-load ang mga hindi nagastos na coin", - "cold_or_recover_wallet": "Magdagdag ng read-only na wallet mula sa Cupcake o cold wallet, o i-recover ang paper wallet", + "cold_or_recover_wallet": "Magdagdag ng air-gapped na cold wallet o i-recover ang isang paper wallet.", "collection_address": "Address ng Koleksyon", "collection_description": "Paglalarawan ng Koleksyon", "collection_name": "Pangalan ng Koleksyon", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 1ce2f60d8..3ce254f3d 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -208,7 +208,7 @@ "coin_control": "Coin kontrolü (isteğe bağlı)", "coin_control_desc": "Bu işlem için çıktıları filtreleyin. Burada yapacağınız değişiklikler yalnızca mevcut işleme yansıyacaktır.", "coin_control_load_failed": "Harcanmamış coin'ler yüklenemedi", - "cold_or_recover_wallet": "Cupcake’ten veya bir soğuk cüzdandan salt okunur bir cüzdan ekleyin ya da bir kâğıt cüzdanı kurtarın", + "cold_or_recover_wallet": "Hava boşluklu bir cold wallet ekleyin veya bir paper wallet kurtarın.", "collection_address": "Tahsilat Adresi", "collection_description": "Koleksiyon Açıklaması", "collection_name": "Koleksiyon Adı", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 515faa41b..0e6582989 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -208,7 +208,7 @@ "coin_control": "Контроль монет (необов’язково)", "coin_control_desc": "Відфільтрувати виходи для цієї транзакції. Зміни, які ви вносите тут, застосовуватимуться лише до поточної транзакції.", "coin_control_load_failed": "Не вдалося завантажити невитрачені монети", - "cold_or_recover_wallet": "Додайте гаманець лише для читання з Cupcake, холодного гаманця або відновіть паперовий гаманець", + "cold_or_recover_wallet": "Додайте ізольований холодний гаманець або відновіть паперовий гаманець.", "collection_address": "Адреса для збору", "collection_description": "Опис колекції", "collection_name": "Назва колекції", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 67be12dea..676c5e266 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -208,7 +208,7 @@ "coin_control": "کوائن کنٹرول (اختیاری)", "coin_control_desc": "اس لین دین کے لیے آؤٹ پٹس فلٹر کریں۔ آپ یہاں جو تبدیلیاں کریں گے وہ صرف موجودہ لین دین پر ہی لاگو ہوں گی۔", "coin_control_load_failed": "غیر خرچ شدہ سکے لوڈ کرنے میں ناکام", - "cold_or_recover_wallet": "Cupcake یا کولڈ والیٹ سے صرف پڑھنے کے لیے والیٹ شامل کریں یا پیپر والیٹ بازیافت کریں", + "cold_or_recover_wallet": "ایک ایئر-گیپڈ کولڈ والیٹ شامل کریں یا ایک پیپر والیٹ بازیاب کریں۔", "collection_address": "وصولی کا پتہ", "collection_description": "کلیکشن کی تفصیل", "collection_name": "کلیکشن کا نام", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index cf47aab46..4a921b4a1 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -206,7 +206,7 @@ "coin_control": "Kiểm soát coin (tùy chọn)", "coin_control_desc": "Lọc các đầu ra cho giao dịch này. Những thay đổi bạn thực hiện ở đây sẽ chỉ áp dụng cho giao dịch hiện tại.", "coin_control_load_failed": "Không thể tải các coin chưa sử dụng", - "cold_or_recover_wallet": "Thêm ví chỉ đọc từ Cupcake hoặc ví lạnh hoặc khôi phục ví giấy", + "cold_or_recover_wallet": "Thêm ví lạnh air-gapped hoặc khôi phục ví giấy.", "collection_address": "Địa chỉ thu tiền", "collection_description": "Mô tả bộ sưu tập", "collection_name": "Tên bộ sưu tập", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 3188cd5db..c46c4caf0 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -208,7 +208,7 @@ "coin_control": "Ìṣàkóso ẹyọ (àṣàyàn)", "coin_control_desc": "Àlẹmọ àwọn àbájáde (outputs) fún ìdúnàdúrà yìí. Àwọn àtúnṣe tí o ṣe níbí yóò kan ìdúnàdúrà lọ́wọ́lọ́wọ́ nìkan.", "coin_control_load_failed": "Kuna lati kojọpọ awọn owó tí a kò tíì ná", - "cold_or_recover_wallet": "Ṣafikun apamọwọ kika-nìkan láti Cupcake tàbí apamọwọ tútù, tàbí gba apamọwọ ìwé padà", + "cold_or_recover_wallet": "Ṣafikun air-gapped cold wallet tabi gba paper wallet padà.", "collection_address": "Adirẹsi ìkójọpọ̀", "collection_description": "Apejuwe akojọpọ", "collection_name": "Orúkọ Àkójọpọ̀", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 4a00f0a10..9a0956ac8 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -208,7 +208,7 @@ "coin_control": "币种控制(可选)", "coin_control_desc": "筛选此交易的输出。您在此处所做的更改仅会影响当前交易。", "coin_control_load_failed": "无法加载未花费的币", - "cold_or_recover_wallet": "添加来自 Cupcake 的只读钱包、冷钱包,或恢复纸钱包", + "cold_or_recover_wallet": "添加气隙冷钱包或恢复纸钱包。", "collection_address": "收款地址", "collection_description": "收藏描述", "collection_name": "收藏名称", diff --git a/res/values/strings_zh_tw.arb b/res/values/strings_zh_tw.arb index dd920063c..12c458ed8 100644 --- a/res/values/strings_zh_tw.arb +++ b/res/values/strings_zh_tw.arb @@ -172,7 +172,7 @@ "clearnet_link": "Clearnet 連結", "close": "關閉", "coin_control": "幣種控制(可選)", - "cold_or_recover_wallet": "新增來自 Cupcake 的唯讀錢包、冷錢包,或復原紙錢包", + "cold_or_recover_wallet": "新增氣隙冷錢包或復原紙錢包。", "collection_address": "收款地址", "collection_description": "收藏描述", "collection_name": "收藏名稱", @@ -1180,4 +1180,4 @@ "youCanGoBackToYourDapp": "您現在可以返回您的 dApp", "your": "您的", "yy": "YY" -} \ No newline at end of file +}