From c4cc09489f7adefe36c4a2e98d84558ac676f49e Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Mon, 19 May 2025 12:38:04 +0100 Subject: [PATCH 01/12] feat(): start websocket API client wrapper, chore(): misc typo fixes & cleaning --- src/index.ts | 1 + src/util/BaseWSClient.ts | 2 +- src/util/logger.ts | 2 + src/websocket-api-client.ts | 134 ++++++++++++++++++++++++++++++++++++ src/websocket-client.ts | 24 +++---- 5 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 src/websocket-api-client.ts diff --git a/src/index.ts b/src/index.ts index 2a85c6d..1210d85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from './rest-client-v5'; export * from './spot-client-v3'; export * from './websocket-client'; +export * from './websocket-api-client'; export * from './util/logger'; export * from './util'; export * from './types'; diff --git a/src/util/BaseWSClient.ts b/src/util/BaseWSClient.ts index ed01ed6..9035851 100644 --- a/src/util/BaseWSClient.ts +++ b/src/util/BaseWSClient.ts @@ -124,7 +124,7 @@ export abstract class BaseWebsocketClient< */ private wsStore: WsStore>; - protected logger: typeof DefaultLogger; + public logger: typeof DefaultLogger; protected options: WebsocketClientOptions; diff --git a/src/util/logger.ts b/src/util/logger.ts index 0c5613e..bf6067d 100644 --- a/src/util/logger.ts +++ b/src/util/logger.ts @@ -3,6 +3,8 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type LogParams = null | any; +export type DefaultLogger = typeof DefaultLogger; + export const DefaultLogger = { /** Ping/pong events and other raw messages that might be noisy. Enable this while troubleshooting. */ trace: (..._params: LogParams): void => { diff --git a/src/websocket-api-client.ts b/src/websocket-api-client.ts new file mode 100644 index 0000000..4e97ab0 --- /dev/null +++ b/src/websocket-api-client.ts @@ -0,0 +1,134 @@ +import { OrderParamsV5, OrderResultV5 } from './types'; +import { WSAPIResponse } from './types/websockets/ws-api'; +import { WSClientConfigurableOptions } from './types/websockets/ws-general'; +import { DefaultLogger } from './util'; +import { WS_KEY_MAP } from './util/websockets/websocket-util'; +import { WebsocketClient } from './websocket-client'; + +/** + * Configurable options specific to only the REST-like WebsocketAPIClient + */ +export interface WSAPIClientConfigurableOptions { + /** + * Default: true + * + * Attach default event listeners, which will console log any high level + * events (opened/reconnecting/reconnected/etc). + * + * If you disable this, you should set your own event listeners + * on the embedded WS Client `wsApiClient.getWSClient().on(....)`. + */ + attachEventListeners: boolean; +} + +/** + * This is a minimal Websocket API wrapper around the WebsocketClient. + * + * Some methods support passing in a custom "wsKey". This is a reference to which WS connection should + * be used to transmit that message. This is only useful if you wish to use an alternative wss + * domain that is supported by the SDK. + * + * Note: To use testnet, don't set the wsKey - use `testnet: true` in + * the constructor instead. + * + * Note: You can also directly use the sendWSAPIRequest() method to make WS API calls, but some + * may find the below methods slightly more intuitive. + * + * Refer to the WS API promises example for a more detailed example on using sendWSAPIRequest() directly: + * https://github.com/tiagosiebler/binance/blob/master/examples/WebSockets/ws-api-raw-promises.ts#L108 + */ +export class WebsocketAPIClient { + private wsClient: WebsocketClient; + + private logger: DefaultLogger; + + private options: WSClientConfigurableOptions & WSAPIClientConfigurableOptions; + + constructor( + options?: WSClientConfigurableOptions & + Partial, + logger?: DefaultLogger, + ) { + this.wsClient = new WebsocketClient(options, logger); + + this.options = { + attachEventListeners: true, + ...options, + }; + + this.logger = this.wsClient.logger; + + this.setupDefaultEventListeners(); + } + + public getWSClient(): WebsocketClient { + return this.wsClient; + } + + public setTimeOffsetMs(newOffset: number): void { + return this.getWSClient().setTimeOffsetMs(newOffset); + } + + /* + * Bybit WebSocket API Methods + * https://bybit-exchange.github.io/docs/v5/websocket/trade/guideline + */ + + /** + * Submit a new order + * + * @param params + * @returns + */ + submitNewOrder( + params: OrderParamsV5, + ): Promise> { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.v5PrivateTrade, + 'order.create', + params, + ); + } + + /** + * + * + * + * + * + * + * + * Private methods for handling some of the convenience/automation provided by the WS API Client + * + * + * + * + * + * + * + */ + + private setupDefaultEventListeners() { + if (this.options.attachEventListeners) { + /** + * General event handlers for monitoring the WebsocketClient + */ + this.wsClient + .on('open', (data) => { + console.log(new Date(), 'ws connected', data.wsKey); + }) + .on('reconnect', ({ wsKey }) => { + console.log(new Date(), 'ws automatically reconnecting.... ', wsKey); + }) + .on('reconnected', (data) => { + console.log(new Date(), 'ws has reconnected ', data?.wsKey); + }) + .on('authenticated', (data) => { + console.info(new Date(), 'ws has authenticated ', data?.wsKey); + }) + .on('exception', (data) => { + console.error(new Date(), 'ws exception: ', JSON.stringify(data)); + }); + } + } +} diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 4f1b485..25eb3ce 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -343,23 +343,23 @@ export class WebsocketClient extends BaseWebsocketClient< // do not trigger excess property checks // Without these overloads, TypeScript won't complain if you include an // unexpected property with your request (if it doesn't clash with an existing property) - sendWSAPIRequest( + sendWSAPIRequest( wsKey: typeof WS_KEY_MAP.v5PrivateTrade, - operation: TWSOpreation, - params: WsAPITopicRequestParamMap[TWSOpreation], - ): Promise; + operation: TWSOperation, + params: WsAPITopicRequestParamMap[TWSOperation], + ): Promise; - sendWSAPIRequest( + sendWSAPIRequest( wsKey: typeof WS_KEY_MAP.v5PrivateTrade, - operation: TWSOpreation, - params: WsAPITopicRequestParamMap[TWSOpreation], - ): Promise; + operation: TWSOperation, + params: WsAPITopicRequestParamMap[TWSOperation], + ): Promise; - sendWSAPIRequest( + sendWSAPIRequest( wsKey: typeof WS_KEY_MAP.v5PrivateTrade, - operation: TWSOpreation, - params: WsAPITopicRequestParamMap[TWSOpreation], - ): Promise; + operation: TWSOperation, + params: WsAPITopicRequestParamMap[TWSOperation], + ): Promise; async sendWSAPIRequest< TWSKey extends keyof WsAPIWsKeyTopicMap, From 910f80a55b8c60274bd8a136cb23c12cc3887c7b Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Mon, 19 May 2025 13:37:57 +0100 Subject: [PATCH 02/12] feat(): upgrade base WS client with improvements from binance SDK, upgrade ws client with deferred promise enrichment --- src/util/BaseWSClient.ts | 30 +++++++++++++++---- src/util/websockets/WsStore.ts | 41 ++++++++++++++++++++++---- src/util/websockets/WsStore.types.ts | 3 +- src/websocket-client.ts | 43 ++++++++++++++++++++++------ 4 files changed, 98 insertions(+), 19 deletions(-) diff --git a/src/util/BaseWSClient.ts b/src/util/BaseWSClient.ts index 9035851..6c73915 100644 --- a/src/util/BaseWSClient.ts +++ b/src/util/BaseWSClient.ts @@ -413,7 +413,7 @@ export abstract class BaseWebsocketClient< wsTopicRequests, }, ); - return; + return isConnectionInProgress; } // We're connected. Check if auth is needed and if already authenticated @@ -532,7 +532,11 @@ export abstract class BaseWebsocketClient< /** * Request connection to a specific websocket, instead of waiting for automatic connection. */ - public async connect(wsKey: TWSKey): Promise { + public async connect( + wsKey: TWSKey, + customUrl?: string | undefined, + throwOnError?: boolean, + ): Promise { try { if (this.wsStore.isWsOpen(wsKey)) { this.logger.error( @@ -549,7 +553,7 @@ export abstract class BaseWebsocketClient< 'Refused to connect to ws, connection attempt already active', { ...WS_LOGGER_CATEGORY, wsKey }, ); - return; + return this.wsStore.getConnectionInProgressPromise(wsKey)?.promise; } if ( @@ -563,7 +567,7 @@ export abstract class BaseWebsocketClient< this.wsStore.createConnectionInProgressPromise(wsKey, false); } - const url = await this.getWsUrl(wsKey); + const url = customUrl || (await this.getWsUrl(wsKey)); const ws = this.connectToWsUrl(url, wsKey); this.wsStore.setWs(wsKey, ws); @@ -572,6 +576,10 @@ export abstract class BaseWebsocketClient< } catch (err) { this.parseWsError('Connection failed', err, wsKey); this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!); + + if (throwOnError) { + throw err; + } } } @@ -590,6 +598,8 @@ export abstract class BaseWebsocketClient< this.parseWsError('Websocket onWsError', event, wsKey); ws.onclose = (event: any) => this.onWsClose(event, wsKey); + ws.wsKey = wsKey; + return ws; } @@ -668,12 +678,18 @@ export abstract class BaseWebsocketClient< this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING); } + this.logger.info('Reconnecting to websocket with delay...', { + ...WS_LOGGER_CATEGORY, + wsKey, + connectionDelayMs, + }); + if (this.wsStore.get(wsKey)?.activeReconnectTimer) { this.clearReconnectTimer(wsKey); } this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => { - this.logger.info('Reconnecting to websocket', { + this.logger.info('Reconnecting to websocket now', { ...WS_LOGGER_CATEGORY, wsKey, }); @@ -1250,6 +1266,10 @@ export abstract class BaseWebsocketClient< ); this.getWsStore().rejectAllDeferredPromises(wsKey, 'disconnected'); this.setWsState(wsKey, WsConnectionStateEnum.INITIAL); + + // This was an intentional close, delete all state for this connection, as if it never existed: + this.wsStore.delete(wsKey); + this.emit('close', { wsKey, event }); } } diff --git a/src/util/websockets/WsStore.ts b/src/util/websockets/WsStore.ts index de32820..fee9a72 100644 --- a/src/util/websockets/WsStore.ts +++ b/src/util/websockets/WsStore.ts @@ -47,9 +47,9 @@ export class WsStore< private wsState: Record> = {}; - private logger: typeof DefaultLogger; + private logger: DefaultLogger; - constructor(logger: typeof DefaultLogger) { + constructor(logger: DefaultLogger) { this.logger = logger || DefaultLogger; } @@ -131,6 +131,10 @@ export class WsStore< return wsConnection; } + /** + * deferred promises + */ + getDeferredPromise( wsKey: WsKey, promiseRef: string | DeferredPromiseRef, @@ -206,9 +210,15 @@ export class WsStore< if (promise?.reject) { this.logger.trace( - `rejectDeferredPromise(): rejecting ${wsKey}/${promiseRef}/${value}`, + `rejectDeferredPromise(): rejecting ${wsKey}/${promiseRef}`, + value, ); - promise.reject(value); + + if (typeof value === 'string') { + promise.reject(new Error(value)); + } else { + promise.reject(value); + } } if (removeAfter) { @@ -252,6 +262,9 @@ export class WsStore< } try { + this.logger.trace( + `rejectAllDeferredPromises(): rejecting ${wsKey}/${promiseRef}/${reason}`, + ); this.rejectDeferredPromise(wsKey, promiseRef, reason, true); } catch (e) { this.logger.error( @@ -339,6 +352,7 @@ export class WsStore< setConnectionState(key: WsKey, state: WsConnectionStateEnum) { this.get(key, true).connectionState = state; + this.get(key, true).connectionStateChangedAt = new Date(); } isConnectionState(key: WsKey, state: WsConnectionStateEnum): boolean { @@ -355,6 +369,22 @@ export class WsStore< this.isConnectionState(key, WsConnectionStateEnum.CONNECTING) || this.isConnectionState(key, WsConnectionStateEnum.RECONNECTING); + if (isConnectionInProgress) { + const wsState = this.get(key, true); + const stateLastChangedAt = wsState?.connectionStateChangedAt; + const stateChangedAtTimestamp = stateLastChangedAt?.getTime(); + if (stateChangedAtTimestamp) { + const timestampNow = new Date().getTime(); + const stateChangedTimeAgo = timestampNow - stateChangedAtTimestamp; + const stateChangeTimeout = 15000; // allow a max 15 second timeout since the last state change before assuming stuck; + if (stateChangedTimeAgo >= stateChangeTimeout) { + const msg = 'State change timed out, reconnect workflow stuck?'; + this.logger.error(msg, { key, wsState }); + this.setConnectionState(key, WsConnectionStateEnum.ERROR); + } + } + } + return isConnectionInProgress; } @@ -366,13 +396,14 @@ export class WsStore< getTopicsByKey(): Record> { const result: any = {}; + for (const refKey in this.wsState) { result[refKey] = this.getTopics(refKey as WsKey); } + return result; } - // Since topics are objects we can't rely on the set to detect duplicates /** * Find matching "topic" request from the store * @param key diff --git a/src/util/websockets/WsStore.types.ts b/src/util/websockets/WsStore.types.ts index d2a4d49..26f8626 100644 --- a/src/util/websockets/WsStore.types.ts +++ b/src/util/websockets/WsStore.types.ts @@ -8,7 +8,7 @@ export enum WsConnectionStateEnum { CLOSING = 3, RECONNECTING = 4, // ERROR_RECONNECTING = 5, - // ERROR = 5, + ERROR = 5, } export interface DeferredPromise { @@ -26,6 +26,7 @@ export interface WsStoredState { ws?: WebSocket; /** The current lifecycle state of the connection (enum) */ connectionState?: WsConnectionStateEnum; + connectionStateChangedAt?: Date; /** A timer that will send an upstream heartbeat (ping) when it expires */ activePingTimer?: ReturnType | undefined; /** A timer tracking that an upstream heartbeat was sent, expecting a reply before it expires */ diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 25eb3ce..ec980ce 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -396,21 +396,48 @@ export class WebsocketClient extends BaseWebsocketClient< // Store deferred promise, resolved within the "resolveEmittableEvents" method while parsing incoming events const promiseRef = getPromiseRefForWSAPIRequest(requestEvent); - const deferredPromise = - this.getWsStore().createDeferredPromise( - wsKey, - promiseRef, - false, - ); + const deferredPromise = this.getWsStore().createDeferredPromise< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TWSAPIResponse & { request: any } + >(wsKey, promiseRef, false); + + // Enrich returned promise with request context for easier debugging + deferredPromise.promise + ?.then((res) => { + if (!Array.isArray(res)) { + res.request = { + wsKey, + ...signedEvent, + }; + } + + return res; + }) + .catch((e) => { + if (typeof e === 'string') { + this.logger.error('unexpcted string', { e }); + return e; + } + e.request = { + wsKey, + operation, + params: params, + }; + // throw e; + return e; + }); this.logger.trace( `sendWSAPIRequest(): sending raw request: ${JSON.stringify(signedEvent, null, 2)}`, ); // Send event - this.tryWsSend(wsKey, JSON.stringify(signedEvent)); + const throwExceptions = false; + this.tryWsSend(wsKey, JSON.stringify(signedEvent), throwExceptions); - this.logger.trace(`sendWSAPIRequest(): sent ${operation} event`); + this.logger.trace( + `sendWSAPIRequest(): sent "${operation}" event with promiseRef(${promiseRef})`, + ); // Return deferred promise, so caller can await this call return deferredPromise.promise!; From 93a9165fb3c7b1055c307139bf02495905ece7be Mon Sep 17 00:00:00 2001 From: JJ-Cro Date: Tue, 20 May 2025 13:16:21 +0200 Subject: [PATCH 03/12] feat(): added bybit wsapi client all functions --- src/types/websockets/ws-api.ts | 2 + src/websocket-api-client.ts | 123 +++++++++++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/src/types/websockets/ws-api.ts b/src/types/websockets/ws-api.ts index ffbfed9..f6de45f 100644 --- a/src/types/websockets/ws-api.ts +++ b/src/types/websockets/ws-api.ts @@ -65,6 +65,7 @@ export interface WSAPIRequest< export interface WSAPIResponse< TResponseData extends object = object, TOperation extends WSAPIOperation = WSAPIOperation, + TResponseExtInfo = {}, // added as optional for batch calls > { wsKey: WsKey; /** Auto-generated */ @@ -73,6 +74,7 @@ export interface WSAPIResponse< retMsg: 'OK' | string; op: TOperation; data: TResponseData; + retExtInfo: TResponseExtInfo; header?: { 'X-Bapi-Limit': string; 'X-Bapi-Limit-Status': string; diff --git a/src/websocket-api-client.ts b/src/websocket-api-client.ts index 4e97ab0..4bf3eae 100644 --- a/src/websocket-api-client.ts +++ b/src/websocket-api-client.ts @@ -1,4 +1,15 @@ -import { OrderParamsV5, OrderResultV5 } from './types'; +import { + AmendOrderParamsV5, + BatchAmendOrderParamsV5, + BatchAmendOrderResultV5, + BatchCancelOrderParamsV5, + BatchCancelOrderResultV5, + BatchCreateOrderResultV5, + BatchOrderParamsV5, + BatchOrdersRetExtInfoV5, + CancelOrderParamsV5, + OrderResultV5, +} from './types'; import { WSAPIResponse } from './types/websockets/ws-api'; import { WSClientConfigurableOptions } from './types/websockets/ws-general'; import { DefaultLogger } from './util'; @@ -75,21 +86,121 @@ export class WebsocketAPIClient { */ /** - * Submit a new order + * Amend an order * * @param params * @returns */ - submitNewOrder( - params: OrderParamsV5, - ): Promise> { + amendOrder( + params: AmendOrderParamsV5, + ): Promise> { return this.wsClient.sendWSAPIRequest( WS_KEY_MAP.v5PrivateTrade, - 'order.create', + 'order.amend', params, ); } + /** + * Cancel an order + * + * @param params + * @returns + */ + cancelOrder( + params: CancelOrderParamsV5, + ): Promise> { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.v5PrivateTrade, + 'order.cancel', + params, + ); + } + + /** + * Batch submit orders + * + * @param params + * @returns + */ + batchSubmitOrders( + category: 'option' | 'linear', + orders: BatchOrderParamsV5[], + ): Promise< + WSAPIResponse< + { + list: BatchCreateOrderResultV5[]; + }, + 'order.create-batch', + BatchOrdersRetExtInfoV5 + > + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.v5PrivateTrade, + 'order.create-batch', + { + category, + request: orders, + }, + ); + } + + /** + * Batch amend orders + * + * @param params + * @returns + */ + batchAmendOrder( + category: 'option' | 'linear', + orders: BatchAmendOrderParamsV5[], + ): Promise< + WSAPIResponse< + { + list: BatchAmendOrderResultV5[]; + }, + 'order.amend-batch', + BatchOrdersRetExtInfoV5 + > + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.v5PrivateTrade, + 'order.amend-batch', + { + category, + request: orders, + }, + ); + } + + /** + * Batch cancel orders + * + * @param params + * @returns + */ + batchCancelOrder( + category: 'option' | 'linear', + orders: BatchCancelOrderParamsV5[], + ): Promise< + WSAPIResponse< + { + list: BatchCancelOrderResultV5[]; + }, + 'order.cancel-batch', + BatchOrdersRetExtInfoV5 + > + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.v5PrivateTrade, + 'order.cancel-batch', + { + category, + request: orders, + }, + ); + } + /** * * From 9a86f3daa6d668ce67de3252deea9aca8088b48f Mon Sep 17 00:00:00 2001 From: JJ-Cro Date: Tue, 20 May 2025 13:48:03 +0200 Subject: [PATCH 04/12] fix(): add submitOrder --- src/websocket-api-client.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/websocket-api-client.ts b/src/websocket-api-client.ts index 4bf3eae..d08037a 100644 --- a/src/websocket-api-client.ts +++ b/src/websocket-api-client.ts @@ -8,6 +8,7 @@ import { BatchOrderParamsV5, BatchOrdersRetExtInfoV5, CancelOrderParamsV5, + OrderParamsV5, OrderResultV5, } from './types'; import { WSAPIResponse } from './types/websockets/ws-api'; @@ -85,6 +86,22 @@ export class WebsocketAPIClient { * https://bybit-exchange.github.io/docs/v5/websocket/trade/guideline */ + /** + * Submit a new order + * + * @param params + * @returns + */ + submitNewOrder( + params: OrderParamsV5, + ): Promise> { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.v5PrivateTrade, + 'order.create', + params, + ); + } + /** * Amend an order * From 4108f191104dc1bdf679a7808aa9f7e54e589c2a Mon Sep 17 00:00:00 2001 From: JJ-Cro Date: Thu, 22 May 2025 10:24:07 +0200 Subject: [PATCH 05/12] feat(): added wsapiclient examples, updated endpoint map, updated links to point to bybit --- docs/endpointFunctionList.md | 18 +++- examples/ws-api-client.ts | 154 +++++++++++++++++++++++++++++++++++ examples/ws-api-events.ts | 3 +- src/websocket-api-client.ts | 2 +- 4 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 examples/ws-api-client.ts diff --git a/docs/endpointFunctionList.md b/docs/endpointFunctionList.md index f3a7c44..60e7f2e 100644 --- a/docs/endpointFunctionList.md +++ b/docs/endpointFunctionList.md @@ -20,6 +20,7 @@ All REST clients are in the [src](/src) folder. For usage examples, make sure to List of clients: - [rest-client-v5](#rest-client-v5ts) +- [websocket-api-client](#websocket-api-clientts) If anything is missing or wrong, please open an issue or let us know in our [Node.js Traders](https://t.me/nodetraders) telegram group! @@ -241,4 +242,19 @@ This table includes all endpoints from the official Exchange API docs and corres | [getP2POrderMessages()](https://github.com/tiagosiebler/bybit-api/blob/master/src/rest-client-v5.ts#L3013) | :closed_lock_with_key: | POST | `/v5/p2p/order/message/listpage` | | [getP2PUserInfo()](https://github.com/tiagosiebler/bybit-api/blob/master/src/rest-client-v5.ts#L3027) | :closed_lock_with_key: | POST | `/v5/p2p/user/personal/info` | | [getP2PCounterpartyUserInfo()](https://github.com/tiagosiebler/bybit-api/blob/master/src/rest-client-v5.ts#L3034) | :closed_lock_with_key: | POST | `/v5/p2p/user/order/personal/info` | -| [getP2PUserPayments()](https://github.com/tiagosiebler/bybit-api/blob/master/src/rest-client-v5.ts#L3043) | :closed_lock_with_key: | POST | `/v5/p2p/user/payment/list` | \ No newline at end of file +| [getP2PUserPayments()](https://github.com/tiagosiebler/bybit-api/blob/master/src/rest-client-v5.ts#L3043) | :closed_lock_with_key: | POST | `/v5/p2p/user/payment/list` | + +# websocket-api-client.ts + +This table includes all endpoints from the official Exchange API docs and corresponding SDK functions for each endpoint that are found in [websocket-api-client.ts](/src/websocket-api-client.ts). + +This client provides WebSocket API endpoints which allow for faster interactions with the Bybit API via a WebSocket connection. + +| Function | AUTH | HTTP Method | Endpoint | +| -------- | :------: | :------: | -------- | +| [submitNewOrder()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L95) | :closed_lock_with_key: | WS | `order.create` | +| [amendOrder()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L111) | :closed_lock_with_key: | WS | `order.amend` | +| [cancelOrder()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L127) | :closed_lock_with_key: | WS | `order.cancel` | +| [batchSubmitOrders()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L143) | :closed_lock_with_key: | WS | `order.create-batch` | +| [batchAmendOrder()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L171) | :closed_lock_with_key: | WS | `order.amend-batch` | +| [batchCancelOrder()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L199) | :closed_lock_with_key: | WS | `order.cancel-batch` | \ No newline at end of file diff --git a/examples/ws-api-client.ts b/examples/ws-api-client.ts new file mode 100644 index 0000000..6b5e1f3 --- /dev/null +++ b/examples/ws-api-client.ts @@ -0,0 +1,154 @@ +import { DefaultLogger, WebsocketAPIClient } from '../src'; + +// or +// import { DefaultLogger, WebsocketClient } from 'bybit-api'; + +const key = process.env.API_KEY_COM || '1j3PcP9EMjGPbjcjlC'; +const secret = + process.env.API_SECRET_COM || 'VlvBfQqMcGKX77XgYcMqZmOR7biwA9ZvaTwk'; + +async function main() { + // Optional + const logger = { + ...DefaultLogger, + // For a more detailed view of the WebsocketClient, enable the `trace` level by uncommenting the below line: + // trace: (...params) => console.log('trace', ...params), + }; + + const wsClient = new WebsocketAPIClient( + { + 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 + }, + logger, // Optional: inject a custom logger + ); + + // To do: Check if this is how listeners are handled + + 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); + }); + + // 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); + + try { + 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.log('submitNewOrder error: ', e); + } + + try { + 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 { + 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); + } + + try { + const response = await wsClient.batchSubmitOrders('linear', [ + { + symbol: 'BTCUSDT', + orderType: 'Limit', + qty: '0.001', + side: 'Buy', + price: '50000', + }, + { + symbol: 'BTCUSDT', + orderType: 'Limit', + qty: '0.001', + side: 'Buy', + price: '60000', + }, + { + symbol: 'BTCUSDT', + orderType: 'Limit', + qty: '0.001', + side: 'Buy', + price: '70000', + }, + ]); + console.log('batchSubmitOrders response: ', response); + } catch (e) { + console.log('batchSubmitOrders error: ', e); + } + + try { + const response = await wsClient.batchAmendOrder('linear', [ + { + symbol: 'BTCUSDT', + orderId: '2473ee58', + price: '80000', + }, + { + symbol: 'BTCUSDT', + orderId: 'b4b9e205-793c-4777-8112-0bf3c2d26b6e', + price: '80000', + }, + ]); + console.log('batchAmendOrder response: ', response); + } catch (e) { + console.log('batchAmendOrder error: ', e); + } + + try { + const response = await wsClient.batchCancelOrder('linear', [ + { + symbol: 'BTCUSDT', + orderId: '2473ee58', + }, + { + symbol: 'BTCUSDT', + orderId: 'b4b9e205-793c-4777-8112-0bf3c2d26b6e', + }, + ]); + console.log('batchCancelOrder response: ', response); + } catch (e) { + console.log('batchCancelOrder error: ', e); + } +} + +main(); diff --git a/examples/ws-api-events.ts b/examples/ws-api-events.ts index ce47005..99aa670 100644 --- a/examples/ws-api-events.ts +++ b/examples/ws-api-events.ts @@ -55,7 +55,8 @@ 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` example for a demonstration you can compare. + * promise-driven interface is recommended. See the `ws-api-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 * any unnecessary "unhandled promise rejection" exceptions. diff --git a/src/websocket-api-client.ts b/src/websocket-api-client.ts index d08037a..d1ec80d 100644 --- a/src/websocket-api-client.ts +++ b/src/websocket-api-client.ts @@ -47,7 +47,7 @@ export interface WSAPIClientConfigurableOptions { * may find the below methods slightly more intuitive. * * Refer to the WS API promises example for a more detailed example on using sendWSAPIRequest() directly: - * https://github.com/tiagosiebler/binance/blob/master/examples/WebSockets/ws-api-raw-promises.ts#L108 + * https://github.com/tiagosiebler/bybit-api/blob/master/examples/ws-api-client.ts#L58 */ export class WebsocketAPIClient { private wsClient: WebsocketClient; From 599616651d9f085ae6327dd0ff4adee39f4cdf2e Mon Sep 17 00:00:00 2001 From: JJ-Cro Date: Thu, 22 May 2025 11:03:56 +0200 Subject: [PATCH 06/12] chore(): cleanup --- examples/ws-api-client.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/ws-api-client.ts b/examples/ws-api-client.ts index 6b5e1f3..b6c595c 100644 --- a/examples/ws-api-client.ts +++ b/examples/ws-api-client.ts @@ -3,9 +3,8 @@ import { DefaultLogger, WebsocketAPIClient } from '../src'; // or // import { DefaultLogger, WebsocketClient } from 'bybit-api'; -const key = process.env.API_KEY_COM || '1j3PcP9EMjGPbjcjlC'; -const secret = - process.env.API_SECRET_COM || 'VlvBfQqMcGKX77XgYcMqZmOR7biwA9ZvaTwk'; +const key = process.env.API_KEY_COM; +const secret = process.env.API_SECRET_COM; async function main() { // Optional From 9f9732a035d9e7c0dc941ac32115cc300d80dd8f Mon Sep 17 00:00:00 2001 From: JJ-Cro Date: Thu, 22 May 2025 11:10:15 +0200 Subject: [PATCH 07/12] feat(): add example event handlers for WebsocketAPIClient and clean up listener code --- examples/ws-api-client.ts | 48 ++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/examples/ws-api-client.ts b/examples/ws-api-client.ts index b6c595c..dec15e2 100644 --- a/examples/ws-api-client.ts +++ b/examples/ws-api-client.ts @@ -1,31 +1,14 @@ import { DefaultLogger, WebsocketAPIClient } from '../src'; // or -// import { DefaultLogger, WebsocketClient } from 'bybit-api'; +// import { DefaultLogger, WebsocketAPIClient } from 'bybit-api'; const key = process.env.API_KEY_COM; const secret = process.env.API_SECRET_COM; -async function main() { - // Optional - const logger = { - ...DefaultLogger, - // For a more detailed view of the WebsocketClient, enable the `trace` level by uncommenting the below line: - // trace: (...params) => console.log('trace', ...params), - }; - - const wsClient = new WebsocketAPIClient( - { - 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 - }, - logger, // Optional: inject a custom logger - ); - - // To do: Check if this is how listeners are handled - +/* function attachEventHandlers( + wsClient: TWSClient, +): void { wsClient.getWSClient().on('update', (data) => { console.log('raw message received ', JSON.stringify(data)); }); @@ -42,6 +25,29 @@ async function main() { wsClient.getWSClient().on('authenticated', (data) => { console.log('ws has authenticated ', data?.wsKey); }); +} */ + +async function main() { + // Optional + const logger = { + ...DefaultLogger, + // For a more detailed view of the WebsocketClient, enable the `trace` level by uncommenting the below line: + // trace: (...params) => console.log('trace', ...params), + }; + + const wsClient = new WebsocketAPIClient( + { + 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 + + // If you want your own event handlers instead of the default ones with logs, + // disable this setting and see the `attachEventHandlers` example below: + // attachEventListeners: false + }, + logger, // Optional: inject a custom logger + ); // Optional, if you see RECV Window errors, you can use this to manage time issues. // ! However, make sure you sync your system clock first! From 55fbf71e05b870bd6137822f1f1feb9003f7f873 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 22 May 2025 11:42:33 +0100 Subject: [PATCH 08/12] 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 = { From 0bc368462e939bab9249fd551017149d21db4978 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 22 May 2025 11:45:39 +0100 Subject: [PATCH 09/12] chore(): typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9e74ba..201576a 100644 --- a/README.md +++ b/README.md @@ -425,7 +425,7 @@ The below example demonstrates the promise-driven approach, which behaves simila 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'); +const { WS_KEY_MAP, WebsocketAPIClient } = require('bybit-api'); // or // import { WS_KEY_MAP, WebsocketAPIClient } from 'bybit-api'; From 9e72e203ebf98b254e6ab1690e144a4a6e94b079 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 22 May 2025 11:46:23 +0100 Subject: [PATCH 10/12] chore(): update example --- examples/ws-api-client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/ws-api-client.ts b/examples/ws-api-client.ts index 999581c..217e2d1 100644 --- a/examples/ws-api-client.ts +++ b/examples/ws-api-client.ts @@ -1,7 +1,8 @@ -import { DefaultLogger, WebsocketAPIClient, WebsocketClient } from '../src'; +import { DefaultLogger, WebsocketAPIClient } from '../src'; // or // import { DefaultLogger, WebsocketAPIClient } from 'bybit-api'; +// const { DefaultLogger, WebsocketAPIClient } = require('bybit-api'); const key = process.env.API_KEY_COM; const secret = process.env.API_SECRET_COM; From aedfb0b32febb2a6b1c4a401e7702dbe212bc567 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 22 May 2025 11:48:13 +0100 Subject: [PATCH 11/12] chore(): update example --- examples/ws-api-client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/ws-api-client.ts b/examples/ws-api-client.ts index 217e2d1..fa6e7e5 100644 --- a/examples/ws-api-client.ts +++ b/examples/ws-api-client.ts @@ -61,7 +61,9 @@ async function main() { // https://github.com/tiagosiebler/awesome-crypto-examples/wiki/Timestamp-for-this-request-is-outside-of-the-recvWindow // wsClient.setTimeOffsetMs(-5000); - await wsClient.getWSClient().connectWSAPI(); + // Optional: prepare the WebSocket API connection in advance. + // This happens automatically but you can do this early before making any API calls, to prevent delays from a cold start. + // await wsClient.getWSClient().connectWSAPI(); try { const response = await wsClient.submitNewOrder({ From 9c592a2a48f2be4906ae78b6ef6343d387ab4e74 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 22 May 2025 11:53:54 +0100 Subject: [PATCH 12/12] chore(): update example doc --- src/websocket-api-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/websocket-api-client.ts b/src/websocket-api-client.ts index d1ec80d..0582b6a 100644 --- a/src/websocket-api-client.ts +++ b/src/websocket-api-client.ts @@ -47,7 +47,7 @@ export interface WSAPIClientConfigurableOptions { * may find the below methods slightly more intuitive. * * Refer to the WS API promises example for a more detailed example on using sendWSAPIRequest() directly: - * https://github.com/tiagosiebler/bybit-api/blob/master/examples/ws-api-client.ts#L58 + * https://github.com/tiagosiebler/bybit-api/blob/master/examples/ws-api-raw-promises.ts */ export class WebsocketAPIClient { private wsClient: WebsocketClient;