Merge branch 'master' into debugcrash

pull/5370/head
bunsenstraat 3 years ago committed by GitHub
commit f94f6184bb
  1. 3
      apps/debugger/src/app/debugger-api.ts
  2. 9
      apps/remix-ide-e2e/src/tests/ballot.test.ts
  3. 2
      apps/remix-ide-e2e/src/tests/debugger.spec.ts
  4. 17
      apps/remix-ide-e2e/src/tests/generalSettings.test.ts
  5. 2
      apps/remix-ide-e2e/src/tests/pluginManager.spec.ts
  6. 2
      apps/remix-ide-e2e/src/tests/runAndDeploy.ts
  7. 4
      apps/remix-ide-e2e/src/tests/specialFunctions.test.ts
  8. 22
      apps/remix-ide-e2e/src/tests/transactionExecution.spec.ts
  9. 24
      apps/remix-ide/src/app/panels/file-panel.js
  10. 2
      apps/remix-ide/src/app/tabs/compileTab/compilerContainer.js
  11. 13
      apps/remix-ide/src/app/tabs/hardhat-provider.js
  12. 221
      apps/remix-ide/src/app/tabs/settings-tab.js
  13. 2
      apps/remix-ide/src/app/ui/landing-page/landing-page.js
  14. 21
      apps/remix-ide/src/app/ui/txLogger.js
  15. 8
      apps/remix-ide/src/app/ui/universal-dapp-ui.js
  16. 5
      apps/remix-ide/src/blockchain/blockchain.js
  17. 40
      apps/remix-ide/src/blockchain/execution-context.js
  18. 2
      apps/remix-ide/src/remixAppManager.js
  19. 4
      libs/remix-debug/src/Ethdebugger.ts
  20. 27
      libs/remix-debug/src/code/codeManager.ts
  21. 4
      libs/remix-debug/src/debugger/VmDebugger.ts
  22. 3
      libs/remix-debug/src/debugger/debugger.ts
  23. 11
      libs/remix-debug/src/solidity-decoder/decodeInfo.ts
  24. 4
      libs/remix-debug/src/solidity-decoder/stateDecoder.ts
  25. 2
      libs/remix-debug/src/solidity-decoder/types/Mapping.ts
  26. 3
      libs/remix-debug/src/solidity-decoder/types/Struct.ts
  27. 11
      libs/remix-debug/src/trace/traceAnalyser.ts
  28. 14
      libs/remix-debug/src/trace/traceCache.ts
  29. 20
      libs/remix-debug/src/trace/traceManager.ts
  30. 121
      libs/remix-lib/src/execution/forkAt.ts
  31. 30
      libs/remix-lib/src/execution/txExecution.ts
  32. 4
      libs/remix-lib/src/index.ts
  33. 7
      libs/remix-lib/src/util.ts
  34. 2
      libs/remix-lib/src/web3Provider/web3VmProvider.ts
  35. 4
      libs/remix-simulator/src/vm-context.ts
  36. 2
      libs/remix-solidity/src/compiler/types.ts
  37. 11
      libs/remix-ui/clipboard/src/lib/copy-to-clipboard/copy-to-clipboard.tsx
  38. 5
      libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx
  39. 101
      libs/remix-ui/debugger-ui/src/lib/vm-debugger/assembly-items.tsx
  40. 19
      libs/remix-ui/debugger-ui/src/lib/vm-debugger/dropdown-panel.tsx
  41. 2
      libs/remix-ui/debugger-ui/src/lib/vm-debugger/memory-panel.tsx
  42. 2
      libs/remix-ui/debugger-ui/src/lib/vm-debugger/stack-panel.tsx
  43. 2
      libs/remix-ui/debugger-ui/src/lib/vm-debugger/step-detail.tsx
  44. 34
      libs/remix-ui/debugger-ui/src/reducers/assembly-items.ts
  45. 5
      libs/remix-ui/debugger-ui/src/types/index.ts
  46. 3
      libs/remix-ui/file-explorer/src/lib/file-explorer-context-menu.tsx
  47. 28
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  48. 7
      libs/remix-ui/file-explorer/src/lib/types/index.ts
  49. 4
      libs/remix-ui/settings/.babelrc
  50. 19
      libs/remix-ui/settings/.eslintrc
  51. 7
      libs/remix-ui/settings/README.md
  52. 1
      libs/remix-ui/settings/src/index.ts
  53. 12
      libs/remix-ui/settings/src/lib/constants.ts
  54. 0
      libs/remix-ui/settings/src/lib/remix-ui-settings.css
  55. 167
      libs/remix-ui/settings/src/lib/remix-ui-settings.tsx
  56. 52
      libs/remix-ui/settings/src/lib/settingsAction.ts
  57. 103
      libs/remix-ui/settings/src/lib/settingsReducer.ts
  58. 16
      libs/remix-ui/settings/tsconfig.json
  59. 13
      libs/remix-ui/settings/tsconfig.lib.json
  60. 6
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  61. 20
      libs/remixd/README.md
  62. 6
      libs/remixd/src/bin/remixd.ts
  63. 6
      nx.json
  64. 11
      package-lock.json
  65. 8
      package.json
  66. 4
      tsconfig.json
  67. 18
      workspace.json

@ -111,8 +111,7 @@ export const DebuggerApiMixin = (Base) => class extends Base {
}
return null
},
debugWithGeneratedSources: false,
fork: 'berlin'
debugWithGeneratedSources: false
})
return await debug.debugger.traceManager.getTrace(hash)
}

@ -121,7 +121,8 @@ const stateCheck = {
chairperson: {
value: '0xCA35B7D915458EF540ADE6068DFE2F44E8FA733C',
type: 'address',
constant: false
constant: false,
immutable: false
},
voters: {
value: {
@ -148,7 +149,8 @@ const stateCheck = {
}
},
type: 'mapping(address => struct Ballot.Voter)',
constant: false
constant: false,
immutable: false
},
proposals: {
value: [
@ -168,7 +170,8 @@ const stateCheck = {
],
length: '0x1',
type: 'struct Ballot.Proposal[]',
constant: false
constant: false,
immutable: false
}
}

@ -228,7 +228,7 @@ module.exports = {
.waitForElementVisible('*[data-id="solidityLocals"]', 60000)
.pause(10000)
.checkVariableDebug('soliditylocals', { num: { value: '2', type: 'uint256' } })
.checkVariableDebug('soliditystate', { number: { value: '0', type: 'uint256', constant: false } })
.checkVariableDebug('soliditystate', { number: { value: '0', type: 'uint256', constant: false, immutable: false } })
.end()
}
}

@ -18,6 +18,7 @@ module.exports = {
'Should activate `generate contract metadata`': function (browser) {
browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000)
.waitForElementVisible('*[data-id="settingsTabGenerateContractMetadataLabel"]', 5000)
.verify.elementPresent('[data-id="settingsTabGenerateContractMetadata"]:checked')
.click('*[data-id="verticalIconsFileExplorerIcons"]')
.click('[data-id="treeViewLitreeViewItemcontracts"]')
.openFile('contracts/3_Ballot.sol')
@ -39,22 +40,26 @@ module.exports = {
.click('*[data-id="verticalIconsKindsettings"]')
.setValue('*[data-id="settingsTabGistAccessToken"]', '**********')
.click('*[data-id="settingsTabSaveGistToken"]')
.waitForElementVisible('*[data-shared="tooltipPopup"]:nth-last-of-type(1)', 5000)
.assert.containsText('*[data-shared="tooltipPopup"]:nth-last-of-type(1)', 'Access token has been saved')
.waitForElementVisible('*[data-shared="tooltipPopup"]', 5000)
.assert.containsText('*[data-shared="tooltipPopup"]', 'Access token has been saved')
.pause(3000)
},
'Should copy github access token to clipboard': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]', 5000)
.click('*[data-id="copyToClipboardCopyIcon"]')
.waitForElementVisible('*[data-shared="tooltipPopup"]:nth-last-of-type(1)', 5000)
.assert.containsText('*[data-shared="tooltipPopup"]:nth-last-of-type(1)', 'Copied value to clipboard.')
.waitForElementVisible('*[data-shared="tooltipPopup"]', 5000)
// .waitForElementVisible('*[data-shared="tooltipPopup"]:nth-last-of-type(1) , 5000)
// .assert.containsText('*[data-shared="tooltipPopup"]', 'Copied value to clipboard.')
// .assert.containsText('*[data-shared="tooltipPopup"]:nth-last-of-type(1)', 'Copied value to clipboard.')
},
'Should remove github access token': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]', 5000)
.pause(1000)
.click('*[data-id="settingsTabRemoveGistToken"]')
.waitForElementVisible('*[data-shared="tooltipPopup"]:nth-last-of-type(1)', 5000)
.assert.containsText('*[data-shared="tooltipPopup"]:nth-last-of-type(1)', 'Access token removed')
.waitForElementVisible('*[data-shared="tooltipPopup"]', 5000)
.assert.containsText('*[data-shared="tooltipPopup"]', 'Access token removed')
.assert.containsText('*[data-id="settingsTabGistAccessToken"]', '')
},

@ -5,7 +5,7 @@ import init from '../helpers/init'
const testData = {
pluginName: 'remixIde',
pluginDisplayName: 'Remix IDE',
pluginUrl: 'https://zokrates-remix-plugin.netlify.app/'
pluginUrl: 'https://zokrates.github.io/zokrates-remix-plugin/'
}
module.exports = {

@ -32,6 +32,8 @@ module.exports = {
'Should sign message using account key': function (browser: NightwatchBrowser) {
browser.waitForElementPresent('*[data-id="settingsRemixRunSignMsg"]')
.click('select[id="selectExEnvOptions"] option[value="vm-berlin"]')
.pause(2000)
.click('*[data-id="settingsRemixRunSignMsg"]')
.pause(2000)
.waitForElementPresent('*[data-id="modalDialogCustomPromptText"]')

@ -177,10 +177,10 @@ module.exports = {
.pause(1000)
.perform((done) => {
browser.getAddressAtPosition(4, (address) => {
browser.sendLowLevelTx(address, '1', '0xaa')
browser.sendLowLevelTx(address, '999999998765257135', '0xaa')
.pause(1000)
.journalLastChildIncludes('to: CheckSpecials.(fallback)')
.journalLastChildIncludes('value: 1 wei')
.journalLastChildIncludes('value: 999999998765257135 wei')
.journalLastChildIncludes('data: 0xaa')
.perform(done)
})

@ -148,9 +148,14 @@ module.exports = {
.click('.instance:nth-of-type(3) > div > button')
.clickFunction('g - transact (not payable)')
.journalLastChildIncludes('Error provided by the contract:')
.journalLastChildIncludes('CustomError')
.journalLastChildIncludes('CustomError : error description')
.journalLastChildIncludes('Parameters:')
.journalLastChildIncludes('2,3,error_string_2')
.journalLastChildIncludes('"value": "2",')
.journalLastChildIncludes('"value": "3",')
.journalLastChildIncludes('"value": "error_string_2",')
.journalLastChildIncludes('"documentation": "param1"')
.journalLastChildIncludes('"documentation": "param2"')
.journalLastChildIncludes('"documentation": "param3"')
.journalLastChildIncludes('Debug the transaction to get more information.')
},
@ -163,9 +168,14 @@ module.exports = {
.click('.instance:nth-of-type(2) > div > button')
.clickFunction('g - transact (not payable)')
.journalLastChildIncludes('Error provided by the contract:')
.journalLastChildIncludes('CustomError')
.journalLastChildIncludes('CustomError : error description')
.journalLastChildIncludes('Parameters:')
.journalLastChildIncludes('2,3,error_string_2')
.journalLastChildIncludes('"value": "2",')
.journalLastChildIncludes('"value": "3",')
.journalLastChildIncludes('"value": "error_string_2",')
.journalLastChildIncludes('"documentation": "param1"')
.journalLastChildIncludes('"documentation": "param2"')
.journalLastChildIncludes('"documentation": "param3"')
.journalLastChildIncludes('Debug the transaction to get more information.')
.end()
}
@ -256,6 +266,10 @@ contract C {
pragma solidity ^0.8.4;
/// error description
/// @param a param1
/// @param b param2
/// @param c param3
error CustomError(uint a, uint b, string c);
contract C {
function f() public pure {

@ -34,7 +34,7 @@ const modalDialogCustom = require('../ui/modal-dialog-custom')
const profile = {
name: 'filePanel',
displayName: 'File explorers',
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace', 'setWorkspace'],
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem'],
events: ['setWorkspace', 'renameWorkspace', 'deleteWorkspace', 'createWorkspace'],
icon: 'assets/img/fileManager.webp',
description: ' - ',
@ -60,9 +60,11 @@ module.exports = class Filepanel extends ViewPlugin {
this.gitHandle = new GitHandle()
this.hardhatHandle = new HardhatHandle()
this.registeredMenuItems = []
this.removedMenuItems = []
this.request = {}
this.workspaces = []
this.initialWorkspace = null
this.appManager = appManager
}
render () {
@ -88,6 +90,7 @@ module.exports = class Filepanel extends ViewPlugin {
request={this.request}
workspaces={this.workspaces}
registeredMenuItems={this.registeredMenuItems}
removedMenuItems={this.removedMenuItems}
initialWorkspace={this.initialWorkspace}
/>
, this.el)
@ -101,8 +104,22 @@ module.exports = class Filepanel extends ViewPlugin {
if (!item) throw new Error('Invalid register context menu argument')
if (!item.name || !item.id) throw new Error('Item name and id is mandatory')
if (!item.type && !item.path && !item.extension && !item.pattern) throw new Error('Invalid file matching criteria provided')
if (this.registeredMenuItems.filter((o) => {
return o.id === item.id && o.name === item.name
}).length) throw new Error(`Action ${item.name} already exists on ${item.id}`)
this.registeredMenuItems = [...this.registeredMenuItems, item]
this.removedMenuItems = this.removedMenuItems.filter(menuItem => item.id !== menuItem.id)
this.renderComponent()
}
removePluginActions (plugin) {
this.registeredMenuItems = this.registeredMenuItems.filter((item) => {
if (item.id !== plugin.name || item.sticky === true) return true
else {
this.removedMenuItems.push(item)
return false
}
})
this.renderComponent()
}
@ -163,6 +180,9 @@ module.exports = class Filepanel extends ViewPlugin {
}
return
}
const self = this
this.appManager.on('manager', 'pluginDeactivated', self.removePluginActions.bind(this))
// insert example contracts if there are no files to show
return new Promise((resolve, reject) => {
this._deps.fileProviders.browser.resolveDirectory('/', async (error, filesList) => {

@ -7,6 +7,7 @@ const addTooltip = require('../../ui/tooltip')
const semver = require('semver')
const modalDialogCustom = require('../../ui/modal-dialog-custom')
const css = require('../styles/compile-tab-styles')
const _paq = window._paq = window._paq || []
class CompilerContainer {
constructor (compileTabLogic, editor, config, queryParams) {
@ -111,6 +112,7 @@ class CompilerContainer {
this._view.compileIcon.setAttribute('title', 'idle')
this._view.compileIcon.classList.remove(`${css.spinningIcon}`)
this._view.compileIcon.classList.remove(`${css.bouncingIcon}`)
_paq.push(['trackEvent', 'compiler', `compiled_with_v_${this._retrieveVersion()}`])
})
}

@ -17,11 +17,13 @@ export default class HardhatProvider extends Plugin {
constructor (blockchain) {
super(profile)
this.provider = null
this.blocked = false // used to block any call when trying to recover after a failed connection.
this.blockchain = blockchain
}
onDeactivation () {
this.provider = null
this.blocked = false
}
hardhatProviderDialogBody () {
@ -39,6 +41,8 @@ export default class HardhatProvider extends Plugin {
sendAsync (data) {
return new Promise((resolve, reject) => {
if (this.blocked) return reject(new Error('provider unable to connect'))
// If provider is not set, allow to open modal only when provider is trying to connect
if (!this.provider) {
modalDialogCustom.prompt('Hardhat node request', this.hardhatProviderDialogBody(), 'http://127.0.0.1:8545', (target) => {
this.provider = new Web3.providers.HttpProvider(target)
@ -54,9 +58,16 @@ export default class HardhatProvider extends Plugin {
sendAsyncInternal (data, resolve, reject) {
if (this.provider) {
this.provider[this.provider.sendAsync ? 'sendAsync' : 'send'](data, (error, message) => {
// Check the case where current environment is VM on UI and it still sends RPC requests
// This will be displayed on UI tooltip as 'cannot get account list: Environment Updated !!'
if (this.blockchain.getProvider() !== 'Hardhat Provider' && data.method !== 'net_listening') return reject(new Error('Environment Updated !!'))
this.provider[this.provider.sendAsync ? 'sendAsync' : 'send'](data, async (error, message) => {
if (error) {
this.blocked = true
modalDialogCustom.alert('Hardhat Provider', `Error while connecting to the hardhat provider: ${error.message}`)
await this.call('udapp', 'setEnvironmentMode', 'vm')
this.provider = null
setTimeout(_ => { this.blocked = false }, 1000) // we wait 1 second for letting remix to switch to vm
return reject(error)
}
resolve(message)

@ -1,12 +1,10 @@
import React from 'react' // eslint-disable-line
import { ViewPlugin } from '@remixproject/engine-web'
import ReactDOM from 'react-dom'
import * as packageJson from '../../../../../package.json'
const yo = require('yo-yo')
import { RemixUiSettings } from '@remix-ui/settings' //eslint-disable-line
const globalRegistry = require('../../global/registry')
const tooltip = require('../ui/tooltip')
const copyToClipboard = require('../ui/copy-to-clipboard')
const EventManager = require('../../lib/events')
const css = require('./styles/settings-tab-styles')
const _paq = window._paq = window._paq || []
const profile = {
name: 'settings',
@ -51,210 +49,27 @@ module.exports = class SettingsTab extends ViewPlugin {
textWrapLabel: null
} /* eslint-enable */
this.event = new EventManager()
this.element = document.createElement('div')
this.element.setAttribute('id', 'settingsTab')
}
createThemeCheckies () {
const themes = this._deps.themeModule.getThemes()
const onswitchTheme = (event, name) => {
this._deps.themeModule.switchTheme(name)
}
if (themes) {
return yo`<div class="card-text themes-container">
${themes.map((aTheme) => {
const el = yo`<div class="radio custom-control custom-radio mb-1 form-check ${css.crow}">
<input type="radio" onchange=${event => { onswitchTheme(event, aTheme.name) }} class="align-middle custom-control-input" name="theme" id="${aTheme.name}" data-id="settingsTabTheme${aTheme.name}">
<label class="form-check-label custom-control-label" data-id="settingsTabThemeLabel${aTheme.name}" for="${aTheme.name}">${aTheme.name} (${aTheme.quality})</label>
</div>`
if (this._deps.themeModule.active === aTheme.name) el.querySelector('input').setAttribute('checked', 'checked')
return el
})}
</div>`
}
onActivation () {
this.renderComponent()
}
render () {
const self = this
if (self._view.el) return self._view.el
// Gist settings
const token = this.config.get('settings/gist-access-token')
const gistAccessToken = yo`<input id="gistaccesstoken" data-id="settingsTabGistAccessToken" type="password" class="form-control">`
if (token) gistAccessToken.value = token
const removeToken = () => { self.config.set('settings/gist-access-token', ''); gistAccessToken.value = ''; tooltip('Access token removed') }
const saveToken = () => {
this.config.set('settings/gist-access-token', gistAccessToken.value)
tooltip('Access token has been saved. RELOAD the page to apply it.')
}
const gistAddToken = yo`<input class="btn btn-sm btn-primary ml-2" id="savegisttoken" data-id="settingsTabSaveGistToken" onclick=${() => saveToken()} value="Save" type="button">`
const gistRemoveToken = yo`<button class="btn btn-sm btn-secondary ml-2" id="removegisttoken" data-id="settingsTabRemoveGistToken" title="Delete Github access token" onclick=${() => removeToken()}>Remove</button>`
this._view.gistToken = yo`
<div class="text-secondary mb-0 h6">
${gistAccessToken}
<div class="d-flex justify-content-end pt-2">
${copyToClipboard(() => gistAccessToken.value)}${gistAddToken}${gistRemoveToken}
</div>
<p class="pt-1">
<i class="${css.icon} fas fa-exclamation-triangle text-warning" aria-hidden="true"></i>
<span class="text-warning">Please reload Remix after having saved the token.</span>
</p>
</div>
`
this._view.optionVM = yo`<input onchange=${onchangeOption} class="custom-control-input" id="alwaysUseVM" data-id="settingsTabAlwaysUseVM" type="checkbox">`
this._view.optionVMLabel = yo`<label class="form-check-label custom-control-label align-middle" for="alwaysUseVM">Always use Ethereum VM at Load</label>`
if (this.config.get('settings/always-use-vm') === undefined) this.config.set('settings/always-use-vm', true)
if (this.config.get('settings/always-use-vm')) this._view.optionVM.setAttribute('checked', '')
elementStateChanged(self._view.optionVMLabel, !this.config.get('settings/always-use-vm'))
this._view.textWrap = yo`<input id="editorWrap" class="custom-control-input" type="checkbox" onchange=${textWrapEvent}>`
this._view.textWrapLabel = yo`<label class="form-check-label custom-control-label align-middle" for="editorWrap">Text Wrap</label>`
if (this.config.get('settings/text-wrap')) this._view.textWrap.setAttribute('checked', '')
elementStateChanged(self._view.textWrapLabel, !this.config.get('settings/text-wrap'))
const warnText = `Transaction sent over Web3 will use the web3.personal API - be sure the endpoint is opened before enabling it.
This mode allows to provide the passphrase in the Remix interface without having to unlock the account.
Although this is very convenient, you should completely trust the backend you are connected to (Geth, Parity, ...).
Remix never persist any passphrase.`.split('\n').map(s => s.trim()).join(' ')
this._view.personal = yo`<input onchange=${onchangePersonal} id="personal" type="checkbox" class="custom-control-input">`
this._view.warnPersonalMode = yo`<i class="${css.icon} fas fa-exclamation-triangle text-warning" aria-hidden="true"></i>`
this._view.personalLabel = yo`<label class="form-check-label custom-control-label align-middle" for="personal"> ${this._view.warnPersonalMode} Enable Personal Mode for web3 provider. ${warnText}></label>`
if (this.config.get('settings/personal-mode')) this._view.personal.setAttribute('checked', '')
elementStateChanged(self._view.personalLabel, !this.config.get('settings/personal-mode'))
this._view.useMatomoAnalytics = yo`<input onchange=${onchangeMatomoAnalytics} id="settingsMatomoAnalytics" type="checkbox" class="custom-control-input">`
this._view.useMatomoAnalyticsLabel = yo`
<label class="form-check-label custom-control-label align-middle" for="settingsMatomoAnalytics">
<span>Enable Matomo Analytics. We do not collect personally identifiable information (PII). The info is used to improve the sites UX & UI. See more about</span>
<a href="https://medium.com/p/66ef69e14931/" target="_blank">Analytics in Remix IDE</a> <span>&</span> <a target="_blank" href="https://matomo.org/free-software">Matomo</a>
</label>
`
if (this.config.get('settings/matomo-analytics')) {
this._view.useMatomoAnalytics.setAttribute('checked', '')
_paq.push(['forgetUserOptOut'])
// @TODO remove next line when https://github.com/matomo-org/matomo/commit/9e10a150585522ca30ecdd275007a882a70c6df5 is used
document.cookie = 'mtm_consent_removed=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
} else {
_paq.push(['optUserOut'])
}
elementStateChanged(self._view.useMatomoAnalyticsLabel, !this.config.get('settings/matomo-analytics'))
this._view.generateContractMetadata = yo`<input onchange=${onchangeGenerateContractMetadata} id="generatecontractmetadata" data-id="settingsTabGenerateContractMetadata" type="checkbox" class="custom-control-input">`
this._view.generateContractMetadataLabel = yo`<label class="form-check-label custom-control-label align-middle" data-id="settingsTabGenerateContractMetadataLabel" for="generatecontractmetadata">Generate contract metadata. Generate a JSON file in the contract folder. Allows to specify library addresses the contract depends on. If nothing is specified, Remix deploys libraries automatically.</label>`
if (this.config.get('settings/generate-contract-metadata') === undefined) this.config.set('settings/generate-contract-metadata', true)
if (this.config.get('settings/generate-contract-metadata')) this._view.generateContractMetadata.setAttribute('checked', '')
elementStateChanged(self._view.generateContractMetadataLabel, !this.config.get('settings/generate-contract-metadata'))
this._view.pluginInput = yo`<textarea rows="4" cols="70" id="plugininput" type="text" class="${css.pluginTextArea}" ></textarea>`
this._view.themes = this._deps.themeModule.getThemes()
this._view.themesCheckBoxes = this.createThemeCheckies()
this._view.config.general = yo`
<div class="${css.info} border-top">
<div class="card-body pt-3 pb-2">
<h6 class="${css.title} card-title">General settings</h6>
<div class="mt-2 custom-control custom-checkbox mb-1">
${this._view.generateContractMetadata}
${this._view.generateContractMetadataLabel}
</div>
<div class="fmt-2 custom-control custom-checkbox mb-1">
${this._view.optionVM}
${this._view.optionVMLabel}
</div>
<div class="mt-2 custom-control custom-checkbox mb-1">
${this._view.textWrap}
${this._view.textWrapLabel}
</div>
<div class="custom-control custom-checkbox mb-1">
${this._view.personal}
${this._view.personalLabel}
</div>
<div class="custom-control custom-checkbox mb-1">
${this._view.useMatomoAnalytics}
${this._view.useMatomoAnalyticsLabel}
</div>
</div>
</div>
`
this._view.gistToken = yo`
<div class="${css.info} border-top">
<div class="card-body pt-3 pb-2">
<h6 class="${css.title} card-title">Github Access Token</h6>
<p class="mb-1">Manage the access token used to publish to Gist and retrieve Github contents.</p>
<p class="">Go to github token page (link below) to create a new token and save it in Remix. Make sure this token has only 'create gist' permission.</p>
<p class="${css.crowNoFlex} mb-1"><a class="text-primary ${css.text}" target="_blank" href="https://github.com/settings/tokens">https://github.com/settings/tokens</a></p>
<div class="${css.crowNoFlex}"><label>TOKEN:</label>${this._view.gistToken}</div>
</div>
</div>`
this._view.config.themes = yo`
<div class="${css.info} border-top">
<div class="card-body pt-3 pb-2">
<h6 class="${css.title} card-title">Themes</h6>
${this._view.themesCheckBoxes}
</div>
</div>`
this._view.el = yo`
<div id="settingsView" data-id="settingsTabSettingsView">
${this._view.config.general}
${this._view.gistToken}
${this._view.config.themes}
</div>`
function onchangeGenerateContractMetadata (event) {
const isChecked = self.config.get('settings/generate-contract-metadata')
self.config.set('settings/generate-contract-metadata', !isChecked)
elementStateChanged(self._view.generateContractMetadataLabel, isChecked)
}
function onchangeOption (event) {
const isChecked = self.config.get('settings/always-use-vm')
self.config.set('settings/always-use-vm', !isChecked)
elementStateChanged(self._view.optionVMLabel, isChecked)
}
function textWrapEvent (event) {
const isChecked = self.config.get('settings/text-wrap')
self.config.set('settings/text-wrap', !isChecked)
elementStateChanged(self._view.textWrapLabel, isChecked)
self.editor.resize(!isChecked)
}
function onchangePersonal (event) {
const isChecked = self.config.get('settings/personal-mode')
self.config.set('settings/personal-mode', !isChecked)
elementStateChanged(self._view.personalLabel, isChecked)
}
function onchangeMatomoAnalytics (event) {
const isChecked = self.config.get('settings/matomo-analytics')
self.config.set('settings/matomo-analytics', !isChecked)
elementStateChanged(self._view.useMatomoAnalyticsLabel, isChecked)
if (event.target.checked) {
_paq.push(['forgetUserOptOut'])
// @TODO remove next line when https://github.com/matomo-org/matomo/commit/9e10a150585522ca30ecdd275007a882a70c6df5 is used
document.cookie = 'mtm_consent_removed=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
} else {
_paq.push(['optUserOut'])
}
}
function elementStateChanged (el, isChanged) {
if (isChanged) {
el.classList.remove('text-dark')
el.classList.add('text-secondary')
} else {
el.classList.add('text-dark')
el.classList.remove('text-secondary')
}
}
return this.element
}
this._deps.themeModule.switchTheme()
return this._view.el
renderComponent () {
ReactDOM.render(
<RemixUiSettings
config = { this.config }
editor = { this.editor }
_deps = { this._deps }
/>,
this.element
)
}
getGithubAccessToken () {

@ -534,7 +534,7 @@ export class LandingPage extends ViewPlugin {
<i class="far fa-hdd"></i>
<span class="ml-1 ${css.text}" onclick=${() => connectToLocalhost()}>Connect to Localhost</span>
</p>
<p class="mt-3 mb-0"><label>IMPORT FROM:</label></p>
<p class="mt-3 mb-0"><label>LOAD FROM:</label></p>
<div class="btn-group">
<button class="btn mr-1 btn-secondary" data-id="landingPageImportFromGistButton" onclick="${() => importFromGist()}">Gist</button>
<button class="btn mx-1 btn-secondary" onclick="${() => load('Github', 'github URL', ['https://github.com/0xcert/ethereum-erc721/src/contracts/tokens/nf-token-metadata.sol', 'https://github.com/OpenZeppelin/openzeppelin-solidity/blob/67bca857eedf99bf44a4b6a0fc5b5ed553135316/contracts/access/Roles.sol'])}">GitHub</button>

@ -354,6 +354,17 @@ module.exports = TxLogger
// helpers
function isDescendant (parent, child) {
var node = child.parentNode
while (node != null) {
if (node === parent) {
return true
}
node = node.parentNode
}
return false
}
function txDetails (e, tx, data, obj) {
const from = obj.from
const to = obj.to
@ -368,9 +379,13 @@ function txDetails (e, tx, data, obj) {
} else break
}
let table = blockElement.querySelector(`#${tx.id} [class^="txTable"]`)
const log = blockElement.querySelector(`#${tx.id} [class^='log']`)
const arrow = blockElement.querySelector(`#${tx.id} [class^='arrow']`)
const tables = blockElement.querySelectorAll(`#${tx.id} [class^="txTable"]`)
const logs = blockElement.querySelectorAll(`#${tx.id} [class^='log']`)
const arrows = blockElement.querySelectorAll(`#${tx.id} [class^='arrow']`)
let table = [...tables].filter((t) => isDescendant(tx, t))[0]
const log = [...logs].filter((t) => isDescendant(tx, t))[0]
const arrow = [...arrows].filter((t) => isDescendant(tx, t))[0]
if (table && table.parentNode) {
tx.removeChild(table)

@ -44,14 +44,14 @@ UniversalDAppUI.prototype.renderInstance = function (contract, address, contract
noInstances.parentNode.removeChild(noInstances)
}
const abi = txHelper.sortAbiFunction(contract.abi)
return this.renderInstanceFromABI(abi, address, contractName)
return this.renderInstanceFromABI(abi, address, contractName, contract)
}
// TODO this function was named before "appendChild".
// this will render an instance: contract name, contract address, and all the public functions
// basically this has to be called for the "atAddress" (line 393) and when a contract creation succeed
// this returns a DOM element
UniversalDAppUI.prototype.renderInstanceFromABI = function (contractABI, address, contractName) {
UniversalDAppUI.prototype.renderInstanceFromABI = function (contractABI, address, contractName, contract) {
const self = this
address = (address.slice(0, 2) === '0x' ? '' : '0x') + address.toString('hex')
address = ethJSUtil.toChecksumAddress(address)
@ -117,7 +117,8 @@ UniversalDAppUI.prototype.renderInstanceFromABI = function (contractABI, address
funABI: funABI,
address: address,
contractABI: contractABI,
contractName: contractName
contractName: contractName,
contract
}))
})
@ -255,6 +256,7 @@ UniversalDAppUI.prototype.runTransaction = function (lookupOnly, args, valArr, i
args.contractName,
args.contractABI,
args.funABI,
args.contract,
inputsValues,
args.address,
params,

@ -249,7 +249,7 @@ class Blockchain {
return txlistener
}
runOrCallContractMethod (contractName, contractAbi, funABI, value, address, callType, lookupOnly, logMsg, logCallback, outputCb, confirmationCb, continueCb, promptCb) {
runOrCallContractMethod (contractName, contractAbi, funABI, contract, value, address, callType, lookupOnly, logMsg, logCallback, outputCb, confirmationCb, continueCb, promptCb) {
// contractsDetails is used to resolve libraries
txFormat.buildData(contractName, contractAbi, {}, false, funABI, callType, (error, data) => {
if (error) {
@ -265,6 +265,7 @@ class Blockchain {
if (data) {
data.contractName = contractName
data.contractABI = contractAbi
data.contract = contract
}
const useCall = funABI.stateMutability === 'view' || funABI.stateMutability === 'pure'
this.runTx({ to: address, data, useCall }, confirmationCb, continueCb, promptCb, (error, txResult, _address, returnValue) => {
@ -490,7 +491,7 @@ class Blockchain {
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)
const vmError = txExecution.checkVMError(execResult, args.data.contractABI, args.data.contract)
if (vmError.error) {
return cb(vmError.message)
}

@ -1,6 +1,7 @@
/* global ethereum */
'use strict'
import Web3 from 'web3'
import { execution } from '@remix-project/remix-lib'
import EventManager from '../lib/events'
let web3
@ -21,8 +22,7 @@ export class ExecutionContext {
this.executionContext = null
this.blockGasLimitDefault = 4300000
this.blockGasLimit = this.blockGasLimitDefault
this.defaultFork = 'berlin'
this.currentFork = this.defaultFork
this.currentFork = 'berlin'
this.mainNetGenesisHash = '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3'
this.customNetWorks = {}
this.blocks = {}
@ -123,21 +123,18 @@ export class ExecutionContext {
this.executionContextChange(context, endPointUrl, confirmCb, infoCb, null)
}
executionContextChange (value, endPointUrl, confirmCb, infoCb, cb) {
async executionContextChange (value, endPointUrl, confirmCb, infoCb, cb) {
const context = value.context
const fork = value.fork || this.defaultFork
if (!cb) cb = () => {}
if (!confirmCb) confirmCb = () => {}
if (!infoCb) infoCb = () => {}
if (context === 'vm') {
this.executionContext = context
this.currentFork = fork
this.currentFork = value.fork
this.event.trigger('contextChanged', ['vm'])
return cb()
}
this.currentFork = this.defaultFork // in the case of injected and web3, we default to the last fork.
if (context === 'injected') {
if (injectedProvider === undefined) {
infoCb('No injected Web3 provider found. Make sure your provider (e.g. MetaMask) is active and running (when recently activated you may have to reload the page).')
@ -146,7 +143,7 @@ export class ExecutionContext {
this.askPermission()
this.executionContext = context
web3.setProvider(injectedProvider)
this._updateBlockGasLimit()
await this._updateChainContext()
this.event.trigger('contextChanged', ['injected'])
return cb()
}
@ -173,22 +170,29 @@ export class ExecutionContext {
this.listenOnLastBlockId = null
}
_updateBlockGasLimit () {
async _updateChainContext () {
if (this.getProvider() !== 'vm') {
web3.eth.getBlock('latest', (err, block) => {
if (!err) {
// we can't use the blockGasLimit cause the next blocks could have a lower limit : https://github.com/ethereum/remix/issues/506
this.blockGasLimit = (block && block.gasLimit) ? Math.floor(block.gasLimit - (5 * block.gasLimit) / 1024) : this.blockGasLimitDefault
} else {
this.blockGasLimit = this.blockGasLimitDefault
try {
const block = await web3.eth.getBlock('latest')
// we can't use the blockGasLimit cause the next blocks could have a lower limit : https://github.com/ethereum/remix/issues/506
this.blockGasLimit = (block && block.gasLimit) ? Math.floor(block.gasLimit - (5 * block.gasLimit) / 1024) : this.blockGasLimitDefault
try {
this.currentFork = execution.forkAt(await web3.eth.net.getId(), block.number)
} catch (e) {
this.currentFork = 'berlin'
console.log(`unable to detect fork, defaulting to ${this.currentFork}..`)
console.error(e)
}
})
} catch (e) {
console.error(e)
this.blockGasLimit = this.blockGasLimitDefault
}
}
}
listenOnLastBlock () {
this.listenOnLastBlockId = setInterval(() => {
this._updateBlockGasLimit()
this._updateChainContext()
}, 15000)
}
@ -202,7 +206,7 @@ export class ExecutionContext {
web3.eth.net.isListening((err, isConnected) => {
if (!err && isConnected === true) {
this.executionContext = context
this._updateBlockGasLimit()
this._updateChainContext()
this.event.trigger('contextChanged', [context])
this.event.trigger('web3EndpointChanged')
cb()

@ -14,7 +14,7 @@ const requiredModules = [ // services + layout views + system views
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', 'solidity']
const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity', 'hardhat-provider']
return nativePlugins.includes(name) || requiredModules.includes(name)
}

@ -41,7 +41,7 @@ export class Ethdebugger {
this.opts = opts
this.event = new EventManager()
this.traceManager = new TraceManager({ web3: this.web3, fork: this.opts.fork })
this.traceManager = new TraceManager({ web3: this.web3 })
this.codeManager = new CodeManager(this.traceManager)
this.solidityProxy = new SolidityProxy({ getCurrentCalledAddressAt: this.traceManager.getCurrentCalledAddressAt.bind(this.traceManager), getCode: this.codeManager.getCode.bind(this.codeManager) })
this.storageResolver = null
@ -55,7 +55,7 @@ export class Ethdebugger {
}
setManagers () {
this.traceManager = new TraceManager({ web3: this.web3, fork: this.opts.fork })
this.traceManager = new TraceManager({ web3: this.web3 })
this.codeManager = new CodeManager(this.traceManager)
this.solidityProxy = new SolidityProxy({ getCurrentCalledAddressAt: this.traceManager.getCurrentCalledAddressAt.bind(this.traceManager), getCode: this.codeManager.getCode.bind(this.codeManager) })
this.storageResolver = null

@ -145,15 +145,38 @@ export class CodeManager {
})
}
private retrieveIndexAndTrigger (codeMananger, address, step, code) {
private async retrieveIndexAndTrigger (codeMananger, address, step, code) {
let result
let next
const returnInstructionIndexes = []
const outOfGasInstructionIndexes = []
try {
result = codeMananger.getInstructionIndex(address, step)
next = codeMananger.getInstructionIndex(address, step + 1)
let values = this.traceManager.getAllStopIndexes()
if (values) {
for (const value of values) {
if (value.address === address) {
returnInstructionIndexes.push({ instructionIndex: this.getInstructionIndex(address, value.index), address })
}
}
}
values = this.traceManager.getAllOutofGasIndexes()
if (values) {
for (const value of values) {
if (value.address === address) {
outOfGasInstructionIndexes.push({ instructionIndex: this.getInstructionIndex(address, value.index), address })
}
}
}
} catch (error) {
return console.log(error)
}
try {
codeMananger.event.trigger('changed', [code, address, result])
codeMananger.event.trigger('changed', [code, address, result, next, returnInstructionIndexes, outOfGasInstructionIndexes])
} catch (e) {
console.log('dispatching event failed', e)
}

@ -59,8 +59,8 @@ export class VmDebuggerLogic {
}
listenToCodeManagerEvents () {
this._codeManager.event.register('changed', (code, address, index) => {
this.event.trigger('codeManagerChanged', [code, address, index])
this._codeManager.event.register('changed', (code, address, index, nextIndex, returnInstructionIndexes, outOfGasInstructionIndexes) => {
this.event.trigger('codeManagerChanged', [code, address, index, nextIndex, returnInstructionIndexes, outOfGasInstructionIndexes])
})
}

@ -26,8 +26,7 @@ export class Debugger {
this.debugger = new Ethdebugger({
web3: options.web3,
debugWithGeneratedSources: options.debugWithGeneratedSources,
compilationResult: this.compilationResult,
fork: options.fork
compilationResult: this.compilationResult
})
const { traceManager, callTree, solidityProxy } = this.debugger

@ -336,7 +336,9 @@ function computeOffsets (types, stateDefinitions, contractName, location) {
console.log('unable to retrieve decode info of ' + variable.typeDescriptions.typeString)
return null
}
if (!variable.constant && storagelocation.offset + type.storageBytes > 32) {
const immutable = variable.mutability === 'immutable'
const hasStorageSlots = !immutable && !variable.constant
if (hasStorageSlots && storagelocation.offset + type.storageBytes > 32) {
storagelocation.slot++
storagelocation.offset = 0
}
@ -344,12 +346,13 @@ function computeOffsets (types, stateDefinitions, contractName, location) {
name: variable.name,
type: type,
constant: variable.constant,
immutable,
storagelocation: {
offset: variable.constant ? 0 : storagelocation.offset,
slot: variable.constant ? 0 : storagelocation.slot
offset: !hasStorageSlots ? 0 : storagelocation.offset,
slot: !hasStorageSlots ? 0 : storagelocation.slot
}
})
if (!variable.constant) {
if (hasStorageSlots) {
if (type.storageSlots === 1 && storagelocation.offset + type.storageBytes <= 32) {
storagelocation.offset += type.storageBytes
} else {

@ -15,9 +15,13 @@ export async function decodeState (stateVars, storageResolver) {
try {
const decoded = await stateVar.type.decodeFromStorage(stateVar.storagelocation, storageResolver)
decoded.constant = stateVar.constant
decoded.immutable = stateVar.immutable
if (decoded.constant) {
decoded.value = '<constant>'
}
if (decoded.immutable) {
decoded.value = '<immutable>'
}
ret[stateVar.name] = decoded
} catch (e) {
console.log(e)

@ -38,7 +38,7 @@ export class Mapping extends RefType {
decodeFromMemoryInternal (offset, memory) {
// mappings can only exist in storage and not in memory
// so this should never be called
return { value: '<not implemented>', length: '0x', type: this.typeName }
return { value: '', length: '0x0', type: this.typeName }
}
async decodeMappingsLocation (preimages, location, storageResolver) {

@ -1,6 +1,7 @@
'use strict'
import { add } from './util'
import { RefType } from './RefType'
import { Mapping } from './Mapping'
export class Struct extends RefType {
members
@ -33,7 +34,7 @@ export class Struct extends RefType {
var contentOffset = offset
var member = item.type.decodeFromMemory(contentOffset, memory)
ret[item.name] = member
offset += 32
if (!(item.type instanceof Mapping)) offset += 32
})
return { value: ret, type: this.typeName }
}

@ -49,6 +49,17 @@ export class TraceAnalyser {
this.traceCache.pushReturnValue(index, returnParamsObj)
}
if (traceHelper.isReturnInstruction(step) || traceHelper.isStopInstruction(step) || traceHelper.isRevertInstruction(step)) {
this.traceCache.pushStopIndex(index, this.traceCache.currentCall.call.address)
}
try {
if (parseInt(step.gas) - parseInt(step.gasCost) <= 0 || step.error === 'OutOfGas') {
this.traceCache.pushOutOfGasIndex(index, this.traceCache.currentCall.call.address)
}
} catch (e) {
console.error(e)
}
}
buildCalldata (index, step, tx, newContext) {

@ -5,6 +5,8 @@ const { sha3_256 } = util
export class TraceCache {
returnValues
stopIndexes
outofgasIndexes
currentCall
callsTree
callsData
@ -24,6 +26,8 @@ export class TraceCache {
// ...Changes contains index in the vmtrace of the corresponding changes
this.returnValues = {}
this.stopIndexes = []
this.outofgasIndexes = []
this.currentCall = null
this.callsTree = null
this.callsData = {}
@ -59,7 +63,7 @@ export class TraceCache {
this.currentCall.call.reverted = reverted
}
var parent = this.currentCall.parent
this.currentCall = parent ? { call: parent.call, parent: parent.parent } : null
if (parent) this.currentCall = { call: parent.call, parent: parent.parent }
return
}
const call = {
@ -78,6 +82,14 @@ export class TraceCache {
this.currentCall = { call: call, parent: this.currentCall }
}
pushOutOfGasIndex (index, address) {
this.outofgasIndexes.push({ index, address })
}
pushStopIndex (index, address) {
this.stopIndexes.push({ index, address })
}
pushReturnValue (step, value) {
this.returnValues[step] = value
}

@ -1,9 +1,9 @@
'use strict'
import { util, execution } from '@remix-project/remix-lib'
import { TraceAnalyser } from './traceAnalyser'
import { TraceCache } from './traceCache'
import { TraceStepManager } from './traceStepManager'
import { isCreateInstruction } from './traceHelper'
import { util } from '@remix-project/remix-lib'
export class TraceManager {
web3
@ -17,7 +17,6 @@ export class TraceManager {
constructor (options) {
this.web3 = options.web3
this.fork = options.fork
this.isLoading = false
this.trace = null
this.traceCache = new TraceCache()
@ -37,6 +36,15 @@ export class TraceManager {
if (result['structLogs'].length > 0) {
this.trace = result['structLogs']
try {
const networkId = await this.web3.eth.net.getId()
this.fork = execution.forkAt(networkId, tx.blockNumber)
} catch (e) {
this.fork = 'berlin'
console.log(`unable to detect fork, defaulting to ${this.fork}..`)
console.error(e)
}
this.traceAnalyser.analyse(result['structLogs'], tx)
this.isLoading = false
return true
@ -201,6 +209,14 @@ export class TraceManager {
return this.trace[stepIndex].pc
}
getAllStopIndexes () {
return this.traceCache.stopIndexes
}
getAllOutofGasIndexes () {
return this.traceCache.outofgasIndexes
}
getReturnValue (stepIndex) {
try {
this.checkRequestedStep(stepIndex)

@ -0,0 +1,121 @@
'use strict'
/**
* returns the fork name for the @argument networkId and @argument blockNumber
*
* @param {Object} networkId - network Id (1 for VM, 3 for Ropsten, 4 for Rinkeby, 5 for Goerli)
* @param {Object} blockNumber - block number
* @return {String} - fork name (Berlin, Istanbul, ...)
*/
export function forkAt (networkId, blockNumber) {
if (forks[networkId]) {
let currentForkName = forks[networkId][0].name
for (const fork of forks[networkId]) {
if (blockNumber >= fork.number) {
currentForkName = fork.name
}
}
return currentForkName
}
return 'berlin'
}
// see https://github.com/ethereum/go-ethereum/blob/master/params/config.go
const forks = {
1: [
{
number: 4370000,
name: 'byzantium'
},
{
number: 7280000,
name: 'constantinople'
},
{
number: 7280000,
name: 'petersburg'
},
{
number: 9069000,
name: 'istanbul'
},
{
number: 9200000,
name: 'muirglacier'
},
{
number: 12244000,
name: 'berlin'
}
],
3: [
{
number: 1700000,
name: 'byzantium'
},
{
number: 4230000,
name: 'constantinople'
},
{
number: 4939394,
name: 'petersburg'
},
{
number: 6485846,
name: 'istanbul'
},
{
number: 7117117,
name: 'muirglacier'
},
{
number: 9812189,
name: 'berlin'
},
{
number: 10499401,
name: 'london'
}
],
4: [
{
number: 1035301,
name: 'byzantium'
},
{
number: 3660663,
name: 'constantinople'
},
{
number: 4321234,
name: 'petersburg'
},
{
number: 5435345,
name: 'istanbul'
},
{
number: 8290928,
name: 'berlin'
},
{
number: 8897988,
name: 'london'
}
],
5: [
{
number: 1561651,
name: 'istanbul'
},
{
number: 4460644,
name: 'berlin'
},
{
number: 5062605,
name: 'london'
}
]
}

@ -57,7 +57,7 @@ export function callFunction (from, to, data, value, gasLimit, funAbi, txRunner,
* @param {Object} execResult - execution result given by the VM
* @return {Object} - { error: true/false, message: DOMNode }
*/
export function checkVMError (execResult, abi) {
export function checkVMError (execResult, abi, contract) {
const errorCode = {
OUT_OF_GAS: 'out of gas',
STACK_UNDERFLOW: 'stack underflow',
@ -92,7 +92,7 @@ export function checkVMError (execResult, abi) {
const returnDataHex = returnData.slice(0, 4).toString('hex')
let customError
if (abi) {
let decodedCustomErrorInputs
let decodedCustomErrorInputsClean
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:
@ -104,16 +104,36 @@ export function checkVMError (execResult, abi) {
if (!sign) continue
if (returnDataHex === sign.replace('0x', '')) {
customError = item.name
decodedCustomErrorInputs = fn.decodeFunctionData(fn.getFunction(item.name), returnData)
const functionDesc = fn.getFunction(item.name)
// decoding error parameters
const decodedCustomErrorInputs = fn.decodeFunctionData(functionDesc, returnData)
decodedCustomErrorInputsClean = {}
let devdoc = {}
// "contract" reprensents the compilation result containing the NATSPEC documentation
if (contract && fn.functions && Object.keys(fn.functions).length) {
const functionSignature = Object.keys(fn.functions)[0]
// we check in the 'devdoc' if there's a developer documentation for this error
devdoc = contract.object.devdoc.errors[functionSignature][0] || {}
// we check in the 'userdoc' if there's an user documentation for this error
const userdoc = contract.object.userdoc.errors[functionSignature][0] || {}
if (userdoc) customError += ' : ' + (userdoc as any).notice // we append the user doc if any
}
for (const input of functionDesc.inputs) {
const v = decodedCustomErrorInputs[input.name]
decodedCustomErrorInputsClean[input.name] = {
value: v.toString ? v.toString() : v,
documentation: (devdoc as any).params[input.name] // we add the developer documentation for this input parameter if any
}
}
break
}
}
}
if (decodedCustomErrorInputs) {
if (decodedCustomErrorInputsClean) {
msg = '\tThe transaction has been reverted to the initial state.\nError provided by the contract:'
msg += `\n${customError}`
msg += '\nParameters:'
msg += `\n${decodedCustomErrorInputs}`
msg += `\n${JSON.stringify(decodedCustomErrorInputsClean, null, ' ')}`
}
}
if (!customError) {

@ -13,6 +13,7 @@ import * as txFormat from './execution/txFormat'
import { TxListener } from './execution/txListener'
import { TxRunner } from './execution/txRunner'
import { LogsManager } from './execution/logsManager'
import { forkAt } from './execution/forkAt'
import * as typeConversion from './execution/typeConversion'
import { TxRunnerVM } from './execution/txRunnerVM'
import { TxRunnerWeb3 } from './execution/txRunnerWeb3'
@ -45,7 +46,8 @@ function modules () {
TxRunnerWeb3: TxRunnerWeb3,
TxRunnerVM: TxRunnerVM,
typeConversion: typeConversion,
LogsManager
LogsManager,
forkAt
}
}
}

@ -1,5 +1,6 @@
'use strict'
import { BN, bufferToHex, keccak, setLengthLeft, toBuffer, addHexPrefix } from 'ethereumjs-util'
import stringSimilarity from 'string-similarity'
/*
contains misc util: @TODO should be splitted
@ -222,9 +223,11 @@ export function compareByteCode (code1, code2) {
code2 = this.extractSwarmHash(code2)
code2 = this.extractcborMetadata(code2)
if (code1 && code2 && code1.indexOf(code2) === 0) {
return true
if (code1 && code2) {
const compare = stringSimilarity.compareTwoStrings(code1, code2)
return compare > 0.93
}
return false
}
/* util extracted out from remix-ide. @TODO split this file, cause it mix real util fn with solidity related stuff ... */

@ -138,7 +138,7 @@ export class Web3VmProvider {
async txProcessed (data) {
const lastOp = this.vmTraces[this.processingHash].structLogs[this.processingIndex - 1]
if (lastOp) {
lastOp.error = lastOp.op !== 'RETURN' && lastOp.op !== 'STOP' && lastOp.op !== 'thisDESTRUCT'
lastOp.error = lastOp.op !== 'RETURN' && lastOp.op !== 'STOP' && lastOp.op !== 'DESTRUCT'
}
this.vmTraces[this.processingHash].gas = '0x' + data.gasUsed.toString(16)

@ -91,7 +91,6 @@ export class VMContext {
blocks
latestBlockNumber
txs
defaultFork
currentVm
web3vm
logsManager
@ -100,8 +99,7 @@ export class VMContext {
constructor (fork?) {
this.blockGasLimitDefault = 4300000
this.blockGasLimit = this.blockGasLimitDefault
this.defaultFork = fork || 'berlin'
this.currentFork = this.defaultFork
this.currentFork = fork || 'berlin'
this.currentVm = this.createVm(this.currentFork)
this.blocks = {}
this.latestBlockNumber = 0

@ -150,7 +150,7 @@ export interface CompilerInputOptions {
language?: Language
}
export type EVMVersion = 'homestead' | 'tangerineWhistle' | 'spuriousDragon' | 'byzantium' | 'constantinople' | 'petersburg' | 'istanbul' | 'muirGlacier' | 'berlin' | null
export type EVMVersion = 'homestead' | 'tangerineWhistle' | 'spuriousDragon' | 'byzantium' | 'constantinople' | 'petersburg' | 'istanbul' | 'muirGlacier' | 'berlin' | 'london' | null
export type Language = 'Solidity' | 'Yul'

@ -6,8 +6,9 @@ import './copy-to-clipboard.css'
export const CopyToClipboard = ({ content, tip='Copy', icon='fa-copy', ...otherProps }) => {
const [message, setMessage] = useState(tip)
const handleClick = () => {
if (content && content !== '') { // module `copy` keeps last copied thing in the memory, so don't show tooltip if nothing is copied, because nothing was added to memory
const handleClick = (event) => {
if (content && content !== '') { // module `copy` keeps last copied thing in the memory, so don't show tooltip if nothing is copied, because nothing was added to memory
try {
if (typeof content !== 'string') {
content = JSON.stringify(content, null, '\t')
@ -20,6 +21,8 @@ export const CopyToClipboard = ({ content, tip='Copy', icon='fa-copy', ...otherP
} else {
setMessage('Cannot copy empty content!')
}
event.preventDefault()
return false
}
const reset = () => {
@ -27,10 +30,10 @@ export const CopyToClipboard = ({ content, tip='Copy', icon='fa-copy', ...otherP
}
return (
<a href="#" onClick={handleClick} onMouseLeave={reset}>
<a href='#' onClick={handleClick} onMouseLeave={reset}>
<OverlayTrigger placement="right" overlay={
<Tooltip id="overlay-tooltip">
{ message }
{ message }
</Tooltip>
}>
<i className={`far ${icon} ml-1 p-2`} aria-hidden="true"

@ -208,8 +208,7 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
}
return null
},
debugWithGeneratedSources: state.opt.debugWithGeneratedSources,
fork: 'berlin'
debugWithGeneratedSources: state.opt.debugWithGeneratedSources
})
debuggerInstance.debug(blockNumber, txNumber, tx, () => {
@ -275,7 +274,7 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
<div className="px-2">
<div>
<p className="my-2 debuggerLabel">Debugger Configuration</p>
<div className="mt-2 debuggerConfig custom-control custom-checkbox">
<div className="mt-2 mb-2 debuggerConfig custom-control custom-checkbox">
<input className="custom-control-input" id="debugGeneratedSourcesInput" onChange={({ target: { checked } }) => {
setState(prevState => {
return { ...prevState, opt: { debugWithGeneratedSources: checked } }

@ -4,41 +4,116 @@ import './styles/assembly-items.css'
export const AssemblyItems = ({ registerEvent }) => {
const [assemblyItems, dispatch] = useReducer(reducer, initialState)
const [absoluteSelectedIndex, setAbsoluteSelectedIndex] = useState(0)
const [selectedItem, setSelectedItem] = useState(0)
const [nextSelectedItem, setNextSelectedItem] = useState(1)
const [returnInstructionIndexes, setReturnInstructionIndexes] = useState([])
const [outOfGasInstructionIndexes, setOutOfGasInstructionIndexes] = useState([])
const refs = useRef({})
const asmItemsRef = useRef(null)
useEffect(() => {
registerEvent && registerEvent('codeManagerChanged', (code, address, index) => {
dispatch({ type: 'FETCH_OPCODES_SUCCESS', payload: { code, address, index } })
registerEvent && registerEvent('codeManagerChanged', (code, address, index, nextIndex, returnInstructionIndexes, outOfGasInstructionIndexes) => {
dispatch({ type: 'FETCH_OPCODES_SUCCESS', payload: { code, address, index, nextIndex, returnInstructionIndexes, outOfGasInstructionIndexes } })
})
}, [])
useEffect(() => {
if (selectedItem !== assemblyItems.index) {
if (absoluteSelectedIndex !== assemblyItems.index) {
clearItems()
indexChanged(assemblyItems.index)
nextIndexChanged(assemblyItems.nextIndex)
returnIndexes(assemblyItems.returnInstructionIndexes)
outOfGasIndexes(assemblyItems.outOfGasInstructionIndexes)
}
}, [assemblyItems.index])
const indexChanged = (index: number) => {
if (index < 0) return
let currentItem = refs.current[selectedItem] ? refs.current[selectedItem] : null
}, [assemblyItems.opCodes.index])
const clearItem = (currentItem) => {
if (currentItem) {
currentItem.removeAttribute('selected')
currentItem.removeAttribute('style')
if (currentItem.firstChild) {
currentItem.firstChild.removeAttribute('style')
}
const codeView = asmItemsRef.current
}
}
const clearItems = () => {
clearItem(refs.current[selectedItem] ? refs.current[selectedItem] : null)
clearItem(refs.current[nextSelectedItem] ? refs.current[nextSelectedItem] : null)
returnInstructionIndexes.map((index) => {
if (index < 0) return
clearItem(refs.current[index] ? refs.current[index] : null)
})
outOfGasInstructionIndexes.map((index) => {
if (index < 0) return
clearItem(refs.current[index] ? refs.current[index] : null)
})
}
const indexChanged = (index: number) => {
if (index < 0) return
currentItem = codeView.children[index]
currentItem.style.setProperty('border-color', 'var(--primary)')
currentItem.style.setProperty('border-style', 'solid')
const codeView = asmItemsRef.current
const currentItem = codeView.children[index]
if (currentItem) {
currentItem.style.setProperty('background-color', 'var(--primary)')
currentItem.style.setProperty('color', 'var(--light)')
currentItem.setAttribute('selected', 'selected')
codeView.scrollTop = currentItem.offsetTop - parseInt(codeView.offsetTop)
setSelectedItem(index)
}
setSelectedItem(index)
setAbsoluteSelectedIndex(assemblyItems.opCodes.index)
}
const nextIndexChanged = (index: number) => {
if (index < 0) return
const codeView = asmItemsRef.current
const currentItem = codeView.children[index]
if (currentItem) {
currentItem.style.setProperty('border-color', 'var(--secondary)')
currentItem.style.setProperty('border-style', 'dotted')
currentItem.setAttribute('selected', 'selected')
}
setNextSelectedItem(index)
}
const returnIndexes = (indexes) => {
indexes.map((index) => {
if (index < 0) return
const codeView = asmItemsRef.current
const currentItem = codeView.children[index]
if (currentItem) {
currentItem.style.setProperty('border-color', 'var(--warning)')
currentItem.style.setProperty('border-style', 'dotted')
currentItem.setAttribute('selected', 'selected')
}
})
setReturnInstructionIndexes(indexes)
}
const outOfGasIndexes = (indexes) => {
indexes.map((index) => {
if (index < 0) return
const codeView = asmItemsRef.current
const currentItem = codeView.children[index]
if (currentItem) {
currentItem.style.setProperty('border-color', 'var(--danger)')
currentItem.style.setProperty('border-style', 'dotted')
currentItem.setAttribute('selected', 'selected')
}
})
setOutOfGasInstructionIndexes(indexes)
}
return (

@ -7,7 +7,7 @@ import './styles/dropdown-panel.css'
export const DropdownPanel = (props: DropdownPanelProps) => {
const [calldataObj, dispatch] = useReducer(reducer, initialState)
const { dropdownName, dropdownMessage, calldata, header, loading, extractFunc, formatSelfFunc, registerEvent, triggerEvent, loadMoreEvent, loadMoreCompletedEvent } = props
const { dropdownName, dropdownMessage, calldata, header, loading, extractFunc, formatSelfFunc, registerEvent, triggerEvent, loadMoreEvent, loadMoreCompletedEvent, headStyle, bodyStyle, hexHighlight } = props
const extractDataDefault: ExtractFunc = (item, parent?) => {
const ret: ExtractData = {}
@ -34,10 +34,21 @@ export const DropdownPanel = (props: DropdownPanelProps) => {
return ret
}
const formatSelfDefault = (key: string | number, data: ExtractData) => {
let value
if (hexHighlight && typeof (data.self) === 'string') {
const isHex = data.self.startsWith('0x') || hexHighlight
if (isHex) {
const regex = /^(0+)(.*)/g
const split = regex.exec(data.self.replace('0x', ''))
if (split && split[1]) {
value = (<span><span className="m-0 label_value">0x</span><span className="m-0 label_value">{split[1]}</span>{ split[2] && <span className="m-0 label_value font-weight-bold text-dark">{split[2]}</span> }</span>)
} else value = (<span><span className="m-0 label_value">0x</span><span className="m-0 label_value font-weight-bold text-dark">{data.self.replace('0x', '')}</span></span>)
} else value = <span className="m-0 label_value">{data.self}</span>
} else value = <span className="m-0 label_value">{data.self}</span>
return (
<div className="d-flex mr-1 flex-row label_item">
<label className="small font-weight-bold mb-0 pr-1 label_key">{key}:</label>
<label className="m-0 label_value">{data.self}</label>
<label className="m-0 label_value">{value}</label>
</div>
)
}
@ -184,14 +195,14 @@ export const DropdownPanel = (props: DropdownPanelProps) => {
return (
<div className="border rounded px-1 mt-1 bg-light">
<div className="py-0 px-1 title">
<div className="py-0 px-1 title" style={headStyle}>
<div className={state.toggleDropdown ? 'icon fas fa-caret-down' : 'icon fas fa-caret-right'} onClick={handleToggle}></div>
<div className="name" data-id={`dropdownPanel${uniquePanelName}`} onClick={handleToggle}>{dropdownName}</div><span className="nameDetail" onClick={handleToggle}>{header}</span>
<CopyToClipboard content={state.copiableContent} data-id={`dropdownPanelCopyToClipboard${uniquePanelName}`} />
</div>
<div className='dropdownpanel' style={{ display: state.toggleDropdown ? 'block' : 'none' }}>
<i className="refresh fas fa-sync" style={{ display: state.updating ? 'inline-block' : 'none' }} aria-hidden="true"></i>
<div className='dropdowncontent' style={{ display: state.dropdownContent.display }}>
<div className='dropdowncontent' style={{ display: state.dropdownContent.display, ...bodyStyle }}>
{
state.data &&
<TreeView id="treeView">

@ -3,7 +3,7 @@ import DropdownPanel from './dropdown-panel' // eslint-disable-line
export const MemoryPanel = ({ calldata }) => {
return (
<DropdownPanel dropdownName='Memory' calldata={calldata || {}} />
<DropdownPanel hexHighlight={true} bodyStyle={{ fontFamily: 'monospace' }} dropdownName='Memory' calldata={calldata || {}} />
)
}

@ -4,7 +4,7 @@ import DropdownPanel from './dropdown-panel' // eslint-disable-line
export const StackPanel = ({ calldata }) => {
return (
<div id='stackpanel'>
<DropdownPanel dropdownName='Stack' calldata={calldata || {}} />
<DropdownPanel hexHighlight={true} bodyStyle={{ fontFamily: 'monospace' }} dropdownName='Stack' calldata={calldata || {}} />
</div>
)
}

@ -4,7 +4,7 @@ import DropdownPanel from './dropdown-panel' // eslint-disable-line
export const StepDetail = ({ stepDetail }) => {
return (
<div id='stepdetail' data-id='stepdetail'>
<DropdownPanel dropdownName='Step details' calldata={stepDetail || {}} />
<DropdownPanel hexHighlight={false} dropdownName='Step details' calldata={stepDetail || {}} />
</div>
)
}

@ -13,6 +13,9 @@ export const initialState = {
},
display: [],
index: 0,
nextIndex: -1,
returnInstructionIndexes: [],
outOfGasInstructionIndexes: [],
top: 0,
bottom: 0,
isRequesting: false,
@ -20,6 +23,20 @@ export const initialState = {
hasError: null
}
const reducedOpcode = (opCodes, payload) => {
const length = 100
let bottom = opCodes.index - 10
bottom = bottom < 0 ? 0 : bottom
const top = bottom + length
return {
index: opCodes.index - bottom,
nextIndex: opCodes.nextIndex - bottom,
display: opCodes.code.slice(bottom, top),
returnInstructionIndexes: payload.returnInstructionIndexes.map((index) => index.instructionIndex - bottom),
outOfGasInstructionIndexes: payload.outOfGasInstructionIndexes.map((index) => index.instructionIndex - bottom)
}
}
export const reducer = (state = initialState, action: Action) => {
switch (action.type) {
case 'FETCH_OPCODES_REQUEST': {
@ -32,21 +49,20 @@ export const reducer = (state = initialState, action: Action) => {
}
case 'FETCH_OPCODES_SUCCESS': {
const opCodes = action.payload.address === state.opCodes.address ? {
...state.opCodes, index: action.payload.index
...state.opCodes, index: action.payload.index, nextIndex: action.payload.nextIndex
} : deepEqual(action.payload.code, state.opCodes.code) ? state.opCodes : action.payload
const top = opCodes.index - 3 > 0 ? opCodes.index - 3 : 0
const bottom = opCodes.index + 4 < opCodes.code.length ? opCodes.index + 4 : opCodes.code.length
const display = opCodes.code.slice(top, bottom)
const reduced = reducedOpcode(opCodes, action.payload)
return {
opCodes,
display,
index: display.findIndex(code => code === opCodes.code[opCodes.index]),
top,
bottom,
display: reduced.display,
index: reduced.index,
nextIndex: reduced.nextIndex,
isRequesting: false,
isSuccessful: true,
hasError: null
hasError: null,
returnInstructionIndexes: reduced.returnInstructionIndexes,
outOfGasInstructionIndexes: reduced.outOfGasInstructionIndexes
}
}
case 'FETCH_OPCODES_ERROR': {

@ -27,7 +27,10 @@ export interface DropdownPanelProps {
registerEvent?: Function,
triggerEvent?: Function,
loadMoreEvent?: string,
loadMoreCompletedEvent?: string
loadMoreCompletedEvent?: string,
bodyStyle?: React.CSSProperties,
headStyle?: React.CSSProperties,
hexHighlight?: boolean // highlight non zero value of hex value
}
export type FormatSelfFunc = (key: string | number, data: ExtractData) => JSX.Element

@ -2,6 +2,7 @@ import React, { useRef, useEffect } from 'react' // eslint-disable-line
import { action, FileExplorerContextMenuProps } from './types'
import './css/file-explorer-context-menu.css'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => {
const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, copy, paste, runScript, emit, pageX, pageY, path, type, focus, ...otherProps } = props
@ -96,7 +97,7 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
deletePath(getPath())
break
default:
emit && emit(item.id, getPath())
emit && emit({ ...item, path: [path] } as customAction)
break
}
hideContextMenu()

@ -11,13 +11,14 @@ import { fileSystemReducer, fileSystemInitialState } from './reducers/fileSystem
import { fetchDirectory, init, resolveDirectory, addInputField, removeInputField } from './actions/fileSystem'
import * as helper from '../../../../../apps/remix-ide/src/lib/helper'
import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
import './css/file-explorer.css'
const queryParams = new QueryParams()
export const FileExplorer = (props: FileExplorerProps) => {
const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads } = props
const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads, removedContextMenuItems } = props
const [state, setState] = useState({
focusElement: [{
key: '',
@ -203,6 +204,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}, [contextMenuItems])
useEffect(() => {
if (removedContextMenuItems) {
removeMenuItems(removedContextMenuItems)
}
}, [contextMenuItems])
useEffect(() => {
if (displayInput) {
handleNewFileInput()
@ -278,7 +285,15 @@ export const FileExplorer = (props: FileExplorerProps) => {
multiselect: false
}])
} else {
removeMenuItems(['paste'])
removeMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: false
}])
}
}, [canPaste])
@ -291,10 +306,9 @@ export const FileExplorer = (props: FileExplorerProps) => {
})
}
const removeMenuItems = (ids: string[]) => {
const removeMenuItems = (items: MenuItems) => {
setState(prevState => {
const actions = prevState.actions.filter(({ id }) => ids.findIndex(value => value === id) === -1)
const actions = prevState.actions.filter(({ id, name }) => items.findIndex(item => id === item.id && name === item.name) === -1)
return { ...prevState, actions }
})
}
@ -601,8 +615,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
})
}
const emitContextMenuEvent = (id: string, path: string | string[]) => {
plugin.emit(id, path)
const emitContextMenuEvent = (cmd: customAction) => {
plugin.call(cmd.id, cmd.name, cmd)
}
const handleHideModal = () => {

@ -1,3 +1,5 @@
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
/* eslint-disable-next-line */
export interface FileExplorerProps {
name: string,
@ -7,8 +9,9 @@ export interface FileExplorerProps {
plugin: any,
focusRoot: boolean,
contextMenuItems: MenuItems,
removedContextMenuItems: MenuItems,
displayInput?: boolean,
externalUploads?: EventTarget & HTMLInputElement
externalUploads?: EventTarget & HTMLInputElement,
}
export interface File {
@ -44,7 +47,7 @@ export interface FileExplorerContextMenuProps {
publishFolderToGist?: (path?: string, type?: string) => void,
publishFileToGist?: (path?: string, type?: string) => void,
runScript?: (path: string) => void,
emit?: (id: string, path: string | string[]) => void,
emit?: (cmd: customAction) => void,
pageX: number,
pageY: number,
path: string,

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

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

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

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

@ -0,0 +1,12 @@
export const generateContractMetadataText = 'Generate contract metadata. Generate a JSON file in the contract folder. Allows to specify library addresses the contract depends on. If nothing is specified, Remix deploys libraries automatically.'
export const textSecondary = 'text-secondary'
export const textDark = 'text-dark'
export const warnText = 'Be sure the endpoint is opened before enabling it. \nThis mode allows a user to provide a passphrase in the Remix interface without having to unlock the account. Although this is very convenient, you should completely trust the backend you are connected to (Geth, Parity, ...). Remix never persists any passphrase'.split('\n').map(s => s.trim()).join(' ')
export const gitAccessTokenTitle = 'Github Access Token'
export const gitAccessTokenText = 'Manage the access token used to publish to Gist and retrieve Github contents.'
export const gitAccessTokenText2 = 'Go to github token page (link below) to create a new token and save it in Remix. Make sure this token has only \'create gist\' permission.'
export const gitAccessTokenLink = 'https://github.com/settings/tokens'
export const ethereunVMText = 'Always use Ethereum VM at load'
export const wordWrapText = 'Word wrap in editor'
export const enablePersonalModeText = ' Enable Personal Mode for web3 provider. Transaction sent over Web3 will use the web3.personal API.\n'
export const matomoAnalytics = 'Enable Matomo Analytics. We do not collect personally identifiable information (PII). The info is used to improve the site’s UX & UI. See more about '

@ -0,0 +1,167 @@
import React, { useState, useReducer, useEffect, useCallback } from 'react' // eslint-disable-line
import { CopyToClipboard } from '@remix-ui/clipboard' // eslint-disable-line
import { enablePersonalModeText, ethereunVMText, generateContractMetadataText, gitAccessTokenLink, gitAccessTokenText, gitAccessTokenText2, gitAccessTokenTitle, matomoAnalytics, textDark, textSecondary, warnText, wordWrapText } from './constants'
import './remix-ui-settings.css'
import { etherumVM, generateContractMetadat, personal, textWrapEventAction, useMatomoAnalytics, saveTokenToast, removeTokenToast } from './settingsAction'
import { initialState, toastInitialState, toastReducer, settingReducer } from './settingsReducer'
import { Toaster } from '@remix-ui/toaster'// eslint-disable-line
/* eslint-disable-next-line */
export interface RemixUiSettingsProps {
config: any,
editor: any,
_deps: any
}
export const RemixUiSettings = (props: RemixUiSettingsProps) => {
const [, dispatch] = useReducer(settingReducer, initialState)
const [state, dispatchToast] = useReducer(toastReducer, toastInitialState)
const [tokenValue, setTokenValue] = useState('')
const [themeName, setThemeName] = useState('')
useEffect(() => {
props._deps.themeModule.switchTheme()
const token = props.config.get('settings/gist-access-token')
if (token === undefined) {
props.config.set('settings/generate-contract-metadata', true)
dispatch({ type: 'contractMetadata', payload: { name: 'contractMetadata', isChecked: true, textClass: textDark } })
}
if (token) {
setTokenValue(token)
}
}, [themeName, state.message])
const onchangeGenerateContractMetadata = (event) => {
generateContractMetadat(props, event, dispatch)
}
const onchangeOption = (event) => {
etherumVM(props, event, dispatch)
}
const textWrapEvent = (event) => {
textWrapEventAction(props, event, dispatch)
}
const onchangePersonal = event => {
personal(props, event, dispatch)
}
const onchangeMatomoAnalytics = event => {
useMatomoAnalytics(props, event, dispatch)
}
const onswitchTheme = (event, name) => {
props._deps.themeModule.switchTheme(name)
setThemeName(name)
}
const getTextClass = (key) => {
if (props.config.get(key)) {
return textDark
} else {
return textSecondary
}
}
const generalConfig = () => (
<div className="$border-top">
<div className="card-body pt-3 pb-2">
<h6 className="card-title">General settings</h6>
<div className="mt-2 custom-control custom-checkbox mb-1">
<input onChange={onchangeGenerateContractMetadata} id="generatecontractmetadata" data-id="settingsTabGenerateContractMetadata" type="checkbox" className="custom-control-input" name="contractMetadata" checked = { props.config.get('settings/generate-contract-metadata') }/>
<label className={`form-check-label custom-control-label align-middle ${getTextClass('settings/generate-contract-metadata')}`} data-id="settingsTabGenerateContractMetadataLabel" htmlFor="generatecontractmetadata">{generateContractMetadataText}</label>
</div>
<div className="fmt-2 custom-control custom-checkbox mb-1">
<input onChange={onchangeOption} className="custom-control-input" id="alwaysUseVM" data-id="settingsTabAlwaysUseVM" type="checkbox" name="ethereumVM" checked={ props.config.get('settings/always-use-vm') }/>
<label className={`form-check-label custom-control-label align-middle ${getTextClass('settings/always-use-vm')}`} htmlFor="alwaysUseVM">{ethereunVMText}</label>
</div>
<div className="mt-2 custom-control custom-checkbox mb-1">
<input id="editorWrap" className="custom-control-input" type="checkbox" onChange={textWrapEvent} checked = { props.config.get('settings/text-wrap')}/>
<label className={`form-check-label custom-control-label align-middle ${getTextClass('settings/text-wrap')}`} htmlFor="editorWrap">{wordWrapText}</label>
</div>
<div className="custom-control custom-checkbox mb-1">
<input onChange={onchangePersonal} id="personal" type="checkbox" className="custom-control-input" checked = { props.config.get('settings/personal-mode')}/>
<label className={`form-check-label custom-control-label align-middle ${getTextClass('settings/personal-mode')}`} htmlFor="personal">
<i className="fas fa-exclamation-triangle text-warning" aria-hidden="true"></i> <span> </span>
<span> </span>{enablePersonalModeText} {warnText}
</label>
</div>
<div className="custom-control custom-checkbox mb-1">
<input onChange={onchangeMatomoAnalytics} id="settingsMatomoAnalytics" type="checkbox" className="custom-control-input" checked={ props.config.get('settings/matomo-analytics')}/>
<label className={`form-check-label custom-control-label align-middle ${getTextClass('settings/matomo-analytics')}`} htmlFor="settingsMatomoAnalytics">
<span>{matomoAnalytics}</span>
<a href="https://medium.com/p/66ef69e14931/" target="_blank"> Analytics in Remix IDE</a> <span>&</span> <a target="_blank" href="https://matomo.org/free-software">Matomo</a>
</label>
</div>
</div>
</div>
)
const saveToken = () => {
saveTokenToast(props, dispatchToast, tokenValue)
}
const removeToken = () => {
setTokenValue('')
removeTokenToast(props, dispatchToast)
}
const handleSaveTokenState = useCallback(
(event) => {
setTokenValue(event.target.value)
},
[tokenValue]
)
const gistToken = () => (
<div className="border-top">
<div className="card-body pt-3 pb-2">
<h6 className="card-title">{ gitAccessTokenTitle }</h6>
<p className="mb-1">{ gitAccessTokenText }</p>
<p className="">{ gitAccessTokenText2 }</p>
<p className="mb-1"><a className="text-primary" target="_blank" href="https://github.com/settings/tokens">{ gitAccessTokenLink }</a></p>
<div className=""><label>TOKEN:</label>
<div className="text-secondary mb-0 h6">
<input id="gistaccesstoken" data-id="settingsTabGistAccessToken" type="password" className="form-control" onChange={handleSaveTokenState} value={ tokenValue } />
<div className="d-flex justify-content-end pt-2">
<CopyToClipboard content={tokenValue} data-id='copyToClipboardCopyIcon' />
<input className="btn btn-sm btn-primary ml-2" id="savegisttoken" data-id="settingsTabSaveGistToken" onClick={() => saveToken()} value="Save" type="button" disabled={tokenValue === ''}></input>
<button className="btn btn-sm btn-secondary ml-2" id="removegisttoken" data-id="settingsTabRemoveGistToken" title="Delete Github access token" onClick={() => removeToken()}>Remove</button>
</div>
</div></div>
</div>
</div>
)
const themes = () => {
const themes = props._deps.themeModule.getThemes()
if (themes) {
return themes.map((aTheme, index) => (
<div className="radio custom-control custom-radio mb-1 form-check" key={index}>
<input type="radio" onChange={event => { onswitchTheme(event, aTheme.name) }} className="align-middle custom-control-input" name='theme' id={aTheme.name} data-id={`settingsTabTheme${aTheme.name}`} checked = {props._deps.themeModule.active === aTheme.name }/>
<label className="form-check-label custom-control-label" data-id={`settingsTabThemeLabel${aTheme.name}`} htmlFor={aTheme.name}>{aTheme.name} ({aTheme.quality})</label>
</div>
)
)
}
}
return (
<div>
{state.message ? <Toaster message= {state.message}/> : null}
{generalConfig()}
{gistToken()}
<div className="border-top">
<div className="card-body pt-3 pb-2">
<h6 className="card-title">Themes</h6>
<div className="card-text themes-container">
{themes()}
</div>
</div>
</div>
</div>
)
}

@ -0,0 +1,52 @@
import { textDark, textSecondary } from './constants'
declare global {
interface Window {
_paq: any
}
}
const _paq = window._paq = window._paq || [] //eslint-disable-line
export const generateContractMetadat = (element, event, dispatch) => {
element.config.set('settings/generate-contract-metadata', event.target.checked)
dispatch({ type: 'contractMetadata', payload: { name: event.target.name, isChecked: event.target.checked, textClass: event.target.checked ? textDark : textSecondary } })
}
export const etherumVM = (element, event, dispatch) => {
element.config.set('settings/always-use-vm', event.target.checked)
dispatch({ type: 'ethereumVM', payload: { name: event.target.name, isChecked: event.target.checked, textClass: event.target.checked ? textDark : textSecondary } })
}
export const textWrapEventAction = (element, event, dispatch) => {
element.config.set('settings/text-wrap', event.target.checked)
element.editor.resize(event.target.checked)
dispatch({ type: 'textWrap', payload: { name: event.target.name, isChecked: event.target.checked, textClass: event.target.checked ? textDark : textSecondary } })
}
export const personal = (element, event, dispatch) => {
element.config.set('settings/personal-mode', event.target.checked)
dispatch({ type: 'personal', payload: { name: event.target.name, isChecked: event.target.checked, textClass: event.target.checked ? textDark : textSecondary } })
}
export const useMatomoAnalytics = (element, event, dispatch) => {
element.config.set('settings/matomo-analytics', event.target.checked)
dispatch({ type: 'useMatomoAnalytics', payload: { name: event.target.name, isChecked: event.target.checked, textClass: event.target.checked ? textDark : textSecondary } })
if (event.target.checked) {
_paq.push(['forgetUserOptOut'])
// @TODO remove next line when https://github.com/matomo-org/matomo/commit/9e10a150585522ca30ecdd275007a882a70c6df5 is used
document.cookie = 'mtm_consent_removed=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
} else {
_paq.push(['optUserOut'])
}
}
export const saveTokenToast = (props, dispatch, tokenValue) => {
props.config.set('settings/gist-access-token', tokenValue)
dispatch({ type: 'save', payload: { message: 'Access token has been saved' } })
}
export const removeTokenToast = (props, dispatch) => {
props.config.set('settings/gist-access-token', '')
dispatch({ type: 'removed', payload: { message: 'Access token removed' } })
}

@ -0,0 +1,103 @@
import { textSecondary } from './constants'
export const initialState = {
elementState: [
{
name: 'contractMetadata',
isChecked: false,
textClass: textSecondary
},
{
name: 'ethereumVM',
isChecked: false,
textClass: textSecondary
},
{
name: 'textWrap',
isChecked: false,
textClass: textSecondary
},
{
name: 'personal',
isChecked: false,
textClass: textSecondary
},
{
name: 'useMatomoAnalytics',
isChecked: false,
textClass: textSecondary
}
]
}
export const settingReducer = (state, action) => {
switch (action.type) {
case 'contractMetadata':
state.elementState.map(element => {
if (element.name === 'contractMetadata') {
element.isChecked = action.payload.isChecked
element.textClass = action.payload.textClass
}
})
return {
...state
}
case 'ethereumVM':
state.elementState.map(element => {
if (element.name === 'ethereumVM') {
element.isChecked = action.payload.isChecked
element.textClass = action.payload.textClass
}
})
return {
...state
}
case 'textWrap':
state.elementState.map(element => {
if (element.name === 'textWrap') {
element.isChecked = action.payload.isChecked
element.textClass = action.payload.textClass
}
})
return {
...state
}
case 'personal':
state.elementState.map(element => {
if (element.name === 'personal') {
element.isChecked = action.payload.isChecked
element.textClass = action.payload.textClass
}
})
return {
...state
}
case 'useMatomoAnalytics':
state.elementState.map(element => {
if (element.name === 'useMatomoAnalytics') {
element.isChecked = action.payload.isChecked
element.textClass = action.payload.textClass
}
})
return {
...state
}
default:
return initialState
}
}
export const toastInitialState = {
message: ''
}
export const toastReducer = (state, action) => {
switch (action.type) {
case 'save' :
return { ...state, message: action.payload.message }
case 'removed' :
return { ...state, message: action.payload.message }
default :
return { ...state, message: '' }
}
}

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

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

@ -3,6 +3,7 @@ import { FileExplorer } from '@remix-ui/file-explorer' // eslint-disable-line
import './remix-ui-workspace.css'
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster'// eslint-disable-line
import { MenuItems } from 'libs/remix-ui/file-explorer/src/lib/types'
/* eslint-disable-next-line */
export interface WorkspaceProps {
@ -20,7 +21,8 @@ export interface WorkspaceProps {
plugin: any // plugin call and resetFocus
request: any // api request,
workspaces: any,
registeredMenuItems: [] // menu items
registeredMenuItems: MenuItems // menu items
removedMenuItems: MenuItems
initialWorkspace: string
}
@ -409,6 +411,7 @@ export const Workspace = (props: WorkspaceProps) => {
plugin={props.plugin}
focusRoot={state.reset}
contextMenuItems={props.registeredMenuItems}
removedContextMenuItems={props.removedMenuItems}
displayInput={state.displayNewFile}
externalUploads={state.uploadFileEvent}
/>
@ -426,6 +429,7 @@ export const Workspace = (props: WorkspaceProps) => {
plugin={props.plugin}
focusRoot={state.reset}
contextMenuItems={props.registeredMenuItems}
removedContextMenuItems={props.removedMenuItems}
/>
}
</div>

@ -26,18 +26,20 @@ If you were using the old one you need to:
## HELP SECTION
```
Usage: remixd -s <shared folder> --remix-ide https://remix.ethereum.org
Usage: remixd -s <shared folder>
Provide a two-way connection between the local computer and Remix IDE.
Provide a two-way connection between the local computer and Remix IDE
Options:
Options:
-v, --version output the version number
-u, --remix-ide <url> URL of remix instance allowed to connect to this web sockect connection
-s, --shared-folder <path> Folder to share with Remix IDE
-r, --read-only Treat shared folder as read-only (experimental)
-h, --help output usage information
--remix-ide <url> URL of remix instance allowed to connect to this
web sockect connection
-s, --shared-folder <path> Folder to share with Remix IDE
--read-only Treat shared folder as read-only (experimental)
-h, --help output usage information
Example:
remixd -s ./ -u http://localhost:8080
```

@ -58,11 +58,11 @@ function errorHandler (error: any, service: string) {
program
.usage('-s <shared folder>')
.description('Provide a two-way connection between the local computer and Remix IDE')
.option('--remix-ide <url>', 'URL of remix instance allowed to connect to this web sockect connection')
.option('-u, --remix-ide <url>', 'URL of remix instance allowed to connect to this web sockect connection')
.option('-s, --shared-folder <path>', 'Folder to share with Remix IDE')
.option('--read-only', 'Treat shared folder as read-only (experimental)')
.option('-r, --read-only', 'Treat shared folder as read-only (experimental)')
.on('--help', function () {
console.log('\nExample:\n\n remixd -s ./ --remix-ide http://localhost:8080')
console.log('\nExample:\n\n remixd -s ./ -u http://localhost:8080')
}).parse(process.argv)
// eslint-disable-next-line

@ -96,11 +96,17 @@
"remix-ui-workspace": {
"tags": []
},
"remix-ui-settings": {
"tags": []
},
"remix-ui-static-analyser": {
"tags": []
},
"remix-ui-checkbox": {
"tags": []
},
"remix-ui-settings": {
"tags": []
}
}
}

11
package-lock.json generated

@ -8443,9 +8443,9 @@
"dev": true
},
"ace-mode-zokrates": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ace-mode-zokrates/-/ace-mode-zokrates-1.0.1.tgz",
"integrity": "sha512-+rTOLj1AJzV/XRXsMLNkWIjNQCIa8TYjWRunCTGJ620iUy7WRlMkU7uVRydq//t4GUdr0j2TkNM0fSqVs0zNWw==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/ace-mode-zokrates/-/ace-mode-zokrates-1.0.4.tgz",
"integrity": "sha512-jLpIg+PhJTlCWKu52U/EdJPQPJez9mMB0uzvCiyHgCJsX6+FY+s7jmBDrpxGdgNdNWJPQ20/MKzOx3oUnSF27A==",
"dev": true
},
"acorn": {
@ -36072,6 +36072,11 @@
}
}
},
"string-similarity": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz",
"integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ=="
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",

@ -41,7 +41,7 @@
"workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph",
"help": "nx help",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-file-explorer,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-file-explorer,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings",
"build:libs": "nx run-many --target=build --parallel=false --with-deps=true --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"test:libs": "nx run-many --target=test --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"publish:libs": "npm run build:libs && lerna publish --skip-git && npm run bumpVersion:libs",
@ -163,7 +163,6 @@
"jquery": "^3.3.1",
"jszip": "^3.6.0",
"latest-version": "^5.1.0",
"lodash": "^4.17.21",
"merge": "^1.2.0",
"npm-install-version": "^6.0.2",
"react": "16.13.1",
@ -172,12 +171,13 @@
"react-dom": "16.13.1",
"selenium": "^2.20.0",
"signale": "^1.4.0",
"string-similarity": "^4.0.4",
"time-stamp": "^2.2.0",
"tslib": "^2.3.0",
"web3": "1.2.4",
"winston": "^3.3.3",
"ws": "^7.3.0"
},
},
"devDependencies": {
"@babel/core": "^7.4.5",
"@babel/plugin-transform-modules-amd": "^7.10.4",
@ -219,7 +219,7 @@
"ace-mode-lexon": "^1.*.*",
"ace-mode-move": "0.0.1",
"ace-mode-solidity": "^0.1.0",
"ace-mode-zokrates": "^1.0.0",
"ace-mode-zokrates": "^1.0.4",
"babel-eslint": "^10.0.0",
"babel-jest": "25.1.0",
"babel-plugin-add-module-exports": "^1.0.2",

@ -39,8 +39,10 @@
"@remix-ui/toaster": ["libs/remix-ui/toaster/src/index.ts"],
"@remix-ui/file-explorer": ["libs/remix-ui/file-explorer/src/index.ts"],
"@remix-ui/workspace": ["libs/remix-ui/workspace/src/index.ts"],
"@remix-ui/settings": ["libs/remix-ui/settings/src/index.ts"],
"@remix-ui/static-analyser": ["libs/remix-ui/static-analyser/src/index.ts"],
"@remix-ui/checkbox": ["libs/remix-ui/checkbox/src/index.ts"]
"@remix-ui/checkbox": ["libs/remix-ui/checkbox/src/index.ts"],
"@remix-ui/settings": ["libs/remix-ui/settings/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]

@ -726,6 +726,22 @@
}
}
},
"remix-ui-settings": {
"root": "libs/remix-ui/settings",
"sourceRoot": "libs/remix-ui/settings/src",
"projectType": "library",
"schematics": {},
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"tsConfig": ["libs/remix-ui/settings/tsconfig.lib.json"],
"exclude": ["**/node_modules/**", "!libs/remix-ui/settings/**/*"]
}
}
}
},
"remix-ui-static-analyser": {
"root": "libs/remix-ui/static-analyser",
"sourceRoot": "libs/remix-ui/static-analyser/src",
@ -828,4 +844,4 @@
}
},
"defaultProject": "remix-ide"
}
}
Loading…
Cancel
Save