This reverts commitpull/5100/heada247fcce2d
, reversing changes made to108c41428b
.
parent
996e76cc26
commit
a2ef615e00
@ -1,13 +0,0 @@ |
|||||||
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'] |
|
||||||
} |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
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'] |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,193 @@ |
|||||||
|
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.')) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -1,248 +0,0 @@ |
|||||||
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.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') |
|
||||||
if(this.currentSharedFolder) this.startListening() |
|
||||||
} |
|
||||||
|
|
||||||
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() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
|
|
@ -1,220 +0,0 @@ |
|||||||
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.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') |
|
||||||
if(this.currentSharedFolder) this.startListening() |
|
||||||
} |
|
||||||
|
|
||||||
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) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,157 +0,0 @@ |
|||||||
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() |
|
||||||
}) |
|
||||||
server.stderr.on('err', function (data) { |
|
||||||
console.log('err', data.toString()) |
|
||||||
}) |
|
||||||
server.stdout.on('data', function (data) { |
|
||||||
console.log('data', data.toString()) |
|
||||||
}) |
|
||||||
}) |
|
||||||
} 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 : {} |
|
||||||
} |
|
@ -1,90 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
@ -1,40 +0,0 @@ |
|||||||
const path = require('path'); |
|
||||||
const nodeExternals = require('webpack-node-externals'); |
|
||||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); |
|
||||||
const mode = process.env.NODE_ENV || 'development'; |
|
||||||
const webpack = require('webpack'); |
|
||||||
module.exports = { |
|
||||||
mode, |
|
||||||
entry: { |
|
||||||
main: './src/main.ts', |
|
||||||
preload: './src/preload.ts', |
|
||||||
}, |
|
||||||
target: 'electron-main', |
|
||||||
externals: [nodeExternals()], |
|
||||||
module: { |
|
||||||
rules: [ |
|
||||||
{ |
|
||||||
test: /\.ts$/, |
|
||||||
include: /src/, |
|
||||||
use: [{ loader: 'ts-loader' }] |
|
||||||
} |
|
||||||
] |
|
||||||
}, |
|
||||||
resolve: { |
|
||||||
extensions: ['.ts', '.js'], |
|
||||||
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })] |
|
||||||
}, |
|
||||||
plugins: [ |
|
||||||
new webpack.DefinePlugin({ |
|
||||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || mode) |
|
||||||
}) |
|
||||||
], |
|
||||||
output: { |
|
||||||
path: path.resolve(__dirname, 'build'), |
|
||||||
filename: '[name].js' |
|
||||||
}, |
|
||||||
node: { |
|
||||||
__dirname: false, |
|
||||||
__filename: false |
|
||||||
} |
|
||||||
} |
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue