import { BYTES_PER_FIELD_ELEMENT, BYTES_PER_LOGS_BLOOM, CELLS_PER_EXT_BLOB, CONSOLIDATION_REQUEST_TYPE, DEPOSIT_REQUEST_TYPE, FIELD_ELEMENTS_PER_BLOB, ForkSeq, WITHDRAWAL_REQUEST_TYPE, } from "@lodestar/params";
import { ssz, } from "@lodestar/types";
import { isExecutionRequestType, } from "./interface.js";
import { bytesToData, dataIntoBytes, dataToBytes, numToQuantity, quantityToBigint, quantityToNum, } from "./utils.js";
const BLOB_BYTES = BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB;
const PROOF_BYTES = 48;
export const BLOB_AND_PROOF_V2_RPC_BYTES = BLOB_BYTES + PROOF_BYTES * CELLS_PER_EXT_BLOB;
export function serializeExecutionPayload(fork, data) {
    const payload = {
        parentHash: bytesToData(data.parentHash),
        feeRecipient: bytesToData(data.feeRecipient),
        stateRoot: bytesToData(data.stateRoot),
        receiptsRoot: bytesToData(data.receiptsRoot),
        logsBloom: bytesToData(data.logsBloom),
        prevRandao: bytesToData(data.prevRandao),
        blockNumber: numToQuantity(data.blockNumber),
        gasLimit: numToQuantity(data.gasLimit),
        gasUsed: numToQuantity(data.gasUsed),
        timestamp: numToQuantity(data.timestamp),
        extraData: bytesToData(data.extraData),
        baseFeePerGas: numToQuantity(data.baseFeePerGas),
        blockHash: bytesToData(data.blockHash),
        transactions: data.transactions.map((tran) => bytesToData(tran)),
    };
    // Capella adds withdrawals to the ExecutionPayload
    if (ForkSeq[fork] >= ForkSeq.capella) {
        const { withdrawals } = data;
        payload.withdrawals = withdrawals.map(serializeWithdrawal);
    }
    // DENEB adds blobGasUsed & excessBlobGas to the ExecutionPayload
    if (ForkSeq[fork] >= ForkSeq.deneb) {
        const { blobGasUsed, excessBlobGas } = data;
        payload.blobGasUsed = numToQuantity(blobGasUsed);
        payload.excessBlobGas = numToQuantity(excessBlobGas);
    }
    // No changes in Electra
    return payload;
}
export function serializeVersionedHashes(vHashes) {
    return vHashes.map(bytesToData);
}
export function hasPayloadValue(response) {
    return response.blockValue !== undefined;
}
export function parseExecutionPayload(fork, response) {
    let data;
    let executionPayloadValue;
    let blobsBundle;
    let executionRequests;
    let shouldOverrideBuilder;
    if (hasPayloadValue(response)) {
        executionPayloadValue = quantityToBigint(response.blockValue);
        data = response.executionPayload;
        blobsBundle = response.blobsBundle ? parseBlobsBundle(response.blobsBundle) : undefined;
        executionRequests = response.executionRequests
            ? deserializeExecutionRequests(response.executionRequests)
            : undefined;
        shouldOverrideBuilder = response.shouldOverrideBuilder ?? false;
    }
    else {
        data = response;
        // Just set it to zero as default
        executionPayloadValue = BigInt(0);
        blobsBundle = undefined;
        executionRequests = undefined;
        shouldOverrideBuilder = false;
    }
    const executionPayload = {
        parentHash: dataToBytes(data.parentHash, 32),
        feeRecipient: dataToBytes(data.feeRecipient, 20),
        stateRoot: dataToBytes(data.stateRoot, 32),
        receiptsRoot: dataToBytes(data.receiptsRoot, 32),
        logsBloom: dataToBytes(data.logsBloom, BYTES_PER_LOGS_BLOOM),
        prevRandao: dataToBytes(data.prevRandao, 32),
        blockNumber: quantityToNum(data.blockNumber),
        gasLimit: quantityToNum(data.gasLimit),
        gasUsed: quantityToNum(data.gasUsed),
        timestamp: quantityToNum(data.timestamp),
        extraData: dataToBytes(data.extraData, null),
        baseFeePerGas: quantityToBigint(data.baseFeePerGas),
        blockHash: dataToBytes(data.blockHash, 32),
        transactions: data.transactions.map((tran) => dataToBytes(tran, null)),
    };
    if (ForkSeq[fork] >= ForkSeq.capella) {
        const { withdrawals } = data;
        // Geth can also reply with null
        if (withdrawals == null) {
            throw Error(`withdrawals missing for ${fork} >= capella executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`);
        }
        executionPayload.withdrawals = withdrawals.map((w) => deserializeWithdrawal(w));
    }
    // DENEB adds excessBlobGas to the ExecutionPayload
    if (ForkSeq[fork] >= ForkSeq.deneb) {
        const { blobGasUsed, excessBlobGas } = data;
        if (blobGasUsed == null) {
            throw Error(`blobGasUsed missing for ${fork} >= deneb executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`);
        }
        if (excessBlobGas == null) {
            throw Error(`excessBlobGas missing for ${fork} >= deneb executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`);
        }
        executionPayload.blobGasUsed = quantityToBigint(blobGasUsed);
        executionPayload.excessBlobGas = quantityToBigint(excessBlobGas);
    }
    // No changes in Electra
    return { executionPayload, executionPayloadValue, blobsBundle, executionRequests, shouldOverrideBuilder };
}
export function serializePayloadAttributes(data) {
    return {
        timestamp: numToQuantity(data.timestamp),
        prevRandao: bytesToData(data.prevRandao),
        suggestedFeeRecipient: data.suggestedFeeRecipient,
        withdrawals: data.withdrawals?.map(serializeWithdrawal),
        parentBeaconBlockRoot: data.parentBeaconBlockRoot ? bytesToData(data.parentBeaconBlockRoot) : undefined,
    };
}
export function serializeBeaconBlockRoot(data) {
    return bytesToData(data);
}
export function deserializePayloadAttributes(data) {
    return {
        timestamp: quantityToNum(data.timestamp),
        prevRandao: dataToBytes(data.prevRandao, 32),
        // DATA is anyway a hex string, so we can just track it as a hex string to
        // avoid any conversions
        suggestedFeeRecipient: data.suggestedFeeRecipient,
        withdrawals: data.withdrawals?.map((withdrawal) => deserializeWithdrawal(withdrawal)),
        parentBeaconBlockRoot: data.parentBeaconBlockRoot ? dataToBytes(data.parentBeaconBlockRoot, 32) : undefined,
    };
}
export function parseBlobsBundle(data) {
    return {
        // As of Nov 17th 2022 according to Dan's tests Geth returns null if no blobs in block
        commitments: (data.commitments ?? []).map((kzg) => dataToBytes(kzg, 48)),
        blobs: (data.blobs ?? []).map((blob) => dataToBytes(blob, BLOB_BYTES)),
        proofs: (data.proofs ?? []).map((kzg) => dataToBytes(kzg, PROOF_BYTES)),
    };
}
export function serializeBlobsBundle(data) {
    return {
        commitments: data.commitments.map((kzg) => bytesToData(kzg)),
        blobs: data.blobs.map((blob) => bytesToData(blob)),
        proofs: data.proofs.map((proof) => bytesToData(proof)),
    };
}
export function serializeWithdrawal(withdrawal) {
    return {
        index: numToQuantity(withdrawal.index),
        validatorIndex: numToQuantity(withdrawal.validatorIndex),
        address: bytesToData(withdrawal.address),
        // Both CL and EL now deal in Gwei, just little-endian to big-endian conversion required
        amount: numToQuantity(withdrawal.amount),
    };
}
export function deserializeWithdrawal(serialized) {
    return {
        index: quantityToNum(serialized.index),
        validatorIndex: quantityToNum(serialized.validatorIndex),
        address: dataToBytes(serialized.address, 20),
        // Both CL and EL now deal in Gwei, just big-endian to little-endian conversion required
        amount: quantityToBigint(serialized.amount),
    };
}
/**
 * Prepend a single-byte requestType to requestsBytes
 */
function prefixRequests(requestsBytes, requestType) {
    const prefixedRequests = new Uint8Array(1 + requestsBytes.length);
    prefixedRequests[0] = requestType;
    prefixedRequests.set(requestsBytes, 1);
    return prefixedRequests;
}
function serializeDepositRequests(depositRequests) {
    const requestsBytes = ssz.electra.DepositRequests.serialize(depositRequests);
    return bytesToData(prefixRequests(requestsBytes, DEPOSIT_REQUEST_TYPE));
}
function deserializeDepositRequests(serialized) {
    return ssz.electra.DepositRequests.deserialize(dataToBytes(serialized, null));
}
function serializeWithdrawalRequests(withdrawalRequests) {
    const requestsBytes = ssz.electra.WithdrawalRequests.serialize(withdrawalRequests);
    return bytesToData(prefixRequests(requestsBytes, WITHDRAWAL_REQUEST_TYPE));
}
function deserializeWithdrawalRequests(serialized) {
    return ssz.electra.WithdrawalRequests.deserialize(dataToBytes(serialized, null));
}
function serializeConsolidationRequests(consolidationRequests) {
    const requestsBytes = ssz.electra.ConsolidationRequests.serialize(consolidationRequests);
    return bytesToData(prefixRequests(requestsBytes, CONSOLIDATION_REQUEST_TYPE));
}
function deserializeConsolidationRequests(serialized) {
    return ssz.electra.ConsolidationRequests.deserialize(dataToBytes(serialized, null));
}
/**
 * This is identical to get_execution_requests_list in
 * https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.8/specs/electra/beacon-chain.md#new-get_execution_requests_list
 */
export function serializeExecutionRequests(executionRequests) {
    const { deposits, withdrawals, consolidations } = executionRequests;
    const result = [];
    if (deposits.length !== 0) {
        result.push(serializeDepositRequests(deposits));
    }
    if (withdrawals.length !== 0) {
        result.push(serializeWithdrawalRequests(withdrawals));
    }
    if (consolidations.length !== 0) {
        result.push(serializeConsolidationRequests(consolidations));
    }
    return result;
}
export function deserializeExecutionRequests(serialized) {
    const result = {
        deposits: [],
        withdrawals: [],
        consolidations: [],
    };
    if (serialized.length === 0) {
        return result;
    }
    let prevRequestType;
    for (let prefixedRequests of serialized) {
        // Slice out 0x so it is easier to extract request type
        if (prefixedRequests.startsWith("0x")) {
            prefixedRequests = prefixedRequests.slice(2);
        }
        const currentRequestType = parseInt(prefixedRequests.substring(0, 2), 16);
        if (!isExecutionRequestType(currentRequestType)) {
            throw Error(`Invalid request type currentRequestType=${prefixedRequests.substring(0, 2)}`);
        }
        const requests = prefixedRequests.slice(2);
        if (requests.length === 0) {
            throw Error(`Request with empty data must be excluded from execution requests currentRequestType=${currentRequestType}`);
        }
        if (prevRequestType !== undefined && prevRequestType >= currentRequestType) {
            throw Error(`Current request type must be larger than previous request type prevRequestType=${prevRequestType} currentRequestType=${currentRequestType}`);
        }
        switch (currentRequestType) {
            case DEPOSIT_REQUEST_TYPE: {
                result.deposits = deserializeDepositRequests(requests);
                break;
            }
            case WITHDRAWAL_REQUEST_TYPE: {
                result.withdrawals = deserializeWithdrawalRequests(requests);
                break;
            }
            case CONSOLIDATION_REQUEST_TYPE: {
                result.consolidations = deserializeConsolidationRequests(requests);
                break;
            }
        }
        prevRequestType = currentRequestType;
    }
    return result;
}
export function deserializeExecutionPayloadBody(data) {
    return data
        ? {
            transactions: data.transactions.map((tran) => dataToBytes(tran, null)),
            withdrawals: data.withdrawals ? data.withdrawals.map(deserializeWithdrawal) : null,
        }
        : null;
}
export function serializeExecutionPayloadBody(data) {
    return data
        ? {
            transactions: data.transactions.map((tran) => bytesToData(tran)),
            withdrawals: data.withdrawals ? data.withdrawals.map(serializeWithdrawal) : null,
        }
        : null;
}
export function deserializeBlobAndProofs(data) {
    return data
        ? {
            blob: dataToBytes(data.blob, BLOB_BYTES),
            proof: dataToBytes(data.proof, PROOF_BYTES),
        }
        : null;
}
export function deserializeBlobAndProofsV2(data) {
    return {
        blob: dataToBytes(data.blob, BLOB_BYTES),
        proofs: data.proofs.map((proof) => dataToBytes(proof, PROOF_BYTES)),
    };
}
/**
 * The same to deserializeBlobAndProofsV2 but using preallocated buffers since BlobAndProofV2Rpc is fixed size
 */
export function deserializeBlobAndProofsV2IntoBytes(data, buffer) {
    if (buffer.length !== BLOB_AND_PROOF_V2_RPC_BYTES) {
        throw Error(`Invalid buffer length ${buffer.length}, expected ${BLOB_AND_PROOF_V2_RPC_BYTES} to hold BlobAndProofV2Rpc`);
    }
    // https://github.com/ethereum/execution-apis/blob/main/src/engine/osaka.md#blobandproofv2
    // proofs MUST contain exactly CELLS_PER_EXT_BLOB cell proofs.
    if (data.proofs.length !== CELLS_PER_EXT_BLOB) {
        throw Error(`Invalid proofs length ${data.proofs.length}, expected ${CELLS_PER_EXT_BLOB}`);
    }
    const blob = dataIntoBytes(data.blob, buffer.subarray(0, BLOB_BYTES));
    const proofs = [];
    for (let i = 0; i < CELLS_PER_EXT_BLOB; i++) {
        const proof = dataIntoBytes(data.proofs[i], buffer.subarray(BLOB_BYTES + i * PROOF_BYTES, BLOB_BYTES + (i + 1) * PROOF_BYTES));
        if (proof.length !== PROOF_BYTES) {
            throw Error(`Invalid proof length ${proof.length}, expected ${PROOF_BYTES}`);
        }
        proofs.push(proof);
    }
    return {
        blob,
        proofs,
    };
}
export function assertReqSizeLimit(blockHashesReqCount, count) {
    if (blockHashesReqCount > count) {
        throw new Error(`Requested blocks must not be > ${count}`);
    }
    return;
}
//# sourceMappingURL=types.js.map