import axios, { AxiosInstance } from "axios"; import { BlockBase, BlockHeaderBase, BlockTipBase, UtxoTransaction } from "../base-objects"; import { UtxoBlockTip } from "../base-objects/blockTips/UtxoBlockTip"; import { FullBlockBase } from "../base-objects/FullBlockBase"; import { UtxoNodeStatus } from "../base-objects/status/UtxoStatus"; import { UtxoMccCreate } from "../types"; import { ChainType, getTransactionOptions, ReadRpcInterface } from "../types/genericMccTypes"; import { IUtxoChainTip, IUtxoGetAlternativeBlocksOptions, IUtxoGetAlternativeBlocksRes, IUtxoGetBlockHeaderRes, IUtxoGetBlockRes, IUtxoGetTransactionRes, IUtxoNodeStatus, IUtxoTransactionAdditionalData, } from "../types/utxoTypes"; import { PREFIXED_STD_BLOCK_HASH_REGEX, PREFIXED_STD_TXID_REGEX } from "../utils/constants"; import { mccError, mccErrorCode } from "../utils/errors"; import { unPrefix0x } from "../utils/utils"; import { utxo_check_expect_block_out_of_range, utxo_check_expect_empty, utxo_ensure_data } from "../utils/utxoUtils"; const DEFAULT_TIMEOUT = 60000; export interface objectConstructors< BTipCon extends BlockTipBase, BHeadCon extends BlockHeaderBase, BlockCon extends BlockBase, FBlockCon extends FullBlockBase, TranCon extends UtxoTransaction, > { transactionConstructor: new (d: IUtxoGetTransactionRes, a?: IUtxoTransactionAdditionalData) => TranCon; fullBlockConstructor: new (d: IUtxoGetBlockRes) => FBlockCon; blockConstructor: new (d: IUtxoGetBlockRes) => BlockCon; blockHeaderConstructor: new (d: IUtxoGetBlockHeaderRes) => BHeadCon; blockTipConstructor: new (d: IUtxoChainTip) => BTipCon; } export abstract class UtxoCore< BTipCon extends UtxoBlockTip, BHeadCon extends BlockHeaderBase, BlockCon extends BlockBase, FBlockCon extends FullBlockBase, TranCon extends UtxoTransaction, > implements ReadRpcInterface { client: AxiosInstance; inRegTest: boolean; constructors: objectConstructors; chainType: ChainType; constructor( createConfig: UtxoMccCreate, constructors: objectConstructors ) { this.client = axios.create({ baseURL: createConfig.url, timeout: DEFAULT_TIMEOUT, headers: { "Content-Type": "application/json", "x-apikey": createConfig.apiTokenKey || "", }, auth: { username: createConfig.username, password: createConfig.password, }, validateStatus: function (status: number) { return (status >= 200 && status < 300) || status === 500; }, }); this.inRegTest = createConfig.inRegTest || false; // This has to be shadowed this.constructors = constructors; this.chainType = ChainType.BTC; } /** * Return node status object for Utxo nodes */ async getNodeStatus(): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const res = await this.getNetworkInfo(); const infoRes = await this.client.post(``, { jsonrpc: "1.0", id: "rpc", method: "getblockchaininfo", params: [], }); utxo_ensure_data(infoRes.data); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return new UtxoNodeStatus({ ...infoRes.data.result, ...res.result } as IUtxoNodeStatus); } /** * On Utxo chains nodes always need full history * @returns 0 */ // eslint-disable-next-line @typescript-eslint/require-await async getBottomBlockHeight(): Promise { return 0; } /////////////////////////////////////////////////////////////////////////////////////// // Read part of RPC Client //////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////// // Block methods ////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////// private async blockRequestBase(blockNumberOrHash: string | number, full: boolean) { let blockHash: string; if (typeof blockNumberOrHash === "string") { blockHash = blockNumberOrHash; } else if (typeof blockNumberOrHash === "number") { try { blockHash = await this.getBlockHashFromHeight(blockNumberOrHash); } catch { throw new mccError(mccErrorCode.InvalidParameter); } } else { // This type is not supported throw new mccError(mccErrorCode.InvalidParameter); } // eslint-disable-next-line @typescript-eslint/no-explicit-any let params: any[] = [blockHash, full ? 3 : 1]; if (this.chainType === ChainType.DOGE) { params = [blockHash, true]; } return await this.client.post("", { jsonrpc: "1.0", id: "rpc", method: "getblock", params: params, }); } async getFullBlock(blockNumberOrHash: string | number): Promise { const res = await this.blockRequestBase(blockNumberOrHash, true); if (utxo_check_expect_block_out_of_range(res.data)) { throw new mccError(mccErrorCode.InvalidBlock); } utxo_ensure_data(res.data); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument 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: string | number): Promise { const res = await this.blockRequestBase(blockNumberOrHash, true); if (utxo_check_expect_block_out_of_range(res.data)) { throw new mccError(mccErrorCode.InvalidBlock); } utxo_ensure_data(res.data); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument return new this.constructors.blockConstructor(res.data.result); } // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-redundant-type-constituents async getBlockHeader(blockNumberOrHash: number | string | any): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 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(): Promise { const res = await this.client.post("", { jsonrpc: "1.0", id: "rpc", method: "getblockcount", params: [], }); utxo_ensure_data(res.data); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return 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 */ async getTransaction(txId: string, options?: getTransactionOptions): Promise { if (PREFIXED_STD_TXID_REGEX.test(txId)) { txId = unPrefix0x(txId); } // TODO trow if txid does not match expected input const unTxId = unPrefix0x(txId); // eslint-disable-next-line @typescript-eslint/no-explicit-any let params: any[] = [unTxId, 2]; if (this.chainType === 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 (utxo_check_expect_empty(res.data)) { throw new mccError(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 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (res.data && res.data.result && res.data.result.confirmations && res.data.result.confirmations < 1) { throw new mccError(mccErrorCode.InvalidTransaction); } utxo_ensure_data(res.data); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument const blockHeaderBase = await this.getBlockHeaderBase(res.data.result.blockhash); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const transactionData = { mediantime: blockHeaderBase.mediantime, // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access ...res.data.result, }; // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return new this.constructors.transactionConstructor(transactionData); } /////////////////////////////////////////////////////////////////////////////////////// // Client specific methods //////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////// /** * Get header information about the block * @param blockHash * @returns */ async getBlockHeaderBase(blockHashOrHeight: string | number): Promise { let blockHash: string | null = null; if (typeof blockHashOrHeight === "string") { blockHash = blockHashOrHeight; if (PREFIXED_STD_BLOCK_HASH_REGEX.test(blockHash)) { blockHash = 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 (utxo_check_expect_empty(res.data)) { throw new mccError(mccErrorCode.InvalidData); } utxo_ensure_data(res.data); // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access return res.data.result; } /** * Get only tips of all the branches * @param height_gte * @returns */ async getBlockTips(height_gte: number): Promise { 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: number, read_main: boolean = true): Promise { const height = await this.getBlockHeight(); let callBranchLength: undefined | number = 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?: IUtxoGetAlternativeBlocksOptions): Promise { const res = await this.client.post(``, { jsonrpc: "1.0", id: "rpc", method: "getchaintips", params: [], }); utxo_ensure_data(res.data); const gte_height = option?.height_gte || 0; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access const response = res.data.result.filter((el: IUtxoChainTip) => { return el.height >= gte_height; }); if (option !== undefined) { if (option.all_blocks !== undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access let extended = response.map((el: IUtxoChainTip) => this.recursive_block_hash(el.hash, el.branchlen)); extended = await Promise.all(extended); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access for (let i = 0; i < response.length; i++) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access response[i].all_block_hashes = extended[i]; } } } // eslint-disable-next-line @typescript-eslint/no-unsafe-return return response; } /////////////////////////////////////////////////////////////////////////////////////// // Client helper (private) methods //////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////// /** * Get network info * @returns network info details */ // eslint-disable-next-line @typescript-eslint/no-explicit-any private async getNetworkInfo(): Promise { 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 (utxo_check_expect_empty(res.data)) { return null; } 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 */ private async getBlockHashFromHeight(blockNumber: number): Promise { const res = await this.client.post("", { jsonrpc: "1.0", id: "rpc", method: "getblockhash", params: [blockNumber], }); if (utxo_check_expect_block_out_of_range(res.data)) { throw new mccError(mccErrorCode.InvalidBlock); } utxo_ensure_data(res.data); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return res.data.result as string; } /** * * @param height_gte * @param mainBranchProcess * @returns */ private async getBlockTipsHelper(height_gte: number, mainBranchProcess?: number): Promise { const tips = await this.getTopBlocks({ height_gte: height_gte, all_blocks: true }); let mainBranchHashes: UtxoBlockTip[] = []; 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: IUtxoChainTip) => { const tempTips = []; // all_block_hashes exist due to all_blocks: true in getTopBlocks call for (let hashIndex = 0; hashIndex < UtxoTip.all_block_hashes!.length; hashIndex++) { tempTips.push( new this.constructors.blockTipConstructor({ 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: BTipCon[], nev: BTipCon[]) => acc.concat(nev), []) .concat(mainBranchHashes as unknown as BTipCon[]); const unique = new Set(); return reducedTips.filter((elem: BTipCon) => { const key = `${elem.number}_${elem.blockHash}`; if (unique.has(key)) { return false; } else { unique.add(key); return true; } }); } /////////////////////////////////////////////////////////////////////////////////////// // Block tip recursive class methods ////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////// async recursive_block_hash(hash: string, processHeight: number): Promise { 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: BTipCon, processHeight: number): Promise { 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: string, amount: number) { 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], }); utxo_ensure_data(res.data); // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access return res.data.result; } }