UNPKG

14.6 kBJavaScriptView Raw
1"use strict";
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 return new (P || (P = Promise))(function (resolve, reject) {
5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 step((generator = generator.apply(thisArg, _arguments || [])).next());
9 });
10};
11var __importDefault = (this && this.__importDefault) || function (mod) {
12 return (mod && mod.__esModule) ? mod : { "default": mod };
13};
14Object.defineProperty(exports, "__esModule", { value: true });
15exports.getAltStatusMessage = exports.StatusCodes = exports.TransportStatusError = exports.TransportError = void 0;
16const events_1 = __importDefault(require("events"));
17const errors_1 = require("@ledgerhq/errors");
18Object.defineProperty(exports, "TransportError", { enumerable: true, get: function () { return errors_1.TransportError; } });
19Object.defineProperty(exports, "StatusCodes", { enumerable: true, get: function () { return errors_1.StatusCodes; } });
20Object.defineProperty(exports, "getAltStatusMessage", { enumerable: true, get: function () { return errors_1.getAltStatusMessage; } });
21Object.defineProperty(exports, "TransportStatusError", { enumerable: true, get: function () { return errors_1.TransportStatusError; } });
22const logs_1 = require("@ledgerhq/logs");
23const DEFAULT_LOG_TYPE = "transport";
24/**
25 * The Transport class defines a generic interface for communicating with a Ledger hardware wallet.
26 * There are different kind of transports based on the technology (channels like U2F, HID, Bluetooth, Webusb) and environment (Node, Web,...).
27 * It is an abstract class that needs to be implemented.
28 */
29class Transport {
30 constructor({ context, logType } = {}) {
31 this.exchangeTimeout = 30000;
32 this.unresponsiveTimeout = 15000;
33 this.deviceModel = null;
34 this._events = new events_1.default();
35 /**
36 * Send data to the device using the higher level API.
37 *
38 * @param {number} cla - The instruction class for the command.
39 * @param {number} ins - The instruction code for the command.
40 * @param {number} p1 - The first parameter for the instruction.
41 * @param {number} p2 - The second parameter for the instruction.
42 * @param {Buffer} data - The data to be sent. Defaults to an empty buffer.
43 * @param {Array<number>} statusList - A list of acceptable status codes for the response. Defaults to [StatusCodes.OK].
44 * @param {Object} options - Contains optional options for the exchange function
45 * - abortTimeoutMs: stop the send after a given timeout. Another timeout exists
46 * to detect unresponsive device (see `unresponsiveTimeout`). This timeout aborts the exchange.
47 * @returns {Promise<Buffer>} A promise that resolves with the response data from the device.
48 */
49 this.send = (cla, ins, p1, p2, data = Buffer.alloc(0), statusList = [errors_1.StatusCodes.OK], { abortTimeoutMs } = {}) => __awaiter(this, void 0, void 0, function* () {
50 const tracer = this.tracer.withUpdatedContext({ function: "send" });
51 if (data.length >= 256) {
52 tracer.trace("data.length exceeded 256 bytes limit", { dataLength: data.length });
53 throw new errors_1.TransportError("data.length exceed 256 bytes limit. Got: " + data.length, "DataLengthTooBig");
54 }
55 tracer.trace("Starting an exchange", { abortTimeoutMs });
56 const response = yield this.exchange(
57 // The size of the data is added in 1 byte just before `data`
58 Buffer.concat([Buffer.from([cla, ins, p1, p2]), Buffer.from([data.length]), data]), { abortTimeoutMs });
59 tracer.trace("Received response from exchange");
60 const sw = response.readUInt16BE(response.length - 2);
61 if (!statusList.some(s => s === sw)) {
62 throw new errors_1.TransportStatusError(sw);
63 }
64 return response;
65 });
66 this._appAPIlock = null;
67 this.tracer = new logs_1.LocalTracer(logType !== null && logType !== void 0 ? logType : DEFAULT_LOG_TYPE, context);
68 }
69 /**
70 * Send data to the device using a low level API.
71 * It's recommended to use the "send" method for a higher level API.
72 * @param {Buffer} apdu - The data to send.
73 * @param {Object} options - Contains optional options for the exchange function
74 * - abortTimeoutMs: stop the exchange after a given timeout. Another timeout exists
75 * to detect unresponsive device (see `unresponsiveTimeout`). This timeout aborts the exchange.
76 * @returns {Promise<Buffer>} A promise that resolves with the response data from the device.
77 */
78 exchange(_apdu, { abortTimeoutMs: _abortTimeoutMs } = {}) {
79 throw new Error("exchange not implemented");
80 }
81 /**
82 * Send apdus in batch to the device using a low level API.
83 * The default implementation is to call exchange for each apdu.
84 * @param {Array<Buffer>} apdus - array of apdus to send.
85 * @param {Observer<Buffer>} observer - an observer that will receive the response of each apdu.
86 * @returns {Subscription} A Subscription object on which you can call ".unsubscribe()" to stop sending apdus.
87 */
88 exchangeBulk(apdus, observer) {
89 let unsubscribed = false;
90 const unsubscribe = () => {
91 unsubscribed = true;
92 };
93 const main = () => __awaiter(this, void 0, void 0, function* () {
94 if (unsubscribed)
95 return;
96 for (const apdu of apdus) {
97 const r = yield this.exchange(apdu);
98 if (unsubscribed)
99 return;
100 const status = r.readUInt16BE(r.length - 2);
101 if (status !== errors_1.StatusCodes.OK) {
102 throw new errors_1.TransportStatusError(status);
103 }
104 observer.next(r);
105 }
106 });
107 main().then(() => !unsubscribed && observer.complete(), e => !unsubscribed && observer.error(e));
108 return { unsubscribe };
109 }
110 /**
111 * Set the "scramble key" for the next data exchanges with the device.
112 * Each app can have a different scramble key and it is set internally during instantiation.
113 * @param {string} key - The scramble key to set.
114 * deprecated This method is no longer needed for modern transports and should be migrated away from.
115 * no @ before deprecated as it breaks documentationjs on version 14.0.2
116 * https://github.com/documentationjs/documentation/issues/1596
117 */
118 setScrambleKey(_key) { }
119 /**
120 * Close the connection with the device.
121 *
122 * Note: for certain transports (hw-transport-node-hid-singleton for ex), once the promise resolved,
123 * the transport instance is actually still cached, and the device is disconnected only after a defined timeout.
124 * But for the consumer of the Transport, this does not matter and it can consider the transport to be closed.
125 *
126 * @returns {Promise<void>} A promise that resolves when the transport is closed.
127 */
128 close() {
129 return Promise.resolve();
130 }
131 /**
132 * Listen for an event on the transport instance.
133 * Transport implementations may have specific events. Common events include:
134 * "disconnect" : triggered when the transport is disconnected.
135 * @param {string} eventName - The name of the event to listen for.
136 * @param {(...args: Array<any>) => any} cb - The callback function to be invoked when the event occurs.
137 */
138 on(eventName, cb) {
139 this._events.on(eventName, cb);
140 }
141 /**
142 * Stop listening to an event on an instance of transport.
143 */
144 off(eventName, cb) {
145 this._events.removeListener(eventName, cb);
146 }
147 emit(event, ...args) {
148 this._events.emit(event, ...args);
149 }
150 /**
151 * Enable or not logs of the binary exchange
152 */
153 setDebugMode() {
154 console.warn("setDebugMode is deprecated. use @ledgerhq/logs instead. No logs are emitted in this anymore.");
155 }
156 /**
157 * Set a timeout (in milliseconds) for the exchange call. Only some transport might implement it. (e.g. U2F)
158 */
159 setExchangeTimeout(exchangeTimeout) {
160 this.exchangeTimeout = exchangeTimeout;
161 }
162 /**
163 * Define the delay before emitting "unresponsive" on an exchange that does not respond
164 */
165 setExchangeUnresponsiveTimeout(unresponsiveTimeout) {
166 this.unresponsiveTimeout = unresponsiveTimeout;
167 }
168 /**
169 * create() allows to open the first descriptor available or
170 * throw if there is none or if timeout is reached.
171 * This is a light helper, alternative to using listen() and open() (that you may need for any more advanced usecase)
172 * @example
173 TransportFoo.create().then(transport => ...)
174 */
175 static create(openTimeout = 3000, listenTimeout) {
176 return new Promise((resolve, reject) => {
177 let found = false;
178 const sub = this.listen({
179 next: e => {
180 found = true;
181 if (sub)
182 sub.unsubscribe();
183 if (listenTimeoutId)
184 clearTimeout(listenTimeoutId);
185 this.open(e.descriptor, openTimeout).then(resolve, reject);
186 },
187 error: e => {
188 if (listenTimeoutId)
189 clearTimeout(listenTimeoutId);
190 reject(e);
191 },
192 complete: () => {
193 if (listenTimeoutId)
194 clearTimeout(listenTimeoutId);
195 if (!found) {
196 reject(new errors_1.TransportError(this.ErrorMessage_NoDeviceFound, "NoDeviceFound"));
197 }
198 },
199 });
200 const listenTimeoutId = listenTimeout
201 ? setTimeout(() => {
202 sub.unsubscribe();
203 reject(new errors_1.TransportError(this.ErrorMessage_ListenTimeout, "ListenTimeout"));
204 }, listenTimeout)
205 : null;
206 });
207 }
208 /**
209 * Wrapper to make an exchange "atomic" (blocking any other exchange)
210 *
211 * It also handles "unresponsiveness" by emitting "unresponsive" and "responsive" events.
212 *
213 * @param f The exchange job, using the transport to run
214 * @returns a Promise resolving with the output of the given job
215 */
216 exchangeAtomicImpl(f) {
217 return __awaiter(this, void 0, void 0, function* () {
218 const tracer = this.tracer.withUpdatedContext({
219 function: "exchangeAtomicImpl",
220 unresponsiveTimeout: this.unresponsiveTimeout,
221 });
222 if (this.exchangeBusyPromise) {
223 tracer.trace("Atomic exchange is already busy");
224 throw new errors_1.TransportPendingOperation("An action was already pending on the Ledger device. Please deny or reconnect.");
225 }
226 // Sets the atomic guard
227 let resolveBusy;
228 const busyPromise = new Promise(r => {
229 resolveBusy = r;
230 });
231 this.exchangeBusyPromise = busyPromise;
232 // The device unresponsiveness handler
233 let unresponsiveReached = false;
234 const timeout = setTimeout(() => {
235 tracer.trace(`Timeout reached, emitting Transport event "unresponsive"`, {
236 unresponsiveTimeout: this.unresponsiveTimeout,
237 });
238 unresponsiveReached = true;
239 this.emit("unresponsive");
240 }, this.unresponsiveTimeout);
241 try {
242 const res = yield f();
243 if (unresponsiveReached) {
244 tracer.trace("Device was unresponsive, emitting responsive");
245 this.emit("responsive");
246 }
247 return res;
248 }
249 finally {
250 tracer.trace("Finalize, clearing busy guard");
251 clearTimeout(timeout);
252 if (resolveBusy)
253 resolveBusy();
254 this.exchangeBusyPromise = null;
255 }
256 });
257 }
258 decorateAppAPIMethods(self, methods, scrambleKey) {
259 for (const methodName of methods) {
260 self[methodName] = this.decorateAppAPIMethod(methodName, self[methodName], self, scrambleKey);
261 }
262 }
263 decorateAppAPIMethod(methodName, f, ctx, scrambleKey) {
264 return (...args) => __awaiter(this, void 0, void 0, function* () {
265 const { _appAPIlock } = this;
266 if (_appAPIlock) {
267 return Promise.reject(new errors_1.TransportError("Ledger Device is busy (lock " + _appAPIlock + ")", "TransportLocked"));
268 }
269 try {
270 this._appAPIlock = methodName;
271 this.setScrambleKey(scrambleKey);
272 return yield f.apply(ctx, args);
273 }
274 finally {
275 this._appAPIlock = null;
276 }
277 });
278 }
279 /**
280 * Sets the context used by the logging/tracing mechanism
281 *
282 * Useful when re-using (cached) the same Transport instance,
283 * but with a new tracing context.
284 *
285 * @param context A TraceContext, that can undefined to reset the context
286 */
287 setTraceContext(context) {
288 this.tracer = this.tracer.withContext(context);
289 }
290 /**
291 * Updates the context used by the logging/tracing mechanism
292 *
293 * The update only overrides the key-value that are already defined in the current context.
294 *
295 * @param contextToAdd A TraceContext that will be added to the current context
296 */
297 updateTraceContext(contextToAdd) {
298 this.tracer.updateContext(contextToAdd);
299 }
300 /**
301 * Gets the tracing context of the transport instance
302 */
303 getTraceContext() {
304 return this.tracer.getContext();
305 }
306}
307Transport.ErrorMessage_ListenTimeout = "No Ledger device found (timeout)";
308Transport.ErrorMessage_NoDeviceFound = "No Ledger device found";
309exports.default = Transport;
310//# sourceMappingURL=Transport.js.map
\No newline at end of file