"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.postToArchiver = exports.getFromArchiver = exports.getLastHashedArchiverList = exports.getArchiverListHash = exports.computeNewArchiverListHash = exports.sortedByPubKey = exports.registerRoutes = exports.removeArchiverConnection = exports.addArchiverConnection = exports.getRefreshedArchivers = exports.sendData = exports.removeDataRecipient = exports.forwardAccounts = exports.instantForwardOriginalTxData = exports.instantForwardReceipts = exports.getArchiversList = exports.addDataRecipient = exports.updateArchivers = exports.removeArchiver = exports.removeArchiverByPublicKey = exports.getArchiverUpdates = exports.addLeaveRequest = exports.addArchiverJoinRequest = exports.resetLeaveRequests = exports.resetJoinRequests = exports.queueRequest = exports.sendRequests = exports.parseRecord = exports.updateRecord = exports.validateRecordTypes = exports.getTxs = exports.reset = exports.init = exports.getRandomArchiver = exports.getArchiverWithPublicKey = exports.getNumArchivers = exports.archiverDataSubscriptionsUpdateFeatureActivated = exports.DataRequestTypes = exports.RECEIPT_FORWARD_INTERVAL_MS = exports.connectedSockets = exports.recipients = exports.archivers = void 0;
const lib_types_1 = require("@shardeum-foundation/lib-types");
const deepmerge_1 = __importDefault(require("deepmerge"));
const http = __importStar(require("../http"));
const logger_1 = require("../logger");
const snapshot_1 = require("../snapshot");
const utils_1 = require("../utils");
const nestedCounters_1 = require("../utils/nestedCounters");
const profiler_1 = require("../utils/profiler");
const Self = __importStar(require("./Self"));
const Comms = __importStar(require("./Comms"));
const Context = __importStar(require("./Context"));
const checkIP_1 = require("../utils/functions/checkIP");
const Context_1 = require("./Context");
const CycleChain_1 = require("./CycleChain");
const CycleCreator = __importStar(require("./CycleCreator"));
const NodeList = __importStar(require("./NodeList"));
const Apoptosis_1 = require("./Apoptosis");
const crypto_1 = require("crypto");
const CycleChain = __importStar(require("./CycleChain"));
const rfdc_1 = __importDefault(require("rfdc"));
const network_1 = require("../network");
const functions_1 = require("../p2p/LostArchivers/functions");
const neverthrow_1 = require("neverthrow");
const lib_types_2 = require("@shardeum-foundation/lib-types");
const shardus_types_1 = require("../shardus/shardus-types");
const Helpers_1 = require("../types/ajv/Helpers");
const AJVSchemaEnum_1 = require("../types/enum/AJVSchemaEnum");
const customHttpFunctions_1 = require("../http/customHttpFunctions");
const clone = (0, rfdc_1.default)();
/** STATE */
let p2pLogger;
let allowedArchivers = [];
let allowedArchiversInterval = null;
let joinRequests;
let leaveRequests;
let receiptForwardInterval = null;
let networkCheckInterval = null;
let networkCheckInProgress = false;
exports.connectedSockets = {};
let lastSentCycle = -1;
let lastTimeForwardedArchivers = [];
exports.RECEIPT_FORWARD_INTERVAL_MS = 5000;
const ALLOWED_ARCHIVERS_UPDATE_INTERVAL_MS = process.env.ALLOWED_ARCHIVERS_UPDATE_INTERVAL_MS || 601000;
const MAX_BODY_LENGTH_ALLOWED_ARCHIVERS = 1024 * 1024 * 1; // 1MB
var DataRequestTypes;
(function (DataRequestTypes) {
    DataRequestTypes["SUBSCRIBE"] = "SUBSCRIBE";
    DataRequestTypes["UNSUBSCRIBE"] = "UNSUBSCRIBE";
})(DataRequestTypes = exports.DataRequestTypes || (exports.DataRequestTypes = {}));
// This is to check if the new archiver data subscriptions feature is activated in shardeum v1.1.3
// We can remove later after v1.1.3 upgrade
exports.archiverDataSubscriptionsUpdateFeatureActivated = false;
/** FUNCTIONS */
/** Accessor Functions */
function getNumArchivers() {
    return exports.archivers.size;
}
exports.getNumArchivers = getNumArchivers;
function getArchiverWithPublicKey(publicKey) {
    return exports.archivers.get(publicKey);
}
exports.getArchiverWithPublicKey = getArchiverWithPublicKey;
function getRandomArchiver() {
    if (exports.archivers.size === 0)
        return null;
    const list = Array.from(exports.archivers.values());
    const index = (0, crypto_1.randomInt)(0, list.length);
    return list[index];
}
exports.getRandomArchiver = getRandomArchiver;
/** CycleCreator Functions */
function init() {
    console.log('init archiver module');
    p2pLogger = Context_1.logger.getLogger('p2p');
    exports.archivers = new Map();
    exports.recipients = new Map();
    reset();
    resetLeaveRequests();
    registerRoutes();
    getAllowedArchivers().then((archivers) => {
        if (archivers.length >= 1) {
            allowedArchivers = archivers;
        }
    });
    setTimeout(() => {
        // Set up interval to fetch allowed archivers
        allowedArchiversInterval = setInterval(async () => {
            try {
                const newArchiversList = await getAllowedArchivers();
                if (newArchiversList.length >= 1) {
                    allowedArchivers = newArchiversList;
                }
            }
            catch (err) {
                if (logger_1.logFlags.important_as_fatal) {
                    console.error('Periodic update: failed to update allowedArchivers:', err);
                }
            }
        }, Number(ALLOWED_ARCHIVERS_UPDATE_INTERVAL_MS)); // Default is 60 seconds
    }, (0, crypto_1.randomInt)(Number(ALLOWED_ARCHIVERS_UPDATE_INTERVAL_MS))); // Stagger
    if (Context_1.config.p2p.experimentalSnapshot && !receiptForwardInterval) {
        receiptForwardInterval = setInterval(forwardReceipts, exports.RECEIPT_FORWARD_INTERVAL_MS);
    }
    if (Context_1.config.p2p.checkNetworkStopped) {
        setTimeout(() => {
            networkCheckInterval = setInterval(() => {
                hasNetworkStopped().then((stopped) => {
                    if (stopped) {
                        const msg = 'checkNetworkStopped: Network has stopped. Initiating apoptosis';
                        /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                            info(msg);
                        this.fatalLogger.fatal('checkNetworkStopped: Network has stopped. Initiating apoptosis');
                        nestedCounters_1.nestedCountersInstance.countEvent('checkNetworkStopped', `Network has stopped: apop self. ${(0, network_1.shardusGetTime)()}`);
                        (0, Apoptosis_1.apoptosizeSelf)(msg, 'Node stopped due to the network shutting down.');
                    }
                });
            }, 1000 * 60 * 5); // Check every 5 min
        }, (0, crypto_1.randomInt)(1000 * 60, 1000 * 60 * 5)); // Stagger initial checks between 1-5 min
    }
}
exports.init = init;
async function getAllowedArchivers() {
    // Helper function to verify signatures
    function verifyArchiverData(data) {
        // Get allowed signers and min sig required from the global account
        const allowedSigners = Context_1.config.debug.multisigKeys;
        const minSigRequired = Context_1.config.debug.minSigRequiredForArchiverWhitelist;
        if (!data.signatures || data.signatures.length < minSigRequired) {
            p2pLogger.warn('getAllowedArchivers: insufficient signatures');
            return false;
        }
        const payload = {
            allowedArchivers: data.allowedArchivers,
        };
        const isValidList = Context.stateManager.app.verifyMultiSigs(payload, data.signatures, allowedSigners, minSigRequired, shardus_types_1.DevSecurityLevel.High);
        if (!isValidList) {
            p2pLogger.warn('getAllowedArchivers: invalid signatures');
            return false;
        }
        return true;
    }
    // Helper function to fetch and validate archiver data
    async function fetchArchiverData(ip, port) {
        try {
            const response = await (0, customHttpFunctions_1.customFetch)(`http://${ip}:${port}/allowed-archivers`);
            if (!response.ok) {
                /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('Archiver', `getAllowedArchivers: Response body not ok: ${ip}:${port} `);
                return null;
            }
            const bodyText = await response.text();
            // Check if the body length exceeds the maximum allowed length
            if (bodyText.length > MAX_BODY_LENGTH_ALLOWED_ARCHIVERS) {
                p2pLogger.warn(`Response body for allowed archivers is too large: ${bodyText.length} bytes. Max allowed is ${MAX_BODY_LENGTH_ALLOWED_ARCHIVERS} bytes.`);
                /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('Archiver', `getAllowedArchivers: Response body for allowed archivers is too large: ${bodyText.length} bytes.  ${ip}:${port} `);
                return null;
            }
            const data = lib_types_2.Utils.safeJsonParse(bodyText);
            // Validate the parsed JSON data against the AJV schema
            const inValidResponse = (0, Helpers_1.verifyPayload)(AJVSchemaEnum_1.AJVSchemaEnum.AllowedArchiverResponse, data);
            if (inValidResponse) {
                p2pLogger.warn('getAllowedArchivers: invalid allowed archivers response');
                /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('Archiver', `getAllowedArchivers: invalid allowed archivers response  ${ip}:${port} `);
                return null;
            }
            if (data.allowedArchivers?.length > 0 && verifyArchiverData(data)) {
                return data;
            }
            else {
                p2pLogger.warn('getAllowedArchivers: invalid signatures or no allowed archivers');
                /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('Archiver', `getAllowedArchivers: invalid signatures or no allowed archivers  ${ip}:${port} `);
                return null;
            }
        }
        catch (err) {
            console.error('getAllowedArchivers: Failed to get archiver data from ', ip, ':', port, err);
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('Archiver', `getAllowedArchivers: err ${ip}:${port} ${err.message}`);
            p2pLogger.warn(`Failed to get archiver data from ${ip}:${port}`);
        }
        return null;
    }
    // Try getting from existing archivers first
    if (exports.archivers.size > 0) {
        const randomArchiver = getRandomArchiver();
        if (!randomArchiver)
            return [];
        const data = await fetchArchiverData(randomArchiver.ip, randomArchiver.port);
        if (data) {
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('Archiver', `getAllowedArchivers: got data from: ${randomArchiver.ip}:${randomArchiver.port}`);
            return data.allowedArchivers;
        }
        else {
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('Archiver', `getAllowedArchivers: failed to get data from: ${randomArchiver.ip}:${randomArchiver.port}`);
        }
    }
    // Fall back to configured existing archivers
    const existingArchivers = Context_1.config.p2p.existingArchivers;
    if (existingArchivers.length > 0) {
        for (const archiver of existingArchivers) {
            const data = await fetchArchiverData(archiver.ip, archiver.port);
            if (data) {
                /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('Archiver', `getAllowedArchivers: got data from fallback: ${archiver.ip}:${archiver.port}`);
                return data.allowedArchivers;
            }
            else {
                /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('Archiver', `getAllowedArchivers: failed to get data from fallback: ${archiver.ip}:${archiver.port}`);
            }
        }
    }
    return [];
}
function reset() {
    resetJoinRequests();
}
exports.reset = reset;
function getTxs() {
    // [IMPORTANT] Must return a copy to avoid mutation
    const requestsCopy = (0, deepmerge_1.default)({}, [...joinRequests, ...leaveRequests]);
    if (logger_1.logFlags.console)
        console.log(`getTxs: Cycle ${CycleCreator.currentCycle}, Quarter: ${CycleCreator.currentQuarter}`, {
            archivers: requestsCopy,
        });
    return {
        archivers: requestsCopy,
    };
}
exports.getTxs = getTxs;
function validateRecordTypes(rec) {
    let err = (0, utils_1.validateTypes)(rec, { joinedArchivers: 'a' });
    if (err)
        return err;
    for (const item of rec.joinedArchivers) {
        err = (0, utils_1.validateTypes)(item, {
            publicKey: 's',
            ip: 's',
            port: 'n',
            curvePk: 's',
        });
        if (err)
            return 'in joinedArchivers array ' + err;
    }
    return '';
}
exports.validateRecordTypes = validateRecordTypes;
function updateRecord(txs, record) {
    // Add joining archivers to the cycle record
    const joinedArchivers = txs.archivers
        .filter((request) => request.requestType === lib_types_1.P2P.ArchiversTypes.RequestTypes.JOIN)
        .map((joinRequest) => joinRequest.nodeInfo);
    // Add leaving archivers to the cycle record
    const leavingArchivers = txs.archivers
        .filter((request) => request.requestType === lib_types_1.P2P.ArchiversTypes.RequestTypes.LEAVE)
        .map((leaveRequest) => leaveRequest.nodeInfo);
    // Remove archivers that are not in the allowed list
    for (const existingArchiver of exports.archivers.values()) {
        if (allowedArchivers.length > 0 &&
            !allowedArchivers.some((allowed) => allowed.publicKey === existingArchiver.publicKey) &&
            !leavingArchivers.some((leaving) => leaving.publicKey === existingArchiver.publicKey)) {
            leavingArchivers.push(existingArchiver);
        }
    }
    if (logger_1.logFlags.console)
        console.log(`Archiver before updating record: Cycle ${CycleCreator.currentCycle}, Quarter: ${CycleCreator.currentQuarter}`, joinedArchivers, leavingArchivers);
    record.joinedArchivers = joinedArchivers.sort((a, b) => (a.publicKey > b.publicKey ? 1 : -1));
    record.leavingArchivers = leavingArchivers.sort((a, b) => (a.publicKey > b.publicKey ? 1 : -1));
    if (logger_1.logFlags.console)
        console.log(`Archiver after updating record: Cycle ${CycleCreator.currentCycle}, Quarter: ${CycleCreator.currentQuarter}`, record);
    // resetLeaveRequests()
}
exports.updateRecord = updateRecord;
function parseRecord(record) {
    // Update our archivers list
    updateArchivers(record);
    // Since we don't touch the NodeList, return an empty Change
    return {
        added: [],
        removed: [],
        updated: [],
    };
}
exports.parseRecord = parseRecord;
/** Not used by Archivers */
function sendRequests() { }
exports.sendRequests = sendRequests;
/** Not used by Archivers */
function queueRequest() { }
exports.queueRequest = queueRequest;
/** Original Functions */
function resetJoinRequests() {
    joinRequests = [];
}
exports.resetJoinRequests = resetJoinRequests;
function resetLeaveRequests() {
    leaveRequests = [];
}
exports.resetLeaveRequests = resetLeaveRequests;
function addArchiverJoinRequest(joinRequest, tracker, gossip = true) {
    // validate input
    let err = (0, utils_1.validateTypes)(joinRequest, { nodeInfo: 'o', requestType: 's', requestTimestamp: 'n', sign: 'o' });
    if (err) {
        warn('addJoinRequest: bad joinRequest ' + err);
        return { success: false, reason: 'bad joinRequest ' + err };
    }
    err = (0, utils_1.validateTypes)(joinRequest.nodeInfo, {
        curvePk: 's',
        ip: 's',
        port: 'n',
        publicKey: 's',
    });
    if (err) {
        warn('addJoinRequest: bad joinRequest.nodeInfo ' + err);
        return { success: false, reason: 'bad joinRequest ' + err };
    }
    if (joinRequest.requestType !== lib_types_1.P2P.ArchiversTypes.RequestTypes.JOIN) {
        warn('addJoinRequest: invalid joinRequest.requestType');
        return { success: false, reason: 'invalid joinRequest.requestType' };
    }
    err = (0, utils_1.validateTypes)(joinRequest.sign, { owner: 's', sig: 's' });
    if (err) {
        warn('addJoinRequest: bad joinRequest.sign ' + err);
        return { success: false, reason: 'bad joinRequest.sign ' + err };
    }
    if (!Context_1.crypto.verify(joinRequest, joinRequest.nodeInfo.publicKey)) {
        warn('addJoinRequest: bad signature');
        return { success: false, reason: 'bad signature ' };
    }
    if (exports.archivers.get(joinRequest.nodeInfo.publicKey)) {
        warn('addJoinRequest: This archiver is already in the active archiver list');
        return { success: false, reason: 'This archiver is already in the active archiver list' };
    }
    const existingJoinRequest = joinRequests.find((j) => j.nodeInfo.publicKey === joinRequest.nodeInfo.publicKey);
    if (existingJoinRequest) {
        warn('addJoinRequest: This archiver join request already exists');
        return { success: false, reason: 'This archiver join request already exists' };
    }
    if (Context.config.p2p.forceBogonFilteringOn) {
        if ((0, checkIP_1.isBogonIP)(joinRequest.nodeInfo.ip)) {
            warn('addJoinRequest: This archiver join request uses a bogon IP');
            return { success: false, reason: 'This archiver join request is a bogon IP' };
        }
    }
    const isPublicKeyWhitelisted = allowedArchivers.some((archiver) => archiver.publicKey === joinRequest.nodeInfo.publicKey);
    if (!isPublicKeyWhitelisted && exports.archivers.size > 0) {
        warn('addJoinRequest: Archiver not found in the allowed list');
        return { success: false, reason: 'Archiver not found in the allowed list' };
    }
    if (exports.archivers.size > 0) {
        // Check the archiver version from dapp
        if (Context.config.p2p.validateArchiverAppData) {
            const validationResponse = validateArchiverAppData(joinRequest);
            if (validationResponse && !validationResponse.success)
                return validationResponse;
        }
        // Check if the archiver request timestamp is within the acceptable timestamp range (after current cycle, before next cycle)
        const requestTimestamp = joinRequest.requestTimestamp;
        const cycleDuration = CycleChain_1.newest.duration;
        const cycleStart = CycleChain_1.newest.start;
        const currentCycleStartTime = (cycleStart + cycleDuration) * 1000;
        const nextCycleStartTime = (cycleStart + 2 * cycleDuration) * 1000;
        if (requestTimestamp < currentCycleStartTime) {
            warn('addJoinRequest: This archiver join request timestamp is earlier than acceptable timestamp range');
            return {
                success: false,
                reason: 'This archiver join request timestamp is earlier than acceptable timestamp range',
            };
        }
        if (requestTimestamp > nextCycleStartTime) {
            warn('addJoinRequest: This archiver join request timestamp exceeds acceptable timestamp range');
            return {
                success: false,
                reason: 'This archiver join request timestamp exceeds acceptable timestamp range',
            };
        }
        // Get the consensus radius of the network
        try {
            const { shardGlobals: { consensusRadius }, } = Context.stateManager.getCurrentCycleShardData();
            if (exports.archivers.size >= consensusRadius * Context_1.config.p2p.maxArchiversSubscriptionPerNode) {
                warn('addJoinRequest: This archiver cannot join as max archivers limit has been reached');
                return { success: false, reason: 'Max number of archivers limit reached' };
            }
        }
        catch (e) {
            warn('addJoinRequest: Failed to get consensus radius', e);
            return { success: false, reason: 'This node is not ready to accept this request!' };
        }
    }
    joinRequests.push(joinRequest);
    if (logger_1.logFlags.console)
        console.log(`Join request added in cycle ${CycleCreator.currentCycle}, quarter ${CycleCreator.currentQuarter}`, joinRequest);
    if (gossip === true) {
        Comms.sendGossip('joinarchiver', joinRequest, tracker, null, NodeList.byIdOrder, true);
    }
    return { success: true };
}
exports.addArchiverJoinRequest = addArchiverJoinRequest;
function validateArchiverAppData(joinRequest) {
    if (typeof Context_1.shardus.app.validateArchiverJoinRequest === 'function') {
        try {
            const validationResponse = Context_1.shardus.app.validateArchiverJoinRequest(joinRequest);
            if (validationResponse.success !== true) {
                error(`Validation of Archiver join request data failed due to ${validationResponse.reason || 'unknown reason'}`);
                nestedCounters_1.nestedCountersInstance.countEvent('Archiver', `Join-reject-dapp`);
                return {
                    success: validationResponse.success,
                    reason: validationResponse.reason,
                };
            }
            return { success: true };
        }
        catch (e) {
            warn(`shardus.app.validateArchiverJoinRequest failed due to ${e}`);
            nestedCounters_1.nestedCountersInstance.countEvent('Archiver', `Join-reject-ex ${e}`);
            return {
                success: false,
                reason: `Could not validate archiver join request due to Error`,
            };
        }
    }
}
function addLeaveRequest(leaveRequest, tracker, gossip = true) {
    // validate input
    let err = (0, utils_1.validateTypes)(leaveRequest, { nodeInfo: 'o', requestType: 's', sign: 'o' });
    if (err) {
        warn('addLeaveRequest: bad leaveRequest ' + err);
        return { success: false, reason: 'bad leaveRequest ' + err };
    }
    err = (0, utils_1.validateTypes)(leaveRequest.nodeInfo, {
        curvePk: 's',
        ip: 's',
        port: 'n',
        publicKey: 's',
    });
    if (err) {
        warn('addLeaveRequest: bad leaveRequest.nodeInfo ' + err);
        return { success: false, reason: 'bad leaveRequest.nodeInfo ' + err };
    }
    if (leaveRequest.requestType !== lib_types_1.P2P.ArchiversTypes.RequestTypes.LEAVE) {
        warn('addLeaveRequest: invalid leaveRequest.requestType');
        return { success: false, reason: 'invalid leaveRequest.requestType' };
    }
    err = (0, utils_1.validateTypes)(leaveRequest.sign, { owner: 's', sig: 's' });
    if (err) {
        warn('addLeaveRequest: bad leaveRequest.sign ' + err);
        return { success: false, reason: 'bad leaveRequest.sign ' + err };
    }
    if (!Context_1.crypto.verify(leaveRequest, leaveRequest.nodeInfo.publicKey)) {
        warn('addLeaveRequest: bad signature');
        return { success: false, reason: 'bad signature' };
    }
    if (!exports.archivers.get(leaveRequest.nodeInfo.publicKey)) {
        warn('addLeaveRequest: Not a valid archiver to be sending leave request, archiver was not found in active archiver list');
        return {
            success: false,
            reason: 'Not a valid archiver to be sending leave request, archiver was not found in active archiver list',
        };
    }
    const existingLeaveRequest = leaveRequests.find((j) => j.nodeInfo.publicKey === leaveRequest.nodeInfo.publicKey);
    if (existingLeaveRequest) {
        warn('addLeaveRequest: This archiver leave request already exists');
        return { success: false, reason: 'This archiver leave request already exists' };
    }
    // Check if the archiver request timestamp is within the acceptable timestamp range (after current cycle, before next cycle)
    const requestTimestamp = leaveRequest.requestTimestamp;
    const cycleDuration = CycleChain_1.newest.duration;
    const cycleStart = CycleChain_1.newest.start;
    const currentCycleStartTime = (cycleStart + cycleDuration) * 1000;
    const nextCycleStartTime = (cycleStart + 2 * cycleDuration) * 1000;
    if (requestTimestamp < currentCycleStartTime) {
        warn('addLeaveRequest: This archiver leave request timestamp is earlier than acceptable timestamp range');
        return {
            success: false,
            reason: 'This archiver leave request timestamp is earlier than acceptable timestamp range',
        };
    }
    if (requestTimestamp > nextCycleStartTime) {
        warn('addLeaveRequest: This archiver leave request timestamp exceeds acceptable timestamp range');
        return {
            success: false,
            reason: 'This archiver leave request timestamp exceeds acceptable timestamp range',
        };
    }
    leaveRequests.push(leaveRequest);
    if (logger_1.logFlags.console)
        console.log('adding leave requests', leaveRequests);
    if (gossip === true) {
        Comms.sendGossip('leavingarchiver', leaveRequest, tracker, null, NodeList.byIdOrder, true);
    }
    return { success: true };
}
exports.addLeaveRequest = addLeaveRequest;
function getArchiverUpdates() {
    return joinRequests;
}
exports.getArchiverUpdates = getArchiverUpdates;
function removeArchiverByPublicKey(publicKey) {
    const archiverInfo = exports.archivers.get(publicKey);
    removeArchiver(archiverInfo);
}
exports.removeArchiverByPublicKey = removeArchiverByPublicKey;
function removeArchiver(nodeInfo) {
    exports.archivers.delete(nodeInfo.publicKey);
    removeDataRecipient(nodeInfo.publicKey);
    removeArchiverConnection(nodeInfo.publicKey);
    leaveRequests = leaveRequests.filter((request) => request.nodeInfo.publicKey !== nodeInfo.publicKey);
}
exports.removeArchiver = removeArchiver;
function updateArchivers(record) {
    // Update archiversList
    for (const nodeInfo of record.leavingArchivers) {
        removeArchiver(nodeInfo);
    }
    for (const nodeInfo of record.joinedArchivers) {
        exports.archivers.set(nodeInfo.publicKey, nodeInfo);
    }
}
exports.updateArchivers = updateArchivers;
// Add data type of dataRequests used in experimentalSnapshot
function addDataRecipient(nodeInfo, dataRequests, overrideLastSentCycle) {
    if (logger_1.logFlags.console)
        console.log('Adding data recipient..', arguments);
    if (Context_1.config.p2p.experimentalSnapshot && Context_1.config.features.archiverDataSubscriptionsUpdate) {
        // This is to flush out previous archiver connections when it first activated
        if (!exports.archiverDataSubscriptionsUpdateFeatureActivated) {
            console.log('archiverDataSubscriptionsUpdateFeatureActivated', exports.connectedSockets, exports.recipients);
            for (const [key, value] of Object.entries(exports.connectedSockets)) {
                removeArchiverConnection(key);
            }
            for (const [key, value] of exports.recipients) {
                exports.recipients.delete(key);
            }
            exports.archiverDataSubscriptionsUpdateFeatureActivated = true;
            console.log('archiverDataSubscriptionsUpdateFeatureActivated', exports.connectedSockets, exports.recipients);
        }
        const recipient = {
            nodeInfo,
            dataRequestCycle: dataRequests['dataRequestCycle'],
            curvePk: Context_1.crypto.convertPublicKeyToCurve(nodeInfo.publicKey),
        };
        if (overrideLastSentCycle)
            lastSentCycle = overrideLastSentCycle;
        if (lastSentCycle > recipient.dataRequestCycle) {
            // If the dataRequestCycle is behind many cycles, Set it to 10 cycles before the current cycle
            if (lastSentCycle - recipient.dataRequestCycle > 10)
                lastSentCycle = lastSentCycle - 10;
            else
                lastSentCycle = recipient.dataRequestCycle;
        }
        if (logger_1.logFlags.console)
            console.log(`dataRequests: ${nodeInfo.ip}:${nodeInfo.port}`);
        exports.recipients.set(nodeInfo.publicKey, recipient);
        return;
    }
    const recipient = {
        nodeInfo,
        // TODO: dataRequest should be an array
        dataRequests: dataRequests,
        curvePk: Context_1.crypto.convertPublicKeyToCurve(nodeInfo.publicKey),
    };
    if (logger_1.logFlags.console)
        console.log('dataRequests: ', recipient.dataRequests);
    exports.recipients.set(nodeInfo.publicKey, recipient);
}
exports.addDataRecipient = addDataRecipient;
function getArchiversList() {
    return [...exports.archivers.values()];
}
exports.getArchiversList = getArchiversList;
async function forwardReceipts() {
    if (!Context_1.config.p2p.experimentalSnapshot)
        return;
    profiler_1.profilerInstance.scopedProfileSectionStart('forwardReceipts');
    let pingNeeded = true;
    // Ping the archivers if the last ping/forward receipts was 5 seconds ago
    const LEAST_LAST_PING_TIME_MS = 3000;
    if (Context_1.config.p2p.instantForwardReceipts &&
        (0, network_1.shardusGetTime)() - Context_1.stateManager.transactionQueue.receiptsForwardedTimestamp < LEAST_LAST_PING_TIME_MS) {
        pingNeeded = false;
    }
    // TODO: add a new type for receipt
    const responses = {};
    responses.RECEIPT = []; // It's just to ping the archivers with empty receipts
    const newArchiversToForward = [];
    const stillConnectedArchivers = [];
    for (const [publicKey, recipient] of exports.recipients) {
        if (Context_1.config.p2p.instantForwardReceipts)
            if (!lastTimeForwardedArchivers.includes(publicKey)) {
                newArchiversToForward.push(publicKey);
            }
            else
                stillConnectedArchivers.push(publicKey);
        if (pingNeeded)
            Context_1.stateManager.transactionQueue.receiptsForwardedTimestamp = (0, network_1.shardusGetTime)();
        else
            continue;
        if (logger_1.logFlags.console)
            console.log('pingNeeded', pingNeeded, Context_1.stateManager.transactionQueue.receiptsForwardedTimestamp);
        forwardDataToSubscribedArchivers(responses, publicKey, recipient);
    }
    if (Context_1.config.p2p.instantForwardReceipts) {
        if (newArchiversToForward.length > 0) {
            const receipts = Context_1.stateManager.transactionQueue.getReceiptsToForward();
            if (receipts && receipts.length > 0) {
                responses.RECEIPT = receipts;
                if (logger_1.logFlags.console)
                    console.log('newArchiversToForward', newArchiversToForward);
                for (let publicKey of newArchiversToForward) {
                    if (logger_1.logFlags.console)
                        console.log('Sending last 15s receipts to newly subscribed archivers', publicKey);
                    const recipient = exports.recipients.get(publicKey);
                    if (!recipient)
                        continue;
                    forwardDataToSubscribedArchivers(responses, publicKey, recipient);
                }
            }
        }
        lastTimeForwardedArchivers = [...newArchiversToForward, ...stillConnectedArchivers];
        if (logger_1.logFlags.console)
            console.log('lastTimeForwardedArchivers', lastTimeForwardedArchivers);
    }
    Context_1.stateManager.transactionQueue.resetReceiptsToForward();
    profiler_1.profilerInstance.scopedProfileSectionEnd('forwardReceipts');
}
async function forwardDataToSubscribedArchivers(responses, publicKey, recipient) {
    const dataResponse = {
        publicKey: Context_1.crypto.getPublicKey(),
        responses,
        recipient: publicKey,
    };
    // Tag dataResponse
    const taggedDataResponse = Context_1.crypto.tag(dataResponse, recipient.curvePk);
    if (logger_1.logFlags.console)
        console.log('Sending data to subscribed archivers', taggedDataResponse);
    try {
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            info('Sending data to subscribed archivers: ', lib_types_2.Utils.safeStringify(taggedDataResponse));
        if (Context_1.io.sockets.sockets[exports.connectedSockets[publicKey]]) {
            if (logger_1.logFlags.console)
                console.log('Forwarded Archiver', recipient.nodeInfo.ip + ':' + recipient.nodeInfo.port);
            Context_1.io.sockets.sockets[exports.connectedSockets[publicKey]].emit('DATA', lib_types_2.Utils.safeStringify(taggedDataResponse));
            /* prettier-ignore */ if (nestedCounters_1.nestedCountersInstance)
                nestedCounters_1.nestedCountersInstance.countEvent('forwardDataToSubscribedArchivers', `archiver connected — ${publicKey}`);
        }
        else {
            warn(`Subscribed Archiver ${publicKey} is not connected over socket connection`);
            // Call into LostArchivers to report Archiver as lost
            /* prettier-ignore */ if (nestedCounters_1.nestedCountersInstance)
                nestedCounters_1.nestedCountersInstance.countEvent('forwardDataToSubscribedArchivers', `archiver not connected — ${publicKey}`);
            (0, functions_1.reportLostArchiver)(publicKey, 'forwardDataToSubscribedArchivers() error');
        }
    }
    catch (e) {
        error('Run into issue in forwarding data to - ', publicKey, e);
        // Call into LostArchivers to report Archiver as lost
        /* prettier-ignore */ if (nestedCounters_1.nestedCountersInstance)
            nestedCounters_1.nestedCountersInstance.countEvent('forwardDataToSubscribedArchivers', `unknown error — ${publicKey}`);
        (0, functions_1.reportLostArchiver)(publicKey, 'forwardDataToSubscribedArchivers() error');
    }
}
async function instantForwardReceipts(receipts) {
    if (!Context_1.config.p2p.experimentalSnapshot)
        return;
    profiler_1.profilerInstance.scopedProfileSectionStart('instantForwardReceipts');
    // TODO: add a new type for receipt
    const responses = {};
    responses.RECEIPT = [...receipts];
    for (const [publicKey, recipient] of exports.recipients) {
        forwardDataToSubscribedArchivers(responses, publicKey, recipient);
    }
    if (nestedCounters_1.nestedCountersInstance)
        nestedCounters_1.nestedCountersInstance.countEvent('Archiver', 'instantForwardReceipts');
    profiler_1.profilerInstance.scopedProfileSectionEnd('instantForwardReceipts');
}
exports.instantForwardReceipts = instantForwardReceipts;
async function instantForwardOriginalTxData(originalTxData) {
    if (!Context_1.config.p2p.experimentalSnapshot)
        return;
    profiler_1.profilerInstance.scopedProfileSectionStart('instantForwardOriginalTxData');
    // TODO: add a new type for receipt
    const responses = {};
    responses.ORIGINAL_TX_DATA = [originalTxData];
    for (const [publicKey, recipient] of exports.recipients) {
        forwardDataToSubscribedArchivers(responses, publicKey, recipient);
    }
    nestedCounters_1.nestedCountersInstance.countEvent('Archiver', 'instantForwardOriginalTxData');
    profiler_1.profilerInstance.scopedProfileSectionEnd('instantForwardOriginalTxData');
}
exports.instantForwardOriginalTxData = instantForwardOriginalTxData;
/**
 * This function is used by the checkNetworkStopped feature to check if the
 * network is down by checking if all Archivers are down. If so, it causes
 * the node to apoptosize itself.
 */
async function hasNetworkStopped() {
    // If network check still in progress, return
    if (networkCheckInProgress)
        return;
    networkCheckInProgress = true;
    try {
        // Get a randomized list of all Archivers
        const shuffledArchivers = (0, utils_1.shuffleMapIterator)(exports.archivers);
        // Loop through them and check their /nodelist endpoint for a response
        for (const archiver of shuffledArchivers) {
            const response = await getFromArchiver(archiver, '/nodelist', 'hasNetworkStopped() could not fetch nodelist');
            // If any one of them responds, return false
            if (response.isOk() && response.value.data) {
                return false;
            }
            else if (response.isErr()) {
                warn(`hasNetworkStopped(): network error: ${response.error.message}`);
            }
        }
        // If all of them do not respond, initiate apoptosis
        return true;
    }
    finally {
        networkCheckInProgress = false;
    }
}
async function forwardAccounts(data) {
    if (!Context_1.config.p2p.experimentalSnapshot)
        return;
    const responses = {};
    responses.ACCOUNT = data;
    if (exports.recipients.size === 0) {
        console.log('No Connected Archiver To Forward!');
    }
    for (const [publicKey, recipient] of exports.recipients) {
        const dataResponse = {
            publicKey: Context_1.crypto.getPublicKey(),
            responses,
            recipient: publicKey,
        };
        // Tag dataResponse
        const taggedDataResponse = Context_1.crypto.tag(dataResponse, recipient.curvePk);
        if (logger_1.logFlags.console)
            console.log('Sending accounts to archivers', taggedDataResponse);
        try {
            if (Context_1.io.sockets.sockets[exports.connectedSockets[publicKey]]) {
                Context_1.io.sockets.sockets[exports.connectedSockets[publicKey]].emit('DATA', lib_types_2.Utils.safeStringify(taggedDataResponse));
                console.log(`forward Accounts Successfully`);
            }
        }
        catch (e) {
            error('Run into error in forwarding accounts', e);
            // Call into LostArchivers to report Archiver as lost
            (0, functions_1.reportLostArchiver)(publicKey, 'forwardAccounts() error');
        }
    }
}
exports.forwardAccounts = forwardAccounts;
function removeDataRecipient(publicKey) {
    if (exports.recipients.has(publicKey)) {
        if (logger_1.logFlags.console)
            console.log('Removing data recipient', publicKey);
        exports.recipients.delete(publicKey);
    }
    else {
        if (logger_1.logFlags.console)
            console.log(`Data recipient ${publicKey} is already removed`);
    }
}
exports.removeDataRecipient = removeDataRecipient;
function sendData() {
    if (logger_1.logFlags.console)
        console.log('Recient List before sending data');
    if (logger_1.logFlags.console)
        console.log(exports.recipients);
    const responses = {};
    if (Context_1.config.p2p.experimentalSnapshot && Context_1.config.features.archiverDataSubscriptionsUpdate) {
        if (exports.recipients.size === 0) {
            lastSentCycle = CycleCreator.currentCycle;
            return;
        }
        // Get latest cycles since lastSentCycle
        const cycleRecords = (0, CycleChain_1.getCycleChain)(lastSentCycle + 1);
        const cyclesWithMarker = [];
        for (let i = 0; i < cycleRecords.length; i++) {
            if (logger_1.logFlags.console)
                console.log('cycleRecords counter to sent to the archiver', cycleRecords[i].counter);
            cyclesWithMarker.push({
                ...cycleRecords[i],
                marker: (0, CycleChain_1.computeCycleMarker)(cycleRecords[i]),
                certificates: CycleCreator.getBestCycleCerts(cycleRecords[i].counter),
            });
        }
        // Update lastSentCycle
        if (cyclesWithMarker.length > 0) {
            lastSentCycle = cyclesWithMarker[cyclesWithMarker.length - 1].counter;
        }
        // Add to responses
        responses.CYCLE = cyclesWithMarker;
        for (const [publicKey, recipient] of exports.recipients) {
            const dataResponse = {
                publicKey: Context_1.crypto.getPublicKey(),
                responses,
                recipient: publicKey,
            };
            // Tag dataResponse
            const taggedDataResponse = Context_1.crypto.tag(dataResponse, recipient.curvePk);
            try {
                // console.log('connected socketes', publicKey, connectedSockets)
                if (Context_1.io.sockets.sockets[exports.connectedSockets[publicKey]])
                    Context_1.io.sockets.sockets[exports.connectedSockets[publicKey]].emit('DATA', lib_types_2.Utils.safeStringify(taggedDataResponse));
                else {
                    warn(`Subscribed Archiver ${publicKey} is not connected over socket connection`);
                    // Call into LostArchivers to report Archiver as lost
                    (0, functions_1.reportLostArchiver)(publicKey, 'sendData() error');
                }
            }
            catch (e) {
                error('Run into issue in forwarding cycles data', e);
                // Call into LostArchivers to report Archiver as lost
                (0, functions_1.reportLostArchiver)(publicKey, 'sendData() error');
            }
        }
        return;
    }
    for (const [publicKey, recipient] of exports.recipients) {
        // const recipientUrl = `http://${recipient.nodeInfo.ip}:${recipient.nodeInfo.port}/newdata`
        const responses = {};
        for (const request of recipient.dataRequests) {
            switch (request.type) {
                case lib_types_1.P2P.SnapshotTypes.TypeNames.CYCLE: {
                    // Identify request type
                    const typedRequest = request;
                    // Get latest cycles since lastData
                    const cycleRecords = (0, CycleChain_1.getCycleChain)(typedRequest.lastData + 1);
                    const cyclesWithMarker = [];
                    for (let i = 0; i < cycleRecords.length; i++) {
                        cyclesWithMarker.push({
                            ...cycleRecords[i],
                            marker: (0, CycleChain_1.computeCycleMarker)(cycleRecords[i]),
                        });
                    }
                    // Update lastData
                    if (cyclesWithMarker.length > 0) {
                        typedRequest.lastData = cyclesWithMarker[cyclesWithMarker.length - 1].counter;
                    }
                    // Add to responses
                    responses.CYCLE = cyclesWithMarker;
                    break;
                }
                case lib_types_1.P2P.SnapshotTypes.TypeNames.STATE_METADATA: {
                    // Identify request type
                    const typedRequest = request;
                    if (logger_1.logFlags.console)
                        console.log('STATE_METADATA typedRequest', typedRequest);
                    // Get latest state hash data since lastData
                    const stateHashes = (0, snapshot_1.getStateHashes)(typedRequest.lastData + 1);
                    const receiptHashes = (0, snapshot_1.getReceiptHashes)(typedRequest.lastData + 1);
                    const summaryHashes = (0, snapshot_1.getSummaryHashes)(typedRequest.lastData + 1);
                    // Update lastData
                    if (stateHashes.length > 0) {
                        typedRequest.lastData = stateHashes[stateHashes.length - 1].counter;
                    }
                    const metadata = {
                        counter: typedRequest.lastData >= 0 ? typedRequest.lastData : 0,
                        stateHashes,
                        receiptHashes,
                        summaryHashes,
                    };
                    // console.log('Metadata to send', metadata)
                    // console.log('Metadata to send: summary hashes', summaryHashes)
                    // Add to responses
                    responses.STATE_METADATA = [metadata];
                    break;
                }
                default:
            }
        }
        const dataResponse = {
            publicKey: Context_1.crypto.getPublicKey(),
            responses,
            recipient: publicKey,
        };
        // Tag dataResponse
        const taggedDataResponse = Context_1.crypto.tag(dataResponse, recipient.curvePk);
        // if(logFlags.console) {
        //   console.log(
        //     `Sending data for cycle ${
        //       getNewest().counter
        //     } to archiver ${recipientUrl}`,
        //     recipient.curvePk
        //   )
        // }
        try {
            // console.log('connected socketes', publicKey, connectedSockets)
            if (Context_1.io.sockets.sockets[exports.connectedSockets[publicKey]])
                Context_1.io.sockets.sockets[exports.connectedSockets[publicKey]].emit('DATA', lib_types_2.Utils.safeStringify(taggedDataResponse));
            else {
                warn(`Subscribed Archiver ${publicKey} is not connected over socket connection`);
                // Call into LostArchivers to report Archiver as lost
                (0, functions_1.reportLostArchiver)(publicKey, 'sendData() error');
            }
        }
        catch (e) {
            error('Run into issue in forwarding cycles data', e);
            // Call into LostArchivers to report Archiver as lost
            (0, functions_1.reportLostArchiver)(publicKey, 'sendData() error');
        }
        // http
        //   .post(recipientUrl, taggedDataResponse)
        //   .then((dataKeepAlive) => {
        //     if (dataKeepAlive.keepAlive === false) {
        //       // Remove recipient from dataRecipients
        //       removeDataRecipient(recipient.nodeInfo.publicKey)
        //     }
        //   })
        //   .catch((err) => {
        //     // Remove recipient from dataRecipients
        //     warn('Error sending data to dataRecipient.', err)
        //     removeDataRecipient(recipient.nodeInfo.publicKey)
        //   })
    }
}
exports.sendData = sendData;
function getRefreshedArchivers(record) {
    let refreshedArchivers = getArchiversList();
    // if (leaveRequests.length > 0) {
    //   for (const archiverInfo of leaveRequests) {
    //     refreshedArchivers = refreshedArchivers.filter(
    //       (archiver) => archiver.publicKey !== archiverInfo.nodeInfo.publicKey
    //     )
    //   }
    // }
    if (record.leavingArchivers) {
        for (const archiverInfo of record.leavingArchivers) {
            refreshedArchivers = refreshedArchivers.filter((archiver) => archiver.publicKey !== archiverInfo.publicKey);
        }
    }
    return refreshedArchivers;
}
exports.getRefreshedArchivers = getRefreshedArchivers;
function addArchiverConnection(publicKey, socketId) {
    exports.connectedSockets[publicKey] = socketId;
}
exports.addArchiverConnection = addArchiverConnection;
function removeArchiverConnection(publicKey) {
    if (Context_1.io.sockets.sockets[exports.connectedSockets[publicKey]]) {
        Context_1.io.sockets.sockets[exports.connectedSockets[publicKey]].disconnect();
    }
    delete exports.connectedSockets[publicKey];
}
exports.removeArchiverConnection = removeArchiverConnection;
function registerRoutes() {
    Context_1.network.registerExternalPost('joinarchiver', (req, res) => {
        const err = (0, utils_1.validateTypes)(req, { body: 'o' });
        if (err) {
            warn(`joinarchiver: bad req ${err}`);
            res.json({ success: false, error: err });
            return;
        }
        const joinRequest = req.body;
        if (logger_1.logFlags.p2pNonFatal)
            info(`Archiver join request received: ${lib_types_2.Utils.safeStringify(joinRequest)}`);
        const accepted = addArchiverJoinRequest(joinRequest);
        if (!accepted.success) {
            warn('Archiver join request not accepted.');
            res.json({ success: false, error: `Archiver join request rejected! ${accepted.reason}` });
            return;
        }
        if (logger_1.logFlags.p2pNonFatal)
            info('Archiver join request accepted!');
        res.json({ success: true });
        return;
    });
    Context_1.network.registerExternalPost('leavingarchivers', (req, res) => {
        const err = (0, utils_1.validateTypes)(req, { body: 'o' });
        if (err) {
            warn(`leavingarchivers: bad req ${err}`);
            res.json({ success: false, error: err });
            return;
        }
        const leaveRequest = req.body;
        if (logger_1.logFlags.p2pNonFatal)
            info(`Archiver leave request received: ${lib_types_2.Utils.safeStringify(leaveRequest)}`);
        const accepted = addLeaveRequest(leaveRequest);
        if (!accepted.success) {
            warn('Archiver leave request not accepted.');
            res.json({ success: false, error: `Archiver leave request rejected! ${accepted.reason}` });
            return;
        }
        if (logger_1.logFlags.p2pNonFatal)
            info('Archiver leave request accepted!');
        res.json({ success: true });
        return;
    });
    Comms.registerGossipHandler('joinarchiver', async (payload, sender, tracker) => {
        profiler_1.profilerInstance.scopedProfileSectionStart('joinarchiver');
        try {
            if (logger_1.logFlags.console)
                console.log('Join request gossip received:', payload);
            const accepted = await addArchiverJoinRequest(payload, tracker, false);
            if (logger_1.logFlags.console) {
                console.log('This join request is new. Should forward the join request');
                console.log('join request gossip accepted', accepted);
            }
            if (!accepted.success)
                return warn('Archiver join request not accepted.');
            if (logger_1.logFlags.p2pNonFatal)
                info('Archiver join request accepted!');
            Comms.sendGossip('joinarchiver', payload, tracker, sender, NodeList.byIdOrder, false);
        }
        finally {
            profiler_1.profilerInstance.scopedProfileSectionEnd('joinarchiver');
        }
    });
    Comms.registerGossipHandler('leavingarchiver', async (payload, sender, tracker) => {
        if (payload === undefined || payload === null)
            return warn('Archiver leave payload empty.');
        if (sender === undefined || sender === null)
            return warn('Archiver leave sender empty.');
        if (tracker === undefined || tracker === null)
            return warn('Archiver leave tracker empty.');
        profiler_1.profilerInstance.scopedProfileSectionStart('leavingarchiver');
        try {
            if (NodeList.nodes.get(sender) == null) {
                return warn('Archiver leave gossip came from invalid consensor');
            }
            if (logger_1.logFlags.console)
                console.log('Leave request gossip received:', payload);
            const accepted = await addLeaveRequest(payload, tracker, false);
            if (!accepted.success)
                return warn('Archiver leave request not accepted.');
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                info('Archiver leave request accepted!');
            Comms.sendGossip('leavingarchiver', payload, tracker, sender, NodeList.byIdOrder, false);
        }
        finally {
            profiler_1.profilerInstance.scopedProfileSectionEnd('leavingarchiver');
        }
    });
    Context_1.network.registerExternalPost('requestdata', (req, res) => {
        let err = (0, utils_1.validateTypes)(req, { body: 'o' });
        if (err) {
            /* prettier-ignore */ if (logger_1.logFlags.error)
                warn(`requestdata: bad req ${err}`);
            res.json({ success: false, error: err });
            return;
        }
        err = (0, utils_1.validateTypes)(req.body, {
            tag: 's',
        });
        if (err) {
            /* prettier-ignore */ if (logger_1.logFlags.error)
                warn(`requestdata: bad req.body ${err}`);
            res.json({ success: false, error: err });
            return;
        }
        const dataRequest = req.body;
        if (logger_1.logFlags.p2pNonFatal)
            info('dataRequest received', lib_types_2.Utils.safeStringify(dataRequest));
        const foundArchiver = exports.archivers.get(dataRequest.publicKey);
        if (!foundArchiver) {
            const archiverNotFoundErr = 'Archiver not found in list';
            /* prettier-ignore */ if (logger_1.logFlags.error)
                warn(archiverNotFoundErr);
            res.json({ success: false, error: archiverNotFoundErr });
            return;
        }
        const invalidTagErr = 'Tag is invalid';
        const archiverCurvePk = Context_1.crypto.convertPublicKeyToCurve(foundArchiver.publicKey);
        if (!Context_1.crypto.authenticate(dataRequest, archiverCurvePk)) {
            /* prettier-ignore */ if (logger_1.logFlags.error)
                warn(invalidTagErr);
            res.json({ success: false, error: invalidTagErr });
            return;
        }
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            info('Tag in data request is valid');
        if (Context_1.config.p2p.experimentalSnapshot && Context_1.config.features.archiverDataSubscriptionsUpdate) {
            if (dataRequest.dataRequestType === DataRequestTypes.SUBSCRIBE) {
                // if the archiver is already in the recipients list, remove it first
                if (dataRequest.nodeInfo && exports.recipients.has(dataRequest.nodeInfo.publicKey)) {
                    removeArchiverConnection(dataRequest.nodeInfo.publicKey);
                    exports.recipients.delete(dataRequest.nodeInfo.publicKey);
                }
                if (exports.recipients.size >= Context_1.config.p2p.maxArchiversSubscriptionPerNode) {
                    const maxArchiversSupportErr = 'Max archivers support reached';
                    warn(maxArchiversSupportErr);
                    res.json({ success: false, error: maxArchiversSupportErr });
                    return;
                }
                addDataRecipient(dataRequest.nodeInfo, dataRequest);
            }
            if (dataRequest.dataRequestType === DataRequestTypes.UNSUBSCRIBE) {
                removeDataRecipient(dataRequest.publicKey);
                removeArchiverConnection(dataRequest.publicKey);
            }
            res.json({ success: true });
            return;
        }
        delete dataRequest.publicKey;
        delete dataRequest.tag;
        const dataRequestCycle = dataRequest.dataRequestCycle;
        const dataRequestStateMetaData = dataRequest.dataRequestStateMetaData;
        const dataRequests = [];
        if (dataRequestCycle) {
            dataRequests.push(dataRequestCycle);
        }
        if (dataRequestStateMetaData) {
            dataRequests.push(dataRequestStateMetaData);
        }
        if (dataRequests.length > 0) {
            addDataRecipient(dataRequest.nodeInfo, dataRequests);
        }
        res.json({ success: true });
    });
    Context_1.network.registerExternalPost('querydata', (req, res) => {
        let err = (0, utils_1.validateTypes)(req, { body: 'o' });
        if (err) {
            warn(`querydata: bad req ${err}`);
            res.json({ success: false, error: err });
            return;
        }
        err = (0, utils_1.validateTypes)(req.body, {
            publicKey: 's',
            tag: 's',
            nodeInfo: 'o',
        });
        if (err) {
            warn(`querydata: bad req.body ${err}`);
            res.json({ success: false, error: err });
            return;
        }
        // [TODO] Authenticate tag
        const queryRequest = req.body;
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            info('queryRequest received', lib_types_2.Utils.safeStringify(queryRequest));
        const foundArchiver = exports.archivers.get(queryRequest.publicKey);
        if (!foundArchiver) {
            const archiverNotFoundErr = 'Archiver not found in list';
            warn(archiverNotFoundErr);
            res.json({ success: false, error: archiverNotFoundErr });
            return;
        }
        delete queryRequest.publicKey;
        delete queryRequest.tag;
        let data;
        if (queryRequest.type === 'RECEIPT_MAP') {
            data = (0, snapshot_1.getReceiptMap)(queryRequest.lastData);
        }
        else if (queryRequest.type === 'SUMMARY_BLOB') {
            data = (0, snapshot_1.getSummaryBlob)(queryRequest.lastData);
            // console.log('Summary blob to send', data)
        }
        res.json({ success: true, data: data });
    });
    Context_1.network.registerExternalGet('archivers', (req, res) => {
        let archivers = getArchiversList();
        // In restart network, when there is only one node, we just send the first archiver which is serving as data recipient
        if (Self.isFirst && Self.isRestartNetwork && NodeList.nodes.size < 2)
            archivers = [...exports.recipients.values()];
        res.json({ archivers });
    });
    Context_1.network.registerExternalGet('joinedArchiver/:publicKey', ({ params: { publicKey } }, res) => {
        const isJoined = exports.archivers.has(publicKey);
        res.json({ isJoined });
    });
    Context_1.network.registerExternalGet('datarecipients', (req, res) => {
        res.json({ dataRecipients: [...exports.recipients.values()] });
    });
}
exports.registerRoutes = registerRoutes;
function sortedByPubKey() {
    return [...exports.archivers.values()].sort((a, b) => 
    // using mathematical comparison in case localeCompare is inconsistent.
    // we will use a simple ternary statement for this that doens't account for
    // equality. this should be fine as no two public keys should be the same.
    a.publicKey > b.publicKey ? 1 : -1);
}
exports.sortedByPubKey = sortedByPubKey;
/** Calculates and returns a hash based on the list of archivers, sorted by public key. This will also update the recorded `lastHashedList` of nodes, which can be retrieved via `getLastHashedArchiverList`. */
function computeNewArchiverListHash() {
    // set the lastHashedList to the current list by pubkey, then hash.
    // deep cloning is necessary as archiver information may be mutated by
    // reference.
    lastHashedList = clone(sortedByPubKey());
    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
        info('hashing archiver list:', lib_types_2.Utils.safeStringify(lastHashedList));
    const hash = Context_1.crypto.hash(lastHashedList);
    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
        info('the new archiver list hash is', hash);
    return hash;
}
exports.computeNewArchiverListHash = computeNewArchiverListHash;
/**
 * Returns the archiver list hash from the last complete cycle, if available. If you
 * want to compute a new hash instead, use `computeNewArchiverListHash`.
 */
function getArchiverListHash() {
    if (Context_1.config.p2p.writeSyncProtocolV2 || Context_1.config.p2p.useSyncProtocolV2) {
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            info('returning archiver hash:', CycleChain.newest?.archiverListHash);
        return CycleChain.newest?.archiverListHash;
    }
    else {
        // if we're not using sync v2, just compute a simple hash based on the
        // public keys of the archivers.
        const archiverListIDs = [...exports.archivers.keys()].sort();
        return Context_1.crypto.hash(archiverListIDs);
    }
}
exports.getArchiverListHash = getArchiverListHash;
let lastHashedList = [];
/** Returns the last list of archivers that had its hash computed. */
function getLastHashedArchiverList() {
    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
        info('returning last hashed archiver list:', lib_types_2.Utils.safeStringify(lastHashedList));
    return lastHashedList;
}
exports.getLastHashedArchiverList = getLastHashedArchiverList;
/**
 * A utility function that performs an HTTP GET request to an archiver. If
 * there is an error, it will handle scheduling a lost archiver report and
 * return undefined.
 */
function getFromArchiver(archiver, endpoint, failureReportMessage, timeout) {
    return neverthrow_1.ResultAsync.fromPromise(http.get(`http://${archiver.ip}:${archiver.port}/${endpoint}`, false, timeout ?? 1000), (e) => {
        warn(`${archiver.ip}:${archiver.port} is unreachable`);
        (0, functions_1.reportLostArchiver)(archiver.publicKey, failureReportMessage || `cannot GET archiver endpoint ${endpoint}`);
        return e;
    });
}
exports.getFromArchiver = getFromArchiver;
/**
 * A utility function that performs an HTTP POST request to an archiver. If
 * there is an error, it will handle scheduling a lost archiver report and
 * return undefined.
 */
function postToArchiver(archiver, endpoint, body, timeout, failureReportMessage) {
    return neverthrow_1.ResultAsync.fromPromise(http.post(`http://${archiver.ip}:${archiver.port}/${endpoint}`, body, false, timeout), (e) => {
        warn(`${archiver.ip}:${archiver.port} is unreachable`);
        (0, functions_1.reportLostArchiver)(archiver.publicKey, failureReportMessage || `cannot POST archiver endpoint ${endpoint}`);
        return e;
    });
}
exports.postToArchiver = postToArchiver;
function info(...msg) {
    const entry = `Archiver: ${msg.join(' ')}`;
    p2pLogger.info(entry);
}
function warn(...msg) {
    const entry = `Archiver: ${msg.join(' ')}`;
    p2pLogger.warn(entry);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function error(...msg) {
    const entry = `Archiver: ${msg.join(' ')}`;
    p2pLogger.error(entry);
}
//# sourceMappingURL=Archivers.js.map