"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateMaciStateFromContract = void 0;
/* eslint-disable no-underscore-dangle */
const core_1 = require("@maci-protocol/core");
const domainobjs_1 = require("@maci-protocol/domainobjs");
const assert_1 = __importDefault(require("assert"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const typechain_types_1 = require("../typechain-types");
const ipfs_1 = require("./ipfs");
const utils_1 = require("./utils");
/**
 * Generate a MaciState object from the events of a MACI and Poll smart contracts
 * @param provider - the ethereum provider
 * @param address - the address of the MACI contract
 * @param coordinatorKeypair - the keypair of the coordinator
 * @param pollId - the id of the poll for which we are fetching events
 * @param fromBlock - the block number from which to start fetching events
 * @param blocksPerRequest - the number of blocks to fetch in each request
 * @param endBlock - the block number at which to stop fetching events
 * @param sleepAmount - the amount of time to sleep between each request
 * @param ipfsMessageBackupFiles - backup files for ipfs messages
 * @param logsOutputPath - the file path where to save the logs for debugging and auditing purposes
 * @returns an instance of MaciState
 */
const generateMaciStateFromContract = async ({ provider, address, coordinatorKeypair, pollId, fromBlock = 0, blocksPerRequest = 50, endBlock, sleepAmount, ipfsMessageBackupFiles = [], logsOutputPath, }) => {
    // ensure the pollId is valid
    (0, assert_1.default)(pollId >= 0);
    const ipfsService = ipfs_1.IpfsService.getInstance();
    const maciContract = typechain_types_1.MACI__factory.connect(address, provider);
    // Check stateTreeDepth
    const stateTreeDepth = await maciContract.stateTreeDepth();
    // we need to pass the stateTreeDepth
    const maciState = new core_1.MaciState(Number(stateTreeDepth));
    // ensure it is set correctly
    (0, assert_1.default)(stateTreeDepth === BigInt(maciState.stateTreeDepth));
    // if no last block is set then we fetch until the current block number
    const lastBlock = endBlock || (await provider.getBlockNumber());
    const actions = [];
    const foundPollIds = new Set();
    const pollContractAddresses = new Map();
    const backupMessagesByIpfsHash = await extractBackupIpfsMessages(ipfsMessageBackupFiles);
    // Fetch event logs in batches (lastBlock inclusive)
    for (let i = fromBlock; i <= lastBlock; i += blocksPerRequest + 1) {
        // the last block batch will be either current iteration block + blockPerRequest
        // or the end block if it is set
        const toBlock = i + blocksPerRequest >= lastBlock ? lastBlock : i + blocksPerRequest;
        const [signUpLogs, deployPollLogs] = 
        // eslint-disable-next-line no-await-in-loop
        await Promise.all([
            maciContract.queryFilter(maciContract.filters.SignUp(), i, toBlock),
            maciContract.queryFilter(maciContract.filters.DeployPoll(), i, toBlock),
        ]);
        signUpLogs.forEach((event) => {
            (0, assert_1.default)(!!event);
            actions.push({
                type: "SignUp",
                blockNumber: event.blockNumber,
                transactionIndex: event.transactionIndex,
                data: {
                    stateIndex: Number(event.args._stateIndex),
                    publicKey: new domainobjs_1.PublicKey([BigInt(event.args._userPublicKeyX), BigInt(event.args._userPublicKeyY)]),
                    timestamp: Number(event.args._timestamp),
                },
            });
        });
        // eslint-disable-next-line @typescript-eslint/prefer-for-of
        for (let j = 0; j < deployPollLogs.length; j += 1) {
            const event = deployPollLogs[j];
            (0, assert_1.default)(!!event);
            const id = event.args._pollId;
            const pollMode = Number(event.args._mode);
            const publicKey = new domainobjs_1.PublicKey([
                BigInt(event.args._coordinatorPublicKeyX),
                BigInt(event.args._coordinatorPublicKeyY),
            ]);
            // eslint-disable-next-line no-await-in-loop
            const pollContracts = await maciContract.getPoll(id);
            actions.push({
                type: "DeployPoll",
                blockNumber: event.blockNumber,
                transactionIndex: event.transactionIndex,
                data: { pollId: id, pollAddresses: pollContracts.poll, publicKey, pollMode },
            });
            foundPollIds.add(Number(id));
            pollContractAddresses.set(BigInt(id), pollContracts.poll);
        }
        if (sleepAmount) {
            // eslint-disable-next-line no-await-in-loop
            await (0, utils_1.sleep)(sleepAmount);
        }
    }
    // Check whether each pollId exists
    (0, assert_1.default)(foundPollIds.has(Number(pollId)), "Error: the specified pollId does not exist on-chain");
    const pollContractAddress = pollContractAddresses.get(pollId);
    const pollContract = typechain_types_1.Poll__factory.connect(pollContractAddress, provider);
    const [coordinatorPublicKeyOnChain, pollEndTimestamp, onChainTreeDepths, messageBatchSizeOnChain] = await Promise.all([
        pollContract.coordinatorPublicKey(),
        pollContract.endDate(),
        pollContract.treeDepths(),
        pollContract.messageBatchSize(),
    ]);
    (0, assert_1.default)(coordinatorPublicKeyOnChain[0].toString() === coordinatorKeypair.publicKey.raw[0].toString());
    (0, assert_1.default)(coordinatorPublicKeyOnChain[1].toString() === coordinatorKeypair.publicKey.raw[1].toString());
    const treeDepths = {
        tallyProcessingStateTreeDepth: Number(onChainTreeDepths.tallyProcessingStateTreeDepth),
        voteOptionTreeDepth: Number(onChainTreeDepths.voteOptionTreeDepth),
        stateTreeDepth: Number(onChainTreeDepths.stateTreeDepth),
    };
    const messageBatchSize = Number(messageBatchSizeOnChain);
    // fetch poll contract logs
    for (let i = fromBlock; i <= lastBlock; i += blocksPerRequest + 1) {
        const toBlock = i + blocksPerRequest >= lastBlock ? lastBlock : i + blocksPerRequest;
        // eslint-disable-next-line no-await-in-loop
        const [publishMessageLogs, joinPollLogs, ipfsHashAddedLogs] = await Promise.all([
            pollContract.queryFilter(pollContract.filters.PublishMessage(), i, toBlock),
            pollContract.queryFilter(pollContract.filters.PollJoined(), i, toBlock),
            pollContract.queryFilter(pollContract.filters.IpfsHashAdded(), i, toBlock),
        ]);
        joinPollLogs.forEach((event) => {
            (0, assert_1.default)(!!event);
            const nullifier = BigInt(event.args._nullifier);
            const publicKeyX = BigInt(event.args._pollPublicKeyX);
            const publicKeyY = BigInt(event.args._pollPublicKeyY);
            const voiceCreditBalance = BigInt(event.args._voiceCreditBalance);
            actions.push({
                type: "PollJoined",
                blockNumber: event.blockNumber,
                transactionIndex: event.transactionIndex,
                data: {
                    publicKey: new domainobjs_1.PublicKey([publicKeyX, publicKeyY]),
                    newVoiceCreditBalance: voiceCreditBalance,
                    nullifier,
                },
            });
        });
        // eslint-disable-next-line no-await-in-loop
        const ipfsMessages = await Promise.all(ipfsHashAddedLogs.map(async (event) => {
            (0, assert_1.default)(!!event);
            return ipfsService
                .read(event.args._ipfsHash)
                .then((data) => {
                const messages = data && data.length > 0 ? data : backupMessagesByIpfsHash.get(event.args._ipfsHash);
                if (!messages || messages.length === 0) {
                    throw new Error(`invalid json for cid ${event.args._ipfsHash}`);
                }
                return messages;
            })
                .then((messages) => ({
                data: messages.map((value) => ({
                    message: new domainobjs_1.Message(value.data.map(BigInt)),
                    encryptionPublicKey: new domainobjs_1.PublicKey([BigInt(value.publicKey[0]), BigInt(value.publicKey[1])]),
                })),
                blockNumber: event.blockNumber,
                transactionIndex: event.transactionIndex,
            }));
        }));
        ipfsHashAddedLogs.forEach((event) => {
            (0, assert_1.default)(!!event);
            const ipfsHash = event.args._ipfsHash;
            actions.push({
                type: "IpfsHashAdded",
                blockNumber: event.blockNumber,
                transactionIndex: event.transactionIndex,
                data: {
                    ipfsHash,
                },
            });
        });
        ipfsMessages.forEach(({ data, blockNumber, transactionIndex }) => {
            data.forEach(({ message, encryptionPublicKey }) => {
                actions.push({
                    type: "PublishMessage",
                    blockNumber,
                    transactionIndex,
                    data: {
                        message,
                        encryptionPublicKey,
                    },
                });
            });
        });
        publishMessageLogs.forEach((event) => {
            (0, assert_1.default)(!!event);
            const message = new domainobjs_1.Message(event.args._message[0].map((x) => BigInt(x)));
            const encryptionPublicKey = new domainobjs_1.PublicKey(event.args._encryptionPublicKey.map((x) => BigInt(x.toString())));
            actions.push({
                type: "PublishMessage",
                blockNumber: event.blockNumber,
                transactionIndex: event.transactionIndex,
                data: {
                    message,
                    encryptionPublicKey,
                },
            });
        });
        if (sleepAmount) {
            // eslint-disable-next-line no-await-in-loop
            await (0, utils_1.sleep)(sleepAmount);
        }
    }
    const voteOptions = await pollContract.voteOptions();
    // Reconstruct MaciState in order
    (0, utils_1.sortActions)(actions).forEach((action) => {
        switch (true) {
            case action.type === "SignUp": {
                const { publicKey } = action.data;
                maciState.signUp(publicKey);
                break;
            }
            case action.type === "DeployPoll" && action.data.pollId?.toString() === pollId.toString(): {
                maciState.deployPoll(pollEndTimestamp, treeDepths, messageBatchSize, coordinatorKeypair, voteOptions, action.data.pollMode);
                break;
            }
            case action.type === "DeployPoll" && action.data.pollId?.toString() !== pollId.toString(): {
                maciState.deployNullPoll();
                break;
            }
            case action.type === "PublishMessage": {
                const { encryptionPublicKey, message } = action.data;
                maciState.polls.get(pollId)?.publishMessage(message, encryptionPublicKey);
                break;
            }
            case action.type === "PollJoined": {
                const { publicKey, newVoiceCreditBalance, nullifier } = action.data;
                maciState.polls.get(pollId)?.joinPoll(nullifier, publicKey, newVoiceCreditBalance);
                break;
            }
            default:
                break;
        }
    });
    // Set totalSignups
    const totalSignupsAndMessages = await pollContract.totalSignupsAndMessages();
    const poll = maciState.polls.get(pollId);
    // ensure all messages were recorded
    (0, assert_1.default)(Number(totalSignupsAndMessages[1]) === poll?.messages.length);
    // set the number of signups
    poll.updatePoll(totalSignupsAndMessages[0]);
    maciState.polls.set(pollId, poll);
    // Save logs if output path is provided
    if (logsOutputPath) {
        const logsOutputDirectory = path_1.default.resolve(logsOutputPath);
        if (!fs_1.default.existsSync(logsOutputDirectory)) {
            await fs_1.default.promises.mkdir(logsOutputDirectory, { recursive: true });
        }
        const logs = {
            maciAddress: address,
            pollId: pollId.toString(),
            fromBlock,
            toBlock: lastBlock,
            timestamp: new Date().toISOString(),
            actions,
        };
        await fs_1.default.promises.writeFile(logsOutputPath, JSON.stringify(logs, null, 2));
    }
    return maciState;
};
exports.generateMaciStateFromContract = generateMaciStateFromContract;
async function extractBackupIpfsMessages(ipfsMessageBackupFiles) {
    try {
        const data = await Promise.all(ipfsMessageBackupFiles.map((file) => fs_1.default.promises
            .readFile(file, "utf-8")
            .then((res) => JSON.parse(res))
            .then((messages) => ({ ipfsHash: path_1.default.parse(file).base.replace(".json", ""), messages }))));
        return data.reduce((acc, { ipfsHash, messages }) => {
            acc.set(ipfsHash, messages);
            return acc;
        }, new Map());
    }
    catch (error) {
        return new Map();
    }
}
//# sourceMappingURL=generateMaciState.js.map