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 );