The Live Trading control stored its mode in localStorage, so the shell could claim live money while the bot was in dry-run. Show the Tradingbot status when TRADING_BOT_URL is set, and an unknown state when it is not. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
import {
|
|
mapTradingBotStatus,
|
|
readBoundedJsonResponse,
|
|
TRADING_BOT_PROXY_PATH,
|
|
TRADING_BOT_STATUS_MAX_BYTES,
|
|
TRADING_BOT_STATUS_TIMEOUT_MS,
|
|
tradingBotStatusUrl,
|
|
} from "@rakazo/core";
|
|
import type { Hono } from "hono";
|
|
|
|
export function mountTradingBotStatus(
|
|
app: Hono,
|
|
options: {
|
|
/** Normalized http(s) origin. Unset leaves the route idle for SaaS builds. */
|
|
baseUrl: string | undefined;
|
|
authorize: (request: Request) => Promise<boolean>;
|
|
fetchImpl?: typeof fetch;
|
|
},
|
|
) {
|
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
app.get(TRADING_BOT_PROXY_PATH, async (c) => {
|
|
c.header("cache-control", "no-store");
|
|
if (!options.baseUrl) return c.json({ configured: false });
|
|
const allowed = await options.authorize(c.req.raw).catch(() => false);
|
|
if (!allowed) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const timeout = linkAbort(c.req.raw.signal, TRADING_BOT_STATUS_TIMEOUT_MS);
|
|
try {
|
|
const response = await fetchImpl(tradingBotStatusUrl(options.baseUrl), {
|
|
method: "GET",
|
|
redirect: "error",
|
|
cache: "no-store",
|
|
signal: timeout.signal,
|
|
headers: { accept: "application/json" },
|
|
});
|
|
if (!response.ok) {
|
|
cancelBody(response);
|
|
return c.json({ configured: true, reachable: false }, 502);
|
|
}
|
|
const body = await readBoundedJsonResponse<unknown>(
|
|
response,
|
|
TRADING_BOT_STATUS_MAX_BYTES,
|
|
timeout.signal,
|
|
);
|
|
const status = mapTradingBotStatus(body);
|
|
return c.json({ configured: true, reachable: true, status: status ?? null });
|
|
} catch {
|
|
return c.json({ configured: true, reachable: false }, 502);
|
|
} finally {
|
|
timeout.cancel();
|
|
}
|
|
});
|
|
}
|
|
|
|
function linkAbort(
|
|
parent: AbortSignal,
|
|
timeoutMs: number,
|
|
): { signal: AbortSignal; cancel: () => void } {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
const onParent = () => controller.abort();
|
|
if (parent.aborted) controller.abort();
|
|
else parent.addEventListener("abort", onParent);
|
|
return {
|
|
signal: controller.signal,
|
|
cancel: () => {
|
|
clearTimeout(timer);
|
|
parent.removeEventListener("abort", onParent);
|
|
},
|
|
};
|
|
}
|
|
|
|
function cancelBody(response: Response): void {
|
|
try {
|
|
void Promise.resolve(response.body?.cancel()).catch(() => undefined);
|
|
} catch {
|
|
// Best-effort. The caller still reports the bot as unreachable.
|
|
}
|
|
}
|