mirror of
https://github.com/Routstr/routstrd.git
synced 2026-08-09 03:44:38 +00:00
feat: add history command and /wallet/history endpoint
- Add 'history' CLI command with --limit, --offset, --verbose, --json options - Add GET /wallet/history daemon endpoint - Implement getHistory() in coco-client using coco.history.getPaginatedHistory() - Add getHistory stub (returns []) to cocod-client interface
This commit is contained in:
+102
@@ -1113,6 +1113,108 @@ npubsCmd
|
||||
);
|
||||
});
|
||||
|
||||
// History - show transaction history
|
||||
program
|
||||
.command("history")
|
||||
.description("Show wallet transaction history")
|
||||
.option("-n, --limit <number>", "Number of entries to show", "50")
|
||||
.option("--offset <number>", "Number of entries to skip", "0")
|
||||
.option("-v, --verbose", "Show full details including encoded Cashu tokens")
|
||||
.option("--json", "Output raw JSON with token objects (no encoding)")
|
||||
.action(async (options: { limit: string; offset: string; verbose: boolean; json: boolean }) => {
|
||||
await ensureDaemonRunning();
|
||||
|
||||
const limit = Math.min(parseInt(options.limit, 10) || 50, 1000);
|
||||
const offset = parseInt(options.offset, 10) || 0;
|
||||
|
||||
const result = await callDaemon(
|
||||
`/wallet/history?offset=${offset}&limit=${limit}`,
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
console.log(result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = result.output as
|
||||
| { entries?: Record<string, unknown>[]; offset?: number; limit?: number }
|
||||
| undefined;
|
||||
const entries = data?.entries || [];
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log("No transaction history yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
// Raw JSON output with token as JSON object
|
||||
const jsonOutput = entries.map((e: Record<string, unknown>) => {
|
||||
const entry = { ...e };
|
||||
delete entry.encodedToken;
|
||||
return entry;
|
||||
});
|
||||
console.log(JSON.stringify(jsonOutput, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.verbose) {
|
||||
// Verbose: JSON-like with encoded token
|
||||
for (const entry of entries) {
|
||||
const e = entry as Record<string, unknown>;
|
||||
const display: Record<string, unknown> = {};
|
||||
for (const key of Object.keys(e).sort()) {
|
||||
display[key] = e[key];
|
||||
}
|
||||
// Replace raw token with encoded one if present
|
||||
if (e.encodedToken && e.token) {
|
||||
display.token = e.encodedToken;
|
||||
delete display.encodedToken;
|
||||
}
|
||||
console.log(JSON.stringify(display, null, 2));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: table format
|
||||
const idCol = "ID";
|
||||
const timeCol = "Date/Time";
|
||||
const typeCol = "Type";
|
||||
const mintCol = "Mint";
|
||||
const amtCol = "Amount";
|
||||
|
||||
const rows = entries.map((entry: Record<string, unknown>) => {
|
||||
const id = String(entry.id ?? "");
|
||||
const time = new Date(Number(entry.createdAt)).toISOString().replace("T", " ").slice(0, 19);
|
||||
const type = String(entry.type ?? "").toUpperCase();
|
||||
const mint = String(entry.mintUrl ?? "");
|
||||
const unit = String(entry.unit ?? "sat");
|
||||
const amount = `${entry.amount} ${unit}`;
|
||||
return { id, time, type, mint, amount };
|
||||
});
|
||||
|
||||
const widths = {
|
||||
id: Math.max(idCol.length, ...rows.map((r) => r.id.length)),
|
||||
time: Math.max(timeCol.length, ...rows.map((r) => r.time.length)),
|
||||
type: Math.max(typeCol.length, ...rows.map((r) => r.type.length)),
|
||||
mint: Math.max(mintCol.length, ...rows.map((r) => r.mint.length)),
|
||||
amount: Math.max(amtCol.length, ...rows.map((r) => r.amount.length)),
|
||||
};
|
||||
|
||||
const pad = (s: string, w: number) => s.padEnd(w);
|
||||
const sep = Object.values(widths).map((w) => "-".repeat(w)).join(" | ");
|
||||
|
||||
console.log(
|
||||
`${pad(idCol, widths.id)} | ${pad(timeCol, widths.time)} | ${pad(typeCol, widths.type)} | ${pad(mintCol, widths.mint)} | ${pad(amtCol, widths.amount)}`,
|
||||
);
|
||||
console.log(sep);
|
||||
|
||||
for (const row of rows) {
|
||||
console.log(
|
||||
`${pad(row.id, widths.id)} | ${pad(row.time, widths.time)} | ${pad(row.type, widths.type)} | ${pad(row.mint, widths.mint)} | ${pad(row.amount, widths.amount)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor - interactive TUI
|
||||
program
|
||||
.command("monitor")
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from "@routstr/sdk";
|
||||
import type { UsageTrackingDriver, SdkLogger } from "@routstr/sdk";
|
||||
import type { RequestResponseLogSink } from "../request-response-log-sink";
|
||||
import { getEncodedToken } from "@cashu/coco-core";
|
||||
import type { HistoryEntry } from "@cashu/coco-core";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { loadDaemonConfig, saveDaemonConfig } from "../config-store";
|
||||
import {
|
||||
@@ -437,6 +439,31 @@ export function createDaemonRequestHandler(deps: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/wallet/history") {
|
||||
await respond(res, async () => {
|
||||
const offsetParam = url.searchParams.get("offset");
|
||||
const limitParam = url.searchParams.get("limit");
|
||||
const offset = offsetParam ? parseInt(offsetParam, 10) || 0 : 0;
|
||||
const limit = limitParam ? parseInt(limitParam, 10) || 50 : 50;
|
||||
const entries = await deps.walletClient.getHistory(offset, limit);
|
||||
|
||||
// Encode tokens for send/receive entries
|
||||
const encoded = entries.map((entry: HistoryEntry) => {
|
||||
const base = { ...entry } as Record<string, unknown>;
|
||||
if (
|
||||
(entry.type === "send" || entry.type === "receive") &&
|
||||
entry.token
|
||||
) {
|
||||
base.encodedToken = getEncodedToken(entry.token);
|
||||
}
|
||||
return base;
|
||||
});
|
||||
|
||||
return { output: { entries: encoded, offset, limit } };
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── NWC endpoints ─────────────────────────────────────────────
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/nwc/status") {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { initializeCoco, getEncodedToken } from "@cashu/coco-core";
|
||||
import type { HistoryEntry } from "@cashu/coco-core";
|
||||
import { SqliteRepositories } from "@cashu/coco-sqlite-bun";
|
||||
import { Database } from "bun:sqlite";
|
||||
import {
|
||||
@@ -433,5 +434,9 @@ export async function createCocoClient(
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async getHistory(offset?: number, limit?: number): Promise<HistoryEntry[]> {
|
||||
return coco.history.getPaginatedHistory(offset, limit);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { existsSync } from "fs";
|
||||
import { createHash } from "crypto";
|
||||
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`;
|
||||
@@ -58,6 +59,7 @@ export interface CocodClient {
|
||||
getMintInfo(url: string): Promise<unknown>;
|
||||
/** Release resources held by in-process wallet implementations. */
|
||||
dispose?(): Promise<void>;
|
||||
getHistory(offset?: number, limit?: number): Promise<HistoryEntry[]>;
|
||||
}
|
||||
|
||||
export function resolveCocodExecutable(cocodPath?: string | null): string {
|
||||
@@ -355,5 +357,8 @@ export function createCocodClient(
|
||||
async getMintInfo(url: string): Promise<unknown> {
|
||||
return post<unknown>("/mints/info", { url });
|
||||
},
|
||||
async getHistory(_offset?: number, _limit?: number): Promise<HistoryEntry[]> {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user