mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 12:28:23 +00:00
chore(wallet): remove the dead legacy cocod client
The external `cocod` binary was replaced by the in-process Coco wallet long ago, but its HTTP/unix-socket client kept shipping ~280 lines of unreachable code next to the wallet contract. Remove it: - delete `createCocodClient` (spawns `cocod init`, talks over a unix socket, takes a cross-process startup lock) and its helpers `resolveCocodExecutable`, `isCocodInstalled`, `normalizeBalances` and `CocodBalanceOutput`. None of them are reachable from production or from tests; `isCocodInstalled` had no callers at all. - drop the `options.walletClient || createCocodClient(...)` fallback in `createWalletAdapter`. The daemon always passes the in-process client built by `createCocoClient()`, so `walletClient` is now required and the adapter can no longer silently spawn an external binary. - drop the vestigial `cocodPath` config key, which only ever fed that fallback. - delete the "legacy cocod client NPC passthroughs" test block, which covered only the deleted client. The shared wallet contract moves from `cocod-client.ts` to `wallet-client.ts` and loses its `Cocod` prefixes (`WalletClient`, `WalletRuntimeState`, `WalletHttpError`) — none of it describes cocod any more. `WalletRuntimeState` avoids colliding with the existing `WalletState` in `wallet-state.ts`. Legacy cocod *migration* code is deliberately untouched: `stopLegacyCocod`, `assertLegacyCocodNotRunning`, `claimLegacyCocodPidFile`, `migration.ts` and the `legacyCocod*` paths still migrate an existing `~/.cocod` wallet and fence off a running external daemon. Net -456 lines. Existing configs that still contain `cocodPath` load unchanged (unknown keys are ignored) — the key is just no longer written.
This commit is contained in:
@@ -275,7 +275,6 @@ Configuration is stored in `~/.routstrd/config.json`:
|
|||||||
"port": 8008,
|
"port": 8008,
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"provider": null,
|
"provider": null,
|
||||||
"cocodPath": null,
|
|
||||||
"autoModelPath": false,
|
"autoModelPath": false,
|
||||||
"autoRefresh": { "enabled": true }
|
"autoRefresh": { "enabled": true }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,7 +488,6 @@ Config file: `~/.routstrd/config.json`
|
|||||||
| `port` | number | 8008 | Daemon HTTP port |
|
| `port` | number | 8008 | Daemon HTTP port |
|
||||||
| `host` | string | `"127.0.0.1"` | Bind address |
|
| `host` | string | `"127.0.0.1"` | Bind address |
|
||||||
| `provider` | string\|null | null | Default provider URL |
|
| `provider` | string\|null | null | Default provider URL |
|
||||||
| `cocodPath` | string\|null | null | Custom path to a legacy cocod executable |
|
|
||||||
| `mode` | string | `"apikeys"` | Client mode (`apikeys` or `xcashu`) |
|
| `mode` | string | `"apikeys"` | Client mode (`apikeys` or `xcashu`) |
|
||||||
| `maxTokens` | number | 64000 | Completion budget applied when a client sets no output-token limit |
|
| `maxTokens` | number | 64000 | Completion budget applied when a client sets no output-token limit |
|
||||||
| `daemonUrl` | string | — | Remote daemon URL (set by `routstrd remote`) |
|
| `daemonUrl` | string | — | Remote daemon URL (set by `routstrd remote`) |
|
||||||
|
|||||||
+1
-4
@@ -305,10 +305,7 @@ async function initDaemon(integrationKey?: IntegrationKey): Promise<void> {
|
|||||||
|
|
||||||
// Create initial config (0600, atomic write)
|
// Create initial config (0600, atomic write)
|
||||||
if (!existsSync(CONFIG_FILE)) {
|
if (!existsSync(CONFIG_FILE)) {
|
||||||
const config: RoutstrdConfig = {
|
const config: RoutstrdConfig = { ...DEFAULT_CONFIG };
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
cocodPath: null,
|
|
||||||
};
|
|
||||||
saveDaemonConfig(config);
|
saveDaemonConfig(config);
|
||||||
console.log(`Created config file: ${CONFIG_FILE}`);
|
console.log(`Created config file: ${CONFIG_FILE}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-23
@@ -13,11 +13,11 @@ import type { HistoryEntry } from "@cashu/coco-core";
|
|||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
import { loadDaemonConfig, saveDaemonConfig } from "../config-store";
|
import { loadDaemonConfig, saveDaemonConfig } from "../config-store";
|
||||||
import {
|
import {
|
||||||
CocodHttpError,
|
WalletHttpError,
|
||||||
type CocodClient,
|
type WalletClient,
|
||||||
type CocodState,
|
type WalletRuntimeState,
|
||||||
type WalletRecoveryProgress,
|
type WalletRecoveryProgress,
|
||||||
} from "../wallet/cocod-client";
|
} from "../wallet/wallet-client";
|
||||||
import { receiveCashuToken } from "../wallet";
|
import { receiveCashuToken } from "../wallet";
|
||||||
import { getClientsFromStore } from "../../utils/clients";
|
import { getClientsFromStore } from "../../utils/clients";
|
||||||
import { getUsageSummary } from "./usage-summary";
|
import { getUsageSummary } from "./usage-summary";
|
||||||
@@ -50,7 +50,7 @@ type ClientMode = "xcashu" | "lazyrefund" | "apikeys";
|
|||||||
type WalletStatusOutput = {
|
type WalletStatusOutput = {
|
||||||
daemon: "running";
|
daemon: "running";
|
||||||
wallet: "connected" | "recovering" | "error";
|
wallet: "connected" | "recovering" | "error";
|
||||||
walletState: CocodState;
|
walletState: WalletRuntimeState;
|
||||||
balances?: Record<string, number>;
|
balances?: Record<string, number>;
|
||||||
mode: ClientMode;
|
mode: ClientMode;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -61,7 +61,7 @@ type DaemonDeps = {
|
|||||||
server: { close(cb?: () => void): void };
|
server: { close(cb?: () => void): void };
|
||||||
shutdown?: () => void;
|
shutdown?: () => void;
|
||||||
store: any;
|
store: any;
|
||||||
walletClient: CocodClient;
|
walletClient: WalletClient;
|
||||||
walletAdapter: any;
|
walletAdapter: any;
|
||||||
storageAdapter: any;
|
storageAdapter: any;
|
||||||
discoveryAdapter: any;
|
discoveryAdapter: any;
|
||||||
@@ -132,7 +132,7 @@ async function readJsonBody(
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(bodyText) as Record<string, unknown>;
|
return JSON.parse(bodyText) as Record<string, unknown>;
|
||||||
} catch {
|
} catch {
|
||||||
throw new CocodHttpError(400, "Invalid JSON body.");
|
throw new WalletHttpError(400, "Invalid JSON body.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ function toErrorMessage(error: unknown): string {
|
|||||||
return error instanceof Error ? error.message : String(error);
|
return error instanceof Error ? error.message : String(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWalletStateMessage(state: CocodState): string {
|
function getWalletStateMessage(state: WalletRuntimeState): string {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case "LOCKED":
|
case "LOCKED":
|
||||||
return "Wallet is locked. Unlock it before performing wallet operations.";
|
return "Wallet is locked. Unlock it before performing wallet operations.";
|
||||||
@@ -220,7 +220,7 @@ function respondWithError(
|
|||||||
error: unknown,
|
error: unknown,
|
||||||
fallbackStatus = 500,
|
fallbackStatus = 500,
|
||||||
): void {
|
): void {
|
||||||
if (error instanceof CocodHttpError) {
|
if (error instanceof WalletHttpError) {
|
||||||
sendJson(res, error.status, { error: error.message });
|
sendJson(res, error.status, { error: error.message });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -253,7 +253,7 @@ function getRequiredStringField(
|
|||||||
): string {
|
): string {
|
||||||
const value = requireStringField(body, field);
|
const value = requireStringField(body, field);
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new CocodHttpError(400, `Missing required '${field}' field.`);
|
throw new WalletHttpError(400, `Missing required '${field}' field.`);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ function getRequiredPositiveNumberField(
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new CocodHttpError(400, `Missing required '${field}' field.`);
|
throw new WalletHttpError(400, `Missing required '${field}' field.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function optionalStringField(
|
function optionalStringField(
|
||||||
@@ -293,7 +293,7 @@ function optionalStringArrayField(
|
|||||||
!Array.isArray(value) ||
|
!Array.isArray(value) ||
|
||||||
value.some((item) => typeof item !== "string" || !item.trim())
|
value.some((item) => typeof item !== "string" || !item.trim())
|
||||||
) {
|
) {
|
||||||
throw new CocodHttpError(400, `'${field}' must be an array of non-empty strings.`);
|
throw new WalletHttpError(400, `'${field}' must be an array of non-empty strings.`);
|
||||||
}
|
}
|
||||||
return value.map((item: string) => item.trim());
|
return value.map((item: string) => item.trim());
|
||||||
}
|
}
|
||||||
@@ -349,7 +349,7 @@ async function buildStatusOutput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function buildWalletDetails(deps: DaemonDeps): Promise<{
|
async function buildWalletDetails(deps: DaemonDeps): Promise<{
|
||||||
state: CocodState;
|
state: WalletRuntimeState;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
balances?: Record<string, number>;
|
balances?: Record<string, number>;
|
||||||
unit?: "sat";
|
unit?: "sat";
|
||||||
@@ -411,7 +411,7 @@ const HISTORY_TYPE_SCAN_PAGE_SIZE = 200;
|
|||||||
* scan pages until enough matches accumulate (or history is exhausted).
|
* scan pages until enough matches accumulate (or history is exhausted).
|
||||||
*/
|
*/
|
||||||
export async function getHistoryByTypes(
|
export async function getHistoryByTypes(
|
||||||
client: Pick<CocodClient, "getHistory">,
|
client: Pick<WalletClient, "getHistory">,
|
||||||
types: string[],
|
types: string[],
|
||||||
offset: number,
|
offset: number,
|
||||||
limit: number,
|
limit: number,
|
||||||
@@ -435,7 +435,7 @@ export function createDaemonRequestHandler(deps: {
|
|||||||
server: { close(cb?: () => void): void };
|
server: { close(cb?: () => void): void };
|
||||||
shutdown?: () => void;
|
shutdown?: () => void;
|
||||||
store: any;
|
store: any;
|
||||||
walletClient: CocodClient;
|
walletClient: WalletClient;
|
||||||
walletAdapter: any;
|
walletAdapter: any;
|
||||||
storageAdapter: any;
|
storageAdapter: any;
|
||||||
discoveryAdapter: any;
|
discoveryAdapter: any;
|
||||||
@@ -514,7 +514,7 @@ export function createDaemonRequestHandler(deps: {
|
|||||||
if (req.method === "POST" && url.pathname === "/wallet/cleanup") {
|
if (req.method === "POST" && url.pathname === "/wallet/cleanup") {
|
||||||
await respond(res, async () => {
|
await respond(res, async () => {
|
||||||
if (!deps.walletClient.cleanupStuckOperations) {
|
if (!deps.walletClient.cleanupStuckOperations) {
|
||||||
throw new CocodHttpError(
|
throw new WalletHttpError(
|
||||||
501,
|
501,
|
||||||
"Wallet cleanup is not supported by this wallet client.",
|
"Wallet cleanup is not supported by this wallet client.",
|
||||||
);
|
);
|
||||||
@@ -538,7 +538,7 @@ export function createDaemonRequestHandler(deps: {
|
|||||||
if (req.method === "POST" && url.pathname === "/wallet/recover") {
|
if (req.method === "POST" && url.pathname === "/wallet/recover") {
|
||||||
await respond(res, async () => {
|
await respond(res, async () => {
|
||||||
if (!deps.walletClient.recoverMintQuotes) {
|
if (!deps.walletClient.recoverMintQuotes) {
|
||||||
throw new CocodHttpError(
|
throw new WalletHttpError(
|
||||||
501,
|
501,
|
||||||
"Mint quote recovery is not supported by this wallet client.",
|
"Mint quote recovery is not supported by this wallet client.",
|
||||||
);
|
);
|
||||||
@@ -547,7 +547,7 @@ export function createDaemonRequestHandler(deps: {
|
|||||||
const body = await readJsonBody(req);
|
const body = await readJsonBody(req);
|
||||||
const operationIds = optionalStringArrayField(body, "operationIds");
|
const operationIds = optionalStringArrayField(body, "operationIds");
|
||||||
if (body.includeFailed === true && !operationIds?.length) {
|
if (body.includeFailed === true && !operationIds?.length) {
|
||||||
throw new CocodHttpError(
|
throw new WalletHttpError(
|
||||||
400,
|
400,
|
||||||
"'includeFailed' requires non-empty 'operationIds'.",
|
"'includeFailed' requires non-empty 'operationIds'.",
|
||||||
);
|
);
|
||||||
@@ -557,7 +557,7 @@ export function createDaemonRequestHandler(deps: {
|
|||||||
(typeof body.timeoutMs !== "number" ||
|
(typeof body.timeoutMs !== "number" ||
|
||||||
!Number.isFinite(body.timeoutMs) || body.timeoutMs <= 0)
|
!Number.isFinite(body.timeoutMs) || body.timeoutMs <= 0)
|
||||||
) {
|
) {
|
||||||
throw new CocodHttpError(
|
throw new WalletHttpError(
|
||||||
400,
|
400,
|
||||||
"'timeoutMs' must be a positive finite number.",
|
"'timeoutMs' must be a positive finite number.",
|
||||||
);
|
);
|
||||||
@@ -575,7 +575,7 @@ export function createDaemonRequestHandler(deps: {
|
|||||||
if (req.method === "POST" && url.pathname === "/wallet/recover/operations") {
|
if (req.method === "POST" && url.pathname === "/wallet/recover/operations") {
|
||||||
await respond(res, async () => {
|
await respond(res, async () => {
|
||||||
if (!deps.walletClient.recoverStuckOperations) {
|
if (!deps.walletClient.recoverStuckOperations) {
|
||||||
throw new CocodHttpError(
|
throw new WalletHttpError(
|
||||||
501,
|
501,
|
||||||
"Stuck operation recovery is not supported by this wallet client.",
|
"Stuck operation recovery is not supported by this wallet client.",
|
||||||
);
|
);
|
||||||
@@ -615,7 +615,7 @@ export function createDaemonRequestHandler(deps: {
|
|||||||
const operationId = decodeURIComponent(mintQuoteMatch[1]);
|
const operationId = decodeURIComponent(mintQuoteMatch[1]);
|
||||||
await respond(res, async () => {
|
await respond(res, async () => {
|
||||||
const quote = await deps.walletClient.getMintQuote?.(operationId);
|
const quote = await deps.walletClient.getMintQuote?.(operationId);
|
||||||
if (!quote) throw new CocodHttpError(404, "Mint quote not found");
|
if (!quote) throw new WalletHttpError(404, "Mint quote not found");
|
||||||
return { output: quote };
|
return { output: quote };
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -759,12 +759,12 @@ export function createDaemonRequestHandler(deps: {
|
|||||||
const amount = typeof pr.amount === "number" ? pr.amount : 0;
|
const amount = typeof pr.amount === "number" ? pr.amount : 0;
|
||||||
const mints = Array.isArray(pr.mints) ? pr.mints.join(", ") : "";
|
const mints = Array.isArray(pr.mints) ? pr.mints.join(", ") : "";
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
throw new CocodHttpError(
|
throw new WalletHttpError(
|
||||||
402,
|
402,
|
||||||
`Failed to set username. Required amount: ${amount} SATS. Required mints: ${mints}`,
|
`Failed to set username. Required amount: ${amount} SATS. Required mints: ${mints}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new CocodHttpError(
|
throw new WalletHttpError(
|
||||||
402,
|
402,
|
||||||
`Payment required to set username: ${amount} SATS. ` +
|
`Payment required to set username: ${amount} SATS. ` +
|
||||||
`Use 'routstrd wallet npc username ${username} --confirm' to proceed`,
|
`Use 'routstrd wallet npc username ${username} --confirm' to proceed`,
|
||||||
|
|||||||
@@ -181,7 +181,6 @@ export async function runDaemon(argv: string[] = process.argv): Promise<void> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const walletAdapter = await createWalletAdapter({
|
const walletAdapter = await createWalletAdapter({
|
||||||
cocodPath: config.cocodPath,
|
|
||||||
walletClient,
|
walletClient,
|
||||||
getAutoRefillConfig,
|
getAutoRefillConfig,
|
||||||
nwcConnectionString: config.nwc?.connectionString,
|
nwcConnectionString: config.nwc?.connectionString,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// Uses applesauce-wallet-connect (same approach as nwc_integration/pay_invoice.mts).
|
// Uses applesauce-wallet-connect (same approach as nwc_integration/pay_invoice.mts).
|
||||||
|
|
||||||
import type { CocodClient } from "./cocod-client";
|
import type { WalletClient } from "./wallet-client";
|
||||||
import type { WalletConnect } from "applesauce-wallet-connect";
|
import type { WalletConnect } from "applesauce-wallet-connect";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ function isFatalError(message: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function startAutoRefillLoop(
|
export function startAutoRefillLoop(
|
||||||
cocod: CocodClient,
|
walletClient: WalletClient,
|
||||||
getWallet: () => WalletConnect | undefined,
|
getWallet: () => WalletConnect | undefined,
|
||||||
getConfig: () => AutoRefillConfig | undefined,
|
getConfig: () => AutoRefillConfig | undefined,
|
||||||
intervalMs: number = 5000,
|
intervalMs: number = 5000,
|
||||||
@@ -88,7 +88,7 @@ export function startAutoRefillLoop(
|
|||||||
checkInProgress = true;
|
checkInProgress = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const balances = await cocod.getBalances();
|
const balances = await walletClient.getBalances();
|
||||||
const totalBalance = Object.values(balances).reduce<number>(
|
const totalBalance = Object.values(balances).reduce<number>(
|
||||||
(sum, b) => sum + (typeof b === "number" ? b : 0),
|
(sum, b) => sum + (typeof b === "number" ? b : 0),
|
||||||
0,
|
0,
|
||||||
@@ -103,17 +103,20 @@ export function startAutoRefillLoop(
|
|||||||
`[auto-refill] Balance ${totalBalance} sats < threshold ${config.threshold}. Refilling ${config.amount} sats...`,
|
`[auto-refill] Balance ${totalBalance} sats < threshold ${config.threshold}. Refilling ${config.amount} sats...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mintUrl = await cocod.getDefaultMint();
|
const mintUrl = await walletClient.getDefaultMint();
|
||||||
if (!mintUrl) {
|
if (!mintUrl) {
|
||||||
logger.error("[auto-refill] No default mint configured");
|
logger.error("[auto-refill] No default mint configured");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Create a BOLT-11 invoice via cocod to fund the Cashu wallet
|
// Step 1: Create a BOLT-11 invoice via the wallet to fund the Cashu wallet
|
||||||
logger.log(
|
logger.log(
|
||||||
`[auto-refill] Creating BOLT-11 invoice for ${config.amount} sats via ${mintUrl}...`,
|
`[auto-refill] Creating BOLT-11 invoice for ${config.amount} sats via ${mintUrl}...`,
|
||||||
);
|
);
|
||||||
const { invoice } = await cocod.receiveBolt11(config.amount, mintUrl);
|
const { invoice } = await walletClient.receiveBolt11(
|
||||||
|
config.amount,
|
||||||
|
mintUrl,
|
||||||
|
);
|
||||||
|
|
||||||
// Step 2: Pay the invoice via NWC (applesauce)
|
// Step 2: Pay the invoice via NWC (applesauce)
|
||||||
logger.log(`[auto-refill] Paying invoice via NWC...`);
|
logger.log(`[auto-refill] Paying invoice via NWC...`);
|
||||||
@@ -125,7 +128,7 @@ export function startAutoRefillLoop(
|
|||||||
const payment = await payInvoice(invoice);
|
const payment = await payInvoice(invoice);
|
||||||
|
|
||||||
// Step 3: The Cashu mint should automatically detect the paid invoice
|
// Step 3: The Cashu mint should automatically detect the paid invoice
|
||||||
// and issue tokens. We don't need to explicitly mint here; cocod
|
// and issue tokens. We don't need to explicitly mint here; the wallet
|
||||||
// handles this on its end when the mint sees the payment.
|
// handles this on its end when the mint sees the payment.
|
||||||
const preimage = payment.preimage;
|
const preimage = payment.preimage;
|
||||||
if (preimage) {
|
if (preimage) {
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ mock.module("coco-cashu-plugin-npc", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const { createCocoClient } = await import("./coco-client");
|
const { createCocoClient } = await import("./coco-client");
|
||||||
const { createCocodClient, CocodHttpError } = await import("./cocod-client");
|
|
||||||
|
|
||||||
const TEST_MNEMONIC =
|
const TEST_MNEMONIC =
|
||||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
||||||
@@ -169,84 +168,3 @@ describe("in-process coco client NPC integration", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("legacy cocod client NPC passthroughs", () => {
|
|
||||||
type FetchImpl = (
|
|
||||||
input: string | URL | Request,
|
|
||||||
init?: RequestInit & { unix: string },
|
|
||||||
) => Promise<Response>;
|
|
||||||
|
|
||||||
function legacyClient(handler: (path: string, body?: unknown) => Response) {
|
|
||||||
const requests: Array<{ path: string; body?: unknown }> = [];
|
|
||||||
const fetchImpl: FetchImpl = async (input, init) => {
|
|
||||||
const path = String(input).replace("http://localhost", "");
|
|
||||||
const body =
|
|
||||||
typeof init?.body === "string" ? JSON.parse(init.body) : undefined;
|
|
||||||
if (path !== "/ping") requests.push({ path, body });
|
|
||||||
if (path === "/ping") {
|
|
||||||
return new Response(JSON.stringify({ output: "pong" }));
|
|
||||||
}
|
|
||||||
return handler(path, body);
|
|
||||||
};
|
|
||||||
const client = createCocodClient({
|
|
||||||
socketPath: "/tmp/routstrd-test-legacy-npc/cocod.sock",
|
|
||||||
fetchImpl,
|
|
||||||
});
|
|
||||||
return { client, requests };
|
|
||||||
}
|
|
||||||
|
|
||||||
it("fetches and parses a username NPC address", async () => {
|
|
||||||
const { client, requests } = legacyClient(
|
|
||||||
() => new Response(JSON.stringify({ output: "alice@npubx.cash" })),
|
|
||||||
);
|
|
||||||
const info = await client.getNpcAddress();
|
|
||||||
expect(info).toEqual({ address: "alice@npubx.cash", name: "alice" });
|
|
||||||
expect(requests[0]?.path).toBe("/npc/address");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("omits the name for npub fallback addresses", async () => {
|
|
||||||
const { client } = legacyClient(
|
|
||||||
() =>
|
|
||||||
new Response(JSON.stringify({ output: "npub1abc123@npubx.cash" })),
|
|
||||||
);
|
|
||||||
const info = await client.getNpcAddress();
|
|
||||||
expect(info).toEqual({ address: "npub1abc123@npubx.cash" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("posts username claims with the confirm flag", async () => {
|
|
||||||
const { client, requests } = legacyClient(
|
|
||||||
() => new Response(JSON.stringify({ output: { success: true } })),
|
|
||||||
);
|
|
||||||
const result = await client.setNpcUsername("bob", true);
|
|
||||||
expect(result).toEqual({ success: true });
|
|
||||||
expect(requests[0]).toEqual({
|
|
||||||
path: "/npc/username",
|
|
||||||
body: { username: "bob", confirm: true },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("preserves the 402 payment-required error from cocod", async () => {
|
|
||||||
const { client } = legacyClient(
|
|
||||||
() =>
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({ error: "Payment required to set username" }),
|
|
||||||
{ status: 402 },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await client.setNpcUsername("bob");
|
|
||||||
expect.unreachable("should have thrown");
|
|
||||||
} catch (error) {
|
|
||||||
expect(error).toBeInstanceOf(CocodHttpError);
|
|
||||||
expect((error as InstanceType<typeof CocodHttpError>).status).toBe(402);
|
|
||||||
expect((error as Error).message).toContain("Payment required");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reports manual sync as unsupported", async () => {
|
|
||||||
const { client } = legacyClient(
|
|
||||||
() => new Response(JSON.stringify({ output: {} })),
|
|
||||||
);
|
|
||||||
await expect(client.syncNpc()).rejects.toThrow("not supported");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ import {
|
|||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
import { mnemonicToSeedSync } from "@scure/bip39";
|
import { mnemonicToSeedSync } from "@scure/bip39";
|
||||||
import type {
|
import type {
|
||||||
CocodClient,
|
WalletClient,
|
||||||
CocodState,
|
WalletRuntimeState,
|
||||||
NpcAddress,
|
NpcAddress,
|
||||||
NpcUsernameResult,
|
NpcUsernameResult,
|
||||||
WalletCleanupOptions,
|
WalletCleanupOptions,
|
||||||
WalletCleanupResult,
|
WalletCleanupResult,
|
||||||
WalletRecoveryProgress,
|
WalletRecoveryProgress,
|
||||||
} from "./cocod-client";
|
} from "./wallet-client";
|
||||||
import { selectCleanupOperations, summarizeMintCleanup } from "./cleanup";
|
import { selectCleanupOperations, summarizeMintCleanup } from "./cleanup";
|
||||||
import {
|
import {
|
||||||
classifyMintQuoteObservation,
|
classifyMintQuoteObservation,
|
||||||
@@ -1812,7 +1812,7 @@ export async function runWalletRecovery(
|
|||||||
|
|
||||||
export async function createCocoClient(
|
export async function createCocoClient(
|
||||||
options: CreateCocoClientOptions = {},
|
options: CreateCocoClientOptions = {},
|
||||||
): Promise<CocodClient> {
|
): Promise<WalletClient> {
|
||||||
const configDir = options.walletDir || options.configDir || defaultWalletDir();
|
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");
|
||||||
@@ -2148,7 +2148,7 @@ export async function createCocoClient(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getStatus(): Promise<CocodState> {
|
async getStatus(): Promise<WalletRuntimeState> {
|
||||||
if (recoveryError) return "ERROR";
|
if (recoveryError) return "ERROR";
|
||||||
if (!recoveryDone) return "RECOVERING";
|
if (!recoveryDone) return "RECOVERING";
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,579 +0,0 @@
|
|||||||
import { existsSync } from "fs";
|
|
||||||
import { createHash } from "crypto";
|
|
||||||
import { isAbsolute } from "path";
|
|
||||||
import { logger } from "../../utils/logger";
|
|
||||||
import { withCrossProcessLock } from "../../utils/process-lock";
|
|
||||||
import type { HistoryEntry } from "@cashu/coco-core";
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG_DIR =
|
|
||||||
process.env.COCOD_DIR || `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`;
|
|
||||||
const DEFAULT_SOCKET_PATH =
|
|
||||||
process.env.COCOD_SOCKET || `${DEFAULT_CONFIG_DIR}/cocod.sock`;
|
|
||||||
|
|
||||||
type UnixRequestInit = RequestInit & { unix: string };
|
|
||||||
|
|
||||||
type CommandResponse<T> = {
|
|
||||||
output?: T;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CocodFetch = (
|
|
||||||
input: string | URL | Request,
|
|
||||||
init?: UnixRequestInit,
|
|
||||||
) => Promise<Response>;
|
|
||||||
|
|
||||||
type SpawnedProcess = {
|
|
||||||
exited: Promise<number>;
|
|
||||||
unref?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SpawnDaemon = (
|
|
||||||
args: string[],
|
|
||||||
env: Record<string, string>,
|
|
||||||
) => SpawnedProcess;
|
|
||||||
|
|
||||||
export type CocodState =
|
|
||||||
| "UNINITIALIZED"
|
|
||||||
| "LOCKED"
|
|
||||||
| "UNLOCKED"
|
|
||||||
| "RECOVERING"
|
|
||||||
| "ERROR";
|
|
||||||
|
|
||||||
/** Live progress for background wallet recovery started at daemon startup. */
|
|
||||||
export interface WalletRecoveryProgress {
|
|
||||||
state: "RECOVERING" | "UNLOCKED" | "ERROR";
|
|
||||||
/** Current recovery phase, e.g. "Mint recovery" or "done". */
|
|
||||||
phase: string;
|
|
||||||
pendingSends: number;
|
|
||||||
inflightProofs: number;
|
|
||||||
pendingMints: number;
|
|
||||||
/** Expired unpaid mint quotes failed locally without a mint round-trip. */
|
|
||||||
failedMintQuotes: number;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CocodBalanceOutput = Record<string, { sats?: number } | number>;
|
|
||||||
|
|
||||||
/** NPC (npubx.cash) Lightning address details for this wallet. */
|
|
||||||
export interface NpcAddress {
|
|
||||||
/** Full Lightning address, e.g. "alice@npubx.cash" (npub fallback when no username is set). */
|
|
||||||
address: string;
|
|
||||||
/** NPC username, when one has been claimed. */
|
|
||||||
name?: string;
|
|
||||||
/** Nostr hex pubkey of the NPC account (only available from the in-process wallet). */
|
|
||||||
pubkey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Result of an NPC username claim attempt. */
|
|
||||||
export interface NpcUsernameResult {
|
|
||||||
success: boolean;
|
|
||||||
/** Present when NPC requires payment to claim the username. */
|
|
||||||
paymentRequest?: {
|
|
||||||
amount?: number;
|
|
||||||
mints?: string[];
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Options for the wallet cleanup command. */
|
|
||||||
export interface WalletCleanupOptions {
|
|
||||||
/** Only clean up operations for this mint URL. */
|
|
||||||
mintUrl?: string;
|
|
||||||
/** Minimum operation age in milliseconds (defaults to 7 days / 1 week). */
|
|
||||||
minAgeMs?: number;
|
|
||||||
/** Report what would be cleaned without applying changes. */
|
|
||||||
dryRun?: boolean;
|
|
||||||
/**
|
|
||||||
* Fail expired mint quotes without confirming UNPAID with the mint. Only for
|
|
||||||
* operators who accept the risk of stranding a quote that was paid before
|
|
||||||
* its invoice expired; recovery is the safe default.
|
|
||||||
*/
|
|
||||||
force?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Summary of a wallet cleanup run. */
|
|
||||||
export interface WalletCleanupResult {
|
|
||||||
dryRun: boolean;
|
|
||||||
/** Expired quotes selected for checking; dry runs do not contact the mint. */
|
|
||||||
mintQuoteCandidates: number;
|
|
||||||
/** Number actually marked failed (always zero in a dry run). */
|
|
||||||
failedMintQuotes: number;
|
|
||||||
/** Expired quotes kept pending because they are paid/issued or unverified. */
|
|
||||||
leftForRecovery: number;
|
|
||||||
/** Number of stale pending send operations reclaimed. */
|
|
||||||
reclaimedSends: number;
|
|
||||||
/** Number of stale prepared melt operations cancelled. */
|
|
||||||
cancelledMelts: number;
|
|
||||||
/** Number of in-flight operations that were left untouched. */
|
|
||||||
skipped: number;
|
|
||||||
errors: Array<{ operationId: string; error: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CocodHttpError extends Error {
|
|
||||||
status: number;
|
|
||||||
|
|
||||||
constructor(status: number, message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = "CocodHttpError";
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Progress of a Lightning top-up created with `receiveBolt11`. */
|
|
||||||
export interface MintQuoteStatus {
|
|
||||||
operationId: string;
|
|
||||||
state: "pending" | "executing" | "finalized" | "failed";
|
|
||||||
/** Last quote state reported by the mint (UNPAID, PAID, ISSUED). */
|
|
||||||
mintState?: string;
|
|
||||||
amount: number;
|
|
||||||
mintUrl: string;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Options for explicit PAID mint-quote recovery. */
|
|
||||||
export interface WalletMintQuoteRecoveryOptions {
|
|
||||||
/** Target only these operation ids (may include failed operations). */
|
|
||||||
operationIds?: string[];
|
|
||||||
/** Re-open failed operations instead of skipping them. */
|
|
||||||
includeFailed?: boolean;
|
|
||||||
/** Per-quote mint timeout in milliseconds. */
|
|
||||||
timeoutMs?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Summary of a PAID mint-quote recovery run. */
|
|
||||||
export interface WalletMintQuoteRecoveryResult {
|
|
||||||
/** Operations whose quote state was checked with the mint. */
|
|
||||||
checked: number;
|
|
||||||
/** Operations whose paid sats were minted or restored. */
|
|
||||||
recovered: number;
|
|
||||||
/** Quotes the mint still reports UNPAID; left pending. */
|
|
||||||
waiting: number;
|
|
||||||
/** Quotes the mint can no longer issue. */
|
|
||||||
terminal: number;
|
|
||||||
/** Failed operations moved back to pending before checking. */
|
|
||||||
reopened: number;
|
|
||||||
/** Operations left to a later run (mint unreachable, budget spent, non-terminal). */
|
|
||||||
retryable: number;
|
|
||||||
/** Operations skipped because an earlier recovery of them is still running. */
|
|
||||||
busy: number;
|
|
||||||
errors: Array<{ operationId: string; error: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Summary of a stuck-operation (send/melt/mint) recovery run. */
|
|
||||||
export interface WalletStuckOperationRecoveryResult {
|
|
||||||
/** Timed-out waits; the underlying operation remains tracked. */
|
|
||||||
timedOut: number;
|
|
||||||
/** Operations for which recovery was attempted (not necessarily completed). */
|
|
||||||
attempted: number;
|
|
||||||
/** Locked operations or unfinished work from another pass; retry later. */
|
|
||||||
busy: number;
|
|
||||||
/** Operations skipped for unreachable mints, shutdown, or pass budget exhaustion. */
|
|
||||||
skipped: number;
|
|
||||||
/** Operations at reachable mints whose recovery still failed. */
|
|
||||||
failed: number;
|
|
||||||
/** Unreachable mint URL -> number of operations skipped there. */
|
|
||||||
skippedMints: Record<string, number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CocodClient {
|
|
||||||
ping(): Promise<boolean>;
|
|
||||||
getStatus(): Promise<CocodState>;
|
|
||||||
unlock(passphrase: string): Promise<string>;
|
|
||||||
getBalances(): Promise<Record<string, number>>;
|
|
||||||
receiveCashu(token: string): Promise<string>;
|
|
||||||
receiveBolt11(
|
|
||||||
amount: number,
|
|
||||||
mintUrl?: string,
|
|
||||||
): Promise<{ invoice: string; operationId?: string }>;
|
|
||||||
/** Progress of a top-up, when the wallet tracks mint operations. */
|
|
||||||
getMintQuote?(operationId: string): Promise<MintQuoteStatus | null>;
|
|
||||||
sendCashu(amount: number, mintUrl?: string): Promise<string>;
|
|
||||||
sendBolt11(invoice: string, mintUrl?: string): Promise<string>;
|
|
||||||
listMints(): Promise<string[]>;
|
|
||||||
addMint(url: string): Promise<string>;
|
|
||||||
getMintInfo(url: string): Promise<unknown>;
|
|
||||||
getDefaultMint(): Promise<string | null>;
|
|
||||||
setDefaultMint(url: string): Promise<string>;
|
|
||||||
/** Release resources held by in-process wallet implementations. */
|
|
||||||
dispose?(): Promise<void>;
|
|
||||||
getHistory(offset?: number, limit?: number): Promise<HistoryEntry[]>;
|
|
||||||
/** Look up a single transaction by its history entry ID. */
|
|
||||||
getHistoryEntryById(id: string): Promise<HistoryEntry | null>;
|
|
||||||
/** NPC (npubx.cash) Lightning address for this wallet. */
|
|
||||||
getNpcAddress(): Promise<NpcAddress>;
|
|
||||||
/** Claim an NPC username; pass confirm=true to pay the claim fee from the wallet. */
|
|
||||||
setNpcUsername(username: string, confirm?: boolean): Promise<NpcUsernameResult>;
|
|
||||||
/** Manually trigger an NPC quote sync into the wallet. */
|
|
||||||
syncNpc(): Promise<void>;
|
|
||||||
/** Clear stuck pending/in-flight wallet operations that are safe to resolve. */
|
|
||||||
cleanupStuckOperations?(
|
|
||||||
options?: WalletCleanupOptions,
|
|
||||||
): Promise<WalletCleanupResult>;
|
|
||||||
/**
|
|
||||||
* Re-issue PAID mint quotes whose sats were never claimed, optionally
|
|
||||||
* targeting specific operations (including ones coco already failed).
|
|
||||||
*/
|
|
||||||
recoverMintQuotes?(
|
|
||||||
options?: WalletMintQuoteRecoveryOptions,
|
|
||||||
onProgress?: (message: string) => void,
|
|
||||||
): Promise<WalletMintQuoteRecoveryResult>;
|
|
||||||
/**
|
|
||||||
* Recover stuck send/melt/mint operations whose mints answer a
|
|
||||||
* reachability probe. Operations a live execute holds are reported busy,
|
|
||||||
* never driven. Receive stays startup-only (receive dedup classification).
|
|
||||||
*/
|
|
||||||
recoverStuckOperations?(): Promise<WalletStuckOperationRecoveryResult>;
|
|
||||||
/** Report background wallet recovery progress, when the wallet supports it. */
|
|
||||||
getRecoveryProgress?(): Promise<WalletRecoveryProgress>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveCocodExecutable(cocodPath?: string | null): string {
|
|
||||||
const trimmed = cocodPath?.trim();
|
|
||||||
return trimmed || "cocod";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function isCocodInstalled(
|
|
||||||
cocodPath?: string | null,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const executable = resolveCocodExecutable(cocodPath);
|
|
||||||
|
|
||||||
if (
|
|
||||||
isAbsolute(executable) ||
|
|
||||||
executable.includes("/") ||
|
|
||||||
executable.includes("\\")
|
|
||||||
) {
|
|
||||||
return existsSync(executable);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const command = process.platform === "win32" ? "where.exe" : "which";
|
|
||||||
const proc = Bun.spawn({
|
|
||||||
cmd: [command, executable],
|
|
||||||
stdout: "ignore",
|
|
||||||
stderr: "ignore",
|
|
||||||
});
|
|
||||||
return (await proc.exited) === 0;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeBalances(
|
|
||||||
output: CocodBalanceOutput | undefined,
|
|
||||||
): Record<string, number> {
|
|
||||||
if (!output) return {};
|
|
||||||
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(output).map(([mintUrl, value]) => {
|
|
||||||
if (typeof value === "number") {
|
|
||||||
return [mintUrl, value];
|
|
||||||
}
|
|
||||||
return [mintUrl, Number(value?.sats ?? 0)];
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMintList(output: string | undefined): string[] {
|
|
||||||
return (output || "")
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function delay(ms: number): Promise<void> {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
function toErrorText(value: unknown): string {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value);
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenFingerprint(token: string): string {
|
|
||||||
return createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createCocodClient(
|
|
||||||
options: {
|
|
||||||
cocodPath?: string | null;
|
|
||||||
socketPath?: string;
|
|
||||||
fetchImpl?: CocodFetch;
|
|
||||||
spawnDaemon?: SpawnDaemon;
|
|
||||||
pollIntervalMs?: number;
|
|
||||||
startupTimeoutMs?: number;
|
|
||||||
} = {},
|
|
||||||
): CocodClient {
|
|
||||||
const executable = resolveCocodExecutable(options.cocodPath);
|
|
||||||
const socketPath = options.socketPath || DEFAULT_SOCKET_PATH;
|
|
||||||
const startupLockPath = `${socketPath}.startup.lock`;
|
|
||||||
const fetchImpl = options.fetchImpl || (fetch as CocodFetch);
|
|
||||||
const pollIntervalMs = options.pollIntervalMs ?? 100;
|
|
||||||
const startupTimeoutMs = options.startupTimeoutMs ?? 5000;
|
|
||||||
|
|
||||||
const spawnDaemon: SpawnDaemon =
|
|
||||||
options.spawnDaemon ||
|
|
||||||
((args, env) => {
|
|
||||||
const proc = Bun.spawn(args, {
|
|
||||||
stdin: "ignore",
|
|
||||||
stdout: "ignore",
|
|
||||||
stderr: "ignore",
|
|
||||||
detached: true,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
proc.unref();
|
|
||||||
return proc;
|
|
||||||
});
|
|
||||||
|
|
||||||
let startPromise: Promise<void> | null = null;
|
|
||||||
|
|
||||||
async function fetchJson<T>(
|
|
||||||
path: string,
|
|
||||||
init: Omit<UnixRequestInit, "unix"> = {},
|
|
||||||
): Promise<CommandResponse<T>> {
|
|
||||||
const method = init.method || "GET";
|
|
||||||
const requestInit: UnixRequestInit = {
|
|
||||||
...init,
|
|
||||||
unix: socketPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetchImpl(`http://localhost${path}`, requestInit);
|
|
||||||
const rawText = await response.text();
|
|
||||||
// logger.debug(
|
|
||||||
// `[fetchJson] ${method} ${path} status=${response.status} body=${rawText}`,
|
|
||||||
// );
|
|
||||||
|
|
||||||
if (!rawText.trim()) {
|
|
||||||
throw new CocodHttpError(
|
|
||||||
response.ok ? 502 : response.status,
|
|
||||||
`Empty response from cocod for ${method} ${path}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let data: CommandResponse<T>;
|
|
||||||
try {
|
|
||||||
data = JSON.parse(rawText) as CommandResponse<T>;
|
|
||||||
} catch {
|
|
||||||
throw new CocodHttpError(
|
|
||||||
response.ok ? 502 : response.status,
|
|
||||||
`Invalid JSON response from cocod for ${method} ${path}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data || typeof data !== "object") {
|
|
||||||
throw new CocodHttpError(
|
|
||||||
response.ok ? 502 : response.status,
|
|
||||||
`Unexpected response shape from cocod for ${method} ${path}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMessage = toErrorText((data as CommandResponse<T>).error);
|
|
||||||
if (errorMessage) {
|
|
||||||
throw new CocodHttpError(
|
|
||||||
response.ok ? 400 : response.status,
|
|
||||||
errorMessage,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new CocodHttpError(
|
|
||||||
response.status,
|
|
||||||
data.error || response.statusText || `HTTP ${response.status}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pingInternal(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await fetchJson<string>("/ping");
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startDaemon(): Promise<void> {
|
|
||||||
const env = { ...process.env, COCOD_SOCKET: socketPath };
|
|
||||||
const proc = spawnDaemon([executable, "init"], env);
|
|
||||||
const maxPolls = Math.ceil(startupTimeoutMs / pollIntervalMs);
|
|
||||||
let exitCode: number | null = null;
|
|
||||||
|
|
||||||
void proc.exited.then((code) => {
|
|
||||||
exitCode = code;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < maxPolls; i++) {
|
|
||||||
await delay(pollIntervalMs);
|
|
||||||
|
|
||||||
if (exitCode !== null && exitCode !== 0) {
|
|
||||||
throw new Error(`cocod init exited early with code ${exitCode}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await pingInternal()) {
|
|
||||||
logger.debug(`Connected to cocod daemon on ${socketPath}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`cocod failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureDaemonRunning(): Promise<void> {
|
|
||||||
if (await pingInternal()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!startPromise) {
|
|
||||||
startPromise = withCrossProcessLock(
|
|
||||||
startupLockPath,
|
|
||||||
async () => {
|
|
||||||
if (await pingInternal()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Starting cocod daemon via ${executable} init...`);
|
|
||||||
await startDaemon();
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acquireTimeoutMs: startupTimeoutMs + 30_000,
|
|
||||||
staleAfterMs: startupTimeoutMs + 30_000,
|
|
||||||
log: (message) => logger.debug(message),
|
|
||||||
},
|
|
||||||
).finally(() => {
|
|
||||||
startPromise = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await startPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function callDaemon<T>(
|
|
||||||
path: string,
|
|
||||||
init: Omit<UnixRequestInit, "unix"> = {},
|
|
||||||
): Promise<T> {
|
|
||||||
await ensureDaemonRunning();
|
|
||||||
const response = await fetchJson<T>(path, init);
|
|
||||||
return response.output as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
function post<T>(path: string, body: Record<string, unknown>): Promise<T> {
|
|
||||||
return callDaemon<T>(path, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
async ping(): Promise<boolean> {
|
|
||||||
return pingInternal();
|
|
||||||
},
|
|
||||||
async getStatus(): Promise<CocodState> {
|
|
||||||
return callDaemon<CocodState>("/status");
|
|
||||||
},
|
|
||||||
async unlock(passphrase: string): Promise<string> {
|
|
||||||
return post<string>("/unlock", { passphrase });
|
|
||||||
},
|
|
||||||
async getBalances(): Promise<Record<string, number>> {
|
|
||||||
const output = await callDaemon<CocodBalanceOutput>("/balance");
|
|
||||||
return normalizeBalances(output);
|
|
||||||
},
|
|
||||||
async receiveCashu(token: string): Promise<string> {
|
|
||||||
logger.debug(
|
|
||||||
`[receiveCashu] Receiving Cashu token ${tokenFingerprint(token)}`,
|
|
||||||
);
|
|
||||||
const message = await callDaemon<string>("/receive/cashu", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ token }),
|
|
||||||
});
|
|
||||||
if (typeof message !== "string" || !message.trim()) {
|
|
||||||
throw new CocodHttpError(
|
|
||||||
502,
|
|
||||||
"Unexpected response from cocod while receiving Cashu token.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
logger.debug(`[receiveCashu] Full response.output:`, message);
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
async receiveBolt11(amount: number, mintUrl?: string) {
|
|
||||||
return { invoice: await post<string>("/receive/bolt11", { amount, mintUrl }) };
|
|
||||||
},
|
|
||||||
async sendCashu(amount: number, mintUrl?: string): Promise<string> {
|
|
||||||
return post<string>("/send/cashu", { amount, mintUrl });
|
|
||||||
},
|
|
||||||
async sendBolt11(invoice: string, mintUrl?: string): Promise<string> {
|
|
||||||
return post<string>("/send/bolt11", { invoice, mintUrl });
|
|
||||||
},
|
|
||||||
async listMints(): Promise<string[]> {
|
|
||||||
const output = await callDaemon<string>("/mints/list");
|
|
||||||
return parseMintList(output);
|
|
||||||
},
|
|
||||||
async addMint(url: string): Promise<string> {
|
|
||||||
return post<string>("/mints/add", { url });
|
|
||||||
},
|
|
||||||
async getMintInfo(url: string): Promise<unknown> {
|
|
||||||
return post<unknown>("/mints/info", { url });
|
|
||||||
},
|
|
||||||
async getDefaultMint(): Promise<string | null> {
|
|
||||||
return callDaemon<string | null>("/mints/default");
|
|
||||||
},
|
|
||||||
async setDefaultMint(url: string): Promise<string> {
|
|
||||||
return post<string>("/mints/default", { url });
|
|
||||||
},
|
|
||||||
async getHistory(_offset?: number, _limit?: number): Promise<HistoryEntry[]> {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
async getHistoryEntryById(_id: string): Promise<HistoryEntry | null> {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
async getNpcAddress(): Promise<NpcAddress> {
|
|
||||||
const address = await callDaemon<string>("/npc/address");
|
|
||||||
if (typeof address !== "string" || !address.trim()) {
|
|
||||||
throw new CocodHttpError(
|
|
||||||
502,
|
|
||||||
"Unexpected response from cocod while fetching NPC address.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const trimmed = address.trim();
|
|
||||||
const localPart = trimmed.split("@")[0];
|
|
||||||
return {
|
|
||||||
address: trimmed,
|
|
||||||
...(localPart && !localPart.startsWith("npub1")
|
|
||||||
? { name: localPart }
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async setNpcUsername(
|
|
||||||
username: string,
|
|
||||||
confirm?: boolean,
|
|
||||||
): Promise<NpcUsernameResult> {
|
|
||||||
// cocod answers 402 with a payment-required message when the claim fee
|
|
||||||
// has not been confirmed; fetchJson preserves that status on the thrown
|
|
||||||
// CocodHttpError, so callers can surface it unchanged.
|
|
||||||
const result = await post<{ success?: boolean }>("/npc/username", {
|
|
||||||
username,
|
|
||||||
confirm: confirm === true,
|
|
||||||
});
|
|
||||||
return { success: result?.success !== false };
|
|
||||||
},
|
|
||||||
async syncNpc(): Promise<void> {
|
|
||||||
throw new CocodHttpError(
|
|
||||||
501,
|
|
||||||
"Manual NPC sync is not supported by the legacy cocod client.",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ import { WalletBaseError } from "applesauce-wallet-connect/helpers/error";
|
|||||||
import { RelayPool } from "applesauce-relay";
|
import { RelayPool } from "applesauce-relay";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
import { withTimeout } from "../../utils/with-timeout";
|
import { withTimeout } from "../../utils/with-timeout";
|
||||||
import { createCocodClient, type CocodClient } from "./cocod-client";
|
import type { WalletClient } from "./wallet-client";
|
||||||
import { startAutoRefillLoop, type AutoRefillConfig } from "./auto-refill";
|
import { startAutoRefillLoop, type AutoRefillConfig } from "./auto-refill";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,7 +34,7 @@ export function decodeCashuTokenAmount(token: string): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function receiveCashuToken(
|
export async function receiveCashuToken(
|
||||||
client: Pick<CocodClient, "receiveCashu">,
|
client: Pick<WalletClient, "receiveCashu">,
|
||||||
token: string,
|
token: string,
|
||||||
): Promise<{ message: string; amount: number; unit: "sat" | "msat" }> {
|
): Promise<{ message: string; amount: number; unit: "sat" | "msat" }> {
|
||||||
// Validate the token before handing it to a state-changing wallet call. This
|
// Validate the token before handing it to a state-changing wallet call. This
|
||||||
@@ -46,8 +46,8 @@ export async function receiveCashuToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WalletAdapterOptions {
|
export interface WalletAdapterOptions {
|
||||||
cocodPath?: string | null;
|
/** The in-process wallet engine. Required — construct it with `createCocoClient()`. */
|
||||||
walletClient?: CocodClient;
|
walletClient: WalletClient;
|
||||||
/** NWC connection string for Lightning funding (uses applesauce-wallet-connect) */
|
/** NWC connection string for Lightning funding (uses applesauce-wallet-connect) */
|
||||||
nwcConnectionString?: string;
|
nwcConnectionString?: string;
|
||||||
/** Override the NWC read timeout in milliseconds (test hook). */
|
/** Override the NWC read timeout in milliseconds (test hook). */
|
||||||
@@ -65,10 +65,9 @@ export interface WalletAdapterOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createWalletAdapter(
|
export async function createWalletAdapter(
|
||||||
options: WalletAdapterOptions = {},
|
options: WalletAdapterOptions,
|
||||||
) {
|
) {
|
||||||
const client =
|
const client = options.walletClient;
|
||||||
options.walletClient || createCocodClient({ cocodPath: options.cocodPath });
|
|
||||||
let activeMintUrl: string | null = null;
|
let activeMintUrl: string | null = null;
|
||||||
let mintUnits: Record<string, "sat" | "msat"> = {};
|
let mintUnits: Record<string, "sat" | "msat"> = {};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import type { HistoryEntry } from "@cashu/coco-core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract shared by every wallet implementation.
|
||||||
|
*
|
||||||
|
* The wallet runs in-process (`createCocoClient` in `./coco-client`); this
|
||||||
|
* module only describes what the rest of routstrd is allowed to ask of it.
|
||||||
|
* Historically this lived in a `cocod-client` module that also shipped an
|
||||||
|
* HTTP/unix-socket client for the external `cocod` binary. That client was
|
||||||
|
* removed once the in-process wallet replaced it — see `./migration` and the
|
||||||
|
* `legacyCocod*` path helpers in `./paths` for the migration-only code that
|
||||||
|
* still knows about external cocod installs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type WalletRuntimeState =
|
||||||
|
| "UNINITIALIZED"
|
||||||
|
| "LOCKED"
|
||||||
|
| "UNLOCKED"
|
||||||
|
| "RECOVERING"
|
||||||
|
| "ERROR";
|
||||||
|
|
||||||
|
/** Live progress for background wallet recovery started at daemon startup. */
|
||||||
|
export interface WalletRecoveryProgress {
|
||||||
|
state: "RECOVERING" | "UNLOCKED" | "ERROR";
|
||||||
|
/** Current recovery phase, e.g. "Mint recovery" or "done". */
|
||||||
|
phase: string;
|
||||||
|
pendingSends: number;
|
||||||
|
inflightProofs: number;
|
||||||
|
pendingMints: number;
|
||||||
|
/** Expired unpaid mint quotes failed locally without a mint round-trip. */
|
||||||
|
failedMintQuotes: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** NPC (npubx.cash) Lightning address details for this wallet. */
|
||||||
|
export interface NpcAddress {
|
||||||
|
/** Full Lightning address, e.g. "alice@npubx.cash" (npub fallback when no username is set). */
|
||||||
|
address: string;
|
||||||
|
/** NPC username, when one has been claimed. */
|
||||||
|
name?: string;
|
||||||
|
/** Nostr hex pubkey of the NPC account (only available from the in-process wallet). */
|
||||||
|
pubkey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Result of an NPC username claim attempt. */
|
||||||
|
export interface NpcUsernameResult {
|
||||||
|
success: boolean;
|
||||||
|
/** Present when NPC requires payment to claim the username. */
|
||||||
|
paymentRequest?: {
|
||||||
|
amount?: number;
|
||||||
|
mints?: string[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Options for the wallet cleanup command. */
|
||||||
|
export interface WalletCleanupOptions {
|
||||||
|
/** Only clean up operations for this mint URL. */
|
||||||
|
mintUrl?: string;
|
||||||
|
/** Minimum operation age in milliseconds (defaults to 7 days / 1 week). */
|
||||||
|
minAgeMs?: number;
|
||||||
|
/** Report what would be cleaned without applying changes. */
|
||||||
|
dryRun?: boolean;
|
||||||
|
/**
|
||||||
|
* Fail expired mint quotes without confirming UNPAID with the mint. Only for
|
||||||
|
* operators who accept the risk of stranding a quote that was paid before
|
||||||
|
* its invoice expired; recovery is the safe default.
|
||||||
|
*/
|
||||||
|
force?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Summary of a wallet cleanup run. */
|
||||||
|
export interface WalletCleanupResult {
|
||||||
|
dryRun: boolean;
|
||||||
|
/** Expired quotes selected for checking; dry runs do not contact the mint. */
|
||||||
|
mintQuoteCandidates: number;
|
||||||
|
/** Number actually marked failed (always zero in a dry run). */
|
||||||
|
failedMintQuotes: number;
|
||||||
|
/** Expired quotes kept pending because they are paid/issued or unverified. */
|
||||||
|
leftForRecovery: number;
|
||||||
|
/** Number of stale pending send operations reclaimed. */
|
||||||
|
reclaimedSends: number;
|
||||||
|
/** Number of stale prepared melt operations cancelled. */
|
||||||
|
cancelledMelts: number;
|
||||||
|
/** Number of in-flight operations that were left untouched. */
|
||||||
|
skipped: number;
|
||||||
|
errors: Array<{ operationId: string; error: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WalletHttpError extends Error {
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
constructor(status: number, message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "WalletHttpError";
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Progress of a Lightning top-up created with `receiveBolt11`. */
|
||||||
|
export interface MintQuoteStatus {
|
||||||
|
operationId: string;
|
||||||
|
state: "pending" | "executing" | "finalized" | "failed";
|
||||||
|
/** Last quote state reported by the mint (UNPAID, PAID, ISSUED). */
|
||||||
|
mintState?: string;
|
||||||
|
amount: number;
|
||||||
|
mintUrl: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Options for explicit PAID mint-quote recovery. */
|
||||||
|
export interface WalletMintQuoteRecoveryOptions {
|
||||||
|
/** Target only these operation ids (may include failed operations). */
|
||||||
|
operationIds?: string[];
|
||||||
|
/** Re-open failed operations instead of skipping them. */
|
||||||
|
includeFailed?: boolean;
|
||||||
|
/** Per-quote mint timeout in milliseconds. */
|
||||||
|
timeoutMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Summary of a PAID mint-quote recovery run. */
|
||||||
|
export interface WalletMintQuoteRecoveryResult {
|
||||||
|
/** Operations whose quote state was checked with the mint. */
|
||||||
|
checked: number;
|
||||||
|
/** Operations whose paid sats were minted or restored. */
|
||||||
|
recovered: number;
|
||||||
|
/** Quotes the mint still reports UNPAID; left pending. */
|
||||||
|
waiting: number;
|
||||||
|
/** Quotes the mint can no longer issue. */
|
||||||
|
terminal: number;
|
||||||
|
/** Failed operations moved back to pending before checking. */
|
||||||
|
reopened: number;
|
||||||
|
/** Operations left to a later run (mint unreachable, budget spent, non-terminal). */
|
||||||
|
retryable: number;
|
||||||
|
/** Operations skipped because an earlier recovery of them is still running. */
|
||||||
|
busy: number;
|
||||||
|
errors: Array<{ operationId: string; error: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Summary of a stuck-operation (send/melt/mint) recovery run. */
|
||||||
|
export interface WalletStuckOperationRecoveryResult {
|
||||||
|
/** Timed-out waits; the underlying operation remains tracked. */
|
||||||
|
timedOut: number;
|
||||||
|
/** Operations for which recovery was attempted (not necessarily completed). */
|
||||||
|
attempted: number;
|
||||||
|
/** Locked operations or unfinished work from another pass; retry later. */
|
||||||
|
busy: number;
|
||||||
|
/** Operations skipped for unreachable mints, shutdown, or pass budget exhaustion. */
|
||||||
|
skipped: number;
|
||||||
|
/** Operations at reachable mints whose recovery still failed. */
|
||||||
|
failed: number;
|
||||||
|
/** Unreachable mint URL -> number of operations skipped there. */
|
||||||
|
skippedMints: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletClient {
|
||||||
|
ping(): Promise<boolean>;
|
||||||
|
getStatus(): Promise<WalletRuntimeState>;
|
||||||
|
unlock(passphrase: string): Promise<string>;
|
||||||
|
getBalances(): Promise<Record<string, number>>;
|
||||||
|
receiveCashu(token: string): Promise<string>;
|
||||||
|
receiveBolt11(
|
||||||
|
amount: number,
|
||||||
|
mintUrl?: string,
|
||||||
|
): Promise<{ invoice: string; operationId?: string }>;
|
||||||
|
/** Progress of a top-up, when the wallet tracks mint operations. */
|
||||||
|
getMintQuote?(operationId: string): Promise<MintQuoteStatus | null>;
|
||||||
|
sendCashu(amount: number, mintUrl?: string): Promise<string>;
|
||||||
|
sendBolt11(invoice: string, mintUrl?: string): Promise<string>;
|
||||||
|
listMints(): Promise<string[]>;
|
||||||
|
addMint(url: string): Promise<string>;
|
||||||
|
getMintInfo(url: string): Promise<unknown>;
|
||||||
|
getDefaultMint(): Promise<string | null>;
|
||||||
|
setDefaultMint(url: string): Promise<string>;
|
||||||
|
/** Release resources held by in-process wallet implementations. */
|
||||||
|
dispose?(): Promise<void>;
|
||||||
|
getHistory(offset?: number, limit?: number): Promise<HistoryEntry[]>;
|
||||||
|
/** Look up a single transaction by its history entry ID. */
|
||||||
|
getHistoryEntryById(id: string): Promise<HistoryEntry | null>;
|
||||||
|
/** NPC (npubx.cash) Lightning address for this wallet. */
|
||||||
|
getNpcAddress(): Promise<NpcAddress>;
|
||||||
|
/** Claim an NPC username; pass confirm=true to pay the claim fee from the wallet. */
|
||||||
|
setNpcUsername(username: string, confirm?: boolean): Promise<NpcUsernameResult>;
|
||||||
|
/** Manually trigger an NPC quote sync into the wallet. */
|
||||||
|
syncNpc(): Promise<void>;
|
||||||
|
/** Clear stuck pending/in-flight wallet operations that are safe to resolve. */
|
||||||
|
cleanupStuckOperations?(
|
||||||
|
options?: WalletCleanupOptions,
|
||||||
|
): Promise<WalletCleanupResult>;
|
||||||
|
/**
|
||||||
|
* Re-issue PAID mint quotes whose sats were never claimed, optionally
|
||||||
|
* targeting specific operations (including ones coco already failed).
|
||||||
|
*/
|
||||||
|
recoverMintQuotes?(
|
||||||
|
options?: WalletMintQuoteRecoveryOptions,
|
||||||
|
onProgress?: (message: string) => void,
|
||||||
|
): Promise<WalletMintQuoteRecoveryResult>;
|
||||||
|
/**
|
||||||
|
* Recover stuck send/melt/mint operations whose mints answer a
|
||||||
|
* reachability probe. Operations a live execute holds are reported busy,
|
||||||
|
* never driven. Receive stays startup-only (receive dedup classification).
|
||||||
|
*/
|
||||||
|
recoverStuckOperations?(): Promise<WalletStuckOperationRecoveryResult>;
|
||||||
|
/** Report background wallet recovery progress, when the wallet supports it. */
|
||||||
|
getRecoveryProgress?(): Promise<WalletRecoveryProgress>;
|
||||||
|
}
|
||||||
@@ -46,7 +46,6 @@ export interface RoutstrdConfig {
|
|||||||
port: number;
|
port: number;
|
||||||
host: string;
|
host: string;
|
||||||
provider: string | null;
|
provider: string | null;
|
||||||
cocodPath: string | null;
|
|
||||||
mode?: "xcashu" | "apikeys";
|
mode?: "xcashu" | "apikeys";
|
||||||
/** Opt into SDK automatic model-path selection for DeepSeek V4.1 Flash. Disabled by default; requires daemon restart after changing. */
|
/** Opt into SDK automatic model-path selection for DeepSeek V4.1 Flash. Disabled by default; requires daemon restart after changing. */
|
||||||
autoModelPath?: boolean;
|
autoModelPath?: boolean;
|
||||||
@@ -87,7 +86,6 @@ export const DEFAULT_CONFIG: RoutstrdConfig = {
|
|||||||
port: 8008,
|
port: 8008,
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
provider: null,
|
provider: null,
|
||||||
cocodPath: null,
|
|
||||||
mode: "apikeys",
|
mode: "apikeys",
|
||||||
autoModelPath: false,
|
autoModelPath: false,
|
||||||
maxTokens: 64000,
|
maxTokens: 64000,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ const baseConfig = {
|
|||||||
port: 8008,
|
port: 8008,
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
provider: null,
|
provider: null,
|
||||||
cocodPath: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function assert(cond: unknown, msg: string): void {
|
function assert(cond: unknown, msg: string): void {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ function config(host: string): RoutstrdConfig {
|
|||||||
port: 8008,
|
port: 8008,
|
||||||
host,
|
host,
|
||||||
provider: null,
|
provider: null,
|
||||||
cocodPath: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
createWalletAdapter,
|
createWalletAdapter,
|
||||||
decodeCashuTokenAmount,
|
decodeCashuTokenAmount,
|
||||||
} from "../../src/daemon/wallet";
|
} from "../../src/daemon/wallet";
|
||||||
import type { CocodClient } from "../../src/daemon/wallet/cocod-client";
|
import type { WalletClient } from "../../src/daemon/wallet/wallet-client";
|
||||||
|
|
||||||
// A full modern keyset ID with the same format as Minibits' post-migration
|
// A full modern keyset ID with the same format as Minibits' post-migration
|
||||||
// active keyset. getEncodedToken stores only its first eight bytes in TokenV4.
|
// active keyset. getEncodedToken stores only its first eight bytes in TokenV4.
|
||||||
@@ -38,11 +38,11 @@ function makeToken({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeWalletClient(
|
function makeWalletClient(
|
||||||
receiveCashu: CocodClient["receiveCashu"],
|
receiveCashu: WalletClient["receiveCashu"],
|
||||||
): CocodClient {
|
): WalletClient {
|
||||||
// createWalletAdapter is intentionally lazy; this receive-path test only
|
// createWalletAdapter is intentionally lazy; this receive-path test only
|
||||||
// needs the one capability exercised by receiveToken.
|
// needs the one capability exercised by receiveToken.
|
||||||
return { receiveCashu } as CocodClient;
|
return { receiveCashu } as WalletClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("short keyset TokenV4 compatibility", () => {
|
describe("short keyset TokenV4 compatibility", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user