import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios'; import { RestClientType } from '../types'; import { signMessage } from './node-support'; import { RestClientOptions, serializeParams, getRestBaseUrl, } from './requestUtils'; import { neverGuard } from './websocket-util'; interface SignedRequest { originalParams: T; paramsWithSign?: T & { sign: string }; serializedParams: string; sign: string; queryParamsWithSign: string; timestamp: number; recvWindow: number; } interface UnsignedRequest { originalParams: T; paramsWithSign: T; } type SignMethod = 'bitget'; export default abstract class BaseRestClient { private options: RestClientOptions; private baseUrl: string; private globalRequestOptions: AxiosRequestConfig; private apiKey: string | undefined; private apiSecret: string | undefined; private apiPass: string | undefined; /** Defines the client type (affecting how requests & signatures behave) */ abstract getClientType(): RestClientType; /** * 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: RestClientOptions = {}, networkOptions: AxiosRequestConfig = {} ) { this.options = { recvWindow: 5000, /** Throw errors if any request params are empty */ strictParamValidation: false, ...restOptions, }; this.globalRequestOptions = { // in ms == 5 minutes by default timeout: 1000 * 60 * 5, // custom request options based on axios specs - see: https://github.com/axios/axios#request-config ...networkOptions, headers: { 'X-CHANNEL-API-CODE': '3tem', 'Content-Type': 'application/json', locale: 'en-US', }, }; 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' ); } } get(endpoint: string, params?: any) { return this._call('GET', endpoint, params, true); } getPrivate(endpoint: string, params?: any) { return this._call('GET', endpoint, params, false); } post(endpoint: string, params?: any) { return this._call('POST', endpoint, params, true); } postPrivate(endpoint: string, params?: any) { return this._call('POST', endpoint, params, false); } deletePrivate(endpoint: string, params?: any) { return this._call('DELETE', endpoint, params, false); } /** * @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed. */ private async _call( method: Method, endpoint: string, params?: any, isPublicApi?: boolean ): Promise { // 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 ); // console.log('full request: ', options); // Dispatch request return axios(options) .then((response) => { // console.log('response: ', response.data); // console.error('res: ', response); // if (response.data && response.data?.code !== API_ERROR_CODE.SUCCESS) { // throw response.data; // } 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: any): unknown { 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: AxiosResponse = e.response; // console.error('err: ', response?.data); throw { code: response.status, message: response.statusText, body: response.data, headers: response.headers, requestOptions: { ...this.options, apiPass: 'omittedFromError', apiSecret: 'omittedFromError', }, }; } /** * @private sign request and set recv window */ private async signRequest( data: T, endpoint: string, method: Method, signMethod: SignMethod ): Promise> { const timestamp = Date.now(); const res: SignedRequest = { 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; if (signMethod === 'bitget') { const signRequestParams = method === 'GET' ? serializeParams(data, strictParamValidation, '?') : JSON.stringify(data) || ''; const paramsStr = timestamp + method.toUpperCase() + endpoint + signRequestParams; // console.log('sign params: ', paramsStr); res.sign = await signMessage(paramsStr, this.apiSecret, 'base64'); res.queryParamsWithSign = signRequestParams; return res; } console.error( new Date(), neverGuard(signMethod, `Unhandled sign method: "${signMessage}"`) ); return res; } private async prepareSignParams( method: Method, endpoint: string, signMethod: SignMethod, params?: TParams, isPublicApi?: true ): Promise>; private async prepareSignParams( method: Method, endpoint: string, signMethod: SignMethod, params?: TParams, isPublicApi?: false | undefined ): Promise>; private async prepareSignParams( method: Method, endpoint: string, signMethod: SignMethod, params?: TParams, isPublicApi?: boolean ) { 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 */ private async buildRequest( method: Method, endpoint: string, url: string, params?: any, isPublicApi?: boolean ): Promise { const options: AxiosRequestConfig = { ...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 ); if (!options.headers) { options.headers = {}; } options.headers['ACCESS-KEY'] = this.apiKey; options.headers['ACCESS-PASSPHRASE'] = this.apiPass; options.headers['ACCESS-TIMESTAMP'] = signResult.timestamp; options.headers['ACCESS-SIGN'] = signResult.sign; options.headers['Content-Type'] = 'application/json'; if (method === 'GET') { return { ...options, url: options.url + signResult.queryParamsWithSign, }; } return { ...options, data: params, }; } }