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());
    });
};
import { randomBytes } from "crypto";
import cloneDeep from "lodash/cloneDeep";
import { logger } from "../..";
import { toBuffer, toString, toQueryObjectGTV } from "../formatter";
import { decodeValue, encodeValue } from "../gtx/serialization";
import { handleGetResponse, handlePostResponse, } from "../restclient/restclient";
import { requestWithRetry, sleep } from "../restclient/restclientutil";
import * as gtxTool from "../gtx/gtx";
import { TxRejectedError } from "../restclient/errors";
import { callbackPromiseBuilder, formatBlockInfoResponse, formatTransactionInfoResponse, getClientConfigFromSettings, getGTXFromBufferOrTransactionOrOperation, getSerializedGTX, handlePostResponsePromisified, isKeyPair, } from "./utils";
import { Web3PromiEvent } from "../promiEvent/promiEvents";
import { NumberOfSignersAndSignaturesException } from "../gtx/errors";
import { GetTransactionRidException } from "./errors";
import { Method } from "../restclient/enums";
import { ResponseStatus } from "./enums";
import { isBlockIdentifierValid } from "./validation/blockIdentifier";
import { isNetworkSettingValid } from "./validation/networkSettings";
import { isSignMethodValid } from "./validation/signMethod";
import { isTxRidValid } from "./validation/txRid";
export function createClient(settings) {
    return __awaiter(this, void 0, void 0, function* () {
        isNetworkSettingValid(settings, { throwOnError: true });
        return {
            config: yield getClientConfigFromSettings(settings),
            query(nameOrQueryObject, args, callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    let _name, _args;
                    if (typeof nameOrQueryObject === "string") {
                        _name = nameOrQueryObject;
                        _args = args;
                    }
                    else {
                        _name = nameOrQueryObject === null || nameOrQueryObject === void 0 ? void 0 : nameOrQueryObject.name;
                        _args = nameOrQueryObject === null || nameOrQueryObject === void 0 ? void 0 : nameOrQueryObject.args;
                    }
                    const { error, statusCode, rspBody } = yield requestWithRetry(Method.POST, `query_gtv/${this.config.blockchainRid}`, this.config, encodeValue(toQueryObjectGTV(_name, _args)));
                    return new Promise((resolve, reject) => {
                        handlePostResponse(error, statusCode, rspBody, callbackPromiseBuilder(reject, resolve, callback));
                    });
                });
            },
            signTransaction(transaction, signMethod, callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    logger.debug(`Signing transaction with ${!isKeyPair(signMethod) ? "signature provider containing " : ""}pubKey: ${toString(signMethod.pubKey)}`);
                    const gtx = getGTXFromBufferOrTransactionOrOperation(transaction, this.config.blockchainRid);
                    try {
                        const signedTx = yield (isKeyPair(signMethod)
                            ? gtxTool.sign(gtx, signMethod.privKey, signMethod.pubKey)
                            : gtxTool.sign(gtx, signMethod));
                        const gtxBytes = getSerializedGTX(signedTx);
                        if (typeof callback === "function") {
                            callback(null, gtxBytes);
                        }
                        return gtxBytes;
                    }
                    catch (error) {
                        if (typeof callback === "function") {
                            callback(error, null);
                        }
                        throw new Error(error);
                    }
                });
            },
            sendTransaction(transaction, doStatusPolling = true, callback = undefined) {
                const promiEvent = new Web3PromiEvent((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
                    var _a;
                    try {
                        const gtx = getGTXFromBufferOrTransactionOrOperation(transaction, this.config.blockchainRid);
                        if (gtx.signers.length !== ((_a = gtx.signatures) === null || _a === void 0 ? void 0 : _a.length)) {
                            reject(new NumberOfSignersAndSignaturesException());
                        }
                        const gtxBytes = getSerializedGTX(gtx);
                        const transactionObject = {
                            tx: gtxBytes.toString("hex"),
                        };
                        const { error, statusCode, rspBody } = yield requestWithRetry(Method.POST, `tx/${this.config.blockchainRid}`, this.config, transactionObject);
                        const transactionRid = gtxTool.getDigestToSign(gtx);
                        try {
                            yield handlePostResponsePromisified(error, statusCode, rspBody);
                            if (typeof callback === "function") {
                                callback(null, {
                                    status: ResponseStatus.Waiting,
                                    statusCode,
                                    transactionRid: transactionRid,
                                });
                            }
                        }
                        catch (_error) {
                            if (typeof callback === "function") {
                                callback(_error, null);
                            }
                            return reject(_error);
                        }
                        const transactionReceipt = {
                            status: ResponseStatus.Waiting,
                            statusCode: statusCode,
                            transactionRid: transactionRid,
                        };
                        promiEvent.emit("sent", transactionReceipt);
                        if (doStatusPolling === false) {
                            return resolve(transactionReceipt);
                        }
                        const awaitConfirmation = (txRID) => __awaiter(this, void 0, void 0, function* () {
                            var _b;
                            let lastKnownResult;
                            for (let i = 0; i < this.config.statusPollCount; i++) {
                                lastKnownResult = yield this.getTransactionStatus(txRID);
                                if (lastKnownResult.status === ResponseStatus.Confirmed) {
                                    return ResponseStatus.Confirmed;
                                }
                                else if (lastKnownResult.status === ResponseStatus.Rejected) {
                                    throw new TxRejectedError((_b = lastKnownResult.rejectReason) !== null && _b !== void 0 ? _b : "");
                                }
                                yield sleep(this.config.statusPollInterval);
                            }
                            // TS issue. This could be fixed by implementing new retry strategy
                            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                            //@ts-expect-error
                            return lastKnownResult.status;
                        });
                        const confirmationStatus = yield awaitConfirmation(gtxTool.getDigestToSign(gtx));
                        resolve({
                            status: confirmationStatus,
                            statusCode: statusCode,
                            transactionRid: transactionRid,
                        });
                    }
                    catch (error) {
                        reject(error);
                    }
                }));
                return promiEvent;
            },
            signAndSendUniqueTransaction(transactionOrOperation, signMethod, doStatusPolling = true, callback = undefined) {
                isSignMethodValid(signMethod, { throwOnError: true });
                const promiEvent = new Web3PromiEvent((resolve, reject) => {
                    const transaction = "name" in transactionOrOperation
                        ? {
                            operations: [transactionOrOperation],
                            signers: [signMethod.pubKey],
                        }
                        : transactionOrOperation;
                    const hasNop = transaction.operations.some((operation) => {
                        return operation.name === "nop";
                    });
                    const transactionWithNop = hasNop
                        ? transaction
                        : this.addNop(transaction);
                    this.signTransaction(transactionWithNop, signMethod)
                        .then((signedTx) => {
                        const sendTransactionPromiEvent = this.sendTransaction(signedTx, doStatusPolling, callback);
                        sendTransactionPromiEvent.on("sent", (receipt) => {
                            promiEvent.emit("sent", receipt);
                        });
                        resolve(sendTransactionPromiEvent);
                    })
                        .catch((error) => {
                        reject(error);
                    });
                });
                return promiEvent;
            },
            getTransaction(transactionRid, callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    try {
                        isTxRidValid(transactionRid, { throwOnError: true });
                    }
                    catch (error) {
                        callback === null || callback === void 0 ? void 0 : callback(error, null);
                        throw error;
                    }
                    const { error, statusCode, rspBody } = yield requestWithRetry(Method.GET, `tx/${this.config.blockchainRid}/${transactionRid.toString("hex")}`, this.config);
                    return new Promise((resolve, reject) => {
                        handleGetResponse(error, statusCode, statusCode === 200 ? toBuffer(rspBody.tx) : rspBody, callbackPromiseBuilder(reject, resolve, callback));
                    });
                });
            },
            getTransactionStatus(transactionRid, callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    try {
                        isTxRidValid(transactionRid, { throwOnError: true });
                    }
                    catch (error) {
                        callback === null || callback === void 0 ? void 0 : callback(error, null);
                        throw error;
                    }
                    const { error, statusCode, rspBody } = yield requestWithRetry(Method.GET, `tx/${this.config.blockchainRid}/${transactionRid.toString("hex")}/status`, this.config);
                    return new Promise((resolve, reject) => {
                        handleGetResponse(error, statusCode, rspBody, callbackPromiseBuilder(reject, resolve, callback));
                    });
                });
            },
            addNop(transaction) {
                const _transaction = cloneDeep(transaction);
                const noOperation = {
                    name: "nop",
                    args: [randomBytes(32)],
                };
                _transaction.operations = [..._transaction.operations, noOperation];
                return _transaction;
            },
            getTransactionRid(transaction) {
                try {
                    const gtx = getGTXFromBufferOrTransactionOrOperation(transaction, this.config.blockchainRid);
                    return gtxTool.getDigestToSign(gtx);
                }
                catch (e) {
                    throw new GetTransactionRidException(e);
                }
            },
            getTransactionsInfo(limit = 25, beforeTime, callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    const beforeTimeQueryParam = beforeTime
                        ? `&before-time=${beforeTime.getTime()}`
                        : "";
                    const { error, statusCode, rspBody } = yield requestWithRetry(Method.GET, `transactions/${this.config.blockchainRid}?limit=${limit}${beforeTimeQueryParam}`, this.config);
                    const body = statusCode === 200
                        ? rspBody === null || rspBody === void 0 ? void 0 : rspBody.map(formatTransactionInfoResponse)
                        : rspBody;
                    return new Promise((resolve, reject) => {
                        handleGetResponse(error, statusCode, body, callbackPromiseBuilder(reject, resolve, callback));
                    });
                });
            },
            getTransactionInfo(transactionRid, callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    try {
                        isTxRidValid(transactionRid, { throwOnError: true });
                    }
                    catch (error) {
                        callback === null || callback === void 0 ? void 0 : callback(error, null);
                        throw error;
                    }
                    const { error, statusCode, rspBody } = yield requestWithRetry(Method.GET, `transactions/${this.config.blockchainRid}/${toString(transactionRid)}`, this.config);
                    const body = statusCode === 200 && rspBody
                        ? formatTransactionInfoResponse(rspBody)
                        : rspBody;
                    return new Promise((resolve, reject) => {
                        handleGetResponse(error, statusCode, body, callbackPromiseBuilder(reject, resolve, callback));
                    });
                });
            },
            getTransactionCount(callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    const { error, statusCode, rspBody } = yield requestWithRetry(Method.GET, `transactions/${this.config.blockchainRid}/count`, this.config);
                    return new Promise((resolve, reject) => {
                        handleGetResponse(error, statusCode, statusCode === 200 ? rspBody.transactionsCount : rspBody, callbackPromiseBuilder(reject, resolve, callback));
                    });
                });
            },
            getBlockInfo(blockIdentifier, txs = false, callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    isBlockIdentifierValid(blockIdentifier, { throwOnError: true });
                    const queryString = typeof blockIdentifier === "string"
                        ? blockIdentifier
                        : `height/${blockIdentifier}`;
                    const { error, statusCode, rspBody } = yield requestWithRetry(Method.GET, `blocks/${this.config.blockchainRid}/${queryString}?txs=${txs}`, this.config);
                    return new Promise((resolve, reject) => {
                        handleGetResponse(error, statusCode, statusCode === 200 && rspBody !== null && rspBody
                            ? formatBlockInfoResponse(rspBody)
                            : rspBody, callbackPromiseBuilder(reject, resolve, callback));
                    });
                });
            },
            getLatestBlock(txs = false, callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    const shouldIncludeFullTransaction = txs ? `&txs=${txs}` : "";
                    const { error, statusCode, rspBody } = yield requestWithRetry(Method.GET, `blocks/${this.config.blockchainRid}?limit=1${shouldIncludeFullTransaction}`, this.config);
                    const indexOfLatestBlock = 0;
                    return new Promise((resolve, reject) => {
                        handleGetResponse(error, statusCode, statusCode === 200 && rspBody !== null && rspBody
                            ? formatBlockInfoResponse(rspBody[indexOfLatestBlock])
                            : rspBody, callbackPromiseBuilder(reject, resolve, callback));
                    });
                });
            },
            getBlocksInfo(limit = 25, beforeTime, beforeHeight, txs, callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    let filteringQueryParam = "";
                    if (beforeTime) {
                        filteringQueryParam = `&before-time=${beforeTime.getTime()}`;
                    }
                    else if (beforeHeight) {
                        filteringQueryParam = `&before-height=${beforeHeight}`;
                    }
                    const shouldIncludeFullTransaction = txs ? `&txs=${txs}` : "";
                    const { error, statusCode, rspBody } = yield requestWithRetry(Method.GET, `blocks/${this.config.blockchainRid}?limit=${limit}${filteringQueryParam}${shouldIncludeFullTransaction}`, this.config);
                    return new Promise((resolve, reject) => {
                        handleGetResponse(error, statusCode, statusCode === 200 && rspBody
                            ? rspBody.map(formatBlockInfoResponse)
                            : rspBody, callbackPromiseBuilder(reject, resolve, callback));
                    });
                });
            },
            encodeTransaction(transaction) {
                const gtx = getGTXFromBufferOrTransactionOrOperation(transaction, this.config.blockchainRid);
                return gtxTool.serialize(gtx);
            },
            decodeTransactionToGtx(encodedTransaction) {
                const gtx = gtxTool.deserialize(encodedTransaction);
                logger.debug(`Output from deserializing a raw transaction: ${JSON.stringify(gtx)}`);
                return gtx;
            },
            getClientNodeUrlPool() {
                return this.config.endpointPool.map((endpoint) => endpoint.url);
            },
            /**
             * Retrieves a confirmation proof for a transaction with the specified sha256
             * hash.
             * @param txRid A buffer of 32 bytes
             * @param callback parameters (error, responseObjectProof) if first
             * parameter is not null, an error occurred.
             * If first parameter is null, then the second parameter is an object
             * like the following:
             *
             * {hash: messageHashBuffer,
             *  blockHeader: blockHeaderBuffer,
             *  signatures: [{pubKey: pubKeyBuffer, signature: sigBuffer}, ...],
             *  merklePath: [{side: <0|1>, hash: <hash buffer level n-1>},
             *               ...
             *               {side: <0|1>, hash: <hash buffer level 1>}]}
             *
             * If no such transaction RID exists, the callback will be called with (null, null).
             *
             * The proof object can be validated using
             * postchain-common.util.validateMerklePath(proof.merklePath, proof.hash,
             * proof.blockHeader.slice(32, 64))
             *
             * The signatures must be validated agains some know trusted source for valid signers
             * at this specific block height.
             */
            getConfirmationProof: function (txRid, callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    try {
                        isTxRidValid(txRid, { throwOnError: true });
                    }
                    catch (error) {
                        callback === null || callback === void 0 ? void 0 : callback(error, null);
                        throw error;
                    }
                    const { error, statusCode, rspBody } = yield requestWithRetry(Method.GET, `tx/${this.config.blockchainRid}/${txRid.toString("hex")}/confirmationProof`, this.config);
                    const confirmationProof = {
                        merkleProofTree: "",
                        txIndex: 0,
                    };
                    if (statusCode === 200) {
                        try {
                            const decodedProof = decodeValue(toBuffer(rspBody.proof));
                            confirmationProof.txIndex = decodedProof.txIndex;
                            confirmationProof.hash = decodedProof.hash;
                            confirmationProof.blockHeader = decodedProof.blockHeader;
                            confirmationProof.witness = decodedProof.witness;
                            confirmationProof.merkleProofTree = decodedProof.merkleProofTree;
                        }
                        catch (decodeError) {
                            if (callback) {
                                callback(decodeError, null);
                            }
                            throw decodeError;
                        }
                    }
                    return new Promise((resolve, reject) => {
                        handleGetResponse(error, statusCode, statusCode === 200 ? confirmationProof : rspBody, callbackPromiseBuilder(reject, resolve, callback));
                    });
                });
            },
            /**
             * Get app structure. Returns list of app modules in JSON format
             * @param callback - leverages error-first pattern and called when promise resolves
             */
            getAppStructure(callback) {
                return __awaiter(this, void 0, void 0, function* () {
                    const { error, statusCode, rspBody } = yield requestWithRetry(Method.GET, `/query/${this.config.blockchainRid}?type=rell.get_app_structure`, this.config);
                    return new Promise((resolve, reject) => {
                        handleGetResponse(error, statusCode, rspBody, callbackPromiseBuilder(reject, resolve, callback));
                    });
                });
            },
        };
    });
}
//# sourceMappingURL=blockchainClient.js.map