From 30ceb61771b07d9b270c033c96e70bb35d875e72 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 11 Nov 2022 10:23:56 +0000 Subject: [PATCH 01/20] comment on usdc perp --- src/usdc-perpetual-client.ts | 1 + 1 file changed, 1 insertion(+) 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; } From e0799a623fb4305c850a5f94ccf25e0a4b2f9dec Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 11 Nov 2022 10:24:16 +0000 Subject: [PATCH 02/20] force close test websockets --- test/inverse/ws.private.test.ts | 4 ++-- test/inverse/ws.public.test.ts | 2 +- test/linear/ws.private.test.ts | 4 ++-- test/linear/ws.public.test.ts | 2 +- test/spot/ws.private.v1.test.ts | 2 +- test/spot/ws.private.v3.test.ts | 4 ++-- test/spot/ws.public.v1.test.ts | 2 +- test/spot/ws.public.v3.test.ts | 2 +- test/unified-margin/ws.private.test.ts | 2 +- test/unified-margin/ws.public.option.test.ts | 2 +- test/unified-margin/ws.public.perp.usdc.test.ts | 2 +- test/unified-margin/ws.public.perp.usdt.test.ts | 2 +- test/usdc/options/ws.private.test.ts | 4 ++-- test/usdc/options/ws.public.test.ts | 2 +- test/usdc/perpetual/ws.private.test.ts | 4 ++-- test/usdc/perpetual/ws.public.test.ts | 2 +- 16 files changed, 21 insertions(+), 21 deletions(-) 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/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..5a98082 100644 --- a/test/spot/ws.private.v1.test.ts +++ b/test/spot/ws.private.v1.test.ts @@ -36,7 +36,7 @@ describe('Private Spot V1 Websocket Client', () => { }); 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..313321d 100644 --- a/test/unified-margin/ws.private.test.ts +++ b/test/unified-margin/ws.private.test.ts @@ -29,7 +29,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..cb1a31f 100644 --- a/test/unified-margin/ws.public.perp.usdc.test.ts +++ b/test/unified-margin/ws.public.perp.usdc.test.ts @@ -29,7 +29,7 @@ describe('Public Unified Margin Websocket Client (Perps - USDC)', () => { }); afterAll(() => { - wsClient.closeAll(); + wsClient.closeAll(true); }); it('should open a public ws connection', async () => { 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 () => { From e068b35957a4c4ffe4c402f3826220bdce1028e7 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 11 Nov 2022 10:30:55 +0000 Subject: [PATCH 03/20] add missing parameter to test call --- test/inverse/private.write.test.ts | 1 + 1 file changed, 1 insertion(+) 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, From e0f69792087663f076185915c0cafaab100b0724 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 11 Nov 2022 10:31:59 +0000 Subject: [PATCH 04/20] rename ci action --- .github/workflows/{integrationtest.yml => e2etest.yml} | 0 README.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{integrationtest.yml => e2etest.yml} (100%) 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..2a845a8 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). From d6ca73b08a4a27a2a9d71e8648c461f57a5ebe44 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 11:26:53 +0000 Subject: [PATCH 05/20] feat(#195): add contract client for REST endpoints --- README.md | 4 +- examples/rest-contract-private.ts | 24 ++ src/contract-client.ts | 377 ++++++++++++++++++++++++++++++ src/index.ts | 1 + src/types/request/contract.ts | 67 ++++++ src/types/request/index.ts | 1 + 6 files changed, 472 insertions(+), 2 deletions(-) create mode 100644 examples/rest-contract-private.ts create mode 100644 src/contract-client.ts create mode 100644 src/types/request/contract.ts diff --git a/README.md b/README.md index 2a845a8..4866680 100644 --- a/README.md +++ b/README.md @@ -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: 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/src/contract-client.ts b/src/contract-client.ts new file mode 100644 index 0000000..6878194 --- /dev/null +++ b/src/contract-client.ts @@ -0,0 +1,377 @@ +import { + APIResponseWithTime, + APIResponseV3, + InternalTransferRequest, + UM7DayTradingHistoryRequest, + UMBorrowHistoryRequest, + UMCandlesRequest, + UMCategory, + UMExchangeCoinsRequest, + UMFundingRateHistoryRequest, + UMInstrumentInfoRequest, + UMOpenInterestRequest, + UMOptionDeliveryPriceRequest, + UMOptionsSettlementHistoryRequest, + UMPerpSettlementHistoryRequest, + UMPublicTradesRequest, + UMSetTPSLRequest, + UMTransactionLogRequest, + ContractOrderRequest, + ContractHistoricOrdersRequest, + ContractCancelOrderRequest, + ContractModifyOrderRequest, + ContractActiveOrdersRequest, + ContractPositionsRequest, +} 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. */ + setAuthAddMargin(params: { + symbol: string; + side: string; + autoAddMargin: 1 | 0; + positionIdx?: 0 | 1 | 2; + }): Promise> { + return this.postPrivate( + '/contract/v3/private/position/set-auto-add-margin', + params + ); + } + + /** Switch cross margin mode/isolated margin mode */ + setMarginSwitch(params: { + symbol: string; + tradeMode: 0 | 1; + buyLeverage: string; + sellLeverage: string; + }): 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: { + symbol?: string; + coin?: string; + mode: 0 | 3; + }): 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: { + 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; + }): 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: { + symbol: string; + orderId?: string; + startTime?: number; + endTime?: number; + execType?: 'Trade' | 'AdlTrade' | 'Funding' | 'BustTrade'; + limit?: number; + cursor?: string; + }): 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: { + symbol: string; + startTime?: number; + endTime?: number; + limit?: number; + cursor?: string; + }): 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?: { + startTime?: string; + endTime?: string; + coin?: string; + walletFundType?: string; + limit?: string; + cursor?: string; + }): 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..7b4ce52 --- /dev/null +++ b/src/types/request/contract.ts @@ -0,0 +1,67 @@ +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; +} 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'; From 38b798fd81fc6fa5c191a5f9beada2be5aebad62 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 11:35:38 +0000 Subject: [PATCH 06/20] exported contract client request types --- src/contract-client.ts | 88 +++++++++++------------------------ src/types/request/contract.ts | 61 ++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 62 deletions(-) diff --git a/src/contract-client.ts b/src/contract-client.ts index 6878194..6844b1c 100644 --- a/src/contract-client.ts +++ b/src/contract-client.ts @@ -1,27 +1,26 @@ import { APIResponseWithTime, APIResponseV3, - InternalTransferRequest, - UM7DayTradingHistoryRequest, - UMBorrowHistoryRequest, UMCandlesRequest, UMCategory, - UMExchangeCoinsRequest, UMFundingRateHistoryRequest, UMInstrumentInfoRequest, UMOpenInterestRequest, UMOptionDeliveryPriceRequest, - UMOptionsSettlementHistoryRequest, - UMPerpSettlementHistoryRequest, UMPublicTradesRequest, - UMSetTPSLRequest, - UMTransactionLogRequest, 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'; @@ -192,12 +191,9 @@ export class ContractClient extends BaseRestClient { } /** Set auto add margin, or Auto-Margin Replenishment. */ - setAuthAddMargin(params: { - symbol: string; - side: string; - autoAddMargin: 1 | 0; - positionIdx?: 0 | 1 | 2; - }): Promise> { + setAutoAddMargin( + params: ContractSetAutoAddMarginRequest + ): Promise> { return this.postPrivate( '/contract/v3/private/position/set-auto-add-margin', params @@ -205,12 +201,9 @@ export class ContractClient extends BaseRestClient { } /** Switch cross margin mode/isolated margin mode */ - setMarginSwitch(params: { - symbol: string; - tradeMode: 0 | 1; - buyLeverage: string; - sellLeverage: string; - }): Promise> { + setMarginSwitch( + params: ContractSetMarginSwitchRequest + ): Promise> { return this.postPrivate( '/contract/v3/private/position/switch-isolated', params @@ -218,11 +211,9 @@ export class ContractClient extends BaseRestClient { } /** Supports switching between One-Way Mode and Hedge Mode at the coin level. */ - setPositionMode(params: { - symbol?: string; - coin?: string; - mode: 0 | 3; - }): Promise> { + setPositionMode( + params: ContractSetPositionModeRequest + ): Promise> { return this.postPrivate( '/contract/v3/private/position/switch-mode', params @@ -259,19 +250,7 @@ export class ContractClient extends BaseRestClient { * 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: { - 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; - }): Promise> { + setTPSL(params: ContractSetTPSLRequest): Promise> { return this.postPrivate( '/contract/v3/private/position/trading-stop', params @@ -296,15 +275,9 @@ export class ContractClient extends BaseRestClient { * 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: { - symbol: string; - orderId?: string; - startTime?: number; - endTime?: number; - execType?: 'Trade' | 'AdlTrade' | 'Funding' | 'BustTrade'; - limit?: number; - cursor?: string; - }): Promise> { + getUserExecutionHistory( + params: ContractUserExecutionHistoryRequest + ): Promise> { return this.getPrivate('/contract/v3/private/execution/list', params); } @@ -312,13 +285,9 @@ export class ContractClient extends BaseRestClient { * Get user's closed profit and loss records. * The results are ordered in descending order (the first item is the latest). */ - getClosedProfitAndLoss(params: { - symbol: string; - startTime?: number; - endTime?: number; - limit?: number; - cursor?: string; - }): Promise> { + getClosedProfitAndLoss( + params: ContractClosedPNLRequest + ): Promise> { return this.getPrivate('/contract/v3/private/position/closed-pnl', params); } @@ -351,14 +320,9 @@ export class ContractClient extends BaseRestClient { * This endpoint returns incomplete information for transfers involving the derivatives wallet. * Use the account asset API for creating and querying internal transfers. */ - getWalletFundRecords(params?: { - startTime?: string; - endTime?: string; - coin?: string; - walletFundType?: string; - limit?: string; - cursor?: string; - }): Promise> { + getWalletFundRecords( + params?: ContractWalletFundRecordRequest + ): Promise> { return this.getPrivate( '/contract/v3/private/account/wallet/fund-records', params diff --git a/src/types/request/contract.ts b/src/types/request/contract.ts index 7b4ce52..88828c5 100644 --- a/src/types/request/contract.ts +++ b/src/types/request/contract.ts @@ -65,3 +65,64 @@ export interface ContractPositionsRequest { settleCoin?: string; dataFilter?: string; } + +export interface ContractSetAutoAddMarginRequest { + symbol: string; + side: string; + 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; +} From 5d07151e2dfcb8466f3fb08e64498c2807be4055 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 11:55:56 +0000 Subject: [PATCH 07/20] add public tests for contract client --- test/contract/public.read.test.ts | 103 ++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 test/contract/public.read.test.ts 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()); + }); +}); From 26ef7fd834ad396c0218ce5b7c200540c37628fe Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 12:01:22 +0000 Subject: [PATCH 08/20] add private read e2e contract tests --- test/contract/private.read.test.ts | 71 ++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 test/contract/private.read.test.ts 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() + ); + }); +}); From 7f600271780c1acd6deb464d421dbfe5b7d707ac Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 12:26:09 +0000 Subject: [PATCH 09/20] add e2e contract write tests --- src/constants/enum.ts | 6 ++ src/types/request/contract.ts | 2 +- test/contract/private.write.test.ts | 135 ++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 test/contract/private.write.test.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/types/request/contract.ts b/src/types/request/contract.ts index 88828c5..125df40 100644 --- a/src/types/request/contract.ts +++ b/src/types/request/contract.ts @@ -68,7 +68,7 @@ export interface ContractPositionsRequest { export interface ContractSetAutoAddMarginRequest { symbol: string; - side: string; + side: 'Buy' | 'Sell'; autoAddMargin: 1 | 0; positionIdx?: 0 | 1 | 2; } diff --git a/test/contract/private.write.test.ts b/test/contract/private.write.test.ts new file mode 100644 index 0000000..9e42db3 --- /dev/null +++ b/test/contract/private.write.test.ts @@ -0,0 +1,135 @@ +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', + }) + ).toMatchObject({ + 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: 0, + side: 'Buy', + symbol, + }) + ).toMatchObject({ + retMsg: expect.stringMatching(/not modified/gim), + retCode: API_ERROR_CODE.PARAMS_MISSING_OR_WRONG, + }); + }); + + it('setMarginSwitch()', async () => { + expect( + await api.setMarginSwitch({ + symbol, + tradeMode: 0, + buyLeverage: '5', + sellLeverage: '5', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.CONTRACT_MARGIN_MODE_NOT_MODIFIED, + }); + }); + + it('setPositionMode()', async () => { + expect( + await api.setPositionMode({ + symbol, + mode: 0, + }) + ).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: 0, 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, 0)).toMatchObject({ + retCode: API_ERROR_CODE.CONTRACT_RISK_LIMIT_INFO_NOT_EXISTS, + }); + }); +}); From 7c6d02ea8b86ed898a074683b269384f8300d525 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 12:27:39 +0000 Subject: [PATCH 10/20] fix(#192): add more resilience around ws reconnection workflow --- src/websocket-client.ts | 73 ++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/src/websocket-client.ts b/src/websocket-client.ts index f7063ce..0a2bef7 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -399,7 +399,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); } } @@ -425,6 +425,7 @@ export class WebsocketClient extends EventEmitter { }"`, { ...loggerCategory, wsKey, error } ); + this.executeReconnectableClose(wsKey, 'unhandled onWsError'); break; } this.emit('error', error); @@ -518,11 +519,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 +543,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 +604,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 +720,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 +820,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 +829,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); From 5df19e83bc2f00927571243f7f056eec1e65df9c Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 12:40:43 +0000 Subject: [PATCH 11/20] add support for contract v3 websockets --- src/types/shared.ts | 4 +++- src/types/websockets.ts | 4 +++- src/util/websocket-util.ts | 42 +++++++++++++++++++++++++++++++++++++- src/websocket-client.ts | 33 +++++++++++++++++++++++++++++- 4 files changed, 79 insertions(+), 4 deletions(-) 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/util/websocket-util.ts b/src/util/websocket-util.ts index d8dc899..fa309f5 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) */ @@ -251,6 +279,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 +305,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 0a2bef7..6292df1 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -35,6 +35,7 @@ import { import { USDCOptionClient } from './usdc-option-client'; import { USDCPerpetualClient } from './usdc-perpetual-client'; import { UnifiedMarginClient } from './unified-margin-client'; +import { ContractClient } from './contract-client'; const loggerCategory = { category: 'bybit-ws' }; @@ -232,6 +233,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, @@ -285,7 +294,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 +334,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 +371,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, @@ -899,6 +918,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, From a51bd9f6e4344cf3689ed914a794f5966c364597 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 12:50:54 +0000 Subject: [PATCH 12/20] add public usdt contract ws test --- test/contract/ws.public.usdt.test.ts | 74 ++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 test/contract/ws.public.usdt.test.ts 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); + } + }); +}); From 6a30f712399a571f9582215b9202fef011ff1ea2 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 12:55:10 +0000 Subject: [PATCH 13/20] add contract public inverse ws test --- test/contract/ws.public.inverse.test.ts | 74 +++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 test/contract/ws.public.inverse.test.ts 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); + } + }); +}); From 750db0232ca9034b98c8902322c82863449aba2b Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 13:28:48 +0000 Subject: [PATCH 14/20] fix(#192): potential crash if a ws network exception happens and there's no error listener in the client --- src/websocket-client.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 6292df1..24eb687 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,10 +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'; -import { ContractClient } from './contract-client'; const loggerCategory = { category: 'bybit-ws' }; @@ -107,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', () => {}); } /** From 9d464be3b236de595ea4712745b209f783cce472 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 13:29:12 +0000 Subject: [PATCH 15/20] add example and test for private contract v3 websocket topics --- examples/ws-private.ts | 18 ++++++- examples/ws-public.ts | 3 ++ src/util/websocket-util.ts | 5 ++ test/contract/ws.private.test.ts | 74 ++++++++++++++++++++++++++ test/unified-margin/ws.private.test.ts | 5 ++ 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 test/contract/ws.private.test.ts 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/src/util/websocket-util.ts b/src/util/websocket-util.ts index fa309f5..520e7c0 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -221,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( diff --git a/test/contract/ws.private.test.ts b/test/contract/ws.private.test.ts new file mode 100644 index 0000000..162c803 --- /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(async () => { + 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/unified-margin/ws.private.test.ts b/test/unified-margin/ws.private.test.ts index 313321d..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(() => { From c29d9a6634628cc05c109b9e74124a47c0342f34 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 13:31:35 +0000 Subject: [PATCH 16/20] add contract v3 markets to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4866680..2701469 100644 --- a/README.md +++ b/README.md @@ -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'); From 2b0a46e9e3665c6fb73ece6af12a809616a1392d Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 13:36:05 +0000 Subject: [PATCH 17/20] v3.2.0: add contract v3 REST & WebSocket clients --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From ba7099d11c7d666269229538a68c3b8c5f87ae1e Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 17:42:01 +0000 Subject: [PATCH 18/20] fix tests --- test/contract/private.write.test.ts | 10 +++++++--- test/contract/ws.private.test.ts | 2 +- test/linear/private.write.test.ts | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/test/contract/private.write.test.ts b/test/contract/private.write.test.ts index 9e42db3..dd3dee1 100644 --- a/test/contract/private.write.test.ts +++ b/test/contract/private.write.test.ts @@ -33,8 +33,10 @@ describe('Private Contract REST API POST Endpoints', () => { price: '20000', orderLinkId: Date.now().toString(), timeInForce: 'GoodTillCancel', + positionIdx: '2', }) ).toMatchObject({ + // retMsg: '', retCode: API_ERROR_CODE.CONTRACT_INSUFFICIENT_BALANCE, }); }); @@ -74,6 +76,7 @@ describe('Private Contract REST API POST Endpoints', () => { autoAddMargin: 0, side: 'Buy', symbol, + positionIdx: 1, }) ).toMatchObject({ retMsg: expect.stringMatching(/not modified/gim), @@ -98,7 +101,7 @@ describe('Private Contract REST API POST Endpoints', () => { expect( await api.setPositionMode({ symbol, - mode: 0, + mode: 3, }) ).toMatchObject({ retCode: API_ERROR_CODE.CONTRACT_POSITION_MODE_NOT_MODIFIED, @@ -120,7 +123,7 @@ describe('Private Contract REST API POST Endpoints', () => { it('setTPSL()', async () => { expect( - await api.setTPSL({ symbol, positionIdx: 0, stopLoss: '100' }) + await api.setTPSL({ symbol, positionIdx: 1, stopLoss: '100' }) ).toMatchObject({ retMsg: expect.stringMatching(/zero position/gim), retCode: API_ERROR_CODE.PARAMS_MISSING_OR_WRONG, @@ -128,7 +131,8 @@ describe('Private Contract REST API POST Endpoints', () => { }); it('setRiskLimit()', async () => { - expect(await api.setRiskLimit(symbol, 43, 0)).toMatchObject({ + expect(await api.setRiskLimit(symbol, 43, 2)).toMatchObject({ + // retMsg: '', retCode: API_ERROR_CODE.CONTRACT_RISK_LIMIT_INFO_NOT_EXISTS, }); }); diff --git a/test/contract/ws.private.test.ts b/test/contract/ws.private.test.ts index 162c803..49937e6 100644 --- a/test/contract/ws.private.test.ts +++ b/test/contract/ws.private.test.ts @@ -23,7 +23,7 @@ describe('Private Contract Websocket Client', () => { secret: API_SECRET, }; - beforeAll(async () => { + beforeAll(() => { wsClient = new WebsocketClient( wsClientOptions, getSilentLogger('expectSuccessNoAuth') diff --git a/test/linear/private.write.test.ts b/test/linear/private.write.test.ts index 424420b..fb82088 100644 --- a/test/linear/private.write.test.ts +++ b/test/linear/private.write.test.ts @@ -176,7 +176,7 @@ describe('Private Linear REST API POST Endpoints', () => { margin: 5, }) ).toMatchObject({ - ret_code: API_ERROR_CODE.POSITION_SIZE_IS_ZERO, + ret_code: API_ERROR_CODE.ISOLATED_NOT_MODIFIED_LINEAR, }); }); From d3b2c65c2260bdceabba3814059f2fd62c0cc000 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 17:56:52 +0000 Subject: [PATCH 19/20] fix forceful close workflow --- src/websocket-client.ts | 24 +++++++++++++------ test/spot/ws.private.v1.test.ts | 2 ++ .../ws.public.perp.usdc.test.ts | 6 ++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 24eb687..2d63d46 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -276,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); }); @@ -441,13 +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 } - ); - this.executeReconnectableClose(wsKey, 'unhandled onWsError'); + 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); diff --git a/test/spot/ws.private.v1.test.ts b/test/spot/ws.private.v1.test.ts index 5a98082..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,6 +31,7 @@ describe('Private Spot V1 Websocket Client', () => { beforeAll(() => { wsClient = new WebsocketClient( wsClientOptions, + // fullLogger getSilentLogger('expectSuccess') ); logAllEvents(wsClient); diff --git a/test/unified-margin/ws.public.perp.usdc.test.ts b/test/unified-margin/ws.public.perp.usdc.test.ts index cb1a31f..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(true); - }); - 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 From a5fafbf46846138649cdbcaca52a27b2394f7c63 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 12 Nov 2022 18:05:50 +0000 Subject: [PATCH 20/20] fix conflicting tests --- test/contract/private.write.test.ts | 4 ++-- test/linear/private.write.test.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/contract/private.write.test.ts b/test/contract/private.write.test.ts index dd3dee1..2145a58 100644 --- a/test/contract/private.write.test.ts +++ b/test/contract/private.write.test.ts @@ -73,7 +73,7 @@ describe('Private Contract REST API POST Endpoints', () => { it('setAutoAddMargin()', async () => { expect( await api.setAutoAddMargin({ - autoAddMargin: 0, + autoAddMargin: 1, side: 'Buy', symbol, positionIdx: 1, @@ -88,7 +88,7 @@ describe('Private Contract REST API POST Endpoints', () => { expect( await api.setMarginSwitch({ symbol, - tradeMode: 0, + tradeMode: 1, buyLeverage: '5', sellLeverage: '5', }) diff --git a/test/linear/private.write.test.ts b/test/linear/private.write.test.ts index fb82088..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_code: API_ERROR_CODE.ISOLATED_NOT_MODIFIED_LINEAR, + // ret_msg: '', + ret_code: API_ERROR_CODE.POSITION_SIZE_IS_ZERO, + // ret_code: API_ERROR_CODE.ISOLATED_NOT_MODIFIED_LINEAR, }); });