diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 54ccaaef5..a8cb7ee9f 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -68,8 +68,8 @@ class ElectrumClient { try { await socket?.close(); - socket = null; } catch (_) {} + socket = null; try { if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { @@ -102,7 +102,8 @@ class ElectrumClient { return; } - _setConnectionStatus(ConnectionStatus.connected); + // use ping to determine actual connection status since we could've just not timed out yet: + // _setConnectionStatus(ConnectionStatus.connected); socket!.listen( (Uint8List event) { @@ -128,7 +129,7 @@ class ElectrumClient { print("SOCKET CLOSED!!!!!"); unterminatedString = ''; try { - if (host == socket?.address.host) { + if (host == socket?.address.host || socket == null) { _setConnectionStatus(ConnectionStatus.disconnected); socket?.destroy(); } @@ -178,7 +179,7 @@ class ElectrumClient { unterminatedString = ''; } } catch (e) { - print(e.toString()); + print("parse $e"); } } @@ -191,7 +192,7 @@ class ElectrumClient { try { await callWithTimeout(method: 'server.ping'); _setConnectionStatus(ConnectionStatus.connected); - } on RequestFailedTimeoutException catch (_) { + } catch (_) { _setConnectionStatus(ConnectionStatus.disconnected); } } @@ -431,7 +432,7 @@ class ElectrumClient { return subscription; } catch (e) { - print(e.toString()); + print("subscribe $e"); return null; } } @@ -470,7 +471,8 @@ class ElectrumClient { return completer.future; } catch (e) { - print(e.toString()); + print("callWithTimeout $e"); + rethrow; } } @@ -537,6 +539,12 @@ class ElectrumClient { onConnectionStatusChange?.call(status); _connectionStatus = status; _isConnected = status == ConnectionStatus.connected; + if (!_isConnected) { + try { + socket?.destroy(); + } catch (_) {} + socket = null; + } } void _handleResponse(Map response) { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 527bef3ea..893cff6f2 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -249,7 +250,7 @@ abstract class ElectrumWalletBase int? _currentChainTip; Future getCurrentChainTip() async { - if (_currentChainTip != null) { + if ((_currentChainTip ?? 0) > 0) { return _currentChainTip!; } _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; @@ -301,6 +302,7 @@ abstract class ElectrumWalletBase @action Future _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async { + if (this is! BitcoinWallet) return; final chainTip = chainTipParam ?? await getUpdatedChainTip(); if (chainTip == height) { @@ -467,7 +469,7 @@ abstract class ElectrumWalletBase } } catch (e, stacktrace) { print(stacktrace); - print(e.toString()); + print("startSync $e"); syncStatus = FailedSyncStatus(); } } @@ -479,10 +481,10 @@ abstract class ElectrumWalletBase final response = await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended")); - final result = json.decode(response.body) as Map; - final slowFee = result['economyFee']?.toInt() ?? 0; - int mediumFee = result['hourFee']?.toInt() ?? 0; - int fastFee = result['fastestFee']?.toInt() ?? 0; + final result = json.decode(response.body) as Map; + final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0; + int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0; + int fastFee = (result['fastestFee'] as num?)?.toInt() ?? 0; if (slowFee == mediumFee) { mediumFee++; } @@ -491,7 +493,9 @@ abstract class ElectrumWalletBase } _feeRates = [slowFee, mediumFee, fastFee]; return; - } catch (_) {} + } catch (e) { + print(e); + } } final feeRates = await electrumClient.feeRates(network: network); @@ -571,7 +575,7 @@ abstract class ElectrumWalletBase await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); } catch (e, stacktrace) { print(stacktrace); - print(e.toString()); + print("connectToNode $e"); syncStatus = FailedSyncStatus(); } } @@ -826,8 +830,8 @@ abstract class ElectrumWalletBase } final changeAddress = await walletAddresses.getChangeAddress( + inputs: utxoDetails.availableInputs, outputs: updatedOutputs, - utxoDetails: utxoDetails, ); final address = RegexUtils.addressTypeFromStr(changeAddress, network); updatedOutputs.add(BitcoinOutput( @@ -1181,6 +1185,7 @@ abstract class ElectrumWalletBase hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, )..addListener((transaction) async { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { @@ -1370,7 +1375,7 @@ abstract class ElectrumWalletBase }); } - // Set the balance of all non-silent payment addresses to 0 before updating + // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating walletAddresses.allAddresses .where((element) => element.type != SegwitAddresType.mweb) .forEach((addr) { @@ -1487,7 +1492,7 @@ abstract class ElectrumWalletBase await unspentCoinsInfo.deleteAll(keys); } } catch (e) { - print(e.toString()); + print("refreshUnspentCoinsInfo $e"); } } @@ -1831,7 +1836,7 @@ abstract class ElectrumWalletBase return historiesWithDetails; } catch (e) { - print(e.toString()); + print("fetchTransactions $e"); return {}; } } @@ -1905,7 +1910,9 @@ abstract class ElectrumWalletBase if (height > 0) { storedTx.height = height; // the tx's block itself is the first confirmation so add 1 - if (currentHeight != null) storedTx.confirmations = currentHeight - height + 1; + if ((currentHeight ?? 0) > 0) { + storedTx.confirmations = currentHeight! - height + 1; + } storedTx.isPending = storedTx.confirmations == 0; } @@ -1946,9 +1953,13 @@ abstract class ElectrumWalletBase } await getCurrentChainTip(); - transactionHistory.transactions.values.forEach((tx) async { - if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height != null && tx.height! > 0) { - tx.confirmations = await getCurrentChainTip() - tx.height! + 1; + transactionHistory.transactions.values.forEach((tx) { + if (tx.unspents != null && + tx.unspents!.isNotEmpty && + tx.height != null && + tx.height! > 0 && + (_currentChainTip ?? 0) > 0) { + tx.confirmations = _currentChainTip! - tx.height! + 1; } }); @@ -1973,9 +1984,17 @@ abstract class ElectrumWalletBase await Future.wait(unsubscribedScriptHashes.map((address) async { final sh = address.getScriptHash(network); if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) { - await _scripthashesUpdateSubject[sh]?.close(); + try { + await _scripthashesUpdateSubject[sh]?.close(); + } catch (e) { + print("failed to close: $e"); + } + } + try { + _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); + } catch (e) { + print("failed scripthashUpdate: $e"); } - _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh]?.listen((event) async { try { await updateUnspentsForAddress(address); @@ -2171,6 +2190,7 @@ abstract class ElectrumWalletBase @action void _onConnectionStatusChange(ConnectionStatus status) { + switch (status) { case ConnectionStatus.connected: if (syncStatus is NotConnectedSyncStatus || @@ -2182,19 +2202,26 @@ abstract class ElectrumWalletBase break; case ConnectionStatus.disconnected: - syncStatus = NotConnectedSyncStatus(); + if (syncStatus is! NotConnectedSyncStatus) { + syncStatus = NotConnectedSyncStatus(); + } break; case ConnectionStatus.failed: - syncStatus = LostConnectionSyncStatus(); + if (syncStatus is! LostConnectionSyncStatus) { + syncStatus = LostConnectionSyncStatus(); + } break; case ConnectionStatus.connecting: - syncStatus = ConnectingSyncStatus(); + if (syncStatus is! ConnectingSyncStatus) { + syncStatus = ConnectingSyncStatus(); + } break; default: } } void _syncStatusReaction(SyncStatus syncStatus) async { + print("SYNC_STATUS_CHANGE: ${syncStatus}"); if (syncStatus is SyncingSyncStatus) { return; } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index ce1ff9713..6c10dc615 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -3,7 +3,7 @@ import 'dart:io' show Platform; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -267,7 +267,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { + Future getChangeAddress({List? inputs, List? outputs, bool isPegIn = false}) async { updateChangeAddresses(); if (changeAddresses.isEmpty) { @@ -478,7 +478,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await saveAddressesInBox(); } catch (e) { - print(e.toString()); + print("updateAddresses $e"); } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 29a2df48a..a09850f59 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -7,6 +7,7 @@ import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -95,6 +96,36 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); + reaction((_) => mwebSyncStatus, (status) async { + if (mwebSyncStatus is FailedSyncStatus) { + // we failed to connect to mweb, check if we are connected to the litecoin node: + late int nodeHeight; + try { + nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; + } catch (_) { + nodeHeight = 0; + } + + if (nodeHeight == 0) { + // we aren't connected to the litecoin node, so the current electrum_wallet reactions will take care of this case for us + } else { + // we're connected to the litecoin node, but we failed to connect to mweb, try again after a few seconds: + await CwMweb.stop(); + await Future.delayed(const Duration(seconds: 5)); + startSync(); + } + } else if (mwebSyncStatus is SyncingSyncStatus) { + syncStatus = mwebSyncStatus; + } else if (mwebSyncStatus is SyncronizingSyncStatus) { + if (syncStatus is! SyncronizingSyncStatus) { + syncStatus = mwebSyncStatus; + } + } else if (mwebSyncStatus is SyncedSyncStatus) { + if (syncStatus is! SyncedSyncStatus) { + syncStatus = mwebSyncStatus; + } + } + }); } late final Bip32Slip10Secp256k1 mwebHd; late final Box mwebUtxosBox; @@ -105,6 +136,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { late bool mwebEnabled; bool processingUtxos = false; + @observable + SyncStatus mwebSyncStatus = NotConnectedSyncStatus(); + List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; @@ -244,13 +278,24 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override Future startSync() async { print("startSync() called!"); - if (syncStatus is SyncronizingSyncStatus) { + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); + if (!mwebEnabled) { + try { + // in case we're switching from a litecoin wallet that had mweb enabled + CwMweb.stop(); + } catch (_) {} + super.startSync(); return; } + + if (mwebSyncStatus is SyncronizingSyncStatus) { + return; + } + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); _syncTimer?.cancel(); try { - syncStatus = SyncronizingSyncStatus(); + mwebSyncStatus = SyncronizingSyncStatus(); try { await subscribeForUpdates(); } catch (e) { @@ -261,45 +306,32 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { _feeRatesTimer = Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); - if (!mwebEnabled) { - try { - // in case we're switching from a litecoin wallet that had mweb enabled - CwMweb.stop(); - } catch (_) {} - try { - await updateAllUnspents(); - await updateTransactions(); - await updateBalance(); - syncStatus = SyncedSyncStatus(); - } catch (e, s) { - print(e); - print(s); - syncStatus = FailedSyncStatus(); - } - return; - } - + print("START SYNC FUNCS"); await waitForMwebAddresses(); await processMwebUtxos(); await updateTransactions(); await updateUnspent(); await updateBalance(); - } catch (e) { - print("failed to start mweb sync: $e"); - syncStatus = FailedSyncStatus(error: "failed to start"); + print("DONE SYNC FUNCS"); + } catch (e, s) { + print("mweb sync failed: $e $s"); + mwebSyncStatus = FailedSyncStatus(error: "mweb sync failed: $e"); return; } _syncTimer = Timer.periodic(const Duration(milliseconds: 3000), (timer) async { - if (syncStatus is FailedSyncStatus) return; + if (mwebSyncStatus is FailedSyncStatus) { + _syncTimer?.cancel(); + return; + } final nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node if (nodeHeight == 0) { // we aren't connected to the ltc node yet - if (syncStatus is! NotConnectedSyncStatus) { - syncStatus = FailedSyncStatus(error: "Failed to connect to Litecoin node"); + if (mwebSyncStatus is! NotConnectedSyncStatus) { + mwebSyncStatus = FailedSyncStatus(error: "litecoin node isn't connected"); } return; } @@ -309,12 +341,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { try { if (resp.blockHeaderHeight < nodeHeight) { int h = resp.blockHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); } else if (resp.mwebHeaderHeight < nodeHeight) { int h = resp.mwebHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); } else if (resp.mwebUtxosHeight < nodeHeight) { - syncStatus = SyncingSyncStatus(1, 0.999); + mwebSyncStatus = SyncingSyncStatus(1, 0.999); } else { if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); @@ -325,6 +357,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { int txHeight = transaction.height ?? resp.mwebUtxosHeight; final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; if (transaction.confirmations == confirmations) continue; + if (transaction.confirmations == 0) { + updateBalance(); + } transaction.confirmations = confirmations; transactionHistory.addOne(transaction); } @@ -332,17 +367,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } // prevent unnecessary reaction triggers: - if (syncStatus is! SyncedSyncStatus) { + if (mwebSyncStatus is! SyncedSyncStatus) { // mwebd is synced, but we could still be processing incoming utxos: if (!processingUtxos) { - syncStatus = SyncedSyncStatus(); + mwebSyncStatus = SyncedSyncStatus(); } } return; } } catch (e) { print("error syncing: $e"); - syncStatus = FailedSyncStatus(error: e.toString()); + mwebSyncStatus = FailedSyncStatus(error: e.toString()); } }); } @@ -512,8 +547,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } _utxoStream = responseStream.listen((Utxo sUtxo) async { // we're processing utxos, so our balance could still be innacurate: - if (syncStatus is! SyncronizingSyncStatus && syncStatus is! SyncingSyncStatus) { - syncStatus = SyncronizingSyncStatus(); + if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) { + mwebSyncStatus = SyncronizingSyncStatus(); processingUtxos = true; _processingTimer?.cancel(); _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { @@ -530,10 +565,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { value: sUtxo.value.toInt(), ); - // if (mwebUtxosBox.containsKey(utxo.outputId)) { - // // we've already stored this utxo, skip it: - // return; - // } + if (mwebUtxosBox.containsKey(utxo.outputId)) { + // we've already stored this utxo, skip it: + // but do update the utxo height if it's somehow different: + final existingUtxo = mwebUtxosBox.get(utxo.outputId); + if (existingUtxo!.height != utxo.height) { + print( + "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}"); + existingUtxo.height = utxo.height; + await mwebUtxosBox.put(utxo.outputId, existingUtxo); + } + return; + } await updateUnspent(); await updateBalance(); @@ -579,7 +622,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final height = await electrumClient.getCurrentBlockChainTip(); if (height == null || status.blockHeaderHeight != height) return; if (status.mwebUtxosHeight != height) return; // we aren't synced - int amount = 0; Set inputAddresses = {}; var output = convert.AccumulatorSink(); @@ -673,10 +715,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override @action Future updateAllUnspents() async { - // get ltc unspents: - await super.updateAllUnspents(); - if (!mwebEnabled) { + await super.updateAllUnspents(); return; } @@ -712,6 +752,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } mwebUnspentCoins.add(unspent); }); + + // copy coin control attributes to mwebCoins: + await updateCoins(mwebUnspentCoins); + // get regular ltc unspents (this resets unspentCoins): + await super.updateAllUnspents(); + // add the mwebCoins: unspentCoins.addAll(mwebUnspentCoins); } @@ -890,6 +936,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { tx.isMweb = mwebEnabled; if (!mwebEnabled) { + tx.changeAddressOverride = + await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false); return tx; } await waitForMwebAddresses(); @@ -913,12 +961,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { hasMwebOutput = true; break; } + if (output.address.toLowerCase().contains("mweb")) { + hasMwebOutput = true; + break; + } } - if (tx2.mwebBytes != null && tx2.mwebBytes!.isNotEmpty) { - hasMwebInput = true; + // check if mweb inputs are used: + for (final utxo in tx.utxos) { + if (utxo.utxo.scriptType == SegwitAddresType.mweb) { + hasMwebInput = true; + } } + bool isPegIn = !hasMwebInput && hasMwebOutput; + bool isRegular = !hasMwebInput && !hasMwebOutput; + tx.changeAddressOverride = await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: isPegIn || isRegular); if (!hasMwebInput && !hasMwebOutput) { tx.isMweb = false; return tx; @@ -971,7 +1030,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final addresses = {}; transaction.inputAddresses?.forEach((id) async { final utxo = mwebUtxosBox.get(id); - // await mwebUtxosBox.delete(id);// gets deleted in checkMwebUtxosSpent + await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent if (utxo == null) return; final addressRecord = walletAddresses.allAddresses .firstWhere((addressRecord) => addressRecord.address == utxo.address); @@ -990,6 +1049,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { print(e); print(s); if (e.toString().contains("commit failed")) { + print(e); throw Exception("Transaction commit failed (no peers responded), please try again."); } rethrow; diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 0a20665bf..6154a0ead 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; @@ -142,14 +143,15 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @action @override - Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { + Future getChangeAddress( + {List? inputs, List? outputs, bool isPegIn = false}) async { // use regular change address on peg in, otherwise use mweb for change address: - if (!mwebEnabled) { + if (!mwebEnabled || isPegIn) { return super.getChangeAddress(); } - if (outputs != null && utxoDetails != null) { + if (inputs != null && outputs != null) { // check if this is a PEGIN: bool outputsToMweb = false; bool comesFromMweb = false; @@ -161,14 +163,18 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with outputsToMweb = true; } } - // TODO: this doesn't respect coin control because it doesn't know which available inputs are selected - utxoDetails.availableInputs.forEach((element) { + + inputs.forEach((element) { + if (!element.isSending || element.isFrozen) { + return; + } if (element.address.contains("mweb")) { comesFromMweb = true; } }); bool isPegIn = !comesFromMweb && outputsToMweb; + if (isPegIn && mwebEnabled) { return super.getChangeAddress(); } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index c722dc14f..5ed84dbf4 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -24,6 +24,7 @@ class PendingBitcoinTransaction with PendingTransaction { this.isSendAll = false, this.hasTaprootInputs = false, this.isMweb = false, + this.utxos = const [], }) : _listeners = []; final WalletType type; @@ -36,7 +37,9 @@ class PendingBitcoinTransaction with PendingTransaction { final bool isSendAll; final bool hasChange; final bool hasTaprootInputs; + List utxos; bool isMweb; + String? changeAddressOverride; String? idOverride; String? hexOverride; List? outputAddresses; @@ -63,6 +66,9 @@ class PendingBitcoinTransaction with PendingTransaction { PendingChange? get change { try { final change = _tx.outputs.firstWhere((out) => out.isChange); + if (changeAddressOverride != null) { + return PendingChange(changeAddressOverride!, BtcUtils.fromSatoshi(change.amount)); + } return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount)); } catch (_) { return null; diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index 39aa433cd..a1a592fb8 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -40,7 +40,7 @@ class CwMweb { } static Future _initializeClient() async { - print("initialize client called!"); + print("_initializeClient() called!"); final appDir = await getApplicationSupportDirectory(); const ltcNodeUri = "ltc-electrum.cakewallet.com:9333"; @@ -54,7 +54,7 @@ class CwMweb { log("Attempting to connect to server on port: $_port"); // wait for the server to finish starting up before we try to connect to it: - await Future.delayed(const Duration(seconds: 5)); + await Future.delayed(const Duration(seconds: 8)); _clientChannel = ClientChannel('127.0.0.1', port: _port!, channelShutdownHandler: () { _rpcClient = null; @@ -83,10 +83,13 @@ class CwMweb { log("Attempt $i failed: $e"); log('Caught grpc error: ${e.message}'); _rpcClient = null; + // necessary if the database isn't open: + await stop(); await Future.delayed(const Duration(seconds: 3)); } catch (e) { log("Attempt $i failed: $e"); _rpcClient = null; + await stop(); await Future.delayed(const Duration(seconds: 3)); } } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 91232792a..365d86be5 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -208,7 +208,7 @@ class CWBitcoin extends Bitcoin { {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) { final bitcoinWallet = wallet as ElectrumWallet; return bitcoinWallet.unspentCoins.where((element) { - switch(coinTypeToSpendFrom) { + switch (coinTypeToSpendFrom) { case UnspentCoinType.mweb: return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; case UnspentCoinType.nonMweb: @@ -216,7 +216,6 @@ class CWBitcoin extends Bitcoin { case UnspentCoinType.any: return true; } - }).toList(); } @@ -399,19 +398,21 @@ class CWBitcoin extends Bitcoin { final history = await electrumClient.getHistory(sh); final balance = await electrumClient.getBalance(sh); - dInfoCopy.balance = balance.entries.first.value.toString(); + dInfoCopy.balance = balance.entries.firstOrNull?.value.toString() ?? "0"; dInfoCopy.address = address; dInfoCopy.transactionsCount = history.length; list.add(dInfoCopy); - } catch (e) { - print(e); + } catch (e, s) { + print("derivationInfoError: $e"); + print("derivationInfoStack: $s"); } } } // sort the list such that derivations with the most transactions are first: list.sort((a, b) => b.transactionsCount.compareTo(a.transactionsCount)); + return list; } @@ -682,4 +683,15 @@ class CWBitcoin extends Bitcoin { return null; } } + + String? getUnusedSegwitAddress(Object wallet) { + try { + final electrumWallet = wallet as ElectrumWallet; + final segwitAddress = electrumWallet.walletAddresses.allAddresses + .firstWhere((element) => !element.isUsed && element.type == SegwitAddresType.p2wpkh); + return segwitAddress.address; + } catch (_) { + return null; + } + } } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 2b8c49631..198781cea 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -899,7 +899,9 @@ Future changeDefaultBitcoinNode( final newCakeWalletBitcoinNode = Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: false); - await nodeSource.add(newCakeWalletBitcoinNode); + if (!nodeSource.values.any((element) => element.uriRaw == newCakeWalletBitcoinUri)) { + await nodeSource.add(newCakeWalletBitcoinNode); + } if (needToReplaceCurrentBitcoinNode) { await sharedPreferences.setInt( @@ -931,6 +933,10 @@ Future _addBitcoinNode({ bool replaceExisting = false, bool useSSL = false, }) async { + bool isNodeExists = nodeSource.values.any((element) => element.uriRaw == nodeUri); + if (isNodeExists) { + return; + } const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; final currentBitcoinNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 708941023..84bdfd58b 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -17,7 +17,6 @@ import 'package:cake_wallet/src/widgets/standard_switch.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/payment_request.dart'; @@ -843,7 +842,7 @@ class BalanceRowWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), + margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), child: Stack( children: [ if (currency == CryptoCurrency.ltc) @@ -851,17 +850,15 @@ class BalanceRowWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ Container( - padding: EdgeInsets.only(right: 16, top: 16), + padding: EdgeInsets.only(right: 16, top: 0), child: Column( children: [ Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), child: ImageIcon( AssetImage('assets/images/mweb_logo.png'), - color: Color.fromARGB(255, 11, 70, 129), + color: Theme.of(context) + .extension()! + .assetTitleColor, size: 40, ), ), @@ -889,7 +886,6 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 24), Text( '${secondAvailableBalanceLabel}', textAlign: TextAlign.center, @@ -907,9 +903,9 @@ class BalanceRowWidget extends StatelessWidget { AutoSizeText( secondAvailableBalance, style: TextStyle( - fontSize: 20, + fontSize: 24, fontFamily: 'Lato', - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w900, color: Theme.of(context) .extension()! .assetTitleColor, @@ -918,15 +914,15 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.center, ), - SizedBox(height: 4), + SizedBox(height: 6), if (!isTestnet) Text( '${secondAvailableFiatBalance}', textAlign: TextAlign.center, style: TextStyle( - fontSize: 12, + fontSize: 16, fontFamily: 'Lato', - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w500, color: Theme.of(context) .extension()! .textColor, @@ -1019,7 +1015,6 @@ class BalanceRowWidget extends StatelessWidget { paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); } - Navigator.pushNamed( context, Routes.send, @@ -1030,11 +1025,10 @@ class BalanceRowWidget extends StatelessWidget { ); }, style: OutlinedButton.styleFrom( - backgroundColor: Theme.of(context) - .extension()! - .textFieldButtonIconColor + backgroundColor: Colors.grey.shade400 .withAlpha(50), - side: BorderSide(color: Colors.grey.shade400, width: 0), + side: BorderSide(color: Colors.grey.shade400 + .withAlpha(50), width: 0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), @@ -1058,7 +1052,7 @@ class BalanceRowWidget extends StatelessWidget { style: TextStyle( color: Theme.of(context) .extension()! - .assetTitleColor, + .textColor, ), ), ], @@ -1074,13 +1068,12 @@ class BalanceRowWidget extends StatelessWidget { child: OutlinedButton( onPressed: () { final litecoinAddress = - bitcoin!.getAddress(dashboardViewModel.wallet); + bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); PaymentRequest? paymentRequest = null; - if (litecoinAddress.isNotEmpty) { + if ((litecoinAddress?.isNotEmpty ?? false)) { paymentRequest = PaymentRequest.fromUri( Uri.parse("litecoin:${litecoinAddress}")); } - Navigator.pushNamed( context, Routes.send, @@ -1091,11 +1084,10 @@ class BalanceRowWidget extends StatelessWidget { ); }, style: OutlinedButton.styleFrom( - backgroundColor: Theme.of(context) - .extension()! - .textFieldButtonIconColor + backgroundColor: Colors.grey.shade400 .withAlpha(50), - side: BorderSide(color: Colors.grey.shade400, width: 0), + side: BorderSide(color: Colors.grey.shade400 + .withAlpha(50), width: 0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), @@ -1119,7 +1111,7 @@ class BalanceRowWidget extends StatelessWidget { style: TextStyle( color: Theme.of(context) .extension()! - .assetTitleColor, + .textColor, ), ), ], diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index ccf4a1dc4..d881d0341 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -28,6 +28,7 @@ import 'package:cake_wallet/utils/request_review_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; @@ -508,6 +509,10 @@ class SendPage extends BasePage { if (state is TransactionCommitted) { newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); + + if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) { + newContactAddress = null; + } final successMessage = S.of(_dialogContext).send_success( sendViewModel.selectedCryptoCurrency.toString()); diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 90b4c5c3a..17a8d6d28 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -115,7 +115,9 @@ abstract class WalletCreationVMBase with Store { getIt.get().registerSyncTask(); _appStore.authenticationStore.allowed(); state = ExecutedSuccessfullyState(); - } catch (e, _) { + } catch (e, s) { + print("error: $e"); + print("stack: $s"); state = FailureState(e.toString()); } } @@ -194,31 +196,29 @@ abstract class WalletCreationVMBase with Store { final walletType = restoreWallet.type; var appStore = getIt.get(); var node = appStore.settingsStore.getCurrentNode(walletType); - - switch (walletType) { - case WalletType.bitcoin: - case WalletType.litecoin: - final derivationList = await bitcoin!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - passphrase: restoreWallet.passphrase, - ); + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: + final derivationList = await bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + passphrase: restoreWallet.passphrase, + ); + if (derivationList.firstOrNull?.transactionsCount == 0 && derivationList.length > 1) + return []; + return derivationList; - if (derivationList.first.transactionsCount == 0 && derivationList.length > 1) return []; - - return derivationList; - - case WalletType.nano: - return nanoUtil!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - ); - default: - break; - } - return list; + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; + } + return list; } WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); diff --git a/tool/configure.dart b/tool/configure.dart index ce079dd29..704b47526 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -231,6 +231,7 @@ abstract class Bitcoin { Future setMwebEnabled(Object wallet, bool enabled); bool getMwebEnabled(Object wallet); String? getUnusedMwebAddress(Object wallet); + String? getUnusedSegwitAddress(Object wallet); } """;