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. 9
      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. 38
      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. 53
      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. 53
      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. 59
      apps/remix-ide/src/app/ui/confirmDialog.js
  36. 10
      apps/remix-ide/src/app/ui/universal-dapp-ui.js
  37. 137
      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. 54
      libs/remix-debug/src/solidity-decoder/types/RefType.ts
  47. 14
      libs/remix-debug/test/decoder/contracts/calldata.ts
  48. 37
      libs/remix-debug/test/decoder/localDecoder.ts
  49. 61
      libs/remix-debug/test/decoder/localsTests/calldata.ts
  50. 207
      libs/remix-debug/test/decoder/localsTests/int.ts
  51. 113
      libs/remix-debug/test/decoder/localsTests/misc.ts
  52. 85
      libs/remix-debug/test/decoder/localsTests/misc2.ts
  53. 201
      libs/remix-debug/test/decoder/localsTests/structArray.ts
  54. 7
      libs/remix-debug/test/decoder/stateTests/mapping.ts
  55. 64
      libs/remix-debug/test/decoder/vmCall.ts
  56. 56
      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. 37
      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. 27
      libs/remix-tests/tsconfig.lib.json
  88. 29
      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. 484
      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
- run: npm install
- run: npx nx build remix-ide --with-deps
- run: npm run downloadsolc_assets
- run:
name: Deploy
command: |
@ -291,6 +292,7 @@ jobs:
- checkout
- run: npm install
- run: npx nx build remix-ide --with-deps
- run: npm run downloadsolc_assets
- run:
name: Deploy
command: |
@ -319,6 +321,7 @@ jobs:
- run: npm install
- run: npm run build:libs
- run: npm run build
- run: npm run downloadsolc_assets
- run:
name: Deploy
command: |

@ -21,7 +21,8 @@ function getLastTransactionHash (browser: NightwatchBrowser, callback: (hash: st
for (let i = deployedContracts.length - 1; i >= 0; i--) {
const current = deployedContracts[i]
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', '')
}
}

@ -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) {
browser.pause(500)
.click('*[data-id="txLoggerDebugButton0x41fab8ea5b1d9fba5e0a6545ca1a2d62fff518578802c033c2b9a031a01c31b3"]')
@ -51,7 +64,8 @@ module.exports = {
browser.clickLaunchIcon('udapp')
.click('*[data-id="universalDappUiUdappClose"]')
.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')
.addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3A', true, true)
.pause(500)
@ -79,6 +93,19 @@ module.exports = {
.clickFunction('delegate - transact (not payable)', { types: 'address to', values: '0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c' })
.journalLastChildIncludes('Ballot.delegate(address)')
.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()
}
}

@ -60,7 +60,8 @@ module.exports = {
browser.clickLaunchIcon('udapp')
.click('*[data-id="universalDappUiUdappClose"]')
.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')
.addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3A', true, true)
.pause(500)

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

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

@ -69,6 +69,9 @@ module.exports = {
.testFunction('last', {
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()
},

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

@ -62,7 +62,7 @@ module.exports = {
'Call web3.eth.getAccounts() using JavaScript VM': function (browser: NightwatchBrowser) {
browser
.executeScript('web3.eth.getAccounts()')
.waitForElementContainsText('*[data-id="terminalJournal"]', '[ "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB", "0x617F2E2fD72FD9D5503197092aC168c91465E7f2", "0x17F6AD8Ef982297579C203069C1DbfFE4348c372", "0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678", "0x03C6FcED478cBbC9a4FAB34eF9f40767739D1Ff7", "0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C", "0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC", "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c", "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C", "0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB", "0x583031D1113aD414F02576BD6afaBfb302140225", "0xdD870fA1b7C4700F2BD7f44238821C26f7392148" ]', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', '"0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c", "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C", "0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB", "0x583031D1113aD414F02576BD6afaBfb302140225", "0xdD870fA1b7C4700F2BD7f44238821C26f7392148"', 80000)
},
'Call web3.eth.getAccounts() using Web3 Provider': function (browser: NightwatchBrowser) {

@ -137,6 +137,21 @@ module.exports = {
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite
.click('#runTabView button[class^="instanceButton"]')
.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()
}
}
@ -218,5 +233,23 @@ contract C {
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 compiler = require('solc')
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')
compiler.loadRemoteVersion(defaultVersion, (error, solcSnapshot) => {

@ -29,11 +29,13 @@ const { OffsetToLineColumnConverter } = require('./lib/offsetToLineColumnConvert
const QueryParams = require('./lib/query-params')
const Storage = remixLib.Storage
const RemixDProvider = require('./app/files/remixDProvider')
const HardhatProvider = require('./app/tabs/hardhat-provider')
const Config = require('./config')
const modalDialogCustom = require('./app/ui/modal-dialog-custom')
const modalDialog = require('./app/ui/modaldialog')
const FileManager = require('./app/files/fileManager')
const FileProvider = require('./app/files/fileProvider')
const DGitProvider = require('./app/files/dgitProvider')
const WorkspaceFileProvider = require('./app/files/workspaceFileProvider')
const toolTip = require('./app/ui/tooltip')
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 ----------------------------
const fileManager = new FileManager(editor, appManager)
registry.put({ api: fileManager, name: 'filemanager' })
// ----------------- dGit provider ---------------------------------
const dGitProvider = new DGitProvider()
// ----------------- import content service ------------------------
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)
// ----------------- represent the current selected web3 provider ----
const web3Provider = new Web3ProviderModule(blockchain)
const hardhatProvider = new HardhatProvider(blockchain)
// ----------------- convert offset to line/column service -----------
const offsetToLineColumnConverter = new 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,
terminal,
web3Provider,
fetchAndCompile
fetchAndCompile,
dGitProvider,
hardhatProvider
])
// LAYOUT & SYSTEM VIEWS
@ -431,12 +438,14 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
engine.register([
compileTab,
compileTab.compileTabLogic,
run,
debug,
analysis,
test,
filePanel.remixdHandle,
filePanel.gitHandle
filePanel.gitHandle,
filePanel.hardhatHandle
])
if (isElectron()) {

@ -114,6 +114,8 @@ class PluginManagerComponent extends ViewPlugin {
renderItem (profile) {
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
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}" >
<div class="${css.row} justify-content-between align-items-center mb-2">
<h6 class="${css.displayName} plugin-name">
${displayName}
${versionWarning}
<div>
${displayName}
${doclink}
${versionWarning}
</div>
${activationButton}
</h6>
</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 Treeview = require('../ui/TreeView')
const modalDialog = require('../ui/modaldialog')
const EventManager = require('../../lib/events')
const EventManager = require('events')
const contextMenu = require('../ui/contextMenu')
const css = require('./styles/file-explorer-styles')
const globalRegistry = require('../../global/registry')
@ -94,11 +94,11 @@ function fileExplorer (localRegistry, files, menuItems, plugin) {
})
// register to event of the file provider
files.event.register('fileRemoved', fileRemoved)
files.event.register('fileRenamed', fileRenamed)
files.event.register('fileRenamedError', fileRenamedError)
files.event.register('fileAdded', fileAdded)
files.event.register('folderAdded', folderAdded)
files.event.on('fileRemoved', fileRemoved)
files.event.on('fileRenamed', fileRenamed)
files.event.on('fileRenamedError', fileRenamedError)
files.event.on('fileAdded', fileAdded)
files.event.on('folderAdded', folderAdded)
function fileRenamedError (error) {
modalDialogCustom.alert(error)

@ -22,7 +22,7 @@ const profile = {
icon: 'assets/img/fileManager.webp',
permission: true,
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'
}
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
* @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
* @returns {void}
*/
async copyFile (src, dest) {
async copyFile (src, dest, customName) {
try {
src = this.limitPluginScope(src)
dest = this.limitPluginScope(dest)
await this._handleExists(src, `Cannot copy from ${src}`)
await this._handleIsFile(src, `Cannot copy from ${src}`)
await this._handleIsFile(dest, `Cannot paste content into ${dest}`)
await this._handleExists(src, `Cannot copy from ${src}. Path does not exist.`)
await this._handleIsFile(src, `Cannot copy from ${src}. Path is not a file.`)
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)
let copiedFilePath = dest + (customName ? '/' + customName : '/' + `Copy_${helper.extractNameFromKey(src)}`)
copiedFilePath = await helper.createNonClashingNameAsync(copiedFilePath, this)
await this.writeFile(copiedFilePath, content)
} catch (e) {
throw new Error(e)
}
}
await this.writeFile(dest, content)
/**
* 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) {
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
* @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,
filesProviders: this._components.registry.get('fileproviders').api
}
this._deps.browserExplorer.event.register('fileChanged', (path) => { this.fileChangedEvent(path) })
this._deps.browserExplorer.event.register('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) })
this._deps.localhostExplorer.event.register('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) })
this._deps.browserExplorer.event.register('fileRemoved', (path) => { this.fileRemovedEvent(path) })
this._deps.browserExplorer.event.register('fileAdded', (path) => { this.fileAddedEvent(path) })
this._deps.localhostExplorer.event.register('fileRemoved', (path) => { this.fileRemovedEvent(path) })
this._deps.localhostExplorer.event.register('errored', (event) => { this.removeTabsOf(this._deps.localhostExplorer) })
this._deps.localhostExplorer.event.register('closed', (event) => { this.removeTabsOf(this._deps.localhostExplorer) })
this._deps.workspaceExplorer.event.register('fileChanged', (path) => { this.fileChangedEvent(path) })
this._deps.workspaceExplorer.event.register('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) })
this._deps.workspaceExplorer.event.register('fileRemoved', (path) => { this.fileRemovedEvent(path) })
this._deps.workspaceExplorer.event.register('fileAdded', (path) => { this.fileAddedEvent(path) })
this._deps.browserExplorer.event.on('fileChanged', (path) => { this.fileChangedEvent(path) })
this._deps.browserExplorer.event.on('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) })
this._deps.localhostExplorer.event.on('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) })
this._deps.browserExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) })
this._deps.browserExplorer.event.on('fileAdded', (path) => { this.fileAddedEvent(path) })
this._deps.localhostExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) })
this._deps.localhostExplorer.event.on('errored', (event) => { this.removeTabsOf(this._deps.localhostExplorer) })
this._deps.localhostExplorer.event.on('closed', (event) => { this.removeTabsOf(this._deps.localhostExplorer) })
this._deps.workspaceExplorer.event.on('fileChanged', (path) => { this.fileChangedEvent(path) })
this._deps.workspaceExplorer.event.on('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) })
this._deps.workspaceExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) })
this._deps.workspaceExplorer.event.on('fileAdded', (path) => { this.fileAddedEvent(path) })
this.getCurrentFile = this.file
this.getFile = this.readFile

@ -1,7 +1,7 @@
'use strict'
const CompilerImport = require('../compiler/compiler-imports')
const EventManager = require('../../lib/events')
const EventManager = require('events')
const modalDialogCustom = require('../ui/modal-dialog-custom')
const tooltip = require('../ui/tooltip')
const remixLib = require('@remix-project/remix-lib')
@ -111,9 +111,9 @@ class FileProvider {
return false
}
if (!exists) {
this.event.trigger('fileAdded', [this._normalizePath(unprefixedpath), false])
this.event.emit('fileAdded', this._normalizePath(unprefixedpath), false)
} else {
this.event.trigger('fileChanged', [this._normalizePath(unprefixedpath)])
this.event.emit('fileChanged', this._normalizePath(unprefixedpath))
}
cb()
return true
@ -128,7 +128,7 @@ class FileProvider {
currentCheck = currentCheck + '/' + value
if (!window.remixFileSystem.existsSync(currentCheck)) {
window.remixFileSystem.mkdirSync(currentCheck)
this.event.trigger('folderAdded', [this._normalizePath(currentCheck)])
this.event.emit('folderAdded', this._normalizePath(currentCheck))
}
})
if (cb) cb()
@ -184,7 +184,7 @@ class FileProvider {
// folder is empty
window.remixFileSystem.rmdirSync(path, console.log)
}
this.event.trigger('fileRemoved', [this._normalizePath(path)])
this.event.emit('fileRemoved', this._normalizePath(path))
}
} catch (e) {
console.log(e)
@ -249,7 +249,7 @@ class FileProvider {
path = this.removePrefix(path)
if (window.remixFileSystem.existsSync(path) && !window.remixFileSystem.statSync(path).isDirectory()) {
window.remixFileSystem.unlinkSync(path, console.log)
this.event.trigger('fileRemoved', [this._normalizePath(path)])
this.event.emit('fileRemoved', this._normalizePath(path))
return true
} else return false
}
@ -259,11 +259,11 @@ class FileProvider {
var unprefixednewPath = this.removePrefix(newPath)
if (this._exists(unprefixedoldPath)) {
window.remixFileSystem.renameSync(unprefixedoldPath, unprefixednewPath)
this.event.trigger('fileRenamed', [
this.event.emit('fileRenamed',
this._normalizePath(unprefixedoldPath),
this._normalizePath(unprefixednewPath),
isFolder
])
)
return true
}
return false

@ -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']
remixdEvents.forEach((value) => {
this._appManager.on('remixd', value, (event) => {
this.event.trigger(value, [event])
this.event.emit(value, event)
})
})
this._appManager.on('remixd', 'folderAdded', (path) => {
this.event.trigger('folderAdded', [path])
this.event.emit('folderAdded', path)
})
this._appManager.on('remixd', 'fileAdded', (path) => {
this.event.trigger('fileAdded', [path])
this.event.emit('fileAdded', path)
})
this._appManager.on('remixd', 'fileChanged', (path) => {
this.event.trigger('fileChanged', [path])
this.event.emit('fileChanged', path)
})
this._appManager.on('remixd', 'fileRemoved', (path) => {
this.event.trigger('fileRemoved', [path])
this.event.emit('fileRemoved', path)
})
this._appManager.on('remixd', 'fileRenamed', (oldPath, newPath) => {
this.event.trigger('fileRemoved', [oldPath, newPath])
this.event.emit('fileRemoved', oldPath, newPath)
})
this._appManager.on('remixd', 'rootFolderChanged', () => {
this.event.trigger('rootFolderChanged', [])
this.event.emit('rootFolderChanged')
})
}
@ -53,11 +53,11 @@ module.exports = class RemixDProvider extends FileProvider {
close (cb) {
this._isReady = false
cb()
this.event.trigger('disconnected')
this.event.emit('disconnected')
}
preInit () {
this.event.trigger('loading')
this.event.emit('loading')
}
init (cb) {
@ -67,7 +67,7 @@ module.exports = class RemixDProvider extends FileProvider {
this._isReady = true
this._readOnlyMode = result
this._registerEvent()
this.event.trigger('connected')
this.event.emit('connected')
cb && cb()
}).catch((error) => {
cb && cb(error)
@ -164,13 +164,13 @@ module.exports = class RemixDProvider extends FileProvider {
this.filesContent[newPath] = this.filesContent[oldPath]
delete this.filesContent[oldPath]
this.init(() => {
this.event.trigger('fileRenamed', [oldPath, newPath, isFolder])
this.event.emit('fileRenamed', oldPath, newPath, isFolder)
})
return result
}).catch(error => {
console.log(error)
if (this.error[error.code]) error = this.error[error.code]
this.event.trigger('fileRenamedError', [this.error[error.code]])
this.event.emit('fileRenamedError', this.error[error.code])
})
}

@ -4,6 +4,7 @@ import * as packageJson from '../../../../../package.json'
var yo = require('yo-yo')
var modalDialog = require('../ui/modaldialog')
var modalDialogCustom = require('../ui/modal-dialog-custom')
var copyToClipboard = require('../ui/copy-to-clipboard')
var csjs = require('csjs-inject')
@ -30,16 +31,17 @@ const profile = {
}
export class RemixdHandle extends WebsocketPlugin {
constructor (locahostProvider, appManager) {
constructor (localhostProvider, appManager) {
super(profile)
this.locahostProvider = locahostProvider
this.localhostProvider = localhostProvider
this.appManager = appManager
}
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.locahostProvider.close((error) => {
if (this.appManager.actives.includes('hardhat')) this.appManager.deactivatePlugin('hardhat')
this.localhostProvider.close((error) => {
if (error) console.log(error)
})
}
@ -80,11 +82,11 @@ export class RemixdHandle extends WebsocketPlugin {
this.canceled()
}
}, 3000)
this.locahostProvider.init(() => {})
// this.call('manager', 'activatePlugin', 'git')
this.localhostProvider.init(() => {})
this.call('manager', 'activatePlugin', 'hardhat')
}
}
if (this.locahostProvider.isConnected()) {
if (this.localhostProvider.isConnected()) {
this.deactivate()
} else if (!isElectron()) {
// warn the user only if he/she is in the browser context
@ -95,7 +97,7 @@ export class RemixdHandle extends WebsocketPlugin {
label: 'Connect',
fn: () => {
try {
this.locahostProvider.preInit()
this.localhostProvider.preInit()
super.activate()
setTimeout(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
@ -128,22 +130,27 @@ export class RemixdHandle extends WebsocketPlugin {
}
function remixdDialog () {
const commandText = 'remixd -s absolute-path-to-the-shared-folder --remix-ide your-remix-ide-URL-instance'
return yo`
<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 class=${css.dialogParagraph}>If you have looked at the Remixd docs and just need remixd command, <br> here it is:
<br><b>remixd -s absolute-path-to-the-shared-folder --remix-ide your-remix-ide-URL-instance</b>
<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>To see that a connection has been made, check that there is a localhost section in the Files Explorer
</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>
so please make sure your system is secured enough (port 65520 neither opened nor forwarded).
<div class=${css.dialogParagraph}>Please make sure your system is secured enough (port 65520 should not be opened nor forwarded).
This feature is still in Alpha, so we recommend you to keep a copy of the shared folder.
</div>
<div class=${css.dialogParagraph}>
<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>
</h6>
</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>
`
}

@ -1,6 +1,6 @@
'use strict'
const EventManager = require('../../lib/events')
const EventManager = require('events')
const FileProvider = require('./fileProvider')
const pathModule = require('path')
@ -82,7 +82,7 @@ class WorkspaceFileProvider extends FileProvider {
createWorkspace (name) {
if (!name) name = 'default_workspace'
this.event.trigger('createWorkspace', [name])
this.event.emit('createWorkspace', name)
}
}

@ -6,13 +6,13 @@ import ReactDOM from 'react-dom'
import { Workspace } from '@remix-ui/workspace' // eslint-disable-line
import { bufferToHex, keccakFromString } from 'ethereumjs-util'
import { checkSpecialChars, checkSlash } from '../../lib/helper'
var EventManager = require('../../lib/events')
var { RemixdHandle } = require('../files/remixd-handle.js')
var { GitHandle } = require('../files/git-handle.js')
var globalRegistry = require('../../global/registry')
var examples = require('../editor/examples')
var GistHandler = require('../../lib/gist-handler')
var QueryParams = require('../../lib/query-params')
const { RemixdHandle } = require('../files/remixd-handle.js')
const { GitHandle } = require('../files/git-handle.js')
const { HardhatHandle } = require('../files/hardhat-handle.js')
const globalRegistry = require('../../global/registry')
const examples = require('../editor/examples')
const GistHandler = require('../../lib/gist-handler')
const QueryParams = require('../../lib/query-params')
const modalDialogCustom = require('../ui/modal-dialog-custom')
/*
Overview of APIs:
@ -35,7 +35,7 @@ const profile = {
name: 'filePanel',
displayName: 'File explorers',
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace'],
events: ['setWorkspace', 'renameWorkspace', 'deleteWorkspace'],
events: ['setWorkspace', 'renameWorkspace', 'deleteWorkspace', 'createWorkspace'],
icon: 'assets/img/fileManager.webp',
description: ' - ',
kind: 'fileexplorer',
@ -47,7 +47,6 @@ const profile = {
module.exports = class Filepanel extends ViewPlugin {
constructor (appManager) {
super(profile)
this.event = new EventManager()
this._components = {}
this._components.registry = globalRegistry
this._deps = {
@ -60,6 +59,7 @@ module.exports = class Filepanel extends ViewPlugin {
this.remixdHandle = new RemixdHandle(this._deps.fileProviders.localhost, appManager)
this.gitHandle = new GitHandle()
this.hardhatHandle = new HardhatHandle()
this.registeredMenuItems = []
this.request = {}
this.workspaces = []
@ -202,7 +202,7 @@ module.exports = class Filepanel extends ViewPlugin {
return browserProvider.exists(workspacePath)
}
async createWorkspace (workspaceName) {
async createWorkspace (workspaceName, setDefaults = true) {
if (!workspaceName) throw new Error('name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists')
@ -211,14 +211,16 @@ module.exports = class Filepanel extends ViewPlugin {
await this.processCreateWorkspace(workspaceName)
workspaceProvider.setWorkspace(workspaceName)
await this.request.setWorkspace(workspaceName) // tells the react component to switch to that workspace
for (const file in examples) {
setTimeout(async () => { // space creation of files to give react ui time to update.
try {
await workspaceProvider.set(examples[file].name, examples[file].content)
} catch (error) {
console.error(error)
}
}, 10)
if (setDefaults) {
for (const file in examples) {
setTimeout(async () => { // space creation of files to give react ui time to update.
try {
await workspaceProvider.set(examples[file].name, examples[file].content)
} catch (error) {
console.error(error)
}
}, 10)
}
}
}
}

@ -1,6 +1,7 @@
/* global Node, requestAnimationFrame */
import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json'
import * as remixBleach from '../../lib/remixBleach'
var yo = require('yo-yo')
var javascriptserialize = require('javascript-serialize')
@ -26,7 +27,7 @@ var ghostbar = yo`<div class=${css.ghostbar} bg-secondary></div>`
const profile = {
displayName: 'Terminal',
name: 'terminal',
methods: [],
methods: ['log'],
events: [],
description: ' - ',
version: packageJson.version
@ -113,6 +114,32 @@ class Terminal extends Plugin {
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) {
var command = this.commands.html
if (typeof command === 'function') command(html)
@ -653,7 +680,11 @@ class Terminal extends Plugin {
return function logger (args, scopedCommands, append) {
var types = args.filter(filterUndefined).map(type)
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)
return val
})

@ -65,15 +65,10 @@ class CompileTab extends ViewPlugin {
eventHandlers: {},
loading: false
}
this.compileTabLogic = new CompileTabLogic(this.queryParams, this.fileManager, this.editor, this.config, this.fileProvider, this.contentImport)
}
onActivationInternal () {
const miscApi = {
clearAnnotations: () => {
this.call('editor', 'clearAnnotations')
}
}
this.compileTabLogic = new CompileTabLogic(this.queryParams, this.fileManager, this.editor, this.config, this.fileProvider, this.contentImport, miscApi)
this.compiler = this.compileTabLogic.compiler
this.compileTabLogic.init()
@ -85,11 +80,28 @@ class CompileTab extends ViewPlugin {
)
}
resetResults () {
if (this._view.errorContainer) {
this._view.errorContainer.innerHTML = ''
}
this.compilerContainer.currentFile = ''
this.data.contractsDetails = {}
yo.update(this._view.contractSelection, this.contractSelection())
this.emit('statusChanged', { key: 'none' })
}
/************
* EVENTS
*/
listenToEvents () {
this.on('filePanel', 'setWorkspace', (workspace) => {
this.compileTabLogic.isHardhatProject().then((result) => {
if (result && workspace.isLocalhost) this.compilerContainer.hardhatCompilation.style.display = 'flex'
else this.compilerContainer.hardhatCompilation.style.display = 'none'
})
})
this.data.eventHandlers.onContentChanged = () => {
this.emit('statusChanged', { key: 'edited', title: 'the content has changed, needs recompilation', type: 'info' })
}
@ -113,6 +125,9 @@ class CompileTab extends ViewPlugin {
}
this.emit('statusChanged', { key: 'loading', title: 'compiling...', type: 'info' })
}
this.on('filePanel', 'setWorkspace', () => this.resetResults())
this.compileTabLogic.event.on('startingCompilation', this.data.eventHandlers.onStartingCompilation)
this.data.eventHandlers.onCurrentFileChanged = (name) => {
@ -199,7 +214,7 @@ class CompileTab extends ViewPlugin {
// ctrl+s or command+s
if ((e.metaKey || e.ctrlKey) && e.keyCode === 83) {
e.preventDefault()
this.compileTabLogic.runCompiler()
this.compileTabLogic.runCompiler(this.compilerContainer.hhCompilation)
}
})
}
@ -479,6 +494,7 @@ class CompileTab extends ViewPlugin {
}
onActivation () {
this.call('manager', 'activatePlugin', 'solidity-logic')
this.listenToEvents()
}
@ -492,6 +508,7 @@ class CompileTab extends ViewPlugin {
this.fileManager.events.removeListener('noFileSelected', this.data.eventHandlers.onNoFileSelected)
this.compiler.event.unregister('compilationFinished', this.data.eventHandlers.onCompilationFinished)
globalRegistry.get('themeModule').api.events.removeListener('themeChanged', this.data.eventHandlers.onThemeChanged)
this.call('manager', 'deactivatePlugin', 'solidity-logic')
}
}

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

@ -15,6 +15,7 @@ class CompilerContainer {
this.editor = editor
this.config = config
this.queryParams = queryParams
this.hhCompilation = false
this.data = {
hideWarnings: config.get('hideWarnings') || false,
@ -23,7 +24,7 @@ class CompilerContainer {
timeout: 300,
allversions: 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.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">`
@ -299,6 +304,7 @@ class CompilerContainer {
<label class="form-check-label custom-control-label" for="hideWarningsBox">Hide warnings</label>
</div>
</div>
${this.hardhatCompilation}
${this._view.compilationButton}
</header>
</article>
@ -326,12 +332,16 @@ class CompilerContainer {
this.config.set('autoCompile', this._view.autoCompile.checked)
}
updatehhCompilation (event) {
this.hhCompilation = event.target.checked
}
compile (event) {
const currentFile = this.config.get('currentFile')
if (!this.isSolFileSelected()) return
this._setCompilerVersionFromPragma(currentFile)
this.compileTabLogic.runCompiler()
this.compileTabLogic.runCompiler(this.hhCompilation)
}
compileIfAutoCompileOn () {
@ -517,7 +527,7 @@ class CompilerContainer {
// fetching both normal and wasm builds and creating a [version, baseUrl] map
async fetchAllVersion (callback) {
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
const binRes = await promisedMiniXhr(`${baseURLBin}/list.json`)
// 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.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) */

@ -9,6 +9,7 @@ const confirmDialog = require('../../ui/confirmDialog')
const modalDialog = require('../../ui/modaldialog')
const MultiParamManager = require('../../ui/multiParamManager')
const helper = require('../../../lib/helper')
const addTooltip = require('../../ui/tooltip')
const _paq = window._paq = window._paq || []
class ContractDropdownUI {
@ -48,24 +49,22 @@ class ContractDropdownUI {
}
listenToContextChange () {
this.blockchain.event.register('contextChanged', () => {
this.blockchain.updateNetwork((err, { name } = {}) => {
if (err) {
console.log('can\'t detect network')
return
}
this.exEnvironment = this.blockchain.getProvider()
this.networkName = name
this.blockchain.event.register('networkStatus', ({ error, network }) => {
if (error) {
console.log('can\'t detect network')
return
}
this.exEnvironment = this.blockchain.getProvider()
this.networkName = network.name
const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`)
const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`)
// check if an already selected option exist else use default workflow
if (savedConfig !== null) {
this.setCheckedState(savedConfig)
} else {
this.setCheckedState(this.networkName === 'Main')
}
})
// check if an already selected option exist else use default workflow
if (savedConfig !== null) {
this.setCheckedState(savedConfig)
} else {
this.setCheckedState(this.networkName === 'Main')
}
})
}
@ -99,7 +98,10 @@ class ContractDropdownUI {
enableAtAddress (enable) {
if (enable) {
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.setAttribute('title', 'Interact with the given contract.')
} else {
@ -306,10 +308,10 @@ class ContractDropdownUI {
const data = self.runView.compilersArtefacts.getCompilerAbstract(contractObject.contract.file)
self.runView.compilersArtefacts.addResolvedContract(helper.addressToString(address), data)
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)
} else {
_paq.push(['trackEvent', 'udapp', `DeployOnly_${this.networkName}`])
_paq.push(['trackEvent', 'udapp', 'DeployOnly', this.networkName])
}
}
@ -389,10 +391,19 @@ class ContractDropdownUI {
loadFromAddress () {
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,
(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) => {
if (error) {

@ -55,7 +55,7 @@ class DropdownLogic {
cb(null, 'abi', abi)
})
} else {
_paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithInstance'])
_paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithArtifacts'])
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 (call) return
const rawAddress = txResult.receipt.contractAddress
if (!rawAddress) return // not a contract creation
const address = helper.addressToString(rawAddress)
// save back created addresses for the convertion from tokens to real adresses

@ -1,3 +1,4 @@
import { BN } from 'ethereumjs-util'
const $ = require('jquery')
const yo = require('yo-yo')
const remixLib = require('@remix-project/remix-lib')
@ -65,14 +66,26 @@ class SettingsUI {
validateValue () {
const valueEl = this.el.querySelector('#value')
valueEl.value = parseInt(valueEl.value)
// assign 0 if given value is
// - empty
// - not valid (for ex 4345-54)
// - contains only '0's (for ex 0000) copy past or edit
if (!valueEl.value) valueEl.value = 0
if (!valueEl.value) {
// assign 0 if given value is
// - 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)
// - contains only '0's (for ex 0000) copy past or edit
valueEl.value = 0
}
// if giveen value is negative(possible with copy-pasting) set to 0
if (valueEl.value < 0) valueEl.value = 0
if (v.lt(0)) valueEl.value = 0
}
render () {
@ -84,7 +97,7 @@ class SettingsUI {
Environment
</label>
<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"
title="Execution environment does not connect to any node, everything is local and in memory only."
value="vm" name="executionContext"> JavaScript VM
@ -179,8 +192,17 @@ class SettingsUI {
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(() => {
this.updateNetwork()
this.fillAccountsList()
}, 1000)
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
this.selectExEnv.value = this.blockchain.getProvider()
this.event.trigger('clearInstance', [])
this.updateNetwork()
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
async fillAccountsList () {
this.accountListCallId++

@ -52,7 +52,7 @@ module.exports = class TestTab extends ViewPlugin {
}
listenToEvents () {
this.filePanel.event.register('newTestFileCreated', file => {
this.on('filePanel', 'newTestFileCreated', file => {
var testList = this._view.el.querySelector("[class^='testList']")
var test = this.createSingleTest(file)
testList.appendChild(test)

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

@ -25,24 +25,49 @@ function confirmDialog (tx, amount, gasEstimation, self, newGasPriceCb, initialP
}
var el = yo`
<div>
<div>You are creating a transaction on the main network. Click confirm if you are sure to continue.</div>
<div class=${css.txInfoBox}>
<div>From: ${tx.from}</div>
<div>To: ${tx.to ? tx.to : '(Contract Creation)'}</div>
<div>Amount: ${amount} Ether</div>
<div>Gas estimation: ${gasEstimation}</div>
<div>Gas limit: ${tx.gas}</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>Max transaction fee:<span id='txfee'></span></div>
<div>Data:</div>
<pre class=${css.wrapword}>${tx.data && tx.data.length > 50 ? tx.data.substring(0, 49) + '...' : tx.data} ${copyToClipboard(() => { return tx.data })}</pre>
<div>
<div>You are about to create a transaction on the Main Network. Confirm the details to send the info to your provider.
<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 class="mt-3 ${css.txInfoBox}">
<div>
<span class="text-dark mr-2">From:</span>
<span>${tx.from}</span>
</div>
<div>
<span class="text-dark mr-2">To:</span>
<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>
</div>
</div>
<div class="d-flex py-1 align-items-center custom-control custom-checkbox ${css.checkbox}">
<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 class=${css.checkbox}>
<input id='confirmsetting' type="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)
</div>
</div>
`
initialParamsCb((txFeeText, gasPriceValue, gasPriceStatus) => {

@ -244,12 +244,16 @@ UniversalDAppUI.prototype.runTransaction = function (lookupOnly, args, valArr, i
outputOverride.appendChild(decoded)
}
}
const info = `${lookupOnly ? 'call' : args.funABI.type !== 'fallback' ? 'lowLevelInteracions' : 'transact'}_${this.blockchain.executionContext.executionContext}`
_paq.push(['trackEvent', 'udapp', info])
let callinfo = ''
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 : ''
this.blockchain.runOrCallContractMethod(
args.contractName,
args.contractAbi,
args.contractABI,
args.funABI,
inputsValues,
args.address,

@ -1,33 +1,24 @@
const remixLib = require('@remix-project/remix-lib')
const txFormat = remixLib.execution.txFormat
const txExecution = remixLib.execution.txExecution
const typeConversion = remixLib.execution.typeConversion
const Txlistener = remixLib.execution.txListener
const TxRunner = remixLib.execution.txRunner
const txHelper = remixLib.execution.txHelper
const EventManager = remixLib.EventManager
const executionContext = remixLib.execution.executionContext
const Web3 = require('web3')
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')
import Web3 from 'web3'
import { toBuffer, addHexPrefix } from 'ethereumjs-util'
import { waterfall } from 'async'
import { EventEmitter } from 'events'
import { ExecutionContext } from './execution-context'
import VMProvider from './providers/vm.js'
import InjectedProvider from './providers/injected.js'
import NodeProvider from './providers/node.js'
import { execution, EventManager, helpers } from '@remix-project/remix-lib'
const { txFormat, txExecution, typeConversion, txListener: Txlistener, TxRunner, TxRunnerWeb3, txHelper } = execution
const { txResultHelper: resultToRemixTx } = helpers
class Blockchain {
// NOTE: the config object will need to be refactored out in remix-lib
constructor (config) {
this.event = new EventManager()
this.executionContext = executionContext
this.executionContext = new ExecutionContext()
this.events = new EventEmitter()
this.config = config
this.txRunner = new TxRunner({}, {
const web3Runner = new TxRunnerWeb3({
config: config,
detectNetwork: (cb) => {
this.executionContext.detectNetwork(cb)
@ -35,10 +26,13 @@ class Blockchain {
personalMode: () => {
return this.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false
}
}, this.executionContext)
}, _ => this.executionContext.web3(), _ => this.executionContext.currentblockGasLimit())
this.txRunner = new TxRunner(web3Runner, { runAsync: true })
this.executionContext.event.register('contextChanged', this.resetEnvironment.bind(this))
this.networkcallid = 0
this.networkStatus = { name: ' - ', id: ' - ' }
this.setupEvents()
this.setupProviders()
}
@ -55,6 +49,17 @@ class Blockchain {
this.executionContext.event.register('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 () {
@ -123,7 +128,7 @@ class Blockchain {
if (error) {
return finalCb(`creation of ${selectedContract.name} errored: ${(error.message ? error.message : error)}`)
}
if (txResult.result.status && txResult.result.status === '0x0') {
if (txResult.receipt.status === false || txResult.receipt.status === '0x0') {
return finalCb(`creation of ${selectedContract.name} errored: transaction execution failed`)
}
finalCb(null, selectedContract, address)
@ -195,15 +200,6 @@ class Blockchain {
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) {
return this.executionContext.detectNetwork(cb)
}
@ -257,6 +253,10 @@ class Blockchain {
}
if (funABI.type === 'fallback') data.dataHex = value
if (data) {
data.contractName = contractName
data.contractABI = contractAbi
}
const useCall = funABI.stateMutability === 'view' || funABI.stateMutability === 'pure'
this.runTx({ to: address, data, useCall }, confirmationCb, continueCb, promptCb, (error, txResult, _address, returnValue) => {
if (error) {
@ -309,18 +309,17 @@ class Blockchain {
resetEnvironment () {
this.getCurrentProvider().resetEnvironment()
// TODO: most params here can be refactored away in txRunner
// this.txRunner = new TxRunner(this.providers.vm.accounts, {
this.txRunner = new TxRunner(this.providers.vm.RemixSimulatorProvider.Accounts.accounts, {
// TODO: only used to check value of doNotShowTransactionConfirmationAgain property
const web3Runner = new TxRunnerWeb3({
config: this.config,
// TODO: to refactor, TxRunner already has access to executionContext
detectNetwork: (cb) => {
this.executionContext.detectNetwork(cb)
},
personalMode: () => {
return this.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false
}
}, this.executionContext)
}, _ => this.executionContext.web3(), _ => this.executionContext.currentblockGasLimit())
this.txRunner = new TxRunner(web3Runner, { runAsync: true })
this.txRunner.event.register('transactionBroadcasted', (txhash) => {
this.executionContext.detectNetwork((error, network) => {
if (error || !network) return
@ -372,10 +371,11 @@ class Blockchain {
(network, tx, gasEstimation, continueTxExecution, cancelCb) => { continueTxExecution() },
(error, continueTxExecution, cancelCb) => { if (error) { reject(error) } else { continueTxExecution() } },
(okCb, cancelCb) => { okCb() },
(error, result) => {
async (error, result) => {
if (error) return reject(error)
try {
resolve(resultToRemixTx(result))
const execResult = await this.web3().eth.getExecutionResultFromSimulator(result.transactionHash)
resolve(resultToRemixTx(result, execResult))
} catch (e) {
reject(e)
}
@ -387,7 +387,7 @@ class Blockchain {
runTx (args, confirmationCb, continueCb, promptCb, cb) {
const self = this
async.waterfall([
waterfall([
function getGasLimit (next) {
if (self.transactionContextAPI.getGasLimit) {
return self.transactionContextAPI.getGasLimit(next)
@ -429,19 +429,24 @@ class Blockchain {
function runTransaction (fromAddress, value, gasLimit, next) {
const tx = { to: args.to, data: args.data.dataHex, useCall: args.useCall, from: fromAddress, value: value, gasLimit: gasLimit, timestamp: args.data.timestamp }
const payLoad = { funAbi: args.data.funAbi, funArgs: args.data.funArgs, contractBytecode: args.data.contractBytecode, contractName: args.data.contractName, contractABI: args.data.contractABI, linkReferences: args.data.linkReferences }
let timestamp = Date.now()
if (tx.timestamp) {
timestamp = tx.timestamp
}
if (!tx.timestamp) tx.timestamp = Date.now()
const timestamp = tx.timestamp
self.event.trigger('initiatingTransaction', [timestamp, tx, payLoad])
self.txRunner.rawRun(tx, confirmationCb, continueCb, promptCb,
function (error, result) {
async (error, result) => {
if (error) return next(error)
const rawAddress = self.executionContext.isVM() ? (result.result.createdAddress && result.result.createdAddress.toBuffer()) : result.result.contractAddress
const isVM = self.executionContext.isVM()
if (isVM && tx.useCall) {
try {
result.transactionHash = await self.web3().eth.getHashFromTagBySimulator(timestamp)
} catch (e) {
console.log('unable to retrieve back the "call" hash', e)
}
}
const eventName = (tx.useCall ? 'callExecuted' : 'transactionExecuted')
self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad, rawAddress])
self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad])
if (error && (typeof (error) !== 'string')) {
if (error.message) error = error.message
@ -449,30 +454,46 @@ class Blockchain {
try { error = 'error: ' + JSON.stringify(error) } catch (e) {}
}
}
next(error, result)
next(error, result, tx)
}
)
}
],
(error, txResult) => {
async (error, txResult, tx) => {
if (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()
let execResult
let returnValue = null
if (isVM) {
const vmError = txExecution.checkVMError(txResult)
if (vmError.error) {
return cb(vmError.message)
execResult = await this.web3().eth.getExecutionResultFromSimulator(txResult.transactionHash)
if (execResult) {
// if it's not the VM, we don't have return value. We only have the transaction, and it does not contain the return value.
returnValue = execResult ? execResult.returnValue : toBuffer(addHexPrefix(txResult.result) || '0x0000000000000000000000000000000000000000000000000000000000000000')
const vmError = txExecution.checkVMError(execResult, args.data.contractABI)
if (vmError.error) {
return cb(vmError.message)
}
}
}
if (!isVM && tx && tx.useCall) {
returnValue = toBuffer(addHexPrefix(txResult.result))
}
let address = null
let returnValue = null
if (txResult && txResult.result) {
address = isVM ? (txResult.result.createdAddress && txResult.result.createdAddress.toBuffer()) : txResult.result.contractAddress
// if it's not the VM, we don't have return value. We only have the transaction, and it does not contain the return value.
returnValue = (txResult.result.execResult && isVM) ? txResult.result.execResult.returnValue : txResult.result
if (txResult && txResult.receipt) {
address = txResult.receipt.contractAddress
}
cb(error, txResult, address, returnValue)

@ -1,139 +1,33 @@
/* global ethereum */
'use strict'
import Web3 from 'web3'
import { EventManager } from '../eventManager'
import { rlp, keccak, bufferToHex } from 'ethereumjs-util'
import { Web3VmProvider } from '../web3Provider/web3VmProvider'
import { LogsManager } from './logsManager'
import VM from '@ethereumjs/vm'
import Common from '@ethereumjs/common'
import StateManager from '@ethereumjs/vm/dist/state/stateManager'
import { StorageDump } from '@ethereumjs/vm/dist/state/interface'
import EventManager from '../lib/events'
declare let ethereum: any
let web3
if (typeof window !== 'undefined' && typeof window['ethereum'] !== 'undefined') {
var injectedProvider = window['ethereum']
if (typeof window !== 'undefined' && typeof window.ethereum !== 'undefined') {
var injectedProvider = window.ethereum
web3 = new Web3(injectedProvider)
} else {
web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'))
}
/*
extend vm state manager and instanciate VM
*/
class StateManagerCommonStorageDump extends StateManager {
/*
* dictionary containing keccak(b) as key and b as value. used to get the initial value from its hash
*/
keyHashes: { [key: string]: string }
constructor () {
super()
this.keyHashes = {}
}
putContractStorage (address, key, value) {
this.keyHashes[keccak(key).toString('hex')] = bufferToHex(key)
return super.putContractStorage(address, key, value)
}
async dumpStorage (address) {
let trie
try {
trie = await this._getStorageTrie(address)
} catch (e) {
console.log(e)
throw e
}
return new Promise<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
*/
export class ExecutionContext {
event
logsManager
blockGasLimitDefault
blockGasLimit
customNetWorks
blocks
latestBlockNumber
txs
executionContext
listenOnLastBlockId
currentFork: string
vms
mainNetGenesisHash: string
constructor () {
this.event = new EventManager()
this.logsManager = new LogsManager()
this.executionContext = null
this.blockGasLimitDefault = 4300000
this.blockGasLimit = this.blockGasLimitDefault
this.currentFork = 'berlin'
this.vms = {
/*
byzantium: createVm('byzantium'),
constantinople: createVm('constantinople'),
petersburg: createVm('petersburg'),
istanbul: createVm('istanbul'),
*/
berlin: this.createVm('berlin')
}
this.mainNetGenesisHash = '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3'
this.customNetWorks = {}
this.blocks = {}
this.latestBlockNumber = 0
this.txs = {}
this.customWeb3 = {} // mapping between a context name and a web3.js instance
}
init (config) {
@ -145,20 +39,6 @@ export class ExecutionContext {
}
}
createVm (hardfork) {
const stateManager = new StateManagerCommonStorageDump()
const common = new Common({ chain: 'mainnet', hardfork })
const vm = new VM({
common,
activatePrecompiles: true,
stateManager: stateManager
})
const web3vm = new Web3VmProvider()
web3vm.setVM(vm)
return { vm, web3vm, stateManager, common }
}
askPermission () {
// metamask
if (ethereum && typeof ethereum.enable === 'function') ethereum.enable()
@ -172,7 +52,12 @@ export class ExecutionContext {
return this.executionContext === 'vm'
}
setWeb3 (context, web3) {
this.customWeb3[context] = web3
}
web3 () {
if (this.customWeb3[this.executionContext]) return this.customWeb3[this.executionContext]
return this.isVM() ? this.vms[this.currentFork].web3vm : web3
}
@ -228,14 +113,6 @@ export class ExecutionContext {
return new Web3()
}
vm () {
return this.vms[this.currentFork].vm
}
vmObject () {
return this.vms[this.currentFork]
}
setContext (context, endPointUrl, confirmCb, infoCb) {
this.executionContext = context
this.executionContextChange(context, endPointUrl, confirmCb, infoCb, null)
@ -268,7 +145,6 @@ export class ExecutionContext {
if (context === 'web3') {
confirmCb(cb)
}
if (this.customNetWorks[context]) {
var network = this.customNetWorks[context]
this.setProviderFromEndpoint(network.provider, network.name, (error) => {
@ -312,14 +188,16 @@ export class ExecutionContext {
const oldProvider = web3.currentProvider
web3.setProvider(endpoint)
web3.eth.net.isListening((err, isConnected) => {
if (!err && isConnected) {
if (!err && isConnected === true) {
this.executionContext = context
this._updateBlockGasLimit()
this.event.trigger('contextChanged', [context])
this.event.trigger('web3EndpointChanged')
cb()
} else if (isConnected === 'canceled') {
web3.setProvider(oldProvider)
cb()
} else {
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.')
@ -340,22 +218,4 @@ export class ExecutionContext {
return transactionDetailsLinks[network] + hash
}
}
addBlock (block) {
let blockNumber = '0x' + block.header.number.toString('hex')
if (blockNumber === '0x') {
blockNumber = '0x0'
}
blockNumber = web3.utils.toHex(web3.utils.toBN(blockNumber))
this.blocks['0x' + block.hash().toString('hex')] = block
this.blocks[blockNumber] = block
this.latestBlockNumber = blockNumber
this.logsManager.checkBlock(blockNumber, block, this.web3())
}
trackTx (tx, block) {
this.txs[tx] = block
}
}

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

@ -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 = {}
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) => {
if (!errorLoadingFile) {

@ -71,6 +71,20 @@ module.exports = {
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) {
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)
if (paths.length === 1) return paths[0]
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',
'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp']
const dependentModules = ['git'] // module which shouldn't be manually activated (e.g git is activated by remixd)
const dependentModules = ['git', 'hardhat'] // module which shouldn't be manually activated (e.g git is activated by remixd)
export function isNative (name) {
const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons']
const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity']
return nativePlugins.includes(name) || requiredModules.includes(name)
}
@ -111,6 +111,12 @@ export class RemixAppManager extends PluginManager {
try {
const res = await fetch(this.pluginsDirectory)
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))
} catch (e) {
console.log('getting plugins list from localstorage...')

@ -5,5 +5,18 @@ module.exports = {
},
resolver: '@nrwl/jest/plugins/resolver',
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'
import { RefType } from './RefType'
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 {
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.
// key should be a hex string, and position an int
const mappingK = toBuffer('0x' + key)
let mappingP = toBuffer(position)
const mappingK = toBuffer(addHexPrefix(key))
let mappingP = toBuffer(addHexPrefix(position))
mappingP = setLengthLeft(mappingP, 32)
const mappingKeyBuf = concatTypedArrays(mappingK, mappingP)
const mappingStorageLocation: Buffer = keccak(mappingKeyBuf)

@ -52,27 +52,45 @@ export class RefType {
offset = parseInt(offset, 16)
return this.decodeFromMemoryInternal(offset, memory, cursor)
} else if (this.isInCallData()) {
calldata = calldata.length > 0 ? calldata[0] : '0x'
const ethersAbi = new ethers.utils.Interface(variableDetails.abi)
const fnSign = calldata.substr(0, 10)
const decodedData = ethersAbi.decodeFunctionData(ethersAbi.getFunction(fnSign), calldata)
let decodedValue = decodedData[variableDetails.name]
const isArray = Array.isArray(decodedValue)
if (isArray) {
decodedValue = decodedValue.map((el) => {
return {
value: el.toString(),
type: this.underlyingType.typeName
}
})
}
return 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'
const ethersAbi = new ethers.utils.Interface(variableDetails.abi)
const fnSign = calldata.substr(0, 10)
const decodedData = ethersAbi.decodeFunctionData(ethersAbi.getFunction(fnSign), calldata)
const decodedValue = decodedData[variableDetails.name]
const isArray = Array.isArray(decodedValue)
if (isArray) {
return this._decodeCallDataArray(decodedValue, this)
}
return {
length: isArray ? '0x' + decodedValue.length.toString(16) : undefined,
value: decodedValue,
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: Array.isArray(decodedValue) ? '0x' + decodedValue.length.toString(16) : undefined,
value: decodedValue,
type: this.typeName
length: value.length.toString(16),
value: value,
type: type.typeName
}
} 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 miscLocal = require('./contracts/miscLocal')
var structArrayLocal = require('./contracts/structArrayLocal')
var calldataLocal = require('./contracts/calldata')
var vmCall = require('./vmCall')
var intLocalTest = require('./localsTests/int')
var miscLocalTest = require('./localsTests/misc')
var misc2LocalTest = require('./localsTests/misc2')
var structArrayLocalTest = require('./localsTests/structArray')
var calldataLocalTest = require('./localsTests/calldata')
var compilerInput = require('../helpers/compilerHelper').compilerInput
tape('solidity', function (t) {
t.test('local decoder', async function (st) {
var privateKey = Buffer.from('dae9801649ba2d95a21e688b56f77905e5667c44ce868ec83f82e838712a2c7a', 'hex')
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))
output = JSON.parse(output)
intLocalTest(st, vm, privateKey, output.contracts['test.sol']['intLocal'].evm.bytecode.object, output, function () {
output = compiler.compile(compilerInput(miscLocal.contract))
output = JSON.parse(output)
miscLocalTest(st, vm, privateKey, output.contracts['test.sol']['miscLocal'].evm.bytecode.object, output, function () {
output = compiler.compile(compilerInput(miscLocal.contract))
output = JSON.parse(output)
misc2LocalTest(st, vm, privateKey, output.contracts['test.sol']['miscLocal2'].evm.bytecode.object, output, function () {
output = compiler.compile(compilerInput(structArrayLocal.contract))
output = JSON.parse(output)
structArrayLocalTest(st, vm, privateKey, output.contracts['test.sol']['structArrayLocal'].evm.bytecode.object, output, function () {
st.end()
})
})
})
})
await intLocalTest(st, vm, privateKey, output.contracts['test.sol']['intLocal'].evm.bytecode.object, output)
output = compiler.compile(compilerInput(miscLocal.contract))
output = JSON.parse(output)
await miscLocalTest(st, vm, privateKey, output.contracts['test.sol']['miscLocal'].evm.bytecode.object, output)
output = compiler.compile(compilerInput(miscLocal.contract))
output = JSON.parse(output)
await misc2LocalTest(st, vm, privateKey, output.contracts['test.sol']['miscLocal2'].evm.bytecode.object, output)
output = compiler.compile(compilerInput(structArrayLocal.contract))
output = JSON.parse(output)
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()
}

@ -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,119 +9,122 @@ import { InternalCallTree } from '../../../src/solidity-decoder/internalCallTree
import { EventManager } from '../../../src/eventManager'
import * as helper from './helper'
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, txHash) {
if (error) {
return st.fail(error)
}
vm.web3.eth.getTransaction(txHash, function (error, tx) {
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult) {
return new Promise((resolve) => {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, data) {
if (error) {
return st.fail(error)
}
tx.to = contractCreationToken('0')
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) => {
try {
let functions1 = callTree.retrieveFunctionsStack(102)
let functions2 = callTree.retrieveFunctionsStack(115)
let functions3 = callTree.retrieveFunctionsStack(13)
st.equals(functions1.length, 1)
st.equals(functions2.length, 2)
st.equals(functions3.length, 0)
st.equals(Object.keys(functions1[0])[0], 'functionDefinition')
st.equals(Object.keys(functions1[0])[1], 'inputs')
st.equals(functions1[0].inputs[0], 'foo')
st.equals(Object.keys(functions2[0])[0], 'functionDefinition')
st.equals(Object.keys(functions2[0])[1], 'inputs')
st.equals(Object.keys(functions2[1])[0], 'functionDefinition')
st.equals(Object.keys(functions2[1])[1], 'inputs')
st.equals(functions2[0].inputs[0], 'asd')
st.equals(functions2[1].inputs[0], 'foo')
st.equals(functions1[0].functionDefinition.name, 'level11')
st.equals(functions2[0].functionDefinition.name, 'level12')
st.equals(functions2[1].functionDefinition.name, 'level11')
st.equals(scopeStarts[0], '')
st.equals(scopeStarts[13], '1')
st.equals(scopeStarts[102], '2')
st.equals(scopeStarts[115], '2.1')
st.equals(scopeStarts[136], '3')
st.equals(scopeStarts[153], '4')
st.equals(scopeStarts[166], '4.1')
st.equals(scopes[''].locals['ui8'].type.typeName, 'uint8')
st.equals(scopes[''].locals['ui16'].type.typeName, 'uint16')
st.equals(scopes[''].locals['ui32'].type.typeName, 'uint32')
st.equals(scopes[''].locals['ui64'].type.typeName, 'uint64')
st.equals(scopes[''].locals['ui128'].type.typeName, 'uint128')
st.equals(scopes[''].locals['ui256'].type.typeName, 'uint256')
st.equals(scopes[''].locals['ui'].type.typeName, 'uint256')
st.equals(scopes[''].locals['i8'].type.typeName, 'int8')
st.equals(scopes[''].locals['i16'].type.typeName, 'int16')
st.equals(scopes[''].locals['i32'].type.typeName, 'int32')
st.equals(scopes[''].locals['i64'].type.typeName, 'int64')
st.equals(scopes[''].locals['i128'].type.typeName, 'int128')
st.equals(scopes[''].locals['i256'].type.typeName, 'int256')
st.equals(scopes[''].locals['i'].type.typeName, 'int256')
st.equals(scopes[''].locals['ishrink'].type.typeName, 'int32')
st.equals(scopes['2'].locals['ui8'].type.typeName, 'uint8')
st.equals(scopes['2.1'].locals['ui81'].type.typeName, 'uint8')
st.equals(scopes['3'].locals['ui81'].type.typeName, 'uint8')
st.equals(scopes['4'].locals['ui8'].type.typeName, 'uint8')
st.equals(scopes['4.1'].locals['ui81'].type.typeName, 'uint8')
} catch (e) {
st.fail(e.message)
const txHash = data.hash
vm.web3.eth.getTransaction(txHash, function (error, tx) {
if (error) {
return st.fail(error)
}
helper.decodeLocals(st, 95, traceManager, callTree, function (locals) {
st.equals(Object.keys(locals).length, 16)
st.equals(locals['ui8'].value, '130')
st.equals(locals['ui16'].value, '456')
st.equals(locals['ui32'].value, '4356')
st.equals(locals['ui64'].value, '3543543543')
st.equals(locals['ui128'].value, '234567')
st.equals(locals['ui256'].value, '115792089237316195423570985008687907853269984665640564039457584007880697216513')
st.equals(locals['ui'].value, '123545666')
st.equals(locals['i8'].value, '-45')
st.equals(locals['i16'].value, '-1234')
st.equals(locals['i32'].value, '3455')
st.equals(locals['i64'].value, '-35566')
st.equals(locals['i128'].value, '-444444')
st.equals(locals['i256'].value, '3434343')
st.equals(locals['i'].value, '-32432423423')
st.equals(locals['ishrink'].value, '2')
tx.to = contractCreationToken('0')
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)
})
helper.decodeLocals(st, 106, traceManager, callTree, function (locals) {
callTree.event.register('callTreeNotReady', (reason) => {
st.fail(reason)
})
callTree.event.register('callTreeReady', (scopes, scopeStarts) => {
try {
st.equals(locals['ui8'].value, '123')
st.equals(Object.keys(locals).length, 2)
let functions1 = callTree.retrieveFunctionsStack(102)
let functions2 = callTree.retrieveFunctionsStack(115)
let functions3 = callTree.retrieveFunctionsStack(13)
st.equals(functions1.length, 1)
st.equals(functions2.length, 2)
st.equals(functions3.length, 0)
st.equals(Object.keys(functions1[0])[0], 'functionDefinition')
st.equals(Object.keys(functions1[0])[1], 'inputs')
st.equals(functions1[0].inputs[0], 'foo')
st.equals(Object.keys(functions2[0])[0], 'functionDefinition')
st.equals(Object.keys(functions2[0])[1], 'inputs')
st.equals(Object.keys(functions2[1])[0], 'functionDefinition')
st.equals(Object.keys(functions2[1])[1], 'inputs')
st.equals(functions2[0].inputs[0], 'asd')
st.equals(functions2[1].inputs[0], 'foo')
st.equals(functions1[0].functionDefinition.name, 'level11')
st.equals(functions2[0].functionDefinition.name, 'level12')
st.equals(functions2[1].functionDefinition.name, 'level11')
st.equals(scopeStarts[0], '')
st.equals(scopeStarts[13], '1')
st.equals(scopeStarts[102], '2')
st.equals(scopeStarts[115], '2.1')
st.equals(scopeStarts[136], '3')
st.equals(scopeStarts[153], '4')
st.equals(scopeStarts[166], '4.1')
st.equals(scopes[''].locals['ui8'].type.typeName, 'uint8')
st.equals(scopes[''].locals['ui16'].type.typeName, 'uint16')
st.equals(scopes[''].locals['ui32'].type.typeName, 'uint32')
st.equals(scopes[''].locals['ui64'].type.typeName, 'uint64')
st.equals(scopes[''].locals['ui128'].type.typeName, 'uint128')
st.equals(scopes[''].locals['ui256'].type.typeName, 'uint256')
st.equals(scopes[''].locals['ui'].type.typeName, 'uint256')
st.equals(scopes[''].locals['i8'].type.typeName, 'int8')
st.equals(scopes[''].locals['i16'].type.typeName, 'int16')
st.equals(scopes[''].locals['i32'].type.typeName, 'int32')
st.equals(scopes[''].locals['i64'].type.typeName, 'int64')
st.equals(scopes[''].locals['i128'].type.typeName, 'int128')
st.equals(scopes[''].locals['i256'].type.typeName, 'int256')
st.equals(scopes[''].locals['i'].type.typeName, 'int256')
st.equals(scopes[''].locals['ishrink'].type.typeName, 'int32')
st.equals(scopes['2'].locals['ui8'].type.typeName, 'uint8')
st.equals(scopes['2.1'].locals['ui81'].type.typeName, 'uint8')
st.equals(scopes['3'].locals['ui81'].type.typeName, 'uint8')
st.equals(scopes['4'].locals['ui8'].type.typeName, 'uint8')
st.equals(scopes['4.1'].locals['ui81'].type.typeName, 'uint8')
} catch (e) {
st.fail(e.message)
}
cb()
helper.decodeLocals(st, 95, traceManager, callTree, function (locals) {
st.equals(Object.keys(locals).length, 16)
st.equals(locals['ui8'].value, '130')
st.equals(locals['ui16'].value, '456')
st.equals(locals['ui32'].value, '4356')
st.equals(locals['ui64'].value, '3543543543')
st.equals(locals['ui128'].value, '234567')
st.equals(locals['ui256'].value, '115792089237316195423570985008687907853269984665640564039457584007880697216513')
st.equals(locals['ui'].value, '123545666')
st.equals(locals['i8'].value, '-45')
st.equals(locals['i16'].value, '-1234')
st.equals(locals['i32'].value, '3455')
st.equals(locals['i64'].value, '-35566')
st.equals(locals['i128'].value, '-444444')
st.equals(locals['i256'].value, '3434343')
st.equals(locals['i'].value, '-32432423423')
st.equals(locals['ishrink'].value, '2')
})
helper.decodeLocals(st, 106, traceManager, callTree, function (locals) {
try {
st.equals(locals['ui8'].value, '123')
st.equals(Object.keys(locals).length, 2)
} catch (e) {
st.fail(e.message)
}
resolve({})
})
})
traceManager.resolveTrace(tx).then(() => {
debuggerEvent.trigger('newTraceLoaded', [traceManager.trace])
}).catch((error) => {
st.fail(error)
})
})
traceManager.resolveTrace(tx).then(() => {
debuggerEvent.trigger('newTraceLoaded', [traceManager.trace])
}).catch((error) => {
st.fail(error)
})
})
})
})
}

@ -8,66 +8,69 @@ import * as helper from './helper'
import { TraceManager } from '../../../src/trace/traceManager'
import { CodeManager } from '../../../src/code/codeManager'
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, txHash) {
if (error) {
return st.fail(error)
}
vm.web3.eth.getTransaction(txHash, function (error, tx) {
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult) {
return new Promise((resolve) => {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, data) {
if (error) {
return st.fail(error)
}
tx.to = contractCreationToken('0')
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('callTreeReady', (scopes, scopeStarts) => {
helper.decodeLocals(st, 70, traceManager, callTree, function (locals) {
try {
st.equals(locals['boolFalse'].value, false)
st.equals(locals['boolTrue'].value, true)
st.equals(locals['testEnum'].value, 'three')
st.equals(locals['sender'].value, '0x4B0897B0513FDC7C541B6D9D7E929C4E5364D2DB')
st.equals(locals['_bytes1'].value, '0x99')
st.equals(locals['__bytes1'].value, '0x99')
st.equals(locals['__bytes2'].value, '0x99AB')
st.equals(locals['__bytes4'].value, '0x99FA0000')
st.equals(locals['__bytes6'].value, '0x990000000000')
st.equals(locals['__bytes7'].value, '0x99356700000000')
st.equals(locals['__bytes8'].value, '0x99ABD41700000000')
st.equals(locals['__bytes9'].value, '0x99156744AF00000000')
st.equals(locals['__bytes13'].value, '0x99123423425300000000000000')
st.equals(locals['__bytes16'].value, '0x99AFAD23432400000000000000000000')
st.equals(locals['__bytes24'].value, '0x99AFAD234324000000000000000000000000000000000000')
st.equals(locals['__bytes32'].value, '0x9999ABD41799ABD4170000000000000000000000000000000000000000000000')
st.equals(Object.keys(locals).length, 16)
} catch (e) {
st.fail(e.message)
}
const txHash = data.hash
vm.web3.eth.getTransaction(txHash, function (error, tx) {
if (error) {
return st.fail(error)
}
tx.to = contractCreationToken('0')
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)
})
helper.decodeLocals(st, 7, traceManager, callTree, function (locals) {
try {
// st.equals(Object.keys(locals).length, 0)
st.equals(0, 0)
} catch (e) {
st.fail(e.message)
}
cb()
callTree.event.register('callTreeReady', (scopes, scopeStarts) => {
helper.decodeLocals(st, 70, traceManager, callTree, function (locals) {
try {
st.equals(locals['boolFalse'].value, false)
st.equals(locals['boolTrue'].value, true)
st.equals(locals['testEnum'].value, 'three')
st.equals(locals['sender'].value, '0x4B0897B0513FDC7C541B6D9D7E929C4E5364D2DB')
st.equals(locals['_bytes1'].value, '0x99')
st.equals(locals['__bytes1'].value, '0x99')
st.equals(locals['__bytes2'].value, '0x99AB')
st.equals(locals['__bytes4'].value, '0x99FA0000')
st.equals(locals['__bytes6'].value, '0x990000000000')
st.equals(locals['__bytes7'].value, '0x99356700000000')
st.equals(locals['__bytes8'].value, '0x99ABD41700000000')
st.equals(locals['__bytes9'].value, '0x99156744AF00000000')
st.equals(locals['__bytes13'].value, '0x99123423425300000000000000')
st.equals(locals['__bytes16'].value, '0x99AFAD23432400000000000000000000')
st.equals(locals['__bytes24'].value, '0x99AFAD234324000000000000000000000000000000000000')
st.equals(locals['__bytes32'].value, '0x9999ABD41799ABD4170000000000000000000000000000000000000000000000')
st.equals(Object.keys(locals).length, 16)
} catch (e) {
st.fail(e.message)
}
})
helper.decodeLocals(st, 7, traceManager, callTree, function (locals) {
try {
// st.equals(Object.keys(locals).length, 0)
st.equals(0, 0)
} catch (e) {
st.fail(e.message)
}
resolve({})
})
})
traceManager.resolveTrace(tx).then(() => {
debuggerEvent.trigger('newTraceLoaded', [traceManager.trace])
}).catch((error) => {
st.fail(error)
})
})
traceManager.resolveTrace(tx).then(() => {
debuggerEvent.trigger('newTraceLoaded', [traceManager.trace])
}).catch((error) => {
st.fail(error)
})
})
})
})
}

@ -8,52 +8,55 @@ import * as helper from './helper'
import { TraceManager } from '../../../src/trace/traceManager'
import { CodeManager } from '../../../src/code/codeManager'
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, txHash) {
if (error) {
return st.fail(error)
}
vm.web3.eth.getTransaction(txHash, function (error, tx) {
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult) {
return new Promise((resolve) => {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, data) {
if (error) {
return st.fail(error)
}
tx.to = contractCreationToken('0')
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('callTreeReady', (scopes, scopeStarts) => {
helper.decodeLocals(st, 49, traceManager, callTree, function (locals) {
try {
st.equals(locals['dynbytes'].value, '0x64796e616d69636279746573')
st.equals(locals['smallstring'].value, 'test_test_test')
st.equals(Object.keys(locals).length, 2)
} catch (e) {
st.fail(e.message)
}
const txHash = data.hash
vm.web3.eth.getTransaction(txHash, function (error, tx) {
if (error) {
return st.fail(error)
}
tx.to = contractCreationToken('0')
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)
})
helper.decodeLocals(st, 7, traceManager, callTree, function (locals) {
try {
// st.equals(Object.keys(locals).length, 0)
st.equals(0, 0)
} catch (e) {
st.fail(e.message)
}
cb()
callTree.event.register('callTreeReady', (scopes, scopeStarts) => {
helper.decodeLocals(st, 49, traceManager, callTree, function (locals) {
try {
st.equals(locals['dynbytes'].value, '0x64796e616d69636279746573')
st.equals(locals['smallstring'].value, 'test_test_test')
st.equals(Object.keys(locals).length, 2)
} catch (e) {
st.fail(e.message)
}
})
helper.decodeLocals(st, 7, traceManager, callTree, function (locals) {
try {
// st.equals(Object.keys(locals).length, 0)
st.equals(0, 0)
} catch (e) {
st.fail(e.message)
}
resolve({})
})
})
traceManager.resolveTrace(tx).then(() => {
debuggerEvent.trigger('newTraceLoaded', [traceManager.trace])
}).catch((error) => {
st.fail(error)
})
})
traceManager.resolveTrace(tx).then(() => {
debuggerEvent.trigger('newTraceLoaded', [traceManager.trace])
}).catch((error) => {
st.fail(error)
})
})
})
})
}

@ -8,110 +8,113 @@ import * as helper from './helper'
import { TraceManager } from '../../../src/trace/traceManager'
import { CodeManager } from '../../../src/code/codeManager'
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, txHash) {
if (error) {
return st.fail(error)
}
vm.web3.eth.getTransaction(txHash, function (error, tx) {
module.exports = function (st, vm, privateKey, contractBytecode, compilationResult) {
return new Promise((resolve) => {
sendTx(vm, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, data) {
if (error) {
return st.fail(error)
}
tx.to = contractCreationToken('0')
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('callTreeReady', (scopes, scopeStarts) => {
helper.decodeLocals(st, 1622, traceManager, callTree, function (locals) {
try {
st.equals(locals['bytesSimple'].length, '0x14')
st.equals(locals['bytesSimple'].value, '0x746573745f7375706572')
st.equals(locals['e'].value['a'].value, 'test')
st.equals(locals['e'].value['a'].length, '0x8')
st.equals(locals['e'].value['a'].raw, '0x74657374')
st.equals(locals['e'].value['b'].value, '5')
st.equals(locals['e'].value['c'].length, '0x220')
st.equals(locals['e'].value['c'].raw, '0x746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374')
st.equals(locals['e'].value['c'].value, 'test_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_test')
st.equals(locals['e'].value['d'].value, '3')
st.equals(locals['f'].length, '0x1b8')
st.equals(locals['f'].raw, '0x746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f')
st.equals(locals['f'].value, 'test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_')
st.equals(locals['e'].value['e'].value, true)
st.equals(locals['simpleArray'].value[0].value, '45')
st.equals(locals['simpleArray'].value[1].value, '324324')
st.equals(locals['simpleArray'].value[2].value, '-333')
st.equals(locals['simpleArray'].value[3].value, '5656')
st.equals(locals['simpleArray'].value[4].value, '-1111')
st.equals(locals['stringArray'].value[0].value, 'long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_')
st.equals(locals['stringArray'].value[1].value, 'two')
st.equals(locals['stringArray'].value[2].value, 'three')
st.equals(locals['dynArray'].value[0].value[0].value, '3423423532')
st.equals(locals['dynArray'].value[1].value[0].value, '-342343323532')
st.equals(locals['dynArray'].value[1].value[1].value, '23432')
st.equals(locals['dynArray'].value[2].value[0].value, '-432432')
st.equals(locals['dynArray'].value[2].value[1].value, '3423423532')
st.equals(locals['dynArray'].value[2].value[2].value, '-432432')
st.equals(locals['structArray'].value[0].value['a'].value, 'test')
st.equals(locals['structArray'].value[0].value['a'].length, '0x8')
st.equals(locals['structArray'].value[0].value['a'].raw, '0x74657374')
st.equals(locals['structArray'].value[0].value['b'].value, '5')
st.equals(locals['structArray'].value[0].value['c'].length, '0x220')
st.equals(locals['structArray'].value[0].value['c'].raw, '0x746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374')
st.equals(locals['structArray'].value[0].value['c'].value, 'test_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_test')
st.equals(locals['structArray'].value[0].value['d'].value, '3')
st.equals(locals['structArray'].value[0].value['e'].value, true)
st.equals(locals['structArray'].value[1].value['a'].value, 'item1 a')
st.equals(locals['structArray'].value[1].value['b'].value, '20')
st.equals(locals['structArray'].value[1].value['c'].value, 'item1 c')
st.equals(locals['structArray'].value[1].value['d'].value, '-45')
st.equals(locals['structArray'].value[1].value['e'].value, false)
st.equals(locals['structArray'].value[2].value['a'].value, 'item2 a')
st.equals(locals['structArray'].value[2].value['b'].value, '200')
st.equals(locals['structArray'].value[2].value['c'].value, 'item2 c')
st.equals(locals['structArray'].value[2].value['d'].value, '-450')
st.equals(locals['structArray'].value[2].value['e'].value, true)
st.equals(locals['arrayStruct'].value.a.value[0].value, 'string')
st.equals(locals['arrayStruct'].value.b.value[0].value, '34')
st.equals(locals['arrayStruct'].value.b.value[1].value, '-23')
st.equals(locals['arrayStruct'].value.b.value[2].value, '-3')
st.equals(locals['arrayStruct'].value.c.value, 'three')
st.equals(Object.keys(locals).length, 8)
} catch (e) {
st.fail(e.message)
}
const txHash = data.hash
vm.web3.eth.getTransaction(txHash, function (error, tx) {
if (error) {
return st.fail(error)
}
tx.to = contractCreationToken('0')
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)
})
helper.decodeLocals(st, 7, traceManager, callTree, function (locals) {
try {
st.equals(0, 0)
// st.equals(Object.keys(locals).length, 0)
} catch (e) {
st.fail(e.message)
}
cb()
callTree.event.register('callTreeReady', (scopes, scopeStarts) => {
helper.decodeLocals(st, 1622, traceManager, callTree, function (locals) {
try {
st.equals(locals['bytesSimple'].length, '0x14')
st.equals(locals['bytesSimple'].value, '0x746573745f7375706572')
st.equals(locals['e'].value['a'].value, 'test')
st.equals(locals['e'].value['a'].length, '0x8')
st.equals(locals['e'].value['a'].raw, '0x74657374')
st.equals(locals['e'].value['b'].value, '5')
st.equals(locals['e'].value['c'].length, '0x220')
st.equals(locals['e'].value['c'].raw, '0x746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374')
st.equals(locals['e'].value['c'].value, 'test_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_test')
st.equals(locals['e'].value['d'].value, '3')
st.equals(locals['f'].length, '0x1b8')
st.equals(locals['f'].raw, '0x746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f')
st.equals(locals['f'].value, 'test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_')
st.equals(locals['e'].value['e'].value, true)
st.equals(locals['simpleArray'].value[0].value, '45')
st.equals(locals['simpleArray'].value[1].value, '324324')
st.equals(locals['simpleArray'].value[2].value, '-333')
st.equals(locals['simpleArray'].value[3].value, '5656')
st.equals(locals['simpleArray'].value[4].value, '-1111')
st.equals(locals['stringArray'].value[0].value, 'long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_')
st.equals(locals['stringArray'].value[1].value, 'two')
st.equals(locals['stringArray'].value[2].value, 'three')
st.equals(locals['dynArray'].value[0].value[0].value, '3423423532')
st.equals(locals['dynArray'].value[1].value[0].value, '-342343323532')
st.equals(locals['dynArray'].value[1].value[1].value, '23432')
st.equals(locals['dynArray'].value[2].value[0].value, '-432432')
st.equals(locals['dynArray'].value[2].value[1].value, '3423423532')
st.equals(locals['dynArray'].value[2].value[2].value, '-432432')
st.equals(locals['structArray'].value[0].value['a'].value, 'test')
st.equals(locals['structArray'].value[0].value['a'].length, '0x8')
st.equals(locals['structArray'].value[0].value['a'].raw, '0x74657374')
st.equals(locals['structArray'].value[0].value['b'].value, '5')
st.equals(locals['structArray'].value[0].value['c'].length, '0x220')
st.equals(locals['structArray'].value[0].value['c'].raw, '0x746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374')
st.equals(locals['structArray'].value[0].value['c'].value, 'test_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_test')
st.equals(locals['structArray'].value[0].value['d'].value, '3')
st.equals(locals['structArray'].value[0].value['e'].value, true)
st.equals(locals['structArray'].value[1].value['a'].value, 'item1 a')
st.equals(locals['structArray'].value[1].value['b'].value, '20')
st.equals(locals['structArray'].value[1].value['c'].value, 'item1 c')
st.equals(locals['structArray'].value[1].value['d'].value, '-45')
st.equals(locals['structArray'].value[1].value['e'].value, false)
st.equals(locals['structArray'].value[2].value['a'].value, 'item2 a')
st.equals(locals['structArray'].value[2].value['b'].value, '200')
st.equals(locals['structArray'].value[2].value['c'].value, 'item2 c')
st.equals(locals['structArray'].value[2].value['d'].value, '-450')
st.equals(locals['structArray'].value[2].value['e'].value, true)
st.equals(locals['arrayStruct'].value.a.value[0].value, 'string')
st.equals(locals['arrayStruct'].value.b.value[0].value, '34')
st.equals(locals['arrayStruct'].value.b.value[1].value, '-23')
st.equals(locals['arrayStruct'].value.b.value[2].value, '-3')
st.equals(locals['arrayStruct'].value.c.value, 'three')
st.equals(Object.keys(locals).length, 8)
} catch (e) {
st.fail(e.message)
}
})
helper.decodeLocals(st, 7, traceManager, callTree, function (locals) {
try {
st.equals(0, 0)
// st.equals(Object.keys(locals).length, 0)
} catch (e) {
st.fail(e.message)
}
resolve({})
})
})
traceManager.resolveTrace(tx).then(() => {
debuggerEvent.trigger('newTraceLoaded', [traceManager.trace])
}).catch((error) => {
st.fail(error)
})
})
traceManager.resolveTrace(tx).then(() => {
debuggerEvent.trigger('newTraceLoaded', [traceManager.trace])
}).catch((error) => {
st.fail(error)
})
})
})
})
}

@ -12,11 +12,12 @@ module.exports = async function testMappingStorage (st, cb) {
var vm = await initVM(st, privateKey)
var output = compile(compilerInput(mappingStorage.contract))
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) {
console.log(error)
st.end(error)
} else {
const txHash = data.hash
vm.web3.eth.getTransaction(txHash, (error, tx) => {
if (error) {
console.log(error)
@ -31,12 +32,12 @@ module.exports = async function testMappingStorage (st, cb) {
function testMapping (st, vm, privateKey, contractAddress, output, cb) {
sendTx(vm, {nonce: 1, privateKey: privateKey}, contractAddress, 0, '2fd0a83a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001074686973206973206120737472696e6700000000000000000000000000000000',
function (error, txHash) {
function (error, data) {
if (error) {
console.log(error)
st.end(error)
} else {
console.log(txHash)
const txHash = data.hash
vm.web3.eth.getTransaction(txHash, (error, tx) => {
if (error) {
console.log(error)

@ -6,36 +6,42 @@ import { vm as remixlibVM } from '@remix-project/remix-lib'
import VM from '@ethereumjs/vm'
import Common from '@ethereumjs/common'
export function sendTx (vm, from, to, value, data, cb) {
var tx = new Tx({
nonce: new BN(from.nonce++),
gasPrice: new BN(1),
gasLimit: new BN(3000000, 10),
to: to,
value: new BN(value, 10),
data: Buffer.from(data, 'hex')
})
tx = tx.sign(from.privateKey)
var block = Block.fromBlockData({
header: {
timestamp: new Date().getTime() / 1000 | 0,
number: 0
}
}) // still using default common
try {
vm.runTx({block: block, tx: tx, skipBalance: true, skipNonce: true}).then(function (result) {
setTimeout(() => {
cb(null, bufferToHex(tx.hash()))
}, 500)
}).catch((error) => {
console.error(error)
cb(error)
export function sendTx (vm, from, to, value, data, cb?) {
cb = cb || (() => {})
return new Promise ((resolve, reject) => {
var tx = new Tx({
nonce: new BN(from.nonce++),
gasPrice: new BN(1),
gasLimit: new BN(3000000, 10),
to: to,
value: new BN(value, 10),
data: Buffer.from(data, 'hex')
})
} catch (e) {
console.error(e)
}
tx = tx.sign(from.privateKey)
var block = Block.fromBlockData({
header: {
timestamp: new Date().getTime() / 1000 | 0,
number: 0
}
}) // still using default common
try {
vm.runTx({block: block, tx: tx, skipBalance: true, skipNonce: true}).then(function (result) {
setTimeout(() => {
const hash = bufferToHex(tx.hash())
cb(null, { hash, result })
resolve({ hash, result })
}, 500)
}).catch((error) => {
console.error(error)
cb(error)
reject(error)
})
} catch (e) {
console.error(e)
}
})
}
async function createVm (hardfork) {

@ -1,5 +1,6 @@
'use strict'
import { ethers } from 'ethers'
import { getFunctionFragment } from './txHelper'
/**
* deploy the given contract
@ -53,10 +54,10 @@ export function callFunction (from, to, data, value, gasLimit, funAbi, txRunner,
/**
* check if the vm has errored
*
* @param {Object} txResult - the value returned by the vm
* @param {Object} execResult - execution result given by the VM
* @return {Object} - { error: true/false, message: DOMNode }
*/
export function checkVMError (txResult) {
export function checkVMError (execResult, abi) {
const errorCode = {
OUT_OF_GAS: 'out of gas',
STACK_UNDERFLOW: 'stack underflow',
@ -74,10 +75,10 @@ export function checkVMError (txResult) {
error: false,
message: ''
}
if (!txResult.result.execResult.exceptionError) {
if (!execResult.exceptionError) {
return ret
}
const exceptionError = txResult.result.execResult.exceptionError.error || ''
const exceptionError = execResult.exceptionError.error || ''
const error = `VM error: ${exceptionError}.\n`
let msg
if (exceptionError === errorCode.INVALID_OPCODE) {
@ -87,20 +88,49 @@ export function checkVMError (txResult) {
msg = '\tThe transaction ran out of gas. Please increase the Gas Limit.\n'
ret.error = true
} else if (exceptionError === errorCode.REVERT) {
const returnData = txResult.result.execResult.returnValue
// It is the hash of Error(string)
if (returnData && (returnData.slice(0, 4).toString('hex') === '08c379a0')) {
const abiCoder = new ethers.utils.AbiCoder()
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}".`
} 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.'
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)
if (returnData && (returnDataHex === '08c379a0')) {
const abiCoder = new ethers.utils.AbiCoder()
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}".`
} 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.'
}
}
ret.error = true
} else if (exceptionError === errorCode.STATIC_STATE_CHANGE) {
msg = '\tState changes is not allowed in Static Call context\n'
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
}

@ -314,7 +314,7 @@ export function deployLibrary (libraryName, libraryShortName, library, contracts
if (err) {
return callback(err)
}
const address = txResult.result.createdAddress || txResult.result.contractAddress
const address = txResult.receipt.contractAddress
library.address = address
callback(err, address)
})
@ -357,14 +357,12 @@ export function decodeResponse (response, fnabi) {
if (fnabi.outputs && fnabi.outputs.length > 0) {
try {
let i
const outputTypes = []
for (i = 0; i < fnabi.outputs.length; i++) {
const type = fnabi.outputs[i].type
outputTypes.push(type.indexOf('tuple') === 0 ? makeFullTypeDefinition(fnabi.outputs[i]) : type)
}
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
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
// decode data
const abiCoder = new ethers.utils.AbiCoder()
const decodedObj = abiCoder.decode(outputTypes, response)

@ -38,6 +38,11 @@ export function encodeFunctionId (funABI) {
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) {
// Check if function is constant (introduced with Solidity 0.6.0)
const isConstant = ({ stateMutability }) => stateMutability === 'view' || stateMutability === 'pure'

@ -1,20 +1,19 @@
'use strict'
import { each } from 'async'
import { ethers } from 'ethers'
import { toBuffer } from 'ethereumjs-util'
import { toBuffer, addHexPrefix } from 'ethereumjs-util'
import { EventManager } from '../eventManager'
import { compareByteCode } from '../util'
import { ExecutionContext } from './execution-context'
import { decodeResponse } from './txFormat'
import { getFunction, getReceiveInterface, getConstructorInterface, visitContracts, makeFullTypeDefinition } from './txHelper'
function addExecutionCosts (txResult, tx) {
if (txResult && txResult.result) {
if (txResult.result.execResult) {
tx.returnValue = txResult.result.execResult.returnValue
if (txResult.result.execResult.gasUsed) tx.executionCost = txResult.result.execResult.gasUsed.toString(10)
function addExecutionCosts (txResult, tx, execResult) {
if (txResult) {
if (execResult) {
tx.returnValue = execResult.returnValue
if (execResult.gasUsed) tx.executionCost = execResult.gasUsed.toString(10)
}
if (txResult.result.gasUsed) tx.transactionCost = txResult.result.gasUsed.toString(10)
if (txResult.receipt && txResult.receipt.gasUsed) tx.transactionCost = txResult.receipt.gasUsed.toString(10)
}
}
@ -40,7 +39,7 @@ export class TxListener {
constructor (opt, executionContext) {
this.event = new EventManager()
// has a default for now for backwards compatability
this.executionContext = executionContext || new ExecutionContext()
this.executionContext = executionContext
this._api = opt.api
this._resolvedTransactions = {}
this._resolvedContracts = {}
@ -55,7 +54,7 @@ export class TxListener {
}
})
opt.event.udapp.register('callExecuted', (error, from, to, data, lookupOnly, txResult) => {
opt.event.udapp.register('callExecuted', async (error, from, to, data, lookupOnly, txResult) => {
if (error) return
// we go for that case if
// in VM mode
@ -63,17 +62,25 @@ export class TxListener {
if (!this._isListening) return // we don't listen
if (this._loopId && this.executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network
let returnValue
let execResult
if (this.executionContext.isVM()) {
execResult = await this.executionContext.web3().eth.getExecutionResultFromSimulator(txResult.transactionHash)
returnValue = execResult.returnValue
} else {
returnValue = toBuffer(addHexPrefix(txResult.result))
}
const call = {
from: from,
to: to,
input: data,
hash: txResult.transactionHash ? txResult.transactionHash : 'call' + (from || '') + to + data,
isCall: true,
returnValue: this.executionContext.isVM() ? txResult.result.execResult.returnValue : toBuffer(txResult.result),
returnValue,
envMode: this.executionContext.getProvider()
}
addExecutionCosts(txResult, call)
addExecutionCosts(txResult, call, execResult)
this._resolveTx(call, call, (error, resolvedData) => {
if (!error) {
this.event.trigger('newCall', [call])
@ -89,12 +96,17 @@ export class TxListener {
// in web3 mode && listen remix txs only
if (!this._isListening) return // we don't listen
if (this._loopId && this.executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network
this.executionContext.web3().eth.getTransaction(txResult.transactionHash, (error, tx) => {
this.executionContext.web3().eth.getTransaction(txResult.transactionHash, async (error, tx) => {
if (error) return console.log(error)
addExecutionCosts(txResult, tx)
let execResult
if (this.executionContext.isVM()) {
execResult = await this.executionContext.web3().eth.getExecutionResultFromSimulator(txResult.transactionHash)
}
addExecutionCosts(txResult, tx, execResult)
tx.envMode = this.executionContext.getProvider()
tx.status = txResult.result.status // 0x0 or 0x1
tx.status = txResult.receipt.status // 0x0 or 0x1
this._resolve([tx], () => {
})
})
@ -346,7 +358,7 @@ export class TxListener {
}
_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
const inputTypes = []

@ -1,91 +1,26 @@
'use strict'
import { Transaction } from '@ethereumjs/tx'
import { Block } from '@ethereumjs/block'
import { BN, bufferToHex, Address } from 'ethereumjs-util'
import { ExecutionContext } from './execution-context'
import { EventManager } from '../eventManager'
export class TxRunner {
event
executionContext
_api
blockNumber
runAsync
pendingTxs
vmaccounts
queusTxs
blocks
commonContext
constructor (vmaccounts, api, executionContext) {
opt
internalRunner
constructor (internalRunner, opt) {
this.opt = opt || {}
this.internalRunner = internalRunner
this.event = new EventManager()
// has a default for now for backwards compatability
this.executionContext = executionContext || new ExecutionContext()
this.commonContext = this.executionContext.vmObject().common
this._api = api
this.blockNumber = 0
this.runAsync = true
if (this.executionContext.isVM()) {
// this.blockNumber = 1150000 // The VM is running in Homestead mode, which started at this block.
this.blockNumber = 0 // The VM is running in Homestead mode, which started at this block.
this.runAsync = false // We have to run like this cause the VM Event Manager does not support running multiple txs at the same time.
}
this.runAsync = this.opt.runAsync || true // We have to run like this cause the VM Event Manager does not support running multiple txs at the same time.
this.pendingTxs = {}
this.vmaccounts = vmaccounts
this.queusTxs = []
this.blocks = []
}
rawRun (args, confirmationCb, gasEstimationForceSend, promptCb, cb) {
let timestamp = Date.now()
if (args.timestamp) {
timestamp = args.timestamp
}
run(this, args, timestamp, confirmationCb, gasEstimationForceSend, promptCb, cb)
}
_executeTx (tx, gasPrice, api, promptCb, callback) {
if (gasPrice) tx.gasPrice = this.executionContext.web3().utils.toHex(gasPrice)
if (api.personalMode()) {
promptCb(
(value) => {
this._sendTransaction(this.executionContext.web3().personal.sendTransaction, tx, value, callback)
},
() => {
return callback('Canceled by user.')
}
)
} else {
this._sendTransaction(this.executionContext.web3().eth.sendTransaction, tx, null, callback)
}
}
_sendTransaction (sendTx, tx, pass, callback) {
const cb = (err, resp) => {
if (err) {
return callback(err, resp)
}
this.event.trigger('transactionBroadcasted', [resp])
var listenOnResponse = () => {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
const result = await tryTillReceiptAvailable(resp, this.executionContext)
tx = await tryTillTxAvailable(resp, this.executionContext)
resolve({
result,
tx,
transactionHash: result ? result['transactionHash'] : null
})
})
}
listenOnResponse().then((txData) => { callback(null, txData) }).catch((error) => { callback(error) })
}
const args = pass !== null ? [tx, pass, cb] : [tx, cb]
try {
sendTx.apply({}, args)
} catch (e) {
return callback(`Send transaction failed: ${e.message} . if you use an injected provider, please check it is properly unlocked. `)
}
run(this, args, args.timestamp || Date.now(), confirmationCb, gasEstimationForceSend, promptCb, cb)
}
execute (args, confirmationCb, gasEstimationForceSend, promptCb, callback) {
@ -93,175 +28,10 @@ export class TxRunner {
if (data.slice(0, 2) !== '0x') {
data = '0x' + data
}
if (!this.executionContext.isVM()) {
return this.runInNode(args.from, args.to, data, args.value, args.gasLimit, args.useCall, confirmationCb, gasEstimationForceSend, promptCb, callback)
}
try {
this.runInVm(args.from, args.to, data, args.value, args.gasLimit, args.useCall, args.timestamp, callback)
} catch (e) {
callback(e, null)
}
}
runInVm (from, to, data, value, gasLimit, useCall, timestamp, callback) {
const self = this
const account = self.vmaccounts[from]
if (!account) {
return callback('Invalid account selected')
}
if (Number.isInteger(gasLimit)) {
gasLimit = '0x' + gasLimit.toString(16)
}
this.executionContext.vm().stateManager.getAccount(Address.fromString(from)).then((res) => {
// See https://github.com/ethereumjs/ethereumjs-tx/blob/master/docs/classes/transaction.md#constructor
// for initialization fields and their types
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)
}
})
})
this.internalRunner.execute(args, confirmationCb, gasEstimationForceSend, promptCb, callback)
}
}
async function tryTillReceiptAvailable (txhash, executionContext) {
return new Promise((resolve, reject) => {
executionContext.web3().eth.getTransactionReceipt(txhash, async (err, receipt) => {
if (err || !receipt) {
// Try again with a bit of delay if error or if result still null
await pause()
return resolve(await tryTillReceiptAvailable(txhash, executionContext))
}
return resolve(receipt)
})
})
}
async function tryTillTxAvailable (txhash, executionContext) {
return new Promise((resolve, reject) => {
executionContext.web3().eth.getTransaction(txhash, async (err, tx) => {
if (err || !tx) {
// Try again with a bit of delay if error or if result still null
await pause()
return resolve(await tryTillTxAvailable(txhash, executionContext))
}
return resolve(tx)
})
})
}
async function pause () { return new Promise((resolve, reject) => { setTimeout(resolve, 500) }) }
function run (self, tx, stamp, confirmationCb, gasEstimationForceSend = null, promptCb = null, callback = null) {
if (!self.runAsync && Object.keys(self.pendingTxs).length) {
return self.queusTxs.push({ tx, stamp, callback })

@ -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,
So we need to normalize the values to prefixed hex strings
*/
export function resultToRemixTx (txResult) {
const { result, transactionHash } = txResult
const { status, execResult, gasUsed, createdAddress, contractAddress } = result
export function resultToRemixTx (txResult, execResult) {
const { receipt, transactionHash, result } = txResult
const { status, gasUsed, contractAddress } = receipt
let returnValue, errorMessage
if (isHexString(result)) {
@ -38,6 +38,6 @@ export function resultToRemixTx (txResult) {
gasUsed: convertToPrefixedHex(gasUsed),
error: errorMessage,
return: convertToPrefixedHex(returnValue),
createdAddress: convertToPrefixedHex(createdAddress || contractAddress)
createdAddress: convertToPrefixedHex(contractAddress)
}
}

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

@ -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'
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
@ -142,7 +142,7 @@ export function buildCallPath (index, rootCall) {
*/
// eslint-disable-next-line camelcase
export function sha3_256 (value) {
value = toBuffer(value)
value = toBuffer(addHexPrefix(value))
const retInBuffer: Buffer = keccak(setLengthLeft(value, 32))
return bufferToHex(retInBuffer)
}

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

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

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

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

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

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

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

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

@ -1,8 +1,8 @@
export class Debug {
executionContext
vmContext
constructor (executionContext) {
this.executionContext = executionContext
constructor (vmContext) {
this.vmContext = vmContext
}
methods () {
@ -14,15 +14,15 @@ export class Debug {
}
debug_traceTransaction (payload, cb) {
this.executionContext.web3().debug.traceTransaction(payload.params[0], {}, cb)
this.vmContext.web3().debug.traceTransaction(payload.params[0], {}, cb)
}
debug_preimage (payload, cb) {
this.executionContext.web3().debug.preimage(payload.params[0], cb)
this.vmContext.web3().debug.preimage(payload.params[0], cb)
}
debug_storageRangeAt (payload, cb) {
this.executionContext.web3().debug.storageRangeAt(
this.vmContext.web3().debug.storageRangeAt(
payload.params[0],
payload.params[1],
payload.params[2],

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

@ -1,17 +1,48 @@
import Web3 from 'web3'
import { toChecksumAddress, BN, Address } from 'ethereumjs-util'
import { processTx } from './txProcess'
import { execution } from '@remix-project/remix-lib'
const TxRunnerVM = execution.TxRunnerVM
const TxRunner = execution.TxRunner
export class Transactions {
executionContext
vmContext
accounts
tags
txRunnerVMInstance
txRunnerInstance
constructor (executionContext) {
this.executionContext = executionContext
constructor (vmContext) {
this.vmContext = vmContext
this.tags = {}
}
init (accounts) {
this.accounts = accounts
const api = {
logMessage: (msg) => {
},
logHtmlMessage: (msg) => {
},
config: {
getUnpersistedProperty: (key) => {
return true
},
get: () => {
return true
}
},
detectNetwork: (cb) => {
cb()
},
personalMode: () => {
return false
}
}
this.txRunnerVMInstance = new TxRunnerVM(accounts, api, _ => this.vmContext.vmObject())
this.txRunnerInstance = new TxRunner(this.txRunnerVMInstance, { runAsync: false })
this.txRunnerInstance.vmaccounts = accounts
}
methods () {
@ -24,7 +55,9 @@ export class Transactions {
eth_getTransactionCount: this.eth_getTransactionCount.bind(this),
eth_getTransactionByHash: this.eth_getTransactionByHash.bind(this),
eth_getTransactionByBlockHashAndIndex: this.eth_getTransactionByBlockHashAndIndex.bind(this),
eth_getTransactionByBlockNumberAndIndex: this.eth_getTransactionByBlockNumberAndIndex.bind(this)
eth_getTransactionByBlockNumberAndIndex: this.eth_getTransactionByBlockNumberAndIndex.bind(this),
eth_getExecutionResultFromSimulator: this.eth_getExecutionResultFromSimulator.bind(this),
eth_getHashFromTagBySimulator: this.eth_getHashFromTagBySimulator.bind(this)
}
}
@ -33,16 +66,30 @@ export class Transactions {
if (payload.params && payload.params.length > 0 && payload.params[0].from) {
payload.params[0].from = toChecksumAddress(payload.params[0].from)
}
processTx(this.executionContext, this.accounts, payload, false, cb)
processTx(this.txRunnerInstance, payload, false, (error, result) => {
if (!error && result) {
this.vmContext.addBlock(result.block)
const hash = '0x' + result.tx.hash().toString('hex')
this.vmContext.trackTx(hash, result.block)
this.vmContext.trackExecResult(hash, result.result.execResult)
return cb(null, result.transactionHash)
}
cb(error)
})
}
eth_getExecutionResultFromSimulator (payload, cb) {
const txHash = payload.params[0]
cb(null, this.vmContext.exeResults[txHash])
}
eth_getTransactionReceipt (payload, cb) {
this.executionContext.web3().eth.getTransactionReceipt(payload.params[0], (error, receipt) => {
this.vmContext.web3().eth.getTransactionReceipt(payload.params[0], (error, receipt) => {
if (error) {
return cb(error)
}
const txBlock = this.executionContext.txs[receipt.hash]
const txBlock = this.vmContext.txs[receipt.hash]
const r: Record <string, unknown> = {
transactionHash: receipt.hash,
@ -72,7 +119,7 @@ export class Transactions {
eth_getCode (payload, cb) {
const address = payload.params[0]
this.executionContext.web3().eth.getCode(address, (error, result) => {
this.vmContext.web3().eth.getCode(address, (error, result) => {
if (error) {
console.dir('error getting code')
console.dir(error)
@ -92,13 +139,31 @@ export class Transactions {
payload.params[0].value = undefined
processTx(this.executionContext, this.accounts, payload, true, cb)
const tag = payload.params[0].timestamp // e2e reference
processTx(this.txRunnerInstance, payload, true, (error, result) => {
if (!error && result) {
this.vmContext.addBlock(result.block)
const hash = '0x' + result.tx.hash().toString('hex')
this.vmContext.trackTx(hash, result.block)
this.vmContext.trackExecResult(hash, result.result.execResult)
this.tags[tag] = result.transactionHash
// calls are not supposed to return a transaction hash. we do this for keeping track of it and allowing debugging calls.
const returnValue = `0x${result.result.execResult.returnValue.toString('hex') || '0'}`
return cb(null, returnValue)
}
cb(error)
})
}
eth_getHashFromTagBySimulator (payload, cb) {
return cb(null, this.tags[payload.params[0]])
}
eth_getTransactionCount (payload, cb) {
const address = payload.params[0]
this.executionContext.vm().stateManager.getAccount(Address.fromString(address)).then((account) => {
this.vmContext.vm().stateManager.getAccount(Address.fromString(address)).then((account) => {
const nonce = new BN(account.nonce).toString(10)
cb(null, nonce)
}).catch((error) => {
@ -109,12 +174,12 @@ export class Transactions {
eth_getTransactionByHash (payload, cb) {
const address = payload.params[0]
this.executionContext.web3().eth.getTransactionReceipt(address, (error, receipt) => {
this.vmContext.web3().eth.getTransactionReceipt(address, (error, receipt) => {
if (error) {
return cb(error)
}
const txBlock = this.executionContext.txs[receipt.transactionHash]
const txBlock = this.vmContext.txs[receipt.transactionHash]
// TODO: params to add later
const r: Record<string, unknown> = {
@ -154,10 +219,10 @@ export class Transactions {
eth_getTransactionByBlockHashAndIndex (payload, cb) {
const txIndex = payload.params[1]
const txBlock = this.executionContext.blocks[payload.params[0]]
const txBlock = this.vmContext.blocks[payload.params[0]]
const txHash = '0x' + txBlock.transactions[Web3.utils.toDecimal(txIndex)].hash().toString('hex')
this.executionContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => {
this.vmContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => {
if (error) {
return cb(error)
}
@ -196,10 +261,10 @@ export class Transactions {
eth_getTransactionByBlockNumberAndIndex (payload, cb) {
const txIndex = payload.params[1]
const txBlock = this.executionContext.blocks[payload.params[0]]
const txBlock = this.vmContext.blocks[payload.params[0]]
const txHash = '0x' + txBlock.transactions[Web3.utils.toDecimal(txIndex)].hash().toString('hex')
this.executionContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => {
this.vmContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => {
if (error) {
return cb(error)
}

@ -1,15 +1,12 @@
import { execution } from '@remix-project/remix-lib'
const TxExecution = execution.txExecution
const TxRunner = execution.txRunner
function runCall (payload, from, to, data, value, gasLimit, txRunner, callbacks, callback) {
const finalCallback = function (err, result) {
if (err) {
return callback(err)
}
const returnValue = result.result.execResult.returnValue.toString('hex')
const toReturn = `0x${returnValue || '0'}`
return callback(null, toReturn)
return callback(null, result)
}
TxExecution.callFunction(from, to, data, value, gasLimit, { constant: true }, txRunner, callbacks, finalCallback)
@ -20,7 +17,7 @@ function runTx (payload, from, to, data, value, gasLimit, txRunner, callbacks, c
if (err) {
return callback(err)
}
callback(null, result.transactionHash)
callback(null, result)
}
TxExecution.callFunction(from, to, data, value, gasLimit, { constant: false }, txRunner, callbacks, finalCallback)
@ -31,43 +28,13 @@ function createContract (payload, from, data, value, gasLimit, txRunner, callbac
if (err) {
return callback(err)
}
callback(null, result.transactionHash)
callback(null, result)
}
TxExecution.createContract(from, data, value, gasLimit, txRunner, callbacks, finalCallback)
}
let txRunnerInstance
export function processTx (executionContext, accounts, payload, isCall, callback) {
const api = {
logMessage: (msg) => {
},
logHtmlMessage: (msg) => {
},
config: {
getUnpersistedProperty: (key) => {
return true
},
get: () => {
return true
}
},
detectNetwork: (cb) => {
cb()
},
personalMode: () => {
return false
}
}
executionContext.init(api.config)
// let txRunner = new TxRunner(accounts, api)
if (!txRunnerInstance) {
txRunnerInstance = new TxRunner(accounts, api, executionContext)
}
txRunnerInstance.vmaccounts = accounts
export function processTx (txRunnerInstance, payload, isCall, callback) {
let { from, to, data, value, gas } = payload.params[0]
gas = gas || 3000000

@ -1,5 +1,4 @@
import { Blocks } from './methods/blocks'
import { execution } from '@remix-project/remix-lib'
import { info } from './utils/logs'
import merge from 'merge'
@ -11,11 +10,11 @@ import { methods as netMethods } from './methods/net'
import { Transactions } from './methods/transactions'
import { Debug } from './methods/debug'
import { generateBlock } from './genesis'
const { executionContext } = execution
import { VMContext } from './vm-context'
export class Provider {
options: Record<string, unknown>
executionContext
vmContext
Accounts
Transactions
methods
@ -26,23 +25,23 @@ export class Provider {
this.options = options
this.host = host
this.connected = true
// TODO: init executionContext here
this.executionContext = executionContext
this.Accounts = new Accounts(this.executionContext)
this.Transactions = new Transactions(this.executionContext)
this.vmContext = new VMContext()
this.Accounts = new Accounts(this.vmContext)
this.Transactions = new Transactions(this.vmContext)
this.methods = {}
this.methods = merge(this.methods, this.Accounts.methods())
this.methods = merge(this.methods, (new Blocks(this.executionContext, options)).methods())
this.methods = merge(this.methods, (new Blocks(this.vmContext, options)).methods())
this.methods = merge(this.methods, miscMethods())
this.methods = merge(this.methods, (new Filters(this.executionContext)).methods())
this.methods = merge(this.methods, (new Filters(this.vmContext)).methods())
this.methods = merge(this.methods, netMethods())
this.methods = merge(this.methods, this.Transactions.methods())
this.methods = merge(this.methods, (new Debug(this.executionContext)).methods())
this.methods = merge(this.methods, (new Debug(this.vmContext)).methods())
}
async init () {
await generateBlock(this.executionContext)
await generateBlock(this.vmContext)
await this.Accounts.resetAccounts()
this.Transactions.init(this.Accounts.accounts)
}
@ -87,6 +86,39 @@ export class Provider {
};
on (type, cb) {
this.executionContext.logsManager.addListener(type, cb)
this.vmContext.logsManager.addListener(type, cb)
}
}
export function extend (web3) {
if (!web3.extend) {
return
}
// DEBUG
const methods = []
if (!(web3.eth && web3.eth.getExecutionResultFromSimulator)) {
methods.push(new web3.extend.Method({
name: 'getExecutionResultFromSimulator',
call: 'eth_getExecutionResultFromSimulator',
inputFormatter: [null],
params: 1
}))
}
if (!(web3.eth && web3.eth.getHashFromTagBySimulator)) {
methods.push(new web3.extend.Method({
name: 'getHashFromTagBySimulator',
call: 'eth_getHashFromTagBySimulator',
inputFormatter: [null],
params: 1
}))
}
if (methods.length > 0) {
web3.extend({
property: 'eth',
methods: methods,
properties: []
})
}
}

@ -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": {
"@remix-project/remix-lib": "^0.4.34",
"@remix-project/remix-lib": "../remix-lib",
"eslint-scope": "^5.0.0",
"@ethereumjs/vm": "^5.3.2",
"@ethereumjs/block": "^3.2.1",

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

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

@ -79,7 +79,7 @@ export function deployAll (compileResult: compilationInterface, web3: Web3, with
contracts[contractName] = contractObject
contracts[contractName].filename = filename
callback(null, { result: { createdAddress: receipt.contractAddress } }) // TODO this will only work with JavaScriptV VM
callback(null, { receipt: { contractAddress: receipt.contractAddress } }) // TODO this will only work with JavaScriptV VM
}).on('error', function (err) {
console.error(err)
callback(err)

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

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

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

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

@ -9,6 +9,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
/* eslint-disable-next-line */
import './debugger-ui.css'
const helper = require('../../../../../apps/remix-ide/src/lib/helper')
const _paq = (window as any)._paq = (window as any)._paq || []
export const DebuggerUI = (props: DebuggerUIProps) => {
const debuggerModule = props.debuggerAPI
@ -167,7 +168,9 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
const web3 = await debuggerModule.getDebugWeb3()
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 => {
return {
...prevState,

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

@ -4,7 +4,7 @@ import { action, FileExplorerContextMenuProps } from './types'
import './css/file-explorer-context-menu.css'
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)
useEffect(() => {
contextMenuRef.current.focus()
@ -21,43 +21,24 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
}
}, [pageX, pageY])
const filterItem = (item: action) => {
/**
* 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'
* for example : 'downloadAsZip' with type ['file','folder','multi'] will work on files and folders when multiple are selected
**/
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
const itemMatchesCondition = (item: action) => {
if (item.type && Array.isArray(item.type) && (item.type.findIndex(name => name === type) !== -1)) return true
else if (item.path && Array.isArray(item.path) && (item.path.findIndex(key => key === path) !== -1)) return true
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
else return false
}
const getPath = () => {
const nonRootFocus = focus.filter((el) => { return !(el.key === '' && el.type === 'folder') })
if (nonRootFocus.length > 1) {
return nonRootFocus.map((element) => { return element.key })
if (focus.length > 1) {
return focus.map((element) => element.key)
} else {
return path
}
}
const menu = () => {
return actions.filter(item => {
return filterItem(item)
}).map((item, index) => {
return actions.filter(item => itemMatchesCondition(item)).map((item, index) => {
return <li
id={`menuitem${item.name.toLowerCase()}`}
key={index}
@ -78,11 +59,26 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
deletePath(getPath())
break
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
case 'Run':
runScript(path)
break
case 'Copy':
copy(path, type)
break
case 'Paste':
paste(path, type)
break
case 'Delete All':
deletePath(getPath())
break
default:
emit && emit(item.id, getPath())
break

@ -6,7 +6,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import Gists from 'gists'
import { FileExplorerMenu } from './file-explorer-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 { fetchDirectory, init, resolveDirectory, addInputField, removeInputField } from './actions/fileSystem'
import * as helper from '../../../../../apps/remix-ide/src/lib/helper'
@ -19,8 +19,10 @@ const queryParams = new QueryParams()
export const FileExplorer = (props: FileExplorerProps) => {
const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads } = props
const [state, setState] = useState({
focusElement: [{ key: '', type: 'folder' }],
focusPath: null,
focusElement: [{
key: '',
type: 'folder'
}],
files: [],
fileManager: null,
ctrlKey: false,
@ -28,45 +30,83 @@ export const FileExplorer = (props: FileExplorerProps) => {
actions: [{
id: 'newFile',
name: 'New File',
type: ['folder'],
type: ['folder', 'gist'],
path: [],
extension: [],
pattern: []
pattern: [],
multiselect: false
}, {
id: 'newFolder',
name: 'New Folder',
type: ['folder'],
type: ['folder', 'gist'],
path: [],
extension: [],
pattern: []
pattern: [],
multiselect: false
}, {
id: 'rename',
name: 'Rename',
type: ['file', 'folder'],
path: [],
extension: [],
pattern: []
pattern: [],
multiselect: false
}, {
id: 'delete',
name: 'Delete',
type: ['file', 'folder', 'multi'],
type: ['file', 'folder', 'gist'],
path: [],
extension: [],
pattern: []
pattern: [],
multiselect: false
}, {
id: 'run',
name: 'Run',
type: [],
path: [],
extension: ['.js'],
pattern: [],
multiselect: false
}, {
id: 'pushChangesToGist',
name: 'Push changes to gist',
type: [],
type: ['gist'],
path: [],
extension: [],
pattern: ['^browser/gists/([0-9]|[a-z])*$']
pattern: [],
multiselect: false
}, {
id: 'run',
name: 'Run',
type: [],
id: 'publishFolderToGist',
name: 'Publish folder to gist',
type: ['folder'],
path: [],
extension: ['.js'],
pattern: []
extension: [],
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: {
element: null,
@ -85,30 +125,26 @@ export const FileExplorer = (props: FileExplorerProps) => {
hide: true,
title: '',
message: '',
children: <></>,
ok: {
label: '',
fn: () => {}
},
cancel: {
label: '',
fn: () => {}
},
okLabel: '',
okFn: () => {},
cancelLabel: '',
cancelFn: () => {},
handleHide: null
},
modals: [],
toasterMsg: '',
mouseOverElement: null,
showContextMenu: false
showContextMenu: false,
reservedKeywords: [name, 'gist-'],
copyElement: []
})
const [canPaste, setCanPaste] = useState(false)
const [fileSystem, dispatch] = useReducer(fileSystemReducer, fileSystemInitialState)
const editRef = useRef(null)
useEffect(() => {
if (props.filesProvider) {
init(props.filesProvider, props.name, props.plugin, props.registry)(dispatch)
}
}, [props.filesProvider, props.name])
init(props.filesProvider, props.name, props.plugin, props.registry)(dispatch)
}, [])
useEffect(() => {
const provider = fileSystem.provider.provider
@ -120,13 +156,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
useEffect(() => {
if (fileSystem.notification.message) {
modal(fileSystem.notification.title, fileSystem.notification.message, {
label: fileSystem.notification.labelOk,
fn: fileSystem.notification.actionOk
}, {
label: fileSystem.notification.labelCancel,
fn: fileSystem.notification.actionCancel
})
modal(fileSystem.notification.title, fileSystem.notification.message, fileSystem.notification.labelOk, fileSystem.notification.actionOk, fileSystem.notification.labelCancel, fileSystem.notification.actionCancel)
}
}, [fileSystem.notification.message])
@ -169,12 +199,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
useEffect(() => {
if (contextMenuItems) {
setState(prevState => {
// filter duplicate items
const items = contextMenuItems.filter(({ name }) => prevState.actions.findIndex(action => action.name === name) === -1)
return { ...prevState, actions: [...prevState.actions, ...items] }
})
addMenuItems(contextMenuItems)
}
}, [contextMenuItems])
@ -199,10 +224,11 @@ export const FileExplorer = (props: FileExplorerProps) => {
hide: false,
title: prevState.modals[0].title,
message: prevState.modals[0].message,
ok: prevState.modals[0].ok,
cancel: prevState.modals[0].cancel,
handleHide: prevState.modals[0].handleHide,
children: prevState.modals[0].children
okLabel: prevState.modals[0].okLabel,
okFn: prevState.modals[0].okFn,
cancelLabel: prevState.modals[0].cancelLabel,
cancelFn: prevState.modals[0].cancelFn,
handleHide: prevState.modals[0].handleHide
}
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 keyPath = key.split('/')
@ -254,6 +313,20 @@ export const FileExplorer = (props: FileExplorerProps) => {
return keyPath.join('/')
}
const hasReservedKeyword = (content: string): boolean => {
if (state.reservedKeywords.findIndex(value => content.startsWith(value)) !== -1) return true
else return false
}
const 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 fileManager = state.fileManager
@ -272,10 +345,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
})
}
} catch (error) {
return modal('File Creation Failed', typeof error === 'string' ? error : error.message, {
label: 'Close',
fn: async () => {}
}, null)
return modal('File Creation Failed', typeof error === 'string' ? error : error.message, 'Close', async () => {})
}
}
@ -287,49 +357,36 @@ export const FileExplorer = (props: FileExplorerProps) => {
const exists = await fileManager.exists(dirName)
if (exists) {
return modal('Rename File Failed', `A file or folder ${extractNameFromKey(newFolderPath)} already exists at this location. Please choose a different name.`, {
label: 'Close',
fn: () => {}
}, null)
return modal('Rename File Failed', `A file or folder ${extractNameFromKey(newFolderPath)} already exists at this location. Please choose a different name.`, 'Close', () => {})
}
await fileManager.mkdir(dirName)
setState(prevState => {
return { ...prevState, focusElement: [{ key: newFolderPath, type: 'folder' }] }
})
} catch (e) {
return modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, {
label: 'Close',
fn: async () => {}
}, null)
return modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, 'Close', async () => {})
}
}
const deletePath = async (path: string | string[]) => {
const filesProvider = fileSystem.provider.provider
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) {
if (filesProvider.isReadOnly(p)) {
return toast('cannot delete file. ' + name + ' is a read only explorer')
}
}
modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, '', {
label: 'OK',
fn: async () => {
const fileManager = state.fileManager
for (const p of path) {
try {
await fileManager.remove(p)
} catch (e) {
const isDir = state.fileManager.isDirectory(p)
toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`)
}
modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', async () => {
const fileManager = state.fileManager
for (const p of path) {
try {
await fileManager.remove(p)
} catch (e) {
const isDir = state.fileManager.isDirectory(p)
toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`)
}
}
}, {
label: 'Cancel',
fn: () => {}
}, children)
}, 'Cancel', () => {})
}
const renamePath = async (oldPath: string, newPath: string) => {
@ -338,18 +395,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
const exists = await fileManager.exists(newPath)
if (exists) {
modal('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, {
label: 'Close',
fn: () => {}
}, null)
modal('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', () => {})
} else {
await fileManager.rename(oldPath, newPath)
}
} catch (error) {
modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, {
label: 'Close',
fn: async () => {}
}, null)
modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {})
}
}
@ -359,7 +410,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
// 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
// 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])]
setState(prevState => {
@ -372,19 +423,13 @@ export const FileExplorer = (props: FileExplorerProps) => {
fileReader.onload = async function (event) {
if (helper.checkSpecialChars(file.name)) {
modal('File Upload Failed', 'Special characters are not allowed', {
label: 'Close',
fn: async () => {}
}, null)
modal('File Upload Failed', 'Special characters are not allowed', 'Close', async () => {})
return
}
const success = await filesProvider.set(name, event.target.result)
if (!success) {
return modal('File Upload Failed', 'Failed to create file ' + name, {
label: 'Close',
fn: async () => {}
}, null)
return modal('File Upload Failed', 'Failed to create file ' + name, 'Close', async () => {})
}
const config = registry.get('config').api
const editor = registry.get('editor').api
@ -401,15 +446,9 @@ export const FileExplorer = (props: FileExplorerProps) => {
if (!exist) {
loadFile(name)
} else {
modal('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, {
label: 'OK',
fn: () => {
loadFile(name)
}
}, {
label: 'Cancel',
fn: () => {}
})
modal('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, 'OK', () => {
loadFile(name)
}, 'Cancel', () => {})
}
}).catch(error => {
if (error) console.log(error)
@ -417,42 +456,56 @@ export const FileExplorer = (props: FileExplorerProps) => {
})
}
const publishToGist = () => {
modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, {
label: 'OK',
fn: toGist
}, {
label: 'Cancel',
fn: () => {}
})
const copyFile = (src: string, dest: string) => {
const fileManager = state.fileManager
try {
fileManager.copyFile(src, dest)
} catch (error) {
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 proccedResult = function (error, data) {
if (error) {
modal('Publish to gist Failed', 'Failed to manage gist: ' + error, {
label: 'Close',
fn: async () => {}
}, null)
modal('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', () => {})
} else {
if (data.html_url) {
modal('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, {
label: 'OK',
fn: () => {
window.open(data.html_url, '_blank')
}
}, {
label: 'Cancel',
fn: () => {}
})
modal('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', () => {
window.open(data.html_url, '_blank')
}, 'Cancel', () => {})
} else {
const error = JSON.stringify(data.errors, null, '\t') || ''
const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message
modal('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, {
label: 'Close',
fn: async () => {}
}, null)
modal('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', () => {})
}
}
}
@ -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.
const folder = id ? '/gists/' + id : '/'
const folder = path || '/'
const id = type === 'gist' ? extractNameFromKey(path).split('-')[1] : null
packageFiles(filesProvider, folder, async (error, packaged) => {
if (error) {
console.log(error)
modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, {
label: 'Close',
fn: async () => {}
}, null)
modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', async () => {})
} else {
// check for token
const config = registry.get('config').api
const accessToken = config.get('settings/gist-access-token')
if (!accessToken) {
modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', {
label: 'Close',
fn: async () => {}
}, null)
modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', () => {})
} else {
const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' +
queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist='
@ -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 => {
return {
...prevState,
modals: [...prevState.modals,
{
message,
children,
title,
ok,
cancel,
okLabel,
okFn,
cancelLabel,
cancelFn,
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
if (!state.ctrlKey) {
state.fileManager.open(path)
setState(prevState => {
return { ...prevState, focusElement: [{ key: path, type: 'file' }] }
return { ...prevState, focusElement: [{ key: path, type }] }
})
} else {
if (state.focusElement.findIndex(item => item.key === path) !== -1) {
setState(prevState => {
return { ...prevState, focusElement: [...prevState.focusElement.filter(item => item.key !== path)] }
return { ...prevState, focusElement: prevState.focusElement.filter(item => item.key !== path) }
})
} else {
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.focusElement.findIndex(item => item.key === path) !== -1) {
setState(prevState => {
@ -614,7 +666,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
})
} else {
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 {
@ -628,22 +683,22 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
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
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
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,21 +739,28 @@ export const FileExplorer = (props: FileExplorerProps) => {
})
}
if (helper.checkSpecialChars(content)) {
modal('Validation Error', 'Special characters are not allowed', {
label: 'OK',
fn: () => {}
}, null)
modal('Validation Error', 'Special characters are not allowed', 'OK', () => {})
} else {
if (state.focusEdit.isNew) {
state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content))
removeInputField(parentFolder)(dispatch)
if (hasReservedKeyword(content)) {
removeInputField(parentFolder)(dispatch)
modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {})
} else {
state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content))
removeInputField(parentFolder)(dispatch)
}
} else {
const oldPath: string = state.focusEdit.element
const oldName = extractNameFromKey(oldPath)
const newPath = oldPath.replace(oldName, content)
if (hasReservedKeyword(content)) {
editRef.current.textContent = state.focusEdit.lastEdit
modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {})
} else {
const oldPath: string = state.focusEdit.element
const oldName = extractNameFromKey(oldPath)
const newPath = oldPath.replace(oldName, content)
editRef.current.textContent = extractNameFromKey(oldPath)
renamePath(oldPath, newPath)
editRef.current.textContent = extractNameFromKey(oldPath)
renamePath(oldPath, newPath)
}
}
setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
@ -708,7 +770,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
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])]
await addInputField(fileSystem.provider.provider, 'file', parentFolder)(dispatch)
@ -719,7 +781,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
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)
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) => {
return (
<div
@ -795,12 +883,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
label={label(file)}
onClick={(e) => {
e.stopPropagation()
if (state.focusEdit.element !== file.path) handleClickFolder(file.path)
if (state.focusEdit.element !== file.path) handleClickFolder(file.path, file.type)
}}
onContextMenu={(e) => {
e.preventDefault()
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}
controlBehaviour={ state.ctrlKey }
@ -832,12 +920,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
label={label(file)}
onClick={(e) => {
e.stopPropagation()
if (state.focusEdit.element !== file.path) handleClickFile(file.path)
if (state.focusEdit.element !== file.path) handleClickFile(file.path, file.type)
}}
onContextMenu={(e) => {
e.preventDefault()
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}
labelClass={labelClass}
@ -906,21 +994,25 @@ export const FileExplorer = (props: FileExplorerProps) => {
message={ state.focusModal.message }
children={ state.focusModal.children }
hide={ state.focusModal.hide }
ok={ state.focusModal.ok }
cancel={ state.focusModal.cancel }
okLabel={ state.focusModal.okLabel }
okFn={ state.focusModal.okFn }
cancelLabel={ state.focusModal.cancelLabel }
cancelFn={ state.focusModal.cancelFn }
handleHide={ handleHideModal }
/>
}
<Toaster message={state.toasterMsg} />
{ state.showContextMenu &&
<FileExplorerContextMenu
actions={state.actions}
actions={state.focusElement.length > 1 ? state.actions.filter(item => item.multiselect) : state.actions.filter(item => !item.multiselect)}
hideContextMenu={hideContextMenu}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
deletePath={deletePath}
renamePath={editModeOn}
runScript={runScript}
copy={handleCopyClick}
paste={handlePasteClick}
emit={emitContextMenuEvent}
pageX={state.focusContext.x}
pageY={state.focusContext.y}
@ -931,6 +1023,9 @@ export const FileExplorer = (props: FileExplorerProps) => {
e.stopPropagation()
handleMouseOver(state.focusContext.element)
}}
pushChangesToGist={pushChangesToGist}
publishFolderToGist={publishFolderToGist}
publishFileToGist={publishFileToGist}
/>
}
</div>
@ -940,18 +1035,41 @@ export const FileExplorer = (props: FileExplorerProps) => {
export default FileExplorer
async function packageFiles (filesProvider, directory, callback) {
const isFile = filesProvider.isFile(directory)
const ret = {}
try {
await filesProvider.copyFolderToJson(directory, ({ path, content }) => {
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
path = path.replace(/\//g, '...')
ret[path] = { content }
})
callback(null, ret)
} catch (e) {
return callback(e)
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 {
await filesProvider.copyFolderToJson(directory, ({ path, content }) => {
if (/^\s+$/.test(content) || !content.length) {
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, '...')
ret[path] = { content }
})
callback(null, ret)
} catch (e) {
return callback(e)
}
}
}

@ -9,7 +9,6 @@ export const fileSystemInitialState = {
files: {
files: [],
expandPath: [],
workspaceName: null,
blankPath: null,
isRequesting: false,
isSuccessful: false,
@ -83,7 +82,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...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,
isSuccessful: true,
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': {
return {
...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,
isRequesting: false,
isSuccessful: true,
@ -162,7 +152,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...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,
isRequesting: false,
isSuccessful: true,
@ -175,7 +165,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...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])],
isRequesting: false,
isSuccessful: true,
@ -188,7 +178,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...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])],
isRequesting: false,
isSuccessful: true,
@ -201,7 +191,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...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,
isSuccessful: true,
error: null
@ -213,7 +203,7 @@ export const fileSystemReducer = (state = fileSystemInitialState, action: Action
...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,
isSuccessful: true,
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] } }
const pathArr: string[] = path.split('/').filter(value => value)
@ -258,7 +250,8 @@ const resolveDirectory = (root, path: string, files, content) => {
files = _.set(files, _path, {
isDirectory: true,
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 : {}) }
})
@ -278,21 +271,26 @@ const removePath = (root, path: string, pathName, files) => {
files = _.set(files, _path, {
isDirectory: true,
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 : {}
})
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] } }
const result = resolveDirectory(root, path, files, content)
const result = resolveDirectory(provider, path, files, content)
return result
}
const removeInputField = (root, path: string, files) => {
const removeInputField = (provider, path: string, files) => {
const root = provider.workspace || provider.type || ''
if (path === root) {
delete files[root][path + '/' + 'blank']
return files
@ -300,15 +298,17 @@ const removeInputField = (root, path: string, files) => {
return removePath(root, path, path + '/' + 'blank', files)
}
const fileAdded = (root, path: string, files, content) => {
return resolveDirectory(root, path, files, content)
const fileAdded = (provider, path: string, files, content) => {
return resolveDirectory(provider, path, files, content)
}
const folderAdded = (root, path: string, files, content) => {
return resolveDirectory(root, path, files, content)
const folderAdded = (provider, path: string, 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) {
delete files[root][removedPath]
@ -317,7 +317,9 @@ const fileRemoved = (root, path: string, removedPath: string, 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) {
const allFiles = { [root]: { ...content[root], ...files[root] } }
@ -336,7 +338,8 @@ const fileRenamed = (root, path: string, removePath: string, files, content) =>
files = _.set(files, _path, {
isDirectory: true,
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 }
})

@ -6,7 +6,7 @@ export interface FileExplorerProps {
menuItems?: string[],
plugin: any,
focusRoot: boolean,
contextMenuItems: { id: string, name: string, type: string[], path: string[], extension: string[], pattern: string[] }[],
contextMenuItems: MenuItems,
displayInput?: boolean,
externalUploads?: EventTarget & HTMLInputElement
}
@ -15,6 +15,7 @@ export interface File {
path: string,
name: string,
isDirectory: boolean,
type: string,
child?: File[]
}
@ -24,10 +25,13 @@ export interface FileExplorerMenuProps {
fileManager: any,
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
publishToGist: () => void,
publishToGist: (path?: string) => 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 {
actions: action[],
createNewFile: (folder?: string) => void,
@ -35,13 +39,18 @@ export interface FileExplorerContextMenuProps {
deletePath: (path: string | string[]) => void,
renamePath: (path: string, type: string) => 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,
emit?: (id: string, path: string | string[]) => void,
pageX: number,
pageY: number,
path: string,
type: string,
focus: {key:string, type:string}[]
onMouseOver?: (...args) => void
focus: {key:string, type:string}[],
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) => {
if (keyCode === 27) { // Esc
if (props.cancel && props.cancel.fn) props.cancel.fn()
if (props.cancelFn) props.cancelFn()
handleHide()
} else if (keyCode === 13) { // Enter
enterHandler()
@ -33,9 +33,9 @@ export const ModalDialog = (props: ModalDialogProps) => {
const enterHandler = () => {
if (state.toggleBtn) {
if (props.ok && props.ok.fn) props.ok.fn()
if (props.okFn) props.okFn()
} else {
if (props.cancel && props.cancel.fn) props.cancel.fn()
if (props.cancelFn) props.cancelFn()
}
handleHide()
}
@ -79,29 +79,29 @@ export const ModalDialog = (props: ModalDialogProps) => {
</div>
<div className="modal-footer" data-id={`${props.id}ModalDialogModalFooter-react`}>
{/* todo add autofocus ^^ */}
{ props.ok &&
{ props.okLabel &&
<span
data-id={`${props.id}-modal-footer-ok-react`}
className={'modal-ok btn btn-sm ' + (state.toggleBtn ? 'btn-dark' : 'btn-light')}
onClick={() => {
if (props.ok.fn) props.ok.fn()
if (props.okFn) props.okFn()
handleHide()
}}
>
{ props.ok.label ? props.ok.label : 'OK' }
{ props.okLabel ? props.okLabel : 'OK' }
</span>
}
{ props.cancel &&
{ props.cancelLabel &&
<span
data-id={`${props.id}-modal-footer-cancel-react`}
className={'modal-cancel btn btn-sm ' + (state.toggleBtn ? 'btn-light' : 'btn-dark')}
data-dismiss="modal"
onClick={() => {
if (props.cancel.fn) props.cancel.fn()
if (props.cancelFn) props.cancelFn()
handleHide()
}}
>
{ props.cancel.label ? props.cancel.label : 'Cancel' }
{ props.cancelLabel ? props.cancelLabel : 'Cancel' }
</span>
}
</div>

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

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

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

@ -59,7 +59,7 @@ describe('testRunner', () => {
// Test github import
describe('test getting github imports', () => {
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 = {}
before(done => {
@ -78,8 +78,8 @@ describe('testRunner', () => {
})
it('should return contract content of given github path', () => {
const expt: object = {
cleanUrl: 'MathCody/solidity-examples/greeter/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}',
cleanUrl: 'ethential/solidity-examples/solidity-features-check/greeter.sol',
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'
}
assert.deepEqual(results, expt)
@ -268,6 +268,7 @@ describe('testRunner', () => {
})
// Test SWARM imports
/*
describe('test getting SWARM imports', () => {
const urlResolver = new RemixURLResolver()
const fileName = 'bzz-raw://a728627437140f2b0b46c1bcfb0de2126d18b40e9b61c3e31bd96abebf714619'
@ -297,6 +298,7 @@ describe('testRunner', () => {
assert.deepEqual(results, expt)
})
})
*/
})
})
})

@ -4,7 +4,7 @@ import * as semver from 'semver'
import WebSocket from '../websocket'
import * as servicesList from '../serviceList'
import * as WS from 'ws' // eslint-disable-line
import { getDomain } from '../utils'
import { getDomain, absolutePath } from '../utils'
import Axios from 'axios'
import * as fs from 'fs-extra'
import * as path from 'path'
@ -24,16 +24,19 @@ async function warnLatestVersion () {
const services = {
git: (readOnly: boolean) => new servicesList.GitClient(readOnly),
hardhat: (readOnly: boolean) => new servicesList.HardhatClient(readOnly),
folder: (readOnly: boolean) => new servicesList.Sharedfolder(readOnly)
}
// Similar object is also defined in websocket.ts
const ports = {
git: 65521,
hardhat: 65522,
folder: 65520
}
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))
socket.start(callback)
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.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) => {
sharedFolderClient.setWebSocket(ws)

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

Loading…
Cancel
Save