UNPKG

evdevkit

Version:

Developer toolkit for Evernode smart contract deployment

1,418 lines (1,173 loc) 7.75 MB
#! /usr/bin/env node /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ /***/ 41175: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { var map = { "./build/Release/secp256k1.node": 63416 }; function webpackContext(req) { var id = webpackContextResolve(req); return __nccwpck_require__(id); } function webpackContextResolve(req) { if(!__nccwpck_require__.o(map, req)) { var e = new Error("Cannot find module '" + req + "'"); e.code = 'MODULE_NOT_FOUND'; throw e; } return map[req]; } webpackContext.keys = function webpackContextKeys() { return Object.keys(map); }; webpackContext.resolve = webpackContextResolve; module.exports = webpackContext; webpackContext.id = 41175; /***/ }), /***/ 62902: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { var map = { "./prebuilds/linux-x64/node.napi1.node": 25643 }; function webpackContext(req) { var id = webpackContextResolve(req); return __nccwpck_require__(id); } function webpackContextResolve(req) { if(!__nccwpck_require__.o(map, req)) { var e = new Error("Cannot find module '" + req + "'"); e.code = 'MODULE_NOT_FOUND'; throw e; } return map[req]; } webpackContext.keys = function webpackContextKeys() { return Object.keys(map); }; webpackContext.resolve = webpackContextResolve; module.exports = webpackContext; webpackContext.id = 62902; /***/ }), /***/ 48279: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { var map = { "./prebuilds/linux-x64/node.napi.node": 31884 }; function webpackContext(req) { var id = webpackContextResolve(req); return __nccwpck_require__(id); } function webpackContextResolve(req) { if(!__nccwpck_require__.o(map, req)) { var e = new Error("Cannot find module '" + req + "'"); e.code = 'MODULE_NOT_FOUND'; throw e; } return map[req]; } webpackContext.keys = function webpackContextKeys() { return Object.keys(map); }; webpackContext.resolve = webpackContextResolve; module.exports = webpackContext; webpackContext.id = 48279; /***/ }), /***/ 73771: /***/ ((module) => { function webpackEmptyAsyncContext(req) { // Here Promise.resolve().then() is used instead of new Promise() to prevent // uncaught exception popping up in devtools return Promise.resolve().then(() => { var e = new Error("Cannot find module '" + req + "'"); e.code = 'MODULE_NOT_FOUND'; throw e; }); } webpackEmptyAsyncContext.keys = () => ([]); webpackEmptyAsyncContext.resolve = webpackEmptyAsyncContext; webpackEmptyAsyncContext.id = 73771; module.exports = webpackEmptyAsyncContext; /***/ }), /***/ 63416: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { module.exports = require(__nccwpck_require__.ab + "build/Release/secp256k1.node") /***/ }), /***/ 31884: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { module.exports = require(__nccwpck_require__.ab + "prebuilds/linux-x64/node.napi.node") /***/ }), /***/ 25643: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { module.exports = require(__nccwpck_require__.ab + "prebuilds/linux-x64/node.napi1.node") /***/ }), /***/ 2785: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { const process = __nccwpck_require__(932); const fs = __nccwpck_require__(79896); const MAINNET = 'mainnet'; // Throw errors if the env value is required. const appenv = { instanceImage: 'evernode/sashimono:hp.0.6.4-ubt.20.04-njs.20', get tenantSecret() { if (!process.env.EV_TENANT_SECRET) throw 'EV_TENANT_SECRET environment variable has not been set.'; return process.env.EV_TENANT_SECRET; }, get userPrivateKey() { if (!process.env.EV_USER_PRIVATE_KEY) throw 'EV_USER_PRIVATE_KEY environment variable has not been set.'; return process.env.EV_USER_PRIVATE_KEY; }, get hpInitCfg() { if (process.env.EV_HP_INIT_CFG_PATH && !fs.existsSync(process.env.EV_HP_INIT_CFG_PATH)) throw `HotPocket config file does not exist in EV_HP_INIT_CFG_PATH=${process.env.EV_HP_INIT_CFG_PATH}`; if (!process.env.EV_HP_INIT_CFG_PATH) { return {}; } try { return JSON.parse(fs.readFileSync(process.env.EV_HP_INIT_CFG_PATH)); } catch (e) { throw `EV_HP_INIT_CFG_PATH=${process.env.EV_HP_INIT_CFG_PATH} - ${e}`; } }, get hpOverrideCfg() { if (process.env.EV_HP_OVERRIDE_CFG_PATH && !fs.existsSync(process.env.EV_HP_OVERRIDE_CFG_PATH)) throw `HotPocket override config file does not exist in EV_HP_OVERRIDE_CFG_PATH=${process.env.EV_HP_OVERRIDE_CFG_PATH}`; if (!process.env.EV_HP_OVERRIDE_CFG_PATH) { return {}; } try { return JSON.parse(fs.readFileSync(process.env.EV_HP_OVERRIDE_CFG_PATH)); } catch (e) { throw `EV_HP_OVERRIDE_CFG_PATH=${process.env.EV_HP_OVERRIDE_CFG_PATH} - ${e}`; } }, get network() { // TODO: Default will be changed to MAINNET after the launch. return process.env.EV_NETWORK || MAINNET; }, get xahaudServer() { return process.env.EV_XAHAUD_SERVER || null; }, } Object.freeze(appenv); module.exports = appenv /***/ }), /***/ 68310: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { const { execSync } = __nccwpck_require__(35317); function exec(commad, streamOut = false) { return execSync(commad, streamOut ? { stdio: 'inherit' } : {stdio : 'pipe' }); } module.exports = { exec }; /***/ }), /***/ 24475: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { const fs = __nccwpck_require__(79896); const os = __nccwpck_require__(70857); const crypto = __nccwpck_require__(76982); const uuid = __nccwpck_require__(76193); const path = __nccwpck_require__(16928); const { EvernodeManager } = __nccwpck_require__(81917); const { info, log, error } = __nccwpck_require__(48035); const appenv = __nccwpck_require__(2785); const { generateKeys } = __nccwpck_require__(51034); const BLACKLIST_SCORE_THRESHOLD = 2; const NodeStatus = { NONE: 0, CREATED: 1, CONFIGURED: 2, ACKNOWLEDGED: 3, ADDED_TO_UNL: 4 } const ClusterOwner = { NONE: 0, SELF_MANAGER: 1 } const LifePlan = { STATIC: 'stat', INCREMENTAL: 'inc', RANDOM: 'rand', } class ClusterManager { #size; #tenantSecret; #ownerPrivateKey; #moments; #contractId; #instanceImage; #config; #hosts; multisig; #quorum; #signerCount; #signerMoments; #lifePlan #lifeGap #minLifeMoments; #maxLifeMoments; #nodes; #evernodeMgr; #signers; #evrBalance; #leaseAmounts; constructor(options = {}) { this.#size = options.size || 3; this.#tenantSecret = options.tenantSecret; this.#ownerPrivateKey = options.ownerPrivateKey; this.#moments = options.moments || 1; this.#contractId = options.contractId || uuid.v4(); this.#instanceImage = options.instanceImage || appenv.instanceImage; this.#config = options.config || {}; this.multisig = options.multisig || false; this.#lifePlan = options.lifePlan || LifePlan.STATIC; this.#evrBalance = options.evrLimit || null; this.#leaseAmounts = []; if (this.#lifePlan == LifePlan.RANDOM) { this.#minLifeMoments = options.minLifeMoments || this.#moments; this.#maxLifeMoments = options.maxLifeMoments || this.#moments; } else if (this.#lifePlan == LifePlan.INCREMENTAL) { this.#lifeGap = options.lifeGap; } if (this.multisig) { this.#signers = options.signers || []; this.#signerCount = this.#signers.length || options.signerCount || this.#size; // Required minimum accumulated weight for txn submission. // Quorum = quorum as a ratio * sum of weights this.#quorum = (this.#signers.length > 0) ? Math.ceil(options.quorum * (this.#signers.reduce(function (acc, s) { return acc + s.weight }, 0))) : Math.ceil(options.quorum * this.#signerCount); // Here we consider the signer weight as 1. Hence the sum of weight will be equal to `this.#signerCount`. this.#signerMoments = options.signerMoments || this.#moments; } } async init(hostAddresses) { if (!this.#tenantSecret) throw "Tenant secret is missing!"; else if (!this.#ownerPrivateKey) throw "Owner private key is missing!"; else if (!this.#instanceImage) throw "Instance image is missing!"; this.#evernodeMgr = new EvernodeManager({ tenantSecret: this.#tenantSecret }); await this.#evernodeMgr.init(); const hostList = (await this.#evernodeMgr.getActiveHosts(hostAddresses)).filter(h => h.maxInstances > h.activeInstances); this.#hosts = hostList.reduce((map, host) => { host.blacklistScore = 0; map[host.address] = host; return map; }, {}); this.#nodes = []; } async terminate() { if (this.#evernodeMgr) await this.#evernodeMgr.terminate(); } getClusterInfoCachePath() { const createRef = crypto.createHash('md5').update(process.argv.join()).digest('hex'); const tmpClusterDir = `${os.tmpdir()}/evdevkit-cluster`; const tmpClusterName = `partial-cluster-${createRef}.json`; return `${tmpClusterDir}/${tmpClusterName}`; } cacheClusterInfo() { const cachePath = this.getClusterInfoCachePath(); const cacheDir = path.dirname(cachePath); if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir); fs.writeFileSync(cachePath, JSON.stringify(this.#nodes, null, 4)); } getClusterInfoCache() { const cachePath = this.getClusterInfoCachePath(); return fs.existsSync(cachePath) ? JSON.parse(fs.readFileSync(cachePath)) : []; } clearClusterInfoCache() { const cachePath = this.getClusterInfoCachePath(); if (fs.existsSync(cachePath)) fs.rmSync(cachePath); } async #createNode(hostAddress, ownerPubKey, config = {}, leaseOfferIndex = null, tenantSequence = null) { // Set consensus mode to public since primary node need to send proposals to others. if (!config.contract) config.contract = {}; if (!config.contract.consensus) config.contract.consensus = {}; config.contract.consensus.mode = "public"; return await this.#evernodeMgr.acquire( hostAddress, 1, ownerPubKey, this.#contractId, this.#instanceImage, config, { tenantSequence: tenantSequence, leaseOfferIndex: leaseOfferIndex }); } async #extendCluster() { let tenantSequence = await this.#evernodeMgr.getTenantSequence(); const assignedLives = []; let success = true; await Promise.all(this.#nodes.map(async (node, i) => { await new Promise(resolve => setTimeout(resolve, 500 * i)); try { let life; switch (this.#lifePlan) { case LifePlan.RANDOM: { const isFarEnough = (lifeValue, lifeArray, minimumDistance) => { if (lifeArray.length == 0) return true; for (let i = 0; i < lifeArray.length; i++) { if (Math.abs(lifeValue - lifeArray[i]) < minimumDistance) { return false; } } return true; } life = Math.floor( Math.random() * (this.#maxLifeMoments - this.#minLifeMoments) + this.#minLifeMoments); while (!isFarEnough(life, assignedLives, 1)) { life = Math.floor( Math.random() * (this.#maxLifeMoments - this.#minLifeMoments) + this.#minLifeMoments); } assignedLives.push(life) break; } case LifePlan.INCREMENTAL: { life = 1 + i * this.#lifeGap; break; } default: { life = node.signer_detail ? this.#signerMoments : this.#moments; break; } } if (life > 1) { info(`Extending ${node.name} by ${life - 1} moments...`); await this.#evernodeMgr.extend(node.host, node.name, life - 1, { tenantSequence: tenantSequence++ }); } node.life_moments = life; } catch (e) { success = false; error(e.reason || e); } })); return success; } async #createClusterChunk(chunkSize = 1, optimalNodes) { const nodes = []; const curNodeCount = this.#nodes.length; let tenantSequence = await this.#evernodeMgr.getTenantSequence(); await Promise.all(Array(chunkSize).fill(0).map(async (v, i) => { await new Promise(resolve => setTimeout(resolve, 1000 * i)); const host = optimalNodes[(curNodeCount + i) % optimalNodes.length]; const nodeNumber = curNodeCount + i + 1; let nodeNumberText = nodeNumber; if (Math.floor(nodeNumber / 10) != 1 && nodeNumber % 10 === 1) nodeNumberText += 'st'; else if (Math.floor(nodeNumber / 10) != 1 && nodeNumber % 10 === 2) nodeNumberText += 'nd'; else if (Math.floor(nodeNumber / 10) != 1 && nodeNumber % 10 === 3) nodeNumberText += 'rd'; else nodeNumberText += 'th'; try { info(`Creating ${nodeNumberText} node on host ${host.address}...`); const hostLeases = await this.#evernodeMgr.getHostLeases(host.address); const selectedLeaseIndex = hostLeases && hostLeases[0] && hostLeases[0].index; if (!selectedLeaseIndex) throw "No offers available."; let config = JSON.parse(JSON.stringify(this.#config)); if (!config.mesh) config.mesh = {} if (this.#nodes.length > 0) { const primaryNode = this.#nodes[0]; config.mesh.known_peers = [`${primaryNode.domain}:${primaryNode.peer_port}`]; // If the cluster does not require multi-sig feature, then we can allow other nodes to sync with primary node from the beginning. if (!this.multisig) { if (!config.contract) config.contract = {} config.contract.unl = [primaryNode.pubkey]; } } // Set random user keys for the secondary nodes. let userKeys = await generateKeys((this.multisig && this.#nodes.length > 0) ? null : this.#ownerPrivateKey, 'hex'); const result = await this.#createNode(host.address, userKeys.publicKey, config, selectedLeaseIndex, tenantSequence++); info(`${nodeNumberText} node created! Name: ${result.name}`); nodes.push({ host: host.address, userKeys: userKeys, ...result }); this.#hosts[host.address].activeInstances++; } catch (e) { log(`${nodeNumberText} node creation failed!`, e.reason || e); this.#hosts[host.address].blacklistScore += this.#incrementBlacklistScore(e.reason); } if (this.#evrBalance != null) { this.#evrBalance -= host.leaseAmount; } })); return nodes; } #incrementBlacklistScore(reason = null) { const instantBanReasons = ['max_alloc_reached', 'HOST_INVALID']; if (instantBanReasons.includes(reason)) return BLACKLIST_SCORE_THRESHOLD; else return 1; } #estimateCost(preferredHosts, targetSize, returnNodes = false) { if (!preferredHosts || preferredHosts.length == 0) throw `All hosts are invalid or occupied.`; let optimalCost = 0; let optimalNodes = []; let totalInstances = 0; for (const host of preferredHosts) { totalInstances += host.availableInstances; } if (targetSize > totalInstances) throw `Number of available instances of the preferred hosts is insufficient to create the cluster.` while (optimalNodes.length < targetSize) { for (let hostIndex in preferredHosts) { let host = preferredHosts[hostIndex] if (host.availableInstances > 0 && optimalNodes.length < targetSize) { optimalNodes.push(host); optimalCost += host.leaseAmount; host.availableInstances -= 1; } } } optimalCost *= this.#moments; if (this.#evrBalance != null && optimalCost > this.#evrBalance) throw `Defined EVR limit is insufficient. Estimated cost for remaining node creation: ${optimalCost}` if (returnNodes) return optimalNodes; else info(`EVR cost estimated for cluster creation: ${optimalCost}`); } async #checkFeasibility(targetSize, preferredHostsArray) { let hosts = []; hosts = Object.values(this.#hosts).filter((host) => host.maxInstances > host.activeInstances); let preferredHosts = preferredHostsArray.map(ph => hosts.find(h => h.address === ph)).filter(h => h); for (const host of preferredHosts) { host.availableInstances = host.maxInstances - host.activeInstances; this.#leaseAmounts.push({ host: host.address, leaseAmount: host.leaseAmount }) } preferredHosts = preferredHosts.map(({ address, availableInstances, leaseAmount }) => ({ address, availableInstances, leaseAmount })) .filter(h => h.leaseAmount != null).sort((a, b) => a.leaseAmount - b.leaseAmount); this.#estimateCost(preferredHosts, targetSize); } #getOptimalNodesList(targetSize, preferredHostsArray) { const hosts = Object.values(this.#hosts).filter((host) => host.blacklistScore < BLACKLIST_SCORE_THRESHOLD && host.maxInstances > host.activeInstances); let preferredHosts = preferredHostsArray.map(ph => hosts.find(h => h.address === ph)).filter(h => h); for (const host of preferredHosts) { host.availableInstances = host.maxInstances - host.activeInstances; if (this.#evrBalance) host.leaseAmount = this.#leaseAmounts.find(la => la.host === host.address)?.leaseAmount || null; host.nodes = this.#nodes.filter((node) => node.host === host.address).length; } preferredHosts = preferredHosts.map(({ address, availableInstances, leaseAmount, nodes }) => ({ address, availableInstances, leaseAmount, nodes })); if (this.#evrBalance) preferredHosts = preferredHosts.filter(h => h.leaseAmount != null).sort((a, b) => a.leaseAmount - b.leaseAmount); const optimalNodes = this.#estimateCost(preferredHosts, targetSize, true); return optimalNodes; } async createCluster(preferredHostsArray, recover = false) { if (recover) { const nodes = this.getClusterInfoCache(); if (nodes.length > 0) { this.#nodes.push(...nodes); this.#contractId = nodes[0].contract_id; } } let targetSize = this.#size - this.#nodes.length; //Initial feasibility check before cluster creation if (this.#evrBalance) { await this.#checkFeasibility(targetSize, preferredHostsArray); } while (targetSize > 0) { const chunkSize = Math.min((targetSize == this.#size ? 1 : targetSize), preferredHostsArray.length) const optimalNodes = this.#getOptimalNodesList(chunkSize, preferredHostsArray); if (optimalNodes.length == 0) throw 'No available optimal nodes to acquire.'; const nodes = await this.#createClusterChunk(chunkSize, optimalNodes); this.#nodes.push(...nodes); this.cacheClusterInfo(); targetSize -= nodes.length; } // Set the signers if the cluster requires multi-sig feature. if (this.multisig) { this.#signers = await this.#evernodeMgr.setSigners(this.#signers, this.#quorum, this.#signerCount); // Map the signers to nodes. (No any order) for (let i = 0; i < this.#signerCount; i++) { this.#nodes[i].signer_detail = this.#signers[i]; } this.cacheClusterInfo(); } info(`Extending the nodes life...`); if (!(await this.#extendCluster())) { this.cacheClusterInfo(); throw 'Error occurred while extending the nodes.'; } else { this.cacheClusterInfo(); } return this.#nodes; } async writeSigner(destination, nodePubkey) { const normalizedPath = path.normalize(destination); const node = this.#nodes.find(n => n.pubkey === nodePubkey && n.hasOwnProperty('signer_detail')); if (node) fs.writeFileSync(normalizedPath, JSON.stringify(node.signer_detail, null, 4)); } getTenantAddress() { return this.#evernodeMgr.getTenantAddress(); } } module.exports = { ClusterManager, NodeStatus, ClusterOwner, LifePlan }; /***/ }), /***/ 97447: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { const fs = __nccwpck_require__(79896); const path = __nccwpck_require__(16928); const uuid = __nccwpck_require__(76193); const appenv = __nccwpck_require__(2785); const { exec } = __nccwpck_require__(68310); const { ClusterManager, NodeStatus, ClusterOwner, LifePlan } = __nccwpck_require__(24475); const { CONSTANTS, bundleContract, generateKeys, validateArrayElements, removeDirectorySync, questionSync } = __nccwpck_require__(51034); const { EvernodeManager } = __nccwpck_require__(81917); const { InstanceManager } = __nccwpck_require__(57482); const { error, info, success, log } = __nccwpck_require__(48035); const { TimeTracker } = __nccwpck_require__(40884); const NODES_BUNDLE_PATH = `./nodes/`; const DEFAULT_QUORUM = 0.8; const MAX_UPLOAD_TRIES = 5; const DEFAULT_LIFE_GAP = 2; // Number of Moments const MAX_LIFE_UPPER_BOUND = 48; // Number of Moments const DEFAULT_OPERATIONAL_TIME_BOUND = 48; // Number of Hours function version() { info(`command: version`); try { const res = exec(`npm -g list ${CONSTANTS.npmPackageName} --depth=0`); const splitted = res.toString().split('\n'); if (splitted.length > 1) { success(`\n${splitted[1].split('@')[1]}\n`); return; } } catch (e) { error('Error getting the version info:', e); } error(`\n${CONSTANTS.npmPackageName} is not installed.`); } async function list(options) { info(`command: list`); if (options.desc && !options.orderBy) { error('orderBy option is required to order in descending manner.'); return; } let evernodeMgr; try { evernodeMgr = new EvernodeManager(); await evernodeMgr.init(); const hosts = await evernodeMgr.getActiveHostsFromLedger().catch(error); if (hosts) { let formatted = hosts.map(h => { return { address: h.address, domain: h.domain, ram: `${h.ramMb} MB`, storage: `${h.diskMb} MB`, cpu: { model: h.cpuModelName, time: `${h.cpuMicrosec} us`, cores: h.cpuCount, speed: `${h.cpuMHz} MHz` }, sashimonoVersion: h.version, countryCode: h.countryCode, totalInstanceSlots: h.maxInstances, availableInstanceSlots: h.maxInstances - h.activeInstances, leaseFee: h.leaseAmount, reputation: h.hostReputation, } }); if (formatted.length > 0 && options.orderBy && !(options.orderBy in formatted[0])) error(`Host info does not contain a key named ${options.orderBy}.`); if (options.orderBy) formatted = formatted.filter(h => h[options.orderBy]).sort((a, b) => ((a[options.orderBy] - b[options.orderBy]) * (options.desc ? -1 : 1))); log(formatted.slice(0, options.limit).map(h => { if (!options.props) return h; const keys = options.props.split(',').filter(k => k in h); let obj = {}; for (const key of keys) { obj[key] = h[key]; } return obj; })); } } catch (e) { error('Error occurred while getting the host list:', e); } finally { if (evernodeMgr) await evernodeMgr.terminate(); } } async function hostInfo(options) { info(`command: info`); let evernodeMgr; try { let hostsToSearch = []; let tempFileName = `hostDetails_${Date.now()}.csv`; if (!options.filePath && !options.hostAddress) throw 'Either host address or file path is required to search for host info.'; if (options.filePath) { if (!fs.existsSync(options.filePath) || !fs.statSync(options.filePath).isFile()) throw `Hosts file ${options.filePath} does not exists.`; hostsToSearch = options.filePath ? fs.readFileSync(options.filePath, 'UTF-8').split(/\r?\n/).filter(h => h) : []; } if (options.hostAddress && (typeof options.hostAddress !== 'string' || options.hostAddress.trim() === '')) throw 'Host address should be a non-empty string.'; hostsToSearch.push(options.hostAddress); if (options.output && (!(fs.existsSync(options.output) && fs.statSync(options.output).isDirectory()))) throw `Output path ${options.output} does not exist or is not a directory.`; evernodeMgr = new EvernodeManager(); await evernodeMgr.init(); // Fetch all host details in parallel const allHostDetails = await Promise.all( hostsToSearch.map(async (hostAddress) => { try { const host = await evernodeMgr.getHostInfo(hostAddress); if (host) { return { address: host.address, domain: host.domain, ram: `${host.ramMb} MB`, storage: `${host.diskMb} MB`, cpu: { model: host.cpuModelName, time: `${host.cpuMicrosec} us`, cores: host.cpuCount, speed: `${host.cpuMHz} MHz` }, sashimonoVersion: host.version, countryCode: host.countryCode, totalInstanceSlots: host.maxInstances, availableInstanceSlots: host.maxInstances - host.activeInstances, active: host.active }; } } catch (error) { console.error(`Error fetching details for host ${hostAddress}:`, error); } }) ); const validHostDetails = allHostDetails.filter(host => host !== undefined); // Write them to the CSV file if (options.output && validHostDetails.length > 0) { tempFileName = path.join(options.output, tempFileName); const csvHeader = [ 'Address', 'Domain', 'RAM', 'Storage', 'CPU Model', 'CPU Time', 'CPU Cores', 'CPU Speed', 'Sashimono Version', 'Country Code', 'Total Instance Slots', 'Available Instance Slots', 'Active' ]; const csvData = validHostDetails.map(host => [ host.address, host.domain, host.ram, host.storage, host.cpu.model, host.cpu.time, host.cpu.cores, host.cpu.speed, host.sashimonoVersion, host.countryCode, host.totalInstanceSlots, host.availableInstanceSlots, host.active ]); // Convert header and data into CSV format const csvContent = [csvHeader, ...csvData].map(row => row.join(',')).join('\n'); // Write the CSV string to the temporary file fs.writeFile(tempFileName, csvContent, (err) => { if (err) { console.error('Error writing to the CSV file:', err.message); } else { console.log('CSV file written successfully.'); } }); } log(validHostDetails); } catch (e) { error('Error occurred while getting the host info:', e); } finally { if (evernodeMgr) await evernodeMgr.terminate(); } } async function keygen() { info(`command: keygen`); try { const keys = await generateKeys(); success('New key pair generated', keys); info('Record these keys and set the private key to the environment variable called EV_USER_PRIVATE_KEY for future operations.'); } catch (e) { error('Error occurred while generating key pair:', e); } } async function acquire(host, options) { info(`command: acquire`); let evernodeMgr; try { evernodeMgr = new EvernodeManager({ tenantSecret: appenv.tenantSecret }); await evernodeMgr.init(); const userKeys = await generateKeys(appenv.userPrivateKey, 'hex'); const result = await evernodeMgr.acquire( host, options.moments || 1, userKeys.publicKey, options.contractId, options.image, appenv.hpInitCfg || {}); success('Instance created!', result); } catch (e) { error('Error occurred while acquiring the instance:', e); } finally { if (evernodeMgr) await evernodeMgr.terminate(); } } async function extend(instancesFilePath, options) { info(`command: extend`); let evernodeMgr; try { if (!instancesFilePath || !fs.existsSync(instancesFilePath)) throw 'Instance file path does not exist.'; evernodeMgr = new EvernodeManager({ tenantSecret: appenv.tenantSecret }); await evernodeMgr.init(); const moments = options?.moments ? options.moments : 1; // Read contents of the file const data = fs.readFileSync(instancesFilePath, 'UTF-8'); // Split the contents by new line const instances = data.split(/\r?\n/).filter(e => e); await Promise.all(instances.map(async (line, i) => { await new Promise(resolve => setTimeout(resolve, 1000 * i)); try { let [hostAddress, instanceName, life] = line.split(":"); hostAddress = hostAddress.trim(); instanceName = instanceName.trim(); if (life) life = life.trim(); if (!hostAddress || !instanceName) throw 'Host address and instance name are required.'; if (life && isNaN(life)) throw 'Moment life should be a integer number.'; else if (life) { life = Number(life); if (!Number.isInteger(life)) throw 'Moment life should be a integer number.'; } else { life = moments; } const result = await evernodeMgr.extend(hostAddress, instanceName, life); info(`Extending the instance for ${life} ${life === 1 ? 'moment' : 'moments'}.`); success(`Extension txn Ref: ${result.extendRefId}\nExpiry moment: ${result.expiryMoment}`); } catch (e) { error(e.reason || e); } })); } catch (e) { error('Error occurred while extending the instance:.', e); } finally { if (evernodeMgr) await evernodeMgr.terminate(); } } async function extendInstance(hostAddress, instanceName, options) { info(`command: extend`); let evernodeMgr; try { evernodeMgr = new EvernodeManager({ tenantSecret: appenv.tenantSecret }); await evernodeMgr.init(); const moments = options?.moments ? options.moments : 1; const result = await evernodeMgr.extend(hostAddress, instanceName, moments); info(`Extending the instance for ${moments} ${moments === 1 ? 'moment' : 'moments'}.`); success(`Extension txn Ref: ${result.extendRefId}\nExpiry moment: ${result.expiryMoment}`); } catch (e) { error('Error occurred while extending the instance:.', e); } finally { if (evernodeMgr) await evernodeMgr.terminate(); } } async function audit(options) { info(`command: audit`); let hostsToAudit = []; let auditResults = []; let totalAmount = 0; let evernodeMgr, evrBalance; const errorDescription = { 1: 'Host inactive', 2: 'Host invalid (not registered)', 3: 'No lease offer', 4: 'Timeout during Acquire', 5: 'User Install error during Acquire', 6: 'Transaction failed', 7: 'Insufficient funds during Acquire', 8: 'Connection failed during auditing', 9: 'Transaction failed due to timeout', 100: 'Unknown error', 'N/A': 'Not Applicable' }; const errorMap = { 'HOST_INACTIVE': 1, 'HOST_INVALID': 2, 'NO_OFFER': 3, 'TIMEOUT': 4, 'user_install_error': 5, 'TRANSACTION_FAILURE': 6, 'TRANSACTION_FAILURE (tecINSUFFICIENT_FUNDS)': 7, 'Connection failed': 8, 'TRANSACTION_FAILURE (TimeoutError)': 9, 'N/A': 'N/A' }; const expectedOperationalTime = options?.opTime ? options.opTime : DEFAULT_OPERATIONAL_TIME_BOUND; try { if (options.filePath) { if (!fs.existsSync(options.filePath) || !fs.statSync(options.filePath).isFile()) throw `Hosts file ${options.filePath} does not exists.`; hostsToAudit = options.filePath ? fs.readFileSync(options.filePath, 'UTF-8').split(/\r?\n/).filter(h => h) : []; } if (options.hostAddress) hostsToAudit.push(options.hostAddress); if (!hostsToAudit || hostsToAudit.length == 0) throw `No hosts specified to audit.`; evernodeMgr = new EvernodeManager({ tenantSecret: (options?.aliveness) ? null : appenv.tenantSecret }); await evernodeMgr.init(); let userKeys; if (!options?.aliveness) userKeys = await generateKeys(appenv.userPrivateKey, 'hex'); const defaultValue = 'N/A'; const auditStatus = ['Success', 'Failed', 'Cannot Audit']; const AuditResult = (hostAddress, status, alivenessData, timeAcquire = defaultValue, timeReadRequestResponse = defaultValue, timeContractResponse = defaultValue, hpVersion = defaultValue, ledgerSeqNo = defaultValue, errorMessage = defaultValue, inactiveStatus = null) => { return { "HOST ADDRESS": hostAddress, "STATUS": inactiveStatus || auditStatus[status], "CONT_ALIVENESS": alivenessData.aliveness, "SUSTAINED_UP_TIME (H:m)": alivenessData.uptime, "ACQUISITION DURATION (s)": timeAcquire, "READ RES DURATION (s)": timeReadRequestResponse, "CONTRACT RES DURATION (s)": timeContractResponse, "HP VER": hpVersion, "LEDGER SEQ NO": ledgerSeqNo, "ERROR": errorMap[errorMessage] || '100' } } const AlivenessResult = (hostAddress, alivenessData) => { return { "HOST ADDRESS": hostAddress, "CONT_ALIVENESS": alivenessData.aliveness, "SUSTAINED_UP_TIME (H:m)": alivenessData.uptime } } if (!options?.aliveness) { evrBalance = parseFloat(await evernodeMgr.getEVRBalance()); for (let hostIndex in hostsToAudit) { const hostAddress = hostsToAudit[hostIndex]; const leases = await evernodeMgr.getHostLeases(hostAddress); if (leases.length === 0) { totalAmount += 0; } else { const amountValue = parseFloat(leases[0].Amount.value); if (!isNaN(amountValue)) { totalAmount += amountValue; } else { console.error('Invalid amount value in the first lease:', leases[0].Amount.value); } } } if (evrBalance < totalAmount) { console.log(`Not enough EVRs to proceed.\nNeed ${totalAmount} EVRs.\nBut you have only ${evrBalance} EVRs.`) process.exit(1); } else { const answer = await questionSync(`It will cost ${totalAmount} EVRs from your account for the Audit. Do you wish to proceed [Y/n] ? `); if ((answer.trim().toLowerCase() === 'n')) { console.log("Exiting..."); process.exit(0); } else if (answer.trim().toLowerCase() !== 'y' && answer.trim() !== '') { console.log('Invalid input. Please enter either "y" or "n".\nExiting...'); process.exit(0); } } } const currentTimestamp = (Date.now()) / 1000; const latestXrplLedgerIndex = evernodeMgr.getLatestLedgerIndex(); for (let hostIndex in hostsToAudit) { const hostAddress = hostsToAudit[hostIndex]; const timeTracker = new TimeTracker(); let timeAcquire, timeContractResponse, timeReadRequestResponse, hpVersion, ledgerSeqNo, status, errorMessage, alivenessData; let inactiveStatus = null; let instanceMgr; try { info(`Auditing ${hostAddress} ...`); alivenessData = await evernodeMgr.checkHostRealAliveness(hostAddress, currentTimestamp, latestXrplLedgerIndex, expectedOperationalTime); if (!options?.aliveness) { timeTracker.start(); const result = await evernodeMgr.acquire( hostAddress, 1, userKeys.publicKey, uuid.v4(), options.image || appenv.instanceImage, appenv.hpInitCfg || {}); success('Instance created!', result); timeAcquire = timeTracker.end(); const instanceIp = result.domain; const instanceUserPort = result.user_port; instanceMgr = new InstanceManager({ ip: instanceIp, userPort: instanceUserPort, userPrivateKey: appenv.userPrivateKey }); await instanceMgr.init(); timeTracker.start(); const readRequestResponse = await instanceMgr.checkReadRequestBootstrapResponse(); if (readRequestResponse == null) throw 'Read request response check failed' timeReadRequestResponse = timeTracker.end(); timeTracker.start(); const bootstrapStatusResult = await instanceMgr.checkBootstrapStatus(); if (bootstrapStatusResult == null) throw 'Bootstrap status response check failed' timeContractResponse = timeTracker.end(); const statusResult = await instanceMgr.checkStatus(); if (statusResult == null) throw 'Status check failed' status = 0; hpVersion = statusResult.hpVersion; ledgerSeqNo = statusResult.ledgerSeqNo; } } catch (e) { status = 1; errorMessage = e.reason; if (e.reason == 'HOST_INACTIVE') { const hostInfo = await evernodeMgr.getHostInfo(hostAddress); const lastActiveTimestamp = hostInfo?.lastHeartbeatIndex || hostInfo?.registrationTimestamp; const downtime = Math.floor(Date.now() / 1000) - lastActiveTimestamp; const days = Math.floor(downtime / (3600 * 24)); const hours = Math.floor((downtime % (3600 * 24)) / 3600); inactiveStatus = `Inactive(${days}D-${hours}H)`; } else if (e.reason == 'NO_OFFER') { status = 2; } else if (typeof e === 'string' && e.includes('connection failed')) { errorMessage = 'Connection failed'; } error(`Error occurred while auditing ${hostAddress}. Error:`, e); } finally { if (options?.aliveness) { auditResults.push(AlivenessResult(hostAddress, alivenessData)); } else { auditResults.push(AuditResult(hostAddress, status, alivenessData, timeAcquire, timeReadRequestResponse, timeContractResponse, hpVersion, ledgerSeqNo, errorMessage, inactiveStatus)); if (instanceMgr) await instanceMgr.terminate(); } } } } catch (e) { error('Error occurred while auditing. Error:', e); } finally { if (auditResults && auditResults.length) { console.table(auditResults); info(`NOTE: Considered ${expectedOperationalTime}Hrs. for the Continuous Aliveness check.`) if (!options?.aliveness) { info('Error Descriptions:'); for (const key in errorDescription) { console.log(`${key}: ${errorDescription[key]}`); } } } else error("No hosts were audited."); if (evernodeMgr) await evernodeMgr.terminate(); } } async function bundle(contractDirectoryPath, instancePublicKey, contractBin, options) { info(`command: bundle`); try { contractDirectoryPath = path.normalize(contractDirectoryPath); const stats = fs.existsSync(contractDirectoryPath) ? fs.statSync(contractDirectoryPath) : null; if (!stats || !stats.isDirectory()) throw `Contract directory ${contractDirectoryPath} does not exists.`; const userOverrideCfg = appenv.hpOverrideCfg || {}; const hpOverrideCfg = { ...userOverrideCfg, contract: { ...(userOverrideCfg.contract || {}), unl: [ ...(userOverrideCfg.contract?.unl || []), instancePublicKey ], bin_path: contractBin, bin_args: options.contractArgs } } const bundlePath = await bundleContract( contractDirectoryPath, hpOverrideCfg); if (bundlePath) success(`Archive finished. (location: ${bundlePath})`); } catch (e) { error('Error occurred while bundling:', e); } } async function deploy(contractBundlePath, instanceIp, instanceUserPort) { info(`command: deploy`); let instanceMgr; try { instanceMgr = new InstanceManager({ ip: instanceIp, userPort: instanceUserPort, userPrivateKey: appenv.userPrivateKey }); await instanceMgr.init(); await instanceMgr.uploadBundle(contractBundlePath); success(`Contract bundle uploaded!`); } catch (e) { error('Error occurred while uploading the bundle:', e); } finally { if (instanceMgr) await instanceMgr.terminate(); } } async function acquireAndDeploy(contractDirectoryPath, contractBin, host, options) { info(`command: acquire-and-deploy`); let evernodeMgr; let instanceMgr; try { evernodeMgr = new EvernodeManager({ tenantSecret: appenv.tenantSecret }); contractDirectoryPath = path.normalize(contractDirectoryPath); const stats = fs.existsSync(contractDirectoryPath) ? fs.statSync(contractDirectoryPath) : null; if (!stats || !stats.isDirectory()) throw `Contract directory ${contractDirectoryPath} does not exists.`; await evernodeMgr.init(); const hpConfig = appenv.hpInitCfg || {}; const userOverrideCfg = appenv.hpOverrideCfg || {}; const userKeys = await generateKeys(appenv.userPrivateKey, 'hex'); const result = await evernodeMgr.acquire( host, options.moments || 1, userKeys.publicKey, options.contractId, options.image, hpConfig); const instancePublicKey = result.pubkey; const instanceIp = result.domain; const instanceUserPort = result.user_port; info('Instance created!', result); const hpOverrideCfg = { ...userOverrideCfg, contract: { ...(userOverrideCfg.contract || {}), unl: [ ...(userOverrideCfg.contract?.unl || []), instancePublicKey ], bin_path: contractBin, bin_args: options.contractArgs } }; const bundlePath = await bundleContract( contractDirectoryPath, hpOverrideCfg); if (!bundlePath) throw 'Archive failed.'; info(`Archive finished. (location: ${bundlePath})`); instanceMgr = new InstanceManager({ ip: instanceIp, userPort: instanceUserPort, userPrivateKey: appenv.userPrivateKey }); await instanceMgr.init(); await instanceMgr.uploadBundle(bundlePath); info(`Contract bundle uploaded!`); success(`Contract deployed!`); } catch (e) { error('Error occurred while deploying:', e); } finally { if (evernodeMgr) await evernodeMgr.terminate(); if (instanceMgr) await instanceMgr.terminate(); } } async function clusterCreate(size, contractDirectoryPath, contractBin, hostsFilePath, options) { info(`command: create-cluster`); let clusterMgr; let instanceMgr; try { contractDirectoryPath = path.normalize(contractDirectoryPath); const stats = fs.existsSync(contractDirectoryPath) ? fs.statSync(contractDirectoryPath) : null; if (!stats || !stats.isDirectory()) throw `Contract directory ${contractDirectoryPath} does not exist.`; if (!hostsFilePath || !fs.existsSync(hostsFilePath)) throw 'Preferred Host file path does not exist.'; if (options?.signers && !fs.existsSync(options.signers)) throw 'Signer Details file path does not exist.'; if (options?.lifePlan) { if (!(Object.values(LifePlan).includes(options.lifePlan))) throw 'Invalid cluster node life plan is provided.'; switch (options.lifePlan) { case LifePlan.RANDOM: { info("Randomized node life planning is considered."); if (options?.signerLife) throw 'Defining --signer-life is not applicable in Random life plan.'; if (options?.moments) throw 'Defining --moments is not applicable in Random life plan.'; if (options?.evrLimit) throw 'Defining --evr-limit is not applicable in Random life plan.'; if (options?.lifeGap) throw 'Defining --life-gap is not applicable in Random life plan.'; if (!options?.minLife) throw 'Defining --min-life is not applicable in Random life plan.'; if (!options?.maxLife) { info(`Default value of --max-life (${MAX_LIFE_UPPER_BO