feat(): add example for REST-like WS API usage for Bybit in Node.js/JavaScript/TypeScript. Update type flowing and docs for stricter types.

This commit is contained in:
tiagosiebler
2025-01-22 12:07:05 +00:00
parent 13cd799e7c
commit 98d2331f0e
6 changed files with 284 additions and 118 deletions

View File

@@ -22,15 +22,10 @@ export const WS_API_Operations: WSAPIOperation[] = [
'order.cancel',
];
export interface WsRequestOperationBybit<
TWSTopic extends string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
// TWSPayload = any,
> {
export interface WsRequestOperationBybit<TWSTopic extends string> {
req_id: string;
op: WsOperation;
args?: (TWSTopic | string | number)[];
// payload?: TWSPayload;
}
export interface WSAPIRequest<
@@ -48,17 +43,6 @@ export interface WSAPIRequest<
args: [TRequestParams];
}
export interface WsAPIWsKeyTopicMap {
[WS_KEY_MAP.v5PrivateTrade]: WSAPIOperation;
}
export interface WsAPITopicRequestParamMap {
'order.create': OrderParamsV5;
'order.amend': AmendOrderParamsV5;
'order.cancel': CancelOrderParamsV5;
// ping: undefined;
}
export interface WSAPIResponse<
TResponseData extends object = object,
TOperation extends WSAPIOperation = WSAPIOperation,
@@ -80,12 +64,32 @@ export interface WSAPIResponse<
connId: string;
}
// export interface WsAPIResponseMap<TChannel extends WSAPITopic = WSAPITopic> {
// 'spot.login': WSAPIResponse<WSAPILoginResponse, TChannel>;
// 'futures.login': WSAPIResponse<WSAPILoginResponse, TChannel>;
// string: object;
// }
export type Exact<T> = {
// This part says: if there's any key that's not in T, it's an error
[K: string]: never;
} & {
[K in keyof T]: T[K];
};
/**
* List of operations supported for this WsKey (connection)
*/
export interface WsAPIWsKeyTopicMap {
[WS_KEY_MAP.v5PrivateTrade]: WSAPIOperation;
}
/**
* Request parameters expected per operation
*/
export interface WsAPITopicRequestParamMap {
'order.create': OrderParamsV5;
'order.amend': AmendOrderParamsV5;
'order.cancel': CancelOrderParamsV5;
}
/**
* Response structure expected for each operation
*/
export interface WsAPIOperationResponseMap {
'order.create': WSAPIResponse<OrderResultV5, 'order.create'>;
'order.amend': WSAPIResponse<OrderResultV5, 'order.amend'>;
@@ -97,36 +101,4 @@ export interface WsAPIOperationResponseMap {
data: [string];
connId: string;
};
// 'spot.login': WSAPIResponse<WSAPILoginResponse, 'spot.login'>;
// 'futures.login': WSAPIResponse<WSAPILoginResponse, 'futures.login'>;
// 'spot.order_place': WSAPIResponse<TResponseType, 'spot.order_place'>;
// 'spot.order_cancel': WSAPIResponse<TResponseType, 'spot.order_cancel'>;
// 'spot.order_cancel_ids': WSAPIResponse<
// TResponseType,
// 'spot.order_cancel_ids'
// >;
// 'spot.order_cancel_cp': WSAPIResponse<TResponseType, 'spot.order_cancel_cp'>;
// 'spot.order_amend': WSAPIResponse<TResponseType, 'spot.order_amend'>;
// 'spot.order_status': WSAPIResponse<
// WSAPIOrderStatusResponse,
// 'spot.order_status'
// >;
// 'futures.order_place': WSAPIResponse<TResponseType[], 'futures.order_place'>;
// 'futures.order_batch_place': WSAPIResponse<
// TResponseType[],
// 'futures.order_batch_place'
// >;
// 'futures.order_cancel': WSAPIResponse<TResponseType, 'futures.order_cancel'>;
// 'futures.order_cancel_cp': WSAPIResponse<
// TResponseType,
// 'futures.order_cancel_cp'
// >;
// 'futures.order_amend': WSAPIResponse<TResponseType, 'futures.order_amend'>;
// 'futures.order_list': WSAPIResponse<TResponseType[], 'futures.order_list'>;
// 'futures.order_status': WSAPIResponse<
// WSAPIOrderStatusResponse,
// 'futures.order_status'
// >;
}

View File

@@ -39,10 +39,7 @@ interface WSClientEventMap<WsKey extends string> {
/** Received data for topic */
update: (response: any & { wsKey: WsKey }) => void;
/** Exception from ws client OR custom listeners (e.g. if you throw inside your event handler) */
exception: (
response: any & { wsKey: WsKey; isWSAPIResponse?: boolean },
) => void;
error: (response: any & { wsKey: WsKey }) => void;
error: (response: any & { wsKey: WsKey; isWSAPIResponse?: boolean }) => void;
/** Confirmation that a connection successfully authenticated */
authenticated: (event: {
wsKey: WsKey;
@@ -52,7 +49,7 @@ interface WSClientEventMap<WsKey extends string> {
}
export interface EmittableEvent<TEvent = any> {
eventType: 'response' | 'update' | 'exception' | 'authenticated';
eventType: 'response' | 'update' | 'error' | 'authenticated';
event: TEvent;
isWSAPIResponse?: boolean;
}
@@ -594,7 +591,7 @@ export abstract class BaseWebsocketClient<
if (!error.message) {
this.logger.error(`${context} due to unexpected error: `, error);
this.emit('response', { ...error, wsKey });
this.emit('exception', { ...error, wsKey });
this.emit('error', { ...error, wsKey });
return;
}
@@ -627,7 +624,7 @@ export abstract class BaseWebsocketClient<
}
this.emit('response', { ...error, wsKey });
this.emit('exception', { ...error, wsKey });
this.emit('error', { ...error, wsKey });
}
/** Get a signature, build the auth request and send it */

View File

@@ -34,6 +34,7 @@ import {
MidflightWsRequestEvent,
} from './util/BaseWSClient';
import {
Exact,
WSAPIRequest,
WsAPIOperationResponseMap,
WsAPITopicRequestParamMap,
@@ -838,7 +839,7 @@ export class WebsocketClient extends BaseWebsocketClient<
}
results.push({
eventType: 'exception',
eventType: 'error',
event: parsed,
isWSAPIResponse: true,
});
@@ -888,7 +889,7 @@ export class WebsocketClient extends BaseWebsocketClient<
// Failed request
if (parsed.success === false) {
results.push({
eventType: 'exception',
eventType: 'error',
event: parsed,
// isWSAPIResponse: isWSAPIResponseEvent,
});
@@ -938,7 +939,7 @@ export class WebsocketClient extends BaseWebsocketClient<
exception: e,
eventData: event.data,
},
eventType: 'exception',
eventType: 'error',
});
this.logger.error('Failed to parse event data due to exception: ', {
@@ -963,43 +964,59 @@ export class WebsocketClient extends BaseWebsocketClient<
/**
* Send a Websocket API event on a connection. Returns a promise that resolves on reply.
*
* Returned promise is rejected if an exception is detected in the reply OR the connection disconnects for any reason (even if automatic reconnect will happen).
*
* Authentication is automatic. If you didn't request authentication yourself, there might be a small delay after your first request, while the SDK automatically authenticates.
*
* Returned promise is rejected if:
* - an exception is detected in the reply, OR
* - the connection disconnects for any reason (even if automatic reconnect will happen).
*
* If you authenticated once and you're reconnected later (e.g. connection temporarily lost), the SDK will by default automatically:
* - Detect you were authenticated to the WS API before
* - Try to re-authenticate (up to 5 times, in case something (bad timestamp) goes wrong)
* - If it succeeds, it will emit the 'authenticated' event.
* - If it fails and gives up, it will emit an 'exception' event (type: 'wsapi.auth', reason: detailed text).
* - If it fails and gives up, it will emit an 'exception' event.
*
* You can turn off the automatic re-auth WS API logic using `reauthWSAPIOnReconnect: false` in the WSClient config.
*
* @param wsKey - The connection this event is for (e.g. "spotV4" | "perpFuturesUSDTV4" | "perpFuturesBTCV4" | "deliveryFuturesUSDTV4" | "deliveryFuturesBTCV4" | "optionsV4")
* @param channel - The channel this event is for (e.g. "spot.login" to authenticate)
* @param params - Any request parameters for the payload (contents of req_param in the docs). Signature generation is automatic, only send parameters such as order ID as per the docs.
* @param wsKey - The connection this event is for. Currently only "v5PrivateTrade" is supported, since that is the dedicated WS API connection.
* @param operation - The command being sent, e.g. "order.create" to submit a new order.
* @param params - Any request parameters for the command. E.g. `OrderParamsV5` to submit a new order. Only send parameters for the request body. Everything else is automatically handled.
* @returns Promise - tries to resolve with async WS API response. Rejects if disconnected or exception is seen in async WS API response
*/
// This overload allows the caller to omit the 3rd param, if it isn't required (e.g. for the login call)
async sendWSAPIRequest<
TWSKey extends keyof WsAPIWsKeyTopicMap = keyof WsAPIWsKeyTopicMap,
TWSOperation extends
WsAPIWsKeyTopicMap[TWSKey] = WsAPIWsKeyTopicMap[TWSKey],
TWSParams extends
WsAPITopicRequestParamMap[TWSOperation] = WsAPITopicRequestParamMap[TWSOperation],
// This overload allows the caller to omit the 3rd param, if it isn't required
sendWSAPIRequest<
TWSKey extends keyof WsAPIWsKeyTopicMap,
TWSOperation extends WsAPIWsKeyTopicMap[TWSKey],
TWSParams extends Exact<WsAPITopicRequestParamMap[TWSOperation]>,
>(
wsKey: TWSKey,
operation: TWSOperation,
...params: TWSParams extends undefined ? [] : [TWSParams]
): Promise<WsAPIOperationResponseMap[TWSOperation]>;
// These overloads give stricter types than mapped generics, since generic constraints 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(
wsKey: typeof WS_KEY_MAP.v5PrivateTrade,
operation: 'order.create',
params: WsAPITopicRequestParamMap['order.create'],
): Promise<WsAPIOperationResponseMap['order.create']>;
sendWSAPIRequest(
wsKey: typeof WS_KEY_MAP.v5PrivateTrade,
operation: 'order.amend',
params: WsAPITopicRequestParamMap['order.amend'],
): Promise<WsAPIOperationResponseMap['order.amend']>;
sendWSAPIRequest(
wsKey: typeof WS_KEY_MAP.v5PrivateTrade,
operation: 'order.cancel',
params: WsAPITopicRequestParamMap['order.cancel'],
): Promise<WsAPIOperationResponseMap['order.cancel']>;
async sendWSAPIRequest<
TWSKey extends keyof WsAPIWsKeyTopicMap = keyof WsAPIWsKeyTopicMap,
TWSOperation extends
WsAPIWsKeyTopicMap[TWSKey] = WsAPIWsKeyTopicMap[TWSKey],
TWSParams extends
WsAPITopicRequestParamMap[TWSOperation] = WsAPITopicRequestParamMap[TWSOperation],
TWSKey extends keyof WsAPIWsKeyTopicMap,
TWSOperation extends WsAPIWsKeyTopicMap[TWSKey],
TWSParams extends Exact<WsAPITopicRequestParamMap[TWSOperation]>,
TWSAPIResponse extends
WsAPIOperationResponseMap[TWSOperation] = WsAPIOperationResponseMap[TWSOperation],
>(