@trezor/connect
Version:
High-level javascript interface for Trezor hardware wallet.
265 lines • 11.3 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 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