diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 2d1722213..8f924eacc 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -37,6 +37,7 @@ import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:http/http.dart' as http; @@ -359,7 +360,7 @@ abstract class ElectrumWalletBase } syncStatus = message.syncStatus; - walletInfo.restoreHeight = message.height; + await walletInfo.updateRestoreHeight(message.height); } } } @@ -381,6 +382,8 @@ abstract class ElectrumWalletBase await updateBalance(); await updateFeeRates(); + + syncStatus = SyncedSyncStatus(); } catch (e, stacktrace) { print(stacktrace); print(e.toString()); @@ -1141,7 +1144,8 @@ abstract class ElectrumWalletBase coin.isFrozen = coinInfo.isFrozen; coin.isSending = coinInfo.isSending; coin.note = coinInfo.note; - coin.bitcoinAddressRecord.balance += coinInfo.value; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; } else { _addCoinInfo(coin); } @@ -1172,7 +1176,8 @@ abstract class ElectrumWalletBase coin.isFrozen = coinInfo.isFrozen; coin.isSending = coinInfo.isSending; coin.note = coinInfo.note; - coin.bitcoinAddressRecord.balance += coinInfo.value; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; } else { _addCoinInfo(coin); } @@ -1588,7 +1593,6 @@ abstract class ElectrumWalletBase Future updateTransactions() async { try { if (_isTransactionUpdating) { - _isTransactionUpdating = false; return; } @@ -1734,7 +1738,7 @@ abstract class ElectrumWalletBase _currentChainTip = height; if (_currentChainTip != null && _currentChainTip! > 0 && walletInfo.restoreHeight == 0) { - walletInfo.restoreHeight = _currentChainTip!; + await walletInfo.updateRestoreHeight(_currentChainTip!); } }); } @@ -1818,202 +1822,156 @@ class SyncResponse { } Future startRefresh(ScanData scanData) async { - Future getElectrumConnection() async { - final electrumClient = scanData.electrumClient; - if (!electrumClient.isConnected) { - final node = scanData.node; - await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); - } - return electrumClient; - } - - var lastKnownBlockHeight = 0; - var initialSyncHeight = 0; - - var syncHeight = scanData.height; - var currentChainTip = scanData.chainTip; - - if (syncHeight <= 0) { - syncHeight = currentChainTip; - } - - if (initialSyncHeight <= 0) { - initialSyncHeight = syncHeight; - } - - if (lastKnownBlockHeight == syncHeight) { - scanData.sendPort.send(SyncResponse(currentChainTip, SyncedSyncStatus())); - return; - } + int syncHeight = scanData.height; + int initialSyncHeight = syncHeight; BehaviorSubject? tweaksSubscription = null; - lastKnownBlockHeight = syncHeight; - - SyncingSyncStatus syncingStatus; - if (scanData.isSingleScan) { - syncingStatus = SyncingSyncStatus(1, 0); - } else { - syncingStatus = - SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); - } + final syncingStatus = scanData.isSingleScan + ? SyncingSyncStatus(1, 0) + : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + // Initial status UI update, send how many blocks left to scan scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); - if (syncingStatus.blocksLeft <= 0 || (scanData.isSingleScan && scanData.height != syncHeight)) { - scanData.sendPort.send(SyncResponse(scanData.chainTip, SyncedSyncStatus())); - return; - } + final electrumClient = scanData.electrumClient; + await electrumClient.connectToUri(scanData.node.uri, useSSL: scanData.node.useSSL); - try { - final electrumClient = await getElectrumConnection(); + if (tweaksSubscription == null) { + final count = scanData.isSingleScan ? 1 : TWEAKS_COUNT; + final receiver = Receiver( + scanData.silentAddress.b_scan.toHex(), + scanData.silentAddress.B_spend.toHex(), + scanData.network == BitcoinNetwork.testnet, + scanData.labelIndexes, + scanData.labelIndexes.length, + ); - if (tweaksSubscription == null) { - final count = scanData.isSingleScan ? 1 : TWEAKS_COUNT; + tweaksSubscription = await electrumClient.tweaksSubscribe(height: syncHeight, count: count); + tweaksSubscription?.listen((t) async { + final tweaks = t as Map; + + if (tweaks["message"] != null) { + // re-subscribe to continue receiving messages + electrumClient.tweaksSubscribe(height: syncHeight, count: count); + return; + } + + final blockHeight = tweaks.keys.first; + final tweakHeight = int.parse(blockHeight); try { - tweaksSubscription = await electrumClient.tweaksSubscribe(height: syncHeight, count: count); + final blockTweaks = tweaks[blockHeight] as Map; - tweaksSubscription?.listen((t) async { - final tweaks = t as Map; - - if (tweaks["message"] != null && !scanData.isSingleScan) { - // re-subscribe to continue receiving messages - electrumClient.tweaksSubscribe(height: syncHeight, count: count); - return; - } - - final blockHeight = tweaks.keys.first; - final tweakHeight = int.parse(blockHeight); + for (var j = 0; j < blockTweaks.keys.length; j++) { + final txid = blockTweaks.keys.elementAt(j); + final details = blockTweaks[txid] as Map; + final outputPubkeys = (details["output_pubkeys"] as Map); + final tweak = details["tweak"].toString(); try { - final blockTweaks = tweaks[blockHeight] as Map; + // scanOutputs called from rust here + final addToWallet = scanOutputs( + outputPubkeys.values.toList(), + tweak, + receiver, + ); - for (var j = 0; j < blockTweaks.keys.length; j++) { - final txid = blockTweaks.keys.elementAt(j); - final details = blockTweaks[txid] as Map; - final outputPubkeys = (details["output_pubkeys"] as Map); - final tweak = details["tweak"].toString(); + if (addToWallet.isEmpty) { + // no results tx, continue to next tx + continue; + } - try { - // scanOutputs called from rust here - final addToWallet = scanOutputs( - outputPubkeys.values.map((o) => o[0].toString()).toList(), - tweak, - Receiver( - scanData.silentAddress.b_scan.toHex(), - scanData.silentAddress.B_spend.toHex(), - scanData.network == BitcoinNetwork.testnet, - scanData.labelIndexes, - scanData.labelIndexes.length, - ), - ); + // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) + final txInfo = ElectrumTransactionInfo( + WalletType.bitcoin, + id: txid, + height: tweakHeight, + amount: 0, + fee: 0, + direction: TransactionDirection.incoming, + isPending: false, + date: scanData.network == BitcoinNetwork.mainnet + ? getDateByBitcoinHeight(tweakHeight) + : DateTime.now(), + confirmations: scanData.chainTip - tweakHeight + 1, + unspents: [], + ); - if (addToWallet.isEmpty) { - // no results tx, continue to next tx - continue; - } + addToWallet.forEach((label, value) { + (value as Map).forEach((output, tweak) { + final t_k = tweak.toString(); - // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) - final txInfo = ElectrumTransactionInfo( - WalletType.bitcoin, - id: txid, - height: tweakHeight, - amount: 0, - fee: 0, - direction: TransactionDirection.incoming, - isPending: false, - date: DateTime.now(), - confirmations: scanData.chainTip - tweakHeight + 1, - unspents: [], - ); + final receivingOutputAddress = ECPublic.fromHex(output) + .toTaprootAddress(tweak: false) + .toAddress(scanData.network); - addToWallet.forEach((label, value) { - (value as Map).forEach((output, tweak) { - final t_k = tweak.toString(); - - final receivingOutputAddress = ECPublic.fromHex(output) - .toTaprootAddress(tweak: false) - .toAddress(scanData.network); - - final receivedAddressRecord = BitcoinSilentPaymentAddressRecord( - receivingOutputAddress, - index: 0, - isHidden: false, - isUsed: true, - network: scanData.network, - silentPaymentTweak: t_k, - type: SegwitAddresType.p2tr, - txCount: 1, - ); - - int? amount; - int? pos; - outputPubkeys.entries.firstWhere((k) { - final isMatchingOutput = k.value[0] == output; - if (isMatchingOutput) { - amount = int.parse(k.value[1].toString()); - pos = int.parse(k.key.toString()); - return true; - } - return false; - }); - - final unspent = BitcoinSilentPaymentsUnspent( - receivedAddressRecord, - txid, - amount!, - pos!, - silentPaymentTweak: t_k, - silentPaymentLabel: label == "None" ? null : label, - ); - - txInfo.unspents!.add(unspent); - txInfo.amount += unspent.value; - }); + int? amount; + int? pos; + outputPubkeys.entries.firstWhere((k) { + final isMatchingOutput = k.value[0] == output; + if (isMatchingOutput) { + amount = int.parse(k.value[1].toString()); + pos = int.parse(k.key.toString()); + return true; + } + return false; }); - scanData.sendPort.send({txInfo.id: txInfo}); - } catch (_) {} - } + final receivedAddressRecord = BitcoinSilentPaymentAddressRecord( + receivingOutputAddress, + index: 0, + isHidden: false, + isUsed: true, + network: scanData.network, + silentPaymentTweak: t_k, + type: SegwitAddresType.p2tr, + txCount: 1, + balance: amount!, + ); + + final unspent = BitcoinSilentPaymentsUnspent( + receivedAddressRecord, + txid, + amount!, + pos!, + silentPaymentTweak: t_k, + silentPaymentLabel: label == "None" ? null : label, + ); + + txInfo.unspents!.add(unspent); + txInfo.amount += unspent.value; + }); + }); + + scanData.sendPort.send({txInfo.id: txInfo}); } catch (_) {} - - syncHeight = tweakHeight; - scanData.sendPort.send( - SyncResponse( - syncHeight, - SyncingSyncStatus.fromHeightValues( - currentChainTip, - initialSyncHeight, - syncHeight, - ), - ), - ); - - if (int.parse(blockHeight) >= scanData.chainTip || scanData.isSingleScan) { - scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); - await tweaksSubscription!.close(); - } - }); - } catch (e) { - if (e is RequestFailedTimeoutException) { - return scanData.sendPort.send( - SyncResponse(syncHeight, TimedOutSyncStatus()), - ); } - } - } + } catch (_) {} - if (tweaksSubscription == null) { - return scanData.sendPort.send( - SyncResponse(syncHeight, UnsupportedSyncStatus()), + syncHeight = tweakHeight; + scanData.sendPort.send( + SyncResponse( + syncHeight, + SyncingSyncStatus.fromHeightValues( + scanData.chainTip, + initialSyncHeight, + syncHeight, + ), + ), ); - } - } catch (e, stacktrace) { - print(stacktrace); - print(e.toString()); - scanData.sendPort.send(SyncResponse(syncHeight, NotConnectedSyncStatus())); + if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { + scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); + await tweaksSubscription!.close(); + } + }); + } + + if (tweaksSubscription == null) { + return scanData.sendPort.send( + SyncResponse(syncHeight, UnsupportedSyncStatus()), + ); } } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 85c7bd7f7..fe2d0c2af 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -794,7 +794,7 @@ packages: description: path: "." ref: master - resolved-ref: a6b14bcc37ec16f56931e48afa8a8f8e6939431d + resolved-ref: "4977a0d31fc8614d27193b07d92c5992d163131e" url: "https://github.com/rafael-xmr/sp_scanner" source: git version: "0.0.1" diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 11bc370c5..3d23d24af 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -245,6 +245,7 @@ Future getHavenCurrentHeight() async { // Data taken from https://timechaincalendar.com/ const bitcoinDates = { + "2024-05": 841590, "2024-04": 837182, "2024-03": 832623, "2024-02": 828319, @@ -265,7 +266,9 @@ const bitcoinDates = { int getBitcoinHeightByDate({required DateTime date}) { String dateKey = '${date.year}-${date.month.toString().padLeft(2, '0')}'; - int startBlock = bitcoinDates[dateKey] ?? bitcoinDates.values.last; + final closestKey = bitcoinDates.keys + .firstWhere((key) => formatMapKey(key).isBefore(date), orElse: () => bitcoinDates.keys.last); + int startBlock = bitcoinDates[dateKey] ?? bitcoinDates[closestKey]!; DateTime startOfMonth = DateTime(date.year, date.month); int daysDifference = date.difference(startOfMonth).inDays; @@ -275,3 +278,9 @@ int getBitcoinHeightByDate({required DateTime date}) { return startBlock + estimatedBlocksSinceStartOfMonth; } + +DateTime getDateByBitcoinHeight(int height) { + final date = bitcoinDates.entries + .lastWhere((entry) => entry.value >= height, orElse: () => bitcoinDates.entries.last); + return formatMapKey(date.key); +} diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 57cdad81b..ff0c011bb 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -66,21 +66,21 @@ class DerivationInfo extends HiveObject { @HiveType(typeId: WalletInfo.typeId) class WalletInfo extends HiveObject { WalletInfo( - this.id, - this.name, - this.type, - this.isRecovery, - this.restoreHeight, - this.timestamp, - this.dirPath, - this.path, - this.address, - this.yatEid, - this.yatLastUsedAddressRaw, - this.showIntroCakePayCard, - this.derivationInfo, - this.hardwareWalletType, - ): _yatLastUsedAddressController = StreamController.broadcast(); + this.id, + this.name, + this.type, + this.isRecovery, + this.restoreHeight, + this.timestamp, + this.dirPath, + this.path, + this.address, + this.yatEid, + this.yatLastUsedAddressRaw, + this.showIntroCakePayCard, + this.derivationInfo, + this.hardwareWalletType, + ) : _yatLastUsedAddressController = StreamController.broadcast(); factory WalletInfo.external({ required String id, @@ -207,4 +207,9 @@ class WalletInfo extends HiveObject { Stream get yatLastUsedAddressStream => _yatLastUsedAddressController.stream; StreamController _yatLastUsedAddressController; + + Future updateRestoreHeight(int height) async { + restoreHeight = height; + await save(); + } } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 44a1fdd46..3c9670f11 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -36,7 +36,6 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'rpc.nano.to'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; const solanaDefaultNodeUri = 'rpc.ankr.com'; -const tronDefaultNodeUri = 'api.trongrid.io'; const tronDefaultNodeUri = 'tron-rpc.publicnode.com:443'; const newCakeWalletBitcoinUri = '198.58.111.154:50001'; diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index 9a13db597..a52d4edf5 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -12,28 +12,27 @@ import 'package:wakelock_plus/wakelock_plus.dart'; ReactionDisposer? _onWalletSyncStatusChangeReaction; void startWalletSyncStatusChangeReaction( - WalletBase, - TransactionInfo> wallet, + WalletBase, TransactionInfo> wallet, FiatConversionStore fiatConversionStore) { _onWalletSyncStatusChangeReaction?.reaction.dispose(); - _onWalletSyncStatusChangeReaction = - reaction((_) => wallet.syncStatus, (SyncStatus status) async { - try { - if (status is ConnectedSyncStatus) { - await wallet.startSync(); + _onWalletSyncStatusChangeReaction = reaction((_) => wallet.syncStatus, (SyncStatus status) async { + if (!(status is SyncingSyncStatus) || wallet.type != WalletType.bitcoin) + try { + if (status is ConnectedSyncStatus) { + await wallet.startSync(); - if (wallet.type == WalletType.haven) { - await updateHavenRate(fiatConversionStore); + if (wallet.type == WalletType.haven) { + await updateHavenRate(fiatConversionStore); + } } + if (status is SyncingSyncStatus) { + await WakelockPlus.enable(); + } + if (status is SyncedSyncStatus || status is FailedSyncStatus) { + await WakelockPlus.disable(); + } + } catch (e) { + print(e.toString()); } - if (status is SyncingSyncStatus) { - await WakelockPlus.enable(); - } - if (status is SyncedSyncStatus || status is FailedSyncStatus) { - await WakelockPlus.disable(); - } - } catch(e) { - print(e.toString()); - } }); } diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index f6c0450e6..5e96effab 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -252,6 +252,7 @@ class CryptoBalanceWidget extends StatelessWidget { Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: DashBoardRoundedCardWidget( + customBorder: 30, title: S.of(context).silent_payments, subTitle: S.of(context).enable_silent_payments_scanning, hint: Column( diff --git a/lib/src/widgets/dashboard_card_widget.dart b/lib/src/widgets/dashboard_card_widget.dart index b0831c7cf..5a8ca14a4 100644 --- a/lib/src/widgets/dashboard_card_widget.dart +++ b/lib/src/widgets/dashboard_card_widget.dart @@ -13,6 +13,7 @@ class DashBoardRoundedCardWidget extends StatelessWidget { this.svgPicture, this.icon, this.onClose, + this.customBorder, }); final VoidCallback onTap; @@ -22,6 +23,7 @@ class DashBoardRoundedCardWidget extends StatelessWidget { final Widget? hint; final SvgPicture? svgPicture; final Icon? icon; + final double? customBorder; @override Widget build(BuildContext context) { @@ -37,7 +39,7 @@ class DashBoardRoundedCardWidget extends StatelessWidget { width: double.infinity, decoration: BoxDecoration( color: Theme.of(context).extension()!.syncedBackgroundColor, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(customBorder ?? 20), border: Border.all( color: Theme.of(context).extension()!.cardBorderColor, ), diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 635d43d53..f9c3dd922 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -218,7 +218,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title; @computed - bool get isReadyForSend => wallet.syncStatus is SyncedSyncStatus; + bool get isReadyForSend => + wallet.syncStatus is SyncedSyncStatus || + // If silent payments scanning, can still send payments + (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus); @computed List