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>

Serhii committed Jan 23, 2024 at 07:15 UTC 9754d676012d2d5195ac0d1f56bc8c7f5f06c373
20 files changed +662 -328
cw_bitcoin/lib/bitcoin_address_record.dart
+48 -18
@@ -1,40 +1,70 @@
1 import 'dart:convert';
2 +import 'package:bitbox/bitbox.dart' as bitbox;
3
4 class BitcoinAddressRecord {
4 - BitcoinAddressRecord(this.address,
5 - {required this.index, this.isHidden = false, bool isUsed = false})
6 - : _isUsed = isUsed;
5 + BitcoinAddressRecord(
6 + this.address, {
7 + required this.index,
8 + this.isHidden = false,
9 + int txCount = 0,
10 + int balance = 0,
11 + String name = '',
12 + bool isUsed = false,
13 + }) : _txCount = txCount,
14 + _balance = balance,
15 + _name = name,
16 + _isUsed = isUsed;
17
18 factory BitcoinAddressRecord.fromJSON(String jsonSource) {
19 final decoded = json.decode(jsonSource) as Map;
20
11 - return BitcoinAddressRecord(
12 - decoded['address'] as String,
13 - index: decoded['index'] as int,
14 - isHidden: decoded['isHidden'] as bool? ?? false,
15 - isUsed: decoded['isUsed'] as bool? ?? false);
21 + return BitcoinAddressRecord(decoded['address'] as String,
22 + index: decoded['index'] as int,
23 + isHidden: decoded['isHidden'] as bool? ?? false,
24 + isUsed: decoded['isUsed'] as bool? ?? false,
25 + txCount: decoded['txCount'] as int? ?? 0,
26 + name: decoded['name'] as String? ?? '',
27 + balance: decoded['balance'] as int? ?? 0);
28 }
29
18 - @override
19 - bool operator ==(Object o) =>
20 - o is BitcoinAddressRecord && address == o.address;
21 -
30 final String address;
31 final bool isHidden;
32 final int index;
33 + int _txCount;
34 + int _balance;
35 + String _name;
36 + bool _isUsed;
37 +
38 + int get txCount => _txCount;
39 +
40 + String get name => _name;
41 +
42 + int get balance => _balance;
43 +
44 + set txCount(int value) => _txCount = value;
45 +
46 + set balance(int value) => _balance = value;
47 +
48 bool get isUsed => _isUsed;
49
50 + void setAsUsed() => _isUsed = true;
51 + void setNewName(String label) => _name = label;
52 +
53 @override
28 - int get hashCode => address.hashCode;
54 + bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address;
55
30 - bool _isUsed;
56 + @override
57 + int get hashCode => address.hashCode;
58
32 - void setAsUsed() => _isUsed = true;
59 + String get cashAddr => bitbox.Address.toCashAddress(address);
60
34 - String toJSON() =>
35 - json.encode({
61 + String toJSON() => json.encode({
62 'address': address,
63 'index': index,
64 'isHidden': isHidden,
39 - 'isUsed': isUsed});
65 + 'txCount': txCount,
66 + 'name': name,
67 + 'isUsed': isUsed,
68 + 'balance': balance,
69 + });
70 }
cw_bitcoin/lib/bitcoin_wallet.dart
+3
@@ -47,6 +47,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
47 sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType)
48 .derivePath("m/0'/1"),
49 networkType: networkType);
50 + autorun((_) {
51 + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
52 + });
53 }
54
55 static Future<BitcoinWallet> create({
cw_bitcoin/lib/electrum_wallet.dart
+58 -25
@@ -63,6 +63,7 @@ abstract class ElectrumWalletBase
63 _password = password,
64 _feeRates = <int>[],
65 _isTransactionUpdating = false,
66 + isEnabledAutoGenerateSubaddress = true,
67 unspentCoins = [],
68 _scripthashesUpdateSubject = {},
69 balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null
@@ -87,6 +88,10 @@ abstract class ElectrumWalletBase
88 final bitcoin.HDWallet hd;
89 final String mnemonic;
90
91 + @override
92 + @observable
93 + bool isEnabledAutoGenerateSubaddress;
94 +
95 late ElectrumClient electrumClient;
96 Box<UnspentCoinsInfo> unspentCoinsInfo;
97
@@ -583,38 +588,66 @@ abstract class ElectrumWalletBase
588 Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
589 final addressHashes = <String, BitcoinAddressRecord>{};
590 final normalizedHistories = <Map<String, dynamic>>[];
591 + final newTxCounts = <String, int>{};
592 +
593 walletAddresses.addresses.forEach((addressRecord) {
594 final sh = scriptHash(addressRecord.address, networkType: networkType);
595 addressHashes[sh] = addressRecord;
596 + newTxCounts[sh] = 0;
597 });
590 - final histories = addressHashes.keys.map((scriptHash) =>
591 - electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history}));
592 - final historyResults = await Future.wait(histories);
593 - historyResults.forEach((history) {
594 - history.entries.forEach((historyItem) {
595 - if (historyItem.value.isNotEmpty) {
596 - final address = addressHashes[historyItem.key];
597 - address?.setAsUsed();
598 - normalizedHistories.addAll(historyItem.value);
599 - }
598 +
599 + try {
600 + final histories = addressHashes.keys.map((scriptHash) =>
601 + electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history}));
602 + final historyResults = await Future.wait(histories);
603 +
604 +
605 +
606 + historyResults.forEach((history) {
607 + history.entries.forEach((historyItem) {
608 + if (historyItem.value.isNotEmpty) {
609 + final address = addressHashes[historyItem.key];
610 + address?.setAsUsed();
611 + newTxCounts[historyItem.key] = historyItem.value.length;
612 + normalizedHistories.addAll(historyItem.value);
613 + }
614 + });
615 });
601 - });
602 - final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) {
603 - try {
604 - return fetchTransactionInfo(
605 - hash: transaction['tx_hash'] as String, height: transaction['height'] as int);
606 - } catch (_) {
607 - return Future.value(null);
616 +
617 + for (var sh in addressHashes.keys) {
618 + var balanceData = await electrumClient.getBalance(sh);
619 + var addressRecord = addressHashes[sh];
620 + if (addressRecord != null) {
621 + addressRecord.balance = balanceData['confirmed'] as int? ?? 0;
622 + }
623 }
609 - }));
610 - return historiesWithDetails
611 - .fold<Map<String, ElectrumTransactionInfo>>(<String, ElectrumTransactionInfo>{}, (acc, tx) {
612 - if (tx == null) {
624 +
625 +
626 + addressHashes.forEach((sh, addressRecord) {
627 + addressRecord.txCount = newTxCounts[sh] ?? 0;
628 + });
629 +
630 + final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) {
631 + try {
632 + return fetchTransactionInfo(
633 + hash: transaction['tx_hash'] as String, height: transaction['height'] as int);
634 + } catch (_) {
635 + return Future.value(null);
636 + }
637 + }));
638 +
639 + return historiesWithDetails.fold<Map<String, ElectrumTransactionInfo>>(
640 + <String, ElectrumTransactionInfo>{}, (acc, tx) {
641 + if (tx == null) {
642 + return acc;
643 + }
644 + acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
645 return acc;
614 - }
615 - acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
616 - return acc;
617 - });
646 + });
647 + } catch (e) {
648 + print(e.toString());
649 + return {};
650 + }
651 }
652
653 Future<void> updateTransactions() async {
cw_bitcoin/lib/electrum_wallet_addresses.dart
+98 -80
@@ -1,5 +1,5 @@
1 -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
1 import 'package:bitbox/bitbox.dart' as bitbox;
2 +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
3 import 'package:cw_bitcoin/bitcoin_address_record.dart';
4 import 'package:cw_bitcoin/electrum.dart';
5 import 'package:cw_bitcoin/script_hash.dart';
@@ -10,8 +10,7 @@ import 'package:mobx/mobx.dart';
10
11 part 'electrum_wallet_addresses.g.dart';
12
13 -class ElectrumWalletAddresses = ElectrumWalletAddressesBase
14 - with _$ElectrumWalletAddresses;
13 +class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
14
15 abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
16 ElectrumWalletAddressesBase(WalletInfo walletInfo,
@@ -22,19 +21,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
21 List<BitcoinAddressRecord>? initialAddresses,
22 int initialRegularAddressIndex = 0,
23 int initialChangeAddressIndex = 0})
25 - : addresses = ObservableList<BitcoinAddressRecord>.of(
26 - (initialAddresses ?? []).toSet()),
27 - receiveAddresses = ObservableList<BitcoinAddressRecord>.of(
28 - (initialAddresses ?? [])
24 + : addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
25 + receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
26 .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
30 - .toSet()),
31 - changeAddresses = ObservableList<BitcoinAddressRecord>.of(
32 - (initialAddresses ?? [])
27 + .toSet()),
28 + changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
29 .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
34 - .toSet()),
30 + .toSet()),
31 currentReceiveAddressIndex = initialRegularAddressIndex,
32 currentChangeAddressIndex = initialChangeAddressIndex,
37 - super(walletInfo);
33 + super(walletInfo);
34
35 static const defaultReceiveAddressesCount = 22;
36 static const defaultChangeAddressesCount = 17;
@@ -42,6 +38,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
38
39 static String toCashAddr(String address) => bitbox.Address.toCashAddress(address);
40
41 + static String toLegacy(String address) => bitbox.Address.toLegacyAddress(address);
42 +
43 final ObservableList<BitcoinAddressRecord> addresses;
44 final ObservableList<BitcoinAddressRecord> receiveAddresses;
45 final ObservableList<BitcoinAddressRecord> changeAddresses;
@@ -53,41 +51,67 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
51 @override
52 @computed
53 String get address {
56 - if (receiveAddresses.isEmpty) {
57 - final address = generateNewAddress().address;
58 - return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(address) : address;
59 - }
60 - final receiveAddress = receiveAddresses.first.address;
54 + if (isEnabledAutoGenerateSubaddress) {
55 + if (receiveAddresses.isEmpty) {
56 + final newAddress = generateNewAddress().address;
57 + return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(newAddress) : newAddress;
58 + }
59 + final receiveAddress = receiveAddresses.first.address;
60
62 - return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress;
61 + return walletInfo.type == WalletType.bitcoinCash
62 + ? toCashAddr(receiveAddress)
63 + : receiveAddress;
64 + } else {
65 + final receiveAddress = (receiveAddresses.first.address != addresses.first.address &&
66 + previousAddressRecord != null)
67 + ? previousAddressRecord!.address
68 + : addresses.first.address;
69 +
70 + return walletInfo.type == WalletType.bitcoinCash
71 + ? toCashAddr(receiveAddress)
72 + : receiveAddress;
73 + }
74 }
75
76 + @observable
77 + bool isEnabledAutoGenerateSubaddress = true;
78 +
79 @override
66 - String get primaryAddress => getAddress(index: 0, hd: mainHd);
80 + set address(String addr) {
81 + if (addr.startsWith('bitcoincash:')) {
82 + addr = toLegacy(addr);
83 + }
84 + final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == addr);
85 +
86 + previousAddressRecord = addressRecord;
87 + receiveAddresses.remove(addressRecord);
88 + receiveAddresses.insert(0, addressRecord);
89 + }
90
91 @override
69 - set address(String addr) => null;
92 + String get primaryAddress => getAddress(index: 0, hd: mainHd);
93
94 int currentReceiveAddressIndex;
95 int currentChangeAddressIndex;
96
97 + @observable
98 + BitcoinAddressRecord? previousAddressRecord;
99 +
100 @computed
75 - int get totalCountOfReceiveAddresses =>
76 - addresses.fold(0, (acc, addressRecord) {
77 - if (!addressRecord.isHidden) {
78 - return acc + 1;
79 - }
80 - return acc;
81 - });
101 + int get totalCountOfReceiveAddresses => addresses.fold(0, (acc, addressRecord) {
102 + if (!addressRecord.isHidden) {
103 + return acc + 1;
104 + }
105 + return acc;
106 + });
107
108 @computed
84 - int get totalCountOfChangeAddresses =>
85 - addresses.fold(0, (acc, addressRecord) {
86 - if (addressRecord.isHidden) {
87 - return acc + 1;
88 - }
89 - return acc;
90 - });
109 + int get totalCountOfChangeAddresses => addresses.fold(0, (acc, addressRecord) {
110 + if (addressRecord.isHidden) {
111 + return acc + 1;
112 + }
113 + return acc;
114 + });
115
116 Future<void> discoverAddresses() async {
117 await _discoverAddresses(mainHd, false);
@@ -117,11 +141,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
141
142 if (changeAddresses.isEmpty) {
143 final newAddresses = await _createNewAddresses(gap,
120 - hd: sideHd,
121 - startIndex: totalCountOfChangeAddresses > 0
122 - ? totalCountOfChangeAddresses - 1
123 - : 0,
124 - isHidden: true);
144 + hd: sideHd,
145 + startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0,
146 + isHidden: true);
147 _addAddresses(newAddresses);
148 }
149
@@ -135,14 +157,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
157 return address;
158 }
159
138 - BitcoinAddressRecord generateNewAddress(
139 - {bitcoin.HDWallet? hd, bool isHidden = false}) {
140 - currentReceiveAddressIndex += 1;
141 - // FIX-ME: Check logic for whichi HD should be used here ???
142 - final address = BitcoinAddressRecord(
143 - getAddress(index: currentReceiveAddressIndex, hd: hd ?? sideHd),
144 - index: currentReceiveAddressIndex,
145 - isHidden: isHidden);
160 + BitcoinAddressRecord generateNewAddress({bitcoin.HDWallet? hd, String? label}) {
161 + final isHidden = hd == sideHd;
162 +
163 + final newAddressIndex = addresses.fold(
164 + 0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc);
165 +
166 + final address = BitcoinAddressRecord(getAddress(index: newAddressIndex, hd: hd ?? sideHd),
167 + index: newAddressIndex, isHidden: isHidden, name: label ?? '');
168 addresses.add(address);
169 return address;
170 }
@@ -160,20 +182,32 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
182 }
183 }
184
185 + @action
186 + void updateAddress(String address, String label) {
187 + if (address.startsWith('bitcoincash:')) {
188 + address = toLegacy(address);
189 + }
190 + final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == address);
191 + addressRecord.setNewName(label);
192 + final index = addresses.indexOf(addressRecord);
193 + addresses.remove(addressRecord);
194 + addresses.insert(index, addressRecord);
195 + }
196 +
197 @action
198 void updateReceiveAddresses() {
199 receiveAddresses.removeRange(0, receiveAddresses.length);
166 - final newAdresses = addresses
167 - .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
168 - receiveAddresses.addAll(newAdresses);
200 + final newAddresses =
201 + addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
202 + receiveAddresses.addAll(newAddresses);
203 }
204
205 @action
206 void updateChangeAddresses() {
207 changeAddresses.removeRange(0, changeAddresses.length);
174 - final newAdresses = addresses
175 - .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed);
176 - changeAddresses.addAll(newAdresses);
208 + final newAddresses =
209 + addresses.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed);
210 + changeAddresses.addAll(newAddresses);
211 }
212
213 Future<void> _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async {
@@ -181,20 +215,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
215 List<BitcoinAddressRecord> addrs;
216
217 if (addresses.isNotEmpty) {
184 - addrs = addresses
185 - .where((addr) => addr.isHidden == isHidden)
186 - .toList();
218 + addrs = addresses.where((addr) => addr.isHidden == isHidden).toList();
219 } else {
220 addrs = await _createNewAddresses(
189 - isHidden
190 - ? defaultChangeAddressesCount
191 - : defaultReceiveAddressesCount,
221 + isHidden ? defaultChangeAddressesCount : defaultReceiveAddressesCount,
222 startIndex: 0,
223 hd: hd,
224 isHidden: isHidden);
225 }
226
197 - while(hasAddrUse) {
227 + while (hasAddrUse) {
228 final addr = addrs.last.address;
229 hasAddrUse = await _hasAddressUsed(addr);
230
@@ -204,11 +234,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
234
235 final start = addrs.length;
236 final count = start + gap;
207 - final batch = await _createNewAddresses(
208 - count,
209 - startIndex: start,
210 - hd: hd,
211 - isHidden: isHidden);
237 + final batch = await _createNewAddresses(count, startIndex: start, hd: hd, isHidden: isHidden);
238 addrs.addAll(batch);
239 }
240
@@ -232,21 +258,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
258
259 if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
260 final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
235 - final newAddresses = await _createNewAddresses(
236 - addressesCount,
237 - startIndex: countOfReceiveAddresses,
238 - hd: mainHd,
239 - isHidden: false);
261 + final newAddresses = await _createNewAddresses(addressesCount,
262 + startIndex: countOfReceiveAddresses, hd: mainHd, isHidden: false);
263 addresses.addAll(newAddresses);
264 }
265
266 if (countOfHiddenAddresses < defaultChangeAddressesCount) {
267 final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
245 - final newAddresses = await _createNewAddresses(
246 - addressesCount,
247 - startIndex: countOfHiddenAddresses,
248 - hd: sideHd,
249 - isHidden: true);
268 + final newAddresses = await _createNewAddresses(addressesCount,
269 + startIndex: countOfHiddenAddresses, hd: sideHd, isHidden: true);
270 addresses.addAll(newAddresses);
271 }
272 }
@@ -256,10 +276,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
276 final list = <BitcoinAddressRecord>[];
277
278 for (var i = startIndex; i < count + startIndex; i++) {
259 - final address = BitcoinAddressRecord(
260 - getAddress(index: i, hd: hd),
261 - index: i,
262 - isHidden: isHidden);
279 + final address =
280 + BitcoinAddressRecord(getAddress(index: i, hd: hd), index: i, isHidden: isHidden);
281 list.add(address);
282 }
283
@@ -278,4 +296,4 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
296 final transactionHistory = await electrumClient.getHistory(sh);
297 return transactionHistory.isNotEmpty;
298 }
281 -}
\ No newline at end of file
299 +}
cw_bitcoin/lib/litecoin_wallet.dart
+3
@@ -51,6 +51,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
51 .fromSeed(seedBytes, network: networkType)
52 .derivePath("m/0'/1"),
53 networkType: networkType,);
54 + autorun((_) {
55 + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
56 + });
57 }
58
59 static Future<LitecoinWallet> create({
cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart
+3
@@ -57,6 +57,9 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
57 sideHd: bitcoin.HDWallet.fromSeed(seedBytes)
58 .derivePath("m/44'/145'/0'/1"),
59 networkType: networkType);
60 + autorun((_) {
61 + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
62 + });
63 }
64
65
cw_core/lib/amount_converter.dart
+1
@@ -81,6 +81,7 @@ class AmountConverter {
81 return _moneroAmountToString(amount);
82 case CryptoCurrency.btc:
83 case CryptoCurrency.bch:
84 + case CryptoCurrency.ltc:
85 return _bitcoinAmountToString(amount);
86 case CryptoCurrency.xhv:
87 case CryptoCurrency.xag:
lib/bitcoin/cw_bitcoin.dart
+25 -2
@@ -63,9 +63,17 @@ class CWBitcoin extends Bitcoin {
63 }
64
65 @override
66 - Future<void> generateNewAddress(Object wallet) async {
66 + Future<void> generateNewAddress(Object wallet, String label) async {
67 final bitcoinWallet = wallet as ElectrumWallet;
68 - await bitcoinWallet.walletAddresses.generateNewAddress();
68 + await bitcoinWallet.walletAddresses.generateNewAddress(label: label);
69 + await wallet.save();
70 + }
71 +
72 + @override
73 + Future<void> updateAddress(Object wallet,String address, String label) async {
74 + final bitcoinWallet = wallet as ElectrumWallet;
75 + bitcoinWallet.walletAddresses.updateAddress(address, label);
76 + await wallet.save();
77 }
78
79 @override
@@ -99,6 +107,21 @@ class CWBitcoin extends Bitcoin {
107 .toList();
108 }
109
110 + @override
111 + @computed
112 + List<ElectrumSubAddress> getSubAddresses(Object wallet) {
113 + final electrumWallet = wallet as ElectrumWallet;
114 + return electrumWallet.walletAddresses.addresses
115 + .map((BitcoinAddressRecord addr) => ElectrumSubAddress(
116 + id: addr.index,
117 + name: addr.name,
118 + address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address,
119 + txCount: addr.txCount,
120 + balance: addr.balance,
121 + isChange: addr.isHidden))
122 + .toList();
123 + }
124 +
125 @override
126 String getAddress(Object wallet) {
127 final bitcoinWallet = wallet as ElectrumWallet;
lib/reactions/on_current_wallet_change.dart
+2 -1
@@ -68,7 +68,8 @@ void startCurrentWalletChangeReaction(
68 .get<SharedPreferences>()
69 .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type));
70
71 - if (wallet.type == WalletType.monero) {
71 + if (wallet.type == WalletType.monero || wallet.type == WalletType.bitcoin ||
72 + wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash ) {
73 _setAutoGenerateSubaddressStatus(wallet, settingsStore);
74 }
75
lib/src/screens/dashboard/pages/address_page.dart
+17 -51
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
2 import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
3 import 'package:cake_wallet/di.dart';
4 import 'package:cake_wallet/src/screens/base_page.dart';
@@ -15,6 +16,7 @@ import 'package:cake_wallet/utils/share_util.dart';
16 import 'package:cake_wallet/utils/show_pop_up.dart';
17 import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
18 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
19 +import 'package:cw_core/wallet_type.dart';
20 import 'package:flutter/material.dart';
21 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
22 import 'package:cake_wallet/src/screens/receive/widgets/qr_widget.dart';
@@ -155,63 +157,27 @@ class AddressPage extends BasePage {
157 amountController: _amountController,
158 isLight: dashboardViewModel.settingsStore.currentTheme.type ==
159 ThemeType.light))),
160 + SizedBox(height: 16),
161 Observer(builder: (_) {
162 if (addressListViewModel.hasAddressList) {
160 - return GestureDetector(
161 - onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled
163 + return SelectButton(
164 + text: addressListViewModel.buttonTitle,
165 + onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled &&
166 + (WalletType.monero == addressListViewModel.wallet.type ||
167 + WalletType.haven == addressListViewModel.wallet.type)
168 ? await showPopUp<void>(
163 - context: context, builder: (_) => getIt.get<MoneroAccountListPage>())
169 + context: context,
170 + builder: (_) => getIt.get<MoneroAccountListPage>())
171 : Navigator.of(context).pushNamed(Routes.receive),
165 - child: Container(
166 - height: 50,
167 - padding: EdgeInsets.only(left: 24, right: 12),
168 - alignment: Alignment.center,
169 - decoration: BoxDecoration(
170 - borderRadius: BorderRadius.all(Radius.circular(25)),
171 - border: Border.all(
172 - color:
173 - Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
174 - width: 1),
175 - color: Theme.of(context)
176 - .extension<SyncIndicatorTheme>()!
177 - .syncedBackgroundColor),
178 - child: Row(
179 - mainAxisSize: MainAxisSize.max,
180 - mainAxisAlignment: MainAxisAlignment.spaceBetween,
181 - children: <Widget>[
182 - Observer(
183 - builder: (_) {
184 - String label = addressListViewModel.hasAccounts
185 - ? S.of(context).accounts_subaddresses
186 - : S.of(context).addresses;
187 -
188 - if (dashboardViewModel.isAutoGenerateSubaddressesEnabled) {
189 - label = addressListViewModel.hasAccounts
190 - ? S.of(context).accounts
191 - : S.of(context).account;
192 - }
193 - return Text(
194 - label,
195 - style: TextStyle(
196 - fontSize: 14,
197 - fontWeight: FontWeight.w500,
198 - color: Theme.of(context)
199 - .extension<SyncIndicatorTheme>()!
200 - .textColor),
201 - );
202 - },
203 - ),
204 - Icon(
205 - Icons.arrow_forward_ios,
206 - size: 14,
207 - color: Theme.of(context).extension<SyncIndicatorTheme>()!.textColor,
208 - )
209 - ],
210 - ),
211 - ),
172 + textColor: Theme.of(context).extension<SyncIndicatorTheme>()!.textColor,
173 + color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
174 + borderColor: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
175 + arrowColor: Theme.of(context).extension<SyncIndicatorTheme>()!.textColor,
176 + textSize: 14,
177 + height: 50,
178 );
179 } else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled ||
214 - addressListViewModel.showElectrumAddressDisclaimer) {
180 + addressListViewModel.isElectrumWallet) {
181 return Text(S.of(context).electrum_address_disclaimer,
182 textAlign: TextAlign.center,
183 style: TextStyle(
lib/src/screens/new_wallet/widgets/select_button.dart
+21 -11
@@ -11,29 +11,37 @@ class SelectButton extends StatelessWidget {
11 this.isSelected = false,
12 this.showTrailingIcon = true,
13 this.height = 60,
14 + this.textSize = 18,
15 + this.color,
16 + this.textColor,
17 + this.arrowColor,
18 + this.borderColor,
19 });
20
21 final Image? image;
22 final String text;
23 + final double textSize;
24 final bool isSelected;
25 final VoidCallback onTap;
26 final bool showTrailingIcon;
27 final double height;
28 + final Color? color;
29 + final Color? textColor;
30 + final Color? arrowColor;
31 + final Color? borderColor;
32
33 @override
34 Widget build(BuildContext context) {
25 - final color = isSelected
26 - ? Colors.green
27 - : Theme.of(context).cardColor;
28 - final textColor = isSelected
35 + final backgroundColor = color ?? (isSelected ? Colors.green : Theme.of(context).cardColor);
36 + final effectiveTextColor = textColor ?? (isSelected
37 ? Theme.of(context).extension<WalletListTheme>()!.restoreWalletButtonTextColor
30 - : Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor;
31 - final arrowColor = isSelected
38 + : Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor);
39 + final effectiveArrowColor = arrowColor ?? (isSelected
40 ? Theme.of(context).extension<WalletListTheme>()!.restoreWalletButtonTextColor
33 - : Theme.of(context).extension<FilterTheme>()!.titlesColor;
41 + : Theme.of(context).extension<FilterTheme>()!.titlesColor);
42
43 final selectArrowImage = Image.asset('assets/images/select_arrow.png',
36 - color: arrowColor);
44 + color: effectiveArrowColor);
45
46 return GestureDetector(
47 onTap: onTap,
@@ -44,7 +52,9 @@ class SelectButton extends StatelessWidget {
52 alignment: Alignment.center,
53 decoration: BoxDecoration(
54 borderRadius: BorderRadius.all(Radius.circular(30)),
47 - color: color
55 + color: backgroundColor,
56 + border: borderColor != null ? Border.all(color: borderColor!) : null,
57 +
58 ),
59 child: Row(
60 mainAxisSize: MainAxisSize.max,
@@ -63,9 +73,9 @@ class SelectButton extends StatelessWidget {
73 child: Text(
74 text,
75 style: TextStyle(
66 - fontSize: 18,
76 + fontSize: textSize,
77 fontWeight: FontWeight.w500,
68 - color: textColor
78 + color: effectiveTextColor,
79 ),
80 ),
81 )
lib/src/screens/receive/receive_page.dart
+23 -14
@@ -49,7 +49,7 @@ class ReceivePage extends BasePage {
49 bool get gradientBackground => true;
50
51 @override
52 - bool get resizeToAvoidBottomInset => false;
52 + bool get resizeToAvoidBottomInset => true;
53
54 final FocusNode _cryptoAmountFocus;
55
@@ -99,10 +99,11 @@ class ReceivePage extends BasePage {
99
100 @override
101 Widget body(BuildContext context) {
102 + final isElectrumWallet = addressListViewModel.isElectrumWallet;
103 return (addressListViewModel.type == WalletType.monero ||
104 addressListViewModel.type == WalletType.haven ||
105 addressListViewModel.type == WalletType.nano ||
105 - addressListViewModel.type == WalletType.banano)
106 + isElectrumWallet)
107 ? KeyboardActions(
108 config: KeyboardActionsConfig(
109 keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
@@ -140,7 +141,9 @@ class ReceivePage extends BasePage {
141
142 if (item is WalletAccountListHeader) {
143 cell = HeaderTile(
143 - onTap: () async {
144 + showTrailingButton: true,
145 + walletAddressListViewModel: addressListViewModel,
146 + trailingButtonTap: () async {
147 if (addressListViewModel.type == WalletType.monero ||
148 addressListViewModel.type == WalletType.haven) {
149 await showPopUp<void>(
@@ -153,7 +156,7 @@ class ReceivePage extends BasePage {
156 }
157 },
158 title: S.of(context).accounts,
156 - icon: Icon(
159 + trailingIcon: Icon(
160 Icons.arrow_forward_ios,
161 size: 14,
162 color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
@@ -161,16 +164,21 @@ class ReceivePage extends BasePage {
164 }
165
166 if (item is WalletAddressListHeader) {
164 - cell = HeaderTile(
165 - onTap: () =>
166 - Navigator.of(context).pushNamed(Routes.newSubaddress),
167 - title: S.of(context).addresses,
168 - icon: Icon(
169 - Icons.add,
170 - size: 20,
171 - color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
172 - ));
173 - }
167 + cell = HeaderTile(
168 + title: S.of(context).addresses,
169 + walletAddressListViewModel: addressListViewModel,
170 + showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled,
171 + showSearchButton: true,
172 + trailingButtonTap: () =>
173 + Navigator.of(context).pushNamed(Routes.newSubaddress),
174 + trailingIcon: Icon(
175 + Icons.add,
176 + size: 20,
177 + color: Theme.of(context)
178 + .extension<ReceivePageTheme>()!
179 + .iconsColor,
180 + ));
181 + }
182
183 if (item is WalletAddressListItem) {
184 cell = Observer(builder: (_) {
@@ -185,6 +193,7 @@ class ReceivePage extends BasePage {
193
194 return AddressCell.fromItem(item,
195 isCurrent: isCurrent,
196 + hasBalance: addressListViewModel.isElectrumWallet,
197 backgroundColor: backgroundColor,
198 textColor: textColor,
199 onTap: (_) => addressListViewModel.setAddress(item),
lib/src/screens/receive/widgets/address_cell.dart
+134 -46
@@ -1,7 +1,8 @@
1 -import 'package:flutter/material.dart';
2 -import 'package:flutter_slidable/flutter_slidable.dart';
1 +import 'package:auto_size_text/auto_size_text.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
4 +import 'package:flutter/material.dart';
5 +import 'package:flutter_slidable/flutter_slidable.dart';
6
7 class AddressCell extends StatelessWidget {
8 AddressCell(
@@ -12,13 +13,18 @@ class AddressCell extends StatelessWidget {
13 required this.backgroundColor,
14 required this.textColor,
15 this.onTap,
15 - this.onEdit});
16 + this.onEdit,
17 + this.txCount,
18 + this.balance,
19 + this.isChange = false,
20 + this.hasBalance = false});
21
22 factory AddressCell.fromItem(WalletAddressListItem item,
23 {required bool isCurrent,
24 required Color backgroundColor,
25 required Color textColor,
26 Function(String)? onTap,
27 + bool hasBalance = false,
28 Function()? onEdit}) =>
29 AddressCell(
30 address: item.address,
@@ -28,7 +34,11 @@ class AddressCell extends StatelessWidget {
34 backgroundColor: backgroundColor,
35 textColor: textColor,
36 onTap: onTap,
31 - onEdit: onEdit);
37 + onEdit: onEdit,
38 + txCount: item.txCount,
39 + balance: item.balance,
40 + isChange: item.isChange,
41 + hasBalance: hasBalance);
42
43 final String address;
44 final String name;
@@ -38,17 +48,22 @@ class AddressCell extends StatelessWidget {
48 final Color textColor;
49 final Function(String)? onTap;
50 final Function()? onEdit;
51 + final int? txCount;
52 + final String? balance;
53 + final bool isChange;
54 + final bool hasBalance;
55 +
56 + static const int addressPreviewLength = 8;
57
42 - String get label {
43 - if (name.isEmpty){
44 - if(address.length<=16){
45 - return address;
46 - }else{
47 - return address.substring(0,8)+'...'+
48 - address.substring(address.length-8,address.length);
49 - }
50 - }else{
51 - return name;
58 + String get formattedAddress {
59 + final formatIfCashAddr = address.replaceAll('bitcoincash:', '');
60 +
61 + if (formatIfCashAddr.length <= (name.isNotEmpty ? 16 : 43)) {
62 + return formatIfCashAddr;
63 + } else {
64 + return formatIfCashAddr.substring(0, addressPreviewLength) +
65 + '...' +
66 + formatIfCashAddr.substring(formatIfCashAddr.length - addressPreviewLength, formatIfCashAddr.length);
67 }
68 }
69
@@ -59,41 +74,114 @@ class AddressCell extends StatelessWidget {
74 child: Container(
75 width: double.infinity,
76 color: backgroundColor,
62 - padding: EdgeInsets.only(left: 24, right: 24, top: 28, bottom: 28),
63 - child: Text(
64 - label,
65 - maxLines: 1,
66 - overflow: TextOverflow.ellipsis,
67 - style: TextStyle(
68 - fontSize: 14,
69 - color: textColor,
70 - ),
77 + padding: EdgeInsets.only(left: 24, right: 24, top: 20, bottom: 20),
78 + child: Row(
79 + children: [
80 + Expanded(
81 + child: Column(
82 + children: [
83 + Row(
84 + mainAxisAlignment: MainAxisAlignment.center,
85 + mainAxisSize: MainAxisSize.max,
86 + children: [
87 + if (isChange)
88 + Padding(
89 + padding: const EdgeInsets.only(right: 8.0),
90 + child: Container(
91 + height: 20,
92 + padding: EdgeInsets.all(4),
93 + decoration: BoxDecoration(
94 + borderRadius: BorderRadius.all(Radius.circular(8.5)),
95 + color: textColor),
96 + alignment: Alignment.center,
97 + child: Text(
98 + S.of(context).unspent_change,
99 + style: TextStyle(
100 + color: backgroundColor,
101 + fontSize: 10,
102 + fontWeight: FontWeight.w600,
103 + ),
104 + ),
105 + ),
106 + ),
107 + if (name.isNotEmpty)
108 + Text(
109 + '$name - ',
110 + style: TextStyle(
111 + fontSize: 14,
112 + fontWeight: FontWeight.w600,
113 + color: textColor,
114 + ),
115 + ),
116 + AutoSizeText(
117 + formattedAddress,
118 + maxLines: 1,
119 + overflow: TextOverflow.ellipsis,
120 + style: TextStyle(
121 + fontSize: isChange ? 10 : 14,
122 + color: textColor,
123 + ),
124 + ),
125 + ],
126 + ),
127 + if (hasBalance)
128 + Padding(
129 + padding: const EdgeInsets.only(top: 8.0),
130 + child: Row(
131 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
132 + mainAxisSize: MainAxisSize.max,
133 + children: [
134 + Text(
135 + 'Balance: $balance',
136 + style: TextStyle(
137 + fontSize: 16,
138 + fontWeight: FontWeight.w600,
139 + color: textColor,
140 + ),
141 + ),
142 + Text(
143 + '${S.of(context).transactions.toLowerCase()}: $txCount',
144 + style: TextStyle(
145 + fontSize: 16,
146 + fontWeight: FontWeight.w600,
147 + color: textColor,
148 + ),
149 + ),
150 + ],
151 + ),
152 + ),
153 + ],
154 + ),
155 + ),
156 + ],
157 ),
158 ));
73 - return Semantics(
74 - label: S.of(context).slidable,
75 - selected: isCurrent,
76 - enabled: !isCurrent,
77 - child: Slidable(
78 - key: Key(address),
79 - startActionPane: _actionPane(context),
80 - endActionPane: _actionPane(context),
81 - child: cell,
82 - ),
83 - );
159 + return onEdit == null
160 + ? cell
161 + : Semantics(
162 + label: S.of(context).slidable,
163 + selected: isCurrent,
164 + enabled: !isCurrent,
165 + child: Slidable(
166 + key: Key(address),
167 + startActionPane: _actionPane(context),
168 + endActionPane: _actionPane(context),
169 + child: cell,
170 + ),
171 + );
172 }
173
174 ActionPane _actionPane(BuildContext context) => ActionPane(
87 - motion: const ScrollMotion(),
88 - extentRatio: 0.3,
89 - children: [
90 - SlidableAction(
91 - onPressed: (_) => onEdit?.call(),
92 - backgroundColor: Colors.blue,
93 - foregroundColor: Colors.white,
94 - icon: Icons.edit,
95 - label: S.of(context).edit,
96 - ),
97 - ],
98 - );
175 + motion: const ScrollMotion(),
176 + extentRatio: 0.3,
177 + children: [
178 + SlidableAction(
179 + onPressed: (_) => onEdit?.call(),
180 + backgroundColor: Colors.blue,
181 + foregroundColor: Colors.white,
182 + icon: Icons.edit,
183 + label: S.of(context).edit,
184 + ),
185 + ],
186 + );
187 }
lib/src/screens/receive/widgets/header_tile.dart
+101 -37
@@ -1,50 +1,114 @@
1 -import 'package:flutter/material.dart';
1 +import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
3 +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
4 +import 'package:flutter/material.dart';
5
4 -class HeaderTile extends StatelessWidget {
6 +class HeaderTile extends StatefulWidget {
7 HeaderTile({
6 - required this.onTap,
8 required this.title,
8 - required this.icon
9 + required this.walletAddressListViewModel,
10 + this.showSearchButton = false,
11 + this.showTrailingButton = false,
12 + this.trailingButtonTap,
13 + this.trailingIcon,
14 });
15
11 - final VoidCallback onTap;
16 final String title;
13 - final Icon icon;
17 + final WalletAddressListViewModel walletAddressListViewModel;
18 + final bool showSearchButton;
19 + final bool showTrailingButton;
20 + final VoidCallback? trailingButtonTap;
21 + final Icon? trailingIcon;
22 +
23 + @override
24 + _HeaderTileState createState() => _HeaderTileState();
25 +}
26 +
27 +class _HeaderTileState extends State<HeaderTile> {
28 + bool _isSearchActive = false;
29
30 @override
31 Widget build(BuildContext context) {
17 - return GestureDetector(
18 - onTap: onTap,
19 - child: Container(
20 - padding: EdgeInsets.only(
21 - left: 24,
22 - right: 24,
23 - top: 24,
24 - bottom: 24
25 - ),
26 - color: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor,
27 - child: Row(
28 - mainAxisSize: MainAxisSize.max,
29 - mainAxisAlignment: MainAxisAlignment.spaceBetween,
30 - children: <Widget>[
31 - Text(
32 - title,
33 - style: TextStyle(
34 - fontSize: 18,
35 - fontWeight: FontWeight.w600,
36 - color: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor),
37 - ),
38 - Container(
39 - height: 32,
40 - width: 32,
41 - decoration: BoxDecoration(
42 - shape: BoxShape.circle,
43 - color: Theme.of(context).extension<ReceivePageTheme>()!.iconsBackgroundColor),
44 - child: icon,
45 - )
46 - ],
47 - ),
32 + final searchIcon = Image.asset("assets/images/search_icon.png",
33 + color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor);
34 +
35 + return Container(
36 + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
37 + color: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor,
38 + child: Row(
39 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
40 + children: <Widget>[
41 + _isSearchActive
42 + ? Expanded(
43 + child: TextField(
44 + onChanged: (value) => widget.walletAddressListViewModel.updateSearchText(value),
45 + cursorColor: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor,
46 + cursorWidth: 0.5,
47 + decoration: InputDecoration(
48 + hintText: '${S.of(context).search}...',
49 + isDense: true,
50 + contentPadding: EdgeInsets.zero,
51 + hintStyle: TextStyle(
52 + fontSize: 16,
53 + fontWeight: FontWeight.w600,
54 + color: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor),
55 + border: UnderlineInputBorder(
56 + borderSide: BorderSide(color: Theme.of(context).dividerColor),
57 + ),
58 + focusedBorder: UnderlineInputBorder(
59 + borderSide: BorderSide(color: Theme.of(context).dividerColor),
60 + ),
61 + enabledBorder: UnderlineInputBorder(
62 + borderSide: BorderSide(color: Theme.of(context).dividerColor),
63 + ),
64 + ),
65 + autofocus: true,
66 + ),
67 + )
68 + : Text(
69 + widget.title,
70 + style: TextStyle(
71 + fontSize: 16,
72 + fontWeight: FontWeight.w600,
73 + color: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor),
74 + ),
75 + Row(
76 + children: [
77 + if (widget.showSearchButton)
78 + GestureDetector(
79 + onTap: () {
80 + setState(() {
81 + _isSearchActive = !_isSearchActive;
82 + widget.walletAddressListViewModel.updateSearchText('');
83 + });
84 + },
85 + child: Container(
86 + height: 32,
87 + width: 32,
88 + decoration: BoxDecoration(
89 + shape: BoxShape.circle,
90 + color: Theme.of(context)
91 + .extension<ReceivePageTheme>()!
92 + .iconsBackgroundColor),
93 + child: searchIcon,
94 + )),
95 + const SizedBox(width: 8),
96 + if (widget.showTrailingButton)
97 + GestureDetector(
98 + onTap: widget.trailingButtonTap,
99 + child: Container(
100 + height: 32,
101 + width: 32,
102 + decoration: BoxDecoration(
103 + shape: BoxShape.circle,
104 + color:
105 + Theme.of(context).extension<ReceivePageTheme>()!.iconsBackgroundColor),
106 + child: widget.trailingIcon,
107 + ),
108 + ),
109 + ],
110 + ),
111 + ],
112 ),
113 );
114 }
lib/src/screens/receive/widgets/qr_widget.dart
+4 -1
@@ -1,3 +1,4 @@
1 +import 'package:auto_size_text/auto_size_text.dart';
2 import 'package:cake_wallet/entities/qr_view_data.dart';
3 import 'package:cake_wallet/themes/extensions/qr_code_theme.dart';
4 import 'package:cake_wallet/routes.dart';
@@ -6,6 +7,7 @@ import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dar
7 import 'package:cake_wallet/utils/brightness_util.dart';
8 import 'package:cake_wallet/utils/show_bar.dart';
9 import 'package:cake_wallet/utils/show_pop_up.dart';
10 +import 'package:cw_core/wallet_type.dart';
11 import 'package:flutter/material.dart';
12 import 'package:flutter/services.dart';
13 import 'package:flutter_mobx/flutter_mobx.dart';
@@ -144,9 +146,10 @@ class QRWidget extends StatelessWidget {
146 crossAxisAlignment: CrossAxisAlignment.start,
147 children: <Widget>[
148 Expanded(
147 - child: Text(
149 + child: AutoSizeText(
150 addressListViewModel.address.address,
151 textAlign: TextAlign.center,
152 + maxLines: addressListViewModel.wallet.type == WalletType.monero ? 2 : 1,
153 style: TextStyle(
154 fontSize: 15,
155 fontWeight: FontWeight.w500,
lib/view_model/settings/privacy_settings_view_model.dart
+5 -1
@@ -38,7 +38,11 @@ abstract class PrivacySettingsViewModelBase with Store {
38 }
39 }
40
41 - bool get isAutoGenerateSubaddressesVisible => _wallet.type == WalletType.monero;
41 + bool get isAutoGenerateSubaddressesVisible =>
42 + _wallet.type == WalletType.monero ||
43 + _wallet.type == WalletType.bitcoin ||
44 + _wallet.type == WalletType.litecoin ||
45 + _wallet.type == WalletType.bitcoinCash;
46
47 @computed
48 bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress;
lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart
+8 -12
@@ -1,6 +1,5 @@
1 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
2 import 'package:mobx/mobx.dart';
3 -import 'package:flutter/foundation.dart';
3 import 'package:cw_core/wallet_base.dart';
4 import 'package:cake_wallet/bitcoin/bitcoin.dart';
5 import 'package:cake_wallet/monero/monero.dart';
@@ -33,7 +32,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
32 state = AddressEditOrCreateStateInitial(),
33 label = item?.name ?? '',
34 _item = item,
36 - _wallet = wallet;
35 + _wallet = wallet;
36
37 @observable
38 AddressEditOrCreateState state;
@@ -46,6 +45,10 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
45 final WalletAddressListItem? _item;
46 final WalletBase _wallet;
47
48 + bool get isElectrum => _wallet.type == WalletType.bitcoin ||
49 + _wallet.type == WalletType.bitcoinCash ||
50 + _wallet.type == WalletType.litecoin;
51 +
52 Future<void> save() async {
53 try {
54 state = AddressIsSaving();
@@ -65,12 +68,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
68 Future<void> _createNew() async {
69 final wallet = _wallet;
70
68 - if (wallet.type == WalletType.bitcoin
69 - || wallet.type == WalletType.litecoin
70 - || wallet.type == WalletType.bitcoinCash) {
71 - await bitcoin!.generateNewAddress(wallet);
72 - await wallet.save();
73 - }
71 + if (isElectrum) await bitcoin!.generateNewAddress(wallet, label);
72
73 if (wallet.type == WalletType.monero) {
74 await monero
@@ -96,10 +94,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
94 Future<void> _update() async {
95 final wallet = _wallet;
96
99 - /*if (wallet is BitcoinWallet) {
100 - await wallet.walletAddresses.updateAddress(_item.address as String);
101 - await wallet.save();
102 - }*/
97 + if (isElectrum) await bitcoin!.updateAddress(wallet, _item!.address, label);
98 +
99 final index = _item?.id;
100 if (index != null) {
101 if (wallet.type == WalletType.monero) {
lib/view_model/wallet_address_list/wallet_address_list_item.dart
+7 -2
@@ -1,4 +1,3 @@
1 -import 'package:flutter/foundation.dart';
1 import 'package:cake_wallet/utils/list_item.dart';
2
3 class WalletAddressListItem extends ListItem {
@@ -6,13 +5,19 @@ class WalletAddressListItem extends ListItem {
5 required this.address,
6 required this.isPrimary,
7 this.id,
9 - this.name})
8 + this.name,
9 + this.txCount,
10 + this.balance,
11 + this.isChange = false})
12 : super();
13
14 final int? id;
15 final bool isPrimary;
16 final String address;
17 final String? name;
18 + final int? txCount;
19 + final String? balance;
20 + final bool isChange;
21
22 @override
23 String toString() => name ?? address;
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+78 -26
@@ -1,21 +1,25 @@
1 +import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
2 -import 'package:cake_wallet/ethereum/ethereum.dart';
3 +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
4 import 'package:cake_wallet/entities/fiat_currency.dart';
5 +import 'package:cake_wallet/ethereum/ethereum.dart';
6 +import 'package:cake_wallet/generated/i18n.dart';
7 +import 'package:cake_wallet/haven/haven.dart';
8 +import 'package:cake_wallet/monero/monero.dart';
9 import 'package:cake_wallet/polygon/polygon.dart';
10 +import 'package:cake_wallet/store/app_store.dart';
11 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
12 +import 'package:cake_wallet/store/settings_store.dart';
13 import 'package:cake_wallet/store/yat/yat_store.dart';
7 -import 'package:cw_core/currency.dart';
8 -import 'package:intl/intl.dart';
9 -import 'package:mobx/mobx.dart';
14 import 'package:cake_wallet/utils/list_item.dart';
15 import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart';
16 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart';
17 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
18 +import 'package:cw_core/amount_converter.dart';
19 +import 'package:cw_core/currency.dart';
20 import 'package:cw_core/wallet_type.dart';
15 -import 'package:cake_wallet/bitcoin/bitcoin.dart';
16 -import 'package:cake_wallet/store/app_store.dart';
17 -import 'package:cake_wallet/monero/monero.dart';
18 -import 'package:cake_wallet/haven/haven.dart';
21 +import 'package:intl/intl.dart';
22 +import 'package:mobx/mobx.dart';
23
24 part 'wallet_address_list_view_model.g.dart';
25
@@ -110,7 +114,8 @@ class EthereumURI extends PaymentURI {
114
115 class BitcoinCashURI extends PaymentURI {
116 BitcoinCashURI({required String amount, required String address})
113 - : super(amount: amount, address: address);
117 + : super(amount: amount, address: address);
118 +
119 @override
120 String toString() {
121 var base = address;
@@ -121,9 +126,7 @@ class BitcoinCashURI extends PaymentURI {
126
127 return base;
128 }
124 - }
125 -
126 -
129 +}
130
131 class NanoURI extends PaymentURI {
132 NanoURI({required String amount, required String address})
@@ -167,6 +170,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
170 hasAccounts =
171 appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven,
172 amount = '',
173 + _settingsStore = appStore.settingsStore,
174 super(appStore: appStore) {
175 _init();
176 }
@@ -184,12 +188,28 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
188 final NumberFormat _cryptoNumberFormat;
189
190 final FiatConversionStore fiatConversionStore;
191 + final SettingsStore _settingsStore;
192
193 List<Currency> get currencies => [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all];
194
195 + String get buttonTitle {
196 + if (isElectrumWallet) {
197 + return S.current.addresses;
198 + }
199 +
200 + if (isAutoGenerateSubaddressEnabled) {
201 + return hasAccounts ? S.current.accounts : S.current.account;
202 + }
203 +
204 + return hasAccounts ? S.current.accounts_subaddresses : S.current.addresses;
205 + }
206 +
207 @observable
208 Currency selectedCurrency;
209
210 + @observable
211 + String searchText = '';
212 +
213 @computed
214 int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency);
215
@@ -277,14 +297,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
297 addressList.addAll(addressItems);
298 }
299
280 - if (wallet.type == WalletType.bitcoin) {
281 - final primaryAddress = bitcoin!.getAddress(wallet);
282 - final bitcoinAddresses = bitcoin!.getAddresses(wallet).map((addr) {
283 - final isPrimary = addr == primaryAddress;
300 + if (isElectrumWallet) {
301 + final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) {
302 + final isPrimary = subaddress.id == 0;
303
285 - return WalletAddressListItem(isPrimary: isPrimary, name: null, address: addr);
304 + return WalletAddressListItem(
305 + id: subaddress.id,
306 + isPrimary: isPrimary,
307 + name: subaddress.name,
308 + address: subaddress.address,
309 + txCount: subaddress.txCount,
310 + balance: AmountConverter.amountIntToString(
311 + walletTypeToCryptoCurrency(type), subaddress.balance),
312 + isChange: subaddress.isChange);
313 });
287 - addressList.addAll(bitcoinAddresses);
314 + addressList.addAll(addressItems);
315 }
316
317 if (wallet.type == WalletType.ethereum) {
@@ -299,6 +326,15 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
326 addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress));
327 }
328
329 + if (searchText.isNotEmpty) {
330 + return ObservableList.of(addressList.where((item) {
331 + if (item is WalletAddressListItem) {
332 + return item.address.toLowerCase().contains(searchText.toLowerCase());
333 + }
334 + return false;
335 + }));
336 + }
337 +
338 return addressList;
339 }
340
@@ -321,15 +357,23 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
357 @computed
358 bool get hasAddressList =>
359 wallet.type == WalletType.monero ||
324 - wallet.type == WalletType.haven;/* ||
325 - wallet.type == WalletType.nano ||
326 - wallet.type == WalletType.banano;*/// TODO: nano accounts are disabled for now
360 + wallet.type == WalletType.haven ||
361 + wallet.type == WalletType.bitcoinCash ||
362 + wallet.type == WalletType.bitcoin ||
363 + wallet.type == WalletType.litecoin;
364 +
365 + // wallet.type == WalletType.nano ||
366 + // wallet.type == WalletType.banano; TODO: nano accounts are disabled for now
367
368 @computed
329 - bool get showElectrumAddressDisclaimer =>
369 + bool get isElectrumWallet =>
370 wallet.type == WalletType.bitcoin ||
331 - wallet.type == WalletType.litecoin ||
332 - wallet.type == WalletType.bitcoinCash;
371 + wallet.type == WalletType.litecoin ||
372 + wallet.type == WalletType.bitcoinCash;
373 +
374 + @computed
375 + bool get isAutoGenerateSubaddressEnabled =>
376 + _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled;
377
378 List<ListItem> _baseItems;
379
@@ -343,9 +387,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
387 _baseItems = [];
388
389 if (wallet.type == WalletType.monero ||
346 - wallet.type == WalletType.haven /*||
390 + wallet.type ==
391 + WalletType
392 + .haven /*||
393 wallet.type == WalletType.nano ||
348 - wallet.type == WalletType.banano*/) {
394 + wallet.type == WalletType.banano*/
395 + ) {
396 _baseItems.add(WalletAccountListHeader());
397 }
398
@@ -367,6 +414,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
414 }
415 }
416
417 + @action
418 + void updateSearchText(String text) {
419 + searchText = text;
420 + }
421 +
422 void _convertAmountToCrypto() {
423 final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type);
424 try {
tool/configure.dart
+23 -1
@@ -64,6 +64,7 @@ import 'package:cw_core/output_info.dart';
64 import 'package:cw_core/unspent_coins_info.dart';
65 import 'package:cw_core/wallet_service.dart';
66 import 'package:cake_wallet/view_model/send/output.dart';
67 +import 'package:cw_core/wallet_type.dart';
68 import 'package:hive/hive.dart';""";
69 const bitcoinCWHeaders = """
70 import 'package:cw_bitcoin/electrum_wallet.dart';
@@ -76,9 +77,27 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart';
77 import 'package:cw_bitcoin/bitcoin_address_record.dart';
78 import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
79 import 'package:cw_bitcoin/litecoin_wallet_service.dart';
80 +import 'package:mobx/mobx.dart';
81 """;
82 const bitcoinCwPart = "part 'cw_bitcoin.dart';";
83 const bitcoinContent = """
84 +
85 + class ElectrumSubAddress {
86 + ElectrumSubAddress({
87 + required this.id,
88 + required this.name,
89 + required this.address,
90 + required this.txCount,
91 + required this.balance,
92 + required this.isChange});
93 + final int id;
94 + final String name;
95 + final String address;
96 + final int txCount;
97 + final int balance;
98 + final bool isChange;
99 +}
100 +
101 abstract class Bitcoin {
102 TransactionPriority getMediumTransactionPriority();
103
@@ -92,13 +111,16 @@ abstract class Bitcoin {
111 TransactionPriority deserializeBitcoinTransactionPriority(int raw);
112 TransactionPriority deserializeLitecoinTransactionPriority(int raw);
113 int getFeeRate(Object wallet, TransactionPriority priority);
95 - Future<void> generateNewAddress(Object wallet);
114 + Future<void> generateNewAddress(Object wallet, String label);
115 + Future<void> updateAddress(Object wallet,String address, String label);
116 Object createBitcoinTransactionCredentials(List<Output> outputs, {required TransactionPriority priority, int? feeRate});
117 Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs, {TransactionPriority? priority, required int feeRate});
118
119 List<String> getAddresses(Object wallet);
120 String getAddress(Object wallet);
121
122 + List<ElectrumSubAddress> getSubAddresses(Object wallet);
123 +
124 String formatterBitcoinAmountToString({required int amount});
125 double formatterBitcoinAmountToDouble({required int amount});
126 int formatterStringDoubleToBitcoinAmount(String amount);