feat: monero cache debugging (#2436)
* feat: monero cache debugging feat: better JSON explorer for network traffic * frozen coins debugging
cyan committed
Sep 12, 2025 at 08:47 UTC
52e778f2757f380aef710f83fc8bd14b4bedb37a
19 files changed
+584
-22
cw_monero/lib/api/get_all_unspent.dart
new
+30
@@ -0,0 +1,30 @@
1
+import 'package:cw_monero/api/account_list.dart';
2
+import 'package:cw_monero/monero_unspent.dart';
3
+
4
+Map<String, Map<String, dynamic>> getAllUnspent() {
5
+ final coins = currentWallet!.coins();
6
+ coins.refresh();
7
+ final coinCount = coins.count();
8
+
9
+ final ret = <String, Map<String, dynamic>>{};
10
+ ret["_count"] = {coinCount.toString(): coinCount};
11
+ for (var i = 0; i < coinCount; i++) {
12
+ final coin = coins.coin(i);
13
+ final subaddr = coin.subaddrAccount();
14
+ if (ret[subaddr.toString()] == null) {
15
+ ret[subaddr.toString()] = {};
16
+ }
17
+
18
+ final unspent = MoneroUnspent.fromUnspent(
19
+ address: coin.address(),
20
+ hash: coin.hash(),
21
+ keyImage: coin.keyImage(),
22
+ value: coin.amount(),
23
+ isFrozen: coin.frozen(),
24
+ isUnlocked: coin.unlocked(),
25
+ isSpent: coin.spent(),
26
+ );
27
+ ret[subaddr.toString()]!["0x${coin.ffiAddress().toRadixString(16)}"] = unspent.toJson();
28
+ }
29
+ return ret;
30
+}
cw_monero/lib/api/wallet.dart
+18
-2
@@ -1,4 +1,5 @@
1
import 'dart:async';
2
+import 'dart:convert';
3
import 'dart:ffi';
4
import 'dart:isolate';
5
import 'dart:math';
@@ -6,6 +7,7 @@ import 'dart:math';
7
import 'package:cw_core/utils/print_verbose.dart';
8
import 'package:cw_monero/api/account_list.dart';
9
import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
10
+import 'package:cw_monero/api/get_all_unspent.dart';
11
import 'package:monero/monero.dart' as monero;
12
import 'package:mutex/mutex.dart';
13
import 'package:polyseed/polyseed.dart';
@@ -144,8 +146,9 @@ String getAddress({int accountIndex = 0, int addressIndex = 0}) {
146
return addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex]!;
147
}
148
147
-int getFullBalance({int accountIndex = 0}) =>
148
- currentWallet?.balance(accountIndex: accountIndex) ?? 0;
149
+int getFullBalance({int accountIndex = 0}) {
150
+ return currentWallet?.balance(accountIndex: accountIndex) ?? 0;
151
+}
152
153
int getUnlockedBalance({int accountIndex = 0}) =>
154
currentWallet?.unlockedBalance(accountIndex: accountIndex) ?? 0;
@@ -422,3 +425,16 @@ bool verifyMessage(String message, String address, String signature) {
425
}
426
427
Map<String, List<int>> debugCallLength() => monero.debugCallLength;
428
+
429
+Map<String, dynamic> getWalletCacheDebug() {
430
+ try {
431
+ final jsonString = monero.MONERO_Wallet_serializeCacheToJson(Pointer.fromAddress(currentWallet!.ffiAddress()));
432
+ final blob = json.decode(jsonString);
433
+ blob['cake:unspent'] = getAllUnspent();
434
+ return blob;
435
+ } catch (e) {
436
+ return {
437
+ "error": e.toString(),
438
+ };
439
+ }
440
+}
\ No newline at end of file
cw_monero/lib/monero_unspent.dart
+27
-4
@@ -1,17 +1,25 @@
1
import 'package:cw_core/unspent_transaction_output.dart';
2
import 'package:cw_core/utils/print_verbose.dart';
3
import 'package:cw_monero/api/coins_info.dart';
4
-import 'package:monero/src/monero.dart';
4
5
class MoneroUnspent extends Unspent {
7
- static Future<MoneroUnspent> fromUnspent(String address, String hash, String keyImage, int value, bool isFrozen, bool isUnlocked) async {
6
+ static MoneroUnspent fromUnspent({
7
+ required String address,
8
+ required String hash,
9
+ required String keyImage,
10
+ required int value,
11
+ required bool isFrozen,
12
+ required bool isUnlocked,
13
+ required bool isSpent,
14
+ }) {
15
return MoneroUnspent(
16
address: address,
17
hash: hash,
18
keyImage: keyImage,
19
value: value,
20
isFrozen: isFrozen,
14
- isUnlocked: isUnlocked);
21
+ isUnlocked: isUnlocked,
22
+ isSpent: isSpent);
23
}
24
25
MoneroUnspent(
@@ -20,7 +28,8 @@ class MoneroUnspent extends Unspent {
28
required String keyImage,
29
required int value,
30
required bool isFrozen,
23
- required this.isUnlocked})
31
+ required this.isUnlocked,
32
+ required this.isSpent})
33
: super(address, hash, value, 0, keyImage) {
34
_frozen = isFrozen;
35
}
@@ -47,4 +56,18 @@ class MoneroUnspent extends Unspent {
56
bool get isFrozen => _frozen;
57
58
final bool isUnlocked;
59
+ final bool isSpent;
60
+
61
+ Map<String, dynamic> toJson() {
62
+ return {
63
+ 'address': address,
64
+ 'hash': hash,
65
+ 'keyImage': keyImage,
66
+ 'value': value,
67
+ 'isFrozen': isFrozen,
68
+ 'isUnlocked': isUnlocked,
69
+ 'isChange': isChange,
70
+ 'isSpent': isSpent,
71
+ };
72
+ }
73
}
cw_monero/lib/monero_wallet.dart
+7
-6
@@ -643,12 +643,13 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
643
final coinSpent = coin.spent();
644
if (coinSpent == false && coin.subaddrAccount() == walletAddresses.account!.id) {
645
final unspent = await MoneroUnspent.fromUnspent(
646
- coin.address(),
647
- coin.hash(),
648
- coin.keyImage(),
649
- coin.amount(),
650
- coin.frozen(),
651
- coin.unlocked(),
646
+ address: coin.address(),
647
+ hash: coin.hash(),
648
+ keyImage: coin.keyImage(),
649
+ value: coin.amount(),
650
+ isFrozen: coin.frozen(),
651
+ isUnlocked: coin.unlocked(),
652
+ isSpent: coinSpent,
653
);
654
// TODO: double-check the logic here
655
if (unspent.hash.isNotEmpty) {
cw_monero/pubspec.lock
+2
-2
@@ -573,8 +573,8 @@ packages:
573
dependency: "direct main"
574
description:
575
path: "impls/monero.dart"
576
- ref: b576312e4d466569cd03482b61c597b39a9f4dc3
577
- resolved-ref: b576312e4d466569cd03482b61c597b39a9f4dc3
576
+ ref: "5225c47e5033d46bfd60e612e87b24c562f67efa"
577
+ resolved-ref: "5225c47e5033d46bfd60e612e87b24c562f67efa"
578
url: "https://github.com/mrcyjanek/monero_c"
579
source: git
580
version: "0.0.0"
cw_monero/pubspec.yaml
+1
-1
@@ -26,7 +26,7 @@ dependencies:
26
monero:
27
git:
28
url: https://github.com/mrcyjanek/monero_c
29
- ref: b576312e4d466569cd03482b61c597b39a9f4dc3
29
+ ref: 5225c47e5033d46bfd60e612e87b24c562f67efa
30
path: impls/monero.dart
31
mutex: ^3.1.0
32
ledger_flutter_plus: ^1.4.1
cw_wownero/pubspec.lock
+2
-2
@@ -493,8 +493,8 @@ packages:
493
dependency: "direct main"
494
description:
495
path: "impls/monero.dart"
496
- ref: b576312e4d466569cd03482b61c597b39a9f4dc3
497
- resolved-ref: b576312e4d466569cd03482b61c597b39a9f4dc3
496
+ ref: "5225c47e5033d46bfd60e612e87b24c562f67efa"
497
+ resolved-ref: "5225c47e5033d46bfd60e612e87b24c562f67efa"
498
url: "https://github.com/mrcyjanek/monero_c"
499
source: git
500
version: "0.0.0"
cw_wownero/pubspec.yaml
+1
-1
@@ -24,7 +24,7 @@ dependencies:
24
monero:
25
git:
26
url: https://github.com/mrcyjanek/monero_c
27
- ref: b576312e4d466569cd03482b61c597b39a9f4dc3 # monero_c hash
27
+ ref: 5225c47e5033d46bfd60e612e87b24c562f67efa # monero_c hash
28
path: impls/monero.dart
29
mutex: ^3.1.0
30
cw_zano/pubspec.lock
+2
-2
@@ -498,8 +498,8 @@ packages:
498
dependency: "direct main"
499
description:
500
path: "impls/monero.dart"
501
- ref: b576312e4d466569cd03482b61c597b39a9f4dc3
502
- resolved-ref: b576312e4d466569cd03482b61c597b39a9f4dc3
501
+ ref: "5225c47e5033d46bfd60e612e87b24c562f67efa"
502
+ resolved-ref: "5225c47e5033d46bfd60e612e87b24c562f67efa"
503
url: "https://github.com/mrcyjanek/monero_c"
504
source: git
505
version: "0.0.0"
cw_zano/pubspec.yaml
+1
-1
@@ -25,7 +25,7 @@ dependencies:
25
monero:
26
git:
27
url: https://github.com/mrcyjanek/monero_c
28
- ref: b576312e4d466569cd03482b61c597b39a9f4dc3 # monero_c hash
28
+ ref: 5225c47e5033d46bfd60e612e87b24c562f67efa # monero_c hash
29
path: impls/monero.dart
30
dev_dependencies:
31
flutter_test:
lib/di.dart
+3
@@ -33,6 +33,7 @@ import 'package:cake_wallet/entities/parse_address_from_domain.dart';
33
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
34
import 'package:cake_wallet/haven/cw_haven.dart';
35
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
36
+import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
37
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
38
import 'package:cake_wallet/src/screens/dev/network_requests.dart';
39
import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
@@ -1550,6 +1551,8 @@ Future<void> setup({
1551
1552
getIt.registerFactory(() => DevMoneroCallProfilerPage());
1553
1554
+ getIt.registerFactory(() => DevMoneroWalletCacheDebugPage());
1555
+
1556
getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get<DevSharedPreferences>()));
1557
1558
getIt.registerFactory(() => DevSecurePreferencesPage(getIt.get<DevSecurePreferences>()));
lib/monero/cw_monero.dart
+5
@@ -442,4 +442,9 @@ class CWMonero extends Monero {
442
Map<String, List<int>> debugCallLength() {
443
return monero_wallet_api.debugCallLength();
444
}
445
+
446
+ @override
447
+ Map<String, dynamic> getWalletCacheDebug() {
448
+ return monero_wallet_api.getWalletCacheDebug();
449
+ }
450
}
lib/router.dart
+6
@@ -36,6 +36,7 @@ import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart';
36
import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
37
import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
38
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
39
+import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
40
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
41
import 'package:cake_wallet/src/screens/dev/network_requests.dart';
42
import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
@@ -929,6 +930,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
930
builder: (_) => getIt.get<DevMoneroCallProfilerPage>(),
931
);
932
933
+ case Routes.devMoneroWalletCacheDebug:
934
+ return MaterialPageRoute<void>(
935
+ builder: (_) => getIt.get<DevMoneroWalletCacheDebugPage>(),
936
+ );
937
+
938
case Routes.devSecurePreferences:
939
return MaterialPageRoute<void>(
940
builder: (_) => getIt.get<DevSecurePreferencesPage>(),
lib/routes.dart
+1
@@ -116,6 +116,7 @@ class Routes {
116
117
static const devMoneroBackgroundSync = '/dev/monero_background_sync';
118
static const devMoneroCallProfiler = '/dev/monero_call_profiler';
119
+ static const devMoneroWalletCacheDebug = '/dev/monero_wallet_cache_debug';
120
121
static const devSharedPreferences = '/dev/shared_preferences';
122
static const devSecurePreferences = '/dev/secure_preferences';
lib/src/screens/dev/moneroc_cache_debug.dart
new
+443
@@ -0,0 +1,443 @@
1
+import 'dart:convert';
2
+import 'dart:io';
3
+
4
+import 'package:cake_wallet/di.dart';
5
+import 'package:cake_wallet/monero/monero.dart';
6
+import 'package:cake_wallet/utils/share_util.dart';
7
+import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
8
+import 'package:cw_core/root_dir.dart';
9
+import 'package:cw_core/wallet_type.dart';
10
+import 'package:flutter/material.dart';
11
+import 'package:flutter/services.dart';
12
+import 'package:path/path.dart' as path;
13
+
14
+class DevMoneroWalletCacheDebugPage extends StatelessWidget {
15
+ DevMoneroWalletCacheDebugPage();
16
+
17
+ @override
18
+ Widget build(BuildContext context) {
19
+ return MoneroCacheDebug();
20
+ }
21
+}
22
+
23
+class MoneroCacheDebug extends StatefulWidget {
24
+ const MoneroCacheDebug({super.key});
25
+
26
+ @override
27
+ State<MoneroCacheDebug> createState() => _MoneroCacheDebugState();
28
+}
29
+
30
+enum DebuggableWallets {
31
+ monero,
32
+}
33
+
34
+class _MoneroCacheDebugState extends State<MoneroCacheDebug> {
35
+ final dashboardViewModel = getIt.get<DashboardViewModel>();
36
+
37
+ late DebuggableWallets wallet = switch (dashboardViewModel.wallet.type) {
38
+ WalletType.monero => DebuggableWallets.monero,
39
+ _ => throw Exception("Unknown wallet type"),
40
+ };
41
+
42
+ late Map<String, dynamic> walletCache = switch (wallet) {
43
+ DebuggableWallets.monero => monero!.getWalletCacheDebug(),
44
+ };
45
+
46
+ @override
47
+ Widget build(BuildContext context) {
48
+ return JsonExplorerPage(
49
+ data: walletCache,
50
+ title: 'Wallet Cache',
51
+ );
52
+ }
53
+}
54
+
55
+class JsonExplorerPage extends StatelessWidget {
56
+ final dynamic data;
57
+ final String title;
58
+
59
+ const JsonExplorerPage({
60
+ super.key,
61
+ required this.data,
62
+ required this.title,
63
+ });
64
+
65
+ @override
66
+ Widget build(BuildContext context) {
67
+ return Scaffold(
68
+ appBar: AppBar(
69
+ title: Text(title),
70
+ actions: [
71
+ IconButton(
72
+ onPressed: () => _copyToClipboard(context),
73
+ icon: const Icon(Icons.copy),
74
+ tooltip: 'Copy all to clipboard',
75
+ ),
76
+ ],
77
+ ),
78
+ body: JsonExplorer(
79
+ data: data,
80
+ title: title,
81
+ ),
82
+ );
83
+ }
84
+
85
+ Future<void> _copyToClipboard(BuildContext context) async {
86
+ final jsonString = JsonEncoder.withIndent(' ').convert(data);
87
+ try {
88
+ await Clipboard.setData(ClipboardData(text: jsonString));
89
+ ScaffoldMessenger.of(context).showSnackBar(
90
+ const SnackBar(content: Text('Data copied to clipboard')),
91
+ );
92
+ return;
93
+ } catch (e) {
94
+ ScaffoldMessenger.of(context).showSnackBar(
95
+ SnackBar(content: Text('Failed to copy to clipboard: $e')),
96
+ );
97
+ }
98
+ try {
99
+ final appDir = await getAppDir();
100
+ final filePath = appDir.path + '/.json_dump_temp.json';
101
+ await File(filePath).writeAsString(jsonString);
102
+ await ShareUtil.shareFile(filePath: filePath, fileName: path.basename(filePath), context: context);
103
+ } catch (e) {
104
+ ScaffoldMessenger.of(context).showSnackBar(
105
+ SnackBar(content: Text('Failed to share file: $e')),
106
+ );
107
+ }
108
+ }
109
+}
110
+
111
+class JsonExplorer extends StatefulWidget {
112
+ final dynamic data;
113
+ final String title;
114
+
115
+ const JsonExplorer({
116
+ super.key,
117
+ required this.data,
118
+ required this.title,
119
+ });
120
+
121
+ @override
122
+ State<JsonExplorer> createState() => _JsonExplorerState();
123
+}
124
+
125
+class _JsonExplorerState extends State<JsonExplorer> {
126
+ final TextEditingController _searchController = TextEditingController();
127
+ String _searchQuery = '';
128
+ List<CacheItem> _filteredItems = [];
129
+ final List<CacheItem> _allItems = [];
130
+
131
+ @override
132
+ void initState() {
133
+ super.initState();
134
+ _buildItemList();
135
+ }
136
+
137
+ @override
138
+ void dispose() {
139
+ _searchController.dispose();
140
+ super.dispose();
141
+ }
142
+
143
+ void _buildItemList() {
144
+ _allItems.clear();
145
+
146
+ if (widget.data is Map) {
147
+ final map = widget.data as Map<String, dynamic>;
148
+ final sortedKeys = map.keys.toList()..sort();
149
+
150
+ for (final key in sortedKeys) {
151
+ _allItems.add(CacheItem(
152
+ key: key,
153
+ value: map[key],
154
+ displayKey: key,
155
+ ));
156
+ }
157
+ } else if (widget.data is List) {
158
+ final list = widget.data as List;
159
+ for (int i = 0; i < list.length; i++) {
160
+ _allItems.add(CacheItem(
161
+ key: i.toString(),
162
+ value: list[i],
163
+ displayKey: '[$i]',
164
+ ));
165
+ }
166
+ }
167
+
168
+ _applyFilter();
169
+ }
170
+
171
+ void _applyFilter() {
172
+ if (_searchQuery.isEmpty) {
173
+ _filteredItems = _allItems;
174
+ } else {
175
+ _filteredItems = _allItems.where((item) {
176
+ return item.displayKey.toLowerCase().contains(_searchQuery.toLowerCase()) ||
177
+ _valueContainsSearch(item.value, _searchQuery.toLowerCase());
178
+ }).toList();
179
+ }
180
+ }
181
+
182
+ bool _valueContainsSearch(dynamic value, String search) {
183
+ if (value == null) return false;
184
+ return value.toString().toLowerCase().contains(search);
185
+ }
186
+
187
+ void _copyItemToClipboard(CacheItem item) {
188
+ final jsonString = JsonEncoder.withIndent(' ').convert(item.value);
189
+ Clipboard.setData(ClipboardData(text: jsonString));
190
+ ScaffoldMessenger.of(context).showSnackBar(
191
+ SnackBar(content: Text('${item.displayKey} copied to clipboard')),
192
+ );
193
+ }
194
+
195
+ void _navigateToItem(CacheItem item) {
196
+ if (item.value is Map || item.value is List) {
197
+ Navigator.push(
198
+ context,
199
+ MaterialPageRoute(
200
+ builder: (context) => JsonExplorerPage(
201
+ data: item.value,
202
+ title: item.displayKey,
203
+ ),
204
+ ),
205
+ );
206
+ }
207
+ }
208
+
209
+ @override
210
+ Widget build(BuildContext context) {
211
+ final theme = Theme.of(context);
212
+ final totalItems = widget.data is Map
213
+ ? (widget.data as Map).length
214
+ : widget.data is List
215
+ ? (widget.data as List).length
216
+ : 0;
217
+
218
+ return Column(
219
+ children: [
220
+ Padding(
221
+ padding: const EdgeInsets.all(8.0),
222
+ child: Row(
223
+ children: [
224
+ Expanded(
225
+ child: TextField(
226
+ controller: _searchController,
227
+ decoration: InputDecoration(
228
+ hintText: 'Search in ${totalItems} items...',
229
+ prefixIcon: const Icon(Icons.search),
230
+ border: const OutlineInputBorder(),
231
+ isDense: true,
232
+ suffixText: _searchQuery.isNotEmpty
233
+ ? '${_filteredItems.length} found'
234
+ : null,
235
+ ),
236
+ onChanged: (value) {
237
+ setState(() {
238
+ _searchQuery = value;
239
+ _applyFilter();
240
+ });
241
+ },
242
+ ),
243
+ ),
244
+ ],
245
+ ),
246
+ ),
247
+
248
+ Expanded(
249
+ child: _filteredItems.isEmpty && _searchQuery.isNotEmpty
250
+ ? Center(
251
+ child: Column(
252
+ mainAxisAlignment: MainAxisAlignment.center,
253
+ children: [
254
+ const Icon(Icons.search_off, size: 64, color: Colors.grey),
255
+ const SizedBox(height: 16),
256
+ Text(
257
+ 'No items found for "${_searchQuery}"',
258
+ style: theme.textTheme.titleMedium?.copyWith(
259
+ color: Colors.grey,
260
+ ),
261
+ ),
262
+ ],
263
+ ),
264
+ )
265
+ : ListView.builder(
266
+ itemCount: _filteredItems.length,
267
+ itemBuilder: (context, index) {
268
+ final item = _filteredItems[index];
269
+ return CacheItemTile(
270
+ item: item,
271
+ onTap: () => _navigateToItem(item),
272
+ onCopy: () => _copyItemToClipboard(item),
273
+ searchQuery: _searchQuery,
274
+ );
275
+ },
276
+ ),
277
+ ),
278
+ ],
279
+ );
280
+ }
281
+}
282
+
283
+class CacheItem {
284
+ final String key;
285
+ final dynamic value;
286
+ final String displayKey;
287
+
288
+ CacheItem({
289
+ required this.key,
290
+ required this.value,
291
+ required this.displayKey,
292
+ });
293
+}
294
+
295
+class CacheItemTile extends StatelessWidget {
296
+ final CacheItem item;
297
+ final VoidCallback onTap;
298
+ final VoidCallback onCopy;
299
+ final String searchQuery;
300
+
301
+ const CacheItemTile({
302
+ super.key,
303
+ required this.item,
304
+ required this.onTap,
305
+ required this.onCopy,
306
+ this.searchQuery = '',
307
+ });
308
+
309
+ Color _getTypeColor(BuildContext context, dynamic value) {
310
+ final theme = Theme.of(context);
311
+ final isDark = theme.brightness == Brightness.dark;
312
+
313
+ if (value == null) {
314
+ return Colors.grey;
315
+ }
316
+
317
+ switch (value.runtimeType) {
318
+ case String:
319
+ return Colors.green;
320
+ case int:
321
+ case double:
322
+ return Colors.blue;
323
+ case bool:
324
+ return Colors.orange;
325
+ // ignore: strict_raw_type
326
+ case Map:
327
+ return Colors.purple;
328
+ // ignore: strict_raw_type
329
+ case List:
330
+ return Colors.cyan;
331
+ default:
332
+ return Colors.pink;
333
+ }
334
+ }
335
+
336
+ String _getValuePreview(dynamic value) {
337
+ if (value == null) return 'null';
338
+
339
+ if (value is Map) {
340
+ return '{${value.length} items}';
341
+ }
342
+
343
+ if (value is List) {
344
+ return '[${value.length} items]';
345
+ }
346
+
347
+ if (value is String) {
348
+ if (value.length > 100) {
349
+ return '"${value.substring(0, 97)}..."';
350
+ }
351
+ return '"$value"';
352
+ }
353
+
354
+ final str = value.toString();
355
+ if (str.length > 100) {
356
+ return '${str.substring(0, 97)}...';
357
+ }
358
+ return str;
359
+ }
360
+
361
+ Widget _buildHighlightedText(String text, String query, TextStyle style) {
362
+ if (query.isEmpty) {
363
+ return Text(text, style: style);
364
+ }
365
+
366
+ final lowerText = text.toLowerCase();
367
+ final lowerQuery = query.toLowerCase();
368
+ final matches = <TextSpan>[];
369
+ int start = 0;
370
+
371
+ while (true) {
372
+ final index = lowerText.indexOf(lowerQuery, start);
373
+ if (index == -1) {
374
+ if (start < text.length) {
375
+ matches.add(TextSpan(text: text.substring(start), style: style));
376
+ }
377
+ break;
378
+ }
379
+
380
+ if (index > start) {
381
+ matches.add(TextSpan(text: text.substring(start, index), style: style));
382
+ }
383
+
384
+ matches.add(TextSpan(
385
+ text: text.substring(index, index + query.length),
386
+ style: style.copyWith(
387
+ backgroundColor: Colors.yellow.withOpacity(0.3),
388
+ fontWeight: FontWeight.bold,
389
+ ),
390
+ ));
391
+
392
+ start = index + query.length;
393
+ }
394
+
395
+ return RichText(text: TextSpan(children: matches));
396
+ }
397
+
398
+ @override
399
+ Widget build(BuildContext context) {
400
+ final theme = Theme.of(context);
401
+ final canNavigate = item.value is Map || item.value is List;
402
+ final valuePreview = _getValuePreview(item.value);
403
+
404
+ return Card(
405
+ margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
406
+ child: ListTile(
407
+ leading: Icon(
408
+ canNavigate
409
+ ? (item.value is Map ? Icons.folder : Icons.list)
410
+ : Icons.description,
411
+ color: _getTypeColor(context, item.value),
412
+ ),
413
+ title: _buildHighlightedText(
414
+ item.displayKey,
415
+ searchQuery,
416
+ theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
417
+ ),
418
+ subtitle: _buildHighlightedText(
419
+ valuePreview,
420
+ searchQuery,
421
+ theme.textTheme.bodySmall!.copyWith(
422
+ color: _getTypeColor(context, item.value),
423
+ fontFamily: 'monospace',
424
+ ),
425
+ ),
426
+ trailing: Row(
427
+ mainAxisSize: MainAxisSize.min,
428
+ children: [
429
+ IconButton(
430
+ onPressed: onCopy,
431
+ icon: const Icon(Icons.copy, size: 18),
432
+ tooltip: 'Copy ${item.displayKey}',
433
+ ),
434
+ if (canNavigate)
435
+ const Icon(Icons.chevron_right),
436
+ ],
437
+ ),
438
+ onTap: canNavigate ? onTap : null,
439
+ enabled: canNavigate,
440
+ ),
441
+ );
442
+ }
443
+}
\ No newline at end of file
lib/src/screens/dev/network_requests.dart
+27
@@ -2,6 +2,8 @@ import 'dart:convert';
2
import 'dart:typed_data';
3
4
import 'package:cake_wallet/src/screens/base_page.dart';
5
+import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
6
+import 'package:cake_wallet/src/widgets/primary_button.dart';
7
import 'package:cake_wallet/view_model/dev/network_requests_view_model.dart';
8
import 'package:cw_core/utils/proxy_logger/abstract.dart';
9
import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart';
@@ -133,9 +135,11 @@ class DevRequestDetails extends BasePage {
135
136
_sectionTitle("Body (as UTF-8)"),
137
SelectableText(_tryDecodeBody(req.body)),
138
+ _buildJsonExplorer(context, _tryDecodeBody(req.body)),
139
140
_sectionTitle("Response"),
141
SelectableText(req.response?.body ?? "null"),
142
+ _buildJsonExplorer(context, req.response?.body ?? "{}"),
143
144
_sectionTitle("Error"),
145
SelectableText(req.error ?? "No error"),
@@ -146,6 +150,29 @@ class DevRequestDetails extends BasePage {
150
);
151
}
152
153
+ Widget _buildJsonExplorer(BuildContext context, String body) {
154
+ try {
155
+ final jsonData = json.decode(body);
156
+ return PrimaryButton(
157
+ text: "View JSON",
158
+ color: Colors.blue,
159
+ textColor: Colors.white,
160
+ onPressed: () {
161
+ Navigator.of(context).push(
162
+ MaterialPageRoute(
163
+ builder: (context) {
164
+ return JsonExplorerPage(data: jsonData, title: "body");
165
+ },
166
+ ),
167
+ );
168
+ },
169
+ );
170
+
171
+ } catch (e) {
172
+ return SelectableText("Invalid JSON: $e");
173
+ }
174
+ }
175
+
176
Widget _sectionTitle(String title) {
177
return Padding(
178
padding: const EdgeInsets.symmetric(vertical: 8.0),
lib/src/screens/settings/other_settings_page.dart
+6
@@ -75,6 +75,12 @@ class OtherSettingsPage extends BasePage {
75
handler: (BuildContext context) =>
76
Navigator.of(context).pushNamed(Routes.devMoneroCallProfiler),
77
),
78
+ if (FeatureFlag.hasDevOptions && [WalletType.monero].contains(_otherSettingsViewModel.walletType))
79
+ SettingsCellWithArrow(
80
+ title: '[dev] xmr wallet cache debug',
81
+ handler: (BuildContext context) =>
82
+ Navigator.of(context).pushNamed(Routes.devMoneroWalletCacheDebug),
83
+ ),
84
if (FeatureFlag.hasDevOptions)
85
SettingsCellWithArrow(
86
title: '[dev] shared preferences',
scripts/prepare_moneroc.sh
+1
-1
@@ -9,7 +9,7 @@ then
9
rm -rf monero_c
10
git clone https://github.com/mrcyjanek/monero_c --branch master monero_c
11
cd monero_c
12
- git checkout b576312e4d466569cd03482b61c597b39a9f4dc3
12
+ git checkout 5225c47e5033d46bfd60e612e87b24c562f67efa
13
git reset --hard
14
git submodule update --init --force --recursive
15
./apply_patches.sh monero
tool/configure.dart
+1
@@ -447,6 +447,7 @@ WalletCredentials createMoneroNewWalletCredentials({required String name, requir
447
void setGlobalLedgerConnection(ledger.LedgerConnection connection);
448
String? getLastLedgerCommand();
449
Map<String, List<int>> debugCallLength();
450
+ Map<String, dynamic> getWalletCacheDebug();
451
}
452
453
abstract class MoneroSubaddressList {