diff --git a/apps/remix-ide-e2e/src/helpers/init.ts b/apps/remix-ide-e2e/src/helpers/init.ts index 484645b391..ba6266be54 100644 --- a/apps/remix-ide-e2e/src/helpers/init.ts +++ b/apps/remix-ide-e2e/src/helpers/init.ts @@ -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, () => { diff --git a/apps/remix-ide-e2e/src/tests/pluginManager.spec.ts b/apps/remix-ide-e2e/src/tests/pluginManager.spec.ts index f87bf1269e..8ea40977ca 100644 --- a/apps/remix-ide-e2e/src/tests/pluginManager.spec.ts +++ b/apps/remix-ide-e2e/src/tests/pluginManager.spec.ts @@ -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) { diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts index 9223ae1728..2c3a43ebc4 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts @@ -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) diff --git a/apps/remix-ide-e2e/src/tests/verticalIconsPanel.spec.ts b/apps/remix-ide-e2e/src/tests/verticalIconsPanel.spec.ts index 29eaf4a95b..d0593433c9 100644 --- a/apps/remix-ide-e2e/src/tests/verticalIconsPanel.spec.ts +++ b/apps/remix-ide-e2e/src/tests/verticalIconsPanel.spec.ts @@ -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"]') diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index d88b3272cf..aa17788170 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -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; @@ -126,11 +131,11 @@ class App { self._components = {} self._view = {} self._view.splashScreen = yo` -
- ${basicLogo()} -
- REMIX IDE -
+
+ ${basicLogo()} +
+ REMIX IDE +
` document.body.appendChild(self._view.splashScreen) @@ -443,7 +448,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()) { @@ -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 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() + } } diff --git a/apps/remix-ide/src/app/components/vertical-icons.js b/apps/remix-ide/src/app/components/vertical-icons.js index 7a31682ed1..54b77f9720 100644 --- a/apps/remix-ide/src/app/components/vertical-icons.js +++ b/apps/remix-ide/src/app/components/vertical-icons.js @@ -69,12 +69,14 @@ export class VerticalIcons extends Plugin { title = title.replace(/^\w/, c => c.toUpperCase()) this.icons[name] = yo`
+ data-id="verticalIconsKind${name}" + id="verticalIconsKind${name}" + > ${name}
` this.iconKind[kind || 'none'].appendChild(this.icons[name]) @@ -249,13 +251,14 @@ export class VerticalIcons extends Plugin { render () { const home = yo`
${basicLogo()}
@@ -270,16 +273,18 @@ export class VerticalIcons extends Plugin { this.iconKind.settings = yo`
` this.view = yo` -
- ${home} - ${this.iconKind.fileexplorer} - ${this.iconKind.compiler} - ${this.iconKind.udapp} - ${this.iconKind.testing} - ${this.iconKind.analysis} - ${this.iconKind.debugging} - ${this.iconKind.none} - ${this.iconKind.settings} +
+
+ ${home} + ${this.iconKind.fileexplorer} + ${this.iconKind.compiler} + ${this.iconKind.udapp} + ${this.iconKind.testing} + ${this.iconKind.analysis} + ${this.iconKind.debugging} + ${this.iconKind.none} + ${this.iconKind.settings} +
` 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; diff --git a/apps/remix-ide/src/app/editor/sourceHighlighter.js b/apps/remix-ide/src/app/editor/sourceHighlighter.js index e25e2c948f..22f705001e 100644 --- a/apps/remix-ide/src/app/editor/sourceHighlighter.js +++ b/apps/remix-ide/src/app/editor/sourceHighlighter.js @@ -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() diff --git a/apps/remix-ide/src/app/files/remixd-handle.js b/apps/remix-ide/src/app/files/remixd-handle.js index abea437b64..6f06d8773b 100644 --- a/apps/remix-ide/src/app/files/remixd-handle.js +++ b/apps/remix-ide/src/app/files/remixd-handle.js @@ -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()) { diff --git a/apps/remix-ide/src/app/files/slither-handle.js b/apps/remix-ide/src/app/files/slither-handle.js new file mode 100644 index 0000000000..e8ff1e66c7 --- /dev/null +++ b/apps/remix-ide/src/app/files/slither-handle.js @@ -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) + } +} diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index 0d1523fa08..4ed0e6c8c9 100644 --- a/apps/remix-ide/src/app/panels/file-panel.js +++ b/apps/remix-ide/src/app/panels/file-panel.js @@ -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 = {} diff --git a/apps/remix-ide/src/app/tabs/compileTab/compileTab.js b/apps/remix-ide/src/app/tabs/compileTab/compileTab.js index b357f8a8aa..a9c17192cc 100644 --- a/apps/remix-ide/src/app/tabs/compileTab/compileTab.js +++ b/apps/remix-ide/src/app/tabs/compileTab/compileTab.js @@ -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 diff --git a/apps/remix-ide/src/app/tabs/test-tab.js b/apps/remix-ide/src/app/tabs/test-tab.js index b5c2c2fafd..83665e8ed5 100644 --- a/apps/remix-ide/src/app/tabs/test-tab.js +++ b/apps/remix-ide/src/app/tabs/test-tab.js @@ -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.data.allTests.push(file) - this.data.selectedTests.push(file) }) this.on('filePanel', 'setWorkspace', async () => { diff --git a/apps/remix-ide/src/assets/js/intro.min.js b/apps/remix-ide/src/assets/js/intro.min.js new file mode 100644 index 0000000000..b3c2bfa36f --- /dev/null +++ b/apps/remix-ide/src/assets/js/intro.min.js @@ -0,0 +1,10 @@ +/*! + * Intro.js v4.1.0 + * https://introjs.com + * + * Copyright (C) 2012-2021 Afshin Mehrabani (@afshinmeh). + * https://raw.githubusercontent.com/usablica/intro.js/master/license.md + * + * Date: Fri, 18 Jun 2021 10:48:16 GMT + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).introJs=e()}(this,(function(){"use strict";function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=function(){var t={};return function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"introjs-stamp";return t[n]=t[n]||0,void 0===e[n]&&(e[n]=t[n]++),e[n]}}();function n(t,e,n){if(t)for(var i=0,o=t.length;i0?ht:ut)(t)},pt=Math.min,dt=function(t){return t>0?pt(ft(t),9007199254740991):0},gt=Math.max,vt=Math.min,mt=function(t,e){var n=ft(t);return n<0?gt(n+e,0):vt(n,e)},bt=function(t){return function(e,n,i){var o,r=y(e),l=dt(r.length),a=mt(i,l);if(t&&n!=n){for(;l>a;)if((o=r[a++])!=o)return!0}else for(;l>a;a++)if((t||a in r)&&r[a]===n)return t||a||0;return!t&&-1}},yt={includes:bt(!0),indexOf:bt(!1)},wt=yt.indexOf,_t=function(t,e){var n,i=y(t),o=0,r=[];for(n in i)!j(J,n)&&j(i,n)&&r.push(n);for(;e.length>o;)j(i,n=e[o++])&&(~wt(r,n)||r.push(n));return r},St=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],xt=St.concat("length","prototype"),jt={f:Object.getOwnPropertyNames||function(t){return _t(t,xt)}},Ct={f:Object.getOwnPropertySymbols},Et=ct("Reflect","ownKeys")||function(t){var e=jt.f(I(t)),n=Ct.f;return n?e.concat(n(t)):e},At=function(t,e){for(var n=Et(e),i=O.f,o=N.f,r=0;r0&&(!r.multiline||r.multiline&&"\n"!==t[r.lastIndex-1])&&(s="(?: "+s+")",u=" "+u,c++),n=new RegExp("^(?:"+s+")",a)),Vt&&(n=new RegExp("^"+s+"$(?!\\s)",a)),Wt&&(e=r.lastIndex),i=Ut.call(l?n:r,u),l?i?(i.input=i.input.slice(c),i[0]=i[0].slice(c),i.index=r.lastIndex,r.lastIndex+=i[0].length):r.lastIndex=0:Wt&&i&&(r.lastIndex=r.global?i.index+i[0].length:e),Vt&&i&&i.length>1&&Ft.call(i[0],n,(function(){for(o=1;o=74)&&(Yt=Jt.match(/Chrome\/(\d+)/))&&(Xt=Yt[1]);var ee=Xt&&+Xt,ne=!!Object.getOwnPropertySymbols&&!s((function(){var t=Symbol();return!String(t)||!(Object(t)instanceof Symbol)||!Symbol.sham&&ee&&ee<41})),ie=ne&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,oe=W("wks"),re=a.Symbol,le=ie?re:re&&re.withoutSetter||K,ae=function(t){return j(oe,t)&&(ne||"string"==typeof oe[t])||(ne&&j(re,t)?oe[t]=re[t]:oe[t]=le("Symbol."+t)),oe[t]},se=ae("species"),ce=RegExp.prototype,ue=!s((function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$")})),he="$0"==="a".replace(/./,"$0"),fe=ae("replace"),pe=!!/./[fe]&&""===/./[fe]("a","$0"),de=!s((function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var n="ab".split(t);return 2!==n.length||"a"!==n[0]||"b"!==n[1]})),ge=function(t,e,n,i){var o=ae(t),r=!s((function(){var e={};return e[o]=function(){return 7},7!=""[t](e)})),l=r&&!s((function(){var e=!1,n=/a/;return"split"===t&&((n={}).constructor={},n.constructor[se]=function(){return n},n.flags="",n[o]=/./[o]),n.exec=function(){return e=!0,null},n[o](""),!e}));if(!r||!l||"replace"===t&&(!ue||!he||pe)||"split"===t&&!de){var a=/./[o],c=n(o,""[t],(function(t,e,n,i,o){var l=e.exec;return l===Kt||l===ce.exec?r&&!o?{done:!0,value:a.call(e,n,i)}:{done:!0,value:t.call(n,e,i)}:{done:!1}}),{REPLACE_KEEPS_$0:he,REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE:pe}),u=c[0],h=c[1];lt(String.prototype,t,u),lt(ce,o,2==e?function(t,e){return h.call(t,this,e)}:function(t){return h.call(t,this)})}i&&L(ce[o],"sham",!0)},ve=function(t){return function(e,n){var i,o,r=String(b(e)),l=ft(n),a=r.length;return l<0||l>=a?t?"":void 0:(i=r.charCodeAt(l))<55296||i>56319||l+1===a||(o=r.charCodeAt(l+1))<56320||o>57343?t?r.charAt(l):i:t?r.slice(l,l+2):o-56320+(i-55296<<10)+65536}},me={codeAt:ve(!1),charAt:ve(!0)}.charAt,be=function(t,e,n){return e+(n?me(t,e).length:1)},ye=function(t,e){var n=t.exec;if("function"==typeof n){var i=n.call(t,e);if("object"!=typeof i)throw TypeError("RegExp exec method returned something other than an Object or null");return i}if("RegExp"!==g(t))throw TypeError("RegExp#exec called on incompatible receiver");return Kt.call(t,e)};ge("match",1,(function(t,e,n){return[function(e){var n=b(this),i=null==e?void 0:e[t];return void 0!==i?i.call(e,n):new RegExp(e)[t](String(n))},function(t){var i=n(e,t,this);if(i.done)return i.value;var o=I(t),r=String(this);if(!o.global)return ye(o,r);var l=o.unicode;o.lastIndex=0;for(var a,s=[],c=0;null!==(a=ye(o,r));){var u=String(a[0]);s[c]=u,""===u&&(o.lastIndex=be(r,dt(o.lastIndex),l)),c++}return 0===c?null:s}]}));var we=Array.isArray||function(t){return"Array"==g(t)},_e=function(t,e,n){var i=_(e);i in t?O.f(t,i,p(0,n)):t[i]=n},Se=ae("species"),xe=function(t,e){var n;return we(t)&&("function"!=typeof(n=t.constructor)||n!==Array&&!we(n.prototype)?w(n)&&null===(n=n[Se])&&(n=void 0):n=void 0),new(void 0===n?Array:n)(0===e?0:e)},je=ae("species"),Ce=function(t){return ee>=51||!s((function(){var e=[];return(e.constructor={})[je]=function(){return{foo:1}},1!==e[t](Boolean).foo}))},Ee=ae("isConcatSpreadable"),Ae=9007199254740991,ke="Maximum allowed index exceeded",Te=ee>=51||!s((function(){var t=[];return t[Ee]=!1,t.concat()[0]!==t})),Ne=Ce("concat"),Ie=function(t){if(!w(t))return!1;var e=t[Ee];return void 0!==e?!!e:we(t)};qt({target:"Array",proto:!0,forced:!Te||!Ne},{concat:function(t){var e,n,i,o,r,l=S(this),a=xe(l,0),s=0;for(e=-1,i=arguments.length;eAe)throw TypeError(ke);for(n=0;n=Ae)throw TypeError(ke);_e(a,s++,r)}return a.length=s,a}});var Pe={};Pe[ae("toStringTag")]="z";var Oe="[object z]"===String(Pe),Le=ae("toStringTag"),Re="Arguments"==g(function(){return arguments}()),qe=Oe?g:function(t){var e,n,i;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=function(t,e){try{return t[e]}catch(t){}}(e=Object(t),Le))?n:Re?g(e):"Object"==(i=g(e))&&"function"==typeof e.callee?"Arguments":i},Be=Oe?{}.toString:function(){return"[object "+qe(this)+"]"};Oe||lt(Object.prototype,"toString",Be,{unsafe:!0});var Me="toString",He=RegExp.prototype,De=He.toString,$e=s((function(){return"/a/b"!=De.call({source:"a",flags:"b"})})),Ue=De.name!=Me;($e||Ue)&<(RegExp.prototype,Me,(function(){var t=I(this),e=String(t.source),n=t.flags;return"/"+e+"/"+String(void 0===n&&t instanceof RegExp&&!("flags"in He)?Bt.call(t):n)}),{unsafe:!0});var Fe=ae("match"),ze=function(t){var e;return w(t)&&(void 0!==(e=t[Fe])?!!e:"RegExp"==g(t))},We=function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function");return t},Ge=ae("species"),Ve=$t.UNSUPPORTED_Y,Ke=[].push,Ye=Math.min,Xe=4294967295;function Je(t,e){if(t instanceof SVGElement){var i=t.getAttribute("class")||"";i.match(e)||t.setAttribute("class","".concat(i," ").concat(e))}else{if(void 0!==t.classList)n(e.split(" "),(function(e){t.classList.add(e)}));else t.className.match(e)||(t.className+=" ".concat(e))}}function Qe(t,e){var n="";return t.currentStyle?n=t.currentStyle[e]:document.defaultView&&document.defaultView.getComputedStyle&&(n=document.defaultView.getComputedStyle(t,null).getPropertyValue(e)),n&&n.toLowerCase?n.toLowerCase():n}function Ze(t){var e=t.element;if(this._options.scrollToElement){var n=function(t){var e=window.getComputedStyle(t),n="absolute"===e.position,i=/(auto|scroll)/;if("fixed"===e.position)return document.body;for(var o=t;o=o.parentElement;)if(e=window.getComputedStyle(o),(!n||"static"!==e.position)&&i.test(e.overflow+e.overflowY+e.overflowX))return o;return document.body}(e);n!==document.body&&(n.scrollTop=e.offsetTop-n.offsetTop)}}function tn(){if(void 0!==window.innerWidth)return{width:window.innerWidth,height:window.innerHeight};var t=document.documentElement;return{width:t.clientWidth,height:t.clientHeight}}function en(t,e,n){var i,o=e.element;if("off"!==t&&(this._options.scrollToElement&&(i="tooltip"===t?n.getBoundingClientRect():o.getBoundingClientRect(),!function(t){var e=t.getBoundingClientRect();return e.top>=0&&e.left>=0&&e.bottom+80<=window.innerHeight&&e.right<=window.innerWidth}(o)))){var r=tn().height;i.bottom-(i.bottom-i.top)<0||o.clientHeight>r?window.scrollBy(0,i.top-(r/2-i.height/2)-this._options.scrollPadding):window.scrollBy(0,i.top-(r/2-i.height/2)+this._options.scrollPadding)}}function nn(t){t.setAttribute("role","button"),t.tabIndex=0}ge("split",2,(function(t,e,n){var i;return i="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(t,n){var i=String(b(this)),o=void 0===n?Xe:n>>>0;if(0===o)return[];if(void 0===t)return[i];if(!ze(t))return e.call(i,t,o);for(var r,l,a,s=[],c=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),u=0,h=new RegExp(t.source,c+"g");(r=Kt.call(h,i))&&!((l=h.lastIndex)>u&&(s.push(i.slice(u,r.index)),r.length>1&&r.index=o));)h.lastIndex===r.index&&h.lastIndex++;return u===i.length?!a&&h.test("")||s.push(""):s.push(i.slice(u)),s.length>o?s.slice(0,o):s}:"0".split(void 0,0).length?function(t,n){return void 0===t&&0===n?[]:e.call(this,t,n)}:e,[function(e,n){var o=b(this),r=null==e?void 0:e[t];return void 0!==r?r.call(e,o,n):i.call(String(o),e,n)},function(t,o){var r=n(i,t,this,o,i!==e);if(r.done)return r.value;var l=I(t),a=String(this),s=function(t,e){var n,i=I(t).constructor;return void 0===i||null==(n=I(i)[Ge])?e:We(n)}(l,RegExp),c=l.unicode,u=(l.ignoreCase?"i":"")+(l.multiline?"m":"")+(l.unicode?"u":"")+(Ve?"g":"y"),h=new s(Ve?"^(?:"+l.source+")":l,u),f=void 0===o?Xe:o>>>0;if(0===f)return[];if(0===a.length)return null===ye(h,a)?[a]:[];for(var p=0,d=0,g=[];do;)for(var a,s=m(arguments[o++]),u=r?on(s).concat(r(s)):on(s),h=u.length,p=0;h>p;)a=u[p++],c&&!l.call(s,a)||(n[a]=s[a]);return n}:rn;function sn(t,e){var n=document.body,i=document.documentElement,o=window.pageYOffset||i.scrollTop||n.scrollTop,r=window.pageXOffset||i.scrollLeft||n.scrollLeft;e=e||n;var l=t.getBoundingClientRect(),a=e.getBoundingClientRect(),s=Qe(e,"position"),c={width:l.width,height:l.height};return"body"!==e.tagName.toLowerCase()&&"relative"===s||"sticky"===s?Object.assign(c,{top:l.top-a.top,left:l.left-a.left}):Object.assign(c,{top:l.top+o,left:l.left+r})}function cn(t){var e=t.parentNode;return!(!e||"HTML"===e.nodeName)&&("fixed"===Qe(t,"position")||cn(e))}qt({target:"Object",stat:!0,forced:Object.assign!==an},{assign:an});var un=Math.floor,hn="".replace,fn=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,pn=/\$([$&'`]|\d{1,2})/g,dn=function(t,e,n,i,o,r){var l=n+t.length,a=i.length,s=pn;return void 0!==o&&(o=S(o),s=fn),hn.call(r,s,(function(r,s){var c;switch(s.charAt(0)){case"$":return"$";case"&":return t;case"`":return e.slice(0,n);case"'":return e.slice(l);case"<":c=o[s.slice(1,-1)];break;default:var u=+s;if(0===u)return r;if(u>a){var h=un(u/10);return 0===h?r:h<=a?void 0===i[h-1]?s.charAt(1):i[h-1]+s.charAt(1):r}c=i[u-1]}return void 0===c?"":c}))},gn=Math.max,vn=Math.min;function mn(t,e){if(t instanceof SVGElement){var n=t.getAttribute("class")||"";t.setAttribute("class",n.replace(e,"").replace(/^\s+|\s+$/g,""))}else t.className=t.className.replace(e,"").replace(/^\s+|\s+$/g,"")}function bn(t,e){var n="";if(t.style.cssText&&(n+=t.style.cssText),"string"==typeof e)n+=e;else for(var i in e)n+="".concat(i,":").concat(e[i],";");t.style.cssText=n}function yn(t){if(t){if(!this._introItems[this._currentStep])return;var e=this._introItems[this._currentStep],n=sn(e.element,this._targetElement),i=this._options.helperElementPadding;cn(e.element)?Je(t,"introjs-fixedTooltip"):mn(t,"introjs-fixedTooltip"),"floating"===e.position&&(i=0),bn(t,{width:"".concat(n.width+i,"px"),height:"".concat(n.height+i,"px"),top:"".concat(n.top-i/2,"px"),left:"".concat(n.left-i/2,"px")})}}ge("replace",2,(function(t,e,n,i){var o=i.REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE,r=i.REPLACE_KEEPS_$0,l=o?"$":"$0";return[function(n,i){var o=b(this),r=null==n?void 0:n[t];return void 0!==r?r.call(n,o,i):e.call(String(o),n,i)},function(t,i){if(!o&&r||"string"==typeof i&&-1===i.indexOf(l)){var a=n(e,t,this,i);if(a.done)return a.value}var s=I(t),c=String(this),u="function"==typeof i;u||(i=String(i));var h=s.global;if(h){var f=s.unicode;s.lastIndex=0}for(var p=[];;){var d=ye(s,c);if(null===d)break;if(p.push(d),!h)break;""===String(d[0])&&(s.lastIndex=be(c,dt(s.lastIndex),f))}for(var g,v="",m=0,b=0;b=m&&(v+=c.slice(m,w)+C,m=w+y.length)}return v+c.slice(m)}]}));var wn,_n=c?Object.defineProperties:function(t,e){I(t);for(var n,i=on(e),o=i.length,r=0;o>r;)O.f(t,n=i[r++],e[n]);return t},Sn=ct("document","documentElement"),xn=X("IE_PROTO"),jn=function(){},Cn=function(t){return" @@ -56,6 +57,7 @@ } + - + + + + diff --git a/apps/remix-ide/src/remixEngine.js b/apps/remix-ide/src/remixEngine.js index abf20340f6..2836d974b2 100644 --- a/apps/remix-ide/src/remixEngine.js +++ b/apps/remix-ide/src/remixEngine.js @@ -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 } } diff --git a/apps/remix-ide/src/walkthroughService.js b/apps/remix-ide/src/walkthroughService.js new file mode 100644 index 0000000000..2969d1190f --- /dev/null +++ b/apps/remix-ide/src/walkthroughService.js @@ -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 () { + } +} diff --git a/libs/remix-solidity/src/index.ts b/libs/remix-solidity/src/index.ts index db63062910..7de163dc97 100644 --- a/libs/remix-solidity/src/index.ts +++ b/libs/remix-solidity/src/index.ts @@ -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' diff --git a/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx index 5535a05971..95913e533b 100644 --- a/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx +++ b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx @@ -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 ( -
+
{ 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 (` @@ -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] = [] - } - // Add object to list for given key's value - acc[key].push(obj) - return acc - }, {}) - } + // 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 - const groupedCategory = groupBy(resultArray, 'warningModuleName') - setWarningState(groupedCategory) + 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 + } + 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] + } + } + 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' }) + }) + 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]) + } }) - if (categoryIndex.length > 0) { - 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={() => {}} /> -
+
+ {}} + visibility = {showSlither} + />
@@ -318,7 +420,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { }
- last results for: + Last results for: { {state.file}
+
{Object.entries(warningState).length > 0 &&
@@ -333,9 +436,9 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { (Object.entries(warningState).map((element, index) => (
{element[0]} - {element[1].map((x, i) => ( - x.hasWarning ? ( -
+ {element[1]['map']((x, i) => ( // eslint-disable-line dot-notation + x.hasWarning ? ( // eslint-disable-next-line dot-notation +
diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.css b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.css index f190b84fac..534da8bca6 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.css +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.css @@ -11,6 +11,7 @@ position : relative; width : 100%; padding-left : 6px; + padding-right : 6px; padding-top : 6px; } .remixui_fileExplorerTree { diff --git a/libs/remixd/src/bin/remixd.ts b/libs/remixd/src/bin/remixd.ts index 2085735b8c..623fa5ef97 100644 --- a/libs/remixd/src/bin/remixd.ts +++ b/libs/remixd/src/bin/remixd.ts @@ -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 startService (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder, error?:Error) => void) { +function startService (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) diff --git a/libs/remixd/src/index.ts b/libs/remixd/src/index.ts index 849f35b6fa..8b35cea6dd 100644 --- a/libs/remixd/src/index.ts +++ b/libs/remixd/src/index.ts @@ -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 } } diff --git a/libs/remixd/src/serviceList.ts b/libs/remixd/src/serviceList.ts index 19d613b7c2..ec8c1f374d 100644 --- a/libs/remixd/src/serviceList.ts +++ b/libs/remixd/src/serviceList.ts @@ -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' diff --git a/libs/remixd/src/services/slitherClient.ts b/libs/remixd/src/services/slitherClient.ts new file mode 100644 index 0000000000..e2d9a281bb --- /dev/null +++ b/libs/remixd/src/services/slitherClient.ts @@ -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 + 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[]): 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) { + 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.')) + }) + }) + } +} diff --git a/libs/remixd/src/types/index.ts b/libs/remixd/src/types/index.ts index cde06fcf07..66818dff48 100644 --- a/libs/remixd/src/types/index.ts +++ b/libs/remixd/src/types/index.ts @@ -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] diff --git a/libs/remixd/src/websocket.ts b/libs/remixd/src/websocket.ts index 370dc48abb..324fa3332b 100644 --- a/libs/remixd/src/websocket.ts +++ b/libs/remixd/src/websocket.ts @@ -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) => { diff --git a/package-lock.json b/package-lock.json index 7f91b1a64c..83fc0b9385 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2a5b0660ec..bb82c62830 100644 --- a/package.json +++ b/package.json @@ -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",