From e97aeca80022736c8e6460a360395982f0bb0a9e Mon Sep 17 00:00:00 2001 From: yann300 Date: Fri, 17 Apr 2020 09:30:56 +0200 Subject: [PATCH] fetch and compile before debugging --- src/app/compiler/compiler-artefacts.js | 12 ++ ...compiler-sourceVerifier-fetchAndCompile.js | 129 ++++++++++++++++++ src/app/compiler/compiler-utils.js | 5 + src/app/tabs/debugger-tab.js | 38 +++++- src/app/tabs/debugger/debuggerUI.js | 37 +++-- src/app/tabs/test-tab.js | 7 +- src/remixAppManager.js | 8 +- 7 files changed, 211 insertions(+), 25 deletions(-) create mode 100644 src/app/compiler/compiler-sourceVerifier-fetchAndCompile.js diff --git a/src/app/compiler/compiler-artefacts.js b/src/app/compiler/compiler-artefacts.js index 98c9cabb29..ecc25cbe93 100644 --- a/src/app/compiler/compiler-artefacts.js +++ b/src/app/compiler/compiler-artefacts.js @@ -33,4 +33,16 @@ module.exports = class CompilerArtefacts extends Plugin { this.compilersArtefacts['__last'] = new CompilerAbstract(languageVersion, data, source) }) } + + addResolvedContract (address, compilerData) { + this.compilersArtefacts[address] = compilerData + } + + isResolved (address) { + return this.compilersArtefacts[address] !== undefined + } + + get (key) { + return this.compilersArtefacts[key] + } } diff --git a/src/app/compiler/compiler-sourceVerifier-fetchAndCompile.js b/src/app/compiler/compiler-sourceVerifier-fetchAndCompile.js new file mode 100644 index 0000000000..7a01768caa --- /dev/null +++ b/src/app/compiler/compiler-sourceVerifier-fetchAndCompile.js @@ -0,0 +1,129 @@ +import { EventEmitter } from 'events' +import { Compiler } from 'remix-solidity' +import { canUseWorker, urlFromVersion } from './compiler-utils' +import CompilerAbstract from './compiler-abstract' +import remixLib from 'remix-lib' + +class FetchAndCompile { + + constructor () { + this.event = new EventEmitter() + this.compiler = null + this.unresolvedAddresses = [] + this.firstResolvedAddress = null + this.sourceVerifierNetWork = ['Main', 'Rinkeby', 'Ropsten', 'Goerli'] + } + + /** + * Fetch compiliation metadata from source-Verify from a given @arg contractAddress - https://github.com/ethereum/source-verify + * Compile the code using Solidity compiler. + * if no contract address are passed, we default to the first resolved address. + * + * @param {string} contractAddress - Address of the contrac to resolve + * @param {string} compilersartefacts - Object containing a mapping of compilation results (byContractAddress and __last) + * @param {object} pluginAccess - any registered plugin (for making the calls) + * @return {CompilerAbstract} - compilation data targeting the given @arg contractAddress + */ + async resolve (contractAddress, compilersartefacts, pluginAccess, targetPath, web3) { + contractAddress = contractAddress || this.firstResolvedAddress + + const localCompilation = () => compilersartefacts.get('__last') ? compilersartefacts.get('__last') : null + + const resolved = compilersartefacts.get(contractAddress) + if (resolved) return resolved + if (this.unresolvedAddresses.includes(contractAddress)) return localCompilation() + if (this.firstResolvedAddress) return compilersartefacts.get(this.firstResolvedAddress) + // ^ for the moment we don't add compilation result for each adddress, but just for the root addres ^ . We can add this later. + // usecase is: sometimes when doing an internal call, the only available artifact is the Solidity interface. + // resolving addresses of internal call would allow to step over the source code, even if the declaration was made using an Interface. + + let network + try { + network = await pluginAccess.call('network', 'detectNetwork') + } catch (e) { + return localCompilation() + } + if (!network) return localCompilation() + if (!this.sourceVerifierNetWork.includes(network.name)) return localCompilation() + + // check if the contract if part of the local compilation result + const codeAtAddress = await web3.eth.getCode(contractAddress) + const compilation = localCompilation() + if (compilation) { + let found = false + compilation.visitContracts((contract) => { + found = remixLib.util.compareByteCode('0x' + contract.object.evm.deployedBytecode.object, codeAtAddress) + return found + }) + if (found) { + compilersartefacts.addResolvedContract(contractAddress, compilation) + this.firstResolvedAddress = contractAddress + setTimeout(_ => this.event.emit('usingLocalCompilation', contractAddress), 0) + return compilation + } + } + + let name = network.name.toLowerCase() + name === 'main' ? 'mainnet' : name // source-verifier api expect "mainnet" and not "main" + await pluginAccess.call('manager', 'activatePlugin', 'source-verification') + const data = await pluginAccess.call('source-verification', 'fetch', contractAddress, name.toLowerCase()) + if (!data || !data.metadata) { + setTimeout(_ => this.event.emit('notFound', contractAddress), 0) + this.unresolvedAddresses.push(contractAddress) + return localCompilation() + } + + // set the solidity contract code using metadata + await pluginAccess.call('fileManager', 'setFile', `${targetPath}/${contractAddress}/metadata.json`, JSON.stringify(data.metadata, null, '\t')) + let compilationTargets = {} + for (let file in data.metadata.sources) { + const urls = data.metadata.sources[file].urls + for (let url of urls) { + if (url.includes('ipfs')) { + let stdUrl = `ipfs://${url.split('/')[2]}` + const source = await pluginAccess.call('contentImport', 'resolve', stdUrl) + file = file.replace('browser/', '') // should be fixed in the remix IDE end. + const path = `${targetPath}/${contractAddress}/${file}` + await pluginAccess.call('fileManager', 'setFile', path, source.content) + compilationTargets[path] = { content: source.content } + break + } + } + } + // compile + const settings = { + version: data.metadata.compiler.version, + languageName: data.metadata.language, + evmVersion: data.metadata.settings.evmVersion, + optimize: data.metadata.settings.optimizer.enabled, + compilerUrl: urlFromVersion(data.metadata.compiler.version) + } + return await (() => { + return new Promise((resolve, reject) => { + setTimeout(_ => this.event.emit('compiling', settings), 0) + if (!this.compiler) this.compiler = new Compiler(() => {}) + this.compiler.set('evmVersion', settings.evmVersion) + this.compiler.set('optimize', settings.optimize) + this.compiler.loadVersion(canUseWorker(settings.version), settings.compilerUrl) + this.compiler.event.register('compilationFinished', (success, compilationData, source) => { + if (!success) { + this.unresolvedAddresses.push(contractAddress) + setTimeout(_ => this.event.emit('compilationFailed', compilationData), 0) + return resolve(null) + } + const compilerData = new CompilerAbstract(settings.version, compilationData, source) + compilersartefacts.addResolvedContract(contractAddress, compilerData) + this.firstResolvedAddress = contractAddress + resolve(compilerData) + }) + this.compiler.event.register('compilerLoaded', (version) => { + this.compiler.compile(compilationTargets, '') + }) + }) + })() + } +} + +const fetchAndCompile = new FetchAndCompile() + +export default fetchAndCompile diff --git a/src/app/compiler/compiler-utils.js b/src/app/compiler/compiler-utils.js index 8ea9f3ae6f..f7afcf9307 100644 --- a/src/app/compiler/compiler-utils.js +++ b/src/app/compiler/compiler-utils.js @@ -1,6 +1,11 @@ const semver = require('semver') /* global Worker */ +export const baseUrl = 'https://solc-bin.ethereum.org/bin' + +export function urlFromVersion (version) { + return `${baseUrl}/soljson-v${version}.js` +} /** * Checks if the worker can be used to load a compiler. * checks a compiler whitelist, browser support and OS. diff --git a/src/app/tabs/debugger-tab.js b/src/app/tabs/debugger-tab.js index ff636f8a7c..157483e510 100644 --- a/src/app/tabs/debugger-tab.js +++ b/src/app/tabs/debugger-tab.js @@ -1,8 +1,9 @@ -var yo = require('yo-yo') -var css = require('./styles/debugger-tab-styles') - -var DebuggerUI = require('./debugger/debuggerUI') - +const yo = require('yo-yo') +const css = require('./styles/debugger-tab-styles') +var globalRegistry = require('../../global/registry') +import fetchAndCompile from '../compiler/compiler-sourceVerifier-fetchAndCompile' +import toaster from '../ui/tooltip' +const DebuggerUI = require('./debugger/debuggerUI') import { ViewPlugin } from '@remixproject/engine' import * as packageJson from '../../../package.json' @@ -34,7 +35,32 @@ class DebuggerTab extends ViewPlugin {
` - this.debuggerUI = new DebuggerUI(this.el.querySelector('#debugger'), this.blockchain) + + let compilers = globalRegistry.get('compilersartefacts').api + + fetchAndCompile.event.on('compiling', (settings) => { + toaster(yo`
Recompiling and debugging with params
${JSON.stringify(settings, null, '\t')}
`) + }) + + fetchAndCompile.event.on('compilationFailed', (data) => { + toaster(yo`
Compilation failed... continuing without source code debugging.
`) + }) + + fetchAndCompile.event.on('notFound', (contractAddress) => { + toaster(yo`
Contract ${contractAddress} not found in source code repository continuing without source code debugging.
`) + }) + + fetchAndCompile.event.on('usingLocalCompilation', (contractAddress) => { + toaster(yo`
Using compilation result from Solidity module
`) + }) + + this.debuggerUI = new DebuggerUI( + this.el.querySelector('#debugger'), + this.blockchain, + adddress => fetchAndCompile.resolve(adddress, compilers, this, '.debug', this.blockchain.web3())) + + this.call('manager', 'activatePlugin', 'udapp') + return this.el } diff --git a/src/app/tabs/debugger/debuggerUI.js b/src/app/tabs/debugger/debuggerUI.js index 5b199e045b..c6d774df3a 100644 --- a/src/app/tabs/debugger/debuggerUI.js +++ b/src/app/tabs/debugger/debuggerUI.js @@ -30,9 +30,10 @@ var css = csjs` class DebuggerUI { - constructor (container, blockchain) { + constructor (container, blockchain, fetchContractAndCompile) { this.registry = globalRegistry this.blockchain = blockchain + this.fetchContractAndCompile = fetchContractAndCompile this.event = new EventManager() this.isActive = false @@ -77,8 +78,12 @@ class DebuggerUI { self.isActive = isActive }) - this.debugger.event.register('newSourceLocation', function (lineColumnPos, rawLocation) { - self.sourceHighlighter.currentSourceLocation(lineColumnPos, rawLocation) + this.debugger.event.register('newSourceLocation', async function (lineColumnPos, rawLocation) { + const contracts = await self.fetchContractAndCompile() + if (contracts) { + const path = contracts.getSourceName(rawLocation.file) + if (path) self.sourceHighlighter.currentSourceLocationFromfileName(lineColumnPos, path) + } }) this.debugger.event.register('debuggerUnloaded', self.unLoad.bind(this)) @@ -122,15 +127,18 @@ class DebuggerUI { async startDebugging (blockNumber, txNumber, tx) { if (this.debugger) this.unLoad() - let compilers = this.registry.get('compilersartefacts').api - let lastCompilationResult - if (compilers['__last']) lastCompilationResult = compilers['__last'] - let web3 = await this.getDebugWeb3() this.debugger = new Debugger({ web3, offsetToLineColumnConverter: this.registry.get('offsettolinecolumnconverter').api, - compiler: { lastCompilationResult } + compilationResult: async (address) => { + try { + return await this.fetchContractAndCompile(address) + } catch (e) { + console.error(e) + } + return null + } }) this.listenToEvents() @@ -147,16 +155,19 @@ class DebuggerUI { getTrace (hash) { return new Promise(async (resolve, reject) => { - const compilers = this.registry.get('compilersartefacts').api - let lastCompilationResult - if (compilers['__last']) lastCompilationResult = compilers['__last'] - const web3 = await this.getDebugWeb3() const debug = new Debugger({ web3, offsetToLineColumnConverter: this.registry.get('offsettolinecolumnconverter').api, - compiler: { lastCompilationResult } + compilationResult: async (address) => { + try { + return await this.fetchContractAndCompile(address) + } catch (e) { + console.error(e) + } + return null + } }) debug.debugger.traceManager.traceRetriever.getTrace(hash, (error, trace) => { if (error) return reject(error) diff --git a/src/app/tabs/test-tab.js b/src/app/tabs/test-tab.js index d0964c61b9..6259675d31 100644 --- a/src/app/tabs/test-tab.js +++ b/src/app/tabs/test-tab.js @@ -4,7 +4,7 @@ var tooltip = require('../ui/tooltip') var css = require('./styles/test-tab-styles') var remixTests = require('remix-tests') import { ViewPlugin } from '@remixproject/engine' -import { canUseWorker } from '../compiler/compiler-utils' +import { canUseWorker, baseUrl } from '../compiler/compiler-utils' const TestTabLogic = require('./testTab/testTab') @@ -33,7 +33,6 @@ module.exports = class TestTab extends ViewPlugin { this.runningTestsNumber = 0 this.readyTestsNumber = 0 this.areTestsRunning = false - this.baseurl = 'https://solc-bin.ethereum.org/bin' appManager.event.on('activate', (name) => { if (name === 'solidity') this.updateRunAction(fileManager.currentFile()) }) @@ -204,7 +203,7 @@ module.exports = class TestTab extends ViewPlugin { let runningTest = {} runningTest[path] = { content } const {currentVersion, evmVersion, optimize} = this.compileTab.getCurrentCompilerConfig() - const currentCompilerUrl = this.baseurl + '/' + currentVersion + const currentCompilerUrl = baseUrl + '/' + currentVersion const compilerConfig = { currentCompilerUrl, evmVersion, @@ -230,7 +229,7 @@ module.exports = class TestTab extends ViewPlugin { const runningTest = {} runningTest[testFilePath] = { content } const {currentVersion, evmVersion, optimize} = this.compileTab.getCurrentCompilerConfig() - const currentCompilerUrl = this.baseurl + '/' + currentVersion + const currentCompilerUrl = baseUrl + '/' + currentVersion const compilerConfig = { currentCompilerUrl, evmVersion, diff --git a/src/remixAppManager.js b/src/remixAppManager.js index 383e43d126..36b53d2c31 100644 --- a/src/remixAppManager.js +++ b/src/remixAppManager.js @@ -10,10 +10,14 @@ const requiredModules = [ // services + layout views + system views 'terminal', 'settings', 'pluginManager'] export function isNative (name) { - const nativePlugins = ['vyper', 'workshops', 'ethdoc', 'etherscan'] + const nativePlugins = ['vyper', 'workshops'] return nativePlugins.includes(name) || requiredModules.includes(name) } +export function canActivate (name) { + return ['manager', 'debugger'].includes(name) +} + export class RemixAppManager extends PluginManager { constructor (plugins) { @@ -25,7 +29,7 @@ export class RemixAppManager extends PluginManager { } async canActivate (from, to) { - return from.name === 'manager' + return canActivate(from.name) } async canDeactivate (from, to) {