Merge branch 'master' into fixrefresh

pull/5370/head
bunsenstraat 3 years ago committed by GitHub
commit 88404ec925
  1. 10
      apps/remix-ide-e2e/src/commands/journalChildIncludes.ts
  2. 23
      apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts
  3. 83
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  4. 2
      apps/remix-ide-e2e/src/types/index.d.ts
  5. 13
      apps/remix-ide/src/app/editor/editor.js
  6. 5
      apps/remix-ide/src/app/files/fileProvider.js
  7. 50
      apps/remix-ide/src/app/panels/tab-proxy.js
  8. 22
      apps/remix-ide/src/app/panels/terminal.js
  9. 7
      apps/remix-ide/src/app/tabs/runTab/contractDropdown.js
  10. 30
      apps/remix-ide/src/app/tabs/test-tab.js
  11. 9
      apps/remix-ide/src/app/tabs/theme-module.js
  12. 2
      apps/remix-ide/src/assets/js/react-tabs.production.min.js
  13. 2
      apps/remix-ide/webpack.config.js
  14. 10
      apps/solidity-compiler/src/app/compiler-api.ts
  15. 2
      libs/remix-core-plugin/src/lib/compiler-content-imports.ts
  16. 15
      libs/remix-lib/src/execution/txRunnerVM.ts
  17. 4
      libs/remix-lib/src/web3Provider/web3VmProvider.ts
  18. 2
      libs/remix-tests/sol/tests_accounts.sol.ts
  19. 11
      libs/remix-tests/src/compiler.ts
  20. 10
      libs/remix-tests/src/deployer.ts
  21. 1
      libs/remix-tests/src/index.ts
  22. 4
      libs/remix-tests/src/runTestFiles.ts
  23. 40
      libs/remix-tests/src/runTestSources.ts
  24. 2
      libs/remix-tests/tests/testRunner.spec.ts
  25. 70
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  26. 1384
      libs/remix-ui/editor/src/lib/syntax.ts
  27. 12
      libs/remix-ui/tabs/src/lib/remix-ui-tabs.css
  28. 37
      libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx
  29. 2
      libs/remix-ui/terminal/src/lib/components/Context.tsx
  30. 2
      libs/remix-ui/terminal/src/lib/components/RenderKnownTransactions.tsx
  31. 286
      libs/remix-ui/terminal/src/lib/components/Table.tsx
  32. 19
      libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx
  33. 3
      libs/remix-ui/terminal/src/lib/types/terminalTypes.ts

@ -5,10 +5,10 @@ import EventEmitter from 'events'
Checks if any child elements of journal (console) contains a matching value. Checks if any child elements of journal (console) contains a matching value.
*/ */
class JournalChildIncludes extends EventEmitter { class JournalChildIncludes extends EventEmitter {
command (this: NightwatchBrowser, val: string): NightwatchBrowser { command (this: NightwatchBrowser, val: string, opts = { shouldHaveOnlyOneOccurence: false }): NightwatchBrowser {
let isTextFound = false let isTextFound = false
const browser = this.api const browser = this.api
let occurence = 0
this.api.elements('css selector', '*[data-id="terminalJournal"]', (res) => { this.api.elements('css selector', '*[data-id="terminalJournal"]', (res) => {
Array.isArray(res.value) && res.value.forEach(function (jsonWebElement) { Array.isArray(res.value) && res.value.forEach(function (jsonWebElement) {
const jsonWebElementId = jsonWebElement.ELEMENT || jsonWebElement[Object.keys(jsonWebElement)[0]] const jsonWebElementId = jsonWebElement.ELEMENT || jsonWebElement[Object.keys(jsonWebElement)[0]]
@ -16,12 +16,16 @@ class JournalChildIncludes extends EventEmitter {
browser.elementIdText(jsonWebElementId, (jsonElement) => { browser.elementIdText(jsonWebElementId, (jsonElement) => {
const text = jsonElement.value const text = jsonElement.value
if (typeof text === 'string' && text.indexOf(val) !== -1) isTextFound = true if (typeof text === 'string' && text.indexOf(val) !== -1) {
isTextFound = true
occurence++
}
}) })
}) })
}) })
browser.perform(() => { browser.perform(() => {
browser.assert.ok(isTextFound, isTextFound ? `<*[data-id="terminalJournal"]> contains ${val}.` : `${val} not found in <*[data-id="terminalJournal"]> div:last-child>`) browser.assert.ok(isTextFound, isTextFound ? `<*[data-id="terminalJournal"]> contains ${val}.` : `${val} not found in <*[data-id="terminalJournal"]> div:last-child>`)
if (opts.shouldHaveOnlyOneOccurence) browser.assert.ok(occurence === 1, `${occurence} occurence found of "${val}"`)
this.emit('complete') this.emit('complete')
}) })
return this return this

@ -13,7 +13,7 @@ module.exports = {
return sources return sources
}, },
'Should launch solidity unit test plugin': function (browser: NightwatchBrowser) { 'Should launch solidity unit test plugin and create test files in FE': function (browser: NightwatchBrowser) {
browser.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') browser.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]')
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.addFile('simple_storage.sol', sources[0]['simple_storage.sol']) .addFile('simple_storage.sol', sources[0]['simple_storage.sol'])
@ -23,6 +23,15 @@ module.exports = {
.click('*[data-id="verticalIconsKindsolidityUnitTesting"]') .click('*[data-id="verticalIconsKindsolidityUnitTesting"]')
.waitForElementPresent('*[data-id="sidePanelSwapitTitle"]') .waitForElementPresent('*[data-id="sidePanelSwapitTitle"]')
.assert.containsText('*[data-id="sidePanelSwapitTitle"]', 'SOLIDITY UNIT TESTING') .assert.containsText('*[data-id="sidePanelSwapitTitle"]', 'SOLIDITY UNIT TESTING')
.clickLaunchIcon('filePanel')
.waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_tests.sol"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_accounts.sol"]')
.openFile('.deps/remix-tests/remix_tests.sol')
// remix_test.sol should be opened in editor
.getEditorValue((content) => browser.assert.ok(content.indexOf('library Assert {') !== -1))
.openFile('.deps/remix-tests/remix_accounts.sol')
// remix_accounts.sol should be opened in editor
.getEditorValue((content) => browser.assert.ok(content.indexOf('library TestsAccounts {') !== -1))
}, },
'Should generate test file': function (browser: NightwatchBrowser) { 'Should generate test file': function (browser: NightwatchBrowser) {
@ -150,7 +159,7 @@ module.exports = {
.click('*[data-id="testTabGenerateTestFolder"]') .click('*[data-id="testTabGenerateTestFolder"]')
}, },
'Changing current path when workspace changed': function (browser: NightwatchBrowser) { 'Changing current path when workspace changed and checking test files creation': function (browser: NightwatchBrowser) {
browser browser
.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') .waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]')
.clickLaunchIcon('settings') .clickLaunchIcon('settings')
@ -168,6 +177,14 @@ module.exports = {
.waitForElementVisible('*[data-id="fileSystem-modal-footer-ok-react"]') .waitForElementVisible('*[data-id="fileSystem-modal-footer-ok-react"]')
.execute(function () { (document.querySelector('[data-id="fileSystem-modal-footer-ok-react"]') as HTMLElement).click() }) .execute(function () { (document.querySelector('[data-id="fileSystem-modal-footer-ok-react"]') as HTMLElement).click() })
.waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_new"]') .waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_new"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_tests.sol"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_accounts.sol"]')
.openFile('.deps/remix-tests/remix_tests.sol')
// remix_test.sol should be opened in editor
.getEditorValue((content) => browser.assert.ok(content.indexOf('library Assert {') !== -1))
.openFile('.deps/remix-tests/remix_accounts.sol')
// remix_accounts.sol should be opened in editor
.getEditorValue((content) => browser.assert.ok(content.indexOf('library TestsAccounts {') !== -1))
// end of creating // end of creating
.clickLaunchIcon('solidityUnitTesting') .clickLaunchIcon('solidityUnitTesting')
.pause(2000) .pause(2000)
@ -276,6 +293,8 @@ module.exports = {
.setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW))
.waitForElementContainsText('*[data-id="functionPanel"]', 'equal(a, b, message)', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'equal(a, b, message)', 60000)
.waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalPassed()', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalPassed()', 60000)
// remix_test.sol should be opened in editor
.getEditorValue((content) => browser.assert.ok(content.indexOf('library Assert {') !== -1))
.pause(1000) .pause(1000)
.clickLaunchIcon('solidityUnitTesting') .clickLaunchIcon('solidityUnitTesting')
.scrollAndClick('#Check_winning_proposal_again') .scrollAndClick('#Check_winning_proposal_again')

@ -128,6 +128,31 @@ module.exports = {
.waitForElementContainsText('*[data-id="terminalJournal"]', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', 60000) // check that the script is logging the event .waitForElementContainsText('*[data-id="terminalJournal"]', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', 60000) // check that the script is logging the event
.waitForElementContainsText('*[data-id="terminalJournal"]', 'newOwner', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', 'newOwner', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', '0xd9145CCE52D386f254917e481eB44e9943F39138', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', '0xd9145CCE52D386f254917e481eB44e9943F39138', 60000)
},
'Should print hardhat logs': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="terminalClearConsole"]') // clear the terminal
.addFile('printHardhatlog.sol', { content: hardhatLog })
.clickLaunchIcon('solidity')
.waitForElementVisible('[for="autoCompile"]')
.click('[for="autoCompile"]')
.testContracts('printHardhatlog.sol', { content: hardhatLog }, ['OwnerTest'])
.clickLaunchIcon('udapp')
.click('*[data-id="deployAndRunClearInstances"]')
.selectContract('OwnerTest')
.createContract('')
.pause(1000)
.journalChildIncludes('constructor', { shouldHaveOnlyOneOccurence: true })
.pause(5000)
.click('*[data-id="terminalClearConsole"]') // clear the terminal
.clickInstance(0)
.clickFunction('changeOwner - transact (not payable)', { types: 'address newOwner', values: '0xd9145CCE52D386f254917e481eB44e9943F39138' })
.pause(1000)
.journalChildIncludes('inside changeOwner', { shouldHaveOnlyOneOccurence: true })
.clickFunction('getOwner - call')
.pause(1000)
.journalChildIncludes('inside getOwner', { shouldHaveOnlyOneOccurence: true })
.end() .end()
} }
} }
@ -238,3 +263,61 @@ const deployWithEthersJs = `
console.log(e.message) console.log(e.message)
} }
})()` })()`
const hardhatLog = `
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
/**
* @title Owner
* @dev Set & change owner
*/
contract OwnerTest {
address private owner;
// event for EVM logging
event OwnerSet(address indexed oldOwner, address indexed newOwner);
// modifier to check if caller is owner
modifier isOwner() {
// If the first argument of 'require' evaluates to 'false', execution terminates and all
// changes to the state and to Ether balances are reverted.
// This used to consume all gas in old EVM versions, but not anymore.
// It is often a good idea to use 'require' to check if functions are called correctly.
// As a second argument, you can also provide an explanation about what went wrong.
require(msg.sender == owner, "Caller is not owner");
_;
}
/**
* @dev Set contract deployer as owner
*/
constructor() {
console.log("constructor");
owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor
emit OwnerSet(address(0), owner);
}
/**
* @dev Change owner
* @param newOwner address of new owner
*/
function changeOwner(address newOwner) public isOwner {
console.log("inside changeOwner");
emit OwnerSet(owner, newOwner);
owner = newOwner;
}
/**
* @dev Return owner address
* @return address of owner
*/
function getOwner() external view returns (address) {
console.log("inside getOwner");
return owner;
}
}`

@ -23,7 +23,7 @@ declare module 'nightwatch' {
journalLastChildIncludes(val: string): NightwatchBrowser, journalLastChildIncludes(val: string): NightwatchBrowser,
executeScript(script: string): NightwatchBrowser, executeScript(script: string): NightwatchBrowser,
clearEditableContent(cssSelector: string): NightwatchBrowser, clearEditableContent(cssSelector: string): NightwatchBrowser,
journalChildIncludes(val: string): NightwatchBrowser, journalChildIncludes(val: string, opts = { shouldHaveOnlyOneOccurence: boolean }): NightwatchBrowser,
debugTransaction(index: number): NightwatchBrowser, debugTransaction(index: number): NightwatchBrowser,
checkElementStyle(cssSelector: string, styleProperty: string, expectedResult: string): NightwatchBrowser, checkElementStyle(cssSelector: string, styleProperty: string, expectedResult: string): NightwatchBrowser,
openFile(name: string): NightwatchBrowser, openFile(name: string): NightwatchBrowser,

@ -97,7 +97,7 @@ class Editor extends Plugin {
this.emit(name, ...params) // plugin stack this.emit(name, ...params) // plugin stack
} }
onActivation () { async onActivation () {
this.activated = true this.activated = true
this.on('sidePanel', 'focusChanged', (name) => { this.on('sidePanel', 'focusChanged', (name) => {
this.keepDecorationsFor(name, 'sourceAnnotationsPerFile') this.keepDecorationsFor(name, 'sourceAnnotationsPerFile')
@ -108,14 +108,15 @@ class Editor extends Plugin {
}) })
const translateTheme = (theme) => this._themes[theme.name === 'Dark' ? 'remixDark' : theme.quality] const translateTheme = (theme) => this._themes[theme.name === 'Dark' ? 'remixDark' : theme.quality]
this.on('theme', 'themeChanged', (theme) => { this.on('theme', 'themeLoaded', (theme) => {
this.currentTheme = translateTheme(theme)
this.renderComponent()
})
this.call('theme', 'currentTheme', (theme) => {
this.currentTheme = translateTheme(theme) this.currentTheme = translateTheme(theme)
this.renderComponent() this.renderComponent()
}) })
try {
this.currentTheme = translateTheme(await this.call('theme', 'currentTheme'))
} catch (e) {
console.log('unable to select the theme ' + e.message)
}
this.renderComponent() this.renderComponent()
} }

@ -17,8 +17,9 @@ class FileProvider {
} }
addNormalizedName (path, url) { addNormalizedName (path, url) {
this.providerExternalsStorage.set(this.type + '/' + path, url) if (this.type) path = this.type + '/' + path
this.providerExternalsStorage.set(this.reverseKey + url, this.type + '/' + path) this.providerExternalsStorage.set(path, url)
this.providerExternalsStorage.set(this.reverseKey + url, path)
} }
removeNormalizedName (path) { removeNormalizedName (path) {

@ -191,13 +191,6 @@ export class TabProxy extends Plugin {
} }
} }
switchToActiveTab () {
const active = this.tabsApi.active()
if (active && this._handlers[active]) {
this.switchTab(active)
}
}
renameTab (oldName, newName) { renameTab (oldName, newName) {
this.addTab(newName, '', () => { this.addTab(newName, '', () => {
this.fileManager.open(newName) this.fileManager.open(newName)
@ -236,19 +229,21 @@ export class TabProxy extends Plugin {
}) })
formatPath.shift() formatPath.shift()
if (formatPath.length > 0) { if (formatPath.length > 0) {
const duplicateTabName = this.loadedTabs.find(({ title }) => title === formatPath.join('/')).name const index = this.loadedTabs.findIndex(({ title }) => title === formatPath.join('/'))
const duplicateTabPath = duplicateTabName.split('/') if (index > -1) {
const duplicateTabFormatPath = [...duplicateTabPath].reverse() const duplicateTabName = this.loadedTabs[index].name
const duplicateTabTitle = duplicateTabFormatPath.slice(0, titleLength).reverse().join('/') const duplicateTabPath = duplicateTabName.split('/')
const duplicateTabFormatPath = [...duplicateTabPath].reverse()
this.loadedTabs.push({ const duplicateTabTitle = duplicateTabFormatPath.slice(0, titleLength).reverse().join('/')
id: duplicateTabName, this.loadedTabs[index] = {
name: duplicateTabName, id: duplicateTabName,
title: duplicateTabTitle, name: duplicateTabName,
icon, title: duplicateTabTitle,
tooltip: duplicateTabName, icon,
iconClass: helper.getPathIcon(duplicateTabName) tooltip: duplicateTabName,
}) iconClass: helper.getPathIcon(duplicateTabName)
}
}
} }
break break
} }
@ -271,10 +266,14 @@ export class TabProxy extends Plugin {
removeTab (name) { removeTab (name) {
delete this._handlers[name] delete this._handlers[name]
this.switchToActiveTab() let previous = null
this.loadedTabs = this.loadedTabs.filter(tab => tab.name !== name) this.loadedTabs = this.loadedTabs.filter((tab, index) => {
if (tab.name === name) previous = this.loadedTabs[index - 1]
return tab.name !== name
})
this.renderComponent() this.renderComponent()
this.updateImgStyles() this.updateImgStyles()
if (previous) this.switchTab(previous.name)
} }
addHandler (type, fn) { addHandler (type, fn) {
@ -309,8 +308,13 @@ export class TabProxy extends Plugin {
} }
renderTabsbar () { renderTabsbar () {
window.React = React
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'assets/js/react-tabs.production.min.js'
document.head.appendChild(script)
script.addEventListener('load', () => this.renderComponent())
this.el = document.createElement('div') this.el = document.createElement('div')
this.renderComponent()
return this.el return this.el
} }
} }

@ -86,8 +86,6 @@ class Terminal extends Plugin {
this.call('menuicons', 'select', 'debugger') this.call('menuicons', 'select', 'debugger')
this.call('debugger', 'debug', hash) this.call('debugger', 'debug', hash)
}) })
this.logHtmlResponse = []
this.logResponse = []
} }
onActivation () { onActivation () {
@ -102,23 +100,11 @@ class Terminal extends Plugin {
} }
logHtml (html) { logHtml (html) {
this.logHtmlResponse.push(html.innerText) this.terminalApi.logHtml(html)
this.renderComponent()
this.resetLogHtml()
}
resetLogHtml () {
this.logHtmlResponse = []
} }
log (message) { log (message) {
this.logResponse.push(message) this.terminalApi.log(message)
this.renderComponent()
this.resetLog()
}
resetLog () {
this.logResponse = []
} }
render () { render () {
@ -126,9 +112,11 @@ class Terminal extends Plugin {
} }
renderComponent () { renderComponent () {
const onReady = (api) => { this.terminalApi = api }
ReactDOM.render( ReactDOM.render(
<RemixUiTerminal <RemixUiTerminal
plugin = {this} plugin={this}
onReady={onReady}
/>, />,
this.element this.element
) )

@ -56,6 +56,7 @@ class ContractDropdownUI {
} }
this.exEnvironment = this.blockchain.getProvider() this.exEnvironment = this.blockchain.getProvider()
this.networkName = network.name this.networkName = network.name
this.networkId = network.id
const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`) const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`)
@ -309,10 +310,10 @@ class ContractDropdownUI {
const data = self.runView.compilersArtefacts.getCompilerAbstract(contractObject.contract.file) const data = self.runView.compilersArtefacts.getCompilerAbstract(contractObject.contract.file)
self.runView.compilersArtefacts.addResolvedContract(helper.addressToString(address), data) self.runView.compilersArtefacts.addResolvedContract(helper.addressToString(address), data)
if (self.ipfsCheckedState) { if (self.ipfsCheckedState) {
_paq.push(['trackEvent', 'udapp', 'DeployAndPublish', this.networkName]) _paq.push(['trackEvent', 'udapp', 'DeployAndPublish', this.networkName, this.networkId])
publishToStorage('ipfs', self.runView.fileProvider, self.runView.fileManager, selectedContract) publishToStorage('ipfs', self.runView.fileProvider, self.runView.fileManager, selectedContract)
} else { } else {
_paq.push(['trackEvent', 'udapp', 'DeployOnly', this.networkName]) _paq.push(['trackEvent', 'udapp', 'DeployOnly', this.networkName, this.networkId])
} }
} }
@ -346,7 +347,7 @@ class ContractDropdownUI {
} }
deployContract (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) { deployContract (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) {
_paq.push(['trackEvent', 'udapp', 'DeployContractTo', this.networkName]) _paq.push(['trackEvent', 'udapp', 'DeployContractTo', this.networkName, this.networkId])
const { statusCb } = callbacks const { statusCb } = callbacks
if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) { if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) {
return this.blockchain.deployContractAndLibraries(selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) return this.blockchain.deployContractAndLibraries(selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb)

@ -7,7 +7,7 @@ var async = require('async')
var tooltip = require('../ui/tooltip') var tooltip = require('../ui/tooltip')
var Renderer = require('../ui/renderer') var Renderer = require('../ui/renderer')
var css = require('./styles/test-tab-styles') var css = require('./styles/test-tab-styles')
var { UnitTestRunner } = require('@remix-project/remix-tests') var { UnitTestRunner, assertLibCode } = require('@remix-project/remix-tests')
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
@ -16,7 +16,7 @@ const TestTabLogic = require('./testTab/testTab')
const profile = { const profile = {
name: 'solidityUnitTesting', name: 'solidityUnitTesting',
displayName: 'Solidity unit testing', displayName: 'Solidity unit testing',
methods: ['testFromPath', 'testFromSource', 'setTestFolderPath'], methods: ['testFromPath', 'testFromSource', 'setTestFolderPath', 'getTestlibs'],
events: [], events: [],
icon: 'assets/img/unitTesting.webp', icon: 'assets/img/unitTesting.webp',
description: 'Fast tool to generate unit tests for your contracts', description: 'Fast tool to generate unit tests for your contracts',
@ -42,7 +42,7 @@ module.exports = class TestTab extends ViewPlugin {
this.areTestsRunning = false this.areTestsRunning = false
this.defaultPath = 'tests' this.defaultPath = 'tests'
this.offsetToLineColumnConverter = offsetToLineColumnConverter this.offsetToLineColumnConverter = offsetToLineColumnConverter
this.allFilesInvolved = [] this.allFilesInvolved = ['.deps/remix-tests/remix_tests.sol', '.deps/remix-tests/remix_accounts.sol']
this.isDebugging = false this.isDebugging = false
this.currentErrors = [] this.currentErrors = []
@ -74,11 +74,25 @@ module.exports = class TestTab extends ViewPlugin {
} }
} }
getTestlibs () {
return { assertLibCode, accountsLibCode: this.testRunner.accountsLibCode }
}
async createTestLibs () {
const provider = await this.fileManager.currentFileProvider()
if (provider) {
provider.addExternal('.deps/remix-tests/remix_tests.sol', assertLibCode, 'remix_tests.sol')
provider.addExternal('.deps/remix-tests/remix_accounts.sol', this.testRunner.accountsLibCode, 'remix_accounts.sol')
}
}
async onActivation () { async onActivation () {
const isSolidityActive = await this.call('manager', 'isActive', 'solidity') const isSolidityActive = await this.call('manager', 'isActive', 'solidity')
if (!isSolidityActive) { if (!isSolidityActive) {
await this.call('manager', 'activatePlugin', 'solidity') await this.call('manager', 'activatePlugin', 'solidity')
} }
await this.testRunner.init()
await this.createTestLibs()
this.updateRunAction() this.updateRunAction()
} }
@ -110,9 +124,13 @@ module.exports = class TestTab extends ViewPlugin {
this.setCurrentPath(this.defaultPath) this.setCurrentPath(this.defaultPath)
}) })
this.on('filePanel', 'workspaceCreated', async () => {
this.createTestLibs()
})
this.testRunner.event.on('compilationFinished', (success, data, source) => { this.testRunner.event.on('compilationFinished', (success, data, source) => {
if (success) { if (success) {
this.allFilesInvolved = Object.keys(data.sources) this.allFilesInvolved.push(...Object.keys(data.sources))
// forwarding the event to the appManager infra // forwarding the event to the appManager infra
// This is listened by compilerArtefacts to show data while debugging // This is listened by compilerArtefacts to show data while debugging
this.emit('compilationFinished', source.target, source, 'soljson', data) this.emit('compilationFinished', source.target, source, 'soljson', data)
@ -526,8 +544,8 @@ module.exports = class TestTab extends ViewPlugin {
this.fileManager.readFile(testFilePath).then((content) => { this.fileManager.readFile(testFilePath).then((content) => {
const runningTests = {} const runningTests = {}
runningTests[testFilePath] = { content } runningTests[testFilePath] = { content }
const { currentVersion, evmVersion, optimize, runs } = this.compileTab.getCurrentCompilerConfig() const { currentVersion, evmVersion, optimize, runs, isUrl } = this.compileTab.getCurrentCompilerConfig()
const currentCompilerUrl = urlFromVersion(currentVersion) const currentCompilerUrl = isUrl ? currentVersion : urlFromVersion(currentVersion)
const compilerConfig = { const compilerConfig = {
currentCompilerUrl, currentCompilerUrl,
evmVersion, evmVersion,

@ -79,10 +79,17 @@ export class ThemeModule extends Plugin {
throw new Error(`Theme ${themeName} doesn't exist`) throw new Error(`Theme ${themeName} doesn't exist`)
} }
const next = themeName || this.active // Name const next = themeName || this.active // Name
if (next === this.active) return
_paq.push(['trackEvent', 'themeModule', 'switchTo', next]) _paq.push(['trackEvent', 'themeModule', 'switchTo', next])
const nextTheme = this.themes[next] // Theme const nextTheme = this.themes[next] // Theme
if (!this.forced) this._deps.config.set('settings/theme', next) if (!this.forced) this._deps.config.set('settings/theme', next)
document.getElementById('theme-link').setAttribute('href', nextTheme.url) document.getElementById('theme-link').remove()
const theme = yo`<link rel="stylesheet" href="${nextTheme.url}" id="theme-link"/>`
theme.addEventListener('load', () => {
this.emit('themeLoaded', nextTheme)
this.events.emit('themeLoaded', nextTheme)
})
document.head.insertBefore(theme, document.head.firstChild)
document.documentElement.style.setProperty('--theme', nextTheme.quality) document.documentElement.style.setProperty('--theme', nextTheme.quality)
if (themeName) this.active = themeName if (themeName) this.active = themeName
// TODO: Only keep `this.emit` (issue#2210) // TODO: Only keep `this.emit` (issue#2210)

File diff suppressed because one or more lines are too long

@ -21,7 +21,7 @@ module.exports = config => {
mode: 'production', mode: 'production',
devtool: 'source-map', devtool: 'source-map',
optimization: { optimization: {
minimize: true, minimize: false,
minimizer: [new TerserPlugin()] minimizer: [new TerserPlugin()]
} }
} }

@ -143,12 +143,17 @@ export const CompilerApiMixin = (Base) => class extends Base {
// This function is used for passing the compiler configuration to 'remix-tests' // This function is used for passing the compiler configuration to 'remix-tests'
getCurrentCompilerConfig () { getCurrentCompilerConfig () {
const compilerState = this.getCompilerState() const compilerState = this.getCompilerState()
return { let compilerDetails: any = {
currentVersion: compilerState.currentVersion, currentVersion: compilerState.currentVersion,
evmVersion: compilerState.evmVersion, evmVersion: compilerState.evmVersion,
optimize: compilerState.optimize, optimize: compilerState.optimize,
runs: compilerState.runs runs: compilerState.runs
} }
if (this.data.loading) {
compilerDetails.currentVersion = this.data.loadingUrl
compilerDetails.isUrl = true
}
return compilerDetails
} }
/** /**
@ -193,8 +198,9 @@ export const CompilerApiMixin = (Base) => class extends Base {
if (this.onContentChanged) this.onContentChanged() if (this.onContentChanged) this.onContentChanged()
}) })
this.data.eventHandlers.onLoadingCompiler = () => { this.data.eventHandlers.onLoadingCompiler = (url) => {
this.data.loading = true this.data.loading = true
this.data.loadingUrl = url
this.emit('statusChanged', { key: 'loading', title: 'loading compiler...', type: 'info' }) this.emit('statusChanged', { key: 'loading', title: 'loading compiler...', type: 'info' })
} }
this.compiler.event.register('loadingCompiler', this.data.eventHandlers.onLoadingCompiler) this.compiler.event.register('loadingCompiler', this.data.eventHandlers.onLoadingCompiler)

@ -1,7 +1,6 @@
'use strict' 'use strict'
import { Plugin } from '@remixproject/engine' import { Plugin } from '@remixproject/engine'
import { RemixURLResolver } from '@remix-project/remix-url-resolver' import { RemixURLResolver } from '@remix-project/remix-url-resolver'
const remixTests = require('@remix-project/remix-tests')
const profile = { const profile = {
name: 'contentImport', name: 'contentImport',
@ -117,7 +116,6 @@ export class CompilerImports extends Plugin {
* @returns {Promise} - string content * @returns {Promise} - string content
*/ */
async resolveAndSave (url, targetPath) { async resolveAndSave (url, targetPath) {
if (url.indexOf('remix_tests.sol') !== -1) return remixTests.assertLibCode
try { try {
const provider = await this.call('fileManager', 'getProviderOf', url) const provider = await this.call('fileManager', 'getProviderOf', url)
if (provider) { if (provider) {

@ -15,6 +15,7 @@ export class TxRunnerVM {
blocks blocks
logsManager logsManager
commonContext commonContext
nextNonceForCall: number
getVMObject: () => any getVMObject: () => any
constructor (vmaccounts, api, getVMObject) { constructor (vmaccounts, api, getVMObject) {
@ -31,6 +32,13 @@ export class TxRunnerVM {
this.vmaccounts = vmaccounts this.vmaccounts = vmaccounts
this.queusTxs = [] this.queusTxs = []
this.blocks = [] this.blocks = []
/*
txHash is generated using the nonce,
in order to have unique transaction hash, we need to keep using different nonce (in case of a call)
so we increment this value after each call.
For this to function we also need to skip nonce validation, in the vm: `{ skipNonce: true }`
*/
this.nextNonceForCall = 0
} }
execute (args, confirmationCb, gasEstimationForceSend, promptCb, callback) { execute (args, confirmationCb, gasEstimationForceSend, promptCb, callback) {
@ -75,7 +83,7 @@ export class TxRunnerVM {
let tx let tx
if (!EIP1559) { if (!EIP1559) {
tx = Transaction.fromTxData({ tx = Transaction.fromTxData({
nonce: new BN(res.nonce), nonce: useCall ? this.nextNonceForCall : new BN(res.nonce),
gasPrice: '0x1', gasPrice: '0x1',
gasLimit: gasLimit, gasLimit: gasLimit,
to: to, to: to,
@ -84,7 +92,7 @@ export class TxRunnerVM {
}, { common: this.commonContext }).sign(account.privateKey) }, { common: this.commonContext }).sign(account.privateKey)
} else { } else {
tx = FeeMarketEIP1559Transaction.fromTxData({ tx = FeeMarketEIP1559Transaction.fromTxData({
nonce: new BN(res.nonce), nonce: useCall ? this.nextNonceForCall : new BN(res.nonce),
maxPriorityFeePerGas: '0x01', maxPriorityFeePerGas: '0x01',
maxFeePerGas: '0x1', maxFeePerGas: '0x1',
gasLimit: gasLimit, gasLimit: gasLimit,
@ -93,6 +101,7 @@ export class TxRunnerVM {
data: Buffer.from(data.slice(2), 'hex') data: Buffer.from(data.slice(2), 'hex')
}).sign(account.privateKey) }).sign(account.privateKey)
} }
if (useCall) this.nextNonceForCall++
const coinbases = ['0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e'] const coinbases = ['0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e']
const difficulties = [new BN('69762765929000', 10), new BN('70762765929000', 10), new BN('71762765929000', 10)] const difficulties = [new BN('69762765929000', 10), new BN('70762765929000', 10), new BN('71762765929000', 10)]
@ -127,7 +136,7 @@ export class TxRunnerVM {
} }
runBlockInVm (tx, block, callback) { runBlockInVm (tx, block, callback) {
this.getVMObject().vm.runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false }).then((results) => { this.getVMObject().vm.runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false, skipNonce: true }).then((results) => {
const result = results.results[0] const result = results.results[0]
if (result) { if (result) {
const status = result.execResult.exceptionError ? 0 : 1 const status = result.execResult.exceptionError ? 0 : 1

@ -304,7 +304,9 @@ export class Web3VmProvider {
nextKey: null nextKey: null
}) })
} }
cb('unable to retrieve storage ' + txIndex + ' ' + address) // Before https://github.com/ethereum/remix-project/pull/1703, it used to throw error as
// 'unable to retrieve storage ' + txIndex + ' ' + address
cb(null, { storage: {} })
} }
getBlockNumber (cb) { cb(null, 'vm provider') } getBlockNumber (cb) { cb(null, 'vm provider') }

@ -3,7 +3,7 @@ module.exports = `// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0; pragma solidity >=0.4.22 <0.9.0;
library TestsAccounts { library TestsAccounts {
function getAccount(uint index) public returns (address) { function getAccount(uint index) pure public returns (address) {
>accounts< >accounts<
return accounts[index]; return accounts[index];
} }

@ -12,13 +12,13 @@ function regexIndexOf (inputString: string, regex: RegExp, startpos = 0) {
return (indexOf >= 0) ? (indexOf + (startpos)) : indexOf return (indexOf >= 0) ? (indexOf + (startpos)) : indexOf
} }
function writeTestAccountsContract (accounts: string[]) { export function writeTestAccountsContract (accounts: string[]) {
const testAccountContract = require('../sol/tests_accounts.sol') const testAccountContract = require('../sol/tests_accounts.sol')
let body = `address[${accounts.length}] memory accounts;` let body = `address[${accounts.length}] memory accounts;`
if (!accounts.length) body += ';' if (!accounts.length) body += ';'
else { else {
accounts.map((address, index) => { accounts.map((address, index) => {
body += `\naccounts[${index}] = ${address};\n` body += `\n\t\taccounts[${index}] = ${address};\n`
}) })
} }
return testAccountContract.replace('>accounts<', body) return testAccountContract.replace('>accounts<', body)
@ -172,14 +172,7 @@ export function compileFileOrFiles (filename: string, isDirectory: boolean, opts
*/ */
export function compileContractSources (sources: SrcIfc, compilerConfig: CompilerConfiguration, importFileCb: any, opts: any, cb): void { export function compileContractSources (sources: SrcIfc, compilerConfig: CompilerConfiguration, importFileCb: any, opts: any, cb): void {
let compiler let compiler
const accounts: string[] = opts.accounts || []
const filepath = opts.testFilePath || '' const filepath = opts.testFilePath || ''
// Iterate over sources keys. Inject test libraries. Inject test library import statements.
if (!('remix_tests.sol' in sources) && !('tests.sol' in sources)) {
sources['tests.sol'] = { content: require('../sol/tests.sol.js') }
sources['remix_tests.sol'] = { content: require('../sol/tests.sol.js') }
sources['remix_accounts.sol'] = { content: writeTestAccountsContract(accounts) }
}
const testFileImportRegEx = /^(import)\s['"](remix_tests.sol|tests.sol)['"];/gm const testFileImportRegEx = /^(import)\s['"](remix_tests.sol|tests.sol)['"];/gm
const includeTestLibs = '\nimport \'remix_tests.sol\';\n' const includeTestLibs = '\nimport \'remix_tests.sol\';\n'

@ -11,18 +11,12 @@ import { compilationInterface } from './types'
* @param callback Callback * @param callback Callback
*/ */
export function deployAll (compileResult: compilationInterface, web3: Web3, withDoubleGas: boolean, deployCb, callback) { export function deployAll (compileResult: compilationInterface, web3: Web3, testsAccounts, withDoubleGas: boolean, deployCb, callback) {
const compiledObject = {} const compiledObject = {}
const contracts = {} const contracts = {}
let accounts: string[] = [] const accounts: string[] = testsAccounts
async.waterfall([ async.waterfall([
function getAccountList (next) {
web3.eth.getAccounts((_err, _accounts) => {
accounts = _accounts
next()
})
},
function getContractData (next) { function getContractData (next) {
for (const contractFile in compileResult) { for (const contractFile in compileResult) {
for (const contractName in compileResult[contractFile]) { for (const contractName in compileResult[contractFile]) {

@ -3,3 +3,4 @@ export { UnitTestRunner } from './runTestSources'
export { runTest } from './testRunner' export { runTest } from './testRunner'
export * from './types' export * from './types'
export const assertLibCode = require('../sol/tests.sol') export const assertLibCode = require('../sol/tests.sol')
export { writeTestAccountsContract } from './compiler'

@ -61,13 +61,13 @@ export function runTestFiles (filepath: string, isDirectory: boolean, web3: Web3
for (const filename in asts) { for (const filename in asts) {
if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast } if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast }
} }
deployAll(compilationResult, web3, false, null, (err, contracts) => { deployAll(compilationResult, web3, accounts, false, null, (err, contracts) => {
if (err) { if (err) {
// If contract deployment fails because of 'Out of Gas' error, try again with double gas // If contract deployment fails because of 'Out of Gas' error, try again with double gas
// This is temporary, should be removed when remix-tests will have a dedicated UI to // This is temporary, should be removed when remix-tests will have a dedicated UI to
// accept deployment params from UI // accept deployment params from UI
if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) { if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) {
deployAll(compilationResult, web3, true, null, (error, contracts) => { deployAll(compilationResult, web3, accounts, true, null, (error, contracts) => {
if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array
else next(null, compilationResult, contracts) else next(null, compilationResult, contracts)
}) })

@ -1,9 +1,7 @@
import async, { ErrorCallback } from 'async' import async, { ErrorCallback } from 'async'
import { compileContractSources, writeTestAccountsContract } from './compiler'
import { compileContractSources } from './compiler'
import { deployAll } from './deployer' import { deployAll } from './deployer'
import { runTest } from './testRunner' import { runTest } from './testRunner'
import Web3 from 'web3' import Web3 from 'web3'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { Provider, extend } from '@remix-project/remix-simulator' import { Provider, extend } from '@remix-project/remix-simulator'
@ -15,13 +13,22 @@ require('colors')
export class UnitTestRunner { export class UnitTestRunner {
event event
accountsLibCode
testsAccounts: string[] | null
web3
constructor () { constructor () {
this.event = new EventEmitter() this.event = new EventEmitter()
} }
async createWeb3Provider () { async init (web3 = null, accounts = null) {
const web3 = new Web3() this.web3 = await this.createWeb3Provider(web3)
this.testsAccounts = accounts || await this.web3.eth.getAccounts()
this.accountsLibCode = writeTestAccountsContract(this.testsAccounts)
}
async createWeb3Provider (optWeb3) {
const web3 = optWeb3 || new Web3()
const provider: any = new Provider() const provider: any = new Provider()
await provider.init() await provider.init()
web3.setProvider(provider) web3.setProvider(provider)
@ -42,30 +49,23 @@ export class UnitTestRunner {
async runTestSources (contractSources: SrcIfc, compilerConfig: CompilerConfiguration, testCallback, resultCallback, deployCb:any, finalCallback: any, importFileCb, opts: Options) { async runTestSources (contractSources: SrcIfc, compilerConfig: CompilerConfiguration, testCallback, resultCallback, deployCb:any, finalCallback: any, importFileCb, opts: Options) {
opts = opts || {} opts = opts || {}
const sourceASTs: any = {} const sourceASTs: any = {}
const web3 = opts.web3 || await this.createWeb3Provider() if (opts.web3 || opts.accounts) this.init(opts.web3, opts.accounts)
let accounts: string[] | null = opts.accounts || null
async.waterfall([ async.waterfall([
function getAccountList (next) {
if (accounts) return next()
web3.eth.getAccounts((_err, _accounts) => {
accounts = _accounts
next()
})
},
(next) => { (next) => {
compileContractSources(contractSources, compilerConfig, importFileCb, { accounts, testFilePath: opts.testFilePath, event: this.event }, next) compileContractSources(contractSources, compilerConfig, importFileCb, { accounts: this.testsAccounts, testFilePath: opts.testFilePath, event: this.event }, next)
}, },
function deployAllContracts (compilationResult: compilationInterface, asts: ASTInterface, next) { (compilationResult: compilationInterface, asts: ASTInterface, next) => {
for (const filename in asts) { for (const filename in asts) {
if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast } if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast }
} }
deployAll(compilationResult, web3, false, deployCb, (err, contracts) => { deployAll(compilationResult, this.web3, this.testsAccounts, false, deployCb, (err, contracts) => {
if (err) { if (err) {
// If contract deployment fails because of 'Out of Gas' error, try again with double gas // If contract deployment fails because of 'Out of Gas' error, try again with double gas
// This is temporary, should be removed when remix-tests will have a dedicated UI to // This is temporary, should be removed when remix-tests will have a dedicated UI to
// accept deployment params from UI // accept deployment params from UI
if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) { if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) {
deployAll(compilationResult, web3, true, deployCb, (error, contracts) => { deployAll(compilationResult, this.web3, this.testsAccounts, true, deployCb, (error, contracts) => {
if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array
else next(null, compilationResult, contracts) else next(null, compilationResult, contracts)
}) })
@ -88,7 +88,7 @@ export class UnitTestRunner {
} }
next(null, contractsToTest, contractsToTestDetails, contracts) next(null, contractsToTest, contractsToTestDetails, contracts)
}, },
function runTests (contractsToTest: string[], contractsToTestDetails: any[], contracts: any, next) { (contractsToTest: string[], contractsToTestDetails: any[], contracts: any, next) => {
let totalPassing = 0 let totalPassing = 0
let totalFailing = 0 let totalFailing = 0
let totalTime = 0 let totalTime = 0
@ -111,7 +111,7 @@ export class UnitTestRunner {
async.eachOfLimit(contractsToTest, 1, (contractName: string, index: string | number, cb: ErrorCallback) => { async.eachOfLimit(contractsToTest, 1, (contractName: string, index: string | number, cb: ErrorCallback) => {
const fileAST: AstNode = sourceASTs[contracts[contractName]['filename']] const fileAST: AstNode = sourceASTs[contracts[contractName]['filename']]
runTest(contractName, contracts[contractName], contractsToTestDetails[index], fileAST, { accounts, web3 }, _testCallback, (err, result) => { runTest(contractName, contracts[contractName], contractsToTestDetails[index], fileAST, { accounts: this.testsAccounts, web3: this.web3 }, _testCallback, (err, result) => {
if (err) { if (err) {
return cb(err) return cb(err)
} }

@ -67,7 +67,7 @@ async function compileAndDeploy(filename: string, callback: Function) {
} }
try { try {
compilationData = compilationResult compilationData = compilationResult
deployAll(compilationResult, web3, false, null, next) deployAll(compilationResult, web3, accounts, false, null, next)
} catch (e) { } catch (e) {
throw e throw e
} }

@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line
import Editor from '@monaco-editor/react' import Editor from '@monaco-editor/react'
import { reducerActions, reducerListener, initialState } from './actions/editor' import { reducerActions, reducerListener, initialState } from './actions/editor'
import { language } from './syntax'
import './remix-ui-editor.css' import './remix-ui-editor.css'
@ -77,13 +78,39 @@ export const EditorUI = (props: EditorUIProps) => {
const [editorModelsState, dispatch] = useReducer(reducerActions, initialState) const [editorModelsState, dispatch] = useReducer(reducerActions, initialState)
const defineAndSetDarkTheme = (monaco) => {
// see https://microsoft.github.io/monaco-editor/playground.html#customizing-the-appearence-exposed-colors
const lightColor = window.getComputedStyle(document.documentElement).getPropertyValue('--light').trim()
const infoColor = window.getComputedStyle(document.documentElement).getPropertyValue('--info').trim()
const darkColor = window.getComputedStyle(document.documentElement).getPropertyValue('--dark').trim()
const grayColor = window.getComputedStyle(document.documentElement).getPropertyValue('--gray-dark').trim()
monaco.editor.defineTheme('remix-dark', {
base: 'vs-dark',
inherit: true, // can also be false to completely replace the builtin rules
rules: [
{ background: darkColor.replace('#', '') },
{ token: 'keyword.external', foreground: infoColor }
],
colors: {
'editor.background': darkColor,
'editorSuggestWidget.background': lightColor,
'editorSuggestWidget.selectedBackground': lightColor,
'editorSuggestWidget.highlightForeground': infoColor,
'editor.lineHighlightBorder': lightColor,
'editor.lineHighlightBackground': grayColor,
'editorGutter.background': lightColor
}
})
monacoRef.current.editor.setTheme('remix-dark')
}
useEffect(() => { useEffect(() => {
if (!monacoRef.current) return if (!monacoRef.current) return
monacoRef.current.editor.setTheme(props.theme) if (props.theme === 'remix-dark') {
defineAndSetDarkTheme(monacoRef.current)
} else monacoRef.current.editor.setTheme(props.theme)
}, [props.theme]) }, [props.theme])
if (monacoRef.current) monacoRef.current.editor.setTheme(props.theme)
const setAnnotationsbyFile = (uri) => { const setAnnotationsbyFile = (uri) => {
if (props.sourceAnnotationsPerFile[uri]) { if (props.sourceAnnotationsPerFile[uri]) {
const model = editorModelsState[uri]?.model const model = editorModelsState[uri]?.model
@ -137,8 +164,10 @@ export const EditorUI = (props: EditorUIProps) => {
useEffect(() => { useEffect(() => {
if (!editorRef.current) return if (!editorRef.current) return
currentFileRef.current = props.currentFile currentFileRef.current = props.currentFile
editorRef.current.setModel(editorModelsState[props.currentFile].model) const file = editorModelsState[props.currentFile]
editorRef.current.setModel(file.model)
editorRef.current.updateOptions({ readOnly: editorModelsState[props.currentFile].readOnly }) editorRef.current.updateOptions({ readOnly: editorModelsState[props.currentFile].readOnly })
if (file.language === 'sol') monacoRef.current.editor.setModelLanguage(file.model, 'remix-solidity')
setAnnotationsbyFile(props.currentFile) setAnnotationsbyFile(props.currentFile)
setMarkerbyFile(props.currentFile) setMarkerbyFile(props.currentFile)
}, [props.currentFile]) }, [props.currentFile])
@ -207,7 +236,9 @@ export const EditorUI = (props: EditorUIProps) => {
function handleEditorDidMount (editor) { function handleEditorDidMount (editor) {
editorRef.current = editor editorRef.current = editor
monacoRef.current.editor.setTheme(props.theme) if (props.theme === 'remix-dark') {
defineAndSetDarkTheme(monacoRef.current)
} else monacoRef.current.editor.setTheme(props.theme)
reducerListener(props.plugin, dispatch, monacoRef.current, editorRef.current, props.events) reducerListener(props.plugin, dispatch, monacoRef.current, editorRef.current, props.events)
props.events.onEditorMounted() props.events.onEditorMounted()
editor.onMouseUp((e) => { editor.onMouseUp((e) => {
@ -215,29 +246,20 @@ export const EditorUI = (props: EditorUIProps) => {
(window as any).addRemixBreakpoint(e.target.position) (window as any).addRemixBreakpoint(e.target.position)
} }
}) })
editor.addCommand(monacoRef.current.KeyMod.CtrlCmd | monacoRef.current.KeyCode.US_EQUAL, () => {
editor.updateOptions({ fontSize: editor.getOption(42).fontSize + 1 })
})
editor.addCommand(monacoRef.current.KeyMod.CtrlCmd | monacoRef.current.KeyCode.US_MINUS, () => {
editor.updateOptions({ fontSize: editor.getOption(42).fontSize - 1 })
})
} }
function handleEditorWillMount (monaco) { function handleEditorWillMount (monaco) {
monacoRef.current = monaco monacoRef.current = monaco
// see https://microsoft.github.io/monaco-editor/playground.html#customizing-the-appearence-exposed-colors // Register a new language
const lightColor = window.getComputedStyle(document.documentElement).getPropertyValue('--light').trim() monacoRef.current.languages.register({ id: 'remix-solidity' })
const infoColor = window.getComputedStyle(document.documentElement).getPropertyValue('--info').trim() // Register a tokens provider for the language
const darkColor = window.getComputedStyle(document.documentElement).getPropertyValue('--dark').trim() monacoRef.current.languages.setMonarchTokensProvider('remix-solidity', language)
const grayColor = window.getComputedStyle(document.documentElement).getPropertyValue('--gray-dark').trim()
monaco.editor.defineTheme('remix-dark', {
base: 'vs-dark',
inherit: true, // can also be false to completely replace the builtin rules
rules: [{ background: darkColor.replace('#', '') }],
colors: {
'editor.background': darkColor,
'editorSuggestWidget.background': lightColor,
'editorSuggestWidget.selectedBackground': lightColor,
'editorSuggestWidget.highlightForeground': infoColor,
'editor.lineHighlightBorder': lightColor,
'editor.lineHighlightBackground': grayColor,
'editorGutter.background': lightColor
}
})
} }
return ( return (

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
.remix-ui-tabs { .remix-ui-tabs {
display: -webkit-box; display: -webkit-box;
max-height: 42px max-height: 2.15rem;
} }
.remix-ui-tabs li { .remix-ui-tabs li {
display: inline-block; display: inline-block;
@ -28,12 +28,11 @@
} }
.close-tabs { .close-tabs {
visibility: hidden; visibility: hidden;
padding-top: 4px;
font-size: medium; font-size: medium;
} }
.iconImage { .iconImage {
width: 16px; width: 1rem;
height: 16px; height: 1rem;
} }
.active { .active {
border: 1px solid transparent; border: 1px solid transparent;
@ -44,11 +43,6 @@
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
white-space: nowrap; white-space: nowrap;
max-width: 1000px;
}
.left-icon {
width: 70px;
height: 49px;
} }
/* Hide scrollbar for Chrome, Safari and Opera */ /* Hide scrollbar for Chrome, Safari and Opera */

@ -1,6 +1,5 @@
import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs' // import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'
import './remix-ui-tabs.css' import './remix-ui-tabs.css'
/* eslint-disable-next-line */ /* eslint-disable-next-line */
@ -18,7 +17,10 @@ export interface TabsUIApi {
active: () => string active: () => string
} }
declare var ReactTabs: any
export const TabsUI = (props: TabsUIProps) => { export const TabsUI = (props: TabsUIProps) => {
const { Tab, Tabs, TabList, TabPanel } = ReactTabs
const [selectedIndex, setSelectedIndex] = useState(-1) const [selectedIndex, setSelectedIndex] = useState(-1)
const currentIndexRef = useRef(-1) const currentIndexRef = useRef(-1)
const tabsRef = useRef({}) const tabsRef = useRef({})
@ -33,12 +35,12 @@ export const TabsUI = (props: TabsUIProps) => {
const renderTab = (tab, index) => { const renderTab = (tab, index) => {
const classNameImg = 'my-1 mr-1 text-dark ' + tab.iconClass const classNameImg = 'my-1 mr-1 text-dark ' + tab.iconClass
const classNameTab = 'nav-item nav-link tab' + (index === currentIndexRef.current ? ' active' : '') const classNameTab = 'nav-item nav-link d-flex justify-content-center align-items-center px-2 py-1 tab' + (index === currentIndexRef.current ? ' active' : '')
return ( return (
<div ref={el => { tabsRef.current[index] = el }} className={classNameTab} title={tab.tooltip}> <div onClick={() => { props.onSelect(index); currentIndexRef.current = index; setSelectedIndex(index) }} ref={el => { tabsRef.current[index] = el }} className={classNameTab} title={tab.tooltip}>
{tab.icon ? (<img className="my-1 mr-1 iconImage" src={tab.icon} />) : (<i className={classNameImg}></i>)} {tab.icon ? (<img className="my-1 mr-1 iconImage" src={tab.icon} />) : (<i className={classNameImg}></i>)}
<span className="title-tabs">{tab.title}</span> <span className="title-tabs">{tab.title}</span>
<span className="close-tabs" onClick={() => props.onClose(index)}> <span className="close-tabs" onClick={(event) => { props.onClose(index); event.stopPropagation() }}>
<i className="text-dark fas fa-times"></i> <i className="text-dark fas fa-times"></i>
</span> </span>
</div> </div>
@ -62,16 +64,23 @@ export const TabsUI = (props: TabsUIProps) => {
}, []) }, [])
return ( return (
<div className="remix-ui-tabs header nav nav-tabs"> <div className="remix-ui-tabs d-flex justify-content-between border-0 header nav-tabs">
<div className="d-flex flex-row justify-content-center align-items-center left-icon"> <div className="d-flex flex-row" style={ { maxWidth: 'fit-content', width: '97%' } }>
<span data-id="tabProxyZoomOut" className="btn btn-sm px-1 fas fa-search-minus text-dark" title="Zoom out" onClick={() => props.onZoomOut()}></span> <div className="d-flex flex-row justify-content-center align-items-center m-1 mt-2">
<span data-id="tabProxyZoomIn" className="btn btn-sm px-1 fas fa-search-plus text-dark" title="Zoom in" onClick={() => props.onZoomIn()}></span> <span data-id="tabProxyZoomOut" className="btn btn-sm px-2 fas fa-search-minus text-dark" title="Zoom out" onClick={() => props.onZoomOut()}></span>
<i className="d-flex flex-row justify-content-center align-items-center far fa-sliders-v px-1" title="press F1 when focusing the editor to show advanced configuration settings"></i> <span data-id="tabProxyZoomIn" className="btn btn-sm px-2 fas fa-search-plus text-dark" title="Zoom in" onClick={() => props.onZoomIn()}></span>
</div>
<Tabs
className="tab-scroll"
selectedIndex={selectedIndex}
>
<TabList className="d-flex flex-row justify-content-center align-items-center">
{props.tabs.map((tab, i) => <Tab className="py-1" key={tab.name}>{renderTab(tab, i)}</Tab>)}
</TabList>
{props.tabs.map((tab) => <TabPanel key={tab.name} ></TabPanel>)}
</Tabs>
</div> </div>
<Tabs className="tab-scroll" selectedIndex={selectedIndex} onSelect={index => { props.onSelect(index); currentIndexRef.current = index; setSelectedIndex(index) }} > <i className="mt-2 mr-2 fas fa-arrows-alt-h" title="Scroll to see all tabs"></i>
<TabList>{props.tabs.map((tab, i) => <Tab key={tab.name}>{renderTab(tab, i)}</Tab>)}</TabList>
{props.tabs.map((tab) => <TabPanel key={tab.name} ></TabPanel>)}
</Tabs>
</div> </div>
) )
} }

@ -12,7 +12,7 @@ const Context = ({ opts, blockchain }) => {
const val = data.value const val = data.value
let hash = data.hash ? helper.shortenHexData(data.hash) : '' let hash = data.hash ? helper.shortenHexData(data.hash) : ''
const input = data.input ? helper.shortenHexData(data.input) : '' const input = data.input ? helper.shortenHexData(data.input) : ''
const logs = data.logs && data.logs.decoded && data.logs.decoded.length ? data.logs.decoded.length : 0 const logs = opts.logs && opts.logs.decoded && opts.logs.decoded.length ? opts.logs.decoded.length : 0
const block = data.receipt ? data.receipt.blockNumber : data.blockNumber || '' const block = data.receipt ? data.receipt.blockNumber : data.blockNumber || ''
const i = data.receipt ? data.transactionIndex : data.transactionIndex const i = data.receipt ? data.transactionIndex : data.transactionIndex
const value = val ? typeConversion.toInt(val) : 0 const value = val ? typeConversion.toInt(val) : 0

@ -21,7 +21,7 @@ const RenderKnownTransactions = ({ tx, receipt, resolvedData, logs, index, plugi
const from = tx.from const from = tx.from
const to = resolvedData.contractName + '.' + resolvedData.fn const to = resolvedData.contractName + '.' + resolvedData.fn
const txType = 'knownTx' const txType = 'knownTx'
const options = { from, to, tx } const options = { from, to, tx, logs }
return ( return (
<span id={`tx${tx.hash}`} key={index}> <span id={`tx${tx.hash}`} key={index}>
<div className="log" onClick={(event) => txDetails(event, tx)}> <div className="log" onClick={(event) => txDetails(event, tx)}>

@ -36,125 +36,219 @@ const showTable = (opts, showTableHash) => {
} }
const val = opts.val != null ? typeConversion.toInt(opts.val) : 0 const val = opts.val != null ? typeConversion.toInt(opts.val) : 0
return ( return (
<table className={`txTable ${showTableHash.includes(opts.hash) ? 'active' : ''}`} id='txTable' data-id={`txLoggerTable${opts.hash}`}> <table
className={`txTable ${showTableHash.includes(opts.hash) ? 'active' : ''}`}
id="txTable"
data-id={`txLoggerTable${opts.hash}`}
>
<tbody> <tbody>
<tr className='tr'> {opts.status !== undefined ? (
<td className='td' data-shared={`key_${opts.hash}`}>status</td> <tr className="tr">
<td className='td' data-id={`txLoggerTableStatus${opts.hash}`} data-shared={`pair_${opts.hash}`}>{`${opts.status} ${msg}`}</td> <td className="td" data-shared={`key_${opts.hash}`}>
</tr> status
{opts.hash ? (<tr className='tr'> </td>
<td className='td' data-shared={`key_${opts.hash}`}>transaction hash</td> <td
<td className='td' data-id={`txLoggerTableHash${opts.hash}`} data-shared={`pair_${opts.hash}`}>{opts.hash} className="td"
<CopyToClipboard content={opts.hash}/> data-id={`txLoggerTableStatus${opts.hash}`}
</td> data-shared={`pair_${opts.hash}`}
</tr>) : null } >{`${opts.status} ${msg}`}</td>
{ </tr>
opts.contractAddress ? ( ) : null}
<tr className='tr'> {opts.hash && !opts.isCall ? (
<td className='td' data-shared={`key_${opts.hash}`}>contract address</td> <tr className="tr">
<td className='td' data-id={`txLoggerTableContractAddress${opts.hash}`} data-shared={`pair_${opts.hash}`}>{opts.contractAddress} <td className="td" data-shared={`key_${opts.hash}`}>
<CopyToClipboard content={opts.contractAddress}/> transaction hash
</td> </td>
</tr> <td
) : null className="td"
} data-id={`txLoggerTableHash${opts.hash}`}
{ data-shared={`pair_${opts.hash}`}
opts.from ? ( >
<tr className='tr'> {opts.hash}
<td className='td tableTitle' data-shared={`key_${opts.hash}`}>from</td> <CopyToClipboard content={opts.hash} />
<td className='td' data-id={`txLoggerTableFrom${opts.hash}`} data-shared={`pair_${opts.hash}`}>{opts.from} </td>
<CopyToClipboard content={opts.from}/> </tr>
</td> ) : null}
</tr> {opts.contractAddress ? (
) : null <tr className="tr">
} <td className="td" data-shared={`key_${opts.hash}`}>
{ contract address
opts.to ? ( </td>
<tr className='tr'> <td
<td className='td' data-shared={`key_${opts.hash}`}>to</td> className="td"
<td className='td' data-id={`txLoggerTableTo${opts.hash}`} data-shared={`pair_${opts.hash}`}>{toHash} data-id={`txLoggerTableContractAddress${opts.hash}`}
<CopyToClipboard content={data.to ? data.to : toHash}/> data-shared={`pair_${opts.hash}`}
</td> >
</tr> {opts.contractAddress}
) : null <CopyToClipboard content={opts.contractAddress} />
} </td>
{ </tr>
opts.gas ? ( ) : null}
<tr className='tr'> {opts.from ? (
<td className='td' data-shared={`key_${opts.hash}`}>gas</td> <tr className="tr">
<td className='td' data-id={`txLoggerTableGas${opts.hash}`} data-shared={`pair_${opts.hash}`}>{opts.gas} gas <td className="td tableTitle" data-shared={`key_${opts.hash}`}>
<CopyToClipboard content={opts.gas}/> from
</td> </td>
</tr> <td
) : null className="td"
} data-id={`txLoggerTableFrom${opts.hash}`}
{ data-shared={`pair_${opts.hash}`}
opts.transactionCost ? ( >
<tr className='tr'> {opts.from}
<td className='td' data-shared={`key_${opts.hash}`}>transaction cost</td> <CopyToClipboard content={opts.from} />
<td className='td' data-id={`txLoggerTableTransactionCost${opts.hash}`} data-shared={`pair_${opts.hash}`}>{opts.transactionCost} gas {callWarning} </td>
<CopyToClipboard content={opts.transactionCost}/> </tr>
</td> ) : null}
</tr> {opts.to ? (
) : null <tr className="tr">
} <td className="td" data-shared={`key_${opts.hash}`}>
{ to
opts.executionCost ? ( </td>
<tr className='tr'> <td
<td className='td' data-shared={`key_${opts.hash}`}>execution cost</td> className="td"
<td className='td' data-id={`txLoggerTableExecutionHash${opts.hash}`} data-shared={`pair_${opts.hash}`}>{opts.executionCost} gas {callWarning} data-id={`txLoggerTableTo${opts.hash}`}
<CopyToClipboard content={opts.executionCost}/> data-shared={`pair_${opts.hash}`}
</td> >
</tr> {toHash}
) : null <CopyToClipboard content={data.to ? data.to : toHash} />
} </td>
</tr>
) : null}
{opts.gas ? (
<tr className="tr">
<td className="td" data-shared={`key_${opts.hash}`}>
gas
</td>
<td
className="td"
data-id={`txLoggerTableGas${opts.hash}`}
data-shared={`pair_${opts.hash}`}
>
{opts.gas} gas
<CopyToClipboard content={opts.gas} />
</td>
</tr>
) : null}
{opts.transactionCost ? (
<tr className="tr">
<td className="td" data-shared={`key_${opts.hash}`}>
transaction cost
</td>
<td
className="td"
data-id={`txLoggerTableTransactionCost${opts.hash}`}
data-shared={`pair_${opts.hash}`}
>
{opts.transactionCost} gas {callWarning}
<CopyToClipboard content={opts.transactionCost} />
</td>
</tr>
) : null}
{opts.executionCost ? (
<tr className="tr">
<td className="td" data-shared={`key_${opts.hash}`}>
execution cost
</td>
<td
className="td"
data-id={`txLoggerTableExecutionHash${opts.hash}`}
data-shared={`pair_${opts.hash}`}
>
{opts.executionCost} gas {callWarning}
<CopyToClipboard content={opts.executionCost} />
</td>
</tr>
) : null}
{opts.hash ? ( {opts.hash ? (
<tr className='tr'> <tr className="tr">
<td className='td' data-shared={`key_${opts.hash}`}>hash</td> <td className="td" data-shared={`key_${opts.hash}`}>
<td className='td' data-id={`txLoggerTableHash${opts.hash}`} data-shared={`pair_${opts.hash}`}>{opts.hash} hash
<CopyToClipboard content={opts.hash}/> </td>
<td
className="td"
data-id={`txLoggerTableHash${opts.hash}`}
data-shared={`pair_${opts.hash}`}
>
{opts.hash}
<CopyToClipboard content={opts.hash} />
</td> </td>
</tr> </tr>
) : null} ) : null}
{opts.input ? ( {opts.input ? (
<tr className='tr'> <tr className="tr">
<td className='td' data-shared={`key_${opts.hash}`}>input</td> <td className="td" data-shared={`key_${opts.hash}`}>
<td className='td' data-id={`txLoggerTableHash${opts.hash}`} data-shared={`pair_${opts.hash}`}>{helper.shortenHexData(opts.input)} input
<CopyToClipboard content={opts.input}/> </td>
<td
className="td"
data-id={`txLoggerTableHash${opts.hash}`}
data-shared={`pair_${opts.hash}`}
>
{helper.shortenHexData(opts.input)}
<CopyToClipboard content={opts.input} />
</td> </td>
</tr> </tr>
) : null} ) : null}
{opts['decoded input'] ? ( {opts['decoded input'] ? (
<tr className='tr'> <tr className="tr">
<td className='td' data-shared={`key_${opts.hash}`}>decoded input</td> <td className="td" data-shared={`key_${opts.hash}`}>
<td className='td' data-id={`txLoggerTableHash${opts.hash}`} data-shared={`pair_${opts.hash}`}>{opts['decoded input'].trim()} decoded input
<CopyToClipboard content={opts['decoded input']}/> </td>
<td
className="td"
data-id={`txLoggerTableHash${opts.hash}`}
data-shared={`pair_${opts.hash}`}
>
{opts['decoded input'].trim()}
<CopyToClipboard content={opts['decoded input']} />
</td> </td>
</tr> </tr>
) : null} ) : null}
{opts['decoded output'] ? ( {opts['decoded output'] ? (
<tr className='tr'> <tr className="tr">
<td className='td' data-shared={`key_${opts.hash}`}>decoded output</td> <td className="td" data-shared={`key_${opts.hash}`}>
<td className='td' data-id={`txLoggerTableHash${opts.hash}`} data-shared={`pair_${opts.hash}`}>{opts['decoded output']} decoded output
<CopyToClipboard content={opts['decoded output']}/> </td>
<td
className="td"
data-id={`txLoggerTableHash${opts.hash}`}
data-shared={`pair_${opts.hash}`}
>
{opts['decoded output']}
<CopyToClipboard content={opts['decoded output']} />
</td> </td>
</tr> </tr>
) : null} ) : null}
{opts.logs ? ( {opts.logs ? (
<tr className='tr'> <tr className="tr">
<td className='td' data-shared={`key_${opts.hash}`}>logs</td> <td className="td" data-shared={`key_${opts.hash}`}>
<td className='td' data-id={`txLoggerTableHash${opts.hash}`} data-shared={`pair_${opts.hash}`}> logs
</td>
<td
className="td"
data-id={`txLoggerTableHash${opts.hash}`}
data-shared={`pair_${opts.hash}`}
>
{JSON.stringify(stringified, null, '\t')} {JSON.stringify(stringified, null, '\t')}
<CopyToClipboard content={JSON.stringify(stringified, null, '\t')}/> <CopyToClipboard
<CopyToClipboard content={JSON.stringify(opts.logs.raw || '0')}/> content={JSON.stringify(stringified, null, '\t')}
/>
<CopyToClipboard content={JSON.stringify(opts.logs.raw || '0')} />
</td> </td>
</tr> </tr>
) : null} ) : null}
{opts.val ? ( {opts.val ? (
<tr className='tr'> <tr className="tr">
<td className='td' data-shared={`key_${opts.hash}`}>val</td> <td className="td" data-shared={`key_${opts.hash}`}>
<td className='td' data-id={`txLoggerTableHash${opts.hash}`} data-shared={`pair_${opts.hash}`}>{val} wei val
<CopyToClipboard content={`${val} wei`}/> </td>
<td
className="td"
data-id={`txLoggerTableHash${opts.hash}`}
data-shared={`pair_${opts.hash}`}
>
{val} wei
<CopyToClipboard content={`${val} wei`} />
</td> </td>
</tr> </tr>
) : null} ) : null}

@ -24,7 +24,7 @@ export interface ClipboardEvent<T = Element> extends SyntheticEvent<T, any> {
} }
export const RemixUiTerminal = (props: RemixUiTerminalProps) => { export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
const { call, _deps, on, config, event, gistHandler, logHtmlResponse, logResponse, version } = props.plugin const { call, _deps, on, config, event, gistHandler, version } = props.plugin
const [toggleDownUp, setToggleDownUp] = useState('fa-angle-double-down') const [toggleDownUp, setToggleDownUp] = useState('fa-angle-double-down')
const [_cmdIndex, setCmdIndex] = useState(-1) const [_cmdIndex, setCmdIndex] = useState(-1)
const [_cmdTemp, setCmdTemp] = useState('') const [_cmdTemp, setCmdTemp] = useState('')
@ -84,12 +84,15 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
} }
useEffect(() => { useEffect(() => {
scriptRunnerDispatch({ type: 'html', payload: { message: logHtmlResponse } }) props.onReady({
}, [logHtmlResponse]) logHtml: (html) => {
scriptRunnerDispatch({ type: 'html', payload: { message: [html.innerText] } })
useEffect(() => { },
scriptRunnerDispatch({ type: 'log', payload: { message: logResponse } }) log: (message) => {
}, [logResponse]) scriptRunnerDispatch({ type: 'log', payload: { message: [message] } })
}
})
}, [])
// events // events
useEffect(() => { useEffect(() => {
@ -115,7 +118,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
useEffect(() => { useEffect(() => {
scrollToBottom() scrollToBottom()
}, [newstate.journalBlocks.length, logHtmlResponse.length, toaster]) }, [newstate.journalBlocks.length, toaster])
function execute (file, cb) { function execute (file, cb) {
function _execute (content, cb) { function _execute (content, cb) {

@ -24,5 +24,6 @@ export const LISTEN_ON_NETWORK = 'listenOnNetWork'
export const CMD_HISTORY = 'cmdHistory' export const CMD_HISTORY = 'cmdHistory'
export interface RemixUiTerminalProps { export interface RemixUiTerminalProps {
plugin: any plugin: any,
onReady: (api: any) => void
} }

Loading…
Cancel
Save