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,
|
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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
...data,
|
): Promise<SignedRequest<T>> {
|
||||||
api_key: this.key,
|
const res: SignedRequest<T> = {
|
||||||
timestamp: Date.now() + (this.timeOffset || 0),
|
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.
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
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