diff --git a/cw_bitcoin/.metadata b/cw_bitcoin/.metadata index 4ce247fd6..d25ddea79 100644 --- a/cw_bitcoin/.metadata +++ b/cw_bitcoin/.metadata @@ -1,10 +1,33 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: b1395592de68cc8ac4522094ae59956dd21a91db + revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 channel: stable -project_type: package +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: macos + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: linux + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 89f82a02b..72c928b67 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:typed_data'; class BitcoinAddressRecord { BitcoinAddressRecord(this.address, @@ -24,7 +25,7 @@ class BitcoinAddressRecord { final String address; final bool isHidden; final String? silentAddressLabel; - final String? silentPaymentTweak; + final Uint8List? silentPaymentTweak; final int index; bool get isUsed => _isUsed; @@ -35,6 +36,12 @@ class BitcoinAddressRecord { void setAsUsed() => _isUsed = true; - String toJSON() => - json.encode({'address': address, 'index': index, 'isHidden': isHidden, 'isUsed': isUsed}); + String toJSON() => json.encode({ + 'address': address, + 'index': index, + 'isHidden': isHidden, + 'isUsed': isUsed, + 'silentAddressLabel': silentAddressLabel, + 'silentPaymentTweak': silentPaymentTweak + }); } diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 00b3a02b6..fd5ed325d 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_core/unspent_transaction_output.dart'; @@ -12,5 +14,5 @@ class BitcoinUnspent extends Unspent { address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int); final BitcoinAddressRecord bitcoinAddressRecord; - String? silentPaymentTweak; + Uint8List? silentPaymentTweak; } diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 5ca43d085..d7438061c 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -232,14 +232,14 @@ class ElectrumClient { {required String hash, required NetworkType networkType}) async => callWithTimeout( method: 'blockchain.transaction.get', - params: networkType == bitcoin ? [hash, true] : [hash], + params: networkType.bech32 == bitcoin.bech32 ? [hash, true] : [hash], timeout: 10000) .then((dynamic result) { if (result is Map<String, dynamic>) { return result; } - if (networkType == testnet && result is String) { + if (networkType.bech32 == testnet.bech32 && result is String) { return result; } @@ -262,6 +262,7 @@ class ElectrumClient { headers: <String, String>{'Content-Type': 'application/json; charset=utf-8'}, body: transactionRaw) .then((http.Response response) { + print(response.body); if (response.statusCode == 200) { return response.body; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index f1252f044..6d3e1d3a3 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -132,6 +132,8 @@ abstract class ElectrumWalletBase Map<String, BehaviorSubject<Object>?> _scripthashesUpdateSubject; BehaviorSubject<Object>? _chainTipUpdateSubject; bool _isTransactionUpdating; + int _initialSyncHeight = 0; + Future<Isolate>? _isolate; void Function(FlutterErrorDetails)? _onError; @@ -299,33 +301,56 @@ abstract class ElectrumWalletBase List<bitcoin.PrivateKeyInfo> inputPrivKeys = []; List<bitcoin.Outpoint> outpoints = []; inputs.forEach((input) { - inputPrivKeys.add(bitcoin.PrivateKeyInfo( - bitcoin.PrivateKey.fromHex( - bitcoin.getSecp256k1(), - HEX.encode(generateKeyPair( - hd: input.bitcoinAddressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd, - index: input.bitcoinAddressRecord.index, - network: networkType) - .privateKey!)), - false)); + final isSilentPayment = input.bitcoinAddressRecord.silentPaymentTweak != null; + outpoints.add(bitcoin.Outpoint(txid: input.hash, index: input.vout)); - if (input.isP2wpkh) { - final p2wpkh = bitcoin - .P2WPKH( - data: generatePaymentData( - hd: input.bitcoinAddressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd, - index: input.bitcoinAddressRecord.index), + if (isSilentPayment) { + // https://github.com/bitcoin/bips/blob/c55f80c53c98642357712c1839cfdc0551d531c4/bip-0352.mediawiki#user-content-Spending + final d = walletAddresses.silentAddress!.spendPrivkey + .tweakAdd(input.bitcoinAddressRecord.silentPaymentTweak!.bigint)!; + + inputPrivKeys.add(bitcoin.PrivateKeyInfo(d, true)); + + print(["output", d]); + + final p2tr = bitcoin + .P2TR( + data: bitcoin.PaymentData(pubkey: d.publicKey.toCompressedHex().fromHex), network: networkType) .data; - txb.addInput(input.hash, input.vout, null, p2wpkh.output); + print(["output", p2tr.output]); + txb.addInput(input.hash, input.vout, null, p2tr.output); } else { - txb.addInput(input.hash, input.vout); + inputPrivKeys.add(bitcoin.PrivateKeyInfo( + bitcoin.PrivateKey.fromHex( + bitcoin.getSecp256k1(), + generateKeyPair( + hd: input.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: input.bitcoinAddressRecord.index, + network: networkType) + .privateKey! + .hex), + false)); + + if (input.isP2wpkh) { + final p2wpkh = bitcoin + .P2WPKH( + data: generatePaymentData( + hd: input.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: input.bitcoinAddressRecord.index), + network: networkType) + .data; + + txb.addInput(input.hash, input.vout, null, p2wpkh.output); + } else { + txb.addInput(input.hash, input.vout); + } } }); @@ -503,51 +528,85 @@ abstract class ElectrumWalletBase bitcoin.ECPair keyPairFor({required int index}) => generateKeyPair(hd: hd, index: index, network: networkType); + @action @override - Future<void> rescan({required int height}) async { + Future<void> rescan({required int height, int? chainTip, ScanData? scanData}) async { syncStatus = AttemptingSyncStatus(); walletInfo.restoreHeight = height; - ReceivePort receivePort = ReceivePort(); - Isolate.spawn(startRefresh, { - "silentAddress": walletAddresses.silentAddress!.toString(), - "scanPrivateKeyCompressed": walletAddresses.silentAddress!.scanPrivkey.toCompressedHex(), - "spendPubkeyCompressed": walletAddresses.silentAddress!.spendPubkey.toCompressedHex(), - "height": walletInfo.restoreHeight.toString(), - "node": electrumClient.uri.toString(), - "receivePort": receivePort.sendPort, - }); - await for (var unspentScannedCoins in receivePort) { - if (unspentScannedCoins is List<BitcoinUnspent>) { - if (unspentScannedCoins.isNotEmpty) { - print("UNSPENT COIN FOUND!"); - final myNewAddresses = unspentScannedCoins.map((coin) { - return BitcoinAddressRecord( - coin.address, - index: 0, - isHidden: false, - isUsed: true, - silentAddressLabel: null, - silentPaymentTweak: coin.silentPaymentTweak, - ); - }); - walletAddresses.addAddresses(myNewAddresses); - await updateTransactions(); - _subscribeForUpdates(); - await updateUnspent(); - await updateBalance(); + await walletInfo.save(); + + final currentChainTip = chainTip ?? await electrumClient.getCurrentBlockChainTip() ?? 0; + + if (_isolate != null) { + final runningIsolate = await _isolate!; + runningIsolate.kill(priority: Isolate.immediate); + } + + if (currentChainTip <= height) { + syncStatus = SyncedSyncStatus(); + return; + } + + final receivePort = ReceivePort(); + _isolate = Isolate.spawn( + startRefresh, + ScanData( + sendPort: receivePort.sendPort, + silentAddress: walletAddresses.silentAddress!.toString(), + scanPrivkeyCompressed: + walletAddresses.silentAddress!.scanPrivkey.toCompressedHex().fromHex, + spendPubkeyCompressed: + walletAddresses.silentAddress!.spendPubkey.toCompressedHex().fromHex, + networkType: networkType, + height: walletInfo.restoreHeight, + chainTip: currentChainTip, + initialSyncHeight: _initialSyncHeight, + electrumClient: ElectrumClient(), + transactionHistoryIds: transactionHistory.transactions.keys.toList(), + node: electrumClient.uri.toString())); + + await for (var message in receivePort) { + if (message is BitcoinUnspent) { + final myNewUnspent = message; + final hasUnspent = unspentCoins.any((element) { + if (element.address == message.address) { + unspentCoins.remove(element); + unspentCoins.add(myNewUnspent); + return true; + } + return false; + }); + if (!hasUnspent) { + unspentCoins.add(myNewUnspent); } - final currentHeight = await electrumClient.getCurrentBlockChainTip(); - - if (currentHeight != null && currentHeight > walletInfo.restoreHeight) { - walletInfo.restoreHeight = walletInfo.restoreHeight + 1; - await save(); - // recursive, scan next block again until at the current tip - rescan(height: walletInfo.restoreHeight); - } else { - syncStatus = SyncedSyncStatus(); + final myNewAddress = message.bitcoinAddressRecord; + final hasAddress = walletAddresses.addresses.any((element) { + if (element.address == message.address) { + walletAddresses.addresses.remove(element); + walletAddresses.addresses.add(myNewAddress); + return true; + } + return false; + }); + if (!hasAddress) { + walletAddresses.addresses.add(myNewAddress); } + + await save(); + + await updateUnspent(); + await updateBalance(); + await updateTransactions(); + _subscribeForUpdates(); + } + + // check if is a SyncStatus type since "is SyncStatus" doesn't work here + if (message is SyncResponse) { + syncStatus = message.syncStatus; + walletInfo.restoreHeight = message.height; + await walletInfo.save(); } } } @@ -571,7 +630,15 @@ abstract class ElectrumWalletBase return null; } }).whereNotNull()))); - unspentCoins.addAll(unspent.expand((e) => e).toList()); + unspent.expand((e) => e).forEach((newUnspent) { + try { + if (!unspentCoins.any((currentUnspent) => + currentUnspent.address.contains(newUnspent.address) && + currentUnspent.hash.contains(newUnspent.hash))) { + unspentCoins.add(newUnspent); + } + } catch (_) {} + }); if (unspentCoinsInfo.isEmpty) { unspentCoins.forEach((coin) => _addCoinInfo(coin)); @@ -580,8 +647,10 @@ abstract class ElectrumWalletBase if (unspentCoins.isNotEmpty) { unspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values - .where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash)); + final coinInfoList = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.address.contains(coin.address)); if (coinInfoList.isNotEmpty) { final coinInfo = coinInfoList.first; @@ -642,6 +711,10 @@ abstract class ElectrumWalletBase final addressHashes = <String, BitcoinAddressRecord>{}; final normalizedHistories = <Map<String, dynamic>>[]; walletAddresses.addresses.forEach((addressRecord) { + if (addressRecord.address == + "tb1pch9qmsq87wy4my4akd60x2r2yt784zfmfwqeuk7w7g7u45za4ktq9pdnmf") { + print(["during fetch txs", addressRecord.address, addressRecord.silentPaymentTweak]); + } final sh = scriptHash(addressRecord.address, networkType: networkType); addressHashes[sh] = addressRecord; }); @@ -708,6 +781,9 @@ abstract class ElectrumWalletBase await updateUnspent(); await updateBalance(); await updateTransactions(); + final currentHeight = await electrumClient.getCurrentBlockChainTip(); + if (currentHeight != null) walletInfo.restoreHeight = currentHeight; + rescan(height: walletInfo.restoreHeight); } catch (e, s) { print(e.toString()); _onError?.call(FlutterErrorDetails( @@ -718,22 +794,6 @@ abstract class ElectrumWalletBase } }); }); - await _chainTipUpdateSubject?.close(); - _chainTipUpdateSubject = electrumClient.chainTipUpdate(); - _chainTipUpdateSubject?.listen((event) async { - try { - final currentHeight = await electrumClient.getCurrentBlockChainTip(); - if (currentHeight != null) walletInfo.restoreHeight = currentHeight; - rescan(height: walletInfo.restoreHeight); - } catch (e, s) { - print(e.toString()); - _onError?.call(FlutterErrorDetails( - exception: e, - stack: s, - library: this.runtimeType.toString(), - )); - } - }); } Future<ElectrumBalance> _fetchBalances() async { @@ -810,13 +870,16 @@ abstract class ElectrumWalletBase } Future<void> _setInitialHeight() async { - if (walletInfo.isRecovery || walletInfo.restoreHeight != 0) { + if (walletInfo.isRecovery) { return; } - // final currentHeight = 2538835; - final currentHeight = await electrumClient.getCurrentBlockChainTip(); - if (currentHeight != null) walletInfo.restoreHeight = currentHeight; + if (walletInfo.restoreHeight == 0) { + final currentHeight = await electrumClient.getCurrentBlockChainTip(); + if (currentHeight != null) walletInfo.restoreHeight = currentHeight; + } + + _initialSyncHeight = walletInfo.restoreHeight; } } @@ -849,7 +912,7 @@ Future<ElectrumTransactionBundle> getTransactionExpanded( String transactionHex; int? time; int confirmations = 0; - if (networkType == bitcoin.testnet) { + if (networkType.bech32 == bitcoin.testnet.bech32) { transactionHex = verboseTransaction as String; confirmations = 1; } else { @@ -871,132 +934,231 @@ Future<ElectrumTransactionBundle> getTransactionExpanded( return ElectrumTransactionBundle(original, ins: ins, time: time, confirmations: confirmations); } -Future<void> startRefresh(Map<String, dynamic> data) async { - final sendPort = data["receivePort"] as SendPort; - String? checkpointTx = data["checkpoint_tx"] as String?; +class ScanData { + final SendPort sendPort; + final Uint8List scanPrivkeyCompressed; + final Uint8List spendPubkeyCompressed; + final String silentAddress; + final int height; + final String node; + final bitcoin.NetworkType networkType; + final int chainTip; + final int initialSyncHeight; + final ElectrumClient electrumClient; + final List<String> transactionHistoryIds; + + final int? checkpointTxPos; + + ScanData( + {required this.sendPort, + required this.scanPrivkeyCompressed, + required this.spendPubkeyCompressed, + required this.silentAddress, + required this.height, + required this.node, + required this.networkType, + required this.chainTip, + required this.initialSyncHeight, + required this.electrumClient, + required this.transactionHistoryIds, + this.checkpointTxPos}); + + factory ScanData.withCheckpoint(ScanData scanData, int newHeight, int? checkpointTx) { + return ScanData( + sendPort: scanData.sendPort, + scanPrivkeyCompressed: scanData.scanPrivkeyCompressed, + spendPubkeyCompressed: scanData.spendPubkeyCompressed, + silentAddress: scanData.silentAddress, + height: newHeight, + node: scanData.node, + networkType: scanData.networkType, + chainTip: scanData.chainTip, + initialSyncHeight: scanData.initialSyncHeight, + electrumClient: scanData.electrumClient, + transactionHistoryIds: scanData.transactionHistoryIds, + checkpointTxPos: checkpointTx); + } +} + +class SyncResponse { + final int height; + final SyncStatus syncStatus; + + SyncResponse(this.height, this.syncStatus); +} + +Future<void> startRefresh(ScanData scanData) async { + final currentChainTip = scanData.chainTip; + if (scanData.height >= currentChainTip) { + scanData.sendPort.send(SyncResponse(scanData.height, SyncedSyncStatus())); + return; + } + + var checkpointTxPos = scanData.checkpointTxPos; + final height = scanData.height; + + print(["HEIGHT:", height]); try { - final height = int.parse(data["height"] as String); - // final height = 2538835; + final track = currentChainTip - height; + final diff = (currentChainTip - scanData.initialSyncHeight) - track; + final ptc = diff <= 0 ? 0.0 : diff / track; + scanData.sendPort.send(SyncResponse(height, SyncingSyncStatus(track, ptc))); - final node = data["node"] as String; - final electrumClient = ElectrumClient(); - await electrumClient.connectToUri(Uri.parse(node)); + final electrumClient = scanData.electrumClient; + if (!electrumClient.isConnected) { + final node = scanData.node; + await electrumClient.connectToUri(Uri.parse(node)); + } - print(["HEIGHT:", height]); - - List<String> txids = []; - int pos = 0; + // tx pos always begin from 1 --> we know pos 0 is coinbase tx + int pos = checkpointTxPos ?? 1; while (true) { try { - var txid = await electrumClient.getTxidFromPos(height: height, pos: pos); - txids.add(txid); + final txid = await electrumClient.getTxidFromPos(height: height, pos: pos); + print(["scanning tx:", txid]); + + // TODO: if already tx already scanned & stored skip + // if (scanData.transactionHistoryIds.contains(txid)) { + // // already scanned tx, continue to next tx + // checkpointTxPos = pos; + // pos++; + // continue; + // } + + List<String> pubkeys = []; + List<bitcoin.Outpoint> outpoints = []; + + try { + final txBundle = await getTransactionExpanded( + hash: txid, + height: height, + electrumClient: electrumClient, + networkType: scanData.networkType); + + bool skip = false; + txBundle.originalTransaction.ins.forEach((input) { + if (input.witness == null) { + skip = true; + return; + } + + if (input.witness!.length != 2) { + skip = true; + return; + } + + final pubkey = input.witness![1].hex; + pubkeys.add(pubkey); + outpoints.add(bitcoin.Outpoint( + txid: HEX.encode(input.hash!.reversed.toList()), index: input.index!)); + }); + + if (skip) { + // skipped tx, save checkpoint in case of issues and continue to next tx + checkpointTxPos = pos; + pos++; + continue; + } + + Map<String, bitcoin.Outpoint> outpointsByP2TRpubkey = {}; + int i = 0; + txBundle.originalTransaction.outs.forEach((output) { + if (bitcoin.classifyOutput(output.script!) != "taproot") { + return; + } + + final address = bitcoin.P2trAddress(program: output.script!.sublist(2).hex).toAddress( + scanData.networkType.bech32 == bitcoin.testnet.bech32 + ? bitcoin.NetworkInfo.TESTNET + : bitcoin.NetworkInfo.BITCOIN); + + print(["verifying taproot address:", address]); + + outpointsByP2TRpubkey[address] = + bitcoin.Outpoint(txid: txid, index: i, value: output.value!); + + i++; + }); + + if (pubkeys.isEmpty || outpoints.isEmpty || outpointsByP2TRpubkey.isEmpty) { + // skipped tx, save checkpoint in case of issues and continue to next tx + checkpointTxPos = pos; + pos++; + continue; + } + + Uint8List sumOfInputPublicKeys = + bitcoin.getSumInputPubKeys(pubkeys).toCompressedHex().fromHex; + final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints); + + final result = bitcoin.scanOutputs( + scanData.scanPrivkeyCompressed, + scanData.spendPubkeyCompressed, + sumOfInputPublicKeys, + outpointHash, + outpointsByP2TRpubkey.keys.toList()); + + if (result.isEmpty) { + // no results tx, save checkpoint in case of issues and continue to next tx + checkpointTxPos = pos; + pos++; + continue; + } + + print("UNSPENT COIN FOUND!"); + + print(result); + result.forEach((key, value) { + final outpoint = outpointsByP2TRpubkey[key]; + + if (outpoint == null) { + return; + } + + // found utxo for tx + scanData.sendPort.send(BitcoinUnspent( + BitcoinAddressRecord( + key, + index: 0, + isHidden: false, + isUsed: true, + silentAddressLabel: null, + silentPaymentTweak: value, + ), + outpoint.txid, + outpoint.value!, + outpoint.index, + silentPaymentTweak: value, + )); + }); + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + } + pos++; - } catch (_) { - // no more txs + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + + // last position, no more txs for the given block height break; } } - if (checkpointTx != null && checkpointTx.isNotEmpty) { - txids = txids.sublist(txids.indexOf(checkpointTx) + 1); + final newHeight = height + 1; + if (newHeight < currentChainTip) { + // recursive, scan next block again until at the current tip + return startRefresh(ScanData.withCheckpoint(scanData, newHeight, 1)); } - List<BitcoinUnspent> unspentCoins = []; - - for (var txid in txids) { - checkpointTx = txid.toString(); - - List<String> pubkeys = []; - List<bitcoin.Outpoint> outpoints = []; - Map<String, bitcoin.Outpoint> outpointsByP2TRpubkey = {}; - - try { - // TODO: networkType - final txBundle = await getTransactionExpanded( - hash: txid, - height: height, - electrumClient: electrumClient, - networkType: bitcoin.testnet); - - bool skip = false; - txBundle.originalTransaction.ins.forEach((input) { - if (input.witness == null) { - skip = true; - return; - } - - if (input.witness!.length != 2) { - skip = true; - return; - } - - final pubkey = input.witness![1].hex; - pubkeys.add(pubkey); - outpoints.add(bitcoin.Outpoint( - txid: HEX.encode(input.hash!.reversed.toList()), index: input.index!)); - }); - - if (skip) continue; - - int i = 0; - txBundle.originalTransaction.outs.forEach((output) { - if (bitcoin.classifyOutput(output.script!) != "taproot") { - return; - } - - print(bitcoin.P2trAddress(program: output.script!.sublist(2).hex) - .toAddress(bitcoin.NetworkInfo.TESTNET)); - outpointsByP2TRpubkey[bitcoin.P2trAddress(program: output.script!.sublist(2).hex) - .toAddress(bitcoin.NetworkInfo.TESTNET)] = - bitcoin.Outpoint(txid: txid, index: i, value: output.value!); - - i++; - }); - - if (pubkeys.isEmpty && outpoints.isEmpty && outpointsByP2TRpubkey.isEmpty) { - continue; - } - - Uint8List sumOfInputPublicKeys = - bitcoin.getSumInputPubKeys(pubkeys).toCompressedHex().fromHex; - final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints); - - final result = bitcoin.scanOutputs( - (data["scanPrivateKeyCompressed"] as String).fromHex, - (data["spendPubkeyCompressed"] as String).fromHex, - sumOfInputPublicKeys, - outpointHash, - outpointsByP2TRpubkey.keys.toList()); - - if (result.isEmpty) { - continue; - } - - result.forEach((key, value) { - final outpoint = outpointsByP2TRpubkey[key]; - - if (outpoint == null) { - return; - } - - unspentCoins.add(BitcoinUnspent( - BitcoinAddressRecord(key, index: 0), - outpoint.txid, - outpoint.value!, - outpoint.index, - silentPaymentTweak: value.hex, - )); - }); - } catch (_) {} - } - - sendPort.send(unspentCoins); + // otherwise, finished scanning + scanData.sendPort.send(SyncResponse(currentChainTip, SyncedSyncStatus())); } catch (e, stacktrace) { print(stacktrace); print(e.toString()); - // timeout, wait 30sec - sendPort.send(Future.delayed(const Duration(seconds: 30), - () => startRefresh({...data, "checkpoint_tx": checkpointTx ?? ""}))); + + startRefresh(ScanData.withCheckpoint(scanData, height, checkpointTxPos)); } } diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 4983967d0..8b4c3210c 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -51,4 +51,6 @@ class ConnectedSyncStatus extends SyncStatus { class LostConnectionSyncStatus extends SyncStatus { @override double progress() => 1.0; -} \ No newline at end of file + @override + String toString() => 'Reconnecting'; +} diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 66094de2b..fbb86fa9f 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -3,7 +3,9 @@ import 'package:cw_core/sync_status.dart'; String syncStatusTitle(SyncStatus syncStatus) { if (syncStatus is SyncingSyncStatus) { - return S.current.Blocks_remaining('${syncStatus.blocksLeft}'); + return syncStatus.blocksLeft == 1 + ? S.current.Block_remaining('${syncStatus.blocksLeft}') + : S.current.Blocks_remaining('${syncStatus.blocksLeft}'); } if (syncStatus is SyncedSyncStatus) { @@ -35,4 +37,4 @@ String syncStatusTitle(SyncStatus syncStatus) { } return ''; -} \ No newline at end of file +} diff --git a/lib/entities/background_tasks.dart b/lib/entities/background_tasks.dart index 5f08a6458..9681c45de 100644 --- a/lib/entities/background_tasks.dart +++ b/lib/entities/background_tasks.dart @@ -194,7 +194,7 @@ void callbackDispatcher() { outpoint.txid, outpoint.value!, outpoint.index, - silentPaymentTweak: value.hex, + silentPaymentTweak: value, )); }); } @@ -455,7 +455,7 @@ Future<List<BitcoinUnspent>> startRefresh(Map<dynamic, dynamic> data) async { outpoint.txid, outpoint.value!, outpoint.index, - silentPaymentTweak: value.hex, + silentPaymentTweak: value, )); }); } diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart index 1a173f62a..1e0a61ad7 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -120,4 +120,4 @@ void showUnspentCoinsAlert(BuildContext context) { buttonText: S.of(context).ok, buttonAction: () => Navigator.of(context).pop()); }); -} \ No newline at end of file +} diff --git a/lib/view_model/rescan_view_model.dart b/lib/view_model/rescan_view_model.dart index c973b7b3f..f6077f1e4 100644 --- a/lib/view_model/rescan_view_model.dart +++ b/lib/view_model/rescan_view_model.dart @@ -9,8 +9,8 @@ enum RescanWalletState { rescaning, none } abstract class RescanViewModelBase with Store { RescanViewModelBase(this._wallet) - : state = RescanWalletState.none, - isButtonEnabled = false; + : state = RescanWalletState.none, + isButtonEnabled = false; final WalletBase _wallet; @@ -23,8 +23,9 @@ abstract class RescanViewModelBase with Store { @action Future<void> rescanCurrentWallet({required int restoreHeight}) async { state = RescanWalletState.rescaning; - await _wallet.rescan(height: restoreHeight); + _wallet.rescan(height: restoreHeight); _wallet.transactionHistory.clear(); state = RescanWalletState.none; } -} \ No newline at end of file +} + diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 1075853fd..af2b8672e 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -735,5 +735,6 @@ "switchToETHWallet": "ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ Ethereum ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ", "use_testnet": "استخدم testnet", "address_and_silent_addresses": "العنوان والعناوين الصامتة", - "silent_addresses": "عناوين صامتة" -} + "silent_addresses": "عناوين صامتة", + "Block_remaining": "كتلة ${status} المتبقية" +} \ No newline at end of file diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index d80f6724e..2dfde29f3 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -731,5 +731,6 @@ "switchToETHWallet": "Моля, преминете към портфейл Ethereum и опитайте отново", "use_testnet": "Използвайте TestNet", "address_and_silent_addresses": "Адрес и мълчаливи адреси", - "silent_addresses": "Безшумни адреси" -} + "silent_addresses": "Безшумни адреси", + "Block_remaining": "${status} останал блок" +} \ No newline at end of file diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index eb1f9d274..950ba472b 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -731,5 +731,6 @@ "switchToETHWallet": "Přejděte na peněženku Ethereum a zkuste to znovu", "use_testnet": "Použijte testNet", "address_and_silent_addresses": "Adresa a tiché adresy", - "silent_addresses": "Tiché adresy" -} + "silent_addresses": "Tiché adresy", + "Block_remaining": "${status} Blok zbývající" +} \ No newline at end of file diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 158c3c1b0..644f09320 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -729,18 +729,16 @@ "awaitDAppProcessing": "Bitte warten Sie, bis die dApp die Verarbeitung abgeschlossen hat.", "copyWalletConnectLink": "Kopieren Sie den WalletConnect-Link von dApp und fügen Sie ihn hier ein", "enterWalletConnectURI": "Geben Sie den WalletConnect-URI ein", - "seed_key": "Seed-Schlüssel", - "enter_seed_phrase": "Geben Sie Ihre Seed-Phrase ein", + "seed_key": "Samenschlüssel", + "enter_seed_phrase": "Geben Sie Ihre Samenphrase ein", "add_contact": "Kontakt hinzufügen", "exchange_provider_unsupported": "${providerName} wird nicht mehr unterstützt!", "domain_looks_up": "Domain-Suchen", "require_for_exchanges_to_external_wallets": "Erforderlich für den Umtausch in externe Wallets", "camera_permission_is_required": "Eine Kameraerlaubnis ist erforderlich.\nBitte aktivieren Sie es in den App-Einstellungen.", "switchToETHWallet": "Bitte wechseln Sie zu einem Ethereum-Wallet und versuchen Sie es erneut", - "seed_key": "Samenschlüssel", - "enter_seed_phrase": "Geben Sie Ihre Samenphrase ein", - "add_contact": "Kontakt hinzufügen", "use_testnet": "TESTNET verwenden", "address_and_silent_addresses": "Adresse und stille Adressen", - "silent_addresses": "Stille Adressen" -} + "silent_addresses": "Stille Adressen", + "Block_remaining": "${status} Block verbleibend" +} \ No newline at end of file diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 1c923f7bc..045a62687 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -28,6 +28,7 @@ "failed_authentication": "Failed authentication. ${state_error}", "wallet_menu": "Menu", "Blocks_remaining": "${status} Blocks Remaining", + "Block_remaining": "${status} Block Remaining", "please_try_to_connect_to_another_node": "Please try to connect to another node", "xmr_hidden": "Hidden", "xmr_available_balance": "Available Balance", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index d8d8a1845..8015d42ed 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -739,5 +739,6 @@ "switchToETHWallet": "Cambie a una billetera Ethereum e inténtelo nuevamente.", "use_testnet": "Use TestNet", "address_and_silent_addresses": "Dirección y direcciones silenciosas", - "silent_addresses": "Direcciones silenciosas" -} + "silent_addresses": "Direcciones silenciosas", + "Block_remaining": "${status} bloque restante" +} \ No newline at end of file diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index bea153d5f..517d916d1 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -739,5 +739,6 @@ "switchToETHWallet": "Veuillez passer à un portefeuille Ethereum et réessayer", "use_testnet": "Utiliser TestNet", "address_and_silent_addresses": "Adresse et adresses silencieuses", - "silent_addresses": "Adresses silencieuses" -} + "silent_addresses": "Adresses silencieuses", + "Block_remaining": "${status} bloc restant" +} \ No newline at end of file diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index d7b88d0eb..c7c48076c 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -717,5 +717,6 @@ "switchToETHWallet": "Da fatan za a canza zuwa walat ɗin Ethereum kuma a sake gwadawa", "use_testnet": "Amfani da gwaji", "address_and_silent_addresses": "Adireshin da adreshin shiru", - "silent_addresses": "Adireshin Shiru" -} + "silent_addresses": "Adireshin Shiru", + "Block_remaining": "${status} toshe ragowar" +} \ No newline at end of file diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index f2b0d6ae0..c0a61bf5b 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -739,5 +739,6 @@ "switchToETHWallet": "कृपया एथेरियम वॉलेट पर स्विच करें और पुनः प्रयास करें", "use_testnet": "टेस्टनेट का उपयोग करें", "address_and_silent_addresses": "पता और मूक पते", - "silent_addresses": "मूक पते" -} + "silent_addresses": "मूक पते", + "Block_remaining": "${status} शेष ब्लॉक" +} \ No newline at end of file diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index a5a9e82ee..ea3ec3bd0 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -737,5 +737,6 @@ "switchToETHWallet": "Prijeđite na Ethereum novčanik i pokušajte ponovno", "use_testnet": "Koristite TestNet", "address_and_silent_addresses": "Adresa i tihe adrese", - "silent_addresses": "Tihe adrese" -} + "silent_addresses": "Tihe adrese", + "Block_remaining": "${status} blok preostaje" +} \ No newline at end of file diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index cb654ab36..d394a8a49 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -727,5 +727,6 @@ "switchToETHWallet": "Silakan beralih ke dompet Ethereum dan coba lagi", "use_testnet": "Gunakan TestNet", "address_and_silent_addresses": "Alamat dan alamat diam", - "silent_addresses": "Alamat diam" -} + "silent_addresses": "Alamat diam", + "Block_remaining": "${status} blok tersisa" +} \ No newline at end of file diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 358b06c60..80d9510b3 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -739,5 +739,6 @@ "switchToETHWallet": "Passa a un portafoglio Ethereum e riprova", "use_testnet": "Usa TestNet", "address_and_silent_addresses": "Indirizzo e indirizzi silenziosi", - "silent_addresses": "Indirizzi silenziosi" -} + "silent_addresses": "Indirizzi silenziosi", + "Block_remaining": "${status} blocco rimanente" +} \ No newline at end of file diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index dccc04294..ff8c62aed 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -739,5 +739,6 @@ "switchToETHWallet": "イーサリアムウォレットに切り替えてもう一度お試しください", "use_testnet": "TestNetを使用します", "address_and_silent_addresses": "住所とサイレントアドレス", - "silent_addresses": "サイレントアドレス" -} + "silent_addresses": "サイレントアドレス", + "Block_remaining": "${status}ブロックの残り" +} \ No newline at end of file diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index f749e49b0..cc8c01610 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -737,5 +737,6 @@ "switchToETHWallet": "이더리움 지갑으로 전환한 후 다시 시도해 주세요.", "use_testnet": "TestNet을 사용하십시오", "address_and_silent_addresses": "주소 및 조용한 주소", - "silent_addresses": "조용한 주소" -} + "silent_addresses": "조용한 주소", + "Block_remaining": "${status} 나머지 블록" +} \ No newline at end of file diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 46cfb98db..906912275 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -737,5 +737,6 @@ "switchToETHWallet": "ကျေးဇူးပြု၍ Ethereum ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ။", "use_testnet": "testnet ကိုသုံးပါ", "address_and_silent_addresses": "လိပ်စာနှင့်အသံတိတ်လိပ်စာများ", - "silent_addresses": "အသံတိတ်လိပ်စာများ" -} + "silent_addresses": "အသံတိတ်လိပ်စာများ", + "Block_remaining": "ကျန်ရှိသော ${status}" +} \ No newline at end of file diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 5ea60e5e9..eb3ff69ac 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -739,5 +739,6 @@ "switchToETHWallet": "Schakel over naar een Ethereum-portemonnee en probeer het opnieuw", "use_testnet": "Gebruik testnet", "address_and_silent_addresses": "Adres en stille adressen", - "silent_addresses": "Stille adressen" -} + "silent_addresses": "Stille adressen", + "Block_remaining": "${status} blok resterend" +} \ No newline at end of file diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 21255c28f..306782f61 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -739,5 +739,6 @@ "switchToETHWallet": "Przejdź na portfel Ethereum i spróbuj ponownie", "use_testnet": "Użyj testne", "address_and_silent_addresses": "Adres i ciche adresy", - "silent_addresses": "Ciche adresy" -} + "silent_addresses": "Ciche adresy", + "Block_remaining": "${status} Block pozostały" +} \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 3184a0c6d..66f5bcf9e 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -738,5 +738,6 @@ "switchToETHWallet": "Mude para uma carteira Ethereum e tente novamente", "use_testnet": "Use testNet", "address_and_silent_addresses": "Endereço e endereços silenciosos", - "silent_addresses": "Endereços silenciosos" -} + "silent_addresses": "Endereços silenciosos", + "Block_remaining": "${status} bloco restante" +} \ No newline at end of file diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index c07e78a7b..8aa17a1f7 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -739,5 +739,6 @@ "switchToETHWallet": "Пожалуйста, переключитесь на кошелек Ethereum и повторите попытку.", "use_testnet": "Используйте Testnet", "address_and_silent_addresses": "Адрес и молчаливые адреса", - "silent_addresses": "Молчаливые адреса" -} + "silent_addresses": "Молчаливые адреса", + "Block_remaining": "${status} оставшееся блок" +} \ No newline at end of file diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 94625d50b..bc236debb 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -737,5 +737,6 @@ "switchToETHWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงิน Ethereum แล้วลองอีกครั้ง", "use_testnet": "ใช้ testnet", "address_and_silent_addresses": "ที่อยู่และที่อยู่เงียบ", - "silent_addresses": "ที่อยู่เงียบ" -} + "silent_addresses": "ที่อยู่เงียบ", + "Block_remaining": "${status} เหลือบล็อกที่เหลืออยู่" +} \ No newline at end of file diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 94bb189d0..1101db8e1 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -734,5 +734,6 @@ "switchToETHWallet": "Mangyaring lumipat sa isang Ethereum wallet at subukang muli", "use_testnet": "Gumamit ng testnet", "address_and_silent_addresses": "Address at tahimik na mga address", - "silent_addresses": "Tahimik na mga address" + "silent_addresses": "Tahimik na mga address", + "Block_remaining": "${status} I -block ang natitira" } \ No newline at end of file diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 2e85d9749..d679b49e1 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -737,5 +737,6 @@ "switchToETHWallet": "Lütfen bir Ethereum cüzdanına geçin ve tekrar deneyin", "use_testnet": "TestNet kullanın", "address_and_silent_addresses": "Adres ve sessiz adresler", - "silent_addresses": "Sessiz adresler" -} + "silent_addresses": "Sessiz adresler", + "Block_remaining": "${status} blok kalan blok" +} \ No newline at end of file diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index eb78fd250..a9472e8ac 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -739,5 +739,6 @@ "switchToETHWallet": "Перейдіть на гаманець Ethereum і повторіть спробу", "use_testnet": "Використовуйте тестову мережу", "address_and_silent_addresses": "Адреса та мовчазні адреси", - "silent_addresses": "Мовчазні адреси" -} + "silent_addresses": "Мовчазні адреси", + "Block_remaining": "${status} блок залишився" +} \ No newline at end of file diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index f972a44f9..1df4ed670 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -731,5 +731,6 @@ "switchToETHWallet": "۔ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ Ethereum ﻡﺮﮐ ﮦﺍﺮﺑ", "use_testnet": "ٹیسٹ نیٹ استعمال کریں", "address_and_silent_addresses": "پتہ اور خاموش پتے", - "silent_addresses": "خاموش پتے" + "silent_addresses": "خاموش پتے", + "Block_remaining": "${status} باقی بلاک" } diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 6de0c87b4..8bc56c74b 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -733,5 +733,6 @@ "switchToETHWallet": "Jọwọ yipada si apamọwọ Ethereum ki o tun gbiyanju lẹẹkansi", "use_testnet": "Lo tele", "address_and_silent_addresses": "Adirẹsi ati awọn adirẹsi ipalọlọ", - "silent_addresses": "Awọn adirẹsi ipalọlọ" -} + "silent_addresses": "Awọn adirẹsi ipalọlọ", + "Block_remaining": "${status} Bdund díẹ" +} \ No newline at end of file diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index eb6a9ce28..cc42d2e2c 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -738,5 +738,6 @@ "switchToETHWallet": "请切换到以太坊钱包并重试", "use_testnet": "使用TestNet", "address_and_silent_addresses": "地址和无声地址", - "silent_addresses": "无声地址" -} + "silent_addresses": "无声地址", + "Block_remaining": "${status}块剩余" +} \ No newline at end of file