"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ethers_1 = require("ethers");
const config_1 = require("./config");
const eip712_1 = require("./eip712");
const ipfs_1 = require("./ipfs");
const blockscout_1 = require("./blockscout");
const usdEnrich_1 = require("./usdEnrich");
const scanState_1 = require("./scanState");
const checkpoint_core_1 = require("@dbis/checkpoint-core");
const leafCodec_1 = require("./leafCodec");
const adminIngress_1 = require("./ingress/adminIngress");
const dualMirror_1 = require("./dualMirror");
const activityRegistry_1 = require("./activityRegistry");
const activityRegistryV2_1 = require("./activityRegistryV2");
const iso20022Enrich_1 = require("./iso20022Enrich");
const iso20022LocalStore_1 = require("./iso20022LocalStore");
const participantSurface_1 = require("./participantSurface");
const receiptsRoot_1 = require("./receiptsRoot");
(0, config_1.assertAggregatorConfig)();
const BLOCK_ORACLE_ABI = ['function setBlockHeader(uint256,bytes32,bytes32) external'];
class CheckpointAggregator {
    chain138;
    mainnetWallet;
    checkpoint;
    ingress;
    blockOracle;
    buffer = [];
    nextBatchId = 1n;
    hubImplVersion = 3n;
    flushTimer;
    scanning = false;
    includedTxCache = new Map();
    constructor() {
        const mainnet = new ethers_1.ethers.JsonRpcProvider(config_1.checkpointAggregatorConfig.mainnetRpc);
        this.mainnetWallet = new ethers_1.ethers.Wallet(process.env.PRIVATE_KEY, mainnet);
        this.checkpoint = (0, adminIngress_1.buildCheckpointContract)(this.mainnetWallet, config_1.checkpointAggregatorConfig.checkpointProxy, config_1.checkpointAggregatorConfig.submitMode);
        this.ingress = new adminIngress_1.AdminCheckpointIngress(this.checkpoint, config_1.checkpointAggregatorConfig.submitMode);
        this.chain138 = new ethers_1.ethers.JsonRpcProvider(config_1.checkpointAggregatorConfig.chain138Rpc);
        if (config_1.checkpointAggregatorConfig.blockOracleExtension) {
            this.blockOracle = new ethers_1.ethers.Contract(config_1.checkpointAggregatorConfig.blockOracleExtension, BLOCK_ORACLE_ABI, this.mainnetWallet);
        }
    }
    async start() {
        await this.syncBatchId();
        const state = (0, scanState_1.loadScanState)(config_1.checkpointAggregatorConfig.scanStatePath, config_1.checkpointAggregatorConfig.scanFromBlock);
        console.log(`Checkpoint aggregator mode=${config_1.checkpointAggregatorConfig.submitMode} batch=${config_1.checkpointAggregatorConfig.batchSize} nextBatchId=${this.nextBatchId} impl=${this.hubImplVersion} blockscout=${config_1.checkpointAggregatorConfig.useBlockscout} emitter=${config_1.checkpointAggregatorConfig.batchEmitter138 || 'n/a'}`);
        this.schedulePartialFlush();
        await this.runScanLoop(state);
    }
    async syncBatchId() {
        const latest = await this.checkpoint.getLatestBatchId();
        this.nextBatchId = latest > 0n ? BigInt(latest) + 1n : 1n;
        try {
            this.hubImplVersion = await this.checkpoint.IMPLEMENTATION_VERSION();
        }
        catch {
            this.hubImplVersion = 3n;
        }
    }
    schedulePartialFlush() {
        if (this.flushTimer)
            clearTimeout(this.flushTimer);
        this.flushTimer = setTimeout(() => {
            void this.flush(true).catch((e) => console.error('partial flush failed', e));
        }, config_1.checkpointAggregatorConfig.maxWaitMs);
    }
    async runScanLoop(initialState) {
        const state = { ...initialState };
        for (;;) {
            try {
                await this.scanCatchUp(state);
            }
            catch (e) {
                console.error('scanCatchUp', e);
            }
            await new Promise((r) => setTimeout(r, config_1.checkpointAggregatorConfig.scanPollMs));
        }
    }
    async scanCatchUp(state) {
        if (this.scanning)
            return;
        this.scanning = true;
        try {
            const head = await this.chain138.getBlockNumber();
            const safeHead = head - config_1.checkpointAggregatorConfig.confirmationBlocks;
            let from = state.lastScannedBlock + 1;
            while (from <= safeHead) {
                const to = Math.min(from + config_1.checkpointAggregatorConfig.scanChunkBlocks - 1, safeHead);
                let added = 0;
                if (config_1.checkpointAggregatorConfig.useBlockscout) {
                    added = await this.scanBlockscoutRange(from, to);
                }
                else {
                    for (let b = from; b <= to; b++)
                        added += await this.scanBlockRpc(b);
                }
                state.lastScannedBlock = to;
                (0, scanState_1.saveScanState)(config_1.checkpointAggregatorConfig.scanStatePath, state);
                console.log(`Scanned blocks ${from}-${to} (+${added} txs) buffer=${this.buffer.length}`);
                from = to + 1;
            }
        }
        finally {
            this.scanning = false;
        }
    }
    async scanBlockscoutRange(from, to) {
        const txs = await (0, blockscout_1.fetchTransactionsInBlockRange)(config_1.checkpointAggregatorConfig.blockscoutApi, from, to);
        let added = 0;
        for (const tx of txs) {
            if (await this.txAlreadyIncluded(tx.hash))
                continue;
            const receipt = await this.chain138.getTransactionReceipt(tx.hash);
            const leaf = {
                txHash: tx.hash,
                from: tx.from,
                to: tx.to,
                value: tx.value,
                blockNumber: tx.blockNumber,
                blockTimestamp: tx.blockTimestamp,
                gasUsed: receipt?.gasUsed ?? 0n,
                success: receipt?.status === 1,
            };
            await this.enrichLeafFromBlockscout(leaf);
            if ((0, leafCodec_1.effectiveValue)(leaf) < config_1.checkpointAggregatorConfig.minValueWei)
                continue;
            this.buffer.push(leaf);
            added++;
            if (this.buffer.length >= config_1.checkpointAggregatorConfig.batchSize)
                await this.flush(false);
        }
        return added;
    }
    async scanBlockRpc(blockNumber) {
        const block = await this.chain138.getBlock(blockNumber, true);
        if (!block)
            return 0;
        let added = 0;
        const entries = block.prefetchedTransactions?.length
            ? block.prefetchedTransactions
            : block.transactions ?? [];
        for (const entry of entries) {
            const tx = typeof entry === 'string' ? await this.chain138.getTransaction(entry) : entry;
            if (!tx || typeof tx === 'string')
                continue;
            const value = tx.value ?? 0n;
            if (await this.txAlreadyIncluded(tx.hash))
                continue;
            const receipt = await this.chain138.getTransactionReceipt(tx.hash);
            const leaf = {
                txHash: tx.hash,
                from: tx.from,
                to: tx.to ?? ethers_1.ethers.ZeroAddress,
                value,
                blockNumber,
                blockTimestamp: block.timestamp,
                gasUsed: receipt?.gasUsed ?? 0n,
                success: receipt?.status === 1,
            };
            await this.enrichLeafFromBlockscout(leaf);
            if ((0, leafCodec_1.effectiveValue)(leaf) < config_1.checkpointAggregatorConfig.minValueWei)
                continue;
            this.buffer.push(leaf);
            added++;
            if (this.buffer.length >= config_1.checkpointAggregatorConfig.batchSize)
                await this.flush(false);
        }
        return added;
    }
    async enrichLeafFromBlockscout(leaf) {
        const { allErc20 } = await (0, usdEnrich_1.enrichLeafFromBlockscoutApi)(leaf, config_1.checkpointAggregatorConfig.blockscoutApi, config_1.checkpointAggregatorConfig.useBlockscout);
        await (0, usdEnrich_1.enrichLeafUsd)(leaf, {
            apiBaseUrl: config_1.checkpointAggregatorConfig.tokenAggregationApiUrl,
            chainId: 138,
            enabled: config_1.checkpointAggregatorConfig.usdEnrichEnabled,
            requestDelayMs: config_1.checkpointAggregatorConfig.usdRequestDelayMs,
            blockscoutApi: config_1.checkpointAggregatorConfig.blockscoutApi,
            useBlockscout: config_1.checkpointAggregatorConfig.useBlockscout,
        }, allErc20);
    }
    async txAlreadyIncluded(txHash) {
        const cached = this.includedTxCache.get(txHash);
        if (cached !== undefined)
            return cached;
        try {
            const [included] = await this.checkpoint.isTxIncluded(txHash);
            this.includedTxCache.set(txHash, included);
            return included;
        }
        catch {
            return false;
        }
    }
    async flush(partial) {
        if (this.buffer.length === 0)
            return;
        const leaves = this.buffer.splice(0, config_1.checkpointAggregatorConfig.batchSize);
        if (config_1.checkpointAggregatorConfig.attachReceiptMeta) {
            await (0, receiptsRoot_1.attachReceiptMeta)(this.chain138, leaves);
        }
        else {
            for (const leaf of leaves) {
                const meta = await (0, receiptsRoot_1.fetchReceiptMeta)(this.chain138, leaf.txHash);
                leaf.logCount = meta.logCount;
                leaf.receiptHash = meta.receiptHash;
            }
        }
        (0, iso20022Enrich_1.attachIso20022ToLeaves)(leaves);
        const root = (0, leafCodec_1.paymentsRoot)(138, leaves);
        const receiptMetas = leaves.map((l) => ({
            logCount: l.logCount ?? 0,
            receiptHash: l.receiptHash ?? ethers_1.ethers.ZeroHash,
        }));
        const receiptsRootHash = (0, receiptsRoot_1.receiptsRoot)(leaves.map((l) => l.txHash), receiptMetas);
        const checkpointBlock = Math.max(...leaves.map((l) => l.blockNumber));
        const block = await this.chain138.getBlock(checkpointBlock);
        const blockHash = block?.hash ?? ethers_1.ethers.ZeroHash;
        const stateRoot = block?.stateRoot ?? ethers_1.ethers.ZeroHash;
        if (config_1.checkpointAggregatorConfig.updateBlockOracle && this.blockOracle) {
            const tx = await this.blockOracle.setBlockHeader(checkpointBlock, blockHash, stateRoot);
            await tx.wait();
        }
        const header = {
            batchId: this.nextBatchId,
            previousBatchId: this.nextBatchId > 1n ? this.nextBatchId - 1n : 0n,
            chainId: 138n,
            checkpointBlock: BigInt(checkpointBlock),
            startBlock: BigInt(Math.min(...leaves.map((l) => l.blockNumber))),
            endBlock: BigInt(checkpointBlock),
            blockHash,
            stateRoot,
            paymentsRoot: root,
            receiptsRoot: receiptsRootHash,
            txCount: partial && leaves.length < config_1.checkpointAggregatorConfig.batchSize ? leaves.length : config_1.checkpointAggregatorConfig.batchSize,
            flags: partial && leaves.length < config_1.checkpointAggregatorConfig.batchSize ? 1 : 0,
            submittedAt: 0n,
            submitter: ethers_1.ethers.ZeroAddress,
            contentURI: ethers_1.ethers.ZeroHash,
        };
        const payload = {
            batchId: this.nextBatchId.toString(),
            chainId: 138,
            checkpointBlock,
            paymentsRoot: root,
            leaves: leaves.map((l) => ({
                txHash: l.txHash,
                from: l.from,
                to: l.to,
                value: l.value.toString(),
                nativeValueWei: l.value.toString(),
                onChainValueWei: (0, leafCodec_1.effectiveValue)(l).toString(),
                blockNumber: l.blockNumber,
                blockTimestamp: l.blockTimestamp,
                gasUsed: l.gasUsed.toString(),
                success: l.success,
                ...(l.token
                    ? {
                        token: l.token,
                        tokenSymbol: l.tokenSymbol,
                        tokenDecimals: l.tokenDecimals,
                        tokenValue: l.tokenValue?.toString(),
                        tokenLogIndex: l.tokenLogIndex,
                    }
                    : {}),
                ...(l.valueUsd ? { valueUsd: l.valueUsd } : {}),
                ...(l.nativeValueUsd ? { nativeValueUsd: l.nativeValueUsd } : {}),
                ...(l.tokenValueUsd ? { tokenValueUsd: l.tokenValueUsd } : {}),
                ...(l.nativePriceUsd != null ? { nativePriceUsd: l.nativePriceUsd } : {}),
                ...(l.tokenPriceUsd != null ? { tokenPriceUsd: l.tokenPriceUsd } : {}),
                ...(l.priceSource ? { priceSource: l.priceSource } : {}),
                ...(l.priceEffectiveTimestamp ? { priceEffectiveTimestamp: l.priceEffectiveTimestamp } : {}),
                ...(l.totalTransfersUsd ? { totalTransfersUsd: l.totalTransfersUsd } : {}),
                ...(l.transfers?.length ? { transfers: l.transfers } : {}),
                ...(l.logCount != null ? { logCount: l.logCount } : {}),
                ...(l.receiptHash ? { receiptHash: l.receiptHash } : {}),
                ...(l.iso20022 ? { iso20022: l.iso20022 } : {}),
            })),
            submittedAt: new Date().toISOString(),
            ...(config_1.checkpointAggregatorConfig.isoEnrichEnabled
                ? { iso20022Schema: 'canonical-v1', isoMsgTypeDefault: 'chain138.synthetic' }
                : {}),
            ...(config_1.checkpointAggregatorConfig.usdEnrichEnabled
                ? {
                    batchTotalUsd: (0, checkpoint_core_1.sumUsdStrings)(leaves.map((l) => l.totalTransfersUsd ?? l.valueUsd)) ?? '0.000000',
                    usdEnrichedAt: new Date().toISOString(),
                }
                : {}),
        };
        const contentURI = await (0, ipfs_1.publishBatchPayload)(this.nextBatchId, payload);
        header.contentURI = contentURI;
        const validatorSignatures = config_1.checkpointAggregatorConfig.requireValidatorSigs
            ? await (0, eip712_1.signBatchAttestation)(this.mainnetWallet, config_1.checkpointAggregatorConfig.checkpointProxy, header)
            : '0x01';
        const txHashes = config_1.checkpointAggregatorConfig.submitMode === 'commitment' ? [] : leaves.map((l) => l.txHash);
        const extensionData = adminIngress_1.AdminCheckpointIngress.extensionDataForMode(config_1.checkpointAggregatorConfig.submitMode, leaves);
        const { hash } = await this.ingress.submit({
            header,
            leaves,
            txHashes,
            extensionData,
            contentURI,
            validatorSignatures,
        });
        const batchIdSubmitted = this.nextBatchId;
        console.log(`Submitted batch ${batchIdSubmitted} (${leaves.length} txs) mode=${config_1.checkpointAggregatorConfig.submitMode} tx=${hash}`);
        if (config_1.checkpointAggregatorConfig.dualWriteV1Mirror) {
            try {
                const mirrorTx = await (0, dualMirror_1.dualWriteV1Mirror)(this.mainnetWallet, config_1.checkpointAggregatorConfig.transactionMirrorMainnet, leaves);
                if (mirrorTx)
                    console.log(`  v1 TransactionMirror dual-write tx=${mirrorTx}`);
            }
            catch (e) {
                console.error('  v1 TransactionMirror dual-write failed', e);
            }
        }
        if (config_1.checkpointAggregatorConfig.recordAddressActivity && config_1.checkpointAggregatorConfig.addressActivityRegistry) {
            try {
                const activityTx = await (0, activityRegistry_1.recordActivityBatch)(this.mainnetWallet, config_1.checkpointAggregatorConfig.addressActivityRegistry, batchIdSubmitted, leaves);
                if (activityTx)
                    console.log(`  AddressActivityRegistry tx=${activityTx}`);
            }
            catch (e) {
                console.error('  AddressActivityRegistry record failed', e);
            }
        }
        if (config_1.checkpointAggregatorConfig.recordIsoAttestation && config_1.checkpointAggregatorConfig.addressActivityRegistryV2) {
            try {
                const isoTx = await (0, activityRegistryV2_1.recordIsoAttestationBatch)(this.mainnetWallet, config_1.checkpointAggregatorConfig.addressActivityRegistryV2, batchIdSubmitted, leaves);
                if (isoTx)
                    console.log(`  AddressActivityRegistryV2 tx=${isoTx}`);
            }
            catch (e) {
                console.error('  AddressActivityRegistryV2 record failed', e);
            }
        }
        if (config_1.checkpointAggregatorConfig.isoEnrichEnabled) {
            try {
                const stored = (0, iso20022LocalStore_1.persistIso20022ForLeaves)(leaves, batchIdSubmitted);
                if (stored.length)
                    console.log(`  ISO20022 OMNL store: ${stored.length} message(s)`);
            }
            catch (e) {
                console.error('  ISO20022 local store failed', e);
            }
        }
        if (config_1.checkpointAggregatorConfig.surfaceParticipants && config_1.checkpointAggregatorConfig.participantSurface) {
            try {
                const surfaceTx = await (0, participantSurface_1.surfaceParticipantsOnMainnet)(this.mainnetWallet, config_1.checkpointAggregatorConfig.participantSurface, batchIdSubmitted, leaves);
                if (surfaceTx)
                    console.log(`  Chain138ParticipantSurface tx=${surfaceTx}`);
            }
            catch (e) {
                console.error('  Chain138ParticipantSurface failed', e);
            }
        }
        if (config_1.checkpointAggregatorConfig.surfaceTopLevelZeroEth) {
            try {
                const n = await (0, participantSurface_1.surfaceTopLevelZeroEth)(this.mainnetWallet, leaves, batchIdSubmitted);
                if (n > 0)
                    console.log(`  Top-level 0 ETH surface txs=${n} (Etherscan Transactions tab)`);
            }
            catch (e) {
                console.error('  Top-level 0 ETH surface failed', e);
            }
        }
        this.nextBatchId += 1n;
        this.schedulePartialFlush();
    }
}
new CheckpointAggregator().start().catch((e) => {
    console.error(e);
    process.exit(1);
});
