UNPKG

bybit-api

Version:

Complete & robust Node.js SDK for Bybit's REST APIs and WebSockets, with TypeScript & strong end to end tests.

293 lines 13.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /* eslint-disable @typescript-eslint/no-explicit-any */ const axios_1 = __importDefault(require("axios")); const https_1 = __importDefault(require("https")); const requestUtils_1 = require("./requestUtils"); const webCryptoAPI_1 = require("./webCryptoAPI"); const ENABLE_HTTP_TRACE = typeof process === 'object' && typeof process.env === 'object' && process.env.BYBITTRACE; if (ENABLE_HTTP_TRACE) { // axios.interceptors.request.use((request) => { // console.log( // new Date(), // 'Starting Request', // JSON.stringify(request, null, 2) // ); // return request; // }); axios_1.default.interceptors.response.use((response) => { console.log(new Date(), 'Response:', { request: { url: response.config.url, method: response.config.method, data: response.config.data, headers: response.config.headers, params: response.config.params, }, response: { status: response.status, statusText: response.statusText, headers: response.headers, data: response.data, }, }); return response; }); } class BaseRestClient { /** * Create an instance of the REST client. Pass API credentials in the object in the first parameter. * @param {RestClientOptions} [restOptions={}] 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 = Object.assign({ 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, /** Determines whether to perform time synchronization before sending private requests */ syncTimeBeforePrivateRequests: false, /** Request parameter values are now URI encoded by default during signing. Set to false to override this behaviour. */ encodeSerialisedValues: true }, restOptions); this.globalRequestOptions = Object.assign(Object.assign({ // in ms == 5 minutes by default timeout: 1000 * 60 * 5 }, networkOptions), { headers: Object.assign(Object.assign({}, networkOptions.headers), { 'x-referer': requestUtils_1.APIID }) }); // If enabled, configure a https agent with keepAlive enabled if (this.options.keepAlive) { // For more advanced configuration, raise an issue on GitHub or use the "networkOptions" // parameter to define a custom httpsAgent with the desired properties this.globalRequestOptions.httpsAgent = new https_1.default.Agent({ keepAlive: true, keepAliveMsecs: this.options.keepAliveMsecs, }); } 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); } } 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); } prepareSignParams(method, signMethod, params, isPublicApi) { return __awaiter(this, void 0, void 0, function* () { 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) { yield this.syncTime(); } if (this.options.syncTimeBeforePrivateRequests) { this.timeOffset = yield this.fetchTimeOffset(); } return this.signRequest(params || {}, method, signMethod); }); } /** Returns an axios request object. Handles signing process automatically if this is a private API call */ buildRequest(method, url, params, isPublicApi) { return __awaiter(this, void 0, void 0, function* () { const options = Object.assign(Object.assign({}, this.globalRequestOptions), { url: url, method: method }); for (const key in params) { if (typeof params[key] === 'undefined') { delete params[key]; } } if (isPublicApi) { return Object.assign(Object.assign({}, options), { params: params }); } const signResult = yield this.prepareSignParams(method, 'v5auth', params, isPublicApi); const headers = Object.assign({ 'X-BAPI-SIGN-TYPE': 2, 'X-BAPI-API-KEY': this.key, 'X-BAPI-TIMESTAMP': signResult.timestamp, 'X-BAPI-SIGN': signResult.sign, 'X-BAPI-RECV-WINDOW': signResult.recvWindow }, options.headers); if (method === 'GET') { return Object.assign(Object.assign({}, options), { headers, url: signResult.serializedParams ? options.url + '?' + signResult.serializedParams : options.url }); } return Object.assign(Object.assign({}, options), { headers, data: signResult.originalParams }); }); } /** * @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed. */ _call(method, endpoint, params, isPublicApi) { return __awaiter(this, void 0, void 0, function* () { // 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 = yield this.buildRequest(method, requestUrl, params, isPublicApi); if (ENABLE_HTTP_TRACE) { console.log('full request: ', options); } // Dispatch request return (0, axios_1.default)(options) .then((response) => { if (response.status == 200) { const perAPIRateLimits = this.options.parseAPIRateLimits ? (0, requestUtils_1.parseRateLimitHeaders)(response.headers, this.options.throwOnFailedRateLimitParse === true) : undefined; return Object.assign({ rateLimitApi: perAPIRateLimits }, 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, }; } signMessage(paramsStr, secret, method, algorithm) { return __awaiter(this, void 0, void 0, function* () { if (typeof this.options.customSignMessageFn === 'function') { return this.options.customSignMessageFn(paramsStr, secret); } return yield (0, webCryptoAPI_1.signMessage)(paramsStr, secret, method, algorithm); }); } /** * @private sign request and set recv window */ signRequest(data, method, signMethod) { return __awaiter(this, void 0, void 0, function* () { const timestamp = Date.now() + (this.timeOffset || 0); const res = { originalParams: Object.assign({}, 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 === 'v5auth') { 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 = yield this.signMessage(paramsStr, this.secret, 'hex', 'SHA-256'); res.serializedParams = signRequestParams; // console.log('sign req: ', { // req: paramsStr, // 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 */ fetchTimeOffset() { return __awaiter(this, void 0, void 0, function* () { try { const start = Date.now(); const serverTime = yield 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