CW1272 tails, linux generic fixes (#2630)
* fix: linux/tails fixes, sqlite fix on linux * add sqlite deps * fix: old/new dir migration from .local * fix: tails path, local/share old names for compatibility * Update cw_core/lib/db/sqlite.dart Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com> * fix: close icon on desktop [skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com> Co-authored-by: Konstiantin Ullrich <konstantin@cakewallet.com>
cyan committed
Nov 20, 2025 at 15:25 UTC
a7734c5c4447d38c9bdd17b04c3d1d783b4c7172
16 files changed
+189
-25
cw_bitcoin/lib/electrum_wallet.dart
+2
-1
@@ -5,6 +5,7 @@ import 'dart:isolate';
5
6
import 'package:bitcoin_base/bitcoin_base.dart';
7
import 'package:cw_core/hardware/hardware_wallet_service.dart';
8
+import 'package:cw_core/root_dir.dart';
9
import 'package:cw_core/utils/proxy_wrapper.dart';
10
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
11
import 'package:cw_core/utils/print_verbose.dart';
@@ -374,7 +375,7 @@ abstract class ElectrumWalletBase
375
runningIsolate.kill(priority: Isolate.immediate);
376
}
377
377
- final appDir = await getApplicationSupportDirectory();
378
+ final appDir = await getAppDir();
379
String debugLogPath = "${appDir.path}/logs/debug.log";
380
381
final receivePort = ReceivePort();
cw_bitcoin/pubspec.lock
+8
@@ -1084,6 +1084,14 @@ packages:
1084
url: "https://pub.dev"
1085
source: hosted
1086
version: "2.9.0"
1087
+ sqlite3_flutter_libs:
1088
+ dependency: transitive
1089
+ description:
1090
+ name: sqlite3_flutter_libs
1091
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
1092
+ url: "https://pub.dev"
1093
+ source: hosted
1094
+ version: "0.5.40"
1095
stack_trace:
1096
dependency: transitive
1097
description:
cw_core/lib/db/sqlite.dart
+6
-1
@@ -1,9 +1,14 @@
1
2
-import 'package:sqflite/sqflite.dart';
2
+import 'dart:io';
3
+
4
+import 'package:sqflite_common_ffi/sqflite_ffi.dart';
5
6
late Database db;
7
8
Future<void> initDb({String? pathOverride}) async {
9
+ if (Platform.isLinux || Platform.isWindows) {
10
+ databaseFactory = databaseFactoryFfi;
11
+ }
12
db = await openDatabase(
13
pathOverride ?? "cake.db",
14
version: 1,
cw_core/lib/root_dir.dart
+100
-6
@@ -1,11 +1,76 @@
1
import 'dart:io';
2
+import 'package:cw_core/utils/print_verbose.dart';
3
import 'package:path_provider/path_provider.dart';
4
+import 'package:path/path.dart' as p;
5
6
String? _rootDirPath;
7
6
-void setRootDirFromEnv() => _rootDirPath = Platform.environment['CAKE_WALLET_DIR'];
8
+const String _tailsData = '/live/persistence/TailsData_unlocked/Persistent';
9
8
-Future<Directory> getAppDir({String appName = 'cake_wallet'}) async {
10
+bool get isNonAmnesticTails {
11
+ try {
12
+ final os = File("/etc/os-release").readAsLinesSync();
13
+ for (var line in os) {
14
+ if (!line.startsWith("ID=")) continue;
15
+ if (!line.contains("tails")) continue;
16
+ return Directory(_tailsData).existsSync();
17
+ }
18
+ } catch (e) {
19
+ return false;
20
+ }
21
+ return false;
22
+}
23
+
24
+bool showNotice = true;
25
+
26
+void setRootDirFromEnv() =>
27
+ _rootDirPath = Platform.environment['CAKE_WALLET_DIR'];
28
+
29
+void copyDirectory(Directory source, Directory destination) {
30
+ source.listSync(recursive: false).forEach((var entity) {
31
+ if (entity is Directory) {
32
+ var newDirectory = Directory(p.join(destination.absolute.path, p.basename(entity.path)));
33
+ newDirectory.createSync(recursive: true);
34
+ copyDirectory(entity.absolute, newDirectory);
35
+ } else if (entity is File) {
36
+ destination.createSync(recursive: true);
37
+ entity.copySync(p.join(destination.path, p.basename(entity.path)));
38
+ }
39
+ });
40
+}
41
+
42
+Future<void> linuxSymlinkSharedPreferences() async {
43
+ if (!Platform.isLinux) return; // nuh-uh
44
+ final dataHome = Platform.environment["XDG_DATA_HOME"] ?? p.join(Platform.environment["HOME"] ?? "", ".local", "share");
45
+ var cakeNames = ['com.example.cake_wallet', 'cake_wallet'];
46
+ for (String name in cakeNames) {
47
+ final oldPath = p.join(dataHome, name);
48
+ final newPath = p.join((await getAppDir()).path, "_local_share");
49
+ final oldDir = Directory(oldPath);
50
+ final oldLink = Link(oldPath);
51
+ final newDir = Directory(newPath);
52
+ if (oldDir.existsSync()) {
53
+ if (oldLink.existsSync()) {
54
+ printV("not creating, link exists");
55
+ } else {
56
+ if (newDir.existsSync()) {
57
+ newDir.renameSync("${newPath}_${DateTime.now().millisecondsSinceEpoch~/1000}");
58
+ }
59
+ copyDirectory(oldDir, newDir);
60
+ oldDir.deleteSync(recursive: true);
61
+ }
62
+ }
63
+ if (!oldLink.existsSync()) {
64
+ oldLink.create(newPath, recursive: true);
65
+ }
66
+ if (!newDir.existsSync()) {
67
+ newDir.createSync(recursive: true);
68
+ }
69
+ }
70
+}
71
+
72
+Future<Directory> getAppDir() async {
73
+ const String appName = 'cake_wallet';
74
Directory dir;
75
76
if (_rootDirPath != null && _rootDirPath!.isNotEmpty) {
@@ -15,16 +80,45 @@ Future<Directory> getAppDir({String appName = 'cake_wallet'}) async {
80
if (Platform.isWindows) {
81
dir = await getApplicationSupportDirectory();
82
} else if (Platform.isLinux) {
18
- String appDirPath;
19
-
83
+ String? appDirPath;
84
try {
85
dir = await getApplicationDocumentsDirectory();
86
appDirPath = '${dir.path}/$appName';
87
} catch (e) {
24
- appDirPath = '/home/${Platform.environment['USER']}/.$appName';
88
+ appDirPath = null;
89
+ }
90
+ // App will try to use last entry in here, so {distro,package}-specific paths can be
91
+ // be put as one of last items (tails - I'm looking at you), and other paths can be
92
+ // added in the order of preference
93
+ // Which currently is $HOME/.config/$appName - as this is the most standard directory
94
+ // for storing things that users in general back-up
95
+ var linuxAppPath = [
96
+ if (appDirPath != null) appDirPath, // old preferred
97
+ p.join('/home', Platform.environment['USER']??"null", appName), // old fallback
98
+ if (Platform.environment['HOME'] != null) p.join(Platform.environment['HOME']!, ".$appName"), // old fallback but using HOME
99
+ if (Platform.environment['HOME'] != null) p.join(Platform.environment['HOME']!, '.config', appName), // old fallback but using HOME
100
+ if (isNonAmnesticTails) p.join(_tailsData, ".$appName") // tails (if persistance is enabled)
101
+ ];
102
+
103
+ String preferredPath = linuxAppPath.last;
104
+
105
+ preferredLoop:
106
+ for (String notSoPreferredPath in linuxAppPath) {
107
+ if (notSoPreferredPath == linuxAppPath.last) continue;
108
+ bool useThisOne = Directory(notSoPreferredPath).existsSync();
109
+ if (useThisOne) {
110
+ if (showNotice) {
111
+ showNotice = false;
112
+ printV("Not using $preferredPath because $notSoPreferredPath exists, falling back for backwards compatibility");
113
+ printV("Can't see your wallet? Check\n - ${linuxAppPath.join("\n - ")}\n and move directory that to $preferredPath");
114
+ printV("Or use CAKE_WALLET_DIR=/path/to/app/ ${Platform.executable}");
115
+ }
116
+ preferredPath = notSoPreferredPath;
117
+ break preferredLoop;
118
+ }
119
}
120
27
- dir = Directory.fromUri(Uri.file(appDirPath));
121
+ dir = Directory.fromUri(Uri.file(preferredPath));
122
await dir.create(recursive: true);
123
} else {
124
dir = await getApplicationDocumentsDirectory();
cw_core/lib/utils/tor/abstract.dart
+8
-7
@@ -31,16 +31,17 @@ abstract class CakeTorInstance {
31
final uri = Uri.tryParse(socksServer);
32
if (uri != null) {
33
return CakeTorSocks(uri.port);
34
+ } else {
35
+ final uri = Uri.tryParse("socks5://$socksServer");
36
+ if (uri != null) {
37
+ return CakeTorSocks(uri.port);
38
+ }
39
}
40
}
36
- final os = File("/etc/os-release").readAsLinesSync();
37
- for (var line in os) {
38
- if (!line.startsWith("ID=")) continue;
39
- if (!line.contains("tails")) continue;
40
- return CakeTorSocks(9150);
41
- }
41
} catch (e) {
43
- printV("Failed to identify linux version - /etc/os-release missing");
42
+ printV(
43
+ "Failed to identify linux version - no SOCKS_SERVER variable found or malformed",
44
+ );
45
}
46
}
47
try {
cw_core/pubspec.lock
+8
@@ -730,6 +730,14 @@ packages:
730
url: "https://pub.dev"
731
source: hosted
732
version: "2.9.0"
733
+ sqlite3_flutter_libs:
734
+ dependency: "direct main"
735
+ description:
736
+ name: sqlite3_flutter_libs
737
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
738
+ url: "https://pub.dev"
739
+ source: hosted
740
+ version: "0.5.40"
741
stack_trace:
742
dependency: transitive
743
description:
cw_core/pubspec.yaml
+2
-1
@@ -46,6 +46,7 @@ dependencies:
46
ref: cake-update-v2
47
sqflite: ^2.4.1
48
sqflite_common_ffi: ^2.3.4+4
49
+ sqlite3_flutter_libs: 0.5.40
50
51
dev_dependencies:
52
flutter_test:
@@ -63,7 +64,7 @@ dependency_overrides:
64
65
# The following section is specific to Flutter.
66
flutter:
66
- uses-material-design: true
67
+ uses-material-design: true
68
69
# To add assets to your package, add an assets section, like this:
70
# assets:
cw_decred/pubspec.lock
+8
@@ -761,6 +761,14 @@ packages:
761
url: "https://pub.dev"
762
source: hosted
763
version: "2.9.0"
764
+ sqlite3_flutter_libs:
765
+ dependency: transitive
766
+ description:
767
+ name: sqlite3_flutter_libs
768
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
769
+ url: "https://pub.dev"
770
+ source: hosted
771
+ version: "0.5.40"
772
stack_trace:
773
dependency: transitive
774
description:
cw_monero/pubspec.lock
+8
@@ -874,6 +874,14 @@ packages:
874
url: "https://pub.dev"
875
source: hosted
876
version: "2.9.0"
877
+ sqlite3_flutter_libs:
878
+ dependency: transitive
879
+ description:
880
+ name: sqlite3_flutter_libs
881
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
882
+ url: "https://pub.dev"
883
+ source: hosted
884
+ version: "0.5.40"
885
stack_trace:
886
dependency: transitive
887
description:
cw_monero/test/monero_wallet_service_test.dart
-1
@@ -21,7 +21,6 @@ Future<void> main() async {
21
late File moneroCBinary;
22
23
setUpAll(() async {
24
- databaseFactory = databaseFactoryFfi;
24
await initDb(pathOverride: './test/data/db');
25
Hive.init('./test/data/db');
26
PathProviderPlatform.instance = MockPathProviderPlatform();
cw_nano/pubspec.lock
+8
@@ -879,6 +879,14 @@ packages:
879
url: "https://pub.dev"
880
source: hosted
881
version: "2.9.0"
882
+ sqlite3_flutter_libs:
883
+ dependency: transitive
884
+ description:
885
+ name: sqlite3_flutter_libs
886
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
887
+ url: "https://pub.dev"
888
+ source: hosted
889
+ version: "0.5.40"
890
stack_trace:
891
dependency: transitive
892
description:
cw_wownero/pubspec.lock
+8
@@ -778,6 +778,14 @@ packages:
778
url: "https://pub.dev"
779
source: hosted
780
version: "2.9.0"
781
+ sqlite3_flutter_libs:
782
+ dependency: transitive
783
+ description:
784
+ name: sqlite3_flutter_libs
785
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
786
+ url: "https://pub.dev"
787
+ source: hosted
788
+ version: "0.5.40"
789
stack_trace:
790
dependency: transitive
791
description:
cw_zano/pubspec.lock
+8
@@ -775,6 +775,14 @@ packages:
775
url: "https://pub.dev"
776
source: hosted
777
version: "2.9.0"
778
+ sqlite3_flutter_libs:
779
+ dependency: transitive
780
+ description:
781
+ name: sqlite3_flutter_libs
782
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
783
+ url: "https://pub.dev"
784
+ source: hosted
785
+ version: "0.5.40"
786
stack_trace:
787
dependency: transitive
788
description:
integration_test_runner.sh
+8
-7
@@ -14,6 +14,7 @@ RETRY_COUNT=${RETRY_COUNT:-1}
14
DATA_DIRS=(
15
"$HOME/.local/share/com.example.cake_wallet"
16
"$HOME/Documents/cake_wallet"
17
+ "$HOME/.config/cake_wallet"
18
)
19
20
# Global state
@@ -46,7 +47,7 @@ format_duration() {
47
local hours=$((seconds / 3600))
48
local minutes=$(((seconds % 3600) / 60))
49
local secs=$((seconds % 60))
49
-
50
+
51
if (( hours > 0 )); then
52
echo "${hours}h ${minutes}m ${secs}s"
53
elif (( minutes > 0 )); then
@@ -92,7 +93,7 @@ run_test() {
93
local end_time=$(date +%s)
94
local duration=$((end_time - start_time))
95
test_durations+=("$duration")
95
-
96
+
97
log "✅ Test passed: $test_name ($(format_duration $duration))"
98
passed_tests+=("$test_name")
99
return 0
@@ -101,9 +102,9 @@ run_test() {
102
local end_time=$(date +%s)
103
local duration=$((end_time - start_time))
104
test_durations+=("$duration")
104
-
105
+
106
log "❌ Test failed: $test_name ($(format_duration $duration))"
106
-
107
+
108
if (( retry_count < RETRY_COUNT )); then
109
log "Retrying test: $test_name"
110
retry_count=$((retry_count + 1))
@@ -125,7 +126,7 @@ main() {
126
while IFS= read -r -d $'\0' file; do
127
targets+=("$file")
128
done < <(find integration_test/test_suites -name "*.dart" -type f -print0)
128
-
129
+
130
if [[ $? -ne 0 ]]; then
131
error "Failed to find test files"
132
exit 1
@@ -140,7 +141,7 @@ main() {
141
142
# Record overall start time
143
local overall_start_time=$(date +%s)
143
-
144
+
145
# Run tests sequentially
146
for target in "${targets[@]}"; do
147
run_test "$target"
@@ -156,7 +157,7 @@ main() {
157
echo "Passed: ${#passed_tests[@]}"
158
echo "Failed: ${#failed_tests[@]}"
159
echo "Total duration: $(format_duration $total_duration)"
159
-
160
+
161
if (( ${#passed_tests[@]} > 0 )); then
162
echo -e "\n✅ Passed Tests:"
163
for i in $(seq 0 $((${#passed_tests[@]} - 1))); do
lib/main.dart
+6
@@ -96,6 +96,12 @@ Future<void> runAppWithZone({Key? topLevelKey}) async {
96
} catch (e) {
97
printV("Failed to initialize tor: $e");
98
}
99
+
100
+ try {
101
+ await linuxSymlinkSharedPreferences();
102
+ } catch (e) {
103
+ printV("Failed to symlink linux preferences: $e");
104
+ }
105
106
await initializeAppAtRoot();
107
lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart
+1
-1
@@ -48,7 +48,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
48
final zanoIcon = Image.asset('assets/images/crypto/zano.webp', height: 24, width: 24);
49
final decredIcon = Image.asset('assets/images/crypto/decred.webp', height: 24, width: 24);
50
final dogeIcon = Image.asset('assets/images/crypto/dogecoin.webp', height: 24, width: 24);
51
- final nonWalletTypeIcon = Image.asset('assets/images/close.webp', height: 24, width: 24);
51
+ final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
52
53
Image _newWalletImage(BuildContext context) => Image.asset(
54
'assets/images/new_wallet.png',