cake_wallet/cw_bitcoin/lib/electrum_wallet_addresses.dart
2023-11-30 14:32:11 -03:00

340 lines
11 KiB
Dart

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<BitcoinAddressRecord>? initialAddresses,
int initialRegularAddressIndex = 0,
int initialChangeAddressIndex = 0,
bitcoin.SilentPaymentReceiver? silentAddress,
}) : addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
silentAddress = silentAddress,
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
.toSet()),
changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
.toSet()),
silentAddresses = ObservableList<BitcoinAddressRecord>.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<BitcoinAddressRecord> addresses;
final ObservableList<BitcoinAddressRecord> receiveAddresses;
final ObservableList<BitcoinAddressRecord> changeAddresses;
final ObservableList<BitcoinAddressRecord> 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<void> discoverAddresses() async {
await _discoverAddresses(mainHd, false);
await _discoverAddresses(sideHd, true);
await updateAddressesInBox();
}
@override
Future<void> init() async {
await _generateInitialAddresses();
updateReceiveAddresses();
updateChangeAddresses();
await updateAddressesInBox();
if (currentReceiveAddressIndex >= receiveAddresses.length) {
currentReceiveAddressIndex = 0;
}
if (currentChangeAddressIndex >= changeAddresses.length) {
currentChangeAddressIndex = 0;
}
}
@action
Future<String> 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<void> 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<void> _discoverAddresses(bitcoin.HDWallet hd, bool isHidden,
{bitcoin.AddressType? addressType}) async {
var hasAddrUse = true;
List<BitcoinAddressRecord> 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<void> _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<List<BitcoinAddressRecord>> _createNewAddresses(int count,
{required bitcoin.HDWallet hd,
int startIndex = 0,
bool isHidden = false,
bitcoin.AddressType? addressType}) async {
final list = <BitcoinAddressRecord>[];
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<BitcoinAddressRecord> addresses) {
final addressesSet = this.addresses.toSet();
addressesSet.addAll(addresses);
this.addresses.removeRange(0, this.addresses.length);
this.addresses.addAll(addressesSet);
}
Future<bool> _hasAddressUsed(String address) async {
return transactionHistory.transactions.values.any((txInfo) => txInfo.to == address);
}
@override
@action
Future<void> setAddressType(dynamic type) async {
_addressPageType = type as bitcoin.AddressType;
if (addressPageType != bitcoin.AddressType.p2sp) {
await _discoverAddresses(mainHd, false, addressType: addressPageType as bitcoin.AddressType);
updateReceiveAddresses();
}
}
}