diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index a18c038fa..df8e14119 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -124,6 +124,7 @@ class ElectrumClient { final errorMsg = error.toString(); print(errorMsg); unterminatedString = ''; + socket = null; }, onDone: () { print("SOCKET CLOSED!!!!!"); @@ -132,6 +133,7 @@ class ElectrumClient { if (host == socket?.address.host || socket == null) { _setConnectionStatus(ConnectionStatus.disconnected); socket?.destroy(); + socket = null; } } catch (e) { print("onDone: $e"); diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 4e37f40b1..ebd2f06ae 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -24,9 +24,12 @@ class ElectrumBalance extends Balance { final decoded = json.decode(jsonSource) as Map; return ElectrumBalance( - confirmed: decoded['confirmed'] as int? ?? 0, - unconfirmed: decoded['unconfirmed'] as int? ?? 0, - frozen: decoded['frozen'] as int? ?? 0); + confirmed: decoded['confirmed'] as int? ?? 0, + unconfirmed: decoded['unconfirmed'] as int? ?? 0, + frozen: decoded['frozen'] as int? ?? 0, + secondConfirmed: decoded['secondConfirmed'] as int? ?? 0, + secondUnconfirmed: decoded['secondUnconfirmed'] as int? ?? 0, + ); } int confirmed; @@ -36,8 +39,7 @@ class ElectrumBalance extends Balance { int secondUnconfirmed = 0; @override - String get formattedAvailableBalance => - bitcoinAmountToString(amount: confirmed - frozen); + String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen); @override String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 1ab7799e3..7a8b3b951 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -41,6 +41,7 @@ class ElectrumTransactionInfo extends TransactionInfo { String? to, this.unspents, this.isReceivedSilentPayment = false, + Map<String, dynamic>? additionalInfo, }) { this.id = id; this.height = height; @@ -54,6 +55,7 @@ class ElectrumTransactionInfo extends TransactionInfo { this.isReplaced = isReplaced; this.confirmations = confirmations; this.to = to; + this.additionalInfo = additionalInfo ?? {}; } factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type, @@ -212,6 +214,7 @@ class ElectrumTransactionInfo extends TransactionInfo { BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map<String, dynamic>)) .toList(), isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false, + additionalInfo: data['additionalInfo'] as Map<String, dynamic>?, ); } @@ -246,7 +249,8 @@ class ElectrumTransactionInfo extends TransactionInfo { isReplaced: isReplaced ?? false, inputAddresses: inputAddresses, outputAddresses: outputAddresses, - confirmations: info.confirmations); + confirmations: info.confirmations, + additionalInfo: additionalInfo); } Map<String, dynamic> toJson() { @@ -265,10 +269,11 @@ class ElectrumTransactionInfo extends TransactionInfo { m['inputAddresses'] = inputAddresses; m['outputAddresses'] = outputAddresses; m['isReceivedSilentPayment'] = isReceivedSilentPayment; + m['additionalInfo'] = additionalInfo; return m; } String toString() { - return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)'; + return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses, additionalInfo: $additionalInfo)'; } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index c05095cf1..7988a4479 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -5,6 +5,7 @@ import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; @@ -52,10 +53,9 @@ part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; -abstract class ElectrumWalletBase extends WalletBase< - ElectrumBalance, - ElectrumTransactionHistory, - ElectrumTransactionInfo> with Store, WalletKeysFile { +abstract class ElectrumWalletBase + extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo> + with Store, WalletKeysFile { ElectrumWalletBase({ required String password, required WalletInfo walletInfo, @@ -71,8 +71,8 @@ abstract class ElectrumWalletBase extends WalletBase< ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, - }) : accountHD = getAccountHDWallet( - currency, network, seedBytes, xpub, walletInfo.derivationInfo), + }) : accountHD = + getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = <int>[], @@ -107,12 +107,8 @@ abstract class ElectrumWalletBase extends WalletBase< sharedPrefs.complete(SharedPreferences.getInstance()); } - static Bip32Slip10Secp256k1 getAccountHDWallet( - CryptoCurrency? currency, - BasedUtxoNetwork network, - Uint8List? seedBytes, - String? xpub, - DerivationInfo? derivationInfo) { + static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, + Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { if (seedBytes == null && xpub == null) { throw Exception( "To create a Wallet you need either a seed or an xpub. This should not happen"); @@ -123,9 +119,8 @@ abstract class ElectrumWalletBase extends WalletBase< case CryptoCurrency.btc: case CryptoCurrency.ltc: case CryptoCurrency.tbtc: - return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)) - .derivePath(_hardenedDerivationPath( - derivationInfo?.derivationPath ?? electrum_path)) + return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)).derivePath( + _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)) as Bip32Slip10Secp256k1; case CryptoCurrency.bch: return bitcoinCashHDWallet(seedBytes); @@ -134,13 +129,11 @@ abstract class ElectrumWalletBase extends WalletBase< } } - return Bip32Slip10Secp256k1.fromExtendedKey( - xpub!, getKeyNetVersion(network)); + return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network)); } static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) => - Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") - as Bip32Slip10Secp256k1; + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1; static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; @@ -250,7 +243,7 @@ abstract class ElectrumWalletBase extends WalletBase< } if (tip > walletInfo.restoreHeight) { - _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip); + _setListeners(walletInfo.restoreHeight, chainTipParam: currentChainTip); } } else { alwaysScan = false; @@ -265,23 +258,23 @@ abstract class ElectrumWalletBase extends WalletBase< } } - int? _currentChainTip; + int? currentChainTip; Future<int> getCurrentChainTip() async { - if ((_currentChainTip ?? 0) > 0) { - return _currentChainTip!; + if ((currentChainTip ?? 0) > 0) { + return currentChainTip!; } - _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; + currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; - return _currentChainTip!; + return currentChainTip!; } Future<int> getUpdatedChainTip() async { final newTip = await electrumClient.getCurrentBlockChainTip(); - if (newTip != null && newTip > (_currentChainTip ?? 0)) { - _currentChainTip = newTip; + if (newTip != null && newTip > (currentChainTip ?? 0)) { + currentChainTip = newTip; } - return _currentChainTip ?? 0; + return currentChainTip ?? 0; } @override @@ -357,7 +350,7 @@ abstract class ElectrumWalletBase extends WalletBase< isSingleScan: doSingleScan ?? false, )); - _receiveStream?.cancel(); + await _receiveStream?.cancel(); _receiveStream = receivePort.listen((var message) async { if (message is Map<String, ElectrumTransactionInfo>) { for (final map in message.entries) { @@ -618,7 +611,7 @@ abstract class ElectrumWalletBase extends WalletBase< bool spendsUnconfirmedTX = false; int leftAmount = credentialsAmount; - final availableInputs = unspentCoins.where((utx) { + var availableInputs = unspentCoins.where((utx) { if (!utx.isSending || utx.isFrozen) { return false; } @@ -634,6 +627,9 @@ abstract class ElectrumWalletBase extends WalletBase< }).toList(); final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList(); + // sort the unconfirmed coins so that mweb coins are first: + availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? -1 : 1); + for (int i = 0; i < availableInputs.length; i++) { final utx = availableInputs[i]; if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; @@ -652,9 +648,8 @@ abstract class ElectrumWalletBase extends WalletBase< ECPrivate? privkey; bool? isSilentPayment = false; - final hd = utx.bitcoinAddressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd; + final hd = + utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; @@ -1233,8 +1228,7 @@ abstract class ElectrumWalletBase extends WalletBase< } } - void setLedgerConnection(ledger.LedgerConnection connection) => - throw UnimplementedError(); + void setLedgerConnection(ledger.LedgerConnection connection) => throw UnimplementedError(); Future<BtcTransaction> buildHardwareWalletTransaction({ required List<BitcoinBaseOutput> outputs, @@ -1593,9 +1587,7 @@ abstract class ElectrumWalletBase extends WalletBase< final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network); final privkey = generateECPrivate( - hd: addressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd, + hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, index: addressRecord.index, network: network); @@ -1777,8 +1769,7 @@ abstract class ElectrumWalletBase extends WalletBase< if (height != null) { if (time == null && height > 0) { - time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000) - .round(); + time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); } if (confirmations == null) { @@ -1847,6 +1838,7 @@ abstract class ElectrumWalletBase extends WalletBase< .map((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))); } @@ -1958,6 +1950,20 @@ abstract class ElectrumWalletBase extends WalletBase< // 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; + } + } + } transactionHistory.addOne(tx); await transactionHistory.save(); } @@ -1984,18 +1990,28 @@ abstract class ElectrumWalletBase extends WalletBase< if (_isTransactionUpdating) { return; } - await getCurrentChainTip(); + currentChainTip = await getUpdatedChainTip(); + bool updated = false; transactionHistory.transactions.values.forEach((tx) { - if (tx.unspents != null && - tx.unspents!.isNotEmpty && - tx.height != null && - tx.height! > 0 && - (_currentChainTip ?? 0) > 0) { - tx.confirmations = _currentChainTip! - tx.height! + 1; + if ((tx.height ?? 0) > 0 && (currentChainTip ?? 0) > 0) { + var confirmations = currentChainTip! - tx.height! + 1; + if (confirmations < 0) { + // if our chain tip is outdated then it could lead to negative confirmations so this is just a failsafe: + confirmations = 0; + } + if (confirmations != tx.confirmations) { + updated = true; + tx.confirmations = confirmations; + transactionHistory.addOne(tx); + } } }); + if (updated) { + await transactionHistory.save(); + } + _isTransactionUpdating = true; await fetchTransactions(); walletAddresses.updateReceiveAddresses(); @@ -2043,6 +2059,8 @@ abstract class ElectrumWalletBase extends WalletBase< library: this.runtimeType.toString(), )); } + }, onError: (e, s) { + print("sub_listen error: $e $s"); }); })); } @@ -2092,6 +2110,13 @@ abstract class ElectrumWalletBase extends WalletBase< 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: + print("got null balance responses from the server, setting connection status to lost"); + syncStatus = LostConnectionSyncStatus(); + return balance[currency] ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); + } + for (var i = 0; i < balances.length; i++) { final addressRecord = addresses[i]; final balance = balances[i]; @@ -2197,10 +2222,10 @@ abstract class ElectrumWalletBase extends WalletBase< Future<void> _setInitialHeight() async { if (_chainTipUpdateSubject != null) return; - _currentChainTip = await getUpdatedChainTip(); + currentChainTip = await getUpdatedChainTip(); - if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) { - await walletInfo.updateRestoreHeight(_currentChainTip!); + if ((currentChainTip == null || currentChainTip! == 0) && walletInfo.restoreHeight == 0) { + await walletInfo.updateRestoreHeight(currentChainTip!); } _chainTipUpdateSubject = electrumClient.chainTipSubscribe(); @@ -2209,7 +2234,7 @@ abstract class ElectrumWalletBase extends WalletBase< final height = int.tryParse(event['height'].toString()); if (height != null) { - _currentChainTip = height; + currentChainTip = height; if (alwaysScan == true && syncStatus is SyncedSyncStatus) { _setListeners(walletInfo.restoreHeight); @@ -2223,7 +2248,6 @@ abstract class ElectrumWalletBase extends WalletBase< @action void _onConnectionStatusChange(ConnectionStatus status) { - switch (status) { case ConnectionStatus.connected: if (syncStatus is NotConnectedSyncStatus || @@ -2270,8 +2294,6 @@ abstract class ElectrumWalletBase extends WalletBase< Timer(Duration(seconds: 5), () { if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) { - if (node == null) return; - this.electrumClient.connectToUri( node!.uri, useSSL: node!.useSSL ?? false, diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 1fb39c878..86228fc83 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -9,6 +9,7 @@ import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; +import 'package:cw_core/node.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -47,6 +48,7 @@ import 'package:cw_mweb/cw_mweb.dart'; import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart'; import 'package:pointycastle/ecc/api.dart'; import 'package:pointycastle/ecc/curves/secp256k1.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'litecoin_wallet.g.dart'; @@ -85,8 +87,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { alwaysScan: alwaysScan, ) { if (seedBytes != null) { - mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( - "m/1000'") as Bip32Slip10Secp256k1; + mwebHd = + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; mwebEnabled = alwaysScan ?? false; } else { mwebHd = null; @@ -287,6 +289,16 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020); } + @action + @override + Future<void> connectToNode({required Node node}) async { + await super.connectToNode(node: node); + + final prefs = await SharedPreferences.getInstance(); + final mwebNodeUri = prefs.getString("mwebNodeUri") ?? "ltc-electrum.cakewallet.com:9333"; + await CwMweb.setNodeUriOverride(mwebNodeUri); + } + @action @override Future<void> startSync() async { @@ -349,6 +361,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return; } + // update the current chain tip so that confirmation calculations are accurate: + currentChainTip = nodeHeight; + final resp = await CwMweb.status(StatusRequest()); try { @@ -361,22 +376,46 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } else if (resp.mwebUtxosHeight < nodeHeight) { mwebSyncStatus = SyncingSyncStatus(1, 0.999); } else { + bool confirmationsUpdated = false; if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); await checkMwebUtxosSpent(); // update the confirmations for each transaction: - for (final transaction in transactionHistory.transactions.values) { - if (transaction.isPending) continue; - int txHeight = transaction.height ?? resp.mwebUtxosHeight; - final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; - if (transaction.confirmations == confirmations) continue; - if (transaction.confirmations == 0) { - updateBalance(); + for (final tx in transactionHistory.transactions.values) { + if (tx.height == null || tx.height == 0) { + // update with first confirmation on next block since it hasn't been confirmed yet: + tx.height = resp.mwebUtxosHeight; + continue; } - transaction.confirmations = confirmations; - transactionHistory.addOne(transaction); + + final confirmations = (resp.mwebUtxosHeight - tx.height!) + 1; + + // if the confirmations haven't changed, skip updating: + if (tx.confirmations == confirmations) continue; + + + // if an outgoing tx is now confirmed, delete the utxo from the box (delete the unspent coin): + if (confirmations >= 2 && + tx.direction == TransactionDirection.outgoing && + tx.unspents != null) { + for (var coin in tx.unspents!) { + final utxo = mwebUtxosBox.get(coin.address); + if (utxo != null) { + print("deleting utxo ${coin.address} @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + await mwebUtxosBox.delete(coin.address); + } + } + } + + tx.confirmations = confirmations; + tx.isPending = false; + transactionHistory.addOne(tx); + confirmationsUpdated = true; + } + if (confirmationsUpdated) { + await transactionHistory.save(); + await updateTransactions(); } - await transactionHistory.save(); } // prevent unnecessary reaction triggers: @@ -501,13 +540,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { outputAddresses: [utxo.outputId], isReplaced: false, ); - } - - // don't update the confirmations if the tx is updated by electrum: - if (tx.confirmations == 0 || utxo.height != 0) { - tx.height = utxo.height; - tx.isPending = utxo.height == 0; - tx.confirmations = confirmations; + } else { + if (tx.confirmations != confirmations || tx.height != utxo.height) { + tx.height = utxo.height; + tx.confirmations = confirmations; + tx.isPending = utxo.height == 0; + } } bool isNew = transactionHistory.transactions[tx.id] == null; @@ -557,56 +595,88 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (responseStream == null) { throw Exception("failed to get utxos stream!"); } - _utxoStream = responseStream.listen((Utxo sUtxo) async { - // we're processing utxos, so our balance could still be innacurate: - if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) { - mwebSyncStatus = SyncronizingSyncStatus(); - processingUtxos = true; - _processingTimer?.cancel(); - _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { - processingUtxos = false; - timer.cancel(); - }); - } - - final utxo = MwebUtxo( - address: sUtxo.address, - blockTime: sUtxo.blockTime, - height: sUtxo.height, - outputId: sUtxo.outputId, - value: sUtxo.value.toInt(), - ); - - if (mwebUtxosBox.containsKey(utxo.outputId)) { - // we've already stored this utxo, skip it: - // but do update the utxo height if it's somehow different: - final existingUtxo = mwebUtxosBox.get(utxo.outputId); - if (existingUtxo!.height != utxo.height) { - print( - "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}"); - existingUtxo.height = utxo.height; - await mwebUtxosBox.put(utxo.outputId, existingUtxo); + _utxoStream = responseStream.listen( + (Utxo sUtxo) async { + // we're processing utxos, so our balance could still be innacurate: + if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) { + mwebSyncStatus = SyncronizingSyncStatus(); + processingUtxos = true; + _processingTimer?.cancel(); + _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { + processingUtxos = false; + timer.cancel(); + }); } - return; - } - await updateUnspent(); - await updateBalance(); + final utxo = MwebUtxo( + address: sUtxo.address, + blockTime: sUtxo.blockTime, + height: sUtxo.height, + outputId: sUtxo.outputId, + value: sUtxo.value.toInt(), + ); - final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + if (mwebUtxosBox.containsKey(utxo.outputId)) { + // we've already stored this utxo, skip it: + // but do update the utxo height if it's somehow different: + final existingUtxo = mwebUtxosBox.get(utxo.outputId); + if (existingUtxo!.height != utxo.height) { + print( + "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}"); + existingUtxo.height = utxo.height; + await mwebUtxosBox.put(utxo.outputId, existingUtxo); + } + return; + } - // don't process utxos with addresses that are not in the mwebAddrs list: - if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) { - return; - } + await updateUnspent(); + await updateBalance(); - await mwebUtxosBox.put(utxo.outputId, utxo); + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; - await handleIncoming(utxo); - }); + // don't process utxos with addresses that are not in the mwebAddrs list: + if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) { + return; + } + + await mwebUtxosBox.put(utxo.outputId, utxo); + + await handleIncoming(utxo); + }, + onError: (error) { + print("error in utxo stream: $error"); + mwebSyncStatus = FailedSyncStatus(error: error.toString()); + }, + cancelOnError: true, + ); + } + + Future<void> deleteSpentUtxos() async { + print("deleteSpentUtxos() called!"); + final chainHeight = await electrumClient.getCurrentBlockChainTip(); + final status = await CwMweb.status(StatusRequest()); + if (chainHeight == null || status.blockHeaderHeight != chainHeight) return; + if (status.mwebUtxosHeight != chainHeight) return; // we aren't synced + + // delete any spent utxos with >= 2 confirmations: + final spentOutputIds = mwebUtxosBox.values + .where((utxo) => utxo.spent && (chainHeight - utxo.height) >= 2) + .map((utxo) => utxo.outputId) + .toList(); + + if (spentOutputIds.isEmpty) return; + + final resp = await CwMweb.spent(SpentRequest(outputId: spentOutputIds)); + final spent = resp.outputId; + if (spent.isEmpty) return; + + for (final outputId in spent) { + await mwebUtxosBox.delete(outputId); + } } Future<void> checkMwebUtxosSpent() async { + print("checkMwebUtxosSpent() called!"); if (!mwebEnabled) { return; } @@ -620,15 +690,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { updatedAny = await isConfirmed(tx) || updatedAny; } + await deleteSpentUtxos(); + // get output ids of all the mweb utxos that have > 0 height: - final outputIds = - mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList(); + final outputIds = mwebUtxosBox.values + .where((utxo) => utxo.height > 0 && !utxo.spent) + .map((utxo) => utxo.outputId) + .toList(); final resp = await CwMweb.spent(SpentRequest(outputId: outputIds)); final spent = resp.outputId; - if (spent.isEmpty) { - return; - } + if (spent.isEmpty) return; final status = await CwMweb.status(StatusRequest()); final height = await electrumClient.getCurrentBlockChainTip(); @@ -739,7 +811,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mwebUtxosBox.keys.forEach((dynamic oId) { final String outputId = oId as String; final utxo = mwebUtxosBox.get(outputId); - if (utxo == null) { + if (utxo == null || utxo.spent) { return; } if (utxo.address.isEmpty) { @@ -789,15 +861,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { int unconfirmedMweb = 0; try { mwebUtxosBox.values.forEach((utxo) { - if (utxo.height > 0) { + bool isConfirmed = utxo.height > 0; + + print( + "utxo: ${isConfirmed ? "confirmed" : "unconfirmed"} ${utxo.spent ? "spent" : "unspent"} ${utxo.outputId} ${utxo.height} ${utxo.value}"); + + if (isConfirmed) { confirmedMweb += utxo.value.toInt(); - } else { + } + + if (isConfirmed && utxo.spent) { + unconfirmedMweb -= utxo.value.toInt(); + } + + if (!isConfirmed && !utxo.spent) { unconfirmedMweb += utxo.value.toInt(); } }); - if (unconfirmedMweb > 0) { - unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb); - } } catch (_) {} for (var addressRecord in walletAddresses.allAddresses) { @@ -829,7 +909,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // update the txCount for each address using the tx history, since we can't rely on mwebd // to have an accurate count, we should just keep it in sync with what we know from the tx history: for (final tx in transactionHistory.transactions.values) { - // if (tx.isPending) continue; if (tx.inputAddresses == null || tx.outputAddresses == null) { continue; } @@ -908,7 +987,26 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation final preOutputSum = outputs.fold<BigInt>(BigInt.zero, (acc, output) => acc + output.toOutput.amount); - final fee = utxos.sumOfUtxosValue() - preOutputSum; + var fee = utxos.sumOfUtxosValue() - preOutputSum; + + // determines if the fee is correct: + BigInt _sumOutputAmounts(List<TxOutput> outputs) { + BigInt sum = BigInt.zero; + for (final e in outputs) { + sum += e.amount; + } + return sum; + } + + final sum1 = _sumOutputAmounts(outputs.map((e) => e.toOutput).toList()) + fee; + final sum2 = utxos.sumOfUtxosValue(); + if (sum1 != sum2) { + print("@@@@@ WE HAD TO ADJUST THE FEE! @@@@@@@@"); + final diff = sum2 - sum1; + // add the difference to the fee (abs value): + fee += diff.abs(); + } + final txb = BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network); final resp = await CwMweb.create(CreateRequest( @@ -949,8 +1047,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { tx.changeAddressOverride = - (await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(isPegIn: false)) + (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false)) .address; return tx; } @@ -969,15 +1066,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { bool hasMwebInput = false; bool hasMwebOutput = false; + bool hasRegularOutput = false; for (final output in transactionCredentials.outputs) { - if (output.extractedAddress?.toLowerCase().contains("mweb") ?? false) { + final address = output.address.toLowerCase(); + final extractedAddress = output.extractedAddress?.toLowerCase(); + + if (address.contains("mweb")) { hasMwebOutput = true; - break; } - if (output.address.toLowerCase().contains("mweb")) { - hasMwebOutput = true; - break; + if (!address.contains("mweb")) { + hasRegularOutput = true; + } + if (extractedAddress != null && extractedAddress.isNotEmpty) { + if (extractedAddress.contains("mweb")) { + hasMwebOutput = true; + } + if (!extractedAddress.contains("mweb")) { + hasRegularOutput = true; + } } } @@ -989,11 +1096,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } bool isPegIn = !hasMwebInput && hasMwebOutput; + bool isPegOut = hasMwebInput && hasRegularOutput; bool isRegular = !hasMwebInput && !hasMwebOutput; - tx.changeAddressOverride = - (await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(isPegIn: isPegIn || isRegular)) - .address; + tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: isPegIn || isRegular)) + .address; if (!hasMwebInput && !hasMwebOutput) { tx.isMweb = false; return tx; @@ -1046,8 +1153,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final addresses = <String>{}; transaction.inputAddresses?.forEach((id) async { final utxo = mwebUtxosBox.get(id); - await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent + // await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent if (utxo == null) return; + // mark utxo as spent so we add it to the unconfirmed balance (as negative): + utxo.spent = true; + await mwebUtxosBox.put(id, utxo); final addressRecord = walletAddresses.allAddresses .firstWhere((addressRecord) => addressRecord.address == utxo.address); if (!addresses.contains(utxo.address)) { @@ -1056,7 +1166,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { addressRecord.balance -= utxo.value.toInt(); }); transaction.inputAddresses?.addAll(addresses); - + print("isPegIn: $isPegIn, isPegOut: $isPegOut"); + transaction.additionalInfo["isPegIn"] = isPegIn; + transaction.additionalInfo["isPegOut"] = isPegOut; transactionHistory.addOne(transaction); await updateUnspent(); await updateBalance(); @@ -1240,8 +1352,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override void setLedgerConnection(LedgerConnection connection) { _ledgerConnection = connection; - _litecoinLedgerApp = - LitecoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + _litecoinLedgerApp = LitecoinLedgerApp(_ledgerConnection!, + derivationPath: walletInfo.derivationInfo!.derivationPath!); } @override @@ -1277,19 +1389,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath; } - final rawHex = await _litecoinLedgerApp!.createTransaction( - inputs: readyInputs, - outputs: outputs - .map((e) => TransactionOutput.fromBigInt( - (e as BitcoinOutput).value, Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) - .toList(), - changePath: changePath, - sigHashType: 0x01, - additionals: ["bech32"], - isSegWit: true, - useTrustedInputForSegwit: true - ); + inputs: readyInputs, + outputs: outputs + .map((e) => TransactionOutput.fromBigInt((e as BitcoinOutput).value, + Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) + .toList(), + changePath: changePath, + sigHashType: 0x01, + additionals: ["bech32"], + isSegWit: true, + useTrustedInputForSegwit: true); return BtcTransaction.fromRaw(rawHex); } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index c55f5fc76..062c590ba 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -16,11 +16,9 @@ import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; -class LitecoinWalletAddresses = LitecoinWalletAddressesBase - with _$LitecoinWalletAddresses; +class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses; -abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses - with Store { +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { LitecoinWalletAddressesBase( WalletInfo walletInfo, { required super.mainHd, @@ -46,8 +44,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses List<String> mwebAddrs = []; bool generating = false; - List<int> get scanSecret => - mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List<int> get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List<int> get spendPubkey => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @@ -203,4 +200,12 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses return super.getChangeAddress(); } + + @override + String get addressForExchange { + // don't use mweb addresses for exchange refund address: + final addresses = receiveAddresses + .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); + return addresses.first.address; + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 7cc266f5b..d519f4d0a 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -112,6 +112,7 @@ class LitecoinWalletService extends WalletService< File neturinoDb = File('$appDirPath/neutrino.db'); File blockHeaders = File('$appDirPath/block_headers.bin'); File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin'); + File mwebdLogs = File('$appDirPath/logs/debug.log'); if (neturinoDb.existsSync()) { neturinoDb.deleteSync(); } @@ -121,6 +122,9 @@ class LitecoinWalletService extends WalletService< if (regFilterHeaders.existsSync()) { regFilterHeaders.deleteSync(); } + if (mwebdLogs.existsSync()) { + mwebdLogs.deleteSync(); + } } } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 5ed84dbf4..140965655 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -118,8 +118,7 @@ class PendingBitcoinTransaction with PendingTransaction { Future<void> _ltcCommit() async { try { - final stub = await CwMweb.stub(); - final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); + final resp = await CwMweb.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); idOverride = resp.txid; } on GrpcError catch (e) { throw BitcoinTransactionCommitFailed(errorMessage: e.message); diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 9f1cee67d..7a1576a98 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -64,7 +64,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v8 + ref: cake-update-v9 pointycastle: 3.7.4 ffi: 2.1.0 diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index cd1e52f51..14ff407e2 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -42,7 +42,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v8 + ref: cake-update-v9 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_core/lib/mweb_utxo.dart b/cw_core/lib/mweb_utxo.dart index f8dfab395..5eab11734 100644 --- a/cw_core/lib/mweb_utxo.dart +++ b/cw_core/lib/mweb_utxo.dart @@ -11,6 +11,7 @@ class MwebUtxo extends HiveObject { required this.address, required this.outputId, required this.blockTime, + this.spent = false, }); static const typeId = MWEB_UTXO_TYPE_ID; @@ -30,4 +31,7 @@ class MwebUtxo extends HiveObject { @HiveField(4) int blockTime; + + @HiveField(5, defaultValue: false) + bool spent; } diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index e19d2a54b..1094b6402 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -79,6 +79,9 @@ class Node extends HiveObject with Keyable { @HiveField(9) bool? supportsSilentPayments; + @HiveField(10) + bool? supportsMweb; + bool get isSSL => useSSL ?? false; bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty; diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index 9d0c968d8..3e34af75f 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -25,6 +25,5 @@ abstract class TransactionInfo extends Object with Keyable { @override dynamic get keyIndex => id; - late Map<String, dynamic> additionalInfo; + Map<String, dynamic> additionalInfo = {}; } - diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index a1a592fb8..75cc0bcca 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -13,8 +13,18 @@ class CwMweb { static RpcClient? _rpcClient; static ClientChannel? _clientChannel; static int? _port; - static const TIMEOUT_DURATION = Duration(seconds: 5); + static const TIMEOUT_DURATION = Duration(seconds: 15); static Timer? logTimer; + static String? nodeUriOverride; + + + static Future<void> setNodeUriOverride(String uri) async { + nodeUriOverride = uri; + if (_rpcClient != null) { + await stop(); + // will be re-started automatically when the next rpc call is made + } + } static void readFileWithTimer(String filePath) { final file = File(filePath); @@ -47,7 +57,7 @@ class CwMweb { String debugLogPath = "${appDir.path}/logs/debug.log"; readFileWithTimer(debugLogPath); - _port = await CwMwebPlatform.instance.start(appDir.path, ltcNodeUri); + _port = await CwMwebPlatform.instance.start(appDir.path, nodeUriOverride ?? ltcNodeUri); if (_port == null || _port == 0) { throw Exception("Failed to start server"); } @@ -197,4 +207,18 @@ class CwMweb { } return null; } + + static Future<BroadcastResponse> broadcast(BroadcastRequest request) async { + log("mweb.broadcast() called"); + try { + _rpcClient = await stub(); + return await _rpcClient!.broadcast(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + throw "error from broadcast mweb: $e"; + } catch (e) { + log("Error getting create: $e"); + rethrow; + } + } } diff --git a/lib/di.dart b/lib/di.dart index 13ffd839e..14fdbd56d 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -35,6 +35,8 @@ import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; @@ -945,6 +947,10 @@ Future<void> setup({ getIt.registerFactory(() => MwebSettingsPage(getIt.get<MwebSettingsViewModel>())); + getIt.registerFactory(() => MwebLogsPage(getIt.get<MwebSettingsViewModel>())); + + getIt.registerFactory(() => MwebNodePage(getIt.get<MwebSettingsViewModel>())); + getIt.registerFactory(() => OtherSettingsPage(getIt.get<OtherSettingsViewModel>())); getIt.registerFactory(() => NanoChangeRepPage( diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 73556452c..8e8bfb6ed 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -54,6 +54,7 @@ class PreferencesKey { static const mwebEnabled = 'mwebEnabled'; static const hasEnabledMwebBefore = 'hasEnabledMwebBefore'; static const mwebAlwaysScan = 'mwebAlwaysScan'; + static const mwebNodeUri = 'mwebNodeUri'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const shouldShowRepWarning = 'should_show_rep_warning'; diff --git a/lib/router.dart b/lib/router.dart index 3b4c38546..6c234ff80 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -72,6 +72,8 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart'; import 'package:cake_wallet/src/screens/settings/mweb_settings.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; @@ -461,6 +463,14 @@ Route<dynamic> createRoute(RouteSettings settings) { return CupertinoPageRoute<void>( fullscreenDialog: true, builder: (_) => getIt.get<MwebSettingsPage>()); + case Routes.mwebLogs: + return CupertinoPageRoute<void>( + fullscreenDialog: true, builder: (_) => getIt.get<MwebLogsPage>()); + + case Routes.mwebNode: + return CupertinoPageRoute<void>( + fullscreenDialog: true, builder: (_) => getIt.get<MwebNodePage>()); + case Routes.connectionSync: return CupertinoPageRoute<void>( fullscreenDialog: true, builder: (_) => getIt.get<ConnectionSyncPage>()); diff --git a/lib/routes.dart b/lib/routes.dart index 0529d7c6f..5f11b11a3 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -74,6 +74,8 @@ class Routes { static const webViewPage = '/web_view_page'; static const silentPaymentsSettings = '/silent_payments_settings'; static const mwebSettings = '/mweb_settings'; + static const mwebLogs = '/mweb_logs'; + static const mwebNode = '/mweb_node'; static const connectionSync = '/connection_sync_page'; static const securityBackupPage = '/security_and_backup_page'; static const privacyPage = '/privacy_page'; diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 028b3cfcf..b16a7b090 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -886,17 +886,37 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - '${secondAvailableBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension<BalancePageTheme>()! - .labelTextColor, - height: 1, + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse( + "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), + mode: LaunchMode.externalApplication, + ), + child: Row( + children: [ + Text( + '${secondAvailableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension<BalancePageTheme>()! + .labelTextColor, + height: 1, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension<BalancePageTheme>()! + .labelTextColor), + ) + ], ), ), SizedBox(height: 8), diff --git a/lib/src/screens/settings/mweb_logs_page.dart b/lib/src/screens/settings/mweb_logs_page.dart new file mode 100644 index 000000000..3c5470214 --- /dev/null +++ b/lib/src/screens/settings/mweb_logs_page.dart @@ -0,0 +1,127 @@ +import 'dart:io'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/utils/share_util.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; +import 'package:cw_core/root_dir.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; + +class MwebLogsPage extends BasePage { + MwebLogsPage(this.mwebSettingsViewModelBase); + + final MwebSettingsViewModelBase mwebSettingsViewModelBase; + + @override + String get title => S.current.litecoin_mweb_logs; + + @override + Widget body(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + FutureBuilder<String>( + future: mwebSettingsViewModelBase.getAbbreviatedLogs(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { + return Center(child: Text('No logs found')); + } else { + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text( + snapshot.data!, + style: TextStyle(fontFamily: 'Monospace'), + ), + ), + ); + } + }, + ), + Positioned( + child: LoadingPrimaryButton( + onPressed: () => onExportLogs(context), + text: S.of(context).export_logs, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + bottom: 24, + left: 24, + right: 24, + ) + ], + ); + } + + void onExportLogs(BuildContext context) { + if (Platform.isAndroid) { + onExportAndroid(context); + } else if (Platform.isIOS) { + share(context); + } else { + _saveFile(); + } + } + + void onExportAndroid(BuildContext context) { + showPopUp<void>( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).export_backup, + alertContent: S.of(context).select_destination, + rightButtonText: S.of(context).save_to_downloads, + leftButtonText: S.of(context).share, + actionRightButton: () async { + const downloadDirPath = "/storage/emulated/0/Download"; + final filePath = downloadDirPath + "/debug.log"; + await mwebSettingsViewModelBase.saveLogsLocally(filePath); + Navigator.of(dialogContext).pop(); + }, + actionLeftButton: () async { + Navigator.of(dialogContext).pop(); + try { + await share(context); + } catch (e, s) { + ExceptionHandler.onError(FlutterErrorDetails( + exception: e, + stack: s, + library: "Export Logs", + )); + } + }); + }); + } + + Future<void> share(BuildContext context) async { + final filePath = (await getAppDir()).path + "/debug.log"; + bool success = await mwebSettingsViewModelBase.saveLogsLocally(filePath); + if (!success) return; + await ShareUtil.shareFile(filePath: filePath, fileName: "debug.log", context: context); + await mwebSettingsViewModelBase.removeLogsLocally(filePath); + } + + Future<void> _saveFile() async { + String? outputFile = await FilePicker.platform + .saveFile(dialogTitle: 'Save Your File to desired location', fileName: "debug.log"); + + try { + final filePath = (await getApplicationSupportDirectory()).path + "/debug.log"; + File debugLogFile = File(filePath); + await debugLogFile.copy(outputFile!); + } catch (exception, stackTrace) { + ExceptionHandler.onError(FlutterErrorDetails( + exception: exception, + stack: stackTrace, + library: "Export Logs", + )); + } + } +} diff --git a/lib/src/screens/settings/mweb_node_page.dart b/lib/src/screens/settings/mweb_node_page.dart new file mode 100644 index 000000000..801ab3ac7 --- /dev/null +++ b/lib/src/screens/settings/mweb_node_page.dart @@ -0,0 +1,56 @@ +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class MwebNodePage extends BasePage { + MwebNodePage(this.mwebSettingsViewModelBase) + : _nodeUriController = TextEditingController(text: mwebSettingsViewModelBase.mwebNodeUri), + super(); + + final MwebSettingsViewModelBase mwebSettingsViewModelBase; + final TextEditingController _nodeUriController; + + @override + String get title => S.current.litecoin_mweb_node; + + @override + Widget body(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: <Widget>[ + Expanded( + child: BaseTextFormField(controller: _nodeUriController), + ) + ], + ), + ), + Positioned( + child: Observer( + builder: (_) => LoadingPrimaryButton( + onPressed: () => save(context), + text: S.of(context).save, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ), + bottom: 24, + left: 24, + right: 24, + ) + ], + ); + } + + void save(BuildContext context) { + mwebSettingsViewModelBase.setMwebNodeUri(_nodeUriController.text); + Navigator.pop(context); + } +} diff --git a/lib/src/screens/settings/mweb_settings.dart b/lib/src/screens/settings/mweb_settings.dart index 722ffa9aa..e78fdf596 100644 --- a/lib/src/screens/settings/mweb_settings.dart +++ b/lib/src/screens/settings/mweb_settings.dart @@ -31,7 +31,7 @@ class MwebSettingsPage extends BasePage { }, ), SettingsSwitcherCell( - title: S.current.litecoin_mweb_always_scan, + title: S.current.litecoin_mweb_enable, value: _mwebSettingsViewModel.mwebEnabled, onValueChange: (_, bool value) { _mwebSettingsViewModel.setMwebEnabled(value); @@ -41,6 +41,14 @@ class MwebSettingsPage extends BasePage { title: S.current.litecoin_mweb_scanning, handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan), ), + SettingsCellWithArrow( + title: S.current.litecoin_mweb_logs, + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.mwebLogs), + ), + SettingsCellWithArrow( + title: S.current.litecoin_mweb_node, + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.mwebNode), + ), ], ), ); diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 2eadbd8a9..c966420db 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -120,6 +120,7 @@ abstract class SettingsStoreBase with Store { required this.mwebCardDisplay, required this.mwebEnabled, required this.hasEnabledMwebBefore, + required this.mwebNodeUri, TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialWowneroTransactionPriority, @@ -358,8 +359,8 @@ abstract class SettingsStoreBase with Store { reaction( (_) => bitcoinSeedType, - (BitcoinSeedType bitcoinSeedType) => sharedPreferences.setInt( - PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw)); + (BitcoinSeedType bitcoinSeedType) => + sharedPreferences.setInt(PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw)); reaction( (_) => nanoSeedType, @@ -442,8 +443,10 @@ abstract class SettingsStoreBase with Store { reaction((_) => useTronGrid, (bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid)); - reaction((_) => useMempoolFeeAPI, - (bool useMempoolFeeAPI) => _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI)); + reaction( + (_) => useMempoolFeeAPI, + (bool useMempoolFeeAPI) => + _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI)); reaction((_) => defaultNanoRep, (String nanoRep) => _sharedPreferences.setString(PreferencesKey.defaultNanoRep, nanoRep)); @@ -591,6 +594,11 @@ abstract class SettingsStoreBase with Store { (bool hasEnabledMwebBefore) => _sharedPreferences.setBool(PreferencesKey.hasEnabledMwebBefore, hasEnabledMwebBefore)); + reaction( + (_) => mwebNodeUri, + (String mwebNodeUri) => + _sharedPreferences.setString(PreferencesKey.mwebNodeUri, mwebNodeUri)); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -822,6 +830,9 @@ abstract class SettingsStoreBase with Store { @observable bool hasEnabledMwebBefore; + @observable + String mwebNodeUri; + final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final BackgroundTasks _backgroundTasks; @@ -988,6 +999,8 @@ abstract class SettingsStoreBase with Store { final mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; final hasEnabledMwebBefore = sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false; + final mwebNodeUri = sharedPreferences.getString(PreferencesKey.mwebNodeUri) ?? + "ltc-electrum.cakewallet.com:9333"; // If no value if (pinLength == null || pinLength == 0) { @@ -1259,6 +1272,7 @@ abstract class SettingsStoreBase with Store { mwebAlwaysScan: mwebAlwaysScan, mwebCardDisplay: mwebCardDisplay, mwebEnabled: mwebEnabled, + mwebNodeUri: mwebNodeUri, hasEnabledMwebBefore: hasEnabledMwebBefore, initialMoneroTransactionPriority: moneroTransactionPriority, initialWowneroTransactionPriority: wowneroTransactionPriority, @@ -1686,7 +1700,8 @@ abstract class SettingsStoreBase with Store { deviceName = windowsInfo.productName; } catch (e) { print(e); - print('likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged'); + print( + 'likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged'); deviceName = "Windows Device"; } } diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 56a5935c9..20dca292c 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -381,7 +381,7 @@ abstract class BalanceViewModelBase with Store { bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { if (wallet.type == WalletType.litecoin) { - if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) > 0) { + if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0) { return true; } } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 47fc32ab6..b218c8e9f 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -56,25 +56,53 @@ class TransactionListItem extends ActionListItem with Keyable { } String get formattedPendingStatus { - if (balanceViewModel.wallet.type == WalletType.monero || - balanceViewModel.wallet.type == WalletType.haven) { - if (transaction.confirmations >= 0 && transaction.confirmations < 10) { - return ' (${transaction.confirmations}/10)'; - } - } else if (balanceViewModel.wallet.type == WalletType.wownero) { - if (transaction.confirmations >= 0 && transaction.confirmations < 3) { - return ' (${transaction.confirmations}/3)'; - } + switch (balanceViewModel.wallet.type) { + case WalletType.monero: + case WalletType.haven: + if (transaction.confirmations >= 0 && transaction.confirmations < 10) { + return ' (${transaction.confirmations}/10)'; + } + break; + case WalletType.wownero: + if (transaction.confirmations >= 0 && transaction.confirmations < 3) { + return ' (${transaction.confirmations}/3)'; + } + break; + case WalletType.litecoin: + bool isPegIn = (transaction.additionalInfo["isPegIn"] as bool?) ?? false; + bool isPegOut = (transaction.additionalInfo["isPegOut"] as bool?) ?? false; + bool fromPegOut = (transaction.additionalInfo["fromPegOut"] as bool?) ?? false; + String str = ''; + if (transaction.confirmations <= 0) { + str = S.current.pending; + } + if ((isPegOut || fromPegOut) && transaction.confirmations >= 0 && transaction.confirmations < 6) { + str = " (${transaction.confirmations}/6)"; + } + if (isPegIn) { + str += " (Peg In)"; + } + if (isPegOut) { + str += " (Peg Out)"; + } + return str; + default: + return ''; } + return ''; } String get formattedStatus { - if (balanceViewModel.wallet.type == WalletType.monero || - balanceViewModel.wallet.type == WalletType.wownero || - balanceViewModel.wallet.type == WalletType.haven) { + if ([ + WalletType.monero, + WalletType.haven, + WalletType.wownero, + WalletType.litecoin, + ].contains(balanceViewModel.wallet.type)) { return formattedPendingStatus; } + return transaction.isPending ? S.current.pending : ''; } diff --git a/lib/view_model/settings/mweb_settings_view_model.dart b/lib/view_model/settings/mweb_settings_view_model.dart index c6370e23f..11e4c8177 100644 --- a/lib/view_model/settings/mweb_settings_view_model.dart +++ b/lib/view_model/settings/mweb_settings_view_model.dart @@ -1,7 +1,12 @@ +import 'dart:io'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:flutter/widgets.dart'; import 'package:mobx/mobx.dart'; +import 'package:path_provider/path_provider.dart'; part 'mweb_settings_view_model.g.dart'; @@ -22,15 +27,60 @@ abstract class MwebSettingsViewModelBase with Store { @observable late bool mwebEnabled; + @computed + String get mwebNodeUri => _settingsStore.mwebNodeUri; + @action void setMwebCardDisplay(bool value) { _settingsStore.mwebCardDisplay = value; } + @action + void setMwebNodeUri(String value) { + _settingsStore.mwebNodeUri = value; + } + @action void setMwebEnabled(bool value) { mwebEnabled = value; bitcoin!.setMwebEnabled(_wallet, value); _settingsStore.mwebAlwaysScan = value; } + + Future<bool> saveLogsLocally(String filePath) async { + try { + final appSupportPath = (await getApplicationSupportDirectory()).path; + final logsFile = File("$appSupportPath/logs/debug.log"); + if (!logsFile.existsSync()) { + throw Exception('Logs file does not exist'); + } + await logsFile.copy(filePath); + return true; + } catch (e, s) { + ExceptionHandler.onError(FlutterErrorDetails( + exception: e, + stack: s, + library: "Export Logs", + )); + return false; + } + } + + Future<String> getAbbreviatedLogs() async { + final appSupportPath = (await getApplicationSupportDirectory()).path; + final logsFile = File("$appSupportPath/logs/debug.log"); + if (!logsFile.existsSync()) { + return ""; + } + final logs = logsFile.readAsStringSync(); + // return last 10000 characters: + return logs.substring(logs.length > 10000 ? logs.length - 10000 : 0); + } + + Future<void> removeLogsLocally(String filePath) async { + final logsFile = File(filePath); + if (logsFile.existsSync()) { + await logsFile.delete(); + } + } } diff --git a/pubspec_base.yaml b/pubspec_base.yaml index d5fce76e9..2abd8401a 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -134,7 +134,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v8 + ref: cake-update-v9 ffi: 2.1.0 flutter_icons: diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index c2be1504a..0db2031dd 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -295,6 +295,7 @@ "expiresOn": "ﻲﻓ ﻪﺘﻴﺣﻼﺻ ﻲﻬﺘﻨﺗ", "expiry_and_validity": "انتهاء الصلاحية والصلاحية", "export_backup": "تصدير نسخة احتياطية", + "export_logs": "سجلات التصدير", "extra_id": "معرف إضافي:", "extracted_address_content": "سوف ترسل الأموال إلى\n${recipient_name}", "failed_authentication": "${state_error} فشل المصادقة.", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB هو بروتوكول جديد يجلب معاملات أسرع وأرخص وأكثر خصوصية إلى Litecoin", "litecoin_mweb_dismiss": "رفض", "litecoin_mweb_display_card": "عرض بطاقة mweb", + "litecoin_mweb_enable": "تمكين MWEB", "litecoin_mweb_enable_later": "يمكنك اختيار تمكين MWEB مرة أخرى ضمن إعدادات العرض.", + "litecoin_mweb_logs": "سجلات MWEB", + "litecoin_mweb_node": "عقدة MWEB", "litecoin_mweb_pegin": "ربط في", "litecoin_mweb_pegout": "ربط", "litecoin_mweb_scanning": "MWEB المسح الضوئي", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 8b68d0d1e..7e78dd69a 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -295,6 +295,7 @@ "expiresOn": "Изтича на", "expiry_and_validity": "Изтичане и валидност", "export_backup": "Експортиране на резервно копие", + "export_logs": "Експортни дневници", "extra_id": "Допълнително ID:", "extracted_address_content": "Ще изпратите средства на \n${recipient_name}", "failed_authentication": "Неуспешно удостоверяване. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWeb е нов протокол, който носи по -бърз, по -евтин и повече частни транзакции на Litecoin", "litecoin_mweb_dismiss": "Уволнение", "litecoin_mweb_display_card": "Показване на MWEB карта", + "litecoin_mweb_enable": "Активирайте MWeb", "litecoin_mweb_enable_later": "Можете да изберете да активирате MWEB отново под настройките на дисплея.", + "litecoin_mweb_logs": "MWeb logs", + "litecoin_mweb_node": "MWEB възел", "litecoin_mweb_pegin": "PEG в", "litecoin_mweb_pegout": "PEG OUT", "litecoin_mweb_scanning": "Сканиране на MWEB", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 9b48ebdc7..f17466a9f 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -295,6 +295,7 @@ "expiresOn": "Vyprší dne", "expiry_and_validity": "Vypršení a platnost", "export_backup": "Exportovat zálohu", + "export_logs": "Vývozní protokoly", "extra_id": "Extra ID:", "extracted_address_content": "Prostředky budete posílat na\n${recipient_name}", "failed_authentication": "Ověřování selhalo. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB je nový protokol, který do Litecoin přináší rychlejší, levnější a více soukromých transakcí", "litecoin_mweb_dismiss": "Propustit", "litecoin_mweb_display_card": "Zobrazit kartu MWeb", + "litecoin_mweb_enable": "Povolit mWeb", "litecoin_mweb_enable_later": "V nastavení zobrazení můžete vybrat znovu povolit MWeb.", + "litecoin_mweb_logs": "Protokoly mWeb", + "litecoin_mweb_node": "Uzel mWeb", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Zkrachovat", "litecoin_mweb_scanning": "Skenování mWeb", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 2ec59f349..cb0a59574 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -295,6 +295,7 @@ "expiresOn": "Läuft aus am", "expiry_and_validity": "Ablauf und Gültigkeit", "export_backup": "Sicherung exportieren", + "export_logs": "Exportprotokolle", "extra_id": "Extra ID:", "extracted_address_content": "Sie senden Geld an\n${recipient_name}", "failed_authentication": "Authentifizierung fehlgeschlagen. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWWB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt", "litecoin_mweb_dismiss": "Zurückweisen", "litecoin_mweb_display_card": "MWEB-Karte anzeigen", + "litecoin_mweb_enable": "Aktivieren Sie MWeb", "litecoin_mweb_enable_later": "Sie können MWEB unter Anzeigeeinstellungen erneut aktivieren.", + "litecoin_mweb_logs": "MWEB -Protokolle", + "litecoin_mweb_node": "MWEB -Knoten", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Abstecken", "litecoin_mweb_scanning": "MWEB Scanning", @@ -941,4 +945,4 @@ "you_will_get": "Konvertieren zu", "you_will_send": "Konvertieren von", "yy": "YY" -} +} \ No newline at end of file diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 4f5fd23b3..40be8fd08 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -295,6 +295,7 @@ "expiresOn": "Expires on", "expiry_and_validity": "Expiry and Validity", "export_backup": "Export backup", + "export_logs": "Export logs", "extra_id": "Extra ID:", "extracted_address_content": "You will be sending funds to\n${recipient_name}", "failed_authentication": "Failed authentication. ${state_error}", @@ -373,7 +374,10 @@ "litecoin_mweb_description": "MWEB is a new protocol that brings faster, cheaper, and more private transactions to Litecoin", "litecoin_mweb_dismiss": "Dismiss", "litecoin_mweb_display_card": "Show MWEB card", + "litecoin_mweb_enable": "Enable MWEB", "litecoin_mweb_enable_later": "You can choose to enable MWEB again under Display Settings.", + "litecoin_mweb_logs": "MWEB Logs", + "litecoin_mweb_node": "MWEB Node", "litecoin_mweb_pegin": "Peg In", "litecoin_mweb_pegout": "Peg Out", "litecoin_mweb_scanning": "MWEB Scanning", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index fd20a8b30..5a693cf32 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -295,6 +295,7 @@ "expiresOn": "Expira el", "expiry_and_validity": "Vencimiento y validez", "export_backup": "Exportar copia de seguridad", + "export_logs": "Registros de exportación", "extra_id": "ID adicional:", "extracted_address_content": "Enviará fondos a\n${recipient_name}", "failed_authentication": "Autenticación fallida. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "Mweb es un nuevo protocolo que trae transacciones más rápidas, más baratas y más privadas a Litecoin", "litecoin_mweb_dismiss": "Despedir", "litecoin_mweb_display_card": "Mostrar tarjeta MWEB", + "litecoin_mweb_enable": "Habilitar mweb", "litecoin_mweb_enable_later": "Puede elegir habilitar MWEB nuevamente en la configuración de visualización.", + "litecoin_mweb_logs": "Registros de mweb", + "litecoin_mweb_node": "Nodo mweb", "litecoin_mweb_pegin": "Convertir", "litecoin_mweb_pegout": "Recuperar", "litecoin_mweb_scanning": "Escaneo mweb", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 50aceb2f1..1865e41f2 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -295,6 +295,7 @@ "expiresOn": "Expire le", "expiry_and_validity": "Expiration et validité", "export_backup": "Exporter la sauvegarde", + "export_logs": "Journaux d'exportation", "extra_id": "ID supplémentaire :", "extracted_address_content": "Vous allez envoyer des fonds à\n${recipient_name}", "failed_authentication": "Échec d'authentification. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB est un nouveau protocole qui apporte des transactions plus rapides, moins chères et plus privées à Litecoin", "litecoin_mweb_dismiss": "Rejeter", "litecoin_mweb_display_card": "Afficher la carte MWeb", + "litecoin_mweb_enable": "Activer Mweb", "litecoin_mweb_enable_later": "Vous pouvez choisir d'activer à nouveau MWEB sous Paramètres d'affichage.", + "litecoin_mweb_logs": "Journaux MWEB", + "litecoin_mweb_node": "Node MWEB", "litecoin_mweb_pegin": "Entraver", "litecoin_mweb_pegout": "Crever", "litecoin_mweb_scanning": "Scann mweb", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index e66859531..52cf14128 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -295,6 +295,7 @@ "expiresOn": "Yana ƙarewa", "expiry_and_validity": "Karewa da inganci", "export_backup": "Ajiyayyen fitarwa", + "export_logs": "Injin fitarwa", "extra_id": "Karin ID:", "extracted_address_content": "Za ku aika da kudade zuwa\n${recipient_name}", "failed_authentication": "Binne wajen shiga. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "Mweb shine sabon tsarin yarjejeniya da ya kawo da sauri, mai rahusa, da kuma ma'amaloli masu zaman kansu zuwa Litecoin", "litecoin_mweb_dismiss": "Tuɓe \\ sallama", "litecoin_mweb_display_card": "Nuna katin Mweb", + "litecoin_mweb_enable": "Kunna Mweb", "litecoin_mweb_enable_later": "Kuna iya zaɓar kunna Mweb kuma a ƙarƙashin saitunan nuni.", + "litecoin_mweb_logs": "Jagoran Mweb", + "litecoin_mweb_node": "Mweb Node", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Peg fita", "litecoin_mweb_scanning": "Mweb scanning", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index bc8b81211..64f112a42 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -295,6 +295,7 @@ "expiresOn": "पर समय सीमा समाप्त", "expiry_and_validity": "समाप्ति और वैधता", "export_backup": "निर्यात बैकअप", + "export_logs": "निर्यात लॉग", "extra_id": "अतिरिक्त आईडी:", "extracted_address_content": "आपको धनराशि भेजी जाएगी\n${recipient_name}", "failed_authentication": "प्रमाणीकरण विफल. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB एक नया प्रोटोकॉल है जो लिटकोइन के लिए तेजी से, सस्ता और अधिक निजी लेनदेन लाता है", "litecoin_mweb_dismiss": "नकार देना", "litecoin_mweb_display_card": "MWEB कार्ड दिखाएं", + "litecoin_mweb_enable": "MWEB सक्षम करें", "litecoin_mweb_enable_later": "आप प्रदर्शन सेटिंग्स के तहत फिर से MWEB को सक्षम करने के लिए चुन सकते हैं।", + "litecoin_mweb_logs": "MWEB लॉग", + "litecoin_mweb_node": "MWEB नोड", "litecoin_mweb_pegin": "खूंटी", "litecoin_mweb_pegout": "मरना", "litecoin_mweb_scanning": "MWEB स्कैनिंग", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 9ff57707d..4bbf4f141 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -295,6 +295,7 @@ "expiresOn": "Istječe", "expiry_and_validity": "Istek i valjanost", "export_backup": "Izvezi sigurnosnu kopiju", + "export_logs": "Izvozni trupci", "extra_id": "Dodatni ID:", "extracted_address_content": "Poslat ćete sredstva primatelju\n${recipient_name}", "failed_authentication": "Autentifikacija neuspješna. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB je novi protokol koji u Litecoin donosi brže, jeftinije i privatnije transakcije", "litecoin_mweb_dismiss": "Odbaciti", "litecoin_mweb_display_card": "Prikaži MWeb karticu", + "litecoin_mweb_enable": "Omogući MWeb", "litecoin_mweb_enable_later": "Možete odabrati da MWEB ponovo omogućite pod postavkama zaslona.", + "litecoin_mweb_logs": "MWEB trupci", + "litecoin_mweb_node": "MWEB čvor", "litecoin_mweb_pegin": "Uvući se", "litecoin_mweb_pegout": "Odapeti", "litecoin_mweb_scanning": "MWEB skeniranje", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 40ed1e116..cbaf3a548 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -295,6 +295,7 @@ "expiresOn": "Վավերականությունը լրանում է", "expiry_and_validity": "Վավերականություն և լրացում", "export_backup": "Արտահանել կրկնօրինակը", + "export_logs": "Արտահանման տեղեկամատյաններ", "extra_id": "Լրացուցիչ ID", "extracted_address_content": "Դուք կուղարկեք գումար ${recipient_name}", "failed_authentication": "Վավերացումը ձախողվեց. ${state_error}", @@ -367,7 +368,10 @@ "light_theme": "Լուսավոր", "litecoin_mweb_description": "Mweb- ը նոր արձանագրություն է, որը բերում է ավելի արագ, ավելի էժան եւ ավելի մասնավոր գործարքներ դեպի LITECOIN", "litecoin_mweb_dismiss": "Հեռացնել", + "litecoin_mweb_enable": "Միացնել Mweb- ը", "litecoin_mweb_enable_later": "Կարող եք ընտրել Mweb- ը կրկին միացնել ցուցադրման պարամետրերը:", + "litecoin_mweb_logs": "Mweb տեղեկամատյաններ", + "litecoin_mweb_node": "Mweb հանգույց", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Հափշտակել", "live_fee_rates": "Ապակի վարձավճարներ API- ի միջոցով", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 2858fa193..273087eec 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -295,6 +295,7 @@ "expiresOn": "Kadaluarsa pada", "expiry_and_validity": "Kedaluwarsa dan validitas", "export_backup": "Ekspor cadangan", + "export_logs": "Log ekspor", "extra_id": "ID tambahan:", "extracted_address_content": "Anda akan mengirim dana ke\n${recipient_name}", "failed_authentication": "Otentikasi gagal. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB adalah protokol baru yang membawa transaksi yang lebih cepat, lebih murah, dan lebih pribadi ke Litecoin", "litecoin_mweb_dismiss": "Membubarkan", "litecoin_mweb_display_card": "Tunjukkan kartu mWeb", + "litecoin_mweb_enable": "Aktifkan MWEB", "litecoin_mweb_enable_later": "Anda dapat memilih untuk mengaktifkan MWEB lagi di bawah pengaturan tampilan.", + "litecoin_mweb_logs": "Log MWeb", + "litecoin_mweb_node": "Node MWEB", "litecoin_mweb_pegin": "Pasak masuk", "litecoin_mweb_pegout": "Mati", "litecoin_mweb_scanning": "Pemindaian MWEB", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 034376a6c..917315aa6 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -296,6 +296,7 @@ "expiresOn": "Scade il", "expiry_and_validity": "Scadenza e validità", "export_backup": "Esporta backup", + "export_logs": "Registri di esportazione", "extra_id": "Extra ID:", "extracted_address_content": "Invierai i tuoi fondi a\n${recipient_name}", "failed_authentication": "Autenticazione fallita. ${state_error}", @@ -372,7 +373,10 @@ "litecoin_mweb_description": "MWeb è un nuovo protocollo che porta transazioni più veloci, più economiche e più private a Litecoin", "litecoin_mweb_dismiss": "Congedare", "litecoin_mweb_display_card": "Mostra la scheda MWeb", + "litecoin_mweb_enable": "Abilita mWeb", "litecoin_mweb_enable_later": "È possibile scegliere di abilitare nuovamente MWeb nelle impostazioni di visualizzazione.", + "litecoin_mweb_logs": "Registri mWeb", + "litecoin_mweb_node": "Nodo MWeb", "litecoin_mweb_pegin": "Piolo in", "litecoin_mweb_pegout": "PEG OUT", "litecoin_mweb_scanning": "Scansione MWeb", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 7f0792063..ae3e32ea8 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -295,6 +295,7 @@ "expiresOn": "有効期限は次のとおりです", "expiry_and_validity": "有効期限と有効性", "export_backup": "バックアップのエクスポート", + "export_logs": "ログをエクスポートします", "extra_id": "追加ID:", "extracted_address_content": "に送金します\n${recipient_name}", "failed_authentication": "認証失敗. ${state_error}", @@ -372,7 +373,10 @@ "litecoin_mweb_description": "MWEBは、Litecoinにより速く、より安価で、よりプライベートなトランザクションをもたらす新しいプロトコルです", "litecoin_mweb_dismiss": "却下する", "litecoin_mweb_display_card": "MWEBカードを表示します", + "litecoin_mweb_enable": "MWEBを有効にします", "litecoin_mweb_enable_later": "表示設定の下で、MWEBを再度有効にすることを選択できます。", + "litecoin_mweb_logs": "MWEBログ", + "litecoin_mweb_node": "MWEBノード", "litecoin_mweb_pegin": "ペグイン", "litecoin_mweb_pegout": "ペグアウト", "litecoin_mweb_scanning": "MWEBスキャン", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 6ba402ce3..2dc1a2dfc 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -295,6 +295,7 @@ "expiresOn": "만료 날짜", "expiry_and_validity": "만료와 타당성", "export_backup": "백업 내보내기", + "export_logs": "내보내기 로그", "extra_id": "추가 ID:", "extracted_address_content": "당신은에 자금을 보낼 것입니다\n${recipient_name}", "failed_authentication": "인증 실패. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB는 Litecoin에 더 빠르고 저렴하며 개인 거래를 제공하는 새로운 프로토콜입니다.", "litecoin_mweb_dismiss": "해고하다", "litecoin_mweb_display_card": "mweb 카드를 보여주십시오", + "litecoin_mweb_enable": "mweb 활성화", "litecoin_mweb_enable_later": "디스플레이 설정에서 MWEB를 다시 활성화하도록 선택할 수 있습니다.", + "litecoin_mweb_logs": "mweb 로그", + "litecoin_mweb_node": "mweb 노드", "litecoin_mweb_pegin": "페그를 입력하십시오", "litecoin_mweb_pegout": "죽다", "litecoin_mweb_scanning": "mweb 스캔", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 69b888b5a..f42af6863 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -295,6 +295,7 @@ "expiresOn": "သက်တမ်းကုန်သည်။", "expiry_and_validity": "သက်တမ်းကုန်ဆုံးခြင်းနှင့်တရားဝင်မှု", "export_backup": "အရန်ကူးထုတ်ရန်", + "export_logs": "ပို့ကုန်မှတ်တမ်းများ", "extra_id": "အပို ID-", "extracted_address_content": "သင်သည် \n${recipient_name} သို့ ရန်ပုံငွေများ ပေးပို့ပါမည်", "failed_authentication": "အထောက်အထားစိစစ်ခြင်း မအောင်မြင်ပါ။. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "Mweb သည် Protocol အသစ်ဖြစ်ပြီး LitCoin သို့ပိုမိုဈေးချိုသာသော, စျေးသက်သက်သာသာသုံးခြင်းနှင့်ပိုမိုများပြားသောပုဂ္ဂလိကငွေပို့ဆောင်မှုများကိုဖြစ်ပေါ်စေသည်", "litecoin_mweb_dismiss": "ထုတ်ပစ်", "litecoin_mweb_display_card": "MweB ကဒ်ကိုပြပါ", + "litecoin_mweb_enable": "mweb enable", "litecoin_mweb_enable_later": "သင် MweB ကို display settings အောက်ရှိ ထပ်မံ. ခွင့်ပြုရန်ရွေးချယ်နိုင်သည်။", + "litecoin_mweb_logs": "Mweb မှတ်တမ်းများ", + "litecoin_mweb_node": "mweb node ကို", "litecoin_mweb_pegin": "တံစို့", "litecoin_mweb_pegout": "တံစို့", "litecoin_mweb_scanning": "mweb scanning", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 2c259bf80..73e3df6b6 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -295,6 +295,7 @@ "expiresOn": "Verloopt op", "expiry_and_validity": "Vervallen en geldigheid", "export_backup": "Back-up exporteren", + "export_logs": "Exporteer logboeken", "extra_id": "Extra ID:", "extracted_address_content": "U stuurt geld naar\n${recipient_name}", "failed_authentication": "Mislukte authenticatie. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB is een nieuw protocol dat snellere, goedkopere en meer privé -transacties naar Litecoin brengt", "litecoin_mweb_dismiss": "Afwijzen", "litecoin_mweb_display_card": "Toon MWEB -kaart", + "litecoin_mweb_enable": "MWEB inschakelen", "litecoin_mweb_enable_later": "U kunt ervoor kiezen om MWeb opnieuw in te schakelen onder weergave -instellingen.", + "litecoin_mweb_logs": "MWEB -logboeken", + "litecoin_mweb_node": "MWEB -knooppunt", "litecoin_mweb_pegin": "Vastmaken", "litecoin_mweb_pegout": "Uithakken", "litecoin_mweb_scanning": "MWEB -scanning", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 936339708..aaa82a3bf 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -295,6 +295,7 @@ "expiresOn": "Upływa w dniu", "expiry_and_validity": "Wygaśnięcie i ważność", "export_backup": "Eksportuj kopię zapasową", + "export_logs": "Dzienniki eksportu", "extra_id": "Dodatkowy ID:", "extracted_address_content": "Wysyłasz środki na\n${recipient_name}", "failed_authentication": "Nieudane uwierzytelnienie. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB to nowy protokół, który przynosi szybciej, tańsze i bardziej prywatne transakcje do Litecoin", "litecoin_mweb_dismiss": "Odrzucać", "litecoin_mweb_display_card": "Pokaż kartę MWEB", + "litecoin_mweb_enable": "Włącz MWEB", "litecoin_mweb_enable_later": "Możesz ponownie włączyć MWEB w ustawieniach wyświetlania.", + "litecoin_mweb_logs": "Dzienniki MWEB", + "litecoin_mweb_node": "Węzeł MWEB", "litecoin_mweb_pegin": "Kołek", "litecoin_mweb_pegout": "Palikować", "litecoin_mweb_scanning": "Skanowanie MWEB", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 0cc0cc00a..194307a3d 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -295,6 +295,7 @@ "expiresOn": "Expira em", "expiry_and_validity": "Expiração e validade", "export_backup": "Backup de exportação", + "export_logs": "Exportar logs", "extra_id": "ID extra:", "extracted_address_content": "Você enviará fundos para\n${recipient_name}", "failed_authentication": "Falha na autenticação. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB é um novo protocolo que traz transações mais rápidas, baratas e mais privadas para o Litecoin", "litecoin_mweb_dismiss": "Liberar", "litecoin_mweb_display_card": "Mostre o cartão MWEB", + "litecoin_mweb_enable": "Ativar Mweb", "litecoin_mweb_enable_later": "Você pode optar por ativar o MWEB novamente em Configurações de exibição.", + "litecoin_mweb_logs": "Logs MWeb", + "litecoin_mweb_node": "Nó MWeb", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Peg fora", "litecoin_mweb_scanning": "MWEB Scanning", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 8aeb56a86..f42c28c15 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -295,6 +295,7 @@ "expiresOn": "Годен до", "expiry_and_validity": "Истечение и достоверность", "export_backup": "Экспорт резервной копии", + "export_logs": "Экспортные журналы", "extra_id": "Дополнительный ID:", "extracted_address_content": "Вы будете отправлять средства\n${recipient_name}", "failed_authentication": "Ошибка аутентификации. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB - это новый протокол, который приносит быстрее, дешевле и более частные транзакции в Litecoin", "litecoin_mweb_dismiss": "Увольнять", "litecoin_mweb_display_card": "Показать карту MWEB", + "litecoin_mweb_enable": "Включить MWEB", "litecoin_mweb_enable_later": "Вы можете снова включить MWEB в настройках отображения.", + "litecoin_mweb_logs": "MWEB журналы", + "litecoin_mweb_node": "Узел MWEB", "litecoin_mweb_pegin": "Внедрять", "litecoin_mweb_pegout": "Выкрикивать", "litecoin_mweb_scanning": "MWEB сканирование", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 736eb8762..9d8fb0af2 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -295,6 +295,7 @@ "expiresOn": "หมดอายุวันที่", "expiry_and_validity": "หมดอายุและถูกต้อง", "export_backup": "ส่งออกข้อมูลสำรอง", + "export_logs": "บันทึกการส่งออก", "extra_id": "ไอดีเพิ่มเติม:", "extracted_address_content": "คุณกำลังจะส่งเงินไปยัง\n${recipient_name}", "failed_authentication": "การยืนยันสิทธิ์ล้มเหลว ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB เป็นโปรโตคอลใหม่ที่นำการทำธุรกรรมที่เร็วกว่าราคาถูกกว่าและเป็นส่วนตัวมากขึ้นไปยัง Litecoin", "litecoin_mweb_dismiss": "อนุญาตให้ออกไป", "litecoin_mweb_display_card": "แสดงการ์ด mweb", + "litecoin_mweb_enable": "เปิดใช้งาน mweb", "litecoin_mweb_enable_later": "คุณสามารถเลือกเปิดใช้งาน MWEB อีกครั้งภายใต้การตั้งค่าการแสดงผล", + "litecoin_mweb_logs": "บันทึก MWEB", + "litecoin_mweb_node": "โหนด MWEB", "litecoin_mweb_pegin": "หมุด", "litecoin_mweb_pegout": "ตรึง", "litecoin_mweb_scanning": "การสแกน MWEB", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index e68bd0b22..ca35e7997 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -295,6 +295,7 @@ "expiresOn": "Mag-e-expire sa", "expiry_and_validity": "Pag-expire at Bisa", "export_backup": "I-export ang backup", + "export_logs": "Mga log ng pag -export", "extra_id": "Dagdag na ID:", "extracted_address_content": "Magpapadala ka ng pondo sa\n${recipient_name}", "failed_authentication": "Nabigo ang pagpapatunay. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "Ang MWeb ay isang bagong protocol na nagdadala ng mas mabilis, mas mura, at mas maraming pribadong mga transaksyon sa Litecoin", "litecoin_mweb_dismiss": "Tanggalin", "litecoin_mweb_display_card": "Ipakita ang MWEB Card", + "litecoin_mweb_enable": "Paganahin ang MWeb", "litecoin_mweb_enable_later": "Maaari kang pumili upang paganahin muli ang MWeb sa ilalim ng mga setting ng pagpapakita.", + "litecoin_mweb_logs": "MWEB log", + "litecoin_mweb_node": "Mweb node", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Peg out", "litecoin_mweb_scanning": "Pag -scan ng Mweb", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 83e8f6cbc..1fee66369 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -295,6 +295,7 @@ "expiresOn": "Tarihinde sona eriyor", "expiry_and_validity": "Sona erme ve geçerlilik", "export_backup": "Yedeği dışa aktar", + "export_logs": "Dışa aktarma günlükleri", "extra_id": "Ekstra ID:", "extracted_address_content": "Parayı buraya gönderceksin:\n${recipient_name}", "failed_authentication": "Doğrulama başarısız oldu. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB, Litecoin'e daha hızlı, daha ucuz ve daha fazla özel işlem getiren yeni bir protokoldür", "litecoin_mweb_dismiss": "Azletmek", "litecoin_mweb_display_card": "MWEB kartını göster", + "litecoin_mweb_enable": "MWEB'i etkinleştir", "litecoin_mweb_enable_later": "Ekran ayarlarının altında MWEB'yi tekrar etkinleştirmeyi seçebilirsiniz.", + "litecoin_mweb_logs": "MWEB günlükleri", + "litecoin_mweb_node": "MWEB düğümü", "litecoin_mweb_pegin": "Takılmak", "litecoin_mweb_pegout": "Çiğnemek", "litecoin_mweb_scanning": "MWEB taraması", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 909d9c01f..5d9a56c1e 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -295,6 +295,7 @@ "expiresOn": "Термін дії закінчується", "expiry_and_validity": "Закінчення та обгрунтованість", "export_backup": "Експортувати резервну копію", + "export_logs": "Експортні журнали", "extra_id": "Додатковий ID:", "extracted_address_content": "Ви будете відправляти кошти\n${recipient_name}", "failed_authentication": "Помилка аутентифікації. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB - це новий протокол, який приносить швидкі, дешевші та більш приватні транзакції Litecoin", "litecoin_mweb_dismiss": "Звільнити", "litecoin_mweb_display_card": "Показати карту MWeb", + "litecoin_mweb_enable": "Увімкнути mweb", "litecoin_mweb_enable_later": "Ви можете знову ввімкнути MWEB в налаштуваннях дисплея.", + "litecoin_mweb_logs": "Журнали MWeb", + "litecoin_mweb_node": "Вузол MWeb", "litecoin_mweb_pegin": "Подякувати", "litecoin_mweb_pegout": "Подякувати", "litecoin_mweb_scanning": "Сканування Mweb", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 2dbc97102..106f00f96 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -295,6 +295,7 @@ "expiresOn": "ﺩﺎﻌﯿﻣ ﯽﻣﺎﺘﺘﺧﺍ", "expiry_and_validity": "میعاد ختم اور صداقت", "export_backup": "بیک اپ برآمد کریں۔", + "export_logs": "نوشتہ جات برآمد کریں", "extra_id": "اضافی ID:", "extracted_address_content": "آپ فنڈز بھیج رہے ہوں گے\n${recipient_name}", "failed_authentication": "ناکام تصدیق۔ ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB ایک نیا پروٹوکول ہے جو لیٹیکوئن میں تیز ، سستا اور زیادہ نجی لین دین لاتا ہے", "litecoin_mweb_dismiss": "خارج", "litecoin_mweb_display_card": "MWEB کارڈ دکھائیں", + "litecoin_mweb_enable": "MWEB کو فعال کریں", "litecoin_mweb_enable_later": "آپ ڈسپلے کی ترتیبات کے تحت MWEB کو دوبارہ فعال کرنے کا انتخاب کرسکتے ہیں۔", + "litecoin_mweb_logs": "MWEB لاگز", + "litecoin_mweb_node": "MWEB نوڈ", "litecoin_mweb_pegin": "پیگ میں", "litecoin_mweb_pegout": "پیگ آؤٹ", "litecoin_mweb_scanning": "MWEB اسکیننگ", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 8d28d48a2..a95f32aaa 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -296,6 +296,7 @@ "expiresOn": "Hết hạn vào", "expiry_and_validity": "Hạn và hiệu lực", "export_backup": "Xuất sao lưu", + "export_logs": "Nhật ký xuất khẩu", "extra_id": "ID bổ sung:", "extracted_address_content": "Bạn sẽ gửi tiền cho\n${recipient_name}", "failed_authentication": "Xác thực không thành công. ${state_error}", @@ -368,7 +369,10 @@ "light_theme": "Chủ đề sáng", "litecoin_mweb_description": "MWEB là một giao thức mới mang lại các giao dịch nhanh hơn, rẻ hơn và riêng tư hơn cho Litecoin", "litecoin_mweb_dismiss": "Miễn nhiệm", + "litecoin_mweb_enable": "Bật MWEB", "litecoin_mweb_enable_later": "Bạn có thể chọn bật lại MWEB trong cài đặt hiển thị.", + "litecoin_mweb_logs": "Nhật ký MWEB", + "litecoin_mweb_node": "Nút MWEB", "litecoin_mweb_pegin": "Chốt vào", "litecoin_mweb_pegout": "Chốt ra", "live_fee_rates": "Tỷ lệ phí hiện tại qua API", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 03c7c95ef..1a1583da5 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -296,6 +296,7 @@ "expiresOn": "Ipari lori", "expiry_and_validity": "Ipari ati idaniloju", "export_backup": "Sún ẹ̀dà nípamọ́ síta", + "export_logs": "Wọle si okeere", "extra_id": "Àmì ìdánimọ̀ tó fikún:", "extracted_address_content": "Ẹ máa máa fi owó ránṣẹ́ sí\n${recipient_name}", "failed_authentication": "Ìfẹ̀rílàdí pipòfo. ${state_error}", @@ -372,7 +373,10 @@ "litecoin_mweb_description": "Mweb jẹ ilana ilana tuntun ti o mu iyara wa yiyara, din owo, ati awọn iṣowo ikọkọ diẹ sii si Livcoin", "litecoin_mweb_dismiss": "Tuka", "litecoin_mweb_display_card": "Fihan kaadi Mweb", + "litecoin_mweb_enable": "Mu mweb", "litecoin_mweb_enable_later": "O le yan lati ṣiṣẹ Mweb lẹẹkansi labẹ awọn eto ifihan.", + "litecoin_mweb_logs": "MTweb logs", + "litecoin_mweb_node": "Alweb joko", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Peg jade", "litecoin_mweb_scanning": "Mweb scanning", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 5c8d708f4..79fe8faa5 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -295,6 +295,7 @@ "expiresOn": "到期", "expiry_and_validity": "到期和有效性", "export_backup": "导出备份", + "export_logs": "导出日志", "extra_id": "额外ID:", "extracted_address_content": "您将汇款至\n${recipient_name}", "failed_authentication": "身份验证失败. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB是一项新协议,它将更快,更便宜和更多的私人交易带给Litecoin", "litecoin_mweb_dismiss": "解雇", "litecoin_mweb_display_card": "显示MWEB卡", + "litecoin_mweb_enable": "启用MWEB", "litecoin_mweb_enable_later": "您可以选择在显示设置下再次启用MWEB。", + "litecoin_mweb_logs": "MWEB日志", + "litecoin_mweb_node": "MWEB节点", "litecoin_mweb_pegin": "钉进", "litecoin_mweb_pegout": "昏倒", "litecoin_mweb_scanning": "MWEB扫描",