diff --git a/.github/workflows/integrationtest.yml b/.github/workflows/e2etest.yml similarity index 100% rename from .github/workflows/integrationtest.yml rename to .github/workflows/e2etest.yml diff --git a/README.md b/README.md index 3b337c0..2701469 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Node.js connector for the Bybit APIs and WebSockets: - Complete integration with all bybit APIs. - 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. - Browser support (via webpack bundle - see "Browser Usage" below). @@ -25,10 +25,10 @@ Node.js connector for the Bybit APIs and WebSockets: ## Related projects Check out my related projects: - 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) + - [bybit-api](https://www.npmjs.com/package/bybit-api) - [okx-api](https://www.npmjs.com/package/okx-api) + - [ftx-api](https://www.npmjs.com/package/ftx-api) - Try my misc utilities: - [orderbooks](https://www.npmjs.com/package/orderbooks) - 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. | | 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. | +| 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 const { WebsocketClient } = require('bybit-api'); diff --git a/examples/rest-contract-private.ts b/examples/rest-contract-private.ts new file mode 100644 index 0000000..8a0801b --- /dev/null +++ b/examples/rest-contract-private.ts @@ -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); + } +})(); diff --git a/examples/ws-private.ts b/examples/ws-private.ts index 30fa731..ae55f56 100644 --- a/examples/ws-private.ts +++ b/examples/ws-private.ts @@ -13,10 +13,13 @@ import { WebsocketClient, WS_KEY_MAP, DefaultLogger } from '../src'; const secret = process.env.API_SECRET; // USDT Perps: - const market = 'linear'; + // const market = 'linear'; // Inverse Perp // const market = 'inverse'; // const market = 'spotv3'; + // Contract v3 + const market = 'contractUSDT'; + // const market = 'contractInverse'; // Note: the WebsocketClient defaults to testnet. Set `livenet: true` to use live markets. const wsClient = new WebsocketClient( @@ -48,8 +51,19 @@ import { WebsocketClient, WS_KEY_MAP, DefaultLogger } from '../src'; wsClient.on('reconnected', (data) => { console.log('ws has reconnected ', data?.wsKey); }); + wsClient.on('error', (data) => { + console.error('ws exception: ', data); + }); // subscribe to private endpoints // 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', + ]); })(); diff --git a/examples/ws-public.ts b/examples/ws-public.ts index c3b01dd..71362ad 100644 --- a/examples/ws-public.ts +++ b/examples/ws-public.ts @@ -51,6 +51,9 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src'; wsClient.on('reconnected', (data) => { console.log('ws has reconnected ', data?.wsKey); }); + // wsClient.on('error', (data) => { + // console.error('ws exception: ', data); + // }); // Inverse // wsClient.subscribe('trade'); diff --git a/package.json b/package.json index 2d7cf31..418972e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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.", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/constants/enum.ts b/src/constants/enum.ts index 3ac4d79..68d9514 100644 --- a/src/constants/enum.ts +++ b/src/constants/enum.ts @@ -54,6 +54,12 @@ export const API_ERROR_CODE = { INSUFFICIENT_BALANCE_FOR_ORDER_COST_LINEAR: 130080, SAME_SLTP_MODE_LINEAR: 130150, 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 */ CONTRACT_NAME_NOT_EXIST: 3100111, ORDER_NOT_EXIST: 3100136, diff --git a/src/contract-client.ts b/src/contract-client.ts new file mode 100644 index 0000000..6844b1c --- /dev/null +++ b/src/contract-client.ts @@ -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 { + 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> { + return this.get('/derivatives/v3/public/order-book/L2', { + category, + symbol, + limit, + }); + } + + /** Get candles/klines */ + getCandles(params: UMCandlesRequest): Promise> { + return this.get('/derivatives/v3/public/kline', params); + } + + /** Get a symbol price/statistics ticker */ + getSymbolTicker( + category: UMCategory, + symbol?: string + ): Promise> { + 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> { + return this.get('/derivatives/v3/public/instruments-info', params); + } + + /** Query mark price kline (like getCandles() but for mark price). */ + getMarkPriceCandles(params: UMCandlesRequest): Promise> { + return this.get('/derivatives/v3/public/mark-price-kline', params); + } + + /** Query Index Price Kline */ + getIndexPriceCandles(params: UMCandlesRequest): Promise> { + 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> { + return this.get( + '/derivatives/v3/public/funding/history-funding-rate', + params + ); + } + + /** Get Risk Limit */ + getRiskLimit( + category: UMCategory, + symbol: string + ): Promise> { + return this.get('/derivatives/v3/public/risk-limit/list', { + category, + symbol, + }); + } + + /** Get option delivery price */ + getOptionDeliveryPrice( + params: UMOptionDeliveryPriceRequest + ): Promise> { + return this.get('/derivatives/v3/public/delivery-price', params); + } + + /** Get public trading history */ + getTrades(params: UMPublicTradesRequest): Promise> { + 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> { + return this.get('/derivatives/v3/public/open-interest', params); + } + + /** + * + * Contract Account Endpoints + * + */ + + /** -> Order API */ + + /** Place an order */ + submitOrder(params: ContractOrderRequest): Promise> { + 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> { + return this.getPrivate('/contract/v3/private/order/list', params); + } + + /** Cancel order */ + cancelOrder(params: ContractCancelOrderRequest): Promise> { + return this.postPrivate('/contract/v3/private/order/cancel', params); + } + + /** Cancel all orders */ + cancelAllOrders(symbol: string): Promise> { + 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> { + return this.postPrivate('/contract/v3/private/order/replace', params); + } + + /** Query Open Order(s) (real-time) */ + getActiveOrders( + params: ContractActiveOrdersRequest + ): Promise> { + 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> { + return this.getPrivate('/contract/v3/private/position/list', params); + } + + /** Set auto add margin, or Auto-Margin Replenishment. */ + setAutoAddMargin( + params: ContractSetAutoAddMarginRequest + ): Promise> { + return this.postPrivate( + '/contract/v3/private/position/set-auto-add-margin', + params + ); + } + + /** Switch cross margin mode/isolated margin mode */ + setMarginSwitch( + params: ContractSetMarginSwitchRequest + ): Promise> { + 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> { + return this.postPrivate( + '/contract/v3/private/position/switch-mode', + params + ); + } + + /** + * Switch mode between Full or Partial + */ + setTPSLMode( + symbol: string, + tpSlMode: 'Full' | 'Partial' + ): Promise> { + return this.postPrivate('/contract/v3/private/position/switch-tpsl-mode', { + symbol, + tpSlMode, + }); + } + + /** Leverage setting. */ + setLeverage( + symbol: string, + buyLeverage: string, + sellLeverage: string + ): Promise> { + 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> { + 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> { + 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> { + 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> { + return this.getPrivate('/contract/v3/private/position/closed-pnl', params); + } + + /** Get the information of open interest limit. */ + getOpenInterestLimitInfo(symbol: string): Promise> { + return this.getPrivate('/contract/v3/private/position/closed-pnl', { + symbol, + }); + } + + /** -> Account API */ + + /** Query wallet balance */ + getBalances(coin?: string): Promise> { + return this.getPrivate('/contract/v3/private/account/wallet/balance', { + coin, + }); + } + + /** Get user trading fee rate */ + getTradingFeeRate(symbol?: string): Promise> { + 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> { + return this.getPrivate( + '/contract/v3/private/account/wallet/fund-records', + params + ); + } + + /** + * + * API Data Endpoints + * + */ + + getServerTime(): Promise { + return this.get('/v2/public/time'); + } +} diff --git a/src/index.ts b/src/index.ts index 4f8c4c4..62836ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export * from './spot-client-v3'; export * from './usdc-option-client'; export * from './usdc-perpetual-client'; export * from './unified-margin-client'; +export * from './contract-client'; export * from './websocket-client'; export * from './util/logger'; export * from './util'; diff --git a/src/types/request/contract.ts b/src/types/request/contract.ts new file mode 100644 index 0000000..125df40 --- /dev/null +++ b/src/types/request/contract.ts @@ -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; +} diff --git a/src/types/request/index.ts b/src/types/request/index.ts index fb11d51..bfbba68 100644 --- a/src/types/request/index.ts +++ b/src/types/request/index.ts @@ -7,3 +7,4 @@ export * from './usdc-perp'; export * from './usdc-options'; export * from './usdc-shared'; export * from './unified-margin'; +export * from './contract'; diff --git a/src/types/shared.ts b/src/types/shared.ts index 3d70cff..4a1f2ec 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -1,3 +1,4 @@ +import { ContractClient } from '../contract-client'; import { InverseClient } from '../inverse-client'; import { LinearClient } from '../linear-client'; import { SpotClient } from '../spot-client'; @@ -13,7 +14,8 @@ export type RESTClient = | SpotClientV3 | USDCOptionClient | USDCPerpetualClient - | UnifiedMarginClient; + | UnifiedMarginClient + | ContractClient; export type numberInString = string; diff --git a/src/types/websockets.ts b/src/types/websockets.ts index 68b100f..6354c39 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -9,7 +9,9 @@ export type APIMarket = | 'usdcOption' | 'usdcPerp' | 'unifiedPerp' - | 'unifiedOption'; + | 'unifiedOption' + | 'contractUSDT' + | 'contractInverse'; // Same as inverse futures export type WsPublicInverseTopic = diff --git a/src/usdc-perpetual-client.ts b/src/usdc-perpetual-client.ts index 807d498..b446755 100644 --- a/src/usdc-perpetual-client.ts +++ b/src/usdc-perpetual-client.ts @@ -25,6 +25,7 @@ import BaseRestClient from './util/BaseRestClient'; */ export class USDCPerpetualClient extends BaseRestClient { getClientType() { + // Follows the same authentication mechanism as other v3 APIs (e.g. USDC) return REST_CLIENT_TYPE_ENUM.v3; } diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index d8dc899..520e7c0 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -121,6 +121,26 @@ export const WS_BASE_URL_MAP: Record< 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 = { @@ -139,6 +159,10 @@ export const WS_KEY_MAP = { unifiedOptionPublic: 'unifiedOptionPublic', unifiedPerpUSDTPublic: 'unifiedPerpUSDTPublic', unifiedPerpUSDCPublic: 'unifiedPerpUSDCPublic', + contractUSDTPublic: 'contractUSDTPublic', + contractUSDTPrivate: 'contractUSDTPrivate', + contractInversePublic: 'contractInversePublic', + contractInversePrivate: 'contractInversePrivate', } as const; 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.usdcPerpPrivate, WS_KEY_MAP.unifiedPrivate, + WS_KEY_MAP.contractUSDTPrivate, + WS_KEY_MAP.contractInversePrivate, ]; export const PUBLIC_WS_KEYS = [ @@ -157,6 +183,8 @@ export const PUBLIC_WS_KEYS = [ WS_KEY_MAP.unifiedOptionPublic, WS_KEY_MAP.unifiedPerpUSDTPublic, WS_KEY_MAP.unifiedPerpUSDCPublic, + WS_KEY_MAP.contractUSDTPublic, + WS_KEY_MAP.contractInversePublic, ] as string[]; /** 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.wallet.unifiedAccount', 'user.greeks.unifiedAccount', + // contract v3 + 'user.position.contractAccount', + 'user.execution.contractAccount', + 'user.order.contractAccount', + 'user.wallet.contractAccount', ]; export function getWsKeyForTopic( @@ -251,6 +284,16 @@ export function getWsKeyForTopic( `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: { throw neverGuard(market, `getWsKeyForTopic(): Unhandled market`); } @@ -267,7 +310,9 @@ export function getMaxTopicsPerSubscribeEvent( case 'usdcPerp': case 'unifiedOption': case 'unifiedPerp': - case 'spot': { + case 'spot': + case 'contractInverse': + case 'contractUSDT': { return null; } case 'spotv3': { diff --git a/src/websocket-client.ts b/src/websocket-client.ts index f7063ce..2d63d46 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -5,6 +5,10 @@ import { InverseClient } from './inverse-client'; import { LinearClient } from './linear-client'; import { SpotClientV3 } from './spot-client-v3'; 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 WsStore from './util/WsStore'; @@ -32,9 +36,6 @@ import { neverGuard, getMaxTopicsPerSubscribeEvent, } 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' }; @@ -106,6 +107,9 @@ export class WebsocketClient extends EventEmitter { }; 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; } + case 'contractInverse': + case 'contractUSDT': { + this.restClient = new ContractClient( + this.options.restOptions, + this.options.requestOptions + ); + break; + } default: { throw neverGuard( this.options.market, @@ -264,6 +276,7 @@ export class WebsocketClient extends EventEmitter { public closeAll(force?: boolean) { const keys = this.wsStore.getKeys(); + this.logger.info(`Closing all ws connections: ${keys}`); keys.forEach((key) => { this.close(key, force); }); @@ -285,7 +298,9 @@ export class WebsocketClient extends EventEmitter { case 'usdcOption': case 'usdcPerp': case 'unifiedPerp': - case 'unifiedOption': { + case 'unifiedOption': + case 'contractUSDT': + case 'contractInverse': { return [...this.connectPublic(), this.connectPrivate()]; } default: { @@ -323,6 +338,10 @@ export class WebsocketClient extends EventEmitter { 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: { throw neverGuard( this.options.market, @@ -356,6 +375,10 @@ export class WebsocketClient extends EventEmitter { case 'unifiedOption': { 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: { throw neverGuard( this.options.market, @@ -399,7 +422,7 @@ export class WebsocketClient extends EventEmitter { return this.wsStore.setWs(wsKey, ws); } catch (err) { 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; default: - this.logger.error( - `${context} due to unexpected response error: "${ - error?.msg || error?.message || error - }"`, - { ...loggerCategory, wsKey, error } - ); + if ( + this.wsStore.getConnectionState(wsKey) !== + WsConnectionStateEnum.CLOSING + ) { + 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; } this.emit('error', error); @@ -518,11 +551,16 @@ export class WebsocketClient extends EventEmitter { this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING); } + if (this.wsStore.get(wsKey)?.activeReconnectTimer) { + this.clearReconnectTimer(wsKey); + } + this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => { this.logger.info('Reconnecting to websocket', { ...loggerCategory, wsKey, }); + this.clearReconnectTimer(wsKey); this.connect(wsKey); }, connectionDelayMs); } @@ -537,23 +575,47 @@ export class WebsocketClient extends EventEmitter { this.logger.silly('Sending ping', { ...loggerCategory, wsKey }); this.tryWsSend(wsKey, JSON.stringify({ op: 'ping' })); - this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => { - this.logger.info('Pong timeout - closing socket to reconnect', { - ...loggerCategory, - wsKey, - }); - this.getWs(wsKey)?.terminate(); - delete this.wsStore.get(wsKey, true).activePongTimer; - }, this.options.pongTimeout); + this.wsStore.get(wsKey, true).activePongTimer = setTimeout( + () => this.executeReconnectableClose(wsKey, 'Pong timeout'), + 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) { this.clearPingTimer(wsKey); this.clearPongTimer(wsKey); - const wsState = this.wsStore.get(wsKey); - if (wsState?.activeReconnectTimer) { - clearTimeout(wsState.activeReconnectTimer); - } + this.clearReconnectTimer(wsKey); } // 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. */ @@ -682,7 +752,8 @@ export class WebsocketClient extends EventEmitter { const ws = new WebSocket(url, undefined, agent ? { agent } : undefined); ws.onopen = (event) => this.onWsOpen(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); 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) { this.logger.info('Websocket connection closed', { ...loggerCategory, @@ -794,7 +861,7 @@ export class WebsocketClient extends EventEmitter { if ( this.wsStore.getConnectionState(wsKey) !== WsConnectionStateEnum.CLOSING ) { - this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!); + this.reconnectWithDelay(wsKey, this.options.reconnectTimeout); this.emit('reconnect', { wsKey, event }); } else { this.setWsState(wsKey, WsConnectionStateEnum.INITIAL); @@ -864,6 +931,18 @@ export class WebsocketClient extends EventEmitter { case WS_KEY_MAP.unifiedPrivate: { 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: { this.logger.error('getWsUrl(): Unhandled wsKey: ', { ...loggerCategory, diff --git a/test/contract/private.read.test.ts b/test/contract/private.read.test.ts new file mode 100644 index 0000000..c954df5 --- /dev/null +++ b/test/contract/private.read.test.ts @@ -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() + ); + }); +}); diff --git a/test/contract/private.write.test.ts b/test/contract/private.write.test.ts new file mode 100644 index 0000000..2145a58 --- /dev/null +++ b/test/contract/private.write.test.ts @@ -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, + }); + }); +}); diff --git a/test/contract/public.read.test.ts b/test/contract/public.read.test.ts new file mode 100644 index 0000000..5c245b0 --- /dev/null +++ b/test/contract/public.read.test.ts @@ -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()); + }); +}); diff --git a/test/contract/ws.private.test.ts b/test/contract/ws.private.test.ts new file mode 100644 index 0000000..49937e6 --- /dev/null +++ b/test/contract/ws.private.test.ts @@ -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(); + // } + }); +}); diff --git a/test/contract/ws.public.inverse.test.ts b/test/contract/ws.public.inverse.test.ts new file mode 100644 index 0000000..fd7a5cf --- /dev/null +++ b/test/contract/ws.public.inverse.test.ts @@ -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); + } + }); +}); diff --git a/test/contract/ws.public.usdt.test.ts b/test/contract/ws.public.usdt.test.ts new file mode 100644 index 0000000..aacc40a --- /dev/null +++ b/test/contract/ws.public.usdt.test.ts @@ -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); + } + }); +}); diff --git a/test/inverse/private.write.test.ts b/test/inverse/private.write.test.ts index 6b106ef..e216b57 100644 --- a/test/inverse/private.write.test.ts +++ b/test/inverse/private.write.test.ts @@ -110,6 +110,7 @@ describe('Private Inverse REST API POST Endpoints', () => { symbol, p_r_price: '50000', p_r_qty: 1, + order_link_id: 'fakeorderid', }) ).toMatchObject({ ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, diff --git a/test/inverse/ws.private.test.ts b/test/inverse/ws.private.test.ts index 83b01c3..6c6bc5c 100644 --- a/test/inverse/ws.private.test.ts +++ b/test/inverse/ws.private.test.ts @@ -40,7 +40,7 @@ describe('Private Inverse Perps Websocket Client', () => { // console.error() expect(e?.message).toStrictEqual('Unexpected server response: 401'); } - badClient.closeAll(); + badClient.closeAll(true); }); }); @@ -62,7 +62,7 @@ describe('Private Inverse Perps Websocket Client', () => { afterAll(() => { // await promiseSleep(2000); - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a ws connection', async () => { diff --git a/test/inverse/ws.public.test.ts b/test/inverse/ws.public.test.ts index 8d02b6d..901e14f 100644 --- a/test/inverse/ws.public.test.ts +++ b/test/inverse/ws.public.test.ts @@ -24,7 +24,7 @@ describe('Public Inverse Perps Websocket Client', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a public ws connection', async () => { diff --git a/test/linear/private.write.test.ts b/test/linear/private.write.test.ts index 424420b..dd5c175 100644 --- a/test/linear/private.write.test.ts +++ b/test/linear/private.write.test.ts @@ -176,7 +176,9 @@ describe('Private Linear REST API POST Endpoints', () => { margin: 5, }) ).toMatchObject({ + // ret_msg: '', ret_code: API_ERROR_CODE.POSITION_SIZE_IS_ZERO, + // ret_code: API_ERROR_CODE.ISOLATED_NOT_MODIFIED_LINEAR, }); }); diff --git a/test/linear/ws.private.test.ts b/test/linear/ws.private.test.ts index 09290b1..60c16fd 100644 --- a/test/linear/ws.private.test.ts +++ b/test/linear/ws.private.test.ts @@ -38,7 +38,7 @@ describe('Private Linear Perps Websocket Client', () => { } catch (e) { expect(e?.message).toStrictEqual('Unexpected server response: 401'); } - badClient.closeAll(); + badClient.closeAll(true); }); }); @@ -65,7 +65,7 @@ describe('Private Linear Perps Websocket Client', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a ws connection', async () => { diff --git a/test/linear/ws.public.test.ts b/test/linear/ws.public.test.ts index a29f159..744edc1 100644 --- a/test/linear/ws.public.test.ts +++ b/test/linear/ws.public.test.ts @@ -22,7 +22,7 @@ describe('Public Linear Perps Websocket Client', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a public ws connection', async () => { diff --git a/test/spot/ws.private.v1.test.ts b/test/spot/ws.private.v1.test.ts index 7361a36..617e21b 100644 --- a/test/spot/ws.private.v1.test.ts +++ b/test/spot/ws.private.v1.test.ts @@ -9,6 +9,7 @@ import { getSilentLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, + fullLogger, } from '../ws.util'; describe('Private Spot V1 Websocket Client', () => { @@ -30,13 +31,14 @@ describe('Private Spot V1 Websocket Client', () => { beforeAll(() => { wsClient = new WebsocketClient( wsClientOptions, + // fullLogger getSilentLogger('expectSuccess') ); logAllEvents(wsClient); }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); // TODO: how to detect if auth failed for the v1 spot ws diff --git a/test/spot/ws.private.v3.test.ts b/test/spot/ws.private.v3.test.ts index 6fb8442..39a3b0b 100644 --- a/test/spot/ws.private.v3.test.ts +++ b/test/spot/ws.private.v3.test.ts @@ -50,7 +50,7 @@ describe('Private Spot V3 Websocket Client', () => { } catch (e) { // console.error() } - badClient.closeAll(); + badClient.closeAll(true); }); }); @@ -72,7 +72,7 @@ describe('Private Spot V3 Websocket Client', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a private ws connection', async () => { diff --git a/test/spot/ws.public.v1.test.ts b/test/spot/ws.public.v1.test.ts index 3b021b2..0e2219c 100644 --- a/test/spot/ws.public.v1.test.ts +++ b/test/spot/ws.public.v1.test.ts @@ -28,7 +28,7 @@ describe('Public Spot V1 Websocket Client', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a public ws connection', async () => { diff --git a/test/spot/ws.public.v3.test.ts b/test/spot/ws.public.v3.test.ts index 0f2d662..43377cc 100644 --- a/test/spot/ws.public.v3.test.ts +++ b/test/spot/ws.public.v3.test.ts @@ -28,7 +28,7 @@ describe('Public Spot V3 Websocket Client', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a public ws connection', async () => { diff --git a/test/unified-margin/ws.private.test.ts b/test/unified-margin/ws.private.test.ts index 72c16a1..9e738a5 100644 --- a/test/unified-margin/ws.private.test.ts +++ b/test/unified-margin/ws.private.test.ts @@ -12,10 +12,15 @@ import { } from '../ws.util'; 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; const wsClientOptions: WSClientConfigurableOptions = { market: 'unifiedPerp', + key: API_KEY, + secret: API_SECRET, }; beforeAll(() => { @@ -29,7 +34,7 @@ describe('Private Unified Margin Websocket Client', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a public ws connection', async () => { diff --git a/test/unified-margin/ws.public.option.test.ts b/test/unified-margin/ws.public.option.test.ts index cd05b9b..0870511 100644 --- a/test/unified-margin/ws.public.option.test.ts +++ b/test/unified-margin/ws.public.option.test.ts @@ -26,7 +26,7 @@ describe('Public Unified Margin Websocket Client (Options)', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a public ws connection', async () => { diff --git a/test/unified-margin/ws.public.perp.usdc.test.ts b/test/unified-margin/ws.public.perp.usdc.test.ts index 1b526a3..86247f7 100644 --- a/test/unified-margin/ws.public.perp.usdc.test.ts +++ b/test/unified-margin/ws.public.perp.usdc.test.ts @@ -28,10 +28,6 @@ describe('Public Unified Margin Websocket Client (Perps - USDC)', () => { wsClient.connectPublic(); }); - afterAll(() => { - wsClient.closeAll(); - }); - it('should open a public ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); try { @@ -42,6 +38,8 @@ describe('Public Unified Margin Websocket Client (Perps - USDC)', () => { } catch (e) { expect(e).toBeFalsy(); } + + wsClient.closeAll(true); }); // TODO: are there USDC topics? This doesn't seem to work diff --git a/test/unified-margin/ws.public.perp.usdt.test.ts b/test/unified-margin/ws.public.perp.usdt.test.ts index 800c961..6880524 100644 --- a/test/unified-margin/ws.public.perp.usdt.test.ts +++ b/test/unified-margin/ws.public.perp.usdt.test.ts @@ -29,7 +29,7 @@ describe('Public Unified Margin Websocket Client (Perps - USDT)', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a public ws connection', async () => { diff --git a/test/usdc/options/ws.private.test.ts b/test/usdc/options/ws.private.test.ts index ae93be4..b6f52d4 100644 --- a/test/usdc/options/ws.private.test.ts +++ b/test/usdc/options/ws.private.test.ts @@ -60,7 +60,7 @@ describe('Private USDC Option Websocket Client', () => { // badClient.subscribe(wsTopic); badClient.removeAllListeners(); - badClient.closeAll(); + badClient.closeAll(true); }); }); @@ -81,7 +81,7 @@ describe('Private USDC Option Websocket Client', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a private ws connection', async () => { diff --git a/test/usdc/options/ws.public.test.ts b/test/usdc/options/ws.public.test.ts index 90b4822..63d6ef9 100644 --- a/test/usdc/options/ws.public.test.ts +++ b/test/usdc/options/ws.public.test.ts @@ -26,7 +26,7 @@ describe('Public USDC Option Websocket Client', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a public ws connection', async () => { diff --git a/test/usdc/perpetual/ws.private.test.ts b/test/usdc/perpetual/ws.private.test.ts index 7508292..5c2e302 100644 --- a/test/usdc/perpetual/ws.private.test.ts +++ b/test/usdc/perpetual/ws.private.test.ts @@ -60,7 +60,7 @@ describe('Private USDC Perp Websocket Client', () => { // badClient.subscribe(wsTopic); badClient.removeAllListeners(); - badClient.closeAll(); + badClient.closeAll(true); }); }); @@ -82,7 +82,7 @@ describe('Private USDC Perp Websocket Client', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a private ws connection', async () => { diff --git a/test/usdc/perpetual/ws.public.test.ts b/test/usdc/perpetual/ws.public.test.ts index d5bcb87..c858f07 100644 --- a/test/usdc/perpetual/ws.public.test.ts +++ b/test/usdc/perpetual/ws.public.test.ts @@ -27,7 +27,7 @@ describe('Public USDC Perp Websocket Client', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a public ws connection', async () => {