@trezor/connect
Version:
High-level javascript interface for Trezor hardware wallet.
191 lines • 8.42 kB
JavaScript
;
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 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.DeviceUniquePath)(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;
}
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.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);
}
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));
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;
}
getAllDevices() {
return this.devices;
}
getOnlyDevice() {
return this.devices.length === 1 ? this.devices[0] : undefined;
}
getDeviceByPath(path) {
return this.devices.find(d => d.getUniquePath() === path);
}
getDeviceByStaticState(state) {
const deviceId = state.split('@')[1].split(':')[0];
return this.devices.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