"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UtxoCore = void 0;
const axios_1 = __importDefault(require("axios"));
const axios_rate_limit_1 = __importDefault(require("../axios-rate-limiter/axios-rate-limit"));
const UtxoStatus_1 = require("../base-objects/status/UtxoStatus");
const genericMccTypes_1 = require("../types/genericMccTypes");
const constants_1 = require("../utils/constants");
const errors_1 = require("../utils/errors");
const utils_1 = require("../utils/utils");
const utxoUtils_1 = require("../utils/utxoUtils");
const DEFAULT_TIMEOUT = 60000;
const DEFAULT_RATE_LIMIT_OPTIONS = {
    maxRPS: 5,
};
class UtxoCore {
    constructor(createConfig, constructors) {
        var _a;
        const client = axios_1.default.create({
            baseURL: createConfig.url,
            timeout: ((_a = createConfig.rateLimitOptions) === null || _a === void 0 ? void 0 : _a.timeoutMs) || DEFAULT_TIMEOUT,
            headers: {
                "Content-Type": "application/json",
                "x-apikey": createConfig.apiTokenKey || "",
            },
            auth: {
                username: createConfig.username,
                password: createConfig.password,
            },
            validateStatus: function (status) {
                return (status >= 200 && status < 300) || status == 500;
            },
        });
        this.client = (0, axios_rate_limit_1.default)(client, Object.assign(Object.assign({}, DEFAULT_RATE_LIMIT_OPTIONS), createConfig.rateLimitOptions));
        this.inRegTest = createConfig.inRegTest || false;
        // This has to be shadowed
        this.constructors = constructors;
        this.chainType = genericMccTypes_1.ChainType.BTC;
    }
    /**
     * Return node status object for Utxo nodes
     */
    async getNodeStatus() {
        const res = await this.getNetworkInfo();
        const infoRes = await this.client.post(``, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "getblockchaininfo",
            params: [],
        });
        (0, utxoUtils_1.utxo_ensure_data)(infoRes.data);
        return new UtxoStatus_1.UtxoNodeStatus(Object.assign(Object.assign({}, infoRes.data.result), res.result));
    }
    /**
     * On Utxo chains nodes always need full history
     * @returns 0
     */
    async getBottomBlockHeight() {
        return 0;
    }
    ///////////////////////////////////////////////////////////////////////////////////////
    // Read part of RPC Client ////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////
    // Block methods //////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////
    async blockRequestBase(blockNumberOrHash, full) {
        let blockHash;
        if (typeof blockNumberOrHash === "string") {
            blockHash = blockNumberOrHash;
        }
        else if (typeof blockNumberOrHash === "number") {
            try {
                blockHash = await this.getBlockHashFromHeight(blockNumberOrHash);
            }
            catch (e) {
                throw new errors_1.mccError(errors_1.mccErrorCode.InvalidParameter);
            }
        }
        else {
            // This type is not supported
            throw new errors_1.mccError(errors_1.mccErrorCode.InvalidParameter);
        }
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        let params = [blockHash, full ? 3 : 1];
        if (this.chainType === genericMccTypes_1.ChainType.DOGE) {
            params = [blockHash, true];
        }
        return await this.client.post("", {
            jsonrpc: "1.0",
            id: "rpc",
            method: "getblock",
            params: params,
        });
    }
    async getFullBlock(blockNumberOrHash) {
        const res = await this.blockRequestBase(blockNumberOrHash, true);
        if ((0, utxoUtils_1.utxo_check_expect_block_out_of_range)(res.data)) {
            throw new errors_1.mccError(errors_1.mccErrorCode.InvalidBlock);
        }
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return new this.constructors.fullBlockConstructor(res.data.result);
    }
    /**
     * Returns the block information
     * @param blockHashOrHeight Provide either block hash or height of the block
     * @returns All available block information
     */
    //
    async getBlock(blockNumberOrHash) {
        const res = await this.blockRequestBase(blockNumberOrHash, true);
        if ((0, utxoUtils_1.utxo_check_expect_block_out_of_range)(res.data)) {
            throw new errors_1.mccError(errors_1.mccErrorCode.InvalidBlock);
        }
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return new this.constructors.blockConstructor(res.data.result);
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    async getBlockHeader(blockNumberOrHash) {
        const header = await this.getBlockHeaderBase(blockNumberOrHash);
        return new this.constructors.blockHeaderConstructor(header);
    }
    /**
     * Get Block height (number of blocks) from connected chain
     * @returns block height (block count)
     */
    async getBlockHeight() {
        const res = await this.client.post("", {
            jsonrpc: "1.0",
            id: "rpc",
            method: "getblockcount",
            params: [],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
    ///////////////////////////////////////////////////////////////////////////////////////
    // Transaction methods ////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////
    /**
     * Get transaction details
     * @param txId Transaction id
     * @param options provide verbose:boolean, set true if you want more info such as block hash...
     * @returns transaction details
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async getTransaction(txId, options) {
        if (constants_1.PREFIXED_STD_TXID_REGEX.test(txId)) {
            txId = (0, utils_1.unPrefix0x)(txId);
        }
        // TODO trow if txid does not match expected input
        const unTxId = (0, utils_1.unPrefix0x)(txId);
        let params = [unTxId, 2];
        if (this.chainType === genericMccTypes_1.ChainType.DOGE) {
            params = [unTxId, true];
        }
        const res = await this.client.post("", {
            jsonrpc: "1.0",
            id: "rpc",
            method: "getrawtransaction",
            params: params,
        });
        // Error codes https://github.com/bitcoin/bitcoin/blob/master/src/rpc/protocol.h
        if ((0, utxoUtils_1.utxo_check_expect_empty)(res.data)) {
            throw new errors_1.mccError(errors_1.mccErrorCode.InvalidTransaction);
        }
        // It transaction number of confirmations is not at least 1, we got a transaction from mempool, we don't consider this transaction as valid
        if (res.data && res.data.result && res.data.result.confirmations && res.data.result.confirmations < 1) {
            throw new errors_1.mccError(errors_1.mccErrorCode.InvalidTransaction);
        }
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return new this.constructors.transactionConstructor(res.data.result);
    }
    ///////////////////////////////////////////////////////////////////////////////////////
    // Client specific methods ////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////
    /**
     * Get header information about the block
     * @param blockHash
     * @returns
     */
    async getBlockHeaderBase(blockHashOrHeight) {
        let blockHash = null;
        if (typeof blockHashOrHeight === "string") {
            blockHash = blockHashOrHeight;
            if (constants_1.PREFIXED_STD_BLOCK_HASH_REGEX.test(blockHash)) {
                blockHash = (0, utils_1.unPrefix0x)(blockHash);
            }
            // TODO match with some regex
        }
        if (typeof blockHashOrHeight === "number") {
            blockHash = await this.getBlockHashFromHeight(blockHashOrHeight);
        }
        const res = await this.client.post("", {
            jsonrpc: "1.0",
            id: "rpc",
            method: "getblockheader",
            params: [blockHash],
        });
        if ((0, utxoUtils_1.utxo_check_expect_empty)(res.data)) {
            throw new errors_1.mccError(errors_1.mccErrorCode.InvalidData);
        }
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
    /**
     * Get only tips of all the branches
     * @param height_gte
     * @returns
     */
    async getBlockTips(height_gte) {
        return this.getBlockTipsHelper(height_gte);
    }
    /**
     * Get block tips and full block chain for all tips (all of heir branches) for the last x blocks (defined bt branch_len parameter)
     * @param branch_len the branch length indicating how long can branches be
     * @returns Array of LiteBlocks
     */
    async getTopLiteBlocks(branch_len, read_main = true) {
        const height = await this.getBlockHeight();
        let callBranchLength = undefined;
        if (read_main) {
            callBranchLength = branch_len;
        }
        return this.getBlockTipsHelper(height - branch_len, callBranchLength);
    }
    /**
     * Get an array of all alternative chain tips
     * @returns
     */
    async getTopBlocks(option) {
        const res = await this.client.post(``, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "getchaintips",
            params: [],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        const gte_height = (option === null || option === void 0 ? void 0 : option.height_gte) || 0;
        const response = res.data.result.filter((el) => {
            return el.height >= gte_height;
        });
        if (option !== undefined) {
            if (option.all_blocks !== undefined) {
                let extended = response.map((el) => this.recursive_block_hash(el.hash, el.branchlen));
                extended = await Promise.all(extended);
                for (let i = 0; i < response.length; i++) {
                    response[i].all_block_hashes = extended[i];
                }
            }
        }
        return response;
    }
    ///////////////////////////////////////////////////////////////////////////////////////
    // Client helper (private) methods ////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////
    /**
     * Get network info
     * @returns network info details
     */
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    async getNetworkInfo() {
        const res = await this.client.post("", {
            jsonrpc: "1.0",
            id: "rpc",
            method: "getnetworkinfo",
            params: [],
        });
        // Error codes https://github.com/bitcoin/bitcoin/blob/master/src/rpc/protocol.h
        if ((0, utxoUtils_1.utxo_check_expect_empty)(res.data)) {
            return null;
        }
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data;
    }
    /**
     * Gets the block from main mining tip with provided height
     * @param blockNumber Block height
     * @returns Block hash
     */
    async getBlockHashFromHeight(blockNumber) {
        const res = await this.client.post("", {
            jsonrpc: "1.0",
            id: "rpc",
            method: "getblockhash",
            params: [blockNumber],
        });
        if ((0, utxoUtils_1.utxo_check_expect_block_out_of_range)(res.data)) {
            throw new errors_1.mccError(errors_1.mccErrorCode.InvalidBlock);
        }
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
    /**
     *
     * @param height_gte
     * @param mainBranchProcess
     * @returns
     */
    async getBlockTipsHelper(height_gte, mainBranchProcess) {
        const tips = await this.getTopBlocks({ height_gte: height_gte, all_blocks: true });
        let mainBranchHashes = [];
        const activeTip = tips.filter((a) => a.status === "active")[0];
        const ActiveTip = new this.constructors.blockTipConstructor({
            hash: activeTip.hash,
            height: activeTip.height,
            branchlen: activeTip.branchlen,
            status: activeTip.status,
        });
        if (mainBranchProcess !== undefined) {
            mainBranchHashes = await this.recursive_block_tip(ActiveTip, mainBranchProcess);
        }
        const allTips = tips.map((UtxoTip) => {
            const tempTips = [];
            // all_block_hashes exist due to all_blocks: true in getTopBlocks call
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            for (let hashIndex = 0; hashIndex < UtxoTip.all_block_hashes.length; hashIndex++) {
                tempTips.push(new this.constructors.blockTipConstructor({
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    hash: UtxoTip.all_block_hashes[hashIndex],
                    height: UtxoTip.height - hashIndex,
                    branchlen: UtxoTip.branchlen,
                    status: UtxoTip.status,
                }));
            }
            return tempTips;
        });
        // filter out duplicates
        const reducedTips = allTips.reduce((acc, nev) => acc.concat(nev), []).concat(mainBranchHashes);
        const unique = new Set();
        return reducedTips.filter((elem) => {
            const key = `${elem.number}_${elem.blockHash}`;
            if (unique.has(key)) {
                return false;
            }
            else {
                unique.add(key);
                return true;
            }
        });
    }
    ///////////////////////////////////////////////////////////////////////////////////////
    // Write part of RPC Client ///////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////
    // Not yet tested or used
    /**
     * Creates a new wallet on node's database
     * @param walletLabel label of your wallet used as a reference for future use
     * @returns name of the created wallet and possible warnings
     */
    /* istanbul ignore next */
    async createWallet(walletLabel) {
        const res = await this.client.post("", {
            jsonrpc: "1.0",
            id: "rpc",
            method: "createwallet",
            params: [walletLabel],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        // TODO try to import wallet if it already exists but is not imported
        return res.data.result;
    }
    /**
     * loads the wallet if it exist on node, but it has to be relisted
     * @param walletLabel wallet label to load
     * @returns
     */
    /* istanbul ignore next */
    async loadWallet(walletLabel) {
        const res = await this.client.post("", {
            jsonrpc: "1.0",
            id: "rpc",
            method: "loadwallet",
            params: [walletLabel],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
    /**
     *
     * @param walletLabel label of wallet, if you dont have one create one with createWallet
     * @param label label of address within wallet (default to "")
     * @param address_type type of address (default to "legacy") options = ["legacy", "p2sh-segwit", "bech32"]
     * @returns
     */
    /* istanbul ignore next */
    async createAddress(walletLabel, addressLabel = "", address_type = "legacy") {
        const res = await this.client.post(`wallet/${walletLabel}`, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "getnewaddress",
            params: [addressLabel, address_type],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
    /**
     * List all wallets on node
     * @returns
     */
    /* istanbul ignore next */
    async listAllWallets() {
        const res = await this.client.post(``, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "listwallets",
            params: [],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
    /**
     * List all addresses by their label on provided wallet
     * @param walletLabel label of the parent wallet we want to list addresses
     * @param addressLabel label of the addresses we want to list
     * @returns
     */
    /* istanbul ignore next */
    async listAllAddressesByLabel(walletLabel, addressLabel = "") {
        const res = await this.client.post(`wallet/${walletLabel}`, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "getaddressesbylabel",
            params: [addressLabel],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        const address_labels = Object.keys(res.data.result);
        const response_array = [];
        for (const addL of address_labels) {
            response_array.push({ address: addL, purpose: res.data.result[addL].purpose });
        }
        return response_array;
    }
    /**
     * List all unspent transactions that happened between min and max blocks before current block
     * If we are in block 100 and set min to 10 and max to 40 we will get all transactions that happened
     * between block 60 and 90
     * @param walletLabel
     * @param min min block offset
     * @param max max block offset
     * @returns
     */
    /* istanbul ignore next */
    async listUnspentTransactions(walletLabel, min = 0, max = 1e6) {
        const res = await this.client.post(`wallet/${walletLabel}`, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "listunspent",
            params: [min, max],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
    /* istanbul ignore next */
    async createRawTransaction(walletLabel, vin, out) {
        let voutArr = "[";
        let first = true;
        for (const i of out) {
            if (first) {
                first = false;
            }
            else {
                voutArr += ",";
            }
            const row = `{"${i.address}" : ${i.amount}}`;
            voutArr += row;
        }
        voutArr += "]";
        const VoutArr = JSON.parse(voutArr);
        const res = await this.client.post(`wallet/${walletLabel}`, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "createrawtransaction",
            params: [vin, VoutArr],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
    /* istanbul ignore next */
    async signRawTransaction(walletLabel, rawTx, keysList) {
        const res = await this.client.post(`wallet/${walletLabel}`, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "signrawtransactionwithkey",
            params: [rawTx, keysList],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
    /**
     * Send raw transaction
     * @param walletLabel the label of the wallet we are sending the transaction from
     * @param signedRawTx hash of signed transaction
     * @returns transaction sending status
     */
    /* istanbul ignore next */
    async sendRawTransaction(walletLabel, signedRawTx) {
        const res = await this.client.post(`wallet/${walletLabel}`, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "sendrawtransaction",
            params: [signedRawTx],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
    /**
     * Send raw transaction and wait for it to be in next block
     * @param walletLabel the label of the wallet we are sending the transaction from
     * @param signedRawTx hash of signed transaction
     * @returns transaction sending status
     */
    /* istanbul ignore next */
    async sendRawTransactionInBlock(walletLabel, signedRawTx) {
        const res = await this.client.post(`wallet/${walletLabel}`, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "sendrawtransaction",
            params: [signedRawTx],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        let tx = await this.getTransaction(res.data.result);
        while (res.data.blockhash) {
            await (0, utils_1.sleepMs)(3000);
            tx = await this.getTransaction(res.data.result);
        }
        return res.data.result;
    }
    /**
     * Get private key from wallet
     * @notice Dont share this with anyone
     * @param walletLabel wallet label that owns the address
     * @param address
     * @returns private key
     */
    /* istanbul ignore next */
    async getPrivateKey(walletLabel, address) {
        const res = await this.client.post(`wallet/${walletLabel}`, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "dumpprivkey",
            params: [address],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
    ///////////////////////////////////////////////////////////////////////////////////////
    // Block tip recursive class methods //////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////
    async recursive_block_hash(hash, processHeight) {
        if (hash === "") {
            return [];
        }
        if (processHeight <= 1) {
            return [hash];
        }
        else {
            const Cblock = await this.getBlockHeader(hash);
            const hs = Cblock.previousBlockHash;
            return (await this.recursive_block_hash(hs, processHeight - 1)).concat([hash]);
        }
    }
    async recursive_block_tip(tip, processHeight) {
        if (tip.stdBlockHash === "") {
            return [];
        }
        const tempTip = new this.constructors.blockTipConstructor({
            hash: tip.stdBlockHash,
            height: tip.number,
            branchlen: tip.branchLen,
            status: tip.chainTipStatus,
        });
        if (processHeight <= 1) {
            return [tempTip];
        }
        else {
            const CurrBlock = await this.getBlockHeader(tip.stdBlockHash);
            const previousHash = CurrBlock.previousBlockHash;
            const previousHeight = CurrBlock.number - 1;
            return (await this.recursive_block_tip(new this.constructors.blockTipConstructor({
                hash: previousHash,
                height: previousHeight,
                branchlen: tip.branchLen,
                status: tip.chainTipStatus,
            }), processHeight - 1)).concat([tempTip]);
        }
    }
    ///////////////////////////////////////////////////////////////////////////////////////
    // Regtest faucet part of RPC Client //////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////
    /**
     *
     * @dev Note that miner has to be registered in miner wallet
     * @dev only in regtest mode
     * @param address
     * @param amount
     * @returns
     */
    /* istanbul ignore next */
    async fundAddress(address, amount) {
        if (!this.inRegTest) {
            throw Error("You have to run client in regression test mode to use this ");
        }
        const res = await this.client.post(`wallet/miner`, {
            jsonrpc: "1.0",
            id: "rpc",
            method: "sendtoaddress",
            params: [address, amount],
        });
        (0, utxoUtils_1.utxo_ensure_data)(res.data);
        return res.data.result;
    }
}
exports.UtxoCore = UtxoCore;
//# sourceMappingURL=UtxoCore.js.map