UNPKG

@maticnetwork/matic-cli

Version:

Testing toolkit to setup, manage and operate Polygon networks

1,121 lines (1,011 loc) 33.8 kB
// noinspection JSUnresolvedFunction,JSUnresolvedVariable import inquirer from 'inquirer' import { Listr } from 'listr2' import path from 'path' import chalk from 'chalk' import execa from 'execa' import fs from 'fs-extra' import nunjucks from 'nunjucks' import { bufferToHex, privateToPublic, toBuffer } from 'ethereumjs-util' import { Heimdall } from '../heimdall' import { Bor } from '../bor' import { Ganache } from '../ganache' import { Genesis } from '../genesis' import { getDefaultBranch } from '../helper' import { errorMissingConfigs, getAccountFromPrivateKey, getKeystoreFile, getNewPrivateKey, processTemplateFiles } from '../../lib/utils' import { loadConfig } from '../config' import fileReplacer from '../../lib/file-replacer' import { getRemoteStdio } from '../../express/common/remote-worker' const timer = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) const getAllFiles = function (dirPath, arrayOfFiles) { const files = fs.readdirSync(dirPath) arrayOfFiles = arrayOfFiles || [] files.forEach(function (file) { if (fs.statSync(dirPath + '/' + file).isDirectory()) { if (file === 'bor' || file === 'heimdall') { arrayOfFiles = getAllFiles(dirPath + '/' + file, arrayOfFiles) } } else { arrayOfFiles.push(path.join(dirPath, '/', file)) } }) return arrayOfFiles } export class Devnet { constructor(config) { this.config = config } get testnetDir() { return path.join(this.config.targetDirectory, 'devnet') } get signerDumpPath() { return path.join(this.testnetDir, 'signer-dump.json') } get signerDumpData() { return require(this.signerDumpPath) } get totalNodes() { // noinspection JSUnresolvedVariable return this.config.numOfValidators + this.config.numOfNonValidators + this.config.numOfArchiveNodes } nodeDir(index) { return path.join(this.testnetDir, `node${index}`) } heimdallDir(index) { return path.join(this.nodeDir(index), 'heimdalld') } heimdallConfigFilePath(index) { return path.join(this.heimdallDir(index), 'config', 'config.toml') } heimdallGenesisFilePath(index) { return path.join(this.heimdallDir(index), 'config', 'genesis.json') } heimdallHeimdallConfigFilePath(index) { return path.join(this.heimdallDir(index), 'config', 'heimdall-config.toml') } borDir(index) { return path.join(this.nodeDir(index), 'bor') } borDataDir(index) { return path.join(this.borDir(index), 'data') } borKeystoreDir(index) { return path.join(this.borDir(index), 'keystore') } borGenesisFilePath(index) { return path.join(this.borDir(index), 'genesis.json') } borPasswordFilePath(index) { return path.join(this.borDir(index), 'password.txt') } borPrivateKeyFilePath(index) { return path.join(this.borDir(index), 'privatekey.txt') } borAddressFilePath(index) { return path.join(this.borDir(index), 'address.txt') } borNodeKeyPath(index) { return path.join(this.borDir(index), 'nodekey') } borEnodeFilePath(index) { return path.join(this.borDir(index), 'enode.txt') } borStaticNodesPath(index) { return path.join(this.borDir(index), 'static-nodes.json') } async getEnodeTask() { return { title: 'Setup enode', task: async () => { const staticNodes = [] // create new enode for (let i = 0; i < this.totalNodes; i++) { const enodeObj = await getNewPrivateKey() const pubKey = bufferToHex( privateToPublic(toBuffer(enodeObj.privateKey)) ).replace('0x', '') // draft enode const enode = `enode://${pubKey}@${this.config.devnetBorHosts[i]}:30303` // add into static nodes staticNodes.push(enode) // store data into nodekey and enode const p = [ // create nodekey file fs.writeFile( this.borNodeKeyPath(i), `${enodeObj.privateKey.replace('0x', '')}\n`, { mode: 0o600 } ), // create enode file fs.writeFile(this.borEnodeFilePath(i), `${enode}\n`, { mode: 0o600 }) ] await Promise.all(p) } // create static-nodes const data = JSON.stringify(staticNodes, null, 2) for (let i = 0; i < this.totalNodes; i++) { await fs.writeFile(this.borStaticNodesPath(i), data, { mode: 0o600 }) } } } } async getDockerTasks() { const enodeTask = await this.getEnodeTask() return [ enodeTask, { title: 'Process Heimdall configs', task: async () => { // set heimdall for (let i = 0; i < this.totalNodes; i++) { fileReplacer(this.heimdallHeimdallConfigFilePath(i)) .replace( /eth_rpc_url[ ]*=[ ]*".*"/gi, `eth_rpc_url = "${this.config.ethURL}"` ) .replace( /bor_rpc_url[ ]*=[ ]*".*"/gi, `bor_rpc_url = "http://bor${i}:8545"` ) .replace( /amqp_url[ ]*=[ ]*".*"/gi, `amqp_url = "amqp://guest:guest@rabbit${i}:5672/"` ) .replace( /span_poll_interval[ ]*=[ ]*".*"/gi, 'span_poll_interval = "0m15s"' ) .replace( /checkpoint_poll_interval[ ]*=[ ]*".*"/gi, 'checkpoint_poll_interval = "1m0s"' ) .save() } } }, { title: 'Process contract addresses', task: () => { // get root contracts const rootContracts = this.config.contractAddresses.root // set heimdall peers with devnet heimdall hosts for (let i = 0; i < this.totalNodes; i++) { fileReplacer(this.heimdallGenesisFilePath(i)) .replace( /"matic_token_address":[ ]*".*"/gi, `"matic_token_address": "${rootContracts.tokens.TestToken}"` ) .replace( /"staking_manager_address":[ ]*".*"/gi, `"staking_manager_address": "${rootContracts.StakeManagerProxy}"` ) .replace( /"root_chain_address":[ ]*".*"/gi, `"root_chain_address": "${rootContracts.RootChainProxy}"` ) .replace( /"staking_info_address":[ ]*".*"/gi, `"staking_info_address": "${rootContracts.StakingInfo}"` ) .replace( /"state_sender_address":[ ]*".*"/gi, `"state_sender_address": "${rootContracts.StateSender}"` ) .save() } }, enabled: () => { return this.config.contractAddresses } }, { title: 'Process templates', task: async () => { const templateDir = path.resolve( new URL(import.meta.url).pathname, '../templates' ) // copy docker related templates await fs.copy( path.join(templateDir, 'docker'), this.config.targetDirectory ) // process template files await processTemplateFiles(this.config.targetDirectory, { obj: this, ganache: this.ganache }) } } ] } async initRemoteTasks() { return new Listr([ await this.getEnodeTask(), { title: 'Process Heimdall configs', task: async () => { // set heimdall for (let i = 0; i < this.totalNodes; i++) { fileReplacer(this.heimdallHeimdallConfigFilePath(i)) .replace( /eth_rpc_url[ ]*=[ ]*".*"/gi, `eth_rpc_url = "${this.config.ethURL}"` ) .replace( /bor_rpc_url[ ]*=[ ]*".*"/gi, 'bor_rpc_url = "http://localhost:8545"' ) .replace( /amqp_url[ ]*=[ ]*".*"/gi, 'amqp_url = "amqp://guest:guest@localhost:5672/"' ) .save() } } }, { title: 'Process contract addresses', task: () => { // get root contracts const rootContracts = this.config.contractAddresses.root // set heimdall peers with devnet heimdall hosts for (let i = 0; i < this.totalNodes; i++) { fileReplacer(this.heimdallGenesisFilePath(i)) .replace( /"matic_token_address":[ ]*".*"/gi, `"matic_token_address": "${rootContracts.tokens.TestToken}"` ) .replace( /"staking_manager_address":[ ]*".*"/gi, `"staking_manager_address": "${rootContracts.StakeManagerProxy}"` ) .replace( /"root_chain_address":[ ]*".*"/gi, `"root_chain_address": "${rootContracts.RootChainProxy}"` ) .replace( /"staking_info_address":[ ]*".*"/gi, `"staking_info_address": "${rootContracts.StakingInfo}"` ) .replace( /"state_sender_address":[ ]*".*"/gi, `"state_sender_address": "${rootContracts.StateSender}"` ) .save() } }, enabled: () => { return this.config.contractAddresses } }, { title: 'Process templates', task: async () => { const templateDir = path.resolve( new URL(import.meta.url).pathname, '../templates' ) // copy remote related templates await fs.copy( path.join(templateDir, 'remote'), this.config.targetDirectory ) // promises const p = [] const signerDumpData = this.signerDumpData // process njk files for (const file of getAllFiles(this.config.targetDirectory, [])) { if (file.indexOf('.njk') !== -1) { const fp = path.join(this.config.targetDirectory, file) // process all njk files and copy to each node directory for (let i = 0; i < this.totalNodes; i++) { const file2array = file.split('/') const file2 = file2array[file2array.length - 1] fs.writeFileSync( path.join(this.nodeDir(i), file2.replace('.njk', '')), nunjucks.render(file, { obj: this, node: i, signerData: signerDumpData[i] }) ) } // remove njk file p.push( execa('rm', ['-rf', fp], { cwd: this.config.targetDirectory, stdio: getRemoteStdio() }) ) } } // fulfill all promises await Promise.all(p) } }], { concurrent: true }) } async getRemoteTasks() { const initRemoteTasks = await this.initRemoteTasks() await initRemoteTasks.run() return [ { title: 'Copy files to remote servers', task: async () => { if (this.config.devnetBorHosts === undefined) { return } // copy the Ganache files to the first node const ganacheURL = new URL(this.config.ethURL) const ganacheUser = this.config.ethHostUser await execa( 'scp', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.targetDirectory}/ganache-start.sh`, `${ganacheUser}@${ganacheURL.hostname}:~/ganache-start.sh` ], { stdio: getRemoteStdio() } ) await execa( 'scp', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-r', '-i', '~/cert.pem', `${this.config.targetDirectory}/data`, `${ganacheUser}@${ganacheURL.hostname}:~/data` ], { stdio: getRemoteStdio() } ) // Generate service files for (let i = 0; i < this.totalNodes; i++) { await execa( 'scp', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.targetDirectory}/service.sh`, `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}:~/service.sh` ], { stdio: getRemoteStdio() } ) if (i === 0) { await execa('ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, `bash ${this.config.targetDirectory}/service-host.sh` ], { stdio: getRemoteStdio() }) // NOTE: Target location would vary depending on bor/heimdall version. Currently the setup works with bor and heimdall v0.3.x await execa('ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, 'sudo mv ~/ganache.service /lib/systemd/system/' ], { stdio: getRemoteStdio() }) } await execa('ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, 'bash ~/service.sh' ], { stdio: getRemoteStdio() }) await execa('ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, 'sudo mv ~/bor.service /lib/systemd/system/' ], { stdio: getRemoteStdio() }) await execa( 'ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, 'sudo mv ~/heimdalld.service /lib/systemd/system/' ], { stdio: getRemoteStdio() } ) } for (let i = 0; i < this.totalNodes; i++) { // copy files to remote servers await execa( 'scp', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.targetDirectory}/code/bor/build/bin/bor`, `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}:~/go/bin/bor` ], { stdio: getRemoteStdio() } ) await execa( 'scp', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.targetDirectory}/code/heimdall/build/heimdalld`, `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}:~/go/bin/heimdalld` ], { stdio: getRemoteStdio() } ) await execa( 'scp', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.targetDirectory}/code/heimdall/build/heimdallcli`, `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}:~/go/bin/heimdallcli` ], { stdio: getRemoteStdio() } ) await execa( 'scp', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-r', '-i', '~/cert.pem', `${this.testnetDir}/node${i}/`, `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}:~/node/` ], { stdio: getRemoteStdio() } ) // Execute service files if (i === 0) { await execa( 'ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, 'sudo systemctl start ganache.service' ], { stdio: getRemoteStdio() } ) } if (i >= this.config.numOfValidators + this.config.numOfNonValidators) { await execa('ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, // eslint-disable-next-line `sed -i '$s,$, \\\\,' node/bor-start.sh` ], { stdio: getRemoteStdio() }) await execa('ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, // eslint-disable-next-line `printf %s " --gcmode 'archive'" >> ~/node/bor-start.sh ` ], { stdio: getRemoteStdio() }) } await execa( 'ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, 'bash ~/node/heimdalld-setup.sh' ], { stdio: getRemoteStdio() } ) await execa( 'ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, 'sudo ln -sf ~/go/bin/heimdalld /usr/bin/heimdalld' ], { stdio: getRemoteStdio() } ) await execa( 'ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, 'sudo systemctl start heimdalld.service' ], { stdio: getRemoteStdio() } ) await execa( 'ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, 'bash ~/node/bor-setup.sh ' ], { stdio: getRemoteStdio() } ) await execa('ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, 'sudo systemctl start bor.service' ], { stdio: getRemoteStdio() }) } } } ] } async getCreateTestnetTask(heimdall) { return new Listr([ heimdall.cloneRepositoryTask(), heimdall.buildTask(), { title: 'Create testnet files for Heimdall', task: async () => { const args = [ 'create-testnet', '--home', 'devnet', '--v', this.config.numOfValidators, '--n', this.config.numOfNonValidators + this.config.numOfArchiveNodes, '--chain-id', this.config.heimdallChainId, '--node-host-prefix', 'heimdall', '--output-dir', 'devnet' ] // Create heimdall folders if (this.config.devnetType === 'remote') { // create heimdall folder for all the nodes in remote setup for (let i = 0; i < this.totalNodes; i++) { await execa('ssh', [ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-i', '~/cert.pem', `${this.config.devnetBorUsers[i]}@${this.config.devnetBorHosts[i]}`, 'sudo mkdir -p /var/lib/heimdall && sudo chmod 777 -R /var/lib/heimdall/' ], { stdio: getRemoteStdio() }) } } // create testnet await execa(heimdall.heimdalldCmd, args, { cwd: this.config.targetDirectory, stdio: getRemoteStdio() }) // set heimdall peers with devnet heimdall hosts for (let i = 0; i < this.totalNodes; i++) { fileReplacer(this.heimdallConfigFilePath(i)) .replace(/heimdall([^:]+):/gi, (d, index) => { return `${this.config.devnetHeimdallHosts[index]}:` }) .replace(/moniker.+=.+/gi, `moniker = "heimdall${i}"`) .save() fileReplacer(this.heimdallGenesisFilePath(i)) .replace( /"bor_chain_id"[ ]*:[ ]*".*"/gi, `"bor_chain_id": "${this.config.borChainId}"` ) .save() } } } ]) } async borTask(bor) { return new Listr([{ title: bor.taskTitle, task: () => { return bor.getTasks() }, enabled: () => { return this.config.devnetType === 'remote' } }]) } async genesisTask(genesis) { return new Listr([{ title: genesis.taskTitle, task: () => { // get genesis tasks return genesis.getTasks() } }]) } async accountTask() { return new Listr([{ title: 'Setup accounts', task: () => { // set validator addresses const genesisAddresses = [] const signerDumpData = this.signerDumpData for (let i = 0; i < this.config.numOfValidators; i++) { const d = signerDumpData[i] genesisAddresses.push(d.address) } // set genesis addresses this.config.genesisAddresses = genesisAddresses // setup accounts from signer dump data (based on number of validators) this.config.accounts = this.signerDumpData .slice(0, this.config.numOfValidators) .map((s) => { return getAccountFromPrivateKey(s.priv_key) }) } }]) } async getDockerOrRemoteTask() { return new Listr([{ title: 'Docker', task: async () => { const tasks = await this.getDockerTasks() return new Listr(tasks, { concurrent: true }) }, enabled: () => { return this.config.devnetType === 'docker' } }, { title: 'Remote', task: async () => { const tasks = await this.getRemoteTasks() return new Listr(tasks) }, enabled: () => { return this.config.devnetType === 'remote' } }]) } async getTasks() { const ganache = this.ganache const heimdall = this.heimdall const bor = this.bor const genesis = this.genesis // create testnet tasks const createTestnetTasks = await this.getCreateTestnetTask(heimdall) await createTestnetTasks.run() const accountTasks = await this.accountTask() await accountTasks.run() const genesisTasks = await this.genesisTask(genesis) await genesisTasks.run() return new Listr([ { title: bor.taskTitle, task: () => { return bor.getTasks() }, enabled: () => { return this.config.devnetType === 'remote' } }, { title: 'Setup Bor keystore and genesis files', task: async () => { const signerDumpData = this.signerDumpData for (let i = 0; i < this.totalNodes; i++) { // create directories await execa( 'mkdir', ['-p', this.borDataDir(i), this.borKeystoreDir(i)], { stdio: getRemoteStdio() } ) const password = `password${i}` // create keystore files const keystoreFileObj = getKeystoreFile( signerDumpData[i].priv_key, password ) const p = [ // save password file fs.writeFile(this.borPasswordFilePath(i), `${password}\n`), // save private key file fs.writeFile( this.borPrivateKeyFilePath(i), `${signerDumpData[i].priv_key}\n` ), // save address file fs.writeFile( this.borAddressFilePath(i), `${signerDumpData[i].address}\n` ), // save keystore file fs.writeFile( path.join( this.borKeystoreDir(i), keystoreFileObj.keystoreFilename ), JSON.stringify(keystoreFileObj.keystore, null, 2) ), // copy genesis file to each node bor directory execa( 'cp', [genesis.borGenesisFilePath, this.borGenesisFilePath(i)], { stdio: getRemoteStdio() } ) ] await Promise.all(p) } } }, { title: ganache.taskTitle, task: () => { return ganache.getTasks() }, enabled: () => { return this.config.devnetType === 'docker' || 'remote' } }, { title: 'Remove multiple keystore files', task: async () => { for (let i = 0; i < this.totalNodes; i++) { // remove multiple keystore files from node[i]/bor/keystore const keystoreDir = path.join( this.testnetDir, `node${i}`, 'bor', 'keystore' ) fs.readdir(keystoreDir, async (err, files) => { if (err) console.log(err) // harmless if (files) { for (let j = 1; j < files.length; j++) { await fs.unlink(path.join(keystoreDir, files[j])) } } }) await timer(2000) } }, enabled: () => { return this.config.devnetType === 'docker' || 'remote' } } ], { concurrent: true }) } } async function setupDevnet(config) { const devnet = new Devnet(config) devnet.ganache = new Ganache(config, { contractsBranch: config.contractsBranch }) devnet.bor = new Bor(config, { repositoryUrl: config.borRepo, repositoryBranch: config.borBranch, dockerContext: config.borDockerBuildContext }) devnet.heimdall = new Heimdall(config, { repositoryUrl: config.heimdallRepo, repositoryBranch: config.heimdallBranch, dockerContext: config.heimdallDockerBuildContext }) devnet.genesis = new Genesis(config, { repositoryUrl: config.genesisContractsRepo, repositoryBranch: config.genesisContractsBranch }) const tasks = await devnet.getTasks() await tasks.run() const dockerOrRemoteTasks = await devnet.getDockerOrRemoteTask() await dockerOrRemoteTasks.run() console.log('%s Devnet is ready', chalk.green.bold('DONE')) } export async function getHosts(n) { const answers = await inquirer.prompt([ { type: 'input', name: 'devnetHosts', message: 'Please enter comma separated hosts/IPs', validate: (input) => { const hosts = input.split(',').map((a) => { return a.trim().toLowerCase() }) if (hosts.length === 0 || hosts.length !== n) { return `Enter valid ${n} hosts/IPs (comma separated)` } return true } } ]) return answers.devnetHosts.split(',').map((a) => { return a.trim().toLowerCase() }) } export async function getUsers(n) { const answers = await inquirer.prompt([ { type: 'input', name: 'devnetUsers', message: 'Please enter comma separated Users', validate: (input) => { const hosts = input.split(',').map((a) => { return a.trim().toLowerCase() }) if (hosts.length === 0 || hosts.length !== n) { return `Enter valid ${n} Users (comma separated)` } return true } } ]) return answers.devnetHosts.split(',').map((a) => { return a.trim().toLowerCase() }) } export default async function (command) { // configuration const config = await loadConfig({ targetDirectory: command.parent.directory, fileName: command.parent.config, interactive: command.parent.interactive }) await config.loadChainIds() // load branch let answers = await getDefaultBranch(config) config.set(answers) const questions = [] if (!('numOfValidators' in config)) { questions.push({ type: 'number', name: 'numOfValidators', message: 'Please enter number of validator nodes', default: 2 }) } if (!('numOfNonValidators' in config)) { questions.push({ type: 'number', name: 'numOfNonValidators', message: 'Please enter number of non-validator nodes', default: 0 }) } if (!('ethURL' in config)) { questions.push({ type: 'input', name: 'ethURL', message: 'Please enter ETH url', default: 'http://ganache:9545' }) } if (!('ethHostUser' in config)) { questions.push({ type: 'input', name: 'ethHostUser', message: 'Please enter ETH host', default: 'ubuntu' }) } if (!('devnetType' in config)) { questions.push({ type: 'list', name: 'devnetType', message: 'Please select devnet type', choices: ['docker', 'remote'] }) } if (!config.interactive) { errorMissingConfigs( questions.map((q) => { return q.name }) ) } answers = await inquirer.prompt(questions) config.set(answers) // set devent hosts let devnetBorHosts = config.devnetBorHosts || [] let devnetBorUsers = config.devnetBorUsers || [] let devnetHeimdallHosts = config.devnetHeimdallHosts || [] let devnetHeimdallUsers = config.devnetHeimdallUsers || [] const totalValidators = config.numOfValidators + config.numOfNonValidators + config.numOfArchiveNodes // For docker, the devnetBorHosts conform to the subnet 172.20.1.0/24 if (config.devnetType === 'docker') { devnetBorHosts = [] devnetHeimdallHosts = [] for (let i = 0; i < totalValidators; i++) { devnetBorHosts.push(`172.20.1.${i + 100}`) devnetHeimdallHosts.push(`heimdall${i}`) } } else { const missing = [ 'devnetBorHosts', 'devnetBorUsers', 'devnetHeimdallHosts', 'devnetHeimdallUsers' ].filter((c) => { if ( c in config && config[c].length !== totalValidators && !config.interactive ) { console.error( `Wrong number of hosts provided in ${c}, got ${config[c].length}, expect ${totalValidators}.` ) process.exit(1) } return !(c in config) || config[c].length !== totalValidators }) if (missing.length > 0) { const hosts = await getHosts(totalValidators) devnetBorHosts = hosts devnetHeimdallHosts = hosts } if (missing.length > 0) { const users = await getUsers(totalValidators) devnetBorUsers = users devnetHeimdallUsers = users } } config.set({ devnetBorHosts, devnetBorUsers, devnetHeimdallHosts, devnetHeimdallUsers }) // start setup await setupDevnet(config) }