gethClient.js
1 const async = require('async'); 2 const GethMiner = require('./miner'); 3 const os = require('os'); 4 5 const DEFAULTS = { 6 "BIN": "geth", 7 "NETWORK_TYPE": "custom", 8 "NETWORK_ID": 1337, 9 "RPC_API": ['eth', 'web3', 'net', 'debug'], 10 "WS_API": ['eth', 'web3', 'net', 'shh', 'debug', 'pubsub'], 11 "DEV_WS_API": ['eth', 'web3', 'net', 'shh', 'debug', 'pubsub', 'personal'], 12 "TARGET_GAS_LIMIT": 8000000 13 }; 14 15 // TODO: make all of this async 16 class GethClient { 17 18 static get DEFAULTS() { 19 return DEFAULTS; 20 } 21 22 constructor(options) { 23 this.config = options && options.hasOwnProperty('config') ? options.config : {}; 24 this.env = options && options.hasOwnProperty('env') ? options.env : 'development'; 25 this.isDev = options && options.hasOwnProperty('isDev') ? options.isDev : (this.env === 'development'); 26 this.name = "geth"; 27 this.prettyName = "Go-Ethereum (https://github.com/ethereum/go-ethereum)"; 28 this.bin = this.config.ethereumClientBin || DEFAULTS.BIN; 29 this.httpReady = false; 30 this.wsReady = !this.config.wsRPC; 31 } 32 33 isReady(data) { 34 if (data.indexOf('HTTP endpoint opened') > -1) { 35 this.httpReady = true; 36 } 37 if (data.indexOf('WebSocket endpoint opened') > -1) { 38 this.wsReady = true; 39 } 40 return this.httpReady && this.wsReady; 41 } 42 43 /** 44 * Check if the client needs some sort of 'keep alive' transactions to avoid freezing by inactivity 45 * @returns {boolean} if keep alive is needed 46 */ 47 needKeepAlive() { 48 // TODO: check version also (geth version < 1.8.15) 49 if (this.isDev) { 50 // Trigger regular txs due to a bug in geth (< 1.8.15) and stuck transactions in --dev mode. 51 return true; 52 } 53 return false; 54 } 55 56 commonOptions(firstAccount = false) { 57 let config = this.config; 58 let cmd = []; 59 60 cmd.push(this.determineNetworkType(config)); 61 62 if (config.datadir) { 63 cmd.push(`--datadir=${config.datadir}`); 64 } 65 66 if (config.syncMode) { 67 cmd.push("--syncmode=" + config.syncMode); 68 } 69 70 // geth in dev mode needs the first account to have a blank password, so we use for convenience the same Parity's devpassword 71 if (config.account && config.account.password) { 72 if (firstAccount) cmd.push(`--password=${config.account.devPassword}`); 73 else cmd.push(`--password=${config.account.password}`); 74 } 75 76 if (Number.isInteger(config.verbosity) && config.verbosity >= 0 && config.verbosity <= 5) { 77 cmd.push("--verbosity=" + config.verbosity); 78 } 79 80 return cmd; 81 } 82 83 getMiner() { 84 return new GethMiner({datadir: this.config.datadir}); 85 } 86 87 getBinaryPath() { 88 return this.bin; 89 } 90 91 determineVersionCommand() { 92 return this.bin + " version"; 93 } 94 95 determineNetworkType(config) { 96 let cmd; 97 if (config.networkType === 'testnet') { 98 cmd = "--testnet"; 99 } else if (config.networkType === 'rinkeby') { 100 cmd = "--rinkeby"; 101 } else if (config.networkType === 'custom') { 102 cmd = "--networkid=" + config.networkId; 103 } 104 return cmd; 105 } 106 107 initGenesisCommmand() { 108 let config = this.config; 109 let cmd = this.bin + " " + this.commonOptions().join(' '); 110 if (config.genesisBlock) { 111 cmd += " init \"" + config.genesisBlock + "\" "; 112 } 113 return cmd; 114 } 115 116 newAccountCommand(firstAccount = false) { 117 if (!(this.config.account && this.config.account.password)) { 118 console.warn(__('Your blockchain is missing a password and creating an account may fail. Please consider updating ').yellow + __('config/blockchain > account > password').cyan + __(' then re-run the command').yellow); 119 } 120 return this.bin + " " + this.commonOptions(firstAccount).join(' ') + " account new "; 121 } 122 123 parseNewAccountCommandResultToAddress(data = "") { 124 if (data.match(/{(\w+)}/)) return "0x" + data.match(/{(\w+)}/)[1]; 125 return ""; 126 } 127 128 listAccountsCommand() { 129 return this.bin + " " + this.commonOptions().join(' ') + " account list "; 130 } 131 132 parseListAccountsCommandResultToAddress(data = "") { 133 if (data.match(/{(\w+)}/)) return "0x" + data.match(/{(\w+)}/)[1]; 134 return ""; 135 } 136 137 parseListAccountsCommandResultToAddressList(data = "") { 138 let list = data.split(os.EOL); 139 list.pop(); // Remove empty value 140 return list.map(el => "0x" + el.match(/{(\w+)}/)[1]); 141 } 142 143 parseListAccountsCommandResultToAddressCount(data = "") { 144 const count = this.parseListAccountsCommandResultToAddressList(data).length; 145 return (count > 0 ? count : 0); 146 } 147 148 determineRpcOptions(config) { 149 let cmd = []; 150 cmd.push("--port=" + config.port); 151 cmd.push("--rpc"); 152 cmd.push("--rpcport=" + config.rpcPort); 153 cmd.push("--rpcaddr=" + config.rpcHost); 154 if (config.rpcCorsDomain) { 155 if (config.rpcCorsDomain === '*') { 156 console.warn('=================================='); 157 console.warn(__('rpcCorsDomain set to *')); 158 console.warn(__('make sure you know what you are doing')); 159 console.warn('=================================='); 160 } 161 cmd.push("--rpccorsdomain=" + config.rpcCorsDomain); 162 } else { 163 console.warn('=================================='); 164 console.warn(__('warning: cors is not set')); 165 console.warn('=================================='); 166 } 167 return cmd; 168 } 169 170 determineWsOptions(config) { 171 let cmd = []; 172 if (config.wsRPC) { 173 cmd.push("--ws"); 174 cmd.push("--wsport=" + config.wsPort); 175 cmd.push("--wsaddr=" + config.wsHost); 176 if (config.wsOrigins) { 177 if (config.wsOrigins === '*') { 178 console.warn('=================================='); 179 console.warn(__('wsOrigins set to *')); 180 console.warn(__('make sure you know what you are doing')); 181 console.warn('=================================='); 182 } 183 cmd.push("--wsorigins=" + config.wsOrigins); 184 } else { 185 console.warn('=================================='); 186 console.warn(__('warning: wsOrigins is not set')); 187 console.warn('=================================='); 188 } 189 } 190 return cmd; 191 } 192 193 initDevChain(datadir, callback) { 194 // No specific configuration needed for the dev chain 195 return callback(); 196 } 197 198 mainCommand(address, done) { 199 let self = this; 200 let config = this.config; 201 let rpc_api = this.config.rpcApi; 202 let ws_api = this.config.wsApi; 203 let args = []; 204 async.series([ 205 function commonOptions(callback) { 206 let cmd = self.commonOptions(); 207 args = args.concat(cmd); 208 callback(null, cmd); 209 }, 210 function rpcOptions(callback) { 211 let cmd = self.determineRpcOptions(self.config); 212 args = args.concat(cmd); 213 callback(null, cmd); 214 }, 215 function wsOptions(callback) { 216 let cmd = self.determineWsOptions(self.config); 217 args = args.concat(cmd); 218 callback(null, cmd); 219 }, 220 function dontGetPeers(callback) { 221 if (config.nodiscover) { 222 args.push("--nodiscover"); 223 return callback(null, "--nodiscover"); 224 } 225 callback(null, ""); 226 }, 227 function vmDebug(callback) { 228 if (config.vmdebug) { 229 args.push("--vmdebug"); 230 return callback(null, "--vmdebug"); 231 } 232 callback(null, ""); 233 }, 234 function maxPeers(callback) { 235 let cmd = "--maxpeers=" + config.maxpeers; 236 args.push(cmd); 237 callback(null, cmd); 238 }, 239 function mining(callback) { 240 if (config.mineWhenNeeded || config.mine) { 241 args.push("--mine"); 242 return callback(null, "--mine"); 243 } 244 callback(""); 245 }, 246 function bootnodes(callback) { 247 if (config.bootnodes && config.bootnodes !== "" && config.bootnodes !== []) { 248 args.push("--bootnodes=" + config.bootnodes); 249 return callback(null, "--bootnodes=" + config.bootnodes); 250 } 251 callback(""); 252 }, 253 function whisper(callback) { 254 if (config.whisper) { 255 rpc_api.push('shh'); 256 if (ws_api.indexOf('shh') === -1) { 257 ws_api.push('shh'); 258 } 259 args.push("--shh"); 260 return callback(null, "--shh "); 261 } 262 callback(""); 263 }, 264 function rpcApi(callback) { 265 args.push('--rpcapi=' + rpc_api.join(',')); 266 callback(null, '--rpcapi=' + rpc_api.join(',')); 267 }, 268 function wsApi(callback) { 269 args.push('--wsapi=' + ws_api.join(',')); 270 callback(null, '--wsapi=' + ws_api.join(',')); 271 }, 272 function accountToUnlock(callback) { 273 if (self.isDev && self.config.unlockAddressList) { 274 // The first address is the dev account, that is automatically unlocked by the client using blank password 275 args.push("--unlock=" + self.config.unlockAddressList.slice(1)); 276 return callback(null, "--unlock=" + self.config.unlockAddressList.slice(1)); 277 } 278 let accountAddress = ""; 279 if (config.account && config.account.address) { 280 accountAddress = config.account.address; 281 } else { 282 accountAddress = address; 283 } 284 if (accountAddress) { 285 if(!(self.config && self.config.account && self.config.account.password)){ 286 console.warn(__("\n===== Password needed =====\nPassword for account {{account}} not found. Unlocking this account may fail. Please ensure a password is specified in config/blockchain.js > {{env}} > account > password.\n", {account: address, env: self.env})); 287 } 288 args.push("--unlock=" + accountAddress); 289 return callback(null, "--unlock=" + accountAddress); 290 } 291 callback(null, ""); 292 }, 293 function gasLimit(callback) { 294 if (config.targetGasLimit) { 295 args.push("--miner.gastarget=" + config.targetGasLimit); 296 return callback(null, "--miner.gastarget=" + config.targetGasLimit); 297 } 298 callback(null, ""); 299 }, 300 function isDev(callback) { 301 if (self.isDev) { 302 args.push('--dev'); 303 return callback(null, '--dev'); 304 } 305 callback(null, ''); 306 } 307 ], function(err) { 308 if (err) { 309 throw new Error(err.message); 310 } 311 return done(self.bin, args); 312 }); 313 } 314 } 315 316 module.exports = GethClient;