trufflesuite/truffle

View on GitHub
packages/debugger/lib/txlog/reducers.js

Summary

Maintainability
F
4 days
Test Coverage
import debugModule from "debug";
const debug = debugModule("debugger:txlog:reducers");

import { combineReducers } from "redux";

import * as actions from "./actions";

//NOTE: even though we refer to nodes by JSON pointer,
//these pointers are "fake" in that we don't actually
//use them *as* JSON pointers; it's just a convenient
//method of IDing them that also has a nice intuitive
//meaning (you'll notice we don't actually import
//json-pointer here or anywhere else in this submodule)
const DEFAULT_TX_LOG = {
  byPointer: {
    "": {
      // "" is the root node
      type: "transaction",
      actions: []
    }
  }
};

function transactionLog(state = DEFAULT_TX_LOG, action) {
  const { pointer, newPointer, step } = action;
  const node = state.byPointer[pointer];
  switch (action.type) {
    case actions.RECORD_ORIGIN:
      if (node.type === "transaction") {
        return {
          byPointer: {
            ...state.byPointer,
            [pointer]: {
              ...node,
              origin: action.address
            }
          }
        };
      } else {
        debug("attempt to set origin of bad node type!");
        return state;
      }

    case actions.LOG_EVENT:
      return {
        byPointer: {
          ...state.byPointer,
          [pointer]: {
            ...node,
            actions: [...node.actions, newPointer]
          },
          [newPointer]: {
            type: "event",
            decoding: action.decoding,
            raw: action.rawEventInfo,
            step
          }
        }
      };

    case actions.STORE:
      //this case will likely need to be redone when decoding is added
      //(likely split into multiple cases)
      //for now, there is no combining of different writes, but when decoding
      //is added there will need to be!
      return {
        byPointer: {
          ...state.byPointer,
          [pointer]: {
            ...node,
            actions: [...node.actions, newPointer]
          },
          [newPointer]: {
            type: "write",
            raw: {
              [action.rawSlot]: action.rawValue
            },
            steps: {
              [action.rawSlot]: step
            }
          }
        }
      };
    case actions.INTERNAL_CALL:
      return {
        byPointer: {
          ...state.byPointer,
          [pointer]: {
            ...node,
            actions: [...node.actions, newPointer]
          },
          [newPointer]: {
            type: "callinternal",
            actions: [],
            beginStep: step,
            waitingForFunctionDefinition: true
          }
        }
      };
    case actions.ABSORBED_CALL:
      return {
        byPointer: {
          ...state.byPointer,
          [pointer]: {
            ...node,
            absorbNextInternalCall: false
          }
        }
      };
    case actions.INTERNAL_RETURN:
      //pop the top call from the stack if it's internal (and set its return values)
      //if the top call is instead external, just set its return values if appropriate.
      //(this is how we handle internal/external return absorption)
      const modifiedNode = { ...node };
      if (modifiedNode.type === "callinternal") {
        modifiedNode.returnKind = "return";
        modifiedNode.returnValues = action.variables;
        delete modifiedNode.waitingForFunctionDefinition;
      } else if (modifiedNode.type === "callexternal") {
        if (modifiedNode.kind === "function") {
          //don't set return variables for non-function external calls
          modifiedNode.returnValues = action.variables;
        }
      } else {
        debug("returninternal once tx done!");
      }
      modifiedNode.endStep = step;
      return {
        byPointer: {
          ...state.byPointer,
          [pointer]: modifiedNode
        }
      };
    case actions.INSTANT_EXTERNAL_CALL:
    case actions.EXTERNAL_CALL:
    case actions.INSTANT_CREATE:
    case actions.CREATE: {
      const instant =
        action.type === actions.INSTANT_EXTERNAL_CALL ||
        action.type === actions.INSTANT_CREATE;
      let modifiedNode = {
        ...node,
        actions: [...node.actions, newPointer]
      };
      if (
        modifiedNode.type === "callexternal" &&
        modifiedNode.kind === "library"
      ) {
        //didn't identify it as function, so set it to message
        modifiedNode.kind = "message";
      }
      const {
        address,
        binary, //only for creates
        context,
        value,
        salt, //only for creates
        isDelegate,
        decoding,
        calldata,
        status
      } = action;
      let kind;
      if (
        action.type === actions.CREATE ||
        action.type === actions.INSTANT_CREATE
      ) {
        //these don't have kind in the action, so we instead determine
        //it this way
        kind = context ? "constructor" : "unknowncreate";
      } else {
        kind = action.kind;
      }
      const contractName = context ? context.contractName : undefined;
      let functionName, variables;
      if (decoding.kind === "function" || decoding.kind === "constructor") {
        functionName = decoding.abi.name;
        variables = decoding.arguments;
      }
      let call = {
        type: "callexternal",
        address,
        contextHash: context.context || null,
        value,
        kind,
        isDelegate,
        functionName,
        contractName,
        arguments: variables,
        actions: [],
        beginStep: step
      };
      if (kind === "message" || kind === "library") {
        call.data = calldata;
      } else if (kind === "unknowncreate") {
        call.binary = binary;
      }
      if (kind === "constructor" || kind === "unknowncreate") {
        call.salt = salt;
      }
      if (instant) {
        call.returnKind = status ? "return" : "revert";
        call.endStep = step;
      } else {
        //If kind === "message", set waiting to false.
        //Why?  Well, because fallback functions and receive functions
        //typically have their function definitions skipped over, so the next
        //one we hit would instead be a function *called* from the fallback
        //function, which is not what we want.
        call.waitingForFunctionDefinition = kind !== "message";
        //if kind is message or constructor, we don't want to absorb.
        call.absorbNextInternalCall =
          (kind === "function" || kind === "library") &&
          action.absorbNextInternalCall;
      }
      //include raw data regardless
      call.raw = {};
      if (calldata) {
        call.raw.calldata = calldata;
      }
      if (binary) {
        call.raw.binary = binary;
      }
      return {
        byPointer: {
          ...state.byPointer,
          [pointer]: modifiedNode,
          [newPointer]: call
        }
      };
    }
    case actions.EXTERNAL_RETURN:
    case actions.REVERT:
    case actions.SELFDESTRUCT: {
      //first: set the returnKind and other info
      let modifiedNode = { ...node };
      if (
        modifiedNode.type === "callexternal" &&
        modifiedNode.kind === "library"
      ) {
        //didn't identify it as function, so set it to message
        modifiedNode.kind = "message";
      }
      switch (action.type) {
        case actions.EXTERNAL_RETURN:
          if (!modifiedNode.returnKind) {
            modifiedNode.returnKind = "return";
          }
          break;
        case actions.REVERT:
          modifiedNode.returnKind = "revert";
          modifiedNode.error = action.error;
          break;
        case actions.SELFDESTRUCT:
          modifiedNode.returnKind = "selfdestruct";
          modifiedNode.beneficiary = action.beneficiary;
          break;
      }
      modifiedNode.endStep = step;
      let newState = {
        byPointer: {
          ...state.byPointer,
          [pointer]: modifiedNode
        }
      };
      //now: pop all calls from stack until we pop an external call.
      //we don't handle return values here since those are handled
      //in returninternal (yay absorption)
      let currentPointer;
      for (
        currentPointer = pointer;
        currentPointer.replace(/\/actions\/\d+$/, "") !== newPointer; //stop *before* the stop pointer
        currentPointer = currentPointer.replace(/\/actions\/\d+$/, "") //cut off end
      ) {
        debug("currentNode!");
        let currentNode = { ...newState.byPointer[currentPointer] }; //clone
        if (!currentNode.returnKind) {
          //set the return kind on any nodes popped along the way that don't have
          //one already to note that they failed to return due to a call they made
          //reverting
          currentNode.returnKind = "unwind";
          currentNode.endStep = step;
        }
        delete currentNode.waitingForFunctionDefinition;
        debug("set currentNode!");
        newState.byPointer[currentPointer] = currentNode;
      }
      //now handle the external call.
      //note that currentPointer now points to it.
      debug("finalNode!");
      let finalNode = { ...newState.byPointer[currentPointer] }; //clone
      //first let's set the returnKind if there isn't one already
      //(in which case we can infer it was unwound).
      if (!finalNode.returnKind) {
        finalNode.returnKind = "unwind";
      }
      //now let's set its return variables if applicable.
      if (
        finalNode.kind === "function" &&
        action.type === actions.EXTERNAL_RETURN &&
        action.decodings
      ) {
        //functions get returnValues
        const decoding = action.decodings.find(
          decoding => decoding.kind === "return"
        );
        if (decoding) {
          //we'll trust this method over the method resulting from an internal return,
          //*if* it produces a valid return-value decoding.  if it doesn't, we ignore it.
          finalNode.returnValues = decoding.arguments;
        }
      }
      //and we'll set raw return data if applicable
      //(we don't use codec here to increase robustness)
      if (
        finalNode.kind === "message" &&
        action.type === actions.EXTERNAL_RETURN
      ) {
        finalNode.returnData = action.returnData;
      }
      //also, set immutables if applicable -- note that we do *not* attempt to set
      //these the internal way, as we don't have a reliable way of doing that
      if (
        finalNode.kind === "constructor" &&
        action.type === actions.EXTERNAL_RETURN &&
        action.decodings
      ) {
        const decoding = action.decodings.find(
          decoding => decoding.kind === "bytecode"
        );
        if (decoding && decoding.immutables) {
          finalNode.returnImmutables = decoding.immutables;
        }
      }
      //and of course set the end step
      finalNode.endStep = step;
      //finally, delete internal info
      delete finalNode.waitingForFunctionDefinition;
      delete finalNode.absorbNextInternalCall;
      debug("set finalNode!");
      newState.byPointer[currentPointer] = finalNode;
      return newState;
    }
    case actions.IDENTIFY_FUNCTION_CALL: {
      const { functionNode, contractNode, variables } = action;
      const functionName = functionNode.name || undefined; //replace "" with undefined
      const contractName =
        contractNode && contractNode.nodeType === "ContractDefinition"
          ? contractNode.name
          : null;
      let modifiedNode = {
        ...node,
        waitingForFunctionDefinition: false
      };
      //note: I don't handle the following three fields in the object spread above
      //because I don't want undefined or null counting against it
      if (!modifiedNode.functionName) {
        modifiedNode.functionName = functionName;
      }
      if (!modifiedNode.contractName) {
        modifiedNode.contractName = contractName;
      }
      if (!modifiedNode.arguments) {
        modifiedNode.arguments = variables;
      }
      if (
        modifiedNode.type === "callexternal" &&
        modifiedNode.kind === "library"
      ) {
        modifiedNode.kind = "function";
        delete modifiedNode.data;
      }
      return {
        byPointer: {
          ...state.byPointer,
          [pointer]: modifiedNode
        }
      };
    }
    case actions.RESET: //we'll reset everything and re-put initial action afterwards
      //...well, almost everything.  we'll leave the origin in place.
      return {
        byPointer: {
          "": {
            type: "transaction",
            origin: state.byPointer[""].origin, //keep origin
            actions: []
          }
        }
      };
    case actions.UNLOAD_TRANSACTION:
      return DEFAULT_TX_LOG;
    default:
      return state;
  }
}

function currentNodePointer(state = "", action) {
  switch (action.type) {
    case actions.INTERNAL_CALL:
    case actions.EXTERNAL_CALL:
    case actions.CREATE:
    case actions.INTERNAL_RETURN:
    case actions.EXTERNAL_RETURN:
    case actions.REVERT:
    case actions.SELFDESTRUCT:
      //note that instant calls/creates are not included!
      return action.newPointer;
    case actions.RESET: //we'll reset everything and re-put initial action afterwards
    case actions.UNLOAD_TRANSACTION:
      return "";
    default:
      //includes events & stores
      return state;
  }
}

//this is a stack of the pointers to external calls.
//note: not to the frames below them!
function pointerStack(state = [], action) {
  switch (action.type) {
    case actions.EXTERNAL_CALL:
    case actions.CREATE:
      //note that instant calls & creates are not included!
      return [...state, action.newPointer];
    case actions.EXTERNAL_RETURN:
    case actions.REVERT:
    case actions.SELFDESTRUCT:
      return state.slice(0, -1);
    case actions.RESET: //we'll reset everything and re-put initial action afterwards
    case actions.UNLOAD_TRANSACTION:
      return [];
    default:
      //includes events & stores
      return state;
  }
}

function initialCall(state = null, action) {
  switch (action.type) {
    case actions.EXTERNAL_CALL:
    case actions.CREATE:
      //we only want to save the initial call, so return
      //the current state if it's not null
      //(we can skip instant case here, initial call is never instant)
      if (state !== null) {
        return state;
      } else {
        //we'll just store the action itself in the state
        return action;
      }
    case actions.UNLOAD_TRANSACTION:
      return null;
    default:
      return state;
  }
}

const proc = combineReducers({
  transactionLog,
  currentNodePointer,
  pointerStack
});

const transaction = combineReducers({
  initialCall
});

const reducer = combineReducers({
  proc,
  transaction
});

export default reducer;