diff --git a/remix-debug/index.js b/remix-debug/index.js index a5e3ac4f80..3ea8d98482 100644 --- a/remix-debug/index.js +++ b/remix-debug/index.js @@ -1,5 +1,6 @@ 'use strict' var EthDebugger = require('./src/Ethdebugger') +var TransactionDebugger = require('./src/debugger/debugger') var StorageViewer = require('./src/storage/storageViewer') var StorageResolver = require('./src/storage/storageResolver') @@ -19,6 +20,7 @@ var BreakpointManager = remixLib.code.BreakpointManager */ module.exports = { EthDebugger: EthDebugger, + TransactionDebugger: TransactionDebugger, /** * constructor * diff --git a/remix-debug/src/debugger/SolidityLocals.js b/remix-debug/src/debugger/SolidityLocals.js new file mode 100644 index 0000000000..7560770bb2 --- /dev/null +++ b/remix-debug/src/debugger/SolidityLocals.js @@ -0,0 +1,65 @@ +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var remixDebug = require('remix-debug') +var localDecoder = remixDebug.SolidityDecoder.localDecoder +var StorageViewer = remixDebug.storage.StorageViewer + +class DebuggerSolidityLocals { + + constructor (tx, _stepManager, _traceManager, _internalTreeCall) { + this.event = new EventManager() + this.stepManager = _stepManager + this.internalTreeCall = _internalTreeCall + this.storageResolver = null + this.traceManager = _traceManager + this.tx = tx + } + + init (sourceLocation) { + const self = this + var decodeTimeout = null + if (!this.storageResolver) { + return self.event.trigger('solidityLocalsMessage', ['storage not ready']) + } + if (decodeTimeout) { + window.clearTimeout(decodeTimeout) + } + self.event.trigger('solidityLocalsUpdating') + decodeTimeout = setTimeout(function () { + self.decode(sourceLocation) + }, 500) + } + + decode (sourceLocation) { + const self = this + self.event.trigger('solidityLocalsMessage', ['']) + self.traceManager.waterfall([ + self.traceManager.getStackAt, + self.traceManager.getMemoryAt, + self.traceManager.getCurrentCalledAddressAt], + self.stepManager.currentStepIndex, + (error, result) => { + if (error) { + return console.log(error) + } + var stack = result[0].value + var memory = result[1].value + try { + var storageViewer = new StorageViewer({ stepIndex: self.stepManager.currentStepIndex, tx: self.tx, address: result[2].value }, self.storageResolver, self.traceManager) + localDecoder.solidityLocals(self.stepManager.currentStepIndex, self.internalTreeCall, stack, memory, storageViewer, sourceLocation).then((locals) => { + if (!locals.error) { + self.event.trigger('solidityLocals', [locals]) + } + if (!Object.keys(locals).length) { + self.event.trigger('solidityLocalsMessage', ['no locals']) + } + }) + } catch (e) { + self.event.trigger('solidityLocalsMessage', [e.message]) + } + }) + } + +} + +module.exports = DebuggerSolidityLocals diff --git a/remix-debug/src/debugger/VmDebugger.js b/remix-debug/src/debugger/VmDebugger.js new file mode 100644 index 0000000000..c9e4e069bf --- /dev/null +++ b/remix-debug/src/debugger/VmDebugger.js @@ -0,0 +1,242 @@ +var remixDebug = require('remix-debug') +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var ui = remixLib.helpers.ui +var StorageResolver = remixDebug.storage.StorageResolver +var StorageViewer = remixDebug.storage.StorageViewer + +var DebuggerSolidityState = require('./solidityState') +var DebuggerSolidityLocals = require('./solidityLocals') + +class VmDebuggerLogic { + + constructor (_debugger, tx, _stepManager, _traceManager, _codeManager, _solidityProxy, _callTree) { + this.event = new EventManager() + this.debugger = _debugger + this.stepManager = _stepManager + this._traceManager = _traceManager + this._codeManager = _codeManager + this._solidityProxy = _solidityProxy + this._callTree = _callTree + this.storageResolver = null + this.tx = tx + + this.debuggerSolidityState = new DebuggerSolidityState(tx, _stepManager, _traceManager, _codeManager, _solidityProxy) + this.debuggerSolidityLocals = new DebuggerSolidityLocals(tx, _stepManager, _traceManager, _callTree) + } + + start () { + this.listenToEvents() + this.listenToCodeManagerEvents() + this.listenToTraceManagerEvents() + this.listenToFullStorageChanges() + this.listenToNewChanges() + + this.listenToSolidityStateEvents() + this.listenToSolidityLocalsEvents() + } + + listenToEvents () { + const self = this + this.debugger.event.register('traceUnloaded', function () { + self.event.trigger('traceUnloaded') + }) + + this.debugger.event.register('newTraceLoaded', function () { + self.event.trigger('newTraceLoaded') + }) + } + + listenToCodeManagerEvents () { + const self = this + this._codeManager.event.register('changed', function (code, address, index) { + self.event.trigger('codeManagerChanged', [code, address, index]) + }) + } + + listenToTraceManagerEvents () { + const self = this + + this.event.register('indexChanged', this, function (index) { + if (index < 0) return + if (self.stepManager.currentStepIndex !== index) return + + self.event.trigger('indexUpdate', [index]) + + self._traceManager.getCallDataAt(index, function (error, calldata) { + if (error) { + console.log(error) + self.event.trigger('traceManagerCallDataUpdate', [{}]) + } else if (self.stepManager.currentStepIndex === index) { + self.event.trigger('traceManagerCallDataUpdate', [calldata]) + } + }) + + self._traceManager.getMemoryAt(index, function (error, memory) { + if (error) { + console.log(error) + self.event.trigger('traceManagerMemoryUpdate', [{}]) + } else if (self.stepManager.currentStepIndex === index) { + self.event.trigger('traceManagerMemoryUpdate', [ui.formatMemory(memory, 16)]) + } + }) + + self._traceManager.getCallStackAt(index, function (error, callstack) { + if (error) { + console.log(error) + self.event.trigger('traceManagerCallStackUpdate', [{}]) + } else if (self.stepManager.currentStepIndex === index) { + self.event.trigger('traceManagerCallStackUpdate', [callstack]) + } + }) + + self._traceManager.getStackAt(index, function (error, callstack) { + if (error) { + console.log(error) + self.event.trigger('traceManagerStackUpdate', [{}]) + } else if (self.stepManager.currentStepIndex === index) { + self.event.trigger('traceManagerStackUpdate', [callstack]) + } + }) + + self._traceManager.getCurrentCalledAddressAt(index, (error, address) => { + if (error) return + if (!self.storageResolver) return + + var storageViewer = new StorageViewer({ stepIndex: self.stepManager.currentStepIndex, tx: self.tx, address: address }, self.storageResolver, self._traceManager) + + storageViewer.storageRange((error, storage) => { + if (error) { + console.log(error) + self.event.trigger('traceManagerStorageUpdate', [{}]) + } else if (self.stepManager.currentStepIndex === index) { + var header = storageViewer.isComplete(address) ? 'completely loaded' : 'partially loaded...' + self.event.trigger('traceManagerStorageUpdate', [storage, header]) + } + }) + }) + + self._traceManager.getCurrentStep(index, function (error, step) { + self.event.trigger('traceCurrentStepUpdate', [error, step]) + }) + + self._traceManager.getMemExpand(index, function (error, addmem) { + self.event.trigger('traceMemExpandUpdate', [error, addmem]) + }) + + self._traceManager.getStepCost(index, function (error, gas) { + self.event.trigger('traceStepCostUpdate', [error, gas]) + }) + + self._traceManager.getCurrentCalledAddressAt(index, function (error, address) { + self.event.trigger('traceCurrentCalledAddressAtUpdate', [error, address]) + }) + + self._traceManager.getRemainingGas(index, function (error, remaining) { + self.event.trigger('traceRemainingGasUpdate', [error, remaining]) + }) + + self._traceManager.getReturnValue(index, function (error, returnValue) { + if (error) { + self.event.trigger('traceReturnValueUpdate', [[error]]) + } else if (self.stepManager.currentStepIndex === index) { + self.event.trigger('traceReturnValueUpdate', [[returnValue]]) + } + }) + }) + } + + listenToFullStorageChanges () { + const self = this + + this.address = [] + this.traceLength = 0 + + self.debugger.event.register('newTraceLoaded', function (length) { + self._traceManager.getAddresses(function (error, addresses) { + if (error) return + self.event.trigger('traceAddressesUpdate', [addresses]) + self.addresses = addresses + }) + + self._traceManager.getLength(function (error, length) { + if (error) return + self.event.trigger('traceLengthUpdate', [length]) + self.traceLength = length + }) + }) + + self.debugger.event.register('indexChanged', this, function (index) { + if (index < 0) return + if (self.stepManager.currentStepIndex !== index) return + if (!self.storageResolver) return + + if (index !== self.traceLength - 1) { + return self.event.trigger('traceLengthUpdate', [{}]) + } + var storageJSON = {} + for (var k in self.addresses) { + var address = self.addresses[k] + var storageViewer = new StorageViewer({ stepIndex: self.stepManager.currentStepIndex, tx: self.tx, address: address }, self.storageResolver, self._traceManager) + storageViewer.storageRange(function (error, result) { + if (!error) { + storageJSON[address] = result + self.event.trigger('traceLengthUpdate', [storageJSON]) + } + }) + } + }) + } + + listenToNewChanges () { + const self = this + self.debugger.event.register('newTraceLoaded', this, function () { + self.storageResolver = new StorageResolver({web3: self.debugger.web3}) + self.debuggerSolidityState.storageResolver = self.storageResolver + self.debuggerSolidityLocals.storageResolver = self.storageResolver + self.event.trigger('newTrace', []) + }) + + self.debugger.event.register('callTreeReady', function () { + if (self.debugger.callTree.reducedTrace.length) { + return self.event.trigger('newCallTree', []) + } + }) + } + + listenToSolidityStateEvents () { + const self = this + this.event.register('indexChanged', this.debuggerSolidityState.init.bind(this.debuggerSolidityState)) + this.debuggerSolidityState.event.register('solidityState', function (state) { + self.event.trigger('solidityState', [state]) + }) + this.debuggerSolidityState.event.register('solidityStateMessage', function (message) { + self.event.trigger('solidityStateMessage', [message]) + }) + this.debuggerSolidityState.event.register('solidityStateUpdating', function () { + self.event.trigger('solidityStateUpdating', []) + }) + this.event.register('traceUnloaded', this.debuggerSolidityState.reset.bind(this.debuggerSolidityState)) + this.event.register('newTraceLoaded', this.debuggerSolidityState.reset.bind(this.debuggerSolidityState)) + } + + listenToSolidityLocalsEvents () { + const self = this + this.event.register('sourceLocationChanged', this.debuggerSolidityLocals.init.bind(this.debuggerSolidityLocals)) + this.debuggerSolidityLocals.event.register('solidityLocals', function (state) { + self.event.trigger('solidityLocals', [state]) + }) + this.debuggerSolidityLocals.event.register('solidityLocalsMessage', function (message) { + self.event.trigger('solidityLocalsMessage', [message]) + }) + this.debuggerSolidityLocals.event.register('solidityLocalsUpdating', function () { + self.event.trigger('solidityLocalsUpdating', []) + }) + this.debuggerSolidityLocals.event.register('traceReturnValueUpdate', function (data, header) { + self.event.trigger('traceReturnValueUpdate', [data, header]) + }) + } + +} + +module.exports = VmDebuggerLogic diff --git a/remix-debug/src/debugger/debugger.js b/remix-debug/src/debugger/debugger.js new file mode 100644 index 0000000000..cc8b2aa37b --- /dev/null +++ b/remix-debug/src/debugger/debugger.js @@ -0,0 +1,144 @@ +'use strict' +var Ethdebugger = require('remix-debug').EthDebugger +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var traceHelper = remixLib.helpers.trace + +var StepManager = require('./stepManager') +var VmDebuggerLogic = require('./VmDebugger') + +function Debugger (options) { + var self = this + this.event = new EventManager() + + this.executionContext = options.executionContext + this.offsetToLineColumnConverter = options.offsetToLineColumnConverter + this.compiler = options.compiler + + this.debugger = new Ethdebugger({ + executionContext: this.executionContext, + compilationResult: () => { + + console.dir(self.compiler.lastCompilationResult) + window.a = self.compiler.lastCompilationResult + + var compilationResult = this.compiler.lastCompilationResult + if (compilationResult) { + return compilationResult.data + } + return null + } + }) + + this.breakPointManager = new remixLib.code.BreakpointManager(this.debugger, (sourceLocation) => { + return self.offsetToLineColumnConverter.offsetToLineColumn(sourceLocation, sourceLocation.file, this.compiler.lastCompilationResult.source.sources) + }, (step) => { + self.event.trigger('breakpointStep', [step]) + }) + + this.debugger.setBreakpointManager(this.breakPointManager) + + this.executionContext.event.register('contextChanged', this, function (context) { + // TODO: was already broken + // self.switchProvider(context) + }) + + this.debugger.event.register('newTraceLoaded', this, function () { + self.event.trigger('debuggerStatus', [true]) + }) + + this.debugger.event.register('traceUnloaded', this, function () { + self.event.trigger('debuggerStatus', [false]) + }) + + this.event.register('breakpointStep', function (step) { + self.step_manager.jumpTo(step) + }) + + this.debugger.addProvider('vm', this.executionContext.vm()) + this.debugger.addProvider('injected', this.executionContext.internalWeb3()) + this.debugger.addProvider('web3', this.executionContext.internalWeb3()) + this.debugger.switchProvider(this.executionContext.getProvider()) +} + +Debugger.prototype.registerAndHighlightCodeItem = function (index) { + const self = this + // register selected code item, highlight the corresponding source location + if (!self.compiler.lastCompilationResult) return + self.debugger.traceManager.getCurrentCalledAddressAt(index, (error, address) => { + if (error) return console.log(error) + self.debugger.callTree.sourceLocationTracker.getSourceLocationFromVMTraceIndex(address, index, self.compiler.lastCompilationResult.data.contracts, function (error, rawLocation) { + if (!error && self.compiler.lastCompilationResult && self.compiler.lastCompilationResult.data) { + var lineColumnPos = self.offsetToLineColumnConverter.offsetToLineColumn(rawLocation, rawLocation.file, self.compiler.lastCompilationResult.source.sources) + self.event.trigger('newSourceLocation', [lineColumnPos, rawLocation]) + } else { + self.event.trigger('newSourceLocation', [null]) + } + }) + }) +} + +Debugger.prototype.debug = function (blockNumber, txNumber, tx, loadingCb) { + const self = this + let web3 = this.executionContext.web3() + + if (this.debugger.traceManager.isLoading) { + return + } + + self.debugger.solidityProxy.reset({}) + + if (tx) { + if (!tx.to) { + tx.to = traceHelper.contractCreationToken('0') + } + return self.debugTx(tx, loadingCb) + } + + try { + if (txNumber.indexOf('0x') !== -1) { + return web3.eth.getTransaction(txNumber, function (_error, result) { + let tx = result + self.debugTx(tx, loadingCb) + }) + } + web3.eth.getTransactionFromBlock(blockNumber, txNumber, function (_error, result) { + let tx = result + self.debugTx(tx, loadingCb) + }) + } catch (e) { + console.error(e.message) + } +} + +Debugger.prototype.debugTx = function (tx, loadingCb) { + const self = this + this.step_manager = new StepManager(this.debugger, this.debugger.traceManager) + + this.debugger.codeManager.event.register('changed', this, (code, address, instIndex) => { + self.debugger.callTree.sourceLocationTracker.getSourceLocationFromVMTraceIndex(address, this.step_manager.currentStepIndex, this.debugger.solidityProxy.contracts, (error, sourceLocation) => { + if (!error) { + self.vmDebuggerLogic.event.trigger('sourceLocationChanged', [sourceLocation]) + } + }) + }) + + this.vmDebuggerLogic = new VmDebuggerLogic(this.debugger, tx, this.step_manager, this.debugger.traceManager, this.debugger.codeManager, this.debugger.solidityProxy, this.debugger.callTree) + + this.step_manager.event.register('stepChanged', this, function (stepIndex) { + self.debugger.codeManager.resolveStep(stepIndex, tx) + self.step_manager.event.trigger('indexChanged', [stepIndex]) + self.vmDebuggerLogic.event.trigger('indexChanged', [stepIndex]) + self.registerAndHighlightCodeItem(stepIndex) + }) + + loadingCb() + this.debugger.debug(tx) +} + +Debugger.prototype.unload = function () { + this.debugger.unLoad() + this.event.trigger('debuggerUnloaded') +} + +module.exports = Debugger diff --git a/remix-debug/src/debugger/solidityState.js b/remix-debug/src/debugger/solidityState.js new file mode 100644 index 0000000000..5b5ceed16c --- /dev/null +++ b/remix-debug/src/debugger/solidityState.js @@ -0,0 +1,81 @@ +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var remixDebug = require('remix-debug') +var stateDecoder = remixDebug.SolidityDecoder.stateDecoder +var StorageViewer = remixDebug.storage.StorageViewer + +class DebuggerSolidityState { + + constructor (tx, _stepManager, _traceManager, _codeManager, _solidityProxy) { + this.event = new EventManager() + this.storageResolver = null + this.stepManager = _stepManager + this.traceManager = _traceManager + this.codeManager = _codeManager + this.solidityProxy = _solidityProxy + this.stateVariablesByAddresses = {} + this.tx = tx + } + + init (index) { + var self = this + var decodeTimeout = null + if (index < 0) { + return self.event.trigger('solidityStateMessage', ['invalid step index']) + } + + if (self.stepManager.currentStepIndex !== index) return + if (!self.solidityProxy.loaded()) { + return self.event.trigger('solidityStateMessage', ['invalid step index']) + } + + if (!self.storageResolver) { + return + } + if (decodeTimeout) { + window.clearTimeout(decodeTimeout) + } + self.event.trigger('solidityStateUpdating') + decodeTimeout = setTimeout(function () { + self.decode(index) + }, 500) + } + + reset () { + this.stateVariablesByAddresses = {} + } + + decode (index) { + const self = this + self.traceManager.getCurrentCalledAddressAt(self.stepManager.currentStepIndex, function (error, address) { + if (error) { + return self.event.trigger('solidityState', [{}]) + } + if (self.stateVariablesByAddresses[address]) { + return self.extractStateVariables(self.stateVariablesByAddresses[address], address) + } + self.solidityProxy.extractStateVariablesAt(index, function (error, stateVars) { + if (error) { + return self.event.trigger('solidityState', [{}]) + } + self.stateVariablesByAddresses[address] = stateVars + self.extractStateVariables(stateVars, address) + }) + }) + } + + extractStateVariables (stateVars, address) { + const self = this + var storageViewer = new StorageViewer({ stepIndex: self.stepManager.currentStepIndex, tx: self.tx, address: address }, self.storageResolver, self.traceManager) + stateDecoder.decodeState(stateVars, storageViewer).then((result) => { + self.event.trigger('solidityStateMessage', ['']) + if (result.error) { + return self.event.trigger('solidityStateMessage', [result.error]) + } + self.event.trigger('solidityState', [result]) + }) + } + +} + +module.exports = DebuggerSolidityState diff --git a/remix-debug/src/debugger/stepManager.js b/remix-debug/src/debugger/stepManager.js new file mode 100644 index 0000000000..26e48ecd4b --- /dev/null +++ b/remix-debug/src/debugger/stepManager.js @@ -0,0 +1,145 @@ +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager + +class DebuggerStepManager { + + constructor (_debugger, traceManager) { + this.event = new EventManager() + this.debugger = _debugger + this.traceManager = traceManager + this.currentStepIndex = 0 + this.traceLength = 0 + this.revertionPoint = null + + this.listenToEvents() + } + + listenToEvents () { + const self = this + + this.debugger.event.register('newTraceLoaded', this, function () { + self.traceManager.getLength(function (error, newLength) { + if (error) { + return console.log(error) + } + if (self.traceLength !== newLength) { + self.event.trigger('traceLengthChanged', [newLength]) + self.traceLength = newLength + } + self.jumpTo(0) + }) + }) + + this.debugger.callTree.event.register('callTreeReady', () => { + if (self.debugger.callTree.functionCallStack.length) { + self.jumpTo(self.debugger.callTree.functionCallStack[0]) + } + }) + + this.event.register('indexChanged', this, (index) => { + if (index < 0) return + if (self.currentStepIndex !== index) return + + self.traceManager.buildCallPath(index, (error, callsPath) => { + if (error) { + console.log(error) + return self.event.trigger('revertWarning', ['']) + } + self.currentCall = callsPath[callsPath.length - 1] + if (self.currentCall.reverted) { + let revertedReason = self.currentCall.outofgas ? 'outofgas' : '' + self.revertionPoint = self.currentCall.return + return self.event.trigger('revertWarning', [revertedReason]) + } + for (var k = callsPath.length - 2; k >= 0; k--) { + var parent = callsPath[k] + if (!parent.reverted) continue + self.revertionPoint = parent.return + self.event.trigger('revertWarning', ['parenthasthrown']) + } + self.event.trigger('revertWarning', ['']) + }) + }) + } + + triggerStepChanged (step) { + const self = this + this.traceManager.getLength(function (error, length) { + let stepState = 'valid' + + if (error) { + stepState = 'invalid' + } else if (step <= 0) { + stepState = 'initial' + } else if (step >= length - 1) { + stepState = 'end' + } + + let jumpOutDisabled = (step === self.traceManager.findStepOut(step)) + + self.event.trigger('stepChanged', [step, stepState, jumpOutDisabled]) + }) + } + + stepIntoBack () { + if (!this.traceManager.isLoaded()) return + var step = this.currentStepIndex - 1 + this.currentStepIndex = step + if (!this.traceManager.inRange(step)) { + return + } + this.event.trigger('stepChanged', [step]) + } + + stepIntoForward () { + if (!this.traceManager.isLoaded()) return + var step = this.currentStepIndex + 1 + this.currentStepIndex = step + if (!this.traceManager.inRange(step)) { + return + } + this.event.trigger('stepChanged', [step]) + } + + stepOverBack () { + if (!this.traceManager.isLoaded()) return + var step = this.traceManager.findStepOverBack(this.currentStepIndex) + this.currentStepIndex = step + this.event.trigger('stepChanged', [step]) + } + + stepOverForward () { + if (!this.traceManager.isLoaded()) return + var step = this.traceManager.findStepOverForward(this.currentStepIndex) + this.currentStepIndex = step + this.event.trigger('stepChanged', [step]) + } + + jumpOut () { + if (!this.traceManager.isLoaded()) return + var step = this.traceManager.findStepOut(this.currentStepIndex) + this.currentStepIndex = step + this.event.trigger('stepChanged', [step]) + } + + jumpTo (step) { + if (!this.traceManager.inRange(step)) return + this.currentStepIndex = step + this.event.trigger('stepChanged', [step]) + } + + jumpToException () { + this.jumpTo(this.revertionPoint) + } + + jumpNextBreakpoint () { + this.debugger.breakpointManager.jumpNextBreakpoint(this.currentStepIndex, true) + } + + jumpPreviousBreakpoint () { + this.debugger.breakpointManager.jumpPreviousBreakpoint(this.currentStepIndex, true) + } + +} + +module.exports = DebuggerStepManager