ftx-api-typed
Version:
Node.js/typescript connector for FTX's REST APIs and WebSockets
217 lines • 9.04 kB
JavaScript
"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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_support_1 = require("./node-support");
const requestUtils_1 = require("./requestUtils");
const getHeader = (headerId, domain = 'ftxcom') => {
if (domain === 'ftxcom') {
switch (headerId) {
case 'key':
return 'FTX-KEY';
case 'ts':
return 'FTX-TS';
case 'sign':
return 'FTX-SIGN';
case 'subaccount':
return 'FTX-SUBACCOUNT';
}
}
if (domain === 'ftxus') {
switch (headerId) {
case 'key':
return 'FTXUS-KEY';
case 'ts':
return 'FTXUS-TS';
case 'sign':
return 'FTXUS-SIGN';
case 'subaccount':
return 'FTXUS-SUBACCOUNT';
}
}
console.warn('Unknown header requested: ', { headerId, domain });
return 'null';
};
class RequestUtil {
constructor(key, secret, baseUrl, options = {}, requestOptions = {}, axiosInstance) {
this.axiosInstance = axiosInstance;
this.timeOffset = null;
this.syncTimePromise = null;
this.options = Object.assign({ recv_window: 5000,
// how often to sync time drift with exchange servers
sync_interval_ms: 3600000,
// if true, we'll throw errors if any params are undefined
strict_param_validation: false }, options);
this.globalRequestOptions = Object.assign({
// in ms == 5 minutes by default
timeout: 1000 * 60 * 5, headers: {} }, requestOptions);
if (typeof key === 'string') {
this.globalRequestOptions.headers[getHeader('key', options.domain)] = key;
}
if (typeof this.options.subAccountName === 'string') {
this.globalRequestOptions.headers[getHeader('subaccount', options.domain)] = this.options.subAccountName;
}
this.baseUrl = baseUrl;
if (key && !secret) {
throw new Error('API Key & Secret are both required for private endpoints');
}
if (this.options.disable_time_sync !== true) {
this.syncTime();
setInterval(this.syncTime.bind(this), +this.options.sync_interval_ms);
}
this.key = key;
this.secret = secret;
}
get(endpoint, params) {
return this._call('GET', endpoint, params);
}
post(endpoint, params) {
return this._call('POST', endpoint, Object.assign(Object.assign({}, params), { [requestUtils_1.programKey]: (0, requestUtils_1.isFtxUS)(this.options) ? requestUtils_1.programId : requestUtils_1.programId2 }));
}
postFormData(endpoint, formData) {
const headers = (formData === null || formData === void 0 ? void 0 : formData.getHeaders()) || {};
return this._call('POST', endpoint, formData, Object.assign({}, headers));
}
delete(endpoint, params) {
return this._call('DELETE', endpoint, params);
}
/**
* @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed.
*/
_call(method, endpoint, params, headers) {
return __awaiter(this, void 0, void 0, function* () {
let options = Object.assign(Object.assign({}, this.globalRequestOptions), { method: method, json: true });
options = Object.assign(Object.assign({}, options), { headers: Object.assign(Object.assign({}, options.headers), headers) });
options.url = endpoint.startsWith('https') ? endpoint : [this.baseUrl, endpoint].join('/');
const isGetRequest = method === 'GET';
const serialisedParams = (0, requestUtils_1.serializeParamPayload)(isGetRequest, params, this.options.strict_param_validation);
// Add request sign
if (this.key && this.secret) {
if (this.timeOffset === null && !this.options.disable_time_sync) {
yield this.syncTime();
}
const { timestamp, sign } = yield this.getRequestSignature(method, endpoint, this.secret, serialisedParams);
options.headers[getHeader('ts', this.options.domain)] = String(timestamp);
options.headers[getHeader('sign', this.options.domain)] = sign;
}
if (isGetRequest) {
options.url += serialisedParams;
}
else {
options.data = params;
}
return this.axiosInstance(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
};
}
getRequestSignature(method, endpoint, secret, serialisedParams = '') {
return __awaiter(this, void 0, void 0, function* () {
const timestamp = Date.now() + (this.timeOffset || 0);
if (!secret) {
return {
timestamp,
sign: ''
};
}
let signature_payload;
if (serialisedParams === '?') {
signature_payload = `${timestamp}${method}/api/${endpoint}`;
}
else {
signature_payload = `${timestamp}${method}/api/${endpoint}${serialisedParams}`;
}
return {
timestamp,
sign: yield (0, node_support_1.signMessage)(signature_payload, secret)
};
});
}
/**
* @private sign request and set recv window
*/
signRequest(data) {
const params = Object.assign(Object.assign({}, data), { api_key: this.key, timestamp: Date.now() + (this.timeOffset || 0) });
// 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.key && this.secret) {
const serializedParams = (0, requestUtils_1.serializeParams)(params, this.options.strict_param_validation);
params.sign = (0, node_support_1.signMessage)(serializedParams, this.secret);
}
return params;
}
/**
* @private trigger time sync and store promise
*/
syncTime() {
if (this.options.disable_time_sync === true) {
return Promise.resolve(false);
}
if (this.syncTimePromise !== null) {
return this.syncTimePromise;
}
this.syncTimePromise = this.getTimeOffset().then(offset => {
this.timeOffset = offset;
this.syncTimePromise = null;
});
return this.syncTimePromise;
}
/**
* @deprecated move this somewhere else, because endpoints shouldn't be hardcoded here
*/
getTimeOffset() {
return __awaiter(this, void 0, void 0, function* () {
const start = Date.now();
try {
const response = yield this.get('https://otc.ftx.com/api/time');
const result = new Date(response.result).getTime();
const end = Date.now();
return Math.ceil(result - end + ((end - start) / 2));
}
catch (e) {
return 0;
}
});
}
}
exports.default = RequestUtil;
;
//# sourceMappingURL=requestWrapper.js.map