import { MessageTransmitterClient, SvmSpokeClient, TokenMessengerMinterClient } from "@across-protocol/contracts";
import { decodeFillStatusAccount, fetchState } from "@across-protocol/contracts/dist/src/svm/clients/SvmSpoke";
import { decodeMessageHeader } from "@across-protocol/contracts/dist/src/svm/web3-v1";
import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system";
import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS, fetchMint, getApproveCheckedInstruction, getCreateAssociatedTokenIdempotentInstruction, getCreateAssociatedTokenInstruction, } from "@solana-program/token";
import { AccountRole, appendTransactionMessageInstruction, fetchEncodedAccount, fetchEncodedAccounts, getAddressEncoder, getBase64EncodedWireTransaction, getProgramDerivedAddress, getSignatureFromTransaction, getU32Encoder, getU64Encoder, pipe, signTransactionMessageWithSigners, some, compileTransaction, } from "@solana/kit";
import assert from "assert";
import { arrayify } from "ethers/lib/utils";
import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../../constants";
import { FillStatus, } from "../../interfaces";
import { EvmAddress, SvmAddress, bs58, chainIsProd, chainIsSvm, chunk, getMessageHash, isDefined, isUnsafeDepositId, keccak256, mapAsync, unpackDepositEvent, unpackFillEvent, } from "../../utils";
import { createDefaultTransaction, getCCTPNoncePda, getEventAuthority, getFillStatusPda, getSelfAuthority, getStatePda, isDepositForBurnEvent, simulateAndDecode, toAddress, unwrapEventData, getRootBundlePda, getAcrossPlusMessageDecoder, getAccountMeta, getInstructionParamsPda, toSvmRelayData, } from "./";
import { SVM_LONG_TERM_STORAGE_SLOT_SKIPPED, SVM_SLOT_SKIPPED, isSolanaError } from "./provider";
import { SVMEventNames } from "./types";
import { getEmergencyDeleteRootBundleRootBundleId, getNearestSlotTime, isEmergencyDeleteRootBundleMessageBody, isRelayRootBundleMessageBody, } from "./utils";
/**
 * @note: Average Solana slot duration is about 400-500ms. We can be conservative
 *        and choose 400 to ensure that the most slots get included in our ranges
 */
export const SLOT_DURATION_MS = 400;
export const SOLANA_TX_SIZE_LIMIT = 1232;
export function getSlot(provider, commitment, logger) {
    return _callGetSlotWithRetry(provider, commitment, logger);
}
async function _callGetSlotWithRetry(provider, commitment, logger) {
    try {
        return await provider.getSlot({ commitment }).send();
    }
    catch (err) {
        if (isSolanaError(err)) {
            const { __code: code } = err.context;
            logger?.debug({
                at: "_getSlotWithRetry",
                message: "Caught error from getSlot()",
                code,
                commitment,
            });
        }
        // TODO: Implement retry logic once we better understand how these errors look:
        throw err;
    }
}
/**
 * Retrieves the chain time at a particular slot.
 */
export function getTimestampForSlot(provider, slotNumber, maxRetries = 2, logger) {
    return _callGetTimestampForSlotWithRetry(provider, slotNumber, 0, maxRetries, logger);
}
async function _callGetTimestampForSlotWithRetry(provider, slotNumber, retryAttempt, maxRetries, logger) {
    const slot = slotNumber.toString();
    let _timestamp;
    try {
        // @note: getBlockTime receives a slot number, not a block number.
        _timestamp = await provider.getBlockTime(slotNumber).send();
    }
    catch (err) {
        if (!isSolanaError(err)) {
            throw err;
        }
        const at = "getTimestampForSlot";
        const { __code: code } = err.context;
        switch (code) {
            case SVM_SLOT_SKIPPED:
            case SVM_LONG_TERM_STORAGE_SLOT_SKIPPED:
                // No block available for this slot; caller must decide on how to handle this.
                return undefined;
            default: {
                const message = "Caught error from getBlockTime()";
                logger?.debug({ at, message, errorCode: code, slot, retryAttempt, maxRetries });
                throw new Error(`Unhandled SVM getBlockTime() error for slot ${slot}: ${code}`, { cause: err });
            }
        }
    }
    // _timestamp should be a BigInt or undefined. If not undefined, ensure that conversion to number does not truncate.
    const timestamp = Number(_timestamp);
    assert(!isDefined(_timestamp) || BigInt(timestamp) === _timestamp, `Unexpected block timestamp for SVM slot ${slot}: ${_timestamp}`);
    return timestamp;
}
/**
 * Returns the current fill deadline buffer.
 * @param provider SVM Provider instance
 * @param statePda Spoke Pool's State PDA
 * @returns fill deadline buffer
 */
export async function getFillDeadline(provider, statePda) {
    const state = await fetchState(provider, statePda);
    return state.data.fillDeadlineBuffer;
}
/**
 * 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) {
    throw new Error("getDepositIdAtBlock: not implemented");
}
/**
 * Finds deposit events within a 2-day window ending at the specified slot.
 *
 * @remarks
 * This implementation uses a slot-limited search approach because Solana PDA state has
 * limitations that prevent directly referencing old deposit IDs. Unlike EVM chains where
 * we might use binary search across the entire chain history, in Solana we must query within
 * a constrained slot range.
 *
 * The search window is calculated by:
 * 1. Using the provided slot (or current confirmed slot if none is provided)
 * 2. Looking back 2 days worth of slots from that point
 *
 * We use a 2-day window because:
 * 1. Most valid deposits that need to be processed will be recent
 * 2. This covers multiple bundle submission periods
 * 3. It balances performance with practical deposit age
 *
 * @important
 * This function may return `undefined` for valid deposit IDs that are older than the search
 * window (approximately 2 days before the specified slot). This is an acceptable limitation
 * as deposits this old are typically not relevant to current operations.
 *
 * @param eventClient - SvmCpiEventsClient instance
 * @param depositId - The deposit ID to search for
 * @param slot - The slot to search up to (defaults to current slot). The search will look
 *              for deposits between (slot - secondsLookback) and slot.
 * @param secondsLookback - The number of seconds to look back for deposits (defaults to 2 days).
 * @returns The deposit if found within the slot window, undefined otherwise
 */
export async function findDeposit(eventClient, depositId, logger, slot, secondsLookback = 2 * 24 * 60 * 60 // 2 days
) {
    // We can only perform this search when we have a safe deposit ID.
    if (isUnsafeDepositId(depositId)) {
        throw new Error(`Cannot binary search for depositId ${depositId}`);
    }
    const provider = eventClient.getRpc();
    const opts = undefined;
    const { slot: currentSlot } = await getNearestSlotTime(provider, opts, logger);
    // If no slot is provided, use the current slot
    // If a slot is provided, ensure it's not in the future
    const endSlot = slot !== undefined ? BigInt(Math.min(Number(slot), Number(currentSlot))) : currentSlot;
    // Calculate start slot (approximately secondsLookback seconds earlier)
    const slotsInElapsed = BigInt(Math.round((secondsLookback * 1000) / SLOT_DURATION_MS));
    const startSlot = endSlot - slotsInElapsed;
    // Query for the deposit events with this limited slot range. Filter by deposit id.
    const depositEvent = (await eventClient.queryEvents("FundsDeposited", startSlot, endSlot))?.find((event) => depositId.eq(event.data.depositId));
    // If no deposit event is found, return undefined
    if (!depositEvent) {
        return undefined;
    }
    const txnIndex = 0;
    const logIndex = 0;
    const blockNumber = Number(depositEvent.slot);
    const txnRef = depositEvent.signature.toString();
    const rawData = unwrapEventData(depositEvent.data, ["depositId", "outputAmount"]);
    const { originChainId, ...deposit } = unpackDepositEvent({ ...rawData, blockNumber, txnRef, txnIndex, logIndex }, CHAIN_IDs.SOLANA);
    return {
        ...deposit,
    };
}
/**
 * Resolves the fill status of a deposit at a specific slot or at the current confirmed one.
 *
 * If no slot is provided, attempts to solve the fill status using the PDA. Otherwise, it is reconstructed from PDA events.
 *
 * @param programId - The spoke pool program ID.
 * @param relayData - Deposit information used to locate the fill status.
 * @param destinationChainId - Destination chain ID (must be an SVM chain).
 * @param provider - SVM provider instance.
 * @param svmEventsClient - SVM events client for querying events.
 * @param atHeight - (Optional) Specific slot number to query. Defaults to the latest confirmed slot.
 * @returns The fill status for the deposit at the specified or current slot.
 */
export async function relayFillStatus(programId, relayData, destinationChainId, svmEventsClient, logger, atHeight) {
    assert(chainIsSvm(destinationChainId), "Destination chain must be an SVM chain");
    const provider = svmEventsClient.getRpc();
    // Get fill status PDA using relayData
    const fillStatusPda = await getFillStatusPda(programId, relayData, destinationChainId);
    let toSlot = BigInt(atHeight ?? 0);
    // If no specific slot is requested, try fetching the current status from the PDA
    if (atHeight === undefined) {
        const commitment = "confirmed";
        const [fillStatusAccount, { slot: currentSlot, timestamp }] = await Promise.all([
            fetchEncodedAccount(provider, fillStatusPda, { commitment }),
            getNearestSlotTime(provider, { commitment }, logger),
        ]);
        toSlot = currentSlot;
        // If the PDA exists, return the stored fill status
        if (fillStatusAccount.exists) {
            const decodedAccountData = decodeFillStatusAccount(fillStatusAccount);
            return decodedAccountData.data.status;
        }
        // If the PDA doesn't exist and the deadline hasn't passed yet, the deposit must be unfilled,
        // since PDAs can't be closed before the fill deadline.
        else if (timestamp < relayData.fillDeadline) {
            return FillStatus.Unfilled;
        }
    }
    // If status couldn't be determined from the PDA, or if a specific slot was requested, reconstruct from events.
    return resolveFillStatusFromPdaEvents(fillStatusPda, toSlot, svmEventsClient);
}
/**
 * Resolves fill statuses for multiple deposits at a specific or latest confirmed slot,
 * using PDAs when possible and falling back to events if needed.
 *
 * @param programId The spoke pool program ID.
 * @param relayData An array of relay data to resolve fill statuses for.
 * @param destinationChainId The destination chain ID (must be an SVM chain).
 * @param provider SVM Provider instance.
 * @param svmEventsClient SVM events client instance for querying events.
 * @param atHeight (Optional) The slot number to query at. If omitted, queries the latest confirmed slot.
 * @returns An array of fill statuses for the specified deposits at the requested slot (or at the current confirmed slot).
 */
export async function fillStatusArray(programId, relayData, destinationChainId, svmEventsClient, logger, atHeight) {
    assert(chainIsSvm(destinationChainId), "Destination chain must be an SVM chain");
    const provider = svmEventsClient.getRpc();
    const chunkSize = 100;
    const chunkedRelayData = chunk(relayData, chunkSize);
    // Get all PDAs
    const fillStatusPdas = (await Promise.all(chunkedRelayData.map((relayDataChunk) => Promise.all(relayDataChunk.map((relayData) => getFillStatusPda(programId, relayData, destinationChainId)))))).flat();
    if (atHeight !== undefined && logger) {
        logger.warn({
            at: "SvmSpokeUtils#fillStatusArray",
            message: "Querying specific slots for large arrays is slow. For current status, omit 'atHeight' param to use latest confirmed slot instead.",
        });
    }
    // If no specific slot is requested, try fetching current statuses from PDAs
    // Otherwise, initialize all statuses as undefined
    const fillStatuses = atHeight === undefined
        ? await fetchBatchFillStatusFromPdaAccounts(provider, fillStatusPdas, relayData, logger)
        : new Array(relayData.length).fill(undefined);
    // Collect indices of deposits that still need their status resolved
    const missingStatuses = fillStatuses.reduce((acc, status, index) => {
        if (status === undefined) {
            acc.push(index);
        }
        return acc;
    }, []);
    // Chunk the missing deposits for batch processing
    const missingChunked = chunk(missingStatuses, chunkSize);
    const missingResults = [];
    // Determine the toSlot to use for event reconstruction
    const opts = undefined;
    const toSlot = atHeight ? BigInt(atHeight) : (await getNearestSlotTime(provider, opts, logger)).slot;
    // @note: This path is mostly used for deposits past their fill deadline.
    // If it becomes a bottleneck, consider returning an "Unknown" status that can be handled downstream.
    for (const chunk of missingChunked) {
        const chunkResults = await Promise.all(chunk.map(async (missingIndex) => {
            return {
                index: missingIndex,
                fillStatus: await resolveFillStatusFromPdaEvents(fillStatusPdas[missingIndex], toSlot, svmEventsClient),
            };
        }));
        missingResults.push(...chunkResults);
    }
    // Fill in missing statuses back to the result array
    missingResults.forEach(({ index, fillStatus }) => {
        fillStatuses[index] = fillStatus;
    });
    return fillStatuses;
}
/**
 * Finds the `FilledRelay` event for a given deposit within the provided slot range.
 *
 * @param relayData - Deposit information that is used to complete a fill.
 * @param destinationChainId - Destination chain ID (must be an SVM chain).
 * @param svmEventsClient - SVM events client instance for querying events.
 * @param fromSlot - Starting slot to search.
 * @param toSlot (Optional) Ending slot to search. If not provided, the current confirmed slot will be used.
 * @returns The fill event with block info, or `undefined` if not found.
 */
export async function findFillEvent(relayData, destinationChainId, svmEventsClient, fromSlot, toSlot, logger) {
    assert(chainIsSvm(destinationChainId), "Destination chain must be an SVM chain");
    const opts = undefined;
    toSlot ??= Number((await getNearestSlotTime(svmEventsClient.getRpc(), opts, logger)).slot);
    // Get fillStatus PDA using relayData
    const programId = svmEventsClient.getProgramAddress();
    const fillStatusPda = await getFillStatusPda(programId, relayData, destinationChainId);
    // Get fill events from fillStatus PDA
    const fillEvents = await svmEventsClient.queryDerivedAddressEvents(SVMEventNames.FilledRelay, fillStatusPda, BigInt(fromSlot), BigInt(toSlot), { limit: 10 });
    assert(fillEvents.length <= 1, `Expected at most one fill event for ${fillStatusPda}, got ${fillEvents.length}`);
    const [rawEvent] = fillEvents;
    if (!isDefined(rawEvent)) {
        return;
    }
    const rawFill = unwrapEventData(rawEvent.data, ["depositId", "inputAmount"]);
    const fill = unpackFillEvent(rawFill, destinationChainId);
    return fill;
}
/**
 * @param spokePool Address (program ID) of the SvmSpoke.
 * @param relayData RelayData instance, supplemented with destinationChainId
 * @param relayer Address of the relayer filling the deposit.
 * @param repaymentChainId Optional repaymentChainId (defaults to destinationChainId).
 * @returns An Ethers UnsignedTransaction instance.
 */
export async function fillRelayInstruction(spokePool, relayData, signer, recipientTokenAccount, repaymentAddress, repaymentChainId) {
    const program = toAddress(spokePool);
    assert(repaymentAddress.isValidOn(repaymentChainId), `Invalid repayment address for chain ${repaymentChainId}: ${repaymentAddress.toNative()}.`);
    const messageHash = getMessageHash(relayData.message);
    const _relayDataHash = getRelayDataHash({ ...relayData, messageHash }, relayData.destinationChainId);
    const relayDataHash = new Uint8Array(Buffer.from(_relayDataHash.slice(2), "hex"));
    const relayer = SvmAddress.from(signer.address);
    const [statePda, fillStatusPda, eventAuthority, delegatePda, relayerTokenAccount] = await Promise.all([
        getStatePda(program),
        getFillStatusPda(program, relayData, relayData.destinationChainId),
        getEventAuthority(program),
        getFillRelayDelegatePda(relayDataHash, BigInt(repaymentChainId), signer.address, program),
        getAssociatedTokenAddress(relayer, relayData.outputToken),
    ]);
    const svmRelayData = toSvmRelayData(relayData);
    return SvmSpokeClient.getFillRelayInstruction({
        signer,
        state: statePda,
        delegate: delegatePda,
        mint: svmRelayData.outputToken,
        relayerTokenAccount: relayerTokenAccount,
        recipientTokenAccount: recipientTokenAccount,
        fillStatus: fillStatusPda,
        eventAuthority,
        program,
        relayHash: relayDataHash,
        relayData: some(svmRelayData),
        repaymentChainId: some(BigInt(repaymentChainId)),
        repaymentAddress: some(toAddress(repaymentAddress)),
    });
}
/**
 * @param mint Address of the token corresponding to the account being made.
 * @param relayer Address of the relayer filling the deposit.
 * @returns An instruction for creating a new token account.
 */
export function createTokenAccountsInstruction(mint, relayer) {
    return SvmSpokeClient.getCreateTokenAccountsInstruction({
        signer: relayer,
        mint: toAddress(mint),
    });
}
/**
 * @notice Return the fillRelay transaction for a given deposit
 * @param spokePoolAddr Address of the spoke pool we're trying to fill through
 * @param solanaClient RPC client to interact with Solana chain
 * @param relayData RelayData instance, supplemented with destinationChainId
 * @param signer signer associated with the relayer creating a Fill. Can be VoidSigner for gas estimation
 * @param repaymentChainId Chain id where relayer repayment is desired
 * @param repaymentAddress Address to which repayment will go to on repaymentChainId
 * @returns FillRelay transaction
 */
export async function getFillRelayTx(spokePoolAddr, solanaClient, relayData, signer, repaymentChainId, repaymentAddress) {
    const svmRelayData = toSvmRelayData(relayData);
    assert(repaymentAddress.isValidOn(repaymentChainId), `getFillRelayTx: repayment address ${repaymentAddress} not valid on chain ${repaymentChainId})`);
    const program = toAddress(spokePoolAddr);
    const messageHash = getMessageHash(relayData.message);
    const _relayDataHash = getRelayDataHash({ ...relayData, messageHash }, relayData.destinationChainId);
    const relayDataHash = new Uint8Array(Buffer.from(_relayDataHash.slice(2), "hex"));
    const [state, delegate, mintInfo, fillStatus, eventAuthority] = await Promise.all([
        getStatePda(program),
        getFillRelayDelegatePda(relayDataHash, BigInt(repaymentChainId), toAddress(repaymentAddress), program),
        getMintInfo(solanaClient, svmRelayData.outputToken),
        getFillStatusPda(program, relayData, relayData.destinationChainId),
        getEventAuthority(program),
    ]);
    const [recipientAta, relayerAta] = await Promise.all([
        getAssociatedTokenAddress(relayData.recipient, relayData.outputToken, mintInfo.programAddress),
        getAssociatedTokenAddress(SvmAddress.from(signer.address), relayData.outputToken, mintInfo.programAddress),
    ]);
    const recipientAtaEncodedAccount = await fetchEncodedAccount(solanaClient, recipientAta);
    // Add remaining accounts if the relayData has a non-empty message.
    // @dev ! since in the context of creating a `fillRelayTx`, `relayData` must be defined.
    const remainingAccounts = [];
    if (relayData.message !== "0x") {
        const acrossPlusMessage = deserializeMessage(relayData.message);
        // The first `remainingAccount` _must_ be the handler address.
        // https://github.com/across-protocol/contracts/blob/3310f8dc716407a5f97ef5fd2eae63df83251f2f/programs/svm-spoke/src/utils/message_utils.rs#L36.
        remainingAccounts.push(getAccountMeta(acrossPlusMessage.handler, true));
        remainingAccounts.push(...acrossPlusMessage.accounts.map((account, idx) => getAccountMeta(account, idx < acrossPlusMessage.accounts.length - acrossPlusMessage.read_only_len)));
    }
    const fillInput = {
        signer: signer,
        state,
        delegate,
        mint: svmRelayData.outputToken,
        relayerTokenAccount: relayerAta,
        recipientTokenAccount: recipientAta,
        fillStatus,
        tokenProgram: mintInfo.programAddress,
        associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
        systemProgram: SYSTEM_PROGRAM_ADDRESS,
        eventAuthority,
        program,
        relayHash: relayDataHash,
        relayData: svmRelayData,
        repaymentChainId: BigInt(repaymentChainId),
        repaymentAddress: toAddress(repaymentAddress),
    };
    // Pass createRecipientAtaIfNeeded =true to the createFillInstruction function to create the recipient token account
    // if it doesn't exist.
    return createFillInstruction(signer, solanaClient, fillInput, svmRelayData, mintInfo.data.decimals, !recipientAtaEncodedAccount.exists, remainingAccounts);
}
/**
 * Creates a fill instruction with an instruction params PDA as the relayData input.
 * @param spokePoolAddr Address of the spoke pool we're trying to fill through
 * @param solanaClient RPC client to interact with Solana chain
 * @param relayData RelayData instance, supplemented with destinationChainId
 * @param signer signer associated with the relayer creating a Fill. Can be VoidSigner for gas estimation
 * @param repaymentChainId Chain id where relayer repayment is desired
 * @param repaymentAddress Address to which repayment will go to on repaymentChainId
 * @returns FillRelay transaction
 */
export async function getIPFillRelayTx(spokePoolAddr, solanaClient, relayData, signer, repaymentChainId, repaymentAddress) {
    const program = toAddress(spokePoolAddr);
    const _relayDataHash = getRelayDataHash({ ...relayData, messageHash: getMessageHash(relayData.message) }, relayData.destinationChainId);
    const relayDataHash = new Uint8Array(Buffer.from(_relayDataHash.slice(2), "hex"));
    const [state, delegate, instructionParams] = await Promise.all([
        getStatePda(program),
        getFillRelayDelegatePda(relayDataHash, BigInt(repaymentChainId), toAddress(repaymentAddress), program),
        getInstructionParamsPda(program, signer.address),
    ]);
    const mint = toAddress(relayData.outputToken);
    const mintInfo = await getMintInfo(solanaClient, mint);
    const [recipientAta, relayerAta, fillStatus, eventAuthority] = await Promise.all([
        getAssociatedTokenAddress(relayData.recipient, relayData.outputToken, mintInfo.programAddress),
        getAssociatedTokenAddress(SvmAddress.from(signer.address), relayData.outputToken, mintInfo.programAddress),
        getFillStatusPda(program, relayData, relayData.destinationChainId),
        getEventAuthority(program),
    ]);
    const recipientAtaEncodedAccount = await fetchEncodedAccount(solanaClient, recipientAta);
    // Add remaining accounts if the relayData has a non-empty message.
    // @dev ! since in the context of creating a `fillRelayTx`, `relayData` must be defined.
    const remainingAccounts = [];
    if (relayData.message !== "0x") {
        const acrossPlusMessage = deserializeMessage(relayData.message);
        // The first `remainingAccount` _must_ be the handler address.
        // https://github.com/across-protocol/contracts/blob/3310f8dc716407a5f97ef5fd2eae63df83251f2f/programs/svm-spoke/src/utils/message_utils.rs#L36.
        remainingAccounts.push(getAccountMeta(acrossPlusMessage.handler, true));
        remainingAccounts.push(...acrossPlusMessage.accounts.map((account, idx) => getAccountMeta(account, idx < acrossPlusMessage.accounts.length - acrossPlusMessage.read_only_len)));
    }
    const fillInput = {
        signer: signer,
        state,
        delegate,
        mint,
        relayerTokenAccount: relayerAta,
        recipientTokenAccount: recipientAta,
        fillStatus,
        tokenProgram: mintInfo.programAddress,
        associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
        systemProgram: SYSTEM_PROGRAM_ADDRESS,
        eventAuthority,
        program,
        instructionParams,
        relayHash: relayDataHash,
        relayData: null,
        repaymentChainId: null,
        repaymentAddress: null,
    };
    // Pass createRecipientAtaIfNeeded =true to the createFillInstruction function to create the recipient token account
    // if it doesn't exist.
    return createFillInstruction(signer, solanaClient, fillInput, { outputAmount: relayData.outputAmount.toBigInt(), recipient: toAddress(relayData.recipient) }, mintInfo.data.decimals, !recipientAtaEncodedAccount.exists, remainingAccounts);
}
/**
 * Creates a fill instruction.
 * @param signer - The signer of the transaction.
 * @param solanaClient - The Solana client.
 * @param fillInput - The fill input.
 * @param tokenDecimals - The token decimals.
 * @param createRecipientAta - Whether to create a recipient token account.
 * @returns The fill instruction.
 */
export const createFillInstruction = async (signer, solanaClient, fillInput, relayData, tokenDecimals, createRecipientAta = false, remainingAccounts = []) => {
    const mintInfo = await getMintInfo(solanaClient, fillInput.mint);
    const approveIx = getApproveCheckedInstruction({
        source: fillInput.relayerTokenAccount,
        mint: fillInput.mint,
        delegate: fillInput.delegate,
        owner: fillInput.signer,
        amount: relayData.outputAmount,
        decimals: tokenDecimals,
    }, {
        programAddress: mintInfo.programAddress,
    });
    const getCreateAssociatedTokenIx = () => getCreateAssociatedTokenInstruction({
        payer: signer,
        owner: relayData.recipient,
        mint: fillInput.mint,
        ata: fillInput.recipientTokenAccount,
        systemProgram: SYSTEM_PROGRAM_ADDRESS,
        tokenProgram: fillInput.tokenProgram,
    });
    const createFillIx = SvmSpokeClient.getFillRelayInstruction(fillInput);
    // Add remaining accounts.
    createFillIx.accounts.push(...remainingAccounts);
    return pipe(await createDefaultTransaction(solanaClient, signer), (tx) => (createRecipientAta ? appendTransactionMessageInstruction(getCreateAssociatedTokenIx(), tx) : tx), (tx) => appendTransactionMessageInstruction(approveIx, tx), (tx) => appendTransactionMessageInstruction(createFillIx, tx));
};
export function deserializeMessage(_message) {
    const message = new Uint8Array(Buffer.from(_message.startsWith("0x") ? _message.slice(2) : _message, "hex"));
    // Add remaining accounts if the relayData has a non-empty message.
    // @dev ! since in the context of creating a `fillRelayTx`, `relayData` must be defined.
    const acrossPlusMessageDecoder = getAcrossPlusMessageDecoder();
    const deserialized = acrossPlusMessageDecoder.decode(message);
    const valueAmountMethod2 = extractValueAmount(message);
    assert(deserialized.value_amount === valueAmountMethod2, "svm | deserializeMessage: Deserialization mismatch for value_amount");
    return deserialized;
}
/**
 * Creates a deposit instruction.
 * @param signer - The signer of the transaction.
 * @param solanaClient - The Solana client.
 * @param depositInput - The deposit input.
 * @param tokenDecimals - The token decimals.
 * @param createVaultAtaIfNeeded - Whether to create a vault token account.
 * @returns The deposit instruction.
 */
export const createDepositInstruction = async (signer, solanaClient, depositInput, tokenDecimals, createVaultAtaIfNeeded = true) => {
    const getCreateAssociatedTokenIdempotentIx = () => getCreateAssociatedTokenIdempotentInstruction({
        payer: signer,
        owner: depositInput.state,
        mint: depositInput.mint,
        ata: depositInput.vault,
        systemProgram: depositInput.systemProgram,
        tokenProgram: depositInput.tokenProgram,
    });
    const mintInfo = await getMintInfo(solanaClient, depositInput.mint);
    const approveIx = getApproveCheckedInstruction({
        source: depositInput.depositorTokenAccount,
        mint: depositInput.mint,
        delegate: depositInput.delegate,
        owner: depositInput.depositor,
        amount: depositInput.inputAmount,
        decimals: tokenDecimals,
    }, {
        programAddress: mintInfo.programAddress,
    });
    const depositIx = SvmSpokeClient.getDepositInstruction(depositInput);
    return pipe(await createDefaultTransaction(solanaClient, signer), (tx) => createVaultAtaIfNeeded ? appendTransactionMessageInstruction(getCreateAssociatedTokenIdempotentIx(), tx) : tx, (tx) => appendTransactionMessageInstruction(approveIx, tx), (tx) => appendTransactionMessageInstruction(depositIx, tx));
};
/**
 * Creates a request slow fill instruction.
 * @param signer - The signer of the transaction.
 * @param solanaClient - The Solana client.
 * @param requestSlowFillInput - The input arguments for the `requestSlowFill` instruction.
 * @returns The request slow fill instruction.
 */
export const createRequestSlowFillInstruction = async (signer, solanaClient, requestSlowFillInput) => {
    const requestSlowFillIx = SvmSpokeClient.getRequestSlowFillInstruction(requestSlowFillInput);
    return pipe(await createDefaultTransaction(solanaClient, signer), (tx) => appendTransactionMessageInstruction(requestSlowFillIx, tx));
};
/**
 * @notice Return the requestSlowFill transaction for a given deposit
 * @param spokePoolAddr Address of the spoke pool we're trying to fill through
 * @param solanaClient RPC client to interact with Solana chain
 * @param relayData RelayData instance, supplemented with destinationChainId
 * @param signer signer associated with the relayer creating a Fill.
 * @returns requestSlowFill transaction
 */
export async function getSlowFillRequestTx(spokePoolAddr, solanaClient, relayData, signer) {
    const program = toAddress(spokePoolAddr);
    const messageHash = getMessageHash(relayData.message);
    const relayDataHash = getRelayDataHash({ ...relayData, messageHash }, relayData.destinationChainId);
    const [state, fillStatus, eventAuthority] = await Promise.all([
        getStatePda(program),
        getFillStatusPda(program, relayData, relayData.destinationChainId),
        getEventAuthority(program),
    ]);
    const svmRelayData = toSvmRelayData(relayData);
    const requestSlowFillInput = {
        signer,
        state,
        fillStatus,
        eventAuthority,
        program,
        relayHash: arrayify(relayDataHash),
        relayData: svmRelayData,
        systemProgram: SYSTEM_PROGRAM_ADDRESS,
    };
    return createRequestSlowFillInstruction(signer, solanaClient, requestSlowFillInput);
}
/**
 * Creates a close fill PDA instruction.
 * @param signer - The signer of the transaction.
 * @param solanaClient - The Solana client.
 * @param fillStatusPda - The fill status PDA.
 * @returns The close fill PDA instruction.
 */
export const createCloseFillPdaInstruction = async (signer, solanaClient, fillStatusPda) => {
    const closeFillPdaIx = SvmSpokeClient.getCloseFillPdaInstruction({
        signer,
        state: await getStatePda(SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS),
        fillStatus: fillStatusPda,
    });
    return pipe(await createDefaultTransaction(solanaClient, signer), (tx) => appendTransactionMessageInstruction(closeFillPdaIx, tx));
};
export const createReceiveMessageInstruction = async (signer, solanaClient, input, remainingAccounts) => {
    const receiveMessageIx = MessageTransmitterClient.getReceiveMessageInstruction(input);
    receiveMessageIx.accounts.push(...remainingAccounts);
    return pipe(await createDefaultTransaction(solanaClient, signer), (tx) => appendTransactionMessageInstruction(receiveMessageIx, tx));
};
export async function getAssociatedTokenAddress(owner, mint, tokenProgramId = TOKEN_PROGRAM_ADDRESS) {
    const encoder = getAddressEncoder();
    const [associatedToken] = await getProgramDerivedAddress({
        programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
        seeds: [encoder.encode(toAddress(owner)), encoder.encode(tokenProgramId), encoder.encode(toAddress(mint))],
    });
    return associatedToken;
}
export function getRelayDataHash(relayData, destinationChainId) {
    assert(relayData.messageHash.startsWith("0x"), "Message hash must be a hex string");
    const uint64Encoder = getU64Encoder();
    const svmRelayData = toSvmRelayData(relayData);
    const relayDataEncoder = SvmSpokeClient.getRelayDataEncoder();
    const encodedRelayData = relayDataEncoder.encode(svmRelayData);
    const encodedMessage = Buffer.from(relayData.message.slice(2), "hex");
    const encodedMessageHash = Uint8Array.from(Buffer.from(relayData.messageHash.slice(2), "hex"));
    // Reformat the encoded relay data the same way it is done in the SvmSpoke:
    // https://github.com/across-protocol/contracts/blob/3310f8dc716407a5f97ef5fd2eae63df83251f2f/programs/svm-spoke/src/utils/merkle_proof_utils.rs#L5
    // We want to use messageHash always so we can construct the relayDataHash just from the Fill.
    // If we don't have a message, we can just pass an empty message here.
    const messageOffset = encodedRelayData.length - 4 - encodedMessage.length;
    const contentToHash = Buffer.concat([
        encodedRelayData.slice(0, messageOffset),
        encodedMessageHash,
        Uint8Array.from(uint64Encoder.encode(BigInt(destinationChainId))),
    ]);
    return keccak256(contentToHash);
}
async function resolveFillStatusFromPdaEvents(fillStatusPda, toSlot, svmEventsClient) {
    // Get fill and requested slow fill events from fillStatus PDA
    const eventsToQuery = [SVMEventNames.FilledRelay, SVMEventNames.RequestedSlowFill];
    const relevantEvents = (await Promise.all(eventsToQuery.map((eventName) => 
    // PDAs should have only a few events, requesting up to 10 should be enough.
    svmEventsClient.queryDerivedAddressEvents(eventName, fillStatusPda, undefined, toSlot, { limit: 10 })))).flat();
    if (relevantEvents.length === 0) {
        // No fill or requested slow fill events found for this PDA
        return FillStatus.Unfilled;
    }
    // Sort events in ascending order of slot number
    relevantEvents.sort((a, b) => Number(a.slot - b.slot));
    // At this point we have an ordered array of only fill and requested slow fill events and
    // since it's not possible to submit a slow fill request once a fill has been submitted,
    // we can use the last event in the list to determine the fill status at the requested slot.
    const fillStatusEvent = relevantEvents.pop();
    switch (fillStatusEvent.name) {
        case SVMEventNames.FilledRelay:
            return FillStatus.Filled;
        case SVMEventNames.RequestedSlowFill:
            return FillStatus.RequestedSlowFill;
        default:
            throw new Error(`Unexpected event name: ${fillStatusEvent.name}`);
    }
}
/**
 * Attempts to resolve the fill status for an array of deposits by reading their fillStatus PDAs.
 *
 * - If a PDA exists, the status is read directly from it.
 * - If the PDA does not exist but the deposit's fill deadline has not passed, the deposit is considered unfilled.
 * - If the PDA does not exist and the fill deadline has passed, the status cannot be determined and is set to undefined.
 *
 * Assumes PDAs can only be closed after the fill deadline expires.
 *
 * @param provider SVM provider instance
 * @param fillStatusPdas An array of fill status PDAs to retrieve the fill status for.
 * @param relayData An array of relay data from which the fill status PDAs were derived.
 */
async function fetchBatchFillStatusFromPdaAccounts(provider, fillStatusPdas, relayDataArray, logger) {
    const chunkSize = 100; // SVM method getMultipleAccounts allows a max of 100 addresses per request
    const commitment = "confirmed";
    const [pdaAccounts, { timestamp }] = await Promise.all([
        Promise.all(chunk(fillStatusPdas, chunkSize).map((chunk) => fetchEncodedAccounts(provider, chunk, { commitment }))),
        getNearestSlotTime(provider, { commitment }, logger),
    ]);
    const fillStatuses = pdaAccounts.flat().map((account, index) => {
        // If the PDA exists, we can fetch the status directly.
        if (account.exists) {
            const decodedAccount = decodeFillStatusAccount(account);
            return decodedAccount.data.status;
        }
        // If the PDA doesn't exist and the deadline hasn't passed yet, the deposit must be unfilled,
        // since PDAs can't be closed before the fill deadline.
        if (timestamp < relayDataArray[index].fillDeadline) {
            return FillStatus.Unfilled;
        }
        // If the PDA doesn't exist and the fill deadline has passed, then the status can't be determined and is set to undefined.
        return undefined;
    });
    return fillStatuses;
}
export async function getIPForFillRelayTxs(spokePool, relayData, repaymentChainId, repaymentAddress, signer, provider) {
    const ixs = await getFillRelayViaInstructionParamsInstructions(toAddress(spokePool), relayData, repaymentChainId, repaymentAddress, signer, provider);
    const txns = await mapAsync(ixs, async (ix) => {
        return pipe(await createDefaultTransaction(provider, signer), (tx) => appendTransactionMessageInstruction(ix, tx));
    });
    return txns;
}
/**
 * Returns a set of instructions to execute to fill a relay via instruction params.
 * @param spokePool The program ID of the Solana spoke pool.
 * @param relayData The relay data to write to the instruction params PDA.
 * @param repaymentChainId: The chain ID to take repayment.
 * @param repaymentAddress: The address to receive repayment on the specified repayment chain.
 * @param signer The transaction signer and authority of the instruction params PDA.
 * @param maxWriteSize The maximum fragment size to write to instruction params.
 */
export async function getFillRelayViaInstructionParamsInstructions(spokePool, relayData, repaymentChainId, repaymentAddress, signer, provider, maxWriteSize = 450) {
    const instructionParams = await getInstructionParamsPda(spokePool, signer.address);
    const encodedInstructionParams = await fetchEncodedAccount(provider, instructionParams);
    const relayDataEncoder = SvmSpokeClient.getFillRelayParamsEncoder();
    const svmRelayData = toSvmRelayData(relayData);
    const encodedRelayData = relayDataEncoder.encode({
        relayData: svmRelayData,
        repaymentChainId,
        repaymentAddress: toAddress(repaymentAddress),
    });
    const initInstructionParamsIx = SvmSpokeClient.getInitializeInstructionParamsInstruction({
        signer,
        instructionParams,
        totalSize: encodedRelayData.length,
    });
    const instructions = [initInstructionParamsIx];
    for (let i = 0; i <= encodedRelayData.length / maxWriteSize; ++i) {
        const offset = i * maxWriteSize;
        const offsetEnd = Math.min(offset + maxWriteSize, encodedRelayData.length);
        const fragment = encodedRelayData.slice(offset, offsetEnd);
        const writeInstructionParamsIx = SvmSpokeClient.getWriteInstructionParamsFragmentInstruction({
            signer,
            instructionParams,
            offset,
            fragment,
        });
        instructions.push(writeInstructionParamsIx);
    }
    return encodedInstructionParams.exists
        ? [SvmSpokeClient.getCloseInstructionParamsInstruction({ signer, instructionParams }), ...instructions]
        : instructions;
}
/**
 * Returns the delegate PDA for deposit.
 */
export async function getDepositDelegatePda(depositData, programId) {
    const addrEnc = getAddressEncoder();
    const u64 = getU64Encoder();
    const u32 = getU32Encoder();
    const parts = [
        Uint8Array.from(addrEnc.encode(depositData.depositor)),
        Uint8Array.from(addrEnc.encode(depositData.recipient)),
        Uint8Array.from(addrEnc.encode(depositData.inputToken)),
        Uint8Array.from(addrEnc.encode(depositData.outputToken)),
        Uint8Array.from(u64.encode(depositData.inputAmount)),
        Uint8Array.from(depositData.outputAmount),
        Uint8Array.from(u64.encode(depositData.destinationChainId)),
        Uint8Array.from(addrEnc.encode(depositData.exclusiveRelayer)),
        Uint8Array.from(u32.encode(depositData.quoteTimestamp)),
        Uint8Array.from(u32.encode(depositData.fillDeadline)),
        Uint8Array.from(u32.encode(depositData.exclusivityParameter)),
        Uint8Array.from(u32.encode(BigInt(depositData.message.length))),
        depositData.message,
    ];
    const seedHash = Buffer.from(keccak256(Buffer.concat(parts)).slice(2), "hex");
    const [pda] = await getProgramDerivedAddress({
        programAddress: programId,
        seeds: [Buffer.from("delegate"), seedHash],
    });
    return pda;
}
/**
 * Returns the delegate PDA for depositNow.
 */
export async function getDepositNowDelegatePda(depositData, programId) {
    const addrEnc = getAddressEncoder();
    const u64 = getU64Encoder();
    const u32 = getU32Encoder();
    const parts = [
        Uint8Array.from(addrEnc.encode(depositData.depositor)),
        Uint8Array.from(addrEnc.encode(depositData.recipient)),
        Uint8Array.from(addrEnc.encode(depositData.inputToken)),
        Uint8Array.from(addrEnc.encode(depositData.outputToken)),
        Uint8Array.from(u64.encode(depositData.inputAmount)),
        Uint8Array.from(depositData.outputAmount),
        Uint8Array.from(u64.encode(depositData.destinationChainId)),
        Uint8Array.from(addrEnc.encode(depositData.exclusiveRelayer)),
        Uint8Array.from(u32.encode(depositData.fillDeadlineOffset)),
        Uint8Array.from(u32.encode(depositData.exclusivityPeriod)),
        Uint8Array.from(u32.encode(BigInt(depositData.message.length))),
        depositData.message,
    ];
    const seedHash = Buffer.from(keccak256(Buffer.concat(parts)).slice(2), "hex");
    const [pda] = await getProgramDerivedAddress({
        programAddress: programId,
        seeds: [Buffer.from("delegate"), seedHash],
    });
    return pda;
}
/**
 * Returns the fill-delegate PDA for fillRelay.
 */
export async function getFillRelayDelegatePda(relayHash, repaymentChainId, repaymentAddress, programId) {
    const addrEnc = getAddressEncoder();
    const u64 = getU64Encoder();
    const parts = [
        relayHash,
        Uint8Array.from(u64.encode(repaymentChainId)),
        Uint8Array.from(addrEnc.encode(repaymentAddress)),
    ];
    const seedHash = Buffer.from(keccak256(Buffer.concat(parts)).slice(2), "hex");
    const [pda] = await getProgramDerivedAddress({
        programAddress: programId,
        seeds: [Buffer.from("delegate"), seedHash],
    });
    return pda;
}
/**
 * Checks if a CCTP message has been processed.
 * @param solanaClient The Solana client.
 * @param signer The signer of the transaction.
 * @param nonce The nonce to check.
 * @param sourceDomain The source domain.
 * @returns True if the message has been processed, false otherwise.
 * @dev This function intentionally does not have error handling for `getCCTPNoncePda` nor `simulateAndDecode` since
 * the error handling would have to account for the asynchronous opening/closing of PDAs, which is better handled downstream,
 * where the caller of this function has more context.
 */
export const hasCCTPV1MessageBeenProcessed = async (solanaClient, signer, nonce, sourceDomain, latestBlockhash) => {
    const noncePda = await getCCTPNoncePda(solanaClient, signer, nonce, sourceDomain);
    const isNonceUsedIx = MessageTransmitterClient.getIsNonceUsedInstruction({
        nonce: nonce,
        usedNonces: noncePda,
    });
    const parserFunction = (buf) => {
        if (buf.length != 1) {
            throw new Error("Invalid buffer length for isNonceUsedIx");
        }
        return Boolean(buf[0]);
    };
    return await simulateAndDecode(solanaClient, isNonceUsedIx, signer, parserFunction, latestBlockhash);
};
/**
 * Returns the account metas for a tokenless message.
 * @returns The account metas for a tokenless message.
 */
export async function getAccountMetasForTokenlessMessage(solanaClient, signer, messageBytes) {
    const messageHex = messageBytes.slice(2);
    const messageHeader = decodeMessageHeader(Buffer.from(messageHex, "hex"));
    const programAddress = SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS;
    const statePda = await getStatePda(programAddress);
    const selfAuthority = await getSelfAuthority();
    const eventAuthority = await getEventAuthority(programAddress);
    const base = [
        { address: statePda, role: AccountRole.READONLY },
        { address: selfAuthority, role: AccountRole.READONLY },
        { address: programAddress, role: AccountRole.READONLY },
    ];
    if (isRelayRootBundleMessageBody(messageHeader.messageBody)) {
        const { data: { rootBundleId }, } = await SvmSpokeClient.fetchState(solanaClient, statePda);
        const rootBundle = await getRootBundlePda(programAddress, rootBundleId);
        return [
            ...base,
            { address: signer.address, role: AccountRole.WRITABLE },
            { address: statePda, role: AccountRole.WRITABLE },
            { address: rootBundle, role: AccountRole.WRITABLE },
            { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
            { address: eventAuthority, role: AccountRole.READONLY },
            { address: programAddress, role: AccountRole.READONLY },
        ];
    }
    if (isEmergencyDeleteRootBundleMessageBody(messageHeader.messageBody)) {
        const rootBundleId = getEmergencyDeleteRootBundleRootBundleId(messageHeader.messageBody);
        const rootBundle = await getRootBundlePda(programAddress, rootBundleId);
        return [
            ...base,
            { address: signer.address, role: AccountRole.READONLY },
            { address: statePda, role: AccountRole.READONLY },
            { address: rootBundle, role: AccountRole.WRITABLE },
            { address: eventAuthority, role: AccountRole.READONLY },
            { address: programAddress, role: AccountRole.READONLY },
        ];
    }
    return [
        ...base,
        { address: statePda, role: AccountRole.WRITABLE },
        { address: eventAuthority, role: AccountRole.READONLY },
        { address: programAddress, role: AccountRole.READONLY },
    ];
}
/**
 * Returns the required PDAs for a deposit message.
 * @param hubChainId The chain ID of the corresponding Across hub.
 * @param cctpSourceDomain The source chain (Solana) domain ID.
 * @param tokenMessengerMinter The token messenger minter address.
 * @param messageTransmitterAddress The message transmitter address.
 */
export async function getCCTPDepositAccounts(hubChainId, cctpDestinationDomainId, tokenMessengerMinterAddress, messageTransmitterAddress) {
    const l2Usdc = SvmAddress.from(TOKEN_SYMBOLS_MAP.USDC.addresses[chainIsProd(hubChainId) ? CHAIN_IDs.SOLANA : CHAIN_IDs.SOLANA_DEVNET]);
    const [[tokenMessenger], [tokenMinter], [localToken], [cctpEventAuthority], [remoteTokenMessenger], [tokenMessengerMinterSenderAuthority], [messageTransmitter],] = await Promise.all([
        getProgramDerivedAddress({
            programAddress: tokenMessengerMinterAddress,
            seeds: ["token_messenger"],
        }),
        getProgramDerivedAddress({
            programAddress: tokenMessengerMinterAddress,
            seeds: ["token_minter"],
        }),
        getProgramDerivedAddress({
            programAddress: tokenMessengerMinterAddress,
            seeds: ["local_token", bs58.decode(l2Usdc.toBase58())],
        }),
        getProgramDerivedAddress({
            programAddress: tokenMessengerMinterAddress,
            seeds: ["__event_authority"],
        }),
        getProgramDerivedAddress({
            programAddress: tokenMessengerMinterAddress,
            seeds: ["remote_token_messenger", String(cctpDestinationDomainId)],
        }),
        getProgramDerivedAddress({
            programAddress: tokenMessengerMinterAddress,
            seeds: ["sender_authority"],
        }),
        getProgramDerivedAddress({
            programAddress: messageTransmitterAddress,
            seeds: ["message_transmitter"],
        }),
    ]);
    return {
        tokenMessenger,
        tokenMinter,
        localToken,
        cctpEventAuthority,
        remoteTokenMessenger,
        tokenMessengerMinterSenderAuthority,
        messageTransmitter,
    };
}
/**
 * Returns true if the input deposit's corresponding relay data would result in a transaction size
 * that is larger than the Solana transaction size limit.
 * @param fillRelayTx The compilable fill relay transaction to check.
 * @returns Object containing a boolean if the input deposit requires a multipart fill, false otherwise and
 * the number of bytes in the serialized transaction.
 */
export function isSVMFillTooLarge(fillRelayTx) {
    const sizeBytes = calculateFillSizeBytes(fillRelayTx);
    return {
        tooLarge: sizeBytes > SOLANA_TX_SIZE_LIMIT,
        sizeBytes,
    };
}
/**
 * Returns the byte size of a base64 transaction.
 * @param base64TxString base64 serialized Solana transaction.
 * @returns The number of bytes in the transaction.
 */
export function base64StrToByteSize(base64TxString) {
    // base64 string has 6 bits per character, so every 4 symbols represent 3 bytes
    // However, we also need to account for padding: https://en.wikipedia.org/wiki/Base64#Padding
    const paddingLen = base64TxString.endsWith("==") ? 2 : base64TxString.endsWith("=") ? 1 : 0;
    return (base64TxString.length * 3) / 4 - paddingLen;
}
/**
 * Returns the size of the fill relay transaction using the input relayData.
 * @param fillTx The compilable fill relay transaction.
 * @returns The number of bytes in the serialized fillRelay transaction.
 */
export function calculateFillSizeBytes(fillTx) {
    const signedTransaction = compileTransaction(fillTx);
    const serializedTx = getBase64EncodedWireTransaction(signedTransaction);
    return base64StrToByteSize(serializedTx);
}
/**
 * Returns the account metas for a deposit message.
 * @param message The CCTP message.
 * @param hubChainId The chain ID of the hub.
 * @param tokenMessengerMinter The token messenger minter address.
 * @param recipientAta The ATA of the recipient address.
 * @returns The account metas for a deposit message.
 */
async function getAccountMetasForDepositMessage(message, hubChainId, tokenMessengerMinter, recipientAta) {
    const l1Usdc = EvmAddress.from(TOKEN_SYMBOLS_MAP.USDC.addresses[hubChainId]);
    const l2Usdc = SvmAddress.from(TOKEN_SYMBOLS_MAP.USDC.addresses[chainIsProd(hubChainId) ? CHAIN_IDs.SOLANA : CHAIN_IDs.SOLANA_DEVNET]);
    const [tokenMessengerPda] = await getProgramDerivedAddress({
        programAddress: tokenMessengerMinter,
        seeds: ["token_messenger"],
    });
    const [tokenMinterPda] = await getProgramDerivedAddress({
        programAddress: tokenMessengerMinter,
        seeds: ["token_minter"],
    });
    const [localTokenPda] = await getProgramDerivedAddress({
        programAddress: tokenMessengerMinter,
        seeds: ["local_token", bs58.decode(l2Usdc.toBase58())],
    });
    const [tokenMessengerEventAuthorityPda] = await getProgramDerivedAddress({
        programAddress: tokenMessengerMinter,
        seeds: ["__event_authority"],
    });
    const [custodyTokenAccountPda] = await getProgramDerivedAddress({
        programAddress: tokenMessengerMinter,
        seeds: ["custody", bs58.decode(l2Usdc.toBase58())],
    });
    // Define accounts dependent on deposit information.
    const [tokenPairPda] = await getProgramDerivedAddress({
        programAddress: tokenMessengerMinter,
        seeds: [
            new Uint8Array(Buffer.from("token_pair")),
            new Uint8Array(Buffer.from(String(message.sourceDomain))),
            new Uint8Array(Buffer.from(l1Usdc.toBytes32().slice(2), "hex")),
        ],
    });
    const [remoteTokenMessengerPda] = await getProgramDerivedAddress({
        programAddress: tokenMessengerMinter,
        seeds: ["remote_token_messenger", String(message.sourceDomain)],
    });
    return [
        { address: tokenMessengerPda, role: AccountRole.READONLY },
        { address: remoteTokenMessengerPda, role: AccountRole.READONLY },
        { address: tokenMinterPda, role: AccountRole.WRITABLE },
        { address: localTokenPda, role: AccountRole.WRITABLE },
        { address: tokenPairPda, role: AccountRole.READONLY },
        { address: toAddress(recipientAta), role: AccountRole.WRITABLE },
        { address: custodyTokenAccountPda, role: AccountRole.WRITABLE },
        { address: TOKEN_PROGRAM_ADDRESS, role: AccountRole.READONLY },
        { address: tokenMessengerEventAuthorityPda, role: AccountRole.READONLY },
        { address: tokenMessengerMinter, role: AccountRole.READONLY },
    ];
}
/**
 * Returns the CCTP v1 receive message transaction.
 * @param solanaClient The Solana client.
 * @param signer The signer of the transaction.
 * @param message The CCTP message.
 * @param hubChainId The chain ID of the hub.
 * @param recipientAta The ATA of the recipient address (used for token finalizations only).
 * @returns The CCTP v1 receive message transaction.
 */
export async function getCCTPV1ReceiveMessageTx(solanaClient, signer, message, hubChainId, recipientAta) {
    const [messageTransmitterPda] = await getProgramDerivedAddress({
        programAddress: MessageTransmitterClient.MESSAGE_TRANSMITTER_PROGRAM_ADDRESS,
        seeds: ["message_transmitter"],
    });
    const [eventAuthorityPda] = await getProgramDerivedAddress({
        programAddress: MessageTransmitterClient.MESSAGE_TRANSMITTER_PROGRAM_ADDRESS,
        seeds: ["__event_authority"],
    });
    const cctpMessageReceiver = isDepositForBurnEvent(message)
        ? TokenMessengerMinterClient.TOKEN_MESSENGER_MINTER_PROGRAM_ADDRESS
        : SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS;
    const [authorityPda] = await getProgramDerivedAddress({
        programAddress: MessageTransmitterClient.MESSAGE_TRANSMITTER_PROGRAM_ADDRESS,
        seeds: ["message_transmitter_authority", bs58.decode(cctpMessageReceiver)],
    });
    // Notice: message.nonce is only valid for v1 messages
    const usedNonces = await getCCTPNoncePda(solanaClient, signer, message.nonce, message.sourceDomain);
    // Notice: for Svm tokenless messages, we currently only support very specific finalizations: Hub -> Spoke relayRootBundle calls
    const accountMetas = isDepositForBurnEvent(message)
        ? await getAccountMetasForDepositMessage(message, hubChainId, TokenMessengerMinterClient.TOKEN_MESSENGER_MINTER_PROGRAM_ADDRESS, recipientAta)
        : await getAccountMetasForTokenlessMessage(solanaClient, signer, message.messageBytes);
    const messageBytes = message.messageBytes.startsWith("0x")
        ? Buffer.from(message.messageBytes.slice(2), "hex")
        : Buffer.from(message.messageBytes, "hex");
    const input = {
        program: MessageTransmitterClient.MESSAGE_TRANSMITTER_PROGRAM_ADDRESS,
        payer: signer,
        caller: signer,
        authorityPda,
        messageTransmitter: messageTransmitterPda,
        eventAuthority: eventAuthorityPda,
        usedNonces,
        receiver: cctpMessageReceiver,
        systemProgram: SYSTEM_PROGRAM_ADDRESS,
        message: messageBytes,
        attestation: Buffer.from(message.attestation.slice(2), "hex"),
    };
    return createReceiveMessageInstruction(signer, solanaClient, input, accountMetas);
}
/**
 * Finalizes CCTP deposits and messages on Solana.
 *
 * @param solanaClient The Solana client.
 * @param attestedMessages The CCTP messages to Solana.
 * @param signer A base signer to be converted into a Solana signer.
 * @param recipientAta The ATA of the recipient address (used for token finalizations only).
 * @param simulate Whether to simulate the transaction.
 * @param hubChainId The chain ID of the hub.
 * @returns A list of executed transaction signatures.
 */
export function finalizeCCTPV1Messages(solanaClient, attestedMessages, signer, recipientAta, simulate = false, hubChainId = 1) {
    return mapAsync(attestedMessages, async (message) => {
        const receiveMessageIx = await getCCTPV1ReceiveMessageTx(solanaClient, signer, message, hubChainId, recipientAta);
        if (simulate) {
            const result = await solanaClient
                .simulateTransaction(getBase64EncodedWireTransaction(await signTransactionMessageWithSigners(receiveMessageIx)), {
                encoding: "base64",
            })
                .send();
            if (result.value.err) {
                throw new Error(result.value.err.toString());
            }
            return "";
        }
        const signedTransaction = await signTransactionMessageWithSigners(receiveMessageIx);
        const signature = getSignatureFromTransaction(signedTransaction);
        const encodedTransaction = getBase64EncodedWireTransaction(signedTransaction);
        await solanaClient
            .sendTransaction(encodedTransaction, { preflightCommitment: "confirmed", encoding: "base64" })
            .send();
        return signature;
    });
}
export async function getMintInfo(solanaClient, mint, config) {
    return await fetchMint(solanaClient, mint, config);
}
// Extracts value_amount from the AcrossPlusMessage bytes. This serves as a 2nd method of deserializing the value, as
// a way to protect us against potential bugs in the deserialization logic.
function extractValueAmount(acrossPlusMessageBytes) {
    // Layout of the AcrossPlusMessage struct
    // #[derive(AnchorDeserialize)]
    // pub struct AcrossPlusMessage {
    //   pub handler: Pubkey,
    //   pub read_only_len: u8,
    //   pub value_amount: u64,
    //   pub accounts: Vec<Pubkey>,
    //   pub handler_message: Vec<u8>,
    // }
    const VALUE_OFFSET = 32 + 1; // 33
    const VALUE_END = VALUE_OFFSET + 8; // 41
    if (acrossPlusMessageBytes.length < VALUE_END) {
        throw new Error(`svm | extractValueAmount: Message too short, need at least ${VALUE_END} bytes, got ${acrossPlusMessageBytes.length}`);
    }
    return readU64LEExact(acrossPlusMessageBytes.subarray(VALUE_OFFSET, VALUE_END));
}
// Reads exactly 8 bytes as a little-endian u64 and returns bigint
function readU64LEExact(bytes) {
    if (bytes.length !== 8) {
        throw new Error(`readU64LEExact expected 8 bytes, received ${bytes.length}`);
    }
    const view = new DataView(bytes.buffer, bytes.byteOffset, 8);
    return view.getBigUint64(0, true); // little-endian
}
//# sourceMappingURL=SpokeUtils.js.map