Source: braid.js

/**
 * This module provides functions for braids, which is how we define
 * a group of xpubs with some additional multisig information to define
 * a multisig setup. Sometimes, the word `wallet` is used here, but we
 * view the traditional use of the word 'wallet' as a collection of Braids.
 *
 * @module braid
 */

import { Struct } from "bufio";
import assert from "assert";
import {
  bip32PathToSequence,
  validateBIP32Index,
  validateBIP32Path,
} from "./paths";
import { NETWORKS } from "./networks";
import {
  MULTISIG_ADDRESS_TYPES,
  generateMultisigFromPublicKeys,
} from "./multisig";
import {
  validateExtendedPublicKey,
  deriveChildPublicKey,
  extendedPublicKeyRootFingerprint,
} from "./keys";

// In building the information objects that PSBTs want, one must include information
// about the root fingerprint for the device. If that information is unknown, just fill
// it in with zeros.
const FAKE_ROOT_FINGERPRINT = "00000000";

/**
 * Struct object for encoding and decoding braids.
 *
 * @param {string} options.network = mainnet - mainnet or testnet
 * @param {string} options.addressType P2SH, P2SH-P2WSH, P2WSH
 * @param {ExtendedPublicKey[]} options.extendedPublicKeys ExtendedPublicKeys that make up this braid
 * @param {number} options.requiredSigners - how many required signers in this braid
 * @param {string} options.index - One value, relative, to add on to all xpub absolute bip32paths (usually 0=deposit, 1=change)
 */
export class Braid extends Struct {
  constructor(options) {
    super();
    if (!options || !Object.keys(options).length) {
      return this;
    }

    assert(
      Object.values(MULTISIG_ADDRESS_TYPES).includes(options.addressType),
      `Expected addressType to be one of:  ${Object.values(
        MULTISIG_ADDRESS_TYPES
      )}. You sent ${options.addressType}`
    );
    this.addressType = options.addressType;
    assert(
      Object.values(NETWORKS).includes(options.network),
      `Expected network to be one of:  ${NETWORKS}.`
    );
    this.network = options.network;

    options.extendedPublicKeys.forEach((xpub) => {
      const xpubValidationError = validateExtendedPublicKey(
        typeof xpub === "string" ? xpub : xpub.base58String,
        this.network
      );
      assert(!xpubValidationError.length, xpubValidationError);
    });
    this.extendedPublicKeys = options.extendedPublicKeys;

    assert(typeof options.requiredSigners === "number");
    assert(
      options.requiredSigners <= this.extendedPublicKeys.length,
      `Can't have more requiredSigners than there are keys.`
    );
    this.requiredSigners = options.requiredSigners;

    // index is a technically a bip32path, but it's also just an
    // unhardened index (single number) - if we think of the bip32path as a
    // filepath, then this is a directory that historically/typically tells you
    // deposit (0) or change (1) braid, but could be any unhardened index.
    const pathError = validateBIP32Index(options.index, { mode: "unhardened" });
    assert(!pathError.length, pathError);
    this.index = options.index;
    this.sequence = bip32PathToSequence(this.index);
  }

  toJSON() {
    return braidConfig(this);
  }

  static fromData(data) {
    return new this(data);
  }

  static fromJSON(string) {
    return new this(JSON.parse(string));
  }
}

/**
 * @param {Braid} braid A Braid struct to be 'exported'
 * @returns {string} string of JSON data which can used to reconstitute the Braid later
 */
export function braidConfig(braid) {
  return JSON.stringify({
    network: braid.network,
    addressType: braid.addressType,
    extendedPublicKeys: braid.extendedPublicKeys,
    requiredSigners: braid.requiredSigners,
    index: braid.index,
  });
}

/**
 * Returns the braid's network
 * @param {Braid} braid the braid to interrogate
 * @returns {string} network string testnet/mainnet
 */
export function braidNetwork(braid) {
  return braid.network;
}

/**
 * Returns the braid's addressType
 * @param {Braid} braid the braid to interrogate
 * @returns {string} address type p2sh/p2sh-p2wsh/p2wsh
 */
export function braidAddressType(braid) {
  return braid.addressType;
}

/**
 * Returns the braid's extendedPublicKeys
 * @param {Braid} braid the braid to interrogate
 * @returns {ExtendedPublicKey[]} array of ExtendedPublicKeys in the braid
 */
export function braidExtendedPublicKeys(braid) {
  return braid.extendedPublicKeys;
}

/**
 * Returns the braid's requiredSigners
 * @param {Braid} braid the braid to interrogate
 * @returns {number} number of required signers
 */
export function braidRequiredSigners(braid) {
  return braid.requiredSigners;
}

/**
 * Returns the braid's index
 * @param {Braid} braid the braid to interrogate
 * @returns {string} index (singular) for the braid: 0 = deposit, 1 = change
 */
export function braidIndex(braid) {
  return braid.index;
}

/**
 * Validate that a requested path is derivable from a particular braid
 * e.g. it's both a valid bip32path *and* its first index is the same as the index
 *
 * @param {Braid} braid the braid to interrogate
 * @param {string} path the path to validate
 * @returns {void} the assertions will fire errors if invalid
 */
export function validateBip32PathForBraid(braid, path) {
  const pathError = validateBIP32Path(path);
  assert(!pathError.length, pathError);

  // The function bip32PathToSequence blindly slices the first index after splitting on '/',
  // so make sure the slash is there. E.g. a path of "0/0" would validate in the above function,
  // but fail to do what we expect here unless we prepend '/' as '/0/0'.
  const pathToCheck =
    path.startsWith("m/") || path.startsWith("/") ? path : "/" + path;
  const pathSequence = bip32PathToSequence(pathToCheck);
  assert(
    pathSequence[0].toString() === braid.index,
    `Cannot derive paths outside of the braid's index: ${braid.index}`
  );
}

/**
 * Returns an object with a braid's pubkeys + bip32derivation info
 * at a particular path (respects the index)
 *
 * @param {Braid} braid the braid to interrogate
 * @param {string} path what suffix to generate pubkeys at
 * @returns {Object} Object where the keys make up an array of public keys at a particular path and the values are the bip32Derivations (used in other places)
 */
function derivePublicKeyObjectsAtPath(braid, path) {
  validateBip32PathForBraid(braid, path);
  const dataRichPubKeyObjects = {};
  const actualPathSuffix = path.startsWith("m/") ? path.slice(2) : path;

  braidExtendedPublicKeys(braid).forEach((xpub) => {
    const completePath = xpub.path + "/" + actualPathSuffix;
    // Provide ability to work whether this was called with plain xpub strings or with xpub structs
    const pubkey = deriveChildPublicKey(
      typeof xpub === "string" ? xpub : xpub.base58String,
      path,
      braidNetwork(braid)
    );
    // It's ok if this is faked - but at least one of them should be correct otherwise
    // signing won't work. On Coldcard, this must match what was included in the multisig
    // wallet config file.
    const rootFingerprint = extendedPublicKeyRootFingerprint(xpub);
    const masterFingerprint = rootFingerprint
      ? rootFingerprint
      : FAKE_ROOT_FINGERPRINT;
    dataRichPubKeyObjects[pubkey] = {
      masterFingerprint: Buffer.from(masterFingerprint, "hex"),
      path: completePath,
      pubkey: Buffer.from(pubkey, "hex"),
    };
  });
  return dataRichPubKeyObjects;
}

/**
 * Returns the braid's pubkeys at particular path (respects the index)
 *
 * @param {Braid} braid the braid to interrogate
 * @param {string} path the suffix to generate pubkeys at
 * @returns {string[]} array of sorted (BIP67) public keys at a particular index from the braid
 */
export function generatePublicKeysAtPath(braid, path) {
  return Object.keys(derivePublicKeyObjectsAtPath(braid, path)).sort(); // BIP67
}

/**
 * Returns the braid's pubkeys at particular index under the index
 *
 * @param {Braid} braid the braid to interrogate
 * @param {number} index the suffix to generate pubkeys at
 * @returns {string[]} array of public keys at a particular index from the braid
 */
export function generatePublicKeysAtIndex(braid, index) {
  let pathToDerive = braidIndex(braid);
  pathToDerive += "/" + index.toString();
  return generatePublicKeysAtPath(braid, pathToDerive);
}

/**
 * Returns the braid's bip32PathDerivation (array of bip32 infos)
 * @param {Braid} braid the braid to interrogate
 * @param {string} path what suffix to generate bip32PathDerivation at
 * @returns {Object[]} array of getBip32Derivation objects
 */
export function generateBip32DerivationByPath(braid, path) {
  return Object.values(derivePublicKeyObjectsAtPath(braid, path));
}

/**
 * Returns the braid's bip32PathDerivation at a particular index (array of bip32 info)
 * @param {Braid} braid the braid to interrogate
 * @param {number} index what suffix to generate bip32PathDerivation at
 * @returns {Object[]} array of getBip32Derivation objects
 */
export function generateBip32DerivationByIndex(braid, index) {
  let pathToDerive = braidIndex(braid); // deposit or change
  pathToDerive += "/" + index.toString();
  return generateBip32DerivationByPath(braid, pathToDerive);
}

/**
 * Returns a braid-aware Multisig object at particular path (respects index)
 * @param {Braid} braid the braid to interrogate
 * @param {string} path what suffix to generate the multisig at
 * @returns {module:multisig.Multisig} braid-aware MULTISIG object at path
 */
export function deriveMultisigByPath(braid, path) {
  const pubkeys = generatePublicKeysAtPath(braid, path);
  const bip32Derivation = generateBip32DerivationByPath(braid, path);
  return generateBraidAwareMultisigFromPublicKeys(
    braid,
    pubkeys,
    bip32Derivation
  );
}

/**
 * Returns a braid-aware Multisig object at particular index
 * @param {Braid} braid the braid to interrogate
 * @param {number} index what suffix to generate the multisig at
 * @returns {module:multisig.Multisig} braid-aware MULTISIG object at index
 */
export function deriveMultisigByIndex(braid, index) {
  let pathToDerive = braidIndex(braid);
  pathToDerive += "/" + index.toString();
  return deriveMultisigByPath(braid, pathToDerive);
}

/**
 * Returns a braid-aware Multisig object from a set of public keys
 *
 * @param {Braid} braid the braid to interrogate
 * @param {string[]} pubkeys what suffix to generate the multisig at
 * @param {Object[]} bip32Derivation this is the array of bip32info for each member of the multisig
 * @returns {module:multisig.Multisig} braid-aware MULTISIG object
 */
function generateBraidAwareMultisigFromPublicKeys(
  braid,
  pubkeys,
  bip32Derivation
) {
  const multisig = generateMultisigFromPublicKeys(
    braidNetwork(braid),
    braidAddressType(braid),
    braidRequiredSigners(braid),
    ...pubkeys
  );
  multisig.braidDetails = braidConfig(braid);
  multisig.bip32Derivation = bip32Derivation;
  return multisig;
}

/**
 * Generate a braid from its parts
 *
 * @param {string} network - mainnet or testnet
 * @param {string} addressType - P2SH/P2SH-P2WSH/P2WSH
 * @param {module:keys.ExtendedPublicKey[]} extendedPublicKeys - array of xpubs that make up the braid
 * @param {number} requiredSigners - number signers needed to sign
 * @param {string} index (usually deposit/change) - e.g. '0' or '1'
 * @returns {Braid} Braid struct is returned
 */
export function generateBraid(
  network,
  addressType,
  extendedPublicKeys,
  requiredSigners,
  index
) {
  return new Braid({
    network,
    addressType,
    extendedPublicKeys,
    requiredSigners,
    index,
  });
}