From 5601c66dce8960fa9e2da647124a72c3d6a3ab35 Mon Sep 17 00:00:00 2001 From: Cheethas <47148561+cheethas@users.noreply.github.com> Date: Fri, 2 Sep 2022 23:22:39 +0100 Subject: [PATCH] feat: Support debugging with call value --- src/features/debugger/function/debugger.js | 194 +++--- .../function/functionDebuggerViewProvider.js | 211 +++--- src/features/debugger/macro/macroDebugger.js | 245 ++++--- .../macro/macroDebuggerViewProvider.js | 305 +++++---- webview/function/functions.main.js | 322 +++++---- webview/macro/macro.main.js | 642 +++++++++--------- 6 files changed, 1066 insertions(+), 853 deletions(-) diff --git a/src/features/debugger/function/debugger.js b/src/features/debugger/function/debugger.js index 65a28e1..b048882 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; @@ -28,82 +44,93 @@ async function startDebugger(sourceDirectory, currentFile, imports, functionSele mountedDrive = wslMountedDriveRegex.exec(sourceDirectory)?.groups?.drive; sourceDirectory = sourceDirectory.replace(`/mnt/${mountedDrive}`, ""); } - const cwd = sourceDirectory.replace("/c:",""); - + const cwd = sourceDirectory.replace("/c:", ""); + // Create deterministic deployment address for each contract for the deployed contract const config = { - ...hevmConfig, + ...hevmConfig, + ...options, hevmContractAddress: createKeccakHash("keccak256") .update(Buffer.from(currentFile)) .digest("hex") .toString("hex") - .slice(0,42), + .slice(0, 42), stateChecked: true, - mountedDrive - } - + mountedDrive, + }; + // Get calldata const calldata = await encodeCalldata(functionSelector, argsArray); - + // Flatten file to prevent the need to file linking -> this will be required for a wasm implementation 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){ +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, "") - }) + fs.readFileSync(cwd + "/" + currentFile).toString(), + ...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,51 @@ function runDebugger(bytecode, calldata, flags, config, cwd) { --address ${config.hevmContractAddress} \ --caller ${config.hevmCaller} \ --gas 0xffffffff \ - --state ${((isWsl) ? ("/mnt/" + config.mountedDrive) : "") + cwd + "/" + config.statePath} \ + --state ${ + (isWsl ? "/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) + writeHevmCommand(hevmCommand, config.tempHevmCommandFilename, cwd); + 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...") - try { - if (argsObject.length == 0) return `0x${functionSelector[0]}`; - - // TODO: error handle with user prompts - const abiEncoder = new AbiCoder() - - // create interface readable by the abi encoder - let type = []; - let value = []; - 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)}` - - } catch (e){ - registerError(e, `Compilation failed\nSee\n${e}`); - } +async function encodeCalldata(functionSelector, argsObject) { + console.log("Preparing debugger calldata..."); + try { + if (argsObject.length == 0) return `0x${functionSelector[0]}`; + + // TODO: error handle with user prompts + const abiEncoder = new AbiCoder(); + + // create interface readable by the abi encoder + let type = []; + let value = []; + 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)}`; + } 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 86e5fbd..9a3bb6a 100644 --- a/src/features/debugger/macro/macroDebugger.js +++ b/src/features/debugger/macro/macroDebugger.js @@ -1,183 +1,225 @@ -const createKeccakHash = require('keccak'); +const createKeccakHash = require("keccak"); const fs = require("fs"); const { hevmConfig } = require("../../../options"); const path = require("path"); -const { - deployContract, - runInUserTerminal, - writeHevmCommand, - registerError, - compileFromFile, - checkInstallations, +const { + deployContract, + runInUserTerminal, + writeHevmCommand, + registerError, + compileFromFile, + checkInstallations, formatEvenBytes, - craftTerminalCommand, + craftTerminalCommand, } = 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){ - if (!(await checkInstallations())) return; - - // Remove /c: appended to source directory on windows systems - nodes file system apis absolutely hate it - let mountedDrive = null; - if (isWsl) { - mountedDrive = wslMountedDriveRegex.exec(sourceDirectory)?.groups?.drive; - sourceDirectory = sourceDirectory.replace(`/mnt/${mountedDrive}`, ""); - } - const cwd = sourceDirectory.replace("/c:","") - - try { - // Create deterministic deployment address for each contract - const config = { - ...hevmConfig, - ...options, - mountedDrive, - hevmContractAddress: createKeccakHash("keccak256") - .update(Buffer.from(macro.toString())) - .digest("hex") - .toString("hex") - .slice(0,42), - } +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 + let mountedDrive = null; + if (isWsl) { + mountedDrive = wslMountedDriveRegex.exec(sourceDirectory)?.groups?.drive; + sourceDirectory = sourceDirectory.replace(`/mnt/${mountedDrive}`, ""); + } + const cwd = sourceDirectory.replace("/c:", ""); + + try { + // Create deterministic deployment address for each contract + const config = { + ...hevmConfig, + ...options, + mountedDrive, + hevmContractAddress: createKeccakHash("keccak256") + .update(Buffer.from(macro.toString())) + .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); + if (config.storageChecked) + compilableMacro = overrideStorage(compilableMacro, config); - const bytecode = compileFromFile(compilableMacro, config.tempMacroFilename, cwd); + 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"); - return null; - } - + } catch (e) { + 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('"', ""))); - 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 - ); + // 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('"', "") + ) + ); + paths.push(cwd + "/" + currentFile); + 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"); + macroBody = macroBody.replace(jumpLabelRegex, "error"); + macroBody += "error:\n\t0x0 dup1 stop"; + } - // replace jump labels - let macroBody = macro.body; - const jumpLabelRegex = /<.*>/gm; - if (jumpLabelRegex.test(macroBody)) { - console.log("jump label found in macro") - macroBody = macroBody.replace(jumpLabelRegex, "error"); - macroBody += "error:\n\t0x0 dup1 stop"; - } - - // Main contains the debugable macro - const compilableMacro = ` + // Main contains the debugable macro + const compilableMacro = ` ${files.join("\n")} #define macro MAIN() = takes(0) returns (0) { ${argsObject.reverse().join(" ")} ${macroBody} }`; - return compilableMacro + return compilableMacro; } - function runMacroDebugger(bytecode, runtimeBytecode, config, cwd) { // extract state - const { - stateChecked, - hevmContractAddress, - hevmCaller, - statePath, + const { + stateChecked, + hevmContractAddress, + hevmCaller, + statePath, calldataChecked, calldataValue, storageChecked, - mountedDrive - } = config; - + callValueChecked, + callValue, + mountedDrive, + } = config; + const hevmCommand = `hevm exec \ --code ${runtimeBytecode.toString()} \ --address ${hevmContractAddress} \ --caller ${hevmCaller} \ --gas 0xffffffff \ - ${stateChecked || storageChecked ? "--state " + ((isWsl) ? ("/mnt/" + mountedDrive) : "") + cwd + "/" + statePath : ""} \ + ${ + stateChecked || storageChecked + ? "--state " + + (isWsl ? "/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) + // command is cached into a file as execSync has a limit on the command size that it can execute + writeHevmCommand(hevmCommand, config.tempHevmCommandFilename, cwd); - // TODO: run the debugger - attach this to a running terminal - const terminalCommand = craftTerminalCommand(cwd, config) - // "`cat " + cwd + "/" + config.tempHevmCommandFilename + "`"; - runInUserTerminal(terminalCommand); + // TODO: run the debugger - attach this to a running terminal + 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 { 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` + for (const state of stateValues) { + 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 let content = ""; content = "\n#define macro CONSTRUCTOR() = takes(0) returns(0) {\n\t"; @@ -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..53529d6 100644 --- a/src/features/debugger/macro/macroDebuggerViewProvider.js +++ b/src/features/debugger/macro/macroDebuggerViewProvider.js @@ -1,143 +1,176 @@ 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, + calldataValue, + 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 +201,15 @@ class MacroDebuggerViewProvider{
- + + + +
+ +
- @@ -186,11 +223,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 }); + } +})();