Source: hermit.js

/**
 * Provides classes for interacting with Hermit.
 *
 * Hermit uses the Blockchain Commons UR encoding for data IO with
 * individual UR parts represented as QR codes.
 *
 * When receiving data from Hermit, calling applications are
 * responsible for parsing UR parts from the animated sequence of QR
 * codes Hermit displays.  The `BCURDecode` class is designed to make
 * this easy.
 *
 * When sending data to Hermit, these interaction classes encode the
 * data into UR parts.  Calling applications are responsible for
 * displaying these UR parts as an animated QR code sequence.
 *
 * The following API classes are implemented:
 *
 * * HermitExportExtendedPublicKey
 * * HermitSignMultisigTransaction
 *
 * @module hermit
 */
import {
  parseSignaturesFromPSBT,
} from "unchained-bitcoin";
import {
  IndirectKeystoreInteraction,
  PENDING,
  ACTIVE,
  INFO,
  ERROR,
} from "./interaction";
import {BCUREncoder} from "./bcur";

export const HERMIT = 'hermit';

function commandMessage(data) {
  return {
    ...{
      state: PENDING,
      level: INFO,
      code: "hermit.command",
      mode: "wallet",
    },
    ...{text: `${data.instructions} '${data.command}'`},
    ...data,
  };
}

/**
 * Base class for interactions with Hermit.
 *
 * @extends {module:interaction.IndirectKeystoreInteraction}
 */
export class HermitInteraction extends IndirectKeystoreInteraction {

  messages() {
    const messages = super.messages();
    messages.push({
      state: ACTIVE,
      level: INFO,
      code: "hermit.scanning",
      text: "Scan Hermit QR code sequence now.",
    });
    return messages;
  }

}

/**
 * Reads an extended public key from data returned by Hermit's
 * `display-xpub` command.
 *
 * This interaction class works in tandem with the `BCURDecoder`
 * class.  The `BCURDecoder` parses data from Hermit, this class
 * interprets it.
 * 
 * @extends {module:hermit.HermitInteraction}
 * @example
 * // Hermit returns a descriptor encoded as hex through BC-UR.  Some
 * // application function needs to work with the BCURDecoder class to
 * // parse this data.
 * const descriptorHex = readQRCodeSequence();
 * 
 * // The interaction parses the data from Hermit
 * const interaction = new HermitExportExtendedPublicKey();
 * const {xpub, bip32Path, rootFingerprint} = interaction.parse(descriptorHex);
 * 
 * console.log(xpub);
 * // "xpub..."
 * 
 * console.log(bip32Path);
 * // "m/45'/0'/0'"
 * 
 * console.log(rootFingerprint);
 * // "abcdefgh"
 * 
 */

// FIXME -- move all this descriptor regex and extraction stuff to unchained-bitcoin
const DESCRIPTOR_REGEXP = new RegExp("^\\[([a-fA-F0-9]{8})((?:/[0-9]+'?)+)\\]([a-km-zA-NP-Z1-9]+)$");

export class HermitExportExtendedPublicKey extends HermitInteraction {

  constructor({bip32Path}) {
    super();
    this.bip32Path = bip32Path;
  }

  messages() {
    const messages = super.messages();
    messages.push(commandMessage({
      instructions: "Run the following Hermit command, replacing the BIP32 path if you need to:",
      command: `display-xpub ${this.bip32Path}`,
    }));
    return messages;
  }

  parse(descriptorHex) {
    if (!descriptorHex) {
      throw new Error("No descriptor received from Hermit.");
    }
    const descriptor = Buffer.from(descriptorHex, 'hex').toString('utf8');
    const result = descriptor.match(DESCRIPTOR_REGEXP);
    if (result && result.length == 4) {
      return {
        rootFingerprint: result[1],
        bip32Path: `m${result[2]}`,
        xpub: result[3],
      };
    } else {
      throw new Error("Invalid descriptor received from Hermit.");
    }
  }
}

/**
 * Displays a signature request for Hermit's `sign` command and reads
 * the resulting signature.
 *
 * This interaction class works in tandem with the `BCURDecoder`
 * class.  The `BCURDecoder` parses data from Hermit, this class
 * interprets it.
 *
 * @extends {module:hermit.HermitInteraction}
 * @example
 * const interaction = new HermitSignMultisigTransaction({psbt});
 * const urParts = interaction.request();
 * console.log(urParts);
 * // [ "ur:...", "ur:...", ... ]
 *
 * // Some application function which knows how to display an animated
 * // QR code sequence.
 * displayQRCodeSequence(urParts);
 *
 * // Hermit returns a PSBT encoded as hex through BC-UR.  Some
 * // application function needs to work with the BCURDecoder class to
 * // parse this data.
 * const signedPSBTHex = readQRCodeSequence();
 *
 * // The interaction parses the data from Hermit.
 * const signedPSBTBase64 = interaction.parse(signedPSBTHex);
 * console.log(signedPSBTBase64);
 * // "cHNidP8B..."
 *
 */
export class HermitSignMultisigTransaction extends HermitInteraction {

  /**
   *
   * @param {object} options - options argument
   * @param {array<object>} options.psbt - unsigned PSBT to sign
   * @param {bool} options.returnSignatureArray - return a signed PSBT or an array of signatures (useful in Caravan's testing app)
   */
  constructor({psbt, returnSignatureArray=false}) {
    super();
    this.psbt = psbt;
    this.workflow.unshift("request");
    this.returnSignatureArray = returnSignatureArray;
  }

  messages() {
    const messages = super.messages();

    messages.push(commandMessage({
      instructions: "Run the following Hermit command to scan this signature request:",
      command: "sign",
    }));

    if (!this.psbt) {
      messages.push({
        state: PENDING,
        level: ERROR,
        code: "hermit.sign",
        text: "PSBT is required.",
      });
    }
    
    // FIXME validate PSBT!

    return messages;
  }

  request() {
    const unsignedPSBTHex = Buffer.from(this.psbt, 'base64').toString('hex');
    const encoder = new BCUREncoder(unsignedPSBTHex);
    return encoder.parts();
  }

  parse(signedPSBTHex) {
    if (!signedPSBTHex) {
      throw new Error("No signature received from Hermit.");
    }
    if (this.returnSignatureArray) {
      const signatures = parseSignaturesFromPSBT(signedPSBTHex);
      return Object.values(signatures)[0];
    } else {
      return Buffer.from(signedPSBTHex, 'hex').toString('base64');
    }
  }

}