mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-12 09:48:07 +00:00
- AsterOrder → Order - AsterAccountSnapshot → AccountSnapshot - AsterAccountPosition → AccountPosition - AsterAccountAsset → AccountAsset - AsterDepthLevel → DepthLevel - AsterDepth → Depth - AsterTicker → Ticker - AsterKline → Kline These types are the platform-agnostic contract used by all 8 exchanges, not Aster-specific. Renamed across 63 files.
191 lines
7.0 KiB
TypeScript
191 lines
7.0 KiB
TypeScript
import type {
|
|
AccountListener,
|
|
DepthListener,
|
|
ExchangeAdapter,
|
|
ExchangePrecision,
|
|
KlineListener,
|
|
OrderListener,
|
|
TickerListener,
|
|
} from "../adapter";
|
|
import type { Order, CreateOrderParams } from "../types";
|
|
import { extractMessage } from "../../utils/errors";
|
|
import { LighterGateway, type LighterGatewayOptions } from "./gateway";
|
|
|
|
export interface LighterCredentials {
|
|
displaySymbol?: string;
|
|
marketSymbol?: string;
|
|
symbol?: string; // fallback alias
|
|
accountIndex?: number;
|
|
l1Address?: string;
|
|
apiKeys?: Record<number, string>;
|
|
apiPrivateKey?: string;
|
|
apiKeyIndex?: number;
|
|
environment?: string;
|
|
baseUrl?: string;
|
|
marketId?: number;
|
|
priceDecimals?: number;
|
|
sizeDecimals?: number;
|
|
chainId?: number;
|
|
tickerPollMs?: number;
|
|
klinePollMs?: number;
|
|
}
|
|
|
|
export class LighterExchangeAdapter implements ExchangeAdapter {
|
|
readonly id = "lighter";
|
|
private readonly gateway: LighterGateway;
|
|
private initPromise: Promise<void> | null = null;
|
|
private readonly initContexts = new Set<string>();
|
|
|
|
constructor(credentials: LighterCredentials = {}) {
|
|
const displaySymbolSource = credentials.displaySymbol ?? process.env.TRADE_SYMBOL ?? "";
|
|
if (!displaySymbolSource) {
|
|
throw new Error("TRADE_SYMBOL environment variable is required");
|
|
}
|
|
const displaySymbol = displaySymbolSource;
|
|
const marketSymbolSource = credentials.marketSymbol ?? credentials.symbol ?? process.env.LIGHTER_SYMBOL ?? displaySymbol;
|
|
const marketSymbol = marketSymbolSource.toUpperCase();
|
|
const accountIndex = credentials.accountIndex ?? parseInt(process.env.LIGHTER_ACCOUNT_INDEX ?? "", 10);
|
|
if (!Number.isFinite(accountIndex)) {
|
|
throw new Error("LIGHTER_ACCOUNT_INDEX environment variable is required");
|
|
}
|
|
const apiKeys = resolveApiKeys(credentials);
|
|
const environment = credentials.environment ?? process.env.LIGHTER_ENV;
|
|
const marketId = credentials.marketId ?? (process.env.LIGHTER_MARKET_ID ? Number(process.env.LIGHTER_MARKET_ID) : undefined);
|
|
const priceDecimals = credentials.priceDecimals ?? (process.env.LIGHTER_PRICE_DECIMALS ? Number(process.env.LIGHTER_PRICE_DECIMALS) : undefined);
|
|
const sizeDecimals = credentials.sizeDecimals ?? (process.env.LIGHTER_SIZE_DECIMALS ? Number(process.env.LIGHTER_SIZE_DECIMALS) : undefined);
|
|
const l1Address = credentials.l1Address ?? process.env.LIGHTER_L1_ADDRESS ?? null;
|
|
const gatewayOptions: LighterGatewayOptions = {
|
|
symbol: displaySymbol,
|
|
marketSymbol,
|
|
accountIndex,
|
|
apiKeys,
|
|
baseUrl: credentials.baseUrl ?? process.env.LIGHTER_BASE_URL,
|
|
environment: environment as LighterGatewayOptions["environment"],
|
|
marketId,
|
|
priceDecimals,
|
|
sizeDecimals,
|
|
chainId: credentials.chainId ?? (process.env.LIGHTER_CHAIN_ID ? Number(process.env.LIGHTER_CHAIN_ID) : undefined),
|
|
tickerPollMs: credentials.tickerPollMs,
|
|
klinePollMs: credentials.klinePollMs,
|
|
logger: (context, error) => this.logError(context, error),
|
|
l1Address: l1Address ?? undefined,
|
|
};
|
|
this.gateway = new LighterGateway(gatewayOptions);
|
|
}
|
|
|
|
supportsTrailingStops(): boolean {
|
|
return false;
|
|
}
|
|
|
|
watchAccount(handler: AccountListener): void {
|
|
void this.ensureInitialized("watchAccount");
|
|
this.gateway.onAccount(handler);
|
|
}
|
|
|
|
watchOrders(handler: OrderListener): void {
|
|
void this.ensureInitialized("watchOrders");
|
|
this.gateway.onOrders(handler);
|
|
}
|
|
|
|
watchDepth(_symbol: string, handler: DepthListener): void {
|
|
void this.ensureInitialized("watchDepth");
|
|
this.gateway.onDepth(handler);
|
|
}
|
|
|
|
watchTicker(_symbol: string, handler: TickerListener): void {
|
|
void this.ensureInitialized("watchTicker");
|
|
this.gateway.onTicker(handler);
|
|
}
|
|
|
|
watchKlines(_symbol: string, interval: string, handler: KlineListener): void {
|
|
void this.ensureInitialized(`watchKlines:${interval}`);
|
|
this.gateway.watchKlines(interval, handler);
|
|
}
|
|
|
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
|
await this.ensureInitialized("createOrder");
|
|
return this.gateway.createOrder(params);
|
|
}
|
|
|
|
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
|
await this.ensureInitialized("cancelOrder");
|
|
// Accept both clientOrderId and order_index as strings; forward as-is to preserve precision
|
|
await this.gateway.cancelOrder({ orderId: String(params.orderId) });
|
|
}
|
|
|
|
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
|
await this.ensureInitialized("cancelOrders");
|
|
for (const orderId of params.orderIdList) {
|
|
await this.gateway.cancelOrder({ orderId: String(orderId) });
|
|
}
|
|
}
|
|
|
|
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
|
|
await this.ensureInitialized("cancelAllOrders");
|
|
await this.gateway.cancelAllOrders();
|
|
}
|
|
|
|
async getPrecision(): Promise<ExchangePrecision | null> {
|
|
try {
|
|
const precision = await this.gateway.getPrecision();
|
|
return {
|
|
priceTick: precision.priceTick,
|
|
qtyStep: precision.qtyStep,
|
|
priceDecimals: precision.priceDecimals,
|
|
sizeDecimals: precision.sizeDecimals,
|
|
marketId: precision.marketId ?? undefined,
|
|
};
|
|
} catch (error) {
|
|
this.logError("precision", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private ensureInitialized(context?: string): Promise<void> {
|
|
if (!this.initPromise) {
|
|
this.initContexts.clear();
|
|
this.initPromise = this.gateway.ensureInitialized().catch((error) => {
|
|
this.logError("initialize", error);
|
|
this.initPromise = null;
|
|
throw error;
|
|
});
|
|
}
|
|
if (context && !this.initContexts.has(context)) {
|
|
this.initContexts.add(context);
|
|
this.initPromise.catch((error) => this.logError(context, error));
|
|
}
|
|
return this.initPromise;
|
|
}
|
|
|
|
private logError(context: string, error: unknown): void {
|
|
if (process.env.LIGHTER_DEBUG !== "1" && process.env.LIGHTER_DEBUG !== "true") {
|
|
return;
|
|
}
|
|
if (isSuccessfulResponse(error)) {
|
|
return; // success responses are noisy; ignore unless non-200
|
|
}
|
|
console.error(`[LighterExchangeAdapter] ${context} failed: ${extractMessage(error)}`);
|
|
}
|
|
}
|
|
|
|
function isSuccessfulResponse(value: unknown): value is { code?: number } {
|
|
if (typeof value !== "object" || value == null) return false;
|
|
const code = (value as { code?: unknown }).code;
|
|
return typeof code === "number" && code === 200;
|
|
}
|
|
|
|
function resolveApiKeys(credentials: LighterCredentials): Record<number, string> {
|
|
if (credentials.apiKeys && Object.keys(credentials.apiKeys).length) {
|
|
return credentials.apiKeys;
|
|
}
|
|
const privateKey = credentials.apiPrivateKey ?? process.env.LIGHTER_API_PRIVATE_KEY;
|
|
if (!privateKey) {
|
|
throw new Error("LIGHTER_API_PRIVATE_KEY environment variable is required");
|
|
}
|
|
const apiKeyIndex = credentials.apiKeyIndex ?? (process.env.LIGHTER_API_KEY_INDEX ? Number(process.env.LIGHTER_API_KEY_INDEX) : 0);
|
|
if (!Number.isInteger(apiKeyIndex) || apiKeyIndex < 0) {
|
|
throw new Error("Invalid LIGHTER_API_KEY_INDEX value");
|
|
}
|
|
return { [apiKeyIndex]: privateKey };
|
|
}
|