"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const anchor = __importStar(require("@coral-xyz/anchor"));
const anchor_1 = require("@coral-xyz/anchor");
const token_1 = require("@solana-program/token");
const kit_1 = require("@solana/kit");
const spl_token_1 = require("@solana/spl-token");
const web3_js_1 = require("@solana/web3.js");
const svm_1 = require("../../src/svm");
const web3_v1_1 = require("../../src/svm/web3-v1");
const SvmSpoke_common_1 = require("./SvmSpoke.common");
const utils_1 = require("./utils");
const { provider, connection, program, owner, chainId, seedBalance, initializeState, assertSE } = SvmSpoke_common_1.common;
describe("svm_spoke.fill.across_plus", () => {
    anchor.setProvider(provider);
    const payer = anchor.AnchorProvider.env().wallet.payer;
    const relayer = web3_js_1.Keypair.generate();
    const handlerProgram = anchor.workspace.MulticallHandler;
    let handlerSigner, handlerATA, finalRecipient, finalRecipientATA, state, mint, relayerATA;
    const relayAmount = 500000;
    const mintDecimals = 6;
    let relayData; // reused relay data for all tests.
    let accounts; // Store accounts to simplify contract interactions.
    function updateRelayData(newRelayData) {
        relayData = newRelayData;
        const relayHashUint8Array = (0, web3_v1_1.calculateRelayHashUint8Array)(relayData, chainId);
        const [fillStatusPDA] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("fills"), relayHashUint8Array], program.programId);
        accounts = {
            state,
            signer: relayer.publicKey,
            instructionParams: program.programId,
            mint: mint,
            relayerTokenAccount: relayerATA,
            recipientTokenAccount: handlerATA,
            fillStatus: fillStatusPDA,
            tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
            associatedTokenProgram: spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID,
            systemProgram: anchor.web3.SystemProgram.programId,
        };
    }
    async function createApproveAndFillIx(multicallHandlerCoder, bufferParams = false) {
        // Delegate state PDA to pull relayer tokens.
        const approveIx = await (0, spl_token_1.createApproveCheckedInstruction)(accounts.relayerTokenAccount, accounts.mint, accounts.state, accounts.signer, BigInt(relayAmount), mintDecimals);
        const remainingAccounts = [
            { pubkey: handlerProgram.programId, isSigner: false, isWritable: false },
            ...multicallHandlerCoder.compiledKeyMetas,
        ];
        const relayHash = Array.from((0, web3_v1_1.calculateRelayHashUint8Array)(relayData, chainId));
        // Prepare fill instruction.
        const fillV3RelayValues = [relayHash, relayData, new anchor_1.BN(1), relayer.publicKey];
        if (bufferParams) {
            await (0, web3_v1_1.loadFillRelayParams)(program, relayer, fillV3RelayValues[1], fillV3RelayValues[2], fillV3RelayValues[3]);
            [accounts.instructionParams] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("instruction_params"), relayer.publicKey.toBuffer()], program.programId);
        }
        const fillV3RelayParams = bufferParams
            ? [fillV3RelayValues[0], null, null, null]
            : fillV3RelayValues;
        const fillIx = await program.methods
            .fillRelay(...fillV3RelayParams)
            .accounts(accounts)
            .remainingAccounts(remainingAccounts)
            .instruction();
        return { approveIx, fillIx };
    }
    before("Creates token mint and associated token accounts", async () => {
        mint = await (0, spl_token_1.createMint)(connection, payer, owner, owner, mintDecimals);
        relayerATA = (await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(connection, payer, mint, relayer.publicKey)).address;
        await (0, spl_token_1.mintTo)(connection, payer, mint, relayerATA, owner, seedBalance);
        await connection.requestAirdrop(relayer.publicKey, 10_000_000_000); // 10 SOL
        [handlerSigner] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("handler_signer")], handlerProgram.programId);
        handlerATA = (await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(connection, payer, mint, handlerSigner, true)).address;
    });
    beforeEach(async () => {
        finalRecipient = web3_js_1.Keypair.generate().publicKey;
        finalRecipientATA = (await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(connection, payer, mint, finalRecipient)).address;
        ({ state } = await initializeState());
        const initialRelayData = {
            depositor: finalRecipient,
            recipient: handlerSigner, // Handler PDA that can forward tokens as needed within the message call.
            exclusiveRelayer: relayer.publicKey,
            inputToken: mint, // This is lazy. it should be an encoded token from a separate domain most likely.
            outputToken: mint,
            inputAmount: new anchor_1.BN(relayAmount),
            outputAmount: new anchor_1.BN(relayAmount),
            originChainId: new anchor_1.BN(1),
            depositId: (0, web3_v1_1.intToU8Array32)(Math.floor(Math.random() * 1000000)), // force that we always have a new deposit id.
            fillDeadline: new anchor_1.BN(Math.floor(Date.now() / 1000) + 60), // 1 minute from now
            exclusivityDeadline: new anchor_1.BN(Math.floor(Date.now() / 1000) + 30), // 30 seconds from now
            message: Buffer.from(""), // Will be populated in the tests below.
        };
        updateRelayData(initialRelayData);
    });
    it("Forwards tokens to the final recipient within invoked message call", async () => {
        const iRelayerBal = (await (0, spl_token_1.getAccount)(connection, relayerATA)).amount;
        // Construct ix to transfer all tokens from handler to the final recipient.
        const transferIx = (0, spl_token_1.createTransferCheckedInstruction)(handlerATA, mint, finalRecipientATA, handlerSigner, relayData.outputAmount, mintDecimals);
        const multicallHandlerCoder = new web3_v1_1.MulticallHandlerCoder([transferIx]);
        const handlerMessage = multicallHandlerCoder.encode();
        const message = new web3_v1_1.AcrossPlusMessageCoder({
            handler: handlerProgram.programId,
            readOnlyLen: multicallHandlerCoder.readOnlyLen,
            valueAmount: new anchor_1.BN(0),
            accounts: multicallHandlerCoder.compiledMessage.accountKeys,
            handlerMessage,
        });
        const encodedMessage = message.encode();
        // Update relay data with the encoded message.
        const newRelayData = { ...relayData, message: encodedMessage };
        updateRelayData(newRelayData);
        // Send approval and fill in one transaction.
        const { approveIx, fillIx } = await createApproveAndFillIx(multicallHandlerCoder);
        await (0, web3_js_1.sendAndConfirmTransaction)(connection, new web3_js_1.Transaction().add(approveIx, fillIx), [relayer]);
        // Verify relayer's balance after the fill
        const fRelayerBal = (await (0, spl_token_1.getAccount)(connection, relayerATA)).amount;
        assertSE(fRelayerBal, iRelayerBal - BigInt(relayAmount), "Relayer's balance should be reduced by the relay amount");
        // Verify final recipient's balance after the fill
        const finalRecipientAccount = await (0, spl_token_1.getAccount)(connection, finalRecipientATA);
        assertSE(finalRecipientAccount.amount, relayAmount, "Final recipient's balance should be increased by the relay amount");
    });
    describe("Max token distributions within invoked message call", async () => {
        const fillTokenDistributions = async (numberOfDistributions, bufferParams = false) => {
            const iRelayerBal = (await (0, spl_token_1.getAccount)(connection, relayerATA)).amount;
            const distributionAmount = Math.floor(relayAmount / numberOfDistributions);
            const recipientAccounts = [];
            const transferInstructions = [];
            for (let i = 0; i < numberOfDistributions; i++) {
                const recipient = web3_js_1.Keypair.generate().publicKey;
                const recipientATA = (await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(connection, payer, mint, recipient)).address;
                recipientAccounts.push(recipientATA);
                // Construct ix to transfer tokens from handler to the recipient.
                const transferInstruction = (0, spl_token_1.createTransferCheckedInstruction)(handlerATA, mint, recipientATA, handlerSigner, distributionAmount, mintDecimals);
                transferInstructions.push(transferInstruction);
            }
            const multicallHandlerCoder = new web3_v1_1.MulticallHandlerCoder(transferInstructions);
            const handlerMessage = multicallHandlerCoder.encode();
            const message = new web3_v1_1.AcrossPlusMessageCoder({
                handler: handlerProgram.programId,
                readOnlyLen: multicallHandlerCoder.readOnlyLen,
                valueAmount: new anchor_1.BN(0),
                accounts: multicallHandlerCoder.compiledMessage.accountKeys,
                handlerMessage,
            });
            const encodedMessage = message.encode();
            // Update relay data with the encoded message and total relay amount.
            const newRelayData = {
                ...relayData,
                message: encodedMessage,
                outputAmount: new anchor_1.BN(distributionAmount * numberOfDistributions),
            };
            updateRelayData(newRelayData);
            // Prepare approval and fill instructions as we will need to use Address Lookup Table (ALT).
            const { approveIx, fillIx } = await createApproveAndFillIx(multicallHandlerCoder, bufferParams);
            // Fill using the ALT.
            const computeBudgetIx = web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 });
            await (0, web3_v1_1.sendTransactionWithLookupTable)(connection, [computeBudgetIx, approveIx, fillIx], relayer);
            // Verify relayer's balance after the fill
            await new Promise((resolve) => setTimeout(resolve, 500)); // Make sure token transfers get processed.
            const fRelayerBal = (await (0, spl_token_1.getAccount)(connection, relayerATA)).amount;
            assertSE(fRelayerBal, iRelayerBal - BigInt(distributionAmount * numberOfDistributions), "Relayer's balance should be reduced by the relay amount");
            // Verify all recipient account balances after the fill.
            const recipientBalances = await Promise.all(recipientAccounts.map(async (account) => (await connection.getTokenAccountBalance(account)).value.amount));
            recipientBalances.forEach((balance, i) => {
                assertSE(balance, distributionAmount, `Recipient account ${i} balance should match distribution amount`);
            });
        };
        it("Max token distributions within invoked message call, regular params", async () => {
            // Larger distribution would exceed message size limits.
            const numberOfDistributions = 7;
            await fillTokenDistributions(numberOfDistributions);
        });
        it("Max token distributions within invoked message call, buffer account params", async () => {
            // Larger distribution count hits inner instruction size limit when invoking CPI to message handler on public
            // devnet. On localnet this is not an issue, but we hit out of memory panic above 34 distributions.
            const numberOfDistributions = 19;
            await fillTokenDistributions(numberOfDistributions, true);
        });
    });
    it("Sends lamports from the relayer to value recipient", async () => {
        const valueAmount = new anchor_1.BN(1_000_000_000);
        const valueRecipient = web3_js_1.Keypair.generate().publicKey;
        const multicallHandlerCoder = new web3_v1_1.MulticallHandlerCoder([], valueRecipient);
        const handlerMessage = multicallHandlerCoder.encode();
        const message = new web3_v1_1.AcrossPlusMessageCoder({
            handler: handlerProgram.programId,
            readOnlyLen: multicallHandlerCoder.readOnlyLen,
            valueAmount,
            accounts: multicallHandlerCoder.compiledMessage.accountKeys,
            handlerMessage,
        });
        const encodedMessage = message.encode();
        // Update relay data with the encoded message.
        const newRelayData = { ...relayData, message: encodedMessage };
        updateRelayData(newRelayData);
        // Send approval and fill in one transaction.
        const { approveIx, fillIx } = await createApproveAndFillIx(multicallHandlerCoder);
        await (0, web3_js_1.sendAndConfirmTransaction)(connection, new web3_js_1.Transaction().add(approveIx, fillIx), [relayer]);
        // Verify value recipient balance.
        const valueRecipientAccount = await connection.getAccountInfo(valueRecipient);
        if (valueRecipientAccount === null)
            throw new Error("Account not found");
        assertSE(valueRecipientAccount.lamports, valueAmount.toNumber(), "Value recipient's balance should be increased by the value amount");
    });
    it("Creates new ATA when forwarding tokens within invoked message call", async () => {
        // We need precise estimate of required funding for ATA creation.
        const valueAmount = await (0, spl_token_1.getMinimumBalanceForRentExemptAccount)(connection);
        const anotherRecipient = web3_js_1.Keypair.generate().publicKey;
        const anotherRecipientATA = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, anotherRecipient);
        // Construct ix to create recipient ATA funded via handler PDA.
        const createTokenAccountInstruction = (0, spl_token_1.createAssociatedTokenAccountInstruction)(handlerSigner, anotherRecipientATA, anotherRecipient, mint);
        // Construct ix to transfer all tokens from handler to the recipient ATA.
        const transferInstruction = (0, spl_token_1.createTransferCheckedInstruction)(handlerATA, mint, anotherRecipientATA, handlerSigner, relayData.outputAmount, mintDecimals);
        // Encode both instructions with handler PDA as the payer for ATA initialization.
        const multicallHandlerCoder = new web3_v1_1.MulticallHandlerCoder([createTokenAccountInstruction, transferInstruction], handlerSigner);
        const handlerMessage = multicallHandlerCoder.encode();
        const message = new web3_v1_1.AcrossPlusMessageCoder({
            handler: handlerProgram.programId,
            readOnlyLen: multicallHandlerCoder.readOnlyLen,
            valueAmount: new anchor_1.BN(valueAmount), // Must exactly cover ATA creation.
            accounts: multicallHandlerCoder.compiledMessage.accountKeys,
            handlerMessage,
        });
        const encodedMessage = message.encode();
        // Update relay data with the encoded message.
        const newRelayData = { ...relayData, message: encodedMessage };
        updateRelayData(newRelayData);
        // Prepare approval and fill instructions as we will need to use Address Lookup Table (ALT).
        const { approveIx, fillIx } = await createApproveAndFillIx(multicallHandlerCoder);
        // Fill using the ALT.
        await (0, web3_v1_1.sendTransactionWithLookupTable)(connection, [approveIx, fillIx], relayer);
        // Verify recipient's balance after the fill
        await new Promise((resolve) => setTimeout(resolve, 500)); // Make sure token transfer gets processed.
        const anotherRecipientAccount = await (0, spl_token_1.getAccount)(connection, anotherRecipientATA);
        assertSE(anotherRecipientAccount.amount, relayAmount, "Recipient's balance should be increased by the relay amount");
    });
    describe("codama client and solana kit", () => {
        it("Forwards tokens to the final recipient within invoked message call using codama client", async () => {
            const iRelayerBal = (await (0, spl_token_1.getAccount)(connection, relayerATA)).amount;
            // Construct ix to transfer all tokens from handler to the final recipient.
            const transferIx = (0, spl_token_1.createTransferCheckedInstruction)(handlerATA, mint, finalRecipientATA, handlerSigner, relayData.outputAmount, mintDecimals);
            const multicallHandlerCoder = new web3_v1_1.MulticallHandlerCoder([transferIx]);
            const handlerMessage = multicallHandlerCoder.encode();
            const message = new web3_v1_1.AcrossPlusMessageCoder({
                handler: handlerProgram.programId,
                readOnlyLen: multicallHandlerCoder.readOnlyLen,
                valueAmount: new anchor_1.BN(0),
                accounts: multicallHandlerCoder.compiledMessage.accountKeys,
                handlerMessage,
            });
            const encodedMessage = message.encode();
            // Update relay data with the encoded message.
            const newRelayData = { ...relayData, message: encodedMessage };
            updateRelayData(newRelayData);
            const rpcClient = (0, utils_1.createDefaultSolanaClient)();
            const signer = await (0, kit_1.createSignerFromKeyPair)(await (0, kit_1.createKeyPairFromBytes)(relayer.secretKey));
            const [eventAuthority] = await (0, kit_1.getProgramDerivedAddress)({
                programAddress: (0, kit_1.address)(program.programId.toString()),
                seeds: ["__event_authority"],
            });
            const relayHash = Array.from((0, web3_v1_1.calculateRelayHashUint8Array)(relayData, chainId));
            const formattedAccounts = {
                state: (0, kit_1.address)(accounts.state.toString()),
                instructionParams: (0, kit_1.address)(program.programId.toString()),
                mint: (0, kit_1.address)(mint.toString()),
                relayerTokenAccount: (0, kit_1.address)(relayerATA.toString()),
                recipientTokenAccount: (0, kit_1.address)(handlerATA.toString()),
                fillStatus: (0, kit_1.address)(accounts.fillStatus.toString()),
                tokenProgram: (0, kit_1.address)(spl_token_1.TOKEN_PROGRAM_ID.toString()),
                associatedTokenProgram: (0, kit_1.address)(spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID.toString()),
                systemProgram: (0, kit_1.address)(anchor.web3.SystemProgram.programId.toString()),
                program: (0, kit_1.address)(program.programId.toString()),
                eventAuthority,
                signer,
            };
            const formattedRelayData = {
                relayHash: new Uint8Array(relayHash),
                relayData: {
                    depositor: (0, kit_1.address)(relayData.depositor.toString()),
                    recipient: (0, kit_1.address)(relayData.recipient.toString()),
                    exclusiveRelayer: (0, kit_1.address)(relayData.exclusiveRelayer.toString()),
                    inputToken: (0, kit_1.address)(relayData.inputToken.toString()),
                    outputToken: (0, kit_1.address)(relayData.outputToken.toString()),
                    inputAmount: relayData.inputAmount.toNumber(),
                    outputAmount: relayData.outputAmount.toNumber(),
                    originChainId: relayData.originChainId.toNumber(),
                    depositId: new Uint8Array(relayData.depositId),
                    fillDeadline: relayData.fillDeadline,
                    exclusivityDeadline: relayData.exclusivityDeadline,
                    message: encodedMessage,
                },
                repaymentChainId: 1,
                repaymentAddress: (0, kit_1.address)(relayer.publicKey.toString()),
            };
            const approveIx = (0, token_1.getApproveCheckedInstruction)({
                source: (0, kit_1.address)(accounts.relayerTokenAccount.toString()),
                mint: (0, kit_1.address)(accounts.mint.toString()),
                delegate: (0, kit_1.address)(accounts.state.toString()),
                owner: (0, kit_1.address)(accounts.signer.toString()),
                amount: BigInt(relayData.outputAmount.toString()),
                decimals: mintDecimals,
            });
            const fillRelayInput = {
                ...formattedRelayData,
                ...formattedAccounts,
            };
            const fillRelayIxData = await svm_1.SvmSpokeClient.getFillRelayInstructionAsync(fillRelayInput);
            const fillRelayIx = {
                ...fillRelayIxData,
                accounts: fillRelayIxData.accounts.map((account) => account.address === program.programId.toString() ||
                    account.address === spl_token_1.TOKEN_PROGRAM_ID.toString() ||
                    account.address === spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID.toString()
                    ? { ...account, role: kit_1.AccountRole.READONLY }
                    : account),
            };
            const _remainingAccounts = [
                { pubkey: handlerProgram.programId, isSigner: false, isWritable: false },
                ...multicallHandlerCoder.compiledKeyMetas,
            ];
            const remainingAccounts = _remainingAccounts.map((account) => ({
                address: (0, kit_1.address)(account.pubkey.toString()),
                role: account.isWritable ? kit_1.AccountRole.WRITABLE : kit_1.AccountRole.READONLY,
            }));
            fillRelayIx.accounts.push(...remainingAccounts);
            await (0, kit_1.pipe)(await (0, svm_1.createDefaultTransaction)(rpcClient, signer), (tx) => (0, kit_1.appendTransactionMessageInstruction)(approveIx, tx), (tx) => (0, kit_1.appendTransactionMessageInstruction)(fillRelayIx, tx), (tx) => (0, svm_1.signAndSendTransaction)(rpcClient, tx));
            // Verify relayer's balance after the fill
            const fRelayerBal = (await (0, spl_token_1.getAccount)(connection, relayerATA)).amount;
            assertSE(fRelayerBal, iRelayerBal - BigInt(relayAmount), "Relayer's balance should be reduced by the relay amount");
            // Verify final recipient's balance after the fill
            const finalRecipientAccount = await (0, spl_token_1.getAccount)(connection, finalRecipientATA);
            assertSE(finalRecipientAccount.amount, relayAmount, "Final recipient's balance should be increased by the relay amount");
        });
    });
});
