fixes for private spot GET calls, improve signing process, add private read tests for spot & linear

This commit is contained in:
tiagosiebler
2022-05-08 01:00:12 +01:00
parent 7840829454
commit 38f5a6286c
9 changed files with 176 additions and 41 deletions

View File

@@ -3,6 +3,7 @@ import {
GenericAPIResponse, GenericAPIResponse,
getRestBaseUrl, getRestBaseUrl,
RestClientOptions, RestClientOptions,
REST_CLIENT_TYPE_ENUM,
} from './util/requestUtils'; } from './util/requestUtils';
import RequestWrapper from './util/requestWrapper'; import RequestWrapper from './util/requestWrapper';
import { import {
@@ -46,7 +47,8 @@ export class InverseClient extends BaseRestClient {
secret, secret,
getRestBaseUrl(useLivenet, restClientOptions), getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions, restClientOptions,
requestOptions requestOptions,
REST_CLIENT_TYPE_ENUM.inverse
); );
this.requestWrapper = new RequestWrapper( this.requestWrapper = new RequestWrapper(
key, key,

View File

@@ -3,6 +3,7 @@ import {
GenericAPIResponse, GenericAPIResponse,
getRestBaseUrl, getRestBaseUrl,
RestClientOptions, RestClientOptions,
REST_CLIENT_TYPE_ENUM,
} from './util/requestUtils'; } from './util/requestUtils';
import RequestWrapper from './util/requestWrapper'; import RequestWrapper from './util/requestWrapper';
import { import {
@@ -44,7 +45,8 @@ export class InverseFuturesClient extends BaseRestClient {
secret, secret,
getRestBaseUrl(useLivenet, restClientOptions), getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions, restClientOptions,
requestOptions requestOptions,
REST_CLIENT_TYPE_ENUM.inverseFutures
); );
this.requestWrapper = new RequestWrapper( this.requestWrapper = new RequestWrapper(
key, key,

View File

@@ -3,6 +3,7 @@ import {
GenericAPIResponse, GenericAPIResponse,
getRestBaseUrl, getRestBaseUrl,
RestClientOptions, RestClientOptions,
REST_CLIENT_TYPE_ENUM,
} from './util/requestUtils'; } from './util/requestUtils';
import RequestWrapper from './util/requestWrapper'; import RequestWrapper from './util/requestWrapper';
import { import {
@@ -46,7 +47,8 @@ export class LinearClient extends BaseRestClient {
secret, secret,
getRestBaseUrl(useLivenet, restClientOptions), getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions, restClientOptions,
requestOptions requestOptions,
REST_CLIENT_TYPE_ENUM.linear
); );
this.requestWrapper = new RequestWrapper( this.requestWrapper = new RequestWrapper(

View File

@@ -8,7 +8,11 @@ import {
SpotSymbolInfo, SpotSymbolInfo,
} from './types/spot'; } from './types/spot';
import BaseRestClient from './util/BaseRestClient'; 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 { export class SpotClient extends BaseRestClient {
/** /**
@@ -32,7 +36,8 @@ export class SpotClient extends BaseRestClient {
secret, secret,
getRestBaseUrl(useLivenet, restClientOptions), getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions, restClientOptions,
requestOptions requestOptions,
REST_CLIENT_TYPE_ENUM.spot
); );
return this; return this;

View File

@@ -9,11 +9,26 @@ import { signMessage } from './node-support';
import { import {
RestClientOptions, RestClientOptions,
GenericAPIResponse, GenericAPIResponse,
getRestBaseUrl,
serializeParams, serializeParams,
isPublicEndpoint, RestClientType,
REST_CLIENT_TYPE_ENUM,
} from './requestUtils'; } from './requestUtils';
interface SignedRequestContext {
timestamp: number;
api_key?: string;
recv_window?: number;
// spot is diff from the rest...
recvWindow?: number;
}
interface SignedRequest<T> {
originalParams: T & SignedRequestContext;
paramsWithSign?: T & SignedRequestContext & { sign: string };
sign: string;
}
export default abstract class BaseRestClient { export default abstract class BaseRestClient {
private timeOffset: number | null; private timeOffset: number | null;
private syncTimePromise: null | Promise<any>; private syncTimePromise: null | Promise<any>;
@@ -22,6 +37,7 @@ export default abstract class BaseRestClient {
private globalRequestOptions: AxiosRequestConfig; private globalRequestOptions: AxiosRequestConfig;
private key: string | undefined; private key: string | undefined;
private secret: string | undefined; private secret: string | undefined;
private clientType: RestClientType;
/** Function that calls exchange API to query & resolve server time, used by time sync */ /** Function that calls exchange API to query & resolve server time, used by time sync */
abstract fetchServerTime(): Promise<number>; abstract fetchServerTime(): Promise<number>;
@@ -31,11 +47,14 @@ export default abstract class BaseRestClient {
secret: string | undefined, secret: string | undefined,
baseUrl: string, baseUrl: string,
options: RestClientOptions = {}, options: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {} requestOptions: AxiosRequestConfig = {},
clientType: RestClientType
) { ) {
this.timeOffset = null; this.timeOffset = null;
this.syncTimePromise = null; this.syncTimePromise = null;
this.clientType = clientType;
this.options = { this.options = {
recv_window: 5000, recv_window: 5000,
// how often to sync time drift with bybit servers // how often to sync time drift with bybit servers
@@ -72,6 +91,10 @@ export default abstract class BaseRestClient {
this.secret = secret; this.secret = secret;
} }
private isSpotClient() {
return this.clientType === REST_CLIENT_TYPE_ENUM.spot;
}
get(endpoint: string, params?: any): GenericAPIResponse { get(endpoint: string, params?: any): GenericAPIResponse {
return this._call('GET', endpoint, params, true); return this._call('GET', endpoint, params, true);
} }
@@ -92,6 +115,26 @@ export default abstract class BaseRestClient {
return this._call('DELETE', endpoint, params, false); 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. * @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, params?: any,
isPublicApi?: boolean isPublicApi?: boolean
): GenericAPIResponse { ): 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 = { const options = {
...this.globalRequestOptions, ...this.globalRequestOptions,
url: [this.baseUrl, endpoint].join(endpoint.startsWith('/') ? '' : '/'), url: [this.baseUrl, endpoint].join(endpoint.startsWith('/') ? '' : '/'),
@@ -120,10 +151,21 @@ export default abstract class BaseRestClient {
json: true, 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') { if (method === 'GET') {
options.params = params; options.params = preparedRequestParams.paramsWithSign;
} else { } else {
options.data = params; options.data = preparedRequestParams.paramsWithSign;
} }
return axios(options) return axios(options)
@@ -170,27 +212,40 @@ export default abstract class BaseRestClient {
/** /**
* @private sign request and set recv window * @private sign request and set recv window
*/ */
async signRequest(data: any): Promise<any> { private async signRequest<T extends Object>(
const params = { data: T & SignedRequestContext
): Promise<SignedRequest<T>> {
const res: SignedRequest<T> = {
originalParams: {
...data, ...data,
api_key: this.key, api_key: this.key,
timestamp: Date.now() + (this.timeOffset || 0), timestamp: Date.now() + (this.timeOffset || 0),
},
sign: '',
}; };
// Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen. // Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen.
if (this.options.recv_window && !params.recv_window) { if (this.options.recv_window && !res.originalParams.recv_window) {
params.recv_window = this.options.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) { if (this.key && this.secret) {
const serializedParams = serializeParams( const serializedParams = serializeParams(
params, res.originalParams,
this.options.strict_param_validation 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;
} }
/** /**

View File

@@ -76,7 +76,7 @@ export function isPublicEndpoint(endpoint: string): boolean {
} }
export function isWsPong(response: any) { export function isWsPong(response: any) {
if (response.pong) { if (response.pong || response.ping) {
return true; return true;
} }
return ( return (
@@ -86,3 +86,13 @@ export function isWsPong(response: any) {
response.success === true 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];

View File

@@ -1,9 +1,5 @@
import { LinearClient } from '../../src/linear-client'; import { LinearClient } from '../../src/linear-client';
import { import { successResponseList, successResponseObject } from '../response.util';
notAuthenticatedError,
successResponseList,
successResponseObject,
} from '../response.util';
describe('Public Linear REST API Endpoints', () => { describe('Public Linear REST API Endpoints', () => {
const useLivenet = true; const useLivenet = true;
@@ -20,9 +16,6 @@ describe('Public Linear REST API Endpoints', () => {
}); });
const symbol = 'BTCUSDT'; 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', () => { describe('Linear only private GET endpoints', () => {
it('getApiKeyInfo()', async () => { it('getApiKeyInfo()', async () => {

View File

@@ -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() { export function notAuthenticatedError() {
return new Error('Private endpoints require api and private keys set'); return new Error('Private endpoints require api and private keys set');
} }

View File

@@ -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: '',
});
});
});