import { EthAddress, Fr, computeSecretHash, } from '@aztec/aztec.js';
import { extractEvent } from '@aztec/ethereum';
import { sha256ToField } from '@aztec/foundation/crypto';
import { FeeJuicePortalAbi, OutboxAbi, TestERC20Abi, TokenPortalAbi } from '@aztec/l1-artifacts';
import { getContract, toFunctionSelector, } from 'viem';
/** Stringifies an eth address for logging. */
function stringifyEthAddress(address, name) {
    return name ? `${name} (${address.toString()})` : address.toString();
}
/** Generates a pair secret and secret hash */
export function generateClaimSecret(logger) {
    const secret = Fr.random();
    const secretHash = computeSecretHash(secret);
    logger?.verbose(`Generated claim secret=${secret.toString()} hash=${secretHash.toString()}`);
    return [secret, secretHash];
}
/** Helper for managing an ERC20 on L1. */
export class L1TokenManager {
    constructor(
    /** Address of the ERC20 contract. */
    address, publicClient, walletClient, logger) {
        this.address = address;
        this.publicClient = publicClient;
        this.walletClient = walletClient;
        this.logger = logger;
        this.contract = getContract({
            address: this.address.toString(),
            abi: TestERC20Abi,
            client: this.walletClient,
        });
    }
    /**
     * Returns the balance of the given address.
     * @param address - Address to get the balance of.
     */
    async getL1TokenBalance(address) {
        return await this.contract.read.balanceOf([address]);
    }
    /**
     * Mints tokens for the given address. Returns once the tx has been mined.
     * @param amount - Amount to mint.
     * @param address - Address to mint the tokens for.
     * @param addressName - Optional name of the address for logging.
     */
    async mint(amount, address, addressName) {
        this.logger.info(`Minting ${amount} tokens for ${stringifyEthAddress(address, addressName)}`);
        await this.publicClient.waitForTransactionReceipt({
            hash: await this.contract.write.mint([address, amount]),
        });
    }
    /**
     * Approves tokens for the given address. Returns once the tx has been mined.
     * @param amount - Amount to approve.
     * @param address - Address to approve the tokens for.
     * @param addressName - Optional name of the address for logging.
     */
    async approve(amount, address, addressName = '') {
        this.logger.info(`Approving ${amount} tokens for ${stringifyEthAddress(address, addressName)}`);
        await this.publicClient.waitForTransactionReceipt({
            hash: await this.contract.write.approve([address, amount]),
        });
    }
}
/** Helper for interacting with the FeeJuicePortal on L1. */
export class L1FeeJuicePortalManager {
    constructor(portalAddress, tokenAddress, publicClient, walletClient, logger) {
        this.publicClient = publicClient;
        this.walletClient = walletClient;
        this.logger = logger;
        this.tokenManager = new L1TokenManager(tokenAddress, publicClient, walletClient, logger);
        this.contract = getContract({
            address: portalAddress.toString(),
            abi: FeeJuicePortalAbi,
            client: this.walletClient,
        });
    }
    /** Returns the associated token manager for the L1 ERC20. */
    getTokenManager() {
        return this.tokenManager;
    }
    /**
     * Bridges fee juice from L1 to L2 publicly. Handles L1 ERC20 approvals. Returns once the tx has been mined.
     * @param to - Address to send the tokens to on L2.
     * @param amount - Amount of tokens to send.
     * @param mint - Whether to mint the tokens before sending (only during testing).
     */
    async bridgeTokensPublic(to, amount, mint = false) {
        const [claimSecret, claimSecretHash] = generateClaimSecret();
        if (mint) {
            await this.tokenManager.mint(amount, this.walletClient.account.address);
        }
        await this.tokenManager.approve(amount, this.contract.address, 'FeeJuice Portal');
        this.logger.info('Sending L1 Fee Juice to L2 to be claimed publicly');
        const args = [to.toString(), amount, claimSecretHash.toString()];
        await this.contract.simulate.depositToAztecPublic(args);
        const txReceipt = await this.publicClient.waitForTransactionReceipt({
            hash: await this.contract.write.depositToAztecPublic(args),
        });
        const log = extractEvent(txReceipt.logs, this.contract.address, this.contract.abi, 'DepositToAztecPublic', log => log.args.secretHash === claimSecretHash.toString() &&
            log.args.amount === amount &&
            log.args.to === to.toString(), this.logger);
        return {
            claimAmount: new Fr(amount),
            claimSecret,
            claimSecretHash,
            messageHash: log.args.key,
            messageLeafIndex: log.args.index,
        };
    }
    /**
     * Creates a new instance
     * @param pxe - PXE client used for retrieving the L1 contract addresses.
     * @param publicClient - L1 public client.
     * @param walletClient - L1 wallet client.
     * @param logger - Logger.
     */
    static async new(pxe, publicClient, walletClient, logger) {
        const { l1ContractAddresses: { feeJuiceAddress, feeJuicePortalAddress }, } = await pxe.getNodeInfo();
        if (feeJuiceAddress.isZero() || feeJuicePortalAddress.isZero()) {
            throw new Error('Portal or token not deployed on L1');
        }
        return new L1FeeJuicePortalManager(feeJuicePortalAddress, feeJuiceAddress, publicClient, walletClient, logger);
    }
}
/** Helper for interacting with a test TokenPortal on L1 for sending tokens to L2. */
export class L1ToL2TokenPortalManager {
    constructor(portalAddress, tokenAddress, publicClient, walletClient, logger) {
        this.publicClient = publicClient;
        this.walletClient = walletClient;
        this.logger = logger;
        this.tokenManager = new L1TokenManager(tokenAddress, publicClient, walletClient, logger);
        this.portal = getContract({
            address: portalAddress.toString(),
            abi: TokenPortalAbi,
            client: this.walletClient,
        });
    }
    /** Returns the token manager for the underlying L1 token. */
    getTokenManager() {
        return this.tokenManager;
    }
    /**
     * Bridges tokens from L1 to L2. Handles token approvals. Returns once the tx has been mined.
     * @param to - Address to send the tokens to on L2.
     * @param amount - Amount of tokens to send.
     * @param mint - Whether to mint the tokens before sending (only during testing).
     */
    async bridgeTokensPublic(to, amount, mint = false) {
        const [claimSecret, claimSecretHash] = await this.bridgeSetup(amount, mint);
        this.logger.info('Sending L1 tokens to L2 to be claimed publicly');
        const { request } = await this.portal.simulate.depositToAztecPublic([
            to.toString(),
            amount,
            claimSecretHash.toString(),
        ]);
        const txReceipt = await this.publicClient.waitForTransactionReceipt({
            hash: await this.walletClient.writeContract(request),
        });
        const log = extractEvent(txReceipt.logs, this.portal.address, this.portal.abi, 'DepositToAztecPublic', log => log.args.secretHash === claimSecretHash.toString() &&
            log.args.amount === amount &&
            log.args.to === to.toString(), this.logger);
        return {
            claimAmount: new Fr(amount),
            claimSecret,
            claimSecretHash,
            messageHash: log.args.key,
            messageLeafIndex: log.args.index,
        };
    }
    /**
     * Bridges tokens from L1 to L2 privately. Handles token approvals. Returns once the tx has been mined.
     * @param to - Address to send the tokens to on L2.
     * @param amount - Amount of tokens to send.
     * @param mint - Whether to mint the tokens before sending (only during testing).
     */
    async bridgeTokensPrivate(to, amount, mint = false) {
        const [claimSecret, claimSecretHash] = await this.bridgeSetup(amount, mint);
        this.logger.info('Sending L1 tokens to L2 to be claimed privately');
        const { request } = await this.portal.simulate.depositToAztecPrivate([amount, claimSecretHash.toString()]);
        const txReceipt = await this.publicClient.waitForTransactionReceipt({
            hash: await this.walletClient.writeContract(request),
        });
        const log = extractEvent(txReceipt.logs, this.portal.address, this.portal.abi, 'DepositToAztecPrivate', log => log.args.amount === amount && log.args.secretHashForL2MessageConsumption === claimSecretHash.toString(), this.logger);
        this.logger.info(`Claim message secret: ${claimSecret.toString()}, claim message secret hash: ${claimSecretHash.toString()}`);
        return {
            claimAmount: new Fr(amount),
            claimSecret,
            claimSecretHash,
            recipient: to,
            messageHash: log.args.key,
            messageLeafIndex: log.args.index,
        };
    }
    async bridgeSetup(amount, mint) {
        if (mint) {
            await this.tokenManager.mint(amount, this.walletClient.account.address);
        }
        await this.tokenManager.approve(amount, this.portal.address, 'TokenPortal');
        return generateClaimSecret();
    }
}
/** Helper for interacting with a test TokenPortal on L1 for both withdrawing from and bridging to L2. */
export class L1TokenPortalManager extends L1ToL2TokenPortalManager {
    constructor(portalAddress, tokenAddress, outboxAddress, publicClient, walletClient, logger) {
        super(portalAddress, tokenAddress, publicClient, walletClient, logger);
        this.outbox = getContract({
            address: outboxAddress.toString(),
            abi: OutboxAbi,
            client: walletClient,
        });
    }
    /**
     * Withdraws funds from the portal by consuming an L2 to L1 message. Returns once the tx is mined on L1.
     * @param amount - Amount to withdraw.
     * @param recipient - Who will receive the funds.
     * @param blockNumber - L2 block number of the message.
     * @param messageIndex - Index of the message.
     * @param siblingPath - Sibling path of the message.
     */
    async withdrawFunds(amount, recipient, blockNumber, messageIndex, siblingPath) {
        this.logger.info(`Sending L1 tx to consume message at block ${blockNumber} index ${messageIndex} to withdraw ${amount}`);
        const isConsumedBefore = await this.outbox.read.hasMessageBeenConsumedAtBlockAndIndex([blockNumber, messageIndex]);
        if (isConsumedBefore) {
            throw new Error(`L1 to L2 message at block ${blockNumber} index ${messageIndex} has already been consumed`);
        }
        // Call function on L1 contract to consume the message
        const { request: withdrawRequest } = await this.portal.simulate.withdraw([
            recipient.toString(),
            amount,
            false,
            BigInt(blockNumber),
            messageIndex,
            siblingPath.toBufferArray().map((buf) => `0x${buf.toString('hex')}`),
        ]);
        await this.publicClient.waitForTransactionReceipt({ hash: await this.walletClient.writeContract(withdrawRequest) });
        const isConsumedAfter = await this.outbox.read.hasMessageBeenConsumedAtBlockAndIndex([blockNumber, messageIndex]);
        if (!isConsumedAfter) {
            throw new Error(`L1 to L2 message at block ${blockNumber} index ${messageIndex} not consumed after withdrawal`);
        }
    }
    /**
     * Computes the L2 to L1 message leaf for the given parameters.
     * @param amount - Amount to bridge.
     * @param recipient - Recipient on L1.
     * @param l2Bridge - Address of the L2 bridge.
     * @param callerOnL1 - Caller address on L1.
     */
    getL2ToL1MessageLeaf(amount, recipient, l2Bridge, callerOnL1 = EthAddress.ZERO) {
        const content = sha256ToField([
            Buffer.from(toFunctionSelector('withdraw(address,uint256,address)').substring(2), 'hex'),
            recipient.toBuffer32(),
            new Fr(amount).toBuffer(),
            callerOnL1.toBuffer32(),
        ]);
        const leaf = sha256ToField([
            l2Bridge.toBuffer(),
            new Fr(1).toBuffer(), // aztec version
            EthAddress.fromString(this.portal.address).toBuffer32() ?? Buffer.alloc(32, 0),
            new Fr(this.publicClient.chain.id).toBuffer(), // chain id
            content.toBuffer(),
        ]);
        return leaf;
    }
}
//# sourceMappingURL=data:application/json;base64, 