Files
bitget-api/src/util/BaseRestClient.ts

386 lines
9.9 KiB
TypeScript

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<T extends object | undefined = {}> {
originalParams: T;
paramsWithSign?: T & { sign: string };
serializedParams: string;
sign: string;
queryParamsWithSign: string;
timestamp: number;
recvWindow: number;
}
interface UnsignedRequest<T extends object | undefined = {}> {
originalParams: T;
paramsWithSign: T;
}
type SignMethod = 'bitget';
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 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,
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',
},
};
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 endpoint API calls are automatically signed.
*/
private async _call(
method: Method,
endpoint: string,
params?: any,
isPublicApi?: boolean,
): Promise<any> {
// 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: 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,
// Prevent credentials from leaking into error messages
apiPass: 'omittedFromError',
apiSecret: 'omittedFromError',
},
};
}
/**
* @private sign request and set recv window
*/
private async signRequest<T extends object | undefined = {}>(
data: T,
endpoint: string,
method: Method,
signMethod: SignMethod,
): Promise<SignedRequest<T>> {
const timestamp = Date.now();
const res: SignedRequest<T> = {
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 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<TParams extends object | undefined>(
method: Method,
endpoint: string,
signMethod: SignMethod,
params?: TParams,
isPublicApi?: true,
): Promise<UnsignedRequest<TParams>>;
private async prepareSignParams<TParams extends object | undefined>(
method: Method,
endpoint: string,
signMethod: SignMethod,
params?: TParams,
isPublicApi?: false | undefined,
): Promise<SignedRequest<TParams>>;
private async prepareSignParams<TParams extends object | undefined>(
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<AxiosRequestConfig> {
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,
);
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,
};
}
}