import { Ethereum } from '../ethereum/ethereum'; import { CryptoNodeData, VersionDockerImage } from '../../interfaces/crypto-node'; import { defaultDockerNetwork, NetworkType, NodeClient, NodeType, Role } from '../../constants'; import { timeout } from '../../util'; import * as coreConfig from './config/core'; import { Docker } from '../../util/docker'; import { v4 as uuid } from 'uuid'; import { FS } from '../../util/fs'; import { ChildProcess } from 'child_process'; import os from 'os'; import path from 'path'; import * as genesis from './config/genesis'; import { pathExists } from 'fs-extra'; interface PolygonCryptoNodeData extends CryptoNodeData { heimdallDockerImage?: string heimdallDockerCPUs?: number heimdallDockerMem?: number heimdallPeerPort?: number heimdallRPCPort?: number } interface PolygonVersionDockerImage extends VersionDockerImage { heimdallImage: string heimdallDataDir: string heimdallWalletDir: string heimdallConfigDir: string generateHeimdallRuntimeArgs: (data: CryptoNodeData) => string } export class Polygon extends Ethereum { static versions(client: string, networkType: string): PolygonVersionDockerImage[] { client = client || Polygon.clients[0]; let versions: PolygonVersionDockerImage[]; switch(client) { case NodeClient.CORE: versions = [ { version: '0.2.14', clientVersion: '0.2.14', image: 'maticnetwork/bor:v0.2.14', dataDir: '/root/data', walletDir: '/root/keys', configDir: '/root/config', heimdallImage: 'maticnetwork/heimdall:v0.2.8', heimdallDataDir: '/root/.heimdalld/data', heimdallWalletDir: '/root/keys', heimdallConfigDir: '/root/.heimdalld/config', networks: [NetworkType.MAINNET, NetworkType.TESTNET], breaking: false, generateRuntimeArgs(data: CryptoNodeData): string { const { id = '' } = data; return ` bor --config=${this.configDir}/${Polygon.fileName.config} --bor.heimdall http://${Polygon.generateHeimdallDockerName(id)}:1317 --pprof --pprof.port 7071 --pprof.addr 0.0.0.0`; }, generateHeimdallRuntimeArgs(data: CryptoNodeData): string { return ' /bin/bash /start.sh'; }, }, ]; break; default: versions = []; } return versions .filter(v => v.networks.includes(networkType)); } static clients = [ NodeClient.CORE, ]; static nodeTypes = [ NodeType.FULL, ]; static networkTypes = [ NetworkType.MAINNET, NetworkType.TESTNET, ]; static roles = [ Role.NODE, ]; static defaultRPCPort = { [NetworkType.MAINNET]: 8545, [NetworkType.TESTNET]: 8545, }; static defaultPeerPort = { [NetworkType.MAINNET]: 30303, [NetworkType.TESTNET]: 30303, }; static defaultHeimdallRPCPort = { [NetworkType.MAINNET]: 1317, [NetworkType.TESTNET]: 1317, }; static defaultHeimdallPeerPort = { [NetworkType.MAINNET]: 26656, [NetworkType.TESTNET]: 26656, }; static defaultCPUs = 16; static defaultMem = 32768; static heimdallDefaultCPUs = 2; static heimdallDefaultMem = 2048; static generateConfig(client: Polygon|string = Polygon.clients[0], network = NetworkType.MAINNET, peerPort = Polygon.defaultPeerPort[NetworkType.MAINNET], rpcPort = Polygon.defaultRPCPort[NetworkType.MAINNET]): string { let clientStr: string; if(typeof client === 'string') { clientStr = client; } else { clientStr = client.client; network = client.network || network; peerPort = client.peerPort || peerPort; rpcPort = client.rpcPort || rpcPort; } let baseConfig: string; switch(clientStr) { case NodeClient.CORE: baseConfig = coreConfig.borConfig; break; default: baseConfig = ''; } let bootstrapNodes: string; if(network === NetworkType.MAINNET) { bootstrapNodes = '"enode://0cb82b395094ee4a2915e9714894627de9ed8498fb881cec6db7c65e8b9a5bd7f2f25cc84e71e89d0947e51c76e85d0847de848c7782b13c0255247a6758178c@44.232.55.71:30303", "enode://88116f4295f5a31538ae409e4d44ad40d22e44ee9342869e7d68bdec55b0f83c1530355ce8b41fbec0928a7d75a5745d528450d30aec92066ab6ba1ee351d710@159.203.9.164:30303"'; } else if(network === NetworkType.TESTNET) { bootstrapNodes = '"enode://095c4465fe509bd7107bbf421aea0d3ad4d4bfc3ff8f9fdc86f4f950892ae3bbc3e5c715343c4cf60c1c06e088e621d6f1b43ab9130ae56c2cacfd356a284ee4@18.213.200.99:30303"'; } else { bootstrapNodes = ''; } return baseConfig .replace(/{{BOOTSTRAP_NODES}}/, bootstrapNodes) .replace(/{{RPC_PORT}}/, rpcPort.toString(10)) .replace(/{{PEER_PORT}}/g, peerPort.toString(10)) .replace(/{{NETWORK_ID}}/, Polygon.getBorChainId(network)); } static generateHeimdallConfig(id: string, peerPort: number, network: string): string { let seeds: string; switch(network) { case NetworkType.MAINNET: seeds = '[email protected]:26656,[email protected]:26656'; break; case NetworkType.TESTNET: seeds = '[email protected]:26656,[email protected]:26656'; break; default: seeds = ''; } return coreConfig.heimdallConfig .replace(/{{MONIKER}}/, id) .replace(/{{PEER_PORT}}/g, peerPort.toString(10)) .replace(/{{SEEDS}}/, seeds); } static generateHeimdallServerConfig(borName: string, borRPCPort: number): string { return coreConfig.heimdallServerConfig .replace(/{{BOR_NAME}}/, borName) .replace(/{{BOR_RPC_PORT}}/, borRPCPort.toString(10)); } static generateHeimdallDockerName(id: string): string { return id + '-heimdall'; } static getHeimdallChainId(network: string): string { switch(network) { case NetworkType.MAINNET: return 'heimdall-137'; case NetworkType.TESTNET: return 'heimdall-80001'; default: return ''; } } static getBorChainId(network: string): string { switch(network) { case NetworkType.MAINNET: return '137'; case NetworkType.TESTNET: return '80001'; default: return ''; } } static fileName = { config: 'config.toml', heimdallConfig: 'config.toml', heimdallServerConfig: 'heimdall-config.toml', genesis: 'genesis.json', }; id: string; ticker = 'matic'; name = 'Polygon'; version: string; clientVersion: string; archival = false; dockerImage: string; heimdallDockerImage: string; network: string; peerPort: number; rpcPort: number; heimdallRPCPort: number; heimdallPeerPort: number; rpcUsername: string; rpcPassword: string; client: string; dockerCPUs = Polygon.defaultCPUs; dockerMem = Polygon.defaultMem; heimdallDockerCPUs = Polygon.heimdallDefaultCPUs; heimdallDockerMem = Polygon.heimdallDefaultMem; dockerNetwork = defaultDockerNetwork; dataDir = ''; walletDir = ''; configDir = ''; remote = false; remoteDomain = ''; remoteProtocol = ''; constructor(data: PolygonCryptoNodeData, docker?: Docker) { super(data, docker); this.id = data.id || uuid(); this.network = data.network || NetworkType.MAINNET; this.peerPort = data.peerPort || Polygon.defaultPeerPort[this.network]; this.rpcPort = data.rpcPort || Polygon.defaultRPCPort[this.network]; this.heimdallPeerPort = data.heimdallPeerPort || Polygon.defaultHeimdallPeerPort[this.network]; this.heimdallRPCPort = data.heimdallRPCPort || Polygon.defaultHeimdallRPCPort[this.network]; this.rpcUsername = data.rpcUsername || ''; this.rpcPassword = data.rpcPassword || ''; this.client = data.client || Polygon.clients[0]; this.dockerCPUs = data.dockerCPUs || this.dockerCPUs; this.dockerMem = data.dockerMem || this.dockerMem; this.heimdallDockerCPUs = data.heimdallDockerCPUs || this.heimdallDockerCPUs; this.heimdallDockerMem = data.heimdallDockerMem || this.heimdallDockerMem; this.dockerNetwork = data.dockerNetwork || this.dockerNetwork; this.dataDir = data.dataDir || this.dataDir; this.walletDir = data.walletDir || this.walletDir; this.configDir = data.configDir || this.configDir; this.createdAt = data.createdAt || this.createdAt; this.updatedAt = data.updatedAt || this.updatedAt; this.remote = data.remote || this.remote; this.remoteDomain = data.remoteDomain || this.remoteDomain; this.remoteProtocol = data.remoteProtocol || this.remoteProtocol; const versions = Polygon.versions(this.client, this.network); this.version = data.version || (versions && versions[0] ? versions[0].version : ''); const versionObj = versions.find(v => v.version === this.version) || versions[0] || {}; this.clientVersion = data.clientVersion || versionObj.clientVersion || ''; this.dockerImage = this.remote ? '' : data.dockerImage ? data.dockerImage : (versionObj.image || ''); this.heimdallDockerImage = this.remote ? '' : data.heimdallDockerImage ? data.heimdallDockerImage : (versionObj.heimdallImage || ''); this.archival = data.archival || this.archival; this.role = data.role || this.role; if(docker) { this._docker = docker; this._fs = new FS(docker); } } async start(): Promise<ChildProcess[]> { const versions = Polygon.versions(this.client, this.network); const versionData = versions.find(({ version }) => version === this.version) || versions[0]; if(!versionData) throw new Error(`Unknown version ${this.version}`); const { heimdallArgs, borArgs, } = await this.prestart(versionData); const heimdallInstance = this.startHeimdall(versionData, heimdallArgs); const borInstance = this.startBor(versionData, borArgs); this._instance = borInstance; this._instances = [ borInstance, heimdallInstance, ]; return this.instances(); } async prestart(versionData: PolygonVersionDockerImage): Promise<{heimdallArgs: string[], borArgs: string[]}> { const fs = this._fs; const { dataDir: containerDataDir, walletDir: containerWalletDir, configDir: containerConfigDir, heimdallDataDir: containerHeimdallDataDir, heimdallWalletDir: containerHeimdallWalletDir, heimdallConfigDir: containerHeimdallConfigDir, } = versionData; const tmpdir = os.tmpdir(); const heimdallStartScriptPath = path.join(tmpdir, uuid()); const heimdallStartScript = 'heimdalld start & heimdalld rest-server'; await fs.writeFile(heimdallStartScriptPath, heimdallStartScript, 'utf8'); let borArgs = [ '-i', '--rm', '--memory', `${this.dockerMem}MB`, '--cpus', this.dockerCPUs.toString(10), '--name', this.id, '--network', this.dockerNetwork, '-p', `${this.peerPort}:${this.peerPort}`, '-p', `${this.rpcPort}:${this.rpcPort}`, ]; let heimdallArgs = [ '-i', '--rm', '--memory', `${this.heimdallDockerMem}MB`, '--cpus', this.heimdallDockerCPUs.toString(10), '--name', this.polygonGenerateHeimdallDockerName(), '--network', this.dockerNetwork, '-v', `${heimdallStartScriptPath}:/start.sh`, '-p', `${this.heimdallPeerPort}:${this.heimdallPeerPort}`, ]; const dataDir = this.dataDir || path.join(tmpdir, uuid()); await fs.ensureDir(dataDir); const configDir = this.configDir || path.join(tmpdir, 'config'); await fs.ensureDir(configDir); // bor directories const borDataDir = path.join(dataDir, 'bor'); const borDBDir = path.join(borDataDir, 'data'); await fs.ensureDir(borDataDir); await fs.ensureDir(borDBDir); const borConfigDir = path.join(configDir, 'bor'); const borConfigConfigDir = path.join(borConfigDir, 'config'); await fs.ensureDir(borConfigDir); await fs.ensureDir(borConfigConfigDir); const borConfigPath = path.join(borConfigConfigDir, Polygon.fileName.config); const borGenesisPath = path.join(borConfigConfigDir, Polygon.fileName.genesis); borArgs = [...borArgs, '-v', `${borDBDir}:${containerDataDir}`, '-v', `${borConfigConfigDir}:${containerConfigDir}`, ]; // heimdall directories const heimdallDataDir = path.join(dataDir, 'heimdall'); const heimdallDBDir = path.join(heimdallDataDir, 'data'); await fs.ensureDir(heimdallDataDir); await fs.ensureDir(heimdallDBDir); const heimdallConfigDir = path.join(configDir, 'heimdall'); const heimdallConfigConfigDir = path.join(heimdallConfigDir, 'config'); await fs.ensureDir(heimdallConfigDir); await fs.ensureDir(heimdallConfigConfigDir); heimdallArgs = [...heimdallArgs, '-v', `${heimdallDBDir}:${containerHeimdallDataDir}`, '-v', `${heimdallConfigConfigDir}:${containerHeimdallConfigDir}`, ]; const heimdallConfigPath = path.join(heimdallConfigConfigDir, Polygon.fileName.heimdallConfig); const heimdallServerConfigPath = path.join(heimdallConfigConfigDir, Polygon.fileName.heimdallServerConfig); const heimdallGenesisPath = path.join(heimdallConfigConfigDir, Polygon.fileName.genesis); const configExists = await fs.pathExists(heimdallConfigPath); await this._docker.pull(this.heimdallDockerImage, str => this._logOutput(str)); await this._docker.pull(this.dockerImage, str => this._logOutput(str)); if(!configExists) { await new Promise((resolve, reject) => { this._docker.run( this.heimdallDockerImage + ` heimdalld init --chain-id ${Polygon.getHeimdallChainId(this.network)}`, heimdallArgs, output => this._logOutput(output), err => { this._logError(err); reject(err); }, code => { resolve(code); }, ); }); await fs.writeFile(heimdallConfigPath, this.polygonGenerateHeimdallConfig(), 'utf8'); await fs.writeFile(heimdallServerConfigPath, this.polygonGenerateHeimdallServerConfig(), 'utf8'); await fs.writeFile(heimdallGenesisPath, this.polygonGenerateHeimdallGenesis(), 'utf8'); } const borConfigExists = await fs.pathExists(borConfigPath); if(!borConfigExists) { await fs.writeFile(borGenesisPath, this.polygonGenerateBorGenesis(), 'utf8'); await fs.writeFile(borConfigPath, this.generateConfig(), 'utf8'); } const borDir = path.join(borDBDir, 'bor'); const borDataExists = await pathExists(borDir); if(!borDataExists) { await new Promise((resolve, reject) => { this._docker.run( this.dockerImage + ` bor --config=${versionData.configDir}/${Polygon.fileName.config} init ${versionData.configDir}/${Polygon.fileName.genesis}`, borArgs, output => this._logOutput(output), err => { this._logError(err); reject(err); }, code => { resolve(code); }, ); }); } return { heimdallArgs, borArgs, }; } startHeimdall(versionData: PolygonVersionDockerImage, heimdallArgs: string[]): ChildProcess { return this._docker.run( this.heimdallDockerImage + versionData.generateHeimdallRuntimeArgs(this), heimdallArgs, output => this._logOutput('heimdall - ' + output), err => this._logError(err), code => this._logClose(code), ); } startBor(versionData: PolygonVersionDockerImage, borArgs: string[]): ChildProcess { return this._docker.run( this.dockerImage + versionData.generateRuntimeArgs(this), borArgs, output => this._logOutput('bor - ' + output), err => this._logError(err), code => this._logClose(code), ); } async stop(): Promise<void> { await Promise.all([ this.stopBor(), this.stopHeimdall(), ]); } async stopHeimdall(): Promise<string|undefined> { try { const name = this.polygonGenerateHeimdallDockerName(); const success = await this._docker.stop(name); await timeout(1000); return success; } catch(err) { this._logError(err); } } async stopBor(): Promise<string|undefined> { try { const name = this.id; const success = await this._docker.stop(name); await timeout(1000); return success; } catch(err) { this._logError(err); } } toObject(): PolygonCryptoNodeData { return { ...this._toObject(), heimdallDockerImage: this.heimdallDockerImage, heimdallDockerCPUs: this.heimdallDockerCPUs, heimdallDockerMem: this.heimdallDockerMem, heimdallPeerPort: this.heimdallPeerPort, heimdallRPCPort: this.heimdallRPCPort, }; } generateConfig(): string { return Polygon.generateConfig(this); } polygonGenerateHeimdallConfig(): string { return Polygon.generateHeimdallConfig(this.id, this.peerPort, this.network); } polygonGenerateHeimdallServerConfig(): string { return Polygon.generateHeimdallServerConfig(this.id, this.rpcPort); } polygonGenerateBorGenesis(): string { const { network } = this; switch(network) { case NetworkType.MAINNET: return genesis.borMainnet; case NetworkType.TESTNET: return genesis.borTestnet; default: return ''; } } polygonGenerateHeimdallGenesis(): string { const { network } = this; switch(network) { case NetworkType.MAINNET: return genesis.heimdallMainnet; case NetworkType.TESTNET: return genesis.heimdallTestnet; default: return ''; } } polygonGenerateHeimdallDockerName(): string { return Polygon.generateHeimdallDockerName(this.id); } async rpcGetVersion(): Promise<string> { let borVersion: string; try { borVersion = await this.rpcGetBorVersion(); } catch(err) { borVersion = ''; } let heimdallVersion: string; try { heimdallVersion = await this.rpcGetHeimdallVersion(); } catch(err) { heimdallVersion = ''; } const joinedVersion = `${heimdallVersion}/${borVersion}`; return joinedVersion === '/' ? '' : joinedVersion; } async rpcGetBorVersion(): Promise<string> { return this._rpcGetVersion(); } async rpcGetHeimdallVersion(): Promise<string> { try { const version: string = await new Promise((resolve, reject) => { let res = ''; this._docker.exec( this.polygonGenerateHeimdallDockerName(), [], 'heimdalld version', output => { res += output; }, err => { reject(err); }, () => { resolve(res); }); }); const trimmedVersion = version.trim(); return /^\d+\.\d+\.\d+/.test(trimmedVersion) ? trimmedVersion : ''; } catch(err) { return ''; } } async rpcGetBlockCount(): Promise<string> { let borBlockCount: string; try { borBlockCount = await this.rpcGetBorBlockCount(); } catch(err) { borBlockCount = ''; } let heimdallBlockCount: string; try { heimdallBlockCount = await this.rpcGetHeimdallBlockCount(); } catch(err) { heimdallBlockCount = ''; } const joinedBlockCount = `${heimdallBlockCount}/${borBlockCount}`; return joinedBlockCount === '/' ? '' : joinedBlockCount; } async rpcGetBorBlockCount(): Promise<string> { return this._rpcGetBlockCount(); } async rpcGetHeimdallBlockCount(): Promise<string> { const { blockCount = '' } = await this.rpcGetHeimdallStatus(); return blockCount; } async rpcGetHeimdallStatus(): Promise<{syncing?: boolean, blockCount?: string}> { try { const statusJson: string = await new Promise((resolve, reject) => { let res = ''; this._docker.exec( this.polygonGenerateHeimdallDockerName(), [], 'curl -s http://localhost:26657/status', output => { res += output; }, err => { reject(err); }, () => { resolve(res); }); }); const status = JSON.parse(statusJson.trim()); const syncing: boolean = status.result.sync_info.catching_up; const blockCount: string = status.result.sync_info.latest_block_height; return { syncing, blockCount, }; } catch(err) { return {}; } } }