bybit-api-gnome
Version:
Forked for Lick Hunter, Complete & robust node.js SDK for Bybit's REST APIs and WebSockets v5, with TypeScript & integration tests.
281 lines • 11.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const axios_1 = __importDefault(require("axios"));
const node_support_1 = require("./node-support");
const requestUtils_1 = require("./requestUtils");
class BaseRestClient {
/**
* Create an instance of the REST client. Pass API credentials in the object in the first parameter.
* @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity
* @param {AxiosRequestConfig} [networkOptions={}] HTTP networking options for axios
*/
constructor(restOptions = {}, networkOptions = {}) {
this.timeOffset = null;
this.syncTimePromise = null;
this.clientType = this.getClientType();
this.options = {
recv_window: 5000,
/** Throw errors if any request params are empty */
strict_param_validation: false,
/** Disable time sync by default */
enable_time_sync: false,
/** How often to sync time drift with bybit servers (if time sync is enabled) */
sync_interval_ms: 3600000,
/** Request parameter values are now URI encoded by default during signing. Set to false to override this behaviour. */
encodeSerialisedValues: true,
...restOptions,
};
this.globalRequestOptions = {
// in ms == 5 minutes by default
timeout: 1000 * 60 * 5,
// custom request options based on axios specs - see: https://github.com/axios/axios#request-config
...networkOptions,
headers: {
'x-referer': requestUtils_1.APIID,
},
};
this.baseUrl = (0, requestUtils_1.getRestBaseUrl)(!!this.options.testnet, restOptions);
this.key = this.options.key;
this.secret = this.options.secret;
if (this.key && !this.secret) {
throw new Error('API Key & Secret are both required for private endpoints');
}
if (this.options.enable_time_sync) {
this.syncTime();
setInterval(this.syncTime.bind(this), +this.options.sync_interval_ms);
}
}
isSpotV1Client() {
return this.clientType === requestUtils_1.REST_CLIENT_TYPE_ENUM.spot;
}
get(endpoint, params) {
return this._call('GET', endpoint, params, true);
}
getPrivate(endpoint, params) {
return this._call('GET', endpoint, params, false);
}
post(endpoint, params) {
return this._call('POST', endpoint, params, true);
}
postPrivate(endpoint, params) {
return this._call('POST', endpoint, params, false);
}
deletePrivate(endpoint, params) {
return this._call('DELETE', endpoint, params, false);
}
async prepareSignParams(method, signMethod, params, isPublicApi) {
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();
}
return this.signRequest((params || {}), method, signMethod);
}
/** Returns an axios request object. Handles signing process automatically if this is a private API call */
async buildRequest(method, url, params, isPublicApi) {
const options = {
...this.globalRequestOptions,
url: url,
method: method,
};
for (const key in params) {
if (typeof params[key] === 'undefined') {
delete params[key];
}
}
if (isPublicApi) {
return {
...options,
params: params,
};
}
// USDC endpoints, unified margin and a few others use a different way of authenticating requests (headers instead of params)
if (this.clientType === requestUtils_1.REST_CLIENT_TYPE_ENUM.v3) {
if (!options.headers) {
options.headers = {};
}
const signResult = await this.prepareSignParams(method, 'usdc', params, isPublicApi);
options.headers['X-BAPI-SIGN-TYPE'] = 2;
options.headers['X-BAPI-API-KEY'] = this.key;
options.headers['X-BAPI-TIMESTAMP'] = signResult.timestamp;
options.headers['X-BAPI-SIGN'] = signResult.sign;
options.headers['X-BAPI-RECV-WINDOW'] = signResult.recvWindow;
if (method === 'GET') {
// const serialisedParams = signResult.serializedParams;
return {
...options,
params: signResult.originalParams,
// url: url + (serialisedParams ? '?' + serialisedParams : ''),
};
}
return {
...options,
data: signResult.originalParams,
};
}
const signResult = await this.prepareSignParams(method, 'keyInBody', params, isPublicApi);
if (method === 'GET' || this.isSpotV1Client()) {
return {
...options,
params: signResult.paramsWithSign,
};
}
return {
...options,
data: signResult.paramsWithSign,
};
}
/**
* @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed.
*/
async _call(method, endpoint, params, isPublicApi) {
// Sanity check to make sure it's only ever prefixed by one forward slash
const requestUrl = [this.baseUrl, endpoint].join(endpoint.startsWith('/') ? '' : '/');
// Build a request and handle signature process
const options = await this.buildRequest(method, requestUrl, params, isPublicApi);
// Dispatch request
return (0, axios_1.default)(options)
.then((response) => {
if (response.status == 200) {
return response.data;
}
throw response;
})
.catch((e) => this.parseException(e));
}
/**
* @private generic handler to parse request exceptions
*/
parseException(e) {
if (this.options.parse_exceptions === false) {
throw e;
}
// Something happened in setting up the request that triggered an Error
if (!e.response) {
if (!e.request) {
throw e.message;
}
// request made but no response received
throw e;
}
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
const response = e.response;
throw {
code: response.status,
message: response.statusText,
body: response.data,
headers: response.headers,
requestOptions: this.options,
};
}
/**
* @private sign request and set recv window
*/
async signRequest(data, method, signMethod) {
const timestamp = Date.now() + (this.timeOffset || 0);
const res = {
originalParams: {
...data,
},
sign: '',
timestamp,
recvWindow: 0,
serializedParams: '',
};
if (!this.key || !this.secret) {
return res;
}
const key = this.key;
const recvWindow = res.originalParams.recv_window || this.options.recv_window || 5000;
const strictParamValidation = this.options.strict_param_validation;
const encodeSerialisedValues = this.options.encodeSerialisedValues;
// In case the parent function needs it (e.g. USDC uses a header)
res.recvWindow = recvWindow;
// usdc is different for some reason
if (signMethod === 'usdc') {
const sortProperties = false;
const signRequestParams = method === 'GET'
? (0, requestUtils_1.serializeParams)(res.originalParams, strictParamValidation, sortProperties, encodeSerialisedValues)
: JSON.stringify(res.originalParams);
const paramsStr = timestamp + key + recvWindow + signRequestParams;
res.sign = await (0, node_support_1.signMessage)(paramsStr, this.secret);
res.serializedParams = signRequestParams;
// console.log('sign req: ', paramsStr);
return res;
}
// spot/v2 derivatives
if (signMethod === 'keyInBody') {
res.originalParams.api_key = key;
res.originalParams.timestamp = timestamp;
// Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen.
if (recvWindow) {
if (this.isSpotV1Client()) {
res.originalParams.recvWindow = recvWindow;
}
else {
res.originalParams.recv_window = recvWindow;
}
}
const sortProperties = true;
const encodeValues = false;
res.serializedParams = (0, requestUtils_1.serializeParams)(res.originalParams, strictParamValidation, sortProperties, encodeValues);
res.sign = await (0, node_support_1.signMessage)(res.serializedParams, this.secret);
res.paramsWithSign = {
...res.originalParams,
sign: res.sign,
};
return res;
}
return res;
}
/**
* Trigger time sync and store promise. Use force: true, if automatic time sync is disabled
*/
syncTime(force) {
if (!force && !this.options.enable_time_sync) {
this.timeOffset = 0;
return Promise.resolve(false);
}
if (this.syncTimePromise !== null) {
return this.syncTimePromise;
}
this.syncTimePromise = this.fetchTimeOffset().then((offset) => {
this.timeOffset = offset;
this.syncTimePromise = null;
});
return this.syncTimePromise;
}
/**
* Estimate drift based on client<->server latency
*/
async fetchTimeOffset() {
try {
const start = Date.now();
const serverTime = await this.fetchServerTime();
if (!serverTime || isNaN(serverTime)) {
throw new Error(`fetchServerTime() returned non-number: "${serverTime}" typeof(${typeof serverTime})`);
}
const end = Date.now();
const severTimeMs = serverTime * 1000;
const avgDrift = (end - start) / 2;
return Math.ceil(severTimeMs - end + avgDrift);
}
catch (e) {
console.error('Failed to fetch get time offset: ', e);
return 0;
}
}
}
exports.default = BaseRestClient;
//# sourceMappingURL=BaseRestClient.js.map