commit
56867ace88
@ -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'] |
||||||
|
} |
||||||
|
} |
@ -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.')) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
@ -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() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
} |
Loading…
Reference in new issue