mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-12 01:38:07 +00:00
Add Nado documentation and examples, including new API endpoints, FAQs, and guides for using the TypeScript SDK. Update .env.example with additional configuration options.
This commit is contained in:
@@ -27,6 +27,16 @@ export interface KlineListener {
|
||||
(klines: AsterKline[]): void;
|
||||
}
|
||||
|
||||
export interface FundingRateSnapshot {
|
||||
symbol: string;
|
||||
fundingRate: number;
|
||||
updateTime: number;
|
||||
}
|
||||
|
||||
export interface FundingRateListener {
|
||||
(snapshot: FundingRateSnapshot): void;
|
||||
}
|
||||
|
||||
export interface ExchangePrecision {
|
||||
priceTick: number;
|
||||
qtyStep: number;
|
||||
@@ -45,6 +55,7 @@ export interface ExchangeAdapter {
|
||||
watchDepth(symbol: string, cb: DepthListener): void;
|
||||
watchTicker(symbol: string, cb: TickerListener): void;
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void;
|
||||
watchFundingRate?(symbol: string, cb: FundingRateListener): void;
|
||||
createOrder(params: CreateOrderParams): Promise<AsterOrder>;
|
||||
cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void>;
|
||||
cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter";
|
||||
import { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapter";
|
||||
import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter";
|
||||
import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapter";
|
||||
import { NadoExchangeAdapter, type NadoCredentials } from "./nado/adapter";
|
||||
|
||||
export interface ExchangeFactoryOptions {
|
||||
symbol: string;
|
||||
@@ -13,9 +14,10 @@ export interface ExchangeFactoryOptions {
|
||||
lighter?: LighterCredentials;
|
||||
backpack?: BackpackCredentials;
|
||||
paradex?: ParadexCredentials;
|
||||
nado?: NadoCredentials;
|
||||
}
|
||||
|
||||
export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack" | "paradex";
|
||||
export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack" | "paradex" | "nado";
|
||||
|
||||
export function resolveExchangeId(value?: string | null): SupportedExchangeId {
|
||||
const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster")
|
||||
@@ -26,6 +28,7 @@ export function resolveExchangeId(value?: string | null): SupportedExchangeId {
|
||||
if (fallback === "lighter") return "lighter";
|
||||
if (fallback === "backpack") return "backpack";
|
||||
if (fallback === "paradex") return "paradex";
|
||||
if (fallback === "nado") return "nado";
|
||||
return "aster";
|
||||
}
|
||||
|
||||
@@ -34,6 +37,7 @@ export function getExchangeDisplayName(id: SupportedExchangeId): string {
|
||||
if (id === "lighter") return "Lighter";
|
||||
if (id === "backpack") return "Backpack";
|
||||
if (id === "paradex") return "Paradex";
|
||||
if (id === "nado") return "Nado";
|
||||
return "AsterDex";
|
||||
}
|
||||
|
||||
@@ -51,5 +55,8 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange
|
||||
if (id === "paradex") {
|
||||
return new ParadexExchangeAdapter({ ...options.paradex, symbol: options.symbol });
|
||||
}
|
||||
if (id === "nado") {
|
||||
return new NadoExchangeAdapter({ ...options.nado, symbol: options.symbol });
|
||||
}
|
||||
return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { setTimeout, clearTimeout } from "timers";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
ExchangeAdapter,
|
||||
ExchangePrecision,
|
||||
FundingRateListener,
|
||||
KlineListener,
|
||||
OrderListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { NadoGateway, type NadoGatewayOptions } from "./gateway";
|
||||
import type { ChainEnv } from "@nadohq/shared";
|
||||
import type { Address } from "viem";
|
||||
|
||||
export interface NadoCredentials {
|
||||
env?: ChainEnv;
|
||||
symbol?: string;
|
||||
subaccountOwner?: Address;
|
||||
subaccountName?: string;
|
||||
signerPrivateKey?: string;
|
||||
gatewayWsUrl?: string;
|
||||
subscriptionsWsUrl?: string;
|
||||
archiveUrl?: string;
|
||||
triggerUrl?: string;
|
||||
pollIntervals?: NadoGatewayOptions["pollIntervals"];
|
||||
marketSlippagePct?: number;
|
||||
stopTriggerSource?: NadoGatewayOptions["stopTriggerSource"];
|
||||
}
|
||||
|
||||
export class NadoExchangeAdapter implements ExchangeAdapter {
|
||||
readonly id = "nado";
|
||||
|
||||
private readonly gateway: NadoGateway;
|
||||
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: NadoCredentials = {}) {
|
||||
const signerPrivateKey = credentials.signerPrivateKey ?? process.env.NADO_SIGNER_PRIVATE_KEY;
|
||||
const subaccountOwner = (credentials.subaccountOwner ??
|
||||
(process.env.NADO_SUBACCOUNT_OWNER as Address | undefined) ??
|
||||
(process.env.NADO_EVM_ADDRESS as Address | undefined)) as Address | undefined;
|
||||
const symbol =
|
||||
credentials.symbol ??
|
||||
process.env.NADO_SYMBOL ??
|
||||
process.env.TRADE_SYMBOL ??
|
||||
"BTC-PERP";
|
||||
|
||||
if (!signerPrivateKey) {
|
||||
throw new Error("Missing NADO_SIGNER_PRIVATE_KEY environment variable");
|
||||
}
|
||||
if (!subaccountOwner) {
|
||||
throw new Error("Missing NADO_SUBACCOUNT_OWNER (or NADO_EVM_ADDRESS) environment variable");
|
||||
}
|
||||
|
||||
this.symbol = symbol;
|
||||
this.gateway = new NadoGateway({
|
||||
env: credentials.env,
|
||||
symbol,
|
||||
subaccountOwner,
|
||||
subaccountName: credentials.subaccountName ?? process.env.NADO_SUBACCOUNT_NAME ?? "default",
|
||||
signerPrivateKey,
|
||||
gatewayWsUrl: credentials.gatewayWsUrl ?? process.env.NADO_GATEWAY_WS_URL,
|
||||
subscriptionsWsUrl: credentials.subscriptionsWsUrl ?? process.env.NADO_SUBSCRIPTIONS_WS_URL,
|
||||
archiveUrl: credentials.archiveUrl ?? process.env.NADO_ARCHIVE_URL,
|
||||
triggerUrl: credentials.triggerUrl ?? process.env.NADO_TRIGGER_URL,
|
||||
pollIntervals: credentials.pollIntervals,
|
||||
marketSlippagePct:
|
||||
credentials.marketSlippagePct ??
|
||||
(process.env.NADO_MARKET_SLIPPAGE_PCT ? Number(process.env.NADO_MARKET_SLIPPAGE_PCT) : undefined),
|
||||
stopTriggerSource:
|
||||
credentials.stopTriggerSource ??
|
||||
(process.env.NADO_STOP_TRIGGER_SOURCE as NadoGatewayOptions["stopTriggerSource"] | undefined),
|
||||
logger: (context, error) => this.logError(context, error),
|
||||
});
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
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:${symbol}`);
|
||||
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(symbol: string, cb: TickerListener): void {
|
||||
void this.ensureInitialized(`watchTicker:${symbol}`);
|
||||
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
||||
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
||||
void this.ensureInitialized(`watchFundingRate:${symbol}`);
|
||||
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", 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(params);
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders(params);
|
||||
}
|
||||
|
||||
async getPrecision(): Promise<ExchangePrecision | null> {
|
||||
try {
|
||||
return await this.gateway.getPrecision(this.symbol);
|
||||
} catch (error) {
|
||||
this.logError("getPrecision", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||
const wrapped = ((...args: any[]) => {
|
||||
try {
|
||||
cb(...args);
|
||||
} catch (error) {
|
||||
console.error(`[NadoExchangeAdapter] ${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(`[NadoExchangeAdapter] ${context} failed`, error);
|
||||
}
|
||||
|
||||
private logError(context: string, error: unknown): void {
|
||||
const detail = extractMessage(error);
|
||||
const message = `[NadoExchangeAdapter] ${context} failed: ${detail}`;
|
||||
const criticalContexts = ["initialize", "accountPoll", "ordersPoll", "triggerOrdersPoll"];
|
||||
if (criticalContexts.some((prefix) => context.startsWith(prefix)) || process.env.NADO_DEBUG === "1") {
|
||||
console.error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import type {
|
||||
BaseOrderIntent,
|
||||
ClosePositionIntent,
|
||||
LimitOrderIntent,
|
||||
MarketOrderIntent,
|
||||
StopOrderIntent,
|
||||
TrailingStopOrderIntent,
|
||||
} from "../order-schema";
|
||||
import { toStringBoolean } from "../order-schema";
|
||||
|
||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||
if (params.quantity === undefined) {
|
||||
params.quantity = intent.quantity;
|
||||
}
|
||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||
params.timeInForce = intent.timeInForce;
|
||||
}
|
||||
if (intent.reduceOnly !== undefined) {
|
||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||
}
|
||||
if (intent.closePosition !== undefined) {
|
||||
params.closePosition = toStringBoolean(intent.closePosition);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "LIMIT",
|
||||
quantity: intent.quantity,
|
||||
price: intent.price,
|
||||
timeInForce: intent.timeInForce ?? "GTC",
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
|
||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "MARKET",
|
||||
quantity: intent.quantity,
|
||||
timeInForce: intent.timeInForce ?? "IOC",
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
|
||||
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "STOP_MARKET",
|
||||
quantity: intent.quantity,
|
||||
stopPrice: intent.stopPrice,
|
||||
timeInForce: intent.timeInForce ?? "GTC",
|
||||
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
|
||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
|
||||
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
||||
throw new Error("Nado exchange does not support trailing stop orders");
|
||||
}
|
||||
|
||||
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "MARKET",
|
||||
quantity: intent.quantity,
|
||||
reduceOnly: "true",
|
||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
||||
timeInForce: intent.timeInForce ?? "IOC",
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
export type NadoProductType = "spot" | "perp";
|
||||
|
||||
export interface NadoContractsResponse {
|
||||
status: "success" | "failure";
|
||||
data?: {
|
||||
chain_id: string;
|
||||
endpoint_addr: string;
|
||||
};
|
||||
error?: string;
|
||||
error_code?: number;
|
||||
request_type?: string;
|
||||
}
|
||||
|
||||
export interface NadoSymbolsResponse {
|
||||
status: "success" | "failure";
|
||||
data?: {
|
||||
symbols: Record<
|
||||
string,
|
||||
{
|
||||
type: NadoProductType;
|
||||
product_id: number;
|
||||
symbol: string;
|
||||
price_increment_x18: string;
|
||||
size_increment: string;
|
||||
min_size: string;
|
||||
maker_fee_rate_x18?: string;
|
||||
taker_fee_rate_x18?: string;
|
||||
}
|
||||
>;
|
||||
};
|
||||
error?: string;
|
||||
error_code?: number;
|
||||
request_type?: string;
|
||||
}
|
||||
|
||||
export interface NadoSubaccountOrdersResponse {
|
||||
status: "success" | "failure";
|
||||
data?: {
|
||||
sender: string;
|
||||
product_id: number;
|
||||
orders: Array<{
|
||||
product_id: number;
|
||||
sender: string;
|
||||
price_x18: string;
|
||||
amount: string;
|
||||
expiration: string;
|
||||
nonce: string;
|
||||
unfilled_amount: string;
|
||||
digest: string;
|
||||
placed_at: number;
|
||||
appendix?: string;
|
||||
order_type?: string;
|
||||
}>;
|
||||
};
|
||||
error?: string;
|
||||
error_code?: number;
|
||||
request_type?: string;
|
||||
}
|
||||
|
||||
export interface NadoSubaccountInfoResponse {
|
||||
status: "success" | "failure";
|
||||
data?: {
|
||||
subaccount: string;
|
||||
exists: boolean;
|
||||
healths: Array<{ assets: string; liabilities: string; health: string }>;
|
||||
spot_balances: Array<{ product_id: number; balance: { amount: string } }>;
|
||||
perp_balances: Array<{
|
||||
product_id: number;
|
||||
balance: {
|
||||
amount: string;
|
||||
v_quote_balance: string;
|
||||
last_cumulative_funding_x18?: string;
|
||||
};
|
||||
}>;
|
||||
spot_products: Array<{
|
||||
product_id: number;
|
||||
oracle_price_x18: string;
|
||||
book_info?: {
|
||||
size_increment: string;
|
||||
price_increment_x18: string;
|
||||
min_size: string;
|
||||
};
|
||||
}>;
|
||||
perp_products: Array<{
|
||||
product_id: number;
|
||||
oracle_price_x18: string;
|
||||
book_info?: {
|
||||
size_increment: string;
|
||||
price_increment_x18: string;
|
||||
min_size: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
error?: string;
|
||||
error_code?: number;
|
||||
request_type?: string;
|
||||
}
|
||||
|
||||
export interface NadoSubscriptionAck {
|
||||
result: unknown;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface NadoOrderUpdateEvent {
|
||||
type: "order_update";
|
||||
timestamp: string;
|
||||
product_id: number;
|
||||
digest: string;
|
||||
amount: string;
|
||||
reason: "filled" | "cancelled" | "placed";
|
||||
id?: number;
|
||||
}
|
||||
|
||||
export interface NadoPositionChangeEvent {
|
||||
type: "position_change";
|
||||
timestamp: string;
|
||||
product_id: number;
|
||||
subaccount: string;
|
||||
isolated: boolean;
|
||||
amount: string;
|
||||
v_quote_amount: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface NadoBestBidOfferEvent {
|
||||
type: "best_bid_offer";
|
||||
timestamp: string;
|
||||
product_id: number;
|
||||
bid_price: string;
|
||||
bid_qty: string;
|
||||
ask_price: string;
|
||||
ask_qty: string;
|
||||
}
|
||||
|
||||
export interface NadoTradeEvent {
|
||||
type: "trade";
|
||||
timestamp: string;
|
||||
product_id: number;
|
||||
price: string;
|
||||
taker_qty: string;
|
||||
maker_qty: string;
|
||||
is_taker_buyer: boolean;
|
||||
}
|
||||
|
||||
export interface NadoLatestCandlestickEvent {
|
||||
type: "latest_candlestick";
|
||||
timestamp: number;
|
||||
product_id: number;
|
||||
granularity: number;
|
||||
open_x18: string;
|
||||
high_x18: string;
|
||||
low_x18: string;
|
||||
close_x18: string;
|
||||
volume: string;
|
||||
}
|
||||
|
||||
export interface NadoFundingRateEvent {
|
||||
type: "funding_rate";
|
||||
// timestamp when the event was generated, in nanoseconds
|
||||
timestamp: string;
|
||||
product_id: number;
|
||||
// latest 24hr funding rate, multiplied by 1e18
|
||||
funding_rate_x18: string;
|
||||
// epoch time in seconds when the funding rate was updated
|
||||
update_time: string;
|
||||
}
|
||||
@@ -13,8 +13,9 @@ import * as backpackOrders from "./backpack/order";
|
||||
import * as grvtOrders from "./grvt/order";
|
||||
import * as lighterOrders from "./lighter/order";
|
||||
import * as paradexOrders from "./paradex/order";
|
||||
import * as nadoOrders from "./nado/order";
|
||||
|
||||
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex";
|
||||
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex" | "nado";
|
||||
|
||||
interface ExchangeOrderHandlers {
|
||||
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
|
||||
@@ -60,9 +61,16 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
|
||||
trailingStop: paradexOrders.createTrailingStopOrder,
|
||||
close: paradexOrders.createClosePositionOrder,
|
||||
},
|
||||
nado: {
|
||||
limit: nadoOrders.createLimitOrder,
|
||||
market: nadoOrders.createMarketOrder,
|
||||
stop: nadoOrders.createStopOrder,
|
||||
trailingStop: nadoOrders.createTrailingStopOrder,
|
||||
close: nadoOrders.createClosePositionOrder,
|
||||
},
|
||||
};
|
||||
|
||||
const knownExchanges: ExchangeKey[] = ["aster", "backpack", "grvt", "lighter", "paradex"];
|
||||
const knownExchanges: ExchangeKey[] = ["aster", "backpack", "grvt", "lighter", "paradex", "nado"];
|
||||
|
||||
function normalizeExchangeId(value: string | undefined | null): string | undefined {
|
||||
if (!value) return undefined;
|
||||
@@ -115,4 +123,3 @@ export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise
|
||||
export function routeCloseOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
||||
return getHandlers(intent).close(intent);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ 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 { t } from "../i18n";
|
||||
import type { Address } from "viem";
|
||||
|
||||
interface BuildAdapterOptions {
|
||||
symbol: string;
|
||||
@@ -35,6 +37,11 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt
|
||||
return createExchangeAdapter({ exchange: id, symbol, paradex: credentials });
|
||||
}
|
||||
|
||||
if (id === "nado") {
|
||||
const credentials = resolveNadoCredentials(symbol);
|
||||
return createExchangeAdapter({ exchange: id, symbol, nado: credentials });
|
||||
}
|
||||
|
||||
return createExchangeAdapter({ exchange: id, symbol, grvt: { symbol } });
|
||||
}
|
||||
|
||||
@@ -115,6 +122,37 @@ function resolveParadexCredentials(): ParadexCredentials {
|
||||
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 isHex32(value: string): boolean {
|
||||
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user