CW-611-Refactor-Address-Handling (#1630)

* subaddress fix * fix subaddress generation * rewrite usedAddresses for xmr and wow * [skip ci] remove print statements * refactor address handling * do not remove manual addresses, just mark them * monero display latest address on receive page when autogenerate is enabled [skip ci] * WIP subaddresses, hidden addresses, and UI improvements for monero * update configure script * fix subaddress generation, display latest address * Update lib/core/wallet_loading_service.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Exclude manually created addresses Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * don't call .save function multiple times Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * - revert usedAddress functinality - add mutex to prevent crashes - fix UI flashing in tx screen - fixes from comments * account index fixes added code to wownero code comment * - added subaddress index - fixed received count also accounting for change (we don't want that) - fix bad state: no element - fix search - fix automatic generation * prevent crashes by acquiring mutex before setting the pointer * - fix ttDetails generation in larger/restored wallets - show manual add icon in monero/wownero even when autogeneration is enabled - disable colors on non-debug builds - cache getAddress call in xmr/wow [skip ci] * fix: silent payment error in address setter enable fancy new features only for xmr / wow * refresh subaddress list, when we add new address fix manual addresses marking * add toggle to hide and show address * update transaction details after restore * show only one address in address book for xmr, wow and haven * fix address book reset address only when autogenerate is on * enable isEnabledAutoGenerateSubaddress on new wallets * hide addresses after exchange only for XMR and WOW * fix: bad-state no element * Update cw_monero/lib/monero_wallet_addresses.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Update cw_monero/lib/monero_wallet_addresses.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * improvements to performance * 0, 0 -> accountIndex, addressIndex * make constant variables final * Update cw_wownero/lib/wownero_wallet_addresses.dart [skip ci] * Update cw_wownero/lib/wownero_wallet_addresses.dart [skip ci] * Update cw_monero/lib/monero_wallet.dart [skip ci] * fix potential exception * fix after removing late * remove orElse, replaced it with a try catch block. fix strings * fix valid seed function * fix null check error [skip ci] * fix updateSubaddressList for wow and haven --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

cyan committed Sep 28, 2024 at 04:38 UTC f8b0c0ad2af05bc5c53e61e7c23522b20322ffd4
35 files changed +776 -243
cw_bitcoin/lib/electrum_wallet.dart
+2 -1
@@ -1772,7 +1772,8 @@ abstract class ElectrumWalletBase
1772 final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type);
1773 final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true);
1774 final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false);
1775 -
1775 + walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address));
1776 + await walletAddresses.saveAddressesInBox();
1777 await Future.wait(addressesByType.map((addressRecord) async {
1778 final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip());
1779
cw_bitcoin/lib/electrum_wallet_addresses.dart
+10 -2
@@ -163,6 +163,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
163
164 @override
165 set address(String addr) {
166 + if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) {
167 + return;
168 + }
169 if (addressPageType == SilentPaymentsAddresType.p2sp) {
170 final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr);
171
@@ -174,12 +177,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
177 }
178 return;
179 }
177 -
178 - final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr);
180 + try {
181 + final addressRecord = _addresses.firstWhere(
182 + (addressRecord) => addressRecord.address == addr,
183 + );
184
185 previousAddressRecord = addressRecord;
186 receiveAddresses.remove(addressRecord);
187 receiveAddresses.insert(0, addressRecord);
188 + } catch (e) {
189 + print("ElectrumWalletAddressBase: set address ($addr): $e");
190 + }
191 }
192
193 @override
cw_core/lib/subaddress.dart
+12 -2
@@ -1,12 +1,22 @@
1 class Subaddress {
2 - Subaddress({required this.id, required this.address, required this.label});
2 + Subaddress({
3 + required this.id,
4 + required this.address,
5 + required this.label,
6 + this.balance = null,
7 + this.txCount = null,
8 + });
9
10 Subaddress.fromMap(Map<String, Object?> map)
11 : this.id = map['id'] == null ? 0 : int.parse(map['id'] as String),
12 this.address = (map['address'] ?? '') as String,
7 - this.label = (map['label'] ?? '') as String;
13 + this.label = (map['label'] ?? '') as String,
14 + this.balance = (map['balance'] ?? '') as String?,
15 + this.txCount = (map['txCount'] ?? '') as int?;
16
17 final int id;
18 final String address;
19 final String label;
20 + final String? balance;
21 + final int? txCount;
22 }
cw_core/lib/wallet_addresses.dart
+37 -3
@@ -1,26 +1,58 @@
1 import 'package:cw_core/address_info.dart';
2 import 'package:cw_core/wallet_info.dart';
3 +import 'package:cw_core/wallet_type.dart';
4
5 abstract class WalletAddresses {
6 WalletAddresses(this.walletInfo)
7 : addressesMap = {},
8 allAddressesMap = {},
8 - addressInfos = {};
9 + addressInfos = {},
10 + usedAddresses = {},
11 + hiddenAddresses = walletInfo.hiddenAddresses?.toSet() ?? {},
12 + manualAddresses = walletInfo.manualAddresses?.toSet() ?? {};
13
14 final WalletInfo walletInfo;
15
16 String get address;
17
18 + String get latestAddress {
19 + if (walletInfo.type == WalletType.monero || walletInfo.type == WalletType.wownero) {
20 + if (addressesMap.keys.length == 0) return address;
21 + return addressesMap[addressesMap.keys.last] ?? address;
22 + }
23 + return _localAddress ?? address;
24 + }
25 +
26 String? get primaryAddress => null;
27
16 - set address(String address);
28 + String? _localAddress;
29 +
30 + set address(String address) => _localAddress = address;
31 +
32 + String get addressForExchange => address;
33
34 Map<String, String> addressesMap;
35 Map<String, String> allAddressesMap;
36
37 + Map<String, String> get usableAddressesMap {
38 + final tmp = addressesMap.map((key, value) => MapEntry(key, value)); // copy address map
39 + tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key));
40 + return tmp;
41 + }
42 +
43 + Map<String, String> get usableAllAddressesMap {
44 + final tmp = allAddressesMap.map((key, value) => MapEntry(key, value)); // copy address map
45 + tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key));
46 + return tmp;
47 + }
48 +
49 Map<int, List<AddressInfo>> addressInfos;
50
23 - Set<String> usedAddresses = {};
51 + Set<String> usedAddresses;
52 +
53 + Set<String> hiddenAddresses;
54 +
55 + Set<String> manualAddresses;
56
57 Future<void> init();
58
@@ -32,6 +64,8 @@ abstract class WalletAddresses {
64 walletInfo.addresses = addressesMap;
65 walletInfo.addressInfos = addressInfos;
66 walletInfo.usedAddresses = usedAddresses.toList();
67 + walletInfo.hiddenAddresses = hiddenAddresses.toList();
68 + walletInfo.manualAddresses = manualAddresses.toList();
69
70 if (walletInfo.isInBox) {
71 await walletInfo.save();
cw_core/lib/wallet_info.dart
+9
@@ -189,6 +189,15 @@ class WalletInfo extends HiveObject {
189
190 @HiveField(22)
191 String? parentAddress;
192 +
193 + @HiveField(23)
194 + List<String>? hiddenAddresses;
195 +
196 + @HiveField(24)
197 + List<String>? manualAddresses;
198 +
199 +
200 +
201
202 String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';
203
cw_haven/lib/haven_wallet_addresses.dart
+5 -3
@@ -1,6 +1,7 @@
1 import 'package:cw_core/wallet_addresses_with_account.dart';
2 import 'package:cw_core/wallet_info.dart';
3 import 'package:cw_core/account.dart';
4 +import 'package:cw_haven/api/wallet.dart';
5 import 'package:cw_haven/haven_account_list.dart';
6 import 'package:cw_haven/haven_subaddress_list.dart';
7 import 'package:cw_core/subaddress.dart';
@@ -36,7 +37,7 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount<Accou
37 @override
38 Future<void> init() async {
39 accountList.update();
39 - account = accountList.accounts.first;
40 + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first;
41 updateSubaddressList(accountIndex: account?.id ?? 0);
42 await updateAddressesInBox();
43 }
@@ -81,8 +82,9 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount<Accou
82
83 void updateSubaddressList({required int accountIndex}) {
84 subaddressList.update(accountIndex: accountIndex);
84 - subaddress = subaddressList.subaddresses.first;
85 - address = subaddress!.address;
85 + address = subaddressList.subaddresses.isNotEmpty
86 + ? subaddressList.subaddresses.first.address
87 + : getAddress();
88 }
89
90 @override
cw_monero/lib/api/subaddress_list.dart
+75 -7
@@ -1,5 +1,6 @@
1
2 import 'package:cw_monero/api/account_list.dart';
3 +import 'package:cw_monero/api/transaction_history.dart';
4 import 'package:cw_monero/api/wallet.dart';
5 import 'package:monero/monero.dart' as monero;
6
@@ -14,6 +15,10 @@ class SubaddressInfoMetadata {
15
16 SubaddressInfoMetadata? subaddress = null;
17
18 +String getRawLabel({required int accountIndex, required int addressIndex}) {
19 + return monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
20 +}
21 +
22 void refreshSubaddresses({required int accountIndex}) {
23 try {
24 isUpdating = true;
@@ -29,31 +34,94 @@ class Subaddress {
34 Subaddress({
35 required this.addressIndex,
36 required this.accountIndex,
37 + required this.received,
38 + required this.txCount,
39 });
33 - String get address => monero.Wallet_address(
34 - wptr!,
35 - accountIndex: accountIndex,
36 - addressIndex: addressIndex,
37 - );
40 + late String address = getAddress(
41 + accountIndex: accountIndex,
42 + addressIndex: addressIndex,
43 + );
44 final int addressIndex;
45 final int accountIndex;
40 - String get label => monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
46 + final int received;
47 + final int txCount;
48 + String get label {
49 + final localLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
50 + if (localLabel.startsWith("#$addressIndex")) return localLabel; // don't duplicate the ID if it was user-providen
51 + return "#$addressIndex ${localLabel}".trim();
52 + }
53 }
54
55 +class TinyTransactionDetails {
56 + TinyTransactionDetails({
57 + required this.address,
58 + required this.amount,
59 + });
60 + final List<String> address;
61 + final int amount;
62 +}
63 +
64 +int lastWptr = 0;
65 +int lastTxCount = 0;
66 +List<TinyTransactionDetails> ttDetails = [];
67 +
68 List<Subaddress> getAllSubaddresses() {
69 + txhistory = monero.Wallet_history(wptr!);
70 + final txCount = monero.TransactionHistory_count(txhistory!);
71 + if (lastTxCount != txCount && lastWptr != wptr!.address) {
72 + final List<TinyTransactionDetails> newttDetails = [];
73 + lastTxCount = txCount;
74 + lastWptr = wptr!.address;
75 + for (var i = 0; i < txCount; i++) {
76 + final tx = monero.TransactionHistory_transaction(txhistory!, index: i);
77 + if (monero.TransactionInfo_direction(tx) == monero.TransactionInfo_Direction.Out) continue;
78 + final subaddrs = monero.TransactionInfo_subaddrIndex(tx).split(",");
79 + final account = monero.TransactionInfo_subaddrAccount(tx);
80 + newttDetails.add(TinyTransactionDetails(
81 + address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)),
82 + amount: monero.TransactionInfo_amount(tx),
83 + ));
84 + }
85 + ttDetails.clear();
86 + ttDetails.addAll(newttDetails);
87 + }
88 final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex);
89 final list = List.generate(size, (index) {
90 + final ttDetailsLocal = ttDetails.where((element) {
91 + final address = getAddress(
92 + accountIndex: subaddress!.accountIndex,
93 + addressIndex: index,
94 + );
95 + if (element.address.contains(address)) return true;
96 + return false;
97 + }).toList();
98 + int received = 0;
99 + for (var i = 0; i < ttDetailsLocal.length; i++) {
100 + received += ttDetailsLocal[i].amount;
101 + }
102 return Subaddress(
103 accountIndex: subaddress!.accountIndex,
104 addressIndex: index,
105 + received: received,
106 + txCount: ttDetailsLocal.length,
107 );
108 }).reversed.toList();
109 if (list.length == 0) {
52 - list.add(Subaddress(addressIndex: subaddress!.accountIndex, accountIndex: 0));
110 + list.add(
111 + Subaddress(
112 + addressIndex: subaddress!.accountIndex,
113 + accountIndex: 0,
114 + received: 0,
115 + txCount: 0,
116 + ));
117 }
118 return list;
119 }
120
121 +int numSubaddresses(int subaccountIndex) {
122 + return monero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex);
123 +}
124 +
125 void addSubaddressSync({required int accountIndex, required String label}) {
126 monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex, label: label);
127 refreshSubaddresses(accountIndex: accountIndex);
cw_monero/lib/api/transaction_history.dart
+38 -13
@@ -5,32 +5,42 @@ import 'package:cw_monero/api/account_list.dart';
5 import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart';
6 import 'package:cw_monero/api/monero_output.dart';
7 import 'package:cw_monero/api/structs/pending_transaction.dart';
8 +import 'package:cw_monero/api/wallet.dart';
9 import 'package:ffi/ffi.dart';
10 import 'package:monero/monero.dart' as monero;
11 import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen;
12 +import 'package:mutex/mutex.dart';
13
14
15 String getTxKey(String txId) {
16 return monero.Wallet_getTxKey(wptr!, txid: txId);
17 }
16 -
18 +final txHistoryMutex = Mutex();
19 monero.TransactionHistory? txhistory;
18 -
19 -void refreshTransactions() {
20 +bool isRefreshingTx = false;
21 +Future<void> refreshTransactions() async {
22 + if (isRefreshingTx == true) return;
23 + isRefreshingTx = true;
24 txhistory ??= monero.Wallet_history(wptr!);
21 - monero.TransactionHistory_refresh(txhistory!);
25 + final ptr = txhistory!.address;
26 + await txHistoryMutex.acquire();
27 + await Isolate.run(() {
28 + monero.TransactionHistory_refresh(Pointer.fromAddress(ptr));
29 + });
30 + txHistoryMutex.release();
31 + isRefreshingTx = false;
32 }
33
34 int countOfTransactions() => monero.TransactionHistory_count(txhistory!);
35
26 -List<Transaction> getAllTransactions() {
36 +Future<List<Transaction>> getAllTransactions() async {
37 List<Transaction> dummyTxs = [];
28 -
38 +
39 + await txHistoryMutex.acquire();
40 txhistory ??= monero.Wallet_history(wptr!);
30 - monero.TransactionHistory_refresh(txhistory!);
41 int size = countOfTransactions();
42 final list = List.generate(size, (index) => Transaction(txInfo: monero.TransactionHistory_transaction(txhistory!, index: index)));
33 -
43 + txHistoryMutex.release();
44 final accts = monero.Wallet_numSubaddressAccounts(wptr!);
45 for (var i = 0; i < accts; i++) {
46 final fullBalance = monero.Wallet_balance(wptr!, accountIndex: i);
@@ -45,6 +55,8 @@ List<Transaction> getAllTransactions() {
55 confirmations: 0,
56 blockheight: 0,
57 accountIndex: i,
58 + addressIndex: 0,
59 + addressIndexList: [0],
60 paymentId: "",
61 amount: fullBalance - availBalance,
62 isSpend: false,
@@ -251,19 +263,28 @@ Future<PendingTransactionDescription> createTransactionMultDest(
263
264 class Transaction {
265 final String displayLabel;
254 - String subaddressLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0);
255 - late final String address = monero.Wallet_address(
266 + late final String subaddressLabel = monero.Wallet_getSubaddressLabel(
267 wptr!,
257 - accountIndex: 0,
258 - addressIndex: 0,
268 + accountIndex: accountIndex,
269 + addressIndex: addressIndex,
270 + );
271 + late final String address = getAddress(
272 + accountIndex: accountIndex,
273 + addressIndex: addressIndex,
274 );
275 + late final List<String> addressList = List.generate(addressIndexList.length, (index) =>
276 + getAddress(
277 + accountIndex: accountIndex,
278 + addressIndex: addressIndexList[index],
279 + ));
280 final String description;
281 final int fee;
282 final int confirmations;
283 late final bool isPending = confirmations < 10;
284 final int blockheight;
265 - final int addressIndex = 0;
285 + final int addressIndex;
286 final int accountIndex;
287 + final List<int> addressIndexList;
288 final String paymentId;
289 final int amount;
290 final bool isSpend;
@@ -309,6 +330,8 @@ class Transaction {
330 amount = monero.TransactionInfo_amount(txInfo),
331 paymentId = monero.TransactionInfo_paymentId(txInfo),
332 accountIndex = monero.TransactionInfo_subaddrAccount(txInfo),
333 + addressIndex = int.tryParse(monero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0,
334 + addressIndexList = monero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(),
335 blockheight = monero.TransactionInfo_blockHeight(txInfo),
336 confirmations = monero.TransactionInfo_confirmations(txInfo),
337 fee = monero.TransactionInfo_fee(txInfo),
@@ -331,6 +354,8 @@ class Transaction {
354 required this.confirmations,
355 required this.blockheight,
356 required this.accountIndex,
357 + required this.addressIndexList,
358 + required this.addressIndex,
359 required this.paymentId,
360 required this.amount,
361 required this.isSpend,
cw_monero/lib/api/wallet.dart
+13 -2
@@ -66,9 +66,20 @@ String getSeedLegacy(String? language) {
66 return legacy;
67 }
68
69 -String getAddress({int accountIndex = 0, int addressIndex = 0}) =>
70 - monero.Wallet_address(wptr!,
69 +Map<int, Map<int, Map<int, String>>> addressCache = {};
70 +
71 +String getAddress({int accountIndex = 0, int addressIndex = 0}) {
72 + // print("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}");
73 + while (monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) {
74 + print("adding subaddress");
75 + monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex);
76 + }
77 + addressCache[wptr!.address] ??= {};
78 + addressCache[wptr!.address]![accountIndex] ??= {};
79 + addressCache[wptr!.address]![accountIndex]![addressIndex] ??= monero.Wallet_address(wptr!,
80 accountIndex: accountIndex, addressIndex: addressIndex);
81 + return addressCache[wptr!.address]![accountIndex]![addressIndex]!;
82 +}
83
84 int getFullBalance({int accountIndex = 0}) =>
85 monero.Wallet_balance(wptr!, accountIndex: accountIndex);
cw_monero/lib/monero_subaddress_list.dart
+12 -12
@@ -1,6 +1,7 @@
1 import 'package:cw_core/subaddress.dart';
2 import 'package:cw_monero/api/coins_info.dart';
3 import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list;
4 +import 'package:cw_monero/api/wallet.dart';
5 import 'package:flutter/services.dart';
6 import 'package:mobx/mobx.dart';
7
@@ -54,18 +55,12 @@ abstract class MoneroSubaddressListBase with Store {
55 final address = s.address;
56 final label = s.label;
57 final id = s.addressIndex;
57 - final hasDefaultAddressName =
58 - label.toLowerCase() == 'Primary account'.toLowerCase() ||
59 - label.toLowerCase() == 'Untitled account'.toLowerCase();
60 - final isPrimaryAddress = id == 0 && hasDefaultAddressName;
58 return Subaddress(
59 id: id,
60 address: address,
64 - label: isPrimaryAddress
65 - ? 'Primary address'
66 - : hasDefaultAddressName
67 - ? ''
68 - : label);
61 + balance: (s.received/1e12).toStringAsFixed(6),
62 + txCount: s.txCount,
63 + label: label);
64 }).toList();
65 }
66
@@ -103,6 +98,9 @@ abstract class MoneroSubaddressListBase with Store {
98 required List<String> usedAddresses,
99 }) async {
100 _usedAddresses.addAll(usedAddresses);
101 + final _all = _usedAddresses.toSet().toList();
102 + _usedAddresses.clear();
103 + _usedAddresses.addAll(_all);
104 if (_isUpdating) {
105 return;
106 }
@@ -124,7 +122,8 @@ abstract class MoneroSubaddressListBase with Store {
122 Future<List<Subaddress>> _getAllUnusedAddresses(
123 {required int accountIndex, required String label}) async {
124 final allAddresses = subaddress_list.getAllSubaddresses();
127 - if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last)) {
125 + // first because addresses come in reversed order.
126 + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.first.address)) {
127 final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label);
128 if (!isAddressUnused) {
129 return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label);
@@ -139,12 +138,13 @@ abstract class MoneroSubaddressListBase with Store {
138 return Subaddress(
139 id: id,
140 address: address,
141 + balance: (s.received/1e12).toStringAsFixed(6),
142 + txCount: s.txCount,
143 label: id == 0 &&
144 label.toLowerCase() == 'Primary account'.toLowerCase()
145 ? 'Primary address'
146 : label);
146 - })
147 - .toList();
147 + }).toList().reversed.toList();
148 }
149
150 Future<bool> _newSubaddress({required int accountIndex, required String label}) async {
cw_monero/lib/monero_wallet.dart
+13 -6
@@ -59,7 +59,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
59 }),
60 _isTransactionUpdating = false,
61 _hasSyncAfterStartup = false,
62 - isEnabledAutoGenerateSubaddress = false,
62 + isEnabledAutoGenerateSubaddress = true,
63 _password = password,
64 syncStatus = NotConnectedSyncStatus(),
65 unspentCoins = [],
@@ -86,6 +86,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
86 reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) {
87 _updateSubAddress(enabled, account: walletAddresses.account);
88 });
89 + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) {
90 + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account);
91 + });
92 }
93
94 static const int _autoSaveInterval = 30;
@@ -128,6 +131,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
131
132 monero_wallet.SyncListener? _listener;
133 ReactionDisposer? _onAccountChangeReaction;
134 + ReactionDisposer? _onTxHistoryChangeReaction;
135 bool _isTransactionUpdating;
136 bool _hasSyncAfterStartup;
137 Timer? _autoSaveTimer;
@@ -158,6 +162,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
162
163 _autoSaveTimer = Timer.periodic(
164 Duration(seconds: _autoSaveInterval), (_) async => await save());
165 + // update transaction details after restore
166 + walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id??0);
167 }
168
169 @override
@@ -167,6 +173,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
173 void close() async {
174 _listener?.stop();
175 _onAccountChangeReaction?.reaction.dispose();
176 + _onTxHistoryChangeReaction?.reaction.dispose();
177 _autoSaveTimer?.cancel();
178 }
179
@@ -578,7 +585,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
585 @override
586 Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
587 transaction_history.refreshTransactions();
581 - return _getAllTransactionsOfAccount(walletAddresses.account?.id)
588 + return (await _getAllTransactionsOfAccount(walletAddresses.account?.id))
589 .fold<Map<String, MoneroTransactionInfo>>(
590 <String, MoneroTransactionInfo>{},
591 (Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
@@ -594,8 +601,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
601 }
602
603 _isTransactionUpdating = true;
597 - transactionHistory.clear();
604 final transactions = await fetchTransactions();
605 + transactionHistory.clear();
606 transactionHistory.addMany(transactions);
607 await transactionHistory.save();
608 _isTransactionUpdating = false;
@@ -608,9 +615,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
615 String getSubaddressLabel(int accountIndex, int addressIndex) =>
616 monero_wallet.getSubaddressLabel(accountIndex, addressIndex);
617
611 - List<MoneroTransactionInfo> _getAllTransactionsOfAccount(int? accountIndex) =>
612 - transaction_history
613 - .getAllTransactions()
618 + Future<List<MoneroTransactionInfo>> _getAllTransactionsOfAccount(int? accountIndex) async =>
619 + (await transaction_history
620 + .getAllTransactions())
621 .map(
622 (row) => MoneroTransactionInfo(
623 row.hash,
cw_monero/lib/monero_wallet_addresses.dart
+36 -4
@@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart';
3 import 'package:cw_core/subaddress.dart';
4 import 'package:cw_core/wallet_addresses.dart';
5 import 'package:cw_core/wallet_info.dart';
6 +import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list;
7 +import 'package:cw_monero/api/transaction_history.dart';
8 import 'package:cw_monero/api/wallet.dart';
9 import 'package:cw_monero/monero_account_list.dart';
10 import 'package:cw_monero/monero_subaddress_list.dart';
@@ -27,6 +29,30 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
29 @observable
30 String address;
31
32 + @override
33 + String get latestAddress {
34 + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
35 + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
36 + while (hiddenAddresses.contains(address)) {
37 + addressIndex++;
38 + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
39 + subaddressList.update(accountIndex: account?.id??0);
40 + }
41 + return address;
42 + }
43 +
44 + @override
45 + String get addressForExchange {
46 + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
47 + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
48 + while (hiddenAddresses.contains(address) || manualAddresses.contains(address) || subaddress_list.getRawLabel(accountIndex: account?.id??0, addressIndex: addressIndex).isNotEmpty) {
49 + addressIndex++;
50 + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
51 + subaddressList.update(accountIndex: account?.id??0);
52 + }
53 + return address;
54 + }
55 +
56 @observable
57 Account? account;
58
@@ -37,10 +63,12 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
63
64 MoneroAccountList accountList;
65
66 + Set<String> usedAddresses = Set();
67 +
68 @override
69 Future<void> init() async {
70 accountList.update();
43 - account = accountList.accounts.first;
71 + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first;
72 updateSubaddressList(accountIndex: account?.id ?? 0);
73 await updateAddressesInBox();
74 }
@@ -89,8 +117,9 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
117
118 void updateSubaddressList({required int accountIndex}) {
119 subaddressList.update(accountIndex: accountIndex);
92 - subaddress = subaddressList.subaddresses.first;
93 - address = subaddress!.address;
120 + address = subaddressList.subaddresses.isNotEmpty
121 + ? subaddressList.subaddresses.first.address
122 + : getAddress();
123 }
124
125 Future<void> updateUsedSubaddress() async {
@@ -109,7 +138,10 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
138 accountIndex: accountIndex,
139 defaultLabel: defaultLabel,
140 usedAddresses: usedAddresses.toList());
112 - subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last;
141 + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last;
142 + if (num.tryParse(subaddress!.balance??'0') != 0) {
143 + getAddress(accountIndex: accountIndex, addressIndex: (subaddress?.id??0)+1);
144 + }
145 address = subaddress!.address;
146 }
147
cw_wownero/lib/api/subaddress_list.dart
+59 -6
@@ -1,4 +1,5 @@
1 import 'package:cw_wownero/api/account_list.dart';
2 +import 'package:cw_wownero/api/transaction_history.dart';
3 import 'package:cw_wownero/api/wallet.dart';
4 import 'package:monero/wownero.dart' as wownero;
5
@@ -28,27 +29,75 @@ class Subaddress {
29 Subaddress({
30 required this.addressIndex,
31 required this.accountIndex,
32 + required this.txCount,
33 + required this.received,
34 });
32 - String get address => wownero.Wallet_address(
33 - wptr!,
34 - accountIndex: accountIndex,
35 - addressIndex: addressIndex,
36 - );
35 + late String address = getAddress(
36 + accountIndex: accountIndex,
37 + addressIndex: addressIndex,
38 + );
39 final int addressIndex;
40 final int accountIndex;
41 String get label => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
42 + final int txCount;
43 + final int received;
44 +}
45 +
46 +class TinyTransactionDetails {
47 + TinyTransactionDetails({
48 + required this.address,
49 + required this.amount,
50 + });
51 + final List<String> address;
52 + final int amount;
53 }
54
55 +int lastWptr = 0;
56 +int lastTxCount = 0;
57 +List<TinyTransactionDetails> ttDetails = [];
58 +
59 List<Subaddress> getAllSubaddresses() {
60 + txhistory = wownero.Wallet_history(wptr!);
61 + final txCount = wownero.TransactionHistory_count(txhistory!);
62 + if (lastTxCount != txCount && lastWptr != wptr!.address) {
63 + final List<TinyTransactionDetails> newttDetails = [];
64 + lastTxCount = txCount;
65 + lastWptr = wptr!.address;
66 + for (var i = 0; i < txCount; i++) {
67 + final tx = wownero.TransactionHistory_transaction(txhistory!, index: i);
68 + final subaddrs = wownero.TransactionInfo_subaddrIndex(tx).split(",");
69 + final account = wownero.TransactionInfo_subaddrAccount(tx);
70 + newttDetails.add(TinyTransactionDetails(
71 + address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)),
72 + amount: wownero.TransactionInfo_amount(tx),
73 + ));
74 + }
75 + ttDetails.clear();
76 + ttDetails.addAll(newttDetails);
77 + }
78 final size = wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex);
79 final list = List.generate(size, (index) {
80 + final ttDetailsLocal = ttDetails.where((element) {
81 + final address = getAddress(
82 + accountIndex: subaddress!.accountIndex,
83 + addressIndex: index,
84 + );
85 + if (address == element.address) return true;
86 + return false;
87 + }).toList();
88 + int received = 0;
89 + for (var i = 0; i < ttDetailsLocal.length; i++) {
90 + received += ttDetailsLocal[i].amount;
91 + }
92 return Subaddress(
93 accountIndex: subaddress!.accountIndex,
94 addressIndex: index,
95 + received: received,
96 + txCount: ttDetailsLocal.length,
97 );
98 }).reversed.toList();
99 if (list.isEmpty) {
51 - list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex));
100 + list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0));
101 }
102 return list;
103 }
@@ -58,6 +107,10 @@ void addSubaddressSync({required int accountIndex, required String label}) {
107 refreshSubaddresses(accountIndex: accountIndex);
108 }
109
110 +int numSubaddresses(int subaccountIndex) {
111 + return wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex);
112 +}
113 +
114 void setLabelForSubaddressSync(
115 {required int accountIndex, required int addressIndex, required String label}) {
116 wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label);
cw_wownero/lib/api/transaction_history.dart
+28 -9
@@ -3,6 +3,7 @@ import 'dart:isolate';
3
4 import 'package:cw_wownero/api/account_list.dart';
5 import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart';
6 +import 'package:cw_wownero/api/wallet.dart';
7 import 'package:cw_wownero/api/wownero_output.dart';
8 import 'package:cw_wownero/api/structs/pending_transaction.dart';
9 import 'package:ffi/ffi.dart';
@@ -16,9 +17,16 @@ String getTxKey(String txId) {
17
18 wownero.TransactionHistory? txhistory;
19
19 -void refreshTransactions() {
20 +bool isRefreshingTx = false;
21 +Future<void> refreshTransactions() async {
22 + if (isRefreshingTx == true) return;
23 + isRefreshingTx = true;
24 txhistory ??= wownero.Wallet_history(wptr!);
21 - wownero.TransactionHistory_refresh(txhistory!);
25 + final ptr = txhistory!.address;
26 + await Isolate.run(() {
27 + wownero.TransactionHistory_refresh(Pointer.fromAddress(ptr));
28 + });
29 + isRefreshingTx = false;
30 }
31
32 int countOfTransactions() => wownero.TransactionHistory_count(txhistory!);
@@ -45,6 +53,8 @@ List<Transaction> getAllTransactions() {
53 confirmations: 0,
54 blockheight: 0,
55 accountIndex: i,
56 + addressIndex: 0,
57 + addressIndexList: [0],
58 paymentId: "",
59 amount: fullBalance - availBalance,
60 isSpend: false,
@@ -243,23 +253,28 @@ Future<PendingTransactionDescription> createTransactionMultDest(
253
254 class Transaction {
255 final String displayLabel;
246 - String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0);
247 - late final String address = wownero.Wallet_address(
248 - wptr!,
249 - accountIndex: 0,
250 - addressIndex: 0,
256 + late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
257 + late final String address = getAddress(
258 + accountIndex: accountIndex,
259 + addressIndex: addressIndex,
260 );
261 + late final List<String> addressList = List.generate(addressIndexList.length, (index) =>
262 + getAddress(
263 + accountIndex: accountIndex,
264 + addressIndex: addressIndexList[index],
265 + ));
266 final String description;
267 final int fee;
268 final int confirmations;
269 late final bool isPending = confirmations < 3;
270 final int blockheight;
257 - final int addressIndex = 0;
271 + final int addressIndex;
272 final int accountIndex;
273 + final List<int> addressIndexList;
274 final String paymentId;
275 final int amount;
276 final bool isSpend;
262 - late DateTime timeStamp;
277 + late final DateTime timeStamp;
278 late final bool isConfirmed = !isPending;
279 final String hash;
280 final String key;
@@ -301,6 +316,8 @@ class Transaction {
316 amount = wownero.TransactionInfo_amount(txInfo),
317 paymentId = wownero.TransactionInfo_paymentId(txInfo),
318 accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo),
319 + addressIndex = int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0,
320 + addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(),
321 blockheight = wownero.TransactionInfo_blockHeight(txInfo),
322 confirmations = wownero.TransactionInfo_confirmations(txInfo),
323 fee = wownero.TransactionInfo_fee(txInfo),
@@ -314,6 +331,8 @@ class Transaction {
331 required this.confirmations,
332 required this.blockheight,
333 required this.accountIndex,
334 + required this.addressIndex,
335 + required this.addressIndexList,
336 required this.paymentId,
337 required this.amount,
338 required this.isSpend,
cw_wownero/lib/api/wallet.dart
+11 -2
@@ -67,10 +67,19 @@ String getSeedLegacy(String? language) {
67 }
68 return legacy;
69 }
70 +Map<int, Map<int, Map<int, String>>> addressCache = {};
71
71 -String getAddress({int accountIndex = 0, int addressIndex = 1}) =>
72 - wownero.Wallet_address(wptr!,
72 +String getAddress({int accountIndex = 0, int addressIndex = 1}) {
73 + while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) {
74 + print("adding subaddress");
75 + wownero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex);
76 + }
77 + addressCache[wptr!.address] ??= {};
78 + addressCache[wptr!.address]![accountIndex] ??= {};
79 + addressCache[wptr!.address]![accountIndex]![addressIndex] ??= wownero.Wallet_address(wptr!,
80 accountIndex: accountIndex, addressIndex: addressIndex);
81 + return addressCache[wptr!.address]![accountIndex]![addressIndex]!;
82 +}
83
84 int getFullBalance({int accountIndex = 0}) =>
85 wownero.Wallet_balance(wptr!, accountIndex: accountIndex);
cw_wownero/lib/wownero_subaddress_list.dart
+8
@@ -1,6 +1,7 @@
1 import 'package:cw_core/subaddress.dart';
2 import 'package:cw_wownero/api/coins_info.dart';
3 import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list;
4 +import 'package:cw_wownero/api/wallet.dart';
5 import 'package:flutter/services.dart';
6 import 'package:mobx/mobx.dart';
7
@@ -61,6 +62,8 @@ abstract class WowneroSubaddressListBase with Store {
62 return Subaddress(
63 id: id,
64 address: address,
65 + balance: (s.received/1e12).toStringAsFixed(6),
66 + txCount: s.txCount,
67 label: isPrimaryAddress
68 ? 'Primary address'
69 : hasDefaultAddressName
@@ -103,6 +106,9 @@ abstract class WowneroSubaddressListBase with Store {
106 required List<String> usedAddresses,
107 }) async {
108 _usedAddresses.addAll(usedAddresses);
109 + final _all = _usedAddresses.toSet().toList();
110 + _usedAddresses.clear();
111 + _usedAddresses.addAll(_all);
112 if (_isUpdating) {
113 return;
114 }
@@ -140,6 +146,8 @@ abstract class WowneroSubaddressListBase with Store {
146 return Subaddress(
147 id: id,
148 address: address,
149 + balance: (s.received/1e12).toStringAsFixed(6),
150 + txCount: s.txCount,
151 label: id == 0 &&
152 label.toLowerCase() == 'Primary account'.toLowerCase()
153 ? 'Primary address'
cw_wownero/lib/wownero_wallet.dart
+8 -2
@@ -59,7 +59,7 @@ abstract class WowneroWalletBase
59 _isTransactionUpdating = false,
60 _hasSyncAfterStartup = false,
61 _password = password,
62 - isEnabledAutoGenerateSubaddress = false,
62 + isEnabledAutoGenerateSubaddress = true,
63 syncStatus = NotConnectedSyncStatus(),
64 unspentCoins = [],
65 this.unspentCoinsInfo = unspentCoinsInfo,
@@ -82,6 +82,10 @@ abstract class WowneroWalletBase
82 reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) {
83 _updateSubAddress(enabled, account: walletAddresses.account);
84 });
85 +
86 + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) {
87 + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account);
88 + });
89 }
90
91 static const int _autoSaveInterval = 30;
@@ -123,6 +127,7 @@ abstract class WowneroWalletBase
127
128 wownero_wallet.SyncListener? _listener;
129 ReactionDisposer? _onAccountChangeReaction;
130 + ReactionDisposer? _onTxHistoryChangeReaction;
131 bool _isTransactionUpdating;
132 bool _hasSyncAfterStartup;
133 Timer? _autoSaveTimer;
@@ -158,6 +163,7 @@ abstract class WowneroWalletBase
163 void close() async {
164 _listener?.stop();
165 _onAccountChangeReaction?.reaction.dispose();
166 + _onTxHistoryChangeReaction?.reaction.dispose();
167 _autoSaveTimer?.cancel();
168 }
169
@@ -564,8 +570,8 @@ abstract class WowneroWalletBase
570 }
571
572 _isTransactionUpdating = true;
567 - transactionHistory.clear();
573 final transactions = await fetchTransactions();
574 + transactionHistory.clear();
575 transactionHistory.addMany(transactions);
576 await transactionHistory.save();
577 _isTransactionUpdating = false;
cw_wownero/lib/wownero_wallet_addresses.dart
+31 -4
@@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart';
3 import 'package:cw_core/subaddress.dart';
4 import 'package:cw_core/wallet_addresses.dart';
5 import 'package:cw_core/wallet_info.dart';
6 +import 'package:cw_wownero/api/transaction_history.dart';
7 +import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list;
8 import 'package:cw_wownero/api/wallet.dart';
9 import 'package:cw_wownero/wownero_account_list.dart';
10 import 'package:cw_wownero/wownero_subaddress_list.dart';
@@ -27,6 +29,27 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
29 @observable
30 String address;
31
32 + @override
33 + String get latestAddress {
34 + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
35 + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
36 + while (hiddenAddresses.contains(address)) {
37 + addressIndex++;
38 + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
39 + }
40 + return address;
41 + }
42 +
43 + @override
44 + String get addressForExchange {
45 + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
46 + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
47 + while (hiddenAddresses.contains(address) || manualAddresses.contains(address)) {
48 + addressIndex++;
49 + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
50 + }
51 + return address;
52 + }
53 @observable
54 Account? account;
55
@@ -36,11 +59,14 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
59 WowneroSubaddressList subaddressList;
60
61 WowneroAccountList accountList;
62 +
63 + @override
64 + Set<String> usedAddresses = Set();
65
66 @override
67 Future<void> init() async {
68 accountList.update();
43 - account = accountList.accounts.first;
69 + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first;
70 updateSubaddressList(accountIndex: account?.id ?? 0);
71 await updateAddressesInBox();
72 }
@@ -89,8 +115,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
115
116 void updateSubaddressList({required int accountIndex}) {
117 subaddressList.update(accountIndex: accountIndex);
92 - subaddress = subaddressList.subaddresses.first;
93 - address = subaddress!.address;
118 + address = subaddressList.subaddresses.isNotEmpty
119 + ? subaddressList.subaddresses.first.address
120 + : getAddress();
121 }
122
123 Future<void> updateUsedSubaddress() async {
@@ -109,7 +136,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
136 accountIndex: accountIndex,
137 defaultLabel: defaultLabel,
138 usedAddresses: usedAddresses.toList());
112 - subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last;
139 + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last;
140 address = subaddress!.address;
141 }
142
lib/monero/cw_monero.dart
+14 -3
@@ -61,7 +61,13 @@ class CWMoneroSubaddressList extends MoneroSubaddressList {
61 ObservableList<Subaddress> get subaddresses {
62 final moneroWallet = _wallet as MoneroWallet;
63 final subAddresses = moneroWallet.walletAddresses.subaddressList.subaddresses
64 - .map((sub) => Subaddress(id: sub.id, address: sub.address, label: sub.label))
64 + .map((sub) => Subaddress(
65 + id: sub.id,
66 + address: sub.address,
67 + label: sub.label,
68 + received: sub.balance??"unknown",
69 + txCount: sub.txCount??0,
70 + ))
71 .toList();
72 return ObservableList<Subaddress>.of(subAddresses);
73 }
@@ -83,7 +89,12 @@ class CWMoneroSubaddressList extends MoneroSubaddressList {
89 final moneroWallet = wallet as MoneroWallet;
90 return moneroWallet.walletAddresses.subaddressList
91 .getAll()
86 - .map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address))
92 + .map((sub) => Subaddress(
93 + id: sub.id,
94 + label: sub.label,
95 + address: sub.address,
96 + txCount: sub.txCount??0,
97 + received: sub.balance??'unknown'))
98 .toList();
99 }
100
@@ -91,7 +102,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList {
102 Future<void> addSubaddress(Object wallet,
103 {required int accountIndex, required String label}) async {
104 final moneroWallet = wallet as MoneroWallet;
94 - await moneroWallet.walletAddresses.subaddressList
105 + return await moneroWallet.walletAddresses.subaddressList
106 .addSubaddress(accountIndex: accountIndex, label: label);
107 }
108
lib/src/screens/dashboard/pages/address_page.dart
+3 -2
@@ -155,13 +155,14 @@ class AddressPage extends BasePage {
155 amountTextFieldFocusNode: _cryptoAmountFocus,
156 amountController: _amountController,
157 isLight: dashboardViewModel.settingsStore.currentTheme.type ==
158 - ThemeType.light))),
158 + ThemeType.light,
159 + ))),
160 SizedBox(height: 16),
161 Observer(builder: (_) {
162 if (addressListViewModel.hasAddressList) {
163 return SelectButton(
164 text: addressListViewModel.buttonTitle,
164 - onTap: () async => Navigator.of(context).pushNamed(Routes.receive),
165 + onTap: () => Navigator.of(context).pushNamed(Routes.receive),
166 textColor: Theme.of(context).extension<SyncIndicatorTheme>()!.textColor,
167 color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
168 borderColor: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
lib/src/screens/exchange/exchange_page.dart
+6 -6
@@ -509,7 +509,7 @@ class ExchangePage extends BasePage {
509 }
510 });
511
512 - reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) {
512 + reaction((_) => exchangeViewModel.wallet.walletAddresses.addressForExchange, (String address) {
513 if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) {
514 depositKey.currentState!.changeAddress(address: address);
515 }
@@ -565,7 +565,7 @@ class ExchangePage extends BasePage {
565 key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
566
567 key.currentState!.changeAddress(
568 - address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : '');
568 + address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : '');
569
570 key.currentState!.changeAmount(amount: '');
571 }
@@ -576,9 +576,9 @@ class ExchangePage extends BasePage {
576
577 if (isCurrentTypeWallet) {
578 key.currentState!.changeWalletName(exchangeViewModel.wallet.name);
579 - key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address;
579 + key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.addressForExchange;
580 } else if (key.currentState!.addressController.text ==
581 - exchangeViewModel.wallet.walletAddresses.address) {
581 + exchangeViewModel.wallet.walletAddresses.addressForExchange) {
582 key.currentState!.changeWalletName('');
583 key.currentState!.addressController.text = '';
584 }
@@ -629,7 +629,7 @@ class ExchangePage extends BasePage {
629 initialCurrency: exchangeViewModel.depositCurrency,
630 initialWalletName: depositWalletName ?? '',
631 initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency
632 - ? exchangeViewModel.wallet.walletAddresses.address
632 + ? exchangeViewModel.wallet.walletAddresses.addressForExchange
633 : exchangeViewModel.depositAddress,
634 initialIsAmountEditable: true,
635 initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
@@ -694,7 +694,7 @@ class ExchangePage extends BasePage {
694 initialCurrency: exchangeViewModel.receiveCurrency,
695 initialWalletName: receiveWalletName ?? '',
696 initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency
697 - ? exchangeViewModel.wallet.walletAddresses.address
697 + ? exchangeViewModel.wallet.walletAddresses.addressForExchange
698 : exchangeViewModel.receiveAddress,
699 initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable,
700 isAmountEstimated: true,
lib/src/screens/exchange/exchange_template_page.dart
+2 -2
@@ -129,7 +129,7 @@ class ExchangeTemplatePage extends BasePage {
129 initialWalletName: depositWalletName ?? '',
130 initialAddress: exchangeViewModel.depositCurrency ==
131 exchangeViewModel.wallet.currency
132 - ? exchangeViewModel.wallet.walletAddresses.address
132 + ? exchangeViewModel.wallet.walletAddresses.addressForExchange
133 : exchangeViewModel.depositAddress,
134 initialIsAmountEditable: true,
135 initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
@@ -166,7 +166,7 @@ class ExchangeTemplatePage extends BasePage {
166 initialWalletName: receiveWalletName ?? '',
167 initialAddress: exchangeViewModel.receiveCurrency ==
168 exchangeViewModel.wallet.currency
169 - ? exchangeViewModel.wallet.walletAddresses.address
169 + ? exchangeViewModel.wallet.walletAddresses.addressForExchange
170 : exchangeViewModel.receiveAddress,
171 initialIsAmountEditable: false,
172 isAmountEstimated: true,
lib/src/screens/receive/receive_page.dart
+2 -1
@@ -121,7 +121,8 @@ class ReceivePage extends BasePage {
121 heroTag: _heroTag,
122 amountTextFieldFocusNode: _cryptoAmountFocus,
123 amountController: _amountController,
124 - isLight: currentTheme.type == ThemeType.light),
124 + isLight: currentTheme.type == ThemeType.light,
125 + ),
126 ),
127 AddressList(addressListViewModel: addressListViewModel),
128 Padding(
lib/src/screens/receive/widgets/address_cell.dart
+34 -7
@@ -2,6 +2,7 @@ import 'package:auto_size_text/auto_size_text.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/utils/responsive_layout_util.dart';
4 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
5 +import 'package:flutter/cupertino.dart';
6 import 'package:flutter/material.dart';
7 import 'package:flutter_slidable/flutter_slidable.dart';
8
@@ -15,11 +16,14 @@ class AddressCell extends StatelessWidget {
16 required this.textColor,
17 this.onTap,
18 this.onEdit,
19 + this.onHide,
20 + this.isHidden = false,
21 this.onDelete,
22 this.txCount,
23 this.balance,
24 this.isChange = false,
22 - this.hasBalance = false});
25 + this.hasBalance = false,
26 + this.hasReceived = false});
27
28 factory AddressCell.fromItem(
29 WalletAddressListItem item, {
@@ -28,7 +32,10 @@ class AddressCell extends StatelessWidget {
32 required Color textColor,
33 Function(String)? onTap,
34 bool hasBalance = false,
35 + bool hasReceived = false,
36 Function()? onEdit,
37 + Function()? onHide,
38 + bool isHidden = false,
39 Function()? onDelete,
40 }) =>
41 AddressCell(
@@ -40,11 +47,14 @@ class AddressCell extends StatelessWidget {
47 textColor: textColor,
48 onTap: onTap,
49 onEdit: onEdit,
50 + onHide: onHide,
51 + isHidden: isHidden,
52 onDelete: onDelete,
53 txCount: item.txCount,
54 balance: item.balance,
55 isChange: item.isChange,
47 - hasBalance: hasBalance);
56 + hasBalance: hasBalance,
57 + hasReceived: hasReceived,);
58
59 final String address;
60 final String name;
@@ -54,11 +64,14 @@ class AddressCell extends StatelessWidget {
64 final Color textColor;
65 final Function(String)? onTap;
66 final Function()? onEdit;
67 + final Function()? onHide;
68 + final bool isHidden;
69 final Function()? onDelete;
70 final int? txCount;
71 final String? balance;
72 final bool isChange;
73 final bool hasBalance;
74 + final bool hasReceived;
75
76 static const int addressPreviewLength = 8;
77
@@ -138,7 +151,7 @@ class AddressCell extends StatelessWidget {
151 ),
152 ],
153 ),
141 - if (hasBalance)
154 + if (hasBalance || hasReceived)
155 Padding(
156 padding: const EdgeInsets.only(top: 8.0),
157 child: Row(
@@ -146,7 +159,7 @@ class AddressCell extends StatelessWidget {
159 mainAxisSize: MainAxisSize.max,
160 children: [
161 Text(
149 - '${S.of(context).balance}: $balance',
162 + '${hasReceived ? S.of(context).received : S.of(context).balance}: $balance',
163 style: TextStyle(
164 fontSize: 16,
165 fontWeight: FontWeight.w600,
@@ -178,14 +191,28 @@ class AddressCell extends StatelessWidget {
191 enabled: !isCurrent,
192 child: Slidable(
193 key: Key(address),
181 - startActionPane: _actionPane(context),
182 - endActionPane: _actionPane(context),
194 + startActionPane: _actionPaneStart(context),
195 + endActionPane: _actionPaneEnd(context),
196 child: cell,
197 ),
198 );
199 }
200
188 - ActionPane _actionPane(BuildContext context) => ActionPane(
201 + ActionPane _actionPaneEnd(BuildContext context) => ActionPane(
202 + motion: const ScrollMotion(),
203 + extentRatio: onDelete != null ? 0.4 : 0.3,
204 + children: [
205 + SlidableAction(
206 + onPressed: (_) => onHide?.call(),
207 + backgroundColor: isHidden ? Colors.green : Colors.red,
208 + foregroundColor: Colors.white,
209 + icon: isHidden ? CupertinoIcons.arrow_left : CupertinoIcons.arrow_right,
210 + label: isHidden ? S.of(context).show : S.of(context).hide,
211 + ),
212 + ],
213 + );
214 +
215 + ActionPane _actionPaneStart(BuildContext context) => ActionPane(
216 motion: const ScrollMotion(),
217 extentRatio: onDelete != null ? 0.4 : 0.3,
218 children: [
lib/src/screens/receive/widgets/address_list.dart
+147 -64
@@ -10,16 +10,19 @@ import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart';
10 import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart';
11 import 'package:cake_wallet/src/widgets/section_divider.dart';
12 import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
13 +import 'package:cake_wallet/utils/list_item.dart';
14 import 'package:cake_wallet/utils/show_pop_up.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_hidden_list_header.dart';
17 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart';
18 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
19 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
20 import 'package:cw_core/wallet_type.dart';
21 +import 'package:flutter/foundation.dart';
22 import 'package:flutter/material.dart';
23 import 'package:flutter_mobx/flutter_mobx.dart';
24
22 -class AddressList extends StatelessWidget {
25 +class AddressList extends StatefulWidget {
26 const AddressList({
27 super.key,
28 required this.addressListViewModel,
@@ -29,59 +32,123 @@ class AddressList extends StatelessWidget {
32 final WalletAddressListViewModel addressListViewModel;
33 final Function(String)? onSelect;
34
35 + @override
36 + State<AddressList> createState() => _AddressListState();
37 +}
38 +
39 +class _AddressListState extends State<AddressList> {
40 +
41 + bool showHiddenAddresses = false;
42 +
43 + void _toggleHiddenAddresses() {
44 + setState(() {
45 + showHiddenAddresses = !showHiddenAddresses;
46 + });
47 + updateItems();
48 + }
49 +
50 + List<ListItem> getItems(List<ListItem> list, bool showHidden) {
51 + return list.where((element) {
52 + if (element is WalletAddressListItem) {
53 + if (showHidden && element.isHidden) return true;
54 + if (!showHidden && !element.isHidden) return true;
55 + return false;
56 + }
57 + return true;
58 + }).toList();
59 + }
60 +
61 + List<ListItem> items = [];
62 +
63 + void updateItems() {
64 + setState(() {
65 + items = getItems(widget.addressListViewModel.items, showHiddenAddresses);
66 + });
67 + }
68 +
69 + @override
70 + void initState() {
71 + super.initState();
72 +
73 + items = getItems(widget.addressListViewModel.items, showHiddenAddresses);
74 + }
75 +
76 @override
77 Widget build(BuildContext context) {
34 - bool editable = onSelect == null;
35 - return Observer(
36 - builder: (_) => ListView.separated(
37 - padding: EdgeInsets.all(0),
38 - separatorBuilder: (context, _) => const HorizontalSectionDivider(),
39 - shrinkWrap: true,
40 - physics: NeverScrollableScrollPhysics(),
41 - itemCount: addressListViewModel.items.length,
42 - itemBuilder: (context, index) {
43 - final item = addressListViewModel.items[index];
44 - Widget cell = Container();
45 -
46 - if (item is WalletAccountListHeader) {
47 - cell = HeaderTile(
48 - showTrailingButton: true,
49 - walletAddressListViewModel: addressListViewModel,
50 - trailingButtonTap: () async {
51 - if (addressListViewModel.type == WalletType.monero ||
52 - addressListViewModel.type == WalletType.haven) {
53 - await showPopUp<void>(
54 - context: context, builder: (_) => getIt.get<MoneroAccountListPage>());
55 - } else {
56 - await showPopUp<void>(
57 - context: context, builder: (_) => getIt.get<NanoAccountListPage>());
58 - }
59 - },
60 - title: S.of(context).accounts,
61 - trailingIcon: Icon(
62 - Icons.arrow_forward_ios,
63 - size: 14,
64 - color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
65 - ));
66 - }
78 + bool editable = widget.onSelect == null;
79 + return ListView.separated(
80 + padding: EdgeInsets.all(0),
81 + separatorBuilder: (context, _) => const HorizontalSectionDivider(),
82 + shrinkWrap: true,
83 + physics: NeverScrollableScrollPhysics(),
84 + itemCount: items.length,
85 + itemBuilder: (context, index) {
86 + final item = items[index];
87 + Widget cell = Container();
88
68 - if (item is WalletAddressListHeader) {
69 - cell = HeaderTile(
70 - title: S.of(context).addresses,
71 - walletAddressListViewModel: addressListViewModel,
72 - showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled,
73 - showSearchButton: true,
74 - trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress),
75 - trailingIcon: Icon(
76 - Icons.add,
77 - size: 20,
78 - color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
79 - ));
80 - }
89 + if (item is WalletAccountListHeader) {
90 + cell = HeaderTile(
91 + showTrailingButton: true,
92 + walletAddressListViewModel: widget.addressListViewModel,
93 + trailingButtonTap: () async {
94 + if (widget.addressListViewModel.type == WalletType.monero ||
95 + widget.addressListViewModel.type == WalletType.haven) {
96 + await showPopUp<void>(
97 + context: context, builder: (_) => getIt.get<MoneroAccountListPage>());
98 + updateItems();
99 + } else {
100 + await showPopUp<void>(
101 + context: context, builder: (_) => getIt.get<NanoAccountListPage>());
102 + updateItems();
103 + }
104 + },
105 + title: S.of(context).accounts,
106 + trailingIcon: Icon(
107 + Icons.arrow_forward_ios,
108 + size: 14,
109 + color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
110 + ));
111 + }
112 +
113 + if (item is WalletAddressHiddenListHeader) {
114 + cell = HeaderTile(
115 + title: S.of(context).hidden_addresses,
116 + walletAddressListViewModel: widget.addressListViewModel,
117 + showTrailingButton: true,
118 + showSearchButton: false,
119 + trailingButtonTap: _toggleHiddenAddresses,
120 + trailingIcon: Icon(
121 + showHiddenAddresses ? Icons.toggle_on : Icons.toggle_off,
122 + size: 20,
123 + color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
124 + ));
125 + }
126
82 - if (item is WalletAddressListItem) {
127 + if (item is WalletAddressListHeader) {
128 + cell = HeaderTile(
129 + title: S.of(context).addresses,
130 + walletAddressListViewModel: widget.addressListViewModel,
131 + showTrailingButton: widget.addressListViewModel.showAddManualAddresses,
132 + showSearchButton: true,
133 + onSearchCallback: updateItems,
134 + trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) {
135 + updateItems(); // refresh the new address
136 + }),
137 + trailingIcon: Icon(
138 + Icons.add,
139 + size: 20,
140 + color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
141 + ));
142 + }
143 +
144 + if (item is WalletAddressListItem) {
145 + if (item.isHidden && !showHiddenAddresses) {
146 + cell = Container();
147 + } else if (!item.isHidden && showHiddenAddresses) {
148 + cell = Container();
149 + } else {
150 cell = Observer(builder: (_) {
84 - final isCurrent = item.address == addressListViewModel.address.address && editable;
151 + final isCurrent = item.address == widget.addressListViewModel.address.address && editable;
152 final backgroundColor = isCurrent
153 ? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
154 : Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor;
@@ -89,35 +156,51 @@ class AddressList extends StatelessWidget {
156 ? Theme.of(context).extension<ReceivePageTheme>()!.currentTileTextColor
157 : Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor;
158
159 +
160 return AddressCell.fromItem(
161 item,
162 isCurrent: isCurrent,
95 - hasBalance: addressListViewModel.isElectrumWallet,
96 - backgroundColor: backgroundColor,
163 + hasBalance: widget.addressListViewModel.isBalanceAvailable,
164 + hasReceived: widget.addressListViewModel.isReceivedAvailable,
165 + // hasReceived:
166 + backgroundColor: (kDebugMode && item.isHidden) ?
167 + Theme.of(context).colorScheme.error :
168 + (kDebugMode && item.isManual) ? Theme.of(context).colorScheme.error.withBlue(255) :
169 + backgroundColor,
170 textColor: textColor,
171 onTap: (_) {
99 - if (onSelect != null) {
100 - onSelect!(item.address);
172 + if (widget.onSelect != null) {
173 + widget.onSelect!(item.address);
174 return;
175 }
103 - addressListViewModel.setAddress(item);
176 + widget.addressListViewModel.setAddress(item);
177 },
178 onEdit: editable
106 - ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item)
179 + ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item).then((value) {
180 + updateItems(); // refresh the new address
181 + })
182 : null,
183 + isHidden: item.isHidden,
184 + onHide: () => _hideAddress(item),
185 );
186 });
187 }
188 + }
189
112 - return index != 0
113 - ? cell
114 - : ClipRRect(
115 - borderRadius: BorderRadius.only(
116 - topLeft: Radius.circular(30), topRight: Radius.circular(30)),
117 - child: cell,
118 - );
119 - },
120 - ),
190 + return index != 0
191 + ? cell
192 + : ClipRRect(
193 + borderRadius: BorderRadius.only(
194 + topLeft: Radius.circular(30), topRight: Radius.circular(30)),
195 + child: cell,
196 + );
197 + },
198 );
199 }
200 +
201 + void _hideAddress(WalletAddressListItem item) async {
202 + await widget.addressListViewModel.toggleHideAddress(item);
203 + updateItems();
204 + }
205 +
206 }
lib/src/screens/receive/widgets/header_tile.dart
+6 -1
@@ -10,6 +10,7 @@ class HeaderTile extends StatefulWidget {
10 this.showSearchButton = false,
11 this.showTrailingButton = false,
12 this.trailingButtonTap,
13 + this.onSearchCallback,
14 this.trailingIcon,
15 });
16
@@ -18,6 +19,7 @@ class HeaderTile extends StatefulWidget {
19 final bool showSearchButton;
20 final bool showTrailingButton;
21 final VoidCallback? trailingButtonTap;
22 + final VoidCallback? onSearchCallback;
23 final Icon? trailingIcon;
24
25 @override
@@ -41,7 +43,10 @@ class _HeaderTileState extends State<HeaderTile> {
43 _isSearchActive
44 ? Expanded(
45 child: TextField(
44 - onChanged: (value) => widget.walletAddressListViewModel.updateSearchText(value),
46 + onChanged: (value) {
47 + widget.walletAddressListViewModel.updateSearchText(value);
48 + widget.onSearchCallback?.call();
49 + },
50 cursorColor: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor,
51 cursorWidth: 0.5,
52 decoration: InputDecoration(
lib/src/screens/receive/widgets/qr_widget.dart
+9 -5
@@ -37,6 +37,10 @@ class QRWidget extends StatelessWidget {
37 final int? qrVersion;
38 final String? heroTag;
39
40 + PaymentURI get addressUri {
41 + return addressListViewModel.uri;
42 + }
43 +
44 @override
45 Widget build(BuildContext context) {
46 final copyImage = Image.asset('assets/images/copy_address.png',
@@ -77,14 +81,14 @@ class QRWidget extends StatelessWidget {
81 () async {
82 await Navigator.pushNamed(context, Routes.fullscreenQR,
83 arguments: QrViewData(
80 - data: addressListViewModel.uri.toString(),
84 + data: addressUri.toString(),
85 heroTag: heroTag,
86 ));
87 },
88 );
89 },
90 child: Hero(
87 - tag: Key(heroTag ?? addressListViewModel.uri.toString()),
91 + tag: Key(heroTag ?? addressUri.toString()),
92 child: Center(
93 child: AspectRatio(
94 aspectRatio: 1.0,
@@ -105,7 +109,7 @@ class QRWidget extends StatelessWidget {
109 color: Colors.white,
110 ),
111 ),
108 - child: QrImage(data: addressListViewModel.uri.toString())),
112 + child: QrImage(data: addressUri.toString())),
113 ),
114 ),
115 ),
@@ -148,7 +152,7 @@ class QRWidget extends StatelessWidget {
152 builder: (context) => Observer(
153 builder: (context) => GestureDetector(
154 onTap: () {
151 - Clipboard.setData(ClipboardData(text: addressListViewModel.address.address));
155 + Clipboard.setData(ClipboardData(text: addressUri.address));
156 showBar<void>(context, S.of(context).copied_to_clipboard);
157 },
158 child: Row(
@@ -157,7 +161,7 @@ class QRWidget extends StatelessWidget {
161 children: <Widget>[
162 Expanded(
163 child: Text(
160 - addressListViewModel.address.address,
164 + addressUri.address,
165 textAlign: TextAlign.center,
166 style: TextStyle(
167 fontSize: 15,
lib/view_model/contact_list/contact_list_view_model.dart
+30 -21
@@ -27,32 +27,41 @@ abstract class ContactListViewModelBase with Store {
27 settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled {
28 walletInfoSource.values.forEach((info) {
29 if (isAutoGenerateEnabled && info.type == WalletType.monero && info.addressInfos != null) {
30 - info.addressInfos!.forEach((key, value) {
31 - final nextUnusedAddress = value.firstWhereOrNull(
32 - (addressInfo) => !(info.usedAddresses?.contains(addressInfo.address) ?? false));
33 - if (nextUnusedAddress != null) {
34 - final name = _createName(info.name, nextUnusedAddress.label);
35 - walletContacts.add(WalletContact(
36 - nextUnusedAddress.address,
37 - name,
38 - walletTypeToCryptoCurrency(info.type),
39 - ));
40 - }
41 - });
30 + final key = info.addressInfos!.keys.first;
31 + final value = info.addressInfos![key];
32 + final address = value?.first;
33 + if (address != null) {
34 + final name = _createName(info.name, address.label);
35 + walletContacts.add(WalletContact(
36 + address.address,
37 + name,
38 + walletTypeToCryptoCurrency(info.type),
39 + ));
40 + }
41 } else if (info.addresses?.isNotEmpty == true && info.addresses!.length > 1) {
43 - info.addresses!.forEach((address, label) {
44 - if (label.isEmpty) {
45 - return;
46 - }
47 - final name = _createName(info.name, label);
42 + if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)) {
43 + final address = info.address;
44 + final name = _createName(info.name, "");
45 walletContacts.add(WalletContact(
46 address,
47 name,
51 - walletTypeToCryptoCurrency(info.type,
52 - isTestnet:
53 - info.network == null ? false : info.network!.toLowerCase().contains("testnet")),
48 + walletTypeToCryptoCurrency(info.type),
49 ));
55 - });
50 + } else {
51 + info.addresses!.forEach((address, label) {
52 + if (label.isEmpty) {
53 + return;
54 + }
55 + final name = _createName(info.name, label);
56 + walletContacts.add(WalletContact(
57 + address,
58 + name,
59 + walletTypeToCryptoCurrency(info.type,
60 + isTestnet:
61 + info.network == null ? false : info.network!.toLowerCase().contains("testnet")),
62 + ));
63 + });
64 + }
65 } else {
66 walletContacts.add(WalletContact(
67 info.address,
lib/view_model/exchange/exchange_view_model.dart
+12 -3
@@ -121,7 +121,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
121 depositAmount = '';
122 receiveAmount = '';
123 receiveAddress = '';
124 - depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.address : '';
124 + depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
125 provider = providersForCurrentPair().first;
126 final initialProvider = provider;
127 provider!.checkIsAvailable().then((bool isAvailable) {
@@ -155,6 +155,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
155 wallet.type == WalletType.litecoin ||
156 wallet.type == WalletType.bitcoinCash;
157
158 + bool get hideAddressAfterExchange =>
159 + wallet.type == WalletType.monero ||
160 + wallet.type == WalletType.wownero;
161 +
162 bool _useTorOnly;
163 final Box<Trade> trades;
164 final ExchangeTemplateStore _exchangeTemplateStore;
@@ -540,6 +544,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
544 isFixedRate: isFixedRateMode,
545 );
546
547 + if (hideAddressAfterExchange) {
548 + wallet.walletAddresses.hiddenAddresses.add(depositAddress);
549 + await wallet.walletAddresses.saveAddressesInBox();
550 + }
551 +
552 var amount = isFixedRateMode ? receiveAmount : depositAmount;
553 amount = amount.replaceAll(',', '.');
554
@@ -603,8 +612,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
612 isReceiveAmountEntered = false;
613 depositAmount = '';
614 receiveAmount = '';
606 - depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.address : '';
607 - receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.address : '';
615 + depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
616 + receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
617 isDepositAddressEnabled = !(depositCurrency == wallet.currency);
618 isFixedRateMode = false;
619 _onPairChange();
lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart
+4
@@ -78,6 +78,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
78 wallet,
79 accountIndex: monero!.getCurrentAccount(wallet).id,
80 label: label);
81 + final addr = await monero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed
82 + wallet.walletAddresses.manualAddresses.add(addr);
83 await wallet.save();
84 }
85
@@ -88,6 +90,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
90 wallet,
91 accountIndex: wownero!.getCurrentAccount(wallet).id,
92 label: label);
93 + final addr = await wownero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed
94 + wallet.walletAddresses.manualAddresses.add(addr);
95 await wallet.save();
96 }
97
lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart new
+3
@@ -0,0 +1,3 @@
1 +import 'package:cake_wallet/utils/list_item.dart';
2 +
3 +class WalletAddressHiddenListHeader extends ListItem {}
\ No newline at end of file
lib/view_model/wallet_address_list/wallet_address_list_item.dart
+5 -1
@@ -1,7 +1,7 @@
1 import 'package:cake_wallet/utils/list_item.dart';
2
3 class WalletAddressListItem extends ListItem {
4 - const WalletAddressListItem({
4 + WalletAddressListItem({
5 required this.address,
6 required this.isPrimary,
7 this.id,
@@ -11,6 +11,8 @@ class WalletAddressListItem extends ListItem {
11 this.isChange = false,
12 // Address that is only ever used once, shouldn't be used to receive funds, copy and paste, share etc
13 this.isOneTimeReceiveAddress = false,
14 + this.isHidden = false,
15 + this.isManual = false,
16 }) : super();
17
18 final int? id;
@@ -20,6 +22,8 @@ class WalletAddressListItem extends ListItem {
22 final int? txCount;
23 final String? balance;
24 final bool isChange;
25 + bool isHidden;
26 + bool isManual;
27 final bool? isOneTimeReceiveAddress;
28
29 @override
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+84 -48
@@ -18,12 +18,16 @@ import 'package:cake_wallet/store/yat/yat_store.dart';
18 import 'package:cake_wallet/tron/tron.dart';
19 import 'package:cake_wallet/utils/list_item.dart';
20 import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart';
21 +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart';
22 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart';
23 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
24 import 'package:cake_wallet/wownero/wownero.dart';
25 import 'package:cw_core/amount_converter.dart';
26 import 'package:cw_core/currency.dart';
27 +import 'package:cw_core/wallet_info.dart';
28 import 'package:cw_core/wallet_type.dart';
29 +import 'package:cw_monero/api/wallet.dart';
30 +import 'package:flutter/foundation.dart';
31 import 'package:intl/intl.dart';
32 import 'package:mobx/mobx.dart';
33
@@ -271,56 +275,40 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
275 WalletType get type => wallet.type;
276
277 @computed
274 - WalletAddressListItem get address =>
275 - WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false);
278 + WalletAddressListItem get address {
279 + return WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false);
280 + }
281
282 @computed
283 PaymentURI get uri {
279 - if (wallet.type == WalletType.monero) {
280 - return MoneroURI(amount: amount, address: address.address);
281 - }
282 -
283 - if (wallet.type == WalletType.haven) {
284 - return HavenURI(amount: amount, address: address.address);
285 - }
286 -
287 - if (wallet.type == WalletType.bitcoin) {
288 - return BitcoinURI(amount: amount, address: address.address);
289 - }
290 -
291 - if (wallet.type == WalletType.litecoin) {
292 - return LitecoinURI(amount: amount, address: address.address);
293 - }
294 -
295 - if (wallet.type == WalletType.ethereum) {
296 - return EthereumURI(amount: amount, address: address.address);
297 - }
298 -
299 - if (wallet.type == WalletType.bitcoinCash) {
300 - return BitcoinCashURI(amount: amount, address: address.address);
301 - }
302 -
303 - if (wallet.type == WalletType.nano) {
304 - return NanoURI(amount: amount, address: address.address);
305 - }
306 -
307 - if (wallet.type == WalletType.polygon) {
308 - return PolygonURI(amount: amount, address: address.address);
309 - }
310 -
311 - if (wallet.type == WalletType.solana) {
312 - return SolanaURI(amount: amount, address: address.address);
313 - }
314 -
315 - if (wallet.type == WalletType.tron) {
316 - return TronURI(amount: amount, address: address.address);
317 - }
318 -
319 - if (wallet.type == WalletType.wownero) {
320 - return WowneroURI(amount: amount, address: address.address);
284 + switch (wallet.type) {
285 + case WalletType.monero:
286 + return MoneroURI(amount: amount, address: address.address);
287 + case WalletType.haven:
288 + return HavenURI(amount: amount, address: address.address);
289 + case WalletType.bitcoin:
290 + return BitcoinURI(amount: amount, address: address.address);
291 + case WalletType.litecoin:
292 + return LitecoinURI(amount: amount, address: address.address);
293 + case WalletType.ethereum:
294 + return EthereumURI(amount: amount, address: address.address);
295 + case WalletType.bitcoinCash:
296 + return BitcoinCashURI(amount: amount, address: address.address);
297 + case WalletType.banano:
298 + return NanoURI(amount: amount, address: address.address);
299 + case WalletType.nano:
300 + return NanoURI(amount: amount, address: address.address);
301 + case WalletType.polygon:
302 + return PolygonURI(amount: amount, address: address.address);
303 + case WalletType.solana:
304 + return SolanaURI(amount: amount, address: address.address);
305 + case WalletType.tron:
306 + return TronURI(amount: amount, address: address.address);
307 + case WalletType.wownero:
308 + return WowneroURI(amount: amount, address: address.address);
309 + case WalletType.none:
310 + throw Exception('Unexpected type: ${type.toString()}');
311 }
322 -
323 - throw Exception('Unexpected type: ${type.toString()}');
312 }
313
314 @computed
@@ -341,7 +329,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
329 id: subaddress.id,
330 isPrimary: isPrimary,
331 name: subaddress.label,
344 - address: subaddress.address);
332 + address: subaddress.address,
333 + balance: subaddress.received,
334 + txCount: subaddress.txCount,
335 + );
336 });
337 addressList.addAll(addressItems);
338 }
@@ -468,6 +459,16 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
459 addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress));
460 }
461
462 + for (var i = 0; i < addressList.length; i++) {
463 + if (!(addressList[i] is WalletAddressListItem)) continue;
464 + (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses.contains((addressList[i] as WalletAddressListItem).address);
465 + }
466 +
467 + for (var i = 0; i < addressList.length; i++) {
468 + if (!(addressList[i] is WalletAddressListItem)) continue;
469 + (addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses.contains((addressList[i] as WalletAddressListItem).address);
470 + }
471 +
472 if (searchText.isNotEmpty) {
473 return ObservableList.of(addressList.where((item) {
474 if (item is WalletAddressListItem) {
@@ -479,7 +480,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
480
481 return addressList;
482 }
482 -
483 + Future<void> toggleHideAddress(WalletAddressListItem item) async {
484 + if (item.isHidden) {
485 + wallet.walletAddresses.hiddenAddresses.removeWhere((element) => element == item.address);
486 + } else {
487 + wallet.walletAddresses.hiddenAddresses.add(item.address);
488 + }
489 + await wallet.walletAddresses.saveAddressesInBox();
490 + if (wallet.type == WalletType.monero) {
491 + monero!.getSubaddressList(wallet).update(wallet, accountIndex: monero!.getCurrentAccount(wallet).id);
492 + } else if (wallet.type == WalletType.wownero) {
493 + wownero!.getSubaddressList(wallet).update(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id);
494 + } else if (wallet.type == WalletType.haven) {
495 + haven!.getSubaddressList(wallet).update(wallet, accountIndex: haven!.getCurrentAccount(wallet).id);
496 + }
497 + }
498 @observable
499 bool hasAccounts;
500
@@ -515,6 +530,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
530 wallet.type == WalletType.litecoin ||
531 wallet.type == WalletType.bitcoinCash;
532
533 + @computed
534 + bool get isBalanceAvailable => isElectrumWallet;
535 +
536 + @computed
537 + bool get isReceivedAvailable =>
538 + wallet.type == WalletType.monero ||
539 + wallet.type == WalletType.wownero;
540 +
541 @computed
542 bool get isSilentPayments =>
543 wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet);
@@ -524,6 +547,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
547 _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled &&
548 !isSilentPayments;
549
550 + @computed
551 + bool get showAddManualAddresses =>
552 + !isAutoGenerateSubaddressEnabled ||
553 + wallet.type == WalletType.monero ||
554 + wallet.type == WalletType.wownero;
555 +
556 List<ListItem> _baseItems;
557
558 final YatStore yatStore;
@@ -542,6 +571,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
571 void _init() {
572 _baseItems = [];
573
574 + if (wallet.walletAddresses.hiddenAddresses.isNotEmpty) {
575 + _baseItems.add(WalletAddressHiddenListHeader());
576 + }
577 +
578 if (wallet.type == WalletType.monero ||
579 wallet.type == WalletType.wownero ||
580 wallet.type == WalletType.haven) {
@@ -551,6 +584,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
584 if (wallet.type != WalletType.nano && wallet.type != WalletType.banano) {
585 _baseItems.add(WalletAddressListHeader());
586 }
587 + if (wallet.isEnabledAutoGenerateSubaddress) {
588 + wallet.walletAddresses.address = wallet.walletAddresses.latestAddress;
589 + }
590 }
591
592 @action
res/values/strings_en.arb
+3
@@ -333,6 +333,8 @@
333 "haven_app": "Haven by Cake Wallet",
334 "haven_app_wallet_text": "Awesome wallet for Haven",
335 "help": "help",
336 + "hide": "Hide",
337 + "hidden_addresses": "Hidden Addresses",
338 "hidden_balance": "Hidden Balance",
339 "hide_details": "Hide Details",
340 "high_contrast_theme": "High Contrast Theme",
@@ -683,6 +685,7 @@
685 "setup_your_debit_card": "Set up your debit card",
686 "share": "Share",
687 "share_address": "Share address",
688 + "show": "Show",
689 "shared_seed_wallet_groups": "Shared Seed Wallet Groups",
690 "show_details": "Show Details",
691 "show_keys": "Show seed/keys",
tool/configure.dart
+5 -1
@@ -303,10 +303,14 @@ class Subaddress {
303 Subaddress({
304 required this.id,
305 required this.label,
306 - required this.address});
306 + required this.address,
307 + required this.received,
308 + required this.txCount});
309 final int id;
310 final String label;
311 final String address;
312 + final String? received;
313 + final int txCount;
314 }
315
316 class MoneroBalance extends Balance {