"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BatchedEthereumClient = exports.EthereumClient = exports.executeBatchRequest = exports.md5 = void 0;
const tslib_1 = require("tslib");
const async_tasks_1 = require("@splunkdlt/async-tasks");
const debug_logging_1 = require("@splunkdlt/debug-logging");
const crypto_1 = tslib_1.__importDefault(require("crypto"));
const jsonrpc_1 = require("./jsonrpc");
const error_1 = require("../utils/error");
const obj_1 = require("../utils/obj");
const { info, debug, warn, error } = debug_logging_1.createModuleDebug('eth:client');
async function batchRequestWithFailover(reqs, transport, splits = 1 // 1 is no split
) {
    if (splits < 1) {
        throw new error_1.RuntimeError(`batch request splits can't be less than 1`);
    }
    const maxSplits = 10;
    if (splits <= maxSplits) {
        if (splits > 1) {
            warn('Splitting batch into %d chunks', splits);
        }
        // try to send the batch
        const chunkSize = Math.ceil(reqs.length / splits);
        const reqChunks = obj_1.chunkArray(reqs, chunkSize);
        // if it fails, split the batch and try again
        try {
            const responses = [];
            for (const chunk of reqChunks) {
                const chunkResponses = await transport.sendBatch(chunk);
                for (const response of chunkResponses) {
                    responses.push(response);
                }
            }
            return responses;
        }
        catch (e) {
            const error = e.toString();
            info('client batch request error: %s', error);
            const retryable = ['Socket timeout', 'too large', 'too big'];
            let retry = false;
            for (const retryableError of retryable) {
                if (error.includes(retryableError)) {
                    retry = true;
                }
            }
            if (retry) {
                return batchRequestWithFailover(reqs, transport, splits + 1);
            }
            return Promise.reject(e);
        }
    }
    throw new error_1.RuntimeError('Too many batch splits');
}
function md5(data) {
    const stringValue = typeof data === 'string' ? data : JSON.stringify(data);
    return crypto_1.default
        .createHash('md5')
        .update(stringValue)
        .digest('hex');
}
exports.md5 = md5;
async function executeBatchRequest(batch, transport, abortHandle, waitAfterFailure = async_tasks_1.linearBackoff({ min: 0, step: 2500, max: 120000 })) {
    debug('Processing batch of %d JSON RPC requests', batch.length);
    if (batch.length > 0) {
        try {
            // const items = new Map<string, BatchReq>()
            const reqOrder = [];
            const unique = new Map();
            const reqs = new Map();
            for (const batchItem of batch) {
                const { method, params } = batchItem.request;
                const hash = md5({ method, params });
                if (!unique.has(hash)) {
                    const req = jsonrpc_1.createJsonRpcPayload(method, params);
                    unique.set(hash, batchItem);
                    reqs.set(req.id, req);
                }
                reqOrder.push({ hash, batchItem });
            }
            const results = await async_tasks_1.retry(() => batchRequestWithFailover(Array.from(reqs.values()), transport), {
                attempts: 10,
                waitBetween: waitAfterFailure,
                taskName: `eth api batch request`,
                abortHandle: abortHandle,
                warnOnError: true,
                onError: e => {
                    info('client batch request error: %s', e.toString());
                    debug('requests: %s', Array.from(reqs.values().toString()));
                },
                onRetry: attempt => warn('Retrying eth api batch request (attempt %d)', attempt)
            });
            const resultsByHash = new Map();
            for (const result of results) {
                const req = reqs.get(result.id);
                if (req == null) {
                    error(`Found unassociated batch item in batch response`);
                    continue;
                }
                const { method, params } = req;
                const hash = md5({ method, params });
                resultsByHash.set(hash, result);
                reqs.delete(result.id);
            }
            const reqLength = reqOrder.length;
            const missingItems = [];
            for (let i = 0; i < reqLength; i++) {
                const req = reqOrder.shift();
                if (req == null) {
                    error(`error processing batch requests`);
                    continue;
                }
                const { hash, batchItem } = req;
                const result = resultsByHash.get(hash);
                if (result == null) {
                    missingItems.push(batchItem);
                    error(`expected response not found`);
                    continue;
                }
                batchItem.callback(null, result);
            }
            if (reqs.size > 0) {
                error(`Unprocessed batch request after receiving results`);
                // dump unprocessed batch items
                console.log(JSON.stringify(Array.from(reqs.entries()), null, 2));
            }
            if (missingItems.length > 0) {
                error(`Unprocessed batch request after receiving results`);
                for (const unprocessed of missingItems) {
                    unprocessed.callback(new Error('Result missing from batch response'), null);
                }
            }
        }
        catch (e) {
            batch.forEach(({ callback }) => callback(e, null));
        }
    }
}
exports.executeBatchRequest = executeBatchRequest;
class EthereumClient {
    constructor(transport) {
        this.transport = transport;
        this.abortHandle = new async_tasks_1.AbortHandle();
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async request(req, options) {
        const payload = jsonrpc_1.createJsonRpcPayload(req.method, req.params);
        const res = await this.transport.send(payload);
        if (payload.id !== res.id) {
            throw new jsonrpc_1.JsonRpcError(`JSON RPC Response ID mismatch. Expected ${payload.id} but got ${res.id}`);
        }
        if (options != null && !!options.ignoreErrors) {
            // ignore errors
        }
        else {
            jsonrpc_1.checkError(res);
        }
        return typeof req.response === 'function' ? req.response(res) : res.result;
    }
    async requestBatch(reqs) {
        const batchReqs = [];
        const promises = [];
        for (const request of reqs) {
            promises.push(new Promise((resolve, reject) => {
                batchReqs.push({
                    request,
                    callback: (e, result) => {
                        if (e) {
                            reject(e);
                        }
                        try {
                            jsonrpc_1.checkError(result);
                            resolve(typeof request.response === 'function' ? request.response(result) : result.result);
                        }
                        catch (e) {
                            debug(`JSON RPC request method: %s failed`, request.method, e);
                            reject(e);
                        }
                    }
                });
            }));
        }
        await async_tasks_1.sleep(0);
        await executeBatchRequest(batchReqs, this.transport, this.abortHandle);
        return await Promise.all(promises);
    }
}
exports.EthereumClient = EthereumClient;
class BatchedEthereumClient extends EthereumClient {
    constructor(transport, config) {
        super(transport);
        this.queue = [];
        this.flushTimer = null;
        this.processQueue = async () => {
            if (this.flushTimer != null) {
                clearTimeout(this.flushTimer);
                this.flushTimer = null;
            }
            if (this.queue.length === 0) {
                return;
            }
            const batch = this.queue;
            this.queue = [];
            return await executeBatchRequest(batch, this.transport, this.abortHandle);
        };
        this.config = config;
    }
    request(req, { immediate = false } = {}) {
        if (immediate) {
            return super.request(req);
        }
        return new Promise((resolve, reject) => {
            this.queue.push({
                request: req,
                callback: (e, result) => {
                    if (e) {
                        reject(e);
                        return;
                    }
                    try {
                        jsonrpc_1.checkError(result);
                        resolve(typeof req.response === 'function' ? req.response(result) : result.result);
                    }
                    catch (e) {
                        debug(`JSON RPC request method: %s(%o) failed`, req.method, req.params, e);
                        reject(e);
                    }
                }
            });
            this.scheduleFlush();
        });
    }
    scheduleFlush() {
        if (this.queue.length >= this.config.maxBatchSize) {
            this.processQueue();
            return;
        }
        if (this.flushTimer == null) {
            this.flushTimer = setTimeout(this.processQueue, this.config.maxBatchTime);
        }
    }
}
exports.BatchedEthereumClient = BatchedEthereumClient;
