Mweb enhancements 4 (#1768)

* [skip-ci] show mweb confirmations, show last mweb balance while syncing * potential send-all fix * [skip-ci] undo fix that didn't work * [skip-ci] undo unnecessary changes * [skip ci] add export mweb logs screen * [skip ci] cleanup * confirmation fixes * catch electrum call errors * [skip ci] undo some changes * potential electrum fixes + mweb logs display only last 10000 characters * Add question mark and link to MWEB card * updates * show negative unconfirmed mweb balanaces + other fixes [skip ci] * error handling * [skip ci] [wip] check if node supports mweb * check fee before building tx * [skip ci] minor * [skip ci] minor * mweb node setting [wip] [skip ci] * prioritize mweb coins when selecting inputs from the pool * potential connection edgecase fix * translations + mweb node fixes * don't use mweb for exchange refund address * add peg in / out labels and make 6 confs only show up for peg in / out * bump bitcoin_base version to v9 * [skip ci] fix logs page * don't fetch txinfo for non-mweb addresses [skip ci] * fix non-mweb confirmations * rename always scan to enable mweb * Update litecoin_wallet_addresses.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Update cw_mweb.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * [skip ci] review updates pt.1 * [skip ci] minor code cleanup * [skip ci] use exception handler * exception handling [skip ci] * [skip ci] exception handling * trigger build * pegout label fixes * fix showing change transactions on peg-out * minor code cleanup and minor peg-out fix * final balance fixes * non-mweb confirmations potential fix * [skip ci] wip * trigger build --------- Co-authored-by: tuxpizza <tuxsudo@tux.pizza> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

Matthew Fosse committed Nov 6, 2024 at 18:57 UTC c8cfc2cff1026c4c433d0f9e683a2c4cbb4023eb
55 files changed +819 -205
cw_bitcoin/lib/electrum.dart
+2
@@ -124,6 +124,7 @@ class ElectrumClient {
124 final errorMsg = error.toString();
125 print(errorMsg);
126 unterminatedString = '';
127 + socket = null;
128 },
129 onDone: () {
130 print("SOCKET CLOSED!!!!!");
@@ -132,6 +133,7 @@ class ElectrumClient {
133 if (host == socket?.address.host || socket == null) {
134 _setConnectionStatus(ConnectionStatus.disconnected);
135 socket?.destroy();
136 + socket = null;
137 }
138 } catch (e) {
139 print("onDone: $e");
cw_bitcoin/lib/electrum_balance.dart
+7 -5
@@ -24,9 +24,12 @@ class ElectrumBalance extends Balance {
24 final decoded = json.decode(jsonSource) as Map;
25
26 return ElectrumBalance(
27 - confirmed: decoded['confirmed'] as int? ?? 0,
28 - unconfirmed: decoded['unconfirmed'] as int? ?? 0,
29 - frozen: decoded['frozen'] as int? ?? 0);
27 + confirmed: decoded['confirmed'] as int? ?? 0,
28 + unconfirmed: decoded['unconfirmed'] as int? ?? 0,
29 + frozen: decoded['frozen'] as int? ?? 0,
30 + secondConfirmed: decoded['secondConfirmed'] as int? ?? 0,
31 + secondUnconfirmed: decoded['secondUnconfirmed'] as int? ?? 0,
32 + );
33 }
34
35 int confirmed;
@@ -36,8 +39,7 @@ class ElectrumBalance extends Balance {
39 int secondUnconfirmed = 0;
40
41 @override
39 - String get formattedAvailableBalance =>
40 - bitcoinAmountToString(amount: confirmed - frozen);
42 + String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen);
43
44 @override
45 String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed);
cw_bitcoin/lib/electrum_transaction_info.dart
+7 -2
@@ -41,6 +41,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
41 String? to,
42 this.unspents,
43 this.isReceivedSilentPayment = false,
44 + Map<String, dynamic>? additionalInfo,
45 }) {
46 this.id = id;
47 this.height = height;
@@ -54,6 +55,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
55 this.isReplaced = isReplaced;
56 this.confirmations = confirmations;
57 this.to = to;
58 + this.additionalInfo = additionalInfo ?? {};
59 }
60
61 factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type,
@@ -212,6 +214,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
214 BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map<String, dynamic>))
215 .toList(),
216 isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false,
217 + additionalInfo: data['additionalInfo'] as Map<String, dynamic>?,
218 );
219 }
220
@@ -246,7 +249,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
249 isReplaced: isReplaced ?? false,
250 inputAddresses: inputAddresses,
251 outputAddresses: outputAddresses,
249 - confirmations: info.confirmations);
252 + confirmations: info.confirmations,
253 + additionalInfo: additionalInfo);
254 }
255
256 Map<String, dynamic> toJson() {
@@ -265,10 +269,11 @@ class ElectrumTransactionInfo extends TransactionInfo {
269 m['inputAddresses'] = inputAddresses;
270 m['outputAddresses'] = outputAddresses;
271 m['isReceivedSilentPayment'] = isReceivedSilentPayment;
272 + m['additionalInfo'] = additionalInfo;
273 return m;
274 }
275
276 String toString() {
272 - 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)';
277 + 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)';
278 }
279 }
cw_bitcoin/lib/electrum_wallet.dart
+76 -54
@@ -5,6 +5,7 @@ import 'dart:isolate';
5
6 import 'package:bitcoin_base/bitcoin_base.dart';
7 import 'package:cw_bitcoin/bitcoin_wallet.dart';
8 +import 'package:cw_bitcoin/litecoin_wallet.dart';
9 import 'package:shared_preferences/shared_preferences.dart';
10 import 'package:blockchain_utils/blockchain_utils.dart';
11 import 'package:collection/collection.dart';
@@ -52,10 +53,9 @@ part 'electrum_wallet.g.dart';
53
54 class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet;
55
55 -abstract class ElectrumWalletBase extends WalletBase<
56 - ElectrumBalance,
57 - ElectrumTransactionHistory,
58 - ElectrumTransactionInfo> with Store, WalletKeysFile {
56 +abstract class ElectrumWalletBase
57 + extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo>
58 + with Store, WalletKeysFile {
59 ElectrumWalletBase({
60 required String password,
61 required WalletInfo walletInfo,
@@ -71,8 +71,8 @@ abstract class ElectrumWalletBase extends WalletBase<
71 ElectrumBalance? initialBalance,
72 CryptoCurrency? currency,
73 this.alwaysScan,
74 - }) : accountHD = getAccountHDWallet(
75 - currency, network, seedBytes, xpub, walletInfo.derivationInfo),
74 + }) : accountHD =
75 + getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo),
76 syncStatus = NotConnectedSyncStatus(),
77 _password = password,
78 _feeRates = <int>[],
@@ -107,12 +107,8 @@ abstract class ElectrumWalletBase extends WalletBase<
107 sharedPrefs.complete(SharedPreferences.getInstance());
108 }
109
110 - static Bip32Slip10Secp256k1 getAccountHDWallet(
111 - CryptoCurrency? currency,
112 - BasedUtxoNetwork network,
113 - Uint8List? seedBytes,
114 - String? xpub,
115 - DerivationInfo? derivationInfo) {
110 + static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network,
111 + Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) {
112 if (seedBytes == null && xpub == null) {
113 throw Exception(
114 "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<
119 case CryptoCurrency.btc:
120 case CryptoCurrency.ltc:
121 case CryptoCurrency.tbtc:
126 - return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network))
127 - .derivePath(_hardenedDerivationPath(
128 - derivationInfo?.derivationPath ?? electrum_path))
122 + return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)).derivePath(
123 + _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path))
124 as Bip32Slip10Secp256k1;
125 case CryptoCurrency.bch:
126 return bitcoinCashHDWallet(seedBytes);
@@ -134,13 +129,11 @@ abstract class ElectrumWalletBase extends WalletBase<
129 }
130 }
131
137 - return Bip32Slip10Secp256k1.fromExtendedKey(
138 - xpub!, getKeyNetVersion(network));
132 + return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network));
133 }
134
135 static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) =>
142 - Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'")
143 - as Bip32Slip10Secp256k1;
136 + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1;
137
138 static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
139 inputsCount * 68 + outputsCounts * 34 + 10;
@@ -250,7 +243,7 @@ abstract class ElectrumWalletBase extends WalletBase<
243 }
244
245 if (tip > walletInfo.restoreHeight) {
253 - _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip);
246 + _setListeners(walletInfo.restoreHeight, chainTipParam: currentChainTip);
247 }
248 } else {
249 alwaysScan = false;
@@ -265,23 +258,23 @@ abstract class ElectrumWalletBase extends WalletBase<
258 }
259 }
260
268 - int? _currentChainTip;
261 + int? currentChainTip;
262
263 Future<int> getCurrentChainTip() async {
271 - if ((_currentChainTip ?? 0) > 0) {
272 - return _currentChainTip!;
264 + if ((currentChainTip ?? 0) > 0) {
265 + return currentChainTip!;
266 }
274 - _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0;
267 + currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0;
268
276 - return _currentChainTip!;
269 + return currentChainTip!;
270 }
271
272 Future<int> getUpdatedChainTip() async {
273 final newTip = await electrumClient.getCurrentBlockChainTip();
281 - if (newTip != null && newTip > (_currentChainTip ?? 0)) {
282 - _currentChainTip = newTip;
274 + if (newTip != null && newTip > (currentChainTip ?? 0)) {
275 + currentChainTip = newTip;
276 }
284 - return _currentChainTip ?? 0;
277 + return currentChainTip ?? 0;
278 }
279
280 @override
@@ -357,7 +350,7 @@ abstract class ElectrumWalletBase extends WalletBase<
350 isSingleScan: doSingleScan ?? false,
351 ));
352
360 - _receiveStream?.cancel();
353 + await _receiveStream?.cancel();
354 _receiveStream = receivePort.listen((var message) async {
355 if (message is Map<String, ElectrumTransactionInfo>) {
356 for (final map in message.entries) {
@@ -618,7 +611,7 @@ abstract class ElectrumWalletBase extends WalletBase<
611 bool spendsUnconfirmedTX = false;
612
613 int leftAmount = credentialsAmount;
621 - final availableInputs = unspentCoins.where((utx) {
614 + var availableInputs = unspentCoins.where((utx) {
615 if (!utx.isSending || utx.isFrozen) {
616 return false;
617 }
@@ -634,6 +627,9 @@ abstract class ElectrumWalletBase extends WalletBase<
627 }).toList();
628 final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList();
629
630 + // sort the unconfirmed coins so that mweb coins are first:
631 + availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? -1 : 1);
632 +
633 for (int i = 0; i < availableInputs.length; i++) {
634 final utx = availableInputs[i];
635 if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
@@ -652,9 +648,8 @@ abstract class ElectrumWalletBase extends WalletBase<
648 ECPrivate? privkey;
649 bool? isSilentPayment = false;
650
655 - final hd = utx.bitcoinAddressRecord.isHidden
656 - ? walletAddresses.sideHd
657 - : walletAddresses.mainHd;
651 + final hd =
652 + utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd;
653
654 if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
655 final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
@@ -1233,8 +1228,7 @@ abstract class ElectrumWalletBase extends WalletBase<
1228 }
1229 }
1230
1236 - void setLedgerConnection(ledger.LedgerConnection connection) =>
1237 - throw UnimplementedError();
1231 + void setLedgerConnection(ledger.LedgerConnection connection) => throw UnimplementedError();
1232
1233 Future<BtcTransaction> buildHardwareWalletTransaction({
1234 required List<BitcoinBaseOutput> outputs,
@@ -1593,9 +1587,7 @@ abstract class ElectrumWalletBase extends WalletBase<
1587
1588 final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
1589 final privkey = generateECPrivate(
1596 - hd: addressRecord.isHidden
1597 - ? walletAddresses.sideHd
1598 - : walletAddresses.mainHd,
1590 + hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
1591 index: addressRecord.index,
1592 network: network);
1593
@@ -1777,8 +1769,7 @@ abstract class ElectrumWalletBase extends WalletBase<
1769
1770 if (height != null) {
1771 if (time == null && height > 0) {
1780 - time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000)
1781 - .round();
1772 + time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round();
1773 }
1774
1775 if (confirmations == null) {
@@ -1847,6 +1838,7 @@ abstract class ElectrumWalletBase extends WalletBase<
1838 .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
1839 } else if (type == WalletType.litecoin) {
1840 await Future.wait(LITECOIN_ADDRESS_TYPES
1841 + .where((type) => type != SegwitAddresType.mweb)
1842 .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
1843 }
1844
@@ -1958,6 +1950,20 @@ abstract class ElectrumWalletBase extends WalletBase<
1950
1951 // Got a new transaction fetched, add it to the transaction history
1952 // instead of waiting all to finish, and next time it will be faster
1953 +
1954 + if (this is LitecoinWallet) {
1955 + // if we have a peg out transaction with the same value
1956 + // that matches this received transaction, mark it as being from a peg out:
1957 + for (final tx2 in transactionHistory.transactions.values) {
1958 + final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs();
1959 + // 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
1960 + if (tx2.additionalInfo["isPegOut"] == true &&
1961 + tx2.amount == tx.amount &&
1962 + heightDiff <= 5) {
1963 + tx.additionalInfo["fromPegOut"] = true;
1964 + }
1965 + }
1966 + }
1967 transactionHistory.addOne(tx);
1968 await transactionHistory.save();
1969 }
@@ -1984,18 +1990,28 @@ abstract class ElectrumWalletBase extends WalletBase<
1990 if (_isTransactionUpdating) {
1991 return;
1992 }
1987 - await getCurrentChainTip();
1993 + currentChainTip = await getUpdatedChainTip();
1994
1995 + bool updated = false;
1996 transactionHistory.transactions.values.forEach((tx) {
1990 - if (tx.unspents != null &&
1991 - tx.unspents!.isNotEmpty &&
1992 - tx.height != null &&
1993 - tx.height! > 0 &&
1994 - (_currentChainTip ?? 0) > 0) {
1995 - tx.confirmations = _currentChainTip! - tx.height! + 1;
1997 + if ((tx.height ?? 0) > 0 && (currentChainTip ?? 0) > 0) {
1998 + var confirmations = currentChainTip! - tx.height! + 1;
1999 + if (confirmations < 0) {
2000 + // if our chain tip is outdated then it could lead to negative confirmations so this is just a failsafe:
2001 + confirmations = 0;
2002 + }
2003 + if (confirmations != tx.confirmations) {
2004 + updated = true;
2005 + tx.confirmations = confirmations;
2006 + transactionHistory.addOne(tx);
2007 + }
2008 }
2009 });
2010
2011 + if (updated) {
2012 + await transactionHistory.save();
2013 + }
2014 +
2015 _isTransactionUpdating = true;
2016 await fetchTransactions();
2017 walletAddresses.updateReceiveAddresses();
@@ -2043,6 +2059,8 @@ abstract class ElectrumWalletBase extends WalletBase<
2059 library: this.runtimeType.toString(),
2060 ));
2061 }
2062 + }, onError: (e, s) {
2063 + print("sub_listen error: $e $s");
2064 });
2065 }));
2066 }
@@ -2092,6 +2110,13 @@ abstract class ElectrumWalletBase extends WalletBase<
2110
2111 final balances = await Future.wait(balanceFutures);
2112
2113 + if (balances.isNotEmpty && balances.first['confirmed'] == null) {
2114 + // if we got null balance responses from the server, set our connection status to lost and return our last known balance:
2115 + print("got null balance responses from the server, setting connection status to lost");
2116 + syncStatus = LostConnectionSyncStatus();
2117 + return balance[currency] ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
2118 + }
2119 +
2120 for (var i = 0; i < balances.length; i++) {
2121 final addressRecord = addresses[i];
2122 final balance = balances[i];
@@ -2197,10 +2222,10 @@ abstract class ElectrumWalletBase extends WalletBase<
2222 Future<void> _setInitialHeight() async {
2223 if (_chainTipUpdateSubject != null) return;
2224
2200 - _currentChainTip = await getUpdatedChainTip();
2225 + currentChainTip = await getUpdatedChainTip();
2226
2202 - if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) {
2203 - await walletInfo.updateRestoreHeight(_currentChainTip!);
2227 + if ((currentChainTip == null || currentChainTip! == 0) && walletInfo.restoreHeight == 0) {
2228 + await walletInfo.updateRestoreHeight(currentChainTip!);
2229 }
2230
2231 _chainTipUpdateSubject = electrumClient.chainTipSubscribe();
@@ -2209,7 +2234,7 @@ abstract class ElectrumWalletBase extends WalletBase<
2234 final height = int.tryParse(event['height'].toString());
2235
2236 if (height != null) {
2212 - _currentChainTip = height;
2237 + currentChainTip = height;
2238
2239 if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
2240 _setListeners(walletInfo.restoreHeight);
@@ -2223,7 +2248,6 @@ abstract class ElectrumWalletBase extends WalletBase<
2248
2249 @action
2250 void _onConnectionStatusChange(ConnectionStatus status) {
2226 -
2251 switch (status) {
2252 case ConnectionStatus.connected:
2253 if (syncStatus is NotConnectedSyncStatus ||
@@ -2270,8 +2294,6 @@ abstract class ElectrumWalletBase extends WalletBase<
2294 Timer(Duration(seconds: 5), () {
2295 if (this.syncStatus is NotConnectedSyncStatus ||
2296 this.syncStatus is LostConnectionSyncStatus) {
2273 - if (node == null) return;
2274 -
2297 this.electrumClient.connectToUri(
2298 node!.uri,
2299 useSSL: node!.useSSL ?? false,
cw_bitcoin/lib/litecoin_wallet.dart
+208 -98
@@ -9,6 +9,7 @@ import 'package:crypto/crypto.dart';
9 import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
10 import 'package:cw_core/cake_hive.dart';
11 import 'package:cw_core/mweb_utxo.dart';
12 +import 'package:cw_core/node.dart';
13 import 'package:cw_mweb/mwebd.pbgrpc.dart';
14 import 'package:fixnum/fixnum.dart';
15 import 'package:bip39/bip39.dart' as bip39;
@@ -47,6 +48,7 @@ import 'package:cw_mweb/cw_mweb.dart';
48 import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
49 import 'package:pointycastle/ecc/api.dart';
50 import 'package:pointycastle/ecc/curves/secp256k1.dart';
51 +import 'package:shared_preferences/shared_preferences.dart';
52
53 part 'litecoin_wallet.g.dart';
54
@@ -85,8 +87,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
87 alwaysScan: alwaysScan,
88 ) {
89 if (seedBytes != null) {
88 - mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
89 - "m/1000'") as Bip32Slip10Secp256k1;
90 + mwebHd =
91 + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1;
92 mwebEnabled = alwaysScan ?? false;
93 } else {
94 mwebHd = null;
@@ -287,6 +289,16 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
289 await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020);
290 }
291
292 + @action
293 + @override
294 + Future<void> connectToNode({required Node node}) async {
295 + await super.connectToNode(node: node);
296 +
297 + final prefs = await SharedPreferences.getInstance();
298 + final mwebNodeUri = prefs.getString("mwebNodeUri") ?? "ltc-electrum.cakewallet.com:9333";
299 + await CwMweb.setNodeUriOverride(mwebNodeUri);
300 + }
301 +
302 @action
303 @override
304 Future<void> startSync() async {
@@ -349,6 +361,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
361 return;
362 }
363
364 + // update the current chain tip so that confirmation calculations are accurate:
365 + currentChainTip = nodeHeight;
366 +
367 final resp = await CwMweb.status(StatusRequest());
368
369 try {
@@ -361,22 +376,46 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
376 } else if (resp.mwebUtxosHeight < nodeHeight) {
377 mwebSyncStatus = SyncingSyncStatus(1, 0.999);
378 } else {
379 + bool confirmationsUpdated = false;
380 if (resp.mwebUtxosHeight > walletInfo.restoreHeight) {
381 await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight);
382 await checkMwebUtxosSpent();
383 // update the confirmations for each transaction:
368 - for (final transaction in transactionHistory.transactions.values) {
369 - if (transaction.isPending) continue;
370 - int txHeight = transaction.height ?? resp.mwebUtxosHeight;
371 - final confirmations = (resp.mwebUtxosHeight - txHeight) + 1;
372 - if (transaction.confirmations == confirmations) continue;
373 - if (transaction.confirmations == 0) {
374 - updateBalance();
384 + for (final tx in transactionHistory.transactions.values) {
385 + if (tx.height == null || tx.height == 0) {
386 + // update with first confirmation on next block since it hasn't been confirmed yet:
387 + tx.height = resp.mwebUtxosHeight;
388 + continue;
389 }
376 - transaction.confirmations = confirmations;
377 - transactionHistory.addOne(transaction);
390 +
391 + final confirmations = (resp.mwebUtxosHeight - tx.height!) + 1;
392 +
393 + // if the confirmations haven't changed, skip updating:
394 + if (tx.confirmations == confirmations) continue;
395 +
396 +
397 + // if an outgoing tx is now confirmed, delete the utxo from the box (delete the unspent coin):
398 + if (confirmations >= 2 &&
399 + tx.direction == TransactionDirection.outgoing &&
400 + tx.unspents != null) {
401 + for (var coin in tx.unspents!) {
402 + final utxo = mwebUtxosBox.get(coin.address);
403 + if (utxo != null) {
404 + print("deleting utxo ${coin.address} @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
405 + await mwebUtxosBox.delete(coin.address);
406 + }
407 + }
408 + }
409 +
410 + tx.confirmations = confirmations;
411 + tx.isPending = false;
412 + transactionHistory.addOne(tx);
413 + confirmationsUpdated = true;
414 + }
415 + if (confirmationsUpdated) {
416 + await transactionHistory.save();
417 + await updateTransactions();
418 }
379 - await transactionHistory.save();
419 }
420
421 // prevent unnecessary reaction triggers:
@@ -501,13 +540,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
540 outputAddresses: [utxo.outputId],
541 isReplaced: false,
542 );
504 - }
505 -
506 - // don't update the confirmations if the tx is updated by electrum:
507 - if (tx.confirmations == 0 || utxo.height != 0) {
508 - tx.height = utxo.height;
509 - tx.isPending = utxo.height == 0;
510 - tx.confirmations = confirmations;
543 + } else {
544 + if (tx.confirmations != confirmations || tx.height != utxo.height) {
545 + tx.height = utxo.height;
546 + tx.confirmations = confirmations;
547 + tx.isPending = utxo.height == 0;
548 + }
549 }
550
551 bool isNew = transactionHistory.transactions[tx.id] == null;
@@ -557,56 +595,88 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
595 if (responseStream == null) {
596 throw Exception("failed to get utxos stream!");
597 }
560 - _utxoStream = responseStream.listen((Utxo sUtxo) async {
561 - // we're processing utxos, so our balance could still be innacurate:
562 - if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) {
563 - mwebSyncStatus = SyncronizingSyncStatus();
564 - processingUtxos = true;
565 - _processingTimer?.cancel();
566 - _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
567 - processingUtxos = false;
568 - timer.cancel();
569 - });
570 - }
598 + _utxoStream = responseStream.listen(
599 + (Utxo sUtxo) async {
600 + // we're processing utxos, so our balance could still be innacurate:
601 + if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) {
602 + mwebSyncStatus = SyncronizingSyncStatus();
603 + processingUtxos = true;
604 + _processingTimer?.cancel();
605 + _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
606 + processingUtxos = false;
607 + timer.cancel();
608 + });
609 + }
610
572 - final utxo = MwebUtxo(
573 - address: sUtxo.address,
574 - blockTime: sUtxo.blockTime,
575 - height: sUtxo.height,
576 - outputId: sUtxo.outputId,
577 - value: sUtxo.value.toInt(),
578 - );
611 + final utxo = MwebUtxo(
612 + address: sUtxo.address,
613 + blockTime: sUtxo.blockTime,
614 + height: sUtxo.height,
615 + outputId: sUtxo.outputId,
616 + value: sUtxo.value.toInt(),
617 + );
618
580 - if (mwebUtxosBox.containsKey(utxo.outputId)) {
581 - // we've already stored this utxo, skip it:
582 - // but do update the utxo height if it's somehow different:
583 - final existingUtxo = mwebUtxosBox.get(utxo.outputId);
584 - if (existingUtxo!.height != utxo.height) {
585 - print(
586 - "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}");
587 - existingUtxo.height = utxo.height;
588 - await mwebUtxosBox.put(utxo.outputId, existingUtxo);
619 + if (mwebUtxosBox.containsKey(utxo.outputId)) {
620 + // we've already stored this utxo, skip it:
621 + // but do update the utxo height if it's somehow different:
622 + final existingUtxo = mwebUtxosBox.get(utxo.outputId);
623 + if (existingUtxo!.height != utxo.height) {
624 + print(
625 + "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}");
626 + existingUtxo.height = utxo.height;
627 + await mwebUtxosBox.put(utxo.outputId, existingUtxo);
628 + }
629 + return;
630 }
590 - return;
591 - }
631
593 - await updateUnspent();
594 - await updateBalance();
632 + await updateUnspent();
633 + await updateBalance();
634
596 - final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
635 + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
636
598 - // don't process utxos with addresses that are not in the mwebAddrs list:
599 - if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) {
600 - return;
601 - }
637 + // don't process utxos with addresses that are not in the mwebAddrs list:
638 + if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) {
639 + return;
640 + }
641
603 - await mwebUtxosBox.put(utxo.outputId, utxo);
642 + await mwebUtxosBox.put(utxo.outputId, utxo);
643
605 - await handleIncoming(utxo);
606 - });
644 + await handleIncoming(utxo);
645 + },
646 + onError: (error) {
647 + print("error in utxo stream: $error");
648 + mwebSyncStatus = FailedSyncStatus(error: error.toString());
649 + },
650 + cancelOnError: true,
651 + );
652 + }
653 +
654 + Future<void> deleteSpentUtxos() async {
655 + print("deleteSpentUtxos() called!");
656 + final chainHeight = await electrumClient.getCurrentBlockChainTip();
657 + final status = await CwMweb.status(StatusRequest());
658 + if (chainHeight == null || status.blockHeaderHeight != chainHeight) return;
659 + if (status.mwebUtxosHeight != chainHeight) return; // we aren't synced
660 +
661 + // delete any spent utxos with >= 2 confirmations:
662 + final spentOutputIds = mwebUtxosBox.values
663 + .where((utxo) => utxo.spent && (chainHeight - utxo.height) >= 2)
664 + .map((utxo) => utxo.outputId)
665 + .toList();
666 +
667 + if (spentOutputIds.isEmpty) return;
668 +
669 + final resp = await CwMweb.spent(SpentRequest(outputId: spentOutputIds));
670 + final spent = resp.outputId;
671 + if (spent.isEmpty) return;
672 +
673 + for (final outputId in spent) {
674 + await mwebUtxosBox.delete(outputId);
675 + }
676 }
677
678 Future<void> checkMwebUtxosSpent() async {
679 + print("checkMwebUtxosSpent() called!");
680 if (!mwebEnabled) {
681 return;
682 }
@@ -620,15 +690,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
690 updatedAny = await isConfirmed(tx) || updatedAny;
691 }
692
693 + await deleteSpentUtxos();
694 +
695 // get output ids of all the mweb utxos that have > 0 height:
624 - final outputIds =
625 - mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList();
696 + final outputIds = mwebUtxosBox.values
697 + .where((utxo) => utxo.height > 0 && !utxo.spent)
698 + .map((utxo) => utxo.outputId)
699 + .toList();
700
701 final resp = await CwMweb.spent(SpentRequest(outputId: outputIds));
702 final spent = resp.outputId;
629 - if (spent.isEmpty) {
630 - return;
631 - }
703 + if (spent.isEmpty) return;
704
705 final status = await CwMweb.status(StatusRequest());
706 final height = await electrumClient.getCurrentBlockChainTip();
@@ -739,7 +811,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
811 mwebUtxosBox.keys.forEach((dynamic oId) {
812 final String outputId = oId as String;
813 final utxo = mwebUtxosBox.get(outputId);
742 - if (utxo == null) {
814 + if (utxo == null || utxo.spent) {
815 return;
816 }
817 if (utxo.address.isEmpty) {
@@ -789,15 +861,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
861 int unconfirmedMweb = 0;
862 try {
863 mwebUtxosBox.values.forEach((utxo) {
792 - if (utxo.height > 0) {
864 + bool isConfirmed = utxo.height > 0;
865 +
866 + print(
867 + "utxo: ${isConfirmed ? "confirmed" : "unconfirmed"} ${utxo.spent ? "spent" : "unspent"} ${utxo.outputId} ${utxo.height} ${utxo.value}");
868 +
869 + if (isConfirmed) {
870 confirmedMweb += utxo.value.toInt();
794 - } else {
871 + }
872 +
873 + if (isConfirmed && utxo.spent) {
874 + unconfirmedMweb -= utxo.value.toInt();
875 + }
876 +
877 + if (!isConfirmed && !utxo.spent) {
878 unconfirmedMweb += utxo.value.toInt();
879 }
880 });
798 - if (unconfirmedMweb > 0) {
799 - unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
800 - }
881 } catch (_) {}
882
883 for (var addressRecord in walletAddresses.allAddresses) {
@@ -829,7 +909,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
909 // update the txCount for each address using the tx history, since we can't rely on mwebd
910 // to have an accurate count, we should just keep it in sync with what we know from the tx history:
911 for (final tx in transactionHistory.transactions.values) {
832 - // if (tx.isPending) continue;
912 if (tx.inputAddresses == null || tx.outputAddresses == null) {
913 continue;
914 }
@@ -908,7 +987,26 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
987 // https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation
988 final preOutputSum =
989 outputs.fold<BigInt>(BigInt.zero, (acc, output) => acc + output.toOutput.amount);
911 - final fee = utxos.sumOfUtxosValue() - preOutputSum;
990 + var fee = utxos.sumOfUtxosValue() - preOutputSum;
991 +
992 + // determines if the fee is correct:
993 + BigInt _sumOutputAmounts(List<TxOutput> outputs) {
994 + BigInt sum = BigInt.zero;
995 + for (final e in outputs) {
996 + sum += e.amount;
997 + }
998 + return sum;
999 + }
1000 +
1001 + final sum1 = _sumOutputAmounts(outputs.map((e) => e.toOutput).toList()) + fee;
1002 + final sum2 = utxos.sumOfUtxosValue();
1003 + if (sum1 != sum2) {
1004 + print("@@@@@ WE HAD TO ADJUST THE FEE! @@@@@@@@");
1005 + final diff = sum2 - sum1;
1006 + // add the difference to the fee (abs value):
1007 + fee += diff.abs();
1008 + }
1009 +
1010 final txb =
1011 BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network);
1012 final resp = await CwMweb.create(CreateRequest(
@@ -949,8 +1047,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1047
1048 if (!mwebEnabled) {
1049 tx.changeAddressOverride =
952 - (await (walletAddresses as LitecoinWalletAddresses)
953 - .getChangeAddress(isPegIn: false))
1050 + (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false))
1051 .address;
1052 return tx;
1053 }
@@ -969,15 +1066,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1066
1067 bool hasMwebInput = false;
1068 bool hasMwebOutput = false;
1069 + bool hasRegularOutput = false;
1070
1071 for (final output in transactionCredentials.outputs) {
974 - if (output.extractedAddress?.toLowerCase().contains("mweb") ?? false) {
1072 + final address = output.address.toLowerCase();
1073 + final extractedAddress = output.extractedAddress?.toLowerCase();
1074 +
1075 + if (address.contains("mweb")) {
1076 hasMwebOutput = true;
976 - break;
1077 }
978 - if (output.address.toLowerCase().contains("mweb")) {
979 - hasMwebOutput = true;
980 - break;
1078 + if (!address.contains("mweb")) {
1079 + hasRegularOutput = true;
1080 + }
1081 + if (extractedAddress != null && extractedAddress.isNotEmpty) {
1082 + if (extractedAddress.contains("mweb")) {
1083 + hasMwebOutput = true;
1084 + }
1085 + if (!extractedAddress.contains("mweb")) {
1086 + hasRegularOutput = true;
1087 + }
1088 }
1089 }
1090
@@ -989,11 +1096,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1096 }
1097
1098 bool isPegIn = !hasMwebInput && hasMwebOutput;
1099 + bool isPegOut = hasMwebInput && hasRegularOutput;
1100 bool isRegular = !hasMwebInput && !hasMwebOutput;
993 - tx.changeAddressOverride =
994 - (await (walletAddresses as LitecoinWalletAddresses)
995 - .getChangeAddress(isPegIn: isPegIn || isRegular))
996 - .address;
1101 + tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses)
1102 + .getChangeAddress(isPegIn: isPegIn || isRegular))
1103 + .address;
1104 if (!hasMwebInput && !hasMwebOutput) {
1105 tx.isMweb = false;
1106 return tx;
@@ -1046,8 +1153,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1153 final addresses = <String>{};
1154 transaction.inputAddresses?.forEach((id) async {
1155 final utxo = mwebUtxosBox.get(id);
1049 - await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent
1156 + // await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent
1157 if (utxo == null) return;
1158 + // mark utxo as spent so we add it to the unconfirmed balance (as negative):
1159 + utxo.spent = true;
1160 + await mwebUtxosBox.put(id, utxo);
1161 final addressRecord = walletAddresses.allAddresses
1162 .firstWhere((addressRecord) => addressRecord.address == utxo.address);
1163 if (!addresses.contains(utxo.address)) {
@@ -1056,7 +1166,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1166 addressRecord.balance -= utxo.value.toInt();
1167 });
1168 transaction.inputAddresses?.addAll(addresses);
1059 -
1169 + print("isPegIn: $isPegIn, isPegOut: $isPegOut");
1170 + transaction.additionalInfo["isPegIn"] = isPegIn;
1171 + transaction.additionalInfo["isPegOut"] = isPegOut;
1172 transactionHistory.addOne(transaction);
1173 await updateUnspent();
1174 await updateBalance();
@@ -1240,8 +1352,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1352 @override
1353 void setLedgerConnection(LedgerConnection connection) {
1354 _ledgerConnection = connection;
1243 - _litecoinLedgerApp =
1244 - LitecoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!);
1355 + _litecoinLedgerApp = LitecoinLedgerApp(_ledgerConnection!,
1356 + derivationPath: walletInfo.derivationInfo!.derivationPath!);
1357 }
1358
1359 @override
@@ -1277,19 +1389,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1389 if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath;
1390 }
1391
1280 -
1392 final rawHex = await _litecoinLedgerApp!.createTransaction(
1282 - inputs: readyInputs,
1283 - outputs: outputs
1284 - .map((e) => TransactionOutput.fromBigInt(
1285 - (e as BitcoinOutput).value, Uint8List.fromList(e.address.toScriptPubKey().toBytes())))
1286 - .toList(),
1287 - changePath: changePath,
1288 - sigHashType: 0x01,
1289 - additionals: ["bech32"],
1290 - isSegWit: true,
1291 - useTrustedInputForSegwit: true
1292 - );
1393 + inputs: readyInputs,
1394 + outputs: outputs
1395 + .map((e) => TransactionOutput.fromBigInt((e as BitcoinOutput).value,
1396 + Uint8List.fromList(e.address.toScriptPubKey().toBytes())))
1397 + .toList(),
1398 + changePath: changePath,
1399 + sigHashType: 0x01,
1400 + additionals: ["bech32"],
1401 + isSegWit: true,
1402 + useTrustedInputForSegwit: true);
1403
1404 return BtcTransaction.fromRaw(rawHex);
1405 }
cw_bitcoin/lib/litecoin_wallet_addresses.dart
+11 -6
@@ -16,11 +16,9 @@ import 'package:mobx/mobx.dart';
16
17 part 'litecoin_wallet_addresses.g.dart';
18
19 -class LitecoinWalletAddresses = LitecoinWalletAddressesBase
20 - with _$LitecoinWalletAddresses;
19 +class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses;
20
22 -abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses
23 - with Store {
21 +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
22 LitecoinWalletAddressesBase(
23 WalletInfo walletInfo, {
24 required super.mainHd,
@@ -46,8 +44,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses
44 List<String> mwebAddrs = [];
45 bool generating = false;
46
49 - List<int> get scanSecret =>
50 - mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
47 + List<int> get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
48 List<int> get spendPubkey =>
49 mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed;
50
@@ -203,4 +200,12 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses
200
201 return super.getChangeAddress();
202 }
203 +
204 + @override
205 + String get addressForExchange {
206 + // don't use mweb addresses for exchange refund address:
207 + final addresses = receiveAddresses
208 + .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed);
209 + return addresses.first.address;
210 + }
211 }
cw_bitcoin/lib/litecoin_wallet_service.dart
+4
@@ -112,6 +112,7 @@ class LitecoinWalletService extends WalletService<
112 File neturinoDb = File('$appDirPath/neutrino.db');
113 File blockHeaders = File('$appDirPath/block_headers.bin');
114 File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin');
115 + File mwebdLogs = File('$appDirPath/logs/debug.log');
116 if (neturinoDb.existsSync()) {
117 neturinoDb.deleteSync();
118 }
@@ -121,6 +122,9 @@ class LitecoinWalletService extends WalletService<
122 if (regFilterHeaders.existsSync()) {
123 regFilterHeaders.deleteSync();
124 }
125 + if (mwebdLogs.existsSync()) {
126 + mwebdLogs.deleteSync();
127 + }
128 }
129 }
130
cw_bitcoin/lib/pending_bitcoin_transaction.dart
+1 -2
@@ -118,8 +118,7 @@ class PendingBitcoinTransaction with PendingTransaction {
118
119 Future<void> _ltcCommit() async {
120 try {
121 - final stub = await CwMweb.stub();
122 - final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex)));
121 + final resp = await CwMweb.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex)));
122 idOverride = resp.txid;
123 } on GrpcError catch (e) {
124 throw BitcoinTransactionCommitFailed(errorMessage: e.message);
cw_bitcoin/pubspec.yaml
+1 -1
@@ -64,7 +64,7 @@ dependency_overrides:
64 bitcoin_base:
65 git:
66 url: https://github.com/cake-tech/bitcoin_base
67 - ref: cake-update-v8
67 + ref: cake-update-v9
68 pointycastle: 3.7.4
69 ffi: 2.1.0
70
cw_bitcoin_cash/pubspec.yaml
+1 -1
@@ -42,7 +42,7 @@ dependency_overrides:
42 bitcoin_base:
43 git:
44 url: https://github.com/cake-tech/bitcoin_base
45 - ref: cake-update-v8
45 + ref: cake-update-v9
46
47 # For information on the generic Dart part of this file, see the
48 # following page: https://dart.dev/tools/pub/pubspec
cw_core/lib/mweb_utxo.dart
+4
@@ -11,6 +11,7 @@ class MwebUtxo extends HiveObject {
11 required this.address,
12 required this.outputId,
13 required this.blockTime,
14 + this.spent = false,
15 });
16
17 static const typeId = MWEB_UTXO_TYPE_ID;
@@ -30,4 +31,7 @@ class MwebUtxo extends HiveObject {
31
32 @HiveField(4)
33 int blockTime;
34 +
35 + @HiveField(5, defaultValue: false)
36 + bool spent;
37 }
cw_core/lib/node.dart
+3
@@ -79,6 +79,9 @@ class Node extends HiveObject with Keyable {
79 @HiveField(9)
80 bool? supportsSilentPayments;
81
82 + @HiveField(10)
83 + bool? supportsMweb;
84 +
85 bool get isSSL => useSSL ?? false;
86
87 bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;
cw_core/lib/transaction_info.dart
+1 -2
@@ -25,6 +25,5 @@ abstract class TransactionInfo extends Object with Keyable {
25 @override
26 dynamic get keyIndex => id;
27
28 - late Map<String, dynamic> additionalInfo;
28 + Map<String, dynamic> additionalInfo = {};
29 }
30 -
cw_mweb/lib/cw_mweb.dart
+26 -2
@@ -13,8 +13,18 @@ class CwMweb {
13 static RpcClient? _rpcClient;
14 static ClientChannel? _clientChannel;
15 static int? _port;
16 - static const TIMEOUT_DURATION = Duration(seconds: 5);
16 + static const TIMEOUT_DURATION = Duration(seconds: 15);
17 static Timer? logTimer;
18 + static String? nodeUriOverride;
19 +
20 +
21 + static Future<void> setNodeUriOverride(String uri) async {
22 + nodeUriOverride = uri;
23 + if (_rpcClient != null) {
24 + await stop();
25 + // will be re-started automatically when the next rpc call is made
26 + }
27 + }
28
29 static void readFileWithTimer(String filePath) {
30 final file = File(filePath);
@@ -47,7 +57,7 @@ class CwMweb {
57 String debugLogPath = "${appDir.path}/logs/debug.log";
58 readFileWithTimer(debugLogPath);
59
50 - _port = await CwMwebPlatform.instance.start(appDir.path, ltcNodeUri);
60 + _port = await CwMwebPlatform.instance.start(appDir.path, nodeUriOverride ?? ltcNodeUri);
61 if (_port == null || _port == 0) {
62 throw Exception("Failed to start server");
63 }
@@ -197,4 +207,18 @@ class CwMweb {
207 }
208 return null;
209 }
210 +
211 + static Future<BroadcastResponse> broadcast(BroadcastRequest request) async {
212 + log("mweb.broadcast() called");
213 + try {
214 + _rpcClient = await stub();
215 + return await _rpcClient!.broadcast(request, options: CallOptions(timeout: TIMEOUT_DURATION));
216 + } on GrpcError catch (e) {
217 + log('Caught grpc error: ${e.message}');
218 + throw "error from broadcast mweb: $e";
219 + } catch (e) {
220 + log("Error getting create: $e");
221 + rethrow;
222 + }
223 + }
224 }
lib/di.dart
+6
@@ -35,6 +35,8 @@ import 'package:cake_wallet/entities/parse_address_from_domain.dart';
35 import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
36 import 'package:cake_wallet/entities/wallet_manager.dart';
37 import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
38 +import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
39 +import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
40 import 'package:cake_wallet/view_model/link_view_model.dart';
41 import 'package:cake_wallet/tron/tron.dart';
42 import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
@@ -945,6 +947,10 @@ Future<void> setup({
947
948 getIt.registerFactory(() => MwebSettingsPage(getIt.get<MwebSettingsViewModel>()));
949
950 + getIt.registerFactory(() => MwebLogsPage(getIt.get<MwebSettingsViewModel>()));
951 +
952 + getIt.registerFactory(() => MwebNodePage(getIt.get<MwebSettingsViewModel>()));
953 +
954 getIt.registerFactory(() => OtherSettingsPage(getIt.get<OtherSettingsViewModel>()));
955
956 getIt.registerFactory(() => NanoChangeRepPage(
lib/entities/preferences_key.dart
+1
@@ -54,6 +54,7 @@ class PreferencesKey {
54 static const mwebEnabled = 'mwebEnabled';
55 static const hasEnabledMwebBefore = 'hasEnabledMwebBefore';
56 static const mwebAlwaysScan = 'mwebAlwaysScan';
57 + static const mwebNodeUri = 'mwebNodeUri';
58 static const shouldShowReceiveWarning = 'should_show_receive_warning';
59 static const shouldShowYatPopup = 'should_show_yat_popup';
60 static const shouldShowRepWarning = 'should_show_rep_warning';
lib/router.dart
+10
@@ -72,6 +72,8 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin
72 import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
73 import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart';
74 import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
75 +import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
76 +import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
77 import 'package:cake_wallet/src/screens/settings/mweb_settings.dart';
78 import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
79 import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
@@ -461,6 +463,14 @@ Route<dynamic> createRoute(RouteSettings settings) {
463 return CupertinoPageRoute<void>(
464 fullscreenDialog: true, builder: (_) => getIt.get<MwebSettingsPage>());
465
466 + case Routes.mwebLogs:
467 + return CupertinoPageRoute<void>(
468 + fullscreenDialog: true, builder: (_) => getIt.get<MwebLogsPage>());
469 +
470 + case Routes.mwebNode:
471 + return CupertinoPageRoute<void>(
472 + fullscreenDialog: true, builder: (_) => getIt.get<MwebNodePage>());
473 +
474 case Routes.connectionSync:
475 return CupertinoPageRoute<void>(
476 fullscreenDialog: true, builder: (_) => getIt.get<ConnectionSyncPage>());
lib/routes.dart
+2
@@ -74,6 +74,8 @@ class Routes {
74 static const webViewPage = '/web_view_page';
75 static const silentPaymentsSettings = '/silent_payments_settings';
76 static const mwebSettings = '/mweb_settings';
77 + static const mwebLogs = '/mweb_logs';
78 + static const mwebNode = '/mweb_node';
79 static const connectionSync = '/connection_sync_page';
80 static const securityBackupPage = '/security_and_backup_page';
81 static const privacyPage = '/privacy_page';
lib/src/screens/dashboard/pages/balance_page.dart
+31 -11
@@ -886,17 +886,37 @@ class BalanceRowWidget extends StatelessWidget {
886 Column(
887 crossAxisAlignment: CrossAxisAlignment.start,
888 children: [
889 - Text(
890 - '${secondAvailableBalanceLabel}',
891 - textAlign: TextAlign.center,
892 - style: TextStyle(
893 - fontSize: 12,
894 - fontFamily: 'Lato',
895 - fontWeight: FontWeight.w400,
896 - color: Theme.of(context)
897 - .extension<BalancePageTheme>()!
898 - .labelTextColor,
899 - height: 1,
889 + GestureDetector(
890 + behavior: HitTestBehavior.opaque,
891 + onTap: () => launchUrl(
892 + Uri.parse(
893 + "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"),
894 + mode: LaunchMode.externalApplication,
895 + ),
896 + child: Row(
897 + children: [
898 + Text(
899 + '${secondAvailableBalanceLabel}',
900 + textAlign: TextAlign.center,
901 + style: TextStyle(
902 + fontSize: 12,
903 + fontFamily: 'Lato',
904 + fontWeight: FontWeight.w400,
905 + color: Theme.of(context)
906 + .extension<BalancePageTheme>()!
907 + .labelTextColor,
908 + height: 1,
909 + ),
910 + ),
911 + Padding(
912 + padding: const EdgeInsets.symmetric(horizontal: 4),
913 + child: Icon(Icons.help_outline,
914 + size: 16,
915 + color: Theme.of(context)
916 + .extension<BalancePageTheme>()!
917 + .labelTextColor),
918 + )
919 + ],
920 ),
921 ),
922 SizedBox(height: 8),
lib/src/screens/settings/mweb_logs_page.dart new
+127
@@ -0,0 +1,127 @@
1 +import 'dart:io';
2 +import 'package:cake_wallet/src/screens/base_page.dart';
3 +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
4 +import 'package:cake_wallet/src/widgets/primary_button.dart';
5 +import 'package:cake_wallet/generated/i18n.dart';
6 +import 'package:cake_wallet/utils/exception_handler.dart';
7 +import 'package:cake_wallet/utils/share_util.dart';
8 +import 'package:cake_wallet/utils/show_pop_up.dart';
9 +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart';
10 +import 'package:cw_core/root_dir.dart';
11 +import 'package:file_picker/file_picker.dart';
12 +import 'package:flutter/material.dart';
13 +import 'package:path_provider/path_provider.dart';
14 +
15 +class MwebLogsPage extends BasePage {
16 + MwebLogsPage(this.mwebSettingsViewModelBase);
17 +
18 + final MwebSettingsViewModelBase mwebSettingsViewModelBase;
19 +
20 + @override
21 + String get title => S.current.litecoin_mweb_logs;
22 +
23 + @override
24 + Widget body(BuildContext context) {
25 + return Stack(
26 + fit: StackFit.expand,
27 + children: [
28 + FutureBuilder<String>(
29 + future: mwebSettingsViewModelBase.getAbbreviatedLogs(),
30 + builder: (context, snapshot) {
31 + if (snapshot.connectionState == ConnectionState.waiting) {
32 + return Center(child: CircularProgressIndicator());
33 + } else if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
34 + return Center(child: Text('No logs found'));
35 + } else {
36 + return SingleChildScrollView(
37 + child: Padding(
38 + padding: EdgeInsets.all(16.0),
39 + child: Text(
40 + snapshot.data!,
41 + style: TextStyle(fontFamily: 'Monospace'),
42 + ),
43 + ),
44 + );
45 + }
46 + },
47 + ),
48 + Positioned(
49 + child: LoadingPrimaryButton(
50 + onPressed: () => onExportLogs(context),
51 + text: S.of(context).export_logs,
52 + color: Theme.of(context).primaryColor,
53 + textColor: Colors.white,
54 + ),
55 + bottom: 24,
56 + left: 24,
57 + right: 24,
58 + )
59 + ],
60 + );
61 + }
62 +
63 + void onExportLogs(BuildContext context) {
64 + if (Platform.isAndroid) {
65 + onExportAndroid(context);
66 + } else if (Platform.isIOS) {
67 + share(context);
68 + } else {
69 + _saveFile();
70 + }
71 + }
72 +
73 + void onExportAndroid(BuildContext context) {
74 + showPopUp<void>(
75 + context: context,
76 + builder: (dialogContext) {
77 + return AlertWithTwoActions(
78 + alertTitle: S.of(context).export_backup,
79 + alertContent: S.of(context).select_destination,
80 + rightButtonText: S.of(context).save_to_downloads,
81 + leftButtonText: S.of(context).share,
82 + actionRightButton: () async {
83 + const downloadDirPath = "/storage/emulated/0/Download";
84 + final filePath = downloadDirPath + "/debug.log";
85 + await mwebSettingsViewModelBase.saveLogsLocally(filePath);
86 + Navigator.of(dialogContext).pop();
87 + },
88 + actionLeftButton: () async {
89 + Navigator.of(dialogContext).pop();
90 + try {
91 + await share(context);
92 + } catch (e, s) {
93 + ExceptionHandler.onError(FlutterErrorDetails(
94 + exception: e,
95 + stack: s,
96 + library: "Export Logs",
97 + ));
98 + }
99 + });
100 + });
101 + }
102 +
103 + Future<void> share(BuildContext context) async {
104 + final filePath = (await getAppDir()).path + "/debug.log";
105 + bool success = await mwebSettingsViewModelBase.saveLogsLocally(filePath);
106 + if (!success) return;
107 + await ShareUtil.shareFile(filePath: filePath, fileName: "debug.log", context: context);
108 + await mwebSettingsViewModelBase.removeLogsLocally(filePath);
109 + }
110 +
111 + Future<void> _saveFile() async {
112 + String? outputFile = await FilePicker.platform
113 + .saveFile(dialogTitle: 'Save Your File to desired location', fileName: "debug.log");
114 +
115 + try {
116 + final filePath = (await getApplicationSupportDirectory()).path + "/debug.log";
117 + File debugLogFile = File(filePath);
118 + await debugLogFile.copy(outputFile!);
119 + } catch (exception, stackTrace) {
120 + ExceptionHandler.onError(FlutterErrorDetails(
121 + exception: exception,
122 + stack: stackTrace,
123 + library: "Export Logs",
124 + ));
125 + }
126 + }
127 +}
lib/src/screens/settings/mweb_node_page.dart new
+56
@@ -0,0 +1,56 @@
1 +import 'package:cake_wallet/src/screens/base_page.dart';
2 +import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
3 +import 'package:cake_wallet/src/widgets/primary_button.dart';
4 +import 'package:cake_wallet/generated/i18n.dart';
5 +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart';
6 +import 'package:flutter/material.dart';
7 +import 'package:flutter_mobx/flutter_mobx.dart';
8 +
9 +class MwebNodePage extends BasePage {
10 + MwebNodePage(this.mwebSettingsViewModelBase)
11 + : _nodeUriController = TextEditingController(text: mwebSettingsViewModelBase.mwebNodeUri),
12 + super();
13 +
14 + final MwebSettingsViewModelBase mwebSettingsViewModelBase;
15 + final TextEditingController _nodeUriController;
16 +
17 + @override
18 + String get title => S.current.litecoin_mweb_node;
19 +
20 + @override
21 + Widget body(BuildContext context) {
22 + return Stack(
23 + fit: StackFit.expand,
24 + children: [
25 + Container(
26 + padding: EdgeInsets.symmetric(horizontal: 24),
27 + child: Row(
28 + children: <Widget>[
29 + Expanded(
30 + child: BaseTextFormField(controller: _nodeUriController),
31 + )
32 + ],
33 + ),
34 + ),
35 + Positioned(
36 + child: Observer(
37 + builder: (_) => LoadingPrimaryButton(
38 + onPressed: () => save(context),
39 + text: S.of(context).save,
40 + color: Theme.of(context).primaryColor,
41 + textColor: Colors.white,
42 + ),
43 + ),
44 + bottom: 24,
45 + left: 24,
46 + right: 24,
47 + )
48 + ],
49 + );
50 + }
51 +
52 + void save(BuildContext context) {
53 + mwebSettingsViewModelBase.setMwebNodeUri(_nodeUriController.text);
54 + Navigator.pop(context);
55 + }
56 +}
lib/src/screens/settings/mweb_settings.dart
+9 -1
@@ -31,7 +31,7 @@ class MwebSettingsPage extends BasePage {
31 },
32 ),
33 SettingsSwitcherCell(
34 - title: S.current.litecoin_mweb_always_scan,
34 + title: S.current.litecoin_mweb_enable,
35 value: _mwebSettingsViewModel.mwebEnabled,
36 onValueChange: (_, bool value) {
37 _mwebSettingsViewModel.setMwebEnabled(value);
@@ -41,6 +41,14 @@ class MwebSettingsPage extends BasePage {
41 title: S.current.litecoin_mweb_scanning,
42 handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan),
43 ),
44 + SettingsCellWithArrow(
45 + title: S.current.litecoin_mweb_logs,
46 + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.mwebLogs),
47 + ),
48 + SettingsCellWithArrow(
49 + title: S.current.litecoin_mweb_node,
50 + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.mwebNode),
51 + ),
52 ],
53 ),
54 );
lib/store/settings_store.dart
+20 -5
@@ -120,6 +120,7 @@ abstract class SettingsStoreBase with Store {
120 required this.mwebCardDisplay,
121 required this.mwebEnabled,
122 required this.hasEnabledMwebBefore,
123 + required this.mwebNodeUri,
124 TransactionPriority? initialBitcoinTransactionPriority,
125 TransactionPriority? initialMoneroTransactionPriority,
126 TransactionPriority? initialWowneroTransactionPriority,
@@ -358,8 +359,8 @@ abstract class SettingsStoreBase with Store {
359
360 reaction(
361 (_) => bitcoinSeedType,
361 - (BitcoinSeedType bitcoinSeedType) => sharedPreferences.setInt(
362 - PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw));
362 + (BitcoinSeedType bitcoinSeedType) =>
363 + sharedPreferences.setInt(PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw));
364
365 reaction(
366 (_) => nanoSeedType,
@@ -442,8 +443,10 @@ abstract class SettingsStoreBase with Store {
443 reaction((_) => useTronGrid,
444 (bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid));
445
445 - reaction((_) => useMempoolFeeAPI,
446 - (bool useMempoolFeeAPI) => _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI));
446 + reaction(
447 + (_) => useMempoolFeeAPI,
448 + (bool useMempoolFeeAPI) =>
449 + _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI));
450
451 reaction((_) => defaultNanoRep,
452 (String nanoRep) => _sharedPreferences.setString(PreferencesKey.defaultNanoRep, nanoRep));
@@ -591,6 +594,11 @@ abstract class SettingsStoreBase with Store {
594 (bool hasEnabledMwebBefore) =>
595 _sharedPreferences.setBool(PreferencesKey.hasEnabledMwebBefore, hasEnabledMwebBefore));
596
597 + reaction(
598 + (_) => mwebNodeUri,
599 + (String mwebNodeUri) =>
600 + _sharedPreferences.setString(PreferencesKey.mwebNodeUri, mwebNodeUri));
601 +
602 this.nodes.observe((change) {
603 if (change.newValue != null && change.key != null) {
604 _saveCurrentNode(change.newValue!, change.key!);
@@ -822,6 +830,9 @@ abstract class SettingsStoreBase with Store {
830 @observable
831 bool hasEnabledMwebBefore;
832
833 + @observable
834 + String mwebNodeUri;
835 +
836 final SecureStorage _secureStorage;
837 final SharedPreferences _sharedPreferences;
838 final BackgroundTasks _backgroundTasks;
@@ -988,6 +999,8 @@ abstract class SettingsStoreBase with Store {
999 final mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false;
1000 final hasEnabledMwebBefore =
1001 sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false;
1002 + final mwebNodeUri = sharedPreferences.getString(PreferencesKey.mwebNodeUri) ??
1003 + "ltc-electrum.cakewallet.com:9333";
1004
1005 // If no value
1006 if (pinLength == null || pinLength == 0) {
@@ -1259,6 +1272,7 @@ abstract class SettingsStoreBase with Store {
1272 mwebAlwaysScan: mwebAlwaysScan,
1273 mwebCardDisplay: mwebCardDisplay,
1274 mwebEnabled: mwebEnabled,
1275 + mwebNodeUri: mwebNodeUri,
1276 hasEnabledMwebBefore: hasEnabledMwebBefore,
1277 initialMoneroTransactionPriority: moneroTransactionPriority,
1278 initialWowneroTransactionPriority: wowneroTransactionPriority,
@@ -1686,7 +1700,8 @@ abstract class SettingsStoreBase with Store {
1700 deviceName = windowsInfo.productName;
1701 } catch (e) {
1702 print(e);
1689 - print('likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged');
1703 + print(
1704 + 'likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged');
1705 deviceName = "Windows Device";
1706 }
1707 }
lib/view_model/dashboard/balance_view_model.dart
+1 -1
@@ -381,7 +381,7 @@ abstract class BalanceViewModelBase with Store {
381
382 bool _hasSecondAdditionalBalanceForWalletType(WalletType type) {
383 if (wallet.type == WalletType.litecoin) {
384 - if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) > 0) {
384 + if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0) {
385 return true;
386 }
387 }
lib/view_model/dashboard/transaction_list_item.dart
+40 -12
@@ -56,25 +56,53 @@ class TransactionListItem extends ActionListItem with Keyable {
56 }
57
58 String get formattedPendingStatus {
59 - if (balanceViewModel.wallet.type == WalletType.monero ||
60 - balanceViewModel.wallet.type == WalletType.haven) {
61 - if (transaction.confirmations >= 0 && transaction.confirmations < 10) {
62 - return ' (${transaction.confirmations}/10)';
63 - }
64 - } else if (balanceViewModel.wallet.type == WalletType.wownero) {
65 - if (transaction.confirmations >= 0 && transaction.confirmations < 3) {
66 - return ' (${transaction.confirmations}/3)';
67 - }
59 + switch (balanceViewModel.wallet.type) {
60 + case WalletType.monero:
61 + case WalletType.haven:
62 + if (transaction.confirmations >= 0 && transaction.confirmations < 10) {
63 + return ' (${transaction.confirmations}/10)';
64 + }
65 + break;
66 + case WalletType.wownero:
67 + if (transaction.confirmations >= 0 && transaction.confirmations < 3) {
68 + return ' (${transaction.confirmations}/3)';
69 + }
70 + break;
71 + case WalletType.litecoin:
72 + bool isPegIn = (transaction.additionalInfo["isPegIn"] as bool?) ?? false;
73 + bool isPegOut = (transaction.additionalInfo["isPegOut"] as bool?) ?? false;
74 + bool fromPegOut = (transaction.additionalInfo["fromPegOut"] as bool?) ?? false;
75 + String str = '';
76 + if (transaction.confirmations <= 0) {
77 + str = S.current.pending;
78 + }
79 + if ((isPegOut || fromPegOut) && transaction.confirmations >= 0 && transaction.confirmations < 6) {
80 + str = " (${transaction.confirmations}/6)";
81 + }
82 + if (isPegIn) {
83 + str += " (Peg In)";
84 + }
85 + if (isPegOut) {
86 + str += " (Peg Out)";
87 + }
88 + return str;
89 + default:
90 + return '';
91 }
92 +
93 return '';
94 }
95
96 String get formattedStatus {
73 - if (balanceViewModel.wallet.type == WalletType.monero ||
74 - balanceViewModel.wallet.type == WalletType.wownero ||
75 - balanceViewModel.wallet.type == WalletType.haven) {
97 + if ([
98 + WalletType.monero,
99 + WalletType.haven,
100 + WalletType.wownero,
101 + WalletType.litecoin,
102 + ].contains(balanceViewModel.wallet.type)) {
103 return formattedPendingStatus;
104 }
105 +
106 return transaction.isPending ? S.current.pending : '';
107 }
108
lib/view_model/settings/mweb_settings_view_model.dart
+50
@@ -1,7 +1,12 @@
1 +import 'dart:io';
2 +
3 import 'package:cake_wallet/bitcoin/bitcoin.dart';
4 import 'package:cake_wallet/store/settings_store.dart';
5 +import 'package:cake_wallet/utils/exception_handler.dart';
6 import 'package:cw_core/wallet_base.dart';
7 +import 'package:flutter/widgets.dart';
8 import 'package:mobx/mobx.dart';
9 +import 'package:path_provider/path_provider.dart';
10
11 part 'mweb_settings_view_model.g.dart';
12
@@ -22,15 +27,60 @@ abstract class MwebSettingsViewModelBase with Store {
27 @observable
28 late bool mwebEnabled;
29
30 + @computed
31 + String get mwebNodeUri => _settingsStore.mwebNodeUri;
32 +
33 @action
34 void setMwebCardDisplay(bool value) {
35 _settingsStore.mwebCardDisplay = value;
36 }
37
38 + @action
39 + void setMwebNodeUri(String value) {
40 + _settingsStore.mwebNodeUri = value;
41 + }
42 +
43 @action
44 void setMwebEnabled(bool value) {
45 mwebEnabled = value;
46 bitcoin!.setMwebEnabled(_wallet, value);
47 _settingsStore.mwebAlwaysScan = value;
48 }
49 +
50 + Future<bool> saveLogsLocally(String filePath) async {
51 + try {
52 + final appSupportPath = (await getApplicationSupportDirectory()).path;
53 + final logsFile = File("$appSupportPath/logs/debug.log");
54 + if (!logsFile.existsSync()) {
55 + throw Exception('Logs file does not exist');
56 + }
57 + await logsFile.copy(filePath);
58 + return true;
59 + } catch (e, s) {
60 + ExceptionHandler.onError(FlutterErrorDetails(
61 + exception: e,
62 + stack: s,
63 + library: "Export Logs",
64 + ));
65 + return false;
66 + }
67 + }
68 +
69 + Future<String> getAbbreviatedLogs() async {
70 + final appSupportPath = (await getApplicationSupportDirectory()).path;
71 + final logsFile = File("$appSupportPath/logs/debug.log");
72 + if (!logsFile.existsSync()) {
73 + return "";
74 + }
75 + final logs = logsFile.readAsStringSync();
76 + // return last 10000 characters:
77 + return logs.substring(logs.length > 10000 ? logs.length - 10000 : 0);
78 + }
79 +
80 + Future<void> removeLogsLocally(String filePath) async {
81 + final logsFile = File(filePath);
82 + if (logsFile.existsSync()) {
83 + await logsFile.delete();
84 + }
85 + }
86 }
pubspec_base.yaml
+1 -1
@@ -134,7 +134,7 @@ dependency_overrides:
134 bitcoin_base:
135 git:
136 url: https://github.com/cake-tech/bitcoin_base
137 - ref: cake-update-v8
137 + ref: cake-update-v9
138 ffi: 2.1.0
139
140 flutter_icons:
res/values/strings_ar.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "ﻲﻓ ﻪﺘﻴﺣﻼﺻ ﻲﻬﺘﻨﺗ",
296 "expiry_and_validity": "انتهاء الصلاحية والصلاحية",
297 "export_backup": "تصدير نسخة احتياطية",
298 + "export_logs": "سجلات التصدير",
299 "extra_id": "معرف إضافي:",
300 "extracted_address_content": "سوف ترسل الأموال إلى\n${recipient_name}",
301 "failed_authentication": "${state_error} فشل المصادقة.",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB هو بروتوكول جديد يجلب معاملات أسرع وأرخص وأكثر خصوصية إلى Litecoin",
373 "litecoin_mweb_dismiss": "رفض",
374 "litecoin_mweb_display_card": "عرض بطاقة mweb",
375 + "litecoin_mweb_enable": "تمكين MWEB",
376 "litecoin_mweb_enable_later": "يمكنك اختيار تمكين MWEB مرة أخرى ضمن إعدادات العرض.",
377 + "litecoin_mweb_logs": "سجلات MWEB",
378 + "litecoin_mweb_node": "عقدة MWEB",
379 "litecoin_mweb_pegin": "ربط في",
380 "litecoin_mweb_pegout": "ربط",
381 "litecoin_mweb_scanning": "MWEB المسح الضوئي",
res/values/strings_bg.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Изтича на",
296 "expiry_and_validity": "Изтичане и валидност",
297 "export_backup": "Експортиране на резервно копие",
298 + "export_logs": "Експортни дневници",
299 "extra_id": "Допълнително ID:",
300 "extracted_address_content": "Ще изпратите средства на \n${recipient_name}",
301 "failed_authentication": "Неуспешно удостоверяване. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWeb е нов протокол, който носи по -бърз, по -евтин и повече частни транзакции на Litecoin",
373 "litecoin_mweb_dismiss": "Уволнение",
374 "litecoin_mweb_display_card": "Показване на MWEB карта",
375 + "litecoin_mweb_enable": "Активирайте MWeb",
376 "litecoin_mweb_enable_later": "Можете да изберете да активирате MWEB отново под настройките на дисплея.",
377 + "litecoin_mweb_logs": "MWeb logs",
378 + "litecoin_mweb_node": "MWEB възел",
379 "litecoin_mweb_pegin": "PEG в",
380 "litecoin_mweb_pegout": "PEG OUT",
381 "litecoin_mweb_scanning": "Сканиране на MWEB",
res/values/strings_cs.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Vyprší dne",
296 "expiry_and_validity": "Vypršení a platnost",
297 "export_backup": "Exportovat zálohu",
298 + "export_logs": "Vývozní protokoly",
299 "extra_id": "Extra ID:",
300 "extracted_address_content": "Prostředky budete posílat na\n${recipient_name}",
301 "failed_authentication": "Ověřování selhalo. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB je nový protokol, který do Litecoin přináší rychlejší, levnější a více soukromých transakcí",
373 "litecoin_mweb_dismiss": "Propustit",
374 "litecoin_mweb_display_card": "Zobrazit kartu MWeb",
375 + "litecoin_mweb_enable": "Povolit mWeb",
376 "litecoin_mweb_enable_later": "V nastavení zobrazení můžete vybrat znovu povolit MWeb.",
377 + "litecoin_mweb_logs": "Protokoly mWeb",
378 + "litecoin_mweb_node": "Uzel mWeb",
379 "litecoin_mweb_pegin": "Peg in",
380 "litecoin_mweb_pegout": "Zkrachovat",
381 "litecoin_mweb_scanning": "Skenování mWeb",
res/values/strings_de.arb
+5 -1
@@ -295,6 +295,7 @@
295 "expiresOn": "Läuft aus am",
296 "expiry_and_validity": "Ablauf und Gültigkeit",
297 "export_backup": "Sicherung exportieren",
298 + "export_logs": "Exportprotokolle",
299 "extra_id": "Extra ID:",
300 "extracted_address_content": "Sie senden Geld an\n${recipient_name}",
301 "failed_authentication": "Authentifizierung fehlgeschlagen. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWWB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt",
373 "litecoin_mweb_dismiss": "Zurückweisen",
374 "litecoin_mweb_display_card": "MWEB-Karte anzeigen",
375 + "litecoin_mweb_enable": "Aktivieren Sie MWeb",
376 "litecoin_mweb_enable_later": "Sie können MWEB unter Anzeigeeinstellungen erneut aktivieren.",
377 + "litecoin_mweb_logs": "MWEB -Protokolle",
378 + "litecoin_mweb_node": "MWEB -Knoten",
379 "litecoin_mweb_pegin": "Peg in",
380 "litecoin_mweb_pegout": "Abstecken",
381 "litecoin_mweb_scanning": "MWEB Scanning",
@@ -941,4 +945,4 @@
945 "you_will_get": "Konvertieren zu",
946 "you_will_send": "Konvertieren von",
947 "yy": "YY"
944 -}
948 +}
\ No newline at end of file
res/values/strings_en.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Expires on",
296 "expiry_and_validity": "Expiry and Validity",
297 "export_backup": "Export backup",
298 + "export_logs": "Export logs",
299 "extra_id": "Extra ID:",
300 "extracted_address_content": "You will be sending funds to\n${recipient_name}",
301 "failed_authentication": "Failed authentication. ${state_error}",
@@ -373,7 +374,10 @@
374 "litecoin_mweb_description": "MWEB is a new protocol that brings faster, cheaper, and more private transactions to Litecoin",
375 "litecoin_mweb_dismiss": "Dismiss",
376 "litecoin_mweb_display_card": "Show MWEB card",
377 + "litecoin_mweb_enable": "Enable MWEB",
378 "litecoin_mweb_enable_later": "You can choose to enable MWEB again under Display Settings.",
379 + "litecoin_mweb_logs": "MWEB Logs",
380 + "litecoin_mweb_node": "MWEB Node",
381 "litecoin_mweb_pegin": "Peg In",
382 "litecoin_mweb_pegout": "Peg Out",
383 "litecoin_mweb_scanning": "MWEB Scanning",
res/values/strings_es.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Expira el",
296 "expiry_and_validity": "Vencimiento y validez",
297 "export_backup": "Exportar copia de seguridad",
298 + "export_logs": "Registros de exportación",
299 "extra_id": "ID adicional:",
300 "extracted_address_content": "Enviará fondos a\n${recipient_name}",
301 "failed_authentication": "Autenticación fallida. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "Mweb es un nuevo protocolo que trae transacciones más rápidas, más baratas y más privadas a Litecoin",
373 "litecoin_mweb_dismiss": "Despedir",
374 "litecoin_mweb_display_card": "Mostrar tarjeta MWEB",
375 + "litecoin_mweb_enable": "Habilitar mweb",
376 "litecoin_mweb_enable_later": "Puede elegir habilitar MWEB nuevamente en la configuración de visualización.",
377 + "litecoin_mweb_logs": "Registros de mweb",
378 + "litecoin_mweb_node": "Nodo mweb",
379 "litecoin_mweb_pegin": "Convertir",
380 "litecoin_mweb_pegout": "Recuperar",
381 "litecoin_mweb_scanning": "Escaneo mweb",
res/values/strings_fr.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Expire le",
296 "expiry_and_validity": "Expiration et validité",
297 "export_backup": "Exporter la sauvegarde",
298 + "export_logs": "Journaux d'exportation",
299 "extra_id": "ID supplémentaire :",
300 "extracted_address_content": "Vous allez envoyer des fonds à\n${recipient_name}",
301 "failed_authentication": "Échec d'authentification. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB est un nouveau protocole qui apporte des transactions plus rapides, moins chères et plus privées à Litecoin",
373 "litecoin_mweb_dismiss": "Rejeter",
374 "litecoin_mweb_display_card": "Afficher la carte MWeb",
375 + "litecoin_mweb_enable": "Activer Mweb",
376 "litecoin_mweb_enable_later": "Vous pouvez choisir d'activer à nouveau MWEB sous Paramètres d'affichage.",
377 + "litecoin_mweb_logs": "Journaux MWEB",
378 + "litecoin_mweb_node": "Node MWEB",
379 "litecoin_mweb_pegin": "Entraver",
380 "litecoin_mweb_pegout": "Crever",
381 "litecoin_mweb_scanning": "Scann mweb",
res/values/strings_ha.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Yana ƙarewa",
296 "expiry_and_validity": "Karewa da inganci",
297 "export_backup": "Ajiyayyen fitarwa",
298 + "export_logs": "Injin fitarwa",
299 "extra_id": "Karin ID:",
300 "extracted_address_content": "Za ku aika da kudade zuwa\n${recipient_name}",
301 "failed_authentication": "Binne wajen shiga. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "Mweb shine sabon tsarin yarjejeniya da ya kawo da sauri, mai rahusa, da kuma ma'amaloli masu zaman kansu zuwa Litecoin",
373 "litecoin_mweb_dismiss": "Tuɓe \\ sallama",
374 "litecoin_mweb_display_card": "Nuna katin Mweb",
375 + "litecoin_mweb_enable": "Kunna Mweb",
376 "litecoin_mweb_enable_later": "Kuna iya zaɓar kunna Mweb kuma a ƙarƙashin saitunan nuni.",
377 + "litecoin_mweb_logs": "Jagoran Mweb",
378 + "litecoin_mweb_node": "Mweb Node",
379 "litecoin_mweb_pegin": "Peg in",
380 "litecoin_mweb_pegout": "Peg fita",
381 "litecoin_mweb_scanning": "Mweb scanning",
res/values/strings_hi.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "पर समय सीमा समाप्त",
296 "expiry_and_validity": "समाप्ति और वैधता",
297 "export_backup": "निर्यात बैकअप",
298 + "export_logs": "निर्यात लॉग",
299 "extra_id": "अतिरिक्त आईडी:",
300 "extracted_address_content": "आपको धनराशि भेजी जाएगी\n${recipient_name}",
301 "failed_authentication": "प्रमाणीकरण विफल. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB एक नया प्रोटोकॉल है जो लिटकोइन के लिए तेजी से, सस्ता और अधिक निजी लेनदेन लाता है",
373 "litecoin_mweb_dismiss": "नकार देना",
374 "litecoin_mweb_display_card": "MWEB कार्ड दिखाएं",
375 + "litecoin_mweb_enable": "MWEB सक्षम करें",
376 "litecoin_mweb_enable_later": "आप प्रदर्शन सेटिंग्स के तहत फिर से MWEB को सक्षम करने के लिए चुन सकते हैं।",
377 + "litecoin_mweb_logs": "MWEB लॉग",
378 + "litecoin_mweb_node": "MWEB नोड",
379 "litecoin_mweb_pegin": "खूंटी",
380 "litecoin_mweb_pegout": "मरना",
381 "litecoin_mweb_scanning": "MWEB स्कैनिंग",
res/values/strings_hr.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Istječe",
296 "expiry_and_validity": "Istek i valjanost",
297 "export_backup": "Izvezi sigurnosnu kopiju",
298 + "export_logs": "Izvozni trupci",
299 "extra_id": "Dodatni ID:",
300 "extracted_address_content": "Poslat ćete sredstva primatelju\n${recipient_name}",
301 "failed_authentication": "Autentifikacija neuspješna. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB je novi protokol koji u Litecoin donosi brže, jeftinije i privatnije transakcije",
373 "litecoin_mweb_dismiss": "Odbaciti",
374 "litecoin_mweb_display_card": "Prikaži MWeb karticu",
375 + "litecoin_mweb_enable": "Omogući MWeb",
376 "litecoin_mweb_enable_later": "Možete odabrati da MWEB ponovo omogućite pod postavkama zaslona.",
377 + "litecoin_mweb_logs": "MWEB trupci",
378 + "litecoin_mweb_node": "MWEB čvor",
379 "litecoin_mweb_pegin": "Uvući se",
380 "litecoin_mweb_pegout": "Odapeti",
381 "litecoin_mweb_scanning": "MWEB skeniranje",
res/values/strings_hy.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Վավերականությունը լրանում է",
296 "expiry_and_validity": "Վավերականություն և լրացում",
297 "export_backup": "Արտահանել կրկնօրինակը",
298 + "export_logs": "Արտահանման տեղեկամատյաններ",
299 "extra_id": "Լրացուցիչ ID",
300 "extracted_address_content": "Դուք կուղարկեք գումար ${recipient_name}",
301 "failed_authentication": "Վավերացումը ձախողվեց. ${state_error}",
@@ -367,7 +368,10 @@
368 "light_theme": "Լուսավոր",
369 "litecoin_mweb_description": "Mweb- ը նոր արձանագրություն է, որը բերում է ավելի արագ, ավելի էժան եւ ավելի մասնավոր գործարքներ դեպի LITECOIN",
370 "litecoin_mweb_dismiss": "Հեռացնել",
371 + "litecoin_mweb_enable": "Միացնել Mweb- ը",
372 "litecoin_mweb_enable_later": "Կարող եք ընտրել Mweb- ը կրկին միացնել ցուցադրման պարամետրերը:",
373 + "litecoin_mweb_logs": "Mweb տեղեկամատյաններ",
374 + "litecoin_mweb_node": "Mweb հանգույց",
375 "litecoin_mweb_pegin": "Peg in",
376 "litecoin_mweb_pegout": "Հափշտակել",
377 "live_fee_rates": "Ապակի վարձավճարներ API- ի միջոցով",
res/values/strings_id.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Kadaluarsa pada",
296 "expiry_and_validity": "Kedaluwarsa dan validitas",
297 "export_backup": "Ekspor cadangan",
298 + "export_logs": "Log ekspor",
299 "extra_id": "ID tambahan:",
300 "extracted_address_content": "Anda akan mengirim dana ke\n${recipient_name}",
301 "failed_authentication": "Otentikasi gagal. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB adalah protokol baru yang membawa transaksi yang lebih cepat, lebih murah, dan lebih pribadi ke Litecoin",
373 "litecoin_mweb_dismiss": "Membubarkan",
374 "litecoin_mweb_display_card": "Tunjukkan kartu mWeb",
375 + "litecoin_mweb_enable": "Aktifkan MWEB",
376 "litecoin_mweb_enable_later": "Anda dapat memilih untuk mengaktifkan MWEB lagi di bawah pengaturan tampilan.",
377 + "litecoin_mweb_logs": "Log MWeb",
378 + "litecoin_mweb_node": "Node MWEB",
379 "litecoin_mweb_pegin": "Pasak masuk",
380 "litecoin_mweb_pegout": "Mati",
381 "litecoin_mweb_scanning": "Pemindaian MWEB",
res/values/strings_it.arb
+4
@@ -296,6 +296,7 @@
296 "expiresOn": "Scade il",
297 "expiry_and_validity": "Scadenza e validità",
298 "export_backup": "Esporta backup",
299 + "export_logs": "Registri di esportazione",
300 "extra_id": "Extra ID:",
301 "extracted_address_content": "Invierai i tuoi fondi a\n${recipient_name}",
302 "failed_authentication": "Autenticazione fallita. ${state_error}",
@@ -372,7 +373,10 @@
373 "litecoin_mweb_description": "MWeb è un nuovo protocollo che porta transazioni più veloci, più economiche e più private a Litecoin",
374 "litecoin_mweb_dismiss": "Congedare",
375 "litecoin_mweb_display_card": "Mostra la scheda MWeb",
376 + "litecoin_mweb_enable": "Abilita mWeb",
377 "litecoin_mweb_enable_later": "È possibile scegliere di abilitare nuovamente MWeb nelle impostazioni di visualizzazione.",
378 + "litecoin_mweb_logs": "Registri mWeb",
379 + "litecoin_mweb_node": "Nodo MWeb",
380 "litecoin_mweb_pegin": "Piolo in",
381 "litecoin_mweb_pegout": "PEG OUT",
382 "litecoin_mweb_scanning": "Scansione MWeb",
res/values/strings_ja.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "有効期限は次のとおりです",
296 "expiry_and_validity": "有効期限と有効性",
297 "export_backup": "バックアップのエクスポート",
298 + "export_logs": "ログをエクスポートします",
299 "extra_id": "追加ID:",
300 "extracted_address_content": "に送金します\n${recipient_name}",
301 "failed_authentication": "認証失敗. ${state_error}",
@@ -372,7 +373,10 @@
373 "litecoin_mweb_description": "MWEBは、Litecoinにより速く、より安価で、よりプライベートなトランザクションをもたらす新しいプロトコルです",
374 "litecoin_mweb_dismiss": "却下する",
375 "litecoin_mweb_display_card": "MWEBカードを表示します",
376 + "litecoin_mweb_enable": "MWEBを有効にします",
377 "litecoin_mweb_enable_later": "表示設定の下で、MWEBを再度有効にすることを選択できます。",
378 + "litecoin_mweb_logs": "MWEBログ",
379 + "litecoin_mweb_node": "MWEBノード",
380 "litecoin_mweb_pegin": "ペグイン",
381 "litecoin_mweb_pegout": "ペグアウト",
382 "litecoin_mweb_scanning": "MWEBスキャン",
res/values/strings_ko.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "만료 날짜",
296 "expiry_and_validity": "만료와 타당성",
297 "export_backup": "백업 내보내기",
298 + "export_logs": "내보내기 로그",
299 "extra_id": "추가 ID:",
300 "extracted_address_content": "당신은에 자금을 보낼 것입니다\n${recipient_name}",
301 "failed_authentication": "인증 실패. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB는 Litecoin에 더 빠르고 저렴하며 개인 거래를 제공하는 새로운 프로토콜입니다.",
373 "litecoin_mweb_dismiss": "해고하다",
374 "litecoin_mweb_display_card": "mweb 카드를 보여주십시오",
375 + "litecoin_mweb_enable": "mweb 활성화",
376 "litecoin_mweb_enable_later": "디스플레이 설정에서 MWEB를 다시 활성화하도록 선택할 수 있습니다.",
377 + "litecoin_mweb_logs": "mweb 로그",
378 + "litecoin_mweb_node": "mweb 노드",
379 "litecoin_mweb_pegin": "페그를 입력하십시오",
380 "litecoin_mweb_pegout": "죽다",
381 "litecoin_mweb_scanning": "mweb 스캔",
res/values/strings_my.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "သက်တမ်းကုန်သည်။",
296 "expiry_and_validity": "သက်တမ်းကုန်ဆုံးခြင်းနှင့်တရားဝင်မှု",
297 "export_backup": "အရန်ကူးထုတ်ရန်",
298 + "export_logs": "ပို့ကုန်မှတ်တမ်းများ",
299 "extra_id": "အပို ID-",
300 "extracted_address_content": "သင်သည် \n${recipient_name} သို့ ရန်ပုံငွေများ ပေးပို့ပါမည်",
301 "failed_authentication": "အထောက်အထားစိစစ်ခြင်း မအောင်မြင်ပါ။. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "Mweb သည် Protocol အသစ်ဖြစ်ပြီး LitCoin သို့ပိုမိုဈေးချိုသာသော, စျေးသက်သက်သာသာသုံးခြင်းနှင့်ပိုမိုများပြားသောပုဂ္ဂလိကငွေပို့ဆောင်မှုများကိုဖြစ်ပေါ်စေသည်",
373 "litecoin_mweb_dismiss": "ထုတ်ပစ်",
374 "litecoin_mweb_display_card": "MweB ကဒ်ကိုပြပါ",
375 + "litecoin_mweb_enable": "mweb enable",
376 "litecoin_mweb_enable_later": "သင် MweB ကို display settings အောက်ရှိ ထပ်မံ. ခွင့်ပြုရန်ရွေးချယ်နိုင်သည်။",
377 + "litecoin_mweb_logs": "Mweb မှတ်တမ်းများ",
378 + "litecoin_mweb_node": "mweb node ကို",
379 "litecoin_mweb_pegin": "တံစို့",
380 "litecoin_mweb_pegout": "တံစို့",
381 "litecoin_mweb_scanning": "mweb scanning",
res/values/strings_nl.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Verloopt op",
296 "expiry_and_validity": "Vervallen en geldigheid",
297 "export_backup": "Back-up exporteren",
298 + "export_logs": "Exporteer logboeken",
299 "extra_id": "Extra ID:",
300 "extracted_address_content": "U stuurt geld naar\n${recipient_name}",
301 "failed_authentication": "Mislukte authenticatie. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB is een nieuw protocol dat snellere, goedkopere en meer privé -transacties naar Litecoin brengt",
373 "litecoin_mweb_dismiss": "Afwijzen",
374 "litecoin_mweb_display_card": "Toon MWEB -kaart",
375 + "litecoin_mweb_enable": "MWEB inschakelen",
376 "litecoin_mweb_enable_later": "U kunt ervoor kiezen om MWeb opnieuw in te schakelen onder weergave -instellingen.",
377 + "litecoin_mweb_logs": "MWEB -logboeken",
378 + "litecoin_mweb_node": "MWEB -knooppunt",
379 "litecoin_mweb_pegin": "Vastmaken",
380 "litecoin_mweb_pegout": "Uithakken",
381 "litecoin_mweb_scanning": "MWEB -scanning",
res/values/strings_pl.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Upływa w dniu",
296 "expiry_and_validity": "Wygaśnięcie i ważność",
297 "export_backup": "Eksportuj kopię zapasową",
298 + "export_logs": "Dzienniki eksportu",
299 "extra_id": "Dodatkowy ID:",
300 "extracted_address_content": "Wysyłasz środki na\n${recipient_name}",
301 "failed_authentication": "Nieudane uwierzytelnienie. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB to nowy protokół, który przynosi szybciej, tańsze i bardziej prywatne transakcje do Litecoin",
373 "litecoin_mweb_dismiss": "Odrzucać",
374 "litecoin_mweb_display_card": "Pokaż kartę MWEB",
375 + "litecoin_mweb_enable": "Włącz MWEB",
376 "litecoin_mweb_enable_later": "Możesz ponownie włączyć MWEB w ustawieniach wyświetlania.",
377 + "litecoin_mweb_logs": "Dzienniki MWEB",
378 + "litecoin_mweb_node": "Węzeł MWEB",
379 "litecoin_mweb_pegin": "Kołek",
380 "litecoin_mweb_pegout": "Palikować",
381 "litecoin_mweb_scanning": "Skanowanie MWEB",
res/values/strings_pt.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Expira em",
296 "expiry_and_validity": "Expiração e validade",
297 "export_backup": "Backup de exportação",
298 + "export_logs": "Exportar logs",
299 "extra_id": "ID extra:",
300 "extracted_address_content": "Você enviará fundos para\n${recipient_name}",
301 "failed_authentication": "Falha na autenticação. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB é um novo protocolo que traz transações mais rápidas, baratas e mais privadas para o Litecoin",
373 "litecoin_mweb_dismiss": "Liberar",
374 "litecoin_mweb_display_card": "Mostre o cartão MWEB",
375 + "litecoin_mweb_enable": "Ativar Mweb",
376 "litecoin_mweb_enable_later": "Você pode optar por ativar o MWEB novamente em Configurações de exibição.",
377 + "litecoin_mweb_logs": "Logs MWeb",
378 + "litecoin_mweb_node": "Nó MWeb",
379 "litecoin_mweb_pegin": "Peg in",
380 "litecoin_mweb_pegout": "Peg fora",
381 "litecoin_mweb_scanning": "MWEB Scanning",
res/values/strings_ru.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Годен до",
296 "expiry_and_validity": "Истечение и достоверность",
297 "export_backup": "Экспорт резервной копии",
298 + "export_logs": "Экспортные журналы",
299 "extra_id": "Дополнительный ID:",
300 "extracted_address_content": "Вы будете отправлять средства\n${recipient_name}",
301 "failed_authentication": "Ошибка аутентификации. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB - это новый протокол, который приносит быстрее, дешевле и более частные транзакции в Litecoin",
373 "litecoin_mweb_dismiss": "Увольнять",
374 "litecoin_mweb_display_card": "Показать карту MWEB",
375 + "litecoin_mweb_enable": "Включить MWEB",
376 "litecoin_mweb_enable_later": "Вы можете снова включить MWEB в настройках отображения.",
377 + "litecoin_mweb_logs": "MWEB журналы",
378 + "litecoin_mweb_node": "Узел MWEB",
379 "litecoin_mweb_pegin": "Внедрять",
380 "litecoin_mweb_pegout": "Выкрикивать",
381 "litecoin_mweb_scanning": "MWEB сканирование",
res/values/strings_th.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "หมดอายุวันที่",
296 "expiry_and_validity": "หมดอายุและถูกต้อง",
297 "export_backup": "ส่งออกข้อมูลสำรอง",
298 + "export_logs": "บันทึกการส่งออก",
299 "extra_id": "ไอดีเพิ่มเติม:",
300 "extracted_address_content": "คุณกำลังจะส่งเงินไปยัง\n${recipient_name}",
301 "failed_authentication": "การยืนยันสิทธิ์ล้มเหลว ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB เป็นโปรโตคอลใหม่ที่นำการทำธุรกรรมที่เร็วกว่าราคาถูกกว่าและเป็นส่วนตัวมากขึ้นไปยัง Litecoin",
373 "litecoin_mweb_dismiss": "อนุญาตให้ออกไป",
374 "litecoin_mweb_display_card": "แสดงการ์ด mweb",
375 + "litecoin_mweb_enable": "เปิดใช้งาน mweb",
376 "litecoin_mweb_enable_later": "คุณสามารถเลือกเปิดใช้งาน MWEB อีกครั้งภายใต้การตั้งค่าการแสดงผล",
377 + "litecoin_mweb_logs": "บันทึก MWEB",
378 + "litecoin_mweb_node": "โหนด MWEB",
379 "litecoin_mweb_pegin": "หมุด",
380 "litecoin_mweb_pegout": "ตรึง",
381 "litecoin_mweb_scanning": "การสแกน MWEB",
res/values/strings_tl.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Mag-e-expire sa",
296 "expiry_and_validity": "Pag-expire at Bisa",
297 "export_backup": "I-export ang backup",
298 + "export_logs": "Mga log ng pag -export",
299 "extra_id": "Dagdag na ID:",
300 "extracted_address_content": "Magpapadala ka ng pondo sa\n${recipient_name}",
301 "failed_authentication": "Nabigo ang pagpapatunay. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "Ang MWeb ay isang bagong protocol na nagdadala ng mas mabilis, mas mura, at mas maraming pribadong mga transaksyon sa Litecoin",
373 "litecoin_mweb_dismiss": "Tanggalin",
374 "litecoin_mweb_display_card": "Ipakita ang MWEB Card",
375 + "litecoin_mweb_enable": "Paganahin ang MWeb",
376 "litecoin_mweb_enable_later": "Maaari kang pumili upang paganahin muli ang MWeb sa ilalim ng mga setting ng pagpapakita.",
377 + "litecoin_mweb_logs": "MWEB log",
378 + "litecoin_mweb_node": "Mweb node",
379 "litecoin_mweb_pegin": "Peg in",
380 "litecoin_mweb_pegout": "Peg out",
381 "litecoin_mweb_scanning": "Pag -scan ng Mweb",
res/values/strings_tr.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Tarihinde sona eriyor",
296 "expiry_and_validity": "Sona erme ve geçerlilik",
297 "export_backup": "Yedeği dışa aktar",
298 + "export_logs": "Dışa aktarma günlükleri",
299 "extra_id": "Ekstra ID:",
300 "extracted_address_content": "Parayı buraya gönderceksin:\n${recipient_name}",
301 "failed_authentication": "Doğrulama başarısız oldu. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB, Litecoin'e daha hızlı, daha ucuz ve daha fazla özel işlem getiren yeni bir protokoldür",
373 "litecoin_mweb_dismiss": "Azletmek",
374 "litecoin_mweb_display_card": "MWEB kartını göster",
375 + "litecoin_mweb_enable": "MWEB'i etkinleştir",
376 "litecoin_mweb_enable_later": "Ekran ayarlarının altında MWEB'yi tekrar etkinleştirmeyi seçebilirsiniz.",
377 + "litecoin_mweb_logs": "MWEB günlükleri",
378 + "litecoin_mweb_node": "MWEB düğümü",
379 "litecoin_mweb_pegin": "Takılmak",
380 "litecoin_mweb_pegout": "Çiğnemek",
381 "litecoin_mweb_scanning": "MWEB taraması",
res/values/strings_uk.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "Термін дії закінчується",
296 "expiry_and_validity": "Закінчення та обгрунтованість",
297 "export_backup": "Експортувати резервну копію",
298 + "export_logs": "Експортні журнали",
299 "extra_id": "Додатковий ID:",
300 "extracted_address_content": "Ви будете відправляти кошти\n${recipient_name}",
301 "failed_authentication": "Помилка аутентифікації. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB - це новий протокол, який приносить швидкі, дешевші та більш приватні транзакції Litecoin",
373 "litecoin_mweb_dismiss": "Звільнити",
374 "litecoin_mweb_display_card": "Показати карту MWeb",
375 + "litecoin_mweb_enable": "Увімкнути mweb",
376 "litecoin_mweb_enable_later": "Ви можете знову ввімкнути MWEB в налаштуваннях дисплея.",
377 + "litecoin_mweb_logs": "Журнали MWeb",
378 + "litecoin_mweb_node": "Вузол MWeb",
379 "litecoin_mweb_pegin": "Подякувати",
380 "litecoin_mweb_pegout": "Подякувати",
381 "litecoin_mweb_scanning": "Сканування Mweb",
res/values/strings_ur.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "ﺩﺎﻌﯿﻣ ﯽﻣﺎﺘﺘﺧﺍ",
296 "expiry_and_validity": "میعاد ختم اور صداقت",
297 "export_backup": "بیک اپ برآمد کریں۔",
298 + "export_logs": "نوشتہ جات برآمد کریں",
299 "extra_id": "اضافی ID:",
300 "extracted_address_content": "آپ فنڈز بھیج رہے ہوں گے\n${recipient_name}",
301 "failed_authentication": "ناکام تصدیق۔ ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB ایک نیا پروٹوکول ہے جو لیٹیکوئن میں تیز ، سستا اور زیادہ نجی لین دین لاتا ہے",
373 "litecoin_mweb_dismiss": "خارج",
374 "litecoin_mweb_display_card": "MWEB کارڈ دکھائیں",
375 + "litecoin_mweb_enable": "MWEB کو فعال کریں",
376 "litecoin_mweb_enable_later": "آپ ڈسپلے کی ترتیبات کے تحت MWEB کو دوبارہ فعال کرنے کا انتخاب کرسکتے ہیں۔",
377 + "litecoin_mweb_logs": "MWEB لاگز",
378 + "litecoin_mweb_node": "MWEB نوڈ",
379 "litecoin_mweb_pegin": "پیگ میں",
380 "litecoin_mweb_pegout": "پیگ آؤٹ",
381 "litecoin_mweb_scanning": "MWEB اسکیننگ",
res/values/strings_vi.arb
+4
@@ -296,6 +296,7 @@
296 "expiresOn": "Hết hạn vào",
297 "expiry_and_validity": "Hạn và hiệu lực",
298 "export_backup": "Xuất sao lưu",
299 + "export_logs": "Nhật ký xuất khẩu",
300 "extra_id": "ID bổ sung:",
301 "extracted_address_content": "Bạn sẽ gửi tiền cho\n${recipient_name}",
302 "failed_authentication": "Xác thực không thành công. ${state_error}",
@@ -368,7 +369,10 @@
369 "light_theme": "Chủ đề sáng",
370 "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",
371 "litecoin_mweb_dismiss": "Miễn nhiệm",
372 + "litecoin_mweb_enable": "Bật MWEB",
373 "litecoin_mweb_enable_later": "Bạn có thể chọn bật lại MWEB trong cài đặt hiển thị.",
374 + "litecoin_mweb_logs": "Nhật ký MWEB",
375 + "litecoin_mweb_node": "Nút MWEB",
376 "litecoin_mweb_pegin": "Chốt vào",
377 "litecoin_mweb_pegout": "Chốt ra",
378 "live_fee_rates": "Tỷ lệ phí hiện tại qua API",
res/values/strings_yo.arb
+4
@@ -296,6 +296,7 @@
296 "expiresOn": "Ipari lori",
297 "expiry_and_validity": "Ipari ati idaniloju",
298 "export_backup": "Sún ẹ̀dà nípamọ́ síta",
299 + "export_logs": "Wọle si okeere",
300 "extra_id": "Àmì ìdánimọ̀ tó fikún:",
301 "extracted_address_content": "Ẹ máa máa fi owó ránṣẹ́ sí\n${recipient_name}",
302 "failed_authentication": "Ìfẹ̀rílàdí pipòfo. ${state_error}",
@@ -372,7 +373,10 @@
373 "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",
374 "litecoin_mweb_dismiss": "Tuka",
375 "litecoin_mweb_display_card": "Fihan kaadi Mweb",
376 + "litecoin_mweb_enable": "Mu mweb",
377 "litecoin_mweb_enable_later": "O le yan lati ṣiṣẹ Mweb lẹẹkansi labẹ awọn eto ifihan.",
378 + "litecoin_mweb_logs": "MTweb logs",
379 + "litecoin_mweb_node": "Alweb joko",
380 "litecoin_mweb_pegin": "Peg in",
381 "litecoin_mweb_pegout": "Peg jade",
382 "litecoin_mweb_scanning": "Mweb scanning",
res/values/strings_zh.arb
+4
@@ -295,6 +295,7 @@
295 "expiresOn": "到期",
296 "expiry_and_validity": "到期和有效性",
297 "export_backup": "导出备份",
298 + "export_logs": "导出日志",
299 "extra_id": "额外ID:",
300 "extracted_address_content": "您将汇款至\n${recipient_name}",
301 "failed_authentication": "身份验证失败. ${state_error}",
@@ -371,7 +372,10 @@
372 "litecoin_mweb_description": "MWEB是一项新协议,它将更快,更便宜和更多的私人交易带给Litecoin",
373 "litecoin_mweb_dismiss": "解雇",
374 "litecoin_mweb_display_card": "显示MWEB卡",
375 + "litecoin_mweb_enable": "启用MWEB",
376 "litecoin_mweb_enable_later": "您可以选择在显示设置下再次启用MWEB。",
377 + "litecoin_mweb_logs": "MWEB日志",
378 + "litecoin_mweb_node": "MWEB节点",
379 "litecoin_mweb_pegin": "钉进",
380 "litecoin_mweb_pegout": "昏倒",
381 "litecoin_mweb_scanning": "MWEB扫描",