spot v3 ws support with tests
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { RestClientOptions, WS_KEY_MAP } from '../util';
|
import { RestClientOptions, WS_KEY_MAP } from '../util';
|
||||||
|
|
||||||
export type APIMarket = 'inverse' | 'linear' | 'spot' | 'spotV3'; //| 'v3';
|
/** For spot markets, spotV3 is recommended */
|
||||||
|
export type APIMarket = 'inverse' | 'linear' | 'spot' | 'spotv3'; //| 'v3';
|
||||||
|
|
||||||
// Same as inverse futures
|
// Same as inverse futures
|
||||||
export type WsPublicInverseTopic =
|
export type WsPublicInverseTopic =
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const WS_BASE_URL_MAP: Record<
|
|||||||
testnet: 'wss://stream-testnet.bybit.com/spot/ws',
|
testnet: 'wss://stream-testnet.bybit.com/spot/ws',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spotV3: {
|
spotv3: {
|
||||||
public: {
|
public: {
|
||||||
livenet: 'wss://stream.bybit.com/spot/public/v3',
|
livenet: 'wss://stream.bybit.com/spot/public/v3',
|
||||||
testnet: 'wss://stream-testnet.bybit.com/spot/public/v3',
|
testnet: 'wss://stream-testnet.bybit.com/spot/public/v3',
|
||||||
@@ -69,6 +69,8 @@ export const WS_KEY_MAP = {
|
|||||||
spotV3Public: 'spotV3Public',
|
spotV3Public: 'spotV3Public',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [WS_KEY_MAP.spotV3Private];
|
||||||
|
|
||||||
export const PUBLIC_WS_KEYS = [
|
export const PUBLIC_WS_KEYS = [
|
||||||
WS_KEY_MAP.linearPublic,
|
WS_KEY_MAP.linearPublic,
|
||||||
WS_KEY_MAP.spotPublic,
|
WS_KEY_MAP.spotPublic,
|
||||||
@@ -115,3 +117,8 @@ export function getSpotWsKeyForTopic(
|
|||||||
}
|
}
|
||||||
return WS_KEY_MAP.spotPublic;
|
return WS_KEY_MAP.spotPublic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const WS_ERROR_ENUM = {
|
||||||
|
NOT_AUTHENTICATED_SPOT_V3: '-1004',
|
||||||
|
BAD_API_KEY_SPOT_V3: '10003',
|
||||||
|
};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
getSpotWsKeyForTopic,
|
getSpotWsKeyForTopic,
|
||||||
WsConnectionStateEnum,
|
WsConnectionStateEnum,
|
||||||
PUBLIC_WS_KEYS,
|
PUBLIC_WS_KEYS,
|
||||||
|
WS_AUTH_ON_CONNECT_KEYS,
|
||||||
WS_KEY_MAP,
|
WS_KEY_MAP,
|
||||||
DefaultLogger,
|
DefaultLogger,
|
||||||
WS_BASE_URL_MAP,
|
WS_BASE_URL_MAP,
|
||||||
@@ -136,7 +137,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
this.connectPublic();
|
this.connectPublic();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'spotV3': {
|
case 'spotv3': {
|
||||||
this.restClient = new SpotClientV3(
|
this.restClient = new SpotClientV3(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -221,7 +222,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
this.connect(WS_KEY_MAP.spotPrivate),
|
this.connect(WS_KEY_MAP.spotPrivate),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
case 'spotV3': {
|
case 'spotv3': {
|
||||||
return [
|
return [
|
||||||
this.connect(WS_KEY_MAP.spotV3Public),
|
this.connect(WS_KEY_MAP.spotV3Public),
|
||||||
this.connect(WS_KEY_MAP.spotV3Private),
|
this.connect(WS_KEY_MAP.spotV3Private),
|
||||||
@@ -244,7 +245,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
case 'spot': {
|
case 'spot': {
|
||||||
return this.connect(WS_KEY_MAP.spotPublic);
|
return this.connect(WS_KEY_MAP.spotPublic);
|
||||||
}
|
}
|
||||||
case 'spotV3': {
|
case 'spotv3': {
|
||||||
return this.connect(WS_KEY_MAP.spotV3Public);
|
return this.connect(WS_KEY_MAP.spotV3Public);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -267,7 +268,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
case 'spot': {
|
case 'spot': {
|
||||||
return this.connect(WS_KEY_MAP.spotPrivate);
|
return this.connect(WS_KEY_MAP.spotPrivate);
|
||||||
}
|
}
|
||||||
case 'spotV3': {
|
case 'spotv3': {
|
||||||
return this.connect(WS_KEY_MAP.spotV3Private);
|
return this.connect(WS_KEY_MAP.spotV3Private);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -354,12 +355,49 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { signature, expiresAt } = await this.getWsAuthSignature(wsKey);
|
||||||
|
|
||||||
|
const authParams = {
|
||||||
|
api_key: this.options.key,
|
||||||
|
expires: expiresAt,
|
||||||
|
signature,
|
||||||
|
};
|
||||||
|
|
||||||
|
return '?' + serializeParams(authParams);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e, { ...loggerCategory, wsKey });
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendAuthRequest(wsKey: WsKey): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { signature, expiresAt } = await this.getWsAuthSignature(wsKey);
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
op: 'auth',
|
||||||
|
args: [this.options.key, expiresAt, signature],
|
||||||
|
req_id: `${wsKey}-auth`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.tryWsSend(wsKey, JSON.stringify(request));
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e, { ...loggerCategory, wsKey });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getWsAuthSignature(
|
||||||
|
wsKey: WsKey
|
||||||
|
): Promise<{ expiresAt: number; signature: string }> {
|
||||||
|
const { key, secret } = this.options;
|
||||||
|
|
||||||
if (!key || !secret) {
|
if (!key || !secret) {
|
||||||
this.logger.warning(
|
this.logger.warning(
|
||||||
'Cannot authenticate websocket, either api or private keys missing.',
|
'Cannot authenticate websocket, either api or private keys missing.',
|
||||||
{ ...loggerCategory, wsKey }
|
{ ...loggerCategory, wsKey }
|
||||||
);
|
);
|
||||||
return '';
|
throw new Error(`Cannot auth - missing api or secret in config`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug("Getting auth'd request params", {
|
this.logger.debug("Getting auth'd request params", {
|
||||||
@@ -378,13 +416,10 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
secret
|
secret
|
||||||
);
|
);
|
||||||
|
|
||||||
const authParams = {
|
return {
|
||||||
api_key: this.options.key,
|
expiresAt: signatureExpiresAt,
|
||||||
expires: signatureExpiresAt,
|
|
||||||
signature,
|
signature,
|
||||||
};
|
};
|
||||||
|
|
||||||
return '?' + serializeParams(authParams);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) {
|
private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) {
|
||||||
@@ -451,6 +486,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wsMessage = JSON.stringify({
|
const wsMessage = JSON.stringify({
|
||||||
|
req_id: topics.join(','),
|
||||||
op: 'subscribe',
|
op: 'subscribe',
|
||||||
args: topics,
|
args: topics,
|
||||||
});
|
});
|
||||||
@@ -518,7 +554,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
return ws;
|
return ws;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWsOpen(event, wsKey: WsKey) {
|
private async onWsOpen(event, wsKey: WsKey) {
|
||||||
if (
|
if (
|
||||||
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING)
|
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING)
|
||||||
) {
|
) {
|
||||||
@@ -538,8 +574,14 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
|
|
||||||
this.setWsState(wsKey, WsConnectionStateEnum.CONNECTED);
|
this.setWsState(wsKey, WsConnectionStateEnum.CONNECTED);
|
||||||
|
|
||||||
// TODO: persistence not working yet for spot topics
|
// Some websockets require an auth packet to be sent after opening the connection
|
||||||
if (wsKey !== 'spotPublic' && wsKey !== 'spotPrivate') {
|
if (WS_AUTH_ON_CONNECT_KEYS.includes(wsKey)) {
|
||||||
|
this.logger.info(`Sending auth request...`);
|
||||||
|
await this.sendAuthRequest(wsKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: persistence not working yet for spot v1 topics
|
||||||
|
if (wsKey !== WS_KEY_MAP.spotPublic && wsKey !== WS_KEY_MAP.spotPrivate) {
|
||||||
this.requestSubscribeTopics(wsKey, [...this.wsStore.getTopics(wsKey)]);
|
this.requestSubscribeTopics(wsKey, [...this.wsStore.getTopics(wsKey)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,6 +596,8 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
// any message can clear the pong timer - wouldn't get a message if the ws dropped
|
// any message can clear the pong timer - wouldn't get a message if the ws dropped
|
||||||
this.clearPongTimer(wsKey);
|
this.clearPongTimer(wsKey);
|
||||||
|
|
||||||
|
// this.logger.silly('Received event', { ...this.logger, wsKey, event });
|
||||||
|
|
||||||
const msg = JSON.parse((event && event.data) || event);
|
const msg = JSON.parse((event && event.data) || event);
|
||||||
if (msg['success'] || msg?.pong) {
|
if (msg['success'] || msg?.pong) {
|
||||||
if (isWsPong(msg)) {
|
if (isWsPong(msg)) {
|
||||||
@@ -564,11 +608,20 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.topic) {
|
if (msg?.topic) {
|
||||||
return this.emit('update', msg);
|
return this.emit('update', msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.warning('Got unhandled ws message', {
|
if (
|
||||||
|
// spot v1
|
||||||
|
msg?.code ||
|
||||||
|
// spot v3
|
||||||
|
msg?.type === 'error'
|
||||||
|
) {
|
||||||
|
return this.emit('error', msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warning('Unhandled/unrecognised ws event message', {
|
||||||
...loggerCategory,
|
...loggerCategory,
|
||||||
message: msg,
|
message: msg,
|
||||||
event,
|
event,
|
||||||
@@ -639,10 +692,10 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
return WS_BASE_URL_MAP.spot.private[networkKey];
|
return WS_BASE_URL_MAP.spot.private[networkKey];
|
||||||
}
|
}
|
||||||
case WS_KEY_MAP.spotV3Public: {
|
case WS_KEY_MAP.spotV3Public: {
|
||||||
return WS_BASE_URL_MAP.spot.public[networkKey];
|
return WS_BASE_URL_MAP.spotv3.public[networkKey];
|
||||||
}
|
}
|
||||||
case WS_KEY_MAP.spotV3Private: {
|
case WS_KEY_MAP.spotV3Private: {
|
||||||
return WS_BASE_URL_MAP.spot.private[networkKey];
|
return WS_BASE_URL_MAP.spotv3.private[networkKey];
|
||||||
}
|
}
|
||||||
case WS_KEY_MAP.inverse: {
|
case WS_KEY_MAP.inverse: {
|
||||||
// private and public are on the same WS connection
|
// private and public are on the same WS connection
|
||||||
@@ -669,7 +722,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
case 'spot': {
|
case 'spot': {
|
||||||
return getSpotWsKeyForTopic(topic, 'v1');
|
return getSpotWsKeyForTopic(topic, 'v1');
|
||||||
}
|
}
|
||||||
case 'spotV3': {
|
case 'spotv3': {
|
||||||
return getSpotWsKeyForTopic(topic, 'v3');
|
return getSpotWsKeyForTopic(topic, 'v3');
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -740,7 +793,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: persistance for subbed topics. Look at ftx-api implementation.
|
/** @deprecated use "market: 'spotv3" client */
|
||||||
public subscribePublicSpotTrades(symbol: string, binary?: boolean) {
|
public subscribePublicSpotTrades(symbol: string, binary?: boolean) {
|
||||||
if (!this.isSpot()) {
|
if (!this.isSpot()) {
|
||||||
throw this.wrongMarketError('spot');
|
throw this.wrongMarketError('spot');
|
||||||
@@ -759,6 +812,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated use "market: 'spotv3" client */
|
||||||
public subscribePublicSpotTradingPair(symbol: string, binary?: boolean) {
|
public subscribePublicSpotTradingPair(symbol: string, binary?: boolean) {
|
||||||
if (!this.isSpot()) {
|
if (!this.isSpot()) {
|
||||||
throw this.wrongMarketError('spot');
|
throw this.wrongMarketError('spot');
|
||||||
@@ -777,6 +831,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated use "market: 'spotv3" client */
|
||||||
public subscribePublicSpotV1Kline(
|
public subscribePublicSpotV1Kline(
|
||||||
symbol: string,
|
symbol: string,
|
||||||
candleSize: KlineInterval,
|
candleSize: KlineInterval,
|
||||||
@@ -803,6 +858,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
//ws.send('{"symbol":"BTCUSDT","topic":"mergedDepth","event":"sub","params":{"binary":false,"dumpScale":1}}');
|
//ws.send('{"symbol":"BTCUSDT","topic":"mergedDepth","event":"sub","params":{"binary":false,"dumpScale":1}}');
|
||||||
//ws.send('{"symbol":"BTCUSDT","topic":"diffDepth","event":"sub","params":{"binary":false}}');
|
//ws.send('{"symbol":"BTCUSDT","topic":"diffDepth","event":"sub","params":{"binary":false}}');
|
||||||
|
|
||||||
|
/** @deprecated use "market: 'spotv3" client */
|
||||||
public subscribePublicSpotOrderbook(
|
public subscribePublicSpotOrderbook(
|
||||||
symbol: string,
|
symbol: string,
|
||||||
depth: 'full' | 'merge' | 'delta',
|
depth: 'full' | 'merge' | 'delta',
|
||||||
|
|||||||
124
test/spot/ws.private.v3.test.ts
Normal file
124
test/spot/ws.private.v3.test.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import {
|
||||||
|
WebsocketClient,
|
||||||
|
WSClientConfigurableOptions,
|
||||||
|
WS_ERROR_ENUM,
|
||||||
|
WS_KEY_MAP,
|
||||||
|
} from '../../src';
|
||||||
|
import {
|
||||||
|
silentLogger,
|
||||||
|
waitForSocketEvent,
|
||||||
|
WS_OPEN_EVENT_PARTIAL,
|
||||||
|
} from '../ws.util';
|
||||||
|
|
||||||
|
describe('Private Spot V3 Websocket Client', () => {
|
||||||
|
const API_KEY = process.env.API_KEY_COM;
|
||||||
|
const API_SECRET = process.env.API_SECRET_COM;
|
||||||
|
|
||||||
|
const wsClientOptions: WSClientConfigurableOptions = {
|
||||||
|
market: 'spotv3',
|
||||||
|
key: API_KEY,
|
||||||
|
secret: API_SECRET,
|
||||||
|
};
|
||||||
|
const wsTopic = `outboundAccountInfo`;
|
||||||
|
|
||||||
|
describe('with invalid credentials', () => {
|
||||||
|
it('should reject private subscribe if keys/signature are incorrect', async () => {
|
||||||
|
const badClient = new WebsocketClient(
|
||||||
|
{
|
||||||
|
...wsClientOptions,
|
||||||
|
key: 'bad',
|
||||||
|
secret: 'bad',
|
||||||
|
},
|
||||||
|
silentLogger
|
||||||
|
);
|
||||||
|
|
||||||
|
// const wsOpenPromise = waitForSocketEvent(badClient, 'open');
|
||||||
|
const wsResponsePromise = waitForSocketEvent(badClient, 'response');
|
||||||
|
// const wsUpdatePromise = waitForSocketEvent(wsClient, 'update');
|
||||||
|
|
||||||
|
badClient.connectPrivate();
|
||||||
|
badClient.subscribe(wsTopic);
|
||||||
|
|
||||||
|
expect(wsResponsePromise).rejects.toMatchObject({
|
||||||
|
ret_code: WS_ERROR_ENUM.BAD_API_KEY_SPOT_V3,
|
||||||
|
ret_msg: expect.any(String),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([wsResponsePromise]);
|
||||||
|
} catch (e) {
|
||||||
|
// console.error()
|
||||||
|
}
|
||||||
|
badClient.closeAll();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with valid API credentails', () => {
|
||||||
|
let wsClient: WebsocketClient;
|
||||||
|
|
||||||
|
it('should have api credentials to test with', () => {
|
||||||
|
expect(API_KEY).toStrictEqual(expect.any(String));
|
||||||
|
expect(API_SECRET).toStrictEqual(expect.any(String));
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
wsClient = new WebsocketClient(wsClientOptions, silentLogger);
|
||||||
|
wsClient.connectPrivate();
|
||||||
|
// logAllEvents(wsClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
wsClient.closeAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open a private ws connection', async () => {
|
||||||
|
const wsOpenPromise = waitForSocketEvent(wsClient, 'open');
|
||||||
|
const wsResponsePromise = waitForSocketEvent(wsClient, 'response');
|
||||||
|
|
||||||
|
expect(wsOpenPromise).resolves.toMatchObject({
|
||||||
|
event: WS_OPEN_EVENT_PARTIAL,
|
||||||
|
wsKey: WS_KEY_MAP.spotV3Private,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([wsOpenPromise]);
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toBeFalsy();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(await wsResponsePromise).toMatchObject({
|
||||||
|
op: 'auth',
|
||||||
|
success: true,
|
||||||
|
req_id: `${WS_KEY_MAP.spotV3Private}-auth`,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Wait for "${wsTopic}" event exception: `, e);
|
||||||
|
expect(e).toBeFalsy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should subscribe to private outboundAccountInfo events', async () => {
|
||||||
|
const wsResponsePromise = waitForSocketEvent(wsClient, 'response');
|
||||||
|
|
||||||
|
// expect(wsUpdatePromise).resolves.toStrictEqual('');
|
||||||
|
wsClient.subscribe(wsTopic);
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(await wsResponsePromise).toMatchObject({
|
||||||
|
op: 'subscribe',
|
||||||
|
success: true,
|
||||||
|
ret_msg: '',
|
||||||
|
req_id: wsTopic,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Wait for "${wsTopic}" subscription response exception: `,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
expect(e).toBeFalsy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
logAllEvents,
|
logAllEvents,
|
||||||
silentLogger,
|
silentLogger,
|
||||||
|
fullLogger,
|
||||||
waitForSocketEvent,
|
waitForSocketEvent,
|
||||||
WS_OPEN_EVENT_PARTIAL,
|
WS_OPEN_EVENT_PARTIAL,
|
||||||
} from '../ws.util';
|
} from '../ws.util';
|
||||||
@@ -20,7 +21,7 @@ describe('Public Spot V1 Websocket Client', () => {
|
|||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
wsClient = new WebsocketClient(wsClientOptions, silentLogger);
|
wsClient = new WebsocketClient(wsClientOptions, silentLogger);
|
||||||
wsClient.connectPublic();
|
wsClient.connectPublic();
|
||||||
// logAllEvents(wsClient);
|
logAllEvents(wsClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
logAllEvents,
|
logAllEvents,
|
||||||
silentLogger,
|
silentLogger,
|
||||||
|
fullLogger,
|
||||||
waitForSocketEvent,
|
waitForSocketEvent,
|
||||||
WS_OPEN_EVENT_PARTIAL,
|
WS_OPEN_EVENT_PARTIAL,
|
||||||
} from '../ws.util';
|
} from '../ws.util';
|
||||||
@@ -14,7 +15,7 @@ describe('Public Spot V3 Websocket Client', () => {
|
|||||||
let wsClient: WebsocketClient;
|
let wsClient: WebsocketClient;
|
||||||
|
|
||||||
const wsClientOptions: WSClientConfigurableOptions = {
|
const wsClientOptions: WSClientConfigurableOptions = {
|
||||||
market: 'spotV3',
|
market: 'spotv3',
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -42,15 +43,27 @@ describe('Public Spot V3 Websocket Client', () => {
|
|||||||
const wsResponsePromise = waitForSocketEvent(wsClient, 'response');
|
const wsResponsePromise = waitForSocketEvent(wsClient, 'response');
|
||||||
const wsUpdatePromise = waitForSocketEvent(wsClient, 'update');
|
const wsUpdatePromise = waitForSocketEvent(wsClient, 'update');
|
||||||
|
|
||||||
const wsTopic = 'orderbook.40.BTCUSDT';
|
const symbol = 'BTCUSDT';
|
||||||
|
const wsTopic = `orderbook.40.${symbol}`;
|
||||||
|
|
||||||
expect(wsResponsePromise).resolves.toMatchObject({
|
expect(wsResponsePromise).resolves.toMatchObject({
|
||||||
request: {
|
op: 'subscribe',
|
||||||
args: [wsTopic],
|
|
||||||
op: 'subscribe',
|
|
||||||
},
|
|
||||||
success: true,
|
success: true,
|
||||||
|
ret_msg: 'subscribe',
|
||||||
|
req_id: wsTopic,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wsUpdatePromise).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
a: expect.any(Array),
|
||||||
|
b: expect.any(Array),
|
||||||
|
s: symbol,
|
||||||
|
t: expect.any(Number),
|
||||||
|
},
|
||||||
|
topic: wsTopic,
|
||||||
|
ts: expect.any(Number),
|
||||||
|
type: 'delta',
|
||||||
});
|
});
|
||||||
expect(wsUpdatePromise).resolves.toStrictEqual('');
|
|
||||||
|
|
||||||
wsClient.subscribe(wsTopic);
|
wsClient.subscribe(wsTopic);
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,17 @@ export const silentLogger = {
|
|||||||
debug: () => {},
|
debug: () => {},
|
||||||
notice: () => {},
|
notice: () => {},
|
||||||
info: () => {},
|
info: () => {},
|
||||||
warning: () => {},
|
warning: (...params) => console.warn('warning', ...params),
|
||||||
error: () => {},
|
error: (...params) => console.error('error', ...params),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fullLogger = {
|
||||||
|
silly: (...params) => console.log('silly', ...params),
|
||||||
|
debug: (...params) => console.log('debug', ...params),
|
||||||
|
notice: (...params) => console.log('notice', ...params),
|
||||||
|
info: (...params) => console.info('info', ...params),
|
||||||
|
warning: (...params) => console.warn('warning', ...params),
|
||||||
|
error: (...params) => console.error('error', ...params),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WS_OPEN_EVENT_PARTIAL = {
|
export const WS_OPEN_EVENT_PARTIAL = {
|
||||||
@@ -26,20 +35,29 @@ export function waitForSocketEvent(
|
|||||||
);
|
);
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolvedOnce = true;
|
||||||
|
wsClient.removeListener(event, (e) => resolver(e));
|
||||||
|
wsClient.removeListener('error', (e) => rejector(e));
|
||||||
|
}
|
||||||
|
|
||||||
let resolvedOnce = false;
|
let resolvedOnce = false;
|
||||||
|
|
||||||
wsClient.on(event, (event) => {
|
function resolver(event) {
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve(event);
|
resolve(event);
|
||||||
resolvedOnce = true;
|
cleanup();
|
||||||
});
|
}
|
||||||
|
|
||||||
wsClient.on('error', (event) => {
|
function rejector(event) {
|
||||||
clearTimeout(timeout);
|
|
||||||
if (!resolvedOnce) {
|
if (!resolvedOnce) {
|
||||||
reject(event);
|
reject(event);
|
||||||
}
|
}
|
||||||
});
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
wsClient.on(event, (e) => resolver(e));
|
||||||
|
wsClient.on('error', (e) => rejector(e));
|
||||||
|
|
||||||
// if (event !== 'close') {
|
// if (event !== 'close') {
|
||||||
// wsClient.on('close', (event) => {
|
// wsClient.on('close', (event) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user