/ lib / utils / utils.js
utils.js
  1  let http = require('follow-redirects').http;
  2  let https = require('follow-redirects').https;
  3  const {canonicalHost} = require('./host');
  4  
  5  const balanceRegex = /([0-9]+) ?([a-zA-Z]*)/;
  6  
  7  function joinPath() {
  8    const path = require('path');
  9    return path.join.apply(path.join, arguments);
 10  }
 11  
 12  function dirname() {
 13    const path = require('path');
 14    return path.dirname.apply(path.dirname, arguments);
 15  }
 16  
 17  function filesMatchingPattern(files) {
 18    const globule = require('globule');
 19    return globule.find(files, {nonull: true});
 20  }
 21  
 22  function fileMatchesPattern(patterns, intendedPath) {
 23    const globule = require('globule');
 24    return globule.isMatch(patterns, intendedPath);
 25  }
 26  
 27  function recursiveMerge(target, source) {
 28    const merge = require('merge');
 29    return merge.recursive(target, source);
 30  }
 31  
 32  function checkIsAvailable(url, callback) {
 33    http.get(url, function (_res) {
 34      callback(true);
 35    }).on('error', function (_res) {
 36      callback(false);
 37    });
 38  }
 39  
 40  function httpGetRequest(httpObj, url, callback) {
 41    httpObj.get(url, function (res) {
 42      let body = '';
 43      res.on('data', function (d) {
 44        body += d;
 45      });
 46      res.on('end', function () {
 47        callback(null, body);
 48      });
 49    }).on('error', function (err) {
 50      callback(err);
 51    });
 52  }
 53  
 54  function httpGet(url, callback) {
 55    httpGetRequest(http, url, callback);
 56  }
 57  
 58  function httpsGet(url, callback) {
 59    httpGetRequest(https, url, callback);
 60  }
 61  
 62  function httpGetJson(url, callback) {
 63    httpGetRequest(http, url, function (err, body) {
 64      try {
 65        let parsed = JSON.parse(body);
 66        return callback(err, parsed);
 67      } catch (e) {
 68        return callback(e);
 69      }
 70    });
 71  }
 72  
 73  function httpsGetJson(url, callback) {
 74    httpGetRequest(https, url, function (err, body) {
 75      try {
 76        let parsed = JSON.parse(body);
 77        return callback(err, parsed);
 78      } catch (e) {
 79        return callback(e);
 80      }
 81    });
 82  }
 83  
 84  function getJson(url, cb) {
 85    if (url.indexOf('https') === 0) {
 86      return httpsGetJson(url, cb);
 87    }
 88    httpGetJson(url, cb);
 89  }
 90  
 91  function pingEndpoint(host, port, type, protocol, origin, callback) {
 92    const options = {
 93      protocolVersion: 13,
 94      perMessageDeflate: true,
 95      origin: origin,
 96      host: host,
 97      port: port
 98    };
 99    if (type === 'ws') {
100      options.headers = {
101        'Sec-WebSocket-Version': 13,
102        Connection: 'Upgrade',
103        Upgrade: 'websocket',
104        'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
105        Origin: origin
106      };
107    }
108    let req;
109    // remove trailing api key from infura, ie rinkeby.infura.io/nmY8WtT4QfEwz2S7wTbl
110    if (options.host.indexOf('/') > -1) {
111      options.host = options.host.split('/')[0];
112    }
113    if (protocol === 'https') {
114      req = require('https').get(options);
115    } else {
116      req = require('http').get(options);
117    }
118  
119    req.on('error', (err) => {
120      callback(err);
121    });
122  
123    req.on('response', (_response) => {
124      callback();
125    });
126  
127    req.on('upgrade', (_res, _socket, _head) => {
128      callback();
129    });
130  }
131  
132  function runCmd(cmd, options, callback) {
133    const shelljs = require('shelljs');
134    options = Object.assign({silent: true, exitOnError: true, async: true}, options || {});
135    const outputToConsole = !options.silent;
136    options.silent = true;
137    let result = shelljs.exec(cmd, options, function (code, stdout) {
138      if(code !== 0) {
139        if (options.exitOnError) {
140          return exit();
141        }
142        if(typeof callback === 'function') {
143          callback(`shell returned code ${code}`);
144        }
145      } else {
146        if(typeof callback === 'function') {
147          return callback(null, stdout);
148        }
149      }
150    });
151  
152    result.stdout.on('data', function(data) {
153      if(outputToConsole) {
154        console.log(data);
155      }
156    });
157  
158    result.stderr.on('data', function(data) {
159      if (outputToConsole) {
160        console.log(data);
161      }
162    });
163  }
164  
165  function cd(folder) {
166    const shelljs = require('shelljs');
167    shelljs.cd(folder);
168  }
169  
170  function sed(file, pattern, replace) {
171    const shelljs = require('shelljs');
172    shelljs.sed('-i', pattern, replace, file);
173  }
174  
175  function exit(code) {
176    process.exit(code);
177  }
178  
179  function downloadFile(url, dest, cb) {
180    const o_fs = require('fs-extra');
181    var file = o_fs.createWriteStream(dest);
182    (url.substring(0, 5) === 'https' ? https : http).get(url, function (response) {
183      if (response.statusCode !== 200) {
184        cb(`Download failed, response code ${response.statusCode}`);
185        return;
186      }
187      response.pipe(file);
188      file.on('finish', function () {
189        file.close(cb);
190      });
191    }).on('error', function (err) {
192      o_fs.unlink(dest);
193      cb(err.message);
194    });
195  }
196  
197  function extractTar(filename, packageDirectory, cb) {
198    const o_fs = require('fs-extra');
199    const tar = require('tar');
200    o_fs.createReadStream(filename).pipe(
201      tar.x({
202        strip: 1,
203        C: packageDirectory
204      }).on('end', function () {
205        cb();
206      })
207    );
208  }
209  
210  function extractZip(filename, packageDirectory, opts, cb) {
211    const decompress = require('decompress');
212  
213    decompress(filename, packageDirectory, opts).then((_files) => {
214      cb();
215    });
216  }
217  
218  function proposeAlternative(word, _dictionary, _exceptions) {
219    const propose = require('propose');
220    let exceptions = _exceptions || [];
221    let dictionary = _dictionary.filter((entry) => {
222      return exceptions.indexOf(entry) < 0;
223    });
224    return propose(word, dictionary, {threshold: 0.3});
225  }
226  
227  function getExternalContractUrl(file) {
228    const constants = require('../constants');
229    let url;
230    const RAW_URL = 'https://raw.githubusercontent.com/';
231    const MALFORMED_ERROR = 'Malformed Github URL for ';
232    if (file.startsWith('https://github')) {
233      const match = file.match(/https:\/\/github\.[a-z]+\/(.*)/);
234      if (!match) {
235        console.error(MALFORMED_ERROR + file);
236        return null;
237      }
238      url = `${RAW_URL}${match[1].replace('blob/', '')}`;
239    } else if (file.startsWith('git')) {
240      // Match values
241      // [0] entire input
242      // [1] git://
243      // [2] user
244      // [3] repository
245      // [4] path
246      // [5] branch
247      const match = file.match(
248        /(git:\/\/)?github\.[a-z]+\/([-a-zA-Z0-9@:%_+.~#?&=]+)\/([-a-zA-Z0-9@:%_+.~#?&=]+)\/([-a-zA-Z0-9@:%_+.~?\/&=]+)#?([a-zA-Z0-9\/_.-]*)?/
249      );
250      if (!match) {
251        console.error(MALFORMED_ERROR + file);
252        return null;
253      }
254      let branch = match[5];
255      if (!branch) {
256        branch = 'master';
257      }
258      url = `${RAW_URL}${match[2]}/${match[3]}/${branch}/${match[4]}`;
259    } else if (file.startsWith('http')) {
260      url = file;
261    } else {
262      return null;
263    }
264    const match = url.match(
265      /\.[a-z]+\/([-a-zA-Z0-9@:%_+.~#?&\/=]+)/
266    );
267    return {
268      url,
269      filePath: constants.httpContractsDirectory + match[1]
270    };
271  }
272  
273  function hexToNumber(hex) {
274    const Web3 = require('web3');
275    return Web3.utils.hexToNumber(hex);
276  }
277  
278  function isHex(hex) {
279    const Web3 = require('web3');
280    return Web3.utils.isHex(hex);
281  }
282  
283  function hashTo32ByteHexString(hash) {
284    if (isHex(hash)) {
285      if (!hash.startsWith('0x')) {
286        hash = '0x' + hash;
287      }
288      return hash;
289    }
290    const multihash = require('multihashes');
291    let buf = multihash.fromB58String(hash);
292    let digest = multihash.decode(buf).digest;
293    return '0x' + multihash.toHexString(digest);
294  }
295  
296  function isValidDomain(domain) {
297    const isValidDomain = require('is-valid-domain');
298    return isValidDomain(domain);
299  }
300  
301  function isValidEthDomain(ethDomain) {
302    if (!isValidDomain(ethDomain)) {
303      return false;
304    }
305    return ethDomain.substring(ethDomain.lastIndexOf('.'), ethDomain.length) === '.eth';
306  }
307  
308  function decodeParams(typesArray, hexString) {
309    var Web3EthAbi = require('web3-eth-abi');
310    return Web3EthAbi.decodeParameters(typesArray, hexString);
311  }
312  
313  function toChecksumAddress(address) {
314    const Web3 = require('web3');
315    return Web3.utils.toChecksumAddress(address);
316  }
317  
318  function sha3(arg) {
319    const Web3 = require('web3');
320    return Web3.utils.sha3(arg);
321  }
322  
323  function soliditySha3(arg) {
324    const Web3 = require('web3');
325    return Web3.utils.soliditySha3(arg);
326  }
327  
328  function normalizeInput(input) {
329    if(typeof input === 'string') return input;
330    let args = Object.values(input);
331    if (args.length === 0) {
332      return "";
333    }
334    if (args.length === 1) {
335      if (Array.isArray(args[0])) {
336        return args[0].join(',');
337      }
338      return args[0] || "";
339    }
340    return ('[' + args.map((x) => {
341      if (x === null) {
342        return "null";
343      }
344      if (x === undefined) {
345        return "undefined";
346      }
347      if (Array.isArray(x)) {
348        return x.join(',');
349      }
350      return x;
351    }).toString() + ']');
352  }
353  
354  /**
355   * Builds a URL
356   *
357   * @param {string} protocol
358   *  The URL protocol, defaults to http.
359   * @param {string} host
360   *  The URL host, required.
361   * @param {string} port
362   *  The URL port, default to empty string.
363   * @param {string} [type]
364   *  Type of connection
365   * @returns {string} the constructued URL, with defaults
366   */
367  function buildUrl(protocol, host, port, type) {
368    if (!host) throw new Error('utils.buildUrl: parameter \'host\' is required');
369    if (port) port = ':' + port;
370    else port = '';
371    if (!protocol) {
372      protocol = type === 'ws' ? 'ws' : 'http';
373    }
374    return `${protocol}://${host}${port}`;
375  }
376  
377  /**
378   * Builds a URL
379   *
380   * @param {object} configObj Object containing protocol, host, and port to be used to construct the url.
381   *      * protocol      {String}    (optional) The URL protocol, defaults to http.
382   *      * host          {String}    (required) The URL host.
383   *      * port          {String}    (optional) The URL port, default to empty string.
384   * @returns {string} the constructued URL, with defaults
385   */
386  function buildUrlFromConfig(configObj) {
387    if (!configObj) throw new Error('[utils.buildUrlFromConfig]: config object must cannot be null');
388    if (!configObj.host) throw new Error('[utils.buildUrlFromConfig]: object must contain a \'host\' property');
389    return this.buildUrl(configObj.protocol, canonicalHost(configObj.host), configObj.port, configObj.type);
390  }
391  
392  function deconstructUrl(endpoint) {
393    const matches = endpoint.match(/(ws|https?):\/\/([a-zA-Z0-9_.-]*):?([0-9]*)?/);
394    return {
395      protocol: matches[1],
396      host: matches[2],
397      port: matches[3],
398      type: matches[1] === 'ws' ? 'ws' : 'rpc'
399    };
400  }
401  
402  function getWeiBalanceFromString(balanceString, web3){
403    if(!web3){
404      throw new Error(__('[utils.getWeiBalanceFromString]: Missing parameter \'web3\''));
405    }
406    if (!balanceString) {
407      return 0;
408    }
409    const match = balanceString.match(balanceRegex);
410    if (!match) {
411      throw new Error(__('Unrecognized balance string "%s"', balanceString));
412    }
413    if (!match[2]) {
414      return web3.utils.toHex(parseInt(match[1], 10));
415    }
416  
417    return web3.utils.toWei(match[1], match[2]);
418  }
419  
420  function getHexBalanceFromString(balanceString, web3) {
421    if(!web3){
422      throw new Error(__('[utils.getWeiBalanceFromString]: Missing parameter \'web3\''));
423    }
424    if (!balanceString) {
425      return 0xFFFFFFFFFFFFFFFFFF;
426    }
427    if (web3.utils.isHexStrict(balanceString)) {
428      return balanceString;
429    }
430    const match = balanceString.match(balanceRegex);
431    if (!match) {
432      throw new Error(__('Unrecognized balance string "%s"', balanceString));
433    }
434    if (!match[2]) {
435      return web3.utils.toHex(parseInt(match[1], 10));
436    }
437  
438    return web3.utils.toHex(web3.utils.toWei(match[1], match[2]));
439  }
440  
441  function compact(array) {
442    return array.filter(n => n);
443  }
444  
445  function groupBy(array, key) {
446    return array.reduce(function (rv, x) {
447      (rv[x[key]] = rv[x[key]] || []).push(x);
448      return rv;
449    }, {});
450  }
451  
452  function sample(array) {
453    return array[Math.floor(Math.random() * array.length)];
454  }
455  
456  function last(array) {
457    return array[array.length - 1];
458  }
459  
460  function interceptLogs(consoleContext, logger) {
461    let context = {};
462    context.console = consoleContext;
463  
464    context.console.log = function () {
465      logger.info(normalizeInput(arguments));
466    };
467    context.console.warn = function () {
468      logger.warn(normalizeInput(arguments));
469    };
470    context.console.info = function () {
471      logger.info(normalizeInput(arguments));
472    };
473    context.console.debug = function () {
474      // TODO: ue JSON.stringify
475      logger.debug(normalizeInput(arguments));
476    };
477    context.console.trace = function () {
478      logger.trace(normalizeInput(arguments));
479    };
480    context.console.dir = function () {
481      logger.dir(normalizeInput(arguments));
482    };
483  }
484  
485  function errorMessage(e) {
486    if (typeof e === 'string') {
487      return e;
488    } else if (e && e.message) {
489      return e.message;
490    }
491    return e;
492  }
493  
494  function timer(ms) {
495    return new Promise(resolve => setTimeout(resolve, ms));
496  }
497  
498  function isFolder(node) {
499    return node.children && node.children.length;
500  }
501  
502  function isNotFolder(node){
503    return !isFolder(node);
504  }
505  
506  function byName(a, b) {
507      return a.name.localeCompare(b.name);
508  }
509  
510  function fileTreeSort(nodes){
511    const folders = nodes.filter(isFolder).sort(byName);
512    const files = nodes.filter(isNotFolder).sort(byName);
513  
514    return folders.concat(files);
515  }
516  
517  function copyToClipboard(text) {
518    const clipboardy = require('clipboardy');
519    clipboardy.writeSync(text);
520  }
521  
522  function fuzzySearch(text, list, filter) {
523    const fuzzy = require('fuzzy');
524    return fuzzy.filter(text, list, {extract: (filter || function () {})})
525  }
526  
527  module.exports = {
528    joinPath,
529    dirname,
530    filesMatchingPattern,
531    fileMatchesPattern,
532    recursiveMerge,
533    checkIsAvailable,
534    httpGet,
535    httpsGet,
536    httpGetJson,
537    httpsGetJson,
538    getJson,
539    hexToNumber,
540    isHex,
541    hashTo32ByteHexString,
542    isValidDomain,
543    isValidEthDomain,
544    pingEndpoint,
545    decodeParams,
546    runCmd,
547    cd,
548    sed,
549    exit,
550    downloadFile,
551    extractTar,
552    extractZip,
553    proposeAlternative,
554    getExternalContractUrl,
555    toChecksumAddress,
556    sha3,
557    soliditySha3,
558    normalizeInput,
559    buildUrl,
560    buildUrlFromConfig,
561    deconstructUrl,
562    getWeiBalanceFromString,
563    getHexBalanceFromString,
564    compact,
565    groupBy,
566    sample,
567    last,
568    interceptLogs,
569    errorMessage,
570    timer,
571    fileTreeSort,
572    copyToClipboard,
573    fuzzySearch
574  };