From e595d2d6cd0a2e5b2d0af00ab258ca8f031adaf4 Mon Sep 17 00:00:00 2001 From: Matthew Fosse <matt@fosse.co> Date: Mon, 24 Jun 2024 12:48:42 -0700 Subject: [PATCH] [skip ci] lots of fixes, still testing --- cw_bitcoin/lib/litecoin_wallet.dart | 236 +++++++++++------- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 11 +- .../lib/pending_bitcoin_transaction.dart | 1 + 3 files changed, 155 insertions(+), 93 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 0e1cf6f06..5a58196e1 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -51,18 +51,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ElectrumBalance? initialBalance, Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialChangeAddressIndex, + int? initialMwebHeight, }) : mwebHd = bitcoin.HDWallet.fromSeed(seedBytes, network: litecoinNetwork).derivePath("m/1000'"), super( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - networkType: litecoinNetwork, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: seedBytes, - currency: CryptoCurrency.ltc) { + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + networkType: litecoinNetwork, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + currency: CryptoCurrency.ltc, + ) { walletAddresses = LitecoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, @@ -81,6 +83,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final bitcoin.HDWallet mwebHd; late final Box<MwebUtxo> mwebUtxosBox; Timer? _syncTimer; + // late int lastMwebUtxosHeight; + int mwebUtxosHeight = 0; static Future<LitecoinWallet> create( {required String mnemonic, @@ -143,10 +147,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } - // final Map<String, Utxo> mwebUtxos = {}; - int mwebUtxosHeight = 0; - int lastMwebUtxosHeight = 2699272; - @action @override Future<void> startSync() async { @@ -158,7 +158,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final height = await electrumClient.getCurrentBlockChainTip() ?? 0; final resp = await stub.status(StatusRequest()); // print("stats:"); - // print(resp.mwebHeaderHeight); + // print("???????????????????"); + // print(resp.blockHeaderHeight); // print(resp.mwebUtxosHeight); // print(height); if (resp.blockHeaderHeight < height) { @@ -183,6 +184,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final confirmations = mwebUtxosHeight - transaction.height + 1; if (transaction.confirmations == confirmations) continue; transaction.confirmations = confirmations; + print("BEING ADDED HERE@@@@@@@@@@@@@@@@@@@@@@@4"); transactionHistory.addOne(transaction); } await transactionHistory.save(); @@ -206,24 +208,16 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Future<void> processMwebUtxos() async { final stub = await CwMweb.stub(); final scanSecret = mwebHd.derive(0x80000000).privKey!; - final req = UtxosRequest(scanSecret: hex.decode(scanSecret), fromHeight: lastMwebUtxosHeight); - var initDone = false; - await for (Utxo sUtxo in stub.utxos(req)) { - if (sUtxo.address.isEmpty) { - await updateUnspent(); - await updateBalance(); + print("SCANNING FROM HEIGHT: ${walletInfo.restoreHeight}"); + final req = + UtxosRequest(scanSecret: hex.decode(scanSecret), fromHeight: walletInfo.restoreHeight); + bool initDone = false; + + for (final utxo in mwebUtxosBox.values) { + if (utxo.address.isEmpty) { initDone = true; + continue; } - final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; - if (!mwebAddrs.contains(sUtxo.address)) continue; - final utxo = MwebUtxo( - address: sUtxo.address, - blockTime: sUtxo.blockTime, - height: sUtxo.height, - outputId: sUtxo.outputId, - value: sUtxo.value.toInt(), - ); - mwebUtxosBox.put(utxo.outputId, utxo); final status = await stub.status(StatusRequest()); var date = DateTime.now(); @@ -234,26 +228,33 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } 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]); + + 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], + ); + } + tx.height = utxo.height; tx.isPending = utxo.height == 0; tx.confirmations = confirmations; - var isNew = transactionHistory.transactions[tx.id] == null; + bool isNew = transactionHistory.transactions[tx.id] == null; + if (!(tx.outputAddresses?.contains(utxo.address) ?? false)) { tx.outputAddresses?.add(utxo.address); isNew = true; } + if (isNew) { final addressRecord = walletAddresses.allAddresses .firstWhere((addressRecord) => addressRecord.address == utxo.address); @@ -261,13 +262,39 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { addressRecord.balance += utxo.value.toInt(); addressRecord.setAsUsed(); } + print("BEING ADDED HERE@@@@@@@@@@@@@@@@@@@@@@@"); transactionHistory.addOne(tx); if (initDone) { await updateUnspent(); await updateBalance(); } - lastMwebUtxosHeight = utxo.height; - print("latest mweb utxo height: $lastMwebUtxosHeight"); + + if (utxo.height > walletInfo.restoreHeight) { + walletInfo.updateRestoreHeight(utxo.height); + } + } + + await for (Utxo sUtxo in stub.utxos(req)) { + final utxo = MwebUtxo( + address: sUtxo.address, + blockTime: sUtxo.blockTime, + height: sUtxo.height, + outputId: sUtxo.outputId, + value: sUtxo.value.toInt(), + ); + + if (utxo.address.isEmpty) { + await updateUnspent(); + await updateBalance(); + initDone = true; + } + + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + + if (!mwebAddrs.contains(utxo.address) && utxo.address.isNotEmpty) { + continue; + } + await mwebUtxosBox.put(utxo.outputId, utxo); } } @@ -277,7 +304,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { .map(checkPendingTransaction))) .any((x) => x)); final outputIds = - mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId); + mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList(); final stub = await CwMweb.stub(); final resp = await stub.spent(SpentRequest(outputId: outputIds)); @@ -293,6 +320,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { var input = sha256.startChunkedConversion(output); for (final outputId in spent) { final utxo = mwebUtxosBox.get(outputId); + await mwebUtxosBox.delete(outputId); if (utxo == null) continue; final addressRecord = walletAddresses.allAddresses .firstWhere((addressRecord) => addressRecord.address == utxo.address); @@ -300,7 +328,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { addressRecord.balance -= utxo.value.toInt(); amount += utxo.value.toInt(); inputAddresses.add(utxo.address); - mwebUtxosBox.delete(outputId); input.add(hex.decode(outputId)); } if (inputAddresses.isEmpty) return; @@ -317,6 +344,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { confirmations: 1, inputAddresses: inputAddresses.toList(), outputAddresses: []); + print("BEING ADDED HERE@@@@@@@@@@@@@@@@@@@@@@@2"); + transactionHistory.addOne(tx); await transactionHistory.save(); } @@ -356,9 +385,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mwebUtxosBox.keys.forEach((dynamic oId) { final String outputId = oId as String; final utxo = mwebUtxosBox.get(outputId); - if (utxo == null) return; + if (utxo == null) { + return; + } + if (utxo.address.isEmpty) { + // not sure if a bug or a special case but we definitely ignore these + return; + } final addressRecord = walletAddresses.allAddresses - .firstWhere((addressRecord) => addressRecord.address == utxo.address); + .firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address); + + // print("^^^^^^^^^^^^^^^^^^"); + // print(utxo.address); + // for (var a in walletAddresses.allAddresses) { + // print(a.address); + // } + if (addressRecord == null) { + print("addressRecord is null! TODO: handle this case"); + return; + } final unspent = BitcoinUnspent( addressRecord, outputId, utxo.value.toInt(), mwebAddrs.indexOf(utxo.address)); if (unspent.vout == 0) unspent.isChange = true; @@ -378,6 +423,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { unconfirmed += utxo.value.toInt(); } }); + print("confirmed: $confirmed, unconfirmed: $unconfirmed"); return ElectrumBalance(confirmed: confirmed, unconfirmed: unconfirmed, frozen: balance.frozen); } @@ -465,53 +511,63 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override Future<PendingTransaction> createTransaction(Object credentials) async { - final tx = await super.createTransaction(credentials) as PendingBitcoinTransaction; - final stub = await CwMweb.stub(); - final resp = await stub.create(CreateRequest( + try { + final tx = await super.createTransaction(credentials) as PendingBitcoinTransaction; + final stub = await CwMweb.stub(); + final resp = await stub.create(CreateRequest( rawTx: hex.decode(tx.hex), scanSecret: hex.decode(mwebHd.derive(0x80000000).privKey!), 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( + 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.outputs = resp.outputId; - return tx - ..addListener((transaction) async { - final addresses = <String>{}; - transaction.inputAddresses?.forEach((id) { - final utxo = mwebUtxosBox.get(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(); - mwebUtxosBox.delete(id); + 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) async { + final utxo = mwebUtxosBox.get(id); + await mwebUtxosBox.delete(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); + print("BEING ADDED HERE@@@@@@@@@@@@@@@@@@@@@@@3"); + + transactionHistory.addOne(transaction); + await updateUnspent(); + await updateBalance(); }); - transaction.inputAddresses?.addAll(addresses); - transactionHistory.addOne(transaction); - await updateUnspent(); - await updateBalance(); - }); + } catch (e, s) { + print(e); + print(s); + rethrow; + } } @override diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index f1b05c5bf..d405ad58f 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -22,7 +22,9 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, - }) : super(walletInfo); + }) : super(walletInfo) { + topUpMweb(0); + } final HDWallet mwebHd; List<String> mwebAddrs = []; @@ -49,13 +51,14 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with String getAddress({required int index, required HDWallet hd, BitcoinAddressType? addressType}) { if (addressType == SegwitAddresType.mweb) { topUpMweb(index); - return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index+1]; + return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1]; } return generateP2WPKHAddress(hd: hd, index: index, network: network); } @override - Future<String> getAddressAsync({required int index, required HDWallet hd, BitcoinAddressType? addressType}) async { + Future<String> getAddressAsync( + {required int index, required HDWallet hd, BitcoinAddressType? addressType}) async { if (addressType == SegwitAddresType.mweb) { await topUpMweb(index); } @@ -65,6 +68,8 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @action @override Future<String> getChangeAddress() async { + updateChangeAddresses(); + // this means all change addresses used will be mweb addresses!: await topUpMweb(0); return mwebAddrs[0]; } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index ab05621ac..dfc032c24 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -103,6 +103,7 @@ class PendingBitcoinTransaction with PendingTransaction { await _commit(); } + _listeners.forEach((listener) => listener(transactionInfo())); }