"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Debugger = void 0;
const sentio_provider_1 = require("./sentio-provider");
const Codec = __importStar(require("@truffle/codec"));
const helper_1 = require("./helper");
const Interpreter = require("js-interpreter");
const DebugUtils = require("@truffle/debug-utils");
const truffle_debugger = require("@sentio/truffle-debugger");
const selectors = require("@sentio/truffle-debugger").selectors;
let { data, evm, trace, session, sourcemapping, stacktrace, controller, txlog } = selectors;
class Debugger {
    constructor() {
        this.sectionPrintouts = new Set(["bui", "glo", "con", "loc"]);
        this.initialized = false;
    }
    static get Instance() {
        return this._instance || (this._instance = new this());
    }
    initialize(init_arguments) {
        return __awaiter(this, void 0, void 0, function* () {
            this.provider = new sentio_provider_1.SentioProvider(init_arguments.providerURL, { keepAlive: false });
            yield this.provider.init(init_arguments.txnHash, init_arguments.chainId);
            this.truffle_debugger = yield truffle_debugger.forTx(init_arguments.txnHash, {
                provider: this.provider,
                compilations: init_arguments.shimmedCompilations,
                lightMode: true,
                storageLookup: init_arguments.storageLookup,
            });
            yield this.truffle_debugger.startFullMode();
            this.initialized = true;
        });
    }
    reset() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.truffle_debugger.view(session.status.loaded)) {
                yield this.truffle_debugger.reset();
            }
        });
    }
    stepOver() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.truffle_debugger.stepOver();
        });
    }
    stepInto() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.truffle_debugger.stepInto();
        });
    }
    stepOut() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.truffle_debugger.stepOut();
        });
    }
    stepNext() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.truffle_debugger.stepNext();
        });
    }
    advance(steps) {
        return __awaiter(this, void 0, void 0, function* () {
            if (steps) {
                yield this.truffle_debugger.advance(steps);
            }
            else {
                yield this.truffle_debugger.advance();
            }
        });
    }
    continueUntilBreakpoint() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.truffle_debugger.continueUntilBreakpoint();
        });
    }
    addBreakpoint(breakpoint) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.truffle_debugger.addBreakpoint(this.determinBreakPoint(breakpoint));
        });
    }
    removeBreakpoint(breakpoint) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.truffle_debugger.removeBreakpoint(this.determinBreakPoint(breakpoint));
        });
    }
    removeAllBreakpoints() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.truffle_debugger.removeAllBreakpoints();
        });
    }
    runToEnd() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.truffle_debugger.runToEnd();
        });
    }
    variables() {
        return __awaiter(this, void 0, void 0, function* () {
            const values = yield this.truffle_debugger.variables({ indicateUnknown: true });
            const sections = this.truffle_debugger.view(data.current.identifiers.sections);
            const sectionVars = {
                builtin: [],
                global: [],
                contract: [],
                local: []
            };
            for (const [section, variables] of Object.entries(sections)) {
                // only check the first 3 characters of each name given in the input sectionPrintouts
                // since each section name defined in the constructor contains 3 characters
                const includeThisSection = this.sectionPrintouts.has(section.slice(0, 3));
                //@ts-ignore
                if (includeThisSection && variables.length > 0) {
                    //@ts-ignore
                    for (const variable of variables) {
                        const value = values[variable];
                        //@ts-ignore
                        sectionVars[section].push({
                            name: variable,
                            raw: value
                        });
                    }
                }
            }
            return sectionVars;
        });
    }
    stackTrace() {
        let final = false;
        if (this.truffle_debugger.view(session.status.loaded)) {
            //print final report if finished & failed, intermediate if not
            if (this.truffle_debugger.view(trace.finished) &&
                !this.truffle_debugger.view(evm.transaction.status)) {
                final = true; //print final stack trace
            }
            else {
                final = false; //intermediate call stack
            }
        }
        let report = final
            ? this.truffle_debugger.view(stacktrace.current.finalReport)
            : this.truffle_debugger.view(stacktrace.current.report);
        return report;
    }
    currentLocation(session) {
        var _a, _b;
        session = session || this.truffle_debugger;
        if (session.view(trace.finishedOrUnloaded)) {
            return undefined;
        }
        const currentLocation = session.view(controller.current.location);
        const currentLines = currentLocation.sourceRange
            ? currentLocation.sourceRange.lines
            : null;
        const currentLength = currentLocation.sourceRange
            ? currentLocation.sourceRange.length
            : null;
        const currentSourceId = currentLocation.source
            ? currentLocation.source.id
            : null;
        const index = session.view(trace.index);
        // if there's no source
        if (currentSourceId === undefined) {
            return {
                lines: currentLines,
                length: currentLength,
                instructionIndex: index,
            };
        }
        //search sources for given string
        let sources = Object.values(this.truffle_debugger.view(sourcemapping.views.sources));
        //we will indeed need the sources here, not just IDs
        let matchingSources = sources.filter(source => 
        // @ts-ignore
        source.id === currentSourceId);
        return {
            // @ts-ignore
            compilationId: (_a = matchingSources[0]) === null || _a === void 0 ? void 0 : _a.compilationId,
            lines: currentLines,
            length: currentLength,
            // @ts-ignore
            sourcePath: (_b = matchingSources[0]) === null || _b === void 0 ? void 0 : _b.sourcePath,
            instructionIndex: index,
        };
    }
    currentStep() {
        return this.truffle_debugger.view(trace.step);
    }
    terminate() {
        return __awaiter(this, void 0, void 0, function* () {
            this.truffle_debugger = null;
            this.initialized = false;
        });
    }
    getCallTrace() {
        var _a, _b, _c, _d;
        return __awaiter(this, void 0, void 0, function* () {
            const bugger = this.truffle_debugger;
            yield bugger.resetTrace();
            let finished;
            let functionDepth = 0;
            let currentCallDepth = 1;
            let prevOp;
            let prevLocation;
            let prevPc;
            let depthCallMap = new Map();
            const filter = (op) => {
                return op.includes("JUMP") || ["CALL", "STATICCALL", "DELEGATECALL", "CALLCODE",
                    "CREATE", "CREATE2", "RETURN", "STOP", "REVERT"].includes(op);
            };
            do {
                yield bugger.advance(1, filter);
                const location = this.currentLocation(bugger);
                const step = bugger.view(trace.step);
                const op = step.op;
                const astNode = bugger.view(data.current.node);
                let currentCall = undefined;
                let parentCalls = depthCallMap.get(functionDepth - 1);
                let currentParentCall;
                if (parentCalls) {
                    currentParentCall = parentCalls[parentCalls.length - 1];
                }
                if (currentCallDepth - 1 === step.depth) {
                    // call ends
                    functionDepth--;
                }
                // Handle the external call and delegate call
                if (op === "CALL" || op === 'CALLCODE' || op === 'DELEGATECALL' || op === 'STATICCALL') {
                    if ((astNode === null || astNode === void 0 ? void 0 : astNode.nodeType) === "FunctionCall") {
                        var memberName = (_a = astNode === null || astNode === void 0 ? void 0 : astNode.expression) === null || _a === void 0 ? void 0 : _a.memberName;
                        // Handle the following low-level call cases
                        // xxx.call(abi.encodeWithSignature("transfer(address,uint256)", to, amount))
                        // xxx.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount))
                        // xxx.call(abi.encodeCall(IERC20.transfer, (to, amount)))
                        if (memberName === "call" || memberName === "staticcall" || memberName === "delegatecall") {
                            if ((astNode === null || astNode === void 0 ? void 0 : astNode.arguments) != null && (astNode === null || astNode === void 0 ? void 0 : astNode.arguments.length) > 0) {
                                var firstArg = astNode === null || astNode === void 0 ? void 0 : astNode.arguments[0];
                                if (firstArg.expression.memberName === "encodeWithSelector") {
                                    var selector = firstArg.arguments[0];
                                    // IERC20.transfer.selector, pick the 'transfer'
                                    memberName = selector.expression.memberName;
                                }
                                else if (firstArg.expression.memberName === "encodeWithSignature") {
                                    var signature = firstArg.arguments[0];
                                    // abi.encodeWithSignature("transfer(address,uint256)", to, amount)
                                    // pick the 'transfer(address,uint256)'
                                    memberName = signature.value;
                                }
                                else if (firstArg.expression.memberName === "encodeCall") {
                                    var call = firstArg.arguments[0];
                                    // abi.encodeCall(IERC20.transfer, (to, amount))
                                    // pick the 'transfer'
                                    memberName = call.memberName;
                                }
                            }
                        }
                        currentCall = {
                            op: op,
                            pc: step.pc,
                            inputs: this.getInputs(bugger),
                            location: location,
                            calls: [],
                            externalCall: true,
                            functionName: memberName
                        };
                    }
                    else if ((astNode === null || astNode === void 0 ? void 0 : astNode.nodeType) === "YulFunctionCall" && ((_b = astNode === null || astNode === void 0 ? void 0 : astNode.functionName) === null || _b === void 0 ? void 0 : _b.name) === 'delegatecall') {
                        currentCall = {
                            op: op,
                            pc: step.pc,
                            inputs: [],
                            location: location,
                            calls: [],
                            externalCall: true,
                            functionName: 'delegatecall'
                        };
                    }
                }
                // Handle the internal function call or post delegate call
                if (step.op == "JUMPDEST" && (astNode === null || astNode === void 0 ? void 0 : astNode.nodeType) === 'FunctionDefinition') {
                    const thisFuncName = astNode.name;
                    // handle the root call
                    if (!depthCallMap.get(0)) {
                        currentCall = {
                            op: "JUMPDEST",
                            pc: step.pc,
                            location: location,
                            functionName: thisFuncName,
                            inputs: this.getInputs(bugger),
                            calls: [],
                            externalCall: false,
                        };
                    }
                    else if (prevOp === "JUMPI" && (currentParentCall === null || currentParentCall === void 0 ? void 0 : currentParentCall.op) === "DELEGATECALL") {
                        if (currentParentCall.functionName === "delegatecall") {
                            // Update the function name and input if with delegate call
                            currentParentCall.functionName = thisFuncName;
                            currentParentCall.inputs = this.getInputs(bugger);
                        }
                    }
                    else if (prevLocation && prevOp === "JUMP") {
                        currentCall = {
                            op: prevOp,
                            pc: prevPc,
                            location: prevLocation,
                            functionName: thisFuncName,
                            inputs: this.getInputs(bugger),
                            calls: [],
                            externalCall: false,
                        };
                        prevLocation = null;
                    }
                    else if (prevOp === "JUMP" && ((currentParentCall === null || currentParentCall === void 0 ? void 0 : currentParentCall.op) === "CALL" || ((currentParentCall === null || currentParentCall === void 0 ? void 0 : currentParentCall.op) === "STATICCALL"))) {
                        if (thisFuncName === currentParentCall.functionName) {
                            currentParentCall.inputs = this.getInputs(bugger);
                        }
                    }
                }
                // Handle the return of function call
                if (step.op === "JUMPDEST" && (astNode === null || astNode === void 0 ? void 0 : astNode.nodeType) === "FunctionCall") {
                    if (((currentParentCall === null || currentParentCall === void 0 ? void 0 : currentParentCall.functionName) === ((_c = astNode === null || astNode === void 0 ? void 0 : astNode.expression) === null || _c === void 0 ? void 0 : _c.memberName) || (currentParentCall === null || currentParentCall === void 0 ? void 0 : currentParentCall.functionName) === ((_d = astNode === null || astNode === void 0 ? void 0 : astNode.expression) === null || _d === void 0 ? void 0 : _d.name))) {
                        functionDepth--;
                    }
                }
                if (step.op === "JUMP") {
                    if (bugger.view(txlog.current.jumpDirection) === "o" &&
                        ((astNode === null || astNode === void 0 ? void 0 : astNode.nodeType) === "FunctionDefinition" || (astNode === null || astNode === void 0 ? void 0 : astNode.nodeType) === "InlineAssembly") &&
                        (currentParentCall === null || currentParentCall === void 0 ? void 0 : currentParentCall.functionName) === (astNode === null || astNode === void 0 ? void 0 : astNode.name)) {
                        functionDepth--;
                    }
                    else if ((astNode === null || astNode === void 0 ? void 0 : astNode.nodeType) === "Return") {
                        functionDepth--;
                    }
                    if ((astNode === null || astNode === void 0 ? void 0 : astNode.nodeType) === "FunctionCall") {
                        prevLocation = location;
                    }
                }
                // Handle YUL return 
                if (step.op === "RETURN" && (astNode === null || astNode === void 0 ? void 0 : astNode.nodeType) === "YulFunctionCall" && (astNode === null || astNode === void 0 ? void 0 : astNode.functionName.name) === "return") {
                    functionDepth--;
                }
                if (currentCall) {
                    let existingSameLevelCalls = depthCallMap.get(functionDepth);
                    if (existingSameLevelCalls) {
                        depthCallMap.set(functionDepth++, [...existingSameLevelCalls, currentCall]);
                    }
                    else {
                        depthCallMap.set(functionDepth++, [currentCall]);
                    }
                    if (currentParentCall) {
                        currentParentCall.calls.push(currentCall);
                    }
                }
                finished = bugger.view(controller.current.trace.finished);
                prevOp = op;
                prevPc = step.pc;
            } while (!finished);
            yield bugger.reset();
            return depthCallMap.get(0);
        });
    }
    evalExpression(expression) {
        return __awaiter(this, void 0, void 0, function* () {
            let variables = yield this.truffle_debugger.variables({ indicateUnknown: true });
            //if we're just dealing with a single variable, handle that case
            //separately (so that we can do things in a better way for that
            //case)
            let variable = expression.trim();
            if (variable in variables) {
                return variables[variable];
            }
            // converts all !<...> expressions to JS-valid selector requests
            const preprocessSelectors = (expr) => {
                const regex = /!<([^>]+)>/g;
                const select = "$"; // expect repl context to have this func
                const replacer = (_, selector) => `${select}("${selector}")`;
                return expr.replace(regex, replacer);
            };
            //HACK
            //if we're not in the single-variable case, we'll need to do some
            //things to Javascriptify our variables so that the JS syntax for
            //using them is closer to the Solidity syntax
            let context = (0, helper_1.safeNativizeVariables)(variables);
            //HACK -- we can't use "this" as a variable name, so we're going to
            //find an available replacement name, and then modify the context
            //and expression appropriately
            let pseudoThis = "_this";
            while (pseudoThis in context) {
                pseudoThis = "_" + pseudoThis;
            }
            //in addition to pseudoThis, which replaces this, we also have
            //pseudoPseudoThis, which replaces pseudoThis in order to ensure
            //that any uses of pseudoThis yield an error instead of showing this
            let pseudoPseudoThis = "thereisnovariableofthatname";
            while (pseudoPseudoThis in context) {
                pseudoPseudoThis = "_" + pseudoPseudoThis;
            }
            context = DebugUtils.cleanThis(context, pseudoThis);
            let expr = expression.replace(
            //those characters in [] are the legal JS variable name characters
            //note that pseudoThis contains no special characters
            new RegExp("(?<![a-zA-Z0-9_$])" + pseudoThis + "(?![a-zA-Z0-9_$])"), pseudoPseudoThis);
            expr = expr.replace(
            //those characters in [] are the legal JS variable name characters
            /(?<![a-zA-Z0-9_$])this(?![a-zA-Z0-9_$])/, pseudoThis);
            //note that pseudoThis contains no dollar signs to screw things up
            expr = preprocessSelectors(expr);
            try {
                return this.safelyEvaluateWithSelectors(expr, context);
            }
            catch (e) {
                throw new Error(e);
            }
        });
    }
    get session() {
        return this.truffle_debugger;
    }
    //evaluates expression with the variables in context,
    //but also has `$` as a variable that is the select function
    safelyEvaluateWithSelectors(expression, context) {
        const select = (expr) => {
            let selector, result;
            try {
                selector = expr
                    .split(".")
                    .reduce((sel, next) => (next.length ? sel[next] : sel), selectors);
            }
            catch (_) {
                throw new Error(`Unknown selector: ${expr}`);
            }
            // throws its own exception
            result = this.truffle_debugger.view(selector);
            return result;
        };
        let interpreter;
        interpreter = new Interpreter(expression, function (interpreter, globalObject) {
            //first let's set up our select function (which will be called $)
            interpreter.setProperty(globalObject, "$", interpreter.createNativeFunction((selectorName) => {
                return interpreter.nativeToPseudo(select(selectorName));
            }));
            //now let's set up the variables
            for (const [variable, value] of Object.entries(context)) {
                try {
                    //note: circular objects wll raise an exception here and get excluded.
                    interpreter.setProperty(globalObject, variable, interpreter.nativeToPseudo(value));
                }
                catch (_) {
                    //just omit things that don't work
                }
            }
        });
        interpreter.run();
        return interpreter.pseudoToNative(interpreter.value);
    }
    determinBreakPoint(breakPoint) {
        //search sources for given string
        let sources = Object.values(this.truffle_debugger.view(sourcemapping.views.sources));
        //we will indeed need the sources here, not just IDs
        let matchingSources = sources.filter(source => 
        // @ts-ignore
        source.sourcePath === breakPoint.sourcePath && source.compilationId === breakPoint.compilationId);
        return {
            // @ts-ignore
            sourceId: matchingSources[0].id,
            line: breakPoint.line
        };
    }
    getInputs(session) {
        const userDefinedTypes = session.view(data.views.userDefinedTypes);
        const state = session.view(data.current.state);
        const mappingKeys = session.view(data.views.mappingKeys);
        const allocations = session.view(data.info.allocations);
        const contexts = session.view(data.views.contexts);
        const currentContext = session.view(data.current.context);
        const internalFunctionsTable = session.view(data.current.functionsByProgramCounter);
        const inputAllocations = session.view(txlog.current.inputParameterAllocations);
        let variables = [];
        if (inputAllocations) {
            const compilationId = session.view(txlog.current.compilationId);
            //can't do a yield* inside a map, have to do this loop manually
            for (let { name, definition, pointer } of inputAllocations) {
                const decoder = Codec.decodeVariable(definition, pointer, {
                    userDefinedTypes: userDefinedTypes,
                    state,
                    mappingKeys: mappingKeys,
                    allocations: allocations,
                    contexts: contexts,
                    currentContext,
                    internalFunctionsTable
                }, compilationId);
                variables.push({ name, value: decoder.next().value });
            }
        }
        return variables;
    }
}
exports.Debugger = Debugger;
//# sourceMappingURL=debugger.js.map