import { EPOCHS_PER_HISTORICAL_VECTOR, SLOTS_PER_EPOCH, SYNC_COMMITTEE_SUBNET_SIZE, isForkPostElectra, isForkPostFulu, } from "@lodestar/params";
import { computeEpochAtSlot, computeStartSlotAtEpoch, getCurrentEpoch, getRandaoMix, loadState, } from "@lodestar/state-transition";
import { getValidatorStatus } from "@lodestar/types";
import { ApiError } from "../../errors.js";
import { assertUniqueItems } from "../../utils.js";
import { filterStateValidatorsByStatus, getStateResponseWithRegen, getStateValidatorIndex, toValidatorResponse, } from "./utils.js";
export function getBeaconStateApi({ chain, config, }) {
    async function getState(stateId) {
        const { state, executionOptimistic, finalized } = await getStateResponseWithRegen(chain, stateId);
        return {
            state: state instanceof Uint8Array ? loadState(config, chain.getHeadState(), state).state : state,
            executionOptimistic,
            finalized,
        };
    }
    return {
        async getStateRoot({ stateId }) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            return {
                data: { root: state.hashTreeRoot() },
                meta: { executionOptimistic, finalized },
            };
        },
        async getStateFork({ stateId }) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            return {
                data: state.fork,
                meta: { executionOptimistic, finalized },
            };
        },
        async getStateRandao({ stateId, epoch }) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            const stateEpoch = computeEpochAtSlot(state.slot);
            const usedEpoch = epoch ?? stateEpoch;
            if (!(stateEpoch < usedEpoch + EPOCHS_PER_HISTORICAL_VECTOR && usedEpoch <= stateEpoch)) {
                throw new ApiError(400, "Requested epoch is out of range");
            }
            const randao = getRandaoMix(state, usedEpoch);
            return {
                data: { randao },
                meta: { executionOptimistic, finalized },
            };
        },
        async getStateFinalityCheckpoints({ stateId }) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            return {
                data: {
                    currentJustified: state.currentJustifiedCheckpoint,
                    previousJustified: state.previousJustifiedCheckpoint,
                    finalized: state.finalizedCheckpoint,
                },
                meta: { executionOptimistic, finalized },
            };
        },
        async getStateValidators({ stateId, validatorIds = [], statuses = [] }) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            const currentEpoch = getCurrentEpoch(state);
            const { validators, balances } = state; // Get the validators sub tree once for all the loop
            const { pubkeyCache } = chain;
            const validatorResponses = [];
            if (validatorIds.length) {
                assertUniqueItems(validatorIds, "Duplicate validator IDs provided");
                for (const id of validatorIds) {
                    const resp = getStateValidatorIndex(id, state, pubkeyCache);
                    if (resp.valid) {
                        const validatorIndex = resp.validatorIndex;
                        const validator = validators.getReadonly(validatorIndex);
                        if (statuses.length && !statuses.includes(getValidatorStatus(validator, currentEpoch))) {
                            continue;
                        }
                        const validatorResponse = toValidatorResponse(validatorIndex, validator, balances.get(validatorIndex), currentEpoch);
                        validatorResponses.push(validatorResponse);
                    }
                }
                return {
                    data: validatorResponses,
                    meta: { executionOptimistic, finalized },
                };
            }
            if (statuses.length) {
                assertUniqueItems(statuses, "Duplicate statuses provided");
                const validatorsByStatus = filterStateValidatorsByStatus(statuses, state, pubkeyCache, currentEpoch);
                return {
                    data: validatorsByStatus,
                    meta: { executionOptimistic, finalized },
                };
            }
            // TODO: This loops over the entire state, it's a DOS vector
            const validatorsArr = state.validators.getAllReadonlyValues();
            const balancesArr = state.balances.getAll();
            const resp = [];
            for (let i = 0; i < validatorsArr.length; i++) {
                resp.push(toValidatorResponse(i, validatorsArr[i], balancesArr[i], currentEpoch));
            }
            return {
                data: resp,
                meta: { executionOptimistic, finalized },
            };
        },
        async postStateValidators(args, context) {
            return this.getStateValidators(args, context);
        },
        async postStateValidatorIdentities({ stateId, validatorIds = [] }) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            const { pubkeyCache } = chain;
            let validatorIdentities;
            if (validatorIds.length) {
                assertUniqueItems(validatorIds, "Duplicate validator IDs provided");
                validatorIdentities = [];
                for (const id of validatorIds) {
                    const resp = getStateValidatorIndex(id, state, pubkeyCache);
                    if (resp.valid) {
                        const index = resp.validatorIndex;
                        const { pubkey, activationEpoch } = state.validators.getReadonly(index);
                        validatorIdentities.push({ index, pubkey, activationEpoch });
                    }
                }
            }
            else {
                const validatorsArr = state.validators.getAllReadonlyValues();
                validatorIdentities = new Array(validatorsArr.length);
                for (let i = 0; i < validatorsArr.length; i++) {
                    const { pubkey, activationEpoch } = validatorsArr[i];
                    validatorIdentities[i] = { index: i, pubkey, activationEpoch };
                }
            }
            return {
                data: validatorIdentities,
                meta: { executionOptimistic, finalized },
            };
        },
        async getStateValidator({ stateId, validatorId }) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            const { pubkeyCache } = chain;
            const resp = getStateValidatorIndex(validatorId, state, pubkeyCache);
            if (!resp.valid) {
                throw new ApiError(resp.code, resp.reason);
            }
            const validatorIndex = resp.validatorIndex;
            return {
                data: toValidatorResponse(validatorIndex, state.validators.getReadonly(validatorIndex), state.balances.get(validatorIndex), getCurrentEpoch(state)),
                meta: { executionOptimistic, finalized },
            };
        },
        async getStateValidatorBalances({ stateId, validatorIds = [] }) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            if (validatorIds.length) {
                assertUniqueItems(validatorIds, "Duplicate validator IDs provided");
                const balances = [];
                for (const id of validatorIds) {
                    const resp = getStateValidatorIndex(id, state, chain.pubkeyCache);
                    if (resp.valid) {
                        balances.push({
                            index: resp.validatorIndex,
                            balance: state.balances.get(resp.validatorIndex),
                        });
                    }
                }
                return {
                    data: balances,
                    meta: { executionOptimistic, finalized },
                };
            }
            // TODO: This loops over the entire state, it's a DOS vector
            const balancesArr = state.balances.getAll();
            const resp = [];
            for (let i = 0; i < balancesArr.length; i++) {
                resp.push({ index: i, balance: balancesArr[i] });
            }
            return {
                data: resp,
                meta: { executionOptimistic, finalized },
            };
        },
        async postStateValidatorBalances(args, context) {
            return this.getStateValidatorBalances(args, context);
        },
        async getEpochCommittees({ stateId, ...filters }) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            const stateCached = state;
            if (stateCached.epochCtx === undefined) {
                throw new ApiError(400, `No cached state available for stateId: ${stateId}`);
            }
            const stateEpoch = computeEpochAtSlot(state.slot);
            const epoch = filters.epoch ?? stateEpoch;
            const startSlot = computeStartSlotAtEpoch(epoch);
            const endSlot = startSlot + SLOTS_PER_EPOCH - 1;
            if (Math.abs(epoch - stateEpoch) > 1) {
                throw new ApiError(400, `Epoch ${epoch} must be within one epoch of state epoch ${stateEpoch}`);
            }
            if (filters.slot !== undefined && (filters.slot < startSlot || filters.slot > endSlot)) {
                throw new ApiError(400, `Slot ${filters.slot} is not in epoch ${epoch}`);
            }
            const decisionRoot = stateCached.epochCtx.getShufflingDecisionRoot(epoch);
            const shuffling = await chain.shufflingCache.get(epoch, decisionRoot);
            if (!shuffling) {
                throw new ApiError(500, `No shuffling found to calculate committees for epoch: ${epoch} and decisionRoot: ${decisionRoot}`);
            }
            const committees = shuffling.committees;
            const committeesFlat = committees.flatMap((slotCommittees, slotInEpoch) => {
                const slot = startSlot + slotInEpoch;
                if (filters.slot !== undefined && filters.slot !== slot) {
                    return [];
                }
                return slotCommittees.flatMap((committee, committeeIndex) => {
                    if (filters.index !== undefined && filters.index !== committeeIndex) {
                        return [];
                    }
                    return [
                        {
                            index: committeeIndex,
                            slot,
                            validators: Array.from(committee),
                        },
                    ];
                });
            });
            return {
                data: committeesFlat,
                meta: { executionOptimistic, finalized },
            };
        },
        /**
         * Retrieves the sync committees for the given state.
         * @param epoch Fetch sync committees for the given epoch. If not present then the sync committees for the epoch of the state will be obtained.
         */
        async getEpochSyncCommittees({ stateId, epoch }) {
            // TODO: Should pick a state with the provided epoch too
            const { state, executionOptimistic, finalized } = await getState(stateId);
            // TODO: If possible compute the syncCommittees in advance of the fork and expose them here.
            // So the validators can prepare and potentially attest the first block. Not critical tho, it's very unlikely
            const stateEpoch = computeEpochAtSlot(state.slot);
            if (stateEpoch < config.ALTAIR_FORK_EPOCH) {
                throw new ApiError(400, "Requested state before ALTAIR_FORK_EPOCH");
            }
            const stateCached = state;
            if (stateCached.epochCtx === undefined) {
                throw new ApiError(400, `No cached state available for stateId: ${stateId}`);
            }
            const syncCommitteeCache = stateCached.epochCtx.getIndexedSyncCommitteeAtEpoch(epoch ?? stateEpoch);
            const validatorIndices = new Array(...syncCommitteeCache.validatorIndices);
            // Subcommittee assignments of the current sync committee
            const validatorAggregates = [];
            for (let i = 0; i < validatorIndices.length; i += SYNC_COMMITTEE_SUBNET_SIZE) {
                validatorAggregates.push(validatorIndices.slice(i, i + SYNC_COMMITTEE_SUBNET_SIZE));
            }
            return {
                data: {
                    validators: validatorIndices,
                    validatorAggregates,
                },
                meta: { executionOptimistic, finalized },
            };
        },
        async getPendingDeposits({ stateId }, context) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            const fork = config.getForkName(state.slot);
            if (!isForkPostElectra(fork)) {
                throw new ApiError(400, `Cannot retrieve pending deposits for pre-electra state fork=${fork}`);
            }
            const { pendingDeposits } = state;
            return {
                data: context?.returnBytes ? pendingDeposits.serialize() : pendingDeposits.toValue(),
                meta: { executionOptimistic, finalized, version: fork },
            };
        },
        async getPendingPartialWithdrawals({ stateId }, context) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            const fork = config.getForkName(state.slot);
            if (!isForkPostElectra(fork)) {
                throw new ApiError(400, `Cannot retrieve pending partial withdrawals for pre-electra state fork=${fork}`);
            }
            const { pendingPartialWithdrawals } = state;
            return {
                data: context?.returnBytes ? pendingPartialWithdrawals.serialize() : pendingPartialWithdrawals.toValue(),
                meta: { executionOptimistic, finalized, version: fork },
            };
        },
        async getPendingConsolidations({ stateId }, context) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            const fork = config.getForkName(state.slot);
            if (!isForkPostElectra(fork)) {
                throw new ApiError(400, `Cannot retrieve pending consolidations for pre-electra state fork=${fork}`);
            }
            const { pendingConsolidations } = state;
            return {
                data: context?.returnBytes ? pendingConsolidations.serialize() : pendingConsolidations.toValue(),
                meta: { executionOptimistic, finalized, version: fork },
            };
        },
        async getProposerLookahead({ stateId }, context) {
            const { state, executionOptimistic, finalized } = await getState(stateId);
            const fork = config.getForkName(state.slot);
            if (!isForkPostFulu(fork)) {
                throw new ApiError(400, `Cannot retrieve proposer lookahead for pre-fulu state fork=${fork}`);
            }
            const { proposerLookahead } = state;
            return {
                data: context?.returnBytes ? proposerLookahead.serialize() : proposerLookahead.toValue(),
                meta: { executionOptimistic, finalized, version: fork },
            };
        },
    };
}
//# sourceMappingURL=index.js.map