feat(): upgrade WebSocket layer to extend BaseWS abstraction. feat(): add promisified WS workflows, feat(): add WS API integration

This commit is contained in:
tiagosiebler
2025-01-16 16:47:09 +00:00
parent b613fd956d
commit 8a7c8ea274
9 changed files with 2512 additions and 1200 deletions

View File

@@ -0,0 +1,136 @@
import { APIID, WS_KEY_MAP } from '../../util';
import {
AmendOrderParamsV5,
CancelOrderParamsV5,
OrderParamsV5,
} from '../request';
import { WsKey } from './ws-general';
export type WSAPIOperation = 'order.create' | 'order.amend' | 'order.cancel';
export type WsOperation =
| 'subscribe'
| 'unsubscribe'
| 'auth'
| 'ping'
| 'pong';
export const WS_API_Operations: WSAPIOperation[] = [
'order.create',
'order.amend',
'order.cancel',
];
export interface WsRequestOperationBybit<
TWSTopic extends string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
// TWSPayload = any,
> {
req_id: string;
op: WsOperation;
args?: (TWSTopic | string | number)[];
// payload?: TWSPayload;
}
export interface WSAPIRequest<
TRequestParams = undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TWSOperation extends WSAPIOperation = any,
> {
reqId: string;
op: TWSOperation;
header: {
'X-BAPI-TIMESTAMP': string;
'X-BAPI-RECV-WINDOW': string;
Referer: typeof APIID;
};
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 type WsAPITopicRequestParams =
WsAPITopicRequestParamMap[keyof WsAPITopicRequestParamMap];
export interface WSAPIResponse<
TResponseData extends object = object,
TOperation extends WSAPIOperation = WSAPIOperation,
> {
wsKey: WsKey;
/** Auto-generated */
reqId: string;
retCode: 0 | number;
retMsg: 'OK' | string;
op: TOperation;
data: [TResponseData];
header?: {
'X-Bapi-Limit': string;
'X-Bapi-Limit-Status': string;
'X-Bapi-Limit-Reset-Timestamp': string;
Traceid: string;
Timenow: string;
};
connId: string;
}
// export interface WsAPIResponseMap<TChannel extends WSAPITopic = WSAPITopic> {
// 'spot.login': WSAPIResponse<WSAPILoginResponse, TChannel>;
// 'futures.login': WSAPIResponse<WSAPILoginResponse, TChannel>;
// string: object;
// }
export interface WsAPIOperationResponseMap<
TResponseType extends object = object,
> {
'order.create': WSAPIResponse<TResponseType, 'order.cancel'>;
'order.amend': WSAPIResponse<TResponseType, 'order.amend'>;
'order.cancel': WSAPIResponse<TResponseType, 'order.cancel'>;
ping: {
retCode: 0 | number;
retMsg: 'OK' | string;
op: 'pong';
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

@@ -1,3 +1,5 @@
import WebSocket from 'isomorphic-ws';
import { import {
CategoryV5, CategoryV5,
ExecTypeV5, ExecTypeV5,
@@ -18,7 +20,23 @@ import {
TPSLModeV5, TPSLModeV5,
TradeModeV5, TradeModeV5,
} from '../shared-v5'; } from '../shared-v5';
import { WsKey } from '.';
import { WsKey } from './ws-general';
export interface MessageEventLike {
target: WebSocket;
type: 'message';
data: string;
}
export function isMessageEvent(msg: unknown): msg is MessageEventLike {
if (typeof msg !== 'object' || !msg) {
return false;
}
const message = msg as MessageEventLike;
return message['type'] === 'message' && typeof message['data'] === 'string';
}
export interface WSPublicTopicEventV5<TTopic extends string, TType, TData> { export interface WSPublicTopicEventV5<TTopic extends string, TType, TData> {
id?: string; id?: string;

View File

@@ -82,10 +82,23 @@ export type WsTopic = WsPublicTopics | WsPrivateTopic;
/** This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets) */ /** This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets) */
export type WsKey = (typeof WS_KEY_MAP)[keyof typeof WS_KEY_MAP]; export type WsKey = (typeof WS_KEY_MAP)[keyof typeof WS_KEY_MAP];
export type WsMarket = 'all';
export interface WSClientConfigurableOptions { export interface WSClientConfigurableOptions {
/** Your API key */
key?: string; key?: string;
/** Your API secret */
secret?: string; secret?: string;
/**
* Set to `true` to connect to Bybit's testnet environment.
*
* Notes:
*
* - If demo trading, `testnet` should be set to false!
* - If testing a strategy, use demo trading instead. Testnet market data is very different from real market conditions.
*/
testnet?: boolean; testnet?: boolean;
/** /**
@@ -96,28 +109,54 @@ export interface WSClientConfigurableOptions {
demoTrading?: boolean; demoTrading?: boolean;
/** /**
* The API group this client should connect to. * The API group this client should connect to. The V5 market is currently used by default.
* *
* For the V3 APIs use `v3` as the market (spot/unified margin/usdc/account asset/copy trading) * For the V3 APIs use `v3` as the market (spot/unified margin/usdc/account asset/copy trading)
*/ */
market: APIMarket; market?: APIMarket;
pongTimeout?: number; /** Define a recv window when preparing a private websocket signature. This is in milliseconds, so 5000 == 5 seconds */
pingInterval?: number;
reconnectTimeout?: number;
/** Override the recv window for authenticating over websockets (default: 5000 ms) */
recvWindow?: number; recvWindow?: number;
/** How often to check if the connection is alive */
pingInterval?: number;
/** How long to wait for a pong (heartbeat reply) before assuming the connection is dead */
pongTimeout?: number;
/** Delay in milliseconds before respawning the connection */
reconnectTimeout?: number;
restOptions?: RestClientOptions; restOptions?: RestClientOptions;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
requestOptions?: any; requestOptions?: any;
wsUrl?: string; wsUrl?: string;
/** If true, fetch server time before trying to authenticate (disabled by default) */
fetchTimeOffsetBeforeAuth?: boolean; /**
* Allows you to provide a custom "signMessage" function, e.g. to use node's much faster createHmac method
*
* Look in the examples folder for a demonstration on using node's createHmac instead.
*/
customSignMessageFn?: (message: string, secret: string) => Promise<string>;
/**
* If you authenticated the WS API before, automatically try to
* re-authenticate the WS API if you're disconnected/reconnected for any reason.
*/
reauthWSAPIOnReconnect?: boolean;
} }
/**
* WS configuration that's always defined, regardless of user configuration
* (usually comes from defaults if there's no user-provided values)
*/
export interface WebsocketClientOptions extends WSClientConfigurableOptions { export interface WebsocketClientOptions extends WSClientConfigurableOptions {
market: APIMarket; market: APIMarket;
pongTimeout: number; pongTimeout: number;
pingInterval: number; pingInterval: number;
reconnectTimeout: number; reconnectTimeout: number;
recvWindow: number;
authPrivateConnectionsOnConnect: boolean;
authPrivateRequests: boolean;
reauthWSAPIOnReconnect: boolean;
} }

1305
src/util/BaseWSClient.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,22 +4,13 @@
export type LogParams = null | any; export type LogParams = null | any;
export const DefaultLogger = { export const DefaultLogger = {
/** Ping/pong events and other raw messages that might be noisy */ /** Ping/pong events and other raw messages that might be noisy. Enable this while troubleshooting. */
silly: (...params: LogParams): void => { trace: (..._params: LogParams): void => {
// console.log(params); // console.log(_params);
},
debug: (...params: LogParams): void => {
console.log(params);
},
notice: (...params: LogParams): void => {
console.log(params);
}, },
info: (...params: LogParams): void => { info: (...params: LogParams): void => {
console.info(params); console.info(params);
}, },
warning: (...params: LogParams): void => {
console.error(params);
},
error: (...params: LogParams): void => { error: (...params: LogParams): void => {
console.error(params); console.error(params);
}, },

View File

@@ -4,6 +4,7 @@ import {
WebsocketSucceededTopicSubscriptionConfirmationEvent, WebsocketSucceededTopicSubscriptionConfirmationEvent,
WebsocketTopicSubscriptionConfirmationEvent, WebsocketTopicSubscriptionConfirmationEvent,
} from '../types/websockets/ws-confirmations'; } from '../types/websockets/ws-confirmations';
import { WSAPIResponse, WS_API_Operations } from '../types/websockets/ws-api';
export interface RestClientOptions { export interface RestClientOptions {
/** Your API key */ /** Your API key */
@@ -199,6 +200,20 @@ export function isTopicSubscriptionConfirmation(
return true; return true;
} }
export function isWSAPIResponse(
msg: unknown,
): msg is Omit<WSAPIResponse, 'wsKey'> {
if (typeof msg !== 'object' || !msg) {
return false;
}
if (typeof msg['op'] !== 'string') {
return false;
}
return (WS_API_Operations as string[]).includes(msg['op']);
}
export function isTopicSubscriptionSuccess( export function isTopicSubscriptionSuccess(
msg: unknown, msg: unknown,
): msg is WebsocketSucceededTopicSubscriptionConfirmationEvent { ): msg is WebsocketSucceededTopicSubscriptionConfirmationEvent {

View File

@@ -32,8 +32,9 @@ export function isDeepObjectMatch(object1: unknown, object2: unknown): boolean {
return true; return true;
} }
const DEFERRED_PROMISE_REF = { export const DEFERRED_PROMISE_REF = {
CONNECTION_IN_PROGRESS: 'CONNECTION_IN_PROGRESS', CONNECTION_IN_PROGRESS: 'CONNECTION_IN_PROGRESS',
AUTHENTICATION_IN_PROGRESS: 'AUTHENTICATION_IN_PROGRESS',
} as const; } as const;
type DeferredPromiseRef = type DeferredPromiseRef =
@@ -266,6 +267,15 @@ export class WsStore<
); );
} }
getAuthenticationInProgressPromise(
wsKey: WsKey,
): DeferredPromise<WSConnectedResult & { event: any }> | undefined {
return this.getDeferredPromise(
wsKey,
DEFERRED_PROMISE_REF.AUTHENTICATION_IN_PROGRESS,
);
}
/** /**
* Create a deferred promise designed to track a connection attempt in progress. * Create a deferred promise designed to track a connection attempt in progress.
* *
@@ -282,6 +292,17 @@ export class WsStore<
); );
} }
createAuthenticationInProgressPromise(
wsKey: WsKey,
throwIfExists: boolean,
): DeferredPromise<WSConnectedResult & { event: any }> {
return this.createDeferredPromise(
wsKey,
DEFERRED_PROMISE_REF.AUTHENTICATION_IN_PROGRESS,
throwIfExists,
);
}
/** Remove promise designed to track a connection attempt in progress */ /** Remove promise designed to track a connection attempt in progress */
removeConnectingInProgressPromise(wsKey: WsKey): void { removeConnectingInProgressPromise(wsKey: WsKey): void {
return this.removeDeferredPromise( return this.removeDeferredPromise(
@@ -290,6 +311,13 @@ export class WsStore<
); );
} }
removeAuthenticationInProgressPromise(wsKey: WsKey): void {
return this.removeDeferredPromise(
wsKey,
DEFERRED_PROMISE_REF.AUTHENTICATION_IN_PROGRESS,
);
}
/* connection state */ /* connection state */
isWsOpen(key: WsKey): boolean { isWsOpen(key: WsKey): boolean {

View File

@@ -1,7 +1,141 @@
import WebSocket from 'isomorphic-ws'; import WebSocket from 'isomorphic-ws';
import {
APIMarket,
CategoryV5,
WebsocketClientOptions,
WsKey,
} from '../../types';
import { APIMarket, CategoryV5, WebsocketClientOptions, WsKey } from '../types'; import { DefaultLogger } from '../logger';
import { DefaultLogger } from './logger'; import { WSAPIRequest } from '../../types/websockets/ws-api';
export const WS_LOGGER_CATEGORY = { category: 'bybit-ws' };
export const WS_KEY_MAP = {
inverse: 'inverse',
linearPrivate: 'linearPrivate',
linearPublic: 'linearPublic',
spotPrivate: 'spotPrivate',
spotPublic: 'spotPublic',
spotV3Private: 'spotV3Private',
spotV3Public: 'spotV3Public',
usdcOptionPrivate: 'usdcOptionPrivate',
usdcOptionPublic: 'usdcOptionPublic',
usdcPerpPrivate: 'usdcPerpPrivate',
usdcPerpPublic: 'usdcPerpPublic',
unifiedPrivate: 'unifiedPrivate',
unifiedOptionPublic: 'unifiedOptionPublic',
unifiedPerpUSDTPublic: 'unifiedPerpUSDTPublic',
unifiedPerpUSDCPublic: 'unifiedPerpUSDCPublic',
contractUSDTPublic: 'contractUSDTPublic',
contractUSDTPrivate: 'contractUSDTPrivate',
contractInversePublic: 'contractInversePublic',
contractInversePrivate: 'contractInversePrivate',
v5SpotPublic: 'v5SpotPublic',
v5LinearPublic: 'v5LinearPublic',
v5InversePublic: 'v5InversePublic',
v5OptionPublic: 'v5OptionPublic',
v5Private: 'v5Private',
/**
* The V5 Websocket API (for sending orders over WS)
*/
v5PrivateTrade: 'v5PrivateTrade',
} as const;
export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [
WS_KEY_MAP.spotV3Private,
WS_KEY_MAP.usdcOptionPrivate,
WS_KEY_MAP.usdcPerpPrivate,
WS_KEY_MAP.unifiedPrivate,
WS_KEY_MAP.contractUSDTPrivate,
WS_KEY_MAP.contractInversePrivate,
WS_KEY_MAP.v5Private,
WS_KEY_MAP.v5PrivateTrade,
];
export const PUBLIC_WS_KEYS = [
WS_KEY_MAP.linearPublic,
WS_KEY_MAP.spotPublic,
WS_KEY_MAP.spotV3Public,
WS_KEY_MAP.usdcOptionPublic,
WS_KEY_MAP.usdcPerpPublic,
WS_KEY_MAP.unifiedOptionPublic,
WS_KEY_MAP.unifiedPerpUSDTPublic,
WS_KEY_MAP.unifiedPerpUSDCPublic,
WS_KEY_MAP.contractUSDTPublic,
WS_KEY_MAP.contractInversePublic,
WS_KEY_MAP.v5SpotPublic,
WS_KEY_MAP.v5LinearPublic,
WS_KEY_MAP.v5InversePublic,
WS_KEY_MAP.v5OptionPublic,
] as string[];
/** Used to automatically determine if a sub request should be to the public or private ws (when there's two) */
const PRIVATE_TOPICS = [
'stop_order',
'outboundAccountInfo',
'executionReport',
'ticketInfo',
// copy trading apis
'copyTradePosition',
'copyTradeOrder',
'copyTradeExecution',
'copyTradeWallet',
// usdc options
'user.openapi.option.position',
'user.openapi.option.trade',
'user.order',
'user.openapi.option.order',
'user.service',
'user.openapi.greeks',
'user.mmp.event',
// usdc perps
'user.openapi.perp.position',
'user.openapi.perp.trade',
'user.openapi.perp.order',
'user.service',
// unified margin
'user.position.unifiedAccount',
'user.execution.unifiedAccount',
'user.order.unifiedAccount',
'user.wallet.unifiedAccount',
'user.greeks.unifiedAccount',
// contract v3
'user.position.contractAccount',
'user.execution.contractAccount',
'user.order.contractAccount',
'user.wallet.contractAccount',
// v5
'position',
'execution',
'order',
'wallet',
'greeks',
];
/**
* Normalised internal format for a request (subscribe/unsubscribe/etc) on a topic, with optional parameters.
*
* - Topic: the topic this event is for
* - Payload: the parameters to include, optional. E.g. auth requires key + sign. Some topics allow configurable parameters.
* - Category: required for bybit, since different categories have different public endpoints
*/
export interface WsTopicRequest<
TWSTopic extends string = string,
TWSPayload = unknown,
> {
topic: TWSTopic;
payload?: TWSPayload;
category?: CategoryV5;
}
/**
* Conveniently allow users to request a topic either as string topics or objects (containing string topic + params)
*/
export type WsTopicRequestOrStringTopic<
TWSTopic extends string,
TWSPayload = unknown,
> = WsTopicRequest<TWSTopic, TWSPayload> | string;
interface NetworkMapV3 { interface NetworkMapV3 {
livenet: string; livenet: string;
@@ -33,7 +167,55 @@ export const WS_BASE_URL_MAP: Record<
APIMarket, APIMarket,
Record<PublicPrivateNetwork, NetworkMapV3> Record<PublicPrivateNetwork, NetworkMapV3>
> & > &
Record<PublicOnlyWsKeys, Record<'public', NetworkMapV3>> = { Record<PublicOnlyWsKeys, Record<'public', NetworkMapV3>> &
Record<
typeof WS_KEY_MAP.v5PrivateTrade,
Record<PublicPrivateNetwork, NetworkMapV3>
> = {
v5: {
public: {
livenet: 'public topics are routed internally via the public wskeys',
testnet: 'public topics are routed internally via the public wskeys',
},
private: {
livenet: 'wss://stream.bybit.com/v5/private',
testnet: 'wss://stream-testnet.bybit.com/v5/private',
},
},
v5PrivateTrade: {
public: {
livenet: 'public topics are routed internally via the public wskeys',
testnet: 'public topics are routed internally via the public wskeys',
},
private: {
livenet: 'wss://stream.bybit.com/v5/trade',
testnet: 'wss://stream-testnet.bybit.com/v5/trade',
},
},
v5SpotPublic: {
public: {
livenet: 'wss://stream.bybit.com/v5/public/spot',
testnet: 'wss://stream-testnet.bybit.com/v5/public/spot',
},
},
v5LinearPublic: {
public: {
livenet: 'wss://stream.bybit.com/v5/public/linear',
testnet: 'wss://stream-testnet.bybit.com/v5/public/linear',
},
},
v5InversePublic: {
public: {
livenet: 'wss://stream.bybit.com/v5/public/inverse',
testnet: 'wss://stream-testnet.bybit.com/v5/public/inverse',
},
},
v5OptionPublic: {
public: {
livenet: 'wss://stream.bybit.com/v5/public/option',
testnet: 'wss://stream-testnet.bybit.com/v5/public/option',
},
},
inverse: { inverse: {
public: { public: {
livenet: 'wss://stream.bybit.com/realtime', livenet: 'wss://stream.bybit.com/realtime',
@@ -154,139 +336,8 @@ export const WS_BASE_URL_MAP: Record<
testnet: 'wss://stream-testnet.bybit.com/contract/private/v3', testnet: 'wss://stream-testnet.bybit.com/contract/private/v3',
}, },
}, },
v5: {
public: {
livenet: 'public topics are routed internally via the public wskeys',
testnet: 'public topics are routed internally via the public wskeys',
},
private: {
livenet: 'wss://stream.bybit.com/v5/private',
testnet: 'wss://stream-testnet.bybit.com/v5/private',
},
},
v5SpotPublic: {
public: {
livenet: 'wss://stream.bybit.com/v5/public/spot',
testnet: 'wss://stream-testnet.bybit.com/v5/public/spot',
},
},
v5LinearPublic: {
public: {
livenet: 'wss://stream.bybit.com/v5/public/linear',
testnet: 'wss://stream-testnet.bybit.com/v5/public/linear',
},
},
v5InversePublic: {
public: {
livenet: 'wss://stream.bybit.com/v5/public/inverse',
testnet: 'wss://stream-testnet.bybit.com/v5/public/inverse',
},
},
v5OptionPublic: {
public: {
livenet: 'wss://stream.bybit.com/v5/public/option',
testnet: 'wss://stream-testnet.bybit.com/v5/public/option',
},
},
}; };
export const WS_KEY_MAP = {
inverse: 'inverse',
linearPrivate: 'linearPrivate',
linearPublic: 'linearPublic',
spotPrivate: 'spotPrivate',
spotPublic: 'spotPublic',
spotV3Private: 'spotV3Private',
spotV3Public: 'spotV3Public',
usdcOptionPrivate: 'usdcOptionPrivate',
usdcOptionPublic: 'usdcOptionPublic',
usdcPerpPrivate: 'usdcPerpPrivate',
usdcPerpPublic: 'usdcPerpPublic',
unifiedPrivate: 'unifiedPrivate',
unifiedOptionPublic: 'unifiedOptionPublic',
unifiedPerpUSDTPublic: 'unifiedPerpUSDTPublic',
unifiedPerpUSDCPublic: 'unifiedPerpUSDCPublic',
contractUSDTPublic: 'contractUSDTPublic',
contractUSDTPrivate: 'contractUSDTPrivate',
contractInversePublic: 'contractInversePublic',
contractInversePrivate: 'contractInversePrivate',
v5SpotPublic: 'v5SpotPublic',
v5LinearPublic: 'v5LinearPublic',
v5InversePublic: 'v5InversePublic',
v5OptionPublic: 'v5OptionPublic',
v5Private: 'v5Private',
} as const;
export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [
WS_KEY_MAP.spotV3Private,
WS_KEY_MAP.usdcOptionPrivate,
WS_KEY_MAP.usdcPerpPrivate,
WS_KEY_MAP.unifiedPrivate,
WS_KEY_MAP.contractUSDTPrivate,
WS_KEY_MAP.contractInversePrivate,
WS_KEY_MAP.v5Private,
];
export const PUBLIC_WS_KEYS = [
WS_KEY_MAP.linearPublic,
WS_KEY_MAP.spotPublic,
WS_KEY_MAP.spotV3Public,
WS_KEY_MAP.usdcOptionPublic,
WS_KEY_MAP.usdcPerpPublic,
WS_KEY_MAP.unifiedOptionPublic,
WS_KEY_MAP.unifiedPerpUSDTPublic,
WS_KEY_MAP.unifiedPerpUSDCPublic,
WS_KEY_MAP.contractUSDTPublic,
WS_KEY_MAP.contractInversePublic,
WS_KEY_MAP.v5SpotPublic,
WS_KEY_MAP.v5LinearPublic,
WS_KEY_MAP.v5InversePublic,
WS_KEY_MAP.v5OptionPublic,
] as string[];
/** Used to automatically determine if a sub request should be to the public or private ws (when there's two) */
const PRIVATE_TOPICS = [
'stop_order',
'outboundAccountInfo',
'executionReport',
'ticketInfo',
// copy trading apis
'copyTradePosition',
'copyTradeOrder',
'copyTradeExecution',
'copyTradeWallet',
// usdc options
'user.openapi.option.position',
'user.openapi.option.trade',
'user.order',
'user.openapi.option.order',
'user.service',
'user.openapi.greeks',
'user.mmp.event',
// usdc perps
'user.openapi.perp.position',
'user.openapi.perp.trade',
'user.openapi.perp.order',
'user.service',
// unified margin
'user.position.unifiedAccount',
'user.execution.unifiedAccount',
'user.order.unifiedAccount',
'user.wallet.unifiedAccount',
'user.greeks.unifiedAccount',
// contract v3
'user.position.contractAccount',
'user.execution.contractAccount',
'user.order.contractAccount',
'user.wallet.contractAccount',
// v5
'position',
'execution',
'order',
'wallet',
'greeks',
];
export function isPrivateWsTopic(topic: string): boolean { export function isPrivateWsTopic(topic: string): boolean {
return PRIVATE_TOPICS.includes(topic); return PRIVATE_TOPICS.includes(topic);
} }
@@ -416,6 +467,24 @@ export function getWsUrl(
const networkKey = isTestnet ? 'testnet' : 'livenet'; const networkKey = isTestnet ? 'testnet' : 'livenet';
switch (wsKey) { switch (wsKey) {
case WS_KEY_MAP.v5Private: {
return WS_BASE_URL_MAP.v5.private[networkKey];
}
case WS_KEY_MAP.v5PrivateTrade: {
return WS_BASE_URL_MAP[wsKey].private[networkKey];
}
case WS_KEY_MAP.v5SpotPublic: {
return WS_BASE_URL_MAP.v5SpotPublic.public[networkKey];
}
case WS_KEY_MAP.v5LinearPublic: {
return WS_BASE_URL_MAP.v5LinearPublic.public[networkKey];
}
case WS_KEY_MAP.v5InversePublic: {
return WS_BASE_URL_MAP.v5InversePublic.public[networkKey];
}
case WS_KEY_MAP.v5OptionPublic: {
return WS_BASE_URL_MAP.v5OptionPublic.public[networkKey];
}
case WS_KEY_MAP.linearPublic: { case WS_KEY_MAP.linearPublic: {
return WS_BASE_URL_MAP.linear.public[networkKey]; return WS_BASE_URL_MAP.linear.public[networkKey];
} }
@@ -474,21 +543,6 @@ export function getWsUrl(
case WS_KEY_MAP.contractUSDTPublic: { case WS_KEY_MAP.contractUSDTPublic: {
return WS_BASE_URL_MAP.contractUSDT.public[networkKey]; return WS_BASE_URL_MAP.contractUSDT.public[networkKey];
} }
case WS_KEY_MAP.v5Private: {
return WS_BASE_URL_MAP.v5.private[networkKey];
}
case WS_KEY_MAP.v5SpotPublic: {
return WS_BASE_URL_MAP.v5SpotPublic.public[networkKey];
}
case WS_KEY_MAP.v5LinearPublic: {
return WS_BASE_URL_MAP.v5LinearPublic.public[networkKey];
}
case WS_KEY_MAP.v5InversePublic: {
return WS_BASE_URL_MAP.v5InversePublic.public[networkKey];
}
case WS_KEY_MAP.v5OptionPublic: {
return WS_BASE_URL_MAP.v5OptionPublic.public[networkKey];
}
default: { default: {
logger.error('getWsUrl(): Unhandled wsKey: ', { logger.error('getWsUrl(): Unhandled wsKey: ', {
category: 'bybit-ws', category: 'bybit-ws',
@@ -569,3 +623,13 @@ export function safeTerminateWs(ws?: WebSocket | unknown) {
ws.terminate(); ws.terminate();
} }
} }
/**
* WS API promises are stored using a primary key. This key is constructed using
* properties found in every request & reply.
*/
export function getPromiseRefForWSAPIRequest(
requestEvent: WSAPIRequest<unknown>,
): string {
const promiseRef = [requestEvent.op, requestEvent.reqId].join('_');
return promiseRef;
}

File diff suppressed because it is too large Load Diff