"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 crypto = __importStar(require("crypto"));
const anchor_1 = require("@coral-xyz/anchor");
const spl_token_1 = require("@solana/spl-token");
const web3_js_1 = require("@solana/web3.js");
const MerkleTree_1 = require("@uma/common/dist/MerkleTree");
const web3_v1_1 = require("../../src/svm/web3-v1");
const SvmSpoke_common_1 = require("./SvmSpoke.common");
const { provider, connection, program, owner, chainId, setCurrentTime } = SvmSpoke_common_1.common;
const { initializeState, assertSE, assert } = SvmSpoke_common_1.common;
describe("svm_spoke.slow_fill.across_plus", () => {
    anchor.setProvider(provider);
    const payer = anchor.AnchorProvider.env().wallet.payer;
    const relayer = web3_js_1.Keypair.generate();
    const tokenDecimals = 6;
    const handlerProgram = anchor.workspace.MulticallHandler;
    let handlerSigner, handlerATA, finalRecipient, finalRecipientATA, state, vault, mint;
    const relayAmount = 500_000;
    let relayData; // reused relay data for all tests.
    let requestAccounts; // Store accounts to simplify program interactions.
    let seed;
    const seedBalance = 10_000_000_000;
    async 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 for requestingSlowFill.
        requestAccounts = {
            signer: relayer.publicKey,
            instructionParams: program.programId,
            state,
            fillStatusPDA,
            systemProgram: anchor.web3.SystemProgram.programId,
        };
    }
    const relaySlowFillRootBundle = async () => {
        const slowRelayLeafs = [];
        const slowRelayLeaf = {
            relayData,
            chainId,
            updatedOutputAmount: relayData.outputAmount,
        };
        slowRelayLeafs.push(slowRelayLeaf);
        // Generate bunch of other leaves, so that we have large proofs array to test (this gets 4 proofs elements).
        for (let i = 0; i < 15; i++) {
            slowRelayLeafs.push({
                relayData,
                chainId: new anchor_1.BN(Math.random() * 100000),
                updatedOutputAmount: relayData.outputAmount,
            });
        }
        const merkleTree = new MerkleTree_1.MerkleTree(slowRelayLeafs, web3_v1_1.slowFillHashFn);
        const slowRelayRoot = merkleTree.getRoot();
        const proof = merkleTree.getProof(slowRelayLeafs[0]);
        const leaf = slowRelayLeafs[0];
        const stateAccountData = await program.account.state.fetch(state);
        const rootBundleId = stateAccountData.rootBundleId;
        const rootBundleIdBuffer = Buffer.alloc(4);
        rootBundleIdBuffer.writeUInt32LE(rootBundleId);
        const seeds = [Buffer.from("root_bundle"), seed.toArrayLike(Buffer, "le", 8), rootBundleIdBuffer];
        const [rootBundle] = web3_js_1.PublicKey.findProgramAddressSync(seeds, program.programId);
        const relayerRefundRoot = crypto.randomBytes(32);
        // Relay root bundle
        const relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId };
        await program.methods
            .relayRootBundle(Array.from(relayerRefundRoot), Array.from(slowRelayRoot))
            .accounts(relayRootBundleAccounts)
            .rpc();
        const proofAsNumbers = proof.map((p) => Array.from(p));
        const relayHash = (0, web3_v1_1.calculateRelayHashUint8Array)(slowRelayLeaf.relayData, chainId);
        return { relayHash, leaf, rootBundleId, proofAsNumbers, rootBundle };
    };
    const createSlowFillIx = async (multicallHandlerCoder, bufferParams = false) => {
        // Relay root bundle with slow fill leaf.
        const { relayHash, leaf, rootBundleId, proofAsNumbers, rootBundle } = await relaySlowFillRootBundle();
        const requestSlowFillValues = [Array.from(relayHash), leaf.relayData];
        let loadRequestParamsInstructions = [];
        if (bufferParams) {
            loadRequestParamsInstructions = await (0, web3_v1_1.loadRequestSlowFillParams)(program, relayer, requestSlowFillValues[1]);
            [requestAccounts.instructionParams] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("instruction_params"), relayer.publicKey.toBuffer()], program.programId);
        }
        const requestV3SlowFillParams = bufferParams
            ? [requestSlowFillValues[0], null]
            : requestSlowFillValues;
        const requestIx = await program.methods
            .requestSlowFill(...requestV3SlowFillParams)
            .accounts(requestAccounts)
            .instruction();
        const executeAccounts = {
            state,
            rootBundle,
            signer: relayer.publicKey,
            instructionParams: requestAccounts.instructionParams,
            fillStatus: requestAccounts.fillStatus,
            vault: vault,
            tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
            mint: mint,
            recipientTokenAccount: handlerATA,
            program: program.programId,
        };
        const executeRemainingAccounts = [
            { pubkey: handlerProgram.programId, isSigner: false, isWritable: false },
            ...multicallHandlerCoder.compiledKeyMetas,
        ];
        const executeSlowRelayLeafValues = [
            Array.from(relayHash),
            leaf,
            rootBundleId,
            proofAsNumbers,
        ];
        let loadExecuteParamsInstructions = [];
        if (bufferParams) {
            loadExecuteParamsInstructions = await (0, web3_v1_1.loadExecuteSlowRelayLeafParams)(program, relayer, executeSlowRelayLeafValues[1], executeSlowRelayLeafValues[2], executeSlowRelayLeafValues[3]);
            [requestAccounts.instructionParams] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("instruction_params"), relayer.publicKey.toBuffer()], program.programId);
        }
        const executeSlowRelayLeafParams = bufferParams
            ? [executeSlowRelayLeafValues[0], null, null, null]
            : executeSlowRelayLeafValues;
        const executeIx = await program.methods
            .executeSlowRelayLeaf(...executeSlowRelayLeafParams)
            .accounts(executeAccounts)
            .remainingAccounts(executeRemainingAccounts)
            .instruction();
        return { loadRequestParamsInstructions, requestIx, loadExecuteParamsInstructions, executeIx };
    };
    before("Creates token mint and associated token accounts", async () => {
        mint = await (0, spl_token_1.createMint)(connection, payer, owner, owner, tokenDecimals);
        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, seed } = await initializeState());
        vault = (await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(connection, payer, mint, state, true)).address; // Initialize vault
        // mint mint to vault
        await (0, spl_token_1.mintTo)(connection, payer, mint, vault, owner, seedBalance);
        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)), // Unique ID for each test.
            fillDeadline: Math.floor(Date.now() / 1000) + 60, // 1 minute from now
            exclusivityDeadline: Math.floor(Date.now() / 1000) - 30, // Note we set time in past to avoid exclusivity deadline
            message: Buffer.from(""), // Will be populated in the tests below.
        };
        await updateRelayData(initialRelayData);
    });
    it("Forwards tokens to the final recipient within invoked message call", async () => {
        const iVaultBal = (await (0, spl_token_1.getAccount)(connection, vault)).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.toNumber(), tokenDecimals);
        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);
        // Request and execute slow fill.
        const { requestIx, executeIx } = await createSlowFillIx(multicallHandlerCoder);
        await (0, web3_js_1.sendAndConfirmTransaction)(connection, new web3_js_1.Transaction().add(requestIx), [relayer]);
        await (0, web3_js_1.sendAndConfirmTransaction)(connection, new web3_js_1.Transaction().add(executeIx), [relayer]);
        // Verify vault's balance after the fill
        const fVaultBal = (await (0, spl_token_1.getAccount)(connection, vault)).amount;
        assertSE(fVaultBal, iVaultBal - BigInt(relayAmount), "Vault'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 iVaultBal = (await (0, spl_token_1.getAccount)(connection, vault)).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, tokenDecimals);
                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 request and execute slow fill instructions as we will need to use Address Lookup Table (ALT).
            // Request and execute slow fill.
            const { loadRequestParamsInstructions, requestIx, loadExecuteParamsInstructions, executeIx } = await createSlowFillIx(multicallHandlerCoder, bufferParams);
            // Fill using the ALT and submit load params transactions if needed.
            const computeBudgetIx = web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 });
            if (bufferParams) {
                for (let i = 0; i < loadRequestParamsInstructions.length; i += 1) {
                    await (0, web3_js_1.sendAndConfirmTransaction)(program.provider.connection, new web3_js_1.Transaction().add(loadRequestParamsInstructions[i]), [relayer]);
                }
            }
            await (0, web3_v1_1.sendTransactionWithLookupTable)(connection, [computeBudgetIx, requestIx], relayer);
            await new Promise((resolve) => setTimeout(resolve, 1000)); // Make sure request tx gets processed.
            if (bufferParams) {
                for (let i = 0; i < loadExecuteParamsInstructions.length; i += 1) {
                    await (0, web3_js_1.sendAndConfirmTransaction)(program.provider.connection, new web3_js_1.Transaction().add(loadExecuteParamsInstructions[i]), [relayer]);
                }
            }
            await (0, web3_v1_1.sendTransactionWithLookupTable)(connection, [computeBudgetIx, executeIx], relayer);
            // Verify vault's balance after the fill
            await new Promise((resolve) => setTimeout(resolve, 1000)); // Make sure token transfers get processed.
            const fVaultBal = (await (0, spl_token_1.getAccount)(connection, vault)).amount;
            assertSE(fVaultBal, iVaultBal - BigInt(distributionAmount * numberOfDistributions), "Vault'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 = 6;
            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("Can recover and close fill status PDA from event data", async () => {
        // 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.toNumber(), tokenDecimals);
        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);
        // Prepare request and execute slow fill instructions as we will need to use Address Lookup Table (ALT).
        // Request and execute slow fill.
        const { loadRequestParamsInstructions, requestIx, loadExecuteParamsInstructions, executeIx } = await createSlowFillIx(multicallHandlerCoder, true);
        // Fill using the ALT and submit load params transactions.
        for (let i = 0; i < loadRequestParamsInstructions.length; i += 1) {
            await (0, web3_js_1.sendAndConfirmTransaction)(program.provider.connection, new web3_js_1.Transaction().add(loadRequestParamsInstructions[i]), [relayer]);
        }
        await (0, web3_v1_1.sendTransactionWithLookupTable)(connection, [requestIx], relayer);
        await new Promise((resolve) => setTimeout(resolve, 1000)); // Make sure request tx gets processed.
        for (let i = 0; i < loadExecuteParamsInstructions.length; i += 1) {
            await (0, web3_js_1.sendAndConfirmTransaction)(program.provider.connection, new web3_js_1.Transaction().add(loadExecuteParamsInstructions[i]), [relayer]);
        }
        const { txSignature } = await (0, web3_v1_1.sendTransactionWithLookupTable)(connection, [executeIx], relayer);
        await connection.confirmTransaction(txSignature, "confirmed");
        await connection.getTransaction(txSignature, {
            commitment: "confirmed",
            maxSupportedTransactionVersion: 0,
        });
        // We don't close ALT here as that would require ~4 minutes between deactivation and closing, but we demonstrate
        // being able to close the fill status PDA using only event data.
        const events = await (0, web3_v1_1.readEventsUntilFound)(connection, txSignature, [program]);
        const eventData = events.find((event) => event.name === "filledRelay")?.data;
        assert.isNotNull(eventData, "FilledRelay event should be emitted");
        // Recover relay hash and derived fill status from event data.
        const relayHashUint8Array = (0, web3_v1_1.calculateRelayEventHashUint8Array)(eventData, chainId);
        const [fillStatusPDA] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("fills"), relayHashUint8Array], program.programId);
        const fillStatusAccount = await program.account.fillStatusAccount.fetch(fillStatusPDA);
        assert.isTrue("filled" in fillStatusAccount.status, "Fill status account should be marked as filled");
        assertSE(fillStatusAccount.relayer, relayer.publicKey, "Relayer should match in the fill status");
        // Set the current time to past the fill deadline
        await setCurrentTime(program, state, relayer, new anchor_1.BN(fillStatusAccount.fillDeadline + 1));
        const closeFillPdaAccounts = {
            signer: relayer.publicKey,
            state,
            fillStatus: fillStatusPDA,
        };
        await program.methods.closeFillPda().accounts(closeFillPdaAccounts).signers([relayer]).rpc();
        // Verify the fill PDA is closed
        const fillStatusAccountAfter = await connection.getAccountInfo(fillStatusPDA);
        assert.isNull(fillStatusAccountAfter, "Fill PDA should be closed after closing");
    });
});
