// Create a combined `refunds` object containing refunds for V2 + V3 fills
import assert from "assert";
import { bnZero, fixedPointAdjustment, count2DDictionaryValues, count3DDictionaryValues, toAddressType, getImpliedBundleBlockRanges, isDefined, EvmAddress, } from "../../../utils";
import { addLastRunningBalance, constructPoolRebalanceLeaves, updateRunningBalance, updateRunningBalanceForDeposit, } from "./PoolRebalanceUtils";
import { buildPoolRebalanceLeafTree } from "./MerkleTreeUtils";
// and expired deposits.
export function getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3) {
    const combinedRefunds = {};
    Object.entries(bundleFillsV3).forEach(([repaymentChainId, fillsForChain]) => {
        combinedRefunds[repaymentChainId] ??= {};
        Object.entries(fillsForChain).forEach(([l2TokenAddress, { refunds }]) => {
            // refunds can be undefined if these fills were all slow fill executions.
            if (refunds === undefined) {
                return;
            }
            // @dev use shallow copy so that modifying combinedRefunds doesn't modify the original refunds object.
            const refundsShallowCopy = { ...refunds };
            if (combinedRefunds[repaymentChainId][l2TokenAddress] === undefined) {
                combinedRefunds[repaymentChainId][l2TokenAddress] = refundsShallowCopy;
            }
            else {
                // Each refunds object should have a unique refund address so we can add new ones to the
                // existing dictionary.
                combinedRefunds[repaymentChainId][l2TokenAddress] = {
                    ...combinedRefunds[repaymentChainId][l2TokenAddress],
                    ...refundsShallowCopy,
                };
            }
        });
    });
    Object.entries(expiredDepositsToRefundV3).forEach(([originChainId, depositsForChain]) => {
        combinedRefunds[originChainId] ??= {};
        Object.entries(depositsForChain).forEach(([l2TokenAddress, deposits]) => {
            deposits.forEach((deposit) => {
                if (combinedRefunds[originChainId][l2TokenAddress] === undefined) {
                    combinedRefunds[originChainId][l2TokenAddress] = { [deposit.depositor.toBytes32()]: deposit.inputAmount };
                }
                else {
                    const existingRefundAmount = combinedRefunds[originChainId][l2TokenAddress][deposit.depositor.toBytes32()];
                    combinedRefunds[originChainId][l2TokenAddress][deposit.depositor.toBytes32()] = deposit.inputAmount.add(existingRefundAmount ?? bnZero);
                }
            });
        });
    });
    return combinedRefunds;
}
export function prettyPrintV3SpokePoolEvents(bundleDepositsV3, bundleFillsV3, bundleSlowFillsV3, expiredDepositsToRefundV3, unexecutableSlowFills) {
    return {
        bundleDepositsV3: count2DDictionaryValues(bundleDepositsV3),
        bundleFillsV3: count3DDictionaryValues(bundleFillsV3, "fills"),
        bundleSlowFillsV3: count2DDictionaryValues(bundleSlowFillsV3),
        expiredDepositsToRefundV3: count2DDictionaryValues(expiredDepositsToRefundV3),
        unexecutableSlowFills: count2DDictionaryValues(unexecutableSlowFills),
    };
}
export function getEndBlockBuffers(chainIdListForBundleEvaluationBlockNumbers, blockRangeEndBlockBuffer) {
    // These buffers can be configured by the bot runner. They have two use cases:
    // 1) Validate the end blocks specified in the pending root bundle. If the end block is greater than the latest
    // block for its chain, then we should dispute the bundle because we can't look up events in the future for that
    // chain. However, there are some cases where the proposer's node for that chain is returning a higher HEAD block
    // than the bot-runner is seeing, so we can use this buffer to allow the proposer some margin of error. If
    // the bundle end block is less than HEAD but within this buffer, then we won't dispute and we'll just exit
    // early from this function.
    // 2) Subtract from the latest block in a new root bundle proposal. This can be used to reduce the chance that
    // bot runs using different providers see different contract state close to the HEAD block for a chain.
    // Reducing the latest block that we query also gives partially filled deposits slightly more buffer for relayers
    // to fully fill the deposit and reduces the chance that the data worker includes a slow fill payment that gets
    // filled during the challenge period.
    return chainIdListForBundleEvaluationBlockNumbers.map((chainId) => blockRangeEndBlockBuffer[chainId] ?? 0);
}
/*
 * @notice Constructs a new pool rebalance root given the input bundle data.
 * @dev It is assumed that the input bundle data corresponds to the block ranges of the input mainnetBundleEndBlock.
 * If the mainnetBundleEndBlock does not correspond to any historical or pending root bundle, then the output pool rebalance
 * root will be constructed under the assumption that the pending root bundle is valid and passes liveness.
 * @param latestMainnetBlock The latest mainnet block number.
 * @param mainnetBundleEndBlock The end block number of the block range corresponding to the bundle data.
 * @param bundleV3Deposits Deposit bundle data for the implied block range given by the mainnetBundleEndBlock.
 * @param bundleFillsV3 Fill bundle data.
 * @param bundleSlowFillsV3 Slow fill bundle data.
 * @param unexecutableSlowFills Expired slow fill bundle data.
 * @param expiredDepositsToRefundV3 Expired deposit bundle data.
 * @param clients Clients required to construct a new pool rebalance root.
 * @maxL1TokenCountOverride Optional parameter to cap the number of tokens in a single pool rebalance leaf.
 */
export async function _buildPoolRebalanceRoot(latestMainnetBlock, mainnetBundleEndBlock, bundleV3Deposits, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, expiredDepositsToRefundV3, clients, maxL1TokenCountOverride) {
    // If there is a pending proposal and the mainnet bundle end block is greater than the pending proposal's mainnet end block, then this pool rebalance root is being built during the liveness
    // of a different root bundle, so running balance calculations will be slightly different.
    if (clients.hubPoolClient.hasPendingProposal() &&
        clients.hubPoolClient.getPendingRootBundle().bundleEvaluationBlockNumbers[0] < mainnetBundleEndBlock) {
        return await _buildOptimisticPoolRebalanceRoot(latestMainnetBlock, mainnetBundleEndBlock, bundleV3Deposits, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, expiredDepositsToRefundV3, clients, maxL1TokenCountOverride);
    }
    // Otherwise, we can synchronously reconstruct a historical pool rebalance root from the input data.
    return _buildHistoricalPoolRebalanceRoot(latestMainnetBlock, mainnetBundleEndBlock, bundleV3Deposits, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, expiredDepositsToRefundV3, clients, maxL1TokenCountOverride);
}
/*
 * @notice Constructs a new pool rebalance root given historical bundle data.
 * @param latestMainnetBlock The latest mainnet block number.
 * @param mainnetBundleEndBlock The end block number of the block range corresponding to the bundle data.
 * @param bundleV3Deposits Deposit bundle data for the implied block range given by the mainnetBundleEndBlock.
 * @param bundleFillsV3 Fill bundle data.
 * @param bundleSlowFillsV3 Slow fill bundle data.
 * @param unexecutableSlowFills Expired slow fill bundle data.
 * @param expiredDepositsToRefundV3 Expired deposit bundle data.
 * @param clients Clients required to construct a new pool rebalance root.
 * @maxL1TokenCountOverride Optional parameter to cap the number of tokens in a single pool rebalance leaf.
 */
export function _buildHistoricalPoolRebalanceRoot(latestMainnetBlock, mainnetBundleEndBlock, bundleV3Deposits, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, expiredDepositsToRefundV3, clients, maxL1TokenCountOverride) {
    const { runningBalances, realizedLpFees, chainWithRefundsOnly } = _getMarginalRunningBalances(mainnetBundleEndBlock, bundleV3Deposits, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, expiredDepositsToRefundV3, clients);
    addLastRunningBalance(latestMainnetBlock, runningBalances, clients.hubPoolClient);
    const leaves = constructPoolRebalanceLeaves(mainnetBundleEndBlock, runningBalances, realizedLpFees, Array.from(chainWithRefundsOnly).filter((chainId) => !Object.keys(runningBalances).includes(chainId.toString())), clients.configStoreClient, maxL1TokenCountOverride);
    return {
        runningBalances,
        realizedLpFees,
        leaves,
        tree: buildPoolRebalanceLeafTree(leaves),
    };
}
/*
 * @notice Constructs a new pool rebalance root given the input bundle data. This function assumes there is a pending root bundle which will eventually clear liveness.
 * @param latestMainnetBlock The latest mainnet block number.
 * @param mainnetBundleEndBlock The end block number of the block range corresponding to the bundle data.
 * @param bundleV3Deposits Deposit bundle data for the implied block range given by the mainnetBundleEndBlock.
 * @param bundleFillsV3 Fill bundle data.
 * @param bundleSlowFillsV3 Slow fill bundle data.
 * @param unexecutableSlowFills Expired slow fill bundle data.
 * @param expiredDepositsToRefundV3 Expired deposit bundle data.
 * @param clients Clients required to construct a new pool rebalance root.
 * @maxL1TokenCountOverride Optional parameter to cap the number of tokens in a single pool rebalance leaf.
 */
export async function _buildOptimisticPoolRebalanceRoot(latestMainnetBlock, mainnetBundleEndBlock, bundleV3Deposits, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, expiredDepositsToRefundV3, clients, maxL1TokenCountOverride) {
    const { runningBalances, realizedLpFees, chainWithRefundsOnly } = _getMarginalRunningBalances(mainnetBundleEndBlock, bundleV3Deposits, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, expiredDepositsToRefundV3, clients);
    // Get the pool rebalance root for the pending bundle so that we may account for its calculated running balances.
    // @dev It is safe to index the hub pool client's proposed root bundles here since there is guaranteed to be a pending proposal in this code block.
    const mostRecentProposedRootBundle = clients.hubPoolClient.getLatestProposedRootBundle();
    const blockRangesForChains = getImpliedBundleBlockRanges(clients.hubPoolClient, clients.configStoreClient, mostRecentProposedRootBundle);
    // We are loading data from a pending root bundle which should be well into liveness, so we want to use arweave if possible.
    const pendingRootBundleData = await clients.bundleDataClient.loadData(blockRangesForChains, clients.spokePoolClients, true);
    // Build the pool rebalance root for the pending root bundle.
    const { leaves, tree } = _buildHistoricalPoolRebalanceRoot(latestMainnetBlock, blockRangesForChains[0][1], pendingRootBundleData.bundleDepositsV3, pendingRootBundleData.bundleFillsV3, pendingRootBundleData.bundleSlowFillsV3, pendingRootBundleData.unexecutableSlowFills, pendingRootBundleData.expiredDepositsToRefundV3, clients, maxL1TokenCountOverride);
    // Assert that the rebuilt pool rebalance root matches the pending root bundle's value. If it does not, then we likely misconstructed the pending root bundle and should throw.
    assert(tree.getHexRoot() === mostRecentProposedRootBundle.poolRebalanceRoot);
    // Only add marginal pending running balances if there is already an entry in `runningBalances`. If there is no entry in `runningBalances`, then
    // The running balance for this entry was unchanged since the last root bundle.
    Object.keys(runningBalances).forEach((_repaymentChainId) => {
        Object.keys(runningBalances[Number(_repaymentChainId)]).forEach((_l1TokenAddress) => {
            const repaymentChainId = Number(_repaymentChainId);
            const l1TokenAddress = EvmAddress.from(_l1TokenAddress);
            const pendingPoolRebalanceLeaf = leaves.find((leaf) => leaf.chainId === repaymentChainId && leaf.l1Tokens.some((l1Token) => l1Token.eq(l1TokenAddress)));
            // If the pending pool rebalance root has running balances defined, then add it to `runningBalances`.
            if (isDefined(pendingPoolRebalanceLeaf)) {
                const pendingLeafTokenIdx = pendingPoolRebalanceLeaf.l1Tokens.findIndex((l1Token) => l1Token.eq(l1TokenAddress));
                assert(pendingLeafTokenIdx !== -1);
                const pendingRunningBalanceAmount = pendingPoolRebalanceLeaf.runningBalances[pendingLeafTokenIdx];
                if (!pendingRunningBalanceAmount.eq(bnZero)) {
                    updateRunningBalance(runningBalances, repaymentChainId, _l1TokenAddress, pendingRunningBalanceAmount);
                }
            }
            else {
                // Otherwise, add the last running balance for this token.
                const { runningBalance: lastExecutedBundleRunningBalance } = clients.hubPoolClient.getRunningBalanceBeforeBlockForChain(latestMainnetBlock, repaymentChainId, l1TokenAddress);
                if (!lastExecutedBundleRunningBalance.eq(bnZero)) {
                    updateRunningBalance(runningBalances, repaymentChainId, _l1TokenAddress, lastExecutedBundleRunningBalance);
                }
            }
        });
    });
    const poolRebalanceLeaves = constructPoolRebalanceLeaves(mainnetBundleEndBlock, runningBalances, realizedLpFees, Array.from(chainWithRefundsOnly).filter((chainId) => !Object.keys(runningBalances).includes(chainId.toString())), clients.configStoreClient, maxL1TokenCountOverride);
    return {
        runningBalances,
        realizedLpFees,
        leaves: poolRebalanceLeaves,
        tree: buildPoolRebalanceLeafTree(poolRebalanceLeaves),
    };
}
/*
 * @notice Gets the running balance amounts derived from the input bundle data.
 * @param mainnetBundleEndBlock The end block number of the block range corresponding to the bundle data.
 * @param bundleV3Deposits Deposit bundle data for the implied block range given by the mainnetBundleEndBlock.
 * @param bundleFillsV3 Fill bundle data.
 * @param bundleSlowFillsV3 Slow fill bundle data.
 * @param unexecutableSlowFills Expired slow fill bundle data.
 * @param expiredDepositsToRefundV3 Expired deposit bundle data.
 * @param clients Clients required to construct a new pool rebalance root.
 */
export function _getMarginalRunningBalances(mainnetBundleEndBlock, bundleV3Deposits, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, expiredDepositsToRefundV3, clients) {
    // Running balances are the amount of tokens that we need to send to each SpokePool to pay for all instant and
    // slow relay refunds. They are decreased by the amount of funds already held by the SpokePool. Balances are keyed
    // by the SpokePool's network and L1 token equivalent of the L2 token to refund.
    // Realized LP fees are keyed the same as running balances and represent the amount of LP fees that should be paid
    // to LP's for each running balance.
    // For each FilledV3Relay group, identified by { repaymentChainId, L1TokenAddress }, initialize a "running balance"
    // to the total refund amount for that group.
    const runningBalances = {};
    const realizedLpFees = {};
    // Keep track of any chains that we want to create a PoolRebalanceLeaf (and send a root bundle to) but don't have
    // any running balances.
    const chainWithRefundsOnly = new Set();
    /**
     * REFUNDS FOR FAST FILLS
     */
    // Add running balances and lp fees for v3 relayer refunds using BundleDataClient.bundleFillsV3. Refunds
    // should be equal to inputAmount - lpFees so that relayers get to keep the relayer fee. Add the refund amount
    // to the running balance for the repayment chain.
    Object.entries(bundleFillsV3).forEach(([_repaymentChainId, fillsForChain]) => {
        const repaymentChainId = Number(_repaymentChainId);
        Object.entries(fillsForChain).forEach(([l2TokenAddress, { realizedLpFees: totalRealizedLpFee, totalRefundAmount }]) => {
            // If the repayment token and repayment chain ID do not map to a PoolRebalanceRoute graph, then
            // there are no relevant L1 running balances.
            const l1Token = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(toAddressType(l2TokenAddress, repaymentChainId), repaymentChainId, mainnetBundleEndBlock);
            if (!l1Token) {
                chainWithRefundsOnly.add(repaymentChainId);
                return;
            }
            const l1TokenAddr = l1Token.toNative();
            assert(l1Token.isEVM(), `Expected an EVM address: ${l1TokenAddr}`);
            updateRunningBalance(runningBalances, repaymentChainId, l1TokenAddr, totalRefundAmount);
            updateRunningBalance(realizedLpFees, repaymentChainId, l1TokenAddr, totalRealizedLpFee);
        });
    });
    /**
     * PAYMENTS SLOW FILLS
     */
    // Add running balances and lp fees for v3 slow fills using BundleDataClient.bundleSlowFillsV3.
    // Slow fills should still increment bundleLpFees and updatedOutputAmount should be equal to inputAmount - lpFees.
    // Increment the updatedOutputAmount to the destination chain.
    Object.entries(bundleSlowFillsV3).forEach(([_destinationChainId, depositsForChain]) => {
        const destinationChainId = Number(_destinationChainId);
        Object.entries(depositsForChain).forEach(([outputToken, deposits]) => {
            deposits.forEach((deposit) => {
                const l1TokenCounterpart = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(toAddressType(outputToken, destinationChainId), destinationChainId, mainnetBundleEndBlock);
                assert(isDefined(l1TokenCounterpart), "getRefundInformationFromFill: l1TokenCounterpart is undefined");
                const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment);
                updateRunningBalance(runningBalances, destinationChainId, l1TokenCounterpart.toEvmAddress(), deposit.inputAmount.sub(lpFee));
                // Slow fill LP fees are accounted for when the slow fill executes and a V3FilledRelay is emitted. i.e. when
                // the slow fill execution is included in bundleFillsV3.
            });
        });
    });
    /**
     * EXCESSES FROM UNEXECUTABLE SLOW FILLS
     */
    // Subtract destination chain running balances for BundleDataClient.unexecutableSlowFills.
    // These are all slow fills that are impossible to execute and therefore the amount to return would be
    // the updatedOutputAmount = inputAmount - lpFees.
    Object.entries(unexecutableSlowFills).forEach(([_destinationChainId, slowFilledDepositsForChain]) => {
        const destinationChainId = Number(_destinationChainId);
        Object.entries(slowFilledDepositsForChain).forEach(([outputToken, slowFilledDeposits]) => {
            slowFilledDeposits.forEach((deposit) => {
                const l1TokenCounterpart = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(toAddressType(outputToken, destinationChainId), destinationChainId, mainnetBundleEndBlock);
                assert(isDefined(l1TokenCounterpart), "getRefundInformationFromFill: l1TokenCounterpart is undefined");
                const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment);
                updateRunningBalance(runningBalances, destinationChainId, l1TokenCounterpart.toEvmAddress(), lpFee.sub(deposit.inputAmount));
                // Slow fills don't add to lpFees, only when the slow fill is executed and a V3FilledRelay is emitted, so
                // we don't need to subtract it here. Moreover, the HubPoole expects bundleLpFees to be > 0.
            });
        });
    });
    /**
     * DEPOSITS
     */
    // Handle v3Deposits. These decrement running balances from the origin chain equal to the inputAmount.
    // There should not be early deposits in v3.
    Object.entries(bundleV3Deposits).forEach(([, depositsForChain]) => {
        Object.entries(depositsForChain).forEach(([, deposits]) => {
            deposits.forEach((deposit) => {
                // If the repayment token and repayment chain ID do not map to a PoolRebalanceRoute graph, then
                // there are no relevant L1 running balances.
                if (!clients.hubPoolClient.l2TokenHasPoolRebalanceRoute(deposit.inputToken, deposit.originChainId, mainnetBundleEndBlock)) {
                    return;
                }
                updateRunningBalanceForDeposit(runningBalances, clients.hubPoolClient, deposit, deposit.inputAmount.mul(-1), mainnetBundleEndBlock);
            });
        });
    });
    /**
     * REFUNDS FOR EXPIRED DEPOSITS
     */
    // Add origin chain running balance for expired v3 deposits. These should refund the inputAmount.
    Object.entries(expiredDepositsToRefundV3).forEach(([_originChainId, depositsForChain]) => {
        const originChainId = Number(_originChainId);
        Object.entries(depositsForChain).forEach(([inputToken, deposits]) => {
            deposits.forEach((deposit) => {
                // If the repayment token and repayment chain ID do not map to a PoolRebalanceRoute graph, then
                // there are no relevant L1 running balances.
                const l1TokenCounterpart = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(toAddressType(inputToken, originChainId), originChainId, mainnetBundleEndBlock);
                if (!l1TokenCounterpart) {
                    chainWithRefundsOnly.add(deposit.originChainId);
                    return;
                }
                updateRunningBalance(runningBalances, originChainId, l1TokenCounterpart.toEvmAddress(), deposit.inputAmount);
            });
        });
    });
    return { runningBalances, chainWithRefundsOnly, realizedLpFees };
}
//# sourceMappingURL=DataworkerUtils.js.map