mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-23 03:59:23 +00:00
feat: tx history worker
This commit is contained in:
parent
02fabf8594
commit
4a4250a905
13 changed files with 904 additions and 681 deletions
|
@ -24,7 +24,7 @@ abstract class BaseBitcoinAddressRecord {
|
|||
bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address;
|
||||
|
||||
final String address;
|
||||
final bool _isHidden;
|
||||
bool _isHidden;
|
||||
bool get isHidden => _isHidden;
|
||||
final bool _isChange;
|
||||
bool get isChange => _isChange;
|
||||
|
@ -46,7 +46,12 @@ abstract class BaseBitcoinAddressRecord {
|
|||
|
||||
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;
|
||||
|
||||
int get hashCode => address.hashCode;
|
||||
|
@ -119,6 +124,26 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
|
|||
'type': type.toString(),
|
||||
'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 {
|
||||
|
|
|
@ -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_unspent.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_bitcoin/electrum_derivations.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;
|
||||
BitcoinLedgerApp? _bitcoinLedgerApp;
|
||||
|
||||
|
@ -327,11 +357,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
|
||||
_isolate?.then((value) => value.kill(priority: Isolate.immediate));
|
||||
|
||||
if (rpc!.isConnected) {
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} else {
|
||||
syncStatus = NotConnectedSyncStatus();
|
||||
}
|
||||
// if (rpc!.isConnected) {
|
||||
// syncStatus = SyncedSyncStatus();
|
||||
// } else {
|
||||
// syncStatus = NotConnectedSyncStatus();
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -367,7 +397,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
return;
|
||||
}
|
||||
|
||||
await updateCoins(unspentCoins);
|
||||
await updateCoins(unspentCoins.toSet());
|
||||
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
|
||||
void _updateSilentAddressRecord(BitcoinUnspent unspent) {
|
||||
final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord;
|
||||
|
@ -593,41 +637,42 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
@override
|
||||
@action
|
||||
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
|
||||
try {
|
||||
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
|
||||
throw UnimplementedError();
|
||||
// try {
|
||||
// final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
|
||||
|
||||
await Future.wait(
|
||||
BITCOIN_ADDRESS_TYPES.map(
|
||||
(type) => fetchTransactionsForAddressType(historiesWithDetails, type),
|
||||
),
|
||||
);
|
||||
// await Future.wait(
|
||||
// BITCOIN_ADDRESS_TYPES.map(
|
||||
// (type) => fetchTransactionsForAddressType(historiesWithDetails, type),
|
||||
// ),
|
||||
// );
|
||||
|
||||
transactionHistory.transactions.values.forEach((tx) async {
|
||||
final isPendingSilentPaymentUtxo =
|
||||
(tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null;
|
||||
// transactionHistory.transactions.values.forEach((tx) async {
|
||||
// final isPendingSilentPaymentUtxo =
|
||||
// (tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null;
|
||||
|
||||
if (isPendingSilentPaymentUtxo) {
|
||||
final info = await fetchTransactionInfo(hash: tx.id, height: tx.height);
|
||||
// if (isPendingSilentPaymentUtxo) {
|
||||
// final info = await fetchTransactionInfo(hash: tx.id, height: tx.height);
|
||||
|
||||
if (info != null) {
|
||||
tx.confirmations = info.confirmations;
|
||||
tx.isPending = tx.confirmations == 0;
|
||||
transactionHistory.addOne(tx);
|
||||
await transactionHistory.save();
|
||||
}
|
||||
}
|
||||
});
|
||||
// if (info != null) {
|
||||
// tx.confirmations = info.confirmations;
|
||||
// tx.isPending = tx.confirmations == 0;
|
||||
// transactionHistory.addOne(tx);
|
||||
// await transactionHistory.save();
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
return historiesWithDetails;
|
||||
} catch (e) {
|
||||
print("fetchTransactions $e");
|
||||
return {};
|
||||
}
|
||||
// return historiesWithDetails;
|
||||
// } catch (e) {
|
||||
// print("fetchTransactions $e");
|
||||
// return {};
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<void> updateTransactions() async {
|
||||
Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async {
|
||||
super.updateTransactions();
|
||||
|
||||
transactionHistory.transactions.values.forEach((tx) {
|
||||
|
@ -641,32 +686,32 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
Future<ElectrumBalance> fetchBalances() async {
|
||||
final balance = await super.fetchBalances();
|
||||
// @action
|
||||
// Future<ElectrumBalance> fetchBalances() async {
|
||||
// final balance = await super.fetchBalances();
|
||||
|
||||
int totalFrozen = balance.frozen;
|
||||
int totalConfirmed = balance.confirmed;
|
||||
// int totalFrozen = balance.frozen;
|
||||
// int totalConfirmed = balance.confirmed;
|
||||
|
||||
// Add values from unspent coins that are not fetched by the address list
|
||||
// i.e. scanned silent payments
|
||||
transactionHistory.transactions.values.forEach((tx) {
|
||||
if (tx.unspents != null) {
|
||||
tx.unspents!.forEach((unspent) {
|
||||
if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
|
||||
if (unspent.isFrozen) totalFrozen += unspent.value;
|
||||
totalConfirmed += unspent.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// // Add values from unspent coins that are not fetched by the address list
|
||||
// // i.e. scanned silent payments
|
||||
// transactionHistory.transactions.values.forEach((tx) {
|
||||
// if (tx.unspents != null) {
|
||||
// tx.unspents!.forEach((unspent) {
|
||||
// if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
|
||||
// if (unspent.isFrozen) totalFrozen += unspent.value;
|
||||
// totalConfirmed += unspent.value;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
return ElectrumBalance(
|
||||
confirmed: totalConfirmed,
|
||||
unconfirmed: balance.unconfirmed,
|
||||
frozen: totalFrozen,
|
||||
);
|
||||
}
|
||||
// return ElectrumBalance(
|
||||
// confirmed: totalConfirmed,
|
||||
// unconfirmed: balance.unconfirmed,
|
||||
// frozen: totalFrozen,
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
@action
|
||||
|
@ -713,15 +758,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@action
|
||||
void onHeadersResponse(ElectrumHeaderResponse response) {
|
||||
super.onHeadersResponse(response);
|
||||
// @override
|
||||
// @action
|
||||
// void onHeadersResponse(ElectrumHeaderResponse response) {
|
||||
// super.onHeadersResponse(response);
|
||||
|
||||
if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
|
||||
_setListeners(walletInfo.restoreHeight);
|
||||
}
|
||||
}
|
||||
// if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
|
||||
// _setListeners(walletInfo.restoreHeight);
|
||||
// }
|
||||
// }
|
||||
|
||||
@override
|
||||
@action
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'dart:io';
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.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_core/encryption_file_utils.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:hive/hive.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
|
||||
class BitcoinWalletService extends WalletService<
|
||||
BitcoinNewWalletCredentials,
|
||||
|
@ -172,10 +170,6 @@ class BitcoinWalletService extends WalletService<
|
|||
@override
|
||||
Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials,
|
||||
{bool? isTestnet}) async {
|
||||
if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) {
|
||||
throw BitcoinMnemonicIsIncorrectException();
|
||||
}
|
||||
|
||||
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
|
||||
credentials.walletInfo?.network = network.value;
|
||||
|
||||
|
|
|
@ -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,13 @@ 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/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;
|
||||
// import 'package:http/http.dart' as http;
|
||||
|
||||
part 'electrum_wallet.g.dart';
|
||||
|
||||
|
@ -77,8 +77,8 @@ abstract class ElectrumWalletBase
|
|||
_isTransactionUpdating = false,
|
||||
isEnabledAutoGenerateSubaddress = true,
|
||||
// TODO: inital unspent coins
|
||||
unspentCoins = ObservableSet(),
|
||||
scripthashesListening = {},
|
||||
unspentCoins = BitcoinUnspentCoins(),
|
||||
scripthashesListening = [],
|
||||
balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null
|
||||
? {
|
||||
currency: initialBalance ??
|
||||
|
@ -107,7 +107,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
@action
|
||||
void _handleWorkerResponse(dynamic message) {
|
||||
Future<void> _handleWorkerResponse(dynamic message) async {
|
||||
print('Main: received message: $message');
|
||||
|
||||
Map<String, dynamic> messageJson;
|
||||
|
@ -146,15 +146,17 @@ abstract class ElectrumWalletBase
|
|||
break;
|
||||
case ElectrumRequestMethods.headersSubscribeMethod:
|
||||
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;
|
||||
// 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;
|
||||
|
||||
late ElectrumClient electrumClient;
|
||||
ElectrumApiProvider? electrumClient2;
|
||||
BitcoinBaseElectrumRPCService? get rpc => electrumClient2?.rpc;
|
||||
ApiProvider? apiProvider;
|
||||
Box<UnspentCoinsInfo> unspentCoinsInfo;
|
||||
|
||||
|
@ -235,10 +235,10 @@ abstract class ElectrumWalletBase
|
|||
@observable
|
||||
SyncStatus syncStatus;
|
||||
|
||||
Set<String> get addressesSet => walletAddresses.allAddresses
|
||||
List<String> get addressesSet => walletAddresses.allAddresses
|
||||
.where((element) => element.type != SegwitAddresType.mweb)
|
||||
.map((addr) => addr.address)
|
||||
.toSet();
|
||||
.toList();
|
||||
|
||||
List<String> get scriptHashes => walletAddresses.addressesByReceiveType
|
||||
.where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress)
|
||||
|
@ -288,14 +288,14 @@ abstract class ElectrumWalletBase
|
|||
);
|
||||
|
||||
String _password;
|
||||
ObservableSet<BitcoinUnspent> unspentCoins;
|
||||
BitcoinUnspentCoins unspentCoins;
|
||||
|
||||
@observable
|
||||
TransactionPriorities? feeRates;
|
||||
int feeRate(TransactionPriority priority) => feeRates![priority];
|
||||
|
||||
@observable
|
||||
Set<String> scripthashesListening;
|
||||
List<String> scripthashesListening;
|
||||
|
||||
bool _chainTipListenerOn = false;
|
||||
bool _isTransactionUpdating;
|
||||
|
@ -323,16 +323,22 @@ abstract class ElectrumWalletBase
|
|||
|
||||
syncStatus = SynchronizingSyncStatus();
|
||||
|
||||
// INFO: FIRST: Call subscribe for headers, get the initial chainTip update in case it is zero
|
||||
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 updateBalance();
|
||||
// INFO: THIRD: Start loading the TX history
|
||||
await updateBalance();
|
||||
|
||||
// await subscribeForUpdates();
|
||||
|
||||
// await updateFeeRates();
|
||||
|
||||
_updateFeeRateTimer ??=
|
||||
Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates());
|
||||
// _updateFeeRateTimer ??=
|
||||
// Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates());
|
||||
|
||||
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
|
||||
void callError(FlutterErrorDetails error) {
|
||||
_onError?.call(error);
|
||||
|
@ -366,9 +358,9 @@ abstract class ElectrumWalletBase
|
|||
@action
|
||||
Future<void> updateFeeRates() async {
|
||||
try {
|
||||
feeRates = BitcoinElectrumTransactionPriorities.fromList(
|
||||
await electrumClient2!.getFeeRates(),
|
||||
);
|
||||
// feeRates = BitcoinElectrumTransactionPriorities.fromList(
|
||||
// await electrumClient2!.getFeeRates(),
|
||||
// );
|
||||
} catch (e, stacktrace) {
|
||||
// _onError?.call(FlutterErrorDetails(
|
||||
// exception: e,
|
||||
|
@ -403,36 +395,6 @@ abstract class ElectrumWalletBase
|
|||
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
|
||||
@override
|
||||
Future<void> connectToNode({required Node node}) async {
|
||||
|
@ -1176,7 +1138,7 @@ abstract class ElectrumWalletBase
|
|||
|
||||
final path = await makePath();
|
||||
await encryptionFileUtils.write(path: path, password: _password, data: toJSON());
|
||||
// await transactionHistory.save();
|
||||
await transactionHistory.save();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1226,28 +1188,23 @@ abstract class ElectrumWalletBase
|
|||
Future<void> updateAllUnspents() async {
|
||||
List<BitcoinUnspent> updatedUnspentCoins = [];
|
||||
|
||||
// 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<String> scripthashes = {};
|
||||
walletAddresses.allAddresses.forEach((addressRecord) {
|
||||
scripthashes.add(addressRecord.scriptHash);
|
||||
});
|
||||
|
||||
workerSendPort!.send(
|
||||
ElectrumWorkerGetBalanceRequest(scripthashes: scripthashes).toJson(),
|
||||
);
|
||||
|
||||
await Future.wait(walletAddresses.allAddresses
|
||||
.where((element) => element.type != SegwitAddresType.mweb)
|
||||
.map((address) async {
|
||||
updatedUnspentCoins.addAll(await fetchUnspent(address));
|
||||
}));
|
||||
|
||||
unspentCoins.addAll(updatedUnspentCoins);
|
||||
|
||||
if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
|
||||
unspentCoins.forEach((coin) => addCoinInfo(coin));
|
||||
return;
|
||||
}
|
||||
|
||||
await updateCoins(unspentCoins);
|
||||
// await refreshUnspentCoinsInfo();
|
||||
await updateCoins(unspentCoins.toSet());
|
||||
await refreshUnspentCoinsInfo();
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -1294,18 +1251,17 @@ abstract class ElectrumWalletBase
|
|||
|
||||
@action
|
||||
Future<List<BitcoinUnspent>> fetchUnspent(BitcoinAddressRecord address) async {
|
||||
List<Map<String, dynamic>> unspents = [];
|
||||
List<BitcoinUnspent> updatedUnspentCoins = [];
|
||||
|
||||
final unspents = await electrumClient2!.request(
|
||||
ElectrumScriptHashListUnspent(scriptHash: address.scriptHash),
|
||||
);
|
||||
unspents = await electrumClient.getListUnspent(address.scriptHash);
|
||||
|
||||
await Future.wait(unspents.map((unspent) async {
|
||||
try {
|
||||
final coin = BitcoinUnspent.fromUTXO(address, unspent);
|
||||
final tx = await fetchTransactionInfo(hash: coin.hash);
|
||||
coin.isChange = address.isChange;
|
||||
coin.confirmations = tx?.confirmations;
|
||||
final coin = BitcoinUnspent.fromJSON(address, unspent);
|
||||
// final tx = await fetchTransactionInfo(hash: coin.hash);
|
||||
coin.isChange = address.isHidden;
|
||||
// coin.confirmations = tx?.confirmations;
|
||||
|
||||
updatedUnspentCoins.add(coin);
|
||||
} catch (_) {}
|
||||
|
@ -1332,6 +1288,7 @@ abstract class ElectrumWalletBase
|
|||
await unspentCoinsInfo.add(newInfo);
|
||||
}
|
||||
|
||||
// TODO: ?
|
||||
Future<void> refreshUnspentCoinsInfo() async {
|
||||
try {
|
||||
final List<dynamic> keys = <dynamic>[];
|
||||
|
@ -1415,7 +1372,7 @@ abstract class ElectrumWalletBase
|
|||
final vout = input.txIndex;
|
||||
final outTransaction = inputTransaction.outputs[vout];
|
||||
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
|
||||
allInputsAmount += outTransaction.amount.toInt();
|
||||
// allInputsAmount += outTransaction.amount.toInt();
|
||||
|
||||
final addressRecord =
|
||||
walletAddresses.allAddresses.firstWhere((element) => element.address == address);
|
||||
|
@ -1565,72 +1522,15 @@ abstract class ElectrumWalletBase
|
|||
Future<ElectrumTransactionBundle> getTransactionExpanded({required String hash}) async {
|
||||
int? time;
|
||||
int? height;
|
||||
|
||||
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 (_) {}
|
||||
}
|
||||
final transactionHex = await electrumClient.getTransactionHex(hash: hash);
|
||||
|
||||
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 ins = <BtcTransaction>[];
|
||||
|
||||
for (final vin in original.inputs) {
|
||||
final inputTransactionHex = await electrumClient2!.request(
|
||||
ElectrumGetTransactionHex(transactionHash: vin.txId),
|
||||
);
|
||||
final inputTransactionHex = await electrumClient.getTransactionHex(hash: hash);
|
||||
|
||||
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
|
||||
@action
|
||||
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
|
||||
try {
|
||||
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)));
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
return historiesWithDetails;
|
||||
} catch (e) {
|
||||
print("fetchTransactions $e");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@action
|
||||
Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async {
|
||||
// TODO: all
|
||||
addresses ??= walletAddresses.allAddresses
|
||||
.where(
|
||||
(element) => element.type == SegwitAddresType.p2wpkh && element.isChange == false,
|
||||
)
|
||||
.toList();
|
||||
|
||||
Future<void> fetchTransactionsForAddressType(
|
||||
Map<String, ElectrumTransactionInfo> historiesWithDetails,
|
||||
BitcoinAddressType type,
|
||||
) async {
|
||||
final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type);
|
||||
await Future.wait(addressesByType.map((addressRecord) async {
|
||||
final history = await _fetchAddressHistory(addressRecord);
|
||||
|
||||
if (history.isNotEmpty) {
|
||||
historiesWithDetails.addAll(history);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
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),
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return historiesWithDetails;
|
||||
} catch (e, stacktrace) {
|
||||
_onError?.call(FlutterErrorDetails(
|
||||
exception: "$txid - $e",
|
||||
stack: stacktrace,
|
||||
library: this.runtimeType.toString(),
|
||||
));
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> updateTransactions() async {
|
||||
try {
|
||||
if (_isTransactionUpdating) {
|
||||
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),
|
||||
Future<void> subscribeForUpdates([Iterable<String>? unsubscribedScriptHashes]) async {
|
||||
unsubscribedScriptHashes ??= walletAddresses.allScriptHashes.where(
|
||||
(sh) => !scripthashesListening.contains(sh),
|
||||
);
|
||||
|
||||
Map<String, String> scripthashByAddress = {};
|
||||
List<String> scriptHashesList = [];
|
||||
walletAddresses.allAddresses.forEach((addressRecord) {
|
||||
scripthashByAddress[addressRecord.address] = addressRecord.scriptHash;
|
||||
scriptHashesList.add(addressRecord.scriptHash);
|
||||
});
|
||||
|
||||
workerSendPort!.send(
|
||||
ElectrumWorkerScripthashesSubscribeRequest(scripthashByAddress: scripthashByAddress).toJson(),
|
||||
ElectrumWorkerScripthashesSubscribeRequest(
|
||||
scripthashByAddress: scripthashByAddress,
|
||||
).toJson(),
|
||||
);
|
||||
scripthashesListening.addAll(scriptHashesList);
|
||||
}
|
||||
|
||||
@action
|
||||
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,
|
||||
);
|
||||
scripthashesListening.addAll(scripthashByAddress.values);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> updateBalance() async {
|
||||
balance[currency] = await fetchBalances();
|
||||
workerSendPort!.send(
|
||||
ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes).toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1925,12 +1680,102 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
@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;
|
||||
|
||||
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<void> subscribeForHeaders() async {
|
||||
print(_chainTipListenerOn);
|
||||
if (_chainTipListenerOn) return;
|
||||
|
||||
workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson());
|
||||
|
@ -1970,12 +1815,17 @@ abstract class ElectrumWalletBase
|
|||
|
||||
@action
|
||||
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
|
||||
scripthashesListening = {};
|
||||
scripthashesListening = [];
|
||||
_isTransactionUpdating = false;
|
||||
_chainTipListenerOn = false;
|
||||
}
|
||||
|
||||
if (isDisconnectedStatus) {
|
||||
if (_isTryingToConnect) return;
|
||||
|
||||
_isTryingToConnect = true;
|
||||
|
@ -1985,10 +1835,7 @@ abstract class ElectrumWalletBase
|
|||
this.syncStatus is LostConnectionSyncStatus) {
|
||||
if (node == null) return;
|
||||
|
||||
this.electrumClient.connectToUri(
|
||||
node!.uri,
|
||||
useSSL: node!.useSSL ?? false,
|
||||
);
|
||||
connectToNode(node: this.node!);
|
||||
}
|
||||
_isTryingToConnect = false;
|
||||
});
|
||||
|
@ -2102,3 +1949,35 @@ class TxCreateUtxoDetails {
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
int initialSilentAddressIndex = 0,
|
||||
List<BitcoinAddressRecord>? initialMwebAddresses,
|
||||
BitcoinAddressType? initialAddressPageType,
|
||||
}) : _allAddresses = ObservableSet.of(initialAddresses ?? []),
|
||||
}) : _allAddresses = ObservableList.of(initialAddresses ?? []),
|
||||
addressesByReceiveType =
|
||||
ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
|
||||
receiveAddresses = ObservableList<BitcoinAddressRecord>.of(
|
||||
|
@ -89,7 +89,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
static const defaultChangeAddressesCount = 17;
|
||||
static const gap = 20;
|
||||
|
||||
final ObservableSet<BitcoinAddressRecord> _allAddresses;
|
||||
final ObservableList<BitcoinAddressRecord> _allAddresses;
|
||||
final ObservableList<BaseBitcoinAddressRecord> addressesByReceiveType;
|
||||
final ObservableList<BitcoinAddressRecord> receiveAddresses;
|
||||
final ObservableList<BitcoinAddressRecord> changeAddresses;
|
||||
|
@ -116,6 +116,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
@computed
|
||||
List<BitcoinAddressRecord> get allAddresses => _allAddresses.toList();
|
||||
|
||||
@computed
|
||||
Set<String> get allScriptHashes =>
|
||||
_allAddresses.map((addressRecord) => addressRecord.scriptHash).toSet();
|
||||
|
||||
BitcoinAddressRecord getFromAddresses(String address) {
|
||||
return _allAddresses.firstWhere((element) => element.address == address);
|
||||
}
|
||||
|
@ -629,6 +633,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
return list;
|
||||
}
|
||||
|
||||
@action
|
||||
void updateAdresses(Iterable<BitcoinAddressRecord> addresses) {
|
||||
for (final address in addresses) {
|
||||
_allAddresses.replaceRange(address.index, address.index + 1, [address]);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void addAddresses(Iterable<BitcoinAddressRecord> addresses) {
|
||||
this._allAddresses.addAll(addresses);
|
||||
|
|
|
@ -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());
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -3,10 +3,16 @@ import 'dart:convert';
|
|||
import 'dart:isolate';
|
||||
|
||||
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_balance.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
// TODO: ping
|
||||
|
||||
class ElectrumWorker {
|
||||
final SendPort sendPort;
|
||||
|
@ -58,11 +64,15 @@ class ElectrumWorker {
|
|||
ElectrumWorkerScripthashesSubscribeRequest.fromJson(messageJson),
|
||||
);
|
||||
break;
|
||||
// case 'blockchain.scripthash.get_balance':
|
||||
// await _handleGetBalance(message);
|
||||
// break;
|
||||
case 'blockchain.scripthash.get_history':
|
||||
// await _handleGetHistory(workerMessage);
|
||||
case ElectrumRequestMethods.getBalanceMethod:
|
||||
await _handleGetBalance(
|
||||
ElectrumWorkerGetBalanceRequest.fromJson(messageJson),
|
||||
);
|
||||
break;
|
||||
case ElectrumRequestMethods.getHistoryMethod:
|
||||
await _handleGetHistory(
|
||||
ElectrumWorkerGetHistoryRequest.fromJson(messageJson),
|
||||
);
|
||||
break;
|
||||
case 'blockchain.scripthash.listunspent':
|
||||
// await _handleListUnspent(workerMessage);
|
||||
|
@ -108,6 +118,7 @@ class ElectrumWorker {
|
|||
await Future.wait(request.scripthashByAddress.entries.map((entry) async {
|
||||
final address = entry.key;
|
||||
final scripthash = entry.value;
|
||||
|
||||
final listener = await _electrumClient!.subscribe(
|
||||
ElectrumScriptHashSubscribe(scriptHash: scripthash),
|
||||
);
|
||||
|
@ -129,43 +140,214 @@ class ElectrumWorker {
|
|||
}));
|
||||
}
|
||||
|
||||
// Future<void> _handleGetBalance(ElectrumWorkerRequest message) async {
|
||||
// try {
|
||||
// final scriptHash = message.params['scriptHash'] as String;
|
||||
// final result = await _electrumClient!.request(
|
||||
// ElectrumGetScriptHashBalance(scriptHash: scriptHash),
|
||||
// );
|
||||
Future<void> _handleGetHistory(ElectrumWorkerGetHistoryRequest result) async {
|
||||
final Map<String, AddressHistoriesResponse> histories = {};
|
||||
final addresses = result.addresses;
|
||||
|
||||
// final balance = ElectrumBalance(
|
||||
// confirmed: result['confirmed'] as int? ?? 0,
|
||||
// unconfirmed: result['unconfirmed'] as int? ?? 0,
|
||||
// frozen: 0,
|
||||
// );
|
||||
await Future.wait(addresses.map((addressRecord) async {
|
||||
final history = await _electrumClient!.request(ElectrumScriptHashGetHistory(
|
||||
scriptHash: addressRecord.scriptHash,
|
||||
));
|
||||
|
||||
// _sendResponse(message.method, balance.toJSON());
|
||||
// } catch (e, s) {
|
||||
// print(s);
|
||||
// _sendError(message.method, e.toString());
|
||||
// }
|
||||
// }
|
||||
if (history.isNotEmpty) {
|
||||
addressRecord.setAsUsed();
|
||||
addressRecord.txCount = history.length;
|
||||
|
||||
// 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());
|
||||
// }
|
||||
// }
|
||||
await Future.wait(history.map((transaction) async {
|
||||
final txid = transaction['tx_hash'] as String;
|
||||
final height = transaction['height'] as int;
|
||||
late ElectrumTransactionInfo tx;
|
||||
|
||||
// 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());
|
||||
// }
|
||||
// }
|
||||
try {
|
||||
// Exception thrown on null
|
||||
tx = result.storedTxs.firstWhere((tx) => tx.id == txid);
|
||||
|
||||
if (height > 0) {
|
||||
tx.height = height;
|
||||
|
||||
// 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,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
52
cw_bitcoin/lib/electrum_worker/methods/get_balance.dart
Normal file
52
cw_bitcoin/lib/electrum_worker/methods/get_balance.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
110
cw_bitcoin/lib/electrum_worker/methods/get_history.dart
Normal file
110
cw_bitcoin/lib/electrum_worker/methods/get_history.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
53
cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart
Normal file
53
cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart
Normal 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?,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
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';
|
||||
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';
|
||||
part 'connection.dart';
|
||||
part 'headers_subscribe.dart';
|
||||
part 'scripthashes_subscribe.dart';
|
||||
part 'get_balance.dart';
|
||||
part 'get_history.dart';
|
||||
|
|
|
@ -774,113 +774,114 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
@override
|
||||
@action
|
||||
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
|
||||
try {
|
||||
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
|
||||
throw UnimplementedError();
|
||||
// try {
|
||||
// final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
|
||||
|
||||
await Future.wait(LITECOIN_ADDRESS_TYPES
|
||||
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
|
||||
// await Future.wait(LITECOIN_ADDRESS_TYPES
|
||||
// .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
|
||||
|
||||
return historiesWithDetails;
|
||||
} catch (e) {
|
||||
print("fetchTransactions $e");
|
||||
return {};
|
||||
}
|
||||
// return historiesWithDetails;
|
||||
// } catch (e) {
|
||||
// print("fetchTransactions $e");
|
||||
// return {};
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<void> subscribeForUpdates([
|
||||
Iterable<BitcoinAddressRecord>? unsubscribedScriptHashes,
|
||||
]) async {
|
||||
final unsubscribedScriptHashes = walletAddresses.allAddresses.where(
|
||||
(address) =>
|
||||
!scripthashesListening.contains(address.scriptHash) &&
|
||||
address.type != SegwitAddresType.mweb,
|
||||
);
|
||||
// @override
|
||||
// @action
|
||||
// Future<void> subscribeForUpdates([
|
||||
// Iterable<BitcoinAddressRecord>? unsubscribedScriptHashes,
|
||||
// ]) async {
|
||||
// final unsubscribedScriptHashes = walletAddresses.allAddresses.where(
|
||||
// (address) =>
|
||||
// !scripthashesListening.contains(address.scriptHash) &&
|
||||
// address.type != SegwitAddresType.mweb,
|
||||
// );
|
||||
|
||||
return super.subscribeForUpdates(unsubscribedScriptHashes);
|
||||
}
|
||||
// return super.subscribeForUpdates(unsubscribedScriptHashes);
|
||||
// }
|
||||
|
||||
@override
|
||||
Future<ElectrumBalance> fetchBalances() async {
|
||||
final balance = await super.fetchBalances();
|
||||
// @override
|
||||
// Future<ElectrumBalance> fetchBalances() async {
|
||||
// final balance = await super.fetchBalances();
|
||||
|
||||
if (!mwebEnabled) {
|
||||
return balance;
|
||||
}
|
||||
// if (!mwebEnabled) {
|
||||
// return balance;
|
||||
// }
|
||||
|
||||
// update unspent balances:
|
||||
await updateUnspent();
|
||||
// // update unspent balances:
|
||||
// await updateUnspent();
|
||||
|
||||
int confirmed = balance.confirmed;
|
||||
int unconfirmed = balance.unconfirmed;
|
||||
int confirmedMweb = 0;
|
||||
int unconfirmedMweb = 0;
|
||||
try {
|
||||
mwebUtxosBox.values.forEach((utxo) {
|
||||
if (utxo.height > 0) {
|
||||
confirmedMweb += utxo.value.toInt();
|
||||
} else {
|
||||
unconfirmedMweb += utxo.value.toInt();
|
||||
}
|
||||
});
|
||||
if (unconfirmedMweb > 0) {
|
||||
unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
|
||||
}
|
||||
} catch (_) {}
|
||||
// int confirmed = balance.confirmed;
|
||||
// int unconfirmed = balance.unconfirmed;
|
||||
// int confirmedMweb = 0;
|
||||
// int unconfirmedMweb = 0;
|
||||
// try {
|
||||
// mwebUtxosBox.values.forEach((utxo) {
|
||||
// if (utxo.height > 0) {
|
||||
// confirmedMweb += utxo.value.toInt();
|
||||
// } else {
|
||||
// unconfirmedMweb += utxo.value.toInt();
|
||||
// }
|
||||
// });
|
||||
// if (unconfirmedMweb > 0) {
|
||||
// unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
|
||||
// }
|
||||
// } catch (_) {}
|
||||
|
||||
for (var addressRecord in walletAddresses.allAddresses) {
|
||||
addressRecord.balance = 0;
|
||||
addressRecord.txCount = 0;
|
||||
}
|
||||
// for (var addressRecord in walletAddresses.allAddresses) {
|
||||
// addressRecord.balance = 0;
|
||||
// addressRecord.txCount = 0;
|
||||
// }
|
||||
|
||||
unspentCoins.forEach((coin) {
|
||||
final coinInfoList = unspentCoinsInfo.values.where(
|
||||
(element) =>
|
||||
element.walletId.contains(id) &&
|
||||
element.hash.contains(coin.hash) &&
|
||||
element.vout == coin.vout,
|
||||
);
|
||||
// unspentCoins.forEach((coin) {
|
||||
// final coinInfoList = unspentCoinsInfo.values.where(
|
||||
// (element) =>
|
||||
// element.walletId.contains(id) &&
|
||||
// element.hash.contains(coin.hash) &&
|
||||
// element.vout == coin.vout,
|
||||
// );
|
||||
|
||||
if (coinInfoList.isNotEmpty) {
|
||||
final coinInfo = coinInfoList.first;
|
||||
// if (coinInfoList.isNotEmpty) {
|
||||
// final coinInfo = coinInfoList.first;
|
||||
|
||||
coin.isFrozen = coinInfo.isFrozen;
|
||||
coin.isSending = coinInfo.isSending;
|
||||
coin.note = coinInfo.note;
|
||||
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
|
||||
coin.bitcoinAddressRecord.balance += coinInfo.value;
|
||||
} else {
|
||||
super.addCoinInfo(coin);
|
||||
}
|
||||
});
|
||||
// coin.isFrozen = coinInfo.isFrozen;
|
||||
// coin.isSending = coinInfo.isSending;
|
||||
// coin.note = coinInfo.note;
|
||||
// if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
|
||||
// coin.bitcoinAddressRecord.balance += coinInfo.value;
|
||||
// } else {
|
||||
// super.addCoinInfo(coin);
|
||||
// }
|
||||
// });
|
||||
|
||||
// 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:
|
||||
for (final tx in transactionHistory.transactions.values) {
|
||||
// if (tx.isPending) continue;
|
||||
if (tx.inputAddresses == null || tx.outputAddresses == null) {
|
||||
continue;
|
||||
}
|
||||
final txAddresses = tx.inputAddresses! + tx.outputAddresses!;
|
||||
for (final address in txAddresses) {
|
||||
final addressRecord = walletAddresses.allAddresses
|
||||
.firstWhereOrNull((addressRecord) => addressRecord.address == address);
|
||||
if (addressRecord == null) {
|
||||
continue;
|
||||
}
|
||||
addressRecord.txCount++;
|
||||
}
|
||||
}
|
||||
// // 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:
|
||||
// for (final tx in transactionHistory.transactions.values) {
|
||||
// // if (tx.isPending) continue;
|
||||
// if (tx.inputAddresses == null || tx.outputAddresses == null) {
|
||||
// continue;
|
||||
// }
|
||||
// final txAddresses = tx.inputAddresses! + tx.outputAddresses!;
|
||||
// for (final address in txAddresses) {
|
||||
// final addressRecord = walletAddresses.allAddresses
|
||||
// .firstWhereOrNull((addressRecord) => addressRecord.address == address);
|
||||
// if (addressRecord == null) {
|
||||
// continue;
|
||||
// }
|
||||
// addressRecord.txCount++;
|
||||
// }
|
||||
// }
|
||||
|
||||
return ElectrumBalance(
|
||||
confirmed: confirmed,
|
||||
unconfirmed: unconfirmed,
|
||||
frozen: balance.frozen,
|
||||
secondConfirmed: confirmedMweb,
|
||||
secondUnconfirmed: unconfirmedMweb,
|
||||
);
|
||||
}
|
||||
// return ElectrumBalance(
|
||||
// confirmed: confirmed,
|
||||
// unconfirmed: unconfirmed,
|
||||
// frozen: balance.frozen,
|
||||
// secondConfirmed: confirmedMweb,
|
||||
// secondUnconfirmed: unconfirmedMweb,
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
int feeRate(TransactionPriority priority) {
|
||||
|
|
|
@ -603,7 +603,7 @@ class CWBitcoin extends Bitcoin {
|
|||
|
||||
@override
|
||||
Future<void> registerSilentPaymentsKey(Object wallet, bool active) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
final bitcoinWallet = wallet as BitcoinWallet;
|
||||
return await bitcoinWallet.registerSilentPaymentsKey();
|
||||
}
|
||||
|
||||
|
@ -634,7 +634,7 @@ class CWBitcoin extends Bitcoin {
|
|||
|
||||
@override
|
||||
Future<bool> getNodeIsElectrsSPEnabled(Object wallet) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
final bitcoinWallet = wallet as BitcoinWallet;
|
||||
return bitcoinWallet.getNodeSupportsSilentPayments();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue