mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-01-18 16:55:58 +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.derivationInfo == derivationInfo &&
|
||||
other.scriptHash == scriptHash &&
|
||||
other.type == type;
|
||||
other.type == type &&
|
||||
other.derivationType == derivationType;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -148,7 +149,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
|
|||
index.hashCode ^
|
||||
derivationInfo.hashCode ^
|
||||
scriptHash.hashCode ^
|
||||
type.hashCode;
|
||||
type.hashCode ^
|
||||
derivationType.hashCode;
|
||||
}
|
||||
|
||||
class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
|
||||
|
|
|
@ -14,8 +14,8 @@ class BitcoinUnspent extends Unspent {
|
|||
BitcoinUnspent(
|
||||
address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()),
|
||||
json['tx_hash'] as String,
|
||||
json['value'] as int,
|
||||
json['tx_pos'] as int,
|
||||
int.parse(json['value'].toString()),
|
||||
int.parse(json['tx_pos'].toString()),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'dart:isolate';
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
|
||||
import 'package:cw_bitcoin/psbt_transaction_builder.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||
|
@ -36,7 +37,6 @@ part 'bitcoin_wallet.g.dart';
|
|||
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
|
||||
|
||||
abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
Future<Isolate>? _isolate;
|
||||
StreamSubscription<dynamic>? _receiveStream;
|
||||
|
||||
BitcoinWalletBase({
|
||||
|
@ -121,18 +121,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
if (derivation.derivationType == DerivationType.bip39) {
|
||||
seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
|
||||
hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
|
||||
hdWallets[CWBitcoinDerivationType.old] = hdWallets[CWBitcoinDerivationType.bip39]!;
|
||||
|
||||
try {
|
||||
hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed(
|
||||
seedBytes,
|
||||
ElectrumWalletBase.getKeyNetVersion(network ?? BitcoinNetwork.mainnet),
|
||||
).derivePath(
|
||||
_hardenedDerivationPath(derivation.derivationPath ?? electrum_path),
|
||||
) as Bip32Slip10Secp256k1;
|
||||
} catch (e) {
|
||||
print("bip39 seed error: $e");
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
try {
|
||||
|
@ -149,17 +137,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed(
|
||||
seedBytes,
|
||||
).derivePath(
|
||||
_hardenedDerivationPath(derivation.derivationPath ?? electrum_path),
|
||||
) as Bip32Slip10Secp256k1;
|
||||
} catch (_) {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hdWallets[CWBitcoinDerivationType.old] =
|
||||
hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!;
|
||||
|
||||
return BitcoinWallet(
|
||||
mnemonic: mnemonic,
|
||||
passphrase: passphrase ?? "",
|
||||
|
@ -243,18 +227,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
if (derivation.derivationType == DerivationType.bip39) {
|
||||
seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
|
||||
hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
|
||||
hdWallets[CWBitcoinDerivationType.old] = hdWallets[CWBitcoinDerivationType.bip39]!;
|
||||
|
||||
try {
|
||||
hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed(
|
||||
seedBytes,
|
||||
ElectrumWalletBase.getKeyNetVersion(network),
|
||||
).derivePath(
|
||||
_hardenedDerivationPath(derivation.derivationPath ?? electrum_path),
|
||||
) as Bip32Slip10Secp256k1;
|
||||
} catch (e) {
|
||||
print("bip39 seed error: $e");
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
try {
|
||||
|
@ -272,15 +245,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
hdWallets[CWBitcoinDerivationType.old] =
|
||||
Bip32Slip10Secp256k1.fromSeed(seedBytes!).derivePath(
|
||||
_hardenedDerivationPath(derivation.derivationPath ?? electrum_path),
|
||||
) as Bip32Slip10Secp256k1;
|
||||
} catch (_) {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hdWallets[CWBitcoinDerivationType.old] =
|
||||
hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!;
|
||||
}
|
||||
|
||||
return BitcoinWallet(
|
||||
|
@ -362,7 +332,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
|
||||
final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
|
||||
for (final utxo in utxos) {
|
||||
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
|
||||
final rawTx =
|
||||
(await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex();
|
||||
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
|
||||
|
||||
psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
|
||||
|
@ -421,7 +392,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
} else {
|
||||
alwaysScan = false;
|
||||
|
||||
_isolate?.then((value) => value.kill(priority: Isolate.immediate));
|
||||
// _isolate?.then((value) => value.kill(priority: Isolate.immediate));
|
||||
|
||||
// if (rpc!.isConnected) {
|
||||
// syncStatus = SyncedSyncStatus();
|
||||
|
@ -431,41 +402,41 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<void> updateAllUnspents() async {
|
||||
List<BitcoinUnspent> updatedUnspentCoins = [];
|
||||
// @override
|
||||
// @action
|
||||
// Future<void> updateAllUnspents() async {
|
||||
// List<BitcoinUnspent> updatedUnspentCoins = [];
|
||||
|
||||
// Update unspents stored from scanned silent payment transactions
|
||||
transactionHistory.transactions.values.forEach((tx) {
|
||||
if (tx.unspents != null) {
|
||||
updatedUnspentCoins.addAll(tx.unspents!);
|
||||
}
|
||||
});
|
||||
// // Update unspents stored from scanned silent payment transactions
|
||||
// transactionHistory.transactions.values.forEach((tx) {
|
||||
// if (tx.unspents != null) {
|
||||
// updatedUnspentCoins.addAll(tx.unspents!);
|
||||
// }
|
||||
// });
|
||||
|
||||
// Set the balance of all non-silent payment and non-mweb addresses to 0 before updating
|
||||
walletAddresses.allAddresses
|
||||
.where((element) => element.type != SegwitAddresType.mweb)
|
||||
.forEach((addr) {
|
||||
if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0;
|
||||
});
|
||||
// // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating
|
||||
// walletAddresses.allAddresses
|
||||
// .where((element) => element.type != SegwitAddresType.mweb)
|
||||
// .forEach((addr) {
|
||||
// if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0;
|
||||
// });
|
||||
|
||||
await Future.wait(walletAddresses.allAddresses
|
||||
.where((element) => element.type != SegwitAddresType.mweb)
|
||||
.map((address) async {
|
||||
updatedUnspentCoins.addAll(await fetchUnspent(address));
|
||||
}));
|
||||
// await Future.wait(walletAddresses.allAddresses
|
||||
// .where((element) => element.type != SegwitAddresType.mweb)
|
||||
// .map((address) async {
|
||||
// updatedUnspentCoins.addAll(await fetchUnspent(address));
|
||||
// }));
|
||||
|
||||
unspentCoins.addAll(updatedUnspentCoins);
|
||||
// unspentCoins.addAll(updatedUnspentCoins);
|
||||
|
||||
if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
|
||||
unspentCoins.forEach((coin) => addCoinInfo(coin));
|
||||
return;
|
||||
}
|
||||
// if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
|
||||
// unspentCoins.forEach((coin) => addCoinInfo(coin));
|
||||
// return;
|
||||
// }
|
||||
|
||||
await updateCoins(unspentCoins.toSet());
|
||||
await refreshUnspentCoinsInfo();
|
||||
}
|
||||
// await updateCoins(unspentCoins.toSet());
|
||||
// await refreshUnspentCoinsInfo();
|
||||
// }
|
||||
|
||||
@override
|
||||
void updateCoin(BitcoinUnspent coin) {
|
||||
|
@ -489,17 +460,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _setInitialHeight() async {
|
||||
final validChainTip = currentChainTip != null && currentChainTip != 0;
|
||||
if (validChainTip && walletInfo.restoreHeight == 0) {
|
||||
await walletInfo.updateRestoreHeight(currentChainTip!);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> startSync() async {
|
||||
await _setInitialHeight();
|
||||
await _setInitialScanHeight();
|
||||
|
||||
await super.startSync();
|
||||
|
||||
|
@ -547,16 +511,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
|
||||
@action
|
||||
Future<void> registerSilentPaymentsKey() async {
|
||||
final registered = await electrumClient.tweaksRegister(
|
||||
secViewKey: walletAddresses.silentAddress!.b_scan.toHex(),
|
||||
pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(),
|
||||
labels: walletAddresses.silentAddresses
|
||||
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
|
||||
.map((addr) => addr.labelIndex)
|
||||
.toList(),
|
||||
);
|
||||
// final registered = await electrumClient.tweaksRegister(
|
||||
// secViewKey: walletAddresses.silentAddress!.b_scan.toHex(),
|
||||
// pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(),
|
||||
// labels: walletAddresses.silentAddresses
|
||||
// .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
|
||||
// .map((addr) => addr.labelIndex)
|
||||
// .toList(),
|
||||
// );
|
||||
|
||||
print("registered: $registered");
|
||||
// print("registered: $registered");
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -583,51 +547,31 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<void> _setListeners(int height, {bool? doSingleScan}) async {
|
||||
if (currentChainTip == null) {
|
||||
throw Exception("currentChainTip is null");
|
||||
Future<void> handleWorkerResponse(dynamic message) async {
|
||||
super.handleWorkerResponse(message);
|
||||
|
||||
Map<String, dynamic> messageJson;
|
||||
if (message is String) {
|
||||
messageJson = jsonDecode(message) as Map<String, dynamic>;
|
||||
} else {
|
||||
messageJson = message as Map<String, dynamic>;
|
||||
}
|
||||
final workerMethod = messageJson['method'] as String;
|
||||
|
||||
switch (workerMethod) {
|
||||
case ElectrumRequestMethods.tweaksSubscribeMethod:
|
||||
final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson);
|
||||
onTweaksSyncResponse(response.result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final chainTip = currentChainTip!;
|
||||
|
||||
if (chainTip == height) {
|
||||
syncStatus = SyncedSyncStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
syncStatus = AttemptingScanSyncStatus();
|
||||
|
||||
if (_isolate != null) {
|
||||
final runningIsolate = await _isolate!;
|
||||
runningIsolate.kill(priority: Isolate.immediate);
|
||||
}
|
||||
|
||||
final receivePort = ReceivePort();
|
||||
_isolate = Isolate.spawn(
|
||||
startRefresh,
|
||||
ScanData(
|
||||
sendPort: receivePort.sendPort,
|
||||
silentAddress: walletAddresses.silentAddress!,
|
||||
network: network,
|
||||
height: height,
|
||||
chainTip: chainTip,
|
||||
transactionHistoryIds: transactionHistory.transactions.keys.toList(),
|
||||
node: (await getNodeSupportsSilentPayments()) == true
|
||||
? ScanNode(node!.uri, node!.useSSL)
|
||||
: null,
|
||||
labels: walletAddresses.labels,
|
||||
labelIndexes: walletAddresses.silentAddresses
|
||||
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
|
||||
.map((addr) => addr.labelIndex)
|
||||
.toList(),
|
||||
isSingleScan: doSingleScan ?? false,
|
||||
));
|
||||
|
||||
_receiveStream?.cancel();
|
||||
_receiveStream = receivePort.listen((var message) async {
|
||||
if (message is Map<String, ElectrumTransactionInfo>) {
|
||||
for (final map in message.entries) {
|
||||
@action
|
||||
Future<void> onTweaksSyncResponse(TweaksSyncResponse result) async {
|
||||
if (result.transactions?.isNotEmpty == true) {
|
||||
for (final map in result.transactions!.entries) {
|
||||
final txid = map.key;
|
||||
final tx = map.value;
|
||||
|
||||
|
@ -673,7 +617,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
tx.unspents!.forEach(_updateSilentAddressRecord);
|
||||
|
||||
// Add new TX record
|
||||
transactionHistory.addMany(message);
|
||||
transactionHistory.addOne(tx);
|
||||
// Update balance record
|
||||
balance[currency]!.confirmed += tx.amount;
|
||||
}
|
||||
|
@ -683,21 +627,55 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
}
|
||||
}
|
||||
|
||||
if (message is SyncResponse) {
|
||||
if (message.syncStatus is UnsupportedSyncStatus) {
|
||||
final newSyncStatus = result.syncStatus;
|
||||
|
||||
if (newSyncStatus != null) {
|
||||
if (newSyncStatus is UnsupportedSyncStatus) {
|
||||
nodeSupportsSilentPayments = false;
|
||||
}
|
||||
|
||||
if (message.syncStatus is SyncingSyncStatus) {
|
||||
var status = message.syncStatus as SyncingSyncStatus;
|
||||
syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc);
|
||||
if (newSyncStatus is SyncingSyncStatus) {
|
||||
syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc);
|
||||
} else {
|
||||
syncStatus = message.syncStatus;
|
||||
syncStatus = newSyncStatus;
|
||||
}
|
||||
|
||||
await walletInfo.updateRestoreHeight(message.height);
|
||||
await walletInfo.updateRestoreHeight(result.height!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> _setListeners(int height, {bool? doSingleScan}) async {
|
||||
if (currentChainTip == null) {
|
||||
throw Exception("currentChainTip is null");
|
||||
}
|
||||
|
||||
final chainTip = currentChainTip!;
|
||||
|
||||
if (chainTip == height) {
|
||||
syncStatus = SyncedSyncStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
syncStatus = AttemptingScanSyncStatus();
|
||||
|
||||
workerSendPort!.send(
|
||||
ElectrumWorkerTweaksSubscribeRequest(
|
||||
scanData: ScanData(
|
||||
silentAddress: walletAddresses.silentAddress!,
|
||||
network: network,
|
||||
height: height,
|
||||
chainTip: chainTip,
|
||||
transactionHistoryIds: transactionHistory.transactions.keys.toList(),
|
||||
labels: walletAddresses.labels,
|
||||
labelIndexes: walletAddresses.silentAddresses
|
||||
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
|
||||
.map((addr) => addr.labelIndex)
|
||||
.toList(),
|
||||
isSingleScan: doSingleScan ?? false,
|
||||
),
|
||||
).toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -824,15 +802,28 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
}
|
||||
}
|
||||
|
||||
// @override
|
||||
// @action
|
||||
// void onHeadersResponse(ElectrumHeaderResponse response) {
|
||||
// super.onHeadersResponse(response);
|
||||
@override
|
||||
@action
|
||||
Future<void> onHeadersResponse(ElectrumHeaderResponse response) async {
|
||||
super.onHeadersResponse(response);
|
||||
|
||||
// if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
|
||||
// _setListeners(walletInfo.restoreHeight);
|
||||
// }
|
||||
// }
|
||||
_setInitialScanHeight();
|
||||
|
||||
// New headers received, start scanning
|
||||
if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
|
||||
_setListeners(walletInfo.restoreHeight);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setInitialScanHeight() async {
|
||||
final validChainTip = currentChainTip != null && currentChainTip != 0;
|
||||
if (validChainTip && walletInfo.restoreHeight == 0) {
|
||||
await walletInfo.updateRestoreHeight(currentChainTip!);
|
||||
}
|
||||
}
|
||||
|
||||
static String _hardenedDerivationPath(String derivationPath) =>
|
||||
derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1);
|
||||
|
||||
@override
|
||||
@action
|
||||
|
@ -850,355 +841,4 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
super.syncStatusReaction(syncStatus);
|
||||
}
|
||||
}
|
||||
|
||||
static String _hardenedDerivationPath(String derivationPath) =>
|
||||
derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1);
|
||||
}
|
||||
|
||||
Future<void> startRefresh(ScanData scanData) async {
|
||||
int syncHeight = scanData.height;
|
||||
int initialSyncHeight = syncHeight;
|
||||
|
||||
final electrumClient = ElectrumApiProvider(
|
||||
await ElectrumTCPService.connect(
|
||||
scanData.node?.uri ?? Uri.parse("tcp://198.58.115.71:50001"),
|
||||
),
|
||||
);
|
||||
|
||||
int getCountPerRequest(int syncHeight) {
|
||||
if (scanData.isSingleScan) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
final amountLeft = scanData.chainTip - syncHeight + 1;
|
||||
return amountLeft;
|
||||
}
|
||||
|
||||
final receiver = Receiver(
|
||||
scanData.silentAddress.b_scan.toHex(),
|
||||
scanData.silentAddress.B_spend.toHex(),
|
||||
scanData.network == BitcoinNetwork.testnet,
|
||||
scanData.labelIndexes,
|
||||
scanData.labelIndexes.length,
|
||||
);
|
||||
|
||||
// Initial status UI update, send how many blocks in total to scan
|
||||
final initialCount = getCountPerRequest(syncHeight);
|
||||
scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
|
||||
|
||||
final listener = await electrumClient.subscribe(
|
||||
ElectrumTweaksSubscribe(height: syncHeight, count: initialCount),
|
||||
);
|
||||
|
||||
Future<void> listenFn(ElectrumTweaksSubscribeResponse response) async {
|
||||
// success or error msg
|
||||
final noData = response.message != null;
|
||||
|
||||
if (noData) {
|
||||
// re-subscribe to continue receiving messages, starting from the next unscanned height
|
||||
final nextHeight = syncHeight + 1;
|
||||
final nextCount = getCountPerRequest(nextHeight);
|
||||
|
||||
if (nextCount > 0) {
|
||||
final nextListener = await electrumClient.subscribe(
|
||||
ElectrumTweaksSubscribe(height: syncHeight, count: initialCount),
|
||||
);
|
||||
nextListener?.call(listenFn);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Continuous status UI update, send how many blocks left to scan
|
||||
final syncingStatus = scanData.isSingleScan
|
||||
? SyncingSyncStatus(1, 0)
|
||||
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
|
||||
scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
|
||||
|
||||
final tweakHeight = response.block;
|
||||
|
||||
try {
|
||||
final blockTweaks = response.blockTweaks;
|
||||
|
||||
for (final txid in blockTweaks.keys) {
|
||||
final tweakData = blockTweaks[txid];
|
||||
final outputPubkeys = tweakData!.outputPubkeys;
|
||||
final tweak = tweakData.tweak;
|
||||
|
||||
try {
|
||||
// scanOutputs called from rust here
|
||||
final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver);
|
||||
|
||||
if (addToWallet.isEmpty) {
|
||||
// no results tx, continue to next tx
|
||||
continue;
|
||||
}
|
||||
|
||||
// placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
|
||||
final txInfo = ElectrumTransactionInfo(
|
||||
WalletType.bitcoin,
|
||||
id: txid,
|
||||
height: tweakHeight,
|
||||
amount: 0,
|
||||
fee: 0,
|
||||
direction: TransactionDirection.incoming,
|
||||
isPending: false,
|
||||
isReplaced: false,
|
||||
date: scanData.network == BitcoinNetwork.mainnet
|
||||
? getDateByBitcoinHeight(tweakHeight)
|
||||
: DateTime.now(),
|
||||
confirmations: scanData.chainTip - tweakHeight + 1,
|
||||
unspents: [],
|
||||
isReceivedSilentPayment: true,
|
||||
);
|
||||
|
||||
addToWallet.forEach((label, value) {
|
||||
(value as Map<String, dynamic>).forEach((output, tweak) {
|
||||
final t_k = tweak.toString();
|
||||
|
||||
final receivingOutputAddress = ECPublic.fromHex(output)
|
||||
.toTaprootAddress(tweak: false)
|
||||
.toAddress(scanData.network);
|
||||
|
||||
final matchingOutput = outputPubkeys[output]!;
|
||||
final amount = matchingOutput.amount;
|
||||
final pos = matchingOutput.vout;
|
||||
|
||||
final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
|
||||
receivingOutputAddress,
|
||||
labelIndex: 1, // TODO: get actual index/label
|
||||
isUsed: true,
|
||||
spendKey: scanData.silentAddress.b_spend.tweakAdd(
|
||||
BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)),
|
||||
),
|
||||
txCount: 1,
|
||||
balance: amount,
|
||||
);
|
||||
|
||||
final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos);
|
||||
|
||||
txInfo.unspents!.add(unspent);
|
||||
txInfo.amount += unspent.value;
|
||||
});
|
||||
});
|
||||
|
||||
scanData.sendPort.send({txInfo.id: txInfo});
|
||||
} catch (e, stacktrace) {
|
||||
print(stacktrace);
|
||||
print(e.toString());
|
||||
}
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
print(stacktrace);
|
||||
print(e.toString());
|
||||
}
|
||||
|
||||
syncHeight = tweakHeight;
|
||||
|
||||
if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
|
||||
if (tweakHeight >= scanData.chainTip)
|
||||
scanData.sendPort.send(SyncResponse(
|
||||
syncHeight,
|
||||
SyncedTipSyncStatus(scanData.chainTip),
|
||||
));
|
||||
|
||||
if (scanData.isSingleScan) {
|
||||
scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listener?.call(listenFn);
|
||||
|
||||
// if (tweaksSubscription == null) {
|
||||
// return scanData.sendPort.send(
|
||||
// SyncResponse(syncHeight, UnsupportedSyncStatus()),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> delegatedScan(ScanData scanData) async {
|
||||
// int syncHeight = scanData.height;
|
||||
// int initialSyncHeight = syncHeight;
|
||||
|
||||
// BehaviorSubject<Object>? tweaksSubscription = null;
|
||||
|
||||
// final electrumClient = scanData.electrumClient;
|
||||
// await electrumClient.connectToUri(
|
||||
// scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"),
|
||||
// useSSL: scanData.node?.useSSL ?? false,
|
||||
// );
|
||||
|
||||
// if (tweaksSubscription == null) {
|
||||
// scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
|
||||
|
||||
// tweaksSubscription = await electrumClient.tweaksScan(
|
||||
// pubSpendKey: scanData.silentAddress.B_spend.toHex(),
|
||||
// );
|
||||
|
||||
// Future<void> listenFn(t) async {
|
||||
// final tweaks = t as Map<String, dynamic>;
|
||||
// final msg = tweaks["message"];
|
||||
|
||||
// // success or error msg
|
||||
// final noData = msg != null;
|
||||
// if (noData) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Continuous status UI update, send how many blocks left to scan
|
||||
// final syncingStatus = scanData.isSingleScan
|
||||
// ? SyncingSyncStatus(1, 0)
|
||||
// : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
|
||||
// scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
|
||||
|
||||
// final blockHeight = tweaks.keys.first;
|
||||
// final tweakHeight = int.parse(blockHeight);
|
||||
|
||||
// try {
|
||||
// final blockTweaks = tweaks[blockHeight] as Map<String, dynamic>;
|
||||
|
||||
// for (var j = 0; j < blockTweaks.keys.length; j++) {
|
||||
// final txid = blockTweaks.keys.elementAt(j);
|
||||
// final details = blockTweaks[txid] as Map<String, dynamic>;
|
||||
// final outputPubkeys = (details["output_pubkeys"] as Map<dynamic, dynamic>);
|
||||
// final spendingKey = details["spending_key"].toString();
|
||||
|
||||
// try {
|
||||
// // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
|
||||
// final txInfo = ElectrumTransactionInfo(
|
||||
// WalletType.bitcoin,
|
||||
// id: txid,
|
||||
// height: tweakHeight,
|
||||
// amount: 0,
|
||||
// fee: 0,
|
||||
// direction: TransactionDirection.incoming,
|
||||
// isPending: false,
|
||||
// isReplaced: false,
|
||||
// date: scanData.network == BitcoinNetwork.mainnet
|
||||
// ? getDateByBitcoinHeight(tweakHeight)
|
||||
// : DateTime.now(),
|
||||
// confirmations: scanData.chainTip - tweakHeight + 1,
|
||||
// unspents: [],
|
||||
// isReceivedSilentPayment: true,
|
||||
// );
|
||||
|
||||
// outputPubkeys.forEach((pos, value) {
|
||||
// final secKey = ECPrivate.fromHex(spendingKey);
|
||||
// final receivingOutputAddress =
|
||||
// secKey.getPublic().toTaprootAddress(tweak: false).toAddress(scanData.network);
|
||||
|
||||
// late int amount;
|
||||
// try {
|
||||
// amount = int.parse(value[1].toString());
|
||||
// } catch (_) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
|
||||
// receivingOutputAddress,
|
||||
// labelIndex: 0,
|
||||
// isUsed: true,
|
||||
// spendKey: secKey,
|
||||
// txCount: 1,
|
||||
// balance: amount,
|
||||
// );
|
||||
|
||||
// final unspent = BitcoinUnspent(
|
||||
// receivedAddressRecord,
|
||||
// txid,
|
||||
// amount,
|
||||
// int.parse(pos.toString()),
|
||||
// );
|
||||
|
||||
// txInfo.unspents!.add(unspent);
|
||||
// txInfo.amount += unspent.value;
|
||||
// });
|
||||
|
||||
// scanData.sendPort.send({txInfo.id: txInfo});
|
||||
// } catch (_) {}
|
||||
// }
|
||||
// } catch (_) {}
|
||||
|
||||
// syncHeight = tweakHeight;
|
||||
|
||||
// if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
|
||||
// if (tweakHeight >= scanData.chainTip)
|
||||
// scanData.sendPort.send(SyncResponse(
|
||||
// syncHeight,
|
||||
// SyncedTipSyncStatus(scanData.chainTip),
|
||||
// ));
|
||||
|
||||
// if (scanData.isSingleScan) {
|
||||
// scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
|
||||
// }
|
||||
|
||||
// await tweaksSubscription!.close();
|
||||
// await electrumClient.close();
|
||||
// }
|
||||
// }
|
||||
|
||||
// tweaksSubscription?.listen(listenFn);
|
||||
// }
|
||||
|
||||
// if (tweaksSubscription == null) {
|
||||
// return scanData.sendPort.send(
|
||||
// SyncResponse(syncHeight, UnsupportedSyncStatus()),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
class ScanNode {
|
||||
final Uri uri;
|
||||
final bool? useSSL;
|
||||
|
||||
ScanNode(this.uri, this.useSSL);
|
||||
}
|
||||
|
||||
class ScanData {
|
||||
final SendPort sendPort;
|
||||
final SilentPaymentOwner silentAddress;
|
||||
final int height;
|
||||
final ScanNode? node;
|
||||
final BasedUtxoNetwork network;
|
||||
final int chainTip;
|
||||
final List<String> transactionHistoryIds;
|
||||
final Map<String, String> labels;
|
||||
final List<int> labelIndexes;
|
||||
final bool isSingleScan;
|
||||
|
||||
ScanData({
|
||||
required this.sendPort,
|
||||
required this.silentAddress,
|
||||
required this.height,
|
||||
required this.node,
|
||||
required this.network,
|
||||
required this.chainTip,
|
||||
required this.transactionHistoryIds,
|
||||
required this.labels,
|
||||
required this.labelIndexes,
|
||||
required this.isSingleScan,
|
||||
});
|
||||
|
||||
factory ScanData.fromHeight(ScanData scanData, int newHeight) {
|
||||
return ScanData(
|
||||
sendPort: scanData.sendPort,
|
||||
silentAddress: scanData.silentAddress,
|
||||
height: newHeight,
|
||||
node: scanData.node,
|
||||
network: scanData.network,
|
||||
chainTip: scanData.chainTip,
|
||||
transactionHistoryIds: scanData.transactionHistoryIds,
|
||||
labels: scanData.labels,
|
||||
labelIndexes: scanData.labelIndexes,
|
||||
isSingleScan: scanData.isSingleScan,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SyncResponse {
|
||||
final int height;
|
||||
final SyncStatus syncStatus;
|
||||
|
||||
SyncResponse(this.height, this.syncStatus);
|
||||
}
|
||||
|
|
|
@ -23,8 +23,8 @@ class BitcoinWalletService extends WalletService<
|
|||
this.walletInfoSource,
|
||||
this.unspentCoinsInfoSource,
|
||||
this.alwaysScan,
|
||||
this.mempoolAPIEnabled,
|
||||
this.isDirect,
|
||||
this.mempoolAPIEnabled,
|
||||
);
|
||||
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/address_from_output.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
|
@ -11,13 +10,35 @@ import 'package:cw_core/wallet_type.dart';
|
|||
import 'package:hex/hex.dart';
|
||||
|
||||
class ElectrumTransactionBundle {
|
||||
ElectrumTransactionBundle(this.originalTransaction,
|
||||
{required this.ins, required this.confirmations, this.time});
|
||||
ElectrumTransactionBundle(
|
||||
this.originalTransaction, {
|
||||
required this.ins,
|
||||
required this.confirmations,
|
||||
this.time,
|
||||
});
|
||||
|
||||
final BtcTransaction originalTransaction;
|
||||
final List<BtcTransaction> ins;
|
||||
final int? time;
|
||||
final int confirmations;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'originalTransaction': originalTransaction.toHex(),
|
||||
'ins': ins.map((e) => e.toHex()).toList(),
|
||||
'confirmations': confirmations,
|
||||
'time': time,
|
||||
};
|
||||
}
|
||||
|
||||
static ElectrumTransactionBundle fromJson(Map<String, dynamic> data) {
|
||||
return ElectrumTransactionBundle(
|
||||
BtcTransaction.fromRaw(data['originalTransaction'] as String),
|
||||
ins: (data['ins'] as List<Object>).map((e) => BtcTransaction.fromRaw(e as String)).toList(),
|
||||
confirmations: data['confirmations'] as int,
|
||||
time: data['time'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ElectrumTransactionInfo extends TransactionInfo {
|
||||
|
@ -128,9 +149,11 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
final inputTransaction = bundle.ins[i];
|
||||
final outTransaction = inputTransaction.outputs[input.txIndex];
|
||||
inputAmount += outTransaction.amount.toInt();
|
||||
if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) {
|
||||
if (addresses.contains(
|
||||
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network))) {
|
||||
direction = TransactionDirection.outgoing;
|
||||
inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network));
|
||||
inputAddresses.add(
|
||||
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -144,8 +167,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
final receivedAmounts = <int>[];
|
||||
for (final out in bundle.originalTransaction.outputs) {
|
||||
totalOutAmount += out.amount.toInt();
|
||||
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
|
||||
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||
final addressExists = addresses
|
||||
.contains(BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network));
|
||||
final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network);
|
||||
|
||||
if (address.isNotEmpty) outputAddresses.add(address);
|
||||
|
||||
|
|
|
@ -6,11 +6,11 @@ import 'dart:isolate';
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/electrum_worker.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_bitcoin/address_from_output.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||
|
@ -25,7 +25,7 @@ import 'package:cw_bitcoin/exceptions.dart';
|
|||
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
// import 'package:cw_core/get_height_by_date.dart';
|
||||
import 'package:cw_core/get_height_by_date.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
|
@ -35,13 +35,11 @@ import 'package:cw_core/unspent_coins_info.dart';
|
|||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_keys_file.dart';
|
||||
// import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger;
|
||||
import 'package:mobx/mobx.dart';
|
||||
// import 'package:http/http.dart' as http;
|
||||
|
||||
part 'electrum_wallet.g.dart';
|
||||
|
||||
|
@ -52,8 +50,11 @@ abstract class ElectrumWalletBase
|
|||
with Store, WalletKeysFile {
|
||||
ReceivePort? receivePort;
|
||||
SendPort? workerSendPort;
|
||||
StreamSubscription? _workerSubscription;
|
||||
StreamSubscription<dynamic>? _workerSubscription;
|
||||
Isolate? _workerIsolate;
|
||||
final Map<int, dynamic> _responseCompleters = {};
|
||||
final Map<int, dynamic> _errorCompleters = {};
|
||||
int _messageId = 0;
|
||||
|
||||
ElectrumWalletBase({
|
||||
required String password,
|
||||
|
@ -67,7 +68,6 @@ abstract class ElectrumWalletBase
|
|||
List<int>? seedBytes,
|
||||
this.passphrase,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumClient? electrumClient,
|
||||
ElectrumBalance? initialBalance,
|
||||
CryptoCurrency? currency,
|
||||
this.alwaysScan,
|
||||
|
@ -103,7 +103,6 @@ abstract class ElectrumWalletBase
|
|||
this.isTestnet = !network.isMainnet,
|
||||
this._mnemonic = mnemonic,
|
||||
super(walletInfo) {
|
||||
this.electrumClient = electrumClient ?? ElectrumClient();
|
||||
this.walletInfo = walletInfo;
|
||||
transactionHistory = ElectrumTransactionHistory(
|
||||
walletInfo: walletInfo,
|
||||
|
@ -116,8 +115,27 @@ abstract class ElectrumWalletBase
|
|||
sharedPrefs.complete(SharedPreferences.getInstance());
|
||||
}
|
||||
|
||||
Future<dynamic> sendWorker(ElectrumWorkerRequest request) {
|
||||
final messageId = ++_messageId;
|
||||
|
||||
final completer = Completer<dynamic>();
|
||||
_responseCompleters[messageId] = completer;
|
||||
|
||||
final json = request.toJson();
|
||||
json['id'] = messageId;
|
||||
workerSendPort!.send(json);
|
||||
|
||||
try {
|
||||
return completer.future.timeout(Duration(seconds: 5));
|
||||
} catch (e) {
|
||||
_errorCompleters.addAll({messageId: e});
|
||||
_responseCompleters.remove(messageId);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> _handleWorkerResponse(dynamic message) async {
|
||||
Future<void> handleWorkerResponse(dynamic message) async {
|
||||
print('Main: received message: $message');
|
||||
|
||||
Map<String, dynamic> messageJson;
|
||||
|
@ -149,6 +167,12 @@ abstract class ElectrumWalletBase
|
|||
// return;
|
||||
// }
|
||||
|
||||
final responseId = messageJson['id'] as int?;
|
||||
if (responseId != null && _responseCompleters.containsKey(responseId)) {
|
||||
_responseCompleters[responseId]!.complete(message);
|
||||
_responseCompleters.remove(responseId);
|
||||
}
|
||||
|
||||
switch (workerMethod) {
|
||||
case ElectrumWorkerMethods.connectionMethod:
|
||||
final response = ElectrumWorkerConnectionResponse.fromJson(messageJson);
|
||||
|
@ -157,7 +181,6 @@ abstract class ElectrumWalletBase
|
|||
case ElectrumRequestMethods.headersSubscribeMethod:
|
||||
final response = ElectrumWorkerHeadersSubscribeResponse.fromJson(messageJson);
|
||||
await onHeadersResponse(response.result);
|
||||
|
||||
break;
|
||||
case ElectrumRequestMethods.getBalanceMethod:
|
||||
final response = ElectrumWorkerGetBalanceResponse.fromJson(messageJson);
|
||||
|
@ -167,6 +190,10 @@ abstract class ElectrumWalletBase
|
|||
final response = ElectrumWorkerGetHistoryResponse.fromJson(messageJson);
|
||||
onHistoriesResponse(response.result);
|
||||
break;
|
||||
case ElectrumRequestMethods.listunspentMethod:
|
||||
final response = ElectrumWorkerListUnspentResponse.fromJson(messageJson);
|
||||
onUnspentResponse(response.result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,7 +246,6 @@ abstract class ElectrumWalletBase
|
|||
@observable
|
||||
bool isEnabledAutoGenerateSubaddress;
|
||||
|
||||
late ElectrumClient electrumClient;
|
||||
ApiProvider? apiProvider;
|
||||
Box<UnspentCoinsInfo> unspentCoinsInfo;
|
||||
|
||||
|
@ -298,6 +324,7 @@ abstract class ElectrumWalletBase
|
|||
|
||||
bool _chainTipListenerOn = false;
|
||||
bool _isTransactionUpdating;
|
||||
bool _isInitialSync = true;
|
||||
|
||||
void Function(FlutterErrorDetails)? _onError;
|
||||
Timer? _autoSaveTimer;
|
||||
|
@ -323,16 +350,18 @@ abstract class ElectrumWalletBase
|
|||
syncStatus = SynchronizingSyncStatus();
|
||||
|
||||
// INFO: FIRST: Call subscribe for headers, get the initial chainTip update in case it is zero
|
||||
await subscribeForHeaders();
|
||||
await sendWorker(ElectrumWorkerHeadersSubscribeRequest());
|
||||
|
||||
// INFO: SECOND: Start loading transaction histories for every address, this will help discover addresses until the unused gap limit has been reached, which will help finding the full balance and unspents later.
|
||||
await updateTransactions();
|
||||
|
||||
// await updateAllUnspents();
|
||||
// INFO: THIRD: Start loading the TX history
|
||||
await updateBalance();
|
||||
|
||||
// await subscribeForUpdates();
|
||||
// INFO: FOURTH: Finish with unspents
|
||||
await updateAllUnspents();
|
||||
|
||||
_isInitialSync = false;
|
||||
|
||||
// await updateFeeRates();
|
||||
|
||||
|
@ -377,7 +406,7 @@ abstract class ElectrumWalletBase
|
|||
return false;
|
||||
}
|
||||
|
||||
final version = await electrumClient.version();
|
||||
// final version = await electrumClient.version();
|
||||
|
||||
if (version.isNotEmpty) {
|
||||
final server = version[0];
|
||||
|
@ -416,10 +445,13 @@ abstract class ElectrumWalletBase
|
|||
if (message is SendPort) {
|
||||
workerSendPort = message;
|
||||
workerSendPort!.send(
|
||||
ElectrumWorkerConnectionRequest(uri: node.uri).toJson(),
|
||||
ElectrumWorkerConnectionRequest(
|
||||
uri: node.uri,
|
||||
network: network,
|
||||
).toJson(),
|
||||
);
|
||||
} else {
|
||||
_handleWorkerResponse(message);
|
||||
handleWorkerResponse(message);
|
||||
}
|
||||
});
|
||||
} catch (e, stacktrace) {
|
||||
|
@ -927,11 +959,10 @@ abstract class ElectrumWalletBase
|
|||
return PendingBitcoinTransaction(
|
||||
transaction,
|
||||
type,
|
||||
electrumClient: electrumClient,
|
||||
sendWorker: sendWorker,
|
||||
amount: estimatedTx.amount,
|
||||
fee: estimatedTx.fee,
|
||||
feeRate: feeRateInt.toString(),
|
||||
network: network,
|
||||
hasChange: estimatedTx.hasChange,
|
||||
isSendAll: estimatedTx.isSendAll,
|
||||
hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot
|
||||
|
@ -1007,11 +1038,10 @@ abstract class ElectrumWalletBase
|
|||
return PendingBitcoinTransaction(
|
||||
transaction,
|
||||
type,
|
||||
electrumClient: electrumClient,
|
||||
sendWorker: sendWorker,
|
||||
amount: estimatedTx.amount,
|
||||
fee: estimatedTx.fee,
|
||||
feeRate: feeRateInt.toString(),
|
||||
network: network,
|
||||
hasChange: estimatedTx.hasChange,
|
||||
isSendAll: estimatedTx.isSendAll,
|
||||
hasTaprootInputs: hasTaprootInputs,
|
||||
|
@ -1177,7 +1207,9 @@ abstract class ElectrumWalletBase
|
|||
@override
|
||||
Future<void> close({required bool shouldCleanup}) async {
|
||||
try {
|
||||
await electrumClient.close();
|
||||
_workerIsolate!.kill(priority: Isolate.immediate);
|
||||
await _workerSubscription?.cancel();
|
||||
receivePort?.close();
|
||||
} catch (_) {}
|
||||
_autoSaveTimer?.cancel();
|
||||
_updateFeeRateTimer?.cancel();
|
||||
|
@ -1185,25 +1217,15 @@ abstract class ElectrumWalletBase
|
|||
|
||||
@action
|
||||
Future<void> updateAllUnspents() async {
|
||||
List<BitcoinUnspent> updatedUnspentCoins = [];
|
||||
|
||||
Set<String> scripthashes = {};
|
||||
walletAddresses.allAddresses.forEach((addressRecord) {
|
||||
scripthashes.add(addressRecord.scriptHash);
|
||||
});
|
||||
|
||||
workerSendPort!.send(
|
||||
ElectrumWorkerGetBalanceRequest(scripthashes: scripthashes).toJson(),
|
||||
final req = ElectrumWorkerListUnspentRequest(
|
||||
scripthashes: walletAddresses.allScriptHashes.toList(),
|
||||
);
|
||||
|
||||
await Future.wait(walletAddresses.allAddresses
|
||||
.where((element) => element.type != SegwitAddresType.mweb)
|
||||
.map((address) async {
|
||||
updatedUnspentCoins.addAll(await fetchUnspent(address));
|
||||
}));
|
||||
|
||||
await updateCoins(unspentCoins.toSet());
|
||||
await refreshUnspentCoinsInfo();
|
||||
if (_isInitialSync) {
|
||||
await sendWorker(req);
|
||||
} else {
|
||||
workerSendPort!.send(req.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -1227,46 +1249,38 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
@action
|
||||
Future<void> updateCoins(Set<BitcoinUnspent> newUnspentCoins) async {
|
||||
if (newUnspentCoins.isEmpty) {
|
||||
return;
|
||||
}
|
||||
newUnspentCoins.forEach(updateCoin);
|
||||
Future<void> onUnspentResponse(Map<String, List<ElectrumUtxo>> unspents) async {
|
||||
final updatedUnspentCoins = <BitcoinUnspent>[];
|
||||
|
||||
await Future.wait(unspents.entries.map((entry) async {
|
||||
final unspent = entry.value;
|
||||
final scriptHash = entry.key;
|
||||
|
||||
final addressRecord = walletAddresses.allAddresses.firstWhereOrNull(
|
||||
(element) => element.scriptHash == scriptHash,
|
||||
);
|
||||
|
||||
if (addressRecord == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> updateUnspentsForAddress(BitcoinAddressRecord addressRecord) async {
|
||||
final newUnspentCoins = (await fetchUnspent(addressRecord)).toSet();
|
||||
await updateCoins(newUnspentCoins);
|
||||
|
||||
unspentCoins.addAll(newUnspentCoins);
|
||||
|
||||
// if (unspentCoinsInfo.length != unspentCoins.length) {
|
||||
// unspentCoins.forEach(addCoinInfo);
|
||||
// }
|
||||
|
||||
// await refreshUnspentCoinsInfo();
|
||||
await Future.wait(unspent.map((unspent) async {
|
||||
final coin = BitcoinUnspent.fromJSON(addressRecord, unspent.toJson());
|
||||
coin.isChange = addressRecord.isChange;
|
||||
final tx = await fetchTransactionInfo(hash: coin.hash);
|
||||
if (tx != null) {
|
||||
coin.confirmations = tx.confirmations;
|
||||
}
|
||||
|
||||
@action
|
||||
Future<List<BitcoinUnspent>> fetchUnspent(BitcoinAddressRecord address) async {
|
||||
List<Map<String, dynamic>> unspents = [];
|
||||
List<BitcoinUnspent> updatedUnspentCoins = [];
|
||||
|
||||
unspents = await electrumClient.getListUnspent(address.scriptHash);
|
||||
|
||||
await Future.wait(unspents.map((unspent) async {
|
||||
try {
|
||||
final coin = BitcoinUnspent.fromJSON(address, unspent);
|
||||
// final tx = await fetchTransactionInfo(hash: coin.hash);
|
||||
coin.isChange = address.isHidden;
|
||||
// coin.confirmations = tx?.confirmations;
|
||||
|
||||
updatedUnspentCoins.add(coin);
|
||||
} catch (_) {}
|
||||
}));
|
||||
}));
|
||||
|
||||
return updatedUnspentCoins;
|
||||
unspentCoins.clear();
|
||||
unspentCoins.addAll(updatedUnspentCoins);
|
||||
unspentCoins.forEach(updateCoin);
|
||||
|
||||
await refreshUnspentCoinsInfo();
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -1287,7 +1301,6 @@ abstract class ElectrumWalletBase
|
|||
await unspentCoinsInfo.add(newInfo);
|
||||
}
|
||||
|
||||
// TODO: ?
|
||||
Future<void> refreshUnspentCoinsInfo() async {
|
||||
try {
|
||||
final List<dynamic> keys = <dynamic>[];
|
||||
|
@ -1313,6 +1326,92 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> onHeadersResponse(ElectrumHeaderResponse response) async {
|
||||
currentChainTip = response.height;
|
||||
|
||||
bool updated = false;
|
||||
transactionHistory.transactions.values.forEach((tx) {
|
||||
if (tx.height != null && tx.height! > 0) {
|
||||
final newConfirmations = currentChainTip! - tx.height! + 1;
|
||||
|
||||
if (tx.confirmations != newConfirmations) {
|
||||
tx.confirmations = newConfirmations;
|
||||
tx.isPending = tx.confirmations == 0;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (updated) {
|
||||
await save();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> subscribeForHeaders() async {
|
||||
if (_chainTipListenerOn) return;
|
||||
|
||||
workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson());
|
||||
_chainTipListenerOn = true;
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> onHistoriesResponse(List<AddressHistoriesResponse> histories) async {
|
||||
if (histories.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final firstAddress = histories.first;
|
||||
final isChange = firstAddress.addressRecord.isChange;
|
||||
final type = firstAddress.addressRecord.type;
|
||||
|
||||
final totalAddresses = (isChange
|
||||
? walletAddresses.receiveAddresses.where((element) => element.type == type).length
|
||||
: walletAddresses.changeAddresses.where((element) => element.type == type).length);
|
||||
final gapLimit = (isChange
|
||||
? ElectrumWalletAddressesBase.defaultChangeAddressesCount
|
||||
: ElectrumWalletAddressesBase.defaultReceiveAddressesCount);
|
||||
|
||||
bool hasUsedAddressesUnderGap = false;
|
||||
final addressesWithHistory = <BitcoinAddressRecord>[];
|
||||
|
||||
for (final addressHistory in histories) {
|
||||
final txs = addressHistory.txs;
|
||||
|
||||
if (txs.isNotEmpty) {
|
||||
final address = addressHistory.addressRecord;
|
||||
addressesWithHistory.add(address);
|
||||
|
||||
hasUsedAddressesUnderGap =
|
||||
address.index < totalAddresses && (address.index >= totalAddresses - gapLimit);
|
||||
|
||||
for (final tx in txs) {
|
||||
transactionHistory.addOne(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addressesWithHistory.isNotEmpty) {
|
||||
walletAddresses.updateAdresses(addressesWithHistory);
|
||||
}
|
||||
|
||||
if (hasUsedAddressesUnderGap) {
|
||||
// Discover new addresses for the same address type until the gap limit is respected
|
||||
final newAddresses = await walletAddresses.discoverAddresses(
|
||||
isChange: isChange,
|
||||
derivationType: firstAddress.addressRecord.derivationType,
|
||||
type: type,
|
||||
derivationInfo: BitcoinAddressUtils.getDerivationFromType(type),
|
||||
);
|
||||
|
||||
if (newAddresses.isNotEmpty) {
|
||||
// Update the transactions for the new discovered addresses
|
||||
await updateTransactions(newAddresses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async {
|
||||
try {
|
||||
final bundle = await getTransactionExpanded(hash: tx.txHash);
|
||||
|
@ -1331,8 +1430,9 @@ abstract class ElectrumWalletBase
|
|||
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isChange);
|
||||
|
||||
// look for a change address in the outputs
|
||||
final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any(
|
||||
(element) => element.address == addressFromOutputScript(output.scriptPubKey, network)));
|
||||
final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any((element) =>
|
||||
element.address ==
|
||||
BitcoinAddressUtils.addressFromOutputScript(output.scriptPubKey, network)));
|
||||
|
||||
var allInputsAmount = 0;
|
||||
|
||||
|
@ -1370,7 +1470,8 @@ abstract class ElectrumWalletBase
|
|||
final inputTransaction = bundle.ins[i];
|
||||
final vout = input.txIndex;
|
||||
final outTransaction = inputTransaction.outputs[vout];
|
||||
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
|
||||
final address =
|
||||
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network);
|
||||
// allInputsAmount += outTransaction.amount.toInt();
|
||||
|
||||
final addressRecord =
|
||||
|
@ -1417,7 +1518,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||
final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network);
|
||||
final btcAddress = RegexUtils.addressTypeFromStr(address, network);
|
||||
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
|
||||
}
|
||||
|
@ -1496,10 +1597,9 @@ abstract class ElectrumWalletBase
|
|||
return PendingBitcoinTransaction(
|
||||
transaction,
|
||||
type,
|
||||
electrumClient: electrumClient,
|
||||
sendWorker: sendWorker,
|
||||
amount: sendingAmount,
|
||||
fee: newFee,
|
||||
network: network,
|
||||
hasChange: changeOutputs.isNotEmpty,
|
||||
feeRate: newFee.toString(),
|
||||
)..addListener((transaction) async {
|
||||
|
@ -1519,27 +1619,23 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
Future<ElectrumTransactionBundle> getTransactionExpanded({required String hash}) async {
|
||||
int? time;
|
||||
int? height;
|
||||
final transactionHex = await electrumClient.getTransactionHex(hash: hash);
|
||||
|
||||
int? confirmations;
|
||||
|
||||
final original = BtcTransaction.fromRaw(transactionHex);
|
||||
final ins = <BtcTransaction>[];
|
||||
|
||||
for (final vin in original.inputs) {
|
||||
final inputTransactionHex = await electrumClient.getTransactionHex(hash: hash);
|
||||
|
||||
ins.add(BtcTransaction.fromRaw(inputTransactionHex));
|
||||
return await sendWorker(
|
||||
ElectrumWorkerTxExpandedRequest(txHash: hash, currentChainTip: currentChainTip!))
|
||||
as ElectrumTransactionBundle;
|
||||
}
|
||||
|
||||
return ElectrumTransactionBundle(
|
||||
original,
|
||||
ins: ins,
|
||||
time: time,
|
||||
confirmations: confirmations ?? 0,
|
||||
Future<ElectrumTransactionInfo?> fetchTransactionInfo({required String hash, int? height}) async {
|
||||
try {
|
||||
return ElectrumTransactionInfo.fromElectrumBundle(
|
||||
await getTransactionExpanded(hash: hash),
|
||||
walletInfo.type,
|
||||
network,
|
||||
addresses: walletAddresses.allAddresses.map((e) => e.address).toSet(),
|
||||
height: height,
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1550,27 +1646,24 @@ abstract class ElectrumWalletBase
|
|||
|
||||
@action
|
||||
Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async {
|
||||
// TODO: all
|
||||
addresses ??= walletAddresses.allAddresses
|
||||
.where(
|
||||
(element) => element.type == SegwitAddresType.p2wpkh && element.isChange == false,
|
||||
)
|
||||
.toList();
|
||||
addresses ??= walletAddresses.allAddresses.toList();
|
||||
|
||||
workerSendPort!.send(
|
||||
ElectrumWorkerGetHistoryRequest(
|
||||
final req = ElectrumWorkerGetHistoryRequest(
|
||||
addresses: addresses,
|
||||
storedTxs: transactionHistory.transactions.values.toList(),
|
||||
walletType: type,
|
||||
// If we still don't have currentChainTip, txs will still be fetched but shown
|
||||
// with confirmations as 0 but will be auto fixed on onHeadersResponse
|
||||
chainTip: currentChainTip ?? 0,
|
||||
chainTip: currentChainTip ?? getBitcoinHeightByDate(date: DateTime.now()),
|
||||
network: network,
|
||||
// mempoolAPIEnabled: mempoolAPIEnabled,
|
||||
// TODO:
|
||||
mempoolAPIEnabled: true,
|
||||
).toJson(),
|
||||
mempoolAPIEnabled: mempoolAPIEnabled,
|
||||
);
|
||||
|
||||
if (_isInitialSync) {
|
||||
await sendWorker(req);
|
||||
} else {
|
||||
workerSendPort!.send(req.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -1594,16 +1687,41 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
@action
|
||||
Future<void> updateBalance() async {
|
||||
workerSendPort!.send(
|
||||
ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes).toJson(),
|
||||
void onBalanceResponse(ElectrumBalance balanceResult) {
|
||||
var totalFrozen = 0;
|
||||
var totalConfirmed = balanceResult.confirmed;
|
||||
var totalUnconfirmed = balanceResult.unconfirmed;
|
||||
|
||||
unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) {
|
||||
if (unspentCoinInfo.isFrozen) {
|
||||
// TODO: verify this works well
|
||||
totalFrozen += unspentCoinInfo.value;
|
||||
totalConfirmed -= unspentCoinInfo.value;
|
||||
totalUnconfirmed -= unspentCoinInfo.value;
|
||||
}
|
||||
});
|
||||
|
||||
balance[currency] = ElectrumBalance(
|
||||
confirmed: totalConfirmed,
|
||||
unconfirmed: totalUnconfirmed,
|
||||
frozen: totalFrozen,
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> updateBalance() async {
|
||||
final req = ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes);
|
||||
|
||||
if (_isInitialSync) {
|
||||
await sendWorker(req);
|
||||
} else {
|
||||
workerSendPort!.send(req.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError;
|
||||
|
||||
@override
|
||||
Future<String> signMessage(String message, {String? address = null}) async {
|
||||
final record = walletAddresses.getFromAddresses(address!);
|
||||
|
||||
|
@ -1678,115 +1796,6 @@ abstract class ElectrumWalletBase
|
|||
return false;
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> onHistoriesResponse(List<AddressHistoriesResponse> histories) async {
|
||||
if (histories.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final firstAddress = histories.first;
|
||||
final isChange = firstAddress.addressRecord.isChange;
|
||||
final type = firstAddress.addressRecord.type;
|
||||
|
||||
final totalAddresses = (isChange
|
||||
? walletAddresses.receiveAddresses.where((element) => element.type == type).length
|
||||
: walletAddresses.changeAddresses.where((element) => element.type == type).length);
|
||||
final gapLimit = (isChange
|
||||
? ElectrumWalletAddressesBase.defaultChangeAddressesCount
|
||||
: ElectrumWalletAddressesBase.defaultReceiveAddressesCount);
|
||||
bool hasUsedAddressesUnderGap = false;
|
||||
|
||||
final addressesWithHistory = <BitcoinAddressRecord>[];
|
||||
|
||||
for (final addressHistory in histories) {
|
||||
final txs = addressHistory.txs;
|
||||
|
||||
if (txs.isNotEmpty) {
|
||||
final address = addressHistory.addressRecord;
|
||||
addressesWithHistory.add(address);
|
||||
|
||||
hasUsedAddressesUnderGap =
|
||||
address.index < totalAddresses && (address.index >= totalAddresses - gapLimit);
|
||||
|
||||
for (final tx in txs) {
|
||||
transactionHistory.addOne(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addressesWithHistory.isNotEmpty) {
|
||||
walletAddresses.updateAdresses(addressesWithHistory);
|
||||
}
|
||||
|
||||
if (hasUsedAddressesUnderGap) {
|
||||
// Discover new addresses for the same address type until the gap limit is respected
|
||||
final newAddresses = await walletAddresses.discoverAddresses(
|
||||
isChange: isChange,
|
||||
derivationType: firstAddress.addressRecord.derivationType,
|
||||
type: type,
|
||||
derivationInfo: BitcoinAddressUtils.getDerivationFromType(type),
|
||||
);
|
||||
|
||||
if (newAddresses.isNotEmpty) {
|
||||
// Update the transactions for the new discovered addresses
|
||||
await updateTransactions(newAddresses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void onBalanceResponse(ElectrumBalance balanceResult) {
|
||||
var totalFrozen = 0;
|
||||
var totalConfirmed = balanceResult.confirmed;
|
||||
var totalUnconfirmed = balanceResult.unconfirmed;
|
||||
|
||||
unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) {
|
||||
if (unspentCoinInfo.isFrozen) {
|
||||
// TODO: verify this works well
|
||||
totalFrozen += unspentCoinInfo.value;
|
||||
totalConfirmed -= unspentCoinInfo.value;
|
||||
totalUnconfirmed -= unspentCoinInfo.value;
|
||||
}
|
||||
});
|
||||
|
||||
balance[currency] = ElectrumBalance(
|
||||
confirmed: totalConfirmed,
|
||||
unconfirmed: totalUnconfirmed,
|
||||
frozen: totalFrozen,
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> onHeadersResponse(ElectrumHeaderResponse response) async {
|
||||
currentChainTip = response.height;
|
||||
|
||||
bool updated = false;
|
||||
transactionHistory.transactions.values.forEach((tx) {
|
||||
if (tx.height != null && tx.height! > 0) {
|
||||
final newConfirmations = currentChainTip! - tx.height! + 1;
|
||||
|
||||
if (tx.confirmations != newConfirmations) {
|
||||
tx.confirmations = newConfirmations;
|
||||
tx.isPending = tx.confirmations == 0;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (updated) {
|
||||
await save();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> subscribeForHeaders() async {
|
||||
print(_chainTipListenerOn);
|
||||
if (_chainTipListenerOn) return;
|
||||
|
||||
workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson());
|
||||
_chainTipListenerOn = true;
|
||||
}
|
||||
|
||||
@action
|
||||
void _onConnectionStatusChange(ConnectionStatus status) {
|
||||
switch (status) {
|
||||
|
@ -1862,14 +1871,15 @@ abstract class ElectrumWalletBase
|
|||
final inputTransaction = bundle.ins[i];
|
||||
final vout = input.txIndex;
|
||||
final outTransaction = inputTransaction.outputs[vout];
|
||||
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
|
||||
final address =
|
||||
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network);
|
||||
|
||||
if (address.isNotEmpty) inputAddresses.add(address);
|
||||
}
|
||||
|
||||
for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) {
|
||||
final out = bundle.originalTransaction.outputs[i];
|
||||
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||
final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network);
|
||||
|
||||
if (address.isNotEmpty) outputAddresses.add(address);
|
||||
|
||||
|
|
|
@ -649,7 +649,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
),
|
||||
index: i,
|
||||
isChange: isChange,
|
||||
isHidden: derivationType == CWBitcoinDerivationType.old,
|
||||
isHidden: derivationType == CWBitcoinDerivationType.old && type != SegwitAddresType.p2wpkh,
|
||||
type: type ?? addressPageType,
|
||||
network: network,
|
||||
derivationInfo: derivationInfo,
|
||||
|
@ -664,7 +664,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
@action
|
||||
void updateAdresses(Iterable<BitcoinAddressRecord> addresses) {
|
||||
for (final address in addresses) {
|
||||
_allAddresses.replaceRange(address.index, address.index + 1, [address]);
|
||||
final index = _allAddresses.indexWhere((element) => element.address == address.address);
|
||||
_allAddresses.replaceRange(index, index + 1, [address]);
|
||||
|
||||
updateAddressesByMatch();
|
||||
updateReceiveAddresses();
|
||||
updateChangeAddresses();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,16 +3,20 @@ import 'dart:convert';
|
|||
import 'dart:isolate';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||
import 'package:cw_core/get_height_by_date.dart';
|
||||
import 'package:cw_bitcoin/electrum_balance.dart';
|
||||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
|
||||
// import 'package:cw_bitcoin/electrum_balance.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
// TODO: ping
|
||||
import 'package:sp_scanner/sp_scanner.dart';
|
||||
|
||||
class ElectrumWorker {
|
||||
final SendPort sendPort;
|
||||
|
@ -56,8 +60,15 @@ class ElectrumWorker {
|
|||
ElectrumWorkerConnectionRequest.fromJson(messageJson),
|
||||
);
|
||||
break;
|
||||
case ElectrumWorkerMethods.txHashMethod:
|
||||
await _handleGetTxExpanded(
|
||||
ElectrumWorkerTxExpandedRequest.fromJson(messageJson),
|
||||
);
|
||||
break;
|
||||
case ElectrumRequestMethods.headersSubscribeMethod:
|
||||
await _handleHeadersSubscribe();
|
||||
await _handleHeadersSubscribe(
|
||||
ElectrumWorkerHeadersSubscribeRequest.fromJson(messageJson),
|
||||
);
|
||||
break;
|
||||
case ElectrumRequestMethods.scripthashesSubscribeMethod:
|
||||
await _handleScriphashesSubscribe(
|
||||
|
@ -74,12 +85,21 @@ class ElectrumWorker {
|
|||
ElectrumWorkerGetHistoryRequest.fromJson(messageJson),
|
||||
);
|
||||
break;
|
||||
case 'blockchain.scripthash.listunspent':
|
||||
// await _handleListUnspent(workerMessage);
|
||||
case ElectrumRequestMethods.listunspentMethod:
|
||||
await _handleListUnspent(
|
||||
ElectrumWorkerListUnspentRequest.fromJson(messageJson),
|
||||
);
|
||||
break;
|
||||
case ElectrumRequestMethods.broadcastMethod:
|
||||
await _handleBroadcast(
|
||||
ElectrumWorkerBroadcastRequest.fromJson(messageJson),
|
||||
);
|
||||
break;
|
||||
case ElectrumRequestMethods.tweaksSubscribeMethod:
|
||||
await _handleScanSilentPayments(
|
||||
ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson),
|
||||
);
|
||||
break;
|
||||
// Add other method handlers here
|
||||
// default:
|
||||
// _sendError(workerMethod, 'Unsupported method: ${workerMessage.method}');
|
||||
}
|
||||
} catch (e, s) {
|
||||
print(s);
|
||||
|
@ -88,11 +108,11 @@ class ElectrumWorker {
|
|||
}
|
||||
|
||||
Future<void> _handleConnect(ElectrumWorkerConnectionRequest request) async {
|
||||
_electrumClient = ElectrumApiProvider(
|
||||
await ElectrumTCPService.connect(
|
||||
_electrumClient = await ElectrumApiProvider.connect(
|
||||
ElectrumTCPService.connect(
|
||||
request.uri,
|
||||
onConnectionStatusChange: (status) {
|
||||
_sendResponse(ElectrumWorkerConnectionResponse(status: status));
|
||||
_sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id));
|
||||
},
|
||||
defaultRequestTimeOut: const Duration(seconds: 5),
|
||||
connectionTimeOut: const Duration(seconds: 5),
|
||||
|
@ -100,7 +120,7 @@ class ElectrumWorker {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _handleHeadersSubscribe() async {
|
||||
Future<void> _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async {
|
||||
final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe());
|
||||
if (listener == null) {
|
||||
_sendError(ElectrumWorkerHeadersSubscribeError(error: 'Failed to subscribe'));
|
||||
|
@ -108,7 +128,9 @@ class ElectrumWorker {
|
|||
}
|
||||
|
||||
listener((event) {
|
||||
_sendResponse(ElectrumWorkerHeadersSubscribeResponse(result: event));
|
||||
_sendResponse(
|
||||
ElectrumWorkerHeadersSubscribeResponse(result: event, id: request.id),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -135,6 +157,7 @@ class ElectrumWorker {
|
|||
|
||||
_sendResponse(ElectrumWorkerScripthashesSubscribeResponse(
|
||||
result: {address: status},
|
||||
id: request.id,
|
||||
));
|
||||
});
|
||||
}));
|
||||
|
@ -171,7 +194,7 @@ class ElectrumWorker {
|
|||
}
|
||||
} catch (_) {
|
||||
tx = ElectrumTransactionInfo.fromElectrumBundle(
|
||||
await getTransactionExpanded(
|
||||
await _getTransactionExpanded(
|
||||
hash: txid,
|
||||
currentChainTip: result.chainTip,
|
||||
mempoolAPIEnabled: result.mempoolAPIEnabled,
|
||||
|
@ -201,10 +224,113 @@ class ElectrumWorker {
|
|||
return histories;
|
||||
}));
|
||||
|
||||
_sendResponse(ElectrumWorkerGetHistoryResponse(result: histories.values.toList()));
|
||||
_sendResponse(ElectrumWorkerGetHistoryResponse(
|
||||
result: histories.values.toList(),
|
||||
id: result.id,
|
||||
));
|
||||
}
|
||||
|
||||
Future<ElectrumTransactionBundle> getTransactionExpanded({
|
||||
// Future<void> _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async {
|
||||
// final balanceFutures = <Future<Map<String, dynamic>>>[];
|
||||
|
||||
// for (final scripthash in request.scripthashes) {
|
||||
// final balanceFuture = _electrumClient!.request(
|
||||
// ElectrumGetScriptHashBalance(scriptHash: scripthash),
|
||||
// );
|
||||
// balanceFutures.add(balanceFuture);
|
||||
// }
|
||||
|
||||
// var totalConfirmed = 0;
|
||||
// var totalUnconfirmed = 0;
|
||||
|
||||
// final balances = await Future.wait(balanceFutures);
|
||||
|
||||
// for (final balance in balances) {
|
||||
// final confirmed = balance['confirmed'] as int? ?? 0;
|
||||
// final unconfirmed = balance['unconfirmed'] as int? ?? 0;
|
||||
// totalConfirmed += confirmed;
|
||||
// totalUnconfirmed += unconfirmed;
|
||||
// }
|
||||
|
||||
// _sendResponse(ElectrumWorkerGetBalanceResponse(
|
||||
// result: ElectrumBalance(
|
||||
// confirmed: totalConfirmed,
|
||||
// unconfirmed: totalUnconfirmed,
|
||||
// frozen: 0,
|
||||
// ),
|
||||
// ));
|
||||
// }
|
||||
|
||||
Future<void> _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async {
|
||||
final balanceFutures = <Future<Map<String, dynamic>>>[];
|
||||
|
||||
for (final scripthash in request.scripthashes) {
|
||||
final balanceFuture = _electrumClient!.request(
|
||||
ElectrumGetScriptHashBalance(scriptHash: scripthash),
|
||||
);
|
||||
balanceFutures.add(balanceFuture);
|
||||
}
|
||||
|
||||
var totalConfirmed = 0;
|
||||
var totalUnconfirmed = 0;
|
||||
|
||||
final balances = await Future.wait(balanceFutures);
|
||||
|
||||
for (final balance in balances) {
|
||||
final confirmed = balance['confirmed'] as int? ?? 0;
|
||||
final unconfirmed = balance['unconfirmed'] as int? ?? 0;
|
||||
totalConfirmed += confirmed;
|
||||
totalUnconfirmed += unconfirmed;
|
||||
}
|
||||
|
||||
_sendResponse(
|
||||
ElectrumWorkerGetBalanceResponse(
|
||||
result: ElectrumBalance(
|
||||
confirmed: totalConfirmed,
|
||||
unconfirmed: totalUnconfirmed,
|
||||
frozen: 0,
|
||||
),
|
||||
id: request.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleListUnspent(ElectrumWorkerListUnspentRequest request) async {
|
||||
final unspents = <String, List<ElectrumUtxo>>{};
|
||||
|
||||
await Future.wait(request.scripthashes.map((scriptHash) async {
|
||||
final scriptHashUnspents = await _electrumClient!.request(
|
||||
ElectrumScriptHashListUnspent(scriptHash: scriptHash),
|
||||
);
|
||||
|
||||
if (scriptHashUnspents.isNotEmpty) {
|
||||
unspents[scriptHash] = scriptHashUnspents;
|
||||
}
|
||||
}));
|
||||
|
||||
_sendResponse(ElectrumWorkerListUnspentResponse(utxos: unspents, id: request.id));
|
||||
}
|
||||
|
||||
Future<void> _handleBroadcast(ElectrumWorkerBroadcastRequest request) async {
|
||||
final txHash = await _electrumClient!.request(
|
||||
ElectrumBroadCastTransaction(transactionRaw: request.transactionRaw),
|
||||
);
|
||||
|
||||
_sendResponse(ElectrumWorkerBroadcastResponse(txHash: txHash, id: request.id));
|
||||
}
|
||||
|
||||
Future<void> _handleGetTxExpanded(ElectrumWorkerTxExpandedRequest request) async {
|
||||
final tx = await _getTransactionExpanded(
|
||||
hash: request.txHash,
|
||||
currentChainTip: request.currentChainTip,
|
||||
mempoolAPIEnabled: false,
|
||||
getConfirmations: false,
|
||||
);
|
||||
|
||||
_sendResponse(ElectrumWorkerTxExpandedResponse(expandedTx: tx, id: request.id));
|
||||
}
|
||||
|
||||
Future<ElectrumTransactionBundle> _getTransactionExpanded({
|
||||
required String hash,
|
||||
required int currentChainTip,
|
||||
required bool mempoolAPIEnabled,
|
||||
|
@ -289,65 +415,312 @@ class ElectrumWorker {
|
|||
);
|
||||
}
|
||||
|
||||
// Future<void> _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async {
|
||||
// final balanceFutures = <Future<Map<String, dynamic>>>[];
|
||||
Future<void> _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async {
|
||||
final scanData = request.scanData;
|
||||
int syncHeight = scanData.height;
|
||||
int initialSyncHeight = syncHeight;
|
||||
|
||||
// for (final scripthash in request.scripthashes) {
|
||||
// final balanceFuture = _electrumClient!.request(
|
||||
// ElectrumGetScriptHashBalance(scriptHash: scripthash),
|
||||
// );
|
||||
// balanceFutures.add(balanceFuture);
|
||||
// }
|
||||
int getCountPerRequest(int syncHeight) {
|
||||
if (scanData.isSingleScan) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// var totalConfirmed = 0;
|
||||
// var totalUnconfirmed = 0;
|
||||
final amountLeft = scanData.chainTip - syncHeight + 1;
|
||||
return amountLeft;
|
||||
}
|
||||
|
||||
// final balances = await Future.wait(balanceFutures);
|
||||
|
||||
// for (final balance in balances) {
|
||||
// final confirmed = balance['confirmed'] as int? ?? 0;
|
||||
// final unconfirmed = balance['unconfirmed'] as int? ?? 0;
|
||||
// totalConfirmed += confirmed;
|
||||
// totalUnconfirmed += unconfirmed;
|
||||
// }
|
||||
|
||||
// _sendResponse(ElectrumWorkerGetBalanceResponse(
|
||||
// result: ElectrumBalance(
|
||||
// confirmed: totalConfirmed,
|
||||
// unconfirmed: totalUnconfirmed,
|
||||
// frozen: 0,
|
||||
// ),
|
||||
// ));
|
||||
// }
|
||||
|
||||
Future<void> _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async {
|
||||
final balanceFutures = <Future<Map<String, dynamic>>>[];
|
||||
|
||||
for (final scripthash in request.scripthashes) {
|
||||
final balanceFuture = _electrumClient!.request(
|
||||
ElectrumGetScriptHashBalance(scriptHash: scripthash),
|
||||
final receiver = Receiver(
|
||||
scanData.silentAddress.b_scan.toHex(),
|
||||
scanData.silentAddress.B_spend.toHex(),
|
||||
scanData.network == BitcoinNetwork.testnet,
|
||||
scanData.labelIndexes,
|
||||
scanData.labelIndexes.length,
|
||||
);
|
||||
balanceFutures.add(balanceFuture);
|
||||
}
|
||||
|
||||
var totalConfirmed = 0;
|
||||
var totalUnconfirmed = 0;
|
||||
|
||||
final balances = await Future.wait(balanceFutures);
|
||||
|
||||
for (final balance in balances) {
|
||||
final confirmed = balance['confirmed'] as int? ?? 0;
|
||||
final unconfirmed = balance['unconfirmed'] as int? ?? 0;
|
||||
totalConfirmed += confirmed;
|
||||
totalUnconfirmed += unconfirmed;
|
||||
}
|
||||
|
||||
_sendResponse(ElectrumWorkerGetBalanceResponse(
|
||||
result: ElectrumBalance(
|
||||
confirmed: totalConfirmed,
|
||||
unconfirmed: totalUnconfirmed,
|
||||
frozen: 0,
|
||||
// Initial status UI update, send how many blocks in total to scan
|
||||
final initialCount = getCountPerRequest(syncHeight);
|
||||
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
|
||||
result: TweaksSyncResponse(
|
||||
height: syncHeight,
|
||||
syncStatus: StartingScanSyncStatus(syncHeight),
|
||||
),
|
||||
));
|
||||
|
||||
print([syncHeight, initialCount]);
|
||||
final listener = await _electrumClient!.subscribe(
|
||||
ElectrumTweaksSubscribe(height: syncHeight, count: initialCount),
|
||||
);
|
||||
|
||||
Future<void> listenFn(ElectrumTweaksSubscribeResponse response) async {
|
||||
// success or error msg
|
||||
final noData = response.message != null;
|
||||
|
||||
if (noData) {
|
||||
// re-subscribe to continue receiving messages, starting from the next unscanned height
|
||||
final nextHeight = syncHeight + 1;
|
||||
final nextCount = getCountPerRequest(nextHeight);
|
||||
|
||||
if (nextCount > 0) {
|
||||
final nextListener = await _electrumClient!.subscribe(
|
||||
ElectrumTweaksSubscribe(height: syncHeight, count: initialCount),
|
||||
);
|
||||
nextListener?.call(listenFn);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Continuous status UI update, send how many blocks left to scan
|
||||
final syncingStatus = scanData.isSingleScan
|
||||
? SyncingSyncStatus(1, 0)
|
||||
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
|
||||
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
|
||||
result: TweaksSyncResponse(height: syncHeight, syncStatus: syncingStatus),
|
||||
));
|
||||
|
||||
final tweakHeight = response.block;
|
||||
|
||||
try {
|
||||
final blockTweaks = response.blockTweaks;
|
||||
|
||||
for (final txid in blockTweaks.keys) {
|
||||
final tweakData = blockTweaks[txid];
|
||||
final outputPubkeys = tweakData!.outputPubkeys;
|
||||
final tweak = tweakData.tweak;
|
||||
|
||||
try {
|
||||
// scanOutputs called from rust here
|
||||
final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver);
|
||||
|
||||
if (addToWallet.isEmpty) {
|
||||
// no results tx, continue to next tx
|
||||
continue;
|
||||
}
|
||||
|
||||
// placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
|
||||
final txInfo = ElectrumTransactionInfo(
|
||||
WalletType.bitcoin,
|
||||
id: txid,
|
||||
height: tweakHeight,
|
||||
amount: 0,
|
||||
fee: 0,
|
||||
direction: TransactionDirection.incoming,
|
||||
isPending: false,
|
||||
isReplaced: false,
|
||||
date: scanData.network == BitcoinNetwork.mainnet
|
||||
? getDateByBitcoinHeight(tweakHeight)
|
||||
: DateTime.now(),
|
||||
confirmations: scanData.chainTip - tweakHeight + 1,
|
||||
unspents: [],
|
||||
isReceivedSilentPayment: true,
|
||||
);
|
||||
|
||||
addToWallet.forEach((label, value) {
|
||||
(value as Map<String, dynamic>).forEach((output, tweak) {
|
||||
final t_k = tweak.toString();
|
||||
|
||||
final receivingOutputAddress = ECPublic.fromHex(output)
|
||||
.toTaprootAddress(tweak: false)
|
||||
.toAddress(scanData.network);
|
||||
|
||||
final matchingOutput = outputPubkeys[output]!;
|
||||
final amount = matchingOutput.amount;
|
||||
final pos = matchingOutput.vout;
|
||||
|
||||
final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
|
||||
receivingOutputAddress,
|
||||
labelIndex: 1, // TODO: get actual index/label
|
||||
isUsed: true,
|
||||
spendKey: scanData.silentAddress.b_spend.tweakAdd(
|
||||
BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)),
|
||||
),
|
||||
txCount: 1,
|
||||
balance: amount,
|
||||
);
|
||||
|
||||
final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos);
|
||||
|
||||
txInfo.unspents!.add(unspent);
|
||||
txInfo.amount += unspent.value;
|
||||
});
|
||||
});
|
||||
|
||||
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
|
||||
result: TweaksSyncResponse(transactions: {txInfo.id: txInfo}),
|
||||
));
|
||||
} catch (e, stacktrace) {
|
||||
print(stacktrace);
|
||||
print(e.toString());
|
||||
}
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
print(stacktrace);
|
||||
print(e.toString());
|
||||
}
|
||||
|
||||
syncHeight = tweakHeight;
|
||||
|
||||
if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
|
||||
if (tweakHeight >= scanData.chainTip)
|
||||
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
|
||||
result: TweaksSyncResponse(
|
||||
height: syncHeight,
|
||||
syncStatus: SyncedTipSyncStatus(scanData.chainTip),
|
||||
),
|
||||
));
|
||||
|
||||
if (scanData.isSingleScan) {
|
||||
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
|
||||
result: TweaksSyncResponse(height: syncHeight, syncStatus: SyncedSyncStatus()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listener?.call(listenFn);
|
||||
|
||||
// if (tweaksSubscription == null) {
|
||||
// return scanData.sendPort.send(
|
||||
// SyncResponse(syncHeight, UnsupportedSyncStatus()),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> delegatedScan(ScanData scanData) async {
|
||||
// int syncHeight = scanData.height;
|
||||
// int initialSyncHeight = syncHeight;
|
||||
|
||||
// BehaviorSubject<Object>? tweaksSubscription = null;
|
||||
|
||||
// final electrumClient = scanData.electrumClient;
|
||||
// await electrumClient.connectToUri(
|
||||
// scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"),
|
||||
// useSSL: scanData.node?.useSSL ?? false,
|
||||
// );
|
||||
|
||||
// if (tweaksSubscription == null) {
|
||||
// scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
|
||||
|
||||
// tweaksSubscription = await electrumClient.tweaksScan(
|
||||
// pubSpendKey: scanData.silentAddress.B_spend.toHex(),
|
||||
// );
|
||||
|
||||
// Future<void> listenFn(t) async {
|
||||
// final tweaks = t as Map<String, dynamic>;
|
||||
// final msg = tweaks["message"];
|
||||
|
||||
// // success or error msg
|
||||
// final noData = msg != null;
|
||||
// if (noData) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Continuous status UI update, send how many blocks left to scan
|
||||
// final syncingStatus = scanData.isSingleScan
|
||||
// ? SyncingSyncStatus(1, 0)
|
||||
// : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
|
||||
// scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
|
||||
|
||||
// final blockHeight = tweaks.keys.first;
|
||||
// final tweakHeight = int.parse(blockHeight);
|
||||
|
||||
// try {
|
||||
// final blockTweaks = tweaks[blockHeight] as Map<String, dynamic>;
|
||||
|
||||
// for (var j = 0; j < blockTweaks.keys.length; j++) {
|
||||
// final txid = blockTweaks.keys.elementAt(j);
|
||||
// final details = blockTweaks[txid] as Map<String, dynamic>;
|
||||
// final outputPubkeys = (details["output_pubkeys"] as Map<dynamic, dynamic>);
|
||||
// final spendingKey = details["spending_key"].toString();
|
||||
|
||||
// try {
|
||||
// // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
|
||||
// final txInfo = ElectrumTransactionInfo(
|
||||
// WalletType.bitcoin,
|
||||
// id: txid,
|
||||
// height: tweakHeight,
|
||||
// amount: 0,
|
||||
// fee: 0,
|
||||
// direction: TransactionDirection.incoming,
|
||||
// isPending: false,
|
||||
// isReplaced: false,
|
||||
// date: scanData.network == BitcoinNetwork.mainnet
|
||||
// ? getDateByBitcoinHeight(tweakHeight)
|
||||
// : DateTime.now(),
|
||||
// confirmations: scanData.chainTip - tweakHeight + 1,
|
||||
// unspents: [],
|
||||
// isReceivedSilentPayment: true,
|
||||
// );
|
||||
|
||||
// outputPubkeys.forEach((pos, value) {
|
||||
// final secKey = ECPrivate.fromHex(spendingKey);
|
||||
// final receivingOutputAddress =
|
||||
// secKey.getPublic().toTaprootAddress(tweak: false).toAddress(scanData.network);
|
||||
|
||||
// late int amount;
|
||||
// try {
|
||||
// amount = int.parse(value[1].toString());
|
||||
// } catch (_) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
|
||||
// receivingOutputAddress,
|
||||
// labelIndex: 0,
|
||||
// isUsed: true,
|
||||
// spendKey: secKey,
|
||||
// txCount: 1,
|
||||
// balance: amount,
|
||||
// );
|
||||
|
||||
// final unspent = BitcoinUnspent(
|
||||
// receivedAddressRecord,
|
||||
// txid,
|
||||
// amount,
|
||||
// int.parse(pos.toString()),
|
||||
// );
|
||||
|
||||
// txInfo.unspents!.add(unspent);
|
||||
// txInfo.amount += unspent.value;
|
||||
// });
|
||||
|
||||
// scanData.sendPort.send({txInfo.id: txInfo});
|
||||
// } catch (_) {}
|
||||
// }
|
||||
// } catch (_) {}
|
||||
|
||||
// syncHeight = tweakHeight;
|
||||
|
||||
// if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
|
||||
// if (tweakHeight >= scanData.chainTip)
|
||||
// scanData.sendPort.send(SyncResponse(
|
||||
// syncHeight,
|
||||
// SyncedTipSyncStatus(scanData.chainTip),
|
||||
// ));
|
||||
|
||||
// if (scanData.isSingleScan) {
|
||||
// scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
|
||||
// }
|
||||
|
||||
// await tweaksSubscription!.close();
|
||||
// await electrumClient.close();
|
||||
// }
|
||||
// }
|
||||
|
||||
// tweaksSubscription?.listen(listenFn);
|
||||
// }
|
||||
|
||||
// if (tweaksSubscription == null) {
|
||||
// return scanData.sendPort.send(
|
||||
// SyncResponse(syncHeight, UnsupportedSyncStatus()),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
class ScanNode {
|
||||
final Uri uri;
|
||||
final bool? useSSL;
|
||||
|
||||
ScanNode(this.uri, this.useSSL);
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@ class ElectrumWorkerMethods {
|
|||
|
||||
static const String connectionMethod = "connection";
|
||||
static const String unknownMethod = "unknown";
|
||||
static const String txHashMethod = "txHash";
|
||||
|
||||
static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod);
|
||||
static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod);
|
||||
static const ElectrumWorkerMethods txHash = ElectrumWorkerMethods._(txHashMethod);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
|
|
@ -4,17 +4,24 @@ import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
|
|||
|
||||
abstract class ElectrumWorkerRequest {
|
||||
abstract final String method;
|
||||
abstract final int? id;
|
||||
|
||||
Map<String, dynamic> toJson();
|
||||
ElectrumWorkerRequest.fromJson(Map<String, dynamic> json);
|
||||
}
|
||||
|
||||
class ElectrumWorkerResponse<RESULT, RESPONSE> {
|
||||
ElectrumWorkerResponse({required this.method, required this.result, this.error});
|
||||
ElectrumWorkerResponse({
|
||||
required this.method,
|
||||
required this.result,
|
||||
this.error,
|
||||
this.id,
|
||||
});
|
||||
|
||||
final String method;
|
||||
final RESULT result;
|
||||
final String? error;
|
||||
final int? id;
|
||||
|
||||
RESPONSE resultJson(RESULT result) {
|
||||
throw UnimplementedError();
|
||||
|
@ -25,21 +32,22 @@ class ElectrumWorkerResponse<RESULT, RESPONSE> {
|
|||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'method': method, 'result': resultJson(result), 'error': error};
|
||||
return {'method': method, 'result': resultJson(result), 'error': error, 'id': id};
|
||||
}
|
||||
}
|
||||
|
||||
class ElectrumWorkerErrorResponse {
|
||||
ElectrumWorkerErrorResponse({required this.error});
|
||||
ElectrumWorkerErrorResponse({required this.error, this.id});
|
||||
|
||||
String get method => ElectrumWorkerMethods.unknown.method;
|
||||
final int? id;
|
||||
final String error;
|
||||
|
||||
factory ElectrumWorkerErrorResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ElectrumWorkerErrorResponse(error: json['error'] as String);
|
||||
return ElectrumWorkerErrorResponse(error: json['error'] as String, id: json['id'] as int);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'method': method, 'error': error};
|
||||
return {'method': method, 'error': error, 'id': id};
|
||||
}
|
||||
}
|
||||
|
|
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';
|
||||
|
||||
class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest {
|
||||
ElectrumWorkerConnectionRequest({required this.uri});
|
||||
ElectrumWorkerConnectionRequest({
|
||||
required this.uri,
|
||||
required this.network,
|
||||
this.id,
|
||||
});
|
||||
|
||||
final Uri uri;
|
||||
final BasedUtxoNetwork network;
|
||||
final int? id;
|
||||
|
||||
@override
|
||||
final String method = ElectrumWorkerMethods.connect.method;
|
||||
|
||||
@override
|
||||
factory ElectrumWorkerConnectionRequest.fromJson(Map<String, dynamic> json) {
|
||||
return ElectrumWorkerConnectionRequest(uri: Uri.parse(json['params'] as String));
|
||||
return ElectrumWorkerConnectionRequest(
|
||||
uri: Uri.parse(json['uri'] as String),
|
||||
network: BasedUtxoNetwork.values.firstWhere(
|
||||
(e) => e.toString() == json['network'] as String,
|
||||
),
|
||||
id: json['id'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'method': method, 'params': uri.toString()};
|
||||
return {
|
||||
'method': method,
|
||||
'uri': uri.toString(),
|
||||
'network': network.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ElectrumWorkerConnectionError extends ElectrumWorkerErrorResponse {
|
||||
ElectrumWorkerConnectionError({required String error}) : super(error: error);
|
||||
ElectrumWorkerConnectionError({
|
||||
required super.error,
|
||||
super.id,
|
||||
}) : super();
|
||||
|
||||
@override
|
||||
String get method => ElectrumWorkerMethods.connect.method;
|
||||
}
|
||||
|
||||
class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse<ConnectionStatus, String> {
|
||||
ElectrumWorkerConnectionResponse({required ConnectionStatus status, super.error})
|
||||
: super(
|
||||
ElectrumWorkerConnectionResponse({
|
||||
required ConnectionStatus status,
|
||||
super.error,
|
||||
super.id,
|
||||
}) : super(
|
||||
result: status,
|
||||
method: ElectrumWorkerMethods.connect.method,
|
||||
);
|
||||
|
@ -45,6 +67,7 @@ class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse<Connection
|
|||
(e) => e.toString() == json['result'] as String,
|
||||
),
|
||||
error: json['error'] as String?,
|
||||
id: json['id'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
part of 'methods.dart';
|
||||
|
||||
class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
|
||||
ElectrumWorkerGetBalanceRequest({required this.scripthashes});
|
||||
ElectrumWorkerGetBalanceRequest({required this.scripthashes, this.id});
|
||||
|
||||
final Set<String> scripthashes;
|
||||
final int? id;
|
||||
|
||||
@override
|
||||
final String method = ElectrumRequestMethods.getBalance.method;
|
||||
|
@ -12,6 +13,7 @@ class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
|
|||
factory ElectrumWorkerGetBalanceRequest.fromJson(Map<String, dynamic> json) {
|
||||
return ElectrumWorkerGetBalanceRequest(
|
||||
scripthashes: (json['scripthashes'] as List<String>).toSet(),
|
||||
id: json['id'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -22,7 +24,10 @@ class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
|
|||
}
|
||||
|
||||
class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse {
|
||||
ElectrumWorkerGetBalanceError({required String error}) : super(error: error);
|
||||
ElectrumWorkerGetBalanceError({
|
||||
required super.error,
|
||||
super.id,
|
||||
}) : super();
|
||||
|
||||
@override
|
||||
final String method = ElectrumRequestMethods.getBalance.method;
|
||||
|
@ -30,8 +35,11 @@ class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse {
|
|||
|
||||
class ElectrumWorkerGetBalanceResponse
|
||||
extends ElectrumWorkerResponse<ElectrumBalance, Map<String, int>?> {
|
||||
ElectrumWorkerGetBalanceResponse({required super.result, super.error})
|
||||
: super(method: ElectrumRequestMethods.getBalance.method);
|
||||
ElectrumWorkerGetBalanceResponse({
|
||||
required super.result,
|
||||
super.error,
|
||||
super.id,
|
||||
}) : super(method: ElectrumRequestMethods.getBalance.method);
|
||||
|
||||
@override
|
||||
Map<String, int>? resultJson(result) {
|
||||
|
@ -47,6 +55,7 @@ class ElectrumWorkerGetBalanceResponse
|
|||
frozen: 0,
|
||||
),
|
||||
error: json['error'] as String?,
|
||||
id: json['id'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
|
|||
required this.chainTip,
|
||||
required this.network,
|
||||
required this.mempoolAPIEnabled,
|
||||
this.id,
|
||||
});
|
||||
|
||||
final List<BitcoinAddressRecord> addresses;
|
||||
|
@ -16,6 +17,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
|
|||
final int chainTip;
|
||||
final BasedUtxoNetwork network;
|
||||
final bool mempoolAPIEnabled;
|
||||
final int? id;
|
||||
|
||||
@override
|
||||
final String method = ElectrumRequestMethods.getHistory.method;
|
||||
|
@ -35,6 +37,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
|
|||
chainTip: json['chainTip'] as int,
|
||||
network: BasedUtxoNetwork.fromName(json['network'] as String),
|
||||
mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool,
|
||||
id: json['id'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -53,7 +56,10 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
|
|||
}
|
||||
|
||||
class ElectrumWorkerGetHistoryError extends ElectrumWorkerErrorResponse {
|
||||
ElectrumWorkerGetHistoryError({required String error}) : super(error: error);
|
||||
ElectrumWorkerGetHistoryError({
|
||||
required super.error,
|
||||
super.id,
|
||||
}) : super();
|
||||
|
||||
@override
|
||||
final String method = ElectrumRequestMethods.getHistory.method;
|
||||
|
@ -90,8 +96,11 @@ class AddressHistoriesResponse {
|
|||
|
||||
class ElectrumWorkerGetHistoryResponse
|
||||
extends ElectrumWorkerResponse<List<AddressHistoriesResponse>, List<Map<String, dynamic>>> {
|
||||
ElectrumWorkerGetHistoryResponse({required super.result, super.error})
|
||||
: super(method: ElectrumRequestMethods.getHistory.method);
|
||||
ElectrumWorkerGetHistoryResponse({
|
||||
required super.result,
|
||||
super.error,
|
||||
super.id,
|
||||
}) : super(method: ElectrumRequestMethods.getHistory.method);
|
||||
|
||||
@override
|
||||
List<Map<String, dynamic>> resultJson(result) {
|
||||
|
@ -105,6 +114,7 @@ class ElectrumWorkerGetHistoryResponse
|
|||
.map((e) => AddressHistoriesResponse.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
error: json['error'] as String?,
|
||||
id: json['id'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
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';
|
||||
|
||||
class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest {
|
||||
ElectrumWorkerHeadersSubscribeRequest();
|
||||
ElectrumWorkerHeadersSubscribeRequest({this.id});
|
||||
|
||||
@override
|
||||
final String method = ElectrumRequestMethods.headersSubscribe.method;
|
||||
final int? id;
|
||||
|
||||
@override
|
||||
factory ElectrumWorkerHeadersSubscribeRequest.fromJson(Map<String, dynamic> json) {
|
||||
return ElectrumWorkerHeadersSubscribeRequest();
|
||||
return ElectrumWorkerHeadersSubscribeRequest(
|
||||
id: json['id'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -18,7 +21,10 @@ class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest {
|
|||
}
|
||||
|
||||
class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse {
|
||||
ElectrumWorkerHeadersSubscribeError({required String error}) : super(error: error);
|
||||
ElectrumWorkerHeadersSubscribeError({
|
||||
required super.error,
|
||||
super.id,
|
||||
}) : super();
|
||||
|
||||
@override
|
||||
final String method = ElectrumRequestMethods.headersSubscribe.method;
|
||||
|
@ -26,8 +32,11 @@ class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse {
|
|||
|
||||
class ElectrumWorkerHeadersSubscribeResponse
|
||||
extends ElectrumWorkerResponse<ElectrumHeaderResponse, Map<String, dynamic>> {
|
||||
ElectrumWorkerHeadersSubscribeResponse({required super.result, super.error})
|
||||
: super(method: ElectrumRequestMethods.headersSubscribe.method);
|
||||
ElectrumWorkerHeadersSubscribeResponse({
|
||||
required super.result,
|
||||
super.error,
|
||||
super.id,
|
||||
}) : super(method: ElectrumRequestMethods.headersSubscribe.method);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> resultJson(result) {
|
||||
|
@ -39,6 +48,7 @@ class ElectrumWorkerHeadersSubscribeResponse
|
|||
return ElectrumWorkerHeadersSubscribeResponse(
|
||||
result: ElectrumHeaderResponse.fromJson(json['result'] as Map<String, dynamic>),
|
||||
error: json['error'] as String?,
|
||||
id: json['id'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
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:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/electrum_balance.dart';
|
||||
|
@ -6,8 +5,14 @@ import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
|
|||
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
|
||||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
|
||||
part 'connection.dart';
|
||||
part 'headers_subscribe.dart';
|
||||
part 'scripthashes_subscribe.dart';
|
||||
part 'get_balance.dart';
|
||||
part 'get_history.dart';
|
||||
part 'get_tx_expanded.dart';
|
||||
part 'broadcast.dart';
|
||||
part 'list_unspent.dart';
|
||||
part 'tweaks_subscribe.dart';
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
part of 'methods.dart';
|
||||
|
||||
class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerRequest {
|
||||
ElectrumWorkerScripthashesSubscribeRequest({required this.scripthashByAddress});
|
||||
ElectrumWorkerScripthashesSubscribeRequest({
|
||||
required this.scripthashByAddress,
|
||||
this.id,
|
||||
});
|
||||
|
||||
final Map<String, String> scripthashByAddress;
|
||||
final int? id;
|
||||
|
||||
@override
|
||||
final String method = ElectrumRequestMethods.scriptHashSubscribe.method;
|
||||
|
@ -12,6 +16,7 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques
|
|||
factory ElectrumWorkerScripthashesSubscribeRequest.fromJson(Map<String, dynamic> json) {
|
||||
return ElectrumWorkerScripthashesSubscribeRequest(
|
||||
scripthashByAddress: json['scripthashes'] as Map<String, String>,
|
||||
id: json['id'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -22,7 +27,10 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques
|
|||
}
|
||||
|
||||
class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorResponse {
|
||||
ElectrumWorkerScripthashesSubscribeError({required String error}) : super(error: error);
|
||||
ElectrumWorkerScripthashesSubscribeError({
|
||||
required super.error,
|
||||
super.id,
|
||||
}) : super();
|
||||
|
||||
@override
|
||||
final String method = ElectrumRequestMethods.scriptHashSubscribe.method;
|
||||
|
@ -30,8 +38,11 @@ class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorRespon
|
|||
|
||||
class ElectrumWorkerScripthashesSubscribeResponse
|
||||
extends ElectrumWorkerResponse<Map<String, String>?, Map<String, String>?> {
|
||||
ElectrumWorkerScripthashesSubscribeResponse({required super.result, super.error})
|
||||
: super(method: ElectrumRequestMethods.scriptHashSubscribe.method);
|
||||
ElectrumWorkerScripthashesSubscribeResponse({
|
||||
required super.result,
|
||||
super.error,
|
||||
super.id,
|
||||
}) : super(method: ElectrumRequestMethods.scriptHashSubscribe.method);
|
||||
|
||||
@override
|
||||
Map<String, String>? resultJson(result) {
|
||||
|
@ -43,6 +54,7 @@ class ElectrumWorkerScripthashesSubscribeResponse
|
|||
return ElectrumWorkerScripthashesSubscribeResponse(
|
||||
result: json['result'] as Map<String, String>?,
|
||||
error: json['error'] as String?,
|
||||
id: json['id'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
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:crypto/crypto.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
|
||||
// import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
|
||||
import 'package:cw_core/cake_hive.dart';
|
||||
import 'package:cw_core/mweb_utxo.dart';
|
||||
import 'package:cw_mweb/mwebd.pbgrpc.dart';
|
||||
|
@ -109,22 +109,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
});
|
||||
reaction((_) => mwebSyncStatus, (status) async {
|
||||
if (mwebSyncStatus is FailedSyncStatus) {
|
||||
// we failed to connect to mweb, check if we are connected to the litecoin node:
|
||||
late int nodeHeight;
|
||||
try {
|
||||
nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0;
|
||||
} catch (_) {
|
||||
nodeHeight = 0;
|
||||
}
|
||||
|
||||
if (nodeHeight == 0) {
|
||||
// we aren't connected to the litecoin node, so the current electrum_wallet reactions will take care of this case for us
|
||||
} else {
|
||||
// we're connected to the litecoin node, but we failed to connect to mweb, try again after a few seconds:
|
||||
await CwMweb.stop();
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
startSync();
|
||||
}
|
||||
} else if (mwebSyncStatus is SyncingSyncStatus) {
|
||||
syncStatus = mwebSyncStatus;
|
||||
} else if (mwebSyncStatus is SynchronizingSyncStatus) {
|
||||
|
@ -348,8 +335,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
return;
|
||||
}
|
||||
|
||||
final nodeHeight =
|
||||
await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node
|
||||
final nodeHeight = await currentChainTip ?? 0;
|
||||
|
||||
if (nodeHeight == 0) {
|
||||
// we aren't connected to the ltc node yet
|
||||
|
@ -635,7 +621,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
}
|
||||
|
||||
final status = await CwMweb.status(StatusRequest());
|
||||
final height = await electrumClient.getCurrentBlockChainTip();
|
||||
final height = await currentChainTip;
|
||||
if (height == null || status.blockHeaderHeight != height) return;
|
||||
if (status.mwebUtxosHeight != height) return; // we aren't synced
|
||||
int amount = 0;
|
||||
|
@ -770,7 +756,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
});
|
||||
|
||||
// copy coin control attributes to mwebCoins:
|
||||
await updateCoins(mwebUnspentCoins.toSet());
|
||||
// await updateCoins(mwebUnspentCoins);
|
||||
// get regular ltc unspents (this resets unspentCoins):
|
||||
await super.updateAllUnspents();
|
||||
// add the mwebCoins:
|
||||
|
@ -1289,7 +1275,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
}) async {
|
||||
final readyInputs = <LedgerTransaction>[];
|
||||
for (final utxo in utxos) {
|
||||
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
|
||||
final rawTx =
|
||||
(await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex();
|
||||
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
|
||||
|
||||
readyInputs.add(LedgerTransaction(
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
import 'package:cw_bitcoin/exceptions.dart';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
@ -14,11 +15,10 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
PendingBitcoinTransaction(
|
||||
this._tx,
|
||||
this.type, {
|
||||
required this.electrumClient,
|
||||
required this.sendWorker,
|
||||
required this.amount,
|
||||
required this.fee,
|
||||
required this.feeRate,
|
||||
this.network,
|
||||
required this.hasChange,
|
||||
this.isSendAll = false,
|
||||
this.hasTaprootInputs = false,
|
||||
|
@ -28,11 +28,10 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
|
||||
final WalletType type;
|
||||
final BtcTransaction _tx;
|
||||
final ElectrumClient electrumClient;
|
||||
Future<dynamic> Function(ElectrumWorkerRequest) sendWorker;
|
||||
final int amount;
|
||||
final int fee;
|
||||
final String feeRate;
|
||||
final BasedUtxoNetwork? network;
|
||||
final bool isSendAll;
|
||||
final bool hasChange;
|
||||
final bool hasTaprootInputs;
|
||||
|
@ -79,40 +78,39 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
Future<void> _commit() async {
|
||||
int? callId;
|
||||
|
||||
final result = await electrumClient.broadcastTransaction(
|
||||
transactionRaw: hex, network: network, idCallback: (id) => callId = id);
|
||||
final result = await sendWorker(ElectrumWorkerBroadcastRequest(transactionRaw: hex)) as String;
|
||||
|
||||
if (result.isEmpty) {
|
||||
if (callId != null) {
|
||||
final error = electrumClient.getErrorMessage(callId!);
|
||||
// if (result.isEmpty) {
|
||||
// if (callId != null) {
|
||||
// final error = sendWorker(getErrorMessage(callId!));
|
||||
|
||||
if (error.contains("dust")) {
|
||||
if (hasChange) {
|
||||
throw BitcoinTransactionCommitFailedDustChange();
|
||||
} else if (!isSendAll) {
|
||||
throw BitcoinTransactionCommitFailedDustOutput();
|
||||
} else {
|
||||
throw BitcoinTransactionCommitFailedDustOutputSendAll();
|
||||
}
|
||||
}
|
||||
// if (error.contains("dust")) {
|
||||
// if (hasChange) {
|
||||
// throw BitcoinTransactionCommitFailedDustChange();
|
||||
// } else if (!isSendAll) {
|
||||
// throw BitcoinTransactionCommitFailedDustOutput();
|
||||
// } else {
|
||||
// throw BitcoinTransactionCommitFailedDustOutputSendAll();
|
||||
// }
|
||||
// }
|
||||
|
||||
if (error.contains("bad-txns-vout-negative")) {
|
||||
throw BitcoinTransactionCommitFailedVoutNegative();
|
||||
}
|
||||
// if (error.contains("bad-txns-vout-negative")) {
|
||||
// throw BitcoinTransactionCommitFailedVoutNegative();
|
||||
// }
|
||||
|
||||
if (error.contains("non-BIP68-final")) {
|
||||
throw BitcoinTransactionCommitFailedBIP68Final();
|
||||
}
|
||||
// if (error.contains("non-BIP68-final")) {
|
||||
// throw BitcoinTransactionCommitFailedBIP68Final();
|
||||
// }
|
||||
|
||||
if (error.contains("min fee not met")) {
|
||||
throw BitcoinTransactionCommitFailedLessThanMin();
|
||||
}
|
||||
// if (error.contains("min fee not met")) {
|
||||
// throw BitcoinTransactionCommitFailedLessThanMin();
|
||||
// }
|
||||
|
||||
throw BitcoinTransactionCommitFailed(errorMessage: error);
|
||||
}
|
||||
// throw BitcoinTransactionCommitFailed(errorMessage: error);
|
||||
// }
|
||||
|
||||
throw BitcoinTransactionCommitFailed();
|
||||
}
|
||||
// throw BitcoinTransactionCommitFailed();
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> _ltcCommit() async {
|
||||
|
|
|
@ -96,3 +96,58 @@ class LostConnectionSyncStatus extends NotConnectedSyncStatus {
|
|||
@override
|
||||
String toString() => 'Reconnecting';
|
||||
}
|
||||
|
||||
Map<String, dynamic> syncStatusToJson(SyncStatus? status) {
|
||||
if (status == null) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
'progress': status.progress(),
|
||||
'type': status.runtimeType.toString(),
|
||||
'data': status is SyncingSyncStatus
|
||||
? {'blocksLeft': status.blocksLeft, 'ptc': status.ptc}
|
||||
: status is SyncedTipSyncStatus
|
||||
? {'tip': status.tip}
|
||||
: status is FailedSyncStatus
|
||||
? {'error': status.error}
|
||||
: status is StartingScanSyncStatus
|
||||
? {'beginHeight': status.beginHeight}
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
SyncStatus syncStatusFromJson(Map<String, dynamic> json) {
|
||||
final type = json['type'] as String;
|
||||
final data = json['data'] as Map<String, dynamic>?;
|
||||
|
||||
switch (type) {
|
||||
case 'StartingScanSyncStatus':
|
||||
return StartingScanSyncStatus(data!['beginHeight'] as int);
|
||||
case 'SyncingSyncStatus':
|
||||
return SyncingSyncStatus(data!['blocksLeft'] as int, data['ptc'] as double);
|
||||
case 'SyncedTipSyncStatus':
|
||||
return SyncedTipSyncStatus(data!['tip'] as int);
|
||||
case 'FailedSyncStatus':
|
||||
return FailedSyncStatus(error: data!['error'] as String?);
|
||||
case 'SynchronizingSyncStatus':
|
||||
return SynchronizingSyncStatus();
|
||||
case 'NotConnectedSyncStatus':
|
||||
return NotConnectedSyncStatus();
|
||||
case 'AttemptingSyncStatus':
|
||||
return AttemptingSyncStatus();
|
||||
case 'AttemptingScanSyncStatus':
|
||||
return AttemptingScanSyncStatus();
|
||||
case 'ConnectedSyncStatus':
|
||||
return ConnectedSyncStatus();
|
||||
case 'ConnectingSyncStatus':
|
||||
return ConnectingSyncStatus();
|
||||
case 'UnsupportedSyncStatus':
|
||||
return UnsupportedSyncStatus();
|
||||
case 'TimedOutSyncStatus':
|
||||
return TimedOutSyncStatus();
|
||||
case 'LostConnectionSyncStatus':
|
||||
return LostConnectionSyncStatus();
|
||||
default:
|
||||
throw Exception('Unknown sync status type: $type');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue