@andylu1988/matic-cli
Version:
A CLI to setup and manage Matic validator nodes
528 lines (461 loc) • 15.9 kB
JavaScript
import inquirer from 'inquirer'
import Listr from 'listr'
import path from 'path'
import chalk from 'chalk'
import execa from 'execa'
import fs from 'fs-extra'
import nunjucks from 'nunjucks'
import { toBuffer, privateToPublic, bufferToHex } from 'ethereumjs-util'
import { Heimdall } from '../heimdall'
//import { Ganache } from '../ganache'
import { Genesis } from '../genesis'
import { printDependencyInstructions, getDefaultBranch } from '../helper'
import { getNewPrivateKey, getKeystoreFile, processTemplateFiles, getAccountFromPrivateKey } from '../../lib/utils'
import { loadConfig } from '../config'
import fileReplacer from '../../lib/file-replacer'
export class Devnet {
constructor(config, options = {}) {
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() {
return this.config.numOfValidators + this.config.numOfNonValidators
}
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')
}
borDataBorDir(index) {
return path.join(this.borDir(index), 'data', 'bor')
}
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})
}
}
]
}
async getRemoteTasks() {
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://localhost:8545"')
.replace(/amqp_url[ ]*=[ ]*".*"/gi, 'amqp_url = "amqp://guest:guest@localhost:5672/"')
.save()
}
}
},
{
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
fs.readdirSync(this.config.targetDirectory).forEach(file => {
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++) {
fs.writeFileSync(
path.join(this.nodeDir(i), file.replace('.njk', '')),
nunjucks.render(fp, { obj: this, node: i, signerData: signerDumpData[i] })
)
}
// remove njk file
p.push(execa('rm', ['-rf', fp], {
cwd: this.config.targetDirectory
}))
}
})
// fulfill all promises
await Promise.all(p)
}
}
]
}
async getCreateTestnetTask(heimdall) {
return [
heimdall.cloneRepositoryTask(),
heimdall.buildTask(),
{
title: 'Create testnet files for Heimdall',
task: async () => {
const args = [
'create-testnet',
'--v', this.config.numOfValidators,
'--n', this.config.numOfNonValidators,
'--chain-id', this.config.heimdallChainId,
'--node-host-prefix', 'heimdall',
'--output-dir', 'devnet'
]
// create testnet
await execa(heimdall.heimdalldCmd, args, {
cwd: this.config.targetDirectory
})
// 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 getTasks() {
//const ganache = this.ganache
const heimdall = this.heimdall
const genesis = this.genesis
// create testnet tasks
const createTestnetTasks = await this.getCreateTestnetTask(heimdall)
return new Listr(
[
...createTestnetTasks,
{
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)
})
}
},
{
title: genesis.taskTitle,
task: () => {
// get genesis tasks
return genesis.getTasks()
}
},
{
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)])
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)])
]
await Promise.all(p)
}
}
},
// {
// title: ganache.taskTitle,
// task: () => {
// return ganache.getTasks()
// },
// enabled: () => {
// return this.config.devnetType === 'docker'
// }
// },
{
title: 'Docker',
task: async () => {
const tasks = await this.getDockerTasks()
return new Listr(tasks)
},
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 function setupDevnet(config) {
const devnet = new Devnet(config)
//devnet.ganache = new Ganache(config, { contractsBranch: config.contractsBranch })
devnet.heimdall = new Heimdall(config, { repositoryBranch: config.heimdallBranch })
devnet.genesis = new Genesis(config, { repositoryBranch: 'master' })
const tasks = await devnet.getTasks()
await tasks.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 default async function () {
await printDependencyInstructions()
// configuration
const config = await loadConfig()
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: 2
})
}
if (!('ethURL' in config)) {
questions.push({
type: 'input',
name: 'ethURL',
message: 'Please enter ETH url',
default: ''
})
}
if (!('devnetType' in config)) {
questions.push({
type: 'list',
name: 'devnetType',
message: 'Please select devnet type',
choices: [
'docker',
'remote'
]
})
}
answers = await inquirer.prompt(questions)
config.set(answers)
// set devent hosts
let devnetBorHosts = []
let devnetHeimdallHosts = []
const totalValidators = config.numOfValidators + config.numOfNonValidators
if (config.devnetType === 'docker') {
[...Array(totalValidators).keys()].forEach((i) => {
devnetBorHosts.push(`172.20.1.${i + 100}`)
devnetHeimdallHosts.push(`heimdall${i}`)
})
} else {
const hosts = await getHosts(totalValidators)
devnetBorHosts = hosts
devnetHeimdallHosts = hosts
}
config.set({ devnetBorHosts, devnetHeimdallHosts })
// start setup
await setupDevnet(config)
}