Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 2 additions & 17 deletions market-maker/src/apps/futures/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { HealthCheck } from "../../core/healthcheck.ts";
import { runMakerLoop } from "../../core/runner.ts";
import { serializeError } from "../../core/errSerializer.ts";
import { createFuturesVenue } from "../../adapters/futures/index.ts";
import { sanitiseConfig } from "../../core/config/base.ts";
import { loadFuturesConfig } from "./config.ts";

async function main(): Promise<void> {
Expand Down Expand Up @@ -128,7 +129,7 @@ async function main(): Promise<void> {
const health = new HealthCheck({
port: config.health.port,
appName: "futures-mm",
configSummary: summariseConfig(config),
configSummary: sanitiseConfig(config),
oracle,
inventory,
collateral,
Expand All @@ -155,20 +156,4 @@ async function main(): Promise<void> {
});
}

function summariseConfig(c: ReturnType<typeof loadFuturesConfig>): Record<string, unknown> {
return {
nodeEnv: c.nodeEnv,
commitHash: c.commitHash,
logLevel: c.logLevel,
dryRun: c.dryRun,
network: c.network.name,
venue: { kind: c.venue.kind, address: c.venue.address },
pricing: c.pricing,
sizing: c.sizing,
risk: c.risk,
gas: c.gas,
timing: c.timing,
};
}

main();
19 changes: 2 additions & 17 deletions market-maker/src/apps/perps/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { HealthCheck } from "../../core/healthcheck.ts";
import { runMakerLoop } from "../../core/runner.ts";
import { serializeError } from "../../core/errSerializer.ts";
import { createPerpsVenue } from "../../adapters/perps/index.ts";
import { sanitiseConfig } from "../../core/config/base.ts";
import { loadPerpsConfig } from "./config.ts";

async function main(): Promise<void> {
Expand Down Expand Up @@ -121,7 +122,7 @@ async function main(): Promise<void> {
const health = new HealthCheck({
port: config.health.port,
appName: "perps-mm",
configSummary: summariseConfig(config),
configSummary: sanitiseConfig(config),
oracle,
inventory,
collateral,
Expand All @@ -148,20 +149,4 @@ async function main(): Promise<void> {
});
}

function summariseConfig(c: ReturnType<typeof loadPerpsConfig>): Record<string, unknown> {
return {
nodeEnv: c.nodeEnv,
commitHash: c.commitHash,
logLevel: c.logLevel,
dryRun: c.dryRun,
network: c.network.name,
venue: { kind: c.venue.kind, address: c.venue.address },
pricing: c.pricing,
sizing: c.sizing,
risk: c.risk,
gas: c.gas,
timing: c.timing,
};
}

main();
52 changes: 52 additions & 0 deletions market-maker/src/core/config/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import yaml from "js-yaml";
import { type StringOptions, type TUnsafe, type TSchema, Type } from "@sinclair/typebox";
import Ajv from "ajv";
import addFormats from "ajv-formats";
import type { Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { ConfigError } from "../errors.ts";
import { parseUsd, secondsToMs } from "./units.ts";

Expand Down Expand Up @@ -402,3 +404,53 @@ export function configBigint(value: string, field: string): bigint {
throw new ConfigError(`Invalid bigint value for ${field}: "${value}"`);
}
}

/**
* Returns a deep clone of the full parsed config with secrets redacted, safe
* to expose on the /health endpoint. Specifically:
*
* - Each `wallets[name].privateKey` is replaced with "[REDACTED]" and a
* derived `address` is added so operators can still verify which signer
* is configured.
* - `network.rpcUrl` is masked to origin only — paths/query strings on
* managed RPC providers (Alchemy, Infura, …) usually carry API keys.
*
* Bigints in the parsed config (e.g. risk caps, sizing.baseQuantity) are
* preserved as-is; the caller is expected to JSON.stringify with a replacer
* that handles bigints.
*/
export function sanitiseConfig<
T extends {
wallets: Record<string, { privateKey: Hex }>;
network: { rpcUrl: string };
},
>(config: T): Record<string, unknown> {
const clone = structuredClone(config) as Record<string, unknown>;

const wallets = clone.wallets as Record<string, { privateKey: string; address?: string }>;
for (const [name, wallet] of Object.entries(wallets)) {
let address: string;
try {
address = privateKeyToAccount(wallet.privateKey as Hex).address;
} catch {
address = "[invalid]";
}
wallets[name] = { privateKey: "[REDACTED]", address };
}

const network = clone.network as { rpcUrl: string };
network.rpcUrl = maskRpcUrl(network.rpcUrl);

return clone;
}

function maskRpcUrl(raw: string): string {
try {
const url = new URL(raw);
const hasPath = url.pathname && url.pathname !== "/";
const hasQuery = url.search.length > 0;
return hasPath || hasQuery ? `${url.protocol}//${url.host}/[redacted]` : `${url.protocol}//${url.host}`;
} catch {
return "[invalid url]";
}
}
134 changes: 79 additions & 55 deletions market-maker/src/core/healthcheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ export interface ExecutorStats {
export interface HealthCheckOptions {
port: number;
appName: string;
/** Sanitised, JSON-safe config slice for /health output. */
/**
* Full parsed config with secrets redacted (private keys, RPC API keys),
* surfaced verbatim under `config` in /health output. Build via
* `sanitiseConfig` from `core/config/base.ts`. Bigints are serialised to
* strings by the /health JSON.stringify replacer.
*/
configSummary: Record<string, unknown>;
oracle: OracleTracker;
inventory: InventoryManager;
Expand Down Expand Up @@ -97,63 +102,72 @@ export class HealthCheck {

private handleHealth(res: ServerResponse): void {
const { oracle, inventory, collateral, book, gas, risk } = this.opts;
const body = JSON.stringify({
app: this.opts.appName,
status: this.status,
walletAddress: this.walletAddress,
lastError: this.lastError,
uptimeSeconds: Math.floor((Date.now() - this.startedAt) / 1000),
config: this.opts.configSummary,
market: {
oraclePrice: oracle.currentPrice.toString(),
volatility: fractionToNumber(oracle.volatility),
bestBid: book.bestBid.toString(),
bestAsk: book.bestAsk.toString(),
ownOrders: book.ownOrders.size,
const body = JSON.stringify(
{
app: this.opts.appName,
status: this.status,
walletAddress: this.walletAddress,
lastError: this.lastError,
uptimeSeconds: Math.floor((Date.now() - this.startedAt) / 1000),
config: this.opts.configSummary,
market: {
oraclePrice: oracle.currentPrice.toString(),
volatility: fractionToNumber(oracle.volatility),
bestBid: book.bestBid.toString(),
bestAsk: book.bestAsk.toString(),
ownOrders: book.ownOrders.size,
},
inventory: {
netPosition: inventory.netQuantity.toString(),
inventorySkew: fractionToNumber(inventory.inventorySkew),
},
collateral: {
vaultBalance: collateral.vaultBalance.toString(),
portfolioIM: collateral.portfolioIM.toString(),
portfolioMM: collateral.portfolioMM.toString(),
venueOrderMargin: collateral.venueOrderMargin.toString(),
venueUnrealizedPnl: collateral.venueUnrealizedPnl.toString(),
walletTokenBalance: collateral.walletTokenBalance.toString(),
nativeBalance: collateral.nativeBalance.toString(),
utilizationPct: collateral.utilizationPct,
},
gas: {
gasGwei: (Number(gas.currentGasPrice) / 1e9).toFixed(2),
gasSpiking: gas.isGasSpiking,
gasSpikePct: fractionToNumber(gas.gasSpikePct).toFixed(0),
},
risk: {
throttled: risk.throttled,
throttleReason: risk.throttleReason,
cumulativeGasCostUsd: risk.cumulativeGasCostUsd.toString(),
},
stats: {
tickCount: this.tickCount,
lastTickAt: this.lastTickAt,
ordersPlaced: this.executorStats?.ordersPlaced ?? 0,
ordersCancelled: this.executorStats?.ordersCancelled ?? 0,
reconcileCount: this.executorStats?.reconcileCount ?? 0,
},
},
inventory: {
netPosition: inventory.netQuantity.toString(),
inventorySkew: fractionToNumber(inventory.inventorySkew),
},
collateral: {
vaultBalance: collateral.vaultBalance.toString(),
portfolioIM: collateral.portfolioIM.toString(),
portfolioMM: collateral.portfolioMM.toString(),
venueOrderMargin: collateral.venueOrderMargin.toString(),
venueUnrealizedPnl: collateral.venueUnrealizedPnl.toString(),
walletTokenBalance: collateral.walletTokenBalance.toString(),
nativeBalance: collateral.nativeBalance.toString(),
utilizationPct: collateral.utilizationPct,
},
gas: {
gasGwei: (Number(gas.currentGasPrice) / 1e9).toFixed(2),
gasSpiking: gas.isGasSpiking,
gasSpikePct: fractionToNumber(gas.gasSpikePct).toFixed(0),
},
risk: {
throttled: risk.throttled,
throttleReason: risk.throttleReason,
cumulativeGasCostUsd: risk.cumulativeGasCostUsd.toString(),
},
stats: {
tickCount: this.tickCount,
lastTickAt: this.lastTickAt,
ordersPlaced: this.executorStats?.ordersPlaced ?? 0,
ordersCancelled: this.executorStats?.ordersCancelled ?? 0,
reconcileCount: this.executorStats?.reconcileCount ?? 0,
},
});
bigIntReplacer,
);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(body);
}

private handleStop(res: ServerResponse): void {
if (this.paused) return this.respondOk(res);
if (this.paused) {
this.respondOk(res);
return;
}
this.paused = true;
this.status = "stopped";
this.lastError = null;

if (!this.onStop) return this.respondOk(res);
if (!this.onStop) {
this.respondOk(res);
return;
}
this.onStop()
.then(() => this.respondOk(res))
.catch((err) => {
Expand All @@ -163,13 +177,24 @@ export class HealthCheck {
});
}

private respondOk(res: ServerResponse): void {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, status: this.status }));
}

private handleStart(res: ServerResponse): void {
if (!this.paused) return this.respondOk(res);
if (!this.paused) {
this.respondOk(res);
return;
}
this.paused = false;
this.status = "running";
this.lastError = null;

if (!this.onStart) return this.respondOk(res);
if (!this.onStart) {
this.respondOk(res);
return;
}
this.onStart()
.then(() => this.respondOk(res))
.catch((err) => {
Expand All @@ -178,14 +203,13 @@ export class HealthCheck {
res.end(JSON.stringify({ ok: false, error: "start callback failed" }));
});
}

private respondOk(res: ServerResponse): void {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, status: this.status }));
}
}

function fractionToNumber(value: Fraction): number {
// diagnostic only — never used in trading math
return (Number(value.s) * Number(value.n)) / Number(value.d);
}

function bigIntReplacer(_key: string, value: unknown): unknown {
return typeof value === "bigint" ? value.toString() : value;
}
Loading