Files
ritmex-bot/src/exchanges/resolve-from-env.ts
T

249 lines
9.5 KiB
TypeScript

import type { ExchangeAdapter } from "./adapter";
import { createExchangeAdapter, resolveExchangeId, type SupportedExchangeId } from "./create-adapter";
import type { AsterCredentials } from "./aster/adapter";
import type { LighterCredentials } from "./lighter/adapter";
import type { BackpackCredentials } from "./backpack/adapter";
import type { ParadexCredentials } from "./paradex/adapter";
import type { NadoCredentials } from "./nado/adapter";
import type { StandxCredentials } from "./standx/adapter";
import type { BinanceCredentials } from "./binance/adapter";
import type { OndoperpsCredentials } from "./ondoperps/adapter";
import { t } from "../i18n";
import type { Address } from "viem";
interface BuildAdapterOptions {
symbol: string;
exchangeId?: string | SupportedExchangeId;
}
export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapter {
const id = resolveExchangeId(options.exchangeId);
const symbol = options.symbol;
switch (id) {
case "aster": {
const credentials = resolveAsterCredentials();
return createExchangeAdapter({ exchange: id, symbol, aster: credentials });
}
case "grvt":
return createExchangeAdapter({ exchange: id, symbol, grvt: { symbol } });
case "lighter": {
const credentials = resolveLighterCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, lighter: credentials });
}
case "backpack": {
const credentials = resolveBackpackCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, backpack: credentials });
}
case "paradex": {
const credentials = resolveParadexCredentials();
return createExchangeAdapter({ exchange: id, symbol, paradex: credentials });
}
case "nado": {
const credentials = resolveNadoCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, nado: credentials });
}
case "standx": {
const credentials = resolveStandxCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, standx: credentials });
}
case "binance": {
const credentials = resolveBinanceCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, binance: credentials });
}
case "ondoperps": {
const credentials = resolveOndoperpsCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, ondoperps: credentials });
}
}
}
function resolveAsterCredentials(): AsterCredentials {
const apiKey = process.env.ASTER_API_KEY;
const apiSecret = process.env.ASTER_API_SECRET;
if (!apiKey || !apiSecret) {
throw new Error(t("env.missingAster"));
}
return { apiKey, apiSecret };
}
function resolveLighterCredentials(symbol: string): LighterCredentials {
const accountIndexRaw = process.env.LIGHTER_ACCOUNT_INDEX;
const apiPrivateKey = process.env.LIGHTER_API_PRIVATE_KEY;
if (!accountIndexRaw || !apiPrivateKey) {
throw new Error(t("env.missingLighter"));
}
const accountIndex = Number(accountIndexRaw);
if (!Number.isInteger(accountIndex)) {
throw new Error(t("env.lighterIndexInteger"));
}
const credentials: LighterCredentials = {
displaySymbol: symbol,
accountIndex,
apiPrivateKey,
apiKeyIndex: process.env.LIGHTER_API_KEY_INDEX ? Number(process.env.LIGHTER_API_KEY_INDEX) : 0,
environment: process.env.LIGHTER_ENV,
baseUrl: process.env.LIGHTER_BASE_URL,
l1Address: process.env.LIGHTER_L1_ADDRESS,
marketSymbol: process.env.LIGHTER_SYMBOL,
marketId: process.env.LIGHTER_MARKET_ID ? Number(process.env.LIGHTER_MARKET_ID) : undefined,
priceDecimals: process.env.LIGHTER_PRICE_DECIMALS ? Number(process.env.LIGHTER_PRICE_DECIMALS) : undefined,
sizeDecimals: process.env.LIGHTER_SIZE_DECIMALS ? Number(process.env.LIGHTER_SIZE_DECIMALS) : undefined,
};
return credentials;
}
function resolveBackpackCredentials(symbol: string): BackpackCredentials {
const apiKey = process.env.BACKPACK_API_KEY;
const apiSecret = process.env.BACKPACK_API_SECRET;
if (!apiKey || !apiSecret) {
throw new Error(t("env.missingBackpack"));
}
const credentials: BackpackCredentials = {
apiKey,
apiSecret,
password: process.env.BACKPACK_PASSWORD,
subaccount: process.env.BACKPACK_SUBACCOUNT,
symbol: process.env.BACKPACK_SYMBOL ?? symbol,
sandbox: parseOptionalBoolean(process.env.BACKPACK_SANDBOX),
};
return credentials;
}
function resolveParadexCredentials(): ParadexCredentials {
const privateKey = process.env.PARADEX_PRIVATE_KEY;
const walletAddress = process.env.PARADEX_WALLET_ADDRESS;
if (!privateKey || !walletAddress) {
throw new Error(t("env.missingParadex"));
}
if (!isHex32(privateKey)) {
throw new Error(t("env.invalidParadexPrivateKey"));
}
if (!isHexAddress(walletAddress)) {
throw new Error(t("env.invalidParadexAddress"));
}
const credentials: ParadexCredentials = {
privateKey,
walletAddress,
sandbox: parseOptionalBoolean(process.env.PARADEX_SANDBOX),
usePro: parseOptionalBoolean(process.env.PARADEX_USE_PRO),
watchReconnectDelayMs: parseOptionalNumber(process.env.PARADEX_RECONNECT_DELAY_MS),
};
return credentials;
}
function resolveNadoCredentials(symbol: string): NadoCredentials {
const signerPrivateKey = process.env.NADO_SIGNER_PRIVATE_KEY;
const subaccountOwner = process.env.NADO_SUBACCOUNT_OWNER ?? process.env.NADO_EVM_ADDRESS;
if (!signerPrivateKey || !subaccountOwner) {
throw new Error(t("env.missingNado"));
}
if (!isHex32(signerPrivateKey)) {
throw new Error(t("env.invalidNadoPrivateKey"));
}
if (!isHexAddress(subaccountOwner)) {
throw new Error(t("env.invalidNadoAddress"));
}
const credentials: NadoCredentials = {
symbol: process.env.NADO_SYMBOL ?? symbol,
signerPrivateKey,
subaccountOwner: subaccountOwner as Address,
subaccountName: process.env.NADO_SUBACCOUNT_NAME ?? undefined,
env: process.env.NADO_ENV as any,
gatewayWsUrl: process.env.NADO_GATEWAY_WS_URL ?? undefined,
subscriptionsWsUrl: process.env.NADO_SUBSCRIPTIONS_WS_URL ?? undefined,
archiveUrl: process.env.NADO_ARCHIVE_URL ?? undefined,
triggerUrl: process.env.NADO_TRIGGER_URL ?? undefined,
marketSlippagePct: parseOptionalNumber(process.env.NADO_MARKET_SLIPPAGE_PCT),
stopTriggerSource: process.env.NADO_STOP_TRIGGER_SOURCE as any,
};
return credentials;
}
function resolveStandxCredentials(symbol: string): StandxCredentials {
const token = process.env.STANDX_TOKEN;
if (!token) {
throw new Error(t("env.missingStandx"));
}
return {
token,
symbol: process.env.STANDX_SYMBOL ?? symbol,
baseUrl: process.env.STANDX_BASE_URL ?? undefined,
wsUrl: process.env.STANDX_WS_URL ?? undefined,
sessionId: process.env.STANDX_SESSION_ID ?? undefined,
signingKey: process.env.STANDX_REQUEST_PRIVATE_KEY ?? undefined,
};
}
function resolveBinanceCredentials(symbol: string): BinanceCredentials {
const apiKey = process.env.BINANCE_API_KEY;
const apiSecret = process.env.BINANCE_API_SECRET;
if (!apiKey || !apiSecret) {
throw new Error("Missing BINANCE_API_KEY or BINANCE_API_SECRET environment variable");
}
const marketTypeRaw = process.env.BINANCE_MARKET_TYPE?.trim().toLowerCase();
const marketType: BinanceCredentials["marketType"] =
marketTypeRaw === "spot" ? "spot" : marketTypeRaw === "auto" ? "auto" : "perp";
return {
apiKey,
apiSecret,
symbol: process.env.BINANCE_SYMBOL ?? symbol,
marketType,
sandbox: parseOptionalBoolean(process.env.BINANCE_SANDBOX),
spotRestUrl: process.env.BINANCE_SPOT_REST_URL ?? undefined,
futuresRestUrl: process.env.BINANCE_FUTURES_REST_URL ?? undefined,
spotWsUrl: process.env.BINANCE_SPOT_WS_URL ?? undefined,
futuresWsUrl: process.env.BINANCE_FUTURES_WS_URL ?? undefined,
};
}
function resolveOndoperpsCredentials(symbol: string): OndoperpsCredentials {
const apiKeyId = process.env.ONDOPERPS_API_KEY_ID ?? process.env.ONDOPERP_API_KEY_ID ?? process.env.ONDO_KEY_ID;
const apiSecret = process.env.ONDOPERPS_API_SECRET ?? process.env.ONDOPERP_API_SECRET ?? process.env.ONDO_API_SECRET;
if (!apiKeyId || !apiSecret) {
throw new Error(t("env.missingOndoperps"));
}
const sandbox = parseOptionalBoolean(process.env.ONDOPERPS_SANDBOX ?? process.env.ONDOPERP_SANDBOX) === true;
return {
apiKeyId,
apiSecret,
symbol: process.env.ONDOPERPS_SYMBOL ?? process.env.ONDOPERP_SYMBOL ?? symbol,
baseUrl: process.env.ONDOPERPS_BASE_URL ?? process.env.ONDOPERP_BASE_URL ?? (sandbox ? "https://api.ondoperps-sandbox.xyz" : undefined),
wsUrl: process.env.ONDOPERPS_WS_URL ?? process.env.ONDOPERP_WS_URL ?? (sandbox ? "wss://api.ondoperps-sandbox.xyz/ws" : undefined),
builderCode: process.env.ONDOPERPS_BUILDER_CODE ?? process.env.ONDOPERP_BUILDER_CODE ?? undefined,
builderFeeRateBps: parseOptionalNumber(
process.env.ONDOPERPS_BUILDER_FEE_RATE_BPS ?? process.env.ONDOPERP_BUILDER_FEE_RATE_BPS,
),
};
}
function isHex32(value: string): boolean {
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
}
function isHexAddress(value: string): boolean {
return /^0x[0-9a-fA-F]{40}$/.test(value.trim());
}
function parseOptionalBoolean(value: string | undefined): boolean | undefined {
if (value == null) return undefined;
const normalized = value.trim().toLowerCase();
if (!normalized) return undefined;
if (["false", "0", "no", "off"].includes(normalized)) return false;
return true;
}
function parseOptionalNumber(value: string | undefined): number | undefined {
if (!value) return undefined;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}