From 5fd43d8397972578c63b1aebe13bddf5cd58fb9b Mon Sep 17 00:00:00 2001 From: Iuri Matias Date: Tue, 30 Jan 2018 15:37:27 -0500 Subject: [PATCH] move compiler code from browser-solidity including txHelper --- .../src/compiler/compiler-imports.js | 85 +++++ remix-solidity/src/compiler/compiler-input.js | 21 + .../src/compiler/compiler-worker.js | 45 +++ remix-solidity/src/compiler/compiler.js | 358 ++++++++++++++++++ remix-solidity/src/compiler/txHelper.js | 129 +++++++ 5 files changed, 638 insertions(+) create mode 100644 remix-solidity/src/compiler/compiler-imports.js create mode 100644 remix-solidity/src/compiler/compiler-input.js create mode 100644 remix-solidity/src/compiler/compiler-worker.js create mode 100644 remix-solidity/src/compiler/compiler.js create mode 100644 remix-solidity/src/compiler/txHelper.js diff --git a/remix-solidity/src/compiler/compiler-imports.js b/remix-solidity/src/compiler/compiler-imports.js new file mode 100644 index 0000000000..a6162cb972 --- /dev/null +++ b/remix-solidity/src/compiler/compiler-imports.js @@ -0,0 +1,85 @@ +'use strict' +// TODO: can just use request or fetch instead +var $ = require('jquery') +var base64 = require('js-base64').Base64 +var swarmgw = require('swarmgw') + +module.exports = { + handleGithubCall: function (root, path, cb) { + return $.getJSON('https://api.github.com/repos/' + root + '/contents/' + path) + .done(function (data) { + if ('content' in data) { + cb(null, base64.decode(data.content), root + '/' + path) + } else { + cb('Content not received') + } + }) + .fail(function (xhr, text, err) { + // NOTE: on some browsers, err equals to '' for certain errors (such as offline browser) + cb(err || 'Unknown transport error') + }) + }, + + handleSwarmImport: function (url, cb) { + swarmgw.get(url, function (err, content) { + cb(err, content, url) + }) + }, + + handleIPFS: function (url, cb) { + // replace ipfs:// with /ipfs/ + url = url.replace(/^ipfs:\/\/?/, 'ipfs/') + + return $.ajax({ type: 'GET', url: 'https://gateway.ipfs.io/' + url }) + .done(function (data) { + cb(null, data, url) + }) + .fail(function (xhr, text, err) { + // NOTE: on some browsers, err equals to '' for certain errors (such as offline browser) + cb(err || 'Unknown transport error') + }) + }, + + handlers: function () { + return [ + { type: 'github', match: /^(https?:\/\/)?(www.)?github.com\/([^/]*\/[^/]*)\/(.*)/, handler: (match, cb) => { this.handleGithubCall(match[3], match[4], cb) } }, + { type: 'swarm', match: /^(bzz[ri]?:\/\/?.*)$/, handler: (match, cb) => { this.handleSwarmImport(match[1], cb) } }, + { type: 'ipfs', match: /^(ipfs:\/\/?.+)/, handler: (match, cb) => { this.handleIPFS(match[1], cb) } } + ] + }, + + import: function (url, cb) { + var handlers = this.handlers() + + var found = false + handlers.forEach(function (handler) { + if (found) { + return + } + + var match = handler.match.exec(url) + if (match) { + found = true + + // TODO: this needs to be moved to the caller + $('#output').append($('
').append($('
').text('Loading ' + url + ' ...')))
+        handler.handler(match, function (err, content, cleanUrl) {
+          if (err) {
+            cb('Unable to import "' + cleanUrl + '": ' + err)
+            return
+          }
+
+          cb(null, content, cleanUrl, handler.type, url)
+        })
+      }
+    })
+
+    if (found) {
+      return
+    } else if (/^[^:]*:\/\//.exec(url)) {
+      cb('Unable to import "' + url + '": Unsupported URL schema')
+    } else {
+      cb('Unable to import "' + url + '": File not found')
+    }
+  }
+}
diff --git a/remix-solidity/src/compiler/compiler-input.js b/remix-solidity/src/compiler/compiler-input.js
new file mode 100644
index 0000000000..0929a3be3d
--- /dev/null
+++ b/remix-solidity/src/compiler/compiler-input.js
@@ -0,0 +1,21 @@
+'use strict'
+
+module.exports = (sources, opts) => {
+  return JSON.stringify({
+    language: 'Solidity',
+    sources: sources,
+    settings: {
+      optimizer: {
+        enabled: opts.optimize === true || opts.optimize === 1,
+        runs: 200
+      },
+      libraries: opts.libraries,
+      outputSelection: {
+        '*': {
+          '': [ 'legacyAST' ],
+          '*': [ 'abi', 'metadata', 'evm.legacyAssembly', 'evm.bytecode', 'evm.deployedBytecode', 'evm.methodIdentifiers', 'evm.gasEstimates' ]
+        }
+      }
+    }
+  })
+}
diff --git a/remix-solidity/src/compiler/compiler-worker.js b/remix-solidity/src/compiler/compiler-worker.js
new file mode 100644
index 0000000000..14e630aeaf
--- /dev/null
+++ b/remix-solidity/src/compiler/compiler-worker.js
@@ -0,0 +1,45 @@
+'use strict'
+
+var solc = require('solc/wrapper')
+
+var compileJSON = function () { return '' }
+var missingInputs = []
+
+module.exports = function (self) {
+  self.addEventListener('message', function (e) {
+    var data = e.data
+    switch (data.cmd) {
+      case 'loadVersion':
+        delete self.Module
+        // NOTE: workaround some browsers?
+        self.Module = undefined
+
+        compileJSON = null
+
+        self.importScripts(data.data)
+
+        var compiler = solc(self.Module)
+
+        compileJSON = function (input) {
+          try {
+            return compiler.compileStandardWrapper(input, function (path) {
+              missingInputs.push(path)
+              return { 'error': 'Deferred import' }
+            })
+          } catch (exception) {
+            return JSON.stringify({ error: 'Uncaught JavaScript exception:\n' + exception })
+          }
+        }
+
+        self.postMessage({
+          cmd: 'versionLoaded',
+          data: compiler.version()
+        })
+        break
+      case 'compile':
+        missingInputs.length = 0
+        self.postMessage({cmd: 'compiled', job: data.job, data: compileJSON(data.input), missingInputs: missingInputs})
+        break
+    }
+  }, false)
+}
diff --git a/remix-solidity/src/compiler/compiler.js b/remix-solidity/src/compiler/compiler.js
new file mode 100644
index 0000000000..d5b59cc567
--- /dev/null
+++ b/remix-solidity/src/compiler/compiler.js
@@ -0,0 +1,358 @@
+'use strict'
+
+var solc = require('solc/wrapper')
+var solcABI = require('solc/abi')
+
+var webworkify = require('webworkify')
+
+var compilerInput = require('./compiler-input')
+
+var remixLib = require('remix-lib')
+var EventManager = remixLib.EventManager
+
+var txHelper = require('./txHelper')
+
+/*
+  trigger compilationFinished, compilerLoaded, compilationStarted, compilationDuration
+*/
+function Compiler (handleImportCall) {
+  var self = this
+  this.event = new EventManager()
+
+  var compileJSON
+
+  var worker = null
+
+  var currentVersion
+
+  var optimize = false
+
+  this.setOptimize = function (_optimize) {
+    optimize = _optimize
+  }
+
+  var compilationStartTime = null
+  this.event.register('compilationFinished', (success, data, source) => {
+    if (success && compilationStartTime) {
+      this.event.trigger('compilationDuration', [(new Date().getTime()) - compilationStartTime])
+    }
+    compilationStartTime = null
+  })
+
+  this.event.register('compilationStarted', () => {
+    compilationStartTime = new Date().getTime()
+  })
+
+  var internalCompile = function (files, target, missingInputs) {
+    gatherImports(files, target, missingInputs, function (error, input) {
+      if (error) {
+        self.lastCompilationResult = null
+        self.event.trigger('compilationFinished', [false, {'error': { formattedMessage: error, severity: 'error' }}, files])
+      } else {
+        compileJSON(input, optimize ? 1 : 0)
+      }
+    })
+  }
+
+  var compile = function (files, target) {
+    self.event.trigger('compilationStarted', [])
+    internalCompile(files, target)
+  }
+  this.compile = compile
+
+  function setCompileJSON (_compileJSON) {
+    compileJSON = _compileJSON
+  }
+  this.setCompileJSON = setCompileJSON // this is exposed for testing
+
+  function onCompilerLoaded (version) {
+    currentVersion = version
+    self.event.trigger('compilerLoaded', [version])
+  }
+
+  function onInternalCompilerLoaded () {
+    if (worker === null) {
+      var compiler = solc(window.Module)
+
+      compileJSON = function (source, optimize, cb) {
+        var missingInputs = []
+        var missingInputsCallback = function (path) {
+          missingInputs.push(path)
+          return { error: 'Deferred import' }
+        }
+
+        var result
+        try {
+          var input = compilerInput(source.sources, {optimize: optimize, target: source.target})
+          result = compiler.compileStandardWrapper(input, missingInputsCallback)
+          result = JSON.parse(result)
+        } catch (exception) {
+          result = { error: 'Uncaught JavaScript exception:\n' + exception }
+        }
+
+        compilationFinished(result, missingInputs, source)
+      }
+      onCompilerLoaded(compiler.version())
+    }
+  }
+
+  this.lastCompilationResult = {
+    data: null,
+    source: null
+  }
+
+  /**
+    * return the contract obj of the given @arg name. Uses last compilation result.
+    * return null if not found
+    * @param {String} name    - contract name
+    * @returns contract obj and associated file: { contract, file } or null
+    */
+  this.getContract = (name) => {
+    if (this.lastCompilationResult.data && this.lastCompilationResult.data.contracts) {
+      return txHelper.getContract(name, this.lastCompilationResult.data.contracts)
+    }
+    return null
+  }
+
+  /**
+    * call the given @arg cb (function) for all the contracts. Uses last compilation result
+    * @param {Function} cb    - callback
+    */
+  this.visitContracts = (cb) => {
+    if (this.lastCompilationResult.data && this.lastCompilationResult.data.contracts) {
+      return txHelper.visitContracts(this.lastCompilationResult.data.contracts, cb)
+    }
+    return null
+  }
+
+  /**
+    * return the compiled contracts from the last compilation result
+    * @return {Object}     - contracts
+    */
+  this.getContracts = () => {
+    if (this.lastCompilationResult.data && this.lastCompilationResult.data.contracts) {
+      return this.lastCompilationResult.data.contracts
+    }
+    return null
+  }
+
+   /**
+    * return the sources from the last compilation result
+    * @param {Object} cb    - map of sources
+    */
+  this.getSources = () => {
+    if (this.lastCompilationResult.source) {
+      return this.lastCompilationResult.source.sources
+    }
+    return null
+  }
+
+  /**
+    * return the sources @arg fileName from the last compilation result
+    * @param {Object} cb    - map of sources
+    */
+  this.getSource = (fileName) => {
+    if (this.lastCompilationResult.source) {
+      return this.lastCompilationResult.source.sources[fileName]
+    }
+    return null
+  }
+
+  /**
+    * return the source from the last compilation result that has the given index. null if source not found
+    * @param {Int} index    - index of the source
+    */
+  this.getSourceName = (index) => {
+    if (this.lastCompilationResult.data && this.lastCompilationResult.data.sources) {
+      return Object.keys(this.lastCompilationResult.data.sources)[index]
+    }
+    return null
+  }
+
+  function compilationFinished (data, missingInputs, source) {
+    var noFatalErrors = true // ie warnings are ok
+
+    function isValidError (error) {
+      // The deferred import is not a real error
+      // FIXME: maybe have a better check?
+      if (/Deferred import/.exec(error.message)) {
+        return false
+      }
+
+      return error.severity !== 'warning'
+    }
+
+    if (data['error'] !== undefined) {
+      // Ignore warnings (and the 'Deferred import' error as those are generated by us as a workaround
+      if (isValidError(data['error'])) {
+        noFatalErrors = false
+      }
+    }
+    if (data['errors'] !== undefined) {
+      data['errors'].forEach(function (err) {
+        // Ignore warnings and the 'Deferred import' error as those are generated by us as a workaround
+        if (isValidError(err)) {
+          noFatalErrors = false
+        }
+      })
+    }
+
+    if (!noFatalErrors) {
+      // There are fatal errors - abort here
+      self.lastCompilationResult = null
+      self.event.trigger('compilationFinished', [false, data, source])
+    } else if (missingInputs !== undefined && missingInputs.length > 0) {
+      // try compiling again with the new set of inputs
+      internalCompile(source.sources, source.target, missingInputs)
+    } else {
+      data = updateInterface(data)
+
+      self.lastCompilationResult = {
+        data: data,
+        source: source
+      }
+      self.event.trigger('compilationFinished', [true, data, source])
+    }
+  }
+
+  this.loadVersion = function (usingWorker, url) {
+    console.log('Loading ' + url + ' ' + (usingWorker ? 'with worker' : 'without worker'))
+    self.event.trigger('loadingCompiler', [url, usingWorker])
+
+    if (usingWorker) {
+      loadWorker(url)
+    } else {
+      loadInternal(url)
+    }
+  }
+
+  function loadInternal (url) {
+    delete window.Module
+    // NOTE: workaround some browsers?
+    window.Module = undefined
+
+    // Set a safe fallback until the new one is loaded
+    setCompileJSON(function (source, optimize) {
+      compilationFinished({ error: { formattedMessage: 'Compiler not yet loaded.' } })
+    })
+
+    var newScript = document.createElement('script')
+    newScript.type = 'text/javascript'
+    newScript.src = url
+    document.getElementsByTagName('head')[0].appendChild(newScript)
+    var check = window.setInterval(function () {
+      if (!window.Module) {
+        return
+      }
+      window.clearInterval(check)
+      onInternalCompilerLoaded()
+    }, 200)
+  }
+
+  function loadWorker (url) {
+    if (worker !== null) {
+      worker.terminate()
+    }
+    worker = webworkify(require('./compiler-worker.js'))
+    var jobs = []
+    worker.addEventListener('message', function (msg) {
+      var data = msg.data
+      switch (data.cmd) {
+        case 'versionLoaded':
+          onCompilerLoaded(data.data)
+          break
+        case 'compiled':
+          var result
+          try {
+            result = JSON.parse(data.data)
+          } catch (exception) {
+            result = { 'error': 'Invalid JSON output from the compiler: ' + exception }
+          }
+          var sources = {}
+          if (data.job in jobs !== undefined) {
+            sources = jobs[data.job].sources
+            delete jobs[data.job]
+          }
+          compilationFinished(result, data.missingInputs, sources)
+          break
+      }
+    })
+    worker.onerror = function (msg) {
+      compilationFinished({ error: 'Worker error: ' + msg.data })
+    }
+    worker.addEventListener('error', function (msg) {
+      compilationFinished({ error: 'Worker error: ' + msg.data })
+    })
+    compileJSON = function (source, optimize) {
+      jobs.push({sources: source})
+      worker.postMessage({cmd: 'compile', job: jobs.length - 1, input: compilerInput(source.sources, {optimize: optimize, target: source.target})})
+    }
+    worker.postMessage({cmd: 'loadVersion', data: url})
+  }
+
+  function gatherImports (files, target, importHints, cb) {
+    importHints = importHints || []
+
+    // FIXME: This will only match imports if the file begins with one.
+    //        It should tokenize by lines and check each.
+    // eslint-disable-next-line no-useless-escape
+    var importRegex = /^\s*import\s*[\'\"]([^\'\"]+)[\'\"];/g
+
+    for (var fileName in files) {
+      var match
+      while ((match = importRegex.exec(files[fileName].content))) {
+        var importFilePath = match[1]
+        if (importFilePath.startsWith('./')) {
+          var path = /(.*\/).*/.exec(target)
+          if (path !== null) {
+            importFilePath = importFilePath.replace('./', path[1])
+          } else {
+            importFilePath = importFilePath.slice(2)
+          }
+        }
+
+        // FIXME: should be using includes or sets, but there's also browser compatibility..
+        if (importHints.indexOf(importFilePath) === -1) {
+          importHints.push(importFilePath)
+        }
+      }
+    }
+
+    while (importHints.length > 0) {
+      var m = importHints.pop()
+      if (m in files) {
+        continue
+      }
+
+      handleImportCall(m, function (err, content) {
+        if (err) {
+          cb(err)
+        } else {
+          files[m] = { content }
+          gatherImports(files, target, importHints, cb)
+        }
+      })
+
+      return
+    }
+
+    cb(null, { 'sources': files, 'target': target })
+  }
+
+  function truncateVersion (version) {
+    var tmp = /^(\d+.\d+.\d+)/.exec(version)
+    if (tmp) {
+      return tmp[1]
+    }
+    return version
+  }
+
+  function updateInterface (data) {
+    txHelper.visitContracts(data.contracts, (contract) => {
+      data.contracts[contract.file][contract.name].abi = solcABI.update(truncateVersion(currentVersion), contract.object.abi)
+    })
+    return data
+  }
+}
+
+module.exports = Compiler
diff --git a/remix-solidity/src/compiler/txHelper.js b/remix-solidity/src/compiler/txHelper.js
new file mode 100644
index 0000000000..81275a835c
--- /dev/null
+++ b/remix-solidity/src/compiler/txHelper.js
@@ -0,0 +1,129 @@
+'use strict'
+var ethJSABI = require('ethereumjs-abi')
+var $ = require('jquery')
+
+module.exports = {
+  encodeParams: function (funABI, args) {
+    var types = []
+    if (funABI.inputs && funABI.inputs.length) {
+      for (var i = 0; i < funABI.inputs.length; i++) {
+        var type = funABI.inputs[i].type
+        types.push(type)
+        if (args.length < types.length) {
+          args.push('')
+        }
+      }
+    }
+
+    // NOTE: the caller will concatenate the bytecode and this
+    //       it could be done here too for consistency
+    return ethJSABI.rawEncode(types, args)
+  },
+
+  encodeFunctionId: function (funABI) {
+    var types = []
+    if (funABI.inputs && funABI.inputs.length) {
+      for (var i = 0; i < funABI.inputs.length; i++) {
+        types.push(funABI.inputs[i].type)
+      }
+    }
+
+    return ethJSABI.methodID(funABI.name, types)
+  },
+
+  sortAbiFunction: function (contractabi) {
+    var abi = contractabi.sort(function (a, b) {
+      if (a.name > b.name) {
+        return -1
+      } else {
+        return 1
+      }
+    }).sort(function (a, b) {
+      if (a.constant === true) {
+        return -1
+      } else {
+        return 1
+      }
+    })
+    return abi
+  },
+
+  getConstructorInterface: function (abi) {
+    var funABI = { 'name': '', 'inputs': [], 'type': 'constructor', 'outputs': [] }
+    if (typeof abi === 'string') {
+      try {
+        abi = JSON.parse(abi)
+      } catch (e) {
+        console.log('exception retrieving ctor abi ' + abi)
+        return funABI
+      }
+    }
+
+    for (var i = 0; i < abi.length; i++) {
+      if (abi[i].type === 'constructor') {
+        funABI.inputs = abi[i].inputs || []
+        break
+      }
+    }
+
+    return funABI
+  },
+
+  getFunction: function (abi, fnName) {
+    for (var i = 0; i < abi.length; i++) {
+      if (abi[i].name === fnName) {
+        return abi[i]
+      }
+    }
+    return null
+  },
+
+  getFallbackInterface: function (abi) {
+    for (var i = 0; i < abi.length; i++) {
+      if (abi[i].type === 'fallback') {
+        return abi[i]
+      }
+    }
+  },
+
+  /**
+    * return the contract obj of the given @arg name. Uses last compilation result.
+    * return null if not found
+    * @param {String} name    - contract name
+    * @returns contract obj and associated file: { contract, file } or null
+    */
+  getContract: (contractName, contracts) => {
+    for (var file in contracts) {
+      if (contracts[file][contractName]) {
+        return { object: contracts[file][contractName], file: file }
+      }
+    }
+    return null
+  },
+
+  /**
+    * call the given @arg cb (function) for all the contracts. Uses last compilation result
+    * stop visiting when cb return true
+    * @param {Function} cb    - callback
+    */
+  visitContracts: (contracts, cb) => {
+    for (var file in contracts) {
+      for (var name in contracts[file]) {
+        if (cb({ name: name, object: contracts[file][name], file: file })) return
+      }
+    }
+  },
+
+  inputParametersDeclarationToString: function (abiinputs) {
+    var inputs = ''
+    if (abiinputs) {
+      $.each(abiinputs, function (i, inp) {
+        if (inputs !== '') {
+          inputs += ', '
+        }
+        inputs += inp.type + ' ' + inp.name
+      })
+    }
+    return inputs
+  }
+}