diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 69433bfc6..f6d390389 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -108,6 +108,53 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { int initialSilentAddressIndex = 0, required bool mempoolAPIEnabled, }) async { + List? seedBytes = null; + final Map hdWallets = {}; + + if (walletInfo.isRecovery) { + for (final derivation in walletInfo.derivations ?? []) { + if (derivation.description?.contains("SP") ?? false) { + continue; + } + + if (derivation.derivationType == DerivationType.bip39) { + seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + + break; + } else { + try { + seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + print("electrum_v2 seed error: $e"); + + try { + seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); + hdWallets[CWBitcoinDerivationType.electrum] = + Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + print("electrum_v1 seed error: $e"); + } + } + + break; + } + } + + if (hdWallets[CWBitcoinDerivationType.bip39] != null) { + hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; + } + if (hdWallets[CWBitcoinDerivationType.electrum] != null) { + hdWallets[CWBitcoinDerivationType.old_electrum] = + hdWallets[CWBitcoinDerivationType.electrum]!; + } + } else { + seedBytes = walletInfo.derivationInfo?.derivationType == DerivationType.electrum + ? ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase) + : Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + } + return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", @@ -119,9 +166,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialSilentAddressIndex: initialSilentAddressIndex, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, - seedBytes: walletInfo.derivationInfo?.derivationType == DerivationType.electrum - ? ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase) - : Bip39SeedGenerator.generateFromString(mnemonic, passphrase), + seedBytes: seedBytes, + hdWallets: hdWallets, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, @@ -253,9 +299,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } Future getNodeIsElectrs() async { - final version = await sendWorker(ElectrumWorkerGetVersionRequest()) as List; + if (node?.uri.host.contains("electrs") ?? false) { + return true; + } - if (version.isNotEmpty) { + final version = await sendWorker(ElectrumWorkerGetVersionRequest()); + + if (version is List && version.isNotEmpty) { final server = version[0]; if (server.toLowerCase().contains('electrs')) { @@ -263,6 +313,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { node!.save(); return node!.isElectrs!; } + } else if (version is String && version.toLowerCase().contains('electrs')) { + node!.isElectrs = true; + node!.save(); + return node!.isElectrs!; } node!.isElectrs = false; @@ -271,33 +325,39 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } Future getNodeSupportsSilentPayments() async { - return true; + // TODO: handle disconnection on check + // TODO: use cached values + if (node == null) { + return false; + } + + final isFulcrum = node!.uri.host.contains("fulcrum"); + if (isFulcrum) { + return false; + } + // As of today (august 2024), only ElectrumRS supports silent payments - // if (!(await getNodeIsElectrs())) { - // return false; - // } + if (!(await getNodeIsElectrs())) { + return false; + } - // if (node == null) { - // return false; - // } + try { + final workerResponse = (await sendWorker(ElectrumWorkerCheckTweaksRequest())) as String; + final tweaksResponse = ElectrumWorkerCheckTweaksResponse.fromJson( + json.decode(workerResponse) as Map, + ); + final supportsScanning = tweaksResponse.result == true; - // try { - // final tweaksResponse = await electrumClient.getTweaks(height: 0); + if (supportsScanning) { + node!.supportsSilentPayments = true; + node!.save(); + return node!.supportsSilentPayments!; + } + } catch (_) {} - // if (tweaksResponse != null) { - // node!.supportsSilentPayments = true; - // node!.save(); - // return node!.supportsSilentPayments!; - // } - // } on RequestFailedTimeoutException catch (_) { - // node!.supportsSilentPayments = false; - // node!.save(); - // return node!.supportsSilentPayments!; - // } catch (_) {} - - // node!.supportsSilentPayments = false; - // node!.save(); - // return node!.supportsSilentPayments!; + node!.supportsSilentPayments = false; + node!.save(); + return node!.supportsSilentPayments!; } LedgerConnection? _ledgerConnection; @@ -383,16 +443,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (tip > walletInfo.restoreHeight) { _setListeners(walletInfo.restoreHeight); } - } else { - alwaysScan = false; - - // _isolate?.then((value) => value.kill(priority: Isolate.immediate)); - - // if (rpc!.isConnected) { - // syncStatus = SyncedSyncStatus(); - // } else { - // syncStatus = NotConnectedSyncStatus(); - // } + } else if (syncStatus is! SyncedSyncStatus) { + await sendWorker(ElectrumWorkerStopScanningRequest()); + await startSync(); } } @@ -565,9 +618,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { messageJson = message as Map; } final workerMethod = messageJson['method'] as String; + final workerError = messageJson['error'] as String?; switch (workerMethod) { case ElectrumRequestMethods.tweaksSubscribeMethod: + if (workerError != null) { + print(messageJson); + // _onConnectionStatusChange(ConnectionStatus.failed); + break; + } + final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson); onTweaksSyncResponse(response.result); break; @@ -651,9 +711,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc); } else { syncStatus = newSyncStatus; + + if (newSyncStatus is SyncedSyncStatus) { + silentPaymentsScanningActive = false; + } } - await walletInfo.updateRestoreHeight(result.height!); + final height = result.height; + if (height != null) { + await walletInfo.updateRestoreHeight(height); + } } await save(); @@ -801,6 +868,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { case SyncingSyncStatus: return; case SyncedTipSyncStatus: + silentPaymentsScanningActive = false; + // Message is shown on the UI for 3 seconds, then reverted to synced Timer(Duration(seconds: 3), () { if (this.syncStatus is SyncedTipSyncStatus) this.syncStatus = SyncedSyncStatus(); diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 1016867fa..2f2f87084 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -361,6 +361,15 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S return labels; } + @override + @action + void updateHiddenAddresses() { + super.updateHiddenAddresses(); + this.hiddenAddresses.addAll(silentPaymentAddresses + .where((addressRecord) => addressRecord.isHidden) + .map((addressRecord) => addressRecord.address)); + } + Map toJson() { final json = super.toJson(); json['silentPaymentAddresses'] = diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 45575dc96..de14efadd 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -125,7 +125,7 @@ abstract class ElectrumWalletBase workerSendPort!.send(json); try { - return completer.future.timeout(Duration(seconds: 5)); + return completer.future.timeout(Duration(seconds: 30)); } catch (e) { _errorCompleters.addAll({messageId: e}); _responseCompleters.remove(messageId); @@ -146,13 +146,8 @@ abstract class ElectrumWalletBase final workerMethod = messageJson['method'] as String; final workerError = messageJson['error'] as String?; - - if (workerError != null) { - print('Worker error: $workerError'); - return; - } - final responseId = messageJson['id'] as int?; + if (responseId != null && _responseCompleters.containsKey(responseId)) { _responseCompleters[responseId]!.complete(message); _responseCompleters.remove(responseId); @@ -160,6 +155,11 @@ abstract class ElectrumWalletBase switch (workerMethod) { case ElectrumWorkerMethods.connectionMethod: + if (workerError != null) { + _onConnectionStatusChange(ConnectionStatus.failed); + break; + } + final response = ElectrumWorkerConnectionResponse.fromJson(messageJson); _onConnectionStatusChange(response.result); break; @@ -214,6 +214,7 @@ abstract class ElectrumWalletBase bool? alwaysScan; bool mempoolAPIEnabled; + bool _updatingHistories = false; final Map hdWallets; Bip32Slip10Secp256k1 get bip32 => walletAddresses.hdWallet; @@ -323,7 +324,8 @@ abstract class ElectrumWalletBase List scripthashesListening; bool _chainTipListenerOn = false; - bool _isInitialSync = true; + // TODO: improve this + int _syncedTimes = 0; void Function(FlutterErrorDetails)? _onError; Timer? _autoSaveTimer; @@ -348,9 +350,11 @@ abstract class ElectrumWalletBase syncStatus = SynchronizingSyncStatus(); - // INFO: FIRST: Call subscribe for headers, wait for completion to update currentChainTip (needed for other methods) + // INFO: FIRST (always): Call subscribe for headers, wait for completion to update currentChainTip (needed for other methods) await sendWorker(ElectrumWorkerHeadersSubscribeRequest()); + _syncedTimes = 0; + // INFO: SECOND: Start loading transaction histories for every address, this will help discover addresses until the unused gap limit has been reached, which will help finding the full balance and unspents next await updateTransactions(); @@ -365,13 +369,14 @@ abstract class ElectrumWalletBase _updateFeeRateTimer ??= Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); - _isInitialSync = false; - syncStatus = SyncedSyncStatus(); + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } await save(); } catch (e, stacktrace) { - print(stacktrace); print("startSync $e"); + print(stacktrace); syncStatus = FailedSyncStatus(); } } @@ -389,8 +394,10 @@ abstract class ElectrumWalletBase } @action - Future onFeesResponse(TransactionPriorities result) async { - feeRates = result; + Future onFeesResponse(TransactionPriorities? result) async { + if (result != null) { + feeRates = result; + } } Node? node; @@ -400,8 +407,6 @@ abstract class ElectrumWalletBase Future connectToNode({required Node node}) async { this.node = node; - if (syncStatus is ConnectingSyncStatus) return; - try { syncStatus = ConnectingSyncStatus(); @@ -416,6 +421,7 @@ abstract class ElectrumWalletBase _workerIsolate = await Isolate.spawn(ElectrumWorker.run, receivePort!.sendPort); _workerSubscription = receivePort!.listen((message) { + print('Main: received message: $message'); if (message is SendPort) { workerSendPort = message; workerSendPort!.send( @@ -1159,15 +1165,11 @@ abstract class ElectrumWalletBase @action Future updateAllUnspents() async { - final req = ElectrumWorkerListUnspentRequest( - scripthashes: walletAddresses.allScriptHashes.toList(), + workerSendPort!.send( + ElectrumWorkerListUnspentRequest( + scripthashes: walletAddresses.allScriptHashes.toList(), + ).toJson(), ); - - if (_isInitialSync) { - await sendWorker(req); - } else { - workerSendPort!.send(req.toJson()); - } } @action @@ -1222,6 +1224,11 @@ abstract class ElectrumWalletBase unspentCoins.forEach(updateCoin); await refreshUnspentCoinsInfo(); + + _syncedTimes++; + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } } @action @@ -1299,10 +1306,13 @@ abstract class ElectrumWalletBase @action Future onHistoriesResponse(List histories) async { - if (histories.isEmpty) { + if (histories.isEmpty || _updatingHistories) { + _updatingHistories = false; return; } + _updatingHistories = true; + final addressesWithHistory = []; BitcoinAddressType? lastDiscoveredType; @@ -1340,7 +1350,13 @@ abstract class ElectrumWalletBase isChange: isChange, derivationType: addressRecord.derivationType, addressType: addressRecord.addressType, - derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressRecord.addressType), + derivationInfo: BitcoinAddressUtils.getDerivationFromType( + addressRecord.addressType, + isElectrum: [ + CWBitcoinDerivationType.electrum, + CWBitcoinDerivationType.old_electrum, + ].contains(addressRecord.derivationType), + ), ); final newAddressList = @@ -1364,6 +1380,12 @@ abstract class ElectrumWalletBase } walletAddresses.updateHiddenAddresses(); + _updatingHistories = false; + + _syncedTimes++; + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } } Future canReplaceByFee(ElectrumTransactionInfo tx) async { @@ -1606,10 +1628,8 @@ abstract class ElectrumWalletBase @action Future updateTransactions([List? addresses]) async { - addresses ??= walletAddresses.allAddresses.toList(); - - final req = ElectrumWorkerGetHistoryRequest( - addresses: addresses, + workerSendPort!.send(ElectrumWorkerGetHistoryRequest( + addresses: walletAddresses.allAddresses.toList(), storedTxs: transactionHistory.transactions.values.toList(), walletType: type, // If we still don't have currentChainTip, txs will still be fetched but shown @@ -1617,13 +1637,7 @@ abstract class ElectrumWalletBase chainTip: currentChainTip ?? getBitcoinHeightByDate(date: DateTime.now()), network: network, mempoolAPIEnabled: mempoolAPIEnabled, - ); - - if (_isInitialSync) { - await sendWorker(req); - } else { - workerSendPort!.send(req.toJson()); - } + ).toJson()); } @action @@ -1663,17 +1677,18 @@ abstract class ElectrumWalletBase unconfirmed: totalUnconfirmed, frozen: totalFrozen, ); + + _syncedTimes++; + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } } @action Future updateBalance() async { - final req = ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes); - - if (_isInitialSync) { - await sendWorker(req); - } else { - workerSendPort!.send(req.toJson()); - } + workerSendPort!.send(ElectrumWorkerGetBalanceRequest( + scripthashes: walletAddresses.allScriptHashes, + ).toJson()); } @override diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 9e8909dc6..3a0cdf4cb 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -23,6 +23,8 @@ class ElectrumWorker { final SendPort sendPort; ElectrumApiProvider? _electrumClient; BasedUtxoNetwork? _network; + bool _isScanning = false; + bool _stopScanRequested = false; ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient}) : _electrumClient = electrumClient; @@ -45,8 +47,6 @@ class ElectrumWorker { } void handleMessage(dynamic message) async { - print("Worker: received message: $message"); - try { Map messageJson; if (message is String) { @@ -97,10 +97,35 @@ class ElectrumWorker { ElectrumWorkerBroadcastRequest.fromJson(messageJson), ); break; - case ElectrumRequestMethods.tweaksSubscribeMethod: - await _handleScanSilentPayments( - ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson), + case ElectrumWorkerMethods.checkTweaksMethod: + await _handleCheckTweaks( + ElectrumWorkerCheckTweaksRequest.fromJson(messageJson), ); + break; + case ElectrumWorkerMethods.stopScanningMethod: + await _handleStopScanning( + ElectrumWorkerStopScanningRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.estimateFeeMethod: + case ElectrumRequestMethods.tweaksSubscribeMethod: + if (_isScanning) { + _stopScanRequested = false; + } + + if (!_stopScanRequested) { + await _handleScanSilentPayments( + ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson), + ); + } else { + _stopScanRequested = false; + _sendResponse( + ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse(syncStatus: SyncedSyncStatus()), + ), + ); + } + break; case ElectrumRequestMethods.estimateFeeMethod: await _handleGetFeeRates( @@ -113,8 +138,7 @@ class ElectrumWorker { ); break; } - } catch (e, s) { - print(s); + } catch (e) { _sendError(ElectrumWorkerErrorResponse(error: e.toString())); } } @@ -122,25 +146,29 @@ class ElectrumWorker { Future _handleConnect(ElectrumWorkerConnectionRequest request) async { _network = request.network; - _electrumClient = await ElectrumApiProvider.connect( - request.useSSL - ? ElectrumSSLService.connect( - request.uri, - onConnectionStatusChange: (status) { - _sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); - }, - defaultRequestTimeOut: const Duration(seconds: 5), - connectionTimeOut: const Duration(seconds: 5), - ) - : ElectrumTCPService.connect( - request.uri, - onConnectionStatusChange: (status) { - _sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); - }, - defaultRequestTimeOut: const Duration(seconds: 5), - connectionTimeOut: const Duration(seconds: 5), - ), - ); + try { + _electrumClient = await ElectrumApiProvider.connect( + request.useSSL + ? ElectrumSSLService.connect( + request.uri, + onConnectionStatusChange: (status) { + _sendResponse( + ElectrumWorkerConnectionResponse(status: status, id: request.id), + ); + }, + ) + : ElectrumTCPService.connect( + request.uri, + onConnectionStatusChange: (status) { + _sendResponse( + ElectrumWorkerConnectionResponse(status: status, id: request.id), + ); + }, + ), + ); + } catch (e) { + _sendError(ElectrumWorkerConnectionError(error: e.toString())); + } } Future _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async { @@ -230,6 +258,7 @@ class ElectrumWorker { hash: txid, currentChainTip: result.chainTip, mempoolAPIEnabled: result.mempoolAPIEnabled, + getTime: true, confirmations: tx?.confirmations, date: tx?.date, ), @@ -367,6 +396,7 @@ class ElectrumWorker { required String hash, required int currentChainTip, required bool mempoolAPIEnabled, + bool getTime = false, int? confirmations, DateTime? date, }) async { @@ -378,52 +408,54 @@ class ElectrumWorker { ElectrumGetTransactionHex(transactionHash: hash), ); - if (mempoolAPIEnabled) { - try { - final txVerbose = await http.get( - Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status", - ), - ); - - if (txVerbose.statusCode == 200 && - txVerbose.body.isNotEmpty && - jsonDecode(txVerbose.body) != null) { - height = jsonDecode(txVerbose.body)['block_height'] as int; - - final blockHash = await http.get( + if (getTime) { + if (mempoolAPIEnabled) { + try { + final txVerbose = await http.get( Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", + "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status", ), ); - if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty) { - final blockResponse = await http.get( + if (txVerbose.statusCode == 200 && + txVerbose.body.isNotEmpty && + jsonDecode(txVerbose.body) != null) { + height = jsonDecode(txVerbose.body)['block_height'] as int; + + final blockHash = await http.get( Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", ), ); - if (blockResponse.statusCode == 200 && - blockResponse.body.isNotEmpty && - jsonDecode(blockResponse.body)['timestamp'] != null) { - time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty) { + final blockResponse = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + ), + ); - if (date != null) { - final newDate = DateTime.fromMillisecondsSinceEpoch(time * 1000); - isDateValidated = newDate == date; + if (blockResponse.statusCode == 200 && + blockResponse.body.isNotEmpty && + jsonDecode(blockResponse.body)['timestamp'] != null) { + time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + + if (date != null) { + final newDate = DateTime.fromMillisecondsSinceEpoch(time * 1000); + isDateValidated = newDate == date; + } } } } - } - } catch (_) {} - } + } catch (_) {} + } - if (confirmations == null && height != null) { - final tip = currentChainTip; - if (tip > 0 && height > 0) { - // Add one because the block itself is the first confirmation - confirmations = tip - height + 1; + if (confirmations == null && height != null) { + final tip = currentChainTip; + if (tip > 0 && height > 0) { + // Add one because the block itself is the first confirmation + confirmations = tip - height + 1; + } } } @@ -498,20 +530,47 @@ class ElectrumWorker { } } + Future _handleCheckTweaks(ElectrumWorkerCheckTweaksRequest request) async { + final response = await _electrumClient!.request( + ElectrumTweaksSubscribe( + height: 0, + count: 1, + historicalMode: false, + ), + ); + + final supportsScanning = response != null; + _sendResponse( + ElectrumWorkerCheckTweaksResponse(result: supportsScanning, id: request.id), + ); + } + + Future _handleStopScanning(ElectrumWorkerStopScanningRequest request) async { + _stopScanRequested = true; + _sendResponse( + ElectrumWorkerStopScanningResponse(result: true, id: request.id), + ); + } + Future _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async { + _isScanning = true; final scanData = request.scanData; + + // TODO: confirmedSwitch use new connection + // final _electrumClient = await ElectrumApiProvider.connect( + // ElectrumTCPService.connect( + // Uri.parse("tcp://electrs.cakewallet.com:50001"), + // onConnectionStatusChange: (status) { + // _sendResponse( + // ElectrumWorkerConnectionResponse(status: status, id: request.id), + // ); + // }, + // ), + // ); + int syncHeight = scanData.height; int initialSyncHeight = syncHeight; - int getCountPerRequest(int syncHeight) { - if (scanData.isSingleScan) { - return 1; - } - - final amountLeft = scanData.chainTip - syncHeight + 1; - return amountLeft; - } - final receivers = scanData.silentPaymentsWallets.map( (wallet) { return Receiver( @@ -525,7 +584,6 @@ class ElectrumWorker { ); // Initial status UI update, send how many blocks in total to scan - final initialCount = getCountPerRequest(syncHeight); _sendResponse(ElectrumWorkerTweaksSubscribeResponse( result: TweaksSyncResponse( height: syncHeight, @@ -535,14 +593,19 @@ class ElectrumWorker { final req = ElectrumTweaksSubscribe( height: syncHeight, - count: initialCount, + count: 1, historicalMode: false, ); final stream = await _electrumClient!.subscribe(req); - Future listenFn(Map event, ElectrumTweaksSubscribe req) async { + void listenFn(Map event, ElectrumTweaksSubscribe req) { final response = req.onResponse(event); + if (_stopScanRequested || response == null) { + _stopScanRequested = false; + _isScanning = false; + return; + } // success or error msg final noData = response.message != null; @@ -554,13 +617,12 @@ class ElectrumWorker { // re-subscribe to continue receiving messages, starting from the next unscanned height final nextHeight = syncHeight + 1; - final nextCount = getCountPerRequest(nextHeight); - if (nextCount > 0) { - final nextStream = await _electrumClient!.subscribe( + if (nextHeight <= scanData.chainTip) { + final nextStream = _electrumClient!.subscribe( ElectrumTweaksSubscribe( - height: syncHeight, - count: initialCount, + height: nextHeight, + count: 1, historicalMode: false, ), ); @@ -693,6 +755,7 @@ class ElectrumWorker { } stream?.listen((event) => listenFn(event, req)); + _isScanning = false; } Future _handleGetVersion(ElectrumWorkerGetVersionRequest request) async { diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart index 6bd4d296e..4d9c85a47 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart @@ -5,10 +5,14 @@ class ElectrumWorkerMethods { static const String connectionMethod = "connection"; static const String unknownMethod = "unknown"; static const String txHashMethod = "txHash"; + static const String checkTweaksMethod = "checkTweaks"; + static const String stopScanningMethod = "stopScanning"; static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod); static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod); static const ElectrumWorkerMethods txHash = ElectrumWorkerMethods._(txHashMethod); + static const ElectrumWorkerMethods checkTweaks = ElectrumWorkerMethods._(checkTweaksMethod); + static const ElectrumWorkerMethods stopScanning = ElectrumWorkerMethods._(stopScanningMethod); @override String toString() { diff --git a/cw_bitcoin/lib/electrum_worker/methods/check_tweaks_method.dart b/cw_bitcoin/lib/electrum_worker/methods/check_tweaks_method.dart new file mode 100644 index 000000000..a67279778 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/check_tweaks_method.dart @@ -0,0 +1,49 @@ +part of 'methods.dart'; + +class ElectrumWorkerCheckTweaksRequest implements ElectrumWorkerRequest { + ElectrumWorkerCheckTweaksRequest({this.id}); + + final int? id; + + @override + final String method = ElectrumWorkerMethods.checkTweaks.method; + + @override + factory ElectrumWorkerCheckTweaksRequest.fromJson(Map json) { + return ElectrumWorkerCheckTweaksRequest(id: json['id'] as int?); + } + + @override + Map toJson() { + return {'method': method, 'id': id}; + } +} + +class ElectrumWorkerCheckTweaksError extends ElectrumWorkerErrorResponse { + ElectrumWorkerCheckTweaksError({required super.error, super.id}) : super(); + + @override + final String method = ElectrumWorkerMethods.checkTweaks.method; +} + +class ElectrumWorkerCheckTweaksResponse extends ElectrumWorkerResponse { + ElectrumWorkerCheckTweaksResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumWorkerMethods.checkTweaks.method); + + @override + String resultJson(result) { + return result.toString(); + } + + @override + factory ElectrumWorkerCheckTweaksResponse.fromJson(Map json) { + return ElectrumWorkerCheckTweaksResponse( + result: json['result'] == "true", + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart index be81e5346..1892e2cb7 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart @@ -37,7 +37,7 @@ class ElectrumWorkerGetFeesError extends ElectrumWorkerErrorResponse { } class ElectrumWorkerGetFeesResponse - extends ElectrumWorkerResponse> { + extends ElectrumWorkerResponse> { ElectrumWorkerGetFeesResponse({ required super.result, super.error, @@ -46,13 +46,15 @@ class ElectrumWorkerGetFeesResponse @override Map resultJson(result) { - return result.toJson(); + return result?.toJson() ?? {}; } @override factory ElectrumWorkerGetFeesResponse.fromJson(Map json) { return ElectrumWorkerGetFeesResponse( - result: deserializeTransactionPriorities(json['result'] as Map), + result: json['result'] == null + ? null + : deserializeTransactionPriorities(json['result'] as Map), error: json['error'] as String?, id: json['id'] as int?, ); diff --git a/cw_bitcoin/lib/electrum_worker/methods/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart index 295522d39..8f23d1d6a 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/methods.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -20,3 +20,5 @@ part 'list_unspent.dart'; part 'tweaks_subscribe.dart'; part 'get_fees.dart'; part 'version.dart'; +part 'check_tweaks_method.dart'; +part 'stop_scanning.dart'; diff --git a/cw_bitcoin/lib/electrum_worker/methods/stop_scanning.dart b/cw_bitcoin/lib/electrum_worker/methods/stop_scanning.dart new file mode 100644 index 000000000..a84a171b5 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/stop_scanning.dart @@ -0,0 +1,49 @@ +part of 'methods.dart'; + +class ElectrumWorkerStopScanningRequest implements ElectrumWorkerRequest { + ElectrumWorkerStopScanningRequest({this.id}); + + final int? id; + + @override + final String method = ElectrumWorkerMethods.stopScanning.method; + + @override + factory ElectrumWorkerStopScanningRequest.fromJson(Map json) { + return ElectrumWorkerStopScanningRequest(id: json['id'] as int?); + } + + @override + Map toJson() { + return {'method': method, 'id': id}; + } +} + +class ElectrumWorkerStopScanningError extends ElectrumWorkerErrorResponse { + ElectrumWorkerStopScanningError({required super.error, super.id}) : super(); + + @override + final String method = ElectrumWorkerMethods.stopScanning.method; +} + +class ElectrumWorkerStopScanningResponse extends ElectrumWorkerResponse { + ElectrumWorkerStopScanningResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumWorkerMethods.stopScanning.method); + + @override + String resultJson(result) { + return result.toString(); + } + + @override + factory ElectrumWorkerStopScanningResponse.fromJson(Map json) { + return ElectrumWorkerStopScanningResponse( + result: json['result'] as bool, + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 2b0b77a89..15451f31f 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -245,6 +245,9 @@ Future getHavenCurrentHeight() async { // Data taken from https://timechaincalendar.com/ const bitcoinDates = { + "2024-11": 868345, + "2024-10": 863584, + "2024-09": 859317, "2024-08": 854889, "2024-07": 850182, "2024-06": 846005, diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index a0be212eb..3eb560ba7 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -422,7 +422,9 @@ class CWBitcoin extends Bitcoin { var bip39SeedBytes; try { bip39SeedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); - } catch (_) {} + } catch (e) { + print("bip39 seed error: $e"); + } if (bip39SeedBytes != null) { for (final addressType in BITCOIN_ADDRESS_TYPES) { diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index 38421c1da..5a1267bb4 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -161,16 +161,21 @@ class AddressCell extends StatelessWidget { if (derivationPath.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 8.0), - child: Flexible( - child: AutoSizeText( - derivationPath, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: isChange ? 10 : 14, - color: textColor, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: AutoSizeText( + derivationPath, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isChange ? 10 : 14, + color: textColor, + ), + ), ), - ), + ], ), ), if (hasBalance || hasReceived) diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 0d5805e52..004690b67 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -167,6 +167,10 @@ class _AddressListState extends State { : backgroundColor, textColor: textColor, onTap: (_) { + if (item.isChange || item.isHidden) { + return; + } + if (widget.onSelect != null) { widget.onSelect!(item.address); return;