"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 spl_token_1 = require("@solana/spl-token");
const web3_js_1 = require("@solana/web3.js");
const SvmSpoke_common_1 = require("./SvmSpoke.common");
const utils_1 = require("./utils");
const { provider, connection, program, owner, seedBalance, initializeState, depositData } = SvmSpoke_common_1.common;
const { createRoutePda, getVaultAta, assertSE, assert, getCurrentTime, depositQuoteTimeBuffer, fillDeadlineBuffer } = SvmSpoke_common_1.common;
describe("svm_spoke.deposit", () => {
    anchor.setProvider(provider);
    const depositor = web3_js_1.Keypair.generate();
    const payer = anchor.AnchorProvider.env().wallet.payer;
    let state, inputToken, depositorTA, vault;
    let depositAccounts; // Re-used between tests to simplify props.
    let setEnableRouteAccounts; // Common variable for setEnableRoute accounts
    const setupInputToken = async () => {
        inputToken = await (0, spl_token_1.createMint)(connection, payer, owner, owner, 6);
        depositorTA = (await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(connection, payer, inputToken, depositor.publicKey)).address;
        await (0, spl_token_1.mintTo)(connection, payer, inputToken, depositorTA, owner, seedBalance);
    };
    const enableRoute = async () => {
        const routeChainId = new anchor_1.BN(1);
        const route = createRoutePda(inputToken, state, routeChainId);
        vault = getVaultAta(inputToken, state);
        setEnableRouteAccounts = {
            signer: owner,
            payer: owner,
            state,
            route,
            vault,
            originTokenMint: inputToken, // Note the Sol expects this to be named originTokenMint.
            tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
            associatedTokenProgram: spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID,
            systemProgram: anchor.web3.SystemProgram.programId,
        };
        await program.methods.setEnableRoute(inputToken, routeChainId, true).accounts(setEnableRouteAccounts).rpc();
        // Set known fields in the depositData.
        depositData.depositor = depositor.publicKey;
        depositData.inputToken = inputToken;
        depositAccounts = {
            state,
            route,
            signer: depositor.publicKey,
            depositorTokenAccount: depositorTA,
            vault,
            mint: inputToken,
            tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
        };
    };
    before("Creates token mint and associated token accounts", async () => {
        await setupInputToken();
    });
    beforeEach(async () => {
        state = await initializeState();
        await enableRoute();
    });
    it("Deposits tokens via deposit_v3 function and checks balances", async () => {
        // Verify vault balance is zero before the deposit
        let vaultAccount = await (0, spl_token_1.getAccount)(connection, vault);
        assertSE(vaultAccount.amount, "0", "Vault balance should be zero before the deposit");
        // Execute the deposit_v3 call
        let depositDataValues = Object.values(depositData);
        await program.methods
            .depositV3(...depositDataValues)
            .accounts(depositAccounts)
            .signers([depositor])
            .rpc();
        // Verify tokens leave the depositor's account
        let depositorAccount = await (0, spl_token_1.getAccount)(connection, depositorTA);
        assertSE(depositorAccount.amount, seedBalance - depositData.inputAmount.toNumber(), "Depositor's balance should be reduced by the deposited amount");
        // Verify tokens are credited into the vault
        vaultAccount = await (0, spl_token_1.getAccount)(connection, vault);
        assertSE(vaultAccount.amount, depositData.inputAmount, "Vault balance should be increased by the deposited amount");
        // Modify depositData for the second deposit
        const secondInputAmount = new anchor_1.BN(300000);
        // Execute the second deposit_v3 call
        depositDataValues = Object.values({ ...depositData, inputAmount: secondInputAmount });
        await program.methods
            .depositV3(...depositDataValues)
            .accounts(depositAccounts)
            .signers([depositor])
            .rpc();
        // Verify tokens leave the depositor's account again
        depositorAccount = await (0, spl_token_1.getAccount)(connection, depositorTA);
        assertSE(depositorAccount.amount, seedBalance - depositData.inputAmount.toNumber() - secondInputAmount.toNumber(), "Depositor's balance should be reduced by the total deposited amount");
        // Verify tokens are credited into the vault again
        vaultAccount = await (0, spl_token_1.getAccount)(connection, vault);
        assertSE(vaultAccount.amount, depositData.inputAmount.add(secondInputAmount), "Vault balance should be increased by the total deposited amount");
    });
    it("Verifies V3FundsDeposited after deposits", async () => {
        depositData.inputAmount = depositData.inputAmount.add(new anchor_1.BN(69));
        // Execute the first deposit_v3 call
        let depositDataValues = Object.values(depositData);
        await program.methods
            .depositV3(...depositDataValues)
            .accounts(depositAccounts)
            .signers([depositor])
            .rpc();
        await new Promise((resolve) => setTimeout(resolve, 500));
        // Fetch and verify the depositEvent
        let events = await (0, utils_1.readProgramEvents)(connection, program);
        let event = events.find((event) => event.name === "v3FundsDeposited").data;
        const currentTime = await getCurrentTime(program, state);
        const { exclusivityPeriod, ...restOfDepositData } = depositData; // Strip exclusivityPeriod from depositData
        const expectedValues1 = {
            ...restOfDepositData,
            depositId: "1",
            exclusivityDeadline: currentTime + exclusivityPeriod.toNumber(),
        }; // Verify the event props emitted match the depositData.
        for (const [key, value] of Object.entries(expectedValues1)) {
            assertSE(event[key], value, `${key} should match`);
        }
        // Execute the second deposit_v3 call
        await program.methods
            .depositV3(...depositDataValues)
            .accounts(depositAccounts)
            .signers([depositor])
            .rpc();
        await new Promise((resolve) => setTimeout(resolve, 500));
        // Fetch and verify the depositEvent for the second deposit
        events = await (0, utils_1.readProgramEvents)(connection, program);
        event = events.find((event) => event.name === "v3FundsDeposited" && event.data.depositId.toString() === "2").data;
        const expectedValues2 = { ...expectedValues1, depositId: "2" }; // Verify the event props emitted match the depositData.
        for (const [key, value] of Object.entries(expectedValues2)) {
            assertSE(event[key], value, `${key} should match`);
        }
    });
    it("Fails to deposit tokens to a route that is uninitalized", async () => {
        const differentChainId = new anchor_1.BN(2); // Different chain ID
        if (!depositData.inputToken) {
            throw new Error("Input token is null");
        }
        const differentRoutePda = createRoutePda(depositData.inputToken, state, differentChainId);
        depositAccounts.route = differentRoutePda;
        try {
            const depositDataValues = Object.values({
                ...depositData,
                destinationChainId: differentChainId,
            });
            await program.methods
                .depositV3(...depositDataValues)
                .accounts(depositAccounts)
                .signers([depositor])
                .rpc();
            assert.fail("Deposit should have failed for a route that is not initialized");
        }
        catch (err) {
            assert.include(err.toString(), "AccountNotInitialized", "Expected AccountNotInitialized error");
        }
    });
    it("Fails to deposit tokens to a route that is explicitly disabled", async () => {
        // Disable the route
        await program.methods
            .setEnableRoute(depositData.inputToken, depositData.destinationChainId, false)
            .accounts(setEnableRouteAccounts)
            .rpc();
        try {
            const depositDataValues = Object.values(depositData);
            await program.methods
                .depositV3(...depositDataValues)
                .accounts(depositAccounts)
                .signers([depositor])
                .rpc();
            assert.fail("Deposit should have failed for a route that is explicitly disabled");
        }
        catch (err) {
            assert.include(err.toString(), "DisabledRoute", "Expected DisabledRoute error");
        }
    });
    it("Fails to process deposit when deposits are paused", async () => {
        // Pause deposits
        const pauseDepositsAccounts = { state, signer: owner, program: program.programId };
        await program.methods.pauseDeposits(true).accounts(pauseDepositsAccounts).rpc();
        const stateAccountData = await program.account.state.fetch(state);
        assert.isTrue(stateAccountData.pausedDeposits, "Deposits should be paused");
        // Try to deposit. This should fail because deposits are paused.
        try {
            const depositDataValues = Object.values(depositData);
            await program.methods
                .depositV3(...depositDataValues)
                .accounts(depositAccounts)
                .signers([depositor])
                .rpc();
            assert.fail("Should not be able to process deposit when deposits are paused");
        }
        catch (err) {
            assert.instanceOf(err, anchor.AnchorError);
            assert.strictEqual(err.error.errorCode.code, "DepositsArePaused", "Expected error code DepositsArePaused");
        }
    });
    it("Fails to deposit tokens with InvalidQuoteTimestamp when quote timestamp is in the future", async () => {
        const currentTime = await getCurrentTime(program, state);
        const futureQuoteTimestamp = new anchor_1.BN(currentTime + 10); // 10 seconds in the future
        depositData.quoteTimestamp = futureQuoteTimestamp;
        try {
            const depositDataValues = Object.values(depositData);
            await program.methods
                .depositV3(...depositDataValues)
                .accounts(depositAccounts)
                .signers([depositor])
                .rpc();
            assert.fail("Deposit should have failed due to InvalidQuoteTimestamp");
        }
        catch (err) {
            assert.include(err.toString(), "Error Code: InvalidQuoteTimestamp", "Expected InvalidQuoteTimestamp error");
        }
    });
    it("Fails to deposit tokens with quoteTimestamp is too old", async () => {
        const currentTime = await getCurrentTime(program, state);
        const futureQuoteTimestamp = new anchor_1.BN(currentTime - depositQuoteTimeBuffer.toNumber() - 1); // older than buffer.
        depositData.quoteTimestamp = futureQuoteTimestamp;
        try {
            const depositDataValues = Object.values(depositData);
            await program.methods
                .depositV3(...depositDataValues)
                .accounts(depositAccounts)
                .signers([depositor])
                .rpc();
            assert.fail("Deposit should have failed due to InvalidQuoteTimestamp");
        }
        catch (err) {
            assert.include(err.toString(), "Error Code: InvalidQuoteTimestamp", "Expected InvalidQuoteTimestamp error");
        }
    });
    it("Fails to deposit tokens with InvalidFillDeadline when fill deadline is invalid", async () => {
        const currentTime = await getCurrentTime(program, state);
        // Case 1: Fill deadline is older than the current time on the contract
        let invalidFillDeadline = currentTime - 1; // 1 second before current time on the contract.
        depositData.fillDeadline = new anchor_1.BN(invalidFillDeadline);
        depositData.quoteTimestamp = new anchor_1.BN(currentTime - 1); // 1 second before current time on the contract to reset.
        try {
            const depositDataValues = Object.values(depositData);
            await program.methods
                .depositV3(...depositDataValues)
                .accounts(depositAccounts)
                .signers([depositor])
                .rpc();
            assert.fail("Deposit should have failed due to InvalidFillDeadline (past deadline)");
        }
        catch (err) {
            assert.include(err.toString(), "InvalidFillDeadline", "Expected InvalidFillDeadline error for past deadline");
        }
        // Case 2: Fill deadline is too far ahead (longer than fill_deadline_buffer + currentTime)
        invalidFillDeadline = currentTime + fillDeadlineBuffer.toNumber() + 1; // 1 seconds beyond the buffer
        depositData.fillDeadline = new anchor_1.BN(invalidFillDeadline);
        try {
            const depositDataValues = Object.values(depositData);
            await program.methods
                .depositV3(...depositDataValues)
                .accounts(depositAccounts)
                .signers([depositor])
                .rpc();
            assert.fail("Deposit should have failed due to InvalidFillDeadline (future deadline)");
        }
        catch (err) {
            assert.include(err.toString(), "InvalidFillDeadline", "Expected InvalidFillDeadline error for future deadline");
        }
    });
    it("Fails to process deposit for mint inconsistent input_token", async () => {
        // Save the correct data and accounts from global scope before changing it when creating a new input token.
        const firstInputToken = inputToken;
        const firstDepositAccounts = depositAccounts;
        // Create a new input token and enable the route (this updates global scope variables).
        await setupInputToken();
        await enableRoute();
        // Try to execute the deposit_v3 call with malformed inputs where the first input token and its derived route is
        // passed combined with mint, vault and user token account from the second input token.
        const malformedDepositData = { ...depositData, inputToken: firstInputToken };
        const malformedDepositAccounts = { ...depositAccounts, route: firstDepositAccounts.route };
        try {
            const depositDataValues = Object.values(malformedDepositData);
            await program.methods
                .depositV3(...depositDataValues)
                .accounts(malformedDepositAccounts)
                .signers([depositor])
                .rpc();
            assert.fail("Should not be able to process deposit for inconsistent mint");
        }
        catch (err) {
            assert.instanceOf(err, anchor.AnchorError);
            assert.strictEqual(err.error.errorCode.code, "InvalidMint", "Expected error code InvalidMint");
        }
    });
    it("Tests deposit with a fake route PDA", async () => {
        // Create fake program state
        const fakeState = await initializeState();
        const fakeVault = getVaultAta(inputToken, fakeState);
        const fakeRouteChainId = new anchor_1.BN(3);
        const fakeRoutePda = createRoutePda(inputToken, fakeState, fakeRouteChainId);
        // A seeds constraint was violated.
        const fakeSetEnableRouteAccounts = {
            signer: owner,
            payer: owner,
            state: fakeState,
            route: fakeRoutePda,
            vault: fakeVault,
            originTokenMint: inputToken,
            tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
            associatedTokenProgram: spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID,
            systemProgram: anchor.web3.SystemProgram.programId,
            program: program.programId,
        };
        await program.methods.setEnableRoute(inputToken, fakeRouteChainId, true).accounts(fakeSetEnableRouteAccounts).rpc();
        await new Promise((resolve) => setTimeout(resolve, 2000));
        const fakeDepositAccounts = {
            state: fakeState,
            route: fakeRoutePda,
            signer: depositor.publicKey,
            depositorTokenAccount: depositorTA,
            vault: fakeVault,
            mint: inputToken,
            tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
            program: program.programId,
        };
        // Deposit with the fake state and route PDA should succeed.
        const depositDataValues = Object.values({
            ...depositData,
            destinationChainId: fakeRouteChainId,
        });
        await program.methods
            .depositV3(...depositDataValues)
            .accounts(fakeDepositAccounts)
            .signers([depositor])
            .rpc();
        await new Promise((resolve) => setTimeout(resolve, 500));
        // Fetch and verify the depositEvent
        let events = await (0, utils_1.readProgramEvents)(connection, program);
        let event = events.find((event) => event.name === "v3FundsDeposited").data;
        const { exclusivityPeriod, ...restOfDepositData } = depositData; // Strip exclusivityPeriod from depositData
        const expectedValues = { ...{ ...restOfDepositData, destinationChainId: fakeRouteChainId }, depositId: "1" }; // Verify the event props emitted match the depositData.
        for (const [key, value] of Object.entries(expectedValues)) {
            assertSE(event[key], value, `${key} should match`);
        }
        // Check fake vault acount balance
        const fakeVaultAccount = await (0, spl_token_1.getAccount)(connection, fakeVault);
        assertSE(fakeVaultAccount.amount, depositData.inputAmount.toNumber(), "Fake vault balance should be increased by the deposited amount");
        // Deposit with the fake route in the original program state should fail.
        try {
            const depositDataValues = Object.values({
                ...{ ...depositData, destinationChainId: fakeRouteChainId },
            });
            await program.methods
                .depositV3(...depositDataValues)
                .accounts({ ...depositAccounts, route: fakeRoutePda })
                .signers([depositor])
                .rpc();
            assert.fail("Deposit should have failed for a fake route PDA");
        }
        catch (err) {
            assert.include(err.toString(), "A seeds constraint was violated");
        }
        const vaultAccount = await (0, spl_token_1.getAccount)(connection, vault);
        assertSE(vaultAccount.amount, 0, "Vault balance should not be changed by the fake route deposit");
    });
    it("depositV3Now behaves as deposit but forces the quote timestamp as expected", async () => {
        // Set up initial deposit data. Note that this method has a slightly different interface to deposit, using
        // fillDeadlineOffset rather than fillDeadline. current chain time is added to fillDeadlineOffset to set the
        // fillDeadline for the deposit. exclusivityPeriod operates the same as in standard deposit.
        // Equally, depositV3Now does not have `quoteTimestamp`. this is set to the current time from the program.
        const fillDeadlineOffset = 60; // 60 seconds offset
        // Execute the deposit_v3_now call. Remove the quoteTimestamp from the depositData as not needed for this method.
        await program.methods
            .depositV3Now(depositData.depositor, depositData.recipient, depositData.inputToken, depositData.outputToken, depositData.inputAmount, depositData.outputAmount, depositData.destinationChainId, depositData.exclusiveRelayer, fillDeadlineOffset, depositData.exclusivityPeriod.toNumber(), depositData.message)
            .accounts(depositAccounts)
            .signers([depositor])
            .rpc();
        await new Promise((resolve) => setTimeout(resolve, 500));
        // Fetch and verify the depositEvent
        const events = await (0, utils_1.readProgramEvents)(connection, program);
        const event = events.find((event) => event.name === "v3FundsDeposited").data;
        // Verify the event props emitted match the expected values
        const currentTime = await getCurrentTime(program, state);
        const { exclusivityPeriod, ...restOfDepositData } = depositData; // Strip exclusivityPeriod from depositData
        const expectedValues = {
            ...restOfDepositData,
            quoteTimestamp: currentTime,
            fillDeadline: currentTime + fillDeadlineOffset,
            exclusivityDeadline: currentTime + exclusivityPeriod.toNumber(),
            depositId: "1",
        };
        for (const [key, value] of Object.entries(expectedValues)) {
            assertSE(event[key], value, `${key} should match`);
        }
    });
});
