Merge branch 'master' into totestexternals

totestexternals
Liana Husikyan 4 years ago committed by GitHub
commit 185c280709
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/debugger/src/index.html
  2. 6
      apps/remix-ide-e2e/src/commands/addFile.ts
  3. 9
      apps/remix-ide-e2e/src/helpers/init.ts
  4. 4
      apps/remix-ide-e2e/src/tests/debugger.spec.ts
  5. 12
      apps/remix-ide-e2e/src/tests/fileExplorer.test.ts
  6. 6
      apps/remix-ide-e2e/src/tests/gist.spec.ts
  7. 5
      apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts
  8. 2
      apps/remix-ide-e2e/src/tests/staticAnalysis.spec.ts
  9. 2
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  10. 2
      apps/remix-ide-e2e/src/tests/url.spec.ts
  11. 4
      apps/remix-ide/src/app.js
  12. 6
      apps/remix-ide/src/app/compiler/compiler-imports.js
  13. 5
      apps/remix-ide/src/app/editor/contextView.js
  14. 12
      apps/remix-ide/src/app/files/file-explorer.js
  15. 29
      apps/remix-ide/src/app/files/fileManager.js
  16. 20
      apps/remix-ide/src/app/files/fileProvider.js
  17. 18
      apps/remix-ide/src/app/files/hardhat-handle.js
  18. 33
      apps/remix-ide/src/app/files/remixDProvider.js
  19. 6
      apps/remix-ide/src/app/files/remixd-handle.js
  20. 6
      apps/remix-ide/src/app/files/workspaceFileProvider.js
  21. 35
      apps/remix-ide/src/app/panels/file-panel.js
  22. 58
      apps/remix-ide/src/app/tabs/analysis-tab.js
  23. 31
      apps/remix-ide/src/app/tabs/compile-tab.js
  24. 41
      apps/remix-ide/src/app/tabs/compileTab/compileTab.js
  25. 14
      apps/remix-ide/src/app/tabs/compileTab/compilerContainer.js
  26. 12
      apps/remix-ide/src/app/tabs/network-module.js
  27. 6
      apps/remix-ide/src/app/tabs/runTab/contractDropdown.js
  28. 11
      apps/remix-ide/src/app/tabs/runTab/model/dropdownlogic.js
  29. 4
      apps/remix-ide/src/app/tabs/runTab/model/recorder.js
  30. 27
      apps/remix-ide/src/app/tabs/runTab/settings.js
  31. 302
      apps/remix-ide/src/app/tabs/staticanalysis/staticAnalysisView.js
  32. 36
      apps/remix-ide/src/app/tabs/staticanalysis/styles/staticAnalysisView-styles.js
  33. 20
      apps/remix-ide/src/app/tabs/test-tab.js
  34. 4
      apps/remix-ide/src/app/tabs/testTab/testTab.js
  35. 2
      apps/remix-ide/src/app/udapp/run-tab.js
  36. 5
      apps/remix-ide/src/app/ui/renderer.js
  37. 3
      apps/remix-ide/src/app/ui/universal-dapp-ui.js
  38. 73
      apps/remix-ide/src/blockchain/blockchain.js
  39. 159
      apps/remix-ide/src/blockchain/execution-context.js
  40. 6
      apps/remix-ide/src/blockchain/providers/vm.js
  41. 46
      apps/remix-ide/src/blockchain/txResultHelper.js
  42. 35
      apps/remix-ide/src/lib/helper.js
  43. 4
      apps/remix-ide/src/remixAppManager.js
  44. 15
      jest.config.js
  45. 3
      libs/remix-debug/src/Ethdebugger.ts
  46. 11
      libs/remix-debug/src/debugger/solidityLocals.ts
  47. 13
      libs/remix-debug/src/solidity-decoder/internalCallTree.ts
  48. 4
      libs/remix-debug/src/solidity-decoder/localDecoder.ts
  49. 33
      libs/remix-debug/src/solidity-decoder/types/RefType.ts
  50. 4
      libs/remix-debug/src/solidity-decoder/types/StringType.ts
  51. 2
      libs/remix-debug/src/solidity-decoder/types/ValueType.ts
  52. 10
      libs/remix-debug/test/decoder/localsTests/helper.ts
  53. 10
      libs/remix-lib/src/execution/txExecution.ts
  54. 2
      libs/remix-lib/src/execution/txFormat.ts
  55. 40
      libs/remix-lib/src/execution/txListener.ts
  56. 250
      libs/remix-lib/src/execution/txRunner.ts
  57. 121
      libs/remix-lib/src/execution/txRunnerVM.ts
  58. 147
      libs/remix-lib/src/execution/txRunnerWeb3.ts
  59. 8
      libs/remix-lib/src/helpers/txResultHelper.ts
  60. 20
      libs/remix-lib/src/index.ts
  61. 379
      libs/remix-lib/src/universalDapp.ts
  62. 6
      libs/remix-lib/src/web3Provider/web3VmProvider.ts
  63. 7
      libs/remix-lib/test/txFormat.ts
  64. 37
      libs/remix-lib/test/txResultHelper.ts
  65. 2
      libs/remix-simulator/package.json
  66. 8
      libs/remix-simulator/src/genesis.ts
  67. 2
      libs/remix-simulator/src/index.ts
  68. 16
      libs/remix-simulator/src/methods/accounts.ts
  69. 18
      libs/remix-simulator/src/methods/blocks.ts
  70. 12
      libs/remix-simulator/src/methods/debug.ts
  71. 24
      libs/remix-simulator/src/methods/filters.ts
  72. 97
      libs/remix-simulator/src/methods/transactions.ts
  73. 41
      libs/remix-simulator/src/methods/txProcess.ts
  74. 56
      libs/remix-simulator/src/provider.ts
  75. 169
      libs/remix-simulator/src/vm-context.ts
  76. 2
      libs/remix-solidity/package.json
  77. 3
      libs/remix-tests/jest.config.js
  78. 6
      libs/remix-tests/package.json
  79. 2
      libs/remix-tests/src/deployer.ts
  80. 3
      libs/remix-tests/tests/testRunner.cli.spec.ts
  81. 2
      libs/remix-tests/tsconfig.json
  82. 27
      libs/remix-tests/tsconfig.lib.json
  83. 29
      libs/remix-tests/tsconfig.spec.json
  84. 4
      libs/remix-ui/checkbox/.babelrc
  85. 19
      libs/remix-ui/checkbox/.eslintrc
  86. 7
      libs/remix-ui/checkbox/README.md
  87. 1
      libs/remix-ui/checkbox/src/index.ts
  88. 0
      libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.css
  89. 47
      libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx
  90. 16
      libs/remix-ui/checkbox/tsconfig.json
  91. 13
      libs/remix-ui/checkbox/tsconfig.lib.json
  92. 293
      libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts
  93. 598
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  94. 344
      libs/remix-ui/file-explorer/src/lib/reducers/fileSystem.ts
  95. 2
      libs/remix-ui/file-explorer/src/lib/types/index.ts
  96. 13
      libs/remix-ui/file-explorer/src/lib/utils/index.ts
  97. 18
      libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx
  98. 6
      libs/remix-ui/modal-dialog/src/lib/types/index.ts
  99. 4
      libs/remix-ui/static-analyser/.babelrc
  100. 19
      libs/remix-ui/static-analyser/.eslintrc
  101. Some files were not shown because too many files have changed in this diff Show More

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Debugger</title> <title>Debugger</title>
<base href="/" /> <base href="./" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />

@ -18,9 +18,9 @@ function addFile (browser: NightwatchBrowser, name: string, content: NightwatchC
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory
.click('.newFile') .click('.newFile')
.waitForElementContainsText('*[data-id="treeViewLitreeViewItem/blank"]', '', 60000) .waitForElementContainsText('*[data-id$="/blank"]', '', 60000)
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', name) .sendKeys('*[data-id$="/blank"] .remixui_items', name)
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER)
.pause(2000) .pause(2000)
.waitForElementVisible(`li[data-id="treeViewLitreeViewItem${name}"]`, 60000) .waitForElementVisible(`li[data-id="treeViewLitreeViewItem${name}"]`, 60000)
.setEditorValue(content.content) .setEditorValue(content.content)

@ -2,11 +2,18 @@ import { NightwatchBrowser } from 'nightwatch'
require('dotenv').config() require('dotenv').config()
export default function (browser: NightwatchBrowser, callback: VoidFunction, url?: string, preloadPlugins = true): void { export default function (browser: NightwatchBrowser, callback: VoidFunction, url?: string, preloadPlugins = true, closeWorkspaceAlert = true): void {
browser browser
.url(url || 'http://127.0.0.1:8080') .url(url || 'http://127.0.0.1:8080')
.pause(5000) .pause(5000)
.switchBrowserTab(0) .switchBrowserTab(0)
.perform((done) => {
if (closeWorkspaceAlert) {
browser.waitForElementVisible('*[data-id="modalDialogModalBody"]', 60000)
.modalFooterOKClick()
}
done()
})
.fullscreenWindow(() => { .fullscreenWindow(() => {
if (preloadPlugins) { if (preloadPlugins) {
initModules(browser, () => { initModules(browser, () => {

@ -323,7 +323,7 @@ const localVariable_step266_ABIEncoder = { // eslint-disable-line
value: '0x0000000000000000000000000000000000000000000000000000000000000002' value: '0x0000000000000000000000000000000000000000000000000000000000000002'
}, },
userData: { userData: {
error: '<decoding failed - no decoder for calldata>', value: '0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000015b38da6a701c568545dcfcb03fcb875f56beddc4',
type: 'bytes' type: 'bytes'
} }
} }
@ -360,7 +360,7 @@ const localVariable_step717_ABIEncoder = { // eslint-disable-line
value: '0x5b38da6a701c568545dcfcb03fcb875f56beddc45b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001' value: '0x5b38da6a701c568545dcfcb03fcb875f56beddc45b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001'
}, },
userData: { userData: {
error: '<decoding failed - no decoder for calldata>', value: '0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000015b38da6a701c568545dcfcb03fcb875f56beddc4',
type: 'bytes' type: 'bytes'
} }
} }

@ -22,9 +22,9 @@ module.exports = {
.click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory
.click('*[data-id="fileExplorerNewFilecreateNewFile"]') .click('*[data-id="fileExplorerNewFilecreateNewFile"]')
.pause(1000) .pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItem/blank"]') .waitForElementVisible('*[data-id$="/blank"]')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', '5_New_contract.sol') .sendKeys('*[data-id$="/blank"] .remixui_items', '5_New_contract.sol')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER)
.waitForElementVisible('*[data-id="treeViewLitreeViewItem5_New_contract.sol"]', 7000) .waitForElementVisible('*[data-id="treeViewLitreeViewItem5_New_contract.sol"]', 7000)
}, },
@ -49,9 +49,9 @@ module.exports = {
.click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory
.click('[data-id="fileExplorerNewFilecreateNewFolder"]') .click('[data-id="fileExplorerNewFilecreateNewFolder"]')
.pause(1000) .pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItem/blank"]') .waitForElementVisible('*[data-id$="/blank"]')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', 'Browser_Tests') .sendKeys('*[data-id$="/blank"] .remixui_items', 'Browser_Tests')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER)
.waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_Tests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_Tests"]')
}, },

@ -29,9 +29,9 @@ module.exports = {
.waitForElementVisible('*[data-id="fileExplorerNewFilecreateNewFolder"]') .waitForElementVisible('*[data-id="fileExplorerNewFilecreateNewFolder"]')
.click('[data-id="fileExplorerNewFilecreateNewFolder"]') .click('[data-id="fileExplorerNewFilecreateNewFolder"]')
.pause(1000) .pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItem/blank"]') .waitForElementVisible('*[data-id$="/blank"]')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', 'Browser_Tests') .sendKeys('*[data-id$="/blank"] .remixui_items', 'Browser_Tests')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER)
.waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_Tests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_Tests"]')
.addFile('File.sol', { content: '' }) .addFile('File.sol', { content: '' })
.click('*[data-id="fileExplorerNewFilepublishToGist"]') .click('*[data-id="fileExplorerNewFilepublishToGist"]')

@ -82,7 +82,7 @@ module.exports = {
.clickElementAtPosition('.singleTestLabel', 0) .clickElementAtPosition('.singleTestLabel', 0)
.clickElementAtPosition('.singleTestLabel', 1) .clickElementAtPosition('.singleTestLabel', 1)
.scrollAndClick('*[data-id="testTabRunTestsTabRunAction"]') .scrollAndClick('*[data-id="testTabRunTestsTabRunAction"]')
.pause(5000) .pause(2000)
.click('*[data-id="testTabRunTestsTabStopAction"]') .click('*[data-id="testTabRunTestsTabStopAction"]')
.waitForElementContainsText('*[data-id="testTabRunTestsTabStopAction"]', 'Stopping', 60000) .waitForElementContainsText('*[data-id="testTabRunTestsTabStopAction"]', 'Stopping', 60000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/ks2b_test.sol', 120000) .waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/ks2b_test.sol', 120000)
@ -179,9 +179,8 @@ function runTests (browser: NightwatchBrowser) {
.click('*[data-id="treeViewLitreeViewItemcontracts"]') .click('*[data-id="treeViewLitreeViewItemcontracts"]')
.openFile('contracts/3_Ballot.sol') .openFile('contracts/3_Ballot.sol')
.clickLaunchIcon('solidityUnitTesting') .clickLaunchIcon('solidityUnitTesting')
.pause(500)
.setValue('*[data-id="uiPathInput"]', 'tests')
.pause(2000) .pause(2000)
.verify.attributeEquals('*[data-id="uiPathInput"]', 'value', 'tests')
.scrollAndClick('#runTestsTabRunAction') .scrollAndClick('#runTestsTabRunAction')
.waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000)
.waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000) .waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000)

@ -40,7 +40,7 @@ function runTests (browser: NightwatchBrowser) {
.pause(10000) .pause(10000)
.testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['TooMuchGas', 'test1', 'test2']) .testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['TooMuchGas', 'test1', 'test2'])
.clickLaunchIcon('solidityStaticAnalysis') .clickLaunchIcon('solidityStaticAnalysis')
.click('#staticanalysisView button') .click('#staticanalysisButton button')
.waitForElementPresent('#staticanalysisresult .warning', 2000, true, function () { .waitForElementPresent('#staticanalysisresult .warning', 2000, true, function () {
listSelectorContains(['Use of tx.origin', listSelectorContains(['Use of tx.origin',
'Fallback function of contract TooMuchGas requires too much gas', 'Fallback function of contract TooMuchGas requires too much gas',

@ -62,7 +62,7 @@ module.exports = {
'Call web3.eth.getAccounts() using JavaScript VM': function (browser: NightwatchBrowser) { 'Call web3.eth.getAccounts() using JavaScript VM': function (browser: NightwatchBrowser) {
browser browser
.executeScript('web3.eth.getAccounts()') .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) { 'Call web3.eth.getAccounts() using Web3 Provider': function (browser: NightwatchBrowser) {

@ -10,7 +10,7 @@ const sources = [
module.exports = { module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) { before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done, 'http://127.0.0.1:8080/#optimize=true&runs=300&evmVersion=istanbul&version=soljson-v0.7.4+commit.3f05b770.js&code=cHJhZ21hIHNvbGlkaXR5ID49MC42LjAgPDAuNy4wOwoKaW1wb3J0ICJodHRwczovL2dpdGh1Yi5jb20vT3BlblplcHBlbGluL29wZW56ZXBwZWxpbi1jb250cmFjdHMvYmxvYi9tYXN0ZXIvY29udHJhY3RzL2FjY2Vzcy9Pd25hYmxlLnNvbCI7Cgpjb250cmFjdCBHZXRQYWlkIGlzIE93bmFibGUgewogIGZ1bmN0aW9uIHdpdGhkcmF3KCkgZXh0ZXJuYWwgb25seU93bmVyIHsKICB9Cn0') init(browser, done, 'http://127.0.0.1:8080/#optimize=true&runs=300&evmVersion=istanbul&version=soljson-v0.7.4+commit.3f05b770.js&code=cHJhZ21hIHNvbGlkaXR5ID49MC42LjAgPDAuNy4wOwoKaW1wb3J0ICJodHRwczovL2dpdGh1Yi5jb20vT3BlblplcHBlbGluL29wZW56ZXBwZWxpbi1jb250cmFjdHMvYmxvYi9tYXN0ZXIvY29udHJhY3RzL2FjY2Vzcy9Pd25hYmxlLnNvbCI7Cgpjb250cmFjdCBHZXRQYWlkIGlzIE93bmFibGUgewogIGZ1bmN0aW9uIHdpdGhkcmF3KCkgZXh0ZXJuYWwgb25seU93bmVyIHsKICB9Cn0', true, false)
}, },
'@sources': function () { '@sources': function () {

@ -431,12 +431,14 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
engine.register([ engine.register([
compileTab, compileTab,
compileTab.compileTabLogic,
run, run,
debug, debug,
analysis, analysis,
test, test,
filePanel.remixdHandle, filePanel.remixdHandle,
filePanel.gitHandle filePanel.gitHandle,
filePanel.hardhatHandle
]) ])
if (isElectron()) { if (isElectron()) {

@ -121,9 +121,7 @@ module.exports = class CompilerImports extends Plugin {
if (provider.type === 'localhost' && !provider.isConnected()) { if (provider.type === 'localhost' && !provider.isConnected()) {
return reject(new Error(`file provider ${provider.type} not available while trying to resolve ${url}`)) return reject(new Error(`file provider ${provider.type} not available while trying to resolve ${url}`))
} }
provider.exists(url, (error, exist) => { provider.exists(url).then(exist => {
if (error) return reject(error)
/* /*
if the path is absolute and the file does not exist, we can stop here if the path is absolute and the file does not exist, we can stop here
Doesn't make sense to try to resolve "localhost/node_modules/localhost/node_modules/<path>" and we'll end in an infinite loop. Doesn't make sense to try to resolve "localhost/node_modules/localhost/node_modules/<path>" and we'll end in an infinite loop.
@ -162,6 +160,8 @@ module.exports = class CompilerImports extends Plugin {
if (error) return reject(error) if (error) return reject(error)
resolve(content) resolve(content)
}) })
}).catch(error => {
return reject(error)
}) })
} }
}) })

@ -109,10 +109,11 @@ class ContextView {
if (filename !== this._deps.config.get('currentFile')) { if (filename !== this._deps.config.get('currentFile')) {
const provider = this._deps.fileManager.fileProviderOf(filename) const provider = this._deps.fileManager.fileProviderOf(filename)
if (provider) { if (provider) {
provider.exists(filename, (error, exist) => { provider.exists(filename).then(exist => {
if (error) return console.log(error)
this._deps.fileManager.open(filename) this._deps.fileManager.open(filename)
jumpToLine(lineColumn) jumpToLine(lineColumn)
}).catch(error => {
if (error) return console.log(error)
}) })
} }
} else { } else {

@ -9,7 +9,7 @@ const helper = require('../../lib/helper')
const yo = require('yo-yo') const yo = require('yo-yo')
const Treeview = require('../ui/TreeView') const Treeview = require('../ui/TreeView')
const modalDialog = require('../ui/modaldialog') const modalDialog = require('../ui/modaldialog')
const EventManager = require('../../lib/events') const EventManager = require('events')
const contextMenu = require('../ui/contextMenu') const contextMenu = require('../ui/contextMenu')
const css = require('./styles/file-explorer-styles') const css = require('./styles/file-explorer-styles')
const globalRegistry = require('../../global/registry') const globalRegistry = require('../../global/registry')
@ -94,11 +94,11 @@ function fileExplorer (localRegistry, files, menuItems, plugin) {
}) })
// register to event of the file provider // register to event of the file provider
files.event.register('fileRemoved', fileRemoved) files.event.on('fileRemoved', fileRemoved)
files.event.register('fileRenamed', fileRenamed) files.event.on('fileRenamed', fileRenamed)
files.event.register('fileRenamedError', fileRenamedError) files.event.on('fileRenamedError', fileRenamedError)
files.event.register('fileAdded', fileAdded) files.event.on('fileAdded', fileAdded)
files.event.register('folderAdded', folderAdded) files.event.on('folderAdded', folderAdded)
function fileRenamedError (error) { function fileRenamedError (error) {
modalDialogCustom.alert(error) modalDialogCustom.alert(error)

@ -121,10 +121,7 @@ class FileManager extends Plugin {
try { try {
path = this.limitPluginScope(path) path = this.limitPluginScope(path)
const provider = this.fileProviderOf(path) const provider = this.fileProviderOf(path)
const result = provider.exists(path, (err, result) => { const result = provider.exists(path)
if (err) return false
return result
})
return result return result
} catch (e) { } catch (e) {
@ -332,18 +329,18 @@ class FileManager extends Plugin {
workspaceExplorer: this._components.registry.get('fileproviders/workspace').api, workspaceExplorer: this._components.registry.get('fileproviders/workspace').api,
filesProviders: this._components.registry.get('fileproviders').api filesProviders: this._components.registry.get('fileproviders').api
} }
this._deps.browserExplorer.event.register('fileChanged', (path) => { this.fileChangedEvent(path) }) this._deps.browserExplorer.event.on('fileChanged', (path) => { this.fileChangedEvent(path) })
this._deps.browserExplorer.event.register('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) }) this._deps.browserExplorer.event.on('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.localhostExplorer.event.on('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) })
this._deps.browserExplorer.event.register('fileRemoved', (path) => { this.fileRemovedEvent(path) }) this._deps.browserExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) })
this._deps.browserExplorer.event.register('fileAdded', (path) => { this.fileAddedEvent(path) }) this._deps.browserExplorer.event.on('fileAdded', (path) => { this.fileAddedEvent(path) })
this._deps.localhostExplorer.event.register('fileRemoved', (path) => { this.fileRemovedEvent(path) }) this._deps.localhostExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) })
this._deps.localhostExplorer.event.register('errored', (event) => { this.removeTabsOf(this._deps.localhostExplorer) }) this._deps.localhostExplorer.event.on('errored', (event) => { this.removeTabsOf(this._deps.localhostExplorer) })
this._deps.localhostExplorer.event.register('closed', (event) => { this.removeTabsOf(this._deps.localhostExplorer) }) this._deps.localhostExplorer.event.on('closed', (event) => { this.removeTabsOf(this._deps.localhostExplorer) })
this._deps.workspaceExplorer.event.register('fileChanged', (path) => { this.fileChangedEvent(path) }) this._deps.workspaceExplorer.event.on('fileChanged', (path) => { this.fileChangedEvent(path) })
this._deps.workspaceExplorer.event.register('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) }) this._deps.workspaceExplorer.event.on('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) })
this._deps.workspaceExplorer.event.register('fileRemoved', (path) => { this.fileRemovedEvent(path) }) this._deps.workspaceExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) })
this._deps.workspaceExplorer.event.register('fileAdded', (path) => { this.fileAddedEvent(path) }) this._deps.workspaceExplorer.event.on('fileAdded', (path) => { this.fileAddedEvent(path) })
this.getCurrentFile = this.file this.getCurrentFile = this.file
this.getFile = this.readFile this.getFile = this.readFile

@ -1,7 +1,7 @@
'use strict' 'use strict'
const CompilerImport = require('../compiler/compiler-imports') const CompilerImport = require('../compiler/compiler-imports')
const EventManager = require('../../lib/events') const EventManager = require('events')
const modalDialogCustom = require('../ui/modal-dialog-custom') const modalDialogCustom = require('../ui/modal-dialog-custom')
const tooltip = require('../ui/tooltip') const tooltip = require('../ui/tooltip')
const remixLib = require('@remix-project/remix-lib') const remixLib = require('@remix-project/remix-lib')
@ -63,11 +63,11 @@ class FileProvider {
}) })
} }
exists (path, cb) { async exists (path) {
// todo check the type (directory/file) as well #2386 // todo check the type (directory/file) as well #2386
// currently it is not possible to have a file and folder with same path // currently it is not possible to have a file and folder with same path
const ret = this._exists(path) const ret = this._exists(path)
if (cb) cb(null, ret)
return ret return ret
} }
@ -111,9 +111,9 @@ class FileProvider {
return false return false
} }
if (!exists) { if (!exists) {
this.event.trigger('fileAdded', [this._normalizePath(unprefixedpath), false]) this.event.emit('fileAdded', this._normalizePath(unprefixedpath), false)
} else { } else {
this.event.trigger('fileChanged', [this._normalizePath(unprefixedpath)]) this.event.emit('fileChanged', this._normalizePath(unprefixedpath))
} }
cb() cb()
return true return true
@ -128,7 +128,7 @@ class FileProvider {
currentCheck = currentCheck + '/' + value currentCheck = currentCheck + '/' + value
if (!window.remixFileSystem.existsSync(currentCheck)) { if (!window.remixFileSystem.existsSync(currentCheck)) {
window.remixFileSystem.mkdirSync(currentCheck) window.remixFileSystem.mkdirSync(currentCheck)
this.event.trigger('folderAdded', [this._normalizePath(currentCheck)]) this.event.emit('folderAdded', this._normalizePath(currentCheck))
} }
}) })
if (cb) cb() if (cb) cb()
@ -184,7 +184,7 @@ class FileProvider {
// folder is empty // folder is empty
window.remixFileSystem.rmdirSync(path, console.log) window.remixFileSystem.rmdirSync(path, console.log)
} }
this.event.trigger('fileRemoved', [this._normalizePath(path)]) this.event.emit('fileRemoved', this._normalizePath(path))
} }
} catch (e) { } catch (e) {
console.log(e) console.log(e)
@ -249,7 +249,7 @@ class FileProvider {
path = this.removePrefix(path) path = this.removePrefix(path)
if (window.remixFileSystem.existsSync(path) && !window.remixFileSystem.statSync(path).isDirectory()) { if (window.remixFileSystem.existsSync(path) && !window.remixFileSystem.statSync(path).isDirectory()) {
window.remixFileSystem.unlinkSync(path, console.log) window.remixFileSystem.unlinkSync(path, console.log)
this.event.trigger('fileRemoved', [this._normalizePath(path)]) this.event.emit('fileRemoved', this._normalizePath(path))
return true return true
} else return false } else return false
} }
@ -259,11 +259,11 @@ class FileProvider {
var unprefixednewPath = this.removePrefix(newPath) var unprefixednewPath = this.removePrefix(newPath)
if (this._exists(unprefixedoldPath)) { if (this._exists(unprefixedoldPath)) {
window.remixFileSystem.renameSync(unprefixedoldPath, unprefixednewPath) window.remixFileSystem.renameSync(unprefixedoldPath, unprefixednewPath)
this.event.trigger('fileRenamed', [ this.event.emit('fileRenamed',
this._normalizePath(unprefixedoldPath), this._normalizePath(unprefixedoldPath),
this._normalizePath(unprefixednewPath), this._normalizePath(unprefixednewPath),
isFolder isFolder
]) )
return true return true
} }
return false return false

@ -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)
}
}

@ -17,32 +17,32 @@ module.exports = class RemixDProvider extends FileProvider {
var remixdEvents = ['connecting', 'connected', 'errored', 'closed'] var remixdEvents = ['connecting', 'connected', 'errored', 'closed']
remixdEvents.forEach((value) => { remixdEvents.forEach((value) => {
this._appManager.on('remixd', value, (event) => { this._appManager.on('remixd', value, (event) => {
this.event.trigger(value, [event]) this.event.emit(value, event)
}) })
}) })
this._appManager.on('remixd', 'folderAdded', (path) => { this._appManager.on('remixd', 'folderAdded', (path) => {
this.event.trigger('folderAdded', [path]) this.event.emit('folderAdded', path)
}) })
this._appManager.on('remixd', 'fileAdded', (path) => { this._appManager.on('remixd', 'fileAdded', (path) => {
this.event.trigger('fileAdded', [path]) this.event.emit('fileAdded', path)
}) })
this._appManager.on('remixd', 'fileChanged', (path) => { this._appManager.on('remixd', 'fileChanged', (path) => {
this.event.trigger('fileChanged', [path]) this.event.emit('fileChanged', path)
}) })
this._appManager.on('remixd', 'fileRemoved', (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._appManager.on('remixd', 'fileRenamed', (oldPath, newPath) => {
this.event.trigger('fileRemoved', [oldPath, newPath]) this.event.emit('fileRemoved', oldPath, newPath)
}) })
this._appManager.on('remixd', 'rootFolderChanged', () => { 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) { close (cb) {
this._isReady = false this._isReady = false
cb() cb()
this.event.trigger('disconnected') this.event.emit('disconnected')
} }
preInit () { preInit () {
this.event.trigger('loading') this.event.emit('loading')
} }
init (cb) { init (cb) {
@ -67,23 +67,22 @@ module.exports = class RemixDProvider extends FileProvider {
this._isReady = true this._isReady = true
this._readOnlyMode = result this._readOnlyMode = result
this._registerEvent() this._registerEvent()
this.event.trigger('connected') this.event.emit('connected')
cb && cb() cb && cb()
}).catch((error) => { }).catch((error) => {
cb && cb(error) cb && cb(error)
}) })
} }
exists (path, cb) { exists (path) {
if (!this._isReady) return cb && cb('provider not ready') if (!this._isReady) throw new Error('provider not ready')
const unprefixedpath = this.removePrefix(path) const unprefixedpath = this.removePrefix(path)
return this._appManager.call('remixd', 'exists', { path: unprefixedpath }) return this._appManager.call('remixd', 'exists', { path: unprefixedpath })
.then((result) => { .then((result) => {
if (cb) return cb(null, result)
return result return result
}).catch((error) => { })
if (cb) return cb(error) .catch((error) => {
throw new Error(error) throw new Error(error)
}) })
} }
@ -165,13 +164,13 @@ module.exports = class RemixDProvider extends FileProvider {
this.filesContent[newPath] = this.filesContent[oldPath] this.filesContent[newPath] = this.filesContent[oldPath]
delete this.filesContent[oldPath] delete this.filesContent[oldPath]
this.init(() => { this.init(() => {
this.event.trigger('fileRenamed', [oldPath, newPath, isFolder]) this.event.emit('fileRenamed', oldPath, newPath, isFolder)
}) })
return result return result
}).catch(error => { }).catch(error => {
console.log(error) console.log(error)
if (this.error[error.code]) error = this.error[error.code] 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])
}) })
} }

@ -39,6 +39,7 @@ export class RemixdHandle extends WebsocketPlugin {
deactivate () { deactivate () {
if (super.socket) super.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('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342
this.appManager.deactivatePlugin('hardhat')
this.locahostProvider.close((error) => { this.locahostProvider.close((error) => {
if (error) console.log(error) if (error) console.log(error)
}) })
@ -51,6 +52,7 @@ export class RemixdHandle extends WebsocketPlugin {
async canceled () { 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('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('remixd')
await this.appManager.deactivatePlugin('hardhat')
} }
/** /**
@ -81,7 +83,7 @@ export class RemixdHandle extends WebsocketPlugin {
} }
}, 3000) }, 3000)
this.locahostProvider.init(() => {}) this.locahostProvider.init(() => {})
// this.call('manager', 'activatePlugin', 'git') this.call('manager', 'activatePlugin', 'hardhat')
} }
} }
if (this.locahostProvider.isConnected()) { if (this.locahostProvider.isConnected()) {
@ -135,7 +137,7 @@ function remixdDialog () {
<div class=${css.dialogParagraph}>If you have looked at the Remixd docs and just need remixd command, <br> here it is: <div class=${css.dialogParagraph}>If you have looked at the Remixd docs and just need remixd command, <br> here it is:
<br><b>remixd -s absolute-path-to-the-shared-folder --remix-ide your-remix-ide-URL-instance</b> <br><b>remixd -s absolute-path-to-the-shared-folder --remix-ide your-remix-ide-URL-instance</b>
</div> </div>
<div class=${css.dialogParagraph}>Connection will start a session between <em>${window.location.href}</em> and your local file system <i>ws://127.0.0.1:65520</i> <div class=${css.dialogParagraph}>Connection will start a session between <em>${window.location.origin}</em> and your local file system <i>ws://127.0.0.1:65520</i>
so please make sure your system is secured enough (port 65520 neither opened nor forwarded). so please make sure your system is secured enough (port 65520 neither opened nor forwarded).
</div> </div>
<div class=${css.dialogParagraph}> <div class=${css.dialogParagraph}>

@ -1,6 +1,6 @@
'use strict' 'use strict'
const EventManager = require('../../lib/events') const EventManager = require('events')
const FileProvider = require('./fileProvider') const FileProvider = require('./fileProvider')
const pathModule = require('path') const pathModule = require('path')
@ -33,7 +33,7 @@ class WorkspaceFileProvider extends FileProvider {
if (!this.workspace) this.createWorkspace() if (!this.workspace) this.createWorkspace()
path = path.replace(/^\/|\/$/g, '') // remove first and last slash path = path.replace(/^\/|\/$/g, '') // remove first and last slash
if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path
if (path.startsWith(this.workspace)) return this.workspacesPath + '/' + this.workspace if (path.startsWith(this.workspace)) return path.replace(this.workspace, this.workspacesPath + '/' + this.workspace)
path = super.removePrefix(path) path = super.removePrefix(path)
let ret = this.workspacesPath + '/' + this.workspace + '/' + (path === '/' ? '' : path) let ret = this.workspacesPath + '/' + this.workspace + '/' + (path === '/' ? '' : path)
@ -82,7 +82,7 @@ class WorkspaceFileProvider extends FileProvider {
createWorkspace (name) { createWorkspace (name) {
if (!name) name = 'default_workspace' if (!name) name = 'default_workspace'
this.event.trigger('createWorkspace', [name]) this.event.emit('createWorkspace', name)
} }
} }

@ -6,13 +6,13 @@ import ReactDOM from 'react-dom'
import { Workspace } from '@remix-ui/workspace' // eslint-disable-line import { Workspace } from '@remix-ui/workspace' // eslint-disable-line
import { bufferToHex, keccakFromString } from 'ethereumjs-util' import { bufferToHex, keccakFromString } from 'ethereumjs-util'
import { checkSpecialChars, checkSlash } from '../../lib/helper' import { checkSpecialChars, checkSlash } from '../../lib/helper'
var EventManager = require('../../lib/events') const { RemixdHandle } = require('../files/remixd-handle.js')
var { RemixdHandle } = require('../files/remixd-handle.js') const { GitHandle } = require('../files/git-handle.js')
var { GitHandle } = require('../files/git-handle.js') const { HardhatHandle } = require('../files/hardhat-handle.js')
var globalRegistry = require('../../global/registry') const globalRegistry = require('../../global/registry')
var examples = require('../editor/examples') const examples = require('../editor/examples')
var GistHandler = require('../../lib/gist-handler') const GistHandler = require('../../lib/gist-handler')
var QueryParams = require('../../lib/query-params') const QueryParams = require('../../lib/query-params')
const modalDialogCustom = require('../ui/modal-dialog-custom') const modalDialogCustom = require('../ui/modal-dialog-custom')
/* /*
Overview of APIs: Overview of APIs:
@ -47,7 +47,6 @@ const profile = {
module.exports = class Filepanel extends ViewPlugin { module.exports = class Filepanel extends ViewPlugin {
constructor (appManager) { constructor (appManager) {
super(profile) super(profile)
this.event = new EventManager()
this._components = {} this._components = {}
this._components.registry = globalRegistry this._components.registry = globalRegistry
this._deps = { this._deps = {
@ -60,6 +59,7 @@ module.exports = class Filepanel extends ViewPlugin {
this.remixdHandle = new RemixdHandle(this._deps.fileProviders.localhost, appManager) this.remixdHandle = new RemixdHandle(this._deps.fileProviders.localhost, appManager)
this.gitHandle = new GitHandle() this.gitHandle = new GitHandle()
this.hardhatHandle = new HardhatHandle()
this.registeredMenuItems = [] this.registeredMenuItems = []
this.request = {} this.request = {}
this.workspaces = [] this.workspaces = []
@ -188,8 +188,11 @@ module.exports = class Filepanel extends ViewPlugin {
const browserProvider = this._deps.fileProviders.browser const browserProvider = this._deps.fileProviders.browser
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name
const workspaceRootPath = 'browser/' + workspaceProvider.workspacesPath const workspaceRootPath = 'browser/' + workspaceProvider.workspacesPath
if (!browserProvider.exists(workspaceRootPath)) browserProvider.createDir(workspaceRootPath) const workspaceRootPathExists = await browserProvider.exists(workspaceRootPath)
if (!browserProvider.exists(workspacePath)) browserProvider.createDir(workspacePath) const workspacePathExists = await browserProvider.exists(workspacePath)
if (!workspaceRootPathExists) browserProvider.createDir(workspaceRootPath)
if (!workspacePathExists) browserProvider.createDir(workspacePath)
} }
async workspaceExists (name) { async workspaceExists (name) {
@ -209,11 +212,13 @@ module.exports = class Filepanel extends ViewPlugin {
workspaceProvider.setWorkspace(workspaceName) workspaceProvider.setWorkspace(workspaceName)
await this.request.setWorkspace(workspaceName) // tells the react component to switch to that workspace await this.request.setWorkspace(workspaceName) // tells the react component to switch to that workspace
for (const file in examples) { for (const file in examples) {
try { setTimeout(async () => { // space creation of files to give react ui time to update.
await workspaceProvider.set(examples[file].name, examples[file].content) try {
} catch (error) { await workspaceProvider.set(examples[file].name, examples[file].content)
console.error(error) } catch (error) {
} console.error(error)
}
}, 10)
} }
} }
} }

@ -1,9 +1,11 @@
import React from 'react' // eslint-disable-line
import { ViewPlugin } from '@remixproject/engine-web' import { ViewPlugin } from '@remixproject/engine-web'
import ReactDOM from 'react-dom'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import {RemixUiStaticAnalyser} from '@remix-ui/static-analyser' // eslint-disable-line
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
var Renderer = require('../ui/renderer')
var yo = require('yo-yo')
var StaticAnalysis = require('./staticanalysis/staticAnalysisView')
var EventManager = require('../../lib/events') var EventManager = require('../../lib/events')
const profile = { const profile = {
@ -25,23 +27,49 @@ class AnalysisTab extends ViewPlugin {
this.event = new EventManager() this.event = new EventManager()
this.events = new EventEmitter() this.events = new EventEmitter()
this.registry = registry this.registry = registry
this.element = document.createElement('div')
this.element.setAttribute('id', 'staticAnalyserView')
this._components = {
renderer: new Renderer(this)
}
this._components.registry = this.registry
this._deps = {
offsetToLineColumnConverter: this.registry.get(
'offsettolinecolumnconverter').api
}
}
onActivation () {
this.renderComponent()
} }
render () { render () {
this.staticanalysis = new StaticAnalysis(this.registry, this) return this.element
this.staticanalysis.event.register('staticAnaysisWarning', (count) => { }
if (count > 0) {
this.emit('statusChanged', { key: count, title: `${count} warning${count === 1 ? '' : 's'}`, type: 'warning' })
} else if (count === 0) {
this.emit('statusChanged', { key: 'succeed', title: 'no warning', type: 'success' })
} else {
// count ==-1 no compilation result
this.emit('statusChanged', { key: 'none' })
}
})
this.registry.put({ api: this.staticanalysis, name: 'staticanalysis' })
return yo`<div class="px-3 pb-1" id="staticanalysisView">${this.staticanalysis.render()}</div>` renderComponent () {
ReactDOM.render(
<RemixUiStaticAnalyser
analysisRunner={this.runner}
registry={this.registry}
staticanalysis={this.staticanalysis}
analysisModule={this}
event={this.event}
/>,
this.element,
() => {
this.event.register('staticAnaysisWarning', (count) => {
if (count > 0) {
this.emit('statusChanged', { key: count, title: `${count} warning${count === 1 ? '' : 's'}`, type: 'warning' })
} else if (count === 0) {
this.emit('statusChanged', { key: 'succeed', title: 'no warning', type: 'success' })
} else {
// count ==-1 no compilation result
this.emit('statusChanged', { key: 'none' })
}
})
}
)
} }
} }

@ -65,15 +65,10 @@ class CompileTab extends ViewPlugin {
eventHandlers: {}, eventHandlers: {},
loading: false loading: false
} }
this.compileTabLogic = new CompileTabLogic(this.queryParams, this.fileManager, this.editor, this.config, this.fileProvider, this.contentImport)
} }
onActivationInternal () { 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.compiler = this.compileTabLogic.compiler
this.compileTabLogic.init() 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 * EVENTS
*/ */
listenToEvents () { 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.data.eventHandlers.onContentChanged = () => {
this.emit('statusChanged', { key: 'edited', title: 'the content has changed, needs recompilation', type: 'info' }) 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.emit('statusChanged', { key: 'loading', title: 'compiling...', type: 'info' })
} }
this.on('filePanel', 'setWorkspace', () => this.resetResults())
this.compileTabLogic.event.on('startingCompilation', this.data.eventHandlers.onStartingCompilation) this.compileTabLogic.event.on('startingCompilation', this.data.eventHandlers.onStartingCompilation)
this.data.eventHandlers.onCurrentFileChanged = (name) => { this.data.eventHandlers.onCurrentFileChanged = (name) => {
@ -199,7 +214,7 @@ class CompileTab extends ViewPlugin {
// ctrl+s or command+s // ctrl+s or command+s
if ((e.metaKey || e.ctrlKey) && e.keyCode === 83) { if ((e.metaKey || e.ctrlKey) && e.keyCode === 83) {
e.preventDefault() e.preventDefault()
this.compileTabLogic.runCompiler() this.compileTabLogic.runCompiler(this.compilerContainer.hhCompilation)
} }
}) })
} }
@ -479,6 +494,7 @@ class CompileTab extends ViewPlugin {
} }
onActivation () { onActivation () {
this.call('manager', 'activatePlugin', 'solidity-logic')
this.listenToEvents() this.listenToEvents()
} }
@ -492,6 +508,7 @@ class CompileTab extends ViewPlugin {
this.fileManager.events.removeListener('noFileSelected', this.data.eventHandlers.onNoFileSelected) this.fileManager.events.removeListener('noFileSelected', this.data.eventHandlers.onNoFileSelected)
this.compiler.event.unregister('compilationFinished', this.data.eventHandlers.onCompilationFinished) this.compiler.event.unregister('compilationFinished', this.data.eventHandlers.onCompilationFinished)
globalRegistry.get('themeModule').api.events.removeListener('themeChanged', this.data.eventHandlers.onThemeChanged) globalRegistry.get('themeModule').api.events.removeListener('themeChanged', this.data.eventHandlers.onThemeChanged)
this.call('manager', 'deactivatePlugin', 'solidity-logic')
} }
} }

@ -1,10 +1,19 @@
import * as packageJson from '../../../../../../package.json'
import { Plugin } from '@remixproject/engine'
const EventEmitter = require('events') const EventEmitter = require('events')
var Compiler = require('@remix-project/remix-solidity').Compiler var Compiler = require('@remix-project/remix-solidity').Compiler
class CompileTab { const profile = {
constructor (queryParams, fileManager, editor, config, fileProvider, contentImport, miscApi) { 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.event = new EventEmitter()
this.miscApi = miscApi
this.queryParams = queryParams this.queryParams = queryParams
this.compilerImport = contentImport this.compilerImport = contentImport
this.compiler = new Compiler((url, cb) => this.compilerImport.resolveAndSave(url).then((result) => cb(null, result)).catch((error) => cb(error.message))) 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 { 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.fileManager.saveCurrentFile()
this.miscApi.clearAnnotations() this.call('editor', 'clearAnnotations')
var currentFile = this.config.get('currentFile') var currentFile = this.config.get('currentFile')
return this.compileFile(currentFile) return this.compileFile(currentFile)
} catch (err) { } catch (err) {

@ -15,6 +15,7 @@ class CompilerContainer {
this.editor = editor this.editor = editor
this.config = config this.config = config
this.queryParams = queryParams this.queryParams = queryParams
this.hhCompilation = false
this.data = { this.data = {
hideWarnings: config.get('hideWarnings') || false, hideWarnings: config.get('hideWarnings') || false,
@ -183,6 +184,10 @@ class CompilerContainer {
} }
}) })
this.hardhatCompilation = yo`<div class="mt-2 ${css.compilerConfig} custom-control custom-checkbox" style="display:none">
<input class="${css.autocompile} custom-control-input" onchange=${(e) => this.updatehhCompilation(e)} id="enableHardhat" type="checkbox" title="Enable Hardhat Compilation">
<label class="form-check-label custom-control-label" for="enableHardhat">Enable Hardhat Compilation</label>
</div>`
this._view.warnCompilationSlow = yo`<i title="Compilation Slow" style="visibility:hidden" class="${css.warnCompilationSlow} fas fa-exclamation-triangle" aria-hidden="true"></i>` this._view.warnCompilationSlow = yo`<i title="Compilation Slow" style="visibility:hidden" class="${css.warnCompilationSlow} fas fa-exclamation-triangle" aria-hidden="true"></i>`
this._view.compileIcon = yo`<i class="fas fa-sync ${css.icon}" aria-hidden="true"></i>` this._view.compileIcon = yo`<i class="fas fa-sync ${css.icon}" aria-hidden="true"></i>`
this._view.autoCompile = yo`<input class="${css.autocompile} custom-control-input" onchange=${() => this.updateAutoCompile()} data-id="compilerContainerAutoCompile" id="autoCompile" type="checkbox" title="Auto compile">` this._view.autoCompile = yo`<input class="${css.autocompile} custom-control-input" onchange=${() => this.updateAutoCompile()} data-id="compilerContainerAutoCompile" id="autoCompile" type="checkbox" title="Auto compile">`
@ -299,6 +304,7 @@ class CompilerContainer {
<label class="form-check-label custom-control-label" for="hideWarningsBox">Hide warnings</label> <label class="form-check-label custom-control-label" for="hideWarningsBox">Hide warnings</label>
</div> </div>
</div> </div>
${this.hardhatCompilation}
${this._view.compilationButton} ${this._view.compilationButton}
</header> </header>
</article> </article>
@ -326,12 +332,16 @@ class CompilerContainer {
this.config.set('autoCompile', this._view.autoCompile.checked) this.config.set('autoCompile', this._view.autoCompile.checked)
} }
updatehhCompilation (event) {
this.hhCompilation = event.target.checked
}
compile (event) { compile (event) {
const currentFile = this.config.get('currentFile') const currentFile = this.config.get('currentFile')
if (!this.isSolFileSelected()) return if (!this.isSolFileSelected()) return
this._setCompilerVersionFromPragma(currentFile) this._setCompilerVersionFromPragma(currentFile)
this.compileTabLogic.runCompiler() this.compileTabLogic.runCompiler(this.hhCompilation)
} }
compileIfAutoCompileOn () { compileIfAutoCompileOn () {
@ -517,7 +527,7 @@ class CompilerContainer {
// fetching both normal and wasm builds and creating a [version, baseUrl] map // fetching both normal and wasm builds and creating a [version, baseUrl] map
async fetchAllVersion (callback) { async fetchAllVersion (callback) {
let selectedVersion, allVersionsWasm, isURL let selectedVersion, allVersionsWasm, isURL
let allVersions = [{ path: 'builtin', longVersion: 'latest local version - 0.7.4' }] let allVersions = [{ path: 'builtin', longVersion: 'Stable local version - 0.7.4' }]
// fetch normal builds // fetch normal builds
const binRes = await promisedMiniXhr(`${baseURLBin}/list.json`) const binRes = await promisedMiniXhr(`${baseURLBin}/list.json`)
// fetch wasm builds // fetch wasm builds

@ -22,18 +22,6 @@ export class NetworkModule extends Plugin {
this.blockchain.event.register('contextChanged', (provider) => { this.blockchain.event.register('contextChanged', (provider) => {
this.emit('providerChanged', 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) */ /** Return the current network provider (web3, vm, injected) */

@ -9,6 +9,7 @@ const confirmDialog = require('../../ui/confirmDialog')
const modalDialog = require('../../ui/modaldialog') const modalDialog = require('../../ui/modaldialog')
const MultiParamManager = require('../../ui/multiParamManager') const MultiParamManager = require('../../ui/multiParamManager')
const helper = require('../../../lib/helper') const helper = require('../../../lib/helper')
const _paq = window._paq = window._paq || []
class ContractDropdownUI { class ContractDropdownUI {
constructor (blockchain, dropdownLogic, logCallback, runView) { constructor (blockchain, dropdownLogic, logCallback, runView) {
@ -300,13 +301,15 @@ class ContractDropdownUI {
if (error) { if (error) {
return this.logCallback(error) return this.logCallback(error)
} }
self.event.trigger('newContractInstanceAdded', [contractObject, address, contractObject.name]) self.event.trigger('newContractInstanceAdded', [contractObject, address, contractObject.name])
const data = self.runView.compilersArtefacts.getCompilerAbstract(contractObject.contract.file) const data = self.runView.compilersArtefacts.getCompilerAbstract(contractObject.contract.file)
self.runView.compilersArtefacts.addResolvedContract(helper.addressToString(address), data) self.runView.compilersArtefacts.addResolvedContract(helper.addressToString(address), data)
if (self.ipfsCheckedState) { if (self.ipfsCheckedState) {
_paq.push(['trackEvent', 'udapp', `DeployAndPublish_${this.networkName}`])
publishToStorage('ipfs', self.runView.fileProvider, self.runView.fileManager, selectedContract) publishToStorage('ipfs', self.runView.fileProvider, self.runView.fileManager, selectedContract)
} else {
_paq.push(['trackEvent', 'udapp', `DeployOnly_${this.networkName}`])
} }
} }
@ -340,6 +343,7 @@ class ContractDropdownUI {
} }
deployContract (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) { deployContract (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) {
_paq.push(['trackEvent', 'udapp', 'DeployContractTo', this.networkName])
const { statusCb } = callbacks const { statusCb } = callbacks
if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) { if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) {
return this.blockchain.deployContractAndLibraries(selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) return this.blockchain.deployContractAndLibraries(selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb)

@ -1,7 +1,8 @@
var remixLib = require('@remix-project/remix-lib') const remixLib = require('@remix-project/remix-lib')
var txHelper = remixLib.execution.txHelper const txHelper = remixLib.execution.txHelper
var CompilerAbstract = require('../../../compiler/compiler-abstract') const CompilerAbstract = require('../../../compiler/compiler-abstract')
var EventManager = remixLib.EventManager const EventManager = remixLib.EventManager
const _paq = window._paq = window._paq || []
class DropdownLogic { class DropdownLogic {
constructor (compilersArtefacts, config, editor, runView) { constructor (compilersArtefacts, config, editor, runView) {
@ -50,9 +51,11 @@ class DropdownLogic {
} catch (e) { } catch (e) {
return cb('Failed to parse the current file as JSON ABI.') return cb('Failed to parse the current file as JSON ABI.')
} }
_paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithABI'])
cb(null, 'abi', abi) cb(null, 'abi', abi)
}) })
} else { } else {
_paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithInstance'])
cb(null, 'instance') cb(null, 'instance')
} }
} }

@ -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 (error) return console.log(error)
if (call) return if (call) return
const rawAddress = txResult.receipt.contractAddress
if (!rawAddress) return // not a contract creation if (!rawAddress) return // not a contract creation
const address = helper.addressToString(rawAddress) const address = helper.addressToString(rawAddress)
// save back created addresses for the convertion from tokens to real adresses // save back created addresses for the convertion from tokens to real adresses

@ -1,3 +1,4 @@
import { BN } from 'ethereumjs-util'
const $ = require('jquery') const $ = require('jquery')
const yo = require('yo-yo') const yo = require('yo-yo')
const remixLib = require('@remix-project/remix-lib') const remixLib = require('@remix-project/remix-lib')
@ -65,14 +66,26 @@ class SettingsUI {
validateValue () { validateValue () {
const valueEl = this.el.querySelector('#value') const valueEl = this.el.querySelector('#value')
valueEl.value = parseInt(valueEl.value) if (!valueEl.value) {
// assign 0 if given value is // assign 0 if given value is
// - empty // - empty
// - not valid (for ex 4345-54) valueEl.value = 0
// - contains only '0's (for ex 0000) copy past or edit return
if (!valueEl.value) valueEl.value = 0 }
let v
try {
v = new BN(valueEl.value, 10)
valueEl.value = v.toString(10)
} catch (e) {
// assign 0 if given value is
// - not valid (for ex 4345-54)
// - contains only '0's (for ex 0000) copy past or edit
valueEl.value = 0
}
// if giveen value is negative(possible with copy-pasting) set to 0 // if giveen value is negative(possible with copy-pasting) set to 0
if (valueEl.value < 0) valueEl.value = 0 if (v.lt(0)) valueEl.value = 0
} }
render () { render () {

@ -1,302 +0,0 @@
'use strict'
var StaticAnalysisRunner = require('@remix-project/remix-analyzer').CodeAnalysis
var yo = require('yo-yo')
var $ = require('jquery')
var remixLib = require('@remix-project/remix-lib')
var utils = remixLib.util
var css = require('./styles/staticAnalysisView-styles')
var Renderer = require('../../ui/renderer')
const SourceHighlighter = require('../../editor/sourceHighlighter')
var EventManager = require('../../../lib/events')
function staticAnalysisView (localRegistry, analysisModule) {
var self = this
this.event = new EventManager()
this.view = null
this.runner = new StaticAnalysisRunner()
this.modulesView = this.renderModules()
this.lastCompilationResult = null
this.lastCompilationSource = null
this.currentFile = 'No file compiled'
this.sourceHighlighter = new SourceHighlighter()
this.analysisModule = analysisModule
self._components = {
renderer: new Renderer(analysisModule)
}
self._components.registry = localRegistry
// dependencies
self._deps = {
offsetToLineColumnConverter: self._components.registry.get('offsettolinecolumnconverter').api
}
analysisModule.on('solidity', 'compilationFinished', (file, source, languageVersion, data) => {
self.lastCompilationResult = null
self.lastCompilationSource = null
if (languageVersion.indexOf('soljson') !== 0) return
self.lastCompilationResult = data
self.lastCompilationSource = source
self.currentFile = file
self.correctRunBtnDisabled()
if (self.view && self.view.querySelector('#autorunstaticanalysis').checked) {
self.run()
}
})
}
staticAnalysisView.prototype.render = function () {
this.runBtn = yo`<button class="btn btn-sm w-25 btn-primary" onclick="${() => { this.run() }}" >Run</button>`
const view = yo`
<div class="${css.analysis}">
<div class="my-2 d-flex flex-column align-items-left">
<div class="${css.top} d-flex justify-content-between">
<div class="pl-2 ${css.label}" for="checkAllEntries">
<input id="checkAllEntries"
type="checkbox"
onclick="${(event) => { this.checkAll(event) }}"
style="vertical-align:bottom"
checked="true"
>
<label class="text-nowrap pl-2 mb-0" for="checkAllEntries">
Select all
</label>
</div>
<div class="${css.label}" for="autorunstaticanalysis">
<input id="autorunstaticanalysis"
type="checkbox"
style="vertical-align:bottom"
checked="true"
>
<label class="text-nowrap pl-2 mb-0" for="autorunstaticanalysis">
Autorun
</label>
</div>
${this.runBtn}
</div>
</div>
<div id="staticanalysismodules" class="list-group list-group-flush">
${this.modulesView}
</div>
<div class="mt-2 p-2 d-flex border-top flex-column">
<span>last results for:</span>
<span class="text-break break-word word-break font-weight-bold" id="staticAnalysisCurrentFile">${this.currentFile}</span>
</div>
<div class="${css.result} my-1" id='staticanalysisresult'></div>
</div>
`
if (!this.view) {
this.view = view
}
this.correctRunBtnDisabled()
return view
}
staticAnalysisView.prototype.selectedModules = function () {
if (!this.view) {
return []
}
const selected = this.view.querySelectorAll('[name="staticanalysismodule"]:checked')
var toRun = []
for (var i = 0; i < selected.length; i++) {
toRun.push(selected[i].attributes.index.value)
}
return toRun
}
staticAnalysisView.prototype.run = function () {
if (!this.view) {
return
}
const highlightLocation = async (location, fileName) => {
await this.analysisModule.call('editor', 'discardHighlight')
await this.analysisModule.call('editor', 'highlight', location, fileName)
}
const selected = this.selectedModules()
const warningContainer = $('#staticanalysisresult')
warningContainer.empty()
this.view.querySelector('#staticAnalysisCurrentFile').innerText = this.currentFile
var self = this
if (this.lastCompilationResult && selected.length) {
this.runBtn.removeAttribute('disabled')
let warningCount = 0
this.runner.run(this.lastCompilationResult, selected, (results) => {
const groupedModules = utils.groupBy(preProcessModules(this.runner.modules()), 'categoryId')
results.map((result, j) => {
let moduleName
Object.keys(groupedModules).map((key) => {
groupedModules[key].forEach((el) => {
if (el.name === result.name) {
moduleName = groupedModules[key][0].categoryDisplayName
}
})
})
const alreadyExistedEl = this.view.querySelector(`[id="staticAnalysisModule${moduleName}"]`)
if (!alreadyExistedEl) {
warningContainer.append(`
<div class="mb-4" name="staticAnalysisModules" id="staticAnalysisModule${moduleName}">
<span class="text-dark h6">${moduleName}</span>
</div>
`)
}
result.report.map((item, i) => {
let location = ''
let locationString = 'not available'
let column = 0
let row = 0
let fileName = this.currentFile
if (item.location) {
var split = item.location.split(':')
var file = split[2]
location = {
start: parseInt(split[0]),
length: parseInt(split[1])
}
location = self._deps.offsetToLineColumnConverter.offsetToLineColumn(
location,
parseInt(file),
self.lastCompilationSource.sources,
self.lastCompilationResult.sources
)
row = location.start.line
column = location.start.column
locationString = (row + 1) + ':' + column + ':'
fileName = Object.keys(self.lastCompilationResult.contracts)[file]
}
warningCount++
const msg = yo`
<span class="d-flex flex-column">
<span class="h6 font-weight-bold">${result.name}</span>
${item.warning}
${item.more ? yo`<span><a href="${item.more}" target="_blank">more</a></span>` : yo`<span></span>`}
<span class="" title="Position in ${fileName}">Pos: ${locationString}</span>
</span>`
self._components.renderer.error(
msg,
this.view.querySelector(`[id="staticAnalysisModule${moduleName}"]`),
{
click: () => highlightLocation(location, fileName),
type: 'warning',
useSpan: true,
errFile: fileName,
errLine: row,
errCol: column
}
)
})
})
// hide empty staticAnalysisModules sections
this.view.querySelectorAll('[name="staticAnalysisModules"]').forEach((section) => {
if (!section.getElementsByClassName('alert-warning').length) section.hidden = true
})
self.event.trigger('staticAnaysisWarning', [warningCount])
})
} else {
this.runBtn.setAttribute('disabled', 'disabled')
if (selected.length) {
warningContainer.html('No compiled AST available')
}
self.event.trigger('staticAnaysisWarning', [-1])
}
}
staticAnalysisView.prototype.checkModule = function (event) {
const selected = this.view.querySelectorAll('[name="staticanalysismodule"]:checked')
const checkAll = this.view.querySelector('[id="checkAllEntries"]')
this.correctRunBtnDisabled()
if (event.target.checked) {
checkAll.checked = true
} else if (!selected.length) {
checkAll.checked = false
}
}
staticAnalysisView.prototype.correctRunBtnDisabled = function () {
if (!this.view) {
return
}
const selected = this.view.querySelectorAll('[name="staticanalysismodule"]:checked')
if (this.lastCompilationResult && selected.length !== 0) {
this.runBtn.removeAttribute('disabled')
} else {
this.runBtn.setAttribute('disabled', 'disabled')
}
}
staticAnalysisView.prototype.checkAll = function (event) {
if (!this.view) {
return
}
// checks/unchecks all
const checkBoxes = this.view.querySelectorAll('[name="staticanalysismodule"]')
checkBoxes.forEach((checkbox) => { checkbox.checked = event.target.checked })
this.correctRunBtnDisabled()
}
staticAnalysisView.prototype.handleCollapse = function (e) {
const downs = e.toElement.parentElement.getElementsByClassName('fas fa-angle-double-right')
const iEls = document.getElementsByTagName('i')
for (var i = 0; i < iEls.length; i++) { iEls[i].hidden = false }
downs[0].hidden = true
}
staticAnalysisView.prototype.renderModules = function () {
const groupedModules = utils.groupBy(preProcessModules(this.runner.modules()), 'categoryId')
const moduleEntries = Object.keys(groupedModules).map((categoryId, i) => {
const category = groupedModules[categoryId]
const entriesDom = category.map((item, i) => {
return yo`
<div class="form-check">
<input id="staticanalysismodule_${categoryId}_${i}"
type="checkbox"
class="form-check-input staticAnalysisItem"
name="staticanalysismodule"
index=${item._index}
checked="true"
style="vertical-align:bottom"
onclick="${(event) => this.checkModule(event)}"
>
<label for="staticanalysismodule_${categoryId}_${i}" class="form-check-label mb-1">
<p class="mb-0 font-weight-bold text-capitalize">${item.name}</p>
${item.description}
</label>
</div>
`
})
return yo`
<div class="${css.block}">
<input type="radio" name="accordion" class="w-100 d-none card" id="heading${categoryId}" onclick=${(e) => this.handleCollapse(e)}"/>
<label for="heading${categoryId}" style="cursor: pointer;" class="pl-3 card-header h6 d-flex justify-content-between font-weight-bold border-left px-1 py-2 w-100">
${category[0].categoryDisplayName}
<div>
<i class="fas fa-angle-double-right"></i>
</div>
</label>
<div class="w-100 d-block px-2 my-1 ${css.entries}">
${entriesDom}
</div>
</div>
`
})
// collaps first module
moduleEntries[0].getElementsByTagName('input')[0].checked = true
moduleEntries[0].getElementsByTagName('i')[0].hidden = true
return yo`
<div class="accordion" id="accordionModules">
${moduleEntries}
</div>`
}
module.exports = staticAnalysisView
/**
* @dev Process & categorize static analysis modules to show them on UI
* @param arr list of static analysis modules received from remix-analyzer module
*/
function preProcessModules (arr) {
return arr.map((Item, i) => {
const itemObj = new Item()
itemObj._index = i
itemObj.categoryDisplayName = itemObj.category.displayName
itemObj.categoryId = itemObj.category.id
return itemObj
})
}

@ -1,36 +0,0 @@
var csjs = require('csjs-inject')
var css = csjs`
.analysis {
display: flex;
flex-direction: column;
}
.result {
margin-top: 1%;
max-height: 300px;
word-break: break-word;
}
.buttons {
margin: 1rem 0;
}
.label {
display: flex;
align-items: center;
}
.label {
display: flex;
align-items: center;
user-select: none;
}
.block input[type='radio']:checked ~ .entries{
height: auto;
transition: .5s ease-in;
}
.entries{
height: 0;
overflow: hidden;
transition: .5s ease-out;
}
`
module.exports = css

@ -52,7 +52,7 @@ module.exports = class TestTab extends ViewPlugin {
} }
listenToEvents () { listenToEvents () {
this.filePanel.event.register('newTestFileCreated', file => { this.on('filePanel', 'newTestFileCreated', file => {
var testList = this._view.el.querySelector("[class^='testList']") var testList = this._view.el.querySelector("[class^='testList']")
var test = this.createSingleTest(file) var test = this.createSingleTest(file)
testList.appendChild(test) testList.appendChild(test)
@ -76,6 +76,7 @@ module.exports = class TestTab extends ViewPlugin {
this.updateGenerateFileAction() this.updateGenerateFileAction()
if (!this.areTestsRunning) this.updateRunAction(file) if (!this.areTestsRunning) this.updateRunAction(file)
this.updateTestFileList() this.updateTestFileList()
this.clearResults()
this.testTabLogic.getTests((error, tests) => { this.testTabLogic.getTests((error, tests) => {
if (error) return tooltip(error) if (error) return tooltip(error)
this.data.allTests = tests this.data.allTests = tests
@ -434,21 +435,26 @@ module.exports = class TestTab extends ViewPlugin {
this.uiPathList.appendChild(yo`<option>${this.inputPath.value}</option>`) this.uiPathList.appendChild(yo`<option>${this.inputPath.value}</option>`)
} }
clearResults () {
yo.update(this.resultStatistics, yo`<span></span>`)
this.call('editor', 'clearAnnotations')
this.testsOutput.innerHTML = ''
this.testsOutput.hidden = true
this.testsExecutionStopped.hidden = true
this.testsExecutionStoppedError.hidden = true
}
runTests () { runTests () {
this.areTestsRunning = true this.areTestsRunning = true
this.hasBeenStopped = false this.hasBeenStopped = false
this.readyTestsNumber = 0 this.readyTestsNumber = 0
this.runningTestsNumber = this.data.selectedTests.length this.runningTestsNumber = this.data.selectedTests.length
yo.update(this.resultStatistics, this.createResultLabel())
const stopBtn = document.getElementById('runTestsTabStopAction') const stopBtn = document.getElementById('runTestsTabStopAction')
stopBtn.removeAttribute('disabled') stopBtn.removeAttribute('disabled')
const runBtn = document.getElementById('runTestsTabRunAction') const runBtn = document.getElementById('runTestsTabRunAction')
runBtn.setAttribute('disabled', 'disabled') runBtn.setAttribute('disabled', 'disabled')
this.call('editor', 'clearAnnotations') this.clearResults()
this.testsOutput.innerHTML = '' yo.update(this.resultStatistics, this.createResultLabel())
this.testsOutput.hidden = true
this.testsExecutionStopped.hidden = true
this.testsExecutionStoppedError.hidden = true
const tests = this.data.selectedTests const tests = this.data.selectedTests
if (!tests) return if (!tests) return
this.resultStatistics.hidden = tests.length === 0 this.resultStatistics.hidden = tests.length === 0

@ -18,7 +18,9 @@ class TestTabLogic {
// Checking to ignore the value which contains only whitespaces // Checking to ignore the value which contains only whitespaces
if (!path || !(/\S/.test(path))) return if (!path || !(/\S/.test(path))) return
const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0]) const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0])
fileProvider.exists(path, (e, res) => { if (!res) fileProvider.createDir(path) }) fileProvider.exists(path).then(res => {
if (!res) fileProvider.createDir(path)
})
} }
pathExists (path) { pathExists (path) {

@ -15,6 +15,7 @@ const RecorderUI = require('../tabs/runTab/recorder.js')
const DropdownLogic = require('../tabs/runTab/model/dropdownlogic.js') const DropdownLogic = require('../tabs/runTab/model/dropdownlogic.js')
const ContractDropdownUI = require('../tabs/runTab/contractDropdown.js') const ContractDropdownUI = require('../tabs/runTab/contractDropdown.js')
const toaster = require('../ui/tooltip') const toaster = require('../ui/tooltip')
const _paq = window._paq = window._paq || []
const UniversalDAppUI = require('../ui/universal-dapp-ui') const UniversalDAppUI = require('../ui/universal-dapp-ui')
@ -91,6 +92,7 @@ export class RunTab extends ViewPlugin {
} }
sendTransaction (tx) { sendTransaction (tx) {
_paq.push(['trackEvent', 'udapp', 'sendTx'])
return this.blockchain.sendTransaction(tx) return this.blockchain.sendTransaction(tx)
} }

@ -39,10 +39,11 @@ Renderer.prototype._errorClick = function (errFile, errLine, errCol) {
// TODO: refactor with this._components.contextView.jumpTo // TODO: refactor with this._components.contextView.jumpTo
var provider = self._deps.fileManager.fileProviderOf(errFile) var provider = self._deps.fileManager.fileProviderOf(errFile)
if (provider) { if (provider) {
provider.exists(errFile, (error, exist) => { provider.exists(errFile).then(exist => {
if (error) return console.log(error)
self._deps.fileManager.open(errFile) self._deps.fileManager.open(errFile)
editor.gotoLine(errLine, errCol) editor.gotoLine(errLine, errCol)
}).catch(error => {
if (error) return console.log(error)
}) })
} }
} else { } else {

@ -14,6 +14,7 @@ var txFormat = remixLib.execution.txFormat
const txHelper = remixLib.execution.txHelper const txHelper = remixLib.execution.txHelper
var TreeView = require('./TreeView') var TreeView = require('./TreeView')
var txCallBacks = require('./sendTxCallbacks') var txCallBacks = require('./sendTxCallbacks')
const _paq = window._paq = window._paq || []
function UniversalDAppUI (blockchain, logCallback) { function UniversalDAppUI (blockchain, logCallback) {
this.blockchain = blockchain this.blockchain = blockchain
@ -243,6 +244,8 @@ UniversalDAppUI.prototype.runTransaction = function (lookupOnly, args, valArr, i
outputOverride.appendChild(decoded) outputOverride.appendChild(decoded)
} }
} }
const info = `${lookupOnly ? 'call' : args.funABI.type !== 'fallback' ? 'lowLevelInteracions' : 'transact'}_${this.blockchain.executionContext.executionContext}`
_paq.push(['trackEvent', 'udapp', info])
const params = args.funABI.type !== 'fallback' ? inputsValues : '' const params = args.funABI.type !== 'fallback' ? inputsValues : ''
this.blockchain.runOrCallContractMethod( this.blockchain.runOrCallContractMethod(
args.contractName, args.contractName,

@ -3,16 +3,17 @@ const txFormat = remixLib.execution.txFormat
const txExecution = remixLib.execution.txExecution const txExecution = remixLib.execution.txExecution
const typeConversion = remixLib.execution.typeConversion const typeConversion = remixLib.execution.typeConversion
const Txlistener = remixLib.execution.txListener 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 txHelper = remixLib.execution.txHelper
const EventManager = remixLib.EventManager const EventManager = remixLib.EventManager
const executionContext = remixLib.execution.executionContext const { ExecutionContext } = require('./execution-context')
const Web3 = require('web3') const Web3 = require('web3')
const async = require('async') const async = require('async')
const { EventEmitter } = require('events') const { EventEmitter } = require('events')
const { resultToRemixTx } = require('./txResultHelper') const { resultToRemixTx } = remixLib.helpers.txResultHelper
const VMProvider = require('./providers/vm.js') const VMProvider = require('./providers/vm.js')
const InjectedProvider = require('./providers/injected.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 // NOTE: the config object will need to be refactored out in remix-lib
constructor (config) { constructor (config) {
this.event = new EventManager() this.event = new EventManager()
this.executionContext = executionContext this.executionContext = new ExecutionContext()
this.events = new EventEmitter() this.events = new EventEmitter()
this.config = config this.config = config
const web3Runner = new TxRunnerWeb3({
this.txRunner = new TxRunner({}, {
config: config, config: config,
detectNetwork: (cb) => { detectNetwork: (cb) => {
this.executionContext.detectNetwork(cb) this.executionContext.detectNetwork(cb)
@ -35,7 +35,9 @@ class Blockchain {
personalMode: () => { personalMode: () => {
return this.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false 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.executionContext.event.register('contextChanged', this.resetEnvironment.bind(this))
this.networkcallid = 0 this.networkcallid = 0
@ -123,7 +125,7 @@ class Blockchain {
if (error) { if (error) {
return finalCb(`creation of ${selectedContract.name} errored: ${(error.message ? error.message : 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`) return finalCb(`creation of ${selectedContract.name} errored: transaction execution failed`)
} }
finalCb(null, selectedContract, address) finalCb(null, selectedContract, address)
@ -309,18 +311,17 @@ class Blockchain {
resetEnvironment () { resetEnvironment () {
this.getCurrentProvider().resetEnvironment() this.getCurrentProvider().resetEnvironment()
// TODO: most params here can be refactored away in txRunner // TODO: most params here can be refactored away in txRunner
// this.txRunner = new TxRunner(this.providers.vm.accounts, { const web3Runner = new TxRunnerWeb3({
this.txRunner = new TxRunner(this.providers.vm.RemixSimulatorProvider.Accounts.accounts, {
// TODO: only used to check value of doNotShowTransactionConfirmationAgain property
config: this.config, config: this.config,
// TODO: to refactor, TxRunner already has access to executionContext
detectNetwork: (cb) => { detectNetwork: (cb) => {
this.executionContext.detectNetwork(cb) this.executionContext.detectNetwork(cb)
}, },
personalMode: () => { personalMode: () => {
return this.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false 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.txRunner.event.register('transactionBroadcasted', (txhash) => {
this.executionContext.detectNetwork((error, network) => { this.executionContext.detectNetwork((error, network) => {
if (error || !network) return if (error || !network) return
@ -372,10 +373,11 @@ class Blockchain {
(network, tx, gasEstimation, continueTxExecution, cancelCb) => { continueTxExecution() }, (network, tx, gasEstimation, continueTxExecution, cancelCb) => { continueTxExecution() },
(error, continueTxExecution, cancelCb) => { if (error) { reject(error) } else { continueTxExecution() } }, (error, continueTxExecution, cancelCb) => { if (error) { reject(error) } else { continueTxExecution() } },
(okCb, cancelCb) => { okCb() }, (okCb, cancelCb) => { okCb() },
(error, result) => { async (error, result) => {
if (error) return reject(error) if (error) return reject(error)
try { try {
resolve(resultToRemixTx(result)) const execResult = await this.web3().eth.getExecutionResultFromSimulator(result.transactionHash)
resolve(resultToRemixTx(result, execResult))
} catch (e) { } catch (e) {
reject(e) reject(e)
} }
@ -429,19 +431,24 @@ class Blockchain {
function runTransaction (fromAddress, value, gasLimit, next) { 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 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 } 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) tx.timestamp = Date.now()
if (tx.timestamp) {
timestamp = tx.timestamp
}
const timestamp = tx.timestamp
self.event.trigger('initiatingTransaction', [timestamp, tx, payLoad]) self.event.trigger('initiatingTransaction', [timestamp, tx, payLoad])
self.txRunner.rawRun(tx, confirmationCb, continueCb, promptCb, self.txRunner.rawRun(tx, confirmationCb, continueCb, promptCb,
function (error, result) { async (error, result) => {
if (error) return next(error) 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') 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 && (typeof (error) !== 'string')) {
if (error.message) error = error.message if (error.message) error = error.message
@ -454,25 +461,29 @@ class Blockchain {
) )
} }
], ],
(error, txResult) => { async (error, txResult) => {
if (error) { if (error) {
return cb(error) return cb(error)
} }
const isVM = this.executionContext.isVM() const isVM = this.executionContext.isVM()
let execResult
let returnValue = null
if (isVM) { if (isVM) {
const vmError = txExecution.checkVMError(txResult) execResult = await this.web3().eth.getExecutionResultFromSimulator(txResult.transactionHash)
if (vmError.error) { if (execResult) {
return cb(vmError.message) // 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 address = null
let returnValue = null if (txResult && txResult.receipt) {
if (txResult && txResult.result) { address = txResult.receipt.contractAddress
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
} }
cb(error, txResult, address, returnValue) cb(error, txResult, address, returnValue)

@ -1,139 +1,33 @@
/* global ethereum */ /* global ethereum */
'use strict' 'use strict'
import Web3 from 'web3' import Web3 from 'web3'
import { EventManager } from '../eventManager' import EventManager from '../lib/events'
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'
declare let ethereum: any
let web3 let web3
if (typeof window !== 'undefined' && typeof window['ethereum'] !== 'undefined') { if (typeof window !== 'undefined' && typeof window.ethereum !== 'undefined') {
var injectedProvider = window['ethereum'] var injectedProvider = window.ethereum
web3 = new Web3(injectedProvider) web3 = new Web3(injectedProvider)
} else { } else {
web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) 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<StorageDump>((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<Buffer> {
await this._cache.flush()
const stateRoot = this._trie.root
return stateRoot
}
async setStateRoot (stateRoot: Buffer): Promise<void> {
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 trigger contextChanged, web3EndpointChanged
*/ */
export class ExecutionContext { export class ExecutionContext {
event
logsManager
blockGasLimitDefault
blockGasLimit
customNetWorks
blocks
latestBlockNumber
txs
executionContext
listenOnLastBlockId
currentFork: string
vms
mainNetGenesisHash: string
constructor () { constructor () {
this.event = new EventManager() this.event = new EventManager()
this.logsManager = new LogsManager()
this.executionContext = null this.executionContext = null
this.blockGasLimitDefault = 4300000 this.blockGasLimitDefault = 4300000
this.blockGasLimit = this.blockGasLimitDefault this.blockGasLimit = this.blockGasLimitDefault
this.currentFork = 'berlin' 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.mainNetGenesisHash = '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3'
this.customNetWorks = {} this.customNetWorks = {}
this.blocks = {} this.blocks = {}
this.latestBlockNumber = 0 this.latestBlockNumber = 0
this.txs = {} this.txs = {}
this.customWeb3 = {} // mapping between a context name and a web3.js instance
} }
init (config) { 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 () { askPermission () {
// metamask // metamask
if (ethereum && typeof ethereum.enable === 'function') ethereum.enable() if (ethereum && typeof ethereum.enable === 'function') ethereum.enable()
@ -172,7 +52,12 @@ export class ExecutionContext {
return this.executionContext === 'vm' return this.executionContext === 'vm'
} }
setWeb3 (context, web3) {
this.customWeb3[context] = web3
}
web3 () { web3 () {
if (this.customWeb3[this.executionContext]) return this.customWeb3[this.executionContext]
return this.isVM() ? this.vms[this.currentFork].web3vm : web3 return this.isVM() ? this.vms[this.currentFork].web3vm : web3
} }
@ -228,14 +113,6 @@ export class ExecutionContext {
return new Web3() return new Web3()
} }
vm () {
return this.vms[this.currentFork].vm
}
vmObject () {
return this.vms[this.currentFork]
}
setContext (context, endPointUrl, confirmCb, infoCb) { setContext (context, endPointUrl, confirmCb, infoCb) {
this.executionContext = context this.executionContext = context
this.executionContextChange(context, endPointUrl, confirmCb, infoCb, null) this.executionContextChange(context, endPointUrl, confirmCb, infoCb, null)
@ -340,22 +217,4 @@ export class ExecutionContext {
return transactionDetailsLinks[network] + hash 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
}
} }

@ -1,14 +1,16 @@
const Web3 = require('web3') const Web3 = require('web3')
const { BN, privateToAddress, hashPersonalMessage } = require('ethereumjs-util') const { BN, privateToAddress, hashPersonalMessage } = require('ethereumjs-util')
const RemixSimulator = require('@remix-project/remix-simulator') const { Provider, extend } = require('@remix-project/remix-simulator')
class VMProvider { class VMProvider {
constructor (executionContext) { constructor (executionContext) {
this.executionContext = executionContext this.executionContext = executionContext
this.RemixSimulatorProvider = new RemixSimulator.Provider({ executionContext: this.executionContext }) this.RemixSimulatorProvider = new Provider({})
this.RemixSimulatorProvider.init() this.RemixSimulatorProvider.init()
this.web3 = new Web3(this.RemixSimulatorProvider) this.web3 = new Web3(this.RemixSimulatorProvider)
extend(this.web3)
this.accounts = {} this.accounts = {}
this.executionContext.setWeb3('vm', this.web3)
} }
getAccounts (cb) { getAccounts (cb) {

@ -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
}

@ -36,14 +36,12 @@ module.exports = {
async.whilst( async.whilst(
() => { return exist }, () => { return exist },
(callback) => { (callback) => {
fileProvider.exists(name + counter + prefix + '.' + ext, (error, currentExist) => { fileProvider.exists(name + counter + prefix + '.' + ext).then(currentExist => {
if (error) { exist = currentExist
callback(error) if (exist) counter = (counter | 0) + 1
} else { callback()
exist = currentExist }).catch(error => {
if (exist) counter = (counter | 0) + 1 if (error) console.log(error)
callback()
}
}) })
}, },
(error) => { cb(error, name + counter + prefix + '.' + ext) } (error) => { cb(error, name + counter + prefix + '.' + ext) }
@ -52,6 +50,27 @@ module.exports = {
createNonClashingName (name, fileProvider, cb) { createNonClashingName (name, fileProvider, cb) {
this.createNonClashingNameWithPrefix(name, fileProvider, '', cb) this.createNonClashingNameWithPrefix(name, fileProvider, '', cb)
}, },
async createNonClashingNameAsync (name, fileManager, prefix = '') {
if (!name) name = 'Undefined'
let counter = ''
let ext = 'sol'
const reg = /(.*)\.([^.]+)/g
const split = reg.exec(name)
if (split) {
name = split[1]
ext = split[2]
}
let exist = true
do {
const isDuplicate = await fileManager.exists(name + counter + prefix + '.' + ext)
if (isDuplicate) counter = (counter | 0) + 1
else exist = false
} while (exist)
return name + counter + prefix + '.' + ext
},
checkSpecialChars (name) { checkSpecialChars (name) {
return name.match(/[:*?"<>\\'|]/) != null return name.match(/[:*?"<>\\'|]/) != null
}, },

@ -11,10 +11,10 @@ const requiredModules = [ // services + layout views + system views
'fileManager', 'contentImport', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons', 'fileManager', 'contentImport', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons',
'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp'] '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) { 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) return nativePlugins.includes(name) || requiredModules.includes(name)
} }

@ -5,5 +5,18 @@ module.exports = {
}, },
resolver: '@nrwl/jest/plugins/resolver', resolver: '@nrwl/jest/plugins/resolver',
moduleFileExtensions: ['ts', 'js', 'html'], moduleFileExtensions: ['ts', 'js', 'html'],
coverageReporters: ['html'] coverageReporters: ['html'],
moduleNameMapper:{
"@remix-project/remix-analyzer": "<rootDir>/../../dist/libs/remix-analyzer/index.js",
"@remix-project/remix-astwalker": "<rootDir>/../../dist/libs/remix-astwalker/index.js",
"@remix-project/remix-debug": "<rootDir>/../../dist/libs/remix-debug/src/index.js",
"@remix-project/remix-lib": "<rootDir>/../../dist/libs/remix-lib/src/index.js",
"@remix-project/remix-simulator": "<rootDir>/../../dist/libs/remix-simulator/src/index.js",
"@remix-project/remix-solidity": "<rootDir>/../../dist/libs/remix-solidity/index.js",
"@remix-project/remix-tests": "<rootDir>/../../dist/libs/remix-tests/src/index.js",
"@remix-project/remix-url-resolver":
"<rootDir>/../../dist/libs/remix-url-resolver/index.js"
,
"@remix-project/remixd": "<rootDir>/../../dist/libs/remixd/index.js"
}
}; };

@ -104,9 +104,10 @@ export class Ethdebugger {
const stack = this.traceManager.getStackAt(step) const stack = this.traceManager.getStackAt(step)
const memory = this.traceManager.getMemoryAt(step) const memory = this.traceManager.getMemoryAt(step)
const address = this.traceManager.getCurrentCalledAddressAt(step) const address = this.traceManager.getCurrentCalledAddressAt(step)
const calldata = this.traceManager.getCallDataAt(step)
try { try {
const storageViewer = new StorageViewer({ stepIndex: step, tx: this.tx, address: address }, this.storageResolver, this.traceManager) const storageViewer = new StorageViewer({ stepIndex: step, tx: this.tx, address: address }, this.storageResolver, this.traceManager)
const locals = await localDecoder.solidityLocals(step, this.callTree, stack, memory, storageViewer, sourceLocation, null) const locals = await localDecoder.solidityLocals(step, this.callTree, stack, memory, storageViewer, calldata, sourceLocation, null)
if (locals['error']) { if (locals['error']) {
return callback(locals['error']) return callback(locals['error'])
} }

@ -62,6 +62,14 @@ export class DebuggerSolidityLocals {
} catch (error) { } catch (error) {
next(error) next(error)
} }
},
function getCallDataAt (stepIndex, next) {
try {
const calldata = self.traceManager.getCallDataAt(stepIndex)
next(null, calldata)
} catch (error) {
next(error)
}
}], }],
this.stepManager.currentStepIndex, this.stepManager.currentStepIndex,
(error, result) => { (error, result) => {
@ -70,9 +78,10 @@ export class DebuggerSolidityLocals {
} }
var stack = result[0].value var stack = result[0].value
var memory = result[1].value var memory = result[1].value
var calldata = result[3].value
try { try {
var storageViewer = new StorageViewer({ stepIndex: this.stepManager.currentStepIndex, tx: this.tx, address: result[2].value }, this.storageResolver, this.traceManager) var storageViewer = new StorageViewer({ stepIndex: this.stepManager.currentStepIndex, tx: this.tx, address: result[2].value }, this.storageResolver, this.traceManager)
solidityLocals(this.stepManager.currentStepIndex, this.internalTreeCall, stack, memory, storageViewer, sourceLocation, cursor).then((locals) => { solidityLocals(this.stepManager.currentStepIndex, this.internalTreeCall, stack, memory, storageViewer, calldata, sourceLocation, cursor).then((locals) => {
if (!cursor) { if (!cursor) {
if (!locals['error']) { if (!locals['error']) {
this.event.trigger('solidityLocals', [locals]) this.event.trigger('solidityLocals', [locals])

@ -108,7 +108,8 @@ export class InternalCallTree {
} }
parentScope (scopeId) { parentScope (scopeId) {
return scopeId.replace(/(.\d|\d)$/, '') if (scopeId.indexOf('.') === -1) return ''
return scopeId.replace(/(\.\d+)$/, '')
} }
findScopeId (vmtraceIndex) { findScopeId (vmtraceIndex) {
@ -309,10 +310,10 @@ async function includeVariableDeclaration (tree, step, sourceLocation, scopeId,
// } // }
// input params // input params
if (inputs && inputs.parameters) { if (inputs && inputs.parameters) {
functionDefinitionAndInputs.inputs = addParams(inputs, tree, scopeId, states, contractObj.name, previousSourceLocation, stack.length, inputs.parameters.length, -1) functionDefinitionAndInputs.inputs = addParams(inputs, tree, scopeId, states, contractObj, previousSourceLocation, stack.length, inputs.parameters.length, -1)
} }
// output params // output params
if (outputs) addParams(outputs, tree, scopeId, states, contractObj.name, previousSourceLocation, stack.length, 0, 1) if (outputs) addParams(outputs, tree, scopeId, states, contractObj, previousSourceLocation, stack.length, 0, 1)
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@ -372,7 +373,8 @@ function extractFunctionDefinitions (ast, astWalker) {
return ret return ret
} }
function addParams (parameterList, tree, scopeId, states, contractName, sourceLocation, stackLength, stackPosition, dir) { function addParams (parameterList, tree, scopeId, states, contractObj, sourceLocation, stackLength, stackPosition, dir) {
const contractName = contractObj.name
const params = [] const params = []
for (const inputParam in parameterList.parameters) { for (const inputParam in parameterList.parameters) {
const param = parameterList.parameters[inputParam] const param = parameterList.parameters[inputParam]
@ -385,7 +387,8 @@ function addParams (parameterList, tree, scopeId, states, contractName, sourceLo
name: attributesName, name: attributesName,
type: parseType(param.typeDescriptions.typeString, states, contractName, location), type: parseType(param.typeDescriptions.typeString, states, contractName, location),
stackDepth: stackDepth, stackDepth: stackDepth,
sourceLocation: sourceLocation sourceLocation: sourceLocation,
abi: contractObj.contract.abi
} }
params.push(attributesName) params.push(attributesName)
} }

@ -1,6 +1,6 @@
'use strict' 'use strict'
export async function solidityLocals (vmtraceIndex, internalTreeCall, stack, memory, storageResolver, currentSourceLocation, cursor) { export async function solidityLocals (vmtraceIndex, internalTreeCall, stack, memory, storageResolver, calldata, currentSourceLocation, cursor) {
const scope = internalTreeCall.findScope(vmtraceIndex) const scope = internalTreeCall.findScope(vmtraceIndex)
if (!scope) { if (!scope) {
const error = { message: 'Can\'t display locals. reason: compilation result might not have been provided' } const error = { message: 'Can\'t display locals. reason: compilation result might not have been provided' }
@ -18,7 +18,7 @@ export async function solidityLocals (vmtraceIndex, internalTreeCall, stack, mem
anonymousIncr++ anonymousIncr++
} }
try { try {
locals[name] = await variable.type.decodeFromStack(variable.stackDepth, stack, memory, storageResolver, cursor) locals[name] = await variable.type.decodeFromStack(variable.stackDepth, stack, memory, storageResolver, calldata, cursor, variable)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
locals[name] = '<decoding failed - ' + e.message + '>' locals[name] = '<decoding failed - ' + e.message + '>'

@ -1,4 +1,5 @@
'use strict' 'use strict'
import { ethers } from 'ethers'
import { toBN } from './util' import { toBN } from './util'
export class RefType { export class RefType {
@ -7,6 +8,7 @@ export class RefType {
storageBytes storageBytes
typeName typeName
basicType basicType
underlyingType
constructor (storageSlots, storageBytes, typeName, location) { constructor (storageSlots, storageBytes, typeName, location) {
this.location = location this.location = location
@ -33,7 +35,7 @@ export class RefType {
* @param {Object} - storageResolver * @param {Object} - storageResolver
* @return {Object} decoded value * @return {Object} decoded value
*/ */
async decodeFromStack (stackDepth, stack, memory, storageResolver, cursor): Promise<any> { async decodeFromStack (stackDepth, stack, memory, storageResolver, calldata, cursor, variableDetails?): Promise<any> {
if (stack.length - 1 < stackDepth) { if (stack.length - 1 < stackDepth) {
return { error: '<decoding failed - stack underflow ' + stackDepth + '>', type: this.typeName } return { error: '<decoding failed - stack underflow ' + stackDepth + '>', type: this.typeName }
} }
@ -49,6 +51,26 @@ export class RefType {
} else if (this.isInMemory()) { } else if (this.isInMemory()) {
offset = parseInt(offset, 16) offset = parseInt(offset, 16)
return this.decodeFromMemoryInternal(offset, memory, cursor) return this.decodeFromMemoryInternal(offset, memory, cursor)
} else if (this.isInCallData()) {
calldata = calldata.length > 0 ? calldata[0] : '0x'
const ethersAbi = new ethers.utils.Interface(variableDetails.abi)
const fnSign = calldata.substr(0, 10)
const decodedData = ethersAbi.decodeFunctionData(ethersAbi.getFunction(fnSign), calldata)
let decodedValue = decodedData[variableDetails.name]
const isArray = Array.isArray(decodedValue)
if (isArray) {
decodedValue = decodedValue.map((el) => {
return {
value: el.toString(),
type: this.underlyingType.typeName
}
})
}
return {
length: Array.isArray(decodedValue) ? '0x' + decodedValue.length.toString(16) : undefined,
value: decodedValue,
type: this.typeName
}
} else { } else {
return { error: '<decoding failed - no decoder for ' + this.location + '>', type: this.typeName } return { error: '<decoding failed - no decoder for ' + this.location + '>', type: this.typeName }
} }
@ -84,4 +106,13 @@ export class RefType {
isInMemory () { isInMemory () {
return this.location.indexOf('memory') === 0 return this.location.indexOf('memory') === 0
} }
/**
* current type defined in storage
*
* @return {Bool} - return true if the type is defined in the storage
*/
isInCallData () {
return this.location.indexOf('calldata') === 0
}
} }

@ -20,9 +20,9 @@ export class StringType extends DynamicByteArray {
return format(decoded) return format(decoded)
} }
async decodeFromStack (stackDepth, stack, memory) { async decodeFromStack (stackDepth, stack, memory, calldata, variableDetails?) {
try { try {
return await super.decodeFromStack(stackDepth, stack, memory, null, null) return await super.decodeFromStack(stackDepth, stack, memory, null, calldata, variableDetails)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
return '<decoding failed - ' + e.message + '>' return '<decoding failed - ' + e.message + '>'

@ -43,7 +43,7 @@ export class ValueType {
* @param {String} - memory * @param {String} - memory
* @return {Object} - decoded value * @return {Object} - decoded value
*/ */
async decodeFromStack (stackDepth, stack, memory) { async decodeFromStack (stackDepth, stack, memory, calldata, variableDetails?) {
let value let value
if (stackDepth >= stack.length) { if (stackDepth >= stack.length) {
value = this.decodeValue('') value = this.decodeValue('')

@ -22,13 +22,21 @@ export function decodeLocals (st, index, traceManager, callTree, verifier) {
} catch (error) { } catch (error) {
callback(error) callback(error)
} }
},
function getCallDataAt (stepIndex, callback) {
try {
const result = traceManager.getCallDataAt(stepIndex)
callback(null, result)
} catch (error) {
callback(error)
}
}], }],
index, index,
function (error, result) { function (error, result) {
if (error) { if (error) {
return st.fail(error) return st.fail(error)
} }
solidityLocals(index, callTree, result[0].value, result[1].value, {}, { start: 5000 }, null).then((locals) => { solidityLocals(index, callTree, result[0].value, result[1].value, {}, result[2].value, { start: 5000 }, null).then((locals) => {
verifier(locals) verifier(locals)
}) })
}) })

@ -53,10 +53,10 @@ export function callFunction (from, to, data, value, gasLimit, funAbi, txRunner,
/** /**
* check if the vm has errored * 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 } * @return {Object} - { error: true/false, message: DOMNode }
*/ */
export function checkVMError (txResult) { export function checkVMError (execResult) {
const errorCode = { const errorCode = {
OUT_OF_GAS: 'out of gas', OUT_OF_GAS: 'out of gas',
STACK_UNDERFLOW: 'stack underflow', STACK_UNDERFLOW: 'stack underflow',
@ -74,10 +74,10 @@ export function checkVMError (txResult) {
error: false, error: false,
message: '' message: ''
} }
if (!txResult.result.execResult.exceptionError) { if (!execResult.exceptionError) {
return ret return ret
} }
const exceptionError = txResult.result.execResult.exceptionError.error || '' const exceptionError = execResult.exceptionError.error || ''
const error = `VM error: ${exceptionError}.\n` const error = `VM error: ${exceptionError}.\n`
let msg let msg
if (exceptionError === errorCode.INVALID_OPCODE) { 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' msg = '\tThe transaction ran out of gas. Please increase the Gas Limit.\n'
ret.error = true ret.error = true
} else if (exceptionError === errorCode.REVERT) { } else if (exceptionError === errorCode.REVERT) {
const returnData = txResult.result.execResult.returnValue const returnData = execResult.returnValue
// It is the hash of Error(string) // It is the hash of Error(string)
if (returnData && (returnData.slice(0, 4).toString('hex') === '08c379a0')) { if (returnData && (returnData.slice(0, 4).toString('hex') === '08c379a0')) {
const abiCoder = new ethers.utils.AbiCoder() const abiCoder = new ethers.utils.AbiCoder()

@ -314,7 +314,7 @@ export function deployLibrary (libraryName, libraryShortName, library, contracts
if (err) { if (err) {
return callback(err) return callback(err)
} }
const address = txResult.result.createdAddress || txResult.result.contractAddress const address = txResult.receipt.contractAddress
library.address = address library.address = address
callback(err, address) callback(err, address)
}) })

@ -4,17 +4,16 @@ import { ethers } from 'ethers'
import { toBuffer } from 'ethereumjs-util' import { toBuffer } from 'ethereumjs-util'
import { EventManager } from '../eventManager' import { EventManager } from '../eventManager'
import { compareByteCode } from '../util' import { compareByteCode } from '../util'
import { ExecutionContext } from './execution-context'
import { decodeResponse } from './txFormat' import { decodeResponse } from './txFormat'
import { getFunction, getReceiveInterface, getConstructorInterface, visitContracts, makeFullTypeDefinition } from './txHelper' import { getFunction, getReceiveInterface, getConstructorInterface, visitContracts, makeFullTypeDefinition } from './txHelper'
function addExecutionCosts (txResult, tx) { function addExecutionCosts (txResult, tx, execResult) {
if (txResult && txResult.result) { if (txResult) {
if (txResult.result.execResult) { if (execResult) {
tx.returnValue = txResult.result.execResult.returnValue tx.returnValue = execResult.returnValue
if (txResult.result.execResult.gasUsed) tx.executionCost = txResult.result.execResult.gasUsed.toString(10) 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) { constructor (opt, executionContext) {
this.event = new EventManager() this.event = new EventManager()
// has a default for now for backwards compatability // has a default for now for backwards compatability
this.executionContext = executionContext || new ExecutionContext() this.executionContext = executionContext
this._api = opt.api this._api = opt.api
this._resolvedTransactions = {} this._resolvedTransactions = {}
this._resolvedContracts = {} 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 if (error) return
// we go for that case if // we go for that case if
// in VM mode // in VM mode
@ -63,17 +62,25 @@ export class TxListener {
if (!this._isListening) return // we don't listen 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 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 = { const call = {
from: from, from: from,
to: to, to: to,
input: data, input: data,
hash: txResult.transactionHash ? txResult.transactionHash : 'call' + (from || '') + to + data, hash: txResult.transactionHash ? txResult.transactionHash : 'call' + (from || '') + to + data,
isCall: true, isCall: true,
returnValue: this.executionContext.isVM() ? txResult.result.execResult.returnValue : toBuffer(txResult.result), returnValue,
envMode: this.executionContext.getProvider() envMode: this.executionContext.getProvider()
} }
addExecutionCosts(txResult, call) addExecutionCosts(txResult, call, execResult)
this._resolveTx(call, call, (error, resolvedData) => { this._resolveTx(call, call, (error, resolvedData) => {
if (!error) { if (!error) {
this.event.trigger('newCall', [call]) this.event.trigger('newCall', [call])
@ -89,12 +96,17 @@ export class TxListener {
// in web3 mode && listen remix txs only // in web3 mode && listen remix txs only
if (!this._isListening) return // we don't listen 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 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) 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.envMode = this.executionContext.getProvider()
tx.status = txResult.result.status // 0x0 or 0x1 tx.status = txResult.receipt.status // 0x0 or 0x1
this._resolve([tx], () => { this._resolve([tx], () => {
}) })
}) })

@ -1,91 +1,26 @@
'use strict' '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' import { EventManager } from '../eventManager'
export class TxRunner { export class TxRunner {
event event
executionContext
_api
blockNumber
runAsync runAsync
pendingTxs pendingTxs
vmaccounts
queusTxs queusTxs
blocks opt
commonContext internalRunner
constructor (internalRunner, opt) {
constructor (vmaccounts, api, executionContext) { this.opt = opt || {}
this.internalRunner = internalRunner
this.event = new EventManager() this.event = new EventManager()
// has a default for now for backwards compatability
this.executionContext = executionContext || new ExecutionContext() 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.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.pendingTxs = {} this.pendingTxs = {}
this.vmaccounts = vmaccounts
this.queusTxs = [] this.queusTxs = []
this.blocks = []
} }
rawRun (args, confirmationCb, gasEstimationForceSend, promptCb, cb) { rawRun (args, confirmationCb, gasEstimationForceSend, promptCb, cb) {
let timestamp = Date.now() run(this, args, args.timestamp || Date.now(), confirmationCb, gasEstimationForceSend, promptCb, cb)
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. `)
}
} }
execute (args, confirmationCb, gasEstimationForceSend, promptCb, callback) { execute (args, confirmationCb, gasEstimationForceSend, promptCb, callback) {
@ -93,175 +28,10 @@ export class TxRunner {
if (data.slice(0, 2) !== '0x') { if (data.slice(0, 2) !== '0x') {
data = '0x' + data data = '0x' + data
} }
this.internalRunner.execute(args, confirmationCb, gasEstimationForceSend, promptCb, callback)
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
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.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)
}
})
})
} }
} }
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) { function run (self, tx, stamp, confirmationCb, gasEstimationForceSend = null, promptCb = null, callback = null) {
if (!self.runAsync && Object.keys(self.pendingTxs).length) { if (!self.runAsync && Object.keys(self.pendingTxs).length) {
return self.queusTxs.push({ tx, stamp, callback }) return self.queusTxs.push({ tx, stamp, callback })

@ -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)
})
}
}

@ -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) }) }

@ -20,9 +20,9 @@ function convertToPrefixedHex (input) {
Also, VM results use BN and Buffers, Node results use hex strings/ints, Also, VM results use BN and Buffers, Node results use hex strings/ints,
So we need to normalize the values to prefixed hex strings So we need to normalize the values to prefixed hex strings
*/ */
export function resultToRemixTx (txResult) { export function resultToRemixTx (txResult, execResult) {
const { result, transactionHash } = txResult const { receipt, transactionHash, result } = txResult
const { status, execResult, gasUsed, createdAddress, contractAddress } = result const { status, gasUsed, contractAddress } = receipt
let returnValue, errorMessage let returnValue, errorMessage
if (isHexString(result)) { if (isHexString(result)) {
@ -38,6 +38,6 @@ export function resultToRemixTx (txResult) {
gasUsed: convertToPrefixedHex(gasUsed), gasUsed: convertToPrefixedHex(gasUsed),
error: errorMessage, error: errorMessage,
return: convertToPrefixedHex(returnValue), return: convertToPrefixedHex(returnValue),
createdAddress: convertToPrefixedHex(createdAddress || contractAddress) createdAddress: convertToPrefixedHex(contractAddress)
} }
} }

@ -12,9 +12,11 @@ import * as txHelper from './execution/txHelper'
import * as txFormat from './execution/txFormat' import * as txFormat from './execution/txFormat'
import { TxListener } from './execution/txListener' import { TxListener } from './execution/txListener'
import { TxRunner } from './execution/txRunner' import { TxRunner } from './execution/txRunner'
import { ExecutionContext } from './execution/execution-context' import { LogsManager } from './execution/logsManager'
import * as typeConversion from './execution/typeConversion' 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() export = modules()
@ -23,7 +25,8 @@ function modules () {
EventManager: EventManager, EventManager: EventManager,
helpers: { helpers: {
ui: uiHelper, ui: uiHelper,
compiler: compilerHelper compiler: compilerHelper,
txResultHelper
}, },
vm: { vm: {
Web3Providers: Web3Providers, Web3Providers: Web3Providers,
@ -36,12 +39,13 @@ function modules () {
EventsDecoder: EventsDecoder, EventsDecoder: EventsDecoder,
txExecution: txExecution, txExecution: txExecution,
txHelper: txHelper, txHelper: txHelper,
executionContext: new ExecutionContext(),
txFormat: txFormat, txFormat: txFormat,
txListener: TxListener, txListener: TxListener,
txRunner: TxRunner, TxRunner: TxRunner,
typeConversion: typeConversion TxRunnerWeb3: TxRunnerWeb3,
}, TxRunnerVM: TxRunnerVM,
UniversalDApp: UniversalDApp typeConversion: typeConversion,
LogsManager
}
} }
} }

@ -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)
}
}

@ -31,6 +31,9 @@ export class Web3VmProvider {
toBigNumber toBigNumber
isAddress isAddress
utils utils
txsMapBlock
blocks
latestBlockNumber
constructor () { constructor () {
this.web3 = new Web3() this.web3 = new Web3()
@ -69,6 +72,9 @@ export class Web3VmProvider {
this.toBigNumber = (...args) => this.web3.utils.toBN(...args) this.toBigNumber = (...args) => this.web3.utils.toBN(...args)
this.isAddress = (...args) => this.web3.utils.isAddress(...args) this.isAddress = (...args) => this.web3.utils.isAddress(...args)
this.utils = Web3.utils || [] this.utils = Web3.utils || []
this.txsMapBlock = {}
this.blocks = {}
this.latestBlockNumber = 0
} }
setVM (vm) { setVM (vm) {

@ -5,8 +5,6 @@ import * as txHelper from '../src/execution/txHelper'
import { hexToIntArray } from '../src/util' import { hexToIntArray } from '../src/util'
let compiler = require('solc') let compiler = require('solc')
import { compilerInput } from '../src/helpers/compilerHelper' import { compilerInput } from '../src/helpers/compilerHelper'
import { ExecutionContext } from '../src/execution/execution-context'
const executionContext = new ExecutionContext()
const solidityVersion = 'v0.6.0+commit.26b70077' const solidityVersion = 'v0.6.0+commit.26b70077'
/* tape *********************************************************** */ /* tape *********************************************************** */
@ -151,7 +149,6 @@ function testInvalidTupleInput (st, params) {
/* tape *********************************************************** */ /* tape *********************************************************** */
tape('ContractParameters - (TxFormat.buildData) - link Libraries', function (t) { tape('ContractParameters - (TxFormat.buildData) - link Libraries', function (t) {
executionContext.setContext('vm', null, null, null)
const compileData = compiler.compile(compilerInput(deploySimpleLib)) const compileData = compiler.compile(compilerInput(deploySimpleLib))
const fakeDeployedContracts = { const fakeDeployedContracts = {
@ -161,8 +158,8 @@ tape('ContractParameters - (TxFormat.buildData) - link Libraries', function (t)
} }
const callbackDeployLibraries = (param, callback) => { const callbackDeployLibraries = (param, callback) => {
callback(null, { callback(null, {
result: { receipt: {
createdAddress: fakeDeployedContracts[param.data.contractName] contractAddress: fakeDeployedContracts[param.data.contractName]
} }
}) })
} // fake } // fake

@ -18,12 +18,13 @@ const GAS_USED_INT = 75427
const GAS_USED_HEX = '0x126a3' const GAS_USED_HEX = '0x126a3'
const NODE_CALL_RESULT = { const NODE_CALL_RESULT = {
receipt: {},
result: RETURN_VALUE_HEX, result: RETURN_VALUE_HEX,
transactionHash: undefined transactionHash: undefined
} }
const NODE_TX_RESULT = { const NODE_TX_RESULT = {
result: { receipt: {
blockHash: '0x380485a4e6372a42e36489783c7f7cb66257612133cd245859c206fd476e9c44', blockHash: '0x380485a4e6372a42e36489783c7f7cb66257612133cd245859c206fd476e9c44',
blockNumber: 5994, blockNumber: 5994,
contractAddress: CONTRACT_ADDRESS_HEX, contractAddress: CONTRACT_ADDRESS_HEX,
@ -39,26 +40,31 @@ const NODE_TX_RESULT = {
} }
const VM_RESULT = { const VM_RESULT = {
result: { receipt: {
amountSpent: new BN(1), amountSpent: new BN(1),
createdAddress: CONTRACT_ADDRESS_BUFFER, contractAddress: CONTRACT_ADDRESS_BUFFER,
gasRefund: new BN(0), gasRefund: new BN(0),
gasUsed: new BN(GAS_USED_INT), gasUsed: new BN(GAS_USED_INT),
status: STATUS_OK, status: STATUS_OK,
execResult: {
exceptionError: null,
gasRefund: new BN(0),
gasUsed: new BN(GAS_USED_INT),
returnValue: RETURN_VALUE_BUFFER
}
}, },
transactionHash: TRANSACTION_HASH 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) { tape('converts node transaction result to RemixTx', function (t) {
// contract creation // contract creation
let txResult = { ...NODE_TX_RESULT } let txResult = { ...NODE_TX_RESULT }
let remixTx = resultToRemixTx(txResult) let remixTx = resultToRemixTx(txResult, {})
t.equal(remixTx.transactionHash, TRANSACTION_HASH) t.equal(remixTx.transactionHash, TRANSACTION_HASH)
t.equal(remixTx.createdAddress, CONTRACT_ADDRESS_HEX) 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) t.equal(remixTx.error, undefined)
// contract method tx // contract method tx
txResult.result.contractAddress = null txResult.receipt.contractAddress = null
remixTx = resultToRemixTx(txResult) remixTx = resultToRemixTx(txResult, {})
t.equal(remixTx.createdAddress, null) t.equal(remixTx.createdAddress, null)
t.end() t.end()
@ -77,7 +83,7 @@ tape('converts node transaction result to RemixTx', function (t) {
tape('converts node call result to RemixTx', function (t) { tape('converts node call result to RemixTx', function (t) {
let txResult = { ...NODE_CALL_RESULT } let txResult = { ...NODE_CALL_RESULT }
let remixTx = resultToRemixTx(txResult) let remixTx = resultToRemixTx(txResult, {})
t.equal(remixTx.transactionHash, undefined) t.equal(remixTx.transactionHash, undefined)
t.equal(remixTx.createdAddress, 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) { tape('converts VM result to RemixTx', function (t) {
let txResult = { ...VM_RESULT } let txResult = { ...VM_RESULT }
let remixTx = resultToRemixTx(txResult) let remixTx = resultToRemixTx(txResult, EXEC_RESULT)
t.equal(remixTx.transactionHash, t.equal(remixTx.transactionHash,
TRANSACTION_HASH) TRANSACTION_HASH)
@ -101,8 +107,7 @@ tape('converts VM result to RemixTx', function (t) {
t.equal(remixTx.return, RETURN_VALUE_HEX) t.equal(remixTx.return, RETURN_VALUE_HEX)
t.equal(remixTx.error, null) t.equal(remixTx.error, null)
txResult.result.execResult.exceptionError = 'this is an error' remixTx = resultToRemixTx(VM_RESULT, EXEC_RESULT_ERROR)
remixTx = resultToRemixTx(txResult)
t.equal(remixTx.error, 'this is an error') t.equal(remixTx.error, 'this is an error')
t.end() t.end()

@ -14,7 +14,7 @@
], ],
"main": "src/index.js", "main": "src/index.js",
"dependencies": { "dependencies": {
"@remix-project/remix-lib": "^0.4.34", "@remix-project/remix-lib": "../remix-lib",
"ansi-gray": "^0.1.1", "ansi-gray": "^0.1.1",
"async": "^3.1.0", "async": "^3.1.0",
"body-parser": "^1.18.2", "body-parser": "^1.18.2",

@ -1,7 +1,7 @@
import { Block } from '@ethereumjs/block' import { Block } from '@ethereumjs/block'
import { BN } from 'ethereumjs-util' import { BN } from 'ethereumjs-util'
export function generateBlock (executionContext) { export function generateBlock (vmContext) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const block: Block = Block.fromBlockData({ const block: Block = Block.fromBlockData({
header: { header: {
@ -11,10 +11,10 @@ export function generateBlock (executionContext) {
difficulty: new BN('69762765929000', 10), difficulty: new BN('69762765929000', 10),
gasLimit: new BN('8000000').imuln(1) 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(() => { vmContext.vm().runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false }).then(() => {
executionContext.addBlock(block) vmContext.addBlock(block)
resolve({}) resolve({})
}).catch((e) => reject(e)) }).catch((e) => reject(e))
}) })

@ -1 +1 @@
export { Provider } from './provider' export { Provider, extend } from './provider'

@ -6,22 +6,20 @@ export class Accounts {
web3 web3
accounts: Record<string, unknown> accounts: Record<string, unknown>
accountsKeys: Record<string, unknown> accountsKeys: Record<string, unknown>
executionContext vmContext
constructor (executionContext) { constructor (vmContext) {
this.web3 = new Web3() this.web3 = new Web3()
this.executionContext = executionContext this.vmContext = vmContext
// TODO: make it random and/or use remix-libs // TODO: make it random and/or use remix-libs
this.accounts = {} this.accounts = {}
this.accountsKeys = {} this.accountsKeys = {}
this.executionContext.init({ get: () => { return true } })
} }
async resetAccounts (): Promise<void> { async resetAccounts (): Promise<void> {
// TODO: setting this to {} breaks the app currently, unclear why still this.accounts = {}
// this.accounts = {} this.accountsKeys = {}
// this.accountsKeys = {}
await this._addAccount('503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb', '0x56BC75E2D63100000') await this._addAccount('503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb', '0x56BC75E2D63100000')
await this._addAccount('7e5bfb82febc4c2c8529167104271ceec190eafdca277314912eaabdb67c6e5f', '0x56BC75E2D63100000') await this._addAccount('7e5bfb82febc4c2c8529167104271ceec190eafdca277314912eaabdb67c6e5f', '0x56BC75E2D63100000')
await this._addAccount('cc6d63f85de8fef05446ebdd3c537c72152d0fc437fd7aa62b3019b79bd1fdd4', '0x56BC75E2D63100000') await this._addAccount('cc6d63f85de8fef05446ebdd3c537c72152d0fc437fd7aa62b3019b79bd1fdd4', '0x56BC75E2D63100000')
@ -47,7 +45,7 @@ export class Accounts {
this.accounts[addressStr] = { privateKey, nonce: 0 } this.accounts[addressStr] = { privateKey, nonce: 0 }
this.accountsKeys[addressStr] = '0x' + privateKey.toString('hex') 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) => { stateManager.getAccount(Address.fromString(addressStr)).then((account) => {
account.balance = new BN(balance.replace('0x', '') || 'f00000000000000001', 16) account.balance = new BN(balance.replace('0x', '') || 'f00000000000000001', 16)
stateManager.putAccount(Address.fromString(addressStr), account).catch((error) => { stateManager.putAccount(Address.fromString(addressStr), account).catch((error) => {
@ -85,7 +83,7 @@ export class Accounts {
eth_getBalance (payload, cb) { eth_getBalance (payload, cb) {
const address = payload.params[0] 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)) cb(null, new BN(account.balance).toString(10))
}).catch((error) => { }).catch((error) => {
cb(error) cb(error)

@ -1,10 +1,10 @@
export class Blocks { export class Blocks {
executionContext vmContext
coinbase: string coinbase: string
blockNumber: number blockNumber: number
constructor (executionContext, _options) { constructor (vmContext, _options) {
this.executionContext = executionContext this.vmContext = vmContext
const options = _options || {} const options = _options || {}
this.coinbase = options.coinbase || '0x0000000000000000000000000000000000000000' this.coinbase = options.coinbase || '0x0000000000000000000000000000000000000000'
this.blockNumber = 0 this.blockNumber = 0
@ -28,13 +28,13 @@ export class Blocks {
eth_getBlockByNumber (payload, cb) { eth_getBlockByNumber (payload, cb) {
let blockIndex = payload.params[0] let blockIndex = payload.params[0]
if (blockIndex === 'latest') { if (blockIndex === 'latest') {
blockIndex = this.executionContext.latestBlockNumber blockIndex = this.vmContext.latestBlockNumber
} }
if (Number.isInteger(blockIndex)) { if (Number.isInteger(blockIndex)) {
blockIndex = '0x' + blockIndex.toString(16) blockIndex = '0x' + blockIndex.toString(16)
} }
const block = this.executionContext.blocks[blockIndex] const block = this.vmContext.blocks[blockIndex]
if (!block) { if (!block) {
return cb(new Error('block not found')) return cb(new Error('block not found'))
@ -70,7 +70,7 @@ export class Blocks {
} }
eth_getBlockByHash (payload, cb) { eth_getBlockByHash (payload, cb) {
const block = this.executionContext.blocks[payload.params[0]] const block = this.vmContext.blocks[payload.params[0]]
const b = { const b = {
number: this.toHex(block.header.number), number: this.toHex(block.header.number),
@ -109,13 +109,13 @@ export class Blocks {
} }
eth_getBlockTransactionCountByHash (payload, cb) { 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) cb(null, block.transactions.length)
} }
eth_getBlockTransactionCountByNumber (payload, cb) { 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) cb(null, block.transactions.length)
} }
@ -131,7 +131,7 @@ export class Blocks {
eth_getStorageAt (payload, cb) { eth_getStorageAt (payload, cb) {
const [address, position, blockNumber] = payload.params 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)) { if (err || (result.storage && Object.values(result.storage).length === 0)) {
return cb(err, '') return cb(err, '')
} }

@ -1,8 +1,8 @@
export class Debug { export class Debug {
executionContext vmContext
constructor (executionContext) { constructor (vmContext) {
this.executionContext = executionContext this.vmContext = vmContext
} }
methods () { methods () {
@ -14,15 +14,15 @@ export class Debug {
} }
debug_traceTransaction (payload, cb) { 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) { 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) { debug_storageRangeAt (payload, cb) {
this.executionContext.web3().debug.storageRangeAt( this.vmContext.web3().debug.storageRangeAt(
payload.params[0], payload.params[0],
payload.params[1], payload.params[1],
payload.params[2], payload.params[2],

@ -1,8 +1,8 @@
export class Filters { export class Filters {
executionContext vmContext
constructor (executionContext) { constructor (vmContext) {
this.executionContext = executionContext this.vmContext = vmContext
} }
methods () { methods () {
@ -14,49 +14,49 @@ export class Filters {
} }
eth_getLogs (payload, cb) { 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) cb(null, results)
} }
eth_subscribe (payload, cb) { eth_subscribe (payload, cb) {
const subscriptionId = this.executionContext.logsManager.subscribe(payload.params) const subscriptionId = this.vmContext.logsManager.subscribe(payload.params)
cb(null, subscriptionId) cb(null, subscriptionId)
} }
eth_unsubscribe (payload, cb) { eth_unsubscribe (payload, cb) {
this.executionContext.logsManager.unsubscribe(payload.params[0]) this.vmContext.logsManager.unsubscribe(payload.params[0])
cb(null, true) cb(null, true)
} }
eth_newFilter (payload, cb) { 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) cb(null, filterId)
} }
eth_newBlockFilter (payload, cb) { eth_newBlockFilter (payload, cb) {
const filterId = this.executionContext.logsManager.newFilter('block') const filterId = this.vmContext.logsManager.newFilter('block')
cb(null, filterId) cb(null, filterId)
} }
eth_newPendingTransactionFilter (payload, cb) { eth_newPendingTransactionFilter (payload, cb) {
const filterId = this.executionContext.logsManager.newFilter('pendingTransactions') const filterId = this.vmContext.logsManager.newFilter('pendingTransactions')
cb(null, filterId) cb(null, filterId)
} }
eth_uninstallfilter (payload, cb) { 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) cb(null, result)
} }
eth_getFilterChanges (payload, cb) { eth_getFilterChanges (payload, cb) {
const filterId = payload.params[0] const filterId = payload.params[0]
const results = this.executionContext.logsManager.getLogsForFilter(filterId) const results = this.vmContext.logsManager.getLogsForFilter(filterId)
cb(null, results) cb(null, results)
} }
eth_getFilterLogs (payload, cb) { eth_getFilterLogs (payload, cb) {
const filterId = payload.params[0] const filterId = payload.params[0]
const results = this.executionContext.logsManager.getLogsForFilter(filterId, true) const results = this.vmContext.logsManager.getLogsForFilter(filterId, true)
cb(null, results) cb(null, results)
} }
} }

@ -1,17 +1,48 @@
import Web3 from 'web3' import Web3 from 'web3'
import { toChecksumAddress, BN, Address } from 'ethereumjs-util' import { toChecksumAddress, BN, Address } from 'ethereumjs-util'
import { processTx } from './txProcess' import { processTx } from './txProcess'
import { execution } from '@remix-project/remix-lib'
const TxRunnerVM = execution.TxRunnerVM
const TxRunner = execution.TxRunner
export class Transactions { export class Transactions {
executionContext vmContext
accounts accounts
tags
txRunnerVMInstance
txRunnerInstance
constructor (executionContext) { constructor (vmContext) {
this.executionContext = executionContext this.vmContext = vmContext
this.tags = {}
} }
init (accounts) { init (accounts) {
this.accounts = 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 () { methods () {
@ -24,7 +55,9 @@ export class Transactions {
eth_getTransactionCount: this.eth_getTransactionCount.bind(this), eth_getTransactionCount: this.eth_getTransactionCount.bind(this),
eth_getTransactionByHash: this.eth_getTransactionByHash.bind(this), eth_getTransactionByHash: this.eth_getTransactionByHash.bind(this),
eth_getTransactionByBlockHashAndIndex: this.eth_getTransactionByBlockHashAndIndex.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) { if (payload.params && payload.params.length > 0 && payload.params[0].from) {
payload.params[0].from = toChecksumAddress(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) { 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) { if (error) {
return cb(error) return cb(error)
} }
const txBlock = this.executionContext.txs[receipt.hash] const txBlock = this.vmContext.txs[receipt.hash]
const r: Record <string, unknown> = { const r: Record <string, unknown> = {
transactionHash: receipt.hash, transactionHash: receipt.hash,
@ -72,7 +119,7 @@ export class Transactions {
eth_getCode (payload, cb) { eth_getCode (payload, cb) {
const address = payload.params[0] const address = payload.params[0]
this.executionContext.web3().eth.getCode(address, (error, result) => { this.vmContext.web3().eth.getCode(address, (error, result) => {
if (error) { if (error) {
console.dir('error getting code') console.dir('error getting code')
console.dir(error) console.dir(error)
@ -92,13 +139,31 @@ export class Transactions {
payload.params[0].value = undefined 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) { eth_getTransactionCount (payload, cb) {
const address = payload.params[0] 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) const nonce = new BN(account.nonce).toString(10)
cb(null, nonce) cb(null, nonce)
}).catch((error) => { }).catch((error) => {
@ -109,12 +174,12 @@ export class Transactions {
eth_getTransactionByHash (payload, cb) { eth_getTransactionByHash (payload, cb) {
const address = payload.params[0] const address = payload.params[0]
this.executionContext.web3().eth.getTransactionReceipt(address, (error, receipt) => { this.vmContext.web3().eth.getTransactionReceipt(address, (error, receipt) => {
if (error) { if (error) {
return cb(error) return cb(error)
} }
const txBlock = this.executionContext.txs[receipt.transactionHash] const txBlock = this.vmContext.txs[receipt.transactionHash]
// TODO: params to add later // TODO: params to add later
const r: Record<string, unknown> = { const r: Record<string, unknown> = {
@ -154,10 +219,10 @@ export class Transactions {
eth_getTransactionByBlockHashAndIndex (payload, cb) { eth_getTransactionByBlockHashAndIndex (payload, cb) {
const txIndex = payload.params[1] 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') 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) { if (error) {
return cb(error) return cb(error)
} }
@ -196,10 +261,10 @@ export class Transactions {
eth_getTransactionByBlockNumberAndIndex (payload, cb) { eth_getTransactionByBlockNumberAndIndex (payload, cb) {
const txIndex = payload.params[1] 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') 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) { if (error) {
return cb(error) return cb(error)
} }

@ -1,15 +1,12 @@
import { execution } from '@remix-project/remix-lib' import { execution } from '@remix-project/remix-lib'
const TxExecution = execution.txExecution const TxExecution = execution.txExecution
const TxRunner = execution.txRunner
function runCall (payload, from, to, data, value, gasLimit, txRunner, callbacks, callback) { function runCall (payload, from, to, data, value, gasLimit, txRunner, callbacks, callback) {
const finalCallback = function (err, result) { const finalCallback = function (err, result) {
if (err) { if (err) {
return callback(err) return callback(err)
} }
const returnValue = result.result.execResult.returnValue.toString('hex') return callback(null, result)
const toReturn = `0x${returnValue || '0'}`
return callback(null, toReturn)
} }
TxExecution.callFunction(from, to, data, value, gasLimit, { constant: true }, txRunner, callbacks, finalCallback) 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) { if (err) {
return callback(err) return callback(err)
} }
callback(null, result.transactionHash) callback(null, result)
} }
TxExecution.callFunction(from, to, data, value, gasLimit, { constant: false }, txRunner, callbacks, finalCallback) 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) { if (err) {
return callback(err) return callback(err)
} }
callback(null, result.transactionHash) callback(null, result)
} }
TxExecution.createContract(from, data, value, gasLimit, txRunner, callbacks, finalCallback) TxExecution.createContract(from, data, value, gasLimit, txRunner, callbacks, finalCallback)
} }
let txRunnerInstance export function processTx (txRunnerInstance, payload, isCall, callback) {
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
let { from, to, data, value, gas } = payload.params[0] let { from, to, data, value, gas } = payload.params[0]
gas = gas || 3000000 gas = gas || 3000000

@ -1,5 +1,4 @@
import { Blocks } from './methods/blocks' import { Blocks } from './methods/blocks'
import { execution } from '@remix-project/remix-lib'
import { info } from './utils/logs' import { info } from './utils/logs'
import merge from 'merge' import merge from 'merge'
@ -11,11 +10,11 @@ import { methods as netMethods } from './methods/net'
import { Transactions } from './methods/transactions' import { Transactions } from './methods/transactions'
import { Debug } from './methods/debug' import { Debug } from './methods/debug'
import { generateBlock } from './genesis' import { generateBlock } from './genesis'
const { executionContext } = execution import { VMContext } from './vm-context'
export class Provider { export class Provider {
options: Record<string, unknown> options: Record<string, unknown>
executionContext vmContext
Accounts Accounts
Transactions Transactions
methods methods
@ -26,23 +25,23 @@ export class Provider {
this.options = options this.options = options
this.host = host this.host = host
this.connected = true this.connected = true
// TODO: init executionContext here this.vmContext = new VMContext()
this.executionContext = executionContext
this.Accounts = new Accounts(this.executionContext) this.Accounts = new Accounts(this.vmContext)
this.Transactions = new Transactions(this.executionContext) this.Transactions = new Transactions(this.vmContext)
this.methods = {} this.methods = {}
this.methods = merge(this.methods, this.Accounts.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, 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, netMethods())
this.methods = merge(this.methods, this.Transactions.methods()) 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 () { async init () {
await generateBlock(this.executionContext) await generateBlock(this.vmContext)
await this.Accounts.resetAccounts() await this.Accounts.resetAccounts()
this.Transactions.init(this.Accounts.accounts) this.Transactions.init(this.Accounts.accounts)
} }
@ -87,6 +86,39 @@ export class Provider {
}; };
on (type, cb) { 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: []
})
} }
} }

@ -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<StorageDump>((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
}
}

@ -15,7 +15,7 @@
} }
], ],
"dependencies": { "dependencies": {
"@remix-project/remix-lib": "^0.4.34", "@remix-project/remix-lib": "../remix-lib",
"eslint-scope": "^5.0.0", "eslint-scope": "^5.0.0",
"@ethereumjs/vm": "^5.3.2", "@ethereumjs/vm": "^5.3.2",
"@ethereumjs/block": "^3.2.1", "@ethereumjs/block": "^3.2.1",

@ -6,6 +6,7 @@ module.exports = {
transform: { transform: {
'^.+\\.[tj]sx?$': 'ts-jest', '^.+\\.[tj]sx?$': 'ts-jest',
}, },
transformIgnorePatterns: ["/node_modules/", "\\.pnp\\.[^\\\/]+$"],
rootDir: "./", rootDir: "./",
testTimeout: 40000, testTimeout: 40000,
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html', 'json'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html', 'json'],
@ -18,6 +19,6 @@ module.exports = {
"!src/types.ts", "!src/types.ts",
"!src/logger.ts" "!src/logger.ts"
], ],
coverageDirectory: '../../coverage/libs/remix-tests', coverageDirectory: '../../coverage/libs/remix-tests'
}; };

@ -35,9 +35,9 @@
}, },
"homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-tests#readme", "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-tests#readme",
"dependencies": { "dependencies": {
"@remix-project/remix-lib": "^0.4.34", "@remix-project/remix-lib": "../remix-lib",
"@remix-project/remix-simulator": "^0.1.10-beta.0", "@remix-project/remix-simulator": "../remix-simulator",
"@remix-project/remix-solidity": "^0.3.35", "@remix-project/remix-solidity": "../remix-solidity",
"ansi-gray": "^0.1.1", "ansi-gray": "^0.1.1",
"async": "^2.6.0", "async": "^2.6.0",
"axios": ">=0.21.1", "axios": ">=0.21.1",

@ -79,7 +79,7 @@ export function deployAll (compileResult: compilationInterface, web3: Web3, with
contracts[contractName] = contractObject contracts[contractName] = contractObject
contracts[contractName].filename = filename 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) { }).on('error', function (err) {
console.error(err) console.error(err)
callback(err) callback(err)

@ -3,7 +3,9 @@ import { resolve } from 'path'
describe('testRunner: remix-tests CLI', () => { describe('testRunner: remix-tests CLI', () => {
// remix-tests binary, after build, is used as executable // remix-tests binary, after build, is used as executable
const executablePath = resolve(__dirname + '/../../../dist/libs/remix-tests/bin/remix-tests') const executablePath = resolve(__dirname + '/../../../dist/libs/remix-tests/bin/remix-tests')
const result = spawnSync('ls', { cwd: resolve(__dirname + '/../../../dist/libs/remix-tests') }) const result = spawnSync('ls', { cwd: resolve(__dirname + '/../../../dist/libs/remix-tests') })
if(result) { if(result) {
const dirContent = result.stdout.toString() const dirContent = result.stdout.toString()
@ -11,6 +13,7 @@ describe('testRunner: remix-tests CLI', () => {
if(!dirContent.includes('node_modules')) execSync('npm install', { cwd: resolve(__dirname + '/../../../dist/libs/remix-tests') }) if(!dirContent.includes('node_modules')) execSync('npm install', { cwd: resolve(__dirname + '/../../../dist/libs/remix-tests') })
} }
describe('test various CLI options', () => { describe('test various CLI options', () => {
test('remix-tests version', () => { test('remix-tests version', () => {
const res = spawnSync(executablePath, ['-V']) const res = spawnSync(executablePath, ['-V'])

@ -3,9 +3,9 @@
"compilerOptions": { "compilerOptions": {
"types": ["node", "jest"], "types": ["node", "jest"],
"module": "commonjs", "module": "commonjs",
"esModuleInterop": true,
"allowJs": true, "allowJs": true,
"rootDir": "./", "rootDir": "./",
"esModuleInterop": true
}, },
"include": ["**/*.ts"] "include": ["**/*.ts"]
} }

@ -1,16 +1,15 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"outDir": "../../dist/out-tsc", "outDir": "../../dist/out-tsc",
"declaration": true, "declaration": true,
"rootDir": "./", "rootDir": "./",
"types": ["node"] "types": ["node"]
}, },
"exclude": [ "exclude": [
"**/*.spec.ts", "**/*.spec.ts",
"tests/" "tests/"
], ],
"include": ["**/*.ts"] "include": ["**/*.ts"]
} }

@ -1,16 +1,15 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "../../dist/out-tsc", "outDir": "../../dist/out-tsc",
"module": "commonjs", "module": "commonjs",
"types": ["jest", "node"] "types": ["jest", "node"]
}, },
"include": [ "include": [
"**/*.spec.ts", "**/*.spec.ts",
"**/*.spec.tsx", "**/*.spec.tsx",
"**/*.spec.js", "**/*.spec.js",
"**/*.spec.jsx", "**/*.spec.jsx",
"**/*.d.ts" "**/*.d.ts"
] ]
} }

@ -0,0 +1,4 @@
{
"presets": ["@nrwl/react/babel"],
"plugins": []
}

@ -0,0 +1,19 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": "../../../.eslintrc",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error"
}
}

@ -0,0 +1,7 @@
# remix-ui-checkbox
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test remix-ui-checkbox` to execute the unit tests via [Jest](https://jestjs.io).

@ -0,0 +1 @@
export * from './lib/remix-ui-checkbox'

@ -0,0 +1,47 @@
import React from 'react' //eslint-disable-line
import './remix-ui-checkbox.css'
/* eslint-disable-next-line */
export interface RemixUiCheckboxProps {
onClick?: (event) => void
onChange?: (event) => void
label?: string
inputType?: string
name?: string
checked?: boolean
id?: string
itemName?: string
categoryId?: string
}
export const RemixUiCheckbox = ({
id,
label,
onClick,
inputType,
name,
checked,
onChange,
itemName,
categoryId
}: RemixUiCheckboxProps) => {
return (
<div className="listenOnNetwork_2A0YE0 custom-control custom-checkbox" style={{ display: 'flex', alignItems: 'center' }} onClick={onClick}>
<input
id={id}
type={inputType}
onChange={onChange}
style={{ verticalAlign: 'bottom' }}
name={name}
className="custom-control-input"
checked={checked}
/>
<label className="form-check-label custom-control-label" id={`heading${categoryId}`} style={{ paddingTop: '0.15rem' }}>
{name ? <div className="font-weight-bold">{itemName}</div> : ''}
{label}
</label>
</div>
)
}
export default RemixUiCheckbox

@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"jsx": "react",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -0,0 +1,293 @@
import React from 'react'
import { File } from '../types'
import { extractNameFromKey, extractParentFromKey } from '../utils'
export const fetchDirectoryError = (error: any) => {
return {
type: 'FETCH_DIRECTORY_ERROR',
payload: error
}
}
export const fetchDirectoryRequest = (promise: Promise<any>) => {
return {
type: 'FETCH_DIRECTORY_REQUEST',
payload: promise
}
}
export const fetchDirectorySuccess = (path: string, files: File[]) => {
return {
type: 'FETCH_DIRECTORY_SUCCESS',
payload: { path, files }
}
}
export const fileSystemReset = () => {
return {
type: 'FILESYSTEM_RESET'
}
}
const normalize = (parent, filesList, newInputType?: string): any => {
const folders = {}
const files = {}
Object.keys(filesList || {}).forEach(key => {
key = key.replace(/^\/|\/$/g, '') // remove first and last slash
let path = key
path = path.replace(/^\/|\/$/g, '') // remove first and last slash
if (filesList[key].isDirectory) {
folders[extractNameFromKey(key)] = {
path,
name: extractNameFromKey(path),
isDirectory: filesList[key].isDirectory
}
} else {
files[extractNameFromKey(key)] = {
path,
name: extractNameFromKey(path),
isDirectory: filesList[key].isDirectory
}
}
})
if (newInputType === 'folder') {
const path = parent + '/blank'
folders[path] = {
path: path,
name: '',
isDirectory: true
}
} else if (newInputType === 'file') {
const path = parent + '/blank'
files[path] = {
path: path,
name: '',
isDirectory: false
}
}
return Object.assign({}, folders, files)
}
const fetchDirectoryContent = async (provider, folderPath: string, newInputType?: string): Promise<any> => {
return new Promise((resolve) => {
provider.resolveDirectory(folderPath, (error, fileTree) => {
if (error) console.error(error)
const files = normalize(folderPath, fileTree, newInputType)
resolve({ [extractNameFromKey(folderPath)]: files })
})
})
}
export const fetchDirectory = (provider, path: string) => (dispatch: React.Dispatch<any>) => {
const promise = fetchDirectoryContent(provider, path)
dispatch(fetchDirectoryRequest(promise))
promise.then((files) => {
dispatch(fetchDirectorySuccess(path, files))
}).catch((error) => {
dispatch(fetchDirectoryError({ error }))
})
return promise
}
export const resolveDirectoryError = (error: any) => {
return {
type: 'RESOLVE_DIRECTORY_ERROR',
payload: error
}
}
export const resolveDirectoryRequest = (promise: Promise<any>) => {
return {
type: 'RESOLVE_DIRECTORY_REQUEST',
payload: promise
}
}
export const resolveDirectorySuccess = (path: string, files: File[]) => {
return {
type: 'RESOLVE_DIRECTORY_SUCCESS',
payload: { path, files }
}
}
export const resolveDirectory = (provider, path: string) => (dispatch: React.Dispatch<any>) => {
const promise = fetchDirectoryContent(provider, path)
dispatch(resolveDirectoryRequest(promise))
promise.then((files) => {
dispatch(resolveDirectorySuccess(path, files))
}).catch((error) => {
dispatch(resolveDirectoryError({ error }))
})
return promise
}
export const fetchProviderError = (error: any) => {
return {
type: 'FETCH_PROVIDER_ERROR',
payload: error
}
}
export const fetchProviderRequest = (promise: Promise<any>) => {
return {
type: 'FETCH_PROVIDER_REQUEST',
payload: promise
}
}
export const fetchProviderSuccess = (provider: any) => {
return {
type: 'FETCH_PROVIDER_SUCCESS',
payload: provider
}
}
export const fileAddedSuccess = (path: string, files) => {
return {
type: 'FILE_ADDED',
payload: { path, files }
}
}
export const folderAddedSuccess = (path: string, files) => {
return {
type: 'FOLDER_ADDED',
payload: { path, files }
}
}
export const fileRemovedSuccess = (path: string, removePath: string) => {
return {
type: 'FILE_REMOVED',
payload: { path, removePath }
}
}
export const fileRenamedSuccess = (path: string, removePath: string, files) => {
return {
type: 'FILE_RENAMED',
payload: { path, removePath, files }
}
}
export const init = (provider, workspaceName: string, plugin, registry) => (dispatch: React.Dispatch<any>) => {
if (provider) {
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.emit('newTestFileCreated', filePath)
}
})
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.on('fileRemoved', async (removePath) => {
const path = extractParentFromKey(removePath) || provider.workspace || provider.type || ''
dispatch(fileRemovedSuccess(path, removePath))
})
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.on('fileExternallyChanged', async (path: string, file: { content: string }) => {
const config = registry.get('config').api
const editor = registry.get('editor').api
if (config.get('currentFile') === path && editor.currentContent() !== file.content) {
if (provider.isReadOnly(path)) return editor.setText(file.content)
dispatch(displayNotification(
path + ' changed',
'This file has been changed outside of Remix IDE.',
'Replace by the new content', 'Keep the content displayed in Remix',
() => {
editor.setText(file.content)
}
))
}
})
provider.event.on('fileRenamedError', async () => {
dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel'))
})
provider.event.on('rootFolderChanged', async () => {
workspaceName = provider.workspace || provider.type || ''
fetchDirectory(provider, workspaceName)(dispatch)
})
dispatch(fetchProviderSuccess(provider))
dispatch(setCurrentWorkspace(workspaceName))
} else {
dispatch(fetchProviderError('No provider available'))
}
}
export const setCurrentWorkspace = (name: string) => {
return {
type: 'SET_CURRENT_WORKSPACE',
payload: name
}
}
export const addInputFieldSuccess = (path: string, files: File[]) => {
return {
type: 'ADD_INPUT_FIELD',
payload: { path, files }
}
}
export const addInputField = (provider, type: string, path: string) => (dispatch: React.Dispatch<any>) => {
const promise = fetchDirectoryContent(provider, path, type)
promise.then((files) => {
dispatch(addInputFieldSuccess(path, files))
}).catch((error) => {
console.error(error)
})
return promise
}
export const removeInputFieldSuccess = (path: string) => {
return {
type: 'REMOVE_INPUT_FIELD',
payload: { path }
}
}
export const removeInputField = (path: string) => (dispatch: React.Dispatch<any>) => {
return dispatch(removeInputFieldSuccess(path))
}
export const displayNotification = (title: string, message: string, labelOk: string, labelCancel: string, actionOk?: (...args) => void, actionCancel?: (...args) => void) => {
return {
type: 'DISPLAY_NOTIFICATION',
payload: { title, message, labelOk, labelCancel, actionOk, actionCancel }
}
}
export const hideNotification = () => {
return {
type: 'DISPLAY_NOTIFICATION'
}
}
export const closeNotificationModal = () => (dispatch: React.Dispatch<any>) => {
dispatch(hideNotification())
}

@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef } from 'react' // eslint-disable-line import React, { useEffect, useState, useRef, useReducer } from 'react' // eslint-disable-line
// import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line // import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
@ -7,6 +7,8 @@ import Gists from 'gists'
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
import { FileExplorerProps, File } from './types' import { FileExplorerProps, File } from './types'
import { fileSystemReducer, fileSystemInitialState } from './reducers/fileSystem'
import { fetchDirectory, init, resolveDirectory, addInputField, removeInputField } from './actions/fileSystem'
import * as helper from '../../../../../apps/remix-ide/src/lib/helper' import * as helper from '../../../../../apps/remix-ide/src/lib/helper'
import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params' import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params'
@ -15,7 +17,7 @@ import './css/file-explorer.css'
const queryParams = new QueryParams() const queryParams = new QueryParams()
export const FileExplorer = (props: FileExplorerProps) => { export const FileExplorer = (props: FileExplorerProps) => {
const { filesProvider, name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads } = props const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads } = props
const [state, setState] = useState({ const [state, setState] = useState({
focusElement: [{ focusElement: [{
key: '', key: '',
@ -24,10 +26,51 @@ export const FileExplorer = (props: FileExplorerProps) => {
focusPath: null, focusPath: null,
files: [], files: [],
fileManager: null, fileManager: null,
filesProvider,
ctrlKey: false, ctrlKey: false,
newFileName: '', newFileName: '',
actions: [], actions: [{
id: 'newFile',
name: 'New File',
type: ['folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'newFolder',
name: 'New Folder',
type: ['folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'rename',
name: 'Rename',
type: ['file', 'folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'delete',
name: 'Delete',
type: ['file', 'folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'pushChangesToGist',
name: 'Push changes to gist',
type: [],
path: [],
extension: [],
pattern: ['^browser/gists/([0-9]|[a-z])*$']
}, {
id: 'run',
name: 'Run',
type: [],
path: [],
extension: ['.js'],
pattern: []
}],
focusContext: { focusContext: {
element: null, element: null,
x: null, x: null,
@ -45,23 +88,49 @@ export const FileExplorer = (props: FileExplorerProps) => {
hide: true, hide: true,
title: '', title: '',
message: '', message: '',
ok: { okLabel: '',
label: '', okFn: () => {},
fn: () => {} cancelLabel: '',
}, cancelFn: () => {},
cancel: {
label: '',
fn: () => {}
},
handleHide: null handleHide: null
}, },
modals: [], modals: [],
toasterMsg: '', toasterMsg: '',
mouseOverElement: null, mouseOverElement: null,
showContextMenu: false showContextMenu: false,
reservedKeywords: [name, 'gist-']
}) })
const [fileSystem, dispatch] = useReducer(fileSystemReducer, fileSystemInitialState)
const editRef = useRef(null) const editRef = useRef(null)
useEffect(() => {
if (props.filesProvider) {
init(props.filesProvider, props.name, props.plugin, props.registry)(dispatch)
}
}, [props.filesProvider, props.name])
useEffect(() => {
const provider = fileSystem.provider.provider
if (provider) {
fetchDirectory(provider, props.name)(dispatch)
}
}, [fileSystem.provider.provider, props.name])
useEffect(() => {
if (fileSystem.notification.message) {
modal(fileSystem.notification.title, fileSystem.notification.message, fileSystem.notification.labelOk, fileSystem.notification.actionOk, fileSystem.notification.labelCancel, fileSystem.notification.actionCancel)
}
}, [fileSystem.notification.message])
useEffect(() => {
if (fileSystem.files.expandPath.length > 0) {
setState(prevState => {
return { ...prevState, expandPath: [...new Set([...prevState.expandPath, ...fileSystem.files.expandPath])] }
})
}
}, [fileSystem.files.expandPath])
useEffect(() => { useEffect(() => {
if (state.focusEdit.element) { if (state.focusEdit.element) {
setTimeout(() => { setTimeout(() => {
@ -75,95 +144,13 @@ export const FileExplorer = (props: FileExplorerProps) => {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const fileManager = registry.get('filemanager').api const fileManager = registry.get('filemanager').api
const files = await fetchDirectoryContent(name)
const actions = [{
id: 'newFile',
name: 'New File',
type: ['folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'newFolder',
name: 'New Folder',
type: ['folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'rename',
name: 'Rename',
type: ['file', 'folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'delete',
name: 'Delete',
type: ['file', 'folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'pushChangesToGist',
name: 'Push changes to gist',
type: [],
path: [],
extension: [],
pattern: ['^browser/gists/([0-9]|[a-z])*$']
}, {
id: 'run',
name: 'Run',
type: [],
path: [],
extension: ['.js'],
pattern: []
}]
setState(prevState => { setState(prevState => {
return { ...prevState, fileManager, files, actions, expandPath: [name] } return { ...prevState, fileManager, expandPath: [name] }
}) })
})() })()
}, [name]) }, [name])
useEffect(() => {
if (state.fileManager) {
filesProvider.event.register('fileExternallyChanged', fileExternallyChanged)
filesProvider.event.register('fileRenamedError', fileRenamedError)
filesProvider.event.register('rootFolderChanged', rootFolderChanged)
}
}, [state.fileManager])
useEffect(() => {
const { expandPath } = state
const expandFn = async () => {
let files = state.files
for (let i = 0; i < expandPath.length; i++) {
files = await resolveDirectory(expandPath[i], files)
await setState(prevState => {
return { ...prevState, files }
})
}
}
if (expandPath && expandPath.length > 0) {
expandFn()
}
}, [state.expandPath])
useEffect(() => {
// unregister event to update state in callback
if (filesProvider.event.registered.fileAdded) filesProvider.event.unregister('fileAdded', fileAdded)
if (filesProvider.event.registered.folderAdded) filesProvider.event.unregister('folderAdded', folderAdded)
if (filesProvider.event.registered.fileRemoved) filesProvider.event.unregister('fileRemoved', fileRemoved)
if (filesProvider.event.registered.fileRenamed) filesProvider.event.unregister('fileRenamed', fileRenamed)
filesProvider.event.register('fileAdded', fileAdded)
filesProvider.event.register('folderAdded', folderAdded)
filesProvider.event.register('fileRemoved', fileRemoved)
filesProvider.event.register('fileRenamed', fileRenamed)
}, [state.files])
useEffect(() => { useEffect(() => {
if (focusRoot) { if (focusRoot) {
setState(prevState => { setState(prevState => {
@ -205,8 +192,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
hide: false, hide: false,
title: prevState.modals[0].title, title: prevState.modals[0].title,
message: prevState.modals[0].message, message: prevState.modals[0].message,
ok: prevState.modals[0].ok, okLabel: prevState.modals[0].okLabel,
cancel: prevState.modals[0].cancel, okFn: prevState.modals[0].okFn,
cancelLabel: prevState.modals[0].cancelLabel,
cancelFn: prevState.modals[0].cancelFn,
handleHide: prevState.modals[0].handleHide handleHide: prevState.modals[0].handleHide
} }
@ -220,83 +209,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
}, [state.modals]) }, [state.modals])
const resolveDirectory = async (folderPath, dir: File[], isChild = false): Promise<File[]> => {
if (!isChild && (state.focusEdit.element === '/blank') && state.focusEdit.isNew && (dir.findIndex(({ path }) => path === '/blank') === -1)) {
dir = state.focusEdit.type === 'file' ? [...dir, {
path: state.focusEdit.element,
name: '',
isDirectory: false
}] : [{
path: state.focusEdit.element,
name: '',
isDirectory: true
}, ...dir]
}
dir = await Promise.all(dir.map(async (file) => {
if (file.path === folderPath) {
if ((extractParentFromKey(state.focusEdit.element) === folderPath) && state.focusEdit.isNew) {
file.child = state.focusEdit.type === 'file' ? [...await fetchDirectoryContent(folderPath), {
path: state.focusEdit.element,
name: '',
isDirectory: false
}] : [{
path: state.focusEdit.element,
name: '',
isDirectory: true
}, ...await fetchDirectoryContent(folderPath)]
} else {
file.child = await fetchDirectoryContent(folderPath)
}
return file
} else if (file.child) {
file.child = await resolveDirectory(folderPath, file.child, true)
return file
} else {
return file
}
}))
return dir
}
const fetchDirectoryContent = async (folderPath: string): Promise<File[]> => {
return new Promise((resolve) => {
filesProvider.resolveDirectory(folderPath, (error, fileTree) => {
if (error) console.error(error)
const files = normalize(fileTree)
resolve(files)
})
})
}
const normalize = (filesList): File[] => {
const folders = []
const files = []
Object.keys(filesList || {}).forEach(key => {
key = key.replace(/^\/|\/$/g, '') // remove first and last slash
let path = key
path = path.replace(/^\/|\/$/g, '') // remove first and last slash
if (filesList[key].isDirectory) {
folders.push({
path,
name: extractNameFromKey(path),
isDirectory: filesList[key].isDirectory
})
} else {
files.push({
path,
name: extractNameFromKey(path),
isDirectory: filesList[key].isDirectory
})
}
})
return [...folders, ...files]
}
const extractNameFromKey = (key: string):string => { const extractNameFromKey = (key: string):string => {
const keyPath = key.split('/') const keyPath = key.split('/')
@ -311,34 +223,30 @@ export const FileExplorer = (props: FileExplorerProps) => {
return keyPath.join('/') return keyPath.join('/')
} }
const createNewFile = (newFilePath: string) => { 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 const fileManager = state.fileManager
try { try {
helper.createNonClashingName(newFilePath, filesProvider, async (error, newName) => { const newName = await helper.createNonClashingNameAsync(newFilePath, fileManager)
if (error) { const createFile = await fileManager.writeFile(newName, '')
modal('Create File Failed', error, {
label: 'Close',
fn: async () => {}
}, null)
} else {
const createFile = await fileManager.writeFile(newName, '')
if (!createFile) { if (!createFile) {
return toast('Failed to create file ' + newName) return toast('Failed to create file ' + newName)
} else { } else {
await fileManager.open(newName) const path = newName.indexOf(props.name + '/') === 0 ? newName.replace(props.name + '/', '') : newName
setState(prevState => {
return { ...prevState, focusElement: [{ key: newName, type: 'file' }] } await fileManager.open(path)
}) setState(prevState => {
} return { ...prevState, focusElement: [{ key: newName, type: 'file' }] }
} })
}) }
} catch (error) { } catch (error) {
return modal('File Creation Failed', typeof error === 'string' ? error : error.message, { return modal('File Creation Failed', typeof error === 'string' ? error : error.message, 'Close', async () => {})
label: 'Close',
fn: async () => {}
}, null)
} }
} }
@ -350,44 +258,34 @@ export const FileExplorer = (props: FileExplorerProps) => {
const exists = await fileManager.exists(dirName) const exists = await fileManager.exists(dirName)
if (exists) { if (exists) {
return modal('Rename File Failed', `A file or folder ${extractNameFromKey(newFolderPath)} already exists at this location. Please choose a different name.`, { return modal('Rename File Failed', `A file or folder ${extractNameFromKey(newFolderPath)} already exists at this location. Please choose a different name.`, 'Close', () => {})
label: 'Close',
fn: () => {}
}, null)
} }
await fileManager.mkdir(dirName) await fileManager.mkdir(dirName)
setState(prevState => { setState(prevState => {
return { ...prevState, focusElement: [{ key: newFolderPath, type: 'folder' }] } return { ...prevState, focusElement: [{ key: newFolderPath, type: 'folder' }] }
}) })
} catch (e) { } catch (e) {
return modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, { return modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, 'Close', async () => {})
label: 'Close',
fn: async () => {}
}, null)
} }
} }
const deletePath = async (path: string) => { const deletePath = async (path: string) => {
const filesProvider = fileSystem.provider.provider
if (filesProvider.isReadOnly(path)) { if (filesProvider.isReadOnly(path)) {
return toast('cannot delete file. ' + name + ' is a read only explorer') return toast('cannot delete file. ' + name + ' is a read only explorer')
} }
const isDir = state.fileManager.isDirectory(path) const isDir = state.fileManager.isDirectory(path)
modal(`Delete ${isDir ? 'folder' : 'file'}`, `Are you sure you want to delete ${path} ${isDir ? 'folder' : 'file'}?`, { modal(`Delete ${isDir ? 'folder' : 'file'}`, `Are you sure you want to delete ${path} ${isDir ? 'folder' : 'file'}?`, 'OK', async () => {
label: 'OK', try {
fn: async () => { const fileManager = state.fileManager
try {
const fileManager = state.fileManager
await fileManager.remove(path) await fileManager.remove(path)
} catch (e) { } catch (e) {
toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${path}.`) toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${path}.`)
}
} }
}, { }, 'Cancel', () => {})
label: 'Cancel',
fn: () => {}
})
} }
const renamePath = async (oldPath: string, newPath: string) => { const renamePath = async (oldPath: string, newPath: string) => {
@ -396,121 +294,17 @@ export const FileExplorer = (props: FileExplorerProps) => {
const exists = await fileManager.exists(newPath) const exists = await fileManager.exists(newPath)
if (exists) { if (exists) {
modal('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, { modal('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', () => {})
label: 'Close',
fn: () => {}
}, null)
} else { } else {
await fileManager.rename(oldPath, newPath) await fileManager.rename(oldPath, newPath)
} }
} catch (error) { } catch (error) {
modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, { modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {})
label: 'Close',
fn: async () => {}
}, null)
}
}
const removePath = (path: string, files: File[]): File[] => {
return files.map(file => {
if (file.path === path) {
return null
} else if (file.child) {
const childFiles = removePath(path, file.child)
file.child = childFiles.filter(file => file)
return file
} else {
return file
}
})
}
const fileAdded = async (filePath: string) => {
const pathArr = filePath.split('/')
const expandPath = pathArr.map((path, index) => {
return [...pathArr.slice(0, index)].join('/')
}).filter(path => path && (path !== props.name))
const files = await fetchDirectoryContent(props.name)
setState(prevState => {
const uniquePaths = [...new Set([...prevState.expandPath, ...expandPath])]
return { ...prevState, files, expandPath: uniquePaths }
})
if (filePath.includes('_test.sol')) {
plugin.event.trigger('newTestFileCreated', [filePath])
}
}
const folderAdded = async (folderPath: string) => {
const pathArr = folderPath.split('/')
const expandPath = pathArr.map((path, index) => {
return [...pathArr.slice(0, index)].join('/')
}).filter(path => path && (path !== props.name))
const files = await fetchDirectoryContent(props.name)
setState(prevState => {
const uniquePaths = [...new Set([...prevState.expandPath, ...expandPath])]
return { ...prevState, files, expandPath: uniquePaths }
})
}
const fileExternallyChanged = (path: string, file: { content: string }) => {
const config = registry.get('config').api
const editor = registry.get('editor').api
if (config.get('currentFile') === path && editor.currentContent() !== file.content) {
if (filesProvider.isReadOnly(path)) return editor.setText(file.content)
modal(path + ' changed', 'This file has been changed outside of Remix IDE.', {
label: 'Replace by the new content',
fn: () => {
editor.setText(file.content)
}
}, {
label: 'Keep the content displayed in Remix',
fn: () => {}
})
} }
} }
const fileRemoved = (filePath) => {
const files = removePath(filePath, state.files)
const updatedFiles = files.filter(file => file)
setState(prevState => {
return { ...prevState, files: updatedFiles }
})
}
const fileRenamed = async () => {
const files = await fetchDirectoryContent(props.name)
setState(prevState => {
return { ...prevState, files, expandPath: [...prevState.expandPath] }
})
}
// register to event of the file provider
// files.event.register('fileRenamed', fileRenamed)
const fileRenamedError = (error: string) => {
modal('File Renamed Failed', error, {
label: 'Close',
fn: () => {}
}, null)
}
// register to event of the file provider
// files.event.register('rootFolderChanged', rootFolderChanged)
const rootFolderChanged = async () => {
const files = await fetchDirectoryContent(name)
setState(prevState => {
return { ...prevState, files }
})
}
const uploadFile = (target) => { const uploadFile = (target) => {
const filesProvider = fileSystem.provider.provider
// TODO The file explorer is merely a view on the current state of // TODO The file explorer is merely a view on the current state of
// the files module. Please ask the user here if they want to overwrite // the files module. Please ask the user here if they want to overwrite
// a file and then just use `files.add`. The file explorer will // a file and then just use `files.add`. The file explorer will
@ -528,19 +322,13 @@ export const FileExplorer = (props: FileExplorerProps) => {
fileReader.onload = async function (event) { fileReader.onload = async function (event) {
if (helper.checkSpecialChars(file.name)) { if (helper.checkSpecialChars(file.name)) {
modal('File Upload Failed', 'Special characters are not allowed', { modal('File Upload Failed', 'Special characters are not allowed', 'Close', async () => {})
label: 'Close',
fn: async () => {}
}, null)
return return
} }
const success = await filesProvider.set(name, event.target.result) const success = await filesProvider.set(name, event.target.result)
if (!success) { if (!success) {
return modal('File Upload Failed', 'Failed to create file ' + name, { return modal('File Upload Failed', 'Failed to create file ' + name, 'Close', async () => {})
label: 'Close',
fn: async () => {}
}, null)
} }
const config = registry.get('config').api const config = registry.get('config').api
const editor = registry.get('editor').api const editor = registry.get('editor').api
@ -553,60 +341,38 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const name = `${parentFolder}/${file.name}` const name = `${parentFolder}/${file.name}`
filesProvider.exists(name, (error, exist) => { filesProvider.exists(name).then(exist => {
if (error) console.log(error)
if (!exist) { if (!exist) {
loadFile(name) loadFile(name)
} else { } else {
modal('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, { modal('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, 'OK', () => {
label: 'OK', loadFile(name)
fn: () => { }, 'Cancel', () => {})
loadFile(name)
}
}, {
label: 'Cancel',
fn: () => {}
})
} }
}).catch(error => {
if (error) console.log(error)
}) })
}) })
} }
const publishToGist = () => { 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?`, { 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', () => {})
label: 'OK',
fn: toGist
}, {
label: 'Cancel',
fn: () => {}
})
} }
const toGist = (id?: string) => { const toGist = (id?: string) => {
const filesProvider = fileSystem.provider.provider
const proccedResult = function (error, data) { const proccedResult = function (error, data) {
if (error) { if (error) {
modal('Publish to gist Failed', 'Failed to manage gist: ' + error, { modal('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', () => {})
label: 'Close',
fn: async () => {}
}, null)
} else { } else {
if (data.html_url) { 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?`, { modal('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', () => {
label: 'OK', window.open(data.html_url, '_blank')
fn: () => { }, 'Cancel', () => {})
window.open(data.html_url, '_blank')
}
}, {
label: 'Cancel',
fn: () => {}
})
} else { } else {
const error = JSON.stringify(data.errors, null, '\t') || '' 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 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, { modal('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', () => {})
label: 'Close',
fn: async () => {}
}, null)
} }
} }
} }
@ -632,20 +398,14 @@ export const FileExplorer = (props: FileExplorerProps) => {
packageFiles(filesProvider, folder, async (error, packaged) => { packageFiles(filesProvider, folder, async (error, packaged) => {
if (error) { if (error) {
console.log(error) console.log(error)
modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, { modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', async () => {})
label: 'Close',
fn: async () => {}
}, null)
} else { } else {
// check for token // check for token
const config = registry.get('config').api const config = registry.get('config').api
const accessToken = config.get('settings/gist-access-token') const accessToken = config.get('settings/gist-access-token')
if (!accessToken) { if (!accessToken) {
modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', { modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', () => {})
label: 'Close',
fn: async () => {}
}, null)
} else { } 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=' + 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=' queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist='
@ -699,6 +459,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const runScript = async (path: string) => { const runScript = async (path: string) => {
const filesProvider = fileSystem.provider.provider
filesProvider.get(path, (error, content: string) => { filesProvider.get(path, (error, content: string) => {
if (error) return console.log(error) if (error) return console.log(error)
plugin.call('scriptRunner', 'execute', content) plugin.call('scriptRunner', 'execute', content)
@ -715,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 => { setState(prevState => {
return { return {
...prevState, ...prevState,
@ -723,8 +485,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
{ {
message, message,
title, title,
ok, okLabel,
cancel, okFn,
cancelLabel,
cancelFn,
handleHide: handleHideModal handleHide: handleHideModal
}] }]
} }
@ -738,6 +502,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const handleClickFile = (path: string) => { const handleClickFile = (path: string) => {
path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path
state.fileManager.open(path) state.fileManager.open(path)
setState(prevState => { setState(prevState => {
return { ...prevState, focusElement: [{ key: path, type: 'file' }] } return { ...prevState, focusElement: [{ key: path, type: 'file' }] }
@ -760,6 +525,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
if (!state.expandPath.includes(path)) { if (!state.expandPath.includes(path)) {
expandPath = [...new Set([...state.expandPath, path])] expandPath = [...new Set([...state.expandPath, path])]
resolveDirectory(fileSystem.provider.provider, path)(dispatch)
} else { } else {
expandPath = [...new Set(state.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))] expandPath = [...new Set(state.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))]
} }
@ -791,7 +557,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const editModeOn = (path: string, type: string, isNew: boolean = false) => { const editModeOn = (path: string, type: string, isNew: boolean = false) => {
if (filesProvider.isReadOnly(path)) return if (fileSystem.provider.provider.isReadOnly(path)) return
setState(prevState => { setState(prevState => {
return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } } return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } }
}) })
@ -803,11 +569,9 @@ export const FileExplorer = (props: FileExplorerProps) => {
if (!content || (content.trim() === '')) { if (!content || (content.trim() === '')) {
if (state.focusEdit.isNew) { if (state.focusEdit.isNew) {
const files = removePath(state.focusEdit.element, state.files) removeInputField(parentFolder)(dispatch)
const updatedFiles = files.filter(file => file)
setState(prevState => { setState(prevState => {
return { ...prevState, files: updatedFiles, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
}) })
} else { } else {
editRef.current.textContent = state.focusEdit.lastEdit editRef.current.textContent = state.focusEdit.lastEdit
@ -823,26 +587,28 @@ export const FileExplorer = (props: FileExplorerProps) => {
}) })
} }
if (helper.checkSpecialChars(content)) { if (helper.checkSpecialChars(content)) {
modal('Validation Error', 'Special characters are not allowed', { modal('Validation Error', 'Special characters are not allowed', 'OK', () => {})
label: 'OK',
fn: () => {}
}, null)
} else { } else {
if (state.focusEdit.isNew) { if (state.focusEdit.isNew) {
state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content)) if (hasReservedKeyword(content)) {
const files = removePath(state.focusEdit.element, state.files) removeInputField(parentFolder)(dispatch)
const updatedFiles = files.filter(file => file) modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {})
} else {
setState(prevState => { state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content))
return { ...prevState, files: updatedFiles } removeInputField(parentFolder)(dispatch)
}) }
} else { } else {
const oldPath: string = state.focusEdit.element if (hasReservedKeyword(content)) {
const oldName = extractNameFromKey(oldPath) editRef.current.textContent = state.focusEdit.lastEdit
const newPath = oldPath.replace(oldName, content) 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) editRef.current.textContent = extractNameFromKey(oldPath)
renamePath(oldPath, newPath) renamePath(oldPath, newPath)
}
} }
setState(prevState => { setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
@ -852,9 +618,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const handleNewFileInput = async (parentFolder?: string) => { const handleNewFileInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key : extractParentFromKey(state.focusElement[0].key) : name if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key ? state.focusElement[0].key : name : extractParentFromKey(state.focusElement[0].key) ? extractParentFromKey(state.focusElement[0].key) : name : name
const expandPath = [...new Set([...state.expandPath, parentFolder])] const expandPath = [...new Set([...state.expandPath, parentFolder])]
await addInputField(fileSystem.provider.provider, 'file', parentFolder)(dispatch)
setState(prevState => { setState(prevState => {
return { ...prevState, expandPath } return { ...prevState, expandPath }
}) })
@ -862,10 +629,11 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const handleNewFolderInput = async (parentFolder?: string) => { const handleNewFolderInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key : extractParentFromKey(state.focusElement[0].key) : name if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key ? state.focusElement[0].key : name : extractParentFromKey(state.focusElement[0].key) ? extractParentFromKey(state.focusElement[0].key) : name : name
else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder) else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder)
const expandPath = [...new Set([...state.expandPath, parentFolder])] const expandPath = [...new Set([...state.expandPath, parentFolder])]
await addInputField(fileSystem.provider.provider, 'folder', parentFolder)(dispatch)
setState(prevState => { setState(prevState => {
return { ...prevState, expandPath } return { ...prevState, expandPath }
}) })
@ -916,12 +684,16 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const renderFiles = (file: File, index: number) => { const renderFiles = (file: File, index: number) => {
if (!file || !file.path || typeof file === 'string' || typeof file === 'number' || typeof file === 'boolean') return
const labelClass = state.focusEdit.element === file.path const labelClass = state.focusEdit.element === file.path
? 'bg-light' : state.focusElement.findIndex(item => item.key === file.path) !== -1 ? 'bg-light' : state.focusElement.findIndex(item => item.key === file.path) !== -1
? 'bg-secondary' : state.mouseOverElement === file.path ? 'bg-secondary' : state.mouseOverElement === file.path
? 'bg-light border' : (state.focusContext.element === file.path) && (state.focusEdit.element !== file.path) ? 'bg-light border' : (state.focusContext.element === file.path) && (state.focusEdit.element !== file.path)
? 'bg-light border' : '' ? 'bg-light border' : ''
const icon = helper.getPathIcon(file.path) const icon = helper.getPathIcon(file.path)
const spreadProps = {
onClick: (e) => e.stopPropagation()
}
if (file.isDirectory) { if (file.isDirectory) {
return ( return (
@ -953,12 +725,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
}} }}
> >
{ {
file.child ? <TreeView id={`treeView${file.path}`} key={index}>{ file.child ? <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }>{
file.child.map((file, index) => { Object.keys(file.child).map((key, index) => {
return renderFiles(file, index) return renderFiles(file.child[key], index)
}) })
} }
</TreeView> : <TreeView id={`treeView${file.path}`} key={index} /> </TreeView> : <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }/>
} }
</TreeViewItem> </TreeViewItem>
) )
@ -966,7 +738,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
return ( return (
<TreeViewItem <TreeViewItem
id={`treeViewItem${file.path}`} id={`treeViewItem${file.path}`}
key={index} key={`treeView${file.path}`}
label={label(file)} label={label(file)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
@ -1029,8 +801,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
<div className='pb-2'> <div className='pb-2'>
<TreeView id='treeViewMenu'> <TreeView id='treeViewMenu'>
{ {
state.files.map((file, index) => { fileSystem.files.files[props.name] && Object.keys(fileSystem.files.files[props.name]).map((key, index) => {
return renderFiles(file, index) return renderFiles(fileSystem.files.files[props.name][key], index)
}) })
} }
</TreeView> </TreeView>
@ -1043,8 +815,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
title={ state.focusModal.title } title={ state.focusModal.title }
message={ state.focusModal.message } message={ state.focusModal.message }
hide={ state.focusModal.hide } hide={ state.focusModal.hide }
ok={ state.focusModal.ok } okLabel={ state.focusModal.okLabel }
cancel={ state.focusModal.cancel } okFn={ state.focusModal.okFn }
cancelLabel={ state.focusModal.cancelLabel }
cancelFn={ state.focusModal.cancelFn }
handleHide={ handleHideModal } handleHide={ handleHideModal }
/> />
} }

@ -0,0 +1,344 @@
import * as _ from 'lodash'
import { extractNameFromKey } from '../utils'
interface Action {
type: string;
payload: Record<string, any>;
}
export const fileSystemInitialState = {
files: {
files: [],
expandPath: [],
workspaceName: null,
blankPath: null,
isRequesting: false,
isSuccessful: false,
error: null
},
provider: {
provider: null,
isRequesting: false,
isSuccessful: false,
error: null
},
notification: {
title: null,
message: null,
actionOk: () => {},
actionCancel: () => {},
labelOk: null,
labelCancel: null
}
}
export const fileSystemReducer = (state = fileSystemInitialState, action: Action) => {
switch (action.type) {
case 'FETCH_DIRECTORY_REQUEST': {
return {
...state,
files: {
...state.files,
isRequesting: true,
isSuccessful: false,
error: null
}
}
}
case 'FETCH_DIRECTORY_SUCCESS': {
return {
...state,
files: {
...state.files,
files: action.payload.files,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FETCH_DIRECTORY_ERROR': {
return {
...state,
files: {
...state.files,
isRequesting: false,
isSuccessful: false,
error: action.payload
}
}
}
case 'RESOLVE_DIRECTORY_REQUEST': {
return {
...state,
files: {
...state.files,
isRequesting: true,
isSuccessful: false,
error: null
}
}
}
case 'RESOLVE_DIRECTORY_SUCCESS': {
return {
...state,
files: {
...state.files,
files: resolveDirectory(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files),
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'RESOLVE_DIRECTORY_ERROR': {
return {
...state,
files: {
...state.files,
isRequesting: false,
isSuccessful: false,
error: action.payload
}
}
}
case 'FETCH_PROVIDER_REQUEST': {
return {
...state,
provider: {
...state.provider,
isRequesting: true,
isSuccessful: false,
error: null
}
}
}
case 'FETCH_PROVIDER_SUCCESS': {
return {
...state,
provider: {
...state.provider,
provider: action.payload,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FETCH_PROVIDER_ERROR': {
return {
...state,
provider: {
...state.provider,
isRequesting: false,
isSuccessful: false,
error: action.payload
}
}
}
case 'SET_CURRENT_WORKSPACE': {
return {
...state,
files: {
...state.files,
workspaceName: action.payload
}
}
}
case 'ADD_INPUT_FIELD': {
return {
...state,
files: {
...state.files,
files: addInputField(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files),
blankPath: action.payload.path,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'REMOVE_INPUT_FIELD': {
return {
...state,
files: {
...state.files,
files: removeInputField(state.files.workspaceName, state.files.blankPath, state.files.files),
blankPath: null,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FILE_ADDED': {
return {
...state,
files: {
...state.files,
files: fileAdded(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files),
expandPath: [...new Set([...state.files.expandPath, action.payload.path])],
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FOLDER_ADDED': {
return {
...state,
files: {
...state.files,
files: folderAdded(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files),
expandPath: [...new Set([...state.files.expandPath, action.payload.path])],
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FILE_REMOVED': {
return {
...state,
files: {
...state.files,
files: fileRemoved(state.files.workspaceName, action.payload.path, action.payload.removePath, state.files.files),
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FILE_RENAMED': {
return {
...state,
files: {
...state.files,
files: fileRenamed(state.files.workspaceName, action.payload.path, action.payload.removePath, state.files.files, action.payload.files),
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'DISPLAY_NOTIFICATION': {
return {
...state,
notification: {
title: action.payload.title,
message: action.payload.message,
actionOk: action.payload.actionOk || fileSystemInitialState.notification.actionOk,
actionCancel: action.payload.actionCancel || fileSystemInitialState.notification.actionCancel,
labelOk: action.payload.labelOk,
labelCancel: action.payload.labelCancel
}
}
}
case 'HIDE_NOTIFICATION': {
return {
...state,
notification: fileSystemInitialState.notification
}
}
default:
throw new Error()
}
}
const resolveDirectory = (root, path: string, files, content) => {
if (path === root) return { [root]: { ...content[root], ...files[root] } }
const pathArr: string[] = path.split('/').filter(value => value)
if (pathArr[0] !== root) pathArr.unshift(root)
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => {
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]
}, [])
const prevFiles = _.get(files, _path)
files = _.set(files, _path, {
isDirectory: true,
path,
name: extractNameFromKey(path),
child: { ...content[pathArr[pathArr.length - 1]], ...(prevFiles ? prevFiles.child : {}) }
})
return files
}
const removePath = (root, path: string, pathName, files) => {
const pathArr: string[] = path.split('/').filter(value => value)
if (pathArr[0] !== root) pathArr.unshift(root)
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => {
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]
}, [])
const prevFiles = _.get(files, _path)
prevFiles && prevFiles.child && prevFiles.child[pathName] && delete prevFiles.child[pathName]
files = _.set(files, _path, {
isDirectory: true,
path,
name: extractNameFromKey(path),
child: prevFiles ? prevFiles.child : {}
})
return files
}
const addInputField = (root, path: string, files, content) => {
if (path === root) return { [root]: { ...content[root], ...files[root] } }
const result = resolveDirectory(root, path, files, content)
return result
}
const removeInputField = (root, path: string, files) => {
if (path === root) {
delete files[root][path + '/' + 'blank']
return files
}
return removePath(root, path, path + '/' + 'blank', files)
}
const fileAdded = (root, path: string, files, content) => {
return resolveDirectory(root, path, files, content)
}
const folderAdded = (root, path: string, files, content) => {
return resolveDirectory(root, path, files, content)
}
const fileRemoved = (root, path: string, removedPath: string, files) => {
if (path === root) {
delete files[root][removedPath]
return files
}
return removePath(root, path, extractNameFromKey(removedPath), files)
}
const fileRenamed = (root, path: string, removePath: string, files, content) => {
if (path === root) {
const allFiles = { [root]: { ...content[root], ...files[root] } }
delete allFiles[root][extractNameFromKey(removePath) || removePath]
return allFiles
}
const pathArr: string[] = path.split('/').filter(value => value)
if (pathArr[0] !== root) pathArr.unshift(root)
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => {
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]
}, [])
const prevFiles = _.get(files, _path)
delete prevFiles.child[extractNameFromKey(removePath)]
files = _.set(files, _path, {
isDirectory: true,
path,
name: extractNameFromKey(path),
child: { ...content[pathArr[pathArr.length - 1]], ...prevFiles.child }
})
return files
}

@ -6,7 +6,7 @@ export interface FileExplorerProps {
menuItems?: string[], menuItems?: string[],
plugin: any, plugin: any,
focusRoot: boolean, focusRoot: boolean,
contextMenuItems: { name: string, type: string[], path: string[], extension: string[], pattern: string[] }[], contextMenuItems: { id: string, name: string, type: string[], path: string[], extension: string[], pattern: string[] }[],
displayInput?: boolean, displayInput?: boolean,
externalUploads?: EventTarget & HTMLInputElement externalUploads?: EventTarget & HTMLInputElement
} }

@ -0,0 +1,13 @@
export const extractNameFromKey = (key: string): string => {
const keyPath = key.split('/')
return keyPath[keyPath.length - 1]
}
export const extractParentFromKey = (key: string):string => {
if (!key) return
const keyPath = key.split('/')
keyPath.pop()
return keyPath.join('/')
}

@ -18,7 +18,7 @@ export const ModalDialog = (props: ModalDialogProps) => {
const modalKeyEvent = (keyCode) => { const modalKeyEvent = (keyCode) => {
if (keyCode === 27) { // Esc if (keyCode === 27) { // Esc
if (props.cancel && props.cancel.fn) props.cancel.fn() if (props.cancelFn) props.cancelFn()
handleHide() handleHide()
} else if (keyCode === 13) { // Enter } else if (keyCode === 13) { // Enter
enterHandler() enterHandler()
@ -33,9 +33,9 @@ export const ModalDialog = (props: ModalDialogProps) => {
const enterHandler = () => { const enterHandler = () => {
if (state.toggleBtn) { if (state.toggleBtn) {
if (props.ok && props.ok.fn) props.ok.fn() if (props.okFn) props.okFn()
} else { } else {
if (props.cancel && props.cancel.fn) props.cancel.fn() if (props.cancelFn) props.cancelFn()
} }
handleHide() handleHide()
} }
@ -79,29 +79,29 @@ export const ModalDialog = (props: ModalDialogProps) => {
</div> </div>
<div className="modal-footer" data-id={`${props.id}ModalDialogModalFooter-react`}> <div className="modal-footer" data-id={`${props.id}ModalDialogModalFooter-react`}>
{/* todo add autofocus ^^ */} {/* todo add autofocus ^^ */}
{ props.ok && { props.okLabel &&
<span <span
data-id={`${props.id}-modal-footer-ok-react`} data-id={`${props.id}-modal-footer-ok-react`}
className={'modal-ok btn btn-sm ' + (state.toggleBtn ? 'btn-dark' : 'btn-light')} className={'modal-ok btn btn-sm ' + (state.toggleBtn ? 'btn-dark' : 'btn-light')}
onClick={() => { onClick={() => {
if (props.ok.fn) props.ok.fn() if (props.okFn) props.okFn()
handleHide() handleHide()
}} }}
> >
{ props.ok.label ? props.ok.label : 'OK' } { props.okLabel ? props.okLabel : 'OK' }
</span> </span>
} }
{ props.cancel && { props.cancelLabel &&
<span <span
data-id={`${props.id}-modal-footer-cancel-react`} data-id={`${props.id}-modal-footer-cancel-react`}
className={'modal-cancel btn btn-sm ' + (state.toggleBtn ? 'btn-light' : 'btn-dark')} className={'modal-cancel btn btn-sm ' + (state.toggleBtn ? 'btn-light' : 'btn-dark')}
data-dismiss="modal" data-dismiss="modal"
onClick={() => { onClick={() => {
if (props.cancel.fn) props.cancel.fn() if (props.cancelFn) props.cancelFn()
handleHide() handleHide()
}} }}
> >
{ props.cancel.label ? props.cancel.label : 'Cancel' } { props.cancelLabel ? props.cancelLabel : 'Cancel' }
</span> </span>
} }
</div> </div>

@ -2,8 +2,10 @@ export interface ModalDialogProps {
id?: string id?: string
title?: string, title?: string,
message?: string, message?: string,
ok?: { label: string, fn: () => void }, okLabel?: string,
cancel: { label: string, fn: () => void }, okFn?: () => void,
cancelLabel?: string,
cancelFn?: () => void,
modalClass?: string, modalClass?: string,
showCancelIcon?: boolean, showCancelIcon?: boolean,
hide: boolean, hide: boolean,

@ -0,0 +1,4 @@
{
"presets": ["@nrwl/react/babel"],
"plugins": []
}

@ -0,0 +1,19 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": "../../../.eslintrc",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error"
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save