truffle
Version:
Truffle - Simple development framework for Ethereum
1,556 lines (1,408 loc) • 68.1 kB
JavaScript
#!/usr/bin/env node
exports.id = 458;
exports.ids = [458];
exports.modules = {
/***/ 458:
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
const debugModule = __webpack_require__(15158);
const debug = debugModule("lib:debug:cli");
const fs = __webpack_require__(55674);
const path = __webpack_require__(71017);
const Debugger = __webpack_require__(92851);
const DebugUtils = __webpack_require__(93293);
const Codec = __webpack_require__(20102);
const { fetchAndCompileForDebugger } = __webpack_require__(5523);
const { DebugInterpreter } = __webpack_require__(79311);
const { DebugCompiler } = __webpack_require__(55887);
const Spinner = (__webpack_require__(92189).Spinner);
class CLIDebugger {
constructor(config, { compilations, txHash } = {}) {
this.config = config;
this.compilations = compilations;
this.txHash = txHash;
}
async run() {
this.config.logger.log("Starting Truffle Debugger...");
const session = await this.connect();
// initialize prompt/breakpoints/ui logic
const interpreter = await this.buildInterpreter(session);
return interpreter;
}
async connect() {
// get compilations (either by shimming compiled artifacts,
// or by doing a recompile)
const compilations = this.compilations || (await this.getCompilations());
// invoke @truffle/debugger
const session = await this.startDebugger(compilations);
return session;
}
async fetchExternalSources(bugger) {
const fetchSpinner = new Spinner(
"core:debug:cli:fetch",
"Getting and compiling external sources..."
);
const {
fetch: badAddresses,
fetchers: badFetchers,
compile: badCompilationAddresses
} = await fetchAndCompileForDebugger(bugger, this.config); //Note: mutates bugger!!
if (
badAddresses.length === 0 &&
badFetchers.length === 0 &&
badCompilationAddresses.length === 0
) {
fetchSpinner.succeed();
} else {
let warningStrings = [];
if (badFetchers.length > 0) {
warningStrings.push(
`Errors occurred connecting to ${badFetchers.join(", ")}.`
);
}
if (badAddresses.length > 0) {
warningStrings.push(
`Errors occurred while getting sources for addresses ${badAddresses.join(
", "
)}.`
);
}
if (badCompilationAddresses.length > 0) {
warningStrings.push(
`Errors occurred while compiling sources for addresses ${badCompilationAddresses.join(
", "
)}.`
);
}
// simulate ora's "warn" feature
fetchSpinner.warn(warningStrings.join(" "));
}
}
async getCompilations() {
//if compileNone is true and configFileSkiped
//we understand that user is debugging using --url and does not have a config file
//so instead of resolving compilations, we return an empty value
if (this.config.compileNone && this.config.configFileSkipped) {
return [];
}
let artifacts;
artifacts = await this.gatherArtifacts();
if ((artifacts && !this.config.compileAll) || this.config.compileNone) {
let shimmedCompilations =
Codec.Compilations.Utils.shimArtifacts(artifacts);
//if they were compiled simultaneously, yay, we can use it!
//(or if we *force* it to...)
if (
this.config.compileNone ||
shimmedCompilations.every(DebugUtils.isUsableCompilation)
) {
debug("shimmed compilations usable");
return shimmedCompilations;
}
debug("shimmed compilations unusable");
}
//if not, or if build directory doesn't exist, we have to recompile
return await this.compileSources();
}
async compileSources() {
const compileSpinner = new Spinner(
"core:debug:cli:compile",
"Compiling your contracts..."
);
const compilationResult = await new DebugCompiler(this.config).compile({
withTests: this.config.compileTests
});
debug("compilationResult: %O", compilationResult);
compileSpinner.succeed();
return Codec.Compilations.Utils.shimCompilations(compilationResult);
}
async startDebugger(compilations) {
const startMessage = DebugUtils.formatStartMessage(
this.txHash !== undefined
);
const specifiedRegistry = this.config.noEns ? null : this.config.registry; //specified at the command line
const registry =
specifiedRegistry !== undefined
? specifiedRegistry
: this.config.ensRegistry?.address;
let bugger;
if (!this.config.fetchExternal) {
//ordinary case, not doing fetch-external
const startSpinner = new Spinner("core:debug:cli:start", startMessage);
bugger = await Debugger.forProject({
provider: this.config.provider,
compilations,
ens: { registryAddress: registry }
});
if (this.txHash !== undefined) {
try {
debug("loading %s", this.txHash);
await bugger.load(this.txHash);
startSpinner.succeed();
} catch (_) {
debug("loading error");
startSpinner.fail();
//just start up unloaded
}
} else {
startSpinner.succeed();
}
} else {
//fetch-external case
//note that in this case we start in light mode
//and only wake up to full mode later!
//also, in this case, we can be sure that txHash is defined
bugger = await Debugger.forTx(this.txHash, {
provider: this.config.provider,
compilations,
ens: { registryAddress: registry },
lightMode: true
}); //note: may throw!
await this.fetchExternalSources(bugger); //note: mutates bugger!
const startSpinner = new Spinner("core:debug:cli:start", startMessage);
await bugger.startFullMode();
//I'm removing the failure check here because I don't think that can
//actually happen
startSpinner.succeed();
}
return bugger;
}
async buildInterpreter(session) {
return new DebugInterpreter(this.config, session, this.txHash);
}
async gatherArtifacts() {
// Gather all available contract artifacts
// if build directory doesn't exist, return undefined to signal that
// a recompile is necessary
if (!fs.existsSync(this.config.contracts_build_directory)) {
return undefined;
}
const files = fs.readdirSync(this.config.contracts_build_directory);
let contracts = files
.filter(filePath => {
return path.extname(filePath) === ".json";
})
.map(filePath => {
return path.basename(filePath, ".json");
})
.map(contractName => {
return this.config.resolver.require(contractName);
});
await Promise.all(
contracts.map(abstraction => abstraction.detectNetwork())
);
return contracts;
}
}
module.exports = {
CLIDebugger
};
/***/ }),
/***/ 55887:
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
const WorkflowCompile = (__webpack_require__(37017)["default"]);
const { Resolver } = __webpack_require__(48511);
const glob = __webpack_require__(12884);
const path = __webpack_require__(71017);
class DebugCompiler {
constructor(config) {
this.config = config;
}
async compile({ withTests }) {
let compileConfig = this.config.with({ quiet: true });
if (withTests) {
const testResolver = new Resolver(this.config);
const testFiles = glob
.sync(`${this.config.test_directory}/**/*.sol`)
.map(filePath => path.resolve(filePath));
compileConfig = compileConfig.with({
resolver: testResolver,
//note we only need to pass *additional* files
files: testFiles
});
}
const { compilations } = await WorkflowCompile.compile(
compileConfig.with({ all: true })
);
return compilations;
}
}
module.exports = {
DebugCompiler
};
/***/ }),
/***/ 79311:
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
const debugModule = __webpack_require__(15158);
const debug = debugModule("lib:debug:interpreter");
const path = __webpack_require__(71017);
const util = __webpack_require__(73837);
const DebugUtils = __webpack_require__(93293);
const selectors = (__webpack_require__(92851).selectors);
const { session, sourcemapping, stacktrace, trace, evm, controller } =
selectors;
const analytics = __webpack_require__(95614);
const repl = __webpack_require__(38102);
const { DebugPrinter } = __webpack_require__(29099);
const Spinner = (__webpack_require__(92189).Spinner);
function watchExpressionAnalytics(raw) {
if (raw.includes("!<")) {
//don't send analytics for watch expressions involving selectors
return;
}
let expression = raw.trim();
//legal Solidity identifiers (= legal JS identifiers)
let identifierRegex = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
let isVariable = expression.match(identifierRegex) !== null;
analytics.send({
command: "debug: watch expression",
args: { isVariable }
});
}
class DebugInterpreter {
constructor(config, session, txHash) {
this.session = session;
this.network = config.network;
this.fetchExternal = config.fetchExternal;
this.printer = new DebugPrinter(config, session);
this.txHash = txHash;
this.lastCommand = "n";
this.enabledExpressions = new Set();
this.repl = null;
}
async setOrClearBreakpoint(args, setOrClear) {
const breakpoints = this.determineBreakpoints(args); //note: not pure, can print
if (breakpoints !== null) {
for (const breakpoint of breakpoints) {
await this.setOrClearBreakpointObject(breakpoint, setOrClear);
}
} else {
//null is a special value representing all, we'll handle it separately
if (setOrClear) {
// only "B all" is legal, not "b all"
this.printer.print("Cannot add breakpoint everywhere.");
} else {
await this.session.removeAllBreakpoints();
this.printer.print("Removed all breakpoints.");
}
}
}
//NOTE: not pure, also prints!
//returns an array of the breakpoints, unless it's remove all breakpoints,
//in which case it returns null
//(if something goes wrong it will return [] to indicate do nothing)
determineBreakpoints(args) {
const currentLocation = this.session.view(controller.current.location);
const currentStart = currentLocation.sourceRange
? currentLocation.sourceRange.start
: null;
const currentLength = currentLocation.sourceRange
? currentLocation.sourceRange.length
: null;
const currentSourceId = currentLocation.source
? currentLocation.source.id
: null;
const currentLine =
currentSourceId !== null && currentSourceId !== undefined
? //sourceRange is never null, so we go by whether currentSourceId is null/undefined
currentLocation.sourceRange.lines.start.line
: null;
if (args.length === 0) {
//no arguments, want currrent node
debug("node case");
if (currentSourceId === null) {
this.printer.print("Cannot determine current location.");
return [];
}
return [
{
start: currentStart,
line: currentLine, //this isn't necessary for the
//breakpoint to work, but we use it for printing messages
length: currentLength,
sourceId: currentSourceId
}
];
}
//the special case of "B all"
else if (args[0] === "all") {
return null;
}
//if the argument starts with a "+" or "-", we have a relative
//line number
else if (args[0][0] === "+" || args[0][0] === "-") {
debug("relative case");
if (currentLine === null) {
this.printer.print("Cannot determine current location.");
return [];
}
let delta = parseInt(args[0], 10); //want an integer
debug("delta %d", delta);
if (isNaN(delta)) {
this.printer.print("Offset must be an integer.");
return [];
}
return [
{
sourceId: currentSourceId,
line: currentLine + delta
}
];
}
//if it contains a colon, it's in the form source:line
else if (args[0].includes(":")) {
debug("source case");
let sourceArgs = args[0].split(":");
let sourceArg = sourceArgs[0];
let lineArg = sourceArgs[1];
debug("sourceArgs %O", sourceArgs);
//first let's get the line number as usual
let line = parseInt(lineArg, 10); //want an integer
if (isNaN(line)) {
this.printer.print("Line number must be an integer.");
return [];
}
//search sources for given string
let sources = Object.values(
this.session.view(sourcemapping.views.sources)
);
//we will indeed need the sources here, not just IDs
let matchingSources = sources.filter(source =>
source.sourcePath.includes(sourceArg)
);
if (matchingSources.length === 0) {
this.printer.print(`No source file found matching ${sourceArg}.`);
return [];
} else if (matchingSources.length > 1) {
//normally if there's multiple matching sources, we want to return no
//breakpoint and print a disambiguation prompt.
//however, if one of them has a source path that is a substring of all
//the others...
if (
matchingSources.some(shortSource =>
matchingSources.every(
source =>
typeof source.sourcePath !== "string" || //just ignore these I guess?
source.sourcePath.includes(shortSource.sourcePath)
)
)
) {
//exceptional case
this.printer.print(
`WARNING: Acting on all matching sources because disambiguation between them is not possible.`
);
return matchingSources.map(source => ({
sourceId: source.id,
line: line - 1 //adjust for breakpoint!
}));
} else {
//normal case
this.printer.print(
`Multiple source files found matching ${sourceArg}. Which did you mean?`
);
matchingSources.forEach(source =>
this.printer.print(source.sourcePath)
);
this.printer.print("");
return [];
}
}
//otherwise, we found it!
return [
{
sourceId: matchingSources[0].id,
line: line - 1 //adjust for zero-indexing!
}
];
}
//otherwise, it's a simple line number
else {
debug("absolute case");
if (currentSourceId === null || currentSourceId === undefined) {
this.printer.print("Cannot determine current file.");
return [];
}
let line = parseInt(args[0], 10); //want an integer
debug("line %d", line);
if (isNaN(line)) {
this.printer.print("Line number must be an integer.");
return [];
}
return [
{
sourceId: currentSourceId,
line: line - 1 //adjust for zero-indexing!
}
];
}
}
//note: also prints!
async setOrClearBreakpointObject(breakpoint, setOrClear) {
const existingBreakpoints = this.session.view(controller.breakpoints);
//OK, we've constructed the breakpoint! But if we're adding, we'll
//want to adjust to make sure we don't set it on an empty line or
//anything like that
if (setOrClear) {
let resolver = this.session.view(controller.breakpoints.resolver);
breakpoint = resolver(breakpoint);
//of course, this might result in finding that there's nowhere to
//add it after that point
if (breakpoint === null) {
this.printer.print(
"Nowhere to add breakpoint at or beyond that location."
);
return;
}
}
const currentSource = this.session.view(controller.current.location.source);
const currentSourceId = currentSource ? currentSource.id : null;
//having constructed and adjusted breakpoint, here's now a
//user-readable message describing its location
let sources = this.session.view(sourcemapping.views.sources);
let sourceNames = Object.assign(
//note: only include user sources
{},
...Object.entries(sources).map(([id, source]) => ({
[id]: path.basename(source.sourcePath)
}))
);
let locationMessage = DebugUtils.formatBreakpointLocation(
breakpoint,
true, //only relevant for node-based breakpoints
currentSourceId,
sourceNames
);
//one last check -- does this breakpoint already exist?
let alreadyExists =
existingBreakpoints.filter(
existingBreakpoint =>
existingBreakpoint.sourceId === breakpoint.sourceId &&
existingBreakpoint.line === breakpoint.line &&
existingBreakpoint.node === breakpoint.node //may be undefined
).length > 0;
//NOTE: in the "set breakpoint" case, the above check is somewhat
//redundant, as we're going to check again when we actually make the
//call to add or remove the breakpoint! But we need to check here so
//that we can display the appropriate message. Hopefully we can find
//some way to avoid this redundant check in the future.
//if it already exists and is being set, or doesn't and is being
//cleared, report back that we can't do that
if (setOrClear === alreadyExists) {
if (setOrClear) {
this.printer.print(`Breakpoint at ${locationMessage} already exists.`);
return;
} else {
this.printer.print(`No breakpoint at ${locationMessage} to remove.`);
return;
}
}
//finally, if we've reached this point, do it!
//also report back to the user on what happened
if (setOrClear) {
await this.session.addBreakpoint(breakpoint);
this.printer.print(`Breakpoint added at ${locationMessage}.`);
} else {
await this.session.removeBreakpoint(breakpoint);
this.printer.print(`Breakpoint removed at ${locationMessage}.`);
}
}
start(terminate) {
// if terminate is not passed, return a Promise instead
if (terminate === undefined) {
return util.promisify(this.start.bind(this))();
}
if (this.session.view(session.status.loaded)) {
debug("loaded");
this.printer.printSessionLoaded();
} else if (this.session.view(session.status.isError)) {
debug("error!");
this.printer.printSessionError();
} else {
debug("didn't attempt a load");
this.printer.printHelp();
}
const prompt = this.session.view(session.status.loaded)
? DebugUtils.formatPrompt(this.network, this.txHash)
: DebugUtils.formatPrompt(this.network);
this.repl = repl.start({
prompt: prompt,
eval: util.callbackify(this.interpreter.bind(this)),
ignoreUndefined: true,
done: terminate
});
}
async interpreter(cmd) {
cmd = cmd.trim();
let cmdArgs, splitArgs;
debug("cmd %s", cmd);
if (cmd === ".exit") {
cmd = "q";
}
//split arguments for commands that want that; split on runs of spaces
splitArgs = cmd.trim().split(/ +/).slice(1);
debug("splitArgs %O", splitArgs);
//warning: this bit *alters* cmd!
if (cmd.length > 0) {
cmdArgs = cmd.slice(1).trim();
cmd = cmd[0];
}
if (cmd === "") {
cmd = this.lastCommand;
cmdArgs = "";
splitArgs = [];
}
//quit if that's what we were given
if (cmd === "q") {
process.exit();
}
let alreadyFinished = this.session.view(trace.finishedOrUnloaded);
let loadFailed = false;
// If not finished, perform commands that require state changes
// (other than quitting or resetting)
if (!alreadyFinished) {
const stepSpinner = new Spinner(
"core:debug:interpreter:step",
"Stepping..."
);
switch (cmd) {
case "o":
await this.session.stepOver();
break;
case "i":
await this.session.stepInto();
break;
case "u":
await this.session.stepOut();
break;
case "n":
await this.session.stepNext();
break;
case ";":
//two cases -- parameterized and unparameterized
if (cmdArgs !== "") {
let count = parseInt(cmdArgs, 10);
debug("cmdArgs=%s", cmdArgs);
if (isNaN(count)) {
this.printer.print("Number of steps must be an integer.");
break;
}
await this.session.advance(count);
} else {
await this.session.advance();
}
break;
case "c":
await this.session.continueUntilBreakpoint();
break;
}
stepSpinner.remove();
} //otherwise, inform the user we can't do that
else {
switch (cmd) {
case "o":
case "i":
case "u":
case "n":
case "c":
case ";":
//are we "finished" because we've reached the end, or because
//nothing is loaded?
if (this.session.view(session.status.loaded)) {
this.printer.print("Transaction has halted; cannot advance.");
this.printer.print("");
} else {
this.printer.print("No transaction loaded.");
this.printer.print("");
}
}
}
if (cmd === "r") {
//reset if given the reset command
//(but not if nothing is loaded)
if (this.session.view(session.status.loaded)) {
await this.session.reset();
} else {
this.printer.print("No transaction loaded.");
this.printer.print("");
}
}
if (cmd === "y") {
if (this.session.view(session.status.loaded)) {
if (this.session.view(trace.finished)) {
if (!this.session.view(evm.current.step.isExceptionalHalting)) {
const errorIndex = this.session.view(
stacktrace.current.innerErrorIndex
);
if (errorIndex !== null) {
const stepSpinner = new Spinner(
"core:debug:interpreter:step",
"Stepping..."
);
await this.session.reset();
await this.session.advance(errorIndex);
stepSpinner.remove();
} else {
this.printer.print("No error to return to.");
}
} else {
this.printer.print("You are already at the final error.");
this.printer.print(
"Use the `Y` command to return to the previous error."
);
this.printer.print("");
}
} else {
this.printer.print(
"This command is only usable at end of transaction; did you mean `Y`?"
);
}
} else {
this.printer.print("No transaction loaded.");
this.printer.print("");
}
}
if (cmd === "Y") {
if (this.session.view(session.status.loaded)) {
const errorIndex = this.session.view(
stacktrace.current.innerErrorIndex
);
if (errorIndex !== null) {
const stepSpinner = new Spinner(
"core:debug:interpreter:step",
"Stepping..."
);
await this.session.reset();
await this.session.advance(errorIndex);
stepSpinner.remove();
} else {
this.printer.print("No previous error to return to.");
}
} else {
this.printer.print("No transaction loaded.");
this.printer.print("");
}
}
if (cmd === "t") {
if (!this.fetchExternal) {
if (!this.session.view(session.status.loaded)) {
const txSpinner = new Spinner(
"core:debug:interpreter:step",
DebugUtils.formatTransactionStartMessage()
);
try {
await this.session.load(cmdArgs);
txSpinner.succeed();
this.repl.setPrompt(DebugUtils.formatPrompt(this.network, cmdArgs));
} catch (_) {
txSpinner.fail();
loadFailed = true;
}
} else {
loadFailed = true;
this.printer.print(
"Please unload the current transaction before loading a new one."
);
}
} else {
loadFailed = true;
this.printer.print(
"Cannot change transactions in fetch-external mode. Please quit and restart the debugger instead."
);
}
}
if (cmd === "T") {
if (!this.fetchExternal) {
if (this.session.view(session.status.loaded)) {
await this.session.unload();
this.printer.print("Transaction unloaded.");
this.repl.setPrompt(DebugUtils.formatPrompt(this.network));
} else {
this.printer.print("No transaction to unload.");
this.printer.print("");
}
} else {
this.printer.print(
"Cannot change transactions in fetch-external mode. Please quit and restart the debugger instead."
);
}
}
if (cmd === "g") {
if (!this.session.view(controller.stepIntoInternalSources)) {
this.session.setInternalStepping(true);
this.printer.print(
"All debugger commands can now step into generated sources."
);
} else {
this.printer.print("Generated sources already activated.");
}
}
if (cmd === "G") {
if (this.session.view(controller.stepIntoInternalSources)) {
this.session.setInternalStepping(false);
this.printer.print(
"Commands other than (;) and (c) will now skip over generated sources."
);
} else {
this.printer.print("Generated sources already off.");
}
}
// Check if execution has (just now) stopped.
if (this.session.view(trace.finished) && !alreadyFinished) {
this.printer.print("");
//check if transaction failed
if (!this.session.view(evm.transaction.status)) {
await this.printer.printRevertMessage();
this.printer.print("");
this.printer.printStacktrace(true); //final stacktrace
this.printer.print("");
this.printer.printErrorLocation();
} else {
//case if transaction succeeded
this.printer.print("Transaction completed successfully.");
if (
this.session.view(sourcemapping.current.source).language !== "Vyper"
) {
//HACK: not supported for vyper yet
await this.printer.printReturnValue();
}
}
}
// Perform post printing
// (we want to see if execution stopped before printing state).
switch (cmd) {
case "+":
if (cmdArgs[0] === ":") {
watchExpressionAnalytics(cmdArgs.substring(1));
}
this.enabledExpressions.add(cmdArgs);
await this.printer.printWatchExpressionResult(cmdArgs);
break;
case "-":
this.enabledExpressions.delete(cmdArgs);
break;
case "!":
this.printer.printSelector(cmdArgs);
break;
case "?":
this.printer.printWatchExpressions(this.enabledExpressions);
this.printer.printBreakpoints();
this.printer.printGeneratedSourcesState();
break;
case "v":
if (
this.session.view(sourcemapping.current.source).language === "Vyper"
) {
this.printer.print(
"Decoding of variables is not currently supported for Vyper."
);
break;
}
//first: process which sections we should print out
const tempPrintouts = this.updatePrintouts(
splitArgs,
this.printer.sections,
this.printer.sectionPrintouts
);
await this.printer.printVariables(tempPrintouts);
if (this.session.view(trace.finished)) {
await this.printer.printReturnValue();
}
break;
case "e":
if (cmdArgs) {
const eventsCount = parseInt(cmdArgs);
if (!isNaN(eventsCount) && eventsCount > 0) {
this.printer.eventsCount = eventsCount;
} else if (cmdArgs === "all") {
this.printer.eventsCount = Infinity;
} else {
this.printer.print(
'Invalid event count given, must be positive integer or "all"'
);
break;
}
}
if (this.session.view(session.status.loaded)) {
this.printer.printEvents();
} else {
this.printer.print("No transaction loaded to print events for.");
}
break;
case ":":
watchExpressionAnalytics(cmdArgs);
this.printer.evalAndPrintExpression(cmdArgs);
break;
case "b":
await this.setOrClearBreakpoint(splitArgs, true);
break;
case "B":
await this.setOrClearBreakpoint(splitArgs, false);
break;
case "p":
// determine the numbers of instructions to be printed
this.printer.instructionLines = this.parsePrintoutLines(
splitArgs,
this.printer.instructionLines
);
// process which locations we should print out
const temporaryPrintouts = this.updatePrintouts(
splitArgs,
this.printer.locations,
this.printer.locationPrintouts
);
if (this.session.view(session.status.loaded)) {
if (this.session.view(trace.steps).length > 0) {
this.printer.printInstruction(temporaryPrintouts);
this.printer.printFile();
this.printer.printState();
} else {
//if there are no trace steps, let's just print a warning message
this.printer.print("No trace steps to inspect.");
}
}
//finally, print watch expressions
await this.printer.printWatchExpressionsResults(
this.enabledExpressions
);
break;
case "l":
if (this.session.view(session.status.loaded)) {
this.printer.printFile();
// determine the numbers of lines to be printed
this.printer.sourceLines = this.parsePrintoutLines(
splitArgs,
this.printer.sourceLines
);
this.printer.printState(
this.printer.sourceLines.beforeLines,
this.printer.sourceLines.afterLines
);
}
break;
case ";":
if (!this.session.view(trace.finishedOrUnloaded)) {
this.printer.printInstruction();
this.printer.printFile();
this.printer.printState();
}
await this.printer.printWatchExpressionsResults(
this.enabledExpressions
);
break;
case "s":
if (this.session.view(session.status.loaded)) {
//print final report if finished & failed, intermediate if not
if (
this.session.view(trace.finished) &&
!this.session.view(evm.transaction.status)
) {
this.printer.printStacktrace(true); //print final stack trace
//Now: actually show the point where things went wrong
this.printer.printErrorLocation(
this.printer.sourceLines.beforeLines,
this.printer.sourceLines.afterLines
);
} else {
this.printer.printStacktrace(false); //intermediate call stack
}
}
break;
case "o":
case "i":
case "u":
case "n":
case "c":
case "y":
case "Y":
if (!this.session.view(trace.finishedOrUnloaded)) {
if (!this.session.view(sourcemapping.current.source).source) {
this.printer.printInstruction();
}
this.printer.printFile();
this.printer.printState();
}
await this.printer.printWatchExpressionsResults(
this.enabledExpressions
);
break;
case "r":
if (this.session.view(session.status.loaded)) {
this.printer.printAddressesAffected();
this.printer.warnIfNoSteps();
this.printer.printFile();
this.printer.printState();
}
break;
case "t":
if (!loadFailed) {
this.printer.printAddressesAffected();
this.printer.warnIfNoSteps();
this.printer.printFile();
this.printer.printState();
} else if (this.session.view(session.status.isError)) {
let loadError = this.session.view(session.status.error);
this.printer.print(loadError);
}
break;
case "T":
case "g":
case "G":
//nothing to print
break;
default:
this.printer.printHelp(this.lastCommand);
}
const nonRepeatableCommands = "bBvhpl?!:+r-tTgGsye";
if (!nonRepeatableCommands.includes(cmd)) {
this.lastCommand = cmd;
}
}
// update the printouts according to user inputs
// called by case v for section printouts and case p for location printouts
//
// NOTE: THIS FUNCTION IS NOT PURE.
// The input printOuts is altered according to the values of other inputs: userArgs and selections.
// The function returns an object, tempPrintouts, that contains the selected printouts.
updatePrintouts(userArgs, selections, printOuts) {
let tempPrintouts = new Set();
for (let argument of userArgs) {
let fullSelection;
if (argument[0] === "+" || argument[0] === "-") {
fullSelection = argument.slice(1);
} else {
fullSelection = argument;
}
let selection = selections.find(possibleSelection =>
fullSelection.startsWith(possibleSelection)
);
if (argument[0] === "+") {
printOuts.add(selection);
} else if (argument[0] === "-") {
printOuts.delete(selection);
} else {
tempPrintouts.add(selection);
}
}
for (let selection of printOuts) {
debug("selection: %s", selection);
tempPrintouts.add(selection);
}
return tempPrintouts;
}
// parse the numbers of lines options -<num>|+<num> from user args
parsePrintoutLines(userArgs, currentLines) {
let { beforeLines, afterLines } = currentLines;
for (const argument of userArgs) {
// ignore an option with length less than 2,such as a bare + or -
if (argument.length < 2) continue;
const newLines = Number(argument.slice(1));
// ignore the arguments that are not of the correct form, number
if (isNaN(newLines)) continue;
if (argument[0] === "-") {
beforeLines = newLines;
} else if (argument[0] === "+") {
afterLines = newLines;
}
}
return { beforeLines, afterLines };
}
}
module.exports = {
DebugInterpreter
};
/***/ }),
/***/ 29099:
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
const debugModule = __webpack_require__(15158);
const debug = debugModule("lib:debug:printer");
const path = __webpack_require__(71017);
const util = __webpack_require__(73837);
const OS = __webpack_require__(22037);
const DebugUtils = __webpack_require__(93293);
const Codec = __webpack_require__(20102);
const colors = __webpack_require__(83196);
const Interpreter = __webpack_require__(97941);
const selectors = (__webpack_require__(92851).selectors);
const {
session,
sourcemapping,
trace,
controller,
data,
txlog,
evm,
stacktrace
} = selectors;
class DebugPrinter {
constructor(config, session) {
this.config = config;
this.session = session;
this.select = expr => {
let selector, result;
try {
selector = expr
.split(".")
.reduce((sel, next) => (next.length ? sel[next] : sel), selectors);
} catch (_) {
throw new Error("Unknown selector: %s", expr);
}
// throws its own exception
// note: we avoid using this.session so that this
// can be called from js-interpreter
result = session.view(selector);
return result;
};
const colorizeSourceObject = source => {
const { source: raw, language } = source;
const detabbed = DebugUtils.tabsToSpaces(raw);
return DebugUtils.colorize(detabbed, language);
};
this.colorizedSources = Object.assign(
{},
...Object.entries(this.session.view(sourcemapping.views.sources)).map(
([id, source]) => ({
[id]: colorizeSourceObject(source)
})
)
);
// location printouts for command (p): print instruction and state
// sto: Storage
// cal: Calldata
// mem: Memory
// sta: Stack
// Note that this is a public variable and can be modified from outside.
this.locationPrintouts = new Set(["sta"]);
this.locations = ["sto", "cal", "mem", "sta"]; //should remain constant
// section printouts for command (v): print variables and values
// bui: Solidity built-ins
// glo: Global constants
// con: Contract variables
// loc: Local variables
// Note that this is a public variable and can be modified from outside.
this.sectionPrintouts = new Set(["bui", "glo", "con", "loc"]);
this.sections = ["bui", "glo", "con", "loc"]; //should remain constant
// numbers of instructions before and after the current instruction to be printed
// used by commands (p) and (;)
// Note that this is a public variable and can be modified from outside.
this.instructionLines = { beforeLines: 3, afterLines: 3 };
// numbers of lines before and after the current line to be printed
// used by commands (l) and (s)
// Note that this is a public variable and can be modified from outside.
this.sourceLines = { beforeLines: 5, afterLines: 3 };
//number of previous events to print. this is a public variable, it may be
//modified from outside.
this.eventsCount = 3;
}
print(...args) {
this.config.logger.log(...args);
}
printSessionLoaded() {
this.printAddressesAffected();
this.warnIfNoSteps();
this.printHelp();
debug("Help printed");
this.printFile();
debug("File printed");
this.printState();
debug("State printed");
}
printSessionError() {
this.print(this.session.view(session.status.error));
this.printHelp();
}
printAddressesAffected() {
const affectedInstances = this.session.view(session.info.affectedInstances);
this.config.logger.log("");
this.config.logger.log("Addresses affected:");
this.config.logger.log(
DebugUtils.formatAffectedInstances(affectedInstances)
);
}
warnIfNoSteps() {
if (this.session.view(trace.steps).length === 0) {
this.config.logger.log(
`${colors.bold(
"Warning:"
)} this transaction has no trace steps. This may happen if you are attempting to debug a transaction sent to an externally-owned account, or if the node you are connecting to failed to produce a trace for some reason. Please check your configuration and try again.`
);
}
}
printHelp(lastCommand) {
this.config.logger.log("");
this.config.logger.log(DebugUtils.formatHelp(lastCommand));
}
printFile(location = this.session.view(controller.current.location)) {
let message = "";
const sourcePath = location.source.sourcePath;
if (sourcePath) {
message += path.basename(sourcePath);
} else {
message += "?";
}
this.config.logger.log("");
this.config.logger.log(message + ":");
}
printState(
contextBefore = 2,
contextAfter = 0,
location = this.session.view(controller.current.location)
) {
const {
source: { id: sourceId },
sourceRange: range
} = location;
if (sourceId === undefined) {
this.config.logger.log();
this.config.logger.log("1: // No source code found.");
this.config.logger.log("");
return;
}
//we don't just get extract the source text from the location because passed-in location may be
//missing the source text
const source = this.session.view(sourcemapping.views.sources)[sourceId]
.source;
const colorizedSource = this.colorizedSources[sourceId];
debug("range: %o", range);
// We were splitting on OS.EOL, but it turns out on Windows,
// in some environments (perhaps?) line breaks are still denoted by just \n
const splitLines = str => str.split(/\r?\n/g);
const lines = splitLines(source);
const colorizedLines = splitLines(colorizedSource);
this.config.logger.log("");
// We create printoutRange with range.lines as initial value for printing.
let printoutRange = range.lines;
// We print a warning message and display the end of source code when the
// instruction's byte-offset to the start of the range in the source code
// is past the end of source code.
if (range.start >= source.length) {
this.config.logger.log(
`${colors.bold(
"Warning:"
)} Location is past end of source, displaying end.`
);
this.config.logger.log("");
// We set the printoutRange with the end of source code.
// Note that "lines" is the split lines of source code as defined above.
printoutRange = {
start: {
line: lines.length - 1,
column: 0
},
end: {
line: lines.length - 1,
column: 0
}
};
}
//HACK -- the line-pointer formatter doesn't work right with colorized
//lines, so we pass in the uncolored version too
this.config.logger.log(
DebugUtils.formatRangeLines(
colorizedLines,
printoutRange,
lines,
contextBefore,
contextAfter
)
);
this.config.logger.log("");
}
printInstruction(locations = this.locationPrintouts) {
const instruction = this.session.view(sourcemapping.current.instruction);
const instructions = this.session.view(sourcemapping.current.instructions);
const step = this.session.view(trace.step);
const traceIndex = this.session.view(trace.index);
const totalSteps = this.session.view(trace.steps).length;
//note calldata will be a Uint8Array, not a hex string or array of such
const calldata = this.session.view(data.current.state.calldata);
//storage here is an object mapping hex words to hex words, all w/o 0x prefix
const storage = this.session.view(evm.current.codex.storage);
this.config.logger.log("");
if (locations.has("sto")) {
this.config.logger.log(DebugUtils.formatStorage(storage));
this.config.logger.log("");
}
if (locations.has("cal")) {
this.config.logger.log(DebugUtils.formatCalldata(calldata));
this.config.logger.log("");
}
if (locations.has("mem")) {
this.config.logger.log(DebugUtils.formatMemory(step.memory));
this.config.logger.log("");
}
if (locations.has("sta")) {
this.config.logger.log(DebugUtils.formatStack(step.stack));
this.config.logger.log("");
}
this.config.logger.log("Instructions:");
if (!instruction || instruction.pc === undefined) {
// printout warning message if the debugger does not have the code for this contract
this.config.logger.log(
`${colors.bold(
"Warning:"
)} The debugger does not have the code for this contract.`
);
} else {
// printout instructions
const previousInstructions = this.instructionLines.beforeLines;
const upcomingInstructions = this.instructionLines.afterLines;
const currentIndex = instruction.index;
// add an ellipse if there exist additional instructions before
if (currentIndex - previousInstructions > 0) {
this.config.logger.log("...");
}
// printout 3 previous instructions
for (
let i = Math.max(currentIndex - previousInstructions, 0);
i < currentIndex;
i++
) {
this.config.logger.log(DebugUtils.formatInstruction(instructions[i]));
}
// printout current instruction
this.config.logger.log(DebugUtils.formatCurrentInstruction(instruction));
// printout 3 upcoming instructions
for (
let i = currentIndex + 1;
i <=
Math.min(currentIndex + upcomingInstructions, instructions.length - 1);
i++
) {
this.config.logger.log(DebugUtils.formatInstruction(instructions[i]));
}
// add an ellipse if there exist additional instructions after
if (currentIndex + upcomingInstructions < instructions.length - 1) {
this.config.logger.log("...");
}
}
this.config.logger.log("");
this.config.logger.log(
"Step " + (traceIndex + 1).toString() + "/" + totalSteps.toString()
);
this.config.logger.log(step.gas + " gas remaining");
}
/**
* @param {string} selector
*/
printSelector(selector) {
const result = this.select(selector);
const debugSelector = debugModule(selector);
debugSelector.enabled = true;
debugSelector("%O", result);
}
printWatchExpressions(expressions) {
if (expressions.size === 0) {
this.config.logger.log("No watch expressions added.");
return;
}
this.config.logger.log("");
for (const expression of expressions) {
this.config.logger.log(" " + expression);
}
}
printBreakpoints() {
const sources = this.session.view(sourcemapping.views.sources);
const sourceNames = Object.assign(
//note: only include user sources
{},
...Object.entries(sources).map(([id, source]) => ({
[id]: path.basename(source.sourcePath)
}))
);
const breakpoints = this.session.view(controller.breakpoints);
if (breakpoints.length > 0) {
for (let breakpoint of this.session.view(controller.breakpoints)) {
let currentLocation = this.session.view(controller.current.location);
let locationMessage = DebugUtils.formatBreakpointLocation(
breakpoint,
currentLocation.node !== undefined &&
breakpoint.sourceId === currentLocation.source.sourceId &&
breakpoint.node === currentLocation.astRef,
currentLocation.source.id,
sourceNames
);
this.config.logger.log(" Breakpoint at " + locationMessage);
}
} else {
this.config.logger.log("No breakpoints added.");
}
}
printGeneratedSourcesState() {
if (this.session.view(controller.stepIntoInternalSources)) {
this.config.logger.log("Generated sources are turned on.");
} else {
this.config.logger.log("Generated sources are turned off.");
}
}
//this doesn't really *need* to be async as we could use codec directly, but, eh
async printRevertMessage() {
this.config.logger.log(
DebugUtils.truffleColors.red("Transaction halted with a RUNTIME ERROR.")
);
this.config.logger.log("");
const revertDecodings = await this.session.returnValue(); //in this context we know it's a revert
debug("revertDecodings: %o", revertDecodings);
switch (revertDecodings.length) {
case 0:
this.config.logger.log(
"There was revert data, but it could not be decoded."
);
break;
case 1:
const revertDecoding = revertDecodings[0];
switch (revertDecoding.kind) {
case "failure":
this.config.logger.log(
"There was no revert message. This may be due to an intentional halting expression, such as assert(), revert(), or require(), or could be due to an unintentional exception such as out-of-gas exceptions."
);
break;
case "revert":
const signature = Codec.AbiData.Utils.abiSignature(
revertDecoding.abi
);
switch (signature) {
case "Error(string)":
const revertStringInfo =
revertDecoding.arguments[0].value.value;
let revertString;
switch (revertStringInfo.kind) {
case "valid":
revertString = revertStringInfo.asString;
this.config.logger.log(`Revert message: ${revertString}`);
break;
case "malformed":
//turn into a JS string while smoothing over invalid UTF-8
//slice 2 to remove 0x prefix
revertString = Buffer.from(
revertStringInfo.asHex.slice(2),
"hex"
).toString();
this.config.logger.log(`Revert message: ${revertString}`);
this.config.logger.log(
`${colors.bold(
"Warning:"
)} This message contained invalid UTF-8.`
);
break;
}
break;
case "Panic(uint256)":
const panicCode = revertDecoding.arguments[0].value.value.asBN;
const panicString = DebugUtils.panicString(panicCode, true); //get verbose panic string :)
this.config.logger.log(
`Panic: Code 0x${panicCode.toString(
16
)}. This code indicates that ${panicString.toLowerCase()}`
);
break;
default:
this.config.logger.log("The following error was thrown:");
this.config.logger.log(
DebugUtils.formatCustomError(revertDecoding, 2)
);
}
break;
}
break;
default:
this.config.logger.log(
"There was revert data, but it could not be unambiguously decoded."