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 {