diff --git a/apps/remix-ide-e2e/src/tests/debugger.test.ts b/apps/remix-ide-e2e/src/tests/debugger.test.ts index 56a1174521..9010b79b58 100644 --- a/apps/remix-ide-e2e/src/tests/debugger.test.ts +++ b/apps/remix-ide-e2e/src/tests/debugger.test.ts @@ -477,7 +477,7 @@ const localVariable_step717_ABIEncoder = { // eslint-disable-line const jsGetTrace = `(async () => { try { - const result = await remix.call('debugger', 'getTrace', '0x16be5c31014a7e1552d136f7ed7bc7788f3bb9e45e31b059df253173f2df31e7') + const result = await remix.call('debugger', 'getTrace', '0x65f0813753462414f9a91f0aabea946188327995f54b893b63a8d7ff186cfca3') console.log('result ', result) } catch (e) { console.log(e.message) @@ -486,7 +486,7 @@ const jsGetTrace = `(async () => { const jsDebug = `(async () => { try { - const result = await remix.call('debugger', 'debug', '0x16be5c31014a7e1552d136f7ed7bc7788f3bb9e45e31b059df253173f2df31e7') + const result = await remix.call('debugger', 'debug', '0x65f0813753462414f9a91f0aabea946188327995f54b893b63a8d7ff186cfca3') console.log('result ', result) } catch (e) { console.log(e.message) diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts index ed5cf3570c..30a2913891 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts @@ -132,7 +132,7 @@ module.exports = { .click('*[data-id="testTabCheckAllTests"]') .clickElementAtPosition('.singleTestLabel', 1) .scrollAndClick('*[data-id="testTabRunTestsTabRunAction"]') - .waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'contract deployment failed after trying twice', 120000) + .waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'revert Deploy Failed', 120000) }, 'Should fail when parameters are passed to method in test contract #group3': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/staticAnalysis.test.ts b/apps/remix-ide-e2e/src/tests/staticAnalysis.test.ts index 504148b266..2a2b39b523 100644 --- a/apps/remix-ide-e2e/src/tests/staticAnalysis.test.ts +++ b/apps/remix-ide-e2e/src/tests/staticAnalysis.test.ts @@ -30,6 +30,27 @@ module.exports = { }, 'Static Analysis': function (browser: NightwatchBrowser) { runTests(browser) + }, + 'run analysis and filter results': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('filePanel') + .click('*[data-id="treeViewLitreeViewItemcontracts"]') + .click('*[data-id="treeViewLitreeViewItemcontracts/2_Owner.sol"]') + .clickLaunchIcon('solidity') + .pause(10000) + .clickLaunchIcon('solidityStaticAnalysis') + .waitForElementPresent('#staticanalysisresult .warning', 5000) + .assert.containsText('#verticalIconsKindsolidityStaticAnalysis .remixui_status', '1') // Check warning count + .verify.elementPresent('input[name="showLibWarnings"]') + .verify.elementNotPresent('input[name="showLibWarnings"]:checked') + .verify.elementPresent('label[id="headingshowLibWarnings"]') + .click('label[id="headingshowLibWarnings"]') + .pause(1000) + .assert.containsText('#verticalIconsKindsolidityStaticAnalysis .remixui_status', '382') + .click('label[id="headingshowLibWarnings"]') + .pause(1000) + .assert.containsText('#verticalIconsKindsolidityStaticAnalysis .remixui_status', '1') + .end() } } @@ -47,14 +68,12 @@ function runTests (browser: NightwatchBrowser) { 'TooMuchGas.() : Variables have very similar names "test" and "test1".', 'TooMuchGas.() : Variables have very similar names "test" and "test1".'], '#staticanalysisresult .warning', - browser, function () { - browser.end() - } + browser ) }) } -function listSelectorContains (textsToFind: string[], selector: string, browser: NightwatchBrowser, callback: VoidFunction) { +function listSelectorContains (textsToFind: string[], selector: string, browser: NightwatchBrowser) { browser.execute(function (selector) { const items = document.querySelectorAll(selector) const ret = [] @@ -68,6 +87,5 @@ function listSelectorContains (textsToFind: string[], selector: string, browser: console.log('testing `' + result.value[k] + '` against `' + textsToFind[k] + '`') browser.assert.equal(result.value[k].indexOf(textsToFind[k]) !== -1, true) } - callback() }) } diff --git a/apps/remix-ide-e2e/src/tests/terminal.test.ts b/apps/remix-ide-e2e/src/tests/terminal.test.ts index 0f90f84199..4e41970b81 100644 --- a/apps/remix-ide-e2e/src/tests/terminal.test.ts +++ b/apps/remix-ide-e2e/src/tests/terminal.test.ts @@ -190,7 +190,17 @@ module.exports = { .waitForElementVisible('*[data-id="terminalCli"]') .click('*[data-id="terminalCli"]') .sendKeys('*[data-id="terminalCliInput"]', 'remix.') - .assert.visible('*[data-id="autoCompletePopUpAutoCompleteItem"]').end() + .assert.visible('*[data-id="autoCompletePopUpAutoCompleteItem"]') + }, + + 'Should run a script right after compilation #group6': function (browser: NightwatchBrowser) { + browser + .addFile('contracts/storage.sol', { content: scriptAutoExec.contract } ) + .addFile('scripts/deploy_storage.js', { content: scriptAutoExec.script } ) + .openFile('contracts/storage.sol') + .sendKeys('body', [browser.Keys.CONTROL, browser.Keys.SHIFT, 's']) + .pause(5000) + .journalLastChildIncludes('147') } } @@ -484,3 +494,115 @@ contract OwnerTest { return owner; } }` + +const scriptAutoExec = { + contract: `// SPDX-License-Identifier: GPL-3.0 + + pragma solidity >=0.7.0 <0.9.0; + + library lib { + function test () public view returns (uint) { + + return 147; + } + } + + /** + * @title Storage + * @dev Store & retrieve value inr a variable + * @custom:dev-run-script ./scripts/deploy_storage.js + */ + contract Storage { + + uint256 number; + + /** + * @dev Store valrue in variable + * @param num value to store + */ + function store(uint256 num) public { + number = num; + } + + /** + * @dev Return value + * @return value of 'number' + */ + function retrieve() public view returns (uint256){ + return number; + } + + function getFromLib() public view returns (uint) { + return lib.test(); + } + } + `, + script: ` + // Right click on the script name and hit "Run" to execute + const { expect } = require("chai"); + const { ethers } = require("hardhat"); + + (async () => { + try { + // function getContractFactoryFromArtifact(artifact: Artifact, signer?: ethers.Signer): Promise; + + // function getContractFactoryFromArtifact(artifact: Artifact, factoryOptions: FactoryOptions): Promise; + + const metadataLib = JSON.parse(await remix.call('fileManager', 'readFile', 'contracts/artifacts/lib.json')) + console.log('deploying lib:') + + const artifactLib = { + contractName: 'Lib', + sourceName: 'contracts/1_Storage.sol', + abi: metadataLib.abi, + bytecode: '0x' + metadataLib.data.bytecode.object, + deployedBytecode: '0x' + metadataLib.data.deployedBytecode.object, + linkReferences: metadataLib.data.bytecode.linkReferences, + deployedLinkReferences: metadataLib.data.deployedBytecode.linkReferences, + } + const optionsLib = {} + + const factoryLib = await ethers.getContractFactoryFromArtifact(artifactLib, optionsLib) + + const lib = await factoryLib.deploy(); + + await lib.deployed() + + console.log('lib deployed', lib.address) + + const metadata = JSON.parse(await remix.call('fileManager', 'readFile', 'contracts/artifacts/Storage.json')) + const artifact = { + contractName: 'Storage', + sourceName: 'contracts/1_Storage.sol', + abi: metadata.abi, + bytecode: '0x' + metadata.data.bytecode.object, + deployedBytecode: '0x' + metadata.data.deployedBytecode.object, + linkReferences: metadata.data.bytecode.linkReferences, + deployedLinkReferences: metadata.data.deployedBytecode.linkReferences, + } + const options = { + libraries: { + 'lib': lib.address + } + } + + const factory = await ethers.getContractFactoryFromArtifact(artifact, options) + + const storage = await factory.deploy(); + + await storage.deployed() + + const storeValue = await storage.store(333); + + // wait until the transaction is mined + await storeValue.wait(); + + console.log((await storage.getFromLib()).toString()) + // expect((await storage.getFromLib()).toString()).to.equal('34'); + + } catch (e) { + console.error(e.message) + } + })() + ` +} \ No newline at end of file diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 35c31e862a..a2ccbfd16b 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -5,6 +5,7 @@ import { RemixAppManager } from './remixAppManager' import { ThemeModule } from './app/tabs/theme-module' import { NetworkModule } from './app/tabs/network-module' import { Web3ProviderModule } from './app/tabs/web3-provider' +import { IntelligentScriptExecutor } from './app/tabs/intelligent-script-executor' import { SidePanel } from './app/components/side-panel' import { HiddenPanel } from './app/components/hidden-panel' import { VerticalIcons } from './app/components/vertical-icons' @@ -179,7 +180,8 @@ class AppComponent { api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter' }) - + // ----------------- run script after each compilation results ----------- + const intelligentScriptExecutor = new IntelligentScriptExecutor() // -------------------Terminal---------------------------------------- makeUdapp(blockchain, compilersArtefacts, domEl => terminal.logHtml(domEl)) const terminal = new Terminal( @@ -222,6 +224,7 @@ class AppComponent { contextualListener, terminal, web3Provider, + intelligentScriptExecutor, fetchAndCompile, dGitProvider, storagePlugin, @@ -343,7 +346,7 @@ class AppComponent { await this.appManager.activatePlugin(['settings', 'config']) await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) await this.appManager.activatePlugin(['settings']) - await this.appManager.activatePlugin(['walkthrough','storage']) + await this.appManager.activatePlugin(['walkthrough','storage', 'intelligentScriptExecutor']) this.appManager.on( 'filePanel', diff --git a/apps/remix-ide/src/app/tabs/intelligent-script-executor.ts b/apps/remix-ide/src/app/tabs/intelligent-script-executor.ts new file mode 100644 index 0000000000..66368b5f56 --- /dev/null +++ b/apps/remix-ide/src/app/tabs/intelligent-script-executor.ts @@ -0,0 +1,62 @@ +import { Plugin } from '@remixproject/engine' +import * as packageJson from '../../../../../package.json' + +export const profile = { + name: 'intelligentScriptExecutor', + displayName: 'Intelligent Script Executor', + description: 'after each compilation, run the script defined in Natspec.', + methods: [], + version: packageJson.version, + kind: 'none' +} + +type listener = (event: KeyboardEvent) => void + +export class IntelligentScriptExecutor extends Plugin { + executionListener: listener + targetFileName: string + + constructor () { + super(profile) + this.executionListener = async (e) => { + // ctrl+e or command+e + const file = await this.call('fileManager', 'file') + if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.keyCode === 83 && file !== '') { + if (file.endsWith('.sol')) { + e.preventDefault() + this.targetFileName = file + await this.call('solidity', 'compile', file) + } else if (file.endsWith('.js') || file.endsWith('.ts')) { + e.preventDefault() + this.runScript(file, false) + } + } + } + } + + async runScript (fileName, clearAllInstances) { + await this.call('terminal', 'log', `running ${fileName} ...`) + const content = await this.call('fileManager', 'readFile', fileName) + if (clearAllInstances) { + await this.call('udapp', 'clearAllInstances') + } + await this.call('scriptRunner', 'execute', content) + } + + onActivation () { + window.document.addEventListener('keydown', this.executionListener) + + this.on('compilerMetadata', 'artefactsUpdated', async (fileName, contract) => { + if (this.targetFileName === contract.file && contract.object && contract.object.devdoc['custom:dev-run-script']) { + this.targetFileName = null + const file = contract.object.devdoc['custom:dev-run-script'] + if (file) this.runScript(file, true) + } + }) + } + + onDeactivation () { + window.document.removeEventListener('keydown', this.executionListener) + this.off('compilerMetadata', 'artefactsUpdated') + } +} diff --git a/apps/remix-ide/src/app/tabs/web3-provider.js b/apps/remix-ide/src/app/tabs/web3-provider.js index 4224106a39..42de424839 100644 --- a/apps/remix-ide/src/app/tabs/web3-provider.js +++ b/apps/remix-ide/src/app/tabs/web3-provider.js @@ -24,8 +24,15 @@ export class Web3ProviderModule extends Plugin { return new Promise((resolve, reject) => { const provider = this.blockchain.web3().currentProvider // see https://github.com/ethereum/web3.js/pull/1018/files#diff-d25786686c1053b786cc2626dc6e048675050593c0ebaafbf0814e1996f22022R129 - provider[provider.sendAsync ? 'sendAsync' : 'send'](payload, (error, message) => { + provider[provider.sendAsync ? 'sendAsync' : 'send'](payload, async (error, message) => { if (error) return reject(error) + if (payload.method === 'eth_sendTransaction') { + if (payload.params.length && !payload.params[0].to && message.result) { + const receipt = await this.call('blockchain', 'getTransactionReceipt', message.result) + const contractData = await this.call('compilerArtefacts', 'getContractDataFromAddress', receipt.contractAddress) + if (contractData) this.call('udapp', 'addInstance', receipt.contractAddress, contractData.contract.abi, contractData.name) + } + } resolve(message) }) }) diff --git a/apps/remix-ide/src/app/udapp/run-tab.js b/apps/remix-ide/src/app/udapp/run-tab.js index 05a015c4fc..313161b121 100644 --- a/apps/remix-ide/src/app/udapp/run-tab.js +++ b/apps/remix-ide/src/app/udapp/run-tab.js @@ -18,7 +18,7 @@ const profile = { version: packageJson.version, permission: true, events: ['newTransaction'], - methods: ['createVMAccount', 'sendTransaction', 'getAccounts', 'pendingTransactionsCount', 'getSettings', 'setEnvironmentMode'] + methods: ['createVMAccount', 'sendTransaction', 'getAccounts', 'pendingTransactionsCount', 'getSettings', 'setEnvironmentMode', 'clearAllInstances', 'addInstance'] } export class RunTab extends ViewPlugin { @@ -64,6 +64,14 @@ export class RunTab extends ViewPlugin { } } + clearAllInstances () { + this.emit('clearAllInstancesReducer') + } + + addInstance (address, abi, name) { + this.emit('addInstanceReducer', address, abi, name) + } + createVMAccount (newAccount) { return this.blockchain.createVMAccount(newAccount) } diff --git a/apps/remix-ide/src/blockchain/blockchain.js b/apps/remix-ide/src/blockchain/blockchain.js index 3382d3a649..705c3089bd 100644 --- a/apps/remix-ide/src/blockchain/blockchain.js +++ b/apps/remix-ide/src/blockchain/blockchain.js @@ -22,7 +22,7 @@ const profile = { name: 'blockchain', displayName: 'Blockchain', description: 'Blockchain - Logic', - methods: [], + methods: ['getCode', 'getTransactionReceipt'], version: packageJson.version } @@ -392,6 +392,14 @@ export class Blockchain extends Plugin { return Object.keys(this.txRunner.pendingTxs).length } + async getCode(address) { + return await this.web3().eth.getCode(address) + } + + async getTransactionReceipt (hash) { + return await this.web3().eth.getTransactionReceipt(hash) + } + /** * This function send a tx only to javascript VM or testnet, will return an error for the mainnet * SHOULD BE TAKEN CAREFULLY! diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 6c1b909dfa..764f7db574 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -8,7 +8,7 @@ const requiredModules = [ // services + layout views + system views 'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme', 'fileManager', 'contentImport', 'blockchain', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons', 'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity-logic', 'gistHandler', 'layout', - 'notification', 'permissionhandler', 'walkthrough', 'storage', 'restorebackupzip', 'link-libraries', 'deploy-libraries'] + 'notification', 'permissionhandler', 'walkthrough', 'storage', 'restorebackupzip', 'link-libraries', 'deploy-libraries', 'intelligentScriptExecutor'] const dependentModules = ['git', 'hardhat', 'slither'] // module which shouldn't be manually activated (e.g git is activated by remixd) diff --git a/apps/solidity-compiler/src/app/compiler-api.ts b/apps/solidity-compiler/src/app/compiler-api.ts index 7d5188be6b..64747b3c51 100644 --- a/apps/solidity-compiler/src/app/compiler-api.ts +++ b/apps/solidity-compiler/src/app/compiler-api.ts @@ -321,7 +321,7 @@ export const CompilerApiMixin = (Base) => class extends Base { // Run the compiler instead of trying to save the website this.data.eventHandlers.onKeyDown = async (e) => { // ctrl+s or command+s - if ((e.metaKey || e.ctrlKey) && e.keyCode === 83 && this.currentFile !== '') { + if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.keyCode === 83 && this.currentFile !== '') { e.preventDefault() this.compileTabLogic.runCompiler(await this.getAppParameter('hardhat-compilation')) } diff --git a/libs/remix-analyzer/src/solidity-analyzer/index.ts b/libs/remix-analyzer/src/solidity-analyzer/index.ts index da6b5bb35e..d2c45ff635 100644 --- a/libs/remix-analyzer/src/solidity-analyzer/index.ts +++ b/libs/remix-analyzer/src/solidity-analyzer/index.ts @@ -15,13 +15,13 @@ export default class staticAnalysisRunner { * @param toRun module indexes (compiled from remix IDE) * @param callback callback */ - run (compilationResult: CompilationResult, toRun: number[], callback: ((reports: AnalysisReport[]) => void)): void { + run (compilationResult: CompilationResult, toRun: number[]): AnalysisReport[] { const modules: ModuleObj[] = toRun.map((i) => { const Module = this.modules()[i] const m = new Module() return { name: m.name, mod: m } }) - this.runWithModuleList(compilationResult, modules, callback) + return this.runWithModuleList(compilationResult, modules) } /** @@ -30,7 +30,7 @@ export default class staticAnalysisRunner { * @param modules analysis module * @param callback callback */ - runWithModuleList (compilationResult: CompilationResult, modules: ModuleObj[], callback: ((reports: AnalysisReport[]) => void)): void { + runWithModuleList (compilationResult: CompilationResult, modules: ModuleObj[]): AnalysisReport[] { let reports: AnalysisReport[] = [] // Also provide convenience analysis via the AST walker. const walker = new AstWalker() @@ -64,7 +64,7 @@ export default class staticAnalysisRunner { } return { name: item.name, report: report } })) - callback(reports) + return reports } /** diff --git a/libs/remix-analyzer/test/analysis/staticAnalysisIntegration-test-0.4.24.ts b/libs/remix-analyzer/test/analysis/staticAnalysisIntegration-test-0.4.24.ts index 67607d7a35..8052a1870a 100644 --- a/libs/remix-analyzer/test/analysis/staticAnalysisIntegration-test-0.4.24.ts +++ b/libs/remix-analyzer/test/analysis/staticAnalysisIntegration-test-0.4.24.ts @@ -814,12 +814,11 @@ test('Integration test forLoopIteratesOverDynamicArray module', function (t: tes function runModuleOnFiles (Module: any, t: test.Test, cb: ((fname: string, report: AnalysisReportObj[]) => void)): void { const statRunner: StatRunner = new StatRunner() testFiles.forEach((fileName: string) => { - statRunner.runWithModuleList(compilationResults[fileName], [{ name: new Module().name, mod: new Module() }], (reports: AnalysisReport[]) => { - let report: AnalysisReportObj[] = reports[0].report - if (report.some((x: AnalysisReportObj) => x.warning.includes('INTERNAL ERROR'))) { - t.comment('Error while executing Module: ' + JSON.stringify(report)) - } - cb(fileName, report) - }) + const reports = statRunner.runWithModuleList(compilationResults[fileName], [{ name: new Module().name, mod: new Module() }]) + let report: AnalysisReportObj[] = reports[0].report + if (report.some((x: AnalysisReportObj) => x.warning.includes('INTERNAL ERROR'))) { + t.comment('Error while executing Module: ' + JSON.stringify(report)) + } + cb(fileName, report) }) } diff --git a/libs/remix-analyzer/test/analysis/staticAnalysisIntegration-test-0.5.0.ts b/libs/remix-analyzer/test/analysis/staticAnalysisIntegration-test-0.5.0.ts index 780cfa2b63..aa14866a69 100644 --- a/libs/remix-analyzer/test/analysis/staticAnalysisIntegration-test-0.5.0.ts +++ b/libs/remix-analyzer/test/analysis/staticAnalysisIntegration-test-0.5.0.ts @@ -816,12 +816,11 @@ test('Integration test forLoopIteratesOverDynamicArray module', function (t: tes function runModuleOnFiles (Module: any, t: test.Test, cb: ((fname: string, report: AnalysisReportObj[]) => void)): void { const statRunner: StatRunner = new StatRunner() testFiles.forEach((fileName: string) => { - statRunner.runWithModuleList(compilationResults[fileName], [{ name: new Module().name, mod: new Module() }], (reports: AnalysisReport[]) => { - let report: AnalysisReportObj[] = reports[0].report - if (report.some((x: AnalysisReportObj) => x['warning'].includes('INTERNAL ERROR'))) { - t.comment('Error while executing Module: ' + JSON.stringify(report)) - } - cb(fileName, report) - }) + const reports = statRunner.runWithModuleList(compilationResults[fileName], [{ name: new Module().name, mod: new Module() }]) + let report: AnalysisReportObj[] = reports[0].report + if (report.some((x: AnalysisReportObj) => x['warning'].includes('INTERNAL ERROR'))) { + t.comment('Error while executing Module: ' + JSON.stringify(report)) + } + cb(fileName, report) }) } diff --git a/libs/remix-analyzer/test/analysis/staticAnalysisIssues-test-0.4.24.ts b/libs/remix-analyzer/test/analysis/staticAnalysisIssues-test-0.4.24.ts index de6303ef89..2a6091dc32 100644 --- a/libs/remix-analyzer/test/analysis/staticAnalysisIssues-test-0.4.24.ts +++ b/libs/remix-analyzer/test/analysis/staticAnalysisIssues-test-0.4.24.ts @@ -31,10 +31,9 @@ test('staticAnalysisIssues.functionParameterPassingError', function (t) { const statRunner: StatRunner = new StatRunner() t.doesNotThrow(() => { - statRunner.runWithModuleList(res, [{ name: new Module().name, mod: new Module() }], (reports: AnalysisReport[]) => {}) + statRunner.runWithModuleList(res, [{ name: new Module().name, mod: new Module()}]) }, 'Analysis should not throw') - statRunner.runWithModuleList(res, [{ name: new Module().name, mod: new Module() }], (reports: AnalysisReport[]) => { - t.ok(!reports.some((mod: AnalysisReport) => mod.report.some((rep: AnalysisReportObj) => rep.warning.includes('INTERNAL ERROR')), 'Should not have internal errors')) - }) + const reports = statRunner.runWithModuleList(res, [{ name: new Module().name, mod: new Module()}]) + t.ok(!reports.some((mod: AnalysisReport) => mod.report.some((rep: AnalysisReportObj) => rep.warning.includes('INTERNAL ERROR')), 'Should not have internal errors')) }) diff --git a/libs/remix-analyzer/test/analysis/staticAnalysisIssues-test-0.5.0.ts b/libs/remix-analyzer/test/analysis/staticAnalysisIssues-test-0.5.0.ts index beac78be6d..43234e4168 100644 --- a/libs/remix-analyzer/test/analysis/staticAnalysisIssues-test-0.5.0.ts +++ b/libs/remix-analyzer/test/analysis/staticAnalysisIssues-test-0.5.0.ts @@ -31,10 +31,9 @@ test('staticAnalysisIssues.functionParameterPassingError', function (t) { const statRunner: StatRunner = new StatRunner() t.doesNotThrow(() => { - statRunner.runWithModuleList(res, [{ name: new Module().name, mod: new Module() }], (reports: AnalysisReport[]) => {}) + statRunner.runWithModuleList(res, [{ name: new Module().name, mod: new Module() }]) }, 'Analysis should not throw') - statRunner.runWithModuleList(res, [{ name: new Module().name, mod: new Module() }], (reports: AnalysisReport[]) => { - t.ok(!reports.some((mod: AnalysisReport) => mod.report.some((rep: AnalysisReportObj) => rep.warning.includes('INTERNAL ERROR')), 'Should not have internal errors')) - }) + const reports = statRunner.runWithModuleList(res, [{ name: new Module().name, mod: new Module() }]) + t.ok(!reports.some((mod: AnalysisReport) => mod.report.some((rep: AnalysisReportObj) => rep.warning.includes('INTERNAL ERROR')), 'Should not have internal errors')) }) diff --git a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts index df7a895bfc..dc44695680 100644 --- a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts +++ b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts @@ -1,10 +1,11 @@ 'use strict' import { Plugin } from '@remixproject/engine' +import { util } from '@remix-project/remix-lib' import { CompilerAbstract } from '@remix-project/remix-solidity' const profile = { name: 'compilerArtefacts', - methods: ['get', 'addResolvedContract', 'getCompilerAbstract', 'getAllContractDatas', 'getLastCompilationResult', 'getArtefactsByContractName'], + methods: ['get', 'addResolvedContract', 'getCompilerAbstract', 'getAllContractDatas', 'getLastCompilationResult', 'getArtefactsByContractName', 'getContractDataFromAddress'], events: [], version: '0.0.1' } @@ -72,15 +73,27 @@ export class CompilerArtefacts extends Plugin { * @returns compilatin output */ getAllContractDatas () { + return this.filterAllContractDatas(() => true) + } + + /** + * filter compilation output for contracts compiled during a session of Remix IDE + * @returns compilatin output + */ + filterAllContractDatas (filter) { const contractsData = {} Object.keys(this.compilersArtefactsPerFile).map((targetFile) => { const contracts = this.compilersArtefactsPerFile[targetFile].getContracts() - Object.keys(contracts).map((file) => { contractsData[file] = contracts[file] }) + Object.keys(contracts).map((file) => { + if (filter(file, contracts[file])) contractsData[file] = contracts[file] + }) }) // making sure we save last compilation result in there if (this.compilersArtefacts.__last) { const contracts = this.compilersArtefacts.__last.getContracts() - Object.keys(contracts).map((file) => { contractsData[file] = contracts[file] }) + Object.keys(contracts).map((file) => { + if (filter(file, contracts[file])) contractsData[file] = contracts[file] + }) } return contractsData } @@ -182,4 +195,20 @@ export class CompilerArtefacts extends Plugin { get (key) { return this.compilersArtefacts[key] } + + async getContractDataFromAddress (address) { + const code = await this.call('blockchain', 'getCode', address) + let found + this.filterAllContractDatas((file, contractsData) => { + for (const name of Object.keys(contractsData)) { + const contract = contractsData[name] + if (util.compareByteCode(code, '0x' + contract.evm.deployedBytecode.object)) { + found = { name, contract } + return true + } + } + return true + }) + return found + } } diff --git a/libs/remix-core-plugin/src/lib/compiler-metadata.ts b/libs/remix-core-plugin/src/lib/compiler-metadata.ts index e7c5f3eec6..2f065e769f 100644 --- a/libs/remix-core-plugin/src/lib/compiler-metadata.ts +++ b/libs/remix-core-plugin/src/lib/compiler-metadata.ts @@ -103,7 +103,8 @@ export class CompilerMetadata extends Plugin { }, abi: contract.object.abi } - await this.call('fileManager', 'writeFile', fileName, JSON.stringify(data, null, '\t')) + await this.call('fileManager', 'writeFile', fileName, JSON.stringify(data, null, '\t')) + this.emit('artefactsUpdated', fileName, contract) } _syncContext (contract, metadata) { diff --git a/libs/remix-lib/src/execution/txListener.ts b/libs/remix-lib/src/execution/txListener.ts index 533a501d3b..0d5b26eb84 100644 --- a/libs/remix-lib/src/execution/txListener.ts +++ b/libs/remix-lib/src/execution/txListener.ts @@ -60,7 +60,7 @@ export class TxListener { // in VM mode // in web3 mode && listen remix txs only if (!this._isListening) return // we don't listen - if (this._loopId && this.executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network + if (this._loopId) return // we seems to already listen on a "web3" network let returnValue let execResult @@ -95,7 +95,7 @@ export class TxListener { // in VM mode // in web3 mode && listen remix txs only if (!this._isListening) return // we don't listen - if (this._loopId && this.executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network + if (this._loopId) return // we seems to already listen on a "web3" network this.executionContext.web3().eth.getTransaction(txResult.transactionHash, async (error, tx) => { if (error) return console.log(error) @@ -133,7 +133,7 @@ export class TxListener { */ init () { this.blocks = [] - this.lastBlock = null + this.lastBlock = -1 } /** @@ -170,8 +170,7 @@ export class TxListener { this.executionContext.web3().eth.getBlockNumber((error, blockNumber) => { if (this._loopId === null) return if (error) return console.log(error) - if (currentLoopId === this._loopId && (!this.lastBlock || blockNumber > this.lastBlock)) { - if (!this.lastBlock) this.lastBlock = blockNumber - 1 + if (currentLoopId === this._loopId && blockNumber > this.lastBlock) { let current = this.lastBlock + 1 this.lastBlock = blockNumber while (blockNumber >= current) { diff --git a/libs/remix-lib/src/execution/txRunnerVM.ts b/libs/remix-lib/src/execution/txRunnerVM.ts index e2b2322a89..fb4e291916 100644 --- a/libs/remix-lib/src/execution/txRunnerVM.ts +++ b/libs/remix-lib/src/execution/txRunnerVM.ts @@ -56,7 +56,12 @@ export class TxRunnerVM { runInVm (from, to, data, value, gasLimit, useCall, timestamp, callback) { const self = this - const account = self.vmaccounts[from] + let account + if (!from && useCall && Object.keys(self.vmaccounts).length) { + from = Object.keys(self.vmaccounts)[0] + account = self.vmaccounts[from] + } else account = self.vmaccounts[from] + if (!account) { return callback('Invalid account selected') } diff --git a/libs/remix-lib/src/execution/txRunnerWeb3.ts b/libs/remix-lib/src/execution/txRunnerWeb3.ts index 1da52c8049..4483a41fe0 100644 --- a/libs/remix-lib/src/execution/txRunnerWeb3.ts +++ b/libs/remix-lib/src/execution/txRunnerWeb3.ts @@ -101,16 +101,16 @@ export class TxRunnerWeb3 { // // @todo(#378) this should be removed when https://github.com/WalletConnect/walletconnect-monorepo/issues/334 is fixed callback(new Error('Gas estimation failed because of an unknown internal error. This may indicated that the transaction will fail.')) } - gasEstimationForceSend(err, () => { - // callback is called whenever no error - tx['gas'] = !gasEstimation ? gasLimit : gasEstimation - - this._api.detectNetwork((err, network) => { - if (err) { - console.log(err) - return - } - + this._api.detectNetwork((errNetWork, network) => { + if (errNetWork) { + console.log(errNetWork) + return + } + err = network.name === 'VM' ? null : err // just send the tx if "VM" + gasEstimationForceSend(err, () => { + // callback is called whenever no error + tx['gas'] = !gasEstimation ? gasLimit : gasEstimation + if (this._api.config.getUnpersistedProperty('doNotShowTransactionConfirmationAgain')) { return this._executeTx(tx, network, null, this._api, promptCb, callback) } @@ -120,23 +120,23 @@ export class TxRunnerWeb3 { }, (error) => { callback(error) }) + }, () => { + const blockGasLimit = this.currentblockGasLimit() + // NOTE: estimateGas very likely will return a large limit if execution of the code failed + // we want to be able to run the code in order to debug and find the cause for the failure + if (err) return callback(err) + + let warnEstimation = ' An important gas estimation might also be the sign of a problem in the contract code. Please check loops and be sure you did not sent value to a non payable function (that\'s also the reason of strong gas estimation). ' + warnEstimation += ' ' + err + + if (gasEstimation > gasLimit) { + return callback('Gas required exceeds limit: ' + gasLimit + '. ' + warnEstimation) + } + if (gasEstimation > blockGasLimit) { + return callback('Gas required exceeds block gas limit: ' + gasLimit + '. ' + warnEstimation) + } }) - }, () => { - const blockGasLimit = this.currentblockGasLimit() - // NOTE: estimateGas very likely will return a large limit if execution of the code failed - // we want to be able to run the code in order to debug and find the cause for the failure - if (err) return callback(err) - - let warnEstimation = ' An important gas estimation might also be the sign of a problem in the contract code. Please check loops and be sure you did not sent value to a non payable function (that\'s also the reason of strong gas estimation). ' - warnEstimation += ' ' + err - - if (gasEstimation > gasLimit) { - return callback('Gas required exceeds limit: ' + gasLimit + '. ' + warnEstimation) - } - if (gasEstimation > blockGasLimit) { - return callback('Gas required exceeds block gas limit: ' + gasLimit + '. ' + warnEstimation) - } - }) + }) }) } } diff --git a/libs/remix-simulator/src/methods/blocks.ts b/libs/remix-simulator/src/methods/blocks.ts index a58319b916..1ecc7b30e4 100644 --- a/libs/remix-simulator/src/methods/blocks.ts +++ b/libs/remix-simulator/src/methods/blocks.ts @@ -1,6 +1,9 @@ +import Web3 from 'web3' + export class Blocks { vmContext coinbase: string + TX_INDEX = '0x0' // currently there's always only 1 tx per block, so the transaction index will always be 0x0 constructor (vmContext, _options) { this.vmContext = vmContext const options = _options || {} @@ -37,6 +40,28 @@ export class Blocks { return cb(new Error('block not found')) } + console.log(block.transactions) + const transactions = block.transactions.map((t) => { + const hash = '0x' + t.hash().toString('hex') + const tx = this.vmContext.txByHash[hash] + const receipt = this.vmContext.currentVm.web3vm.txsReceipt[hash] + if (receipt) { + return { + blockHash: '0x' + block.hash().toString('hex'), + blockNumber: '0x' + block.header.number.toString('hex'), + from: receipt.from, + gas: Web3.utils.toHex(receipt.gas), + chainId: '0xd05', + gasPrice: '0x4a817c800', // 20000000000 + hash: receipt.transactionHash, + input: receipt.input, + nonce: '0x' + tx.nonce.toString('hex'), + transactionIndex: this.TX_INDEX, + value: receipt.value === '0x' ? '0x0' : receipt.value, + to: receipt.to ? receipt.to : null + } + } + }) const b = { baseFeePerGas: '0x01', number: this.toHex(block.header.number), @@ -55,7 +80,7 @@ export class Blocks { gasLimit: this.toHex(block.header.gasLimit), gasUsed: this.toHex(block.header.gasUsed), timestamp: this.toHex(block.header.timestamp), - transactions: block.transactions.map((t) => '0x' + t.hash().toString('hex')), + transactions, uncles: [] } cb(null, b) @@ -70,6 +95,28 @@ export class Blocks { eth_getBlockByHash (payload, cb) { const block = this.vmContext.blocks[payload.params[0]] + console.log(block.transactions) + const transactions = block.transactions.map((t) => { + const hash = '0x' + t.hash().toString('hex') + const tx = this.vmContext.txByHash[hash] + const receipt = this.vmContext.currentVm.web3vm.txsReceipt[hash] + if (receipt) { + return { + blockHash: '0x' + block.hash().toString('hex'), + blockNumber: '0x' + block.header.number.toString('hex'), + from: receipt.from, + gas: Web3.utils.toHex(receipt.gas), + chainId: '0xd05', + gasPrice: '0x4a817c800', // 20000000000 + hash: receipt.transactionHash, + input: receipt.input, + nonce: '0x' + tx.nonce.toString('hex'), + transactionIndex: this.TX_INDEX, + value: receipt.value === '0x' ? '0x0' : receipt.value, + to: receipt.to ? receipt.to : null + } + } + }) const b = { baseFeePerGas: '0x01', number: this.toHex(block.header.number), @@ -88,7 +135,7 @@ export class Blocks { gasLimit: this.toHex(block.header.gasLimit), gasUsed: this.toHex(block.header.gasUsed), timestamp: this.toHex(block.header.timestamp), - transactions: block.transactions.map((t) => '0x' + t.hash().toString('hex')), + transactions, uncles: [] } diff --git a/libs/remix-simulator/src/methods/transactions.ts b/libs/remix-simulator/src/methods/transactions.ts index 93b6850812..4774e948b8 100644 --- a/libs/remix-simulator/src/methods/transactions.ts +++ b/libs/remix-simulator/src/methods/transactions.ts @@ -2,6 +2,7 @@ import Web3 from 'web3' import { toChecksumAddress, BN, Address } from 'ethereumjs-util' import { processTx } from './txProcess' import { execution } from '@remix-project/remix-lib' +import { ethers } from 'ethers' const TxRunnerVM = execution.TxRunnerVM const TxRunner = execution.TxRunner @@ -122,7 +123,31 @@ export class Transactions { } eth_estimateGas (payload, cb) { - cb(null, 10000000 * 8) + // from might be lowercased address (web3) + if (payload.params && payload.params.length > 0 && payload.params[0].from) { + payload.params[0].from = toChecksumAddress(payload.params[0].from) + } + if (payload.params && payload.params.length > 0 && payload.params[0].to) { + payload.params[0].to = toChecksumAddress(payload.params[0].to) + } + + payload.params[0].gas = 10000000 * 10 + + processTx(this.txRunnerInstance, payload, true, (error, result) => { + if (error) return cb(error) + if (result.result.status === '0x0') { + try { + const msg = result.result.execResult.returnValue + const abiCoder = new ethers.utils.AbiCoder() + const reason = abiCoder.decode(['string'], msg.slice(4))[0] + return cb('revert ' + reason) + } catch (e) { + return cb(e.message) + } + } + const gasUsed = result.result.execResult.gasUsed.toNumber() + cb(null, Math.ceil(gasUsed + (15 * gasUsed) / 100)) + }) } eth_getCode (payload, cb) { diff --git a/libs/remix-tests/src/compiler.ts b/libs/remix-tests/src/compiler.ts index 697483d593..f16c925edc 100644 --- a/libs/remix-tests/src/compiler.ts +++ b/libs/remix-tests/src/compiler.ts @@ -183,7 +183,7 @@ export function compileContractSources (sources: SrcIfc, newCompConfig: any, imp async.waterfall([ (next) => { - if (!deepequal(UTRunner.compilerConfig, newCompConfig)) { + if (!compiler || !deepequal(UTRunner.compilerConfig, newCompConfig)) { UTRunner.compilerConfig = newCompConfig const { currentCompilerUrl, evmVersion, optimize, runs, usingWorker } = newCompConfig compiler = new RemixCompiler(importFileCb) diff --git a/libs/remix-tests/src/deployer.ts b/libs/remix-tests/src/deployer.ts index 505281fe00..d3164eb285 100644 --- a/libs/remix-tests/src/deployer.ts +++ b/libs/remix-tests/src/deployer.ts @@ -79,6 +79,9 @@ export function deployAll (compileResult: compilationInterface, web3: Web3, test console.error(err) callback(err) }) + }).catch((err) => { + console.error(err) + callback(err) }) } 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 95913e533b..4b0d4fa9be 100644 --- a/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx +++ b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx @@ -12,6 +12,7 @@ export interface RemixUiCheckboxProps { id?: string itemName?: string categoryId?: string + title?: string visibility?: string display?: string } @@ -26,11 +27,12 @@ export const RemixUiCheckbox = ({ onChange, itemName, categoryId, + title, visibility, display = 'flex' }: RemixUiCheckboxProps) => { return ( -
+
{ const startPluginManager = async () => { plugin.verticalIcons.select('pluginManager') } - const saveAs = (blob, name) => { - const node = document.createElement('a') - node.download = name - node.rel = 'noopener' - node.href = URL.createObjectURL(blob) - setTimeout(function () { URL.revokeObjectURL(node.href) }, 4E4) // 40s - setTimeout(function () { - try { - node.dispatchEvent(new MouseEvent('click')) - } catch (e) { - const evt = document.createEvent('MouseEvents') - evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, - 20, false, false, false, false, 0, null) - node.dispatchEvent(evt) - } - }, 0) // 40s - } - const downloadFiles = async () => { - try { - plugin.call('notification', 'toast', 'preparing files for download, please wait..') - const zip = new JSZip() - zip.file("readme.txt", "This is a Remix backup file.\nThis zip should be used by the restore backup tool in Remix.\nThe .workspaces directory contains your workspaces.") - const browserProvider = fileManager.getProvider('browser') - await browserProvider.copyFolderToJson('/', ({ path, content }) => { - zip.file(path, content) - }) - zip.generateAsync({ type: 'blob' }).then(function (blob) { - const today = new Date() - const date = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate() - const time = today.getHours() + 'h' + today.getMinutes() + 'min' - saveAs(blob, `remix-backup-at-${time}-${date}.zip`) - _paq.push(['trackEvent', 'Backup', 'download', 'home']) - }).catch((e) => { - _paq.push(['trackEvent', 'Backup', 'error', e.message]) - plugin.call('notification', 'toast', e.message) - }) - } catch (e) { - plugin.call('notification', 'toast', e.message) - } - } - - const restoreBackupZip = async () => { - await plugin.appManager.activatePlugin(['restorebackupzip']) - await plugin.call('mainPanel', 'showContent', 'restorebackupzip') - plugin.verticalIcons.select('restorebackupzip') - _paq.push(['trackEvent', 'pluginManager', 'userActivate', 'restorebackupzip']) - } const showFullMessage = (title: string, loadItem: string, examples: Array) => { setState(prevState => { @@ -336,14 +288,6 @@ export const RemixUiHomeTab = (props: RemixUiHomeTabProps) => {

-

- - -

-

- - -

diff --git a/libs/remix-ui/run-tab/src/lib/actions/index.ts b/libs/remix-ui/run-tab/src/lib/actions/index.ts index 17ae11e4bb..6e0ebeae9c 100644 --- a/libs/remix-ui/run-tab/src/lib/actions/index.ts +++ b/libs/remix-ui/run-tab/src/lib/actions/index.ts @@ -83,6 +83,14 @@ const setupEvents = () => { setExecutionContext(env) }) + plugin.on('udapp', 'clearAllInstancesReducer', () => { + dispatch(clearAllInstances()) + }) + + plugin.on('udapp', 'addInstanceReducer', (address, abi, name) => { + addInstance({ abi, address, name }) + }) + plugin.on('filePanel', 'setWorkspace', () => { dispatch(resetUdapp()) resetAndInit() @@ -549,7 +557,7 @@ export const updateTxFeeContent = (content: string) => { dispatch(setTxFeeContent(content)) } -const addInstance = (instance: { contractData?: ContractData, address: string, name: string, abi?: any, decodedResponse?: Record }) => { +export const addInstance = (instance: { contractData?: ContractData, address: string, name: string, abi?: any, decodedResponse?: Record }) => { instance.decodedResponse = {} dispatch(addNewInstance(instance)) } diff --git a/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx b/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx index 6b7503af02..c2ae8c2ecc 100644 --- a/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx +++ b/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useReducer } from 'react' // eslint-disable-line +import React, { useEffect, useState, useReducer, useRef } from 'react' // eslint-disable-line import Button from './Button/StaticAnalyserButton' // eslint-disable-line import { util } from '@remix-project/remix-lib' import _ from 'lodash' @@ -64,10 +64,12 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { const [autoRun, setAutoRun] = useState(true) const [slitherEnabled, setSlitherEnabled] = useState(false) const [showSlither, setShowSlither] = useState(false) + let [showLibsWarning, setShowLibsWarning] = useState(false) // eslint-disable-line prefer-const const [categoryIndex, setCategoryIndex] = useState(groupedModuleIndex(groupedModules)) - - const warningContainer = React.useRef(null) const [warningState, setWarningState] = useState({}) + + const warningContainer = useRef(null) + const allWarnings = useRef({}) const [state, dispatch] = useReducer(analysisReducer, initialState) useEffect(() => { @@ -76,9 +78,12 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { useEffect(() => { setWarningState({}) + const runAnalysis = async () => { + await run(state.data, state.source, state.file) + } if (autoRun) { if (state.data !== null) { - run(state.data, state.source, state.file) + runAnalysis().catch(console.error); } } else { props.event.trigger('staticAnaysisWarning', []) @@ -131,6 +136,30 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { ) } + const filterWarnings = () => { + let newWarningState = {} + let newWarningCount = 0 + if (showLibsWarning) { + for (const category in allWarnings.current) + newWarningCount = newWarningCount + allWarnings.current[category].length + newWarningState = allWarnings.current + } + else { + for (const category in allWarnings.current) { + const warnings = allWarnings.current[category] + newWarningState[category] = [] + for (const warning of warnings) { + if (!warning.options.isLibrary) { + newWarningCount++ + newWarningState[category].push(warning) + } + } + } + } + props.event.trigger('staticAnaysisWarning', [newWarningCount]) + setWarningState(newWarningState) + } + const showWarnings = (warningMessage, groupByKey) => { const resultArray = [] warningMessage.map(x => { @@ -149,143 +178,149 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { } const groupedCategory = groupBy(resultArray, groupByKey) - setWarningState(groupedCategory) + allWarnings.current = groupedCategory + filterWarnings() } - const run = (lastCompilationResult, lastCompilationSource, currentFile) => { + const run = async (lastCompilationResult, lastCompilationSource, currentFile) => { if (state.data !== null) { if (lastCompilationResult && (categoryIndex.length > 0 || slitherEnabled)) { - let warningCount = 0 const warningMessage = [] const warningErrors = [] // Remix Analysis _paq.push(['trackEvent', 'solidityStaticAnalyzer', 'analyzeWithRemixAnalyzer']) - runner.run(lastCompilationResult, categoryIndex, results => { - results.map((result) => { - let moduleName - Object.keys(groupedModules).map(key => { - groupedModules[key].forEach(el => { - if (el.name === result.name) { - moduleName = groupedModules[key][0].categoryDisplayName - } - }) - }) - result.report.map((item) => { - let location: any = {} - let locationString = 'not available' - let column = 0 - let row = 0 - let fileName = currentFile - if (item.location) { - const split = item.location.split(':') - const file = split[2] - location = { - start: parseInt(split[0]), - length: parseInt(split[1]) - } - location = props.analysisModule._deps.offsetToLineColumnConverter.offsetToLineColumn( - location, - parseInt(file), - lastCompilationSource.sources, - lastCompilationResult.sources - ) - row = location.start.line - column = location.start.column - locationString = row + 1 + ':' + column + ':' - fileName = Object.keys(lastCompilationResult.sources)[file] - } - warningCount++ - const msg = message(result.name, item.warning, item.more, fileName, locationString) - const options = { - type: 'warning', - useSpan: true, - errFile: fileName, - fileName, - errLine: row, - errCol: column, - item: item, - name: result.name, - locationString, - more: item.more, - location: location + const results = runner.run(lastCompilationResult, categoryIndex) + for (const result of results) { + let moduleName + Object.keys(groupedModules).map(key => { + groupedModules[key].forEach(el => { + if (el.name === result.name) { + moduleName = groupedModules[key][0].categoryDisplayName } - warningErrors.push(options) - warningMessage.push({ msg, options, hasWarning: true, warningModuleName: moduleName }) }) }) - // Slither Analysis - if (slitherEnabled) { - props.analysisModule.call('solidity', 'getCompilerState').then((compilerState) => { - const { currentVersion, optimize, evmVersion } = compilerState - props.analysisModule.call('terminal', 'log', { type: 'info', value: '[Slither Analysis]: Running...' }) - _paq.push(['trackEvent', 'solidityStaticAnalyzer', 'analyzeWithSlither']) - props.analysisModule.call('slither', 'analyse', state.file, { currentVersion, optimize, evmVersion }).then(async (result) => { - if (result.status) { - props.analysisModule.call('terminal', 'log', { type: 'info', value: `[Slither Analysis]: Analysis Completed!! ${result.count} warnings found.` }) - const report = result.data - for (const item of report) { - let location: any = {} - let locationString = 'not available' - let column = 0 - let row = 0 - let fileName = currentFile + for (const item of result.report) { + let location: any = {} + let locationString = 'not available' + let column = 0 + let row = 0 + let fileName = currentFile + let isLibrary = false - if (item.sourceMap && item.sourceMap.length) { - let path = item.sourceMap[0].source_mapping.filename_relative - let fileIndex = Object.keys(lastCompilationResult.sources).indexOf(path) - if (fileIndex === -1) { - path = await props.analysisModule.call('fileManager', 'getUrlFromPath', path) - fileIndex = Object.keys(lastCompilationResult.sources).indexOf(path.file) - } - 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 + if (item.location) { + const split = item.location.split(':') + const file = split[2] + location = { + start: parseInt(split[0]), + length: parseInt(split[1]) + } + location = props.analysisModule._deps.offsetToLineColumnConverter.offsetToLineColumn( + location, + parseInt(file), + lastCompilationSource.sources, + lastCompilationResult.sources + ) + row = location.start.line + column = location.start.column + locationString = row + 1 + ':' + column + ':' + fileName = Object.keys(lastCompilationResult.sources)[file] + } + if(fileName !== currentFile) { + const {file, provider} = await props.analysisModule.call('fileManager', 'getPathFromUrl', fileName) + if (file.startsWith('.deps') || (provider.type === 'localhost' && file.startsWith('localhost/node_modules'))) isLibrary = true + } + const msg = message(result.name, item.warning, item.more, fileName, locationString) + const options = { + type: 'warning', + useSpan: true, + errFile: fileName, + fileName, + isLibrary, + errLine: row, + errCol: column, + item: item, + name: result.name, + locationString, + more: item.more, + location: location + } + warningErrors.push(options) + warningMessage.push({ msg, options, hasWarning: true, warningModuleName: moduleName }) + } + } + // Slither Analysis + if (slitherEnabled) { + try { + const compilerState = await props.analysisModule.call('solidity', 'getCompilerState') + const { currentVersion, optimize, evmVersion } = compilerState + await props.analysisModule.call('terminal', 'log', { type: 'info', value: '[Slither Analysis]: Running...' }) + _paq.push(['trackEvent', 'solidityStaticAnalyzer', 'analyzeWithSlither']) + const result = await props.analysisModule.call('slither', 'analyse', state.file, { currentVersion, optimize, evmVersion }) + if (result.status) { + props.analysisModule.call('terminal', 'log', { type: 'info', value: `[Slither Analysis]: Analysis Completed!! ${result.count} warnings found.` }) + const report = result.data + for (const item of report) { + let location: any = {} + let locationString = 'not available' + let column = 0 + let row = 0 + let fileName = currentFile + let isLibrary = false + + if (item.sourceMap && item.sourceMap.length) { + let path = item.sourceMap[0].source_mapping.filename_relative + let fileIndex = Object.keys(lastCompilationResult.sources).indexOf(path) + if (fileIndex === -1) { + path = await props.analysisModule.call('fileManager', 'getUrlFromPath', path) + fileIndex = Object.keys(lastCompilationResult.sources).indexOf(path.file) + } + if (fileIndex >= 0) { + location = { + start: item.sourceMap[0].source_mapping.start, + length: item.sourceMap[0].source_mapping.length } - warningErrors.push(options) - warningMessage.push({ msg, options, hasWarning: true, warningModuleName: 'Slither Analysis' }) + 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] } - showWarnings(warningMessage, 'warningModuleName') - props.event.trigger('staticAnaysisWarning', [warningCount]) } - }).catch(() => { - props.analysisModule.call('terminal', 'log', { type: 'error', value: '[Slither Analysis]: Error occured! See remixd console for details.' }) - showWarnings(warningMessage, 'warningModuleName') - }) - }) - } else { + if(fileName !== currentFile) { + const {file, provider} = await props.analysisModule.call('fileManager', 'getPathFromUrl', fileName) + if (file.startsWith('.deps') || (provider.type === 'localhost' && file.startsWith('localhost/node_modules'))) isLibrary = true + } + const msg = message(item.title, item.description, item.more, fileName, locationString) + const options = { + type: 'warning', + useSpan: true, + errFile: fileName, + fileName, + isLibrary, + 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') + } + } catch(error) { + props.analysisModule.call('terminal', 'log', { type: 'error', value: '[Slither Analysis]: Error occured! See remixd console for details.' }) showWarnings(warningMessage, 'warningModuleName') - props.event.trigger('staticAnaysisWarning', [warningCount]) } - }) + } else showWarnings(warningMessage, 'warningModuleName') } else { if (categoryIndex.length) { warningContainer.current.innerText = 'No compiled AST available' @@ -346,6 +381,17 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { } } + const handleShowLibsWarning = () => { + if (showLibsWarning) { + showLibsWarning = false + setShowLibsWarning(false) + } else { + showLibsWarning = true + setShowLibsWarning(true) + } + filterWarnings() + } + const categoryItem = (categoryId, item, i) => { return (
@@ -409,6 +455,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { { return (value.map(x => { return x._index.toString() @@ -421,12 +468,13 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { {}} /> -
{ showSlither &&
@@ -469,14 +517,25 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { {state.file}
-
{Object.entries(warningState).length > 0 &&
+ {}} + /> +
{ (Object.entries(warningState).map((element, index) => (
- {element[0]} + {element[1]['length'] > 0 ? {element[0]} : null} {element[1]['map']((x, i) => ( // eslint-disable-line dot-notation x.hasWarning ? ( // eslint-disable-next-line dot-notation
diff --git a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx index 2da3df49d3..f8eae05bb5 100644 --- a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx +++ b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx @@ -462,7 +462,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => { title="If checked Remix will listen on all transactions mined in the current environment and not only transactions created by you" htmlFor="listenNetworkCheck" > - listen on network + listen on all transactions
diff --git a/libs/remix-ui/workspace/src/lib/actions/index.ts b/libs/remix-ui/workspace/src/lib/actions/index.ts index 7e1a47e43a..a000853f9f 100644 --- a/libs/remix-ui/workspace/src/lib/actions/index.ts +++ b/libs/remix-ui/workspace/src/lib/actions/index.ts @@ -5,13 +5,14 @@ import { customAction } from '@remixproject/plugin-api/lib/file-system/file-pane import { displayNotification, displayPopUp, fetchDirectoryError, fetchDirectoryRequest, fetchDirectorySuccess, focusElement, fsInitializationCompleted, hidePopUp, removeInputFieldSuccess, setCurrentWorkspace, setExpandPath, setMode, setWorkspaces } from './payload' import { listenOnPluginEvents, listenOnProviderEvents } from './events' import { createWorkspaceTemplate, getWorkspaces, loadWorkspacePreset, setPlugin } from './workspace' +import { QueryParams } from '@remix-project/remix-lib' +import JSZip from 'jszip' export * from './events' export * from './workspace' -import { QueryParams } from '@remix-project/remix-lib' - const queryParams = new QueryParams() +const _paq = window._paq = window._paq || [] let plugin, dispatch: React.Dispatch @@ -269,6 +270,40 @@ export const handleExpandPath = (paths: string[]) => { dispatch(setExpandPath(paths)) } +export const handleDownloadFiles = async () => { + try { + plugin.call('notification', 'toast', 'preparing files for download, please wait..') + const zip = new JSZip() + + zip.file("readme.txt", "This is a Remix backup file.\nThis zip should be used by the restore backup tool in Remix.\nThe .workspaces directory contains your workspaces.") + const browserProvider = plugin.fileManager.getProvider('browser') + + await browserProvider.copyFolderToJson('/', ({ path, content }) => { + zip.file(path, content) + }) + zip.generateAsync({ type: 'blob' }).then(function (blob) { + const today = new Date() + const date = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate() + const time = today.getHours() + 'h' + today.getMinutes() + 'min' + + saveAs(blob, `remix-backup-at-${time}-${date}.zip`) + _paq.push(['trackEvent', 'Backup', 'download', 'home']) + }).catch((e) => { + _paq.push(['trackEvent', 'Backup', 'error', e.message]) + plugin.call('notification', 'toast', e.message) + }) + } catch (e) { + plugin.call('notification', 'toast', e.message) + } +} + +export const restoreBackupZip = async () => { + await plugin.appManager.activatePlugin(['restorebackupzip']) + await plugin.call('mainPanel', 'showContent', 'restorebackupzip') + plugin.verticalIcons.select('restorebackupzip') + _paq.push(['trackEvent', 'pluginManager', 'userActivate', 'restorebackupzip']) +} + const packageGistFiles = async (directory) => { const workspaceProvider = plugin.fileProviders.workspace const isFile = await workspaceProvider.isFile(directory) @@ -344,3 +379,23 @@ const getOriginalFiles = async (id) => { const data = await res.json() return data.files || [] } + +const saveAs = (blob, name) => { + const node = document.createElement('a') + + node.download = name + node.rel = 'noopener' + node.href = URL.createObjectURL(blob) + setTimeout(function () { URL.revokeObjectURL(node.href) }, 4E4) // 40s + setTimeout(function () { + try { + node.dispatchEvent(new MouseEvent('click')) + } catch (e) { + const evt = document.createEvent('MouseEvents') + + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, + 20, false, false, false, false, 0, null) + node.dispatchEvent(evt) + } + }, 0) // 40s +} diff --git a/libs/remix-ui/workspace/src/lib/contexts/index.ts b/libs/remix-ui/workspace/src/lib/contexts/index.ts index 1aedce98b5..b5daf01cc9 100644 --- a/libs/remix-ui/workspace/src/lib/contexts/index.ts +++ b/libs/remix-ui/workspace/src/lib/contexts/index.ts @@ -27,5 +27,7 @@ export const FileSystemContext = createContext<{ dispatchRunScript: (path: string) => Promise, dispatchEmitContextMenuEvent: (cmd: customAction) => Promise, dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise - dispatchHandleExpandPath: (paths: string[]) => Promise + dispatchHandleExpandPath: (paths: string[]) => Promise, + dispatchHandleDownloadFiles: () => Promise, + dispatchHandleRestoreBackup: () => Promise }>(null) diff --git a/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css b/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css index 534da8bca6..354be77283 100644 --- a/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css +++ b/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css @@ -58,4 +58,7 @@ .remixui_menuicon { padding-right : 10px; } + .remixui_menuicon:hover { + transform: scale(1.3); + } \ No newline at end of file diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx index 904ed7cade..cb4d4ce864 100644 --- a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -5,7 +5,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line // eslint-disable-next-line @typescript-eslint/no-unused-vars import { FileSystemContext } from '../contexts' import { browserReducer, browserInitialState } from '../reducers/workspace' -import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from '../actions' +import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, handleDownloadFiles, restoreBackupZip } from '../actions' import { Modal, WorkspaceProps } from '../types' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { Workspace } from '../remix-ui-workspace' @@ -115,6 +115,14 @@ export const FileSystemProvider = (props: WorkspaceProps) => { await handleExpandPath(paths) } + const dispatchHandleDownloadFiles = async () => { + await handleDownloadFiles() + } + + const dispatchHandleRestoreBackup = async () => { + await restoreBackupZip() + } + useEffect(() => { dispatchInitWorkspace() }, []) @@ -214,7 +222,9 @@ export const FileSystemProvider = (props: WorkspaceProps) => { dispatchRunScript, dispatchEmitContextMenuEvent, dispatchHandleClickFile, - dispatchHandleExpandPath + dispatchHandleExpandPath, + dispatchHandleDownloadFiles, + dispatchHandleRestoreBackup } return ( diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index a51224f86c..ceb4c08b07 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -50,6 +50,22 @@ export function Workspace () { global.modal('Delete Current Workspace', 'Are you sure to delete the current workspace?', 'OK', onFinishDeleteWorkspace, '') } + const downloadWorkspaces = async () => { + try { + await global.dispatchHandleDownloadFiles() + } catch (e) { + console.error(e) + } + } + + const restoreBackup = async () => { + try { + await global.dispatchHandleRestoreBackup() + } catch (e) { + console.error(e) + } + } + const onFinishRenameWorkspace = async () => { if (workspaceRenameInput.current === undefined) return // @ts-ignore: Object is possibly 'null'. @@ -156,9 +172,30 @@ export function Workspace () { e.stopPropagation() deleteCurrentWorkspace() }} - className='fas fa-trash' + className='fas fa-trash remixui_menuicon' title='Delete'> + + { + e.stopPropagation() + restoreBackup() + }} + className='far fa-upload remixui_menuicon' + title='Restore Workspaces Backup'> +