From 2314a3a32e097722d7435ae2d5ba324361eca1bb Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jul 2024 08:08:42 +0200 Subject: [PATCH] add foundry --- apps/remixdesktop/src/lib/foundry.ts | 207 ++++++++++++++++++ .../remixdesktop/src/plugins/foundryPlugin.ts | 35 +++ 2 files changed, 242 insertions(+) create mode 100644 apps/remixdesktop/src/lib/foundry.ts create mode 100644 apps/remixdesktop/src/plugins/foundryPlugin.ts diff --git a/apps/remixdesktop/src/lib/foundry.ts b/apps/remixdesktop/src/lib/foundry.ts new file mode 100644 index 0000000000..30edf37889 --- /dev/null +++ b/apps/remixdesktop/src/lib/foundry.ts @@ -0,0 +1,207 @@ +import { spawn } from 'child_process' +import chokidar from 'chokidar' +import fs from 'fs' +import { basename, join } from 'path' +import * as utils from './utils' +export const FoundryClientMixin = (Base) => class extends Base { + + currentSharedFolder: string + watcher: chokidar.FSWatcher + warnlog: boolean + buildPath: string + cachePath: string + logTimeout: NodeJS.Timeout + processingTimeout: NodeJS.Timeout + + startListening() { + 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', () => { + if (fs.existsSync(this.buildPath) && fs.existsSync(this.cachePath)) { + this.listenOnFoundryCompilation() + } + }) + } catch (e) { + console.log(e) + } + } + + compile() { + return new Promise((resolve, reject) => { + if (this.readOnly) { + const errMsg = '[Foundry Compilation]: Cannot compile in read-only mode' + return reject(new Error(errMsg)) + } + 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 { + 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.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() + } +} \ No newline at end of file diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts new file mode 100644 index 0000000000..0b3411ceae --- /dev/null +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -0,0 +1,35 @@ +import { Profile } from "@remixproject/plugin-utils"; +import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" + +import { ElectronBasePluginRemixdClient } from "../lib/remixd" + +import { FoundryClientMixin } from "../lib/foundry"; +const profile: Profile = { + name: 'slither', + displayName: 'electron slither', + description: 'electron slither', +} + +export class FoundryPlugin extends ElectronBasePlugin { + clients: any [] + constructor() { + super(profile, clientProfile, FoundryClientMixin(FoundryPluginClient)) + this.methods = [...super.methods] + } +} + +const clientProfile: Profile = { + name: 'foundry', + displayName: 'electron foundry', + description: 'electron foundry', + methods: ['sync', 'compile'] +} + + +class FoundryPluginClient extends ElectronBasePluginRemixdClient { + constructor(webContentsId: number, profile: Profile) { + super(webContentsId, profile); + } +} + +