UNPKG

gateio-api

Version:

Complete & Robust Node.js SDK for Gate.com's REST APIs, WebSockets & WebSocket APIs, with TypeScript declarations.

351 lines 14.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseRestClient = exports.REST_CLIENT_TYPE_ENUM = void 0; const axios_1 = __importDefault(require("axios")); const https_1 = __importDefault(require("https")); const misc_util_js_1 = require("./misc-util.js"); const requestUtils_js_1 = require("./requestUtils.js"); const webCryptoAPI_js_1 = require("./webCryptoAPI.js"); const MISSING_API_KEYS_ERROR = 'API Key, Secret & Application ID are ALL required to use the authenticated REST client'; /** * Used to switch how authentication/requests work under the hood */ exports.REST_CLIENT_TYPE_ENUM = { main: 'main', }; /** * Enables: * - Detailed request/response logging * - Full request dump in any exceptions thrown from API responses */ const ENABLE_HTTP_TRACE = typeof process === 'object' && typeof process.env === 'object' && process.env.GATETRACE; if (ENABLE_HTTP_TRACE) { axios_1.default.interceptors.request.use((request) => { console.log(new Date(), 'Starting Request', JSON.stringify({ url: request.url, method: request.method, params: request.params, data: request.data, }, 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, // }, response: { status: response.status, statusText: response.statusText, headers: response.headers, data: response.data, }, }); return response; }); } /** * Impure, mutates params to remove any values that have a key but are undefined. */ function deleteUndefinedValues(params) { if (!params) { return; } for (const key in params) { const value = params[key]; if (typeof value === 'undefined') { delete params[key]; } } } class BaseRestClient { options; baseUrl; baseUrlPath; globalRequestOptions; apiKey; apiSecret; /** * 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(restClientOptions = {}, networkOptions = {}) { this.options = { recvWindow: 5000, /** Throw errors if any request params are empty */ strictParamValidation: false, ...restClientOptions, }; this.globalRequestOptions = { /** in ms == 5 minutes by default */ timeout: 1000 * 60 * 5, /** inject custom rquest options based on axios specs - see axios docs for more guidance on AxiosRequestConfig: https://github.com/axios/axios#request-config */ ...networkOptions, headers: { 'Content-Type': 'application/json', 'X-Gate-Channel-Id': requestUtils_js_1.CHANNEL_ID, locale: 'en-US', }, }; // If enabled, configure a https agent with keepAlive enabled if (this.options.keepAlive) { // Extract existing https agent parameters, if provided, to prevent the keepAlive flag from overwriting an existing https agent completely const existingHttpsAgent = this.globalRequestOptions.httpsAgent; const existingAgentOptions = existingHttpsAgent?.options || {}; // 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({ ...existingAgentOptions, keepAlive: true, keepAliveMsecs: this.options.keepAliveMsecs, }); } this.baseUrl = (0, requestUtils_js_1.getRestBaseUrl)(restClientOptions); this.baseUrlPath = new URL(this.baseUrl).pathname; this.apiKey = this.options.apiKey; this.apiSecret = this.options.apiSecret; // Check Web Crypto API support when credentials are provided and no custom sign function is used if (this.apiKey && this.apiSecret && !this.options.customSignMessageFn) { (0, webCryptoAPI_js_1.checkWebCryptoAPISupported)(); } // Throw if one of the 3 values is missing, but at least one of them is set const credentials = [this.apiKey, this.apiSecret]; if (credentials.includes(undefined) && credentials.some((v) => typeof v === 'string')) { throw new Error(MISSING_API_KEYS_ERROR); } } /** * Timestamp used to sign the request. Override this method to implement your own timestamp/sync mechanism */ getSignTimestampMs() { return Date.now(); } get(endpoint, params) { const isPublicAPI = true; // GET only supports params in the query string return this._call('GET', endpoint, { query: params }, isPublicAPI); } post(endpoint, params) { const isPublicAPI = true; return this._call('POST', endpoint, params, isPublicAPI); } getPrivate(endpoint, params) { const isPublicAPI = false; // GET only supports params in the query string return this._call('GET', endpoint, { query: params }, isPublicAPI); } postPrivate(endpoint, params) { const isPublicAPI = false; return this._call('POST', endpoint, params, isPublicAPI); } deletePrivate(endpoint, params) { const isPublicAPI = false; return this._call('DELETE', endpoint, params, isPublicAPI); } putPrivate(endpoint, params) { const isPublicAPI = false; return this._call('PUT', endpoint, params, isPublicAPI); } // protected patchPrivate(endpoint: string, params?: any) { patchPrivate(endpoint, params) { const isPublicAPI = false; return this._call('PATCH', endpoint, params, isPublicAPI); } /** * @private Make a HTTP request to a specific endpoint. Private endpoint API calls 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, endpoint, requestUrl, params, isPublicApi); if (ENABLE_HTTP_TRACE) { console.log('full request: ', options); } // Dispatch request return (0, axios_1.default)(options) .then((response) => { // See: https://www.gate.io/docs/developers/apiv4/en/#return-format if (response.status >= 200 && response.status <= 204) { // Throw API rejections by parsing the response code from the body if (typeof response.data?.code === 'number' && response.data?.code !== 1000) { throw { response }; } return response.data; } throw { response }; }) .catch((e) => this.parseException(e, options)); } /** * @private generic handler to parse request exceptions */ parseException(e, request) { if (this.options.parseExceptions === 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; // console.error('err: ', response?.data); const debugData = ENABLE_HTTP_TRACE ? { fullRequest: request } : {}; throw { code: response.status, message: response.statusText, body: response.data, headers: response.headers, requestOptions: { ...this.options, // Prevent credentials from leaking into error messages apiKey: 'omittedFromError', apiMemo: 'omittedFromError', apiSecret: 'omittedFromError', reqUrl: request.url, reqBody: request.data, }, ...debugData, }; } /** * @private sign request and set recv window */ async signRequest(data, endpoint, method, signMethod) { const timestamp = +(this.getSignTimestampMs() / 1000).toFixed(0); // in seconds const res = { originalParams: { // recvWindow: this.options.recvWindow, ...data, }, sign: '', timestamp, recvWindow: 0, serializedParams: '', queryParamsWithSign: '', }; if (!this.apiKey || !this.apiSecret) { return res; } // It's possible to override the recv window on a per rquest level const strictParamValidation = this.options.strictParamValidation; const encodeQueryStringValues = false; if (signMethod === 'gateV4') { const signEncoding = 'hex'; const signAlgoritm = 'SHA-512'; const queryStringToSign = data?.query ? (0, requestUtils_js_1.serializeParams)(res.originalParams?.query, strictParamValidation, encodeQueryStringValues, '') : ''; const requestBodyToHash = res.originalParams?.body ? JSON.stringify(res.originalParams?.body) : ''; const hashedRequestBody = await (0, webCryptoAPI_js_1.hashMessage)(requestBodyToHash, signEncoding, signAlgoritm); const toSign = [ method, this.baseUrlPath + endpoint, queryStringToSign, hashedRequestBody, timestamp, ].join('\n'); // console.log('sign params: ', { // requestBodyToHash, // paramsStr: toSign, // url: this.baseUrl, // urlPath: this.baseUrlPath, // }); res.sign = await this.signMessage(toSign, this.apiSecret, signEncoding, signAlgoritm); res.queryParamsWithSign = queryStringToSign; return res; } console.error(new Date(), (0, misc_util_js_1.neverGuard)(signMethod, `Unhandled sign method: "${webCryptoAPI_js_1.signMessage}"`)); return res; } async signMessage(paramsStr, secret, method, algorithm) { if (typeof this.options.customSignMessageFn === 'function') { return this.options.customSignMessageFn(paramsStr, secret); } return await (0, webCryptoAPI_js_1.signMessage)(paramsStr, secret, method, algorithm); } async prepareSignParams(method, endpoint, signMethod, params, isPublicApi) { if (isPublicApi) { return { originalParams: params, paramsWithSign: params, }; } if (!this.apiKey || !this.apiSecret) { throw new Error(MISSING_API_KEYS_ERROR); } return this.signRequest(params, endpoint, method, signMethod); } /** Returns an axios request object. Handles signing process automatically if this is a private API call */ async buildRequest(method, endpoint, url, params, isPublicApi) { const options = { ...this.globalRequestOptions, url: url, method: method, headers: { ...params?.headers, ...this.globalRequestOptions.headers, }, }; deleteUndefinedValues(params); deleteUndefinedValues(params?.body); deleteUndefinedValues(params?.query); deleteUndefinedValues(params?.headers); if (!isPublicApi && (!this.apiKey || !this.apiSecret)) { throw new Error('API Key & Secret are both required for private endpoints'); } if (isPublicApi || !this.apiKey || !this.apiSecret) { return { ...options, params: params?.query || params?.body || params, }; } const signResult = await this.prepareSignParams(method, endpoint, 'gateV4', params, isPublicApi); const authHeaders = { KEY: this.apiKey, SIGN: signResult.sign, Timestamp: signResult.timestamp, }; const urlWithQueryParams = options.url + '?' + signResult.queryParamsWithSign; if (method === 'GET' || !params?.body) { return { ...options, headers: { ...authHeaders, ...options.headers, }, url: urlWithQueryParams, }; } return { ...options, headers: { ...authHeaders, ...options.headers, }, url: params?.query ? urlWithQueryParams : options.url, data: signResult.originalParams.body, }; } } exports.BaseRestClient = BaseRestClient; //# sourceMappingURL=BaseRestClient.js.map