@onekeyfe/hwk-ledger-adapter 1.1.26-alpha.8 → 1.1.26-patch.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +277 -182
- package/dist/index.d.ts +277 -182
- package/dist/index.js +1575 -589
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1576 -591
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -3,9 +3,11 @@ import {
|
|
|
3
3
|
CHAIN_FINGERPRINT_PATHS,
|
|
4
4
|
DEVICE,
|
|
5
5
|
DeviceJobQueue,
|
|
6
|
+
EConnectorInteraction,
|
|
6
7
|
HardwareErrorCode as HardwareErrorCode2,
|
|
7
8
|
TypedEventEmitter,
|
|
8
9
|
UI_REQUEST,
|
|
10
|
+
UI_REQUEST_PREEMPTED_TAG,
|
|
9
11
|
UiRequestRegistry,
|
|
10
12
|
deriveDeviceFingerprint,
|
|
11
13
|
failure,
|
|
@@ -14,24 +16,32 @@ import {
|
|
|
14
16
|
|
|
15
17
|
// src/errors.ts
|
|
16
18
|
import { HardwareErrorCode, enrichErrorMessage } from "@onekeyfe/hwk-adapter-core";
|
|
17
|
-
|
|
19
|
+
var MULTIPLE_USB_LEDGER_DEVICES_ERROR_MESSAGE = "Multiple Ledger USB devices are connected. Please connect only one Ledger device and try again.";
|
|
20
|
+
function createMultipleUsbLedgerDevicesError() {
|
|
21
|
+
return Object.assign(new Error(MULTIPLE_USB_LEDGER_DEVICES_ERROR_MESSAGE), {
|
|
22
|
+
code: HardwareErrorCode.DeviceOneDeviceOnly
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function ledgerFailure(code, error, appName, tag, params) {
|
|
18
26
|
const payload = { error, code };
|
|
19
27
|
if (appName !== void 0) payload.appName = appName;
|
|
28
|
+
if (tag !== void 0) payload._tag = tag;
|
|
29
|
+
if (params !== void 0) payload.params = params;
|
|
20
30
|
return { success: false, payload };
|
|
21
31
|
}
|
|
22
32
|
var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
|
|
23
33
|
var USER_REJECTED_CODES = /* @__PURE__ */ new Set(["6985", "27013"]);
|
|
24
34
|
var WRONG_APP_CODES = /* @__PURE__ */ new Set(["6e00", "28160", "6d00", "27904", "6a83", "27267"]);
|
|
25
35
|
var APP_NOT_INSTALLED_CODES = /* @__PURE__ */ new Set(["6807", "26631"]);
|
|
26
|
-
var
|
|
36
|
+
var STEP_BLIND_SIGN_TRANSACTION_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
|
|
27
37
|
function getEthAppErrorCode(err) {
|
|
28
38
|
if (!err || typeof err !== "object") return null;
|
|
29
39
|
const e = err;
|
|
30
|
-
if (e._tag ===
|
|
40
|
+
if (e._tag === ERROR_TAG.EthAppCommand && typeof e.errorCode === "string") {
|
|
31
41
|
return e.errorCode.toLowerCase();
|
|
32
42
|
}
|
|
33
43
|
const orig = e.originalError;
|
|
34
|
-
if (orig?._tag ===
|
|
44
|
+
if (orig?._tag === ERROR_TAG.EthAppCommand && typeof orig.errorCode === "string") {
|
|
35
45
|
return orig.errorCode.toLowerCase();
|
|
36
46
|
}
|
|
37
47
|
return null;
|
|
@@ -88,13 +98,8 @@ function mapBtcAppError(hex) {
|
|
|
88
98
|
return null;
|
|
89
99
|
}
|
|
90
100
|
}
|
|
91
|
-
function
|
|
101
|
+
function mapEthAppErrorCode(ethCode) {
|
|
92
102
|
switch (ethCode) {
|
|
93
|
-
case "6a80":
|
|
94
|
-
if (lastStep === STEP_BLIND_SIGN_FALLBACK) {
|
|
95
|
-
return HardwareErrorCode.EvmBlindSigningRequired;
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
103
|
case "6984":
|
|
99
104
|
return HardwareErrorCode.EvmClearSignPluginMissing;
|
|
100
105
|
case "6a84":
|
|
@@ -107,16 +112,133 @@ function mapEthAppError(ethCode, lastStep) {
|
|
|
107
112
|
return null;
|
|
108
113
|
}
|
|
109
114
|
}
|
|
115
|
+
function classifyEthAppError(err) {
|
|
116
|
+
const ethCode = getEthAppErrorCode(err);
|
|
117
|
+
if (!ethCode) return null;
|
|
118
|
+
if (ethCode === "6a80") {
|
|
119
|
+
return hasBlindSignFallbackStep(err) ? HardwareErrorCode.EvmBlindSigningRequired : null;
|
|
120
|
+
}
|
|
121
|
+
return mapEthAppErrorCode(ethCode);
|
|
122
|
+
}
|
|
123
|
+
var ERROR_TAG = {
|
|
124
|
+
// SDK-mint
|
|
125
|
+
DeviceNotAdvertising: "DeviceNotAdvertisingError",
|
|
126
|
+
// BLE scan miss
|
|
127
|
+
DeviceNotInDiscoveryCache: "DeviceNotInDiscoveryCacheError",
|
|
128
|
+
// dm.connect() before enumerate
|
|
129
|
+
BlePairingTimeout: "BlePairingTimeoutError",
|
|
130
|
+
// SMP 30s timeout
|
|
131
|
+
BleGattBondingFailed: "BleGattBondingFailedError",
|
|
132
|
+
// other GATT failure
|
|
133
|
+
UserAborted: "UserAborted",
|
|
134
|
+
DeviceAppStuck: "DeviceAppStuck",
|
|
135
|
+
// chain app wedged (APDU 0x6901)
|
|
136
|
+
DeviceTransportStuck: "DeviceTransportStuck",
|
|
137
|
+
// DMK transport queue wedged
|
|
138
|
+
// DMK-reuse (DMK throws same string; we synthesize too)
|
|
139
|
+
DeviceLocked: "DeviceLockedError",
|
|
140
|
+
DeviceNotRecognized: "DeviceNotRecognizedError",
|
|
141
|
+
DeviceSessionNotFound: "DeviceSessionNotFound",
|
|
142
|
+
OpenAppCommand: "OpenAppCommandError",
|
|
143
|
+
// DMK-only (read only)
|
|
144
|
+
EthAppCommand: "EthAppCommandError",
|
|
145
|
+
UserRefusedOnDevice: "UserRefusedOnDevice",
|
|
146
|
+
WrongAppOpened: "WrongAppOpenedError",
|
|
147
|
+
InvalidStatusWord: "InvalidStatusWordError",
|
|
148
|
+
AlreadySendingApdu: "AlreadySendingApduError",
|
|
149
|
+
UnknownDeviceExchange: "UnknownDeviceExchangeError",
|
|
150
|
+
NoAccessibleDevice: "NoAccessibleDeviceError",
|
|
151
|
+
UnknownDevice: "UnknownDeviceError",
|
|
152
|
+
DeviceSessionRefresher: "DeviceSessionRefresherError",
|
|
153
|
+
DeviceNotInitialized: "DeviceNotInitializedError",
|
|
154
|
+
OpeningConnection: "OpeningConnectionError",
|
|
155
|
+
DeviceDisconnectedBeforeSendingApdu: "DeviceDisconnectedBeforeSendingApdu",
|
|
156
|
+
DeviceDisconnectedWhileSending: "DeviceDisconnectedWhileSendingError",
|
|
157
|
+
Disconnect: "DisconnectError",
|
|
158
|
+
ReconnectionFailed: "ReconnectionFailedError",
|
|
159
|
+
WebHIDDisconnect: "WebHIDDisconnectError",
|
|
160
|
+
// ble-plx surfaces this when GATT notification setup fails — typical
|
|
161
|
+
// outcome when the user doesn't confirm pairing on the device, or the
|
|
162
|
+
// existing bond is invalid. Observed in production after ~30s.
|
|
163
|
+
PairingRefused: "PairingRefusedError"
|
|
164
|
+
};
|
|
110
165
|
function isDeviceLockedError(err) {
|
|
111
166
|
if (!err || typeof err !== "object") return false;
|
|
112
167
|
const e = err;
|
|
113
168
|
if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
|
|
114
169
|
if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
|
|
115
|
-
if (e._tag ===
|
|
170
|
+
if (e._tag === ERROR_TAG.DeviceLocked) return true;
|
|
116
171
|
if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
|
|
117
172
|
if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
|
|
118
173
|
return false;
|
|
119
174
|
}
|
|
175
|
+
function isDeviceNotAdvertisingError(err) {
|
|
176
|
+
if (!err || typeof err !== "object") return false;
|
|
177
|
+
return err._tag === ERROR_TAG.DeviceNotAdvertising;
|
|
178
|
+
}
|
|
179
|
+
var PAIRING_FAILURE_TAGS = /* @__PURE__ */ new Set([
|
|
180
|
+
ERROR_TAG.BlePairingTimeout,
|
|
181
|
+
ERROR_TAG.BleGattBondingFailed,
|
|
182
|
+
ERROR_TAG.PairingRefused
|
|
183
|
+
]);
|
|
184
|
+
function isBlePairingFailureError(err) {
|
|
185
|
+
if (!err || typeof err !== "object") return false;
|
|
186
|
+
const tag = err._tag;
|
|
187
|
+
if (tag && PAIRING_FAILURE_TAGS.has(tag)) return true;
|
|
188
|
+
const orig = err.originalError;
|
|
189
|
+
if (orig != null && isBlePairingFailureError(orig)) return true;
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
|
|
193
|
+
ERROR_TAG.DeviceLocked,
|
|
194
|
+
ERROR_TAG.DeviceNotAdvertising,
|
|
195
|
+
ERROR_TAG.BlePairingTimeout,
|
|
196
|
+
ERROR_TAG.BleGattBondingFailed,
|
|
197
|
+
ERROR_TAG.PairingRefused,
|
|
198
|
+
ERROR_TAG.DeviceNotRecognized,
|
|
199
|
+
ERROR_TAG.NoAccessibleDevice,
|
|
200
|
+
ERROR_TAG.UnknownDevice,
|
|
201
|
+
ERROR_TAG.DeviceSessionNotFound,
|
|
202
|
+
ERROR_TAG.DeviceSessionRefresher,
|
|
203
|
+
ERROR_TAG.DeviceNotInitialized,
|
|
204
|
+
ERROR_TAG.OpeningConnection,
|
|
205
|
+
ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
|
|
206
|
+
ERROR_TAG.DeviceDisconnectedWhileSending,
|
|
207
|
+
ERROR_TAG.Disconnect,
|
|
208
|
+
ERROR_TAG.ReconnectionFailed,
|
|
209
|
+
ERROR_TAG.WebHIDDisconnect
|
|
210
|
+
]);
|
|
211
|
+
var DEVICE_NOT_FOUND_TAGS = /* @__PURE__ */ new Set([
|
|
212
|
+
ERROR_TAG.NoAccessibleDevice,
|
|
213
|
+
ERROR_TAG.UnknownDevice,
|
|
214
|
+
ERROR_TAG.DeviceNotInitialized,
|
|
215
|
+
// SDK-internal: dm.connect() called before _discovered was populated.
|
|
216
|
+
// Map to DeviceNotFound so non-BLE-direct paths get a sensible error code.
|
|
217
|
+
ERROR_TAG.DeviceNotInDiscoveryCache
|
|
218
|
+
]);
|
|
219
|
+
var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OpeningConnection]);
|
|
220
|
+
var DEVICE_DISCONNECTED_TAGS = /* @__PURE__ */ new Set([
|
|
221
|
+
ERROR_TAG.DeviceNotRecognized,
|
|
222
|
+
ERROR_TAG.DeviceSessionNotFound,
|
|
223
|
+
ERROR_TAG.DeviceSessionRefresher,
|
|
224
|
+
ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
|
|
225
|
+
ERROR_TAG.DeviceDisconnectedWhileSending,
|
|
226
|
+
ERROR_TAG.Disconnect,
|
|
227
|
+
ERROR_TAG.ReconnectionFailed,
|
|
228
|
+
ERROR_TAG.WebHIDDisconnect
|
|
229
|
+
]);
|
|
230
|
+
function isConnectionLevelError(err) {
|
|
231
|
+
if (!err || typeof err !== "object") return false;
|
|
232
|
+
const tag = err._tag;
|
|
233
|
+
if (tag && CONNECTION_LEVEL_TAGS.has(tag)) return true;
|
|
234
|
+
const e = err;
|
|
235
|
+
if (e.originalError != null && isConnectionLevelError(e.originalError)) return true;
|
|
236
|
+
if (e.error != null && e._tag && isConnectionLevelError(e.error)) return true;
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
function isKnownConnectionTag(tag) {
|
|
240
|
+
return typeof tag === "string" && CONNECTION_LEVEL_TAGS.has(tag);
|
|
241
|
+
}
|
|
120
242
|
function hasStatusCode(err, codeSet) {
|
|
121
243
|
if (!err || typeof err !== "object") return false;
|
|
122
244
|
const e = err;
|
|
@@ -126,10 +248,39 @@ function hasStatusCode(err, codeSet) {
|
|
|
126
248
|
if (e.error != null && e._tag && hasStatusCode(e.error, codeSet)) return true;
|
|
127
249
|
return false;
|
|
128
250
|
}
|
|
251
|
+
function hasBlindSignFallbackStep(err) {
|
|
252
|
+
if (!err || typeof err !== "object") return false;
|
|
253
|
+
const e = err;
|
|
254
|
+
if (e._lastStep === STEP_BLIND_SIGN_TRANSACTION_FALLBACK) return true;
|
|
255
|
+
if (Array.isArray(e._deviceActionSteps) && e._deviceActionSteps.includes(STEP_BLIND_SIGN_TRANSACTION_FALLBACK)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (e.originalError != null && hasBlindSignFallbackStep(e.originalError)) return true;
|
|
259
|
+
if (e.error != null && e._tag && hasBlindSignFallbackStep(e.error)) return true;
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
function isDeviceNotFoundError(err) {
|
|
263
|
+
if (!err || typeof err !== "object") return false;
|
|
264
|
+
const tag = err._tag;
|
|
265
|
+
if (tag && DEVICE_NOT_FOUND_TAGS.has(tag)) return true;
|
|
266
|
+
const e = err;
|
|
267
|
+
if (e.originalError != null && isDeviceNotFoundError(e.originalError)) return true;
|
|
268
|
+
if (e.error != null && e._tag && isDeviceNotFoundError(e.error)) return true;
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
function isDeviceBusyError(err) {
|
|
272
|
+
if (!err || typeof err !== "object") return false;
|
|
273
|
+
const tag = err._tag;
|
|
274
|
+
if (tag && DEVICE_BUSY_TAGS.has(tag)) return true;
|
|
275
|
+
const e = err;
|
|
276
|
+
if (e.originalError != null && isDeviceBusyError(e.originalError)) return true;
|
|
277
|
+
if (e.error != null && e._tag && isDeviceBusyError(e.error)) return true;
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
129
280
|
function isUserRejectedError(err) {
|
|
130
281
|
if (!err || typeof err !== "object") return false;
|
|
131
282
|
const e = err;
|
|
132
|
-
if (e._tag ===
|
|
283
|
+
if (e._tag === ERROR_TAG.UserRefusedOnDevice) return true;
|
|
133
284
|
if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
|
|
134
285
|
if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
|
|
135
286
|
return false;
|
|
@@ -137,7 +288,7 @@ function isUserRejectedError(err) {
|
|
|
137
288
|
function isWrongAppError(err) {
|
|
138
289
|
if (!err || typeof err !== "object") return false;
|
|
139
290
|
const e = err;
|
|
140
|
-
if (e._tag ===
|
|
291
|
+
if (e._tag === ERROR_TAG.WrongAppOpened || e._tag === ERROR_TAG.InvalidStatusWord) {
|
|
141
292
|
if (hasStatusCode(err, WRONG_APP_CODES)) return true;
|
|
142
293
|
}
|
|
143
294
|
if (typeof e.message === "string") {
|
|
@@ -151,7 +302,6 @@ function isWrongAppError(err) {
|
|
|
151
302
|
function isAppNotInstalledError(err) {
|
|
152
303
|
if (!err || typeof err !== "object") return false;
|
|
153
304
|
const e = err;
|
|
154
|
-
if (e._tag === "OpenAppCommandError") return true;
|
|
155
305
|
if (typeof e.message === "string" && /unknown application/i.test(e.message)) return true;
|
|
156
306
|
if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
|
|
157
307
|
return false;
|
|
@@ -159,7 +309,8 @@ function isAppNotInstalledError(err) {
|
|
|
159
309
|
function isDeviceDisconnectedError(err) {
|
|
160
310
|
if (!err || typeof err !== "object") return false;
|
|
161
311
|
const e = err;
|
|
162
|
-
|
|
312
|
+
const tag = e._tag;
|
|
313
|
+
if (typeof tag === "string" && DEVICE_DISCONNECTED_TAGS.has(tag)) return true;
|
|
163
314
|
if (typeof e.message === "string") {
|
|
164
315
|
const msg = e.message.toLowerCase();
|
|
165
316
|
if (msg.includes("disconnected") || msg.includes("not found") || msg.includes("no device") || msg.includes("unplugged"))
|
|
@@ -179,7 +330,22 @@ function isTimeoutError(err) {
|
|
|
179
330
|
if (e.code === HardwareErrorCode.OperationTimeout) return true;
|
|
180
331
|
return false;
|
|
181
332
|
}
|
|
182
|
-
function
|
|
333
|
+
function isAppStuckByApdu(err) {
|
|
334
|
+
if (!err || typeof err !== "object") return false;
|
|
335
|
+
const tag = err._tag;
|
|
336
|
+
if (tag === ERROR_TAG.DeviceAppStuck) return true;
|
|
337
|
+
return extractApduHex(err) === "6901";
|
|
338
|
+
}
|
|
339
|
+
function isTransportStuck(err) {
|
|
340
|
+
if (!err || typeof err !== "object") return false;
|
|
341
|
+
const tag = err._tag;
|
|
342
|
+
return tag === ERROR_TAG.DeviceTransportStuck || // already wrapped form
|
|
343
|
+
tag === ERROR_TAG.UnknownDeviceExchange || tag === ERROR_TAG.AlreadySendingApdu;
|
|
344
|
+
}
|
|
345
|
+
function isStuckAppStateError(err) {
|
|
346
|
+
return isAppStuckByApdu(err) || isTransportStuck(err);
|
|
347
|
+
}
|
|
348
|
+
function mapLedgerError(err, opts) {
|
|
183
349
|
let originalMessage = "Unknown Ledger error";
|
|
184
350
|
if (err instanceof Error) {
|
|
185
351
|
originalMessage = err.message;
|
|
@@ -190,60 +356,127 @@ function mapLedgerError(err) {
|
|
|
190
356
|
let code;
|
|
191
357
|
if (isDeviceLockedError(err)) {
|
|
192
358
|
code = HardwareErrorCode.DeviceLocked;
|
|
359
|
+
} else if (isDeviceNotAdvertisingError(err) || isDeviceNotFoundError(err)) {
|
|
360
|
+
code = HardwareErrorCode.DeviceNotFound;
|
|
361
|
+
} else if (isDeviceBusyError(err)) {
|
|
362
|
+
code = HardwareErrorCode.DeviceBusy;
|
|
363
|
+
} else if (isBlePairingFailureError(err)) {
|
|
364
|
+
code = HardwareErrorCode.BlePairingTimeout;
|
|
193
365
|
} else if (isUserRejectedError(err)) {
|
|
194
366
|
code = HardwareErrorCode.UserRejected;
|
|
195
367
|
} else if (isWrongAppError(err)) {
|
|
196
368
|
code = HardwareErrorCode.WrongApp;
|
|
197
369
|
} else if (isAppNotInstalledError(err)) {
|
|
198
|
-
code = HardwareErrorCode.
|
|
370
|
+
code = HardwareErrorCode.AppNotInstalled;
|
|
199
371
|
} else if (isDeviceDisconnectedError(err)) {
|
|
200
372
|
code = HardwareErrorCode.DeviceDisconnected;
|
|
201
373
|
} else if (isTimeoutError(err)) {
|
|
202
374
|
code = HardwareErrorCode.OperationTimeout;
|
|
203
375
|
} else {
|
|
204
|
-
const
|
|
205
|
-
const lastStep = err && typeof err === "object" ? err._lastStep : void 0;
|
|
206
|
-
const ethMapped = ethCode ? mapEthAppError(ethCode, lastStep) : null;
|
|
376
|
+
const ethMapped = classifyEthAppError(err);
|
|
207
377
|
const apduHex = ethMapped ? null : extractApduHex(err);
|
|
208
378
|
const chainMapped = apduHex ? mapSolanaAppError(apduHex) ?? mapTronAppError(apduHex) ?? mapBtcAppError(apduHex) : null;
|
|
209
379
|
code = ethMapped ?? chainMapped ?? HardwareErrorCode.UnknownError;
|
|
210
380
|
}
|
|
211
|
-
const
|
|
381
|
+
const errAppName = err && typeof err === "object" ? err.appName : void 0;
|
|
382
|
+
const appName = errAppName ?? opts?.defaultAppName;
|
|
212
383
|
return { code, message: enrichErrorMessage(code, originalMessage), appName };
|
|
213
384
|
}
|
|
214
385
|
|
|
215
|
-
// src/utils/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
386
|
+
// src/utils/ledgerDmkTransport.ts
|
|
387
|
+
function isLedgerDmkBleTransport(transport) {
|
|
388
|
+
const normalized = transport?.toUpperCase();
|
|
389
|
+
return normalized === "BLE" || normalized?.endsWith("_BLE") === true;
|
|
219
390
|
}
|
|
220
|
-
function
|
|
221
|
-
return
|
|
391
|
+
function isLedgerBleConnectionType(connectionType) {
|
|
392
|
+
return connectionType === "ble";
|
|
222
393
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
394
|
+
function isLedgerBleDescriptor(connectionType, descriptor) {
|
|
395
|
+
return isLedgerBleConnectionType(connectionType) || isLedgerDmkBleTransport(descriptor.transport);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/utils/sdkEventBus.ts
|
|
399
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
400
|
+
function onSdkEvent(listener) {
|
|
401
|
+
listeners.add(listener);
|
|
402
|
+
return () => {
|
|
403
|
+
listeners.delete(listener);
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function offSdkEvent(listener) {
|
|
407
|
+
listeners.delete(listener);
|
|
408
|
+
}
|
|
409
|
+
function emitSdkEvent(event) {
|
|
410
|
+
if (listeners.size === 0) return;
|
|
411
|
+
for (const listener of listeners) {
|
|
412
|
+
try {
|
|
413
|
+
listener(event);
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
226
416
|
}
|
|
227
417
|
}
|
|
228
|
-
function
|
|
229
|
-
if (
|
|
230
|
-
|
|
418
|
+
function emitLog(level, ...args) {
|
|
419
|
+
if (listeners.size === 0) return;
|
|
420
|
+
const message = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
|
|
421
|
+
emitSdkEvent({ type: "log", level, message });
|
|
422
|
+
}
|
|
423
|
+
function safeStringify(value) {
|
|
424
|
+
if (value instanceof Error) {
|
|
425
|
+
return value.stack ? `${value.message}
|
|
426
|
+
${value.stack}` : value.message;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
return JSON.stringify(value);
|
|
430
|
+
} catch {
|
|
431
|
+
return String(value);
|
|
231
432
|
}
|
|
232
433
|
}
|
|
233
434
|
|
|
435
|
+
// src/utils/debugLog.ts
|
|
436
|
+
function debugLog(...args) {
|
|
437
|
+
emitLog("debug", ...args);
|
|
438
|
+
}
|
|
439
|
+
function debugError(...args) {
|
|
440
|
+
emitLog("error", ...args);
|
|
441
|
+
}
|
|
442
|
+
|
|
234
443
|
// src/adapter/LedgerAdapter.ts
|
|
235
444
|
function formatDeviceMismatchError(expected, actual) {
|
|
236
445
|
return `Wrong device: expected ${expected}, got ${actual}`;
|
|
237
446
|
}
|
|
447
|
+
var BTC_HIGH_INDEX_THRESHOLD = 100;
|
|
448
|
+
function btcAccountIndexFromPath(path) {
|
|
449
|
+
const segments = path.replace(/^m\//, "").split("/");
|
|
450
|
+
if (segments.length < 3) return null;
|
|
451
|
+
const accountSeg = segments[2].replace(/['h]$/i, "");
|
|
452
|
+
const accountIndex = parseInt(accountSeg, 10);
|
|
453
|
+
return Number.isFinite(accountIndex) ? accountIndex : null;
|
|
454
|
+
}
|
|
238
455
|
var _LedgerAdapter = class _LedgerAdapter {
|
|
239
|
-
constructor(connector
|
|
456
|
+
constructor(connector) {
|
|
240
457
|
this.vendor = "ledger";
|
|
241
458
|
this.emitter = new TypedEventEmitter();
|
|
242
|
-
// Device cache: tracks discovered devices from connector events
|
|
243
459
|
this._discoveredDevices = /* @__PURE__ */ new Map();
|
|
244
|
-
// Session tracking: maps connectId -> sessionId
|
|
245
460
|
this._sessions = /* @__PURE__ */ new Map();
|
|
246
461
|
this._uiRegistry = new UiRequestRegistry();
|
|
462
|
+
// BTC App rejects account index >= 100 unless display=true. Cached per
|
|
463
|
+
// adapter instance: first 100+ path asks the user once via UI request,
|
|
464
|
+
// subsequent 100+ paths in the same session auto-promote silently.
|
|
465
|
+
// The Ledger device itself still requires a per-call confirmation — that's
|
|
466
|
+
// the Ledger app's safety boundary, not ours to bypass.
|
|
467
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
468
|
+
// Shared across concurrent callers — only `cancel()` aborts.
|
|
469
|
+
this._doConnectAbortController = null;
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// Private helpers
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
/**
|
|
474
|
+
* Ensure at least one device is connected and return a valid connectId.
|
|
475
|
+
*
|
|
476
|
+
* - If a session already exists for the given connectId, reuse it.
|
|
477
|
+
* - If ANY session exists (Ledger IDs are ephemeral), reuse it.
|
|
478
|
+
* - Otherwise: search → exactly 1 USB device auto-connects; multiple or none throws.
|
|
479
|
+
*/
|
|
247
480
|
// Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
|
|
248
481
|
this._connectingPromise = null;
|
|
249
482
|
// ---------------------------------------------------------------------------
|
|
@@ -266,47 +499,30 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
266
499
|
payload: { connectId: data.connectId }
|
|
267
500
|
});
|
|
268
501
|
};
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this.
|
|
273
|
-
this.
|
|
502
|
+
// Forward low-level connector 'ui-event' (the four EConnectorInteraction values)
|
|
503
|
+
// to the public hw.emitter so consumers only need to subscribe in one place
|
|
504
|
+
// (hw.on instead of also reaching into connector.on).
|
|
505
|
+
this.uiEventForwarder = (event) => {
|
|
506
|
+
this.emitter.emit("ui-event", event);
|
|
274
507
|
};
|
|
275
508
|
this.connector = connector;
|
|
276
|
-
this.
|
|
277
|
-
this._jobQueue = new DeviceJobQueue({
|
|
278
|
-
emit: (event, data) => this.emitter.emit(event, data),
|
|
279
|
-
uiRegistry: this._uiRegistry
|
|
280
|
-
});
|
|
509
|
+
this._jobQueue = new DeviceJobQueue();
|
|
281
510
|
this.registerEventListeners();
|
|
282
511
|
}
|
|
283
|
-
/**
|
|
284
|
-
* Classify a method's interruptibility.
|
|
285
|
-
* - Signing / typed data / transaction → 'confirm' (user may decide via preemption UI)
|
|
286
|
-
* - Read-only queries (getAddress / getPublicKey / getMasterFingerprint) → 'safe'
|
|
287
|
-
* (auto-cancels any pending read for the same device)
|
|
288
|
-
*/
|
|
289
|
-
static _getInterruptibility(method) {
|
|
290
|
-
if (method.toLowerCase().includes("sign")) return "confirm";
|
|
291
|
-
return "safe";
|
|
292
|
-
}
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
512
|
// Transport
|
|
295
|
-
// ---------------------------------------------------------------------------
|
|
296
|
-
// Transport is decided at connector creation time. These methods
|
|
297
|
-
// satisfy the IHardwareWallet interface with sensible defaults.
|
|
298
513
|
get activeTransport() {
|
|
299
|
-
return this.connector.connectionType
|
|
514
|
+
return isLedgerBleConnectionType(this.connector.connectionType) ? "ble" : "hid";
|
|
300
515
|
}
|
|
301
516
|
getAvailableTransports() {
|
|
302
517
|
return this.activeTransport ? [this.activeTransport] : [];
|
|
303
518
|
}
|
|
304
|
-
|
|
519
|
+
// Connector is bound at construction; switching requires a new adapter.
|
|
520
|
+
switchTransport(_type) {
|
|
521
|
+
return Promise.resolve();
|
|
305
522
|
}
|
|
306
|
-
// ---------------------------------------------------------------------------
|
|
307
523
|
// Lifecycle
|
|
308
|
-
|
|
309
|
-
|
|
524
|
+
init(_config) {
|
|
525
|
+
return Promise.resolve();
|
|
310
526
|
}
|
|
311
527
|
/**
|
|
312
528
|
* Clear cached device/session state without tearing down the adapter.
|
|
@@ -319,6 +535,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
319
535
|
this._connectingPromise = null;
|
|
320
536
|
this._uiRegistry.reset();
|
|
321
537
|
this._jobQueue.clear();
|
|
538
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
322
539
|
}
|
|
323
540
|
async dispose() {
|
|
324
541
|
this._uiRegistry.reset();
|
|
@@ -327,6 +544,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
327
544
|
this.connector.reset();
|
|
328
545
|
this._discoveredDevices.clear();
|
|
329
546
|
this._sessions.clear();
|
|
547
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
330
548
|
this.emitter.removeAllListeners();
|
|
331
549
|
}
|
|
332
550
|
uiResponse(response) {
|
|
@@ -335,10 +553,17 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
335
553
|
// ---------------------------------------------------------------------------
|
|
336
554
|
// Device management
|
|
337
555
|
// ---------------------------------------------------------------------------
|
|
338
|
-
async searchDevices() {
|
|
556
|
+
async searchDevices(options) {
|
|
557
|
+
if (options?.resetSession) {
|
|
558
|
+
this._doConnectAbortController?.abort();
|
|
559
|
+
this._sessions.clear();
|
|
560
|
+
this._connectingPromise = null;
|
|
561
|
+
this._doConnectAbortController = null;
|
|
562
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
563
|
+
}
|
|
339
564
|
await this._ensureDevicePermission();
|
|
340
565
|
const devices = await this.connector.searchDevices();
|
|
341
|
-
|
|
566
|
+
this._discoveredDevices.clear();
|
|
342
567
|
for (const d of devices) {
|
|
343
568
|
if (d.connectId) {
|
|
344
569
|
this._discoveredDevices.set(d.connectId, this.connectorDeviceToDeviceInfo(d));
|
|
@@ -347,11 +572,41 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
347
572
|
if (this._discoveredDevices.size === 0) {
|
|
348
573
|
await this._ensureDevicePermission();
|
|
349
574
|
}
|
|
575
|
+
debugLog(
|
|
576
|
+
`[LedgerAdapter] searchDevices() return count=${this._discoveredDevices.size} ids=[${[
|
|
577
|
+
...this._discoveredDevices.keys()
|
|
578
|
+
].join(",")}]`
|
|
579
|
+
);
|
|
350
580
|
return Array.from(this._discoveredDevices.values());
|
|
351
581
|
}
|
|
582
|
+
// USB single-session invariant: evict all sessions, best-effort (see connectDevice).
|
|
583
|
+
async _evictAllSessions() {
|
|
584
|
+
if (this._sessions.size === 0) return;
|
|
585
|
+
const stale = [...this._sessions.values()];
|
|
586
|
+
this._sessions.clear();
|
|
587
|
+
for (const sid of stale) {
|
|
588
|
+
try {
|
|
589
|
+
await this.connector.disconnect(sid);
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
static _createDeviceBusyError(method) {
|
|
595
|
+
return Object.assign(new Error(`Ledger device is busy while calling ${method}`), {
|
|
596
|
+
code: HardwareErrorCode2.DeviceBusy
|
|
597
|
+
});
|
|
598
|
+
}
|
|
352
599
|
async connectDevice(connectId) {
|
|
353
|
-
await this._ensureDevicePermission(connectId);
|
|
354
600
|
try {
|
|
601
|
+
if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
|
|
602
|
+
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
603
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
if (!isLedgerBleConnectionType(this.connector.connectionType)) {
|
|
607
|
+
await this._evictAllSessions();
|
|
608
|
+
}
|
|
609
|
+
await this._ensureDevicePermission(connectId);
|
|
355
610
|
const session = await this.connector.connect(connectId);
|
|
356
611
|
this._sessions.set(connectId, session.sessionId);
|
|
357
612
|
if (session.deviceInfo) {
|
|
@@ -387,7 +642,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
387
642
|
// Chain call helper
|
|
388
643
|
// ---------------------------------------------------------------------------
|
|
389
644
|
async callChain(connectId, deviceId, chain, method, params, skipFingerprint = false) {
|
|
390
|
-
await this._ensureDevicePermission(connectId, deviceId);
|
|
391
645
|
try {
|
|
392
646
|
const result = await this.connectorCall(connectId, method, params, {
|
|
393
647
|
chain,
|
|
@@ -399,45 +653,12 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
399
653
|
return this.errorToFailure(err);
|
|
400
654
|
}
|
|
401
655
|
}
|
|
402
|
-
/**
|
|
403
|
-
* Batch version of callChain — checks permission once,
|
|
404
|
-
* fingerprint is verified on the first call inside connectorCall.
|
|
405
|
-
*/
|
|
406
|
-
async callChainBatch(connectId, deviceId, chain, method, params, onProgress, skipFingerprint = false) {
|
|
407
|
-
await this._ensureDevicePermission(connectId, deviceId);
|
|
408
|
-
const results = [];
|
|
409
|
-
for (let i = 0; i < params.length; i++) {
|
|
410
|
-
try {
|
|
411
|
-
const result = await this.connectorCall(connectId, method, params[i], {
|
|
412
|
-
chain,
|
|
413
|
-
deviceId,
|
|
414
|
-
// Only verify fingerprint on the first call in the batch
|
|
415
|
-
skipFingerprint: skipFingerprint || i > 0
|
|
416
|
-
});
|
|
417
|
-
results.push(result);
|
|
418
|
-
onProgress?.({ index: i, total: params.length });
|
|
419
|
-
} catch (err) {
|
|
420
|
-
return this.errorToFailure(err);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return success(results);
|
|
424
|
-
}
|
|
425
656
|
// ---------------------------------------------------------------------------
|
|
426
657
|
// EVM chain methods
|
|
427
658
|
// ---------------------------------------------------------------------------
|
|
428
659
|
evmGetAddress(connectId, deviceId, params) {
|
|
429
660
|
return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
|
|
430
661
|
}
|
|
431
|
-
evmGetAddresses(connectId, deviceId, params, onProgress) {
|
|
432
|
-
return this.callChainBatch(
|
|
433
|
-
connectId,
|
|
434
|
-
deviceId,
|
|
435
|
-
"evm",
|
|
436
|
-
"evmGetAddress",
|
|
437
|
-
params,
|
|
438
|
-
onProgress
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
662
|
evmSignTransaction(connectId, deviceId, params) {
|
|
442
663
|
return this.callChain(connectId, deviceId, "evm", "evmSignTransaction", params);
|
|
443
664
|
}
|
|
@@ -453,16 +674,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
453
674
|
btcGetAddress(connectId, deviceId, params) {
|
|
454
675
|
return this.callChain(connectId, deviceId, "btc", "btcGetAddress", params);
|
|
455
676
|
}
|
|
456
|
-
btcGetAddresses(connectId, deviceId, params, onProgress) {
|
|
457
|
-
return this.callChainBatch(
|
|
458
|
-
connectId,
|
|
459
|
-
deviceId,
|
|
460
|
-
"btc",
|
|
461
|
-
"btcGetAddress",
|
|
462
|
-
params,
|
|
463
|
-
onProgress
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
677
|
btcGetPublicKey(connectId, deviceId, params) {
|
|
467
678
|
return this.callChain(connectId, deviceId, "btc", "btcGetPublicKey", params);
|
|
468
679
|
}
|
|
@@ -490,16 +701,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
490
701
|
solGetAddress(connectId, deviceId, params) {
|
|
491
702
|
return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
|
|
492
703
|
}
|
|
493
|
-
solGetAddresses(connectId, deviceId, params, onProgress) {
|
|
494
|
-
return this.callChainBatch(
|
|
495
|
-
connectId,
|
|
496
|
-
deviceId,
|
|
497
|
-
"sol",
|
|
498
|
-
"solGetAddress",
|
|
499
|
-
params,
|
|
500
|
-
onProgress
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
704
|
solSignTransaction(connectId, deviceId, params) {
|
|
504
705
|
return this.callChain(connectId, deviceId, "sol", "solSignTransaction", params);
|
|
505
706
|
}
|
|
@@ -510,38 +711,13 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
510
711
|
// TRON chain methods
|
|
511
712
|
// ---------------------------------------------------------------------------
|
|
512
713
|
tronGetAddress(connectId, deviceId, params) {
|
|
513
|
-
return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params
|
|
514
|
-
}
|
|
515
|
-
tronGetAddresses(connectId, deviceId, params, onProgress) {
|
|
516
|
-
return this.callChainBatch(
|
|
517
|
-
connectId,
|
|
518
|
-
deviceId,
|
|
519
|
-
"tron",
|
|
520
|
-
"tronGetAddress",
|
|
521
|
-
params,
|
|
522
|
-
onProgress,
|
|
523
|
-
true
|
|
524
|
-
);
|
|
714
|
+
return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params);
|
|
525
715
|
}
|
|
526
716
|
tronSignTransaction(connectId, deviceId, params) {
|
|
527
|
-
return this.callChain(
|
|
528
|
-
connectId,
|
|
529
|
-
deviceId,
|
|
530
|
-
"tron",
|
|
531
|
-
"tronSignTransaction",
|
|
532
|
-
params,
|
|
533
|
-
true
|
|
534
|
-
);
|
|
717
|
+
return this.callChain(connectId, deviceId, "tron", "tronSignTransaction", params);
|
|
535
718
|
}
|
|
536
719
|
tronSignMessage(connectId, deviceId, params) {
|
|
537
|
-
return this.callChain(
|
|
538
|
-
connectId,
|
|
539
|
-
deviceId,
|
|
540
|
-
"tron",
|
|
541
|
-
"tronSignMessage",
|
|
542
|
-
params,
|
|
543
|
-
true
|
|
544
|
-
);
|
|
720
|
+
return this.callChain(connectId, deviceId, "tron", "tronSignMessage", params);
|
|
545
721
|
}
|
|
546
722
|
on(event, listener) {
|
|
547
723
|
this.emitter.on(event, listener);
|
|
@@ -550,30 +726,41 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
550
726
|
this.emitter.off(event, listener);
|
|
551
727
|
}
|
|
552
728
|
cancel(connectId) {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
729
|
+
const userAbortReason = Object.assign(new Error("User aborted operation"), {
|
|
730
|
+
code: HardwareErrorCode2.UserAborted,
|
|
731
|
+
_tag: ERROR_TAG.UserAborted
|
|
732
|
+
});
|
|
733
|
+
this._lastCancelReason = userAbortReason;
|
|
734
|
+
setTimeout(() => {
|
|
735
|
+
if (this._lastCancelReason === userAbortReason) {
|
|
736
|
+
this._lastCancelReason = void 0;
|
|
737
|
+
}
|
|
738
|
+
}, 2e3);
|
|
739
|
+
this._uiRegistry.cancel();
|
|
740
|
+
if (connectId) {
|
|
741
|
+
this._jobQueue.cancelActiveAndPending(connectId, userAbortReason);
|
|
742
|
+
} else {
|
|
743
|
+
this._jobQueue.cancelActiveAndPending(void 0, userAbortReason);
|
|
744
|
+
}
|
|
745
|
+
if (connectId) {
|
|
746
|
+
const sessionId = this._sessions.get(connectId) ?? connectId;
|
|
747
|
+
void this.connector.cancel(sessionId);
|
|
748
|
+
} else {
|
|
749
|
+
for (const sid of this._sessions.values()) void this.connector.cancel(sid);
|
|
750
|
+
}
|
|
751
|
+
if (this._connectingPromise) {
|
|
752
|
+
this._doConnectAbortController?.abort(userAbortReason);
|
|
753
|
+
}
|
|
556
754
|
}
|
|
557
755
|
// ---------------------------------------------------------------------------
|
|
558
756
|
// Chain fingerprint
|
|
559
757
|
// ---------------------------------------------------------------------------
|
|
560
758
|
async getChainFingerprint(connectId, deviceId, chain) {
|
|
561
|
-
debugLog(
|
|
562
|
-
"[LedgerAdapter] getChainFingerprint called, chain:",
|
|
563
|
-
chain,
|
|
564
|
-
"connectId:",
|
|
565
|
-
connectId || "(empty)",
|
|
566
|
-
"sessions:",
|
|
567
|
-
this._sessions.size
|
|
568
|
-
);
|
|
569
|
-
await this._ensureDevicePermission(connectId, deviceId);
|
|
570
|
-
debugLog("[LedgerAdapter] getChainFingerprint permission ok, computing fingerprint");
|
|
571
759
|
try {
|
|
572
760
|
const fingerprint = await this._computeChainFingerprint(
|
|
573
761
|
chain,
|
|
574
|
-
(method, params) => this.connectorCall(connectId, method, params)
|
|
762
|
+
(method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
|
|
575
763
|
);
|
|
576
|
-
debugLog("[LedgerAdapter] getChainFingerprint result:", fingerprint?.substring(0, 20));
|
|
577
764
|
return success(fingerprint);
|
|
578
765
|
} catch (err) {
|
|
579
766
|
debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
|
|
@@ -644,102 +831,252 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
644
831
|
if (signal?.aborted) {
|
|
645
832
|
_LedgerAdapter._throwIfAborted(signal);
|
|
646
833
|
}
|
|
834
|
+
const waitPromise = this._uiRegistry.wait(
|
|
835
|
+
UI_REQUEST.REQUEST_DEVICE_CONNECT
|
|
836
|
+
);
|
|
647
837
|
this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_CONNECT, {
|
|
648
838
|
type: UI_REQUEST.REQUEST_DEVICE_CONNECT,
|
|
649
839
|
payload: {
|
|
840
|
+
vendor: "ledger",
|
|
841
|
+
reason: "device-not-found",
|
|
650
842
|
message: "Please connect and unlock your Ledger device"
|
|
651
843
|
}
|
|
652
844
|
});
|
|
653
|
-
const waitPromise = this._uiRegistry.wait(
|
|
654
|
-
UI_REQUEST.REQUEST_DEVICE_CONNECT
|
|
655
|
-
);
|
|
656
845
|
let payload;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
846
|
+
try {
|
|
847
|
+
if (signal) {
|
|
848
|
+
const onAbort = () => {
|
|
849
|
+
this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_CONNECT);
|
|
850
|
+
};
|
|
851
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
852
|
+
try {
|
|
853
|
+
payload = await waitPromise;
|
|
854
|
+
} finally {
|
|
855
|
+
signal.removeEventListener("abort", onAbort);
|
|
856
|
+
}
|
|
857
|
+
} else {
|
|
663
858
|
payload = await waitPromise;
|
|
664
|
-
} finally {
|
|
665
|
-
signal.removeEventListener("abort", onAbort);
|
|
666
859
|
}
|
|
667
|
-
}
|
|
668
|
-
|
|
860
|
+
} catch (err) {
|
|
861
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
862
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
863
|
+
payload: {}
|
|
864
|
+
});
|
|
865
|
+
throw Object.assign(new Error("User cancelled Ledger connection"), {
|
|
866
|
+
_tag: ERROR_TAG.UserAborted,
|
|
867
|
+
code: HardwareErrorCode2.UserAborted,
|
|
868
|
+
cause: err
|
|
869
|
+
});
|
|
669
870
|
}
|
|
670
871
|
if (!payload?.confirmed) {
|
|
671
872
|
throw Object.assign(new Error("User cancelled Ledger connection"), {
|
|
672
|
-
_tag:
|
|
873
|
+
_tag: ERROR_TAG.UserAborted,
|
|
874
|
+
code: HardwareErrorCode2.UserAborted
|
|
673
875
|
});
|
|
674
876
|
}
|
|
877
|
+
await new Promise((resolve) => {
|
|
878
|
+
setTimeout(resolve, 800);
|
|
879
|
+
});
|
|
675
880
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
881
|
+
/**
|
|
882
|
+
* Decide whether a BTC pubkey call needs `showOnDevice=true` because of
|
|
883
|
+
* the BTC App's account-index policy, asking the user once per session.
|
|
884
|
+
*
|
|
885
|
+
* Returns the params to pass through (with `showOnDevice` possibly
|
|
886
|
+
* promoted), or `null` if the user declined.
|
|
887
|
+
*/
|
|
888
|
+
async _gateBtcHighIndex(params) {
|
|
889
|
+
const accountIndex = btcAccountIndexFromPath(params.path);
|
|
890
|
+
if (params.showOnDevice) return params;
|
|
891
|
+
if (accountIndex === null || accountIndex < BTC_HIGH_INDEX_THRESHOLD) {
|
|
892
|
+
return params;
|
|
683
893
|
}
|
|
684
|
-
if (this.
|
|
685
|
-
return
|
|
894
|
+
if (this._btcHighIndexConfirmedThisSession) {
|
|
895
|
+
return { ...params, showOnDevice: true };
|
|
686
896
|
}
|
|
687
|
-
|
|
897
|
+
const confirmed = await this._waitForBtcHighIndexConfirm(params.path, accountIndex);
|
|
898
|
+
if (!confirmed) return null;
|
|
899
|
+
this._btcHighIndexConfirmedThisSession = true;
|
|
900
|
+
return { ...params, showOnDevice: true };
|
|
901
|
+
}
|
|
902
|
+
async _waitForBtcHighIndexConfirm(path, accountIndex) {
|
|
903
|
+
const waitPromise = this._uiRegistry.wait(
|
|
904
|
+
UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM
|
|
905
|
+
);
|
|
906
|
+
this.emitter.emit(UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM, {
|
|
907
|
+
type: UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
|
|
908
|
+
payload: {
|
|
909
|
+
vendor: "ledger",
|
|
910
|
+
path,
|
|
911
|
+
accountIndex
|
|
912
|
+
}
|
|
913
|
+
});
|
|
688
914
|
try {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
915
|
+
const payload = await waitPromise;
|
|
916
|
+
return !!payload?.confirmed;
|
|
917
|
+
} catch (err) {
|
|
918
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
919
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
920
|
+
payload: {}
|
|
921
|
+
});
|
|
922
|
+
return false;
|
|
692
923
|
}
|
|
693
924
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
925
|
+
// Layer 1 entry. Caller signal only races the outer awaiter; the shared
|
|
926
|
+
// `_doConnect` runs under its own internal controller so caller A's cancel
|
|
927
|
+
// doesn't kill caller B's await.
|
|
928
|
+
async ensureConnected(connectId, signal) {
|
|
929
|
+
if (signal.aborted) _LedgerAdapter._throwIfAborted(signal);
|
|
930
|
+
if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
|
|
931
|
+
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
932
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
if (connectId && this._sessions.has(connectId)) return connectId;
|
|
936
|
+
if (!connectId && this._sessions.size > 0) {
|
|
937
|
+
if (!isLedgerBleConnectionType(this.connector.connectionType) && this._sessions.size > 1) {
|
|
938
|
+
throw Object.assign(
|
|
939
|
+
new Error(
|
|
940
|
+
"Ledger USB session invariant violated: more than one session is active. Please reconnect the device."
|
|
941
|
+
),
|
|
942
|
+
{ code: HardwareErrorCode2.DeviceOneDeviceOnly }
|
|
943
|
+
);
|
|
702
944
|
}
|
|
945
|
+
return this._sessions.keys().next().value;
|
|
703
946
|
}
|
|
704
|
-
|
|
705
|
-
new
|
|
706
|
-
|
|
707
|
-
)
|
|
708
|
-
|
|
709
|
-
|
|
947
|
+
if (!this._connectingPromise) {
|
|
948
|
+
this._doConnectAbortController = new AbortController();
|
|
949
|
+
const innerSignal = this._doConnectAbortController.signal;
|
|
950
|
+
this._connectingPromise = (async () => {
|
|
951
|
+
try {
|
|
952
|
+
return await this._doConnect(innerSignal, connectId);
|
|
953
|
+
} finally {
|
|
954
|
+
this._connectingPromise = null;
|
|
955
|
+
this._doConnectAbortController = null;
|
|
956
|
+
}
|
|
957
|
+
})();
|
|
958
|
+
}
|
|
959
|
+
return this._abortable(signal, this._connectingPromise);
|
|
710
960
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
961
|
+
// Layer 1 main loop — the ONLY place in SDK that emits unlock dialog.
|
|
962
|
+
// Bounded by MAX_DOCONNECT_CONFIRMS — after N Confirms with no progress,
|
|
963
|
+
// throw DeviceNotFound so the user is kicked out of the loop.
|
|
964
|
+
async _doConnect(internalSignal, targetConnectId) {
|
|
965
|
+
if (isLedgerBleConnectionType(this.connector.connectionType) && targetConnectId) {
|
|
966
|
+
try {
|
|
967
|
+
return await this._connectDeviceOrThrow(targetConnectId);
|
|
968
|
+
} catch (err) {
|
|
969
|
+
if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
|
|
970
|
+
throw err;
|
|
971
|
+
}
|
|
972
|
+
this._discoveredDevices.delete(targetConnectId);
|
|
973
|
+
if (isDeviceDisconnectedError(err)) {
|
|
974
|
+
try {
|
|
975
|
+
this.connector.reset?.();
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
let confirms = 0;
|
|
982
|
+
while (!internalSignal.aborted) {
|
|
983
|
+
this.emitter.emit("ui-event", {
|
|
984
|
+
type: EConnectorInteraction.Searching,
|
|
985
|
+
payload: { sessionId: "" }
|
|
717
986
|
});
|
|
987
|
+
let devices = await this.searchDevices();
|
|
988
|
+
if (devices.length === 0) {
|
|
989
|
+
for (let i = 0; i < 3 && !internalSignal.aborted; i += 1) {
|
|
990
|
+
await new Promise((resolve) => {
|
|
991
|
+
setTimeout(resolve, 1e3);
|
|
992
|
+
});
|
|
993
|
+
devices = await this.searchDevices();
|
|
994
|
+
if (devices.length > 0) break;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (devices.length > 0) {
|
|
998
|
+
try {
|
|
999
|
+
return await this._connectFirstOrSelect(devices, targetConnectId);
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
|
|
1002
|
+
throw err;
|
|
1003
|
+
}
|
|
1004
|
+
this._discoveredDevices.clear();
|
|
1005
|
+
if (isDeviceDisconnectedError(err)) {
|
|
1006
|
+
try {
|
|
1007
|
+
this.connector.reset?.();
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (confirms >= _LedgerAdapter.MAX_DOCONNECT_CONFIRMS) {
|
|
1014
|
+
throw Object.assign(
|
|
1015
|
+
new Error(
|
|
1016
|
+
"Device not connected after multiple attempts. Please ensure your Ledger is awake, unlocked, and in range, then try again."
|
|
1017
|
+
),
|
|
1018
|
+
{ code: HardwareErrorCode2.DeviceNotFound }
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
await this._waitForDeviceConnect(internalSignal);
|
|
1022
|
+
confirms += 1;
|
|
718
1023
|
}
|
|
719
|
-
|
|
1024
|
+
_LedgerAdapter._throwIfAborted(internalSignal);
|
|
1025
|
+
throw new Error("_doConnect aborted");
|
|
720
1026
|
}
|
|
721
|
-
async
|
|
722
|
-
if (!this.
|
|
1027
|
+
async _connectFirstOrSelect(devices, targetConnectId) {
|
|
1028
|
+
if (!isLedgerBleConnectionType(this.connector.connectionType) && devices.length > 1) {
|
|
723
1029
|
debugLog(
|
|
724
|
-
`[DMK] Multiple Ledger devices found (${devices.length});
|
|
1030
|
+
`[DMK] Multiple Ledger USB devices found (${devices.length}); refusing to auto-select by ephemeral connectId.`
|
|
725
1031
|
);
|
|
726
|
-
|
|
1032
|
+
throw createMultipleUsbLedgerDevicesError();
|
|
727
1033
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
});
|
|
732
|
-
const response = await this._uiRegistry.wait(
|
|
733
|
-
UI_REQUEST.REQUEST_SELECT_DEVICE
|
|
734
|
-
);
|
|
735
|
-
const chosen = devices.find((d) => d.connectId === response?.sdkConnectId);
|
|
736
|
-
if (!chosen) {
|
|
737
|
-
throw Object.assign(
|
|
738
|
-
new Error(`Selected sdkConnectId '${response?.sdkConnectId}' not in discovered list`),
|
|
739
|
-
{ _tag: "DeviceNotRecognizedError" }
|
|
1034
|
+
if (targetConnectId) {
|
|
1035
|
+
const target = devices.find(
|
|
1036
|
+
(d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
|
|
740
1037
|
);
|
|
1038
|
+
if (target) {
|
|
1039
|
+
return this._connectDeviceOrThrow(target.connectId);
|
|
1040
|
+
}
|
|
1041
|
+
if (!isLedgerBleConnectionType(this.connector.connectionType) && devices.length === 1) {
|
|
1042
|
+
debugLog(
|
|
1043
|
+
`[LedgerAdapter] target ${targetConnectId} not in fresh enumeration; accepting sole USB device ${devices[0].connectId} (assumed ephemeral path change)`
|
|
1044
|
+
);
|
|
1045
|
+
return this._connectDeviceOrThrow(devices[0].connectId);
|
|
1046
|
+
}
|
|
1047
|
+
const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
|
|
1048
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
1049
|
+
});
|
|
1050
|
+
if (isLedgerBleConnectionType(this.connector.connectionType)) {
|
|
1051
|
+
err._tag = ERROR_TAG.DeviceNotAdvertising;
|
|
1052
|
+
}
|
|
1053
|
+
throw err;
|
|
1054
|
+
}
|
|
1055
|
+
if (isLedgerBleConnectionType(this.connector.connectionType)) {
|
|
1056
|
+
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
1057
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
if (devices.length !== 1) {
|
|
1061
|
+
throw Object.assign(new Error("Ledger device not found."), {
|
|
1062
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return this._connectDeviceOrThrow(devices[0].connectId);
|
|
1066
|
+
}
|
|
1067
|
+
async _connectDeviceOrThrow(chosenConnectId) {
|
|
1068
|
+
const result = await this.connectDevice(chosenConnectId);
|
|
1069
|
+
if (!result.success) {
|
|
1070
|
+
const payload = result.payload;
|
|
1071
|
+
const rethrow = Object.assign(new Error(payload.error), {
|
|
1072
|
+
code: payload.code
|
|
1073
|
+
});
|
|
1074
|
+
if (payload._tag !== void 0) {
|
|
1075
|
+
rethrow._tag = payload._tag;
|
|
1076
|
+
}
|
|
1077
|
+
throw rethrow;
|
|
741
1078
|
}
|
|
742
|
-
return
|
|
1079
|
+
return chosenConnectId;
|
|
743
1080
|
}
|
|
744
1081
|
/**
|
|
745
1082
|
* Call the connector with automatic session resolution and disconnect retry.
|
|
@@ -749,30 +1086,31 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
749
1086
|
* 3. Calls connector.call()
|
|
750
1087
|
* 4. On disconnect error: clears stale session, re-connects, retries once
|
|
751
1088
|
*/
|
|
752
|
-
async connectorCall(connectId, method, params, fingerprint) {
|
|
1089
|
+
async connectorCall(connectId, method, params, fingerprint, permissionDeviceId) {
|
|
753
1090
|
debugLog("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
|
|
754
1091
|
const queueKey = connectId || "__ledger_default__";
|
|
755
|
-
const interruptibility = _LedgerAdapter._getInterruptibility(method);
|
|
756
1092
|
return this._jobQueue.enqueue(
|
|
757
1093
|
queueKey,
|
|
758
|
-
async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint),
|
|
759
|
-
{
|
|
1094
|
+
async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId),
|
|
1095
|
+
{
|
|
1096
|
+
label: method,
|
|
1097
|
+
rejectIfBusy: true,
|
|
1098
|
+
busyError: _LedgerAdapter._createDeviceBusyError(method)
|
|
1099
|
+
}
|
|
760
1100
|
);
|
|
761
1101
|
}
|
|
762
1102
|
/**
|
|
763
|
-
* Race a promise against an abort signal.
|
|
764
|
-
* signal
|
|
765
|
-
* actually be cancelled on Ledger DMK, but the caller gets the abort immediately.
|
|
1103
|
+
* Race a promise against an abort signal. On abort, rejects with
|
|
1104
|
+
* signal.reason → instance _lastCancelReason → generic Error('Aborted').
|
|
766
1105
|
*/
|
|
767
|
-
|
|
1106
|
+
_abortable(signal, promise) {
|
|
1107
|
+
const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
|
|
768
1108
|
if (signal.aborted) {
|
|
769
|
-
return Promise.reject(
|
|
770
|
-
signal.reason ?? new Error("Aborted")
|
|
771
|
-
);
|
|
1109
|
+
return Promise.reject(getAbortReason());
|
|
772
1110
|
}
|
|
773
1111
|
return new Promise((resolve, reject) => {
|
|
774
1112
|
const onAbort = () => {
|
|
775
|
-
reject(
|
|
1113
|
+
reject(getAbortReason());
|
|
776
1114
|
};
|
|
777
1115
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
778
1116
|
promise.then(
|
|
@@ -794,40 +1132,49 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
794
1132
|
}
|
|
795
1133
|
}
|
|
796
1134
|
/** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
|
|
797
|
-
async _runConnectorCall(connectId, method, params, signal, fingerprint) {
|
|
1135
|
+
async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId) {
|
|
798
1136
|
_LedgerAdapter._throwIfAborted(signal);
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1137
|
+
await this._ensureDevicePermission(
|
|
1138
|
+
connectId,
|
|
1139
|
+
permissionDeviceId ?? fingerprint?.deviceId,
|
|
1140
|
+
signal
|
|
802
1141
|
);
|
|
803
1142
|
_LedgerAdapter._throwIfAborted(signal);
|
|
1143
|
+
let effectiveParams = params;
|
|
1144
|
+
if (method === "btcGetPublicKey") {
|
|
1145
|
+
const gatedParams = await this._gateBtcHighIndex(params);
|
|
1146
|
+
if (gatedParams === null) {
|
|
1147
|
+
throw Object.assign(new Error("User cancelled BTC high-index confirmation"), {
|
|
1148
|
+
_tag: ERROR_TAG.UserAborted,
|
|
1149
|
+
code: HardwareErrorCode2.UserAborted
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
effectiveParams = gatedParams;
|
|
1153
|
+
}
|
|
1154
|
+
const resolvedConnectId = await this.ensureConnected(connectId, signal);
|
|
804
1155
|
const sessionId = this._sessions.get(resolvedConnectId);
|
|
805
|
-
debugLog(
|
|
806
|
-
"[LedgerAdapter] connectorCall resolved:",
|
|
807
|
-
method,
|
|
808
|
-
"resolvedConnectId:",
|
|
809
|
-
resolvedConnectId,
|
|
810
|
-
"sessionId:",
|
|
811
|
-
sessionId
|
|
812
|
-
);
|
|
813
1156
|
if (!sessionId) {
|
|
814
1157
|
throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
|
|
815
|
-
_tag:
|
|
1158
|
+
_tag: ERROR_TAG.DeviceSessionNotFound
|
|
816
1159
|
});
|
|
817
1160
|
}
|
|
818
|
-
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
819
|
-
const fp = await _LedgerAdapter._abortable(
|
|
820
|
-
signal,
|
|
821
|
-
this._verifyDeviceFingerprintWithSession(sessionId, fingerprint.deviceId, fingerprint.chain)
|
|
822
|
-
);
|
|
823
|
-
if (!fp.success) {
|
|
824
|
-
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
825
|
-
code: HardwareErrorCode2.DeviceMismatch
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
1161
|
try {
|
|
830
|
-
|
|
1162
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1163
|
+
const fp = await this._abortable(
|
|
1164
|
+
signal,
|
|
1165
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1166
|
+
sessionId,
|
|
1167
|
+
fingerprint.deviceId,
|
|
1168
|
+
fingerprint.chain
|
|
1169
|
+
)
|
|
1170
|
+
);
|
|
1171
|
+
if (!fp.success) {
|
|
1172
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1173
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return await this._abortable(signal, this.connector.call(sessionId, method, effectiveParams));
|
|
831
1178
|
} catch (err) {
|
|
832
1179
|
if (signal.aborted) throw err;
|
|
833
1180
|
const errObj = err;
|
|
@@ -837,37 +1184,241 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
837
1184
|
errorCode: errObj?.errorCode,
|
|
838
1185
|
statusCode: errObj?.statusCode,
|
|
839
1186
|
isDisconnected: isDeviceDisconnectedError(err),
|
|
840
|
-
isLocked: isDeviceLockedError(err)
|
|
1187
|
+
isLocked: isDeviceLockedError(err),
|
|
1188
|
+
isNotAdvertising: isDeviceNotAdvertisingError(err),
|
|
1189
|
+
isStuckApp: isStuckAppStateError(err)
|
|
841
1190
|
});
|
|
842
|
-
if (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1191
|
+
if (isStuckAppStateError(err)) {
|
|
1192
|
+
try {
|
|
1193
|
+
this._sessions.delete(resolvedConnectId);
|
|
1194
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
1195
|
+
this.connector.reset?.();
|
|
1196
|
+
} catch {
|
|
1197
|
+
}
|
|
1198
|
+
debugLog(
|
|
1199
|
+
"[LedgerAdapter] stuck-app retry: method=",
|
|
1200
|
+
method,
|
|
1201
|
+
"delayMs=",
|
|
1202
|
+
_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
|
|
1203
|
+
"_tag=",
|
|
1204
|
+
err?._tag
|
|
1205
|
+
);
|
|
1206
|
+
try {
|
|
1207
|
+
const retryResult = await this._retryAfterStuckApp(
|
|
1208
|
+
resolvedConnectId,
|
|
1209
|
+
method,
|
|
1210
|
+
effectiveParams,
|
|
1211
|
+
signal,
|
|
1212
|
+
err,
|
|
1213
|
+
fingerprint
|
|
1214
|
+
);
|
|
1215
|
+
debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
|
|
1216
|
+
return retryResult;
|
|
1217
|
+
} catch (retryErr) {
|
|
1218
|
+
if (signal.aborted) throw retryErr;
|
|
1219
|
+
if (isStuckAppStateError(retryErr)) {
|
|
1220
|
+
debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
|
|
1221
|
+
throw err;
|
|
1222
|
+
}
|
|
1223
|
+
debugLog(
|
|
1224
|
+
"[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
|
|
1225
|
+
method,
|
|
1226
|
+
"retryErrTag=",
|
|
1227
|
+
retryErr?._tag
|
|
1228
|
+
);
|
|
1229
|
+
throw retryErr;
|
|
1230
|
+
}
|
|
846
1231
|
}
|
|
847
|
-
if (isDeviceLockedError(err)) {
|
|
848
|
-
|
|
849
|
-
_LedgerAdapter.
|
|
850
|
-
|
|
1232
|
+
if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
|
|
1233
|
+
let lastErr = err;
|
|
1234
|
+
for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
|
|
1235
|
+
if (signal.aborted) throw lastErr;
|
|
1236
|
+
try {
|
|
1237
|
+
this._sessions.delete(resolvedConnectId);
|
|
1238
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
1239
|
+
if (isDeviceDisconnectedError(lastErr)) {
|
|
1240
|
+
try {
|
|
1241
|
+
this.connector.reset?.();
|
|
1242
|
+
} catch {
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const reConnectId = await this.ensureConnected(resolvedConnectId, signal);
|
|
1246
|
+
const reSessionId = this._sessions.get(reConnectId);
|
|
1247
|
+
if (!reSessionId) throw lastErr;
|
|
1248
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1249
|
+
const fp = await this._abortable(
|
|
1250
|
+
signal,
|
|
1251
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1252
|
+
reSessionId,
|
|
1253
|
+
fingerprint.deviceId,
|
|
1254
|
+
fingerprint.chain
|
|
1255
|
+
)
|
|
1256
|
+
);
|
|
1257
|
+
if (!fp.success) {
|
|
1258
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1259
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return await this._abortable(
|
|
1264
|
+
signal,
|
|
1265
|
+
this.connector.call(reSessionId, method, effectiveParams)
|
|
1266
|
+
);
|
|
1267
|
+
} catch (retryErr) {
|
|
1268
|
+
if (signal.aborted) throw retryErr;
|
|
1269
|
+
lastErr = retryErr;
|
|
1270
|
+
const canRetry = attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET - 1 && (isDeviceLockedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isDeviceDisconnectedError(retryErr));
|
|
1271
|
+
if (!canRetry) {
|
|
1272
|
+
throw retryErr;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
throw lastErr;
|
|
851
1277
|
}
|
|
852
1278
|
if (isTimeoutError(err)) {
|
|
853
1279
|
debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
|
|
854
1280
|
this._discoveredDevices.delete(resolvedConnectId);
|
|
855
|
-
return this._retryWithFreshConnection(
|
|
1281
|
+
return this._retryWithFreshConnection(
|
|
1282
|
+
resolvedConnectId,
|
|
1283
|
+
method,
|
|
1284
|
+
effectiveParams,
|
|
1285
|
+
signal,
|
|
1286
|
+
err,
|
|
1287
|
+
fingerprint
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
if (isConnectionLevelError(err)) {
|
|
1291
|
+
debugLog("[LedgerAdapter] connection-level fail-closed reset");
|
|
1292
|
+
this._sessions.delete(resolvedConnectId);
|
|
1293
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
1294
|
+
this.connector.reset();
|
|
1295
|
+
const codeNum = err?.code;
|
|
1296
|
+
throw Object.assign(err, {
|
|
1297
|
+
code: codeNum ?? HardwareErrorCode2.DeviceDisconnected
|
|
1298
|
+
});
|
|
856
1299
|
}
|
|
857
1300
|
throw err;
|
|
858
1301
|
}
|
|
859
1302
|
}
|
|
860
|
-
/**
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1303
|
+
/**
|
|
1304
|
+
* Stuck-app recovery: pause for the device's UI transition, then retry once.
|
|
1305
|
+
*
|
|
1306
|
+
* Caller has already cleared the session + reset connector. We wait so Stax
|
|
1307
|
+
* finishes its post-CloseApp animation, then go through ensureConnected +
|
|
1308
|
+
* fingerprint check + call exactly once. Caller decides what to do on a
|
|
1309
|
+
* second stuck-app hit.
|
|
1310
|
+
*/
|
|
1311
|
+
async _retryAfterStuckApp(resolvedConnectId, method, params, signal, originalErr, fingerprint) {
|
|
1312
|
+
await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
|
|
1313
|
+
const retryConnectId = await this.ensureConnected(resolvedConnectId, signal);
|
|
1314
|
+
const retrySessionId = this._sessions.get(retryConnectId);
|
|
1315
|
+
if (!retrySessionId) throw originalErr;
|
|
1316
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1317
|
+
const fp = await this._abortable(
|
|
1318
|
+
signal,
|
|
1319
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1320
|
+
retrySessionId,
|
|
1321
|
+
fingerprint.deviceId,
|
|
1322
|
+
fingerprint.chain
|
|
1323
|
+
)
|
|
1324
|
+
);
|
|
1325
|
+
if (!fp.success) {
|
|
1326
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1327
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return this._abortable(signal, this.connector.call(retrySessionId, method, params));
|
|
1332
|
+
}
|
|
1333
|
+
_sleepAbortable(ms, signal) {
|
|
1334
|
+
return new Promise((resolve, reject) => {
|
|
1335
|
+
if (signal.aborted) {
|
|
1336
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const timer = setTimeout(() => {
|
|
1340
|
+
signal.removeEventListener("abort", onAbort);
|
|
1341
|
+
resolve();
|
|
1342
|
+
}, ms);
|
|
1343
|
+
const onAbort = () => {
|
|
1344
|
+
clearTimeout(timer);
|
|
1345
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
1346
|
+
};
|
|
1347
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Clear stale session, reconnect, and retry the call.
|
|
1352
|
+
*
|
|
1353
|
+
* Timeout recovery starts with a full connector reset. After an APDU
|
|
1354
|
+
* timeout, DMK/transport state may still emit malformed responses; retrying
|
|
1355
|
+
* on the same DMK can poison the next chain switch.
|
|
1356
|
+
*/
|
|
1357
|
+
async _retryWithFreshConnection(targetConnectId, method, params, signal, originalErr, fingerprint) {
|
|
1358
|
+
this.connector.reset();
|
|
1359
|
+
this._sessions.clear();
|
|
1360
|
+
this._discoveredDevices.clear();
|
|
1361
|
+
this._connectingPromise = null;
|
|
1362
|
+
const retryConnectId = await this.ensureConnected(targetConnectId, signal);
|
|
866
1363
|
const retrySessionId = this._sessions.get(retryConnectId);
|
|
867
1364
|
if (!retrySessionId) {
|
|
868
1365
|
throw originalErr;
|
|
869
1366
|
}
|
|
870
|
-
|
|
1367
|
+
try {
|
|
1368
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1369
|
+
const fp = await this._abortable(
|
|
1370
|
+
signal,
|
|
1371
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1372
|
+
retrySessionId,
|
|
1373
|
+
fingerprint.deviceId,
|
|
1374
|
+
fingerprint.chain
|
|
1375
|
+
)
|
|
1376
|
+
);
|
|
1377
|
+
if (!fp.success) {
|
|
1378
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1379
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return await this._abortable(signal, this.connector.call(retrySessionId, method, params));
|
|
1384
|
+
} catch (retryErr) {
|
|
1385
|
+
if (signal.aborted) throw retryErr;
|
|
1386
|
+
this.connector.reset();
|
|
1387
|
+
this._sessions.clear();
|
|
1388
|
+
this._discoveredDevices.clear();
|
|
1389
|
+
this._connectingPromise = null;
|
|
1390
|
+
if (!isDeviceDisconnectedError(retryErr) && !isTimeoutError(retryErr)) {
|
|
1391
|
+
throw retryErr;
|
|
1392
|
+
}
|
|
1393
|
+
debugLog(
|
|
1394
|
+
"[LedgerAdapter] fresh-session retry still failed; resetting connector and rebuilding DMK"
|
|
1395
|
+
);
|
|
1396
|
+
this.connector.reset();
|
|
1397
|
+
this._sessions.clear();
|
|
1398
|
+
this._discoveredDevices.clear();
|
|
1399
|
+
this._connectingPromise = null;
|
|
1400
|
+
const finalConnectId = await this.ensureConnected(targetConnectId, signal);
|
|
1401
|
+
const finalSessionId = this._sessions.get(finalConnectId);
|
|
1402
|
+
if (!finalSessionId) {
|
|
1403
|
+
throw originalErr;
|
|
1404
|
+
}
|
|
1405
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1406
|
+
const fp = await this._abortable(
|
|
1407
|
+
signal,
|
|
1408
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1409
|
+
finalSessionId,
|
|
1410
|
+
fingerprint.deviceId,
|
|
1411
|
+
fingerprint.chain
|
|
1412
|
+
)
|
|
1413
|
+
);
|
|
1414
|
+
if (!fp.success) {
|
|
1415
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1416
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return this._abortable(signal, this.connector.call(finalSessionId, method, params));
|
|
1421
|
+
}
|
|
871
1422
|
}
|
|
872
1423
|
/**
|
|
873
1424
|
* Ensure OS-level device permission (Bluetooth / USB) before proceeding.
|
|
@@ -881,7 +1432,10 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
881
1432
|
* - No connectId (searchDevices): environment-level permission
|
|
882
1433
|
* - With connectId (business methods): device-level permission
|
|
883
1434
|
*/
|
|
884
|
-
async _ensureDevicePermission(connectId, deviceId) {
|
|
1435
|
+
async _ensureDevicePermission(connectId, deviceId, signal) {
|
|
1436
|
+
if (signal?.aborted) {
|
|
1437
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
1438
|
+
}
|
|
885
1439
|
const transportType = this.activeTransport ?? "hid";
|
|
886
1440
|
const waitPromise = this._uiRegistry.wait(
|
|
887
1441
|
UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
@@ -891,10 +1445,37 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
891
1445
|
type: UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
892
1446
|
payload: { transportType, connectId, deviceId }
|
|
893
1447
|
});
|
|
894
|
-
|
|
1448
|
+
let response;
|
|
1449
|
+
const onAbort = () => {
|
|
1450
|
+
this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_PERMISSION);
|
|
1451
|
+
};
|
|
1452
|
+
try {
|
|
1453
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1454
|
+
response = await waitPromise;
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
if (signal?.aborted) {
|
|
1457
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
1458
|
+
}
|
|
1459
|
+
if (err?._tag === UI_REQUEST_PREEMPTED_TAG) {
|
|
1460
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
1461
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
1462
|
+
payload: {}
|
|
1463
|
+
});
|
|
1464
|
+
throw Object.assign(new Error("Device permission request superseded"), {
|
|
1465
|
+
_tag: ERROR_TAG.UserAborted,
|
|
1466
|
+
code: HardwareErrorCode2.UserAborted,
|
|
1467
|
+
cause: err
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
throw err;
|
|
1471
|
+
} finally {
|
|
1472
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1473
|
+
}
|
|
1474
|
+
const { granted, reason, message } = response;
|
|
895
1475
|
if (!granted) {
|
|
896
|
-
throw Object.assign(new Error("Device permission denied"), {
|
|
897
|
-
code: HardwareErrorCode2.DevicePermissionDenied
|
|
1476
|
+
throw Object.assign(new Error(message ?? "Device permission denied"), {
|
|
1477
|
+
code: HardwareErrorCode2.DevicePermissionDenied,
|
|
1478
|
+
reason
|
|
898
1479
|
});
|
|
899
1480
|
}
|
|
900
1481
|
}
|
|
@@ -904,89 +1485,61 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
904
1485
|
*/
|
|
905
1486
|
errorToFailure(err) {
|
|
906
1487
|
debugError("[LedgerAdapter] error:", err);
|
|
1488
|
+
const tag = err && typeof err === "object" ? err._tag : void 0;
|
|
907
1489
|
if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
|
|
908
1490
|
const e = err;
|
|
909
|
-
|
|
1491
|
+
const params = e.code === HardwareErrorCode2.DevicePermissionDenied && e.reason ? { permissionDeniedReason: e.reason } : void 0;
|
|
1492
|
+
return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName, tag, params);
|
|
910
1493
|
}
|
|
911
1494
|
const mapped = mapLedgerError(err);
|
|
912
|
-
return ledgerFailure(mapped.code, mapped.message, mapped.appName);
|
|
1495
|
+
return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
|
|
913
1496
|
}
|
|
914
1497
|
registerEventListeners() {
|
|
915
1498
|
this.connector.on("device-connect", this.deviceConnectHandler);
|
|
916
1499
|
this.connector.on("device-disconnect", this.deviceDisconnectHandler);
|
|
917
|
-
this.connector.on("ui-
|
|
918
|
-
this.connector.on("ui-event", this.uiEventHandler);
|
|
1500
|
+
this.connector.on("ui-event", this.uiEventForwarder);
|
|
919
1501
|
}
|
|
920
1502
|
unregisterEventListeners() {
|
|
921
1503
|
this.connector.off("device-connect", this.deviceConnectHandler);
|
|
922
1504
|
this.connector.off("device-disconnect", this.deviceDisconnectHandler);
|
|
923
|
-
this.connector.off("ui-
|
|
924
|
-
this.connector.off("ui-event", this.uiEventHandler);
|
|
925
|
-
}
|
|
926
|
-
handleUiEvent(event) {
|
|
927
|
-
if (!event.type) return;
|
|
928
|
-
const payload = event.payload;
|
|
929
|
-
const deviceInfo = payload ? this.extractDeviceInfoFromPayload(payload) : this.unknownDevice();
|
|
930
|
-
switch (event.type) {
|
|
931
|
-
case "ui-request_confirmation":
|
|
932
|
-
this.emitter.emit(UI_REQUEST.REQUEST_BUTTON, {
|
|
933
|
-
type: UI_REQUEST.REQUEST_BUTTON,
|
|
934
|
-
payload: { device: deviceInfo }
|
|
935
|
-
});
|
|
936
|
-
break;
|
|
937
|
-
}
|
|
1505
|
+
this.connector.off("ui-event", this.uiEventForwarder);
|
|
938
1506
|
}
|
|
939
1507
|
// ---------------------------------------------------------------------------
|
|
940
1508
|
// Device info mapping
|
|
941
1509
|
// ---------------------------------------------------------------------------
|
|
942
1510
|
connectorDeviceToDeviceInfo(device) {
|
|
943
|
-
const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
|
|
944
1511
|
return {
|
|
945
1512
|
vendor: "ledger",
|
|
946
1513
|
model: device.model ?? "unknown",
|
|
1514
|
+
modelName: device.modelName,
|
|
947
1515
|
firmwareVersion: "",
|
|
948
1516
|
deviceId: device.deviceId,
|
|
949
1517
|
connectId: device.connectId,
|
|
950
1518
|
label: device.name,
|
|
951
|
-
connectionType:
|
|
1519
|
+
connectionType: this.connector.connectionType,
|
|
1520
|
+
rssi: device.rssi,
|
|
1521
|
+
isConnectable: device.isConnectable,
|
|
1522
|
+
serialNumber: device.serialNumber,
|
|
952
1523
|
capabilities: device.capabilities
|
|
953
1524
|
};
|
|
954
1525
|
}
|
|
955
|
-
extractDeviceInfoFromPayload(payload) {
|
|
956
|
-
return {
|
|
957
|
-
vendor: "ledger",
|
|
958
|
-
model: payload.model ?? "unknown",
|
|
959
|
-
firmwareVersion: "",
|
|
960
|
-
deviceId: payload.deviceId ?? payload.id ?? "",
|
|
961
|
-
connectId: payload.connectId ?? payload.path ?? "",
|
|
962
|
-
label: payload.label,
|
|
963
|
-
connectionType: "usb"
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
unknownDevice() {
|
|
967
|
-
return {
|
|
968
|
-
vendor: "ledger",
|
|
969
|
-
model: "unknown",
|
|
970
|
-
firmwareVersion: "",
|
|
971
|
-
deviceId: "",
|
|
972
|
-
connectId: "",
|
|
973
|
-
connectionType: "usb"
|
|
974
|
-
};
|
|
975
|
-
}
|
|
976
1526
|
};
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
_LedgerAdapter.
|
|
1527
|
+
// Layer 2 retry budget after connection-class error. Each round delegates
|
|
1528
|
+
// to Layer 1 (which owns the unlock prompt). Layer 2 never emits UI itself.
|
|
1529
|
+
_LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET = 3;
|
|
1530
|
+
// Stuck-app (APDU 0x6901) retry pause. Ledger Stax often returns 6901 when
|
|
1531
|
+
// OpenAppCommand lands mid-transition right after CloseApp; a short wait
|
|
1532
|
+
// lets the device finish its UI animation before we retry once.
|
|
1533
|
+
_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS = 500;
|
|
1534
|
+
// Layer 1 confirm budget. After N failed Confirm cycles (user keeps clicking
|
|
1535
|
+
// Confirm but device never shows up / never unlocks), throw DeviceNotFound
|
|
1536
|
+
// instead of looping forever.
|
|
1537
|
+
_LedgerAdapter.MAX_DOCONNECT_CONFIRMS = 3;
|
|
988
1538
|
var LedgerAdapter = _LedgerAdapter;
|
|
989
1539
|
|
|
1540
|
+
// src/connector/LedgerConnectorBase.ts
|
|
1541
|
+
import { HardwareErrorCode as HardwareErrorCode7 } from "@onekeyfe/hwk-adapter-core";
|
|
1542
|
+
|
|
990
1543
|
// src/device/LedgerDeviceManager.ts
|
|
991
1544
|
var LedgerDeviceManager = class {
|
|
992
1545
|
constructor(dmk) {
|
|
@@ -1023,7 +1576,9 @@ var LedgerDeviceManager = class {
|
|
|
1023
1576
|
if (resolved) return;
|
|
1024
1577
|
resolved = true;
|
|
1025
1578
|
this._discovered.clear();
|
|
1026
|
-
debugLog(
|
|
1579
|
+
debugLog(
|
|
1580
|
+
`[DMK] enumerate count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
|
|
1581
|
+
);
|
|
1027
1582
|
for (const d of devices) {
|
|
1028
1583
|
this._discovered.set(d.id, d);
|
|
1029
1584
|
}
|
|
@@ -1033,8 +1588,10 @@ var LedgerDeviceManager = class {
|
|
|
1033
1588
|
devices.map((d) => ({
|
|
1034
1589
|
path: d.id,
|
|
1035
1590
|
type: d.deviceModel.model,
|
|
1591
|
+
modelName: d.deviceModel.name,
|
|
1036
1592
|
name: d.name,
|
|
1037
|
-
transport: d.transport
|
|
1593
|
+
transport: d.transport,
|
|
1594
|
+
rssi: d.rssi
|
|
1038
1595
|
}))
|
|
1039
1596
|
);
|
|
1040
1597
|
} else {
|
|
@@ -1055,8 +1612,10 @@ var LedgerDeviceManager = class {
|
|
|
1055
1612
|
devices.map((d) => ({
|
|
1056
1613
|
path: d.id,
|
|
1057
1614
|
type: d.deviceModel.model,
|
|
1615
|
+
modelName: d.deviceModel.name,
|
|
1058
1616
|
name: d.name,
|
|
1059
|
-
transport: d.transport
|
|
1617
|
+
transport: d.transport,
|
|
1618
|
+
rssi: d.rssi
|
|
1060
1619
|
}))
|
|
1061
1620
|
);
|
|
1062
1621
|
}
|
|
@@ -1080,8 +1639,10 @@ var LedgerDeviceManager = class {
|
|
|
1080
1639
|
descriptor: {
|
|
1081
1640
|
path: d.id,
|
|
1082
1641
|
type: d.deviceModel.model,
|
|
1642
|
+
modelName: d.deviceModel.name,
|
|
1083
1643
|
name: d.name,
|
|
1084
|
-
transport: d.transport
|
|
1644
|
+
transport: d.transport,
|
|
1645
|
+
rssi: d.rssi
|
|
1085
1646
|
}
|
|
1086
1647
|
});
|
|
1087
1648
|
}
|
|
@@ -1096,10 +1657,58 @@ var LedgerDeviceManager = class {
|
|
|
1096
1657
|
}
|
|
1097
1658
|
});
|
|
1098
1659
|
}
|
|
1099
|
-
stopListening() {
|
|
1100
|
-
this._listenSub?.unsubscribe();
|
|
1101
|
-
this._listenSub = null;
|
|
1102
|
-
}
|
|
1660
|
+
stopListening() {
|
|
1661
|
+
this._listenSub?.unsubscribe();
|
|
1662
|
+
this._listenSub = null;
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Resolve to the latest snapshot of advertising devices observed within
|
|
1666
|
+
* `timeoutMs`. Rewrites `_discovered` so it tracks the live list.
|
|
1667
|
+
*
|
|
1668
|
+
* `listenToAvailableDevices` is a BehaviorSubject — it emits the cached
|
|
1669
|
+
* list on subscribe and *only* re-emits when the list changes. A stable
|
|
1670
|
+
* list of currently-advertising devices therefore produces only the
|
|
1671
|
+
* subscription frame; treating that frame as "fossil to be discarded"
|
|
1672
|
+
* caused devices to flicker in/out of the UI as searches alternated
|
|
1673
|
+
* between "saw a change frame" and "saw only the cached frame".
|
|
1674
|
+
*
|
|
1675
|
+
* The window still gives a chance for the active scan to update the list
|
|
1676
|
+
* (e.g. drop a peripheral that just stopped broadcasting), but if nothing
|
|
1677
|
+
* changes we trust the snapshot — DMK silence on a stable list means
|
|
1678
|
+
* "everything's still there".
|
|
1679
|
+
*/
|
|
1680
|
+
getLiveDevices(timeoutMs = 1500) {
|
|
1681
|
+
debugLog(`[DMK] getLiveDevices() called, timeoutMs=${timeoutMs}`);
|
|
1682
|
+
void this.requestDevice();
|
|
1683
|
+
return new Promise((resolve) => {
|
|
1684
|
+
let done = false;
|
|
1685
|
+
let lastSeen = [];
|
|
1686
|
+
let sub = null;
|
|
1687
|
+
const finish = (devices) => {
|
|
1688
|
+
if (done) return;
|
|
1689
|
+
done = true;
|
|
1690
|
+
sub?.unsubscribe();
|
|
1691
|
+
clearTimeout(timer);
|
|
1692
|
+
this._discovered.clear();
|
|
1693
|
+
for (const d of devices) this._discovered.set(d.id, d);
|
|
1694
|
+
debugLog(
|
|
1695
|
+
`[DMK] getLiveDevices() resolved count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
|
|
1696
|
+
);
|
|
1697
|
+
resolve(devices);
|
|
1698
|
+
};
|
|
1699
|
+
const timer = setTimeout(() => finish(lastSeen), timeoutMs);
|
|
1700
|
+
sub = this._dmk.listenToAvailableDevices({}).subscribe({
|
|
1701
|
+
next: (devices) => {
|
|
1702
|
+
lastSeen = devices;
|
|
1703
|
+
},
|
|
1704
|
+
error: (err) => {
|
|
1705
|
+
debugError("[DMK] getLiveDevices() error:", err);
|
|
1706
|
+
finish(lastSeen);
|
|
1707
|
+
},
|
|
1708
|
+
complete: () => finish(lastSeen)
|
|
1709
|
+
});
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1103
1712
|
requestDevice() {
|
|
1104
1713
|
if (this._discoverySub) {
|
|
1105
1714
|
return Promise.resolve();
|
|
@@ -1117,11 +1726,33 @@ var LedgerDeviceManager = class {
|
|
|
1117
1726
|
});
|
|
1118
1727
|
return Promise.resolve();
|
|
1119
1728
|
}
|
|
1729
|
+
/** Lookup a previously-discovered device's display name. */
|
|
1730
|
+
getDeviceName(deviceId) {
|
|
1731
|
+
return this._discovered.get(deviceId)?.name;
|
|
1732
|
+
}
|
|
1733
|
+
/** Lookup minimal model/signal info from a previously-discovered device. */
|
|
1734
|
+
getDiscoveredDeviceInfo(deviceId) {
|
|
1735
|
+
const d = this._discovered.get(deviceId);
|
|
1736
|
+
if (!d) return void 0;
|
|
1737
|
+
return {
|
|
1738
|
+
model: d.deviceModel?.model,
|
|
1739
|
+
modelName: d.deviceModel?.name,
|
|
1740
|
+
name: d.name,
|
|
1741
|
+
rssi: d.rssi
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
hasDiscoveredDevice(deviceId) {
|
|
1745
|
+
return this._discovered.has(deviceId);
|
|
1746
|
+
}
|
|
1120
1747
|
/** Connect to a previously discovered device. Returns sessionId. */
|
|
1121
1748
|
async connect(deviceId) {
|
|
1122
1749
|
const device = this._discovered.get(deviceId);
|
|
1123
1750
|
if (!device) {
|
|
1124
|
-
|
|
1751
|
+
const err = new Error(
|
|
1752
|
+
`Device "${deviceId}" not found in discovery cache. Call enumerate() or listen() first.`
|
|
1753
|
+
);
|
|
1754
|
+
err._tag = ERROR_TAG.DeviceNotInDiscoveryCache;
|
|
1755
|
+
throw err;
|
|
1125
1756
|
}
|
|
1126
1757
|
const sessionId = await this._dmk.connect({ device });
|
|
1127
1758
|
this._sessions.set(deviceId, sessionId);
|
|
@@ -1160,6 +1791,19 @@ var LedgerDeviceManager = class {
|
|
|
1160
1791
|
this._sessionToDevice.clear();
|
|
1161
1792
|
this._dmk.close();
|
|
1162
1793
|
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Tear down RxJS subscriptions and local state without closing the DMK.
|
|
1796
|
+
* For light-reset paths that want to drop session state but keep the
|
|
1797
|
+
* underlying transport alive for retry. Call this instead of orphaning
|
|
1798
|
+
* the manager — bare null assignment leaks _listenSub / _discoverySub.
|
|
1799
|
+
*/
|
|
1800
|
+
disposeKeepingDmk() {
|
|
1801
|
+
this.stopListening();
|
|
1802
|
+
this.stopDiscovery();
|
|
1803
|
+
this._discovered.clear();
|
|
1804
|
+
this._sessions.clear();
|
|
1805
|
+
this._sessionToDevice.clear();
|
|
1806
|
+
}
|
|
1163
1807
|
};
|
|
1164
1808
|
|
|
1165
1809
|
// src/signer/SignerManager.ts
|
|
@@ -1171,11 +1815,12 @@ import { hexToBytes } from "@onekeyfe/hwk-adapter-core";
|
|
|
1171
1815
|
|
|
1172
1816
|
// src/signer/deviceActionToPromise.ts
|
|
1173
1817
|
import { DeviceActionStatus } from "@ledgerhq/device-management-kit";
|
|
1174
|
-
var
|
|
1175
|
-
function deviceActionToPromise(action, onInteraction, timeoutMs =
|
|
1818
|
+
var IDLE_WATCHDOG_MS = 65e3;
|
|
1819
|
+
function deviceActionToPromise(action, onInteraction, timeoutMs = IDLE_WATCHDOG_MS, onRegisterCanceller) {
|
|
1176
1820
|
return new Promise((resolve, reject) => {
|
|
1177
1821
|
let settled = false;
|
|
1178
1822
|
let lastStep;
|
|
1823
|
+
const observedSteps = [];
|
|
1179
1824
|
let sub;
|
|
1180
1825
|
let timer = null;
|
|
1181
1826
|
const cancelAction = () => {
|
|
@@ -1188,44 +1833,59 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1188
1833
|
} catch {
|
|
1189
1834
|
}
|
|
1190
1835
|
};
|
|
1191
|
-
const
|
|
1836
|
+
const armIdleWatchdog = () => {
|
|
1192
1837
|
if (timer) clearTimeout(timer);
|
|
1193
1838
|
if (timeoutMs > 0) {
|
|
1194
1839
|
timer = setTimeout(() => {
|
|
1195
1840
|
if (!settled) {
|
|
1196
1841
|
settled = true;
|
|
1197
1842
|
cancelAction();
|
|
1198
|
-
reject(
|
|
1843
|
+
reject(
|
|
1844
|
+
new Error(
|
|
1845
|
+
"Ledger transport unresponsive: no DMK state change within the watchdog window. The device may have lost connection."
|
|
1846
|
+
)
|
|
1847
|
+
);
|
|
1199
1848
|
}
|
|
1200
1849
|
}, timeoutMs);
|
|
1201
1850
|
}
|
|
1202
1851
|
};
|
|
1203
|
-
|
|
1852
|
+
armIdleWatchdog();
|
|
1204
1853
|
if (onRegisterCanceller) {
|
|
1205
|
-
onRegisterCanceller(() => {
|
|
1206
|
-
if (settled)
|
|
1854
|
+
onRegisterCanceller((reason) => {
|
|
1855
|
+
if (settled) {
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1207
1858
|
settled = true;
|
|
1208
1859
|
if (timer) clearTimeout(timer);
|
|
1209
1860
|
cancelAction();
|
|
1210
|
-
|
|
1861
|
+
const err = new Error(reason?.message ?? "Device action cancelled");
|
|
1862
|
+
if (reason?.code !== void 0) {
|
|
1863
|
+
err.code = reason.code;
|
|
1864
|
+
}
|
|
1865
|
+
if (reason?.tag) {
|
|
1866
|
+
Object.defineProperty(err, "_tag", {
|
|
1867
|
+
value: reason.tag,
|
|
1868
|
+
configurable: true,
|
|
1869
|
+
enumerable: false,
|
|
1870
|
+
writable: true
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
reject(err);
|
|
1211
1874
|
});
|
|
1212
1875
|
}
|
|
1213
|
-
debugLog("[DMK-Observable] subscribing to action.observable...");
|
|
1214
1876
|
sub = action.observable.subscribe({
|
|
1215
1877
|
next: (state) => {
|
|
1216
|
-
if (settled)
|
|
1217
|
-
|
|
1878
|
+
if (settled) {
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
armIdleWatchdog();
|
|
1218
1882
|
const step = state?.intermediateValue?.step;
|
|
1219
|
-
if (step)
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
hasOutput: state.status === DeviceActionStatus.Completed,
|
|
1226
|
-
hasError: state.status === DeviceActionStatus.Error
|
|
1227
|
-
})
|
|
1228
|
-
);
|
|
1883
|
+
if (step) {
|
|
1884
|
+
lastStep = step;
|
|
1885
|
+
if (observedSteps[observedSteps.length - 1] !== step) {
|
|
1886
|
+
observedSteps.push(step);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1229
1889
|
if (state.status === DeviceActionStatus.Completed) {
|
|
1230
1890
|
settled = true;
|
|
1231
1891
|
if (timer) clearTimeout(timer);
|
|
@@ -1237,10 +1897,14 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1237
1897
|
if (timer) clearTimeout(timer);
|
|
1238
1898
|
onInteraction?.("interaction-complete");
|
|
1239
1899
|
sub?.unsubscribe();
|
|
1240
|
-
|
|
1900
|
+
rejectWithStepContext(state.error, lastStep, observedSteps, reject);
|
|
1241
1901
|
} else if (state.status === DeviceActionStatus.Pending && onInteraction) {
|
|
1242
1902
|
const interaction = state.intermediateValue?.requiredUserInteraction;
|
|
1243
1903
|
if (interaction && interaction !== "none") {
|
|
1904
|
+
if (interaction === "unlock-device" && timer) {
|
|
1905
|
+
clearTimeout(timer);
|
|
1906
|
+
timer = null;
|
|
1907
|
+
}
|
|
1244
1908
|
onInteraction(String(interaction));
|
|
1245
1909
|
}
|
|
1246
1910
|
}
|
|
@@ -1250,7 +1914,7 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1250
1914
|
settled = true;
|
|
1251
1915
|
if (timer) clearTimeout(timer);
|
|
1252
1916
|
sub?.unsubscribe();
|
|
1253
|
-
|
|
1917
|
+
rejectWithStepContext(err, lastStep, observedSteps, reject);
|
|
1254
1918
|
}
|
|
1255
1919
|
},
|
|
1256
1920
|
complete: () => {
|
|
@@ -1263,15 +1927,25 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1263
1927
|
});
|
|
1264
1928
|
});
|
|
1265
1929
|
}
|
|
1266
|
-
function
|
|
1267
|
-
if (err && typeof err === "object" && lastStep) {
|
|
1930
|
+
function rejectWithStepContext(err, lastStep, observedSteps, reject) {
|
|
1931
|
+
if (err && typeof err === "object" && (lastStep || observedSteps.length > 0)) {
|
|
1268
1932
|
try {
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1933
|
+
if (lastStep) {
|
|
1934
|
+
Object.defineProperty(err, "_lastStep", {
|
|
1935
|
+
value: lastStep,
|
|
1936
|
+
configurable: true,
|
|
1937
|
+
enumerable: false,
|
|
1938
|
+
writable: true
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
if (observedSteps.length > 0) {
|
|
1942
|
+
Object.defineProperty(err, "_deviceActionSteps", {
|
|
1943
|
+
value: [...observedSteps],
|
|
1944
|
+
configurable: true,
|
|
1945
|
+
enumerable: false,
|
|
1946
|
+
writable: true
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1275
1949
|
} catch {
|
|
1276
1950
|
}
|
|
1277
1951
|
}
|
|
@@ -1337,7 +2011,8 @@ var SignerManager = class _SignerManager {
|
|
|
1337
2011
|
async getOrCreate(sessionId) {
|
|
1338
2012
|
debugLog("[DMK] SignerManager.getOrCreate:", { sessionId });
|
|
1339
2013
|
const builder = await this._builderFn({ dmk: this._dmk, sessionId });
|
|
1340
|
-
|
|
2014
|
+
const builderWithContext = builder.withContextModule ? builder.withContextModule(_SignerManager._createContextModule()) : builder;
|
|
2015
|
+
return new SignerEth(builderWithContext.build());
|
|
1341
2016
|
}
|
|
1342
2017
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
|
|
1343
2018
|
invalidate(_sessionId) {
|
|
@@ -1346,10 +2021,24 @@ var SignerManager = class _SignerManager {
|
|
|
1346
2021
|
clearAll() {
|
|
1347
2022
|
}
|
|
1348
2023
|
static _defaultBuilder() {
|
|
1349
|
-
return (args) =>
|
|
1350
|
-
|
|
1351
|
-
|
|
2024
|
+
return (args) => new SignerEthBuilder(args);
|
|
2025
|
+
}
|
|
2026
|
+
static _createContextModule() {
|
|
2027
|
+
const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
|
|
2028
|
+
return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
|
|
2029
|
+
}
|
|
2030
|
+
static wrapBlindSigningReportNonBlocking(contextModule) {
|
|
2031
|
+
const report = contextModule.report.bind(contextModule);
|
|
2032
|
+
contextModule.report = async (params) => {
|
|
2033
|
+
try {
|
|
2034
|
+
void report(params).catch((err) => {
|
|
2035
|
+
debugLog("[DMK] ContextModule.report failed:", err);
|
|
2036
|
+
});
|
|
2037
|
+
} catch (err) {
|
|
2038
|
+
debugLog("[DMK] ContextModule.report failed:", err);
|
|
2039
|
+
}
|
|
1352
2040
|
};
|
|
2041
|
+
return contextModule;
|
|
1353
2042
|
}
|
|
1354
2043
|
};
|
|
1355
2044
|
|
|
@@ -1357,20 +2046,20 @@ var SignerManager = class _SignerManager {
|
|
|
1357
2046
|
import { HardwareErrorCode as HardwareErrorCode3, padHex64, stripHex } from "@onekeyfe/hwk-adapter-core";
|
|
1358
2047
|
|
|
1359
2048
|
// src/connector/chains/utils.ts
|
|
1360
|
-
import { EConnectorInteraction } from "@onekeyfe/hwk-adapter-core";
|
|
2049
|
+
import { EConnectorInteraction as EConnectorInteraction2 } from "@onekeyfe/hwk-adapter-core";
|
|
1361
2050
|
function normalizePath(path) {
|
|
1362
2051
|
return path.startsWith("m/") ? path.slice(2) : path;
|
|
1363
2052
|
}
|
|
1364
2053
|
function collapseSignerInteraction(interaction) {
|
|
1365
2054
|
switch (interaction) {
|
|
1366
2055
|
case "confirm-open-app":
|
|
1367
|
-
return
|
|
2056
|
+
return EConnectorInteraction2.ConfirmOpenApp;
|
|
1368
2057
|
case "unlock-device":
|
|
1369
|
-
return
|
|
2058
|
+
return EConnectorInteraction2.UnlockDevice;
|
|
1370
2059
|
case "interaction-complete":
|
|
1371
|
-
return
|
|
2060
|
+
return EConnectorInteraction2.InteractionComplete;
|
|
1372
2061
|
default:
|
|
1373
|
-
return
|
|
2062
|
+
return EConnectorInteraction2.ConfirmOnDevice;
|
|
1374
2063
|
}
|
|
1375
2064
|
}
|
|
1376
2065
|
|
|
@@ -1459,6 +2148,7 @@ async function _getEthSigner(ctx, sessionId) {
|
|
|
1459
2148
|
const signerManager = await ctx.getSignerManager();
|
|
1460
2149
|
const signer = await signerManager.getOrCreate(sessionId);
|
|
1461
2150
|
signer.onInteraction = (interaction) => {
|
|
2151
|
+
debugLog("[LedgerConnector] evm.onInteraction:", interaction);
|
|
1462
2152
|
ctx.emit("ui-event", {
|
|
1463
2153
|
type: collapseSignerInteraction(interaction),
|
|
1464
2154
|
payload: { sessionId }
|
|
@@ -1659,6 +2349,7 @@ async function btcSignTransaction(ctx, sessionId, params) {
|
|
|
1659
2349
|
const path = normalizePath(params.path || "84'/0'/0'");
|
|
1660
2350
|
const purpose = path.split("/")[0]?.replace(/'/g, "");
|
|
1661
2351
|
const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
|
|
2352
|
+
debugLog("[LedgerConnector] btcSignTransaction wallet:", { path, purpose, template });
|
|
1662
2353
|
const wallet = new DefaultWallet(path, template);
|
|
1663
2354
|
const signedTxHex = await btcSigner.signTransaction(wallet, params.psbt);
|
|
1664
2355
|
return { serializedTx: stripHex2(signedTxHex) };
|
|
@@ -1683,8 +2374,10 @@ async function btcSignPsbt(ctx, sessionId, params) {
|
|
|
1683
2374
|
const path = normalizePath(params.path || "84'/0'/0'");
|
|
1684
2375
|
const purpose = path.split("/")[0]?.replace(/'/g, "");
|
|
1685
2376
|
const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
|
|
2377
|
+
debugLog("[LedgerConnector] btcSignPsbt wallet:", { path, purpose, template });
|
|
1686
2378
|
const wallet = new DefaultWallet(path, template);
|
|
1687
2379
|
const signatures = await btcSigner.signPsbt(wallet, params.psbt);
|
|
2380
|
+
debugLog("[LedgerConnector] btcSignPsbt signatures received:", signatures.length);
|
|
1688
2381
|
const signedPsbtHex = _applySignaturesToPsbt(params.psbt, signatures);
|
|
1689
2382
|
return { signedPsbt: signedPsbtHex };
|
|
1690
2383
|
} catch (err) {
|
|
@@ -1731,6 +2424,7 @@ async function _createBtcSigner(ctx, sessionId) {
|
|
|
1731
2424
|
const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
|
|
1732
2425
|
const signer = new SignerBtc(sdkSigner);
|
|
1733
2426
|
signer.onInteraction = (interaction) => {
|
|
2427
|
+
debugLog("[LedgerConnector] btc.onInteraction:", interaction);
|
|
1734
2428
|
ctx.emit("ui-event", {
|
|
1735
2429
|
type: collapseSignerInteraction(interaction),
|
|
1736
2430
|
payload: { sessionId }
|
|
@@ -1901,6 +2595,7 @@ async function _createSolSigner(ctx, sessionId) {
|
|
|
1901
2595
|
const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
|
|
1902
2596
|
const signer = new SignerSol(sdkSigner);
|
|
1903
2597
|
signer.onInteraction = (interaction) => {
|
|
2598
|
+
debugLog("[LedgerConnector] sol.onInteraction:", interaction);
|
|
1904
2599
|
ctx.emit("ui-event", {
|
|
1905
2600
|
type: collapseSignerInteraction(interaction),
|
|
1906
2601
|
payload: { sessionId }
|
|
@@ -1913,11 +2608,11 @@ async function _createSolSigner(ctx, sessionId) {
|
|
|
1913
2608
|
}
|
|
1914
2609
|
|
|
1915
2610
|
// src/connector/chains/tron.ts
|
|
1916
|
-
import {
|
|
2611
|
+
import { HardwareErrorCode as HardwareErrorCode6 } from "@onekeyfe/hwk-adapter-core";
|
|
1917
2612
|
import Trx from "@ledgerhq/hw-app-trx";
|
|
1918
2613
|
|
|
1919
|
-
// src/connector/chains/
|
|
1920
|
-
import { EConnectorInteraction as
|
|
2614
|
+
// src/connector/chains/legacyChainCall.ts
|
|
2615
|
+
import { EConnectorInteraction as EConnectorInteraction3 } from "@onekeyfe/hwk-adapter-core";
|
|
1921
2616
|
|
|
1922
2617
|
// src/app/AppManager.ts
|
|
1923
2618
|
import {
|
|
@@ -1926,6 +2621,7 @@ import {
|
|
|
1926
2621
|
OpenAppCommand,
|
|
1927
2622
|
isSuccessCommandResult
|
|
1928
2623
|
} from "@ledgerhq/device-management-kit";
|
|
2624
|
+
import { HardwareErrorCode as HardwareErrorCode5 } from "@onekeyfe/hwk-adapter-core";
|
|
1929
2625
|
var APP_NAME_MAP = {
|
|
1930
2626
|
ETH: "Ethereum",
|
|
1931
2627
|
BTC: "Bitcoin",
|
|
@@ -1961,9 +2657,16 @@ var AppManager = class {
|
|
|
1961
2657
|
* 5. Poll until the device confirms the target app is running.
|
|
1962
2658
|
*/
|
|
1963
2659
|
/**
|
|
1964
|
-
* @param onConfirmOnDevice Called
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
2660
|
+
* @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued — the
|
|
2661
|
+
* device is about to display "Open <app>" on screen and wait for the
|
|
2662
|
+
* user's button press. UI consumers should show their "open app" prompt
|
|
2663
|
+
* in response. NOT called when the target app is already open (no user
|
|
2664
|
+
* interaction needed in that case).
|
|
2665
|
+
*
|
|
2666
|
+
* Important: OpenAppCommand is blocking. It does not resolve until the user
|
|
2667
|
+
* has physically confirmed on the device, so anything that runs AFTER
|
|
2668
|
+
* `await this._openApp(...)` lands AFTER the prompt is already gone.
|
|
2669
|
+
* Hence the callback must fire BEFORE that await.
|
|
1967
2670
|
*/
|
|
1968
2671
|
async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
|
|
1969
2672
|
const currentApp = await this._getCurrentApp(sessionId);
|
|
@@ -1974,8 +2677,8 @@ var AppManager = class {
|
|
|
1974
2677
|
await this._closeCurrentApp(sessionId);
|
|
1975
2678
|
await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
|
|
1976
2679
|
}
|
|
1977
|
-
await this._openApp(sessionId, targetAppName);
|
|
1978
2680
|
onConfirmOnDevice?.();
|
|
2681
|
+
await this._openApp(sessionId, targetAppName);
|
|
1979
2682
|
await this._waitForApp(sessionId, targetAppName);
|
|
1980
2683
|
}
|
|
1981
2684
|
// ---------------------------------------------------------------------------
|
|
@@ -1987,9 +2690,34 @@ var AppManager = class {
|
|
|
1987
2690
|
command: new GetAppAndVersionCommand()
|
|
1988
2691
|
});
|
|
1989
2692
|
if (isSuccessCommandResult(result)) {
|
|
2693
|
+
debugLog("[AppManager] currentApp:", result.data.name);
|
|
1990
2694
|
return result.data.name;
|
|
1991
2695
|
}
|
|
1992
|
-
|
|
2696
|
+
const errResult = result;
|
|
2697
|
+
const dmkErr = errResult.error ?? {};
|
|
2698
|
+
const original = dmkErr.originalError;
|
|
2699
|
+
debugLog(
|
|
2700
|
+
"[AppManager] _getCurrentApp failed sessionId=",
|
|
2701
|
+
sessionId,
|
|
2702
|
+
"tag=",
|
|
2703
|
+
dmkErr._tag,
|
|
2704
|
+
"errorCode=",
|
|
2705
|
+
dmkErr.errorCode,
|
|
2706
|
+
"message=",
|
|
2707
|
+
dmkErr.message,
|
|
2708
|
+
"originalErrorMessage=",
|
|
2709
|
+
original?.message ?? String(original ?? "")
|
|
2710
|
+
);
|
|
2711
|
+
throw Object.assign(
|
|
2712
|
+
new Error(
|
|
2713
|
+
dmkErr.message ?? "Failed to get current app from device"
|
|
2714
|
+
),
|
|
2715
|
+
{
|
|
2716
|
+
_tag: dmkErr._tag,
|
|
2717
|
+
errorCode: dmkErr.errorCode,
|
|
2718
|
+
originalError: original
|
|
2719
|
+
}
|
|
2720
|
+
);
|
|
1993
2721
|
}
|
|
1994
2722
|
async _openApp(sessionId, appName) {
|
|
1995
2723
|
const result = await this._dmk.sendCommand({
|
|
@@ -1998,8 +2726,11 @@ var AppManager = class {
|
|
|
1998
2726
|
});
|
|
1999
2727
|
if (!isSuccessCommandResult(result)) {
|
|
2000
2728
|
const { statusCode } = result;
|
|
2729
|
+
const hasStatusCode2 = statusCode != null && statusCode !== "";
|
|
2730
|
+
debugLog("[AppManager] openApp failed:", appName, "statusCode:", statusCode);
|
|
2001
2731
|
throw Object.assign(new Error(`Failed to open "${appName}"`), {
|
|
2002
|
-
_tag:
|
|
2732
|
+
_tag: ERROR_TAG.OpenAppCommand,
|
|
2733
|
+
code: hasStatusCode2 ? void 0 : HardwareErrorCode5.AppNotInstalled,
|
|
2003
2734
|
errorCode: String(statusCode ?? ""),
|
|
2004
2735
|
statusCode,
|
|
2005
2736
|
appName
|
|
@@ -2007,6 +2738,7 @@ var AppManager = class {
|
|
|
2007
2738
|
}
|
|
2008
2739
|
}
|
|
2009
2740
|
async _closeCurrentApp(sessionId) {
|
|
2741
|
+
debugLog("[AppManager] closeCurrentApp");
|
|
2010
2742
|
await this._dmk.sendCommand({
|
|
2011
2743
|
sessionId,
|
|
2012
2744
|
command: new CloseAppCommand()
|
|
@@ -2017,15 +2749,23 @@ var AppManager = class {
|
|
|
2017
2749
|
* or throw after `_maxRetries` attempts.
|
|
2018
2750
|
*/
|
|
2019
2751
|
async _waitForApp(sessionId, expectedAppName) {
|
|
2752
|
+
let lastSeen = "";
|
|
2020
2753
|
for (let i = 0; i < this._maxRetries; i++) {
|
|
2021
2754
|
await this._wait();
|
|
2022
2755
|
const current = await this._getCurrentApp(sessionId);
|
|
2756
|
+
lastSeen = current;
|
|
2023
2757
|
if (current === expectedAppName) {
|
|
2024
2758
|
return;
|
|
2025
2759
|
}
|
|
2026
2760
|
}
|
|
2761
|
+
debugLog(
|
|
2762
|
+
"[AppManager] waitForApp exhausted: expected=",
|
|
2763
|
+
expectedAppName,
|
|
2764
|
+
"lastSeen=",
|
|
2765
|
+
lastSeen
|
|
2766
|
+
);
|
|
2027
2767
|
throw new Error(
|
|
2028
|
-
`Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries`
|
|
2768
|
+
`Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
|
|
2029
2769
|
);
|
|
2030
2770
|
}
|
|
2031
2771
|
_isDashboard(appName) {
|
|
@@ -2036,46 +2776,92 @@ var AppManager = class {
|
|
|
2036
2776
|
}
|
|
2037
2777
|
};
|
|
2038
2778
|
|
|
2039
|
-
// src/connector/chains/
|
|
2779
|
+
// src/connector/chains/legacyChainCall.ts
|
|
2040
2780
|
function isLegacyWrongAppError(err, _appName) {
|
|
2041
2781
|
return isWrongAppError(err);
|
|
2042
2782
|
}
|
|
2043
|
-
async function
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2783
|
+
async function withLegacyChainCall(ctx, sessionId, options, action) {
|
|
2784
|
+
const { appName, needsConfirmation } = options;
|
|
2785
|
+
let openAppPromptShown = false;
|
|
2786
|
+
const onAppOpenPrompt = () => {
|
|
2787
|
+
openAppPromptShown = true;
|
|
2047
2788
|
ctx.emit("ui-event", {
|
|
2048
|
-
type:
|
|
2789
|
+
type: EConnectorInteraction3.ConfirmOpenApp,
|
|
2049
2790
|
payload: { sessionId }
|
|
2050
2791
|
});
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2792
|
+
};
|
|
2793
|
+
const closeOpenAppUiIfShown = () => {
|
|
2794
|
+
if (openAppPromptShown) {
|
|
2795
|
+
ctx.emit("ui-event", {
|
|
2796
|
+
type: EConnectorInteraction3.InteractionComplete,
|
|
2797
|
+
payload: { sessionId }
|
|
2798
|
+
});
|
|
2799
|
+
openAppPromptShown = false;
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
try {
|
|
2803
|
+
await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
|
|
2804
|
+
} catch (err) {
|
|
2805
|
+
debugLog(
|
|
2806
|
+
"[LegacyChainCall] pre-flight ensureAppOpen failed:",
|
|
2807
|
+
appName,
|
|
2808
|
+
err?.message
|
|
2809
|
+
);
|
|
2810
|
+
closeOpenAppUiIfShown();
|
|
2811
|
+
throw ctx.wrapError(err, { defaultAppName: appName });
|
|
2812
|
+
}
|
|
2813
|
+
const runOnce = async () => {
|
|
2814
|
+
let confirmEmitted = false;
|
|
2815
|
+
if (needsConfirmation) {
|
|
2069
2816
|
ctx.emit("ui-event", {
|
|
2070
|
-
type:
|
|
2817
|
+
type: EConnectorInteraction3.ConfirmOnDevice,
|
|
2071
2818
|
payload: { sessionId }
|
|
2072
2819
|
});
|
|
2820
|
+
confirmEmitted = true;
|
|
2821
|
+
}
|
|
2822
|
+
try {
|
|
2073
2823
|
return await action(sessionId);
|
|
2824
|
+
} finally {
|
|
2825
|
+
if (confirmEmitted || openAppPromptShown) {
|
|
2826
|
+
ctx.emit("ui-event", {
|
|
2827
|
+
type: EConnectorInteraction3.InteractionComplete,
|
|
2828
|
+
payload: { sessionId }
|
|
2829
|
+
});
|
|
2830
|
+
openAppPromptShown = false;
|
|
2831
|
+
}
|
|
2074
2832
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2833
|
+
};
|
|
2834
|
+
try {
|
|
2835
|
+
return await runOnce();
|
|
2836
|
+
} catch (err) {
|
|
2837
|
+
if (!isLegacyWrongAppError(err, appName)) {
|
|
2838
|
+
debugLog("[LegacyChainCall] non-wrong-app failure:", appName, err?.message);
|
|
2839
|
+
ctx.invalidateSession(sessionId);
|
|
2840
|
+
throw ctx.wrapError(err, { defaultAppName: appName });
|
|
2841
|
+
}
|
|
2842
|
+
debugLog("[LegacyChainCall] wrong-app detected, retrying:", appName);
|
|
2843
|
+
try {
|
|
2844
|
+
await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
|
|
2845
|
+
} catch (switchErr) {
|
|
2846
|
+
debugLog(
|
|
2847
|
+
"[LegacyChainCall] retry ensureAppOpen failed:",
|
|
2848
|
+
appName,
|
|
2849
|
+
switchErr?.message
|
|
2850
|
+
);
|
|
2851
|
+
closeOpenAppUiIfShown();
|
|
2852
|
+
throw ctx.wrapError(switchErr, { defaultAppName: appName });
|
|
2853
|
+
}
|
|
2854
|
+
ctx.clearAllSigners();
|
|
2855
|
+
const result = await runOnce();
|
|
2856
|
+
debugLog("[LegacyChainCall] retry succeeded:", appName);
|
|
2857
|
+
return result;
|
|
2077
2858
|
}
|
|
2078
2859
|
}
|
|
2860
|
+
async function _ensureAppOpen(ctx, sessionId, appName, onPrompt) {
|
|
2861
|
+
const dmk = await ctx.getOrCreateDmk();
|
|
2862
|
+
const appManager = new AppManager(dmk);
|
|
2863
|
+
await appManager.ensureAppOpen(sessionId, appName, onPrompt);
|
|
2864
|
+
}
|
|
2079
2865
|
|
|
2080
2866
|
// src/transport/DmkTransport.ts
|
|
2081
2867
|
import Transport from "@ledgerhq/hw-transport";
|
|
@@ -2105,83 +2891,75 @@ var DmkTransport = class extends Transport {
|
|
|
2105
2891
|
// src/connector/chains/tron.ts
|
|
2106
2892
|
async function tronGetAddress(ctx, sessionId, params) {
|
|
2107
2893
|
const path = normalizePath(params.path);
|
|
2108
|
-
|
|
2109
|
-
return
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2894
|
+
const showOnDevice = params.showOnDevice ?? false;
|
|
2895
|
+
return withLegacyChainCall(
|
|
2896
|
+
ctx,
|
|
2897
|
+
sessionId,
|
|
2898
|
+
{
|
|
2899
|
+
appName: "Tron",
|
|
2900
|
+
// Only show "confirm on device" UI when the device is actually going
|
|
2901
|
+
// to display the address for the user to verify.
|
|
2902
|
+
needsConfirmation: showOnDevice
|
|
2903
|
+
},
|
|
2904
|
+
async (sid) => {
|
|
2905
|
+
const trx = await _createTrx(ctx, sid);
|
|
2906
|
+
const result = await trx.getAddress(path, showOnDevice);
|
|
2907
|
+
return { address: result.address, publicKey: result.publicKey, path: params.path };
|
|
2116
2908
|
}
|
|
2117
|
-
|
|
2118
|
-
ctx.emit("ui-event", {
|
|
2119
|
-
type: EConnectorInteraction3.InteractionComplete,
|
|
2120
|
-
payload: { sessionId: sid }
|
|
2121
|
-
});
|
|
2122
|
-
return { address: result.address, publicKey: result.publicKey, path: params.path };
|
|
2123
|
-
});
|
|
2909
|
+
);
|
|
2124
2910
|
}
|
|
2125
2911
|
async function tronSignTransaction(ctx, sessionId, params) {
|
|
2126
2912
|
if (!params.rawTxHex) {
|
|
2127
2913
|
throw Object.assign(
|
|
2128
2914
|
new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
|
|
2129
|
-
{ code:
|
|
2915
|
+
{ code: HardwareErrorCode6.InvalidParams }
|
|
2130
2916
|
);
|
|
2131
2917
|
}
|
|
2132
2918
|
const path = normalizePath(params.path);
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
payload: { sessionId: sid }
|
|
2148
|
-
});
|
|
2149
|
-
return { signature };
|
|
2150
|
-
});
|
|
2919
|
+
return withLegacyChainCall(
|
|
2920
|
+
ctx,
|
|
2921
|
+
sessionId,
|
|
2922
|
+
{ appName: "Tron", needsConfirmation: true },
|
|
2923
|
+
async (sid) => {
|
|
2924
|
+
const trx = await _createTrx(ctx, sid);
|
|
2925
|
+
const signature = await trx.signTransaction(
|
|
2926
|
+
path,
|
|
2927
|
+
params.rawTxHex,
|
|
2928
|
+
params.tokenSignatures ?? []
|
|
2929
|
+
);
|
|
2930
|
+
return { signature };
|
|
2931
|
+
}
|
|
2932
|
+
);
|
|
2151
2933
|
}
|
|
2152
2934
|
async function tronSignMessage(ctx, sessionId, params) {
|
|
2153
2935
|
const path = normalizePath(params.path);
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
payload: { sessionId: sid }
|
|
2165
|
-
});
|
|
2166
|
-
return { signature };
|
|
2167
|
-
});
|
|
2936
|
+
return withLegacyChainCall(
|
|
2937
|
+
ctx,
|
|
2938
|
+
sessionId,
|
|
2939
|
+
{ appName: "Tron", needsConfirmation: true },
|
|
2940
|
+
async (sid) => {
|
|
2941
|
+
const trx = await _createTrx(ctx, sid);
|
|
2942
|
+
const signature = await trx.signPersonalMessage(path, params.messageHex);
|
|
2943
|
+
return { signature };
|
|
2944
|
+
}
|
|
2945
|
+
);
|
|
2168
2946
|
}
|
|
2169
2947
|
async function _createTrx(ctx, sessionId) {
|
|
2170
2948
|
const dmk = await ctx.getOrCreateDmk();
|
|
2171
2949
|
return new Trx(new DmkTransport(dmk, sessionId));
|
|
2172
2950
|
}
|
|
2173
|
-
async function _ensureTronAppOpen(ctx, sessionId) {
|
|
2174
|
-
const dmk = await ctx.getOrCreateDmk();
|
|
2175
|
-
const appManager = new AppManager(dmk);
|
|
2176
|
-
await appManager.ensureAppOpen(sessionId, "Tron", () => {
|
|
2177
|
-
ctx.emit("ui-event", {
|
|
2178
|
-
type: EConnectorInteraction3.ConfirmOpenApp,
|
|
2179
|
-
payload: { sessionId }
|
|
2180
|
-
});
|
|
2181
|
-
});
|
|
2182
|
-
}
|
|
2183
2951
|
|
|
2184
2952
|
// src/connector/LedgerConnectorBase.ts
|
|
2953
|
+
var METHOD_PREFIX_TO_APP_NAME = {
|
|
2954
|
+
evm: "Ethereum",
|
|
2955
|
+
btc: "Bitcoin",
|
|
2956
|
+
sol: "Solana",
|
|
2957
|
+
tron: "Tron"
|
|
2958
|
+
};
|
|
2959
|
+
var HARDWARE_ERROR_CODE_VALUES = new Set(
|
|
2960
|
+
Object.values(HardwareErrorCode7).filter((value) => typeof value === "number")
|
|
2961
|
+
);
|
|
2962
|
+
var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
2185
2963
|
async function defaultLedgerKitImporter(pkg) {
|
|
2186
2964
|
switch (pkg) {
|
|
2187
2965
|
case "@ledgerhq/device-management-kit":
|
|
@@ -2205,16 +2983,6 @@ var LedgerConnectorBase = class {
|
|
|
2205
2983
|
this._dmk = null;
|
|
2206
2984
|
this._eventHandlers = /* @__PURE__ */ new Map();
|
|
2207
2985
|
// ---------------------------------------------------------------------------
|
|
2208
|
-
// ConnectId <-> DMK path mapping
|
|
2209
|
-
//
|
|
2210
|
-
// DMK uses internal paths (BLE MAC, USB UUID) that may change across sessions.
|
|
2211
|
-
// _resolveConnectId() maps these to stable external IDs (BLE: "A58F", USB: same).
|
|
2212
|
-
// This bidirectional map is the SINGLE SOURCE OF TRUTH for all connectId usage.
|
|
2213
|
-
// ---------------------------------------------------------------------------
|
|
2214
|
-
this._connectIdToPath = /* @__PURE__ */ new Map();
|
|
2215
|
-
// "A58F" -> "D5:75:7D:4B:51:E8"
|
|
2216
|
-
this._pathToConnectId = /* @__PURE__ */ new Map();
|
|
2217
|
-
// ---------------------------------------------------------------------------
|
|
2218
2986
|
// Per-session DeviceAction cancellers
|
|
2219
2987
|
//
|
|
2220
2988
|
// Each chain handler registers its active DeviceAction's canceller via
|
|
@@ -2223,17 +2991,30 @@ var LedgerConnectorBase = class {
|
|
|
2223
2991
|
// unsubscribes the observable and releases DMK's IntentQueue slot.
|
|
2224
2992
|
// ---------------------------------------------------------------------------
|
|
2225
2993
|
this._cancellers = /* @__PURE__ */ new Map();
|
|
2994
|
+
// ---------------------------------------------------------------------------
|
|
2995
|
+
// Per-session DMK state subscriptions
|
|
2996
|
+
//
|
|
2997
|
+
// DMK only emits `device-disconnect` on the connector when `disconnect()`
|
|
2998
|
+
// is called explicitly. To pick up autonomous disconnects (USB unplug,
|
|
2999
|
+
// BLE drop, device sleep, DMK transport reset) we subscribe to
|
|
3000
|
+
// `_dmk.getDeviceSessionState({sessionId})` per active session and emit
|
|
3001
|
+
// `device-disconnect` ourselves the moment DMK reports the device went
|
|
3002
|
+
// to NOT_CONNECTED. The subscription is torn down on disconnect /
|
|
3003
|
+
// reset / observable completion to avoid leaks.
|
|
3004
|
+
// ---------------------------------------------------------------------------
|
|
3005
|
+
this._sessionStateSubs = /* @__PURE__ */ new Map();
|
|
2226
3006
|
this._createTransport = createTransport;
|
|
2227
3007
|
this.connectionType = options?.connectionType ?? "usb";
|
|
2228
3008
|
this._providedDmk = options?.dmk;
|
|
2229
3009
|
this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
|
|
3010
|
+
this._requirePreFlightScan = options?.requirePreFlightScan ?? false;
|
|
2230
3011
|
if (this._providedDmk) {
|
|
2231
3012
|
this._initManagers(this._providedDmk);
|
|
2232
3013
|
}
|
|
2233
3014
|
this._ctx = {
|
|
2234
3015
|
emit: (event, data) => this._emit(event, data),
|
|
2235
3016
|
invalidateSession: (sid) => this._invalidateSession(sid),
|
|
2236
|
-
wrapError: (err) => this._wrapError(err),
|
|
3017
|
+
wrapError: (err, opts) => this._wrapError(err, opts),
|
|
2237
3018
|
getOrCreateDmk: () => this._getOrCreateDmk(),
|
|
2238
3019
|
getDeviceManager: () => this._getDeviceManager(),
|
|
2239
3020
|
getSignerManager: () => this._getSignerManager(),
|
|
@@ -2244,38 +3025,23 @@ var LedgerConnectorBase = class {
|
|
|
2244
3025
|
importLedgerKit: this._importLedgerKit
|
|
2245
3026
|
};
|
|
2246
3027
|
}
|
|
2247
|
-
// "D5:75:7D:4B:51:E8" -> "A58F"
|
|
2248
|
-
/** Register a connectId <-> path mapping from a device descriptor. */
|
|
2249
|
-
_registerDeviceId(descriptor) {
|
|
2250
|
-
const connectId = this._resolveConnectId(descriptor);
|
|
2251
|
-
this._connectIdToPath.set(connectId, descriptor.path);
|
|
2252
|
-
this._pathToConnectId.set(descriptor.path, connectId);
|
|
2253
|
-
return connectId;
|
|
2254
|
-
}
|
|
2255
|
-
/** Get DMK path from external connectId. Falls back to connectId itself. */
|
|
2256
|
-
_getPathForConnectId(connectId) {
|
|
2257
|
-
return this._connectIdToPath.get(connectId) ?? connectId;
|
|
2258
|
-
}
|
|
2259
|
-
/** Get external connectId from DMK path. Falls back to path itself. */
|
|
2260
|
-
_getConnectIdForPath(path) {
|
|
2261
|
-
return this._pathToConnectId.get(path) ?? path;
|
|
2262
|
-
}
|
|
2263
3028
|
// ---------------------------------------------------------------------------
|
|
2264
3029
|
// Protected — hooks for subclasses
|
|
2265
3030
|
// ---------------------------------------------------------------------------
|
|
2266
3031
|
/**
|
|
2267
3032
|
* Resolve the connectId for a discovered device descriptor.
|
|
2268
3033
|
* Default: use the DMK path (ephemeral UUID).
|
|
2269
|
-
* Override in subclasses
|
|
3034
|
+
* Override in subclasses only when the public connectId differs from the transport path.
|
|
2270
3035
|
*/
|
|
2271
3036
|
_resolveConnectId(descriptor) {
|
|
2272
3037
|
return descriptor.path;
|
|
2273
3038
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
3039
|
+
/**
|
|
3040
|
+
* Authoritative discovery for this transport. Default = enumerate snapshot
|
|
3041
|
+
* (USB/HID). BLE overrides to drive from a live scan window since DMK's
|
|
3042
|
+
* enumerate() cache survives peripherals going offline.
|
|
3043
|
+
*/
|
|
3044
|
+
async _discoverDescriptors(dm) {
|
|
2279
3045
|
let descriptors = await dm.enumerate();
|
|
2280
3046
|
if (descriptors.length === 0) {
|
|
2281
3047
|
try {
|
|
@@ -2284,137 +3050,331 @@ var LedgerConnectorBase = class {
|
|
|
2284
3050
|
}
|
|
2285
3051
|
descriptors = await dm.enumerate();
|
|
2286
3052
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
};
|
|
2295
|
-
|
|
3053
|
+
return descriptors;
|
|
3054
|
+
}
|
|
3055
|
+
_assertSingleUsbDescriptor(descriptors) {
|
|
3056
|
+
if (isLedgerBleConnectionType(this.connectionType) || descriptors.length <= 1) {
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
debugLog(
|
|
3060
|
+
`[DMK] Multiple Ledger USB devices found (${descriptors.length}); refusing to choose by ephemeral path. paths=[${descriptors.map((d) => d.path).join(",")}]`
|
|
3061
|
+
);
|
|
3062
|
+
throw createMultipleUsbLedgerDevicesError();
|
|
3063
|
+
}
|
|
3064
|
+
// ---------------------------------------------------------------------------
|
|
3065
|
+
// IConnector -- Device discovery
|
|
3066
|
+
// ---------------------------------------------------------------------------
|
|
3067
|
+
async searchDevices() {
|
|
3068
|
+
const dm = await this._getDeviceManager();
|
|
3069
|
+
const descriptors = await this._discoverDescriptors(dm);
|
|
3070
|
+
this._assertSingleUsbDescriptor(descriptors);
|
|
3071
|
+
const resolvedDescriptors = descriptors.map((d) => ({
|
|
3072
|
+
descriptor: d,
|
|
3073
|
+
connectId: this._resolveConnectId(d)
|
|
3074
|
+
}));
|
|
3075
|
+
const result = resolvedDescriptors.map(({ descriptor: d, connectId }) => ({
|
|
3076
|
+
connectId,
|
|
3077
|
+
deviceId: d.path,
|
|
3078
|
+
name: d.name || d.type || "Ledger",
|
|
3079
|
+
model: d.type,
|
|
3080
|
+
modelName: d.modelName,
|
|
3081
|
+
rssi: d.rssi,
|
|
3082
|
+
isConnectable: d.isConnectable,
|
|
3083
|
+
serialNumber: d.serialNumber
|
|
3084
|
+
}));
|
|
3085
|
+
debugLog(
|
|
3086
|
+
`[DMK] connector.searchDevices() return count=${result.length} ids=[${result.map((r) => r.connectId).join(",")}] paths=[${result.map((r) => r.deviceId).join(",")}]`
|
|
3087
|
+
);
|
|
2296
3088
|
return result;
|
|
2297
3089
|
}
|
|
2298
3090
|
// ---------------------------------------------------------------------------
|
|
2299
3091
|
// IConnector -- Connection
|
|
2300
3092
|
// ---------------------------------------------------------------------------
|
|
2301
3093
|
async connect(deviceId) {
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
3094
|
+
const callerSuppliedConnectId = Boolean(deviceId);
|
|
3095
|
+
let targetPath = deviceId;
|
|
3096
|
+
if (callerSuppliedConnectId && !isLedgerBleConnectionType(this.connectionType)) {
|
|
3097
|
+
const dm = await this._getDeviceManager();
|
|
3098
|
+
this._assertSingleUsbDescriptor(await this._discoverDescriptors(dm));
|
|
3099
|
+
}
|
|
2306
3100
|
if (!targetPath) {
|
|
2307
|
-
const
|
|
2308
|
-
if (
|
|
3101
|
+
const discovered = await this.searchDevices();
|
|
3102
|
+
if (discovered.length === 0) {
|
|
2309
3103
|
throw new Error(
|
|
2310
|
-
`No Ledger device found. Make sure the device is connected${this.connectionType
|
|
3104
|
+
`No Ledger device found. Make sure the device is connected${isLedgerBleConnectionType(this.connectionType) ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
|
|
2311
3105
|
);
|
|
2312
3106
|
}
|
|
2313
|
-
targetPath =
|
|
3107
|
+
targetPath = discovered[0].deviceId;
|
|
2314
3108
|
}
|
|
2315
|
-
const externalConnectId =
|
|
3109
|
+
const externalConnectId = targetPath;
|
|
3110
|
+
const HANG_CEILING_MS = 5 * 6e4;
|
|
3111
|
+
const dmConnectWithObserve = async (dm, path) => {
|
|
3112
|
+
if (!isLedgerBleConnectionType(this.connectionType)) return dm.connect(path);
|
|
3113
|
+
let timeoutId;
|
|
3114
|
+
let timedOut = false;
|
|
3115
|
+
const connectPromise = dm.connect(path);
|
|
3116
|
+
const hangPromise = new Promise((_, reject) => {
|
|
3117
|
+
timeoutId = setTimeout(() => {
|
|
3118
|
+
timedOut = true;
|
|
3119
|
+
const err = new Error(
|
|
3120
|
+
`Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
|
|
3121
|
+
);
|
|
3122
|
+
err._tag = ERROR_TAG.BlePairingTimeout;
|
|
3123
|
+
err.code = HardwareErrorCode7.BlePairingTimeout;
|
|
3124
|
+
reject(err);
|
|
3125
|
+
}, HANG_CEILING_MS);
|
|
3126
|
+
});
|
|
3127
|
+
try {
|
|
3128
|
+
return await Promise.race([connectPromise, hangPromise]);
|
|
3129
|
+
} catch (err) {
|
|
3130
|
+
const e = err;
|
|
3131
|
+
debugLog("[DMK] dm.connect rejected \u2014 observed:", {
|
|
3132
|
+
_tag: e?._tag,
|
|
3133
|
+
message: e?.message,
|
|
3134
|
+
errorCode: e?.errorCode,
|
|
3135
|
+
statusCode: e?.statusCode,
|
|
3136
|
+
originalTag: e?.originalError?._tag,
|
|
3137
|
+
originalMessage: e?.originalError?.message
|
|
3138
|
+
});
|
|
3139
|
+
if (timedOut) {
|
|
3140
|
+
void connectPromise.then(
|
|
3141
|
+
(sessionId) => dm.disconnect(sessionId).catch(() => void 0),
|
|
3142
|
+
() => void 0
|
|
3143
|
+
);
|
|
3144
|
+
}
|
|
3145
|
+
throw err;
|
|
3146
|
+
} finally {
|
|
3147
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
3148
|
+
}
|
|
3149
|
+
};
|
|
3150
|
+
const isBleDirectConnect = isLedgerBleConnectionType(this.connectionType) && callerSuppliedConnectId;
|
|
3151
|
+
const throwNotAdvertising = () => {
|
|
3152
|
+
const err = new Error(
|
|
3153
|
+
"Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
|
|
3154
|
+
);
|
|
3155
|
+
err._tag = ERROR_TAG.DeviceNotAdvertising;
|
|
3156
|
+
err.code = HardwareErrorCode7.DeviceNotFound;
|
|
3157
|
+
throw err;
|
|
3158
|
+
};
|
|
2316
3159
|
const doConnect = async (path) => {
|
|
2317
|
-
const
|
|
3160
|
+
const dm = await this._getDeviceManager();
|
|
3161
|
+
if (isBleDirectConnect && !dm.hasDiscoveredDevice(path)) {
|
|
3162
|
+
const live = await dm.getLiveDevices(BLE_CONNECT_SCAN_TIMEOUT_MS);
|
|
3163
|
+
if (this._requirePreFlightScan && !live.some((d) => d.id === path)) {
|
|
3164
|
+
throwNotAdvertising();
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
let sessionId;
|
|
3168
|
+
try {
|
|
3169
|
+
sessionId = await dmConnectWithObserve(dm, path);
|
|
3170
|
+
} catch (err) {
|
|
3171
|
+
if (isBleDirectConnect && err?._tag === ERROR_TAG.DeviceNotInDiscoveryCache) {
|
|
3172
|
+
throwNotAdvertising();
|
|
3173
|
+
}
|
|
3174
|
+
throw err;
|
|
3175
|
+
}
|
|
3176
|
+
try {
|
|
3177
|
+
this._watchSessionState(sessionId, externalConnectId);
|
|
3178
|
+
} catch (subErr) {
|
|
3179
|
+
debugLog("[DMK] state subscription failed during connect; disconnecting session:", subErr);
|
|
3180
|
+
try {
|
|
3181
|
+
await dm.disconnect(sessionId);
|
|
3182
|
+
} catch {
|
|
3183
|
+
}
|
|
3184
|
+
throw subErr;
|
|
3185
|
+
}
|
|
3186
|
+
const info = dm.getDiscoveredDeviceInfo(path);
|
|
2318
3187
|
const session = {
|
|
2319
3188
|
sessionId,
|
|
2320
3189
|
deviceInfo: {
|
|
2321
3190
|
vendor: "ledger",
|
|
2322
|
-
model: "unknown",
|
|
3191
|
+
model: info?.model ?? "unknown",
|
|
3192
|
+
modelName: info?.modelName,
|
|
2323
3193
|
firmwareVersion: "unknown",
|
|
2324
3194
|
deviceId: path,
|
|
2325
3195
|
connectId: externalConnectId,
|
|
2326
3196
|
connectionType: this.connectionType,
|
|
3197
|
+
rssi: info?.rssi,
|
|
2327
3198
|
capabilities: { persistentDeviceIdentity: false }
|
|
2328
3199
|
}
|
|
2329
3200
|
};
|
|
3201
|
+
const name = dm.getDeviceName(path) ?? "Ledger";
|
|
2330
3202
|
this._emit("device-connect", {
|
|
2331
|
-
device: {
|
|
2332
|
-
connectId: externalConnectId,
|
|
2333
|
-
deviceId: path,
|
|
2334
|
-
name: "Ledger"
|
|
2335
|
-
}
|
|
3203
|
+
device: { connectId: externalConnectId, deviceId: path, name }
|
|
2336
3204
|
});
|
|
2337
3205
|
return session;
|
|
2338
3206
|
};
|
|
2339
3207
|
try {
|
|
2340
3208
|
return await doConnect(targetPath);
|
|
2341
|
-
} catch {
|
|
3209
|
+
} catch (err) {
|
|
2342
3210
|
this._resetSignersAndSessions();
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
const descriptors = await dm2.enumerate();
|
|
2348
|
-
if (descriptors.length === 0) {
|
|
2349
|
-
throw new Error(
|
|
2350
|
-
`No Ledger device found after retry. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
|
|
2351
|
-
);
|
|
3211
|
+
if (isLedgerBleConnectionType(this.connectionType)) {
|
|
3212
|
+
const tag = err?._tag;
|
|
3213
|
+
if (isKnownConnectionTag(tag)) {
|
|
3214
|
+
throw err;
|
|
2352
3215
|
}
|
|
2353
|
-
|
|
3216
|
+
const wrapped = new Error(
|
|
3217
|
+
"Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
|
|
3218
|
+
);
|
|
3219
|
+
wrapped._tag = ERROR_TAG.BleGattBondingFailed;
|
|
3220
|
+
wrapped.code = HardwareErrorCode7.BlePairingTimeout;
|
|
3221
|
+
wrapped.originalError = err;
|
|
3222
|
+
throw wrapped;
|
|
3223
|
+
}
|
|
3224
|
+
if (err && typeof err === "object" && err._tag) {
|
|
3225
|
+
const taggedError = err instanceof Error ? err : Object.assign(
|
|
3226
|
+
new Error(err.message ?? "Ledger device error"),
|
|
3227
|
+
err
|
|
3228
|
+
);
|
|
3229
|
+
throw taggedError;
|
|
2354
3230
|
}
|
|
2355
|
-
|
|
3231
|
+
throw new Error("Ledger device appears unplugged. Plug in the device and try again.");
|
|
2356
3232
|
}
|
|
2357
3233
|
}
|
|
2358
3234
|
async disconnect(sessionId) {
|
|
2359
3235
|
if (!this._deviceManager) return;
|
|
2360
3236
|
const deviceId = this._deviceManager.getDeviceId(sessionId);
|
|
2361
3237
|
this._signerManager?.invalidate(sessionId);
|
|
3238
|
+
this._unwatchSessionState(sessionId);
|
|
2362
3239
|
await this._deviceManager.disconnect(sessionId);
|
|
2363
3240
|
if (deviceId) {
|
|
2364
3241
|
this._emit("device-disconnect", { connectId: deviceId });
|
|
2365
3242
|
}
|
|
2366
3243
|
}
|
|
3244
|
+
/**
|
|
3245
|
+
* Subscribe to DMK's per-session state observable so that any autonomous
|
|
3246
|
+
* disconnect (USB unplug, BLE drop, device sleep, transport reset) is
|
|
3247
|
+
* surfaced as a `device-disconnect` event — without this, the upstream
|
|
3248
|
+
* `LedgerAdapter._sessions` map would hold a dead session entry until
|
|
3249
|
+
* the next call hit `DeviceSessionNotFound`.
|
|
3250
|
+
*
|
|
3251
|
+
* Subscribe failure is fatal — see the inline note at the subscribe call.
|
|
3252
|
+
*/
|
|
3253
|
+
_watchSessionState(sessionId, externalConnectId) {
|
|
3254
|
+
const dmk = this._dmk;
|
|
3255
|
+
if (!dmk) return;
|
|
3256
|
+
const previous = this._sessionStateSubs.get(sessionId);
|
|
3257
|
+
if (previous) {
|
|
3258
|
+
try {
|
|
3259
|
+
previous.unsubscribe();
|
|
3260
|
+
} catch {
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
const sub = dmk.getDeviceSessionState({ sessionId }).subscribe({
|
|
3264
|
+
next: (state) => {
|
|
3265
|
+
if (state?.deviceStatus === "NOT CONNECTED") {
|
|
3266
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3267
|
+
}
|
|
3268
|
+
},
|
|
3269
|
+
error: () => {
|
|
3270
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3271
|
+
},
|
|
3272
|
+
complete: () => {
|
|
3273
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3274
|
+
}
|
|
3275
|
+
});
|
|
3276
|
+
this._sessionStateSubs.set(sessionId, sub);
|
|
3277
|
+
}
|
|
3278
|
+
_unwatchSessionState(sessionId) {
|
|
3279
|
+
const sub = this._sessionStateSubs.get(sessionId);
|
|
3280
|
+
if (!sub) return;
|
|
3281
|
+
try {
|
|
3282
|
+
sub.unsubscribe();
|
|
3283
|
+
} catch {
|
|
3284
|
+
}
|
|
3285
|
+
this._sessionStateSubs.delete(sessionId);
|
|
3286
|
+
}
|
|
3287
|
+
_handleAutonomousDisconnect(sessionId, externalConnectId) {
|
|
3288
|
+
if (!this._sessionStateSubs.has(sessionId)) return;
|
|
3289
|
+
debugLog(
|
|
3290
|
+
"[DMK] autonomous disconnect detected \u2014 sessionId:",
|
|
3291
|
+
sessionId,
|
|
3292
|
+
"connectId:",
|
|
3293
|
+
externalConnectId
|
|
3294
|
+
);
|
|
3295
|
+
this._unwatchSessionState(sessionId);
|
|
3296
|
+
this._signerManager?.invalidate(sessionId);
|
|
3297
|
+
this._cancellers.get(sessionId)?.({
|
|
3298
|
+
code: HardwareErrorCode7.DeviceDisconnected,
|
|
3299
|
+
tag: "DeviceDisconnected",
|
|
3300
|
+
message: "Device disconnected"
|
|
3301
|
+
});
|
|
3302
|
+
this._cancellers.delete(sessionId);
|
|
3303
|
+
this._emit("device-disconnect", { connectId: externalConnectId });
|
|
3304
|
+
}
|
|
2367
3305
|
// ---------------------------------------------------------------------------
|
|
2368
3306
|
// IConnector -- Method dispatch
|
|
2369
3307
|
// ---------------------------------------------------------------------------
|
|
2370
3308
|
async call(sessionId, method, params) {
|
|
2371
3309
|
debugLog("[DMK] call:", method, JSON.stringify(params));
|
|
3310
|
+
try {
|
|
3311
|
+
return await this._dispatch(sessionId, method, params);
|
|
3312
|
+
} catch (err) {
|
|
3313
|
+
if (isAppStuckByApdu(err)) {
|
|
3314
|
+
throw Object.assign(new Error("Ledger app is unresponsive"), {
|
|
3315
|
+
code: HardwareErrorCode7.DeviceAppStuck,
|
|
3316
|
+
_tag: ERROR_TAG.DeviceAppStuck,
|
|
3317
|
+
originalError: err
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
if (isTransportStuck(err)) {
|
|
3321
|
+
throw Object.assign(new Error("Device communication interrupted, please retry"), {
|
|
3322
|
+
code: HardwareErrorCode7.TransportError,
|
|
3323
|
+
_tag: ERROR_TAG.DeviceTransportStuck,
|
|
3324
|
+
originalError: err
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
throw err;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
async _dispatch(sessionId, method, params) {
|
|
3331
|
+
const ctx = this._ctxForMethod(method);
|
|
2372
3332
|
switch (method) {
|
|
2373
3333
|
// EVM
|
|
2374
3334
|
case "evmGetAddress":
|
|
2375
|
-
return evmGetAddress(
|
|
3335
|
+
return evmGetAddress(ctx, sessionId, params);
|
|
2376
3336
|
case "evmSignTransaction":
|
|
2377
|
-
return evmSignTransaction(
|
|
3337
|
+
return evmSignTransaction(ctx, sessionId, params);
|
|
2378
3338
|
case "evmSignMessage":
|
|
2379
|
-
return evmSignMessage(
|
|
3339
|
+
return evmSignMessage(ctx, sessionId, params);
|
|
2380
3340
|
case "evmSignTypedData":
|
|
2381
|
-
return evmSignTypedData(
|
|
3341
|
+
return evmSignTypedData(ctx, sessionId, params);
|
|
2382
3342
|
// BTC
|
|
2383
3343
|
case "btcGetAddress":
|
|
2384
|
-
return btcGetAddress(
|
|
3344
|
+
return btcGetAddress(ctx, sessionId, params);
|
|
2385
3345
|
case "btcGetPublicKey":
|
|
2386
|
-
return btcGetPublicKey(
|
|
3346
|
+
return btcGetPublicKey(ctx, sessionId, params);
|
|
2387
3347
|
case "btcSignTransaction":
|
|
2388
|
-
return btcSignTransaction(
|
|
3348
|
+
return btcSignTransaction(ctx, sessionId, params);
|
|
2389
3349
|
case "btcSignPsbt":
|
|
2390
|
-
return btcSignPsbt(
|
|
3350
|
+
return btcSignPsbt(ctx, sessionId, params);
|
|
2391
3351
|
case "btcSignMessage":
|
|
2392
|
-
return btcSignMessage(
|
|
3352
|
+
return btcSignMessage(ctx, sessionId, params);
|
|
2393
3353
|
case "btcGetMasterFingerprint":
|
|
2394
3354
|
return btcGetMasterFingerprint(
|
|
2395
|
-
|
|
3355
|
+
ctx,
|
|
2396
3356
|
sessionId,
|
|
2397
3357
|
params
|
|
2398
3358
|
);
|
|
2399
3359
|
// SOL
|
|
2400
3360
|
case "solGetAddress":
|
|
2401
|
-
return solGetAddress(
|
|
3361
|
+
return solGetAddress(ctx, sessionId, params);
|
|
2402
3362
|
case "solSignTransaction":
|
|
2403
|
-
return solSignTransaction(
|
|
3363
|
+
return solSignTransaction(ctx, sessionId, params);
|
|
2404
3364
|
case "solSignMessage":
|
|
2405
|
-
return solSignMessage(
|
|
3365
|
+
return solSignMessage(ctx, sessionId, params);
|
|
2406
3366
|
// TRON
|
|
2407
3367
|
case "tronGetAddress":
|
|
2408
|
-
return tronGetAddress(
|
|
3368
|
+
return tronGetAddress(ctx, sessionId, params);
|
|
2409
3369
|
case "tronSignTransaction":
|
|
2410
|
-
return tronSignTransaction(
|
|
3370
|
+
return tronSignTransaction(ctx, sessionId, params);
|
|
2411
3371
|
case "tronSignMessage": {
|
|
2412
3372
|
const p = params;
|
|
2413
3373
|
const internalParams = {
|
|
2414
3374
|
path: p.path,
|
|
2415
3375
|
messageHex: p.messageHex
|
|
2416
3376
|
};
|
|
2417
|
-
return tronSignMessage(
|
|
3377
|
+
return tronSignMessage(ctx, sessionId, internalParams);
|
|
2418
3378
|
}
|
|
2419
3379
|
default:
|
|
2420
3380
|
throw new Error(`LedgerConnector: unknown method "${method}"`);
|
|
@@ -2507,14 +3467,13 @@ var LedgerConnectorBase = class {
|
|
|
2507
3467
|
}
|
|
2508
3468
|
/**
|
|
2509
3469
|
* Replace an old session with a new one after app switch.
|
|
2510
|
-
*
|
|
2511
|
-
* and emits device-connect so the adapter updates its _sessions Map.
|
|
3470
|
+
* Emits device-connect so the adapter updates its _sessions Map.
|
|
2512
3471
|
*/
|
|
2513
3472
|
_replaceSession(oldSessionId, _newSessionId) {
|
|
2514
3473
|
const dm = this._deviceManager;
|
|
2515
3474
|
if (!dm) return;
|
|
2516
3475
|
const oldDeviceId = dm.getDeviceId(oldSessionId);
|
|
2517
|
-
const connectId = oldDeviceId
|
|
3476
|
+
const connectId = oldDeviceId;
|
|
2518
3477
|
this._signerManager?.invalidate(oldSessionId);
|
|
2519
3478
|
if (connectId) {
|
|
2520
3479
|
this._emit("device-connect", {
|
|
@@ -2527,13 +3486,18 @@ var LedgerConnectorBase = class {
|
|
|
2527
3486
|
}
|
|
2528
3487
|
}
|
|
2529
3488
|
/**
|
|
2530
|
-
* Light reset: clear signer/session state but keep DMK
|
|
3489
|
+
* Light reset: clear signer/session state but keep DMK alive.
|
|
2531
3490
|
* Used by connect() retry — we want to re-discover with the same transport.
|
|
3491
|
+
*
|
|
3492
|
+
* Note: drops the device manager but tears down its RxJS subs first via
|
|
3493
|
+
* disposeKeepingDmk(). Bare null-assignment would leak _listenSub /
|
|
3494
|
+
* _discoverySub and double-scan after the next _initManagers().
|
|
2532
3495
|
*/
|
|
2533
3496
|
_resetSignersAndSessions() {
|
|
2534
3497
|
debugLog("[DMK] _resetSignersAndSessions called");
|
|
2535
3498
|
this._signerManager?.clearAll();
|
|
2536
3499
|
this._signerManager = null;
|
|
3500
|
+
this._deviceManager?.disposeKeepingDmk();
|
|
2537
3501
|
this._deviceManager = null;
|
|
2538
3502
|
}
|
|
2539
3503
|
_resetAll() {
|
|
@@ -2545,13 +3509,18 @@ var LedgerConnectorBase = class {
|
|
|
2545
3509
|
}
|
|
2546
3510
|
}
|
|
2547
3511
|
this._cancellers.clear();
|
|
3512
|
+
for (const sub of this._sessionStateSubs.values()) {
|
|
3513
|
+
try {
|
|
3514
|
+
sub.unsubscribe();
|
|
3515
|
+
} catch {
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
this._sessionStateSubs.clear();
|
|
2548
3519
|
this._signerManager?.clearAll();
|
|
2549
3520
|
this._deviceManager?.dispose();
|
|
2550
3521
|
this._deviceManager = null;
|
|
2551
3522
|
this._signerManager = null;
|
|
2552
3523
|
this._dmk = null;
|
|
2553
|
-
this._connectIdToPath.clear();
|
|
2554
|
-
this._pathToConnectId.clear();
|
|
2555
3524
|
}
|
|
2556
3525
|
// ---------------------------------------------------------------------------
|
|
2557
3526
|
// Private -- Events
|
|
@@ -2570,16 +3539,36 @@ var LedgerConnectorBase = class {
|
|
|
2570
3539
|
// ---------------------------------------------------------------------------
|
|
2571
3540
|
// Private -- Error handling
|
|
2572
3541
|
// ---------------------------------------------------------------------------
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3542
|
+
/**
|
|
3543
|
+
* Return a per-call ctx with the chain's Ledger app name pre-bound to
|
|
3544
|
+
* wrapError, so chain handlers don't need to repeat `{ defaultAppName: 'X' }`
|
|
3545
|
+
* at every catch site. Falls through unchanged for unknown methods.
|
|
3546
|
+
*/
|
|
3547
|
+
_ctxForMethod(method) {
|
|
3548
|
+
const prefix = /^(evm|btc|sol|tron)/.exec(method)?.[1];
|
|
3549
|
+
const defaultAppName = prefix ? METHOD_PREFIX_TO_APP_NAME[prefix] : void 0;
|
|
3550
|
+
if (!defaultAppName) return this._ctx;
|
|
3551
|
+
return {
|
|
3552
|
+
...this._ctx,
|
|
3553
|
+
wrapError: (err, opts) => this._wrapError(err, { defaultAppName, ...opts })
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
_wrapError(err, opts) {
|
|
2576
3557
|
const src = err && typeof err === "object" ? err : {};
|
|
3558
|
+
const hasSerializedCode = typeof src.code === "number" && HARDWARE_ERROR_CODE_VALUES.has(src.code);
|
|
3559
|
+
const mapped = hasSerializedCode ? {
|
|
3560
|
+
code: src.code,
|
|
3561
|
+
message: typeof src.message === "string" ? src.message : "Unknown Ledger error",
|
|
3562
|
+
appName: typeof src.appName === "string" ? src.appName : opts?.defaultAppName
|
|
3563
|
+
} : mapLedgerError(err, opts);
|
|
3564
|
+
const error = new Error(mapped.message);
|
|
2577
3565
|
Object.assign(error, {
|
|
2578
3566
|
code: mapped.code,
|
|
2579
3567
|
appName: mapped.appName,
|
|
2580
3568
|
_tag: src._tag,
|
|
2581
3569
|
errorCode: src.errorCode,
|
|
2582
|
-
_lastStep: src._lastStep
|
|
3570
|
+
_lastStep: src._lastStep,
|
|
3571
|
+
_deviceActionSteps: src._deviceActionSteps
|
|
2583
3572
|
});
|
|
2584
3573
|
Object.defineProperty(error, "originalError", {
|
|
2585
3574
|
value: err,
|
|
@@ -2590,13 +3579,6 @@ var LedgerConnectorBase = class {
|
|
|
2590
3579
|
return error;
|
|
2591
3580
|
}
|
|
2592
3581
|
};
|
|
2593
|
-
|
|
2594
|
-
// src/utils/bleIdentity.ts
|
|
2595
|
-
function extractBleHexId(name) {
|
|
2596
|
-
if (!name) return void 0;
|
|
2597
|
-
const match = name.match(/\b([0-9A-Fa-f]{4})$/);
|
|
2598
|
-
return match ? match[1].toUpperCase() : void 0;
|
|
2599
|
-
}
|
|
2600
3582
|
export {
|
|
2601
3583
|
AppManager,
|
|
2602
3584
|
DmkTransport,
|
|
@@ -2607,12 +3589,15 @@ export {
|
|
|
2607
3589
|
SignerEth,
|
|
2608
3590
|
SignerManager,
|
|
2609
3591
|
SignerSol,
|
|
3592
|
+
debugLog,
|
|
2610
3593
|
deviceActionToPromise,
|
|
2611
|
-
extractBleHexId,
|
|
2612
|
-
isDebugEnabled,
|
|
2613
3594
|
isDeviceLockedError,
|
|
3595
|
+
isLedgerBleConnectionType,
|
|
3596
|
+
isLedgerBleDescriptor,
|
|
3597
|
+
isLedgerDmkBleTransport,
|
|
2614
3598
|
ledgerFailure,
|
|
2615
3599
|
mapLedgerError,
|
|
2616
|
-
|
|
3600
|
+
offSdkEvent,
|
|
3601
|
+
onSdkEvent
|
|
2617
3602
|
};
|
|
2618
3603
|
//# sourceMappingURL=index.mjs.map
|