diff --git a/apps/remix-ide-e2e/src/tests/terminal.test.ts b/apps/remix-ide-e2e/src/tests/terminal.test.ts index 448642f4b5..a7d43153fa 100644 --- a/apps/remix-ide-e2e/src/tests/terminal.test.ts +++ b/apps/remix-ide-e2e/src/tests/terminal.test.ts @@ -62,7 +62,7 @@ module.exports = { 'Call web3.eth.getAccounts() using JavaScript VM': function (browser: NightwatchBrowser) { browser .executeScript('web3.eth.getAccounts()') - .waitForElementContainsText('*[data-id="terminalJournal"]', '[ "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB", "0x617F2E2fD72FD9D5503197092aC168c91465E7f2", "0x17F6AD8Ef982297579C203069C1DbfFE4348c372", "0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678", "0x03C6FcED478cBbC9a4FAB34eF9f40767739D1Ff7", "0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C", "0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC", "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c", "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C", "0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB", "0x583031D1113aD414F02576BD6afaBfb302140225", "0xdD870fA1b7C4700F2BD7f44238821C26f7392148" ]', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', '"0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c", "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C", "0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB", "0x583031D1113aD414F02576BD6afaBfb302140225", "0xdD870fA1b7C4700F2BD7f44238821C26f7392148"', 80000) }, 'Call web3.eth.getAccounts() using Web3 Provider': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index b05b4ef488..93dd6753b5 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -431,12 +431,14 @@ Please make a backup of your contracts and start using http://remix.ethereum.org engine.register([ compileTab, + compileTab.compileTabLogic, run, debug, analysis, test, filePanel.remixdHandle, - filePanel.gitHandle + filePanel.gitHandle, + filePanel.hardhatHandle ]) if (isElectron()) { diff --git a/apps/remix-ide/src/app/files/file-explorer.js b/apps/remix-ide/src/app/files/file-explorer.js index 9f5f3e3f69..cef877b68b 100644 --- a/apps/remix-ide/src/app/files/file-explorer.js +++ b/apps/remix-ide/src/app/files/file-explorer.js @@ -9,7 +9,7 @@ const helper = require('../../lib/helper') const yo = require('yo-yo') const Treeview = require('../ui/TreeView') const modalDialog = require('../ui/modaldialog') -const EventManager = require('../../lib/events') +const EventManager = require('events') const contextMenu = require('../ui/contextMenu') const css = require('./styles/file-explorer-styles') const globalRegistry = require('../../global/registry') @@ -94,11 +94,11 @@ function fileExplorer (localRegistry, files, menuItems, plugin) { }) // register to event of the file provider - files.event.register('fileRemoved', fileRemoved) - files.event.register('fileRenamed', fileRenamed) - files.event.register('fileRenamedError', fileRenamedError) - files.event.register('fileAdded', fileAdded) - files.event.register('folderAdded', folderAdded) + files.event.on('fileRemoved', fileRemoved) + files.event.on('fileRenamed', fileRenamed) + files.event.on('fileRenamedError', fileRenamedError) + files.event.on('fileAdded', fileAdded) + files.event.on('folderAdded', folderAdded) function fileRenamedError (error) { modalDialogCustom.alert(error) diff --git a/apps/remix-ide/src/app/files/fileManager.js b/apps/remix-ide/src/app/files/fileManager.js index fd74b1722d..44e33ff0d2 100644 --- a/apps/remix-ide/src/app/files/fileManager.js +++ b/apps/remix-ide/src/app/files/fileManager.js @@ -329,18 +329,18 @@ class FileManager extends Plugin { workspaceExplorer: this._components.registry.get('fileproviders/workspace').api, filesProviders: this._components.registry.get('fileproviders').api } - this._deps.browserExplorer.event.register('fileChanged', (path) => { this.fileChangedEvent(path) }) - this._deps.browserExplorer.event.register('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) }) - this._deps.localhostExplorer.event.register('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) }) - this._deps.browserExplorer.event.register('fileRemoved', (path) => { this.fileRemovedEvent(path) }) - this._deps.browserExplorer.event.register('fileAdded', (path) => { this.fileAddedEvent(path) }) - this._deps.localhostExplorer.event.register('fileRemoved', (path) => { this.fileRemovedEvent(path) }) - this._deps.localhostExplorer.event.register('errored', (event) => { this.removeTabsOf(this._deps.localhostExplorer) }) - this._deps.localhostExplorer.event.register('closed', (event) => { this.removeTabsOf(this._deps.localhostExplorer) }) - this._deps.workspaceExplorer.event.register('fileChanged', (path) => { this.fileChangedEvent(path) }) - this._deps.workspaceExplorer.event.register('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) }) - this._deps.workspaceExplorer.event.register('fileRemoved', (path) => { this.fileRemovedEvent(path) }) - this._deps.workspaceExplorer.event.register('fileAdded', (path) => { this.fileAddedEvent(path) }) + this._deps.browserExplorer.event.on('fileChanged', (path) => { this.fileChangedEvent(path) }) + this._deps.browserExplorer.event.on('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) }) + this._deps.localhostExplorer.event.on('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) }) + this._deps.browserExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) }) + this._deps.browserExplorer.event.on('fileAdded', (path) => { this.fileAddedEvent(path) }) + this._deps.localhostExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) }) + this._deps.localhostExplorer.event.on('errored', (event) => { this.removeTabsOf(this._deps.localhostExplorer) }) + this._deps.localhostExplorer.event.on('closed', (event) => { this.removeTabsOf(this._deps.localhostExplorer) }) + this._deps.workspaceExplorer.event.on('fileChanged', (path) => { this.fileChangedEvent(path) }) + this._deps.workspaceExplorer.event.on('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) }) + this._deps.workspaceExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) }) + this._deps.workspaceExplorer.event.on('fileAdded', (path) => { this.fileAddedEvent(path) }) this.getCurrentFile = this.file this.getFile = this.readFile diff --git a/apps/remix-ide/src/app/files/fileProvider.js b/apps/remix-ide/src/app/files/fileProvider.js index 521dcec453..5d152ec74b 100644 --- a/apps/remix-ide/src/app/files/fileProvider.js +++ b/apps/remix-ide/src/app/files/fileProvider.js @@ -1,7 +1,7 @@ 'use strict' const CompilerImport = require('../compiler/compiler-imports') -const EventManager = require('../../lib/events') +const EventManager = require('events') const modalDialogCustom = require('../ui/modal-dialog-custom') const tooltip = require('../ui/tooltip') const remixLib = require('@remix-project/remix-lib') @@ -111,9 +111,9 @@ class FileProvider { return false } if (!exists) { - this.event.trigger('fileAdded', [this._normalizePath(unprefixedpath), false]) + this.event.emit('fileAdded', this._normalizePath(unprefixedpath), false) } else { - this.event.trigger('fileChanged', [this._normalizePath(unprefixedpath)]) + this.event.emit('fileChanged', this._normalizePath(unprefixedpath)) } cb() return true @@ -128,7 +128,7 @@ class FileProvider { currentCheck = currentCheck + '/' + value if (!window.remixFileSystem.existsSync(currentCheck)) { window.remixFileSystem.mkdirSync(currentCheck) - this.event.trigger('folderAdded', [this._normalizePath(currentCheck)]) + this.event.emit('folderAdded', this._normalizePath(currentCheck)) } }) if (cb) cb() @@ -184,7 +184,7 @@ class FileProvider { // folder is empty window.remixFileSystem.rmdirSync(path, console.log) } - this.event.trigger('fileRemoved', [this._normalizePath(path)]) + this.event.emit('fileRemoved', this._normalizePath(path)) } } catch (e) { console.log(e) @@ -249,7 +249,7 @@ class FileProvider { path = this.removePrefix(path) if (window.remixFileSystem.existsSync(path) && !window.remixFileSystem.statSync(path).isDirectory()) { window.remixFileSystem.unlinkSync(path, console.log) - this.event.trigger('fileRemoved', [this._normalizePath(path)]) + this.event.emit('fileRemoved', this._normalizePath(path)) return true } else return false } @@ -259,11 +259,11 @@ class FileProvider { var unprefixednewPath = this.removePrefix(newPath) if (this._exists(unprefixedoldPath)) { window.remixFileSystem.renameSync(unprefixedoldPath, unprefixednewPath) - this.event.trigger('fileRenamed', [ + this.event.emit('fileRenamed', this._normalizePath(unprefixedoldPath), this._normalizePath(unprefixednewPath), isFolder - ]) + ) return true } return false diff --git a/apps/remix-ide/src/app/files/hardhat-handle.js b/apps/remix-ide/src/app/files/hardhat-handle.js new file mode 100644 index 0000000000..e9e8b770ef --- /dev/null +++ b/apps/remix-ide/src/app/files/hardhat-handle.js @@ -0,0 +1,18 @@ +import { WebsocketPlugin } from '@remixproject/engine-web' +import * as packageJson from '../../../../../package.json' + +const profile = { + name: 'hardhat', + displayName: 'Hardhat', + url: 'ws://127.0.0.1:65522', + methods: ['compile'], + description: 'Using Remixd daemon, allow to access hardhat API', + kind: 'other', + version: packageJson.version +} + +export class HardhatHandle extends WebsocketPlugin { + constructor () { + super(profile) + } +} diff --git a/apps/remix-ide/src/app/files/remixDProvider.js b/apps/remix-ide/src/app/files/remixDProvider.js index 60337c11e4..bc59bfe90b 100644 --- a/apps/remix-ide/src/app/files/remixDProvider.js +++ b/apps/remix-ide/src/app/files/remixDProvider.js @@ -17,32 +17,32 @@ module.exports = class RemixDProvider extends FileProvider { var remixdEvents = ['connecting', 'connected', 'errored', 'closed'] remixdEvents.forEach((value) => { this._appManager.on('remixd', value, (event) => { - this.event.trigger(value, [event]) + this.event.emit(value, event) }) }) this._appManager.on('remixd', 'folderAdded', (path) => { - this.event.trigger('folderAdded', [path]) + this.event.emit('folderAdded', path) }) this._appManager.on('remixd', 'fileAdded', (path) => { - this.event.trigger('fileAdded', [path]) + this.event.emit('fileAdded', path) }) this._appManager.on('remixd', 'fileChanged', (path) => { - this.event.trigger('fileChanged', [path]) + this.event.emit('fileChanged', path) }) this._appManager.on('remixd', 'fileRemoved', (path) => { - this.event.trigger('fileRemoved', [path]) + this.event.emit('fileRemoved', path) }) this._appManager.on('remixd', 'fileRenamed', (oldPath, newPath) => { - this.event.trigger('fileRemoved', [oldPath, newPath]) + this.event.emit('fileRemoved', oldPath, newPath) }) this._appManager.on('remixd', 'rootFolderChanged', () => { - this.event.trigger('rootFolderChanged', []) + this.event.emit('rootFolderChanged') }) } @@ -53,11 +53,11 @@ module.exports = class RemixDProvider extends FileProvider { close (cb) { this._isReady = false cb() - this.event.trigger('disconnected') + this.event.emit('disconnected') } preInit () { - this.event.trigger('loading') + this.event.emit('loading') } init (cb) { @@ -67,7 +67,7 @@ module.exports = class RemixDProvider extends FileProvider { this._isReady = true this._readOnlyMode = result this._registerEvent() - this.event.trigger('connected') + this.event.emit('connected') cb && cb() }).catch((error) => { cb && cb(error) @@ -164,13 +164,13 @@ module.exports = class RemixDProvider extends FileProvider { this.filesContent[newPath] = this.filesContent[oldPath] delete this.filesContent[oldPath] this.init(() => { - this.event.trigger('fileRenamed', [oldPath, newPath, isFolder]) + this.event.emit('fileRenamed', oldPath, newPath, isFolder) }) return result }).catch(error => { console.log(error) if (this.error[error.code]) error = this.error[error.code] - this.event.trigger('fileRenamedError', [this.error[error.code]]) + this.event.emit('fileRenamedError', this.error[error.code]) }) } diff --git a/apps/remix-ide/src/app/files/remixd-handle.js b/apps/remix-ide/src/app/files/remixd-handle.js index 3ec635040c..a2d94eb3ea 100644 --- a/apps/remix-ide/src/app/files/remixd-handle.js +++ b/apps/remix-ide/src/app/files/remixd-handle.js @@ -39,6 +39,7 @@ export class RemixdHandle extends WebsocketPlugin { deactivate () { if (super.socket) super.deactivate() // this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342 + this.appManager.deactivatePlugin('hardhat') this.locahostProvider.close((error) => { if (error) console.log(error) }) @@ -51,6 +52,7 @@ export class RemixdHandle extends WebsocketPlugin { async canceled () { // await this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342 await this.appManager.deactivatePlugin('remixd') + await this.appManager.deactivatePlugin('hardhat') } /** @@ -81,7 +83,7 @@ export class RemixdHandle extends WebsocketPlugin { } }, 3000) this.locahostProvider.init(() => {}) - // this.call('manager', 'activatePlugin', 'git') + this.call('manager', 'activatePlugin', 'hardhat') } } if (this.locahostProvider.isConnected()) { diff --git a/apps/remix-ide/src/app/files/workspaceFileProvider.js b/apps/remix-ide/src/app/files/workspaceFileProvider.js index 156723b622..4b0ac2af2e 100644 --- a/apps/remix-ide/src/app/files/workspaceFileProvider.js +++ b/apps/remix-ide/src/app/files/workspaceFileProvider.js @@ -1,6 +1,6 @@ 'use strict' -const EventManager = require('../../lib/events') +const EventManager = require('events') const FileProvider = require('./fileProvider') const pathModule = require('path') @@ -82,7 +82,7 @@ class WorkspaceFileProvider extends FileProvider { createWorkspace (name) { if (!name) name = 'default_workspace' - this.event.trigger('createWorkspace', [name]) + this.event.emit('createWorkspace', name) } } diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index 991108285c..ac7ae97089 100644 --- a/apps/remix-ide/src/app/panels/file-panel.js +++ b/apps/remix-ide/src/app/panels/file-panel.js @@ -6,13 +6,13 @@ import ReactDOM from 'react-dom' import { Workspace } from '@remix-ui/workspace' // eslint-disable-line import { bufferToHex, keccakFromString } from 'ethereumjs-util' import { checkSpecialChars, checkSlash } from '../../lib/helper' -var EventManager = require('../../lib/events') -var { RemixdHandle } = require('../files/remixd-handle.js') -var { GitHandle } = require('../files/git-handle.js') -var globalRegistry = require('../../global/registry') -var examples = require('../editor/examples') -var GistHandler = require('../../lib/gist-handler') -var QueryParams = require('../../lib/query-params') +const { RemixdHandle } = require('../files/remixd-handle.js') +const { GitHandle } = require('../files/git-handle.js') +const { HardhatHandle } = require('../files/hardhat-handle.js') +const globalRegistry = require('../../global/registry') +const examples = require('../editor/examples') +const GistHandler = require('../../lib/gist-handler') +const QueryParams = require('../../lib/query-params') const modalDialogCustom = require('../ui/modal-dialog-custom') /* Overview of APIs: @@ -47,7 +47,6 @@ const profile = { module.exports = class Filepanel extends ViewPlugin { constructor (appManager) { super(profile) - this.event = new EventManager() this._components = {} this._components.registry = globalRegistry this._deps = { @@ -60,6 +59,7 @@ module.exports = class Filepanel extends ViewPlugin { this.remixdHandle = new RemixdHandle(this._deps.fileProviders.localhost, appManager) this.gitHandle = new GitHandle() + this.hardhatHandle = new HardhatHandle() this.registeredMenuItems = [] this.request = {} this.workspaces = [] diff --git a/apps/remix-ide/src/app/tabs/compile-tab.js b/apps/remix-ide/src/app/tabs/compile-tab.js index 12b93ff594..8bb5b48575 100644 --- a/apps/remix-ide/src/app/tabs/compile-tab.js +++ b/apps/remix-ide/src/app/tabs/compile-tab.js @@ -65,15 +65,10 @@ class CompileTab extends ViewPlugin { eventHandlers: {}, loading: false } + this.compileTabLogic = new CompileTabLogic(this.queryParams, this.fileManager, this.editor, this.config, this.fileProvider, this.contentImport) } onActivationInternal () { - const miscApi = { - clearAnnotations: () => { - this.call('editor', 'clearAnnotations') - } - } - this.compileTabLogic = new CompileTabLogic(this.queryParams, this.fileManager, this.editor, this.config, this.fileProvider, this.contentImport, miscApi) this.compiler = this.compileTabLogic.compiler this.compileTabLogic.init() @@ -85,11 +80,28 @@ class CompileTab extends ViewPlugin { ) } + resetResults () { + if (this._view.errorContainer) { + this._view.errorContainer.innerHTML = '' + } + this.compilerContainer.currentFile = '' + this.data.contractsDetails = {} + yo.update(this._view.contractSelection, this.contractSelection()) + this.emit('statusChanged', { key: 'none' }) + } + /************ * EVENTS */ listenToEvents () { + this.on('filePanel', 'setWorkspace', (workspace) => { + this.compileTabLogic.isHardhatProject().then((result) => { + if (result && workspace.isLocalhost) this.compilerContainer.hardhatCompilation.style.display = 'flex' + else this.compilerContainer.hardhatCompilation.style.display = 'none' + }) + }) + this.data.eventHandlers.onContentChanged = () => { this.emit('statusChanged', { key: 'edited', title: 'the content has changed, needs recompilation', type: 'info' }) } @@ -113,6 +125,9 @@ class CompileTab extends ViewPlugin { } this.emit('statusChanged', { key: 'loading', title: 'compiling...', type: 'info' }) } + + this.on('filePanel', 'setWorkspace', () => this.resetResults()) + this.compileTabLogic.event.on('startingCompilation', this.data.eventHandlers.onStartingCompilation) this.data.eventHandlers.onCurrentFileChanged = (name) => { @@ -199,7 +214,7 @@ class CompileTab extends ViewPlugin { // ctrl+s or command+s if ((e.metaKey || e.ctrlKey) && e.keyCode === 83) { e.preventDefault() - this.compileTabLogic.runCompiler() + this.compileTabLogic.runCompiler(this.compilerContainer.hhCompilation) } }) } @@ -479,6 +494,7 @@ class CompileTab extends ViewPlugin { } onActivation () { + this.call('manager', 'activatePlugin', 'solidity-logic') this.listenToEvents() } @@ -492,6 +508,7 @@ class CompileTab extends ViewPlugin { this.fileManager.events.removeListener('noFileSelected', this.data.eventHandlers.onNoFileSelected) this.compiler.event.unregister('compilationFinished', this.data.eventHandlers.onCompilationFinished) globalRegistry.get('themeModule').api.events.removeListener('themeChanged', this.data.eventHandlers.onThemeChanged) + this.call('manager', 'deactivatePlugin', 'solidity-logic') } } diff --git a/apps/remix-ide/src/app/tabs/compileTab/compileTab.js b/apps/remix-ide/src/app/tabs/compileTab/compileTab.js index 79228d1663..1e5de9d5c7 100644 --- a/apps/remix-ide/src/app/tabs/compileTab/compileTab.js +++ b/apps/remix-ide/src/app/tabs/compileTab/compileTab.js @@ -1,10 +1,19 @@ +import * as packageJson from '../../../../../../package.json' +import { Plugin } from '@remixproject/engine' const EventEmitter = require('events') var Compiler = require('@remix-project/remix-solidity').Compiler -class CompileTab { - constructor (queryParams, fileManager, editor, config, fileProvider, contentImport, miscApi) { +const profile = { + name: 'solidity-logic', + displayName: 'Solidity compiler logic', + description: 'Compile solidity contracts - Logic', + version: packageJson.version +} + +class CompileTab extends Plugin { + constructor (queryParams, fileManager, editor, config, fileProvider, contentImport) { + super(profile) this.event = new EventEmitter() - this.miscApi = miscApi this.queryParams = queryParams this.compilerImport = contentImport this.compiler = new Compiler((url, cb) => this.compilerImport.resolveAndSave(url).then((result) => cb(null, result)).catch((error) => cb(error.message))) @@ -78,10 +87,32 @@ class CompileTab { }) } - runCompiler () { + async isHardhatProject () { + if (this.fileManager.mode === 'localhost') { + return await this.fileManager.exists('hardhat.config.js') + } else return false + } + + runCompiler (hhCompilation) { try { + if (this.fileManager.mode === 'localhost' && hhCompilation) { + const { currentVersion, optimize, runs } = this.compiler.state + const fileContent = `module.exports = { + solidity: '${currentVersion.substring(0, currentVersion.indexOf('+commit'))}', + settings: { + optimizer: { + enabled: ${optimize}, + runs: ${runs} + } + } + } + ` + const configFilePath = 'remix-compiler.config.js' + this.fileManager.setFileContent(configFilePath, fileContent) + this.call('hardhat', 'compile', configFilePath) + } this.fileManager.saveCurrentFile() - this.miscApi.clearAnnotations() + this.call('editor', 'clearAnnotations') var currentFile = this.config.get('currentFile') return this.compileFile(currentFile) } catch (err) { diff --git a/apps/remix-ide/src/app/tabs/compileTab/compilerContainer.js b/apps/remix-ide/src/app/tabs/compileTab/compilerContainer.js index 88880bff22..73d0802bf4 100644 --- a/apps/remix-ide/src/app/tabs/compileTab/compilerContainer.js +++ b/apps/remix-ide/src/app/tabs/compileTab/compilerContainer.js @@ -15,6 +15,7 @@ class CompilerContainer { this.editor = editor this.config = config this.queryParams = queryParams + this.hhCompilation = false this.data = { hideWarnings: config.get('hideWarnings') || false, @@ -183,6 +184,10 @@ class CompilerContainer { } }) + this.hardhatCompilation = yo`` this._view.warnCompilationSlow = yo`` this._view.compileIcon = yo`` this._view.autoCompile = yo` this.updateAutoCompile()} data-id="compilerContainerAutoCompile" id="autoCompile" type="checkbox" title="Auto compile">` @@ -299,6 +304,7 @@ class CompilerContainer { + ${this.hardhatCompilation} ${this._view.compilationButton} @@ -326,12 +332,16 @@ class CompilerContainer { this.config.set('autoCompile', this._view.autoCompile.checked) } + updatehhCompilation (event) { + this.hhCompilation = event.target.checked + } + compile (event) { const currentFile = this.config.get('currentFile') if (!this.isSolFileSelected()) return this._setCompilerVersionFromPragma(currentFile) - this.compileTabLogic.runCompiler() + this.compileTabLogic.runCompiler(this.hhCompilation) } compileIfAutoCompileOn () { diff --git a/apps/remix-ide/src/app/tabs/network-module.js b/apps/remix-ide/src/app/tabs/network-module.js index fdae172c15..c1cc1e12b5 100644 --- a/apps/remix-ide/src/app/tabs/network-module.js +++ b/apps/remix-ide/src/app/tabs/network-module.js @@ -22,18 +22,6 @@ export class NetworkModule extends Plugin { this.blockchain.event.register('contextChanged', (provider) => { this.emit('providerChanged', provider) }) - /* - // Events that could be implemented later - executionContext.event.register('removeProvider', (provider) => { - this.events.emit('networkRemoved', provider) - }) - executionContext.event.register('addProvider', (provider) => { - this.events.emit('networkAdded', provider) - }) - executionContext.event.register('web3EndpointChanged', (provider) => { - this.events.emit('web3EndpointChanged', provider) - }) - */ } /** Return the current network provider (web3, vm, injected) */ diff --git a/apps/remix-ide/src/app/tabs/runTab/model/recorder.js b/apps/remix-ide/src/app/tabs/runTab/model/recorder.js index 126b354927..f9a5d9aa78 100644 --- a/apps/remix-ide/src/app/tabs/runTab/model/recorder.js +++ b/apps/remix-ide/src/app/tabs/runTab/model/recorder.js @@ -63,10 +63,10 @@ class Recorder { } }) - this.blockchain.event.register('transactionExecuted', (error, from, to, data, call, txResult, timestamp, _payload, rawAddress) => { + this.blockchain.event.register('transactionExecuted', (error, from, to, data, call, txResult, timestamp, _payload) => { if (error) return console.log(error) if (call) return - + const rawAddress = txResult.receipt.contractAddress if (!rawAddress) return // not a contract creation const address = helper.addressToString(rawAddress) // save back created addresses for the convertion from tokens to real adresses diff --git a/apps/remix-ide/src/app/tabs/test-tab.js b/apps/remix-ide/src/app/tabs/test-tab.js index eb6edd4efb..652be0b9c9 100644 --- a/apps/remix-ide/src/app/tabs/test-tab.js +++ b/apps/remix-ide/src/app/tabs/test-tab.js @@ -52,7 +52,7 @@ module.exports = class TestTab extends ViewPlugin { } listenToEvents () { - this.filePanel.event.register('newTestFileCreated', file => { + this.on('filePanel', 'newTestFileCreated', file => { var testList = this._view.el.querySelector("[class^='testList']") var test = this.createSingleTest(file) testList.appendChild(test) diff --git a/apps/remix-ide/src/blockchain/blockchain.js b/apps/remix-ide/src/blockchain/blockchain.js index 84062f7ac4..973e267683 100644 --- a/apps/remix-ide/src/blockchain/blockchain.js +++ b/apps/remix-ide/src/blockchain/blockchain.js @@ -3,16 +3,17 @@ const txFormat = remixLib.execution.txFormat const txExecution = remixLib.execution.txExecution const typeConversion = remixLib.execution.typeConversion const Txlistener = remixLib.execution.txListener -const TxRunner = remixLib.execution.txRunner +const TxRunner = remixLib.execution.TxRunner +const TxRunnerWeb3 = remixLib.execution.TxRunnerWeb3 const txHelper = remixLib.execution.txHelper const EventManager = remixLib.EventManager -const executionContext = remixLib.execution.executionContext +const { ExecutionContext } = require('./execution-context') const Web3 = require('web3') const async = require('async') const { EventEmitter } = require('events') -const { resultToRemixTx } = require('./txResultHelper') +const { resultToRemixTx } = remixLib.helpers.txResultHelper const VMProvider = require('./providers/vm.js') const InjectedProvider = require('./providers/injected.js') @@ -22,12 +23,11 @@ class Blockchain { // NOTE: the config object will need to be refactored out in remix-lib constructor (config) { this.event = new EventManager() - this.executionContext = executionContext + this.executionContext = new ExecutionContext() this.events = new EventEmitter() this.config = config - - this.txRunner = new TxRunner({}, { + const web3Runner = new TxRunnerWeb3({ config: config, detectNetwork: (cb) => { this.executionContext.detectNetwork(cb) @@ -35,7 +35,9 @@ class Blockchain { personalMode: () => { return this.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false } - }, this.executionContext) + }, _ => this.executionContext.web3(), _ => this.executionContext.currentblockGasLimit()) + this.txRunner = new TxRunner(web3Runner, { runAsync: true }) + this.executionContext.event.register('contextChanged', this.resetEnvironment.bind(this)) this.networkcallid = 0 @@ -123,7 +125,7 @@ class Blockchain { if (error) { return finalCb(`creation of ${selectedContract.name} errored: ${(error.message ? error.message : error)}`) } - if (txResult.result.status && txResult.result.status === '0x0') { + if (txResult.receipt.status === false || txResult.receipt.status === '0x0') { return finalCb(`creation of ${selectedContract.name} errored: transaction execution failed`) } finalCb(null, selectedContract, address) @@ -309,18 +311,17 @@ class Blockchain { resetEnvironment () { this.getCurrentProvider().resetEnvironment() // TODO: most params here can be refactored away in txRunner - // this.txRunner = new TxRunner(this.providers.vm.accounts, { - this.txRunner = new TxRunner(this.providers.vm.RemixSimulatorProvider.Accounts.accounts, { - // TODO: only used to check value of doNotShowTransactionConfirmationAgain property + const web3Runner = new TxRunnerWeb3({ config: this.config, - // TODO: to refactor, TxRunner already has access to executionContext detectNetwork: (cb) => { this.executionContext.detectNetwork(cb) }, personalMode: () => { return this.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false } - }, this.executionContext) + }, _ => this.executionContext.web3(), _ => this.executionContext.currentblockGasLimit()) + + this.txRunner = new TxRunner(web3Runner, { runAsync: true }) this.txRunner.event.register('transactionBroadcasted', (txhash) => { this.executionContext.detectNetwork((error, network) => { if (error || !network) return @@ -372,10 +373,11 @@ class Blockchain { (network, tx, gasEstimation, continueTxExecution, cancelCb) => { continueTxExecution() }, (error, continueTxExecution, cancelCb) => { if (error) { reject(error) } else { continueTxExecution() } }, (okCb, cancelCb) => { okCb() }, - (error, result) => { + async (error, result) => { if (error) return reject(error) try { - resolve(resultToRemixTx(result)) + const execResult = await this.web3().eth.getExecutionResultFromSimulator(result.transactionHash) + resolve(resultToRemixTx(result, execResult)) } catch (e) { reject(e) } @@ -429,19 +431,24 @@ class Blockchain { function runTransaction (fromAddress, value, gasLimit, next) { const tx = { to: args.to, data: args.data.dataHex, useCall: args.useCall, from: fromAddress, value: value, gasLimit: gasLimit, timestamp: args.data.timestamp } const payLoad = { funAbi: args.data.funAbi, funArgs: args.data.funArgs, contractBytecode: args.data.contractBytecode, contractName: args.data.contractName, contractABI: args.data.contractABI, linkReferences: args.data.linkReferences } - let timestamp = Date.now() - if (tx.timestamp) { - timestamp = tx.timestamp - } + if (!tx.timestamp) tx.timestamp = Date.now() + const timestamp = tx.timestamp self.event.trigger('initiatingTransaction', [timestamp, tx, payLoad]) self.txRunner.rawRun(tx, confirmationCb, continueCb, promptCb, - function (error, result) { + async (error, result) => { if (error) return next(error) - const rawAddress = self.executionContext.isVM() ? (result.result.createdAddress && result.result.createdAddress.toBuffer()) : result.result.contractAddress + const isVM = self.executionContext.isVM() + if (isVM && tx.useCall) { + try { + result.transactionHash = await self.web3().eth.getHashFromTagBySimulator(timestamp) + } catch (e) { + console.log('unable to retrieve back the "call" hash', e) + } + } const eventName = (tx.useCall ? 'callExecuted' : 'transactionExecuted') - self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad, rawAddress]) + self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad]) if (error && (typeof (error) !== 'string')) { if (error.message) error = error.message @@ -454,25 +461,29 @@ class Blockchain { ) } ], - (error, txResult) => { + async (error, txResult) => { if (error) { return cb(error) } const isVM = this.executionContext.isVM() + let execResult + let returnValue = null if (isVM) { - const vmError = txExecution.checkVMError(txResult) - if (vmError.error) { - return cb(vmError.message) + execResult = await this.web3().eth.getExecutionResultFromSimulator(txResult.transactionHash) + if (execResult) { + // if it's not the VM, we don't have return value. We only have the transaction, and it does not contain the return value. + returnValue = (execResult && isVM) ? execResult.returnValue : txResult + const vmError = txExecution.checkVMError(execResult) + if (vmError.error) { + return cb(vmError.message) + } } } let address = null - let returnValue = null - if (txResult && txResult.result) { - address = isVM ? (txResult.result.createdAddress && txResult.result.createdAddress.toBuffer()) : txResult.result.contractAddress - // if it's not the VM, we don't have return value. We only have the transaction, and it does not contain the return value. - returnValue = (txResult.result.execResult && isVM) ? txResult.result.execResult.returnValue : txResult.result + if (txResult && txResult.receipt) { + address = txResult.receipt.contractAddress } cb(error, txResult, address, returnValue) diff --git a/libs/remix-lib/src/execution/execution-context.ts b/apps/remix-ide/src/blockchain/execution-context.js similarity index 60% rename from libs/remix-lib/src/execution/execution-context.ts rename to apps/remix-ide/src/blockchain/execution-context.js index 31212412ff..41588e3708 100644 --- a/libs/remix-lib/src/execution/execution-context.ts +++ b/apps/remix-ide/src/blockchain/execution-context.js @@ -1,139 +1,33 @@ /* global ethereum */ 'use strict' import Web3 from 'web3' -import { EventManager } from '../eventManager' -import { rlp, keccak, bufferToHex } from 'ethereumjs-util' -import { Web3VmProvider } from '../web3Provider/web3VmProvider' -import { LogsManager } from './logsManager' -import VM from '@ethereumjs/vm' -import Common from '@ethereumjs/common' -import StateManager from '@ethereumjs/vm/dist/state/stateManager' -import { StorageDump } from '@ethereumjs/vm/dist/state/interface' +import EventManager from '../lib/events' -declare let ethereum: any let web3 -if (typeof window !== 'undefined' && typeof window['ethereum'] !== 'undefined') { - var injectedProvider = window['ethereum'] +if (typeof window !== 'undefined' && typeof window.ethereum !== 'undefined') { + var injectedProvider = window.ethereum web3 = new Web3(injectedProvider) } else { web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) } -/* - extend vm state manager and instanciate VM -*/ - -class StateManagerCommonStorageDump extends StateManager { - /* - * dictionary containing keccak(b) as key and b as value. used to get the initial value from its hash - */ - keyHashes: { [key: string]: string } - constructor () { - super() - this.keyHashes = {} - } - - putContractStorage (address, key, value) { - this.keyHashes[keccak(key).toString('hex')] = bufferToHex(key) - return super.putContractStorage(address, key, value) - } - - async dumpStorage (address) { - let trie - try { - trie = await this._getStorageTrie(address) - } catch (e) { - console.log(e) - throw e - } - return new Promise((resolve, reject) => { - try { - const storage = {} - const stream = trie.createReadStream() - stream.on('data', (val) => { - const value = rlp.decode(val.value) - storage['0x' + val.key.toString('hex')] = { - key: this.keyHashes[val.key.toString('hex')], - value: '0x' + value.toString('hex') - } - }) - stream.on('end', function () { - resolve(storage) - }) - } catch (e) { - reject(e) - } - }) - } - - async getStateRoot (force: boolean = false): Promise { - await this._cache.flush() - - const stateRoot = this._trie.root - return stateRoot - } - - async setStateRoot (stateRoot: Buffer): Promise { - await this._cache.flush() - - if (stateRoot === this._trie.EMPTY_TRIE_ROOT) { - this._trie.root = stateRoot - this._cache.clear() - this._storageTries = {} - return - } - - const hasRoot = await this._trie.checkRoot(stateRoot) - if (!hasRoot) { - throw new Error('State trie does not contain state root') - } - - this._trie.root = stateRoot - this._cache.clear() - this._storageTries = {} - } -} - /* trigger contextChanged, web3EndpointChanged */ export class ExecutionContext { - event - logsManager - blockGasLimitDefault - blockGasLimit - customNetWorks - blocks - latestBlockNumber - txs - executionContext - listenOnLastBlockId - currentFork: string - vms - mainNetGenesisHash: string - constructor () { this.event = new EventManager() - this.logsManager = new LogsManager() this.executionContext = null this.blockGasLimitDefault = 4300000 this.blockGasLimit = this.blockGasLimitDefault this.currentFork = 'berlin' - this.vms = { - /* - byzantium: createVm('byzantium'), - constantinople: createVm('constantinople'), - petersburg: createVm('petersburg'), - istanbul: createVm('istanbul'), - */ - berlin: this.createVm('berlin') - } this.mainNetGenesisHash = '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3' this.customNetWorks = {} this.blocks = {} this.latestBlockNumber = 0 this.txs = {} + this.customWeb3 = {} // mapping between a context name and a web3.js instance } init (config) { @@ -145,20 +39,6 @@ export class ExecutionContext { } } - createVm (hardfork) { - const stateManager = new StateManagerCommonStorageDump() - const common = new Common({ chain: 'mainnet', hardfork }) - const vm = new VM({ - common, - activatePrecompiles: true, - stateManager: stateManager - }) - - const web3vm = new Web3VmProvider() - web3vm.setVM(vm) - return { vm, web3vm, stateManager, common } - } - askPermission () { // metamask if (ethereum && typeof ethereum.enable === 'function') ethereum.enable() @@ -172,7 +52,12 @@ export class ExecutionContext { return this.executionContext === 'vm' } + setWeb3 (context, web3) { + this.customWeb3[context] = web3 + } + web3 () { + if (this.customWeb3[this.executionContext]) return this.customWeb3[this.executionContext] return this.isVM() ? this.vms[this.currentFork].web3vm : web3 } @@ -228,14 +113,6 @@ export class ExecutionContext { return new Web3() } - vm () { - return this.vms[this.currentFork].vm - } - - vmObject () { - return this.vms[this.currentFork] - } - setContext (context, endPointUrl, confirmCb, infoCb) { this.executionContext = context this.executionContextChange(context, endPointUrl, confirmCb, infoCb, null) @@ -340,22 +217,4 @@ export class ExecutionContext { return transactionDetailsLinks[network] + hash } } - - addBlock (block) { - let blockNumber = '0x' + block.header.number.toString('hex') - if (blockNumber === '0x') { - blockNumber = '0x0' - } - blockNumber = web3.utils.toHex(web3.utils.toBN(blockNumber)) - - this.blocks['0x' + block.hash().toString('hex')] = block - this.blocks[blockNumber] = block - this.latestBlockNumber = blockNumber - - this.logsManager.checkBlock(blockNumber, block, this.web3()) - } - - trackTx (tx, block) { - this.txs[tx] = block - } } diff --git a/apps/remix-ide/src/blockchain/providers/vm.js b/apps/remix-ide/src/blockchain/providers/vm.js index 8ec6583ff7..e01b7b08d2 100644 --- a/apps/remix-ide/src/blockchain/providers/vm.js +++ b/apps/remix-ide/src/blockchain/providers/vm.js @@ -1,14 +1,16 @@ const Web3 = require('web3') const { BN, privateToAddress, hashPersonalMessage } = require('ethereumjs-util') -const RemixSimulator = require('@remix-project/remix-simulator') +const { Provider, extend } = require('@remix-project/remix-simulator') class VMProvider { constructor (executionContext) { this.executionContext = executionContext - this.RemixSimulatorProvider = new RemixSimulator.Provider({ executionContext: this.executionContext }) + this.RemixSimulatorProvider = new Provider({}) this.RemixSimulatorProvider.init() this.web3 = new Web3(this.RemixSimulatorProvider) + extend(this.web3) this.accounts = {} + this.executionContext.setWeb3('vm', this.web3) } getAccounts (cb) { diff --git a/apps/remix-ide/src/blockchain/txResultHelper.js b/apps/remix-ide/src/blockchain/txResultHelper.js deleted file mode 100644 index f608324062..0000000000 --- a/apps/remix-ide/src/blockchain/txResultHelper.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict' -const { bufferToHex, isHexString } = require('ethereumjs-util') - -function convertToPrefixedHex (input) { - if (input === undefined || input === null || isHexString(input)) { - return input - } else if (Buffer.isBuffer(input)) { - return bufferToHex(input) - } - return '0x' + input.toString(16) -} - -/* - txResult.result can be 3 different things: - - VM call or tx: ethereumjs-vm result object - - Node transaction: object returned from eth.getTransactionReceipt() - - Node call: return value from function call (not an object) - - Also, VM results use BN and Buffers, Node results use hex strings/ints, - So we need to normalize the values to prefixed hex strings -*/ -function resultToRemixTx (txResult) { - const { result, transactionHash } = txResult - const { status, execResult, gasUsed, createdAddress, contractAddress } = result - let returnValue, errorMessage - - if (isHexString(result)) { - returnValue = result - } else if (execResult !== undefined) { - returnValue = execResult.returnValue - errorMessage = execResult.exceptionError - } - - return { - transactionHash, - status, - gasUsed: convertToPrefixedHex(gasUsed), - error: errorMessage, - return: convertToPrefixedHex(returnValue), - createdAddress: convertToPrefixedHex(createdAddress || contractAddress) - } -} - -module.exports = { - resultToRemixTx -} diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index b495f53c81..7e7f9cd10f 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -11,10 +11,10 @@ const requiredModules = [ // services + layout views + system views 'fileManager', 'contentImport', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons', 'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp'] -const dependentModules = ['git'] // module which shouldn't be manually activated (e.g git is activated by remixd) +const dependentModules = ['git', 'hardhat'] // module which shouldn't be manually activated (e.g git is activated by remixd) export function isNative (name) { - const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons'] + const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity'] return nativePlugins.includes(name) || requiredModules.includes(name) } diff --git a/jest.config.js b/jest.config.js index 30b91f3cbe..4b90409756 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,5 +5,18 @@ module.exports = { }, resolver: '@nrwl/jest/plugins/resolver', moduleFileExtensions: ['ts', 'js', 'html'], - coverageReporters: ['html'] + coverageReporters: ['html'], + moduleNameMapper:{ + "@remix-project/remix-analyzer": "/../../dist/libs/remix-analyzer/index.js", + "@remix-project/remix-astwalker": "/../../dist/libs/remix-astwalker/index.js", + "@remix-project/remix-debug": "/../../dist/libs/remix-debug/src/index.js", + "@remix-project/remix-lib": "/../../dist/libs/remix-lib/src/index.js", + "@remix-project/remix-simulator": "/../../dist/libs/remix-simulator/src/index.js", + "@remix-project/remix-solidity": "/../../dist/libs/remix-solidity/index.js", + "@remix-project/remix-tests": "/../../dist/libs/remix-tests/src/index.js", + "@remix-project/remix-url-resolver": + "/../../dist/libs/remix-url-resolver/index.js" + , + "@remix-project/remixd": "/../../dist/libs/remixd/index.js" + } }; diff --git a/libs/remix-lib/src/execution/txExecution.ts b/libs/remix-lib/src/execution/txExecution.ts index 84edfc1f8e..8eb1c927dd 100644 --- a/libs/remix-lib/src/execution/txExecution.ts +++ b/libs/remix-lib/src/execution/txExecution.ts @@ -53,10 +53,10 @@ export function callFunction (from, to, data, value, gasLimit, funAbi, txRunner, /** * check if the vm has errored * - * @param {Object} txResult - the value returned by the vm + * @param {Object} execResult - execution result given by the VM * @return {Object} - { error: true/false, message: DOMNode } */ -export function checkVMError (txResult) { +export function checkVMError (execResult) { const errorCode = { OUT_OF_GAS: 'out of gas', STACK_UNDERFLOW: 'stack underflow', @@ -74,10 +74,10 @@ export function checkVMError (txResult) { error: false, message: '' } - if (!txResult.result.execResult.exceptionError) { + if (!execResult.exceptionError) { return ret } - const exceptionError = txResult.result.execResult.exceptionError.error || '' + const exceptionError = execResult.exceptionError.error || '' const error = `VM error: ${exceptionError}.\n` let msg if (exceptionError === errorCode.INVALID_OPCODE) { @@ -87,7 +87,7 @@ export function checkVMError (txResult) { msg = '\tThe transaction ran out of gas. Please increase the Gas Limit.\n' ret.error = true } else if (exceptionError === errorCode.REVERT) { - const returnData = txResult.result.execResult.returnValue + const returnData = execResult.returnValue // It is the hash of Error(string) if (returnData && (returnData.slice(0, 4).toString('hex') === '08c379a0')) { const abiCoder = new ethers.utils.AbiCoder() diff --git a/libs/remix-lib/src/execution/txFormat.ts b/libs/remix-lib/src/execution/txFormat.ts index 7d3188a1d0..62f90be1a8 100644 --- a/libs/remix-lib/src/execution/txFormat.ts +++ b/libs/remix-lib/src/execution/txFormat.ts @@ -314,7 +314,7 @@ export function deployLibrary (libraryName, libraryShortName, library, contracts if (err) { return callback(err) } - const address = txResult.result.createdAddress || txResult.result.contractAddress + const address = txResult.receipt.contractAddress library.address = address callback(err, address) }) diff --git a/libs/remix-lib/src/execution/txListener.ts b/libs/remix-lib/src/execution/txListener.ts index fea5cdf270..2ec85c3fc9 100644 --- a/libs/remix-lib/src/execution/txListener.ts +++ b/libs/remix-lib/src/execution/txListener.ts @@ -4,17 +4,16 @@ import { ethers } from 'ethers' import { toBuffer } from 'ethereumjs-util' import { EventManager } from '../eventManager' import { compareByteCode } from '../util' -import { ExecutionContext } from './execution-context' import { decodeResponse } from './txFormat' import { getFunction, getReceiveInterface, getConstructorInterface, visitContracts, makeFullTypeDefinition } from './txHelper' -function addExecutionCosts (txResult, tx) { - if (txResult && txResult.result) { - if (txResult.result.execResult) { - tx.returnValue = txResult.result.execResult.returnValue - if (txResult.result.execResult.gasUsed) tx.executionCost = txResult.result.execResult.gasUsed.toString(10) +function addExecutionCosts (txResult, tx, execResult) { + if (txResult) { + if (execResult) { + tx.returnValue = execResult.returnValue + if (execResult.gasUsed) tx.executionCost = execResult.gasUsed.toString(10) } - if (txResult.result.gasUsed) tx.transactionCost = txResult.result.gasUsed.toString(10) + if (txResult.receipt && txResult.receipt.gasUsed) tx.transactionCost = txResult.receipt.gasUsed.toString(10) } } @@ -40,7 +39,7 @@ export class TxListener { constructor (opt, executionContext) { this.event = new EventManager() // has a default for now for backwards compatability - this.executionContext = executionContext || new ExecutionContext() + this.executionContext = executionContext this._api = opt.api this._resolvedTransactions = {} this._resolvedContracts = {} @@ -55,7 +54,7 @@ export class TxListener { } }) - opt.event.udapp.register('callExecuted', (error, from, to, data, lookupOnly, txResult) => { + opt.event.udapp.register('callExecuted', async (error, from, to, data, lookupOnly, txResult) => { if (error) return // we go for that case if // in VM mode @@ -63,17 +62,25 @@ export class TxListener { if (!this._isListening) return // we don't listen if (this._loopId && this.executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network + let returnValue + let execResult + if (this.executionContext.isVM()) { + execResult = await this.executionContext.web3().eth.getExecutionResultFromSimulator(txResult.transactionHash) + returnValue = execResult.returnValue + } else { + returnValue = toBuffer(txResult.result) + } const call = { from: from, to: to, input: data, hash: txResult.transactionHash ? txResult.transactionHash : 'call' + (from || '') + to + data, isCall: true, - returnValue: this.executionContext.isVM() ? txResult.result.execResult.returnValue : toBuffer(txResult.result), + returnValue, envMode: this.executionContext.getProvider() } - addExecutionCosts(txResult, call) + addExecutionCosts(txResult, call, execResult) this._resolveTx(call, call, (error, resolvedData) => { if (!error) { this.event.trigger('newCall', [call]) @@ -89,12 +96,17 @@ export class TxListener { // in web3 mode && listen remix txs only if (!this._isListening) return // we don't listen if (this._loopId && this.executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network - this.executionContext.web3().eth.getTransaction(txResult.transactionHash, (error, tx) => { + this.executionContext.web3().eth.getTransaction(txResult.transactionHash, async (error, tx) => { if (error) return console.log(error) - addExecutionCosts(txResult, tx) + let execResult + if (this.executionContext.isVM()) { + execResult = await this.executionContext.web3().eth.getExecutionResultFromSimulator(txResult.transactionHash) + } + + addExecutionCosts(txResult, tx, execResult) tx.envMode = this.executionContext.getProvider() - tx.status = txResult.result.status // 0x0 or 0x1 + tx.status = txResult.receipt.status // 0x0 or 0x1 this._resolve([tx], () => { }) }) diff --git a/libs/remix-lib/src/execution/txRunner.ts b/libs/remix-lib/src/execution/txRunner.ts index 498f6ece11..e58ac62d5e 100644 --- a/libs/remix-lib/src/execution/txRunner.ts +++ b/libs/remix-lib/src/execution/txRunner.ts @@ -1,91 +1,26 @@ 'use strict' -import { Transaction } from '@ethereumjs/tx' -import { Block } from '@ethereumjs/block' -import { BN, bufferToHex, Address } from 'ethereumjs-util' -import { ExecutionContext } from './execution-context' import { EventManager } from '../eventManager' export class TxRunner { event - executionContext - _api - blockNumber runAsync pendingTxs - vmaccounts queusTxs - blocks - commonContext - - constructor (vmaccounts, api, executionContext) { + opt + internalRunner + constructor (internalRunner, opt) { + this.opt = opt || {} + this.internalRunner = internalRunner this.event = new EventManager() - // has a default for now for backwards compatability - this.executionContext = executionContext || new ExecutionContext() - this.commonContext = this.executionContext.vmObject().common - this._api = api - this.blockNumber = 0 - this.runAsync = true - if (this.executionContext.isVM()) { - // this.blockNumber = 1150000 // The VM is running in Homestead mode, which started at this block. - this.blockNumber = 0 // The VM is running in Homestead mode, which started at this block. - this.runAsync = false // We have to run like this cause the VM Event Manager does not support running multiple txs at the same time. - } + + this.runAsync = this.opt.runAsync || true // We have to run like this cause the VM Event Manager does not support running multiple txs at the same time. + this.pendingTxs = {} - this.vmaccounts = vmaccounts this.queusTxs = [] - this.blocks = [] } rawRun (args, confirmationCb, gasEstimationForceSend, promptCb, cb) { - let timestamp = Date.now() - if (args.timestamp) { - timestamp = args.timestamp - } - run(this, args, timestamp, confirmationCb, gasEstimationForceSend, promptCb, cb) - } - - _executeTx (tx, gasPrice, api, promptCb, callback) { - if (gasPrice) tx.gasPrice = this.executionContext.web3().utils.toHex(gasPrice) - if (api.personalMode()) { - promptCb( - (value) => { - this._sendTransaction(this.executionContext.web3().personal.sendTransaction, tx, value, callback) - }, - () => { - return callback('Canceled by user.') - } - ) - } else { - this._sendTransaction(this.executionContext.web3().eth.sendTransaction, tx, null, callback) - } - } - - _sendTransaction (sendTx, tx, pass, callback) { - const cb = (err, resp) => { - if (err) { - return callback(err, resp) - } - this.event.trigger('transactionBroadcasted', [resp]) - var listenOnResponse = () => { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - const result = await tryTillReceiptAvailable(resp, this.executionContext) - tx = await tryTillTxAvailable(resp, this.executionContext) - resolve({ - result, - tx, - transactionHash: result ? result['transactionHash'] : null - }) - }) - } - listenOnResponse().then((txData) => { callback(null, txData) }).catch((error) => { callback(error) }) - } - const args = pass !== null ? [tx, pass, cb] : [tx, cb] - try { - sendTx.apply({}, args) - } catch (e) { - return callback(`Send transaction failed: ${e.message} . if you use an injected provider, please check it is properly unlocked. `) - } + run(this, args, args.timestamp || Date.now(), confirmationCb, gasEstimationForceSend, promptCb, cb) } execute (args, confirmationCb, gasEstimationForceSend, promptCb, callback) { @@ -93,185 +28,10 @@ export class TxRunner { if (data.slice(0, 2) !== '0x') { data = '0x' + data } - - if (!this.executionContext.isVM()) { - return this.runInNode(args.from, args.to, data, args.value, args.gasLimit, args.useCall, confirmationCb, gasEstimationForceSend, promptCb, callback) - } - try { - this.runInVm(args.from, args.to, data, args.value, args.gasLimit, args.useCall, args.timestamp, callback) - } catch (e) { - callback(e, null) - } - } - - runInVm (from, to, data, value, gasLimit, useCall, timestamp, callback) { - const self = this - const account = self.vmaccounts[from] - if (!account) { - return callback('Invalid account selected') - } - - if (Number.isInteger(gasLimit)) { - gasLimit = '0x' + gasLimit.toString(16) - } - - this.executionContext.vm().stateManager.getAccount(Address.fromString(from)).then((res) => { - // See https://github.com/ethereumjs/ethereumjs-tx/blob/master/docs/classes/transaction.md#constructor - // for initialization fields and their types - if (!value) value = 0 - if (typeof value === 'string') { - if (value.startsWith('0x')) value = new BN(value.replace('0x', ''), 'hex') - else { - try { - value = new BN(value, 10) - } catch (e) { - return callback('Unable to parse the value ' + e.message) - } - } - } - const tx = Transaction.fromTxData({ - nonce: new BN(res.nonce), - gasPrice: '0x1', - gasLimit: gasLimit, - to: to, - value: value, - data: Buffer.from(data.slice(2), 'hex') - }, { common: this.commonContext }).sign(account.privateKey) - - const coinbases = ['0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e'] - const difficulties = [new BN('69762765929000', 10), new BN('70762765929000', 10), new BN('71762765929000', 10)] - - var block = Block.fromBlockData({ - header: { - timestamp: timestamp || (new Date().getTime() / 1000 | 0), - number: self.blockNumber, - coinbase: coinbases[self.blockNumber % coinbases.length], - difficulty: difficulties[self.blockNumber % difficulties.length], - gasLimit: new BN(gasLimit.replace('0x', ''), 16).imuln(2) - }, - transactions: [tx] - }, { common: this.commonContext }) - - if (!useCall) { - ++self.blockNumber - this.runBlockInVm(tx, block, callback) - } else { - this.executionContext.vm().stateManager.checkpoint().then(() => { - this.runBlockInVm(tx, block, (err, result) => { - this.executionContext.vm().stateManager.revert().then(() => { - callback(err, result) - }) - }) - }) - } - }).catch((e) => { - callback(e) - }) - } - - runBlockInVm (tx, block, callback) { - this.executionContext.vm().runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false }).then((results) => { - const result = results.results[0] - if (result) { - const status = result.execResult.exceptionError ? 0 : 1 - result.status = `0x${status}` - } - this.executionContext.addBlock(block) - this.executionContext.trackTx('0x' + tx.hash().toString('hex'), block) - callback(null, { - result: result, - transactionHash: bufferToHex(Buffer.from(tx.hash())) - }) - }).catch((err) => { - callback(err) - }) - } - - runInNode (from, to, data, value, gasLimit, useCall, confirmCb, gasEstimationForceSend, promptCb, callback) { - const tx = { from: from, to: to, data: data, value: value } - - if (useCall) { - tx['gas'] = gasLimit - return this.executionContext.web3().eth.call(tx, function (error, result) { - callback(error, { - result: result, - transactionHash: result ? result.transactionHash : null - }) - }) - } - this.executionContext.web3().eth.estimateGas(tx, (err, gasEstimation) => { - if (err && err.message.indexOf('Invalid JSON RPC response') !== -1) { - // // @todo(#378) this should be removed when https://github.com/WalletConnect/walletconnect-monorepo/issues/334 is fixed - err = 'Gas estimation failed because of an unknown internal error. This may indicated that the transaction will fail.' - } - gasEstimationForceSend(err, () => { - // callback is called whenever no error - tx['gas'] = !gasEstimation ? gasLimit : gasEstimation - - if (this._api.config.getUnpersistedProperty('doNotShowTransactionConfirmationAgain')) { - return this._executeTx(tx, null, this._api, promptCb, callback) - } - - this._api.detectNetwork((err, network) => { - if (err) { - console.log(err) - return - } - - confirmCb(network, tx, tx['gas'], (gasPrice) => { - return this._executeTx(tx, gasPrice, this._api, promptCb, callback) - }, (error) => { - callback(error) - }) - }) - }, () => { - const blockGasLimit = this.executionContext.currentblockGasLimit() - // NOTE: estimateGas very likely will return a large limit if execution of the code failed - // we want to be able to run the code in order to debug and find the cause for the failure - if (err) return callback(err) - - let warnEstimation = ' An important gas estimation might also be the sign of a problem in the contract code. Please check loops and be sure you did not sent value to a non payable function (that\'s also the reason of strong gas estimation). ' - warnEstimation += ' ' + err - - if (gasEstimation > gasLimit) { - return callback('Gas required exceeds limit: ' + gasLimit + '. ' + warnEstimation) - } - if (gasEstimation > blockGasLimit) { - return callback('Gas required exceeds block gas limit: ' + gasLimit + '. ' + warnEstimation) - } - }) - }) + this.internalRunner.execute(args, confirmationCb, gasEstimationForceSend, promptCb, callback) } } -async function tryTillReceiptAvailable (txhash, executionContext) { - return new Promise((resolve, reject) => { - executionContext.web3().eth.getTransactionReceipt(txhash, async (err, receipt) => { - if (err || !receipt) { - // Try again with a bit of delay if error or if result still null - await pause() - return resolve(await tryTillReceiptAvailable(txhash, executionContext)) - } - return resolve(receipt) - }) - }) -} - -async function tryTillTxAvailable (txhash, executionContext) { - return new Promise((resolve, reject) => { - executionContext.web3().eth.getTransaction(txhash, async (err, tx) => { - if (err || !tx) { - // Try again with a bit of delay if error or if result still null - await pause() - return resolve(await tryTillTxAvailable(txhash, executionContext)) - } - return resolve(tx) - }) - }) -} - -async function pause () { return new Promise((resolve, reject) => { setTimeout(resolve, 500) }) } - function run (self, tx, stamp, confirmationCb, gasEstimationForceSend = null, promptCb = null, callback = null) { if (!self.runAsync && Object.keys(self.pendingTxs).length) { return self.queusTxs.push({ tx, stamp, callback }) diff --git a/libs/remix-lib/src/execution/txRunnerVM.ts b/libs/remix-lib/src/execution/txRunnerVM.ts new file mode 100644 index 0000000000..ba021a7d0e --- /dev/null +++ b/libs/remix-lib/src/execution/txRunnerVM.ts @@ -0,0 +1,121 @@ +'use strict' +import { Transaction } from '@ethereumjs/tx' +import { Block } from '@ethereumjs/block' +import { BN, bufferToHex, Address } from 'ethereumjs-util' +import { EventManager } from '../eventManager' +import { LogsManager } from './logsManager' + +export class TxRunnerVM { + event + blockNumber + runAsync + pendingTxs + vmaccounts + queusTxs + blocks + txs + logsManager + commonContext + getVMObject: () => any + + constructor (vmaccounts, api, getVMObject) { + this.event = new EventManager() + this.logsManager = new LogsManager() + // has a default for now for backwards compatability + this.getVMObject = getVMObject + this.commonContext = this.getVMObject().common + this.blockNumber = 0 + this.runAsync = true + this.blockNumber = 0 // The VM is running in Homestead mode, which started at this block. + this.runAsync = false // We have to run like this cause the VM Event Manager does not support running multiple txs at the same time. + this.pendingTxs = {} + this.vmaccounts = vmaccounts + this.queusTxs = [] + this.blocks = [] + } + + execute (args, confirmationCb, gasEstimationForceSend, promptCb, callback) { + let data = args.data + if (data.slice(0, 2) !== '0x') { + data = '0x' + data + } + + try { + this.runInVm(args.from, args.to, data, args.value, args.gasLimit, args.useCall, args.timestamp, callback) + } catch (e) { + callback(e, null) + } + } + + runInVm (from, to, data, value, gasLimit, useCall, timestamp, callback) { + const self = this + const account = self.vmaccounts[from] + if (!account) { + return callback('Invalid account selected') + } + if (Number.isInteger(gasLimit)) { + gasLimit = '0x' + gasLimit.toString(16) + } + + this.getVMObject().stateManager.getAccount(Address.fromString(from)).then((res) => { + // See https://github.com/ethereumjs/ethereumjs-tx/blob/master/docs/classes/transaction.md#constructor + // for initialization fields and their types + value = value ? parseInt(value) : 0 + const tx = Transaction.fromTxData({ + nonce: new BN(res.nonce), + gasPrice: '0x1', + gasLimit: gasLimit, + to: to, + value: value, + data: Buffer.from(data.slice(2), 'hex') + }, { common: this.commonContext }).sign(account.privateKey) + + const coinbases = ['0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e'] + const difficulties = [new BN('69762765929000', 10), new BN('70762765929000', 10), new BN('71762765929000', 10)] + + var block = Block.fromBlockData({ + header: { + timestamp: timestamp || (new Date().getTime() / 1000 | 0), + number: self.blockNumber, + coinbase: coinbases[self.blockNumber % coinbases.length], + difficulty: difficulties[self.blockNumber % difficulties.length], + gasLimit: new BN(gasLimit.replace('0x', ''), 16).imuln(2) + }, + transactions: [tx] + }, { common: this.commonContext }) + + if (!useCall) { + ++self.blockNumber + this.runBlockInVm(tx, block, callback) + } else { + this.getVMObject().stateManager.checkpoint().then(() => { + this.runBlockInVm(tx, block, (err, result) => { + this.getVMObject().stateManager.revert().then(() => { + callback(err, result) + }) + }) + }) + } + }).catch((e) => { + callback(e) + }) + } + + runBlockInVm (tx, block, callback) { + this.getVMObject().vm.runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false }).then((results) => { + const result = results.results[0] + if (result) { + const status = result.execResult.exceptionError ? 0 : 1 + result.status = `0x${status}` + } + callback(null, { + result: result, + transactionHash: bufferToHex(Buffer.from(tx.hash())), + block, + tx + }) + }).catch(function (err) { + callback(err) + }) + } +} diff --git a/libs/remix-lib/src/execution/txRunnerWeb3.ts b/libs/remix-lib/src/execution/txRunnerWeb3.ts new file mode 100644 index 0000000000..650593dfbc --- /dev/null +++ b/libs/remix-lib/src/execution/txRunnerWeb3.ts @@ -0,0 +1,147 @@ +'use strict' +import { EventManager } from '../eventManager' +import Web3 from 'web3' + +export class TxRunnerWeb3 { + event + _api + getWeb3: () => Web3 + currentblockGasLimit: () => number + + constructor (api, getWeb3, currentblockGasLimit) { + this.event = new EventManager() + this.getWeb3 = getWeb3 + this.currentblockGasLimit = currentblockGasLimit + this._api = api + } + + _executeTx (tx, gasPrice, api, promptCb, callback) { + if (gasPrice) tx.gasPrice = this.getWeb3().utils.toHex(gasPrice) + if (api.personalMode()) { + promptCb( + (value) => { + this._sendTransaction((this.getWeb3() as any).personal.sendTransaction, tx, value, callback) + }, + () => { + return callback('Canceled by user.') + } + ) + } else { + this._sendTransaction(this.getWeb3().eth.sendTransaction, tx, null, callback) + } + } + + _sendTransaction (sendTx, tx, pass, callback) { + const cb = (err, resp) => { + if (err) { + return callback(err, resp) + } + this.event.trigger('transactionBroadcasted', [resp]) + var listenOnResponse = () => { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + const receipt = await tryTillReceiptAvailable(resp, this.getWeb3()) + tx = await tryTillTxAvailable(resp, this.getWeb3()) + resolve({ + receipt, + tx, + transactionHash: receipt ? receipt['transactionHash'] : null + }) + }) + } + listenOnResponse().then((txData) => { callback(null, txData) }).catch((error) => { callback(error) }) + } + const args = pass !== null ? [tx, pass, cb] : [tx, cb] + try { + sendTx.apply({}, args) + } catch (e) { + return callback(`Send transaction failed: ${e.message} . if you use an injected provider, please check it is properly unlocked. `) + } + } + + execute (args, confirmationCb, gasEstimationForceSend, promptCb, callback) { + let data = args.data + if (data.slice(0, 2) !== '0x') { + data = '0x' + data + } + + return this.runInNode(args.from, args.to, data, args.value, args.gasLimit, args.useCall, args.timestamp, confirmationCb, gasEstimationForceSend, promptCb, callback) + } + + runInNode (from, to, data, value, gasLimit, useCall, timestamp, confirmCb, gasEstimationForceSend, promptCb, callback) { + const tx = { from: from, to: to, data: data, value: value } + + if (useCall) { + const tag = Date.now() // for e2e reference + tx['gas'] = gasLimit + tx['timestamp'] = timestamp + return this.getWeb3().eth.call(tx, function (error, result: any) { + if (error) return callback(error) + callback(null, { + result: result + }) + }) + } + this.getWeb3().eth.estimateGas(tx, (err, gasEstimation) => { + if (err && err.message.indexOf('Invalid JSON RPC response') !== -1) { + // // @todo(#378) this should be removed when https://github.com/WalletConnect/walletconnect-monorepo/issues/334 is fixed + callback(new Error('Gas estimation failed because of an unknown internal error. This may indicated that the transaction will fail.')) + } + gasEstimationForceSend(err, () => { + // callback is called whenever no error + tx['gas'] = !gasEstimation ? gasLimit : gasEstimation + + if (this._api.config.getUnpersistedProperty('doNotShowTransactionConfirmationAgain')) { + return this._executeTx(tx, null, this._api, promptCb, callback) + } + + this._api.detectNetwork((err, network) => { + if (err) { + console.log(err) + return + } + + confirmCb(network, tx, tx['gas'], (gasPrice) => { + return this._executeTx(tx, gasPrice, this._api, promptCb, callback) + }, (error) => { + callback(error) + }) + }) + }, () => { + const blockGasLimit = this.currentblockGasLimit() + // NOTE: estimateGas very likely will return a large limit if execution of the code failed + // we want to be able to run the code in order to debug and find the cause for the failure + if (err) return callback(err) + + let warnEstimation = ' An important gas estimation might also be the sign of a problem in the contract code. Please check loops and be sure you did not sent value to a non payable function (that\'s also the reason of strong gas estimation). ' + warnEstimation += ' ' + err + + if (gasEstimation > gasLimit) { + return callback('Gas required exceeds limit: ' + gasLimit + '. ' + warnEstimation) + } + if (gasEstimation > blockGasLimit) { + return callback('Gas required exceeds block gas limit: ' + gasLimit + '. ' + warnEstimation) + } + }) + }) + } +} + +async function tryTillReceiptAvailable (txhash, web3) { + try { + const receipt = await web3.eth.getTransactionReceipt(txhash) + if (receipt) return receipt + } catch (e) {} + await pause() + return await tryTillReceiptAvailable(txhash, web3) +} + +async function tryTillTxAvailable (txhash, web3) { + try { + const tx = await web3.eth.getTransaction(txhash) + if (tx) return tx + } catch (e) {} + return await tryTillTxAvailable(txhash, web3) +} + +async function pause () { return new Promise((resolve, reject) => { setTimeout(resolve, 500) }) } diff --git a/libs/remix-lib/src/helpers/txResultHelper.ts b/libs/remix-lib/src/helpers/txResultHelper.ts index 7aa7967f0d..3264e5b771 100644 --- a/libs/remix-lib/src/helpers/txResultHelper.ts +++ b/libs/remix-lib/src/helpers/txResultHelper.ts @@ -20,9 +20,9 @@ function convertToPrefixedHex (input) { Also, VM results use BN and Buffers, Node results use hex strings/ints, So we need to normalize the values to prefixed hex strings */ -export function resultToRemixTx (txResult) { - const { result, transactionHash } = txResult - const { status, execResult, gasUsed, createdAddress, contractAddress } = result +export function resultToRemixTx (txResult, execResult) { + const { receipt, transactionHash, result } = txResult + const { status, gasUsed, contractAddress } = receipt let returnValue, errorMessage if (isHexString(result)) { @@ -38,6 +38,6 @@ export function resultToRemixTx (txResult) { gasUsed: convertToPrefixedHex(gasUsed), error: errorMessage, return: convertToPrefixedHex(returnValue), - createdAddress: convertToPrefixedHex(createdAddress || contractAddress) + createdAddress: convertToPrefixedHex(contractAddress) } } diff --git a/libs/remix-lib/src/index.ts b/libs/remix-lib/src/index.ts index 5b1108ff70..ae3864a8c9 100644 --- a/libs/remix-lib/src/index.ts +++ b/libs/remix-lib/src/index.ts @@ -12,9 +12,11 @@ import * as txHelper from './execution/txHelper' import * as txFormat from './execution/txFormat' import { TxListener } from './execution/txListener' import { TxRunner } from './execution/txRunner' -import { ExecutionContext } from './execution/execution-context' +import { LogsManager } from './execution/logsManager' import * as typeConversion from './execution/typeConversion' -import { UniversalDApp } from './universalDapp' +import { TxRunnerVM } from './execution/txRunnerVM' +import { TxRunnerWeb3 } from './execution/txRunnerWeb3' +import * as txResultHelper from './helpers/txResultHelper' export = modules() @@ -23,7 +25,8 @@ function modules () { EventManager: EventManager, helpers: { ui: uiHelper, - compiler: compilerHelper + compiler: compilerHelper, + txResultHelper }, vm: { Web3Providers: Web3Providers, @@ -36,12 +39,13 @@ function modules () { EventsDecoder: EventsDecoder, txExecution: txExecution, txHelper: txHelper, - executionContext: new ExecutionContext(), txFormat: txFormat, txListener: TxListener, - txRunner: TxRunner, - typeConversion: typeConversion - }, - UniversalDApp: UniversalDApp + TxRunner: TxRunner, + TxRunnerWeb3: TxRunnerWeb3, + TxRunnerVM: TxRunnerVM, + typeConversion: typeConversion, + LogsManager + } } } diff --git a/libs/remix-lib/src/universalDapp.ts b/libs/remix-lib/src/universalDapp.ts deleted file mode 100644 index 030a8da811..0000000000 --- a/libs/remix-lib/src/universalDapp.ts +++ /dev/null @@ -1,379 +0,0 @@ -import { waterfall } from 'async' -import { BN, privateToAddress, isValidPrivate, toChecksumAddress, Address } from 'ethereumjs-util' -import { randomBytes } from 'crypto' -import { EventEmitter } from 'events' -import { TxRunner } from './execution/txRunner' -import { sortAbiFunction, getFallbackInterface, getReceiveInterface, inputParametersDeclarationToString } from './execution/txHelper' -import { EventManager } from './eventManager' -import { ExecutionContext } from './execution/execution-context' -import { resultToRemixTx } from './helpers/txResultHelper' - -export class UniversalDApp { - events - event - executionContext - config - txRunner - accounts - transactionContextAPI - - constructor (config, executionContext) { - this.events = new EventEmitter() - this.event = new EventManager() - // has a default for now for backwards compatability - this.executionContext = executionContext || new ExecutionContext() - this.config = config - - this.txRunner = new TxRunner({}, { - config: config, - detectNetwork: (cb) => { - this.executionContext.detectNetwork(cb) - }, - personalMode: () => { - return this.executionContext.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false - } - }, this.executionContext) - this.accounts = {} - this.executionContext.event.register('contextChanged', this.resetEnvironment.bind(this)) - } - - // TODO : event should be triggered by Udapp instead of TxListener - /** Listen on New Transaction. (Cannot be done inside constructor because txlistener doesn't exist yet) */ - startListening (txlistener) { - txlistener.event.register('newTransaction', (tx) => { - this.events.emit('newTransaction', tx) - }) - } - - resetEnvironment () { - this.accounts = {} - if (this.executionContext.isVM()) { - this._addAccount('3cd7232cd6f3fc66a57a6bedc1a8ed6c228fff0a327e169c2bcc5e869ed49511', '0x56BC75E2D63100000') - this._addAccount('2ac6c190b09897cd8987869cc7b918cfea07ee82038d492abce033c75c1b1d0c', '0x56BC75E2D63100000') - this._addAccount('dae9801649ba2d95a21e688b56f77905e5667c44ce868ec83f82e838712a2c7a', '0x56BC75E2D63100000') - this._addAccount('d74aa6d18aa79a05f3473dd030a97d3305737cbc8337d940344345c1f6b72eea', '0x56BC75E2D63100000') - this._addAccount('71975fbf7fe448e004ac7ae54cad0a383c3906055a65468714156a07385e96ce', '0x56BC75E2D63100000') - } - // TODO: most params here can be refactored away in txRunner - this.txRunner = new TxRunner(this.accounts, { - // TODO: only used to check value of doNotShowTransactionConfirmationAgain property - config: this.config, - // TODO: to refactor, TxRunner already has access to executionContext - detectNetwork: (cb) => { - this.executionContext.detectNetwork(cb) - }, - personalMode: () => { - return this.executionContext.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false - } - }, this.executionContext) - this.txRunner.event.register('transactionBroadcasted', (txhash) => { - this.executionContext.detectNetwork((error, network) => { - if (error || !network) return - this.event.trigger('transactionBroadcasted', [txhash, network.name]) - }) - }) - } - - resetAPI (transactionContextAPI) { - this.transactionContextAPI = transactionContextAPI - } - - /** - * Create a VM Account - * @param {{privateKey: string, balance: string}} newAccount The new account to create - */ - createVMAccount (newAccount) { - const { privateKey, balance } = newAccount - if (this.executionContext.getProvider() !== 'vm') { - throw new Error('plugin API does not allow creating a new account through web3 connection. Only vm mode is allowed') - } - this._addAccount(privateKey, balance) - const privKey = Buffer.from(privateKey, 'hex') - return '0x' + privateToAddress(privKey).toString('hex') - } - - newAccount (password, passwordPromptCb, cb) { - if (!this.executionContext.isVM()) { - if (!this.config.get('settings/personal-mode')) { - return cb('Not running in personal mode') - } - return passwordPromptCb((passphrase) => { - this.executionContext.web3().personal.newAccount(passphrase, cb) - }) - } - let privateKey - do { - privateKey = randomBytes(32) - } while (!isValidPrivate(privateKey)) - this._addAccount(privateKey, '0x56BC75E2D63100000') - cb(null, '0x' + privateToAddress(privateKey).toString('hex')) - } - - /** Add an account to the list of account (only for Javascript VM) */ - _addAccount (privateKey, balance) { - if (!this.executionContext.isVM()) { - throw new Error('_addAccount() cannot be called in non-VM mode') - } - - if (!this.accounts) { - return - } - privateKey = Buffer.from(privateKey, 'hex') - const address = privateToAddress(privateKey) - - // FIXME: we don't care about the callback, but we should still make this proper - const stateManager = this.executionContext.vm().stateManager - stateManager.getAccount(address).then((account) => { - account.balance = new BN(balance.replace('0x', '') || 'f00000000000000001', 16) - stateManager.putAccount(address, account).catch((error) => { - console.log(error) - }) - }).catch((error) => { - console.log(error) - }) - - this.accounts[toChecksumAddress('0x' + address.toString('hex'))] = { privateKey, nonce: 0 } - } - - /** Return the list of accounts */ - getAccounts (cb) { - return new Promise((resolve, reject) => { - const provider = this.executionContext.getProvider() - switch (provider) { - case 'vm': - if (!this.accounts) { - if (cb) cb('No accounts?') - reject(new Error('No accounts?')) - return - } - if (cb) cb(null, Object.keys(this.accounts)) - resolve(Object.keys(this.accounts)) - break - case 'web3': - if (this.config.get('settings/personal-mode')) { - return this.executionContext.web3().personal.getListAccounts((error, accounts) => { - if (cb) cb(error, accounts) - if (error) return reject(error) - resolve(accounts) - }) - } else { - this.executionContext.web3().eth.getAccounts((error, accounts) => { - if (cb) cb(error, accounts) - if (error) return reject(error) - resolve(accounts) - }) - } - break - case 'injected': { - this.executionContext.web3().eth.getAccounts((error, accounts) => { - if (cb) cb(error, accounts) - if (error) return reject(error) - resolve(accounts) - }) - } - } - }) - } - - /** Get the balance of an address */ - getBalance (address, cb) { - if (!this.executionContext.isVM()) { - return this.executionContext.web3().eth.getBalance(address, (err, res) => { - if (err) { - return cb(err) - } - cb(null, res.toString(10)) - }) - } - if (!this.accounts) { - return cb('No accounts?') - } - - this.executionContext.vm().stateManager.getAccount(Address.fromString(address)).then((res) => { - cb(null, new BN(res.balance).toString(10)) - }).catch(() => { - cb('Account not found') - }) - } - - /** Get the balance of an address, and convert wei to ether */ - getBalanceInEther (address, callback) { - this.getBalance(address, (error, balance) => { - if (error) { - return callback(error) - } - callback(null, this.executionContext.web3().utils.fromWei(balance, 'ether')) - }) - } - - pendingTransactionsCount () { - return Object.keys(this.txRunner.pendingTxs).length - } - - /** - * deploy the given contract - * - * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ). - * @param {Function} callback - callback. - */ - createContract (data, confirmationCb, continueCb, promptCb, callback) { - this.runTx({ data: data, useCall: false }, confirmationCb, continueCb, promptCb, callback) - } - - /** - * call the current given contract - * - * @param {String} to - address of the contract to call. - * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ). - * @param {Object} funAbi - abi definition of the function to call. - * @param {Function} callback - callback. - */ - callFunction (to, data, funAbi, confirmationCb, continueCb, promptCb, callback) { - const useCall = funAbi.stateMutability === 'view' || funAbi.stateMutability === 'pure' - this.runTx({ to, data, useCall }, confirmationCb, continueCb, promptCb, callback) - } - - /** - * call the current given contract - * - * @param {String} to - address of the contract to call. - * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ). - * @param {Function} callback - callback. - */ - sendRawTransaction (to, data, confirmationCb, continueCb, promptCb, callback) { - this.runTx({ to, data, useCall: false }, confirmationCb, continueCb, promptCb, callback) - } - - context () { - return (this.executionContext.isVM() ? 'memory' : 'blockchain') - } - - getABI (contract) { - return sortAbiFunction(contract.abi) - } - - getFallbackInterface (contractABI) { - return getFallbackInterface(contractABI) - } - - getReceiveInterface (contractABI) { - return getReceiveInterface(contractABI) - } - - getInputs (funABI) { - if (!funABI.inputs) { - return '' - } - return inputParametersDeclarationToString(funABI.inputs) - } - - /** - * This function send a tx only to javascript VM or testnet, will return an error for the mainnet - * SHOULD BE TAKEN CAREFULLY! - * - * @param {Object} tx - transaction. - */ - sendTransaction (tx) { - return new Promise((resolve, reject) => { - this.executionContext.detectNetwork((error, network) => { - if (error) return reject(error) - if (network.name === 'Main' && network.id === '1') { - return reject(new Error('It is not allowed to make this action against mainnet')) - } - this.silentRunTx(tx, (error, result) => { - if (error) return reject(error) - try { - resolve(resultToRemixTx(result)) - } catch (e) { - reject(e) - } - }) - }) - }) - } - - /** - * This function send a tx without alerting the user (if mainnet or if gas estimation too high). - * SHOULD BE TAKEN CAREFULLY! - * - * @param {Object} tx - transaction. - * @param {Function} callback - callback. - */ - silentRunTx (tx, cb) { - this.txRunner.rawRun( - tx, - (network, tx, gasEstimation, continueTxExecution, cancelCb) => { continueTxExecution() }, - (error, continueTxExecution, cancelCb) => { if (error) { cb(error) } else { continueTxExecution() } }, - (okCb, cancelCb) => { okCb() }, - cb - ) - } - - runTx (args, confirmationCb, continueCb, promptCb, cb) { - const self = this - waterfall([ - function getGasLimit (next) { - if (self.transactionContextAPI.getGasLimit) { - return self.transactionContextAPI.getGasLimit(next) - } - next(null, 3000000) - }, - function queryValue (gasLimit, next) { - if (args.value) { - return next(null, args.value, gasLimit) - } - if (args.useCall || !self.transactionContextAPI.getValue) { - return next(null, 0, gasLimit) - } - self.transactionContextAPI.getValue(function (err, value) { - next(err, value, gasLimit) - }) - }, - function getAccount (value, gasLimit, next) { - if (args.from) { - return next(null, args.from, value, gasLimit) - } - if (self.transactionContextAPI.getAddress) { - return self.transactionContextAPI.getAddress(function (err, address) { - next(err, address, value, gasLimit) - }) - } - self.getAccounts(function (err, accounts) { - const address = accounts[0] - - if (err) return next(err) - if (!address) return next('No accounts available') - if (self.executionContext.isVM() && !self.accounts[address]) { - return next('Invalid account selected') - } - next(null, address, value, gasLimit) - }) - }, - function runTransaction (fromAddress, value, gasLimit, next) { - const tx = { to: args.to, data: args.data.dataHex, useCall: args.useCall, from: fromAddress, value: value, gasLimit: gasLimit, timestamp: args.data.timestamp } - const payLoad = { funAbi: args.data.funAbi, funArgs: args.data.funArgs, contractBytecode: args.data.contractBytecode, contractName: args.data.contractName, contractABI: args.data.contractABI, linkReferences: args.data.linkReferences } - let timestamp = Date.now() - if (tx.timestamp) { - timestamp = tx.timestamp - } - - self.event.trigger('initiatingTransaction', [timestamp, tx, payLoad]) - self.txRunner.rawRun(tx, confirmationCb, continueCb, promptCb, - function (error, result) { - const eventName = (tx.useCall ? 'callExecuted' : 'transactionExecuted') - self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad]) - - if (error && (typeof (error) !== 'string')) { - if (error.message) error = error.message - else { - // eslint-disable-next-line no-empty - try { error = 'error: ' + JSON.stringify(error) } catch (e) {} - } - } - next(error, result) - } - ) - } - ], cb) - } -} diff --git a/libs/remix-lib/src/web3Provider/web3VmProvider.ts b/libs/remix-lib/src/web3Provider/web3VmProvider.ts index 367370f928..59b0e7e909 100644 --- a/libs/remix-lib/src/web3Provider/web3VmProvider.ts +++ b/libs/remix-lib/src/web3Provider/web3VmProvider.ts @@ -31,6 +31,9 @@ export class Web3VmProvider { toBigNumber isAddress utils + txsMapBlock + blocks + latestBlockNumber constructor () { this.web3 = new Web3() @@ -69,6 +72,9 @@ export class Web3VmProvider { this.toBigNumber = (...args) => this.web3.utils.toBN(...args) this.isAddress = (...args) => this.web3.utils.isAddress(...args) this.utils = Web3.utils || [] + this.txsMapBlock = {} + this.blocks = {} + this.latestBlockNumber = 0 } setVM (vm) { diff --git a/libs/remix-lib/test/txFormat.ts b/libs/remix-lib/test/txFormat.ts index d33760f9f9..8bff069fc2 100644 --- a/libs/remix-lib/test/txFormat.ts +++ b/libs/remix-lib/test/txFormat.ts @@ -5,8 +5,6 @@ import * as txHelper from '../src/execution/txHelper' import { hexToIntArray } from '../src/util' let compiler = require('solc') import { compilerInput } from '../src/helpers/compilerHelper' -import { ExecutionContext } from '../src/execution/execution-context' -const executionContext = new ExecutionContext() const solidityVersion = 'v0.6.0+commit.26b70077' /* tape *********************************************************** */ @@ -151,7 +149,6 @@ function testInvalidTupleInput (st, params) { /* tape *********************************************************** */ tape('ContractParameters - (TxFormat.buildData) - link Libraries', function (t) { - executionContext.setContext('vm', null, null, null) const compileData = compiler.compile(compilerInput(deploySimpleLib)) const fakeDeployedContracts = { @@ -161,8 +158,8 @@ tape('ContractParameters - (TxFormat.buildData) - link Libraries', function (t) } const callbackDeployLibraries = (param, callback) => { callback(null, { - result: { - createdAddress: fakeDeployedContracts[param.data.contractName] + receipt: { + contractAddress: fakeDeployedContracts[param.data.contractName] } }) } // fake diff --git a/libs/remix-lib/test/txResultHelper.ts b/libs/remix-lib/test/txResultHelper.ts index 8fa2e5cdf2..67a136dc84 100644 --- a/libs/remix-lib/test/txResultHelper.ts +++ b/libs/remix-lib/test/txResultHelper.ts @@ -18,12 +18,13 @@ const GAS_USED_INT = 75427 const GAS_USED_HEX = '0x126a3' const NODE_CALL_RESULT = { + receipt: {}, result: RETURN_VALUE_HEX, transactionHash: undefined } const NODE_TX_RESULT = { - result: { + receipt: { blockHash: '0x380485a4e6372a42e36489783c7f7cb66257612133cd245859c206fd476e9c44', blockNumber: 5994, contractAddress: CONTRACT_ADDRESS_HEX, @@ -39,26 +40,31 @@ const NODE_TX_RESULT = { } const VM_RESULT = { - result: { + receipt: { amountSpent: new BN(1), - createdAddress: CONTRACT_ADDRESS_BUFFER, + contractAddress: CONTRACT_ADDRESS_BUFFER, gasRefund: new BN(0), gasUsed: new BN(GAS_USED_INT), status: STATUS_OK, - execResult: { - exceptionError: null, - gasRefund: new BN(0), - gasUsed: new BN(GAS_USED_INT), - returnValue: RETURN_VALUE_BUFFER - } }, transactionHash: TRANSACTION_HASH } +const EXEC_RESULT = { + exceptionError: null, + gasRefund: new BN(0), + gasUsed: new BN(GAS_USED_INT), + returnValue: RETURN_VALUE_BUFFER +} + +const EXEC_RESULT_ERROR = { + exceptionError: 'this is an error' +} + tape('converts node transaction result to RemixTx', function (t) { // contract creation let txResult = { ...NODE_TX_RESULT } - let remixTx = resultToRemixTx(txResult) + let remixTx = resultToRemixTx(txResult, {}) t.equal(remixTx.transactionHash, TRANSACTION_HASH) t.equal(remixTx.createdAddress, CONTRACT_ADDRESS_HEX) @@ -68,8 +74,8 @@ tape('converts node transaction result to RemixTx', function (t) { t.equal(remixTx.error, undefined) // contract method tx - txResult.result.contractAddress = null - remixTx = resultToRemixTx(txResult) + txResult.receipt.contractAddress = null + remixTx = resultToRemixTx(txResult, {}) t.equal(remixTx.createdAddress, null) t.end() @@ -77,7 +83,7 @@ tape('converts node transaction result to RemixTx', function (t) { tape('converts node call result to RemixTx', function (t) { let txResult = { ...NODE_CALL_RESULT } - let remixTx = resultToRemixTx(txResult) + let remixTx = resultToRemixTx(txResult, {}) t.equal(remixTx.transactionHash, undefined) t.equal(remixTx.createdAddress, undefined) @@ -91,7 +97,7 @@ tape('converts node call result to RemixTx', function (t) { tape('converts VM result to RemixTx', function (t) { let txResult = { ...VM_RESULT } - let remixTx = resultToRemixTx(txResult) + let remixTx = resultToRemixTx(txResult, EXEC_RESULT) t.equal(remixTx.transactionHash, TRANSACTION_HASH) @@ -101,8 +107,7 @@ tape('converts VM result to RemixTx', function (t) { t.equal(remixTx.return, RETURN_VALUE_HEX) t.equal(remixTx.error, null) - txResult.result.execResult.exceptionError = 'this is an error' - remixTx = resultToRemixTx(txResult) + remixTx = resultToRemixTx(VM_RESULT, EXEC_RESULT_ERROR) t.equal(remixTx.error, 'this is an error') t.end() diff --git a/libs/remix-simulator/package.json b/libs/remix-simulator/package.json index eb09b20106..b39deedf5a 100644 --- a/libs/remix-simulator/package.json +++ b/libs/remix-simulator/package.json @@ -14,7 +14,7 @@ ], "main": "src/index.js", "dependencies": { - "@remix-project/remix-lib": "^0.4.34", + "@remix-project/remix-lib": "../remix-lib", "ansi-gray": "^0.1.1", "async": "^3.1.0", "body-parser": "^1.18.2", diff --git a/libs/remix-simulator/src/genesis.ts b/libs/remix-simulator/src/genesis.ts index f3d7d0509e..839a0c4cb6 100644 --- a/libs/remix-simulator/src/genesis.ts +++ b/libs/remix-simulator/src/genesis.ts @@ -1,7 +1,7 @@ import { Block } from '@ethereumjs/block' import { BN } from 'ethereumjs-util' -export function generateBlock (executionContext) { +export function generateBlock (vmContext) { return new Promise((resolve, reject) => { const block: Block = Block.fromBlockData({ header: { @@ -11,10 +11,10 @@ export function generateBlock (executionContext) { difficulty: new BN('69762765929000', 10), gasLimit: new BN('8000000').imuln(1) } - }, { common: executionContext.vmObject().common }) + }, { common: vmContext.vmObject().common }) - executionContext.vm().runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false }).then(() => { - executionContext.addBlock(block) + vmContext.vm().runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false }).then(() => { + vmContext.addBlock(block) resolve({}) }).catch((e) => reject(e)) }) diff --git a/libs/remix-simulator/src/index.ts b/libs/remix-simulator/src/index.ts index fd73199808..19ca3ab802 100644 --- a/libs/remix-simulator/src/index.ts +++ b/libs/remix-simulator/src/index.ts @@ -1 +1 @@ -export { Provider } from './provider' +export { Provider, extend } from './provider' diff --git a/libs/remix-simulator/src/methods/accounts.ts b/libs/remix-simulator/src/methods/accounts.ts index 6fd359594b..8d18498afc 100644 --- a/libs/remix-simulator/src/methods/accounts.ts +++ b/libs/remix-simulator/src/methods/accounts.ts @@ -6,22 +6,20 @@ export class Accounts { web3 accounts: Record accountsKeys: Record - executionContext + vmContext - constructor (executionContext) { + constructor (vmContext) { this.web3 = new Web3() - this.executionContext = executionContext + this.vmContext = vmContext // TODO: make it random and/or use remix-libs this.accounts = {} this.accountsKeys = {} - this.executionContext.init({ get: () => { return true } }) } async resetAccounts (): Promise { - // TODO: setting this to {} breaks the app currently, unclear why still - // this.accounts = {} - // this.accountsKeys = {} + this.accounts = {} + this.accountsKeys = {} await this._addAccount('503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb', '0x56BC75E2D63100000') await this._addAccount('7e5bfb82febc4c2c8529167104271ceec190eafdca277314912eaabdb67c6e5f', '0x56BC75E2D63100000') await this._addAccount('cc6d63f85de8fef05446ebdd3c537c72152d0fc437fd7aa62b3019b79bd1fdd4', '0x56BC75E2D63100000') @@ -47,7 +45,7 @@ export class Accounts { this.accounts[addressStr] = { privateKey, nonce: 0 } this.accountsKeys[addressStr] = '0x' + privateKey.toString('hex') - const stateManager = this.executionContext.vm().stateManager + const stateManager = this.vmContext.vm().stateManager stateManager.getAccount(Address.fromString(addressStr)).then((account) => { account.balance = new BN(balance.replace('0x', '') || 'f00000000000000001', 16) stateManager.putAccount(Address.fromString(addressStr), account).catch((error) => { @@ -85,7 +83,7 @@ export class Accounts { eth_getBalance (payload, cb) { const address = payload.params[0] - this.executionContext.vm().stateManager.getAccount(Address.fromString(address)).then((account) => { + this.vmContext.vm().stateManager.getAccount(Address.fromString(address)).then((account) => { cb(null, new BN(account.balance).toString(10)) }).catch((error) => { cb(error) diff --git a/libs/remix-simulator/src/methods/blocks.ts b/libs/remix-simulator/src/methods/blocks.ts index bfed840284..7ac7bc179f 100644 --- a/libs/remix-simulator/src/methods/blocks.ts +++ b/libs/remix-simulator/src/methods/blocks.ts @@ -1,10 +1,10 @@ export class Blocks { - executionContext + vmContext coinbase: string blockNumber: number - constructor (executionContext, _options) { - this.executionContext = executionContext + constructor (vmContext, _options) { + this.vmContext = vmContext const options = _options || {} this.coinbase = options.coinbase || '0x0000000000000000000000000000000000000000' this.blockNumber = 0 @@ -28,13 +28,13 @@ export class Blocks { eth_getBlockByNumber (payload, cb) { let blockIndex = payload.params[0] if (blockIndex === 'latest') { - blockIndex = this.executionContext.latestBlockNumber + blockIndex = this.vmContext.latestBlockNumber } if (Number.isInteger(blockIndex)) { blockIndex = '0x' + blockIndex.toString(16) } - const block = this.executionContext.blocks[blockIndex] + const block = this.vmContext.blocks[blockIndex] if (!block) { return cb(new Error('block not found')) @@ -70,7 +70,7 @@ export class Blocks { } eth_getBlockByHash (payload, cb) { - const block = this.executionContext.blocks[payload.params[0]] + const block = this.vmContext.blocks[payload.params[0]] const b = { number: this.toHex(block.header.number), @@ -109,13 +109,13 @@ export class Blocks { } eth_getBlockTransactionCountByHash (payload, cb) { - const block = this.executionContext.blocks[payload.params[0]] + const block = this.vmContext.blocks[payload.params[0]] cb(null, block.transactions.length) } eth_getBlockTransactionCountByNumber (payload, cb) { - const block = this.executionContext.blocks[payload.params[0]] + const block = this.vmContext.blocks[payload.params[0]] cb(null, block.transactions.length) } @@ -131,7 +131,7 @@ export class Blocks { eth_getStorageAt (payload, cb) { const [address, position, blockNumber] = payload.params - this.executionContext.web3().debug.storageRangeAt(blockNumber, 'latest', address.toLowerCase(), position, 1, (err, result) => { + this.vmContext.web3().debug.storageRangeAt(blockNumber, 'latest', address.toLowerCase(), position, 1, (err, result) => { if (err || (result.storage && Object.values(result.storage).length === 0)) { return cb(err, '') } diff --git a/libs/remix-simulator/src/methods/debug.ts b/libs/remix-simulator/src/methods/debug.ts index ae7f3067b8..b9bf886e50 100644 --- a/libs/remix-simulator/src/methods/debug.ts +++ b/libs/remix-simulator/src/methods/debug.ts @@ -1,8 +1,8 @@ export class Debug { - executionContext + vmContext - constructor (executionContext) { - this.executionContext = executionContext + constructor (vmContext) { + this.vmContext = vmContext } methods () { @@ -14,15 +14,15 @@ export class Debug { } debug_traceTransaction (payload, cb) { - this.executionContext.web3().debug.traceTransaction(payload.params[0], {}, cb) + this.vmContext.web3().debug.traceTransaction(payload.params[0], {}, cb) } debug_preimage (payload, cb) { - this.executionContext.web3().debug.preimage(payload.params[0], cb) + this.vmContext.web3().debug.preimage(payload.params[0], cb) } debug_storageRangeAt (payload, cb) { - this.executionContext.web3().debug.storageRangeAt( + this.vmContext.web3().debug.storageRangeAt( payload.params[0], payload.params[1], payload.params[2], diff --git a/libs/remix-simulator/src/methods/filters.ts b/libs/remix-simulator/src/methods/filters.ts index c27cc49bd6..43404a1c31 100644 --- a/libs/remix-simulator/src/methods/filters.ts +++ b/libs/remix-simulator/src/methods/filters.ts @@ -1,8 +1,8 @@ export class Filters { - executionContext + vmContext - constructor (executionContext) { - this.executionContext = executionContext + constructor (vmContext) { + this.vmContext = vmContext } methods () { @@ -14,49 +14,49 @@ export class Filters { } eth_getLogs (payload, cb) { - const results = this.executionContext.logsManager.getLogsFor(payload.params[0]) + const results = this.vmContext.logsManager.getLogsFor(payload.params[0]) cb(null, results) } eth_subscribe (payload, cb) { - const subscriptionId = this.executionContext.logsManager.subscribe(payload.params) + const subscriptionId = this.vmContext.logsManager.subscribe(payload.params) cb(null, subscriptionId) } eth_unsubscribe (payload, cb) { - this.executionContext.logsManager.unsubscribe(payload.params[0]) + this.vmContext.logsManager.unsubscribe(payload.params[0]) cb(null, true) } eth_newFilter (payload, cb) { - const filterId = this.executionContext.logsManager.newFilter('filter', payload.params[0]) + const filterId = this.vmContext.logsManager.newFilter('filter', payload.params[0]) cb(null, filterId) } eth_newBlockFilter (payload, cb) { - const filterId = this.executionContext.logsManager.newFilter('block') + const filterId = this.vmContext.logsManager.newFilter('block') cb(null, filterId) } eth_newPendingTransactionFilter (payload, cb) { - const filterId = this.executionContext.logsManager.newFilter('pendingTransactions') + const filterId = this.vmContext.logsManager.newFilter('pendingTransactions') cb(null, filterId) } eth_uninstallfilter (payload, cb) { - const result = this.executionContext.logsManager.uninstallFilter(payload.params[0]) + const result = this.vmContext.logsManager.uninstallFilter(payload.params[0]) cb(null, result) } eth_getFilterChanges (payload, cb) { const filterId = payload.params[0] - const results = this.executionContext.logsManager.getLogsForFilter(filterId) + const results = this.vmContext.logsManager.getLogsForFilter(filterId) cb(null, results) } eth_getFilterLogs (payload, cb) { const filterId = payload.params[0] - const results = this.executionContext.logsManager.getLogsForFilter(filterId, true) + const results = this.vmContext.logsManager.getLogsForFilter(filterId, true) cb(null, results) } } diff --git a/libs/remix-simulator/src/methods/transactions.ts b/libs/remix-simulator/src/methods/transactions.ts index f3e8ca3f43..c3a43f5bc4 100644 --- a/libs/remix-simulator/src/methods/transactions.ts +++ b/libs/remix-simulator/src/methods/transactions.ts @@ -1,17 +1,48 @@ import Web3 from 'web3' import { toChecksumAddress, BN, Address } from 'ethereumjs-util' import { processTx } from './txProcess' +import { execution } from '@remix-project/remix-lib' +const TxRunnerVM = execution.TxRunnerVM +const TxRunner = execution.TxRunner export class Transactions { - executionContext + vmContext accounts + tags + txRunnerVMInstance + txRunnerInstance - constructor (executionContext) { - this.executionContext = executionContext + constructor (vmContext) { + this.vmContext = vmContext + this.tags = {} } init (accounts) { this.accounts = accounts + const api = { + logMessage: (msg) => { + }, + logHtmlMessage: (msg) => { + }, + config: { + getUnpersistedProperty: (key) => { + return true + }, + get: () => { + return true + } + }, + detectNetwork: (cb) => { + cb() + }, + personalMode: () => { + return false + } + } + + this.txRunnerVMInstance = new TxRunnerVM(accounts, api, _ => this.vmContext.vmObject()) + this.txRunnerInstance = new TxRunner(this.txRunnerVMInstance, { runAsync: false }) + this.txRunnerInstance.vmaccounts = accounts } methods () { @@ -24,7 +55,9 @@ export class Transactions { eth_getTransactionCount: this.eth_getTransactionCount.bind(this), eth_getTransactionByHash: this.eth_getTransactionByHash.bind(this), eth_getTransactionByBlockHashAndIndex: this.eth_getTransactionByBlockHashAndIndex.bind(this), - eth_getTransactionByBlockNumberAndIndex: this.eth_getTransactionByBlockNumberAndIndex.bind(this) + eth_getTransactionByBlockNumberAndIndex: this.eth_getTransactionByBlockNumberAndIndex.bind(this), + eth_getExecutionResultFromSimulator: this.eth_getExecutionResultFromSimulator.bind(this), + eth_getHashFromTagBySimulator: this.eth_getHashFromTagBySimulator.bind(this) } } @@ -33,16 +66,30 @@ export class Transactions { if (payload.params && payload.params.length > 0 && payload.params[0].from) { payload.params[0].from = toChecksumAddress(payload.params[0].from) } - processTx(this.executionContext, this.accounts, payload, false, cb) + processTx(this.txRunnerInstance, payload, false, (error, result) => { + if (!error && result) { + this.vmContext.addBlock(result.block) + const hash = '0x' + result.tx.hash().toString('hex') + this.vmContext.trackTx(hash, result.block) + this.vmContext.trackExecResult(hash, result.result.execResult) + return cb(null, result.transactionHash) + } + cb(error) + }) + } + + eth_getExecutionResultFromSimulator (payload, cb) { + const txHash = payload.params[0] + cb(null, this.vmContext.exeResults[txHash]) } eth_getTransactionReceipt (payload, cb) { - this.executionContext.web3().eth.getTransactionReceipt(payload.params[0], (error, receipt) => { + this.vmContext.web3().eth.getTransactionReceipt(payload.params[0], (error, receipt) => { if (error) { return cb(error) } - const txBlock = this.executionContext.txs[receipt.hash] + const txBlock = this.vmContext.txs[receipt.hash] const r: Record = { transactionHash: receipt.hash, @@ -72,7 +119,7 @@ export class Transactions { eth_getCode (payload, cb) { const address = payload.params[0] - this.executionContext.web3().eth.getCode(address, (error, result) => { + this.vmContext.web3().eth.getCode(address, (error, result) => { if (error) { console.dir('error getting code') console.dir(error) @@ -92,13 +139,31 @@ export class Transactions { payload.params[0].value = undefined - processTx(this.executionContext, this.accounts, payload, true, cb) + const tag = payload.params[0].timestamp // e2e reference + + processTx(this.txRunnerInstance, payload, true, (error, result) => { + if (!error && result) { + this.vmContext.addBlock(result.block) + const hash = '0x' + result.tx.hash().toString('hex') + this.vmContext.trackTx(hash, result.block) + this.vmContext.trackExecResult(hash, result.result.execResult) + this.tags[tag] = result.transactionHash + // calls are not supposed to return a transaction hash. we do this for keeping track of it and allowing debugging calls. + const returnValue = `0x${result.result.execResult.returnValue.toString('hex') || '0'}` + return cb(null, returnValue) + } + cb(error) + }) + } + + eth_getHashFromTagBySimulator (payload, cb) { + return cb(null, this.tags[payload.params[0]]) } eth_getTransactionCount (payload, cb) { const address = payload.params[0] - this.executionContext.vm().stateManager.getAccount(Address.fromString(address)).then((account) => { + this.vmContext.vm().stateManager.getAccount(Address.fromString(address)).then((account) => { const nonce = new BN(account.nonce).toString(10) cb(null, nonce) }).catch((error) => { @@ -109,12 +174,12 @@ export class Transactions { eth_getTransactionByHash (payload, cb) { const address = payload.params[0] - this.executionContext.web3().eth.getTransactionReceipt(address, (error, receipt) => { + this.vmContext.web3().eth.getTransactionReceipt(address, (error, receipt) => { if (error) { return cb(error) } - const txBlock = this.executionContext.txs[receipt.transactionHash] + const txBlock = this.vmContext.txs[receipt.transactionHash] // TODO: params to add later const r: Record = { @@ -154,10 +219,10 @@ export class Transactions { eth_getTransactionByBlockHashAndIndex (payload, cb) { const txIndex = payload.params[1] - const txBlock = this.executionContext.blocks[payload.params[0]] + const txBlock = this.vmContext.blocks[payload.params[0]] const txHash = '0x' + txBlock.transactions[Web3.utils.toDecimal(txIndex)].hash().toString('hex') - this.executionContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => { + this.vmContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => { if (error) { return cb(error) } @@ -196,10 +261,10 @@ export class Transactions { eth_getTransactionByBlockNumberAndIndex (payload, cb) { const txIndex = payload.params[1] - const txBlock = this.executionContext.blocks[payload.params[0]] + const txBlock = this.vmContext.blocks[payload.params[0]] const txHash = '0x' + txBlock.transactions[Web3.utils.toDecimal(txIndex)].hash().toString('hex') - this.executionContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => { + this.vmContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => { if (error) { return cb(error) } diff --git a/libs/remix-simulator/src/methods/txProcess.ts b/libs/remix-simulator/src/methods/txProcess.ts index 8f8acb5b2f..75173af309 100644 --- a/libs/remix-simulator/src/methods/txProcess.ts +++ b/libs/remix-simulator/src/methods/txProcess.ts @@ -1,15 +1,12 @@ import { execution } from '@remix-project/remix-lib' const TxExecution = execution.txExecution -const TxRunner = execution.txRunner function runCall (payload, from, to, data, value, gasLimit, txRunner, callbacks, callback) { const finalCallback = function (err, result) { if (err) { return callback(err) } - const returnValue = result.result.execResult.returnValue.toString('hex') - const toReturn = `0x${returnValue || '0'}` - return callback(null, toReturn) + return callback(null, result) } TxExecution.callFunction(from, to, data, value, gasLimit, { constant: true }, txRunner, callbacks, finalCallback) @@ -20,7 +17,7 @@ function runTx (payload, from, to, data, value, gasLimit, txRunner, callbacks, c if (err) { return callback(err) } - callback(null, result.transactionHash) + callback(null, result) } TxExecution.callFunction(from, to, data, value, gasLimit, { constant: false }, txRunner, callbacks, finalCallback) @@ -31,43 +28,13 @@ function createContract (payload, from, data, value, gasLimit, txRunner, callbac if (err) { return callback(err) } - callback(null, result.transactionHash) + callback(null, result) } TxExecution.createContract(from, data, value, gasLimit, txRunner, callbacks, finalCallback) } -let txRunnerInstance - -export function processTx (executionContext, accounts, payload, isCall, callback) { - const api = { - logMessage: (msg) => { - }, - logHtmlMessage: (msg) => { - }, - config: { - getUnpersistedProperty: (key) => { - return true - }, - get: () => { - return true - } - }, - detectNetwork: (cb) => { - cb() - }, - personalMode: () => { - return false - } - } - - executionContext.init(api.config) - - // let txRunner = new TxRunner(accounts, api) - if (!txRunnerInstance) { - txRunnerInstance = new TxRunner(accounts, api, executionContext) - } - txRunnerInstance.vmaccounts = accounts +export function processTx (txRunnerInstance, payload, isCall, callback) { let { from, to, data, value, gas } = payload.params[0] gas = gas || 3000000 diff --git a/libs/remix-simulator/src/provider.ts b/libs/remix-simulator/src/provider.ts index 8fdb5b5aba..276b66ca15 100644 --- a/libs/remix-simulator/src/provider.ts +++ b/libs/remix-simulator/src/provider.ts @@ -1,5 +1,4 @@ import { Blocks } from './methods/blocks' -import { execution } from '@remix-project/remix-lib' import { info } from './utils/logs' import merge from 'merge' @@ -11,11 +10,11 @@ import { methods as netMethods } from './methods/net' import { Transactions } from './methods/transactions' import { Debug } from './methods/debug' import { generateBlock } from './genesis' -const { executionContext } = execution +import { VMContext } from './vm-context' export class Provider { options: Record - executionContext + vmContext Accounts Transactions methods @@ -26,23 +25,23 @@ export class Provider { this.options = options this.host = host this.connected = true - // TODO: init executionContext here - this.executionContext = executionContext - this.Accounts = new Accounts(this.executionContext) - this.Transactions = new Transactions(this.executionContext) + this.vmContext = new VMContext() + + this.Accounts = new Accounts(this.vmContext) + this.Transactions = new Transactions(this.vmContext) this.methods = {} this.methods = merge(this.methods, this.Accounts.methods()) - this.methods = merge(this.methods, (new Blocks(this.executionContext, options)).methods()) + this.methods = merge(this.methods, (new Blocks(this.vmContext, options)).methods()) this.methods = merge(this.methods, miscMethods()) - this.methods = merge(this.methods, (new Filters(this.executionContext)).methods()) + this.methods = merge(this.methods, (new Filters(this.vmContext)).methods()) this.methods = merge(this.methods, netMethods()) this.methods = merge(this.methods, this.Transactions.methods()) - this.methods = merge(this.methods, (new Debug(this.executionContext)).methods()) + this.methods = merge(this.methods, (new Debug(this.vmContext)).methods()) } async init () { - await generateBlock(this.executionContext) + await generateBlock(this.vmContext) await this.Accounts.resetAccounts() this.Transactions.init(this.Accounts.accounts) } @@ -87,6 +86,39 @@ export class Provider { }; on (type, cb) { - this.executionContext.logsManager.addListener(type, cb) + this.vmContext.logsManager.addListener(type, cb) + } +} + +export function extend (web3) { + if (!web3.extend) { + return + } + // DEBUG + const methods = [] + if (!(web3.eth && web3.eth.getExecutionResultFromSimulator)) { + methods.push(new web3.extend.Method({ + name: 'getExecutionResultFromSimulator', + call: 'eth_getExecutionResultFromSimulator', + inputFormatter: [null], + params: 1 + })) + } + + if (!(web3.eth && web3.eth.getHashFromTagBySimulator)) { + methods.push(new web3.extend.Method({ + name: 'getHashFromTagBySimulator', + call: 'eth_getHashFromTagBySimulator', + inputFormatter: [null], + params: 1 + })) + } + + if (methods.length > 0) { + web3.extend({ + property: 'eth', + methods: methods, + properties: [] + }) } } diff --git a/libs/remix-simulator/src/vm-context.ts b/libs/remix-simulator/src/vm-context.ts new file mode 100644 index 0000000000..a72be89d1e --- /dev/null +++ b/libs/remix-simulator/src/vm-context.ts @@ -0,0 +1,169 @@ +/* global ethereum */ +'use strict' +import Web3 from 'web3' +import { rlp, keccak, bufferToHex } from 'ethereumjs-util' +import { vm as remixLibVm, execution } from '@remix-project/remix-lib' +import VM from '@ethereumjs/vm' +import Common from '@ethereumjs/common' +import StateManager from '@ethereumjs/vm/dist/state/stateManager' +import { StorageDump } from '@ethereumjs/vm/dist/state/interface' + +/* + extend vm state manager and instanciate VM +*/ + +class StateManagerCommonStorageDump extends StateManager { + keyHashes: { [key: string]: string } + constructor () { + super() + this.keyHashes = {} + } + + putContractStorage (address, key, value) { + this.keyHashes[keccak(key).toString('hex')] = bufferToHex(key) + return super.putContractStorage(address, key, value) + } + + async dumpStorage (address) { + let trie + try { + trie = await this._getStorageTrie(address) + } catch (e) { + console.log(e) + throw e + } + return new Promise((resolve, reject) => { + try { + const storage = {} + const stream = trie.createReadStream() + stream.on('data', (val) => { + const value = rlp.decode(val.value) + storage['0x' + val.key.toString('hex')] = { + key: this.keyHashes[val.key.toString('hex')], + value: '0x' + value.toString('hex') + } + }) + stream.on('end', function () { + resolve(storage) + }) + } catch (e) { + reject(e) + } + }) + } + + async getStateRoot (force = false) { + await this._cache.flush() + + const stateRoot = this._trie.root + return stateRoot + } + + async setStateRoot (stateRoot) { + await this._cache.flush() + + if (stateRoot === this._trie.EMPTY_TRIE_ROOT) { + this._trie.root = stateRoot + this._cache.clear() + this._storageTries = {} + return + } + + const hasRoot = await this._trie.checkRoot(stateRoot) + if (!hasRoot) { + throw new Error('State trie does not contain state root') + } + + this._trie.root = stateRoot + this._cache.clear() + this._storageTries = {} + } +} + +/* + trigger contextChanged, web3EndpointChanged +*/ +export class VMContext { + currentFork: string + blockGasLimitDefault: number + blockGasLimit: number + customNetWorks + blocks + latestBlockNumber + txs + vms + web3vm + logsManager + exeResults + + constructor () { + this.blockGasLimitDefault = 4300000 + this.blockGasLimit = this.blockGasLimitDefault + this.currentFork = 'berlin' + this.vms = { + /* + byzantium: createVm('byzantium'), + constantinople: createVm('constantinople'), + petersburg: createVm('petersburg'), + istanbul: createVm('istanbul'), + */ + berlin: this.createVm('berlin') + } + this.blocks = {} + this.latestBlockNumber = 0 + this.txs = {} + this.exeResults = {} + this.logsManager = new execution.LogsManager() + } + + createVm (hardfork) { + const stateManager = new StateManagerCommonStorageDump() + const common = new Common({ chain: 'mainnet', hardfork }) + const vm = new VM({ + common, + activatePrecompiles: true, + stateManager: stateManager + }) + + const web3vm = new remixLibVm.Web3VMProvider() + web3vm.setVM(vm) + return { vm, web3vm, stateManager, common } + } + + web3 () { + return this.vms[this.currentFork].web3vm + } + + blankWeb3 () { + return new Web3() + } + + vm () { + return this.vms[this.currentFork].vm + } + + vmObject () { + return this.vms[this.currentFork] + } + + addBlock (block) { + let blockNumber = '0x' + block.header.number.toString('hex') + if (blockNumber === '0x') { + blockNumber = '0x0' + } + + this.blocks['0x' + block.hash().toString('hex')] = block + this.blocks[blockNumber] = block + this.latestBlockNumber = blockNumber + + this.logsManager.checkBlock(blockNumber, block, this.web3()) + } + + trackTx (tx, block) { + this.txs[tx] = block + } + + trackExecResult (tx, execReult) { + this.exeResults[tx] = execReult + } +} diff --git a/libs/remix-solidity/package.json b/libs/remix-solidity/package.json index ddb834e951..7a54cfe8d7 100644 --- a/libs/remix-solidity/package.json +++ b/libs/remix-solidity/package.json @@ -15,7 +15,7 @@ } ], "dependencies": { - "@remix-project/remix-lib": "^0.4.34", + "@remix-project/remix-lib": "../remix-lib", "eslint-scope": "^5.0.0", "@ethereumjs/vm": "^5.3.2", "@ethereumjs/block": "^3.2.1", diff --git a/libs/remix-tests/jest.config.js b/libs/remix-tests/jest.config.js index 7923ddf044..0f08d413b7 100644 --- a/libs/remix-tests/jest.config.js +++ b/libs/remix-tests/jest.config.js @@ -6,6 +6,7 @@ module.exports = { transform: { '^.+\\.[tj]sx?$': 'ts-jest', }, + transformIgnorePatterns: ["/node_modules/", "\\.pnp\\.[^\\\/]+$"], rootDir: "./", testTimeout: 40000, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html', 'json'], @@ -18,6 +19,6 @@ module.exports = { "!src/types.ts", "!src/logger.ts" ], - coverageDirectory: '../../coverage/libs/remix-tests', + coverageDirectory: '../../coverage/libs/remix-tests' }; \ No newline at end of file diff --git a/libs/remix-tests/package.json b/libs/remix-tests/package.json index a2480bfa46..9adf364e2d 100644 --- a/libs/remix-tests/package.json +++ b/libs/remix-tests/package.json @@ -35,9 +35,9 @@ }, "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-tests#readme", "dependencies": { - "@remix-project/remix-lib": "^0.4.34", - "@remix-project/remix-simulator": "^0.1.10-beta.0", - "@remix-project/remix-solidity": "^0.3.35", + "@remix-project/remix-lib": "../remix-lib", + "@remix-project/remix-simulator": "../remix-simulator", + "@remix-project/remix-solidity": "../remix-solidity", "ansi-gray": "^0.1.1", "async": "^2.6.0", "axios": ">=0.21.1", diff --git a/libs/remix-tests/src/deployer.ts b/libs/remix-tests/src/deployer.ts index c6e00456fa..1e3fcb8d88 100644 --- a/libs/remix-tests/src/deployer.ts +++ b/libs/remix-tests/src/deployer.ts @@ -79,7 +79,7 @@ export function deployAll (compileResult: compilationInterface, web3: Web3, with contracts[contractName] = contractObject contracts[contractName].filename = filename - callback(null, { result: { createdAddress: receipt.contractAddress } }) // TODO this will only work with JavaScriptV VM + callback(null, { receipt: { contractAddress: receipt.contractAddress } }) // TODO this will only work with JavaScriptV VM }).on('error', function (err) { console.error(err) callback(err) diff --git a/libs/remix-tests/tests/testRunner.cli.spec.ts b/libs/remix-tests/tests/testRunner.cli.spec.ts index 3d906095f3..91174355af 100644 --- a/libs/remix-tests/tests/testRunner.cli.spec.ts +++ b/libs/remix-tests/tests/testRunner.cli.spec.ts @@ -3,13 +3,16 @@ import { resolve } from 'path' describe('testRunner: remix-tests CLI', () => { // remix-tests binary, after build, is used as executable + const executablePath = resolve(__dirname + '/../../../dist/libs/remix-tests/bin/remix-tests') + const result = spawnSync('ls', { cwd: resolve(__dirname + '/../../../dist/libs/remix-tests') }) if(result) { const dirContent = result.stdout.toString() // Install dependencies if 'node_modules' is not already present if(!dirContent.includes('node_modules')) execSync('npm install', { cwd: resolve(__dirname + '/../../../dist/libs/remix-tests') }) } + describe('test various CLI options', () => { test('remix-tests version', () => { diff --git a/libs/remix-tests/tsconfig.json b/libs/remix-tests/tsconfig.json index 4422abf942..cf7076c1d1 100644 --- a/libs/remix-tests/tsconfig.json +++ b/libs/remix-tests/tsconfig.json @@ -3,9 +3,9 @@ "compilerOptions": { "types": ["node", "jest"], "module": "commonjs", + "esModuleInterop": true, "allowJs": true, "rootDir": "./", - "esModuleInterop": true }, "include": ["**/*.ts"] } \ No newline at end of file diff --git a/libs/remix-tests/tsconfig.lib.json b/libs/remix-tests/tsconfig.lib.json index ac117282b1..7b4b23c716 100644 --- a/libs/remix-tests/tsconfig.lib.json +++ b/libs/remix-tests/tsconfig.lib.json @@ -1,16 +1,15 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "commonjs", - "outDir": "../../dist/out-tsc", - "declaration": true, - "rootDir": "./", - "types": ["node"] - }, - "exclude": [ - "**/*.spec.ts", - "tests/" - ], - "include": ["**/*.ts"] + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../dist/out-tsc", + "declaration": true, + "rootDir": "./", + "types": ["node"] + }, + "exclude": [ + "**/*.spec.ts", + "tests/" + ], + "include": ["**/*.ts"] } - \ No newline at end of file diff --git a/libs/remix-tests/tsconfig.spec.json b/libs/remix-tests/tsconfig.spec.json index 118a64f08e..559410b96a 100644 --- a/libs/remix-tests/tsconfig.spec.json +++ b/libs/remix-tests/tsconfig.spec.json @@ -1,16 +1,15 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "**/*.spec.ts", - "**/*.spec.tsx", - "**/*.spec.js", - "**/*.spec.jsx", - "**/*.d.ts" - ] - } - \ No newline at end of file + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts b/libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts index 8e3bc9f1c4..8a397cfd91 100644 --- a/libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts +++ b/libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts @@ -181,35 +181,35 @@ export const fileRenamedSuccess = (path: string, removePath: string, files) => { export const init = (provider, workspaceName: string, plugin, registry) => (dispatch: React.Dispatch) => { if (provider) { - provider.event.register('fileAdded', async (filePath) => { + provider.event.on('fileAdded', async (filePath) => { if (extractParentFromKey(filePath) === '/.workspaces') return const path = extractParentFromKey(filePath) || provider.workspace || provider.type || '' const data = await fetchDirectoryContent(provider, path) dispatch(fileAddedSuccess(path, data)) if (filePath.includes('_test.sol')) { - plugin.event.trigger('newTestFileCreated', [filePath]) + plugin.emit('newTestFileCreated', filePath) } }) - provider.event.register('folderAdded', async (folderPath) => { + provider.event.on('folderAdded', async (folderPath) => { if (extractParentFromKey(folderPath) === '/.workspaces') return const path = extractParentFromKey(folderPath) || provider.workspace || provider.type || '' const data = await fetchDirectoryContent(provider, path) dispatch(folderAddedSuccess(path, data)) }) - provider.event.register('fileRemoved', async (removePath) => { + provider.event.on('fileRemoved', async (removePath) => { const path = extractParentFromKey(removePath) || provider.workspace || provider.type || '' dispatch(fileRemovedSuccess(path, removePath)) }) - provider.event.register('fileRenamed', async (oldPath) => { + provider.event.on('fileRenamed', async (oldPath) => { const path = extractParentFromKey(oldPath) || provider.workspace || provider.type || '' const data = await fetchDirectoryContent(provider, path) dispatch(fileRenamedSuccess(path, oldPath, data)) }) - provider.event.register('fileExternallyChanged', async (path: string, file: { content: string }) => { + provider.event.on('fileExternallyChanged', async (path: string, file: { content: string }) => { const config = registry.get('config').api const editor = registry.get('editor').api @@ -225,10 +225,10 @@ export const init = (provider, workspaceName: string, plugin, registry) => (disp )) } }) - provider.event.register('fileRenamedError', async () => { + provider.event.on('fileRenamedError', async () => { dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel')) }) - provider.event.register('rootFolderChanged', async () => { + provider.event.on('rootFolderChanged', async () => { workspaceName = provider.workspace || provider.type || '' fetchDirectory(provider, workspaceName)(dispatch) }) diff --git a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx index fc8c89450a..c471449ac4 100644 --- a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx +++ b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx @@ -88,20 +88,17 @@ export const FileExplorer = (props: FileExplorerProps) => { hide: true, title: '', message: '', - ok: { - label: '', - fn: () => {} - }, - cancel: { - label: '', - fn: () => {} - }, + okLabel: '', + okFn: () => {}, + cancelLabel: '', + cancelFn: () => {}, handleHide: null }, modals: [], toasterMsg: '', mouseOverElement: null, - showContextMenu: false + showContextMenu: false, + reservedKeywords: [name, 'gist-'] }) const [fileSystem, dispatch] = useReducer(fileSystemReducer, fileSystemInitialState) const editRef = useRef(null) @@ -122,13 +119,7 @@ export const FileExplorer = (props: FileExplorerProps) => { useEffect(() => { if (fileSystem.notification.message) { - modal(fileSystem.notification.title, fileSystem.notification.message, { - label: fileSystem.notification.labelOk, - fn: fileSystem.notification.actionOk - }, { - label: fileSystem.notification.labelCancel, - fn: fileSystem.notification.actionCancel - }) + modal(fileSystem.notification.title, fileSystem.notification.message, fileSystem.notification.labelOk, fileSystem.notification.actionOk, fileSystem.notification.labelCancel, fileSystem.notification.actionCancel) } }, [fileSystem.notification.message]) @@ -201,8 +192,10 @@ export const FileExplorer = (props: FileExplorerProps) => { hide: false, title: prevState.modals[0].title, message: prevState.modals[0].message, - ok: prevState.modals[0].ok, - cancel: prevState.modals[0].cancel, + okLabel: prevState.modals[0].okLabel, + okFn: prevState.modals[0].okFn, + cancelLabel: prevState.modals[0].cancelLabel, + cancelFn: prevState.modals[0].cancelFn, handleHide: prevState.modals[0].handleHide } @@ -230,6 +223,11 @@ export const FileExplorer = (props: FileExplorerProps) => { return keyPath.join('/') } + const hasReservedKeyword = (content: string): boolean => { + if (state.reservedKeywords.findIndex(value => content.startsWith(value)) !== -1) return true + else return false + } + const createNewFile = async (newFilePath: string) => { const fileManager = state.fileManager @@ -248,10 +246,7 @@ export const FileExplorer = (props: FileExplorerProps) => { }) } } catch (error) { - return modal('File Creation Failed', typeof error === 'string' ? error : error.message, { - label: 'Close', - fn: async () => {} - }, null) + return modal('File Creation Failed', typeof error === 'string' ? error : error.message, 'Close', async () => {}) } } @@ -263,20 +258,14 @@ export const FileExplorer = (props: FileExplorerProps) => { const exists = await fileManager.exists(dirName) if (exists) { - return modal('Rename File Failed', `A file or folder ${extractNameFromKey(newFolderPath)} already exists at this location. Please choose a different name.`, { - label: 'Close', - fn: () => {} - }, null) + return modal('Rename File Failed', `A file or folder ${extractNameFromKey(newFolderPath)} already exists at this location. Please choose a different name.`, 'Close', () => {}) } await fileManager.mkdir(dirName) setState(prevState => { return { ...prevState, focusElement: [{ key: newFolderPath, type: 'folder' }] } }) } catch (e) { - return modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, { - label: 'Close', - fn: async () => {} - }, null) + return modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, 'Close', async () => {}) } } @@ -288,21 +277,15 @@ export const FileExplorer = (props: FileExplorerProps) => { } const isDir = state.fileManager.isDirectory(path) - modal(`Delete ${isDir ? 'folder' : 'file'}`, `Are you sure you want to delete ${path} ${isDir ? 'folder' : 'file'}?`, { - label: 'OK', - fn: async () => { - try { - const fileManager = state.fileManager + modal(`Delete ${isDir ? 'folder' : 'file'}`, `Are you sure you want to delete ${path} ${isDir ? 'folder' : 'file'}?`, 'OK', async () => { + try { + const fileManager = state.fileManager - await fileManager.remove(path) - } catch (e) { - toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${path}.`) - } + await fileManager.remove(path) + } catch (e) { + toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${path}.`) } - }, { - label: 'Cancel', - fn: () => {} - }) + }, 'Cancel', () => {}) } const renamePath = async (oldPath: string, newPath: string) => { @@ -311,18 +294,12 @@ export const FileExplorer = (props: FileExplorerProps) => { const exists = await fileManager.exists(newPath) if (exists) { - modal('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, { - label: 'Close', - fn: () => {} - }, null) + modal('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', () => {}) } else { await fileManager.rename(oldPath, newPath) } } catch (error) { - modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, { - label: 'Close', - fn: async () => {} - }, null) + modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {}) } } @@ -345,19 +322,13 @@ export const FileExplorer = (props: FileExplorerProps) => { fileReader.onload = async function (event) { if (helper.checkSpecialChars(file.name)) { - modal('File Upload Failed', 'Special characters are not allowed', { - label: 'Close', - fn: async () => {} - }, null) + modal('File Upload Failed', 'Special characters are not allowed', 'Close', async () => {}) return } const success = await filesProvider.set(name, event.target.result) if (!success) { - return modal('File Upload Failed', 'Failed to create file ' + name, { - label: 'Close', - fn: async () => {} - }, null) + return modal('File Upload Failed', 'Failed to create file ' + name, 'Close', async () => {}) } const config = registry.get('config').api const editor = registry.get('editor').api @@ -374,15 +345,9 @@ export const FileExplorer = (props: FileExplorerProps) => { if (!exist) { loadFile(name) } else { - modal('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, { - label: 'OK', - fn: () => { - loadFile(name) - } - }, { - label: 'Cancel', - fn: () => {} - }) + modal('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, 'OK', () => { + loadFile(name) + }, 'Cancel', () => {}) } }).catch(error => { if (error) console.log(error) @@ -391,41 +356,23 @@ export const FileExplorer = (props: FileExplorerProps) => { } const publishToGist = () => { - modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, { - label: 'OK', - fn: toGist - }, { - label: 'Cancel', - fn: () => {} - }) + modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, 'OK', toGist, 'Cancel', () => {}) } const toGist = (id?: string) => { const filesProvider = fileSystem.provider.provider const proccedResult = function (error, data) { if (error) { - modal('Publish to gist Failed', 'Failed to manage gist: ' + error, { - label: 'Close', - fn: async () => {} - }, null) + modal('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', () => {}) } else { if (data.html_url) { - modal('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, { - label: 'OK', - fn: () => { - window.open(data.html_url, '_blank') - } - }, { - label: 'Cancel', - fn: () => {} - }) + modal('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', () => { + window.open(data.html_url, '_blank') + }, 'Cancel', () => {}) } else { const error = JSON.stringify(data.errors, null, '\t') || '' const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message - modal('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, { - label: 'Close', - fn: async () => {} - }, null) + modal('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', () => {}) } } } @@ -451,20 +398,14 @@ export const FileExplorer = (props: FileExplorerProps) => { packageFiles(filesProvider, folder, async (error, packaged) => { if (error) { console.log(error) - modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, { - label: 'Close', - fn: async () => {} - }, null) + modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', async () => {}) } else { // check for token const config = registry.get('config').api const accessToken = config.get('settings/gist-access-token') if (!accessToken) { - modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', { - label: 'Close', - fn: async () => {} - }, null) + modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', () => {}) } else { const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' + queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist=' @@ -536,7 +477,7 @@ export const FileExplorer = (props: FileExplorerProps) => { }) } - const modal = (title: string, message: string, ok: { label: string, fn: () => void }, cancel: { label: string, fn: () => void }) => { + const modal = (title: string, message: string, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => { setState(prevState => { return { ...prevState, @@ -544,8 +485,10 @@ export const FileExplorer = (props: FileExplorerProps) => { { message, title, - ok, - cancel, + okLabel, + okFn, + cancelLabel, + cancelFn, handleHide: handleHideModal }] } @@ -644,21 +587,28 @@ export const FileExplorer = (props: FileExplorerProps) => { }) } if (helper.checkSpecialChars(content)) { - modal('Validation Error', 'Special characters are not allowed', { - label: 'OK', - fn: () => {} - }, null) + modal('Validation Error', 'Special characters are not allowed', 'OK', () => {}) } else { if (state.focusEdit.isNew) { - state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content)) - removeInputField(parentFolder)(dispatch) + if (hasReservedKeyword(content)) { + removeInputField(parentFolder)(dispatch) + modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {}) + } else { + state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content)) + removeInputField(parentFolder)(dispatch) + } } else { - const oldPath: string = state.focusEdit.element - const oldName = extractNameFromKey(oldPath) - const newPath = oldPath.replace(oldName, content) + if (hasReservedKeyword(content)) { + editRef.current.textContent = state.focusEdit.lastEdit + modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {}) + } else { + const oldPath: string = state.focusEdit.element + const oldName = extractNameFromKey(oldPath) + const newPath = oldPath.replace(oldName, content) - editRef.current.textContent = extractNameFromKey(oldPath) - renamePath(oldPath, newPath) + editRef.current.textContent = extractNameFromKey(oldPath) + renamePath(oldPath, newPath) + } } setState(prevState => { return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } @@ -865,8 +815,10 @@ export const FileExplorer = (props: FileExplorerProps) => { title={ state.focusModal.title } message={ state.focusModal.message } hide={ state.focusModal.hide } - ok={ state.focusModal.ok } - cancel={ state.focusModal.cancel } + okLabel={ state.focusModal.okLabel } + okFn={ state.focusModal.okFn } + cancelLabel={ state.focusModal.cancelLabel } + cancelFn={ state.focusModal.cancelFn } handleHide={ handleHideModal } /> } diff --git a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx index 2caa41219d..ec016068e9 100644 --- a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx +++ b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx @@ -18,7 +18,7 @@ export const ModalDialog = (props: ModalDialogProps) => { const modalKeyEvent = (keyCode) => { if (keyCode === 27) { // Esc - if (props.cancel && props.cancel.fn) props.cancel.fn() + if (props.cancelFn) props.cancelFn() handleHide() } else if (keyCode === 13) { // Enter enterHandler() @@ -33,9 +33,9 @@ export const ModalDialog = (props: ModalDialogProps) => { const enterHandler = () => { if (state.toggleBtn) { - if (props.ok && props.ok.fn) props.ok.fn() + if (props.okFn) props.okFn() } else { - if (props.cancel && props.cancel.fn) props.cancel.fn() + if (props.cancelFn) props.cancelFn() } handleHide() } @@ -79,29 +79,29 @@ export const ModalDialog = (props: ModalDialogProps) => {
{/* todo add autofocus ^^ */} - { props.ok && + { props.okLabel && { - if (props.ok.fn) props.ok.fn() + if (props.okFn) props.okFn() handleHide() }} > - { props.ok.label ? props.ok.label : 'OK' } + { props.okLabel ? props.okLabel : 'OK' } } - { props.cancel && + { props.cancelLabel && { - if (props.cancel.fn) props.cancel.fn() + if (props.cancelFn) props.cancelFn() handleHide() }} > - { props.cancel.label ? props.cancel.label : 'Cancel' } + { props.cancelLabel ? props.cancelLabel : 'Cancel' } }
diff --git a/libs/remix-ui/modal-dialog/src/lib/types/index.ts b/libs/remix-ui/modal-dialog/src/lib/types/index.ts index 29c4d39505..58d15cdb87 100644 --- a/libs/remix-ui/modal-dialog/src/lib/types/index.ts +++ b/libs/remix-ui/modal-dialog/src/lib/types/index.ts @@ -2,8 +2,10 @@ export interface ModalDialogProps { id?: string title?: string, message?: string, - ok?: { label: string, fn: () => void }, - cancel: { label: string, fn: () => void }, + okLabel?: string, + okFn?: () => void, + cancelLabel?: string, + cancelFn?: () => void, modalClass?: string, showCancelIcon?: boolean, hide: boolean, diff --git a/libs/remix-ui/toaster/src/lib/toaster.tsx b/libs/remix-ui/toaster/src/lib/toaster.tsx index b7573f72cc..d3cd160c5c 100644 --- a/libs/remix-ui/toaster/src/lib/toaster.tsx +++ b/libs/remix-ui/toaster/src/lib/toaster.tsx @@ -91,10 +91,8 @@ export const Toaster = (props: ToasterProps) => { <> {} - }} + cancelLabel='Close' + cancelFn={() => {}} hide={!state.showModal} handleHide={hideFullMessage} /> diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index 1baca04eb7..1c7e063aa3 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -91,25 +91,30 @@ export const Workspace = (props: WorkspaceProps) => { const localhostDisconnect = () => { if (state.currentWorkspace === LOCALHOST) setWorkspace(props.workspaces.length > 0 ? props.workspaces[0] : NO_WORKSPACE) + // This should be removed some time after refactoring: https://github.com/ethereum/remix-project/issues/1197 + else { + setWorkspace(state.currentWorkspace) // Useful to switch to last selcted workspace when remixd is disconnected + props.fileManager.setMode('browser') + } } - props.localhost.event.unregister('disconnected', localhostDisconnect) - props.localhost.event.register('disconnected', localhostDisconnect) + props.localhost.event.off('disconnected', localhostDisconnect) + props.localhost.event.on('disconnected', localhostDisconnect) useEffect(() => { - props.localhost.event.register('connected', () => { + props.localhost.event.on('connected', () => { remixdExplorer.show() setWorkspace(LOCALHOST) }) - props.localhost.event.register('disconnected', () => { + props.localhost.event.on('disconnected', () => { remixdExplorer.hide() }) - props.localhost.event.register('loading', () => { + props.localhost.event.on('loading', () => { remixdExplorer.loading() }) - props.workspace.event.register('createWorkspace', (name) => { + props.workspace.event.on('createWorkspace', (name) => { createNewWorkspace(name) }) @@ -145,14 +150,10 @@ export const Workspace = (props: WorkspaceProps) => { hide: true, title: '', message: null, - ok: { - label: '', - fn: () => {} - }, - cancel: { - label: '', - fn: () => {} - }, + okLabel: '', + okFn: () => {}, + cancelLabel: '', + cancelFn: () => {}, handleHide: null }, loadingLocalhost: false, @@ -168,41 +169,20 @@ export const Workspace = (props: WorkspaceProps) => { /* workspace creation, renaming and deletion */ const renameCurrentWorkspace = () => { - modal('Rename Current Workspace', renameModalMessage(), { - label: 'OK', - fn: onFinishRenameWorkspace - }, { - label: '', - fn: () => {} - }) + modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '', () => {}) } const createWorkspace = () => { - modal('Create Workspace', createModalMessage(), { - label: 'OK', - fn: onFinishCreateWorkspace - }, { - label: '', - fn: () => {} - }) + modal('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '', () => {}) } const deleteCurrentWorkspace = () => { - modal('Delete Current Workspace', 'Are you sure to delete the current workspace?', { - label: 'OK', - fn: onFinishDeleteWorkspace - }, { - label: '', - fn: () => {} - }) + modal('Delete Current Workspace', 'Are you sure to delete the current workspace?', 'OK', onFinishDeleteWorkspace, '', () => {}) } const modalMessage = (title: string, body: string) => { setTimeout(() => { // wait for any previous modal a chance to close - modal(title, body, { - label: 'OK', - fn: () => {} - }, null) + modal(title, body, 'OK', () => {}, '', null) }, 200) } @@ -272,11 +252,19 @@ export const Workspace = (props: WorkspaceProps) => { const remixdExplorer = { hide: async () => { - await setWorkspace(NO_WORKSPACE) - props.fileManager.setMode('browser') - setState(prevState => { - return { ...prevState, hideRemixdExplorer: true, loadingLocalhost: false } - }) + // If 'connect to localhost' is clicked from home tab, mode is not 'localhost' + if (props.fileManager.mode === 'localhost') { + await setWorkspace(NO_WORKSPACE) + props.fileManager.setMode('browser') + setState(prevState => { + return { ...prevState, hideRemixdExplorer: true, loadingLocalhost: false } + }) + } else { + // Hide spinner in file explorer + setState(prevState => { + return { ...prevState, loadingLocalhost: false } + }) + } }, show: () => { props.fileManager.setMode('localhost') @@ -297,7 +285,7 @@ export const Workspace = (props: WorkspaceProps) => { }) } - const modal = async (title: string, message: string | JSX.Element, ok: { label: string, fn: () => void }, cancel: { label: string, fn: () => void }) => { + const modal = async (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel: string, cancelFn: () => void) => { await setState(prevState => { return { ...prevState, @@ -306,8 +294,10 @@ export const Workspace = (props: WorkspaceProps) => { hide: false, message, title, - ok, - cancel, + okLabel, + okFn, + cancelLabel, + cancelFn, handleHide: handleHideModal } } @@ -339,8 +329,10 @@ export const Workspace = (props: WorkspaceProps) => { title={ state.modal.title } message={ state.modal.message } hide={ state.modal.hide } - ok={ state.modal.ok } - cancel={ state.modal.cancel } + okLabel={ state.modal.okLabel } + okFn={ state.modal.okFn } + cancelLabel={ state.modal.cancelLabel } + cancelFn={ state.modal.cancelFn } handleHide={ handleHideModal }> { (typeof state.modal.message !== 'string') && state.modal.message } diff --git a/libs/remixd/src/bin/remixd.ts b/libs/remixd/src/bin/remixd.ts index 5b7076fd0d..8d2ebe9bb2 100644 --- a/libs/remixd/src/bin/remixd.ts +++ b/libs/remixd/src/bin/remixd.ts @@ -24,16 +24,18 @@ async function warnLatestVersion () { const services = { git: (readOnly: boolean) => new servicesList.GitClient(readOnly), + hardhat: (readOnly: boolean) => new servicesList.HardhatClient(readOnly), folder: (readOnly: boolean) => new servicesList.Sharedfolder(readOnly) } const ports = { git: 65521, + hardhat: 65522, folder: 65520 } const killCallBack: Array = [] -function startService (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => void) { +function startService (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => void) { const socket = new WebSocket(ports[service], { remixIdeUrl: program.remixIde }, () => services[service](program.readOnly || false)) socket.start(callback) killCallBack.push(socket.close.bind(socket)) @@ -78,6 +80,10 @@ function startService (service: S, callback: (ws: WS sharedFolderClient.setupNotifications(program.sharedFolder) sharedFolderClient.sharedFolder(program.sharedFolder) }) + startService('hardhat', (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => { + sharedFolderClient.setWebSocket(ws) + sharedFolderClient.sharedFolder(program.sharedFolder) + }) /* startService('git', (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => { sharedFolderClient.setWebSocket(ws) diff --git a/libs/remixd/src/index.ts b/libs/remixd/src/index.ts index 04725c14f2..849f35b6fa 100644 --- a/libs/remixd/src/index.ts +++ b/libs/remixd/src/index.ts @@ -1,6 +1,7 @@ 'use strict' import { RemixdClient as sharedFolder } from './services/remixdClient' import { GitClient } from './services/gitClient' +import { HardhatClient } from './services/hardhatClient' import Websocket from './websocket' import * as utils from './utils' @@ -9,6 +10,7 @@ module.exports = { utils, services: { sharedFolder, - GitClient + GitClient, + HardhatClient } } diff --git a/libs/remixd/src/serviceList.ts b/libs/remixd/src/serviceList.ts index 5db445ee66..19d613b7c2 100644 --- a/libs/remixd/src/serviceList.ts +++ b/libs/remixd/src/serviceList.ts @@ -1,2 +1,3 @@ export { RemixdClient as Sharedfolder } from './services/remixdClient' export { GitClient } from './services/gitClient' +export { HardhatClient } from './services/hardhatClient' diff --git a/libs/remixd/src/services/hardhatClient.ts b/libs/remixd/src/services/hardhatClient.ts new file mode 100644 index 0000000000..7c2efd463a --- /dev/null +++ b/libs/remixd/src/services/hardhatClient.ts @@ -0,0 +1,49 @@ +import * as WS from 'ws' // eslint-disable-line +import { PluginClient } from '@remixproject/plugin' +const { spawn } = require('child_process') + +export class HardhatClient extends PluginClient { + methods: Array + websocket: WS + currentSharedFolder: string + + constructor (private readOnly = false) { + super() + this.methods = ['compile'] + } + + setWebSocket (websocket: WS): void { + this.websocket = websocket + } + + sharedFolder (currentSharedFolder: string): void { + this.currentSharedFolder = currentSharedFolder + } + + compile (configPath: string) { + return new Promise((resolve, reject) => { + if (this.readOnly) { + const errMsg = '[Hardhat Compilation]: Cannot compile in read-only mode' + console.log('\x1b[31m%s\x1b[0m', `${errMsg}`) + return reject(new Error(errMsg)) + } + const cmd = `npx hardhat compile --config ${configPath}` + const options = { cwd: this.currentSharedFolder, shell: true } + const child = spawn(cmd, options) + let result = '' + let error = '' + child.stdout.on('data', (data) => { + console.log('\x1b[32m%s\x1b[0m', `[Hardhat Compilation]: ${data.toString()}`) + result += data.toString() + }) + child.stderr.on('data', (err) => { + console.log('\x1b[31m%s\x1b[0m', `[Hardhat Compilation]: ${err.toString()}`) + error += err.toString() + }) + child.on('close', () => { + if (error) reject(error) + else resolve(result) + }) + }) + } +}