From 55fbf71e05b870bd6137822f1f1feb9003f7f873 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 22 May 2025 11:42:33 +0100 Subject: [PATCH] feat(v4.1.8): introduce dedicated WebsocketAPIClient --- README.md | 127 +++++++++++------- examples/ws-api-client.ts | 52 ++++--- ...{ws-api-events.ts => ws-api-raw-events.ts} | 2 +- ...api-promises.ts => ws-api-raw-promises.ts} | 0 package-lock.json | 4 +- package.json | 2 +- src/websocket-client.ts | 10 +- 7 files changed, 117 insertions(+), 80 deletions(-) rename examples/{ws-api-events.ts => ws-api-raw-events.ts} (97%) rename examples/{ws-api-promises.ts => ws-api-raw-promises.ts} (100%) diff --git a/README.md b/README.md index 453244c..e9e74ba 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Node.js & JavaScript SDK for Bybit REST API & WebSockets +# Node.js & JavaScript SDK for Bybit REST API, WebSocket API & WebSocket Events [![Build & Test](https://github.com/tiagosiebler/bybit-api/actions/workflows/e2etest.yml/badge.svg?branch=master)](https://github.com/tiagosiebler/bybit-api/actions/workflows/e2etest.yml) [![npm version](https://img.shields.io/npm/v/bybit-api)][1] @@ -19,7 +19,7 @@ [1]: https://www.npmjs.com/package/bybit-api -Professional Node.js, JavaScript & TypeScript SDK for the Bybit REST APIs and WebSockets: +Professional Node.js, JavaScript & TypeScript SDK for the Bybit REST APIs, WebSocket APIs & WebSocket Events: - Complete integration with all Bybit REST APIs & WebSockets, including the WebSocket API. - Actively maintained with a modern, promise-driven interface. @@ -29,22 +29,22 @@ Professional Node.js, JavaScript & TypeScript SDK for the Bybit REST APIs and We - Proxy support via axios integration. - Robust WebSocket consumer integration with configurable heartbeats & automatic reconnect then resubscribe workflows. - Event driven messaging - - Smart websocket persistence + - Smart WebSocket persistence - Automatically handle silent websocket disconnections through timed heartbeats, including the scheduled 24hr disconnect. - Automatically handle authentication. - Emit `reconnected` event when dropped connection is restored. - WebSocket API integration, with two design patterns to choose from: - - Asynchronous promise-driven responses: + 1. Asynchronous **promise**-driven responses: - Make requests like a REST API, using the WebSocket API. No need to subscribe to asynchronous events. - - Send commands with the await sendWSAPIRequest(...) method. - - Await responses to commands directly in the fully typed sendWSAPIRequest() call. - - The method directly returns a promise. Use a try/catch block for convenient error handling without the complexity of asynchronous WebSockets. - - See example for more details: [examples/ws-api-promises.ts](./examples/ws-api-promises.ts) - - Asynchronous event-driven responses: + - Import the `WebsocketAPIClient` and use it like the REST API client. Call functions and await responses. + - See example for more details: [examples/ws-api-client.ts](./examples/ws-api-client.ts). + - Prefer something more raw? Use the `sendWSAPIRequest(...)` method and await responses + - See example for more details: [examples/ws-api-raw-promises.ts](./examples/ws-api-raw-promises.ts) + 2. Asynchronous **event**-driven responses: - Subscribe to `response` and `error` events from WebsocketClient's event emitter. - - Send commands with the sendWSAPIRequest(...) method. - - Responses to commands will arrive via the `response` and `error` events. - - See example for more details: [examples/ws-api-events.ts](./examples/ws-api-events.ts) + - Send commands with the `sendWSAPIRequest(...)` method. + - Responses to commands will arrive via the `response` and `error` events emitted by the client. + - See example for more details: [examples/ws-api-raw-events.ts](./examples/ws-api-raw-events.ts) - Active community support & collaboration in telegram: [Node.js Algo Traders](https://t.me/nodetraders). # Table of Contents @@ -153,6 +153,7 @@ Here are the available REST clients and the corresponding API groups described i | [ **V5 API** ] | The new unified V5 APIs (successor to previously fragmented APIs for all API groups). | | [RestClientV5](src/rest-client-v5.ts) | Unified V5 all-in-one REST client for all [V5 REST APIs](https://bybit-exchange.github.io/docs/v5/intro) | | [WebsocketClient](src/websocket-client.ts) | All WebSocket features (Public & Private consumers for all API categories & the WebSocket API) | +| [WebsocketAPIClient](src/websocket-api-client.ts) | Use the WebSocket API like a REST API. Call functions and await responses, powered by WebSockets. | ## REST API Usage @@ -398,36 +399,41 @@ ws.on('reconnected', (data) => { ## Websocket API - Sending orders via WebSockets -Bybit supports sending, amending and cancelling orders over a WebSocket connection. The [WebsocketClient](./src/WebsocketClient.ts) fully supports Bybit's WebSocket API via the `sendWSAPIRequest(...)` method. +Bybit supports sending, amending and cancelling orders over a WebSocket connection. The [WebsocketClient](./src/WebsocketClient.ts) fully supports Bybit's WebSocket API via the `sendWSAPIRequest(...)` method. There is also a dedicated [WebsocketAPIClient](./src/websocket-api-client.ts), built over the WSClient's sendWSAPIRequest mechanism for a simpler experience. Links for reference: - [Bybit WebSocket API Documentation](https://bybit-exchange.github.io/docs/v5/websocket/trade/guideline) -- [WebSocket API Example Node.js/TypeScript/JavaScript](./examples/ws-api-promises.ts). +- [WebsocketAPIClient example, use the Websocket API like a REST API](./examples/ws-api-client.ts) +- [Raw Asynchronous Websocket API Node.js/TypeScript/JavaScript example](./examples/ws-api-raw-promises.ts) Note: as of January 2025, the demo trading environment does not support the WebSocket API. There are two ways to use the WS API, depending on individual preference: -- event-driven: - - send requests via `client.sendWSAPIRequest(wsKey, operation, params)`, fire and forget - - handle async replies via event handlers on `client.on('exception', cb)` and `client.on('response', cb)` - - See example for more details: [examples/ws-api-events.ts](./examples/ws-api-events.ts) -- promise-driven: - - send requests via `const result = await client.sendWSAPIRequest(wsKey, operation, params)`, which returns a promise - - await each call - - use try/catch blocks to handle promise rejections - - See example for more details: [examples/ws-api-promises.ts](./examples/ws-api-promises.ts) +1. event-driven: + - send requests via `client.sendWSAPIRequest(wsKey, operation, params)`, fire and forget + - handle async replies via event handlers on `client.on('exception', cb)` and `client.on('response', cb)` + - See example for more details: [examples/ws-api-raw-events.ts](./examples/ws-api-raw-events.ts) +2. promise-driven: + - import the `WebsocketAPIClient` and use it much like a REST API. + - make an instance & call the Websocket API with a function. + - await responses, much like a REST API. + - use try/catch blocks to handle promise rejections + - See example for more details: [examples/ws-api-client.ts](./examples/ws-api-client.ts) -The below example demonstrates the promise-driven approach, which behaves similar to a REST API. The WebSocket API even accepts the same parameters as the corresponding REST API endpoints, so this approach should be compatible with existing REST implementations. Connectivity, authentication, and processing requests wrapped in promises - these are all handled automatically by the WebsocketClient without additional configuration. +The below example demonstrates the promise-driven approach, which behaves similar to a REST API. The WebSocket API even accepts the same parameters as the corresponding REST API endpoints, so this approach should be compatible with existing REST implementations. + +Connectivity, authentication and connecting requests & responses to promises - these are all handled automatically without additional configuration by the WebsocketClient. The WebsocketAPIClient is a wrapper built on top of this, providing dedicated methods for every available Websocket API command. Each method has fully typed requests & responses. Benefit from the capabilities of the WebSocket API without the complexity of managing asynchronous messaging over WebSockets. ```javascript const { WS_KEY_MAP, WebsocketClient } = require('bybit-api'); // or -// import { WS_KEY_MAP, WebsocketClient } from 'bybit-api'; +// import { WS_KEY_MAP, WebsocketAPIClient } from 'bybit-api'; -// Create an instance of the WebsocketClient. -// This will automatically handle connectivity and authentication for you. -const wsClient = new WebsocketClient( +// Create an instance of the WebsocketAPIClient. This is built on +// top of the WebsocketClient and will automatically handle WebSocket +// persistence and authentication for you. +const wsClient = new WebsocketAPIClient( { key: 'yourApiKeyHere', secret: 'yourApiSecretHere', @@ -440,6 +446,10 @@ const wsClient = new WebsocketClient( // Note: As of Jan 2025, demo trading only supports consuming events, it does // NOT support the WS API. // demoTrading: false, + + // If you want your own event handlers instead of the default ones with logs, + // disable this setting and see ws-api-client example for more details. + // attachEventListeners: false } ); @@ -452,32 +462,47 @@ async function main() { * This is not necessary and will happen automatically when * sending a command, if you aren't connected/authenticated yet. */ - // await wsClient.connectWSAPI(); + // await wsClient.getWSClient().connectWSAPI(); try { console.log('Step 1: Create an order'); - - // The type for `wsAPISubmitOrderResult` is automatically - // resolved to `WSAPIResponse` - const wsAPISubmitOrderResult = await wsClient.sendWSAPIRequest( - WS_KEY_MAP.v5PrivateTrade, - 'order.create', - { - symbol: 'BTCUSDT', - side: 'Buy', - orderType: 'Limit', - price: '50000', - qty: '1', - category: 'linear', - }, - ); - - console.log( - `Step 1: Order result (order ID: "${wsAPISubmitOrderResult.data.orderId}"): `, - wsAPISubmitOrderResult, - ); + const response = await wsClient.submitNewOrder({ + category: 'linear', + symbol: 'BTCUSDT', + orderType: 'Limit', + qty: '0.001', + side: 'Buy', + price: '50000', + }); + console.log('submitNewOrder response: ', response); } catch (e) { - console.error('Step 1: Order submit exception: ', e); + console.log('submitNewOrder error: ', e); + } + + try { + console.log('Step 2: Amend an order'); + const response = await wsClient.amendOrder({ + category: 'linear', + symbol: 'BTCUSDT', + orderId: 'b4b9e205-793c-4777-8112-0bf3c2d26b6e', + qty: '0.001', + price: '60000', + }); + console.log('amendOrder response: ', response); + } catch (e) { + console.log('amendOrder error: ', e); + } + + try { + console.log('Step 3: Cancel an order'); + const response = await wsClient.cancelOrder({ + category: 'linear', + symbol: 'BTCUSDT', + orderId: 'b4b9e205-793c-4777-8112-0bf3c2d26b6e', + }); + console.log('cancelOrder response: ', response); + } catch (e) { + console.log('cancelOrder error: ', e); } } @@ -486,8 +511,6 @@ main(); ``` -See the [examples/ws-api-promises.ts](./examples/ws-api-promises.ts) example for a more detailed explanation. - --- ### Balancing load across multiple connections diff --git a/examples/ws-api-client.ts b/examples/ws-api-client.ts index dec15e2..999581c 100644 --- a/examples/ws-api-client.ts +++ b/examples/ws-api-client.ts @@ -1,4 +1,4 @@ -import { DefaultLogger, WebsocketAPIClient } from '../src'; +import { DefaultLogger, WebsocketAPIClient, WebsocketClient } from '../src'; // or // import { DefaultLogger, WebsocketAPIClient } from 'bybit-api'; @@ -6,26 +6,25 @@ import { DefaultLogger, WebsocketAPIClient } from '../src'; const key = process.env.API_KEY_COM; const secret = process.env.API_SECRET_COM; -/* function attachEventHandlers( - wsClient: TWSClient, -): void { - wsClient.getWSClient().on('update', (data) => { - console.log('raw message received ', JSON.stringify(data)); - }); - - wsClient.getWSClient().on('open', (data) => { - console.log('ws connected', data.wsKey); - }); - wsClient.getWSClient().on('reconnect', ({ wsKey }) => { - console.log('ws automatically reconnecting.... ', wsKey); - }); - wsClient.getWSClient().on('reconnected', (data) => { - console.log('ws has reconnected ', data?.wsKey); - }); - wsClient.getWSClient().on('authenticated', (data) => { - console.log('ws has authenticated ', data?.wsKey); - }); -} */ +// function attachEventHandlers( +// wsClient: TWSClient, +// ): void { +// wsClient.on('update', (data) => { +// console.log('raw message received ', JSON.stringify(data)); +// }); +// wsClient.on('open', (data) => { +// console.log('ws connected', data.wsKey); +// }); +// wsClient.on('reconnect', ({ wsKey }) => { +// console.log('ws automatically reconnecting.... ', wsKey); +// }); +// wsClient.on('reconnected', (data) => { +// console.log('ws has reconnected ', data?.wsKey); +// }); +// wsClient.on('authenticated', (data) => { +// console.log('ws has authenticated ', data?.wsKey); +// }); +// } async function main() { // Optional @@ -40,7 +39,11 @@ async function main() { key: key, secret: secret, // testnet: true, // Whether to use the testnet environment: https://testnet.bybit.com/app/user/api-management - // demoTrading: false, // note: As of Jan 2025, demo trading does NOT support the WS API + + // Whether to use the livenet demo trading environment + // Note: As of Jan 2025, demo trading only supports consuming events, it does + // NOT support the WS API. + // demoTrading: false, // If you want your own event handlers instead of the default ones with logs, // disable this setting and see the `attachEventHandlers` example below: @@ -49,11 +52,16 @@ async function main() { logger, // Optional: inject a custom logger ); + // Optional, see above "attachEventListeners". Attach basic event handlers, so nothing is left unhandled + // attachEventHandlers(wsClient.getWSClient()); + // Optional, if you see RECV Window errors, you can use this to manage time issues. // ! However, make sure you sync your system clock first! // https://github.com/tiagosiebler/awesome-crypto-examples/wiki/Timestamp-for-this-request-is-outside-of-the-recvWindow // wsClient.setTimeOffsetMs(-5000); + await wsClient.getWSClient().connectWSAPI(); + try { const response = await wsClient.submitNewOrder({ category: 'linear', diff --git a/examples/ws-api-events.ts b/examples/ws-api-raw-events.ts similarity index 97% rename from examples/ws-api-events.ts rename to examples/ws-api-raw-events.ts index 99aa670..9e60a72 100644 --- a/examples/ws-api-events.ts +++ b/examples/ws-api-raw-events.ts @@ -55,7 +55,7 @@ async function main() { * - Handle any exceptions in a catch block. * * This is a more "raw" workflow in how WebSockets behave. For a more convenient & REST-like approach, using the - * promise-driven interface is recommended. See the `ws-api-promises.ts` and `ws-api-client.ts` examples for a + * promise-driven interface is recommended. See the `ws-api-raw-promises.ts` and `ws-api-client.ts` examples for a * demonstration you can compare. * * Note: even without using promises, you should still tie on a .catch handler to each sendWSAPIRequest call, to prevent diff --git a/examples/ws-api-promises.ts b/examples/ws-api-raw-promises.ts similarity index 100% rename from examples/ws-api-promises.ts rename to examples/ws-api-raw-promises.ts diff --git a/package-lock.json b/package-lock.json index a951b69..9f716a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bybit-api", - "version": "4.1.7", + "version": "4.1.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bybit-api", - "version": "4.1.7", + "version": "4.1.8", "license": "MIT", "dependencies": { "axios": "^1.7.9", diff --git a/package.json b/package.json index 0663b57..d35d2b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bybit-api", - "version": "4.1.7", + "version": "4.1.8", "description": "Complete & robust Node.js SDK for Bybit's REST APIs and WebSockets, with TypeScript & strong end to end tests.", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/websocket-client.ts b/src/websocket-client.ts index ec980ce..2497320 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -379,11 +379,13 @@ export class WebsocketClient extends BaseWebsocketClient< await this.assertIsAuthenticated(wsKey); this.logger.trace('sendWSAPIRequest()->assertIsAuthenticated() ok'); + const timestampMs = Date.now() + (this.getTimeOffsetMs() || 0); + const requestEvent: WSAPIRequest = { reqId: this.getNewRequestId(), header: { 'X-BAPI-RECV-WINDOW': `${this.options.recvWindow}`, - 'X-BAPI-TIMESTAMP': `${Date.now()}`, + 'X-BAPI-TIMESTAMP': `${timestampMs}`, Referer: APIID, }, op: operation, @@ -415,7 +417,11 @@ export class WebsocketClient extends BaseWebsocketClient< }) .catch((e) => { if (typeof e === 'string') { - this.logger.error('unexpcted string', { e }); + this.logger.error('Unexpected string thrown without Error object:', { + e, + wsKey, + signedEvent, + }); return e; } e.request = {