From c9a50233c17ba805d957d484a82682d30a8f60c1 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 5 Nov 2024 12:49:07 -0300 Subject: [PATCH] feat: unspents and tweaks subscribe method --- cw_bitcoin/lib/address_from_output.dart | 23 - cw_bitcoin/lib/bitcoin_address_record.dart | 6 +- cw_bitcoin/lib/bitcoin_unspent.dart | 4 +- cw_bitcoin/lib/bitcoin_wallet.dart | 706 +++++------------- cw_bitcoin/lib/bitcoin_wallet_service.dart | 2 +- cw_bitcoin/lib/electrum_transaction_info.dart | 38 +- cw_bitcoin/lib/electrum_wallet.dart | 470 ++++++------ cw_bitcoin/lib/electrum_wallet_addresses.dart | 9 +- .../lib/electrum_worker/electrum_worker.dart | 513 +++++++++++-- .../electrum_worker_methods.dart | 2 + .../electrum_worker_params.dart | 18 +- .../electrum_worker/methods/broadcast.dart | 56 ++ .../electrum_worker/methods/connection.dart | 35 +- .../electrum_worker/methods/get_balance.dart | 17 +- .../electrum_worker/methods/get_history.dart | 16 +- .../methods/get_tx_expanded.dart | 63 ++ .../methods/headers_subscribe.dart | 20 +- .../electrum_worker/methods/list_unspent.dart | 60 ++ .../methods/list_unspents.dart | 53 -- .../lib/electrum_worker/methods/methods.dart | 7 +- .../methods/scripthashes_subscribe.dart | 20 +- .../methods/tweaks_subscribe.dart | 157 ++++ cw_bitcoin/lib/litecoin_wallet.dart | 31 +- .../lib/pending_bitcoin_transaction.dart | 62 +- cw_core/lib/sync_status.dart | 55 ++ 25 files changed, 1438 insertions(+), 1005 deletions(-) delete mode 100644 cw_bitcoin/lib/address_from_output.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/broadcast.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart delete mode 100644 cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart diff --git a/cw_bitcoin/lib/address_from_output.dart b/cw_bitcoin/lib/address_from_output.dart deleted file mode 100644 index 73bc101c4..000000000 --- a/cw_bitcoin/lib/address_from_output.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:bitcoin_base/bitcoin_base.dart'; - -String addressFromOutputScript(Script script, BasedUtxoNetwork network) { - try { - switch (script.getAddressType()) { - case P2pkhAddressType.p2pkh: - return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network); - case P2shAddressType.p2pkInP2sh: - return P2shAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2wpkh: - return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network); - case P2shAddressType.p2pkhInP2sh: - return P2shAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2wsh: - return P2wshAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2tr: - return P2trAddress.fromScriptPubkey(script: script).toAddress(network); - default: - } - } catch (_) {} - - return ''; -} diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index a15364e6c..d4dd8319f 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -139,7 +139,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { other.index == index && other.derivationInfo == derivationInfo && other.scriptHash == scriptHash && - other.type == type; + other.type == type && + other.derivationType == derivationType; } @override @@ -148,7 +149,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { index.hashCode ^ derivationInfo.hashCode ^ scriptHash.hashCode ^ - type.hashCode; + type.hashCode ^ + derivationType.hashCode; } class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 6dd741b63..93d9c25d5 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -14,8 +14,8 @@ class BitcoinUnspent extends Unspent { BitcoinUnspent( address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()), json['tx_hash'] as String, - json['value'] as int, - json['tx_pos'] as int, + int.parse(json['value'].toString()), + int.parse(json['tx_pos'].toString()), ); Map toJson() { diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 8555fdab8..e695ce67f 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -5,6 +5,7 @@ import 'dart:isolate'; 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_worker/methods/methods.dart'; import 'package:cw_bitcoin/psbt_transaction_builder.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; @@ -36,7 +37,6 @@ part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends ElectrumWallet with Store { - Future? _isolate; StreamSubscription? _receiveStream; BitcoinWalletBase({ @@ -121,18 +121,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (derivation.derivationType == DerivationType.bip39) { seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); - hdWallets[CWBitcoinDerivationType.old] = hdWallets[CWBitcoinDerivationType.bip39]!; - - try { - hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed( - seedBytes, - ElectrumWalletBase.getKeyNetVersion(network ?? BitcoinNetwork.mainnet), - ).derivePath( - _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), - ) as Bip32Slip10Secp256k1; - } catch (e) { - print("bip39 seed error: $e"); - } break; } else { try { @@ -149,17 +137,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - try { - hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed( - seedBytes, - ).derivePath( - _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), - ) as Bip32Slip10Secp256k1; - } catch (_) {} break; } } + hdWallets[CWBitcoinDerivationType.old] = + hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; + return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", @@ -243,18 +227,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (derivation.derivationType == DerivationType.bip39) { seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); - hdWallets[CWBitcoinDerivationType.old] = hdWallets[CWBitcoinDerivationType.bip39]!; - try { - hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed( - seedBytes, - ElectrumWalletBase.getKeyNetVersion(network), - ).derivePath( - _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), - ) as Bip32Slip10Secp256k1; - } catch (e) { - print("bip39 seed error: $e"); - } break; } else { try { @@ -272,15 +245,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - try { - hdWallets[CWBitcoinDerivationType.old] = - Bip32Slip10Secp256k1.fromSeed(seedBytes!).derivePath( - _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), - ) as Bip32Slip10Secp256k1; - } catch (_) {} break; } } + + hdWallets[CWBitcoinDerivationType.old] = + hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; } return BitcoinWallet( @@ -362,7 +332,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final psbtReadyInputs = []; for (final utxo in utxos) { - final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final rawTx = + (await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex(); final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( @@ -421,7 +392,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } else { alwaysScan = false; - _isolate?.then((value) => value.kill(priority: Isolate.immediate)); + // _isolate?.then((value) => value.kill(priority: Isolate.immediate)); // if (rpc!.isConnected) { // syncStatus = SyncedSyncStatus(); @@ -431,41 +402,41 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - @override - @action - Future updateAllUnspents() async { - List updatedUnspentCoins = []; + // @override + // @action + // Future updateAllUnspents() async { + // List updatedUnspentCoins = []; - // Update unspents stored from scanned silent payment transactions - transactionHistory.transactions.values.forEach((tx) { - if (tx.unspents != null) { - updatedUnspentCoins.addAll(tx.unspents!); - } - }); + // // Update unspents stored from scanned silent payment transactions + // transactionHistory.transactions.values.forEach((tx) { + // if (tx.unspents != null) { + // updatedUnspentCoins.addAll(tx.unspents!); + // } + // }); - // 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) { - if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; - }); + // // 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) { + // if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; + // }); - await Future.wait(walletAddresses.allAddresses - .where((element) => element.type != SegwitAddresType.mweb) - .map((address) async { - updatedUnspentCoins.addAll(await fetchUnspent(address)); - })); + // await Future.wait(walletAddresses.allAddresses + // .where((element) => element.type != SegwitAddresType.mweb) + // .map((address) async { + // updatedUnspentCoins.addAll(await fetchUnspent(address)); + // })); - unspentCoins.addAll(updatedUnspentCoins); + // unspentCoins.addAll(updatedUnspentCoins); - if (unspentCoinsInfo.length != updatedUnspentCoins.length) { - unspentCoins.forEach((coin) => addCoinInfo(coin)); - return; - } + // if (unspentCoinsInfo.length != updatedUnspentCoins.length) { + // unspentCoins.forEach((coin) => addCoinInfo(coin)); + // return; + // } - await updateCoins(unspentCoins.toSet()); - await refreshUnspentCoinsInfo(); - } + // await updateCoins(unspentCoins.toSet()); + // await refreshUnspentCoinsInfo(); + // } @override void updateCoin(BitcoinUnspent coin) { @@ -489,17 +460,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - Future _setInitialHeight() async { - final validChainTip = currentChainTip != null && currentChainTip != 0; - if (validChainTip && walletInfo.restoreHeight == 0) { - await walletInfo.updateRestoreHeight(currentChainTip!); - } - } - @action @override Future startSync() async { - await _setInitialHeight(); + await _setInitialScanHeight(); await super.startSync(); @@ -547,16 +511,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @action Future registerSilentPaymentsKey() async { - final registered = await electrumClient.tweaksRegister( - secViewKey: walletAddresses.silentAddress!.b_scan.toHex(), - pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(), - labels: walletAddresses.silentAddresses - .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) - .map((addr) => addr.labelIndex) - .toList(), - ); + // final registered = await electrumClient.tweaksRegister( + // secViewKey: walletAddresses.silentAddress!.b_scan.toHex(), + // pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(), + // labels: walletAddresses.silentAddresses + // .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) + // .map((addr) => addr.labelIndex) + // .toList(), + // ); - print("registered: $registered"); + // print("registered: $registered"); } @action @@ -583,6 +547,103 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ); } + @override + @action + Future handleWorkerResponse(dynamic message) async { + super.handleWorkerResponse(message); + + Map messageJson; + if (message is String) { + messageJson = jsonDecode(message) as Map; + } else { + messageJson = message as Map; + } + final workerMethod = messageJson['method'] as String; + + switch (workerMethod) { + case ElectrumRequestMethods.tweaksSubscribeMethod: + final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson); + onTweaksSyncResponse(response.result); + break; + } + } + + @action + Future onTweaksSyncResponse(TweaksSyncResponse result) async { + if (result.transactions?.isNotEmpty == true) { + for (final map in result.transactions!.entries) { + final txid = map.key; + final tx = map.value; + + if (tx.unspents != null) { + final existingTxInfo = transactionHistory.transactions[txid]; + final txAlreadyExisted = existingTxInfo != null; + + // Updating tx after re-scanned + if (txAlreadyExisted) { + existingTxInfo.amount = tx.amount; + existingTxInfo.confirmations = tx.confirmations; + existingTxInfo.height = tx.height; + + final newUnspents = tx.unspents! + .where((unspent) => !(existingTxInfo.unspents?.any((element) => + element.hash.contains(unspent.hash) && + element.vout == unspent.vout && + element.value == unspent.value) ?? + false)) + .toList(); + + if (newUnspents.isNotEmpty) { + newUnspents.forEach(_updateSilentAddressRecord); + + existingTxInfo.unspents ??= []; + existingTxInfo.unspents!.addAll(newUnspents); + + final newAmount = newUnspents.length > 1 + ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) + : newUnspents[0].value; + + if (existingTxInfo.direction == TransactionDirection.incoming) { + existingTxInfo.amount += newAmount; + } + + // Updates existing TX + transactionHistory.addOne(existingTxInfo); + // Update balance record + balance[currency]!.confirmed += newAmount; + } + } else { + // else: First time seeing this TX after scanning + tx.unspents!.forEach(_updateSilentAddressRecord); + + // Add new TX record + transactionHistory.addOne(tx); + // Update balance record + balance[currency]!.confirmed += tx.amount; + } + + await updateAllUnspents(); + } + } + } + + final newSyncStatus = result.syncStatus; + + if (newSyncStatus != null) { + if (newSyncStatus is UnsupportedSyncStatus) { + nodeSupportsSilentPayments = false; + } + + if (newSyncStatus is SyncingSyncStatus) { + syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc); + } else { + syncStatus = newSyncStatus; + } + + await walletInfo.updateRestoreHeight(result.height!); + } + } + @action Future _setListeners(int height, {bool? doSingleScan}) async { if (currentChainTip == null) { @@ -598,106 +659,23 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { syncStatus = AttemptingScanSyncStatus(); - if (_isolate != null) { - final runningIsolate = await _isolate!; - runningIsolate.kill(priority: Isolate.immediate); - } - - final receivePort = ReceivePort(); - _isolate = Isolate.spawn( - startRefresh, - ScanData( - sendPort: receivePort.sendPort, + workerSendPort!.send( + ElectrumWorkerTweaksSubscribeRequest( + scanData: ScanData( silentAddress: walletAddresses.silentAddress!, network: network, height: height, chainTip: chainTip, transactionHistoryIds: transactionHistory.transactions.keys.toList(), - node: (await getNodeSupportsSilentPayments()) == true - ? ScanNode(node!.uri, node!.useSSL) - : null, labels: walletAddresses.labels, labelIndexes: walletAddresses.silentAddresses .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) .map((addr) => addr.labelIndex) .toList(), isSingleScan: doSingleScan ?? false, - )); - - _receiveStream?.cancel(); - _receiveStream = receivePort.listen((var message) async { - if (message is Map) { - for (final map in message.entries) { - final txid = map.key; - final tx = map.value; - - if (tx.unspents != null) { - final existingTxInfo = transactionHistory.transactions[txid]; - final txAlreadyExisted = existingTxInfo != null; - - // Updating tx after re-scanned - if (txAlreadyExisted) { - existingTxInfo.amount = tx.amount; - existingTxInfo.confirmations = tx.confirmations; - existingTxInfo.height = tx.height; - - final newUnspents = tx.unspents! - .where((unspent) => !(existingTxInfo.unspents?.any((element) => - element.hash.contains(unspent.hash) && - element.vout == unspent.vout && - element.value == unspent.value) ?? - false)) - .toList(); - - if (newUnspents.isNotEmpty) { - newUnspents.forEach(_updateSilentAddressRecord); - - existingTxInfo.unspents ??= []; - existingTxInfo.unspents!.addAll(newUnspents); - - final newAmount = newUnspents.length > 1 - ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) - : newUnspents[0].value; - - if (existingTxInfo.direction == TransactionDirection.incoming) { - existingTxInfo.amount += newAmount; - } - - // Updates existing TX - transactionHistory.addOne(existingTxInfo); - // Update balance record - balance[currency]!.confirmed += newAmount; - } - } else { - // else: First time seeing this TX after scanning - tx.unspents!.forEach(_updateSilentAddressRecord); - - // Add new TX record - transactionHistory.addMany(message); - // Update balance record - balance[currency]!.confirmed += tx.amount; - } - - await updateAllUnspents(); - } - } - } - - if (message is SyncResponse) { - if (message.syncStatus is UnsupportedSyncStatus) { - nodeSupportsSilentPayments = false; - } - - if (message.syncStatus is SyncingSyncStatus) { - var status = message.syncStatus as SyncingSyncStatus; - syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc); - } else { - syncStatus = message.syncStatus; - } - - await walletInfo.updateRestoreHeight(message.height); - } - }); + ), + ).toJson(), + ); } @override @@ -824,15 +802,28 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - // @override - // @action - // void onHeadersResponse(ElectrumHeaderResponse response) { - // super.onHeadersResponse(response); + @override + @action + Future onHeadersResponse(ElectrumHeaderResponse response) async { + super.onHeadersResponse(response); - // if (alwaysScan == true && syncStatus is SyncedSyncStatus) { - // _setListeners(walletInfo.restoreHeight); - // } - // } + _setInitialScanHeight(); + + // New headers received, start scanning + if (alwaysScan == true && syncStatus is SyncedSyncStatus) { + _setListeners(walletInfo.restoreHeight); + } + } + + Future _setInitialScanHeight() async { + final validChainTip = currentChainTip != null && currentChainTip != 0; + if (validChainTip && walletInfo.restoreHeight == 0) { + await walletInfo.updateRestoreHeight(currentChainTip!); + } + } + + static String _hardenedDerivationPath(String derivationPath) => + derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); @override @action @@ -850,355 +841,4 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { super.syncStatusReaction(syncStatus); } } - - static String _hardenedDerivationPath(String derivationPath) => - derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); -} - -Future startRefresh(ScanData scanData) async { - int syncHeight = scanData.height; - int initialSyncHeight = syncHeight; - - final electrumClient = ElectrumApiProvider( - await ElectrumTCPService.connect( - scanData.node?.uri ?? Uri.parse("tcp://198.58.115.71:50001"), - ), - ); - - int getCountPerRequest(int syncHeight) { - if (scanData.isSingleScan) { - return 1; - } - - final amountLeft = scanData.chainTip - syncHeight + 1; - return amountLeft; - } - - final receiver = Receiver( - scanData.silentAddress.b_scan.toHex(), - scanData.silentAddress.B_spend.toHex(), - scanData.network == BitcoinNetwork.testnet, - scanData.labelIndexes, - scanData.labelIndexes.length, - ); - - // Initial status UI update, send how many blocks in total to scan - final initialCount = getCountPerRequest(syncHeight); - scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); - - final listener = await electrumClient.subscribe( - ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), - ); - - Future listenFn(ElectrumTweaksSubscribeResponse response) async { - // success or error msg - final noData = response.message != null; - - if (noData) { - // re-subscribe to continue receiving messages, starting from the next unscanned height - final nextHeight = syncHeight + 1; - final nextCount = getCountPerRequest(nextHeight); - - if (nextCount > 0) { - final nextListener = await electrumClient.subscribe( - ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), - ); - nextListener?.call(listenFn); - } - - return; - } - - // Continuous status UI update, send how many blocks left to scan - final syncingStatus = scanData.isSingleScan - ? SyncingSyncStatus(1, 0) - : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); - scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); - - final tweakHeight = response.block; - - try { - final blockTweaks = response.blockTweaks; - - for (final txid in blockTweaks.keys) { - final tweakData = blockTweaks[txid]; - final outputPubkeys = tweakData!.outputPubkeys; - final tweak = tweakData.tweak; - - try { - // scanOutputs called from rust here - final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver); - - if (addToWallet.isEmpty) { - // no results tx, continue to next tx - continue; - } - - // 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, - isReplaced: false, - date: scanData.network == BitcoinNetwork.mainnet - ? getDateByBitcoinHeight(tweakHeight) - : DateTime.now(), - confirmations: scanData.chainTip - tweakHeight + 1, - unspents: [], - isReceivedSilentPayment: true, - ); - - 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 matchingOutput = outputPubkeys[output]!; - final amount = matchingOutput.amount; - final pos = matchingOutput.vout; - - final receivedAddressRecord = BitcoinReceivedSPAddressRecord( - receivingOutputAddress, - labelIndex: 1, // TODO: get actual index/label - isUsed: true, - spendKey: scanData.silentAddress.b_spend.tweakAdd( - BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)), - ), - txCount: 1, - balance: amount, - ); - - final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos); - - txInfo.unspents!.add(unspent); - txInfo.amount += unspent.value; - }); - }); - - scanData.sendPort.send({txInfo.id: txInfo}); - } catch (e, stacktrace) { - print(stacktrace); - print(e.toString()); - } - } - } catch (e, stacktrace) { - print(stacktrace); - print(e.toString()); - } - - syncHeight = tweakHeight; - - if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { - if (tweakHeight >= scanData.chainTip) - scanData.sendPort.send(SyncResponse( - syncHeight, - SyncedTipSyncStatus(scanData.chainTip), - )); - - if (scanData.isSingleScan) { - scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); - } - } - } - - listener?.call(listenFn); - - // if (tweaksSubscription == null) { - // return scanData.sendPort.send( - // SyncResponse(syncHeight, UnsupportedSyncStatus()), - // ); - // } -} - -Future delegatedScan(ScanData scanData) async { - // int syncHeight = scanData.height; - // int initialSyncHeight = syncHeight; - - // BehaviorSubject? tweaksSubscription = null; - - // final electrumClient = scanData.electrumClient; - // await electrumClient.connectToUri( - // scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"), - // useSSL: scanData.node?.useSSL ?? false, - // ); - - // if (tweaksSubscription == null) { - // scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); - - // tweaksSubscription = await electrumClient.tweaksScan( - // pubSpendKey: scanData.silentAddress.B_spend.toHex(), - // ); - - // Future listenFn(t) async { - // final tweaks = t as Map; - // final msg = tweaks["message"]; - - // // success or error msg - // final noData = msg != null; - // if (noData) { - // return; - // } - - // // Continuous status UI update, send how many blocks left to scan - // final syncingStatus = scanData.isSingleScan - // ? SyncingSyncStatus(1, 0) - // : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); - // scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); - - // final blockHeight = tweaks.keys.first; - // final tweakHeight = int.parse(blockHeight); - - // try { - // final blockTweaks = tweaks[blockHeight] as Map; - - // 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 spendingKey = details["spending_key"].toString(); - - // try { - // // 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, - // isReplaced: false, - // date: scanData.network == BitcoinNetwork.mainnet - // ? getDateByBitcoinHeight(tweakHeight) - // : DateTime.now(), - // confirmations: scanData.chainTip - tweakHeight + 1, - // unspents: [], - // isReceivedSilentPayment: true, - // ); - - // outputPubkeys.forEach((pos, value) { - // final secKey = ECPrivate.fromHex(spendingKey); - // final receivingOutputAddress = - // secKey.getPublic().toTaprootAddress(tweak: false).toAddress(scanData.network); - - // late int amount; - // try { - // amount = int.parse(value[1].toString()); - // } catch (_) { - // return; - // } - - // final receivedAddressRecord = BitcoinReceivedSPAddressRecord( - // receivingOutputAddress, - // labelIndex: 0, - // isUsed: true, - // spendKey: secKey, - // txCount: 1, - // balance: amount, - // ); - - // final unspent = BitcoinUnspent( - // receivedAddressRecord, - // txid, - // amount, - // int.parse(pos.toString()), - // ); - - // txInfo.unspents!.add(unspent); - // txInfo.amount += unspent.value; - // }); - - // scanData.sendPort.send({txInfo.id: txInfo}); - // } catch (_) {} - // } - // } catch (_) {} - - // syncHeight = tweakHeight; - - // if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { - // if (tweakHeight >= scanData.chainTip) - // scanData.sendPort.send(SyncResponse( - // syncHeight, - // SyncedTipSyncStatus(scanData.chainTip), - // )); - - // if (scanData.isSingleScan) { - // scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); - // } - - // await tweaksSubscription!.close(); - // await electrumClient.close(); - // } - // } - - // tweaksSubscription?.listen(listenFn); - // } - - // if (tweaksSubscription == null) { - // return scanData.sendPort.send( - // SyncResponse(syncHeight, UnsupportedSyncStatus()), - // ); - // } -} - -class ScanNode { - final Uri uri; - final bool? useSSL; - - ScanNode(this.uri, this.useSSL); -} - -class ScanData { - final SendPort sendPort; - final SilentPaymentOwner silentAddress; - final int height; - final ScanNode? node; - final BasedUtxoNetwork network; - final int chainTip; - final List transactionHistoryIds; - final Map labels; - final List labelIndexes; - final bool isSingleScan; - - ScanData({ - required this.sendPort, - required this.silentAddress, - required this.height, - required this.node, - required this.network, - required this.chainTip, - required this.transactionHistoryIds, - required this.labels, - required this.labelIndexes, - required this.isSingleScan, - }); - - factory ScanData.fromHeight(ScanData scanData, int newHeight) { - return ScanData( - sendPort: scanData.sendPort, - silentAddress: scanData.silentAddress, - height: newHeight, - node: scanData.node, - network: scanData.network, - chainTip: scanData.chainTip, - transactionHistoryIds: scanData.transactionHistoryIds, - labels: scanData.labels, - labelIndexes: scanData.labelIndexes, - isSingleScan: scanData.isSingleScan, - ); - } -} - -class SyncResponse { - final int height; - final SyncStatus syncStatus; - - SyncResponse(this.height, this.syncStatus); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 941c25265..b310c1db3 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -23,8 +23,8 @@ class BitcoinWalletService extends WalletService< this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, - this.mempoolAPIEnabled, this.isDirect, + this.mempoolAPIEnabled, ); final Box walletInfoSource; diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index ccf9e20d7..f75120531 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/transaction_direction.dart'; @@ -11,13 +10,35 @@ import 'package:cw_core/wallet_type.dart'; import 'package:hex/hex.dart'; class ElectrumTransactionBundle { - ElectrumTransactionBundle(this.originalTransaction, - {required this.ins, required this.confirmations, this.time}); + ElectrumTransactionBundle( + this.originalTransaction, { + required this.ins, + required this.confirmations, + this.time, + }); final BtcTransaction originalTransaction; final List ins; final int? time; final int confirmations; + + Map toJson() { + return { + 'originalTransaction': originalTransaction.toHex(), + 'ins': ins.map((e) => e.toHex()).toList(), + 'confirmations': confirmations, + 'time': time, + }; + } + + static ElectrumTransactionBundle fromJson(Map data) { + return ElectrumTransactionBundle( + BtcTransaction.fromRaw(data['originalTransaction'] as String), + ins: (data['ins'] as List).map((e) => BtcTransaction.fromRaw(e as String)).toList(), + confirmations: data['confirmations'] as int, + time: data['time'] as int?, + ); + } } class ElectrumTransactionInfo extends TransactionInfo { @@ -128,9 +149,11 @@ class ElectrumTransactionInfo extends TransactionInfo { final inputTransaction = bundle.ins[i]; final outTransaction = inputTransaction.outputs[input.txIndex]; inputAmount += outTransaction.amount.toInt(); - if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) { + if (addresses.contains( + BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network))) { direction = TransactionDirection.outgoing; - inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network)); + inputAddresses.add( + BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network)); } } } catch (e) { @@ -144,8 +167,9 @@ class ElectrumTransactionInfo extends TransactionInfo { final receivedAmounts = []; for (final out in bundle.originalTransaction.outputs) { totalOutAmount += out.amount.toInt(); - final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network)); - final address = addressFromOutputScript(out.scriptPubKey, network); + final addressExists = addresses + .contains(BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network)); + final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network); if (address.isNotEmpty) outputAddresses.add(address); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 7986b2cb6..9964751ee 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -6,11 +6,11 @@ import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/electrum_worker/electrum_worker.dart'; import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; -import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; @@ -25,7 +25,7 @@ import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; -// import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -35,13 +35,11 @@ import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; -// import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:mobx/mobx.dart'; -// import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; @@ -52,8 +50,11 @@ abstract class ElectrumWalletBase with Store, WalletKeysFile { ReceivePort? receivePort; SendPort? workerSendPort; - StreamSubscription? _workerSubscription; + StreamSubscription? _workerSubscription; Isolate? _workerIsolate; + final Map _responseCompleters = {}; + final Map _errorCompleters = {}; + int _messageId = 0; ElectrumWalletBase({ required String password, @@ -67,7 +68,6 @@ abstract class ElectrumWalletBase List? seedBytes, this.passphrase, List? initialAddresses, - ElectrumClient? electrumClient, ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, @@ -103,7 +103,6 @@ abstract class ElectrumWalletBase this.isTestnet = !network.isMainnet, this._mnemonic = mnemonic, super(walletInfo) { - this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; transactionHistory = ElectrumTransactionHistory( walletInfo: walletInfo, @@ -116,8 +115,27 @@ abstract class ElectrumWalletBase sharedPrefs.complete(SharedPreferences.getInstance()); } + Future sendWorker(ElectrumWorkerRequest request) { + final messageId = ++_messageId; + + final completer = Completer(); + _responseCompleters[messageId] = completer; + + final json = request.toJson(); + json['id'] = messageId; + workerSendPort!.send(json); + + try { + return completer.future.timeout(Duration(seconds: 5)); + } catch (e) { + _errorCompleters.addAll({messageId: e}); + _responseCompleters.remove(messageId); + rethrow; + } + } + @action - Future _handleWorkerResponse(dynamic message) async { + Future handleWorkerResponse(dynamic message) async { print('Main: received message: $message'); Map messageJson; @@ -149,6 +167,12 @@ abstract class ElectrumWalletBase // return; // } + final responseId = messageJson['id'] as int?; + if (responseId != null && _responseCompleters.containsKey(responseId)) { + _responseCompleters[responseId]!.complete(message); + _responseCompleters.remove(responseId); + } + switch (workerMethod) { case ElectrumWorkerMethods.connectionMethod: final response = ElectrumWorkerConnectionResponse.fromJson(messageJson); @@ -157,7 +181,6 @@ abstract class ElectrumWalletBase case ElectrumRequestMethods.headersSubscribeMethod: final response = ElectrumWorkerHeadersSubscribeResponse.fromJson(messageJson); await onHeadersResponse(response.result); - break; case ElectrumRequestMethods.getBalanceMethod: final response = ElectrumWorkerGetBalanceResponse.fromJson(messageJson); @@ -167,6 +190,10 @@ abstract class ElectrumWalletBase final response = ElectrumWorkerGetHistoryResponse.fromJson(messageJson); onHistoriesResponse(response.result); break; + case ElectrumRequestMethods.listunspentMethod: + final response = ElectrumWorkerListUnspentResponse.fromJson(messageJson); + onUnspentResponse(response.result); + break; } } @@ -219,7 +246,6 @@ abstract class ElectrumWalletBase @observable bool isEnabledAutoGenerateSubaddress; - late ElectrumClient electrumClient; ApiProvider? apiProvider; Box unspentCoinsInfo; @@ -298,6 +324,7 @@ abstract class ElectrumWalletBase bool _chainTipListenerOn = false; bool _isTransactionUpdating; + bool _isInitialSync = true; void Function(FlutterErrorDetails)? _onError; Timer? _autoSaveTimer; @@ -323,16 +350,18 @@ abstract class ElectrumWalletBase syncStatus = SynchronizingSyncStatus(); // INFO: FIRST: Call subscribe for headers, get the initial chainTip update in case it is zero - await subscribeForHeaders(); + await sendWorker(ElectrumWorkerHeadersSubscribeRequest()); // 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 later. await updateTransactions(); - // await updateAllUnspents(); // INFO: THIRD: Start loading the TX history await updateBalance(); - // await subscribeForUpdates(); + // INFO: FOURTH: Finish with unspents + await updateAllUnspents(); + + _isInitialSync = false; // await updateFeeRates(); @@ -377,7 +406,7 @@ abstract class ElectrumWalletBase return false; } - final version = await electrumClient.version(); + // final version = await electrumClient.version(); if (version.isNotEmpty) { final server = version[0]; @@ -416,10 +445,13 @@ abstract class ElectrumWalletBase if (message is SendPort) { workerSendPort = message; workerSendPort!.send( - ElectrumWorkerConnectionRequest(uri: node.uri).toJson(), + ElectrumWorkerConnectionRequest( + uri: node.uri, + network: network, + ).toJson(), ); } else { - _handleWorkerResponse(message); + handleWorkerResponse(message); } }); } catch (e, stacktrace) { @@ -927,11 +959,10 @@ abstract class ElectrumWalletBase return PendingBitcoinTransaction( transaction, type, - electrumClient: electrumClient, + sendWorker: sendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, feeRate: feeRateInt.toString(), - network: network, hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot @@ -1007,11 +1038,10 @@ abstract class ElectrumWalletBase return PendingBitcoinTransaction( transaction, type, - electrumClient: electrumClient, + sendWorker: sendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, feeRate: feeRateInt.toString(), - network: network, hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, hasTaprootInputs: hasTaprootInputs, @@ -1177,7 +1207,9 @@ abstract class ElectrumWalletBase @override Future close({required bool shouldCleanup}) async { try { - await electrumClient.close(); + _workerIsolate!.kill(priority: Isolate.immediate); + await _workerSubscription?.cancel(); + receivePort?.close(); } catch (_) {} _autoSaveTimer?.cancel(); _updateFeeRateTimer?.cancel(); @@ -1185,25 +1217,15 @@ abstract class ElectrumWalletBase @action Future updateAllUnspents() async { - List updatedUnspentCoins = []; - - Set scripthashes = {}; - walletAddresses.allAddresses.forEach((addressRecord) { - scripthashes.add(addressRecord.scriptHash); - }); - - workerSendPort!.send( - ElectrumWorkerGetBalanceRequest(scripthashes: scripthashes).toJson(), + final req = ElectrumWorkerListUnspentRequest( + scripthashes: walletAddresses.allScriptHashes.toList(), ); - await Future.wait(walletAddresses.allAddresses - .where((element) => element.type != SegwitAddresType.mweb) - .map((address) async { - updatedUnspentCoins.addAll(await fetchUnspent(address)); - })); - - await updateCoins(unspentCoins.toSet()); - await refreshUnspentCoinsInfo(); + if (_isInitialSync) { + await sendWorker(req); + } else { + workerSendPort!.send(req.toJson()); + } } @action @@ -1227,46 +1249,38 @@ abstract class ElectrumWalletBase } @action - Future updateCoins(Set newUnspentCoins) async { - if (newUnspentCoins.isEmpty) { - return; - } - newUnspentCoins.forEach(updateCoin); - } + Future onUnspentResponse(Map> unspents) async { + final updatedUnspentCoins = []; - @action - Future updateUnspentsForAddress(BitcoinAddressRecord addressRecord) async { - final newUnspentCoins = (await fetchUnspent(addressRecord)).toSet(); - await updateCoins(newUnspentCoins); + await Future.wait(unspents.entries.map((entry) async { + final unspent = entry.value; + final scriptHash = entry.key; - unspentCoins.addAll(newUnspentCoins); + final addressRecord = walletAddresses.allAddresses.firstWhereOrNull( + (element) => element.scriptHash == scriptHash, + ); - // if (unspentCoinsInfo.length != unspentCoins.length) { - // unspentCoins.forEach(addCoinInfo); - // } + if (addressRecord == null) { + return null; + } - // await refreshUnspentCoinsInfo(); - } - - @action - Future> fetchUnspent(BitcoinAddressRecord address) async { - List> unspents = []; - List updatedUnspentCoins = []; - - unspents = await electrumClient.getListUnspent(address.scriptHash); - - await Future.wait(unspents.map((unspent) async { - try { - final coin = BitcoinUnspent.fromJSON(address, unspent); - // final tx = await fetchTransactionInfo(hash: coin.hash); - coin.isChange = address.isHidden; - // coin.confirmations = tx?.confirmations; + await Future.wait(unspent.map((unspent) async { + final coin = BitcoinUnspent.fromJSON(addressRecord, unspent.toJson()); + coin.isChange = addressRecord.isChange; + final tx = await fetchTransactionInfo(hash: coin.hash); + if (tx != null) { + coin.confirmations = tx.confirmations; + } updatedUnspentCoins.add(coin); - } catch (_) {} + })); })); - return updatedUnspentCoins; + unspentCoins.clear(); + unspentCoins.addAll(updatedUnspentCoins); + unspentCoins.forEach(updateCoin); + + await refreshUnspentCoinsInfo(); } @action @@ -1287,7 +1301,6 @@ abstract class ElectrumWalletBase await unspentCoinsInfo.add(newInfo); } - // TODO: ? Future refreshUnspentCoinsInfo() async { try { final List keys = []; @@ -1313,6 +1326,92 @@ abstract class ElectrumWalletBase } } + @action + Future onHeadersResponse(ElectrumHeaderResponse response) async { + currentChainTip = response.height; + + bool updated = false; + transactionHistory.transactions.values.forEach((tx) { + if (tx.height != null && tx.height! > 0) { + final newConfirmations = currentChainTip! - tx.height! + 1; + + if (tx.confirmations != newConfirmations) { + tx.confirmations = newConfirmations; + tx.isPending = tx.confirmations == 0; + updated = true; + } + } + }); + + if (updated) { + await save(); + } + } + + @action + Future subscribeForHeaders() async { + if (_chainTipListenerOn) return; + + workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson()); + _chainTipListenerOn = true; + } + + @action + Future onHistoriesResponse(List histories) async { + if (histories.isEmpty) { + return; + } + + final firstAddress = histories.first; + final isChange = firstAddress.addressRecord.isChange; + final type = firstAddress.addressRecord.type; + + final totalAddresses = (isChange + ? walletAddresses.receiveAddresses.where((element) => element.type == type).length + : walletAddresses.changeAddresses.where((element) => element.type == type).length); + final gapLimit = (isChange + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + + bool hasUsedAddressesUnderGap = false; + final addressesWithHistory = []; + + for (final addressHistory in histories) { + final txs = addressHistory.txs; + + if (txs.isNotEmpty) { + final address = addressHistory.addressRecord; + addressesWithHistory.add(address); + + hasUsedAddressesUnderGap = + address.index < totalAddresses && (address.index >= totalAddresses - gapLimit); + + for (final tx in txs) { + transactionHistory.addOne(tx); + } + } + } + + if (addressesWithHistory.isNotEmpty) { + walletAddresses.updateAdresses(addressesWithHistory); + } + + if (hasUsedAddressesUnderGap) { + // Discover new addresses for the same address type until the gap limit is respected + final newAddresses = await walletAddresses.discoverAddresses( + isChange: isChange, + derivationType: firstAddress.addressRecord.derivationType, + type: type, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(type), + ); + + if (newAddresses.isNotEmpty) { + // Update the transactions for the new discovered addresses + await updateTransactions(newAddresses); + } + } + } + Future canReplaceByFee(ElectrumTransactionInfo tx) async { try { final bundle = await getTransactionExpanded(hash: tx.txHash); @@ -1331,8 +1430,9 @@ abstract class ElectrumWalletBase final changeAddresses = walletAddresses.allAddresses.where((element) => element.isChange); // look for a change address in the outputs - final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any( - (element) => element.address == addressFromOutputScript(output.scriptPubKey, network))); + final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any((element) => + element.address == + BitcoinAddressUtils.addressFromOutputScript(output.scriptPubKey, network))); var allInputsAmount = 0; @@ -1370,7 +1470,8 @@ abstract class ElectrumWalletBase final inputTransaction = bundle.ins[i]; final vout = input.txIndex; final outTransaction = inputTransaction.outputs[vout]; - final address = addressFromOutputScript(outTransaction.scriptPubKey, network); + final address = + BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network); // allInputsAmount += outTransaction.amount.toInt(); final addressRecord = @@ -1417,7 +1518,7 @@ abstract class ElectrumWalletBase } } - final address = addressFromOutputScript(out.scriptPubKey, network); + final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network); final btcAddress = RegexUtils.addressTypeFromStr(address, network); outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt()))); } @@ -1496,10 +1597,9 @@ abstract class ElectrumWalletBase return PendingBitcoinTransaction( transaction, type, - electrumClient: electrumClient, + sendWorker: sendWorker, amount: sendingAmount, fee: newFee, - network: network, hasChange: changeOutputs.isNotEmpty, feeRate: newFee.toString(), )..addListener((transaction) async { @@ -1519,27 +1619,23 @@ abstract class ElectrumWalletBase } Future getTransactionExpanded({required String hash}) async { - int? time; - int? height; - final transactionHex = await electrumClient.getTransactionHex(hash: hash); + return await sendWorker( + ElectrumWorkerTxExpandedRequest(txHash: hash, currentChainTip: currentChainTip!)) + as ElectrumTransactionBundle; + } - int? confirmations; - - final original = BtcTransaction.fromRaw(transactionHex); - final ins = []; - - for (final vin in original.inputs) { - final inputTransactionHex = await electrumClient.getTransactionHex(hash: hash); - - ins.add(BtcTransaction.fromRaw(inputTransactionHex)); + Future fetchTransactionInfo({required String hash, int? height}) async { + try { + return ElectrumTransactionInfo.fromElectrumBundle( + await getTransactionExpanded(hash: hash), + walletInfo.type, + network, + addresses: walletAddresses.allAddresses.map((e) => e.address).toSet(), + height: height, + ); + } catch (_) { + return null; } - - return ElectrumTransactionBundle( - original, - ins: ins, - time: time, - confirmations: confirmations ?? 0, - ); } @override @@ -1550,27 +1646,24 @@ abstract class ElectrumWalletBase @action Future updateTransactions([List? addresses]) async { - // TODO: all - addresses ??= walletAddresses.allAddresses - .where( - (element) => element.type == SegwitAddresType.p2wpkh && element.isChange == false, - ) - .toList(); + addresses ??= walletAddresses.allAddresses.toList(); - workerSendPort!.send( - ElectrumWorkerGetHistoryRequest( - addresses: addresses, - storedTxs: transactionHistory.transactions.values.toList(), - walletType: type, - // If we still don't have currentChainTip, txs will still be fetched but shown - // with confirmations as 0 but will be auto fixed on onHeadersResponse - chainTip: currentChainTip ?? 0, - network: network, - // mempoolAPIEnabled: mempoolAPIEnabled, - // TODO: - mempoolAPIEnabled: true, - ).toJson(), + final req = ElectrumWorkerGetHistoryRequest( + addresses: addresses, + storedTxs: transactionHistory.transactions.values.toList(), + walletType: type, + // If we still don't have currentChainTip, txs will still be fetched but shown + // with confirmations as 0 but will be auto fixed on onHeadersResponse + chainTip: currentChainTip ?? getBitcoinHeightByDate(date: DateTime.now()), + network: network, + mempoolAPIEnabled: mempoolAPIEnabled, ); + + if (_isInitialSync) { + await sendWorker(req); + } else { + workerSendPort!.send(req.toJson()); + } } @action @@ -1594,16 +1687,41 @@ abstract class ElectrumWalletBase } @action - Future updateBalance() async { - workerSendPort!.send( - ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes).toJson(), + void onBalanceResponse(ElectrumBalance balanceResult) { + var totalFrozen = 0; + var totalConfirmed = balanceResult.confirmed; + var totalUnconfirmed = balanceResult.unconfirmed; + + unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) { + if (unspentCoinInfo.isFrozen) { + // TODO: verify this works well + totalFrozen += unspentCoinInfo.value; + totalConfirmed -= unspentCoinInfo.value; + totalUnconfirmed -= unspentCoinInfo.value; + } + }); + + balance[currency] = ElectrumBalance( + confirmed: totalConfirmed, + unconfirmed: totalUnconfirmed, + frozen: totalFrozen, ); } + @action + Future updateBalance() async { + final req = ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes); + + if (_isInitialSync) { + await sendWorker(req); + } else { + workerSendPort!.send(req.toJson()); + } + } + @override void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError; - @override Future signMessage(String message, {String? address = null}) async { final record = walletAddresses.getFromAddresses(address!); @@ -1678,115 +1796,6 @@ abstract class ElectrumWalletBase return false; } - @action - Future onHistoriesResponse(List histories) async { - if (histories.isEmpty) { - return; - } - - final firstAddress = histories.first; - final isChange = firstAddress.addressRecord.isChange; - final type = firstAddress.addressRecord.type; - - final totalAddresses = (isChange - ? walletAddresses.receiveAddresses.where((element) => element.type == type).length - : walletAddresses.changeAddresses.where((element) => element.type == type).length); - final gapLimit = (isChange - ? ElectrumWalletAddressesBase.defaultChangeAddressesCount - : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); - bool hasUsedAddressesUnderGap = false; - - final addressesWithHistory = []; - - for (final addressHistory in histories) { - final txs = addressHistory.txs; - - if (txs.isNotEmpty) { - final address = addressHistory.addressRecord; - addressesWithHistory.add(address); - - hasUsedAddressesUnderGap = - address.index < totalAddresses && (address.index >= totalAddresses - gapLimit); - - for (final tx in txs) { - transactionHistory.addOne(tx); - } - } - } - - if (addressesWithHistory.isNotEmpty) { - walletAddresses.updateAdresses(addressesWithHistory); - } - - if (hasUsedAddressesUnderGap) { - // Discover new addresses for the same address type until the gap limit is respected - final newAddresses = await walletAddresses.discoverAddresses( - isChange: isChange, - derivationType: firstAddress.addressRecord.derivationType, - type: type, - derivationInfo: BitcoinAddressUtils.getDerivationFromType(type), - ); - - if (newAddresses.isNotEmpty) { - // Update the transactions for the new discovered addresses - await updateTransactions(newAddresses); - } - } - } - - @action - void onBalanceResponse(ElectrumBalance balanceResult) { - var totalFrozen = 0; - var totalConfirmed = balanceResult.confirmed; - var totalUnconfirmed = balanceResult.unconfirmed; - - unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) { - if (unspentCoinInfo.isFrozen) { - // TODO: verify this works well - totalFrozen += unspentCoinInfo.value; - totalConfirmed -= unspentCoinInfo.value; - totalUnconfirmed -= unspentCoinInfo.value; - } - }); - - balance[currency] = ElectrumBalance( - confirmed: totalConfirmed, - unconfirmed: totalUnconfirmed, - frozen: totalFrozen, - ); - } - - @action - Future onHeadersResponse(ElectrumHeaderResponse response) async { - currentChainTip = response.height; - - bool updated = false; - transactionHistory.transactions.values.forEach((tx) { - if (tx.height != null && tx.height! > 0) { - final newConfirmations = currentChainTip! - tx.height! + 1; - - if (tx.confirmations != newConfirmations) { - tx.confirmations = newConfirmations; - tx.isPending = tx.confirmations == 0; - updated = true; - } - } - }); - - if (updated) { - await save(); - } - } - - @action - Future subscribeForHeaders() async { - print(_chainTipListenerOn); - if (_chainTipListenerOn) return; - - workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson()); - _chainTipListenerOn = true; - } - @action void _onConnectionStatusChange(ConnectionStatus status) { switch (status) { @@ -1862,14 +1871,15 @@ abstract class ElectrumWalletBase final inputTransaction = bundle.ins[i]; final vout = input.txIndex; final outTransaction = inputTransaction.outputs[vout]; - final address = addressFromOutputScript(outTransaction.scriptPubKey, network); + final address = + BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network); if (address.isNotEmpty) inputAddresses.add(address); } for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) { final out = bundle.originalTransaction.outputs[i]; - final address = addressFromOutputScript(out.scriptPubKey, network); + final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network); if (address.isNotEmpty) outputAddresses.add(address); diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index f05adbd84..44e3be7f9 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -649,7 +649,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ), index: i, isChange: isChange, - isHidden: derivationType == CWBitcoinDerivationType.old, + isHidden: derivationType == CWBitcoinDerivationType.old && type != SegwitAddresType.p2wpkh, type: type ?? addressPageType, network: network, derivationInfo: derivationInfo, @@ -664,7 +664,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action void updateAdresses(Iterable addresses) { for (final address in addresses) { - _allAddresses.replaceRange(address.index, address.index + 1, [address]); + final index = _allAddresses.indexWhere((element) => element.address == address.address); + _allAddresses.replaceRange(index, index + 1, [address]); + + updateAddressesByMatch(); + updateReceiveAddresses(); + updateChangeAddresses(); } } diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 8b372bd3f..102d6c313 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -3,16 +3,20 @@ import 'dart:convert'; import 'dart:isolate'; 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_core/get_height_by_date.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; -// import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:http/http.dart' as http; - -// TODO: ping +import 'package:sp_scanner/sp_scanner.dart'; class ElectrumWorker { final SendPort sendPort; @@ -56,8 +60,15 @@ class ElectrumWorker { ElectrumWorkerConnectionRequest.fromJson(messageJson), ); break; + case ElectrumWorkerMethods.txHashMethod: + await _handleGetTxExpanded( + ElectrumWorkerTxExpandedRequest.fromJson(messageJson), + ); + break; case ElectrumRequestMethods.headersSubscribeMethod: - await _handleHeadersSubscribe(); + await _handleHeadersSubscribe( + ElectrumWorkerHeadersSubscribeRequest.fromJson(messageJson), + ); break; case ElectrumRequestMethods.scripthashesSubscribeMethod: await _handleScriphashesSubscribe( @@ -74,12 +85,21 @@ class ElectrumWorker { ElectrumWorkerGetHistoryRequest.fromJson(messageJson), ); break; - case 'blockchain.scripthash.listunspent': - // await _handleListUnspent(workerMessage); + case ElectrumRequestMethods.listunspentMethod: + await _handleListUnspent( + ElectrumWorkerListUnspentRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.broadcastMethod: + await _handleBroadcast( + ElectrumWorkerBroadcastRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.tweaksSubscribeMethod: + await _handleScanSilentPayments( + ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson), + ); break; - // Add other method handlers here - // default: - // _sendError(workerMethod, 'Unsupported method: ${workerMessage.method}'); } } catch (e, s) { print(s); @@ -88,11 +108,11 @@ class ElectrumWorker { } Future _handleConnect(ElectrumWorkerConnectionRequest request) async { - _electrumClient = ElectrumApiProvider( - await ElectrumTCPService.connect( + _electrumClient = await ElectrumApiProvider.connect( + ElectrumTCPService.connect( request.uri, onConnectionStatusChange: (status) { - _sendResponse(ElectrumWorkerConnectionResponse(status: status)); + _sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); }, defaultRequestTimeOut: const Duration(seconds: 5), connectionTimeOut: const Duration(seconds: 5), @@ -100,7 +120,7 @@ class ElectrumWorker { ); } - Future _handleHeadersSubscribe() async { + Future _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async { final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe()); if (listener == null) { _sendError(ElectrumWorkerHeadersSubscribeError(error: 'Failed to subscribe')); @@ -108,7 +128,9 @@ class ElectrumWorker { } listener((event) { - _sendResponse(ElectrumWorkerHeadersSubscribeResponse(result: event)); + _sendResponse( + ElectrumWorkerHeadersSubscribeResponse(result: event, id: request.id), + ); }); } @@ -135,6 +157,7 @@ class ElectrumWorker { _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( result: {address: status}, + id: request.id, )); }); })); @@ -171,7 +194,7 @@ class ElectrumWorker { } } catch (_) { tx = ElectrumTransactionInfo.fromElectrumBundle( - await getTransactionExpanded( + await _getTransactionExpanded( hash: txid, currentChainTip: result.chainTip, mempoolAPIEnabled: result.mempoolAPIEnabled, @@ -201,10 +224,113 @@ class ElectrumWorker { return histories; })); - _sendResponse(ElectrumWorkerGetHistoryResponse(result: histories.values.toList())); + _sendResponse(ElectrumWorkerGetHistoryResponse( + result: histories.values.toList(), + id: result.id, + )); } - Future getTransactionExpanded({ + // Future _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { + // final balanceFutures = >>[]; + + // for (final scripthash in request.scripthashes) { + // final balanceFuture = _electrumClient!.request( + // ElectrumGetScriptHashBalance(scriptHash: scripthash), + // ); + // balanceFutures.add(balanceFuture); + // } + + // var totalConfirmed = 0; + // var totalUnconfirmed = 0; + + // final balances = await Future.wait(balanceFutures); + + // for (final balance in balances) { + // final confirmed = balance['confirmed'] as int? ?? 0; + // final unconfirmed = balance['unconfirmed'] as int? ?? 0; + // totalConfirmed += confirmed; + // totalUnconfirmed += unconfirmed; + // } + + // _sendResponse(ElectrumWorkerGetBalanceResponse( + // result: ElectrumBalance( + // confirmed: totalConfirmed, + // unconfirmed: totalUnconfirmed, + // frozen: 0, + // ), + // )); + // } + + Future _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async { + final balanceFutures = >>[]; + + for (final scripthash in request.scripthashes) { + final balanceFuture = _electrumClient!.request( + ElectrumGetScriptHashBalance(scriptHash: scripthash), + ); + balanceFutures.add(balanceFuture); + } + + var totalConfirmed = 0; + var totalUnconfirmed = 0; + + final balances = await Future.wait(balanceFutures); + + for (final balance in balances) { + final confirmed = balance['confirmed'] as int? ?? 0; + final unconfirmed = balance['unconfirmed'] as int? ?? 0; + totalConfirmed += confirmed; + totalUnconfirmed += unconfirmed; + } + + _sendResponse( + ElectrumWorkerGetBalanceResponse( + result: ElectrumBalance( + confirmed: totalConfirmed, + unconfirmed: totalUnconfirmed, + frozen: 0, + ), + id: request.id, + ), + ); + } + + Future _handleListUnspent(ElectrumWorkerListUnspentRequest request) async { + final unspents = >{}; + + await Future.wait(request.scripthashes.map((scriptHash) async { + final scriptHashUnspents = await _electrumClient!.request( + ElectrumScriptHashListUnspent(scriptHash: scriptHash), + ); + + if (scriptHashUnspents.isNotEmpty) { + unspents[scriptHash] = scriptHashUnspents; + } + })); + + _sendResponse(ElectrumWorkerListUnspentResponse(utxos: unspents, id: request.id)); + } + + Future _handleBroadcast(ElectrumWorkerBroadcastRequest request) async { + final txHash = await _electrumClient!.request( + ElectrumBroadCastTransaction(transactionRaw: request.transactionRaw), + ); + + _sendResponse(ElectrumWorkerBroadcastResponse(txHash: txHash, id: request.id)); + } + + Future _handleGetTxExpanded(ElectrumWorkerTxExpandedRequest request) async { + final tx = await _getTransactionExpanded( + hash: request.txHash, + currentChainTip: request.currentChainTip, + mempoolAPIEnabled: false, + getConfirmations: false, + ); + + _sendResponse(ElectrumWorkerTxExpandedResponse(expandedTx: tx, id: request.id)); + } + + Future _getTransactionExpanded({ required String hash, required int currentChainTip, required bool mempoolAPIEnabled, @@ -289,65 +415,312 @@ class ElectrumWorker { ); } - // Future _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { - // final balanceFutures = >>[]; + Future _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async { + final scanData = request.scanData; + int syncHeight = scanData.height; + int initialSyncHeight = syncHeight; - // for (final scripthash in request.scripthashes) { - // final balanceFuture = _electrumClient!.request( - // ElectrumGetScriptHashBalance(scriptHash: scripthash), - // ); - // balanceFutures.add(balanceFuture); - // } + int getCountPerRequest(int syncHeight) { + if (scanData.isSingleScan) { + return 1; + } - // var totalConfirmed = 0; - // var totalUnconfirmed = 0; - - // final balances = await Future.wait(balanceFutures); - - // for (final balance in balances) { - // final confirmed = balance['confirmed'] as int? ?? 0; - // final unconfirmed = balance['unconfirmed'] as int? ?? 0; - // totalConfirmed += confirmed; - // totalUnconfirmed += unconfirmed; - // } - - // _sendResponse(ElectrumWorkerGetBalanceResponse( - // result: ElectrumBalance( - // confirmed: totalConfirmed, - // unconfirmed: totalUnconfirmed, - // frozen: 0, - // ), - // )); - // } - - Future _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async { - final balanceFutures = >>[]; - - for (final scripthash in request.scripthashes) { - final balanceFuture = _electrumClient!.request( - ElectrumGetScriptHashBalance(scriptHash: scripthash), - ); - balanceFutures.add(balanceFuture); + final amountLeft = scanData.chainTip - syncHeight + 1; + return amountLeft; } - var totalConfirmed = 0; - var totalUnconfirmed = 0; + final receiver = Receiver( + scanData.silentAddress.b_scan.toHex(), + scanData.silentAddress.B_spend.toHex(), + scanData.network == BitcoinNetwork.testnet, + scanData.labelIndexes, + scanData.labelIndexes.length, + ); - final balances = await Future.wait(balanceFutures); - - for (final balance in balances) { - final confirmed = balance['confirmed'] as int? ?? 0; - final unconfirmed = balance['unconfirmed'] as int? ?? 0; - totalConfirmed += confirmed; - totalUnconfirmed += unconfirmed; - } - - _sendResponse(ElectrumWorkerGetBalanceResponse( - result: ElectrumBalance( - confirmed: totalConfirmed, - unconfirmed: totalUnconfirmed, - frozen: 0, + // Initial status UI update, send how many blocks in total to scan + final initialCount = getCountPerRequest(syncHeight); + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse( + height: syncHeight, + syncStatus: StartingScanSyncStatus(syncHeight), ), )); + + print([syncHeight, initialCount]); + final listener = await _electrumClient!.subscribe( + ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), + ); + + Future listenFn(ElectrumTweaksSubscribeResponse response) async { + // success or error msg + final noData = response.message != null; + + if (noData) { + // re-subscribe to continue receiving messages, starting from the next unscanned height + final nextHeight = syncHeight + 1; + final nextCount = getCountPerRequest(nextHeight); + + if (nextCount > 0) { + final nextListener = await _electrumClient!.subscribe( + ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), + ); + nextListener?.call(listenFn); + } + + return; + } + + // Continuous status UI update, send how many blocks left to scan + final syncingStatus = scanData.isSingleScan + ? SyncingSyncStatus(1, 0) + : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse(height: syncHeight, syncStatus: syncingStatus), + )); + + final tweakHeight = response.block; + + try { + final blockTweaks = response.blockTweaks; + + for (final txid in blockTweaks.keys) { + final tweakData = blockTweaks[txid]; + final outputPubkeys = tweakData!.outputPubkeys; + final tweak = tweakData.tweak; + + try { + // scanOutputs called from rust here + final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver); + + if (addToWallet.isEmpty) { + // no results tx, continue to next tx + continue; + } + + // 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, + isReplaced: false, + date: scanData.network == BitcoinNetwork.mainnet + ? getDateByBitcoinHeight(tweakHeight) + : DateTime.now(), + confirmations: scanData.chainTip - tweakHeight + 1, + unspents: [], + isReceivedSilentPayment: true, + ); + + 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 matchingOutput = outputPubkeys[output]!; + final amount = matchingOutput.amount; + final pos = matchingOutput.vout; + + final receivedAddressRecord = BitcoinReceivedSPAddressRecord( + receivingOutputAddress, + labelIndex: 1, // TODO: get actual index/label + isUsed: true, + spendKey: scanData.silentAddress.b_spend.tweakAdd( + BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)), + ), + txCount: 1, + balance: amount, + ); + + final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos); + + txInfo.unspents!.add(unspent); + txInfo.amount += unspent.value; + }); + }); + + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse(transactions: {txInfo.id: txInfo}), + )); + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + } + } + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + } + + syncHeight = tweakHeight; + + if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { + if (tweakHeight >= scanData.chainTip) + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse( + height: syncHeight, + syncStatus: SyncedTipSyncStatus(scanData.chainTip), + ), + )); + + if (scanData.isSingleScan) { + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse(height: syncHeight, syncStatus: SyncedSyncStatus()), + )); + } + } + } + + listener?.call(listenFn); + + // if (tweaksSubscription == null) { + // return scanData.sendPort.send( + // SyncResponse(syncHeight, UnsupportedSyncStatus()), + // ); + // } } } + +Future delegatedScan(ScanData scanData) async { + // int syncHeight = scanData.height; + // int initialSyncHeight = syncHeight; + + // BehaviorSubject? tweaksSubscription = null; + + // final electrumClient = scanData.electrumClient; + // await electrumClient.connectToUri( + // scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"), + // useSSL: scanData.node?.useSSL ?? false, + // ); + + // if (tweaksSubscription == null) { + // scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); + + // tweaksSubscription = await electrumClient.tweaksScan( + // pubSpendKey: scanData.silentAddress.B_spend.toHex(), + // ); + + // Future listenFn(t) async { + // final tweaks = t as Map; + // final msg = tweaks["message"]; + + // // success or error msg + // final noData = msg != null; + // if (noData) { + // return; + // } + + // // Continuous status UI update, send how many blocks left to scan + // final syncingStatus = scanData.isSingleScan + // ? SyncingSyncStatus(1, 0) + // : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + // scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); + + // final blockHeight = tweaks.keys.first; + // final tweakHeight = int.parse(blockHeight); + + // try { + // final blockTweaks = tweaks[blockHeight] as Map; + + // 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 spendingKey = details["spending_key"].toString(); + + // try { + // // 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, + // isReplaced: false, + // date: scanData.network == BitcoinNetwork.mainnet + // ? getDateByBitcoinHeight(tweakHeight) + // : DateTime.now(), + // confirmations: scanData.chainTip - tweakHeight + 1, + // unspents: [], + // isReceivedSilentPayment: true, + // ); + + // outputPubkeys.forEach((pos, value) { + // final secKey = ECPrivate.fromHex(spendingKey); + // final receivingOutputAddress = + // secKey.getPublic().toTaprootAddress(tweak: false).toAddress(scanData.network); + + // late int amount; + // try { + // amount = int.parse(value[1].toString()); + // } catch (_) { + // return; + // } + + // final receivedAddressRecord = BitcoinReceivedSPAddressRecord( + // receivingOutputAddress, + // labelIndex: 0, + // isUsed: true, + // spendKey: secKey, + // txCount: 1, + // balance: amount, + // ); + + // final unspent = BitcoinUnspent( + // receivedAddressRecord, + // txid, + // amount, + // int.parse(pos.toString()), + // ); + + // txInfo.unspents!.add(unspent); + // txInfo.amount += unspent.value; + // }); + + // scanData.sendPort.send({txInfo.id: txInfo}); + // } catch (_) {} + // } + // } catch (_) {} + + // syncHeight = tweakHeight; + + // if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { + // if (tweakHeight >= scanData.chainTip) + // scanData.sendPort.send(SyncResponse( + // syncHeight, + // SyncedTipSyncStatus(scanData.chainTip), + // )); + + // if (scanData.isSingleScan) { + // scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); + // } + + // await tweaksSubscription!.close(); + // await electrumClient.close(); + // } + // } + + // tweaksSubscription?.listen(listenFn); + // } + + // if (tweaksSubscription == null) { + // return scanData.sendPort.send( + // SyncResponse(syncHeight, UnsupportedSyncStatus()), + // ); + // } +} + +class ScanNode { + final Uri uri; + final bool? useSSL; + + ScanNode(this.uri, this.useSSL); +} diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart index c171e2cae..6bd4d296e 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart @@ -4,9 +4,11 @@ class ElectrumWorkerMethods { static const String connectionMethod = "connection"; static const String unknownMethod = "unknown"; + static const String txHashMethod = "txHash"; static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod); static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod); + static const ElectrumWorkerMethods txHash = ElectrumWorkerMethods._(txHashMethod); @override String toString() { diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart index f666eed1d..ea3c0b199 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart @@ -4,17 +4,24 @@ import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; abstract class ElectrumWorkerRequest { abstract final String method; + abstract final int? id; Map toJson(); ElectrumWorkerRequest.fromJson(Map json); } class ElectrumWorkerResponse { - ElectrumWorkerResponse({required this.method, required this.result, this.error}); + ElectrumWorkerResponse({ + required this.method, + required this.result, + this.error, + this.id, + }); final String method; final RESULT result; final String? error; + final int? id; RESPONSE resultJson(RESULT result) { throw UnimplementedError(); @@ -25,21 +32,22 @@ class ElectrumWorkerResponse { } Map toJson() { - return {'method': method, 'result': resultJson(result), 'error': error}; + return {'method': method, 'result': resultJson(result), 'error': error, 'id': id}; } } class ElectrumWorkerErrorResponse { - ElectrumWorkerErrorResponse({required this.error}); + ElectrumWorkerErrorResponse({required this.error, this.id}); String get method => ElectrumWorkerMethods.unknown.method; + final int? id; final String error; factory ElectrumWorkerErrorResponse.fromJson(Map json) { - return ElectrumWorkerErrorResponse(error: json['error'] as String); + return ElectrumWorkerErrorResponse(error: json['error'] as String, id: json['id'] as int); } Map toJson() { - return {'method': method, 'error': error}; + return {'method': method, 'error': error, 'id': id}; } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/broadcast.dart b/cw_bitcoin/lib/electrum_worker/methods/broadcast.dart new file mode 100644 index 000000000..f295fa24a --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/broadcast.dart @@ -0,0 +1,56 @@ +part of 'methods.dart'; + +class ElectrumWorkerBroadcastRequest implements ElectrumWorkerRequest { + ElectrumWorkerBroadcastRequest({required this.transactionRaw, this.id}); + + final String transactionRaw; + final int? id; + + @override + final String method = ElectrumRequestMethods.broadcast.method; + + @override + factory ElectrumWorkerBroadcastRequest.fromJson(Map json) { + return ElectrumWorkerBroadcastRequest( + transactionRaw: json['transactionRaw'] as String, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'transactionRaw': transactionRaw}; + } +} + +class ElectrumWorkerBroadcastError extends ElectrumWorkerErrorResponse { + ElectrumWorkerBroadcastError({ + required super.error, + super.id, + }) : super(); + + @override + String get method => ElectrumRequestMethods.broadcast.method; +} + +class ElectrumWorkerBroadcastResponse extends ElectrumWorkerResponse { + ElectrumWorkerBroadcastResponse({ + required String txHash, + super.error, + super.id, + }) : super(result: txHash, method: ElectrumRequestMethods.broadcast.method); + + @override + String resultJson(result) { + return result; + } + + @override + factory ElectrumWorkerBroadcastResponse.fromJson(Map json) { + return ElectrumWorkerBroadcastResponse( + txHash: json['result'] as String, + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/connection.dart b/cw_bitcoin/lib/electrum_worker/methods/connection.dart index 1abbcb81e..2512c6cfd 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/connection.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/connection.dart @@ -1,34 +1,56 @@ part of 'methods.dart'; class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest { - ElectrumWorkerConnectionRequest({required this.uri}); + ElectrumWorkerConnectionRequest({ + required this.uri, + required this.network, + this.id, + }); final Uri uri; + final BasedUtxoNetwork network; + final int? id; @override final String method = ElectrumWorkerMethods.connect.method; @override factory ElectrumWorkerConnectionRequest.fromJson(Map json) { - return ElectrumWorkerConnectionRequest(uri: Uri.parse(json['params'] as String)); + return ElectrumWorkerConnectionRequest( + uri: Uri.parse(json['uri'] as String), + network: BasedUtxoNetwork.values.firstWhere( + (e) => e.toString() == json['network'] as String, + ), + id: json['id'] as int?, + ); } @override Map toJson() { - return {'method': method, 'params': uri.toString()}; + return { + 'method': method, + 'uri': uri.toString(), + 'network': network.toString(), + }; } } class ElectrumWorkerConnectionError extends ElectrumWorkerErrorResponse { - ElectrumWorkerConnectionError({required String error}) : super(error: error); + ElectrumWorkerConnectionError({ + required super.error, + super.id, + }) : super(); @override String get method => ElectrumWorkerMethods.connect.method; } class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse { - ElectrumWorkerConnectionResponse({required ConnectionStatus status, super.error}) - : super( + ElectrumWorkerConnectionResponse({ + required ConnectionStatus status, + super.error, + super.id, + }) : super( result: status, method: ElectrumWorkerMethods.connect.method, ); @@ -45,6 +67,7 @@ class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse e.toString() == json['result'] as String, ), error: json['error'] as String?, + id: json['id'] as int?, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart b/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart index fc79967e1..2fc551367 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart @@ -1,9 +1,10 @@ part of 'methods.dart'; class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { - ElectrumWorkerGetBalanceRequest({required this.scripthashes}); + ElectrumWorkerGetBalanceRequest({required this.scripthashes, this.id}); final Set scripthashes; + final int? id; @override final String method = ElectrumRequestMethods.getBalance.method; @@ -12,6 +13,7 @@ class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { factory ElectrumWorkerGetBalanceRequest.fromJson(Map json) { return ElectrumWorkerGetBalanceRequest( scripthashes: (json['scripthashes'] as List).toSet(), + id: json['id'] as int?, ); } @@ -22,7 +24,10 @@ class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { } class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse { - ElectrumWorkerGetBalanceError({required String error}) : super(error: error); + ElectrumWorkerGetBalanceError({ + required super.error, + super.id, + }) : super(); @override final String method = ElectrumRequestMethods.getBalance.method; @@ -30,8 +35,11 @@ class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse { class ElectrumWorkerGetBalanceResponse extends ElectrumWorkerResponse?> { - ElectrumWorkerGetBalanceResponse({required super.result, super.error}) - : super(method: ElectrumRequestMethods.getBalance.method); + ElectrumWorkerGetBalanceResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.getBalance.method); @override Map? resultJson(result) { @@ -47,6 +55,7 @@ class ElectrumWorkerGetBalanceResponse frozen: 0, ), error: json['error'] as String?, + id: json['id'] as int?, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_history.dart b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart index 584f4b6d1..021ed6899 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_history.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart @@ -8,6 +8,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { required this.chainTip, required this.network, required this.mempoolAPIEnabled, + this.id, }); final List addresses; @@ -16,6 +17,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { final int chainTip; final BasedUtxoNetwork network; final bool mempoolAPIEnabled; + final int? id; @override final String method = ElectrumRequestMethods.getHistory.method; @@ -35,6 +37,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { chainTip: json['chainTip'] as int, network: BasedUtxoNetwork.fromName(json['network'] as String), mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, + id: json['id'] as int?, ); } @@ -53,7 +56,10 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { } class ElectrumWorkerGetHistoryError extends ElectrumWorkerErrorResponse { - ElectrumWorkerGetHistoryError({required String error}) : super(error: error); + ElectrumWorkerGetHistoryError({ + required super.error, + super.id, + }) : super(); @override final String method = ElectrumRequestMethods.getHistory.method; @@ -90,8 +96,11 @@ class AddressHistoriesResponse { class ElectrumWorkerGetHistoryResponse extends ElectrumWorkerResponse, List>> { - ElectrumWorkerGetHistoryResponse({required super.result, super.error}) - : super(method: ElectrumRequestMethods.getHistory.method); + ElectrumWorkerGetHistoryResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.getHistory.method); @override List> resultJson(result) { @@ -105,6 +114,7 @@ class ElectrumWorkerGetHistoryResponse .map((e) => AddressHistoriesResponse.fromJson(e as Map)) .toList(), error: json['error'] as String?, + id: json['id'] as int?, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart new file mode 100644 index 000000000..a2dfcda17 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart @@ -0,0 +1,63 @@ +part of 'methods.dart'; + +class ElectrumWorkerTxExpandedRequest implements ElectrumWorkerRequest { + ElectrumWorkerTxExpandedRequest({ + required this.txHash, + required this.currentChainTip, + this.id, + }); + + final String txHash; + final int currentChainTip; + final int? id; + + @override + final String method = ElectrumWorkerMethods.txHash.method; + + @override + factory ElectrumWorkerTxExpandedRequest.fromJson(Map json) { + return ElectrumWorkerTxExpandedRequest( + txHash: json['txHash'] as String, + currentChainTip: json['currentChainTip'] as int, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'txHash': txHash, 'currentChainTip': currentChainTip}; + } +} + +class ElectrumWorkerTxExpandedError extends ElectrumWorkerErrorResponse { + ElectrumWorkerTxExpandedError({ + required String error, + super.id, + }) : super(error: error); + + @override + String get method => ElectrumWorkerMethods.txHash.method; +} + +class ElectrumWorkerTxExpandedResponse + extends ElectrumWorkerResponse> { + ElectrumWorkerTxExpandedResponse({ + required ElectrumTransactionBundle expandedTx, + super.error, + super.id, + }) : super(result: expandedTx, method: ElectrumWorkerMethods.txHash.method); + + @override + Map resultJson(result) { + return result.toJson(); + } + + @override + factory ElectrumWorkerTxExpandedResponse.fromJson(Map json) { + return ElectrumWorkerTxExpandedResponse( + expandedTx: ElectrumTransactionBundle.fromJson(json['result'] as Map), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart index 619f32aed..de02f5d24 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart @@ -1,14 +1,17 @@ part of 'methods.dart'; class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest { - ElectrumWorkerHeadersSubscribeRequest(); + ElectrumWorkerHeadersSubscribeRequest({this.id}); @override final String method = ElectrumRequestMethods.headersSubscribe.method; + final int? id; @override factory ElectrumWorkerHeadersSubscribeRequest.fromJson(Map json) { - return ElectrumWorkerHeadersSubscribeRequest(); + return ElectrumWorkerHeadersSubscribeRequest( + id: json['id'] as int?, + ); } @override @@ -18,7 +21,10 @@ class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest { } class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse { - ElectrumWorkerHeadersSubscribeError({required String error}) : super(error: error); + ElectrumWorkerHeadersSubscribeError({ + required super.error, + super.id, + }) : super(); @override final String method = ElectrumRequestMethods.headersSubscribe.method; @@ -26,8 +32,11 @@ class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse { class ElectrumWorkerHeadersSubscribeResponse extends ElectrumWorkerResponse> { - ElectrumWorkerHeadersSubscribeResponse({required super.result, super.error}) - : super(method: ElectrumRequestMethods.headersSubscribe.method); + ElectrumWorkerHeadersSubscribeResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.headersSubscribe.method); @override Map resultJson(result) { @@ -39,6 +48,7 @@ class ElectrumWorkerHeadersSubscribeResponse return ElectrumWorkerHeadersSubscribeResponse( result: ElectrumHeaderResponse.fromJson(json['result'] as Map), error: json['error'] as String?, + id: json['id'] as int?, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart b/cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart new file mode 100644 index 000000000..66d1b1a68 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart @@ -0,0 +1,60 @@ +part of 'methods.dart'; + +class ElectrumWorkerListUnspentRequest implements ElectrumWorkerRequest { + ElectrumWorkerListUnspentRequest({required this.scripthashes, this.id}); + + final List scripthashes; + final int? id; + + @override + final String method = ElectrumRequestMethods.listunspent.method; + + @override + factory ElectrumWorkerListUnspentRequest.fromJson(Map json) { + return ElectrumWorkerListUnspentRequest( + scripthashes: json['scripthashes'] as List, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'scripthashes': scripthashes}; + } +} + +class ElectrumWorkerListUnspentError extends ElectrumWorkerErrorResponse { + ElectrumWorkerListUnspentError({ + required super.error, + super.id, + }) : super(); + + @override + String get method => ElectrumRequestMethods.listunspent.method; +} + +class ElectrumWorkerListUnspentResponse + extends ElectrumWorkerResponse>, Map> { + ElectrumWorkerListUnspentResponse({ + required Map> utxos, + super.error, + super.id, + }) : super(result: utxos, method: ElectrumRequestMethods.listunspent.method); + + @override + Map resultJson(result) { + return result.map((key, value) => MapEntry(key, value.map((e) => e.toJson()).toList())); + } + + @override + factory ElectrumWorkerListUnspentResponse.fromJson(Map json) { + return ElectrumWorkerListUnspentResponse( + utxos: (json['result'] as Map).map( + (key, value) => MapEntry(key, + (value as List).map((e) => ElectrumUtxo.fromJson(e as Map)).toList()), + ), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart b/cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart deleted file mode 100644 index c3a626a0b..000000000 --- a/cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart +++ /dev/null @@ -1,53 +0,0 @@ -// part of 'methods.dart'; - -// class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { -// ElectrumWorkerGetBalanceRequest({required this.scripthashes}); - -// final Set scripthashes; - -// @override -// final String method = ElectrumRequestMethods.getBalance.method; - -// @override -// factory ElectrumWorkerGetBalanceRequest.fromJson(Map json) { -// return ElectrumWorkerGetBalanceRequest( -// scripthashes: (json['scripthashes'] as List).toSet(), -// ); -// } - -// @override -// Map toJson() { -// return {'method': method, 'scripthashes': scripthashes.toList()}; -// } -// } - -// class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse { -// ElectrumWorkerGetBalanceError({required String error}) : super(error: error); - -// @override -// final String method = ElectrumRequestMethods.getBalance.method; -// } - -// class ElectrumWorkerGetBalanceResponse -// extends ElectrumWorkerResponse?> { -// ElectrumWorkerGetBalanceResponse({required super.result, super.error}) -// : super(method: ElectrumRequestMethods.getBalance.method); - -// @override -// Map? resultJson(result) { -// return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed}; -// } - -// @override -// factory ElectrumWorkerGetBalanceResponse.fromJson(Map json) { -// return ElectrumWorkerGetBalanceResponse( -// result: ElectrumBalance( -// confirmed: json['result']['confirmed'] as int, -// unconfirmed: json['result']['unconfirmed'] as int, -// frozen: 0, -// ), -// error: json['error'] as String?, -// ); -// } -// } - diff --git a/cw_bitcoin/lib/electrum_worker/methods/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart index 31b82bf9e..6ace715d0 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/methods.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; @@ -6,8 +5,14 @@ import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/sync_status.dart'; + part 'connection.dart'; part 'headers_subscribe.dart'; part 'scripthashes_subscribe.dart'; part 'get_balance.dart'; part 'get_history.dart'; +part 'get_tx_expanded.dart'; +part 'broadcast.dart'; +part 'list_unspent.dart'; +part 'tweaks_subscribe.dart'; diff --git a/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart index 35a73ef49..31f9abe76 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart @@ -1,9 +1,13 @@ part of 'methods.dart'; class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerRequest { - ElectrumWorkerScripthashesSubscribeRequest({required this.scripthashByAddress}); + ElectrumWorkerScripthashesSubscribeRequest({ + required this.scripthashByAddress, + this.id, + }); final Map scripthashByAddress; + final int? id; @override final String method = ElectrumRequestMethods.scriptHashSubscribe.method; @@ -12,6 +16,7 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques factory ElectrumWorkerScripthashesSubscribeRequest.fromJson(Map json) { return ElectrumWorkerScripthashesSubscribeRequest( scripthashByAddress: json['scripthashes'] as Map, + id: json['id'] as int?, ); } @@ -22,7 +27,10 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques } class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorResponse { - ElectrumWorkerScripthashesSubscribeError({required String error}) : super(error: error); + ElectrumWorkerScripthashesSubscribeError({ + required super.error, + super.id, + }) : super(); @override final String method = ElectrumRequestMethods.scriptHashSubscribe.method; @@ -30,8 +38,11 @@ class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorRespon class ElectrumWorkerScripthashesSubscribeResponse extends ElectrumWorkerResponse?, Map?> { - ElectrumWorkerScripthashesSubscribeResponse({required super.result, super.error}) - : super(method: ElectrumRequestMethods.scriptHashSubscribe.method); + ElectrumWorkerScripthashesSubscribeResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.scriptHashSubscribe.method); @override Map? resultJson(result) { @@ -43,6 +54,7 @@ class ElectrumWorkerScripthashesSubscribeResponse return ElectrumWorkerScripthashesSubscribeResponse( result: json['result'] as Map?, error: json['error'] as String?, + id: json['id'] as int?, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart new file mode 100644 index 000000000..0a6f36dc9 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart @@ -0,0 +1,157 @@ +part of 'methods.dart'; + +class ScanData { + final SilentPaymentOwner silentAddress; + final int height; + final BasedUtxoNetwork network; + final int chainTip; + final List transactionHistoryIds; + final Map labels; + final List labelIndexes; + final bool isSingleScan; + + ScanData({ + required this.silentAddress, + required this.height, + required this.network, + required this.chainTip, + required this.transactionHistoryIds, + required this.labels, + required this.labelIndexes, + required this.isSingleScan, + }); + + factory ScanData.fromHeight(ScanData scanData, int newHeight) { + return ScanData( + silentAddress: scanData.silentAddress, + height: newHeight, + network: scanData.network, + chainTip: scanData.chainTip, + transactionHistoryIds: scanData.transactionHistoryIds, + labels: scanData.labels, + labelIndexes: scanData.labelIndexes, + isSingleScan: scanData.isSingleScan, + ); + } + + Map toJson() { + return { + 'silentAddress': silentAddress.toJson(), + 'height': height, + 'network': network.value, + 'chainTip': chainTip, + 'transactionHistoryIds': transactionHistoryIds, + 'labels': labels, + 'labelIndexes': labelIndexes, + 'isSingleScan': isSingleScan, + }; + } + + static ScanData fromJson(Map json) { + return ScanData( + silentAddress: SilentPaymentOwner.fromJson(json['silentAddress'] as Map), + height: json['height'] as int, + network: BasedUtxoNetwork.fromName(json['network'] as String), + chainTip: json['chainTip'] as int, + transactionHistoryIds: + (json['transactionHistoryIds'] as List).map((e) => e as String).toList(), + labels: json['labels'] as Map, + labelIndexes: (json['labelIndexes'] as List).map((e) => e as int).toList(), + isSingleScan: json['isSingleScan'] as bool, + ); + } +} + +class ElectrumWorkerTweaksSubscribeRequest implements ElectrumWorkerRequest { + ElectrumWorkerTweaksSubscribeRequest({ + required this.scanData, + this.id, + }); + + final ScanData scanData; + final int? id; + + @override + final String method = ElectrumRequestMethods.tweaksSubscribe.method; + + @override + factory ElectrumWorkerTweaksSubscribeRequest.fromJson(Map json) { + return ElectrumWorkerTweaksSubscribeRequest( + scanData: ScanData.fromJson(json['scanData'] as Map), + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'scanData': scanData.toJson()}; + } +} + +class ElectrumWorkerTweaksSubscribeError extends ElectrumWorkerErrorResponse { + ElectrumWorkerTweaksSubscribeError({ + required super.error, + super.id, + }) : super(); + + @override + final String method = ElectrumRequestMethods.tweaksSubscribe.method; +} + +class TweaksSyncResponse { + int? height; + SyncStatus? syncStatus; + Map? transactions = {}; + + TweaksSyncResponse({this.height, this.syncStatus, this.transactions}); + + Map toJson() { + return { + 'height': height, + 'syncStatus': syncStatus == null ? null : syncStatusToJson(syncStatus!), + 'transactions': transactions?.map((key, value) => MapEntry(key, value.toJson())), + }; + } + + static TweaksSyncResponse fromJson(Map json) { + return TweaksSyncResponse( + height: json['height'] as int?, + syncStatus: json['syncStatus'] == null + ? null + : syncStatusFromJson(json['syncStatus'] as Map), + transactions: json['transactions'] == null + ? null + : (json['transactions'] as Map).map( + (key, value) => MapEntry( + key, + ElectrumTransactionInfo.fromJson( + value as Map, + WalletType.bitcoin, + )), + ), + ); + } +} + +class ElectrumWorkerTweaksSubscribeResponse + extends ElectrumWorkerResponse> { + ElectrumWorkerTweaksSubscribeResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.tweaksSubscribe.method); + + @override + Map resultJson(result) { + return result.toJson(); + } + + @override + factory ElectrumWorkerTweaksSubscribeResponse.fromJson(Map json) { + return ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse.fromJson(json['result'] as Map), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index ce583759a..277864af7 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -6,7 +6,7 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; -import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +// import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; @@ -109,22 +109,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }); 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(); - } + await CwMweb.stop(); + await Future.delayed(const Duration(seconds: 5)); + startSync(); } else if (mwebSyncStatus is SyncingSyncStatus) { syncStatus = mwebSyncStatus; } else if (mwebSyncStatus is SynchronizingSyncStatus) { @@ -348,8 +335,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return; } - final nodeHeight = - await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node + final nodeHeight = await currentChainTip ?? 0; if (nodeHeight == 0) { // we aren't connected to the ltc node yet @@ -635,7 +621,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } final status = await CwMweb.status(StatusRequest()); - final height = await electrumClient.getCurrentBlockChainTip(); + final height = await currentChainTip; if (height == null || status.blockHeaderHeight != height) return; if (status.mwebUtxosHeight != height) return; // we aren't synced int amount = 0; @@ -770,7 +756,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }); // copy coin control attributes to mwebCoins: - await updateCoins(mwebUnspentCoins.toSet()); + // await updateCoins(mwebUnspentCoins); // get regular ltc unspents (this resets unspentCoins): await super.updateAllUnspents(); // add the mwebCoins: @@ -1289,7 +1275,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }) async { final readyInputs = []; for (final utxo in utxos) { - final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final rawTx = + (await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex(); final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; readyInputs.add(LedgerTransaction( diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 4b77d984d..a8088f642 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,9 +1,10 @@ +import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; +import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; @@ -14,11 +15,10 @@ class PendingBitcoinTransaction with PendingTransaction { PendingBitcoinTransaction( this._tx, this.type, { - required this.electrumClient, + required this.sendWorker, required this.amount, required this.fee, required this.feeRate, - this.network, required this.hasChange, this.isSendAll = false, this.hasTaprootInputs = false, @@ -28,11 +28,10 @@ class PendingBitcoinTransaction with PendingTransaction { final WalletType type; final BtcTransaction _tx; - final ElectrumClient electrumClient; + Future Function(ElectrumWorkerRequest) sendWorker; final int amount; final int fee; final String feeRate; - final BasedUtxoNetwork? network; final bool isSendAll; final bool hasChange; final bool hasTaprootInputs; @@ -79,40 +78,39 @@ class PendingBitcoinTransaction with PendingTransaction { Future _commit() async { int? callId; - final result = await electrumClient.broadcastTransaction( - transactionRaw: hex, network: network, idCallback: (id) => callId = id); + final result = await sendWorker(ElectrumWorkerBroadcastRequest(transactionRaw: hex)) as String; - if (result.isEmpty) { - if (callId != null) { - final error = electrumClient.getErrorMessage(callId!); + // if (result.isEmpty) { + // if (callId != null) { + // final error = sendWorker(getErrorMessage(callId!)); - if (error.contains("dust")) { - if (hasChange) { - throw BitcoinTransactionCommitFailedDustChange(); - } else if (!isSendAll) { - throw BitcoinTransactionCommitFailedDustOutput(); - } else { - throw BitcoinTransactionCommitFailedDustOutputSendAll(); - } - } + // if (error.contains("dust")) { + // if (hasChange) { + // throw BitcoinTransactionCommitFailedDustChange(); + // } else if (!isSendAll) { + // throw BitcoinTransactionCommitFailedDustOutput(); + // } else { + // throw BitcoinTransactionCommitFailedDustOutputSendAll(); + // } + // } - if (error.contains("bad-txns-vout-negative")) { - throw BitcoinTransactionCommitFailedVoutNegative(); - } + // if (error.contains("bad-txns-vout-negative")) { + // throw BitcoinTransactionCommitFailedVoutNegative(); + // } - if (error.contains("non-BIP68-final")) { - throw BitcoinTransactionCommitFailedBIP68Final(); - } + // if (error.contains("non-BIP68-final")) { + // throw BitcoinTransactionCommitFailedBIP68Final(); + // } - if (error.contains("min fee not met")) { - throw BitcoinTransactionCommitFailedLessThanMin(); - } + // if (error.contains("min fee not met")) { + // throw BitcoinTransactionCommitFailedLessThanMin(); + // } - throw BitcoinTransactionCommitFailed(errorMessage: error); - } + // throw BitcoinTransactionCommitFailed(errorMessage: error); + // } - throw BitcoinTransactionCommitFailed(); - } + // throw BitcoinTransactionCommitFailed(); + // } } Future _ltcCommit() async { diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 5790159df..6b4a5da93 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -96,3 +96,58 @@ class LostConnectionSyncStatus extends NotConnectedSyncStatus { @override String toString() => 'Reconnecting'; } + +Map syncStatusToJson(SyncStatus? status) { + if (status == null) { + return {}; + } + return { + 'progress': status.progress(), + 'type': status.runtimeType.toString(), + 'data': status is SyncingSyncStatus + ? {'blocksLeft': status.blocksLeft, 'ptc': status.ptc} + : status is SyncedTipSyncStatus + ? {'tip': status.tip} + : status is FailedSyncStatus + ? {'error': status.error} + : status is StartingScanSyncStatus + ? {'beginHeight': status.beginHeight} + : null + }; +} + +SyncStatus syncStatusFromJson(Map json) { + final type = json['type'] as String; + final data = json['data'] as Map?; + + switch (type) { + case 'StartingScanSyncStatus': + return StartingScanSyncStatus(data!['beginHeight'] as int); + case 'SyncingSyncStatus': + return SyncingSyncStatus(data!['blocksLeft'] as int, data['ptc'] as double); + case 'SyncedTipSyncStatus': + return SyncedTipSyncStatus(data!['tip'] as int); + case 'FailedSyncStatus': + return FailedSyncStatus(error: data!['error'] as String?); + case 'SynchronizingSyncStatus': + return SynchronizingSyncStatus(); + case 'NotConnectedSyncStatus': + return NotConnectedSyncStatus(); + case 'AttemptingSyncStatus': + return AttemptingSyncStatus(); + case 'AttemptingScanSyncStatus': + return AttemptingScanSyncStatus(); + case 'ConnectedSyncStatus': + return ConnectedSyncStatus(); + case 'ConnectingSyncStatus': + return ConnectingSyncStatus(); + case 'UnsupportedSyncStatus': + return UnsupportedSyncStatus(); + case 'TimedOutSyncStatus': + return TimedOutSyncStatus(); + case 'LostConnectionSyncStatus': + return LostConnectionSyncStatus(); + default: + throw Exception('Unknown sync status type: $type'); + } +}