feat: unspents and tweaks subscribe method

This commit is contained in:
Rafael Saes 2024-11-05 12:49:07 -03:00
parent a3e131d369
commit c9a50233c1
25 changed files with 1438 additions and 1005 deletions

View file

@ -1,23 +0,0 @@
import 'package:bitcoin_base/bitcoin_base.dart';
String addressFromOutputScript(Script script, BasedUtxoNetwork network) {
try {
switch (script.getAddressType()) {
case P2pkhAddressType.p2pkh:
return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network);
case P2shAddressType.p2pkInP2sh:
return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
case SegwitAddresType.p2wpkh:
return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network);
case P2shAddressType.p2pkhInP2sh:
return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
case SegwitAddresType.p2wsh:
return P2wshAddress.fromScriptPubkey(script: script).toAddress(network);
case SegwitAddresType.p2tr:
return P2trAddress.fromScriptPubkey(script: script).toAddress(network);
default:
}
} catch (_) {}
return '';
}

View file

@ -139,7 +139,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
other.index == index &&
other.derivationInfo == derivationInfo &&
other.scriptHash == scriptHash &&
other.type == type;
other.type == type &&
other.derivationType == derivationType;
}
@override
@ -148,7 +149,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
index.hashCode ^
derivationInfo.hashCode ^
scriptHash.hashCode ^
type.hashCode;
type.hashCode ^
derivationType.hashCode;
}
class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {

View file

@ -14,8 +14,8 @@ class BitcoinUnspent extends Unspent {
BitcoinUnspent(
address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()),
json['tx_hash'] as String,
json['value'] as int,
json['tx_pos'] as int,
int.parse(json['value'].toString()),
int.parse(json['tx_pos'].toString()),
);
Map<String, dynamic> toJson() {

View file

@ -5,6 +5,7 @@ import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:cw_bitcoin/psbt_transaction_builder.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
@ -36,7 +37,6 @@ part 'bitcoin_wallet.g.dart';
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
abstract class BitcoinWalletBase extends ElectrumWallet with Store {
Future<Isolate>? _isolate;
StreamSubscription<dynamic>? _receiveStream;
BitcoinWalletBase({
@ -121,18 +121,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
if (derivation.derivationType == DerivationType.bip39) {
seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
hdWallets[CWBitcoinDerivationType.old] = hdWallets[CWBitcoinDerivationType.bip39]!;
try {
hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed(
seedBytes,
ElectrumWalletBase.getKeyNetVersion(network ?? BitcoinNetwork.mainnet),
).derivePath(
_hardenedDerivationPath(derivation.derivationPath ?? electrum_path),
) as Bip32Slip10Secp256k1;
} catch (e) {
print("bip39 seed error: $e");
}
break;
} else {
try {
@ -149,17 +137,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}
}
try {
hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed(
seedBytes,
).derivePath(
_hardenedDerivationPath(derivation.derivationPath ?? electrum_path),
) as Bip32Slip10Secp256k1;
} catch (_) {}
break;
}
}
hdWallets[CWBitcoinDerivationType.old] =
hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!;
return BitcoinWallet(
mnemonic: mnemonic,
passphrase: passphrase ?? "",
@ -243,18 +227,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
if (derivation.derivationType == DerivationType.bip39) {
seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
hdWallets[CWBitcoinDerivationType.old] = hdWallets[CWBitcoinDerivationType.bip39]!;
try {
hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed(
seedBytes,
ElectrumWalletBase.getKeyNetVersion(network),
).derivePath(
_hardenedDerivationPath(derivation.derivationPath ?? electrum_path),
) as Bip32Slip10Secp256k1;
} catch (e) {
print("bip39 seed error: $e");
}
break;
} else {
try {
@ -272,15 +245,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}
}
try {
hdWallets[CWBitcoinDerivationType.old] =
Bip32Slip10Secp256k1.fromSeed(seedBytes!).derivePath(
_hardenedDerivationPath(derivation.derivationPath ?? electrum_path),
) as Bip32Slip10Secp256k1;
} catch (_) {}
break;
}
}
hdWallets[CWBitcoinDerivationType.old] =
hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!;
}
return BitcoinWallet(
@ -362,7 +332,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
for (final utxo in utxos) {
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
final rawTx =
(await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex();
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
@ -421,7 +392,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
} else {
alwaysScan = false;
_isolate?.then((value) => value.kill(priority: Isolate.immediate));
// _isolate?.then((value) => value.kill(priority: Isolate.immediate));
// if (rpc!.isConnected) {
// syncStatus = SyncedSyncStatus();
@ -431,41 +402,41 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}
}
@override
@action
Future<void> updateAllUnspents() async {
List<BitcoinUnspent> updatedUnspentCoins = [];
// @override
// @action
// Future<void> updateAllUnspents() async {
// List<BitcoinUnspent> updatedUnspentCoins = [];
// Update unspents stored from scanned silent payment transactions
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null) {
updatedUnspentCoins.addAll(tx.unspents!);
}
});
// // Update unspents stored from scanned silent payment transactions
// transactionHistory.transactions.values.forEach((tx) {
// if (tx.unspents != null) {
// updatedUnspentCoins.addAll(tx.unspents!);
// }
// });
// Set the balance of all non-silent payment and non-mweb addresses to 0 before updating
walletAddresses.allAddresses
.where((element) => element.type != SegwitAddresType.mweb)
.forEach((addr) {
if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0;
});
// // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating
// walletAddresses.allAddresses
// .where((element) => element.type != SegwitAddresType.mweb)
// .forEach((addr) {
// if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0;
// });
await Future.wait(walletAddresses.allAddresses
.where((element) => element.type != SegwitAddresType.mweb)
.map((address) async {
updatedUnspentCoins.addAll(await fetchUnspent(address));
}));
// await Future.wait(walletAddresses.allAddresses
// .where((element) => element.type != SegwitAddresType.mweb)
// .map((address) async {
// updatedUnspentCoins.addAll(await fetchUnspent(address));
// }));
unspentCoins.addAll(updatedUnspentCoins);
// unspentCoins.addAll(updatedUnspentCoins);
if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
unspentCoins.forEach((coin) => addCoinInfo(coin));
return;
}
// if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
// unspentCoins.forEach((coin) => addCoinInfo(coin));
// return;
// }
await updateCoins(unspentCoins.toSet());
await refreshUnspentCoinsInfo();
}
// await updateCoins(unspentCoins.toSet());
// await refreshUnspentCoinsInfo();
// }
@override
void updateCoin(BitcoinUnspent coin) {
@ -489,17 +460,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}
}
Future<void> _setInitialHeight() async {
final validChainTip = currentChainTip != null && currentChainTip != 0;
if (validChainTip && walletInfo.restoreHeight == 0) {
await walletInfo.updateRestoreHeight(currentChainTip!);
}
}
@action
@override
Future<void> startSync() async {
await _setInitialHeight();
await _setInitialScanHeight();
await super.startSync();
@ -547,16 +511,16 @@ 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(),
);
// final registered = await electrumClient.tweaksRegister(
// secViewKey: walletAddresses.silentAddress!.b_scan.toHex(),
// pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(),
// labels: walletAddresses.silentAddresses
// .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
// .map((addr) => addr.labelIndex)
// .toList(),
// );
print("registered: $registered");
// print("registered: $registered");
}
@action
@ -583,51 +547,31 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
);
}
@override
@action
Future<void> _setListeners(int height, {bool? doSingleScan}) async {
if (currentChainTip == null) {
throw Exception("currentChainTip is null");
Future<void> handleWorkerResponse(dynamic message) async {
super.handleWorkerResponse(message);
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 ElectrumRequestMethods.tweaksSubscribeMethod:
final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson);
onTweaksSyncResponse(response.result);
break;
}
}
final chainTip = currentChainTip!;
if (chainTip == height) {
syncStatus = SyncedSyncStatus();
return;
}
syncStatus = AttemptingScanSyncStatus();
if (_isolate != null) {
final runningIsolate = await _isolate!;
runningIsolate.kill(priority: Isolate.immediate);
}
final receivePort = ReceivePort();
_isolate = Isolate.spawn(
startRefresh,
ScanData(
sendPort: receivePort.sendPort,
silentAddress: walletAddresses.silentAddress!,
network: network,
height: height,
chainTip: chainTip,
transactionHistoryIds: transactionHistory.transactions.keys.toList(),
node: (await getNodeSupportsSilentPayments()) == true
? ScanNode(node!.uri, node!.useSSL)
: null,
labels: walletAddresses.labels,
labelIndexes: walletAddresses.silentAddresses
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
.map((addr) => addr.labelIndex)
.toList(),
isSingleScan: doSingleScan ?? false,
));
_receiveStream?.cancel();
_receiveStream = receivePort.listen((var message) async {
if (message is Map<String, ElectrumTransactionInfo>) {
for (final map in message.entries) {
@action
Future<void> onTweaksSyncResponse(TweaksSyncResponse result) async {
if (result.transactions?.isNotEmpty == true) {
for (final map in result.transactions!.entries) {
final txid = map.key;
final tx = map.value;
@ -673,7 +617,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
tx.unspents!.forEach(_updateSilentAddressRecord);
// Add new TX record
transactionHistory.addMany(message);
transactionHistory.addOne(tx);
// Update balance record
balance[currency]!.confirmed += tx.amount;
}
@ -683,21 +627,55 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}
}
if (message is SyncResponse) {
if (message.syncStatus is UnsupportedSyncStatus) {
final newSyncStatus = result.syncStatus;
if (newSyncStatus != null) {
if (newSyncStatus is UnsupportedSyncStatus) {
nodeSupportsSilentPayments = false;
}
if (message.syncStatus is SyncingSyncStatus) {
var status = message.syncStatus as SyncingSyncStatus;
syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc);
if (newSyncStatus is SyncingSyncStatus) {
syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc);
} else {
syncStatus = message.syncStatus;
syncStatus = newSyncStatus;
}
await walletInfo.updateRestoreHeight(message.height);
await walletInfo.updateRestoreHeight(result.height!);
}
});
}
@action
Future<void> _setListeners(int height, {bool? doSingleScan}) async {
if (currentChainTip == null) {
throw Exception("currentChainTip is null");
}
final chainTip = currentChainTip!;
if (chainTip == height) {
syncStatus = SyncedSyncStatus();
return;
}
syncStatus = AttemptingScanSyncStatus();
workerSendPort!.send(
ElectrumWorkerTweaksSubscribeRequest(
scanData: ScanData(
silentAddress: walletAddresses.silentAddress!,
network: network,
height: height,
chainTip: chainTip,
transactionHistoryIds: transactionHistory.transactions.keys.toList(),
labels: walletAddresses.labels,
labelIndexes: walletAddresses.silentAddresses
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
.map((addr) => addr.labelIndex)
.toList(),
isSingleScan: doSingleScan ?? false,
),
).toJson(),
);
}
@override
@ -824,15 +802,28 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}
}
// @override
// @action
// void onHeadersResponse(ElectrumHeaderResponse response) {
// super.onHeadersResponse(response);
@override
@action
Future<void> onHeadersResponse(ElectrumHeaderResponse response) async {
super.onHeadersResponse(response);
// if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
// _setListeners(walletInfo.restoreHeight);
// }
// }
_setInitialScanHeight();
// New headers received, start scanning
if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
_setListeners(walletInfo.restoreHeight);
}
}
Future<void> _setInitialScanHeight() async {
final validChainTip = currentChainTip != null && currentChainTip != 0;
if (validChainTip && walletInfo.restoreHeight == 0) {
await walletInfo.updateRestoreHeight(currentChainTip!);
}
}
static String _hardenedDerivationPath(String derivationPath) =>
derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1);
@override
@action
@ -850,355 +841,4 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
super.syncStatusReaction(syncStatus);
}
}
static String _hardenedDerivationPath(String derivationPath) =>
derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1);
}
Future<void> startRefresh(ScanData scanData) async {
int syncHeight = scanData.height;
int initialSyncHeight = syncHeight;
final electrumClient = ElectrumApiProvider(
await ElectrumTCPService.connect(
scanData.node?.uri ?? Uri.parse("tcp://198.58.115.71:50001"),
),
);
int getCountPerRequest(int syncHeight) {
if (scanData.isSingleScan) {
return 1;
}
final amountLeft = scanData.chainTip - syncHeight + 1;
return amountLeft;
}
final receiver = Receiver(
scanData.silentAddress.b_scan.toHex(),
scanData.silentAddress.B_spend.toHex(),
scanData.network == BitcoinNetwork.testnet,
scanData.labelIndexes,
scanData.labelIndexes.length,
);
// Initial status UI update, send how many blocks in total to scan
final initialCount = getCountPerRequest(syncHeight);
scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
final listener = await electrumClient.subscribe(
ElectrumTweaksSubscribe(height: syncHeight, count: initialCount),
);
Future<void> listenFn(ElectrumTweaksSubscribeResponse response) async {
// success or error msg
final noData = response.message != null;
if (noData) {
// re-subscribe to continue receiving messages, starting from the next unscanned height
final nextHeight = syncHeight + 1;
final nextCount = getCountPerRequest(nextHeight);
if (nextCount > 0) {
final nextListener = await electrumClient.subscribe(
ElectrumTweaksSubscribe(height: syncHeight, count: initialCount),
);
nextListener?.call(listenFn);
}
return;
}
// Continuous status UI update, send how many blocks left to scan
final syncingStatus = scanData.isSingleScan
? SyncingSyncStatus(1, 0)
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
final tweakHeight = response.block;
try {
final blockTweaks = response.blockTweaks;
for (final txid in blockTweaks.keys) {
final tweakData = blockTweaks[txid];
final outputPubkeys = tweakData!.outputPubkeys;
final tweak = tweakData.tweak;
try {
// scanOutputs called from rust here
final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver);
if (addToWallet.isEmpty) {
// no results tx, continue to next tx
continue;
}
// placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
final txInfo = ElectrumTransactionInfo(
WalletType.bitcoin,
id: txid,
height: tweakHeight,
amount: 0,
fee: 0,
direction: TransactionDirection.incoming,
isPending: false,
isReplaced: false,
date: scanData.network == BitcoinNetwork.mainnet
? getDateByBitcoinHeight(tweakHeight)
: DateTime.now(),
confirmations: scanData.chainTip - tweakHeight + 1,
unspents: [],
isReceivedSilentPayment: true,
);
addToWallet.forEach((label, value) {
(value as Map<String, dynamic>).forEach((output, tweak) {
final t_k = tweak.toString();
final receivingOutputAddress = ECPublic.fromHex(output)
.toTaprootAddress(tweak: false)
.toAddress(scanData.network);
final matchingOutput = outputPubkeys[output]!;
final amount = matchingOutput.amount;
final pos = matchingOutput.vout;
final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
receivingOutputAddress,
labelIndex: 1, // TODO: get actual index/label
isUsed: true,
spendKey: scanData.silentAddress.b_spend.tweakAdd(
BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)),
),
txCount: 1,
balance: amount,
);
final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos);
txInfo.unspents!.add(unspent);
txInfo.amount += unspent.value;
});
});
scanData.sendPort.send({txInfo.id: txInfo});
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
}
}
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
}
syncHeight = tweakHeight;
if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
if (tweakHeight >= scanData.chainTip)
scanData.sendPort.send(SyncResponse(
syncHeight,
SyncedTipSyncStatus(scanData.chainTip),
));
if (scanData.isSingleScan) {
scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
}
}
}
listener?.call(listenFn);
// if (tweaksSubscription == null) {
// return scanData.sendPort.send(
// SyncResponse(syncHeight, UnsupportedSyncStatus()),
// );
// }
}
Future<void> delegatedScan(ScanData scanData) async {
// int syncHeight = scanData.height;
// int initialSyncHeight = syncHeight;
// BehaviorSubject<Object>? tweaksSubscription = null;
// final electrumClient = scanData.electrumClient;
// await electrumClient.connectToUri(
// scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"),
// useSSL: scanData.node?.useSSL ?? false,
// );
// if (tweaksSubscription == null) {
// scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
// tweaksSubscription = await electrumClient.tweaksScan(
// pubSpendKey: scanData.silentAddress.B_spend.toHex(),
// );
// Future<void> listenFn(t) async {
// final tweaks = t as Map<String, dynamic>;
// final msg = tweaks["message"];
// // success or error msg
// final noData = msg != null;
// if (noData) {
// return;
// }
// // Continuous status UI update, send how many blocks left to scan
// final syncingStatus = scanData.isSingleScan
// ? SyncingSyncStatus(1, 0)
// : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
// scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
// final blockHeight = tweaks.keys.first;
// final tweakHeight = int.parse(blockHeight);
// try {
// final blockTweaks = tweaks[blockHeight] as Map<String, dynamic>;
// for (var j = 0; j < blockTweaks.keys.length; j++) {
// final txid = blockTweaks.keys.elementAt(j);
// final details = blockTweaks[txid] as Map<String, dynamic>;
// final outputPubkeys = (details["output_pubkeys"] as Map<dynamic, dynamic>);
// final spendingKey = details["spending_key"].toString();
// try {
// // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
// final txInfo = ElectrumTransactionInfo(
// WalletType.bitcoin,
// id: txid,
// height: tweakHeight,
// amount: 0,
// fee: 0,
// direction: TransactionDirection.incoming,
// isPending: false,
// isReplaced: false,
// date: scanData.network == BitcoinNetwork.mainnet
// ? getDateByBitcoinHeight(tweakHeight)
// : DateTime.now(),
// confirmations: scanData.chainTip - tweakHeight + 1,
// unspents: [],
// isReceivedSilentPayment: true,
// );
// outputPubkeys.forEach((pos, value) {
// final secKey = ECPrivate.fromHex(spendingKey);
// final receivingOutputAddress =
// secKey.getPublic().toTaprootAddress(tweak: false).toAddress(scanData.network);
// late int amount;
// try {
// amount = int.parse(value[1].toString());
// } catch (_) {
// return;
// }
// final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
// receivingOutputAddress,
// labelIndex: 0,
// isUsed: true,
// spendKey: secKey,
// txCount: 1,
// balance: amount,
// );
// final unspent = BitcoinUnspent(
// receivedAddressRecord,
// txid,
// amount,
// int.parse(pos.toString()),
// );
// txInfo.unspents!.add(unspent);
// txInfo.amount += unspent.value;
// });
// scanData.sendPort.send({txInfo.id: txInfo});
// } catch (_) {}
// }
// } catch (_) {}
// syncHeight = tweakHeight;
// if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
// if (tweakHeight >= scanData.chainTip)
// scanData.sendPort.send(SyncResponse(
// syncHeight,
// SyncedTipSyncStatus(scanData.chainTip),
// ));
// if (scanData.isSingleScan) {
// scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
// }
// await tweaksSubscription!.close();
// await electrumClient.close();
// }
// }
// tweaksSubscription?.listen(listenFn);
// }
// if (tweaksSubscription == null) {
// return scanData.sendPort.send(
// SyncResponse(syncHeight, UnsupportedSyncStatus()),
// );
// }
}
class ScanNode {
final Uri uri;
final bool? useSSL;
ScanNode(this.uri, this.useSSL);
}
class ScanData {
final SendPort sendPort;
final SilentPaymentOwner silentAddress;
final int height;
final ScanNode? node;
final BasedUtxoNetwork network;
final int chainTip;
final List<String> transactionHistoryIds;
final Map<String, String> labels;
final List<int> labelIndexes;
final bool isSingleScan;
ScanData({
required this.sendPort,
required this.silentAddress,
required this.height,
required this.node,
required this.network,
required this.chainTip,
required this.transactionHistoryIds,
required this.labels,
required this.labelIndexes,
required this.isSingleScan,
});
factory ScanData.fromHeight(ScanData scanData, int newHeight) {
return ScanData(
sendPort: scanData.sendPort,
silentAddress: scanData.silentAddress,
height: newHeight,
node: scanData.node,
network: scanData.network,
chainTip: scanData.chainTip,
transactionHistoryIds: scanData.transactionHistoryIds,
labels: scanData.labels,
labelIndexes: scanData.labelIndexes,
isSingleScan: scanData.isSingleScan,
);
}
}
class SyncResponse {
final int height;
final SyncStatus syncStatus;
SyncResponse(this.height, this.syncStatus);
}

View file

@ -23,8 +23,8 @@ class BitcoinWalletService extends WalletService<
this.walletInfoSource,
this.unspentCoinsInfoSource,
this.alwaysScan,
this.mempoolAPIEnabled,
this.isDirect,
this.mempoolAPIEnabled,
);
final Box<WalletInfo> walletInfoSource;

View file

@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_core/transaction_direction.dart';
@ -11,13 +10,35 @@ import 'package:cw_core/wallet_type.dart';
import 'package:hex/hex.dart';
class ElectrumTransactionBundle {
ElectrumTransactionBundle(this.originalTransaction,
{required this.ins, required this.confirmations, this.time});
ElectrumTransactionBundle(
this.originalTransaction, {
required this.ins,
required this.confirmations,
this.time,
});
final BtcTransaction originalTransaction;
final List<BtcTransaction> ins;
final int? time;
final int confirmations;
Map<String, dynamic> toJson() {
return {
'originalTransaction': originalTransaction.toHex(),
'ins': ins.map((e) => e.toHex()).toList(),
'confirmations': confirmations,
'time': time,
};
}
static ElectrumTransactionBundle fromJson(Map<String, dynamic> data) {
return ElectrumTransactionBundle(
BtcTransaction.fromRaw(data['originalTransaction'] as String),
ins: (data['ins'] as List<Object>).map((e) => BtcTransaction.fromRaw(e as String)).toList(),
confirmations: data['confirmations'] as int,
time: data['time'] as int?,
);
}
}
class ElectrumTransactionInfo extends TransactionInfo {
@ -128,9 +149,11 @@ class ElectrumTransactionInfo extends TransactionInfo {
final inputTransaction = bundle.ins[i];
final outTransaction = inputTransaction.outputs[input.txIndex];
inputAmount += outTransaction.amount.toInt();
if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) {
if (addresses.contains(
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network))) {
direction = TransactionDirection.outgoing;
inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network));
inputAddresses.add(
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network));
}
}
} catch (e) {
@ -144,8 +167,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
final receivedAmounts = <int>[];
for (final out in bundle.originalTransaction.outputs) {
totalOutAmount += out.amount.toInt();
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
final address = addressFromOutputScript(out.scriptPubKey, network);
final addressExists = addresses
.contains(BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network));
final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network);
if (address.isNotEmpty) outputAddresses.add(address);

View file

@ -6,11 +6,11 @@ import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:collection/collection.dart';
import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
@ -25,7 +25,7 @@ import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
// import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
@ -35,13 +35,11 @@ import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
// import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger;
import 'package:mobx/mobx.dart';
// import 'package:http/http.dart' as http;
part 'electrum_wallet.g.dart';
@ -52,8 +50,11 @@ abstract class ElectrumWalletBase
with Store, WalletKeysFile {
ReceivePort? receivePort;
SendPort? workerSendPort;
StreamSubscription? _workerSubscription;
StreamSubscription<dynamic>? _workerSubscription;
Isolate? _workerIsolate;
final Map<int, dynamic> _responseCompleters = {};
final Map<int, dynamic> _errorCompleters = {};
int _messageId = 0;
ElectrumWalletBase({
required String password,
@ -67,7 +68,6 @@ abstract class ElectrumWalletBase
List<int>? seedBytes,
this.passphrase,
List<BitcoinAddressRecord>? initialAddresses,
ElectrumClient? electrumClient,
ElectrumBalance? initialBalance,
CryptoCurrency? currency,
this.alwaysScan,
@ -103,7 +103,6 @@ abstract class ElectrumWalletBase
this.isTestnet = !network.isMainnet,
this._mnemonic = mnemonic,
super(walletInfo) {
this.electrumClient = electrumClient ?? ElectrumClient();
this.walletInfo = walletInfo;
transactionHistory = ElectrumTransactionHistory(
walletInfo: walletInfo,
@ -116,8 +115,27 @@ abstract class ElectrumWalletBase
sharedPrefs.complete(SharedPreferences.getInstance());
}
Future<dynamic> sendWorker(ElectrumWorkerRequest request) {
final messageId = ++_messageId;
final completer = Completer<dynamic>();
_responseCompleters[messageId] = completer;
final json = request.toJson();
json['id'] = messageId;
workerSendPort!.send(json);
try {
return completer.future.timeout(Duration(seconds: 5));
} catch (e) {
_errorCompleters.addAll({messageId: e});
_responseCompleters.remove(messageId);
rethrow;
}
}
@action
Future<void> _handleWorkerResponse(dynamic message) async {
Future<void> handleWorkerResponse(dynamic message) async {
print('Main: received message: $message');
Map<String, dynamic> messageJson;
@ -149,6 +167,12 @@ abstract class ElectrumWalletBase
// return;
// }
final responseId = messageJson['id'] as int?;
if (responseId != null && _responseCompleters.containsKey(responseId)) {
_responseCompleters[responseId]!.complete(message);
_responseCompleters.remove(responseId);
}
switch (workerMethod) {
case ElectrumWorkerMethods.connectionMethod:
final response = ElectrumWorkerConnectionResponse.fromJson(messageJson);
@ -157,7 +181,6 @@ abstract class ElectrumWalletBase
case ElectrumRequestMethods.headersSubscribeMethod:
final response = ElectrumWorkerHeadersSubscribeResponse.fromJson(messageJson);
await onHeadersResponse(response.result);
break;
case ElectrumRequestMethods.getBalanceMethod:
final response = ElectrumWorkerGetBalanceResponse.fromJson(messageJson);
@ -167,6 +190,10 @@ abstract class ElectrumWalletBase
final response = ElectrumWorkerGetHistoryResponse.fromJson(messageJson);
onHistoriesResponse(response.result);
break;
case ElectrumRequestMethods.listunspentMethod:
final response = ElectrumWorkerListUnspentResponse.fromJson(messageJson);
onUnspentResponse(response.result);
break;
}
}
@ -219,7 +246,6 @@ abstract class ElectrumWalletBase
@observable
bool isEnabledAutoGenerateSubaddress;
late ElectrumClient electrumClient;
ApiProvider? apiProvider;
Box<UnspentCoinsInfo> unspentCoinsInfo;
@ -298,6 +324,7 @@ abstract class ElectrumWalletBase
bool _chainTipListenerOn = false;
bool _isTransactionUpdating;
bool _isInitialSync = true;
void Function(FlutterErrorDetails)? _onError;
Timer? _autoSaveTimer;
@ -323,16 +350,18 @@ abstract class ElectrumWalletBase
syncStatus = SynchronizingSyncStatus();
// INFO: FIRST: Call subscribe for headers, get the initial chainTip update in case it is zero
await subscribeForHeaders();
await sendWorker(ElectrumWorkerHeadersSubscribeRequest());
// INFO: SECOND: Start loading transaction histories for every address, this will help discover addresses until the unused gap limit has been reached, which will help finding the full balance and unspents later.
await updateTransactions();
// await updateAllUnspents();
// INFO: THIRD: Start loading the TX history
await updateBalance();
// await subscribeForUpdates();
// INFO: FOURTH: Finish with unspents
await updateAllUnspents();
_isInitialSync = false;
// await updateFeeRates();
@ -377,7 +406,7 @@ abstract class ElectrumWalletBase
return false;
}
final version = await electrumClient.version();
// final version = await electrumClient.version();
if (version.isNotEmpty) {
final server = version[0];
@ -416,10 +445,13 @@ abstract class ElectrumWalletBase
if (message is SendPort) {
workerSendPort = message;
workerSendPort!.send(
ElectrumWorkerConnectionRequest(uri: node.uri).toJson(),
ElectrumWorkerConnectionRequest(
uri: node.uri,
network: network,
).toJson(),
);
} else {
_handleWorkerResponse(message);
handleWorkerResponse(message);
}
});
} catch (e, stacktrace) {
@ -927,11 +959,10 @@ abstract class ElectrumWalletBase
return PendingBitcoinTransaction(
transaction,
type,
electrumClient: electrumClient,
sendWorker: sendWorker,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: feeRateInt.toString(),
network: network,
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot
@ -1007,11 +1038,10 @@ abstract class ElectrumWalletBase
return PendingBitcoinTransaction(
transaction,
type,
electrumClient: electrumClient,
sendWorker: sendWorker,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: feeRateInt.toString(),
network: network,
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs,
@ -1177,7 +1207,9 @@ abstract class ElectrumWalletBase
@override
Future<void> close({required bool shouldCleanup}) async {
try {
await electrumClient.close();
_workerIsolate!.kill(priority: Isolate.immediate);
await _workerSubscription?.cancel();
receivePort?.close();
} catch (_) {}
_autoSaveTimer?.cancel();
_updateFeeRateTimer?.cancel();
@ -1185,25 +1217,15 @@ abstract class ElectrumWalletBase
@action
Future<void> updateAllUnspents() async {
List<BitcoinUnspent> updatedUnspentCoins = [];
Set<String> scripthashes = {};
walletAddresses.allAddresses.forEach((addressRecord) {
scripthashes.add(addressRecord.scriptHash);
});
workerSendPort!.send(
ElectrumWorkerGetBalanceRequest(scripthashes: scripthashes).toJson(),
final req = ElectrumWorkerListUnspentRequest(
scripthashes: walletAddresses.allScriptHashes.toList(),
);
await Future.wait(walletAddresses.allAddresses
.where((element) => element.type != SegwitAddresType.mweb)
.map((address) async {
updatedUnspentCoins.addAll(await fetchUnspent(address));
}));
await updateCoins(unspentCoins.toSet());
await refreshUnspentCoinsInfo();
if (_isInitialSync) {
await sendWorker(req);
} else {
workerSendPort!.send(req.toJson());
}
}
@action
@ -1227,46 +1249,38 @@ abstract class ElectrumWalletBase
}
@action
Future<void> updateCoins(Set<BitcoinUnspent> newUnspentCoins) async {
if (newUnspentCoins.isEmpty) {
return;
}
newUnspentCoins.forEach(updateCoin);
Future<void> onUnspentResponse(Map<String, List<ElectrumUtxo>> unspents) async {
final updatedUnspentCoins = <BitcoinUnspent>[];
await Future.wait(unspents.entries.map((entry) async {
final unspent = entry.value;
final scriptHash = entry.key;
final addressRecord = walletAddresses.allAddresses.firstWhereOrNull(
(element) => element.scriptHash == scriptHash,
);
if (addressRecord == null) {
return null;
}
@action
Future<void> updateUnspentsForAddress(BitcoinAddressRecord addressRecord) async {
final newUnspentCoins = (await fetchUnspent(addressRecord)).toSet();
await updateCoins(newUnspentCoins);
unspentCoins.addAll(newUnspentCoins);
// if (unspentCoinsInfo.length != unspentCoins.length) {
// unspentCoins.forEach(addCoinInfo);
// }
// await refreshUnspentCoinsInfo();
await Future.wait(unspent.map((unspent) async {
final coin = BitcoinUnspent.fromJSON(addressRecord, unspent.toJson());
coin.isChange = addressRecord.isChange;
final tx = await fetchTransactionInfo(hash: coin.hash);
if (tx != null) {
coin.confirmations = tx.confirmations;
}
@action
Future<List<BitcoinUnspent>> fetchUnspent(BitcoinAddressRecord address) async {
List<Map<String, dynamic>> unspents = [];
List<BitcoinUnspent> updatedUnspentCoins = [];
unspents = await electrumClient.getListUnspent(address.scriptHash);
await Future.wait(unspents.map((unspent) async {
try {
final coin = BitcoinUnspent.fromJSON(address, unspent);
// final tx = await fetchTransactionInfo(hash: coin.hash);
coin.isChange = address.isHidden;
// coin.confirmations = tx?.confirmations;
updatedUnspentCoins.add(coin);
} catch (_) {}
}));
}));
return updatedUnspentCoins;
unspentCoins.clear();
unspentCoins.addAll(updatedUnspentCoins);
unspentCoins.forEach(updateCoin);
await refreshUnspentCoinsInfo();
}
@action
@ -1287,7 +1301,6 @@ abstract class ElectrumWalletBase
await unspentCoinsInfo.add(newInfo);
}
// TODO: ?
Future<void> refreshUnspentCoinsInfo() async {
try {
final List<dynamic> keys = <dynamic>[];
@ -1313,6 +1326,92 @@ abstract class ElectrumWalletBase
}
}
@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 {
if (_chainTipListenerOn) return;
workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson());
_chainTipListenerOn = true;
}
@action
Future<void> onHistoriesResponse(List<AddressHistoriesResponse> histories) async {
if (histories.isEmpty) {
return;
}
final firstAddress = histories.first;
final isChange = firstAddress.addressRecord.isChange;
final type = firstAddress.addressRecord.type;
final totalAddresses = (isChange
? walletAddresses.receiveAddresses.where((element) => element.type == type).length
: walletAddresses.changeAddresses.where((element) => element.type == type).length);
final gapLimit = (isChange
? ElectrumWalletAddressesBase.defaultChangeAddressesCount
: ElectrumWalletAddressesBase.defaultReceiveAddressesCount);
bool hasUsedAddressesUnderGap = false;
final addressesWithHistory = <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,
derivationType: firstAddress.addressRecord.derivationType,
type: type,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(type),
);
if (newAddresses.isNotEmpty) {
// Update the transactions for the new discovered addresses
await updateTransactions(newAddresses);
}
}
}
Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async {
try {
final bundle = await getTransactionExpanded(hash: tx.txHash);
@ -1331,8 +1430,9 @@ abstract class ElectrumWalletBase
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isChange);
// look for a change address in the outputs
final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any(
(element) => element.address == addressFromOutputScript(output.scriptPubKey, network)));
final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any((element) =>
element.address ==
BitcoinAddressUtils.addressFromOutputScript(output.scriptPubKey, network)));
var allInputsAmount = 0;
@ -1370,7 +1470,8 @@ abstract class ElectrumWalletBase
final inputTransaction = bundle.ins[i];
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
final address =
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network);
// allInputsAmount += outTransaction.amount.toInt();
final addressRecord =
@ -1417,7 +1518,7 @@ abstract class ElectrumWalletBase
}
}
final address = addressFromOutputScript(out.scriptPubKey, network);
final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network);
final btcAddress = RegexUtils.addressTypeFromStr(address, network);
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
}
@ -1496,10 +1597,9 @@ abstract class ElectrumWalletBase
return PendingBitcoinTransaction(
transaction,
type,
electrumClient: electrumClient,
sendWorker: sendWorker,
amount: sendingAmount,
fee: newFee,
network: network,
hasChange: changeOutputs.isNotEmpty,
feeRate: newFee.toString(),
)..addListener((transaction) async {
@ -1519,27 +1619,23 @@ abstract class ElectrumWalletBase
}
Future<ElectrumTransactionBundle> getTransactionExpanded({required String hash}) async {
int? time;
int? height;
final transactionHex = await electrumClient.getTransactionHex(hash: hash);
int? confirmations;
final original = BtcTransaction.fromRaw(transactionHex);
final ins = <BtcTransaction>[];
for (final vin in original.inputs) {
final inputTransactionHex = await electrumClient.getTransactionHex(hash: hash);
ins.add(BtcTransaction.fromRaw(inputTransactionHex));
return await sendWorker(
ElectrumWorkerTxExpandedRequest(txHash: hash, currentChainTip: currentChainTip!))
as ElectrumTransactionBundle;
}
return ElectrumTransactionBundle(
original,
ins: ins,
time: time,
confirmations: confirmations ?? 0,
Future<ElectrumTransactionInfo?> fetchTransactionInfo({required String hash, int? height}) async {
try {
return ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded(hash: hash),
walletInfo.type,
network,
addresses: walletAddresses.allAddresses.map((e) => e.address).toSet(),
height: height,
);
} catch (_) {
return null;
}
}
@override
@ -1550,27 +1646,24 @@ abstract class ElectrumWalletBase
@action
Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async {
// TODO: all
addresses ??= walletAddresses.allAddresses
.where(
(element) => element.type == SegwitAddresType.p2wpkh && element.isChange == false,
)
.toList();
addresses ??= walletAddresses.allAddresses.toList();
workerSendPort!.send(
ElectrumWorkerGetHistoryRequest(
final req = ElectrumWorkerGetHistoryRequest(
addresses: addresses,
storedTxs: transactionHistory.transactions.values.toList(),
walletType: type,
// If we still don't have currentChainTip, txs will still be fetched but shown
// with confirmations as 0 but will be auto fixed on onHeadersResponse
chainTip: currentChainTip ?? 0,
chainTip: currentChainTip ?? getBitcoinHeightByDate(date: DateTime.now()),
network: network,
// mempoolAPIEnabled: mempoolAPIEnabled,
// TODO:
mempoolAPIEnabled: true,
).toJson(),
mempoolAPIEnabled: mempoolAPIEnabled,
);
if (_isInitialSync) {
await sendWorker(req);
} else {
workerSendPort!.send(req.toJson());
}
}
@action
@ -1594,16 +1687,41 @@ abstract class ElectrumWalletBase
}
@action
Future<void> updateBalance() async {
workerSendPort!.send(
ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes).toJson(),
void onBalanceResponse(ElectrumBalance balanceResult) {
var totalFrozen = 0;
var totalConfirmed = balanceResult.confirmed;
var totalUnconfirmed = balanceResult.unconfirmed;
unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) {
if (unspentCoinInfo.isFrozen) {
// TODO: verify this works well
totalFrozen += unspentCoinInfo.value;
totalConfirmed -= unspentCoinInfo.value;
totalUnconfirmed -= unspentCoinInfo.value;
}
});
balance[currency] = ElectrumBalance(
confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed,
frozen: totalFrozen,
);
}
@action
Future<void> updateBalance() async {
final req = ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes);
if (_isInitialSync) {
await sendWorker(req);
} else {
workerSendPort!.send(req.toJson());
}
}
@override
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError;
@override
Future<String> signMessage(String message, {String? address = null}) async {
final record = walletAddresses.getFromAddresses(address!);
@ -1678,115 +1796,6 @@ abstract class ElectrumWalletBase
return false;
}
@action
Future<void> onHistoriesResponse(List<AddressHistoriesResponse> histories) async {
if (histories.isEmpty) {
return;
}
final firstAddress = histories.first;
final isChange = firstAddress.addressRecord.isChange;
final type = firstAddress.addressRecord.type;
final totalAddresses = (isChange
? walletAddresses.receiveAddresses.where((element) => element.type == type).length
: walletAddresses.changeAddresses.where((element) => element.type == type).length);
final gapLimit = (isChange
? ElectrumWalletAddressesBase.defaultChangeAddressesCount
: ElectrumWalletAddressesBase.defaultReceiveAddressesCount);
bool hasUsedAddressesUnderGap = false;
final addressesWithHistory = <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,
derivationType: firstAddress.addressRecord.derivationType,
type: type,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(type),
);
if (newAddresses.isNotEmpty) {
// Update the transactions for the new discovered addresses
await updateTransactions(newAddresses);
}
}
}
@action
void onBalanceResponse(ElectrumBalance balanceResult) {
var totalFrozen = 0;
var totalConfirmed = balanceResult.confirmed;
var totalUnconfirmed = balanceResult.unconfirmed;
unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) {
if (unspentCoinInfo.isFrozen) {
// TODO: verify this works well
totalFrozen += unspentCoinInfo.value;
totalConfirmed -= unspentCoinInfo.value;
totalUnconfirmed -= unspentCoinInfo.value;
}
});
balance[currency] = ElectrumBalance(
confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed,
frozen: totalFrozen,
);
}
@action
Future<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());
_chainTipListenerOn = true;
}
@action
void _onConnectionStatusChange(ConnectionStatus status) {
switch (status) {
@ -1862,14 +1871,15 @@ abstract class ElectrumWalletBase
final inputTransaction = bundle.ins[i];
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
final address =
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network);
if (address.isNotEmpty) inputAddresses.add(address);
}
for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) {
final out = bundle.originalTransaction.outputs[i];
final address = addressFromOutputScript(out.scriptPubKey, network);
final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network);
if (address.isNotEmpty) outputAddresses.add(address);

View file

@ -649,7 +649,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
),
index: i,
isChange: isChange,
isHidden: derivationType == CWBitcoinDerivationType.old,
isHidden: derivationType == CWBitcoinDerivationType.old && type != SegwitAddresType.p2wpkh,
type: type ?? addressPageType,
network: network,
derivationInfo: derivationInfo,
@ -664,7 +664,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
void updateAdresses(Iterable<BitcoinAddressRecord> addresses) {
for (final address in addresses) {
_allAddresses.replaceRange(address.index, address.index + 1, [address]);
final index = _allAddresses.indexWhere((element) => element.address == address.address);
_allAddresses.replaceRange(index, index + 1, [address]);
updateAddressesByMatch();
updateReceiveAddresses();
updateChangeAddresses();
}
}

View file

@ -3,16 +3,20 @@ import 'dart:convert';
import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
// import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:http/http.dart' as http;
// TODO: ping
import 'package:sp_scanner/sp_scanner.dart';
class ElectrumWorker {
final SendPort sendPort;
@ -56,8 +60,15 @@ class ElectrumWorker {
ElectrumWorkerConnectionRequest.fromJson(messageJson),
);
break;
case ElectrumWorkerMethods.txHashMethod:
await _handleGetTxExpanded(
ElectrumWorkerTxExpandedRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.headersSubscribeMethod:
await _handleHeadersSubscribe();
await _handleHeadersSubscribe(
ElectrumWorkerHeadersSubscribeRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.scripthashesSubscribeMethod:
await _handleScriphashesSubscribe(
@ -74,12 +85,21 @@ class ElectrumWorker {
ElectrumWorkerGetHistoryRequest.fromJson(messageJson),
);
break;
case 'blockchain.scripthash.listunspent':
// await _handleListUnspent(workerMessage);
case ElectrumRequestMethods.listunspentMethod:
await _handleListUnspent(
ElectrumWorkerListUnspentRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.broadcastMethod:
await _handleBroadcast(
ElectrumWorkerBroadcastRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.tweaksSubscribeMethod:
await _handleScanSilentPayments(
ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson),
);
break;
// Add other method handlers here
// default:
// _sendError(workerMethod, 'Unsupported method: ${workerMessage.method}');
}
} catch (e, s) {
print(s);
@ -88,11 +108,11 @@ class ElectrumWorker {
}
Future<void> _handleConnect(ElectrumWorkerConnectionRequest request) async {
_electrumClient = ElectrumApiProvider(
await ElectrumTCPService.connect(
_electrumClient = await ElectrumApiProvider.connect(
ElectrumTCPService.connect(
request.uri,
onConnectionStatusChange: (status) {
_sendResponse(ElectrumWorkerConnectionResponse(status: status));
_sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id));
},
defaultRequestTimeOut: const Duration(seconds: 5),
connectionTimeOut: const Duration(seconds: 5),
@ -100,7 +120,7 @@ class ElectrumWorker {
);
}
Future<void> _handleHeadersSubscribe() async {
Future<void> _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async {
final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe());
if (listener == null) {
_sendError(ElectrumWorkerHeadersSubscribeError(error: 'Failed to subscribe'));
@ -108,7 +128,9 @@ class ElectrumWorker {
}
listener((event) {
_sendResponse(ElectrumWorkerHeadersSubscribeResponse(result: event));
_sendResponse(
ElectrumWorkerHeadersSubscribeResponse(result: event, id: request.id),
);
});
}
@ -135,6 +157,7 @@ class ElectrumWorker {
_sendResponse(ElectrumWorkerScripthashesSubscribeResponse(
result: {address: status},
id: request.id,
));
});
}));
@ -171,7 +194,7 @@ class ElectrumWorker {
}
} catch (_) {
tx = ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded(
await _getTransactionExpanded(
hash: txid,
currentChainTip: result.chainTip,
mempoolAPIEnabled: result.mempoolAPIEnabled,
@ -201,10 +224,113 @@ class ElectrumWorker {
return histories;
}));
_sendResponse(ElectrumWorkerGetHistoryResponse(result: histories.values.toList()));
_sendResponse(ElectrumWorkerGetHistoryResponse(
result: histories.values.toList(),
id: result.id,
));
}
Future<ElectrumTransactionBundle> getTransactionExpanded({
// 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,
),
id: request.id,
),
);
}
Future<void> _handleListUnspent(ElectrumWorkerListUnspentRequest request) async {
final unspents = <String, List<ElectrumUtxo>>{};
await Future.wait(request.scripthashes.map((scriptHash) async {
final scriptHashUnspents = await _electrumClient!.request(
ElectrumScriptHashListUnspent(scriptHash: scriptHash),
);
if (scriptHashUnspents.isNotEmpty) {
unspents[scriptHash] = scriptHashUnspents;
}
}));
_sendResponse(ElectrumWorkerListUnspentResponse(utxos: unspents, id: request.id));
}
Future<void> _handleBroadcast(ElectrumWorkerBroadcastRequest request) async {
final txHash = await _electrumClient!.request(
ElectrumBroadCastTransaction(transactionRaw: request.transactionRaw),
);
_sendResponse(ElectrumWorkerBroadcastResponse(txHash: txHash, id: request.id));
}
Future<void> _handleGetTxExpanded(ElectrumWorkerTxExpandedRequest request) async {
final tx = await _getTransactionExpanded(
hash: request.txHash,
currentChainTip: request.currentChainTip,
mempoolAPIEnabled: false,
getConfirmations: false,
);
_sendResponse(ElectrumWorkerTxExpandedResponse(expandedTx: tx, id: request.id));
}
Future<ElectrumTransactionBundle> _getTransactionExpanded({
required String hash,
required int currentChainTip,
required bool mempoolAPIEnabled,
@ -289,65 +415,312 @@ class ElectrumWorker {
);
}
// Future<void> _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async {
// final balanceFutures = <Future<Map<String, dynamic>>>[];
Future<void> _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async {
final scanData = request.scanData;
int syncHeight = scanData.height;
int initialSyncHeight = syncHeight;
// for (final scripthash in request.scripthashes) {
// final balanceFuture = _electrumClient!.request(
// ElectrumGetScriptHashBalance(scriptHash: scripthash),
// );
// balanceFutures.add(balanceFuture);
// }
int getCountPerRequest(int syncHeight) {
if (scanData.isSingleScan) {
return 1;
}
// var totalConfirmed = 0;
// var totalUnconfirmed = 0;
final amountLeft = scanData.chainTip - syncHeight + 1;
return amountLeft;
}
// 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),
final receiver = Receiver(
scanData.silentAddress.b_scan.toHex(),
scanData.silentAddress.B_spend.toHex(),
scanData.network == BitcoinNetwork.testnet,
scanData.labelIndexes,
scanData.labelIndexes.length,
);
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,
// Initial status UI update, send how many blocks in total to scan
final initialCount = getCountPerRequest(syncHeight);
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(
height: syncHeight,
syncStatus: StartingScanSyncStatus(syncHeight),
),
));
print([syncHeight, initialCount]);
final listener = await _electrumClient!.subscribe(
ElectrumTweaksSubscribe(height: syncHeight, count: initialCount),
);
Future<void> listenFn(ElectrumTweaksSubscribeResponse response) async {
// success or error msg
final noData = response.message != null;
if (noData) {
// re-subscribe to continue receiving messages, starting from the next unscanned height
final nextHeight = syncHeight + 1;
final nextCount = getCountPerRequest(nextHeight);
if (nextCount > 0) {
final nextListener = await _electrumClient!.subscribe(
ElectrumTweaksSubscribe(height: syncHeight, count: initialCount),
);
nextListener?.call(listenFn);
}
return;
}
// Continuous status UI update, send how many blocks left to scan
final syncingStatus = scanData.isSingleScan
? SyncingSyncStatus(1, 0)
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(height: syncHeight, syncStatus: syncingStatus),
));
final tweakHeight = response.block;
try {
final blockTweaks = response.blockTweaks;
for (final txid in blockTweaks.keys) {
final tweakData = blockTweaks[txid];
final outputPubkeys = tweakData!.outputPubkeys;
final tweak = tweakData.tweak;
try {
// scanOutputs called from rust here
final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver);
if (addToWallet.isEmpty) {
// no results tx, continue to next tx
continue;
}
// placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
final txInfo = ElectrumTransactionInfo(
WalletType.bitcoin,
id: txid,
height: tweakHeight,
amount: 0,
fee: 0,
direction: TransactionDirection.incoming,
isPending: false,
isReplaced: false,
date: scanData.network == BitcoinNetwork.mainnet
? getDateByBitcoinHeight(tweakHeight)
: DateTime.now(),
confirmations: scanData.chainTip - tweakHeight + 1,
unspents: [],
isReceivedSilentPayment: true,
);
addToWallet.forEach((label, value) {
(value as Map<String, dynamic>).forEach((output, tweak) {
final t_k = tweak.toString();
final receivingOutputAddress = ECPublic.fromHex(output)
.toTaprootAddress(tweak: false)
.toAddress(scanData.network);
final matchingOutput = outputPubkeys[output]!;
final amount = matchingOutput.amount;
final pos = matchingOutput.vout;
final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
receivingOutputAddress,
labelIndex: 1, // TODO: get actual index/label
isUsed: true,
spendKey: scanData.silentAddress.b_spend.tweakAdd(
BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)),
),
txCount: 1,
balance: amount,
);
final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos);
txInfo.unspents!.add(unspent);
txInfo.amount += unspent.value;
});
});
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(transactions: {txInfo.id: txInfo}),
));
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
}
}
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
}
syncHeight = tweakHeight;
if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
if (tweakHeight >= scanData.chainTip)
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(
height: syncHeight,
syncStatus: SyncedTipSyncStatus(scanData.chainTip),
),
));
if (scanData.isSingleScan) {
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(height: syncHeight, syncStatus: SyncedSyncStatus()),
));
}
}
}
listener?.call(listenFn);
// if (tweaksSubscription == null) {
// return scanData.sendPort.send(
// SyncResponse(syncHeight, UnsupportedSyncStatus()),
// );
// }
}
}
Future<void> delegatedScan(ScanData scanData) async {
// int syncHeight = scanData.height;
// int initialSyncHeight = syncHeight;
// BehaviorSubject<Object>? tweaksSubscription = null;
// final electrumClient = scanData.electrumClient;
// await electrumClient.connectToUri(
// scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"),
// useSSL: scanData.node?.useSSL ?? false,
// );
// if (tweaksSubscription == null) {
// scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
// tweaksSubscription = await electrumClient.tweaksScan(
// pubSpendKey: scanData.silentAddress.B_spend.toHex(),
// );
// Future<void> listenFn(t) async {
// final tweaks = t as Map<String, dynamic>;
// final msg = tweaks["message"];
// // success or error msg
// final noData = msg != null;
// if (noData) {
// return;
// }
// // Continuous status UI update, send how many blocks left to scan
// final syncingStatus = scanData.isSingleScan
// ? SyncingSyncStatus(1, 0)
// : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
// scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
// final blockHeight = tweaks.keys.first;
// final tweakHeight = int.parse(blockHeight);
// try {
// final blockTweaks = tweaks[blockHeight] as Map<String, dynamic>;
// for (var j = 0; j < blockTweaks.keys.length; j++) {
// final txid = blockTweaks.keys.elementAt(j);
// final details = blockTweaks[txid] as Map<String, dynamic>;
// final outputPubkeys = (details["output_pubkeys"] as Map<dynamic, dynamic>);
// final spendingKey = details["spending_key"].toString();
// try {
// // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
// final txInfo = ElectrumTransactionInfo(
// WalletType.bitcoin,
// id: txid,
// height: tweakHeight,
// amount: 0,
// fee: 0,
// direction: TransactionDirection.incoming,
// isPending: false,
// isReplaced: false,
// date: scanData.network == BitcoinNetwork.mainnet
// ? getDateByBitcoinHeight(tweakHeight)
// : DateTime.now(),
// confirmations: scanData.chainTip - tweakHeight + 1,
// unspents: [],
// isReceivedSilentPayment: true,
// );
// outputPubkeys.forEach((pos, value) {
// final secKey = ECPrivate.fromHex(spendingKey);
// final receivingOutputAddress =
// secKey.getPublic().toTaprootAddress(tweak: false).toAddress(scanData.network);
// late int amount;
// try {
// amount = int.parse(value[1].toString());
// } catch (_) {
// return;
// }
// final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
// receivingOutputAddress,
// labelIndex: 0,
// isUsed: true,
// spendKey: secKey,
// txCount: 1,
// balance: amount,
// );
// final unspent = BitcoinUnspent(
// receivedAddressRecord,
// txid,
// amount,
// int.parse(pos.toString()),
// );
// txInfo.unspents!.add(unspent);
// txInfo.amount += unspent.value;
// });
// scanData.sendPort.send({txInfo.id: txInfo});
// } catch (_) {}
// }
// } catch (_) {}
// syncHeight = tweakHeight;
// if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
// if (tweakHeight >= scanData.chainTip)
// scanData.sendPort.send(SyncResponse(
// syncHeight,
// SyncedTipSyncStatus(scanData.chainTip),
// ));
// if (scanData.isSingleScan) {
// scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
// }
// await tweaksSubscription!.close();
// await electrumClient.close();
// }
// }
// tweaksSubscription?.listen(listenFn);
// }
// if (tweaksSubscription == null) {
// return scanData.sendPort.send(
// SyncResponse(syncHeight, UnsupportedSyncStatus()),
// );
// }
}
class ScanNode {
final Uri uri;
final bool? useSSL;
ScanNode(this.uri, this.useSSL);
}

View file

@ -4,9 +4,11 @@ class ElectrumWorkerMethods {
static const String connectionMethod = "connection";
static const String unknownMethod = "unknown";
static const String txHashMethod = "txHash";
static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod);
static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod);
static const ElectrumWorkerMethods txHash = ElectrumWorkerMethods._(txHashMethod);
@override
String toString() {

View file

@ -4,17 +4,24 @@ import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
abstract class ElectrumWorkerRequest {
abstract final String method;
abstract final int? id;
Map<String, dynamic> toJson();
ElectrumWorkerRequest.fromJson(Map<String, dynamic> json);
}
class ElectrumWorkerResponse<RESULT, RESPONSE> {
ElectrumWorkerResponse({required this.method, required this.result, this.error});
ElectrumWorkerResponse({
required this.method,
required this.result,
this.error,
this.id,
});
final String method;
final RESULT result;
final String? error;
final int? id;
RESPONSE resultJson(RESULT result) {
throw UnimplementedError();
@ -25,21 +32,22 @@ class ElectrumWorkerResponse<RESULT, RESPONSE> {
}
Map<String, dynamic> toJson() {
return {'method': method, 'result': resultJson(result), 'error': error};
return {'method': method, 'result': resultJson(result), 'error': error, 'id': id};
}
}
class ElectrumWorkerErrorResponse {
ElectrumWorkerErrorResponse({required this.error});
ElectrumWorkerErrorResponse({required this.error, this.id});
String get method => ElectrumWorkerMethods.unknown.method;
final int? id;
final String error;
factory ElectrumWorkerErrorResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerErrorResponse(error: json['error'] as String);
return ElectrumWorkerErrorResponse(error: json['error'] as String, id: json['id'] as int);
}
Map<String, dynamic> toJson() {
return {'method': method, 'error': error};
return {'method': method, 'error': error, 'id': id};
}
}

View file

@ -0,0 +1,56 @@
part of 'methods.dart';
class ElectrumWorkerBroadcastRequest implements ElectrumWorkerRequest {
ElectrumWorkerBroadcastRequest({required this.transactionRaw, this.id});
final String transactionRaw;
final int? id;
@override
final String method = ElectrumRequestMethods.broadcast.method;
@override
factory ElectrumWorkerBroadcastRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerBroadcastRequest(
transactionRaw: json['transactionRaw'] as String,
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'transactionRaw': transactionRaw};
}
}
class ElectrumWorkerBroadcastError extends ElectrumWorkerErrorResponse {
ElectrumWorkerBroadcastError({
required super.error,
super.id,
}) : super();
@override
String get method => ElectrumRequestMethods.broadcast.method;
}
class ElectrumWorkerBroadcastResponse extends ElectrumWorkerResponse<String, String> {
ElectrumWorkerBroadcastResponse({
required String txHash,
super.error,
super.id,
}) : super(result: txHash, method: ElectrumRequestMethods.broadcast.method);
@override
String resultJson(result) {
return result;
}
@override
factory ElectrumWorkerBroadcastResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerBroadcastResponse(
txHash: json['result'] as String,
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -1,34 +1,56 @@
part of 'methods.dart';
class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest {
ElectrumWorkerConnectionRequest({required this.uri});
ElectrumWorkerConnectionRequest({
required this.uri,
required this.network,
this.id,
});
final Uri uri;
final BasedUtxoNetwork network;
final int? id;
@override
final String method = ElectrumWorkerMethods.connect.method;
@override
factory ElectrumWorkerConnectionRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerConnectionRequest(uri: Uri.parse(json['params'] as String));
return ElectrumWorkerConnectionRequest(
uri: Uri.parse(json['uri'] as String),
network: BasedUtxoNetwork.values.firstWhere(
(e) => e.toString() == json['network'] as String,
),
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'params': uri.toString()};
return {
'method': method,
'uri': uri.toString(),
'network': network.toString(),
};
}
}
class ElectrumWorkerConnectionError extends ElectrumWorkerErrorResponse {
ElectrumWorkerConnectionError({required String error}) : super(error: error);
ElectrumWorkerConnectionError({
required super.error,
super.id,
}) : super();
@override
String get method => ElectrumWorkerMethods.connect.method;
}
class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse<ConnectionStatus, String> {
ElectrumWorkerConnectionResponse({required ConnectionStatus status, super.error})
: super(
ElectrumWorkerConnectionResponse({
required ConnectionStatus status,
super.error,
super.id,
}) : super(
result: status,
method: ElectrumWorkerMethods.connect.method,
);
@ -45,6 +67,7 @@ class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse<Connection
(e) => e.toString() == json['result'] as String,
),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -1,9 +1,10 @@
part of 'methods.dart';
class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
ElectrumWorkerGetBalanceRequest({required this.scripthashes});
ElectrumWorkerGetBalanceRequest({required this.scripthashes, this.id});
final Set<String> scripthashes;
final int? id;
@override
final String method = ElectrumRequestMethods.getBalance.method;
@ -12,6 +13,7 @@ class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
factory ElectrumWorkerGetBalanceRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetBalanceRequest(
scripthashes: (json['scripthashes'] as List<String>).toSet(),
id: json['id'] as int?,
);
}
@ -22,7 +24,10 @@ class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
}
class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse {
ElectrumWorkerGetBalanceError({required String error}) : super(error: error);
ElectrumWorkerGetBalanceError({
required super.error,
super.id,
}) : super();
@override
final String method = ElectrumRequestMethods.getBalance.method;
@ -30,8 +35,11 @@ class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse {
class ElectrumWorkerGetBalanceResponse
extends ElectrumWorkerResponse<ElectrumBalance, Map<String, int>?> {
ElectrumWorkerGetBalanceResponse({required super.result, super.error})
: super(method: ElectrumRequestMethods.getBalance.method);
ElectrumWorkerGetBalanceResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.getBalance.method);
@override
Map<String, int>? resultJson(result) {
@ -47,6 +55,7 @@ class ElectrumWorkerGetBalanceResponse
frozen: 0,
),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -8,6 +8,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
required this.chainTip,
required this.network,
required this.mempoolAPIEnabled,
this.id,
});
final List<BitcoinAddressRecord> addresses;
@ -16,6 +17,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
final int chainTip;
final BasedUtxoNetwork network;
final bool mempoolAPIEnabled;
final int? id;
@override
final String method = ElectrumRequestMethods.getHistory.method;
@ -35,6 +37,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
chainTip: json['chainTip'] as int,
network: BasedUtxoNetwork.fromName(json['network'] as String),
mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool,
id: json['id'] as int?,
);
}
@ -53,7 +56,10 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
}
class ElectrumWorkerGetHistoryError extends ElectrumWorkerErrorResponse {
ElectrumWorkerGetHistoryError({required String error}) : super(error: error);
ElectrumWorkerGetHistoryError({
required super.error,
super.id,
}) : super();
@override
final String method = ElectrumRequestMethods.getHistory.method;
@ -90,8 +96,11 @@ class AddressHistoriesResponse {
class ElectrumWorkerGetHistoryResponse
extends ElectrumWorkerResponse<List<AddressHistoriesResponse>, List<Map<String, dynamic>>> {
ElectrumWorkerGetHistoryResponse({required super.result, super.error})
: super(method: ElectrumRequestMethods.getHistory.method);
ElectrumWorkerGetHistoryResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.getHistory.method);
@override
List<Map<String, dynamic>> resultJson(result) {
@ -105,6 +114,7 @@ class ElectrumWorkerGetHistoryResponse
.map((e) => AddressHistoriesResponse.fromJson(e as Map<String, dynamic>))
.toList(),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,63 @@
part of 'methods.dart';
class ElectrumWorkerTxExpandedRequest implements ElectrumWorkerRequest {
ElectrumWorkerTxExpandedRequest({
required this.txHash,
required this.currentChainTip,
this.id,
});
final String txHash;
final int currentChainTip;
final int? id;
@override
final String method = ElectrumWorkerMethods.txHash.method;
@override
factory ElectrumWorkerTxExpandedRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerTxExpandedRequest(
txHash: json['txHash'] as String,
currentChainTip: json['currentChainTip'] as int,
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'txHash': txHash, 'currentChainTip': currentChainTip};
}
}
class ElectrumWorkerTxExpandedError extends ElectrumWorkerErrorResponse {
ElectrumWorkerTxExpandedError({
required String error,
super.id,
}) : super(error: error);
@override
String get method => ElectrumWorkerMethods.txHash.method;
}
class ElectrumWorkerTxExpandedResponse
extends ElectrumWorkerResponse<ElectrumTransactionBundle, Map<String, dynamic>> {
ElectrumWorkerTxExpandedResponse({
required ElectrumTransactionBundle expandedTx,
super.error,
super.id,
}) : super(result: expandedTx, method: ElectrumWorkerMethods.txHash.method);
@override
Map<String, dynamic> resultJson(result) {
return result.toJson();
}
@override
factory ElectrumWorkerTxExpandedResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerTxExpandedResponse(
expandedTx: ElectrumTransactionBundle.fromJson(json['result'] as Map<String, dynamic>),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -1,14 +1,17 @@
part of 'methods.dart';
class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest {
ElectrumWorkerHeadersSubscribeRequest();
ElectrumWorkerHeadersSubscribeRequest({this.id});
@override
final String method = ElectrumRequestMethods.headersSubscribe.method;
final int? id;
@override
factory ElectrumWorkerHeadersSubscribeRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerHeadersSubscribeRequest();
return ElectrumWorkerHeadersSubscribeRequest(
id: json['id'] as int?,
);
}
@override
@ -18,7 +21,10 @@ class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest {
}
class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse {
ElectrumWorkerHeadersSubscribeError({required String error}) : super(error: error);
ElectrumWorkerHeadersSubscribeError({
required super.error,
super.id,
}) : super();
@override
final String method = ElectrumRequestMethods.headersSubscribe.method;
@ -26,8 +32,11 @@ class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse {
class ElectrumWorkerHeadersSubscribeResponse
extends ElectrumWorkerResponse<ElectrumHeaderResponse, Map<String, dynamic>> {
ElectrumWorkerHeadersSubscribeResponse({required super.result, super.error})
: super(method: ElectrumRequestMethods.headersSubscribe.method);
ElectrumWorkerHeadersSubscribeResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.headersSubscribe.method);
@override
Map<String, dynamic> resultJson(result) {
@ -39,6 +48,7 @@ class ElectrumWorkerHeadersSubscribeResponse
return ElectrumWorkerHeadersSubscribeResponse(
result: ElectrumHeaderResponse.fromJson(json['result'] as Map<String, dynamic>),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,60 @@
part of 'methods.dart';
class ElectrumWorkerListUnspentRequest implements ElectrumWorkerRequest {
ElectrumWorkerListUnspentRequest({required this.scripthashes, this.id});
final List<String> scripthashes;
final int? id;
@override
final String method = ElectrumRequestMethods.listunspent.method;
@override
factory ElectrumWorkerListUnspentRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerListUnspentRequest(
scripthashes: json['scripthashes'] as List<String>,
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'scripthashes': scripthashes};
}
}
class ElectrumWorkerListUnspentError extends ElectrumWorkerErrorResponse {
ElectrumWorkerListUnspentError({
required super.error,
super.id,
}) : super();
@override
String get method => ElectrumRequestMethods.listunspent.method;
}
class ElectrumWorkerListUnspentResponse
extends ElectrumWorkerResponse<Map<String, List<ElectrumUtxo>>, Map<String, dynamic>> {
ElectrumWorkerListUnspentResponse({
required Map<String, List<ElectrumUtxo>> utxos,
super.error,
super.id,
}) : super(result: utxos, method: ElectrumRequestMethods.listunspent.method);
@override
Map<String, dynamic> resultJson(result) {
return result.map((key, value) => MapEntry(key, value.map((e) => e.toJson()).toList()));
}
@override
factory ElectrumWorkerListUnspentResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerListUnspentResponse(
utxos: (json['result'] as Map<String, dynamic>).map(
(key, value) => MapEntry(key,
(value as List).map((e) => ElectrumUtxo.fromJson(e as Map<String, dynamic>)).toList()),
),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -1,53 +0,0 @@
// 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,4 +1,3 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
@ -6,8 +5,14 @@ import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/sync_status.dart';
part 'connection.dart';
part 'headers_subscribe.dart';
part 'scripthashes_subscribe.dart';
part 'get_balance.dart';
part 'get_history.dart';
part 'get_tx_expanded.dart';
part 'broadcast.dart';
part 'list_unspent.dart';
part 'tweaks_subscribe.dart';

View file

@ -1,9 +1,13 @@
part of 'methods.dart';
class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerRequest {
ElectrumWorkerScripthashesSubscribeRequest({required this.scripthashByAddress});
ElectrumWorkerScripthashesSubscribeRequest({
required this.scripthashByAddress,
this.id,
});
final Map<String, String> scripthashByAddress;
final int? id;
@override
final String method = ElectrumRequestMethods.scriptHashSubscribe.method;
@ -12,6 +16,7 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques
factory ElectrumWorkerScripthashesSubscribeRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerScripthashesSubscribeRequest(
scripthashByAddress: json['scripthashes'] as Map<String, String>,
id: json['id'] as int?,
);
}
@ -22,7 +27,10 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques
}
class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorResponse {
ElectrumWorkerScripthashesSubscribeError({required String error}) : super(error: error);
ElectrumWorkerScripthashesSubscribeError({
required super.error,
super.id,
}) : super();
@override
final String method = ElectrumRequestMethods.scriptHashSubscribe.method;
@ -30,8 +38,11 @@ class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorRespon
class ElectrumWorkerScripthashesSubscribeResponse
extends ElectrumWorkerResponse<Map<String, String>?, Map<String, String>?> {
ElectrumWorkerScripthashesSubscribeResponse({required super.result, super.error})
: super(method: ElectrumRequestMethods.scriptHashSubscribe.method);
ElectrumWorkerScripthashesSubscribeResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.scriptHashSubscribe.method);
@override
Map<String, String>? resultJson(result) {
@ -43,6 +54,7 @@ class ElectrumWorkerScripthashesSubscribeResponse
return ElectrumWorkerScripthashesSubscribeResponse(
result: json['result'] as Map<String, String>?,
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,157 @@
part of 'methods.dart';
class ScanData {
final SilentPaymentOwner silentAddress;
final int height;
final BasedUtxoNetwork network;
final int chainTip;
final List<String> transactionHistoryIds;
final Map<String, String> labels;
final List<int> labelIndexes;
final bool isSingleScan;
ScanData({
required this.silentAddress,
required this.height,
required this.network,
required this.chainTip,
required this.transactionHistoryIds,
required this.labels,
required this.labelIndexes,
required this.isSingleScan,
});
factory ScanData.fromHeight(ScanData scanData, int newHeight) {
return ScanData(
silentAddress: scanData.silentAddress,
height: newHeight,
network: scanData.network,
chainTip: scanData.chainTip,
transactionHistoryIds: scanData.transactionHistoryIds,
labels: scanData.labels,
labelIndexes: scanData.labelIndexes,
isSingleScan: scanData.isSingleScan,
);
}
Map<String, dynamic> toJson() {
return {
'silentAddress': silentAddress.toJson(),
'height': height,
'network': network.value,
'chainTip': chainTip,
'transactionHistoryIds': transactionHistoryIds,
'labels': labels,
'labelIndexes': labelIndexes,
'isSingleScan': isSingleScan,
};
}
static ScanData fromJson(Map<String, dynamic> json) {
return ScanData(
silentAddress: SilentPaymentOwner.fromJson(json['silentAddress'] as Map<String, dynamic>),
height: json['height'] as int,
network: BasedUtxoNetwork.fromName(json['network'] as String),
chainTip: json['chainTip'] as int,
transactionHistoryIds:
(json['transactionHistoryIds'] as List).map((e) => e as String).toList(),
labels: json['labels'] as Map<String, String>,
labelIndexes: (json['labelIndexes'] as List).map((e) => e as int).toList(),
isSingleScan: json['isSingleScan'] as bool,
);
}
}
class ElectrumWorkerTweaksSubscribeRequest implements ElectrumWorkerRequest {
ElectrumWorkerTweaksSubscribeRequest({
required this.scanData,
this.id,
});
final ScanData scanData;
final int? id;
@override
final String method = ElectrumRequestMethods.tweaksSubscribe.method;
@override
factory ElectrumWorkerTweaksSubscribeRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerTweaksSubscribeRequest(
scanData: ScanData.fromJson(json['scanData'] as Map<String, dynamic>),
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'scanData': scanData.toJson()};
}
}
class ElectrumWorkerTweaksSubscribeError extends ElectrumWorkerErrorResponse {
ElectrumWorkerTweaksSubscribeError({
required super.error,
super.id,
}) : super();
@override
final String method = ElectrumRequestMethods.tweaksSubscribe.method;
}
class TweaksSyncResponse {
int? height;
SyncStatus? syncStatus;
Map<String, ElectrumTransactionInfo>? transactions = {};
TweaksSyncResponse({this.height, this.syncStatus, this.transactions});
Map<String, dynamic> toJson() {
return {
'height': height,
'syncStatus': syncStatus == null ? null : syncStatusToJson(syncStatus!),
'transactions': transactions?.map((key, value) => MapEntry(key, value.toJson())),
};
}
static TweaksSyncResponse fromJson(Map<String, dynamic> json) {
return TweaksSyncResponse(
height: json['height'] as int?,
syncStatus: json['syncStatus'] == null
? null
: syncStatusFromJson(json['syncStatus'] as Map<String, dynamic>),
transactions: json['transactions'] == null
? null
: (json['transactions'] as Map<String, dynamic>).map(
(key, value) => MapEntry(
key,
ElectrumTransactionInfo.fromJson(
value as Map<String, dynamic>,
WalletType.bitcoin,
)),
),
);
}
}
class ElectrumWorkerTweaksSubscribeResponse
extends ElectrumWorkerResponse<TweaksSyncResponse, Map<String, dynamic>> {
ElectrumWorkerTweaksSubscribeResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.tweaksSubscribe.method);
@override
Map<String, dynamic> resultJson(result) {
return result.toJson();
}
@override
factory ElectrumWorkerTweaksSubscribeResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse.fromJson(json['result'] as Map<String, dynamic>),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -6,7 +6,7 @@ import 'dart:math';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
// import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_mweb/mwebd.pbgrpc.dart';
@ -109,22 +109,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
});
reaction((_) => mwebSyncStatus, (status) async {
if (mwebSyncStatus is FailedSyncStatus) {
// we failed to connect to mweb, check if we are connected to the litecoin node:
late int nodeHeight;
try {
nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0;
} catch (_) {
nodeHeight = 0;
}
if (nodeHeight == 0) {
// we aren't connected to the litecoin node, so the current electrum_wallet reactions will take care of this case for us
} else {
// we're connected to the litecoin node, but we failed to connect to mweb, try again after a few seconds:
await CwMweb.stop();
await Future.delayed(const Duration(seconds: 5));
startSync();
}
} else if (mwebSyncStatus is SyncingSyncStatus) {
syncStatus = mwebSyncStatus;
} else if (mwebSyncStatus is SynchronizingSyncStatus) {
@ -348,8 +335,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
return;
}
final nodeHeight =
await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node
final nodeHeight = await currentChainTip ?? 0;
if (nodeHeight == 0) {
// we aren't connected to the ltc node yet
@ -635,7 +621,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}
final status = await CwMweb.status(StatusRequest());
final height = await electrumClient.getCurrentBlockChainTip();
final height = await currentChainTip;
if (height == null || status.blockHeaderHeight != height) return;
if (status.mwebUtxosHeight != height) return; // we aren't synced
int amount = 0;
@ -770,7 +756,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
});
// copy coin control attributes to mwebCoins:
await updateCoins(mwebUnspentCoins.toSet());
// await updateCoins(mwebUnspentCoins);
// get regular ltc unspents (this resets unspentCoins):
await super.updateAllUnspents();
// add the mwebCoins:
@ -1289,7 +1275,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}) async {
final readyInputs = <LedgerTransaction>[];
for (final utxo in utxos) {
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
final rawTx =
(await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex();
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
readyInputs.add(LedgerTransaction(

View file

@ -1,9 +1,10 @@
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:grpc/grpc.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart';
@ -14,11 +15,10 @@ class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction(
this._tx,
this.type, {
required this.electrumClient,
required this.sendWorker,
required this.amount,
required this.fee,
required this.feeRate,
this.network,
required this.hasChange,
this.isSendAll = false,
this.hasTaprootInputs = false,
@ -28,11 +28,10 @@ class PendingBitcoinTransaction with PendingTransaction {
final WalletType type;
final BtcTransaction _tx;
final ElectrumClient electrumClient;
Future<dynamic> Function(ElectrumWorkerRequest) sendWorker;
final int amount;
final int fee;
final String feeRate;
final BasedUtxoNetwork? network;
final bool isSendAll;
final bool hasChange;
final bool hasTaprootInputs;
@ -79,40 +78,39 @@ class PendingBitcoinTransaction with PendingTransaction {
Future<void> _commit() async {
int? callId;
final result = await electrumClient.broadcastTransaction(
transactionRaw: hex, network: network, idCallback: (id) => callId = id);
final result = await sendWorker(ElectrumWorkerBroadcastRequest(transactionRaw: hex)) as String;
if (result.isEmpty) {
if (callId != null) {
final error = electrumClient.getErrorMessage(callId!);
// if (result.isEmpty) {
// if (callId != null) {
// final error = sendWorker(getErrorMessage(callId!));
if (error.contains("dust")) {
if (hasChange) {
throw BitcoinTransactionCommitFailedDustChange();
} else if (!isSendAll) {
throw BitcoinTransactionCommitFailedDustOutput();
} else {
throw BitcoinTransactionCommitFailedDustOutputSendAll();
}
}
// if (error.contains("dust")) {
// if (hasChange) {
// throw BitcoinTransactionCommitFailedDustChange();
// } else if (!isSendAll) {
// throw BitcoinTransactionCommitFailedDustOutput();
// } else {
// throw BitcoinTransactionCommitFailedDustOutputSendAll();
// }
// }
if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative();
}
// if (error.contains("bad-txns-vout-negative")) {
// throw BitcoinTransactionCommitFailedVoutNegative();
// }
if (error.contains("non-BIP68-final")) {
throw BitcoinTransactionCommitFailedBIP68Final();
}
// if (error.contains("non-BIP68-final")) {
// throw BitcoinTransactionCommitFailedBIP68Final();
// }
if (error.contains("min fee not met")) {
throw BitcoinTransactionCommitFailedLessThanMin();
}
// if (error.contains("min fee not met")) {
// throw BitcoinTransactionCommitFailedLessThanMin();
// }
throw BitcoinTransactionCommitFailed(errorMessage: error);
}
// throw BitcoinTransactionCommitFailed(errorMessage: error);
// }
throw BitcoinTransactionCommitFailed();
}
// throw BitcoinTransactionCommitFailed();
// }
}
Future<void> _ltcCommit() async {

View file

@ -96,3 +96,58 @@ class LostConnectionSyncStatus extends NotConnectedSyncStatus {
@override
String toString() => 'Reconnecting';
}
Map<String, dynamic> syncStatusToJson(SyncStatus? status) {
if (status == null) {
return {};
}
return {
'progress': status.progress(),
'type': status.runtimeType.toString(),
'data': status is SyncingSyncStatus
? {'blocksLeft': status.blocksLeft, 'ptc': status.ptc}
: status is SyncedTipSyncStatus
? {'tip': status.tip}
: status is FailedSyncStatus
? {'error': status.error}
: status is StartingScanSyncStatus
? {'beginHeight': status.beginHeight}
: null
};
}
SyncStatus syncStatusFromJson(Map<String, dynamic> json) {
final type = json['type'] as String;
final data = json['data'] as Map<String, dynamic>?;
switch (type) {
case 'StartingScanSyncStatus':
return StartingScanSyncStatus(data!['beginHeight'] as int);
case 'SyncingSyncStatus':
return SyncingSyncStatus(data!['blocksLeft'] as int, data['ptc'] as double);
case 'SyncedTipSyncStatus':
return SyncedTipSyncStatus(data!['tip'] as int);
case 'FailedSyncStatus':
return FailedSyncStatus(error: data!['error'] as String?);
case 'SynchronizingSyncStatus':
return SynchronizingSyncStatus();
case 'NotConnectedSyncStatus':
return NotConnectedSyncStatus();
case 'AttemptingSyncStatus':
return AttemptingSyncStatus();
case 'AttemptingScanSyncStatus':
return AttemptingScanSyncStatus();
case 'ConnectedSyncStatus':
return ConnectedSyncStatus();
case 'ConnectingSyncStatus':
return ConnectingSyncStatus();
case 'UnsupportedSyncStatus':
return UnsupportedSyncStatus();
case 'TimedOutSyncStatus':
return TimedOutSyncStatus();
case 'LostConnectionSyncStatus':
return LostConnectionSyncStatus();
default:
throw Exception('Unknown sync status type: $type');
}
}