diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 5e3967308..5210604ef 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,14 +1,18 @@ import 'dart:convert'; class BitcoinAddressRecord { - BitcoinAddressRecord(this.address, {this.index, bool isHidden}) - : _isHidden = isHidden; + BitcoinAddressRecord(this.address, + {this.index, this.isHidden = false, bool isUsed = false}) + : _isUsed = isUsed; factory BitcoinAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; - return BitcoinAddressRecord(decoded['address'] as String, - index: decoded['index'] as int, isHidden: decoded['isHidden'] as bool); + return BitcoinAddressRecord( + decoded['address'] as String, + index: decoded['index'] as int, + isHidden: decoded['isHidden'] as bool ?? false, + isUsed: decoded['isUsed'] as bool ?? false); } @override @@ -16,13 +20,21 @@ class BitcoinAddressRecord { o is BitcoinAddressRecord && address == o.address; final String address; - bool get isHidden => _isHidden ?? false; - int index; - final bool _isHidden; + final bool isHidden; + final int index; + bool get isUsed => _isUsed; @override int get hashCode => address.hashCode; + bool _isUsed; + + void setAsUsed() => _isUsed = true; + String toJSON() => - json.encode({'address': address, 'index': index, 'isHidden': isHidden}); + json.encode({ + 'address': address, + 'index': index, + 'isHidden': isHidden, + 'isUsed': isUsed}); } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index f56e72e3c..243e342b7 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -23,7 +23,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @required Box unspentCoinsInfo, List initialAddresses, ElectrumBalance initialBalance, - int accountIndex = 0}) + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0}) : super( mnemonic: mnemonic, password: password, @@ -34,8 +35,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance) { walletAddresses = BitcoinWalletAddresses( walletInfo, + electrumClient: electrumClient, initialAddresses: initialAddresses, - accountIndex: accountIndex, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, sideHd: bitcoin.HDWallet.fromSeed( mnemonicToSeedBytes(mnemonic), network: networkType) @@ -58,6 +61,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, - accountIndex: snp.accountIndex); + initialRegularAddressIndex: snp.regularAddressIndex, + initialChangeAddressIndex: snp.changeAddressIndex); } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index b164975a9..33e79c102 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,4 +1,5 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; @@ -16,18 +17,21 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses BitcoinWalletAddressesBase( WalletInfo walletInfo, {@required List initialAddresses, - int accountIndex = 0, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0, + ElectrumClient electrumClient, @required bitcoin.HDWallet mainHd, @required bitcoin.HDWallet sideHd, - @required this.networkType}) + @required bitcoin.NetworkType networkType}) : super( walletInfo, initialAddresses: initialAddresses, - accountIndex: accountIndex, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, mainHd: mainHd, - sideHd: sideHd); - - bitcoin.NetworkType networkType; + sideHd: sideHd, + electrumClient: electrumClient, + networkType: networkType); @override String getAddress({@required int index, @required bitcoin.HDWallet hd}) => diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index ac787061e..4bd46f43c 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -125,6 +125,7 @@ abstract class ElectrumWalletBase extends WalletBase startSync() async { try { syncStatus = StartingSyncStatus(); + await walletAddresses.discoverAddresses(); await updateTransactions(); _subscribeForUpdates(); await _updateBalance(); @@ -206,12 +207,10 @@ abstract class ElectrumWalletBase extends WalletBase json.encode({ 'mnemonic': mnemonic, - 'account_index': walletAddresses.accountIndex.toString(), + 'account_index': walletAddresses.currentReceiveAddressIndex.toString(), + 'change_address_index': walletAddresses.currentChangeAddressIndex.toString(), 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(), 'balance': balance?.toJSON() }); @@ -563,16 +559,34 @@ abstract class ElectrumWalletBase extends WalletBase _fetchBalances() async { - final balances = await Future.wait( - scriptHashes.map((sh) => electrumClient.getBalance(sh))); - final balance = balances.fold( - ElectrumBalance(confirmed: 0, unconfirmed: 0), - (ElectrumBalance acc, val) => ElectrumBalance( - confirmed: (val['confirmed'] as int ?? 0) + (acc.confirmed ?? 0), - unconfirmed: - (val['unconfirmed'] as int ?? 0) + (acc.unconfirmed ?? 0))); + final addresses = walletAddresses.addresses.toList(); + final balanceFutures = >>[]; - return balance; + for (var i = 0; i < addresses.length; i++) { + final addressRecord = addresses[i]; + final sh = scriptHash(addressRecord.address, networkType: networkType); + final balanceFuture = electrumClient.getBalance(sh); + balanceFutures.add(balanceFuture); + } + + final balances = await Future.wait(balanceFutures); + var totalConfirmed = 0; + var totalUnconfirmed = 0; + + for (var i = 0; i < balances.length; i++) { + final addressRecord = addresses[i]; + final balance = balances[i]; + final confirmed = balance['confirmed'] as int ?? 0; + final unconfirmed = balance['unconfirmed'] as int ?? 0; + totalConfirmed += confirmed; + totalUnconfirmed += unconfirmed; + + if (confirmed > 0 || unconfirmed > 0) { + addressRecord.setAsUsed(); + } + } + + return ElectrumBalance(confirmed: totalConfirmed, unconfirmed: totalUnconfirmed); } Future _updateBalance() async { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 8f0d3cfa0..5df3047af 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,5 +1,7 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/electrum.dart'; +import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:flutter/foundation.dart'; @@ -14,120 +16,118 @@ class ElectrumWalletAddresses = ElectrumWalletAddressesBase abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ElectrumWalletAddressesBase(WalletInfo walletInfo, {@required List initialAddresses, - int accountIndex = 0, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0, this.mainHd, - this.sideHd}) + this.sideHd, + this.electrumClient, + this.networkType}) : super(walletInfo) { - this.accountIndex = accountIndex; + currentReceiveAddressIndex = initialRegularAddressIndex; + currentChangeAddressIndex = initialChangeAddressIndex; addresses = ObservableList.of( (initialAddresses ?? []).toSet()); } - static const regularAddressesCount = 22; - static const hiddenAddressesCount = 17; + static const defaultReceiveAddressesCount = 22; + static const defaultChangeAddressesCount = 17; + static const gap = 20; @override @observable String address; + int currentReceiveAddressIndex; + int currentChangeAddressIndex; + ElectrumClient electrumClient; + bitcoin.NetworkType networkType; bitcoin.HDWallet mainHd; bitcoin.HDWallet sideHd; - ObservableList addresses; - List get availableAddresses => addresses - .where((addr) => !addr.isHidden) + List get receiveAddresses => addresses + .where((addr) => !addr.isHidden && !addr.isUsed) .toList(); - int accountIndex; + List get changeAddresses => addresses + .where((addr) => addr.isHidden && !addr.isUsed) + .toList(); + + Future discoverAddresses() async { + await _discoverAddresses(mainHd, false); + await _discoverAddresses(sideHd, true); + await updateAddressesInBox(); + } @override Future init() async { - await generateAddresses(); - final _availableAddresses = availableAddresses; + await _generateInitialAddresses(); - if (accountIndex >= _availableAddresses.length) { - accountIndex = 0; + if (receiveAddresses.isEmpty) { + final count = currentReceiveAddressIndex + gap; + final newAddresses = await _createNewAddresses( + count, + hd: mainHd, + startIndex: currentReceiveAddressIndex, + isHidden: false); + _addAddresses(newAddresses); + } else if (currentReceiveAddressIndex >= receiveAddresses.length) { + currentReceiveAddressIndex = 0; } - address = _availableAddresses[accountIndex].address; + address = receiveAddresses[currentReceiveAddressIndex].address; await updateAddressesInBox(); } @action - Future nextAddress() async { - accountIndex += 1; - final _availableAddresses = availableAddresses; - - if (accountIndex >= _availableAddresses.length) { - accountIndex = 0; + Future nextReceiveAddress() async { + if (receiveAddresses.isEmpty) { + final count = currentReceiveAddressIndex + gap; + final newAddresses = await _createNewAddresses( + count, + hd: sideHd, + startIndex: currentReceiveAddressIndex, + isHidden: false); + _addAddresses(newAddresses); + } else if (currentReceiveAddressIndex >= receiveAddresses.length) { + currentReceiveAddressIndex = 0; } - address = _availableAddresses[accountIndex].address; - + address = receiveAddresses[currentReceiveAddressIndex].address; + currentReceiveAddressIndex += 1; await updateAddressesInBox(); } - Future generateAddresses() async { - final regularAddresses = []; - final hiddenAddresses = []; - - addresses.forEach((addr) { - if (addr.isHidden) { - hiddenAddresses.add(addr); - return; - } - - regularAddresses.add(addr); - }); - - if (regularAddresses.length < regularAddressesCount) { - final addressesCount = regularAddressesCount - regularAddresses.length; - await generateNewAddresses(addressesCount, - startIndex: regularAddresses.length, hd: mainHd, isHidden: false); + @action + Future getChangeAddress() async { + if (changeAddresses.isEmpty) { + final count = currentChangeAddressIndex + gap; + final newAddresses = await _createNewAddresses( + count, + startIndex: currentChangeAddressIndex, + isHidden: true); + _addAddresses(newAddresses); + } else if (currentChangeAddressIndex >= changeAddresses.length) { + currentChangeAddressIndex = 0; } - if (hiddenAddresses.length < hiddenAddressesCount) { - final addressesCount = hiddenAddressesCount - hiddenAddresses.length; - await generateNewAddresses(addressesCount, - startIndex: hiddenAddresses.length, hd: sideHd, isHidden: true); - } + + final address = changeAddresses[currentChangeAddressIndex].address; + currentChangeAddressIndex += 1; + return address; } Future generateNewAddress( {bool isHidden = false, bitcoin.HDWallet hd}) async { - accountIndex += 1; + currentReceiveAddressIndex += 1; final address = BitcoinAddressRecord( - getAddress(index: accountIndex, hd: hd), - index: accountIndex, + getAddress(index: currentReceiveAddressIndex, hd: hd), + index: currentReceiveAddressIndex, isHidden: isHidden); addresses.add(address); return address; } - Future> generateNewAddresses(int count, - {int startIndex = 0, bitcoin.HDWallet hd, bool isHidden = false}) async { - final list = []; - - for (var i = startIndex; i < count + startIndex; i++) { - final address = BitcoinAddressRecord(getAddress(index: i, hd: hd), - index: i, isHidden: isHidden); - list.add(address); - } - - addresses.addAll(list); - return list; - } - - /*Future updateAddress(String address) async { - for (final addr in addresses) { - if (addr.address == address) { - await save(); - break; - } - } - }*/ - String getAddress({@required int index, @required bitcoin.HDWallet hd}) => ''; @override @@ -135,7 +135,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { try { addressesMap.clear(); addressesMap[address] = ''; - await saveAddressesInBox(); } catch (e) { print(e.toString()); @@ -155,4 +154,107 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { address = availableAddresses[random.nextInt(availableAddresses.length)].address; } + + Future _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async { + var hasAddrUse = true; + List addrs; + + if (addresses.isNotEmpty) { + addrs = addresses + .where((addr) => addr.isHidden == isHidden) + .toList(); + } else { + addrs = await _createNewAddresses( + isHidden + ? defaultChangeAddressesCount + : defaultReceiveAddressesCount, + startIndex: 0, + hd: hd, + isHidden: isHidden); + } + + while(hasAddrUse) { + final addr = addrs.last.address; + hasAddrUse = await _validateAddressUsing(addr); + + if (!hasAddrUse) { + break; + } + + final start = addrs.length; + final count = start + gap; + final batch = await _createNewAddresses( + count, + startIndex: start, + hd: hd, + isHidden: isHidden); + addrs.addAll(batch); + } + + if (addresses.length < addrs.length) { + _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, + {int startIndex = 0, bitcoin.HDWallet hd, bool isHidden = false}) async { + final list = []; + + for (var i = startIndex; i < count + startIndex; i++) { + final address = BitcoinAddressRecord( + getAddress(index: i, hd: hd), + index: i, + isHidden: isHidden); + list.add(address); + } + + return list; + } + + void _addAddresses(Iterable addresses) { + final addressesSet = this.addresses.toSet(); + addressesSet.addAll(addresses); + this.addresses.removeRange(0, this.addresses.length); + this.addresses.addAll(addressesSet); + } + + Future _validateAddressUsing(String address) async { + final sh = scriptHash(address, networkType: networkType); + final balance = await electrumClient.getBalance(sh); + return balance.isEmpty; + } } \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index eb07389af..3bc1f0607 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -15,7 +15,8 @@ class ElectrumWallletSnapshot { String mnemonic; List addresses; ElectrumBalance balance; - int accountIndex; + int regularAddressIndex; + int changeAddressIndex; Future load() async { try { @@ -30,10 +31,12 @@ class ElectrumWallletSnapshot { .toList(); balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0); - accountIndex = 0; + regularAddressIndex = 0; + changeAddressIndex = 0; try { - accountIndex = int.parse(data['account_index'] as String); + regularAddressIndex = int.parse(data['account_index'] as String); + changeAddressIndex = int.parse(data['change_address_index'] as String); } catch (_) {} } catch (e) { print(e); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 44e995f58..6f6ba3880 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -26,7 +26,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @required Box unspentCoinsInfo, List initialAddresses, ElectrumBalance initialBalance, - int accountIndex = 0}) + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0}) : super( mnemonic: mnemonic, password: password, @@ -37,8 +38,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance) { walletAddresses = LitecoinWalletAddresses( walletInfo, + electrumClient: electrumClient, initialAddresses: initialAddresses, - accountIndex: accountIndex, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, sideHd: bitcoin.HDWallet .fromSeed(mnemonicToSeedBytes(mnemonic), network: networkType) @@ -61,7 +64,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, - accountIndex: snp.accountIndex); + initialRegularAddressIndex: snp.regularAddressIndex, + initialChangeAddressIndex: snp.changeAddressIndex); } @override diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index e6658f10d..1a39b8452 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -1,4 +1,5 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; @@ -16,32 +17,23 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses LitecoinWalletAddressesBase( WalletInfo walletInfo, {@required List initialAddresses, - int accountIndex = 0, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0, + ElectrumClient electrumClient, @required bitcoin.HDWallet mainHd, @required bitcoin.HDWallet sideHd, - @required this.networkType}) + @required bitcoin.NetworkType networkType}) : super( walletInfo, initialAddresses: initialAddresses, - accountIndex: accountIndex, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, mainHd: mainHd, - sideHd: sideHd); - - bitcoin.NetworkType networkType; - + sideHd: sideHd, + electrumClient: electrumClient, + networkType: networkType); @override String getAddress({@required int index, @required bitcoin.HDWallet hd}) => generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); - - @override - Future generateAddresses() async { - if (addresses.length < 33) { - final addressesCount = 22 - addresses.length; - await generateNewAddresses(addressesCount, - hd: mainHd, startIndex: addresses.length); - await generateNewAddresses(11, - startIndex: 0, hd: sideHd, isHidden: true); - } - } } \ No newline at end of file diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 109bbf1e3..de5f14af6 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -57,7 +57,7 @@ class CWBitcoin extends Bitcoin { @override Future nextAddress(Object wallet) { final bitcoinWallet = wallet as ElectrumWallet; - bitcoinWallet.walletAddresses.nextAddress(); + bitcoinWallet.walletAddresses.nextReceiveAddress(); } @override