@trezor/connect
Version:
High-level javascript interface for Trezor hardware wallet.
239 lines (238 loc) • 9.11 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 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