Home Reference Source

lib/data/decode/utils.js

import debugModule from "debug";
const debug = debugModule("debugger:data:decode:utils");

import { BigNumber } from "bignumber.js";
import Web3 from "web3";

export const WORD_SIZE = 0x20;
export const MAX_WORD = new BigNumber(2).pow(256).minus(1);

/**
 * recursively converts big numbers into something nicer to look at
 */
export function cleanBigNumbers(value) {
  if (BigNumber.isBigNumber(value)) {
    return value.toNumber();

  } else if (value && value.map != undefined) {
    return value.map( (inner) => cleanBigNumbers(inner) );

  } else if (value && typeof value == "object") {
    return Object.assign(
      {}, ...Object.entries(value)
        .map( ([key, inner]) => ({ [key]: cleanBigNumbers(inner) }) )
    );

  } else {
    return value;
  }
}

export function typeIdentifier(definition) {
  return definition.typeDescriptions.typeIdentifier;
}

/**
 * returns basic type class for a variable definition node
 * e.g.:
 *  `t_uint256` becomes `uint`
 *  `t_struct$_Thing_$20_memory_ptr` becomes `struct`
 */
export function typeClass(definition) {
  return typeIdentifier(definition).match(/t_([^$_0-9]+)/)[1];
}

/**
 * Allocate storage for given variable declarations
 *
 * Postcondition: starts a new slot and occupies whole slots
 */
export function allocateDeclarations(
  declarations,
  refs,
  slot = 0,
  index = WORD_SIZE - 1,
  path = []
) {
  if (index < WORD_SIZE - 1) {  // starts a new slot
    slot++;
    index = WORD_SIZE - 1;
  }

  let parentFrom = { slot, index: 0 };
  var parentTo = { slot, index: WORD_SIZE - 1 };
  let mapping = {};

  for (let declaration of declarations) {
    let { from, to, next, children } =
      allocateDeclaration(declaration, refs, slot, index);

    mapping[declaration.id] = { from, to, name: declaration.name };
    if (children !== undefined) {
      mapping[declaration.id].children = children;
    }

    slot = next.slot;
    index = next.index;

    parentTo = { slot: to.slot, index: WORD_SIZE - 1 };
  }

  if (index < WORD_SIZE - 1) {
    slot++;
    index = WORD_SIZE - 1;
  }

  return {
    from: parentFrom,
    to: parentTo,
    next: { slot, index },
    children: mapping
  };
}

function allocateValue(slot, index, bytes) {
  let from = index - bytes + 1 >= 0 ?
    { slot, index: index - bytes + 1 } :
    { slot: slot + 1, index: WORD_SIZE - bytes };

  let to = { slot: from.slot, index: from.index + bytes - 1 };

  let next = from.index == 0 ?
    { slot: from.slot + 1, index: WORD_SIZE - 1 } :
    { slot: from.slot, index: from.index - 1 };

  return { from, to, next };
}

function allocateDeclaration(declaration, refs, slot, index) {
  let definition = refs[declaration.id].definition;
  var byteSize = storageSize(definition);  // yum

  if (typeClass(definition) != "struct") {
    return allocateValue(slot, index, byteSize);
  }

  let struct = refs[definition.typeName.referencedDeclaration];
  debug("struct: %O", struct);

  let result =  allocateDeclarations(struct.variables || [], refs, slot, index);
  debug("struct result %o", result);
  return result;
}

/**
 * e.g. uint48 -> 6
 * @return size in bytes for explicit type size, or `null` if not stated
 */
export function specifiedSize(definition) {
  let specified = typeIdentifier(definition).match(/t_[a-z]+([0-9]+)/);

  if (!specified) {
    return null;
  }

  let num = specified[1];

  switch (typeClass(definition)) {
    case "int":
    case "uint":
      return num / 8;

    case "bytes":
      return num;

    default:
      debug("Unknown type for size specification: %s", typeIdentifier(definition));
  }
}

export function storageSize(definition) {
  switch (typeClass(definition)) {
    case "bool":
      return 1;

    case "address":
      return 20;

    case "int":
    case "uint":
      // is this a HACK? ("256" / 8)
      return typeIdentifier(definition).match(/t_[a-z]+([0-9]+)/)[1] / 8;

    case "string":
    case "bytes":
    case "array":
      return WORD_SIZE;
  }
}

export function isReference(definition) {
  return typeIdentifier(definition).match(/_(memory|storage)(_ptr)?$/) != null;
}

export function referenceType(definition) {
  return typeIdentifier(definition).match(/_([^_]+)(_ptr)?$/)[1];
}

export function baseDefinition(definition) {
  let baseIdentifier = typeIdentifier(definition)
    // first dollar sign     last dollar sign
    //   `---------.       ,---'
    .match(/^[^$]+\$_(.+)_\$[^$]+$/)[1]
    //              `----' greedy match

  // HACK - internal types for memory or storage also seem to be pointers
  if (baseIdentifier.match(/_(memory|storage)$/) != null) {
    baseIdentifier = `${baseIdentifier}_ptr`;
  }

  // another HACK - we get away with it becausewe're only using that one property
  return {
    typeDescriptions: {
      typeIdentifier: baseIdentifier
    }
  };
}


export function toBigNumber(bytes) {
  if (bytes == undefined) {
    return undefined;
  } else if (typeof bytes == "string") {
    return new BigNumber(bytes, 16);
  } else if (typeof bytes == "number" || BigNumber.isBigNumber(bytes)) {
    return new BigNumber(bytes);
  } else if (bytes.reduce) {
    return bytes.reduce(
      (num, byte) => num.times(0x100).plus(byte),
      new BigNumber(0)
    );
  }
}

export function toSignedBigNumber(bytes) {
  if (bytes[0] < 0b10000000) {  // first bit is 0
    return toBigNumber(bytes);
  } else {
    return toBigNumber(bytes.map( (b) => 0xff - b )).plus(1).negated();
  }
}

/**
 * @param bytes - Uint8Array
 * @param length - desired byte length (pad with zeroes)
 * @param trim - omit leading zeroes
 */
export function toHexString(bytes, length = 0, trim = false) {
  if (typeof length == "boolean") {
    trim = length;
    length = 0;
  }

  if (BigNumber.isBigNumber(bytes)) {
    bytes = toBytes(bytes);
  }

  const pad = (s) => `${"00".slice(0, 2 - s.length)}${s}`

  //                                          0  1  2  3  4
  //                                 0  1  2  3  4  5  6  7
  // bytes.length:        5  -  0x(          e5 c2 aa 09 11 )
  // length (preferred):  8  -  0x( 00 00 00 e5 c2 aa 09 11 )
  //                                `--.---'
  //                                     offset 3
  if (bytes.length < length) {
    let prior = bytes;
    bytes = new Uint8Array(length);

    bytes.set(prior, length - prior.length);
  }

  debug("bytes: %o", bytes);

  let string = bytes.reduce(
    (str, byte) => `${str}${pad(byte.toString(16))}`, ""
  );

  if (trim) {
    string = string.replace(/^(00)+/, "");
  }

  if (string.length == 0) {
    string = "00";
  }

  return `0x${string}`;
}

export function toBytes(number, length = 0) {
  if (number < 0) {
    return [];
  }

  let hex = number.toString(16);
  if (hex.length % 2 == 1) {
    hex = `0${hex}`;
  }

  let bytes = new Uint8Array(
    hex.match(/.{2}/g)
      .map( (byte) => parseInt(byte, 16) )
  );

  if (bytes.length < length) {
    let prior = bytes;
    bytes = new Uint8Array(length);
    bytes.set(prior, length - prior.length);
  }

  return bytes;
}

export function keccak256(...args) {
  let web3 = new Web3();

  args = args.map( (arg) => {
    if (typeof arg == "number" || BigNumber.isBigNumber(arg)) {
      return toHexString(toBytes(arg, WORD_SIZE)).slice(2)
    } else if (typeof arg == "string") {
      return web3.toHex(arg).slice(2);
    } else {
      return "";
    }
  });

  let sha = web3.sha3(args.join(''), { encoding: 'hex' });
  debug("sha %o", sha);
  return toBigNumber(sha);
}