"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const axios_1 = __importDefault(require("axios"));
const beautifier_1 = __importDefault(require("./beautifier"));
const requestUtils_1 = require("./requestUtils");
class BaseRestClient {
    constructor(baseUrlKey, options = {}, requestOptions = {}) {
        this.timeOffset = 0;
        this.options = Object.assign({ recvWindow: 5000, 
            // how often to sync time drift with binance servers
            syncIntervalMs: 3600000, 
            // if true, we'll throw errors if any params are undefined
            strictParamValidation: false, 
            // disable the time sync mechanism by default
            disableTimeSync: true }, options);
        this.globalRequestOptions = Object.assign({ 
            // in ms == 5 minutes by default
            timeout: 1000 * 60 * 5, headers: {
            // 'content-type': 'application/x-www-form-urlencoded';
            } }, requestOptions);
        this.key = options.api_key;
        this.secret = options.api_secret;
        if (this.key) {
            if (!this.globalRequestOptions.headers) {
                this.globalRequestOptions.headers = {};
            }
            this.globalRequestOptions.headers['X-MBX-APIKEY'] = this.key;
        }
        this.baseUrlKey = this.options.baseUrlKey || baseUrlKey;
        this.baseUrl = (0, requestUtils_1.getRestBaseUrl)(this.baseUrlKey, this.options);
        if (this.key && !this.secret) {
            throw new Error('API Key & Secret are both required for private enpoints');
        }
        if (this.options.disableTimeSync !== true) {
            this.syncTime();
            setInterval(this.syncTime.bind(this), +this.options.syncIntervalMs);
        }
        if (this.options.beautifyResponses) {
            this.beautifier = new beautifier_1.default();
        }
        this.syncTimePromise = null;
        this.apiLimitTrackers = {
            'x-mbx-used-weight': 0,
            'x-mbx-used-weight-1m': 0,
            'x-sapi-used-ip-weight-1m': 0,
            'x-mbx-order-count-1s': 0,
            'x-mbx-order-count-1m': 0,
            'x-mbx-order-count-1h': 0,
            'x-mbx-order-count-1d': 0,
        };
    }
    getBaseUrlKey() {
        return this.baseUrlKey;
    }
    getRateLimitStates() {
        return Object.assign(Object.assign({}, this.apiLimitTrackers), { lastUpdated: this.apiLimitLastUpdated });
    }
    /**
     * Return time sync offset, automatically set if time sync is enabled. A higher offset means system clock is behind server time.
     */
    getTimeOffset() {
        return this.timeOffset;
    }
    setTimeOffset(value) {
        this.timeOffset = value;
    }
    get(endpoint, params) {
        return this._call('GET', endpoint, params);
    }
    getForBaseUrl(endpoint, baseUrlKey, params) {
        const baseUrl = (0, requestUtils_1.getRestBaseUrl)(baseUrlKey, {});
        return this._call('GET', endpoint, params, false, baseUrl);
    }
    getPrivate(endpoint, params) {
        return this._call('GET', endpoint, params, true);
    }
    post(endpoint, params) {
        return this._call('POST', endpoint, params);
    }
    postPrivate(endpoint, params) {
        return this._call('POST', endpoint, params, true);
    }
    put(endpoint, params) {
        return this._call('PUT', endpoint, params);
    }
    putPrivate(endpoint, params) {
        return this._call('PUT', endpoint, params, true);
    }
    delete(endpoint, params) {
        return this._call('DELETE', endpoint, params);
    }
    deletePrivate(endpoint, params) {
        return this._call('DELETE', endpoint, params, true);
    }
    /**
     * @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed.
     */
    _call(method, endpoint, params, isPrivate, baseUrlOverride) {
        return __awaiter(this, void 0, void 0, function* () {
            const timestamp = Date.now() + (this.getTimeOffset() || 0);
            if (isPrivate && (!this.key || !this.secret)) {
                throw new Error('Private endpoints require api and private keys to be set');
            }
            // Handles serialisation of params into query string (url?key1=value1&key2=value2), handles encoding of values, adds timestamp and signature to request.
            const { serialisedParams, signature, requestBody } = yield (0, requestUtils_1.getRequestSignature)(params, this.key, this.secret, this.options.recvWindow, timestamp, this.options.strictParamValidation, this.options.filterUndefinedParams);
            const baseUrl = baseUrlOverride || this.baseUrl;
            const options = Object.assign(Object.assign({}, this.globalRequestOptions), { url: [baseUrl, endpoint].join('/'), method: method, json: true });
            if (isPrivate) {
                options.url +=
                    '?' + [serialisedParams, 'signature=' + signature].join('&');
            }
            else if (method === 'GET' || method === 'DELETE') {
                options.params = params;
            }
            else {
                options.data = (0, requestUtils_1.serialiseParams)(requestBody, this.options.strictParamValidation, true);
            }
            // console.log(
            //   'sending request: ',
            //   JSON.stringify(
            //     {
            //       serialisedParams,
            //       requestBody,
            //       signature,
            //       reqOptions: options,
            //       reqParams: params,
            //     },
            //     null,
            //     2,
            //   ),
            // );
            return (0, axios_1.default)(options)
                .then((response) => {
                this.updateApiLimitState(response.headers);
                if (response.status == 200) {
                    return response.data;
                }
                throw response;
            })
                .then((response) => {
                if (!this.options.beautifyResponses || !this.beautifier) {
                    return response;
                }
                // Fallback to original response if beautifier fails
                try {
                    return this.beautifier.beautify(response, endpoint) || response;
                }
                catch (e) {
                    console.error('BaseRestClient response beautify failed: ', JSON.stringify({ response: response, error: e }));
                }
                return response;
            })
                .catch((e) => this.parseException(e, options.url));
        });
    }
    /**
     * @private generic handler to parse request exceptions
     */
    parseException(e, url) {
        var _a, _b;
        const { response, request, message } = e;
        if (response && response.headers) {
            this.updateApiLimitState(response.headers);
        }
        if (this.options.parseExceptions === false) {
            throw e;
        }
        // Something happened in setting up the request that triggered an Error
        if (!response) {
            if (!request) {
                throw 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
        throw {
            code: (_a = response.data) === null || _a === void 0 ? void 0 : _a.code,
            message: (_b = response.data) === null || _b === void 0 ? void 0 : _b.msg,
            body: response.data,
            headers: response.headers,
            requestUrl: url,
            requestBody: request.body,
            requestOptions: Object.assign(Object.assign({}, this.options), { api_key: undefined, api_secret: undefined }),
        };
    }
    updateApiLimitState(responseHeaders) {
        const delta = {};
        for (const headerKey in this.apiLimitTrackers) {
            const headerValue = responseHeaders[headerKey];
            const value = parseInt(headerValue);
            if (headerValue !== undefined && !isNaN(value)) {
                // TODO: track last seen by key? insetad of all? some keys not returned by some endpoints more useful in estimating whether reset should've happened
                this.apiLimitTrackers[headerKey] = value;
                delta[headerKey] = {
                    updated: true,
                    valueParsed: value,
                    valueRaw: headerValue,
                };
            }
            else {
                delta[headerKey] = {
                    updated: false,
                    valueParsed: value,
                    valueRaw: headerValue,
                };
            }
        }
        // console.log('responseHeaders: ', requestedUrl);
        // console.table(responseHeaders);
        // console.table(delta);
        this.apiLimitLastUpdated = new Date().getTime();
    }
    /**
     * Trigger time sync and store promise
     */
    syncTime() {
        if (this.options.disableTimeSync === true) {
            return Promise.resolve();
        }
        if (this.syncTimePromise !== null) {
            return this.syncTimePromise;
        }
        this.syncTimePromise = this.fetchTimeOffset().then((offset) => {
            this.timeOffset = offset;
            this.syncTimePromise = null;
        });
        return this.syncTimePromise;
    }
    /**
     * Estimate drift based on client<->server latency
     */
    fetchTimeOffset() {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const start = Date.now();
                const serverTime = yield this.getServerTime();
                const end = Date.now();
                const avgDrift = (end - start) / 2;
                return Math.ceil(serverTime - end + avgDrift);
            }
            catch (e) {
                console.error('Failed to fetch get time offset: ', e);
                return 0;
            }
        });
    }
}
exports.default = BaseRestClient;
//# sourceMappingURL=BaseRestClient.js.map