"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());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.issueAndRedeem = exports.redeem = exports.sleep = exports.issueSingle = exports.allocateAmountsToVaults = exports.getRequestIdsFromEvents = exports.ExecuteRedeem = exports.SLEEP_TIME_MS = void 0;
const monetary_js_1 = require("@interlay/monetary-js");
const utils_1 = require("../utils");
const encoding_1 = require("../utils/encoding");
const types_1 = require("../types");
const bitcoin_1 = require("./bitcoin");
const currency_1 = require("./currency");
exports.SLEEP_TIME_MS = 1000;
var ExecuteRedeem;
(function (ExecuteRedeem) {
    ExecuteRedeem[ExecuteRedeem["False"] = 0] = "False";
    ExecuteRedeem[ExecuteRedeem["Manually"] = 1] = "Manually";
    ExecuteRedeem[ExecuteRedeem["Auto"] = 2] = "Auto";
})(ExecuteRedeem = exports.ExecuteRedeem || (exports.ExecuteRedeem = {}));
/**
 * @param events The EventRecord array returned after sending a transaction
 * @param methodToCheck The name of the event method whose existence to check
 * @returns The id associated with the transaction. If the EventRecord array does not
 * contain required events, the function throws an error.
 */
function getRequestIdsFromEvents(events, eventToFind, api) {
    const ids = new Array();
    for (const { event } of events) {
        if (eventToFind.is(event)) {
            // the redeem id has type H256 and is the first item of the event data array
            const id = api.createType("Hash", event.data[0]);
            ids.push(id);
        }
    }
    if (ids.length > 0)
        return ids;
    throw new Error("Transaction failed");
}
exports.getRequestIdsFromEvents = getRequestIdsFromEvents;
/**
 * Given a list of vaults with availabilities (e.g. collateral for issue, tokens
 * for redeem) and an amount to allocate, selects one or more vaults to fulfil
 * the request.
 * If the amount cannot be allocated by a single vault, greedily selects the vault
 * with highest available amount and tries again for the remainder. If at leaast
 * one vault can fulfil a request alone, a random one among them is selected.
 **/
function allocateAmountsToVaults(vaultsWithAvailableAmounts, amountToAllocate) {
    const maxReservationPercent = 95; // don't reserve more than 95% of a vault's collateral
    const allocations = new Map();
    // iterable array in ascending order of issuing capacity:
    const vaultsArray = [...vaultsWithAvailableAmounts.entries()]
        .reverse()
        .map((entry) => [entry[0], entry[1].div(100).mul(maxReservationPercent)]);
    while (amountToAllocate.gt((0, currency_1.newMonetaryAmount)(0, amountToAllocate.currency))) {
        // find first vault that can fulfil request (or undefined if none)
        const firstSuitable = vaultsArray.findIndex(([_, available]) => available.gte(amountToAllocate));
        let vault, amount;
        if (firstSuitable !== -1) {
            // at least one vault can fulfil in full
            // select random vault able to fulfil request
            const range = vaultsArray.length - firstSuitable;
            const idx = Math.floor(Math.random() * range) + firstSuitable;
            vault = vaultsArray[idx][0];
            amount = amountToAllocate;
        }
        else {
            // else allocate greedily
            if (vaultsArray.length === 0)
                throw new Error("Insufficient capacity to fulfil request");
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const largestVault = vaultsArray.pop(); // length >= 1, so never undefined
            [vault, amount] = largestVault;
        }
        allocations.set(vault, amount);
        amountToAllocate = amountToAllocate.sub(amount);
    }
    return allocations;
}
exports.allocateAmountsToVaults = allocateAmountsToVaults;
function issueSingle(interBtcApi, bitcoinCoreClient, issuingAccount, amount, vaultId, autoExecute = true, triggerRefund = false, atomic = true) {
    return __awaiter(this, void 0, void 0, function* () {
        const prevAccount = interBtcApi.account;
        interBtcApi.setAccount(issuingAccount);
        try {
            const requesterAccountId = (0, utils_1.newAccountId)(interBtcApi.api, issuingAccount.address);
            const initialWrappedTokenBalance = (yield interBtcApi.tokens.balance(amount.currency, requesterAccountId)).free;
            const blocksToMine = 3;
            const collateralIdLiteral = vaultId ? (0, types_1.currencyIdToLiteral)(vaultId.currencies.collateral) : undefined;
            const rawRequestResult = yield interBtcApi.issue.request(amount, vaultId === null || vaultId === void 0 ? void 0 : vaultId.accountId, collateralIdLiteral, atomic);
            if (rawRequestResult.length !== 1) {
                throw new Error("More than one issue request created");
            }
            const issueRequest = rawRequestResult[0];
            let amountAsBtc = issueRequest.wrappedAmount.add(issueRequest.bridgeFee);
            if (triggerRefund) {
                // Send 1 more Btc than needed
                amountAsBtc = amountAsBtc.add(monetary_js_1.BitcoinAmount.from.BTC(1));
            }
            else if (autoExecute === false) {
                // Send 1 less Satoshi than requested
                // to trigger the user failsafe and disable auto-execution.
                const oneSatoshi = monetary_js_1.BitcoinAmount.from.Satoshi(1);
                amountAsBtc = amountAsBtc.sub(oneSatoshi);
            }
            // send btc tx
            const vaultBtcAddress = issueRequest.vaultWrappedAddress;
            if (vaultBtcAddress === undefined) {
                throw new Error("Undefined vault address returned from RequestIssue");
            }
            const txData = yield bitcoinCoreClient.sendBtcTxAndMine(vaultBtcAddress, amountAsBtc, blocksToMine);
            if (autoExecute === false) {
                console.log("Manually executing, waiting for relay to catchup");
                yield (0, bitcoin_1.waitForBlockFinalization)(bitcoinCoreClient, interBtcApi.btcRelay);
                // execute issue, assuming the selected vault has the `--no-issue-execution` flag enabled
                yield interBtcApi.issue.execute(issueRequest.id, txData.txid);
            }
            else {
                console.log("Auto-executing, waiting for vault to submit proof");
                // wait for vault to execute issue
                while ((yield interBtcApi.issue.getRequestById(issueRequest.id)).status !== types_1.IssueStatus.Completed) {
                    yield sleep(exports.SLEEP_TIME_MS);
                }
            }
            const finalWrappedTokenBalance = (yield interBtcApi.tokens.balance(amount.currency, requesterAccountId)).free;
            return {
                request: issueRequest,
                initialWrappedTokenBalance,
                finalWrappedTokenBalance,
            };
        }
        catch (e) {
            // IssueCompleted errors occur when multiple vaults attempt to execute the same request
            return Promise.reject(new Error(`Issuing failed: ${e}`));
        }
        finally {
            if (prevAccount) {
                interBtcApi.setAccount(prevAccount);
            }
        }
    });
}
exports.issueSingle = issueSingle;
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
exports.sleep = sleep;
function redeem(interBtcApi, bitcoinCoreClient, redeemingAccount, amount, vaultId, autoExecute = ExecuteRedeem.Auto, atomic = true, timeout = 5 * 60 * 1000, retries = 0) {
    return __awaiter(this, void 0, void 0, function* () {
        const prevAccount = interBtcApi.account;
        interBtcApi.setAccount(redeemingAccount);
        const btcAddress = "bcrt1qujs29q4gkyn2uj6y570xl460p4y43ruayxu8ry";
        const [redeemRequest] = yield interBtcApi.redeem.request(amount, btcAddress, vaultId, atomic, retries);
        switch (autoExecute) {
            case ExecuteRedeem.Manually: {
                const opreturnData = (0, encoding_1.stripHexPrefix)(redeemRequest.id.toString());
                const btcTxId = yield interBtcApi.electrsAPI.waitForOpreturn(opreturnData, timeout, 5000).catch((_) => {
                    throw new Error("Redeem request was not executed, timeout expired");
                });
                // Even if the tx was found, the block needs to be relayed to the parachain before `execute` can be called.
                yield (0, bitcoin_1.waitForBlockFinalization)(bitcoinCoreClient, interBtcApi.btcRelay);
                // manually execute issue
                yield interBtcApi.redeem.execute(redeemRequest.id.toString(), btcTxId);
                break;
            }
            case ExecuteRedeem.Auto: {
                // wait for vault to execute issue
                while ((yield interBtcApi.redeem.getRequestById(redeemRequest.id)).status !== types_1.RedeemStatus.Completed) {
                    yield sleep(exports.SLEEP_TIME_MS);
                }
                break;
            }
        }
        if (prevAccount) {
            interBtcApi.setAccount(prevAccount);
        }
        return redeemRequest;
    });
}
exports.redeem = redeem;
function issueAndRedeem(InterBtcApi, bitcoinCoreClient, account, vaultId, issueAmount = monetary_js_1.InterBtcAmount.from.BTC(0.1), redeemAmount = monetary_js_1.InterBtcAmount.from.BTC(0.009), autoExecuteIssue = true, autoExecuteRedeem = ExecuteRedeem.Auto, triggerRefund = false, atomic = true) {
    return __awaiter(this, void 0, void 0, function* () {
        const issueResult = yield issueSingle(InterBtcApi, bitcoinCoreClient, account, issueAmount, vaultId, autoExecuteIssue, triggerRefund, atomic);
        const redeemRequest = yield redeem(InterBtcApi, bitcoinCoreClient, account, redeemAmount, issueResult.request.vaultId, autoExecuteRedeem, atomic);
        return [issueResult.request, redeemRequest];
    });
}
exports.issueAndRedeem = issueAndRedeem;
//# sourceMappingURL=issueRedeem.js.map