From d16dee8caa0d42763e2a8e7da9b4e12175acc49e Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 14:09:01 +0100 Subject: [PATCH] usdc private test --- src/util/WsStore.ts | 2 + src/util/websocket-util.ts | 10 +- src/websocket-client.ts | 52 ++++------ test/spot/ws.private.v3.test.ts | 2 +- test/usdc/options/ws.private.test.ts | 150 +++++++++++++++++++++++++++ test/ws.util.ts | 6 +- 6 files changed, 183 insertions(+), 39 deletions(-) create mode 100644 test/usdc/options/ws.private.test.ts diff --git a/src/util/WsStore.ts b/src/util/WsStore.ts index 9539079..9d68743 100644 --- a/src/util/WsStore.ts +++ b/src/util/WsStore.ts @@ -29,6 +29,8 @@ interface WsStoredState { activePingTimer?: ReturnType | undefined; /** A timer tracking that an upstream heartbeat was sent, expecting a reply before it expires */ activePongTimer?: ReturnType | undefined; + /** If a reconnection is in progress, this will have the timer for the delayed reconnect */ + activeReconnectTimer?: ReturnType | undefined; /** * All the topics we are expected to be subscribed to (and we automatically resubscribe to if the connection drops) */ diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index 7507529..dbc743e 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -97,7 +97,11 @@ export const WS_KEY_MAP = { usdcPerpPublic: 'usdcPerpPublic', } as const; -export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [WS_KEY_MAP.spotV3Private]; +export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [ + WS_KEY_MAP.spotV3Private, + WS_KEY_MAP.usdcOptionPrivate, + WS_KEY_MAP.usdcPerpPrivate, +]; export const PUBLIC_WS_KEYS = [ WS_KEY_MAP.linearPublic, @@ -202,7 +206,9 @@ export function getUsdcWsKeyForTopic( export const WS_ERROR_ENUM = { NOT_AUTHENTICATED_SPOT_V3: '-1004', - BAD_API_KEY_SPOT_V3: '10003', + API_ERROR_GENERIC: '10001', + API_SIGN_AUTH_FAILED: '10003', + USDC_OPTION_AUTH_FAILED: '3303006', }; export function neverGuard(x: never, msg: string): Error { diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 18898b9..1dc88c7 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -40,7 +40,7 @@ export type WsClientEvent = | 'open' | 'update' | 'close' - | 'error' + | 'errorEvent' | 'reconnect' | 'reconnected' | 'response'; @@ -52,7 +52,7 @@ interface WebsocketClientEvents { close: (evt: { wsKey: WsKey; event: any }) => void; response: (response: any) => void; update: (response: any) => void; - error: (response: any) => void; + errorEvent: (response: any) => void; } // Type safety for on and emit handlers: https://stackoverflow.com/a/61609010/880837 @@ -141,7 +141,6 @@ export class WebsocketClient extends EventEmitter { this.options.restOptions, this.options.requestOptions ); - this.connectPublic(); break; } case 'usdcOption': { @@ -152,7 +151,6 @@ export class WebsocketClient extends EventEmitter { this.options.restOptions, this.options.requestOptions ); - this.connectPublic(); break; } case 'usdcPerp': { @@ -163,7 +161,6 @@ export class WebsocketClient extends EventEmitter { this.options.restOptions, this.options.requestOptions ); - this.connectPublic(); break; } default: { @@ -179,23 +176,6 @@ export class WebsocketClient extends EventEmitter { return this.options.testnet === true; } - public isLinear(): boolean { - return this.options.market === 'linear'; - } - - public isSpot(): boolean { - return this.options.market === 'spot'; - } - - public isInverse(): boolean { - return this.options.market === 'inverse'; - } - - /** USDC, spot v3, unified margin, account asset */ - // public isV3(): boolean { - // return this.options.market === 'v3'; - // } - public close(wsKey: WsKey) { this.logger.info('Closing connection', { ...loggerCategory, wsKey }); this.setWsState(wsKey, WsConnectionStateEnum.CLOSING); @@ -333,7 +313,7 @@ export class WebsocketClient extends EventEmitter { private parseWsError(context: string, error: any, wsKey: WsKey) { if (!error.message) { this.logger.error(`${context} due to unexpected error: `, error); - this.emit('error', error); + this.emit('errorEvent', error); return; } @@ -352,7 +332,7 @@ export class WebsocketClient extends EventEmitter { ); break; } - this.emit('error', error); + this.emit('errorEvent', error); } /** @@ -443,7 +423,7 @@ export class WebsocketClient extends EventEmitter { this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING); } - setTimeout(() => { + this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => { this.logger.info('Reconnecting to websocket', { ...loggerCategory, wsKey, @@ -458,7 +438,7 @@ export class WebsocketClient extends EventEmitter { this.logger.silly('Sending ping', { ...loggerCategory, wsKey }); this.tryWsSend(wsKey, JSON.stringify({ op: 'ping' })); - this.wsStore.get(wsKey, true)!.activePongTimer = setTimeout(() => { + this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => { this.logger.info('Pong timeout - closing socket to reconnect', { ...loggerCategory, wsKey, @@ -470,6 +450,10 @@ export class WebsocketClient extends EventEmitter { private clearTimers(wsKey: WsKey) { this.clearPingTimer(wsKey); this.clearPongTimer(wsKey); + const wsState = this.wsStore.get(wsKey); + if (wsState?.activeReconnectTimer) { + clearTimeout(wsState.activeReconnectTimer); + } } // Send a ping at intervals @@ -636,9 +620,11 @@ export class WebsocketClient extends EventEmitter { // spot v1 msg?.code || // spot v3 - msg?.type === 'error' + msg?.type === 'error' || + // usdc options + msg?.success === false ) { - return this.emit('error', msg); + return this.emit('errorEvent', msg); } this.logger.warning('Unhandled/unrecognised ws event message', { @@ -662,7 +648,7 @@ export class WebsocketClient extends EventEmitter { if ( this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) ) { - this.emit('error', error); + this.emit('errorEvent', error); } } @@ -814,7 +800,7 @@ export class WebsocketClient extends EventEmitter { /** @deprecated use "market: 'spotv3" client */ public subscribePublicSpotTrades(symbol: string, binary?: boolean) { - if (!this.isSpot()) { + if (this.options.market !== 'spot') { throw this.wrongMarketError('spot'); } @@ -833,7 +819,7 @@ export class WebsocketClient extends EventEmitter { /** @deprecated use "market: 'spotv3" client */ public subscribePublicSpotTradingPair(symbol: string, binary?: boolean) { - if (!this.isSpot()) { + if (this.options.market !== 'spot') { throw this.wrongMarketError('spot'); } @@ -856,7 +842,7 @@ export class WebsocketClient extends EventEmitter { candleSize: KlineInterval, binary?: boolean ) { - if (!this.isSpot()) { + if (this.options.market !== 'spot') { throw this.wrongMarketError('spot'); } @@ -884,7 +870,7 @@ export class WebsocketClient extends EventEmitter { dumpScale?: number, binary?: boolean ) { - if (!this.isSpot()) { + if (this.options.market !== 'spot') { throw this.wrongMarketError('spot'); } diff --git a/test/spot/ws.private.v3.test.ts b/test/spot/ws.private.v3.test.ts index b430b29..16836fa 100644 --- a/test/spot/ws.private.v3.test.ts +++ b/test/spot/ws.private.v3.test.ts @@ -40,7 +40,7 @@ describe('Private Spot V3 Websocket Client', () => { badClient.subscribe(wsTopic); expect(wsResponsePromise).rejects.toMatchObject({ - ret_code: WS_ERROR_ENUM.BAD_API_KEY_SPOT_V3, + ret_code: WS_ERROR_ENUM.API_SIGN_AUTH_FAILED, ret_msg: expect.any(String), type: 'error', }); diff --git a/test/usdc/options/ws.private.test.ts b/test/usdc/options/ws.private.test.ts new file mode 100644 index 0000000..4f94136 --- /dev/null +++ b/test/usdc/options/ws.private.test.ts @@ -0,0 +1,150 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_ERROR_ENUM, + WS_KEY_MAP, +} from '../../../src'; +import { + fullLogger, + getSilentLogger, + logAllEvents, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../../ws.util'; + +describe('Private USDC Option Websocket Client', () => { + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'usdcOption', + key: API_KEY, + secret: API_SECRET, + }; + + const wsTopic = `user.openapi.option.position`; + + describe('with invalid credentials', () => { + it('should reject private subscribe if keys/signature are incorrect', async () => { + const badClient = new WebsocketClient( + { + ...wsClientOptions, + key: 'bad', + secret: 'bad', + reconnectTimeout: 10000, + }, + // fullLogger + getSilentLogger('expect401') + ); + // logAllEvents(badClient); + + // const wsOpenPromise = waitForSocketEvent(badClient, 'open'); + const wsResponsePromise = waitForSocketEvent(badClient, 'response'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + badClient.connectPrivate(); + + const responsePartial = { + ret_msg: WS_ERROR_ENUM.USDC_OPTION_AUTH_FAILED, + success: false, + type: 'AUTH_RESP', + }; + expect(wsResponsePromise).rejects.toMatchObject(responsePartial); + + try { + await Promise.all([wsResponsePromise]); + } catch (e) { + // console.error() + expect(e).toMatchObject(responsePartial); + } + + // badClient.subscribe(wsTopic); + badClient.removeAllListeners(); + 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, + getSilentLogger('expectSuccess') + ); + 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.usdcOptionPrivate, + }); + + try { + await Promise.all([wsOpenPromise]); + } catch (e) { + expect(e).toBeFalsy(); + } + + try { + expect(await wsResponsePromise).toMatchObject({ + ret_msg: '0', + success: true, + type: 'AUTH_RESP', + }); + } catch (e) { + console.error(`Wait for "${wsTopic}" event exception: `, e); + expect(e).toBeFalsy(); + } + }); + + it(`should subscribe to private "${wsTopic}" events`, async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + // expect(wsUpdatePromise).resolves.toStrictEqual(''); + wsClient.subscribe(wsTopic); + + try { + expect(await wsResponsePromise).toMatchObject({ + data: { + failTopics: [], + successTopics: [wsTopic], + }, + success: true, + type: 'COMMAND_RESP', + }); + } catch (e) { + console.error( + `Wait for "${wsTopic}" subscription response exception: `, + e + ); + expect(e).toBeFalsy(); + } + expect(await wsUpdatePromise).toMatchObject({ + creationTime: expect.any(Number), + data: { + baseLine: expect.any(Number), + dataType: expect.any(String), + result: expect.any(Array), + version: expect.any(Number), + }, + topic: wsTopic, + }); + }); + }); +}); diff --git a/test/ws.util.ts b/test/ws.util.ts index c954169..ae0302c 100644 --- a/test/ws.util.ts +++ b/test/ws.util.ts @@ -59,7 +59,7 @@ export function waitForSocketEvent( } wsClient.on(event, (e) => resolver(e)); - wsClient.on('error', (e) => rejector(e)); + wsClient.on('errorEvent', (e) => rejector(e)); // if (event !== 'close') { // wsClient.on('close', (event) => { @@ -89,7 +89,7 @@ export function listenToSocketEvents(wsClient: WebsocketClient) { wsClient.on('response', retVal.response); wsClient.on('update', retVal.update); wsClient.on('close', retVal.close); - wsClient.on('error', retVal.error); + wsClient.on('errorEvent', retVal.error); return { ...retVal, @@ -98,7 +98,7 @@ export function listenToSocketEvents(wsClient: WebsocketClient) { wsClient.removeListener('response', retVal.response); wsClient.removeListener('update', retVal.update); wsClient.removeListener('close', retVal.close); - wsClient.removeListener('error', retVal.error); + wsClient.removeListener('errorEvent', retVal.error); }, }; }