import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { EventEmitter } from "../utils/event-emitter.js"; import { getSettingsPath } from "../utils/paths.js"; /** * Supported persisted settings keys. */ export type SettingsData = { "default-mnemonic"?: string; currency: string; }; /** * Event payloads emitted by {@link SettingsService}. */ export type SettingsServiceEventMap = { "settings-updated": { key: keyof SettingsData; value: string | undefined; settings: SettingsData; }; }; /** * Runtime defaults for settings that should always exist in memory. */ const DEFAULT_SETTINGS: SettingsData = { currency: "USD", }; /** * Handles loading, migrating, and persisting wallet settings. * * The backing file is `/.wallet`. Historically it stored a raw * mnemonic reference string. This service migrates that legacy format to JSON: * `{ "default-mnemonic": "", "currency": "USD" }`. */ export class SettingsService extends EventEmitter { private readonly settingsPath: string; private settings: SettingsData; /** * Creates a new settings service instance. * * @param settingsPath - Optional custom settings file path (useful for tests) */ constructor(settingsPath: string = getSettingsPath()) { super(); this.settingsPath = settingsPath; this.settings = this.loadSettings(); } /** * Returns the current settings snapshot. */ public getSettings(): SettingsData { return { ...this.settings }; } /** * Returns the currently selected default mnemonic reference. */ public getDefaultMnemonic(): string | undefined { return this.settings["default-mnemonic"]; } /** * Updates the default mnemonic reference and persists it to disk. */ public setDefaultMnemonic(mnemonicRef: string): void { const normalizedMnemonicRef = mnemonicRef.trim(); if (normalizedMnemonicRef.length === 0) { throw new Error("default-mnemonic cannot be empty"); } this.settings["default-mnemonic"] = normalizedMnemonicRef; this.persistSettings(); this.emit("settings-updated", { key: "default-mnemonic", value: normalizedMnemonicRef, settings: this.getSettings(), }); } /** * Returns the selected fiat currency code (ISO-like uppercase). */ public getCurrency(): string { return this.settings.currency; } /** * Updates the selected fiat currency and persists it to disk. */ public setCurrency(currencyCode: string): void { const normalizedCurrency = this.normalizeCurrency(currencyCode); if (this.settings.currency === normalizedCurrency) { return; } this.settings.currency = normalizedCurrency; this.persistSettings(); this.emit("settings-updated", { key: "currency", value: normalizedCurrency, settings: this.getSettings(), }); } /** * Reads and normalizes the settings file from disk. * * If the file contains the old legacy format (raw mnemonic string), the * migrated JSON shape is written back immediately. */ private loadSettings(): SettingsData { if (!existsSync(this.settingsPath)) { return { ...DEFAULT_SETTINGS }; } const rawContents = readFileSync(this.settingsPath, "utf8").trim(); if (rawContents.length === 0) { return { ...DEFAULT_SETTINGS }; } try { const parsed = JSON.parse(rawContents); const normalized = this.normalizeSettings(parsed); return normalized; } catch { const migrated = this.normalizeSettings({ "default-mnemonic": rawContents, }); this.persistSettings(migrated); return migrated; } } /** * Writes the given settings object to disk as pretty JSON. * * @param nextSettings - Optional explicit value, defaults to in-memory state */ private persistSettings(nextSettings?: SettingsData): void { if (nextSettings) { this.settings = nextSettings; } writeFileSync( this.settingsPath, `${JSON.stringify(this.settings, null, 2)}\n`, "utf8", ); } /** * Coerces unknown input into a safe settings object. */ private normalizeSettings(input: unknown): SettingsData { const normalized: SettingsData = { ...DEFAULT_SETTINGS, }; if (!input || typeof input !== "object") { return normalized; } const maybeMnemonic = (input as Record)["default-mnemonic"]; if (typeof maybeMnemonic === "string" && maybeMnemonic.trim().length > 0) { normalized["default-mnemonic"] = maybeMnemonic.trim(); } const maybeCurrency = (input as Record).currency; if (typeof maybeCurrency === "string" && maybeCurrency.trim().length > 0) { normalized.currency = this.normalizeCurrency(maybeCurrency); } return normalized; } /** * Ensures currency values stay uppercase and non-empty. */ private normalizeCurrency(currencyCode: string): string { const normalizedCurrency = currencyCode.trim().toUpperCase(); if (normalizedCurrency.length === 0) { throw new Error("currency cannot be empty"); } return normalizedCurrency; } }