mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-01-03 17:40:43 +00:00
feat: unspents and tweaks subscribe method
This commit is contained in:
parent
a3e131d369
commit
c9a50233c1
25 changed files with 1438 additions and 1005 deletions
|
@ -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 '';
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
56
cw_bitcoin/lib/electrum_worker/methods/broadcast.dart
Normal file
56
cw_bitcoin/lib/electrum_worker/methods/broadcast.dart
Normal 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?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
63
cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart
Normal file
63
cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart
Normal 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?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
60
cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart
Normal file
60
cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart
Normal 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?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
157
cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart
Normal file
157
cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart
Normal 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?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue