diff --git a/README.md b/README.md index 42f397e..68b6679 100644 --- a/README.md +++ b/README.md @@ -2,70 +2,77 @@ **NOTE: Debugger requires path installations of [huffc](https://github.com/huff-language/huff-rs) and [hevm](https://github.com/dapphub/dapptools/tree/master/src/hevm) from [dapptools](https://github.com/dapphub/dapptools) before use.** -Features at a glances: +> A mirror version of this repository can be found on [open-vsx](https://open-vsx.org/extension/huff-language/huff-language) + +Features at a glances: + - Advanced language support - Syntax Highlighting - Hover cards detailing gas usage - Code Generation ## Hevm Powered Debugging -Step through execution of your contracts from external calls, or even individual macros. Manipulate the stack inputs when your macro starts and even override storage slots. Star power your productivity. + +Step through execution of your contracts from external calls, or even individual macros. Manipulate the stack inputs when your macro starts and even override storage slots. Star power your productivity. This is particularly useful for debugging your macros individually. For more e2e debugging, the foundry debugger is an excellent tool. **How it works:** -Open up any .huff file and press `Load Interface` to prepare external functions, enter in your calldata parameters and hit debug to get started. +Open up any .huff file and press `Load Interface` to prepare external functions, enter in your calldata parameters and hit debug to get started. ![function debugging window](./resources/Function_selector.png) The example above is for an erc721 mint function, entering from the function debugger will allow you to step through execution from the MAIN entrypoint. To select another function, click the drop down and select another function! The drop down will be populated with functions that are defined at the top of the file that you are currently debugging. -**Notice - *Relevant to WSL users*** -To get the extension to run in wsl there is a workaround that you will have to make. Unfortunately, vscode extensions do not run in the same environment as your wsl shell so do not have access to the same path (even when mounted with wsl-remote). If the extension detects that you are working in a remote code space it will attempt to execute huffc and hevm within the wsl shell. The wsl api does not run the ~/.bashrc script (the file which huffup adds to the path), therefore to use this extension in wsl you will have to copy the following into your ~/.profile. +**Notice - _Relevant to WSL users_** +To get the extension to run in wsl there is a workaround that you will have to make. Unfortunately, vscode extensions do not run in the same environment as your wsl shell so do not have access to the same path (even when mounted with wsl-remote). If the extension detects that you are working in a remote code space it will attempt to execute huffc and hevm within the wsl shell. The wsl api does not run the ~/.bashrc script (the file which huffup adds to the path), therefore to use this extension in wsl you will have to copy the following into your ~/.profile. + ``` if [ -d "$HOME/.huff/bin" ]; then PATH="$HOME/.huff/bin:$PATH" fi ``` -We will look to address this limitation in further releases of huffup. +We will look to address this limitation in further releases of huffup. ### Macro Debugging + Macro debugging is the tool's most useful feature. Clicking `Load Macros` will load the macros of the currently open file. If the macro has no `takes` arguments then the window will look like this: - ![function debugging window](./resources/macro_window.png) - -You can choose to add arbitrary hex `calldata` to the debugger instance by selecting the `With Calldata` checkbox. +You can choose to add arbitrary hex `calldata` to the debugger instance by selecting the `With Calldata` checkbox. #### Defining `takes` parameters: -![function debugging window](./resources/macro_with_takes_params.png) +![function debugging window](./resources/macro_with_takes_params.png) If the selected macro has `takes(n)` then n windows will appear to allow setting of the stack before execution. #### Overriding storage: -![function debugging window](./resources/storage_overrides.png) +![function debugging window](./resources/storage_overrides.png) `Storage Overrides` allows you to test your macro with arbitrary overrides for specified storage slots! To use, select the `Storage Overrides` checkbox, then add as many slots as you require. Upon execution, the slots will be set to the values that you have defined. You can quickly toggle this feature off by unselecting the `Storage Overrides` checkbox. ## Code Generation + **Switch table generation** Generate the MAIN macro switch table with jumps from just the interface definition. No more visiting keccak online or using `cast sig xxx` to copy function selectors into your code. Just write the interface and let us handle the rest. Usage: - `commandPallete -> Huff: Generate MAIN() Switch Table` +`commandPallete -> Huff: Generate MAIN() Switch Table` **Event signature generation** -Similarly to switch table generation above. Forget about calculating the keccak of you event topics yourself. +Similarly to switch table generation above. Forget about calculating the keccak of you event topics yourself. Usage: - `commandPallete -> Huff: Generate interface signature from interface` +`commandPallete -> Huff: Generate interface signature from interface` ## Hover Cards -Hovering the cursor over an opcode will explain what operation it performs, the minimum amount of gas it uses, as well as a link to evm.codes to read more about it. + +Hovering the cursor over an opcode will explain what operation it performs, the minimum amount of gas it uses, as well as a link to evm.codes to read more about it. ## Currently not supported + [] Constructor Arguments. diff --git a/package.json b/package.json index 3ec7e55..357a2b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "huff-language", - "version": "0.0.30", + "version": "0.0.32", "displayName": "Huff", "preview": true, "icon": "resources/huff.png", diff --git a/src/features/debugger/function/debugger.js b/src/features/debugger/function/debugger.js index 0f6ec8e..a61a421 100644 --- a/src/features/debugger/function/debugger.js +++ b/src/features/debugger/function/debugger.js @@ -1,24 +1,40 @@ const fs = require("fs"); -const createKeccakHash = require('keccak'); +const createKeccakHash = require("keccak"); const path = require("path"); const vscode = require("vscode"); // TODO: use a slimmer abicoder const { AbiCoder } = require("@ethersproject/abi"); const { hevmConfig } = require("../../../options"); -const { deployContract, runInUserTerminal, writeHevmCommand, resetStateRepo, registerError, compileFromFile, checkInstallations, purgeCache, craftTerminalCommand } = require("../debuggerUtils"); +const { + deployContract, + runInUserTerminal, + writeHevmCommand, + resetStateRepo, + registerError, + compileFromFile, + checkInstallations, + purgeCache, + craftTerminalCommand, +} = require("../debuggerUtils"); const { isWsl, wslMountedDriveRegex } = require("../../../settings"); - -/**Start function debugger - * +/**Start function debugger + * * @param {String} sourceDirectory The current working directory of selected files workspace * @param {String} currentFile The path to the currently selected file * @param {String} functionSelector The 4byte function selector of the transaction being debugged * @param {Array>} argsArray Each arg is provided in the format [type, value] so that they can easily be parsed with abi encoder - * @param {Object} options Options - not explicitly defined + * @param {Object} options Options - not explicitly defined */ -async function startDebugger(sourceDirectory, currentFile, imports, functionSelector, argsArray, options = { state: true }) { +async function startDebugger( + sourceDirectory, + currentFile, + imports, + functionSelector, + argsArray, + options = {} +) { try { if (!(await checkInstallations())) return; @@ -33,6 +49,7 @@ async function startDebugger(sourceDirectory, currentFile, imports, functionSele // Create deterministic deployment address for each contract for the deployed contract const config = { ...hevmConfig, + ...options, hevmContractAddress: createKeccakHash("keccak256") .update(Buffer.from(currentFile)) .digest("hex") @@ -40,7 +57,7 @@ async function startDebugger(sourceDirectory, currentFile, imports, functionSele .slice(0, 42), stateChecked: true, mountedDrive, - } + }; // Get calldata const calldata = await encodeCalldata(functionSelector, argsArray); @@ -49,61 +66,71 @@ async function startDebugger(sourceDirectory, currentFile, imports, functionSele const compilableFile = flattenFile(cwd, currentFile, imports); // Compile binary using locally installed compiler - in the future this will be replaced with a wasm compiler - const bytecode = compileFromFile(compilableFile, config.tempMacroFilename, cwd); + const bytecode = compileFromFile( + compilableFile, + config.tempMacroFilename, + cwd + ); // Get runtime bytecode and run constructor logic const runtimeBytecode = deployContract(bytecode, config, cwd); - runDebugger(runtimeBytecode, calldata, options, config, cwd) - } - catch (e) { - registerError(e, "Compilation failed, please contact the team in the huff discord"); - return null + runDebugger(runtimeBytecode, calldata, options, config, cwd); + } catch (e) { + registerError( + e, + "Compilation failed, please contact the team in the huff discord" + ); + return null; } } /**Flatten File - * - * @param {String} cwd - * @param {String} currentFile - * @param {Array} imports declared file imports at the top of the current file - * @returns + * + * @param {String} cwd + * @param {String} currentFile + * @param {Array} imports declared file imports at the top of the current file + * @returns */ function flattenFile(cwd, currentFile, imports) { // Get relative path of files - const dirPath = currentFile.split("/").slice(0, -1).join("/") + const dirPath = currentFile.split("/").slice(0, -1).join("/"); // Get absolute paths - const paths = imports.map(importPath => path.join(`${cwd}/${dirPath}`, importPath.replace(/#include\s?"/, "").replace('"', ""))); + const paths = imports.map((importPath) => + path.join( + `${cwd}/${dirPath}`, + importPath.replace(/#include\s?"/, "").replace('"', "") + ) + ); // Read file contents and remove other instances of main - // main regex - const mainRegex = /#define\s+macro\s+MAIN\s?\((?[^\)]*)\)\s?=\s?takes\s?\((?[\d])\)\s?returns\s?\((?[\d])\)\s?{(?[\s\S]*?(?=}))}/gsm + // main regex + const mainRegex = + /#define\s+macro\s+MAIN\s?\((?[^\)]*)\)\s?=\s?takes\s?\((?[\d])\)\s?returns\s?\((?[\d])\)\s?{(?[\s\S]*?(?=}))}/gms; const files = [ fs.readFileSync(cwd + "/" + currentFile).toString(), - ...paths.map(path => { - return fs.readFileSync(path) - .toString().replace(mainRegex, "") - }) + ...paths.map((path) => { + return fs.readFileSync(path).toString().replace(mainRegex, ""); + }), ]; // Flatten and remove imports return `${files.join("\n")}`.replace(/#include\s".*"/gm, ""); } - /**Run debugger - * + * * Craft hevm command and run it in the user terminal - * - * @param {String} bytecode - * @param {String} calldata - * @param {Object} flags - * @param {Object} config - * @param {String} cwd + * + * @param {String} bytecode + * @param {String} calldata + * @param {Object} flags + * @param {Object} config + * @param {String} cwd */ function runDebugger(bytecode, calldata, flags, config, cwd) { - console.log("Entering debugger...") + console.log("Entering debugger..."); // Hevm Command const hevmCommand = `hevm exec \ @@ -111,50 +138,54 @@ function runDebugger(bytecode, calldata, flags, config, cwd) { --address ${config.hevmContractAddress} \ --caller ${config.hevmCaller} \ --gas 0xffffffff \ - --state ${((config.mountedDrive) ? ("/mnt/" + config.mountedDrive) : "") + cwd + "/" + config.statePath} \ + --state ${ + (config.mountedDrive ? "/mnt/" + config.mountedDrive : "") + + cwd + + "/" + + config.statePath + } \ --debug \ - ${calldata ? "--calldata " + calldata : ""}` + ${config.callValueChecked ? "--value " + config.callValue : ""} \ + ${calldata ? "--calldata " + calldata : ""}`; // command is cached into a file as execSync has a limit on the command size that it can execute writeHevmCommand(hevmCommand, config.tempHevmCommandFilename, cwd); - const terminalCommand = craftTerminalCommand(cwd, config) + const terminalCommand = craftTerminalCommand(cwd, config); runInUserTerminal(terminalCommand); } - /**Prepare Debug Transaction - * + * * Use abi encoder to encode transaction data - * - * @param {String} functionSelector - * @param {Array} argsObject - * @returns + * + * @param {String} functionSelector + * @param {Array} argsObject + * @returns */ async function encodeCalldata(functionSelector, argsObject) { - console.log("Preparing debugger calldata...") + console.log("Preparing debugger calldata..."); try { if (argsObject.length == 0) return `0x${functionSelector[0]}`; // TODO: error handle with user prompts - const abiEncoder = new AbiCoder() + const abiEncoder = new AbiCoder(); // create interface readable by the abi encoder let type = []; let value = []; - argsObject.forEach(arg => { + argsObject.forEach((arg) => { type.push(arg[0]); value.push(arg[1]); }); const encoded = abiEncoder.encode(type, value); - return `0x${functionSelector[0]}${encoded.slice(2, encoded.length)}` - + return `0x${functionSelector[0]}${encoded.slice(2, encoded.length)}`; } catch (e) { registerError(e, `Compilation failed\nSee\n${e}`); } } module.exports = { - startDebugger -} + startDebugger, +}; diff --git a/src/features/debugger/function/functionDebuggerViewProvider.js b/src/features/debugger/function/functionDebuggerViewProvider.js index c99e12c..f9fea8f 100644 --- a/src/features/debugger/function/functionDebuggerViewProvider.js +++ b/src/features/debugger/function/functionDebuggerViewProvider.js @@ -1,100 +1,123 @@ const vscode = require("vscode"); -const {getNonce} = require("../providerUtils"); -const {startDebugger} = require("./debugger"); -const {getFunctionSignaturesAndArgs, getImports} = require("../../regexUtils"); - +const { getNonce } = require("../providerUtils"); +const { startDebugger } = require("./debugger"); +const { + getFunctionSignaturesAndArgs, + getImports, +} = require("../../regexUtils"); /**Debugger View Provider - * - * Interface between the vscode extension and the debug webview envs. + * + * Interface between the vscode extension and the debug webview envs. * The two components communicate with each other via a message bus */ -class DebuggerViewProvider{ - - static viewType = "huff.debugView"; - - constructor(extensionUri){ - this._extensionURI = extensionUri; - this._view = null; - } - - resolveWebviewView( - webviewView, - context, - _token - ){ - // Enable scripting within the webview - webviewView.webview.options = { - enableScripts: true, - localResourceRoots: [ - this._extensionURI - ] +class DebuggerViewProvider { + static viewType = "huff.debugView"; + + constructor(extensionUri) { + this._extensionURI = extensionUri; + this._view = null; + } + + resolveWebviewView(webviewView, context, _token) { + // Enable scripting within the webview + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this._extensionURI], + }; + + // Set the webview's html - written inline + webviewView.webview.html = this.getHtmlForWebView(webviewView.webview); + + // Handle messages from the webview + webviewView.webview.onDidReceiveMessage((data) => { + switch (data.type) { + case "loadDocument": { + const functionSignatures = getFunctionSignaturesAndArgs( + vscode.window.activeTextEditor?.document.getText() + ); + this.addOptionsToFunctionSelector(functionSignatures.sighashes); + + break; } - - // Set the webview's html - written inline - webviewView.webview.html = this.getHtmlForWebView(webviewView.webview); - - // Handle messages from the webview - webviewView.webview.onDidReceiveMessage(data => { - switch (data.type) { - case "loadDocument":{ - const functionSignatures = getFunctionSignaturesAndArgs(vscode.window.activeTextEditor?.document.getText()); - this.addOptionsToFunctionSelector(functionSignatures.sighashes); - - break; - } - case "start-debug": { - const {selectedFunction, argsArr} = data.values; - - const imports = getImports(vscode.window.activeTextEditor?.document.getText()) - - startDebugger( - vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri).uri.path, - vscode.workspace.asRelativePath(vscode.window.activeTextEditor.document.uri), - imports, - selectedFunction, - argsArr, - {} - ); - } + case "start-debug": { + const { selectedFunction, argsArr, callValueChecked, callValue } = + data.values; + + const imports = getImports( + vscode.window.activeTextEditor?.document.getText() + ); + + startDebugger( + vscode.workspace.getWorkspaceFolder( + vscode.window.activeTextEditor.document.uri + ).uri.path, + vscode.workspace.asRelativePath( + vscode.window.activeTextEditor.document.uri + ), + imports, + selectedFunction, + argsArr, + { + callValueChecked, + callValue, } - }); - - this._view = webviewView; - } - - /**Add Options to function selector - * - * Send function selectors back to the web view after they have been scraped from the file - * @param {Object} functionSelectors - */ - addOptionsToFunctionSelector(functionSelectors){ - if (this._view){ - this._view.show?.(true); - this._view.webview.postMessage({ type: "receiveContractInterface", data: functionSelectors }) + ); } + } + }); + + this._view = webviewView; + } + + /**Add Options to function selector + * + * Send function selectors back to the web view after they have been scraped from the file + * @param {Object} functionSelectors + */ + addOptionsToFunctionSelector(functionSelectors) { + if (this._view) { + this._view.show?.(true); + this._view.webview.postMessage({ + type: "receiveContractInterface", + data: functionSelectors, + }); } - - /**Get html for webview - * - * Inline create html to be shown in the function debugger webview - * @param webview - * @returns - */ - getHtmlForWebView(webview) { - // local path of main script to run in the webview - const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionURI, "webview", "function", "functions.main.js")); - const helpersUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionURI, "webview", "helpers.js")); - - // Do the same for the stylesheet - const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionURI, "webview", "css", "vscode.css")); - const styleMainUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionURI, "webview","css", "main.css")); - - // Use nonce to allow only a specific script to be run - const nonce = getNonce(); - - return ` + } + + /**Get html for webview + * + * Inline create html to be shown in the function debugger webview + * @param webview + * @returns + */ + getHtmlForWebView(webview) { + // local path of main script to run in the webview + const scriptUri = webview.asWebviewUri( + vscode.Uri.joinPath( + this._extensionURI, + "webview", + "function", + "functions.main.js" + ) + ); + const helpersUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionURI, "webview", "helpers.js") + ); + + // Do the same for the stylesheet + const styleVSCodeUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionURI, "webview", "css", "vscode.css") + ); + const styleMainUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionURI, "webview", "css", "main.css") + ); + + // Use nonce to allow only a specific script to be run + const nonce = getNonce(); + + return ` @@ -118,6 +141,13 @@ class DebuggerViewProvider{
+ +
+ + +
+ +
@@ -125,10 +155,9 @@ class DebuggerViewProvider{ `; - - } + } } module.exports = { - DebuggerViewProvider -} \ No newline at end of file + DebuggerViewProvider, +}; diff --git a/src/features/debugger/macro/macroDebugger.js b/src/features/debugger/macro/macroDebugger.js index b75dbf8..3f4a635 100644 --- a/src/features/debugger/macro/macroDebugger.js +++ b/src/features/debugger/macro/macroDebugger.js @@ -1,4 +1,4 @@ -const createKeccakHash = require('keccak'); +const createKeccakHash = require("keccak"); const fs = require("fs"); const { hevmConfig } = require("../../../options"); const path = require("path"); @@ -14,24 +14,31 @@ const { } = require("../debuggerUtils"); const vscode = require("vscode"); -const { isWsl, wslMountedDriveRegex } = require('../../../settings'); +const { isWsl, wslMountedDriveRegex } = require("../../../settings"); /**Start Macro Debugger - * + * * Steps: * - Given a macro * - Create a new temporary huff file that contains the macro and all of it's imports - * - Compile the newly created file + * - Compile the newly created file * - Deploy the file using --create to get the runtime bytecode * - Run the runtime bytecode in hevm with the --debug flag in a new terminal - * + * * @param {String} sourceDirectory Working directory of the root of the workspace * @param {String} currentFile The currently selected huff file * @param {Array} imports Imports found at the top of the selected file - to allow the compiler to inline macros from another file * @param {Object} macro Macro object - contains takes, returns and the macro body text * @param {*} argsObject The stack args provided by the user */ -async function startMacroDebugger(sourceDirectory, currentFile, imports, macro, argsObject, options) { +async function startMacroDebugger( + sourceDirectory, + currentFile, + imports, + macro, + argsObject, + options +) { if (!(await checkInstallations())) return; // Remove /c: appended to source directory on windows systems - nodes file system apis absolutely hate it @@ -40,7 +47,7 @@ async function startMacroDebugger(sourceDirectory, currentFile, imports, macro, mountedDrive = wslMountedDriveRegex.exec(sourceDirectory)?.groups?.drive; sourceDirectory = sourceDirectory.replace(`/mnt/${mountedDrive}`, ""); } - const cwd = sourceDirectory.replace("/c:", "") + const cwd = sourceDirectory.replace("/c:", ""); try { // Create deterministic deployment address for each contract @@ -53,56 +60,77 @@ async function startMacroDebugger(sourceDirectory, currentFile, imports, macro, .digest("hex") .toString("hex") .slice(0, 42), - } + }; // Compilable macro is the huff source code for our new macro object - let compilableMacro = createCompiledMacro(cwd, macro, argsObject, currentFile, imports); + let compilableMacro = createCompiledMacro( + cwd, + macro, + argsObject, + currentFile, + imports + ); // Create a constructor that will set storage slots -> TODO: run this manually against hevm git repository - if (config.storageChecked) compilableMacro = overrideStorage(compilableMacro, config); - - const bytecode = compileFromFile(compilableMacro, config.tempMacroFilename, cwd); + if (config.storageChecked) + compilableMacro = overrideStorage(compilableMacro, config); + + const bytecode = compileFromFile( + compilableMacro, + config.tempMacroFilename, + cwd + ); const runtimeBytecode = deployContract(bytecode, config, cwd); // deploy the contract to get the runtime code runMacroDebugger(bytecode, runtimeBytecode, config, cwd); } catch (e) { - registerError(e, "Macro Compilation Error, this is pre-release software, please report this issue to the huff team in the discord"); + registerError( + e, + "Macro Compilation Error, this is pre-release software, please report this issue to the huff team in the discord" + ); return null; } - } /**Create compiled macro - * + * * Creates a huff file that imports all required macros and builds ONLY * the macro we want to test inside its runtime bytecode - * + * * @param {String} cwd The directory of the user's workspace * @param {String} macro The macro being tested * @param {Array} argsObject The args to push onto the stack * @param {String} currentFile The current file being debugged * @param {Array} imports The imports at the top of the file being debugged - * @returns + * @returns */ function createCompiledMacro(cwd, macro, argsObject, currentFile, imports) { // get relative path, remove "/:c" appended on windows routes const dirPath = currentFile.split("/").slice(0, -1).join("/"); - // flatten imports - const paths = imports.map(importPath => path.join(`${cwd}/${dirPath}`, importPath.replace(/#include\s?"/, "").replace('"', ""))); + // flatten imports + const paths = imports.map((importPath) => + path.join( + `${cwd}/${dirPath}`, + importPath.replace(/#include\s?"/, "").replace('"', "") + ) + ); paths.push(cwd + "/" + currentFile); - const files = paths.map(path => fs.readFileSync(path) - .toString() - .replace(/#define\s?macro\s?MAIN[\s\S]*?{[\s\S]*?}/gsm, "") // remove main - .replace(/#include\s".*"/gm, "") // remove include + const files = paths.map( + (path) => + fs + .readFileSync(path) + .toString() + .replace(/#define\s?macro\s?MAIN[\s\S]*?{[\s\S]*?}/gms, "") // remove main + .replace(/#include\s".*"/gm, "") // remove include ); // replace jump labels let macroBody = macro.body; const jumpLabelRegex = /<.*>/gm; if (jumpLabelRegex.test(macroBody)) { - console.log("jump label found in macro") + console.log("jump label found in macro"); macroBody = macroBody.replace(jumpLabelRegex, "error"); macroBody += "error:\n\t0x0 dup1 stop"; } @@ -115,10 +143,9 @@ ${files.join("\n")} ${macroBody} }`; - return compilableMacro + return compilableMacro; } - function runMacroDebugger(bytecode, runtimeBytecode, config, cwd) { // extract state const { @@ -129,7 +156,9 @@ function runMacroDebugger(bytecode, runtimeBytecode, config, cwd) { calldataChecked, calldataValue, storageChecked, - mountedDrive + callValueChecked, + callValue, + mountedDrive, } = config; const hevmCommand = `hevm exec \ @@ -137,45 +166,58 @@ function runMacroDebugger(bytecode, runtimeBytecode, config, cwd) { --address ${hevmContractAddress} \ --caller ${hevmCaller} \ --gas 0xffffffff \ - ${stateChecked || storageChecked ? "--state " + ((mountedDrive) ? ("/mnt/" + mountedDrive) : "") + cwd + "/" + statePath : ""} \ + ${ + stateChecked || storageChecked + ? "--state " + + (config.mountedDrive ? "/mnt/" + mountedDrive : "") + + cwd + + "/" + + statePath + : "" + } \ ${calldataChecked ? "--calldata " + formatEvenBytes(calldataValue) : ""} \ - --debug` + ${callValueChecked ? "--value " + callValue : ""} \ + --debug`; // command is cached into a file as execSync has a limit on the command size that it can execute - writeHevmCommand(hevmCommand, config.tempHevmCommandFilename, cwd) + writeHevmCommand(hevmCommand, config.tempHevmCommandFilename, cwd); // TODO: run the debugger - attach this to a running terminal - const terminalCommand = craftTerminalCommand(cwd, config) - // "`cat " + cwd + "/" + config.tempHevmCommandFilename + "`"; + const terminalCommand = craftTerminalCommand(cwd, config); + // "`cat " + cwd + "/" + config.tempHevmCommandFilename + "`"; runInUserTerminal(terminalCommand); } /**Override storage - * + * * As temp method this will override the storage using the contracts constructor * The long term goal is to convert this method to alter hevms store directly - * - * @param {String} macro - * @param {Object} config - * @returns + * + * @param {String} macro + * @param {Object} config + * @returns */ function overrideStorage(macro, config) { // write a temp file that will set storage slots const { stateValues } = config; - const constructorRegex = /#define\s+macro\s+CONSTRUCTOR\s?\((?[^\)]*)\)\s?=\s?takes\s?\((?[\d])\)\s?returns\s?\((?[\d])\)\s?{(?[\s\S]*?(?=}))/gsm + const constructorRegex = + /#define\s+macro\s+CONSTRUCTOR\s?\((?[^\)]*)\)\s?=\s?takes\s?\((?[\d])\)\s?returns\s?\((?[\d])\)\s?{(?[\s\S]*?(?=}))/gms; const constructorMatch = constructorRegex.exec(macro); // get string of sstore overrides let overrides = ""; for (const state of stateValues) { - overrides += `${state.value} ${state.key} sstore\n` + overrides += `${state.value} ${state.key} sstore\n`; } // if there is a constructor if (constructorMatch) { // append overrides to the end of the constructor macro - return macro.replace(constructorRegex, constructorMatch[0] + "\n\t" + overrides); + return macro.replace( + constructorRegex, + constructorMatch[0] + "\n\t" + overrides + ); } // otherwise create a constructor at the end @@ -187,7 +229,6 @@ function overrideStorage(macro, config) { return macro + content; } - module.exports = { - startMacroDebugger -} \ No newline at end of file + startMacroDebugger, +}; diff --git a/src/features/debugger/macro/macroDebuggerViewProvider.js b/src/features/debugger/macro/macroDebuggerViewProvider.js index cb65866..fc072f0 100644 --- a/src/features/debugger/macro/macroDebuggerViewProvider.js +++ b/src/features/debugger/macro/macroDebuggerViewProvider.js @@ -1,143 +1,175 @@ const vscode = require("vscode"); -const {getNonce, checkInputIsHex, checkCalldataIsHex} = require("../providerUtils"); -const {getMacros, getImports} = require("../../regexUtils"); -const {startMacroDebugger} = require("./macroDebugger"); -const { LANGUAGE_ID} = require("../../../settings"); - +const { + getNonce, + checkInputIsHex, + checkCalldataIsHex, +} = require("../providerUtils"); +const { getMacros, getImports } = require("../../regexUtils"); +const { startMacroDebugger } = require("./macroDebugger"); +const { LANGUAGE_ID } = require("../../../settings"); /**Macro Debugger View Provider - * + * * Macro level debug webview */ -class MacroDebuggerViewProvider{ - - static viewType = "huff.debugMacro"; - - constructor(extensionUri){ - this._extensionURI = extensionUri; - this._view = null; - this._macros = null; - this._currentFile = null; - } - - /** - * - * @param {*} webviewView - * @param {} context - * @param {*} _token - */ - resolveWebviewView( - webviewView, - context, - _token - ){ - // get the last active file - if (!this._currentFile && context && context.state) this._currentFile = context.state.currentFile || null; - if (!this._macros && context && context.state) this._macros = context.state.macros || null; - - webviewView.webview.options = { - enableScripts: true, - localResourceRoots: [ - this._extensionURI - ] +class MacroDebuggerViewProvider { + static viewType = "huff.debugMacro"; + + constructor(extensionUri) { + this._extensionURI = extensionUri; + this._view = null; + this._macros = null; + this._currentFile = null; + } + + /** + * + * @param {*} webviewView + * @param {} context + * @param {*} _token + */ + resolveWebviewView(webviewView, context, _token) { + // get the last active file + if (!this._currentFile && context && context.state) + this._currentFile = context.state.currentFile || null; + if (!this._macros && context && context.state) + this._macros = context.state.macros || null; + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this._extensionURI], + }; + + // Get macros from the file + vscode.workspace.onDidSaveTextDocument((document) => { + // check the file type and only run on the current file + if ( + document.languageId === LANGUAGE_ID && + document.uri.scheme === "file" && + document.uri.path === this._currentFile + ) { + this._macros = getMacros( + vscode.window.activeTextEditor?.document.getText() + ); + } + }); + + webviewView.webview.html = this.getHtmlForWebView(webviewView.webview); + + webviewView.webview.onDidReceiveMessage((data) => { + switch (data.type) { + case "loadMacros": { + const macros = getMacros( + vscode.window.activeTextEditor?.document.getText() + ); + const activeFile = vscode.window.activeTextEditor?.document.uri.path; + this._currentFile = activeFile; + this._macros = macros; + this.addMacrosToOptions(macros, activeFile); + break; } - - // Get macros from the file - vscode.workspace.onDidSaveTextDocument((document) => { - // check the file type and only run on the current file - if ( - document.languageId === LANGUAGE_ID - && document.uri.scheme === "file" - && document.uri.path === this._currentFile - ) { - this._macros = getMacros(vscode.window.activeTextEditor?.document.getText()); + case "start-macro-debug": { + const { + macro, + argsArr, + stateChecked, + calldataChecked, + calldataValue, + storageChecked, + stateValues, + callValueChecked, + callValue, + } = data.values; + + // If no macros are found then reload + if (!this._macros) + this._macros = getMacros( + vscode.window.activeTextEditor?.document.getText() + ); + const macroBody = this._macros[macro]; + + // Prevent debugging and show an error message if the stack inputs are not hex + if (!checkInputIsHex(argsArr)) break; + if (calldataChecked && !checkCalldataIsHex(calldataValue)) break; + + // get required file imports to flatten the file + const imports = getImports( + vscode.window.activeTextEditor?.document.getText() + ); + + startMacroDebugger( + vscode.workspace.getWorkspaceFolder( + vscode.window.activeTextEditor.document.uri + ).uri.path, + vscode.workspace.asRelativePath( + vscode.window.activeTextEditor.document.uri + ), + imports, + macroBody, + argsArr, + { + stateChecked, + calldataChecked, + calldataValue, + storageChecked, + stateValues, + callValueChecked, + callValue, } - }) - - webviewView.webview.html = this.getHtmlForWebView(webviewView.webview); - - webviewView.webview.onDidReceiveMessage(data => { - switch (data.type) { - case "loadMacros":{ - const macros = getMacros(vscode.window.activeTextEditor?.document.getText()); - const activeFile = vscode.window.activeTextEditor?.document.uri.path; - this._currentFile = activeFile; - this._macros = macros; - this.addMacrosToOptions(macros, activeFile); - break; - } - case "start-macro-debug": { - const { - macro, - argsArr, - stateChecked, - calldataChecked, - calldataValue, - storageChecked, - stateValues - } = data.values; - - // If no macros are found then reload - if (!this._macros) this._macros = getMacros(vscode.window.activeTextEditor?.document.getText()); - const macroBody = this._macros[macro]; - - // Prevent debugging and show an error message if the stack inputs are not hex - if (!checkInputIsHex(argsArr)) break; - if (calldataChecked &&!checkCalldataIsHex(calldataValue)) break; - - // get required file imports to flatten the file - const imports = getImports(vscode.window.activeTextEditor?.document.getText()) - - startMacroDebugger( - vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri).uri.path, - vscode.workspace.asRelativePath(vscode.window.activeTextEditor.document.uri), - imports, - macroBody, - argsArr, - { - stateChecked, - calldataChecked, - calldataValue, - storageChecked, - stateValues - }); - } - } - }); - - - this._view = webviewView; - } - - addMacrosToOptions(macros, currentFile){ - if (this._view){ - this._view.show?.(true); - this._view.webview.postMessage({ type: "receiveMacros", data: macros, currentFile }) + ); } + } + }); + + this._view = webviewView; + } + + addMacrosToOptions(macros, currentFile) { + if (this._view) { + this._view.show?.(true); + this._view.webview.postMessage({ + type: "receiveMacros", + data: macros, + currentFile, + }); } + } - updateSavedMacros(macros){ - if (this._view){ - this._view.show?.(true); - this._view.webview.postMessage({ type: "updateMacros", data: macros }) - } + updateSavedMacros(macros) { + if (this._view) { + this._view.show?.(true); + this._view.webview.postMessage({ type: "updateMacros", data: macros }); } - - getHtmlForWebView(webview) { - // local path of main script to run in the webview - - const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionURI, "webview", "macro", "macro.main.js")); - const helpersUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionURI, "webview", "helpers.js")); - - // Do the same for the stylesheet - const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionURI, "webview", "css", "vscode.css")); - const styleMainUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionURI, "webview", "css", "main.css")); - - // Use nonce to allow only a specific script to be run - const nonce = getNonce(); - - return ` + } + + getHtmlForWebView(webview) { + // local path of main script to run in the webview + + const scriptUri = webview.asWebviewUri( + vscode.Uri.joinPath( + this._extensionURI, + "webview", + "macro", + "macro.main.js" + ) + ); + const helpersUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionURI, "webview", "helpers.js") + ); + + // Do the same for the stylesheet + const styleVSCodeUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionURI, "webview", "css", "vscode.css") + ); + const styleMainUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionURI, "webview", "css", "main.css") + ); + + // Use nonce to allow only a specific script to be run + const nonce = getNonce(); + + return ` @@ -168,11 +200,15 @@ class MacroDebuggerViewProvider{
- + + + +
+ +
- @@ -186,11 +222,9 @@ class MacroDebuggerViewProvider{ `; - - } + } } - module.exports = { - MacroDebuggerViewProvider -} \ No newline at end of file + MacroDebuggerViewProvider, +}; diff --git a/webview/function/functions.main.js b/webview/function/functions.main.js index b354e0b..6c19b61 100644 --- a/webview/function/functions.main.js +++ b/webview/function/functions.main.js @@ -3,149 +3,195 @@ import { updateState } from "../helpers.js"; function cleanState(state) { - if (!state) state = {} - return { - functionSelectors: state.functionSelectors || {}, - selectedFunction: state.selectedFunction || null, - argsValues: state.argsValues || {} - }; + if (!state) state = {}; + return { + functionSelectors: state.functionSelectors || {}, + selectedFunction: state.selectedFunction || null, + argsValues: state.argsValues || {}, + showValue: state.showValue || false, + value: state.value || null, + }; } (function () { - const vscode = acquireVsCodeApi(); - const oldState = cleanState(vscode.getState()); - - // load in clean - updateState(vscode, oldState); - - /** @type {Array<{ value: string }>} */ - let functionSelectors = oldState.functionSelectors; - let selectedFunction = oldState.selectedFunction; - - // load in saved functionSelectors - addOptionsToFunctionSelector(functionSelectors, selectedFunction); - - // load in saved argsValues - - - document.querySelector(".load-interface").addEventListener("click", () => { - console.log("load interface clicked") - document.getElementById("function-select").innerHTML = ""; - vscode.postMessage({type: "loadDocument"}); + const vscode = acquireVsCodeApi(); + const oldState = cleanState(vscode.getState()); + + // load in clean + updateState(vscode, oldState); + + /** @type {Array<{ value: string }>} */ + let functionSelectors = oldState.functionSelectors; + let selectedFunction = oldState.selectedFunction; + + // load in saved functionSelectors + addOptionsToFunctionSelector(functionSelectors, selectedFunction); + + // load in saved argsValues + + // load in saved showValue + // Call value set + const callValueCheckbox = document.getElementById("call-value-checkbox"); + const callValueInput = document.getElementById("call-value-input"); + if (oldState.showValue) { + callValueInput.style.display = "block"; + callValueCheckbox.checked = true; + } else { + callValueInput.style.display = "none"; + callValueCheckbox.checked = false; + } + callValueInput.value = oldState.value; + + callValueCheckbox.addEventListener("click", (e) => { + e.target.checked + ? (callValueInput.style.display = "block") + : (callValueInput.style.display = "none"); + + updateState(vscode, { showValue: e.target.checked }); + }); + + // Register listeners + callValueInput.addEventListener("keypress", handleCallValueKeyPress); + callValueInput.addEventListener("change", handleCallValueKeyPress); + + document.querySelector(".load-interface").addEventListener("click", () => { + console.log("load interface clicked"); + document.getElementById("function-select").innerHTML = ""; + vscode.postMessage({ type: "loadDocument" }); + }); + + document.querySelector(".start-debug").addEventListener("click", () => { + prepareDebugSession(); + }); + + function prepareDebugSession() { + // Get the currently selected function selector + const ul = document.querySelector(".args-inputs"); + + // Get the current arguments to execute with + let argsArr = []; + ul.childNodes.forEach((node, i) => + node.childNodes.forEach((input) => + argsArr.push([selectedFunction[1].args[i], input.value]) + ) + ); + + // Pass call value if selected + let callValueChecked = document.getElementById( + "call-value-checkbox" + ).checked; + const callValue = document.getElementById("call-value-input").value; + if (callValue.length === 0) callValueChecked = false; + + // Send a message to the main extension to trigger the hevm session + vscode.postMessage({ + type: "start-debug", + values: { + selectedFunction, + argsArr, + callValueChecked, + callValue, + }, }); - - document.querySelector(".start-debug").addEventListener("click", () => { - prepareDebugSession(); - }) - - - function prepareDebugSession(){ - // Get the currently selected function selector - const ul = document.querySelector(".args-inputs"); - - // Get the current arguments to execute with - let argsArr = []; - ul.childNodes.forEach( - (node,i) => node.childNodes.forEach( - input => argsArr.push([selectedFunction[1].args[i], input.value]))) - - // Send a message to the main extension to trigger the hevm session - vscode.postMessage({type: "start-debug", values: { - selectedFunction, - argsArr - }}) + } + + // Handle messages sent from the extension to the webview + window.addEventListener("message", (event) => { + const message = event.data; // The json data that the extension sent + switch (message.type) { + case "receiveContractInterface": { + addOptionsToFunctionSelector(message.data, null); + break; + } + } + }); + + /**Add Options to function selector + * + * Each function selector defined within the interface will + * be a candidate for the debugger + * + * @param {*} functionSelectors + */ + function addOptionsToFunctionSelector(_functionSelectors, selectedFunction) { + functionSelectors = _functionSelectors; + updateState(vscode, { functionSelectors }); + + var functionSelectorDropdown = document.getElementById("function-select"); + + // listen for changes in the function that is selected + functionSelectorDropdown.addEventListener("click", (event) => + createArgsInputs(event) + ); + + // add each function as a drop down option + for (const fn in _functionSelectors) { + var option = document.createElement("option"); + option.text = _functionSelectors[fn].fnSig; + functionSelectorDropdown.add(option); } - - // Handle messages sent from the extension to the webview - window.addEventListener('message', event => { - const message = event.data; // The json data that the extension sent - switch (message.type) { - case 'receiveContractInterface': { - addOptionsToFunctionSelector(message.data, null) - break; - } - } - }); - - /**Add Options to function selector - * - * Each function selector defined within the interface will - * be a candidate for the debugger - * - * @param {*} functionSelectors - */ - function addOptionsToFunctionSelector(_functionSelectors, selectedFunction) { - functionSelectors = _functionSelectors - updateState(vscode,{functionSelectors}); - - var functionSelectorDropdown = document.getElementById("function-select"); - - // listen for changes in the function that is selected - functionSelectorDropdown.addEventListener("click", (event) => createArgsInputs(event)) - - // add each function as a drop down option - for (const fn in _functionSelectors){ - var option = document.createElement("option"); - option.text = _functionSelectors[fn].fnSig; - functionSelectorDropdown.add(option); - } - - if (selectedFunction) functionSelectorDropdown.value = selectedFunction[1].fnSig; - - functionSelectorDropdown.click(); + if (selectedFunction) + functionSelectorDropdown.value = selectedFunction[1].fnSig; + + functionSelectorDropdown.click(); + } + + function createArgsInputs(event) { + // empty clicks + if (!event.target.value) return; + + const entries = Object.entries(functionSelectors); + const funcProperties = entries.filter( + ([key, value]) => value.fnSig === event.target.value + )[0]; + + // store the whole object + selectedFunction = funcProperties; + updateState(vscode, { selectedFunction }); + + const ul = document.querySelector(".args-inputs"); + ul.textContent = ""; + + let i = 0; + for (const arg of funcProperties[1].args) { + const li = document.createElement("li"); + + const input = document.createElement("input"); + input.className = "arg-input"; + input.id = ++i; + + const state = vscode.getState(); + input.value = + state.argsValues && + state.argsValues[selectedFunction[0]] && + state.argsValues[selectedFunction[0]][i] + ? state.argsValues[selectedFunction[0]][i] + : arg; + + input.type = "text"; + input.addEventListener("input", (e) => { + const id = e.target.id; + input.value = e.target.value; + + // update the state to reflect the new value + const state = vscode.getState(); + state.argsValues[selectedFunction[0]] = + state.argsValues[selectedFunction[0]] || {}; + state.argsValues[selectedFunction[0]][id] = e.target.value; + vscode.setState(state); + }); + + // Add input field to list item + li.appendChild(input); + + // Add list item to the list + ul.appendChild(li); } + } - function createArgsInputs(event){ - // empty clicks - if (!event.target.value) return - - const entries = Object.entries(functionSelectors); - const funcProperties = entries.filter(([key, value]) => value.fnSig === event.target.value)[0]; - - // store the whole object - selectedFunction = funcProperties; - updateState(vscode, {selectedFunction}); - - const ul = document.querySelector(".args-inputs"); - ul.textContent = ""; - - let i = 0; - for (const arg of funcProperties[1].args){ - const li = document.createElement("li"); - - const input = document.createElement("input") - input.className = "arg-input"; - input.id = ++i; - - const state = vscode.getState() - input.value = ( - state.argsValues - && state.argsValues[selectedFunction[0]] - && state.argsValues[selectedFunction[0]][i] - ) ? - state.argsValues[selectedFunction[0]][i] - : arg; - - input.type = "text"; - input.addEventListener("input", (e)=> { - const id = e.target.id; - input.value = e.target.value; - - // update the state to reflect the new value - const state = vscode.getState(); - state.argsValues[selectedFunction[0]] = state.argsValues[selectedFunction[0]] || {}; - state.argsValues[selectedFunction[0]][id] = e.target.value; - vscode.setState(state); - }) - - - // Add input field to list item - li.appendChild(input) - - // Add list item to the list - ul.appendChild(li); - } - } - -}()); \ No newline at end of file + // --------------- helper functions --------------- // + function handleCallValueKeyPress(e) { + updateState(vscode, { value: e.target.value }); + } +})(); diff --git a/webview/macro/macro.main.js b/webview/macro/macro.main.js index d015576..4248fee 100644 --- a/webview/macro/macro.main.js +++ b/webview/macro/macro.main.js @@ -4,326 +4,360 @@ import { updateState } from "../helpers.js"; // defaults for each state variable function cleanState(state) { - if (!state) state = {} - return { - selectedMacro: state.selectedMacro || "", - macroDefinitions: state.macroDefinitions || {}, - stackValues: state.stackValues || {}, - showCalldata: state.showCalldata || false, - calldataValue: state.calldataValue || "", - runConstructorFirst: state.runConstructorFirst || false, - showState: state.showState || false, - stateValues: state.stateValues || {}, - currentFile: state.currentFile || "" - }; + if (!state) state = {}; + return { + selectedMacro: state.selectedMacro || "", + macroDefinitions: state.macroDefinitions || {}, + stackValues: state.stackValues || {}, + showCalldata: state.showCalldata || false, + calldataValue: state.calldataValue || "", + runConstructorFirst: state.runConstructorFirst || false, + showState: state.showState || false, + stateValues: state.stateValues || {}, + currentFile: state.currentFile || "", + showValue: state.showValue || false, + value: state.value || null, + }; } (function () { - - // --------------- Initialization --------------- // - const vscode = acquireVsCodeApi(); - const oldState = cleanState(vscode.getState()); - updateState(vscode, oldState); - - - /** @type {Array<{ value: string }>} */ - let macroDefinitions = oldState.macroDefinitions; - let selectedMacro = oldState.selectedMacro; - - - // ----------------- Load previous state ----------------- // - // load previously stored macros - addOptionsToMacroSelector(macroDefinitions, selectedMacro); - - const calldataInput = document.getElementById("calldata-input"); - const calldataCheckbox = document.getElementById("calldata-checkbox"); - - if (oldState.showCalldata) { - calldataInput.style.display = "block" - calldataCheckbox.checked = true; + // --------------- Initialization --------------- // + const vscode = acquireVsCodeApi(); + const oldState = cleanState(vscode.getState()); + updateState(vscode, oldState); + + /** @type {Array<{ value: string }>} */ + let macroDefinitions = oldState.macroDefinitions; + let selectedMacro = oldState.selectedMacro; + + // ----------------- Load previous state ----------------- // + // load previously stored macros + addOptionsToMacroSelector(macroDefinitions, selectedMacro); + + const calldataInput = document.getElementById("calldata-input"); + const calldataCheckbox = document.getElementById("calldata-checkbox"); + + if (oldState.showCalldata) { + calldataInput.style.display = "block"; + calldataCheckbox.checked = true; + } else { + calldataInput.style.display = "none"; + calldataCheckbox.checked = false; + } + + calldataCheckbox.addEventListener("click", (e) => { + e.target.checked + ? (calldataInput.style.display = "block") + : (calldataInput.style.display = "none"); + + updateState(vscode, { showCalldata: e.target.checked }); + }); + + // Set calldatavalue to saved value + calldataInput.value = oldState.calldataValue; + + // Call value set + const callValueCheckbox = document.getElementById("call-value-checkbox"); + const callValueInput = document.getElementById("call-value-input"); + if (oldState.showValue) { + callValueInput.style.display = "block"; + callValueCheckbox.checked = true; + } else { + callValueInput.style.display = "none"; + callValueCheckbox.checked = false; + } + callValueInput.value = oldState.value; + + callValueCheckbox.addEventListener("click", (e) => { + e.target.checked + ? (callValueInput.style.display = "block") + : (callValueInput.style.display = "none"); + + updateState(vscode, { showValue: e.target.checked }); + }); + + // Is state overrides set + const stateCheckbox = document.getElementById("storage-checkbox"); + const storageList = document.getElementById("storage-overrides"); + const addSlotButton = document.getElementById("add-slot"); + if (oldState.showState) { + stateCheckbox.checked = true; + storageList.style.display = "block"; + addSlotButton.style.display = "block"; + } else { + stateCheckbox.checked = false; + storageList.style.display = "none"; + addSlotButton.style.display = "none"; + } + + // Set storage overrides to saved values + Object.keys(oldState.stateValues).map((id) => { + const { key, value } = oldState.stateValues[id]; + renderStateSetter(id, key, value); + }); + + // ----------------- Event Handlers ----------------- // + document.querySelector(".load-macro").addEventListener("click", () => { + // clear the current macro button + document.getElementById("macro-select").innerHTML = ""; + vscode.postMessage({ type: "loadMacros" }); + }); + + document.querySelector(".start-debug").addEventListener("click", () => { + prepareDebugSession(); + }); + + stateCheckbox.addEventListener("click", (e) => { + if (e.target.checked) { + storageList.style.display = "block"; + addSlotButton.style.display = "block"; } else { - calldataInput.style.display = "none" - calldataCheckbox.checked = false; + storageList.style.display = "none"; + addSlotButton.style.display = "none"; } - calldataCheckbox.addEventListener("click", (e) => { - e.target.checked - ? calldataInput.style.display = "block" - : calldataInput.style.display = "none" - - updateState(vscode, { showCalldata: e.target.checked }); - }) - - // Set calldatavalue to saved value - calldataInput.value = oldState.calldataValue; - - // Is state overrides set - const stateCheckbox = document.getElementById("storage-checkbox"); - const storageList = document.getElementById("storage-overrides"); - const addSlotButton = document.getElementById("add-slot"); - if (oldState.showState) { - stateCheckbox.checked = true; - storageList.style.display = "block"; - addSlotButton.style.display = "block"; - } else { - stateCheckbox.checked = false; - storageList.style.display = "none"; - addSlotButton.style.display = "none"; + updateState(vscode, { showState: e.target.checked }); + }); + + // Whenever the user clicks the add state button + addSlotButton.addEventListener("click", (e) => { + const id = Math.random().toString(); + + renderStateSetter(id, false, false); + }); + + calldataInput.addEventListener("keypress", handleCalldataKeyPress); + calldataInput.addEventListener("change", handleCalldataKeyPress); + callValueInput.addEventListener("keypress", handleCallValueKeyPress); + callValueInput.addEventListener("change", handleCallValueKeyPress); + + // ----------------- Message Handlers ----------------- // + // Handle messages sent from the extension to the webview + window.addEventListener("message", (event) => { + const message = event.data; // The json data that the extension sent + switch (message.type) { + case "receiveMacros": { + // Add options to the macro selector + addOptionsToMacroSelector(message.data, null); + + // save the currently edited file + updateState(vscode, { currentFile: message.currentFile }); + break; + } + case "updateMacros": { + updateMacros(message.data); + break; + } } - - // Set storage overrides to saved values - Object.keys(oldState.stateValues).map(id => { - const { key, value } = oldState.stateValues[id]; - renderStateSetter(id, key, value); - }) - - // ----------------- Event Handlers ----------------- // - document.querySelector(".load-macro") - .addEventListener("click", () => { - // clear the current macro button - document.getElementById("macro-select").innerHTML = ""; - vscode.postMessage({ type: "loadMacros" }); - }); - - document.querySelector(".start-debug") - .addEventListener("click", () => { - prepareDebugSession(); - }) - - stateCheckbox.addEventListener("click", (e) => { - if (e.target.checked) { - storageList.style.display = "block"; - addSlotButton.style.display = "block"; - } else { - storageList.style.display = "none" - addSlotButton.style.display = "none"; - } - - updateState(vscode, { showState: e.target.checked }); - }) - - // Whenever the user clicks the add state button - addSlotButton.addEventListener("click", (e) => { - const id = Math.random().toString(); - - renderStateSetter(id, false, false) - }); - - calldataInput.addEventListener("keypress", handleKeyPress); - calldataInput.addEventListener("change", handleKeyPress); - - - // ----------------- Message Handlers ----------------- // - // Handle messages sent from the extension to the webview - window.addEventListener('message', event => { - const message = event.data; // The json data that the extension sent - switch (message.type) { - case 'receiveMacros': { - // Add options to the macro selector - addOptionsToMacroSelector(message.data, null); - - // save the currently edited file - updateState(vscode, { currentFile: message.currentFile }); - break; - } - case 'updateMacros': { - updateMacros(message.data); - break; - } - } - }); - - - // ----------------- Rendering ----------------- // - // Render a state setter widget - function renderStateSetter(id, key, value) { - const newSlot = document.createElement("div"); - newSlot.id = id; - - newSlot.className = "storage-slot"; - newSlot.innerHTML = ` - state.stateValues[id]) : []; - - // Send a message to the main extension to trigger the hevm session - vscode.postMessage({ - type: "start-macro-debug", values: { - macro: selectedMacro, - argsArr, - stateChecked, - calldataChecked, - calldataValue, - storageChecked, - stateValues - } - }) - } - - - - - /**Add Options to function selector - * - * Each function selector defined within the interface will - * be a candidate for the debugger - * - * @param {*} macroDefinitions - * @param {String} selectedMacro - */ - function addOptionsToMacroSelector(_macroDefinitions, selectedMacro) { - // TODO: make function for this - updateState(vscode, { macroDefinitions: _macroDefinitions }); - macroDefinitions = _macroDefinitions - var functionSelectorDropdown = document.getElementById("macro-select"); - - // listen for changes in the function that is selected - functionSelectorDropdown.addEventListener("click", (event) => createStackInputs(event)); - - // add each function as a drop down option - for (const macro of Object.keys(_macroDefinitions)) { - var option = document.createElement("option"); - option.text = macro; - functionSelectorDropdown.add(option); - } - - if (selectedMacro) functionSelectorDropdown.value = selectedMacro; - - // select the first macro - functionSelectorDropdown.click(); - } - - /**Update Macro Conditions - * - * Update the saved macro conditions so that they track across vscode saves - * @param {Object} _macroDefinitions - */ - function updateMacros(_macroDefinitions) { - macroDefinitions = _macroDefinitions; - updateState(vscode, { macroDefinitions: _macroDefinitions }); + state.stateValues[id] = { + key: e.target.value, + value: newSlot.querySelector(".storage-value-input").value, + }; + vscode.setState(state); + }); + newSlot + .querySelector(".storage-value-input") + .addEventListener("input", (e) => { + // make setting state better + const state = vscode.getState(); + state.stateValues[id] = { + key: newSlot.querySelector(".storage-key-input").value, + value: e.target.value, + }; + vscode.setState(state); + }); + + storageList.appendChild(newSlot); + } + + function prepareDebugSession() { + // Get the currently selected function selector + const ul = document.querySelector(".stack-items"); + const state = vscode.getState(); + + // Get the current arguments to execute with + let argsArr = []; + ul.childNodes.forEach((node) => + node.childNodes.forEach((input) => argsArr.push(input.value)) + ); + + // TODO: reintroduce button - set as false for the meantime + const stateChecked = false; + + // Allow macro's to be spoofed with calldata + const calldataChecked = + document.querySelector(".calldata-checkbox").checked; + const calldataValue = document.getElementById("calldata-input").value; + + // Get the state overrides if there are any + const storageChecked = document.getElementById("storage-checkbox").checked; + const stateValues = state.stateValues + ? Object.keys(state.stateValues).map((id) => state.stateValues[id]) + : []; + + // Pass call value if selected + let callValueChecked = document.getElementById( + "call-value-checkbox" + ).checked; + const callValue = document.getElementById("call-value-input").value; + if (callValue.length === 0) callValueChecked = false; + + // Send a message to the main extension to trigger the hevm session + vscode.postMessage({ + type: "start-macro-debug", + values: { + macro: selectedMacro, + argsArr, + stateChecked, + calldataChecked, + calldataValue, + storageChecked, + stateValues, + callValueChecked, + callValue, + }, + }); + } + + /**Add Options to function selector + * + * Each function selector defined within the interface will + * be a candidate for the debugger + * + * @param {*} macroDefinitions + * @param {String} selectedMacro + */ + function addOptionsToMacroSelector(_macroDefinitions, selectedMacro) { + // TODO: make function for this + updateState(vscode, { macroDefinitions: _macroDefinitions }); + macroDefinitions = _macroDefinitions; + var functionSelectorDropdown = document.getElementById("macro-select"); + + // listen for changes in the function that is selected + functionSelectorDropdown.addEventListener("click", (event) => + createStackInputs(event) + ); + + // add each function as a drop down option + for (const macro of Object.keys(_macroDefinitions)) { + var option = document.createElement("option"); + option.text = macro; + functionSelectorDropdown.add(option); } - /**Create stack inputs - * - * Render stack inputs based on selected macro - * @param event - */ - function createStackInputs(event) { - selectedMacro = event.target.value; - - // TODO: make function for this - updateState(vscode, { selectedMacro: selectedMacro }); - - const macroIfo = macroDefinitions[selectedMacro]; - if (!macroIfo) return; // return if initializing a new file - - const ul = document.querySelector(".stack-items"); - ul.textContent = ""; - for (let i = 1; i <= macroIfo.takes; i++) { - const li = document.createElement("li"); - - const input = document.createElement("input") - input.className = "arg-input"; - input.type = "text"; - input.id = i; - - // give saved value or default - const state = vscode.getState(); - input.value = ( - state.stackValues[selectedMacro] - && state.stackValues[selectedMacro][i] - ) ? - state.stackValues[selectedMacro][i] - : i; - - - // allow for user input to the stack items - input.addEventListener("input", (e) => { - const id = e.target.id; - input.value = e.target.value; - - // Update the state to reflect the change - const state = vscode.getState(); - state.stackValues[selectedMacro] = state.stackValues[selectedMacro] || {}; - state.stackValues[selectedMacro][id] = e.target.value; - vscode.setState(state); - }) - - - // Add input field to list item - li.appendChild(input) - - // Add list item to the list - ul.appendChild(li); - } - } + if (selectedMacro) functionSelectorDropdown.value = selectedMacro; + + // select the first macro + functionSelectorDropdown.click(); + } + + /**Update Macro Conditions + * + * Update the saved macro conditions so that they track across vscode saves + * @param {Object} _macroDefinitions + */ + function updateMacros(_macroDefinitions) { + macroDefinitions = _macroDefinitions; + updateState(vscode, { macroDefinitions: _macroDefinitions }); + } + + /**Create stack inputs + * + * Render stack inputs based on selected macro + * @param event + */ + function createStackInputs(event) { + selectedMacro = event.target.value; + + // TODO: make function for this + updateState(vscode, { selectedMacro: selectedMacro }); + + const macroIfo = macroDefinitions[selectedMacro]; + if (!macroIfo) return; // return if initializing a new file + + const ul = document.querySelector(".stack-items"); + ul.textContent = ""; + for (let i = 1; i <= macroIfo.takes; i++) { + const li = document.createElement("li"); + + const input = document.createElement("input"); + input.className = "arg-input"; + input.type = "text"; + input.id = i; + + // give saved value or default + const state = vscode.getState(); + input.value = + state.stackValues[selectedMacro] && state.stackValues[selectedMacro][i] + ? state.stackValues[selectedMacro][i] + : i; + + // allow for user input to the stack items + input.addEventListener("input", (e) => { + const id = e.target.id; + input.value = e.target.value; + + // Update the state to reflect the change + const state = vscode.getState(); + state.stackValues[selectedMacro] = + state.stackValues[selectedMacro] || {}; + state.stackValues[selectedMacro][id] = e.target.value; + vscode.setState(state); + }); + // Add input field to list item + li.appendChild(input); - // --------------- helper functions --------------- // - // Handle entering calldata - function handleKeyPress(e) { - updateState(vscode, { calldataValue: e.target.value }); + // Add list item to the list + ul.appendChild(li); } - -}()); \ No newline at end of file + } + + // --------------- helper functions --------------- // + // Handle entering calldata + function handleCalldataKeyPress(e) { + updateState(vscode, { calldataValue: e.target.value }); + } + + function handleCallValueKeyPress(e) { + updateState(vscode, { value: e.target.value }); + } +})();