diff --git a/apps/remixdesktop/src/plugins/foundry.ts b/apps/remixdesktop/src/plugins/foundry.ts new file mode 100644 index 0000000000..feaba4992a --- /dev/null +++ b/apps/remixdesktop/src/plugins/foundry.ts @@ -0,0 +1,228 @@ +import * as chokidar from 'chokidar' +import { Profile } from "@remixproject/plugin-utils"; +import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" +import fs from 'fs/promises' +const { spawn } = require('child_process') // eslint-disable-line + +const profile: Profile = { + name: 'electronFoundry', + displayName: 'electronFoundry', + description: 'Foundry plugin', +} + +export class FoundryPlugin extends ElectronBasePlugin { + clients: FoundryPluginClient[] = [] + constructor() { + super(profile, clientProfile, FoundryPluginClient) + } +} + +const clientProfile: Profile = { + name: 'electronFoundry', + displayName: 'electronFoundry', + description: 'Foundry plugin', + methods: ['compile', 'sync'], +} + +class FoundryPluginClient extends ElectronBasePluginClient { + methods: Array + currentSharedFolder: string + watcher: chokidar.FSWatcher + warnlog: boolean + buildPath: string + cachePath: string + logTimeout: NodeJS.Timeout + processingTimeout: NodeJS.Timeout + + constructor(webContentsId: number, profile: Profile) { + super(webContentsId, profile) + this.onActivation = () => { + console.log('Foundry plugin activated') + this.call('terminal', 'log', { type: 'log', value: 'Foundry plugin activated' }) + this.startListening() + } + } + + async startListening() { + const statsBuild = await fs.lstat(this.buildPath) + const statsCache = await fs.lstat(this.cachePath) + + if (statsBuild.isDirectory() && statsCache.isDirectory()) { + 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', async () => { + const statsBuild = await fs.lstat(this.buildPath) + const statsCache = await fs.lstat(this.cachePath) + if (statsBuild.isDirectory() && statsCache.isDirectory()) { + 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: any) => { + const msg = `[Foundry Compilation]: ${data.toString()}` + console.log('\x1b[32m%s\x1b[0m', msg) + result += msg + '\n' + }) + child.stderr.on('data', (err: any) => { + 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.readdir(this.buildPath) // "out" folder + try { + const cache = JSON.parse(await fs.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: {} + }, + solcVersion: null, + compilationTarget: null + } + 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` }) + console.log('Syncing compilation result from Foundry') + }, 1000) + + } catch (e) { + console.log(e) + } + } + + async triggerProcessArtifact() { + // prevent multiple calls + clearTimeout(this.processingTimeout) + this.processingTimeout = setTimeout(async () => await this.processArtifact(), 1000) + } + + listenOnFoundryCompilation() { + try { + if(this.watcher) this.watcher.close() + this.watcher = chokidar.watch(this.cachePath, { depth: 0, ignorePermissionErrors: true, ignoreInitial: true }) + this.watcher.on('change', async () => await this.triggerProcessArtifact()) + this.watcher.on('add', async () => await this.triggerProcessArtifact()) + // process the artifact on activation + this.triggerProcessArtifact() + } catch (e) { + console.log(e) + } + } + + async readContract(contractFolder, compilationResultPart, cache) { + const files = await fs.readdir(contractFolder) + + for (const file of files) { + const path = join(contractFolder, file) + const content = await fs.readFile(path, { encoding: 'utf-8' }) + await this.feedContractArtifactFile(file, content, compilationResultPart, cache) + } + } + + async feedContractArtifactFile(path, content, compilationResultPart, cache) { + const contentJSON = JSON.parse(content) + const contractName = basename(path).replace('.json', '') + + const currentCache = cache.files[contentJSON.ast.absolutePath] + 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.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 = contentJSON.ast.absolutePath + // extract data + if (!compilationResultPart.output['sources'][contentJSON.ast.absolutePath]) compilationResultPart.output['sources'][contentJSON.ast.absolutePath] = {} + compilationResultPart.output['sources'][contentJSON.ast.absolutePath] = { + ast: contentJSON['ast'], + id: contentJSON['id'] + } + if (!compilationResultPart.output['contracts'][contentJSON.ast.absolutePath]) compilationResultPart.output['contracts'][contentJSON.ast.absolutePath] = {} + + contentJSON.bytecode.object = contentJSON.bytecode.object.replace('0x', '') + contentJSON.deployedBytecode.object = contentJSON.deployedBytecode.object.replace('0x', '') + compilationResultPart.output['contracts'][contentJSON.ast.absolutePath][contractName] = { + abi: contentJSON.abi, + evm: { + bytecode: contentJSON.bytecode, + deployedBytecode: contentJSON.deployedBytecode, + methodIdentifiers: contentJSON.methodIdentifiers + } + } + } + + async sync() { + console.log('syncing Foundry with Remix...') + this.processArtifact() + } + +} + + diff --git a/libs/remixd/src/services/foundryClient.ts b/libs/remixd/src/services/foundryClient.ts index add3378407..47939f4944 100644 --- a/libs/remixd/src/services/foundryClient.ts +++ b/libs/remixd/src/services/foundryClient.ts @@ -8,7 +8,6 @@ const { spawn } = require('child_process') // eslint-disable-line export class FoundryClient extends PluginClient { methods: Array - websocket: WS currentSharedFolder: string watcher: chokidar.FSWatcher warnlog: boolean @@ -27,14 +26,6 @@ export class FoundryClient extends PluginClient { } } - setWebSocket(websocket: WS): void { - this.websocket = websocket - this.websocket.addEventListener('close', () => { - this.warnlog = false - if (this.watcher) this.watcher.close() - }) - } - sharedFolder(currentSharedFolder: string): void { this.currentSharedFolder = currentSharedFolder this.buildPath = utils.absolutePath('out', this.currentSharedFolder)