This commit is contained in:
discountry
2025-10-03 15:28:16 +08:00
parent d5c4b04746
commit fcb83bd16b
925 changed files with 165721 additions and 4 deletions
+3 -3
View File
@@ -4,7 +4,7 @@ export interface CliOptions {
strategy?: StrategyId;
silent: boolean;
help: boolean;
exchange?: "aster" | "grvt" | "lighter";
exchange?: "aster" | "grvt" | "lighter" | "backpack";
}
const STRATEGY_VALUES = new Set<StrategyId>(["trend", "maker", "offset-maker"]);
@@ -68,7 +68,7 @@ function assignStrategy(options: CliOptions, raw: string): void {
function assignExchange(options: CliOptions, raw: string): void {
const normalized = raw.trim().toLowerCase();
if (!normalized) return;
if (normalized === "aster" || normalized === "grvt" || normalized === "lighter") {
if (normalized === "aster" || normalized === "grvt" || normalized === "lighter" || normalized === "backpack") {
options.exchange = normalized as CliOptions["exchange"];
} else if (normalized === "gravity" || normalized === "grav" || normalized === "grv") {
options.exchange = "grvt";
@@ -77,7 +77,7 @@ function assignExchange(options: CliOptions, raw: string): void {
export function printCliHelp(): void {
// eslint-disable-next-line no-console
console.log(`Usage: bun run index.ts [--strategy <trend|maker|offset-maker>] [--exchange <aster|grvt|lighter>] [--silent]\n\n` +
console.log(`Usage: bun run index.ts [--strategy <trend|maker|offset-maker>] [--exchange <aster|grvt|lighter|backpack>] [--silent]\n\n` +
`Options:\n` +
` --strategy, -s Automatically start the specified strategy without the interactive menu.\n` +
` Aliases: offset, offset-maker for the offset maker engine.\n` +
+15
View File
@@ -1,3 +1,18 @@
/**
* Trading Configuration
*
* Environment Variables for Backpack Exchange:
* - BACKPACK_API_KEY: Required API key for Backpack
* - BACKPACK_API_SECRET: Required API secret for Backpack
* - BACKPACK_PASSWORD: Optional password for Backpack (if required)
* - BACKPACK_SUBACCOUNT: Optional subaccount name
* - BACKPACK_SYMBOL: Override symbol (defaults to TRADE_SYMBOL)
* - BACKPACK_SANDBOX: Set to "true" for sandbox mode
* - BACKPACK_DEBUG: Set to "true" for debug logging
*
* Usage: Set EXCHANGE=backpack to use Backpack exchange
*/
export interface TradingConfig {
symbol: string;
tradeAmount: number;
+175
View File
@@ -0,0 +1,175 @@
import type {
AccountListener,
DepthListener,
ExchangeAdapter,
KlineListener,
OrderListener,
TickerListener,
} from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors";
import { BackpackGateway, type BackpackGatewayOptions } from "./gateway";
export interface BackpackCredentials {
apiKey?: string;
apiSecret?: string;
password?: string;
subaccount?: string;
symbol?: string;
sandbox?: boolean;
}
export class BackpackExchangeAdapter implements ExchangeAdapter {
readonly id = "backpack";
private readonly gateway: BackpackGateway;
private readonly symbol: string;
private initPromise: Promise<void> | null = null;
private readonly initContexts = new Set<string>();
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
private lastInitErrorAt = 0;
constructor(credentials: BackpackCredentials = {}) {
const apiKey = credentials.apiKey ?? process.env.BACKPACK_API_KEY;
const apiSecret = credentials.apiSecret ?? process.env.BACKPACK_API_SECRET;
const password = credentials.password ?? process.env.BACKPACK_PASSWORD;
const subaccount = credentials.subaccount ?? process.env.BACKPACK_SUBACCOUNT;
const sandbox = credentials.sandbox ?? (process.env.BACKPACK_SANDBOX === "true");
const symbol = credentials.symbol ?? process.env.BACKPACK_SYMBOL ?? process.env.TRADE_SYMBOL ?? "BTCUSDC";
if (!apiKey || !apiSecret) {
throw new Error("BACKPACK_API_KEY and BACKPACK_API_SECRET environment variables are required");
}
const gatewayOptions: BackpackGatewayOptions = {
apiKey,
apiSecret,
password,
subaccount,
symbol,
sandbox,
logger: (context, error) => this.logError(context, error),
};
this.gateway = new BackpackGateway(gatewayOptions);
this.symbol = symbol;
}
supportsTrailingStops(): boolean {
return false; // TODO: Check if Backpack supports trailing stops via ccxt
}
watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
}
watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
}
watchDepth(_symbol: string, cb: DepthListener): void {
void this.ensureInitialized("watchDepth");
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
}
watchTicker(_symbol: string, cb: TickerListener): void {
void this.ensureInitialized("watchTicker");
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
}
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized(`watchKlines:${interval}`);
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder({ orderId: params.orderId });
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders({ orderIdList: params.orderIdList });
}
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders();
}
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[BackpackExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway
.ensureInitialized(this.symbol)
.then((value) => {
this.clearRetry();
return value;
})
.catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[BackpackExchangeAdapter] ${context} failed`, error);
}
private logError(context: string, error: unknown): void {
if (process.env.BACKPACK_DEBUG === "1" || process.env.BACKPACK_DEBUG === "true") {
console.error(`[BackpackExchangeAdapter] ${context} failed: ${extractMessage(error)}`);
}
}
}
+504
View File
@@ -0,0 +1,504 @@
import ccxt, { type Balances, type Order as CcxtOrder, type OrderBook as CcxtOrderBook, type Ticker as CcxtTicker } from "ccxt";
import type {
AsterAccountSnapshot,
AsterOrder,
AsterDepth,
AsterTicker,
AsterKline,
CreateOrderParams,
OrderType,
} from "../types";
import type {
AccountListener,
OrderListener,
DepthListener,
TickerListener,
KlineListener,
} from "../adapter";
export interface BackpackGatewayOptions {
apiKey?: string;
apiSecret?: string;
password?: string;
subaccount?: string;
symbol: string;
sandbox?: boolean;
logger?: (context: string, error: unknown) => void;
}
export class BackpackGateway {
private readonly exchange: any;
private readonly symbol: string;
private marketSymbol: string;
private readonly logger: (context: string, error: unknown) => void;
private initialized = false;
private initPromise: Promise<void> | null = null;
// Event listeners
private accountListeners = new Set<AccountListener>();
private orderListeners = new Set<OrderListener>();
private depthListeners = new Set<DepthListener>();
private tickerListeners = new Set<TickerListener>();
private klineListeners = new Set<{ interval: string; callback: KlineListener }>();
// Polling intervals
private accountPollTimer: NodeJS.Timeout | null = null;
private orderPollTimer: NodeJS.Timeout | null = null;
private depthPollTimer: NodeJS.Timeout | null = null;
private tickerPollTimer: NodeJS.Timeout | null = null;
private klinePollTimers = new Map<string, NodeJS.Timeout>();
// WebSocket streams
private wsOrderBook: any = null;
private wsTicker: any = null;
private wsKlines = new Map<string, any>();
private wsOrders: any = null;
private wsBalance: any = null;
constructor(options: BackpackGatewayOptions) {
this.symbol = options.symbol.toUpperCase();
this.marketSymbol = this.symbol;
this.logger = options.logger ?? ((context, error) => console.error(`[BackpackGateway] ${context}:`, error));
// dynamic constructor for specific exchange
this.exchange = new (ccxt as any).backpack({
apiKey: options.apiKey,
secret: options.apiSecret,
password: options.password,
subaccount: options.subaccount,
sandbox: options.sandbox ?? false,
enableRateLimit: true,
timeout: 30000,
});
}
async ensureInitialized(symbol?: string): Promise<void> {
if (this.initialized) return;
if (this.initPromise) return this.initPromise;
this.initPromise = this.doInitialize(symbol);
return this.initPromise;
}
private async doInitialize(symbol?: string): Promise<void> {
try {
await this.exchange.loadMarkets();
// Verify symbol exists
const requested = (symbol ?? this.symbol).toUpperCase();
const resolved = this.resolveMarketSymbol(requested);
if (!resolved) {
throw new Error(`Symbol ${requested} not found in Backpack markets`);
}
this.marketSymbol = resolved;
this.initialized = true;
this.logger("initialize", `Backpack gateway initialized for ${this.marketSymbol}`);
} catch (error) {
this.logger("initialize", error);
throw error;
}
}
private resolveMarketSymbol(requested: string): string | null {
// normalize helpers (strip non-alphanumerics for robust comparisons)
const strip = (v: string | undefined | null) => (v ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
// Backpack uses USDC quote; accept common USD/USDT aliases in user input
const normalizeUsdAlias = (v: string) => {
const up = v.toUpperCase();
// Replace ...USD... or ...USDT... (optionally before _ or PERP or end) with USDC
// Examples: BTCUSDPERP -> BTCUSDCPERP, BTCUSD -> BTCUSDC, BTC_USDT_PERP -> BTC_USDC_PERP
return up
.replace(/USDT(?=(?:[_-]?PERP)?$)/, "USDC")
.replace(/USD(?=(?:[_-]?PERP)?$)/, "USDC");
};
const requestedWithUsdc = normalizeUsdAlias(requested);
const compactRequested = strip(requestedWithUsdc);
// 1) exact key in markets (e.g. "BTC/USDC" or "BTC/USDC:USDC")
if (this.exchange.markets[requestedWithUsdc]) return requestedWithUsdc;
// 2) direct markets_by_id lookup by exact id
const byId = (this.exchange as any).markets_by_id ?? {};
if (byId[requestedWithUsdc]) return byId[requestedWithUsdc].symbol;
// 3) flexible lookup: compare compacted forms against ids, symbols, and base+quote
const markets = Object.values(this.exchange.markets) as Array<any>;
for (const m of markets) {
const idCompact = strip(m.id as string);
const symbolCompact = strip(m.symbol as string);
const baseQuoteCompact = strip((m.base as string) + (m.quote as string));
if (idCompact === compactRequested) return m.symbol;
if (symbolCompact === compactRequested) return m.symbol;
if (baseQuoteCompact === compactRequested) return m.symbol;
}
// 4) try matching against markets_by_id keys by compacted form
for (const key of Object.keys(byId)) {
if (strip(key) === compactRequested) return byId[key].symbol;
}
return null;
}
private normalizeTimeframe(interval: string): string {
const timeframeMap: Record<string, string> = {
"1m": "1m",
"5m": "5m",
"15m": "15m",
"1h": "1h",
"4h": "4h",
"1d": "1d",
};
return timeframeMap[interval] || "1m";
}
// Event subscription methods
onAccount(callback: AccountListener): void {
this.accountListeners.add(callback);
this.startAccountPolling();
}
onOrders(callback: OrderListener): void {
this.orderListeners.add(callback);
this.startOrderPolling();
}
onDepth(callback: DepthListener): void {
this.depthListeners.add(callback);
this.startDepthPolling();
}
onTicker(callback: TickerListener): void {
this.tickerListeners.add(callback);
this.startTickerPolling();
}
watchKlines(interval: string, callback: KlineListener): void {
const normalizedInterval = this.normalizeTimeframe(interval);
this.klineListeners.add({ interval: normalizedInterval, callback });
this.startKlinePolling(normalizedInterval);
}
// Polling implementations
private startAccountPolling(): void {
if (this.accountPollTimer) return;
const poll = async () => {
try {
const balance = await this.exchange.fetchBalance();
const accountSnapshot = this.mapBalanceToAccountSnapshot(balance);
for (const listener of this.accountListeners) {
listener(accountSnapshot);
}
} catch (error) {
this.logger("accountPoll", error);
}
};
poll(); // Initial fetch
this.accountPollTimer = setInterval(poll, 5000); // Poll every 5 seconds
}
private startOrderPolling(): void {
if (this.orderPollTimer) return;
const poll = async () => {
try {
const [openOrders, closedOrders] = await Promise.all([
this.exchange.fetchOpenOrders(this.marketSymbol),
this.exchange.fetchClosedOrders(this.marketSymbol, undefined, 50), // Last 50 closed orders
]);
const allOrders = [...openOrders, ...closedOrders];
const mappedOrders = allOrders.map(order => this.mapOrderToAsterOrder(order));
for (const listener of this.orderListeners) {
listener(mappedOrders);
}
} catch (error) {
this.logger("orderPoll", error);
}
};
poll(); // Initial fetch
this.orderPollTimer = setInterval(poll, 2000); // Poll every 2 seconds
}
private startDepthPolling(): void {
if (this.depthPollTimer) return;
const poll = async () => {
try {
const orderbook = await this.exchange.fetchOrderBook(this.marketSymbol, 20);
const depth = this.mapOrderBookToDepth(orderbook);
for (const listener of this.depthListeners) {
listener(depth);
}
} catch (error) {
this.logger("depthPoll", error);
}
};
poll(); // Initial fetch
this.depthPollTimer = setInterval(poll, 1000); // Poll every 1 second
}
private startTickerPolling(): void {
if (this.tickerPollTimer) return;
const poll = async () => {
try {
const ticker = await this.exchange.fetchTicker(this.marketSymbol);
const asterTicker = this.mapTickerToAsterTicker(ticker);
for (const listener of this.tickerListeners) {
listener(asterTicker);
}
} catch (error) {
this.logger("tickerPoll", error);
}
};
poll(); // Initial fetch
this.tickerPollTimer = setInterval(poll, 2000); // Poll every 2 seconds
}
private startKlinePolling(interval: string): void {
if (this.klinePollTimers.has(interval)) return;
const poll = async () => {
try {
const ohlcv = await this.exchange.fetchOHLCV(this.marketSymbol, interval, undefined, 100);
const klines = (ohlcv as number[][])
.filter((c) => Array.isArray(c) && c.length >= 6)
.map((c) => this.mapOHLCVToKline([c[0], c[1], c[2], c[3], c[4], c[5]] as [number, number, number, number, number, number], interval));
for (const listener of this.klineListeners) {
if (listener.interval === interval) {
listener.callback(klines);
}
}
} catch (error) {
this.logger("klinePoll", error);
}
};
poll(); // Initial fetch
this.klinePollTimers.set(interval, setInterval(poll, 5000)); // Poll every 5 seconds
}
// Order management
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized();
// Only pass exchange-specific params in the last argument so we don't
// override ccxt's internal request mapping (e.g. side mapping for Backpack).
const symbol = this.marketSymbol;
const type = this.mapOrderTypeToCcxt(params.type);
const side = params.side.toLowerCase();
const amount = params.quantity;
const price = params.price;
const extraParams: Record<string, unknown> = {};
if (params.stopPrice !== undefined) extraParams.stopPrice = params.stopPrice;
// Map GTX (post-only) to Backpack's postOnly boolean and use GTC as TIF
if (params.timeInForce === "GTX") {
extraParams.postOnly = true;
extraParams.timeInForce = "GTC";
} else if (params.timeInForce !== undefined) {
extraParams.timeInForce = params.timeInForce; // GTC, IOC, FOK
}
// Reduce-only string boolean -> boolean per OpenAPI
if (params.reduceOnly !== undefined) {
extraParams.reduceOnly = params.reduceOnly === "true";
}
const order = await this.exchange.createOrder(
symbol,
type,
side,
amount,
price,
extraParams
);
return this.mapOrderToAsterOrder(order);
}
async cancelOrder(params: { orderId: number | string }): Promise<void> {
await this.exchange.cancelOrder(params.orderId.toString(), this.marketSymbol);
}
async cancelOrders(params: { orderIdList: Array<number | string> }): Promise<void> {
await Promise.all(
params.orderIdList.map(orderId =>
this.exchange.cancelOrder(orderId.toString(), this.marketSymbol)
)
);
}
async cancelAllOrders(): Promise<void> {
try {
if (typeof (this.exchange as any).cancelAllOrders === "function") {
await (this.exchange as any).cancelAllOrders(this.marketSymbol);
return;
}
} catch {
// fall through to manual cancel
}
const open = await this.exchange.fetchOpenOrders(this.marketSymbol);
for (const o of open) {
await this.exchange.cancelOrder(o.id as string, this.marketSymbol);
}
}
// Mapping functions
private mapBalanceToAccountSnapshot(balance: Balances): AsterAccountSnapshot {
const positions: any[] = []; // Backpack is spot-only, no positions
const assets: any[] = [];
for (const [currency, amount] of Object.entries(balance)) {
if (typeof amount === 'object' && amount !== null) {
assets.push({
asset: currency,
walletBalance: amount.total?.toString() || "0",
availableBalance: amount.free?.toString() || "0",
updateTime: Date.now(),
});
}
}
return {
canTrade: true,
canDeposit: true,
canWithdraw: true,
updateTime: Date.now(),
totalWalletBalance: balance.total?.toString() || "0",
totalUnrealizedProfit: "0",
positions,
assets,
};
}
private mapOrderToAsterOrder(order: CcxtOrder): AsterOrder {
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
const mappedType = this.mapCcxtOrderTypeToAster(order.type);
return {
orderId: String(order.id ?? ""),
clientOrderId: (order.clientOrderId as any as string) || "",
symbol: order.symbol || this.marketSymbol,
side,
type: mappedType,
status: (order.status as any as string) || "",
price: order.price?.toString() || "0",
origQty: order.amount?.toString() || "0",
executedQty: order.filled?.toString() || "0",
stopPrice: order.stopPrice?.toString() || "0",
time: order.timestamp || Date.now(),
updateTime: order.lastUpdateTimestamp || Date.now(),
reduceOnly: false,
closePosition: false,
avgPrice: order.average?.toString(),
cumQuote: order.cost?.toString(),
};
}
private mapOrderBookToDepth(orderbook: CcxtOrderBook): AsterDepth {
return {
lastUpdateId: orderbook.nonce || Date.now(),
bids: (orderbook.bids || []).filter((t) => t && t.length >= 2).map(([price, amount]) => [String(price ?? 0), String(amount ?? 0)]),
asks: (orderbook.asks || []).filter((t) => t && t.length >= 2).map(([price, amount]) => [String(price ?? 0), String(amount ?? 0)]),
eventTime: orderbook.timestamp,
};
}
private mapTickerToAsterTicker(ticker: CcxtTicker): AsterTicker {
return {
symbol: ticker.symbol,
lastPrice: ticker.last?.toString() || "0",
openPrice: ticker.open?.toString() || "0",
highPrice: ticker.high?.toString() || "0",
lowPrice: ticker.low?.toString() || "0",
volume: ticker.baseVolume?.toString() || "0",
quoteVolume: ticker.quoteVolume?.toString() || "0",
eventTime: ticker.timestamp,
};
}
private mapOHLCVToKline(candle: [number, number, number, number, number, number], interval: string): AsterKline {
const [timestamp, open, high, low, close, volume] = candle;
return {
openTime: timestamp,
closeTime: timestamp + this.getIntervalMs(interval),
open: open.toString(),
high: high.toString(),
low: low.toString(),
close: close.toString(),
volume: volume.toString(),
numberOfTrades: 0,
};
}
private mapOrderTypeToCcxt(type: string): string {
const typeMap: Record<string, string> = {
"LIMIT": "limit",
"MARKET": "market",
"STOP_MARKET": "stop",
"TRAILING_STOP_MARKET": "trailing-stop",
};
return typeMap[type] || "limit";
}
private mapCcxtOrderTypeToAster(type: string | undefined): OrderType {
const typeMap: Record<string, OrderType> = {
"limit": "LIMIT",
"market": "MARKET",
"stop": "STOP_MARKET",
"trailing-stop": "TRAILING_STOP_MARKET",
};
return type ? (typeMap[type] ?? "LIMIT") : "LIMIT";
}
private getIntervalMs(interval: string): number {
const intervalMap: Record<string, number> = {
"1m": 60 * 1000,
"5m": 5 * 60 * 1000,
"15m": 15 * 60 * 1000,
"1h": 60 * 60 * 1000,
"4h": 4 * 60 * 60 * 1000,
"1d": 24 * 60 * 60 * 1000,
};
return intervalMap[interval] || 60 * 1000;
}
// Cleanup
destroy(): void {
if (this.accountPollTimer) {
clearInterval(this.accountPollTimer);
this.accountPollTimer = null;
}
if (this.orderPollTimer) {
clearInterval(this.orderPollTimer);
this.orderPollTimer = null;
}
if (this.depthPollTimer) {
clearInterval(this.depthPollTimer);
this.depthPollTimer = null;
}
if (this.tickerPollTimer) {
clearInterval(this.tickerPollTimer);
this.tickerPollTimer = null;
}
for (const timer of this.klinePollTimers.values()) {
clearInterval(timer);
}
this.klinePollTimers.clear();
}
}
+8 -1
View File
@@ -2,6 +2,7 @@ import type { ExchangeAdapter } from "./adapter";
import { AsterExchangeAdapter, type AsterCredentials } from "./aster-adapter";
import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter";
import { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapter";
import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter";
export interface ExchangeFactoryOptions {
symbol: string;
@@ -9,9 +10,10 @@ export interface ExchangeFactoryOptions {
aster?: AsterCredentials;
grvt?: GrvtCredentials;
lighter?: LighterCredentials;
backpack?: BackpackCredentials;
}
export type SupportedExchangeId = "aster" | "grvt" | "lighter";
export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack";
export function resolveExchangeId(value?: string | null): SupportedExchangeId {
const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster")
@@ -20,12 +22,14 @@ export function resolveExchangeId(value?: string | null): SupportedExchangeId {
.toLowerCase();
if (fallback === "grvt") return "grvt";
if (fallback === "lighter") return "lighter";
if (fallback === "backpack") return "backpack";
return "aster";
}
export function getExchangeDisplayName(id: SupportedExchangeId): string {
if (id === "grvt") return "GRVT";
if (id === "lighter") return "Lighter";
if (id === "backpack") return "Backpack";
return "AsterDex";
}
@@ -37,5 +41,8 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange
if (id === "lighter") {
return new LighterExchangeAdapter({ ...options.lighter, displaySymbol: options.symbol });
}
if (id === "backpack") {
return new BackpackExchangeAdapter({ ...options.backpack, symbol: options.symbol });
}
return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol });
}