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',