From 76567528534a08a342cead5696e329ac1857059c Mon Sep 17 00:00:00 2001 From: Matthew Fosse <matt@fosse.co> Date: Wed, 29 May 2024 08:54:50 -0700 Subject: [PATCH] small fix --- cw_bitcoin/lib/litecoin_wallet.dart | 262 +++++++++++++++------------- 1 file changed, 145 insertions(+), 117 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 86d62ef05..6853d6a84 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -49,9 +49,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ElectrumBalance? initialBalance, Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialChangeAddressIndex, - }) : mwebHd = bitcoin.HDWallet.fromSeed(seedBytes, - network: litecoinNetwork).derivePath("m/1000'"), - super( + }) : mwebHd = + bitcoin.HDWallet.fromSeed(seedBytes, network: litecoinNetwork).derivePath("m/1000'"), + super( mnemonic: mnemonic, password: password, walletInfo: walletInfo, @@ -78,6 +78,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } final bitcoin.HDWallet mwebHd; + Timer? _syncTimer; static Future<LitecoinWallet> create( {required String mnemonic, @@ -147,44 +148,50 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Future<void> startSync() async { await super.startSync(); final stub = await CwMweb.stub(); - Timer.periodic( - const Duration(milliseconds: 1500), (timer) async { - if (syncStatus is FailedSyncStatus) return; - final height = await electrumClient.getCurrentBlockChainTip() ?? 0; - final resp = await stub.status(StatusRequest()); - if (resp.blockHeaderHeight < height) { - int h = resp.blockHeaderHeight; - syncStatus = SyncingSyncStatus(height - h, h / height); - } else if (resp.mwebHeaderHeight < height) { - int h = resp.mwebHeaderHeight; - syncStatus = SyncingSyncStatus(height - h, h / height); - } else if (resp.mwebUtxosHeight < height) { - syncStatus = SyncingSyncStatus(1, 0.999); - } else { + _syncTimer?.cancel(); + _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async { + print(syncStatus); + if (syncStatus is FailedSyncStatus) return; + final height = await electrumClient.getCurrentBlockChainTip() ?? 0; + final resp = await stub.status(StatusRequest()); + if (resp.blockHeaderHeight < height) { + int h = resp.blockHeaderHeight; + syncStatus = SyncingSyncStatus(height - h, h / height); + } else if (resp.mwebHeaderHeight < height) { + int h = resp.mwebHeaderHeight; + syncStatus = SyncingSyncStatus(height - h, h / height); + } else if (resp.mwebUtxosHeight < height) { + syncStatus = SyncingSyncStatus(1, 0.999); + } else { + // prevent unnecessary reaction triggers: + if (syncStatus is! SyncedSyncStatus) { syncStatus = SyncedSyncStatus(); - if (resp.mwebUtxosHeight > mwebUtxosHeight) { - mwebUtxosHeight = resp.mwebUtxosHeight; - await checkMwebUtxosSpent(); - for (final transaction in transactionHistory.transactions.values) { - if (transaction.isPending) continue; - final confirmations = mwebUtxosHeight - transaction.height + 1; - if (transaction.confirmations == confirmations) continue; - transaction.confirmations = confirmations; - transactionHistory.addOne(transaction); - } - await transactionHistory.save(); - } } - }); + + if (resp.mwebUtxosHeight > mwebUtxosHeight) { + mwebUtxosHeight = resp.mwebUtxosHeight; + await checkMwebUtxosSpent(); + for (final transaction in transactionHistory.transactions.values) { + if (transaction.isPending) continue; + final confirmations = mwebUtxosHeight - transaction.height + 1; + if (transaction.confirmations == confirmations) continue; + transaction.confirmations = confirmations; + transactionHistory.addOne(transaction); + } + await transactionHistory.save(); + } + } + }); processMwebUtxos(); } final Map<String, Utxo> mwebUtxos = {}; + int lastMwebUtxosHeight = 0; Future<void> processMwebUtxos() async { final stub = await CwMweb.stub(); final scanSecret = mwebHd.derive(0x80000000).privKey!; - final req = UtxosRequest(scanSecret: hex.decode(scanSecret)); + final req = UtxosRequest(scanSecret: hex.decode(scanSecret), fromHeight: lastMwebUtxosHeight); var initDone = false; await for (var utxo in stub.utxos(req)) { if (utxo.address.isEmpty) { @@ -203,16 +210,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { date = DateTime.fromMillisecondsSinceEpoch(utxo.blockTime * 1000); confirmations = status.blockHeaderHeight - utxo.height + 1; } - var tx = transactionHistory.transactions.values.firstWhereOrNull( - (tx) => tx.outputAddresses?.contains(utxo.outputId) ?? false); + var tx = transactionHistory.transactions.values + .firstWhereOrNull((tx) => tx.outputAddresses?.contains(utxo.outputId) ?? false); if (tx == null) tx = ElectrumTransactionInfo(WalletType.litecoin, - id: utxo.outputId, height: utxo.height, - amount: utxo.value.toInt(), fee: 0, - direction: TransactionDirection.incoming, - isPending: utxo.height == 0, - date: date, confirmations: confirmations, - inputAddresses: [], outputAddresses: [utxo.outputId]); + id: utxo.outputId, + height: utxo.height, + amount: utxo.value.toInt(), + fee: 0, + direction: TransactionDirection.incoming, + isPending: utxo.height == 0, + date: date, + confirmations: confirmations, + inputAddresses: [], + outputAddresses: [utxo.outputId]); tx.height = utxo.height; tx.isPending = utxo.height == 0; tx.confirmations = confirmations; @@ -222,10 +233,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { isNew = true; } if (isNew) { - final addressRecord = walletAddresses.allAddresses.firstWhere( - (addressRecord) => addressRecord.address == utxo.address); - if (!(tx.inputAddresses?.contains(utxo.address) ?? false)) - addressRecord.txCount++; + final addressRecord = walletAddresses.allAddresses + .firstWhere((addressRecord) => addressRecord.address == utxo.address); + if (!(tx.inputAddresses?.contains(utxo.address) ?? false)) addressRecord.txCount++; addressRecord.balance += utxo.value.toInt(); addressRecord.setAsUsed(); } @@ -234,16 +244,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { await updateUnspent(); await updateBalance(); } + lastMwebUtxosHeight = utxo.height; + print("latest mweb utxo height: $lastMwebUtxosHeight"); } } Future<void> checkMwebUtxosSpent() async { while ((await Future.wait(transactionHistory.transactions.values - .where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending) - .map(checkPendingTransaction))).any((x) => x)); - final outputIds = mwebUtxos.values - .where((utxo) => utxo.height > 0) - .map((utxo) => utxo.outputId).toList(); + .where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending) + .map(checkPendingTransaction))) + .any((x) => x)); + final outputIds = + mwebUtxos.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList(); final stub = await CwMweb.stub(); final resp = await stub.spent(SpentRequest(outputId: outputIds)); final spent = resp.outputId; @@ -259,10 +271,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { for (final outputId in spent) { final utxo = mwebUtxos[outputId]; if (utxo == null) continue; - final addressRecord = walletAddresses.allAddresses.firstWhere( - (addressRecord) => addressRecord.address == utxo.address); - if (!inputAddresses.contains(utxo.address)) - addressRecord.txCount++; + final addressRecord = walletAddresses.allAddresses + .firstWhere((addressRecord) => addressRecord.address == utxo.address); + if (!inputAddresses.contains(utxo.address)) addressRecord.txCount++; addressRecord.balance -= utxo.value.toInt(); amount += utxo.value.toInt(); inputAddresses.add(utxo.address); @@ -273,14 +284,16 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { input.close(); var digest = output.events.single; final tx = ElectrumTransactionInfo(WalletType.litecoin, - id: digest.toString(), height: height, - amount: amount, fee: 0, - direction: TransactionDirection.outgoing, - isPending: false, - date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000), - confirmations: 1, - inputAddresses: inputAddresses.toList(), - outputAddresses: []); + id: digest.toString(), + height: height, + amount: amount, + fee: 0, + direction: TransactionDirection.outgoing, + isPending: false, + date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000), + confirmations: 1, + inputAddresses: inputAddresses.toList(), + outputAddresses: []); transactionHistory.addOne(tx); await transactionHistory.save(); } @@ -295,10 +308,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { outputId.addAll(payingToOutputIds); target.addAll(spendingOutputIds); for (final outputId in payingToOutputIds) { - final spendingTx = transactionHistory.transactions.values.firstWhereOrNull( - (tx) => tx.inputAddresses?.contains(outputId) ?? false); - if (spendingTx != null && !spendingTx.isPending) - target.add(outputId); + final spendingTx = transactionHistory.transactions.values + .firstWhereOrNull((tx) => tx.inputAddresses?.contains(outputId) ?? false); + if (spendingTx != null && !spendingTx.isPending) target.add(outputId); } if (outputId.isEmpty) return false; final stub = await CwMweb.stub(); @@ -319,10 +331,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { await checkMwebUtxosSpent(); final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; mwebUtxos.forEach((outputId, utxo) { - final addressRecord = walletAddresses.allAddresses.firstWhere( - (addressRecord) => addressRecord.address == utxo.address); - final unspent = BitcoinUnspent(addressRecord, outputId, - utxo.value.toInt(), mwebAddrs.indexOf(utxo.address)); + final addressRecord = walletAddresses.allAddresses + .firstWhere((addressRecord) => addressRecord.address == utxo.address); + final unspent = BitcoinUnspent( + addressRecord, outputId, utxo.value.toInt(), mwebAddrs.indexOf(utxo.address)); if (unspent.vout == 0) unspent.isChange = true; unspentCoins.add(unspent); }); @@ -339,8 +351,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { else unconfirmed += utxo.value.toInt(); }); - return ElectrumBalance(confirmed: confirmed, - unconfirmed: unconfirmed, frozen: balance.frozen); + return ElectrumBalance(confirmed: confirmed, unconfirmed: unconfirmed, frozen: balance.frozen); } @override @@ -360,30 +371,30 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } @override - Future<int> calcFee({ - required List<UtxoWithAddress> utxos, + Future<int> calcFee( + {required List<UtxoWithAddress> utxos, required List<BitcoinBaseOutput> outputs, required BasedUtxoNetwork network, String? memo, required int feeRate}) async { - final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); - final paysToMweb = outputs.any((output) => - output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); + final paysToMweb = outputs + .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); if (!spendsMweb && !paysToMweb) { - return await super.calcFee(utxos: utxos, outputs: outputs, - network: network, memo: memo, feeRate: feeRate); + return await super + .calcFee(utxos: utxos, outputs: outputs, network: network, memo: memo, feeRate: feeRate); } if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) { - outputs = [BitcoinScriptOutput( - script: outputs[0].toOutput.scriptPubKey, - value: utxos.sumOfUtxosValue())]; + outputs = [ + BitcoinScriptOutput( + script: outputs[0].toOutput.scriptPubKey, value: utxos.sumOfUtxosValue()) + ]; } - final preOutputSum = outputs.fold<BigInt>(BigInt.zero, - (acc, output) => acc + output.toOutput.amount); + final preOutputSum = + outputs.fold<BigInt>(BigInt.zero, (acc, output) => acc + output.toOutput.amount); final fee = utxos.sumOfUtxosValue() - preOutputSum; - final txb = BitcoinTransactionBuilder(utxos: utxos, - outputs: outputs, fee: fee, network: network); + final txb = + BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network); final stub = await CwMweb.stub(); final resp = await stub.create(CreateRequest( rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(), @@ -392,16 +403,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { feeRatePerKb: Int64(feeRate * 1000), dryRun: true)); final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); - final posUtxos = utxos.where((utxo) => tx.inputs.any((input) => - input.txId == utxo.utxo.txHash && input.txIndex == utxo.utxo.vout)).toList(); + final posUtxos = utxos + .where((utxo) => tx.inputs + .any((input) => input.txId == utxo.utxo.txHash && input.txIndex == utxo.utxo.vout)) + .toList(); final posOutputSum = tx.outputs.fold<int>(0, (acc, output) => acc + output.amount.toInt()); final mwebInputSum = utxos.sumOfUtxosValue() - posUtxos.sumOfUtxosValue(); final expectedPegin = max(0, (preOutputSum - mwebInputSum).toInt()); var feeIncrease = posOutputSum - expectedPegin; if (expectedPegin > 0 && fee == BigInt.zero) { - feeIncrease += await super.calcFee(utxos: posUtxos, outputs: tx.outputs.map((output) => - BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount)).toList(), - network: network, memo: memo, feeRate: feeRate) + feeRate * 41; + feeIncrease += await super.calcFee( + utxos: posUtxos, + outputs: tx.outputs + .map((output) => + BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount)) + .toList(), + network: network, + memo: memo, + feeRate: feeRate) + + feeRate * 41; } return fee.toInt() + feeIncrease; } @@ -416,35 +436,43 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { spendSecret: hex.decode(mwebHd.derive(0x80000001).privKey!), feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000)); final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); - tx.hexOverride = tx2.copyWith(witnesses: tx2.inputs.asMap().entries.map((e) { - final utxo = unspentCoins.firstWhere((utxo) => - utxo.hash == e.value.txId && utxo.vout == e.value.txIndex); - final key = generateECPrivate(hd: utxo.bitcoinAddressRecord.isHidden ? - walletAddresses.sideHd : walletAddresses.mainHd, - index: utxo.bitcoinAddressRecord.index, network: network); - final digest = tx2.getTransactionSegwitDigit(txInIndex: e.key, - script: key.getPublic().toP2pkhAddress().toScriptPubKey(), - amount: BigInt.from(utxo.value)); - return TxWitnessInput(stack: [key.signInput(digest), key.getPublic().toHex()]); - }).toList()).toHex(); + tx.hexOverride = tx2 + .copyWith( + witnesses: tx2.inputs.asMap().entries.map((e) { + final utxo = unspentCoins + .firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex); + final key = generateECPrivate( + hd: utxo.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: utxo.bitcoinAddressRecord.index, + network: network); + final digest = tx2.getTransactionSegwitDigit( + txInIndex: e.key, + script: key.getPublic().toP2pkhAddress().toScriptPubKey(), + amount: BigInt.from(utxo.value)); + return TxWitnessInput(stack: [key.signInput(digest), key.getPublic().toHex()]); + }).toList()) + .toHex(); tx.outputs = resp.outputId; - return tx..addListener((transaction) async { - final addresses = <String>{}; - transaction.inputAddresses?.forEach((id) { - final utxo = mwebUtxos.remove(id); - if (utxo == null) return; - final addressRecord = walletAddresses.allAddresses.firstWhere( - (addressRecord) => addressRecord.address == utxo.address); - if (!addresses.contains(utxo.address)) { - addressRecord.txCount++; - addresses.add(utxo.address); - } - addressRecord.balance -= utxo.value.toInt(); + return tx + ..addListener((transaction) async { + final addresses = <String>{}; + transaction.inputAddresses?.forEach((id) { + final utxo = mwebUtxos.remove(id); + if (utxo == null) return; + final addressRecord = walletAddresses.allAddresses + .firstWhere((addressRecord) => addressRecord.address == utxo.address); + if (!addresses.contains(utxo.address)) { + addressRecord.txCount++; + addresses.add(utxo.address); + } + addressRecord.balance -= utxo.value.toInt(); + }); + transaction.inputAddresses?.addAll(addresses); + transactionHistory.addOne(transaction); + await updateUnspent(); + await updateBalance(); }); - transaction.inputAddresses?.addAll(addresses); - transactionHistory.addOne(transaction); - await updateUnspent(); - await updateBalance(); - }); } }