feat: tx history worker

This commit is contained in:
Rafael Saes 2024-11-01 17:31:26 -03:00
parent 02fabf8594
commit 4a4250a905
13 changed files with 904 additions and 681 deletions

View file

@ -24,7 +24,7 @@ abstract class BaseBitcoinAddressRecord {
bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address; bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address;
final String address; final String address;
final bool _isHidden; bool _isHidden;
bool get isHidden => _isHidden; bool get isHidden => _isHidden;
final bool _isChange; final bool _isChange;
bool get isChange => _isChange; bool get isChange => _isChange;
@ -46,7 +46,12 @@ abstract class BaseBitcoinAddressRecord {
bool get isUsed => _isUsed; bool get isUsed => _isUsed;
void setAsUsed() => _isUsed = true; void setAsUsed() {
_isUsed = true;
// TODO: check is hidden flow on addr list
_isHidden = true;
}
void setNewName(String label) => _name = label; void setNewName(String label) => _name = label;
int get hashCode => address.hashCode; int get hashCode => address.hashCode;
@ -119,6 +124,26 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
'type': type.toString(), 'type': type.toString(),
'scriptHash': scriptHash, 'scriptHash': scriptHash,
}); });
@override
operator ==(Object other) {
if (identical(this, other)) return true;
return other is BitcoinAddressRecord &&
other.address == address &&
other.index == index &&
other.derivationInfo == derivationInfo &&
other.scriptHash == scriptHash &&
other.type == type;
}
@override
int get hashCode =>
address.hashCode ^
index.hashCode ^
derivationInfo.hashCode ^
scriptHash.hashCode ^
type.hashCode;
} }
class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {

View file

@ -11,7 +11,7 @@ import 'package:cw_bitcoin/psbt_transaction_builder.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; // import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
@ -240,6 +240,36 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
); );
} }
Future<bool> getNodeSupportsSilentPayments() async {
return true;
// As of today (august 2024), only ElectrumRS supports silent payments
// if (!(await getNodeIsElectrs())) {
// return false;
// }
// if (node == null) {
// return false;
// }
// try {
// final tweaksResponse = await electrumClient.getTweaks(height: 0);
// if (tweaksResponse != null) {
// node!.supportsSilentPayments = true;
// node!.save();
// return node!.supportsSilentPayments!;
// }
// } on RequestFailedTimeoutException catch (_) {
// node!.supportsSilentPayments = false;
// node!.save();
// return node!.supportsSilentPayments!;
// } catch (_) {}
// node!.supportsSilentPayments = false;
// node!.save();
// return node!.supportsSilentPayments!;
}
LedgerConnection? _ledgerConnection; LedgerConnection? _ledgerConnection;
BitcoinLedgerApp? _bitcoinLedgerApp; BitcoinLedgerApp? _bitcoinLedgerApp;
@ -327,11 +357,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
_isolate?.then((value) => value.kill(priority: Isolate.immediate)); _isolate?.then((value) => value.kill(priority: Isolate.immediate));
if (rpc!.isConnected) { // if (rpc!.isConnected) {
syncStatus = SyncedSyncStatus(); // syncStatus = SyncedSyncStatus();
} else { // } else {
syncStatus = NotConnectedSyncStatus(); // syncStatus = NotConnectedSyncStatus();
} // }
} }
} }
@ -367,7 +397,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return; return;
} }
await updateCoins(unspentCoins); await updateCoins(unspentCoins.toSet());
await refreshUnspentCoinsInfo(); await refreshUnspentCoinsInfo();
} }
@ -449,6 +479,20 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
// } // }
// } // }
@action
Future<void> 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(),
);
print("registered: $registered");
}
@action @action
void _updateSilentAddressRecord(BitcoinUnspent unspent) { void _updateSilentAddressRecord(BitcoinUnspent unspent) {
final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord; final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord;
@ -593,41 +637,42 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
@override @override
@action @action
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async { Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
try { throw UnimplementedError();
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {}; // try {
// final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
await Future.wait( // await Future.wait(
BITCOIN_ADDRESS_TYPES.map( // BITCOIN_ADDRESS_TYPES.map(
(type) => fetchTransactionsForAddressType(historiesWithDetails, type), // (type) => fetchTransactionsForAddressType(historiesWithDetails, type),
), // ),
); // );
transactionHistory.transactions.values.forEach((tx) async { // transactionHistory.transactions.values.forEach((tx) async {
final isPendingSilentPaymentUtxo = // final isPendingSilentPaymentUtxo =
(tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null; // (tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null;
if (isPendingSilentPaymentUtxo) { // if (isPendingSilentPaymentUtxo) {
final info = await fetchTransactionInfo(hash: tx.id, height: tx.height); // final info = await fetchTransactionInfo(hash: tx.id, height: tx.height);
if (info != null) { // if (info != null) {
tx.confirmations = info.confirmations; // tx.confirmations = info.confirmations;
tx.isPending = tx.confirmations == 0; // tx.isPending = tx.confirmations == 0;
transactionHistory.addOne(tx); // transactionHistory.addOne(tx);
await transactionHistory.save(); // await transactionHistory.save();
} // }
} // }
}); // });
return historiesWithDetails; // return historiesWithDetails;
} catch (e) { // } catch (e) {
print("fetchTransactions $e"); // print("fetchTransactions $e");
return {}; // return {};
} // }
} }
@override @override
@action @action
Future<void> updateTransactions() async { Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async {
super.updateTransactions(); super.updateTransactions();
transactionHistory.transactions.values.forEach((tx) { transactionHistory.transactions.values.forEach((tx) {
@ -641,32 +686,32 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}); });
} }
@action // @action
Future<ElectrumBalance> fetchBalances() async { // Future<ElectrumBalance> fetchBalances() async {
final balance = await super.fetchBalances(); // final balance = await super.fetchBalances();
int totalFrozen = balance.frozen; // int totalFrozen = balance.frozen;
int totalConfirmed = balance.confirmed; // int totalConfirmed = balance.confirmed;
// Add values from unspent coins that are not fetched by the address list // // Add values from unspent coins that are not fetched by the address list
// i.e. scanned silent payments // // i.e. scanned silent payments
transactionHistory.transactions.values.forEach((tx) { // transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null) { // if (tx.unspents != null) {
tx.unspents!.forEach((unspent) { // tx.unspents!.forEach((unspent) {
if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { // if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
if (unspent.isFrozen) totalFrozen += unspent.value; // if (unspent.isFrozen) totalFrozen += unspent.value;
totalConfirmed += unspent.value; // totalConfirmed += unspent.value;
} // }
}); // });
} // }
}); // });
return ElectrumBalance( // return ElectrumBalance(
confirmed: totalConfirmed, // confirmed: totalConfirmed,
unconfirmed: balance.unconfirmed, // unconfirmed: balance.unconfirmed,
frozen: totalFrozen, // frozen: totalFrozen,
); // );
} // }
@override @override
@action @action
@ -713,15 +758,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
} }
} }
@override // @override
@action // @action
void onHeadersResponse(ElectrumHeaderResponse response) { // void onHeadersResponse(ElectrumHeaderResponse response) {
super.onHeadersResponse(response); // super.onHeadersResponse(response);
if (alwaysScan == true && syncStatus is SyncedSyncStatus) { // if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
_setListeners(walletInfo.restoreHeight); // _setListeners(walletInfo.restoreHeight);
} // }
} // }
@override @override
@action @action

View file

@ -2,7 +2,6 @@ import 'dart:io';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
@ -14,7 +13,6 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:bip39/bip39.dart' as bip39;
class BitcoinWalletService extends WalletService< class BitcoinWalletService extends WalletService<
BitcoinNewWalletCredentials, BitcoinNewWalletCredentials,
@ -172,10 +170,6 @@ class BitcoinWalletService extends WalletService<
@override @override
Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials, Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async { {bool? isTestnet}) async {
if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) {
throw BitcoinMnemonicIsIncorrectException();
}
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value; credentials.walletInfo?.network = network.value;

View file

@ -25,7 +25,7 @@ import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.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/node.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/pending_transaction.dart';
@ -35,13 +35,13 @@ import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/wallet_type.dart'; // import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coin_type.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger;
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:http/http.dart' as http; // import 'package:http/http.dart' as http;
part 'electrum_wallet.g.dart'; part 'electrum_wallet.g.dart';
@ -77,8 +77,8 @@ abstract class ElectrumWalletBase
_isTransactionUpdating = false, _isTransactionUpdating = false,
isEnabledAutoGenerateSubaddress = true, isEnabledAutoGenerateSubaddress = true,
// TODO: inital unspent coins // TODO: inital unspent coins
unspentCoins = ObservableSet(), unspentCoins = BitcoinUnspentCoins(),
scripthashesListening = {}, scripthashesListening = [],
balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null
? { ? {
currency: initialBalance ?? currency: initialBalance ??
@ -107,7 +107,7 @@ abstract class ElectrumWalletBase
} }
@action @action
void _handleWorkerResponse(dynamic message) { Future<void> _handleWorkerResponse(dynamic message) async {
print('Main: received message: $message'); print('Main: received message: $message');
Map<String, dynamic> messageJson; Map<String, dynamic> messageJson;
@ -146,15 +146,17 @@ abstract class ElectrumWalletBase
break; break;
case ElectrumRequestMethods.headersSubscribeMethod: case ElectrumRequestMethods.headersSubscribeMethod:
final response = ElectrumWorkerHeadersSubscribeResponse.fromJson(messageJson); final response = ElectrumWorkerHeadersSubscribeResponse.fromJson(messageJson);
onHeadersResponse(response.result); await onHeadersResponse(response.result);
break;
case ElectrumRequestMethods.getBalanceMethod:
final response = ElectrumWorkerGetBalanceResponse.fromJson(messageJson);
onBalanceResponse(response.result);
break;
case ElectrumRequestMethods.getHistoryMethod:
final response = ElectrumWorkerGetHistoryResponse.fromJson(messageJson);
onHistoriesResponse(response.result);
break; break;
// case 'fetchBalances':
// final balance = ElectrumBalance.fromJSON(
// jsonDecode(workerResponse.data.toString()).toString(),
// );
// Update the balance state
// this.balance[currency] = balance!;
// break;
} }
} }
@ -219,8 +221,6 @@ abstract class ElectrumWalletBase
bool isEnabledAutoGenerateSubaddress; bool isEnabledAutoGenerateSubaddress;
late ElectrumClient electrumClient; late ElectrumClient electrumClient;
ElectrumApiProvider? electrumClient2;
BitcoinBaseElectrumRPCService? get rpc => electrumClient2?.rpc;
ApiProvider? apiProvider; ApiProvider? apiProvider;
Box<UnspentCoinsInfo> unspentCoinsInfo; Box<UnspentCoinsInfo> unspentCoinsInfo;
@ -235,10 +235,10 @@ abstract class ElectrumWalletBase
@observable @observable
SyncStatus syncStatus; SyncStatus syncStatus;
Set<String> get addressesSet => walletAddresses.allAddresses List<String> get addressesSet => walletAddresses.allAddresses
.where((element) => element.type != SegwitAddresType.mweb) .where((element) => element.type != SegwitAddresType.mweb)
.map((addr) => addr.address) .map((addr) => addr.address)
.toSet(); .toList();
List<String> get scriptHashes => walletAddresses.addressesByReceiveType List<String> get scriptHashes => walletAddresses.addressesByReceiveType
.where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress)
@ -288,14 +288,14 @@ abstract class ElectrumWalletBase
); );
String _password; String _password;
ObservableSet<BitcoinUnspent> unspentCoins; BitcoinUnspentCoins unspentCoins;
@observable @observable
TransactionPriorities? feeRates; TransactionPriorities? feeRates;
int feeRate(TransactionPriority priority) => feeRates![priority]; int feeRate(TransactionPriority priority) => feeRates![priority];
@observable @observable
Set<String> scripthashesListening; List<String> scripthashesListening;
bool _chainTipListenerOn = false; bool _chainTipListenerOn = false;
bool _isTransactionUpdating; bool _isTransactionUpdating;
@ -323,16 +323,22 @@ abstract class ElectrumWalletBase
syncStatus = SynchronizingSyncStatus(); syncStatus = SynchronizingSyncStatus();
// INFO: FIRST: Call subscribe for headers, get the initial chainTip update in case it is zero
await subscribeForHeaders(); await subscribeForHeaders();
await subscribeForUpdates();
// await updateTransactions(); // 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(); // await updateAllUnspents();
// await updateBalance(); // INFO: THIRD: Start loading the TX history
await updateBalance();
// await subscribeForUpdates();
// await updateFeeRates(); // await updateFeeRates();
_updateFeeRateTimer ??= // _updateFeeRateTimer ??=
Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); // Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates());
syncStatus = SyncedSyncStatus(); syncStatus = SyncedSyncStatus();
@ -344,20 +350,6 @@ abstract class ElectrumWalletBase
} }
} }
@action
Future<void> 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(),
);
print("registered: $registered");
}
@action @action
void callError(FlutterErrorDetails error) { void callError(FlutterErrorDetails error) {
_onError?.call(error); _onError?.call(error);
@ -366,9 +358,9 @@ abstract class ElectrumWalletBase
@action @action
Future<void> updateFeeRates() async { Future<void> updateFeeRates() async {
try { try {
feeRates = BitcoinElectrumTransactionPriorities.fromList( // feeRates = BitcoinElectrumTransactionPriorities.fromList(
await electrumClient2!.getFeeRates(), // await electrumClient2!.getFeeRates(),
); // );
} catch (e, stacktrace) { } catch (e, stacktrace) {
// _onError?.call(FlutterErrorDetails( // _onError?.call(FlutterErrorDetails(
// exception: e, // exception: e,
@ -403,36 +395,6 @@ abstract class ElectrumWalletBase
return node!.isElectrs!; return node!.isElectrs!;
} }
Future<bool> getNodeSupportsSilentPayments() async {
return true;
// As of today (august 2024), only ElectrumRS supports silent payments
if (!(await getNodeIsElectrs())) {
return false;
}
if (node == null) {
return false;
}
try {
final tweaksResponse = await electrumClient.getTweaks(height: 0);
if (tweaksResponse != null) {
node!.supportsSilentPayments = true;
node!.save();
return node!.supportsSilentPayments!;
}
} on RequestFailedTimeoutException catch (_) {
node!.supportsSilentPayments = false;
node!.save();
return node!.supportsSilentPayments!;
} catch (_) {}
node!.supportsSilentPayments = false;
node!.save();
return node!.supportsSilentPayments!;
}
@action @action
@override @override
Future<void> connectToNode({required Node node}) async { Future<void> connectToNode({required Node node}) async {
@ -1176,7 +1138,7 @@ abstract class ElectrumWalletBase
final path = await makePath(); final path = await makePath();
await encryptionFileUtils.write(path: path, password: _password, data: toJSON()); await encryptionFileUtils.write(path: path, password: _password, data: toJSON());
// await transactionHistory.save(); await transactionHistory.save();
} }
@override @override
@ -1226,28 +1188,23 @@ abstract class ElectrumWalletBase
Future<void> updateAllUnspents() async { Future<void> updateAllUnspents() async {
List<BitcoinUnspent> updatedUnspentCoins = []; List<BitcoinUnspent> updatedUnspentCoins = [];
// Set the balance of all non-silent payment and non-mweb addresses to 0 before updating Set<String> scripthashes = {};
walletAddresses.allAddresses walletAddresses.allAddresses.forEach((addressRecord) {
.where((element) => element.type != SegwitAddresType.mweb) scripthashes.add(addressRecord.scriptHash);
.forEach((addr) {
if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0;
}); });
workerSendPort!.send(
ElectrumWorkerGetBalanceRequest(scripthashes: scripthashes).toJson(),
);
await Future.wait(walletAddresses.allAddresses await Future.wait(walletAddresses.allAddresses
.where((element) => element.type != SegwitAddresType.mweb) .where((element) => element.type != SegwitAddresType.mweb)
.map((address) async { .map((address) async {
updatedUnspentCoins.addAll(await fetchUnspent(address)); updatedUnspentCoins.addAll(await fetchUnspent(address));
})); }));
unspentCoins.addAll(updatedUnspentCoins); await updateCoins(unspentCoins.toSet());
await refreshUnspentCoinsInfo();
if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
unspentCoins.forEach((coin) => addCoinInfo(coin));
return;
}
await updateCoins(unspentCoins);
// await refreshUnspentCoinsInfo();
} }
@action @action
@ -1294,18 +1251,17 @@ abstract class ElectrumWalletBase
@action @action
Future<List<BitcoinUnspent>> fetchUnspent(BitcoinAddressRecord address) async { Future<List<BitcoinUnspent>> fetchUnspent(BitcoinAddressRecord address) async {
List<Map<String, dynamic>> unspents = [];
List<BitcoinUnspent> updatedUnspentCoins = []; List<BitcoinUnspent> updatedUnspentCoins = [];
final unspents = await electrumClient2!.request( unspents = await electrumClient.getListUnspent(address.scriptHash);
ElectrumScriptHashListUnspent(scriptHash: address.scriptHash),
);
await Future.wait(unspents.map((unspent) async { await Future.wait(unspents.map((unspent) async {
try { try {
final coin = BitcoinUnspent.fromUTXO(address, unspent); final coin = BitcoinUnspent.fromJSON(address, unspent);
final tx = await fetchTransactionInfo(hash: coin.hash); // final tx = await fetchTransactionInfo(hash: coin.hash);
coin.isChange = address.isChange; coin.isChange = address.isHidden;
coin.confirmations = tx?.confirmations; // coin.confirmations = tx?.confirmations;
updatedUnspentCoins.add(coin); updatedUnspentCoins.add(coin);
} catch (_) {} } catch (_) {}
@ -1332,6 +1288,7 @@ abstract class ElectrumWalletBase
await unspentCoinsInfo.add(newInfo); await unspentCoinsInfo.add(newInfo);
} }
// TODO: ?
Future<void> refreshUnspentCoinsInfo() async { Future<void> refreshUnspentCoinsInfo() async {
try { try {
final List<dynamic> keys = <dynamic>[]; final List<dynamic> keys = <dynamic>[];
@ -1415,7 +1372,7 @@ abstract class ElectrumWalletBase
final vout = input.txIndex; final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout]; final outTransaction = inputTransaction.outputs[vout];
final address = addressFromOutputScript(outTransaction.scriptPubKey, network); final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
allInputsAmount += outTransaction.amount.toInt(); // allInputsAmount += outTransaction.amount.toInt();
final addressRecord = final addressRecord =
walletAddresses.allAddresses.firstWhere((element) => element.address == address); walletAddresses.allAddresses.firstWhere((element) => element.address == address);
@ -1565,72 +1522,15 @@ abstract class ElectrumWalletBase
Future<ElectrumTransactionBundle> getTransactionExpanded({required String hash}) async { Future<ElectrumTransactionBundle> getTransactionExpanded({required String hash}) async {
int? time; int? time;
int? height; int? height;
final transactionHex = await electrumClient.getTransactionHex(hash: hash);
final transactionHex = await electrumClient2!.request(
ElectrumGetTransactionHex(transactionHash: hash),
);
// TODO:
// if (mempoolAPIEnabled) {
if (true) {
try {
final txVerbose = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status",
),
);
if (txVerbose.statusCode == 200 &&
txVerbose.body.isNotEmpty &&
jsonDecode(txVerbose.body) != null) {
height = jsonDecode(txVerbose.body)['block_height'] as int;
final blockHash = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block-height/$height",
),
);
if (blockHash.statusCode == 200 &&
blockHash.body.isNotEmpty &&
jsonDecode(blockHash.body) != null) {
final blockResponse = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}",
),
);
if (blockResponse.statusCode == 200 &&
blockResponse.body.isNotEmpty &&
jsonDecode(blockResponse.body)['timestamp'] != null) {
time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString());
}
}
}
} catch (_) {}
}
int? confirmations; int? confirmations;
if (height != null) {
if (time == null && height > 0) {
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round();
}
final tip = currentChainTip!;
if (tip > 0 && height > 0) {
// Add one because the block itself is the first confirmation
confirmations = tip - height + 1;
}
}
final original = BtcTransaction.fromRaw(transactionHex); final original = BtcTransaction.fromRaw(transactionHex);
final ins = <BtcTransaction>[]; final ins = <BtcTransaction>[];
for (final vin in original.inputs) { for (final vin in original.inputs) {
final inputTransactionHex = await electrumClient2!.request( final inputTransactionHex = await electrumClient.getTransactionHex(hash: hash);
ElectrumGetTransactionHex(transactionHash: vin.txId),
);
ins.add(BtcTransaction.fromRaw(inputTransactionHex)); ins.add(BtcTransaction.fromRaw(inputTransactionHex));
} }
@ -1643,207 +1543,62 @@ abstract class ElectrumWalletBase
); );
} }
Future<ElectrumTransactionInfo?> fetchTransactionInfo({required String hash, int? height}) async {
try {
return ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded(hash: hash),
walletInfo.type,
network,
addresses: addressesSet,
height: height,
);
} catch (e, s) {
print([e, s]);
return null;
}
}
@override @override
@action @action
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async { Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
try { throw UnimplementedError();
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
if (type == WalletType.bitcoinCash) {
await Future.wait(BITCOIN_CASH_ADDRESS_TYPES
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
} else if (type == WalletType.litecoin) {
await Future.wait(LITECOIN_ADDRESS_TYPES
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
} }
return historiesWithDetails; @action
} catch (e) { Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async {
print("fetchTransactions $e"); // TODO: all
return {}; addresses ??= walletAddresses.allAddresses
} .where(
} (element) => element.type == SegwitAddresType.p2wpkh && element.isChange == false,
)
.toList();
Future<void> fetchTransactionsForAddressType( workerSendPort!.send(
Map<String, ElectrumTransactionInfo> historiesWithDetails, ElectrumWorkerGetHistoryRequest(
BitcoinAddressType type, addresses: addresses,
) async { storedTxs: transactionHistory.transactions.values.toList(),
final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); walletType: type,
await Future.wait(addressesByType.map((addressRecord) async { // If we still don't have currentChainTip, txs will still be fetched but shown
final history = await _fetchAddressHistory(addressRecord); // with confirmations as 0 but will be auto fixed on onHeadersResponse
chainTip: currentChainTip ?? 0,
if (history.isNotEmpty) { network: network,
historiesWithDetails.addAll(history); // mempoolAPIEnabled: mempoolAPIEnabled,
} // TODO:
})); mempoolAPIEnabled: true,
} ).toJson(),
Future<Map<String, ElectrumTransactionInfo>> _fetchAddressHistory(
BitcoinAddressRecord addressRecord,
) async {
String txid = "";
try {
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
final history = await electrumClient2!.request(ElectrumScriptHashGetHistory(
scriptHash: addressRecord.scriptHash,
));
if (history.isNotEmpty) {
addressRecord.setAsUsed();
addressRecord.txCount = history.length;
await Future.wait(history.map((transaction) async {
txid = transaction['tx_hash'] as String;
final height = transaction['height'] as int;
final storedTx = transactionHistory.transactions[txid];
if (storedTx != null) {
if (height > 0) {
storedTx.height = height;
// the tx's block itself is the first confirmation so add 1
if ((currentChainTip ?? 0) > 0) {
storedTx.confirmations = currentChainTip! - height + 1;
}
storedTx.isPending = storedTx.confirmations == 0;
}
historiesWithDetails[txid] = storedTx;
} else {
final tx = await fetchTransactionInfo(hash: txid, height: height);
if (tx != null) {
historiesWithDetails[txid] = tx;
// Got a new transaction fetched, add it to the transaction history
// instead of waiting all to finish, and next time it will be faster
transactionHistory.addOne(tx);
}
}
return Future.value(null);
}));
final totalAddresses = (addressRecord.isChange
? walletAddresses.changeAddresses
.where((addr) => addr.type == addressRecord.type)
.length
: walletAddresses.receiveAddresses
.where((addr) => addr.type == addressRecord.type)
.length);
final gapLimit = (addressRecord.isChange
? ElectrumWalletAddressesBase.defaultChangeAddressesCount
: ElectrumWalletAddressesBase.defaultReceiveAddressesCount);
final isUsedAddressUnderGap = addressRecord.index < totalAddresses &&
(addressRecord.index >= totalAddresses - gapLimit);
if (isUsedAddressUnderGap) {
// Discover new addresses for the same address type until the gap limit is respected
await walletAddresses.discoverAddresses(
isChange: addressRecord.isChange,
gap: gapLimit,
type: addressRecord.type,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressRecord.type),
); );
} }
}
return historiesWithDetails;
} catch (e, stacktrace) {
_onError?.call(FlutterErrorDetails(
exception: "$txid - $e",
stack: stacktrace,
library: this.runtimeType.toString(),
));
return {};
}
}
@action @action
Future<void> updateTransactions() async { Future<void> subscribeForUpdates([Iterable<String>? unsubscribedScriptHashes]) async {
try { unsubscribedScriptHashes ??= walletAddresses.allScriptHashes.where(
if (_isTransactionUpdating) { (sh) => !scripthashesListening.contains(sh),
return;
}
_isTransactionUpdating = true;
await fetchTransactions();
walletAddresses.updateReceiveAddresses();
_isTransactionUpdating = false;
} catch (e, stacktrace) {
print(stacktrace);
print(e);
_isTransactionUpdating = false;
}
}
@action
Future<void> subscribeForUpdates([
Iterable<BitcoinAddressRecord>? unsubscribedScriptHashes,
]) async {
unsubscribedScriptHashes ??= walletAddresses.allAddresses.where(
(address) => !scripthashesListening.contains(address.scriptHash),
); );
Map<String, String> scripthashByAddress = {}; Map<String, String> scripthashByAddress = {};
List<String> scriptHashesList = [];
walletAddresses.allAddresses.forEach((addressRecord) { walletAddresses.allAddresses.forEach((addressRecord) {
scripthashByAddress[addressRecord.address] = addressRecord.scriptHash; scripthashByAddress[addressRecord.address] = addressRecord.scriptHash;
scriptHashesList.add(addressRecord.scriptHash);
}); });
workerSendPort!.send( workerSendPort!.send(
ElectrumWorkerScripthashesSubscribeRequest(scripthashByAddress: scripthashByAddress).toJson(), ElectrumWorkerScripthashesSubscribeRequest(
scripthashByAddress: scripthashByAddress,
).toJson(),
); );
scripthashesListening.addAll(scriptHashesList);
}
@action scripthashesListening.addAll(scripthashByAddress.values);
Future<ElectrumBalance> fetchBalances() async {
var totalFrozen = 0;
var totalConfirmed = 0;
var totalUnconfirmed = 0;
unspentCoins.forEach((element) {
if (element.isFrozen) {
totalFrozen += element.value;
}
if (element.confirmations == 0) {
totalUnconfirmed += element.value;
} else {
totalConfirmed += element.value;
}
});
return ElectrumBalance(
confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed,
frozen: totalFrozen,
);
} }
@action @action
Future<void> updateBalance() async { Future<void> updateBalance() async {
balance[currency] = await fetchBalances(); workerSendPort!.send(
ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes).toJson(),
);
} }
@override @override
@ -1925,12 +1680,102 @@ abstract class ElectrumWalletBase
} }
@action @action
void onHeadersResponse(ElectrumHeaderResponse response) { Future<void> onHistoriesResponse(List<AddressHistoriesResponse> histories) async {
final firstAddress = histories.first;
final isChange = firstAddress.addressRecord.isChange;
final type = firstAddress.addressRecord.type;
final totalAddresses = histories.length;
final gapLimit = (isChange
? ElectrumWalletAddressesBase.defaultChangeAddressesCount
: ElectrumWalletAddressesBase.defaultReceiveAddressesCount);
bool hasUsedAddressesUnderGap = false;
final addressesWithHistory = <BitcoinAddressRecord>[];
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,
gap: gapLimit,
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<void> onHeadersResponse(ElectrumHeaderResponse response) async {
currentChainTip = response.height; 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 @action
Future<void> subscribeForHeaders() async { Future<void> subscribeForHeaders() async {
print(_chainTipListenerOn);
if (_chainTipListenerOn) return; if (_chainTipListenerOn) return;
workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson()); workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson());
@ -1970,12 +1815,17 @@ abstract class ElectrumWalletBase
@action @action
void syncStatusReaction(SyncStatus syncStatus) { void syncStatusReaction(SyncStatus syncStatus) {
if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus) { final isDisconnectedStatus =
syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus;
if (syncStatus is ConnectingSyncStatus || isDisconnectedStatus) {
// Needs to re-subscribe to all scripthashes when reconnected // Needs to re-subscribe to all scripthashes when reconnected
scripthashesListening = {}; scripthashesListening = [];
_isTransactionUpdating = false; _isTransactionUpdating = false;
_chainTipListenerOn = false; _chainTipListenerOn = false;
}
if (isDisconnectedStatus) {
if (_isTryingToConnect) return; if (_isTryingToConnect) return;
_isTryingToConnect = true; _isTryingToConnect = true;
@ -1985,10 +1835,7 @@ abstract class ElectrumWalletBase
this.syncStatus is LostConnectionSyncStatus) { this.syncStatus is LostConnectionSyncStatus) {
if (node == null) return; if (node == null) return;
this.electrumClient.connectToUri( connectToNode(node: this.node!);
node!.uri,
useSSL: node!.useSSL ?? false,
);
} }
_isTryingToConnect = false; _isTryingToConnect = false;
}); });
@ -2102,3 +1949,35 @@ class TxCreateUtxoDetails {
required this.spendsUnconfirmedTX, required this.spendsUnconfirmedTX,
}); });
} }
class BitcoinUnspentCoins extends ObservableList<BitcoinUnspent> {
BitcoinUnspentCoins() : super();
List<UnspentCoinsInfo> forInfo(Iterable<UnspentCoinsInfo> unspentCoinsInfo) {
return unspentCoinsInfo.where((element) {
final info = this.firstWhereOrNull(
(info) =>
element.hash == info.hash &&
element.vout == info.vout &&
element.address == info.bitcoinAddressRecord.address &&
element.value == info.value,
);
return info != null;
}).toList();
}
List<BitcoinUnspent> fromInfo(Iterable<UnspentCoinsInfo> unspentCoinsInfo) {
return this.where((element) {
final info = unspentCoinsInfo.firstWhereOrNull(
(info) =>
element.hash == info.hash &&
element.vout == info.vout &&
element.bitcoinAddressRecord.address == info.address &&
element.value == info.value,
);
return info != null;
}).toList();
}
}

View file

@ -43,7 +43,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
int initialSilentAddressIndex = 0, int initialSilentAddressIndex = 0,
List<BitcoinAddressRecord>? initialMwebAddresses, List<BitcoinAddressRecord>? initialMwebAddresses,
BitcoinAddressType? initialAddressPageType, BitcoinAddressType? initialAddressPageType,
}) : _allAddresses = ObservableSet.of(initialAddresses ?? []), }) : _allAddresses = ObservableList.of(initialAddresses ?? []),
addressesByReceiveType = addressesByReceiveType =
ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()), ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
receiveAddresses = ObservableList<BitcoinAddressRecord>.of( receiveAddresses = ObservableList<BitcoinAddressRecord>.of(
@ -89,7 +89,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
static const defaultChangeAddressesCount = 17; static const defaultChangeAddressesCount = 17;
static const gap = 20; static const gap = 20;
final ObservableSet<BitcoinAddressRecord> _allAddresses; final ObservableList<BitcoinAddressRecord> _allAddresses;
final ObservableList<BaseBitcoinAddressRecord> addressesByReceiveType; final ObservableList<BaseBitcoinAddressRecord> addressesByReceiveType;
final ObservableList<BitcoinAddressRecord> receiveAddresses; final ObservableList<BitcoinAddressRecord> receiveAddresses;
final ObservableList<BitcoinAddressRecord> changeAddresses; final ObservableList<BitcoinAddressRecord> changeAddresses;
@ -116,6 +116,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@computed @computed
List<BitcoinAddressRecord> get allAddresses => _allAddresses.toList(); List<BitcoinAddressRecord> get allAddresses => _allAddresses.toList();
@computed
Set<String> get allScriptHashes =>
_allAddresses.map((addressRecord) => addressRecord.scriptHash).toSet();
BitcoinAddressRecord getFromAddresses(String address) { BitcoinAddressRecord getFromAddresses(String address) {
return _allAddresses.firstWhere((element) => element.address == address); return _allAddresses.firstWhere((element) => element.address == address);
} }
@ -629,6 +633,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return list; return list;
} }
@action
void updateAdresses(Iterable<BitcoinAddressRecord> addresses) {
for (final address in addresses) {
_allAddresses.replaceRange(address.index, address.index + 1, [address]);
}
}
@action @action
void addAddresses(Iterable<BitcoinAddressRecord> addresses) { void addAddresses(Iterable<BitcoinAddressRecord> addresses) {
this._allAddresses.addAll(addresses); this._allAddresses.addAll(addresses);

View file

@ -1,136 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.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';
class ElectrumWorker {
final SendPort sendPort;
ElectrumApiProvider? _electrumClient;
ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient})
: _electrumClient = electrumClient;
static void run(SendPort sendPort) {
final worker = ElectrumWorker._(sendPort);
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen(worker.handleMessage);
}
void _sendResponse(ElectrumWorkerResponse response) {
sendPort.send(jsonEncode(response.toJson()));
}
void _sendError(ElectrumWorkerErrorResponse response) {
sendPort.send(jsonEncode(response.toJson()));
}
void handleMessage(dynamic message) async {
print("Worker: received message: $message");
try {
Map<String, dynamic> messageJson;
if (message is String) {
messageJson = jsonDecode(message) as Map<String, dynamic>;
} else {
messageJson = message as Map<String, dynamic>;
}
final workerMethod = messageJson['method'] as String;
switch (workerMethod) {
case ElectrumWorkerMethods.connectionMethod:
await _handleConnect(ElectrumWorkerConnectRequest.fromJson(messageJson));
break;
// case 'blockchain.headers.subscribe':
// await _handleHeadersSubscribe();
// break;
// case 'blockchain.scripthash.get_balance':
// await _handleGetBalance(message);
// break;
case 'blockchain.scripthash.get_history':
// await _handleGetHistory(workerMessage);
break;
case 'blockchain.scripthash.listunspent':
// await _handleListUnspent(workerMessage);
break;
// Add other method handlers here
// default:
// _sendError(workerMethod, 'Unsupported method: ${workerMessage.method}');
}
} catch (e, s) {
print(s);
_sendError(ElectrumWorkerErrorResponse(error: e.toString()));
}
}
Future<void> _handleConnect(ElectrumWorkerConnectRequest request) async {
_electrumClient = ElectrumApiProvider(
await ElectrumTCPService.connect(
request.uri,
onConnectionStatusChange: (status) {
_sendResponse(ElectrumWorkerConnectResponse(status: status.toString()));
},
defaultRequestTimeOut: const Duration(seconds: 5),
connectionTimeOut: const Duration(seconds: 5),
),
);
}
// Future<void> _handleHeadersSubscribe() async {
// final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe());
// if (listener == null) {
// _sendError('blockchain.headers.subscribe', 'Failed to subscribe');
// return;
// }
// listener((event) {
// _sendResponse('blockchain.headers.subscribe', event);
// });
// }
// Future<void> _handleGetBalance(ElectrumWorkerRequest message) async {
// try {
// final scriptHash = message.params['scriptHash'] as String;
// final result = await _electrumClient!.request(
// ElectrumGetScriptHashBalance(scriptHash: scriptHash),
// );
// final balance = ElectrumBalance(
// confirmed: result['confirmed'] as int? ?? 0,
// unconfirmed: result['unconfirmed'] as int? ?? 0,
// frozen: 0,
// );
// _sendResponse(message.method, balance.toJSON());
// } catch (e, s) {
// print(s);
// _sendError(message.method, e.toString());
// }
// }
// Future<void> _handleGetHistory(ElectrumWorkerMessage message) async {
// try {
// final scriptHash = message.params['scriptHash'] as String;
// final result = await electrumClient.getHistory(scriptHash);
// _sendResponse(message.method, jsonEncode(result));
// } catch (e) {
// _sendError(message.method, e.toString());
// }
// }
// Future<void> _handleListUnspent(ElectrumWorkerMessage message) async {
// try {
// final scriptHash = message.params['scriptHash'] as String;
// final result = await electrumClient.listUnspent(scriptHash);
// _sendResponse(message.method, jsonEncode(result));
// } catch (e) {
// _sendError(message.method, e.toString());
// }
// }
}

View file

@ -3,10 +3,16 @@ import 'dart:convert';
import 'dart:isolate'; import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.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_worker/electrum_worker_methods.dart';
// import 'package:cw_bitcoin/electrum_balance.dart'; // import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:http/http.dart' as http;
// TODO: ping
class ElectrumWorker { class ElectrumWorker {
final SendPort sendPort; final SendPort sendPort;
@ -58,11 +64,15 @@ class ElectrumWorker {
ElectrumWorkerScripthashesSubscribeRequest.fromJson(messageJson), ElectrumWorkerScripthashesSubscribeRequest.fromJson(messageJson),
); );
break; break;
// case 'blockchain.scripthash.get_balance': case ElectrumRequestMethods.getBalanceMethod:
// await _handleGetBalance(message); await _handleGetBalance(
// break; ElectrumWorkerGetBalanceRequest.fromJson(messageJson),
case 'blockchain.scripthash.get_history': );
// await _handleGetHistory(workerMessage); break;
case ElectrumRequestMethods.getHistoryMethod:
await _handleGetHistory(
ElectrumWorkerGetHistoryRequest.fromJson(messageJson),
);
break; break;
case 'blockchain.scripthash.listunspent': case 'blockchain.scripthash.listunspent':
// await _handleListUnspent(workerMessage); // await _handleListUnspent(workerMessage);
@ -108,6 +118,7 @@ class ElectrumWorker {
await Future.wait(request.scripthashByAddress.entries.map((entry) async { await Future.wait(request.scripthashByAddress.entries.map((entry) async {
final address = entry.key; final address = entry.key;
final scripthash = entry.value; final scripthash = entry.value;
final listener = await _electrumClient!.subscribe( final listener = await _electrumClient!.subscribe(
ElectrumScriptHashSubscribe(scriptHash: scripthash), ElectrumScriptHashSubscribe(scriptHash: scripthash),
); );
@ -129,43 +140,214 @@ class ElectrumWorker {
})); }));
} }
// Future<void> _handleGetBalance(ElectrumWorkerRequest message) async { Future<void> _handleGetHistory(ElectrumWorkerGetHistoryRequest result) async {
// try { final Map<String, AddressHistoriesResponse> histories = {};
// final scriptHash = message.params['scriptHash'] as String; final addresses = result.addresses;
// final result = await _electrumClient!.request(
// ElectrumGetScriptHashBalance(scriptHash: scriptHash),
// );
// final balance = ElectrumBalance( await Future.wait(addresses.map((addressRecord) async {
// confirmed: result['confirmed'] as int? ?? 0, final history = await _electrumClient!.request(ElectrumScriptHashGetHistory(
// unconfirmed: result['unconfirmed'] as int? ?? 0, scriptHash: addressRecord.scriptHash,
// frozen: 0, ));
// );
// _sendResponse(message.method, balance.toJSON()); if (history.isNotEmpty) {
// } catch (e, s) { addressRecord.setAsUsed();
// print(s); addressRecord.txCount = history.length;
// _sendError(message.method, e.toString());
// }
// }
// Future<void> _handleGetHistory(ElectrumWorkerMessage message) async { await Future.wait(history.map((transaction) async {
// try { final txid = transaction['tx_hash'] as String;
// final scriptHash = message.params['scriptHash'] as String; final height = transaction['height'] as int;
// final result = await electrumClient.getHistory(scriptHash); late ElectrumTransactionInfo tx;
// _sendResponse(message.method, jsonEncode(result));
// } catch (e) {
// _sendError(message.method, e.toString());
// }
// }
// Future<void> _handleListUnspent(ElectrumWorkerMessage message) async { try {
// try { // Exception thrown on null
// final scriptHash = message.params['scriptHash'] as String; tx = result.storedTxs.firstWhere((tx) => tx.id == txid);
// final result = await electrumClient.listUnspent(scriptHash);
// _sendResponse(message.method, jsonEncode(result)); if (height > 0) {
// } catch (e) { tx.height = height;
// _sendError(message.method, e.toString());
// } // the tx's block itself is the first confirmation so add 1
// } tx.confirmations = result.chainTip - height + 1;
tx.isPending = tx.confirmations == 0;
}
} catch (_) {
tx = ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded(
hash: txid,
currentChainTip: result.chainTip,
mempoolAPIEnabled: result.mempoolAPIEnabled,
),
result.walletType,
result.network,
addresses: result.addresses.map((addr) => addr.address).toSet(),
height: height,
);
}
final addressHistories = histories[addressRecord.address];
if (addressHistories != null) {
addressHistories.txs.add(tx);
} else {
histories[addressRecord.address] = AddressHistoriesResponse(
addressRecord: addressRecord,
txs: [tx],
walletType: result.walletType,
);
}
return Future.value(null);
}));
}
return histories;
}));
_sendResponse(ElectrumWorkerGetHistoryResponse(result: histories.values.toList()));
}
Future<ElectrumTransactionBundle> getTransactionExpanded({
required String hash,
required int currentChainTip,
required bool mempoolAPIEnabled,
bool getConfirmations = true,
}) async {
int? time;
int? height;
int? confirmations;
final transactionHex = await _electrumClient!.request(
ElectrumGetTransactionHex(transactionHash: hash),
);
if (getConfirmations) {
if (mempoolAPIEnabled) {
try {
final txVerbose = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status",
),
);
if (txVerbose.statusCode == 200 &&
txVerbose.body.isNotEmpty &&
jsonDecode(txVerbose.body) != null) {
height = jsonDecode(txVerbose.body)['block_height'] as int;
final blockHash = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block-height/$height",
),
);
if (blockHash.statusCode == 200 &&
blockHash.body.isNotEmpty &&
jsonDecode(blockHash.body) != null) {
final blockResponse = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}",
),
);
if (blockResponse.statusCode == 200 &&
blockResponse.body.isNotEmpty &&
jsonDecode(blockResponse.body)['timestamp'] != null) {
time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString());
}
}
}
} catch (_) {}
}
if (height != null) {
if (time == null && height > 0) {
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round();
}
final tip = currentChainTip;
if (tip > 0 && height > 0) {
// Add one because the block itself is the first confirmation
confirmations = tip - height + 1;
}
}
}
final original = BtcTransaction.fromRaw(transactionHex);
final ins = <BtcTransaction>[];
for (final vin in original.inputs) {
final inputTransactionHex = await _electrumClient!.request(
ElectrumGetTransactionHex(transactionHash: vin.txId),
);
ins.add(BtcTransaction.fromRaw(inputTransactionHex));
}
return ElectrumTransactionBundle(
original,
ins: ins,
time: time,
confirmations: confirmations ?? 0,
);
}
// Future<void> _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async {
// final balanceFutures = <Future<Map<String, dynamic>>>[];
// 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<void> _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async {
final balanceFutures = <Future<Map<String, dynamic>>>[];
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,
),
));
}
} }

View file

@ -0,0 +1,52 @@
part of 'methods.dart';
class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
ElectrumWorkerGetBalanceRequest({required this.scripthashes});
final Set<String> scripthashes;
@override
final String method = ElectrumRequestMethods.getBalance.method;
@override
factory ElectrumWorkerGetBalanceRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetBalanceRequest(
scripthashes: (json['scripthashes'] as List<String>).toSet(),
);
}
@override
Map<String, dynamic> 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<ElectrumBalance, Map<String, int>?> {
ElectrumWorkerGetBalanceResponse({required super.result, super.error})
: super(method: ElectrumRequestMethods.getBalance.method);
@override
Map<String, int>? resultJson(result) {
return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed};
}
@override
factory ElectrumWorkerGetBalanceResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetBalanceResponse(
result: ElectrumBalance(
confirmed: json['result']['confirmed'] as int,
unconfirmed: json['result']['unconfirmed'] as int,
frozen: 0,
),
error: json['error'] as String?,
);
}
}

View file

@ -0,0 +1,110 @@
part of 'methods.dart';
class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
ElectrumWorkerGetHistoryRequest({
required this.addresses,
required this.storedTxs,
required this.walletType,
required this.chainTip,
required this.network,
required this.mempoolAPIEnabled,
});
final List<BitcoinAddressRecord> addresses;
final List<ElectrumTransactionInfo> storedTxs;
final WalletType walletType;
final int chainTip;
final BasedUtxoNetwork network;
final bool mempoolAPIEnabled;
@override
final String method = ElectrumRequestMethods.getHistory.method;
@override
factory ElectrumWorkerGetHistoryRequest.fromJson(Map<String, dynamic> json) {
final walletType = WalletType.values[json['walletType'] as int];
return ElectrumWorkerGetHistoryRequest(
addresses: (json['addresses'] as List)
.map((e) => BitcoinAddressRecord.fromJSON(e as String))
.toList(),
storedTxs: (json['storedTxIds'] as List)
.map((e) => ElectrumTransactionInfo.fromJson(e as Map<String, dynamic>, walletType))
.toList(),
walletType: walletType,
chainTip: json['chainTip'] as int,
network: BasedUtxoNetwork.fromName(json['network'] as String),
mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool,
);
}
@override
Map<String, dynamic> toJson() {
return {
'method': method,
'addresses': addresses.map((e) => e.toJSON()).toList(),
'storedTxIds': storedTxs.map((e) => e.toJson()).toList(),
'walletType': walletType.index,
'chainTip': chainTip,
'network': network.value,
'mempoolAPIEnabled': mempoolAPIEnabled,
};
}
}
class ElectrumWorkerGetHistoryError extends ElectrumWorkerErrorResponse {
ElectrumWorkerGetHistoryError({required String error}) : super(error: error);
@override
final String method = ElectrumRequestMethods.getHistory.method;
}
class AddressHistoriesResponse {
final BitcoinAddressRecord addressRecord;
final List<ElectrumTransactionInfo> txs;
final WalletType walletType;
AddressHistoriesResponse(
{required this.addressRecord, required this.txs, required this.walletType});
factory AddressHistoriesResponse.fromJson(Map<String, dynamic> json) {
final walletType = WalletType.values[json['walletType'] as int];
return AddressHistoriesResponse(
addressRecord: BitcoinAddressRecord.fromJSON(json['address'] as String),
txs: (json['txs'] as List)
.map((e) => ElectrumTransactionInfo.fromJson(e as Map<String, dynamic>, walletType))
.toList(),
walletType: walletType,
);
}
Map<String, dynamic> toJson() {
return {
'address': addressRecord.toJSON(),
'txs': txs.map((e) => e.toJson()).toList(),
'walletType': walletType.index,
};
}
}
class ElectrumWorkerGetHistoryResponse
extends ElectrumWorkerResponse<List<AddressHistoriesResponse>, List<Map<String, dynamic>>> {
ElectrumWorkerGetHistoryResponse({required super.result, super.error})
: super(method: ElectrumRequestMethods.getHistory.method);
@override
List<Map<String, dynamic>> resultJson(result) {
return result.map((e) => e.toJson()).toList();
}
@override
factory ElectrumWorkerGetHistoryResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetHistoryResponse(
result: (json['result'] as List)
.map((e) => AddressHistoriesResponse.fromJson(e as Map<String, dynamic>))
.toList(),
error: json['error'] as String?,
);
}
}

View file

@ -0,0 +1,53 @@
// part of 'methods.dart';
// class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
// ElectrumWorkerGetBalanceRequest({required this.scripthashes});
// final Set<String> scripthashes;
// @override
// final String method = ElectrumRequestMethods.getBalance.method;
// @override
// factory ElectrumWorkerGetBalanceRequest.fromJson(Map<String, dynamic> json) {
// return ElectrumWorkerGetBalanceRequest(
// scripthashes: (json['scripthashes'] as List<String>).toSet(),
// );
// }
// @override
// Map<String, dynamic> 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<ElectrumBalance, Map<String, int>?> {
// ElectrumWorkerGetBalanceResponse({required super.result, super.error})
// : super(method: ElectrumRequestMethods.getBalance.method);
// @override
// Map<String, int>? resultJson(result) {
// return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed};
// }
// @override
// factory ElectrumWorkerGetBalanceResponse.fromJson(Map<String, dynamic> json) {
// return ElectrumWorkerGetBalanceResponse(
// result: ElectrumBalance(
// confirmed: json['result']['confirmed'] as int,
// unconfirmed: json['result']['unconfirmed'] as int,
// frozen: 0,
// ),
// error: json['error'] as String?,
// );
// }
// }

View file

@ -1,6 +1,13 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.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/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
part 'connection.dart'; part 'connection.dart';
part 'headers_subscribe.dart'; part 'headers_subscribe.dart';
part 'scripthashes_subscribe.dart'; part 'scripthashes_subscribe.dart';
part 'get_balance.dart';
part 'get_history.dart';

View file

@ -774,113 +774,114 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@override @override
@action @action
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async { Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
try { throw UnimplementedError();
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {}; // try {
// final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
await Future.wait(LITECOIN_ADDRESS_TYPES // await Future.wait(LITECOIN_ADDRESS_TYPES
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); // .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
return historiesWithDetails; // return historiesWithDetails;
} catch (e) { // } catch (e) {
print("fetchTransactions $e"); // print("fetchTransactions $e");
return {}; // return {};
} // }
} }
@override // @override
@action // @action
Future<void> subscribeForUpdates([ // Future<void> subscribeForUpdates([
Iterable<BitcoinAddressRecord>? unsubscribedScriptHashes, // Iterable<BitcoinAddressRecord>? unsubscribedScriptHashes,
]) async { // ]) async {
final unsubscribedScriptHashes = walletAddresses.allAddresses.where( // final unsubscribedScriptHashes = walletAddresses.allAddresses.where(
(address) => // (address) =>
!scripthashesListening.contains(address.scriptHash) && // !scripthashesListening.contains(address.scriptHash) &&
address.type != SegwitAddresType.mweb, // address.type != SegwitAddresType.mweb,
); // );
return super.subscribeForUpdates(unsubscribedScriptHashes); // return super.subscribeForUpdates(unsubscribedScriptHashes);
} // }
@override // @override
Future<ElectrumBalance> fetchBalances() async { // Future<ElectrumBalance> fetchBalances() async {
final balance = await super.fetchBalances(); // final balance = await super.fetchBalances();
if (!mwebEnabled) { // if (!mwebEnabled) {
return balance; // return balance;
} // }
// update unspent balances: // // update unspent balances:
await updateUnspent(); // await updateUnspent();
int confirmed = balance.confirmed; // int confirmed = balance.confirmed;
int unconfirmed = balance.unconfirmed; // int unconfirmed = balance.unconfirmed;
int confirmedMweb = 0; // int confirmedMweb = 0;
int unconfirmedMweb = 0; // int unconfirmedMweb = 0;
try { // try {
mwebUtxosBox.values.forEach((utxo) { // mwebUtxosBox.values.forEach((utxo) {
if (utxo.height > 0) { // if (utxo.height > 0) {
confirmedMweb += utxo.value.toInt(); // confirmedMweb += utxo.value.toInt();
} else { // } else {
unconfirmedMweb += utxo.value.toInt(); // unconfirmedMweb += utxo.value.toInt();
} // }
}); // });
if (unconfirmedMweb > 0) { // if (unconfirmedMweb > 0) {
unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb); // unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
} // }
} catch (_) {} // } catch (_) {}
for (var addressRecord in walletAddresses.allAddresses) { // for (var addressRecord in walletAddresses.allAddresses) {
addressRecord.balance = 0; // addressRecord.balance = 0;
addressRecord.txCount = 0; // addressRecord.txCount = 0;
} // }
unspentCoins.forEach((coin) { // unspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values.where( // final coinInfoList = unspentCoinsInfo.values.where(
(element) => // (element) =>
element.walletId.contains(id) && // element.walletId.contains(id) &&
element.hash.contains(coin.hash) && // element.hash.contains(coin.hash) &&
element.vout == coin.vout, // element.vout == coin.vout,
); // );
if (coinInfoList.isNotEmpty) { // if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first; // final coinInfo = coinInfoList.first;
coin.isFrozen = coinInfo.isFrozen; // coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending; // coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note; // coin.note = coinInfo.note;
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) // if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance += coinInfo.value; // coin.bitcoinAddressRecord.balance += coinInfo.value;
} else { // } else {
super.addCoinInfo(coin); // super.addCoinInfo(coin);
} // }
}); // });
// update the txCount for each address using the tx history, since we can't rely on mwebd // // update the txCount for each address using the tx history, since we can't rely on mwebd
// to have an accurate count, we should just keep it in sync with what we know from the tx history: // // to have an accurate count, we should just keep it in sync with what we know from the tx history:
for (final tx in transactionHistory.transactions.values) { // for (final tx in transactionHistory.transactions.values) {
// if (tx.isPending) continue; // // if (tx.isPending) continue;
if (tx.inputAddresses == null || tx.outputAddresses == null) { // if (tx.inputAddresses == null || tx.outputAddresses == null) {
continue; // continue;
} // }
final txAddresses = tx.inputAddresses! + tx.outputAddresses!; // final txAddresses = tx.inputAddresses! + tx.outputAddresses!;
for (final address in txAddresses) { // for (final address in txAddresses) {
final addressRecord = walletAddresses.allAddresses // final addressRecord = walletAddresses.allAddresses
.firstWhereOrNull((addressRecord) => addressRecord.address == address); // .firstWhereOrNull((addressRecord) => addressRecord.address == address);
if (addressRecord == null) { // if (addressRecord == null) {
continue; // continue;
} // }
addressRecord.txCount++; // addressRecord.txCount++;
} // }
} // }
return ElectrumBalance( // return ElectrumBalance(
confirmed: confirmed, // confirmed: confirmed,
unconfirmed: unconfirmed, // unconfirmed: unconfirmed,
frozen: balance.frozen, // frozen: balance.frozen,
secondConfirmed: confirmedMweb, // secondConfirmed: confirmedMweb,
secondUnconfirmed: unconfirmedMweb, // secondUnconfirmed: unconfirmedMweb,
); // );
} // }
@override @override
int feeRate(TransactionPriority priority) { int feeRate(TransactionPriority priority) {

View file

@ -603,7 +603,7 @@ class CWBitcoin extends Bitcoin {
@override @override
Future<void> registerSilentPaymentsKey(Object wallet, bool active) async { Future<void> registerSilentPaymentsKey(Object wallet, bool active) async {
final bitcoinWallet = wallet as ElectrumWallet; final bitcoinWallet = wallet as BitcoinWallet;
return await bitcoinWallet.registerSilentPaymentsKey(); return await bitcoinWallet.registerSilentPaymentsKey();
} }
@ -634,7 +634,7 @@ class CWBitcoin extends Bitcoin {
@override @override
Future<bool> getNodeIsElectrsSPEnabled(Object wallet) async { Future<bool> getNodeIsElectrsSPEnabled(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet; final bitcoinWallet = wallet as BitcoinWallet;
return bitcoinWallet.getNodeSupportsSilentPayments(); return bitcoinWallet.getNodeSupportsSilentPayments();
} }