UNPKG

@trezor/connect

Version:

High-level javascript interface for Trezor hardware wallet.

406 lines 15.6 kB
"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