import 'dart:async'; import 'package:convert/convert.dart'; import 'package:crypto/crypto.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_mweb/cw_mweb.dart'; import 'package:cw_mweb/mwebd.pb.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:queue/queue.dart'; part 'litecoin_wallet.g.dart'; class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet; abstract class LitecoinWalletBase extends ElectrumWallet with Store { LitecoinWalletBase({ required String mnemonic, required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, required Uint8List seedBytes, String? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, }) : 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) { walletAddresses = LitecoinWalletAddresses( walletInfo, electrumClient: electrumClient, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), mwebHd: mwebHd, network: network, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); } final bitcoin.HDWallet mwebHd; static Future create( {required String mnemonic, required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, String? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex}) async { return LitecoinWallet( mnemonic: mnemonic, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: await mnemonicToSeedBytes(mnemonic), initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, ); } static Future open({ required String name, required WalletInfo walletInfo, required Box unspentCoinsInfo, required String password, }) async { final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet); return LitecoinWallet( mnemonic: snp.mnemonic, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, seedBytes: await mnemonicToSeedBytes(snp.mnemonic), initialRegularAddressIndex: snp.regularAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex, addressPageType: snp.addressPageType, ); } @action @override Future startSync() async { await super.startSync(); final stub = await CwMweb.stub(); Timer.periodic( const Duration(milliseconds: 1500), (timer) async { 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 { syncStatus = SyncedSyncStatus(); } }); processMwebUtxos(); } final Map mwebUtxos = {}; Future processMwebUtxos() async { final stub = await CwMweb.stub(); final scanSecret = mwebHd.derive(0x80000000).privKey!; final req = UtxosRequest(scanSecret: hex.decode(scanSecret)); await for (var utxo in stub.utxos(req)) { final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; if (!mwebAddrs.contains(utxo.address)) continue; mwebUtxos[utxo.outputId] = utxo; final status = await stub.status(StatusRequest()); var date = DateTime.now(); var confirmations = 0; if (utxo.height > 0) { while (true) try { date = await electrumClient.getBlockTime(height: utxo.height); break; } catch (err) { await Future.delayed(Duration(seconds: 1)); } confirmations = status.blockHeaderHeight - utxo.height + 1; } final 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.address]); transactionHistory.addOne(tx); queueUpdate(1); } } Future checkMwebUtxosSpent() async { final List outputIds = []; mwebUtxos.forEach((outputId, utxo) { if (utxo.height > 0) outputIds.add(outputId); }); final stub = await CwMweb.stub(); final resp = await stub.spent(SpentRequest(outputId: outputIds)); final spent = resp.outputId; if (spent.isEmpty) return; final status = await stub.status(StatusRequest()); final height = await electrumClient.getCurrentBlockChainTip(); if (height == null || status.mwebUtxosHeight != height) return; final date = await electrumClient.getBlockTime(height: height); int amount = 0; Set inputAddresses = {}; var output = AccumulatorSink(); var input = sha256.startChunkedConversion(output); for (final outputId in spent) { input.add(hex.decode(outputId)); amount += mwebUtxos[outputId]!.value.toInt(); inputAddresses.add(mwebUtxos[outputId]!.address); mwebUtxos.remove(outputId); } 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: date, confirmations: 1, inputAddresses: inputAddresses.toList(), outputAddresses: []); transactionHistory.addOne(tx); await transactionHistory.save(); } @override Future updateUnspentCoins() async { await super.updateUnspentCoins(); await checkMwebUtxosSpent(); final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; mwebUtxos.forEach((outputId, utxo) { final addressRecord = walletAddresses.allAddresses.firstWhere( (addressRecord) => addressRecord.address == utxo.address); unspentCoins.add(BitcoinUnspent(addressRecord, outputId, utxo.value.toInt(), mwebAddrs.indexOf(utxo.address))); }); } @override Future fetchBalances() async { final balance = await super.fetchBalances(); var confirmed = balance.confirmed; var unconfirmed = balance.unconfirmed; mwebUtxos.values.forEach((utxo) { if (utxo.height > 0) confirmed += utxo.value.toInt(); else unconfirmed += utxo.value.toInt(); }); return ElectrumBalance(confirmed: confirmed, unconfirmed: unconfirmed, frozen: balance.frozen); } @override Future updateBalance({int delay = 1}) async { queueUpdate(delay); } Timer? _debounceTimer; final _debounceQueue = Queue(); void queueUpdate(int delay) { if (_debounceTimer?.isActive ?? false) _debounceTimer?.cancel(); _debounceTimer = Timer(Duration(seconds: delay), () => _debounceQueue.add(() async { await updateUnspent(); await super.updateBalance(); })); } @override int feeRate(TransactionPriority priority) { if (priority is LitecoinTransactionPriority) { switch (priority) { case LitecoinTransactionPriority.slow: return 1; case LitecoinTransactionPriority.medium: return 2; case LitecoinTransactionPriority.fast: return 3; } } return 0; } }