Merge branch 'master' into remixd_terminal

pull/5370/head
David Zagi 3 years ago committed by GitHub
commit 97fea75484
  1. 2
      apps/remix-ide-e2e/src/helpers/init.ts
  2. 2
      apps/remix-ide-e2e/src/tests/pluginManager.spec.ts
  3. 3
      apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts
  4. 1
      apps/remix-ide-e2e/src/tests/verticalIconsPanel.spec.ts
  5. 17
      apps/remix-ide/src/app.js
  6. 14
      apps/remix-ide/src/app/components/vertical-icons.js
  7. 2
      apps/remix-ide/src/app/editor/sourceHighlighter.js
  8. 2
      apps/remix-ide/src/app/files/remixd-handle.js
  9. 18
      apps/remix-ide/src/app/files/slither-handle.js
  10. 2
      apps/remix-ide/src/app/panels/file-panel.js
  11. 5
      apps/remix-ide/src/app/tabs/compileTab/compileTab.js
  12. 2
      apps/remix-ide/src/app/tabs/test-tab.js
  13. 10
      apps/remix-ide/src/assets/js/intro.min.js
  14. 7
      apps/remix-ide/src/index.html
  15. 1
      apps/remix-ide/src/remixEngine.js
  16. 54
      apps/remix-ide/src/walkthroughService.js
  17. 2
      libs/remix-solidity/src/index.ts
  18. 10
      libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx
  19. 2
      libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx
  20. 5
      libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts
  21. 155
      libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx
  22. 1
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.css
  23. 8
      libs/remixd/src/bin/remixd.ts
  24. 4
      libs/remixd/src/index.ts
  25. 1
      libs/remixd/src/serviceList.ts
  26. 162
      libs/remixd/src/services/slitherClient.ts
  27. 12
      libs/remixd/src/types/index.ts
  28. 3
      libs/remixd/src/websocket.ts
  29. 5
      package-lock.json
  30. 1
      package.json

@ -7,6 +7,8 @@ export default function (browser: NightwatchBrowser, callback: VoidFunction, url
.url(url || 'http://127.0.0.1:8080') .url(url || 'http://127.0.0.1:8080')
.pause(5000) .pause(5000)
.switchBrowserTab(0) .switchBrowserTab(0)
.waitForElementVisible('[id="remixTourSkipbtn"]')
.click('[id="remixTourSkipbtn"]')
.fullscreenWindow(() => { .fullscreenWindow(() => {
if (preloadPlugins) { if (preloadPlugins) {
initModules(browser, () => { initModules(browser, () => {

@ -114,7 +114,7 @@ module.exports = {
.click('*[data-id="localPluginRadioButtonsidePanel"]') .click('*[data-id="localPluginRadioButtonsidePanel"]')
.click('*[data-id="modalDialogModalFooter"]') .click('*[data-id="modalDialogModalFooter"]')
.modalFooterOKClick() .modalFooterOKClick()
.waitForElementVisible('*[data-id="pluginManagerComponentDeactivateButtonremixIde"]', 60000) .waitForElementVisible('*[data-id="pluginManagerComponentDeactivateButtonremixIde"]', 100000)
}, },
'Should display error message for creating already existing plugin': function (browser: NightwatchBrowser) { 'Should display error message for creating already existing plugin': function (browser: NightwatchBrowser) {

@ -84,8 +84,7 @@ module.exports = {
.scrollAndClick('*[data-id="testTabRunTestsTabRunAction"]') .scrollAndClick('*[data-id="testTabRunTestsTabRunAction"]')
.pause(2000) .pause(2000)
.click('*[data-id="testTabRunTestsTabStopAction"]') .click('*[data-id="testTabRunTestsTabStopAction"]')
.waitForElementContainsText('*[data-id="testTabRunTestsTabStopAction"]', 'Stopping', 60000) .waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/ks2b_test.sol', 200000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/ks2b_test.sol', 120000)
.notContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/4_Ballot_test.sol') .notContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/4_Ballot_test.sol')
.notContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/simple_storage_test.sol') .notContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/simple_storage_test.sol')
.waitForElementContainsText('*[data-id="testTabTestsExecutionStopped"]', 'The test execution has been stopped', 60000) .waitForElementContainsText('*[data-id="testTabTestsExecutionStopped"]', 'The test execution has been stopped', 60000)

@ -22,6 +22,7 @@ module.exports = {
'Checks vertical icons panel contex menu deactivate': function (browser: NightwatchBrowser) { 'Checks vertical icons panel contex menu deactivate': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('div[data-id="remixIdeIconPanel"]', 10000) browser.waitForElementVisible('div[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('*[data-id="verticalIconsKinddebugger"]', 7000) .waitForElementVisible('*[data-id="verticalIconsKinddebugger"]', 7000)
.pause(5000)
.rightClick('[data-id="verticalIconsKinddebugger"]') .rightClick('[data-id="verticalIconsKinddebugger"]')
.click('*[id="menuitemdeactivate"]') .click('*[id="menuitemdeactivate"]')
.click('*[data-id="verticalIconsKindsettings"]') .click('*[data-id="verticalIconsKindsettings"]')

@ -7,6 +7,7 @@ import PanelsResize from './lib/panels-resize'
import { RemixEngine } from './remixEngine' import { RemixEngine } from './remixEngine'
import { RemixAppManager } from './remixAppManager' import { RemixAppManager } from './remixAppManager'
import { FramingService } from './framingService' import { FramingService } from './framingService'
import { WalkthroughService } from './walkthroughService'
import { MainView } from './app/panels/main-view' import { MainView } from './app/panels/main-view'
import { ThemeModule } from './app/tabs/theme-module' import { ThemeModule } from './app/tabs/theme-module'
import { NetworkModule } from './app/tabs/network-module' import { NetworkModule } from './app/tabs/network-module'
@ -112,7 +113,11 @@ const css = csjs`
fill: var(--secondary); fill: var(--secondary);
} }
.centered svg polygon { .centered svg polygon {
fill: var(--secondary); fill : var(--secondary);
}
.onboarding {
color : var(--text-info);
background-color : var(--info);
} }
.matomoBtn { .matomoBtn {
width : 100px; width : 100px;
@ -443,7 +448,8 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
test, test,
filePanel.remixdHandle, filePanel.remixdHandle,
filePanel.gitHandle, filePanel.gitHandle,
filePanel.hardhatHandle filePanel.hardhatHandle,
filePanel.slitherHandle
]) ])
if (isElectron()) { if (isElectron()) {
@ -496,7 +502,12 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
// Load and start the service who manager layout and frame // Load and start the service who manager layout and frame
const framingService = new FramingService(sidePanel, menuicons, mainview, this._components.resizeFeature) const framingService = new FramingService(sidePanel, menuicons, mainview, this._components.resizeFeature)
framingService.start(params)
if (params.embed) framingService.embed() if (params.embed) framingService.embed()
framingService.start(params)
const walkthroughService = new WalkthroughService(localStorage)
if (!params.code) {
walkthroughService.start()
}
} }

@ -69,12 +69,14 @@ export class VerticalIcons extends Plugin {
title = title.replace(/^\w/, c => c.toUpperCase()) title = title.replace(/^\w/, c => c.toUpperCase())
this.icons[name] = yo` this.icons[name] = yo`
<div <div
class="${css.icon}" class="${css.icon} m-2"
onclick="${() => { this.toggle(name) }}" onclick="${() => { this.toggle(name) }}"
plugin="${name}" plugin="${name}"
title="${title}" title="${title}"
oncontextmenu="${(e) => this.itemContextMenu(e, name, documentation)}" oncontextmenu="${(e) => this.itemContextMenu(e, name, documentation)}"
data-id="verticalIconsKind${name}"> data-id="verticalIconsKind${name}"
id="verticalIconsKind${name}"
>
<img class="image" src="${icon}" alt="${name}" /> <img class="image" src="${icon}" alt="${name}" />
</div>` </div>`
this.iconKind[kind || 'none'].appendChild(this.icons[name]) this.iconKind[kind || 'none'].appendChild(this.icons[name])
@ -249,13 +251,14 @@ export class VerticalIcons extends Plugin {
render () { render () {
const home = yo` const home = yo`
<div <div
class="${css.homeIcon}" class="m-1 mt-2 ${css.homeIcon}"
onclick="${async () => { onclick="${async () => {
await this.appManager.activatePlugin('home') await this.appManager.activatePlugin('home')
this.call('tabs', 'focus', 'home') this.call('tabs', 'focus', 'home')
}}" }}"
plugin="home" title="Home" plugin="home" title="Home"
data-id="verticalIconsHomeIcon" data-id="verticalIconsHomeIcon"
id="verticalIconsHomeIcon"
> >
${basicLogo()} ${basicLogo()}
</div> </div>
@ -270,6 +273,7 @@ export class VerticalIcons extends Plugin {
this.iconKind.settings = yo`<div id='settingsIcons' data-id="verticalIconsSettingsIcons"></div>` this.iconKind.settings = yo`<div id='settingsIcons' data-id="verticalIconsSettingsIcons"></div>`
this.view = yo` this.view = yo`
<div class="h-100">
<div class=${css.icons}> <div class=${css.icons}>
${home} ${home}
${this.iconKind.fileexplorer} ${this.iconKind.fileexplorer}
@ -281,6 +285,7 @@ export class VerticalIcons extends Plugin {
${this.iconKind.none} ${this.iconKind.none}
${this.iconKind.settings} ${this.iconKind.settings}
</div> </div>
</div>
` `
return this.view return this.view
} }
@ -292,7 +297,6 @@ const css = csjs`
width: 42px; width: 42px;
height: 42px; height: 42px;
margin-bottom: 20px; margin-bottom: 20px;
margin-left: -5px;
cursor: pointer; cursor: pointer;
} }
.homeIcon svg path { .homeIcon svg path {
@ -302,8 +306,6 @@ const css = csjs`
fill: var(--primary); fill: var(--primary);
} }
.icons { .icons {
margin-left: 10px;
margin-top: 15px;
} }
.icon { .icon {
cursor: pointer; cursor: pointer;

@ -37,7 +37,7 @@ class SourceHighlighter {
this.statementMarker = null this.statementMarker = null
this.fullLineMarker = null this.fullLineMarker = null
this.source = null this.source = null
if (lineColumnPos) { if (lineColumnPos && lineColumnPos.start && lineColumnPos.end) {
this.source = filePath this.source = filePath
this.style = style || 'var(--info)' this.style = style || 'var(--info)'
// if (!this.source) this.source = this._deps.fileManager.currentFile() // if (!this.source) this.source = this._deps.fileManager.currentFile()

@ -43,6 +43,7 @@ export class RemixdHandle extends WebsocketPlugin {
if (super.socket) super.deactivate() if (super.socket) super.deactivate()
// this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342 // this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342
if (this.appManager.actives.includes('hardhat')) this.appManager.deactivatePlugin('hardhat') if (this.appManager.actives.includes('hardhat')) this.appManager.deactivatePlugin('hardhat')
if (this.appManager.actives.includes('slither')) this.appManager.deactivatePlugin('slither')
this.localhostProvider.close((error) => { this.localhostProvider.close((error) => {
if (error) console.log(error) if (error) console.log(error)
}) })
@ -88,6 +89,7 @@ export class RemixdHandle extends WebsocketPlugin {
this.call('filePanel', 'setWorkspace', { name: LOCALHOST, isLocalhost: true }, true) this.call('filePanel', 'setWorkspace', { name: LOCALHOST, isLocalhost: true }, true)
}) })
this.call('manager', 'activatePlugin', 'hardhat') this.call('manager', 'activatePlugin', 'hardhat')
this.call('manager', 'activatePlugin', 'slither')
} }
} }
if (this.localhostProvider.isConnected()) { if (this.localhostProvider.isConnected()) {

@ -0,0 +1,18 @@
import { WebsocketPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json'
const profile = {
name: 'slither',
displayName: 'Slither',
url: 'ws://127.0.0.1:65523',
methods: ['analyse'],
description: 'Using Remixd daemon, run slither static analysis',
kind: 'other',
version: packageJson.version
}
export class SlitherHandle extends WebsocketPlugin {
constructor () {
super(profile)
}
}

@ -9,6 +9,7 @@ import { checkSpecialChars, checkSlash } from '../../lib/helper'
const { RemixdHandle } = require('../files/remixd-handle.js') const { RemixdHandle } = require('../files/remixd-handle.js')
const { GitHandle } = require('../files/git-handle.js') const { GitHandle } = require('../files/git-handle.js')
const { HardhatHandle } = require('../files/hardhat-handle.js') const { HardhatHandle } = require('../files/hardhat-handle.js')
const { SlitherHandle } = require('../files/slither-handle.js')
const globalRegistry = require('../../global/registry') const globalRegistry = require('../../global/registry')
const examples = require('../editor/examples') const examples = require('../editor/examples')
const GistHandler = require('../../lib/gist-handler') const GistHandler = require('../../lib/gist-handler')
@ -59,6 +60,7 @@ module.exports = class Filepanel extends ViewPlugin {
this.remixdHandle = new RemixdHandle(this._deps.fileProviders.localhost, appManager) this.remixdHandle = new RemixdHandle(this._deps.fileProviders.localhost, appManager)
this.gitHandle = new GitHandle() this.gitHandle = new GitHandle()
this.hardhatHandle = new HardhatHandle() this.hardhatHandle = new HardhatHandle()
this.slitherHandle = new SlitherHandle()
this.registeredMenuItems = [] this.registeredMenuItems = []
this.removedMenuItems = [] this.removedMenuItems = []
this.request = {} this.request = {}

@ -7,6 +7,7 @@ const profile = {
name: 'solidity-logic', name: 'solidity-logic',
displayName: 'Solidity compiler logic', displayName: 'Solidity compiler logic',
description: 'Compile solidity contracts - Logic', description: 'Compile solidity contracts - Logic',
methods: ['getCompilerState'],
version: packageJson.version version: packageJson.version
} }
@ -68,6 +69,10 @@ class CompileTab extends Plugin {
this.compiler.set('language', lang) this.compiler.set('language', lang)
} }
getCompilerState () {
return this.compiler.state
}
/** /**
* Compile a specific file of the file manager * Compile a specific file of the file manager
* @param {string} target the path to the file to compile * @param {string} target the path to the file to compile

@ -70,9 +70,9 @@ module.exports = class TestTab extends ViewPlugin {
}) })
} catch (e) { } catch (e) {
console.log(e) console.log(e)
}
this.data.allTests.push(file) this.data.allTests.push(file)
this.data.selectedTests.push(file) this.data.selectedTests.push(file)
}
}) })
this.on('filePanel', 'setWorkspace', async () => { this.on('filePanel', 'setWorkspace', async () => {

File diff suppressed because one or more lines are too long

@ -30,6 +30,7 @@
<title>Remix - Ethereum IDE</title> <title>Remix - Ethereum IDE</title>
<link rel="stylesheet" href="assets/css/pygment_trac.css"> <link rel="stylesheet" href="assets/css/pygment_trac.css">
<link rel="icon" type="x-icon" href="assets/img/icon.png"> <link rel="icon" type="x-icon" href="assets/img/icon.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intro.js/4.1.0/introjs.min.css">
<script src="assets/js/browserfs.min.js"></script> <script src="assets/js/browserfs.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<!-- Matomo --> <!-- Matomo -->
@ -56,6 +57,7 @@
} }
</script> </script>
<!-- End Matomo Code --> <!-- End Matomo Code -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/intro.js/2.7.0/introjs.min.css">
</head> </head>
<body> <body>
<script> <script>
@ -112,7 +114,10 @@
}) })
} }
</script> </script>
<script src="runtime.js" type="module"></script><script src="polyfills.js" type="module"></script><script src="vendor.js" type="module"></script> <script src="runtime.js" type="module"></script>
<script src="polyfills.js" type="module"></script>
<script src="vendor.js" type="module"></script>
<script src="https://kit.fontawesome.com/41dd021e94.js" crossorigin="anonymous"></script> <script src="https://kit.fontawesome.com/41dd021e94.js" crossorigin="anonymous"></script>
<script type="text/javascript" src="assets/js/intro.min.js"></script>
</body> </body>
</html> </html>

@ -10,6 +10,7 @@ export class RemixEngine extends Engine {
setPluginOption ({ name, kind }) { setPluginOption ({ name, kind }) {
if (kind === 'provider') return { queueTimeout: 60000 * 2 } if (kind === 'provider') return { queueTimeout: 60000 * 2 }
if (name === 'LearnEth') return { queueTimeout: 60000 } if (name === 'LearnEth') return { queueTimeout: 60000 }
if (name === 'slither') return { queueTimeout: 60000 * 4 } // Requires when a solc version is installed
return { queueTimeout: 10000 } return { queueTimeout: 10000 }
} }

@ -0,0 +1,54 @@
const introJs = require('intro.js')
export class WalkthroughService {
constructor (params) {
this.params = params
}
start (params) {
if (!localStorage.getItem('hadTour_initial')) {
introJs().setOptions({
steps: [{
title: 'Welcome to Remix IDE',
intro: 'Click to launch the Home tab that contains links, tips, and shortcuts..',
element: document.querySelector('#verticalIconsHomeIcon'),
tooltipClass: 'bg-light text-dark',
position: 'right'
},
{
element: document.querySelector('#compileIcons'),
title: 'Solidity Compiler',
intro: 'Having selected a .sol file in the File Explorers (the icon above), compile it with the Solidity Compiler.',
tooltipClass: 'bg-light text-dark',
position: 'right'
},
{
title: 'Deploy your contract',
element: document.querySelector('#runIcons'),
intro: 'Choose a chain, deploy a contract and play with your functions.',
tooltipClass: 'bg-light text-dark',
position: 'right'
}
]
}).onafterchange((targetElement) => {
const header = document.getElementsByClassName('introjs-tooltip-header')[0]
if (header) {
header.classList.add('d-flex')
header.classList.add('justify-content-between')
header.classList.add('text-nowrap')
header.classList.add('pr-0')
}
const skipbutton = document.getElementsByClassName('introjs-skipbutton')[0]
if (skipbutton) {
skipbutton.classList.add('ml-3')
skipbutton.classList.add('text-decoration-none')
skipbutton.id = 'remixTourSkipbtn'
}
}).start()
localStorage.setItem('hadTour_initial', true)
}
}
startFeatureTour () {
}
}

@ -3,4 +3,4 @@ export { compile } from './compiler/compiler-helpers'
export { default as CompilerInput } from './compiler/compiler-input' export { default as CompilerInput } from './compiler/compiler-input'
export { CompilerAbstract } from './compiler/compiler-abstract' export { CompilerAbstract } from './compiler/compiler-abstract'
export * from './compiler/types' export * from './compiler/types'
export * from './compiler/compiler-utils' export { promisedMiniXhr, pathToURL, baseURLBin, baseURLWasm, canUseWorker, urlFromVersion } from './compiler/compiler-utils'

@ -1,4 +1,4 @@
import React from 'react' //eslint-disable-line import React, { CSSProperties } from 'react' //eslint-disable-line
import './remix-ui-checkbox.css' import './remix-ui-checkbox.css'
/* eslint-disable-next-line */ /* eslint-disable-next-line */
@ -12,6 +12,8 @@ export interface RemixUiCheckboxProps {
id?: string id?: string
itemName?: string itemName?: string
categoryId?: string categoryId?: string
visibility?: string
display?: string
} }
export const RemixUiCheckbox = ({ export const RemixUiCheckbox = ({
@ -23,10 +25,12 @@ export const RemixUiCheckbox = ({
checked, checked,
onChange, onChange,
itemName, itemName,
categoryId categoryId,
visibility,
display = 'flex'
}: RemixUiCheckboxProps) => { }: RemixUiCheckboxProps) => {
return ( return (
<div className="listenOnNetwork_2A0YE0 custom-control custom-checkbox" style={{ display: 'flex', alignItems: 'center' }} onClick={onClick}> <div className="listenOnNetwork_2A0YE0 custom-control custom-checkbox" style={{ display: display, alignItems: 'center', visibility: visibility } as CSSProperties } onClick={onClick}>
<input <input
id={id} id={id}
type={inputType} type={inputType}

@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef, useReducer } from 'react' // eslint
import semver from 'semver' import semver from 'semver'
import { CompilerContainerProps, ConfigurationSettings } from './types' import { CompilerContainerProps, ConfigurationSettings } from './types'
import * as helper from '../../../../../apps/remix-ide/src/lib/helper' import * as helper from '../../../../../apps/remix-ide/src/lib/helper'
import { canUseWorker, baseURLBin, baseURLWasm, urlFromVersion, pathToURL, promisedMiniXhr } from '@remix-project/remix-solidity' // @ts-ignore import { canUseWorker, baseURLBin, baseURLWasm, urlFromVersion, pathToURL, promisedMiniXhr } from '@remix-project/remix-solidity'
import { compilerReducer, compilerInitialState } from './reducers/compiler' import { compilerReducer, compilerInitialState } from './reducers/compiler'
import { resetEditorMode, listenToEvents } from './actions/compiler' import { resetEditorMode, listenToEvents } from './actions/compiler'

@ -7,6 +7,7 @@ const profile = {
name: 'solidity-logic', name: 'solidity-logic',
displayName: 'Solidity compiler logic', displayName: 'Solidity compiler logic',
description: 'Compile solidity contracts - Logic', description: 'Compile solidity contracts - Logic',
methods: ['getCompilerState'],
version: packageJson.version version: packageJson.version
} }
export class CompileTab extends Plugin { export class CompileTab extends Plugin {
@ -60,6 +61,10 @@ export class CompileTab extends Plugin {
this.compiler.set('evmVersion', this.evmVersion) this.compiler.set('evmVersion', this.evmVersion)
} }
getCompilerState () {
return this.compiler.state
}
/** /**
* Set the compiler to using Solidity or Yul (default to Solidity) * Set the compiler to using Solidity or Yul (default to Solidity)
* @params lang {'Solidity' | 'Yul'} ... * @params lang {'Solidity' | 'Yul'} ...

@ -55,10 +55,12 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
return indexOfCategory return indexOfCategory
} }
const [autoRun, setAutoRun] = useState(true) const [autoRun, setAutoRun] = useState(true)
const [slitherEnabled, setSlitherEnabled] = useState(false)
const [showSlither, setShowSlither] = useState('hidden')
const [categoryIndex, setCategoryIndex] = useState(groupedModuleIndex(groupedModules)) const [categoryIndex, setCategoryIndex] = useState(groupedModuleIndex(groupedModules))
const warningContainer = React.useRef(null) const warningContainer = React.useRef(null)
const [warningState, setWarningState] = useState([]) const [warningState, setWarningState] = useState({})
const [state, dispatch] = useReducer(analysisReducer, initialState) const [state, dispatch] = useReducer(analysisReducer, initialState)
useEffect(() => { useEffect(() => {
@ -66,7 +68,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
}, []) }, [])
useEffect(() => { useEffect(() => {
setWarningState([]) setWarningState({})
if (autoRun) { if (autoRun) {
if (state.data !== null) { if (state.data !== null) {
run(state.data, state.source, state.file) run(state.data, state.source, state.file)
@ -77,6 +79,20 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
return () => { } return () => { }
}, [state]) }, [state])
useEffect(() => {
props.analysisModule.on('filePanel', 'setWorkspace', (currentWorkspace) => {
// Reset warning state
setWarningState([])
// Reset badge
props.event.trigger('staticAnaysisWarning', [])
// Reset state
dispatch({ type: '', payload: {} })
// Show 'Enable Slither Analysis' checkbox
if (currentWorkspace && currentWorkspace.isLocalhost === true) setShowSlither('visible')
})
return () => { }
}, [props.analysisModule])
const message = (name, warning, more, fileName, locationString) : string => { const message = (name, warning, more, fileName, locationString) : string => {
return (` return (`
<span className='d-flex flex-column'> <span className='d-flex flex-column'>
@ -91,12 +107,35 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
) )
} }
const showWarnings = (warningMessage, groupByKey) => {
const resultArray = []
warningMessage.map(x => {
resultArray.push(x)
})
function groupBy (objectArray, property) {
return objectArray.reduce((acc, obj) => {
const key = obj[property]
if (!acc[key]) {
acc[key] = []
}
// Add object to list for given key's value
acc[key].push(obj)
return acc
}, {})
}
const groupedCategory = groupBy(resultArray, groupByKey)
setWarningState(groupedCategory)
}
const run = (lastCompilationResult, lastCompilationSource, currentFile) => { const run = (lastCompilationResult, lastCompilationSource, currentFile) => {
if (state.data !== null) { if (state.data !== null) {
if (lastCompilationResult && categoryIndex.length > 0) { if (lastCompilationResult && (categoryIndex.length > 0 || slitherEnabled)) {
let warningCount = 0 let warningCount = 0
const warningMessage = [] const warningMessage = []
const warningErrors = []
// Remix Analysis
runner.run(lastCompilationResult, categoryIndex, results => { runner.run(lastCompilationResult, categoryIndex, results => {
results.map((result) => { results.map((result) => {
let moduleName let moduleName
@ -107,7 +146,6 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
} }
}) })
}) })
const warningErrors = []
result.report.map((item) => { result.report.map((item) => {
let location: any = {} let location: any = {}
let locationString = 'not available' let locationString = 'not available'
@ -151,28 +189,73 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
warningMessage.push({ msg, options, hasWarning: true, warningModuleName: moduleName }) warningMessage.push({ msg, options, hasWarning: true, warningModuleName: moduleName })
}) })
}) })
const resultArray = [] // Slither Analysis
warningMessage.map(x => { if (slitherEnabled) {
resultArray.push(x) props.analysisModule.call('solidity-logic', 'getCompilerState').then((compilerState) => {
}) const { currentVersion, optimize, evmVersion } = compilerState
function groupBy (objectArray, property) { props.analysisModule.call('terminal', 'log', { type: 'info', value: '[Slither Analysis]: Running...' })
return objectArray.reduce((acc, obj) => { props.analysisModule.call('slither', 'analyse', state.file, { currentVersion, optimize, evmVersion }).then((result) => {
const key = obj[property] if (result.status) {
if (!acc[key]) { props.analysisModule.call('terminal', 'log', { type: 'info', value: `[Slither Analysis]: Analysis Completed!! ${result.count} warnings found.` })
acc[key] = [] const report = result.data
report.map((item) => {
let location: any = {}
let locationString = 'not available'
let column = 0
let row = 0
let fileName = currentFile
if (item.sourceMap && item.sourceMap.length) {
const fileIndex = Object.keys(lastCompilationResult.sources).indexOf(item.sourceMap[0].source_mapping.filename_relative)
if (fileIndex >= 0) {
location = {
start: item.sourceMap[0].source_mapping.start,
length: item.sourceMap[0].source_mapping.length
} }
// Add object to list for given key's value location = props.analysisModule._deps.offsetToLineColumnConverter.offsetToLineColumn(
acc[key].push(obj) location,
return acc fileIndex,
}, {}) lastCompilationSource.sources,
lastCompilationResult.sources
)
row = location.start.line
column = location.start.column
locationString = row + 1 + ':' + column + ':'
fileName = Object.keys(lastCompilationResult.sources)[fileIndex]
} }
}
const groupedCategory = groupBy(resultArray, 'warningModuleName') warningCount++
setWarningState(groupedCategory) const msg = message(item.title, item.description, item.more, fileName, locationString)
const options = {
type: 'warning',
useSpan: true,
errFile: fileName,
fileName,
errLine: row,
errCol: column,
item: { warning: item.description },
name: item.title,
locationString,
more: item.more,
location: location
}
warningErrors.push(options)
warningMessage.push({ msg, options, hasWarning: true, warningModuleName: 'Slither Analysis' })
}) })
if (categoryIndex.length > 0) { showWarnings(warningMessage, 'warningModuleName')
props.event.trigger('staticAnaysisWarning', [warningCount]) props.event.trigger('staticAnaysisWarning', [warningCount])
} }
}).catch((error) => {
console.log('Error found:', error) // This should be removed once testing done
props.analysisModule.call('terminal', 'log', { type: 'error', value: '[Slither Analysis]: Error occured! See remixd console for details.' })
showWarnings(warningMessage, 'warningModuleName')
})
})
} else {
showWarnings(warningMessage, 'warningModuleName')
props.event.trigger('staticAnaysisWarning', [warningCount])
}
})
} else { } else {
if (categoryIndex.length) { if (categoryIndex.length) {
warningContainer.current.innerText = 'No compiled AST available' warningContainer.current.innerText = 'No compiled AST available'
@ -208,6 +291,14 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
} }
} }
const handleSlitherEnabled = () => {
if (slitherEnabled) {
setSlitherEnabled(false)
} else {
setSlitherEnabled(true)
}
}
const handleAutoRun = () => { const handleAutoRun = () => {
if (autoRun) { if (autoRun) {
setAutoRun(false) setAutoRun(false)
@ -305,7 +396,18 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
label="Autorun" label="Autorun"
onChange={() => {}} onChange={() => {}}
/> />
<Button buttonText="Run" onClick={() => run(state.data, state.source, state.file)} disabled={state.data === null || categoryIndex.length === 0 }/> <Button buttonText="Run" onClick={() => run(state.data, state.source, state.file)} disabled={(state.data === null || categoryIndex.length === 0) && !slitherEnabled }/>
</div>
<div className="d-flex" id="enableSlitherAnalysis">
<RemixUiCheckbox
id="enableSlither"
inputType="checkbox"
onClick={handleSlitherEnabled}
checked={slitherEnabled}
label="Enable Slither Analysis"
onChange={() => {}}
visibility = {showSlither}
/>
</div> </div>
</div> </div>
<div id="staticanalysismodules" className="list-group list-group-flush"> <div id="staticanalysismodules" className="list-group list-group-flush">
@ -318,7 +420,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
} }
</div> </div>
<div className="mt-2 p-2 d-flex border-top flex-column"> <div className="mt-2 p-2 d-flex border-top flex-column">
<span>last results for:</span> <span>Last results for:</span>
<span <span
className="text-break break-word word-break font-weight-bold" className="text-break break-word word-break font-weight-bold"
id="staticAnalysisCurrentFile" id="staticAnalysisCurrentFile"
@ -326,6 +428,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
{state.file} {state.file}
</span> </span>
</div> </div>
<br/>
{Object.entries(warningState).length > 0 && {Object.entries(warningState).length > 0 &&
<div id='staticanalysisresult' > <div id='staticanalysisresult' >
<div className="mb-4"> <div className="mb-4">
@ -333,9 +436,9 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
(Object.entries(warningState).map((element, index) => ( (Object.entries(warningState).map((element, index) => (
<div key={index}> <div key={index}>
<span className="text-dark h6">{element[0]}</span> <span className="text-dark h6">{element[0]}</span>
{element[1].map((x, i) => ( {element[1]['map']((x, i) => ( // eslint-disable-line dot-notation
x.hasWarning ? ( x.hasWarning ? ( // eslint-disable-next-line dot-notation
<div id={`staticAnalysisModule${element[1].warningModuleName}`} key={i}> <div id={`staticAnalysisModule${element[1]['warningModuleName']}`} key={i}>
<ErrorRenderer message={x.msg} opt={x.options} warningErrors={ x.warningErrors} editor={props.analysisModule}/> <ErrorRenderer message={x.msg} opt={x.options} warningErrors={ x.warningErrors} editor={props.analysisModule}/>
</div> </div>

@ -11,6 +11,7 @@
position : relative; position : relative;
width : 100%; width : 100%;
padding-left : 6px; padding-left : 6px;
padding-right : 6px;
padding-top : 6px; padding-top : 6px;
} }
.remixui_fileExplorerTree { .remixui_fileExplorerTree {

@ -25,6 +25,7 @@ async function warnLatestVersion () {
const services = { const services = {
git: (readOnly: boolean) => new servicesList.GitClient(readOnly), git: (readOnly: boolean) => new servicesList.GitClient(readOnly),
hardhat: (readOnly: boolean) => new servicesList.HardhatClient(readOnly), hardhat: (readOnly: boolean) => new servicesList.HardhatClient(readOnly),
slither: (readOnly: boolean) => new servicesList.SlitherClient(readOnly),
folder: (readOnly: boolean) => new servicesList.Sharedfolder(readOnly) folder: (readOnly: boolean) => new servicesList.Sharedfolder(readOnly)
} }
@ -32,11 +33,12 @@ const services = {
const ports = { const ports = {
git: 65521, git: 65521,
hardhat: 65522, hardhat: 65522,
slither: 65523,
folder: 65520 folder: 65520
} }
const killCallBack: Array<Function> = [] const killCallBack: Array<Function> = []
function startService<S extends 'git' | 'hardhat' | 'folder'> (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder, error?:Error) => void) { function startService<S extends 'git' | 'hardhat' | 'slither' | 'folder'> (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder, error?:Error) => void) {
const socket = new WebSocket(ports[service], { remixIdeUrl: program.remixIde }, () => services[service](program.readOnly || false)) const socket = new WebSocket(ports[service], { remixIdeUrl: program.remixIde }, () => services[service](program.readOnly || false))
socket.start(callback) socket.start(callback)
killCallBack.push(socket.close.bind(socket)) killCallBack.push(socket.close.bind(socket))
@ -94,6 +96,10 @@ function errorHandler (error: any, service: string) {
sharedFolderClient.setupNotifications(program.sharedFolder) sharedFolderClient.setupNotifications(program.sharedFolder)
sharedFolderClient.sharedFolder(program.sharedFolder) sharedFolderClient.sharedFolder(program.sharedFolder)
}) })
startService('slither', (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => {
sharedFolderClient.setWebSocket(ws)
sharedFolderClient.sharedFolder(program.sharedFolder)
})
// Run hardhat service if a hardhat project is shared as folder // Run hardhat service if a hardhat project is shared as folder
const hardhatConfigFilePath = absolutePath('./', program.sharedFolder) + '/hardhat.config.js' const hardhatConfigFilePath = absolutePath('./', program.sharedFolder) + '/hardhat.config.js'
const isHardhatProject = fs.existsSync(hardhatConfigFilePath) const isHardhatProject = fs.existsSync(hardhatConfigFilePath)

@ -2,6 +2,7 @@
import { RemixdClient as sharedFolder } from './services/remixdClient' import { RemixdClient as sharedFolder } from './services/remixdClient'
import { GitClient } from './services/gitClient' import { GitClient } from './services/gitClient'
import { HardhatClient } from './services/hardhatClient' import { HardhatClient } from './services/hardhatClient'
import { SlitherClient } from './services/slitherClient'
import Websocket from './websocket' import Websocket from './websocket'
import * as utils from './utils' import * as utils from './utils'
@ -11,6 +12,7 @@ module.exports = {
services: { services: {
sharedFolder, sharedFolder,
GitClient, GitClient,
HardhatClient HardhatClient,
SlitherClient
} }
} }

@ -1,3 +1,4 @@
export { RemixdClient as Sharedfolder } from './services/remixdClient' export { RemixdClient as Sharedfolder } from './services/remixdClient'
export { GitClient } from './services/gitClient' export { GitClient } from './services/gitClient'
export { HardhatClient } from './services/hardhatClient' export { HardhatClient } from './services/hardhatClient'
export { SlitherClient } from './services/slitherClient'

@ -0,0 +1,162 @@
/* eslint dot-notation: "off" */
import * as WS from 'ws' // eslint-disable-line
import { PluginClient } from '@remixproject/plugin'
import { existsSync, readFileSync, readdirSync } from 'fs'
import { OutputStandard } from '../types' // eslint-disable-line
const { spawn, execSync } = require('child_process')
export class SlitherClient extends PluginClient {
methods: Array<string>
websocket: WS
currentSharedFolder: string
constructor (private readOnly = false) {
super()
this.methods = ['analyse']
}
setWebSocket (websocket: WS): void {
this.websocket = websocket
}
sharedFolder (currentSharedFolder: string): void {
this.currentSharedFolder = currentSharedFolder
}
mapNpmDepsDir (list) {
const remixNpmDepsPath = `${this.currentSharedFolder}/.deps/npm`
const localNpmDepsPath = `${this.currentSharedFolder}/node_modules`
const npmDepsExists = existsSync(remixNpmDepsPath)
const nodeModulesExists = existsSync(localNpmDepsPath)
let isLocalDep = false
let isRemixDep = false
let allowPathString = ''
let remapString = ''
for (const e of list) {
const importPath = e.replace(/import ['"]/g, '').trim()
const packageName = importPath.split('/')[0]
if (nodeModulesExists && readdirSync(localNpmDepsPath).includes(packageName)) {
isLocalDep = true
remapString += `${packageName}=./node_modules/${packageName} `
} else if (npmDepsExists && readdirSync(remixNpmDepsPath).includes(packageName)) {
isRemixDep = true
remapString += `${packageName}=./.deps/npm/${packageName} `
}
}
if (isLocalDep) allowPathString += './node_modules,'
if (isRemixDep) allowPathString += './.deps/npm,'
return { remapString, allowPathString }
}
transform (detectors: Record<string, any>[]): OutputStandard[] {
const standardReport: OutputStandard[] = []
for (const e of detectors) {
const obj = {} as OutputStandard
obj.description = e.description
obj.title = e.check
obj.confidence = e.confidence
obj.severity = e.impact
obj.sourceMap = e.elements.map((element) => {
delete element.source_mapping.filename_used
delete element.source_mapping.filename_absolute
return element
})
standardReport.push(obj)
}
return standardReport
}
analyse (filePath: string, compilerConfig: Record<string, any>) {
return new Promise((resolve, reject) => {
if (this.readOnly) {
const errMsg: string = '[Slither Analysis]: Cannot analyse in read-only mode'
return reject(new Error(errMsg))
}
const options = { cwd: this.currentSharedFolder, shell: true }
const { currentVersion, optimize, evmVersion } = compilerConfig
if (currentVersion && currentVersion.includes('+commit')) {
// Get compiler version with commit id e.g: 0.8.2+commit.661d110
const versionString: string = currentVersion.substring(0, currentVersion.indexOf('+commit') + 16)
console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Compiler version is ${versionString}`)
let solcOutput: Buffer
// Check solc current installed version
try {
solcOutput = execSync('solc --version', options)
} catch (err) {
console.log(err)
reject(new Error('Error in running solc command'))
}
if (!solcOutput.toString().includes(versionString)) {
console.log('\x1b[32m%s\x1b[0m', '[Slither Analysis]: Compiler version is different from installed solc version')
// Get compiler version without commit id e.g: 0.8.2
const version: string = versionString.substring(0, versionString.indexOf('+commit'))
// List solc versions installed using solc-select
try {
const solcSelectInstalledVersions: Buffer = execSync('solc-select versions', options)
// Check if required version is already installed
if (!solcSelectInstalledVersions.toString().includes(version)) {
console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Installing ${version} using solc-select`)
// Install required version
execSync(`solc-select install ${version}`, options)
}
console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Setting ${version} as current solc version using solc-select`)
// Set solc current version as required version
execSync(`solc-select use ${version}`, options)
} catch (err) {
console.log(err)
reject(new Error('Error in running solc-select command'))
}
} else console.log('\x1b[32m%s\x1b[0m', '[Slither Analysis]: Compiler version is same as installed solc version')
}
// Allow paths and set solc remapping for import URLs
const fileContent = readFileSync(`${this.currentSharedFolder}/${filePath}`, 'utf8')
const importsArr = fileContent.match(/import ['"][^.|..](.+?)['"];/g)
let allowPaths = ''; let remaps = ''
if (importsArr?.length) {
const { remapString, allowPathString } = this.mapNpmDepsDir(importsArr)
allowPaths = allowPathString
remaps = remapString.trim()
}
const allowPathsOption: string = allowPaths ? `--allow-paths ${allowPaths}` : ''
const optimizeOption: string = optimize ? ' --optimize ' : ''
const evmOption: string = evmVersion ? ` --evm-version ${evmVersion}` : ''
const solcArgs: string = optimizeOption || evmOption || allowPathsOption ? `--solc-args '${allowPathsOption}${optimizeOption}${evmOption}'` : ''
const solcRemaps = remaps ? `--solc-remaps "${remaps}"` : ''
const outputFile: string = 'remix-slitherReport_' + Math.floor(Date.now() / 1000) + '.json'
const cmd: string = `slither ${filePath} ${solcArgs} ${solcRemaps} --json ${outputFile}`
console.log('\x1b[32m%s\x1b[0m', '[Slither Analysis]: Running Slither...')
// Added `stdio: 'ignore'` as for contract with NPM imports analysis which is exported in 'stderr'
// get too big and hangs the process. We process analysis from the report file only
const child = spawn(cmd, { cwd: this.currentSharedFolder, shell: true, stdio: 'ignore' })
const response = {}
child.on('close', () => {
const outputFileAbsPath: string = `${this.currentSharedFolder}/${outputFile}`
// Check if slither report file exists
if (existsSync(outputFileAbsPath)) {
let report = readFileSync(outputFileAbsPath, 'utf8')
report = JSON.parse(report)
if (report['success']) {
response['status'] = true
if (!report['results'] || !report['results'].detectors || !report['results'].detectors.length) {
response['count'] = 0
} else {
const { detectors } = report['results']
response['count'] = detectors.length
response['data'] = this.transform(detectors)
}
console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Analysis Completed!! ${response['count']} warnings found.`)
resolve(response)
} else {
console.log(report['error'])
reject(new Error('Error in running Slither Analysis.'))
}
} else reject(new Error('Error in generating Slither Analysis Report. Make sure Slither is properly installed.'))
})
})
}
}

@ -1,6 +1,18 @@
import * as ServiceList from '../serviceList' import * as ServiceList from '../serviceList'
import * as Websocket from 'ws' import * as Websocket from 'ws'
export interface OutputStandard {
description: string
title: string
confidence: string
severity: string
sourceMap: any
category?: string
reference?: string
example?: any
[key: string]: any
}
type ServiceListKeys = keyof typeof ServiceList; type ServiceListKeys = keyof typeof ServiceList;
export type Service = typeof ServiceList[ServiceListKeys] export type Service = typeof ServiceList[ServiceListKeys]

@ -19,7 +19,8 @@ export default class WebSocket {
const listeners = { const listeners = {
65520: 'remixd', 65520: 'remixd',
65521: 'git', 65521: 'git',
65522: 'hardhat' 65522: 'hardhat',
65523: 'slither'
} }
this.server.on('error', (error: Error) => { this.server.on('error', (error: Error) => {

5
package-lock.json generated

@ -20133,6 +20133,11 @@
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="
}, },
"intro.js": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/intro.js/-/intro.js-4.1.0.tgz",
"integrity": "sha512-+Y+UsP+yvqqlEOjFExMBXKopn3nzwc91PaUl0SrvqiVs6ztko1DzfkoXR2AnfirZVZZhr5Aej6wlXRlvIkuMcA=="
},
"invariant": { "invariant": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",

@ -159,6 +159,7 @@
"form-data": "^4.0.0", "form-data": "^4.0.0",
"fs-extra": "^3.0.1", "fs-extra": "^3.0.1",
"http-server": "^0.11.1", "http-server": "^0.11.1",
"intro.js": "^4.1.0",
"isbinaryfile": "^3.0.2", "isbinaryfile": "^3.0.2",
"isomorphic-git": "^1.8.2", "isomorphic-git": "^1.8.2",
"jquery": "^3.3.1", "jquery": "^3.3.1",

Loading…
Cancel
Save