/* eslint-disable max-lines*/
/**
* Provides classes for interacting with Trezor hardware wallets.
*
* The base class provided is `TrezorInteraction` which wraps calls to [`TrezorConnect`]{@link https://github.com/trezor/connect}. New interactions should subclass `TrezorInteraction`.
*
* Many Trezor calls require knowing the bitcoin network. This
* library uses the API defined by `unchained-bitcoin` to label
* bitcoin networks, and this is the value expected in several off the
* constructors for classes in this module.
*
* The value of the `network` is mapped internally to
* `this.trezorCoin`. This value is useful to subclasses implementing
* the `params()` method as many TrezorConnect methods require the
* `coin` parameter.
*
* The following API classes are implemented:
*
* * TrezorGetMetadata
* * TrezorExportPublicKey
* * TrezorExportExtendedPublicKey
* * TrezorSignMultisigTransaction
* * TrezorConfirmMultisigAddress
*
* @module trezor
*/
import BigNumber from "bignumber.js";
import {
MAINNET,
bip32PathToSequence,
multisigAddress,
multisigPublicKeys,
multisigRequiredSigners,
multisigAddressType,
P2SH,
P2SH_P2WSH,
P2WSH,
networkData,
validateBIP32Path,
fingerprintToFixedLengthHex,
translatePSBT,
addSignaturesToPSBT,
} from "unchained-bitcoin";
import {ECPair, payments} from "bitcoinjs-lib";
import {
DirectKeystoreInteraction,
PENDING,
ACTIVE,
INFO,
ERROR,
} from "./interaction";
import {MULTISIG_ROOT} from './index';
/**
* Constant defining Trezor interactions.
*
* @type {string}
* @default trezor
*/
export const TREZOR = 'trezor';
const TrezorConnect = require("trezor-connect").default;
const ADDRESS_SCRIPT_TYPES = {
[P2SH]: 'SPENDMULTISIG',
[P2SH_P2WSH]: 'SPENDP2SHWITNESS',
[P2WSH]: 'SPENDWITNESS',
};
/**
* Constant representing the action of pushing the left button on a
* Trezor device.
*
* @type {string}
* @default 'trezor_left_button'
*/
export const TREZOR_LEFT_BUTTON = 'trezor_left_button';
/**
* Constant representing the action of pushing the right button on a
* Trezor device.
*
* @type {string}
* @default 'trezor_right_button'
*/
export const TREZOR_RIGHT_BUTTON = 'trezor_right_button';
/**
* Constant representing the action of pushing both buttons on a
* Trezor device.
*
* @type {string}
* @default 'trezor_both_buttons'
*/
export const TREZOR_BOTH_BUTTONS = 'trezor_both_buttons';
/**
* Constant representing the action of pushing and holding the Confirm
* button on a Trezor model T device.
*
* @type {string}
* @default 'trezor_push_and_hold_button'
*/
export const TREZOR_PUSH_AND_HOLD_BUTTON = 'trezor_push_and_hold_button';
// eslint-disable-next-line no-process-env
const env_variables = { ...process.env}; // Accessing directly does not appear to work, let's make a copy
const ENV_TREZOR_CONNECT_URL = env_variables.TREZOR_CONNECT_URL || env_variables.REACT_APP_TREZOR_CONNECT_URL;
const ENV_TREZOR_BLOCKBOOK_URL = env_variables.TREZOR_BLOCKBOOK_URL || env_variables.REACT_APP_TREZOR_BLOCKBOOK_URL;
const TREZOR_CONNECT_URL = ENV_TREZOR_CONNECT_URL || "https://localhost:8088/";
const TREZOR_BLOCKBOOK_URL = ENV_TREZOR_BLOCKBOOK_URL || "http://localhost:3035";
const TREZOR_DEV = env_variables.TREZOR_DEV || env_variables.REACT_APP_TREZOR_DEV;
try {
if (TREZOR_DEV) TrezorConnect.init({
connectSrc: TREZOR_CONNECT_URL,
lazyLoad: true, // this param prevents iframe injection until a TrezorConnect.method is called
manifest: {
email: "help@unchained-capital.com",
appUrl: "https://github.com/unchained-capital/unchained-wallets"
}
})
else TrezorConnect.manifest({
email: "help@unchained-capital.com",
appUrl: "https://github.com/unchained-capital/unchained-wallets"
});
} catch(e) {
// We hit this if we run this code outside of a browser, for example
// during unit testing.
if (env_variables.NODE_ENV !== 'test') {
console.error("Unable to call TrezorConnect.manifest.");
}
}
/**
* Base class for interactions with Trezor hardware wallets.
*
* Assumes we are using TrezorConnect to talk to the device.
*
* Subclasses *must* implement a method `this.connectParams` which
* returns a 2-element array. The first element of this array should
* be a `TrezorConnect` method to use (e.g. -
* `TrezorConnect.getAddress`). The second element of this array
* should be the parameters to pass to the given `TrezorConnect`
* method.
*
* Errors thrown when calling TrezorConnect are not caught, so users
* of this class (and its subclasses) should use `try...catch` as
* always.
*
* Unsuccessful responses (the request succeeded but the Trezor device
* returned an error message) are intercepted and thrown as errors.
* This allows upstream `try...catch` blocks to intercept errors &
* failures uniformly.
*
* Subclasses *may* implement the `parse(payload)` method which
* accepts the response payload object and returns the relevant data.
*
* Subclasses will also want to implement a `messages()` method to
* manipulate the messages returned to the user for each interaction.
*
* @extends {module:interaction.DirectKeystoreInteraction}
* @example
* import {TrezorInteraction} from "unchained-wallets";
* // Simple subclass
*
* class SimpleTrezorInteraction extends TrezorInteraction {
*
* constructor({network, param}) {
* super({network});
* this.param = param;
* }
*
* connectParams() {
* return [
* TrezorConnect.doSomething, // Not a real TrezorConnect function...
* {
* // Many Trezor methods require the `coin` parameter. The
* // value of `this.trezorCoin` is set appropriately based on the
* // `network` provided in the constructor.
* coin: this.trezorCoin,
*
* // Pass whatever arguments are required
* // by the TrezorConnect function being called.
* param: this.param,
* // ...
* }
* ];
* }
*
* parsePayload(payload) {
* return payload.someValue;
* }
*
* }
* // usage
* import {MAINNET} from "unchained-bitcoin";
* const interaction = new SimpleTrezorInteraction({network: MAINNET, param: "foo"});
* const result = await interaction.run();
* console.log(result); // someValue from payload
*/
export class TrezorInteraction extends DirectKeystoreInteraction {
/**
* Trezor interactions require knowing the bitcoin network they are
* for.
*
* @param {object} options - options argument
* @param {string} options.network - bitcoin network
*/
constructor({network}) {
super();
this.network = network;
this.trezorCoin = trezorCoin(network);
}
/**
* Default messages are added asking the user to plug in their
* Trezor device (`device.connect`) and about the TrezorConnect
* popups (`trezor.connect.generic`).
*
* Subclasses should override this method and add their own messages
* (don't forget to call `super()`).
*
* @returns {module:interaction.Message[]} messages for this interaction
*/
messages() {
const messages = super.messages();
messages.push({
version: "One",
state: PENDING,
level: INFO,
text: "Make sure your Trezor device is plugged in.",
code: "device.connect",
});
messages.push({
version: "T",
state: PENDING,
level: INFO,
text: "Make sure your Trezor device is plugged in and unlocked.",
code: "device.connect",
});
messages.push({
state: ACTIVE,
level: INFO,
text: "Your browser should now open a new window to Trezor Connect. Ensure you have enabled popups for this site.",
code: "trezor.connect.generic",
});
return messages;
}
/**
* Awaits the call of `this.method`, passing in the output of
* `this.params()`.
*
* If the call returns but is unsuccessful (`result.success`) is
* false, will throw the returned error message. If some other
* error is thrown, it will not be caught.
*
* Otherwise it returns the result of passing `result.payload` to
* `this.parsePayload`.
*
* @returns {Promise} handles the work of calling TrezorConnect
*/
async run() {
const [method, params] = this.connectParams();
if (TREZOR_DEV && method === TrezorConnect.signTransaction) {
await TrezorConnect.blockchainSetCustomBackend({
coin: "Regtest",
blockchainLink: {
type: 'blockbook',
url: [TREZOR_BLOCKBOOK_URL],
},
})
}
const result = await method(params);
if (!result.success) {
throw new Error(result.payload.error);
}
return this.parsePayload(result.payload);
}
/**
* Override this method in a subclass to return a 2-element array.
*
* The first element should be a functin to call, typically a
* `TrezorConnect` method, e.g. `TrezorConnect.getAddress`.
*
* The second element should be the parameters to pass to this
* function.
*
* By default, the function passed just throws an error.
*
* @returns {Array<function,Object>} the TrezorConnect parameters
*/
connectParams() {
return [
() => { throw new Error("Override the `connectParams` method on a subclass of TrezorInteraction."); },
{},
];
}
/**
* Override this method in a subclass to parse the payload of a
* successful response from the device.
*
* By default, the entire payload is returned.
*
* @param {Object} payload - the raw payload from the device response
* @returns {Object} - relevant or formatted data built from the raw payload
*/
parsePayload(payload) {
return payload;
}
}
/**
* Returns metadata about Trezor device.
*
* Includes model name, device label, firmware version, &
* PIN/passphrase enablement.
*
* @extends {module:trezor.TrezorInteraction}
* @example
* import {TrezorGetMetadata} from "unchained-wallets";
* const interaction = new TrezorGetMetadata();
* const result = await interaction.run();
* console.log(result);
* {
* spec: "Model 1 v1.8.3 w/PIN",
* model: "Model 1",
* version: {
* major: 1,
* minor: 8,
* patch: 3,
* string: "1.8.3",
* },
* label: "My Trezor",
* pin: true,
* passphrase: false,
* }
*/
export class TrezorGetMetadata extends TrezorInteraction {
/**
* This class doesn't actually require a `network`.
*
* @constructor
*/
constructor() {
super({});
}
/**
* It is underdocumented, but TrezorConnect does support the
* `getFeatures` API call.
*
* See {@link https://github.com/trezor/connect/blob/v8/src/js/core/methods/GetFeatures.js}.
*
* @returns {Array<function, Object>} TrezorConnect parameters
*/
connectParams() {
return [
TrezorConnect.getFeatures,
{},
];
}
/**
* Parses Trezor device featuress into an appropriate metadata
* shape.
*
* @param {Object} payload - the original payload from the device response
* @returns {Object} device metadata & features
*/
parsePayload(payload) {
// Example result:
//
// {
// bootloader_hash: "5112...846e9"
// bootloader_mode: null
// device_id: "BDF9...F198"
// firmware_present: null
// flags: 0
// fw_major: null
// fw_minor: null
// fw_patch: null
// fw_vendor: null
// fw_vendor_keys: null
// imported: false
// initialized: true
// label: "My Trezor"
// language: null
// major_version: 1
// minor_version: 6
// model: "1"
// needs_backup: false
// no_backup: null
// passphrase_cached: false
// passphrase_protection: false
// patch_version: 3
// pin_cached: true
// pin_protection: true
// revision: "ef8...862d7"
// unfinished_backup: null
// vendor: "bitcointrezor.com"
// }
const {
major_version, minor_version, patch_version,
label,
model,
pin_protection, passphrase_protection,
} = payload;
let spec = `Model ${model} v.${major_version}.${minor_version}.${patch_version}`;
if (pin_protection) {
spec += " w/PIN";
}
if (passphrase_protection) {
spec += " w/PASS";
}
return {
spec,
model: `Model ${model}`,
version: {
major: major_version,
minor: minor_version,
patch: patch_version,
string: `${major_version}.${minor_version}.${patch_version}`,
},
label,
pin: pin_protection,
passphrase: passphrase_protection,
};
}
}
/**
* Base class for interactions exporting information about an HD node
* at a given BIP32 path.
*
* You may want to use `TrezorExportPublicKey` or
* `TrezorExportExtendedPublicKey` directly.
*
* @extends {module:trezor.TrezorInteraction}
* @example
* import {MAINNET} from "unchained-bitcoin";
* import {TrezorExportHDNode} from "unchained-wallets";
* const interaction = new TrezorExportHDNode({network: MAINNET, bip32Path: "m/48'/0'/0'/2'/0"});
* const node = await interaction.run();
* console.log(node); // {publicKey: "", xpub: "", ...}
*
*/
export class TrezorExportHDNode extends TrezorInteraction {
/**
* Requires a BIP32 path to the node to export as well as which network.
*
* @param {object} options - options argument
* @param {string} options.network - bitcoin network
* @param {string} bip32Path - the BIP32 path for the HD node
* @param {boolean} includeXFP - return xpub with root fingerprint concatenated
*/
constructor({network, bip32Path, includeXFP}) {
super({network});
this.bip32Path = bip32Path;
this.includeXFP = includeXFP;
this.bip32ValidationErrorMessage = {};
const bip32PathError = validateBIP32Path(bip32Path);
if (bip32PathError.length) {
this.bip32ValidationErrorMessage = {
text: bip32PathError,
code: 'trezor.bip32_path.path_error',
};
}
}
/**
* Adds messages related to warnings Trezor devices make depending
* on the BIP32 path passed.
*
* @returns {module:interaction.Message[]} messages for this interaction
*/
messages() {
const messages = super.messages();
const bip32PathSegments = (this.bip32Path || '').split('/');
if (bip32PathSegments.length < 4) { // m, 45', 0', 0', ...
messages.push({
state: PENDING,
level: ERROR,
text: "BIP32 path must be at least depth 3.",
code: "trezor.bip32_path.minimum",
});
}
if (Object.entries(this.bip32ValidationErrorMessage).length) {
messages.push({
state: PENDING,
level: ERROR,
code: this.bip32ValidationErrorMessage.code,
text: this.bip32ValidationErrorMessage.text,
});
}
messages.push({
state: ACTIVE,
level: INFO,
text: "Confirm in the Trezor Connect window that you want to 'Export public key'. You may be prompted to enter your PIN.",
code: "trezor.connect.export_hdnode",
});
return messages;
}
extractDetailsFromPayload({payload, pubkey}) {
if (payload.length !== 2) {
throw new Error("Payload does not have two responses.");
}
let keyMaterial = '';
let rootFingerprint = null;
for (let i = 0; i < payload.length; i++) {
// Find the payload with bip32 = MULTISIG_ROOT to get xfp
if (payload[i].serializedPath === MULTISIG_ROOT) {
let fp = payload[i].fingerprint;
rootFingerprint = fingerprintToFixedLengthHex(fp);
} else {
keyMaterial = pubkey ? payload[i].publicKey : payload[i].xpub;
}
}
return {
rootFingerprint,
keyMaterial,
};
}
/**
* See {@link https://github.com/trezor/connect/blob/v8/docs/methods/getPublicKey.md}.
*
* @returns {Array<function,Object>} TrezorConnect parameters
*/
connectParams() {
if (this.includeXFP) {
return [
TrezorConnect.getPublicKey,
{
bundle: [
{path: this.bip32Path},
{path: MULTISIG_ROOT},
],
coin: this.trezorCoin,
crossChain: true,
},
];
}
return [
TrezorConnect.getPublicKey,
{
path: this.bip32Path,
coin: this.trezorCoin,
crossChain: true,
},
];
}
}
/**
* Returns the public key at a given BIP32 path.
*
* @extends {module:trezor.TrezorExportHDNode}
* @example
* import {MAINNET} from "unchained-bitcoin";
* import {TrezorExportPublicKey} from "unchained-wallets";
* const interaction = new TrezorExportPublicKey({network: MAINNET, bip32Path: "m/48'/0'/0'/2'/0"});
* const publicKey = await interaction.run();
* console.log(publicKey);
* // "03..."
*/
export class TrezorExportPublicKey extends TrezorExportHDNode {
constructor({network, bip32Path, includeXFP}) {
super({
network,
bip32Path,
includeXFP,
});
this.includeXFP = includeXFP;
}
/**
* Parses the public key from the HD node response.
*
* @param {object} payload - the original payload from the device response
* @returns {string|Object} the (compressed) public key in hex or Object if root fingerprint requested
*/
parsePayload(payload) {
if (this.includeXFP) {
const {rootFingerprint, keyMaterial} = this.extractDetailsFromPayload({
payload,
pubkey: true,
});
return {
rootFingerprint,
publicKey: keyMaterial,
};
}
return payload.publicKey;
}
}
/**
* Returns the extended public key at a given BIP32 path.
*
* @extends {module:trezor.TrezorExportHDNode}
* @example
* import {MAINNET} from "unchained-bitcoin";
* import {TrezorExportExtendedPublicKey} from "unchained-wallets";
* const interaction = new TrezorExportExtendedPublicKey({network: MAINNET, bip32Path: "m/48'/0'/0'"});
* const xpub = await interaction.run();
* console.log(xpub);
* // "xpub..."
*/
export class TrezorExportExtendedPublicKey extends TrezorExportHDNode {
constructor({network, bip32Path, includeXFP}) {
super({
network,
bip32Path,
includeXFP,
});
this.includeXFP = includeXFP;
}
/**
* Parses the extended public key from the HD node response.
*
* If asking for XFP, return object with xpub and the root fingerprint.
*
* @param {object} payload the original payload from the device response
* @returns {string|Object} the extended public key (returns object if asked to include root fingerprint)
*/
parsePayload(payload) {
if (this.includeXFP) {
const {rootFingerprint, keyMaterial} = this.extractDetailsFromPayload({
payload,
pubkey: false,
});
return {
rootFingerprint,
xpub: keyMaterial,
};
}
return payload.xpub;
}
}
/**
* 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
*
* @example
* import {
* generateMultisigFromHex, TESTNET, P2SH,
* } from "unchained-bitcoin";
* import {TrezorSignMultisigTransaction} 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 TrezorSignMultisigTransaction({
* 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]
* @extends {module:trezor.TrezorInteraction}
*/
export class TrezorSignMultisigTransaction extends TrezorInteraction {
/**
* @param {object} options - options argument
* @param {string} options.network - bitcoin network
* @param {UTXO[]} options.inputs - inputs for the transaction
* @param {TransactionOutput[]} options.outputs - outputs for the transaction
* @param {string[]} options.bip32Paths - BIP32 paths on this device to sign with, one per each input
* @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,
}) {
super({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: INFO,
text: `Confirm in the Trezor Connect window that you want to 'Sign ${this.network} transaction'. You may be prompted to enter your PIN.`,
code: "trezor.connect.sign",
});
messages.push({
state: ACTIVE,
level: INFO,
version: "One",
text: "Confirm each output on your Trezor device and approve the transaction.",
messages: [
{
text: "For each output, your Trezor device will display the output amount and address.",
action: TREZOR_RIGHT_BUTTON,
},
{
text: "Your Trezor device will display the total output amounts and fee amount.",
action: TREZOR_RIGHT_BUTTON,
},
],
code: "trezor.sign",
});
messages.push({
state: ACTIVE,
level: INFO,
version: "T",
text: "Confirm each output on your Trezor device and approve the transaction.",
messages: [
{
text: `For each input, your Trezor device will display a "Confirm path" dialogue displaying the input BIP32 path. It is safe to continue`,
action: TREZOR_RIGHT_BUTTON,
},
{
text: `For each output, your Trezor device will display a "Confirm sending" dialogue displaying the output amount and address.`,
action: TREZOR_RIGHT_BUTTON,
},
{
text: `Your Trezor device will display the "Confirm transaction" dialogue displaying the total output amount and fee amount.`,
action: TREZOR_PUSH_AND_HOLD_BUTTON,
},
],
code: "trezor.sign",
});
return messages;
}
/**
* See {@link https://github.com/trezor/connect/blob/v8/docs/methods/signTransaction.md}.
*
* @returns {Array<function, Object>} TrezorConnect parameters
*/
connectParams() {
return [
TrezorConnect.signTransaction,
{
inputs: this.inputs.map((input, inputIndex) => trezorInput(input, this.bip32Paths[inputIndex])),
outputs: this.outputs.map((output) => trezorOutput(output)),
coin: this.trezorCoin,
},
];
}
/**
* Parses the signature(s) out of the response payload.
*
* Ensures each input's signature hasa a trailing `...01` {@link https://bitcoin.org/en/glossary/sighash-all SIGHASH_ALL} byte.
*
* @param {Object} payload - the original payload from the device response
* @returns {string[]|string} array of input signatures, one per input or signed psbt with signatures inserted
*/
parsePayload(payload) {
// 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(payload.signatures, "buffer"))
} else {
return this.parseSignature(payload.signatures, "hex")
}
}
}
/**
* Shows a multisig address on the device and prompts the user to
* confirm it.
* If the optional publicKey parameter is used, the public key at
* the given BIP32 path is checked, returning an error if they don't match.
*
* Without the publicKey parameter, this function simply checks that the
* public key at the given BIP32 path is in the redeemscript (with
* validation on-device.
*
* @extends {module:trezor.TrezorInteraction}
* @example
* import {
* generateMultisigFromPublicKeys, MAINNET, P2SH,
* } from "unchained-bitcoin";
* import {TrezorConfirmMultisigAddress} from "unchained-wallets";
* const multisig = generateMultisigFromPublicKeys(MAINNET, P2SH, 2, "03a...", "03b...");
* const interaction = new TrezorConfirmMultisigAddress({network: MAINNET, bip32Path: "m/45'/0'/0'/0/0", multisig});
* await interaction.run();
*/
export class TrezorConfirmMultisigAddress extends TrezorInteraction {
/**
* Most of the information required to confirm a multisig address
* lives in the `Multisig` object from `unchained-bitcoin`.
*
* @param {object} options - options argument
* @param {string} options.network - bitcoin network
* @param {string} options.bip32Path - BIP32 path to the public key on this device used in the multisig address
* @param {Multisig} options.multisig - multisig object
* @param {string} options.publicKey - optional public key to confirm
*/
constructor({network, bip32Path, multisig, publicKey}) {
super({network});
this.bip32Path = bip32Path;
this.multisig = multisig;
this.publicKey = publicKey;
}
/**
* Adds messages about BIP32 path warnings.
*
* @returns {module:interaction.Message[]} messages for this interaction
*
*/
messages() {
const messages = super.messages();
if (this.publicKey) {
messages.push({
state: ACTIVE,
level: INFO,
text:`Confirm in the Trezor Connect window that you want to ‘Export multiple ${this.trezorCoin} addresses’. You may be prompted to enter your PIN. You may also receive a warning about your selected BIP32 path.`,
code: "trezor.connect.confirm_address",
});
} else {
messages.push({
state: ACTIVE,
level: INFO,
text: `Confirm in the Trezor Connect window that you want to 'Export ${this.trezorCoin} address'. You may be prompted to enter your PIN.`,
code: "trezor.connect.confirm_address",
});
}
messages.push({
state: ACTIVE,
level: INFO,
version: "One",
text: "It is safe to continue and confirm the address on your Trezor device.",
messages: [
// FIXME this only shows up on P2SH?
{
text: `Your Trezor device may display a warning "Wrong address path for selected coin". It is safe to continue`,
action: TREZOR_RIGHT_BUTTON,
},
{
text: `Your Trezor device will display the multisig address and BIP32 path.`,
action: TREZOR_RIGHT_BUTTON,
},
],
code: "trezor.confirm_address",
});
messages.push({
state: ACTIVE,
level: INFO,
version: "T",
text: "Confirm the addresss on your Trezor device.",
messages: [
{
text: `For each signer in your quorum, your Trezor device will display a "Confirm path" dialogue displaying the signer's BIP32 path. It is safe to continue`,
action: TREZOR_RIGHT_BUTTON,
},
{
text: `Your Trezor device will display the multisig address.`,
action: TREZOR_RIGHT_BUTTON,
},
],
code: "trezor.confirm_address",
});
return messages;
}
/**
* See {@link https://github.com/trezor/connect/blob/v8/docs/methods/getAddress.md}.
*
* @returns {Array<function, Object>} TrezorConnect parameters
*/
connectParams() {
if (this.publicKey) {
return [
TrezorConnect.getAddress,
{
bundle: [
{
path: this.bip32Path,
showOnTrezor: false,
coin: this.trezorCoin,
crossChain: true,
},
{
path: this.bip32Path,
address: multisigAddress(this.multisig),
showOnTrezor: true,
coin: this.trezorCoin,
crossChain: true,
multisig: {
m: multisigRequiredSigners(this.multisig),
pubkeys: multisigPublicKeys(this.multisig).map((publicKey) => trezorPublicKey(publicKey)),
},
scriptType: ADDRESS_SCRIPT_TYPES[multisigAddressType(this.multisig)],
},
],
}
];
} else {
return [
TrezorConnect.getAddress,
{
path: this.bip32Path,
address: multisigAddress(this.multisig),
showOnTrezor: true,
coin: this.trezorCoin,
crossChain: true,
multisig: {
m: multisigRequiredSigners(this.multisig),
pubkeys: multisigPublicKeys(this.multisig).map((publicKey) => trezorPublicKey(publicKey)),
},
scriptType: ADDRESS_SCRIPT_TYPES[multisigAddressType(this.multisig)],
},
];
}
}
parsePayload(payload) {
if (!this.publicKey) {
return payload;
}
const keyPair = ECPair.fromPublicKey(
Buffer.from(this.publicKey, 'hex'));
const { address } = payments.p2pkh({
pubkey: keyPair.publicKey,
network: networkData(this.network),
});
if (address !== payload[0].address && address !== payload[1].address) {
throw new Error("Wrong public key specified");
}
return payload;
}
}
/**
* Returns the Trezor API version of the given network.
*
* @param {string} network - bitcoin network
* @returns {string} Trezor API spelling for this network
*/
export function trezorCoin(network) {
const testnet_network = TREZOR_DEV ? "Regtest" : "Testnet";
return (network === MAINNET ? "Bitcoin" : testnet_network);
}
function trezorInput(input, bip32Path) {
const requiredSigners = multisigRequiredSigners(input.multisig);
const publicKeys = multisigPublicKeys(input.multisig);
const addressType = multisigAddressType(input.multisig);
const spendType = ADDRESS_SCRIPT_TYPES[addressType];
return {
script_type: spendType,
multisig: {
m: requiredSigners,
pubkeys: publicKeys.map((publicKey) => trezorPublicKey(publicKey)),
signatures: Array(publicKeys.length).fill(''),
},
prev_hash: input.txid,
prev_index: input.index,
address_n: bip32PathToSequence(bip32Path),
...(input.amountSats && {amount: BigNumber(input.amountSats).toString()}),
};
}
function trezorPublicKey(publicKey) {
return {
address_n: [],
node: {
// FIXME are all these 0's OK?
depth: 0,
child_num: 0,
fingerprint: 0,
chain_code: '0'.repeat(64),
public_key: publicKey,
},
};
}
function trezorOutput(output) {
return {
amount: BigNumber(output.amountSats).toFixed(0),
address: output.address,
script_type: 'PAYTOADDRESS',
};
}