From cb699a388276dea188c5a1d0a9b323714faeaffa Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:17:39 +0000 Subject: [PATCH] added a way to disable providers --- src/cli.ts | 97 +++++++++++++++++++++++ src/daemon/http/index.ts | 163 +++++++++++++++++++++++++++++++++++---- 2 files changed, 244 insertions(+), 16 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 09375ef..676151c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -366,6 +366,103 @@ program }); }); +// Providers - list and manage providers +const providersCmd = program + .command("providers") + .description("List and manage providers"); + +providersCmd + .command("list") + .description("List all providers with their enabled/disabled status") + .action(async () => { + await ensureDaemonRunning(); + + const result = await callDaemon("/providers"); + if (result.error) { + console.log(result.error); + process.exit(1); + } + + const output = result.output as { + providers: Array<{ index: number; baseUrl: string; disabled: boolean }>; + disabledCount: number; + totalCount: number; + } | undefined; + + if (!output?.providers) { + console.log("No providers found."); + return; + } + + console.log(`Providers (${output.totalCount} total, ${output.disabledCount} disabled):\n`); + for (const provider of output.providers) { + const status = provider.disabled ? "DISABLED" : "enabled "; + console.log(` [${provider.index}] ${status} ${provider.baseUrl}`); + } + }); + +providersCmd + .command("disable ") + .description("Disable providers by their indices (e.g., routstrd providers disable 0 2 5)") + .action(async (indices: string[]) => { + await ensureDaemonRunning(); + + const indexNums = indices.map((s) => parseInt(s, 10)).filter((n) => Number.isFinite(n)); + if (indexNums.length === 0) { + console.log("No valid indices provided."); + process.exit(1); + } + + const result = await callDaemon("/providers/disable", { + method: "POST", + body: { indices: indexNums }, + }); + + if (result.error) { + console.log(result.error); + process.exit(1); + } + + const output = result.output as { message: string; disabled: string[] } | undefined; + if (output) { + console.log(output.message); + for (const url of output.disabled) { + console.log(` - ${url}`); + } + } + }); + +providersCmd + .command("enable ") + .description("Enable providers by their indices (e.g., routstrd providers enable 0 2 5)") + .action(async (indices: string[]) => { + await ensureDaemonRunning(); + + const indexNums = indices.map((s) => parseInt(s, 10)).filter((n) => Number.isFinite(n)); + if (indexNums.length === 0) { + console.log("No valid indices provided."); + process.exit(1); + } + + const result = await callDaemon("/providers/enable", { + method: "POST", + body: { indices: indexNums }, + }); + + if (result.error) { + console.log(result.error); + process.exit(1); + } + + const output = result.output as { message: string; enabled: string[] } | undefined; + if (output) { + console.log(output.message); + for (const url of output.enabled) { + console.log(` - ${url}`); + } + } + }); + // Monitor - interactive TUI program .command("monitor") diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 9bfb5ea..045e2ae 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -1,9 +1,6 @@ import { type IncomingMessage, type ServerResponse } from "http"; import { Readable } from "stream"; -import { - routeRequests, - InsufficientBalanceError, -} from "@routstr/sdk"; +import { routeRequests, InsufficientBalanceError } from "@routstr/sdk"; import type { UsageTrackingDriver } from "@routstr/sdk"; import { logger } from "../../utils/logger"; @@ -42,8 +39,6 @@ export function createDaemonRequestHandler(deps: { usageTrackingDriver: UsageTrackingDriver; }) { return async function handler(req: IncomingMessage, res: ServerResponse) { - // Log all incoming headers with PipeLineHeaders tag - const host = req.headers.host || "localhost"; const url = new URL(req.url || "/", `http://${host}`); @@ -187,11 +182,7 @@ export function createDaemonRequestHandler(deps: { ); const spender = client.getCashuSpender(); - const results = await spender.refundProviders( - refundBaseUrls, - mintUrl, - true, - ); + const results = await spender.refundProviders(mintUrl); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -291,15 +282,156 @@ export function createDaemonRequestHandler(deps: { return; } + if (req.method === "GET" && url.pathname === "/providers") { + try { + const state = deps.store.getState(); + const baseUrlsList: string[] = state.baseUrlsList || []; + const disabledProviders: string[] = state.disabledProviders || []; + + const providers = baseUrlsList.map((baseUrl, index) => ({ + index, + baseUrl, + disabled: disabledProviders.includes(baseUrl), + })); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + output: { + providers, + disabledCount: disabledProviders.length, + totalCount: baseUrlsList.length, + }, + }), + ); + } catch (error) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: String(error) })); + } + return; + } + + if (req.method === "POST" && url.pathname === "/providers/disable") { + try { + const bodyText = await readBody(req); + const body = bodyText ? JSON.parse(bodyText) : {}; + const indices = body.indices as number[] | undefined; + + if (!Array.isArray(indices)) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: "Missing or invalid 'indices' field (expected number[]).", + }), + ); + return; + } + + const state = deps.store.getState(); + const baseUrlsList: string[] = state.baseUrlsList || []; + const disabledProviders: string[] = [ + ...(state.disabledProviders || []), + ]; + + const toDisable: string[] = []; + for (const idx of indices) { + if ( + typeof idx === "number" && + idx >= 0 && + idx < baseUrlsList.length + ) { + const baseUrl = baseUrlsList[idx]!; + if (!disabledProviders.includes(baseUrl)) { + disabledProviders.push(baseUrl); + toDisable.push(baseUrl); + } + } + } + + deps.store.getState().setDisabledProviders(disabledProviders); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + output: { + message: `Disabled ${toDisable.length} provider(s)`, + disabled: toDisable, + }, + }), + ); + } catch (error) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: String(error) })); + } + return; + } + + if (req.method === "POST" && url.pathname === "/providers/enable") { + try { + const bodyText = await readBody(req); + const body = bodyText ? JSON.parse(bodyText) : {}; + const indices = body.indices as number[] | undefined; + + if (!Array.isArray(indices)) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: "Missing or invalid 'indices' field (expected number[]).", + }), + ); + return; + } + + const state = deps.store.getState(); + const baseUrlsList: string[] = state.baseUrlsList || []; + const disabledProviders: string[] = [ + ...(state.disabledProviders || []), + ]; + + const toEnable: string[] = []; + for (const idx of indices) { + if ( + typeof idx === "number" && + idx >= 0 && + idx < baseUrlsList.length + ) { + const baseUrl = baseUrlsList[idx]!; + const pos = disabledProviders.indexOf(baseUrl); + if (pos !== -1) { + disabledProviders.splice(pos, 1); + toEnable.push(baseUrl); + } + } + } + + deps.store.getState().setDisabledProviders(disabledProviders); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + output: { + message: `Enabled ${toEnable.length} provider(s)`, + enabled: toEnable, + }, + }), + ); + } catch (error) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: String(error) })); + } + return; + } + if (req.method === "GET" && url.pathname === "/usage") { try { const usageDriver = deps.usageTrackingDriver; const limit = parseLimit(url.searchParams.get("limit")); const entries = await usageDriver.list({ limit }); const totalEntries = await usageDriver.count(); - const totalSatsCost = ( - await usageDriver.list() - ).reduce((sum, entry) => sum + (entry.satsCost || 0), 0); + const totalSatsCost = (await usageDriver.list()).reduce( + (sum, entry) => sum + (entry.satsCost || 0), + 0, + ); const recentSatsCost = entries.reduce( (sum, entry) => sum + (entry.satsCost || 0), 0, @@ -411,7 +543,7 @@ export function createDaemonRequestHandler(deps: { deps.provider || undefined; - // Convert req.headers to Record and log with PipeLineHeaders tag + // Convert req.headers to Record const incomingHeaders: Record = {}; for (const [key, value] of Object.entries(req.headers)) { if (typeof value === "string") { @@ -420,7 +552,6 @@ export function createDaemonRequestHandler(deps: { incomingHeaders[key] = value[0]!; } } - logger.log(`[PipeLineHeaders] Passing headers to routeRequests: ${JSON.stringify(incomingHeaders)}`); try { await deps.ensureProvidersBootstrapped();