UNPKG

@trezor/connect

Version:

High-level javascript interface for Trezor hardware wallet.

265 lines 11.3 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 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 createDeviceCollection = () => { let devices = []; const isEqual = (a) => (b) => a.transport === b.transport && a.transportPath === b.transportPath; const get = (transportPath, transport) => devices.find(isEqual({ transport, transportPath })); const all = () => devices; const add = (device) => { const index = devices.findIndex(isEqual(device)); if (index >= 0) devices[index] = device; else devices.push(device); }; const remove = (transportPath, transport) => { const index = devices.findIndex(isEqual({ transport, transportPath })); const [removed] = index >= 0 ? devices.splice(index, 1) : [undefined]; return removed; }; const clear = (transport) => { let removed; [removed, devices] = (0, utils_1.arrayPartition)(devices, d => !transport || d.transport === transport); return removed; }; return { get, all, 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 = createDeviceCollection(); 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, _sessionsBackgroundUrl, 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, sessionsBackgroundUrl: _sessionsBackgroundUrl, id: manifest?.appUrl || 'unknown app', }); } onDeviceConnected(descriptor, transport) { const id = (this.deviceCounter++).toString(16).slice(-8); const device = new Device_1.Device({ id: (0, types_1.DeviceUniquePath)(id), transport, descriptor, listener: lifecycle => this.emit(lifecycle, device), }); this.devices.add(device); const penalty = this.authPenaltyManager.get(); this.handshakeLock(async () => { if (this.devices.get(descriptor.path, transport)) { await device.handshake(penalty); } }); } onDeviceDisconnected(descriptor, transport) { const device = this.devices.remove(descriptor.path, transport); device?.disconnect(); } onDeviceSessionChanged(descriptor, transport) { const device = this.devices.get(descriptor.path, transport); device?.updateDescriptor(descriptor); } onDeviceRequestRelease(descriptor, transport) { const device = this.devices.get(descriptor.path, transport); device?.usedElsewhere(); } getOrCreateTransportManager(apiType) { if (!this.transportManagers[apiType]) { const manager = new TransportManager_1.TransportManager({ startTransport: this.startTransport.bind(this), stopTransport: this.stopTransport.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 startTransport(transport, pendingTransportEvent, signal) { try { await this.initializeTransport(transport, pendingTransportEvent, signal); } catch (err) { await this.stopTransport(transport); throw err; } } async initializeTransport(transport, pendingTransportEvent, signal) { transport.on(transport_1.TRANSPORT.DEVICE_CONNECTED, d => this.onDeviceConnected(d, transport)); transport.on(transport_1.TRANSPORT.DEVICE_DISCONNECTED, d => this.onDeviceDisconnected(d, transport)); transport.on(transport_1.TRANSPORT.DEVICE_SESSION_CHANGED, d => this.onDeviceSessionChanged(d, transport)); transport.on(transport_1.TRANSPORT.DEVICE_REQUEST_RELEASE, d => this.onDeviceRequestRelease(d, transport)); const enumerateResult = await transport.enumerate({ signal }); if (!enumerateResult.success) { throw new Error(enumerateResult.error); } const descriptors = enumerateResult.payload; const waitForDevicesPromise = pendingTransportEvent && descriptors.length ? this.waitForDevices(transport, descriptors, signal) : Promise.resolve(); transport.handleDescriptorsChange(descriptors); transport.listen(); await waitForDevicesPromise; } waitForDevices(transport, descriptors, 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); const remaining = descriptors.slice(); const onDeviceEvent = (device) => { const index = remaining.findIndex(d => d.path === device.transportPath && transport === device.transport); if (index >= 0) remaining.splice(index, 1); if (!remaining.length) resolve(); }; this.on(events_1.DEVICE.CONNECT, onDeviceEvent); this.on(events_1.DEVICE.CONNECT_UNACQUIRED, onDeviceEvent); this.on(events_1.DEVICE.DISCONNECT, onDeviceEvent); return promise.finally(() => { transport.off(transport_1.TRANSPORT.ERROR, onError); signal.removeEventListener('abort', onAbort); clearTimeout(autoResolveTransportEventTimeout); this.off(events_1.DEVICE.CONNECT, onDeviceEvent); this.off(events_1.DEVICE.CONNECT_UNACQUIRED, onDeviceEvent); this.off(events_1.DEVICE.DISCONNECT, onDeviceEvent); }); } getDeviceCount() { return this.devices.all().length; } getAllDevices() { return this.devices.all(); } getOnlyDevice() { return this.getDeviceCount() === 1 ? this.devices.all()[0] : undefined; } getDeviceByPath(path) { return this.devices.all().find(d => d.getUniquePath() === path); } getDeviceByStaticState(state) { const deviceId = state.split('@')[1].split(':')[0]; return this.devices.all().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 stopTransport(transport) { const devices = this.devices.clear(transport); devices.forEach(device => { this.emit(events_1.DEVICE.DISCONNECT, device); }); await Promise.all(devices.map(async (device) => { this.authPenaltyManager.remove(device); await device.dispose(); })); transport?.stop(); } async enumerate() { const promises = this.getConnectedTransports().map(async (transport) => { const res = await transport.enumerate(); if (!res.success) { return; } res.payload.forEach(d => { this.devices.get(d.path, transport)?.updateDescriptor(d); }); }); 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