import { __assign, __awaiter, __generator } from "tslib";
import assert from "assert";
import { CHAIN_IDs } from "../../constants";
import { FillStatus, } from "../../interfaces";
import { bnUint32Max, BigNumber, toBN, bnZero, chunk, getMessageHash, getRelayDataHash, isDefined, isUnsafeDepositId, getNetworkName, paginatedEventQuery, spreadEventWithBlockNumber, toAddressType, } from "../../utils";
/**
 * @param spokePool SpokePool Contract instance.
 * @param relayData RelayData instance, supplemented with destinationChainId
 * @param repaymentChainId Optional repaymentChainId (defaults to destinationChainId).
 * @returns An Ethers UnsignedTransaction instance.
 */
export function populateV3Relay(spokePool, relayData, repaymentAddress, repaymentChainId) {
    if (repaymentChainId === void 0) { repaymentChainId = relayData.destinationChainId; }
    assert(repaymentAddress.isValidOn(repaymentChainId), "Invalid repayment address for chain ".concat(repaymentChainId, ": ").concat(repaymentAddress.toNative(), "."));
    var 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 (isDefined(relayData.speedUpSignature)) {
        assert(isDefined(relayData.updatedRecipient) && !relayData.updatedRecipient.isZeroAddress());
        assert(isDefined(relayData.updatedOutputAmount));
        assert(isDefined(relayData.updatedMessage));
        return spokePool.populateTransaction.fillRelayWithUpdatedDeposit(relayData, repaymentChainId, repaymentAddress.toBytes32(), relayData.updatedOutputAmount, relayData.updatedRecipient.toBytes32(), relayData.updatedMessage, relayData.speedUpSignature);
    }
    return spokePool.populateTransaction.fillRelay(evmRelayData, repaymentChainId, repaymentAddress.toBytes32());
}
/**
 * Retrieves the time from the SpokePool contract at a particular block.
 * @returns The time at the specified block tag.
 */
export function getTimeAt(spokePool, blockNumber) {
    return __awaiter(this, void 0, void 0, function () {
        var currentTime;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, spokePool.getCurrentTime({ blockTag: blockNumber })];
                case 1:
                    currentTime = _a.sent();
                    assert(BigNumber.isBigNumber(currentTime) && currentTime.lt(bnUint32Max));
                    return [2 /*return*/, currentTime.toNumber()];
            }
        });
    });
}
/**
 * Retrieves the chain time at a particular block.
 * @note This should be the same as getTimeAt() but can differ in test. These two functions should be consolidated.
 * @returns The chain time at the specified block tag.
 */
export function getTimestampForBlock(provider, blockNumber) {
    return __awaiter(this, void 0, void 0, function () {
        var block;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, provider.getBlock(blockNumber)];
                case 1:
                    block = _a.sent();
                    return [2 /*return*/, block.timestamp];
            }
        });
    });
}
/**
 * Return maximum of fill deadline buffer at start and end of block range.
 * @param spokePool SpokePool contract instance
 * @param startBlock start block
 * @param endBlock end block
 * @returns maximum of fill deadline buffer at start and end block
 */
export function getMaxFillDeadlineInRange(spokePool, startBlock, endBlock) {
    return __awaiter(this, void 0, void 0, function () {
        var fillDeadlineBuffers;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, Promise.all([
                        spokePool.fillDeadlineBuffer({ blockTag: startBlock }),
                        spokePool.fillDeadlineBuffer({ blockTag: endBlock }),
                    ])];
                case 1:
                    fillDeadlineBuffers = _a.sent();
                    return [2 /*return*/, Math.max(fillDeadlineBuffers[0], fillDeadlineBuffers[1])];
            }
        });
    });
}
/**
 * Finds the deposit id at a specific block number.
 * @param blockTag The block number to search for the deposit ID at.
 * @returns The deposit ID.
 */
export function getDepositIdAtBlock(contract, blockTag) {
    return __awaiter(this, void 0, void 0, function () {
        var _depositIdAtBlock, depositIdAtBlock;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, contract.numberOfDeposits({ blockTag: blockTag })];
                case 1:
                    _depositIdAtBlock = _a.sent();
                    depositIdAtBlock = toBN(_depositIdAtBlock);
                    // Sanity check to ensure that the deposit ID is greater than or equal to zero.
                    if (depositIdAtBlock.lt(bnZero)) {
                        throw new Error("Invalid deposit count");
                    }
                    return [2 /*return*/, depositIdAtBlock];
            }
        });
    });
}
export function findDepositBlock(spokePool, depositId, lowBlock, highBlock) {
    return __awaiter(this, void 0, void 0, function () {
        var _a, _b, nDepositsLow, nDepositsHigh, midBlock, nDeposits, _c;
        return __generator(this, function (_d) {
            switch (_d.label) {
                case 0:
                    // We can only perform this search when we have a safe deposit ID.
                    if (isUnsafeDepositId(depositId)) {
                        throw new Error("Cannot binary search for depositId ".concat(depositId));
                    }
                    if (!(highBlock !== null && highBlock !== void 0)) return [3 /*break*/, 1];
                    _a = highBlock;
                    return [3 /*break*/, 3];
                case 1: return [4 /*yield*/, spokePool.provider.getBlockNumber()];
                case 2:
                    _a = (highBlock = _d.sent());
                    _d.label = 3;
                case 3:
                    _a;
                    assert(highBlock > lowBlock, "Block numbers out of range (".concat(lowBlock, " >= ").concat(highBlock, ")"));
                    return [4 /*yield*/, Promise.all([
                            spokePool.numberOfDeposits({ blockTag: lowBlock }),
                            spokePool.numberOfDeposits({ blockTag: highBlock }),
                        ])];
                case 4:
                    _b = (_d.sent()).map(function (n) { return toBN(n); }), nDepositsLow = _b[0], nDepositsHigh = _b[1];
                    if (nDepositsLow.gt(depositId) || nDepositsHigh.lte(depositId)) {
                        return [2 /*return*/, undefined]; // Deposit did not occur within the specified block range.
                    }
                    _d.label = 5;
                case 5:
                    midBlock = Math.floor((highBlock + lowBlock) / 2);
                    _c = toBN;
                    return [4 /*yield*/, spokePool.numberOfDeposits({ blockTag: midBlock })];
                case 6:
                    nDeposits = _c.apply(void 0, [_d.sent()]);
                    if (nDeposits.gt(depositId)) {
                        highBlock = midBlock; // depositId occurred at or earlier than midBlock.
                    }
                    else {
                        lowBlock = midBlock + 1; // depositId occurred later than midBlock.
                    }
                    _d.label = 7;
                case 7:
                    if (lowBlock < highBlock) return [3 /*break*/, 5];
                    _d.label = 8;
                case 8: return [2 /*return*/, lowBlock];
            }
        });
    });
}
/**
 * Find the amount filled for a deposit at a particular block.
 * @param spokePool SpokePool contract instance.
 * @param relayData Deposit information that is used to complete a fill.
 * @param blockTag Block tag (numeric or "latest") to query at.
 * @returns The amount filled for the specified deposit at the requested block (or latest).
 */
export function relayFillStatus(spokePool, relayData, blockTag, destinationChainId) {
    if (blockTag === void 0) { blockTag = "latest"; }
    return __awaiter(this, void 0, void 0, function () {
        var _a, hash, _fillStatus, fillStatus, originChainId, depositId;
        return __generator(this, function (_b) {
            switch (_b.label) {
                case 0:
                    if (!(destinationChainId !== null && destinationChainId !== void 0)) return [3 /*break*/, 1];
                    _a = destinationChainId;
                    return [3 /*break*/, 3];
                case 1: return [4 /*yield*/, spokePool.chainId()];
                case 2:
                    _a = (destinationChainId = _b.sent());
                    _b.label = 3;
                case 3:
                    _a;
                    assert(isDefined(destinationChainId));
                    hash = getRelayDataHash(relayData, destinationChainId);
                    return [4 /*yield*/, spokePool.fillStatuses(hash, { blockTag: blockTag })];
                case 4:
                    _fillStatus = _b.sent();
                    fillStatus = Number(_fillStatus);
                    if (![FillStatus.Unfilled, FillStatus.RequestedSlowFill, FillStatus.Filled].includes(fillStatus)) {
                        originChainId = relayData.originChainId, depositId = relayData.depositId;
                        throw new Error("relayFillStatus: Unexpected fillStatus for ".concat(originChainId, " deposit ").concat(depositId.toString(), " (").concat(fillStatus, ")"));
                    }
                    return [2 /*return*/, fillStatus];
            }
        });
    });
}
export function fillStatusArray(spokePool, relayData, blockTag) {
    if (blockTag === void 0) { blockTag = "latest"; }
    return __awaiter(this, void 0, void 0, function () {
        var fillStatuses, destinationChainId, queries, chunkSize, chunkedQueries, multicalls, status, bnUnfilled, bnFilled;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    fillStatuses = "fillStatuses";
                    return [4 /*yield*/, spokePool.chainId()];
                case 1:
                    destinationChainId = _a.sent();
                    queries = relayData.map(function (relayData) {
                        var hash = getRelayDataHash(relayData, destinationChainId);
                        return spokePool.interface.encodeFunctionData(fillStatuses, [hash]);
                    });
                    chunkSize = 250;
                    chunkedQueries = chunk(queries, chunkSize);
                    return [4 /*yield*/, Promise.all(chunkedQueries.map(function (queries) { return spokePool.callStatic.multicall(queries, { blockTag: blockTag }); }))];
                case 2:
                    multicalls = _a.sent();
                    status = multicalls
                        .map(function (multicall) {
                        return multicall.map(function (result) { return spokePool.interface.decodeFunctionResult(fillStatuses, result)[0]; });
                    })
                        .flat();
                    bnUnfilled = toBN(FillStatus.Unfilled);
                    bnFilled = toBN(FillStatus.Filled);
                    return [2 /*return*/, status.map(function (status) {
                            return BigNumber.isBigNumber(status) && status.gte(bnUnfilled) && status.lte(bnFilled)
                                ? status.toNumber()
                                : undefined;
                        })];
            }
        });
    });
}
/**
 * Find the block at which a fill was completed.
 * @todo After SpokePool upgrade, this function can be simplified to use the FillStatus enum.
 * @param spokePool SpokePool contract instance.
 * @param relayData Deposit information that is used to complete a fill.
 * @param lowBlockNumber The lower bound of the search. Must be bounded by SpokePool deployment.
 * @param highBlocknumber Optional upper bound for the search.
 * @returns The block number at which the relay was completed, or undefined.
 */
export function findFillBlock(spokePool, relayData, lowBlockNumber, highBlockNumber) {
    return __awaiter(this, void 0, void 0, function () {
        var provider, _a, destinationChainId, _b, _c, _d, initialFillStatus, finalFillStatus, depositId, originChainId, _e, srcChain, dstChain, midBlockNumber, fillStatus;
        return __generator(this, function (_f) {
            switch (_f.label) {
                case 0:
                    provider = spokePool.provider;
                    if (!(highBlockNumber !== null && highBlockNumber !== void 0)) return [3 /*break*/, 1];
                    _a = highBlockNumber;
                    return [3 /*break*/, 3];
                case 1: return [4 /*yield*/, provider.getBlockNumber()];
                case 2:
                    _a = (highBlockNumber = _f.sent());
                    _f.label = 3;
                case 3:
                    _a;
                    assert(highBlockNumber > lowBlockNumber, "Block numbers out of range (".concat(lowBlockNumber, " >= ").concat(highBlockNumber, ")"));
                    if (!Object.values(CHAIN_IDs).includes(relayData.originChainId)) return [3 /*break*/, 5];
                    return [4 /*yield*/, provider.getNetwork()];
                case 4:
                    _b = (_f.sent()).chainId;
                    return [3 /*break*/, 7];
                case 5:
                    _c = Number;
                    return [4 /*yield*/, spokePool.chainId()];
                case 6:
                    _b = _c.apply(void 0, [_f.sent()]);
                    _f.label = 7;
                case 7:
                    destinationChainId = _b;
                    assert(relayData.originChainId !== destinationChainId, "Origin & destination chain IDs must not be equal (".concat(destinationChainId, ")"));
                    return [4 /*yield*/, Promise.all([
                            relayFillStatus(spokePool, relayData, lowBlockNumber, destinationChainId),
                            relayFillStatus(spokePool, relayData, highBlockNumber, destinationChainId),
                        ])];
                case 8:
                    _d = (_f.sent()).map(Number), initialFillStatus = _d[0], finalFillStatus = _d[1];
                    if (finalFillStatus !== FillStatus.Filled) {
                        return [2 /*return*/, undefined]; // Wasn't filled within the specified block range.
                    }
                    // Was filled earlier than the specified lowBlock. This is an error by the caller.
                    if (initialFillStatus === FillStatus.Filled) {
                        depositId = relayData.depositId, originChainId = relayData.originChainId;
                        _e = [getNetworkName(originChainId), getNetworkName(destinationChainId)], srcChain = _e[0], dstChain = _e[1];
                        throw new Error("".concat(srcChain, " deposit ").concat(depositId.toString(), " filled on ").concat(dstChain, " before block ").concat(lowBlockNumber));
                    }
                    _f.label = 9;
                case 9:
                    midBlockNumber = Math.floor((highBlockNumber + lowBlockNumber) / 2);
                    return [4 /*yield*/, relayFillStatus(spokePool, relayData, midBlockNumber, destinationChainId)];
                case 10:
                    fillStatus = _f.sent();
                    if (fillStatus === FillStatus.Filled) {
                        highBlockNumber = midBlockNumber;
                    }
                    else {
                        lowBlockNumber = midBlockNumber + 1;
                    }
                    _f.label = 11;
                case 11:
                    if (lowBlockNumber < highBlockNumber) return [3 /*break*/, 9];
                    _f.label = 12;
                case 12: return [2 /*return*/, lowBlockNumber];
            }
        });
    });
}
export function findFillEvent(spokePool, relayData, lowBlockNumber, highBlockNumber) {
    return __awaiter(this, void 0, void 0, function () {
        var blockNumber, maxLookBack, _a, from, to, query, event, destinationChainId, _b, _c, fillEvent, fill;
        return __generator(this, function (_d) {
            switch (_d.label) {
                case 0: return [4 /*yield*/, findFillBlock(spokePool, relayData, lowBlockNumber, highBlockNumber)];
                case 1:
                    blockNumber = _d.sent();
                    if (!blockNumber)
                        return [2 /*return*/, undefined];
                    maxLookBack = 0;
                    _a = [blockNumber, blockNumber], from = _a[0], to = _a[1];
                    return [4 /*yield*/, Promise.all([
                            paginatedEventQuery(spokePool, spokePool.filters.FilledRelay(null, null, null, null, null, relayData.originChainId, relayData.depositId), { from: from, to: to, maxLookBack: maxLookBack }),
                            paginatedEventQuery(spokePool, spokePool.filters.FilledV3Relay(null, null, null, null, null, relayData.originChainId, relayData.depositId), { from: from, to: to, maxLookBack: maxLookBack }),
                        ])];
                case 2:
                    query = (_d.sent()).flat();
                    if (query.length === 0)
                        throw new Error("Failed to find fill event at block ".concat(blockNumber));
                    event = query[0];
                    if (!Object.values(CHAIN_IDs).includes(relayData.originChainId)) return [3 /*break*/, 4];
                    return [4 /*yield*/, spokePool.provider.getNetwork()];
                case 3:
                    _b = (_d.sent()).chainId;
                    return [3 /*break*/, 6];
                case 4:
                    _c = Number;
                    return [4 /*yield*/, spokePool.chainId()];
                case 5:
                    _b = _c.apply(void 0, [_d.sent()]);
                    _d.label = 6;
                case 6:
                    destinationChainId = _b;
                    fillEvent = spreadEventWithBlockNumber(event);
                    fill = __assign(__assign({}, fillEvent), { inputToken: toAddressType(fillEvent.inputToken, relayData.originChainId), outputToken: toAddressType(fillEvent.outputToken, destinationChainId), depositor: toAddressType(fillEvent.depositor, relayData.originChainId), recipient: toAddressType(fillEvent.recipient, destinationChainId), exclusiveRelayer: toAddressType(fillEvent.exclusiveRelayer, destinationChainId), relayer: toAddressType(fillEvent.relayer, destinationChainId), relayExecutionInfo: __assign(__assign({}, fillEvent.relayExecutionInfo), { updatedRecipient: toAddressType(fillEvent.relayExecutionInfo.updatedRecipient, destinationChainId) }), destinationChainId: destinationChainId, messageHash: getMessageHash(event.args.message) });
                    return [2 /*return*/, fill];
            }
        });
    });
}
//# sourceMappingURL=SpokeUtils.js.map