mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 20:38:22 +00:00
feat(wallet): migrate cocod data to routstrd wallet directory
This commit is contained in:
@@ -0,0 +1,253 @@
|
|||||||
|
# Migrate wallet storage from `~/.cocod` to `~/.routstrd/wallet`
|
||||||
|
|
||||||
|
Task: [`routstrd-migrate-cocod-to-routstrd-files`](https://github.com/nodestrich/routstrd)
|
||||||
|
Event ID: `2f420644005154c5b106017c4a753854bb2660ba834cde7e6e267532cb5448e9`
|
||||||
|
Priority: 997 · Status: open
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Baseline
|
||||||
|
|
||||||
|
This plan is based on `coco-integration` at `86ac688` (after the default-mint, NPC/npubx.cash, and Windows-support changes).
|
||||||
|
|
||||||
|
The task branch is currently based on `04f8c22`; rebase it onto the latest `coco-integration` before implementing this plan.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Move the in-process Cashu wallet's persistent data from:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.cocod/config.json
|
||||||
|
~/.cocod/coco.db
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.routstrd/wallet/config.json
|
||||||
|
~/.routstrd/wallet/coco.db
|
||||||
|
```
|
||||||
|
|
||||||
|
The migration must preserve the mnemonic, proofs, default mint, and NPC identity. Existing users must not accidentally get a new wallet, and `cocod` and routstrd must never open the same database concurrently.
|
||||||
|
|
||||||
|
Fresh installs must use `~/.routstrd/wallet` immediately. Existing installs must be migrated automatically and safely; merely continuing to run from `~/.cocod` is not sufficient for this task.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Important corrections to the previous plan
|
||||||
|
|
||||||
|
### 1. Do not rename or migrate `cocod.sock`
|
||||||
|
|
||||||
|
The in-process `coco-core` wallet does not expose a Unix socket. `cocod.sock` belongs to the legacy external `cocod` daemon and is used only to determine whether that daemon is still alive.
|
||||||
|
|
||||||
|
There should therefore be no `wallet.sock`. Do not copy a socket inode into the new directory.
|
||||||
|
|
||||||
|
### 2. Keep legacy-process paths separate from wallet-data paths
|
||||||
|
|
||||||
|
On the current base, `src/daemon/wallet/coco-client.ts` derives all of these from one `CONFIG_DIR`:
|
||||||
|
|
||||||
|
- wallet config and database;
|
||||||
|
- legacy cocod socket;
|
||||||
|
- legacy cocod PID/process lock.
|
||||||
|
|
||||||
|
That coupling must be removed. After migration:
|
||||||
|
|
||||||
|
- wallet data comes from `~/.routstrd/wallet`;
|
||||||
|
- probes and shutdown logic for an external legacy cocod continue to use `~/.cocod/cocod.sock` and `~/.cocod/cocod.pid`;
|
||||||
|
- routstrd uses `~/.routstrd/wallet/wallet.pid` as the in-process wallet lock.
|
||||||
|
|
||||||
|
While routstrd owns the migrated wallet, it should also retain the existing legacy-cocod exclusion mechanism (claiming the legacy `cocod.pid`) so an old cocod cannot be started against a stale or partially migrated legacy wallet. Acquisition and release of both locks must be rollback-safe.
|
||||||
|
|
||||||
|
### 3. Do not rename `cocodPath`
|
||||||
|
|
||||||
|
`RoutstrdConfig.cocodPath` is an executable path for the external compatibility client, not a wallet storage path. Renaming it to `walletPath` would change its meaning and would not help this migration.
|
||||||
|
|
||||||
|
The current daemon creates `createCocoClient()` directly and injects it into `createWalletAdapter()`, so `cocodPath` is effectively bypassed on the normal in-process path. Cleanup or removal of that compatibility setting is a separate task.
|
||||||
|
|
||||||
|
### 4. Do not implement fallback as the steady state
|
||||||
|
|
||||||
|
A resolver that permanently falls back to `~/.cocod` does not move the data. Legacy detection is needed to initiate migration, but successful startup should resolve to the canonical directory afterward.
|
||||||
|
|
||||||
|
### 5. Account for the updated base
|
||||||
|
|
||||||
|
The latest `coco-integration` adds:
|
||||||
|
|
||||||
|
- `defaultMintUrl` persistence in the wallet's `config.json`;
|
||||||
|
- the NPC plugin, whose Nostr identity is derived from the same mnemonic;
|
||||||
|
- `src/daemon/wallet/coco-client.npc.test.ts`;
|
||||||
|
- Windows support and `USERPROFILE` fallback.
|
||||||
|
|
||||||
|
Migration must preserve the complete config JSON rather than reconstructing selected fields. Path construction must use `path.join` rather than hard-coded `/` separators.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current runtime path references on the latest base
|
||||||
|
|
||||||
|
### Wallet data and legacy process coordination
|
||||||
|
|
||||||
|
| File | Current responsibility | Required change |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/daemon/wallet/coco-client.ts` | Uses one `.cocod` directory for config, DB, socket, and PID; owns the in-process wallet and legacy-cocod guard | Split canonical data paths from legacy process paths; migrate before opening the DB; use `wallet.pid` for the in-process lock |
|
||||||
|
| `src/daemon/wallet/cocod-client.ts` | External cocod compatibility client; defaults to `.cocod/cocod.sock` | Keep legacy defaults and clarify that they are external-cocod paths |
|
||||||
|
| `src/cli.ts` | Initializes `.cocod`; two restart paths wait on `.cocod/cocod.pid` | Initialize/migrate the canonical wallet and wait on `wallet.pid` |
|
||||||
|
| `src/utils/config.ts` | Defines `~/.routstrd`, but no wallet subdirectory | Export canonical and legacy wallet/process path helpers or constants |
|
||||||
|
| `src/daemon/index.ts` | Calls `createCocoClient()` before building the adapter | Ensure migration occurs before the client opens the DB |
|
||||||
|
|
||||||
|
### Tests and documentation
|
||||||
|
|
||||||
|
| File | Required update |
|
||||||
|
|---|---|
|
||||||
|
| `src/cli.test.ts` | Change fresh-wallet expectations and add migration coverage |
|
||||||
|
| `src/daemon/wallet/coco-client.test.ts` | Update data/lock paths and add migration/dual-lock tests |
|
||||||
|
| `src/daemon/wallet/coco-client.npc.test.ts` | Use the canonical wallet layout in fixtures; verify migrated mnemonic/config still drives NPC identity |
|
||||||
|
| `README.md` | Document the new location and migration behavior; retain legacy cocod terminology only where discussing compatibility |
|
||||||
|
| `SKILL.md` | Update wallet storage and environment-variable documentation |
|
||||||
|
|
||||||
|
The fixture `src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz` keeps its name because it records the fixture's provenance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Target path model
|
||||||
|
|
||||||
|
Define the path model in one dependency-light module (for example `src/daemon/wallet/paths.ts`, or in `src/utils/config.ts` if that does not introduce a cycle):
|
||||||
|
|
||||||
|
```text
|
||||||
|
ROUTSTRD config root process.env.ROUTSTRD_DIR || ~/.routstrd
|
||||||
|
canonical wallet directory process.env.ROUTSTRD_WALLET_DIR || <config root>/wallet
|
||||||
|
canonical wallet config <wallet directory>/config.json
|
||||||
|
canonical wallet database <wallet directory>/coco.db
|
||||||
|
canonical wallet lock process.env.ROUTSTRD_WALLET_PID || <wallet directory>/wallet.pid
|
||||||
|
|
||||||
|
legacy cocod directory process.env.COCOD_DIR || ~/.cocod
|
||||||
|
legacy cocod socket process.env.COCOD_SOCKET || <legacy directory>/cocod.sock
|
||||||
|
legacy cocod PID process.env.COCOD_PID || <legacy directory>/cocod.pid
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `HOME` with `USERPROFILE` fallback, matching the updated Windows-support base. Use functions or injectable path objects where tests need to change environment variables after module import.
|
||||||
|
|
||||||
|
`COCOD_DIR`, `COCOD_SOCKET`, and `COCOD_PID` remain compatibility controls for locating an old external cocod. They must not redirect the new in-process wallet away from `~/.routstrd/wallet`. `ROUTSTRD_WALLET_DIR` is the new explicit data-directory override.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation plan
|
||||||
|
|
||||||
|
### Phase 1 — Centralize and separate paths
|
||||||
|
|
||||||
|
1. Add the canonical wallet and legacy cocod path definitions above.
|
||||||
|
2. Remove the local `.cocod` path construction from `coco-client.ts` and `cli.ts`.
|
||||||
|
3. Change `CreateCocoClientOptions` so tests/tooling can independently override:
|
||||||
|
- wallet data directory;
|
||||||
|
- wallet lock path;
|
||||||
|
- legacy cocod socket and PID paths.
|
||||||
|
4. Keep `cocod-client.ts` pointed at the legacy socket. Do not make it import a resolver that prefers the new wallet directory.
|
||||||
|
|
||||||
|
### Phase 2 — Add a safe automatic migration primitive
|
||||||
|
|
||||||
|
Add an idempotent `migrateLegacyWallet()` helper with injectable paths/filesystem operations for tests.
|
||||||
|
|
||||||
|
Preconditions and behavior:
|
||||||
|
|
||||||
|
1. If the canonical wallet already contains both `config.json` and `coco.db`, return `already-current` and do not touch legacy data.
|
||||||
|
2. If neither canonical nor legacy wallet data exists, return `fresh`; initialization will create the canonical directory.
|
||||||
|
3. If only the legacy wallet exists:
|
||||||
|
- first verify that legacy cocod is not running using the existing PID and socket guard;
|
||||||
|
- create a private staging directory under `~/.routstrd` with mode `0700`;
|
||||||
|
- copy the complete `config.json` and `coco.db` into staging without parsing or rewriting them;
|
||||||
|
- apply `0600` to both files;
|
||||||
|
- validate that the staged files exist and have the expected byte sizes;
|
||||||
|
- atomically rename the staged directory to `wallet`;
|
||||||
|
- only after the canonical directory is committed, remove the legacy `config.json` and `coco.db`;
|
||||||
|
- never copy `cocod.sock` or `cocod.pid`.
|
||||||
|
4. If canonical storage is partial, legacy storage is partial, both contain wallet data, or files conflict, stop with an actionable error. Never merge databases and never generate a new mnemonic over an ambiguous state.
|
||||||
|
5. Clean up an uncommitted staging directory after failure. A committed canonical wallet remains authoritative if cleanup of the old files fails; report that cleanup warning clearly.
|
||||||
|
|
||||||
|
The migration should preserve unknown config fields, including `defaultMintUrl`, and preserve the database byte-for-byte. This also preserves the NPC identity because that identity is derived from the mnemonic.
|
||||||
|
|
||||||
|
A staging copy plus atomic directory rename is preferred over two independent file renames: a crash must not expose a half-created canonical wallet. Copying also permits a clear rollback before commit and supports a legacy directory located on another filesystem through `COCOD_DIR`.
|
||||||
|
|
||||||
|
### Phase 3 — Run migration from every wallet-opening path
|
||||||
|
|
||||||
|
1. **CLI onboarding/init:** run migration before `initializeWallet()`. Only initialize a new mnemonic when migration reports `fresh`.
|
||||||
|
2. **Daemon direct startup:** run migration before `createCocoClient()` opens `coco.db`. This covers users who invoke the daemon without rerunning onboarding.
|
||||||
|
3. **Start/restart/update/service paths:** retain `stopLegacyCocod()` before migration/startup where those paths already stop legacy cocod. Direct daemon startup should refuse with the existing actionable error rather than silently running two wallet engines.
|
||||||
|
4. Print a concise success message showing old and new directories, but never print config contents or the mnemonic during migration.
|
||||||
|
|
||||||
|
A separate `routstrd wallet migrate` command is optional as a manual recovery/preview entry point, but it must call the same migration primitive. It is not a substitute for automatic migration.
|
||||||
|
|
||||||
|
### Phase 4 — Separate process locking
|
||||||
|
|
||||||
|
Refactor the current `claimLegacyCocodPidFile()` behavior into explicit responsibilities:
|
||||||
|
|
||||||
|
1. Claim `wallet.pid` for the lifetime of the in-process wallet to prevent two routstrd wallet instances from opening `coco.db`.
|
||||||
|
2. Continue claiming the legacy `cocod.pid` while routstrd is active, after verifying no real cocod owns it, to prevent an old cocod from starting.
|
||||||
|
3. If either claim fails, release any claim already acquired before returning the error.
|
||||||
|
4. On `CocodClient.dispose()`, startup failure, and daemon shutdown, release only PID files still owned by the current process.
|
||||||
|
5. Keep `stopLegacyCocod()` and `assertLegacyCocodNotRunning()` operating only on legacy cocod paths.
|
||||||
|
6. Update both PID-release waits in `src/cli.ts` (`restartDaemonsAfterUpdate` and `restart`) to wait for the canonical `wallet.pid` rather than `.cocod/cocod.pid`.
|
||||||
|
|
||||||
|
There is no canonical wallet socket.
|
||||||
|
|
||||||
|
### Phase 5 — Update initialization and client defaults
|
||||||
|
|
||||||
|
1. Make `initializeWallet()` default to the canonical wallet directory.
|
||||||
|
2. Preserve directory mode `0700` and config mode `0600` on both migrated and fresh wallets.
|
||||||
|
3. In `createCocoClient()`, derive `config.json` and `coco.db` from the canonical wallet directory, but take legacy guard paths separately.
|
||||||
|
4. Update the wallet-access comment near `unlock()` to reference `~/.routstrd/wallet/config.json`.
|
||||||
|
5. Leave `CocodClient`, `resolveCocodExecutable()`, and `cocodPath` compatibility behavior unchanged.
|
||||||
|
|
||||||
|
### Phase 6 — Tests
|
||||||
|
|
||||||
|
Add or update tests for:
|
||||||
|
|
||||||
|
- fresh initialization creates `~/.routstrd/wallet`, not `.cocod`;
|
||||||
|
- a legacy config and database migrate byte-for-byte;
|
||||||
|
- `defaultMintUrl` and unknown config fields survive migration;
|
||||||
|
- the NPC-derived identity is unchanged after migration;
|
||||||
|
- migration never copies socket or PID files;
|
||||||
|
- migration refuses while a real legacy cocod is running;
|
||||||
|
- stale legacy socket/PID handling remains safe;
|
||||||
|
- an existing complete canonical wallet wins without modifying it;
|
||||||
|
- partial canonical, partial legacy, and conflicting dual-wallet states fail without generating a mnemonic;
|
||||||
|
- staging cleanup and retry after an interrupted migration;
|
||||||
|
- custom `ROUTSTRD_DIR`, `ROUTSTRD_WALLET_DIR`, and legacy `COCOD_DIR` paths;
|
||||||
|
- Windows-compatible path construction;
|
||||||
|
- acquiring the second process lock rolls back the first on failure;
|
||||||
|
- `dispose()` releases both owned locks and does not unlink another process's lock;
|
||||||
|
- both CLI restart paths wait for `wallet.pid`.
|
||||||
|
|
||||||
|
Keep `cocod-0.0.24-wallet.db.gz` unchanged and continue using it to prove the migrated database opens successfully with the current in-process wallet.
|
||||||
|
|
||||||
|
### Phase 7 — Documentation
|
||||||
|
|
||||||
|
Update `README.md` and `SKILL.md` to state:
|
||||||
|
|
||||||
|
- wallet data is stored in `~/.routstrd/wallet`;
|
||||||
|
- existing `~/.cocod` data is migrated automatically on first startup;
|
||||||
|
- users must back up the mnemonic before migration;
|
||||||
|
- `ROUTSTRD_WALLET_DIR` overrides the canonical wallet directory;
|
||||||
|
- `COCOD_DIR`, `COCOD_SOCKET`, `COCOD_PID`, and `cocodPath` refer only to legacy external-cocod compatibility.
|
||||||
|
|
||||||
|
Do not replace every use of the word `cocod`: references to the external daemon, compatibility client, legacy guard, package, and fixture are still accurate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- A fresh install creates wallet data only under `~/.routstrd/wallet`.
|
||||||
|
- Starting from a valid `.cocod` wallet results in the same config and database under the canonical directory without changing balances, mnemonic-derived NPC identity, or default mint.
|
||||||
|
- No startup path silently creates a new mnemonic when recoverable legacy data exists.
|
||||||
|
- A running legacy cocod blocks migration and database opening.
|
||||||
|
- The new wallet directory contains no copied Unix socket and uses `wallet.pid` only as an in-process lock.
|
||||||
|
- Legacy cocod probing and exclusion continue to use `.cocod/cocod.sock` and `.cocod/cocod.pid`.
|
||||||
|
- The implementation works with `USERPROFILE`/Windows path construction and custom directory overrides.
|
||||||
|
- All wallet, CLI, typecheck, and build tests pass on the rebased `coco-integration` branch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggested commit sequence
|
||||||
|
|
||||||
|
1. `refactor(wallet): separate routstrd wallet paths from legacy cocod paths`
|
||||||
|
2. `feat(wallet): atomically migrate legacy cocod wallet data`
|
||||||
|
3. `fix(wallet): use independent routstrd and legacy cocod process locks`
|
||||||
|
4. `test(wallet): cover migration, conflicts, NPC identity, and lock rollback`
|
||||||
|
5. `docs: document routstrd wallet storage and legacy migration`
|
||||||
@@ -161,6 +161,17 @@ Response:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Wallet storage
|
||||||
|
|
||||||
|
The in-process Cashu wallet stores its mnemonic and proof database in
|
||||||
|
`~/.routstrd/wallet/`. On first startup, an existing wallet in `~/.cocod/` is
|
||||||
|
migrated automatically after routstrd verifies that the legacy cocod daemon is
|
||||||
|
not running. Back up your mnemonic before upgrading.
|
||||||
|
|
||||||
|
Set `ROUTSTRD_WALLET_DIR` to override the canonical wallet directory. The
|
||||||
|
`COCOD_DIR`, `COCOD_SOCKET`, and `COCOD_PID` variables are retained only for
|
||||||
|
locating and excluding a legacy external cocod process.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration is stored in `~/.routstrd/config.json`:
|
Configuration is stored in `~/.routstrd/config.json`:
|
||||||
|
|||||||
@@ -170,7 +170,9 @@ Refresh routstr21 models from Nostr and re-run integrations for all registered c
|
|||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `ROUTSTRD_DIR` | `~/.routstrd` | Config directory |
|
| `ROUTSTRD_DIR` | `~/.routstrd` | Config directory |
|
||||||
| `COCOD_DIR` | `~/.cocod` | Wallet config directory |
|
| `ROUTSTRD_WALLET_DIR` | `~/.routstrd/wallet` | In-process Cashu wallet data directory |
|
||||||
|
| `ROUTSTRD_WALLET_PID` | `<wallet>/wallet.pid` | In-process wallet lock path |
|
||||||
|
| `COCOD_DIR` | `~/.cocod` | Legacy external cocod compatibility directory |
|
||||||
|
|
||||||
### `routstrd mode`
|
### `routstrd mode`
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -24,7 +24,7 @@ afterEach(() => {
|
|||||||
|
|
||||||
describe("initializeWallet", () => {
|
describe("initializeWallet", () => {
|
||||||
test("creates the wallet directory and config with restrictive permissions", () => {
|
test("creates the wallet directory and config with restrictive permissions", () => {
|
||||||
const walletDir = join(makeTempDir(), ".cocod");
|
const walletDir = join(makeTempDir(), "wallet");
|
||||||
|
|
||||||
initializeWallet(walletDir);
|
initializeWallet(walletDir);
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ describe("initializeWallet", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("repairs permissions without replacing an existing wallet", () => {
|
test("repairs permissions without replacing an existing wallet", () => {
|
||||||
const walletDir = join(makeTempDir(), ".cocod");
|
const walletDir = join(makeTempDir(), "wallet");
|
||||||
const walletConfig = join(walletDir, "config.json");
|
const walletConfig = join(walletDir, "config.json");
|
||||||
const existingConfig = JSON.stringify({ mnemonic: "existing seed" });
|
const existingConfig = JSON.stringify({ mnemonic: "existing seed" });
|
||||||
|
|
||||||
|
|||||||
+37
-11
@@ -17,6 +17,7 @@ import {
|
|||||||
} from "./utils/clients";
|
} from "./utils/clients";
|
||||||
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
|
import { dirname, join } from "path";
|
||||||
import {
|
import {
|
||||||
CONFIG_DIR,
|
CONFIG_DIR,
|
||||||
DB_PATH,
|
DB_PATH,
|
||||||
@@ -27,7 +28,18 @@ import {
|
|||||||
} from "./utils/config";
|
} from "./utils/config";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
import { setupIntegration, runIntegrationsForClients } from "./integrations";
|
import { setupIntegration, runIntegrationsForClients } from "./integrations";
|
||||||
import { stopLegacyCocod } from "./daemon/wallet/coco-client";
|
import {
|
||||||
|
assertLegacyCocodNotRunning,
|
||||||
|
claimLegacyCocodPidFile,
|
||||||
|
stopLegacyCocod,
|
||||||
|
} from "./daemon/wallet/coco-client";
|
||||||
|
import { migrateLegacyWallet } from "./daemon/wallet/migration";
|
||||||
|
import {
|
||||||
|
legacyCocodPidPath,
|
||||||
|
legacyCocodSocketPath,
|
||||||
|
walletDir as defaultWalletDir,
|
||||||
|
walletPidPath,
|
||||||
|
} from "./daemon/wallet/paths";
|
||||||
import { getClientsList } from "./utils/clients";
|
import { getClientsList } from "./utils/clients";
|
||||||
import * as QRCode from "qrcode";
|
import * as QRCode from "qrcode";
|
||||||
import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98";
|
import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98";
|
||||||
@@ -85,12 +97,8 @@ async function printLightningInvoice(invoice: string): Promise<void> {
|
|||||||
console.log(`${qr}\nInvoice:\n${invoice}`);
|
console.log(`${qr}\nInvoice:\n${invoice}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initializeWallet(
|
export function initializeWallet(walletDir = defaultWalletDir()): void {
|
||||||
walletDir =
|
const walletConfig = join(walletDir, "config.json");
|
||||||
process.env.COCOD_DIR ||
|
|
||||||
`${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`,
|
|
||||||
): void {
|
|
||||||
const walletConfig = `${walletDir}/config.json`;
|
|
||||||
|
|
||||||
// The wallet directory and config contain the plaintext seed phrase. Correct
|
// The wallet directory and config contain the plaintext seed phrase. Correct
|
||||||
// permissions on existing installations as well as newly created ones.
|
// permissions on existing installations as well as newly created ones.
|
||||||
@@ -144,8 +152,8 @@ async function restartDaemonsAfterUpdate(): Promise<void> {
|
|||||||
|
|
||||||
await callDaemon("/stop", { method: "POST" });
|
await callDaemon("/stop", { method: "POST" });
|
||||||
|
|
||||||
// Wait for HTTP health check to fail AND pidfile to be released.
|
// Wait for HTTP health check to fail AND wallet lock to be released.
|
||||||
const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`;
|
const pidFilePath = walletPidPath();
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
const healthDown = !(await isDaemonRunning());
|
const healthDown = !(await isDaemonRunning());
|
||||||
@@ -232,6 +240,24 @@ async function initDaemon(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Database will be stored at: ${DB_PATH}`);
|
console.log(`Database will be stored at: ${DB_PATH}`);
|
||||||
|
const migration = await migrateLegacyWallet({
|
||||||
|
assertLegacyStopped: () =>
|
||||||
|
assertLegacyCocodNotRunning({
|
||||||
|
socketPath: legacyCocodSocketPath(),
|
||||||
|
pidFilePath: legacyCocodPidPath(),
|
||||||
|
}),
|
||||||
|
acquireLegacyLock: () => {
|
||||||
|
mkdirSync(dirname(legacyCocodPidPath()), {
|
||||||
|
recursive: true,
|
||||||
|
mode: 0o700,
|
||||||
|
});
|
||||||
|
return claimLegacyCocodPidFile({ pidFilePath: legacyCocodPidPath() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (migration.status === "migrated") {
|
||||||
|
console.log(`Migrated wallet from ${migration.from} to ${migration.to}.`);
|
||||||
|
for (const warning of migration.cleanupWarnings) console.warn(warning);
|
||||||
|
}
|
||||||
initializeWallet();
|
initializeWallet();
|
||||||
|
|
||||||
await startDaemon({ port: String(config.port || 8008), host: config.host || undefined });
|
await startDaemon({ port: String(config.port || 8008), host: config.host || undefined });
|
||||||
@@ -1833,8 +1859,8 @@ program
|
|||||||
console.log("Stopping daemon...");
|
console.log("Stopping daemon...");
|
||||||
await callDaemon("/stop", { method: "POST" });
|
await callDaemon("/stop", { method: "POST" });
|
||||||
|
|
||||||
// Wait for HTTP health check to fail AND pidfile to be released.
|
// Wait for HTTP health check to fail AND wallet lock to be released.
|
||||||
const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`;
|
const pidFilePath = walletPidPath();
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
const healthDown = !(await isDaemonRunning());
|
const healthDown = !(await isDaemonRunning());
|
||||||
|
|||||||
+31
-1
@@ -54,7 +54,18 @@ import { createDaemonRequestHandler } from "./http";
|
|||||||
import { FileRequestResponseLogSink } from "./request-response-log-sink";
|
import { FileRequestResponseLogSink } from "./request-response-log-sink";
|
||||||
import { refreshModelsAndIntegrations } from "../integrations";
|
import { refreshModelsAndIntegrations } from "../integrations";
|
||||||
import { RoutstrClient } from "@routstr/sdk";
|
import { RoutstrClient } from "@routstr/sdk";
|
||||||
import { createCocoClient } from "./wallet/coco-client";
|
import { mkdirSync } from "fs";
|
||||||
|
import { dirname } from "path";
|
||||||
|
import {
|
||||||
|
assertLegacyCocodNotRunning,
|
||||||
|
claimLegacyCocodPidFile,
|
||||||
|
createCocoClient,
|
||||||
|
} from "./wallet/coco-client";
|
||||||
|
import { migrateLegacyWallet } from "./wallet/migration";
|
||||||
|
import {
|
||||||
|
legacyCocodPidPath,
|
||||||
|
legacyCocodSocketPath,
|
||||||
|
} from "./wallet/paths";
|
||||||
|
|
||||||
// Global error handlers — the daemon is spawned detached with stdout/stderr
|
// Global error handlers — the daemon is spawned detached with stdout/stderr
|
||||||
// redirected to a file, so without these, uncaught async errors would kill
|
// redirected to a file, so without these, uncaught async errors would kill
|
||||||
@@ -115,6 +126,25 @@ async function main(): Promise<void> {
|
|||||||
const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } =
|
const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } =
|
||||||
createModelService(modelManager, providerManager, store);
|
createModelService(modelManager, providerManager, store);
|
||||||
|
|
||||||
|
const migration = await migrateLegacyWallet({
|
||||||
|
assertLegacyStopped: () =>
|
||||||
|
assertLegacyCocodNotRunning({
|
||||||
|
socketPath: legacyCocodSocketPath(),
|
||||||
|
pidFilePath: legacyCocodPidPath(),
|
||||||
|
}),
|
||||||
|
acquireLegacyLock: () => {
|
||||||
|
mkdirSync(dirname(legacyCocodPidPath()), {
|
||||||
|
recursive: true,
|
||||||
|
mode: 0o700,
|
||||||
|
});
|
||||||
|
return claimLegacyCocodPidFile({ pidFilePath: legacyCocodPidPath() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (migration.status === "migrated") {
|
||||||
|
startupProgress(`Wallet migrated from ${migration.from} to ${migration.to}.`);
|
||||||
|
for (const warning of migration.cleanupWarnings) logger.warn(warning);
|
||||||
|
}
|
||||||
|
|
||||||
const walletClient = await createCocoClient();
|
const walletClient = await createCocoClient();
|
||||||
|
|
||||||
// ── Auto-refill configuration ────────────────────────────────
|
// ── Auto-refill configuration ────────────────────────────────
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const tempDirs: string[] = [];
|
|||||||
function makeWalletDir(): string {
|
function makeWalletDir(): string {
|
||||||
const dir = mkdtempSync(join(tmpdir(), "routstrd-coco-npc-test-"));
|
const dir = mkdtempSync(join(tmpdir(), "routstrd-coco-npc-test-"));
|
||||||
tempDirs.push(dir);
|
tempDirs.push(dir);
|
||||||
const walletDir = join(dir, ".cocod");
|
const walletDir = join(dir, "wallet");
|
||||||
mkdirSync(walletDir, { recursive: true });
|
mkdirSync(walletDir, { recursive: true });
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(walletDir, "config.json"),
|
join(walletDir, "config.json"),
|
||||||
@@ -58,6 +58,14 @@ function makeWalletDir(): string {
|
|||||||
return walletDir;
|
return walletDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testClientOptions(walletDir = makeWalletDir()) {
|
||||||
|
return {
|
||||||
|
walletDir,
|
||||||
|
legacySocketPath: join(walletDir, "legacy-cocod.sock"),
|
||||||
|
legacyPidPath: join(walletDir, "legacy-cocod.pid"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function resetNpcState(): void {
|
function resetNpcState(): void {
|
||||||
npcState.info = { name: "alice", pubkey: "ab".repeat(32) };
|
npcState.info = { name: "alice", pubkey: "ab".repeat(32) };
|
||||||
npcState.usernameResult = { success: true };
|
npcState.usernameResult = { success: true };
|
||||||
@@ -74,7 +82,7 @@ afterEach(() => {
|
|||||||
|
|
||||||
describe("in-process coco client NPC integration", () => {
|
describe("in-process coco client NPC integration", () => {
|
||||||
it("registers the plugin and exposes the wallet's NPC lightning address", async () => {
|
it("registers the plugin and exposes the wallet's NPC lightning address", async () => {
|
||||||
const client = await createCocoClient({ configDir: makeWalletDir() });
|
const client = await createCocoClient(testClientOptions());
|
||||||
try {
|
try {
|
||||||
const info = await client.getNpcAddress();
|
const info = await client.getNpcAddress();
|
||||||
expect(info).toEqual({
|
expect(info).toEqual({
|
||||||
@@ -89,7 +97,7 @@ describe("in-process coco client NPC integration", () => {
|
|||||||
|
|
||||||
it("falls back to the npub address when no username is set", async () => {
|
it("falls back to the npub address when no username is set", async () => {
|
||||||
npcState.info = { name: null, pubkey: "ab".repeat(32) };
|
npcState.info = { name: null, pubkey: "ab".repeat(32) };
|
||||||
const client = await createCocoClient({ configDir: makeWalletDir() });
|
const client = await createCocoClient(testClientOptions());
|
||||||
try {
|
try {
|
||||||
const info = await client.getNpcAddress();
|
const info = await client.getNpcAddress();
|
||||||
expect(info.address).toStartWith("npub1");
|
expect(info.address).toStartWith("npub1");
|
||||||
@@ -106,7 +114,7 @@ describe("in-process coco client NPC integration", () => {
|
|||||||
success: false,
|
success: false,
|
||||||
pr: { amount: 21, mints: ["https://mint.example.com"] },
|
pr: { amount: 21, mints: ["https://mint.example.com"] },
|
||||||
};
|
};
|
||||||
const client = await createCocoClient({ configDir: makeWalletDir() });
|
const client = await createCocoClient(testClientOptions());
|
||||||
try {
|
try {
|
||||||
const result = await client.setNpcUsername("bob");
|
const result = await client.setNpcUsername("bob");
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@@ -122,7 +130,7 @@ describe("in-process coco client NPC integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("confirms the claim fee payment when requested", async () => {
|
it("confirms the claim fee payment when requested", async () => {
|
||||||
const client = await createCocoClient({ configDir: makeWalletDir() });
|
const client = await createCocoClient(testClientOptions());
|
||||||
try {
|
try {
|
||||||
const result = await client.setNpcUsername("bob", true);
|
const result = await client.setNpcUsername("bob", true);
|
||||||
expect(result).toEqual({ success: true });
|
expect(result).toEqual({ success: true });
|
||||||
@@ -135,7 +143,7 @@ describe("in-process coco client NPC integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("triggers a manual quote sync", async () => {
|
it("triggers a manual quote sync", async () => {
|
||||||
const client = await createCocoClient({ configDir: makeWalletDir() });
|
const client = await createCocoClient(testClientOptions());
|
||||||
try {
|
try {
|
||||||
await client.syncNpc();
|
await client.syncNpc();
|
||||||
expect(npcState.syncCalls).toBe(1);
|
expect(npcState.syncCalls).toBe(1);
|
||||||
@@ -146,7 +154,7 @@ describe("in-process coco client NPC integration", () => {
|
|||||||
|
|
||||||
it("rejects NPC calls with a clear error when the plugin is disabled", async () => {
|
it("rejects NPC calls with a clear error when the plugin is disabled", async () => {
|
||||||
const client = await createCocoClient({
|
const client = await createCocoClient({
|
||||||
configDir: makeWalletDir(),
|
...testClientOptions(),
|
||||||
enableNpc: false,
|
enableNpc: false,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, mock } from "bun:test";
|
import { afterEach, describe, expect, it, mock } from "bun:test";
|
||||||
import { gunzipSync } from "bun";
|
import { gunzipSync } from "bun";
|
||||||
import {
|
import {
|
||||||
|
existsSync,
|
||||||
mkdtempSync,
|
mkdtempSync,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
@@ -43,7 +44,7 @@ function socketOnly(path: string): boolean {
|
|||||||
|
|
||||||
describe("default mint functionality", () => {
|
describe("default mint functionality", () => {
|
||||||
it("automatically adds default mint when no mints exist", async () => {
|
it("automatically adds default mint when no mints exist", async () => {
|
||||||
const walletDir = join(makeTempDir(), ".cocod");
|
const walletDir = join(makeTempDir(), "wallet");
|
||||||
mkdirSync(walletDir, { recursive: true });
|
mkdirSync(walletDir, { recursive: true });
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(walletDir, "config.json"),
|
join(walletDir, "config.json"),
|
||||||
@@ -55,8 +56,19 @@ describe("default mint functionality", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const client = await createCocoClient({ configDir: walletDir });
|
const client = await createCocoClient({
|
||||||
|
walletDir,
|
||||||
|
legacySocketPath: join(walletDir, "legacy-cocod.sock"),
|
||||||
|
legacyPidPath: join(walletDir, "legacy-cocod.pid"),
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
|
expect(readFileSync(join(walletDir, "wallet.pid"), "utf8")).toBe(
|
||||||
|
String(process.pid),
|
||||||
|
);
|
||||||
|
expect(readFileSync(join(walletDir, "legacy-cocod.pid"), "utf8")).toBe(
|
||||||
|
String(process.pid),
|
||||||
|
);
|
||||||
|
|
||||||
const mints = await client.listMints();
|
const mints = await client.listMints();
|
||||||
expect(mints).toContain("https://mint.cubabitcoin.org");
|
expect(mints).toContain("https://mint.cubabitcoin.org");
|
||||||
|
|
||||||
@@ -65,10 +77,12 @@ describe("default mint functionality", () => {
|
|||||||
} finally {
|
} finally {
|
||||||
await client.dispose?.();
|
await client.dispose?.();
|
||||||
}
|
}
|
||||||
|
expect(existsSync(join(walletDir, "wallet.pid"))).toBe(false);
|
||||||
|
expect(existsSync(join(walletDir, "legacy-cocod.pid"))).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("respects existing default mint in config", async () => {
|
it("respects existing default mint in config", async () => {
|
||||||
const walletDir = join(makeTempDir(), ".cocod");
|
const walletDir = join(makeTempDir(), "wallet");
|
||||||
const configuredDefault = "https://mint.cubabitcoin.org";
|
const configuredDefault = "https://mint.cubabitcoin.org";
|
||||||
mkdirSync(walletDir, { recursive: true });
|
mkdirSync(walletDir, { recursive: true });
|
||||||
|
|
||||||
@@ -84,7 +98,11 @@ describe("default mint functionality", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const client = await createCocoClient({ configDir: walletDir });
|
const client = await createCocoClient({
|
||||||
|
walletDir,
|
||||||
|
legacySocketPath: join(walletDir, "legacy-cocod.sock"),
|
||||||
|
legacyPidPath: join(walletDir, "legacy-cocod.pid"),
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
// Should respect the configured default mint
|
// Should respect the configured default mint
|
||||||
const defaultMint = await client.getDefaultMint();
|
const defaultMint = await client.getDefaultMint();
|
||||||
@@ -99,7 +117,7 @@ describe("default mint functionality", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("allows setting default mint to an already trusted mint", async () => {
|
it("allows setting default mint to an already trusted mint", async () => {
|
||||||
const walletDir = join(makeTempDir(), ".cocod");
|
const walletDir = join(makeTempDir(), "wallet");
|
||||||
mkdirSync(walletDir, { recursive: true });
|
mkdirSync(walletDir, { recursive: true });
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(walletDir, "config.json"),
|
join(walletDir, "config.json"),
|
||||||
@@ -111,7 +129,11 @@ describe("default mint functionality", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const client = await createCocoClient({ configDir: walletDir });
|
const client = await createCocoClient({
|
||||||
|
walletDir,
|
||||||
|
legacySocketPath: join(walletDir, "legacy-cocod.sock"),
|
||||||
|
legacyPidPath: join(walletDir, "legacy-cocod.pid"),
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
// The initial default should be the auto-added Cuba mint
|
// The initial default should be the auto-added Cuba mint
|
||||||
const initialDefault = await client.getDefaultMint();
|
const initialDefault = await client.getDefaultMint();
|
||||||
@@ -133,7 +155,7 @@ describe("default mint functionality", () => {
|
|||||||
|
|
||||||
describe("legacy cocod wallet migration", () => {
|
describe("legacy cocod wallet migration", () => {
|
||||||
it("opens an existing unencrypted config and preserves database balances", async () => {
|
it("opens an existing unencrypted config and preserves database balances", async () => {
|
||||||
const walletDir = join(makeTempDir(), ".cocod");
|
const walletDir = join(makeTempDir(), "wallet");
|
||||||
const mintUrl = "https://mint.example.com";
|
const mintUrl = "https://mint.example.com";
|
||||||
mkdirSync(walletDir, { recursive: true });
|
mkdirSync(walletDir, { recursive: true });
|
||||||
|
|
||||||
@@ -164,7 +186,9 @@ describe("legacy cocod wallet migration", () => {
|
|||||||
// NPC is disabled here: this test verifies database migration, and the
|
// NPC is disabled here: this test verifies database migration, and the
|
||||||
// real plugin would otherwise open a websocket to npubx.cash.
|
// real plugin would otherwise open a websocket to npubx.cash.
|
||||||
const client = await createCocoClient({
|
const client = await createCocoClient({
|
||||||
configDir: walletDir,
|
walletDir,
|
||||||
|
legacySocketPath: join(walletDir, "legacy-cocod.sock"),
|
||||||
|
legacyPidPath: join(walletDir, "legacy-cocod.pid"),
|
||||||
enableNpc: false,
|
enableNpc: false,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@@ -177,7 +201,9 @@ describe("legacy cocod wallet migration", () => {
|
|||||||
// Prove the migrated schema remains reopenable and the pre-existing
|
// Prove the migrated schema remains reopenable and the pre-existing
|
||||||
// proofs survive a complete in-process wallet restart.
|
// proofs survive a complete in-process wallet restart.
|
||||||
const reopenedClient = await createCocoClient({
|
const reopenedClient = await createCocoClient({
|
||||||
configDir: walletDir,
|
walletDir,
|
||||||
|
legacySocketPath: join(walletDir, "legacy-cocod.sock"),
|
||||||
|
legacyPidPath: join(walletDir, "legacy-cocod.pid"),
|
||||||
enableNpc: false,
|
enableNpc: false,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -16,13 +16,14 @@ import { finalizeEvent, nip19, type EventTemplate } from "nostr-tools";
|
|||||||
import {
|
import {
|
||||||
closeSync,
|
closeSync,
|
||||||
existsSync,
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
openSync,
|
openSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
renameSync,
|
renameSync,
|
||||||
unlinkSync,
|
unlinkSync,
|
||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from "fs";
|
} from "fs";
|
||||||
import { join } from "path";
|
import { dirname, join } from "path";
|
||||||
import { mnemonicToSeedSync } from "@scure/bip39";
|
import { mnemonicToSeedSync } from "@scure/bip39";
|
||||||
import type {
|
import type {
|
||||||
CocodClient,
|
CocodClient,
|
||||||
@@ -31,20 +32,15 @@ import type {
|
|||||||
NpcUsernameResult,
|
NpcUsernameResult,
|
||||||
} from "./cocod-client";
|
} from "./cocod-client";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
|
import {
|
||||||
|
legacyCocodPidPath,
|
||||||
|
legacyCocodSocketPath,
|
||||||
|
walletDir as defaultWalletDir,
|
||||||
|
walletPidPath as defaultWalletPidPath,
|
||||||
|
} from "./paths";
|
||||||
|
|
||||||
const NPC_DEFAULT_BASE_URL = "https://npubx.cash";
|
const NPC_DEFAULT_BASE_URL = "https://npubx.cash";
|
||||||
|
|
||||||
const CONFIG_DIR =
|
|
||||||
process.env.COCOD_DIR ||
|
|
||||||
`${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`;
|
|
||||||
|
|
||||||
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
||||||
const DB_PATH = join(CONFIG_DIR, "coco.db");
|
|
||||||
const LEGACY_COCOD_SOCKET =
|
|
||||||
process.env.COCOD_SOCKET || join(CONFIG_DIR, "cocod.sock");
|
|
||||||
const LEGACY_COCOD_PID_FILE =
|
|
||||||
process.env.COCOD_PID || join(CONFIG_DIR, "cocod.pid");
|
|
||||||
|
|
||||||
const STALE_SOCKET_ERROR_CODES = new Set([
|
const STALE_SOCKET_ERROR_CODES = new Set([
|
||||||
"ECONNREFUSED",
|
"ECONNREFUSED",
|
||||||
"ENOENT",
|
"ENOENT",
|
||||||
@@ -167,7 +163,7 @@ function createCocoLogger(bindings: Record<string, unknown> = {}): CocoLogger {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadConfig(configFile: string = CONFIG_FILE): CocodConfig {
|
function loadConfig(configFile: string): CocodConfig {
|
||||||
if (!existsSync(configFile)) {
|
if (!existsSync(configFile)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Config file not found at ${configFile}. Run 'routstrd onboard' first.`,
|
`Config file not found at ${configFile}. Run 'routstrd onboard' first.`,
|
||||||
@@ -182,10 +178,7 @@ function loadConfig(configFile: string = CONFIG_FILE): CocodConfig {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveConfig(
|
function saveConfig(config: CocodConfig, configFile: string): void {
|
||||||
config: CocodConfig,
|
|
||||||
configFile: string = CONFIG_FILE,
|
|
||||||
): void {
|
|
||||||
const temporaryFile = `${configFile}.${process.pid}.tmp`;
|
const temporaryFile = `${configFile}.${process.pid}.tmp`;
|
||||||
try {
|
try {
|
||||||
writeFileSync(temporaryFile, JSON.stringify(config, null, 2), {
|
writeFileSync(temporaryFile, JSON.stringify(config, null, 2), {
|
||||||
@@ -239,8 +232,8 @@ function hasErrorCode(error: unknown, codes: Set<string>): boolean {
|
|||||||
export async function assertLegacyCocodNotRunning(
|
export async function assertLegacyCocodNotRunning(
|
||||||
options: LegacyCocodGuardOptions = {},
|
options: LegacyCocodGuardOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const socketPath = options.socketPath || LEGACY_COCOD_SOCKET;
|
const socketPath = options.socketPath || legacyCocodSocketPath();
|
||||||
const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE;
|
const pidFilePath = options.pidFilePath || legacyCocodPidPath();
|
||||||
const pathExists = options.pathExists || existsSync;
|
const pathExists = options.pathExists || existsSync;
|
||||||
const readFile = options.readFile || ((path) => readFileSync(path, "utf-8"));
|
const readFile = options.readFile || ((path) => readFileSync(path, "utf-8"));
|
||||||
const isProcessRunning = options.isProcessRunning || defaultIsProcessRunning;
|
const isProcessRunning = options.isProcessRunning || defaultIsProcessRunning;
|
||||||
@@ -325,8 +318,8 @@ export async function assertLegacyCocodNotRunning(
|
|||||||
export async function stopLegacyCocod(
|
export async function stopLegacyCocod(
|
||||||
options: LegacyCocodStopOptions = {},
|
options: LegacyCocodStopOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const socketPath = options.socketPath || LEGACY_COCOD_SOCKET;
|
const socketPath = options.socketPath || legacyCocodSocketPath();
|
||||||
const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE;
|
const pidFilePath = options.pidFilePath || legacyCocodPidPath();
|
||||||
const pathExists = options.pathExists || existsSync;
|
const pathExists = options.pathExists || existsSync;
|
||||||
const readFile =
|
const readFile =
|
||||||
options.readFile || ((path: string) => readFileSync(path, "utf-8"));
|
options.readFile || ((path: string) => readFileSync(path, "utf-8"));
|
||||||
@@ -416,7 +409,14 @@ export async function stopLegacyCocod(
|
|||||||
export function claimLegacyCocodPidFile(
|
export function claimLegacyCocodPidFile(
|
||||||
options: LegacyCocodPidClaimOptions = {},
|
options: LegacyCocodPidClaimOptions = {},
|
||||||
): () => void {
|
): () => void {
|
||||||
const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE;
|
return claimPidFile({
|
||||||
|
...options,
|
||||||
|
pidFilePath: options.pidFilePath || legacyCocodPidPath(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function claimPidFile(options: LegacyCocodPidClaimOptions & { pidFilePath: string }): () => void {
|
||||||
|
const pidFilePath = options.pidFilePath;
|
||||||
const pid = options.pid ?? process.pid;
|
const pid = options.pid ?? process.pid;
|
||||||
const openExclusive =
|
const openExclusive =
|
||||||
options.openExclusive || ((path: string) => openSync(path, "wx", 0o600));
|
options.openExclusive || ((path: string) => openSync(path, "wx", 0o600));
|
||||||
@@ -507,10 +507,15 @@ export function claimLegacyCocodPidFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateCocoClientOptions {
|
export interface CreateCocoClientOptions {
|
||||||
/** Override the wallet directory, primarily for migration tests and tooling. */
|
/** Override the canonical wallet data directory. */
|
||||||
|
walletDir?: string;
|
||||||
|
/** Deprecated alias retained for existing callers during migration. */
|
||||||
configDir?: string;
|
configDir?: string;
|
||||||
socketPath?: string;
|
/** Override the in-process wallet lock path. */
|
||||||
pidFilePath?: string;
|
walletPidPath?: string;
|
||||||
|
/** Override legacy external-cocod coordination paths. */
|
||||||
|
legacySocketPath?: string;
|
||||||
|
legacyPidPath?: string;
|
||||||
/** Set to false to skip NPC (npubx.cash) plugin registration. Default: true. */
|
/** Set to false to skip NPC (npubx.cash) plugin registration. Default: true. */
|
||||||
enableNpc?: boolean;
|
enableNpc?: boolean;
|
||||||
/** NPC server base URL. Default: https://npubx.cash */
|
/** NPC server base URL. Default: https://npubx.cash */
|
||||||
@@ -520,20 +525,36 @@ export interface CreateCocoClientOptions {
|
|||||||
export async function createCocoClient(
|
export async function createCocoClient(
|
||||||
options: CreateCocoClientOptions = {},
|
options: CreateCocoClientOptions = {},
|
||||||
): Promise<CocodClient> {
|
): Promise<CocodClient> {
|
||||||
const configDir = options.configDir || CONFIG_DIR;
|
const configDir = options.walletDir || options.configDir || defaultWalletDir();
|
||||||
const configFile = join(configDir, "config.json");
|
const configFile = join(configDir, "config.json");
|
||||||
const dbPath = join(configDir, "coco.db");
|
const dbPath = join(configDir, "coco.db");
|
||||||
const socketPath =
|
const walletPidFile =
|
||||||
options.socketPath ||
|
options.walletPidPath ||
|
||||||
(options.configDir ? join(configDir, "cocod.sock") : LEGACY_COCOD_SOCKET);
|
(options.walletDir || options.configDir
|
||||||
const pidFilePath =
|
? join(configDir, "wallet.pid")
|
||||||
options.pidFilePath ||
|
: defaultWalletPidPath());
|
||||||
(options.configDir ? join(configDir, "cocod.pid") : LEGACY_COCOD_PID_FILE);
|
const legacySocket = options.legacySocketPath || legacyCocodSocketPath();
|
||||||
|
const legacyPidFile = options.legacyPidPath || legacyCocodPidPath();
|
||||||
const npcBaseUrl = options.npcBaseUrl || NPC_DEFAULT_BASE_URL;
|
const npcBaseUrl = options.npcBaseUrl || NPC_DEFAULT_BASE_URL;
|
||||||
const npcAddressDomain = new URL(npcBaseUrl).host;
|
const npcAddressDomain = new URL(npcBaseUrl).host;
|
||||||
|
|
||||||
await assertLegacyCocodNotRunning({ socketPath, pidFilePath });
|
await assertLegacyCocodNotRunning({
|
||||||
const releaseLegacyPidClaim = claimLegacyCocodPidFile({ pidFilePath });
|
socketPath: legacySocket,
|
||||||
|
pidFilePath: legacyPidFile,
|
||||||
|
});
|
||||||
|
// The canonical wallet directory is created by initialization/migration.
|
||||||
|
// Keep a legacy PID claim as an exclusion fence for old cocod binaries.
|
||||||
|
mkdirSync(dirname(legacyPidFile), { recursive: true, mode: 0o700 });
|
||||||
|
const releaseWalletPidClaim = claimPidFile({ pidFilePath: walletPidFile });
|
||||||
|
let releaseLegacyPidClaim: () => void;
|
||||||
|
try {
|
||||||
|
releaseLegacyPidClaim = claimLegacyCocodPidFile({
|
||||||
|
pidFilePath: legacyPidFile,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
releaseWalletPidClaim();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
let database: Database | undefined;
|
let database: Database | undefined;
|
||||||
let coco: Awaited<ReturnType<typeof initializeCoco>> | undefined;
|
let coco: Awaited<ReturnType<typeof initializeCoco>> | undefined;
|
||||||
@@ -616,6 +637,7 @@ export async function createCocoClient(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
database?.close();
|
database?.close();
|
||||||
releaseLegacyPidClaim();
|
releaseLegacyPidClaim();
|
||||||
|
releaseWalletPidClaim();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,7 +675,7 @@ export async function createCocoClient(
|
|||||||
|
|
||||||
async unlock(_passphrase: string): Promise<string> {
|
async unlock(_passphrase: string): Promise<string> {
|
||||||
// coco-core does not support passphrase locking.
|
// coco-core does not support passphrase locking.
|
||||||
// Wallet access is controlled via the mnemonic in ~/.cocod/config.json.
|
// Wallet access is controlled by ~/.routstrd/wallet/config.json.
|
||||||
return "wallet does not require unlocking";
|
return "wallet does not require unlocking";
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -756,6 +778,7 @@ export async function createCocoClient(
|
|||||||
database.close();
|
database.close();
|
||||||
} finally {
|
} finally {
|
||||||
releaseLegacyPidClaim();
|
releaseLegacyPidClaim();
|
||||||
|
releaseWalletPidClaim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { afterEach, describe, expect, it, mock } from "bun:test";
|
||||||
|
import {
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
mkdtempSync,
|
||||||
|
readFileSync,
|
||||||
|
rmSync,
|
||||||
|
statSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "fs";
|
||||||
|
import { tmpdir } from "os";
|
||||||
|
import { join } from "path";
|
||||||
|
import { migrateLegacyWallet } from "./migration";
|
||||||
|
|
||||||
|
const roots: string[] = [];
|
||||||
|
function root(): string {
|
||||||
|
const path = mkdtempSync(join(tmpdir(), "routstrd-migration-"));
|
||||||
|
roots.push(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
afterEach(() => {
|
||||||
|
for (const path of roots.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("migrateLegacyWallet", () => {
|
||||||
|
it("copies wallet data byte-for-byte and leaves process files behind", async () => {
|
||||||
|
const base = root();
|
||||||
|
const legacyDir = join(base, ".cocod");
|
||||||
|
const walletDir = join(base, ".routstrd", "wallet");
|
||||||
|
mkdirSync(legacyDir);
|
||||||
|
const config = JSON.stringify({
|
||||||
|
mnemonic: "seed words",
|
||||||
|
encrypted: false,
|
||||||
|
defaultMintUrl: "https://mint.example",
|
||||||
|
unknown: { preserved: true },
|
||||||
|
});
|
||||||
|
const database = new Uint8Array([0, 1, 2, 3, 255]);
|
||||||
|
writeFileSync(join(legacyDir, "config.json"), config);
|
||||||
|
writeFileSync(join(legacyDir, "coco.db"), database);
|
||||||
|
writeFileSync(join(legacyDir, "cocod.pid"), "123");
|
||||||
|
writeFileSync(join(legacyDir, "cocod.sock"), "not-a-real-socket");
|
||||||
|
const assertStopped = mock(async () => {});
|
||||||
|
const releaseLock = mock(() => {});
|
||||||
|
const acquireLock = mock(() => releaseLock);
|
||||||
|
|
||||||
|
const result = await migrateLegacyWallet({
|
||||||
|
walletDir,
|
||||||
|
legacyDir,
|
||||||
|
assertLegacyStopped: assertStopped,
|
||||||
|
acquireLegacyLock: acquireLock,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.status).toBe("migrated");
|
||||||
|
expect(assertStopped).toHaveBeenCalledTimes(2);
|
||||||
|
expect(acquireLock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(releaseLock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(readFileSync(join(walletDir, "config.json"), "utf8")).toBe(config);
|
||||||
|
expect(readFileSync(join(walletDir, "coco.db"))).toEqual(Buffer.from(database));
|
||||||
|
expect(statSync(walletDir).mode & 0o777).toBe(0o700);
|
||||||
|
expect(statSync(join(walletDir, "config.json")).mode & 0o777).toBe(0o600);
|
||||||
|
expect(existsSync(join(walletDir, "cocod.pid"))).toBe(false);
|
||||||
|
expect(existsSync(join(walletDir, "cocod.sock"))).toBe(false);
|
||||||
|
expect(existsSync(join(legacyDir, "config.json"))).toBe(false);
|
||||||
|
expect(existsSync(join(legacyDir, "coco.db"))).toBe(false);
|
||||||
|
expect(existsSync(join(legacyDir, "cocod.pid"))).toBe(true);
|
||||||
|
expect(existsSync(join(legacyDir, "cocod.sock"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a config-only initialized wallet as migratable", async () => {
|
||||||
|
const base = root();
|
||||||
|
const legacyDir = join(base, ".cocod");
|
||||||
|
const walletDir = join(base, ".routstrd", "wallet");
|
||||||
|
mkdirSync(legacyDir);
|
||||||
|
writeFileSync(join(legacyDir, "config.json"), '{"mnemonic":"seed"}');
|
||||||
|
|
||||||
|
expect((await migrateLegacyWallet({ walletDir, legacyDir })).status).toBe("migrated");
|
||||||
|
expect(existsSync(join(walletDir, "config.json"))).toBe(true);
|
||||||
|
expect(existsSync(join(walletDir, "coco.db"))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuses a database without its mnemonic config", async () => {
|
||||||
|
const base = root();
|
||||||
|
const legacyDir = join(base, ".cocod");
|
||||||
|
mkdirSync(legacyDir);
|
||||||
|
writeFileSync(join(legacyDir, "coco.db"), "database");
|
||||||
|
|
||||||
|
await expect(migrateLegacyWallet({ walletDir: join(base, "wallet"), legacyDir })).rejects.toThrow(
|
||||||
|
"without config.json",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuses when both canonical and legacy configs exist", async () => {
|
||||||
|
const base = root();
|
||||||
|
const legacyDir = join(base, ".cocod");
|
||||||
|
const walletDir = join(base, ".routstrd", "wallet");
|
||||||
|
mkdirSync(legacyDir, { recursive: true });
|
||||||
|
mkdirSync(walletDir, { recursive: true });
|
||||||
|
writeFileSync(join(legacyDir, "config.json"), "legacy");
|
||||||
|
writeFileSync(join(walletDir, "config.json"), "current");
|
||||||
|
|
||||||
|
await expect(migrateLegacyWallet({ walletDir, legacyDir })).rejects.toThrow(
|
||||||
|
"both",
|
||||||
|
);
|
||||||
|
expect(readFileSync(join(walletDir, "config.json"), "utf8")).toBe("current");
|
||||||
|
expect(readFileSync(join(legacyDir, "config.json"), "utf8")).toBe("legacy");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import {
|
||||||
|
chmodSync,
|
||||||
|
copyFileSync,
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
renameSync,
|
||||||
|
rmSync,
|
||||||
|
statSync,
|
||||||
|
unlinkSync,
|
||||||
|
} from "fs";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import { legacyCocodDir, walletDir } from "./paths";
|
||||||
|
|
||||||
|
export type WalletMigrationResult =
|
||||||
|
| { status: "fresh" }
|
||||||
|
| { status: "already-current" }
|
||||||
|
| { status: "migrated"; from: string; to: string; cleanupWarnings: string[] };
|
||||||
|
|
||||||
|
export interface WalletMigrationOptions {
|
||||||
|
walletDir?: string;
|
||||||
|
legacyDir?: string;
|
||||||
|
/** Called after state validation and before legacy files are copied. */
|
||||||
|
assertLegacyStopped?: () => void | Promise<void>;
|
||||||
|
/** Claims the legacy cocod exclusion lock for the duration of the copy. */
|
||||||
|
acquireLegacyLock?: () => (() => void) | Promise<() => void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalletState = "absent" | "database-only" | "initialized";
|
||||||
|
|
||||||
|
function state(configPath: string, dbPath: string): WalletState {
|
||||||
|
const hasConfig = existsSync(configPath);
|
||||||
|
const hasDb = existsSync(dbPath);
|
||||||
|
if (hasConfig) return "initialized";
|
||||||
|
if (hasDb) return "database-only";
|
||||||
|
return "absent";
|
||||||
|
}
|
||||||
|
|
||||||
|
function filesEqual(left: string, right: string): boolean {
|
||||||
|
if (!existsSync(left) || !existsSync(right)) return false;
|
||||||
|
if (statSync(left).size !== statSync(right).size) return false;
|
||||||
|
return readFileSync(left).equals(readFileSync(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy a legacy cocod wallet into the canonical routstrd wallet directory.
|
||||||
|
* The staging directory is renamed atomically, so the canonical path is never
|
||||||
|
* exposed with only one of its two required files.
|
||||||
|
*/
|
||||||
|
export async function migrateLegacyWallet(
|
||||||
|
options: WalletMigrationOptions = {},
|
||||||
|
): Promise<WalletMigrationResult> {
|
||||||
|
const targetDir = options.walletDir || walletDir();
|
||||||
|
const sourceDir = options.legacyDir || legacyCocodDir();
|
||||||
|
const targetConfig = join(targetDir, "config.json");
|
||||||
|
const targetDb = join(targetDir, "coco.db");
|
||||||
|
const sourceConfig = join(sourceDir, "config.json");
|
||||||
|
const sourceDb = join(sourceDir, "coco.db");
|
||||||
|
const targetState = state(targetConfig, targetDb);
|
||||||
|
const sourceState = state(sourceConfig, sourceDb);
|
||||||
|
|
||||||
|
// config.json is sufficient for a newly initialized wallet; coco.db is
|
||||||
|
// created on first open. A database without its mnemonic is never usable.
|
||||||
|
if (targetState === "initialized" && sourceState === "initialized") {
|
||||||
|
// A prior migration may have committed successfully but failed to remove
|
||||||
|
// its source files. Identical leftovers are safe; divergent wallets are not.
|
||||||
|
const configsMatch = filesEqual(targetConfig, sourceConfig);
|
||||||
|
const databasesMatch =
|
||||||
|
!existsSync(targetDb) && !existsSync(sourceDb)
|
||||||
|
? true
|
||||||
|
: filesEqual(targetDb, sourceDb);
|
||||||
|
if (configsMatch && databasesMatch) return { status: "already-current" };
|
||||||
|
throw new Error(
|
||||||
|
`Cannot migrate wallet: both ${targetDir} and ${sourceDir} contain different wallet data. ` +
|
||||||
|
"Refusing to choose a mnemonic or merge wallet databases automatically.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (targetState === "initialized") return { status: "already-current" };
|
||||||
|
if (targetState === "database-only" || sourceState === "database-only") {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot migrate wallet: ${targetDir} is ${targetState} and ${sourceDir} is ${sourceState}. ` +
|
||||||
|
"A coco.db without config.json cannot be opened; restore the matching config first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sourceState === "absent") return { status: "fresh" };
|
||||||
|
|
||||||
|
await options.assertLegacyStopped?.();
|
||||||
|
const releaseLegacyLock = await options.acquireLegacyLock?.();
|
||||||
|
const stagingDir = join(
|
||||||
|
dirname(targetDir),
|
||||||
|
`.${targetDir.split(/[\\/]/).at(-1) || "wallet"}.staging-${process.pid}-${Date.now()}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Recheck after claiming the exclusion lock to close the probe/claim race.
|
||||||
|
await options.assertLegacyStopped?.();
|
||||||
|
mkdirSync(dirname(targetDir), { recursive: true, mode: 0o700 });
|
||||||
|
mkdirSync(stagingDir, { mode: 0o700 });
|
||||||
|
const stagedConfig = join(stagingDir, "config.json");
|
||||||
|
const stagedDb = join(stagingDir, "coco.db");
|
||||||
|
copyFileSync(sourceConfig, stagedConfig);
|
||||||
|
chmodSync(stagedConfig, 0o600);
|
||||||
|
const hasSourceDb = existsSync(sourceDb);
|
||||||
|
if (hasSourceDb) {
|
||||||
|
copyFileSync(sourceDb, stagedDb);
|
||||||
|
chmodSync(stagedDb, 0o600);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
statSync(stagedConfig).size !== statSync(sourceConfig).size ||
|
||||||
|
(hasSourceDb && statSync(stagedDb).size !== statSync(sourceDb).size)
|
||||||
|
) {
|
||||||
|
throw new Error("Cannot migrate wallet: staged files failed size validation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
renameSync(stagingDir, targetDir);
|
||||||
|
} catch (error) {
|
||||||
|
rmSync(stagingDir, { recursive: true, force: true });
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
releaseLegacyLock?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupWarnings: string[] = [];
|
||||||
|
for (const path of [sourceConfig, sourceDb]) {
|
||||||
|
if (!existsSync(path)) continue;
|
||||||
|
try {
|
||||||
|
unlinkSync(path);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
cleanupWarnings.push(`Could not remove legacy wallet file ${path}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: "migrated", from: sourceDir, to: targetDir, cleanupWarnings };
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
function homeDir(): string {
|
||||||
|
return process.env.HOME || process.env.USERPROFILE || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Root directory for routstrd state. Evaluated lazily so env overrides work in tests. */
|
||||||
|
export function routstrdConfigDir(): string {
|
||||||
|
return process.env.ROUTSTRD_DIR || join(homeDir(), ".routstrd");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Canonical directory for the in-process Cashu wallet. */
|
||||||
|
export function walletDir(): string {
|
||||||
|
return process.env.ROUTSTRD_WALLET_DIR || join(routstrdConfigDir(), "wallet");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lock owned by an in-process routstrd wallet instance. */
|
||||||
|
export function walletPidPath(): string {
|
||||||
|
return process.env.ROUTSTRD_WALLET_PID || join(walletDir(), "wallet.pid");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Legacy external cocod directory. This is not the routstrd wallet directory. */
|
||||||
|
export function legacyCocodDir(): string {
|
||||||
|
return process.env.COCOD_DIR || join(homeDir(), ".cocod");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function legacyCocodSocketPath(): string {
|
||||||
|
return process.env.COCOD_SOCKET || join(legacyCocodDir(), "cocod.sock");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function legacyCocodPidPath(): string {
|
||||||
|
return process.env.COCOD_PID || join(legacyCocodDir(), "cocod.pid");
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import { logger } from "./utils/logger";
|
|||||||
import { CONFIG_DIR, LOGS_DIR } from "./utils/config";
|
import { CONFIG_DIR, LOGS_DIR } from "./utils/config";
|
||||||
import { withCrossProcessLock } from "./utils/process-lock";
|
import { withCrossProcessLock } from "./utils/process-lock";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { existsSync } from "fs";
|
|
||||||
|
|
||||||
const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`;
|
const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`;
|
||||||
const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`;
|
const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`;
|
||||||
|
|||||||
Reference in New Issue
Block a user