Merge pull request #4964 from ethereum/desktop-hh-foundry

Desktop hh foundry
pull/5370/head
bunsenstraat 5 months ago committed by GitHub
commit 248a212599
  1. 18
      apps/remix-ide/src/app.js
  2. 4
      apps/remix-ide/src/app/panels/file-panel.js
  3. 13
      apps/remix-ide/src/app/plugins/electron/foundryPlugin.ts
  4. 13
      apps/remix-ide/src/app/plugins/electron/hardhatPlugin.ts
  5. 6
      apps/remix-ide/src/app/tabs/compile-tab.js
  6. 19
      apps/remixdesktop/src/engine.ts
  7. 1
      apps/remixdesktop/src/lib/remixd.ts
  8. 193
      apps/remixdesktop/src/lib/slither.ts
  9. 2
      apps/remixdesktop/src/lib/utils.ts
  10. 248
      apps/remixdesktop/src/plugins/foundryPlugin.ts
  11. 35
      apps/remixdesktop/src/plugins/fsPlugin.ts
  12. 219
      apps/remixdesktop/src/plugins/hardhatPlugin.ts
  13. 173
      apps/remixdesktop/src/plugins/slitherPlugin.ts
  14. 5
      apps/remixdesktop/src/preload.ts
  15. 151
      apps/remixdesktop/test/tests/app/foundry.test.ts
  16. 90
      apps/remixdesktop/test/tests/app/hardhat.test.ts
  17. 4
      apps/solidity-compiler/src/app/compiler.ts
  18. 1
      libs/remix-lib/src/types/ICompilerApi.ts
  19. 9
      libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts
  20. 13
      libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx

@ -58,6 +58,11 @@ import { compilerLoaderPlugin, compilerLoaderPluginDesktop } from './app/plugins
import { appUpdaterPlugin } from './app/plugins/electron/appUpdaterPlugin' import { appUpdaterPlugin } from './app/plugins/electron/appUpdaterPlugin'
import { SlitherHandleDesktop } from './app/plugins/electron/slitherPlugin' import { SlitherHandleDesktop } from './app/plugins/electron/slitherPlugin'
import { SlitherHandle } from './app/files/slither-handle' import { SlitherHandle } from './app/files/slither-handle'
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' import { SolCoder } from './app/plugins/solcoderAI'
const isElectron = require('is-electron') const isElectron = require('is-electron')
@ -75,6 +80,7 @@ const Config = require('./config')
const FileManager = require('./app/files/fileManager') const FileManager = require('./app/files/fileManager')
import FileProvider from "./app/files/fileProvider" import FileProvider from "./app/files/fileProvider"
import { appPlatformTypes } from '@remix-ui/app' import { appPlatformTypes } from '@remix-ui/app'
const DGitProvider = require('./app/files/dgitProvider') const DGitProvider = require('./app/files/dgitProvider')
const WorkspaceFileProvider = require('./app/files/workspaceFileProvider') const WorkspaceFileProvider = require('./app/files/workspaceFileProvider')
@ -389,6 +395,14 @@ class AppComponent {
const slitherPlugin = isElectron() ? new SlitherHandleDesktop() : new SlitherHandle() const slitherPlugin = isElectron() ? new SlitherHandleDesktop() : new SlitherHandle()
this.engine.register([slitherPlugin]) 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 // LAYOUT & SYSTEM VIEWS
const appPanel = new MainPanel() const appPanel = new MainPanel()
Registry.getInstance().put({ api: this.mainview, name: 'mainview' }) Registry.getInstance().put({ api: this.mainview, name: 'mainview' })
@ -443,8 +457,6 @@ class AppComponent {
analysis, analysis,
test, test,
filePanel.remixdHandle, filePanel.remixdHandle,
filePanel.hardhatHandle,
filePanel.foundryHandle,
filePanel.truffleHandle, filePanel.truffleHandle,
linkLibraries, linkLibraries,
deployLibraries, deployLibraries,
@ -512,7 +524,7 @@ class AppComponent {
await this.appManager.activatePlugin(['solidity-script', 'remix-templates']) await this.appManager.activatePlugin(['solidity-script', 'remix-templates'])
if (isElectron()) { if (isElectron()) {
await this.appManager.activatePlugin(['isogit', 'electronconfig', 'electronTemplates', 'xterm', 'ripgrep', 'appUpdater', 'slither']) await this.appManager.activatePlugin(['isogit', 'electronconfig', 'electronTemplates', 'xterm', 'ripgrep', 'appUpdater', 'slither', 'foundry', 'hardhat'])
} }
this.appManager.on( this.appManager.on(

@ -6,8 +6,6 @@ import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line
import {Registry} from '@remix-project/remix-lib' import {Registry} from '@remix-project/remix-lib'
import { RemixdHandle } from '../plugins/remixd-handle' import { RemixdHandle } from '../plugins/remixd-handle'
import {PluginViewWrapper} from '@remix-ui/helper' 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') const { TruffleHandle } = require('../files/truffle-handle.js')
/* /*
@ -68,8 +66,6 @@ module.exports = class Filepanel extends ViewPlugin {
this.el.setAttribute('id', 'fileExplorerView') this.el.setAttribute('id', 'fileExplorerView')
this.remixdHandle = new RemixdHandle(this.fileProviders.localhost, appManager) this.remixdHandle = new RemixdHandle(this.fileProviders.localhost, appManager)
this.hardhatHandle = new HardhatHandle()
this.foundryHandle = new FoundryHandle()
this.truffleHandle = new TruffleHandle() this.truffleHandle = new TruffleHandle()
this.contentImport = contentImport this.contentImport = contentImport
this.workspaces = [] this.workspaces = []

@ -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']
}
}

@ -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']
}
}

@ -9,7 +9,7 @@ import { QueryParams } from '@remix-project/remix-lib'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
import { compilerConfigChangedToastMsg, compileToastMsg } from '@remix-ui/helper' import { compilerConfigChangedToastMsg, compileToastMsg } from '@remix-ui/helper'
import { isNative } from '../../remixAppManager' import { isNative } from '../../remixAppManager'
import { Registry } from '@remix-project/remix-lib'
const profile = { const profile = {
name: 'solidity', name: 'solidity',
displayName: 'Solidity compiler', displayName: 'Solidity compiler',
@ -90,6 +90,10 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
return this.fileManager.mode return this.fileManager.mode
} }
isDesktop () {
return Registry.getInstance().get('platform').api.isDesktop()
}
/** /**
* set the compiler configuration * set the compiler configuration
* This function is used by remix-plugin compiler API. * This function is used by remix-plugin compiler API.

@ -11,6 +11,9 @@ import { RipgrepPlugin } from './plugins/ripgrepPlugin';
import { CompilerLoaderPlugin } from './plugins/compilerLoader'; import { CompilerLoaderPlugin } from './plugins/compilerLoader';
import { SlitherPlugin } from './plugins/slitherPlugin'; import { SlitherPlugin } from './plugins/slitherPlugin';
import { AppUpdaterPlugin } from './plugins/appUpdater'; import { AppUpdaterPlugin } from './plugins/appUpdater';
import { FoundryPlugin } from './plugins/foundryPlugin';
import { HardhatPlugin } from './plugins/hardhatPlugin';
import { isE2E } from './main';
const engine = new Engine() const engine = new Engine()
const appManager = new PluginManager() const appManager = new PluginManager()
@ -23,6 +26,8 @@ const ripgrepPlugin = new RipgrepPlugin()
const compilerLoaderPlugin = new CompilerLoaderPlugin() const compilerLoaderPlugin = new CompilerLoaderPlugin()
const slitherPlugin = new SlitherPlugin() const slitherPlugin = new SlitherPlugin()
const appUpdaterPlugin = new AppUpdaterPlugin() const appUpdaterPlugin = new AppUpdaterPlugin()
const foundryPlugin = new FoundryPlugin()
const hardhatPlugin = new HardhatPlugin()
engine.register(appManager) engine.register(appManager)
engine.register(fsPlugin) engine.register(fsPlugin)
@ -33,7 +38,9 @@ engine.register(templatesPlugin)
engine.register(ripgrepPlugin) engine.register(ripgrepPlugin)
engine.register(compilerLoaderPlugin) engine.register(compilerLoaderPlugin)
engine.register(slitherPlugin) engine.register(slitherPlugin)
engine.register(foundryPlugin)
engine.register(appUpdaterPlugin) engine.register(appUpdaterPlugin)
engine.register(hardhatPlugin)
appManager.activatePlugin('electronconfig') appManager.activatePlugin('electronconfig')
appManager.activatePlugin('fs') appManager.activatePlugin('fs')
@ -46,6 +53,18 @@ ipcMain.on('fs:openFolder', async (event, path?) => {
fsPlugin.openFolder(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) => { ipcMain.on('terminal:new', async (event) => {
xtermPlugin.new(event) xtermPlugin.new(event)

@ -28,6 +28,7 @@ export class ElectronBasePluginRemixdClient extends ElectronBasePluginClient {
this.onload(async () => { this.onload(async () => {
this.on('fs' as any, 'workingDirChanged', async (path: string) => { this.on('fs' as any, 'workingDirChanged', async (path: string) => {
console.log('workingDirChanged base remixd', path)
this.currentSharedFolder = path this.currentSharedFolder = path
}) })
this.currentSharedFolder = await this.call('fs' as any, 'getWorkingDir') this.currentSharedFolder = await this.call('fs' as any, 'getWorkingDir')

@ -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<string>
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<string, any>[]): OutputStandard[] {
const standardReport: OutputStandard[] = []
for (const e of detectors) {
const obj = {} as OutputStandard
obj.description = e.description
obj.title = e.check
obj.confidence = e.confidence
obj.severity = e.impact
obj.sourceMap = e.elements.map((element) => {
delete element.source_mapping.filename_used
delete element.source_mapping.filename_absolute
return element
})
standardReport.push(obj)
}
return standardReport
}
analyse(filePath: string, compilerConfig: Record<string, any>) {
return new Promise((resolve, reject) => {
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.'))
}
})
})
}
}

@ -19,6 +19,6 @@ function normalizePath (path) {
return path return path
} }
export { absolutePath } export { absolutePath, normalizePath }

@ -0,0 +1,248 @@
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: 'foundry',
displayName: 'electron foundry',
description: 'electron foundry',
}
export class FoundryPlugin extends ElectronBasePlugin {
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']
}
class FoundryPluginClient extends ElectronBasePluginRemixdClient {
watcher: chokidar.FSWatcher
warnlog: boolean
buildPath: string
cachePath: string
logTimeout: NodeJS.Timeout
processingTimeout: NodeJS.Timeout
async onActivation(): Promise<void> {
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())
this.watcher.on('unlink', 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()
}
}

@ -32,6 +32,15 @@ const getBaseName = (pathName: string): string => {
return path.basename(pathName) 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 { export class FSPlugin extends ElectronBasePlugin {
clients: FSPluginClient[] = [] clients: FSPluginClient[] = []
constructor() { constructor() {
@ -40,28 +49,28 @@ export class FSPlugin extends ElectronBasePlugin {
} }
async onActivation(): Promise<void> { async onActivation(): Promise<void> {
const config = await this.call('electronconfig' as any, 'readConfig') const config = await this.call('electronconfig', 'readConfig')
const openedFolders = (config && config.openedFolders) || [] const openedFolders = (config && config.openedFolders) || []
const recentFolders = (config && config.recentFolders) || [] const recentFolders: recentFolder[] = (config && config.recentFolders) || []
this.call('electronconfig', 'writeConfig', {...config, this.call('electronconfig', 'writeConfig', {...config,
recentFolders: recentFolders, recentFolders: deplucateFolderList(recentFolders),
openedFolders: openedFolders}) openedFolders: openedFolders})
const foldersToDelete: string[] = [] const foldersToDelete: string[] = []
if (openedFolders && openedFolders.length) { if (recentFolders && recentFolders.length) {
for (const folder of openedFolders) { for (const folder of recentFolders) {
try { try {
const stat = await fs.stat(folder) const stat = await fs.stat(folder.path);
if (stat.isDirectory()) { if (stat.isDirectory()) {
// do nothing // do nothing
} }
} catch (e) { } catch (e) {
console.log('error opening folder', folder, e) console.log('error opening folder', folder, e)
foldersToDelete.push(folder) foldersToDelete.push(folder.path)
} }
} }
if (foldersToDelete.length) { if (foldersToDelete.length) {
const newFolders = openedFolders.filter((f: string) => !foldersToDelete.includes(f)) const newFolders = recentFolders.filter((f: recentFolder) => !foldersToDelete.includes(f.path))
this.call('electronconfig', 'writeConfig', {recentFolders: newFolders}) this.call('electronconfig', 'writeConfig', {recentFolders: deplucateFolderList(newFolders)})
} }
} }
createWindow() createWindow()
@ -85,6 +94,13 @@ export class FSPlugin extends ElectronBasePlugin {
client.openFolder(path) 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 = { const clientProfile: Profile = {
@ -346,6 +362,7 @@ class FSPluginClient extends ElectronBasePluginClient {
path, path,
timestamp, timestamp,
}) })
config.recentFolders = deplucateFolderList(config.recentFolders)
writeConfig(config) writeConfig(config)
} }

@ -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<void> {
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)
}
}
}
}
}
}

@ -2,7 +2,22 @@ import { Profile } from "@remixproject/plugin-utils";
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import { ElectronBasePluginRemixdClient } from "../lib/remixd" 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 = { const profile: Profile = {
name: 'slither', name: 'slither',
displayName: 'electron slither', displayName: 'electron slither',
@ -12,7 +27,7 @@ const profile: Profile = {
export class SlitherPlugin extends ElectronBasePlugin { export class SlitherPlugin extends ElectronBasePlugin {
clients: any[] clients: any[]
constructor() { constructor() {
super(profile, clientProfile, SlitherClientMixin(SlitherPluginClient)) super(profile, clientProfile, SlitherPluginClient)
this.methods = [...super.methods] this.methods = [...super.methods]
} }
} }
@ -24,10 +39,158 @@ const clientProfile: Profile = {
methods: ['analyse'] methods: ['analyse']
} }
class SlitherPluginClient extends ElectronBasePluginRemixdClient { 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<string, any>[]): OutputStandard[] {
const standardReport: OutputStandard[] = []
for (const e of detectors) {
const obj = {} as OutputStandard
obj.description = e.description
obj.title = e.check
obj.confidence = e.confidence
obj.severity = e.impact
obj.sourceMap = e.elements.map((element) => {
delete element.source_mapping.filename_used
delete element.source_mapping.filename_absolute
return element
})
standardReport.push(obj)
}
return standardReport
}
analyse(filePath: string, compilerConfig: Record<string, any>) {
return new Promise((resolve, reject) => {
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.'))
}
})
})
} }
} }

@ -6,7 +6,7 @@ console.log('preload.ts', new Date().toLocaleTimeString())
/* preload script needs statically defined API for each plugin */ /* 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 let webContentsId: number | undefined
@ -19,7 +19,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
isE2E: () => ipcRenderer.invoke('config:isE2E'), isE2E: () => ipcRenderer.invoke('config:isE2E'),
canTrackMatomo: () => ipcRenderer.invoke('config:canTrackMatomo'), canTrackMatomo: () => ipcRenderer.invoke('config:canTrackMatomo'),
trackEvent: (args: any[]) => ipcRenderer.invoke('matomo:trackEvent', args), 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) => { activatePlugin: (name: string) => {
return ipcRenderer.invoke('manager:activatePlugin', name) return ipcRenderer.invoke('manager:activatePlugin', name)
}, },

@ -0,0 +1,151 @@
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 + '/hello_foundry/').then(done)
}, [dir], () => {
console.log('done window opened')
})
.waitForElementVisible('*[data-id="treeViewDivDraggableItemfoundry.toml"]', 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)
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<void> {
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<void> {
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<void> {
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<void> {
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 = {
...process.platform.startsWith('linux') ? tests : {}
}

@ -0,0 +1,90 @@
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)
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())
})
}
}
async function compileHardhatProject(): Promise<void> {
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<void> {
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
}

@ -55,4 +55,8 @@ export class CompilerClientApi extends CompilerApiMixin(PluginClient) implements
getFileManagerMode () { getFileManagerMode () {
return 'browser' return 'browser'
} }
isDesktop() {
return false
}
} }

@ -18,6 +18,7 @@ export interface ICompilerApi {
setAppParameter: (name: string, value: string | boolean) => void setAppParameter: (name: string, value: string | boolean) => void
getFileManagerMode: () => string getFileManagerMode: () => string
isDesktop: () => boolean
setCompilerConfig: (settings: any) => void setCompilerConfig: (settings: any) => void
getCompilationResult: () => any getCompilationResult: () => any

@ -129,26 +129,27 @@ export class CompileTabLogic {
} }
async isHardhatProject () { 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') return await this.api.fileExists('hardhat.config.js') || await this.api.fileExists('hardhat.config.ts')
} else return false } else return false
} }
async isTruffleProject () { async isTruffleProject () {
if (this.api.getFileManagerMode() === 'localhost') { if (this.api.getFileManagerMode() === ('localhost') || this.api.isDesktop()) {
return await this.api.fileExists('truffle-config.js') return await this.api.fileExists('truffle-config.js')
} else return false } else return false
} }
async isFoundryProject () { async isFoundryProject () {
if (this.api.getFileManagerMode() === 'localhost') { if (this.api.getFileManagerMode() === ('localhost') || this.api.isDesktop()) {
return await this.api.fileExists('foundry.toml') return await this.api.fileExists('foundry.toml')
} else return false } else return false
} }
runCompiler (externalCompType) { runCompiler (externalCompType) {
try { try {
if (this.api.getFileManagerMode() === 'localhost') { if (this.api.getFileManagerMode() === 'localhost' || this.api.isDesktop()) {
if (externalCompType === 'hardhat') { if (externalCompType === 'hardhat') {
const { currentVersion, optimize, runs } = this.compiler.state const { currentVersion, optimize, runs } = this.compiler.state
if (currentVersion) { if (currentVersion) {

@ -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 { CompileErrors, ContractsFile, SolidityCompilerProps } from './types'
import { CompilerContainer } from './compiler-container' // eslint-disable-line import { CompilerContainer } from './compiler-container' // eslint-disable-line
import { ContractSelection } from './contract-selection' // 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 * as packageJson from '../../../../../package.json'
import './css/style.css' import './css/style.css'
import { iSolJsonBinData, iSolJsonBinDataBuild } from '@remix-project/remix-lib' import { iSolJsonBinData, iSolJsonBinDataBuild } from '@remix-project/remix-lib'
import { appPlatformTypes, platformContext } from '@remix-ui/app'
export const SolidityCompiler = (props: SolidityCompilerProps) => { export const SolidityCompiler = (props: SolidityCompilerProps) => {
const { const {
@ -47,6 +48,7 @@ export const SolidityCompiler = (props: SolidityCompilerProps) => {
const [compileErrors, setCompileErrors] = useState<Record<string, CompileErrors>>({ [currentFile]: api.compileErrors }) const [compileErrors, setCompileErrors] = useState<Record<string, CompileErrors>>({ [currentFile]: api.compileErrors })
const [badgeStatus, setBadgeStatus] = useState<Record<string, { key: string; title?: string; type?: string }>>({}) const [badgeStatus, setBadgeStatus] = useState<Record<string, { key: string; title?: string; type?: string }>>({})
const [contractsFile, setContractsFile] = useState<ContractsFile>({}) const [contractsFile, setContractsFile] = useState<ContractsFile>({})
const platform = useContext(platformContext)
useEffect(() => { useEffect(() => {
; (async () => { ; (async () => {
@ -77,9 +79,12 @@ export const SolidityCompiler = (props: SolidityCompilerProps) => {
} }
api.onSetWorkspace = async (isLocalhost: boolean, workspaceName: string) => { api.onSetWorkspace = async (isLocalhost: boolean, workspaceName: string) => {
const isHardhat = isLocalhost && (await compileTabLogic.isHardhatProject()) const isDesktop = platform === appPlatformTypes.desktop
const isTruffle = isLocalhost && (await compileTabLogic.isTruffleProject()) console.log('onSetWorkspace', workspaceName, isLocalhost, isDesktop, workspaceName)
const isFoundry = isLocalhost && (await compileTabLogic.isFoundryProject()) 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) => { setState((prevState) => {
return { return {
...prevState, ...prevState,

Loading…
Cancel
Save