Add currency settings, Settings service, and dialog to select fiat currency. Add support for non Official currencies like DOGE when using rates.
This commit is contained in:
194
src/services/settings.ts
Normal file
194
src/services/settings.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
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 `~/.config/xo-cli/.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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user