feat: dev screen for file manager, snapshot and fuzzing

This commit adds tooling to find potential issues with code
that are difficult to notice during debugging (especially on
builds from the CI which lack debugging capabilities).

File manager supports basic operations like browsing files,
renaming, copying and removing

Snapshots allow to take a snapshot of current app data, and
compare to it later on with basic hexdump view - useful to
notice anomalies or modifications to files that shouldn't
be accessed.

File watcher checks for file modifications in real time, which
is used to see what actions are happening to the fs in real
time.

Fuzzing can be used to load/create wallets randomly to see
if any of them will get corrupted (can be used in addition
with wallet_fuzzer.sh script that kills the app and restarts
the process at random intervals to simulate unexpected action
that can happen for example in the background).

In addition I've added toString method to {bitcoin,monero,nano
,zano}_wallet_keys.dart so I can easily compare the results
when fuzzing without accessing wallet-specific code.
This commit is contained in:
Czarek Nakamoto 2025-04-17 12:49:06 +02:00 committed by cyan
parent 1d6e594e04
commit 7ac732198b
17 changed files with 3651 additions and 0 deletions

3
.gitignore vendored
View file

@ -227,3 +227,6 @@ flatpak-build/
**/linux/flutter/generated_plugin_registrant.cc
**/linux/flutter/generated_plugin_registrant.h
**/linux/flutter/generated_plugins.cmake
wallet_fuzzer_data/

View file

@ -4,4 +4,9 @@ class BitcoinWalletKeys {
final String wif;
final String privateKey;
final String publicKey;
@override
String toString() {
return 'BitcoinWalletKeys(wif: $wif, privateKey: $privateKey, publicKey: $publicKey)';
}
}

View file

@ -13,4 +13,9 @@ class MoneroWalletKeys {
final String publicSpendKey;
final String privateSpendKey;
final String passphrase;
@override
String toString() {
return 'MoneroWalletKeys(primaryAddress: $primaryAddress, publicViewKey: $publicViewKey, privateViewKey: $privateViewKey, publicSpendKey: $publicSpendKey, privateSpendKey: $privateSpendKey, passphrase: $passphrase)';
}
}

View file

@ -2,4 +2,9 @@ class NanoWalletKeys {
const NanoWalletKeys({required this.seedKey});
final String seedKey;
@override
String toString() {
return 'NanoWalletKeys(seedKey: $seedKey)';
}
}

View file

@ -9,4 +9,9 @@ class ZanoWalletKeys {
final String privateViewKey;
final String publicSpendKey;
final String privateSpendKey;
@override
String toString() {
return 'ZanoWalletKeys(publicViewKey: $publicViewKey, privateViewKey: $privateViewKey, publicSpendKey: $publicSpendKey, privateSpendKey: $privateSpendKey)';
}
}

View file

@ -35,6 +35,10 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/file_explorer.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/wallet_fuzzer.dart';
import 'package:cake_wallet/src/screens/settings/background_sync_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
@ -1511,5 +1515,9 @@ Future<void> setup({
getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get<BackgroundSyncLogsViewModel>()));
getIt.registerFactory(() => FileExplorerPage());
getIt.registerFactory(() => WalletFuzzerPage());
_isSetupFinished = true;
}

View file

@ -28,8 +28,10 @@ import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/themes/utils/theme_provider.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/dev/file_explorer.dart';
import 'package:cw_core/address_info.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/hive_type_ids.dart';
@ -65,6 +67,9 @@ Future<void> runAppWithZone({Key? topLevelKey}) async {
await runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
if (FeatureFlag.hasDevOptions) {
await checkAndStartFileMonitoring();
}
FlutterError.onError = ExceptionHandler.onError;
/// A callback that is invoked when an unhandled error occurs in the root
@ -449,3 +454,15 @@ Future<void> backgroundSync() async {
}
}
}
Future<void> checkAndStartFileMonitoring() async {
try {
final shouldMonitor = await FileExplorerViewModelBase.checkDevMonitorFileExists();
if (shouldMonitor) {
printV('Dev file monitoring enabled, starting file system watcher...');
await FileExplorerViewModelBase.startMonitoring();
}
} catch (e) {
printV('Failed to initialize file monitoring: $e');
}
}

View file

@ -36,11 +36,13 @@ import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
import 'package:cake_wallet/src/screens/dev/file_explorer.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
import 'package:cake_wallet/src/screens/dev/wallet_fuzzer.dart';
import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart';
@ -920,6 +922,16 @@ Route<dynamic> createRoute(RouteSettings settings) {
builder: (_) => getIt.get<DevSecurePreferencesPage>(),
);
case Routes.devFileExplorer:
return MaterialPageRoute<void>(
builder: (_) => getIt.get<FileExplorerPage>(),
);
case Routes.devWalletFuzzer:
return MaterialPageRoute<void>(
builder: (_) => getIt.get<WalletFuzzerPage>(),
);
default:
return MaterialPageRoute<void>(
builder: (_) => Scaffold(

View file

@ -119,6 +119,8 @@ class Routes {
static const devSecurePreferences = '/dev/secure_preferences';
static const devBackgroundSyncLogs = '/dev/background_sync_logs';
static const devFileExplorer = '/dev/file_explorer';
static const devWalletFuzzer = '/dev/wallet_fuzzer';
static const signPage = '/sign_page';
static const connectDevices = '/device/connect';
static const urqrAnimatedPage = '/urqr/animated_page';

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,238 @@
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/view_model/dev/wallet_fuzzer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:intl/intl.dart';
class WalletFuzzerPage extends BasePage {
WalletFuzzerPage();
@override
String? get title => "[dev] wallet fuzzer";
final viewModel = WalletFuzzerViewModel();
@override
Widget body(BuildContext context) {
return Observer(
builder: (_) {
return Column(
children: [
_buildStatusBar(context),
Expanded(
child: _buildLogsList(context),
),
_buildControls(context),
],
);
},
);
}
Widget _buildStatusBar(BuildContext context) {
return Container(
padding: EdgeInsets.all(8.0),
color: Theme.of(context).primaryColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
viewModel.isRunning ? Icons.warning_amber : Icons.info_outline,
color: viewModel.isRunning ? Colors.amber : Colors.white,
),
SizedBox(width: 8),
Expanded(
child: Text(
'Status: ${viewModel.isRunning ? "RUNNING" : "Stopped"}',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: viewModel.isRunning ? Colors.green.withOpacity(0.3) : Colors.red.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: viewModel.isRunning ? Colors.green : Colors.red,
),
),
child: Text(
'Ops: ${viewModel.operationsCompleted}',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
SizedBox(width: 8),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red),
),
child: Text(
'Errors: ${viewModel.errorsEncountered}',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
],
),
SizedBox(height: 8),
Text(
'Current Operation: ${viewModel.currentOperation}',
style: TextStyle(color: Colors.white),
),
Text(
'Current Wallet: ${viewModel.currentWallet.isEmpty ? "none" : viewModel.currentWallet}',
style: TextStyle(color: Colors.white),
),
],
),
);
}
Widget _buildLogsList(BuildContext context) {
return Container(
padding: EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'Logs:',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
IconButton(
icon: Icon(Icons.delete_sweep),
onPressed: () => viewModel.clearLogs(),
tooltip: 'Clear logs',
),
],
),
Divider(),
Expanded(
child: viewModel.logs.isEmpty
? Center(child: Text('No logs yet. Start fuzzing to see logs.'))
: ListView.builder(
itemCount: viewModel.logs.length,
itemBuilder: (context, index) {
final log = viewModel.logs[index];
final formattedTime = DateFormat('HH:mm:ss').format(log.timestamp);
Color logColor = Colors.black;
if (log.action.contains('Error') || log.result?.contains('Error') == true || log.result?.contains('error') == true) {
logColor = Colors.red;
} else if (log.action.contains('Success') || log.action.contains('successfully')) {
logColor = Colors.green;
}
return Padding(
padding: EdgeInsets.symmetric(vertical: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
formattedTime,
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
SizedBox(width: 8),
Expanded(
child: log.result != null
? RichText(
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: '${log.action} - ',
style: TextStyle(color: logColor),
),
TextSpan(
text: log.result,
style: TextStyle(
color: logColor,
fontWeight: FontWeight.bold,
),
),
],
),
)
: Text(
log.action,
style: TextStyle(color: logColor),
),
),
],
),
);
},
),
),
],
),
);
}
Widget _buildControls(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: Offset(0, -1),
),
],
),
child: Column(
children: [
Row(
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: viewModel.isRunning ? Colors.red : Colors.green,
padding: EdgeInsets.symmetric(vertical: 12),
),
onPressed: () {
if (viewModel.isRunning) {
viewModel.stopFuzzing();
} else {
viewModel.startFuzzing();
}
},
child: Text(
viewModel.isRunning ? 'STOP FUZZING' : 'START FUZZING',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
SizedBox(height: 8),
],
),
);
}
}

View file

@ -93,6 +93,18 @@ class OtherSettingsPage extends BasePage {
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.devBackgroundSyncLogs),
),
if (FeatureFlag.hasDevOptions)
SettingsCellWithArrow(
title: '[dev] file explorer',
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.devFileExplorer),
),
if (FeatureFlag.hasDevOptions)
SettingsCellWithArrow(
title: '[dev] wallet fuzzer',
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.devWalletFuzzer),
),
Spacer(),
SettingsVersionCell(
title: S.of(context).version(_otherSettingsViewModel.currentVersion)),

View file

@ -0,0 +1,967 @@
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'dart:typed_data';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:mobx/mobx.dart';
import 'package:path/path.dart' as p;
import 'package:watcher/watcher.dart';
part 'file_explorer.g.dart';
class FileExplorerViewModel = FileExplorerViewModelBase with _$FileExplorerViewModel;
class Snapshot {
final String name;
final DateTime timestamp;
final Map<String, FileInfo> files;
Snapshot({required this.name, required this.timestamp, required this.files});
}
class FileInfo {
final String path;
final int size;
final DateTime modified;
final Uint8List? content;
FileInfo({
required this.path,
required this.size,
required this.modified,
this.content
});
}
class FileChange {
final String path;
final ChangeType type;
final FileInfo? oldFile;
final FileInfo? newFile;
FileChange({required this.path, required this.type, this.oldFile, this.newFile});
}
enum ChangeType { added, removed, modified, touched }
enum ViewMode { fileExplorer, snapshots, hexdump, comparison, detailedComparison, fileMonitor }
class FileEvent {
final DateTime timestamp;
final WatchEvent event;
final String relativePath;
FileEvent({required this.timestamp, required this.event, required this.relativePath});
}
abstract class FileExplorerViewModelBase with Store {
FileExplorerViewModelBase() {
unawaited(_initialize());
}
static ObservableList<FileEvent> fileEvents = ObservableList<FileEvent>();
static DirectoryWatcher? _watcher;
static bool isMonitoringActive = false;
@observable
String? path;
@observable
String? rootPath;
@observable
String? snapshotsPath;
@observable
File? selectedFile;
@observable
ObservableList<Snapshot> snapshots = ObservableList<Snapshot>();
@observable
ViewMode viewMode = ViewMode.fileExplorer;
@observable
Snapshot? comparisonSnapshot;
@observable
ObservableList<FileChange> fileChanges = ObservableList<FileChange>();
@observable
FileChange? selectedFileChange;
static const int MAX_FILE_SIZE = 1 * 1024 * 1024;
static const int CHUNK_SIZE = 16 * 1024;
@action
Future<void> _initialize() async {
rootPath = (await getAppDir()).path;
path = rootPath;
snapshotsPath = p.join(rootPath!, 'snapshots');
final dir = Directory(snapshotsPath!);
if (!await dir.exists()) {
await dir.create(recursive: true);
}
await loadSnapshots();
}
@computed
List<FileSystemEntity> get files {
if (path == null) {
return [];
}
final dir = Directory(path!);
final entities = dir.listSync();
return entities.where((entity) => entity is File).toList();
}
@computed
List<String> get directories {
if (path == null) {
return [];
}
final dir = Directory(path!);
final entities = dir.listSync();
return entities.where((entity) => entity is Directory).map((entity) => p.basename(entity.path)).toList();
}
@action
void cd(String newPath) {
try {
if (path == null) {
return;
}
final Directory directory = Directory(p.join(path!, newPath));
final String path2 = directory.absolute.path;
final String normalizedPath = p.normalize(path2);
final String normalizedRootPath = p.normalize(rootPath!);
final bool isWithinRoot = normalizedPath.startsWith(normalizedRootPath);
if (!isWithinRoot) {
path = rootPath;
} else {
path = path2;
}
} catch (e) {
path = rootPath;
}
}
@action
void selectFile(File file) {
selectedFile = file;
viewMode = ViewMode.hexdump;
}
@action
void switchToFileExplorer() {
viewMode = ViewMode.fileExplorer;
}
@action
void switchToSnapshots() {
viewMode = ViewMode.snapshots;
}
@action
void switchToComparison(Snapshot snapshot) {
comparisonSnapshot = snapshot;
viewMode = ViewMode.comparison;
compareSnapshot();
}
@action
void switchToDetailedComparison(FileChange fileChange) {
selectedFileChange = fileChange;
viewMode = ViewMode.detailedComparison;
}
@action
void switchToFileMonitor() {
viewMode = ViewMode.fileMonitor;
}
Future<String> getHexDump(File file, {int? maxBytes}) async {
try {
final bytes = await file.readAsBytes();
final bytesToRead = maxBytes != null && bytes.length > maxBytes ? maxBytes : bytes.length;
final buffer = StringBuffer();
for (var i = 0; i < bytesToRead; i += 16) {
// Address
buffer.write('${i.toRadixString(16).padLeft(8, '0')}: ');
// Hex values
for (var j = 0; j < 16; j++) {
if (i + j < bytesToRead) {
buffer.write('${bytes[i + j].toRadixString(16).padLeft(2, '0')} ');
} else {
buffer.write(' ');
}
if (j == 7) buffer.write(' ');
}
// ASCII representation
buffer.write(' |');
for (var j = 0; j < 16; j++) {
if (i + j < bytesToRead) {
final char = bytes[i + j];
// Show printable ASCII characters
if (char >= 32 && char <= 126) {
buffer.write(String.fromCharCode(char));
} else {
buffer.write('.');
}
} else {
buffer.write(' ');
}
}
buffer.write('|\n');
}
if (maxBytes != null && bytes.length > maxBytes) {
buffer.write('\n... (${bytes.length - maxBytes} more bytes) ...');
}
return buffer.toString();
} catch (e) {
return 'Error reading file: $e';
}
}
Future<Uint8List> getFileBytes(String filePath) async {
try {
final file = File(filePath);
return await file.readAsBytes();
} catch (e) {
printV('Error reading file bytes: $e');
return Uint8List(0);
}
}
Future<String> getHexDiffDump(Uint8List oldBytes, Uint8List newBytes) async {
final buffer = StringBuffer();
final maxLength = oldBytes.length > newBytes.length ? oldBytes.length : newBytes.length;
final minLength = oldBytes.length < newBytes.length ? oldBytes.length : newBytes.length;
// Standard for hexdump displays
final chunkSize = 16;
int totalBlocks = (maxLength / chunkSize).ceil();
int diffBlocks = 0;
int sameBlocks = 0;
buffer.writeln('=== Binary Diff Summary ===');
buffer.writeln('Old size: ${_formatSize(oldBytes.length)}');
buffer.writeln('New size: ${_formatSize(newBytes.length)}');
buffer.writeln('Size difference: ${_formatSizeDifference(newBytes.length - oldBytes.length)}');
buffer.writeln('');
buffer.writeln('=== Changed Blocks Only ===');
for (var i = 0; i < maxLength; i += chunkSize) {
bool hasBlockDiff = false;
final blockEnd = i + chunkSize < maxLength ? i + chunkSize : maxLength;
for (var j = i; j < blockEnd && j < minLength; j++) {
if (j < oldBytes.length && j < newBytes.length && oldBytes[j] != newBytes[j]) {
hasBlockDiff = true;
break;
}
}
if (i >= oldBytes.length || i >= newBytes.length) {
hasBlockDiff = true;
}
if (hasBlockDiff) {
diffBlocks++;
if (i < oldBytes.length) {
buffer.write('- ');
buffer.write('${i.toRadixString(16).padLeft(8, '0')}: ');
for (var j = 0; j < chunkSize; j++) {
if (i + j < oldBytes.length) {
final byteValue = oldBytes[i + j].toRadixString(16).padLeft(2, '0');
if (i + j < newBytes.length && oldBytes[i + j] != newBytes[i + j]) {
buffer.write('[$byteValue] ');
} else {
buffer.write(' $byteValue ');
}
} else {
buffer.write(' ');
}
if (j == 7) buffer.write(' ');
}
buffer.write(' |');
for (var j = 0; j < chunkSize; j++) {
if (i + j < oldBytes.length) {
final char = oldBytes[i + j];
if (char >= 32 && char <= 126) {
if (i + j < newBytes.length && oldBytes[i + j] != newBytes[i + j]) {
buffer.write('[${String.fromCharCode(char)}]');
} else {
buffer.write(' ${String.fromCharCode(char)} ');
}
} else {
buffer.write(' . ');
}
} else {
buffer.write(' ');
}
}
buffer.write('|\n');
}
if (i < newBytes.length) {
buffer.write('+ ');
buffer.write('${i.toRadixString(16).padLeft(8, '0')}: ');
for (var j = 0; j < chunkSize; j++) {
if (i + j < newBytes.length) {
final byteValue = newBytes[i + j].toRadixString(16).padLeft(2, '0');
if (i + j < oldBytes.length && oldBytes[i + j] != newBytes[i + j]) {
buffer.write('[$byteValue] ');
} else if (i + j >= oldBytes.length) {
buffer.write('*$byteValue* ');
} else {
buffer.write(' $byteValue ');
}
} else {
buffer.write(' ');
}
if (j == 7) buffer.write(' ');
}
buffer.write(' |');
for (var j = 0; j < chunkSize; j++) {
if (i + j < newBytes.length) {
final char = newBytes[i + j];
if (char >= 32 && char <= 126) {
if (i + j < oldBytes.length && oldBytes[i + j] != newBytes[i + j]) {
buffer.write('[${String.fromCharCode(char)}]');
} else if (i + j >= oldBytes.length) {
buffer.write('*${String.fromCharCode(char)}*');
} else {
buffer.write(' ${String.fromCharCode(char)} ');
}
} else {
buffer.write(' . ');
}
} else {
buffer.write(' ');
}
}
buffer.write('|\n');
}
buffer.write('\n');
} else {
sameBlocks++;
}
}
buffer.writeln('=== Diff Summary ===');
buffer.writeln('Total blocks: $totalBlocks');
buffer.writeln('Different blocks: $diffBlocks');
buffer.writeln('Identical blocks: $sameBlocks');
buffer.writeln('Difference percentage: ${(diffBlocks / totalBlocks * 100).toStringAsFixed(2)}%');
return buffer.toString();
}
Future<String> compareFilesStreamBased(File currentFile, FileInfo oldInfo) async {
final buffer = StringBuffer();
final filePath = currentFile.path;
final fileSize = await currentFile.length();
if (fileSize > MAX_FILE_SIZE) {
return 'File too large to diff (${_formatSize(fileSize)}). Maximum size: ${_formatSize(MAX_FILE_SIZE)}.';
}
buffer.writeln('=== Binary Diff of ${p.basename(filePath)} ===');
buffer.writeln('Path: ${p.relative(filePath, from: rootPath!)}');
buffer.writeln('Old size: ${_formatSize(oldInfo.size)}');
buffer.writeln('New size: ${_formatSize(fileSize)}');
buffer.writeln('Size difference: ${_formatSizeDifference(fileSize - oldInfo.size)}');
buffer.writeln('');
if (oldInfo.content == null) {
buffer.writeln('No content information available for old file. Cannot generate diff.');
return buffer.toString();
}
final oldBytes = oldInfo.content!;
final file = File(filePath);
final fileStream = file.openRead(0, oldBytes.length < fileSize ? oldBytes.length : fileSize);
List<int> newBytes = [];
await for (var chunk in fileStream) {
newBytes.addAll(chunk);
}
if (newBytes.isNotEmpty) {
final newData = Uint8List.fromList(newBytes);
buffer.writeln('=== Detailed Binary Diff (${_formatSize(newData.length < oldBytes.length ? newData.length : oldBytes.length)}) ===');
buffer.write(await getHexDiffDump(oldBytes, newData));
} else {
buffer.writeln('Failed to read file for comparison.');
}
return buffer.toString();
}
Future<Map<String, FileInfo>> _scanDirectory(String directoryPath) async {
final result = <String, FileInfo>{};
final dir = Directory(directoryPath);
final entities = await dir.list(recursive: true).toList();
final normalizedSnapshotsPath = p.normalize(snapshotsPath!);
final normalizedFlutterAssetsPath = p.normalize(p.join(rootPath!, 'flutter_assets'));
for (final entity in entities) {
if (entity is File) {
final normalizedPath = p.normalize(entity.path);
if (normalizedPath.startsWith(normalizedSnapshotsPath) ||
normalizedPath.startsWith(normalizedFlutterAssetsPath)) {
continue;
}
try {
final relativePath = p.relative(entity.path, from: rootPath!);
final stat = await entity.stat();
if (stat.size > MAX_FILE_SIZE) {
printV('Skipping large file: $relativePath (${_formatSize(stat.size)})');
continue;
}
Uint8List? fileContent;
try {
fileContent = await entity.readAsBytes();
} catch (e) {
printV('Error reading file content: $e');
}
result[relativePath] = FileInfo(
path: relativePath,
size: stat.size,
modified: stat.modified,
content: fileContent,
);
} catch (e) {
printV('Error processing file ${entity.path}: $e');
}
}
}
return result;
}
@action
Future<void> loadSnapshots() async {
try {
final dir = Directory(snapshotsPath!);
if (!await dir.exists()) return;
final snapshotFiles = dir.listSync().whereType<File>().where(
(file) => file.path.endsWith('.snapshot')
).toList();
snapshots.clear();
List<String> corruptedFiles = [];
for (final file in snapshotFiles) {
try {
final content = await file.readAsString();
Map<String, dynamic>? jsonData;
try {
jsonData = jsonDecode(content) as Map<String, dynamic>;
} catch (jsonError) {
corruptedFiles.add(file.path);
printV('Error parsing snapshot ${file.path}: $jsonError');
continue;
}
if (!jsonData.containsKey('files') ||
!jsonData.containsKey('name') ||
!jsonData.containsKey('timestamp')) {
corruptedFiles.add(file.path);
printV('Invalid snapshot format in ${file.path}');
continue;
}
try {
final fileInfos = Map<String, FileInfo>.from(
(jsonData['files'] as Map).map((key, value) => MapEntry(
key as String,
FileInfo(
path: value['path'] as String,
size: value['size'] as int,
modified: DateTime.parse(value['modified'] as String),
content: value['content'] != null ? base64Decode(value['content'] as String) : null,
),
)),
);
snapshots.add(Snapshot(
name: jsonData['name'] as String,
timestamp: DateTime.parse(jsonData['timestamp'] as String),
files: fileInfos,
));
} catch (e) {
corruptedFiles.add(file.path);
printV('Error processing snapshot data ${file.path}: $e');
}
} catch (e) {
corruptedFiles.add(file.path);
printV('Error loading snapshot ${file.path}: $e');
}
}
if (corruptedFiles.isNotEmpty) {
await _handleCorruptedSnapshots(corruptedFiles);
}
snapshots.sort((a, b) => b.timestamp.compareTo(a.timestamp));
} catch (e) {
printV('Error loading snapshots: $e');
}
}
Future<void> _handleCorruptedSnapshots(List<String> corruptedFilePaths) async {
printV('Found ${corruptedFilePaths.length} corrupted snapshot files:');
for (final path in corruptedFilePaths) {
printV(' - ${p.basename(path)}');
try {
final file = File(path);
if (await file.exists()) {
final newPath = '$path.corrupted';
await file.rename(newPath);
printV(' Renamed to ${p.basename(newPath)}');
}
} catch (e) {
printV(' Failed to rename corrupted file: $e');
}
}
}
@action
Future<void> createSnapshot(String name) async {
try {
if (rootPath == null) return;
final timestamp = DateTime.now();
final files = await _scanDirectory(rootPath!);
final snapshot = Snapshot(
name: name,
timestamp: timestamp,
files: files,
);
final jsonData = {
'name': snapshot.name,
'timestamp': snapshot.timestamp.toIso8601String(),
'files': snapshot.files.map((path, fileInfo) => MapEntry(
path,
{
'path': fileInfo.path,
'size': fileInfo.size,
'modified': fileInfo.modified.toIso8601String(),
'content': fileInfo.content != null ? base64Encode(fileInfo.content!) : null,
},
)),
};
final snapshotFilePath = p.join(
snapshotsPath!,
'${name.replaceAll(RegExp(r'[^\w\s-]'), '_')}_${timestamp.millisecondsSinceEpoch}.snapshot',
);
final file = File(snapshotFilePath);
await file.writeAsString(jsonEncode(jsonData));
await loadSnapshots();
} catch (e) {
printV('Error creating snapshot: $e');
}
}
@action
Future<void> compareSnapshot() async {
if (comparisonSnapshot == null || rootPath == null) return;
try {
fileChanges.clear();
final currentFiles = await _scanDirectory(rootPath!);
final snapshot = comparisonSnapshot!;
for (final entry in currentFiles.entries) {
final path = entry.key;
final current = entry.value;
if (!snapshot.files.containsKey(path)) {
fileChanges.add(FileChange(
path: path,
type: ChangeType.added,
newFile: current,
));
} else {
final old = snapshot.files[path]!;
if (old.modified != current.modified) {
fileChanges.add(FileChange(
path: path,
type: ChangeType.touched,
oldFile: old,
newFile: current,
));
} else if (old.size != current.size ||
!_areContentsEqual(old.content, current.content)) {
fileChanges.add(FileChange(
path: path,
type: ChangeType.modified,
oldFile: old,
newFile: current,
));
}
}
}
for (final path in snapshot.files.keys) {
if (!currentFiles.containsKey(path)) {
fileChanges.add(FileChange(
path: path,
type: ChangeType.removed,
oldFile: snapshot.files[path],
));
}
}
fileChanges.sort((a, b) => a.path.compareTo(b.path));
} catch (e) {
printV('Error comparing snapshot: $e');
}
}
@action
Future<void> deleteSnapshot(Snapshot snapshot) async {
try {
final snapshotFile = File(p.join(
snapshotsPath!,
'${snapshot.name.replaceAll(RegExp(r'[^\w\s-]'), '_')}_${snapshot.timestamp.millisecondsSinceEpoch}.snapshot',
));
if (await snapshotFile.exists()) {
await snapshotFile.delete();
}
await loadSnapshots();
} catch (e) {
printV('Error deleting snapshot: $e');
}
}
Future<String> getFileDiff(FileChange fileChange) async {
if (fileChange.type == ChangeType.added) {
final filePath = p.join(rootPath!, fileChange.path);
final file = File(filePath);
final fileSize = await file.length();
if (fileSize > MAX_FILE_SIZE) {
return 'File too large to display (${_formatSize(fileSize)}). Maximum size: ${_formatSize(MAX_FILE_SIZE)}.';
}
final buffer = StringBuffer();
buffer.writeln('=== File Added ===');
buffer.writeln('Path: ${fileChange.path}');
buffer.writeln('Size: ${_formatSize(fileChange.newFile!.size)}');
buffer.writeln('Modified: ${fileChange.newFile!.modified}');
buffer.writeln('');
if (fileChange.newFile!.content != null) {
buffer.writeln('=== Hex Dump (Added File) ===');
final bytes = fileChange.newFile!.content!;
final hexDump = _formatHexDump(bytes, false);
final hexDumpLines = hexDump.split('\n');
for (final line in hexDumpLines) {
if (line.isNotEmpty) {
buffer.writeln('+ $line');
}
}
}
return buffer.toString();
} else if (fileChange.type == ChangeType.removed) {
final buffer = StringBuffer();
buffer.writeln('=== File Removed ===');
buffer.writeln('Path: ${fileChange.path}');
buffer.writeln('Size: ${_formatSize(fileChange.oldFile!.size)}');
buffer.writeln('Modified: ${fileChange.oldFile!.modified}');
buffer.writeln('');
if (fileChange.oldFile!.content != null) {
buffer.writeln('=== Hex Dump (Removed File) ===');
final bytes = fileChange.oldFile!.content!;
final hexDump = _formatHexDump(bytes, false);
final hexDumpLines = hexDump.split('\n');
for (final line in hexDumpLines) {
if (line.isNotEmpty) {
buffer.writeln('- $line');
}
}
}
return buffer.toString();
} else if (fileChange.type == ChangeType.modified) {
final filePath = p.join(rootPath!, fileChange.path);
final currentFile = File(filePath);
final fileSize = await currentFile.length();
if (fileSize > MAX_FILE_SIZE) {
return 'File too large to diff (${_formatSize(fileSize)}). Maximum size: ${_formatSize(MAX_FILE_SIZE)}.';
}
final buffer = StringBuffer();
buffer.writeln('=== File Modified ===');
buffer.writeln('Path: ${fileChange.path}');
buffer.writeln('');
buffer.writeln('=== Size ===');
buffer.writeln('Old: ${_formatSize(fileChange.oldFile!.size)}');
buffer.writeln('New: ${_formatSize(fileChange.newFile!.size)}');
buffer.writeln('Difference: ${_formatSizeDifference(fileChange.newFile!.size - fileChange.oldFile!.size)}');
buffer.writeln('');
buffer.writeln('=== Timestamps ===');
buffer.writeln('Old modified: ${fileChange.oldFile!.modified}');
buffer.writeln('New modified: ${fileChange.newFile!.modified}');
buffer.writeln('');
if (fileChange.oldFile!.content != null && fileChange.newFile!.content != null) {
buffer.writeln('=== Binary Diff ===');
final diff = await getHexDiffDump(fileChange.oldFile!.content!, fileChange.newFile!.content!);
buffer.write(diff);
} else {
buffer.writeln('Cannot create binary diff: missing content data.');
}
return buffer.toString();
}
return 'No detailed information available.';
}
String _formatHexDump(Uint8List bytes, bool truncated) {
final buffer = StringBuffer();
for (var i = 0; i < bytes.length; i += 16) {
// Address
buffer.write('${i.toRadixString(16).padLeft(8, '0')}: ');
// Hex values
for (var j = 0; j < 16; j++) {
if (i + j < bytes.length) {
buffer.write('${bytes[i + j].toRadixString(16).padLeft(2, '0')} ');
} else {
buffer.write(' ');
}
if (j == 7) buffer.write(' ');
}
// ASCII representation
buffer.write(' |');
for (var j = 0; j < 16; j++) {
if (i + j < bytes.length) {
final char = bytes[i + j];
if (char >= 32 && char <= 126) {
buffer.write(String.fromCharCode(char));
} else {
buffer.write('.');
}
} else {
buffer.write(' ');
}
}
buffer.write('|\n');
}
if (truncated) {
buffer.write('\n... (file continues) ...');
}
return buffer.toString();
}
Future<String> getTextDiff(String oldText, String newText) async {
final oldLines = oldText.split('\n');
final newLines = newText.split('\n');
final buffer = StringBuffer();
// Very simple diff algorithm - can be improved with a proper diff algorithm
final maxLines = oldLines.length > newLines.length ? oldLines.length : newLines.length;
for (var i = 0; i < maxLines; i++) {
if (i < oldLines.length && i < newLines.length) {
if (oldLines[i] == newLines[i]) {
buffer.writeln(' ${newLines[i]}');
} else {
buffer.writeln('- ${oldLines[i]}');
buffer.writeln('+ ${newLines[i]}');
}
} else if (i < oldLines.length) {
buffer.writeln('- ${oldLines[i]}');
} else if (i < newLines.length) {
buffer.writeln('+ ${newLines[i]}');
}
}
return buffer.toString();
}
String _formatSize(int sizeInBytes) {
if (sizeInBytes < 1024) {
return '$sizeInBytes B';
} else if (sizeInBytes < 1024 * 1024) {
return '${(sizeInBytes / 1024).toStringAsFixed(2)} KB';
} else if (sizeInBytes < 1024 * 1024 * 1024) {
return '${(sizeInBytes / (1024 * 1024)).toStringAsFixed(2)} MB';
} else if (sizeInBytes < 1024 * 1024 * 1024 * 1024) {
return '${(sizeInBytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
} else {
return '${(sizeInBytes / (1024 * 1024 * 1024 * 1024)).toStringAsFixed(2)} TB... why?';
}
}
String _formatSizeDifference(int diffInBytes) {
final prefix = diffInBytes >= 0 ? '+' : '';
return '$prefix${_formatSize(diffInBytes.abs())}';
}
bool _areContentsEqual(Uint8List? a, Uint8List? b) {
if (a == null && b == null) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
@action
Future<bool> renameFile(String oldPath, String newPath) async {
try {
final oldFile = File(oldPath);
final newFile = File(newPath);
printV('Renaming file: $oldPath to $newPath');
if (await newFile.exists()) {
return false;
}
await oldFile.rename(newFile.path);
printV('Renamed file: $oldPath to $newPath');
return true;
} catch (e) {
printV('Error renaming file: $e');
return false;
}
}
@action
Future<bool> deleteFile(String path) async {
try {
final file = File(path);
await file.delete();
return true;
} catch (e) {
printV('Error deleting file: $e');
return false;
}
}
@action
Future<bool> copyFile(String sourcePath, String destinationPath) async {
try {
final sourceFile = File(sourcePath);
final destinationFile = File(destinationPath);
if (await destinationFile.exists()) {
return false;
}
await sourceFile.copy(destinationFile.path);
return true;
} catch (e) {
printV('Error copying file: $e');
return false;
}
}
static Future<void> startMonitoring() async {
if (_watcher != null) {
return;
}
isMonitoringActive = true;
final appDir = await getAppDir();
if (Platform.isAndroid) {
_watcher = DirectoryWatcher(appDir.parent.path); // get rid of weird app_flutter directory
} else {
_watcher = DirectoryWatcher(appDir.path);
}
_watcher!.events.listen((event) {
if (event.path.contains('flutter_engine')) {
return;
}
final relativePath = event.path.replaceFirst(appDir.path, '~');
fileEvents.add(FileEvent(
timestamp: DateTime.now(),
event: event,
relativePath: relativePath,
));
});
}
static void stopMonitoring() {
if (_watcher != null) {
_watcher!.events.drain();
_watcher = null;
isMonitoringActive = false;
}
}
@action
void clearEvents() {
fileEvents.clear();
}
static Future<bool> checkDevMonitorFileExists() async {
final appDir = await getAppDir();
final devMonitorFile = File('${appDir.path}/.dev-monitor-fs');
return devMonitorFile.exists();
}
}

View file

@ -0,0 +1,660 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:convert';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart';
import 'package:path/path.dart' as p;
import 'package:cake_wallet/core/wallet_creation_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cake_wallet/decred/decred.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cake_wallet/entities/seed_type.dart';
import 'package:cake_wallet/core/generate_wallet_password.dart';
part 'wallet_fuzzer.g.dart';
class WalletFuzzerViewModel = WalletFuzzerViewModelBase with _$WalletFuzzerViewModel;
class FuzzyLogEntry {
final DateTime timestamp;
final String action;
final String? result;
FuzzyLogEntry({required this.timestamp, required this.action, this.result});
@override
String toString() {
final formattedTime = DateFormat('yyyy-MM-dd HH:mm:ss.SSS').format(timestamp);
if (result != null) {
return '[$formattedTime] $action - $result';
}
return '[$formattedTime] $action';
}
}
abstract class WalletFuzzerViewModelBase with Store {
WalletFuzzerViewModelBase() {
unawaited(_initialize());
}
static final DateTime appStartDate = DateTime.now();
List<FuzzyLogEntry> logs = [];
@observable
bool isRunning = false;
@observable
String currentOperation = 'Idle';
@observable
String currentWallet = '';
@observable
int operationsCompleted = 0;
@observable
int errorsEncountered = 0;
@observable
Map<String, int> operationStats = {
'loadWallet': 0,
'syncWallet': 0,
'closeWallet': 0,
'switchWallet': 0,
'restartWallet': 0,
'checkAndCreateWallets': 0,
'createWalletsForType': 0,
'performRandomOperation': 0,
};
@computed
String get formattedOperationStats {
final buffer = StringBuffer();
final sortedEntries = operationStats.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
for (final entry in sortedEntries) {
final opName = entry.key;
final opCount = entry.value;
String formattedName = switch (opName) {
'loadWallet' => 'Load Wallet',
'syncWallet' => 'Sync Wallet',
'closeWallet' => 'Close Wallet',
'switchWallet' => 'Switch Wallet',
'restartWallet' => 'Restart Wallet',
'checkAndCreateWallets' => 'Create Wallets',
'performRandomOperation' => 'Perform Random Operation',
_ => opName,
};
buffer.writeln('$formattedName: $opCount');
}
return buffer.toString();
}
@observable
String? logFilePath;
@observable
Directory? logDir;
WalletLoadingService walletLoadingService = getIt.get<WalletLoadingService>();
WalletListViewModel walletListViewModel = getIt.get<WalletListViewModel>();
SettingsStore settingsStore = getIt.get<SettingsStore>();
KeyService keyService = getIt.get<KeyService>();
final _random = Random();
Timer? _fuzzerTimer;
static const String _fuzzyFileName = '.wallet-fuzzer';
static const String _lastFuzzFile = 'last_fuzz';
static const String _statsFile = 'fuzzer_stats';
final allTypes = [
WalletType.monero,
WalletType.bitcoin,
WalletType.litecoin,
WalletType.ethereum,
WalletType.bitcoinCash,
WalletType.polygon,
WalletType.solana,
WalletType.tron,
WalletType.zano,
];
Future<void> _initialize() async {
final appDir = await getAppDir();
logDir = Directory(p.join(appDir.path, 'fuzzy'));
if (!await logDir!.exists()) {
await logDir!.create(recursive: true);
}
final formattedDate = DateFormat('yyyyMMdd_HHmmss').format(appStartDate);
logFilePath = p.join(logDir!.path, 'log-$formattedDate.txt');
await _logToFile('Wallet Fuzzer initialized');
await _loadOperationStats();
}
Future<void> _loadOperationStats() async {
try {
final appDir = await getAppDir();
final statsFilePath = p.join(appDir.path, _statsFile);
final statsFile = File(statsFilePath);
if (await statsFile.exists()) {
final statsJson = await statsFile.readAsString();
final stats = jsonDecode(statsJson) as Map<String, dynamic>;
operationStats = stats.map((key, value) => MapEntry(key, value as int));
await _logAction('Loaded operation statistics',
result: operationStats.entries.map((e) => '${e.key}: ${e.value}').join(', '));
} else {
await _logAction('No previous operation statistics found');
}
} catch (e) {
await _logAction('Failed to load operation statistics', result: e.toString());
}
}
Future<void> _saveOperationStats() async {
try {
final appDir = await getAppDir();
final statsFilePath = p.join(appDir.path, _statsFile);
final statsFile = File(statsFilePath);
final statsJson = jsonEncode(operationStats);
await statsFile.writeAsString(statsJson);
} catch (e) {
await _logAction('Failed to save operation statistics', result: e.toString());
}
}
Future<void> _incrementOperationStat(String operation) async {
if (operationStats.containsKey(operation)) {
operationStats[operation] = operationStats[operation]! + 1;
} else {
operationStats[operation] = 1;
}
await _saveOperationStats();
}
Future<void> startFuzzing() async {
if (isRunning) return;
isRunning = true;
await _logAction('Starting wallet fuzzer');
await _clearLastFuzzFile();
await _ensureEnoughWallets();
_fuzzerTimer = Timer.periodic(Duration(seconds: 1), (_) => _performRandomOperation());
}
Future<void> _createFuzzyFile() async {
final appDir = await getAppDir();
final fuzzyFile = File(p.join(appDir.path, _fuzzyFileName));
if (!await fuzzyFile.exists()) {
await fuzzyFile.create();
await _logAction('Created fuzzy file marker for auto-restart');
}
}
Future<void> _removeFuzzyFile() async {
final appDir = await getAppDir();
final fuzzyFile = File(p.join(appDir.path, _fuzzyFileName));
if (await fuzzyFile.exists()) {
await fuzzyFile.delete();
await _logAction('Removed fuzzy file marker');
}
}
@action
Future<void> stopFuzzing() async {
if (!isRunning) return;
_fuzzerTimer?.cancel();
_fuzzerTimer = null;
isRunning = false;
await _logAction('Stopping wallet fuzzer');
}
Future<void> _ensureEnoughWallets() async {
await _logAction('Checking if we need to create wallets');
final wallets = walletListViewModel.wallets;
final walletsByType = <WalletType, List<WalletListItem>>{};
for (final wallet in wallets.where((w) => !w.isHardware)) {
if (!walletsByType.containsKey(wallet.type)) {
walletsByType[wallet.type] = [];
}
walletsByType[wallet.type]!.add(wallet);
}
const MIN_WALLETS = 8;
for (final type in allTypes) {
final count = walletsByType[type]?.length ?? 0;
if (count < MIN_WALLETS) {
await _logAction('Not enough wallets of type ${type.toString()}. Have: $count, Need: $MIN_WALLETS');
await _createWalletsForType(type, MIN_WALLETS - count);
} else {
await _logAction('Enough wallets of type ${type.toString()}. Have: $count');
}
}
}
Future<void> _createWalletsForType(WalletType type, int count) async {
await _incrementOperationStat('createWalletsForType');
await _logAction('Creating $count wallets for type ${type.toString()}');
final walletCreationService = getIt.get<WalletCreationService>(param1: type);
for (int i = 0; i < count; i++) {
await _logAction('Creating wallet ${i+1} of $count');
final index = i;
await _createSingleWallet(type, index, count, walletCreationService);
await Future.delayed(Duration(milliseconds: 500));
}
}
Future<void> _createSingleWallet(
WalletType type, int index, int totalCount, WalletCreationService service) async {
try {
currentOperation = 'Creating wallet of type ${type.toString()} (${index+1}/$totalCount)';
final timestamp = DateFormat('yyyyMMddHHmmss').format(DateTime.now());
final randomSuffix = _random.nextInt(9999).toString().padLeft(4, '0');
final walletName = 'fuzzy_${type.toString().split('.').last}_${timestamp}_$randomSuffix';
await _logAction('Creating wallet credentials', result: walletName);
final dirPath = await pathForWalletDir(name: walletName, type: type);
final path = await pathForWallet(name: walletName, type: type);
final credentials = await _prepareCredentialsForType(type, walletName);
credentials.walletInfo = WalletInfo.external(
id: WalletBase.idFor(walletName, type),
name: walletName,
type: type,
isRecovery: false,
restoreHeight: credentials.height ?? 0,
date: DateTime.now(),
path: path,
dirPath: dirPath,
address: '',
showIntroCakePayCard: false,
derivationInfo: credentials.derivationInfo,
hardwareWalletType: credentials.hardwareWalletType,
);
_logAction('Creating wallet', result: walletName);
final wallet = await service.create(credentials, isTestnet: false);
_logAction('Wallet created', result: walletName);
await _logWalletState(wallet);
final node = settingsStore.getCurrentNode(wallet.type);
_logAction('Connecting to node', result: walletName);
await wallet.connectToNode(node: node);
_logAction('Starting sync', result: walletName);
await wallet.startSync();
await Future.delayed(Duration(seconds: 5 + _random.nextInt(25)));
_logAction('Stopping sync', result: walletName);
await wallet.stopSync();
_logAction('Closing wallet', result: walletName);
await wallet.close(shouldCleanup: true);
_logAction('Wallet closed', result: walletName);
} catch (e) {
errorsEncountered++;
await _logAction('Failed to create wallet of type ${type.toString()}', result: e.toString());
}
}
Future<WalletCredentials> _prepareCredentialsForType(WalletType type, String name) async {
final password = generateWalletPassword();
switch (type) {
case WalletType.monero:
return monero!.createMoneroNewWalletCredentials(
name: name,
language: 'English',
seedType: MoneroSeedType.legacy.raw,
passphrase: '',
password: password,
);
case WalletType.bitcoin:
return bitcoin!.createBitcoinNewWalletCredentials(
name: name,
password: password,
passphrase: '',
);
case WalletType.litecoin:
return bitcoin!.createBitcoinNewWalletCredentials(
name: name,
password: password,
passphrase: '',
);
case WalletType.bitcoinCash:
return bitcoinCash!.createBitcoinCashNewWalletCredentials(
name: name,
password: password,
passphrase: '',
);
case WalletType.ethereum:
return ethereum!.createEthereumNewWalletCredentials(
name: name,
password: password,
passphrase: '',
);
case WalletType.nano:
case WalletType.banano:
return nano!.createNanoNewWalletCredentials(
name: name,
password: password,
passphrase: '',
);
case WalletType.polygon:
return polygon!.createPolygonNewWalletCredentials(
name: name,
password: password,
passphrase: '',
);
case WalletType.solana:
return solana!.createSolanaNewWalletCredentials(
name: name,
password: password,
passphrase: '',
);
case WalletType.tron:
return tron!.createTronNewWalletCredentials(
name: name,
password: password,
passphrase: '',
);
case WalletType.zano:
return zano!.createZanoNewWalletCredentials(
name: name,
password: password,
passphrase: '',
);
case WalletType.decred:
return decred!.createDecredNewWalletCredentials(name: name);
default:
throw Exception('Wallet creation not yet implemented for ${type.toString()}');
}
}
Future<void> clearLogs() async {
logs.clear();
await _logAction('Logs cleared from UI');
}
@action
Future<void> _logAction(String action, {String? result}) async {
final entry = FuzzyLogEntry(
timestamp: DateTime.now(),
action: action,
result: result,
);
logs.insert(0, entry);
if (logs.length > 500) {
logs.removeLast();
}
await _logToFile(entry.toString());
}
Future<void> _logToFile(String message) async {
try {
final file = File(logFilePath!);
final sink = await file.open(mode: FileMode.append);
sink.writeStringSync('$message\n');
await sink.close();
} catch (e) {
printV('Error writing to log file: $e');
}
}
bool isBusy = false;
Future<void> _performRandomOperation() async {
if (!isRunning) return;
if (isBusy) return;
isBusy = true;
final wallets = walletListViewModel.wallets;
if (wallets.isEmpty) {
await _logAction('No wallets available to test');
return;
}
try {
await _incrementOperationStat('performRandomOperation');
final operations = [
_loadRandomWallet,
_syncRandomWallet,
_checkAndCreateWallets,
];
final operation = operations[_random.nextInt(operations.length)];
await operation();
operationsCompleted++;
} catch (e, s) {
errorsEncountered++;
await _logAction('Error performing operation', result: e.toString());
printV('Error: $e\nStack trace: $s');
} finally {
isBusy = false;
}
}
Future<void> _loadRandomWallet() async {
await _incrementOperationStat('loadWallet');
final wallets = walletListViewModel.wallets
.where((wallet) => !wallet.isHardware)
.toList();
if (wallets.isEmpty) {
await _logAction('No non-hardware wallets available to load');
return;
}
final walletItem = wallets[_random.nextInt(wallets.length)];
currentWallet = walletItem.name;
currentOperation = 'Loading wallet ${walletItem.name}';
await _logAction('Loading wallet', result: walletItem.name);
try {
final wallet = await walletLoadingService.load(walletItem.type, walletItem.name);
await _logAction('Wallet loaded successfully', result: walletItem.name);
await _logWalletState(wallet);
await _closeWallet(wallet);
} catch (e) {
await _logWalletStateByName(walletItem.name, e.toString().replaceAll("\n", ";"));
await _logAction('Failed to load wallet', result: '${walletItem.name}: $e');
throw e;
}
}
Future<void> _syncRandomWallet() async {
await _incrementOperationStat('syncWallet');
final wallets = walletListViewModel.wallets
.where((wallet) => !wallet.isHardware)
.toList();
if (wallets.isEmpty) {
await _logAction('No non-hardware wallets available to sync');
return;
}
final walletItem = wallets[_random.nextInt(wallets.length)];
currentWallet = walletItem.name;
currentOperation = 'Syncing wallet ${walletItem.name}';
await _logAction('Starting sync for wallet', result: walletItem.name);
try {
WalletBase wallet;
wallet = await walletLoadingService.load(walletItem.type, walletItem.name);
await _logAction("loaded wallet: ${wallet.name} as ${walletItem.name}");
await _logWalletState(wallet);
final node = settingsStore.getCurrentNode(wallet.type);
await wallet.connectToNode(node: node);
await wallet.startSync();
await _logAction('Sync started for wallet', result: walletItem.name);
final syncDuration = Duration(seconds: 5 + _random.nextInt(5));
await _logAction('Syncing for ${syncDuration.inSeconds} seconds', result: walletItem.name);
await Future.delayed(syncDuration);
final syncStatus = wallet.syncStatus;
await _logAction('Sync status', result: '${walletItem.name}: ${syncStatus.runtimeType}');
await wallet.stopSync();
await _logAction('Sync stopped for wallet', result: walletItem.name);
await _logWalletState(wallet);
await _closeWallet(wallet);
} catch (e) {
await _logAction('Error during sync operation', result: '${walletItem.name}: $e');
throw e;
}
}
Future<void> _closeWallet(WalletBase wallet) async {
final walletName = wallet.name;
await _logAction('Starting wallet close procedure', result: walletName);
try {
if (wallet.syncStatus is SyncingSyncStatus) {
await wallet.stopSync();
await _logAction('Stopped sync before closing', result: walletName);
}
await wallet.close(shouldCleanup: true);
await _logAction('Wallet closed successfully', result: walletName);
} catch (e) {
await _logAction('Error in wallet close procedure', result: '$walletName: $e');
throw e;
}
}
Future<void> _checkAndCreateWallets() async {
await _incrementOperationStat('checkAndCreateWallets');
currentOperation = 'Checking wallet counts and creating new ones if needed';
await _logAction('Performing wallet count check and creation');
await _ensureEnoughWallets();
}
Future<void> _logWalletStateByName(String walletName, String data) async {
try {
final appDir = await getAppDir();
final lastFuzzFile = File(p.join(appDir.path, _lastFuzzFile));
await lastFuzzFile.writeAsString('$walletName|$data\n', mode: FileMode.append);
await _logAction('Updated wallet state file');
} catch (e) {
await _logAction('Failed to update wallet state file', result: e.toString());
}
}
Future<void> _logWalletState(WalletBase wallet) async {
try {
final appDir = await getAppDir();
final walletInfo = _getWalletStateInfo(wallet);
final lastFuzzFile = File(p.join(appDir.path, _lastFuzzFile));
await lastFuzzFile.writeAsString('$walletInfo\n', mode: FileMode.append);
await _logAction('Updated wallet state file', result: walletInfo);
} catch (e) {
await _logAction('Failed to update wallet state file', result: e.toString());
}
}
String _getWalletStateInfo(WalletBase wallet) {
String seed = wallet.seed??'noseed';
String keys = "";
try {
keys = wallet.keys.toString();
} catch (e) {
keys = 'nokeys$e';
}
final data = '${wallet.name}|${wallet.type}|${seed}|${keys}'.replaceAll("\n", ";");
return data;
}
Future<void> _clearLastFuzzFile() async {
try {
final appDir = await getAppDir();
final lastFuzzFile = File(p.join(appDir.path, _lastFuzzFile));
if (await lastFuzzFile.exists()) {
await lastFuzzFile.writeAsString('');
await _logAction('Cleared last_fuzz file');
}
} catch (e) {
await _logAction('Failed to clear last_fuzz file', result: e.toString());
}
}
Map<String, dynamic> getOperationStatsWithTiming() {
final now = DateTime.now();
final runDuration = now.difference(appStartDate);
final totalOps = operationStats.values.fold(0, (a, b) => a + b);
double opsPerHour = 0;
if (runDuration.inMinutes >= 1) {
final runHours = runDuration.inMilliseconds / (1000 * 60 * 60);
opsPerHour = totalOps / runHours;
}
return {
'stats': Map.from(operationStats),
'totalOperations': totalOps,
'runningTimeMinutes': runDuration.inMinutes,
'operationsPerHour': opsPerHour,
'startTimestamp': appStartDate.toIso8601String(),
'currentTimestamp': now.toIso8601String(),
};
}
}

View file

@ -0,0 +1,120 @@
#!/bin/bash
FUZZER_DATA_DIR="./wallet_fuzzer_data"
clear_screen() {
clear
}
print_header() {
local col_width=$1
shift
printf "%-${col_width}s" "OPERATION"
for device in "$@"; do
printf "| %-15s " "$(basename "$device")"
done
echo
# Print separator line
printf "%${col_width}s" | tr ' ' '-'
for ((i=1; i<=$#; i++)); do
printf "%s" "+-----------------"
done
echo
}
display_stats() {
clear_screen
echo "=== WALLET FUZZER STATISTICS === (refreshes every 5 seconds)"
echo "Last update: $(date)"
echo
device_dirs=($FUZZER_DATA_DIR/*)
if [ ${#device_dirs[@]} -eq 0 ]; then
echo "No device data found in $FUZZER_DATA_DIR"
return
fi
operations=()
for device in "${device_dirs[@]}"; do
if [ -f "$device/fuzzer_stats.json" ]; then
if command -v jq &>/dev/null; then
device_ops=$(jq -r 'keys[]' "$device/fuzzer_stats.json" 2>/dev/null)
for op in $device_ops; do
if [[ ! " ${operations[@]} " =~ " ${op} " ]]; then
operations+=("$op")
fi
done
fi
fi
done
IFS=$'\n' sorted_operations=($(sort <<<"${operations[*]}"))
unset IFS
col_width=20
for op in "${sorted_operations[@]}"; do
if [ ${#op} -gt $col_width ]; then
col_width=${#op}
fi
done
col_width=$((col_width + 5))
print_header $col_width "${device_dirs[@]}"
for op in "${sorted_operations[@]}"; do
printf "%-${col_width}s" "$op"
for device in "${device_dirs[@]}"; do
if [ -f "$device/fuzzer_stats.json" ] && command -v jq &>/dev/null; then
value=$(jq -r ".[\"$op\"] // 0" "$device/fuzzer_stats.json" 2>/dev/null)
if [ -z "$value" ] || [ "$value" = "null" ]; then
value=0
fi
printf "| %-15s " "$value"
else
printf "| %-15s " "N/A"
fi
done
echo
done
printf "%-${col_width}s" "TOTAL"
grand_total=0
for device in "${device_dirs[@]}"; do
if [ -f "$device/fuzzer_stats.json" ] && command -v jq &>/dev/null; then
total=$(jq -r 'to_entries | map(.value) | add' "$device/fuzzer_stats.json" 2>/dev/null)
if [ -z "$total" ] || [ "$total" = "null" ]; then
total=0
fi
printf "| %-15s " "$total"
grand_total=$((grand_total + total))
else
printf "| %-15s " "N/A"
fi
done
echo
echo
echo "Total operations across all devices: $grand_total"
}
main() {
trap 'echo "Exiting..."; exit 0' INT
while true; do
display_stats
sleep 5
done
}
if ! command -v jq &>/dev/null; then
echo "Warning: jq is not installed. This script requires jq to parse JSON data."
echo "Please install jq with your package manager (e.g., apt, brew, etc.)"
exit 1
fi
main

View file

@ -0,0 +1,132 @@
#!/bin/bash
# This script extracts the device's screen resolution and the raw touch coordinate maximums,
# listens for a tap event, scales the raw touch coordinates to the screen resolution,
# and then outputs and executes an adb tap command.
screen_info=$(adb shell wm size | ggrep -oP "\d+x\d+")
if [[ -z "$screen_info" ]]; then
echo "Failed to get screen resolution."
exit 1
fi
SCREEN_WIDTH=${screen_info%x*}
SCREEN_HEIGHT=${screen_info#*x}
echo "Screen resolution: ${SCREEN_WIDTH}x${SCREEN_HEIGHT}"
echo "Detecting touch input device..."
TOUCH_DEVICE=""
devices_info=$(adb shell getevent -i)
echo "Found input devices:"
while read -r line; do
if [[ $line =~ ^add\ device\ ([0-9]+):\ (.+) ]]; then
device_num="${BASH_REMATCH[1]}"
device_path="${BASH_REMATCH[2]}"
device_name=$(echo "$devices_info" | ggrep -A 5 "add device $device_num:" | ggrep "name:" | gawk -F'"' '{print $2}')
echo " Device $device_num: $device_path ($device_name)"
# Check if device has touch capabilities
# Look for ABS events 0035 and 0036 which correspond to X and Y coordinates
touch_info=$(adb shell getevent -pl "$device_path")
if echo "$touch_info" | ggrep -q "ABS.*0035" && echo "$touch_info" | ggrep -q "ABS.*0036"; then
TOUCH_DEVICE="$device_path"
echo "Selected touch device at $TOUCH_DEVICE"
fi
fi
done < <(echo "$devices_info" | ggrep "^add device")
if [[ -z "$TOUCH_DEVICE" ]]; then
echo "Failed to detect touch input device."
echo "Trying to use the first 'multi_touch' device as fallback..."
while read -r line; do
if [[ $line =~ ^add\ device\ ([0-9]+):\ (.+) ]]; then
device_num="${BASH_REMATCH[1]}"
device_path="${BASH_REMATCH[2]}"
device_name=$(echo "$devices_info" | ggrep -A 5 "add device $device_num:" | ggrep "name:" | gawk -F'"' '{print $2}')
if [[ "$device_name" == *"multi_touch"* ]]; then
TOUCH_DEVICE="$device_path"
echo "Selected fallback touch device at $TOUCH_DEVICE ($device_name)"
break
fi
fi
done < <(echo "$devices_info" | ggrep "^add device")
fi
if [[ -z "$TOUCH_DEVICE" ]]; then
echo "No touch device found. Please manually specify the device node as an argument."
echo "Available devices:"
echo "$devices_info" | ggrep -A 5 "^add device" | ggrep -E "^add device|name:"
exit 1
fi
raw_info=$(adb shell getevent -pl "$TOUCH_DEVICE")
# Extract X and Y max values - try multiple ABS codes that could represent X/Y coordinates
extract_max() {
local raw_info="$1"
local abs_code="$2"
echo "$raw_info" | gawk -v code="$abs_code" '
$0 ~ code {
for(i=1; i<=NF; i++) {
if($i == "max") {
print $(i+1)
exit
}
}
}
'
}
# Try to find X max from either ABS_MT_POSITION_X or plain X position (0035)
RAW_MAX_X=$(extract_max "$raw_info" "ABS_MT_POSITION_X")
if [[ -z "$RAW_MAX_X" ]]; then
RAW_MAX_X=$(extract_max "$raw_info" "0035")
fi
# Try to find Y max from either ABS_MT_POSITION_Y or plain Y position (0036)
RAW_MAX_Y=$(extract_max "$raw_info" "ABS_MT_POSITION_Y")
if [[ -z "$RAW_MAX_Y" ]]; then
RAW_MAX_Y=$(extract_max "$raw_info" "0036")
fi
if [[ -z "$RAW_MAX_X" || -z "$RAW_MAX_Y" ]]; then
echo "Failed to extract raw maximum values for touch coordinates."
echo "Using default values for emulator: 32767 x 32767"
RAW_MAX_X=32767
RAW_MAX_Y=32767
fi
echo "Raw touch coordinate range: X max=$RAW_MAX_X, Y max=$RAW_MAX_Y"
echo "Waiting for a tap event. Press Ctrl+C to exit..."
adb shell getevent -lt "$TOUCH_DEVICE" | gawk -v sw="$SCREEN_WIDTH" -v sh="$SCREEN_HEIGHT" -v rx="$RAW_MAX_X" -v ry="$RAW_MAX_Y" '
/ABS_MT_POSITION_X/ || /0035 / {
# Convert hex raw x-coordinate to a number.
raw_x = strtonum("0x" $NF)
}
/ABS_MT_POSITION_Y/ || /0036 / {
# Convert hex raw y-coordinate to a number.
raw_y = strtonum("0x" $NF)
# Extract the timestamp from the beginning of the line (e.g., "[ 43466.939179]")
if (match($0, /\[ *([0-9]+\.[0-9]+)\]/, arr)) {
curr_time = arr[1] + 0 # ensure numeric conversion
}
# If a previous timestamp exists, compute delay and print sleep command.
if (prev_time != "") {
delay = curr_time - prev_time
# Print a sleep command with the delay (formatted with microsecond precision)
printf "sleep %.6f\n", delay
}
prev_time = curr_time
# Scale raw coordinates to screen resolution coordinates.
scaled_x = int(raw_x * sw / rx)
scaled_y = int(raw_y * sh / ry)
# Print the input tap command.
printf "adb shell input tap %d %d\n", scaled_x, scaled_y
}
'

View file

@ -0,0 +1,421 @@
#!/bin/bash
set -e
APP_PACKAGE="com.cakewallet.cake_wallet"
ACTIVITY="com.cakewallet.cake_wallet.MainActivity"
LAST_FUZZ_FILE="app_flutter/last_fuzz"
STATS_FILE="app_flutter/fuzzer_stats"
LOCAL_FUZZ_DIR="./wallet_fuzzer_data"
function get_devices() {
adb devices | grep -v "List" | grep -v "^$" | awk '{print $1}'
}
function init_device_dirs() {
local device_id=$1
mkdir -p "$LOCAL_FUZZ_DIR/$device_id"
}
function start_app() {
local device_id=$1
adb -s $device_id shell am start -n $APP_PACKAGE/$ACTIVITY
echo "[$device_id] App started at $(date)"
# adb shell input tap <- you need to generate these commands in order to open the screen and start
# fuzzing.
# on your desktop run:
# ./scripts/android/debug/record_tap.sh
# and then copy the output into the adb shell input tap commands below.
# make sure to tap very briefly, otherwise multiple events will be generated.
sleep 6
adb -s $device_id shell input tap 602 1940
sleep 2.664044
adb -s $device_id shell input tap 473 1923
sleep 2.713335
adb -s $device_id shell input tap 459 2021
sleep 2.668894
adb -s $device_id shell input tap 515 2027
sleep 3.871710
adb -s $device_id shell input tap 977 141
sleep 4.263207
adb -s $device_id shell input tap 635 1801
sleep 3.804010
adb -s $device_id shell input tap 289 757
sleep 3.833263
adb -s $device_id shell input tap 962 2176
}
function stop_app() {
local device_id=$1
adb -s $device_id shell am force-stop $APP_PACKAGE
echo "[$device_id] App stopped at $(date)"
}
function get_operation_stats() {
local device_id=$1
local LOCAL_STATS_FILE="$LOCAL_FUZZ_DIR/$device_id/fuzzer_stats.json"
local LOCAL_STATS_HISTORY="$LOCAL_FUZZ_DIR/$device_id/stats_history.json"
echo "[$device_id] Pulling operation statistics..."
adb -s $device_id shell run-as $APP_PACKAGE cat $STATS_FILE > $LOCAL_STATS_FILE.new 2>/dev/null
if [ ! -s "$LOCAL_STATS_FILE.new" ]; then
echo "[$device_id] No operation statistics found or couldn't access file."
return 0
fi
if ! jq empty "$LOCAL_STATS_FILE.new" 2>/dev/null; then
echo "[$device_id] Invalid JSON data received from device"
cat "$LOCAL_STATS_FILE.new"
return 1
fi
if [ ! -f "$LOCAL_STATS_FILE" ]; then
# First time stats collection
mv "$LOCAL_STATS_FILE.new" "$LOCAL_STATS_FILE"
echo "[$device_id] Initial operation statistics recorded:"
cat "$LOCAL_STATS_FILE" | jq -r 'to_entries | .[] | "\(.key): \(.value)"' 2>/dev/null || echo "Error parsing JSON, raw content: $(cat $LOCAL_STATS_FILE)"
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
STATS_CONTENT=$(cat "$LOCAL_STATS_FILE")
echo "[{\"timestamp\": \"$TIMESTAMP\", \"stats\": $STATS_CONTENT}]" > "$LOCAL_STATS_HISTORY"
echo "[$device_id] Created initial stats history with timestamp"
return 0
fi
echo "[$device_id] Operation statistics changes since last run:"
if command -v jq >/dev/null 2>&1; then
if ! jq empty "$LOCAL_STATS_FILE" 2>/dev/null; then
echo "[$device_id] Previous stats file corrupted, creating new baseline"
mv "$LOCAL_STATS_FILE.new" "$LOCAL_STATS_FILE"
return 0
fi
PREV_STATS=$(cat "$LOCAL_STATS_FILE")
NEW_STATS=$(cat "$LOCAL_STATS_FILE.new")
echo "[$device_id] Operation | Previous | Current | Difference"
echo "[$device_id] ----------|----------|---------|----------"
ALL_OPS=$(jq -s '.[0] + .[1] | keys | .[]' "$LOCAL_STATS_FILE" "$LOCAL_STATS_FILE.new" | sort | uniq)
for operation in $ALL_OPS; do
clean_op=$(echo "$operation" | sed 's/^"//;s/"$//')
PREV_COUNT=$(jq -r ".[\"${clean_op}\"] // 0" <<< "$PREV_STATS")
NEW_COUNT=$(jq -r ".[\"${clean_op}\"] // 0" <<< "$NEW_STATS")
DIFF=$((NEW_COUNT - PREV_COUNT))
printf "[$device_id] %-10s | %8d | %7d | %10d\n" "$clean_op" $PREV_COUNT $NEW_COUNT $DIFF
done
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
if [ -f "$LOCAL_STATS_HISTORY" ]; then
if grep -q "^\[" "$LOCAL_STATS_HISTORY"; then
TEMP_FILE=$(mktemp)
jq --arg timestamp "$TIMESTAMP" --argjson stats "$NEW_STATS" \
'. + [{"timestamp": $timestamp, "stats": $stats}]' "$LOCAL_STATS_HISTORY" > "$TEMP_FILE"
mv "$TEMP_FILE" "$LOCAL_STATS_HISTORY"
else
TEMP_FILE=$(mktemp)
echo "[" > "$TEMP_FILE"
jq --arg timestamp "$TIMESTAMP" --argjson stats "$NEW_STATS" \
'{"timestamp": $timestamp, "stats": $stats}' <<< "{}" >> "$TEMP_FILE"
echo "]" >> "$TEMP_FILE"
mv "$TEMP_FILE" "$LOCAL_STATS_HISTORY"
fi
else
echo "[{\"timestamp\": \"$TIMESTAMP\", \"stats\": $NEW_STATS}]" > "$LOCAL_STATS_HISTORY"
fi
else
echo "[$device_id] jq not found. Install jq for better statistics display."
echo "[$device_id] Raw statistics: "
cat "$LOCAL_STATS_FILE.new"
fi
mv "$LOCAL_STATS_FILE.new" "$LOCAL_STATS_FILE"
return 0
}
function display_stats_summary() {
local device_id=$1
local LOCAL_STATS_HISTORY="$LOCAL_FUZZ_DIR/$device_id/stats_history.json"
local LOCAL_FUZZ_HISTORY="$LOCAL_FUZZ_DIR/$device_id/fuzz_history.log"
if [ ! -f "$LOCAL_STATS_HISTORY" ]; then
echo "[$device_id] No statistics history found."
return 0
fi
if command -v jq >/dev/null 2>&1; then
echo "[$device_id] === Wallet Fuzzer Statistics Summary ==="
if grep -q "^\[" "$LOCAL_STATS_HISTORY"; then
echo "[$device_id] History contains $(jq 'length' "$LOCAL_STATS_HISTORY") recorded sessions"
FIRST_TS=$(jq -r '.[0].timestamp' "$LOCAL_STATS_HISTORY")
LAST_TS=$(jq -r '.[-1].timestamp' "$LOCAL_STATS_HISTORY")
echo "[$device_id] Tracking period: $FIRST_TS to $LAST_TS"
LATEST_STATS=$(jq -r '.[-1].stats' "$LOCAL_STATS_HISTORY")
TOTAL_OPS=$(jq -r 'to_entries | map(.value) | add' <<< "$LATEST_STATS" 2>/dev/null || echo 0)
if [ "$TOTAL_OPS" = "" ] || [ "$TOTAL_OPS" = "null" ]; then
TOTAL_OPS=0
fi
echo "[$device_id] Grand total operations: $TOTAL_OPS"
echo "[$device_id] Total operations by type:"
jq -r 'to_entries | .[] | "\(.key): \(.value)"' <<< "$LATEST_STATS" | sort -k2 -nr | while read -r line; do
echo "[$device_id] $line"
done
ENTRIES_COUNT=$(jq 'length' "$LOCAL_STATS_HISTORY")
if [ "$ENTRIES_COUNT" -gt 1 ]; then
FIRST_DATE=$(date -d "$FIRST_TS" +%s 2>/dev/null || date -j -f "%Y-%m-%d %H:%M:%S" "$FIRST_TS" +%s)
LAST_DATE=$(date -d "$LAST_TS" +%s 2>/dev/null || date -j -f "%Y-%m-%d %H:%M:%S" "$LAST_TS" +%s)
DURATION_HOURS=$(( (LAST_DATE - FIRST_DATE) / 3600 ))
if [ "$DURATION_HOURS" -gt 0 ]; then
echo "[$device_id] Operations per hour:"
jq -r 'to_entries | .[] | "\(.key): \(.value)"' <<< "$LATEST_STATS" | while read -r line; do
OP_NAME=$(echo $line | cut -d':' -f1)
OP_COUNT=$(echo $line | cut -d':' -f2 | tr -d ' ')
OPS_PER_HOUR=$(echo "scale=2; $OP_COUNT / $DURATION_HOURS" | bc)
echo "[$device_id] $OP_NAME: $OPS_PER_HOUR"
done
TOTAL_OPS_PER_HOUR=$(echo "scale=2; $TOTAL_OPS / $DURATION_HOURS" | bc)
echo "[$device_id] Total: $TOTAL_OPS_PER_HOUR ops/hour"
fi
fi
else
echo "[$device_id] History contains 1 recorded session"
echo "[$device_id] Stats from current session:"
LATEST_STATS=$(cat "$LOCAL_STATS_HISTORY")
TOTAL_OPS=$(jq -r 'to_entries | map(.value) | add' <<< "$LATEST_STATS" 2>/dev/null || echo 0)
if [ "$TOTAL_OPS" = "" ] || [ "$TOTAL_OPS" = "null" ]; then
TOTAL_OPS=0
fi
echo "[$device_id] Grand total operations: $TOTAL_OPS"
echo "[$device_id] Total operations by type:"
jq -r 'to_entries | .[] | "\(.key): \(.value)"' <<< "$LATEST_STATS" | sort -k2 -nr | while read -r line; do
echo "[$device_id] $line"
done
fi
TOTAL_RUNS=$(grep -c "INITIAL" "$LOCAL_FUZZ_HISTORY" 2>/dev/null || echo 0)
echo "[$device_id] Total runs since tracking began: $TOTAL_RUNS"
else
echo "[$device_id] jq not installed. Install jq for statistics summary."
fi
}
function get_app_data() {
local device_id=$1
local LOCAL_FUZZ_FILE="$LOCAL_FUZZ_DIR/$device_id/last_fuzz"
local LOCAL_FUZZ_HISTORY="$LOCAL_FUZZ_DIR/$device_id/fuzz_history.log"
APP_DATA_DIR=$(adb -s $device_id shell run-as $APP_PACKAGE pwd)
if [ -z "$APP_DATA_DIR" ]; then
echo "[$device_id] Failed to get app data directory. Make sure app is in debug mode."
return 1
fi
adb -s $device_id shell run-as $APP_PACKAGE cat $LAST_FUZZ_FILE > $LOCAL_FUZZ_FILE.new 2>/dev/null
if [ ! -s "$LOCAL_FUZZ_FILE.new" ]; then
echo "[$device_id] No wallet state information found or couldn't access file."
return 0
fi
ENTRY_COUNT=$(wc -l < "$LOCAL_FUZZ_FILE.new")
echo "[$device_id] Found $ENTRY_COUNT wallet state entries"
if [ ! -f "$LOCAL_FUZZ_FILE" ]; then
mv "$LOCAL_FUZZ_FILE.new" "$LOCAL_FUZZ_FILE"
echo "[$device_id] Initial wallet state recorded with $ENTRY_COUNT entries:"
cat "$LOCAL_FUZZ_FILE" | while read -r line; do
echo "[$device_id] - $line"
done
echo "$(date): INITIAL: $ENTRY_COUNT entries" >> $LOCAL_FUZZ_HISTORY
return 0
fi
PREV_WALLETS=$(awk -F'|' '{print $1}' "$LOCAL_FUZZ_FILE" | sort | uniq)
NEW_WALLETS=$(awk -F'|' '{print $1}' "$LOCAL_FUZZ_FILE.new" | sort | uniq)
NEW_ONLY_WALLETS=$(comm -13 <(echo "$PREV_WALLETS") <(echo "$NEW_WALLETS"))
if [ ! -z "$NEW_ONLY_WALLETS" ]; then
echo "[$device_id] New wallets detected: $NEW_ONLY_WALLETS (normal behavior)"
echo "[$device_id] New wallet details:"
for wallet in $NEW_ONLY_WALLETS; do
(grep "^$wallet|" "$LOCAL_FUZZ_FILE.new" || true) | while read -r line; do
echo "[$device_id] - $line"
done
done
fi
MISSING_WALLETS=$(comm -23 <(echo "$PREV_WALLETS") <(echo "$NEW_WALLETS"))
if [ ! -z "$MISSING_WALLETS" ]; then
echo "[$device_id] Wallets no longer present: $MISSING_WALLETS"
echo "$(date): MISSING_WALLETS: $MISSING_WALLETS" >> $LOCAL_FUZZ_HISTORY
fi
COMMON_WALLETS=$(comm -12 <(echo "$PREV_WALLETS") <(echo "$NEW_WALLETS"))
CORRUPTION_DETECTED=0
EVIDENCE_DIR="$LOCAL_FUZZ_DIR/$device_id/corruption_$(date +%Y%m%d_%H%M%S)"
if [ ! -z "$COMMON_WALLETS" ]; then
echo "[$device_id] Checking common wallets for changes: $COMMON_WALLETS"
for wallet in $COMMON_WALLETS; do
PREV_ENTRIES=$(grep "^$wallet|" "$LOCAL_FUZZ_FILE" || true)
NEW_ENTRIES=$(grep "^$wallet|" "$LOCAL_FUZZ_FILE.new" || true)
PREV_COUNT=$(echo "$PREV_ENTRIES" | wc -l)
NEW_COUNT=$(echo "$NEW_ENTRIES" | wc -l)
echo "[$device_id] Wallet $wallet: $PREV_COUNT previous entries, $NEW_COUNT new entries"
if [ $NEW_COUNT -gt $PREV_COUNT ]; then
echo "[$device_id] Wallet $wallet has $((NEW_COUNT - PREV_COUNT)) new operation(s)"
fi
PREV_INFO=$(echo "$PREV_ENTRIES" | tail -n1)
NEW_INFO=$(echo "$NEW_ENTRIES" | tail -n1)
if [ "$PREV_INFO" = "$NEW_INFO" ]; then
echo "[$device_id] Wallet $wallet is unchanged"
continue
fi
mkdir -p "$EVIDENCE_DIR"
echo "[$device_id] WALLET CHANGED - POTENTIAL CORRUPTION DETECTED"
echo "[$device_id] Wallet: $wallet" | tee -a "$EVIDENCE_DIR/corruption_details.txt"
echo "[$device_id] Previous: $PREV_INFO" | tee -a "$EVIDENCE_DIR/corruption_details.txt"
echo "[$device_id] Current: $NEW_INFO" | tee -a "$EVIDENCE_DIR/corruption_details.txt"
CORRUPTION_DETECTED=1
done
fi
if [ $CORRUPTION_DETECTED -eq 1 ]; then
echo "$(date): CORRUPTION_DETECTED" >> $LOCAL_FUZZ_HISTORY
cp "$LOCAL_FUZZ_FILE" "$EVIDENCE_DIR/last_fuzz.prev"
cp "$LOCAL_FUZZ_FILE.new" "$EVIDENCE_DIR/last_fuzz.new"
echo "=== DETAILED COMPARISON $(date) ===" > "$EVIDENCE_DIR/comparison.txt"
echo "Previous file contents:" >> "$EVIDENCE_DIR/comparison.txt"
cat "$LOCAL_FUZZ_FILE" >> "$EVIDENCE_DIR/comparison.txt"
echo -e "\nNew file contents:" >> "$EVIDENCE_DIR/comparison.txt"
cat "$LOCAL_FUZZ_FILE.new" >> "$EVIDENCE_DIR/comparison.txt"
echo -e "\nDiff output:" >> "$EVIDENCE_DIR/comparison.txt"
diff "$LOCAL_FUZZ_FILE" "$LOCAL_FUZZ_FILE.new" >> "$EVIDENCE_DIR/comparison.txt" || true
echo "[$device_id] Pulling all files from app package directory..."
APP_FILES_DIR="$EVIDENCE_DIR/app_files"
mkdir -p "$APP_FILES_DIR" || true
FILE_LIST_TMP="$EVIDENCE_DIR/file_list.txt"
adb -s $device_id shell run-as $APP_PACKAGE "find . -type f | grep -v -E 'cache|no_backup'" > "$FILE_LIST_TMP"
TOTAL_FILES=$(wc -l < "$FILE_LIST_TMP")
echo "[$device_id] Found $TOTAL_FILES files to process"
FILE_COUNT=0
OLDIFS="$IFS"
IFS=$'\n'
for file_path in $(cat "$FILE_LIST_TMP"); do
FILE_COUNT=$((FILE_COUNT + 1))
echo "[$device_id] Processing file $FILE_COUNT/$TOTAL_FILES: $file_path"
rel_dir=$(dirname "$file_path")
mkdir -p "$APP_FILES_DIR/$rel_dir" || true
dest_file="$APP_FILES_DIR/$file_path"
adb -s $device_id shell run-as $APP_PACKAGE cat "$file_path" > "$dest_file" 2>/dev/null || echo "[$device_id] Failed to copy $file_path"
done
IFS="$OLDIFS"
echo "[$device_id] Evidence saved to $EVIDENCE_DIR"
fi
mv "$LOCAL_FUZZ_FILE.new" "$LOCAL_FUZZ_FILE"
if [ $CORRUPTION_DETECTED -eq 1 ]; then
return 1
else
return 0
fi
}
function run_device_test() {
local device_id=$1
stop_app $device_id
sleep 2
display_stats_summary $device_id
local error_detected=0
while [ $error_detected -eq 0 ]; do
start_app $device_id
RUN_TIME=$((30 + RANDOM % 120))
echo "[$device_id] Will run for $RUN_TIME seconds"
sleep $RUN_TIME
stop_app $device_id
get_operation_stats $device_id
get_app_data $device_id
if [ $? -ne 0 ]; then
echo "[$device_id] Stopping tests due to potential wallet corruption!"
error_detected=1
break
fi
WAIT_TIME=$((5 + RANDOM % 15))
echo "[$device_id] Will wait for $WAIT_TIME seconds"
sleep $WAIT_TIME
done
echo "[$device_id] Device testing completed"
}
mkdir -p $LOCAL_FUZZ_DIR
DEVICES=$(get_devices)
DEVICE_COUNT=$(echo "$DEVICES" | wc -l)
if [ -z "$DEVICES" ]; then
echo "No ADB devices connected. Exiting."
exit 1
fi
echo "Found $DEVICE_COUNT connected device(s):"
echo "$DEVICES" | while read device; do
echo "- $device"
init_device_dirs $device
done
for device in $DEVICES; do
echo "Starting tests on device $device"
RANDOM_DELAY=$(awk -v min=0.05 -v max=1.00 'BEGIN{srand(); print min+rand()*(max-min)}')
echo "[$device] Waiting for $RANDOM_DELAY seconds before starting test"
sleep $RANDOM_DELAY
run_device_test $device &
done
wait
echo "All device tests completed"