import { __assign, __awaiter, __generator } from "tslib";
import assert from "assert";
import { DEFAULT_CACHING_TTL, EMPTY_MESSAGE, UNDEFINED_MESSAGE_HASH, ZERO_BYTES } from "../constants";
import { getMessageHash, isUnsafeDepositId } from "./SpokeUtils";
import { getNetworkName } from "./NetworkUtils";
import { bnZero } from "./BigNumberUtils";
import { getDepositInCache, getDepositKey, setDepositInCache } from "./CachingUtils";
import { getCurrentTime } from "./TimeUtils";
import { isDefined } from "./TypeGuards";
import { isDepositFormedCorrectly } from "./ValidatorUtils";
// Load a deposit for a fill if the fill's deposit ID is outside this client's search range.
// This can be used by the Dataworker to determine whether to give a relayer a refund for a fill
// of a deposit older or younger than its fixed lookback.
export var InvalidFill;
(function (InvalidFill) {
    InvalidFill[InvalidFill["DepositIdInvalid"] = 0] = "DepositIdInvalid";
    InvalidFill[InvalidFill["DepositIdNotFound"] = 1] = "DepositIdNotFound";
    InvalidFill[InvalidFill["FillMismatch"] = 2] = "FillMismatch";
    InvalidFill[InvalidFill["DepositIdOutOfRange"] = 3] = "DepositIdOutOfRange";
})(InvalidFill || (InvalidFill = {}));
/**
 * Attempts to resolve a deposit for a fill. If the fill's deposit Id is within the spoke pool client's search range,
 * the deposit is returned immediately. Otherwise, the deposit is queried first from the provided cache, and if it is
 * not found in the cache, it is queried from the spoke pool client. If the deposit is found, it is cached before
 * being returned.
 * @param spokePoolClient The spoke pool client to use to query the deposit.
 * @param fill The fill to resolve a deposit for.
 * @param cache An optional cache to use to store the deposit. Optional.
 * @returns The deposit for the fill, or undefined if the deposit could not be found.
 * @throws If the fill's origin chain ID does not match the spoke pool client's chain ID.
 * @throws If the spoke pool client has not been updated.
 */
// @todo relocate
export function queryHistoricalDepositForFill(spokePoolClient, fill, cache) {
    var _a;
    return __awaiter(this, void 0, void 0, function () {
        var depositId, originChain, deposit, match_1, cachedDeposit, result, match;
        return __generator(this, function (_b) {
            switch (_b.label) {
                case 0:
                    if (isUnsafeDepositId(fill.depositId)) {
                        return [2 /*return*/, {
                                found: false,
                                code: InvalidFill.DepositIdOutOfRange,
                                reason: "Cannot find historical deposit for fill with unsafe deposit ID ".concat(fill.depositId, "."),
                            }];
                    }
                    if (fill.originChainId !== spokePoolClient.chainId) {
                        throw new Error("OriginChainId mismatch (".concat(fill.originChainId, " != ").concat(spokePoolClient.chainId, ")"));
                    }
                    // We need to update client so we know the first and last deposit ID's queried for this spoke pool client, as well
                    // as the global first and last deposit ID's for this spoke pool.
                    if (!spokePoolClient.isUpdated) {
                        throw new Error("SpokePoolClient must be updated before querying historical deposits");
                    }
                    depositId = fill.depositId;
                    originChain = getNetworkName(fill.originChainId);
                    deposit = spokePoolClient.getDeposit(depositId);
                    if (isDefined(deposit)) {
                        match_1 = validateFillForDeposit(fill, deposit);
                        if (match_1.valid) {
                            return [2 /*return*/, { found: true, deposit: deposit }];
                        }
                        return [2 /*return*/, {
                                found: false,
                                code: InvalidFill.FillMismatch,
                                reason: "Fill for ".concat(originChain, " deposit ID ").concat(depositId.toString(), " is invalid (").concat(match_1.reason, ")."),
                            }];
                    }
                    if (!cache) return [3 /*break*/, 2];
                    return [4 /*yield*/, getDepositInCache(getDepositKey(fill), cache)];
                case 1:
                    cachedDeposit = _b.sent();
                    // We only want to warn and remove the cached deposit if it
                    //    A: exists
                    //    B: is not formed correctly
                    if (isDefined(cachedDeposit) && !isDepositFormedCorrectly(cachedDeposit)) {
                        spokePoolClient.logger.warn({
                            at: "[SDK]:DepositUtils#queryHistoricalDepositForFill",
                            message: "Cached deposit was not formed correctly, removing from cache",
                            fill: fill,
                            cachedDeposit: cachedDeposit,
                        });
                        // By setting this value to undefined, we eventually have to pull
                        // the deposit from our spoke pool client. Because this new deposit
                        // is formed correctly, we will cache it below.
                        cachedDeposit = undefined;
                    }
                    _b.label = 2;
                case 2:
                    if (!isDefined(cachedDeposit)) return [3 /*break*/, 3];
                    deposit = cachedDeposit;
                    return [3 /*break*/, 6];
                case 3: return [4 /*yield*/, spokePoolClient.findDeposit(fill.depositId)];
                case 4:
                    result = _b.sent();
                    if (!result.found) {
                        return [2 /*return*/, result];
                    }
                    (deposit = result.deposit);
                    if (!cache) return [3 /*break*/, 6];
                    return [4 /*yield*/, setDepositInCache(deposit, getCurrentTime(), cache, DEFAULT_CACHING_TTL)];
                case 5:
                    _b.sent();
                    _b.label = 6;
                case 6:
                    assert(isDefined(deposit), "Unexpectedly failed to locate ".concat(originChain, " deposit ").concat(fill.depositId));
                    (_a = deposit.messageHash) !== null && _a !== void 0 ? _a : (deposit.messageHash = getMessageHash(deposit.message));
                    match = validateFillForDeposit(fill, deposit);
                    if (match.valid) {
                        return [2 /*return*/, { found: true, deposit: deposit }];
                    }
                    return [2 /*return*/, {
                            found: false,
                            code: InvalidFill.FillMismatch,
                            reason: match.reason,
                        }];
            }
        });
    });
}
/**
 * Concatenate all fields from a Deposit, Fill or SlowFillRequest into a single string.
 * This can be used to identify a bridge event in a mapping. This is used instead of the actual keccak256 hash
 * (getRelayDataHash()) for two reasons: performance and the fact that only Deposit includes the `message` field, which
 * is required to compute a complete RelayData hash.
 * note: This function should _not_ be used to query the SpokePool.fillStatuses mapping.
 */
export function getRelayEventKey(data) {
    return [
        data.depositor,
        data.recipient,
        data.exclusiveRelayer,
        data.inputToken,
        data.outputToken,
        data.inputAmount,
        data.outputAmount,
        data.originChainId,
        data.destinationChainId,
        data.depositId,
        data.fillDeadline,
        data.exclusivityDeadline,
        data.messageHash,
    ]
        .map(String)
        .join("-");
}
var RELAYDATA_KEYS = [
    "depositId",
    "originChainId",
    "destinationChainId",
    "depositor",
    "recipient",
    "inputToken",
    "inputAmount",
    "outputToken",
    "outputAmount",
    "fillDeadline",
    "exclusivityDeadline",
    "exclusiveRelayer",
    "messageHash",
];
// Ensure that each deposit element is included with the same value in the fill. This includes all elements defined
// by the depositor as well as destinationToken, which are pulled from other clients.
export function validateFillForDeposit(relayData, deposit) {
    if (deposit === undefined) {
        return { valid: false, reason: "Deposit is undefined" };
    }
    // Note: this short circuits when a key is found where the comparison doesn't match.
    // TODO: if we turn on "strict" in the tsconfig, the elements of FILL_DEPOSIT_COMPARISON_KEYS will be automatically
    // validated against the fields in Fill and Deposit, generating an error if there is a discrepency.
    var invalidKey = RELAYDATA_KEYS.find(function (key) { return relayData[key].toString() !== deposit[key].toString(); });
    // There should be no paths for `messageHash` to be unset, but mask it off anyway.
    if (!isDefined(invalidKey) && [relayData.messageHash, deposit.messageHash].includes(UNDEFINED_MESSAGE_HASH)) {
        invalidKey = "messageHash";
    }
    return isDefined(invalidKey)
        ? { valid: false, reason: "".concat(invalidKey, " mismatch (").concat(relayData[invalidKey], " != ").concat(deposit[invalidKey], ")") }
        : { valid: true };
}
/**
 * Returns true if filling this deposit (as a slow or fast fill) or refunding it would not change any state
 * on-chain. The dataworker functions can use this to conveniently filter out useless deposits.
 * @dev The reason we allow a 0-input deposit to have a non-empty message is that the message might be used
 * to pay the filler in an indirect way so it might have economic value as a fast or slow fill.
 * @param deposit Deposit to check.
 * @returns True if deposit's input amount is 0 and message is empty.
 */
export function isZeroValueDeposit(deposit) {
    return deposit.inputAmount.eq(0) && isMessageEmpty(deposit.message);
}
export function invalidOutputToken(deposit) {
    // If the output token is zero address, then it is invalid.
    return deposit.outputToken.isZeroAddress();
}
export function isZeroValueFillOrSlowFillRequest(e) {
    return e.inputAmount.eq(bnZero) && e.messageHash === ZERO_BYTES;
}
/**
 * Determines if a message is empty or not.
 * @param message The message to check.
 * @returns True if the message is empty, false otherwise.
 */
export function isMessageEmpty(message) {
    if (message === void 0) { message = EMPTY_MESSAGE; }
    return message === "" || message === "0x";
}
export function isFillOrSlowFillRequestMessageEmpty(message) {
    return isMessageEmpty(message) || message === ZERO_BYTES;
}
/**
 * Determines if a deposit was updated via a speed-up transaction.
 * @param deposit Deposit to evaluate.
 * @returns True if the deposit was updated, otherwise false.
 */
export function isDepositSpedUp(deposit) {
    return isDefined(deposit.speedUpSignature) && isDefined(deposit.updatedOutputAmount);
}
/**
 * Resolves the applicable message for a deposit.
 * @param deposit Deposit to evaluate.
 * @returns Original or updated message string, depending on whether the depositor updated the deposit.
 */
export function resolveDepositMessage(deposit) {
    var message = isDepositSpedUp(deposit) ? deposit.updatedMessage : deposit.message;
    assert(isDefined(message)); // Appease tsc about the updatedMessage being possibly undefined.
    return message;
}
/**
 * Converts a RelayData object with `Address` types as address fields to a `RelayData`-like object with
 * strings as address fields.
 * @param relayData RelayData type.
 * @returns a RelayData-like type which has strings as fields.
 */
export function convertRelayDataParamsToBytes32(relayData) {
    return __assign(__assign({}, relayData), { depositor: relayData.depositor.toBytes32(), recipient: relayData.recipient.toBytes32(), inputToken: relayData.inputToken.toBytes32(), outputToken: relayData.outputToken.toBytes32(), exclusiveRelayer: relayData.exclusiveRelayer.toBytes32() });
}
/**
 * Converts a Fill object with `Address` types as address fields to a `RelayData`-like object with
 * strings as address fields.
 * @param relayData RelayData type.
 * @returns a RelayData-like type which has strings as fields.
 */
export function convertFillParamsToBytes32(fill) {
    return __assign(__assign({}, fill), { depositor: fill.depositor.toBytes32(), recipient: fill.recipient.toBytes32(), inputToken: fill.inputToken.toBytes32(), outputToken: fill.outputToken.toBytes32(), exclusiveRelayer: fill.exclusiveRelayer.toBytes32(), relayer: fill.relayer.toBytes32(), relayExecutionInfo: __assign(__assign({}, fill.relayExecutionInfo), { updatedRecipient: fill.relayExecutionInfo.updatedRecipient.toBytes32() }) });
}
//# sourceMappingURL=DepositUtils.js.map