From 2d7c7e9b24bc4364afc5e48ad91dbadd290d7372 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:17:14 +0800 Subject: [PATCH] test(wallet): verify legacy cocod database migration --- src/daemon/wallet/coco-client.test.ts | 63 +++++++++++++++++- src/daemon/wallet/coco-client.ts | 43 ++++++++---- .../wallet/fixtures/cocod-0.0.24-wallet.db.gz | Bin 0 -> 6021 bytes 3 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz diff --git a/src/daemon/wallet/coco-client.test.ts b/src/daemon/wallet/coco-client.test.ts index 38f9143..49d2667 100644 --- a/src/daemon/wallet/coco-client.test.ts +++ b/src/daemon/wallet/coco-client.test.ts @@ -1,7 +1,12 @@ -import { describe, expect, it, mock } from "bun:test"; +import { afterEach, describe, expect, it, mock } from "bun:test"; +import { gunzipSync } from "bun"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; import { assertLegacyCocodNotRunning, claimLegacyCocodPidFile, + createCocoClient, } from "./coco-client"; type GuardOptions = NonNullable< @@ -11,11 +16,67 @@ type LegacyFetch = NonNullable; const SOCKET_PATH = "/tmp/routstrd-test/cocod.sock"; const PID_FILE_PATH = "/tmp/routstrd-test/cocod.pid"; +const tempDirs: string[] = []; + +function makeTempDir(): string { + const dir = mkdtempSync(join(tmpdir(), "routstrd-coco-migration-test-")); + tempDirs.push(dir); + return dir; +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); function socketOnly(path: string): boolean { return path === SOCKET_PATH; } +describe("legacy cocod wallet migration", () => { + it("opens an existing unencrypted config and preserves database balances", async () => { + const walletDir = join(makeTempDir(), ".cocod"); + const mintUrl = "https://mint.example.com"; + mkdirSync(walletDir, { recursive: true }); + writeFileSync( + join(walletDir, "config.json"), + JSON.stringify({ + version: 1, + mnemonic: + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + encrypted: false, + }), + ); + + // This fixture was generated with @routstr/cocod 0.0.24 using its + // coco-cashu-sqlite-bun 1.1.2-rc.50 adapter. Keeping it frozen prevents + // this test from accidentally creating its "legacy" database with the + // same current adapter that createCocoClient uses to read it. + const fixture = readFileSync( + join(import.meta.dir, "fixtures", "cocod-0.0.24-wallet.db.gz"), + ); + writeFileSync(join(walletDir, "coco.db"), gunzipSync(fixture)); + + const client = await createCocoClient({ configDir: walletDir }); + try { + expect(await client.getStatus()).toBe("UNLOCKED"); + expect(await client.getBalances()).toEqual({ [mintUrl]: 10 }); + } finally { + await client.dispose?.(); + } + + // Prove the migrated schema remains reopenable and the pre-existing + // proofs survive a complete in-process wallet restart. + const reopenedClient = await createCocoClient({ configDir: walletDir }); + try { + expect(await reopenedClient.getBalances()).toEqual({ [mintUrl]: 10 }); + } finally { + await reopenedClient.dispose?.(); + } + }); +}); + describe("assertLegacyCocodNotRunning", () => { it("does not probe when the legacy socket and PID file do not exist", async () => { const fetchImpl = mock(async () => new Response("pong")); diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index cac507f..d16f130 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -64,13 +64,13 @@ interface CocodConfig { encrypted: boolean; } -function loadMnemonic(): string { - if (!existsSync(CONFIG_FILE)) { +function loadMnemonic(configFile: string = CONFIG_FILE): string { + if (!existsSync(configFile)) { throw new Error( - `Config file not found at ${CONFIG_FILE}. Run 'routstrd onboard' first.`, + `Config file not found at ${configFile}. Run 'routstrd onboard' first.`, ); } - const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) as CocodConfig; + const config = JSON.parse(readFileSync(configFile, "utf-8")) as CocodConfig; if (config.encrypted) { throw new Error( "Encrypted wallets are not supported yet. Please use an unencrypted wallet.", @@ -79,11 +79,6 @@ function loadMnemonic(): string { return config.mnemonic; } -async function seedGetter(): Promise { - const mnemonic = loadMnemonic(); - return mnemonicToSeedSync(mnemonic); -} - function defaultIsProcessRunning(pid: number): boolean { try { process.kill(pid, 0); @@ -282,21 +277,41 @@ export function claimLegacyCocodPidFile( }; } -export async function createCocoClient(): Promise { - await assertLegacyCocodNotRunning(); - const releaseLegacyPidClaim = claimLegacyCocodPidFile(); +export interface CreateCocoClientOptions { + /** Override the wallet directory, primarily for migration tests and tooling. */ + configDir?: string; + socketPath?: string; + pidFilePath?: string; +} + +export async function createCocoClient( + options: CreateCocoClientOptions = {}, +): Promise { + const configDir = options.configDir || CONFIG_DIR; + const configFile = join(configDir, "config.json"); + const dbPath = join(configDir, "coco.db"); + const socketPath = options.socketPath || + (options.configDir ? join(configDir, "cocod.sock") : LEGACY_COCOD_SOCKET); + const pidFilePath = options.pidFilePath || + (options.configDir ? join(configDir, "cocod.pid") : LEGACY_COCOD_PID_FILE); + + await assertLegacyCocodNotRunning({ socketPath, pidFilePath }); + const releaseLegacyPidClaim = claimLegacyCocodPidFile({ pidFilePath }); let database: Database | undefined; let coco: Awaited> | undefined; try { - database = new Database(DB_PATH); + // Read and validate the existing cocod config during startup rather than + // deferring failure until coco-core first needs wallet key material. + const seed = mnemonicToSeedSync(loadMnemonic(configFile)); + database = new Database(dbPath); const repo = new SqliteRepositories({ database }); await repo.init(); coco = await initializeCoco({ repo, - seedGetter, + seedGetter: async () => seed, }); } catch (error) { database?.close(); diff --git a/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz b/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz new file mode 100644 index 0000000000000000000000000000000000000000..a7fc1aa68cd2ff8d38a4e51ee22493c39d5a5566 GIT binary patch literal 6021 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 E13yy00RR91 literal 0 HcmV?d00001