Merge pull request #196 from tiagosiebler/next

v3.2.0: Contract V3 REST & WebSocket Clients. Improve websocket reconnection resilience
This commit is contained in:
Tiago
2022-11-12 18:13:18 +00:00
committed by GitHub
40 changed files with 1254 additions and 62 deletions

View File

@@ -11,7 +11,7 @@
Node.js connector for the Bybit APIs and WebSockets: Node.js connector for the Bybit APIs and WebSockets:
- Complete integration with all bybit APIs. - Complete integration with all bybit APIs.
- TypeScript support (with type declarations for most API requests & responses). - TypeScript support (with type declarations for most API requests & responses).
- Over 300 integration tests making real API calls & WebSocket connections, validating any changes before they reach npm. - Over 300 end-to-end tests making real API calls & WebSocket connections, validating any changes before they reach npm.
- Robust WebSocket integration with configurable connection heartbeats & automatic reconnect then resubscribe workflows. - Robust WebSocket integration with configurable connection heartbeats & automatic reconnect then resubscribe workflows.
- Browser support (via webpack bundle - see "Browser Usage" below). - Browser support (via webpack bundle - see "Browser Usage" below).
@@ -25,10 +25,10 @@ Node.js connector for the Bybit APIs and WebSockets:
## Related projects ## Related projects
Check out my related projects: Check out my related projects:
- Try my connectors: - Try my connectors:
- [ftx-api](https://www.npmjs.com/package/ftx-api)
- [bybit-api](https://www.npmjs.com/package/bybit-api)
- [binance](https://www.npmjs.com/package/binance) - [binance](https://www.npmjs.com/package/binance)
- [bybit-api](https://www.npmjs.com/package/bybit-api)
- [okx-api](https://www.npmjs.com/package/okx-api) - [okx-api](https://www.npmjs.com/package/okx-api)
- [ftx-api](https://www.npmjs.com/package/ftx-api)
- Try my misc utilities: - Try my misc utilities:
- [orderbooks](https://www.npmjs.com/package/orderbooks) - [orderbooks](https://www.npmjs.com/package/orderbooks)
- Check out my examples: - Check out my examples:
@@ -180,6 +180,8 @@ The WebsocketClient can be configured to a specific API group using the market p
| Copy Trading | `market: 'linear'` | The [copy trading](https://bybit-exchange.github.io/docs/copy_trading/#t-websocket) category. Use the linear market to listen to all copy trading topics. | | Copy Trading | `market: 'linear'` | The [copy trading](https://bybit-exchange.github.io/docs/copy_trading/#t-websocket) category. Use the linear market to listen to all copy trading topics. |
| USDC Perps | `market: 'usdcPerp` | The [USDC perps](https://bybit-exchange.github.io/docs/usdc/perpetual/#t-websocket) category. | | USDC Perps | `market: 'usdcPerp` | The [USDC perps](https://bybit-exchange.github.io/docs/usdc/perpetual/#t-websocket) category. |
| USDC Options | `market: 'usdcOption'`| The [USDC options](https://bybit-exchange.github.io/docs/usdc/option/#t-websocket) category. | | USDC Options | `market: 'usdcOption'`| The [USDC options](https://bybit-exchange.github.io/docs/usdc/option/#t-websocket) category. |
| Contract v3 USDT | `market: 'contractUSDT'`| The [Contract V3](https://bybit-exchange.github.io/docs/derivativesV3/contract/#t-websocket) category (USDT perps) |
| Contract v3 Inverse | `market: 'contractInverse'`| The [Contract V3](https://bybit-exchange.github.io/docs/derivativesV3/contract/#t-websocket) category (inverse perps) |
```javascript ```javascript
const { WebsocketClient } = require('bybit-api'); const { WebsocketClient } = require('bybit-api');

View File

@@ -0,0 +1,24 @@
import { ContractClient } from '../src/index';
// or
// import { ContractClient } from 'bybit-api';
const key = process.env.API_KEY_COM;
const secret = process.env.API_SECRET_COM;
const client = new ContractClient({
key,
secret,
strict_param_validation: true,
});
(async () => {
try {
const getPositions = await client.getPositions({
settleCoin: 'USDT',
});
console.log('getPositions:', getPositions);
} catch (e) {
console.error('request failed: ', e);
}
})();

View File

@@ -13,10 +13,13 @@ import { WebsocketClient, WS_KEY_MAP, DefaultLogger } from '../src';
const secret = process.env.API_SECRET; const secret = process.env.API_SECRET;
// USDT Perps: // USDT Perps:
const market = 'linear'; // const market = 'linear';
// Inverse Perp // Inverse Perp
// const market = 'inverse'; // const market = 'inverse';
// const market = 'spotv3'; // const market = 'spotv3';
// Contract v3
const market = 'contractUSDT';
// const market = 'contractInverse';
// Note: the WebsocketClient defaults to testnet. Set `livenet: true` to use live markets. // Note: the WebsocketClient defaults to testnet. Set `livenet: true` to use live markets.
const wsClient = new WebsocketClient( const wsClient = new WebsocketClient(
@@ -48,8 +51,19 @@ import { WebsocketClient, WS_KEY_MAP, DefaultLogger } from '../src';
wsClient.on('reconnected', (data) => { wsClient.on('reconnected', (data) => {
console.log('ws has reconnected ', data?.wsKey); console.log('ws has reconnected ', data?.wsKey);
}); });
wsClient.on('error', (data) => {
console.error('ws exception: ', data);
});
// subscribe to private endpoints // subscribe to private endpoints
// check the api docs in your api category to see the available topics // check the api docs in your api category to see the available topics
wsClient.subscribe(['position', 'execution', 'order', 'wallet']); // wsClient.subscribe(['position', 'execution', 'order', 'wallet']);
// Contract v3
wsClient.subscribe([
'user.position.contractAccount',
'user.execution.contractAccount',
'user.order.contractAccount',
'user.wallet.contractAccount',
]);
})(); })();

View File

@@ -51,6 +51,9 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src';
wsClient.on('reconnected', (data) => { wsClient.on('reconnected', (data) => {
console.log('ws has reconnected ', data?.wsKey); console.log('ws has reconnected ', data?.wsKey);
}); });
// wsClient.on('error', (data) => {
// console.error('ws exception: ', data);
// });
// Inverse // Inverse
// wsClient.subscribe('trade'); // wsClient.subscribe('trade');

View File

@@ -1,6 +1,6 @@
{ {
"name": "bybit-api", "name": "bybit-api",
"version": "3.1.3", "version": "3.2.0",
"description": "Complete & robust node.js SDK for Bybit's REST APIs and WebSockets, with TypeScript & integration tests.", "description": "Complete & robust node.js SDK 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

@@ -54,6 +54,12 @@ export const API_ERROR_CODE = {
INSUFFICIENT_BALANCE_FOR_ORDER_COST_LINEAR: 130080, INSUFFICIENT_BALANCE_FOR_ORDER_COST_LINEAR: 130080,
SAME_SLTP_MODE_LINEAR: 130150, SAME_SLTP_MODE_LINEAR: 130150,
RISK_ID_NOT_MODIFIED: 134026, RISK_ID_NOT_MODIFIED: 134026,
CONTRACT_ORDER_NOT_EXISTS: 140001,
CONTRACT_INSUFFICIENT_BALANCE: 140007,
CONTRACT_POSITION_MODE_NOT_MODIFIED: 140025,
CONTRACT_MARGIN_MODE_NOT_MODIFIED: 140026,
CONTRACT_RISK_LIMIT_INFO_NOT_EXISTS: 140031,
CONTRACT_SET_LEVERAGE_NOT_MODIFIED: 140043,
/** E.g. USDC Options trading, trying to access a symbol that is no longer active */ /** E.g. USDC Options trading, trying to access a symbol that is no longer active */
CONTRACT_NAME_NOT_EXIST: 3100111, CONTRACT_NAME_NOT_EXIST: 3100111,
ORDER_NOT_EXIST: 3100136, ORDER_NOT_EXIST: 3100136,

341
src/contract-client.ts Normal file
View File

@@ -0,0 +1,341 @@
import {
APIResponseWithTime,
APIResponseV3,
UMCandlesRequest,
UMCategory,
UMFundingRateHistoryRequest,
UMInstrumentInfoRequest,
UMOpenInterestRequest,
UMOptionDeliveryPriceRequest,
UMPublicTradesRequest,
ContractOrderRequest,
ContractHistoricOrdersRequest,
ContractCancelOrderRequest,
ContractModifyOrderRequest,
ContractActiveOrdersRequest,
ContractPositionsRequest,
ContractSetAutoAddMarginRequest,
ContractSetMarginSwitchRequest,
ContractSetPositionModeRequest,
ContractSetTPSLRequest,
ContractUserExecutionHistoryRequest,
ContractClosedPNLRequest,
ContractWalletFundRecordRequest,
} from './types';
import { REST_CLIENT_TYPE_ENUM } from './util';
import BaseRestClient from './util/BaseRestClient';
/**
* REST API client for Derivatives V3 Contract APIs
*/
export class ContractClient extends BaseRestClient {
getClientType() {
// Follows the same authentication mechanism as other v3 APIs (e.g. USDC)
return REST_CLIENT_TYPE_ENUM.v3;
}
async fetchServerTime(): Promise<number> {
const res = await this.getServerTime();
return Number(res.time_now);
}
/**
*
* Market Data Endpoints : these seem exactly the same as the unified margin market data endpoints
*
*/
/** Query order book info. Each side has a depth of 25 orders. */
getOrderBook(
symbol: string,
category: string,
limit?: number
): Promise<APIResponseV3<any>> {
return this.get('/derivatives/v3/public/order-book/L2', {
category,
symbol,
limit,
});
}
/** Get candles/klines */
getCandles(params: UMCandlesRequest): Promise<APIResponseV3<any>> {
return this.get('/derivatives/v3/public/kline', params);
}
/** Get a symbol price/statistics ticker */
getSymbolTicker(
category: UMCategory,
symbol?: string
): Promise<APIResponseV3<any>> {
return this.get('/derivatives/v3/public/tickers', { category, symbol });
}
/** Get trading rules per symbol/contract, incl price/amount/value/leverage filters */
getInstrumentInfo(
params: UMInstrumentInfoRequest
): Promise<APIResponseV3<any>> {
return this.get('/derivatives/v3/public/instruments-info', params);
}
/** Query mark price kline (like getCandles() but for mark price). */
getMarkPriceCandles(params: UMCandlesRequest): Promise<APIResponseV3<any>> {
return this.get('/derivatives/v3/public/mark-price-kline', params);
}
/** Query Index Price Kline */
getIndexPriceCandles(params: UMCandlesRequest): Promise<APIResponseV3<any>> {
return this.get('/derivatives/v3/public/index-price-kline', params);
}
/**
* The funding rate is generated every 8 hours at 00:00 UTC, 08:00 UTC and 16:00 UTC.
* For example, if a request is sent at 12:00 UTC, the funding rate generated earlier that day at 08:00 UTC will be sent.
*/
getFundingRateHistory(
params: UMFundingRateHistoryRequest
): Promise<APIResponseV3<any>> {
return this.get(
'/derivatives/v3/public/funding/history-funding-rate',
params
);
}
/** Get Risk Limit */
getRiskLimit(
category: UMCategory,
symbol: string
): Promise<APIResponseV3<any>> {
return this.get('/derivatives/v3/public/risk-limit/list', {
category,
symbol,
});
}
/** Get option delivery price */
getOptionDeliveryPrice(
params: UMOptionDeliveryPriceRequest
): Promise<APIResponseV3<any>> {
return this.get('/derivatives/v3/public/delivery-price', params);
}
/** Get public trading history */
getTrades(params: UMPublicTradesRequest): Promise<APIResponseV3<any>> {
return this.get('/derivatives/v3/public/recent-trade', params);
}
/**
* Gets the total amount of unsettled contracts.
* In other words, the total number of contracts held in open positions.
*/
getOpenInterest(params: UMOpenInterestRequest): Promise<APIResponseV3<any>> {
return this.get('/derivatives/v3/public/open-interest', params);
}
/**
*
* Contract Account Endpoints
*
*/
/** -> Order API */
/** Place an order */
submitOrder(params: ContractOrderRequest): Promise<APIResponseV3<any>> {
return this.postPrivate('/contract/v3/private/order/create', params);
}
/** Query order history. As order creation/cancellation is asynchronous, the data returned from the interface may be delayed. To access order information in real-time, call getActiveOrders() */
getHistoricOrders(
params: ContractHistoricOrdersRequest
): Promise<APIResponseV3<any>> {
return this.getPrivate('/contract/v3/private/order/list', params);
}
/** Cancel order */
cancelOrder(params: ContractCancelOrderRequest): Promise<APIResponseV3<any>> {
return this.postPrivate('/contract/v3/private/order/cancel', params);
}
/** Cancel all orders */
cancelAllOrders(symbol: string): Promise<APIResponseV3<any>> {
return this.postPrivate('/contract/v3/private/order/cancel-all', {
symbol,
});
}
/** Replace order : Active order parameters (such as quantity, price) and stop order parameters cannot be modified in one request at the same time. Please request modification separately. */
modifyOrder(params: ContractModifyOrderRequest): Promise<APIResponseV3<any>> {
return this.postPrivate('/contract/v3/private/order/replace', params);
}
/** Query Open Order(s) (real-time) */
getActiveOrders(
params: ContractActiveOrdersRequest
): Promise<APIResponseV3<any>> {
return this.getPrivate(
'/contract/v3/private/order/unfilled-orders',
params
);
}
/** -> Positions API */
/**
* Query my positions real-time. Accessing personal list of positions.
* Either symbol or settleCoin is required.
* Users can access their position holding information through this interface, such as the number of position holdings and wallet balance.
*/
getPositions(params?: ContractPositionsRequest): Promise<APIResponseV3<any>> {
return this.getPrivate('/contract/v3/private/position/list', params);
}
/** Set auto add margin, or Auto-Margin Replenishment. */
setAutoAddMargin(
params: ContractSetAutoAddMarginRequest
): Promise<APIResponseV3<any>> {
return this.postPrivate(
'/contract/v3/private/position/set-auto-add-margin',
params
);
}
/** Switch cross margin mode/isolated margin mode */
setMarginSwitch(
params: ContractSetMarginSwitchRequest
): Promise<APIResponseV3<any>> {
return this.postPrivate(
'/contract/v3/private/position/switch-isolated',
params
);
}
/** Supports switching between One-Way Mode and Hedge Mode at the coin level. */
setPositionMode(
params: ContractSetPositionModeRequest
): Promise<APIResponseV3<any>> {
return this.postPrivate(
'/contract/v3/private/position/switch-mode',
params
);
}
/**
* Switch mode between Full or Partial
*/
setTPSLMode(
symbol: string,
tpSlMode: 'Full' | 'Partial'
): Promise<APIResponseV3<any>> {
return this.postPrivate('/contract/v3/private/position/switch-tpsl-mode', {
symbol,
tpSlMode,
});
}
/** Leverage setting. */
setLeverage(
symbol: string,
buyLeverage: string,
sellLeverage: string
): Promise<APIResponseV3<any>> {
return this.postPrivate('/contract/v3/private/position/set-leverage', {
symbol,
buyLeverage,
sellLeverage,
});
}
/**
* Set take profit, stop loss, and trailing stop for your open position.
* If using partial mode, TP/SL/TS orders will not close your entire position.
*/
setTPSL(params: ContractSetTPSLRequest): Promise<APIResponseV3<any>> {
return this.postPrivate(
'/contract/v3/private/position/trading-stop',
params
);
}
/** Set risk limit */
setRiskLimit(
symbol: string,
riskId: number,
/** 0-one-way, 1-buy side, 2-sell side */
positionIdx: 0 | 1 | 2
): Promise<APIResponseV3<any>> {
return this.postPrivate('/contract/v3/private/position/set-risk-limit', {
symbol,
riskId,
positionIdx,
});
}
/**
* Get user's trading records.
* The results are ordered in descending order (the first item is the latest). Returns records up to 2 years old.
*/
getUserExecutionHistory(
params: ContractUserExecutionHistoryRequest
): Promise<APIResponseV3<any>> {
return this.getPrivate('/contract/v3/private/execution/list', params);
}
/**
* Get user's closed profit and loss records.
* The results are ordered in descending order (the first item is the latest).
*/
getClosedProfitAndLoss(
params: ContractClosedPNLRequest
): Promise<APIResponseV3<any>> {
return this.getPrivate('/contract/v3/private/position/closed-pnl', params);
}
/** Get the information of open interest limit. */
getOpenInterestLimitInfo(symbol: string): Promise<APIResponseV3<any>> {
return this.getPrivate('/contract/v3/private/position/closed-pnl', {
symbol,
});
}
/** -> Account API */
/** Query wallet balance */
getBalances(coin?: string): Promise<APIResponseV3<any>> {
return this.getPrivate('/contract/v3/private/account/wallet/balance', {
coin,
});
}
/** Get user trading fee rate */
getTradingFeeRate(symbol?: string): Promise<APIResponseV3<any>> {
return this.getPrivate('/contract/v3/private/account/fee-rate', {
symbol,
});
}
/**
* Get wallet fund records.
* This endpoint also shows exchanges from the Asset Exchange, where the types for the exchange are ExchangeOrderWithdraw and ExchangeOrderDeposit.
* This endpoint returns incomplete information for transfers involving the derivatives wallet.
* Use the account asset API for creating and querying internal transfers.
*/
getWalletFundRecords(
params?: ContractWalletFundRecordRequest
): Promise<APIResponseV3<any>> {
return this.getPrivate(
'/contract/v3/private/account/wallet/fund-records',
params
);
}
/**
*
* API Data Endpoints
*
*/
getServerTime(): Promise<APIResponseWithTime> {
return this.get('/v2/public/time');
}
}

View File

@@ -8,6 +8,7 @@ export * from './spot-client-v3';
export * from './usdc-option-client'; export * from './usdc-option-client';
export * from './usdc-perpetual-client'; export * from './usdc-perpetual-client';
export * from './unified-margin-client'; export * from './unified-margin-client';
export * from './contract-client';
export * from './websocket-client'; export * from './websocket-client';
export * from './util/logger'; export * from './util/logger';
export * from './util'; export * from './util';

View File

@@ -0,0 +1,128 @@
import { OrderSide } from '../shared';
import { UMOrderType } from './unified-margin';
import { USDCOrderFilter, USDCTimeInForce } from './usdc-shared';
export interface ContractOrderRequest {
symbol: string;
side: OrderSide;
positionIdx?: '0' | '1' | '2';
orderType: UMOrderType;
qty: string;
price?: string;
triggerDirection?: '1' | '2';
triggerPrice?: string;
triggerBy?: string;
tpTriggerBy?: string;
slTriggerBy?: string;
timeInForce: USDCTimeInForce;
orderLinkId?: string;
takeProfit?: number;
stopLoss?: number;
reduceOnly?: boolean;
closeOnTrigger?: boolean;
}
export interface ContractHistoricOrdersRequest {
orderId?: string;
orderLinkId?: string;
symbol: string;
orderStatus?: string;
orderFilter?: USDCOrderFilter;
limit?: number;
cursor?: string;
}
export interface ContractCancelOrderRequest {
symbol: string;
orderId?: string;
orderLinkId?: string;
}
export interface ContractModifyOrderRequest {
orderId?: string;
orderLinkId?: string;
symbol: string;
qty?: string;
price?: string;
takeProfit?: number;
stopLoss?: number;
tpTriggerBy?: string;
slTriggerBy?: string;
triggerBy?: string;
}
export interface ContractActiveOrdersRequest {
symbol?: string;
orderId?: string;
orderLinkId?: string;
settleCoin?: string;
orderFilter?: USDCOrderFilter;
limit?: number;
}
export interface ContractPositionsRequest {
symbol?: string;
settleCoin?: string;
dataFilter?: string;
}
export interface ContractSetAutoAddMarginRequest {
symbol: string;
side: 'Buy' | 'Sell';
autoAddMargin: 1 | 0;
positionIdx?: 0 | 1 | 2;
}
export interface ContractSetMarginSwitchRequest {
symbol: string;
tradeMode: 0 | 1;
buyLeverage: string;
sellLeverage: string;
}
export interface ContractSetPositionModeRequest {
symbol?: string;
coin?: string;
mode: 0 | 3;
}
export interface ContractSetTPSLRequest {
symbol: string;
takeProfit?: string;
stopLoss?: string;
activePrice?: string;
trailingStop?: string;
tpTriggerBy?: string;
slTriggerBy?: string;
slSize?: string;
tpSize?: string;
/** 0-one-way, 1-buy side, 2-sell side */
positionIdx?: 0 | 1 | 2;
}
export interface ContractUserExecutionHistoryRequest {
symbol: string;
orderId?: string;
startTime?: number;
endTime?: number;
execType?: 'Trade' | 'AdlTrade' | 'Funding' | 'BustTrade';
limit?: number;
cursor?: string;
}
export interface ContractClosedPNLRequest {
symbol: string;
startTime?: number;
endTime?: number;
limit?: number;
cursor?: string;
}
export interface ContractWalletFundRecordRequest {
startTime?: string;
endTime?: string;
coin?: string;
walletFundType?: string;
limit?: string;
cursor?: string;
}

View File

@@ -7,3 +7,4 @@ export * from './usdc-perp';
export * from './usdc-options'; export * from './usdc-options';
export * from './usdc-shared'; export * from './usdc-shared';
export * from './unified-margin'; export * from './unified-margin';
export * from './contract';

View File

@@ -1,3 +1,4 @@
import { ContractClient } from '../contract-client';
import { InverseClient } from '../inverse-client'; import { InverseClient } from '../inverse-client';
import { LinearClient } from '../linear-client'; import { LinearClient } from '../linear-client';
import { SpotClient } from '../spot-client'; import { SpotClient } from '../spot-client';
@@ -13,7 +14,8 @@ export type RESTClient =
| SpotClientV3 | SpotClientV3
| USDCOptionClient | USDCOptionClient
| USDCPerpetualClient | USDCPerpetualClient
| UnifiedMarginClient; | UnifiedMarginClient
| ContractClient;
export type numberInString = string; export type numberInString = string;

View File

@@ -9,7 +9,9 @@ export type APIMarket =
| 'usdcOption' | 'usdcOption'
| 'usdcPerp' | 'usdcPerp'
| 'unifiedPerp' | 'unifiedPerp'
| 'unifiedOption'; | 'unifiedOption'
| 'contractUSDT'
| 'contractInverse';
// Same as inverse futures // Same as inverse futures
export type WsPublicInverseTopic = export type WsPublicInverseTopic =

View File

@@ -25,6 +25,7 @@ import BaseRestClient from './util/BaseRestClient';
*/ */
export class USDCPerpetualClient extends BaseRestClient { export class USDCPerpetualClient extends BaseRestClient {
getClientType() { getClientType() {
// Follows the same authentication mechanism as other v3 APIs (e.g. USDC)
return REST_CLIENT_TYPE_ENUM.v3; return REST_CLIENT_TYPE_ENUM.v3;
} }

View File

@@ -121,6 +121,26 @@ export const WS_BASE_URL_MAP: Record<
testnet: 'useUnifiedEndpoint', testnet: 'useUnifiedEndpoint',
}, },
}, },
contractUSDT: {
public: {
livenet: 'wss://stream.bybit.com/contract/usdt/public/v3',
testnet: 'wss://stream-testnet.bybit.com/contract/usdt/public/v3',
},
private: {
livenet: 'wss://stream.bybit.com/contract/private/v3',
testnet: 'wss://stream-testnet.bybit.com/contract/private/v3',
},
},
contractInverse: {
public: {
livenet: 'wss://stream.bybit.com/contract/inverse/public/v3',
testnet: 'wss://stream-testnet.bybit.com/contract/inverse/public/v3',
},
private: {
livenet: 'wss://stream.bybit.com/contract/private/v3',
testnet: 'wss://stream-testnet.bybit.com/contract/private/v3',
},
},
}; };
export const WS_KEY_MAP = { export const WS_KEY_MAP = {
@@ -139,6 +159,10 @@ export const WS_KEY_MAP = {
unifiedOptionPublic: 'unifiedOptionPublic', unifiedOptionPublic: 'unifiedOptionPublic',
unifiedPerpUSDTPublic: 'unifiedPerpUSDTPublic', unifiedPerpUSDTPublic: 'unifiedPerpUSDTPublic',
unifiedPerpUSDCPublic: 'unifiedPerpUSDCPublic', unifiedPerpUSDCPublic: 'unifiedPerpUSDCPublic',
contractUSDTPublic: 'contractUSDTPublic',
contractUSDTPrivate: 'contractUSDTPrivate',
contractInversePublic: 'contractInversePublic',
contractInversePrivate: 'contractInversePrivate',
} as const; } as const;
export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [ export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [
@@ -146,6 +170,8 @@ export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [
WS_KEY_MAP.usdcOptionPrivate, WS_KEY_MAP.usdcOptionPrivate,
WS_KEY_MAP.usdcPerpPrivate, WS_KEY_MAP.usdcPerpPrivate,
WS_KEY_MAP.unifiedPrivate, WS_KEY_MAP.unifiedPrivate,
WS_KEY_MAP.contractUSDTPrivate,
WS_KEY_MAP.contractInversePrivate,
]; ];
export const PUBLIC_WS_KEYS = [ export const PUBLIC_WS_KEYS = [
@@ -157,6 +183,8 @@ export const PUBLIC_WS_KEYS = [
WS_KEY_MAP.unifiedOptionPublic, WS_KEY_MAP.unifiedOptionPublic,
WS_KEY_MAP.unifiedPerpUSDTPublic, WS_KEY_MAP.unifiedPerpUSDTPublic,
WS_KEY_MAP.unifiedPerpUSDCPublic, WS_KEY_MAP.unifiedPerpUSDCPublic,
WS_KEY_MAP.contractUSDTPublic,
WS_KEY_MAP.contractInversePublic,
] as string[]; ] as string[];
/** Used to automatically determine if a sub request should be to the public or private ws (when there's two) */ /** Used to automatically determine if a sub request should be to the public or private ws (when there's two) */
@@ -193,6 +221,11 @@ const PRIVATE_TOPICS = [
'user.order.unifiedAccount', 'user.order.unifiedAccount',
'user.wallet.unifiedAccount', 'user.wallet.unifiedAccount',
'user.greeks.unifiedAccount', 'user.greeks.unifiedAccount',
// contract v3
'user.position.contractAccount',
'user.execution.contractAccount',
'user.order.contractAccount',
'user.wallet.contractAccount',
]; ];
export function getWsKeyForTopic( export function getWsKeyForTopic(
@@ -251,6 +284,16 @@ export function getWsKeyForTopic(
`Failed to determine wskey for unified perps topic: "${topic}` `Failed to determine wskey for unified perps topic: "${topic}`
); );
} }
case 'contractInverse': {
return isPrivateTopic
? WS_KEY_MAP.contractInversePrivate
: WS_KEY_MAP.contractInversePublic;
}
case 'contractUSDT': {
return isPrivateTopic
? WS_KEY_MAP.contractUSDTPrivate
: WS_KEY_MAP.contractUSDTPublic;
}
default: { default: {
throw neverGuard(market, `getWsKeyForTopic(): Unhandled market`); throw neverGuard(market, `getWsKeyForTopic(): Unhandled market`);
} }
@@ -267,7 +310,9 @@ export function getMaxTopicsPerSubscribeEvent(
case 'usdcPerp': case 'usdcPerp':
case 'unifiedOption': case 'unifiedOption':
case 'unifiedPerp': case 'unifiedPerp':
case 'spot': { case 'spot':
case 'contractInverse':
case 'contractUSDT': {
return null; return null;
} }
case 'spotv3': { case 'spotv3': {

View File

@@ -5,6 +5,10 @@ import { InverseClient } from './inverse-client';
import { LinearClient } from './linear-client'; import { LinearClient } from './linear-client';
import { SpotClientV3 } from './spot-client-v3'; import { SpotClientV3 } from './spot-client-v3';
import { SpotClient } from './spot-client'; import { SpotClient } from './spot-client';
import { USDCOptionClient } from './usdc-option-client';
import { USDCPerpetualClient } from './usdc-perpetual-client';
import { UnifiedMarginClient } from './unified-margin-client';
import { ContractClient } from './contract-client';
import { signMessage } from './util/node-support'; import { signMessage } from './util/node-support';
import WsStore from './util/WsStore'; import WsStore from './util/WsStore';
@@ -32,9 +36,6 @@ import {
neverGuard, neverGuard,
getMaxTopicsPerSubscribeEvent, getMaxTopicsPerSubscribeEvent,
} from './util'; } from './util';
import { USDCOptionClient } from './usdc-option-client';
import { USDCPerpetualClient } from './usdc-perpetual-client';
import { UnifiedMarginClient } from './unified-margin-client';
const loggerCategory = { category: 'bybit-ws' }; const loggerCategory = { category: 'bybit-ws' };
@@ -106,6 +107,9 @@ export class WebsocketClient extends EventEmitter {
}; };
this.prepareRESTClient(); this.prepareRESTClient();
// add default error handling so this doesn't crash node (if the user didn't set a handler)
this.on('error', () => {});
} }
/** /**
@@ -232,6 +236,14 @@ export class WebsocketClient extends EventEmitter {
); );
break; break;
} }
case 'contractInverse':
case 'contractUSDT': {
this.restClient = new ContractClient(
this.options.restOptions,
this.options.requestOptions
);
break;
}
default: { default: {
throw neverGuard( throw neverGuard(
this.options.market, this.options.market,
@@ -264,6 +276,7 @@ export class WebsocketClient extends EventEmitter {
public closeAll(force?: boolean) { public closeAll(force?: boolean) {
const keys = this.wsStore.getKeys(); const keys = this.wsStore.getKeys();
this.logger.info(`Closing all ws connections: ${keys}`);
keys.forEach((key) => { keys.forEach((key) => {
this.close(key, force); this.close(key, force);
}); });
@@ -285,7 +298,9 @@ export class WebsocketClient extends EventEmitter {
case 'usdcOption': case 'usdcOption':
case 'usdcPerp': case 'usdcPerp':
case 'unifiedPerp': case 'unifiedPerp':
case 'unifiedOption': { case 'unifiedOption':
case 'contractUSDT':
case 'contractInverse': {
return [...this.connectPublic(), this.connectPrivate()]; return [...this.connectPublic(), this.connectPrivate()];
} }
default: { default: {
@@ -323,6 +338,10 @@ export class WebsocketClient extends EventEmitter {
this.connect(WS_KEY_MAP.unifiedPerpUSDCPublic), this.connect(WS_KEY_MAP.unifiedPerpUSDCPublic),
]; ];
} }
case 'contractUSDT':
return [this.connect(WS_KEY_MAP.contractUSDTPublic)];
case 'contractInverse':
return [this.connect(WS_KEY_MAP.contractInversePublic)];
default: { default: {
throw neverGuard( throw neverGuard(
this.options.market, this.options.market,
@@ -356,6 +375,10 @@ export class WebsocketClient extends EventEmitter {
case 'unifiedOption': { case 'unifiedOption': {
return this.connect(WS_KEY_MAP.unifiedPrivate); return this.connect(WS_KEY_MAP.unifiedPrivate);
} }
case 'contractUSDT':
return this.connect(WS_KEY_MAP.contractUSDTPrivate);
case 'contractInverse':
return this.connect(WS_KEY_MAP.contractInversePrivate);
default: { default: {
throw neverGuard( throw neverGuard(
this.options.market, this.options.market,
@@ -399,7 +422,7 @@ export class WebsocketClient extends EventEmitter {
return this.wsStore.setWs(wsKey, ws); return this.wsStore.setWs(wsKey, ws);
} catch (err) { } catch (err) {
this.parseWsError('Connection failed', err, wsKey); this.parseWsError('Connection failed', err, wsKey);
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!); this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
} }
} }
@@ -419,12 +442,22 @@ export class WebsocketClient extends EventEmitter {
break; break;
default: default:
this.logger.error( if (
`${context} due to unexpected response error: "${ this.wsStore.getConnectionState(wsKey) !==
error?.msg || error?.message || error WsConnectionStateEnum.CLOSING
}"`, ) {
{ ...loggerCategory, wsKey, error } this.logger.error(
); `${context} due to unexpected response error: "${
error?.msg || error?.message || error
}"`,
{ ...loggerCategory, wsKey, error }
);
this.executeReconnectableClose(wsKey, 'unhandled onWsError');
} else {
this.logger.info(
`${wsKey} socket forcefully closed. Will not reconnect.`
);
}
break; break;
} }
this.emit('error', error); this.emit('error', error);
@@ -518,11 +551,16 @@ export class WebsocketClient extends EventEmitter {
this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING); this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING);
} }
if (this.wsStore.get(wsKey)?.activeReconnectTimer) {
this.clearReconnectTimer(wsKey);
}
this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => { this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => {
this.logger.info('Reconnecting to websocket', { this.logger.info('Reconnecting to websocket', {
...loggerCategory, ...loggerCategory,
wsKey, wsKey,
}); });
this.clearReconnectTimer(wsKey);
this.connect(wsKey); this.connect(wsKey);
}, connectionDelayMs); }, connectionDelayMs);
} }
@@ -537,23 +575,47 @@ export class WebsocketClient extends EventEmitter {
this.logger.silly('Sending ping', { ...loggerCategory, wsKey }); this.logger.silly('Sending ping', { ...loggerCategory, wsKey });
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', { () => this.executeReconnectableClose(wsKey, 'Pong timeout'),
...loggerCategory, this.options.pongTimeout
wsKey, );
}); }
this.getWs(wsKey)?.terminate();
delete this.wsStore.get(wsKey, true).activePongTimer; /**
}, this.options.pongTimeout); * Closes a connection, if it's even open. If open, this will trigger a reconnect asynchronously.
* If closed, trigger a reconnect immediately
*/
private executeReconnectableClose(wsKey: WsKey, reason: string) {
this.logger.info(`${reason} - closing socket to reconnect`, {
...loggerCategory,
wsKey,
reason,
});
const wasOpen = this.wsStore.isWsOpen(wsKey);
this.getWs(wsKey)?.terminate();
delete this.wsStore.get(wsKey, true).activePongTimer;
this.clearPingTimer(wsKey);
this.clearPongTimer(wsKey);
if (!wasOpen) {
this.logger.info(
`${reason} - socket already closed - trigger immediate reconnect`,
{
...loggerCategory,
wsKey,
reason,
}
);
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
}
} }
private clearTimers(wsKey: WsKey) { private clearTimers(wsKey: WsKey) {
this.clearPingTimer(wsKey); this.clearPingTimer(wsKey);
this.clearPongTimer(wsKey); this.clearPongTimer(wsKey);
const wsState = this.wsStore.get(wsKey); this.clearReconnectTimer(wsKey);
if (wsState?.activeReconnectTimer) {
clearTimeout(wsState.activeReconnectTimer);
}
} }
// Send a ping at intervals // Send a ping at intervals
@@ -574,6 +636,14 @@ export class WebsocketClient extends EventEmitter {
} }
} }
private clearReconnectTimer(wsKey: WsKey) {
const wsState = this.wsStore.get(wsKey);
if (wsState?.activeReconnectTimer) {
clearTimeout(wsState.activeReconnectTimer);
wsState.activeReconnectTimer = undefined;
}
}
/** /**
* @private Use the `subscribe(topics)` method to subscribe to topics. Send WS message to subscribe to topics. * @private Use the `subscribe(topics)` method to subscribe to topics. Send WS message to subscribe to topics.
*/ */
@@ -682,7 +752,8 @@ export class WebsocketClient extends EventEmitter {
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.parseWsError('Websocket onWsError', event, wsKey);
ws.onclose = (event) => this.onWsClose(event, wsKey); ws.onclose = (event) => this.onWsClose(event, wsKey);
return ws; return ws;
@@ -781,10 +852,6 @@ export class WebsocketClient extends EventEmitter {
} }
} }
private onWsError(error: any, wsKey: WsKey) {
this.parseWsError('Websocket error', error, wsKey);
}
private onWsClose(event, wsKey: WsKey) { private onWsClose(event, wsKey: WsKey) {
this.logger.info('Websocket connection closed', { this.logger.info('Websocket connection closed', {
...loggerCategory, ...loggerCategory,
@@ -794,7 +861,7 @@ export class WebsocketClient extends EventEmitter {
if ( if (
this.wsStore.getConnectionState(wsKey) !== WsConnectionStateEnum.CLOSING this.wsStore.getConnectionState(wsKey) !== WsConnectionStateEnum.CLOSING
) { ) {
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!); this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
this.emit('reconnect', { wsKey, event }); this.emit('reconnect', { wsKey, event });
} else { } else {
this.setWsState(wsKey, WsConnectionStateEnum.INITIAL); this.setWsState(wsKey, WsConnectionStateEnum.INITIAL);
@@ -864,6 +931,18 @@ export class WebsocketClient extends EventEmitter {
case WS_KEY_MAP.unifiedPrivate: { case WS_KEY_MAP.unifiedPrivate: {
return WS_BASE_URL_MAP.unifiedPerp.private[networkKey]; return WS_BASE_URL_MAP.unifiedPerp.private[networkKey];
} }
case WS_KEY_MAP.contractInversePrivate: {
return WS_BASE_URL_MAP.contractInverse.private[networkKey];
}
case WS_KEY_MAP.contractInversePublic: {
return WS_BASE_URL_MAP.contractInverse.public[networkKey];
}
case WS_KEY_MAP.contractUSDTPrivate: {
return WS_BASE_URL_MAP.contractUSDT.private[networkKey];
}
case WS_KEY_MAP.contractUSDTPublic: {
return WS_BASE_URL_MAP.contractUSDT.public[networkKey];
}
default: { default: {
this.logger.error('getWsUrl(): Unhandled wsKey: ', { this.logger.error('getWsUrl(): Unhandled wsKey: ', {
...loggerCategory, ...loggerCategory,

View File

@@ -0,0 +1,71 @@
import { API_ERROR_CODE, ContractClient } from '../../src';
import { successResponseObjectV3 } from '../response.util';
describe('Private Contract REST API GET Endpoints', () => {
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 ContractClient({
key: API_KEY,
secret: API_SECRET,
testnet: false,
});
const symbol = 'BTCUSDT';
it('getHistoricOrders()', async () => {
expect(await api.getHistoricOrders({ symbol })).toMatchObject(
successResponseObjectV3()
);
});
it('getActiveOrders()', async () => {
expect(await api.getActiveOrders({ symbol })).toMatchObject(
successResponseObjectV3()
);
});
it('getPositions()', async () => {
expect(await api.getPositions({ symbol })).toMatchObject(
successResponseObjectV3()
);
});
it('getUserExecutionHistory()', async () => {
expect(await api.getUserExecutionHistory({ symbol })).toMatchObject(
successResponseObjectV3()
);
});
it('getClosedProfitAndLoss()', async () => {
expect(await api.getClosedProfitAndLoss({ symbol })).toMatchObject(
successResponseObjectV3()
);
});
it('getOpenInterestLimitInfo()', async () => {
expect(await api.getOpenInterestLimitInfo(symbol)).toMatchObject(
successResponseObjectV3()
);
});
it('getBalances()', async () => {
expect(await api.getBalances()).toMatchObject(successResponseObjectV3());
});
it('getTradingFeeRate()', async () => {
expect(await api.getTradingFeeRate()).toMatchObject(
successResponseObjectV3()
);
});
it('getWalletFundRecords()', async () => {
expect(await api.getWalletFundRecords()).toMatchObject(
successResponseObjectV3()
);
});
});

View File

@@ -0,0 +1,139 @@
import { API_ERROR_CODE, ContractClient } from '../../src';
import { successResponseObjectV3 } from '../response.util';
describe('Private Contract REST API POST Endpoints', () => {
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 ContractClient({
key: API_KEY,
secret: API_SECRET,
testnet: false,
});
const symbol = 'BTCUSDT';
/**
* While it may seem silly, these tests simply validate the request is processed at all.
* Something very wrong would be a sign error or complaints about the endpoint/request method/server error.
*/
it('submitOrder()', async () => {
expect(
await api.submitOrder({
symbol,
side: 'Sell',
orderType: 'Limit',
qty: '1',
price: '20000',
orderLinkId: Date.now().toString(),
timeInForce: 'GoodTillCancel',
positionIdx: '2',
})
).toMatchObject({
// retMsg: '',
retCode: API_ERROR_CODE.CONTRACT_INSUFFICIENT_BALANCE,
});
});
it('cancelOrder()', async () => {
expect(
await api.cancelOrder({
symbol,
orderId: 'somethingFake1',
})
).toMatchObject({
retCode: API_ERROR_CODE.CONTRACT_ORDER_NOT_EXISTS,
});
});
it('cancelAllOrders()', async () => {
expect(await api.cancelAllOrders(symbol)).toMatchObject(
successResponseObjectV3()
);
});
it('modifyOrder()', async () => {
expect(
await api.modifyOrder({
symbol,
orderId: 'somethingFake',
price: '20000',
})
).toMatchObject({
retCode: API_ERROR_CODE.CONTRACT_ORDER_NOT_EXISTS,
});
});
it('setAutoAddMargin()', async () => {
expect(
await api.setAutoAddMargin({
autoAddMargin: 1,
side: 'Buy',
symbol,
positionIdx: 1,
})
).toMatchObject({
retMsg: expect.stringMatching(/not modified/gim),
retCode: API_ERROR_CODE.PARAMS_MISSING_OR_WRONG,
});
});
it('setMarginSwitch()', async () => {
expect(
await api.setMarginSwitch({
symbol,
tradeMode: 1,
buyLeverage: '5',
sellLeverage: '5',
})
).toMatchObject({
retCode: API_ERROR_CODE.CONTRACT_MARGIN_MODE_NOT_MODIFIED,
});
});
it('setPositionMode()', async () => {
expect(
await api.setPositionMode({
symbol,
mode: 3,
})
).toMatchObject({
retCode: API_ERROR_CODE.CONTRACT_POSITION_MODE_NOT_MODIFIED,
});
});
it('setTPSLMode()', async () => {
expect(await api.setTPSLMode(symbol, 'Full')).toMatchObject({
retCode: API_ERROR_CODE.PARAMS_MISSING_OR_WRONG,
retMsg: expect.stringMatching(/same/gim),
});
});
it('setLeverage()', async () => {
expect(await api.setLeverage(symbol, '5', '5')).toMatchObject({
retCode: API_ERROR_CODE.CONTRACT_SET_LEVERAGE_NOT_MODIFIED,
});
});
it('setTPSL()', async () => {
expect(
await api.setTPSL({ symbol, positionIdx: 1, stopLoss: '100' })
).toMatchObject({
retMsg: expect.stringMatching(/zero position/gim),
retCode: API_ERROR_CODE.PARAMS_MISSING_OR_WRONG,
});
});
it('setRiskLimit()', async () => {
expect(await api.setRiskLimit(symbol, 43, 2)).toMatchObject({
// retMsg: '',
retCode: API_ERROR_CODE.CONTRACT_RISK_LIMIT_INFO_NOT_EXISTS,
});
});
});

View File

@@ -0,0 +1,103 @@
import { UMCandlesRequest, ContractClient } from '../../src';
import {
successResponseObject,
successResponseObjectV3,
} from '../response.util';
describe('Public Contract REST API Endpoints', () => {
const API_KEY = undefined;
const API_SECRET = undefined;
const api = new ContractClient({
key: API_KEY,
secret: API_SECRET,
testnet: false,
});
const symbol = 'BTCUSDT';
const category = 'linear';
const start = Number((Date.now() / 1000).toFixed(0));
const end = start + 1000 * 60 * 60 * 24;
const interval = '1';
const candleRequest: UMCandlesRequest = {
category,
symbol,
interval,
start,
end,
};
it('getOrderBook()', async () => {
expect(await api.getOrderBook(symbol, category)).toMatchObject(
successResponseObjectV3()
);
});
it('getCandles()', async () => {
expect(await api.getCandles(candleRequest)).toMatchObject(
successResponseObjectV3()
);
});
it('getSymbolTicker()', async () => {
expect(await api.getSymbolTicker(category)).toMatchObject(
successResponseObjectV3()
);
});
it('getInstrumentInfo()', async () => {
expect(await api.getInstrumentInfo({ category })).toMatchObject(
successResponseObjectV3()
);
});
it('getMarkPriceCandles()', async () => {
expect(await api.getMarkPriceCandles(candleRequest)).toMatchObject(
successResponseObjectV3()
);
});
it('getIndexPriceCandles()', async () => {
expect(await api.getIndexPriceCandles(candleRequest)).toMatchObject(
successResponseObjectV3()
);
});
it('getFundingRateHistory()', async () => {
expect(
await api.getFundingRateHistory({
category,
symbol,
})
).toMatchObject(successResponseObjectV3());
});
it('getRiskLimit()', async () => {
expect(await api.getRiskLimit(category, symbol)).toMatchObject(
successResponseObjectV3()
);
});
it('getOptionDeliveryPrice()', async () => {
expect(await api.getOptionDeliveryPrice({ category })).toMatchObject(
successResponseObjectV3()
);
});
it('getTrades()', async () => {
expect(await api.getTrades({ category, symbol })).toMatchObject(
successResponseObjectV3()
);
});
it('getOpenInterest()', async () => {
expect(
await api.getOpenInterest({ symbol, category, interval: '5min' })
).toMatchObject(successResponseObjectV3());
});
it('getServerTime()', async () => {
expect(await api.getServerTime()).toMatchObject(successResponseObject());
});
});

View File

@@ -0,0 +1,74 @@
import {
WebsocketClient,
WSClientConfigurableOptions,
WS_KEY_MAP,
} from '../../src';
import {
logAllEvents,
getSilentLogger,
waitForSocketEvent,
WS_OPEN_EVENT_PARTIAL,
fullLogger,
} from '../ws.util';
describe('Private Contract Websocket Client', () => {
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
let wsClient: WebsocketClient;
const wsClientOptions: WSClientConfigurableOptions = {
market: 'contractUSDT',
key: API_KEY,
secret: API_SECRET,
};
beforeAll(() => {
wsClient = new WebsocketClient(
wsClientOptions,
getSilentLogger('expectSuccessNoAuth')
// fullLogger
);
// logAllEvents(wsClient);
wsClient.connectPrivate();
});
afterAll(() => {
wsClient.closeAll(true);
});
it('should open a private ws connection', async () => {
const wsOpenPromise = waitForSocketEvent(wsClient, 'open');
try {
expect(await wsOpenPromise).toMatchObject({
event: WS_OPEN_EVENT_PARTIAL,
wsKey: WS_KEY_MAP.contractUSDTPrivate,
});
} catch (e) {
expect(e).toBeFalsy();
}
});
it('should authenticate successfully', async () => {
const wsResponsePromise = waitForSocketEvent(wsClient, 'response');
// const wsUpdatePromise = waitForSocketEvent(wsClient, 'update');
try {
expect(await wsResponsePromise).toMatchObject({
op: 'auth',
req_id: 'contractUSDTPrivate-auth',
success: true,
wsKey: WS_KEY_MAP.contractUSDTPrivate,
});
} catch (e) {
// sub failed
expect(e).toBeFalsy();
}
// try {
// expect(await wsUpdatePromise).toStrictEqual('');
// } catch (e) {
// // no data
// expect(e).toBeFalsy();
// }
});
});

View File

@@ -0,0 +1,74 @@
import {
WebsocketClient,
WSClientConfigurableOptions,
WS_KEY_MAP,
} from '../../src';
import {
logAllEvents,
getSilentLogger,
waitForSocketEvent,
WS_OPEN_EVENT_PARTIAL,
} from '../ws.util';
describe('Public Contract Inverse Websocket Client', () => {
let wsClient: WebsocketClient;
const wsClientOptions: WSClientConfigurableOptions = {
market: 'contractInverse',
};
beforeAll(() => {
wsClient = new WebsocketClient(
wsClientOptions,
getSilentLogger('expectSuccessNoAuth')
);
wsClient.connectPublic();
});
afterAll(() => {
wsClient.closeAll(true);
});
it('should open a public ws connection', async () => {
const wsOpenPromise = waitForSocketEvent(wsClient, 'open');
try {
expect(await wsOpenPromise).toMatchObject({
event: WS_OPEN_EVENT_PARTIAL,
wsKey: WS_KEY_MAP.contractInversePublic,
});
} catch (e) {
console.error('open: ', e);
expect(e).toBeFalsy();
}
});
it('should subscribe to public orderbook events', async () => {
const wsResponsePromise = waitForSocketEvent(wsClient, 'response');
const wsUpdatePromise = waitForSocketEvent(wsClient, 'update');
const wsTopic = 'orderbook.25.BTCUSD';
wsClient.subscribe(wsTopic);
try {
expect(await wsResponsePromise).toMatchObject({
success: true,
op: 'subscribe',
});
} catch (e) {
console.error('response: ', e);
// sub failed (or jest expect threw)
expect(e).toBeFalsy();
}
try {
expect(await wsUpdatePromise).toMatchObject({
topic: wsTopic,
data: {
a: expect.any(Array),
},
});
} catch (e) {
console.error(`Wait for "${wsTopic}" event exception: `, e);
}
});
});

View File

@@ -0,0 +1,74 @@
import {
WebsocketClient,
WSClientConfigurableOptions,
WS_KEY_MAP,
} from '../../src';
import {
logAllEvents,
getSilentLogger,
waitForSocketEvent,
WS_OPEN_EVENT_PARTIAL,
} from '../ws.util';
describe('Public Contract USDT Websocket Client', () => {
let wsClient: WebsocketClient;
const wsClientOptions: WSClientConfigurableOptions = {
market: 'contractUSDT',
};
beforeAll(() => {
wsClient = new WebsocketClient(
wsClientOptions,
getSilentLogger('expectSuccessNoAuth')
);
wsClient.connectPublic();
});
afterAll(() => {
wsClient.closeAll(true);
});
it('should open a public ws connection', async () => {
const wsOpenPromise = waitForSocketEvent(wsClient, 'open');
try {
expect(await wsOpenPromise).toMatchObject({
event: WS_OPEN_EVENT_PARTIAL,
wsKey: WS_KEY_MAP.contractUSDTPublic,
});
} catch (e) {
console.error('open: ', e);
expect(e).toBeFalsy();
}
});
it('should subscribe to public orderbook events', async () => {
const wsResponsePromise = waitForSocketEvent(wsClient, 'response');
const wsUpdatePromise = waitForSocketEvent(wsClient, 'update');
const wsTopic = 'orderbook.25.BTCUSDT';
wsClient.subscribe(wsTopic);
try {
expect(await wsResponsePromise).toMatchObject({
success: true,
op: 'subscribe',
});
} catch (e) {
console.error('response: ', e);
// sub failed (or jest expect threw)
expect(e).toBeFalsy();
}
try {
expect(await wsUpdatePromise).toMatchObject({
topic: wsTopic,
data: {
a: expect.any(Array),
},
});
} catch (e) {
console.error(`Wait for "${wsTopic}" event exception: `, e);
}
});
});

View File

@@ -110,6 +110,7 @@ describe('Private Inverse REST API POST Endpoints', () => {
symbol, symbol,
p_r_price: '50000', p_r_price: '50000',
p_r_qty: 1, p_r_qty: 1,
order_link_id: 'fakeorderid',
}) })
).toMatchObject({ ).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,

View File

@@ -40,7 +40,7 @@ describe('Private Inverse Perps Websocket Client', () => {
// console.error() // console.error()
expect(e?.message).toStrictEqual('Unexpected server response: 401'); expect(e?.message).toStrictEqual('Unexpected server response: 401');
} }
badClient.closeAll(); badClient.closeAll(true);
}); });
}); });
@@ -62,7 +62,7 @@ describe('Private Inverse Perps Websocket Client', () => {
afterAll(() => { afterAll(() => {
// await promiseSleep(2000); // await promiseSleep(2000);
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a ws connection', async () => { it('should open a ws connection', async () => {

View File

@@ -24,7 +24,7 @@ describe('Public Inverse Perps Websocket Client', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a public ws connection', async () => { it('should open a public ws connection', async () => {

View File

@@ -176,7 +176,9 @@ describe('Private Linear REST API POST Endpoints', () => {
margin: 5, margin: 5,
}) })
).toMatchObject({ ).toMatchObject({
// ret_msg: '',
ret_code: API_ERROR_CODE.POSITION_SIZE_IS_ZERO, ret_code: API_ERROR_CODE.POSITION_SIZE_IS_ZERO,
// ret_code: API_ERROR_CODE.ISOLATED_NOT_MODIFIED_LINEAR,
}); });
}); });

View File

@@ -38,7 +38,7 @@ describe('Private Linear Perps Websocket Client', () => {
} catch (e) { } catch (e) {
expect(e?.message).toStrictEqual('Unexpected server response: 401'); expect(e?.message).toStrictEqual('Unexpected server response: 401');
} }
badClient.closeAll(); badClient.closeAll(true);
}); });
}); });
@@ -65,7 +65,7 @@ describe('Private Linear Perps Websocket Client', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a ws connection', async () => { it('should open a ws connection', async () => {

View File

@@ -22,7 +22,7 @@ describe('Public Linear Perps Websocket Client', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a public ws connection', async () => { it('should open a public ws connection', async () => {

View File

@@ -9,6 +9,7 @@ import {
getSilentLogger, getSilentLogger,
waitForSocketEvent, waitForSocketEvent,
WS_OPEN_EVENT_PARTIAL, WS_OPEN_EVENT_PARTIAL,
fullLogger,
} from '../ws.util'; } from '../ws.util';
describe('Private Spot V1 Websocket Client', () => { describe('Private Spot V1 Websocket Client', () => {
@@ -30,13 +31,14 @@ describe('Private Spot V1 Websocket Client', () => {
beforeAll(() => { beforeAll(() => {
wsClient = new WebsocketClient( wsClient = new WebsocketClient(
wsClientOptions, wsClientOptions,
// fullLogger
getSilentLogger('expectSuccess') getSilentLogger('expectSuccess')
); );
logAllEvents(wsClient); logAllEvents(wsClient);
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
// TODO: how to detect if auth failed for the v1 spot ws // TODO: how to detect if auth failed for the v1 spot ws

View File

@@ -50,7 +50,7 @@ describe('Private Spot V3 Websocket Client', () => {
} catch (e) { } catch (e) {
// console.error() // console.error()
} }
badClient.closeAll(); badClient.closeAll(true);
}); });
}); });
@@ -72,7 +72,7 @@ describe('Private Spot V3 Websocket Client', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a private ws connection', async () => { it('should open a private ws connection', async () => {

View File

@@ -28,7 +28,7 @@ describe('Public Spot V1 Websocket Client', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a public ws connection', async () => { it('should open a public ws connection', async () => {

View File

@@ -28,7 +28,7 @@ describe('Public Spot V3 Websocket Client', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a public ws connection', async () => { it('should open a public ws connection', async () => {

View File

@@ -12,10 +12,15 @@ import {
} from '../ws.util'; } from '../ws.util';
describe('Private Unified Margin Websocket Client', () => { describe('Private Unified Margin Websocket Client', () => {
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
let wsClient: WebsocketClient; let wsClient: WebsocketClient;
const wsClientOptions: WSClientConfigurableOptions = { const wsClientOptions: WSClientConfigurableOptions = {
market: 'unifiedPerp', market: 'unifiedPerp',
key: API_KEY,
secret: API_SECRET,
}; };
beforeAll(() => { beforeAll(() => {
@@ -29,7 +34,7 @@ describe('Private Unified Margin Websocket Client', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a public ws connection', async () => { it('should open a public ws connection', async () => {

View File

@@ -26,7 +26,7 @@ describe('Public Unified Margin Websocket Client (Options)', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a public ws connection', async () => { it('should open a public ws connection', async () => {

View File

@@ -28,10 +28,6 @@ describe('Public Unified Margin Websocket Client (Perps - USDC)', () => {
wsClient.connectPublic(); wsClient.connectPublic();
}); });
afterAll(() => {
wsClient.closeAll();
});
it('should open a public ws connection', async () => { it('should open a public ws connection', async () => {
const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); const wsOpenPromise = waitForSocketEvent(wsClient, 'open');
try { try {
@@ -42,6 +38,8 @@ describe('Public Unified Margin Websocket Client (Perps - USDC)', () => {
} catch (e) { } catch (e) {
expect(e).toBeFalsy(); expect(e).toBeFalsy();
} }
wsClient.closeAll(true);
}); });
// TODO: are there USDC topics? This doesn't seem to work // TODO: are there USDC topics? This doesn't seem to work

View File

@@ -29,7 +29,7 @@ describe('Public Unified Margin Websocket Client (Perps - USDT)', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a public ws connection', async () => { it('should open a public ws connection', async () => {

View File

@@ -60,7 +60,7 @@ describe('Private USDC Option Websocket Client', () => {
// badClient.subscribe(wsTopic); // badClient.subscribe(wsTopic);
badClient.removeAllListeners(); badClient.removeAllListeners();
badClient.closeAll(); badClient.closeAll(true);
}); });
}); });
@@ -81,7 +81,7 @@ describe('Private USDC Option Websocket Client', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a private ws connection', async () => { it('should open a private ws connection', async () => {

View File

@@ -26,7 +26,7 @@ describe('Public USDC Option Websocket Client', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a public ws connection', async () => { it('should open a public ws connection', async () => {

View File

@@ -60,7 +60,7 @@ describe('Private USDC Perp Websocket Client', () => {
// badClient.subscribe(wsTopic); // badClient.subscribe(wsTopic);
badClient.removeAllListeners(); badClient.removeAllListeners();
badClient.closeAll(); badClient.closeAll(true);
}); });
}); });
@@ -82,7 +82,7 @@ describe('Private USDC Perp Websocket Client', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a private ws connection', async () => { it('should open a private ws connection', async () => {

View File

@@ -27,7 +27,7 @@ describe('Public USDC Perp Websocket Client', () => {
}); });
afterAll(() => { afterAll(() => {
wsClient.closeAll(); wsClient.closeAll(true);
}); });
it('should open a public ws connection', async () => { it('should open a public ws connection', async () => {