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