Mweb enhancements 3 (#1744)

* version 4.20.0 * update build numbers * UI updates and script fix for ios bundle identifier * disable mweb for desktop * change hardcoded ltc server ip address electrum connection enhancement * MWEB enhancements 2.0 (#1735) * additional logging and minor fixes * additional logging and minor fixes * addresses pt.1 * Allow Wallet Group Names to be the same as Wallet Names (#1730) * fix: Issues with imaging * fix: Allow group names to be the same as wallet names * fix: Bug with wallet grouping when a wallet is minimized * fix: Bug with wallet grouping when a wallet is minimized * logs of fixes and experimental changes, close wallet before opening next * save * fix icon * fixes * [skip ci] updates * [skip ci] updates * updates * minor optimizations * fix for when switching between wallets * [skip ci] updates * [skip ci] updates * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * mobx * mostly logging * stream fix pt.1 [skip ci] * updates * some fixes and enhancements * [skip ci] minor * potential partial fix for streamsink closed * fix stream sink closed errors * fix mweb logo colors * save * minor enhancements [skip ci] * save * experimental * minor * minor [skip ci] --------- Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * fix menu list removing from original list * detach sync status from mwebsyncstatus * minor * keep sync status in sync where necessary * minor * wip * appears to work? * updates * prevent mwebd from submitting non mweb transactions * fix unspent coins info not persisting for mweb coins + other minor fixes * [skip ci] minor * Polish MWEB card UI * make sure current chain tip is updated correctly [skip ci] * [skip ci] review fixes * [skip ci] detect mweb outputs more thoroughly (fix peg-in commit error) * fix change address on send ui * fix qr code scan issue * get segwit address for pegout even if mweb is selected on the receive screen [skip ci] * - Fix adding nodes twice - Fix mempool API parsing error * (potentially) fix duplicate tx history bug * [skip ci] fix bc1 address * don't show contacts prompt on pegin/out + potential unconfirmed balance fixes * [skip ci] minor cleanup * fix mweb input detection * fix showing mweb address for non-mweb transactions --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com> Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Co-authored-by: tuxpizza <tuxsudo@tux.pizza>

Matthew Fosse committed Oct 18, 2024 at 17:05 UTC 50825a62c194cdd97bf5414487d6d40b246c96a9
13 files changed +266 -140
cw_bitcoin/lib/electrum.dart
+15 -7
@@ -68,8 +68,8 @@ class ElectrumClient {
68
69 try {
70 await socket?.close();
71 - socket = null;
71 } catch (_) {}
72 + socket = null;
73
74 try {
75 if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) {
@@ -102,7 +102,8 @@ class ElectrumClient {
102 return;
103 }
104
105 - _setConnectionStatus(ConnectionStatus.connected);
105 + // use ping to determine actual connection status since we could've just not timed out yet:
106 + // _setConnectionStatus(ConnectionStatus.connected);
107
108 socket!.listen(
109 (Uint8List event) {
@@ -128,7 +129,7 @@ class ElectrumClient {
129 print("SOCKET CLOSED!!!!!");
130 unterminatedString = '';
131 try {
131 - if (host == socket?.address.host) {
132 + if (host == socket?.address.host || socket == null) {
133 _setConnectionStatus(ConnectionStatus.disconnected);
134 socket?.destroy();
135 }
@@ -178,7 +179,7 @@ class ElectrumClient {
179 unterminatedString = '';
180 }
181 } catch (e) {
181 - print(e.toString());
182 + print("parse $e");
183 }
184 }
185
@@ -191,7 +192,7 @@ class ElectrumClient {
192 try {
193 await callWithTimeout(method: 'server.ping');
194 _setConnectionStatus(ConnectionStatus.connected);
194 - } on RequestFailedTimeoutException catch (_) {
195 + } catch (_) {
196 _setConnectionStatus(ConnectionStatus.disconnected);
197 }
198 }
@@ -431,7 +432,7 @@ class ElectrumClient {
432
433 return subscription;
434 } catch (e) {
434 - print(e.toString());
435 + print("subscribe $e");
436 return null;
437 }
438 }
@@ -470,7 +471,8 @@ class ElectrumClient {
471
472 return completer.future;
473 } catch (e) {
473 - print(e.toString());
474 + print("callWithTimeout $e");
475 + rethrow;
476 }
477 }
478
@@ -537,6 +539,12 @@ class ElectrumClient {
539 onConnectionStatusChange?.call(status);
540 _connectionStatus = status;
541 _isConnected = status == ConnectionStatus.connected;
542 + if (!_isConnected) {
543 + try {
544 + socket?.destroy();
545 + } catch (_) {}
546 + socket = null;
547 + }
548 }
549
550 void _handleResponse(Map<String, dynamic> response) {
cw_bitcoin/lib/electrum_wallet.dart
+48 -21
@@ -4,6 +4,7 @@ import 'dart:io';
4 import 'dart:isolate';
5
6 import 'package:bitcoin_base/bitcoin_base.dart';
7 +import 'package:cw_bitcoin/bitcoin_wallet.dart';
8 import 'package:shared_preferences/shared_preferences.dart';
9 import 'package:cw_core/encryption_file_utils.dart';
10 import 'package:blockchain_utils/blockchain_utils.dart';
@@ -249,7 +250,7 @@ abstract class ElectrumWalletBase
250 int? _currentChainTip;
251
252 Future<int> getCurrentChainTip() async {
252 - if (_currentChainTip != null) {
253 + if ((_currentChainTip ?? 0) > 0) {
254 return _currentChainTip!;
255 }
256 _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0;
@@ -301,6 +302,7 @@ abstract class ElectrumWalletBase
302
303 @action
304 Future<void> _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async {
305 + if (this is! BitcoinWallet) return;
306 final chainTip = chainTipParam ?? await getUpdatedChainTip();
307
308 if (chainTip == height) {
@@ -467,7 +469,7 @@ abstract class ElectrumWalletBase
469 }
470 } catch (e, stacktrace) {
471 print(stacktrace);
470 - print(e.toString());
472 + print("startSync $e");
473 syncStatus = FailedSyncStatus();
474 }
475 }
@@ -479,10 +481,10 @@ abstract class ElectrumWalletBase
481 final response =
482 await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended"));
483
482 - final result = json.decode(response.body) as Map<String, num>;
483 - final slowFee = result['economyFee']?.toInt() ?? 0;
484 - int mediumFee = result['hourFee']?.toInt() ?? 0;
485 - int fastFee = result['fastestFee']?.toInt() ?? 0;
484 + final result = json.decode(response.body) as Map<String, dynamic>;
485 + final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0;
486 + int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0;
487 + int fastFee = (result['fastestFee'] as num?)?.toInt() ?? 0;
488 if (slowFee == mediumFee) {
489 mediumFee++;
490 }
@@ -491,7 +493,9 @@ abstract class ElectrumWalletBase
493 }
494 _feeRates = [slowFee, mediumFee, fastFee];
495 return;
494 - } catch (_) {}
496 + } catch (e) {
497 + print(e);
498 + }
499 }
500
501 final feeRates = await electrumClient.feeRates(network: network);
@@ -571,7 +575,7 @@ abstract class ElectrumWalletBase
575 await electrumClient.connectToUri(node.uri, useSSL: node.useSSL);
576 } catch (e, stacktrace) {
577 print(stacktrace);
574 - print(e.toString());
578 + print("connectToNode $e");
579 syncStatus = FailedSyncStatus();
580 }
581 }
@@ -826,8 +830,8 @@ abstract class ElectrumWalletBase
830 }
831
832 final changeAddress = await walletAddresses.getChangeAddress(
833 + inputs: utxoDetails.availableInputs,
834 outputs: updatedOutputs,
830 - utxoDetails: utxoDetails,
835 );
836 final address = RegexUtils.addressTypeFromStr(changeAddress, network);
837 updatedOutputs.add(BitcoinOutput(
@@ -1181,6 +1185,7 @@ abstract class ElectrumWalletBase
1185 hasChange: estimatedTx.hasChange,
1186 isSendAll: estimatedTx.isSendAll,
1187 hasTaprootInputs: hasTaprootInputs,
1188 + utxos: estimatedTx.utxos,
1189 )..addListener((transaction) async {
1190 transactionHistory.addOne(transaction);
1191 if (estimatedTx.spendsSilentPayment) {
@@ -1370,7 +1375,7 @@ abstract class ElectrumWalletBase
1375 });
1376 }
1377
1373 - // Set the balance of all non-silent payment addresses to 0 before updating
1378 + // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating
1379 walletAddresses.allAddresses
1380 .where((element) => element.type != SegwitAddresType.mweb)
1381 .forEach((addr) {
@@ -1487,7 +1492,7 @@ abstract class ElectrumWalletBase
1492 await unspentCoinsInfo.deleteAll(keys);
1493 }
1494 } catch (e) {
1490 - print(e.toString());
1495 + print("refreshUnspentCoinsInfo $e");
1496 }
1497 }
1498
@@ -1831,7 +1836,7 @@ abstract class ElectrumWalletBase
1836
1837 return historiesWithDetails;
1838 } catch (e) {
1834 - print(e.toString());
1839 + print("fetchTransactions $e");
1840 return {};
1841 }
1842 }
@@ -1905,7 +1910,9 @@ abstract class ElectrumWalletBase
1910 if (height > 0) {
1911 storedTx.height = height;
1912 // the tx's block itself is the first confirmation so add 1
1908 - if (currentHeight != null) storedTx.confirmations = currentHeight - height + 1;
1913 + if ((currentHeight ?? 0) > 0) {
1914 + storedTx.confirmations = currentHeight! - height + 1;
1915 + }
1916 storedTx.isPending = storedTx.confirmations == 0;
1917 }
1918
@@ -1946,9 +1953,13 @@ abstract class ElectrumWalletBase
1953 }
1954 await getCurrentChainTip();
1955
1949 - transactionHistory.transactions.values.forEach((tx) async {
1950 - if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height != null && tx.height! > 0) {
1951 - tx.confirmations = await getCurrentChainTip() - tx.height! + 1;
1956 + transactionHistory.transactions.values.forEach((tx) {
1957 + if (tx.unspents != null &&
1958 + tx.unspents!.isNotEmpty &&
1959 + tx.height != null &&
1960 + tx.height! > 0 &&
1961 + (_currentChainTip ?? 0) > 0) {
1962 + tx.confirmations = _currentChainTip! - tx.height! + 1;
1963 }
1964 });
1965
@@ -1973,9 +1984,17 @@ abstract class ElectrumWalletBase
1984 await Future.wait(unsubscribedScriptHashes.map((address) async {
1985 final sh = address.getScriptHash(network);
1986 if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) {
1976 - await _scripthashesUpdateSubject[sh]?.close();
1987 + try {
1988 + await _scripthashesUpdateSubject[sh]?.close();
1989 + } catch (e) {
1990 + print("failed to close: $e");
1991 + }
1992 + }
1993 + try {
1994 + _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh);
1995 + } catch (e) {
1996 + print("failed scripthashUpdate: $e");
1997 }
1978 - _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh);
1998 _scripthashesUpdateSubject[sh]?.listen((event) async {
1999 try {
2000 await updateUnspentsForAddress(address);
@@ -2171,6 +2190,7 @@ abstract class ElectrumWalletBase
2190
2191 @action
2192 void _onConnectionStatusChange(ConnectionStatus status) {
2193 +
2194 switch (status) {
2195 case ConnectionStatus.connected:
2196 if (syncStatus is NotConnectedSyncStatus ||
@@ -2182,19 +2202,26 @@ abstract class ElectrumWalletBase
2202
2203 break;
2204 case ConnectionStatus.disconnected:
2185 - syncStatus = NotConnectedSyncStatus();
2205 + if (syncStatus is! NotConnectedSyncStatus) {
2206 + syncStatus = NotConnectedSyncStatus();
2207 + }
2208 break;
2209 case ConnectionStatus.failed:
2188 - syncStatus = LostConnectionSyncStatus();
2210 + if (syncStatus is! LostConnectionSyncStatus) {
2211 + syncStatus = LostConnectionSyncStatus();
2212 + }
2213 break;
2214 case ConnectionStatus.connecting:
2191 - syncStatus = ConnectingSyncStatus();
2215 + if (syncStatus is! ConnectingSyncStatus) {
2216 + syncStatus = ConnectingSyncStatus();
2217 + }
2218 break;
2219 default:
2220 }
2221 }
2222
2223 void _syncStatusReaction(SyncStatus syncStatus) async {
2224 + print("SYNC_STATUS_CHANGE: ${syncStatus}");
2225 if (syncStatus is SyncingSyncStatus) {
2226 return;
2227 }
cw_bitcoin/lib/electrum_wallet_addresses.dart
+3 -3
@@ -3,7 +3,7 @@ import 'dart:io' show Platform;
3 import 'package:bitcoin_base/bitcoin_base.dart';
4 import 'package:blockchain_utils/blockchain_utils.dart';
5 import 'package:cw_bitcoin/bitcoin_address_record.dart';
6 -import 'package:cw_bitcoin/electrum_wallet.dart';
6 +import 'package:cw_bitcoin/bitcoin_unspent.dart';
7 import 'package:cw_core/wallet_addresses.dart';
8 import 'package:cw_core/wallet_info.dart';
9 import 'package:cw_core/wallet_type.dart';
@@ -267,7 +267,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
267 }
268
269 @action
270 - Future<String> getChangeAddress({List<BitcoinOutput>? outputs, UtxoDetails? utxoDetails}) async {
270 + Future<String> getChangeAddress({List<BitcoinUnspent>? inputs, List<BitcoinOutput>? outputs, bool isPegIn = false}) async {
271 updateChangeAddresses();
272
273 if (changeAddresses.isEmpty) {
@@ -478,7 +478,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
478
479 await saveAddressesInBox();
480 } catch (e) {
481 - print(e.toString());
481 + print("updateAddresses $e");
482 }
483 }
484
cw_bitcoin/lib/litecoin_wallet.dart
+105 -45
@@ -7,6 +7,7 @@ import 'package:crypto/crypto.dart';
7 import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
8 import 'package:cw_core/cake_hive.dart';
9 import 'package:cw_core/mweb_utxo.dart';
10 +import 'package:cw_core/unspent_coin_type.dart';
11 import 'package:cw_mweb/mwebd.pbgrpc.dart';
12 import 'package:fixnum/fixnum.dart';
13 import 'package:bip39/bip39.dart' as bip39;
@@ -95,6 +96,36 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
96 autorun((_) {
97 this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
98 });
99 + reaction((_) => mwebSyncStatus, (status) async {
100 + if (mwebSyncStatus is FailedSyncStatus) {
101 + // we failed to connect to mweb, check if we are connected to the litecoin node:
102 + late int nodeHeight;
103 + try {
104 + nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0;
105 + } catch (_) {
106 + nodeHeight = 0;
107 + }
108 +
109 + if (nodeHeight == 0) {
110 + // we aren't connected to the litecoin node, so the current electrum_wallet reactions will take care of this case for us
111 + } else {
112 + // we're connected to the litecoin node, but we failed to connect to mweb, try again after a few seconds:
113 + await CwMweb.stop();
114 + await Future.delayed(const Duration(seconds: 5));
115 + startSync();
116 + }
117 + } else if (mwebSyncStatus is SyncingSyncStatus) {
118 + syncStatus = mwebSyncStatus;
119 + } else if (mwebSyncStatus is SyncronizingSyncStatus) {
120 + if (syncStatus is! SyncronizingSyncStatus) {
121 + syncStatus = mwebSyncStatus;
122 + }
123 + } else if (mwebSyncStatus is SyncedSyncStatus) {
124 + if (syncStatus is! SyncedSyncStatus) {
125 + syncStatus = mwebSyncStatus;
126 + }
127 + }
128 + });
129 }
130 late final Bip32Slip10Secp256k1 mwebHd;
131 late final Box<MwebUtxo> mwebUtxosBox;
@@ -105,6 +136,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
136 late bool mwebEnabled;
137 bool processingUtxos = false;
138
139 + @observable
140 + SyncStatus mwebSyncStatus = NotConnectedSyncStatus();
141 +
142 List<int> get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
143 List<int> get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw;
144
@@ -244,13 +278,24 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
278 @override
279 Future<void> startSync() async {
280 print("startSync() called!");
247 - if (syncStatus is SyncronizingSyncStatus) {
281 + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled");
282 + if (!mwebEnabled) {
283 + try {
284 + // in case we're switching from a litecoin wallet that had mweb enabled
285 + CwMweb.stop();
286 + } catch (_) {}
287 + super.startSync();
288 + return;
289 + }
290 +
291 + if (mwebSyncStatus is SyncronizingSyncStatus) {
292 return;
293 }
294 +
295 print("STARTING SYNC - MWEB ENABLED: $mwebEnabled");
296 _syncTimer?.cancel();
297 try {
253 - syncStatus = SyncronizingSyncStatus();
298 + mwebSyncStatus = SyncronizingSyncStatus();
299 try {
300 await subscribeForUpdates();
301 } catch (e) {
@@ -261,45 +306,32 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
306 _feeRatesTimer =
307 Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
308
264 - if (!mwebEnabled) {
265 - try {
266 - // in case we're switching from a litecoin wallet that had mweb enabled
267 - CwMweb.stop();
268 - } catch (_) {}
269 - try {
270 - await updateAllUnspents();
271 - await updateTransactions();
272 - await updateBalance();
273 - syncStatus = SyncedSyncStatus();
274 - } catch (e, s) {
275 - print(e);
276 - print(s);
277 - syncStatus = FailedSyncStatus();
278 - }
279 - return;
280 - }
281 -
309 + print("START SYNC FUNCS");
310 await waitForMwebAddresses();
311 await processMwebUtxos();
312 await updateTransactions();
313 await updateUnspent();
314 await updateBalance();
287 - } catch (e) {
288 - print("failed to start mweb sync: $e");
289 - syncStatus = FailedSyncStatus(error: "failed to start");
315 + print("DONE SYNC FUNCS");
316 + } catch (e, s) {
317 + print("mweb sync failed: $e $s");
318 + mwebSyncStatus = FailedSyncStatus(error: "mweb sync failed: $e");
319 return;
320 }
321
322 _syncTimer = Timer.periodic(const Duration(milliseconds: 3000), (timer) async {
294 - if (syncStatus is FailedSyncStatus) return;
323 + if (mwebSyncStatus is FailedSyncStatus) {
324 + _syncTimer?.cancel();
325 + return;
326 + }
327
328 final nodeHeight =
329 await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node
330
331 if (nodeHeight == 0) {
332 // we aren't connected to the ltc node yet
301 - if (syncStatus is! NotConnectedSyncStatus) {
302 - syncStatus = FailedSyncStatus(error: "Failed to connect to Litecoin node");
333 + if (mwebSyncStatus is! NotConnectedSyncStatus) {
334 + mwebSyncStatus = FailedSyncStatus(error: "litecoin node isn't connected");
335 }
336 return;
337 }
@@ -309,12 +341,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
341 try {
342 if (resp.blockHeaderHeight < nodeHeight) {
343 int h = resp.blockHeaderHeight;
312 - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
344 + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
345 } else if (resp.mwebHeaderHeight < nodeHeight) {
346 int h = resp.mwebHeaderHeight;
315 - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
347 + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
348 } else if (resp.mwebUtxosHeight < nodeHeight) {
317 - syncStatus = SyncingSyncStatus(1, 0.999);
349 + mwebSyncStatus = SyncingSyncStatus(1, 0.999);
350 } else {
351 if (resp.mwebUtxosHeight > walletInfo.restoreHeight) {
352 await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight);
@@ -325,6 +357,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
357 int txHeight = transaction.height ?? resp.mwebUtxosHeight;
358 final confirmations = (resp.mwebUtxosHeight - txHeight) + 1;
359 if (transaction.confirmations == confirmations) continue;
360 + if (transaction.confirmations == 0) {
361 + updateBalance();
362 + }
363 transaction.confirmations = confirmations;
364 transactionHistory.addOne(transaction);
365 }
@@ -332,17 +367,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
367 }
368
369 // prevent unnecessary reaction triggers:
335 - if (syncStatus is! SyncedSyncStatus) {
370 + if (mwebSyncStatus is! SyncedSyncStatus) {
371 // mwebd is synced, but we could still be processing incoming utxos:
372 if (!processingUtxos) {
338 - syncStatus = SyncedSyncStatus();
373 + mwebSyncStatus = SyncedSyncStatus();
374 }
375 }
376 return;
377 }
378 } catch (e) {
379 print("error syncing: $e");
345 - syncStatus = FailedSyncStatus(error: e.toString());
380 + mwebSyncStatus = FailedSyncStatus(error: e.toString());
381 }
382 });
383 }
@@ -512,8 +547,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
547 }
548 _utxoStream = responseStream.listen((Utxo sUtxo) async {
549 // we're processing utxos, so our balance could still be innacurate:
515 - if (syncStatus is! SyncronizingSyncStatus && syncStatus is! SyncingSyncStatus) {
516 - syncStatus = SyncronizingSyncStatus();
550 + if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) {
551 + mwebSyncStatus = SyncronizingSyncStatus();
552 processingUtxos = true;
553 _processingTimer?.cancel();
554 _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
@@ -530,10 +565,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
565 value: sUtxo.value.toInt(),
566 );
567
533 - // if (mwebUtxosBox.containsKey(utxo.outputId)) {
534 - // // we've already stored this utxo, skip it:
535 - // return;
536 - // }
568 + if (mwebUtxosBox.containsKey(utxo.outputId)) {
569 + // we've already stored this utxo, skip it:
570 + // but do update the utxo height if it's somehow different:
571 + final existingUtxo = mwebUtxosBox.get(utxo.outputId);
572 + if (existingUtxo!.height != utxo.height) {
573 + print(
574 + "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}");
575 + existingUtxo.height = utxo.height;
576 + await mwebUtxosBox.put(utxo.outputId, existingUtxo);
577 + }
578 + return;
579 + }
580
581 await updateUnspent();
582 await updateBalance();
@@ -579,7 +622,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
622 final height = await electrumClient.getCurrentBlockChainTip();
623 if (height == null || status.blockHeaderHeight != height) return;
624 if (status.mwebUtxosHeight != height) return; // we aren't synced
582 -
625 int amount = 0;
626 Set<String> inputAddresses = {};
627 var output = convert.AccumulatorSink<Digest>();
@@ -673,10 +715,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
715 @override
716 @action
717 Future<void> updateAllUnspents() async {
676 - // get ltc unspents:
677 - await super.updateAllUnspents();
678 -
718 if (!mwebEnabled) {
719 + await super.updateAllUnspents();
720 return;
721 }
722
@@ -712,6 +752,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
752 }
753 mwebUnspentCoins.add(unspent);
754 });
755 +
756 + // copy coin control attributes to mwebCoins:
757 + await updateCoins(mwebUnspentCoins);
758 + // get regular ltc unspents (this resets unspentCoins):
759 + await super.updateAllUnspents();
760 + // add the mwebCoins:
761 unspentCoins.addAll(mwebUnspentCoins);
762 }
763
@@ -890,6 +936,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
936 tx.isMweb = mwebEnabled;
937
938 if (!mwebEnabled) {
939 + tx.changeAddressOverride =
940 + await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false);
941 return tx;
942 }
943 await waitForMwebAddresses();
@@ -913,12 +961,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
961 hasMwebOutput = true;
962 break;
963 }
964 + if (output.address.toLowerCase().contains("mweb")) {
965 + hasMwebOutput = true;
966 + break;
967 + }
968 }
969
918 - if (tx2.mwebBytes != null && tx2.mwebBytes!.isNotEmpty) {
919 - hasMwebInput = true;
970 + // check if mweb inputs are used:
971 + for (final utxo in tx.utxos) {
972 + if (utxo.utxo.scriptType == SegwitAddresType.mweb) {
973 + hasMwebInput = true;
974 + }
975 }
976
977 + bool isPegIn = !hasMwebInput && hasMwebOutput;
978 + bool isRegular = !hasMwebInput && !hasMwebOutput;
979 + tx.changeAddressOverride = await (walletAddresses as LitecoinWalletAddresses)
980 + .getChangeAddress(isPegIn: isPegIn || isRegular);
981 if (!hasMwebInput && !hasMwebOutput) {
982 tx.isMweb = false;
983 return tx;
@@ -971,7 +1030,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1030 final addresses = <String>{};
1031 transaction.inputAddresses?.forEach((id) async {
1032 final utxo = mwebUtxosBox.get(id);
974 - // await mwebUtxosBox.delete(id);// gets deleted in checkMwebUtxosSpent
1033 + await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent
1034 if (utxo == null) return;
1035 final addressRecord = walletAddresses.allAddresses
1036 .firstWhere((addressRecord) => addressRecord.address == utxo.address);
@@ -990,6 +1049,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1049 print(e);
1050 print(s);
1051 if (e.toString().contains("commit failed")) {
1052 + print(e);
1053 throw Exception("Transaction commit failed (no peers responded), please try again.");
1054 }
1055 rethrow;
cw_bitcoin/lib/litecoin_wallet_addresses.dart
+11 -5
@@ -5,6 +5,7 @@ import 'dart:typed_data';
5 import 'package:bitcoin_base/bitcoin_base.dart';
6 import 'package:blockchain_utils/blockchain_utils.dart';
7 import 'package:cw_bitcoin/bitcoin_address_record.dart';
8 +import 'package:cw_bitcoin/bitcoin_unspent.dart';
9 import 'package:cw_bitcoin/electrum_wallet.dart';
10 import 'package:cw_bitcoin/utils.dart';
11 import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
@@ -142,14 +143,15 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
143
144 @action
145 @override
145 - Future<String> getChangeAddress({List<BitcoinOutput>? outputs, UtxoDetails? utxoDetails}) async {
146 + Future<String> getChangeAddress(
147 + {List<BitcoinUnspent>? inputs, List<BitcoinOutput>? outputs, bool isPegIn = false}) async {
148 // use regular change address on peg in, otherwise use mweb for change address:
149
148 - if (!mwebEnabled) {
150 + if (!mwebEnabled || isPegIn) {
151 return super.getChangeAddress();
152 }
153
152 - if (outputs != null && utxoDetails != null) {
154 + if (inputs != null && outputs != null) {
155 // check if this is a PEGIN:
156 bool outputsToMweb = false;
157 bool comesFromMweb = false;
@@ -161,14 +163,18 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
163 outputsToMweb = true;
164 }
165 }
164 - // TODO: this doesn't respect coin control because it doesn't know which available inputs are selected
165 - utxoDetails.availableInputs.forEach((element) {
166 +
167 + inputs.forEach((element) {
168 + if (!element.isSending || element.isFrozen) {
169 + return;
170 + }
171 if (element.address.contains("mweb")) {
172 comesFromMweb = true;
173 }
174 });
175
176 bool isPegIn = !comesFromMweb && outputsToMweb;
177 +
178 if (isPegIn && mwebEnabled) {
179 return super.getChangeAddress();
180 }
cw_bitcoin/lib/pending_bitcoin_transaction.dart
+6
@@ -24,6 +24,7 @@ class PendingBitcoinTransaction with PendingTransaction {
24 this.isSendAll = false,
25 this.hasTaprootInputs = false,
26 this.isMweb = false,
27 + this.utxos = const [],
28 }) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
29
30 final WalletType type;
@@ -36,7 +37,9 @@ class PendingBitcoinTransaction with PendingTransaction {
37 final bool isSendAll;
38 final bool hasChange;
39 final bool hasTaprootInputs;
40 + List<UtxoWithAddress> utxos;
41 bool isMweb;
42 + String? changeAddressOverride;
43 String? idOverride;
44 String? hexOverride;
45 List<String>? outputAddresses;
@@ -63,6 +66,9 @@ class PendingBitcoinTransaction with PendingTransaction {
66 PendingChange? get change {
67 try {
68 final change = _tx.outputs.firstWhere((out) => out.isChange);
69 + if (changeAddressOverride != null) {
70 + return PendingChange(changeAddressOverride!, BtcUtils.fromSatoshi(change.amount));
71 + }
72 return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount));
73 } catch (_) {
74 return null;
cw_mweb/lib/cw_mweb.dart
+5 -2
@@ -40,7 +40,7 @@ class CwMweb {
40 }
41
42 static Future<void> _initializeClient() async {
43 - print("initialize client called!");
43 + print("_initializeClient() called!");
44 final appDir = await getApplicationSupportDirectory();
45 const ltcNodeUri = "ltc-electrum.cakewallet.com:9333";
46
@@ -54,7 +54,7 @@ class CwMweb {
54 log("Attempting to connect to server on port: $_port");
55
56 // wait for the server to finish starting up before we try to connect to it:
57 - await Future.delayed(const Duration(seconds: 5));
57 + await Future.delayed(const Duration(seconds: 8));
58
59 _clientChannel = ClientChannel('127.0.0.1', port: _port!, channelShutdownHandler: () {
60 _rpcClient = null;
@@ -83,10 +83,13 @@ class CwMweb {
83 log("Attempt $i failed: $e");
84 log('Caught grpc error: ${e.message}');
85 _rpcClient = null;
86 + // necessary if the database isn't open:
87 + await stop();
88 await Future.delayed(const Duration(seconds: 3));
89 } catch (e) {
90 log("Attempt $i failed: $e");
91 _rpcClient = null;
92 + await stop();
93 await Future.delayed(const Duration(seconds: 3));
94 }
95 }
lib/bitcoin/cw_bitcoin.dart
+17 -5
@@ -208,7 +208,7 @@ class CWBitcoin extends Bitcoin {
208 {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) {
209 final bitcoinWallet = wallet as ElectrumWallet;
210 return bitcoinWallet.unspentCoins.where((element) {
211 - switch(coinTypeToSpendFrom) {
211 + switch (coinTypeToSpendFrom) {
212 case UnspentCoinType.mweb:
213 return element.bitcoinAddressRecord.type == SegwitAddresType.mweb;
214 case UnspentCoinType.nonMweb:
@@ -216,7 +216,6 @@ class CWBitcoin extends Bitcoin {
216 case UnspentCoinType.any:
217 return true;
218 }
219 -
219 }).toList();
220 }
221
@@ -399,19 +398,21 @@ class CWBitcoin extends Bitcoin {
398 final history = await electrumClient.getHistory(sh);
399
400 final balance = await electrumClient.getBalance(sh);
402 - dInfoCopy.balance = balance.entries.first.value.toString();
401 + dInfoCopy.balance = balance.entries.firstOrNull?.value.toString() ?? "0";
402 dInfoCopy.address = address;
403 dInfoCopy.transactionsCount = history.length;
404
405 list.add(dInfoCopy);
407 - } catch (e) {
408 - print(e);
406 + } catch (e, s) {
407 + print("derivationInfoError: $e");
408 + print("derivationInfoStack: $s");
409 }
410 }
411 }
412
413 // sort the list such that derivations with the most transactions are first:
414 list.sort((a, b) => b.transactionsCount.compareTo(a.transactionsCount));
415 +
416 return list;
417 }
418
@@ -682,4 +683,15 @@ class CWBitcoin extends Bitcoin {
683 return null;
684 }
685 }
686 +
687 + String? getUnusedSegwitAddress(Object wallet) {
688 + try {
689 + final electrumWallet = wallet as ElectrumWallet;
690 + final segwitAddress = electrumWallet.walletAddresses.allAddresses
691 + .firstWhere((element) => !element.isUsed && element.type == SegwitAddresType.p2wpkh);
692 + return segwitAddress.address;
693 + } catch (_) {
694 + return null;
695 + }
696 + }
697 }
lib/entities/default_settings_migration.dart
+7 -1
@@ -899,7 +899,9 @@ Future<void> changeDefaultBitcoinNode(
899 final newCakeWalletBitcoinNode =
900 Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: false);
901
902 - await nodeSource.add(newCakeWalletBitcoinNode);
902 + if (!nodeSource.values.any((element) => element.uriRaw == newCakeWalletBitcoinUri)) {
903 + await nodeSource.add(newCakeWalletBitcoinNode);
904 + }
905
906 if (needToReplaceCurrentBitcoinNode) {
907 await sharedPreferences.setInt(
@@ -931,6 +933,10 @@ Future<void> _addBitcoinNode({
933 bool replaceExisting = false,
934 bool useSSL = false,
935 }) async {
936 + bool isNodeExists = nodeSource.values.any((element) => element.uriRaw == nodeUri);
937 + if (isNodeExists) {
938 + return;
939 + }
940 const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com';
941 final currentBitcoinNodeId =
942 sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
lib/src/screens/dashboard/pages/balance_page.dart
+20 -28
@@ -17,7 +17,6 @@ import 'package:cake_wallet/src/widgets/standard_switch.dart';
17 import 'package:cake_wallet/store/settings_store.dart';
18 import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
19 import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
20 -import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
20 import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
21 import 'package:cake_wallet/utils/feature_flag.dart';
22 import 'package:cake_wallet/utils/payment_request.dart';
@@ -843,7 +842,7 @@ class BalanceRowWidget extends StatelessWidget {
842 crossAxisAlignment: CrossAxisAlignment.start,
843 children: [
844 Container(
846 - margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16),
845 + margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16),
846 child: Stack(
847 children: [
848 if (currency == CryptoCurrency.ltc)
@@ -851,17 +850,15 @@ class BalanceRowWidget extends StatelessWidget {
850 mainAxisAlignment: MainAxisAlignment.end,
851 children: [
852 Container(
854 - padding: EdgeInsets.only(right: 16, top: 16),
853 + padding: EdgeInsets.only(right: 16, top: 0),
854 child: Column(
855 children: [
856 Container(
858 - decoration: BoxDecoration(
859 - color: Colors.white,
860 - shape: BoxShape.circle,
861 - ),
857 child: ImageIcon(
858 AssetImage('assets/images/mweb_logo.png'),
864 - color: Color.fromARGB(255, 11, 70, 129),
859 + color: Theme.of(context)
860 + .extension<BalancePageTheme>()!
861 + .assetTitleColor,
862 size: 40,
863 ),
864 ),
@@ -889,7 +886,6 @@ class BalanceRowWidget extends StatelessWidget {
886 Column(
887 crossAxisAlignment: CrossAxisAlignment.start,
888 children: [
892 - SizedBox(height: 24),
889 Text(
890 '${secondAvailableBalanceLabel}',
891 textAlign: TextAlign.center,
@@ -907,9 +903,9 @@ class BalanceRowWidget extends StatelessWidget {
903 AutoSizeText(
904 secondAvailableBalance,
905 style: TextStyle(
910 - fontSize: 20,
906 + fontSize: 24,
907 fontFamily: 'Lato',
912 - fontWeight: FontWeight.w400,
908 + fontWeight: FontWeight.w900,
909 color: Theme.of(context)
910 .extension<BalancePageTheme>()!
911 .assetTitleColor,
@@ -918,15 +914,15 @@ class BalanceRowWidget extends StatelessWidget {
914 maxLines: 1,
915 textAlign: TextAlign.center,
916 ),
921 - SizedBox(height: 4),
917 + SizedBox(height: 6),
918 if (!isTestnet)
919 Text(
920 '${secondAvailableFiatBalance}',
921 textAlign: TextAlign.center,
922 style: TextStyle(
927 - fontSize: 12,
923 + fontSize: 16,
924 fontFamily: 'Lato',
929 - fontWeight: FontWeight.w400,
925 + fontWeight: FontWeight.w500,
926 color: Theme.of(context)
927 .extension<BalancePageTheme>()!
928 .textColor,
@@ -1019,7 +1015,6 @@ class BalanceRowWidget extends StatelessWidget {
1015 paymentRequest =
1016 PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}"));
1017 }
1022 -
1018 Navigator.pushNamed(
1019 context,
1020 Routes.send,
@@ -1030,11 +1025,10 @@ class BalanceRowWidget extends StatelessWidget {
1025 );
1026 },
1027 style: OutlinedButton.styleFrom(
1033 - backgroundColor: Theme.of(context)
1034 - .extension<SendPageTheme>()!
1035 - .textFieldButtonIconColor
1028 + backgroundColor: Colors.grey.shade400
1029 .withAlpha(50),
1037 - side: BorderSide(color: Colors.grey.shade400, width: 0),
1030 + side: BorderSide(color: Colors.grey.shade400
1031 + .withAlpha(50), width: 0),
1032 shape: RoundedRectangleBorder(
1033 borderRadius: BorderRadius.circular(20),
1034 ),
@@ -1058,7 +1052,7 @@ class BalanceRowWidget extends StatelessWidget {
1052 style: TextStyle(
1053 color: Theme.of(context)
1054 .extension<BalancePageTheme>()!
1061 - .assetTitleColor,
1055 + .textColor,
1056 ),
1057 ),
1058 ],
@@ -1074,13 +1068,12 @@ class BalanceRowWidget extends StatelessWidget {
1068 child: OutlinedButton(
1069 onPressed: () {
1070 final litecoinAddress =
1077 - bitcoin!.getAddress(dashboardViewModel.wallet);
1071 + bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet);
1072 PaymentRequest? paymentRequest = null;
1079 - if (litecoinAddress.isNotEmpty) {
1073 + if ((litecoinAddress?.isNotEmpty ?? false)) {
1074 paymentRequest = PaymentRequest.fromUri(
1075 Uri.parse("litecoin:${litecoinAddress}"));
1076 }
1083 -
1077 Navigator.pushNamed(
1078 context,
1079 Routes.send,
@@ -1091,11 +1084,10 @@ class BalanceRowWidget extends StatelessWidget {
1084 );
1085 },
1086 style: OutlinedButton.styleFrom(
1094 - backgroundColor: Theme.of(context)
1095 - .extension<SendPageTheme>()!
1096 - .textFieldButtonIconColor
1087 + backgroundColor: Colors.grey.shade400
1088 .withAlpha(50),
1098 - side: BorderSide(color: Colors.grey.shade400, width: 0),
1089 + side: BorderSide(color: Colors.grey.shade400
1090 + .withAlpha(50), width: 0),
1091 shape: RoundedRectangleBorder(
1092 borderRadius: BorderRadius.circular(20),
1093 ),
@@ -1119,7 +1111,7 @@ class BalanceRowWidget extends StatelessWidget {
1111 style: TextStyle(
1112 color: Theme.of(context)
1113 .extension<BalancePageTheme>()!
1122 - .assetTitleColor,
1114 + .textColor,
1115 ),
1116 ),
1117 ],
lib/src/screens/send/send_page.dart
+5
@@ -28,6 +28,7 @@ import 'package:cake_wallet/utils/request_review_handler.dart';
28 import 'package:cake_wallet/utils/responsive_layout_util.dart';
29 import 'package:cake_wallet/utils/show_pop_up.dart';
30 import 'package:cake_wallet/view_model/send/output.dart';
31 +import 'package:cw_core/unspent_coin_type.dart';
32 import 'package:cw_core/wallet_type.dart';
33 import 'package:cake_wallet/view_model/send/send_view_model.dart';
34 import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
@@ -508,6 +509,10 @@ class SendPage extends BasePage {
509 if (state is TransactionCommitted) {
510 newContactAddress =
511 newContactAddress ?? sendViewModel.newContactAddress();
512 +
513 + if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) {
514 + newContactAddress = null;
515 + }
516
517 final successMessage = S.of(_dialogContext).send_success(
518 sendViewModel.selectedCryptoCurrency.toString());
lib/view_model/wallet_creation_vm.dart
+23 -23
@@ -115,7 +115,9 @@ abstract class WalletCreationVMBase with Store {
115 getIt.get<BackgroundTasks>().registerSyncTask();
116 _appStore.authenticationStore.allowed();
117 state = ExecutedSuccessfullyState();
118 - } catch (e, _) {
118 + } catch (e, s) {
119 + print("error: $e");
120 + print("stack: $s");
121 state = FailureState(e.toString());
122 }
123 }
@@ -194,31 +196,29 @@ abstract class WalletCreationVMBase with Store {
196 final walletType = restoreWallet.type;
197 var appStore = getIt.get<AppStore>();
198 var node = appStore.settingsStore.getCurrentNode(walletType);
197 -
198 - switch (walletType) {
199 - case WalletType.bitcoin:
200 - case WalletType.litecoin:
201 -
202 - final derivationList = await bitcoin!.getDerivationsFromMnemonic(
203 - mnemonic: restoreWallet.mnemonicSeed!,
204 - node: node,
205 - passphrase: restoreWallet.passphrase,
206 - );
199
200 + switch (walletType) {
201 + case WalletType.bitcoin:
202 + case WalletType.litecoin:
203 + final derivationList = await bitcoin!.getDerivationsFromMnemonic(
204 + mnemonic: restoreWallet.mnemonicSeed!,
205 + node: node,
206 + passphrase: restoreWallet.passphrase,
207 + );
208
209 - if (derivationList.first.transactionsCount == 0 && derivationList.length > 1) return [];
210 -
211 - return derivationList;
209 + if (derivationList.firstOrNull?.transactionsCount == 0 && derivationList.length > 1)
210 + return [];
211 + return derivationList;
212
213 - case WalletType.nano:
214 - return nanoUtil!.getDerivationsFromMnemonic(
215 - mnemonic: restoreWallet.mnemonicSeed!,
216 - node: node,
217 - );
218 - default:
219 - break;
220 - }
221 - return list;
213 + case WalletType.nano:
214 + return nanoUtil!.getDerivationsFromMnemonic(
215 + mnemonic: restoreWallet.mnemonicSeed!,
216 + node: node,
217 + );
218 + default:
219 + break;
220 + }
221 + return list;
222 }
223
224 WalletCredentials getCredentials(dynamic options) => throw UnimplementedError();
tool/configure.dart
+1
@@ -231,6 +231,7 @@ abstract class Bitcoin {
231 Future<void> setMwebEnabled(Object wallet, bool enabled);
232 bool getMwebEnabled(Object wallet);
233 String? getUnusedMwebAddress(Object wallet);
234 + String? getUnusedSegwitAddress(Object wallet);
235 }
236 """;
237