195 lines
5.1 KiB
TypeScript
195 lines
5.1 KiB
TypeScript
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 `<XO_CONFIG_DIR>/.wallet`. Historically it stored a raw
|
|
* mnemonic reference string. This service migrates that legacy format to JSON:
|
|
* `{ "default-mnemonic": "<value>", "currency": "USD" }`.
|
|
*/
|
|
export class SettingsService extends EventEmitter<SettingsServiceEventMap> {
|
|
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<string, unknown>)["default-mnemonic"];
|
|
if (typeof maybeMnemonic === "string" && maybeMnemonic.trim().length > 0) {
|
|
normalized["default-mnemonic"] = maybeMnemonic.trim();
|
|
}
|
|
|
|
const maybeCurrency = (input as Record<string, unknown>).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;
|
|
}
|
|
}
|