2026-06-11 10:47:26 +02:00
|
|
|
using PalladiumWallet.Core.Chain;
|
|
|
|
|
|
|
|
|
|
namespace PalladiumWallet.Core.Storage;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-16 14:40:06 +02:00
|
|
|
/// Per-platform data paths (blueprint §8). The data root can be:
|
|
|
|
|
/// 1. <b>portable</b>: "palladium-data" folder next to the executable;
|
|
|
|
|
/// 2. <b>custom</b>: chosen by the user at first launch and stored in
|
|
|
|
|
/// a small "pointer" file at a fixed bootstrap location;
|
|
|
|
|
/// 3. <b>legacy</b>: old location (%APPDATA%/PalladiumWallet) if it already holds data;
|
|
|
|
|
/// 4. <b>default</b>: ~/.PalladiumWallet (Linux/macOS) or %ProgramFiles%\PalladiumWallet (Windows).
|
|
|
|
|
/// Under the root there is a per-network subfolder (config, wallet, header, certificates).
|
2026-06-11 10:47:26 +02:00
|
|
|
/// </summary>
|
|
|
|
|
public static class AppPaths
|
|
|
|
|
{
|
|
|
|
|
public const string PortableDirName = "palladium-data";
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>Application folder name, used in the various paths.</summary>
|
2026-06-12 09:25:00 +02:00
|
|
|
public const string AppDirName = "PalladiumWallet";
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>Explicit override of the data root (e.g. CLI --data-dir). Takes priority over everything.</summary>
|
2026-06-12 09:25:00 +02:00
|
|
|
public static string? OverrideDataRoot { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-16 14:40:06 +02:00
|
|
|
/// Default data root, following each platform's convention:
|
|
|
|
|
/// Windows → %APPDATA%\PalladiumWallet (PascalCase, like Electrum/Bitcoin);
|
|
|
|
|
/// Linux/macOS → ~/.palladium-wallet (lowercase dotfolder, like ~/.bitcoin).
|
|
|
|
|
/// Per-user and always writable, without administrator privileges.
|
2026-06-12 09:25:00 +02:00
|
|
|
/// </summary>
|
|
|
|
|
public static string DefaultDataRoot()
|
|
|
|
|
{
|
2026-06-12 10:13:09 +02:00
|
|
|
if (OperatingSystem.IsWindows())
|
|
|
|
|
return Path.Combine(
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
|
|
|
AppDirName);
|
2026-06-12 09:25:00 +02:00
|
|
|
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
2026-06-12 10:13:09 +02:00
|
|
|
return Path.Combine(home, ".palladium-wallet");
|
2026-06-12 09:25:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>Pointer file to the user-chosen data root. Lives at a
|
|
|
|
|
/// bootstrap location that is always writable and independent of the data root.</summary>
|
2026-06-12 09:25:00 +02:00
|
|
|
private static string LocationPointerPath() =>
|
|
|
|
|
Path.Combine(
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
|
|
|
AppDirName, "data-location");
|
|
|
|
|
|
|
|
|
|
private static string PortableRoot() =>
|
|
|
|
|
Path.Combine(AppContext.BaseDirectory, PortableDirName);
|
|
|
|
|
|
|
|
|
|
private static bool HasData(string root) =>
|
|
|
|
|
Directory.Exists(root) && Directory.EnumerateFileSystemEntries(root).Any();
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>Effective data root, following the precedence order documented on the class.</summary>
|
2026-06-11 10:47:26 +02:00
|
|
|
public static string DataRoot()
|
|
|
|
|
{
|
2026-06-12 09:25:00 +02:00
|
|
|
if (!string.IsNullOrEmpty(OverrideDataRoot))
|
|
|
|
|
return OverrideDataRoot;
|
|
|
|
|
|
|
|
|
|
var portable = PortableRoot();
|
2026-06-11 10:47:26 +02:00
|
|
|
if (Directory.Exists(portable))
|
|
|
|
|
return portable;
|
|
|
|
|
|
2026-06-12 09:25:00 +02:00
|
|
|
if (ReadPointer() is { } custom)
|
|
|
|
|
return custom;
|
|
|
|
|
|
|
|
|
|
return DefaultDataRoot();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-16 14:40:06 +02:00
|
|
|
/// true if the data location is already determined and need not be asked
|
|
|
|
|
/// of the user: portable mode, override, pointer already written, or
|
|
|
|
|
/// data already present at the default location.
|
2026-06-12 09:25:00 +02:00
|
|
|
/// </summary>
|
|
|
|
|
public static bool IsDataLocationConfigured() =>
|
|
|
|
|
!string.IsNullOrEmpty(OverrideDataRoot)
|
|
|
|
|
|| Directory.Exists(PortableRoot())
|
|
|
|
|
|| ReadPointer() is not null
|
|
|
|
|
|| HasData(DefaultDataRoot());
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>Stores the user-chosen data root and creates it on disk.</summary>
|
2026-06-12 09:25:00 +02:00
|
|
|
public static void ConfigureDataLocation(string root)
|
|
|
|
|
{
|
|
|
|
|
root = Path.GetFullPath(root.Trim());
|
|
|
|
|
Directory.CreateDirectory(root);
|
|
|
|
|
var pointer = LocationPointerPath();
|
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(pointer)!);
|
|
|
|
|
File.WriteAllText(pointer, root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string? ReadPointer()
|
|
|
|
|
{
|
|
|
|
|
var pointer = LocationPointerPath();
|
|
|
|
|
if (!File.Exists(pointer))
|
|
|
|
|
return null;
|
|
|
|
|
var path = File.ReadAllText(pointer).Trim();
|
|
|
|
|
return string.IsNullOrEmpty(path) ? null : path;
|
2026-06-11 10:47:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>Network data folder (config, wallet, header, certificates).</summary>
|
2026-06-11 10:47:26 +02:00
|
|
|
public static string ForNetwork(NetKind net)
|
|
|
|
|
{
|
|
|
|
|
var dir = Path.Combine(DataRoot(), ChainProfiles.For(net).NetName);
|
|
|
|
|
Directory.CreateDirectory(dir);
|
|
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string WalletsDir(NetKind net)
|
|
|
|
|
{
|
|
|
|
|
var dir = Path.Combine(ForNetwork(net), "wallets");
|
|
|
|
|
Directory.CreateDirectory(dir);
|
|
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string DefaultWalletPath(NetKind net) =>
|
|
|
|
|
Path.Combine(WalletsDir(net), "default.wallet.json");
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>All wallet files for the network, ordered by name (multi-wallet §8).</summary>
|
2026-06-12 10:13:09 +02:00
|
|
|
public static IReadOnlyList<string> WalletFiles(NetKind net)
|
|
|
|
|
{
|
|
|
|
|
var dir = WalletsDir(net);
|
|
|
|
|
return Directory.EnumerateFiles(dir, "*.wallet.json")
|
|
|
|
|
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
.ToList();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 10:47:26 +02:00
|
|
|
public static string CertificatePinsPath(NetKind net) =>
|
|
|
|
|
Path.Combine(ForNetwork(net), "server-certs.json");
|
|
|
|
|
|
2026-06-11 11:27:14 +02:00
|
|
|
public static string ServersPath(NetKind net) =>
|
|
|
|
|
Path.Combine(ForNetwork(net), "servers.json");
|
|
|
|
|
|
2026-06-11 10:47:26 +02:00
|
|
|
public static string ConfigPath() =>
|
|
|
|
|
Path.Combine(DataRoot(), "config.json");
|
|
|
|
|
}
|