"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.populateV3Relay = populateV3Relay;
exports.getTimeAt = getTimeAt;
exports.getTimestampForBlock = getTimestampForBlock;
exports.getMaxFillDeadlineInRange = getMaxFillDeadlineInRange;
exports.getDepositIdAtBlock = getDepositIdAtBlock;
exports.findDepositBlock = findDepositBlock;
exports.relayFillStatus = relayFillStatus;
exports.fillStatusArray = fillStatusArray;
exports.findFillBlock = findFillBlock;
exports.findFillEvent = findFillEvent;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const constants_1 = require("../../constants");
const interfaces_1 = require("../../interfaces");
const utils_1 = require("../../utils");
function populateV3Relay(spokePool, relayData, repaymentAddress, repaymentChainId = relayData.destinationChainId) {
    (0, assert_1.default)(repaymentAddress.isValidOn(repaymentChainId), `Invalid repayment address for chain ${repaymentChainId}: ${repaymentAddress.toNative()}.`);
    const evmRelayData = {
        depositor: relayData.depositor.toBytes32(),
        recipient: relayData.recipient.toBytes32(),
        inputToken: relayData.inputToken.toBytes32(),
        outputToken: relayData.outputToken.toBytes32(),
        inputAmount: relayData.inputAmount,
        outputAmount: relayData.outputAmount,
        originChainId: relayData.originChainId,
        depositId: relayData.depositId,
        fillDeadline: relayData.fillDeadline,
        exclusivityDeadline: relayData.exclusivityDeadline,
        message: relayData.message,
        exclusiveRelayer: relayData.exclusiveRelayer.toBytes32(),
    };
    if ((0, utils_1.isDefined)(relayData.speedUpSignature)) {
        (0, assert_1.default)((0, utils_1.isDefined)(relayData.updatedRecipient) && !relayData.updatedRecipient.isZeroAddress());
        (0, assert_1.default)((0, utils_1.isDefined)(relayData.updatedOutputAmount));
        (0, assert_1.default)((0, utils_1.isDefined)(relayData.updatedMessage));
        return spokePool.populateTransaction.fillRelayWithUpdatedDeposit(evmRelayData, repaymentChainId, repaymentAddress.toBytes32(), relayData.updatedOutputAmount, relayData.updatedRecipient.toBytes32(), relayData.updatedMessage, relayData.speedUpSignature);
    }
    return spokePool.populateTransaction.fillRelay(evmRelayData, repaymentChainId, repaymentAddress.toBytes32());
}
async function getTimeAt(spokePool, blockNumber) {
    const currentTime = await spokePool.getCurrentTime({ blockTag: blockNumber });
    (0, assert_1.default)(utils_1.BigNumber.isBigNumber(currentTime) && currentTime.lt(utils_1.bnUint32Max));
    return currentTime.toNumber();
}
async function getTimestampForBlock(provider, blockNumber) {
    const block = await provider.getBlock(blockNumber);
    return block.timestamp;
}
async function getMaxFillDeadlineInRange(spokePool, startBlock, endBlock) {
    const fillDeadlineBuffers = await Promise.all([
        spokePool.fillDeadlineBuffer({ blockTag: startBlock }),
        spokePool.fillDeadlineBuffer({ blockTag: endBlock }),
    ]);
    return Math.max(fillDeadlineBuffers[0], fillDeadlineBuffers[1]);
}
async function getDepositIdAtBlock(contract, blockTag) {
    const _depositIdAtBlock = await contract.numberOfDeposits({ blockTag });
    const depositIdAtBlock = (0, utils_1.toBN)(_depositIdAtBlock);
    if (depositIdAtBlock.lt(utils_1.bnZero)) {
        throw new Error("Invalid deposit count");
    }
    return depositIdAtBlock;
}
async function findDepositBlock(spokePool, depositId, lowBlock, highBlock) {
    if ((0, utils_1.isUnsafeDepositId)(depositId)) {
        throw new Error(`Cannot binary search for depositId ${depositId}`);
    }
    highBlock ??= await spokePool.provider.getBlockNumber();
    (0, assert_1.default)(highBlock > lowBlock, `Block numbers out of range (${lowBlock} >= ${highBlock})`);
    const [nDepositsLow, nDepositsHigh] = (await Promise.all([
        spokePool.numberOfDeposits({ blockTag: lowBlock }),
        spokePool.numberOfDeposits({ blockTag: highBlock }),
    ])).map((n) => (0, utils_1.toBN)(n));
    if (nDepositsLow.gt(depositId) || nDepositsHigh.lte(depositId)) {
        return undefined;
    }
    do {
        const midBlock = Math.floor((highBlock + lowBlock) / 2);
        const nDeposits = (0, utils_1.toBN)(await spokePool.numberOfDeposits({ blockTag: midBlock }));
        if (nDeposits.gt(depositId)) {
            highBlock = midBlock;
        }
        else {
            lowBlock = midBlock + 1;
        }
    } while (lowBlock < highBlock);
    return lowBlock;
}
async function relayFillStatus(spokePool, relayData, blockTag = "latest", destinationChainId) {
    destinationChainId ??= await spokePool.chainId();
    (0, assert_1.default)((0, utils_1.isDefined)(destinationChainId));
    const hash = (0, utils_1.getRelayDataHash)(relayData, destinationChainId);
    const _fillStatus = await spokePool.fillStatuses(hash, { blockTag });
    const fillStatus = Number(_fillStatus);
    if (![interfaces_1.FillStatus.Unfilled, interfaces_1.FillStatus.RequestedSlowFill, interfaces_1.FillStatus.Filled].includes(fillStatus)) {
        const { originChainId, depositId } = relayData;
        throw new Error(`relayFillStatus: Unexpected fillStatus for ${originChainId} deposit ${depositId.toString()} (${fillStatus})`);
    }
    return fillStatus;
}
async function fillStatusArray(spokePool, relayData, blockTag = "latest") {
    const fillStatuses = "fillStatuses";
    const destinationChainId = await spokePool.chainId();
    const queries = relayData.map((relayData) => {
        const hash = (0, utils_1.getRelayDataHash)(relayData, destinationChainId);
        return spokePool.interface.encodeFunctionData(fillStatuses, [hash]);
    });
    const chunkSize = 250;
    const chunkedQueries = (0, utils_1.chunk)(queries, chunkSize);
    const multicalls = await Promise.all(chunkedQueries.map((queries) => spokePool.callStatic.multicall(queries, { blockTag })));
    const status = multicalls
        .map((multicall) => multicall.map((result) => spokePool.interface.decodeFunctionResult(fillStatuses, result)[0]))
        .flat();
    const bnUnfilled = (0, utils_1.toBN)(interfaces_1.FillStatus.Unfilled);
    const bnFilled = (0, utils_1.toBN)(interfaces_1.FillStatus.Filled);
    return status.map((status) => {
        return utils_1.BigNumber.isBigNumber(status) && status.gte(bnUnfilled) && status.lte(bnFilled)
            ? status.toNumber()
            : undefined;
    });
}
async function findFillBlock(spokePool, relayData, lowBlockNumber, highBlockNumber) {
    const { provider } = spokePool;
    const destinationChainId = Object.values(constants_1.CHAIN_IDs).includes(relayData.originChainId)
        ? (await provider.getNetwork()).chainId
        : Number(await spokePool.chainId());
    (0, assert_1.default)(relayData.originChainId !== destinationChainId, `Origin & destination chain IDs must not be equal (${destinationChainId})`);
    lowBlockNumber = Math.max(lowBlockNumber, constants_1.SPOKEPOOL_UPGRADE_BLOCKS[destinationChainId] ?? lowBlockNumber);
    highBlockNumber ??= await provider.getBlockNumber();
    (0, assert_1.default)(highBlockNumber > lowBlockNumber, `Block numbers out of range (${lowBlockNumber} >= ${highBlockNumber})`);
    const [initialFillStatus, finalFillStatus] = (await Promise.all([
        relayFillStatus(spokePool, relayData, lowBlockNumber, destinationChainId),
        relayFillStatus(spokePool, relayData, highBlockNumber, destinationChainId),
    ])).map(Number);
    if (finalFillStatus !== interfaces_1.FillStatus.Filled) {
        return undefined;
    }
    if (initialFillStatus === interfaces_1.FillStatus.Filled) {
        const { depositId, originChainId } = relayData;
        const [srcChain, dstChain] = [(0, utils_1.getNetworkName)(originChainId), (0, utils_1.getNetworkName)(destinationChainId)];
        throw new Error(`${srcChain} deposit ${depositId.toString()} filled on ${dstChain} before block ${lowBlockNumber}`);
    }
    do {
        const midBlockNumber = Math.floor((highBlockNumber + lowBlockNumber) / 2);
        const fillStatus = await relayFillStatus(spokePool, relayData, midBlockNumber, destinationChainId);
        if (fillStatus === interfaces_1.FillStatus.Filled) {
            highBlockNumber = midBlockNumber;
        }
        else {
            lowBlockNumber = midBlockNumber + 1;
        }
    } while (lowBlockNumber < highBlockNumber);
    return lowBlockNumber;
}
async function findFillEvent(spokePool, relayData, lowBlockNumber, highBlockNumber) {
    const blockNumber = await findFillBlock(spokePool, relayData, lowBlockNumber, highBlockNumber);
    if (!blockNumber)
        return undefined;
    const maxLookBack = 0;
    const [from, to] = [blockNumber, blockNumber];
    const query = (await Promise.all([
        (0, utils_1.paginatedEventQuery)(spokePool, spokePool.filters.FilledRelay(null, null, null, null, null, relayData.originChainId, relayData.depositId), { from, to, maxLookBack }),
        (0, utils_1.paginatedEventQuery)(spokePool, spokePool.filters.FilledV3Relay(null, null, null, null, null, relayData.originChainId, relayData.depositId), { from, to, maxLookBack }),
    ])).flat();
    if (query.length === 0) {
        const { chainId: destinationChainId } = await spokePool.provider.getNetwork();
        const [origin, destination] = [relayData.originChainId, destinationChainId].map(utils_1.getNetworkName);
        throw new Error(`Failed to find fill event on destination chain ${destination} at block ${blockNumber}, for deposit ${relayData.depositId} on origin chain ${origin}`);
    }
    const event = query[0];
    const destinationChainId = Object.values(constants_1.CHAIN_IDs).includes(relayData.originChainId)
        ? (await spokePool.provider.getNetwork()).chainId
        : Number(await spokePool.chainId());
    return (0, utils_1.unpackFillEvent)((0, utils_1.spreadEventWithBlockNumber)(event), destinationChainId);
}
//# sourceMappingURL=SpokeUtils.js.map