Source: ledger.js

/**
 * This module provides classes for Ledger hardware wallets.
 *
 * The base classes provided are `LedgerDashboardInteraction` and
 * `LedgerBitcoinInteraction` for interactions requiring being in the
 * Ledger dashboard vs. bitcoin app, respectively.
 *
 * The following API classes are implemented:
 *
 * * LedgerGetMetadata
 * * LedgerExportPublicKey
 * * LedgerExportExtendedPublicKey
 * * LedgerSignMultisigTransaction
 *
 * @module ledger
 */

import {
  bip32PathToSequence,
  hardenedBIP32Index,
  compressPublicKey,
  scriptToHex,
  multisigRedeemScript,
  multisigWitnessScript,
  P2SH,
  P2SH_P2WSH,
  P2WSH,
  multisigAddressType,
  getParentBIP32Path,
  getFingerprintFromPublicKey,
  deriveExtendedPublicKey,
  unsignedMultisigTransaction,
  validateBIP32Path,
  fingerprintToFixedLengthHex,
  translatePSBT,
  addSignaturesToPSBT,
} from "unchained-bitcoin";

import {
  ACTIVE,
  PENDING,
  INFO,
  WARNING,
  ERROR,
  DirectKeystoreInteraction,
} from "./interaction";

import {splitTransaction} from "@ledgerhq/hw-app-btc/lib/splitTransaction";
import {serializeTransactionOutputs} from "@ledgerhq/hw-app-btc/lib/serializeTransaction";

/**
 * Constant defining Ledger interactions.
 *
 * @type {string}
 * @default ledger
 */
export const LEDGER = 'ledger';

const TransportU2F = require("@ledgerhq/hw-transport-u2f").default;
const TransportWebUSB = require("@ledgerhq/hw-transport-webusb").default;
const LedgerBtc = require("@ledgerhq/hw-app-btc").default;

/**
 * Constant representing the action of pushing the left button on a
 * Ledger device.
 *
 * @type {string}
 * @default 'ledger_left_button'
 */
export const LEDGER_LEFT_BUTTON = 'ledger_left_button';

/**
 * Constant representing the action of pushing the right button on a
 * Ledger device.
 *
 * @type {string}
 * @default 'ledger_right_button'
 */
export const LEDGER_RIGHT_BUTTON = 'ledger_right_button';

/**
 * Constant representing the action of pushing both buttons on a
 * Ledger device.
 *
 * @type {string}
 * @default 'ledger_both_buttons'
 */
export const LEDGER_BOTH_BUTTONS = 'ledger_both_buttons';


/**
 * Base class for interactions with Ledger hardware wallets.
 *
 * Subclasses must implement their own `run()` method.  They may use
 * the `withTransport` and `withApp` methods to connect to the Ledger
 * API's transport or app layers, respectively.
 *
 * Errors are not caught, so users of this class (and its subclasses)
 * should use `try...catch` as always.
 *
 * @extends {module:interaction.DirectKeystoreInteraction}
 * @example
 * import {LedgerInteraction} from "unchained-wallets";
 * // Simple subclass
 *
 * class SimpleLedgerInteraction extends LedgerInteraction {
 *
 *   constructor({param}) {
 *     super({});
 *     this.param =  param;
 *   }
 *
 *   async run() {
 *     return await this.withApp(async (app, transport) => {
 *       return app.doSomething(this.param); // Not a real Ledger API call
 *     });
 *   }
 *
 * }
 *
 * // usage
 * const interaction = new SimpleLedgerInteraction({param: "foo"});
 * const result = await interaction.run();
 * console.log(result); // whatever value `app.doSomething(...)` returns
 *
 */
export class LedgerInteraction extends DirectKeystoreInteraction {

  /**
   * Adds `pending` messages at the `info` level about ensuring the
   * device is plugged in (`device.connect`) and unlocked
   * (`device.unlock`).  Adds an `active` message at the `info` level
   * when communicating with the device (`device.active`).
   *
   * @return {module:interaction.Message[]} messages for ths interaction
   */
  messages() {
    const messages = super.messages();
    messages.push({
      state: PENDING,
      level: INFO,
      text: "Please plug in and unlock your Ledger.",
      code: "device.setup",
    });
    messages.push({
      state: ACTIVE,
      level: INFO,
      text: "Communicating with Ledger...",
      code: "device.active",
    });
    return messages;
  }

  /**
   * Can be called by a subclass during its `run()` method.
   *
   * Creates a transport layer connection and passes control to the
   * `callback` function, with the transport API as the first argument
   * to the function.
   *
   * See the [Ledger API]{@link https://github.com/LedgerHQ/ledgerjs} for general information or a [specific transport API]{@link https://github.com/LedgerHQ/ledgerjs/tree/master/packages/hw-transport-u2f} for examples of API calls.
   *
   * @param {function} callback -- asynchronous function accepting a single parameter `transport`
   * @returns {Promise} does the work of setting up a transport connection
   * @example
   * async run() {
   *   return await this.withTransport(async (transport) => {
   *     return transport.doSomething(); // Not a real Ledger transport API call
   *   });
   * }
   */
  async withTransport(callback) {
    const useU2F = this.environment.satisfies({
      firefox: ">70",
    });

    if (useU2F) {
      try {
        const transport = await TransportU2F.create();
        return callback(transport);
      } catch (err) {
        throw new Error(err.message);
      }
    }

    try {
      const transport = await TransportWebUSB.create();
      return callback(transport);
    } catch (e) {
      if (e.message) {
        if (e.message === 'No device selected.') {
          e.message = `Select your device in the WebUSB dialog box. Make sure it's plugged in, unlocked, and has the Bitcoin app open.`;
        }
        if (e.message === 'undefined is not an object (evaluating \'navigator.usb.getDevices\')') {
          e.message = `Safari is not a supported browser.`;
        }
      }
      throw new Error(e.message);
    }
  }

  /**
   * Can be called by a subclass during its `run()` method.
   *
   * Creates a transport layer connection, initializes a bitcoin app
   * object, and passes control to the `callback` function, with the
   * app API as the first argument to the function and the transport
   * API as the second.
   *
   * See the [Ledger API]{@link https://github.com/LedgerHQ/ledgerjs} for general information or the [bitcoin app API]{@link https://github.com/LedgerHQ/ledgerjs/tree/master/packages/hw-app-btc} for examples of API calls.
   *
   * @param {function} callback -- accepts two parameters, `app` and `transport`, which are the Ledger APIs for the bitcoin app and the transport layer, respectively.
   * @returns {Promise} does the work of setting up an app instance (and transport connection)
   * @example
   * async run() {
   *   return await this.withApp(async (app, transport) => {
   *     return app.doSomething(); // Not a real Ledger bitcoin app API call
   *   });
   * }
   */
  withApp(callback) {
    return this.withTransport(async (transport) => {
      const app = new LedgerBtc(transport);
      return callback(app, transport);
    });
  }

  /**
   * Close the Transport to free the interface (E.g. could be used in another tab
   * now that the interaction is over)
   *
   * The way the pubkey/xpub/fingerprints are grabbed makes this a little tricky.
   * Instead of re-writing how that works, let's just add a way to explicitly
   * close the transport.
   * @return {Promise} - promise to close the transport
   */
  closeTransport() {
    return this.withTransport(async (transport) => {
      await transport.close();
    })
  }
}

/**
 * Base class for interactions which must occur when the Ledger device
 * is not in any app but in the dashboard.
 *
 * @extends {module:ledger.LedgerInteraction}
 *
 */
export class LedgerDashboardInteraction extends LedgerInteraction {

  /**
   * Adds `pending` and `active` messages at the `info` level urging
   * the user to be in the Ledger dashboard, not the bitcoin app
   * (`ledger.app.dashboard`).
   *
   * @return {module:interaction.Message[]} messages for this interaction
   */
  messages() {
    const messages = super.messages();
    messages.push({
      state: PENDING,
      level: INFO,
      text: "Make sure you have the main Ledger dashboard open, NOT the Bitcoin app.",
      code: "ledger.app.dashboard",
    });
    messages.push({
      state: ACTIVE,
      level: INFO,
      text: "Make sure you have the main Ledger dashboard open, NOT the Bitcoin app.",
      code: "ledger.app.dashboard",
    });
    return messages;
  }
}

/**
 * Base class for interactions which must occur when the Ledger device
 * is open to the bitcoin app.
 *
 * @extends {module:ledger.LedgerInteraction}
 */
export class LedgerBitcoinInteraction extends LedgerInteraction {

  /**
   * Adds `pending` and `active` messages at the `info` level urging
   * the user to be in the bitcoin app (`ledger.app.bitcoin`).
   *
   * @return {module:interaction.Message[]} messages for this interaction
   */
  messages() {
    const messages = super.messages();
    messages.push({
      state: PENDING,
      level: INFO,
      text: "Then open the Bitcoin app.",
      code: "ledger.app.bitcoin",
    });
    messages.push({
      state: ACTIVE,
      level: INFO,
      text: "Make sure you have opened the Bitcoin app.",
      code: "ledger.app.bitcoin",
    });
    return messages;
  }

}

/**
 * Returns metadata about Ledger device.
 *
 * Includes model name, firmware & MCU versions.
 *
 * @extends {module:ledger.LedgerDashboardInteraction}
 * @example
 * import {LedgerGetMetadata} from "unchained-wallets";
 * const interaction = new LedgerGetMetadata();
 * const result = await interaction.run();
 * console.log(result);
 * {
 *   spec: "Nano S v1.4.2 (MCU v1.7)",
 *   model: "Nano S",
 *   version: {
 *     major: "1",
 *     minor: "4",
 *     patch: "2",
 *     string: "1.4.2",
 *   },
 *   mcuVersion: {
 *     major: "1",
 *     minor: "7",
 *     string: "1.7",
 *   }
 * }
 *
 */
export class LedgerGetMetadata extends LedgerDashboardInteraction {
  // FIXME entire implementation here is rickety AF.

  async run() {

    return this.withTransport(async (transport) => {
      try {
        transport.setScrambleKey('B0L0S');
        const rawResult = await transport.send(0xe0, 0x01, 0x00, 0x00);
        return this.parseMetadata(rawResult);
      } finally {
        await super.closeTransport();
      }
    });
  }

  /**
   * Parses the binary data returned from the Ledger API call into a
   * metadata object.
   *
   * @param {ByteArray} response - binary response data
   * @returns {Object} - device metadata
   */
  parseMetadata(response) {
    try {
      // From
      //
      //   https://github.com/LedgerHQ/ledger-live-common/blob/master/src/hw/getVersion.js
      //   https://github.com/LedgerHQ/ledger-live-common/blob/master/src/hw/getDeviceInfo.js
      //   https://git.xmr.pm/LedgerHQ/ledger-live-common/commit/9ffc75acfc7f1e9aa9101a32b3e7481770fb3b89

      const PROVIDERS = {
        "": 1,
        das: 2,
        club: 3,
        shitcoins: 4,
        ee: 5,
      };
      const ManagerAllowedFlag = 0x08;
      const PinValidatedFlag = 0x80;

      const byteArray = [...response];
      const data = byteArray.slice(0, byteArray.length - 2);
      const targetIdStr = Buffer.from(data.slice(0, 4));
      const targetId = targetIdStr.readUIntBE(0, 4);
      const seVersionLength = data[4];
      let seVersion = Buffer.from(data.slice(5, 5 + seVersionLength)).toString();
      const flagsLength = data[5 + seVersionLength];
      let flags = Buffer.from(
        data.slice(5 + seVersionLength + 1, 5 + seVersionLength + 1 + flagsLength),
      );

      const mcuVersionLength = data[5 + seVersionLength + 1 + flagsLength];
      let mcuVersion = Buffer.from(
        data.slice(
          7 + seVersionLength + flagsLength,
          7 + seVersionLength + flagsLength + mcuVersionLength,
        ),
      );
      if (mcuVersion[mcuVersion.length - 1] === 0) {
        mcuVersion = mcuVersion.slice(0, mcuVersion.length - 1);
      }
      mcuVersion = mcuVersion.toString();

      if (!seVersionLength) {
        seVersion = "0.0.0";
        flags = Buffer.allocUnsafeSlow(0);
        mcuVersion = "";
      }

      /* eslint-disable no-unused-vars, no-bitwise */
      const isOSU = seVersion.includes("-osu");
      const version = seVersion.replace("-osu", "");
      const m = seVersion.match(/([0-9]+.[0-9]+)(.[0-9]+)?(-(.*))?/);
      const [, majMin, , , providerName] = m || [];
      const providerId = PROVIDERS[providerName] || 1;
      const isBootloader = (targetId & 0xf0000000) !== 0x30000000;
      const flag = flags.length > 0 ? flags[0] : 0;
      const managerAllowed = Boolean(flag & ManagerAllowedFlag);
      const pin = Boolean(flag & PinValidatedFlag);
      /* eslint-enable */

      const [majorVersion, minorVersion, patchVersion] = (version || '').split('.');
      const [mcuMajorVersion, mcuMinorVersion] = (mcuVersion || '').split('.');


      // https://gist.github.com/TamtamHero/b7651ffe6f1e485e3886bf4aba673348
      // +-----------------+------------+
      // |    FirmWare     | Target ID  |
      // +-----------------+------------+
      // | Nano S <= 1.3.1 | 0x31100002 |
      // | Nano S 1.4.x    | 0x31100003 |
      // | Nano S 1.5.x    | 0x31100004 |
      // |                 |            |
      // | Blue 2.0.x      | 0x31000002 |
      // | Blue 2.1.x      | 0x31000004 |
      // | Blue 2.1.x V2   | 0x31010004 |
      // |                 |            |
      // | Nano X          | 0x33000004 |
      // |                 |            |
      // | MCU,any version | 0x01000001 |
      // +-----------------+------------+
      //
      //  Order matters -- high to low minTargetId
      const MODEL_RANGES = [
        {
          minTargetId: 0x33000004,
          model: "Nano X",
        },
        {
          minTargetId: 0x31100002,
          model: "Nano S",
        },
        {
          minTargetId: 0x31100002,
          model: "Blue",
        },
        {
          minTargetId: 0x01000001,
          model: "MCU",
        },
      ];
      let model = 'Unknown';
      if (targetId) {
        for (let i = 0; i < MODEL_RANGES.length; i++) {
          const range = MODEL_RANGES[i];
          if (targetId >= range.minTargetId) {
            model = range.model;
            break;
          }
        }
      }

      let spec = `${model} v${version} (MCU v${mcuVersion})`;
      // if (pin) {
      //   spec += " w/PIN";
      // }

      return {
        spec,
        model,
        version: {
          major: majorVersion,
          minor: minorVersion,
          patch: patchVersion,
          string: version,
        },
        mcuVersion: {
          major: mcuMajorVersion,
          minor: mcuMinorVersion,
          string: mcuVersion,
        },
        // pin,
      };

    } catch (e) {
      console.error(e);
      throw new Error("Unable to parse metadata from Ledger device.");
    }
  }

}

/**
 * Base class for interactions exporting information about an HD node
 * at a given BIP32 path.
 *
 * You may want to use `LedgerExportPublicKey` or
 * `LedgerExportExtendedPublicKey` directly.
 *
 * @extends {module:ledger.LedgerBitcoinInteraction}
 * @example
 * import {MAINNET} from "unchained-bitcoin";
 * import {LedgerExportHDNode} from "unchained-wallets";
 * const interaction = new LedgerExportHDNode({network: MAINNET, bip32Path: "m/48'/0'/0'/2'/0"});
 * const node = await interaction.run();
 * console.log(node);
 */
class LedgerExportHDNode extends LedgerBitcoinInteraction {

  /**
   * Requires a valid BIP32 path to the node to export.
   *
   * @param {object} options - options argument
   * @param {string} bip32Path - the BIP32 path for the HD node
   */
  constructor({bip32Path}) {
    super();
    this.bip32Path = bip32Path;
    this.bip32ValidationErrorMessage = {};
    const bip32PathError = validateBIP32Path(bip32Path);
    if (bip32PathError.length) {
      this.bip32ValidationErrorMessage = {
        text: bip32PathError,
        code: 'ledger.bip32_path.path_error',
      };
    }
  }

  /**
   * Adds messages related to the warnings Ledger devices produce on various BIP32 paths.
   *
   * @returns {module:interaction.Message[]} messages for this interaction
   */
  messages() {
    const messages = super.messages();

    if (Object.entries(this.bip32ValidationErrorMessage).length) {
      messages.push({
        state: PENDING,
        level: ERROR,
        code: this.bip32ValidationErrorMessage.code,
        text: this.bip32ValidationErrorMessage.text,
      });
    }
    return messages;
  }

  /**
   * Returns whether or not the Ledger device will display a warning
   * to the user about an unusual BIP32 path.
   *
   * A "usual" BIP32 path is exactly 5 segments long.  The segments
   * have the following constraints:
   *
   * - Segment 1: Must be equal to `44'`
   * - Segment 2: Can have any value
   * - Segment 3: Must be between `0'` and `100'`
   * - Segment 4: Must be equal to `0`
   * - Segment 5: Must be between `0 and 50000`
   *
   * Any other kind of path is considered unusual and will trigger the
   * warning.
   *
   * @returns {boolean} whether a BIP32 path warning will be displayed
   */
  hasBIP32PathWarning() {
    // 0 -> 44'
    // 1 -> anything
    // 2 -> 0' - 100'
    // 3 -> 0
    // 4 -> 0 - 50000
    const indices = bip32PathToSequence(this.bip32Path);
    const hardened0 = hardenedBIP32Index(0);
    const hardened44 = hardenedBIP32Index(44);
    const hardened100 = hardenedBIP32Index(100);
    if (indices.length !== 5) {
      return true;
    }
    if (indices[0] !== hardened44) {
      return true;
    }
    if (indices[2] < hardened0 || indices[2] > hardened100) {
      return true;
    }
    if (indices[3] !== 0) {
      return true;
    }
    return indices[4] < 0 || indices[4] > 50000;

  }

  /**
   * Get fingerprint from parent pubkey. This is useful for generating xpubs
   * which need the fingerprint of the parent pubkey
   *
   * Optionally get root fingerprint for device. This is useful for keychecks and necessary
   * for PSBTs
   *
   * @param {boolean} root fingerprint or not
   * @returns {string} fingerprint
   */
  async getFingerprint(root = false) {
    const pubkey = root ? await this.getMultisigRootPublicKey() : await this.getParentPublicKey();
    let fp = getFingerprintFromPublicKey(pubkey);
    // If asked for a root XFP, zero pad it to length of 8.
    return root ? fingerprintToFixedLengthHex(fp) : fp;
  }

  getParentPublicKey() {
    return this.withApp(async (app) => {
      const parentPath = getParentBIP32Path(this.bip32Path);
      return (await app.getWalletPublicKey(parentPath)).publicKey;
    });
  }

  getMultisigRootPublicKey() {
    return this.withApp(async (app) => {
      return (await app.getWalletPublicKey()).publicKey; // Call getWalletPublicKey w no path to get BIP32_ROOT (m)
    });
  }

  /**
   * See {@link https://github.com/LedgerHQ/ledgerjs/tree/master/packages/hw-app-btc#getwalletpublickey}.
   *
   * @returns {object} the HD node object.
   */
  run() {
    return this.withApp(async (app) => {
      return app.getWalletPublicKey(this.bip32Path);
    });
  }
}

/**
 * Returns the public key at a given BIP32 path.
 *
 * @extends {module:ledger.LedgerExportHDNode}
 * @example
 * import {LedgerExportPublicKey} from "unchained-wallets";
 * const interaction = new LedgerExportPublicKey({bip32Path: "m/48'/0'/0'/2'/0"});
 * const publicKey = await interaction.run();
 * console.log(publicKey);
 * // "03..."
 */
export class LedgerExportPublicKey extends LedgerExportHDNode {

  /**
   * @param {string} bip32Path - the BIP32 path for the HD node
   * @param {boolean} includeXFP - return xpub with root fingerprint concatenated
   */
  constructor({bip32Path, includeXFP = false}) {
    super({bip32Path});
    this.includeXFP = includeXFP;
  }

  /**
   * Parses out and compresses the public key from the response of
   * `LedgerExportHDNode`.
   *
   * @returns {string|Object} (compressed) public key in hex (returns object if asked to include root fingerprint)
   */
  async run() {
    try {
      const result = await super.run();
      const publicKey = this.parsePublicKey((result || {}).publicKey);
      if (this.includeXFP) {
        let rootFingerprint = await this.getFingerprint(true);
        return {
          rootFingerprint,
          publicKey,
        };
      }

      return publicKey;
    } finally {
      await super.closeTransport();
    }
  }

  /**
   * Compress the given public key.
   *
   * @param {string} publicKey - the uncompressed public key in hex
   * @returns {string} - the compressed public key in hex
   *
   */
  parsePublicKey(publicKey) {
    if (publicKey) {
      try {
        return compressPublicKey(publicKey);
      } catch (e) {
        console.error(e);
        throw new Error("Unable to compress public key from Ledger device.");
      }
    } else {
      throw new Error("Received no public key from Ledger device.");
    }
  }
}

/**
 * Class for wallet extended public key (xpub) interaction at a given BIP32 path.
 * @extends {module:ledger.LedgerExportHDNode}
 */
export class LedgerExportExtendedPublicKey extends LedgerExportHDNode {

  /**
   * @param {string} bip32Path path
   * @param {string} network bitcoin network
   * @param {boolean} includeXFP - return xpub with root fingerprint concatenated
   */
  constructor({bip32Path, network, includeXFP}) {
    super({bip32Path});
    this.network = network;
    this.includeXFP = includeXFP;
  }

  messages() {
    return super.messages();
  }

  /**
   * Retrieve extended public key (xpub) from Ledger device for a given BIP32 path
   * @example
   * import {LedgerExportExtendedPublicKey} from "unchained-wallets";
   * const interaction = new LedgerExportExtendedPublicKey({network, bip32Path});
   * const xpub = await interaction.run();
   * console.log(xpub);
   *
   * @returns {string|Object} the extended public key (returns object if asked to include root fingerprint)
   */
  async run() {
    try {
      const walletPublicKey = await super.run();
      const fingerprint = await this.getFingerprint();

      const xpub = deriveExtendedPublicKey(
        this.bip32Path,
        walletPublicKey.publicKey,
        walletPublicKey.chainCode,
        fingerprint,
        this.network,
      );

      if (this.includeXFP) {
        let rootFingerprint = await this.getFingerprint(true);
        return {
          rootFingerprint,
          xpub,
        };
      }
      return xpub;
    } finally {
      await super.closeTransport();
    }
  }
}

/**
 * Returns a signature for a bitcoin transaction with inputs from one
 * or many multisig addresses.
 *
 * - `inputs` is an array of `UTXO` objects from `unchained-bitcoin`
 * - `outputs` is an array of `TransactionOutput` objects from `unchained-bitcoin`
 * - `bip32Paths` is an array of (`string`) BIP32 paths, one for each input, identifying the path on this device to sign that input with
 *
 * @extends {module:ledger.LedgerBitcoinInteraction}
 * @example
 * import {
 *   generateMultisigFromHex, TESTNET, P2SH,
 * } from "unchained-bitcoin";
 * import {LedgerSignMultisigTransaction} from "unchained-wallets";
 * const redeemScript = "5...ae";
 * const inputs = [
 *   {
 *     txid: "8d276c76b3550b145e44d35c5833bae175e0351b4a5c57dc1740387e78f57b11",
 *     index: 1,
 *     multisig: generateMultisigFromHex(TESTNET, P2SH, redeemScript),
 *     amountSats: '1234000'
 *   },
 *   // other inputs...
 * ];
 * const outputs = [
 *   {
 *     amountSats: '1299659',
 *     address: "2NGHod7V2TAAXC1iUdNmc6R8UUd4TVTuBmp"
 *   },
 *   // other outputs...
 * ];
 * const interaction = new LedgerSignMultisigTransaction({
 *   network: TESTNET,
 *   inputs,
 *   outputs,
 *   bip32Paths: ["m/45'/0'/0'/0", // add more, 1 per input],
 * });
 * const signature = await interaction.run();
 * console.log(signatures);
 * // ["ababab...", // 1 per input]
 */
export class LedgerSignMultisigTransaction extends LedgerBitcoinInteraction {

  /**
   * @param {object} options - options argument
   * @param {string} options.network - bitcoin network
   * @param {array<object>} [options.inputs] - inputs for the transaction
   * @param {array<object>} [options.outputs] - outputs for the transaction
   * @param {array<string>} [options.bip32Paths] - BIP32 paths
   * @param {string} [options.psbt] - PSBT string encoded in base64
   * @param {object} [options.keyDetails] - Signing Key Details (Fingerprint + bip32 prefix)
   * @param {boolean} [options.returnSignatureArray] - return an array of signatures instead of a signed PSBT (useful for test suite)
   */
  constructor({network, inputs, outputs, bip32Paths, psbt, keyDetails, returnSignatureArray= false}) {
    super();
    this.network = network;
    if (!psbt || !keyDetails) {
      this.inputs = inputs;
      this.outputs = outputs;
      this.bip32Paths = bip32Paths;
    } else {
      const {
        unchainedInputs,
        unchainedOutputs,
        bip32Derivations
      } = translatePSBT(network, P2SH, psbt, keyDetails);
      this.psbt = psbt;
      this.inputs = unchainedInputs;
      this.outputs = unchainedOutputs;
      this.bip32Paths = bip32Derivations.map((b32d) => b32d.path);
      this.pubkeys = bip32Derivations.map((b32d) => b32d.pubkey);
      this.returnSignatureArray = returnSignatureArray;
    }
  }

  /**
   * Adds messages describing the signing flow.
   *
   * @returns {module:interaction.Message[]} messages for this interaction
   */
  messages() {
    const messages = super.messages();

    messages.push({
      state: ACTIVE,
      level: WARNING,
      code: "ledger.sign.delay",
      text: `Your Ledger device may take up to several minutes to process a transaction with many inputs.`,
      preProcessingTime: this.preProcessingTime(),
      postProcessingTime: this.postProcessingTime(),
    });

    if (this.anySegwitInputs()) {

      messages.push({
        state: ACTIVE,
        level: INFO,
        code: "ledger.sign",
        version: "<1.6.0",
        text: `Your Ledger will ask you to "Confirm transaction" and display each output amount and address followed by the the fee amount.`,
        action: LEDGER_RIGHT_BUTTON,
      });

      messages.push({
        state: ACTIVE,
        level: INFO,
        code: "ledger.sign",
        version: ">=1.6.0",
        text: `Confirm each output on your Ledger device and approve the transaction.`,
        messages: [
          {
            text: `Your Ledger will ask you to "Review transaction".`,
            action: LEDGER_RIGHT_BUTTON,
          },
          {
            text: `For each output, your Ledger device will display the output amount...`,
            action: LEDGER_RIGHT_BUTTON,
          },
          {
            text: `...followed by the output address in several parts`,
            action: LEDGER_RIGHT_BUTTON,
          },
          {
            text: `Your Ledger will display the transaction fees.`,
            action: LEDGER_RIGHT_BUTTON,
          },
          {
            text: `Your Ledger will ask you to "Accept and send".`,
            action: LEDGER_BOTH_BUTTONS,
          },
        ],
      });

    } else {

      messages.push({
        state: ACTIVE,
        level: INFO,
        code: "ledger.sign",
        version: "<1.6.0",
        text: `Confirm each output on your Ledger device and approve the transaction.`,
        messages: [
          {
            text: `For each output, your Ledger will display the output amount and address for you to confirm.`,
            action: LEDGER_RIGHT_BUTTON,
          },
          {
            text: `Your Ledger will ask if you want to "Confirm the transaction".  Due to a bug in the Ledger software, your device may display the transaction fee as "UNKNOWN".`,
            action: LEDGER_RIGHT_BUTTON,
          },
        ],
      });

      messages.push({
        state: ACTIVE,
        level: INFO,
        code: "ledger.sign",
        version: ">=1.6.0",
        text: `Confirm each output on your Ledger device and approve the transaction.`,
        messages: [
          {
            text: `For each output, your Ledger will ask you to "Review output".`,
            action: LEDGER_RIGHT_BUTTON,
          },
          {
            text: `Your Ledger will display the output amount.`,
            action: LEDGER_RIGHT_BUTTON,
          },
          {
            text: `Your Ledger will display the output address in several parts.`,
            action: LEDGER_RIGHT_BUTTON,
          },
          {
            text: `Your Ledger will ask if you want to "Accept" the output.`,
            action: LEDGER_BOTH_BUTTONS,
          },
          {
            text: `Your Ledger will ask if you want to "Confirm the transaction".`,
            action: LEDGER_RIGHT_BUTTON,
          },
          {
            text: `Due to a bug in the Ledger software, your device will display the transaction fee as "UNKNOWN".`,
            action: LEDGER_RIGHT_BUTTON,
          },
          {
            text: `Your Ledger will ask you to "Accept and send".`,
            action: LEDGER_BOTH_BUTTONS,
          },
        ],
      });

    }

    return messages;
  }

  preProcessingTime() {
    // FIXME
    return 10;
  }

  postProcessingTime() {
    // FIXME
    return 10;
  }

  /**
   * See {@link https://github.com/LedgerHQ/ledgerjs/tree/master/packages/hw-app-btc#signp2shtransaction}.
   *
   * Input signatures produced will always have a trailing `...01`
   * {@link https://bitcoin.org/en/glossary/sighash-all SIGHASH_ALL}
   * byte.
   *
   * @returns {string[]|string} array of input signatures, one per input or PSBT in Base64
   */
  run() {
    return this.withApp(async (app, transport) => {
      try {
        // FIXME: Explain the rationale behind this choice.
        transport.setExchangeTimeout(20000 * this.outputs.length);
        const transactionSignature = await app.signP2SHTransaction(
          {
            inputs: this.ledgerInputs(),
            associatedKeysets: this.ledgerKeysets(),
            outputScriptHex: this.ledgerOutputScriptHex(),
            lockTime: 0, // locktime, 0 is no locktime
            sigHashType: 1, // sighash type, 1 is SIGHASH_ALL
            segwit: this.anySegwitInputs(),
            transactionVersion: 1, // tx version
          },
        );

        // If we were passed a PSBT initially, we want to return a PSBT with partial signatures
        // rather than the normal array of signatures.
        if (this.psbt && !this.returnSignatureArray) {
          return addSignaturesToPSBT(this.network, this.psbt, this.pubkeys, this.parseSignature(transactionSignature, "buffer"))
        } else {
          return this.parseSignature(transactionSignature, "hex");
        }
      } finally {
        transport.close();
      }
    });
  }

  ledgerInputs() {
    return this.inputs.map(input => {
      const addressType = multisigAddressType(input.multisig);
      const inputTransaction = splitTransaction(input.transactionHex, true); // FIXME: should the 2nd parameter here always be true?
      const scriptFn = (addressType === P2SH ? multisigRedeemScript : multisigWitnessScript);
      const scriptHex = scriptToHex(scriptFn(input.multisig));
      return [inputTransaction, input.index, scriptHex]; // can add sequence number for RBF as an additional element
    });
  }

  ledgerKeysets() {
    return this.bip32Paths.map((bip32Path) => this.ledgerBIP32Path(bip32Path));
  }

  ledgerOutputScriptHex() {
    const txHex = unsignedMultisigTransaction(this.network, this.inputs, this.outputs).toHex();
    const splitTx = splitTransaction(txHex, this.anySegwitInputs());
    return serializeTransactionOutputs(splitTx).toString('hex');
  }

  ledgerBIP32Path(bip32Path) {
    return bip32Path.split("/").slice(1).join("/");
  }

  anySegwitInputs() {
    for (let i = 0; i < this.inputs.length; i++) {
      const input = this.inputs[i];
      const addressType = multisigAddressType(input.multisig);
      if (addressType === P2SH_P2WSH || addressType === P2WSH) {
        return true;
      }
    }
    return false;
  }

}