"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 chai_1 = require("chai");
const SvmSpoke_common_1 = require("./SvmSpoke.common");
const MerkleTree_1 = require("@uma/common/dist/MerkleTree");
const spl_token_1 = require("@solana/spl-token");
const utils_1 = require("./utils");
const { provider, program, owner, initializeState, connection, chainId, assertSE } = SvmSpoke_common_1.common;
describe("svm_spoke.refund_claims", () => {
    anchor.setProvider(provider);
    const claimInitializer = web3_js_1.Keypair.generate();
    const relayer = web3_js_1.Keypair.generate();
    let state, mint, tokenAccount, claimAccount, vault, transferLiability;
    let claimRelayerRefundAccounts;
    const payer = anchor_1.AnchorProvider.env().wallet.payer;
    const initialMintAmount = 10_000_000_000;
    const initializeClaimAccount = async (initializer = claimInitializer) => {
        const initializeClaimAccountIx = await program.methods
            .initializeClaimAccount(mint, relayer.publicKey)
            .accounts({ signer: initializer.publicKey })
            .instruction();
        await anchor_1.web3.sendAndConfirmTransaction(connection, new anchor_1.web3.Transaction().add(initializeClaimAccountIx), [
            initializer,
        ]);
    };
    const executeRelayerRefundToClaim = async (relayerRefund, initializer = claimInitializer) => {
        // Initialize the claim account if it does not exist.
        try {
            await program.account.claimAccount.fetch(claimAccount);
        }
        catch (error) {
            chai_1.assert.include(error.toString(), "Account does not exist or has no data", "Expected non-existent account error");
            await initializeClaimAccount(initializer);
        }
        // Prepare leaf using token accounts.
        const relayerRefundLeaves = [];
        relayerRefundLeaves.push({
            isSolana: true,
            leafId: new anchor_1.BN(0),
            chainId: chainId,
            amountToReturn: new anchor_1.BN(0),
            mintPublicKey: mint,
            refundAddresses: [relayer.publicKey],
            refundAmounts: [relayerRefund],
        });
        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(root)).accounts(relayRootBundleAccounts).rpc();
        // Pass claim account as relayer refund address.
        const remainingAccounts = [{ pubkey: claimAccount, isWritable: true, isSigner: false }];
        const iVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
        const iRefundLiability = (await program.account.claimAccount.fetch(claimAccount)).amount.toString();
        // Verify valid leaf
        const executeRelayerRefundLeafAccounts = {
            state,
            rootBundle: rootBundle,
            signer: owner,
            vault,
            tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
            mint,
            transferLiability,
            systemProgram: anchor_1.web3.SystemProgram.programId,
            program: program.programId,
        };
        const proofAsNumbers = proof.map((p) => Array.from(p));
        await (0, utils_1.loadExecuteRelayerRefundLeafParams)(program, owner, stateAccountData.rootBundleId, leaf, proofAsNumbers);
        await program.methods
            .executeRelayerRefundLeafDeferred()
            .accounts(executeRelayerRefundLeafAccounts)
            .remainingAccounts(remainingAccounts)
            .rpc();
        // No funds should have moved out of the vault.
        const fVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
        assertSE(iVaultBal, fVaultBal, "Vault balance");
        // Refund liability added in the claim account for the relayer.
        const fRefundLiability = (await program.account.claimAccount.fetch(claimAccount)).amount.toString();
        assertSE(BigInt(fRefundLiability) - BigInt(iRefundLiability), relayerRefund, "Refund liability");
    };
    beforeEach(async () => {
        state = await initializeState();
        mint = await (0, spl_token_1.createMint)(connection, payer, owner, owner, 6);
        tokenAccount = (await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(connection, payer, mint, relayer.publicKey)).address;
        [claimAccount] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("claim_account"), mint.toBuffer(), relayer.publicKey.toBuffer()], program.programId);
        vault = (await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(connection, payer, mint, state, true)).address;
        claimRelayerRefundAccounts = {
            signer: owner,
            initializer: claimInitializer.publicKey,
            state,
            vault,
            mint,
            tokenAccount,
            claimAccount,
            tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
            program: program.programId,
        };
        const sig = await connection.requestAirdrop(claimInitializer.publicKey, 10_000_000_000);
        await provider.connection.confirmTransaction(sig);
        // mint mint to vault
        await (0, spl_token_1.mintTo)(connection, payer, mint, vault, provider.publicKey, initialMintAmount);
        [transferLiability] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("transfer_liability"), mint.toBuffer()], program.programId);
    });
    it("Claim on behalf of single relayer", async () => {
        // Execute relayer refund using claim account.
        const relayerRefund = new anchor_1.BN(500000);
        await executeRelayerRefundToClaim(relayerRefund);
        const iVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
        const iRelayerBal = (await connection.getTokenAccountBalance(tokenAccount)).value.amount;
        // Claim refund for the relayer.
        await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
        // The relayer should have received funds from the vault.
        const fVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
        const fRelayerBal = (await connection.getTokenAccountBalance(tokenAccount)).value.amount;
        assertSE(BigInt(iVaultBal) - BigInt(fVaultBal), relayerRefund, "Vault balance");
        assertSE(BigInt(fRelayerBal) - BigInt(iRelayerBal), relayerRefund, "Relayer balance");
        // Verify the ClaimedRelayerRefund event
        await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for event processing
        const events = await (0, utils_1.readProgramEvents)(connection, program);
        const event = events.find((event) => event.name === "claimedRelayerRefund").data;
        assertSE(event.l2TokenAddress, mint, "l2TokenAddress should match");
        assertSE(event.claimAmount, relayerRefund, "Relayer refund amount should match");
        assertSE(event.refundAddress, relayer.publicKey, "Relayer refund address should match");
    });
    it("Cannot Double Claim Relayer Refund", async () => {
        // Execute relayer refund using claim account.
        const relayerRefund = new anchor_1.BN(500000);
        await executeRelayerRefundToClaim(relayerRefund);
        // Claim refund for the relayer.
        await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
        // The claim account should have been automatically closed, so repeated claim should fail.
        try {
            await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
            chai_1.assert.fail("Claiming refund from closed account should fail");
        }
        catch (error) {
            chai_1.assert.instanceOf(error, anchor_1.AnchorError);
            chai_1.assert.strictEqual(error.error.errorCode.code, "AccountNotInitialized", "Expected error code AccountNotInitialized");
        }
        // After reinitalizing the claim account, the repeated claim should still fail.
        await initializeClaimAccount();
        try {
            await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
            chai_1.assert.fail("Claiming refund from reinitalized account should fail");
        }
        catch (error) {
            chai_1.assert.instanceOf(error, anchor_1.AnchorError);
            chai_1.assert.strictEqual(error.error.errorCode.code, "ZeroRefundClaim", "Expected error code ZeroRefundClaim");
        }
    });
    it("Claim Multiple Deferred Relayer Refunds", async () => {
        // Execute two relayer refunds to the same claim account.
        const firstRelayerRefund = new anchor_1.BN(500000);
        const secondRelayerRefund = new anchor_1.BN(1000000);
        await executeRelayerRefundToClaim(firstRelayerRefund);
        await executeRelayerRefundToClaim(secondRelayerRefund);
        const iVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
        const iRelayerBal = (await connection.getTokenAccountBalance(tokenAccount)).value.amount;
        // Claim refund for the relayer.
        await await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
        // The relayer should have received both refunds.
        const fVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
        const fRelayerBal = (await connection.getTokenAccountBalance(tokenAccount)).value.amount;
        const totalRefund = firstRelayerRefund.add(secondRelayerRefund);
        assertSE(BigInt(iVaultBal) - BigInt(fVaultBal), totalRefund, "Vault balance");
        assertSE(BigInt(fRelayerBal) - BigInt(iRelayerBal), totalRefund, "Relayer balance");
    });
    it("Claim Relayer Refund With Another Initializer", async () => {
        // Fund different claim account initializer.
        const anotherInitializer = web3_js_1.Keypair.generate();
        const sig = await connection.requestAirdrop(anotherInitializer.publicKey, 10_000_000_000);
        await provider.connection.confirmTransaction(sig);
        // Execute relayer refund using claim account created by another initializer.
        const relayerRefund = new anchor_1.BN(500000);
        await executeRelayerRefundToClaim(relayerRefund, anotherInitializer);
        const iVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
        const iRelayerBal = (await connection.getTokenAccountBalance(tokenAccount)).value.amount;
        // Claiming with default initializer should fail.
        try {
            await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
        }
        catch (error) {
            chai_1.assert.instanceOf(error, anchor_1.AnchorError);
            chai_1.assert.strictEqual(error.error.errorCode.code, "InvalidClaimInitializer", "Expected error code InvalidClaimInitializer");
        }
        // Claim refund for the relayer passing the correct initializer account.
        claimRelayerRefundAccounts.initializer = anotherInitializer.publicKey;
        await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
        // The relayer should have received funds from the vault.
        const fVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
        const fRelayerBal = (await connection.getTokenAccountBalance(tokenAccount)).value.amount;
        assertSE(BigInt(iVaultBal) - BigInt(fVaultBal), relayerRefund, "Vault balance");
        assertSE(BigInt(fRelayerBal) - BigInt(iRelayerBal), relayerRefund, "Relayer balance");
    });
    it("Close empty claim account", async () => {
        // Initialize the claim account.
        await initializeClaimAccount(claimInitializer);
        // Should not be able to close the claim account from default wallet as the initializer was different.
        try {
            await program.methods.closeClaimAccount(mint, relayer.publicKey).accounts({ signer: payer.publicKey }).rpc();
            chai_1.assert.fail("Closing claim account from different initializer should fail");
        }
        catch (error) {
            chai_1.assert.instanceOf(error, anchor_1.AnchorError);
            chai_1.assert.strictEqual(error.error.errorCode.code, "InvalidClaimInitializer", "Expected error code InvalidClaimInitializer");
        }
        // Close the claim account from initializer before executing relayer refunds.
        await program.methods
            .closeClaimAccount(mint, relayer.publicKey)
            .accounts({ signer: claimInitializer.publicKey })
            .signers([claimInitializer])
            .rpc();
        // Claim account should be closed now.
        try {
            await program.account.claimAccount.fetch(claimAccount);
            chai_1.assert.fail("Claim account should be closed");
        }
        catch (error) {
            chai_1.assert.include(error.toString(), "Account does not exist or has no data", "Expected non-existent account error");
        }
    });
    it("Cannot close non-empty claim account", async () => {
        // Execute relayer refund using claim account.
        const relayerRefund = new anchor_1.BN(500000);
        await executeRelayerRefundToClaim(relayerRefund);
        // It should be not possible to close the claim account with non-zero refund liability.
        try {
            await program.methods
                .closeClaimAccount(mint, relayer.publicKey)
                .accounts({ signer: claimInitializer.publicKey })
                .signers([claimInitializer])
                .rpc();
            chai_1.assert.fail("Closing claim account with non-zero refund liability should fail");
        }
        catch (error) {
            chai_1.assert.instanceOf(error, anchor_1.AnchorError);
            chai_1.assert.strictEqual(error.error.errorCode.code, "NonZeroRefundClaim", "Expected error code NonZeroRefundClaim");
        }
    });
    it("Cannot claim refund on behalf of relayer to wrong token account", async () => {
        // Execute relayer refund using claim account.
        const relayerRefund = new anchor_1.BN(500000);
        await executeRelayerRefundToClaim(relayerRefund);
        // Claim refund for the relayer to a custom token account.
        const wrongOwner = web3_js_1.Keypair.generate().publicKey;
        const wrongTokenAccount = (await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(connection, payer, mint, wrongOwner)).address;
        claimRelayerRefundAccounts.tokenAccount = wrongTokenAccount;
        try {
            await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
            chai_1.assert.fail("Claiming refund to custom token account should fail");
        }
        catch (error) {
            chai_1.assert.instanceOf(error, anchor_1.AnchorError);
            chai_1.assert.strictEqual(error.error.errorCode.code, "ConstraintTokenOwner", "Expected error code ConstraintTokenOwner");
        }
    });
    it("Relayer can claim refunds to custom token account", async () => {
        // Execute relayer refund using claim account.
        const relayerRefund = new anchor_1.BN(500000);
        await executeRelayerRefundToClaim(relayerRefund);
        const iVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
        const iRelayerBal = (await connection.getTokenAccountBalance(tokenAccount)).value.amount;
        // Create custom token account for the relayer (no need to be controlled by the relayer)
        const anotherOwner = web3_js_1.Keypair.generate().publicKey;
        const customTokenAccount = (await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(connection, payer, mint, anotherOwner)).address;
        claimRelayerRefundAccounts.tokenAccount = customTokenAccount;
        claimRelayerRefundAccounts.signer = relayer.publicKey; // Only relayer itself should be able to do this.
        // Relayer can claim refund to custom token account.
        await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).signers([relayer]).rpc();
        // The relayer should have received funds from the vault.
        const fVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
        const fRelayerBal = (await connection.getTokenAccountBalance(customTokenAccount)).value.amount;
        assertSE(BigInt(iVaultBal) - BigInt(fVaultBal), relayerRefund, "Vault balance");
        assertSE(BigInt(fRelayerBal) - BigInt(iRelayerBal), relayerRefund, "Relayer balance");
        // Verify the ClaimedRelayerRefund event
        await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for event processing
        const events = await (0, utils_1.readProgramEvents)(connection, program);
        const event = events.find((event) => event.name === "claimedRelayerRefund").data;
        assertSE(event.l2TokenAddress, mint, "l2TokenAddress should match");
        assertSE(event.claimAmount, relayerRefund, "Relayer refund amount should match");
        assertSE(event.refundAddress, relayer.publicKey, "Relayer refund address should match");
    });
    it("Cannot claim relayer refunds with the wrong signer", async () => {
        // Execute relayer refund using claim account.
        const relayerRefund = new anchor_1.BN(500000);
        await executeRelayerRefundToClaim(relayerRefund);
        // Claim refund for the relayer with the default signer should fail as relayer address is part of claim account derivation.
        try {
            await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).rpc();
            chai_1.assert.fail("Claiming refund with wrong signer should fail");
        }
        catch (error) {
            chai_1.assert.instanceOf(error, anchor_1.AnchorError);
            chai_1.assert.strictEqual(error.error.errorCode.code, "ConstraintSeeds", "Expected error code ConstraintSeeds");
            chai_1.assert.strictEqual(error.error.origin, "claim_account", "Expected error on claim_account");
        }
    });
});
