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

View file

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

View file

@ -5,6 +5,7 @@ import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.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/psbt_transaction_builder.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart';
@ -36,7 +37,6 @@ part 'bitcoin_wallet.g.dart';
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
abstract class BitcoinWalletBase extends ElectrumWallet with Store { abstract class BitcoinWalletBase extends ElectrumWallet with Store {
Future<Isolate>? _isolate;
StreamSubscription<dynamic>? _receiveStream; StreamSubscription<dynamic>? _receiveStream;
BitcoinWalletBase({ BitcoinWalletBase({
@ -121,18 +121,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
if (derivation.derivationType == DerivationType.bip39) { if (derivation.derivationType == DerivationType.bip39) {
seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); 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; break;
} else { } else {
try { 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; break;
} }
} }
hdWallets[CWBitcoinDerivationType.old] =
hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!;
return BitcoinWallet( return BitcoinWallet(
mnemonic: mnemonic, mnemonic: mnemonic,
passphrase: passphrase ?? "", passphrase: passphrase ?? "",
@ -243,18 +227,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
if (derivation.derivationType == DerivationType.bip39) { if (derivation.derivationType == DerivationType.bip39) {
seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); 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; break;
} else { } else {
try { 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; break;
} }
} }
hdWallets[CWBitcoinDerivationType.old] =
hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!;
} }
return BitcoinWallet( return BitcoinWallet(
@ -362,7 +332,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[]; final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
for (final utxo in utxos) { 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()]!; final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
psbtReadyInputs.add(PSBTReadyUtxoWithAddress( psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
@ -421,7 +392,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
} else { } else {
alwaysScan = false; alwaysScan = false;
_isolate?.then((value) => value.kill(priority: Isolate.immediate)); // _isolate?.then((value) => value.kill(priority: Isolate.immediate));
// if (rpc!.isConnected) { // if (rpc!.isConnected) {
// syncStatus = SyncedSyncStatus(); // syncStatus = SyncedSyncStatus();
@ -431,41 +402,41 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
} }
} }
@override // @override
@action // @action
Future<void> updateAllUnspents() async { // Future<void> updateAllUnspents() async {
List<BitcoinUnspent> updatedUnspentCoins = []; // List<BitcoinUnspent> updatedUnspentCoins = [];
// Update unspents stored from scanned silent payment transactions // // Update unspents stored from scanned silent payment transactions
transactionHistory.transactions.values.forEach((tx) { // transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null) { // if (tx.unspents != null) {
updatedUnspentCoins.addAll(tx.unspents!); // updatedUnspentCoins.addAll(tx.unspents!);
} // }
}); // });
// Set the balance of all non-silent payment and non-mweb addresses to 0 before updating // // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating
walletAddresses.allAddresses // walletAddresses.allAddresses
.where((element) => element.type != SegwitAddresType.mweb) // .where((element) => element.type != SegwitAddresType.mweb)
.forEach((addr) { // .forEach((addr) {
if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; // if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0;
}); // });
await Future.wait(walletAddresses.allAddresses // await Future.wait(walletAddresses.allAddresses
.where((element) => element.type != SegwitAddresType.mweb) // .where((element) => element.type != SegwitAddresType.mweb)
.map((address) async { // .map((address) async {
updatedUnspentCoins.addAll(await fetchUnspent(address)); // updatedUnspentCoins.addAll(await fetchUnspent(address));
})); // }));
unspentCoins.addAll(updatedUnspentCoins); // unspentCoins.addAll(updatedUnspentCoins);
if (unspentCoinsInfo.length != updatedUnspentCoins.length) { // if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
unspentCoins.forEach((coin) => addCoinInfo(coin)); // unspentCoins.forEach((coin) => addCoinInfo(coin));
return; // return;
} // }
await updateCoins(unspentCoins.toSet()); // await updateCoins(unspentCoins.toSet());
await refreshUnspentCoinsInfo(); // await refreshUnspentCoinsInfo();
} // }
@override @override
void updateCoin(BitcoinUnspent coin) { 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 @action
@override @override
Future<void> startSync() async { Future<void> startSync() async {
await _setInitialHeight(); await _setInitialScanHeight();
await super.startSync(); await super.startSync();
@ -547,16 +511,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
@action @action
Future<void> registerSilentPaymentsKey() async { Future<void> registerSilentPaymentsKey() async {
final registered = await electrumClient.tweaksRegister( // final registered = await electrumClient.tweaksRegister(
secViewKey: walletAddresses.silentAddress!.b_scan.toHex(), // secViewKey: walletAddresses.silentAddress!.b_scan.toHex(),
pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(), // pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(),
labels: walletAddresses.silentAddresses // labels: walletAddresses.silentAddresses
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) // .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
.map((addr) => addr.labelIndex) // .map((addr) => addr.labelIndex)
.toList(), // .toList(),
); // );
print("registered: $registered"); // print("registered: $registered");
} }
@action @action
@ -583,6 +547,103 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
); );
} }
@override
@action
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;
}
}
@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;
if (tx.unspents != null) {
final existingTxInfo = transactionHistory.transactions[txid];
final txAlreadyExisted = existingTxInfo != null;
// Updating tx after re-scanned
if (txAlreadyExisted) {
existingTxInfo.amount = tx.amount;
existingTxInfo.confirmations = tx.confirmations;
existingTxInfo.height = tx.height;
final newUnspents = tx.unspents!
.where((unspent) => !(existingTxInfo.unspents?.any((element) =>
element.hash.contains(unspent.hash) &&
element.vout == unspent.vout &&
element.value == unspent.value) ??
false))
.toList();
if (newUnspents.isNotEmpty) {
newUnspents.forEach(_updateSilentAddressRecord);
existingTxInfo.unspents ??= [];
existingTxInfo.unspents!.addAll(newUnspents);
final newAmount = newUnspents.length > 1
? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent)
: newUnspents[0].value;
if (existingTxInfo.direction == TransactionDirection.incoming) {
existingTxInfo.amount += newAmount;
}
// Updates existing TX
transactionHistory.addOne(existingTxInfo);
// Update balance record
balance[currency]!.confirmed += newAmount;
}
} else {
// else: First time seeing this TX after scanning
tx.unspents!.forEach(_updateSilentAddressRecord);
// Add new TX record
transactionHistory.addOne(tx);
// Update balance record
balance[currency]!.confirmed += tx.amount;
}
await updateAllUnspents();
}
}
}
final newSyncStatus = result.syncStatus;
if (newSyncStatus != null) {
if (newSyncStatus is UnsupportedSyncStatus) {
nodeSupportsSilentPayments = false;
}
if (newSyncStatus is SyncingSyncStatus) {
syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc);
} else {
syncStatus = newSyncStatus;
}
await walletInfo.updateRestoreHeight(result.height!);
}
}
@action @action
Future<void> _setListeners(int height, {bool? doSingleScan}) async { Future<void> _setListeners(int height, {bool? doSingleScan}) async {
if (currentChainTip == null) { if (currentChainTip == null) {
@ -598,106 +659,23 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
syncStatus = AttemptingScanSyncStatus(); syncStatus = AttemptingScanSyncStatus();
if (_isolate != null) { workerSendPort!.send(
final runningIsolate = await _isolate!; ElectrumWorkerTweaksSubscribeRequest(
runningIsolate.kill(priority: Isolate.immediate); scanData: ScanData(
}
final receivePort = ReceivePort();
_isolate = Isolate.spawn(
startRefresh,
ScanData(
sendPort: receivePort.sendPort,
silentAddress: walletAddresses.silentAddress!, silentAddress: walletAddresses.silentAddress!,
network: network, network: network,
height: height, height: height,
chainTip: chainTip, chainTip: chainTip,
transactionHistoryIds: transactionHistory.transactions.keys.toList(), transactionHistoryIds: transactionHistory.transactions.keys.toList(),
node: (await getNodeSupportsSilentPayments()) == true
? ScanNode(node!.uri, node!.useSSL)
: null,
labels: walletAddresses.labels, labels: walletAddresses.labels,
labelIndexes: walletAddresses.silentAddresses labelIndexes: walletAddresses.silentAddresses
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
.map((addr) => addr.labelIndex) .map((addr) => addr.labelIndex)
.toList(), .toList(),
isSingleScan: doSingleScan ?? false, isSingleScan: doSingleScan ?? false,
)); ),
).toJson(),
_receiveStream?.cancel(); );
_receiveStream = receivePort.listen((var message) async {
if (message is Map<String, ElectrumTransactionInfo>) {
for (final map in message.entries) {
final txid = map.key;
final tx = map.value;
if (tx.unspents != null) {
final existingTxInfo = transactionHistory.transactions[txid];
final txAlreadyExisted = existingTxInfo != null;
// Updating tx after re-scanned
if (txAlreadyExisted) {
existingTxInfo.amount = tx.amount;
existingTxInfo.confirmations = tx.confirmations;
existingTxInfo.height = tx.height;
final newUnspents = tx.unspents!
.where((unspent) => !(existingTxInfo.unspents?.any((element) =>
element.hash.contains(unspent.hash) &&
element.vout == unspent.vout &&
element.value == unspent.value) ??
false))
.toList();
if (newUnspents.isNotEmpty) {
newUnspents.forEach(_updateSilentAddressRecord);
existingTxInfo.unspents ??= [];
existingTxInfo.unspents!.addAll(newUnspents);
final newAmount = newUnspents.length > 1
? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent)
: newUnspents[0].value;
if (existingTxInfo.direction == TransactionDirection.incoming) {
existingTxInfo.amount += newAmount;
}
// Updates existing TX
transactionHistory.addOne(existingTxInfo);
// Update balance record
balance[currency]!.confirmed += newAmount;
}
} else {
// else: First time seeing this TX after scanning
tx.unspents!.forEach(_updateSilentAddressRecord);
// Add new TX record
transactionHistory.addMany(message);
// Update balance record
balance[currency]!.confirmed += tx.amount;
}
await updateAllUnspents();
}
}
}
if (message is SyncResponse) {
if (message.syncStatus is UnsupportedSyncStatus) {
nodeSupportsSilentPayments = false;
}
if (message.syncStatus is SyncingSyncStatus) {
var status = message.syncStatus as SyncingSyncStatus;
syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc);
} else {
syncStatus = message.syncStatus;
}
await walletInfo.updateRestoreHeight(message.height);
}
});
} }
@override @override
@ -824,15 +802,28 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
} }
} }
// @override @override
// @action @action
// void onHeadersResponse(ElectrumHeaderResponse response) { Future<void> onHeadersResponse(ElectrumHeaderResponse response) async {
// super.onHeadersResponse(response); super.onHeadersResponse(response);
// if (alwaysScan == true && syncStatus is SyncedSyncStatus) { _setInitialScanHeight();
// _setListeners(walletInfo.restoreHeight);
// } // 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 @override
@action @action
@ -850,355 +841,4 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
super.syncStatusReaction(syncStatus); 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.walletInfoSource,
this.unspentCoinsInfoSource, this.unspentCoinsInfoSource,
this.alwaysScan, this.alwaysScan,
this.mempoolAPIEnabled,
this.isDirect, this.isDirect,
this.mempoolAPIEnabled,
); );
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;

View file

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

View file

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

View file

@ -649,7 +649,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
), ),
index: i, index: i,
isChange: isChange, isChange: isChange,
isHidden: derivationType == CWBitcoinDerivationType.old, isHidden: derivationType == CWBitcoinDerivationType.old && type != SegwitAddresType.p2wpkh,
type: type ?? addressPageType, type: type ?? addressPageType,
network: network, network: network,
derivationInfo: derivationInfo, derivationInfo: derivationInfo,
@ -664,7 +664,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
void updateAdresses(Iterable<BitcoinAddressRecord> addresses) { void updateAdresses(Iterable<BitcoinAddressRecord> addresses) {
for (final address in 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 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart'; 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_core/get_height_by_date.dart';
import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
// import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package: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; import 'package:http/http.dart' as http;
import 'package:sp_scanner/sp_scanner.dart';
// TODO: ping
class ElectrumWorker { class ElectrumWorker {
final SendPort sendPort; final SendPort sendPort;
@ -56,8 +60,15 @@ class ElectrumWorker {
ElectrumWorkerConnectionRequest.fromJson(messageJson), ElectrumWorkerConnectionRequest.fromJson(messageJson),
); );
break; break;
case ElectrumWorkerMethods.txHashMethod:
await _handleGetTxExpanded(
ElectrumWorkerTxExpandedRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.headersSubscribeMethod: case ElectrumRequestMethods.headersSubscribeMethod:
await _handleHeadersSubscribe(); await _handleHeadersSubscribe(
ElectrumWorkerHeadersSubscribeRequest.fromJson(messageJson),
);
break; break;
case ElectrumRequestMethods.scripthashesSubscribeMethod: case ElectrumRequestMethods.scripthashesSubscribeMethod:
await _handleScriphashesSubscribe( await _handleScriphashesSubscribe(
@ -74,12 +85,21 @@ class ElectrumWorker {
ElectrumWorkerGetHistoryRequest.fromJson(messageJson), ElectrumWorkerGetHistoryRequest.fromJson(messageJson),
); );
break; break;
case 'blockchain.scripthash.listunspent': case ElectrumRequestMethods.listunspentMethod:
// await _handleListUnspent(workerMessage); await _handleListUnspent(
ElectrumWorkerListUnspentRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.broadcastMethod:
await _handleBroadcast(
ElectrumWorkerBroadcastRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.tweaksSubscribeMethod:
await _handleScanSilentPayments(
ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson),
);
break; break;
// Add other method handlers here
// default:
// _sendError(workerMethod, 'Unsupported method: ${workerMessage.method}');
} }
} catch (e, s) { } catch (e, s) {
print(s); print(s);
@ -88,11 +108,11 @@ class ElectrumWorker {
} }
Future<void> _handleConnect(ElectrumWorkerConnectionRequest request) async { Future<void> _handleConnect(ElectrumWorkerConnectionRequest request) async {
_electrumClient = ElectrumApiProvider( _electrumClient = await ElectrumApiProvider.connect(
await ElectrumTCPService.connect( ElectrumTCPService.connect(
request.uri, request.uri,
onConnectionStatusChange: (status) { onConnectionStatusChange: (status) {
_sendResponse(ElectrumWorkerConnectionResponse(status: status)); _sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id));
}, },
defaultRequestTimeOut: const Duration(seconds: 5), defaultRequestTimeOut: const Duration(seconds: 5),
connectionTimeOut: 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()); final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe());
if (listener == null) { if (listener == null) {
_sendError(ElectrumWorkerHeadersSubscribeError(error: 'Failed to subscribe')); _sendError(ElectrumWorkerHeadersSubscribeError(error: 'Failed to subscribe'));
@ -108,7 +128,9 @@ class ElectrumWorker {
} }
listener((event) { listener((event) {
_sendResponse(ElectrumWorkerHeadersSubscribeResponse(result: event)); _sendResponse(
ElectrumWorkerHeadersSubscribeResponse(result: event, id: request.id),
);
}); });
} }
@ -135,6 +157,7 @@ class ElectrumWorker {
_sendResponse(ElectrumWorkerScripthashesSubscribeResponse( _sendResponse(ElectrumWorkerScripthashesSubscribeResponse(
result: {address: status}, result: {address: status},
id: request.id,
)); ));
}); });
})); }));
@ -171,7 +194,7 @@ class ElectrumWorker {
} }
} catch (_) { } catch (_) {
tx = ElectrumTransactionInfo.fromElectrumBundle( tx = ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded( await _getTransactionExpanded(
hash: txid, hash: txid,
currentChainTip: result.chainTip, currentChainTip: result.chainTip,
mempoolAPIEnabled: result.mempoolAPIEnabled, mempoolAPIEnabled: result.mempoolAPIEnabled,
@ -201,10 +224,113 @@ class ElectrumWorker {
return histories; 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 String hash,
required int currentChainTip, required int currentChainTip,
required bool mempoolAPIEnabled, required bool mempoolAPIEnabled,
@ -289,65 +415,312 @@ class ElectrumWorker {
); );
} }
// Future<void> _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { Future<void> _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async {
// final balanceFutures = <Future<Map<String, dynamic>>>[]; final scanData = request.scanData;
int syncHeight = scanData.height;
int initialSyncHeight = syncHeight;
// for (final scripthash in request.scripthashes) { int getCountPerRequest(int syncHeight) {
// final balanceFuture = _electrumClient!.request( if (scanData.isSingleScan) {
// ElectrumGetScriptHashBalance(scriptHash: scripthash), return 1;
// ); }
// balanceFutures.add(balanceFuture);
// }
// var totalConfirmed = 0; final amountLeft = scanData.chainTip - syncHeight + 1;
// var totalUnconfirmed = 0; 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),
);
balanceFutures.add(balanceFuture);
} }
var totalConfirmed = 0; final receiver = Receiver(
var totalUnconfirmed = 0; scanData.silentAddress.b_scan.toHex(),
scanData.silentAddress.B_spend.toHex(),
scanData.network == BitcoinNetwork.testnet,
scanData.labelIndexes,
scanData.labelIndexes.length,
);
final balances = await Future.wait(balanceFutures); // Initial status UI update, send how many blocks in total to scan
final initialCount = getCountPerRequest(syncHeight);
for (final balance in balances) { _sendResponse(ElectrumWorkerTweaksSubscribeResponse(
final confirmed = balance['confirmed'] as int? ?? 0; result: TweaksSyncResponse(
final unconfirmed = balance['unconfirmed'] as int? ?? 0; height: syncHeight,
totalConfirmed += confirmed; syncStatus: StartingScanSyncStatus(syncHeight),
totalUnconfirmed += unconfirmed;
}
_sendResponse(ElectrumWorkerGetBalanceResponse(
result: ElectrumBalance(
confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed,
frozen: 0,
), ),
)); ));
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 connectionMethod = "connection";
static const String unknownMethod = "unknown"; static const String unknownMethod = "unknown";
static const String txHashMethod = "txHash";
static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod); static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod);
static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod); static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod);
static const ElectrumWorkerMethods txHash = ElectrumWorkerMethods._(txHashMethod);
@override @override
String toString() { String toString() {

View file

@ -4,17 +4,24 @@ import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
abstract class ElectrumWorkerRequest { abstract class ElectrumWorkerRequest {
abstract final String method; abstract final String method;
abstract final int? id;
Map<String, dynamic> toJson(); Map<String, dynamic> toJson();
ElectrumWorkerRequest.fromJson(Map<String, dynamic> json); ElectrumWorkerRequest.fromJson(Map<String, dynamic> json);
} }
class ElectrumWorkerResponse<RESULT, RESPONSE> { 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 String method;
final RESULT result; final RESULT result;
final String? error; final String? error;
final int? id;
RESPONSE resultJson(RESULT result) { RESPONSE resultJson(RESULT result) {
throw UnimplementedError(); throw UnimplementedError();
@ -25,21 +32,22 @@ class ElectrumWorkerResponse<RESULT, RESPONSE> {
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return {'method': method, 'result': resultJson(result), 'error': error}; return {'method': method, 'result': resultJson(result), 'error': error, 'id': id};
} }
} }
class ElectrumWorkerErrorResponse { class ElectrumWorkerErrorResponse {
ElectrumWorkerErrorResponse({required this.error}); ElectrumWorkerErrorResponse({required this.error, this.id});
String get method => ElectrumWorkerMethods.unknown.method; String get method => ElectrumWorkerMethods.unknown.method;
final int? id;
final String error; final String error;
factory ElectrumWorkerErrorResponse.fromJson(Map<String, dynamic> json) { 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() { 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'; part of 'methods.dart';
class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest { class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest {
ElectrumWorkerConnectionRequest({required this.uri}); ElectrumWorkerConnectionRequest({
required this.uri,
required this.network,
this.id,
});
final Uri uri; final Uri uri;
final BasedUtxoNetwork network;
final int? id;
@override @override
final String method = ElectrumWorkerMethods.connect.method; final String method = ElectrumWorkerMethods.connect.method;
@override @override
factory ElectrumWorkerConnectionRequest.fromJson(Map<String, dynamic> json) { 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 @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return {'method': method, 'params': uri.toString()}; return {
'method': method,
'uri': uri.toString(),
'network': network.toString(),
};
} }
} }
class ElectrumWorkerConnectionError extends ElectrumWorkerErrorResponse { class ElectrumWorkerConnectionError extends ElectrumWorkerErrorResponse {
ElectrumWorkerConnectionError({required String error}) : super(error: error); ElectrumWorkerConnectionError({
required super.error,
super.id,
}) : super();
@override @override
String get method => ElectrumWorkerMethods.connect.method; String get method => ElectrumWorkerMethods.connect.method;
} }
class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse<ConnectionStatus, String> { class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse<ConnectionStatus, String> {
ElectrumWorkerConnectionResponse({required ConnectionStatus status, super.error}) ElectrumWorkerConnectionResponse({
: super( required ConnectionStatus status,
super.error,
super.id,
}) : super(
result: status, result: status,
method: ElectrumWorkerMethods.connect.method, method: ElectrumWorkerMethods.connect.method,
); );
@ -45,6 +67,7 @@ class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse<Connection
(e) => e.toString() == json['result'] as String, (e) => e.toString() == json['result'] as String,
), ),
error: json['error'] as String?, error: json['error'] as String?,
id: json['id'] as int?,
); );
} }
} }

View file

@ -1,9 +1,10 @@
part of 'methods.dart'; part of 'methods.dart';
class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
ElectrumWorkerGetBalanceRequest({required this.scripthashes}); ElectrumWorkerGetBalanceRequest({required this.scripthashes, this.id});
final Set<String> scripthashes; final Set<String> scripthashes;
final int? id;
@override @override
final String method = ElectrumRequestMethods.getBalance.method; final String method = ElectrumRequestMethods.getBalance.method;
@ -12,6 +13,7 @@ class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
factory ElectrumWorkerGetBalanceRequest.fromJson(Map<String, dynamic> json) { factory ElectrumWorkerGetBalanceRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetBalanceRequest( return ElectrumWorkerGetBalanceRequest(
scripthashes: (json['scripthashes'] as List<String>).toSet(), scripthashes: (json['scripthashes'] as List<String>).toSet(),
id: json['id'] as int?,
); );
} }
@ -22,7 +24,10 @@ class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
} }
class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse { class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse {
ElectrumWorkerGetBalanceError({required String error}) : super(error: error); ElectrumWorkerGetBalanceError({
required super.error,
super.id,
}) : super();
@override @override
final String method = ElectrumRequestMethods.getBalance.method; final String method = ElectrumRequestMethods.getBalance.method;
@ -30,8 +35,11 @@ class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse {
class ElectrumWorkerGetBalanceResponse class ElectrumWorkerGetBalanceResponse
extends ElectrumWorkerResponse<ElectrumBalance, Map<String, int>?> { extends ElectrumWorkerResponse<ElectrumBalance, Map<String, int>?> {
ElectrumWorkerGetBalanceResponse({required super.result, super.error}) ElectrumWorkerGetBalanceResponse({
: super(method: ElectrumRequestMethods.getBalance.method); required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.getBalance.method);
@override @override
Map<String, int>? resultJson(result) { Map<String, int>? resultJson(result) {
@ -47,6 +55,7 @@ class ElectrumWorkerGetBalanceResponse
frozen: 0, frozen: 0,
), ),
error: json['error'] as String?, 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.chainTip,
required this.network, required this.network,
required this.mempoolAPIEnabled, required this.mempoolAPIEnabled,
this.id,
}); });
final List<BitcoinAddressRecord> addresses; final List<BitcoinAddressRecord> addresses;
@ -16,6 +17,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
final int chainTip; final int chainTip;
final BasedUtxoNetwork network; final BasedUtxoNetwork network;
final bool mempoolAPIEnabled; final bool mempoolAPIEnabled;
final int? id;
@override @override
final String method = ElectrumRequestMethods.getHistory.method; final String method = ElectrumRequestMethods.getHistory.method;
@ -35,6 +37,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
chainTip: json['chainTip'] as int, chainTip: json['chainTip'] as int,
network: BasedUtxoNetwork.fromName(json['network'] as String), network: BasedUtxoNetwork.fromName(json['network'] as String),
mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool,
id: json['id'] as int?,
); );
} }
@ -53,7 +56,10 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
} }
class ElectrumWorkerGetHistoryError extends ElectrumWorkerErrorResponse { class ElectrumWorkerGetHistoryError extends ElectrumWorkerErrorResponse {
ElectrumWorkerGetHistoryError({required String error}) : super(error: error); ElectrumWorkerGetHistoryError({
required super.error,
super.id,
}) : super();
@override @override
final String method = ElectrumRequestMethods.getHistory.method; final String method = ElectrumRequestMethods.getHistory.method;
@ -90,8 +96,11 @@ class AddressHistoriesResponse {
class ElectrumWorkerGetHistoryResponse class ElectrumWorkerGetHistoryResponse
extends ElectrumWorkerResponse<List<AddressHistoriesResponse>, List<Map<String, dynamic>>> { extends ElectrumWorkerResponse<List<AddressHistoriesResponse>, List<Map<String, dynamic>>> {
ElectrumWorkerGetHistoryResponse({required super.result, super.error}) ElectrumWorkerGetHistoryResponse({
: super(method: ElectrumRequestMethods.getHistory.method); required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.getHistory.method);
@override @override
List<Map<String, dynamic>> resultJson(result) { List<Map<String, dynamic>> resultJson(result) {
@ -105,6 +114,7 @@ class ElectrumWorkerGetHistoryResponse
.map((e) => AddressHistoriesResponse.fromJson(e as Map<String, dynamic>)) .map((e) => AddressHistoriesResponse.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
error: json['error'] as String?, 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'; part of 'methods.dart';
class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest { class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest {
ElectrumWorkerHeadersSubscribeRequest(); ElectrumWorkerHeadersSubscribeRequest({this.id});
@override @override
final String method = ElectrumRequestMethods.headersSubscribe.method; final String method = ElectrumRequestMethods.headersSubscribe.method;
final int? id;
@override @override
factory ElectrumWorkerHeadersSubscribeRequest.fromJson(Map<String, dynamic> json) { factory ElectrumWorkerHeadersSubscribeRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerHeadersSubscribeRequest(); return ElectrumWorkerHeadersSubscribeRequest(
id: json['id'] as int?,
);
} }
@override @override
@ -18,7 +21,10 @@ class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest {
} }
class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse { class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse {
ElectrumWorkerHeadersSubscribeError({required String error}) : super(error: error); ElectrumWorkerHeadersSubscribeError({
required super.error,
super.id,
}) : super();
@override @override
final String method = ElectrumRequestMethods.headersSubscribe.method; final String method = ElectrumRequestMethods.headersSubscribe.method;
@ -26,8 +32,11 @@ class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse {
class ElectrumWorkerHeadersSubscribeResponse class ElectrumWorkerHeadersSubscribeResponse
extends ElectrumWorkerResponse<ElectrumHeaderResponse, Map<String, dynamic>> { extends ElectrumWorkerResponse<ElectrumHeaderResponse, Map<String, dynamic>> {
ElectrumWorkerHeadersSubscribeResponse({required super.result, super.error}) ElectrumWorkerHeadersSubscribeResponse({
: super(method: ElectrumRequestMethods.headersSubscribe.method); required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.headersSubscribe.method);
@override @override
Map<String, dynamic> resultJson(result) { Map<String, dynamic> resultJson(result) {
@ -39,6 +48,7 @@ class ElectrumWorkerHeadersSubscribeResponse
return ElectrumWorkerHeadersSubscribeResponse( return ElectrumWorkerHeadersSubscribeResponse(
result: ElectrumHeaderResponse.fromJson(json['result'] as Map<String, dynamic>), result: ElectrumHeaderResponse.fromJson(json['result'] as Map<String, dynamic>),
error: json['error'] as String?, 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:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.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_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/sync_status.dart';
part 'connection.dart'; part 'connection.dart';
part 'headers_subscribe.dart'; part 'headers_subscribe.dart';
part 'scripthashes_subscribe.dart'; part 'scripthashes_subscribe.dart';
part 'get_balance.dart'; part 'get_balance.dart';
part 'get_history.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'; part of 'methods.dart';
class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerRequest { class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerRequest {
ElectrumWorkerScripthashesSubscribeRequest({required this.scripthashByAddress}); ElectrumWorkerScripthashesSubscribeRequest({
required this.scripthashByAddress,
this.id,
});
final Map<String, String> scripthashByAddress; final Map<String, String> scripthashByAddress;
final int? id;
@override @override
final String method = ElectrumRequestMethods.scriptHashSubscribe.method; final String method = ElectrumRequestMethods.scriptHashSubscribe.method;
@ -12,6 +16,7 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques
factory ElectrumWorkerScripthashesSubscribeRequest.fromJson(Map<String, dynamic> json) { factory ElectrumWorkerScripthashesSubscribeRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerScripthashesSubscribeRequest( return ElectrumWorkerScripthashesSubscribeRequest(
scripthashByAddress: json['scripthashes'] as Map<String, String>, scripthashByAddress: json['scripthashes'] as Map<String, String>,
id: json['id'] as int?,
); );
} }
@ -22,7 +27,10 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques
} }
class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorResponse { class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorResponse {
ElectrumWorkerScripthashesSubscribeError({required String error}) : super(error: error); ElectrumWorkerScripthashesSubscribeError({
required super.error,
super.id,
}) : super();
@override @override
final String method = ElectrumRequestMethods.scriptHashSubscribe.method; final String method = ElectrumRequestMethods.scriptHashSubscribe.method;
@ -30,8 +38,11 @@ class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorRespon
class ElectrumWorkerScripthashesSubscribeResponse class ElectrumWorkerScripthashesSubscribeResponse
extends ElectrumWorkerResponse<Map<String, String>?, Map<String, String>?> { extends ElectrumWorkerResponse<Map<String, String>?, Map<String, String>?> {
ElectrumWorkerScripthashesSubscribeResponse({required super.result, super.error}) ElectrumWorkerScripthashesSubscribeResponse({
: super(method: ElectrumRequestMethods.scriptHashSubscribe.method); required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.scriptHashSubscribe.method);
@override @override
Map<String, String>? resultJson(result) { Map<String, String>? resultJson(result) {
@ -43,6 +54,7 @@ class ElectrumWorkerScripthashesSubscribeResponse
return ElectrumWorkerScripthashesSubscribeResponse( return ElectrumWorkerScripthashesSubscribeResponse(
result: json['result'] as Map<String, String>?, result: json['result'] as Map<String, String>?,
error: json['error'] as 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:collection/collection.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.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/cake_hive.dart';
import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_mweb/mwebd.pbgrpc.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart';
@ -109,22 +109,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}); });
reaction((_) => mwebSyncStatus, (status) async { reaction((_) => mwebSyncStatus, (status) async {
if (mwebSyncStatus is FailedSyncStatus) { if (mwebSyncStatus is FailedSyncStatus) {
// we failed to connect to mweb, check if we are connected to the litecoin node: await CwMweb.stop();
late int nodeHeight; await Future.delayed(const Duration(seconds: 5));
try { startSync();
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) { } else if (mwebSyncStatus is SyncingSyncStatus) {
syncStatus = mwebSyncStatus; syncStatus = mwebSyncStatus;
} else if (mwebSyncStatus is SynchronizingSyncStatus) { } else if (mwebSyncStatus is SynchronizingSyncStatus) {
@ -348,8 +335,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
return; return;
} }
final nodeHeight = final nodeHeight = await currentChainTip ?? 0;
await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node
if (nodeHeight == 0) { if (nodeHeight == 0) {
// we aren't connected to the ltc node yet // 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 status = await CwMweb.status(StatusRequest());
final height = await electrumClient.getCurrentBlockChainTip(); final height = await currentChainTip;
if (height == null || status.blockHeaderHeight != height) return; if (height == null || status.blockHeaderHeight != height) return;
if (status.mwebUtxosHeight != height) return; // we aren't synced if (status.mwebUtxosHeight != height) return; // we aren't synced
int amount = 0; int amount = 0;
@ -770,7 +756,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}); });
// copy coin control attributes to mwebCoins: // copy coin control attributes to mwebCoins:
await updateCoins(mwebUnspentCoins.toSet()); // await updateCoins(mwebUnspentCoins);
// get regular ltc unspents (this resets unspentCoins): // get regular ltc unspents (this resets unspentCoins):
await super.updateAllUnspents(); await super.updateAllUnspents();
// add the mwebCoins: // add the mwebCoins:
@ -1289,7 +1275,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}) async { }) async {
final readyInputs = <LedgerTransaction>[]; final readyInputs = <LedgerTransaction>[];
for (final utxo in utxos) { 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()]!; final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
readyInputs.add(LedgerTransaction( 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:grpc/grpc.dart';
import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_core/pending_transaction.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_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
@ -14,11 +15,10 @@ class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction( PendingBitcoinTransaction(
this._tx, this._tx,
this.type, { this.type, {
required this.electrumClient, required this.sendWorker,
required this.amount, required this.amount,
required this.fee, required this.fee,
required this.feeRate, required this.feeRate,
this.network,
required this.hasChange, required this.hasChange,
this.isSendAll = false, this.isSendAll = false,
this.hasTaprootInputs = false, this.hasTaprootInputs = false,
@ -28,11 +28,10 @@ class PendingBitcoinTransaction with PendingTransaction {
final WalletType type; final WalletType type;
final BtcTransaction _tx; final BtcTransaction _tx;
final ElectrumClient electrumClient; Future<dynamic> Function(ElectrumWorkerRequest) sendWorker;
final int amount; final int amount;
final int fee; final int fee;
final String feeRate; final String feeRate;
final BasedUtxoNetwork? network;
final bool isSendAll; final bool isSendAll;
final bool hasChange; final bool hasChange;
final bool hasTaprootInputs; final bool hasTaprootInputs;
@ -79,40 +78,39 @@ class PendingBitcoinTransaction with PendingTransaction {
Future<void> _commit() async { Future<void> _commit() async {
int? callId; int? callId;
final result = await electrumClient.broadcastTransaction( final result = await sendWorker(ElectrumWorkerBroadcastRequest(transactionRaw: hex)) as String;
transactionRaw: hex, network: network, idCallback: (id) => callId = id);
if (result.isEmpty) { // if (result.isEmpty) {
if (callId != null) { // if (callId != null) {
final error = electrumClient.getErrorMessage(callId!); // final error = sendWorker(getErrorMessage(callId!));
if (error.contains("dust")) { // if (error.contains("dust")) {
if (hasChange) { // if (hasChange) {
throw BitcoinTransactionCommitFailedDustChange(); // throw BitcoinTransactionCommitFailedDustChange();
} else if (!isSendAll) { // } else if (!isSendAll) {
throw BitcoinTransactionCommitFailedDustOutput(); // throw BitcoinTransactionCommitFailedDustOutput();
} else { // } else {
throw BitcoinTransactionCommitFailedDustOutputSendAll(); // throw BitcoinTransactionCommitFailedDustOutputSendAll();
} // }
} // }
if (error.contains("bad-txns-vout-negative")) { // if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative(); // throw BitcoinTransactionCommitFailedVoutNegative();
} // }
if (error.contains("non-BIP68-final")) { // if (error.contains("non-BIP68-final")) {
throw BitcoinTransactionCommitFailedBIP68Final(); // throw BitcoinTransactionCommitFailedBIP68Final();
} // }
if (error.contains("min fee not met")) { // if (error.contains("min fee not met")) {
throw BitcoinTransactionCommitFailedLessThanMin(); // throw BitcoinTransactionCommitFailedLessThanMin();
} // }
throw BitcoinTransactionCommitFailed(errorMessage: error); // throw BitcoinTransactionCommitFailed(errorMessage: error);
} // }
throw BitcoinTransactionCommitFailed(); // throw BitcoinTransactionCommitFailed();
} // }
} }
Future<void> _ltcCommit() async { Future<void> _ltcCommit() async {

View file

@ -96,3 +96,58 @@ class LostConnectionSyncStatus extends NotConnectedSyncStatus {
@override @override
String toString() => 'Reconnecting'; 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');
}
}