@trezor/connect
Version:
High-level javascript interface for Trezor hardware wallet.
406 lines • 15.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeviceCommands = void 0;
const tslib_1 = require("tslib");
const protobuf_1 = require("@trezor/protobuf");
const schema_utils_1 = require("@trezor/schema-utils");
const utils_1 = require("@trezor/utils");
const constants_1 = require("../constants");
const resolveDescriptorForTaproot_1 = require("./resolveDescriptorForTaproot");
const coinInfo_1 = require("../data/coinInfo");
const events_1 = require("../events");
const prompts_1 = require("./prompts");
const accountUtils_1 = require("../utils/accountUtils");
const debug_1 = require("../utils/debug");
const hdnodeUtils = tslib_1.__importStar(require("../utils/hdnodeUtils"));
const pathUtils_1 = require("../utils/pathUtils");
const logger = (0, debug_1.initLog)('DeviceCommands');
const assertType = (res, resType) => {
const splitResTypes = Array.isArray(resType) ? resType : resType.split('|');
if (!splitResTypes.includes(res.type)) {
throw constants_1.ERRORS.TypedError('Runtime', `assertType: Response of unexpected type: ${res.type}. Should be ${resType}`);
}
};
const filterForLog = (type, msg) => {
const blacklist = {
PassphraseAck: {
passphrase: '(redacted...)',
},
CipheredKeyValue: {
value: '(redacted...)',
},
GetPublicKey: {
address_n: '(redacted...)',
},
PublicKey: {
node: '(redacted...)',
xpub: '(redacted...)',
},
DecryptedMessage: {
message: '(redacted...)',
address: '(redacted...)',
},
Features: '(redacted...)',
};
if (type in blacklist) {
if (typeof blacklist[type] === 'string') {
return blacklist[type];
}
return { ...msg, ...blacklist[type] };
}
return msg;
};
class DeviceCommands {
device;
transport;
transportSession;
disposed;
callPromise;
constructor(device, transport, transportSession) {
this.device = device;
this.transport = transport;
this.transportSession = transportSession;
this.disposed = false;
}
dispose() {
this.disposed = true;
}
isDisposed() {
return this.disposed;
}
unlockPath(params) {
return this.typedCall('UnlockPath', 'UnlockedPathRequest', params);
}
async getPublicKey(params, unlockPath) {
if (unlockPath) {
await this.unlockPath(unlockPath);
}
const response = await this.typedCall('GetPublicKey', 'PublicKey', {
address_n: params.address_n,
coin_name: params.coin_name || 'Bitcoin',
script_type: params.script_type,
show_display: params.show_display,
ignore_xpub_magic: params.ignore_xpub_magic,
ecdsa_curve_name: params.ecdsa_curve_name,
});
return response.message;
}
async getHDNode(params, options) {
const path = params.address_n;
const { coinInfo, unlockPath } = options;
const validation = typeof options.validation === 'boolean' ? options.validation : true;
let network = null;
if (!params.script_type) {
params.script_type = (0, pathUtils_1.getScriptType)(path);
}
if (params.script_type === 'SPENDP2SHWITNESS') {
network = (0, coinInfo_1.getSegwitNetwork)(coinInfo);
}
else if (params.script_type === 'SPENDWITNESS') {
network = (0, coinInfo_1.getBech32Network)(coinInfo);
}
if (!network) {
network = coinInfo.network;
}
if (!params.coin_name) {
params.coin_name = coinInfo.name;
}
let publicKey;
if (params.show_display || !validation) {
publicKey = await this.getPublicKey(params, unlockPath);
}
else {
const suffix = 0;
const childPath = path.concat([suffix]);
const resKey = await this.getPublicKey(params, unlockPath);
const childKey = await this.getPublicKey({ ...params, address_n: childPath }, unlockPath);
publicKey = hdnodeUtils.xpubDerive(resKey, childKey, suffix, network, coinInfo.network);
}
const response = {
path,
serializedPath: (0, pathUtils_1.getSerializedPath)(path),
childNum: publicKey.node.child_num,
xpub: publicKey.xpub,
chainCode: publicKey.node.chain_code,
publicKey: publicKey.node.public_key,
fingerprint: publicKey.node.fingerprint,
depth: publicKey.node.depth,
};
if (network !== coinInfo.network) {
response.xpubSegwit = response.xpub;
response.xpub = hdnodeUtils.convertXpub(publicKey.xpub, network, coinInfo.network);
}
if ((0, pathUtils_1.isTaprootPath)(path)) {
const { checksum, xpub: xpubSegwit } = (0, resolveDescriptorForTaproot_1.resolveDescriptorForTaproot)({
response,
publicKey,
});
response.xpubSegwit = xpubSegwit;
response.descriptorChecksum = checksum;
}
return response;
}
async getAddress({ address_n, show_display, multisig, script_type, chunkify }, coinInfo) {
if (!script_type) {
script_type = (0, pathUtils_1.getScriptType)(address_n);
if (script_type === 'SPENDMULTISIG' && !multisig) {
script_type = 'SPENDADDRESS';
}
}
if (multisig && multisig.pubkeys) {
multisig.pubkeys.forEach(pk => {
if (typeof pk.node === 'string') {
pk.node = hdnodeUtils.xpubToHDNodeType(pk.node, coinInfo.network);
}
});
}
const response = await this.typedCall('GetAddress', 'Address', {
address_n,
coin_name: coinInfo.name,
show_display,
multisig,
script_type: script_type || 'SPENDADDRESS',
chunkify,
});
return {
path: address_n,
serializedPath: (0, pathUtils_1.getSerializedPath)(address_n),
address: response.message.address,
};
}
async ethereumGetAddress({ address_n, show_display, encoded_network, chunkify, }) {
const response = await this.typedCall('EthereumGetAddress', 'EthereumAddress', {
address_n,
show_display,
encoded_network,
chunkify,
});
return {
path: address_n,
serializedPath: (0, pathUtils_1.getSerializedPath)(address_n),
address: response.message.address,
};
}
async ethereumGetPublicKey({ address_n, show_display, }) {
const suffix = 0;
const childPath = address_n.concat([suffix]);
const resKey = await this.typedCall('EthereumGetPublicKey', 'EthereumPublicKey', {
address_n,
show_display,
});
const childKey = await this.typedCall('EthereumGetPublicKey', 'EthereumPublicKey', {
address_n: childPath,
show_display: false,
});
const publicKey = hdnodeUtils.xpubDerive(resKey.message, childKey.message, suffix);
return {
path: address_n,
serializedPath: (0, pathUtils_1.getSerializedPath)(address_n),
childNum: publicKey.node.child_num,
xpub: publicKey.xpub,
chainCode: publicKey.node.chain_code,
publicKey: publicKey.node.public_key,
fingerprint: publicKey.node.fingerprint,
depth: publicKey.node.depth,
};
}
async preauthorize(throwError) {
try {
await this.typedCall('DoPreauthorized', 'PreauthorizedRequest', {});
return true;
}
catch (error) {
if (throwError)
throw error;
return false;
}
}
getDeviceState() {
return this._getAddress();
}
async call(type, msg = {}) {
logger.debug('Sending', type, filterForLog(type, msg));
this.callPromise = this.transport.call({
session: this.transportSession,
name: type,
data: msg,
protocol: this.device.protocol,
});
const res = await this.callPromise;
this.callPromise = undefined;
if (!res.success) {
logger.warn('Received error', res.error, res.message);
throw new Error(res.error);
}
logger.debug('Received', res.payload.type, filterForLog(res.payload.type, res.payload.message));
return res.payload;
}
async typedCall(type, resType, msg) {
if (this.disposed) {
throw constants_1.ERRORS.TypedError('Runtime', 'typedCall: DeviceCommands already disposed');
}
(0, schema_utils_1.Assert)(protobuf_1.MessagesSchema.MessageType.properties[type], msg ?? {});
const response = await this._commonCall(type, msg);
try {
assertType(response, resType);
}
catch (error) {
const abortController = new AbortController();
const timeout = setTimeout(() => {
abortController.abort();
}, 500);
await this.transport
.receive({
session: this.transportSession,
protocol: this.device.protocol,
signal: abortController.signal,
})
.finally(() => {
clearTimeout(timeout);
});
throw error;
}
return response;
}
async _commonCall(type, msg) {
if (this.disposed) {
throw constants_1.ERRORS.TypedError('Runtime', 'typedCall: DeviceCommands already disposed');
}
const resp = await this.call(type, msg);
return this._filterCommonTypes(resp);
}
_filterCommonTypes(res) {
this.device.clearCancelableAction();
if (res.type === 'Failure') {
const { code } = res.message;
let { message } = res.message;
if (code === 'Failure_FirmwareError' && !message) {
message = 'Firmware installation failed';
}
if (code === 'Failure_ActionCancelled' && !message) {
message = 'Action cancelled by user';
}
return Promise.reject(new constants_1.ERRORS.TrezorError(code || 'Failure_UnknownCode', message || 'Failure_UnknownMessage'));
}
if (res.type === 'Features') {
return Promise.resolve(res);
}
if (res.type === 'ButtonRequest') {
this.device.setCancelableAction(() => this.cancelWithFallback());
if (res.message.code === 'ButtonRequest_PassphraseEntry') {
this.device.emit(events_1.DEVICE.PASSPHRASE_ON_DEVICE);
}
else {
this.device.emit(events_1.DEVICE.BUTTON, this.device, res.message);
}
return this._commonCall('ButtonAck', {});
}
if (res.type === 'PinMatrixRequest') {
return (0, prompts_1.promptPin)(this.device, res.message.type).then(pin => this._commonCall('PinMatrixAck', { pin }).then(response => {
if (!this.device.features.unlocked) {
return this.device.getFeatures().then(() => response);
}
return response;
}), error => Promise.reject(error));
}
if (res.type === 'PassphraseRequest') {
return (0, prompts_1.promptPassphrase)(this.device).then(({ value, passphraseOnDevice }) => !passphraseOnDevice
? this._commonCall('PassphraseAck', { passphrase: value.normalize('NFKD') })
: this._commonCall('PassphraseAck', { on_device: true }), error => Promise.reject(error));
}
if (res.type === 'WordRequest') {
return (0, prompts_1.promptWord)(this.device, res.message.type).then(word => this._commonCall('WordAck', { word }), error => Promise.reject(error));
}
return Promise.resolve(res);
}
async _getAddress() {
const { message } = await this.typedCall('GetAddress', 'Address', {
address_n: [(0, pathUtils_1.toHardened)(44), (0, pathUtils_1.toHardened)(1), (0, pathUtils_1.toHardened)(0), 0, 0],
coin_name: 'Testnet',
script_type: 'SPENDADDRESS',
});
return message.address;
}
async getAccountDescriptor(coinInfo, indexOrPath, derivationType) {
const address_n = Array.isArray(indexOrPath)
? indexOrPath
: (0, accountUtils_1.getAccountAddressN)(coinInfo, indexOrPath);
if (coinInfo.type === 'bitcoin') {
const resp = await this.getHDNode({ address_n }, { coinInfo, validation: false });
return {
descriptor: resp.xpubSegwit || resp.xpub,
legacyXpub: resp.xpub,
address_n,
descriptorChecksum: resp.descriptorChecksum,
};
}
if (coinInfo.type === 'ethereum') {
const resp = await this.ethereumGetAddress({ address_n });
return {
descriptor: resp.address,
address_n,
};
}
if (coinInfo.shortcut === 'ADA' || coinInfo.shortcut === 'tADA') {
if (typeof derivationType === 'undefined')
throw new Error('Derivation type is not specified');
const { message } = await this.typedCall('CardanoGetPublicKey', 'CardanoPublicKey', {
address_n,
derivation_type: derivationType,
});
return {
descriptor: message.xpub,
address_n,
};
}
if (coinInfo.shortcut === 'XRP' || coinInfo.shortcut === 'tXRP') {
const { message } = await this.typedCall('RippleGetAddress', 'RippleAddress', {
address_n,
});
return {
descriptor: message.address,
address_n,
};
}
if (coinInfo.shortcut === 'SOL' || coinInfo.shortcut === 'DSOL') {
const { message } = await this.typedCall('SolanaGetAddress', 'SolanaAddress', {
address_n,
});
return {
descriptor: message.address,
address_n,
};
}
throw constants_1.ERRORS.TypedError('Runtime', 'DeviceCommands.getAccountDescriptor: unsupported coinInfo.type');
}
async cancelWithFallback() {
const { name, version } = this.transport;
if (name === 'BridgeTransport' && !utils_1.versionUtils.isNewer(version, '2.0.28')) {
try {
await (0, utils_1.resolveAfter)(1);
await this.device.acquire();
await (0, prompts_1.cancelPrompt)(this.device, false);
}
catch {
}
}
else {
return (0, prompts_1.cancelPrompt)(this.device, false);
}
}
async cancel() {
if (this.disposed) {
return;
}
this.dispose();
if (this.callPromise) {
try {
await this.callPromise;
}
catch {
}
}
}
}
exports.DeviceCommands = DeviceCommands;
//# sourceMappingURL=DeviceCommands.js.map