trufflesuite/truffle

View on GitHub
packages/debugger/lib/web3/sagas/index.js

Summary

Maintainability
C
1 day
Test Coverage
import debugModule from "debug";
const debug = debugModule("debugger:web3:sagas");

import {
  all,
  takeEvery,
  apply,
  fork,
  join,
  take,
  put,
  race,
  call
} from "redux-saga/effects";
import { prefixName } from "lib/helpers";

import * as actions from "../actions";
import * as session from "lib/session/actions";

import BN from "bn.js";
import Web3 from "web3"; //just for utils!
import * as Codec from "@truffle/codec";

import Web3Adapter from "../adapter";

//the following two functions are for Besu compatibility
function padStackAndMemory(steps) {
  return steps.map(step => ({
    ...step,
    stack: step.stack.map(padHexString),
    memory: step.memory.map(padHexString)
  }));
}

//turns Besu-style (begins with 0x, may be shorter than 64 hexdigits)
//to Geth/Ganache-style (no 0x, always 64 hexdigits)
//(I say 64 hexdigits rather than 32 bytes because Besu-style will use
//non-whole numbers of bytes!)
function padHexString(hexString) {
  return hexString.startsWith("0x") //Besu-style or Geth/Ganache-style?
    ? hexString.slice(2).padStart(2 * Codec.Evm.Utils.WORD_SIZE, "0") //convert Besu to Geth/Ganache
    : hexString; //leave Geth/Ganache style alone
}

function* fetchTransactionInfo(adapter, { txHash }) {
  debug("inspecting transaction");
  var trace;
  try {
    trace = yield apply(adapter, adapter.getTrace, [txHash]);
  } catch (e) {
    debug("putting error");
    yield put(actions.error(e));
    return;
  }

  debug("got trace");
  trace = padStackAndMemory(trace); //for Besu compatibility
  yield put(actions.receiveTrace(trace));

  const tx = yield apply(adapter, adapter.getTransaction, [txHash]);
  debug("tx %O", tx);
  const receipt = yield apply(adapter, adapter.getReceipt, [txHash]);
  debug("receipt %O", receipt);
  const block = yield apply(adapter, adapter.getBlock, [tx.blockNumber]);
  debug("block %O", block);
  const chainId = yield apply(adapter, adapter.getChainId);

  yield put(session.saveTransaction(tx));
  yield put(session.saveReceipt(receipt));
  yield put(session.saveBlock(block));

  //Solidity's block.difficulty uses the opcode 0x44.
  //This will return the difficulty (pre-merge) or the mixHash (post-merge).
  //So if the mixHash is present and nonzero, we use that.  Otherwise, we use
  //the difficulty.
  let difficultyOrMixHash;
  const mixHash = block.prevRandao || block.mixHash;
  //mixHash is given in hex, so we have to slice off the 0x;
  //BN doesn't accept 0n
  const numericMixHash = new BN(mixHash.slice(2), 16);
  if (numericMixHash.isZero()) {
    difficultyOrMixHash = new BN(block.difficulty);
  } else {
    difficultyOrMixHash = numericMixHash;
  }

  //these ones get grouped together for convenience
  const solidityBlock = {
    coinbase: block.miner,
    difficulty: difficultyOrMixHash,
    gaslimit: new BN(block.gasLimit),
    number: new BN(block.number),
    timestamp: new BN(block.timestamp),
    chainid: new BN(chainId), //key is lowercase because that's what Solidity does
    basefee: new BN(parseInt(block.baseFeePerGas)) //will be 0 if pre-London [new BN(NaN) yields 0]
    //note we need parseInt on basefee because some web3 versions return it as a hex string,
    //and BN doesn't allow for hex strings as input
  };

  if (tx.to != null) {
    yield put(
      actions.receiveCall({
        address: tx.to,
        data: tx.input,
        storageAddress: tx.to,
        status: receipt.status,
        sender: tx.from,
        value: new BN(tx.value),
        gasprice: new BN(tx.gasPrice),
        block: solidityBlock,
        blockHash: block.hash,
        txIndex: tx.transactionIndex
      })
    );
  } else {
    const storageAddress = Web3.utils.isAddress(receipt.contractAddress)
      ? receipt.contractAddress
      : Codec.Evm.Utils.ZERO_ADDRESS;
    yield put(
      actions.receiveCall({
        binary: tx.input,
        storageAddress,
        status: receipt.status,
        sender: tx.from,
        value: new BN(tx.value),
        gasprice: new BN(tx.gasPrice),
        block: solidityBlock,
        blockHash: block.hash,
        txIndex: tx.transactionIndex
      })
    );
  }
}

export function* inspectTransaction(txHash) {
  debug("putting");
  yield put(actions.inspect(txHash));

  debug("waiting");
  let action = yield take([actions.RECEIVE_TRACE, actions.ERROR_WEB3]);
  debug("action %o", action);

  var trace;
  if (action.type == actions.RECEIVE_TRACE) {
    trace = action.trace;
    debug("received trace");
  } else {
    return { error: action.error };
  }

  let {
    address,
    binary,
    data,
    storageAddress,
    status,
    sender,
    value,
    gasprice,
    block,
    blockHash,
    txIndex
  } = yield take(actions.RECEIVE_CALL);
  debug("received call");

  return {
    trace,
    address,
    binary,
    data,
    storageAddress,
    status,
    sender,
    value,
    gasprice,
    block,
    blockHash,
    txIndex
  };
}

//NOTE: the block argument is optional
export function* obtainBinaries(addresses, block) {
  let tasks = yield all(addresses.map(address => fork(receiveBinary, address)));

  debug("requesting binaries");
  yield all(addresses.map(address => put(actions.fetchBinary(address, block))));

  let binaries = [];
  binaries = yield join(tasks);

  debug("binaries %o", binaries);

  return binaries;
}

function* fetchBinary(adapter, { address, block }) {
  debug("fetching binary for %s", address);
  let binary = yield apply(adapter, adapter.getDeployedCode, [address, block]);

  debug("received binary for %s", address);
  yield put(actions.receiveBinary(address, binary));
}

function* receiveBinary(address) {
  let { binary } = yield take(
    action => action.type == actions.RECEIVE_BINARY && action.address == address
  );
  debug("got binary for %s", address);

  return binary;
}

export function* reverseEnsResolve(address) {
  debug("forking receive name for %s", address);
  const task = yield fork(receiveEnsName, address);
  debug("putting the action to kick things off");
  yield put(actions.reverseEnsResolve(address));
  debug("waiting (outer)");
  return yield join(task);
}

function* performEnsReverseResolution(adapter, { address }) {
  debug("got reverse action; address = %s", address);
  const name = yield apply(adapter, adapter.reverseEnsResolve, [address]);
  debug("got name = %o, passing it on", name);
  yield put(actions.receiveEnsName(address, name));
}

function* receiveEnsName(address) {
  debug("waiting (inner)");
  const { name } = yield take(
    action =>
      action.type == actions.RECEIVE_ENS_NAME && action.address == address
  );
  debug("received name = %s, returning", name);
  return name;
}

export function* ensResolve(name) {
  const task = yield fork(receiveEnsAddress, name);
  yield put(actions.ensResolve(name));
  return yield join(task);
}

function* performEnsResolution(adapter, { name }) {
  const address = yield apply(adapter, adapter.ensResolve, [name]);
  yield put(actions.receiveEnsAddress(name, address));
}

function* receiveEnsAddress(name) {
  const { address } = yield take(
    action => action.type == actions.RECEIVE_ENS_ADDRESS && action.name == name
  );
  return address;
}

export function* obtainStorage(address, slot, blockHash, txIndex) {
  debug("forking");
  const task = yield fork(function* () {
    return yield race({
      success: call(receiveStorage, address, slot),
      failure: call(receiveStorageErrorHandler)
    });
  });
  yield put(actions.fetchStorage(address, slot, blockHash, txIndex));
  debug("joining");
  const result = yield join(task);
  debug("result: %O", result);
  if (result.failure) {
    throw result.failure;
  } else {
    return result.success;
  }
}

function* fetchStorage(adapter, { address, slot, blockHash, txIndex }) {
  const slotAsHex = Codec.Conversion.toHexString(
    slot,
    Codec.Evm.Utils.WORD_SIZE
  );
  try {
    const word = yield apply(adapter, adapter.getExistingStorage, [
      address,
      slotAsHex,
      blockHash,
      txIndex
    ]);
    yield put(actions.receiveStorage(address, slot, word));
  } catch (error) {
    yield put(actions.receiveStorageFail(error));
  }
}

function* receiveStorage(address, slot) {
  const { word } = yield take(
    action =>
      action.type == actions.RECEIVE_STORAGE &&
      action.address == address &&
      action.slot.eq(slot) //remember, these are BNs
  );
  return word;
}

function* receiveStorageErrorHandler() {
  const { error } = yield take(actions.RECEIVE_STORAGE_FAIL);
  return error; //because this is forked, we need to return
  //rather than throw to prevent redux-saga from giving up
}

export function* init(provider, ensOptions) {
  yield put(actions.init(provider, ensOptions));
  yield take(actions.WEB3_READY);
}

export function* saga() {
  // wait for web3 init signal
  const { provider, ensOptions } = yield take(actions.INIT_WEB3);
  let adapter = new Web3Adapter(provider, ensOptions);
  yield apply(adapter, adapter.init); //set up ens

  yield takeEvery(actions.INSPECT, fetchTransactionInfo, adapter);
  yield takeEvery(actions.FETCH_BINARY, fetchBinary, adapter);
  yield takeEvery(actions.FETCH_STORAGE, fetchStorage, adapter);
  yield takeEvery(
    actions.REVERSE_ENS_RESOLVE,
    performEnsReverseResolution,
    adapter
  );
  yield takeEvery(actions.ENS_RESOLVE, performEnsResolution, adapter);
  yield put(actions.web3Ready());
}

export default prefixName("web3", saga);