fixes for private spot GET calls, improve signing process, add private read tests for spot & linear
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<T> {
|
||||
originalParams: T & SignedRequestContext;
|
||||
paramsWithSign?: T & SignedRequestContext & { sign: string };
|
||||
sign: string;
|
||||
}
|
||||
|
||||
export default abstract class BaseRestClient {
|
||||
private timeOffset: number | null;
|
||||
private syncTimePromise: null | Promise<any>;
|
||||
@@ -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<number>;
|
||||
@@ -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<any> {
|
||||
const params = {
|
||||
private async signRequest<T extends Object>(
|
||||
data: T & SignedRequestContext
|
||||
): Promise<SignedRequest<T>> {
|
||||
const res: SignedRequest<T> = {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
54
test/spot/private.read.test.ts
Normal file
54
test/spot/private.read.test.ts
Normal 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: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user