"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 web3_js_1 = require("@solana/web3.js");
const spl_token_1 = require("@solana/spl-token");
const MerkleTree_1 = require("@uma/common/dist/MerkleTree");
const SvmSpoke_common_1 = require("./SvmSpoke.common");
const utils_1 = require("./utils");
const chai_1 = require("chai");
const cctpHelpers_1 = require("./cctpHelpers");
const { provider, program, owner, initializeState, connection, remoteDomain, chainId, crossDomainAdmin } = SvmSpoke_common_1.common;
describe("svm_spoke.token_bridge", () => {
    anchor.setProvider(provider);
    const tokenMessengerMinterProgram = anchor_1.workspace.TokenMessengerMinter;
    const messageTransmitterProgram = anchor_1.workspace.MessageTransmitter;
    let state, mint, vault, tokenMinter, messageTransmitter, tokenMessenger, remoteTokenMessenger, eventAuthority, transferLiability, localToken, tokenMessengerMinterSenderAuthority;
    let messageSentEventData; // This will hold CCTP message data.
    let bridgeTokensToHubPoolAccounts;
    const payer = anchor_1.AnchorProvider.env().wallet.payer;
    const initialMintAmount = 10_000_000_000;
    before(async () => {
        // token_minter state is pulled from devnet (DBD8hAwLDRQkTsu6EqviaYNGKPnsAMmQonxf7AH8ZcFY) with its
        // token_controller field overridden to test wallet.
        tokenMinter = (0, utils_1.findProgramAddress)("token_minter", tokenMessengerMinterProgram.programId).publicKey;
        // message_transmitter state is forked from devnet (BWrwSWjbikT3H7qHAkUEbLmwDQoB4ZDJ4wcSEhSPTZCu).
        messageTransmitter = (0, utils_1.findProgramAddress)("message_transmitter", messageTransmitterProgram.programId).publicKey;
        // token_messenger state is forked from devnet (Afgq3BHEfCE7d78D2XE9Bfyu2ieDqvE24xX8KDwreBms).
        tokenMessenger = (0, utils_1.findProgramAddress)("token_messenger", tokenMessengerMinterProgram.programId).publicKey;
        // Ethereum remote_token_messenger state is forked from devnet (Hazwi3jFQtLKc2ughi7HFXPkpDeso7DQaMR9Ks4afh3j).
        remoteTokenMessenger = (0, utils_1.findProgramAddress)("remote_token_messenger", tokenMessengerMinterProgram.programId, [
            remoteDomain.toString(),
        ]).publicKey;
        // PDA for token_messenger_minter to emit DepositForBurn event via CPI.
        eventAuthority = (0, utils_1.findProgramAddress)("__event_authority", tokenMessengerMinterProgram.programId).publicKey;
        // PDA, used to check that CCTP sendMessage was called by TokenMessenger
        tokenMessengerMinterSenderAuthority = (0, utils_1.findProgramAddress)("sender_authority", tokenMessengerMinterProgram.programId).publicKey;
    });
    beforeEach(async () => {
        // Each test will have different state and mint token.
        state = await initializeState();
        mint = await (0, spl_token_1.createMint)(connection, payer, owner, owner, 6);
        vault = (await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(connection, payer, mint, state, true)).address;
        await (0, spl_token_1.mintTo)(connection, payer, mint, vault, provider.publicKey, initialMintAmount);
        transferLiability = (0, utils_1.findProgramAddress)("transfer_liability", program.programId, [mint]).publicKey;
        localToken = (0, utils_1.findProgramAddress)("local_token", tokenMessengerMinterProgram.programId, [mint]).publicKey;
        // add local cctp token
        const custodyTokenAccount = (0, utils_1.findProgramAddress)("custody", tokenMessengerMinterProgram.programId, [
            mint,
        ]).publicKey;
        const addLocalTokenAccounts = {
            tokenController: owner,
            tokenMinter,
            localToken,
            custodyTokenAccount,
            localTokenMint: mint,
            tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
            systemProgram: anchor_1.web3.SystemProgram.programId,
            program: tokenMessengerMinterProgram.programId,
            eventAuthority,
        };
        await tokenMessengerMinterProgram.methods.addLocalToken({}).accounts(addLocalTokenAccounts).rpc();
        // set max burn amount per CCTP message for local token to total mint amount.
        const setMaxBurnAmountPerMessageAccounts = {
            tokenMinter,
            localToken,
            program: tokenMessengerMinterProgram.programId,
            eventAuthority,
        };
        await tokenMessengerMinterProgram.methods
            .setMaxBurnAmountPerMessage({ burnLimitPerMessage: new anchor_1.BN(initialMintAmount) })
            .accounts(setMaxBurnAmountPerMessageAccounts)
            .rpc();
        // Populate accounts for bridgeTokensToHubPool.
        messageSentEventData = anchor_1.web3.Keypair.generate();
        bridgeTokensToHubPoolAccounts = {
            payer: owner,
            mint,
            state,
            transferLiability,
            vault,
            tokenMessengerMinterSenderAuthority,
            messageTransmitter,
            tokenMessenger,
            remoteTokenMessenger,
            tokenMinter,
            localToken,
            messageSentEventData: messageSentEventData.publicKey,
            messageTransmitterProgram: messageTransmitterProgram.programId,
            tokenMessengerMinterProgram: tokenMessengerMinterProgram.programId,
            tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
            systemProgram: anchor_1.web3.SystemProgram.programId,
            cctpEventAuthority: eventAuthority,
        };
    });
    const initializeBridgeToHubPool = async (amountToReturn) => {
        // Prepare root bundle with a single leaf containing amount to bridge to the HubPool.
        const relayerRefundLeaves = [];
        relayerRefundLeaves.push({
            isSolana: true,
            leafId: new anchor_1.BN(0),
            chainId,
            amountToReturn: new anchor_1.BN(amountToReturn),
            mintPublicKey: mint,
            refundAddresses: [],
            refundAmounts: [],
        });
        const merkleTree = new MerkleTree_1.MerkleTree(relayerRefundLeaves, utils_1.relayerRefundHashFn);
        const root = merkleTree.getRoot();
        const proof = merkleTree.getProof(relayerRefundLeaves[0]);
        const leaf = relayerRefundLeaves[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"), state.toBuffer(), rootBundleIdBuffer];
        const [rootBundle] = web3_js_1.PublicKey.findProgramAddressSync(seeds, program.programId);
        // Relay root bundle
        const relayRootBundleAccounts = {
            state,
            rootBundle,
            signer: owner,
            payer: owner,
            program: program.programId,
        };
        await program.methods
            .relayRootBundle(Array.from(root), Array.from(Buffer.alloc(32)))
            .accounts(relayRootBundleAccounts)
            .rpc();
        // Execute relayer refund leaf.
        const proofAsNumbers = proof.map((p) => Array.from(p));
        const executeRelayerRefundLeafAccounts = {
            state,
            rootBundle,
            signer: owner,
            vault,
            mint,
            transferLiability,
            tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
            systemProgram: anchor_1.web3.SystemProgram.programId,
            program: program.programId,
        };
        await (0, utils_1.loadExecuteRelayerRefundLeafParams)(program, owner, stateAccountData.rootBundleId, leaf, proofAsNumbers);
        await program.methods.executeRelayerRefundLeaf().accounts(executeRelayerRefundLeafAccounts).rpc();
    };
    it("Bridge all pending tokens to HubPool in single transaction", async () => {
        const pendingToHubPool = 1_000_000;
        await initializeBridgeToHubPool(pendingToHubPool);
        const initialVaultBalance = (await connection.getTokenAccountBalance(vault)).value.amount;
        chai_1.assert.strictEqual(initialVaultBalance, initialMintAmount.toString());
        await program.methods
            .bridgeTokensToHubPool(new anchor_1.BN(pendingToHubPool))
            .accounts(bridgeTokensToHubPoolAccounts)
            .signers([messageSentEventData])
            .rpc();
        const finalVaultBalance = (await connection.getTokenAccountBalance(vault)).value.amount;
        chai_1.assert.strictEqual(finalVaultBalance, (initialMintAmount - pendingToHubPool).toString());
        const finalPendingToHubPool = (await program.account.transferLiability.fetch(transferLiability)).pendingToHubPool;
        chai_1.assert.isTrue(finalPendingToHubPool.isZero(), "Invalid pending to HubPool amount");
        const message = (0, cctpHelpers_1.decodeMessageSentData)((await messageTransmitterProgram.account.messageSent.fetch(messageSentEventData.publicKey)).message);
        chai_1.assert.strictEqual(message.destinationDomain, remoteDomain.toNumber(), "Invalid destination domain");
        chai_1.assert.isTrue(message.messageBody.burnToken.equals(mint), "Invalid burn token");
        chai_1.assert.isTrue(message.messageBody.mintRecipient.equals(crossDomainAdmin), "Invalid mint recipient");
        chai_1.assert.strictEqual(message.messageBody.amount.toString(), pendingToHubPool.toString(), "Invalid amount");
    });
    it("Bridge above pending tokens in single transaction to HubPool should fail", async () => {
        const pendingToHubPool = 1_000_000;
        const bridgeAmount = pendingToHubPool + 1;
        await initializeBridgeToHubPool(pendingToHubPool);
        try {
            await program.methods
                .bridgeTokensToHubPool(new anchor_1.BN(bridgeAmount))
                .accounts(bridgeTokensToHubPoolAccounts)
                .signers([messageSentEventData])
                .rpc();
            chai_1.assert.fail("Should not be able to bridge above pending tokens to HubPool");
        }
        catch (error) {
            chai_1.assert.instanceOf(error, anchor_1.AnchorError);
            chai_1.assert.strictEqual(error.error.errorCode.code, "ExceededPendingBridgeAmount", "Expected error code ExceededPendingBridgeAmount");
        }
    });
    it("Bridge pending tokens to HubPool in multiple transactions", async () => {
        const pendingToHubPool = 10_000_000;
        const singleBridgeAmount = pendingToHubPool / 5;
        await initializeBridgeToHubPool(pendingToHubPool);
        const initialVaultBalance = (await connection.getTokenAccountBalance(vault)).value.amount;
        chai_1.assert.strictEqual(initialVaultBalance, initialMintAmount.toString());
        for (let i = 0; i < 5; i++) {
            const loopMessageSentEventData = anchor_1.web3.Keypair.generate();
            await program.methods
                .bridgeTokensToHubPool(new anchor_1.BN(singleBridgeAmount))
                .accounts({ ...bridgeTokensToHubPoolAccounts, messageSentEventData: loopMessageSentEventData.publicKey })
                .signers([loopMessageSentEventData])
                .rpc();
        }
        const finalVaultBalance = (await connection.getTokenAccountBalance(vault)).value.amount;
        chai_1.assert.strictEqual(finalVaultBalance, (initialMintAmount - pendingToHubPool).toString());
        const finalPendingToHubPool = (await program.account.transferLiability.fetch(transferLiability)).pendingToHubPool;
        chai_1.assert.isTrue(finalPendingToHubPool.isZero(), "Invalid pending to HubPool amount");
    });
    it("Bridge above pending tokens in multiple transactions to HubPool should fail", async () => {
        const pendingToHubPool = 10_000_000;
        const singleBridgeAmount = pendingToHubPool / 5;
        await initializeBridgeToHubPool(pendingToHubPool);
        const initialVaultBalance = (await connection.getTokenAccountBalance(vault)).value.amount;
        chai_1.assert.strictEqual(initialVaultBalance, initialMintAmount.toString());
        // Bridge out first 4 tranches.
        for (let i = 0; i < 4; i++) {
            const loopMessageSentEventData = anchor_1.web3.Keypair.generate();
            await program.methods
                .bridgeTokensToHubPool(new anchor_1.BN(singleBridgeAmount))
                .accounts({ ...bridgeTokensToHubPoolAccounts, messageSentEventData: loopMessageSentEventData.publicKey })
                .signers([loopMessageSentEventData])
                .rpc();
        }
        // Try to bridge out more tokens in the final tranche.
        try {
            await program.methods
                .bridgeTokensToHubPool(new anchor_1.BN(singleBridgeAmount + 1))
                .accounts(bridgeTokensToHubPoolAccounts)
                .signers([messageSentEventData])
                .rpc();
            chai_1.assert.fail("Should not be able to bridge above pending tokens to HubPool");
        }
        catch (error) {
            chai_1.assert.instanceOf(error, anchor_1.AnchorError);
            chai_1.assert.strictEqual(error.error.errorCode.code, "ExceededPendingBridgeAmount", "Expected error code ExceededPendingBridgeAmount");
        }
    });
    it("Test BridgedToHubPool event", async () => {
        const simpleBridgeAmount = 500_000;
        // Initialize the bridge with a specific amount.
        await initializeBridgeToHubPool(simpleBridgeAmount);
        const initialVaultBalance = (await connection.getTokenAccountBalance(vault)).value.amount;
        chai_1.assert.strictEqual(initialVaultBalance, initialMintAmount.toString());
        // Create a new Keypair for the message event data.
        const simpleBridgeMessageSentEventData = anchor_1.web3.Keypair.generate();
        // Perform the bridge operation.
        await program.methods
            .bridgeTokensToHubPool(new anchor_1.BN(simpleBridgeAmount))
            .accounts({ ...bridgeTokensToHubPoolAccounts, messageSentEventData: simpleBridgeMessageSentEventData.publicKey })
            .signers([simpleBridgeMessageSentEventData])
            .rpc();
        // Wait for the event to be emitted.
        await new Promise((resolve) => setTimeout(resolve, 500));
        const events = await (0, utils_1.readProgramEvents)(connection, program);
        const event = events.find((event) => event.name === "bridgedToHubPool").data;
        chai_1.assert.isNotNull(event, "BridgedToHubPool event should be emitted");
        chai_1.assert.strictEqual(event.amount.toString(), simpleBridgeAmount.toString(), "Invalid amount");
        chai_1.assert.strictEqual(event.mint.toString(), mint.toString(), "Invalid mint");
    });
});
