UNPKG

kucoin-api

Version:

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

275 lines 11.2 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 = void 0; const axios_1 = __importDefault(require("axios")); 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 & API Passphrase are ALL required to use the authenticated REST client'; const ENABLE_HTTP_TRACE = typeof process === 'object' && typeof process.env === 'object' && process.env.KUCOINTRACE; 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: JSON.stringify(response.data, null, 2), }, }); return response; }); } class BaseRestClient { options; baseUrl; globalRequestOptions; apiKey; apiSecret; apiPassphrase; /** * 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 = { /** Throw errors if any request params are empty */ strictParamValidation: false, apiKeyVersion: 2, ...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', locale: 'en-US', }, }; this.baseUrl = (0, requestUtils_js_1.getRestBaseUrl)(false, restClientOptions, this.getClientType()); this.apiKey = this.options.apiKey; this.apiSecret = this.options.apiSecret; this.apiPassphrase = this.options.apiPassphrase; // Throw if one of the 3 values is missing, but at least one of them is set const credentials = [this.apiKey, this.apiSecret, this.apiPassphrase]; if (credentials.includes(undefined) && credentials.some((v) => typeof v === 'string')) { throw new Error(MISSING_API_KEYS_ERROR); } } /** * Generates a timestamp for signing API requests. * * This method can be overridden or customized using `customTimestampFn` * to implement a custom timestamp synchronization mechanism. * If no custom function is provided, it defaults to the current system time. */ getSignTimestampMs() { if (typeof this.options.customTimestampFn === 'function') { return this.options.customTimestampFn(); } return Date.now(); } get(endpoint, params) { return this._call('GET', endpoint, params, true); } post(endpoint, params) { return this._call('POST', endpoint, params, true); } getPrivate(endpoint, params) { return this._call('GET', endpoint, params, false); } postPrivate(endpoint, params) { return this._call('POST', endpoint, params, false); } deletePrivate(endpoint, params) { return this._call('DELETE', endpoint, params, false); } /** * @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) => { if (response.status == 200) { // Throw if API returns an error (e.g. insufficient balance) if (typeof response.data?.code === 'string' && response.data?.code !== '200000') { throw { response }; } return response.data; } throw { response }; }) .catch((e) => this.parseException(e, { method, endpoint, requestUrl, params })); } /** * @private generic handler to parse request exceptions */ parseException(e, requestParams) { 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); 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', apiSecret: 'omittedFromError', apiPassphrase: 'omittedFromError', }, requestParams, }; } /** * @private sign request and set recv window */ async signRequest(data, endpoint, method, signMethod) { const timestamp = this.getSignTimestampMs(); const res = { originalParams: { ...data, }, sign: '', timestamp, recvWindow: 0, serializedParams: '', queryParamsWithSign: '', }; if (!this.apiKey || !this.apiSecret) { return res; } const strictParamValidation = this.options.strictParamValidation; const encodeQueryStringValues = true; if (signMethod === 'kucoin') { const signRequestParams = method === 'GET' || method === 'DELETE' ? (0, requestUtils_js_1.serializeParams)(data, strictParamValidation, encodeQueryStringValues, '?') : JSON.stringify(data) || ''; const paramsStr = `${timestamp}${method}/${endpoint}${signRequestParams}`; res.sign = await (0, webCryptoAPI_js_1.signMessage)(paramsStr, this.apiSecret, 'base64', 'SHA-256'); res.queryParamsWithSign = signRequestParams; return res; } console.error(new Date(), (0, misc_util_js_1.neverGuard)(signMethod, `Unhandled sign method: "${webCryptoAPI_js_1.signMessage}"`)); return res; } async prepareSignParams(method, endpoint, signMethod, params, isPublicApi) { if (isPublicApi) { return { originalParams: params, paramsWithSign: params, }; } if (!this.apiKey || !this.apiSecret || !this.apiPassphrase) { 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, }; for (const key in params) { if (typeof params[key] === 'undefined') { delete params[key]; } } if (isPublicApi || !this.apiKey || !this.apiSecret) { return { ...options, params: params, }; } const signResult = await this.prepareSignParams(method, endpoint, 'kucoin', params, isPublicApi); const authHeaders = { 'KC-API-KEY': this.apiKey, 'KC-API-PARTNER': this.getClientType() === requestUtils_js_1.REST_CLIENT_TYPE_ENUM.main ? requestUtils_js_1.APIIDMain : requestUtils_js_1.APIIDFutures, 'KC-API-TIMESTAMP': signResult.timestamp, 'KC-API-KEY-VERSION': this.options.apiKeyVersion, }; const partnerSignParam = `${authHeaders['KC-API-TIMESTAMP']}${authHeaders['KC-API-PARTNER']}${authHeaders['KC-API-KEY']}`; const partnerSign = this.getClientType() === requestUtils_js_1.REST_CLIENT_TYPE_ENUM.main ? requestUtils_js_1.APIIDMainSign : requestUtils_js_1.APIIDFuturesSign; const partnerSignResult = await (0, webCryptoAPI_js_1.signMessage)(partnerSignParam, partnerSign, 'base64', 'SHA-256'); const signedPassphrase = await (0, webCryptoAPI_js_1.signMessage)(this.apiPassphrase, this.apiSecret, 'base64', 'SHA-256'); if (method === 'GET') { return { ...options, headers: { ...authHeaders, ...options.headers, 'KC-API-SIGN': signResult.sign, 'KC-API-PARTNER-SIGN': partnerSignResult, 'KC-API-PASSPHRASE': signedPassphrase, }, url: options.url + signResult.queryParamsWithSign, }; } return { ...options, headers: { ...authHeaders, ...options.headers, 'KC-API-SIGN': signResult.sign, 'KC-API-PARTNER-SIGN': partnerSignResult, 'KC-API-PASSPHRASE': signedPassphrase, }, data: params, }; } } exports.BaseRestClient = BaseRestClient; //# sourceMappingURL=BaseRestClient.js.map