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:
2026-05-11 10:41:41 +00:00
parent ebe1d8acda
commit 6c01ac1c1b
28 changed files with 1102 additions and 48 deletions

View File

@@ -0,0 +1,83 @@
/// <reference types="node" />
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { handleSettingsCommand } from "../../../src/cli/commands/settings";
import { CommandError } from "../../../src/cli/commands/types";
import { createMockIO, createMockPaths } from "../mocks/command";
describe("settings command", () => {
let tempDir: string;
beforeEach(() => {
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-settings-command-test-"));
});
afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});
test("shows default settings when .wallet does not exist", async () => {
const { io, capture } = createMockIO();
const paths = createMockPaths(tempDir);
const result = await handleSettingsCommand({ io, paths }, ["show"], {});
expect(result).toEqual({ currency: "USD" });
expect(capture.out.join("\n")).toContain("currency");
expect(capture.out.join("\n")).toContain("USD");
});
test("sets and gets currency", async () => {
const { io, capture } = createMockIO();
const paths = createMockPaths(tempDir);
await handleSettingsCommand({ io, paths }, ["set", "currency", "aud"], {});
const getResult = await handleSettingsCommand(
{ io, paths },
["get", "currency"],
{},
);
expect(getResult).toEqual({ key: "currency", value: "AUD" });
expect(capture.out).toContain("Updated currency: AUD");
expect(capture.out).toContain("AUD");
});
test("sets default-mnemonic and persists JSON .wallet", async () => {
const { io } = createMockIO();
const paths = createMockPaths(tempDir);
await handleSettingsCommand(
{ io, paths },
["set", "default-mnemonic", "mnemonic-primary"],
{},
);
const persisted = JSON.parse(readFileSync(paths.walletConfigPath, "utf8")) as {
currency: string;
"default-mnemonic"?: string;
};
expect(persisted).toEqual({
currency: "USD",
"default-mnemonic": "mnemonic-primary",
});
});
test("throws command error for unknown subcommand", async () => {
const { io } = createMockIO();
const paths = createMockPaths(tempDir);
try {
await handleSettingsCommand({ io, paths }, ["unknown"], {});
expect.fail("Expected settings command to throw");
} catch (error) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe("settings.subcommand.unknown");
}
});
});

View File

@@ -0,0 +1,40 @@
/// <reference types="node" />
import { describe, expect, test } from "vitest";
import { BaseRates } from "../../src/utils/rates/base-rates";
/**
* Minimal concrete adapter used only for testing BaseRates helpers.
*/
class TestRatesAdapter extends BaseRates {
public async start(): Promise<void> {
return;
}
public async stop(): Promise<void> {
return;
}
public async listPairs(): Promise<Set<string>> {
return new Set(["USD/BCH", "DOGE/BCH"]);
}
}
describe("BaseRates.formatCurrency", () => {
test("formats ISO currency codes with Intl currency style", () => {
const rates = new TestRatesAdapter();
const formatted = rates.formatCurrency(12.5, "USD");
expect(formatted).toContain("$");
expect(formatted).toContain("12.50");
});
test("formats non-ISO symbols without throwing", () => {
const rates = new TestRatesAdapter();
const formatted = rates.formatCurrency(12.3456789, "DOGE");
expect(formatted).toContain("DOGE");
expect(formatted).toContain("12.3456789");
});
});

View File

@@ -0,0 +1,96 @@
/// <reference types="node" />
import { beforeEach, afterEach, describe, expect, test } from "vitest";
import {
existsSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { SettingsService } from "../../src/services/settings";
/**
* Tests for SettingsService persistence and migration behavior.
*/
describe("SettingsService", () => {
let testDir: string;
let settingsPath: string;
beforeEach(() => {
testDir = mkdtempSync(join(tmpdir(), "xo-cli-settings-test-"));
settingsPath = join(testDir, ".wallet");
});
afterEach(() => {
rmSync(testDir, { recursive: true, force: true });
});
test("returns defaults when settings file does not exist", () => {
const settings = new SettingsService(settingsPath);
expect(settings.getDefaultMnemonic()).toBeUndefined();
expect(settings.getCurrency()).toBe("USD");
expect(settings.getSettings()).toEqual({ currency: "USD" });
expect(existsSync(settingsPath)).toBe(false);
});
test("migrates legacy .wallet plaintext mnemonic to JSON", () => {
writeFileSync(settingsPath, "mnemonic-legacy", "utf8");
const settings = new SettingsService(settingsPath);
expect(settings.getDefaultMnemonic()).toBe("mnemonic-legacy");
expect(settings.getCurrency()).toBe("USD");
const persisted = JSON.parse(readFileSync(settingsPath, "utf8")) as {
"default-mnemonic"?: string;
currency: string;
};
expect(persisted).toEqual({
"default-mnemonic": "mnemonic-legacy",
currency: "USD",
});
});
test("normalizes and persists currency and default-mnemonic", () => {
const settings = new SettingsService(settingsPath);
settings.setDefaultMnemonic(" mnemonic-primary ");
settings.setCurrency("aud");
expect(settings.getSettings()).toEqual({
"default-mnemonic": "mnemonic-primary",
currency: "AUD",
});
const persisted = JSON.parse(readFileSync(settingsPath, "utf8")) as {
"default-mnemonic"?: string;
currency: string;
};
expect(persisted).toEqual({
"default-mnemonic": "mnemonic-primary",
currency: "AUD",
});
});
test("emits settings-updated events on setting changes", () => {
const settings = new SettingsService(settingsPath);
const events: Array<{ key: string; value: string | undefined }> = [];
settings.on("settings-updated", (event) => {
events.push({ key: event.key, value: event.value });
});
settings.setCurrency("cad");
settings.setDefaultMnemonic("mnemonic-blue");
expect(events).toEqual([
{ key: "currency", value: "CAD" },
{ key: "default-mnemonic", value: "mnemonic-blue" },
]);
});
});