dev: add recovery screen when sqlite db doesn't open (#3179)
* dev: add recovery screen when sqlite db doesn't open * Update cw_core/lib/db/sqlite_debug.dart [skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
cyan committed
Jun 15, 2026 at 01:21 UTC
241503774f64e345ff5ec372775d0a09acf55e1a
3 files changed
+209
cw_core/lib/db/sqlite.dart
+20
@@ -1,8 +1,10 @@
1
import 'dart:io';
2
3
+import 'package:cw_core/db/sqlite_debug.dart';
4
import 'package:cw_core/root_dir.dart';
5
import 'package:cw_core/utils/print_verbose.dart';
6
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
7
+import 'package:path/path.dart' as p;
8
9
Database? db;
10
@@ -22,7 +24,25 @@ Future<void> _addColumnIfNotExists(
24
}
25
}
26
27
+Future<File> sqliteDebugMarkerFile() async {
28
+ final appDir = await getAppDir();
29
+ final dbDebugMarker = p.join(appDir.path, ".sqlite_db_debug");
30
+ return File(dbDebugMarker);
31
+}
32
+
33
Future<void> initDb({String? pathOverride}) async {
34
+ final dbDebugMarker = await sqliteDebugMarkerFile();
35
+ try {
36
+ if (dbDebugMarker.existsSync()) {
37
+ throw Exception("Debug marker is present");
38
+ }
39
+ await _initDb(pathOverride: pathOverride);
40
+ } catch (e, s) {
41
+ await handleSqliteError(e, s);
42
+ }
43
+}
44
+
45
+Future<void> _initDb({String? pathOverride}) async {
46
if (Platform.isLinux || Platform.isWindows) {
47
databaseFactory = databaseFactoryFfi;
48
}
cw_core/lib/db/sqlite_debug.dart
new
+177
@@ -0,0 +1,177 @@
1
+import 'dart:convert';
2
+import 'dart:io';
3
+import 'dart:typed_data';
4
+
5
+import 'package:cw_core/db/sqlite.dart';
6
+import 'package:cw_core/root_dir.dart';
7
+import 'package:flutter/foundation.dart';
8
+import 'package:flutter/material.dart';
9
+
10
+Future<void> handleSqliteError(final dynamic error, final StackTrace stackTrace) async {
11
+ try {
12
+ final f = await sqliteDebugMarkerFile();
13
+ f.createSync();
14
+ } catch (_) {}
15
+ runApp(
16
+ SqliteErrorHandlerApp(
17
+ error: error,
18
+ stackTrace: stackTrace,
19
+ ),
20
+ );
21
+ await Future.delayed(Duration(hours: 24));
22
+}
23
+
24
+class SqliteErrorHandlerApp extends StatelessWidget {
25
+ const SqliteErrorHandlerApp({
26
+ super.key,
27
+ required this.error,
28
+ required this.stackTrace,
29
+ });
30
+
31
+ final dynamic error;
32
+ final StackTrace? stackTrace;
33
+ @override
34
+ Widget build(final BuildContext context) {
35
+ return MaterialApp(
36
+ debugShowCheckedModeBanner: false,
37
+ title: 'Cake Wallet Crash',
38
+ themeMode: ThemeMode.light,
39
+ home: SqliteErrorHandler(
40
+ error: error,
41
+ stackTrace: stackTrace,
42
+ ),
43
+ );
44
+ }
45
+}
46
+
47
+class SqliteErrorHandler extends StatefulWidget {
48
+ SqliteErrorHandler({
49
+ super.key,
50
+ required this.error,
51
+ required this.stackTrace,
52
+ });
53
+
54
+ final dynamic error;
55
+ final StackTrace? stackTrace;
56
+
57
+ @override
58
+ State<SqliteErrorHandler> createState() => _SqliteErrorHandlerState();
59
+}
60
+
61
+class _SqliteErrorHandlerState extends State<SqliteErrorHandler> {
62
+ String log = "";
63
+
64
+ @override
65
+ Widget build(final BuildContext context) {
66
+ return Scaffold(
67
+ appBar: AppBar(
68
+ title: Text("Sqlite Recovery Screen"),
69
+ ),
70
+ body: SingleChildScrollView(
71
+ child: Padding(
72
+ padding: const EdgeInsets.all(8.0),
73
+ child: Column(
74
+ crossAxisAlignment: CrossAxisAlignment.start,
75
+ children: _buildNotice(),
76
+ ),
77
+ ),
78
+ ),
79
+ );
80
+ }
81
+
82
+ Widget _text(final dynamic element) {
83
+ return SelectableText(
84
+ element.toString(),
85
+ );
86
+ }
87
+
88
+ void _dbgRemoveDebugMarker() async {
89
+ final f = await sqliteDebugMarkerFile();
90
+ f.delete();
91
+ }
92
+
93
+ void _corruptTheDb() async {
94
+ final dbFile = File("${(await getAppDir()).path}/cake.db");
95
+ await writeBytesToFile(dbFile);
96
+ }
97
+
98
+ void _dumpDb() async {
99
+ try {
100
+ final dbFile = File("${(await getAppDir()).path}/cake.db");
101
+ final jsonText = JsonEncoder.withIndent(" ").convert(await dumpCustomDb(dbFile.path));
102
+ setState(() {
103
+ log = jsonText;
104
+ });
105
+ } catch (e, s) {
106
+ setState(() {
107
+ log = """
108
+e: $e
109
+
110
+s: $s
111
+""";
112
+ });
113
+ }
114
+ }
115
+
116
+ void _removeDb() async {
117
+ try {
118
+ File("${(await getAppDir()).path}/cake.db.old").deleteSync();
119
+ } catch (_) {}
120
+ final dbFile = File("${(await getAppDir()).path}/cake.db");
121
+ dbFile.renameSync("${(await getAppDir()).path}/cake.db.old");
122
+ }
123
+
124
+ List<Widget> _debugOptions() {
125
+ return [
126
+ _debugButton("Remove debug marker", _dbgRemoveDebugMarker),
127
+ if (kDebugMode || kProfileMode) _debugButton("[DO NOT PRESS] Corrupt the DB", _corruptTheDb),
128
+ _debugButton("Remove DB", _removeDb),
129
+ _debugButton("Dump DB", _dumpDb),
130
+ ];
131
+ }
132
+
133
+ Widget _debugButton(String text, VoidCallback callback) {
134
+ return ElevatedButton(
135
+ onPressed: callback,
136
+ child: Text(text),
137
+ );
138
+ }
139
+
140
+ List<Widget> _buildNotice() {
141
+ return [
142
+ _text("""Critical error occured in SQLite
143
+This is very rare, usually caused by the device killing the app before it finishes writing file.
144
+Here you can recover the database or recreate it to regain access to your wallets.
145
+During this process some of the metadata may get lost.
146
+Please contact support@cakewallet.com if you need any help."""),
147
+ Divider(),
148
+ ..._debugOptions(),
149
+ if (log.isNotEmpty) ...[
150
+ Divider(),
151
+ SelectableText(
152
+ log,
153
+ style: TextStyle(
154
+ fontSize: 8,
155
+ ),
156
+ ),
157
+ ],
158
+ Divider(),
159
+ _text(widget.error),
160
+ Divider(),
161
+ _text(widget.stackTrace),
162
+ ];
163
+ }
164
+}
165
+
166
+Future<void> writeBytesToFile(File file) async {
167
+ const int seek = 5;
168
+ const int count = 50000;
169
+ final raf = await file.open(mode: FileMode.writeOnlyAppend);
170
+ try {
171
+ await raf.setPosition(seek);
172
+ final zeros = Uint8List.fromList(List.generate(count, (_) => 0x42));
173
+ await raf.writeFrom(zeros);
174
+ } finally {
175
+ await raf.close();
176
+ }
177
+}
lib/src/screens/settings/other_settings_page.dart
+12
@@ -1,3 +1,5 @@
1
+import 'dart:io';
2
+
3
import 'package:cake_wallet/bitcoin/bitcoin.dart';
4
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
5
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_selector.dart';
@@ -14,11 +16,13 @@ import 'package:cake_wallet/utils/feature_flag.dart';
16
import 'package:cake_wallet/utils/share_util.dart';
17
import 'package:cake_wallet/utils/show_pop_up.dart';
18
import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
19
+import 'package:cw_core/root_dir.dart';
20
import 'package:cw_core/wallet_info.dart';
21
import 'package:cw_core/transaction_priority.dart';
22
import 'package:cw_core/wallet_type.dart';
23
import 'package:cw_core/db/sqlite.dart';
24
import 'package:flutter/material.dart';
25
+import 'package:path/path.dart' as p;
26
27
class OtherSettingsPage extends BasePage {
28
OtherSettingsPage(this._otherSettingsViewModel) {
@@ -211,6 +215,14 @@ class OtherSettingsPage extends BasePage {
215
),
216
);
217
}),
218
+ ListItemRegularRow(
219
+ keyValue: '[dev] fake corrupt sqlite db',
220
+ label: '[dev] fake corrupt sqlite db',
221
+ onTap: () async {
222
+ final dbDebugMarker = await sqliteDebugMarkerFile();
223
+ dbDebugMarker.create();
224
+ }
225
+ ),
226
]
227
}),
228
);