pull/5370/head
filip mertens 4 years ago
commit 592bcc649b
  1. 3
      .circleci/config.yml
  2. 3
      apps/remix-ide-e2e/src/commands/getLastTransactionHash.ts
  3. 29
      apps/remix-ide-e2e/src/tests/ballot.test.ts
  4. 3
      apps/remix-ide-e2e/src/tests/ballot_0_4_11.spec.ts
  5. 4
      apps/remix-ide-e2e/src/tests/fileManager_api.spec.ts
  6. 12
      apps/remix-ide-e2e/src/tests/gist.spec.ts
  7. 3
      apps/remix-ide-e2e/src/tests/runAndDeploy.ts
  8. 2
      apps/remix-ide-e2e/src/tests/solidityImport.spec.ts
  9. 2
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  10. 33
      apps/remix-ide-e2e/src/tests/transactionExecution.spec.ts
  11. 2
      apps/remix-ide/ci/makeMockCompiler.js
  12. 13
      apps/remix-ide/src/app.js
  13. 5
      apps/remix-ide/src/app/components/plugin-manager-component.js
  14. 399
      apps/remix-ide/src/app/files/dgitProvider.js
  15. 12
      apps/remix-ide/src/app/files/file-explorer.js
  16. 84
      apps/remix-ide/src/app/files/fileManager.js
  17. 16
      apps/remix-ide/src/app/files/fileProvider.js
  18. 18
      apps/remix-ide/src/app/files/hardhat-handle.js
  19. 24
      apps/remix-ide/src/app/files/remixDProvider.js
  20. 33
      apps/remix-ide/src/app/files/remixd-handle.js
  21. 4
      apps/remix-ide/src/app/files/workspaceFileProvider.js
  22. 22
      apps/remix-ide/src/app/panels/file-panel.js
  23. 35
      apps/remix-ide/src/app/panels/terminal.js
  24. 31
      apps/remix-ide/src/app/tabs/compile-tab.js
  25. 47
      apps/remix-ide/src/app/tabs/compileTab/compileTab.js
  26. 16
      apps/remix-ide/src/app/tabs/compileTab/compilerContainer.js
  27. 71
      apps/remix-ide/src/app/tabs/hardhat-provider.js
  28. 12
      apps/remix-ide/src/app/tabs/network-module.js
  29. 31
      apps/remix-ide/src/app/tabs/runTab/contractDropdown.js
  30. 2
      apps/remix-ide/src/app/tabs/runTab/model/dropdownlogic.js
  31. 4
      apps/remix-ide/src/app/tabs/runTab/model/recorder.js
  32. 45
      apps/remix-ide/src/app/tabs/runTab/settings.js
  33. 2
      apps/remix-ide/src/app/tabs/test-tab.js
  34. 2
      apps/remix-ide/src/app/udapp/run-tab.js
  35. 51
      apps/remix-ide/src/app/ui/confirmDialog.js
  36. 10
      apps/remix-ide/src/app/ui/universal-dapp-ui.js
  37. 133
      apps/remix-ide/src/blockchain/blockchain.js
  38. 166
      apps/remix-ide/src/blockchain/execution-context.js
  39. 6
      apps/remix-ide/src/blockchain/providers/vm.js
  40. 46
      apps/remix-ide/src/blockchain/txResultHelper.js
  41. 4
      apps/remix-ide/src/lib/gist-handler.js
  42. 19
      apps/remix-ide/src/lib/helper.js
  43. 10
      apps/remix-ide/src/remixAppManager.js
  44. 15
      jest.config.js
  45. 6
      libs/remix-debug/src/solidity-decoder/types/Mapping.ts
  46. 36
      libs/remix-debug/src/solidity-decoder/types/RefType.ts
  47. 14
      libs/remix-debug/test/decoder/contracts/calldata.ts
  48. 23
      libs/remix-debug/test/decoder/localDecoder.ts
  49. 61
      libs/remix-debug/test/decoder/localsTests/calldata.ts
  50. 9
      libs/remix-debug/test/decoder/localsTests/int.ts
  51. 9
      libs/remix-debug/test/decoder/localsTests/misc.ts
  52. 9
      libs/remix-debug/test/decoder/localsTests/misc2.ts
  53. 9
      libs/remix-debug/test/decoder/localsTests/structArray.ts
  54. 7
      libs/remix-debug/test/decoder/stateTests/mapping.ts
  55. 10
      libs/remix-debug/test/decoder/vmCall.ts
  56. 44
      libs/remix-lib/src/execution/txExecution.ts
  57. 6
      libs/remix-lib/src/execution/txFormat.ts
  58. 5
      libs/remix-lib/src/execution/txHelper.ts
  59. 44
      libs/remix-lib/src/execution/txListener.ts
  60. 250
      libs/remix-lib/src/execution/txRunner.ts
  61. 121
      libs/remix-lib/src/execution/txRunnerVM.ts
  62. 147
      libs/remix-lib/src/execution/txRunnerWeb3.ts
  63. 8
      libs/remix-lib/src/helpers/txResultHelper.ts
  64. 20
      libs/remix-lib/src/index.ts
  65. 379
      libs/remix-lib/src/universalDapp.ts
  66. 4
      libs/remix-lib/src/util.ts
  67. 6
      libs/remix-lib/src/web3Provider/web3VmProvider.ts
  68. 7
      libs/remix-lib/test/txFormat.ts
  69. 31
      libs/remix-lib/test/txResultHelper.ts
  70. 2
      libs/remix-simulator/package.json
  71. 8
      libs/remix-simulator/src/genesis.ts
  72. 2
      libs/remix-simulator/src/index.ts
  73. 16
      libs/remix-simulator/src/methods/accounts.ts
  74. 18
      libs/remix-simulator/src/methods/blocks.ts
  75. 12
      libs/remix-simulator/src/methods/debug.ts
  76. 24
      libs/remix-simulator/src/methods/filters.ts
  77. 97
      libs/remix-simulator/src/methods/transactions.ts
  78. 41
      libs/remix-simulator/src/methods/txProcess.ts
  79. 56
      libs/remix-simulator/src/provider.ts
  80. 169
      libs/remix-simulator/src/vm-context.ts
  81. 2
      libs/remix-solidity/package.json
  82. 3
      libs/remix-tests/jest.config.js
  83. 6
      libs/remix-tests/package.json
  84. 2
      libs/remix-tests/src/deployer.ts
  85. 3
      libs/remix-tests/tests/testRunner.cli.spec.ts
  86. 2
      libs/remix-tests/tsconfig.json
  87. 1
      libs/remix-tests/tsconfig.lib.json
  88. 1
      libs/remix-tests/tsconfig.spec.json
  89. 5
      libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx
  90. 31
      libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts
  91. 54
      libs/remix-ui/file-explorer/src/lib/file-explorer-context-menu.tsx
  92. 428
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  93. 63
      libs/remix-ui/file-explorer/src/lib/reducers/fileSystem.ts
  94. 21
      libs/remix-ui/file-explorer/src/lib/types/index.ts
  95. 18
      libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx
  96. 6
      libs/remix-ui/modal-dialog/src/lib/types/index.ts
  97. 6
      libs/remix-ui/toaster/src/lib/toaster.tsx
  98. 82
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  99. 8
      libs/remix-url-resolver/tests/test.ts
  100. 16
      libs/remixd/src/bin/remixd.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -239,6 +239,7 @@ jobs:
- checkout - checkout
- run: npm install - run: npm install
- run: npx nx build remix-ide --with-deps - run: npx nx build remix-ide --with-deps
- run: npm run downloadsolc_assets
- run: - run:
name: Deploy name: Deploy
command: | command: |
@ -291,6 +292,7 @@ jobs:
- checkout - checkout
- run: npm install - run: npm install
- run: npx nx build remix-ide --with-deps - run: npx nx build remix-ide --with-deps
- run: npm run downloadsolc_assets
- run: - run:
name: Deploy name: Deploy
command: | command: |
@ -319,6 +321,7 @@ jobs:
- run: npm install - run: npm install
- run: npm run build:libs - run: npm run build:libs
- run: npm run build - run: npm run build
- run: npm run downloadsolc_assets
- run: - run:
name: Deploy name: Deploy
command: | command: |

@ -21,7 +21,8 @@ function getLastTransactionHash (browser: NightwatchBrowser, callback: (hash: st
for (let i = deployedContracts.length - 1; i >= 0; i--) { for (let i = deployedContracts.length - 1; i >= 0; i--) {
const current = deployedContracts[i] const current = deployedContracts[i]
const attr = current.getAttribute('data-id') const attr = current.getAttribute('data-id')
if (attr && attr.replace('block_tx', '').startsWith('0x')) { // For web3 provider, a contract call simulates a tx hash starting with 'block_txcall'
if (attr && (attr.replace('block_tx', '').startsWith('0x') || attr.replace('block_txcall', '').startsWith('0x'))) {
return attr.replace('block_tx', '') return attr.replace('block_tx', '')
} }
} }

@ -34,6 +34,19 @@ module.exports = {
}) })
}, },
'Call method from Ballot to check return value': function (browser: NightwatchBrowser) {
browser
.clickFunction('winnerName - call')
// Test in terminal
.testFunction('last',
{
to: 'Ballot.winnerName() 0x692a70D2e424a56D2C6C27aA97D1a86395877b3A',
'decoded output': { 0: 'bytes32: winnerName_ 0x48656c6c6f20576f726c64210000000000000000000000000000000000000000' }
})
// Test in Udapp UI , treeViewDiv0 shows returned value on method click
.assert.containsText('*[data-id="treeViewDiv0"]', 'bytes32: winnerName_ 0x48656c6c6f20576f726c64210000000000000000000000000000000000000000')
},
'Debug Ballot / delegate': function (browser: NightwatchBrowser) { 'Debug Ballot / delegate': function (browser: NightwatchBrowser) {
browser.pause(500) browser.pause(500)
.click('*[data-id="txLoggerDebugButton0x41fab8ea5b1d9fba5e0a6545ca1a2d62fff518578802c033c2b9a031a01c31b3"]') .click('*[data-id="txLoggerDebugButton0x41fab8ea5b1d9fba5e0a6545ca1a2d62fff518578802c033c2b9a031a01c31b3"]')
@ -51,7 +64,8 @@ module.exports = {
browser.clickLaunchIcon('udapp') browser.clickLaunchIcon('udapp')
.click('*[data-id="universalDappUiUdappClose"]') .click('*[data-id="universalDappUiUdappClose"]')
.addFile('ballot.abi', { content: ballotABI }) .addFile('ballot.abi', { content: ballotABI })
.addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3B', true, false) // we are not changing the visibility for not checksumed contracts
// .addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3B', true, false)
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3A', true, true) .addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3A', true, true)
.pause(500) .pause(500)
@ -79,6 +93,19 @@ module.exports = {
.clickFunction('delegate - transact (not payable)', { types: 'address to', values: '0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c' }) .clickFunction('delegate - transact (not payable)', { types: 'address to', values: '0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c' })
.journalLastChildIncludes('Ballot.delegate(address)') .journalLastChildIncludes('Ballot.delegate(address)')
.journalLastChildIncludes('data: 0x5c1...a733c') .journalLastChildIncludes('data: 0x5c1...a733c')
},
'Call method from Ballot to check return value using external web3': function (browser: NightwatchBrowser) {
browser
.clickFunction('winnerName - call')
// Test in terminal
.journalLastChildIncludes('Ballot.winnerName()')
.testFunction('last',
{
'decoded output': { 0: 'bytes32: winnerName_ 0x48656c6c6f20576f726c64210000000000000000000000000000000000000000' }
})
// Test in Udapp UI , treeViewDiv0 shows returned value on method click
.assert.containsText('*[data-id="treeViewDiv0"]', 'bytes32: winnerName_ 0x48656c6c6f20576f726c64210000000000000000000000000000000000000000')
.end() .end()
} }
} }

@ -60,7 +60,8 @@ module.exports = {
browser.clickLaunchIcon('udapp') browser.clickLaunchIcon('udapp')
.click('*[data-id="universalDappUiUdappClose"]') .click('*[data-id="universalDappUiUdappClose"]')
.addFile('ballot.abi', { content: ballotABI }) .addFile('ballot.abi', { content: ballotABI })
.addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3B', true, false) // we are not changing the visibility for not checksumed contracts
// .addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3B', true, false)
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3A', true, true) .addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3A', true, true)
.pause(500) .pause(500)

@ -147,8 +147,8 @@ const executeReadFile = `
const executeCopyFile = ` const executeCopyFile = `
const run = async () => { const run = async () => {
await remix.call('fileManager', 'copyFile', 'contracts/3_Ballot.sol', 'new_contract.sol') await remix.call('fileManager', 'copyFile', 'contracts/3_Ballot.sol', '/', 'copy_contract.sol')
const result = await remix.call('fileManager', 'readFile', 'new_contract.sol') const result = await remix.call('fileManager', 'readFile', 'copy_contract.sol')
console.log(result) console.log(result)
} }

@ -54,9 +54,9 @@ module.exports = {
.click('[data-id="default_workspace-modal-footer-cancel-react"]') .click('[data-id="default_workspace-modal-footer-cancel-react"]')
.executeScript(`remix.loadgist('${gistid}')`) .executeScript(`remix.loadgist('${gistid}')`)
// .perform((done) => { if (runtimeBrowser === 'chrome') { browser.openFile('gists') } done() }) // .perform((done) => { if (runtimeBrowser === 'chrome') { browser.openFile('gists') } done() })
.waitForElementVisible(`[data-id="treeViewLitreeViewItem${gistid}"]`) .waitForElementVisible(`[data-id="treeViewLitreeViewItem/gist-${gistid}"]`)
.click(`[data-id="treeViewLitreeViewItem${gistid}"]`) .click(`[data-id="treeViewLitreeViewItem/gist-${gistid}"]`)
.openFile(`${gistid}/README.txt`) .openFile(`gist-${gistid}/README.txt`)
} }
}) })
}, },
@ -118,9 +118,9 @@ module.exports = {
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptText"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', testData.validGistId) .setValue('*[data-id="modalDialogCustomPromptText"]', testData.validGistId)
.modalFooterOKClick() .modalFooterOKClick()
.openFile(`${testData.validGistId}/ApplicationRegistry`) .openFile(`gist-${testData.validGistId}/ApplicationRegistry`)
.waitForElementVisible(`div[title='default_workspace/${testData.validGistId}/ApplicationRegistry']`) .waitForElementVisible(`div[title='default_workspace/gist-${testData.validGistId}/ApplicationRegistry']`)
.assert.containsText(`div[title='default_workspace/${testData.validGistId}/ApplicationRegistry'] > span`, 'ApplicationRegistry') .assert.containsText(`div[title='default_workspace/gist-${testData.validGistId}/ApplicationRegistry'] > span`, 'ApplicationRegistry')
.end() .end()
} }
} }

@ -69,6 +69,9 @@ module.exports = {
.testFunction('last', { .testFunction('last', {
status: 'true Transaction mined and execution succeed' status: 'true Transaction mined and execution succeed'
}) })
// When this is removed and tests are running by connecting to metamask
// Consider adding tests to check return value of contract call
// See: https://github.com/ethereum/remix-project/pull/1229
.end() .end()
}, },

@ -80,7 +80,7 @@ module.exports = {
'Test NPM Import (with unpkg.com)': function (browser: NightwatchBrowser) { 'Test NPM Import (with unpkg.com)': function (browser: NightwatchBrowser) {
browser browser
.setSolidityCompilerVersion('soljson-v0.8.1+commit.df193b15.js') .setSolidityCompilerVersion('soljson-v0.8.4+commit.c7e474f2.js')
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.click('li[data-id="treeViewLitreeViewItemREADME.txt"') .click('li[data-id="treeViewLitreeViewItemREADME.txt"')
.addFile('Untitled9.sol', sources[8]['Untitled9.sol']) .addFile('Untitled9.sol', sources[8]['Untitled9.sol'])

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

@ -137,6 +137,21 @@ module.exports = {
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite .selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite
.click('#runTabView button[class^="instanceButton"]') .click('#runTabView button[class^="instanceButton"]')
.waitForElementPresent('.instance:nth-of-type(2)') .waitForElementPresent('.instance:nth-of-type(2)')
},
'Should Compile and Deploy a contract which define a custom error, the error should be logged in the terminal': function (browser: NightwatchBrowser) {
browser.testContracts('customError.sol', sources[4]['customError.sol'], ['C'])
.clickLaunchIcon('udapp')
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite
.click('#runTabView button[class^="instanceButton"]')
.waitForElementPresent('.instance:nth-of-type(3)')
.click('.instance:nth-of-type(3) > div > button')
.clickFunction('g - transact (not payable)')
.journalLastChildIncludes('Error provided by the contract:')
.journalLastChildIncludes('CustomError')
.journalLastChildIncludes('Parameters:')
.journalLastChildIncludes('2,3,error_string_2')
.journalLastChildIncludes('Debug the transaction to get more information.')
.end() .end()
} }
} }
@ -218,5 +233,23 @@ contract C {
event Test(function() external); event Test(function() external);
}` }`
} }
},
// https://github.com/ethereum/remix-project/issues/1152
{
'customError.sol': {
content: `// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
error CustomError(uint a, uint b, string c);
contract C {
function f() public pure {
revert CustomError(2, 3, "error_string");
}
function g() public {
revert CustomError(2, 3, "error_string_2");
}
}`
}
} }
] ]

@ -3,7 +3,7 @@
var fs = require('fs') var fs = require('fs')
var compiler = require('solc') var compiler = require('solc')
var compilerInput = require('@remix-project/remix-solidity').CompilerInput var compilerInput = require('@remix-project/remix-solidity').CompilerInput
var defaultVersion = 'soljson-v0.8.1+commit.df193b15.js' var defaultVersion = 'soljson-v0.8.4+commit.c7e474f2.js'
const path = require('path') const path = require('path')
compiler.loadRemoteVersion(defaultVersion, (error, solcSnapshot) => { compiler.loadRemoteVersion(defaultVersion, (error, solcSnapshot) => {

@ -29,11 +29,13 @@ const { OffsetToLineColumnConverter } = require('./lib/offsetToLineColumnConvert
const QueryParams = require('./lib/query-params') const QueryParams = require('./lib/query-params')
const Storage = remixLib.Storage const Storage = remixLib.Storage
const RemixDProvider = require('./app/files/remixDProvider') const RemixDProvider = require('./app/files/remixDProvider')
const HardhatProvider = require('./app/tabs/hardhat-provider')
const Config = require('./config') const Config = require('./config')
const modalDialogCustom = require('./app/ui/modal-dialog-custom') const modalDialogCustom = require('./app/ui/modal-dialog-custom')
const modalDialog = require('./app/ui/modaldialog') const modalDialog = require('./app/ui/modaldialog')
const FileManager = require('./app/files/fileManager') const FileManager = require('./app/files/fileManager')
const FileProvider = require('./app/files/fileProvider') const FileProvider = require('./app/files/fileProvider')
const DGitProvider = require('./app/files/dgitProvider')
const WorkspaceFileProvider = require('./app/files/workspaceFileProvider') const WorkspaceFileProvider = require('./app/files/workspaceFileProvider')
const toolTip = require('./app/ui/tooltip') const toolTip = require('./app/ui/tooltip')
const CompilerMetadata = require('./app/files/compiler-metadata') const CompilerMetadata = require('./app/files/compiler-metadata')
@ -256,6 +258,8 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
// ----------------- fileManager service ---------------------------- // ----------------- fileManager service ----------------------------
const fileManager = new FileManager(editor, appManager) const fileManager = new FileManager(editor, appManager)
registry.put({ api: fileManager, name: 'filemanager' }) registry.put({ api: fileManager, name: 'filemanager' })
// ----------------- dGit provider ---------------------------------
const dGitProvider = new DGitProvider()
// ----------------- import content service ------------------------ // ----------------- import content service ------------------------
const contentImport = new CompilerImport(fileManager) const contentImport = new CompilerImport(fileManager)
@ -274,6 +278,7 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
const networkModule = new NetworkModule(blockchain) const networkModule = new NetworkModule(blockchain)
// ----------------- represent the current selected web3 provider ---- // ----------------- represent the current selected web3 provider ----
const web3Provider = new Web3ProviderModule(blockchain) const web3Provider = new Web3ProviderModule(blockchain)
const hardhatProvider = new HardhatProvider(blockchain)
// ----------------- convert offset to line/column service ----------- // ----------------- convert offset to line/column service -----------
const offsetToLineColumnConverter = new OffsetToLineColumnConverter() const offsetToLineColumnConverter = new OffsetToLineColumnConverter()
registry.put({ api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter' }) registry.put({ api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter' })
@ -309,7 +314,9 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
contextualListener, contextualListener,
terminal, terminal,
web3Provider, web3Provider,
fetchAndCompile fetchAndCompile,
dGitProvider,
hardhatProvider
]) ])
// LAYOUT & SYSTEM VIEWS // LAYOUT & SYSTEM VIEWS
@ -431,12 +438,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()) {

@ -114,6 +114,8 @@ class PluginManagerComponent extends ViewPlugin {
renderItem (profile) { renderItem (profile) {
const displayName = (profile.displayName) ? profile.displayName : profile.name const displayName = (profile.displayName) ? profile.displayName : profile.name
const doclink = profile.documentation ? yo`<a href="${profile.documentation}" class="px-1" title="link to documentation" target="_blank"><i aria-hidden="true" class="fas fa-book"></i></a>`
: yo``
// Check version of the plugin // Check version of the plugin
let versionWarning let versionWarning
@ -147,8 +149,11 @@ class PluginManagerComponent extends ViewPlugin {
<article id="remixPluginManagerListItem_${profile.name}" class="list-group-item py-1 mb-1 plugins-list-group-item" title="${displayName}" > <article id="remixPluginManagerListItem_${profile.name}" class="list-group-item py-1 mb-1 plugins-list-group-item" title="${displayName}" >
<div class="${css.row} justify-content-between align-items-center mb-2"> <div class="${css.row} justify-content-between align-items-center mb-2">
<h6 class="${css.displayName} plugin-name"> <h6 class="${css.displayName} plugin-name">
<div>
${displayName} ${displayName}
${doclink}
${versionWarning} ${versionWarning}
</div>
${activationButton} ${activationButton}
</h6> </h6>
</div> </div>

@ -0,0 +1,399 @@
'use strict'
import {
Plugin
} from '@remixproject/engine'
import git from 'isomorphic-git'
import IpfsHttpClient from 'ipfs-http-client'
import {
saveAs
} from 'file-saver'
const JSZip = require('jszip')
const path = require('path')
const FormData = require('form-data')
const axios = require('axios')
const profile = {
name: 'dGitProvider',
displayName: 'Decentralized git',
description: '',
icon: 'assets/img/fileManager.webp',
version: '0.0.1',
methods: ['init', 'status', 'log', 'commit', 'add', 'remove', 'rm', 'lsfiles', 'readblob', 'resolveref', 'branches', 'branch', 'checkout', 'currentbranch', 'push', 'pin', 'pull', 'pinList', 'unPin', 'setIpfsConfig', 'zip', 'setItem', 'getItem'],
kind: 'file-system'
}
class DGitProvider extends Plugin {
constructor () {
super(profile)
this.ipfsconfig = {
host: 'ipfs.komputing.org',
port: 443,
protocol: 'https',
ipfsurl: 'https://ipfsgw.komputing.org/ipfs/'
}
this.globalIPFSConfig = {
host: 'ipfs.io',
port: 443,
protocol: 'https',
ipfsurl: 'https://ipfs.io/ipfs/'
}
}
async getGitConfig () {
const workspace = await this.call('filePanel', 'getCurrentWorkspace')
return {
fs: window.remixFileSystem,
dir: workspace.absolutePath
}
}
async init () {
await git.init({
...await this.getGitConfig(),
defaultBranch: 'main'
})
}
async status (cmd) {
const status = await git.statusMatrix({
...await this.getGitConfig(),
...cmd
})
return status
}
async add (cmd) {
await git.add({
...await this.getGitConfig(),
...cmd
})
this.call('fileManager', 'refresh')
}
async rm (cmd) {
await git.remove({
...await this.getGitConfig(),
...cmd
})
this.call('fileManager', 'refresh')
}
async checkout (cmd) {
await git.checkout({
...await this.getGitConfig(),
...cmd
})
this.call('fileManager', 'refresh')
}
async log (cmd) {
const status = await git.log({
...await this.getGitConfig(),
...cmd
})
return status
}
async branch (cmd) {
const status = await git.branch({
...await this.getGitConfig(),
...cmd
})
this.call('fileManager', 'refresh')
return status
}
async currentbranch () {
const name = await git.currentBranch({
...await this.getGitConfig()
})
return name
}
async branches () {
const branches = await git.listBranches({
...await this.getGitConfig()
})
return branches
}
async commit (cmd) {
await this.init()
try {
const sha = await git.commit({
...await this.getGitConfig(),
...cmd
})
return sha
} catch (e) {}
}
async lsfiles (cmd) {
const filesInStaging = await git.listFiles({
...await this.getGitConfig(),
...cmd
})
return filesInStaging
}
async resolveref (cmd) {
const oid = await git.resolveRef({
...await this.getGitConfig(),
...cmd
})
return oid
}
async readblob (cmd) {
const readBlobResult = await git.readBlob({
...await this.getGitConfig(),
...cmd
})
return readBlobResult
}
async setIpfsConfig (config) {
this.ipfsconfig = config
return new Promise((resolve, reject) => {
resolve(this.checkIpfsConfig())
})
}
async checkIpfsConfig (config) {
this.ipfs = IpfsHttpClient(config || this.ipfsconfig)
try {
await this.ipfs.config.getAll()
return true
} catch (e) {
return false
}
}
async push () {
if (!this.checkIpfsConfig()) return false
const workspace = await this.call('filePanel', 'getCurrentWorkspace')
const files = await this.getDirectory('/')
this.filesToSend = []
for (const file of files) {
const c = window.remixFileSystem.readFileSync(`${workspace.absolutePath}/${file}`)
const ob = {
path: file,
content: c
}
this.filesToSend.push(ob)
}
const addOptions = {
wrapWithDirectory: true
}
const r = await this.ipfs.add(this.filesToSend, addOptions)
return r.cid.string
}
async pin (pinataApiKey, pinataSecretApiKey) {
const workspace = await this.call('filePanel', 'getCurrentWorkspace')
const files = await this.getDirectory('/')
this.filesToSend = []
const data = new FormData()
files.forEach(async (file) => {
const c = window.remixFileSystem.readFileSync(`${workspace.absolutePath}/${file}`)
data.append('file', new Blob([c]), `base/${file}`)
})
// get last commit data
let ob
try {
const commits = await this.log({ ref: 'HEAD' })
ob = {
ref: commits[0].oid,
message: commits[0].commit.message
}
} catch (e) {
ob = {
ref: 'no commits',
message: 'no commits'
}
}
const today = new Date()
const metadata = JSON.stringify({
name: `remix - ${workspace.name} - ${today.toLocaleString()}`,
keyvalues: ob
})
const pinataOptions = JSON.stringify({
wrapWithDirectory: false
})
data.append('pinataOptions', pinataOptions)
data.append('pinataMetadata', metadata)
const url = 'https://api.pinata.cloud/pinning/pinFileToIPFS'
try {
const result = await axios
.post(url, data, {
maxBodyLength: 'Infinity',
headers: {
'Content-Type': `multipart/form-data; boundary=${data._boundary}`,
pinata_api_key: pinataApiKey,
pinata_secret_api_key: pinataSecretApiKey
}
})
return result.data.IpfsHash
} catch (error) {
throw new Error(error)
}
}
async pinList (pinataApiKey, pinataSecretApiKey) {
const url = 'https://api.pinata.cloud/data/pinList?status=pinned'
try {
const result = await axios
.get(url, {
maxBodyLength: 'Infinity',
headers: {
pinata_api_key: pinataApiKey,
pinata_secret_api_key: pinataSecretApiKey
}
})
return result.data
} catch (error) {
throw new Error(error)
}
}
async unPin (pinataApiKey, pinataSecretApiKey, hashToUnpin) {
const url = `https://api.pinata.cloud/pinning/unpin/${hashToUnpin}`
try {
await axios
.delete(url, {
headers: {
pinata_api_key: pinataApiKey,
pinata_secret_api_key: pinataSecretApiKey
}
})
return true
} catch (error) {
throw new Error(error)
}
};
async pull (cmd) {
const permission = await this.askUserPermission('pull', 'Import multiple files into your workspaces.')
console.log(this.ipfsconfig)
if (!permission) return false
const cid = cmd.cid
if (!cmd.local) {
this.ipfs = IpfsHttpClient(this.globalIPFSConfig)
} else {
if (!this.checkIpfsConfig()) return false
}
await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, false)
const workspace = await this.call('filePanel', 'getCurrentWorkspace')
for await (const file of this.ipfs.get(cid)) {
file.path = file.path.replace(cid, '')
if (!file.content) {
continue
}
const content = []
for await (const chunk of file.content) {
content.push(chunk)
}
const dir = path.dirname(file.path)
try {
this.createDirectories(`${workspace.absolutePath}/${dir}`)
} catch (e) {}
try {
window.remixFileSystem.writeFileSync(`${workspace.absolutePath}/${file.path}`, Buffer.concat(content) || new Uint8Array())
} catch (e) {}
}
this.call('fileManager', 'refresh')
}
async getItem (name) {
if (typeof window !== 'undefined') {
return window.localStorage.getItem(name)
}
}
async setItem (name, content) {
try {
if (typeof window !== 'undefined') {
window.localStorage.setItem(name, content)
}
} catch (exception) {
return false
}
return true
}
async zip () {
const zip = new JSZip()
const workspace = await this.call('filePanel', 'getCurrentWorkspace')
const files = await this.getDirectory('/')
this.filesToSend = []
for (const file of files) {
const c = window.remixFileSystem.readFileSync(`${workspace.absolutePath}/${file}`)
zip.file(file, c)
}
await zip.generateAsync({
type: 'blob'
})
.then(function (content) {
saveAs(content, `${workspace.name}.zip`)
})
}
async createDirectories (strdirectories) {
const ignore = ['.', '/.', '']
if (ignore.indexOf(strdirectories) > -1) return false
const directories = strdirectories.split('/')
for (let i = 0; i < directories.length; i++) {
let previouspath = ''
if (i > 0) previouspath = '/' + directories.slice(0, i).join('/')
const finalPath = previouspath + '/' + directories[i]
try {
window.remixFileSystem.mkdirSync(finalPath)
} catch (e) {}
}
}
async getDirectory (dir) {
let result = []
const files = await this.call('fileManager', 'readdir', dir)
const fileArray = normalize(files)
for (const fi of fileArray) {
if (fi) {
const type = fi.data.isDirectory
if (type === true) {
result = [
...result,
...(await this.getDirectory(
`${fi.filename}`
))
]
} else {
result = [...result, fi.filename]
}
}
}
return result
}
}
const normalize = (filesList) => {
const folders = []
const files = []
Object.keys(filesList || {}).forEach(key => {
if (filesList[key].isDirectory) {
folders.push({
filename: key,
data: filesList[key]
})
} else {
files.push({
filename: key,
data: filesList[key]
})
}
})
return [...folders, ...files]
}
module.exports = DGitProvider

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

@ -22,7 +22,7 @@ const profile = {
icon: 'assets/img/fileManager.webp', icon: 'assets/img/fileManager.webp',
permission: true, permission: true,
version: packageJson.version, version: packageJson.version,
methods: ['file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'rename', 'mkdir', 'readdir', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile'], methods: ['file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh'],
kind: 'file-system' kind: 'file-system'
} }
const errorMsg = { const errorMsg = {
@ -129,6 +129,15 @@ class FileManager extends Plugin {
} }
} }
/*
* refresh the file explorer
*/
refresh () {
const provider = this.fileProviderOf('/')
// emit folderAdded so that File Explorer reloads the file tree
provider.event.emit('folderAdded', '/')
}
/** /**
* Verify if the path provided is a file * Verify if the path provided is a file
* @param {string} path path of the directory or file * @param {string} path path of the directory or file
@ -213,21 +222,60 @@ class FileManager extends Plugin {
* @param {string} dest path of the destrination file * @param {string} dest path of the destrination file
* @returns {void} * @returns {void}
*/ */
async copyFile (src, dest) { async copyFile (src, dest, customName) {
try { try {
src = this.limitPluginScope(src) src = this.limitPluginScope(src)
dest = this.limitPluginScope(dest) dest = this.limitPluginScope(dest)
await this._handleExists(src, `Cannot copy from ${src}`) await this._handleExists(src, `Cannot copy from ${src}. Path does not exist.`)
await this._handleIsFile(src, `Cannot copy from ${src}`) await this._handleIsFile(src, `Cannot copy from ${src}. Path is not a file.`)
await this._handleIsFile(dest, `Cannot paste content into ${dest}`) await this._handleExists(dest, `Cannot paste content into ${dest}. Path does not exist.`)
await this._handleIsDir(dest, `Cannot paste content into ${dest}. Path is not directory.`)
const content = await this.readFile(src) const content = await this.readFile(src)
let copiedFilePath = dest + (customName ? '/' + customName : '/' + `Copy_${helper.extractNameFromKey(src)}`)
copiedFilePath = await helper.createNonClashingNameAsync(copiedFilePath, this)
await this.writeFile(dest, content) await this.writeFile(copiedFilePath, content)
} catch (e) {
throw new Error(e)
}
}
/**
* Upsert a directory with the content of the source directory
* @param {string} src path of the source dir
* @param {string} dest path of the destination dir
* @returns {void}
*/
async copyDir (src, dest) {
try {
src = this.limitPluginScope(src)
dest = this.limitPluginScope(dest)
await this._handleExists(src, `Cannot copy from ${src}. Path does not exist.`)
await this._handleIsDir(src, `Cannot copy from ${src}. Path is not a directory.`)
await this._handleExists(dest, `Cannot paste content into ${dest}. Path does not exist.`)
await this._handleIsDir(dest, `Cannot paste content into ${dest}. Path is not directory.`)
await this.inDepthCopy(src, dest)
} catch (e) { } catch (e) {
throw new Error(e) throw new Error(e)
} }
} }
async inDepthCopy (src, dest, count = 0) {
const content = await this.readdir(src)
let copiedFolderPath = count === 0 ? dest + '/' + `Copy_${helper.extractNameFromKey(src)}` : dest + '/' + helper.extractNameFromKey(src)
copiedFolderPath = await helper.createNonClashingDirNameAsync(copiedFolderPath, this)
await this.mkdir(copiedFolderPath)
for (const [key, value] of Object.entries(content)) {
if (!value.isDirectory) {
await this.copyFile(key, copiedFolderPath, helper.extractNameFromKey(key))
} else {
await this.inDepthCopy(key, copiedFolderPath, count + 1)
}
}
}
/** /**
* Change the path of a file/directory * Change the path of a file/directory
* @param {string} oldPath current path of the file/directory * @param {string} oldPath current path of the file/directory
@ -329,18 +377,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')
@ -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,7 +67,7 @@ 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)
@ -164,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])
}) })
} }

@ -4,6 +4,7 @@ import * as packageJson from '../../../../../package.json'
var yo = require('yo-yo') var yo = require('yo-yo')
var modalDialog = require('../ui/modaldialog') var modalDialog = require('../ui/modaldialog')
var modalDialogCustom = require('../ui/modal-dialog-custom') var modalDialogCustom = require('../ui/modal-dialog-custom')
var copyToClipboard = require('../ui/copy-to-clipboard')
var csjs = require('csjs-inject') var csjs = require('csjs-inject')
@ -30,16 +31,17 @@ const profile = {
} }
export class RemixdHandle extends WebsocketPlugin { export class RemixdHandle extends WebsocketPlugin {
constructor (locahostProvider, appManager) { constructor (localhostProvider, appManager) {
super(profile) super(profile)
this.locahostProvider = locahostProvider this.localhostProvider = localhostProvider
this.appManager = appManager this.appManager = appManager
} }
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.locahostProvider.close((error) => { if (this.appManager.actives.includes('hardhat')) this.appManager.deactivatePlugin('hardhat')
this.localhostProvider.close((error) => {
if (error) console.log(error) if (error) console.log(error)
}) })
} }
@ -80,11 +82,11 @@ export class RemixdHandle extends WebsocketPlugin {
this.canceled() this.canceled()
} }
}, 3000) }, 3000)
this.locahostProvider.init(() => {}) this.localhostProvider.init(() => {})
// this.call('manager', 'activatePlugin', 'git') this.call('manager', 'activatePlugin', 'hardhat')
} }
} }
if (this.locahostProvider.isConnected()) { if (this.localhostProvider.isConnected()) {
this.deactivate() this.deactivate()
} else if (!isElectron()) { } else if (!isElectron()) {
// warn the user only if he/she is in the browser context // warn the user only if he/she is in the browser context
@ -95,7 +97,7 @@ export class RemixdHandle extends WebsocketPlugin {
label: 'Connect', label: 'Connect',
fn: () => { fn: () => {
try { try {
this.locahostProvider.preInit() this.localhostProvider.preInit()
super.activate() super.activate()
setTimeout(() => { setTimeout(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
@ -128,22 +130,27 @@ export class RemixdHandle extends WebsocketPlugin {
} }
function remixdDialog () { function remixdDialog () {
const commandText = 'remixd -s absolute-path-to-the-shared-folder --remix-ide your-remix-ide-URL-instance'
return yo` return yo`
<div class=${css.dialog}> <div class=${css.dialog}>
<div class=${css.dialogParagraph}>Interact with your file system from Remix. <br>See the <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html">Remixd tutorial</a> for more info. <div class=${css.dialogParagraph}>
Access your file system from Remix IDE. Remixd the NPM module needs to be running in the background to use the Remixd plugin. For more info please check the <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html">Remixd tutorial</a>.
</div>
<div class=${css.dialogParagraph}>If you are just looking for the remixd command here it is:
<br><br><b>remixd -s absolute-path-to-the-shared-folder --remix-ide your-remix-ide-URL-instance</b>
<span class="">${copyToClipboard(() => commandText)}</span>
</div> </div>
<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}>A connection will start a session between <em>${window.location.origin}</em> and your local file system <i>ws://127.0.0.1:65520</i>
<br><b>remixd -s absolute-path-to-the-shared-folder --remix-ide your-remix-ide-URL-instance</b> <br>To see that a connection has been made, check that there is a localhost section in the Files Explorer
</div> </div>
<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> <div class=${css.dialogParagraph}>Please make sure your system is secured enough (port 65520 should not be opened nor forwarded).
so please make sure your system is secured enough (port 65520 neither opened nor forwarded). This feature is still in Alpha, so we recommend you to keep a copy of the shared folder.
</div> </div>
<div class=${css.dialogParagraph}> <div class=${css.dialogParagraph}>
<h6 class="text-danger"> <h6 class="text-danger">
Before using, make sure you have the <b>latest remixd version</b>.<br><a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#update-to-the-latest-remixd">Read here how to update it</a> Before using, make sure you have the <b>latest remixd version</b>.<br><a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#update-to-the-latest-remixd">Read here how to update it</a>
</h6> </h6>
</div> </div>
<div class=${css.dialogParagraph}>This feature is still in Alpha, so we recommend you to keep a copy of the shared folder.</div>
</div> </div>
` `
} }

@ -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')
@ -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:
@ -35,7 +35,7 @@ const profile = {
name: 'filePanel', name: 'filePanel',
displayName: 'File explorers', displayName: 'File explorers',
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace'], methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace'],
events: ['setWorkspace', 'renameWorkspace', 'deleteWorkspace'], events: ['setWorkspace', 'renameWorkspace', 'deleteWorkspace', 'createWorkspace'],
icon: 'assets/img/fileManager.webp', icon: 'assets/img/fileManager.webp',
description: ' - ', description: ' - ',
kind: 'fileexplorer', kind: 'fileexplorer',
@ -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 = []
@ -202,7 +202,7 @@ module.exports = class Filepanel extends ViewPlugin {
return browserProvider.exists(workspacePath) return browserProvider.exists(workspacePath)
} }
async createWorkspace (workspaceName) { async createWorkspace (workspaceName, setDefaults = true) {
if (!workspaceName) throw new Error('name cannot be empty') if (!workspaceName) throw new Error('name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed') if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists') if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists')
@ -211,6 +211,7 @@ module.exports = class Filepanel extends ViewPlugin {
await this.processCreateWorkspace(workspaceName) await this.processCreateWorkspace(workspaceName)
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
if (setDefaults) {
for (const file in examples) { for (const file in examples) {
setTimeout(async () => { // space creation of files to give react ui time to update. setTimeout(async () => { // space creation of files to give react ui time to update.
try { try {
@ -222,6 +223,7 @@ module.exports = class Filepanel extends ViewPlugin {
} }
} }
} }
}
async renameWorkspace (oldName, workspaceName) { async renameWorkspace (oldName, workspaceName) {
if (!workspaceName) throw new Error('name cannot be empty') if (!workspaceName) throw new Error('name cannot be empty')

@ -1,6 +1,7 @@
/* global Node, requestAnimationFrame */ /* global Node, requestAnimationFrame */
import { Plugin } from '@remixproject/engine' import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
import * as remixBleach from '../../lib/remixBleach'
var yo = require('yo-yo') var yo = require('yo-yo')
var javascriptserialize = require('javascript-serialize') var javascriptserialize = require('javascript-serialize')
@ -26,7 +27,7 @@ var ghostbar = yo`<div class=${css.ghostbar} bg-secondary></div>`
const profile = { const profile = {
displayName: 'Terminal', displayName: 'Terminal',
name: 'terminal', name: 'terminal',
methods: [], methods: ['log'],
events: [], events: [],
description: ' - ', description: ' - ',
version: packageJson.version version: packageJson.version
@ -113,6 +114,32 @@ class Terminal extends Plugin {
this.off('scriptRunner', 'error') this.off('scriptRunner', 'error')
} }
log (message) {
var command = this.commands[message.type]
if (typeof command === 'function') {
if (typeof message.value === 'string' && message.type === 'html') {
var el = document.createElement('div')
el.innerHTML = remixBleach.sanitize(message.value, {
list: [
'a',
'b',
'p',
'em',
'strong',
'div',
'span',
'ul',
'li',
'ol',
'hr'
]
})
message.value = el
}
command(message.value)
};
}
logHtml (html) { logHtml (html) {
var command = this.commands.html var command = this.commands.html
if (typeof command === 'function') command(html) if (typeof command === 'function') command(html)
@ -653,7 +680,11 @@ class Terminal extends Plugin {
return function logger (args, scopedCommands, append) { return function logger (args, scopedCommands, append) {
var types = args.filter(filterUndefined).map(type) var types = args.filter(filterUndefined).map(type)
var values = javascriptserialize.apply(null, args.filter(filterUndefined)).map(function (val, idx) { var values = javascriptserialize.apply(null, args.filter(filterUndefined)).map(function (val, idx) {
if (typeof args[idx] === 'string') val = args[idx] if (typeof args[idx] === 'string') {
const el = document.createElement('div')
el.innerHTML = args[idx].replace(/(\r\n|\n|\r)/gm, '<br>')
val = el.children.length === 0 ? el.firstChild : el
}
if (types[idx] === 'element') val = jsbeautify.html(val) if (types[idx] === 'element') val = jsbeautify.html(val)
return val return val
}) })

@ -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,38 @@ 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
if (currentVersion) {
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).then((result) => {
this.call('terminal', 'log', { type: 'info', value: result })
}).catch((error) => {
this.call('terminal', 'log', { type: 'error', value: error })
})
}
}
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,
@ -23,7 +24,7 @@ class CompilerContainer {
timeout: 300, timeout: 300,
allversions: null, allversions: null,
selectedVersion: null, selectedVersion: null,
defaultVersion: 'soljson-v0.8.1+commit.df193b15.js' // this default version is defined: in makeMockCompiler (for browser test) defaultVersion: 'soljson-v0.8.4+commit.c7e474f2.js' // this default version is defined: in makeMockCompiler (for browser test)
} }
} }
@ -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.8.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

@ -0,0 +1,71 @@
import * as packageJson from '../../../../../package.json'
import { Plugin } from '@remixproject/engine'
import Web3 from 'web3'
const yo = require('yo-yo')
const modalDialogCustom = require('../ui/modal-dialog-custom')
const profile = {
name: 'hardhat-provider',
displayName: 'Hardhat Provider',
kind: 'provider',
description: 'Hardhat provider',
methods: ['sendAsync'],
version: packageJson.version
}
export default class HardhatProvider extends Plugin {
constructor (blockchain) {
super(profile)
this.provider = null
this.blockchain = blockchain
}
onDeactivation () {
this.provider = null
}
hardhatProviderDialogBody () {
return yo`
<div class="">
Note: To run Hardhat network node on your system, go to hardhat project folder and run command:
<div class="border p-1">npx hardhat node</div>
<br>
For more info, visit: <a href="https://hardhat.org/getting-started/#connecting-a-wallet-or-dapp-to-hardhat-network" target="_blank">Hardhat Documentation</a>
<br><br>
Hardhat JSON-RPC Endpoint
</div>
`
}
sendAsync (data) {
return new Promise((resolve, reject) => {
if (!this.provider) {
modalDialogCustom.prompt('Hardhat node request', this.hardhatProviderDialogBody(), 'http://127.0.0.1:8545', (target) => {
this.provider = new Web3.providers.HttpProvider(target)
this.sendAsyncInternal(data, resolve, reject)
}, () => {
this.sendAsyncInternal(data, resolve, reject)
})
} else {
this.sendAsyncInternal(data, resolve, reject)
}
})
}
sendAsyncInternal (data, resolve, reject) {
if (this.provider) {
this.provider[this.provider.sendAsync ? 'sendAsync' : 'send'](data, (error, message) => {
if (error) {
this.provider = null
return reject(error)
}
resolve(message)
})
} else {
const result = data.method === 'net_listening' ? 'canceled' : []
resolve({ jsonrpc: '2.0', result: result, id: data.id })
}
}
}
module.exports = HardhatProvider

@ -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 addTooltip = require('../../ui/tooltip')
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
class ContractDropdownUI { class ContractDropdownUI {
@ -48,14 +49,13 @@ class ContractDropdownUI {
} }
listenToContextChange () { listenToContextChange () {
this.blockchain.event.register('contextChanged', () => { this.blockchain.event.register('networkStatus', ({ error, network }) => {
this.blockchain.updateNetwork((err, { name } = {}) => { if (error) {
if (err) {
console.log('can\'t detect network') console.log('can\'t detect network')
return return
} }
this.exEnvironment = this.blockchain.getProvider() this.exEnvironment = this.blockchain.getProvider()
this.networkName = name this.networkName = network.name
const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`) const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`)
@ -66,7 +66,6 @@ class ContractDropdownUI {
this.setCheckedState(this.networkName === 'Main') this.setCheckedState(this.networkName === 'Main')
} }
}) })
})
} }
setCheckedState (value) { setCheckedState (value) {
@ -99,7 +98,10 @@ class ContractDropdownUI {
enableAtAddress (enable) { enableAtAddress (enable) {
if (enable) { if (enable) {
const address = this.atAddressButtonInput.value const address = this.atAddressButtonInput.value
if (!address || !ethJSUtil.isValidChecksumAddress(address)) return if (!address || !ethJSUtil.isValidAddress(address)) {
this.enableAtAddress(false)
return
}
this.atAddress.removeAttribute('disabled') this.atAddress.removeAttribute('disabled')
this.atAddress.setAttribute('title', 'Interact with the given contract.') this.atAddress.setAttribute('title', 'Interact with the given contract.')
} else { } else {
@ -306,10 +308,10 @@ class ContractDropdownUI {
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}`]) _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 { } else {
_paq.push(['trackEvent', 'udapp', `DeployOnly_${this.networkName}`]) _paq.push(['trackEvent', 'udapp', 'DeployOnly', this.networkName])
} }
} }
@ -389,10 +391,19 @@ class ContractDropdownUI {
loadFromAddress () { loadFromAddress () {
this.event.trigger('clearInstance') this.event.trigger('clearInstance')
var address = this.atAddressButtonInput.value let address = this.atAddressButtonInput.value
if (!ethJSUtil.isValidChecksumAddress(address)) {
addTooltip(yo`
<span>
It seems you are not using a checksumed address.
<br>A checksummed address is an address that contains uppercase letters, as specified in <a href="https://eips.ethereum.org/EIPS/eip-55" target="_blank">EIP-55</a>.
<br>Checksummed addresses are meant to help prevent users from sending transactions to the wrong address.
</span>`)
address = ethJSUtil.toChecksumAddress(address)
}
this.dropdownLogic.loadContractFromAddress(address, this.dropdownLogic.loadContractFromAddress(address,
(cb) => { (cb) => {
modalDialogCustom.confirm(null, 'Do you really want to interact with ' + address + ' using the current ABI definition?', cb) modalDialogCustom.confirm('At Address', `Do you really want to interact with ${address} using the current ABI definition?`, cb)
}, },
(error, loadType, abi) => { (error, loadType, abi) => {
if (error) { if (error) {

@ -55,7 +55,7 @@ class DropdownLogic {
cb(null, 'abi', abi) cb(null, 'abi', abi)
}) })
} else { } else {
_paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithInstance']) _paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithArtifacts'])
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
valueEl.value = 0
return
}
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) // - not valid (for ex 4345-54)
// - contains only '0's (for ex 0000) copy past or edit // - contains only '0's (for ex 0000) copy past or edit
if (!valueEl.value) valueEl.value = 0 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 () {
@ -84,7 +97,7 @@ class SettingsUI {
Environment Environment
</label> </label>
<div class="${css.environment}"> <div class="${css.environment}">
<select id="selectExEnvOptions" data-id="settingsSelectEnvOptions" onchange=${() => { this.updateNetwork() }} class="form-control ${css.select} custom-select"> <select id="selectExEnvOptions" data-id="settingsSelectEnvOptions" class="form-control ${css.select} custom-select">
<option id="vm-mode" <option id="vm-mode"
title="Execution environment does not connect to any node, everything is local and in memory only." title="Execution environment does not connect to any node, everything is local and in memory only."
value="vm" name="executionContext"> JavaScript VM value="vm" name="executionContext"> JavaScript VM
@ -179,8 +192,17 @@ class SettingsUI {
this.setFinalContext() this.setFinalContext()
}) })
this.blockchain.event.register('networkStatus', ({ error, network }) => {
if (error) {
this.netUI.innerHTML = 'can\'t detect network '
return
}
const networkProvider = this._components.networkModule.getNetworkProvider.bind(this._components.networkModule)
this.netUI.innerHTML = (networkProvider() !== 'vm') ? `${network.name} (${network.id || '-'}) network` : ''
})
setInterval(() => { setInterval(() => {
this.updateNetwork() this.fillAccountsList()
}, 1000) }, 1000)
this.el = el this.el = el
@ -260,7 +282,6 @@ class SettingsUI {
// set the final context. Cause it is possible that this is not the one we've originaly selected // set the final context. Cause it is possible that this is not the one we've originaly selected
this.selectExEnv.value = this.blockchain.getProvider() this.selectExEnv.value = this.blockchain.getProvider()
this.event.trigger('clearInstance', []) this.event.trigger('clearInstance', [])
this.updateNetwork()
this.updatePlusButton() this.updatePlusButton()
} }
@ -377,18 +398,6 @@ class SettingsUI {
}) })
} }
updateNetwork () {
this.blockchain.updateNetwork((err, { id, name } = {}) => {
if (err) {
this.netUI.innerHTML = 'can\'t detect network '
return
}
const network = this._components.networkModule.getNetworkProvider.bind(this._components.networkModule)
this.netUI.innerHTML = (network() !== 'vm') ? `${name} (${id || '-'}) network` : ''
})
this.fillAccountsList()
}
// TODO: unclear what's the goal of accountListCallId, feels like it can be simplified // TODO: unclear what's the goal of accountListCallId, feels like it can be simplified
async fillAccountsList () { async fillAccountsList () {
this.accountListCallId++ this.accountListCallId++

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

@ -41,7 +41,7 @@ export class RunTab extends ViewPlugin {
this.blockchain = blockchain this.blockchain = blockchain
this.fileManager = fileManager this.fileManager = fileManager
this.editor = editor this.editor = editor
this.logCallback = (msg) => { mainView.getTerminal().logHtml(msg) } this.logCallback = (msg) => { mainView.getTerminal().logHtml(yo`<pre>${msg}</pre>`) }
this.filePanel = filePanel this.filePanel = filePanel
this.compilersArtefacts = compilersArtefacts this.compilersArtefacts = compilersArtefacts
this.networkModule = networkModule this.networkModule = networkModule

@ -26,21 +26,46 @@ function confirmDialog (tx, amount, gasEstimation, self, newGasPriceCb, initialP
var el = yo` var el = yo`
<div> <div>
<div>You are creating a transaction on the main network. Click confirm if you are sure to continue.</div> <div>You are about to create a transaction on the Main Network. Confirm the details to send the info to your provider.
<div class=${css.txInfoBox}> <br>The provider for many users is MetaMask. The provider will ask you to sign the transaction before it is sent to the Main Network.</div>
<div>From: ${tx.from}</div> <div class="mt-3 ${css.txInfoBox}">
<div>To: ${tx.to ? tx.to : '(Contract Creation)'}</div> <div>
<div>Amount: ${amount} Ether</div> <span class="text-dark mr-2">From:</span>
<div>Gas estimation: ${gasEstimation}</div> <span>${tx.from}</span>
<div>Gas limit: ${tx.gas}</div> </div>
<div>Gas price: <input id='gasprice' oninput=${onGasPriceChange} /> Gwei <span> (visit <a target='_blank' href='https://ethgasstation.info'>ethgasstation.info</a> to get more info about gas price)</span></div> <div>
<div>Max transaction fee:<span id='txfee'></span></div> <span class="text-dark mr-2">To:</span>
<div>Data:</div> <span>${tx.to ? tx.to : '(Contract Creation)'}</span>
</div>
<div>
<span class="text-dark mr-2">Amount:</span>
<span>${amount} Ether</span>
</div>
<div>
<span class="text-dark mr-2">Gas estimation:</span>
<span>${gasEstimation}</span>
</div>
<div>
<span class="text-dark mr-2">Gas limit:</span>
<span>${tx.gas}</span>
</div>
<div>
<span class="text-dark mr-2">Max transaction fee:</span>
<span id='txfee'></span>
</div>
<div class="d-flex align-items-center my-1">
<span class="text-dark mr-2">Gas price:</span>
<input class="form-control mr-1" style='width: 40px; height: 28px;'id='gasprice' oninput=${onGasPriceChange} />
<span>Gwei (visit <a target='_blank' href='https://ethgasstation.info'>ethgasstation.info</a> for current gas price info.)</span>
</div>
<div class="d-flex align-items-center">
<span class="text-dark mr-2 mb-3">Data:</span>
<pre class=${css.wrapword}>${tx.data && tx.data.length > 50 ? tx.data.substring(0, 49) + '...' : tx.data} ${copyToClipboard(() => { return tx.data })}</pre> <pre class=${css.wrapword}>${tx.data && tx.data.length > 50 ? tx.data.substring(0, 49) + '...' : tx.data} ${copyToClipboard(() => { return tx.data })}</pre>
</div> </div>
<div class=${css.checkbox}> </div>
<input id='confirmsetting' type="checkbox"> <div class="d-flex py-1 align-items-center custom-control custom-checkbox ${css.checkbox}">
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i> Do not ask for confirmation again. (the setting will not be persisted for the next page reload) <input class="form-check-input custom-control-input" id='confirmsetting' type="checkbox">
<label class="m-0 form-check-label custom-control-label">Do not show this warning again.</label>
</div> </div>
</div> </div>
` `

@ -244,12 +244,16 @@ 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}` let callinfo = ''
_paq.push(['trackEvent', 'udapp', info]) if (lookupOnly) callinfo = 'call'
else if (args.funABI.type === 'fallback' || args.funABI.type === 'receive') callinfo = 'lowLevelInteracions'
else callinfo = 'transact'
_paq.push(['trackEvent', 'udapp', callinfo, this.blockchain.getCurrentNetworkStatus().network.name])
const params = args.funABI.type !== 'fallback' ? inputsValues : '' const params = args.funABI.type !== 'fallback' ? inputsValues : ''
this.blockchain.runOrCallContractMethod( this.blockchain.runOrCallContractMethod(
args.contractName, args.contractName,
args.contractAbi, args.contractABI,
args.funABI, args.funABI,
inputsValues, inputsValues,
args.address, args.address,

@ -1,33 +1,24 @@
const remixLib = require('@remix-project/remix-lib') import Web3 from 'web3'
const txFormat = remixLib.execution.txFormat import { toBuffer, addHexPrefix } from 'ethereumjs-util'
const txExecution = remixLib.execution.txExecution import { waterfall } from 'async'
const typeConversion = remixLib.execution.typeConversion import { EventEmitter } from 'events'
const Txlistener = remixLib.execution.txListener import { ExecutionContext } from './execution-context'
const TxRunner = remixLib.execution.txRunner import VMProvider from './providers/vm.js'
const txHelper = remixLib.execution.txHelper import InjectedProvider from './providers/injected.js'
const EventManager = remixLib.EventManager import NodeProvider from './providers/node.js'
const executionContext = remixLib.execution.executionContext import { execution, EventManager, helpers } from '@remix-project/remix-lib'
const Web3 = require('web3') const { txFormat, txExecution, typeConversion, txListener: Txlistener, TxRunner, TxRunnerWeb3, txHelper } = execution
const { txResultHelper: resultToRemixTx } = helpers
const async = require('async')
const { EventEmitter } = require('events')
const { resultToRemixTx } = require('./txResultHelper')
const VMProvider = require('./providers/vm.js')
const InjectedProvider = require('./providers/injected.js')
const NodeProvider = require('./providers/node.js')
class Blockchain { 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,10 +26,13 @@ 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
this.networkStatus = { name: ' - ', id: ' - ' }
this.setupEvents() this.setupEvents()
this.setupProviders() this.setupProviders()
} }
@ -55,6 +49,17 @@ class Blockchain {
this.executionContext.event.register('removeProvider', (name) => { this.executionContext.event.register('removeProvider', (name) => {
this.event.trigger('removeProvider', [name]) this.event.trigger('removeProvider', [name])
}) })
setInterval(() => {
this.detectNetwork((error, network) => {
this.networkStatus = { network, error }
this.event.trigger('networkStatus', [this.networkStatus])
})
}, 1000)
}
getCurrentNetworkStatus () {
return this.networkStatus
} }
setupProviders () { setupProviders () {
@ -123,7 +128,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)
@ -195,15 +200,6 @@ class Blockchain {
return this.executionContext.setProviderFromEndpoint(target, context, cb) return this.executionContext.setProviderFromEndpoint(target, context, cb)
} }
updateNetwork (cb) {
this.executionContext.detectNetwork((err, { id, name } = {}) => {
if (err) {
return cb(err)
}
cb(null, { id, name })
})
}
detectNetwork (cb) { detectNetwork (cb) {
return this.executionContext.detectNetwork(cb) return this.executionContext.detectNetwork(cb)
} }
@ -257,6 +253,10 @@ class Blockchain {
} }
if (funABI.type === 'fallback') data.dataHex = value if (funABI.type === 'fallback') data.dataHex = value
if (data) {
data.contractName = contractName
data.contractABI = contractAbi
}
const useCall = funABI.stateMutability === 'view' || funABI.stateMutability === 'pure' const useCall = funABI.stateMutability === 'view' || funABI.stateMutability === 'pure'
this.runTx({ to: address, data, useCall }, confirmationCb, continueCb, promptCb, (error, txResult, _address, returnValue) => { this.runTx({ to: address, data, useCall }, confirmationCb, continueCb, promptCb, (error, txResult, _address, returnValue) => {
if (error) { if (error) {
@ -309,18 +309,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 +371,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)
} }
@ -387,7 +387,7 @@ class Blockchain {
runTx (args, confirmationCb, continueCb, promptCb, cb) { runTx (args, confirmationCb, continueCb, promptCb, cb) {
const self = this const self = this
async.waterfall([ waterfall([
function getGasLimit (next) { function getGasLimit (next) {
if (self.transactionContextAPI.getGasLimit) { if (self.transactionContextAPI.getGasLimit) {
return self.transactionContextAPI.getGasLimit(next) return self.transactionContextAPI.getGasLimit(next)
@ -429,19 +429,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
@ -449,30 +454,46 @@ class Blockchain {
try { error = 'error: ' + JSON.stringify(error) } catch (e) {} try { error = 'error: ' + JSON.stringify(error) } catch (e) {}
} }
} }
next(error, result) next(error, result, tx)
} }
) )
} }
], ],
(error, txResult) => { async (error, txResult, tx) => {
if (error) { if (error) {
return cb(error) return cb(error)
} }
/*
value of txResult is inconsistent:
- transact to contract:
{"receipt": { ... }, "tx":{ ... }, "transactionHash":"0x7ba4c05075210fdbcf4e6660258379db5cc559e15703f9ac6f970a320c2dee09"}
- call to contract:
{"result":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionHash":"0x5236a76152054a8aad0c7135bcc151f03bccb773be88fbf4823184e47fc76247"}
*/
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 (execResult) {
// if it's not the VM, we don't have return value. We only have the transaction, and it does not contain the return value.
returnValue = execResult ? execResult.returnValue : toBuffer(addHexPrefix(txResult.result) || '0x0000000000000000000000000000000000000000000000000000000000000000')
const vmError = txExecution.checkVMError(execResult, args.data.contractABI)
if (vmError.error) { if (vmError.error) {
return cb(vmError.message) return cb(vmError.message)
} }
} }
}
if (!isVM && tx && tx.useCall) {
returnValue = toBuffer(addHexPrefix(txResult.result))
}
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)
@ -268,7 +145,6 @@ export class ExecutionContext {
if (context === 'web3') { if (context === 'web3') {
confirmCb(cb) confirmCb(cb)
} }
if (this.customNetWorks[context]) { if (this.customNetWorks[context]) {
var network = this.customNetWorks[context] var network = this.customNetWorks[context]
this.setProviderFromEndpoint(network.provider, network.name, (error) => { this.setProviderFromEndpoint(network.provider, network.name, (error) => {
@ -312,14 +188,16 @@ export class ExecutionContext {
const oldProvider = web3.currentProvider const oldProvider = web3.currentProvider
web3.setProvider(endpoint) web3.setProvider(endpoint)
web3.eth.net.isListening((err, isConnected) => { web3.eth.net.isListening((err, isConnected) => {
if (!err && isConnected) { if (!err && isConnected === true) {
this.executionContext = context this.executionContext = context
this._updateBlockGasLimit() this._updateBlockGasLimit()
this.event.trigger('contextChanged', [context]) this.event.trigger('contextChanged', [context])
this.event.trigger('web3EndpointChanged') this.event.trigger('web3EndpointChanged')
cb() cb()
} else if (isConnected === 'canceled') {
web3.setProvider(oldProvider)
cb()
} else { } else {
web3.setProvider(oldProvider) web3.setProvider(oldProvider)
cb('Not possible to connect to the Web3 provider. Make sure the provider is running, a connection is open (via IPC or RPC) or that the provider plugin is properly configured.') cb('Not possible to connect to the Web3 provider. Make sure the provider is running, a connection is open (via IPC or RPC) or that the provider plugin is properly configured.')
@ -340,22 +218,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
}

@ -54,7 +54,9 @@ function GistHandler (_window) {
} }
const obj = {} const obj = {}
Object.keys(data.files).forEach((element) => { Object.keys(data.files).forEach((element) => {
obj['/' + gistId + '/' + element] = data.files[element] const path = element.replace(/\.\.\./g, '/')
obj['/' + 'gist-' + gistId + '/' + path] = data.files[element]
}) })
fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => { fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => {
if (!errorLoadingFile) { if (!errorLoadingFile) {

@ -71,6 +71,20 @@ module.exports = {
return name + counter + prefix + '.' + ext return name + counter + prefix + '.' + ext
}, },
async createNonClashingDirNameAsync (name, fileManager) {
if (!name) name = 'Undefined'
let counter = ''
let exist = true
do {
const isDuplicate = await fileManager.exists(name + counter)
if (isDuplicate) counter = (counter | 0) + 1
else exist = false
} while (exist)
return name + counter
},
checkSpecialChars (name) { checkSpecialChars (name) {
return name.match(/[:*?"<>\\'|]/) != null return name.match(/[:*?"<>\\'|]/) != null
}, },
@ -106,6 +120,11 @@ module.exports = {
paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash) paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash)
if (paths.length === 1) return paths[0] if (paths.length === 1) return paths[0]
return paths.join('/') return paths.join('/')
},
extractNameFromKey (key) {
const keyPath = key.split('/')
return keyPath[keyPath.length - 1]
} }
} }

@ -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)
} }
@ -111,6 +111,12 @@ export class RemixAppManager extends PluginManager {
try { try {
const res = await fetch(this.pluginsDirectory) const res = await fetch(this.pluginsDirectory)
plugins = await res.json() plugins = await res.json()
plugins = plugins.filter((plugin) => {
if (plugin.targets && Array.isArray(plugin.targets) && plugin.targets.length > 0) {
return (plugin.targets.includes('remix'))
}
return true
})
localStorage.setItem('plugins-directory', JSON.stringify(plugins)) localStorage.setItem('plugins-directory', JSON.stringify(plugins))
} catch (e) { } catch (e) {
console.log('getting plugins list from localstorage...') console.log('getting plugins list from localstorage...')

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

@ -1,7 +1,7 @@
'use strict' 'use strict'
import { RefType } from './RefType' import { RefType } from './RefType'
import { normalizeHex } from './util' import { normalizeHex } from './util'
import { toBuffer, setLengthLeft, keccak, BN, bufferToHex } from 'ethereumjs-util' import { toBuffer, setLengthLeft, keccak, BN, bufferToHex, addHexPrefix } from 'ethereumjs-util'
export class Mapping extends RefType { export class Mapping extends RefType {
keyType keyType
@ -64,8 +64,8 @@ function getMappingLocation (key, position) {
// > the value corresponding to a mapping key k is located at keccak256(k . p) where . is concatenation. // > the value corresponding to a mapping key k is located at keccak256(k . p) where . is concatenation.
// key should be a hex string, and position an int // key should be a hex string, and position an int
const mappingK = toBuffer('0x' + key) const mappingK = toBuffer(addHexPrefix(key))
let mappingP = toBuffer(position) let mappingP = toBuffer(addHexPrefix(position))
mappingP = setLengthLeft(mappingP, 32) mappingP = setLengthLeft(mappingP, 32)
const mappingKeyBuf = concatTypedArrays(mappingK, mappingP) const mappingKeyBuf = concatTypedArrays(mappingK, mappingP)
const mappingStorageLocation: Buffer = keccak(mappingKeyBuf) const mappingStorageLocation: Buffer = keccak(mappingKeyBuf)

@ -52,27 +52,45 @@ export class RefType {
offset = parseInt(offset, 16) offset = parseInt(offset, 16)
return this.decodeFromMemoryInternal(offset, memory, cursor) return this.decodeFromMemoryInternal(offset, memory, cursor)
} else if (this.isInCallData()) { } else if (this.isInCallData()) {
return this._decodeFromCallData(variableDetails, calldata)
} else {
return { error: '<decoding failed - no decoder for ' + this.location + '>', type: this.typeName }
}
}
_decodeFromCallData (variableDetails, calldata) {
calldata = calldata.length > 0 ? calldata[0] : '0x' calldata = calldata.length > 0 ? calldata[0] : '0x'
const ethersAbi = new ethers.utils.Interface(variableDetails.abi) const ethersAbi = new ethers.utils.Interface(variableDetails.abi)
const fnSign = calldata.substr(0, 10) const fnSign = calldata.substr(0, 10)
const decodedData = ethersAbi.decodeFunctionData(ethersAbi.getFunction(fnSign), calldata) const decodedData = ethersAbi.decodeFunctionData(ethersAbi.getFunction(fnSign), calldata)
let decodedValue = decodedData[variableDetails.name] const decodedValue = decodedData[variableDetails.name]
const isArray = Array.isArray(decodedValue) const isArray = Array.isArray(decodedValue)
if (isArray) { if (isArray) {
decodedValue = decodedValue.map((el) => { return this._decodeCallDataArray(decodedValue, this)
return {
value: el.toString(),
type: this.underlyingType.typeName
}
})
} }
return { return {
length: Array.isArray(decodedValue) ? '0x' + decodedValue.length.toString(16) : undefined, length: isArray ? '0x' + decodedValue.length.toString(16) : undefined,
value: decodedValue, value: decodedValue,
type: this.typeName type: this.typeName
} }
}
_decodeCallDataArray (value, type) {
const isArray = Array.isArray(value)
if (isArray) {
value = value.map((el) => {
return this._decodeCallDataArray(el, this.underlyingType)
})
return {
length: value.length.toString(16),
value: value,
type: type.typeName
}
} else { } else {
return { error: '<decoding failed - no decoder for ' + this.location + '>', type: this.typeName } return {
value: value.toString(),
type: (type.underlyingType && type.underlyingType.typeName) || type.typeName
}
} }
} }

@ -0,0 +1,14 @@
'use strict'
module.exports = {
contract: `
pragma experimental ABIEncoderV2;
contract calldataLocal {
constructor () public {
}
function level11(uint8[1] calldata foo, string[2][1] calldata boo) public {
uint p = 45;
}
}
`}

@ -4,37 +4,40 @@ var compiler = require('solc')
var intLocal = require('./contracts/intLocal') var intLocal = require('./contracts/intLocal')
var miscLocal = require('./contracts/miscLocal') var miscLocal = require('./contracts/miscLocal')
var structArrayLocal = require('./contracts/structArrayLocal') var structArrayLocal = require('./contracts/structArrayLocal')
var calldataLocal = require('./contracts/calldata')
var vmCall = require('./vmCall') var vmCall = require('./vmCall')
var intLocalTest = require('./localsTests/int') var intLocalTest = require('./localsTests/int')
var miscLocalTest = require('./localsTests/misc') var miscLocalTest = require('./localsTests/misc')
var misc2LocalTest = require('./localsTests/misc2') var misc2LocalTest = require('./localsTests/misc2')
var structArrayLocalTest = require('./localsTests/structArray') var structArrayLocalTest = require('./localsTests/structArray')
var calldataLocalTest = require('./localsTests/calldata')
var compilerInput = require('../helpers/compilerHelper').compilerInput var compilerInput = require('../helpers/compilerHelper').compilerInput
tape('solidity', function (t) { tape('solidity', function (t) {
t.test('local decoder', async function (st) { t.test('local decoder', async function (st) {
var privateKey = Buffer.from('dae9801649ba2d95a21e688b56f77905e5667c44ce868ec83f82e838712a2c7a', 'hex') var privateKey = Buffer.from('dae9801649ba2d95a21e688b56f77905e5667c44ce868ec83f82e838712a2c7a', 'hex')
var vm = await vmCall.initVM(st, privateKey) var vm = await vmCall.initVM(st, privateKey)
test(st, vm, privateKey) await test(st, vm, privateKey)
}) })
}) })
function test (st, vm, privateKey) { async function test (st, vm, privateKey) {
var output = compiler.compile(compilerInput(intLocal.contract)) var output = compiler.compile(compilerInput(intLocal.contract))
output = JSON.parse(output) output = JSON.parse(output)
intLocalTest(st, vm, privateKey, output.contracts['test.sol']['intLocal'].evm.bytecode.object, output, function () { await intLocalTest(st, vm, privateKey, output.contracts['test.sol']['intLocal'].evm.bytecode.object, output)
output = compiler.compile(compilerInput(miscLocal.contract)) output = compiler.compile(compilerInput(miscLocal.contract))
output = JSON.parse(output) output = JSON.parse(output)
miscLocalTest(st, vm, privateKey, output.contracts['test.sol']['miscLocal'].evm.bytecode.object, output, function () { await miscLocalTest(st, vm, privateKey, output.contracts['test.sol']['miscLocal'].evm.bytecode.object, output)
output = compiler.compile(compilerInput(miscLocal.contract)) output = compiler.compile(compilerInput(miscLocal.contract))
output = JSON.parse(output) output = JSON.parse(output)
misc2LocalTest(st, vm, privateKey, output.contracts['test.sol']['miscLocal2'].evm.bytecode.object, output, function () { await misc2LocalTest(st, vm, privateKey, output.contracts['test.sol']['miscLocal2'].evm.bytecode.object, output)
output = compiler.compile(compilerInput(structArrayLocal.contract)) output = compiler.compile(compilerInput(structArrayLocal.contract))
output = JSON.parse(output) output = JSON.parse(output)
structArrayLocalTest(st, vm, privateKey, output.contracts['test.sol']['structArrayLocal'].evm.bytecode.object, output, function () { await structArrayLocalTest(st, vm, privateKey, output.contracts['test.sol']['structArrayLocal'].evm.bytecode.object, output)
output = compiler.compile(compilerInput(calldataLocal.contract))
output = JSON.parse(output)
await calldataLocalTest(st, vm, privateKey, output.contracts['test.sol']['calldataLocal'].evm.bytecode.object, output)
st.end() st.end()
})
})
})
})
} }

@ -0,0 +1,61 @@
'use strict'
import deepequal from 'deep-equal'
import { sendTx } from '../vmCall'
import { TraceManager } from '../../../src/trace/traceManager'
import { CodeManager } from '../../../src/code/codeManager'
import { SolidityProxy } from '../../../src/solidity-decoder/solidityProxy'
import { InternalCallTree } from '../../../src/solidity-decoder/internalCallTree'
import { EventManager } from '../../../src/eventManager'
import * as helper from './helper'
module.exports = async function (st, vm, privateKey, contractBytecode, compilationResult) {
let txHash
try {
let data = await sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode)
const to = (data as any).result.createdAddress.toString()
// call to level11
data = await sendTx(vm, { nonce: 1, privateKey: privateKey }, to, 0, 'a372a595000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001520000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015400000000000000000000000000000000000000000000000000000000000000')
txHash = (data as any).hash
} catch (e) {
return st.fail(e)
}
return new Promise((resolve) => {
vm.web3.eth.getTransaction(txHash, function (error, tx) {
if (error) {
return st.fail(error)
}
var traceManager = new TraceManager({ web3: vm.web3 })
var codeManager = new CodeManager(traceManager)
codeManager.clear()
var solidityProxy = new SolidityProxy({ getCurrentCalledAddressAt: traceManager.getCurrentCalledAddressAt.bind(traceManager), getCode: codeManager.getCode.bind(codeManager) })
solidityProxy.reset(compilationResult)
var debuggerEvent = new EventManager()
var callTree = new InternalCallTree(debuggerEvent, traceManager, solidityProxy, codeManager, { includeLocalVariables: true })
callTree.event.register('callTreeBuildFailed', (error) => {
st.fail(error)
})
callTree.event.register('callTreeNotReady', (reason) => {
st.fail(reason)
})
callTree.event.register('callTreeReady', (scopes, scopeStarts) => {
helper.decodeLocals(st, 140, traceManager, callTree, function (locals) {
try {
const expected = {"p":{"value":"45","type":"uint256"},"foo":{"length":"1","value":[{"value":"3","type":"uint8"}],"type":"uint8[1]"},"boo":{"length":"1","value":[{"length":"2","value":[{"value":"R","type":"string"},{"value":"T","type":"string"}],"type":"string[2]"}],"type":"string[2][1]"}}
st.deepEqual(locals, expected)
} catch (e) {
st.fail(e.message)
}
resolve({})
})
})
traceManager.resolveTrace(tx).then(() => {
debuggerEvent.trigger('newTraceLoaded', [traceManager.trace])
}).catch((error) => {
st.fail(error)
})
})
})
}

@ -9,11 +9,13 @@ import { InternalCallTree } from '../../../src/solidity-decoder/internalCallTree
import { EventManager } from '../../../src/eventManager' import { EventManager } from '../../../src/eventManager'
import * as helper from './helper' import * as helper from './helper'
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) { module.exports = function (st, vm, privateKey, contractBytecode, compilationResult) {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, txHash) { return new Promise((resolve) => {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, data) {
if (error) { if (error) {
return st.fail(error) return st.fail(error)
} }
const txHash = data.hash
vm.web3.eth.getTransaction(txHash, function (error, tx) { vm.web3.eth.getTransaction(txHash, function (error, tx) {
if (error) { if (error) {
return st.fail(error) return st.fail(error)
@ -113,7 +115,7 @@ module.exports = function (st, vm, privateKey, contractBytecode, compilationResu
} catch (e) { } catch (e) {
st.fail(e.message) st.fail(e.message)
} }
cb() resolve({})
}) })
}) })
traceManager.resolveTrace(tx).then(() => { traceManager.resolveTrace(tx).then(() => {
@ -123,5 +125,6 @@ module.exports = function (st, vm, privateKey, contractBytecode, compilationResu
}) })
}) })
}) })
})
} }

@ -8,11 +8,13 @@ import * as helper from './helper'
import { TraceManager } from '../../../src/trace/traceManager' import { TraceManager } from '../../../src/trace/traceManager'
import { CodeManager } from '../../../src/code/codeManager' import { CodeManager } from '../../../src/code/codeManager'
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) { module.exports = function (st, vm, privateKey, contractBytecode, compilationResult) {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, txHash) { return new Promise((resolve) => {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, data) {
if (error) { if (error) {
return st.fail(error) return st.fail(error)
} }
const txHash = data.hash
vm.web3.eth.getTransaction(txHash, function (error, tx) { vm.web3.eth.getTransaction(txHash, function (error, tx) {
if (error) { if (error) {
return st.fail(error) return st.fail(error)
@ -60,7 +62,7 @@ module.exports = function (st, vm, privateKey, contractBytecode, compilationResu
} catch (e) { } catch (e) {
st.fail(e.message) st.fail(e.message)
} }
cb() resolve({})
}) })
}) })
traceManager.resolveTrace(tx).then(() => { traceManager.resolveTrace(tx).then(() => {
@ -70,4 +72,5 @@ module.exports = function (st, vm, privateKey, contractBytecode, compilationResu
}) })
}) })
}) })
})
} }

@ -8,11 +8,13 @@ import * as helper from './helper'
import { TraceManager } from '../../../src/trace/traceManager' import { TraceManager } from '../../../src/trace/traceManager'
import { CodeManager } from '../../../src/code/codeManager' import { CodeManager } from '../../../src/code/codeManager'
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) { module.exports = function (st, vm, privateKey, contractBytecode, compilationResult) {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, txHash) { return new Promise((resolve) => {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, data) {
if (error) { if (error) {
return st.fail(error) return st.fail(error)
} }
const txHash = data.hash
vm.web3.eth.getTransaction(txHash, function (error, tx) { vm.web3.eth.getTransaction(txHash, function (error, tx) {
if (error) { if (error) {
return st.fail(error) return st.fail(error)
@ -46,7 +48,7 @@ module.exports = function (st, vm, privateKey, contractBytecode, compilationResu
} catch (e) { } catch (e) {
st.fail(e.message) st.fail(e.message)
} }
cb() resolve({})
}) })
}) })
traceManager.resolveTrace(tx).then(() => { traceManager.resolveTrace(tx).then(() => {
@ -56,4 +58,5 @@ module.exports = function (st, vm, privateKey, contractBytecode, compilationResu
}) })
}) })
}) })
})
} }

@ -8,11 +8,13 @@ import * as helper from './helper'
import { TraceManager } from '../../../src/trace/traceManager' import { TraceManager } from '../../../src/trace/traceManager'
import { CodeManager } from '../../../src/code/codeManager' import { CodeManager } from '../../../src/code/codeManager'
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) { module.exports = function (st, vm, privateKey, contractBytecode, compilationResult) {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, txHash) { return new Promise((resolve) => {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, data) {
if (error) { if (error) {
return st.fail(error) return st.fail(error)
} }
const txHash = data.hash
vm.web3.eth.getTransaction(txHash, function (error, tx) { vm.web3.eth.getTransaction(txHash, function (error, tx) {
if (error) { if (error) {
return st.fail(error) return st.fail(error)
@ -104,7 +106,7 @@ module.exports = function (st, vm, privateKey, contractBytecode, compilationResu
} catch (e) { } catch (e) {
st.fail(e.message) st.fail(e.message)
} }
cb() resolve({})
}) })
}) })
traceManager.resolveTrace(tx).then(() => { traceManager.resolveTrace(tx).then(() => {
@ -114,4 +116,5 @@ module.exports = function (st, vm, privateKey, contractBytecode, compilationResu
}) })
}) })
}) })
})
} }

@ -12,11 +12,12 @@ module.exports = async function testMappingStorage (st, cb) {
var vm = await initVM(st, privateKey) var vm = await initVM(st, privateKey)
var output = compile(compilerInput(mappingStorage.contract)) var output = compile(compilerInput(mappingStorage.contract))
output = JSON.parse(output) output = JSON.parse(output)
sendTx(vm, {nonce: 0, privateKey: privateKey}, null, 0, output.contracts['test.sol']['SimpleMappingState'].evm.bytecode.object, function (error, txHash) { sendTx(vm, {nonce: 0, privateKey: privateKey}, null, 0, output.contracts['test.sol']['SimpleMappingState'].evm.bytecode.object, function (error, data) {
if (error) { if (error) {
console.log(error) console.log(error)
st.end(error) st.end(error)
} else { } else {
const txHash = data.hash
vm.web3.eth.getTransaction(txHash, (error, tx) => { vm.web3.eth.getTransaction(txHash, (error, tx) => {
if (error) { if (error) {
console.log(error) console.log(error)
@ -31,12 +32,12 @@ module.exports = async function testMappingStorage (st, cb) {
function testMapping (st, vm, privateKey, contractAddress, output, cb) { function testMapping (st, vm, privateKey, contractAddress, output, cb) {
sendTx(vm, {nonce: 1, privateKey: privateKey}, contractAddress, 0, '2fd0a83a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001074686973206973206120737472696e6700000000000000000000000000000000', sendTx(vm, {nonce: 1, privateKey: privateKey}, contractAddress, 0, '2fd0a83a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001074686973206973206120737472696e6700000000000000000000000000000000',
function (error, txHash) { function (error, data) {
if (error) { if (error) {
console.log(error) console.log(error)
st.end(error) st.end(error)
} else { } else {
console.log(txHash) const txHash = data.hash
vm.web3.eth.getTransaction(txHash, (error, tx) => { vm.web3.eth.getTransaction(txHash, (error, tx) => {
if (error) { if (error) {
console.log(error) console.log(error)

@ -6,7 +6,9 @@ import { vm as remixlibVM } from '@remix-project/remix-lib'
import VM from '@ethereumjs/vm' import VM from '@ethereumjs/vm'
import Common from '@ethereumjs/common' import Common from '@ethereumjs/common'
export function sendTx (vm, from, to, value, data, cb) { export function sendTx (vm, from, to, value, data, cb?) {
cb = cb || (() => {})
return new Promise ((resolve, reject) => {
var tx = new Tx({ var tx = new Tx({
nonce: new BN(from.nonce++), nonce: new BN(from.nonce++),
gasPrice: new BN(1), gasPrice: new BN(1),
@ -27,15 +29,19 @@ export function sendTx (vm, from, to, value, data, cb) {
try { try {
vm.runTx({block: block, tx: tx, skipBalance: true, skipNonce: true}).then(function (result) { vm.runTx({block: block, tx: tx, skipBalance: true, skipNonce: true}).then(function (result) {
setTimeout(() => { setTimeout(() => {
cb(null, bufferToHex(tx.hash())) const hash = bufferToHex(tx.hash())
cb(null, { hash, result })
resolve({ hash, result })
}, 500) }, 500)
}).catch((error) => { }).catch((error) => {
console.error(error) console.error(error)
cb(error) cb(error)
reject(error)
}) })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
})
} }
async function createVm (hardfork) { async function createVm (hardfork) {

@ -1,5 +1,6 @@
'use strict' 'use strict'
import { ethers } from 'ethers' import { ethers } from 'ethers'
import { getFunctionFragment } from './txHelper'
/** /**
* deploy the given contract * deploy the given contract
@ -53,10 +54,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, abi) {
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 +75,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,20 +88,49 @@ 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
const returnDataHex = returnData.slice(0, 4).toString('hex')
let customError
if (abi) {
let decodedCustomErrorInputs
for (const item of abi) {
if (item.type === 'error') {
// ethers doesn't crash anymore if "error" type is specified, but it doesn't extract the errors. see:
// https://github.com/ethers-io/ethers.js/commit/bd05aed070ac9e1421a3e2bff2ceea150bedf9b7
// we need here to fake the type, so the "getSighash" function works properly
const fn = getFunctionFragment({ ...item, type: 'function', stateMutability: 'nonpayable' })
if (!fn) continue
const sign = fn.getSighash(item.name)
if (!sign) continue
if (returnDataHex === sign.replace('0x', '')) {
customError = item.name
decodedCustomErrorInputs = fn.decodeFunctionData(fn.getFunction(item.name), returnData)
break
}
}
}
if (decodedCustomErrorInputs) {
msg = '\tThe transaction has been reverted to the initial state.\nError provided by the contract:'
msg += `\n${customError}`
msg += '\nParameters:'
msg += `\n${decodedCustomErrorInputs}`
}
}
if (!customError) {
// 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 && (returnDataHex === '08c379a0')) {
const abiCoder = new ethers.utils.AbiCoder() const abiCoder = new ethers.utils.AbiCoder()
const reason = abiCoder.decode(['string'], returnData.slice(4))[0] const reason = abiCoder.decode(['string'], returnData.slice(4))[0]
msg = `\tThe transaction has been reverted to the initial state.\nReason provided by the contract: "${reason}".` msg = `\tThe transaction has been reverted to the initial state.\nReason provided by the contract: "${reason}".`
} else { } else {
msg = '\tThe transaction has been reverted to the initial state.\nNote: The called function should be payable if you send value and the value you send should be less than your current balance.' msg = '\tThe transaction has been reverted to the initial state.\nNote: The called function should be payable if you send value and the value you send should be less than your current balance.'
} }
}
ret.error = true ret.error = true
} else if (exceptionError === errorCode.STATIC_STATE_CHANGE) { } else if (exceptionError === errorCode.STATIC_STATE_CHANGE) {
msg = '\tState changes is not allowed in Static Call context\n' msg = '\tState changes is not allowed in Static Call context\n'
ret.error = true ret.error = true
} }
ret.message = `${error}${exceptionError}${msg}\tDebug the transaction to get more information.` ret.message = `${error}\n${exceptionError}\n${msg}\nDebug the transaction to get more information.`
return ret return ret
} }

@ -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)
}) })
@ -357,14 +357,12 @@ export function decodeResponse (response, fnabi) {
if (fnabi.outputs && fnabi.outputs.length > 0) { if (fnabi.outputs && fnabi.outputs.length > 0) {
try { try {
let i let i
const outputTypes = [] const outputTypes = []
for (i = 0; i < fnabi.outputs.length; i++) { for (i = 0; i < fnabi.outputs.length; i++) {
const type = fnabi.outputs[i].type const type = fnabi.outputs[i].type
outputTypes.push(type.indexOf('tuple') === 0 ? makeFullTypeDefinition(fnabi.outputs[i]) : type) outputTypes.push(type.indexOf('tuple') === 0 ? makeFullTypeDefinition(fnabi.outputs[i]) : type)
} }
if (!response || !response.length) response = new Uint8Array(32 * fnabi.outputs.length) // ensuring the data is at least filled by 0 cause `AbiCoder` throws if there's not engouh data
if (!response.length) response = new Uint8Array(32 * fnabi.outputs.length) // ensuring the data is at least filled by 0 cause `AbiCoder` throws if there's not engouh data
// decode data // decode data
const abiCoder = new ethers.utils.AbiCoder() const abiCoder = new ethers.utils.AbiCoder()
const decodedObj = abiCoder.decode(outputTypes, response) const decodedObj = abiCoder.decode(outputTypes, response)

@ -38,6 +38,11 @@ export function encodeFunctionId (funABI) {
return abi.getSighash(funABI.name) return abi.getSighash(funABI.name)
} }
export function getFunctionFragment (funABI): ethers.utils.Interface {
if (funABI.type === 'fallback' || funABI.type === 'receive') return null
return new ethers.utils.Interface([funABI])
}
export function sortAbiFunction (contractabi) { export function sortAbiFunction (contractabi) {
// Check if function is constant (introduced with Solidity 0.6.0) // Check if function is constant (introduced with Solidity 0.6.0)
const isConstant = ({ stateMutability }) => stateMutability === 'view' || stateMutability === 'pure' const isConstant = ({ stateMutability }) => stateMutability === 'view' || stateMutability === 'pure'

@ -1,20 +1,19 @@
'use strict' 'use strict'
import { each } from 'async' import { each } from 'async'
import { ethers } from 'ethers' import { ethers } from 'ethers'
import { toBuffer } from 'ethereumjs-util' import { toBuffer, addHexPrefix } 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(addHexPrefix(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], () => {
}) })
}) })
@ -346,7 +358,7 @@ export class TxListener {
} }
_decodeInputParams (data, abi) { _decodeInputParams (data, abi) {
data = toBuffer('0x' + data) data = toBuffer(addHexPrefix(data))
if (!data.length) data = new Uint8Array(32 * abi.inputs.length) // ensuring the data is at least filled by 0 cause `AbiCoder` throws if there's not engouh data if (!data.length) data = new Uint8Array(32 * abi.inputs.length) // ensuring the data is at least filled by 0 cause `AbiCoder` throws if there's not engouh data
const inputTypes = [] const inputTypes = []

@ -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,174 +28,9 @@ 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) {

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

@ -1,5 +1,5 @@
'use strict' 'use strict'
import { BN, bufferToHex, keccak, setLengthLeft, toBuffer } from 'ethereumjs-util' import { BN, bufferToHex, keccak, setLengthLeft, toBuffer, addHexPrefix } from 'ethereumjs-util'
/* /*
contains misc util: @TODO should be splitted contains misc util: @TODO should be splitted
@ -142,7 +142,7 @@ export function buildCallPath (index, rootCall) {
*/ */
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
export function sha3_256 (value) { export function sha3_256 (value) {
value = toBuffer(value) value = toBuffer(addHexPrefix(value))
const retInBuffer: Buffer = keccak(setLengthLeft(value, 32)) const retInBuffer: Buffer = keccak(setLengthLeft(value, 32))
return bufferToHex(retInBuffer) return bufferToHex(retInBuffer)
} }

@ -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: { },
transactionHash: TRANSACTION_HASH
}
const EXEC_RESULT = {
exceptionError: null, exceptionError: null,
gasRefund: new BN(0), gasRefund: new BN(0),
gasUsed: new BN(GAS_USED_INT), gasUsed: new BN(GAS_USED_INT),
returnValue: RETURN_VALUE_BUFFER returnValue: RETURN_VALUE_BUFFER
} }
},
transactionHash: TRANSACTION_HASH 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"]
} }

@ -13,4 +13,3 @@
], ],
"include": ["**/*.ts"] "include": ["**/*.ts"]
} }

@ -13,4 +13,3 @@
"**/*.d.ts" "**/*.d.ts"
] ]
} }

@ -9,6 +9,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
/* eslint-disable-next-line */ /* eslint-disable-next-line */
import './debugger-ui.css' import './debugger-ui.css'
const helper = require('../../../../../apps/remix-ide/src/lib/helper') const helper = require('../../../../../apps/remix-ide/src/lib/helper')
const _paq = (window as any)._paq = (window as any)._paq || []
export const DebuggerUI = (props: DebuggerUIProps) => { export const DebuggerUI = (props: DebuggerUIProps) => {
const debuggerModule = props.debuggerAPI const debuggerModule = props.debuggerAPI
@ -167,7 +168,9 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
const web3 = await debuggerModule.getDebugWeb3() const web3 = await debuggerModule.getDebugWeb3()
try { try {
if (await web3.eth.net.getId() === 42) { const networkId = await web3.eth.net.getId()
_paq.push(['trackEvent', 'debugger', 'startDebugging', networkId])
if (networkId === 42) {
setState(prevState => { setState(prevState => {
return { return {
...prevState, ...prevState,

@ -41,14 +41,16 @@ const normalize = (parent, filesList, newInputType?: string): any => {
if (filesList[key].isDirectory) { if (filesList[key].isDirectory) {
folders[extractNameFromKey(key)] = { folders[extractNameFromKey(key)] = {
path, path,
name: extractNameFromKey(path), name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
isDirectory: filesList[key].isDirectory isDirectory: filesList[key].isDirectory,
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder'
} }
} else { } else {
files[extractNameFromKey(key)] = { files[extractNameFromKey(key)] = {
path, path,
name: extractNameFromKey(path), name: extractNameFromKey(path),
isDirectory: filesList[key].isDirectory isDirectory: filesList[key].isDirectory,
type: 'file'
} }
} }
}) })
@ -59,7 +61,8 @@ const normalize = (parent, filesList, newInputType?: string): any => {
folders[path] = { folders[path] = {
path: path, path: path,
name: '', name: '',
isDirectory: true isDirectory: true,
type: 'folder'
} }
} else if (newInputType === 'file') { } else if (newInputType === 'file') {
const path = parent + '/blank' const path = parent + '/blank'
@ -67,7 +70,8 @@ const normalize = (parent, filesList, newInputType?: string): any => {
files[path] = { files[path] = {
path: path, path: path,
name: '', name: '',
isDirectory: false isDirectory: false,
type: 'file'
} }
} }
@ -181,35 +185,35 @@ export const fileRenamedSuccess = (path: string, removePath: string, files) => {
export const init = (provider, workspaceName: string, plugin, registry) => (dispatch: React.Dispatch<any>) => { export const init = (provider, workspaceName: string, plugin, registry) => (dispatch: React.Dispatch<any>) => {
if (provider) { if (provider) {
provider.event.register('fileAdded', async (filePath) => { provider.event.on('fileAdded', async (filePath) => {
if (extractParentFromKey(filePath) === '/.workspaces') return if (extractParentFromKey(filePath) === '/.workspaces') return
const path = extractParentFromKey(filePath) || provider.workspace || provider.type || '' const path = extractParentFromKey(filePath) || provider.workspace || provider.type || ''
const data = await fetchDirectoryContent(provider, path) const data = await fetchDirectoryContent(provider, path)
dispatch(fileAddedSuccess(path, data)) dispatch(fileAddedSuccess(path, data))
if (filePath.includes('_test.sol')) { if (filePath.includes('_test.sol')) {
plugin.event.trigger('newTestFileCreated', [filePath]) plugin.emit('newTestFileCreated', filePath)
} }
}) })
provider.event.register('folderAdded', async (folderPath) => { provider.event.on('folderAdded', async (folderPath) => {
if (extractParentFromKey(folderPath) === '/.workspaces') return if (extractParentFromKey(folderPath) === '/.workspaces') return
const path = extractParentFromKey(folderPath) || provider.workspace || provider.type || '' const path = extractParentFromKey(folderPath) || provider.workspace || provider.type || ''
const data = await fetchDirectoryContent(provider, path) const data = await fetchDirectoryContent(provider, path)
dispatch(folderAddedSuccess(path, data)) dispatch(folderAddedSuccess(path, data))
}) })
provider.event.register('fileRemoved', async (removePath) => { provider.event.on('fileRemoved', async (removePath) => {
const path = extractParentFromKey(removePath) || provider.workspace || provider.type || '' const path = extractParentFromKey(removePath) || provider.workspace || provider.type || ''
dispatch(fileRemovedSuccess(path, removePath)) dispatch(fileRemovedSuccess(path, removePath))
}) })
provider.event.register('fileRenamed', async (oldPath) => { provider.event.on('fileRenamed', async (oldPath) => {
const path = extractParentFromKey(oldPath) || provider.workspace || provider.type || '' const path = extractParentFromKey(oldPath) || provider.workspace || provider.type || ''
const data = await fetchDirectoryContent(provider, path) const data = await fetchDirectoryContent(provider, path)
dispatch(fileRenamedSuccess(path, oldPath, data)) dispatch(fileRenamedSuccess(path, oldPath, data))
}) })
provider.event.register('fileExternallyChanged', async (path: string, file: { content: string }) => { provider.event.on('fileExternallyChanged', async (path: string, file: { content: string }) => {
const config = registry.get('config').api const config = registry.get('config').api
const editor = registry.get('editor').api const editor = registry.get('editor').api
@ -225,15 +229,14 @@ export const init = (provider, workspaceName: string, plugin, registry) => (disp
)) ))
} }
}) })
provider.event.register('fileRenamedError', async () => { provider.event.on('fileRenamedError', async () => {
dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel')) dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel'))
}) })
provider.event.register('rootFolderChanged', async () => { provider.event.on('rootFolderChanged', async () => {
workspaceName = provider.workspace || provider.type || '' workspaceName = provider.workspace || provider.type || ''
fetchDirectory(provider, workspaceName)(dispatch) fetchDirectory(provider, workspaceName)(dispatch)
}) })
dispatch(fetchProviderSuccess(provider)) dispatch(fetchProviderSuccess(provider))
dispatch(setCurrentWorkspace(workspaceName))
} else { } else {
dispatch(fetchProviderError('No provider available')) dispatch(fetchProviderError('No provider available'))
} }

@ -4,7 +4,7 @@ import { action, FileExplorerContextMenuProps } from './types'
import './css/file-explorer-context-menu.css' import './css/file-explorer-context-menu.css'
export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => { export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => {
const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, publishToGist, runScript, emit, pageX, pageY, path, type, focus, ...otherProps } = props const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, copy, paste, runScript, emit, pageX, pageY, path, type, focus, ...otherProps } = props
const contextMenuRef = useRef(null) const contextMenuRef = useRef(null)
useEffect(() => { useEffect(() => {
contextMenuRef.current.focus() contextMenuRef.current.focus()
@ -21,43 +21,24 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
} }
}, [pageX, pageY]) }, [pageX, pageY])
const filterItem = (item: action) => { const itemMatchesCondition = (item: action) => {
/** if (item.type && Array.isArray(item.type) && (item.type.findIndex(name => name === type) !== -1)) return true
* if there are multiple elements focused we need to take this and all conditions must be met plus the action must be set to 'multi' else if (item.path && Array.isArray(item.path) && (item.path.findIndex(key => key === path) !== -1)) return true
* for example : 'downloadAsZip' with type ['file','folder','multi'] will work on files and folders when multiple are selected else if (item.extension && Array.isArray(item.extension) && (item.extension.findIndex(ext => path.endsWith(ext)) !== -1)) return true
**/ else if (item.pattern && Array.isArray(item.pattern) && (item.pattern.filter(value => path.match(new RegExp(value))).length > 0)) return true
const nonRootFocus = focus.filter((el) => { return !(el.key === '' && el.type === 'folder') })
if (nonRootFocus.length > 1) {
for (const element of nonRootFocus) {
if (!itemMatchesCondition(item, element.type, element.key)) return false
}
return (item.type.includes('multi'))
} else {
return itemMatchesCondition(item, type, path)
}
}
const itemMatchesCondition = (item: action, itemType: string, itemPath: string) => {
if (item.type && Array.isArray(item.type) && (item.type.findIndex(name => name === itemType) !== -1)) return true
else if (item.path && Array.isArray(item.path) && (item.path.findIndex(key => key === itemPath) !== -1)) return true
else if (item.extension && Array.isArray(item.extension) && (item.extension.findIndex(ext => itemPath.endsWith(ext)) !== -1)) return true
else if (item.pattern && Array.isArray(item.pattern) && (item.pattern.filter(value => itemPath.match(new RegExp(value))).length > 0)) return true
else return false else return false
} }
const getPath = () => { const getPath = () => {
const nonRootFocus = focus.filter((el) => { return !(el.key === '' && el.type === 'folder') }) if (focus.length > 1) {
if (nonRootFocus.length > 1) { return focus.map((element) => element.key)
return nonRootFocus.map((element) => { return element.key })
} else { } else {
return path return path
} }
} }
const menu = () => { const menu = () => {
return actions.filter(item => { return actions.filter(item => itemMatchesCondition(item)).map((item, index) => {
return filterItem(item)
}).map((item, index) => {
return <li return <li
id={`menuitem${item.name.toLowerCase()}`} id={`menuitem${item.name.toLowerCase()}`}
key={index} key={index}
@ -78,11 +59,26 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
deletePath(getPath()) deletePath(getPath())
break break
case 'Push changes to gist': case 'Push changes to gist':
publishToGist() pushChangesToGist(path, type)
break
case 'Publish folder to gist':
publishFolderToGist(path, type)
break
case 'Publish file to gist':
publishFileToGist(path, type)
break break
case 'Run': case 'Run':
runScript(path) runScript(path)
break break
case 'Copy':
copy(path, type)
break
case 'Paste':
paste(path, type)
break
case 'Delete All':
deletePath(getPath())
break
default: default:
emit && emit(item.id, getPath()) emit && emit(item.id, getPath())
break break

@ -6,7 +6,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import Gists from 'gists' 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, MenuItems } from './types'
import { fileSystemReducer, fileSystemInitialState } from './reducers/fileSystem' import { fileSystemReducer, fileSystemInitialState } from './reducers/fileSystem'
import { fetchDirectory, init, resolveDirectory, addInputField, removeInputField } from './actions/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'
@ -19,8 +19,10 @@ const queryParams = new QueryParams()
export const FileExplorer = (props: FileExplorerProps) => { export const FileExplorer = (props: FileExplorerProps) => {
const { 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: [{ key: '', type: 'folder' }], focusElement: [{
focusPath: null, key: '',
type: 'folder'
}],
files: [], files: [],
fileManager: null, fileManager: null,
ctrlKey: false, ctrlKey: false,
@ -28,45 +30,83 @@ export const FileExplorer = (props: FileExplorerProps) => {
actions: [{ actions: [{
id: 'newFile', id: 'newFile',
name: 'New File', name: 'New File',
type: ['folder'], type: ['folder', 'gist'],
path: [], path: [],
extension: [], extension: [],
pattern: [] pattern: [],
multiselect: false
}, { }, {
id: 'newFolder', id: 'newFolder',
name: 'New Folder', name: 'New Folder',
type: ['folder'], type: ['folder', 'gist'],
path: [], path: [],
extension: [], extension: [],
pattern: [] pattern: [],
multiselect: false
}, { }, {
id: 'rename', id: 'rename',
name: 'Rename', name: 'Rename',
type: ['file', 'folder'], type: ['file', 'folder'],
path: [], path: [],
extension: [], extension: [],
pattern: [] pattern: [],
multiselect: false
}, { }, {
id: 'delete', id: 'delete',
name: 'Delete', name: 'Delete',
type: ['file', 'folder', 'multi'], type: ['file', 'folder', 'gist'],
path: [], path: [],
extension: [], extension: [],
pattern: [] pattern: [],
multiselect: false
}, {
id: 'run',
name: 'Run',
type: [],
path: [],
extension: ['.js'],
pattern: [],
multiselect: false
}, { }, {
id: 'pushChangesToGist', id: 'pushChangesToGist',
name: 'Push changes to gist', name: 'Push changes to gist',
type: [], type: ['gist'],
path: [], path: [],
extension: [], extension: [],
pattern: ['^browser/gists/([0-9]|[a-z])*$'] pattern: [],
multiselect: false
}, { }, {
id: 'run', id: 'publishFolderToGist',
name: 'Run', name: 'Publish folder to gist',
type: [], type: ['folder'],
path: [], path: [],
extension: ['.js'], extension: [],
pattern: [] pattern: [],
multiselect: false
}, {
id: 'publishFileToGist',
name: 'Publish file to gist',
type: ['file'],
path: [],
extension: [],
pattern: [],
multiselect: false
}, {
id: 'copy',
name: 'Copy',
type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: false
}, {
id: 'deleteAll',
name: 'Delete All',
type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: true
}], }],
focusContext: { focusContext: {
element: null, element: null,
@ -85,30 +125,26 @@ export const FileExplorer = (props: FileExplorerProps) => {
hide: true, hide: true,
title: '', title: '',
message: '', message: '',
children: <></>, okLabel: '',
ok: { okFn: () => {},
label: '', cancelLabel: '',
fn: () => {} cancelFn: () => {},
},
cancel: {
label: '',
fn: () => {}
},
handleHide: null handleHide: null
}, },
modals: [], modals: [],
toasterMsg: '', toasterMsg: '',
mouseOverElement: null, mouseOverElement: null,
showContextMenu: false showContextMenu: false,
reservedKeywords: [name, 'gist-'],
copyElement: []
}) })
const [canPaste, setCanPaste] = useState(false)
const [fileSystem, dispatch] = useReducer(fileSystemReducer, fileSystemInitialState) const [fileSystem, dispatch] = useReducer(fileSystemReducer, fileSystemInitialState)
const editRef = useRef(null) const editRef = useRef(null)
useEffect(() => { useEffect(() => {
if (props.filesProvider) {
init(props.filesProvider, props.name, props.plugin, props.registry)(dispatch) init(props.filesProvider, props.name, props.plugin, props.registry)(dispatch)
} }, [])
}, [props.filesProvider, props.name])
useEffect(() => { useEffect(() => {
const provider = fileSystem.provider.provider const provider = fileSystem.provider.provider
@ -120,13 +156,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
useEffect(() => { useEffect(() => {
if (fileSystem.notification.message) { if (fileSystem.notification.message) {
modal(fileSystem.notification.title, fileSystem.notification.message, { modal(fileSystem.notification.title, fileSystem.notification.message, fileSystem.notification.labelOk, fileSystem.notification.actionOk, fileSystem.notification.labelCancel, fileSystem.notification.actionCancel)
label: fileSystem.notification.labelOk,
fn: fileSystem.notification.actionOk
}, {
label: fileSystem.notification.labelCancel,
fn: fileSystem.notification.actionCancel
})
} }
}, [fileSystem.notification.message]) }, [fileSystem.notification.message])
@ -169,12 +199,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
useEffect(() => { useEffect(() => {
if (contextMenuItems) { if (contextMenuItems) {
setState(prevState => { addMenuItems(contextMenuItems)
// filter duplicate items
const items = contextMenuItems.filter(({ name }) => prevState.actions.findIndex(action => action.name === name) === -1)
return { ...prevState, actions: [...prevState.actions, ...items] }
})
} }
}, [contextMenuItems]) }, [contextMenuItems])
@ -199,10 +224,11 @@ 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,
handleHide: prevState.modals[0].handleHide, cancelLabel: prevState.modals[0].cancelLabel,
children: prevState.modals[0].children cancelFn: prevState.modals[0].cancelFn,
handleHide: prevState.modals[0].handleHide
} }
prevState.modals.shift() prevState.modals.shift()
@ -240,6 +266,39 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
}, []) }, [])
useEffect(() => {
if (canPaste) {
addMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: false
}])
} else {
removeMenuItems(['paste'])
}
}, [canPaste])
const addMenuItems = (items: MenuItems) => {
setState(prevState => {
// filter duplicate items
const actions = items.filter(({ name }) => prevState.actions.findIndex(action => action.name === name) === -1)
return { ...prevState, actions: [...prevState.actions, ...actions] }
})
}
const removeMenuItems = (ids: string[]) => {
setState(prevState => {
const actions = prevState.actions.filter(({ id }) => ids.findIndex(value => value === id) === -1)
return { ...prevState, actions }
})
}
const extractNameFromKey = (key: string):string => { const extractNameFromKey = (key: string):string => {
const keyPath = key.split('/') const keyPath = key.split('/')
@ -254,6 +313,20 @@ export const FileExplorer = (props: FileExplorerProps) => {
return keyPath.join('/') return keyPath.join('/')
} }
const hasReservedKeyword = (content: string): boolean => {
if (state.reservedKeywords.findIndex(value => content.startsWith(value)) !== -1) return true
else return false
}
const getFocusedFolder = () => {
if (state.focusElement[0]) {
if (state.focusElement[0].type === 'folder' && state.focusElement[0].key) return state.focusElement[0].key
else if (state.focusElement[0].type === 'gist' && state.focusElement[0].key) return state.focusElement[0].key
else if (state.focusElement[0].type === 'file' && state.focusElement[0].key) return extractParentFromKey(state.focusElement[0].key) ? extractParentFromKey(state.focusElement[0].key) : name
else return name
}
}
const createNewFile = async (newFilePath: string) => { const createNewFile = async (newFilePath: string) => {
const fileManager = state.fileManager const fileManager = state.fileManager
@ -272,10 +345,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
}) })
} }
} 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)
} }
} }
@ -287,35 +357,26 @@ 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 | string[]) => { const deletePath = async (path: string | string[]) => {
const filesProvider = fileSystem.provider.provider const filesProvider = fileSystem.provider.provider
if (!Array.isArray(path)) path = [path] if (!Array.isArray(path)) path = [path]
const children: React.ReactFragment = <div><div>Are you sure you want to delete {path.length > 1 ? 'these items' : 'this item'}?</div>{path.map((item, i) => (<li key={i}>{item}</li>))}</div>
for (const p of path) { for (const p of path) {
if (filesProvider.isReadOnly(p)) { if (filesProvider.isReadOnly(p)) {
return toast('cannot delete file. ' + name + ' is a read only explorer') return toast('cannot delete file. ' + name + ' is a read only explorer')
} }
} }
modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, '', { modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', async () => {
label: 'OK',
fn: async () => {
const fileManager = state.fileManager const fileManager = state.fileManager
for (const p of path) { for (const p of path) {
try { try {
@ -325,11 +386,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`) toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`)
} }
} }
} }, 'Cancel', () => {})
}, {
label: 'Cancel',
fn: () => {}
}, children)
} }
const renamePath = async (oldPath: string, newPath: string) => { const renamePath = async (oldPath: string, newPath: string) => {
@ -338,18 +395,12 @@ 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)
} }
} }
@ -359,7 +410,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
// 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
// pick that up via the 'fileAdded' event from the files module. // pick that up via the 'fileAdded' event from the files module.
const parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key : extractParentFromKey(state.focusElement[0].key) : name const parentFolder = getFocusedFolder()
const expandPath = [...new Set([...state.expandPath, parentFolder])] const expandPath = [...new Set([...state.expandPath, parentFolder])]
setState(prevState => { setState(prevState => {
@ -372,19 +423,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
@ -401,15 +446,9 @@ export const FileExplorer = (props: FileExplorerProps) => {
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',
fn: () => {
loadFile(name) loadFile(name)
} }, 'Cancel', () => {})
}, {
label: 'Cancel',
fn: () => {}
})
} }
}).catch(error => { }).catch(error => {
if (error) console.log(error) if (error) console.log(error)
@ -417,42 +456,56 @@ export const FileExplorer = (props: FileExplorerProps) => {
}) })
} }
const publishToGist = () => { const copyFile = (src: string, dest: string) => {
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?`, { const fileManager = state.fileManager
label: 'OK',
fn: toGist try {
}, { fileManager.copyFile(src, dest)
label: 'Cancel', } catch (error) {
fn: () => {} console.log('Oops! An error ocurred while performing copyFile operation.' + error)
}) }
}
const copyFolder = (src: string, dest: string) => {
const fileManager = state.fileManager
try {
fileManager.copyDir(src, dest)
} catch (error) {
console.log('Oops! An error ocurred while performing copyDir operation.' + error)
}
}
const publishToGist = (path?: string, type?: string) => {
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(path, type), 'Cancel', () => {})
} }
const toGist = (id?: string) => { const pushChangesToGist = (path?: string, type?: string) => {
modal('Create a public gist', 'Are you sure you want to push changes to remote gist file on github.com?', 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const publishFolderToGist = (path?: string, type?: string) => {
modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${path} folder as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const publishFileToGist = (path?: string, type?: string) => {
modal('Create a public gist', `Are you sure you want to anonymously publish ${path} file as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const toGist = (path?: string, type?: string) => {
const filesProvider = fileSystem.provider.provider 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',
fn: () => {
window.open(data.html_url, '_blank') window.open(data.html_url, '_blank')
} }, 'Cancel', () => {})
}, {
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)
} }
} }
} }
@ -473,25 +526,20 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
// If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer. // If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer.
const folder = id ? '/gists/' + id : '/' const folder = path || '/'
const id = type === 'gist' ? extractNameFromKey(path).split('-')[1] : null
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='
@ -563,17 +611,18 @@ export const FileExplorer = (props: FileExplorerProps) => {
}) })
} }
const modal = (title: string, message: string, ok: { label: string, fn: () => void }, cancel: { label: string, fn: () => void }, children?:React.ReactNode) => { const modal = (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => {
setState(prevState => { setState(prevState => {
return { return {
...prevState, ...prevState,
modals: [...prevState.modals, modals: [...prevState.modals,
{ {
message, message,
children,
title, title,
ok, okLabel,
cancel, okFn,
cancelLabel,
cancelFn,
handleHide: handleHideModal handleHide: handleHideModal
}] }]
} }
@ -586,27 +635,30 @@ export const FileExplorer = (props: FileExplorerProps) => {
}) })
} }
const handleClickFile = (path: string) => { const handleClickFile = (path: string, type: string) => {
path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path
if (!state.ctrlKey) { if (!state.ctrlKey) {
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 }] }
}) })
} else { } else {
if (state.focusElement.findIndex(item => item.key === path) !== -1) { if (state.focusElement.findIndex(item => item.key === path) !== -1) {
setState(prevState => { setState(prevState => {
return { ...prevState, focusElement: [...prevState.focusElement.filter(item => item.key !== path)] } return { ...prevState, focusElement: prevState.focusElement.filter(item => item.key !== path) }
}) })
} else { } else {
setState(prevState => { setState(prevState => {
return { ...prevState, focusElement: [...prevState.focusElement, { key: path, type: 'file' }] } const nonRootFocus = prevState.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') })
nonRootFocus.push({ key: path, type })
return { ...prevState, focusElement: nonRootFocus }
}) })
} }
} }
} }
const handleClickFolder = async (path: string) => { const handleClickFolder = async (path: string, type: string) => {
if (state.ctrlKey) { if (state.ctrlKey) {
if (state.focusElement.findIndex(item => item.key === path) !== -1) { if (state.focusElement.findIndex(item => item.key === path) !== -1) {
setState(prevState => { setState(prevState => {
@ -614,7 +666,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
}) })
} else { } else {
setState(prevState => { setState(prevState => {
return { ...prevState, focusElement: [...prevState.focusElement, { key: path, type: 'folder' }] } const nonRootFocus = prevState.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') })
nonRootFocus.push({ key: path, type })
return { ...prevState, focusElement: nonRootFocus }
}) })
} }
} else { } else {
@ -628,22 +683,22 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
setState(prevState => { setState(prevState => {
return { ...prevState, focusElement: [{ key: path, type: 'folder' }], expandPath } return { ...prevState, focusElement: [{ key: path, type }], expandPath }
}) })
} }
} }
const handleContextMenuFile = (pageX: number, pageY: number, path: string, content: string) => { const handleContextMenuFile = (pageX: number, pageY: number, path: string, content: string, type: string) => {
if (!content) return if (!content) return
setState(prevState => { setState(prevState => {
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY, type: 'file' }, focusEdit: { ...prevState.focusEdit, lastEdit: content }, showContextMenu: prevState.focusEdit.element !== path } return { ...prevState, focusContext: { element: path, x: pageX, y: pageY, type }, focusEdit: { ...prevState.focusEdit, lastEdit: content }, showContextMenu: prevState.focusEdit.element !== path }
}) })
} }
const handleContextMenuFolder = (pageX: number, pageY: number, path: string, content: string) => { const handleContextMenuFolder = (pageX: number, pageY: number, path: string, content: string, type: string) => {
if (!content) return if (!content) return
setState(prevState => { setState(prevState => {
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY, type: 'folder' }, focusEdit: { ...prevState.focusEdit, lastEdit: content }, showContextMenu: prevState.focusEdit.element !== path } return { ...prevState, focusContext: { element: path, x: pageX, y: pageY, type }, focusEdit: { ...prevState.focusEdit, lastEdit: content }, showContextMenu: prevState.focusEdit.element !== path }
}) })
} }
@ -684,14 +739,20 @@ 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) {
if (hasReservedKeyword(content)) {
removeInputField(parentFolder)(dispatch)
modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {})
} else {
state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content)) state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content))
removeInputField(parentFolder)(dispatch) removeInputField(parentFolder)(dispatch)
}
} else {
if (hasReservedKeyword(content)) {
editRef.current.textContent = state.focusEdit.lastEdit
modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {})
} else { } else {
const oldPath: string = state.focusEdit.element const oldPath: string = state.focusEdit.element
const oldName = extractNameFromKey(oldPath) const oldName = extractNameFromKey(oldPath)
@ -700,6 +761,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
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: '' } }
}) })
@ -708,7 +770,7 @@ 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 ? state.focusElement[0].key : name : extractParentFromKey(state.focusElement[0].key) ? extractParentFromKey(state.focusElement[0].key) : name : name if (!parentFolder) parentFolder = getFocusedFolder()
const expandPath = [...new Set([...state.expandPath, parentFolder])] const expandPath = [...new Set([...state.expandPath, parentFolder])]
await addInputField(fileSystem.provider.provider, 'file', parentFolder)(dispatch) await addInputField(fileSystem.provider.provider, 'file', parentFolder)(dispatch)
@ -719,7 +781,7 @@ 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 ? state.focusElement[0].key : name : extractParentFromKey(state.focusElement[0].key) ? extractParentFromKey(state.focusElement[0].key) : name : name if (!parentFolder) parentFolder = getFocusedFolder()
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])]
@ -749,6 +811,32 @@ export const FileExplorer = (props: FileExplorerProps) => {
}) })
} }
const handleCopyClick = (path: string, type: string) => {
setState(prevState => {
return { ...prevState, copyElement: [{ key: path, type }] }
})
setCanPaste(true)
toast(`Copied to clipboard ${path}`)
}
const handlePasteClick = (dest: string, destType: string) => {
dest = destType === 'file' ? extractParentFromKey(dest) || props.name : dest
state.copyElement.map(({ key, type }) => {
type === 'file' ? copyFile(key, dest) : copyFolder(key, dest)
})
}
const deleteMessage = (path: string[]) => {
return (
<div>
<div>Are you sure you want to delete {path.length > 1 ? 'these items' : 'this item'}?</div>
{
path.map((item, i) => (<li key={i}>{item}</li>))
}
</div>
)
}
const label = (file: File) => { const label = (file: File) => {
return ( return (
<div <div
@ -795,12 +883,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
label={label(file)} label={label(file)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
if (state.focusEdit.element !== file.path) handleClickFolder(file.path) if (state.focusEdit.element !== file.path) handleClickFolder(file.path, file.type)
}} }}
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
handleContextMenuFolder(e.pageX, e.pageY, file.path, e.target.textContent) handleContextMenuFolder(e.pageX, e.pageY, file.path, e.target.textContent, file.type)
}} }}
labelClass={labelClass} labelClass={labelClass}
controlBehaviour={ state.ctrlKey } controlBehaviour={ state.ctrlKey }
@ -832,12 +920,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
label={label(file)} label={label(file)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
if (state.focusEdit.element !== file.path) handleClickFile(file.path) if (state.focusEdit.element !== file.path) handleClickFile(file.path, file.type)
}} }}
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
handleContextMenuFile(e.pageX, e.pageY, file.path, e.target.textContent) handleContextMenuFile(e.pageX, e.pageY, file.path, e.target.textContent, file.type)
}} }}
icon={icon} icon={icon}
labelClass={labelClass} labelClass={labelClass}
@ -906,21 +994,25 @@ export const FileExplorer = (props: FileExplorerProps) => {
message={ state.focusModal.message } message={ state.focusModal.message }
children={ state.focusModal.children } children={ state.focusModal.children }
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 }
/> />
} }
<Toaster message={state.toasterMsg} /> <Toaster message={state.toasterMsg} />
{ state.showContextMenu && { state.showContextMenu &&
<FileExplorerContextMenu <FileExplorerContextMenu
actions={state.actions} actions={state.focusElement.length > 1 ? state.actions.filter(item => item.multiselect) : state.actions.filter(item => !item.multiselect)}
hideContextMenu={hideContextMenu} hideContextMenu={hideContextMenu}
createNewFile={handleNewFileInput} createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput} createNewFolder={handleNewFolderInput}
deletePath={deletePath} deletePath={deletePath}
renamePath={editModeOn} renamePath={editModeOn}
runScript={runScript} runScript={runScript}
copy={handleCopyClick}
paste={handlePasteClick}
emit={emitContextMenuEvent} emit={emitContextMenuEvent}
pageX={state.focusContext.x} pageX={state.focusContext.x}
pageY={state.focusContext.y} pageY={state.focusContext.y}
@ -931,6 +1023,9 @@ export const FileExplorer = (props: FileExplorerProps) => {
e.stopPropagation() e.stopPropagation()
handleMouseOver(state.focusContext.element) handleMouseOver(state.focusContext.element)
}} }}
pushChangesToGist={pushChangesToGist}
publishFolderToGist={publishFolderToGist}
publishFileToGist={publishFileToGist}
/> />
} }
</div> </div>
@ -940,12 +1035,34 @@ export const FileExplorer = (props: FileExplorerProps) => {
export default FileExplorer export default FileExplorer
async function packageFiles (filesProvider, directory, callback) { async function packageFiles (filesProvider, directory, callback) {
const isFile = filesProvider.isFile(directory)
const ret = {} const ret = {}
if (isFile) {
try {
filesProvider.get(directory, (error, content) => {
if (error) throw new Error('An error ocurred while getting file content. ' + directory)
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
directory = directory.replace(/\//g, '...')
ret[directory] = { content }
callback(null, ret)
})
} catch (e) {
return callback(e)
}
} else {
try { try {
await filesProvider.copyFolderToJson(directory, ({ path, content }) => { await filesProvider.copyFolderToJson(directory, ({ path, content }) => {
if (/^\s+$/.test(content) || !content.length) { if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.' content = '// this line is added to create a gist. Empty file is not allowed.'
} }
if (path.indexOf('gist-') === 0) {
path = path.split('/')
path.shift()
path = path.join('/')
}
path = path.replace(/\//g, '...') path = path.replace(/\//g, '...')
ret[path] = { content } ret[path] = { content }
}) })
@ -954,6 +1071,7 @@ async function packageFiles (filesProvider, directory, callback) {
return callback(e) return callback(e)
} }
} }
}
function joinPath (...paths) { function joinPath (...paths) {
paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash) paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash)

@ -9,7 +9,6 @@ export const fileSystemInitialState = {
files: { files: {
files: [], files: [],
expandPath: [], expandPath: [],
workspaceName: null,
blankPath: null, blankPath: null,
isRequesting: false, isRequesting: false,
isSuccessful: false, isSuccessful: false,
@ -83,7 +82,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...state, ...state,
files: { files: {
...state.files, ...state.files,
files: resolveDirectory(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), files: resolveDirectory(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
isRequesting: false, isRequesting: false,
isSuccessful: true, isSuccessful: true,
error: null error: null
@ -135,21 +134,12 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
} }
} }
} }
case 'SET_CURRENT_WORKSPACE': {
return {
...state,
files: {
...state.files,
workspaceName: action.payload
}
}
}
case 'ADD_INPUT_FIELD': { case 'ADD_INPUT_FIELD': {
return { return {
...state, ...state,
files: { files: {
...state.files, ...state.files,
files: addInputField(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), files: addInputField(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
blankPath: action.payload.path, blankPath: action.payload.path,
isRequesting: false, isRequesting: false,
isSuccessful: true, isSuccessful: true,
@ -162,7 +152,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...state, ...state,
files: { files: {
...state.files, ...state.files,
files: removeInputField(state.files.workspaceName, state.files.blankPath, state.files.files), files: removeInputField(state.provider.provider, state.files.blankPath, state.files.files),
blankPath: null, blankPath: null,
isRequesting: false, isRequesting: false,
isSuccessful: true, isSuccessful: true,
@ -175,7 +165,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...state, ...state,
files: { files: {
...state.files, ...state.files,
files: fileAdded(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), files: fileAdded(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
expandPath: [...new Set([...state.files.expandPath, action.payload.path])], expandPath: [...new Set([...state.files.expandPath, action.payload.path])],
isRequesting: false, isRequesting: false,
isSuccessful: true, isSuccessful: true,
@ -188,7 +178,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...state, ...state,
files: { files: {
...state.files, ...state.files,
files: folderAdded(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), files: folderAdded(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
expandPath: [...new Set([...state.files.expandPath, action.payload.path])], expandPath: [...new Set([...state.files.expandPath, action.payload.path])],
isRequesting: false, isRequesting: false,
isSuccessful: true, isSuccessful: true,
@ -201,7 +191,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...state, ...state,
files: { files: {
...state.files, ...state.files,
files: fileRemoved(state.files.workspaceName, action.payload.path, action.payload.removePath, state.files.files), files: fileRemoved(state.provider.provider, action.payload.path, action.payload.removePath, state.files.files),
isRequesting: false, isRequesting: false,
isSuccessful: true, isSuccessful: true,
error: null error: null
@ -213,7 +203,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...state, ...state,
files: { files: {
...state.files, ...state.files,
files: fileRenamed(state.files.workspaceName, action.payload.path, action.payload.removePath, state.files.files, action.payload.files), files: fileRenamed(state.provider.provider, action.payload.path, action.payload.removePath, state.files.files, action.payload.files),
isRequesting: false, isRequesting: false,
isSuccessful: true, isSuccessful: true,
error: null error: null
@ -244,7 +234,9 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
} }
} }
const resolveDirectory = (root, path: string, files, content) => { const resolveDirectory = (provider, path: string, files, content) => {
const root = provider.workspace || provider.type
if (path === root) return { [root]: { ...content[root], ...files[root] } } if (path === root) return { [root]: { ...content[root], ...files[root] } }
const pathArr: string[] = path.split('/').filter(value => value) const pathArr: string[] = path.split('/').filter(value => value)
@ -258,7 +250,8 @@ const resolveDirectory = (root, path: string, files, content) => {
files = _.set(files, _path, { files = _.set(files, _path, {
isDirectory: true, isDirectory: true,
path, path,
name: extractNameFromKey(path), name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
child: { ...content[pathArr[pathArr.length - 1]], ...(prevFiles ? prevFiles.child : {}) } child: { ...content[pathArr[pathArr.length - 1]], ...(prevFiles ? prevFiles.child : {}) }
}) })
@ -278,21 +271,26 @@ const removePath = (root, path: string, pathName, files) => {
files = _.set(files, _path, { files = _.set(files, _path, {
isDirectory: true, isDirectory: true,
path, path,
name: extractNameFromKey(path), name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
child: prevFiles ? prevFiles.child : {} child: prevFiles ? prevFiles.child : {}
}) })
return files return files
} }
const addInputField = (root, path: string, files, content) => { const addInputField = (provider, path: string, files, content) => {
const root = provider.workspace || provider.type || ''
if (path === root) return { [root]: { ...content[root], ...files[root] } } if (path === root) return { [root]: { ...content[root], ...files[root] } }
const result = resolveDirectory(root, path, files, content) const result = resolveDirectory(provider, path, files, content)
return result return result
} }
const removeInputField = (root, path: string, files) => { const removeInputField = (provider, path: string, files) => {
const root = provider.workspace || provider.type || ''
if (path === root) { if (path === root) {
delete files[root][path + '/' + 'blank'] delete files[root][path + '/' + 'blank']
return files return files
@ -300,15 +298,17 @@ const removeInputField = (root, path: string, files) => {
return removePath(root, path, path + '/' + 'blank', files) return removePath(root, path, path + '/' + 'blank', files)
} }
const fileAdded = (root, path: string, files, content) => { const fileAdded = (provider, path: string, files, content) => {
return resolveDirectory(root, path, files, content) return resolveDirectory(provider, path, files, content)
} }
const folderAdded = (root, path: string, files, content) => { const folderAdded = (provider, path: string, files, content) => {
return resolveDirectory(root, path, files, content) return resolveDirectory(provider, path, files, content)
} }
const fileRemoved = (root, path: string, removedPath: string, files) => { const fileRemoved = (provider, path: string, removedPath: string, files) => {
const root = provider.workspace || provider.type || ''
if (path === root) { if (path === root) {
delete files[root][removedPath] delete files[root][removedPath]
@ -317,7 +317,9 @@ const fileRemoved = (root, path: string, removedPath: string, files) => {
return removePath(root, path, extractNameFromKey(removedPath), files) return removePath(root, path, extractNameFromKey(removedPath), files)
} }
const fileRenamed = (root, path: string, removePath: string, files, content) => { const fileRenamed = (provider, path: string, removePath: string, files, content) => {
const root = provider.workspace || provider.type || ''
if (path === root) { if (path === root) {
const allFiles = { [root]: { ...content[root], ...files[root] } } const allFiles = { [root]: { ...content[root], ...files[root] } }
@ -336,7 +338,8 @@ const fileRenamed = (root, path: string, removePath: string, files, content) =>
files = _.set(files, _path, { files = _.set(files, _path, {
isDirectory: true, isDirectory: true,
path, path,
name: extractNameFromKey(path), name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
child: { ...content[pathArr[pathArr.length - 1]], ...prevFiles.child } child: { ...content[pathArr[pathArr.length - 1]], ...prevFiles.child }
}) })

@ -6,7 +6,7 @@ export interface FileExplorerProps {
menuItems?: string[], menuItems?: string[],
plugin: any, plugin: any,
focusRoot: boolean, focusRoot: boolean,
contextMenuItems: { id: string, name: string, type: string[], path: string[], extension: string[], pattern: string[] }[], contextMenuItems: MenuItems,
displayInput?: boolean, displayInput?: boolean,
externalUploads?: EventTarget & HTMLInputElement externalUploads?: EventTarget & HTMLInputElement
} }
@ -15,6 +15,7 @@ export interface File {
path: string, path: string,
name: string, name: string,
isDirectory: boolean, isDirectory: boolean,
type: string,
child?: File[] child?: File[]
} }
@ -24,10 +25,13 @@ export interface FileExplorerMenuProps {
fileManager: any, fileManager: any,
createNewFile: (folder?: string) => void, createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void, createNewFolder: (parentFolder?: string) => void,
publishToGist: () => void, publishToGist: (path?: string) => void,
uploadFile: (target: EventTarget & HTMLInputElement) => void uploadFile: (target: EventTarget & HTMLInputElement) => void
} }
export type action = { name: string, type: string[], path: string[], extension: string[], pattern: string[], id: string }
export type action = { name: string, type: string[], path: string[], extension: string[], pattern: string[], id: string, multiselect: boolean }
export type MenuItems = action[]
export interface FileExplorerContextMenuProps { export interface FileExplorerContextMenuProps {
actions: action[], actions: action[],
createNewFile: (folder?: string) => void, createNewFile: (folder?: string) => void,
@ -35,13 +39,18 @@ export interface FileExplorerContextMenuProps {
deletePath: (path: string | string[]) => void, deletePath: (path: string | string[]) => void,
renamePath: (path: string, type: string) => void, renamePath: (path: string, type: string) => void,
hideContextMenu: () => void, hideContextMenu: () => void,
publishToGist?: () => void, publishToGist?: (path?: string, type?: string) => void,
pushChangesToGist?: (path?: string, type?: string) => void,
publishFolderToGist?: (path?: string, type?: string) => void,
publishFileToGist?: (path?: string, type?: string) => void,
runScript?: (path: string) => void, runScript?: (path: string) => void,
emit?: (id: string, path: string | string[]) => void, emit?: (id: string, path: string | string[]) => void,
pageX: number, pageX: number,
pageY: number, pageY: number,
path: string, path: string,
type: string, type: string,
focus: {key:string, type:string}[] focus: {key:string, type:string}[],
onMouseOver?: (...args) => void onMouseOver?: (...args) => void,
copy?: (path: string, type: string) => void,
paste?: (destination: string, type: string) => void
} }

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

@ -91,10 +91,8 @@ export const Toaster = (props: ToasterProps) => {
<> <>
<ModalDialog <ModalDialog
message={props.message} message={props.message}
cancel={{ cancelLabel='Close'
label: 'Close', cancelFn={() => {}}
fn: () => {}
}}
hide={!state.showModal} hide={!state.showModal}
handleHide={hideFullMessage} handleHide={hideFullMessage}
/> />

@ -67,7 +67,7 @@ export const Workspace = (props: WorkspaceProps) => {
} }
props.request.getCurrentWorkspace = () => { props.request.getCurrentWorkspace = () => {
return state.currentWorkspace return { name: state.currentWorkspace, isLocalhost: state.currentWorkspace === LOCALHOST, absolutePath: `${props.workspace.workspacesPath}/${state.currentWorkspace}` }
} }
useEffect(() => { useEffect(() => {
@ -91,25 +91,30 @@ export const Workspace = (props: WorkspaceProps) => {
const localhostDisconnect = () => { const localhostDisconnect = () => {
if (state.currentWorkspace === LOCALHOST) setWorkspace(props.workspaces.length > 0 ? props.workspaces[0] : NO_WORKSPACE) if (state.currentWorkspace === LOCALHOST) setWorkspace(props.workspaces.length > 0 ? props.workspaces[0] : NO_WORKSPACE)
// This should be removed some time after refactoring: https://github.com/ethereum/remix-project/issues/1197
else {
setWorkspace(state.currentWorkspace) // Useful to switch to last selcted workspace when remixd is disconnected
props.fileManager.setMode('browser')
}
} }
props.localhost.event.unregister('disconnected', localhostDisconnect)
props.localhost.event.register('disconnected', localhostDisconnect)
useEffect(() => { useEffect(() => {
props.localhost.event.register('connected', () => { props.localhost.event.off('disconnected', localhostDisconnect)
props.localhost.event.on('disconnected', localhostDisconnect)
props.localhost.event.on('connected', () => {
remixdExplorer.show() remixdExplorer.show()
setWorkspace(LOCALHOST) setWorkspace(LOCALHOST)
}) })
props.localhost.event.register('disconnected', () => { props.localhost.event.on('disconnected', () => {
remixdExplorer.hide() remixdExplorer.hide()
}) })
props.localhost.event.register('loading', () => { props.localhost.event.on('loading', () => {
remixdExplorer.loading() remixdExplorer.loading()
}) })
props.workspace.event.register('createWorkspace', (name) => { props.workspace.event.on('createWorkspace', (name) => {
createNewWorkspace(name) createNewWorkspace(name)
}) })
@ -145,14 +150,10 @@ export const Workspace = (props: WorkspaceProps) => {
hide: true, hide: true,
title: '', title: '',
message: null, message: null,
ok: { okLabel: '',
label: '', okFn: () => {},
fn: () => {} cancelLabel: '',
}, cancelFn: () => {},
cancel: {
label: '',
fn: () => {}
},
handleHide: null handleHide: null
}, },
loadingLocalhost: false, loadingLocalhost: false,
@ -168,41 +169,20 @@ export const Workspace = (props: WorkspaceProps) => {
/* workspace creation, renaming and deletion */ /* workspace creation, renaming and deletion */
const renameCurrentWorkspace = () => { const renameCurrentWorkspace = () => {
modal('Rename Current Workspace', renameModalMessage(), { modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '', () => {})
label: 'OK',
fn: onFinishRenameWorkspace
}, {
label: '',
fn: () => {}
})
} }
const createWorkspace = () => { const createWorkspace = () => {
modal('Create Workspace', createModalMessage(), { modal('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '', () => {})
label: 'OK',
fn: onFinishCreateWorkspace
}, {
label: '',
fn: () => {}
})
} }
const deleteCurrentWorkspace = () => { const deleteCurrentWorkspace = () => {
modal('Delete Current Workspace', 'Are you sure to delete the current workspace?', { modal('Delete Current Workspace', 'Are you sure to delete the current workspace?', 'OK', onFinishDeleteWorkspace, '', () => {})
label: 'OK',
fn: onFinishDeleteWorkspace
}, {
label: '',
fn: () => {}
})
} }
const modalMessage = (title: string, body: string) => { const modalMessage = (title: string, body: string) => {
setTimeout(() => { // wait for any previous modal a chance to close setTimeout(() => { // wait for any previous modal a chance to close
modal(title, body, { modal(title, body, 'OK', () => {}, '', null)
label: 'OK',
fn: () => {}
}, null)
}, 200) }, 200)
} }
@ -272,11 +252,19 @@ export const Workspace = (props: WorkspaceProps) => {
const remixdExplorer = { const remixdExplorer = {
hide: async () => { hide: async () => {
// If 'connect to localhost' is clicked from home tab, mode is not 'localhost'
// if (props.fileManager.mode === 'localhost') {
await setWorkspace(NO_WORKSPACE) await setWorkspace(NO_WORKSPACE)
props.fileManager.setMode('browser') props.fileManager.setMode('browser')
setState(prevState => { setState(prevState => {
return { ...prevState, hideRemixdExplorer: true, loadingLocalhost: false } return { ...prevState, hideRemixdExplorer: true, loadingLocalhost: false }
}) })
// } else {
// // Hide spinner in file explorer
// setState(prevState => {
// return { ...prevState, loadingLocalhost: false }
// })
// }
}, },
show: () => { show: () => {
props.fileManager.setMode('localhost') props.fileManager.setMode('localhost')
@ -297,7 +285,7 @@ export const Workspace = (props: WorkspaceProps) => {
}) })
} }
const modal = async (title: string, message: string | JSX.Element, ok: { label: string, fn: () => void }, cancel: { label: string, fn: () => void }) => { const modal = async (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel: string, cancelFn: () => void) => {
await setState(prevState => { await setState(prevState => {
return { return {
...prevState, ...prevState,
@ -306,8 +294,10 @@ export const Workspace = (props: WorkspaceProps) => {
hide: false, hide: false,
message, message,
title, title,
ok, okLabel,
cancel, okFn,
cancelLabel,
cancelFn,
handleHide: handleHideModal handleHide: handleHideModal
} }
} }
@ -339,8 +329,10 @@ export const Workspace = (props: WorkspaceProps) => {
title={ state.modal.title } title={ state.modal.title }
message={ state.modal.message } message={ state.modal.message }
hide={ state.modal.hide } hide={ state.modal.hide }
ok={ state.modal.ok } okLabel={ state.modal.okLabel }
cancel={ state.modal.cancel } okFn={ state.modal.okFn }
cancelLabel={ state.modal.cancelLabel }
cancelFn={ state.modal.cancelFn }
handleHide={ handleHideModal }> handleHide={ handleHideModal }>
{ (typeof state.modal.message !== 'string') && state.modal.message } { (typeof state.modal.message !== 'string') && state.modal.message }
</ModalDialog> </ModalDialog>

@ -59,7 +59,7 @@ describe('testRunner', () => {
// Test github import // Test github import
describe('test getting github imports', () => { describe('test getting github imports', () => {
const urlResolver = new RemixURLResolver() const urlResolver = new RemixURLResolver()
const fileName: string = 'github.com/MathCody/solidity-examples/greeter/greeter.sol' const fileName: string = 'github.com/ethential/solidity-examples/solidity-features-check/greeter.sol'
let results: object = {} let results: object = {}
before(done => { before(done => {
@ -78,8 +78,8 @@ describe('testRunner', () => {
}) })
it('should return contract content of given github path', () => { it('should return contract content of given github path', () => {
const expt: object = { const expt: object = {
cleanUrl: 'MathCody/solidity-examples/greeter/greeter.sol', cleanUrl: 'ethential/solidity-examples/solidity-features-check/greeter.sol',
content: 'pragma solidity >=0.5.0 <0.6.0;\nimport \"../mortal/mortal.sol\";\n\ncontract Greeter is Mortal {\n /* Define variable greeting of the type string */\n string greeting;\n\n /* This runs when the contract is executed */\n constructor(string memory _greeting) public {\n greeting = _greeting;\n }\n\n /* Main function */\n function greet() public view returns (string memory) {\n return greeting;\n }\n}', content: 'pragma solidity >=0.7.0;\nimport \"./mortal.sol\";\n// SPDX-License-Identifier: GPL-3.0\n\ncontract Greeter is Mortal {\n /* Define variable greeting of the type string */\n string greeting;\n\n /* This runs when the contract is executed */\n constructor(string memory _greeting) {\n greeting = _greeting;\n }\n\n /* Main function */\n function greet() public view returns (string memory) {\n return greeting;\n }\n}\n\n// 0x37aA58B2cE3Bb9576EEBCD51315070eA8806b7c4\n',
type: 'github' type: 'github'
} }
assert.deepEqual(results, expt) assert.deepEqual(results, expt)
@ -268,6 +268,7 @@ describe('testRunner', () => {
}) })
// Test SWARM imports // Test SWARM imports
/*
describe('test getting SWARM imports', () => { describe('test getting SWARM imports', () => {
const urlResolver = new RemixURLResolver() const urlResolver = new RemixURLResolver()
const fileName = 'bzz-raw://a728627437140f2b0b46c1bcfb0de2126d18b40e9b61c3e31bd96abebf714619' const fileName = 'bzz-raw://a728627437140f2b0b46c1bcfb0de2126d18b40e9b61c3e31bd96abebf714619'
@ -297,6 +298,7 @@ describe('testRunner', () => {
assert.deepEqual(results, expt) assert.deepEqual(results, expt)
}) })
}) })
*/
}) })
}) })
}) })

@ -4,7 +4,7 @@ import * as semver from 'semver'
import WebSocket from '../websocket' import WebSocket from '../websocket'
import * as servicesList from '../serviceList' import * as servicesList from '../serviceList'
import * as WS from 'ws' // eslint-disable-line import * as WS from 'ws' // eslint-disable-line
import { getDomain } from '../utils' import { getDomain, absolutePath } from '../utils'
import Axios from 'axios' import Axios from 'axios'
import * as fs from 'fs-extra' import * as fs from 'fs-extra'
import * as path from 'path' import * as path from 'path'
@ -24,16 +24,19 @@ async function warnLatestVersion () {
const services = { const services = {
git: (readOnly: boolean) => new servicesList.GitClient(readOnly), git: (readOnly: boolean) => new servicesList.GitClient(readOnly),
hardhat: (readOnly: boolean) => new servicesList.HardhatClient(readOnly),
folder: (readOnly: boolean) => new servicesList.Sharedfolder(readOnly) folder: (readOnly: boolean) => new servicesList.Sharedfolder(readOnly)
} }
// Similar object is also defined in websocket.ts
const ports = { const ports = {
git: 65521, git: 65521,
hardhat: 65522,
folder: 65520 folder: 65520
} }
const killCallBack: Array<Function> = [] const killCallBack: Array<Function> = []
function startService<S extends 'git' | 'folder'> (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => void) { function startService<S extends 'git' | 'hardhat' | 'folder'> (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => void) {
const socket = new WebSocket(ports[service], { remixIdeUrl: program.remixIde }, () => services[service](program.readOnly || false)) const socket = new WebSocket(ports[service], { remixIdeUrl: program.remixIde }, () => services[service](program.readOnly || false))
socket.start(callback) socket.start(callback)
killCallBack.push(socket.close.bind(socket)) killCallBack.push(socket.close.bind(socket))
@ -78,6 +81,15 @@ function startService<S extends 'git' | 'folder'> (service: S, callback: (ws: WS
sharedFolderClient.setupNotifications(program.sharedFolder) sharedFolderClient.setupNotifications(program.sharedFolder)
sharedFolderClient.sharedFolder(program.sharedFolder) sharedFolderClient.sharedFolder(program.sharedFolder)
}) })
// Run hardhat service if a hardhat project is shared as folder
const hardhatConfigFilePath = absolutePath('./', program.sharedFolder) + '/hardhat.config.js'
const isHardhatProject = fs.existsSync(hardhatConfigFilePath)
if (isHardhatProject) {
startService('hardhat', (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => {
sharedFolderClient.setWebSocket(ws)
sharedFolderClient.sharedFolder(program.sharedFolder)
})
}
/* /*
startService('git', (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => { startService('git', (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => {
sharedFolderClient.setWebSocket(ws) sharedFolderClient.setWebSocket(ws)

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

Loading…
Cancel
Save