mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Merge branch 'cake-tech:dev' into dev
This commit is contained in:
@@ -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<Map<String, List<Map<String, dynamic>>>> getBatchUnspent(
|
||||
List<String> scriptHashes, {
|
||||
int timeout = 10000,
|
||||
}) async {
|
||||
final paramsList = scriptHashes.map((h) => <Object>[h]).toList(growable: false);
|
||||
|
||||
final batchResults = await callBatchWithTimeout(
|
||||
method: 'blockchain.scripthash.listunspent',
|
||||
paramsList: paramsList,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
final unspentMap = <String, List<Map<String, dynamic>>>{};
|
||||
|
||||
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<dynamic, dynamic>>()
|
||||
.map((m) => m.map((k, v) => MapEntry(k.toString(), v)))
|
||||
.cast<Map<String, dynamic>>()
|
||||
.toList();
|
||||
} else {
|
||||
unspentMap[sh] = const [];
|
||||
}
|
||||
}
|
||||
|
||||
return unspentMap;
|
||||
}
|
||||
|
||||
Future<Map<String, Map<String, dynamic>>> getBatchBalance(
|
||||
List<String> scriptHashes, {
|
||||
int timeout = 10000,
|
||||
}) async {
|
||||
final paramsList = scriptHashes.map((h) => <Object>[h]).toList(growable: false);
|
||||
|
||||
final batchResults = await callBatchWithTimeout(
|
||||
method: 'blockchain.scripthash.get_balance',
|
||||
paramsList: paramsList,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
final balanceMap = <String, Map<String, dynamic>>{};
|
||||
|
||||
for (int i = 0; i < scriptHashes.length; i++) {
|
||||
final sh = scriptHashes[i];
|
||||
|
||||
if (i >= batchResults.length) {
|
||||
balanceMap[sh] = <String, dynamic>{};
|
||||
continue;
|
||||
}
|
||||
|
||||
final result = batchResults[i];
|
||||
|
||||
if (result is Map<String, dynamic>) {
|
||||
balanceMap[sh] = result;
|
||||
} else if (result is Map) {
|
||||
balanceMap[sh] = Map<String, dynamic>.from(result);
|
||||
} else {
|
||||
balanceMap[sh] = <String, dynamic>{};
|
||||
}
|
||||
}
|
||||
|
||||
return balanceMap;
|
||||
}
|
||||
|
||||
Future<Map<String, Map<String, dynamic>>> getBatchTransactionVerbose(
|
||||
List<String> hashes, {
|
||||
int timeout = 10000,
|
||||
|
||||
+318
-156
@@ -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<List<List<BitcoinUnspent>?>> _fetchUnspentsRegular(
|
||||
List<BitcoinAddressRecord> addresses,
|
||||
) async {
|
||||
final addressFutures = addresses.map((address) => fetchUnspent(address)).toList();
|
||||
return Future.wait(addressFutures);
|
||||
}
|
||||
|
||||
|
||||
Future<List<List<BitcoinUnspent>?>> _fetchUnspentsBatch(
|
||||
List<BitcoinAddressRecord> addresses,
|
||||
) async {
|
||||
final byScriptHash = <String, BitcoinAddressRecord>{
|
||||
for (final address in addresses) address.getScriptHash(network): address,
|
||||
};
|
||||
|
||||
final scriptHashes = byScriptHash.keys.toList();
|
||||
|
||||
try {
|
||||
final unspentByScriptHash =
|
||||
await _processChunksToMap<String, String, List<Map<String, dynamic>>>(
|
||||
items: scriptHashes,
|
||||
chunkSize: addressHistoryChunkSize,
|
||||
processChunk: _getListUnspentBatch,
|
||||
);
|
||||
|
||||
final txHashes = <String>{};
|
||||
final coinsByScriptHash = <String, List<BitcoinUnspent>>{};
|
||||
|
||||
for (final entry in unspentByScriptHash.entries) {
|
||||
final addressRecord = byScriptHash[entry.key];
|
||||
if (addressRecord == null) continue;
|
||||
|
||||
final coins = <BitcoinUnspent>[];
|
||||
|
||||
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] ?? <BitcoinUnspent>[];
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
printV('fetchUnspentsBatch failed: $e');
|
||||
return List<List<BitcoinUnspent>?>.filled(addresses.length, null);
|
||||
}
|
||||
}
|
||||
|
||||
List<BitcoinUnspent> handleFailedUtxoFetch({
|
||||
required int failedCount,
|
||||
required List<BitcoinUnspent> previousUnspentCoins,
|
||||
@@ -2388,25 +2468,30 @@ abstract class ElectrumWalletBase
|
||||
@override
|
||||
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
|
||||
try {
|
||||
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
|
||||
final shouldUseBatchForHistory = useBatchForHistory && _isBatchSupported == true;
|
||||
final Map<String, ElectrumTransactionInfo> 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<void> fetchTransactionsForAddressTypeBatch(
|
||||
Map<String, ElectrumTransactionInfo> historiesWithDetails, BitcoinAddressType type) async {
|
||||
Map<String, ElectrumTransactionInfo> 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<BitcoinAddressRecord, String, ElectrumTransactionInfo>(
|
||||
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<void> fetchTransactionsForAddressesBranchBatch(
|
||||
Map<String, ElectrumTransactionInfo> historiesWithDetails,
|
||||
BitcoinAddressType type,
|
||||
List<BitcoinAddressRecord> 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<BitcoinAddressRecord, String, ElectrumTransactionInfo>(
|
||||
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<BitcoinAddressRecord> addresses) {
|
||||
for (int i = addresses.length - 1; i >= 0; i--) {
|
||||
if (addresses[i].isUsed) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
Future<Map<String, ElectrumTransactionInfo>> _fetchBatchAddressHistory(
|
||||
List<BitcoinAddressRecord> 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<Map<String, List<Map<String, dynamic>>>> _getListUnspentBatch(
|
||||
List<String> scriptHashes) {
|
||||
return electrumClient.getBatchUnspent(
|
||||
scriptHashes,
|
||||
timeout: transactionBatchTimeoutMs,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, Map<String, dynamic>>> _getBalanceBatch(
|
||||
List<String> scriptHashes) {
|
||||
return electrumClient.getBatchBalance(
|
||||
scriptHashes,
|
||||
timeout: transactionBatchTimeoutMs,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, ElectrumTransactionInfo?>> fetchTransactionInfoBatch({
|
||||
required List<String> hashes,
|
||||
Map<String, int?>? heightsByHash,
|
||||
@@ -2899,8 +3041,16 @@ abstract class ElectrumWalletBase
|
||||
|
||||
final inputTxIdsByHash = _collectInputTxIdsByHash(originalByHash);
|
||||
|
||||
final inputVerboseByTxId = await _fetchInputTransactionVerboseBatch(
|
||||
inputTxIdsByHash);
|
||||
final allInputTxids = <String>{};
|
||||
for (final txids in inputTxIdsByHash.values) {
|
||||
allInputTxids.addAll(txids);
|
||||
}
|
||||
|
||||
final inputTxIds = allInputTxids.toList(growable: false);
|
||||
|
||||
final inputVerboseByTxId = inputTxIds.isEmpty
|
||||
? <String, Map<String, dynamic>>{}
|
||||
: await _fetchTransactionVerboseBatch(inputTxIds);
|
||||
|
||||
final parsedInputTxById = _parseTransactions(inputVerboseByTxId);
|
||||
|
||||
@@ -2992,65 +3142,6 @@ abstract class ElectrumWalletBase
|
||||
return inputTxIdsByHash;
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, Map<String, dynamic>>> _fetchInputTransactionVerboseBatch(
|
||||
Map<String, List<String>> inputTxidsByHash) async {
|
||||
final allInputTxids = <String>{};
|
||||
for (final txids in inputTxidsByHash.values) {
|
||||
allInputTxids.addAll(txids);
|
||||
}
|
||||
|
||||
final inputTxIds = allInputTxids.toList(growable: false);
|
||||
|
||||
final verboseTransactionByHash =
|
||||
await _processChunksToMap<String, String, Map<String, dynamic>>(
|
||||
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 = <String>[];
|
||||
for (final txId in inputTxIds) {
|
||||
final vTx = verboseTransactionByHash[txId];
|
||||
if (vTx == null || vTx.isEmpty || vTx['hex'] == null) {
|
||||
emptyHex.add(txId);
|
||||
}
|
||||
}
|
||||
|
||||
final hexByHash = await _processChunksToMap<String, String, String?>(
|
||||
items: emptyHex,
|
||||
chunkSize: inputTransactionChunkSize,
|
||||
processChunk: _getTransactionHexBatch,
|
||||
);
|
||||
|
||||
for (final txId in inputTxIds) {
|
||||
final verbose = verboseTransactionByHash[txId] ?? <String, dynamic>{};
|
||||
if ((verbose['hex'] as String?) == null) {
|
||||
final hex = hexByHash[txId];
|
||||
if (hex != null && hex.isNotEmpty) {
|
||||
verboseTransactionByHash[txId] = {
|
||||
...verbose,
|
||||
'hex': hex,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return verboseTransactionByHash;
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, ElectrumTransactionBundle>> _buildTransactionBundlesBatch({
|
||||
required List<String> unique,
|
||||
required Map<String, int?>? heightsByHash,
|
||||
@@ -3283,18 +3374,64 @@ abstract class ElectrumWalletBase
|
||||
}));
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> fetchBalancesBatch(
|
||||
List<BitcoinAddressRecord> addresses,
|
||||
) async {
|
||||
final scriptHashes = addresses.map((address) => address.getScriptHash(network)).toList();
|
||||
|
||||
if (scriptHashes.isEmpty) {
|
||||
return <Map<String, dynamic>>[];
|
||||
}
|
||||
|
||||
try {
|
||||
final balancesByScriptHash =
|
||||
await _processChunksToMap<String, String, Map<String, dynamic>>(
|
||||
items: scriptHashes,
|
||||
chunkSize: addressHistoryChunkSize,
|
||||
processChunk: _getBalanceBatch,
|
||||
);
|
||||
|
||||
final balances = scriptHashes
|
||||
.map((scriptHash) => balancesByScriptHash[scriptHash] ?? <String, dynamic>{})
|
||||
.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<List<Map<String, dynamic>>> fetchBalancesRegular(
|
||||
List<BitcoinAddressRecord> addresses,
|
||||
) async {
|
||||
final balanceFutures = <Future<Map<String, dynamic>>>[];
|
||||
|
||||
for (final address in addresses) {
|
||||
final sh = address.getScriptHash(network);
|
||||
balanceFutures.add(electrumClient.getBalance(sh));
|
||||
}
|
||||
|
||||
return Future.wait(balanceFutures);
|
||||
}
|
||||
|
||||
Future<ElectrumBalance> fetchBalances() async {
|
||||
final addresses = walletAddresses.allAddresses
|
||||
.where((address) => address.address.isNotEmpty)
|
||||
.where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress)
|
||||
.toList();
|
||||
final balanceFutures = <Future<Map<String, dynamic>>>[];
|
||||
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<void> _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<void> 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<String, dynamic> &&
|
||||
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) {
|
||||
|
||||
@@ -715,38 +715,42 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
}
|
||||
|
||||
@action
|
||||
Future<bool> discoverAddressesBatch(
|
||||
Future<List<BitcoinAddressRecord>> discoverAddressesBatch(
|
||||
List<BitcoinAddressRecord> addressList,
|
||||
bool isHidden,
|
||||
Future<Set<String>> Function(List<BitcoinAddressRecord>) 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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
+35
-25
@@ -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<String, dynamic> 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<int> deleteAll() async {
|
||||
return await db!.delete(tableName, where: "isPow = ?", whereArgs: [false]);
|
||||
return await db!.delete(tableName, where: "isPow = ?", whereArgs: [0]);
|
||||
}
|
||||
|
||||
static Future<int> deleteAllPow() async {
|
||||
return await db!.delete(tableName, where: "isPow = ?", whereArgs: [true]);
|
||||
return await db!.delete(tableName, where: "isPow = ?", whereArgs: [1]);
|
||||
}
|
||||
|
||||
Future<int> save() async {
|
||||
@@ -173,35 +183,35 @@ class Node {
|
||||
|
||||
|
||||
static Future<List<Node>> getAll() async {
|
||||
return selectList("isPow = ?", [false]);
|
||||
return selectList("isPow = ?", [0]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllBuiltin() async {
|
||||
return selectList("isPow = ? AND isBuiltin = ?", [false, true]);
|
||||
return selectList("isPow = ? AND isBuiltin = ?", [0, 1]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllPowBuiltin() async {
|
||||
return selectList("isPow = ? AND isBuiltin = ?", [true, true]);
|
||||
return selectList("isPow = ? AND isBuiltin = ?", [1, 1]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllForWalletType(WalletType type) async {
|
||||
return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), false]);
|
||||
return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), 0]);
|
||||
}
|
||||
|
||||
static Future<Node?> 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<Node?> 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<List<Node>> getAllForWalletTypePow(WalletType type) async {
|
||||
return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), true]);
|
||||
return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), 1]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllPow() async {
|
||||
return selectList("isPow = ?", [true]);
|
||||
return selectList("isPow = ?", [1]);
|
||||
}
|
||||
|
||||
static Future<Node?> get(int id) async {
|
||||
@@ -475,7 +485,7 @@ class Node {
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
if (response.statusCode != 200 ||
|
||||
data["error"] != null ||
|
||||
|
||||
@@ -342,7 +342,7 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
setState(() {
|
||||
_addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
|
||||
(item) => (item is WalletAddressListItem && item.address == newAddress.address))
|
||||
as WalletAddressListItem;
|
||||
as WalletAddressListItem?;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -972,7 +972,7 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
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(
|
||||
|
||||
@@ -263,7 +263,7 @@ class _SwapAddressSelectionModalRowState extends State<SwapAddressSelectionModal
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Image.asset(widget.iconPath, width: 24, height: 24),
|
||||
CakeImageWidget(imageUrl: widget.iconPath, width: 24, height: 24),
|
||||
Text(widget.wallet.name)
|
||||
],
|
||||
),
|
||||
|
||||
@@ -46,11 +46,7 @@ static void my_application_activate(GApplication* application) {
|
||||
} else {
|
||||
gtk_window_set_title(window, "Cake Wallet");
|
||||
}
|
||||
if (getenv("DESKTOP_FORCE_MOBILE")) {
|
||||
gtk_window_set_default_size(window, 720, 1280);
|
||||
} else {
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
}
|
||||
gtk_window_set_default_size(window, 480, 800);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 8.4 KiB |
@@ -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": "اسم المجموعة",
|
||||
|
||||
@@ -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": "Име на колекцията",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "نام مجموعه",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "संग्रह का नाम",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Հավաքածուի անունը",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "コレクション名",
|
||||
|
||||
@@ -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": "컬렉션 이름",
|
||||
|
||||
@@ -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": "စုစည်းမှုအမည်",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Название коллекции",
|
||||
|
||||
@@ -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": "ชื่อคอลเลกชัน",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ı",
|
||||
|
||||
@@ -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": "Назва колекції",
|
||||
|
||||
@@ -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": "کلیکشن کا نام",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ọ̀",
|
||||
|
||||
@@ -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": "收藏名称",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user