From 2314a3a32e097722d7435ae2d5ba324361eca1bb Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jul 2024 08:08:42 +0200 Subject: [PATCH 1/9] add foundry --- apps/remixdesktop/src/lib/foundry.ts | 207 ++++++++++++++++++ .../remixdesktop/src/plugins/foundryPlugin.ts | 35 +++ 2 files changed, 242 insertions(+) create mode 100644 apps/remixdesktop/src/lib/foundry.ts create mode 100644 apps/remixdesktop/src/plugins/foundryPlugin.ts diff --git a/apps/remixdesktop/src/lib/foundry.ts b/apps/remixdesktop/src/lib/foundry.ts new file mode 100644 index 0000000000..30edf37889 --- /dev/null +++ b/apps/remixdesktop/src/lib/foundry.ts @@ -0,0 +1,207 @@ +import { spawn } from 'child_process' +import chokidar from 'chokidar' +import fs from 'fs' +import { basename, join } from 'path' +import * as utils from './utils' +export const FoundryClientMixin = (Base) => class extends Base { + + currentSharedFolder: string + watcher: chokidar.FSWatcher + warnlog: boolean + buildPath: string + cachePath: string + logTimeout: NodeJS.Timeout + processingTimeout: NodeJS.Timeout + + startListening() { + if (fs.existsSync(this.buildPath) && fs.existsSync(this.cachePath)) { + this.listenOnFoundryCompilation() + } else { + this.listenOnFoundryFolder() + } + } + + listenOnFoundryFolder() { + console.log('Foundry out folder doesn\'t exist... waiting for the compilation.') + try { + if (this.watcher) this.watcher.close() + this.watcher = chokidar.watch(this.currentSharedFolder, { depth: 1, ignorePermissionErrors: true, ignoreInitial: true }) + // watch for new folders + this.watcher.on('addDir', () => { + if (fs.existsSync(this.buildPath) && fs.existsSync(this.cachePath)) { + this.listenOnFoundryCompilation() + } + }) + } catch (e) { + console.log(e) + } + } + + compile() { + return new Promise((resolve, reject) => { + if (this.readOnly) { + const errMsg = '[Foundry Compilation]: Cannot compile in read-only mode' + return reject(new Error(errMsg)) + } + const cmd = `forge build` + const options = { cwd: this.currentSharedFolder, shell: true } + const child = spawn(cmd, options) + let result = '' + let error = '' + child.stdout.on('data', (data) => { + const msg = `[Foundry Compilation]: ${data.toString()}` + console.log('\x1b[32m%s\x1b[0m', msg) + result += msg + '\n' + }) + child.stderr.on('data', (err) => { + error += `[Foundry Compilation]: ${err.toString()} \n` + }) + child.on('close', () => { + if (error && result) resolve(error + result) + else if (error) reject(error) + else resolve(result) + }) + }) + } + + checkPath() { + if (!fs.existsSync(this.buildPath) || !fs.existsSync(this.cachePath)) { + this.listenOnFoundryFolder() + return false + } + if (!fs.existsSync(join(this.cachePath, 'solidity-files-cache.json'))) return false + return true + } + + private async processArtifact() { + if (!this.checkPath()) return + const folderFiles = await fs.promises.readdir(this.buildPath) // "out" folder + try { + const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' })) + // name of folders are file names + for (const file of folderFiles) { + const path = join(this.buildPath, file) // out/Counter.sol/ + const compilationResult = { + input: {}, + output: { + contracts: {}, + sources: {} + }, + inputSources: { sources: {}, target: '' }, + solcVersion: null, + compilationTarget: null + } + compilationResult.inputSources.target = file + await this.readContract(path, compilationResult, cache) + this.emit('compilationFinished', compilationResult.compilationTarget, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) + } + + clearTimeout(this.logTimeout) + this.logTimeout = setTimeout(() => { + // @ts-ignore + this.call('terminal', 'log', { type: 'log', value: `receiving compilation result from Foundry. Select a file to populate the contract interaction interface.` }) + console.log('Syncing compilation result from Foundry') + }, 1000) + + } catch (e) { + console.log(e) + } + } + + async triggerProcessArtifact() { + // prevent multiple calls + clearTimeout(this.processingTimeout) + this.processingTimeout = setTimeout(async () => await this.processArtifact(), 1000) + } + + listenOnFoundryCompilation() { + try { + if (this.watcher) this.watcher.close() + this.watcher = chokidar.watch(this.cachePath, { depth: 0, ignorePermissionErrors: true, ignoreInitial: true }) + this.watcher.on('change', async () => await this.triggerProcessArtifact()) + this.watcher.on('add', async () => await this.triggerProcessArtifact()) + // process the artifact on activation + this.triggerProcessArtifact() + } catch (e) { + console.log(e) + } + } + + async readContract(contractFolder, compilationResultPart, cache) { + const files = await fs.promises.readdir(contractFolder) + for (const file of files) { + const path = join(contractFolder, file) + const content = await fs.promises.readFile(path, { encoding: 'utf-8' }) + compilationResultPart.inputSources.sources[file] = { content } + await this.feedContractArtifactFile(file, content, compilationResultPart, cache) + } + } + + async feedContractArtifactFile(path, content, compilationResultPart, cache) { + const contentJSON = JSON.parse(content) + const contractName = basename(path).replace('.json', '') + + let sourcePath = '' + if (contentJSON?.metadata?.settings?.compilationTarget) { + for (const key in contentJSON.metadata.settings.compilationTarget) { + if (contentJSON.metadata.settings.compilationTarget[key] === contractName) { + sourcePath = key + break + } + } + } + + if (!sourcePath) return + + const currentCache = cache.files[sourcePath] + if (!currentCache.artifacts[contractName]) return + + // extract source and version + const metadata = contentJSON.metadata + if (metadata.compiler && metadata.compiler.version) { + compilationResultPart.solcVersion = metadata.compiler.version + } else { + compilationResultPart.solcVersion = '' + console.log('\x1b[32m%s\x1b[0m', 'compiler version not found, please update Foundry to the latest version.') + } + + if (metadata.sources) { + for (const path in metadata.sources) { + const absPath = utils.absolutePath(path, this.currentSharedFolder) + try { + const content = await fs.promises.readFile(absPath, { encoding: 'utf-8' }) + compilationResultPart.input[path] = { content } + } catch (e) { + compilationResultPart.input[path] = { content: '' } + } + } + } else { + console.log('\x1b[32m%s\x1b[0m', 'sources input not found, please update Foundry to the latest version.') + } + + compilationResultPart.compilationTarget = sourcePath + // extract data + if (!compilationResultPart.output['sources'][sourcePath]) compilationResultPart.output['sources'][sourcePath] = {} + compilationResultPart.output['sources'][sourcePath] = { + ast: contentJSON['ast'], + id: contentJSON['id'] + } + if (!compilationResultPart.output['contracts'][sourcePath]) compilationResultPart.output['contracts'][sourcePath] = {} + + contentJSON.bytecode.object = contentJSON.bytecode.object.replace('0x', '') + contentJSON.deployedBytecode.object = contentJSON.deployedBytecode.object.replace('0x', '') + compilationResultPart.output['contracts'][sourcePath][contractName] = { + abi: contentJSON.abi, + evm: { + bytecode: contentJSON.bytecode, + deployedBytecode: contentJSON.deployedBytecode, + methodIdentifiers: contentJSON.methodIdentifiers + } + } + } + + async sync() { + console.log('syncing Foundry with Remix...') + this.processArtifact() + } +} \ No newline at end of file diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts new file mode 100644 index 0000000000..0b3411ceae --- /dev/null +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -0,0 +1,35 @@ +import { Profile } from "@remixproject/plugin-utils"; +import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" + +import { ElectronBasePluginRemixdClient } from "../lib/remixd" + +import { FoundryClientMixin } from "../lib/foundry"; +const profile: Profile = { + name: 'slither', + displayName: 'electron slither', + description: 'electron slither', +} + +export class FoundryPlugin extends ElectronBasePlugin { + clients: any [] + constructor() { + super(profile, clientProfile, FoundryClientMixin(FoundryPluginClient)) + this.methods = [...super.methods] + } +} + +const clientProfile: Profile = { + name: 'foundry', + displayName: 'electron foundry', + description: 'electron foundry', + methods: ['sync', 'compile'] +} + + +class FoundryPluginClient extends ElectronBasePluginRemixdClient { + constructor(webContentsId: number, profile: Profile) { + super(webContentsId, profile); + } +} + + From ebbdf6b5eaead079c6928b2313b405fd334d6251 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 2 Jul 2024 18:58:07 +0200 Subject: [PATCH 2/9] add plugins --- apps/remix-ide/src/app.js | 138 +++++----- apps/remix-ide/src/app/panels/file-panel.js | 4 - .../src/app/plugins/electron/foundryPlugin.ts | 13 + .../src/app/plugins/electron/hardhatPlugin.ts | 13 + apps/remixdesktop/src/engine.ts | 6 + apps/remixdesktop/src/lib/foundry.ts | 207 --------------- apps/remixdesktop/src/lib/remixd.ts | 1 + apps/remixdesktop/src/lib/slither.ts | 193 -------------- apps/remixdesktop/src/lib/utils.ts | 2 +- .../remixdesktop/src/plugins/foundryPlugin.ts | 246 ++++++++++++++++-- .../remixdesktop/src/plugins/hardhatPlugin.ts | 219 ++++++++++++++++ .../remixdesktop/src/plugins/slitherPlugin.ts | 175 ++++++++++++- apps/remixdesktop/src/preload.ts | 2 +- 13 files changed, 727 insertions(+), 492 deletions(-) create mode 100644 apps/remix-ide/src/app/plugins/electron/foundryPlugin.ts create mode 100644 apps/remix-ide/src/app/plugins/electron/hardhatPlugin.ts delete mode 100644 apps/remixdesktop/src/lib/foundry.ts delete mode 100644 apps/remixdesktop/src/lib/slither.ts create mode 100644 apps/remixdesktop/src/plugins/hardhatPlugin.ts diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index cb6d5826de..2801654f38 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -1,45 +1,45 @@ 'use strict' -import {RunTab, makeUdapp} from './app/udapp' -import {RemixEngine} from './remixEngine' -import {RemixAppManager} from './remixAppManager' -import {ThemeModule} from './app/tabs/theme-module' -import {LocaleModule} from './app/tabs/locale-module' -import {NetworkModule} from './app/tabs/network-module' -import {Web3ProviderModule} from './app/tabs/web3-provider' -import {CompileAndRun} from './app/tabs/compile-and-run' -import {PluginStateLogger} from './app/tabs/state-logger' -import {SidePanel} from './app/components/side-panel' -import {StatusBar} from './app/components/status-bar' -import {HiddenPanel} from './app/components/hidden-panel' -import {PinnedPanel} from './app/components/pinned-panel' -import {VerticalIcons} from './app/components/vertical-icons' -import {LandingPage} from './app/ui/landing-page/landing-page' -import {MainPanel} from './app/components/main-panel' -import {PermissionHandlerPlugin} from './app/plugins/permission-handler-plugin' -import {AstWalker} from '@remix-project/remix-astwalker' -import {LinkLibraries, DeployLibraries, OpenZeppelinProxy} from '@remix-project/core-plugin' -import {CodeParser} from './app/plugins/parser/code-parser' -import {SolidityScript} from './app/plugins/solidity-script' - -import {WalkthroughService} from './walkthroughService' - -import {OffsetToLineColumnConverter, CompilerMetadata, CompilerArtefacts, FetchAndCompile, CompilerImports, GistHandler} from '@remix-project/core-plugin' - -import {Registry} from '@remix-project/remix-lib' -import {ConfigPlugin} from './app/plugins/config' -import {StoragePlugin} from './app/plugins/storage' -import {Layout} from './app/panels/layout' -import {NotificationPlugin} from './app/plugins/notification' -import {Blockchain} from './blockchain/blockchain' -import {MergeVMProvider, LondonVMProvider, BerlinVMProvider, ShanghaiVMProvider, CancunVMProvider} from './app/providers/vm-provider' -import {MainnetForkVMProvider} from './app/providers/mainnet-vm-fork-provider' -import {SepoliaForkVMProvider} from './app/providers/sepolia-vm-fork-provider' -import {GoerliForkVMProvider} from './app/providers/goerli-vm-fork-provider' -import {CustomForkVMProvider} from './app/providers/custom-vm-fork-provider' -import {HardhatProvider} from './app/providers/hardhat-provider' -import {GanacheProvider} from './app/providers/ganache-provider' -import {FoundryProvider} from './app/providers/foundry-provider' -import {ExternalHttpProvider} from './app/providers/external-http-provider' +import { RunTab, makeUdapp } from './app/udapp' +import { RemixEngine } from './remixEngine' +import { RemixAppManager } from './remixAppManager' +import { ThemeModule } from './app/tabs/theme-module' +import { LocaleModule } from './app/tabs/locale-module' +import { NetworkModule } from './app/tabs/network-module' +import { Web3ProviderModule } from './app/tabs/web3-provider' +import { CompileAndRun } from './app/tabs/compile-and-run' +import { PluginStateLogger } from './app/tabs/state-logger' +import { SidePanel } from './app/components/side-panel' +import { StatusBar } from './app/components/status-bar' +import { HiddenPanel } from './app/components/hidden-panel' +import { PinnedPanel } from './app/components/pinned-panel' +import { VerticalIcons } from './app/components/vertical-icons' +import { LandingPage } from './app/ui/landing-page/landing-page' +import { MainPanel } from './app/components/main-panel' +import { PermissionHandlerPlugin } from './app/plugins/permission-handler-plugin' +import { AstWalker } from '@remix-project/remix-astwalker' +import { LinkLibraries, DeployLibraries, OpenZeppelinProxy } from '@remix-project/core-plugin' +import { CodeParser } from './app/plugins/parser/code-parser' +import { SolidityScript } from './app/plugins/solidity-script' + +import { WalkthroughService } from './walkthroughService' + +import { OffsetToLineColumnConverter, CompilerMetadata, CompilerArtefacts, FetchAndCompile, CompilerImports, GistHandler } from '@remix-project/core-plugin' + +import { Registry } from '@remix-project/remix-lib' +import { ConfigPlugin } from './app/plugins/config' +import { StoragePlugin } from './app/plugins/storage' +import { Layout } from './app/panels/layout' +import { NotificationPlugin } from './app/plugins/notification' +import { Blockchain } from './blockchain/blockchain' +import { MergeVMProvider, LondonVMProvider, BerlinVMProvider, ShanghaiVMProvider, CancunVMProvider } from './app/providers/vm-provider' +import { MainnetForkVMProvider } from './app/providers/mainnet-vm-fork-provider' +import { SepoliaForkVMProvider } from './app/providers/sepolia-vm-fork-provider' +import { GoerliForkVMProvider } from './app/providers/goerli-vm-fork-provider' +import { CustomForkVMProvider } from './app/providers/custom-vm-fork-provider' +import { HardhatProvider } from './app/providers/hardhat-provider' +import { GanacheProvider } from './app/providers/ganache-provider' +import { FoundryProvider } from './app/providers/foundry-provider' +import { ExternalHttpProvider } from './app/providers/external-http-provider' import { FileDecorator } from './app/plugins/file-decorator' import { CodeFormat } from './app/plugins/code-format' import { SolidityUmlGen } from './app/plugins/solidity-umlgen' @@ -58,7 +58,12 @@ import { compilerLoaderPlugin, compilerLoaderPluginDesktop } from './app/plugins import { appUpdaterPlugin } from './app/plugins/electron/appUpdaterPlugin' import { SlitherHandleDesktop } from './app/plugins/electron/slitherPlugin' import { SlitherHandle } from './app/files/slither-handle' -import {SolCoder} from './app/plugins/solcoderAI' +import { FoundryHandle } from './app/files/foundry-handle' +import { FoundryHandleDesktop } from './app/plugins/electron/foundryPlugin' +import { HardhatHandle } from './app/files/hardhat-handle' +import { HardhatHandleDesktop } from './app/plugins/electron/hardhatPlugin' + +import { SolCoder } from './app/plugins/solcoderAI' const isElectron = require('is-electron') @@ -75,6 +80,7 @@ const Config = require('./config') const FileManager = require('./app/files/fileManager') import FileProvider from "./app/files/fileProvider" import { appPlatformTypes } from '@remix-ui/app' + const DGitProvider = require('./app/files/dgitProvider') const WorkspaceFileProvider = require('./app/files/workspaceFileProvider') @@ -83,19 +89,19 @@ const PluginManagerComponent = require('./app/components/plugin-manager-componen const CompileTab = require('./app/tabs/compile-tab') const SettingsTab = require('./app/tabs/settings-tab') const AnalysisTab = require('./app/tabs/analysis-tab') -const {DebuggerTab} = require('./app/tabs/debugger-tab') +const { DebuggerTab } = require('./app/tabs/debugger-tab') const TestTab = require('./app/tabs/test-tab') const FilePanel = require('./app/panels/file-panel') const Editor = require('./app/editor/editor') const Terminal = require('./app/panels/terminal') -const {TabProxy} = require('./app/panels/tab-proxy.js') +const { TabProxy } = require('./app/panels/tab-proxy.js') export class platformApi { - get name () { + get name() { return isElectron() ? appPlatformTypes.desktop : appPlatformTypes.web } - isDesktop () { + isDesktop() { return isElectron() } } @@ -115,7 +121,7 @@ class AppComponent { // load app config const config = new Config(configStorage) - Registry.getInstance().put({api: config, name: 'config'}) + Registry.getInstance().put({ api: config, name: 'config' }) // load file system this._components.filesProviders = {} @@ -196,12 +202,12 @@ class AppComponent { this.themeModule = new ThemeModule() // ----------------- locale service --------------------------------- this.localeModule = new LocaleModule() - Registry.getInstance().put({api: this.themeModule, name: 'themeModule'}) - Registry.getInstance().put({api: this.localeModule, name: 'localeModule'}) + Registry.getInstance().put({ api: this.themeModule, name: 'themeModule' }) + Registry.getInstance().put({ api: this.localeModule, name: 'localeModule' }) // ----------------- editor service ---------------------------- const editor = new Editor() // wrapper around ace editor - Registry.getInstance().put({api: editor, name: 'editor'}) + Registry.getInstance().put({ api: editor, name: 'editor' }) editor.event.register('requiringToSaveCurrentfile', (currentFile) => { fileManager.saveCurrentFile() if (currentFile.endsWith('.circom')) this.appManager.activatePlugin(['circuit-compiler']) @@ -209,7 +215,7 @@ class AppComponent { // ----------------- fileManager service ---------------------------- const fileManager = new FileManager(editor, appManager) - Registry.getInstance().put({api: fileManager, name: 'filemanager'}) + Registry.getInstance().put({ api: fileManager, name: 'filemanager' }) // ----------------- dGit provider --------------------------------- const dGitProvider = new DGitProvider() @@ -288,7 +294,7 @@ class AppComponent { // -------------------Terminal---------------------------------------- makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl)) const terminal = new Terminal( - {appManager, blockchain}, + { appManager, blockchain }, { getPosition: (event) => { const limitUp = 36 @@ -382,16 +388,24 @@ class AppComponent { this.engine.register([appUpdater]) } - const compilerloader = isElectron()? new compilerLoaderPluginDesktop(): new compilerLoaderPlugin() + const compilerloader = isElectron() ? new compilerLoaderPluginDesktop() : new compilerLoaderPlugin() this.engine.register([compilerloader]) // slither analyzer plugin (remixd / desktop) const slitherPlugin = isElectron() ? new SlitherHandleDesktop() : new SlitherHandle() this.engine.register([slitherPlugin]) + //foundry plugin + const foundryPlugin = isElectron() ? new FoundryHandleDesktop() : new FoundryHandle() + this.engine.register([foundryPlugin]) + + // hardhat plugin + const hardhatPlugin = isElectron() ? new HardhatHandleDesktop() : new HardhatHandle() + this.engine.register([hardhatPlugin]) + // LAYOUT & SYSTEM VIEWS const appPanel = new MainPanel() - Registry.getInstance().put({api: this.mainview, name: 'mainview'}) + Registry.getInstance().put({ api: this.mainview, name: 'mainview' }) const tabProxy = new TabProxy(fileManager, editor) this.engine.register([appPanel, tabProxy]) @@ -443,8 +457,6 @@ class AppComponent { analysis, test, filePanel.remixdHandle, - filePanel.hardhatHandle, - filePanel.foundryHandle, filePanel.truffleHandle, linkLibraries, deployLibraries, @@ -453,10 +465,10 @@ class AppComponent { ]) this.layout.panels = { - tabs: {plugin: tabProxy, active: true}, - editor: {plugin: editor, active: true}, - main: {plugin: appPanel, active: false}, - terminal: {plugin: terminal, active: true, minimized: false} + tabs: { plugin: tabProxy, active: true }, + editor: { plugin: editor, active: true }, + main: { plugin: appPanel, active: false }, + terminal: { plugin: terminal, active: true, minimized: false } } } @@ -469,7 +481,7 @@ class AppComponent { } catch (e) { console.log("couldn't register iframe plugins", e.message) } - if (isElectron()){ + if (isElectron()) { await this.appManager.activatePlugin(['fs']) } await this.appManager.activatePlugin(['layout']) @@ -511,8 +523,8 @@ class AppComponent { await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder']) await this.appManager.activatePlugin(['solidity-script', 'remix-templates']) - if (isElectron()){ - await this.appManager.activatePlugin(['isogit', 'electronconfig', 'electronTemplates', 'xterm', 'ripgrep', 'appUpdater', 'slither']) + if (isElectron()) { + await this.appManager.activatePlugin(['isogit', 'electronconfig', 'electronTemplates', 'xterm', 'ripgrep', 'appUpdater', 'slither', 'foundry', 'hardhat']) } this.appManager.on( diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index fed62be18a..2260443663 100644 --- a/apps/remix-ide/src/app/panels/file-panel.js +++ b/apps/remix-ide/src/app/panels/file-panel.js @@ -6,8 +6,6 @@ import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line import {Registry} from '@remix-project/remix-lib' import { RemixdHandle } from '../plugins/remixd-handle' import {PluginViewWrapper} from '@remix-ui/helper' -const { HardhatHandle } = require('../files/hardhat-handle.js') -const { FoundryHandle } = require('../files/foundry-handle.js') const { TruffleHandle } = require('../files/truffle-handle.js') /* @@ -68,8 +66,6 @@ module.exports = class Filepanel extends ViewPlugin { this.el.setAttribute('id', 'fileExplorerView') this.remixdHandle = new RemixdHandle(this.fileProviders.localhost, appManager) - this.hardhatHandle = new HardhatHandle() - this.foundryHandle = new FoundryHandle() this.truffleHandle = new TruffleHandle() this.contentImport = contentImport this.workspaces = [] diff --git a/apps/remix-ide/src/app/plugins/electron/foundryPlugin.ts b/apps/remix-ide/src/app/plugins/electron/foundryPlugin.ts new file mode 100644 index 0000000000..1655681697 --- /dev/null +++ b/apps/remix-ide/src/app/plugins/electron/foundryPlugin.ts @@ -0,0 +1,13 @@ +import { ElectronPlugin } from '@remixproject/engine-electron'; + +export class FoundryHandleDesktop extends ElectronPlugin { + constructor() { + super({ + displayName: 'foundry', + name: 'foundry', + description: 'electron foundry', + methods: ['sync', 'compile'] + }) + this.methods = ['sync', 'compile'] + } +} diff --git a/apps/remix-ide/src/app/plugins/electron/hardhatPlugin.ts b/apps/remix-ide/src/app/plugins/electron/hardhatPlugin.ts new file mode 100644 index 0000000000..fad7190df4 --- /dev/null +++ b/apps/remix-ide/src/app/plugins/electron/hardhatPlugin.ts @@ -0,0 +1,13 @@ +import { ElectronPlugin } from '@remixproject/engine-electron'; + +export class HardhatHandleDesktop extends ElectronPlugin { + constructor() { + super({ + displayName: 'hardhat', + name: 'hardhat', + description: 'electron hardhat', + methods: ['sync', 'compile'] + }) + this.methods = ['sync', 'compile'] + } +} diff --git a/apps/remixdesktop/src/engine.ts b/apps/remixdesktop/src/engine.ts index 6a00ceab31..9a8fd78aad 100644 --- a/apps/remixdesktop/src/engine.ts +++ b/apps/remixdesktop/src/engine.ts @@ -11,6 +11,8 @@ import { RipgrepPlugin } from './plugins/ripgrepPlugin'; import { CompilerLoaderPlugin } from './plugins/compilerLoader'; import { SlitherPlugin } from './plugins/slitherPlugin'; import { AppUpdaterPlugin } from './plugins/appUpdater'; +import { FoundryPlugin } from './plugins/foundryPlugin'; +import { HardhatPlugin } from './plugins/hardhatPlugin'; const engine = new Engine() const appManager = new PluginManager() @@ -23,6 +25,8 @@ const ripgrepPlugin = new RipgrepPlugin() const compilerLoaderPlugin = new CompilerLoaderPlugin() const slitherPlugin = new SlitherPlugin() const appUpdaterPlugin = new AppUpdaterPlugin() +const foundryPlugin = new FoundryPlugin() +const hardhatPlugin = new HardhatPlugin() engine.register(appManager) engine.register(fsPlugin) @@ -33,7 +37,9 @@ engine.register(templatesPlugin) engine.register(ripgrepPlugin) engine.register(compilerLoaderPlugin) engine.register(slitherPlugin) +engine.register(foundryPlugin) engine.register(appUpdaterPlugin) +engine.register(hardhatPlugin) appManager.activatePlugin('electronconfig') appManager.activatePlugin('fs') diff --git a/apps/remixdesktop/src/lib/foundry.ts b/apps/remixdesktop/src/lib/foundry.ts deleted file mode 100644 index 30edf37889..0000000000 --- a/apps/remixdesktop/src/lib/foundry.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { spawn } from 'child_process' -import chokidar from 'chokidar' -import fs from 'fs' -import { basename, join } from 'path' -import * as utils from './utils' -export const FoundryClientMixin = (Base) => class extends Base { - - currentSharedFolder: string - watcher: chokidar.FSWatcher - warnlog: boolean - buildPath: string - cachePath: string - logTimeout: NodeJS.Timeout - processingTimeout: NodeJS.Timeout - - startListening() { - if (fs.existsSync(this.buildPath) && fs.existsSync(this.cachePath)) { - this.listenOnFoundryCompilation() - } else { - this.listenOnFoundryFolder() - } - } - - listenOnFoundryFolder() { - console.log('Foundry out folder doesn\'t exist... waiting for the compilation.') - try { - if (this.watcher) this.watcher.close() - this.watcher = chokidar.watch(this.currentSharedFolder, { depth: 1, ignorePermissionErrors: true, ignoreInitial: true }) - // watch for new folders - this.watcher.on('addDir', () => { - if (fs.existsSync(this.buildPath) && fs.existsSync(this.cachePath)) { - this.listenOnFoundryCompilation() - } - }) - } catch (e) { - console.log(e) - } - } - - compile() { - return new Promise((resolve, reject) => { - if (this.readOnly) { - const errMsg = '[Foundry Compilation]: Cannot compile in read-only mode' - return reject(new Error(errMsg)) - } - const cmd = `forge build` - const options = { cwd: this.currentSharedFolder, shell: true } - const child = spawn(cmd, options) - let result = '' - let error = '' - child.stdout.on('data', (data) => { - const msg = `[Foundry Compilation]: ${data.toString()}` - console.log('\x1b[32m%s\x1b[0m', msg) - result += msg + '\n' - }) - child.stderr.on('data', (err) => { - error += `[Foundry Compilation]: ${err.toString()} \n` - }) - child.on('close', () => { - if (error && result) resolve(error + result) - else if (error) reject(error) - else resolve(result) - }) - }) - } - - checkPath() { - if (!fs.existsSync(this.buildPath) || !fs.existsSync(this.cachePath)) { - this.listenOnFoundryFolder() - return false - } - if (!fs.existsSync(join(this.cachePath, 'solidity-files-cache.json'))) return false - return true - } - - private async processArtifact() { - if (!this.checkPath()) return - const folderFiles = await fs.promises.readdir(this.buildPath) // "out" folder - try { - const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' })) - // name of folders are file names - for (const file of folderFiles) { - const path = join(this.buildPath, file) // out/Counter.sol/ - const compilationResult = { - input: {}, - output: { - contracts: {}, - sources: {} - }, - inputSources: { sources: {}, target: '' }, - solcVersion: null, - compilationTarget: null - } - compilationResult.inputSources.target = file - await this.readContract(path, compilationResult, cache) - this.emit('compilationFinished', compilationResult.compilationTarget, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) - } - - clearTimeout(this.logTimeout) - this.logTimeout = setTimeout(() => { - // @ts-ignore - this.call('terminal', 'log', { type: 'log', value: `receiving compilation result from Foundry. Select a file to populate the contract interaction interface.` }) - console.log('Syncing compilation result from Foundry') - }, 1000) - - } catch (e) { - console.log(e) - } - } - - async triggerProcessArtifact() { - // prevent multiple calls - clearTimeout(this.processingTimeout) - this.processingTimeout = setTimeout(async () => await this.processArtifact(), 1000) - } - - listenOnFoundryCompilation() { - try { - if (this.watcher) this.watcher.close() - this.watcher = chokidar.watch(this.cachePath, { depth: 0, ignorePermissionErrors: true, ignoreInitial: true }) - this.watcher.on('change', async () => await this.triggerProcessArtifact()) - this.watcher.on('add', async () => await this.triggerProcessArtifact()) - // process the artifact on activation - this.triggerProcessArtifact() - } catch (e) { - console.log(e) - } - } - - async readContract(contractFolder, compilationResultPart, cache) { - const files = await fs.promises.readdir(contractFolder) - for (const file of files) { - const path = join(contractFolder, file) - const content = await fs.promises.readFile(path, { encoding: 'utf-8' }) - compilationResultPart.inputSources.sources[file] = { content } - await this.feedContractArtifactFile(file, content, compilationResultPart, cache) - } - } - - async feedContractArtifactFile(path, content, compilationResultPart, cache) { - const contentJSON = JSON.parse(content) - const contractName = basename(path).replace('.json', '') - - let sourcePath = '' - if (contentJSON?.metadata?.settings?.compilationTarget) { - for (const key in contentJSON.metadata.settings.compilationTarget) { - if (contentJSON.metadata.settings.compilationTarget[key] === contractName) { - sourcePath = key - break - } - } - } - - if (!sourcePath) return - - const currentCache = cache.files[sourcePath] - if (!currentCache.artifacts[contractName]) return - - // extract source and version - const metadata = contentJSON.metadata - if (metadata.compiler && metadata.compiler.version) { - compilationResultPart.solcVersion = metadata.compiler.version - } else { - compilationResultPart.solcVersion = '' - console.log('\x1b[32m%s\x1b[0m', 'compiler version not found, please update Foundry to the latest version.') - } - - if (metadata.sources) { - for (const path in metadata.sources) { - const absPath = utils.absolutePath(path, this.currentSharedFolder) - try { - const content = await fs.promises.readFile(absPath, { encoding: 'utf-8' }) - compilationResultPart.input[path] = { content } - } catch (e) { - compilationResultPart.input[path] = { content: '' } - } - } - } else { - console.log('\x1b[32m%s\x1b[0m', 'sources input not found, please update Foundry to the latest version.') - } - - compilationResultPart.compilationTarget = sourcePath - // extract data - if (!compilationResultPart.output['sources'][sourcePath]) compilationResultPart.output['sources'][sourcePath] = {} - compilationResultPart.output['sources'][sourcePath] = { - ast: contentJSON['ast'], - id: contentJSON['id'] - } - if (!compilationResultPart.output['contracts'][sourcePath]) compilationResultPart.output['contracts'][sourcePath] = {} - - contentJSON.bytecode.object = contentJSON.bytecode.object.replace('0x', '') - contentJSON.deployedBytecode.object = contentJSON.deployedBytecode.object.replace('0x', '') - compilationResultPart.output['contracts'][sourcePath][contractName] = { - abi: contentJSON.abi, - evm: { - bytecode: contentJSON.bytecode, - deployedBytecode: contentJSON.deployedBytecode, - methodIdentifiers: contentJSON.methodIdentifiers - } - } - } - - async sync() { - console.log('syncing Foundry with Remix...') - this.processArtifact() - } -} \ No newline at end of file diff --git a/apps/remixdesktop/src/lib/remixd.ts b/apps/remixdesktop/src/lib/remixd.ts index 4315da2880..ea3c2afd2b 100644 --- a/apps/remixdesktop/src/lib/remixd.ts +++ b/apps/remixdesktop/src/lib/remixd.ts @@ -28,6 +28,7 @@ export class ElectronBasePluginRemixdClient extends ElectronBasePluginClient { this.onload(async () => { this.on('fs' as any, 'workingDirChanged', async (path: string) => { + console.log('workingDirChanged base remixd', path) this.currentSharedFolder = path }) this.currentSharedFolder = await this.call('fs' as any, 'getWorkingDir') diff --git a/apps/remixdesktop/src/lib/slither.ts b/apps/remixdesktop/src/lib/slither.ts deleted file mode 100644 index 3ff55f80dd..0000000000 --- a/apps/remixdesktop/src/lib/slither.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { existsSync, readFileSync, readdirSync, unlinkSync } from 'fs' -import * as utils from './utils' -const { spawn, execSync } = require('child_process') // eslint-disable-line -export interface OutputStandard { - description: string - title: string - confidence: string - severity: string - sourceMap: any - category?: string - reference?: string - example?: any - [key: string]: any -} - -export const SlitherClientMixin = (Base) => class extends Base { - methods: Array - currentSharedFolder: string - - constructor(...args: any[]) { - super(...args); // Ensure the parent constructor is called - } - - - log(...message: any) { - if (this.log) { - this.log(...message) - } else { - console.log(...message) - } - } - - error(...message: any) { - if (this.error) { - this.error(...message) - } else { - console.error(...message) - } - } - - - mapNpmDepsDir(list) { - const remixNpmDepsPath = utils.absolutePath('.deps/npm', this.currentSharedFolder) - const localNpmDepsPath = utils.absolutePath('node_modules', this.currentSharedFolder) - 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) => { - 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) - this.log(`[Slither Analysis]: Compiler version is ${versionString}`) - let solcOutput: Buffer - // Check solc current installed version - try { - solcOutput = execSync('solc --version', options) - } catch (err) { - this.error(err) - reject(new Error('Error in running solc command')) - } - if (!solcOutput.toString().includes(versionString)) { - this.log('[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)) { - this.log(`[Slither Analysis]: Installing ${version} using solc-select`) - // Install required version - execSync(`solc-select install ${version}`, options) - } - this.log(`[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) { - this.error(err) - reject(new Error('Error in running solc-select command')) - } - } else this.log('[Slither Analysis]: Compiler version is same as installed solc version') - } - // Allow paths and set solc remapping for import URLs - const fileContent = readFileSync(utils.absolutePath(filePath, this.currentSharedFolder), 'utf8') - const importsArr = fileContent.match(/import ['"][^.|..](.+?)['"];/g) - let remaps = '' - if (importsArr?.length) { - const { remapString } = this.mapNpmDepsDir(importsArr) - remaps = remapString.trim() - } - const optimizeOption: string = optimize ? '--optimize' : '' - const evmOption: string = evmVersion ? `--evm-version ${evmVersion}` : '' - let solcArgs = '' - if (optimizeOption) { - solcArgs += optimizeOption + ' ' - } - if (evmOption) { - if (!solcArgs.endsWith(' ')) solcArgs += ' ' - solcArgs += evmOption - } - if (solcArgs) { - solcArgs = `--solc-args "${solcArgs.trimStart()}"` - } - const solcRemaps = remaps ? `--solc-remaps "${remaps}"` : '' - - const outputFile = 'remix-slither-report.json' - try { - // We don't keep the previous analysis - const outputFilePath = utils.absolutePath(outputFile, this.currentSharedFolder) - if (existsSync(outputFilePath)) unlinkSync(outputFilePath) - } catch (e) { - this.error('unable to remove the output file') - this.error(e.message) - } - const cmd = `slither ${filePath} ${solcArgs} ${solcRemaps} --json ${outputFile}` - this.log('[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 = utils.absolutePath(outputFile, this.currentSharedFolder) - // 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) - } - - resolve(response) - } else { - this.log(report['error']) - reject(new Error('Error in running Slither Analysis.')) - } - } else { - this.error('Error in generating Slither Analysis Report. Make sure Slither is properly installed.') - reject(new Error('Error in generating Slither Analysis Report. Make sure Slither is properly installed.')) - } - }) - }) - } -} diff --git a/apps/remixdesktop/src/lib/utils.ts b/apps/remixdesktop/src/lib/utils.ts index e406b647b9..1520915d8b 100644 --- a/apps/remixdesktop/src/lib/utils.ts +++ b/apps/remixdesktop/src/lib/utils.ts @@ -19,6 +19,6 @@ function normalizePath (path) { return path } -export { absolutePath } +export { absolutePath, normalizePath } diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index 0b3411ceae..1eacb81cab 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -1,35 +1,247 @@ import { Profile } from "@remixproject/plugin-utils"; import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" - +import chokidar from 'chokidar' import { ElectronBasePluginRemixdClient } from "../lib/remixd" +import fs from 'fs' +import * as utils from '../lib/utils' -import { FoundryClientMixin } from "../lib/foundry"; +import { basename, join } from "path"; +import { spawn } from "child_process"; const profile: Profile = { - name: 'slither', - displayName: 'electron slither', - description: 'electron slither', + name: 'foundry', + displayName: 'electron foundry', + description: 'electron foundry', } export class FoundryPlugin extends ElectronBasePlugin { - clients: any [] - constructor() { - super(profile, clientProfile, FoundryClientMixin(FoundryPluginClient)) - this.methods = [...super.methods] - } + clients: any[] + constructor() { + super(profile, clientProfile, FoundryPluginClient) + this.methods = [...super.methods] + } } const clientProfile: Profile = { - name: 'foundry', - displayName: 'electron foundry', - description: 'electron foundry', - methods: ['sync', 'compile'] + name: 'foundry', + displayName: 'electron foundry', + description: 'electron foundry', + methods: ['sync', 'compile'] } class FoundryPluginClient extends ElectronBasePluginRemixdClient { - constructor(webContentsId: number, profile: Profile) { - super(webContentsId, profile); - } + + watcher: chokidar.FSWatcher + warnlog: boolean + buildPath: string + cachePath: string + logTimeout: NodeJS.Timeout + processingTimeout: NodeJS.Timeout + + async onActivation(): Promise { + console.log('Foundry plugin activated') + this.call('terminal', 'log', { type: 'log', value: 'Foundry plugin activated' }) + this.startListening() + this.on('fs' as any, 'workingDirChanged', async (path: string) => { + console.log('workingDirChanged foundry', path) + this.currentSharedFolder = path + this.startListening() + }) + this.currentSharedFolder = await this.call('fs' as any, 'getWorkingDir') + } + + startListening() { + this.buildPath = utils.absolutePath('out', this.currentSharedFolder) + this.cachePath = utils.absolutePath('cache', this.currentSharedFolder) + console.log('Foundry plugin checking for', this.buildPath, this.cachePath) + if (fs.existsSync(this.buildPath) && fs.existsSync(this.cachePath)) { + this.listenOnFoundryCompilation() + } else { + this.listenOnFoundryFolder() + } + } + + listenOnFoundryFolder() { + console.log('Foundry out folder doesn\'t exist... waiting for the compilation.') + try { + if (this.watcher) this.watcher.close() + this.watcher = chokidar.watch(this.currentSharedFolder, { depth: 1, ignorePermissionErrors: true, ignoreInitial: true }) + // watch for new folders + this.watcher.on('addDir', (path: string) => { + console.log('add dir foundry', path) + if (fs.existsSync(this.buildPath) && fs.existsSync(this.cachePath)) { + this.listenOnFoundryCompilation() + } + }) + } catch (e) { + console.log(e) + } + } + + compile() { + return new Promise((resolve, reject) => { + const cmd = `forge build` + const options = { cwd: this.currentSharedFolder, shell: true } + const child = spawn(cmd, options) + let result = '' + let error = '' + child.stdout.on('data', (data) => { + const msg = `[Foundry Compilation]: ${data.toString()}` + console.log('\x1b[32m%s\x1b[0m', msg) + result += msg + '\n' + }) + child.stderr.on('data', (err) => { + error += `[Foundry Compilation]: ${err.toString()} \n` + }) + child.on('close', () => { + if (error && result) resolve(error + result) + else if (error) reject(error) + else resolve(result) + }) + }) + } + + checkPath() { + if (!fs.existsSync(this.buildPath) || !fs.existsSync(this.cachePath)) { + this.listenOnFoundryFolder() + return false + } + if (!fs.existsSync(join(this.cachePath, 'solidity-files-cache.json'))) return false + return true + } + + private async processArtifact() { + if (!this.checkPath()) return + const folderFiles = await fs.promises.readdir(this.buildPath) // "out" folder + try { + const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' })) + // name of folders are file names + for (const file of folderFiles) { + const path = join(this.buildPath, file) // out/Counter.sol/ + const compilationResult = { + input: {}, + output: { + contracts: {}, + sources: {} + }, + inputSources: { sources: {}, target: '' }, + solcVersion: null, + compilationTarget: null + } + compilationResult.inputSources.target = file + await this.readContract(path, compilationResult, cache) + this.emit('compilationFinished', compilationResult.compilationTarget, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) + } + + clearTimeout(this.logTimeout) + this.logTimeout = setTimeout(() => { + // @ts-ignore + this.call('terminal', 'log', { type: 'log', value: `receiving compilation result from Foundry. Select a file to populate the contract interaction interface.` }) + console.log('Syncing compilation result from Foundry') + }, 1000) + + } catch (e) { + console.log(e) + } + } + + async triggerProcessArtifact() { + // prevent multiple calls + clearTimeout(this.processingTimeout) + this.processingTimeout = setTimeout(async () => await this.processArtifact(), 1000) + } + + listenOnFoundryCompilation() { + try { + console.log('Foundry out folder exists... processing the artifact.') + if (this.watcher) this.watcher.close() + this.watcher = chokidar.watch(this.cachePath, { depth: 0, ignorePermissionErrors: true, ignoreInitial: true }) + this.watcher.on('change', async () => await this.triggerProcessArtifact()) + this.watcher.on('add', async () => await this.triggerProcessArtifact()) + // process the artifact on activation + this.triggerProcessArtifact() + } catch (e) { + console.log(e) + } + } + + async readContract(contractFolder, compilationResultPart, cache) { + const files = await fs.promises.readdir(contractFolder) + for (const file of files) { + const path = join(contractFolder, file) + const content = await fs.promises.readFile(path, { encoding: 'utf-8' }) + compilationResultPart.inputSources.sources[file] = { content } + await this.feedContractArtifactFile(file, content, compilationResultPart, cache) + } + } + + async feedContractArtifactFile(path, content, compilationResultPart, cache) { + const contentJSON = JSON.parse(content) + const contractName = basename(path).replace('.json', '') + + let sourcePath = '' + if (contentJSON?.metadata?.settings?.compilationTarget) { + for (const key in contentJSON.metadata.settings.compilationTarget) { + if (contentJSON.metadata.settings.compilationTarget[key] === contractName) { + sourcePath = key + break + } + } + } + + if (!sourcePath) return + + const currentCache = cache.files[sourcePath] + if (!currentCache.artifacts[contractName]) return + + // extract source and version + const metadata = contentJSON.metadata + if (metadata.compiler && metadata.compiler.version) { + compilationResultPart.solcVersion = metadata.compiler.version + } else { + compilationResultPart.solcVersion = '' + console.log('\x1b[32m%s\x1b[0m', 'compiler version not found, please update Foundry to the latest version.') + } + + if (metadata.sources) { + for (const path in metadata.sources) { + const absPath = utils.absolutePath(path, this.currentSharedFolder) + try { + const content = await fs.promises.readFile(absPath, { encoding: 'utf-8' }) + compilationResultPart.input[path] = { content } + } catch (e) { + compilationResultPart.input[path] = { content: '' } + } + } + } else { + console.log('\x1b[32m%s\x1b[0m', 'sources input not found, please update Foundry to the latest version.') + } + + compilationResultPart.compilationTarget = sourcePath + // extract data + if (!compilationResultPart.output['sources'][sourcePath]) compilationResultPart.output['sources'][sourcePath] = {} + compilationResultPart.output['sources'][sourcePath] = { + ast: contentJSON['ast'], + id: contentJSON['id'] + } + if (!compilationResultPart.output['contracts'][sourcePath]) compilationResultPart.output['contracts'][sourcePath] = {} + + contentJSON.bytecode.object = contentJSON.bytecode.object.replace('0x', '') + contentJSON.deployedBytecode.object = contentJSON.deployedBytecode.object.replace('0x', '') + compilationResultPart.output['contracts'][sourcePath][contractName] = { + abi: contentJSON.abi, + evm: { + bytecode: contentJSON.bytecode, + deployedBytecode: contentJSON.deployedBytecode, + methodIdentifiers: contentJSON.methodIdentifiers + } + } + } + + async sync() { + console.log('syncing Foundry with Remix...') + this.processArtifact() + } } diff --git a/apps/remixdesktop/src/plugins/hardhatPlugin.ts b/apps/remixdesktop/src/plugins/hardhatPlugin.ts new file mode 100644 index 0000000000..2fae8844db --- /dev/null +++ b/apps/remixdesktop/src/plugins/hardhatPlugin.ts @@ -0,0 +1,219 @@ +import { Profile } from "@remixproject/plugin-utils"; +import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" +import chokidar from 'chokidar' +import { ElectronBasePluginRemixdClient } from "../lib/remixd" +import fs from 'fs' +import * as utils from '../lib/utils' + +import { basename, join } from "path"; +import { spawn } from "child_process"; +const profile: Profile = { + name: 'hardhat', + displayName: 'electron slither', + description: 'electron slither', +} + +export class HardhatPlugin extends ElectronBasePlugin { + clients: any[] + constructor() { + super(profile, clientProfile, HardhatPluginClient) + this.methods = [...super.methods] + } +} + +const clientProfile: Profile = { + name: 'hardhat', + displayName: 'electron hardhat', + description: 'electron hardhat', + methods: ['sync', 'compile'] +} + + +class HardhatPluginClient extends ElectronBasePluginRemixdClient { + watcher: chokidar.FSWatcher + warnlog: boolean + buildPath: string + cachePath: string + logTimeout: NodeJS.Timeout + processingTimeout: NodeJS.Timeout + + async onActivation(): Promise { + console.log('Hardhat plugin activated') + this.call('terminal', 'log', { type: 'log', value: 'Hardhat plugin activated' }) + this.startListening() + this.on('fs' as any, 'workingDirChanged', async (path: string) => { + console.log('workingDirChanged hardhat', path) + this.currentSharedFolder = path + this.startListening() + }) + this.currentSharedFolder = await this.call('fs' as any, 'getWorkingDir') + } + + startListening() { + this.buildPath = utils.absolutePath('artifacts/contracts', this.currentSharedFolder) + if (fs.existsSync(this.buildPath)) { + this.listenOnHardhatCompilation() + } else { + console.log('If you are using Hardhat, run `npx hardhat compile` or run the compilation with `Enable Hardhat Compilation` checked from the Remix IDE.') + this.listenOnHardHatFolder() + } + } + + compile(configPath: string) { + return new Promise((resolve, reject) => { + const cmd = `npx hardhat compile --config ${utils.normalizePath(configPath)}` + const options = { cwd: this.currentSharedFolder, shell: true } + const child = spawn(cmd, options) + let result = '' + let error = '' + child.stdout.on('data', (data) => { + const msg = `[Hardhat Compilation]: ${data.toString()}` + console.log('\x1b[32m%s\x1b[0m', msg) + result += msg + '\n' + }) + child.stderr.on('data', (err) => { + error += `[Hardhat Compilation]: ${err.toString()} \n` + }) + child.on('close', () => { + if (error && result) resolve(error + result) + else if (error) reject(error) + else resolve(result) + }) + }) + } + + checkPath() { + if (!fs.existsSync(this.buildPath)) { + this.listenOnHardHatFolder() + return false + } + return true + } + + private async processArtifact() { + console.log('processing artifact') + if (!this.checkPath()) return + // resolving the files + const folderFiles = await fs.promises.readdir(this.buildPath) + const targetsSynced = [] + // name of folders are file names + for (const file of folderFiles) { // ["artifacts/contracts/Greeter.sol/"] + const contractFilePath = join(this.buildPath, file) + const stat = await fs.promises.stat(contractFilePath) + if (!stat.isDirectory()) continue + const files = await fs.promises.readdir(contractFilePath) + const compilationResult = { + input: {}, + output: { + contracts: {}, + sources: {} + }, + solcVersion: null, + target: null + } + for (const file of files) { + if (file.endsWith('.dbg.json')) { // "artifacts/contracts/Greeter.sol/Greeter.dbg.json" + const stdFile = file.replace('.dbg.json', '.json') + const contentStd = await fs.promises.readFile(join(contractFilePath, stdFile), { encoding: 'utf-8' }) + const contentDbg = await fs.promises.readFile(join(contractFilePath, file), { encoding: 'utf-8' }) + const jsonDbg = JSON.parse(contentDbg) + const jsonStd = JSON.parse(contentStd) + compilationResult.target = jsonStd.sourceName + + targetsSynced.push(compilationResult.target) + const path = join(contractFilePath, jsonDbg.buildInfo) + const content = await fs.promises.readFile(path, { encoding: 'utf-8' }) + + await this.feedContractArtifactFile(content, compilationResult) + } + if (compilationResult.target) { + // we are only interested in the contracts that are in the target of the compilation + compilationResult.output = { + ...compilationResult.output, + contracts: { [compilationResult.target]: compilationResult.output.contracts[compilationResult.target] } + } + this.emit('compilationFinished', compilationResult.target, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) + } + } + } + + clearTimeout(this.logTimeout) + this.logTimeout = setTimeout(() => { + this.call('terminal', 'log', { value: 'receiving compilation result from Hardhat. Select a file to populate the contract interaction interface.', type: 'log' }) + if (targetsSynced.length) { + console.log(`Processing artifacts for files: ${[...new Set(targetsSynced)].join(', ')}`) + // @ts-ignore + this.call('terminal', 'log', { type: 'log', value: `synced with Hardhat: ${[...new Set(targetsSynced)].join(', ')}` }) + } else { + console.log('No artifacts to process') + // @ts-ignore + this.call('terminal', 'log', { type: 'log', value: 'No artifacts from Hardhat to process' }) + } + }, 1000) + + } + + listenOnHardHatFolder() { + console.log('Hardhat artifacts folder doesn\'t exist... waiting for the compilation.') + try { + if (this.watcher) this.watcher.close() + this.watcher = chokidar.watch(this.currentSharedFolder, { depth: 2, ignorePermissionErrors: true, ignoreInitial: true }) + // watch for new folders + this.watcher.on('addDir', (path: string) => { + console.log('add dir hardhat', path) + if (fs.existsSync(this.buildPath)) { + this.listenOnHardhatCompilation() + } + }) + } catch (e) { + console.log('listenOnHardHatFolder', e) + } + } + + async triggerProcessArtifact() { + console.log('triggerProcessArtifact') + // prevent multiple calls + clearTimeout(this.processingTimeout) + this.processingTimeout = setTimeout(async () => await this.processArtifact(), 1000) + } + + listenOnHardhatCompilation() { + try { + console.log('listening on Hardhat compilation...', this.buildPath) + if (this.watcher) this.watcher.close() + this.watcher = chokidar.watch(this.buildPath, { depth: 1, ignorePermissionErrors: true, ignoreInitial: true }) + this.watcher.on('change', async () => await this.triggerProcessArtifact()) + this.watcher.on('add', async () => await this.triggerProcessArtifact()) + this.watcher.on('unlink', async () => await this.triggerProcessArtifact()) + // process the artifact on activation + this.processArtifact() + } catch (e) { + console.log('listenOnHardhatCompilation', e) + } + } + + async sync() { + console.log('syncing from Hardhat') + this.processArtifact() + } + + async feedContractArtifactFile(artifactContent, compilationResultPart) { + const contentJSON = JSON.parse(artifactContent) + compilationResultPart.solcVersion = contentJSON.solcVersion + for (const file in contentJSON.input.sources) { + const source = contentJSON.input.sources[file] + const absPath = join(this.currentSharedFolder, file) + if (fs.existsSync(absPath)) { // if not that is a lib + const contentOnDisk = await fs.promises.readFile(absPath, { encoding: 'utf-8' }) + if (contentOnDisk === source.content) { + compilationResultPart.input[file] = source + compilationResultPart.output['sources'][file] = contentJSON.output.sources[file] + compilationResultPart.output['contracts'][file] = contentJSON.output.contracts[file] + if (contentJSON.output.errors && contentJSON.output.errors.length) { + compilationResultPart.output['errors'] = contentJSON.output.errors.filter(error => error.sourceLocation.file === file) + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/remixdesktop/src/plugins/slitherPlugin.ts b/apps/remixdesktop/src/plugins/slitherPlugin.ts index 5819e0a46b..abf7971e30 100644 --- a/apps/remixdesktop/src/plugins/slitherPlugin.ts +++ b/apps/remixdesktop/src/plugins/slitherPlugin.ts @@ -2,7 +2,22 @@ import { Profile } from "@remixproject/plugin-utils"; import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" import { ElectronBasePluginRemixdClient } from "../lib/remixd" -import { SlitherClientMixin } from "../lib/slither"; +import * as utils from '../lib/utils' +import { existsSync, readdirSync, readFileSync, unlinkSync } from "fs-extra"; + +export interface OutputStandard { + description: string + title: string + confidence: string + severity: string + sourceMap: any + category?: string + reference?: string + example?: any + [key: string]: any +} + +const { spawn, execSync } = require('child_process') // eslint-disable-line const profile: Profile = { name: 'slither', displayName: 'electron slither', @@ -10,9 +25,9 @@ const profile: Profile = { } export class SlitherPlugin extends ElectronBasePlugin { - clients: any [] + clients: any[] constructor() { - super(profile, clientProfile, SlitherClientMixin(SlitherPluginClient)) + super(profile, clientProfile, SlitherPluginClient) this.methods = [...super.methods] } } @@ -24,10 +39,158 @@ const clientProfile: Profile = { methods: ['analyse'] } - class SlitherPluginClient extends ElectronBasePluginRemixdClient { - constructor(webContentsId: number, profile: Profile) { - super(webContentsId, profile); + + mapNpmDepsDir(list) { + const remixNpmDepsPath = utils.absolutePath('.deps/npm', this.currentSharedFolder) + const localNpmDepsPath = utils.absolutePath('node_modules', this.currentSharedFolder) + 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) => { + 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) + this.log(`[Slither Analysis]: Compiler version is ${versionString}`) + let solcOutput: Buffer + // Check solc current installed version + try { + solcOutput = execSync('solc --version', options) + } catch (err) { + this.error(err) + reject(new Error('Error in running solc command')) + } + if (!solcOutput.toString().includes(versionString)) { + this.log('[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)) { + this.log(`[Slither Analysis]: Installing ${version} using solc-select`) + // Install required version + execSync(`solc-select install ${version}`, options) + } + this.log(`[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) { + this.error(err) + reject(new Error('Error in running solc-select command')) + } + } else this.log('[Slither Analysis]: Compiler version is same as installed solc version') + } + // Allow paths and set solc remapping for import URLs + const fileContent = readFileSync(utils.absolutePath(filePath, this.currentSharedFolder), 'utf8') + const importsArr = fileContent.match(/import ['"][^.|..](.+?)['"];/g) + let remaps = '' + if (importsArr?.length) { + const { remapString } = this.mapNpmDepsDir(importsArr) + remaps = remapString.trim() + } + const optimizeOption: string = optimize ? '--optimize' : '' + const evmOption: string = evmVersion ? `--evm-version ${evmVersion}` : '' + let solcArgs = '' + if (optimizeOption) { + solcArgs += optimizeOption + ' ' + } + if (evmOption) { + if (!solcArgs.endsWith(' ')) solcArgs += ' ' + solcArgs += evmOption + } + if (solcArgs) { + solcArgs = `--solc-args "${solcArgs.trimStart()}"` + } + const solcRemaps = remaps ? `--solc-remaps "${remaps}"` : '' + + const outputFile = 'remix-slither-report.json' + try { + // We don't keep the previous analysis + const outputFilePath = utils.absolutePath(outputFile, this.currentSharedFolder) + if (existsSync(outputFilePath)) unlinkSync(outputFilePath) + } catch (e) { + this.error('unable to remove the output file') + this.error(e.message) + } + const cmd = `slither ${filePath} ${solcArgs} ${solcRemaps} --json ${outputFile}` + this.log('[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 = utils.absolutePath(outputFile, this.currentSharedFolder) + // 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) + } + + resolve(response) + } else { + this.log(report['error']) + reject(new Error('Error in running Slither Analysis.')) + } + } else { + this.error('Error in generating Slither Analysis Report. Make sure Slither is properly installed.') + reject(new Error('Error in generating Slither Analysis Report. Make sure Slither is properly installed.')) + } + }) + }) } } diff --git a/apps/remixdesktop/src/preload.ts b/apps/remixdesktop/src/preload.ts index 6c5c2f1fc9..9f757541c1 100644 --- a/apps/remixdesktop/src/preload.ts +++ b/apps/remixdesktop/src/preload.ts @@ -6,7 +6,7 @@ console.log('preload.ts', new Date().toLocaleTimeString()) /* preload script needs statically defined API for each plugin */ -const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig', 'electronTemplates', 'ripgrep', 'compilerloader', 'appUpdater', 'slither'] +const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig', 'electronTemplates', 'ripgrep', 'compilerloader', 'appUpdater', 'slither', 'foundry', 'hardhat'] let webContentsId: number | undefined From 1c04cc41f58e838af70906c151a9e71a14034c4b Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Wed, 3 Jul 2024 15:04:56 +0200 Subject: [PATCH 3/9] compiler for hardhat --- apps/remix-ide/src/app/tabs/compile-tab.js | 6 +++++- apps/solidity-compiler/src/app/compiler.ts | 4 ++++ libs/remix-lib/src/types/ICompilerApi.ts | 1 + .../src/lib/logic/compileTabLogic.ts | 9 +++++---- .../solidity-compiler/src/lib/solidity-compiler.tsx | 13 +++++++++---- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/remix-ide/src/app/tabs/compile-tab.js b/apps/remix-ide/src/app/tabs/compile-tab.js index e3505358af..1fb7dea231 100644 --- a/apps/remix-ide/src/app/tabs/compile-tab.js +++ b/apps/remix-ide/src/app/tabs/compile-tab.js @@ -9,7 +9,7 @@ import { QueryParams } from '@remix-project/remix-lib' import * as packageJson from '../../../../../package.json' import { compilerConfigChangedToastMsg, compileToastMsg } from '@remix-ui/helper' import { isNative } from '../../remixAppManager' - +import { Registry } from '@remix-project/remix-lib' const profile = { name: 'solidity', displayName: 'Solidity compiler', @@ -90,6 +90,10 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA return this.fileManager.mode } + isDesktop () { + return Registry.getInstance().get('platform').api.isDesktop() + } + /** * set the compiler configuration * This function is used by remix-plugin compiler API. diff --git a/apps/solidity-compiler/src/app/compiler.ts b/apps/solidity-compiler/src/app/compiler.ts index d86d5322cb..d20f8db8d8 100644 --- a/apps/solidity-compiler/src/app/compiler.ts +++ b/apps/solidity-compiler/src/app/compiler.ts @@ -55,4 +55,8 @@ export class CompilerClientApi extends CompilerApiMixin(PluginClient) implements getFileManagerMode () { return 'browser' } + + isDesktop() { + return false + } } diff --git a/libs/remix-lib/src/types/ICompilerApi.ts b/libs/remix-lib/src/types/ICompilerApi.ts index da8dc3b694..cfb4ce832f 100644 --- a/libs/remix-lib/src/types/ICompilerApi.ts +++ b/libs/remix-lib/src/types/ICompilerApi.ts @@ -18,6 +18,7 @@ export interface ICompilerApi { setAppParameter: (name: string, value: string | boolean) => void getFileManagerMode: () => string + isDesktop: () => boolean setCompilerConfig: (settings: any) => void getCompilationResult: () => any diff --git a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts index 49aad01d31..0c5800a46e 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts +++ b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts @@ -129,26 +129,27 @@ export class CompileTabLogic { } async isHardhatProject () { - if (this.api.getFileManagerMode() === 'localhost') { + if (this.api.getFileManagerMode() === ('localhost') || this.api.isDesktop()) { + console.log('checking hardhat project') return await this.api.fileExists('hardhat.config.js') || await this.api.fileExists('hardhat.config.ts') } else return false } async isTruffleProject () { - if (this.api.getFileManagerMode() === 'localhost') { + if (this.api.getFileManagerMode() === ('localhost') || this.api.isDesktop()) { return await this.api.fileExists('truffle-config.js') } else return false } async isFoundryProject () { - if (this.api.getFileManagerMode() === 'localhost') { + if (this.api.getFileManagerMode() === ('localhost') || this.api.isDesktop()) { return await this.api.fileExists('foundry.toml') } else return false } runCompiler (externalCompType) { try { - if (this.api.getFileManagerMode() === 'localhost') { + if (this.api.getFileManagerMode() === 'localhost' || this.api.isDesktop()) { if (externalCompType === 'hardhat') { const { currentVersion, optimize, runs } = this.compiler.state if (currentVersion) { diff --git a/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx b/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx index 46f13d3047..04908c35ec 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx +++ b/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' // eslint-disable-line +import React, { useContext, useEffect, useState } from 'react' // eslint-disable-line import { CompileErrors, ContractsFile, SolidityCompilerProps } from './types' import { CompilerContainer } from './compiler-container' // eslint-disable-line import { ContractSelection } from './contract-selection' // eslint-disable-line @@ -9,6 +9,7 @@ import { baseURLBin, baseURLWasm, pathToURL } from '@remix-project/remix-solidit import * as packageJson from '../../../../../package.json' import './css/style.css' import { iSolJsonBinData, iSolJsonBinDataBuild } from '@remix-project/remix-lib' +import { appPlatformTypes, platformContext } from '@remix-ui/app' export const SolidityCompiler = (props: SolidityCompilerProps) => { const { @@ -47,6 +48,7 @@ export const SolidityCompiler = (props: SolidityCompilerProps) => { const [compileErrors, setCompileErrors] = useState>({ [currentFile]: api.compileErrors }) const [badgeStatus, setBadgeStatus] = useState>({}) const [contractsFile, setContractsFile] = useState({}) + const platform = useContext(platformContext) useEffect(() => { ; (async () => { @@ -77,9 +79,12 @@ export const SolidityCompiler = (props: SolidityCompilerProps) => { } api.onSetWorkspace = async (isLocalhost: boolean, workspaceName: string) => { - const isHardhat = isLocalhost && (await compileTabLogic.isHardhatProject()) - const isTruffle = isLocalhost && (await compileTabLogic.isTruffleProject()) - const isFoundry = isLocalhost && (await compileTabLogic.isFoundryProject()) + const isDesktop = platform === appPlatformTypes.desktop + console.log('onSetWorkspace', workspaceName, isLocalhost, isDesktop, workspaceName) + const isHardhat = (isLocalhost || isDesktop) && (await compileTabLogic.isHardhatProject()) + const isTruffle = (isLocalhost || isDesktop) && (await compileTabLogic.isTruffleProject()) + const isFoundry = (isLocalhost || isDesktop) && (await compileTabLogic.isFoundryProject()) + console.log(isFoundry, isHardhat, isTruffle) setState((prevState) => { return { ...prevState, From 986ec1ece36c80711b3a91fbc16d407997edb62a Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Wed, 3 Jul 2024 15:38:22 +0200 Subject: [PATCH 4/9] fix or open folders --- .../remixdesktop/src/plugins/foundryPlugin.ts | 1 + apps/remixdesktop/src/plugins/fsPlugin.ts | 28 +++++++++++++------ .../test/tests/app/hardhat.test.ts | 0 3 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 apps/remixdesktop/test/tests/app/hardhat.test.ts diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index 1eacb81cab..140b826739 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -158,6 +158,7 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { this.watcher = chokidar.watch(this.cachePath, { depth: 0, ignorePermissionErrors: true, ignoreInitial: true }) this.watcher.on('change', async () => await this.triggerProcessArtifact()) this.watcher.on('add', async () => await this.triggerProcessArtifact()) + this.watcher.on('unlink', async () => await this.triggerProcessArtifact()) // process the artifact on activation this.triggerProcessArtifact() } catch (e) { diff --git a/apps/remixdesktop/src/plugins/fsPlugin.ts b/apps/remixdesktop/src/plugins/fsPlugin.ts index d01235de66..65210e9da5 100644 --- a/apps/remixdesktop/src/plugins/fsPlugin.ts +++ b/apps/remixdesktop/src/plugins/fsPlugin.ts @@ -32,6 +32,15 @@ const getBaseName = (pathName: string): string => { return path.basename(pathName) } +function onlyUnique(value: recentFolder, index: number, self: recentFolder[]) { + console.log(index, value) + return self.findIndex((rc, index) => rc.path === value.path) === index +} + +const deplucateFolderList = (list: recentFolder[]): recentFolder[] => { + return list.filter(onlyUnique) +} + export class FSPlugin extends ElectronBasePlugin { clients: FSPluginClient[] = [] constructor() { @@ -40,28 +49,28 @@ export class FSPlugin extends ElectronBasePlugin { } async onActivation(): Promise { - const config = await this.call('electronconfig' as any, 'readConfig') + const config = await this.call('electronconfig', 'readConfig') const openedFolders = (config && config.openedFolders) || [] - const recentFolders = (config && config.recentFolders) || [] + const recentFolders: recentFolder[] = (config && config.recentFolders) || [] this.call('electronconfig', 'writeConfig', {...config, - recentFolders: recentFolders, + recentFolders: deplucateFolderList(recentFolders), openedFolders: openedFolders}) const foldersToDelete: string[] = [] - if (openedFolders && openedFolders.length) { - for (const folder of openedFolders) { + if (recentFolders && recentFolders.length) { + for (const folder of recentFolders) { try { - const stat = await fs.stat(folder) + const stat = await fs.stat(folder.path); if (stat.isDirectory()) { // do nothing } } catch (e) { console.log('error opening folder', folder, e) - foldersToDelete.push(folder) + foldersToDelete.push(folder.path) } } if (foldersToDelete.length) { - const newFolders = openedFolders.filter((f: string) => !foldersToDelete.includes(f)) - this.call('electronconfig', 'writeConfig', {recentFolders: newFolders}) + const newFolders = recentFolders.filter((f: recentFolder) => !foldersToDelete.includes(f.path)) + this.call('electronconfig', 'writeConfig', {recentFolders: deplucateFolderList(newFolders)}) } } createWindow() @@ -346,6 +355,7 @@ class FSPluginClient extends ElectronBasePluginClient { path, timestamp, }) + config.recentFolders = deplucateFolderList(config.recentFolders) writeConfig(config) } diff --git a/apps/remixdesktop/test/tests/app/hardhat.test.ts b/apps/remixdesktop/test/tests/app/hardhat.test.ts new file mode 100644 index 0000000000..e69de29bb2 From 3c537bdcbd3c8d4c24a81154a0742a7e0af5d2e0 Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Fri, 5 Jul 2024 19:34:39 +0200 Subject: [PATCH 5/9] hardhat test --- apps/remixdesktop/src/engine.ts | 13 ++++ apps/remixdesktop/src/plugins/fsPlugin.ts | 7 ++ apps/remixdesktop/src/preload.ts | 3 +- .../test/tests/app/hardhat.test.ts | 73 +++++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/apps/remixdesktop/src/engine.ts b/apps/remixdesktop/src/engine.ts index 9a8fd78aad..534cdf850c 100644 --- a/apps/remixdesktop/src/engine.ts +++ b/apps/remixdesktop/src/engine.ts @@ -13,6 +13,7 @@ import { SlitherPlugin } from './plugins/slitherPlugin'; import { AppUpdaterPlugin } from './plugins/appUpdater'; import { FoundryPlugin } from './plugins/foundryPlugin'; import { HardhatPlugin } from './plugins/hardhatPlugin'; +import { isE2E } from './main'; const engine = new Engine() const appManager = new PluginManager() @@ -52,6 +53,18 @@ ipcMain.on('fs:openFolder', async (event, path?) => { fsPlugin.openFolder(event, path) }) +ipcMain.handle('fs:openFolder', async (event, webContentsId, path?) => { + if(!isE2E) return + console.log('openFolder', webContentsId, path) + fsPlugin.openFolder(webContentsId, path) +}) + +ipcMain.handle('fs:openFolderInSameWindow', async (event, webContentsId, path?) => { + if(!isE2E) return + console.log('openFolderInSameWindow', webContentsId, path) + fsPlugin.openFolderInSameWindow(webContentsId, path) +}) + ipcMain.on('terminal:new', async (event) => { xtermPlugin.new(event) diff --git a/apps/remixdesktop/src/plugins/fsPlugin.ts b/apps/remixdesktop/src/plugins/fsPlugin.ts index 65210e9da5..0f45727066 100644 --- a/apps/remixdesktop/src/plugins/fsPlugin.ts +++ b/apps/remixdesktop/src/plugins/fsPlugin.ts @@ -94,6 +94,13 @@ export class FSPlugin extends ElectronBasePlugin { client.openFolder(path) } } + + openFolderInSameWindow(webContentsId: any, path?: string): void { + const client = this.clients.find((c) => c.webContentsId === webContentsId) + if (client) { + client.openFolderInSameWindow(path) + } + } } const clientProfile: Profile = { diff --git a/apps/remixdesktop/src/preload.ts b/apps/remixdesktop/src/preload.ts index 9f757541c1..356e262347 100644 --- a/apps/remixdesktop/src/preload.ts +++ b/apps/remixdesktop/src/preload.ts @@ -19,7 +19,8 @@ contextBridge.exposeInMainWorld('electronAPI', { isE2E: () => ipcRenderer.invoke('config:isE2E'), canTrackMatomo: () => ipcRenderer.invoke('config:canTrackMatomo'), trackEvent: (args: any[]) => ipcRenderer.invoke('matomo:trackEvent', args), - + openFolder: (path: string) => ipcRenderer.invoke('fs:openFolder', webContentsId, path), + openFolderInSameWindow: (path: string) => ipcRenderer.invoke('fs:openFolderInSameWindow', webContentsId, path), activatePlugin: (name: string) => { return ipcRenderer.invoke('manager:activatePlugin', name) }, diff --git a/apps/remixdesktop/test/tests/app/hardhat.test.ts b/apps/remixdesktop/test/tests/app/hardhat.test.ts index e69de29bb2..492d95700b 100644 --- a/apps/remixdesktop/test/tests/app/hardhat.test.ts +++ b/apps/remixdesktop/test/tests/app/hardhat.test.ts @@ -0,0 +1,73 @@ +import { NightwatchBrowser } from 'nightwatch' +import { ChildProcess, spawn, execSync } from 'child_process' +import { homedir } from 'os' +import path from 'path' +import os from 'os' + +const dir = path.join('remix-desktop-test-' + Date.now().toString()) + +const tests = { + before: function (browser: NightwatchBrowser, done: VoidFunction) { + done() + }, + setuphardhat: function (browser: NightwatchBrowser) { + browser.perform(async (done) => { + await setupHardhatProject() + done() + }) + }, + addScript: function (browser: NightwatchBrowser) { + // run script in console + browser.executeAsync(function (dir, done) { + (window as any).electronAPI.openFolderInSameWindow('/tmp/' + dir).then(done) + }, [dir], () => { + console.log('done window opened') + }) + .waitForElementVisible('*[data-id="treeViewDivDraggableItemhardhat.config.js"]', 10000) + }, + compile: function (browser: NightwatchBrowser) { + browser.perform(async (done) => { + console.log('generating compilation result') + await compileHardhatProject() + done() + }) + .expect.element('*[data-id="terminalJournal"]').text.to.contain('receiving compilation result from Hardhat').before(60000) + } +} + +async function compileHardhatProject(): Promise { + console.log(process.cwd()) + try { + const server = spawn('npx hardhat compile', [], { cwd: '/tmp/' + dir, shell: true, detached: true }) + return new Promise((resolve, reject) => { + server.on('exit', function (exitCode) { + console.log("Child exited with code: " + exitCode); + console.log('end') + resolve() + }) + }) + } catch (e) { + console.log(e) + } +} + +async function setupHardhatProject(): Promise { + console.log('setup hardhat project', dir) + try { + const server = spawn(`git clone https://github.com/NomicFoundation/hardhat-boilerplate ${dir} && cd ${dir} && yarn install && yarn add "@typechain/ethers-v5@^10.1.0" && yarn add "@typechain/hardhat@^6.1.2" && yarn add "typechain@^8.1.0" && echo "END"`, [], { cwd: '/tmp/', shell: true, detached: true }) + return new Promise((resolve, reject) => { + server.on('exit', function (exitCode) { + console.log("Child exited with code: " + exitCode); + console.log('end') + resolve() + }) + }) + } catch (e) { + console.log(e) + } +} + + +module.exports = { + ...tests +} \ No newline at end of file From 502107ac11062fcd8896aac1faf384db2475eee4 Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Sat, 6 Jul 2024 07:48:41 +0200 Subject: [PATCH 6/9] foundry test --- .../test/tests/app/foundry.test.ts | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 apps/remixdesktop/test/tests/app/foundry.test.ts diff --git a/apps/remixdesktop/test/tests/app/foundry.test.ts b/apps/remixdesktop/test/tests/app/foundry.test.ts new file mode 100644 index 0000000000..49004ccd50 --- /dev/null +++ b/apps/remixdesktop/test/tests/app/foundry.test.ts @@ -0,0 +1,132 @@ +import { NightwatchBrowser } from 'nightwatch' +import { ChildProcess, spawn, execSync } from 'child_process' +import { homedir } from 'os' +import path from 'path' +import os from 'os' + +const projectDir = path.join('remix-desktop-test-' + Date.now().toString()) +const dir = '/tmp/' + projectDir + +const tests = { + before: function (browser: NightwatchBrowser, done: VoidFunction) { + done() + }, + installFoundry: function (browser: NightwatchBrowser) { + browser.perform(async (done) => { + await downloadFoundry() + await installFoundry() + await initFoundryProject() + done() + }) + }, + addScript: function (browser: NightwatchBrowser) { + // run script in console + browser.executeAsync(function (dir, done) { + (window as any).electronAPI.openFolderInSameWindow(dir).then(done) + }, [dir], () => { + console.log('done window opened') + }) + .waitForElementVisible('*[data-id="treeViewDivDraggableItemhardhat.config.js"]', 10000) + }, + compile: function (browser: NightwatchBrowser) { + browser.perform(async (done) => { + console.log('generating compilation result') + await buildFoundryProject() + done() + }) + .expect.element('*[data-id="terminalJournal"]').text.to.contain('receiving compilation result from Foundry').before(60000) + } +} +async function downloadFoundry(): Promise { + console.log('downloadFoundry', process.cwd()) + try { + const server = spawn('curl -L https://foundry.paradigm.xyz | bash', [], { cwd: process.cwd(), shell: true, detached: true }) + return new Promise((resolve, reject) => { + server.stdout.on('data', function (data) { + console.log(data.toString()) + if ( + data.toString().includes("simply run 'foundryup' to install Foundry") + || data.toString().includes("foundryup: could not detect shell, manually add") + ) { + console.log('resolving') + resolve() + } + }) + server.stderr.on('err', function (data) { + console.log(data.toString()) + reject(data.toString()) + }) + }) + } catch (e) { + console.log(e) + } + } + + async function installFoundry(): Promise { + console.log('installFoundry', process.cwd()) + try { + const server = spawn('export PATH="' + homedir() + '/.foundry/bin:$PATH" && foundryup', [], { cwd: process.cwd(), shell: true, detached: true }) + return new Promise((resolve, reject) => { + server.stdout.on('data', function (data) { + console.log(data.toString()) + if ( + data.toString().includes("foundryup: done!") + ) { + console.log('resolving') + resolve() + } + }) + server.stderr.on('err', function (data) { + console.log(data.toString()) + reject(data.toString()) + }) + }) + } catch (e) { + console.log(e) + } + } + + async function initFoundryProject(): Promise { + console.log('initFoundryProject', homedir()) + try { + if(process.env.CIRCLECI) { + spawn('git config --global user.email \"you@example.com\"', [], { cwd: homedir(), shell: true, detached: true }) + spawn('git config --global user.name \"Your Name\"', [], { cwd: homedir(), shell: true, detached: true }) + } + spawn('mkdir ' + projectDir, [], { cwd: '/tmp/', shell: true, detached: true }) + const server = spawn('export PATH="' + homedir() + '/.foundry/bin:$PATH" && forge init hello_foundry', [], { cwd: dir, shell: true, detached: true }) + server.stdout.pipe(process.stdout) + return new Promise((resolve, reject) => { + server.on('exit', function (exitCode) { + console.log("Child exited with code: " + exitCode); + console.log('end') + resolve() + }) + }) + } catch (e) { + console.log(e) + } + } + + async function buildFoundryProject(): Promise { + console.log('buildFoundryProject', homedir()) + try { + const server = spawn('export PATH="' + homedir() + '/.foundry/bin:$PATH" && forge build', [], { cwd: dir + '/hello_foundry', shell: true, detached: true }) + server.stdout.pipe(process.stdout) + return new Promise((resolve, reject) => { + server.on('exit', function (exitCode) { + console.log("Child exited with code: " + exitCode); + console.log('end') + resolve() + }) + }) + } catch (e) { + console.log(e) + } + } + + + +module.exports = { + ...tests +} \ No newline at end of file From d4651da0e06f14a1ec3b4508a5efe7a2287793c8 Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Sat, 6 Jul 2024 08:01:55 +0200 Subject: [PATCH 7/9] foundry test --- .../test/tests/app/foundry.test.ts | 71 ++++++++++++------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/apps/remixdesktop/test/tests/app/foundry.test.ts b/apps/remixdesktop/test/tests/app/foundry.test.ts index 49004ccd50..ad4285375d 100644 --- a/apps/remixdesktop/test/tests/app/foundry.test.ts +++ b/apps/remixdesktop/test/tests/app/foundry.test.ts @@ -13,10 +13,10 @@ const tests = { }, installFoundry: function (browser: NightwatchBrowser) { browser.perform(async (done) => { - await downloadFoundry() - await installFoundry() - await initFoundryProject() - done() + await downloadFoundry() + await installFoundry() + await initFoundryProject() + done() }) }, addScript: function (browser: NightwatchBrowser) { @@ -26,7 +26,7 @@ const tests = { }, [dir], () => { console.log('done window opened') }) - .waitForElementVisible('*[data-id="treeViewDivDraggableItemhardhat.config.js"]', 10000) + .waitForElementVisible('*[data-id="treeViewDivDraggableItemfoundry.toml"]', 10000) }, compile: function (browser: NightwatchBrowser) { browser.perform(async (done) => { @@ -34,7 +34,26 @@ const tests = { await buildFoundryProject() done() }) - .expect.element('*[data-id="terminalJournal"]').text.to.contain('receiving compilation result from Foundry').before(60000) + .expect.element('*[data-id="terminalJournal"]').text.to.contain('receiving compilation result from Foundry').before(60000) + + let contractAaddress + browser.clickLaunchIcon('filePanel') + .openFile('src') + .openFile('src/Counter.sol') + .clickLaunchIcon('udapp') + .selectContract('Counter') + .createContract('') + .getAddressAtPosition(0, (address) => { + console.log(contractAaddress) + contractAaddress = address + }) + .clickInstance(0) + .clickFunction('increment - transact (not payable)') + .perform((done) => { + browser.testConstantFunction(contractAaddress, 'number - call', null, '0:\nuint256: 1').perform(() => { + done() + }) + }) } } async function downloadFoundry(): Promise { @@ -60,9 +79,9 @@ async function downloadFoundry(): Promise { } catch (e) { console.log(e) } - } - - async function installFoundry(): Promise { +} + +async function installFoundry(): Promise { console.log('installFoundry', process.cwd()) try { const server = spawn('export PATH="' + homedir() + '/.foundry/bin:$PATH" && foundryup', [], { cwd: process.cwd(), shell: true, detached: true }) @@ -84,12 +103,12 @@ async function downloadFoundry(): Promise { } catch (e) { console.log(e) } - } - - async function initFoundryProject(): Promise { +} + +async function initFoundryProject(): Promise { console.log('initFoundryProject', homedir()) - try { - if(process.env.CIRCLECI) { + try { + if (process.env.CIRCLECI) { spawn('git config --global user.email \"you@example.com\"', [], { cwd: homedir(), shell: true, detached: true }) spawn('git config --global user.name \"Your Name\"', [], { cwd: homedir(), shell: true, detached: true }) } @@ -98,35 +117,35 @@ async function downloadFoundry(): Promise { server.stdout.pipe(process.stdout) return new Promise((resolve, reject) => { server.on('exit', function (exitCode) { - console.log("Child exited with code: " + exitCode); - console.log('end') - resolve() + console.log("Child exited with code: " + exitCode); + console.log('end') + resolve() }) }) } catch (e) { console.log(e) } - } - - async function buildFoundryProject(): Promise { +} + +async function buildFoundryProject(): Promise { console.log('buildFoundryProject', homedir()) try { const server = spawn('export PATH="' + homedir() + '/.foundry/bin:$PATH" && forge build', [], { cwd: dir + '/hello_foundry', shell: true, detached: true }) server.stdout.pipe(process.stdout) return new Promise((resolve, reject) => { server.on('exit', function (exitCode) { - console.log("Child exited with code: " + exitCode); - console.log('end') - resolve() + console.log("Child exited with code: " + exitCode); + console.log('end') + resolve() }) }) } catch (e) { console.log(e) } - } - +} + module.exports = { - ...tests + ...process.platform.startsWith('linux') ? tests : {} } \ No newline at end of file From 355935d7cecd6df34edeee53db7b3c688a28aa2a Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Sat, 6 Jul 2024 08:04:11 +0200 Subject: [PATCH 8/9] HH test --- .../test/tests/app/hardhat.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/remixdesktop/test/tests/app/hardhat.test.ts b/apps/remixdesktop/test/tests/app/hardhat.test.ts index 492d95700b..f8c093102e 100644 --- a/apps/remixdesktop/test/tests/app/hardhat.test.ts +++ b/apps/remixdesktop/test/tests/app/hardhat.test.ts @@ -31,7 +31,24 @@ const tests = { await compileHardhatProject() done() }) - .expect.element('*[data-id="terminalJournal"]').text.to.contain('receiving compilation result from Hardhat').before(60000) + .expect.element('*[data-id="terminalJournal"]').text.to.contain('receiving compilation result from Hardhat').before(60000) + let addressRef + browser.clickLaunchIcon('filePanel') + .openFile('contracts') + .openFile('contracts/Token.sol') + .clickLaunchIcon('udapp') + .selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') + .selectContract('Token') + .createContract('') + .clickInstance(0) + .clickFunction('balanceOf - call', { types: 'address account', values: '0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c' }) + .getAddressAtPosition(0, (address) => { + addressRef = address + }) + .perform((done) => { + browser.verifyCallReturnValue(addressRef, ['0:uint256: 1000000']) + .perform(() => done()) + }) } } From 7bf1781f155261f8f846970801e261d66d185cbb Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Sat, 6 Jul 2024 08:21:45 +0200 Subject: [PATCH 9/9] foundry test --- apps/remixdesktop/test/tests/app/foundry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/remixdesktop/test/tests/app/foundry.test.ts b/apps/remixdesktop/test/tests/app/foundry.test.ts index ad4285375d..c218592428 100644 --- a/apps/remixdesktop/test/tests/app/foundry.test.ts +++ b/apps/remixdesktop/test/tests/app/foundry.test.ts @@ -22,7 +22,7 @@ const tests = { addScript: function (browser: NightwatchBrowser) { // run script in console browser.executeAsync(function (dir, done) { - (window as any).electronAPI.openFolderInSameWindow(dir).then(done) + (window as any).electronAPI.openFolderInSameWindow(dir + '/hello_foundry/').then(done) }, [dir], () => { console.log('done window opened') })