Merge branch 'remixd_terminal' of https://github.com/ethereum/remix-project into remixd_terminal

pull/1342/head
davidzagi93@gmail.com 3 years ago
commit cd5d64a284
  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')
.pause(5000)
.switchBrowserTab(0)
.waitForElementVisible('[id="remixTourSkipbtn"]')
.click('[id="remixTourSkipbtn"]')
.fullscreenWindow(() => {
if (preloadPlugins) {
initModules(browser, () => {

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

@ -84,8 +84,7 @@ module.exports = {
.scrollAndClick('*[data-id="testTabRunTestsTabRunAction"]')
.pause(2000)
.click('*[data-id="testTabRunTestsTabStopAction"]')
.waitForElementContainsText('*[data-id="testTabRunTestsTabStopAction"]', 'Stopping', 60000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/ks2b_test.sol', 120000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/ks2b_test.sol', 200000)
.notContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/4_Ballot_test.sol')
.notContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '/tests/simple_storage_test.sol')
.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) {
browser.waitForElementVisible('div[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('*[data-id="verticalIconsKinddebugger"]', 7000)
.pause(5000)
.rightClick('[data-id="verticalIconsKinddebugger"]')
.click('*[id="menuitemdeactivate"]')
.click('*[data-id="verticalIconsKindsettings"]')

@ -7,6 +7,7 @@ import PanelsResize from './lib/panels-resize'
import { RemixEngine } from './remixEngine'
import { RemixAppManager } from './remixAppManager'
import { FramingService } from './framingService'
import { WalkthroughService } from './walkthroughService'
import { MainView } from './app/panels/main-view'
import { ThemeModule } from './app/tabs/theme-module'
import { NetworkModule } from './app/tabs/network-module'
@ -112,7 +113,11 @@ const css = csjs`
fill: var(--secondary);
}
.centered svg polygon {
fill: var(--secondary);
fill : var(--secondary);
}
.onboarding {
color : var(--text-info);
background-color : var(--info);
}
.matomoBtn {
width : 100px;
@ -444,7 +449,8 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
test,
filePanel.remixdHandle,
filePanel.gitHandle,
filePanel.hardhatHandle
filePanel.hardhatHandle,
filePanel.slitherHandle
])
if (isElectron()) {
@ -497,7 +503,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
const framingService = new FramingService(sidePanel, menuicons, mainview, this._components.resizeFeature)
framingService.start(params)
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())
this.icons[name] = yo`
<div
class="${css.icon}"
class="${css.icon} m-2"
onclick="${() => { this.toggle(name) }}"
plugin="${name}"
title="${title}"
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}" />
</div>`
this.iconKind[kind || 'none'].appendChild(this.icons[name])
@ -249,13 +251,14 @@ export class VerticalIcons extends Plugin {
render () {
const home = yo`
<div
class="${css.homeIcon}"
class="m-1 mt-2 ${css.homeIcon}"
onclick="${async () => {
await this.appManager.activatePlugin('home')
this.call('tabs', 'focus', 'home')
}}"
plugin="home" title="Home"
data-id="verticalIconsHomeIcon"
id="verticalIconsHomeIcon"
>
${basicLogo()}
</div>
@ -270,6 +273,7 @@ export class VerticalIcons extends Plugin {
this.iconKind.settings = yo`<div id='settingsIcons' data-id="verticalIconsSettingsIcons"></div>`
this.view = yo`
<div class="h-100">
<div class=${css.icons}>
${home}
${this.iconKind.fileexplorer}
@ -281,6 +285,7 @@ export class VerticalIcons extends Plugin {
${this.iconKind.none}
${this.iconKind.settings}
</div>
</div>
`
return this.view
}
@ -292,7 +297,6 @@ const css = csjs`
width: 42px;
height: 42px;
margin-bottom: 20px;
margin-left: -5px;
cursor: pointer;
}
.homeIcon svg path {
@ -302,8 +306,6 @@ const css = csjs`
fill: var(--primary);
}
.icons {
margin-left: 10px;
margin-top: 15px;
}
.icon {
cursor: pointer;

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

@ -43,6 +43,7 @@ export class RemixdHandle extends WebsocketPlugin {
if (super.socket) super.deactivate()
// 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('slither')) this.appManager.deactivatePlugin('slither')
this.localhostProvider.close((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('manager', 'activatePlugin', 'hardhat')
this.call('manager', 'activatePlugin', 'slither')
}
}
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 { GitHandle } = require('../files/git-handle.js')
const { HardhatHandle } = require('../files/hardhat-handle.js')
const { SlitherHandle } = require('../files/slither-handle.js')
const globalRegistry = require('../../global/registry')
const examples = require('../editor/examples')
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.gitHandle = new GitHandle()
this.hardhatHandle = new HardhatHandle()
this.slitherHandle = new SlitherHandle()
this.registeredMenuItems = []
this.removedMenuItems = []
this.request = {}

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

@ -70,9 +70,9 @@ module.exports = class TestTab extends ViewPlugin {
})
} catch (e) {
console.log(e)
}
this.data.allTests.push(file)
this.data.selectedTests.push(file)
}
})
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>
<link rel="stylesheet" href="assets/css/pygment_trac.css">
<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>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<!-- Matomo -->
@ -56,6 +57,7 @@
}
</script>
<!-- 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>
<body>
<script>
@ -112,7 +114,10 @@
})
}
</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 type="text/javascript" src="assets/js/intro.min.js"></script>
</body>
</html>

@ -10,6 +10,7 @@ export class RemixEngine extends Engine {
setPluginOption ({ name, kind }) {
if (kind === 'provider') return { queueTimeout: 60000 * 2 }
if (name === 'LearnEth') return { queueTimeout: 60000 }
if (name === 'slither') return { queueTimeout: 60000 * 4 } // Requires when a solc version is installed
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 { CompilerAbstract } from './compiler/compiler-abstract'
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'
/* eslint-disable-next-line */
@ -12,6 +12,8 @@ export interface RemixUiCheckboxProps {
id?: string
itemName?: string
categoryId?: string
visibility?: string
display?: string
}
export const RemixUiCheckbox = ({
@ -23,10 +25,12 @@ export const RemixUiCheckbox = ({
checked,
onChange,
itemName,
categoryId
categoryId,
visibility,
display = 'flex'
}: RemixUiCheckboxProps) => {
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
id={id}
type={inputType}

@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef, useReducer } from 'react' // eslint
import semver from 'semver'
import { CompilerContainerProps, ConfigurationSettings } from './types'
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 { resetEditorMode, listenToEvents } from './actions/compiler'

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

@ -55,10 +55,12 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
return indexOfCategory
}
const [autoRun, setAutoRun] = useState(true)
const [slitherEnabled, setSlitherEnabled] = useState(false)
const [showSlither, setShowSlither] = useState('hidden')
const [categoryIndex, setCategoryIndex] = useState(groupedModuleIndex(groupedModules))
const warningContainer = React.useRef(null)
const [warningState, setWarningState] = useState([])
const [warningState, setWarningState] = useState({})
const [state, dispatch] = useReducer(analysisReducer, initialState)
useEffect(() => {
@ -66,7 +68,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
}, [])
useEffect(() => {
setWarningState([])
setWarningState({})
if (autoRun) {
if (state.data !== null) {
run(state.data, state.source, state.file)
@ -77,6 +79,20 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
return () => { }
}, [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 => {
return (`
<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) => {
if (state.data !== null) {
if (lastCompilationResult && categoryIndex.length > 0) {
if (lastCompilationResult && (categoryIndex.length > 0 || slitherEnabled)) {
let warningCount = 0
const warningMessage = []
const warningErrors = []
// Remix Analysis
runner.run(lastCompilationResult, categoryIndex, results => {
results.map((result) => {
let moduleName
@ -107,7 +146,6 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
}
})
})
const warningErrors = []
result.report.map((item) => {
let location: any = {}
let locationString = 'not available'
@ -151,28 +189,73 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
warningMessage.push({ msg, options, hasWarning: true, warningModuleName: moduleName })
})
})
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] = []
// Slither Analysis
if (slitherEnabled) {
props.analysisModule.call('solidity-logic', 'getCompilerState').then((compilerState) => {
const { currentVersion, optimize, evmVersion } = compilerState
props.analysisModule.call('terminal', 'log', { type: 'info', value: '[Slither Analysis]: Running...' })
props.analysisModule.call('slither', 'analyse', state.file, { currentVersion, optimize, evmVersion }).then((result) => {
if (result.status) {
props.analysisModule.call('terminal', 'log', { type: 'info', value: `[Slither Analysis]: Analysis Completed!! ${result.count} warnings found.` })
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
acc[key].push(obj)
return acc
}, {})
location = props.analysisModule._deps.offsetToLineColumnConverter.offsetToLineColumn(
location,
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')
setWarningState(groupedCategory)
}
warningCount++
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])
}
}).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 {
if (categoryIndex.length) {
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 = () => {
if (autoRun) {
setAutoRun(false)
@ -305,7 +396,18 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
label="Autorun"
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 id="staticanalysismodules" className="list-group list-group-flush">
@ -318,7 +420,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
}
</div>
<div className="mt-2 p-2 d-flex border-top flex-column">
<span>last results for:</span>
<span>Last results for:</span>
<span
className="text-break break-word word-break font-weight-bold"
id="staticAnalysisCurrentFile"
@ -326,6 +428,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
{state.file}
</span>
</div>
<br/>
{Object.entries(warningState).length > 0 &&
<div id='staticanalysisresult' >
<div className="mb-4">
@ -333,9 +436,9 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
(Object.entries(warningState).map((element, index) => (
<div key={index}>
<span className="text-dark h6">{element[0]}</span>
{element[1].map((x, i) => (
x.hasWarning ? (
<div id={`staticAnalysisModule${element[1].warningModuleName}`} key={i}>
{element[1]['map']((x, i) => ( // eslint-disable-line dot-notation
x.hasWarning ? ( // eslint-disable-next-line dot-notation
<div id={`staticAnalysisModule${element[1]['warningModuleName']}`} key={i}>
<ErrorRenderer message={x.msg} opt={x.options} warningErrors={ x.warningErrors} editor={props.analysisModule}/>
</div>

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

@ -25,6 +25,7 @@ async function warnLatestVersion () {
const services = {
git: (readOnly: boolean) => new servicesList.GitClient(readOnly),
hardhat: (readOnly: boolean) => new servicesList.HardhatClient(readOnly),
slither: (readOnly: boolean) => new servicesList.SlitherClient(readOnly),
folder: (readOnly: boolean) => new servicesList.Sharedfolder(readOnly)
}
@ -32,11 +33,12 @@ const services = {
const ports = {
git: 65521,
hardhat: 65522,
slither: 65523,
folder: 65520
}
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))
socket.start(callback)
killCallBack.push(socket.close.bind(socket))
@ -94,6 +96,10 @@ function errorHandler (error: any, service: string) {
sharedFolderClient.setupNotifications(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
const hardhatConfigFilePath = absolutePath('./', program.sharedFolder) + '/hardhat.config.js'
const isHardhatProject = fs.existsSync(hardhatConfigFilePath)

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

@ -1,3 +1,4 @@
export { RemixdClient as Sharedfolder } from './services/remixdClient'
export { GitClient } from './services/gitClient'
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 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;
export type Service = typeof ServiceList[ServiceListKeys]

@ -19,7 +19,8 @@ export default class WebSocket {
const listeners = {
65520: 'remixd',
65521: 'git',
65522: 'hardhat'
65522: 'hardhat',
65523: 'slither'
}
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",
"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": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",

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

Loading…
Cancel
Save