From 38f5a6286cf80869d2885277091176f4b918cb02 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sun, 8 May 2022 01:00:12 +0100 Subject: [PATCH] fixes for private spot GET calls, improve signing process, add private read tests for spot & linear --- src/inverse-client.ts | 4 +- src/inverse-futures-client.ts | 4 +- src/linear-client.ts | 4 +- src/spot-client.ts | 9 ++- src/util/BaseRestClient.ts | 109 +++++++++++++++++++++++-------- src/util/requestUtils.ts | 12 +++- test/linear/private.read.test.ts | 9 +-- test/response.util.ts | 12 ++++ test/spot/private.read.test.ts | 54 +++++++++++++++ 9 files changed, 176 insertions(+), 41 deletions(-) create mode 100644 test/spot/private.read.test.ts diff --git a/src/inverse-client.ts b/src/inverse-client.ts index ca127cf..f2f706e 100644 --- a/src/inverse-client.ts +++ b/src/inverse-client.ts @@ -3,6 +3,7 @@ import { GenericAPIResponse, getRestBaseUrl, RestClientOptions, + REST_CLIENT_TYPE_ENUM, } from './util/requestUtils'; import RequestWrapper from './util/requestWrapper'; import { @@ -46,7 +47,8 @@ export class InverseClient extends BaseRestClient { secret, getRestBaseUrl(useLivenet, restClientOptions), restClientOptions, - requestOptions + requestOptions, + REST_CLIENT_TYPE_ENUM.inverse ); this.requestWrapper = new RequestWrapper( key, diff --git a/src/inverse-futures-client.ts b/src/inverse-futures-client.ts index d3feb21..e4e1dc7 100644 --- a/src/inverse-futures-client.ts +++ b/src/inverse-futures-client.ts @@ -3,6 +3,7 @@ import { GenericAPIResponse, getRestBaseUrl, RestClientOptions, + REST_CLIENT_TYPE_ENUM, } from './util/requestUtils'; import RequestWrapper from './util/requestWrapper'; import { @@ -44,7 +45,8 @@ export class InverseFuturesClient extends BaseRestClient { secret, getRestBaseUrl(useLivenet, restClientOptions), restClientOptions, - requestOptions + requestOptions, + REST_CLIENT_TYPE_ENUM.inverseFutures ); this.requestWrapper = new RequestWrapper( key, diff --git a/src/linear-client.ts b/src/linear-client.ts index df942c5..e5842fb 100644 --- a/src/linear-client.ts +++ b/src/linear-client.ts @@ -3,6 +3,7 @@ import { GenericAPIResponse, getRestBaseUrl, RestClientOptions, + REST_CLIENT_TYPE_ENUM, } from './util/requestUtils'; import RequestWrapper from './util/requestWrapper'; import { @@ -46,7 +47,8 @@ export class LinearClient extends BaseRestClient { secret, getRestBaseUrl(useLivenet, restClientOptions), restClientOptions, - requestOptions + requestOptions, + REST_CLIENT_TYPE_ENUM.linear ); this.requestWrapper = new RequestWrapper( diff --git a/src/spot-client.ts b/src/spot-client.ts index 52decb8..a4cb849 100644 --- a/src/spot-client.ts +++ b/src/spot-client.ts @@ -8,7 +8,11 @@ import { SpotSymbolInfo, } from './types/spot'; import BaseRestClient from './util/BaseRestClient'; -import { getRestBaseUrl, RestClientOptions } from './util/requestUtils'; +import { + getRestBaseUrl, + RestClientOptions, + REST_CLIENT_TYPE_ENUM, +} from './util/requestUtils'; export class SpotClient extends BaseRestClient { /** @@ -32,7 +36,8 @@ export class SpotClient extends BaseRestClient { secret, getRestBaseUrl(useLivenet, restClientOptions), restClientOptions, - requestOptions + requestOptions, + REST_CLIENT_TYPE_ENUM.spot ); return this; diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index e5f8782..4d7e3c7 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -9,11 +9,26 @@ import { signMessage } from './node-support'; import { RestClientOptions, GenericAPIResponse, - getRestBaseUrl, serializeParams, - isPublicEndpoint, + RestClientType, + REST_CLIENT_TYPE_ENUM, } from './requestUtils'; + +interface SignedRequestContext { + timestamp: number; + api_key?: string; + recv_window?: number; + // spot is diff from the rest... + recvWindow?: number; +} + +interface SignedRequest { + originalParams: T & SignedRequestContext; + paramsWithSign?: T & SignedRequestContext & { sign: string }; + sign: string; +} + export default abstract class BaseRestClient { private timeOffset: number | null; private syncTimePromise: null | Promise; @@ -22,6 +37,7 @@ export default abstract class BaseRestClient { private globalRequestOptions: AxiosRequestConfig; private key: string | undefined; private secret: string | undefined; + private clientType: RestClientType; /** Function that calls exchange API to query & resolve server time, used by time sync */ abstract fetchServerTime(): Promise; @@ -31,11 +47,14 @@ export default abstract class BaseRestClient { secret: string | undefined, baseUrl: string, options: RestClientOptions = {}, - requestOptions: AxiosRequestConfig = {} + requestOptions: AxiosRequestConfig = {}, + clientType: RestClientType ) { this.timeOffset = null; this.syncTimePromise = null; + this.clientType = clientType; + this.options = { recv_window: 5000, // how often to sync time drift with bybit servers @@ -72,6 +91,10 @@ export default abstract class BaseRestClient { this.secret = secret; } + private isSpotClient() { + return this.clientType === REST_CLIENT_TYPE_ENUM.spot; + } + get(endpoint: string, params?: any): GenericAPIResponse { return this._call('GET', endpoint, params, true); } @@ -92,6 +115,26 @@ export default abstract class BaseRestClient { return this._call('DELETE', endpoint, params, false); } + private async prepareSignParams(params?: any, isPublicApi?: boolean) { + if (isPublicApi) { + return { + originalParams: params, + paramsWithSign: params, + }; + } + + if (!this.key || !this.secret) { + throw new Error('Private endpoints require api and private keys set'); + } + + if (this.timeOffset === null) { + await this.syncTime(); + } + + const signedRequest = await this.signRequest(params); + return signedRequest; + } + /** * @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed. */ @@ -101,18 +144,6 @@ export default abstract class BaseRestClient { params?: any, isPublicApi?: boolean ): GenericAPIResponse { - if (!isPublicApi) { - if (!this.key || !this.secret) { - throw new Error('Private endpoints require api and private keys set'); - } - - if (this.timeOffset === null) { - await this.syncTime(); - } - - params = await this.signRequest(params); - } - const options = { ...this.globalRequestOptions, url: [this.baseUrl, endpoint].join(endpoint.startsWith('/') ? '' : '/'), @@ -120,10 +151,21 @@ export default abstract class BaseRestClient { json: true, }; + for (const key in params) { + if (typeof params[key] === 'undefined') { + delete params[key]; + } + } + + const preparedRequestParams = await this.prepareSignParams( + params, + isPublicApi + ); + if (method === 'GET') { - options.params = params; + options.params = preparedRequestParams.paramsWithSign; } else { - options.data = params; + options.data = preparedRequestParams.paramsWithSign; } return axios(options) @@ -170,27 +212,40 @@ export default abstract class BaseRestClient { /** * @private sign request and set recv window */ - async signRequest(data: any): Promise { - const params = { - ...data, - api_key: this.key, - timestamp: Date.now() + (this.timeOffset || 0), + private async signRequest( + data: T & SignedRequestContext + ): Promise> { + const res: SignedRequest = { + originalParams: { + ...data, + api_key: this.key, + timestamp: Date.now() + (this.timeOffset || 0), + }, + sign: '', }; // Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen. - if (this.options.recv_window && !params.recv_window) { - params.recv_window = this.options.recv_window; + if (this.options.recv_window && !res.originalParams.recv_window) { + if (this.isSpotClient()) { + res.originalParams.recvWindow = this.options.recv_window; + } else { + res.originalParams.recv_window = this.options.recv_window; + } } if (this.key && this.secret) { const serializedParams = serializeParams( - params, + res.originalParams, this.options.strict_param_validation ); - params.sign = await signMessage(serializedParams, this.secret); + res.sign = await signMessage(serializedParams, this.secret); + res.paramsWithSign = { + ...res.originalParams, + sign: res.sign, + }; } - return params; + return res; } /** diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index 2ff11af..08f5ecc 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -76,7 +76,7 @@ export function isPublicEndpoint(endpoint: string): boolean { } export function isWsPong(response: any) { - if (response.pong) { + if (response.pong || response.ping) { return true; } return ( @@ -86,3 +86,13 @@ export function isWsPong(response: any) { response.success === true ); } + +export const REST_CLIENT_TYPE_ENUM = { + inverse: 'inverse', + inverseFutures: 'inverseFutures', + linear: 'linear', + spot: 'spot', +} as const; + +export type RestClientType = + typeof REST_CLIENT_TYPE_ENUM[keyof typeof REST_CLIENT_TYPE_ENUM]; diff --git a/test/linear/private.read.test.ts b/test/linear/private.read.test.ts index b0da942..f061cf7 100644 --- a/test/linear/private.read.test.ts +++ b/test/linear/private.read.test.ts @@ -1,9 +1,5 @@ import { LinearClient } from '../../src/linear-client'; -import { - notAuthenticatedError, - successResponseList, - successResponseObject, -} from '../response.util'; +import { successResponseList, successResponseObject } from '../response.util'; describe('Public Linear REST API Endpoints', () => { const useLivenet = true; @@ -20,9 +16,6 @@ describe('Public Linear REST API Endpoints', () => { }); const symbol = 'BTCUSDT'; - const interval = '15'; - const timestampOneHourAgo = new Date().getTime() / 1000 - 1000 * 60 * 60; - const from = Number(timestampOneHourAgo.toFixed(0)); describe('Linear only private GET endpoints', () => { it('getApiKeyInfo()', async () => { diff --git a/test/response.util.ts b/test/response.util.ts index 312a67f..fa9313c 100644 --- a/test/response.util.ts +++ b/test/response.util.ts @@ -14,6 +14,18 @@ export function successResponseObject(successMsg: string | null = 'OK') { }; } +export function errorResponseObject( + result: null | any = null, + ret_code: number, + ret_msg: string +) { + return { + result, + ret_code, + ret_msg, + }; +} + export function notAuthenticatedError() { return new Error('Private endpoints require api and private keys set'); } diff --git a/test/spot/private.read.test.ts b/test/spot/private.read.test.ts new file mode 100644 index 0000000..77762d6 --- /dev/null +++ b/test/spot/private.read.test.ts @@ -0,0 +1,54 @@ +import { SpotClient } from '../../src'; +import { + errorResponseObject, + notAuthenticatedError, + successResponseList, + successResponseObject, +} from '../response.util'; + +describe('Private Spot REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new SpotClient(API_KEY, API_SECRET, useLivenet, { + disable_time_sync: true, + }); + + const symbol = 'BTCUSDT'; + const interval = '15m'; + + it('getOrder()', async () => { + // No auth error == test pass + expect(await api.getOrder({ orderId: '123123' })).toMatchObject( + errorResponseObject(null, -2013, 'Order does not exist.') + ); + }); + + it('getOpenOrders()', async () => { + expect(await api.getOpenOrders()).toMatchObject(successResponseList('')); + }); + + it('getPastOrders()', async () => { + expect(await api.getPastOrders()).toMatchObject(successResponseList('')); + }); + + it('getMyTrades()', async () => { + expect(await api.getMyTrades()).toMatchObject(successResponseList('')); + }); + + it('getBalances()', async () => { + expect(await api.getBalances()).toMatchObject({ + result: { + balances: expect.any(Array), + }, + ret_code: 0, + ret_msg: '', + }); + }); +});