Cw-343-a-list-of-previously-used-addresses (#1248)

* add used addresses list

* generate new address button

* fix wallet type issue

* fix addresses button title

* update selectButton

* show all wallet addresses

* add tx amount and balance

* fix ui

* remove cashAddr format

* fix generating new address issue

* disable autogenerating

* fix cashAddr format

* minor fix

* add search bar

* Update address_cell.dart

* fix merge conflict

* address labeling feature

* review fixes

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Serhii 2024-01-23 07:15:24 +02:00 committed by GitHub
parent d664f0a16f
commit 9754d67601
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 664 additions and 330 deletions

View file

@ -1,40 +1,70 @@
import 'dart:convert'; import 'dart:convert';
import 'package:bitbox/bitbox.dart' as bitbox;
class BitcoinAddressRecord { class BitcoinAddressRecord {
BitcoinAddressRecord(this.address, BitcoinAddressRecord(
{required this.index, this.isHidden = false, bool isUsed = false}) this.address, {
: _isUsed = isUsed; required this.index,
this.isHidden = false,
int txCount = 0,
int balance = 0,
String name = '',
bool isUsed = false,
}) : _txCount = txCount,
_balance = balance,
_name = name,
_isUsed = isUsed;
factory BitcoinAddressRecord.fromJSON(String jsonSource) { factory BitcoinAddressRecord.fromJSON(String jsonSource) {
final decoded = json.decode(jsonSource) as Map; final decoded = json.decode(jsonSource) as Map;
return BitcoinAddressRecord( return BitcoinAddressRecord(decoded['address'] as String,
decoded['address'] as String, index: decoded['index'] as int,
index: decoded['index'] as int, isHidden: decoded['isHidden'] as bool? ?? false,
isHidden: decoded['isHidden'] as bool? ?? false, isUsed: decoded['isUsed'] as bool? ?? false,
isUsed: decoded['isUsed'] as bool? ?? false); txCount: decoded['txCount'] as int? ?? 0,
name: decoded['name'] as String? ?? '',
balance: decoded['balance'] as int? ?? 0);
} }
@override
bool operator ==(Object o) =>
o is BitcoinAddressRecord && address == o.address;
final String address; final String address;
final bool isHidden; final bool isHidden;
final int index; final int index;
int _txCount;
int _balance;
String _name;
bool _isUsed;
int get txCount => _txCount;
String get name => _name;
int get balance => _balance;
set txCount(int value) => _txCount = value;
set balance(int value) => _balance = value;
bool get isUsed => _isUsed; bool get isUsed => _isUsed;
void setAsUsed() => _isUsed = true;
void setNewName(String label) => _name = label;
@override
bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address;
@override @override
int get hashCode => address.hashCode; int get hashCode => address.hashCode;
bool _isUsed; String get cashAddr => bitbox.Address.toCashAddress(address);
void setAsUsed() => _isUsed = true; String toJSON() => json.encode({
String toJSON() =>
json.encode({
'address': address, 'address': address,
'index': index, 'index': index,
'isHidden': isHidden, 'isHidden': isHidden,
'isUsed': isUsed}); 'txCount': txCount,
'name': name,
'isUsed': isUsed,
'balance': balance,
});
} }

View file

@ -47,6 +47,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType)
.derivePath("m/0'/1"), .derivePath("m/0'/1"),
networkType: networkType); networkType: networkType);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
});
} }
static Future<BitcoinWallet> create({ static Future<BitcoinWallet> create({

View file

@ -63,6 +63,7 @@ abstract class ElectrumWalletBase
_password = password, _password = password,
_feeRates = <int>[], _feeRates = <int>[],
_isTransactionUpdating = false, _isTransactionUpdating = false,
isEnabledAutoGenerateSubaddress = true,
unspentCoins = [], unspentCoins = [],
_scripthashesUpdateSubject = {}, _scripthashesUpdateSubject = {},
balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null
@ -87,6 +88,10 @@ abstract class ElectrumWalletBase
final bitcoin.HDWallet hd; final bitcoin.HDWallet hd;
final String mnemonic; final String mnemonic;
@override
@observable
bool isEnabledAutoGenerateSubaddress;
late ElectrumClient electrumClient; late ElectrumClient electrumClient;
Box<UnspentCoinsInfo> unspentCoinsInfo; Box<UnspentCoinsInfo> unspentCoinsInfo;
@ -583,38 +588,66 @@ abstract class ElectrumWalletBase
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async { Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
final addressHashes = <String, BitcoinAddressRecord>{}; final addressHashes = <String, BitcoinAddressRecord>{};
final normalizedHistories = <Map<String, dynamic>>[]; final normalizedHistories = <Map<String, dynamic>>[];
final newTxCounts = <String, int>{};
walletAddresses.addresses.forEach((addressRecord) { walletAddresses.addresses.forEach((addressRecord) {
final sh = scriptHash(addressRecord.address, networkType: networkType); final sh = scriptHash(addressRecord.address, networkType: networkType);
addressHashes[sh] = addressRecord; addressHashes[sh] = addressRecord;
newTxCounts[sh] = 0;
}); });
final histories = addressHashes.keys.map((scriptHash) =>
electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history})); try {
final historyResults = await Future.wait(histories); final histories = addressHashes.keys.map((scriptHash) =>
historyResults.forEach((history) { electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history}));
history.entries.forEach((historyItem) { final historyResults = await Future.wait(histories);
if (historyItem.value.isNotEmpty) {
final address = addressHashes[historyItem.key];
address?.setAsUsed();
normalizedHistories.addAll(historyItem.value); historyResults.forEach((history) {
} history.entries.forEach((historyItem) {
if (historyItem.value.isNotEmpty) {
final address = addressHashes[historyItem.key];
address?.setAsUsed();
newTxCounts[historyItem.key] = historyItem.value.length;
normalizedHistories.addAll(historyItem.value);
}
});
}); });
});
final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) { for (var sh in addressHashes.keys) {
try { var balanceData = await electrumClient.getBalance(sh);
return fetchTransactionInfo( var addressRecord = addressHashes[sh];
hash: transaction['tx_hash'] as String, height: transaction['height'] as int); if (addressRecord != null) {
} catch (_) { addressRecord.balance = balanceData['confirmed'] as int? ?? 0;
return Future.value(null); }
} }
}));
return historiesWithDetails
.fold<Map<String, ElectrumTransactionInfo>>(<String, ElectrumTransactionInfo>{}, (acc, tx) { addressHashes.forEach((sh, addressRecord) {
if (tx == null) { addressRecord.txCount = newTxCounts[sh] ?? 0;
});
final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) {
try {
return fetchTransactionInfo(
hash: transaction['tx_hash'] as String, height: transaction['height'] as int);
} catch (_) {
return Future.value(null);
}
}));
return historiesWithDetails.fold<Map<String, ElectrumTransactionInfo>>(
<String, ElectrumTransactionInfo>{}, (acc, tx) {
if (tx == null) {
return acc;
}
acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
return acc; return acc;
} });
acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; } catch (e) {
return acc; print(e.toString());
}); return {};
}
} }
Future<void> updateTransactions() async { Future<void> updateTransactions() async {

View file

@ -1,5 +1,5 @@
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum.dart';
import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/script_hash.dart';
@ -10,8 +10,7 @@ import 'package:mobx/mobx.dart';
part 'electrum_wallet_addresses.g.dart'; part 'electrum_wallet_addresses.g.dart';
class ElectrumWalletAddresses = ElectrumWalletAddressesBase class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
with _$ElectrumWalletAddresses;
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
ElectrumWalletAddressesBase(WalletInfo walletInfo, ElectrumWalletAddressesBase(WalletInfo walletInfo,
@ -22,19 +21,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
int initialRegularAddressIndex = 0, int initialRegularAddressIndex = 0,
int initialChangeAddressIndex = 0}) int initialChangeAddressIndex = 0})
: addresses = ObservableList<BitcoinAddressRecord>.of( : addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
(initialAddresses ?? []).toSet()), receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
receiveAddresses = ObservableList<BitcoinAddressRecord>.of(
(initialAddresses ?? [])
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
.toSet()), .toSet()),
changeAddresses = ObservableList<BitcoinAddressRecord>.of( changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
(initialAddresses ?? [])
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
.toSet()), .toSet()),
currentReceiveAddressIndex = initialRegularAddressIndex, currentReceiveAddressIndex = initialRegularAddressIndex,
currentChangeAddressIndex = initialChangeAddressIndex, currentChangeAddressIndex = initialChangeAddressIndex,
super(walletInfo); super(walletInfo);
static const defaultReceiveAddressesCount = 22; static const defaultReceiveAddressesCount = 22;
static const defaultChangeAddressesCount = 17; static const defaultChangeAddressesCount = 17;
@ -42,6 +38,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
static String toCashAddr(String address) => bitbox.Address.toCashAddress(address); static String toCashAddr(String address) => bitbox.Address.toCashAddress(address);
static String toLegacy(String address) => bitbox.Address.toLegacyAddress(address);
final ObservableList<BitcoinAddressRecord> addresses; final ObservableList<BitcoinAddressRecord> addresses;
final ObservableList<BitcoinAddressRecord> receiveAddresses; final ObservableList<BitcoinAddressRecord> receiveAddresses;
final ObservableList<BitcoinAddressRecord> changeAddresses; final ObservableList<BitcoinAddressRecord> changeAddresses;
@ -53,41 +51,67 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override @override
@computed @computed
String get address { String get address {
if (receiveAddresses.isEmpty) { if (isEnabledAutoGenerateSubaddress) {
final address = generateNewAddress().address; if (receiveAddresses.isEmpty) {
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(address) : address; final newAddress = generateNewAddress().address;
} return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(newAddress) : newAddress;
final receiveAddress = receiveAddresses.first.address; }
final receiveAddress = receiveAddresses.first.address;
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress; return walletInfo.type == WalletType.bitcoinCash
? toCashAddr(receiveAddress)
: receiveAddress;
} else {
final receiveAddress = (receiveAddresses.first.address != addresses.first.address &&
previousAddressRecord != null)
? previousAddressRecord!.address
: addresses.first.address;
return walletInfo.type == WalletType.bitcoinCash
? toCashAddr(receiveAddress)
: receiveAddress;
}
}
@observable
bool isEnabledAutoGenerateSubaddress = true;
@override
set address(String addr) {
if (addr.startsWith('bitcoincash:')) {
addr = toLegacy(addr);
}
final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == addr);
previousAddressRecord = addressRecord;
receiveAddresses.remove(addressRecord);
receiveAddresses.insert(0, addressRecord);
} }
@override @override
String get primaryAddress => getAddress(index: 0, hd: mainHd); String get primaryAddress => getAddress(index: 0, hd: mainHd);
@override
set address(String addr) => null;
int currentReceiveAddressIndex; int currentReceiveAddressIndex;
int currentChangeAddressIndex; int currentChangeAddressIndex;
@computed @observable
int get totalCountOfReceiveAddresses => BitcoinAddressRecord? previousAddressRecord;
addresses.fold(0, (acc, addressRecord) {
if (!addressRecord.isHidden) {
return acc + 1;
}
return acc;
});
@computed @computed
int get totalCountOfChangeAddresses => int get totalCountOfReceiveAddresses => addresses.fold(0, (acc, addressRecord) {
addresses.fold(0, (acc, addressRecord) { if (!addressRecord.isHidden) {
if (addressRecord.isHidden) { return acc + 1;
return acc + 1; }
} return acc;
return acc; });
});
@computed
int get totalCountOfChangeAddresses => addresses.fold(0, (acc, addressRecord) {
if (addressRecord.isHidden) {
return acc + 1;
}
return acc;
});
Future<void> discoverAddresses() async { Future<void> discoverAddresses() async {
await _discoverAddresses(mainHd, false); await _discoverAddresses(mainHd, false);
@ -117,11 +141,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
if (changeAddresses.isEmpty) { if (changeAddresses.isEmpty) {
final newAddresses = await _createNewAddresses(gap, final newAddresses = await _createNewAddresses(gap,
hd: sideHd, hd: sideHd,
startIndex: totalCountOfChangeAddresses > 0 startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0,
? totalCountOfChangeAddresses - 1 isHidden: true);
: 0,
isHidden: true);
_addAddresses(newAddresses); _addAddresses(newAddresses);
} }
@ -135,14 +157,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return address; return address;
} }
BitcoinAddressRecord generateNewAddress( BitcoinAddressRecord generateNewAddress({bitcoin.HDWallet? hd, String? label}) {
{bitcoin.HDWallet? hd, bool isHidden = false}) { final isHidden = hd == sideHd;
currentReceiveAddressIndex += 1;
// FIX-ME: Check logic for whichi HD should be used here ??? final newAddressIndex = addresses.fold(
final address = BitcoinAddressRecord( 0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc);
getAddress(index: currentReceiveAddressIndex, hd: hd ?? sideHd),
index: currentReceiveAddressIndex, final address = BitcoinAddressRecord(getAddress(index: newAddressIndex, hd: hd ?? sideHd),
isHidden: isHidden); index: newAddressIndex, isHidden: isHidden, name: label ?? '');
addresses.add(address); addresses.add(address);
return address; return address;
} }
@ -160,20 +182,32 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
} }
} }
@action
void updateAddress(String address, String label) {
if (address.startsWith('bitcoincash:')) {
address = toLegacy(address);
}
final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == address);
addressRecord.setNewName(label);
final index = addresses.indexOf(addressRecord);
addresses.remove(addressRecord);
addresses.insert(index, addressRecord);
}
@action @action
void updateReceiveAddresses() { void updateReceiveAddresses() {
receiveAddresses.removeRange(0, receiveAddresses.length); receiveAddresses.removeRange(0, receiveAddresses.length);
final newAdresses = addresses final newAddresses =
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
receiveAddresses.addAll(newAdresses); receiveAddresses.addAll(newAddresses);
} }
@action @action
void updateChangeAddresses() { void updateChangeAddresses() {
changeAddresses.removeRange(0, changeAddresses.length); changeAddresses.removeRange(0, changeAddresses.length);
final newAdresses = addresses final newAddresses =
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed); addresses.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed);
changeAddresses.addAll(newAdresses); changeAddresses.addAll(newAddresses);
} }
Future<void> _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async { Future<void> _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async {
@ -181,20 +215,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
List<BitcoinAddressRecord> addrs; List<BitcoinAddressRecord> addrs;
if (addresses.isNotEmpty) { if (addresses.isNotEmpty) {
addrs = addresses addrs = addresses.where((addr) => addr.isHidden == isHidden).toList();
.where((addr) => addr.isHidden == isHidden)
.toList();
} else { } else {
addrs = await _createNewAddresses( addrs = await _createNewAddresses(
isHidden isHidden ? defaultChangeAddressesCount : defaultReceiveAddressesCount,
? defaultChangeAddressesCount
: defaultReceiveAddressesCount,
startIndex: 0, startIndex: 0,
hd: hd, hd: hd,
isHidden: isHidden); isHidden: isHidden);
} }
while(hasAddrUse) { while (hasAddrUse) {
final addr = addrs.last.address; final addr = addrs.last.address;
hasAddrUse = await _hasAddressUsed(addr); hasAddrUse = await _hasAddressUsed(addr);
@ -204,11 +234,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final start = addrs.length; final start = addrs.length;
final count = start + gap; final count = start + gap;
final batch = await _createNewAddresses( final batch = await _createNewAddresses(count, startIndex: start, hd: hd, isHidden: isHidden);
count,
startIndex: start,
hd: hd,
isHidden: isHidden);
addrs.addAll(batch); addrs.addAll(batch);
} }
@ -232,21 +258,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
if (countOfReceiveAddresses < defaultReceiveAddressesCount) { if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
final newAddresses = await _createNewAddresses( final newAddresses = await _createNewAddresses(addressesCount,
addressesCount, startIndex: countOfReceiveAddresses, hd: mainHd, isHidden: false);
startIndex: countOfReceiveAddresses,
hd: mainHd,
isHidden: false);
addresses.addAll(newAddresses); addresses.addAll(newAddresses);
} }
if (countOfHiddenAddresses < defaultChangeAddressesCount) { if (countOfHiddenAddresses < defaultChangeAddressesCount) {
final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
final newAddresses = await _createNewAddresses( final newAddresses = await _createNewAddresses(addressesCount,
addressesCount, startIndex: countOfHiddenAddresses, hd: sideHd, isHidden: true);
startIndex: countOfHiddenAddresses,
hd: sideHd,
isHidden: true);
addresses.addAll(newAddresses); addresses.addAll(newAddresses);
} }
} }
@ -256,10 +276,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final list = <BitcoinAddressRecord>[]; final list = <BitcoinAddressRecord>[];
for (var i = startIndex; i < count + startIndex; i++) { for (var i = startIndex; i < count + startIndex; i++) {
final address = BitcoinAddressRecord( final address =
getAddress(index: i, hd: hd), BitcoinAddressRecord(getAddress(index: i, hd: hd), index: i, isHidden: isHidden);
index: i,
isHidden: isHidden);
list.add(address); list.add(address);
} }

View file

@ -51,6 +51,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
.fromSeed(seedBytes, network: networkType) .fromSeed(seedBytes, network: networkType)
.derivePath("m/0'/1"), .derivePath("m/0'/1"),
networkType: networkType,); networkType: networkType,);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
});
} }
static Future<LitecoinWallet> create({ static Future<LitecoinWallet> create({

View file

@ -57,6 +57,9 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
sideHd: bitcoin.HDWallet.fromSeed(seedBytes) sideHd: bitcoin.HDWallet.fromSeed(seedBytes)
.derivePath("m/44'/145'/0'/1"), .derivePath("m/44'/145'/0'/1"),
networkType: networkType); networkType: networkType);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
});
} }

View file

@ -81,6 +81,7 @@ class AmountConverter {
return _moneroAmountToString(amount); return _moneroAmountToString(amount);
case CryptoCurrency.btc: case CryptoCurrency.btc:
case CryptoCurrency.bch: case CryptoCurrency.bch:
case CryptoCurrency.ltc:
return _bitcoinAmountToString(amount); return _bitcoinAmountToString(amount);
case CryptoCurrency.xhv: case CryptoCurrency.xhv:
case CryptoCurrency.xag: case CryptoCurrency.xag:

View file

@ -63,9 +63,17 @@ class CWBitcoin extends Bitcoin {
} }
@override @override
Future<void> generateNewAddress(Object wallet) async { Future<void> generateNewAddress(Object wallet, String label) async {
final bitcoinWallet = wallet as ElectrumWallet; final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.walletAddresses.generateNewAddress(); await bitcoinWallet.walletAddresses.generateNewAddress(label: label);
await wallet.save();
}
@override
Future<void> updateAddress(Object wallet,String address, String label) async {
final bitcoinWallet = wallet as ElectrumWallet;
bitcoinWallet.walletAddresses.updateAddress(address, label);
await wallet.save();
} }
@override @override
@ -99,6 +107,21 @@ class CWBitcoin extends Bitcoin {
.toList(); .toList();
} }
@override
@computed
List<ElectrumSubAddress> getSubAddresses(Object wallet) {
final electrumWallet = wallet as ElectrumWallet;
return electrumWallet.walletAddresses.addresses
.map((BitcoinAddressRecord addr) => ElectrumSubAddress(
id: addr.index,
name: addr.name,
address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address,
txCount: addr.txCount,
balance: addr.balance,
isChange: addr.isHidden))
.toList();
}
@override @override
String getAddress(Object wallet) { String getAddress(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet; final bitcoinWallet = wallet as ElectrumWallet;

View file

@ -68,7 +68,8 @@ void startCurrentWalletChangeReaction(
.get<SharedPreferences>() .get<SharedPreferences>()
.setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type));
if (wallet.type == WalletType.monero) { if (wallet.type == WalletType.monero || wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash ) {
_setAutoGenerateSubaddressStatus(wallet, settingsStore); _setAutoGenerateSubaddressStatus(wallet, settingsStore);
} }

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
@ -15,6 +16,7 @@ import 'package:cake_wallet/utils/share_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_widget.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_widget.dart';
@ -155,63 +157,27 @@ class AddressPage extends BasePage {
amountController: _amountController, amountController: _amountController,
isLight: dashboardViewModel.settingsStore.currentTheme.type == isLight: dashboardViewModel.settingsStore.currentTheme.type ==
ThemeType.light))), ThemeType.light))),
SizedBox(height: 16),
Observer(builder: (_) { Observer(builder: (_) {
if (addressListViewModel.hasAddressList) { if (addressListViewModel.hasAddressList) {
return GestureDetector( return SelectButton(
onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled text: addressListViewModel.buttonTitle,
onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled &&
(WalletType.monero == addressListViewModel.wallet.type ||
WalletType.haven == addressListViewModel.wallet.type)
? await showPopUp<void>( ? await showPopUp<void>(
context: context, builder: (_) => getIt.get<MoneroAccountListPage>()) context: context,
builder: (_) => getIt.get<MoneroAccountListPage>())
: Navigator.of(context).pushNamed(Routes.receive), : Navigator.of(context).pushNamed(Routes.receive),
child: Container( textColor: Theme.of(context).extension<SyncIndicatorTheme>()!.textColor,
height: 50, color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
padding: EdgeInsets.only(left: 24, right: 12), borderColor: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
alignment: Alignment.center, arrowColor: Theme.of(context).extension<SyncIndicatorTheme>()!.textColor,
decoration: BoxDecoration( textSize: 14,
borderRadius: BorderRadius.all(Radius.circular(25)), height: 50,
border: Border.all(
color:
Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
width: 1),
color: Theme.of(context)
.extension<SyncIndicatorTheme>()!
.syncedBackgroundColor),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Observer(
builder: (_) {
String label = addressListViewModel.hasAccounts
? S.of(context).accounts_subaddresses
: S.of(context).addresses;
if (dashboardViewModel.isAutoGenerateSubaddressesEnabled) {
label = addressListViewModel.hasAccounts
? S.of(context).accounts
: S.of(context).account;
}
return Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.extension<SyncIndicatorTheme>()!
.textColor),
);
},
),
Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).extension<SyncIndicatorTheme>()!.textColor,
)
],
),
),
); );
} else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled || } else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled ||
addressListViewModel.showElectrumAddressDisclaimer) { addressListViewModel.isElectrumWallet) {
return Text(S.of(context).electrum_address_disclaimer, return Text(S.of(context).electrum_address_disclaimer,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(

View file

@ -11,29 +11,37 @@ class SelectButton extends StatelessWidget {
this.isSelected = false, this.isSelected = false,
this.showTrailingIcon = true, this.showTrailingIcon = true,
this.height = 60, this.height = 60,
this.textSize = 18,
this.color,
this.textColor,
this.arrowColor,
this.borderColor,
}); });
final Image? image; final Image? image;
final String text; final String text;
final double textSize;
final bool isSelected; final bool isSelected;
final VoidCallback onTap; final VoidCallback onTap;
final bool showTrailingIcon; final bool showTrailingIcon;
final double height; final double height;
final Color? color;
final Color? textColor;
final Color? arrowColor;
final Color? borderColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final color = isSelected final backgroundColor = color ?? (isSelected ? Colors.green : Theme.of(context).cardColor);
? Colors.green final effectiveTextColor = textColor ?? (isSelected
: Theme.of(context).cardColor;
final textColor = isSelected
? Theme.of(context).extension<WalletListTheme>()!.restoreWalletButtonTextColor ? Theme.of(context).extension<WalletListTheme>()!.restoreWalletButtonTextColor
: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor; : Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor);
final arrowColor = isSelected final effectiveArrowColor = arrowColor ?? (isSelected
? Theme.of(context).extension<WalletListTheme>()!.restoreWalletButtonTextColor ? Theme.of(context).extension<WalletListTheme>()!.restoreWalletButtonTextColor
: Theme.of(context).extension<FilterTheme>()!.titlesColor; : Theme.of(context).extension<FilterTheme>()!.titlesColor);
final selectArrowImage = Image.asset('assets/images/select_arrow.png', final selectArrowImage = Image.asset('assets/images/select_arrow.png',
color: arrowColor); color: effectiveArrowColor);
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
@ -44,7 +52,9 @@ class SelectButton extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(30)), borderRadius: BorderRadius.all(Radius.circular(30)),
color: color color: backgroundColor,
border: borderColor != null ? Border.all(color: borderColor!) : null,
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
@ -63,9 +73,9 @@ class SelectButton extends StatelessWidget {
child: Text( child: Text(
text, text,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: textSize,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: textColor color: effectiveTextColor,
), ),
), ),
) )

View file

@ -49,7 +49,7 @@ class ReceivePage extends BasePage {
bool get gradientBackground => true; bool get gradientBackground => true;
@override @override
bool get resizeToAvoidBottomInset => false; bool get resizeToAvoidBottomInset => true;
final FocusNode _cryptoAmountFocus; final FocusNode _cryptoAmountFocus;
@ -99,10 +99,11 @@ class ReceivePage extends BasePage {
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
final isElectrumWallet = addressListViewModel.isElectrumWallet;
return (addressListViewModel.type == WalletType.monero || return (addressListViewModel.type == WalletType.monero ||
addressListViewModel.type == WalletType.haven || addressListViewModel.type == WalletType.haven ||
addressListViewModel.type == WalletType.nano || addressListViewModel.type == WalletType.nano ||
addressListViewModel.type == WalletType.banano) isElectrumWallet)
? KeyboardActions( ? KeyboardActions(
config: KeyboardActionsConfig( config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS, keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
@ -140,7 +141,9 @@ class ReceivePage extends BasePage {
if (item is WalletAccountListHeader) { if (item is WalletAccountListHeader) {
cell = HeaderTile( cell = HeaderTile(
onTap: () async { showTrailingButton: true,
walletAddressListViewModel: addressListViewModel,
trailingButtonTap: () async {
if (addressListViewModel.type == WalletType.monero || if (addressListViewModel.type == WalletType.monero ||
addressListViewModel.type == WalletType.haven) { addressListViewModel.type == WalletType.haven) {
await showPopUp<void>( await showPopUp<void>(
@ -153,7 +156,7 @@ class ReceivePage extends BasePage {
} }
}, },
title: S.of(context).accounts, title: S.of(context).accounts,
icon: Icon( trailingIcon: Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
size: 14, size: 14,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor, color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
@ -161,16 +164,21 @@ class ReceivePage extends BasePage {
} }
if (item is WalletAddressListHeader) { if (item is WalletAddressListHeader) {
cell = HeaderTile( cell = HeaderTile(
onTap: () => title: S.of(context).addresses,
Navigator.of(context).pushNamed(Routes.newSubaddress), walletAddressListViewModel: addressListViewModel,
title: S.of(context).addresses, showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled,
icon: Icon( showSearchButton: true,
Icons.add, trailingButtonTap: () =>
size: 20, Navigator.of(context).pushNamed(Routes.newSubaddress),
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor, trailingIcon: Icon(
)); Icons.add,
} size: 20,
color: Theme.of(context)
.extension<ReceivePageTheme>()!
.iconsColor,
));
}
if (item is WalletAddressListItem) { if (item is WalletAddressListItem) {
cell = Observer(builder: (_) { cell = Observer(builder: (_) {
@ -185,6 +193,7 @@ class ReceivePage extends BasePage {
return AddressCell.fromItem(item, return AddressCell.fromItem(item,
isCurrent: isCurrent, isCurrent: isCurrent,
hasBalance: addressListViewModel.isElectrumWallet,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
textColor: textColor, textColor: textColor,
onTap: (_) => addressListViewModel.setAddress(item), onTap: (_) => addressListViewModel.setAddress(item),

View file

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
class AddressCell extends StatelessWidget { class AddressCell extends StatelessWidget {
AddressCell( AddressCell(
@ -12,13 +13,18 @@ class AddressCell extends StatelessWidget {
required this.backgroundColor, required this.backgroundColor,
required this.textColor, required this.textColor,
this.onTap, this.onTap,
this.onEdit}); this.onEdit,
this.txCount,
this.balance,
this.isChange = false,
this.hasBalance = false});
factory AddressCell.fromItem(WalletAddressListItem item, factory AddressCell.fromItem(WalletAddressListItem item,
{required bool isCurrent, {required bool isCurrent,
required Color backgroundColor, required Color backgroundColor,
required Color textColor, required Color textColor,
Function(String)? onTap, Function(String)? onTap,
bool hasBalance = false,
Function()? onEdit}) => Function()? onEdit}) =>
AddressCell( AddressCell(
address: item.address, address: item.address,
@ -28,7 +34,11 @@ class AddressCell extends StatelessWidget {
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
textColor: textColor, textColor: textColor,
onTap: onTap, onTap: onTap,
onEdit: onEdit); onEdit: onEdit,
txCount: item.txCount,
balance: item.balance,
isChange: item.isChange,
hasBalance: hasBalance);
final String address; final String address;
final String name; final String name;
@ -38,17 +48,22 @@ class AddressCell extends StatelessWidget {
final Color textColor; final Color textColor;
final Function(String)? onTap; final Function(String)? onTap;
final Function()? onEdit; final Function()? onEdit;
final int? txCount;
final String? balance;
final bool isChange;
final bool hasBalance;
String get label { static const int addressPreviewLength = 8;
if (name.isEmpty){
if(address.length<=16){ String get formattedAddress {
return address; final formatIfCashAddr = address.replaceAll('bitcoincash:', '');
}else{
return address.substring(0,8)+'...'+ if (formatIfCashAddr.length <= (name.isNotEmpty ? 16 : 43)) {
address.substring(address.length-8,address.length); return formatIfCashAddr;
} } else {
}else{ return formatIfCashAddr.substring(0, addressPreviewLength) +
return name; '...' +
formatIfCashAddr.substring(formatIfCashAddr.length - addressPreviewLength, formatIfCashAddr.length);
} }
} }
@ -59,41 +74,114 @@ class AddressCell extends StatelessWidget {
child: Container( child: Container(
width: double.infinity, width: double.infinity,
color: backgroundColor, color: backgroundColor,
padding: EdgeInsets.only(left: 24, right: 24, top: 28, bottom: 28), padding: EdgeInsets.only(left: 24, right: 24, top: 20, bottom: 20),
child: Text( child: Row(
label, children: [
maxLines: 1, Expanded(
overflow: TextOverflow.ellipsis, child: Column(
style: TextStyle( children: [
fontSize: 14, Row(
color: textColor, mainAxisAlignment: MainAxisAlignment.center,
), mainAxisSize: MainAxisSize.max,
children: [
if (isChange)
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Container(
height: 20,
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8.5)),
color: textColor),
alignment: Alignment.center,
child: Text(
S.of(context).unspent_change,
style: TextStyle(
color: backgroundColor,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
),
if (name.isNotEmpty)
Text(
'$name - ',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: textColor,
),
),
AutoSizeText(
formattedAddress,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: isChange ? 10 : 14,
color: textColor,
),
),
],
),
if (hasBalance)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Text(
'Balance: $balance',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: textColor,
),
),
Text(
'${S.of(context).transactions.toLowerCase()}: $txCount',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: textColor,
),
),
],
),
),
],
),
),
],
), ),
)); ));
return Semantics( return onEdit == null
label: S.of(context).slidable, ? cell
selected: isCurrent, : Semantics(
enabled: !isCurrent, label: S.of(context).slidable,
child: Slidable( selected: isCurrent,
key: Key(address), enabled: !isCurrent,
startActionPane: _actionPane(context), child: Slidable(
endActionPane: _actionPane(context), key: Key(address),
child: cell, startActionPane: _actionPane(context),
), endActionPane: _actionPane(context),
); child: cell,
),
);
} }
ActionPane _actionPane(BuildContext context) => ActionPane( ActionPane _actionPane(BuildContext context) => ActionPane(
motion: const ScrollMotion(), motion: const ScrollMotion(),
extentRatio: 0.3, extentRatio: 0.3,
children: [ children: [
SlidableAction( SlidableAction(
onPressed: (_) => onEdit?.call(), onPressed: (_) => onEdit?.call(),
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
foregroundColor: Colors.white, foregroundColor: Colors.white,
icon: Icons.edit, icon: Icons.edit,
label: S.of(context).edit, label: S.of(context).edit,
), ),
], ],
); );
} }

View file

@ -1,50 +1,114 @@
import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:flutter/material.dart';
class HeaderTile extends StatelessWidget { class HeaderTile extends StatefulWidget {
HeaderTile({ HeaderTile({
required this.onTap,
required this.title, required this.title,
required this.icon required this.walletAddressListViewModel,
this.showSearchButton = false,
this.showTrailingButton = false,
this.trailingButtonTap,
this.trailingIcon,
}); });
final VoidCallback onTap;
final String title; final String title;
final Icon icon; final WalletAddressListViewModel walletAddressListViewModel;
final bool showSearchButton;
final bool showTrailingButton;
final VoidCallback? trailingButtonTap;
final Icon? trailingIcon;
@override
_HeaderTileState createState() => _HeaderTileState();
}
class _HeaderTileState extends State<HeaderTile> {
bool _isSearchActive = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( final searchIcon = Image.asset("assets/images/search_icon.png",
onTap: onTap, color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor);
child: Container(
padding: EdgeInsets.only( return Container(
left: 24, padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
right: 24, color: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor,
top: 24, child: Row(
bottom: 24 mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: <Widget>[
color: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor, _isSearchActive
child: Row( ? Expanded(
mainAxisSize: MainAxisSize.max, child: TextField(
mainAxisAlignment: MainAxisAlignment.spaceBetween, onChanged: (value) => widget.walletAddressListViewModel.updateSearchText(value),
children: <Widget>[ cursorColor: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor,
Text( cursorWidth: 0.5,
title, decoration: InputDecoration(
style: TextStyle( hintText: '${S.of(context).search}...',
fontSize: 18, isDense: true,
fontWeight: FontWeight.w600, contentPadding: EdgeInsets.zero,
color: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor), hintStyle: TextStyle(
), fontSize: 16,
Container( fontWeight: FontWeight.w600,
height: 32, color: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor),
width: 32, border: UnderlineInputBorder(
decoration: BoxDecoration( borderSide: BorderSide(color: Theme.of(context).dividerColor),
shape: BoxShape.circle, ),
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsBackgroundColor), focusedBorder: UnderlineInputBorder(
child: icon, borderSide: BorderSide(color: Theme.of(context).dividerColor),
) ),
], enabledBorder: UnderlineInputBorder(
), borderSide: BorderSide(color: Theme.of(context).dividerColor),
),
),
autofocus: true,
),
)
: Text(
widget.title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor),
),
Row(
children: [
if (widget.showSearchButton)
GestureDetector(
onTap: () {
setState(() {
_isSearchActive = !_isSearchActive;
widget.walletAddressListViewModel.updateSearchText('');
});
},
child: Container(
height: 32,
width: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context)
.extension<ReceivePageTheme>()!
.iconsBackgroundColor),
child: searchIcon,
)),
const SizedBox(width: 8),
if (widget.showTrailingButton)
GestureDetector(
onTap: widget.trailingButtonTap,
child: Container(
height: 32,
width: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color:
Theme.of(context).extension<ReceivePageTheme>()!.iconsBackgroundColor),
child: widget.trailingIcon,
),
),
],
),
],
), ),
); );
} }

View file

@ -1,3 +1,4 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/themes/extensions/qr_code_theme.dart'; import 'package:cake_wallet/themes/extensions/qr_code_theme.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
@ -6,6 +7,7 @@ import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dar
import 'package:cake_wallet/utils/brightness_util.dart'; import 'package:cake_wallet/utils/brightness_util.dart';
import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
@ -144,9 +146,10 @@ class QRWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Text( child: AutoSizeText(
addressListViewModel.address.address, addressListViewModel.address.address,
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: addressListViewModel.wallet.type == WalletType.monero ? 2 : 1,
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,

View file

@ -38,7 +38,11 @@ abstract class PrivacySettingsViewModelBase with Store {
} }
} }
bool get isAutoGenerateSubaddressesVisible => _wallet.type == WalletType.monero; bool get isAutoGenerateSubaddressesVisible =>
_wallet.type == WalletType.monero ||
_wallet.type == WalletType.bitcoin ||
_wallet.type == WalletType.litecoin ||
_wallet.type == WalletType.bitcoinCash;
@computed @computed
bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress; bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress;

View file

@ -1,6 +1,5 @@
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:flutter/foundation.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/monero/monero.dart';
@ -33,7 +32,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
state = AddressEditOrCreateStateInitial(), state = AddressEditOrCreateStateInitial(),
label = item?.name ?? '', label = item?.name ?? '',
_item = item, _item = item,
_wallet = wallet; _wallet = wallet;
@observable @observable
AddressEditOrCreateState state; AddressEditOrCreateState state;
@ -46,6 +45,10 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
final WalletAddressListItem? _item; final WalletAddressListItem? _item;
final WalletBase _wallet; final WalletBase _wallet;
bool get isElectrum => _wallet.type == WalletType.bitcoin ||
_wallet.type == WalletType.bitcoinCash ||
_wallet.type == WalletType.litecoin;
Future<void> save() async { Future<void> save() async {
try { try {
state = AddressIsSaving(); state = AddressIsSaving();
@ -65,12 +68,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
Future<void> _createNew() async { Future<void> _createNew() async {
final wallet = _wallet; final wallet = _wallet;
if (wallet.type == WalletType.bitcoin if (isElectrum) await bitcoin!.generateNewAddress(wallet, label);
|| wallet.type == WalletType.litecoin
|| wallet.type == WalletType.bitcoinCash) {
await bitcoin!.generateNewAddress(wallet);
await wallet.save();
}
if (wallet.type == WalletType.monero) { if (wallet.type == WalletType.monero) {
await monero await monero
@ -96,10 +94,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
Future<void> _update() async { Future<void> _update() async {
final wallet = _wallet; final wallet = _wallet;
/*if (wallet is BitcoinWallet) { if (isElectrum) await bitcoin!.updateAddress(wallet, _item!.address, label);
await wallet.walletAddresses.updateAddress(_item.address as String);
await wallet.save();
}*/
final index = _item?.id; final index = _item?.id;
if (index != null) { if (index != null) {
if (wallet.type == WalletType.monero) { if (wallet.type == WalletType.monero) {

View file

@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/utils/list_item.dart';
class WalletAddressListItem extends ListItem { class WalletAddressListItem extends ListItem {
@ -6,13 +5,19 @@ class WalletAddressListItem extends ListItem {
required this.address, required this.address,
required this.isPrimary, required this.isPrimary,
this.id, this.id,
this.name}) this.name,
this.txCount,
this.balance,
this.isChange = false})
: super(); : super();
final int? id; final int? id;
final bool isPrimary; final bool isPrimary;
final String address; final String address;
final String? name; final String? name;
final int? txCount;
final String? balance;
final bool isChange;
@override @override
String toString() => name ?? address; String toString() => name ?? address;

View file

@ -1,21 +1,25 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:cw_core/currency.dart';
import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/utils/list_item.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cw_core/amount_converter.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:intl/intl.dart';
import 'package:cake_wallet/store/app_store.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/haven/haven.dart';
part 'wallet_address_list_view_model.g.dart'; part 'wallet_address_list_view_model.g.dart';
@ -110,7 +114,8 @@ class EthereumURI extends PaymentURI {
class BitcoinCashURI extends PaymentURI { class BitcoinCashURI extends PaymentURI {
BitcoinCashURI({required String amount, required String address}) BitcoinCashURI({required String amount, required String address})
: super(amount: amount, address: address); : super(amount: amount, address: address);
@override @override
String toString() { String toString() {
var base = address; var base = address;
@ -121,9 +126,7 @@ class BitcoinCashURI extends PaymentURI {
return base; return base;
} }
} }
class NanoURI extends PaymentURI { class NanoURI extends PaymentURI {
NanoURI({required String amount, required String address}) NanoURI({required String amount, required String address})
@ -167,6 +170,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
hasAccounts = hasAccounts =
appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven, appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven,
amount = '', amount = '',
_settingsStore = appStore.settingsStore,
super(appStore: appStore) { super(appStore: appStore) {
_init(); _init();
} }
@ -184,12 +188,28 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
final NumberFormat _cryptoNumberFormat; final NumberFormat _cryptoNumberFormat;
final FiatConversionStore fiatConversionStore; final FiatConversionStore fiatConversionStore;
final SettingsStore _settingsStore;
List<Currency> get currencies => [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; List<Currency> get currencies => [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all];
String get buttonTitle {
if (isElectrumWallet) {
return S.current.addresses;
}
if (isAutoGenerateSubaddressEnabled) {
return hasAccounts ? S.current.accounts : S.current.account;
}
return hasAccounts ? S.current.accounts_subaddresses : S.current.addresses;
}
@observable @observable
Currency selectedCurrency; Currency selectedCurrency;
@observable
String searchText = '';
@computed @computed
int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency); int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency);
@ -277,14 +297,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
addressList.addAll(addressItems); addressList.addAll(addressItems);
} }
if (wallet.type == WalletType.bitcoin) { if (isElectrumWallet) {
final primaryAddress = bitcoin!.getAddress(wallet); final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) {
final bitcoinAddresses = bitcoin!.getAddresses(wallet).map((addr) { final isPrimary = subaddress.id == 0;
final isPrimary = addr == primaryAddress;
return WalletAddressListItem(isPrimary: isPrimary, name: null, address: addr); return WalletAddressListItem(
id: subaddress.id,
isPrimary: isPrimary,
name: subaddress.name,
address: subaddress.address,
txCount: subaddress.txCount,
balance: AmountConverter.amountIntToString(
walletTypeToCryptoCurrency(type), subaddress.balance),
isChange: subaddress.isChange);
}); });
addressList.addAll(bitcoinAddresses); addressList.addAll(addressItems);
} }
if (wallet.type == WalletType.ethereum) { if (wallet.type == WalletType.ethereum) {
@ -299,6 +326,15 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress));
} }
if (searchText.isNotEmpty) {
return ObservableList.of(addressList.where((item) {
if (item is WalletAddressListItem) {
return item.address.toLowerCase().contains(searchText.toLowerCase());
}
return false;
}));
}
return addressList; return addressList;
} }
@ -321,15 +357,23 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
@computed @computed
bool get hasAddressList => bool get hasAddressList =>
wallet.type == WalletType.monero || wallet.type == WalletType.monero ||
wallet.type == WalletType.haven;/* || wallet.type == WalletType.haven ||
wallet.type == WalletType.nano || wallet.type == WalletType.bitcoinCash ||
wallet.type == WalletType.banano;*/// TODO: nano accounts are disabled for now wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.litecoin;
// wallet.type == WalletType.nano ||
// wallet.type == WalletType.banano; TODO: nano accounts are disabled for now
@computed @computed
bool get showElectrumAddressDisclaimer => bool get isElectrumWallet =>
wallet.type == WalletType.bitcoin || wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.litecoin || wallet.type == WalletType.litecoin ||
wallet.type == WalletType.bitcoinCash; wallet.type == WalletType.bitcoinCash;
@computed
bool get isAutoGenerateSubaddressEnabled =>
_settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled;
List<ListItem> _baseItems; List<ListItem> _baseItems;
@ -343,9 +387,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
_baseItems = []; _baseItems = [];
if (wallet.type == WalletType.monero || if (wallet.type == WalletType.monero ||
wallet.type == WalletType.haven /*|| wallet.type ==
WalletType
.haven /*||
wallet.type == WalletType.nano || wallet.type == WalletType.nano ||
wallet.type == WalletType.banano*/) { wallet.type == WalletType.banano*/
) {
_baseItems.add(WalletAccountListHeader()); _baseItems.add(WalletAccountListHeader());
} }
@ -367,6 +414,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
} }
} }
@action
void updateSearchText(String text) {
searchText = text;
}
void _convertAmountToCrypto() { void _convertAmountToCrypto() {
final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type); final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type);
try { try {

View file

@ -64,6 +64,7 @@ import 'package:cw_core/output_info.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_service.dart';
import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';"""; import 'package:hive/hive.dart';""";
const bitcoinCWHeaders = """ const bitcoinCWHeaders = """
import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet.dart';
@ -76,9 +77,27 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/litecoin_wallet_service.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart';
import 'package:mobx/mobx.dart';
"""; """;
const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinCwPart = "part 'cw_bitcoin.dart';";
const bitcoinContent = """ const bitcoinContent = """
class ElectrumSubAddress {
ElectrumSubAddress({
required this.id,
required this.name,
required this.address,
required this.txCount,
required this.balance,
required this.isChange});
final int id;
final String name;
final String address;
final int txCount;
final int balance;
final bool isChange;
}
abstract class Bitcoin { abstract class Bitcoin {
TransactionPriority getMediumTransactionPriority(); TransactionPriority getMediumTransactionPriority();
@ -92,13 +111,16 @@ abstract class Bitcoin {
TransactionPriority deserializeBitcoinTransactionPriority(int raw); TransactionPriority deserializeBitcoinTransactionPriority(int raw);
TransactionPriority deserializeLitecoinTransactionPriority(int raw); TransactionPriority deserializeLitecoinTransactionPriority(int raw);
int getFeeRate(Object wallet, TransactionPriority priority); int getFeeRate(Object wallet, TransactionPriority priority);
Future<void> generateNewAddress(Object wallet); Future<void> generateNewAddress(Object wallet, String label);
Future<void> updateAddress(Object wallet,String address, String label);
Object createBitcoinTransactionCredentials(List<Output> outputs, {required TransactionPriority priority, int? feeRate}); Object createBitcoinTransactionCredentials(List<Output> outputs, {required TransactionPriority priority, int? feeRate});
Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs, {TransactionPriority? priority, required int feeRate}); Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs, {TransactionPriority? priority, required int feeRate});
List<String> getAddresses(Object wallet); List<String> getAddresses(Object wallet);
String getAddress(Object wallet); String getAddress(Object wallet);
List<ElectrumSubAddress> getSubAddresses(Object wallet);
String formatterBitcoinAmountToString({required int amount}); String formatterBitcoinAmountToString({required int amount});
double formatterBitcoinAmountToDouble({required int amount}); double formatterBitcoinAmountToDouble({required int amount});
int formatterStringDoubleToBitcoinAmount(String amount); int formatterStringDoubleToBitcoinAmount(String amount);