import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitbox/bitbox.dart' as bitbox; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ElectrumWalletAddressesBase( WalletInfo walletInfo, { required this.mainHd, required this.sideHd, required this.transactionHistory, required this.networkType, List? initialAddresses, int initialRegularAddressIndex = 0, int initialChangeAddressIndex = 0, bitcoin.SilentPaymentReceiver? silentAddress, }) : addresses = ObservableList.of((initialAddresses ?? []).toSet()), silentAddress = silentAddress, receiveAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) .toSet()), changeAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) .toSet()), silentAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => addressRecord.silentAddressLabel != null) .toSet()), currentReceiveAddressIndex = initialRegularAddressIndex, currentChangeAddressIndex = initialChangeAddressIndex, super(walletInfo); static const defaultReceiveAddressesCount = 22; static const defaultChangeAddressesCount = 17; static const gap = 20; static String toCashAddr(String address) => bitbox.Address.toCashAddress(address); final ObservableList addresses; final ObservableList receiveAddresses; final ObservableList changeAddresses; final ObservableList silentAddresses; final ElectrumTransactionHistory transactionHistory; final bitcoin.NetworkType networkType; final bitcoin.HDWallet mainHd; final bitcoin.HDWallet sideHd; // TODO: labels -> disable edit on receive page final bitcoin.SilentPaymentReceiver? silentAddress; @observable // ignore: prefer_final_fields dynamic _addressPageType = bitcoin.AddressType.p2wpkh; @computed dynamic get addressPageType => _addressPageType; @observable String? activeAddress; @computed String get receiveAddress { if (receiveAddresses.isEmpty) { final address = generateNewAddress().address; return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(address) : address; } final receiveAddress = receiveAddresses.first.address; return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress; } @override @computed String get address { if (addressPageType == bitcoin.AddressType.p2sp) { return silentAddress!.toString(); } if (activeAddress != null) { return activeAddress!; } if (receiveAddresses.isEmpty) { return generateNewAddress().address; } try { return receiveAddresses .firstWhere((address) => addressPageType == bitcoin.AddressType.p2wpkh ? address.type == null || address.type == addressPageType : address.type == addressPageType) .address; } catch (_) {} return receiveAddresses.first.address; } @override set address(String addr) => activeAddress = addr; int currentReceiveAddressIndex; int currentChangeAddressIndex; @computed int get totalCountOfReceiveAddresses => addresses.fold(0, (acc, addressRecord) { if (!addressRecord.isHidden) { return acc + 1; } return acc; }); @computed int get totalCountOfChangeAddresses => addresses.fold(0, (acc, addressRecord) { if (addressRecord.isHidden) { return acc + 1; } return acc; }); Future discoverAddresses() async { await _discoverAddresses(mainHd, false); await _discoverAddresses(sideHd, true); await updateAddressesInBox(); } @override Future init() async { await _generateInitialAddresses(); updateReceiveAddresses(); updateChangeAddresses(); await updateAddressesInBox(); if (currentReceiveAddressIndex >= receiveAddresses.length) { currentReceiveAddressIndex = 0; } if (currentChangeAddressIndex >= changeAddresses.length) { currentChangeAddressIndex = 0; } } @action Future getChangeAddress() async { updateChangeAddresses(); if (changeAddresses.isEmpty) { final newAddresses = await _createNewAddresses(gap, hd: sideHd, startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0, isHidden: true); addAddresses(newAddresses); } if (currentChangeAddressIndex >= changeAddresses.length) { currentChangeAddressIndex = 0; } updateChangeAddresses(); final address = changeAddresses[currentChangeAddressIndex].address; currentChangeAddressIndex += 1; return address; } @action BitcoinAddressRecord generateNewAddress( {bitcoin.HDWallet? hd, bool isHidden = false, String? label}) { if (label != null && silentAddress != null) { final address = BitcoinAddressRecord( bitcoin.SilentPaymentAddress.createLabeledSilentPaymentAddress( silentAddress!.scanPubkey, silentAddress!.spendPubkey, '0000000000000000000000000000000000000000000000000000000000000002'.fromHex, hrp: silentAddress!.hrp, version: silentAddress!.version) .toString(), index: currentReceiveAddressIndex, isHidden: isHidden, silentAddressLabel: label); silentAddresses.add(address); return address; } // FIX-ME: Check logic for whichi HD should be used here ??? final address = BitcoinAddressRecord( getAddress( index: currentReceiveAddressIndex, hd: hd ?? sideHd, addressType: addressPageType as bitcoin.AddressType, ), index: currentReceiveAddressIndex, isHidden: isHidden); addresses.add(address); return address; currentReceiveAddressIndex += 1; } String getAddress( {required int index, required bitcoin.HDWallet hd, bitcoin.AddressType? addressType}) => ''; @override Future updateAddressesInBox() async { try { addressesMap.clear(); addressesMap[address] = ''; await saveAddressesInBox(); } catch (e) { print(e.toString()); } } @action void updateReceiveAddresses() { receiveAddresses.removeRange(0, receiveAddresses.length); final newAdresses = addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); receiveAddresses.addAll(newAdresses); } @action void updateChangeAddresses() { changeAddresses.removeRange(0, changeAddresses.length); final newAdresses = addresses.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed); changeAddresses.addAll(newAdresses); } @action Future _discoverAddresses(bitcoin.HDWallet hd, bool isHidden, {bitcoin.AddressType? addressType}) async { var hasAddrUse = true; List addrs; if (addresses.where((addr) => addr.type == addressPageType).isNotEmpty) { addrs = addresses.where((addr) => addr.isHidden == isHidden).toList(); } else { addrs = await _createNewAddresses( isHidden ? defaultChangeAddressesCount : defaultReceiveAddressesCount, startIndex: 0, hd: hd, isHidden: isHidden, addressType: addressType); } while (hasAddrUse) { final addr = addrs.last.address; hasAddrUse = await _hasAddressUsed(addr); if (!hasAddrUse) { break; } final start = addrs.length; final count = start + gap; final batch = await _createNewAddresses(count, startIndex: start, hd: hd, isHidden: isHidden, addressType: addressType); addrs.addAll(batch); } if (addresses.length < addrs.length || addressPageType != null) { addAddresses(addrs); } } Future _generateInitialAddresses() async { var countOfReceiveAddresses = 0; var countOfHiddenAddresses = 0; addresses.forEach((addr) { if (addr.isHidden) { countOfHiddenAddresses += 1; return; } countOfReceiveAddresses += 1; }); if (countOfReceiveAddresses < defaultReceiveAddressesCount) { final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; final newAddresses = await _createNewAddresses(addressesCount, startIndex: countOfReceiveAddresses, hd: mainHd, isHidden: false); addresses.addAll(newAddresses); } if (countOfHiddenAddresses < defaultChangeAddressesCount) { final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; final newAddresses = await _createNewAddresses(addressesCount, startIndex: countOfHiddenAddresses, hd: sideHd, isHidden: true); addresses.addAll(newAddresses); } } Future> _createNewAddresses(int count, {required bitcoin.HDWallet hd, int startIndex = 0, bool isHidden = false, bitcoin.AddressType? addressType}) async { final list = []; for (var i = startIndex; i < count + startIndex; i++) { final address = BitcoinAddressRecord(getAddress(index: i, hd: hd, addressType: addressType), index: i, isHidden: isHidden, type: addressType); list.add(address); } return list; } @action void addAddresses(Iterable addresses) { final addressesSet = this.addresses.toSet(); addressesSet.addAll(addresses); this.addresses.removeRange(0, this.addresses.length); this.addresses.addAll(addressesSet); } Future _hasAddressUsed(String address) async { return transactionHistory.transactions.values.any((txInfo) => txInfo.to == address); } @override @action Future setAddressType(dynamic type) async { _addressPageType = type as bitcoin.AddressType; if (addressPageType != bitcoin.AddressType.p2sp) { await _discoverAddresses(mainHd, false, addressType: addressPageType as bitcoin.AddressType); updateReceiveAddresses(); } } }