import { __assign, __awaiter, __generator, __spreadArray } from "tslib";
import assert from "assert";
import { fetchEncodedAccount, isSome, pipe, appendTransactionMessageInstruction, compileTransaction, getBase64EncodedWireTransaction, } from "@solana/kit";
import { SolanaVoidSigner, getFillRelayTx, getIPFillRelayTx, getFillRelayViaInstructionParamsInstructions, toAddress, createDefaultTransaction, getAssociatedTokenAddress, isSVMFillTooLarge, } from "../../arch/svm";
import { Coingecko } from "../../coingecko";
import { CHAIN_IDs } from "../../constants";
import { getGasPriceEstimate } from "../../gasPriceOracle";
import { toBN, mapAsync } from "../../utils";
import { getDefaultRelayer } from "../relayFeeCalculator";
import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
import { TOKEN_2022_PROGRAM_ADDRESS, getTokenSize, fetchMint } from "@solana-program/token-2022";
import { getSetComputeUnitLimitInstruction, estimateComputeUnitLimitFactory } from "@solana-program/compute-budget";
import { arch } from "../..";
/**
 * A special QueryBase implementation for SVM used for querying gas costs, token prices, and decimals of various tokens
 * on Solana.
 */
var SvmQuery = /** @class */ (function () {
    /**
     * Instantiates a SvmQuery instance
     * @param provider A valid solana/kit rpc client.
     * @param symbolMapping A mapping to valid ERC20 tokens and their respective characteristics
     * @param spokePool The valid address of the Spoke Pool deployment
     * @param simulatedRelayerAddress The address that these queries will reference as the sender. Note: This address must be approved for USDC
     * @param logger A logging utility to report logs
     * @param coingeckoProApiKey An optional CoinGecko API key that links to a PRO account
     * @param fixedGasPrice Overrides the gas price with a fixed value. Note: primarily used for the Boba blockchain
     * @param coingeckoBaseCurrency The basis currency that CoinGecko will use to resolve pricing
     */
    function SvmQuery(provider, symbolMapping, spokePool, simulatedRelayerAddress, logger, coingeckoProApiKey, fixedGasPrice, coingeckoBaseCurrency) {
        if (coingeckoBaseCurrency === void 0) { coingeckoBaseCurrency = "eth"; }
        this.provider = provider;
        this.symbolMapping = symbolMapping;
        this.spokePool = spokePool;
        this.simulatedRelayerAddress = simulatedRelayerAddress;
        this.logger = logger;
        this.coingeckoProApiKey = coingeckoProApiKey;
        this.fixedGasPrice = fixedGasPrice;
        this.coingeckoBaseCurrency = coingeckoBaseCurrency;
        this.computeUnitEstimator = estimateComputeUnitLimitFactory({
            rpc: provider,
        });
    }
    /**
     * Retrieves the current gas costs of performing a fillRelay contract at the referenced SpokePool.
     * @param relayData RelayData instance, supplemented with destinationChainId
     * @param _relayer Relayer address to simulate with.
     * @param options
     * @param options.gasPrice Optional gas price to use for the simulation.
     * @param options.gasUnits Optional gas units to use for the simulation.
     * @param options.transport Optional transport object for custom gas price retrieval.
     * @returns The gas estimate for this function call (multiplied with the optional buffer).
     */
    SvmQuery.prototype.getGasCosts = function (relayData_1) {
        return __awaiter(this, arguments, void 0, function (relayData, relayer, options) {
            var destinationChainId, recipient, outputToken, exclusiveRelayer, _a, repaymentChainId, repaymentAddress, _fillRelayTx, fillTooLarge, fillRelayTx, _b, _c, _computeUnitsConsumed, gasPriceEstimate, tokenAccountInfo, computeUnitsConsumed, tokenOwner, recipientAta, encodedAta, gasPrice, tokenGasCost, extensions, mint, tokenAccountSize, rentCostInLamports;
            if (relayer === void 0) { relayer = getDefaultRelayer(relayData.destinationChainId); }
            if (options === void 0) { options = {}; }
            return __generator(this, function (_d) {
                switch (_d.label) {
                    case 0:
                        destinationChainId = relayData.destinationChainId, recipient = relayData.recipient, outputToken = relayData.outputToken, exclusiveRelayer = relayData.exclusiveRelayer;
                        assert(recipient.isSVM(), "getGasCosts: recipient not an SVM address (".concat(recipient, ")"));
                        assert(outputToken.isSVM(), "getGasCosts: outputToken not an SVM address (".concat(outputToken, ")"));
                        assert(exclusiveRelayer.isSVM(), "getGasCosts: exclusiveRelayer not an SVM address (".concat(exclusiveRelayer, ")"));
                        assert(relayer.isSVM());
                        _a = [destinationChainId, relayer], repaymentChainId = _a[0], repaymentAddress = _a[1];
                        return [4 /*yield*/, this.getFillRelayTx(__assign(__assign({}, relayData), { recipient: recipient, outputToken: outputToken, exclusiveRelayer: exclusiveRelayer }), SolanaVoidSigner(relayer.toBase58()), repaymentChainId, repaymentAddress)];
                    case 1:
                        _fillRelayTx = _d.sent();
                        fillTooLarge = isSVMFillTooLarge(_fillRelayTx);
                        if (!fillTooLarge.tooLarge) return [3 /*break*/, 3];
                        return [4 /*yield*/, this.getFillRelayTx(__assign(__assign({}, relayData), { recipient: recipient, outputToken: outputToken, exclusiveRelayer: exclusiveRelayer, message: "0x" }), SolanaVoidSigner(relayer.toBase58()), repaymentChainId, repaymentAddress)];
                    case 2:
                        _b = _d.sent();
                        return [3 /*break*/, 4];
                    case 3:
                        _b = _fillRelayTx;
                        _d.label = 4;
                    case 4:
                        fillRelayTx = _b;
                        return [4 /*yield*/, Promise.all([
                                fillTooLarge.tooLarge
                                    ? this.estimateComputeUnits(__assign(__assign({}, relayData), { recipient: recipient, outputToken: outputToken, exclusiveRelayer: exclusiveRelayer }), relayer, repaymentChainId, repaymentAddress)
                                    : this.computeUnitEstimator(fillRelayTx),
                                getGasPriceEstimate(this.provider, {
                                    unsignedTx: fillRelayTx,
                                    baseFeeMultiplier: options.baseFeeMultiplier,
                                    priorityFeeMultiplier: options.priorityFeeMultiplier,
                                }),
                                this.provider.getAccountInfo(toAddress(outputToken), { encoding: "base58" }).send(),
                            ])];
                    case 5:
                        _c = _d.sent(), _computeUnitsConsumed = _c[0], gasPriceEstimate = _c[1], tokenAccountInfo = _c[2];
                        computeUnitsConsumed = toBN(_computeUnitsConsumed);
                        tokenOwner = tokenAccountInfo.value.owner;
                        assert(tokenOwner === TOKEN_2022_PROGRAM_ADDRESS || tokenOwner === TOKEN_PROGRAM_ADDRESS, "".concat(outputToken, " has invalid token account owner ").concat(tokenOwner, "."));
                        return [4 /*yield*/, getAssociatedTokenAddress(recipient, outputToken, tokenOwner)];
                    case 6:
                        recipientAta = _d.sent();
                        return [4 /*yield*/, fetchEncodedAccount(this.provider, recipientAta)];
                    case 7:
                        encodedAta = _d.sent();
                        gasPrice = gasPriceEstimate.baseFee.add(gasPriceEstimate.microLamportsPerComputeUnit.mul(computeUnitsConsumed).div(toBN(1000000)) // 1_000_000 microLamports/lamport.
                        );
                        tokenGasCost = gasPrice;
                        if (!!encodedAta.exists) return [3 /*break*/, 11];
                        extensions = undefined;
                        if (!(tokenOwner === TOKEN_2022_PROGRAM_ADDRESS)) return [3 /*break*/, 9];
                        return [4 /*yield*/, fetchMint(this.provider, toAddress(outputToken))];
                    case 8:
                        mint = _d.sent();
                        extensions = isSome(mint.data.extensions) ? mint.data.extensions.value : undefined;
                        _d.label = 9;
                    case 9:
                        tokenAccountSize = getTokenSize(extensions);
                        return [4 /*yield*/, this.provider.getMinimumBalanceForRentExemption(BigInt(tokenAccountSize)).send()];
                    case 10:
                        rentCostInLamports = _d.sent();
                        tokenGasCost = tokenGasCost.add(toBN(Number(rentCostInLamports)));
                        _d.label = 11;
                    case 11: return [2 /*return*/, {
                            nativeGasCost: computeUnitsConsumed,
                            tokenGasCost: tokenGasCost,
                            gasPrice: gasPrice,
                        }];
                }
            });
        });
    };
    /**
     * @notice Return the gas cost of a simulated transaction
     * @param fillRelayTx FillRelay transaction
     * @param relayer SVM address of the relayer
     * @returns Estimated gas cost in compute units
     */
    SvmQuery.prototype.getNativeGasCost = function (deposit_1) {
        return __awaiter(this, arguments, void 0, function (deposit, relayer) {
            var destinationChainId, recipient, outputToken, exclusiveRelayer, _a, repaymentChainId, repaymentAddress, fillRelayTx, _b;
            if (relayer === void 0) { relayer = getDefaultRelayer(deposit.destinationChainId); }
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        destinationChainId = deposit.destinationChainId, recipient = deposit.recipient, outputToken = deposit.outputToken, exclusiveRelayer = deposit.exclusiveRelayer;
                        assert(recipient.isSVM(), "getNativeGasCost: recipient not an SVM address (".concat(recipient, ")"));
                        assert(outputToken.isSVM(), "getNativeGasCost: outputToken not an SVM address (".concat(outputToken, ")"));
                        assert(exclusiveRelayer.isSVM(), "getNativeGasCost: exclusiveRelayer not an SVM address (".concat(exclusiveRelayer, ")"));
                        assert(relayer.isSVM());
                        _a = [destinationChainId, relayer], repaymentChainId = _a[0], repaymentAddress = _a[1];
                        return [4 /*yield*/, this.getFillRelayTx(__assign(__assign({}, deposit), { recipient: recipient, outputToken: outputToken, exclusiveRelayer: exclusiveRelayer }), SolanaVoidSigner(relayer.toBase58()), repaymentChainId, repaymentAddress)];
                    case 1:
                        fillRelayTx = _c.sent();
                        _b = toBN;
                        return [4 /*yield*/, this.computeUnitEstimator(fillRelayTx)];
                    case 2: return [2 /*return*/, _b.apply(void 0, [_c.sent()])];
                }
            });
        });
    };
    /**
     * @notice Return the native token cost of filling a deposit beyond gas cost. If `value_amount` is specified in a message,
     * `value_amount` of SOL gets forwarded to the first Account. We account for that in Fill cost estimation
     * @param deposit RelayData associated with Deposit we're estimating for
     * @throws If deposit.message is malformed (unable to be deserialized into `AcrossPlusMessage`)
     * @returns Native token cost
     */
    SvmQuery.prototype.getAuxiliaryNativeTokenCost = function (deposit) {
        return arch.svm.getAuxiliaryNativeTokenCost(deposit);
    };
    /**
     * @notice Return the fillRelay transaction for a given deposit
     * @param relayData RelayData instance, supplemented with destinationChainId
     * @param relayer SVM address of the relayer
     * @returns FillRelay transaction
     */
    SvmQuery.prototype.getFillRelayTx = function (relayData, signer, repaymentChainId, repaymentAddress) {
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, getFillRelayTx(this.spokePool, this.provider, relayData, signer, repaymentChainId, repaymentAddress)];
                    case 1: return [2 /*return*/, _a.sent()];
                }
            });
        });
    };
    /**
     * Retrieves the current price of a token
     * @param tokenSymbol A valid [CoinGecko-ID](https://api.coingecko.com/api/v3/coins/list)
     * @returns The resolved token price within the specified coingeckoBaseCurrency
     */
    SvmQuery.prototype.getTokenPrice = function (tokenSymbol) {
        return __awaiter(this, void 0, void 0, function () {
            var coingeckoInstance, _a, price;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        if (!this.symbolMapping[tokenSymbol])
                            throw new Error("".concat(tokenSymbol, " does not exist in mapping"));
                        coingeckoInstance = Coingecko.get(this.logger, this.coingeckoProApiKey);
                        return [4 /*yield*/, coingeckoInstance.getCurrentPriceByContract(this.symbolMapping[tokenSymbol].addresses[CHAIN_IDs.MAINNET], this.coingeckoBaseCurrency)];
                    case 1:
                        _a = _b.sent(), price = _a[1];
                        return [2 /*return*/, price];
                }
            });
        });
    };
    /**
     * Resolves the number of decimal places a token can have
     * @param tokenSymbol A valid Across-Enabled Token ID
     * @returns The number of decimals of precision for the corresponding tokenSymbol
     */
    SvmQuery.prototype.getTokenDecimals = function (tokenSymbol) {
        if (!this.symbolMapping[tokenSymbol])
            throw new Error("".concat(tokenSymbol, " does not exist in mapping"));
        return this.symbolMapping[tokenSymbol].decimals;
    };
    // The fill is too large; we need to simulate the transaction in a bundle.
    SvmQuery.prototype.estimateComputeUnits = function (relayData, relayer, repaymentChainId, repaymentAddress) {
        return __awaiter(this, void 0, void 0, function () {
            var provider, spokePoolAddr, voidSigner, _a, instructionParamsIxs, _fillRelayTx, computeUnitLimitIx, fillRelayTx, instructionParamsTxs, bundleTxns, executionAccounts, simulateBundleResponse, failure, totalCuSpent;
            var _this = this;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        provider = this.provider;
                        spokePoolAddr = toAddress(this.spokePool);
                        voidSigner = SolanaVoidSigner(relayer.toBase58());
                        return [4 /*yield*/, Promise.all([
                                getFillRelayViaInstructionParamsInstructions(spokePoolAddr, relayData, repaymentChainId, repaymentAddress, voidSigner, provider),
                                getIPFillRelayTx(this.spokePool, provider, relayData, voidSigner, repaymentChainId, repaymentAddress),
                            ])];
                    case 1:
                        _a = _b.sent(), instructionParamsIxs = _a[0], _fillRelayTx = _a[1];
                        computeUnitLimitIx = getSetComputeUnitLimitInstruction({ units: 10000000 });
                        fillRelayTx = pipe(_fillRelayTx, function (tx) { return appendTransactionMessageInstruction(computeUnitLimitIx, tx); });
                        return [4 /*yield*/, mapAsync(instructionParamsIxs, function (ix) { return __awaiter(_this, void 0, void 0, function () {
                                var _a;
                                return __generator(this, function (_b) {
                                    switch (_b.label) {
                                        case 0:
                                            _a = pipe;
                                            return [4 /*yield*/, createDefaultTransaction(provider, voidSigner)];
                                        case 1: return [2 /*return*/, _a.apply(void 0, [_b.sent(), function (tx) {
                                                    return appendTransactionMessageInstruction(ix, tx);
                                                }])];
                                    }
                                });
                            }); })];
                    case 2:
                        instructionParamsTxs = _b.sent();
                        bundleTxns = __spreadArray(__spreadArray([], instructionParamsTxs, true), [fillRelayTx], false).map(function (txn) {
                            var compiled = compileTransaction(txn);
                            return getBase64EncodedWireTransaction(compiled);
                        });
                        executionAccounts = bundleTxns.map(function () {
                            return { accountIndex: 0, addresses: [_this.spokePool.toBase58(), relayer.toBase58()] };
                        });
                        return [4 /*yield*/, provider
                                .simulateBundle({
                                encodedTransactions: bundleTxns,
                            }, {
                                skipSigVerify: true,
                                preExecutionAccountsConfigs: executionAccounts,
                                postExecutionAccountsConfigs: executionAccounts,
                            })
                                .send()];
                    case 3:
                        simulateBundleResponse = _b.sent();
                        // If the bundle simulation failed, then return data from the failure.
                        if (simulateBundleResponse.value.summary !== "succeeded") {
                            failure = simulateBundleResponse.value.summary.failed.error.TransactionFailure;
                            throw new Error("simulateBundle failed with result: ".concat(failure[1]));
                        }
                        totalCuSpent = simulateBundleResponse.value.transactionResults.reduce(function (sum, res) { return res.unitsConsumed + sum; }, BigInt(0));
                        return [2 /*return*/, totalCuSpent];
                }
            });
        });
    };
    return SvmQuery;
}());
export { SvmQuery };
//# sourceMappingURL=svmQuery.js.map