UNPKG

bitget-api

Version:

Complete Node.js & JavaScript SDK for Bitget V1-V3 REST APIs & WebSockets, with TypeScript & end-to-end tests.

277 lines 11.1 kB
import axios from 'axios'; import https from 'https'; import { getRestBaseUrl, serializeParams, } from './requestUtils.js'; import { checkWebCryptoAPISupported, signMessage, } from './webCryptoAPI.js'; import { neverGuard } from './websocket-util.js'; const ENABLE_HTTP_TRACE = typeof process === 'object' && typeof process.env === 'object' && process.env.BITGETTRACE; if (ENABLE_HTTP_TRACE) { axios.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.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; }); } export default class BaseRestClient { options; baseUrl; globalRequestOptions; apiKey; apiSecret; apiPass; /** * 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.options = { recvWindow: 5000, /** Throw errors if any request params are empty */ strictParamValidation: false, encodeQueryStringValues: true, ...restOptions, }; 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: { 'X-CHANNEL-API-CODE': 'hbnni', 'Content-Type': 'application/json', locale: 'en-US', }, }; if (this.options.demoTrading) { this.globalRequestOptions.headers = { ...this.globalRequestOptions.headers, // Header to enable paper trading with provided demo API keys paptrading: '1', }; } // 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.Agent({ ...existingAgentOptions, keepAlive: true, keepAliveMsecs: this.options.keepAliveMsecs, }); } this.baseUrl = getRestBaseUrl(false, restOptions); this.apiKey = this.options.apiKey; this.apiSecret = this.options.apiSecret; this.apiPass = this.options.apiPass; // Throw if one of the 3 values is missing, but at least one of them is set const credentials = [this.apiKey, this.apiSecret, this.apiPass]; if (credentials.includes(undefined) && credentials.some((v) => typeof v === 'string')) { throw new Error('API Key, Secret & Passphrase are ALL required to use the authenticated REST client'); } // Check Web Crypto API support when credentials are provided and no custom sign function is used if (this.apiKey && this.apiSecret && this.apiPass && !this.options.customSignMessageFn) { // Provide a user friendly error message if the user is using an outdated Node.js version (where Web Crypto API is not available). // A few users have been caught out by using the end-of-life Node.js v18 release. checkWebCryptoAPISupported(); } } 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); } /** * @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 axios(options) .then((response) => { if (response.status == 200) { if (typeof response.data?.code === 'string' && response.data?.code !== '00000') { throw { response }; } return response.data; } throw { response }; }) .catch((e) => this.parseException(e)); } /** * @private generic handler to parse request exceptions */ parseException(e) { 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 apiPass: 'omittedFromError', apiSecret: 'omittedFromError', }, }; } async signMessage(paramsStr, secret, method, algorithm) { if (typeof this.options.customSignMessageFn === 'function') { return this.options.customSignMessageFn(paramsStr, secret); } return await signMessage(paramsStr, secret, method, algorithm); } /** * @private sign request and set recv window */ async signRequest(data, endpoint, method, signMethod) { const timestamp = Date.now(); const res = { originalParams: { ...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 = this.options.encodeQueryStringValues; if (signMethod === 'bitget') { const signRequestParams = method === 'GET' ? serializeParams(data, strictParamValidation, encodeQueryStringValues, '?') : JSON.stringify(data) || ''; const paramsStr = timestamp + method.toUpperCase() + endpoint + signRequestParams; // console.log('sign params: ', paramsStr); res.sign = await this.signMessage(paramsStr, this.apiSecret, 'base64', 'SHA-256'); res.queryParamsWithSign = signRequestParams; return res; } console.error(new Date(), neverGuard(signMethod, `Unhandled sign method: "${signMessage}"`)); return res; } async prepareSignParams(method, endpoint, signMethod, params, isPublicApi) { if (isPublicApi) { return { originalParams: params, paramsWithSign: params, }; } if (!this.apiKey || !this.apiSecret) { throw new Error('Private endpoints require api and private keys set'); } 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.apiPass) { return { ...options, params: params, }; } const signResult = await this.prepareSignParams(method, endpoint, 'bitget', params, isPublicApi); const authHeaders = { 'ACCESS-KEY': this.apiKey, 'ACCESS-PASSPHRASE': this.apiPass, 'ACCESS-TIMESTAMP': signResult.timestamp, 'ACCESS-SIGN': signResult.sign, }; if (method === 'GET') { return { ...options, headers: { ...authHeaders, ...options.headers, }, url: options.url + signResult.queryParamsWithSign, }; } return { ...options, headers: { ...authHeaders, ...options.headers, }, data: params, }; } } //# sourceMappingURL=BaseRestClient.js.map