Source: paths.js

/**
 * This module contains various utility functions for converting and
 * validating BIP32 derivation paths.
 *
 * @module paths
 */

import {MAINNET} from "./networks";
import {P2SH} from "./p2sh";
import {P2SH_P2WSH} from "./p2sh_p2wsh";
import {P2WSH} from "./p2wsh";

const HARDENING_OFFSET = Math.pow(2, 31);

const BIP32_PATH_REGEX = /^(m\/)?(\d+'?\/)*\d+'?$/;
const BIP32_HARDENED_PATH_REGEX = /^(m\/)?(\d+'\/)*\d+'$/;
const BIP32_UNHARDENED_PATH_REGEX = /^(m\/)?(\d+\/)*\d+$/;
const BIP32_INDEX_REGEX = /^\d+'?$/;
const MAX_BIP32_HARDENED_NODE_INDEX = Math.pow(2, 31) - 1;
const MAX_BIP32_NODE_INDEX = Math.pow(2, 32) - 1;

/**
 * Return the hardened version of the given BIP32 index.
 *
 * Hardening is equivalent to adding 2^31.
 *
 * @param {string|number} index - BIP32 index
 * @returns {number} the hardened index
 * @example
 * import {hardenedBIP32Index} from "unchained-bitcoin";
 * console.log(hardenedBIP32Index(44); // 2147483692
 */
export function hardenedBIP32Index(index) {
  return parseInt(index, 10) + HARDENING_OFFSET;
}

/**
 * Convert BIP32 derivation path to an array of integer values
 * representing the corresponding derivation indices.
 *
 * Hardened path segments will have the [hardening offset]{@link module:paths.HARDENING_OFFSET} added to the index.
 *
 * @param {string} pathString - BIP32 derivation path string
 * @returns {number[]} the derivation indices
 * @example
 * import {bip32PathToSequence} from "unchained-bitcoin";
 * console.log(bip32PathToSequence("m/45'/1/99")); // [2147483693, 1, 99]
 */
export function bip32PathToSequence(pathString) {
  const pathSegments = pathString.split("/").splice(1);
  return pathSegments.map(pathSegment => {
    if (pathSegment.substr(-1) === "'") {
      return parseInt(pathSegment.slice(0, -1), 10) + HARDENING_OFFSET;
    } else {
      return parseInt(pathSegment, 10);
    }
  });
}

/**
 * Convert a sequence of derivation indices into the corresponding
 * BIP32 derivation path.
 *
 * Indices above the [hardening offset]{@link * module:paths.HARDENING_OFFSET} will be represented wiith hardened * path segments (using a trailing single-quote).
 *
 * @param {number[]} sequence - the derivation indices
 * @returns {string} BIP32 derivation path
 * @example
 * import {bip32SequenceToPath} from "unchained-bitcoin";
 * console.log(bip32SequenceToPath([2147483693, 1, 99])); // m/45'/1/99
 */
export function bip32SequenceToPath(sequence) {
  return "m/" + sequence.map((index) => {
    if (index >= HARDENING_OFFSET) {
      return `${(index - HARDENING_OFFSET)}'`;
    } else {
      return index.toString();
    }
  }).join('/');
}

/**
 * Validate a given BIP32 derivation path string.
 *
 * - Path segments are validated numerically as well as statically
 *   (the value of 2^33 is an invalid path segment).
 *
 * - The `mode` option can be pass to validate fully `hardened` or
 *   `unhardened` paths.
 *
 * @param {string} pathString - BIP32 derivation path string
 * @param {Object} [options] - additional options
 * @param {string} [options.mode] - "hardened" or "unhardened"
 * @returns {string} empty if valid or corresponding validation message if not
 * @example
 * import {validateBIP32Path} from "unchained-bitcoin";
 * console.log(validateBIP32Path("")); // "BIP32 path cannot be blank."
 * console.log(validateBIP32Path("foo")); // "BIP32 path is invalid."
 * console.log(validateBIP32Path("//45")); // "BIP32 path is invalid."
 * console.log(validateBIP32Path("/45/")); // "BIP32 path is invalid."
 * console.log(validateBIP32Path("/45''")); // "BIP32 path is invalid."
 * console.log(validateBIP32Path('/45"')); // "BIP32 path is invalid."
 * console.log(validateBIP32Path("/-45")); // "BIP32 path is invalid."
 * console.log(validateBIP32Path("/8589934592")); // "BIP32 index is too high."
 * console.log(validateBIP32Path("/45")); // ""
 * console.log(validateBIP32Path("/45/0'")); // ""
 * console.log(validateBIP32Path("/45/0'", {mode: "hardened")); // "BIP32 path must be fully-hardened."
 * console.log(validateBIP32Path("/45'/0'", {mode: "hardened")); // ""
 * console.log(validateBIP32Path("/0'/0", {mode: "unhardened")); // "BIP32 path cannot include hardened segments."
 * console.log(validateBIP32Path("/0/0", {mode: "unhardened")); // ""
 */
export function validateBIP32Path(pathString, options) {
  if (pathString === null || pathString === undefined || pathString === '') {
    return "BIP32 path cannot be blank.";
  }

  if (!pathString.match(BIP32_PATH_REGEX)) {
    return "BIP32 path is invalid.";
  }

  if (options && options.mode === 'hardened') {
    if (!pathString.match(BIP32_HARDENED_PATH_REGEX)) {
      return "BIP32 path must be fully-hardened.";
    }
  }

  if (options && options.mode === 'unhardened') {
    if (!pathString.match(BIP32_UNHARDENED_PATH_REGEX)) {
      return "BIP32 path cannot include hardened segments.";
    }
  }

  const segmentStrings = pathString.toLowerCase().split('/');

  return validateBIP32PathSegments(segmentStrings.slice(1));
}

function validateBIP32PathSegments(segmentStrings) {
  for (let i = 0; i < segmentStrings.length; i++) {
    const indexString = segmentStrings[i];
    const error = validateBIP32Index(indexString);
    if (error !== '') {
      return error;
    }
  }
  return '';
}

/**
 * Validate a given BIP32 index string.
 *
 * - Path segments are validated numerically as well as statically
 *   (the value of 2^33 is an invalid path segment).
 *
 * - By default, 0-4294967295 and 0'-2147483647' are valid.
 *
 * - The `mode` option can be pass to validate index is hardened
 *   `unhardened` paths.
 *
 * - `hardened` paths include 0'-2147483647' and 2147483648-4294967295
 *
 * - `unharded` paths include 0-2147483647
 *
 * @param {string} indexString - BIP32 index string
 * @param {Object} [options] - additional options
 * @param {string} [options.mode] - "hardened" or "unhardened"
 * @returns {string} empty if valid or corresponding validation message if not
 * @example
 * import {validateBIP32Path} from "unchained-bitcoin";
 * console.log(validateBIP32Path("")); // "BIP32 index cannot be blank."
 * console.log(validateBIP32Path("foo")); // "BIP32 index is invalid."
 * console.log(validateBIP32Path("//45")); // "BIP32 index is invalid."
 * console.log(validateBIP32Path("/45/")); // "BIP32 index is invalid."
 * console.log(validateBIP32Index("4294967296")); // "BIP32 index is too high."
 * console.log(validateBIP32Index("2147483648'")); // "BIP32 index is too high."
 * console.log(validateBIP32Index("45", { mode: "hardened" })); // "BIP32 index must be hardened."
 * console.log(validateBIP32Index("45'", { mode: "unhardened" })); // "BIP32 index cannot be hardened."
 * console.log(validateBIP32Index("2147483648", {mode: "unhardened"})); // "BIP32 index cannot be hardened."
 * console.log(validateBIP32Index("45")); // ""
 * console.log(validateBIP32Index("45'")); // ""
 * console.log(validateBIP32Index("0")); // ""
 * console.log(validateBIP32Index("0'")); // ""
 * console.log(validateBIP32Index("4294967295")); // ""
 * console.log(validateBIP32Index("2147483647")); // ""
 * console.log(validateBIP32Index("2147483647'")); // ""
 */
export function validateBIP32Index(indexString, options) {
  if (indexString === null || indexString === undefined || indexString === '') {
    return "BIP32 index cannot be blank.";
  }

  if (!indexString.match(BIP32_INDEX_REGEX)) {
    return "BIP32 index is invalid.";
  }

  let numberString,
    hardened;
  if (indexString.substr(indexString.length - 1) === "'") {
    numberString = indexString.substr(0, indexString.length - 1);
    hardened = true;
  } else {
    numberString = indexString;
    hardened = false;
  }

  // This comes after the regex, so no need to test that parseInt fails.
  const numberError = "Invalid BIP32 index.";
  let number = parseInt(numberString, 10);

  if (Number.isNaN(number) || number.toString().length !== numberString.length) {
    return numberError;
  }

  // allows up to 4294967295 or 2147483647'
  if (number > (hardened ? MAX_BIP32_HARDENED_NODE_INDEX : MAX_BIP32_NODE_INDEX)) {
    return "BIP32 index is too high.";
  }

  // allows 0'-2147483647' or 2147483648-4294967295
  if (options && options.mode === 'hardened') {
    if (!hardened && number <= MAX_BIP32_HARDENED_NODE_INDEX) {
      return "BIP32 index must be hardened.";
    }
  }

  // allows 0-2147483647
  if (options && options.mode === 'unhardened') {
    if (hardened || number > MAX_BIP32_HARDENED_NODE_INDEX) {
      return "BIP32 index cannot be hardened.";
    }
  }


  return '';
}

/**
 * Return the default BIP32 root derivation path for the given
 * `addressType` and `network`.
 *
 * - Mainnet:
 *   - P2SH: m/45'/0'/0'
 *   - P2SH-P2WSH: m/48'/0'/0'/1'
 *   - P2WSH: m/48'/0'/0'/2'
 * - Testnet:
 *   - P2SH: m/45'/1'/0'
 *   - P2SH-P2WSH: m/48'/1'/0'/1'
 *   - P2WSH: m/48'/1'/0'/2'
 *
 * @param {module:multisig.MULTISIG_ADDRESS_TYPES} addressType - address type
 * @param {module:networks.NETWORKS} network - bitcoin network
 * @returns {string} derivation path
 * @example
 * import {multisigBIP32Root} from "unchained-bitcoin";
 * console.log(multisigBIP32Root(P2SH, MAINNET)); // m/45'/0'/0'
 * console.log(multisigBIP32Root(P2SH_P2WSH, TESTNET); // m/48'/1'/0'/1'
 */
export function multisigBIP32Root(addressType, network) {
  const coinPath = (network === MAINNET ? "0'" : "1'");
  switch (addressType) {
    case P2SH:
      return `m/45'/${coinPath}/0'`;
    case P2SH_P2WSH:
      return `m/48'/${coinPath}/0'/1'`;
    case P2WSH:
      return `m/48'/${coinPath}/0'/2'`;
    default:
      return null;
  }
}

/**
 * Returns a BIP32 path at the given `relativePath` under the default
 * BIP32 root path for the given `addressType` and `network`.
 *
 * @param {module:multisig.MULTISIG_ADDRESS_TYPES} addressType - type from which to calculate BIP32 root path
 * @param {module:networks.NETWORKS} network - bitcoin network from which to calculate BIP32 root path
 * @param {number|string} relativePath - the relative BIP32 path (no leading `/`)
 * @returns {string} child BIP32 path
 * @example
 * import {multisigBIP32Path} from "unchained-bitcoin";
 * console.log(multisigBIP32Path(P2SH, MAINNET, 0); // m/45'/0'/0'/0
 * console.log(multisigBIP32Path(P2SH_P2WSH, TESTNET, "3'/4"); // m/48'/1'/0'/1'/3'/4"
 */
export function multisigBIP32Path(addressType, network, relativePath) {
  const root = multisigBIP32Root(addressType, network);
  if (root) {
    return root + `/${relativePath || "0"}`;
  }
  return null;
}

/**
 * Get the path of the parent of the given path
 * @param {string} bip32Path e.g. "m/45'/0'/0'/0"
 * @returns {string} parent path
 * @example
 * import {getParentBIP32Path} from "unchained-bitcoin";
 * console.log(getParentBIP32Path("m/45'/0'/0'/0"); // m/45'/0'/0'
 */
export function getParentBIP32Path(bip32Path) {
  // first validate the input
  let validated = validateBIP32Path(bip32Path);
  if (validated.length) return validated;
  // then slice off then last item in the path
  return bip32Path.split("/").slice(0, -1).join("/");
}

/**
 * Get the path of under the parentBIP32Path of the given path
 * @param {string} parentBIP32Path e.g. "m/45'/0'/0'"
 * @param {string} childBIP32Path e.g. "m/45'/0'/0'/0/1/2"
 * @returns {string} relative path below path
 * @example
 * import {getRelativeBIP32Path} from "unchained-bitcoin";
 * console.log(getRelativeBIP32Path("m/45'/0'/0'", "m/45'/0'/0'/0/1/2"); // 0/1/2
 */
export function getRelativeBIP32Path(parentBIP32Path, childBIP32Path) {
  if (parentBIP32Path === childBIP32Path) return '';
  // first validate the parentBIP32Path
  let validatedParent = validateBIP32Path(parentBIP32Path);
  if (validatedParent.length) return validatedParent;
  // next validate the input
  let validatedChild = validateBIP32Path(childBIP32Path);
  if (validatedChild.length) return validatedChild;
  // check that childBIP32Path starts with parentBIP32Path
  if (!childBIP32Path.startsWith(parentBIP32Path)) return `The provided bip32Path does not start with the chroot.`
  // then return the relative path beyond the parentBIP32Path.

  return childBIP32Path.slice(parentBIP32Path.length + 1);
}