UNPKG

@trezor/connect

Version:

High-level javascript interface for Trezor hardware wallet.

239 lines (238 loc) 9.11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DeviceList = exports.assertDeviceListConnected = void 0; const transport_1 = require("@trezor/transport"); const utils_1 = require("@trezor/utils"); const constants_1 = require("../constants"); const events_1 = require("../events"); const Device_1 = require("./Device"); const types_1 = require("../types"); const TransportList_1 = require("./TransportList"); const TransportManager_1 = require("./TransportManager"); const debug_1 = require("../utils/debug"); const trezorPushNotification_1 = require("./workflow/trezorPushNotification"); const createAuthPenaltyManager = priority => { const penalizedDevices = {}; const get = () => 100 * priority + Object.keys(penalizedDevices).reduce((penalty, key) => Math.max(penalty, penalizedDevices[key]), 0); const add = device => { if (!device.isInitialized() || device.isBootloader() || !device.features.device_id) return; const deviceID = device.features.device_id; const penalty = penalizedDevices[deviceID] ? penalizedDevices[deviceID] + 500 : 2000; penalizedDevices[deviceID] = Math.min(penalty, 5000); }; const remove = device => { if (!device.isInitialized() || device.isBootloader() || !device.features.device_id) return; const deviceID = device.features.device_id; delete penalizedDevices[deviceID]; }; const clear = () => Object.keys(penalizedDevices).forEach(key => delete penalizedDevices[key]); return { get, add, remove, clear }; }; const getTransportInfo = transport => ({ apiType: transport.apiType, type: transport.name, version: transport.version, outdated: transport.isOutdated }); const assertDeviceListConnected = deviceList => { if (!deviceList.isConnected()) { throw constants_1.ERRORS.TypedError('Transport_Missing'); } }; exports.assertDeviceListConnected = assertDeviceListConnected; class DeviceList extends utils_1.TypedEmitter { transportManagers = {}; transports = []; devices = []; deviceCounter = Date.now(); handshakeLock; authPenaltyManager; updateTransports; getConnectedTransports() { return Object.values(this.transportManagers).map(manager => manager.get()).filter(utils_1.isNotUndefined); } isConnected() { return !!this.getConnectedTransports().length; } pendingConnection() { const pending = Object.values(this.transportManagers).map(manager => manager.pending()).filter(utils_1.isNotUndefined); if (pending.length) return Promise.all(pending).then(() => {}); } getActiveTransports() { return this.getConnectedTransports().map(getTransportInfo); } constructor({ messages, priority, debug, manifest }) { super(); const transportLogger = (0, debug_1.initLog)('@trezor/transport', debug); this.handshakeLock = (0, utils_1.getSynchronize)(); this.authPenaltyManager = createAuthPenaltyManager(priority); this.updateTransports = (0, TransportList_1.createTransportList)({ messages, logger: transportLogger, id: manifest?.appName || manifest?.appUrl || 'unknown app' }); } async onDeviceConnected(descriptor, transport) { const id = (this.deviceCounter++).toString(16).slice(-8); const device = new Device_1.Device({ id: (0, types_1.asDeviceUniquePath)(id), transport, descriptor }); const penalty = this.authPenaltyManager.get(); const stillConnected = await this.handshakeLock(() => (0, utils_1.resolveAfter)(penalty && penalty + 501).then(() => device.handshake())); if (!stillConnected) { return; } if (descriptor.id && descriptor.apiType === 'bluetooth') { transport.subscribe({ path: device.descriptor.id, channels: ['battery-level', 'trezor-push-notification'] }); } this.devices.push(device); device.lifecycle.on(events_1.DEVICE.CONNECT, () => this.emit(events_1.DEVICE.CONNECT, device)); device.lifecycle.on(events_1.DEVICE.CHANGED, () => this.emit(events_1.DEVICE.CHANGED, device)); device.lifecycle.on(events_1.DEVICE.CONNECT_UNACQUIRED, () => this.emit(events_1.DEVICE.CONNECT_UNACQUIRED, device)); device.lifecycle.on(events_1.DEVICE.TREZOR_PUSH_NOTIFICATION, payload => { this.emit(events_1.DEVICE.TREZOR_PUSH_NOTIFICATION, { device, ...payload }); }); device.lifecycle.on(events_1.DEVICE.DISCONNECT, () => { device.lifecycle.removeAllListeners(); this.authPenaltyManager.remove(device); const index = this.devices.indexOf(device); if (index >= 0) this.devices.splice(index, 1); this.emit(events_1.DEVICE.DISCONNECT, device); }); this.emit(device.isUnacquired() ? events_1.DEVICE.CONNECT_UNACQUIRED : events_1.DEVICE.CONNECT, device); } onPushNotification(event) { const device = this.devices.find(d => d.descriptor.id === event.id); if (device) { (0, trezorPushNotification_1.trezorPushNotificationHandler)({ device, message: event.data }); } } onBatteryLevel(event) { const device = this.devices.find(d => d.descriptor.id === event.id); device?.updateFeature('soc', event.data[0]); } getOrCreateTransportManager(apiType) { if (!this.transportManagers[apiType]) { const manager = new TransportManager_1.TransportManager(this.initializeTransport.bind(this)); manager.on(transport_1.TRANSPORT.START, transport => this.emit(transport_1.TRANSPORT.START, getTransportInfo(transport))); manager.on(transport_1.TRANSPORT.ERROR, error => this.emit(transport_1.TRANSPORT.ERROR, { apiType, error })); this.transportManagers[apiType] = manager; } return this.transportManagers[apiType]; } async init({ transports, transportReconnect, pendingTransportEvent } = {}) { this.transports = this.updateTransports(this.transports, transports); const promises = this.transports.map(t => t.apiType).concat((0, utils_1.typedObjectKeys)(this.transportManagers)).filter(utils_1.arrayDistinct).map(apiType => this.getOrCreateTransportManager(apiType).init({ transports: this.transports.filter(t => t.apiType === apiType), transportReconnect, pendingTransportEvent })); await Promise.all(promises); } async initializeTransport(transport, pendingTransportEvent, signal) { transport.on(transport_1.TRANSPORT.DEVICE_CONNECTED, d => this.onDeviceConnected(d, transport)); transport.on(transport_1.TRANSPORT.TREZOR_PUSH_NOTIFICATION, this.onPushNotification.bind(this)); transport.on(transport_1.TRANSPORT.BATTERY_LEVEL, this.onBatteryLevel.bind(this)); const enumerateResult = await transport.enumerate({ signal }); if (!enumerateResult.success) { throw new Error(enumerateResult.error); } const descriptors = enumerateResult.payload; transport.handleDescriptorsChange(descriptors); transport.listen(); if (pendingTransportEvent && descriptors.length) { await this.waitForDevices(transport, signal); } } waitForDevices(transport, signal) { const { promise, reject, resolve } = (0, utils_1.createDeferred)(); const onAbort = () => reject(signal.reason); signal.addEventListener('abort', onAbort); const onError = error => reject(new Error(error)); transport.once(transport_1.TRANSPORT.ERROR, onError); const autoResolveTransportEventTimeout = setTimeout(resolve, 10000); this.handshakeLock(resolve); return promise.finally(() => { transport.off(transport_1.TRANSPORT.ERROR, onError); signal.removeEventListener('abort', onAbort); clearTimeout(autoResolveTransportEventTimeout); }); } getDeviceCount() { return this.devices.length; } getPrioritizedDevices() { return [...this.devices].sort((a, b) => (a.descriptor.apiType === 'bluetooth' ? 1 : 0) - (b.descriptor.apiType === 'bluetooth' ? 1 : 0)); } getAllDevices() { return this.getPrioritizedDevices(); } getOnlyDevice() { return this.devices.length === 1 ? this.devices[0] : undefined; } getDeviceByPath(path) { return this.getPrioritizedDevices().find(d => d.getUniquePath() === path); } getDeviceByStaticState(state) { const deviceId = state.split('@')[1].split(':')[0]; return this.getPrioritizedDevices().find(d => d.features?.device_id === deviceId); } async dispose() { this.removeAllListeners(); const promises = Object.values(this.transportManagers).map(manager => manager.dispose()); await Promise.all(promises); } async enumerate() { const promises = this.getConnectedTransports().map(async transport => { const res = await transport.enumerate(); if (res.success) { transport.handleDescriptorsChange(res.payload); } }); await Promise.all(promises); } addAuthPenalty(device) { return this.authPenaltyManager.add(device); } removeAuthPenalty(device) { return this.authPenaltyManager.remove(device); } } exports.DeviceList = DeviceList; //# sourceMappingURL=DeviceList.js.map