Merge pull request #150 from tiagosiebler/deprecateReqWrapper

v2.2.0: Extensive integration tests, revise base REST client, fix Spot REST client, type improvements
This commit is contained in:
Tiago
2022-05-21 23:20:41 +01:00
committed by GitHub
32 changed files with 10134 additions and 1009 deletions

7581
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "bybit-api", "name": "bybit-api",
"version": "2.1.10", "version": "2.2.0",
"description": "Node.js connector for Bybit's REST APIs and WebSockets, with TypeScript & integration tests.", "description": "Node.js connector for Bybit's REST APIs and WebSockets, with TypeScript & integration tests.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",

View File

@@ -9,3 +9,34 @@ export const positionTpSlModeEnum = {
/** Partial take profit/stop loss mode (multiple TP and SL orders can be placed, covering portions of the position) */ /** Partial take profit/stop loss mode (multiple TP and SL orders can be placed, covering portions of the position) */
Partial: 'Partial', Partial: 'Partial',
} as const; } as const;
export const API_ERROR_CODE = {
BALANCE_INSUFFICIENT_SPOT: -1131,
ORDER_NOT_FOUND_OR_TOO_LATE_SPOT: -2013,
/** This could mean bad request, incorrect value types or even incorrect/missing values */
PARAMS_MISSING_OR_WRONG: 10001,
ORDER_NOT_FOUND_OR_TOO_LATE: 20001,
POSITION_STATUS_NOT_NORMAL: 30013,
CANNOT_SET_TRADING_STOP_FOR_ZERO_POS: 30024,
/** Seen when placing an order */
INSUFFICIENT_BALANCE_FOR_ORDER_COST: 30031,
POSITION_IDX_NOT_MATCH_POSITION_MODE: 30041,
/** Seen if a conditional order is too large */
INSUFFICIENT_BALANCE: 30042,
/** E.g. trying to change position margin while on cross */
POSITION_IS_CROSS_MARGIN: 30056,
POSITION_MODE_NOT_MODIFIED: 30083,
ISOLATED_NOT_MODIFIED: 30084,
RISK_LIMIT_NOT_EXISTS: 30090,
LEVERAGE_NOT_MODIFIED: 34036,
SAME_SLTP_MODE: 37002,
ORDER_NOT_FOUND_OR_TOO_LATE_LINEAR: 130010,
ORDER_COST_NOT_AVAILABLE: 130021,
CANNOT_SET_LINEAR_TRADING_STOP_FOR_ZERO_POS: 130024,
ISOLATED_NOT_MODIFIED_LINEAR: 130056,
POSITION_SIZE_IS_ZERO: 130057,
AUTO_ADD_MARGIN_NOT_MODIFIED: 130060,
INSUFFICIENT_BALANCE_FOR_ORDER_COST_LINEAR: 130080,
SAME_SLTP_MODE_LINEAR: 130150,
RISK_ID_NOT_MODIFIED: 134026,
} as const;

View File

@@ -1,10 +1,10 @@
export * from "./inverse-client"; export * from './inverse-client';
export * from "./inverse-futures-client"; export * from './inverse-futures-client';
export * from "./linear-client"; export * from './linear-client';
export * from "./spot-client"; export * from './spot-client';
export * from "./websocket-client"; export * from './websocket-client';
export * from "./logger"; export * from './logger';
export * from "./types/shared"; export * from './types/shared';
export * from "./types/spot"; export * from './types/spot';
export * from "./util/WsStore"; export * from './util/WsStore';
export * from "./constants/enum"; export * from './constants/enum';

View File

@@ -1,12 +1,24 @@
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from 'axios';
import { GenericAPIResponse, getRestBaseUrl, RestClientOptions } from './util/requestUtils'; import {
import RequestWrapper from './util/requestWrapper'; getRestBaseUrl,
import SharedEndpoints from './shared-endpoints'; RestClientOptions,
import { SymbolFromLimitParam, SymbolIntervalFromLimitParam, SymbolParam } from './types/shared'; REST_CLIENT_TYPE_ENUM,
} from './util/requestUtils';
export class InverseClient extends SharedEndpoints { import {
protected requestWrapper: RequestWrapper; APIResponseWithTime,
AssetExchangeRecordsReq,
CoinParam,
SymbolInfo,
SymbolIntervalFromLimitParam,
SymbolLimitParam,
SymbolParam,
SymbolPeriodLimitParam,
WalletFundRecordsReq,
WithdrawRecordsReq,
} from './types/shared';
import BaseRestClient from './util/BaseRestClient';
export class InverseClient extends BaseRestClient {
/** /**
* @public Creates an instance of the inverse REST API client. * @public Creates an instance of the inverse REST API client.
* *
@@ -23,56 +35,149 @@ export class InverseClient extends SharedEndpoints {
restClientOptions: RestClientOptions = {}, restClientOptions: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {} requestOptions: AxiosRequestConfig = {}
) { ) {
super(); super(
this.requestWrapper = new RequestWrapper(
key, key,
secret, secret,
getRestBaseUrl(useLivenet, restClientOptions), getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions, restClientOptions,
requestOptions requestOptions,
REST_CLIENT_TYPE_ENUM.inverse
); );
return this; return this;
} }
async fetchServerTime(): Promise<number> {
const res = await this.getServerTime();
return Number(res.time_now);
}
/** /**
* *
* Market Data Endpoints * Market Data Endpoints
* *
*/ */
getKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse { getOrderBook(params: SymbolParam): Promise<APIResponseWithTime<any[]>> {
return this.requestWrapper.get('v2/public/kline/list', params); return this.get('v2/public/orderBook/L2', params);
}
getKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/kline/list', params);
} }
/** /**
* @deprecated use getTickers() instead * Get latest information for symbol
*/ */
getLatestInformation(params?: Partial<SymbolParam>): GenericAPIResponse { getTickers(
return this.getTickers(params); params?: Partial<SymbolParam>
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/tickers', params);
}
getTrades(params: SymbolLimitParam): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/trading-records', params);
}
getSymbols(): Promise<APIResponseWithTime<SymbolInfo[]>> {
return this.get('v2/public/symbols');
}
getMarkPriceKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/mark-price-kline', params);
}
getIndexPriceKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/index-price-kline', params);
}
getPremiumIndexKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/premium-index-kline', params);
} }
/** /**
* @deprecated use getTrades() instead *
* Market Data : Advanced
*
*/ */
getPublicTradingRecords(params: SymbolFromLimitParam): GenericAPIResponse {
return this.getTrades(params); getOpenInterest(
params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/open-interest', params);
} }
getTrades(params: SymbolFromLimitParam): GenericAPIResponse { getLatestBigDeal(
return this.requestWrapper.get('v2/public/trading-records', params); params: SymbolLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/big-deal', params);
} }
getMarkPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse { getLongShortRatio(
return this.requestWrapper.get('v2/public/mark-price-kline', params); params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/account-ratio', params);
} }
getIndexPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse { /**
return this.requestWrapper.get('v2/public/index-price-kline', params); *
* Account Data Endpoints
*
*/
getApiKeyInfo(): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/account/api-key');
} }
getPremiumIndexKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse { /**
return this.requestWrapper.get('v2/public/premium-index-kline', params); *
* Wallet Data Endpoints
*
*/
getWalletBalance(
params?: Partial<CoinParam>
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/balance', params);
}
getWalletFundRecords(
params?: WalletFundRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/fund/records', params);
}
getWithdrawRecords(
params?: WithdrawRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/withdraw/list', params);
}
getAssetExchangeRecords(
params?: AssetExchangeRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/exchange-order/list', params);
}
/**
*
* API Data Endpoints
*
*/
getServerTime(): Promise<APIResponseWithTime<{}>> {
return this.get('v2/public/time');
}
getApiAnnouncements(): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/announcement');
} }
/** /**
@@ -99,8 +204,8 @@ export class InverseClient extends SharedEndpoints {
sl_trigger_by?: 'LastPrice' | 'MarkPrice' | 'IndexPrice'; sl_trigger_by?: 'LastPrice' | 'MarkPrice' | 'IndexPrice';
close_on_trigger?: boolean; close_on_trigger?: boolean;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('v2/private/order/create', orderRequest); return this.postPrivate('v2/private/order/create', orderRequest);
} }
getActiveOrderList(params: { getActiveOrderList(params: {
@@ -109,20 +214,22 @@ export class InverseClient extends SharedEndpoints {
direction?: string; direction?: string;
limit?: number; limit?: number;
cursor?: string; cursor?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/order/list', params); return this.getPrivate('v2/private/order/list', params);
} }
cancelActiveOrder(params: { cancelActiveOrder(params: {
symbol: string; symbol: string;
order_id?: string; order_id?: string;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('v2/private/order/cancel', params); return this.postPrivate('v2/private/order/cancel', params);
} }
cancelAllActiveOrders(params: SymbolParam): GenericAPIResponse { cancelAllActiveOrders(
return this.requestWrapper.post('v2/private/order/cancelAll', params); params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/order/cancelAll', params);
} }
replaceActiveOrder(params: { replaceActiveOrder(params: {
@@ -135,16 +242,16 @@ export class InverseClient extends SharedEndpoints {
stop_loss?: number; stop_loss?: number;
tp_trigger_by?: string; tp_trigger_by?: string;
sl_trigger_by?: string; sl_trigger_by?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('v2/private/order/replace', params); return this.postPrivate('v2/private/order/replace', params);
} }
queryActiveOrder(params: { queryActiveOrder(params: {
order_id?: string; order_id?: string;
order_link_id?: string; order_link_id?: string;
symbol: string; symbol: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/order', params); return this.getPrivate('v2/private/order', params);
} }
/** /**
@@ -163,30 +270,33 @@ export class InverseClient extends SharedEndpoints {
trigger_by?: string; trigger_by?: string;
close_on_trigger?: boolean; close_on_trigger?: boolean;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('v2/private/stop-order/create', params); return this.postPrivate('v2/private/stop-order/create', params);
} }
/** get conditional order list. This may see delays, use queryConditionalOrder() for real-time queries */
getConditionalOrder(params: { getConditionalOrder(params: {
symbol: string; symbol: string;
stop_order_status?: string; stop_order_status?: string;
direction?: string; direction?: string;
limit?: number; limit?: number;
cursor?: string; cursor?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/stop-order/list', params); return this.getPrivate('v2/private/stop-order/list', params);
} }
cancelConditionalOrder(params: { cancelConditionalOrder(params: {
symbol: string; symbol: string;
stop_order_id?: string; stop_order_id?: string;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('v2/private/stop-order/cancel', params); return this.postPrivate('v2/private/stop-order/cancel', params);
} }
cancelAllConditionalOrders(params: SymbolParam): GenericAPIResponse { cancelAllConditionalOrders(
return this.requestWrapper.post('v2/private/stop-order/cancelAll', params); params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/stop-order/cancelAll', params);
} }
replaceConditionalOrder(params: { replaceConditionalOrder(params: {
@@ -196,45 +306,33 @@ export class InverseClient extends SharedEndpoints {
p_r_qty?: number; p_r_qty?: number;
p_r_price?: string; p_r_price?: string;
p_r_trigger_price?: string; p_r_trigger_price?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('v2/private/stop-order/replace', params); return this.postPrivate('v2/private/stop-order/replace', params);
} }
queryConditionalOrder(params: { queryConditionalOrder(params: {
symbol: string; symbol: string;
stop_order_id?: string; stop_order_id?: string;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/stop-order', params); return this.getPrivate('v2/private/stop-order', params);
} }
/** /**
* Position * Position
*/ */
/** getPosition(
* @deprecated use getPosition() instead params?: Partial<SymbolParam>
*/ ): Promise<APIResponseWithTime<any>> {
getUserLeverage(): GenericAPIResponse { return this.getPrivate('v2/private/position/list', params);
return this.requestWrapper.get('user/leverage');
}
getPosition(params?: Partial<SymbolParam>): GenericAPIResponse {
return this.requestWrapper.get('v2/private/position/list', params);
}
/**
* @deprecated use getPosition() instead
*/
getPositions(): GenericAPIResponse {
return this.requestWrapper.get('position/list');
} }
changePositionMargin(params: { changePositionMargin(params: {
symbol: string; symbol: string;
margin: string; margin: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('position/change-position-margin', params); return this.postPrivate('position/change-position-margin', params);
} }
setTradingStop(params: { setTradingStop(params: {
@@ -245,23 +343,16 @@ export class InverseClient extends SharedEndpoints {
tp_trigger_by?: string; tp_trigger_by?: string;
sl_trigger_by?: string; sl_trigger_by?: string;
new_trailing_active?: number; new_trailing_active?: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('v2/private/position/trading-stop', params); return this.postPrivate('v2/private/position/trading-stop', params);
} }
setUserLeverage(params: { setUserLeverage(params: {
symbol: string; symbol: string;
leverage: number; leverage: number;
leverage_only?: boolean; leverage_only?: boolean;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('v2/private/position/leverage/save', params); return this.postPrivate('v2/private/position/leverage/save', params);
}
/**
* @deprecated use setUserLeverage() instead
*/
changeUserLeverage(params: any): GenericAPIResponse {
return this.setUserLeverage(params);
} }
getTradeRecords(params: { getTradeRecords(params: {
@@ -271,8 +362,8 @@ export class InverseClient extends SharedEndpoints {
page?: number; page?: number;
limit?: number; limit?: number;
order?: string; order?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/execution/list', params); return this.getPrivate('v2/private/execution/list', params);
} }
getClosedPnl(params: { getClosedPnl(params: {
@@ -282,22 +373,15 @@ export class InverseClient extends SharedEndpoints {
exec_type?: string; exec_type?: string;
page?: number; page?: number;
limit?: number; limit?: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/trade/closed-pnl/list', params); return this.getPrivate('v2/private/trade/closed-pnl/list', params);
}
setPositionMode(params: {
symbol: string;
mode: 0 | 3;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/position/switch-mode', params);
} }
setSlTpPositionMode(params: { setSlTpPositionMode(params: {
symbol: string; symbol: string;
tp_sl_mode: 'Full' | 'Partial'; tp_sl_mode: 'Full' | 'Partial';
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('v2/private/tpsl/switch-mode', params); return this.postPrivate('v2/private/tpsl/switch-mode', params);
} }
setMarginType(params: { setMarginType(params: {
@@ -305,52 +389,46 @@ export class InverseClient extends SharedEndpoints {
is_isolated: boolean; is_isolated: boolean;
buy_leverage: number; buy_leverage: number;
sell_leverage: number; sell_leverage: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('v2/private/position/switch-isolated', params); return this.postPrivate('v2/private/position/switch-isolated', params);
} }
/** /**
* Risk Limit * Risk Limit
*/ */
getRiskLimitList(): GenericAPIResponse { getRiskLimitList(): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('open-api/wallet/risk-limit/list'); return this.getPrivate('open-api/wallet/risk-limit/list');
} }
setRiskLimit(params: { setRiskLimit(params: {
symbol: string; symbol: string;
risk_id: string; risk_id: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('open-api/wallet/risk-limit', params); return this.postPrivate('open-api/wallet/risk-limit', params);
} }
/** /**
* Funding * Funding
*/ */
getLastFundingRate(params: SymbolParam): GenericAPIResponse { getLastFundingRate(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/public/funding/prev-funding-rate', params); return this.get('v2/public/funding/prev-funding-rate', params);
} }
getMyLastFundingFee(params: SymbolParam): GenericAPIResponse { getMyLastFundingFee(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/funding/prev-funding', params); return this.getPrivate('v2/private/funding/prev-funding', params);
} }
getPredictedFunding(params: SymbolParam): GenericAPIResponse { getPredictedFunding(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/funding/predicted-funding', params); return this.getPrivate('v2/private/funding/predicted-funding', params);
} }
/** /**
* LCP Info * LCP Info
*/ */
getLcpInfo(params: SymbolParam): GenericAPIResponse { getLcpInfo(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/account/lcp', params); return this.getPrivate('v2/private/account/lcp', params);
} }
//API Key Info
getAPIKeyInfo(): GenericAPIResponse {
return this.requestWrapper.get('v2/private/account/api-key');
} }
};

View File

@@ -1,12 +1,24 @@
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from 'axios';
import { GenericAPIResponse, getRestBaseUrl, RestClientOptions } from './util/requestUtils'; import {
import RequestWrapper from './util/requestWrapper'; getRestBaseUrl,
import SharedEndpoints from './shared-endpoints'; RestClientOptions,
import { SymbolFromLimitParam, SymbolIntervalFromLimitParam, SymbolParam } from './types/shared'; REST_CLIENT_TYPE_ENUM,
} from './util/requestUtils';
export class InverseFuturesClient extends SharedEndpoints { import {
protected requestWrapper: RequestWrapper; APIResponseWithTime,
AssetExchangeRecordsReq,
CoinParam,
SymbolInfo,
SymbolIntervalFromLimitParam,
SymbolLimitParam,
SymbolParam,
SymbolPeriodLimitParam,
WalletFundRecordsReq,
WithdrawRecordsReq,
} from './types/shared';
import BaseRestClient from './util/BaseRestClient';
export class InverseFuturesClient extends BaseRestClient {
/** /**
* @public Creates an instance of the inverse futures REST API client. * @public Creates an instance of the inverse futures REST API client.
* *
@@ -23,45 +35,152 @@ export class InverseFuturesClient extends SharedEndpoints {
restClientOptions: RestClientOptions = {}, restClientOptions: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {} requestOptions: AxiosRequestConfig = {}
) { ) {
super(); super(
this.requestWrapper = new RequestWrapper(
key, key,
secret, secret,
getRestBaseUrl(useLivenet, restClientOptions), getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions, restClientOptions,
requestOptions requestOptions,
REST_CLIENT_TYPE_ENUM.inverseFutures
); );
return this; return this;
} }
async fetchServerTime(): Promise<number> {
const res = await this.getServerTime();
return Number(res.time_now);
}
/** /**
* *
* Market Data Endpoints * Market Data Endpoints
* Note: These are currently the same as the inverse client *
*/ */
getKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse { getOrderBook(params: SymbolParam): Promise<APIResponseWithTime<any[]>> {
return this.requestWrapper.get('v2/public/kline/list', params); return this.get('v2/public/orderBook/L2', params);
}
getKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/kline/list', params);
}
/**
* Get latest information for symbol
*/
getTickers(
params?: Partial<SymbolParam>
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/tickers', params);
} }
/** /**
* Public trading records * Public trading records
*/ */
getTrades(params: SymbolFromLimitParam): GenericAPIResponse { getTrades(params: SymbolLimitParam): Promise<APIResponseWithTime<any[]>> {
return this.requestWrapper.get('v2/public/trading-records', params); return this.get('v2/public/trading-records', params);
} }
getMarkPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse { getSymbols(): Promise<APIResponseWithTime<SymbolInfo[]>> {
return this.requestWrapper.get('v2/public/mark-price-kline', params); return this.get('v2/public/symbols');
} }
getIndexPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse { getMarkPriceKline(
return this.requestWrapper.get('v2/public/index-price-kline', params); params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/mark-price-kline', params);
} }
getPremiumIndexKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse { getIndexPriceKline(
return this.requestWrapper.get('v2/public/premium-index-kline', params); params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/index-price-kline', params);
}
getPremiumIndexKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/premium-index-kline', params);
}
/**
*
* Market Data : Advanced
*
*/
getOpenInterest(
params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/open-interest', params);
}
getLatestBigDeal(
params: SymbolLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/big-deal', params);
}
getLongShortRatio(
params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/account-ratio', params);
}
/**
*
* Account Data Endpoints
*
*/
getApiKeyInfo(): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/account/api-key');
}
/**
*
* Wallet Data Endpoints
*
*/
getWalletBalance(
params?: Partial<CoinParam>
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/balance', params);
}
getWalletFundRecords(
params?: WalletFundRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/fund/records', params);
}
getWithdrawRecords(
params?: WithdrawRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/withdraw/list', params);
}
getAssetExchangeRecords(
params?: AssetExchangeRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/exchange-order/list', params);
}
/**
*
* API Data Endpoints
*
*/
getServerTime(): Promise<APIResponseWithTime<{}>> {
return this.get('v2/public/time');
}
getApiAnnouncements(): Promise<APIResponseWithTime<any>> {
return this.get('v2/public/announcement');
} }
/** /**
@@ -86,8 +205,8 @@ export class InverseFuturesClient extends SharedEndpoints {
reduce_only?: boolean; reduce_only?: boolean;
close_on_trigger?: boolean; close_on_trigger?: boolean;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('futures/private/order/create', orderRequest); return this.postPrivate('futures/private/order/create', orderRequest);
} }
getActiveOrderList(params: { getActiveOrderList(params: {
@@ -96,20 +215,22 @@ export class InverseFuturesClient extends SharedEndpoints {
direction?: string; direction?: string;
limit?: number; limit?: number;
cursor?: string; cursor?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('futures/private/order/list', params); return this.getPrivate('futures/private/order/list', params);
} }
cancelActiveOrder(params: { cancelActiveOrder(params: {
symbol: string; symbol: string;
order_id?: string; order_id?: string;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('futures/private/order/cancel', params); return this.postPrivate('futures/private/order/cancel', params);
} }
cancelAllActiveOrders(params: SymbolParam): GenericAPIResponse { cancelAllActiveOrders(
return this.requestWrapper.post('futures/private/order/cancelAll', params); params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/order/cancelAll', params);
} }
replaceActiveOrder(params: { replaceActiveOrder(params: {
@@ -118,16 +239,16 @@ export class InverseFuturesClient extends SharedEndpoints {
symbol: string; symbol: string;
p_r_qty?: string; p_r_qty?: string;
p_r_price?: string; p_r_price?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('futures/private/order/replace', params); return this.postPrivate('futures/private/order/replace', params);
} }
queryActiveOrder(params: { queryActiveOrder(params: {
order_id?: string; order_id?: string;
order_link_id?: string; order_link_id?: string;
symbol: string; symbol: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('futures/private/order', params); return this.getPrivate('futures/private/order', params);
} }
/** /**
@@ -146,8 +267,8 @@ export class InverseFuturesClient extends SharedEndpoints {
trigger_by?: string; trigger_by?: string;
close_on_trigger?: boolean; close_on_trigger?: boolean;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('futures/private/stop-order/create', params); return this.postPrivate('futures/private/stop-order/create', params);
} }
getConditionalOrder(params: { getConditionalOrder(params: {
@@ -156,20 +277,22 @@ export class InverseFuturesClient extends SharedEndpoints {
direction?: string; direction?: string;
limit?: number; limit?: number;
cursor?: string; cursor?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('futures/private/stop-order/list', params); return this.getPrivate('futures/private/stop-order/list', params);
} }
cancelConditionalOrder(params: { cancelConditionalOrder(params: {
symbol: string; symbol: string;
stop_order_id?: string; stop_order_id?: string;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('futures/private/stop-order/cancel', params); return this.postPrivate('futures/private/stop-order/cancel', params);
} }
cancelAllConditionalOrders(params: SymbolParam): GenericAPIResponse { cancelAllConditionalOrders(
return this.requestWrapper.post('futures/private/stop-order/cancelAll', params); params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/stop-order/cancelAll', params);
} }
replaceConditionalOrder(params: { replaceConditionalOrder(params: {
@@ -179,35 +302,39 @@ export class InverseFuturesClient extends SharedEndpoints {
p_r_qty?: number; p_r_qty?: number;
p_r_price?: string; p_r_price?: string;
p_r_trigger_price?: string; p_r_trigger_price?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('futures/private/stop-order/replace', params); return this.postPrivate('futures/private/stop-order/replace', params);
} }
queryConditionalOrder(params: { queryConditionalOrder(params: {
symbol: string; symbol: string;
stop_order_id?: string; stop_order_id?: string;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('futures/private/stop-order', params); return this.getPrivate('futures/private/stop-order', params);
} }
/** /**
* Position * Position
*/ */
/** /**
* Get position list * Get position list
*/ */
getPosition(params?: Partial<SymbolParam>): GenericAPIResponse { getPosition(
return this.requestWrapper.get('futures/private/position/list', params); params?: Partial<SymbolParam>
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('futures/private/position/list', params);
} }
changePositionMargin(params: { changePositionMargin(params: {
symbol: string; symbol: string;
margin: string; margin: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('futures/private/position/change-position-margin', params); return this.postPrivate(
'futures/private/position/change-position-margin',
params
);
} }
setTradingStop(params: { setTradingStop(params: {
@@ -218,16 +345,16 @@ export class InverseFuturesClient extends SharedEndpoints {
tp_trigger_by?: string; tp_trigger_by?: string;
sl_trigger_by?: string; sl_trigger_by?: string;
new_trailing_active?: number; new_trailing_active?: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('futures/private/position/trading-stop', params); return this.postPrivate('futures/private/position/trading-stop', params);
} }
setUserLeverage(params: { setUserLeverage(params: {
symbol: string; symbol: string;
buy_leverage: number; buy_leverage: number;
sell_leverage: number; sell_leverage: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('futures/private/position/leverage/save', params); return this.postPrivate('futures/private/position/leverage/save', params);
} }
/** /**
@@ -236,8 +363,8 @@ export class InverseFuturesClient extends SharedEndpoints {
setPositionMode(params: { setPositionMode(params: {
symbol: string; symbol: string;
mode: number; mode: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('futures/private/position/switch-mode', params); return this.postPrivate('futures/private/position/switch-mode', params);
} }
/** /**
@@ -248,8 +375,8 @@ export class InverseFuturesClient extends SharedEndpoints {
is_isolated: boolean; is_isolated: boolean;
buy_leverage: number; buy_leverage: number;
sell_leverage: number; sell_leverage: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('futures/private/position/switch-isolated', params); return this.postPrivate('futures/private/position/switch-isolated', params);
} }
getTradeRecords(params: { getTradeRecords(params: {
@@ -259,8 +386,8 @@ export class InverseFuturesClient extends SharedEndpoints {
page?: number; page?: number;
limit?: number; limit?: number;
order?: string; order?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('futures/private/execution/list', params); return this.getPrivate('futures/private/execution/list', params);
} }
getClosedPnl(params: { getClosedPnl(params: {
@@ -270,8 +397,8 @@ export class InverseFuturesClient extends SharedEndpoints {
exec_type?: string; exec_type?: string;
page?: number; page?: number;
limit?: number; limit?: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('futures/private/trade/closed-pnl/list', params); return this.getPrivate('futures/private/trade/closed-pnl/list', params);
} }
/** /**
@@ -281,38 +408,38 @@ export class InverseFuturesClient extends SharedEndpoints {
/** /**
* Risk Limit * Risk Limit
*/ */
getRiskLimitList(): GenericAPIResponse { getRiskLimitList(): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('open-api/wallet/risk-limit/list'); return this.getPrivate('open-api/wallet/risk-limit/list');
} }
setRiskLimit(params: { setRiskLimit(params: {
symbol: string; symbol: string;
risk_id: string; risk_id: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('open-api/wallet/risk-limit', params); return this.postPrivate('open-api/wallet/risk-limit', params);
} }
/** /**
* Funding * Funding
*/ */
getLastFundingRate(params: SymbolParam): GenericAPIResponse { getLastFundingRate(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/public/funding/prev-funding-rate', params); return this.get('v2/public/funding/prev-funding-rate', params);
} }
getMyLastFundingFee(params: SymbolParam): GenericAPIResponse { getMyLastFundingFee(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/funding/prev-funding', params); return this.getPrivate('v2/private/funding/prev-funding', params);
} }
getPredictedFunding(params: SymbolParam): GenericAPIResponse { getPredictedFunding(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/funding/predicted-funding', params); return this.getPrivate('v2/private/funding/predicted-funding', params);
} }
/** /**
* LCP Info * LCP Info
*/ */
getLcpInfo(params: SymbolParam): GenericAPIResponse { getLcpInfo(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('v2/private/account/lcp', params); return this.getPrivate('v2/private/account/lcp', params);
}
} }
};

View File

@@ -1,23 +1,28 @@
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from 'axios';
import { import {
GenericAPIResponse,
getRestBaseUrl, getRestBaseUrl,
RestClientOptions, RestClientOptions,
REST_CLIENT_TYPE_ENUM,
} from './util/requestUtils'; } from './util/requestUtils';
import RequestWrapper from './util/requestWrapper';
import SharedEndpoints from './shared-endpoints';
import { import {
APIResponse,
APIResponseWithTime,
AssetExchangeRecordsReq,
CoinParam,
SymbolInfo,
SymbolIntervalFromLimitParam, SymbolIntervalFromLimitParam,
SymbolLimitParam, SymbolLimitParam,
SymbolParam, SymbolParam,
SymbolPeriodLimitParam,
WalletFundRecordsReq,
WithdrawRecordsReq,
} from './types/shared'; } from './types/shared';
import { linearPositionModeEnum, positionTpSlModeEnum } from './constants/enum'; import { linearPositionModeEnum, positionTpSlModeEnum } from './constants/enum';
import BaseRestClient from './util/BaseRestClient';
export class LinearClient extends SharedEndpoints { export class LinearClient extends BaseRestClient {
protected requestWrapper: RequestWrapper;
/** /**
* @public Creates an instance of the linear REST API client. * @public Creates an instance of the linear (USD Perps) REST API client.
* *
* @param {string} key - your API key * @param {string} key - your API key
* @param {string} secret - your API secret * @param {string} secret - your API secret
@@ -32,54 +37,153 @@ export class LinearClient extends SharedEndpoints {
restClientOptions: RestClientOptions = {}, restClientOptions: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {} requestOptions: AxiosRequestConfig = {}
) { ) {
super(); super(
this.requestWrapper = new RequestWrapper(
key, key,
secret, secret,
getRestBaseUrl(useLivenet, restClientOptions), getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions, restClientOptions,
requestOptions requestOptions,
REST_CLIENT_TYPE_ENUM.linear
); );
return this; return this;
} }
async fetchServerTime(): Promise<number> {
const timeRes = await this.getServerTime();
return Number(timeRes.time_now);
}
/** /**
* *
* Market Data Endpoints * Market Data Endpoints
* *
*/ */
getKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse { getOrderBook(params: SymbolParam): Promise<APIResponseWithTime<any[]>> {
return this.requestWrapper.get('public/linear/kline', params); return this.get('v2/public/orderBook/L2', params);
} }
getTrades(params: SymbolLimitParam): GenericAPIResponse { getKline(
return this.requestWrapper.get( params: SymbolIntervalFromLimitParam
'public/linear/recent-trading-records', ): Promise<APIResponseWithTime<any[]>> {
params return this.get('public/linear/kline', params);
);
} }
getLastFundingRate(params: SymbolParam): GenericAPIResponse { /**
return this.requestWrapper.get( * Get latest information for symbol
'public/linear/funding/prev-funding-rate', */
params getTickers(
); params?: Partial<SymbolParam>
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/tickers', params);
} }
getMarkPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse { getTrades(params: SymbolLimitParam): Promise<APIResponseWithTime<any[]>> {
return this.requestWrapper.get('public/linear/mark-price-kline', params); return this.get('public/linear/recent-trading-records', params);
} }
getIndexPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse { getSymbols(): Promise<APIResponse<SymbolInfo[]>> {
return this.requestWrapper.get('public/linear/index-price-kline', params); return this.get('v2/public/symbols');
}
getLastFundingRate(params: SymbolParam): Promise<APIResponseWithTime<any[]>> {
return this.get('public/linear/funding/prev-funding-rate', params);
}
getMarkPriceKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('public/linear/mark-price-kline', params);
}
getIndexPriceKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('public/linear/index-price-kline', params);
} }
getPremiumIndexKline( getPremiumIndexKline(
params: SymbolIntervalFromLimitParam params: SymbolIntervalFromLimitParam
): GenericAPIResponse { ): Promise<APIResponseWithTime<any[]>> {
return this.requestWrapper.get('public/linear/premium-index-kline', params); return this.get('public/linear/premium-index-kline', params);
}
/**
*
* Market Data : Advanced
*
*/
getOpenInterest(
params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/open-interest', params);
}
getLatestBigDeal(
params: SymbolLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/big-deal', params);
}
getLongShortRatio(
params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/account-ratio', params);
}
/**
*
* Account Data Endpoints
*
*/
getApiKeyInfo(): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/account/api-key');
}
/**
*
* Wallet Data Endpoints
*
*/
getWalletBalance(
params?: Partial<CoinParam>
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/balance', params);
}
getWalletFundRecords(
params?: WalletFundRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/fund/records', params);
}
getWithdrawRecords(
params?: WithdrawRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/withdraw/list', params);
}
getAssetExchangeRecords(
params?: AssetExchangeRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/exchange-order/list', params);
}
/**
*
* API Data Endpoints
*
*/
getServerTime(): Promise<APIResponseWithTime<{}>> {
return this.get('v2/public/time');
}
getApiAnnouncements(): Promise<APIResponseWithTime<any>> {
return this.get('v2/public/announcement');
} }
/** /**
@@ -103,8 +207,8 @@ export class LinearClient extends SharedEndpoints {
close_on_trigger: boolean; close_on_trigger: boolean;
order_link_id?: string; order_link_id?: string;
position_idx?: number; position_idx?: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('private/linear/order/create', params); return this.postPrivate('private/linear/order/create', params);
} }
getActiveOrderList(params: { getActiveOrderList(params: {
@@ -115,20 +219,22 @@ export class LinearClient extends SharedEndpoints {
page?: number; page?: number;
limit?: number; limit?: number;
order_status?: string; order_status?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('private/linear/order/list', params); return this.getPrivate('private/linear/order/list', params);
} }
cancelActiveOrder(params: { cancelActiveOrder(params: {
symbol: string; symbol: string;
order_id?: string; order_id?: string;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('private/linear/order/cancel', params); return this.postPrivate('private/linear/order/cancel', params);
} }
cancelAllActiveOrders(params: SymbolParam): GenericAPIResponse { cancelAllActiveOrders(
return this.requestWrapper.post('private/linear/order/cancel-all', params); params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/order/cancel-all', params);
} }
replaceActiveOrder(params: { replaceActiveOrder(params: {
@@ -141,16 +247,16 @@ export class LinearClient extends SharedEndpoints {
stop_loss?: number; stop_loss?: number;
tp_trigger_by?: string; tp_trigger_by?: string;
sl_trigger_by?: string; sl_trigger_by?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('private/linear/order/replace', params); return this.postPrivate('private/linear/order/replace', params);
} }
queryActiveOrder(params: { queryActiveOrder(params: {
order_id?: string; order_id?: string;
order_link_id?: string; order_link_id?: string;
symbol: string; symbol: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('private/linear/order/search', params); return this.getPrivate('private/linear/order/search', params);
} }
/** /**
@@ -174,8 +280,8 @@ export class LinearClient extends SharedEndpoints {
stop_loss?: number; stop_loss?: number;
tp_trigger_by?: string; tp_trigger_by?: string;
sl_trigger_by?: string; sl_trigger_by?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('private/linear/stop-order/create', params); return this.postPrivate('private/linear/stop-order/create', params);
} }
getConditionalOrder(params: { getConditionalOrder(params: {
@@ -186,23 +292,22 @@ export class LinearClient extends SharedEndpoints {
order?: string; order?: string;
page?: number; page?: number;
limit?: number; limit?: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('private/linear/stop-order/list', params); return this.getPrivate('private/linear/stop-order/list', params);
} }
cancelConditionalOrder(params: { cancelConditionalOrder(params: {
symbol: string; symbol: string;
stop_order_id?: string; stop_order_id?: string;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('private/linear/stop-order/cancel', params); return this.postPrivate('private/linear/stop-order/cancel', params);
} }
cancelAllConditionalOrders(params: SymbolParam): GenericAPIResponse { cancelAllConditionalOrders(
return this.requestWrapper.post( params: SymbolParam
'private/linear/stop-order/cancel-all', ): Promise<APIResponseWithTime<any>> {
params return this.postPrivate('private/linear/stop-order/cancel-all', params);
);
} }
replaceConditionalOrder(params: { replaceConditionalOrder(params: {
@@ -216,35 +321,34 @@ export class LinearClient extends SharedEndpoints {
stop_loss?: number; stop_loss?: number;
tp_trigger_by?: string; tp_trigger_by?: string;
sl_trigger_by?: string; sl_trigger_by?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post( return this.postPrivate('private/linear/stop-order/replace', params);
'private/linear/stop-order/replace',
params
);
} }
queryConditionalOrder(params: { queryConditionalOrder(params: {
symbol: string; symbol: string;
stop_order_id?: string; stop_order_id?: string;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('private/linear/stop-order/search', params); return this.getPrivate('private/linear/stop-order/search', params);
} }
/** /**
* Position * Position
*/ */
getPosition(params?: Partial<SymbolParam>): GenericAPIResponse { getPosition(
return this.requestWrapper.get('private/linear/position/list', params); params?: Partial<SymbolParam>
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('private/linear/position/list', params);
} }
setAutoAddMargin(params?: { setAutoAddMargin(params?: {
symbol: string; symbol: string;
side: string; side: string;
auto_add_margin: boolean; auto_add_margin: boolean;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post( return this.postPrivate(
'private/linear/position/set-auto-add-margin', 'private/linear/position/set-auto-add-margin',
params params
); );
@@ -255,11 +359,8 @@ export class LinearClient extends SharedEndpoints {
is_isolated: boolean; is_isolated: boolean;
buy_leverage: number; buy_leverage: number;
sell_leverage: number; sell_leverage: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post( return this.postPrivate('private/linear/position/switch-isolated', params);
'private/linear/position/switch-isolated',
params
);
} }
/** /**
@@ -268,19 +369,8 @@ export class LinearClient extends SharedEndpoints {
setPositionMode(params: { setPositionMode(params: {
symbol: string; symbol: string;
mode: typeof linearPositionModeEnum[keyof typeof linearPositionModeEnum]; mode: typeof linearPositionModeEnum[keyof typeof linearPositionModeEnum];
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post( return this.postPrivate('private/linear/position/switch-mode', params);
'private/linear/position/switch-mode',
params
);
}
/** @deprecated use setPositionTpSlMode() instead */
setSwitchMode(params?: {
symbol: string;
tp_sl_mode: typeof positionTpSlModeEnum[keyof typeof positionTpSlModeEnum];
}): GenericAPIResponse {
return this.requestWrapper.post('private/linear/tpsl/switch-mode', params);
} }
/** /**
@@ -290,30 +380,24 @@ export class LinearClient extends SharedEndpoints {
setPositionTpSlMode(params: { setPositionTpSlMode(params: {
symbol: string; symbol: string;
tp_sl_mode: typeof positionTpSlModeEnum[keyof typeof positionTpSlModeEnum]; tp_sl_mode: typeof positionTpSlModeEnum[keyof typeof positionTpSlModeEnum];
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post('private/linear/tpsl/switch-mode', params); return this.postPrivate('private/linear/tpsl/switch-mode', params);
} }
setAddReduceMargin(params?: { setAddReduceMargin(params?: {
symbol: string; symbol: string;
side: string; side: string;
margin: number; margin: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post( return this.postPrivate('private/linear/position/add-margin', params);
'private/linear/position/add-margin',
params
);
} }
setUserLeverage(params: { setUserLeverage(params: {
symbol: string; symbol: string;
buy_leverage: number; buy_leverage: number;
sell_leverage: number; sell_leverage: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post( return this.postPrivate('private/linear/position/set-leverage', params);
'private/linear/position/set-leverage',
params
);
} }
setTradingStop(params: { setTradingStop(params: {
@@ -326,11 +410,8 @@ export class LinearClient extends SharedEndpoints {
sl_trigger_by?: string; sl_trigger_by?: string;
sl_size?: number; sl_size?: number;
tp_size?: number; tp_size?: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.post( return this.postPrivate('private/linear/position/trading-stop', params);
'private/linear/position/trading-stop',
params
);
} }
getTradeRecords(params: { getTradeRecords(params: {
@@ -340,11 +421,8 @@ export class LinearClient extends SharedEndpoints {
exec_type?: string; exec_type?: string;
page?: number; page?: number;
limit?: number; limit?: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get( return this.getPrivate('private/linear/trade/execution/list', params);
'private/linear/trade/execution/list',
params
);
} }
getClosedPnl(params: { getClosedPnl(params: {
@@ -354,44 +432,37 @@ export class LinearClient extends SharedEndpoints {
exec_type?: string; exec_type?: string;
page?: number; page?: number;
limit?: number; limit?: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get( return this.getPrivate('private/linear/trade/closed-pnl/list', params);
'private/linear/trade/closed-pnl/list',
params
);
} }
/** /**
* Risk Limit * Risk Limit
*/ */
getRiskLimitList(params: SymbolParam): GenericAPIResponse { getRiskLimitList(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('public/linear/risk-limit', params); return this.getPrivate('public/linear/risk-limit', params);
} }
setRiskLimit(params: { setRiskLimit(params: {
symbol: string; symbol: string;
side: string; side: string;
risk_id: string; risk_id: number;
}): GenericAPIResponse { }): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get('private/linear/position/set-risk', params); return this.postPrivate('private/linear/position/set-risk', params);
} }
/** /**
* Funding * Funding
*/ */
getPredictedFundingFee(params: SymbolParam): GenericAPIResponse { getPredictedFundingFee(
return this.requestWrapper.get( params: SymbolParam
'private/linear/funding/predicted-funding', ): Promise<APIResponseWithTime<any>> {
params return this.getPrivate('private/linear/funding/predicted-funding', params);
);
} }
getLastFundingFee(params: SymbolParam): GenericAPIResponse { getLastFundingFee(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.requestWrapper.get( return this.getPrivate('private/linear/funding/prev-funding', params);
'private/linear/funding/prev-funding',
params
);
} }
} }

View File

@@ -18,5 +18,5 @@ export const DefaultLogger = {
}, },
error: (...params: LogParams): void => { error: (...params: LogParams): void => {
console.error(params); console.error(params);
} },
}; };

View File

@@ -1,111 +0,0 @@
import {
APIResponse,
AssetExchangeRecordsReq,
CoinParam,
SymbolInfo,
SymbolLimitParam,
SymbolParam,
SymbolPeriodLimitParam,
WalletFundRecordsReq,
WithdrawRecordsReq,
} from './types/shared';
import { GenericAPIResponse } from './util/requestUtils';
import RequestWrapper from './util/requestWrapper';
export default class SharedEndpoints {
// TODO: Is there a way to say that Base has to provide this?
protected requestWrapper: RequestWrapper;
/**
*
* Market Data Endpoints
*
*/
getOrderBook(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/orderBook/L2', params);
}
/**
* Get latest information for symbol
*/
getTickers(params?: Partial<SymbolParam>): GenericAPIResponse {
return this.requestWrapper.get('v2/public/tickers', params);
}
getSymbols(): Promise<APIResponse<SymbolInfo[]>> {
return this.requestWrapper.get('v2/public/symbols');
}
/**
*
* Market Data : Advanced
*
*/
getOpenInterest(params: SymbolPeriodLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/open-interest', params);
}
getLatestBigDeal(params: SymbolLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/big-deal', params);
}
getLongShortRatio(params: SymbolPeriodLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/account-ratio', params);
}
/**
*
* Account Data Endpoints
*
*/
getApiKeyInfo(): GenericAPIResponse {
return this.requestWrapper.get('v2/private/account/api-key');
}
/**
*
* Wallet Data Endpoints
*
*/
getWalletBalance(params?: Partial<CoinParam>): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/balance', params)
}
getWalletFundRecords(params?: WalletFundRecordsReq): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/fund/records', params);
}
getWithdrawRecords(params: WithdrawRecordsReq): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/withdraw/list', params);
}
getAssetExchangeRecords(params?: AssetExchangeRecordsReq): GenericAPIResponse {
return this.requestWrapper.get('v2/private/exchange-order/list', params);
}
/**
*
* API Data Endpoints
*
*/
getServerTime(): GenericAPIResponse {
return this.requestWrapper.get('v2/public/time');
}
getApiAnnouncements(): GenericAPIResponse {
return this.requestWrapper.get('v2/public/announcement');
}
async getTimeOffset(): Promise<number> {
const start = Date.now();
return this.getServerTime().then(result => {
const end = Date.now();
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
});
}
}

View File

@@ -1,13 +1,21 @@
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from 'axios';
import { KlineInterval } from './types/shared'; import { APIResponse, KlineInterval } from './types/shared';
import { NewSpotOrder, OrderSide, OrderTypeSpot, SpotOrderQueryById } from './types/spot'; import {
NewSpotOrder,
OrderSide,
OrderTypeSpot,
SpotOrderQueryById,
SpotSymbolInfo,
} from './types/spot';
import BaseRestClient from './util/BaseRestClient'; import BaseRestClient from './util/BaseRestClient';
import { GenericAPIResponse, getRestBaseUrl, RestClientOptions } from './util/requestUtils'; import {
import RequestWrapper from './util/requestWrapper'; agentSource,
getRestBaseUrl,
RestClientOptions,
REST_CLIENT_TYPE_ENUM,
} from './util/requestUtils';
export class SpotClient extends BaseRestClient { export class SpotClient extends BaseRestClient {
protected requestWrapper: RequestWrapper;
/** /**
* @public Creates an instance of the Spot REST API client. * @public Creates an instance of the Spot REST API client.
* *
@@ -24,21 +32,25 @@ export class SpotClient extends BaseRestClient {
restClientOptions: RestClientOptions = {}, restClientOptions: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {} requestOptions: AxiosRequestConfig = {}
) { ) {
super(key, secret, getRestBaseUrl(useLivenet, restClientOptions), restClientOptions, requestOptions); super(
key,
secret,
getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions,
requestOptions,
REST_CLIENT_TYPE_ENUM.spot
);
// this.requestWrapper = new RequestWrapper(
// key,
// secret,
// getRestBaseUrl(useLivenet, restClientOptions),
// restClientOptions,
// requestOptions
// );
return this; return this;
} }
async getServerTime(urlKeyOverride?: string): Promise<number> { fetchServerTime(): Promise<number> {
const result = await this.get('/spot/v1/time'); return this.getServerTime();
return result.serverTime; }
async getServerTime(): Promise<number> {
const res = await this.get('/spot/v1/time');
return res.result.serverTime;
} }
/** /**
@@ -47,17 +59,22 @@ export class SpotClient extends BaseRestClient {
* *
**/ **/
getSymbols() { getSymbols(): Promise<APIResponse<SpotSymbolInfo[]>> {
return this.get('/spot/v1/symbols'); return this.get('/spot/v1/symbols');
} }
getOrderBook(symbol: string, limit?: number) { getOrderBook(symbol: string, limit?: number): Promise<APIResponse<any>> {
return this.get('/spot/quote/v1/depth', { return this.get('/spot/quote/v1/depth', {
symbol, limit symbol,
limit,
}); });
} }
getMergedOrderBook(symbol: string, scale?: number, limit?: number) { getMergedOrderBook(
symbol: string,
scale?: number,
limit?: number
): Promise<APIResponse<any>> {
return this.get('/spot/quote/v1/depth/merged', { return this.get('/spot/quote/v1/depth/merged', {
symbol, symbol,
scale, scale,
@@ -65,14 +82,20 @@ export class SpotClient extends BaseRestClient {
}); });
} }
getTrades(symbol: string, limit?: number) { getTrades(symbol: string, limit?: number): Promise<APIResponse<any[]>> {
return this.get('/spot/v1/trades', { return this.get('/spot/quote/v1/trades', {
symbol, symbol,
limit, limit,
}); });
} }
getCandles(symbol: string, interval: KlineInterval, limit?: number, startTime?: number, endTime?: number) { getCandles(
symbol: string,
interval: KlineInterval,
limit?: number,
startTime?: number,
endTime?: number
): Promise<APIResponse<any[]>> {
return this.get('/spot/quote/v1/kline', { return this.get('/spot/quote/v1/kline', {
symbol, symbol,
interval, interval,
@@ -82,15 +105,15 @@ export class SpotClient extends BaseRestClient {
}); });
} }
get24hrTicker(symbol?: string) { get24hrTicker(symbol?: string): Promise<APIResponse<any>> {
return this.get('/spot/quote/v1/ticker/24hr', { symbol }); return this.get('/spot/quote/v1/ticker/24hr', { symbol });
} }
getLastTradedPrice(symbol?: string) { getLastTradedPrice(symbol?: string): Promise<APIResponse<any>> {
return this.get('/spot/quote/v1/ticker/price', { symbol }); return this.get('/spot/quote/v1/ticker/price', { symbol });
} }
getBestBidAskPrice(symbol?: string) { getBestBidAskPrice(symbol?: string): Promise<APIResponse<any>> {
return this.get('/spot/quote/v1/ticker/book_ticker', { symbol }); return this.get('/spot/quote/v1/ticker/book_ticker', { symbol });
} }
@@ -98,31 +121,40 @@ export class SpotClient extends BaseRestClient {
* Account Data Endpoints * Account Data Endpoints
*/ */
submitOrder(params: NewSpotOrder) { submitOrder(params: NewSpotOrder): Promise<APIResponse<any>> {
return this.postPrivate('/spot/v1/order', params); return this.postPrivate('/spot/v1/order', {
...params,
agentSource,
});
} }
getOrder(params: SpotOrderQueryById) { getOrder(params: SpotOrderQueryById): Promise<APIResponse<any>> {
return this.getPrivate('/spot/v1/order', params); return this.getPrivate('/spot/v1/order', params);
} }
cancelOrder(params: SpotOrderQueryById) { cancelOrder(params: SpotOrderQueryById): Promise<APIResponse<any>> {
return this.deletePrivate('/spot/v1/order', params); return this.deletePrivate('/spot/v1/order', params);
} }
cancelOrderBatch(params: { cancelOrderBatch(params: {
symbol: string; symbol: string;
side?: OrderSide; side?: OrderSide;
orderTypes: OrderTypeSpot[] orderTypes: OrderTypeSpot[];
}) { }): Promise<APIResponse<any>> {
const orderTypes = params.orderTypes ? params.orderTypes.join(',') : undefined; const orderTypes = params.orderTypes
? params.orderTypes.join(',')
: undefined;
return this.deletePrivate('/spot/order/batch-cancel', { return this.deletePrivate('/spot/order/batch-cancel', {
...params, ...params,
orderTypes, orderTypes,
}); });
} }
getOpenOrders(symbol?: string, orderId?: string, limit?: number) { getOpenOrders(
symbol?: string,
orderId?: string,
limit?: number
): Promise<APIResponse<any>> {
return this.getPrivate('/spot/v1/open-orders', { return this.getPrivate('/spot/v1/open-orders', {
symbol, symbol,
orderId, orderId,
@@ -130,7 +162,11 @@ export class SpotClient extends BaseRestClient {
}); });
} }
getPastOrders(symbol?: string, orderId?: string, limit?: number) { getPastOrders(
symbol?: string,
orderId?: string,
limit?: number
): Promise<APIResponse<any>> {
return this.getPrivate('/spot/v1/history-orders', { return this.getPrivate('/spot/v1/history-orders', {
symbol, symbol,
orderId, orderId,
@@ -138,7 +174,12 @@ export class SpotClient extends BaseRestClient {
}); });
} }
getMyTrades(symbol?: string, limit?: number, fromId?: number, toId?: number) { getMyTrades(
symbol?: string,
limit?: number,
fromId?: number,
toId?: number
): Promise<APIResponse<any>> {
return this.getPrivate('/spot/v1/myTrades', { return this.getPrivate('/spot/v1/myTrades', {
symbol, symbol,
limit, limit,
@@ -151,7 +192,7 @@ export class SpotClient extends BaseRestClient {
* Wallet Data Endpoints * Wallet Data Endpoints
*/ */
getBalances() { getBalances(): Promise<APIResponse<any>> {
return this.getPrivate('/spot/v1/account'); return this.getPrivate('/spot/v1/account');
} }
} }

View File

@@ -1,4 +1,5 @@
export type KlineInterval = '1m' export type KlineInterval =
| '1m'
| '3m' | '3m'
| '5m' | '5m'
| '15m' | '15m'
@@ -16,12 +17,17 @@ export type numberInString = string;
export interface APIResponse<T> { export interface APIResponse<T> {
ret_code: number; ret_code: number;
ret_msg: "OK" | string; ret_msg: 'OK' | string;
ext_code: string; ext_code: string | null;
ext_info: string; ext_info: string | null;
result: T; result: T;
} }
export interface APIResponseWithTime<T> extends APIResponse<T> {
/** UTC timestamp */
time_now: numberInString;
}
/** /**
* Request Parameter Types * Request Parameter Types
*/ */

View File

@@ -1,3 +1,5 @@
import { numberInString } from './shared';
export type OrderSide = 'Buy' | 'Sell'; export type OrderSide = 'Buy' | 'Sell';
export type OrderTypeSpot = 'LIMIT' | 'MARKET' | 'LIMIT_MAKER'; export type OrderTypeSpot = 'LIMIT' | 'MARKET' | 'LIMIT_MAKER';
export type OrderTimeInForce = 'GTC' | 'FOK' | 'IOC'; export type OrderTimeInForce = 'GTC' | 'FOK' | 'IOC';
@@ -16,3 +18,18 @@ export interface SpotOrderQueryById {
orderId?: string; orderId?: string;
orderLinkId?: string; orderLinkId?: string;
} }
export interface SpotSymbolInfo {
name: string;
alias: string;
baseCurrency: string;
quoteCurrency: string;
basePrecision: numberInString;
quotePrecision: numberInString;
minTradeQuantity: numberInString;
minTradeAmount: numberInString;
minPricePrecision: numberInString;
maxTradeQuantity: numberInString;
maxTradeAmount: numberInString;
category: numberInString;
}

View File

@@ -1,7 +1,37 @@
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, Method } from 'axios'; import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import { signMessage } from './node-support'; import { signMessage } from './node-support';
import { RestClientOptions, GenericAPIResponse, getRestBaseUrl, serializeParams, isPublicEndpoint } from './requestUtils'; import {
RestClientOptions,
serializeParams,
RestClientType,
REST_CLIENT_TYPE_ENUM,
agentSource,
} from './requestUtils';
// axios.interceptors.request.use((request) => {
// console.log(new Date(), 'Starting Request', JSON.stringify(request, null, 2));
// return request;
// });
// axios.interceptors.response.use((response) => {
// console.log(new Date(), 'Response:', JSON.stringify(response, null, 2));
// return response;
// });
interface SignedRequestContext {
timestamp: number;
api_key?: string;
recv_window?: number;
// spot is diff from the rest...
recvWindow?: number;
}
interface SignedRequest<T> {
originalParams: T & SignedRequestContext;
paramsWithSign?: T & SignedRequestContext & { sign: string };
sign: string;
}
export default abstract class BaseRestClient { export default abstract class BaseRestClient {
private timeOffset: number | null; private timeOffset: number | null;
@@ -11,24 +41,31 @@ export default abstract class BaseRestClient {
private globalRequestOptions: AxiosRequestConfig; private globalRequestOptions: AxiosRequestConfig;
private key: string | undefined; private key: string | undefined;
private secret: string | undefined; private secret: string | undefined;
private clientType: RestClientType;
/** Function that calls exchange API to query & resolve server time, used by time sync */
abstract fetchServerTime(): Promise<number>;
constructor( constructor(
key: string | undefined, key: string | undefined,
secret: string | undefined, secret: string | undefined,
baseUrl: string, baseUrl: string,
options: RestClientOptions = {}, options: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {} requestOptions: AxiosRequestConfig = {},
clientType: RestClientType
) { ) {
this.timeOffset = null; this.timeOffset = null;
this.syncTimePromise = null; this.syncTimePromise = null;
this.clientType = clientType;
this.options = { this.options = {
recv_window: 5000, recv_window: 5000,
// how often to sync time drift with bybit servers // how often to sync time drift with bybit servers
sync_interval_ms: 3600000, sync_interval_ms: 3600000,
// if true, we'll throw errors if any params are undefined // if true, we'll throw errors if any params are undefined
strict_param_validation: false, strict_param_validation: false,
...options ...options,
}; };
this.globalRequestOptions = { this.globalRequestOptions = {
@@ -37,14 +74,16 @@ export default abstract class BaseRestClient {
// custom request options based on axios specs - see: https://github.com/axios/axios#request-config // custom request options based on axios specs - see: https://github.com/axios/axios#request-config
...requestOptions, ...requestOptions,
headers: { headers: {
'x-referer': 'bybitapinode' 'x-referer': 'bybitapinode',
}, },
}; };
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
if (key && !secret) { if (key && !secret) {
throw new Error('API Key & Secret are both required for private enpoints') throw new Error(
'API Key & Secret are both required for private enpoints'
);
} }
if (this.options.disable_time_sync !== true) { if (this.options.disable_time_sync !== true) {
@@ -56,31 +95,38 @@ export default abstract class BaseRestClient {
this.secret = secret; this.secret = secret;
} }
get(endpoint: string, params?: any): GenericAPIResponse { private isSpotClient() {
return this.clientType === REST_CLIENT_TYPE_ENUM.spot;
}
get(endpoint: string, params?: any) {
return this._call('GET', endpoint, params, true); return this._call('GET', endpoint, params, true);
} }
post(endpoint: string, params?: any): GenericAPIResponse { post(endpoint: string, params?: any) {
return this._call('POST', endpoint, params, true); return this._call('POST', endpoint, params, true);
} }
getPrivate(endpoint: string, params?: any): GenericAPIResponse { getPrivate(endpoint: string, params?: any) {
return this._call('GET', endpoint, params, false); return this._call('GET', endpoint, params, false);
} }
postPrivate(endpoint: string, params?: any): GenericAPIResponse { postPrivate(endpoint: string, params?: any) {
return this._call('POST', endpoint, params, false); return this._call('POST', endpoint, params, false);
} }
deletePrivate(endpoint: string, params?: any): GenericAPIResponse { deletePrivate(endpoint: string, params?: any) {
return this._call('DELETE', endpoint, params, false); return this._call('DELETE', endpoint, params, false);
} }
/** private async prepareSignParams(params?: any, isPublicApi?: boolean) {
* @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed. if (isPublicApi) {
*/ return {
private async _call(method: Method, endpoint: string, params?: any, isPublicApi?: boolean): GenericAPIResponse { originalParams: params,
if (!isPublicApi) { paramsWithSign: params,
};
}
if (!this.key || !this.secret) { if (!this.key || !this.secret) {
throw new Error('Private endpoints require api and private keys set'); throw new Error('Private endpoints require api and private keys set');
} }
@@ -89,29 +135,53 @@ export default abstract class BaseRestClient {
await this.syncTime(); await this.syncTime();
} }
params = await this.signRequest(params); return this.signRequest(params);
} }
/**
* @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed.
*/
private async _call(
method: Method,
endpoint: string,
params?: any,
isPublicApi?: boolean
): Promise<any> {
const options = { const options = {
...this.globalRequestOptions, ...this.globalRequestOptions,
url: [this.baseUrl, endpoint].join(endpoint.startsWith('/') ? '' : '/'), url: [this.baseUrl, endpoint].join(endpoint.startsWith('/') ? '' : '/'),
method: method, method: method,
json: true json: true,
}; };
if (method === 'GET') { for (const key in params) {
options.params = params; if (typeof params[key] === 'undefined') {
} else { delete params[key];
options.data = params; }
} }
return axios(options).then(response => { const signResult = await this.prepareSignParams(params, isPublicApi);
if (method === 'GET' || this.isSpotClient()) {
options.params = signResult.paramsWithSign;
if (options.params?.agentSource) {
options.data = {
agentSource: agentSource,
};
}
} else {
options.data = signResult.paramsWithSign;
}
return axios(options)
.then((response) => {
if (response.status == 200) { if (response.status == 200) {
return response.data; return response.data;
} }
throw response; throw response;
}).catch(e => this.parseException(e)); })
.catch((e) => this.parseException(e));
} }
/** /**
@@ -140,37 +210,53 @@ export default abstract class BaseRestClient {
message: response.statusText, message: response.statusText,
body: response.data, body: response.data,
headers: response.headers, headers: response.headers,
requestOptions: this.options requestOptions: this.options,
}; };
} }
/** /**
* @private sign request and set recv window * @private sign request and set recv window
*/ */
async signRequest(data: any): Promise<any> { private async signRequest<T extends Object>(
const params = { data: T & SignedRequestContext
): Promise<SignedRequest<T>> {
const res: SignedRequest<T> = {
originalParams: {
...data, ...data,
api_key: this.key, api_key: this.key,
timestamp: Date.now() + (this.timeOffset || 0) timestamp: Date.now() + (this.timeOffset || 0),
},
sign: '',
}; };
// Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen. // Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen.
if (this.options.recv_window && !params.recv_window) { if (this.options.recv_window && !res.originalParams.recv_window) {
params.recv_window = this.options.recv_window; if (this.isSpotClient()) {
res.originalParams.recvWindow = this.options.recv_window;
} else {
res.originalParams.recv_window = this.options.recv_window;
}
} }
if (this.key && this.secret) { if (this.key && this.secret) {
const serializedParams = serializeParams(params, this.options.strict_param_validation); const serializedParams = serializeParams(
params.sign = await signMessage(serializedParams, this.secret); res.originalParams,
this.options.strict_param_validation
);
res.sign = await signMessage(serializedParams, this.secret);
res.paramsWithSign = {
...res.originalParams,
sign: res.sign,
};
} }
return params; return res;
} }
/** /**
* Trigger time sync and store promise * Trigger time sync and store promise
*/ */
private syncTime(): GenericAPIResponse { private syncTime(): Promise<any> {
if (this.options.disable_time_sync === true) { if (this.options.disable_time_sync === true) {
return Promise.resolve(false); return Promise.resolve(false);
} }
@@ -179,7 +265,7 @@ export default abstract class BaseRestClient {
return this.syncTimePromise; return this.syncTimePromise;
} }
this.syncTimePromise = this.fetchTimeOffset().then(offset => { this.syncTimePromise = this.fetchTimeOffset().then((offset) => {
this.timeOffset = offset; this.timeOffset = offset;
this.syncTimePromise = null; this.syncTimePromise = null;
}); });
@@ -187,22 +273,27 @@ export default abstract class BaseRestClient {
return this.syncTimePromise; return this.syncTimePromise;
} }
abstract getServerTime(baseUrlKeyOverride?: string): Promise<number>;
/** /**
* Estimate drift based on client<->server latency * Estimate drift based on client<->server latency
*/ */
async fetchTimeOffset(): Promise<number> { async fetchTimeOffset(): Promise<number> {
try { try {
const start = Date.now(); const start = Date.now();
const serverTime = await this.getServerTime(); const serverTime = await this.fetchServerTime();
if (!serverTime || isNaN(serverTime)) {
throw new Error(
`fetchServerTime() returned non-number: "${serverTime}" typeof(${typeof serverTime})`
);
}
const end = Date.now(); const end = Date.now();
const avgDrift = ((end - start) / 2); const avgDrift = (end - start) / 2;
return Math.ceil(serverTime - end + avgDrift); return Math.ceil(serverTime - end + avgDrift);
} catch (e) { } catch (e) {
console.error('Failed to fetch get time offset: ', e); console.error('Failed to fetch get time offset: ', e);
return 0; return 0;
} }
} }
}; }

View File

@@ -9,14 +9,13 @@ type WsTopicList = Set<WsTopic>;
interface WsStoredState { interface WsStoredState {
ws?: WebSocket; ws?: WebSocket;
connectionState?: WsConnectionState; connectionState?: WsConnectionState;
activePingTimer?: NodeJS.Timeout | undefined; activePingTimer?: ReturnType<typeof setTimeout> | undefined;
activePongTimer?: NodeJS.Timeout | undefined; activePongTimer?: ReturnType<typeof setTimeout> | undefined;
subscribedTopics: WsTopicList; subscribedTopics: WsTopicList;
}; }
export default class WsStore { export default class WsStore {
private wsState: Record<string, WsStoredState> private wsState: Record<string, WsStoredState>;
private logger: typeof DefaultLogger; private logger: typeof DefaultLogger;
constructor(logger: typeof DefaultLogger) { constructor(logger: typeof DefaultLogger) {
@@ -40,11 +39,14 @@ export default class WsStore {
create(key: string): WsStoredState | undefined { create(key: string): WsStoredState | undefined {
if (this.hasExistingActiveConnection(key)) { if (this.hasExistingActiveConnection(key)) {
this.logger.warning('WsStore setConnection() overwriting existing open connection: ', this.getWs(key)); this.logger.warning(
'WsStore setConnection() overwriting existing open connection: ',
this.getWs(key)
);
} }
this.wsState[key] = { this.wsState[key] = {
subscribedTopics: new Set(), subscribedTopics: new Set(),
connectionState: WsConnectionState.READY_STATE_INITIAL connectionState: WsConnectionState.READY_STATE_INITIAL,
}; };
return this.get(key); return this.get(key);
} }
@@ -52,7 +54,10 @@ export default class WsStore {
delete(key: string) { delete(key: string) {
if (this.hasExistingActiveConnection(key)) { if (this.hasExistingActiveConnection(key)) {
const ws = this.getWs(key); const ws = this.getWs(key);
this.logger.warning('WsStore deleting state for connection still open: ', ws); this.logger.warning(
'WsStore deleting state for connection still open: ',
ws
);
ws?.close(); ws?.close();
} }
delete this.wsState[key]; delete this.wsState[key];
@@ -70,7 +75,10 @@ export default class WsStore {
setWs(key: string, wsConnection: WebSocket): WebSocket { setWs(key: string, wsConnection: WebSocket): WebSocket {
if (this.isWsOpen(key)) { if (this.isWsOpen(key)) {
this.logger.warning('WsStore setConnection() overwriting existing open connection: ', this.getWs(key)); this.logger.warning(
'WsStore setConnection() overwriting existing open connection: ',
this.getWs(key)
);
} }
this.get(key, true)!.ws = wsConnection; this.get(key, true)!.ws = wsConnection;
return wsConnection; return wsConnection;
@@ -80,7 +88,10 @@ export default class WsStore {
isWsOpen(key: string): boolean { isWsOpen(key: string): boolean {
const existingConnection = this.getWs(key); const existingConnection = this.getWs(key);
return !!existingConnection && existingConnection.readyState === existingConnection.OPEN; return (
!!existingConnection &&
existingConnection.readyState === existingConnection.OPEN
);
} }
getConnectionState(key: string): WsConnectionState { getConnectionState(key: string): WsConnectionState {

View File

@@ -1,5 +1,7 @@
export async function signMessage(
export async function signMessage(message: string, secret: string): Promise<string> { message: string,
secret: string
): Promise<string> {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const key = await window.crypto.subtle.importKey( const key = await window.crypto.subtle.importKey(
'raw', 'raw',
@@ -9,10 +11,15 @@ export async function signMessage(message: string, secret: string): Promise<stri
['sign'] ['sign']
); );
const signature = await window.crypto.subtle.sign('HMAC', key, encoder.encode(message)); const signature = await window.crypto.subtle.sign(
'HMAC',
key,
encoder.encode(message)
);
return Array.prototype.map.call( return Array.prototype.map
new Uint8Array(signature), .call(new Uint8Array(signature), (x: any) =>
(x: any) => ('00'+x.toString(16)).slice(-2) ('00' + x.toString(16)).slice(-2)
).join(''); )
}; .join('');
}

View File

@@ -1,7 +1,8 @@
import { createHmac } from 'crypto'; import { createHmac } from 'crypto';
export async function signMessage(message: string, secret: string): Promise<string> { export async function signMessage(
return createHmac('sha256', secret) message: string,
.update(message) secret: string
.digest('hex'); ): Promise<string> {
}; return createHmac('sha256', secret).update(message).digest('hex');
}

View File

@@ -19,25 +19,31 @@ export interface RestClientOptions {
parse_exceptions?: boolean; parse_exceptions?: boolean;
} }
export type GenericAPIResponse = Promise<any>; export function serializeParams(
params: object = {},
export function serializeParams(params: object = {}, strict_validation = false): string { strict_validation = false
): string {
return Object.keys(params) return Object.keys(params)
.sort() .sort()
.map(key => { .map((key) => {
const value = params[key]; const value = params[key];
if (strict_validation === true && typeof value === 'undefined') { if (strict_validation === true && typeof value === 'undefined') {
throw new Error('Failed to sign API request due to undefined parameter'); throw new Error(
'Failed to sign API request due to undefined parameter'
);
} }
return `${key}=${value}`; return `${key}=${value}`;
}) })
.join('&'); .join('&');
}; }
export function getRestBaseUrl(useLivenet: boolean, restInverseOptions: RestClientOptions) { export function getRestBaseUrl(
useLivenet: boolean,
restInverseOptions: RestClientOptions
) {
const baseUrlsInverse = { const baseUrlsInverse = {
livenet: 'https://api.bybit.com', livenet: 'https://api.bybit.com',
testnet: 'https://api-testnet.bybit.com' testnet: 'https://api-testnet.bybit.com',
}; };
if (restInverseOptions.baseUrl) { if (restInverseOptions.baseUrl) {
@@ -51,17 +57,24 @@ export function getRestBaseUrl(useLivenet: boolean, restInverseOptions: RestClie
} }
export function isPublicEndpoint(endpoint: string): boolean { export function isPublicEndpoint(endpoint: string): boolean {
if (endpoint.startsWith('v2/public')) { const publicPrefixes = [
'v2/public',
'public/linear',
'spot/quote/v1',
'spot/v1/symbols',
'spot/v1/time',
];
for (const prefix of publicPrefixes) {
if (endpoint.startsWith(prefix)) {
return true; return true;
} }
if (endpoint.startsWith('public/linear')) {
return true;
} }
return false; return false;
} }
export function isWsPong(response: any) { export function isWsPong(response: any) {
if (response.pong) { if (response.pong || response.ping) {
return true; return true;
} }
return ( return (
@@ -71,3 +84,15 @@ export function isWsPong(response: any) {
response.success === true response.success === true
); );
} }
export const agentSource = 'bybitapinode';
export const REST_CLIENT_TYPE_ENUM = {
inverse: 'inverse',
inverseFutures: 'inverseFutures',
linear: 'linear',
spot: 'spot',
} as const;
export type RestClientType =
typeof REST_CLIENT_TYPE_ENUM[keyof typeof REST_CLIENT_TYPE_ENUM];

View File

@@ -1,191 +0,0 @@
import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import { signMessage } from './node-support';
import { serializeParams, RestClientOptions, GenericAPIResponse, isPublicEndpoint } from './requestUtils';
export default class RequestUtil {
private timeOffset: number | null;
private syncTimePromise: null | Promise<any>;
private options: RestClientOptions;
private baseUrl: string;
private globalRequestOptions: AxiosRequestConfig;
private key: string | undefined;
private secret: string | undefined;
constructor(
key: string | undefined,
secret: string | undefined,
baseUrl: string,
options: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {}
) {
this.timeOffset = null;
this.syncTimePromise = null;
this.options = {
recv_window: 5000,
// how often to sync time drift with bybit servers
sync_interval_ms: 3600000,
// if true, we'll throw errors if any params are undefined
strict_param_validation: false,
...options
};
this.globalRequestOptions = {
// in ms == 5 minutes by default
timeout: 1000 * 60 * 5,
// custom request options based on axios specs - see: https://github.com/axios/axios#request-config
...requestOptions,
headers: {
'x-referer': 'bybitapinode'
},
};
this.baseUrl = baseUrl;
if (key && !secret) {
throw new Error('API Key & Secret are both required for private enpoints')
}
if (this.options.disable_time_sync !== true) {
this.syncTime();
setInterval(this.syncTime.bind(this), +this.options.sync_interval_ms!);
}
this.key = key;
this.secret = secret;
}
get<T>(endpoint: string, params?: any): Promise<T> {
return this._call('GET', endpoint, params);
}
post<T>(endpoint: string, params?: any): Promise<T> {
return this._call('POST', endpoint, params);
}
/**
* @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed.
*/
async _call<T>(method: Method, endpoint: string, params?: any): Promise<T> {
if (!isPublicEndpoint(endpoint)) {
if (!this.key || !this.secret) {
throw new Error('Private endpoints require api and private keys set');
}
if (this.timeOffset === null) {
await this.syncTime();
}
params = await this.signRequest(params);
}
const options = {
...this.globalRequestOptions,
url: [this.baseUrl, endpoint].join('/'),
method: method,
json: true
};
if (method === 'GET') {
options.params = params;
} else {
options.data = params;
}
return axios(options).then(response => {
if (response.status == 200) {
return response.data;
}
throw response;
}).catch(e => this.parseException(e));
}
/**
* @private generic handler to parse request exceptions
*/
parseException(e: any): unknown {
if (this.options.parse_exceptions === false) {
throw e;
}
// Something happened in setting up the request that triggered an Error
if (!e.response) {
if (!e.request) {
throw e.message;
}
// request made but no response received
throw e;
}
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
const response: AxiosResponse = e.response;
throw {
code: response.status,
message: response.statusText,
body: response.data,
headers: response.headers,
requestOptions: this.options
};
}
/**
* @private sign request and set recv window
*/
async signRequest(data: any): Promise<any> {
const params = {
...data,
api_key: this.key,
timestamp: Date.now() + (this.timeOffset || 0)
};
// Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen.
if (this.options.recv_window && !params.recv_window) {
params.recv_window = this.options.recv_window;
}
if (this.key && this.secret) {
const serializedParams = serializeParams(params, this.options.strict_param_validation);
params.sign = await signMessage(serializedParams, this.secret);
}
return params;
}
/**
* @private trigger time sync and store promise
*/
syncTime(): GenericAPIResponse {
if (this.options.disable_time_sync === true) {
return Promise.resolve(false);
}
if (this.syncTimePromise !== null) {
return this.syncTimePromise;
}
this.syncTimePromise = this.getTimeOffset().then(offset => {
this.timeOffset = offset;
this.syncTimePromise = null;
});
return this.syncTimePromise;
}
/**
* @deprecated move this somewhere else, because v2/public/time shouldn't be hardcoded here
*
* @returns {Promise<number>}
* @memberof RequestUtil
*/
async getTimeOffset(): Promise<number> {
const start = Date.now();
const result = await this.get<any>('v2/public/time');
const end = Date.now();
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
}
};

View File

@@ -12,20 +12,20 @@ import WsStore from './util/WsStore';
const inverseEndpoints = { const inverseEndpoints = {
livenet: 'wss://stream.bybit.com/realtime', livenet: 'wss://stream.bybit.com/realtime',
testnet: 'wss://stream-testnet.bybit.com/realtime' testnet: 'wss://stream-testnet.bybit.com/realtime',
}; };
const linearEndpoints = { const linearEndpoints = {
private: { private: {
livenet: 'wss://stream.bybit.com/realtime_private', livenet: 'wss://stream.bybit.com/realtime_private',
livenet2: 'wss://stream.bytick.com/realtime_private', livenet2: 'wss://stream.bytick.com/realtime_private',
testnet: 'wss://stream-testnet.bybit.com/realtime_private' testnet: 'wss://stream-testnet.bybit.com/realtime_private',
}, },
public: { public: {
livenet: 'wss://stream.bybit.com/realtime_public', livenet: 'wss://stream.bybit.com/realtime_public',
livenet2: 'wss://stream.bytick.com/realtime_public', livenet2: 'wss://stream.bytick.com/realtime_public',
testnet: 'wss://stream-testnet.bybit.com/realtime_public' testnet: 'wss://stream-testnet.bybit.com/realtime_public',
} },
}; };
const spotEndpoints = { const spotEndpoints = {
@@ -38,8 +38,8 @@ const spotEndpoints = {
livenet2: 'wss://stream.bybit.com/spot/quote/ws/v2', livenet2: 'wss://stream.bybit.com/spot/quote/ws/v2',
testnet: 'wss://stream-testnet.bybit.com/spot/quote/ws/v1', testnet: 'wss://stream-testnet.bybit.com/spot/quote/ws/v1',
testnet2: 'wss://stream-testnet.bybit.com/spot/quote/ws/v2', testnet2: 'wss://stream-testnet.bybit.com/spot/quote/ws/v2',
} },
} };
const loggerCategory = { category: 'bybit-ws' }; const loggerCategory = { category: 'bybit-ws' };
@@ -54,62 +54,71 @@ export enum WsConnectionState {
READY_STATE_CONNECTING, READY_STATE_CONNECTING,
READY_STATE_CONNECTED, READY_STATE_CONNECTED,
READY_STATE_CLOSING, READY_STATE_CLOSING,
READY_STATE_RECONNECTING READY_STATE_RECONNECTING,
}; }
export type APIMarket = 'inverse' | 'linear' | 'spot'; export type APIMarket = 'inverse' | 'linear' | 'spot';
// Same as inverse futures // Same as inverse futures
export type WsPublicInverseTopic = 'orderBookL2_25' export type WsPublicInverseTopic =
| 'orderBookL2_25'
| 'orderBookL2_200' | 'orderBookL2_200'
| 'trade' | 'trade'
| 'insurance' | 'insurance'
| 'instrument_info' | 'instrument_info'
| 'klineV2'; | 'klineV2';
export type WsPublicUSDTPerpTopic = 'orderBookL2_25' export type WsPublicUSDTPerpTopic =
| 'orderBookL2_25'
| 'orderBookL2_200' | 'orderBookL2_200'
| 'trade' | 'trade'
| 'insurance' | 'insurance'
| 'instrument_info' | 'instrument_info'
| 'kline'; | 'kline';
export type WsPublicSpotV1Topic = 'trade' export type WsPublicSpotV1Topic =
| 'trade'
| 'realtimes' | 'realtimes'
| 'kline' | 'kline'
| 'depth' | 'depth'
| 'mergedDepth' | 'mergedDepth'
| 'diffDepth'; | 'diffDepth';
export type WsPublicSpotV2Topic = 'depth' export type WsPublicSpotV2Topic =
| 'depth'
| 'kline' | 'kline'
| 'trade' | 'trade'
| 'bookTicker' | 'bookTicker'
| 'realtimes'; | 'realtimes';
export type WsPublicTopics = WsPublicInverseTopic export type WsPublicTopics =
| WsPublicInverseTopic
| WsPublicUSDTPerpTopic | WsPublicUSDTPerpTopic
| WsPublicSpotV1Topic | WsPublicSpotV1Topic
| WsPublicSpotV2Topic | WsPublicSpotV2Topic
| string; | string;
// Same as inverse futures // Same as inverse futures
export type WsPrivateInverseTopic = 'position' export type WsPrivateInverseTopic =
| 'position'
| 'execution' | 'execution'
| 'order' | 'order'
| 'stop_order'; | 'stop_order';
export type WsPrivateUSDTPerpTopic = 'position' export type WsPrivateUSDTPerpTopic =
| 'position'
| 'execution' | 'execution'
| 'order' | 'order'
| 'stop_order' | 'stop_order'
| 'wallet'; | 'wallet';
export type WsPrivateSpotTopic = 'outboundAccountInfo' export type WsPrivateSpotTopic =
| 'outboundAccountInfo'
| 'executionReport' | 'executionReport'
| 'ticketInfo'; | 'ticketInfo';
export type WsPrivateTopic = WsPrivateInverseTopic export type WsPrivateTopic =
| WsPrivateInverseTopic
| WsPrivateUSDTPerpTopic | WsPrivateUSDTPerpTopic
| WsPrivateSpotTopic | WsPrivateSpotTopic
| string; | string;
@@ -135,7 +144,7 @@ export interface WSClientConfigurableOptions {
restOptions?: any; restOptions?: any;
requestOptions?: any; requestOptions?: any;
wsUrl?: string; wsUrl?: string;
}; }
export interface WebsocketClientOptions extends WSClientConfigurableOptions { export interface WebsocketClientOptions extends WSClientConfigurableOptions {
livenet: boolean; livenet: boolean;
@@ -147,8 +156,7 @@ export interface WebsocketClientOptions extends WSClientConfigurableOptions {
pongTimeout: number; pongTimeout: number;
pingInterval: number; pingInterval: number;
reconnectTimeout: number; reconnectTimeout: number;
}; }
export const wsKeyInverse = 'inverse'; export const wsKeyInverse = 'inverse';
export const wsKeyLinearPrivate = 'linearPrivate'; export const wsKeyLinearPrivate = 'linearPrivate';
@@ -157,29 +165,54 @@ export const wsKeySpotPrivate = 'spotPrivate';
export const wsKeySpotPublic = 'spotPublic'; export const wsKeySpotPublic = 'spotPublic';
// This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets) // This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets)
export type WsKey = 'inverse' | 'linearPrivate' | 'linearPublic' | 'spotPrivate' | 'spotPublic'; export type WsKey =
| 'inverse'
| 'linearPrivate'
| 'linearPublic'
| 'spotPrivate'
| 'spotPublic';
const getLinearWsKeyForTopic = (topic: string): WsKey => { const getLinearWsKeyForTopic = (topic: string): WsKey => {
const privateLinearTopics = ['position', 'execution', 'order', 'stop_order', 'wallet']; const privateLinearTopics = [
'position',
'execution',
'order',
'stop_order',
'wallet',
];
if (privateLinearTopics.includes(topic)) { if (privateLinearTopics.includes(topic)) {
return wsKeyLinearPrivate; return wsKeyLinearPrivate;
} }
return wsKeyLinearPublic; return wsKeyLinearPublic;
} };
const getSpotWsKeyForTopic = (topic: string): WsKey => { const getSpotWsKeyForTopic = (topic: string): WsKey => {
const privateLinearTopics = ['position', 'execution', 'order', 'stop_order', 'outboundAccountInfo', 'executionReport', 'ticketInfo']; const privateLinearTopics = [
'position',
'execution',
'order',
'stop_order',
'outboundAccountInfo',
'executionReport',
'ticketInfo',
];
if (privateLinearTopics.includes(topic)) { if (privateLinearTopics.includes(topic)) {
return wsKeySpotPrivate; return wsKeySpotPrivate;
} }
return wsKeySpotPublic; return wsKeySpotPublic;
} };
export declare interface WebsocketClient { export declare interface WebsocketClient {
on(event: 'open' | 'reconnected', listener: ({ wsKey: WsKey, event: any }) => void): this; on(
on(event: 'response' | 'update' | 'error', listener: (response: any) => void): this; event: 'open' | 'reconnected',
listener: ({ wsKey: WsKey, event: any }) => void
): this;
on(
event: 'response' | 'update' | 'error',
listener: (response: any) => void
): this;
on(event: 'reconnect' | 'close', listener: ({ wsKey: WsKey }) => void): this; on(event: 'reconnect' | 'close', listener: ({ wsKey: WsKey }) => void): this;
} }
@@ -196,7 +229,10 @@ export class WebsocketClient extends EventEmitter {
private options: WebsocketClientOptions; private options: WebsocketClientOptions;
private wsStore: WsStore; private wsStore: WsStore;
constructor(options: WSClientConfigurableOptions, logger?: typeof DefaultLogger) { constructor(
options: WSClientConfigurableOptions,
logger?: typeof DefaultLogger
) {
super(); super();
this.logger = logger || DefaultLogger; this.logger = logger || DefaultLogger;
@@ -207,7 +243,7 @@ export class WebsocketClient extends EventEmitter {
pongTimeout: 1000, pongTimeout: 1000,
pingInterval: 10000, pingInterval: 10000,
reconnectTimeout: 500, reconnectTimeout: 500,
...options ...options,
}; };
if (!this.options.market) { if (!this.options.market) {
@@ -215,13 +251,31 @@ export class WebsocketClient extends EventEmitter {
} }
if (this.isLinear()) { if (this.isLinear()) {
this.restClient = new LinearClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions); this.restClient = new LinearClient(
undefined,
undefined,
this.isLivenet(),
this.options.restOptions,
this.options.requestOptions
);
} else if (this.isSpot()) { } else if (this.isSpot()) {
// TODO: spot client // TODO: spot client
this.restClient = new LinearClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions); this.restClient = new LinearClient(
undefined,
undefined,
this.isLivenet(),
this.options.restOptions,
this.options.requestOptions
);
this.connectPublic(); this.connectPublic();
} else { } else {
this.restClient = new InverseClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions); this.restClient = new InverseClient(
undefined,
undefined,
this.isLivenet(),
this.options.restOptions,
this.options.requestOptions
);
} }
} }
@@ -246,10 +300,9 @@ export class WebsocketClient extends EventEmitter {
*/ */
public subscribe(wsTopics: WsTopic[] | WsTopic) { public subscribe(wsTopics: WsTopic[] | WsTopic) {
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach(topic => this.wsStore.addTopic( topics.forEach((topic) =>
this.getWsKeyForTopic(topic), this.wsStore.addTopic(this.getWsKeyForTopic(topic), topic)
topic );
));
// attempt to send subscription topic per websocket // attempt to send subscription topic per websocket
this.wsStore.getKeys().forEach((wsKey: WsKey) => { this.wsStore.getKeys().forEach((wsKey: WsKey) => {
@@ -273,10 +326,9 @@ export class WebsocketClient extends EventEmitter {
*/ */
public unsubscribe(wsTopics: WsTopic[] | WsTopic) { public unsubscribe(wsTopics: WsTopic[] | WsTopic) {
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach(topic => this.wsStore.deleteTopic( topics.forEach((topic) =>
this.getWsKeyForTopic(topic), this.wsStore.deleteTopic(this.getWsKeyForTopic(topic), topic)
topic );
));
this.wsStore.getKeys().forEach((wsKey: WsKey) => { this.wsStore.getKeys().forEach((wsKey: WsKey) => {
// unsubscribe request only necessary if active connection exists // unsubscribe request only necessary if active connection exists
@@ -303,7 +355,10 @@ export class WebsocketClient extends EventEmitter {
} }
if (this.isLinear()) { if (this.isLinear()) {
return [this.connect(wsKeyLinearPublic), this.connect(wsKeyLinearPrivate)]; return [
this.connect(wsKeyLinearPublic),
this.connect(wsKeyLinearPrivate),
];
} }
if (this.isSpot()) { if (this.isSpot()) {
@@ -342,12 +397,18 @@ export class WebsocketClient extends EventEmitter {
private async connect(wsKey: WsKey): Promise<WebSocket | undefined> { private async connect(wsKey: WsKey): Promise<WebSocket | undefined> {
try { try {
if (this.wsStore.isWsOpen(wsKey)) { if (this.wsStore.isWsOpen(wsKey)) {
this.logger.error('Refused to connect to ws with existing active connection', { ...loggerCategory, wsKey }) this.logger.error(
'Refused to connect to ws with existing active connection',
{ ...loggerCategory, wsKey }
);
return this.wsStore.getWs(wsKey); return this.wsStore.getWs(wsKey);
} }
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) { if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) {
this.logger.error('Refused to connect to ws, connection attempt already active', { ...loggerCategory, wsKey }) this.logger.error(
'Refused to connect to ws, connection attempt already active',
{ ...loggerCategory, wsKey }
);
return; return;
} }
@@ -377,11 +438,17 @@ export class WebsocketClient extends EventEmitter {
switch (error.message) { switch (error.message) {
case 'Unexpected server response: 401': case 'Unexpected server response: 401':
this.logger.error(`${context} due to 401 authorization failure.`, { ...loggerCategory, wsKey }); this.logger.error(`${context} due to 401 authorization failure.`, {
...loggerCategory,
wsKey,
});
break; break;
default: default:
this.logger.error(`{context} due to unexpected response error: ${error.msg}`, { ...loggerCategory, wsKey }); this.logger.error(
`{context} due to unexpected response error: ${error.msg}`,
{ ...loggerCategory, wsKey }
);
break; break;
} }
} }
@@ -392,23 +459,39 @@ export class WebsocketClient extends EventEmitter {
private async getAuthParams(wsKey: WsKey): Promise<string> { private async getAuthParams(wsKey: WsKey): Promise<string> {
const { key, secret } = this.options; const { key, secret } = this.options;
if (key && secret && wsKey !== wsKeyLinearPublic && wsKey !== wsKeySpotPublic) { if (
this.logger.debug('Getting auth\'d request params', { ...loggerCategory, wsKey }); key &&
secret &&
wsKey !== wsKeyLinearPublic &&
wsKey !== wsKeySpotPublic
) {
this.logger.debug("Getting auth'd request params", {
...loggerCategory,
wsKey,
});
const timeOffset = await this.restClient.getTimeOffset(); const timeOffset = await this.restClient.fetchTimeOffset();
const params: any = { const params: any = {
api_key: this.options.key, api_key: this.options.key,
expires: (Date.now() + timeOffset + 5000) expires: Date.now() + timeOffset + 5000,
}; };
params.signature = await signMessage('GET/realtime' + params.expires, secret); params.signature = await signMessage(
'GET/realtime' + params.expires,
secret
);
return '?' + serializeParams(params); return '?' + serializeParams(params);
} else if (!key || !secret) { } else if (!key || !secret) {
this.logger.warning('Connot authenticate websocket, either api or private keys missing.', { ...loggerCategory, wsKey }); this.logger.warning(
'Connot authenticate websocket, either api or private keys missing.',
{ ...loggerCategory, wsKey }
);
} else { } else {
this.logger.debug('Starting public only websocket client.', { ...loggerCategory, wsKey }); this.logger.debug('Starting public only websocket client.', {
...loggerCategory,
wsKey,
});
} }
return ''; return '';
@@ -421,7 +504,10 @@ export class WebsocketClient extends EventEmitter {
} }
setTimeout(() => { setTimeout(() => {
this.logger.info('Reconnecting to websocket', { ...loggerCategory, wsKey }); this.logger.info('Reconnecting to websocket', {
...loggerCategory,
wsKey,
});
this.connect(wsKey); this.connect(wsKey);
}, connectionDelayMs); }, connectionDelayMs);
} }
@@ -433,7 +519,10 @@ export class WebsocketClient extends EventEmitter {
this.tryWsSend(wsKey, JSON.stringify({ op: 'ping' })); this.tryWsSend(wsKey, JSON.stringify({ op: 'ping' }));
this.wsStore.get(wsKey, true)!.activePongTimer = setTimeout(() => { this.wsStore.get(wsKey, true)!.activePongTimer = setTimeout(() => {
this.logger.info('Pong timeout - closing socket to reconnect', { ...loggerCategory, wsKey }); this.logger.info('Pong timeout - closing socket to reconnect', {
...loggerCategory,
wsKey,
});
this.getWs(wsKey)?.close(); this.getWs(wsKey)?.close();
}, this.options.pongTimeout); }, this.options.pongTimeout);
} }
@@ -470,7 +559,7 @@ export class WebsocketClient extends EventEmitter {
} }
const wsMessage = JSON.stringify({ const wsMessage = JSON.stringify({
op: 'subscribe', op: 'subscribe',
args: topics args: topics,
}); });
this.tryWsSend(wsKey, wsMessage); this.tryWsSend(wsKey, wsMessage);
@@ -485,7 +574,7 @@ export class WebsocketClient extends EventEmitter {
} }
const wsMessage = JSON.stringify({ const wsMessage = JSON.stringify({
op: 'unsubscribe', op: 'unsubscribe',
args: topics args: topics,
}); });
this.tryWsSend(wsKey, wsMessage); this.tryWsSend(wsKey, wsMessage);
@@ -493,38 +582,62 @@ export class WebsocketClient extends EventEmitter {
private tryWsSend(wsKey: WsKey, wsMessage: string) { private tryWsSend(wsKey: WsKey, wsMessage: string) {
try { try {
this.logger.silly(`Sending upstream ws message: `, { ...loggerCategory, wsMessage, wsKey }); this.logger.silly(`Sending upstream ws message: `, {
...loggerCategory,
wsMessage,
wsKey,
});
if (!wsKey) { if (!wsKey) {
throw new Error('Cannot send message due to no known websocket for this wsKey'); throw new Error(
'Cannot send message due to no known websocket for this wsKey'
);
} }
const ws = this.getWs(wsKey); const ws = this.getWs(wsKey);
if (!ws) { if (!ws) {
throw new Error(`${wsKey} socket not connected yet, call "connect(${wsKey}) first then try again when the "open" event arrives`); throw new Error(
`${wsKey} socket not connected yet, call "connect(${wsKey}) first then try again when the "open" event arrives`
);
} }
ws.send(wsMessage); ws.send(wsMessage);
} catch (e) { } catch (e) {
this.logger.error(`Failed to send WS message`, { ...loggerCategory, wsMessage, wsKey, exception: e }); this.logger.error(`Failed to send WS message`, {
...loggerCategory,
wsMessage,
wsKey,
exception: e,
});
} }
} }
private connectToWsUrl(url: string, wsKey: WsKey): WebSocket { private connectToWsUrl(url: string, wsKey: WsKey): WebSocket {
this.logger.silly(`Opening WS connection to URL: ${url}`, { ...loggerCategory, wsKey }) this.logger.silly(`Opening WS connection to URL: ${url}`, {
...loggerCategory,
wsKey,
});
const agent = this.options.requestOptions?.agent; const agent = this.options.requestOptions?.agent;
const ws = new WebSocket(url, undefined, agent ? { agent } : undefined); const ws = new WebSocket(url, undefined, agent ? { agent } : undefined);
ws.onopen = event => this.onWsOpen(event, wsKey); ws.onopen = (event) => this.onWsOpen(event, wsKey);
ws.onmessage = event => this.onWsMessage(event, wsKey); ws.onmessage = (event) => this.onWsMessage(event, wsKey);
ws.onerror = event => this.onWsError(event, wsKey); ws.onerror = (event) => this.onWsError(event, wsKey);
ws.onclose = event => this.onWsClose(event, wsKey); ws.onclose = (event) => this.onWsClose(event, wsKey);
return ws; return ws;
} }
private onWsOpen(event, wsKey: WsKey) { private onWsOpen(event, wsKey: WsKey) {
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) { if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) {
this.logger.info('Websocket connected', { ...loggerCategory, wsKey, livenet: this.isLivenet(), linear: this.isLinear(), spot: this.isSpot() }); this.logger.info('Websocket connected', {
...loggerCategory,
wsKey,
livenet: this.isLivenet(),
linear: this.isLinear(),
spot: this.isSpot(),
});
this.emit('open', { wsKey, event }); this.emit('open', { wsKey, event });
} else if (this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)) { } else if (
this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)
) {
this.logger.info('Websocket reconnected', { ...loggerCategory, wsKey }); this.logger.info('Websocket reconnected', { ...loggerCategory, wsKey });
this.emit('reconnected', { wsKey, event }); this.emit('reconnected', { wsKey, event });
} }
@@ -547,16 +660,26 @@ export class WebsocketClient extends EventEmitter {
// any message can clear the pong timer - wouldn't get a message if the ws dropped // any message can clear the pong timer - wouldn't get a message if the ws dropped
this.clearPongTimer(wsKey); this.clearPongTimer(wsKey);
const msg = JSON.parse(event && event.data || event); const msg = JSON.parse((event && event.data) || event);
if ('success' in msg || msg?.pong) { if ('success' in msg || msg?.pong) {
this.onWsMessageResponse(msg, wsKey); this.onWsMessageResponse(msg, wsKey);
} else if (msg.topic) { } else if (msg.topic) {
this.onWsMessageUpdate(msg); this.onWsMessageUpdate(msg);
} else { } else {
this.logger.warning('Got unhandled ws message', { ...loggerCategory, message: msg, event, wsKey}); this.logger.warning('Got unhandled ws message', {
...loggerCategory,
message: msg,
event,
wsKey,
});
} }
} catch (e) { } catch (e) {
this.logger.error('Failed to parse ws event message', { ...loggerCategory, error: e, event, wsKey}) this.logger.error('Failed to parse ws event message', {
...loggerCategory,
error: e,
event,
wsKey,
});
} }
} }
@@ -568,7 +691,10 @@ export class WebsocketClient extends EventEmitter {
} }
private onWsClose(event, wsKey: WsKey) { private onWsClose(event, wsKey: WsKey) {
this.logger.info('Websocket connection closed', { ...loggerCategory, wsKey}); this.logger.info('Websocket connection closed', {
...loggerCategory,
wsKey,
});
if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CLOSING) { if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CLOSING) {
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!); this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!);
@@ -615,7 +741,10 @@ export class WebsocketClient extends EventEmitter {
return linearEndpoints.private[networkKey]; return linearEndpoints.private[networkKey];
} }
this.logger.error('Unhandled linear wsKey: ', { ...loggerCategory, wsKey }); this.logger.error('Unhandled linear wsKey: ', {
...loggerCategory,
wsKey,
});
return linearEndpoints[networkKey]; return linearEndpoints[networkKey];
} }
@@ -641,13 +770,15 @@ export class WebsocketClient extends EventEmitter {
return wsKeyInverse; return wsKeyInverse;
} }
if (this.isLinear()) { if (this.isLinear()) {
return getLinearWsKeyForTopic(topic) return getLinearWsKeyForTopic(topic);
} }
return getSpotWsKeyForTopic(topic); return getSpotWsKeyForTopic(topic);
} }
private wrongMarketError(market: APIMarket) { private wrongMarketError(market: APIMarket) {
return new Error(`This WS client was instanced for the ${this.options.market} market. Make another WebsocketClient instance with "market: '${market}' to listen to spot topics`); return new Error(
`This WS client was instanced for the ${this.options.market} market. Make another WebsocketClient instance with "market: '${market}' to listen to spot topics`
);
} }
// TODO: persistance for subbed topics. Look at ftx-api implementation. // TODO: persistance for subbed topics. Look at ftx-api implementation.
@@ -656,14 +787,17 @@ export class WebsocketClient extends EventEmitter {
throw this.wrongMarketError('spot'); throw this.wrongMarketError('spot');
} }
return this.tryWsSend(wsKeySpotPublic, JSON.stringify({ return this.tryWsSend(
wsKeySpotPublic,
JSON.stringify({
topic: 'trade', topic: 'trade',
event: 'sub', event: 'sub',
symbol, symbol,
params: { params: {
binary: !!binary, binary: !!binary,
} },
})); })
);
} }
public subscribePublicSpotTradingPair(symbol: string, binary?: boolean) { public subscribePublicSpotTradingPair(symbol: string, binary?: boolean) {
@@ -671,35 +805,50 @@ export class WebsocketClient extends EventEmitter {
throw this.wrongMarketError('spot'); throw this.wrongMarketError('spot');
} }
return this.tryWsSend(wsKeySpotPublic, JSON.stringify({ return this.tryWsSend(
wsKeySpotPublic,
JSON.stringify({
symbol, symbol,
topic: 'realtimes', topic: 'realtimes',
event: 'sub', event: 'sub',
params: { params: {
binary: !!binary, binary: !!binary,
}, },
})); })
);
} }
public subscribePublicSpotV1Kline(symbol: string, candleSize: KlineInterval, binary?: boolean) { public subscribePublicSpotV1Kline(
symbol: string,
candleSize: KlineInterval,
binary?: boolean
) {
if (!this.isSpot()) { if (!this.isSpot()) {
throw this.wrongMarketError('spot'); throw this.wrongMarketError('spot');
} }
return this.tryWsSend(wsKeySpotPublic, JSON.stringify({ return this.tryWsSend(
wsKeySpotPublic,
JSON.stringify({
symbol, symbol,
topic: 'kline_' + candleSize, topic: 'kline_' + candleSize,
event: 'sub', event: 'sub',
params: { params: {
binary: !!binary, binary: !!binary,
}, },
})); })
);
} }
//ws.send('{"symbol":"BTCUSDT","topic":"depth","event":"sub","params":{"binary":false}}'); //ws.send('{"symbol":"BTCUSDT","topic":"depth","event":"sub","params":{"binary":false}}');
//ws.send('{"symbol":"BTCUSDT","topic":"mergedDepth","event":"sub","params":{"binary":false,"dumpScale":1}}'); //ws.send('{"symbol":"BTCUSDT","topic":"mergedDepth","event":"sub","params":{"binary":false,"dumpScale":1}}');
//ws.send('{"symbol":"BTCUSDT","topic":"diffDepth","event":"sub","params":{"binary":false}}'); //ws.send('{"symbol":"BTCUSDT","topic":"diffDepth","event":"sub","params":{"binary":false}}');
public subscribePublicSpotOrderbook(symbol: string, depth: 'full' | 'merge' | 'delta', dumpScale?: number, binary?: boolean) { public subscribePublicSpotOrderbook(
symbol: string,
depth: 'full' | 'merge' | 'delta',
dumpScale?: number,
binary?: boolean
) {
if (!this.isSpot()) { if (!this.isSpot()) {
throw this.wrongMarketError('spot'); throw this.wrongMarketError('spot');
} }
@@ -709,7 +858,7 @@ export class WebsocketClient extends EventEmitter {
case 'full': { case 'full': {
topic = 'depth'; topic = 'depth';
break; break;
}; }
case 'merge': { case 'merge': {
topic = 'mergedDepth'; topic = 'mergedDepth';
if (!dumpScale) { if (!dumpScale) {
@@ -736,5 +885,4 @@ export class WebsocketClient extends EventEmitter {
} }
return this.tryWsSend(wsKeySpotPublic, JSON.stringify(msg)); return this.tryWsSend(wsKeySpotPublic, JSON.stringify(msg));
} }
}
};

View File

@@ -0,0 +1,105 @@
import { InverseFuturesClient } from '../../src/inverse-futures-client';
import { successResponseList, successResponseObject } from '../response.util';
describe('Public Inverse-Futures REST API GET Endpoints', () => {
const useLivenet = true;
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
const api = new InverseFuturesClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
// Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol!
const symbol = 'BTCUSDU22';
it('getApiKeyInfo()', async () => {
expect(await api.getApiKeyInfo()).toMatchObject(successResponseObject());
});
it('getWalletBalance()', async () => {
expect(await api.getWalletBalance()).toMatchObject(successResponseObject());
});
it('getWalletFundRecords()', async () => {
expect(await api.getWalletFundRecords()).toMatchObject(
successResponseObject()
);
});
it('getWithdrawRecords()', async () => {
expect(await api.getWithdrawRecords()).toMatchObject(
successResponseObject()
);
});
it('getAssetExchangeRecords()', async () => {
expect(await api.getAssetExchangeRecords()).toMatchObject(
successResponseList()
);
});
it('getActiveOrderList()', async () => {
expect(await api.getActiveOrderList({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryActiveOrder()', async () => {
expect(await api.queryActiveOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getConditionalOrder()', async () => {
expect(await api.getConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryConditionalOrder()', async () => {
expect(await api.queryConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getPosition()', async () => {
expect(await api.getPosition()).toMatchObject(successResponseObject());
});
it('getTradeRecords()', async () => {
expect(await api.getTradeRecords({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getClosedPnl()', async () => {
expect(await api.getClosedPnl({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getRiskLimitList()', async () => {
expect(await api.getRiskLimitList()).toMatchObject(
successResponseList('ok')
);
});
it('getMyLastFundingFee()', async () => {
expect(await api.getMyLastFundingFee({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getPredictedFunding()', async () => {
expect(await api.getPredictedFunding({ symbol: 'BTCUSD' })).toMatchObject(
successResponseObject()
);
});
it('getLcpInfo()', async () => {
expect(await api.getLcpInfo({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
});

View File

@@ -0,0 +1,198 @@
import { API_ERROR_CODE, InverseFuturesClient } from '../../src';
import { successResponseObject } from '../response.util';
describe('Private Inverse-Futures REST API POST Endpoints', () => {
const useLivenet = true;
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
});
const api = new InverseFuturesClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
// Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol!
const symbol = 'BTCUSDU22';
// These tests are primarily check auth is working by expecting balance or order not found style errors
it('placeActiveOrder()', async () => {
expect(
await api.placeActiveOrder({
side: 'Buy',
symbol,
order_type: 'Limit',
price: 30000,
qty: 1,
time_in_force: 'GoodTillCancel',
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_IDX_NOT_MATCH_POSITION_MODE,
ret_msg: 'position idx not match position mode',
});
});
it('cancelActiveOrder()', async () => {
expect(
await api.cancelActiveOrder({
symbol,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllActiveOrders()', async () => {
expect(
await api.cancelAllActiveOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceActiveOrder()', async () => {
expect(
await api.replaceActiveOrder({
symbol,
order_id: '123123123',
p_r_qty: '1',
p_r_price: '30000',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to replace',
});
});
it('placeConditionalOrder()', async () => {
expect(
await api.placeConditionalOrder({
order_type: 'Limit',
side: 'Buy',
symbol,
qty: '1',
price: '8100',
base_price: '8300',
stop_px: '8150',
time_in_force: 'GoodTillCancel',
order_link_id: 'cus_order_id_1',
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_IDX_NOT_MATCH_POSITION_MODE,
ret_msg: 'position idx not match position mode',
});
});
it('cancelConditionalOrder()', async () => {
expect(
await api.cancelConditionalOrder({
symbol,
order_link_id: 'lkasmdflasd',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllConditionalOrders()', async () => {
expect(
await api.cancelAllConditionalOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceConditionalOrder()', async () => {
expect(
await api.replaceConditionalOrder({
symbol,
p_r_price: '50000',
p_r_qty: 1,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to replace',
});
});
it('changePositionMargin()', async () => {
expect(
await api.changePositionMargin({
symbol,
margin: '10',
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_IDX_NOT_MATCH_POSITION_MODE,
ret_msg: 'position idx not match position mode',
});
});
it('setTradingStop()', async () => {
expect(
await api.setTradingStop({
symbol,
take_profit: 50000,
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_STATUS_NOT_NORMAL,
ret_msg: 'position status is not normal',
});
});
it('setUserLeverage()', async () => {
expect(
await api.setUserLeverage({
symbol,
buy_leverage: 5,
sell_leverage: 5,
})
).toMatchObject({
ret_code: API_ERROR_CODE.LEVERAGE_NOT_MODIFIED,
ret_msg: 'leverage not modified',
});
});
it('setPositionMode()', async () => {
expect(
await api.setPositionMode({
symbol,
mode: 3,
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_MODE_NOT_MODIFIED,
ret_msg: 'position mode not modified',
});
});
it('setMarginType()', async () => {
expect(
await api.setMarginType({
symbol,
is_isolated: false,
buy_leverage: 5,
sell_leverage: 5,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ISOLATED_NOT_MODIFIED,
ret_msg: 'Isolated not modified',
});
});
it('setRiskLimit()', async () => {
expect(
await api.setRiskLimit({
symbol,
risk_id: 'myriskid',
})
).toMatchObject({
ret_code: -1,
ret_msg: `Currently not support symbol[${symbol}]`,
});
});
});

View File

@@ -1,66 +1,93 @@
import { InverseFuturesClient } from "../../src/inverse-futures-client"; import { InverseFuturesClient } from '../../src/inverse-futures-client';
import { notAuthenticatedError, successResponseList, successResponseObject } from "../response.util"; import {
notAuthenticatedError,
successResponseList,
successResponseObject,
} from '../response.util';
describe('Public Inverse Futures REST API Endpoints', () => { describe('Public Inverse Futures REST API Endpoints', () => {
const useLivenet = true; const useLivenet = true;
const api = new InverseFuturesClient(undefined, undefined, useLivenet, { disable_time_sync: true }); const api = new InverseFuturesClient(undefined, undefined, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSD'; const symbol = 'BTCUSD';
const interval = '15'; const interval = '15';
const timestampOneHourAgo = (new Date().getTime() / 1000) - (1000 * 60 * 60); const timestampOneHourAgo = new Date().getTime() / 1000 - 1000 * 60 * 60;
const from = Number(timestampOneHourAgo.toFixed(0)); const from = Number(timestampOneHourAgo.toFixed(0));
describe('Inverse-Futures only endpoints', () => { describe('Inverse-Futures only endpoints', () => {
it('should throw for unauthenticated private calls', async () => { it('should throw for unauthenticated private calls', async () => {
expect(() => api.getPosition()).rejects.toMatchObject(notAuthenticatedError()); expect(() => api.getPosition()).rejects.toMatchObject(
}); notAuthenticatedError()
);
it('getKline()', async () => { expect(() => api.getApiKeyInfo()).rejects.toMatchObject(
expect( notAuthenticatedError()
await api.getKline({ symbol, interval, from }) );
).toMatchObject(successResponseList());
});
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(await api.getIndexPriceKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(await api.getPremiumIndexKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(successResponseObject());
});
});
describe('Shared endpoints', () => {
it('should throw for unauthenticated private calls', async () => {
expect(() => api.getApiKeyInfo()).rejects.toMatchObject(notAuthenticatedError());
}); });
it('getOrderBook()', async () => { it('getOrderBook()', async () => {
expect(await api.getOrderBook({ symbol })).toMatchObject(successResponseList()); expect(await api.getOrderBook({ symbol })).toMatchObject(
successResponseList()
);
});
it('getKline()', async () => {
expect(await api.getKline({ symbol, interval, from })).toMatchObject(
successResponseList()
);
}); });
it('getTickers()', async () => { it('getTickers()', async () => {
expect(await api.getTickers()).toMatchObject(successResponseList()); expect(await api.getTickers()).toMatchObject(successResponseList());
}); });
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(
successResponseList()
);
});
it('getSymbols()', async () => { it('getSymbols()', async () => {
expect(await api.getSymbols()).toMatchObject(successResponseList()); expect(await api.getSymbols()).toMatchObject(successResponseList());
}); });
it('getMarkPriceKline()', async () => {
expect(
await api.getMarkPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(
await api.getIndexPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(
await api.getPremiumIndexKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(
successResponseObject()
);
});
it('getServerTime()', async () => { it('getServerTime()', async () => {
expect(await api.getServerTime()).toMatchObject(successResponseObject()); expect(await api.getServerTime()).toMatchObject(successResponseObject());
}); });
it('fetchServertime() returns number', async () => {
expect(await api.fetchServerTime()).toStrictEqual(expect.any(Number));
});
it('getApiAnnouncements()', async () => { it('getApiAnnouncements()', async () => {
expect(await api.getApiAnnouncements()).toMatchObject(successResponseList()); expect(await api.getApiAnnouncements()).toMatchObject(
successResponseList()
);
}); });
}); });
}); });

View File

@@ -0,0 +1,103 @@
import { InverseClient } from '../../src/inverse-client';
import { successResponseList, successResponseObject } from '../response.util';
describe('Private Inverse REST API Endpoints', () => {
const useLivenet = true;
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
});
const api = new InverseClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSD';
it('getApiKeyInfo()', async () => {
expect(await api.getApiKeyInfo()).toMatchObject(successResponseObject());
});
it('getWalletBalance()', async () => {
expect(await api.getWalletBalance()).toMatchObject(successResponseObject());
});
it('getWalletFundRecords()', async () => {
expect(await api.getWalletFundRecords()).toMatchObject(
successResponseObject()
);
});
it('getWithdrawRecords()', async () => {
expect(await api.getWithdrawRecords()).toMatchObject(
successResponseObject()
);
});
it('getAssetExchangeRecords()', async () => {
expect(await api.getAssetExchangeRecords()).toMatchObject(
successResponseList()
);
});
it('getActiveOrderList()', async () => {
expect(await api.getActiveOrderList({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryActiveOrder()', async () => {
expect(await api.queryActiveOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getConditionalOrder()', async () => {
expect(await api.getConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryConditionalOrder()', async () => {
expect(await api.queryConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getPosition()', async () => {
expect(await api.getPosition()).toMatchObject(successResponseObject());
});
it('getTradeRecords()', async () => {
expect(await api.getTradeRecords({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getRiskLimitList()', async () => {
expect(await api.getRiskLimitList()).toMatchObject(
successResponseList('ok')
);
});
it('getClosedPnl()', async () => {
expect(await api.getClosedPnl({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getMyLastFundingFee()', async () => {
expect(await api.getMyLastFundingFee({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getLcpInfo()', async () => {
expect(await api.getLcpInfo({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
});

View File

@@ -0,0 +1,193 @@
import { API_ERROR_CODE } from '../../src';
import { InverseClient } from '../../src/inverse-client';
import { successResponseObject } from '../response.util';
describe('Private Inverse REST API Endpoints', () => {
const useLivenet = true;
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
});
const api = new InverseClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSD';
// These tests are primarily check auth is working by expecting balance or order not found style errors
it('placeActiveOrder()', async () => {
expect(
await api.placeActiveOrder({
side: 'Buy',
symbol,
order_type: 'Limit',
price: 30000,
qty: 1,
time_in_force: 'GoodTillCancel',
})
).toMatchObject({
ret_code: API_ERROR_CODE.INSUFFICIENT_BALANCE_FOR_ORDER_COST,
});
});
it('cancelActiveOrder()', async () => {
expect(
await api.cancelActiveOrder({
symbol,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllActiveOrders()', async () => {
expect(
await api.cancelAllActiveOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceActiveOrder()', async () => {
expect(
await api.replaceActiveOrder({
symbol,
order_id: '123123123',
p_r_qty: 1,
p_r_price: '30000',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to replace',
});
});
it('placeConditionalOrder()', async () => {
expect(
await api.placeConditionalOrder({
order_type: 'Limit',
side: 'Buy',
symbol,
qty: '1',
price: '8100',
base_price: '8300',
stop_px: '8150',
time_in_force: 'GoodTillCancel',
order_link_id: 'cus_order_id_1',
})
).toMatchObject({
ret_code: API_ERROR_CODE.INSUFFICIENT_BALANCE,
ret_msg: 'Insufficient wallet balance',
});
});
it('cancelConditionalOrder()', async () => {
expect(
await api.cancelConditionalOrder({
symbol,
order_link_id: 'lkasmdflasd',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllConditionalOrders()', async () => {
expect(
await api.cancelAllConditionalOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceConditionalOrder()', async () => {
expect(
await api.replaceConditionalOrder({
symbol,
p_r_price: '50000',
p_r_qty: 1,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to replace',
});
});
it('changePositionMargin()', async () => {
expect(
await api.changePositionMargin({
symbol,
margin: '10',
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_IS_CROSS_MARGIN,
ret_msg: 'position is in crossMargin',
});
});
it('setTradingStop()', async () => {
expect(
await api.setTradingStop({
symbol,
take_profit: 5555,
})
).toMatchObject({
ret_code: API_ERROR_CODE.CANNOT_SET_TRADING_STOP_FOR_ZERO_POS,
ret_msg: 'can not set tp/sl/ts for zero position',
});
});
it('setUserLeverage()', async () => {
expect(
await api.setUserLeverage({
symbol,
leverage: 5,
})
).toMatchObject({
result: 5,
ret_code: 0,
});
});
it('setSlTpPositionMode()', async () => {
expect(
await api.setSlTpPositionMode({
symbol,
tp_sl_mode: 'Full',
})
).toMatchObject({
ret_code: API_ERROR_CODE.SAME_SLTP_MODE,
ret_msg: 'same tp sl mode2',
});
});
it('setMarginType()', async () => {
expect(
await api.setMarginType({
symbol,
is_isolated: false,
buy_leverage: 5,
sell_leverage: 5,
})
).toMatchObject(successResponseObject());
});
it('setRiskLimit()', async () => {
expect(
await api.setRiskLimit({
symbol,
risk_id: 'myriskid',
})
).toMatchObject({
ret_code: API_ERROR_CODE.RISK_LIMIT_NOT_EXISTS,
ret_msg: 'risk limit not exists',
});
});
});

View File

@@ -1,66 +1,93 @@
import { InverseClient } from "../../src/inverse-client"; import { InverseClient } from '../../src/inverse-client';
import { notAuthenticatedError, successResponseList, successResponseObject } from "../response.util"; import {
notAuthenticatedError,
successResponseList,
successResponseObject,
} from '../response.util';
describe('Public Inverse REST API Endpoints', () => { describe('Public Inverse REST API Endpoints', () => {
const useLivenet = true; const useLivenet = true;
const api = new InverseClient(undefined, undefined, useLivenet, { disable_time_sync: true }); const api = new InverseClient(undefined, undefined, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSD'; const symbol = 'BTCUSD';
const interval = '15'; const interval = '15';
const timestampOneHourAgo = (new Date().getTime() / 1000) - (1000 * 60 * 60); const timestampOneHourAgo = new Date().getTime() / 1000 - 1000 * 60 * 60;
const from = Number(timestampOneHourAgo.toFixed(0)); const from = Number(timestampOneHourAgo.toFixed(0));
describe('Inverse only endpoints', () => { describe('Inverse only endpoints', () => {
it('should throw for unauthenticated private calls', async () => { it('should throw for unauthenticated private calls', async () => {
expect(() => api.getPosition()).rejects.toMatchObject(notAuthenticatedError()); expect(() => api.getPosition()).rejects.toMatchObject(
}); notAuthenticatedError()
);
it('getKline()', async () => { expect(() => api.getApiKeyInfo()).rejects.toMatchObject(
expect( notAuthenticatedError()
await api.getKline({ symbol, interval, from }) );
).toMatchObject(successResponseList());
});
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(await api.getIndexPriceKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(await api.getPremiumIndexKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(successResponseObject());
});
});
describe('Shared endpoints', () => {
it('should throw for unauthenticated private calls', async () => {
expect(() => api.getApiKeyInfo()).rejects.toMatchObject(notAuthenticatedError());
}); });
it('getOrderBook()', async () => { it('getOrderBook()', async () => {
expect(await api.getOrderBook({ symbol })).toMatchObject(successResponseList()); expect(await api.getOrderBook({ symbol })).toMatchObject(
successResponseList()
);
});
it('getKline()', async () => {
expect(await api.getKline({ symbol, interval, from })).toMatchObject(
successResponseList()
);
}); });
it('getTickers()', async () => { it('getTickers()', async () => {
expect(await api.getTickers()).toMatchObject(successResponseList()); expect(await api.getTickers()).toMatchObject(successResponseList());
}); });
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(
successResponseList()
);
});
it('getSymbols()', async () => { it('getSymbols()', async () => {
expect(await api.getSymbols()).toMatchObject(successResponseList()); expect(await api.getSymbols()).toMatchObject(successResponseList());
}); });
it('getMarkPriceKline()', async () => {
expect(
await api.getMarkPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(
await api.getIndexPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(
await api.getPremiumIndexKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(
successResponseObject()
);
});
it('getServerTime()', async () => { it('getServerTime()', async () => {
expect(await api.getServerTime()).toMatchObject(successResponseObject()); expect(await api.getServerTime()).toMatchObject(successResponseObject());
}); });
it('fetchServertime() returns number', async () => {
expect(await api.fetchServerTime()).toStrictEqual(expect.any(Number));
});
it('getApiAnnouncements()', async () => { it('getApiAnnouncements()', async () => {
expect(await api.getApiAnnouncements()).toMatchObject(successResponseList()); expect(await api.getApiAnnouncements()).toMatchObject(
successResponseList()
);
}); });
}); });
}); });

View File

@@ -0,0 +1,103 @@
import { LinearClient } from '../../src/linear-client';
import { successResponseList, successResponseObject } from '../response.util';
describe('Public Linear REST API GET Endpoints', () => {
const useLivenet = true;
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
});
const api = new LinearClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSDT';
it('getApiKeyInfo()', async () => {
expect(await api.getApiKeyInfo()).toMatchObject(successResponseObject());
});
it('getWalletBalance()', async () => {
expect(await api.getWalletBalance()).toMatchObject(successResponseObject());
});
it('getWalletFundRecords()', async () => {
expect(await api.getWalletFundRecords()).toMatchObject(
successResponseObject()
);
});
it('getWithdrawRecords()', async () => {
expect(await api.getWithdrawRecords()).toMatchObject(
successResponseObject()
);
});
it('getAssetExchangeRecords()', async () => {
expect(await api.getAssetExchangeRecords()).toMatchObject(
successResponseList()
);
});
it('getActiveOrderList()', async () => {
expect(await api.getActiveOrderList({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryActiveOrder()', async () => {
expect(await api.queryActiveOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getConditionalOrder()', async () => {
expect(await api.getConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryConditionalOrder()', async () => {
expect(await api.queryConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getPosition()', async () => {
expect(await api.getPosition()).toMatchObject(successResponseObject());
});
it('getTradeRecords()', async () => {
expect(await api.getTradeRecords({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getClosedPnl()', async () => {
expect(await api.getClosedPnl({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getRiskLimitList()', async () => {
expect(await api.getRiskLimitList({ symbol: symbol })).toMatchObject(
successResponseList()
);
});
it('getPredictedFundingFee()', async () => {
expect(await api.getPredictedFundingFee({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getLastFundingFee()', async () => {
expect(await api.getLastFundingFee({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
});

View File

@@ -0,0 +1,230 @@
import { API_ERROR_CODE, LinearClient } from '../../src';
import { successResponseObject } from '../response.util';
describe('Private Inverse-Futures REST API POST Endpoints', () => {
const useLivenet = true;
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
});
const api = new LinearClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
// Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol!
const symbol = 'BTCUSDT';
// These tests are primarily check auth is working by expecting balance or order not found style errors
it('placeActiveOrder()', async () => {
expect(
await api.placeActiveOrder({
side: 'Buy',
symbol,
order_type: 'Limit',
price: 20000,
qty: 1,
time_in_force: 'GoodTillCancel',
reduce_only: false,
close_on_trigger: false,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_COST_NOT_AVAILABLE,
});
});
it('cancelActiveOrder()', async () => {
expect(
await api.cancelActiveOrder({
symbol,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllActiveOrders()', async () => {
expect(
await api.cancelAllActiveOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceActiveOrder()', async () => {
expect(
await api.replaceActiveOrder({
symbol,
order_id: '123123123',
p_r_qty: 1,
p_r_price: 30000,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to replace',
});
});
it('placeConditionalOrder()', async () => {
expect(
await api.placeConditionalOrder({
order_type: 'Limit',
side: 'Buy',
symbol,
qty: 1,
price: 8100,
base_price: 8300,
stop_px: 8150,
time_in_force: 'GoodTillCancel',
order_link_id: 'cus_order_id_1',
reduce_only: false,
trigger_by: 'LastPrice',
})
).toMatchObject({
ret_code: API_ERROR_CODE.INSUFFICIENT_BALANCE_FOR_ORDER_COST_LINEAR,
ret_msg: 'Insufficient wallet balance',
});
});
it('cancelConditionalOrder()', async () => {
expect(
await api.cancelConditionalOrder({
symbol,
order_link_id: 'lkasmdflasd',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE_LINEAR,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllConditionalOrders()', async () => {
expect(
await api.cancelAllConditionalOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceConditionalOrder()', async () => {
expect(
await api.replaceConditionalOrder({
symbol,
p_r_price: 50000,
p_r_qty: 1,
order_link_id: 'someorderid',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE_LINEAR,
ret_msg: 'order not exists or too late to replace',
});
});
it('setAutoAddMargin()', async () => {
expect(
await api.setAutoAddMargin({
symbol,
side: 'Buy',
auto_add_margin: true,
})
).toMatchObject({
ret_code: API_ERROR_CODE.AUTO_ADD_MARGIN_NOT_MODIFIED,
ret_msg: 'autoAddMargin not modified',
});
});
it('setMarginSwitch()', async () => {
expect(
await api.setMarginSwitch({
symbol,
is_isolated: true,
buy_leverage: 5,
sell_leverage: 5,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ISOLATED_NOT_MODIFIED_LINEAR,
ret_msg: 'Isolated not modified',
});
});
it('setPositionMode()', async () => {
expect(
await api.setPositionMode({
symbol,
mode: 'BothSide',
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_MODE_NOT_MODIFIED,
ret_msg: 'position mode not modified',
});
});
it('setPositionTpSlMode()', async () => {
expect(
await api.setPositionTpSlMode({
symbol,
tp_sl_mode: 'Full',
})
).toMatchObject({
ret_code: API_ERROR_CODE.SAME_SLTP_MODE_LINEAR,
ret_msg: 'same tp sl mode2',
});
});
it('setAddReduceMargin()', async () => {
expect(
await api.setAddReduceMargin({
symbol,
side: 'Buy',
margin: 5,
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_SIZE_IS_ZERO,
ret_msg: 'position size is zero',
});
});
it('setUserLeverage()', async () => {
expect(
await api.setUserLeverage({
symbol,
buy_leverage: 5,
sell_leverage: 5,
})
).toMatchObject({
ret_code: API_ERROR_CODE.LEVERAGE_NOT_MODIFIED,
ret_msg: 'leverage not modified',
});
});
it('setTradingStop()', async () => {
expect(
await api.setTradingStop({
symbol,
side: 'Buy',
take_profit: 555,
})
).toMatchObject({
ret_code: API_ERROR_CODE.CANNOT_SET_LINEAR_TRADING_STOP_FOR_ZERO_POS,
ret_msg: 'can not set tp/sl/ts for zero position',
});
});
it('setRiskLimit()', async () => {
expect(
await api.setRiskLimit({
symbol,
side: 'Buy',
risk_id: 2,
})
).toMatchObject({
ret_code: API_ERROR_CODE.RISK_ID_NOT_MODIFIED,
ret_msg: 'risk id not modified',
});
});
});

View File

@@ -1,66 +1,92 @@
import { LinearClient } from "../../src/linear-client"; import { LinearClient } from '../../src/linear-client';
import { notAuthenticatedError, successResponseList, successResponseObject } from "../response.util"; import {
notAuthenticatedError,
successResponseList,
successResponseObject,
} from '../response.util';
describe('Public Linear REST API Endpoints', () => { describe('Public Linear REST API Endpoints', () => {
const useLivenet = true; const useLivenet = true;
const api = new LinearClient(undefined, undefined, useLivenet, { disable_time_sync: true }); const api = new LinearClient(undefined, undefined, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSDT'; const symbol = 'BTCUSDT';
const interval = '15'; const interval = '15';
const timestampOneHourAgo = (new Date().getTime() / 1000) - (1000 * 60 * 60); const timestampOneHourAgo = new Date().getTime() / 1000 - 1000 * 60 * 60;
const from = Number(timestampOneHourAgo.toFixed(0)); const from = Number(timestampOneHourAgo.toFixed(0));
describe('Linear only endpoints', () => { describe('Linear only endpoints', () => {
it('should throw for unauthenticated private calls', async () => { it('should throw for unauthenticated private calls', async () => {
expect(() => api.getPosition()).rejects.toMatchObject(notAuthenticatedError()); expect(() => api.getPosition()).rejects.toMatchObject(
}); notAuthenticatedError()
);
it('getKline()', async () => { expect(() => api.getApiKeyInfo()).rejects.toMatchObject(
expect( notAuthenticatedError()
await api.getKline({ symbol, interval, from }) );
).toMatchObject(successResponseList());
});
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(await api.getIndexPriceKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(await api.getPremiumIndexKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(successResponseObject());
});
});
describe('Shared endpoints', () => {
it('should throw for unauthenticated private calls', async () => {
expect(() => api.getApiKeyInfo()).rejects.toMatchObject(notAuthenticatedError());
}); });
it('getOrderBook()', async () => { it('getOrderBook()', async () => {
expect(await api.getOrderBook({ symbol })).toMatchObject(successResponseList()); expect(await api.getOrderBook({ symbol })).toMatchObject(
successResponseList()
);
});
it('getKline()', async () => {
expect(await api.getKline({ symbol, interval, from })).toMatchObject(
successResponseList()
);
}); });
it('getTickers()', async () => { it('getTickers()', async () => {
expect(await api.getTickers()).toMatchObject(successResponseList()); expect(await api.getTickers()).toMatchObject(successResponseList());
}); });
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(
successResponseList()
);
});
it('getSymbols()', async () => { it('getSymbols()', async () => {
expect(await api.getSymbols()).toMatchObject(successResponseList()); expect(await api.getSymbols()).toMatchObject(successResponseList());
}); });
it('getMarkPriceKline()', async () => {
expect(
await api.getMarkPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(
await api.getIndexPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(
await api.getPremiumIndexKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(
successResponseObject()
);
});
it('getServerTime()', async () => { it('getServerTime()', async () => {
expect(await api.getServerTime()).toMatchObject(successResponseObject()); expect(await api.getServerTime()).toMatchObject(successResponseObject());
}); });
it('fetchServertime() returns number', async () => {
expect(await api.fetchServerTime()).toStrictEqual(expect.any(Number));
});
it('getApiAnnouncements()', async () => { it('getApiAnnouncements()', async () => {
expect(await api.getApiAnnouncements()).toMatchObject(successResponseList()); expect(await api.getApiAnnouncements()).toMatchObject(
successResponseList()
);
}); });
}); });
}); });

View File

@@ -1,26 +1,31 @@
export function successResponseList(successMsg: string | null = 'OK') {
export function successResponseList() {
return { return {
"ext_code": "", result: expect.any(Array),
"ext_info": "", ret_code: 0,
"result": expect.any(Array), ret_msg: successMsg,
"ret_code": 0,
"ret_msg": "OK",
"time_now": expect.any(String),
};
}; };
}
export function successResponseObject() { export function successResponseObject(successMsg: string | null = 'OK') {
return { return {
"ext_code": "", result: expect.any(Object),
"ext_info": "", ret_code: 0,
"result": expect.any(Object), ret_msg: successMsg,
"ret_code": 0,
"ret_msg": "OK",
"time_now": expect.any(String),
}; };
}
export function errorResponseObject(
result: null | any = null,
ret_code: number,
ret_msg: string
) {
return {
result,
ret_code,
ret_msg,
}; };
}
export function notAuthenticatedError() { export function notAuthenticatedError() {
return new Error('Private endpoints require api and private keys set'); return new Error('Private endpoints require api and private keys set');
}; }

View File

@@ -0,0 +1,54 @@
import { SpotClient } from '../../src';
import {
errorResponseObject,
notAuthenticatedError,
successResponseList,
successResponseObject,
} from '../response.util';
describe('Private Spot REST API Endpoints', () => {
const useLivenet = true;
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
});
const api = new SpotClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSDT';
const interval = '15m';
it('getOrder()', async () => {
// No auth error == test pass
expect(await api.getOrder({ orderId: '123123' })).toMatchObject(
errorResponseObject(null, -2013, 'Order does not exist.')
);
});
it('getOpenOrders()', async () => {
expect(await api.getOpenOrders()).toMatchObject(successResponseList(''));
});
it('getPastOrders()', async () => {
expect(await api.getPastOrders()).toMatchObject(successResponseList(''));
});
it('getMyTrades()', async () => {
expect(await api.getMyTrades()).toMatchObject(successResponseList(''));
});
it('getBalances()', async () => {
expect(await api.getBalances()).toMatchObject({
result: {
balances: expect.any(Array),
},
ret_code: 0,
ret_msg: '',
});
});
});

View File

@@ -0,0 +1,56 @@
import { API_ERROR_CODE, SpotClient } from '../../src';
import { successResponseObject } from '../response.util';
describe('Private Inverse-Futures REST API POST Endpoints', () => {
const useLivenet = true;
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
});
const api = new SpotClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
// Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol!
const symbol = 'BTCUSDT';
// These tests are primarily check auth is working by expecting balance or order not found style errors
it('submitOrder()', async () => {
expect(
await api.submitOrder({
side: 'Buy',
symbol,
qty: 10000,
type: 'MARKET',
})
).toMatchObject({
ret_code: API_ERROR_CODE.BALANCE_INSUFFICIENT_SPOT,
ret_msg: 'Balance insufficient ',
});
});
it('cancelOrder()', async () => {
expect(
await api.cancelOrder({
orderId: '1231231',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE_SPOT,
ret_msg: 'Order does not exist.',
});
});
it('cancelOrderBatch()', async () => {
expect(
await api.cancelOrderBatch({
symbol,
orderTypes: ['LIMIT', 'LIMIT_MAKER'],
})
).toMatchObject(successResponseObject(''));
});
});

81
test/spot/public.test.ts Normal file
View File

@@ -0,0 +1,81 @@
import { SpotClient } from '../../src';
import {
notAuthenticatedError,
successResponseList,
successResponseObject,
} from '../response.util';
describe('Public Spot REST API Endpoints', () => {
const useLivenet = true;
const api = new SpotClient(undefined, undefined, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSDT';
const interval = '15m';
const timestampOneHourAgo = new Date().getTime() / 1000 - 1000 * 60 * 60;
const from = Number(timestampOneHourAgo.toFixed(0));
it('should throw for unauthenticated private calls', async () => {
expect(() => api.getOpenOrders()).rejects.toMatchObject(
notAuthenticatedError()
);
expect(() => api.getBalances()).rejects.toMatchObject(
notAuthenticatedError()
);
});
it('getSymbols()', async () => {
expect(await api.getSymbols()).toMatchObject(successResponseList(''));
});
it('getOrderBook()', async () => {
expect(await api.getOrderBook(symbol)).toMatchObject(
successResponseObject(null)
);
});
it('getMergedOrderBook()', async () => {
expect(await api.getMergedOrderBook(symbol)).toMatchObject(
successResponseObject(null)
);
});
it('getTrades()', async () => {
expect(await api.getTrades(symbol)).toMatchObject(
successResponseObject(null)
);
});
it('getCandles()', async () => {
expect(await api.getCandles(symbol, interval)).toMatchObject(
successResponseObject(null)
);
});
it('get24hrTicker()', async () => {
expect(await api.get24hrTicker()).toMatchObject(
successResponseObject(null)
);
});
it('getLastTradedPrice()', async () => {
expect(await api.getLastTradedPrice()).toMatchObject(
successResponseObject(null)
);
});
it('getBestBidAskPrice()', async () => {
expect(await api.getBestBidAskPrice()).toMatchObject(
successResponseObject(null)
);
});
it('getServerTime()', async () => {
expect(await api.getServerTime()).toStrictEqual(expect.any(Number));
});
it('fetchServertime() returns number', async () => {
expect(await api.fetchServerTime()).toStrictEqual(expect.any(Number));
});
});