@onekeyfe/hwk-ledger-adapter 1.1.26-alpha.9 → 1.1.26-patch.2
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 +252 -176
- package/dist/index.d.ts +252 -176
- package/dist/index.js +1377 -439
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1378 -441
- 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,6 +330,21 @@ function isTimeoutError(err) {
|
|
|
179
330
|
if (e.code === HardwareErrorCode.OperationTimeout) return true;
|
|
180
331
|
return false;
|
|
181
332
|
}
|
|
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
|
+
}
|
|
182
348
|
function mapLedgerError(err, opts) {
|
|
183
349
|
let originalMessage = "Unknown Ledger error";
|
|
184
350
|
if (err instanceof Error) {
|
|
@@ -190,20 +356,24 @@ function mapLedgerError(err, opts) {
|
|
|
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;
|
|
@@ -213,38 +383,100 @@ function mapLedgerError(err, opts) {
|
|
|
213
383
|
return { code, message: enrichErrorMessage(code, originalMessage), appName };
|
|
214
384
|
}
|
|
215
385
|
|
|
216
|
-
// src/utils/
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
386
|
+
// src/utils/ledgerDmkTransport.ts
|
|
387
|
+
function isLedgerDmkBleTransport(transport) {
|
|
388
|
+
const normalized = transport?.toUpperCase();
|
|
389
|
+
return normalized === "BLE" || normalized?.endsWith("_BLE") === true;
|
|
220
390
|
}
|
|
221
|
-
function
|
|
222
|
-
return
|
|
391
|
+
function isLedgerBleConnectionType(connectionType) {
|
|
392
|
+
return connectionType === "ble";
|
|
223
393
|
}
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
}
|
|
227
416
|
}
|
|
228
417
|
}
|
|
229
|
-
function
|
|
230
|
-
if (
|
|
231
|
-
|
|
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;
|
|
232
427
|
}
|
|
428
|
+
try {
|
|
429
|
+
return JSON.stringify(value);
|
|
430
|
+
} catch {
|
|
431
|
+
return String(value);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/utils/debugLog.ts
|
|
436
|
+
function debugLog(...args) {
|
|
437
|
+
emitLog("debug", ...args);
|
|
438
|
+
}
|
|
439
|
+
function debugError(...args) {
|
|
440
|
+
emitLog("error", ...args);
|
|
233
441
|
}
|
|
234
442
|
|
|
235
443
|
// src/adapter/LedgerAdapter.ts
|
|
236
444
|
function formatDeviceMismatchError(expected, actual) {
|
|
237
445
|
return `Wrong device: expected ${expected}, got ${actual}`;
|
|
238
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
|
+
}
|
|
239
455
|
var _LedgerAdapter = class _LedgerAdapter {
|
|
240
|
-
constructor(connector
|
|
456
|
+
constructor(connector) {
|
|
241
457
|
this.vendor = "ledger";
|
|
242
458
|
this.emitter = new TypedEventEmitter();
|
|
243
|
-
// Device cache: tracks discovered devices from connector events
|
|
244
459
|
this._discoveredDevices = /* @__PURE__ */ new Map();
|
|
245
|
-
// Session tracking: maps connectId -> sessionId
|
|
246
460
|
this._sessions = /* @__PURE__ */ new Map();
|
|
247
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
|
+
*/
|
|
248
480
|
// Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
|
|
249
481
|
this._connectingPromise = null;
|
|
250
482
|
// ---------------------------------------------------------------------------
|
|
@@ -274,40 +506,23 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
274
506
|
this.emitter.emit("ui-event", event);
|
|
275
507
|
};
|
|
276
508
|
this.connector = connector;
|
|
277
|
-
this.
|
|
278
|
-
this._jobQueue = new DeviceJobQueue({
|
|
279
|
-
emit: (event, data) => this.emitter.emit(event, data),
|
|
280
|
-
uiRegistry: this._uiRegistry
|
|
281
|
-
});
|
|
509
|
+
this._jobQueue = new DeviceJobQueue();
|
|
282
510
|
this.registerEventListeners();
|
|
283
511
|
}
|
|
284
|
-
/**
|
|
285
|
-
* Classify a method's interruptibility.
|
|
286
|
-
* - Signing / typed data / transaction → 'confirm' (user may decide via preemption UI)
|
|
287
|
-
* - Read-only queries (getAddress / getPublicKey / getMasterFingerprint) → 'safe'
|
|
288
|
-
* (auto-cancels any pending read for the same device)
|
|
289
|
-
*/
|
|
290
|
-
static _getInterruptibility(method) {
|
|
291
|
-
if (method.toLowerCase().includes("sign")) return "confirm";
|
|
292
|
-
return "safe";
|
|
293
|
-
}
|
|
294
|
-
// ---------------------------------------------------------------------------
|
|
295
512
|
// Transport
|
|
296
|
-
// ---------------------------------------------------------------------------
|
|
297
|
-
// Transport is decided at connector creation time. These methods
|
|
298
|
-
// satisfy the IHardwareWallet interface with sensible defaults.
|
|
299
513
|
get activeTransport() {
|
|
300
|
-
return this.connector.connectionType
|
|
514
|
+
return isLedgerBleConnectionType(this.connector.connectionType) ? "ble" : "hid";
|
|
301
515
|
}
|
|
302
516
|
getAvailableTransports() {
|
|
303
517
|
return this.activeTransport ? [this.activeTransport] : [];
|
|
304
518
|
}
|
|
305
|
-
|
|
519
|
+
// Connector is bound at construction; switching requires a new adapter.
|
|
520
|
+
switchTransport(_type) {
|
|
521
|
+
return Promise.resolve();
|
|
306
522
|
}
|
|
307
|
-
// ---------------------------------------------------------------------------
|
|
308
523
|
// Lifecycle
|
|
309
|
-
|
|
310
|
-
|
|
524
|
+
init(_config) {
|
|
525
|
+
return Promise.resolve();
|
|
311
526
|
}
|
|
312
527
|
/**
|
|
313
528
|
* Clear cached device/session state without tearing down the adapter.
|
|
@@ -320,6 +535,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
320
535
|
this._connectingPromise = null;
|
|
321
536
|
this._uiRegistry.reset();
|
|
322
537
|
this._jobQueue.clear();
|
|
538
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
323
539
|
}
|
|
324
540
|
async dispose() {
|
|
325
541
|
this._uiRegistry.reset();
|
|
@@ -328,6 +544,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
328
544
|
this.connector.reset();
|
|
329
545
|
this._discoveredDevices.clear();
|
|
330
546
|
this._sessions.clear();
|
|
547
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
331
548
|
this.emitter.removeAllListeners();
|
|
332
549
|
}
|
|
333
550
|
uiResponse(response) {
|
|
@@ -336,10 +553,16 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
336
553
|
// ---------------------------------------------------------------------------
|
|
337
554
|
// Device management
|
|
338
555
|
// ---------------------------------------------------------------------------
|
|
339
|
-
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
|
+
}
|
|
340
564
|
await this._ensureDevicePermission();
|
|
341
565
|
const devices = await this.connector.searchDevices();
|
|
342
|
-
debugLog("[DMK] adapter.searchDevices raw:", JSON.stringify(devices));
|
|
343
566
|
this._discoveredDevices.clear();
|
|
344
567
|
for (const d of devices) {
|
|
345
568
|
if (d.connectId) {
|
|
@@ -349,11 +572,41 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
349
572
|
if (this._discoveredDevices.size === 0) {
|
|
350
573
|
await this._ensureDevicePermission();
|
|
351
574
|
}
|
|
575
|
+
debugLog(
|
|
576
|
+
`[LedgerAdapter] searchDevices() return count=${this._discoveredDevices.size} ids=[${[
|
|
577
|
+
...this._discoveredDevices.keys()
|
|
578
|
+
].join(",")}]`
|
|
579
|
+
);
|
|
352
580
|
return Array.from(this._discoveredDevices.values());
|
|
353
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
|
+
}
|
|
354
599
|
async connectDevice(connectId) {
|
|
355
|
-
await this._ensureDevicePermission(connectId);
|
|
356
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);
|
|
357
610
|
const session = await this.connector.connect(connectId);
|
|
358
611
|
this._sessions.set(connectId, session.sessionId);
|
|
359
612
|
if (session.deviceInfo) {
|
|
@@ -389,7 +642,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
389
642
|
// Chain call helper
|
|
390
643
|
// ---------------------------------------------------------------------------
|
|
391
644
|
async callChain(connectId, deviceId, chain, method, params, skipFingerprint = false) {
|
|
392
|
-
await this._ensureDevicePermission(connectId, deviceId);
|
|
393
645
|
try {
|
|
394
646
|
const result = await this.connectorCall(connectId, method, params, {
|
|
395
647
|
chain,
|
|
@@ -401,45 +653,12 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
401
653
|
return this.errorToFailure(err);
|
|
402
654
|
}
|
|
403
655
|
}
|
|
404
|
-
/**
|
|
405
|
-
* Batch version of callChain — checks permission once,
|
|
406
|
-
* fingerprint is verified on the first call inside connectorCall.
|
|
407
|
-
*/
|
|
408
|
-
async callChainBatch(connectId, deviceId, chain, method, params, onProgress, skipFingerprint = false) {
|
|
409
|
-
await this._ensureDevicePermission(connectId, deviceId);
|
|
410
|
-
const results = [];
|
|
411
|
-
for (let i = 0; i < params.length; i++) {
|
|
412
|
-
try {
|
|
413
|
-
const result = await this.connectorCall(connectId, method, params[i], {
|
|
414
|
-
chain,
|
|
415
|
-
deviceId,
|
|
416
|
-
// Only verify fingerprint on the first call in the batch
|
|
417
|
-
skipFingerprint: skipFingerprint || i > 0
|
|
418
|
-
});
|
|
419
|
-
results.push(result);
|
|
420
|
-
onProgress?.({ index: i, total: params.length });
|
|
421
|
-
} catch (err) {
|
|
422
|
-
return this.errorToFailure(err);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
return success(results);
|
|
426
|
-
}
|
|
427
656
|
// ---------------------------------------------------------------------------
|
|
428
657
|
// EVM chain methods
|
|
429
658
|
// ---------------------------------------------------------------------------
|
|
430
659
|
evmGetAddress(connectId, deviceId, params) {
|
|
431
660
|
return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
|
|
432
661
|
}
|
|
433
|
-
evmGetAddresses(connectId, deviceId, params, onProgress) {
|
|
434
|
-
return this.callChainBatch(
|
|
435
|
-
connectId,
|
|
436
|
-
deviceId,
|
|
437
|
-
"evm",
|
|
438
|
-
"evmGetAddress",
|
|
439
|
-
params,
|
|
440
|
-
onProgress
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
662
|
evmSignTransaction(connectId, deviceId, params) {
|
|
444
663
|
return this.callChain(connectId, deviceId, "evm", "evmSignTransaction", params);
|
|
445
664
|
}
|
|
@@ -455,16 +674,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
455
674
|
btcGetAddress(connectId, deviceId, params) {
|
|
456
675
|
return this.callChain(connectId, deviceId, "btc", "btcGetAddress", params);
|
|
457
676
|
}
|
|
458
|
-
btcGetAddresses(connectId, deviceId, params, onProgress) {
|
|
459
|
-
return this.callChainBatch(
|
|
460
|
-
connectId,
|
|
461
|
-
deviceId,
|
|
462
|
-
"btc",
|
|
463
|
-
"btcGetAddress",
|
|
464
|
-
params,
|
|
465
|
-
onProgress
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
677
|
btcGetPublicKey(connectId, deviceId, params) {
|
|
469
678
|
return this.callChain(connectId, deviceId, "btc", "btcGetPublicKey", params);
|
|
470
679
|
}
|
|
@@ -492,16 +701,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
492
701
|
solGetAddress(connectId, deviceId, params) {
|
|
493
702
|
return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
|
|
494
703
|
}
|
|
495
|
-
solGetAddresses(connectId, deviceId, params, onProgress) {
|
|
496
|
-
return this.callChainBatch(
|
|
497
|
-
connectId,
|
|
498
|
-
deviceId,
|
|
499
|
-
"sol",
|
|
500
|
-
"solGetAddress",
|
|
501
|
-
params,
|
|
502
|
-
onProgress
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
704
|
solSignTransaction(connectId, deviceId, params) {
|
|
506
705
|
return this.callChain(connectId, deviceId, "sol", "solSignTransaction", params);
|
|
507
706
|
}
|
|
@@ -512,38 +711,13 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
512
711
|
// TRON chain methods
|
|
513
712
|
// ---------------------------------------------------------------------------
|
|
514
713
|
tronGetAddress(connectId, deviceId, params) {
|
|
515
|
-
return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params
|
|
516
|
-
}
|
|
517
|
-
tronGetAddresses(connectId, deviceId, params, onProgress) {
|
|
518
|
-
return this.callChainBatch(
|
|
519
|
-
connectId,
|
|
520
|
-
deviceId,
|
|
521
|
-
"tron",
|
|
522
|
-
"tronGetAddress",
|
|
523
|
-
params,
|
|
524
|
-
onProgress,
|
|
525
|
-
true
|
|
526
|
-
);
|
|
714
|
+
return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params);
|
|
527
715
|
}
|
|
528
716
|
tronSignTransaction(connectId, deviceId, params) {
|
|
529
|
-
return this.callChain(
|
|
530
|
-
connectId,
|
|
531
|
-
deviceId,
|
|
532
|
-
"tron",
|
|
533
|
-
"tronSignTransaction",
|
|
534
|
-
params,
|
|
535
|
-
true
|
|
536
|
-
);
|
|
717
|
+
return this.callChain(connectId, deviceId, "tron", "tronSignTransaction", params);
|
|
537
718
|
}
|
|
538
719
|
tronSignMessage(connectId, deviceId, params) {
|
|
539
|
-
return this.callChain(
|
|
540
|
-
connectId,
|
|
541
|
-
deviceId,
|
|
542
|
-
"tron",
|
|
543
|
-
"tronSignMessage",
|
|
544
|
-
params,
|
|
545
|
-
true
|
|
546
|
-
);
|
|
720
|
+
return this.callChain(connectId, deviceId, "tron", "tronSignMessage", params);
|
|
547
721
|
}
|
|
548
722
|
on(event, listener) {
|
|
549
723
|
this.emitter.on(event, listener);
|
|
@@ -552,30 +726,41 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
552
726
|
this.emitter.off(event, listener);
|
|
553
727
|
}
|
|
554
728
|
cancel(connectId) {
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
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
|
+
}
|
|
558
754
|
}
|
|
559
755
|
// ---------------------------------------------------------------------------
|
|
560
756
|
// Chain fingerprint
|
|
561
757
|
// ---------------------------------------------------------------------------
|
|
562
758
|
async getChainFingerprint(connectId, deviceId, chain) {
|
|
563
|
-
debugLog(
|
|
564
|
-
"[LedgerAdapter] getChainFingerprint called, chain:",
|
|
565
|
-
chain,
|
|
566
|
-
"connectId:",
|
|
567
|
-
connectId || "(empty)",
|
|
568
|
-
"sessions:",
|
|
569
|
-
this._sessions.size
|
|
570
|
-
);
|
|
571
|
-
await this._ensureDevicePermission(connectId, deviceId);
|
|
572
|
-
debugLog("[LedgerAdapter] getChainFingerprint permission ok, computing fingerprint");
|
|
573
759
|
try {
|
|
574
760
|
const fingerprint = await this._computeChainFingerprint(
|
|
575
761
|
chain,
|
|
576
|
-
(method, params) => this.connectorCall(connectId, method, params)
|
|
762
|
+
(method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
|
|
577
763
|
);
|
|
578
|
-
debugLog("[LedgerAdapter] getChainFingerprint result:", fingerprint?.substring(0, 20));
|
|
579
764
|
return success(fingerprint);
|
|
580
765
|
} catch (err) {
|
|
581
766
|
debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
|
|
@@ -646,103 +831,250 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
646
831
|
if (signal?.aborted) {
|
|
647
832
|
_LedgerAdapter._throwIfAborted(signal);
|
|
648
833
|
}
|
|
834
|
+
const waitPromise = this._uiRegistry.wait(
|
|
835
|
+
UI_REQUEST.REQUEST_DEVICE_CONNECT
|
|
836
|
+
);
|
|
649
837
|
this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_CONNECT, {
|
|
650
838
|
type: UI_REQUEST.REQUEST_DEVICE_CONNECT,
|
|
651
839
|
payload: {
|
|
840
|
+
vendor: "ledger",
|
|
841
|
+
reason: "device-not-found",
|
|
652
842
|
message: "Please connect and unlock your Ledger device"
|
|
653
843
|
}
|
|
654
844
|
});
|
|
655
|
-
const waitPromise = this._uiRegistry.wait(
|
|
656
|
-
UI_REQUEST.REQUEST_DEVICE_CONNECT
|
|
657
|
-
);
|
|
658
845
|
let payload;
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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 {
|
|
665
858
|
payload = await waitPromise;
|
|
666
|
-
} finally {
|
|
667
|
-
signal.removeEventListener("abort", onAbort);
|
|
668
859
|
}
|
|
669
|
-
}
|
|
670
|
-
|
|
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
|
+
});
|
|
671
870
|
}
|
|
672
871
|
if (!payload?.confirmed) {
|
|
673
872
|
throw Object.assign(new Error("User cancelled Ledger connection"), {
|
|
674
|
-
_tag:
|
|
873
|
+
_tag: ERROR_TAG.UserAborted,
|
|
874
|
+
code: HardwareErrorCode2.UserAborted
|
|
675
875
|
});
|
|
676
876
|
}
|
|
877
|
+
await new Promise((resolve) => {
|
|
878
|
+
setTimeout(resolve, 800);
|
|
879
|
+
});
|
|
677
880
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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;
|
|
685
893
|
}
|
|
686
|
-
if (this.
|
|
687
|
-
return
|
|
894
|
+
if (this._btcHighIndexConfirmedThisSession) {
|
|
895
|
+
return { ...params, showOnDevice: true };
|
|
688
896
|
}
|
|
689
|
-
|
|
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
|
+
});
|
|
690
914
|
try {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
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
|
+
);
|
|
944
|
+
}
|
|
945
|
+
return this._sessions.keys().next().value;
|
|
946
|
+
}
|
|
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
|
+
})();
|
|
694
958
|
}
|
|
959
|
+
return this._abortable(signal, this._connectingPromise);
|
|
695
960
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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: "" }
|
|
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
|
+
}
|
|
699
997
|
if (devices.length > 0) {
|
|
700
|
-
|
|
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
|
+
}
|
|
701
1012
|
}
|
|
702
|
-
if (
|
|
703
|
-
|
|
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
|
+
);
|
|
704
1020
|
}
|
|
1021
|
+
await this._waitForDeviceConnect(internalSignal);
|
|
1022
|
+
confirms += 1;
|
|
705
1023
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1024
|
+
_LedgerAdapter._throwIfAborted(internalSignal);
|
|
1025
|
+
throw new Error("_doConnect aborted");
|
|
1026
|
+
}
|
|
1027
|
+
async _connectFirstOrSelect(devices, targetConnectId) {
|
|
1028
|
+
if (targetConnectId) {
|
|
1029
|
+
const target = devices.find(
|
|
1030
|
+
(d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
|
|
1031
|
+
);
|
|
1032
|
+
if (target) {
|
|
1033
|
+
return this._connectDeviceOrThrow(target.connectId);
|
|
1034
|
+
}
|
|
1035
|
+
if (!isLedgerBleConnectionType(this.connector.connectionType) && devices.length === 1) {
|
|
1036
|
+
debugLog(
|
|
1037
|
+
`[LedgerAdapter] target ${targetConnectId} not in fresh enumeration; accepting sole USB device ${devices[0].connectId} (assumed ephemeral path change)`
|
|
1038
|
+
);
|
|
1039
|
+
return this._connectDeviceOrThrow(devices[0].connectId);
|
|
1040
|
+
}
|
|
1041
|
+
const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
|
|
1042
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
1043
|
+
});
|
|
1044
|
+
if (isLedgerBleConnectionType(this.connector.connectionType)) {
|
|
1045
|
+
err._tag = ERROR_TAG.DeviceNotAdvertising;
|
|
1046
|
+
}
|
|
1047
|
+
throw err;
|
|
1048
|
+
}
|
|
1049
|
+
if (isLedgerBleConnectionType(this.connector.connectionType)) {
|
|
1050
|
+
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
1051
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
if (devices.length > 1) {
|
|
1055
|
+
throw createMultipleUsbLedgerDevicesError();
|
|
1056
|
+
}
|
|
1057
|
+
if (devices.length !== 1) {
|
|
1058
|
+
throw Object.assign(new Error("Ledger device not found."), {
|
|
1059
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
return this._connectDeviceOrThrow(devices[0].connectId);
|
|
712
1063
|
}
|
|
713
|
-
async
|
|
714
|
-
const chosenConnectId = devices.length === 1 ? devices[0].connectId : await this._chooseDeviceFromList(devices);
|
|
1064
|
+
async _connectDeviceOrThrow(chosenConnectId) {
|
|
715
1065
|
const result = await this.connectDevice(chosenConnectId);
|
|
716
1066
|
if (!result.success) {
|
|
717
|
-
|
|
718
|
-
|
|
1067
|
+
const payload = result.payload;
|
|
1068
|
+
const rethrow = Object.assign(new Error(payload.error), {
|
|
1069
|
+
code: payload.code
|
|
719
1070
|
});
|
|
1071
|
+
if (payload._tag !== void 0) {
|
|
1072
|
+
rethrow._tag = payload._tag;
|
|
1073
|
+
}
|
|
1074
|
+
throw rethrow;
|
|
720
1075
|
}
|
|
721
1076
|
return chosenConnectId;
|
|
722
1077
|
}
|
|
723
|
-
async _chooseDeviceFromList(devices) {
|
|
724
|
-
if (!this._handleSelectDevice) {
|
|
725
|
-
debugLog(
|
|
726
|
-
`[DMK] Multiple Ledger devices found (${devices.length}); handleSelectDevice=false, picking first (${devices[0].connectId}).`
|
|
727
|
-
);
|
|
728
|
-
return devices[0].connectId;
|
|
729
|
-
}
|
|
730
|
-
this.emitter.emit(UI_REQUEST.REQUEST_SELECT_DEVICE, {
|
|
731
|
-
type: UI_REQUEST.REQUEST_SELECT_DEVICE,
|
|
732
|
-
payload: { devices }
|
|
733
|
-
});
|
|
734
|
-
const response = await this._uiRegistry.wait(
|
|
735
|
-
UI_REQUEST.REQUEST_SELECT_DEVICE
|
|
736
|
-
);
|
|
737
|
-
const chosen = devices.find((d) => d.connectId === response?.sdkConnectId);
|
|
738
|
-
if (!chosen) {
|
|
739
|
-
throw Object.assign(
|
|
740
|
-
new Error(`Selected sdkConnectId '${response?.sdkConnectId}' not in discovered list`),
|
|
741
|
-
{ _tag: "DeviceNotRecognizedError" }
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
return chosen.connectId;
|
|
745
|
-
}
|
|
746
1078
|
/**
|
|
747
1079
|
* Call the connector with automatic session resolution and disconnect retry.
|
|
748
1080
|
*
|
|
@@ -751,30 +1083,31 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
751
1083
|
* 3. Calls connector.call()
|
|
752
1084
|
* 4. On disconnect error: clears stale session, re-connects, retries once
|
|
753
1085
|
*/
|
|
754
|
-
async connectorCall(connectId, method, params, fingerprint) {
|
|
1086
|
+
async connectorCall(connectId, method, params, fingerprint, permissionDeviceId) {
|
|
755
1087
|
debugLog("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
|
|
756
1088
|
const queueKey = connectId || "__ledger_default__";
|
|
757
|
-
const interruptibility = _LedgerAdapter._getInterruptibility(method);
|
|
758
1089
|
return this._jobQueue.enqueue(
|
|
759
1090
|
queueKey,
|
|
760
|
-
async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint),
|
|
761
|
-
{
|
|
1091
|
+
async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId),
|
|
1092
|
+
{
|
|
1093
|
+
label: method,
|
|
1094
|
+
rejectIfBusy: true,
|
|
1095
|
+
busyError: _LedgerAdapter._createDeviceBusyError(method)
|
|
1096
|
+
}
|
|
762
1097
|
);
|
|
763
1098
|
}
|
|
764
1099
|
/**
|
|
765
|
-
* Race a promise against an abort signal.
|
|
766
|
-
* signal
|
|
767
|
-
* actually be cancelled on Ledger DMK, but the caller gets the abort immediately.
|
|
1100
|
+
* Race a promise against an abort signal. On abort, rejects with
|
|
1101
|
+
* signal.reason → instance _lastCancelReason → generic Error('Aborted').
|
|
768
1102
|
*/
|
|
769
|
-
|
|
1103
|
+
_abortable(signal, promise) {
|
|
1104
|
+
const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
|
|
770
1105
|
if (signal.aborted) {
|
|
771
|
-
return Promise.reject(
|
|
772
|
-
signal.reason ?? new Error("Aborted")
|
|
773
|
-
);
|
|
1106
|
+
return Promise.reject(getAbortReason());
|
|
774
1107
|
}
|
|
775
1108
|
return new Promise((resolve, reject) => {
|
|
776
1109
|
const onAbort = () => {
|
|
777
|
-
reject(
|
|
1110
|
+
reject(getAbortReason());
|
|
778
1111
|
};
|
|
779
1112
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
780
1113
|
promise.then(
|
|
@@ -796,40 +1129,49 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
796
1129
|
}
|
|
797
1130
|
}
|
|
798
1131
|
/** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
|
|
799
|
-
async _runConnectorCall(connectId, method, params, signal, fingerprint) {
|
|
1132
|
+
async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId) {
|
|
800
1133
|
_LedgerAdapter._throwIfAborted(signal);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1134
|
+
await this._ensureDevicePermission(
|
|
1135
|
+
connectId,
|
|
1136
|
+
permissionDeviceId ?? fingerprint?.deviceId,
|
|
1137
|
+
signal
|
|
804
1138
|
);
|
|
805
1139
|
_LedgerAdapter._throwIfAborted(signal);
|
|
1140
|
+
let effectiveParams = params;
|
|
1141
|
+
if (method === "btcGetPublicKey") {
|
|
1142
|
+
const gatedParams = await this._gateBtcHighIndex(params);
|
|
1143
|
+
if (gatedParams === null) {
|
|
1144
|
+
throw Object.assign(new Error("User cancelled BTC high-index confirmation"), {
|
|
1145
|
+
_tag: ERROR_TAG.UserAborted,
|
|
1146
|
+
code: HardwareErrorCode2.UserAborted
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
effectiveParams = gatedParams;
|
|
1150
|
+
}
|
|
1151
|
+
const resolvedConnectId = await this.ensureConnected(connectId, signal);
|
|
806
1152
|
const sessionId = this._sessions.get(resolvedConnectId);
|
|
807
|
-
debugLog(
|
|
808
|
-
"[LedgerAdapter] connectorCall resolved:",
|
|
809
|
-
method,
|
|
810
|
-
"resolvedConnectId:",
|
|
811
|
-
resolvedConnectId,
|
|
812
|
-
"sessionId:",
|
|
813
|
-
sessionId
|
|
814
|
-
);
|
|
815
1153
|
if (!sessionId) {
|
|
816
1154
|
throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
|
|
817
|
-
_tag:
|
|
1155
|
+
_tag: ERROR_TAG.DeviceSessionNotFound
|
|
818
1156
|
});
|
|
819
1157
|
}
|
|
820
|
-
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
821
|
-
const fp = await _LedgerAdapter._abortable(
|
|
822
|
-
signal,
|
|
823
|
-
this._verifyDeviceFingerprintWithSession(sessionId, fingerprint.deviceId, fingerprint.chain)
|
|
824
|
-
);
|
|
825
|
-
if (!fp.success) {
|
|
826
|
-
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
827
|
-
code: HardwareErrorCode2.DeviceMismatch
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
1158
|
try {
|
|
832
|
-
|
|
1159
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1160
|
+
const fp = await this._abortable(
|
|
1161
|
+
signal,
|
|
1162
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1163
|
+
sessionId,
|
|
1164
|
+
fingerprint.deviceId,
|
|
1165
|
+
fingerprint.chain
|
|
1166
|
+
)
|
|
1167
|
+
);
|
|
1168
|
+
if (!fp.success) {
|
|
1169
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1170
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return await this._abortable(signal, this.connector.call(sessionId, method, effectiveParams));
|
|
833
1175
|
} catch (err) {
|
|
834
1176
|
if (signal.aborted) throw err;
|
|
835
1177
|
const errObj = err;
|
|
@@ -839,37 +1181,241 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
839
1181
|
errorCode: errObj?.errorCode,
|
|
840
1182
|
statusCode: errObj?.statusCode,
|
|
841
1183
|
isDisconnected: isDeviceDisconnectedError(err),
|
|
842
|
-
isLocked: isDeviceLockedError(err)
|
|
1184
|
+
isLocked: isDeviceLockedError(err),
|
|
1185
|
+
isNotAdvertising: isDeviceNotAdvertisingError(err),
|
|
1186
|
+
isStuckApp: isStuckAppStateError(err)
|
|
843
1187
|
});
|
|
844
|
-
if (
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1188
|
+
if (isStuckAppStateError(err)) {
|
|
1189
|
+
try {
|
|
1190
|
+
this._sessions.delete(resolvedConnectId);
|
|
1191
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
1192
|
+
this.connector.reset?.();
|
|
1193
|
+
} catch {
|
|
1194
|
+
}
|
|
1195
|
+
debugLog(
|
|
1196
|
+
"[LedgerAdapter] stuck-app retry: method=",
|
|
1197
|
+
method,
|
|
1198
|
+
"delayMs=",
|
|
1199
|
+
_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
|
|
1200
|
+
"_tag=",
|
|
1201
|
+
err?._tag
|
|
1202
|
+
);
|
|
1203
|
+
try {
|
|
1204
|
+
const retryResult = await this._retryAfterStuckApp(
|
|
1205
|
+
resolvedConnectId,
|
|
1206
|
+
method,
|
|
1207
|
+
effectiveParams,
|
|
1208
|
+
signal,
|
|
1209
|
+
err,
|
|
1210
|
+
fingerprint
|
|
1211
|
+
);
|
|
1212
|
+
debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
|
|
1213
|
+
return retryResult;
|
|
1214
|
+
} catch (retryErr) {
|
|
1215
|
+
if (signal.aborted) throw retryErr;
|
|
1216
|
+
if (isStuckAppStateError(retryErr)) {
|
|
1217
|
+
debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
|
|
1218
|
+
throw err;
|
|
1219
|
+
}
|
|
1220
|
+
debugLog(
|
|
1221
|
+
"[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
|
|
1222
|
+
method,
|
|
1223
|
+
"retryErrTag=",
|
|
1224
|
+
retryErr?._tag
|
|
1225
|
+
);
|
|
1226
|
+
throw retryErr;
|
|
1227
|
+
}
|
|
848
1228
|
}
|
|
849
|
-
if (isDeviceLockedError(err)) {
|
|
850
|
-
|
|
851
|
-
_LedgerAdapter.
|
|
852
|
-
|
|
1229
|
+
if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
|
|
1230
|
+
let lastErr = err;
|
|
1231
|
+
for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
|
|
1232
|
+
if (signal.aborted) throw lastErr;
|
|
1233
|
+
try {
|
|
1234
|
+
this._sessions.delete(resolvedConnectId);
|
|
1235
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
1236
|
+
if (isDeviceDisconnectedError(lastErr)) {
|
|
1237
|
+
try {
|
|
1238
|
+
this.connector.reset?.();
|
|
1239
|
+
} catch {
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const reConnectId = await this.ensureConnected(resolvedConnectId, signal);
|
|
1243
|
+
const reSessionId = this._sessions.get(reConnectId);
|
|
1244
|
+
if (!reSessionId) throw lastErr;
|
|
1245
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1246
|
+
const fp = await this._abortable(
|
|
1247
|
+
signal,
|
|
1248
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1249
|
+
reSessionId,
|
|
1250
|
+
fingerprint.deviceId,
|
|
1251
|
+
fingerprint.chain
|
|
1252
|
+
)
|
|
1253
|
+
);
|
|
1254
|
+
if (!fp.success) {
|
|
1255
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1256
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return await this._abortable(
|
|
1261
|
+
signal,
|
|
1262
|
+
this.connector.call(reSessionId, method, effectiveParams)
|
|
1263
|
+
);
|
|
1264
|
+
} catch (retryErr) {
|
|
1265
|
+
if (signal.aborted) throw retryErr;
|
|
1266
|
+
lastErr = retryErr;
|
|
1267
|
+
const canRetry = attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET - 1 && (isDeviceLockedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isDeviceDisconnectedError(retryErr));
|
|
1268
|
+
if (!canRetry) {
|
|
1269
|
+
throw retryErr;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
throw lastErr;
|
|
853
1274
|
}
|
|
854
1275
|
if (isTimeoutError(err)) {
|
|
855
1276
|
debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
|
|
856
1277
|
this._discoveredDevices.delete(resolvedConnectId);
|
|
857
|
-
return this._retryWithFreshConnection(
|
|
1278
|
+
return this._retryWithFreshConnection(
|
|
1279
|
+
resolvedConnectId,
|
|
1280
|
+
method,
|
|
1281
|
+
effectiveParams,
|
|
1282
|
+
signal,
|
|
1283
|
+
err,
|
|
1284
|
+
fingerprint
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
if (isConnectionLevelError(err)) {
|
|
1288
|
+
debugLog("[LedgerAdapter] connection-level fail-closed reset");
|
|
1289
|
+
this._sessions.delete(resolvedConnectId);
|
|
1290
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
1291
|
+
this.connector.reset();
|
|
1292
|
+
const codeNum = err?.code;
|
|
1293
|
+
throw Object.assign(err, {
|
|
1294
|
+
code: codeNum ?? HardwareErrorCode2.DeviceDisconnected
|
|
1295
|
+
});
|
|
858
1296
|
}
|
|
859
1297
|
throw err;
|
|
860
1298
|
}
|
|
861
1299
|
}
|
|
862
|
-
/**
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1300
|
+
/**
|
|
1301
|
+
* Stuck-app recovery: pause for the device's UI transition, then retry once.
|
|
1302
|
+
*
|
|
1303
|
+
* Caller has already cleared the session + reset connector. We wait so Stax
|
|
1304
|
+
* finishes its post-CloseApp animation, then go through ensureConnected +
|
|
1305
|
+
* fingerprint check + call exactly once. Caller decides what to do on a
|
|
1306
|
+
* second stuck-app hit.
|
|
1307
|
+
*/
|
|
1308
|
+
async _retryAfterStuckApp(resolvedConnectId, method, params, signal, originalErr, fingerprint) {
|
|
1309
|
+
await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
|
|
1310
|
+
const retryConnectId = await this.ensureConnected(resolvedConnectId, signal);
|
|
1311
|
+
const retrySessionId = this._sessions.get(retryConnectId);
|
|
1312
|
+
if (!retrySessionId) throw originalErr;
|
|
1313
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1314
|
+
const fp = await this._abortable(
|
|
1315
|
+
signal,
|
|
1316
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1317
|
+
retrySessionId,
|
|
1318
|
+
fingerprint.deviceId,
|
|
1319
|
+
fingerprint.chain
|
|
1320
|
+
)
|
|
1321
|
+
);
|
|
1322
|
+
if (!fp.success) {
|
|
1323
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1324
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return this._abortable(signal, this.connector.call(retrySessionId, method, params));
|
|
1329
|
+
}
|
|
1330
|
+
_sleepAbortable(ms, signal) {
|
|
1331
|
+
return new Promise((resolve, reject) => {
|
|
1332
|
+
if (signal.aborted) {
|
|
1333
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const timer = setTimeout(() => {
|
|
1337
|
+
signal.removeEventListener("abort", onAbort);
|
|
1338
|
+
resolve();
|
|
1339
|
+
}, ms);
|
|
1340
|
+
const onAbort = () => {
|
|
1341
|
+
clearTimeout(timer);
|
|
1342
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
1343
|
+
};
|
|
1344
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Clear stale session, reconnect, and retry the call.
|
|
1349
|
+
*
|
|
1350
|
+
* Timeout recovery starts with a full connector reset. After an APDU
|
|
1351
|
+
* timeout, DMK/transport state may still emit malformed responses; retrying
|
|
1352
|
+
* on the same DMK can poison the next chain switch.
|
|
1353
|
+
*/
|
|
1354
|
+
async _retryWithFreshConnection(targetConnectId, method, params, signal, originalErr, fingerprint) {
|
|
1355
|
+
this.connector.reset();
|
|
1356
|
+
this._sessions.clear();
|
|
1357
|
+
this._discoveredDevices.clear();
|
|
1358
|
+
this._connectingPromise = null;
|
|
1359
|
+
const retryConnectId = await this.ensureConnected(targetConnectId, signal);
|
|
868
1360
|
const retrySessionId = this._sessions.get(retryConnectId);
|
|
869
1361
|
if (!retrySessionId) {
|
|
870
1362
|
throw originalErr;
|
|
871
1363
|
}
|
|
872
|
-
|
|
1364
|
+
try {
|
|
1365
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1366
|
+
const fp = await this._abortable(
|
|
1367
|
+
signal,
|
|
1368
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1369
|
+
retrySessionId,
|
|
1370
|
+
fingerprint.deviceId,
|
|
1371
|
+
fingerprint.chain
|
|
1372
|
+
)
|
|
1373
|
+
);
|
|
1374
|
+
if (!fp.success) {
|
|
1375
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1376
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return await this._abortable(signal, this.connector.call(retrySessionId, method, params));
|
|
1381
|
+
} catch (retryErr) {
|
|
1382
|
+
if (signal.aborted) throw retryErr;
|
|
1383
|
+
this.connector.reset();
|
|
1384
|
+
this._sessions.clear();
|
|
1385
|
+
this._discoveredDevices.clear();
|
|
1386
|
+
this._connectingPromise = null;
|
|
1387
|
+
if (!isDeviceDisconnectedError(retryErr) && !isTimeoutError(retryErr)) {
|
|
1388
|
+
throw retryErr;
|
|
1389
|
+
}
|
|
1390
|
+
debugLog(
|
|
1391
|
+
"[LedgerAdapter] fresh-session retry still failed; resetting connector and rebuilding DMK"
|
|
1392
|
+
);
|
|
1393
|
+
this.connector.reset();
|
|
1394
|
+
this._sessions.clear();
|
|
1395
|
+
this._discoveredDevices.clear();
|
|
1396
|
+
this._connectingPromise = null;
|
|
1397
|
+
const finalConnectId = await this.ensureConnected(targetConnectId, signal);
|
|
1398
|
+
const finalSessionId = this._sessions.get(finalConnectId);
|
|
1399
|
+
if (!finalSessionId) {
|
|
1400
|
+
throw originalErr;
|
|
1401
|
+
}
|
|
1402
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1403
|
+
const fp = await this._abortable(
|
|
1404
|
+
signal,
|
|
1405
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1406
|
+
finalSessionId,
|
|
1407
|
+
fingerprint.deviceId,
|
|
1408
|
+
fingerprint.chain
|
|
1409
|
+
)
|
|
1410
|
+
);
|
|
1411
|
+
if (!fp.success) {
|
|
1412
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1413
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return this._abortable(signal, this.connector.call(finalSessionId, method, params));
|
|
1418
|
+
}
|
|
873
1419
|
}
|
|
874
1420
|
/**
|
|
875
1421
|
* Ensure OS-level device permission (Bluetooth / USB) before proceeding.
|
|
@@ -883,7 +1429,10 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
883
1429
|
* - No connectId (searchDevices): environment-level permission
|
|
884
1430
|
* - With connectId (business methods): device-level permission
|
|
885
1431
|
*/
|
|
886
|
-
async _ensureDevicePermission(connectId, deviceId) {
|
|
1432
|
+
async _ensureDevicePermission(connectId, deviceId, signal) {
|
|
1433
|
+
if (signal?.aborted) {
|
|
1434
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
1435
|
+
}
|
|
887
1436
|
const transportType = this.activeTransport ?? "hid";
|
|
888
1437
|
const waitPromise = this._uiRegistry.wait(
|
|
889
1438
|
UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
@@ -893,10 +1442,37 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
893
1442
|
type: UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
894
1443
|
payload: { transportType, connectId, deviceId }
|
|
895
1444
|
});
|
|
896
|
-
|
|
1445
|
+
let response;
|
|
1446
|
+
const onAbort = () => {
|
|
1447
|
+
this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_PERMISSION);
|
|
1448
|
+
};
|
|
1449
|
+
try {
|
|
1450
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1451
|
+
response = await waitPromise;
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
if (signal?.aborted) {
|
|
1454
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
1455
|
+
}
|
|
1456
|
+
if (err?._tag === UI_REQUEST_PREEMPTED_TAG) {
|
|
1457
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
1458
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
1459
|
+
payload: {}
|
|
1460
|
+
});
|
|
1461
|
+
throw Object.assign(new Error("Device permission request superseded"), {
|
|
1462
|
+
_tag: ERROR_TAG.UserAborted,
|
|
1463
|
+
code: HardwareErrorCode2.UserAborted,
|
|
1464
|
+
cause: err
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
throw err;
|
|
1468
|
+
} finally {
|
|
1469
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1470
|
+
}
|
|
1471
|
+
const { granted, reason, message } = response;
|
|
897
1472
|
if (!granted) {
|
|
898
|
-
throw Object.assign(new Error("Device permission denied"), {
|
|
899
|
-
code: HardwareErrorCode2.DevicePermissionDenied
|
|
1473
|
+
throw Object.assign(new Error(message ?? "Device permission denied"), {
|
|
1474
|
+
code: HardwareErrorCode2.DevicePermissionDenied,
|
|
1475
|
+
reason
|
|
900
1476
|
});
|
|
901
1477
|
}
|
|
902
1478
|
}
|
|
@@ -906,12 +1482,14 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
906
1482
|
*/
|
|
907
1483
|
errorToFailure(err) {
|
|
908
1484
|
debugError("[LedgerAdapter] error:", err);
|
|
1485
|
+
const tag = err && typeof err === "object" ? err._tag : void 0;
|
|
909
1486
|
if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
|
|
910
1487
|
const e = err;
|
|
911
|
-
|
|
1488
|
+
const params = e.code === HardwareErrorCode2.DevicePermissionDenied && e.reason ? { permissionDeniedReason: e.reason } : void 0;
|
|
1489
|
+
return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName, tag, params);
|
|
912
1490
|
}
|
|
913
1491
|
const mapped = mapLedgerError(err);
|
|
914
|
-
return ledgerFailure(mapped.code, mapped.message, mapped.appName);
|
|
1492
|
+
return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
|
|
915
1493
|
}
|
|
916
1494
|
registerEventListeners() {
|
|
917
1495
|
this.connector.on("device-connect", this.deviceConnectHandler);
|
|
@@ -927,32 +1505,38 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
927
1505
|
// Device info mapping
|
|
928
1506
|
// ---------------------------------------------------------------------------
|
|
929
1507
|
connectorDeviceToDeviceInfo(device) {
|
|
930
|
-
const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
|
|
931
1508
|
return {
|
|
932
1509
|
vendor: "ledger",
|
|
933
1510
|
model: device.model ?? "unknown",
|
|
1511
|
+
modelName: device.modelName,
|
|
934
1512
|
firmwareVersion: "",
|
|
935
1513
|
deviceId: device.deviceId,
|
|
936
1514
|
connectId: device.connectId,
|
|
937
1515
|
label: device.name,
|
|
938
|
-
connectionType:
|
|
1516
|
+
connectionType: this.connector.connectionType,
|
|
1517
|
+
rssi: device.rssi,
|
|
1518
|
+
isConnectable: device.isConnectable,
|
|
1519
|
+
serialNumber: device.serialNumber,
|
|
939
1520
|
capabilities: device.capabilities
|
|
940
1521
|
};
|
|
941
1522
|
}
|
|
942
1523
|
};
|
|
943
|
-
//
|
|
944
|
-
//
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
_LedgerAdapter.
|
|
1524
|
+
// Layer 2 retry budget after connection-class error. Each round delegates
|
|
1525
|
+
// to Layer 1 (which owns the unlock prompt). Layer 2 never emits UI itself.
|
|
1526
|
+
_LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET = 3;
|
|
1527
|
+
// Stuck-app (APDU 0x6901) retry pause. Ledger Stax often returns 6901 when
|
|
1528
|
+
// OpenAppCommand lands mid-transition right after CloseApp; a short wait
|
|
1529
|
+
// lets the device finish its UI animation before we retry once.
|
|
1530
|
+
_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS = 500;
|
|
1531
|
+
// Layer 1 confirm budget. After N failed Confirm cycles (user keeps clicking
|
|
1532
|
+
// Confirm but device never shows up / never unlocks), throw DeviceNotFound
|
|
1533
|
+
// instead of looping forever.
|
|
1534
|
+
_LedgerAdapter.MAX_DOCONNECT_CONFIRMS = 3;
|
|
954
1535
|
var LedgerAdapter = _LedgerAdapter;
|
|
955
1536
|
|
|
1537
|
+
// src/connector/LedgerConnectorBase.ts
|
|
1538
|
+
import { HardwareErrorCode as HardwareErrorCode7 } from "@onekeyfe/hwk-adapter-core";
|
|
1539
|
+
|
|
956
1540
|
// src/device/LedgerDeviceManager.ts
|
|
957
1541
|
var LedgerDeviceManager = class {
|
|
958
1542
|
constructor(dmk) {
|
|
@@ -989,7 +1573,9 @@ var LedgerDeviceManager = class {
|
|
|
989
1573
|
if (resolved) return;
|
|
990
1574
|
resolved = true;
|
|
991
1575
|
this._discovered.clear();
|
|
992
|
-
debugLog(
|
|
1576
|
+
debugLog(
|
|
1577
|
+
`[DMK] enumerate count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
|
|
1578
|
+
);
|
|
993
1579
|
for (const d of devices) {
|
|
994
1580
|
this._discovered.set(d.id, d);
|
|
995
1581
|
}
|
|
@@ -999,8 +1585,10 @@ var LedgerDeviceManager = class {
|
|
|
999
1585
|
devices.map((d) => ({
|
|
1000
1586
|
path: d.id,
|
|
1001
1587
|
type: d.deviceModel.model,
|
|
1588
|
+
modelName: d.deviceModel.name,
|
|
1002
1589
|
name: d.name,
|
|
1003
|
-
transport: d.transport
|
|
1590
|
+
transport: d.transport,
|
|
1591
|
+
rssi: d.rssi
|
|
1004
1592
|
}))
|
|
1005
1593
|
);
|
|
1006
1594
|
} else {
|
|
@@ -1021,8 +1609,10 @@ var LedgerDeviceManager = class {
|
|
|
1021
1609
|
devices.map((d) => ({
|
|
1022
1610
|
path: d.id,
|
|
1023
1611
|
type: d.deviceModel.model,
|
|
1612
|
+
modelName: d.deviceModel.name,
|
|
1024
1613
|
name: d.name,
|
|
1025
|
-
transport: d.transport
|
|
1614
|
+
transport: d.transport,
|
|
1615
|
+
rssi: d.rssi
|
|
1026
1616
|
}))
|
|
1027
1617
|
);
|
|
1028
1618
|
}
|
|
@@ -1046,8 +1636,10 @@ var LedgerDeviceManager = class {
|
|
|
1046
1636
|
descriptor: {
|
|
1047
1637
|
path: d.id,
|
|
1048
1638
|
type: d.deviceModel.model,
|
|
1639
|
+
modelName: d.deviceModel.name,
|
|
1049
1640
|
name: d.name,
|
|
1050
|
-
transport: d.transport
|
|
1641
|
+
transport: d.transport,
|
|
1642
|
+
rssi: d.rssi
|
|
1051
1643
|
}
|
|
1052
1644
|
});
|
|
1053
1645
|
}
|
|
@@ -1066,6 +1658,54 @@ var LedgerDeviceManager = class {
|
|
|
1066
1658
|
this._listenSub?.unsubscribe();
|
|
1067
1659
|
this._listenSub = null;
|
|
1068
1660
|
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Resolve to the latest snapshot of advertising devices observed within
|
|
1663
|
+
* `timeoutMs`. Rewrites `_discovered` so it tracks the live list.
|
|
1664
|
+
*
|
|
1665
|
+
* `listenToAvailableDevices` is a BehaviorSubject — it emits the cached
|
|
1666
|
+
* list on subscribe and *only* re-emits when the list changes. A stable
|
|
1667
|
+
* list of currently-advertising devices therefore produces only the
|
|
1668
|
+
* subscription frame; treating that frame as "fossil to be discarded"
|
|
1669
|
+
* caused devices to flicker in/out of the UI as searches alternated
|
|
1670
|
+
* between "saw a change frame" and "saw only the cached frame".
|
|
1671
|
+
*
|
|
1672
|
+
* The window still gives a chance for the active scan to update the list
|
|
1673
|
+
* (e.g. drop a peripheral that just stopped broadcasting), but if nothing
|
|
1674
|
+
* changes we trust the snapshot — DMK silence on a stable list means
|
|
1675
|
+
* "everything's still there".
|
|
1676
|
+
*/
|
|
1677
|
+
getLiveDevices(timeoutMs = 1500) {
|
|
1678
|
+
debugLog(`[DMK] getLiveDevices() called, timeoutMs=${timeoutMs}`);
|
|
1679
|
+
void this.requestDevice();
|
|
1680
|
+
return new Promise((resolve) => {
|
|
1681
|
+
let done = false;
|
|
1682
|
+
let lastSeen = [];
|
|
1683
|
+
let sub = null;
|
|
1684
|
+
const finish = (devices) => {
|
|
1685
|
+
if (done) return;
|
|
1686
|
+
done = true;
|
|
1687
|
+
sub?.unsubscribe();
|
|
1688
|
+
clearTimeout(timer);
|
|
1689
|
+
this._discovered.clear();
|
|
1690
|
+
for (const d of devices) this._discovered.set(d.id, d);
|
|
1691
|
+
debugLog(
|
|
1692
|
+
`[DMK] getLiveDevices() resolved count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
|
|
1693
|
+
);
|
|
1694
|
+
resolve(devices);
|
|
1695
|
+
};
|
|
1696
|
+
const timer = setTimeout(() => finish(lastSeen), timeoutMs);
|
|
1697
|
+
sub = this._dmk.listenToAvailableDevices({}).subscribe({
|
|
1698
|
+
next: (devices) => {
|
|
1699
|
+
lastSeen = devices;
|
|
1700
|
+
},
|
|
1701
|
+
error: (err) => {
|
|
1702
|
+
debugError("[DMK] getLiveDevices() error:", err);
|
|
1703
|
+
finish(lastSeen);
|
|
1704
|
+
},
|
|
1705
|
+
complete: () => finish(lastSeen)
|
|
1706
|
+
});
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1069
1709
|
requestDevice() {
|
|
1070
1710
|
if (this._discoverySub) {
|
|
1071
1711
|
return Promise.resolve();
|
|
@@ -1083,11 +1723,33 @@ var LedgerDeviceManager = class {
|
|
|
1083
1723
|
});
|
|
1084
1724
|
return Promise.resolve();
|
|
1085
1725
|
}
|
|
1726
|
+
/** Lookup a previously-discovered device's display name. */
|
|
1727
|
+
getDeviceName(deviceId) {
|
|
1728
|
+
return this._discovered.get(deviceId)?.name;
|
|
1729
|
+
}
|
|
1730
|
+
/** Lookup minimal model/signal info from a previously-discovered device. */
|
|
1731
|
+
getDiscoveredDeviceInfo(deviceId) {
|
|
1732
|
+
const d = this._discovered.get(deviceId);
|
|
1733
|
+
if (!d) return void 0;
|
|
1734
|
+
return {
|
|
1735
|
+
model: d.deviceModel?.model,
|
|
1736
|
+
modelName: d.deviceModel?.name,
|
|
1737
|
+
name: d.name,
|
|
1738
|
+
rssi: d.rssi
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
hasDiscoveredDevice(deviceId) {
|
|
1742
|
+
return this._discovered.has(deviceId);
|
|
1743
|
+
}
|
|
1086
1744
|
/** Connect to a previously discovered device. Returns sessionId. */
|
|
1087
1745
|
async connect(deviceId) {
|
|
1088
1746
|
const device = this._discovered.get(deviceId);
|
|
1089
1747
|
if (!device) {
|
|
1090
|
-
|
|
1748
|
+
const err = new Error(
|
|
1749
|
+
`Device "${deviceId}" not found in discovery cache. Call enumerate() or listen() first.`
|
|
1750
|
+
);
|
|
1751
|
+
err._tag = ERROR_TAG.DeviceNotInDiscoveryCache;
|
|
1752
|
+
throw err;
|
|
1091
1753
|
}
|
|
1092
1754
|
const sessionId = await this._dmk.connect({ device });
|
|
1093
1755
|
this._sessions.set(deviceId, sessionId);
|
|
@@ -1126,6 +1788,19 @@ var LedgerDeviceManager = class {
|
|
|
1126
1788
|
this._sessionToDevice.clear();
|
|
1127
1789
|
this._dmk.close();
|
|
1128
1790
|
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Tear down RxJS subscriptions and local state without closing the DMK.
|
|
1793
|
+
* For light-reset paths that want to drop session state but keep the
|
|
1794
|
+
* underlying transport alive for retry. Call this instead of orphaning
|
|
1795
|
+
* the manager — bare null assignment leaks _listenSub / _discoverySub.
|
|
1796
|
+
*/
|
|
1797
|
+
disposeKeepingDmk() {
|
|
1798
|
+
this.stopListening();
|
|
1799
|
+
this.stopDiscovery();
|
|
1800
|
+
this._discovered.clear();
|
|
1801
|
+
this._sessions.clear();
|
|
1802
|
+
this._sessionToDevice.clear();
|
|
1803
|
+
}
|
|
1129
1804
|
};
|
|
1130
1805
|
|
|
1131
1806
|
// src/signer/SignerManager.ts
|
|
@@ -1137,11 +1812,12 @@ import { hexToBytes } from "@onekeyfe/hwk-adapter-core";
|
|
|
1137
1812
|
|
|
1138
1813
|
// src/signer/deviceActionToPromise.ts
|
|
1139
1814
|
import { DeviceActionStatus } from "@ledgerhq/device-management-kit";
|
|
1140
|
-
var
|
|
1141
|
-
function deviceActionToPromise(action, onInteraction, timeoutMs =
|
|
1815
|
+
var IDLE_WATCHDOG_MS = 65e3;
|
|
1816
|
+
function deviceActionToPromise(action, onInteraction, timeoutMs = IDLE_WATCHDOG_MS, onRegisterCanceller) {
|
|
1142
1817
|
return new Promise((resolve, reject) => {
|
|
1143
1818
|
let settled = false;
|
|
1144
1819
|
let lastStep;
|
|
1820
|
+
const observedSteps = [];
|
|
1145
1821
|
let sub;
|
|
1146
1822
|
let timer = null;
|
|
1147
1823
|
const cancelAction = () => {
|
|
@@ -1154,44 +1830,59 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1154
1830
|
} catch {
|
|
1155
1831
|
}
|
|
1156
1832
|
};
|
|
1157
|
-
const
|
|
1833
|
+
const armIdleWatchdog = () => {
|
|
1158
1834
|
if (timer) clearTimeout(timer);
|
|
1159
1835
|
if (timeoutMs > 0) {
|
|
1160
1836
|
timer = setTimeout(() => {
|
|
1161
1837
|
if (!settled) {
|
|
1162
1838
|
settled = true;
|
|
1163
1839
|
cancelAction();
|
|
1164
|
-
reject(
|
|
1840
|
+
reject(
|
|
1841
|
+
new Error(
|
|
1842
|
+
"Ledger transport unresponsive: no DMK state change within the watchdog window. The device may have lost connection."
|
|
1843
|
+
)
|
|
1844
|
+
);
|
|
1165
1845
|
}
|
|
1166
1846
|
}, timeoutMs);
|
|
1167
1847
|
}
|
|
1168
1848
|
};
|
|
1169
|
-
|
|
1849
|
+
armIdleWatchdog();
|
|
1170
1850
|
if (onRegisterCanceller) {
|
|
1171
|
-
onRegisterCanceller(() => {
|
|
1172
|
-
if (settled)
|
|
1851
|
+
onRegisterCanceller((reason) => {
|
|
1852
|
+
if (settled) {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1173
1855
|
settled = true;
|
|
1174
1856
|
if (timer) clearTimeout(timer);
|
|
1175
1857
|
cancelAction();
|
|
1176
|
-
|
|
1858
|
+
const err = new Error(reason?.message ?? "Device action cancelled");
|
|
1859
|
+
if (reason?.code !== void 0) {
|
|
1860
|
+
err.code = reason.code;
|
|
1861
|
+
}
|
|
1862
|
+
if (reason?.tag) {
|
|
1863
|
+
Object.defineProperty(err, "_tag", {
|
|
1864
|
+
value: reason.tag,
|
|
1865
|
+
configurable: true,
|
|
1866
|
+
enumerable: false,
|
|
1867
|
+
writable: true
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
reject(err);
|
|
1177
1871
|
});
|
|
1178
1872
|
}
|
|
1179
|
-
debugLog("[DMK-Observable] subscribing to action.observable...");
|
|
1180
1873
|
sub = action.observable.subscribe({
|
|
1181
1874
|
next: (state) => {
|
|
1182
|
-
if (settled)
|
|
1183
|
-
|
|
1875
|
+
if (settled) {
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
armIdleWatchdog();
|
|
1184
1879
|
const step = state?.intermediateValue?.step;
|
|
1185
|
-
if (step)
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
hasOutput: state.status === DeviceActionStatus.Completed,
|
|
1192
|
-
hasError: state.status === DeviceActionStatus.Error
|
|
1193
|
-
})
|
|
1194
|
-
);
|
|
1880
|
+
if (step) {
|
|
1881
|
+
lastStep = step;
|
|
1882
|
+
if (observedSteps[observedSteps.length - 1] !== step) {
|
|
1883
|
+
observedSteps.push(step);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1195
1886
|
if (state.status === DeviceActionStatus.Completed) {
|
|
1196
1887
|
settled = true;
|
|
1197
1888
|
if (timer) clearTimeout(timer);
|
|
@@ -1203,10 +1894,14 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1203
1894
|
if (timer) clearTimeout(timer);
|
|
1204
1895
|
onInteraction?.("interaction-complete");
|
|
1205
1896
|
sub?.unsubscribe();
|
|
1206
|
-
|
|
1897
|
+
rejectWithStepContext(state.error, lastStep, observedSteps, reject);
|
|
1207
1898
|
} else if (state.status === DeviceActionStatus.Pending && onInteraction) {
|
|
1208
1899
|
const interaction = state.intermediateValue?.requiredUserInteraction;
|
|
1209
1900
|
if (interaction && interaction !== "none") {
|
|
1901
|
+
if (interaction === "unlock-device" && timer) {
|
|
1902
|
+
clearTimeout(timer);
|
|
1903
|
+
timer = null;
|
|
1904
|
+
}
|
|
1210
1905
|
onInteraction(String(interaction));
|
|
1211
1906
|
}
|
|
1212
1907
|
}
|
|
@@ -1216,7 +1911,7 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1216
1911
|
settled = true;
|
|
1217
1912
|
if (timer) clearTimeout(timer);
|
|
1218
1913
|
sub?.unsubscribe();
|
|
1219
|
-
|
|
1914
|
+
rejectWithStepContext(err, lastStep, observedSteps, reject);
|
|
1220
1915
|
}
|
|
1221
1916
|
},
|
|
1222
1917
|
complete: () => {
|
|
@@ -1229,15 +1924,25 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1229
1924
|
});
|
|
1230
1925
|
});
|
|
1231
1926
|
}
|
|
1232
|
-
function
|
|
1233
|
-
if (err && typeof err === "object" && lastStep) {
|
|
1927
|
+
function rejectWithStepContext(err, lastStep, observedSteps, reject) {
|
|
1928
|
+
if (err && typeof err === "object" && (lastStep || observedSteps.length > 0)) {
|
|
1234
1929
|
try {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1930
|
+
if (lastStep) {
|
|
1931
|
+
Object.defineProperty(err, "_lastStep", {
|
|
1932
|
+
value: lastStep,
|
|
1933
|
+
configurable: true,
|
|
1934
|
+
enumerable: false,
|
|
1935
|
+
writable: true
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
if (observedSteps.length > 0) {
|
|
1939
|
+
Object.defineProperty(err, "_deviceActionSteps", {
|
|
1940
|
+
value: [...observedSteps],
|
|
1941
|
+
configurable: true,
|
|
1942
|
+
enumerable: false,
|
|
1943
|
+
writable: true
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1241
1946
|
} catch {
|
|
1242
1947
|
}
|
|
1243
1948
|
}
|
|
@@ -1303,7 +2008,8 @@ var SignerManager = class _SignerManager {
|
|
|
1303
2008
|
async getOrCreate(sessionId) {
|
|
1304
2009
|
debugLog("[DMK] SignerManager.getOrCreate:", { sessionId });
|
|
1305
2010
|
const builder = await this._builderFn({ dmk: this._dmk, sessionId });
|
|
1306
|
-
|
|
2011
|
+
const builderWithContext = builder.withContextModule ? builder.withContextModule(_SignerManager._createContextModule()) : builder;
|
|
2012
|
+
return new SignerEth(builderWithContext.build());
|
|
1307
2013
|
}
|
|
1308
2014
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
|
|
1309
2015
|
invalidate(_sessionId) {
|
|
@@ -1312,10 +2018,24 @@ var SignerManager = class _SignerManager {
|
|
|
1312
2018
|
clearAll() {
|
|
1313
2019
|
}
|
|
1314
2020
|
static _defaultBuilder() {
|
|
1315
|
-
return (args) =>
|
|
1316
|
-
|
|
1317
|
-
|
|
2021
|
+
return (args) => new SignerEthBuilder(args);
|
|
2022
|
+
}
|
|
2023
|
+
static _createContextModule() {
|
|
2024
|
+
const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
|
|
2025
|
+
return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
|
|
2026
|
+
}
|
|
2027
|
+
static wrapBlindSigningReportNonBlocking(contextModule) {
|
|
2028
|
+
const report = contextModule.report.bind(contextModule);
|
|
2029
|
+
contextModule.report = async (params) => {
|
|
2030
|
+
try {
|
|
2031
|
+
void report(params).catch((err) => {
|
|
2032
|
+
debugLog("[DMK] ContextModule.report failed:", err);
|
|
2033
|
+
});
|
|
2034
|
+
} catch (err) {
|
|
2035
|
+
debugLog("[DMK] ContextModule.report failed:", err);
|
|
2036
|
+
}
|
|
1318
2037
|
};
|
|
2038
|
+
return contextModule;
|
|
1319
2039
|
}
|
|
1320
2040
|
};
|
|
1321
2041
|
|
|
@@ -1323,20 +2043,20 @@ var SignerManager = class _SignerManager {
|
|
|
1323
2043
|
import { HardwareErrorCode as HardwareErrorCode3, padHex64, stripHex } from "@onekeyfe/hwk-adapter-core";
|
|
1324
2044
|
|
|
1325
2045
|
// src/connector/chains/utils.ts
|
|
1326
|
-
import { EConnectorInteraction } from "@onekeyfe/hwk-adapter-core";
|
|
2046
|
+
import { EConnectorInteraction as EConnectorInteraction2 } from "@onekeyfe/hwk-adapter-core";
|
|
1327
2047
|
function normalizePath(path) {
|
|
1328
2048
|
return path.startsWith("m/") ? path.slice(2) : path;
|
|
1329
2049
|
}
|
|
1330
2050
|
function collapseSignerInteraction(interaction) {
|
|
1331
2051
|
switch (interaction) {
|
|
1332
2052
|
case "confirm-open-app":
|
|
1333
|
-
return
|
|
2053
|
+
return EConnectorInteraction2.ConfirmOpenApp;
|
|
1334
2054
|
case "unlock-device":
|
|
1335
|
-
return
|
|
2055
|
+
return EConnectorInteraction2.UnlockDevice;
|
|
1336
2056
|
case "interaction-complete":
|
|
1337
|
-
return
|
|
2057
|
+
return EConnectorInteraction2.InteractionComplete;
|
|
1338
2058
|
default:
|
|
1339
|
-
return
|
|
2059
|
+
return EConnectorInteraction2.ConfirmOnDevice;
|
|
1340
2060
|
}
|
|
1341
2061
|
}
|
|
1342
2062
|
|
|
@@ -1425,6 +2145,7 @@ async function _getEthSigner(ctx, sessionId) {
|
|
|
1425
2145
|
const signerManager = await ctx.getSignerManager();
|
|
1426
2146
|
const signer = await signerManager.getOrCreate(sessionId);
|
|
1427
2147
|
signer.onInteraction = (interaction) => {
|
|
2148
|
+
debugLog("[LedgerConnector] evm.onInteraction:", interaction);
|
|
1428
2149
|
ctx.emit("ui-event", {
|
|
1429
2150
|
type: collapseSignerInteraction(interaction),
|
|
1430
2151
|
payload: { sessionId }
|
|
@@ -1700,6 +2421,7 @@ async function _createBtcSigner(ctx, sessionId) {
|
|
|
1700
2421
|
const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
|
|
1701
2422
|
const signer = new SignerBtc(sdkSigner);
|
|
1702
2423
|
signer.onInteraction = (interaction) => {
|
|
2424
|
+
debugLog("[LedgerConnector] btc.onInteraction:", interaction);
|
|
1703
2425
|
ctx.emit("ui-event", {
|
|
1704
2426
|
type: collapseSignerInteraction(interaction),
|
|
1705
2427
|
payload: { sessionId }
|
|
@@ -1870,6 +2592,7 @@ async function _createSolSigner(ctx, sessionId) {
|
|
|
1870
2592
|
const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
|
|
1871
2593
|
const signer = new SignerSol(sdkSigner);
|
|
1872
2594
|
signer.onInteraction = (interaction) => {
|
|
2595
|
+
debugLog("[LedgerConnector] sol.onInteraction:", interaction);
|
|
1873
2596
|
ctx.emit("ui-event", {
|
|
1874
2597
|
type: collapseSignerInteraction(interaction),
|
|
1875
2598
|
payload: { sessionId }
|
|
@@ -1882,11 +2605,11 @@ async function _createSolSigner(ctx, sessionId) {
|
|
|
1882
2605
|
}
|
|
1883
2606
|
|
|
1884
2607
|
// src/connector/chains/tron.ts
|
|
1885
|
-
import { HardwareErrorCode as
|
|
2608
|
+
import { HardwareErrorCode as HardwareErrorCode6 } from "@onekeyfe/hwk-adapter-core";
|
|
1886
2609
|
import Trx from "@ledgerhq/hw-app-trx";
|
|
1887
2610
|
|
|
1888
2611
|
// src/connector/chains/legacyChainCall.ts
|
|
1889
|
-
import { EConnectorInteraction as
|
|
2612
|
+
import { EConnectorInteraction as EConnectorInteraction3 } from "@onekeyfe/hwk-adapter-core";
|
|
1890
2613
|
|
|
1891
2614
|
// src/app/AppManager.ts
|
|
1892
2615
|
import {
|
|
@@ -1895,6 +2618,7 @@ import {
|
|
|
1895
2618
|
OpenAppCommand,
|
|
1896
2619
|
isSuccessCommandResult
|
|
1897
2620
|
} from "@ledgerhq/device-management-kit";
|
|
2621
|
+
import { HardwareErrorCode as HardwareErrorCode5 } from "@onekeyfe/hwk-adapter-core";
|
|
1898
2622
|
var APP_NAME_MAP = {
|
|
1899
2623
|
ETH: "Ethereum",
|
|
1900
2624
|
BTC: "Bitcoin",
|
|
@@ -1966,7 +2690,31 @@ var AppManager = class {
|
|
|
1966
2690
|
debugLog("[AppManager] currentApp:", result.data.name);
|
|
1967
2691
|
return result.data.name;
|
|
1968
2692
|
}
|
|
1969
|
-
|
|
2693
|
+
const errResult = result;
|
|
2694
|
+
const dmkErr = errResult.error ?? {};
|
|
2695
|
+
const original = dmkErr.originalError;
|
|
2696
|
+
debugLog(
|
|
2697
|
+
"[AppManager] _getCurrentApp failed sessionId=",
|
|
2698
|
+
sessionId,
|
|
2699
|
+
"tag=",
|
|
2700
|
+
dmkErr._tag,
|
|
2701
|
+
"errorCode=",
|
|
2702
|
+
dmkErr.errorCode,
|
|
2703
|
+
"message=",
|
|
2704
|
+
dmkErr.message,
|
|
2705
|
+
"originalErrorMessage=",
|
|
2706
|
+
original?.message ?? String(original ?? "")
|
|
2707
|
+
);
|
|
2708
|
+
throw Object.assign(
|
|
2709
|
+
new Error(
|
|
2710
|
+
dmkErr.message ?? "Failed to get current app from device"
|
|
2711
|
+
),
|
|
2712
|
+
{
|
|
2713
|
+
_tag: dmkErr._tag,
|
|
2714
|
+
errorCode: dmkErr.errorCode,
|
|
2715
|
+
originalError: original
|
|
2716
|
+
}
|
|
2717
|
+
);
|
|
1970
2718
|
}
|
|
1971
2719
|
async _openApp(sessionId, appName) {
|
|
1972
2720
|
const result = await this._dmk.sendCommand({
|
|
@@ -1975,9 +2723,11 @@ var AppManager = class {
|
|
|
1975
2723
|
});
|
|
1976
2724
|
if (!isSuccessCommandResult(result)) {
|
|
1977
2725
|
const { statusCode } = result;
|
|
2726
|
+
const hasStatusCode2 = statusCode != null && statusCode !== "";
|
|
1978
2727
|
debugLog("[AppManager] openApp failed:", appName, "statusCode:", statusCode);
|
|
1979
2728
|
throw Object.assign(new Error(`Failed to open "${appName}"`), {
|
|
1980
|
-
_tag:
|
|
2729
|
+
_tag: ERROR_TAG.OpenAppCommand,
|
|
2730
|
+
code: hasStatusCode2 ? void 0 : HardwareErrorCode5.AppNotInstalled,
|
|
1981
2731
|
errorCode: String(statusCode ?? ""),
|
|
1982
2732
|
statusCode,
|
|
1983
2733
|
appName
|
|
@@ -2033,14 +2783,14 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
|
|
|
2033
2783
|
const onAppOpenPrompt = () => {
|
|
2034
2784
|
openAppPromptShown = true;
|
|
2035
2785
|
ctx.emit("ui-event", {
|
|
2036
|
-
type:
|
|
2786
|
+
type: EConnectorInteraction3.ConfirmOpenApp,
|
|
2037
2787
|
payload: { sessionId }
|
|
2038
2788
|
});
|
|
2039
2789
|
};
|
|
2040
2790
|
const closeOpenAppUiIfShown = () => {
|
|
2041
2791
|
if (openAppPromptShown) {
|
|
2042
2792
|
ctx.emit("ui-event", {
|
|
2043
|
-
type:
|
|
2793
|
+
type: EConnectorInteraction3.InteractionComplete,
|
|
2044
2794
|
payload: { sessionId }
|
|
2045
2795
|
});
|
|
2046
2796
|
openAppPromptShown = false;
|
|
@@ -2061,7 +2811,7 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
|
|
|
2061
2811
|
let confirmEmitted = false;
|
|
2062
2812
|
if (needsConfirmation) {
|
|
2063
2813
|
ctx.emit("ui-event", {
|
|
2064
|
-
type:
|
|
2814
|
+
type: EConnectorInteraction3.ConfirmOnDevice,
|
|
2065
2815
|
payload: { sessionId }
|
|
2066
2816
|
});
|
|
2067
2817
|
confirmEmitted = true;
|
|
@@ -2071,7 +2821,7 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
|
|
|
2071
2821
|
} finally {
|
|
2072
2822
|
if (confirmEmitted || openAppPromptShown) {
|
|
2073
2823
|
ctx.emit("ui-event", {
|
|
2074
|
-
type:
|
|
2824
|
+
type: EConnectorInteraction3.InteractionComplete,
|
|
2075
2825
|
payload: { sessionId }
|
|
2076
2826
|
});
|
|
2077
2827
|
openAppPromptShown = false;
|
|
@@ -2159,7 +2909,7 @@ async function tronSignTransaction(ctx, sessionId, params) {
|
|
|
2159
2909
|
if (!params.rawTxHex) {
|
|
2160
2910
|
throw Object.assign(
|
|
2161
2911
|
new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
|
|
2162
|
-
{ code:
|
|
2912
|
+
{ code: HardwareErrorCode6.InvalidParams }
|
|
2163
2913
|
);
|
|
2164
2914
|
}
|
|
2165
2915
|
const path = normalizePath(params.path);
|
|
@@ -2203,6 +2953,10 @@ var METHOD_PREFIX_TO_APP_NAME = {
|
|
|
2203
2953
|
sol: "Solana",
|
|
2204
2954
|
tron: "Tron"
|
|
2205
2955
|
};
|
|
2956
|
+
var HARDWARE_ERROR_CODE_VALUES = new Set(
|
|
2957
|
+
Object.values(HardwareErrorCode7).filter((value) => typeof value === "number")
|
|
2958
|
+
);
|
|
2959
|
+
var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
2206
2960
|
async function defaultLedgerKitImporter(pkg) {
|
|
2207
2961
|
switch (pkg) {
|
|
2208
2962
|
case "@ledgerhq/device-management-kit":
|
|
@@ -2226,16 +2980,6 @@ var LedgerConnectorBase = class {
|
|
|
2226
2980
|
this._dmk = null;
|
|
2227
2981
|
this._eventHandlers = /* @__PURE__ */ new Map();
|
|
2228
2982
|
// ---------------------------------------------------------------------------
|
|
2229
|
-
// ConnectId <-> DMK path mapping
|
|
2230
|
-
//
|
|
2231
|
-
// DMK uses internal paths (BLE MAC, USB UUID) that may change across sessions.
|
|
2232
|
-
// _resolveConnectId() maps these to stable external IDs (BLE: "A58F", USB: same).
|
|
2233
|
-
// This bidirectional map is the SINGLE SOURCE OF TRUTH for all connectId usage.
|
|
2234
|
-
// ---------------------------------------------------------------------------
|
|
2235
|
-
this._connectIdToPath = /* @__PURE__ */ new Map();
|
|
2236
|
-
// "A58F" -> "D5:75:7D:4B:51:E8"
|
|
2237
|
-
this._pathToConnectId = /* @__PURE__ */ new Map();
|
|
2238
|
-
// ---------------------------------------------------------------------------
|
|
2239
2983
|
// Per-session DeviceAction cancellers
|
|
2240
2984
|
//
|
|
2241
2985
|
// Each chain handler registers its active DeviceAction's canceller via
|
|
@@ -2244,10 +2988,23 @@ var LedgerConnectorBase = class {
|
|
|
2244
2988
|
// unsubscribes the observable and releases DMK's IntentQueue slot.
|
|
2245
2989
|
// ---------------------------------------------------------------------------
|
|
2246
2990
|
this._cancellers = /* @__PURE__ */ new Map();
|
|
2991
|
+
// ---------------------------------------------------------------------------
|
|
2992
|
+
// Per-session DMK state subscriptions
|
|
2993
|
+
//
|
|
2994
|
+
// DMK only emits `device-disconnect` on the connector when `disconnect()`
|
|
2995
|
+
// is called explicitly. To pick up autonomous disconnects (USB unplug,
|
|
2996
|
+
// BLE drop, device sleep, DMK transport reset) we subscribe to
|
|
2997
|
+
// `_dmk.getDeviceSessionState({sessionId})` per active session and emit
|
|
2998
|
+
// `device-disconnect` ourselves the moment DMK reports the device went
|
|
2999
|
+
// to NOT_CONNECTED. The subscription is torn down on disconnect /
|
|
3000
|
+
// reset / observable completion to avoid leaks.
|
|
3001
|
+
// ---------------------------------------------------------------------------
|
|
3002
|
+
this._sessionStateSubs = /* @__PURE__ */ new Map();
|
|
2247
3003
|
this._createTransport = createTransport;
|
|
2248
3004
|
this.connectionType = options?.connectionType ?? "usb";
|
|
2249
3005
|
this._providedDmk = options?.dmk;
|
|
2250
3006
|
this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
|
|
3007
|
+
this._requirePreFlightScan = options?.requirePreFlightScan ?? false;
|
|
2251
3008
|
if (this._providedDmk) {
|
|
2252
3009
|
this._initManagers(this._providedDmk);
|
|
2253
3010
|
}
|
|
@@ -2265,38 +3022,23 @@ var LedgerConnectorBase = class {
|
|
|
2265
3022
|
importLedgerKit: this._importLedgerKit
|
|
2266
3023
|
};
|
|
2267
3024
|
}
|
|
2268
|
-
// "D5:75:7D:4B:51:E8" -> "A58F"
|
|
2269
|
-
/** Register a connectId <-> path mapping from a device descriptor. */
|
|
2270
|
-
_registerDeviceId(descriptor) {
|
|
2271
|
-
const connectId = this._resolveConnectId(descriptor);
|
|
2272
|
-
this._connectIdToPath.set(connectId, descriptor.path);
|
|
2273
|
-
this._pathToConnectId.set(descriptor.path, connectId);
|
|
2274
|
-
return connectId;
|
|
2275
|
-
}
|
|
2276
|
-
/** Get DMK path from external connectId. Falls back to connectId itself. */
|
|
2277
|
-
_getPathForConnectId(connectId) {
|
|
2278
|
-
return this._connectIdToPath.get(connectId) ?? connectId;
|
|
2279
|
-
}
|
|
2280
|
-
/** Get external connectId from DMK path. Falls back to path itself. */
|
|
2281
|
-
_getConnectIdForPath(path) {
|
|
2282
|
-
return this._pathToConnectId.get(path) ?? path;
|
|
2283
|
-
}
|
|
2284
3025
|
// ---------------------------------------------------------------------------
|
|
2285
3026
|
// Protected — hooks for subclasses
|
|
2286
3027
|
// ---------------------------------------------------------------------------
|
|
2287
3028
|
/**
|
|
2288
3029
|
* Resolve the connectId for a discovered device descriptor.
|
|
2289
3030
|
* Default: use the DMK path (ephemeral UUID).
|
|
2290
|
-
* Override in subclasses
|
|
3031
|
+
* Override in subclasses only when the public connectId differs from the transport path.
|
|
2291
3032
|
*/
|
|
2292
3033
|
_resolveConnectId(descriptor) {
|
|
2293
3034
|
return descriptor.path;
|
|
2294
3035
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
3036
|
+
/**
|
|
3037
|
+
* Authoritative discovery for this transport. Default = enumerate snapshot
|
|
3038
|
+
* (USB/HID). BLE overrides to drive from a live scan window since DMK's
|
|
3039
|
+
* enumerate() cache survives peripherals going offline.
|
|
3040
|
+
*/
|
|
3041
|
+
async _discoverDescriptors(dm) {
|
|
2300
3042
|
let descriptors = await dm.enumerate();
|
|
2301
3043
|
if (descriptors.length === 0) {
|
|
2302
3044
|
try {
|
|
@@ -2305,92 +3047,276 @@ var LedgerConnectorBase = class {
|
|
|
2305
3047
|
}
|
|
2306
3048
|
descriptors = await dm.enumerate();
|
|
2307
3049
|
}
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
3050
|
+
return descriptors;
|
|
3051
|
+
}
|
|
3052
|
+
// ---------------------------------------------------------------------------
|
|
3053
|
+
// IConnector -- Device discovery
|
|
3054
|
+
//
|
|
3055
|
+
// Discovery never throws on multiple USB devices — it returns the full list.
|
|
3056
|
+
// The "connect exactly one device" rule is enforced upstream: an explicit
|
|
3057
|
+
// connectId connects that device (or fails as not-found), and only the
|
|
3058
|
+
// no-connectId USB path rejects when more than one device is present
|
|
3059
|
+
// (see LedgerAdapter._connectFirstOrSelect).
|
|
3060
|
+
// ---------------------------------------------------------------------------
|
|
3061
|
+
async searchDevices() {
|
|
3062
|
+
const dm = await this._getDeviceManager();
|
|
3063
|
+
const descriptors = await this._discoverDescriptors(dm);
|
|
3064
|
+
const resolvedDescriptors = descriptors.map((d) => ({
|
|
3065
|
+
descriptor: d,
|
|
3066
|
+
connectId: this._resolveConnectId(d)
|
|
3067
|
+
}));
|
|
3068
|
+
const result = resolvedDescriptors.map(({ descriptor: d, connectId }) => ({
|
|
3069
|
+
connectId,
|
|
3070
|
+
deviceId: d.path,
|
|
3071
|
+
name: d.name || d.type || "Ledger",
|
|
3072
|
+
model: d.type,
|
|
3073
|
+
modelName: d.modelName,
|
|
3074
|
+
rssi: d.rssi,
|
|
3075
|
+
isConnectable: d.isConnectable,
|
|
3076
|
+
serialNumber: d.serialNumber
|
|
3077
|
+
}));
|
|
3078
|
+
debugLog(
|
|
3079
|
+
`[DMK] connector.searchDevices() return count=${result.length} ids=[${result.map((r) => r.connectId).join(",")}] paths=[${result.map((r) => r.deviceId).join(",")}]`
|
|
3080
|
+
);
|
|
2317
3081
|
return result;
|
|
2318
3082
|
}
|
|
2319
3083
|
// ---------------------------------------------------------------------------
|
|
2320
3084
|
// IConnector -- Connection
|
|
2321
3085
|
// ---------------------------------------------------------------------------
|
|
2322
3086
|
async connect(deviceId) {
|
|
2323
|
-
const
|
|
2324
|
-
let
|
|
2325
|
-
const dmkPath = deviceId ? this._getPathForConnectId(deviceId) : void 0;
|
|
2326
|
-
let targetPath = dmkPath;
|
|
3087
|
+
const callerSuppliedConnectId = Boolean(deviceId);
|
|
3088
|
+
let targetPath = deviceId;
|
|
2327
3089
|
if (!targetPath) {
|
|
2328
|
-
const
|
|
2329
|
-
if (
|
|
3090
|
+
const discovered = await this.searchDevices();
|
|
3091
|
+
if (discovered.length === 0) {
|
|
2330
3092
|
throw new Error(
|
|
2331
|
-
`No Ledger device found. Make sure the device is connected${this.connectionType
|
|
3093
|
+
`No Ledger device found. Make sure the device is connected${isLedgerBleConnectionType(this.connectionType) ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
|
|
2332
3094
|
);
|
|
2333
3095
|
}
|
|
2334
|
-
targetPath =
|
|
2335
|
-
}
|
|
2336
|
-
const externalConnectId =
|
|
3096
|
+
targetPath = discovered[0].deviceId;
|
|
3097
|
+
}
|
|
3098
|
+
const externalConnectId = targetPath;
|
|
3099
|
+
const HANG_CEILING_MS = 5 * 6e4;
|
|
3100
|
+
const dmConnectWithObserve = async (dm, path) => {
|
|
3101
|
+
if (!isLedgerBleConnectionType(this.connectionType)) return dm.connect(path);
|
|
3102
|
+
let timeoutId;
|
|
3103
|
+
let timedOut = false;
|
|
3104
|
+
const connectPromise = dm.connect(path);
|
|
3105
|
+
const hangPromise = new Promise((_, reject) => {
|
|
3106
|
+
timeoutId = setTimeout(() => {
|
|
3107
|
+
timedOut = true;
|
|
3108
|
+
const err = new Error(
|
|
3109
|
+
`Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
|
|
3110
|
+
);
|
|
3111
|
+
err._tag = ERROR_TAG.BlePairingTimeout;
|
|
3112
|
+
err.code = HardwareErrorCode7.BlePairingTimeout;
|
|
3113
|
+
reject(err);
|
|
3114
|
+
}, HANG_CEILING_MS);
|
|
3115
|
+
});
|
|
3116
|
+
try {
|
|
3117
|
+
return await Promise.race([connectPromise, hangPromise]);
|
|
3118
|
+
} catch (err) {
|
|
3119
|
+
const e = err;
|
|
3120
|
+
debugLog("[DMK] dm.connect rejected \u2014 observed:", {
|
|
3121
|
+
_tag: e?._tag,
|
|
3122
|
+
message: e?.message,
|
|
3123
|
+
errorCode: e?.errorCode,
|
|
3124
|
+
statusCode: e?.statusCode,
|
|
3125
|
+
originalTag: e?.originalError?._tag,
|
|
3126
|
+
originalMessage: e?.originalError?.message
|
|
3127
|
+
});
|
|
3128
|
+
if (timedOut) {
|
|
3129
|
+
void connectPromise.then(
|
|
3130
|
+
(sessionId) => dm.disconnect(sessionId).catch(() => void 0),
|
|
3131
|
+
() => void 0
|
|
3132
|
+
);
|
|
3133
|
+
}
|
|
3134
|
+
throw err;
|
|
3135
|
+
} finally {
|
|
3136
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
const isBleDirectConnect = isLedgerBleConnectionType(this.connectionType) && callerSuppliedConnectId;
|
|
3140
|
+
const throwNotAdvertising = () => {
|
|
3141
|
+
const err = new Error(
|
|
3142
|
+
"Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
|
|
3143
|
+
);
|
|
3144
|
+
err._tag = ERROR_TAG.DeviceNotAdvertising;
|
|
3145
|
+
err.code = HardwareErrorCode7.DeviceNotFound;
|
|
3146
|
+
throw err;
|
|
3147
|
+
};
|
|
2337
3148
|
const doConnect = async (path) => {
|
|
2338
|
-
const
|
|
3149
|
+
const dm = await this._getDeviceManager();
|
|
3150
|
+
if (isBleDirectConnect && !dm.hasDiscoveredDevice(path)) {
|
|
3151
|
+
const live = await dm.getLiveDevices(BLE_CONNECT_SCAN_TIMEOUT_MS);
|
|
3152
|
+
if (this._requirePreFlightScan && !live.some((d) => d.id === path)) {
|
|
3153
|
+
throwNotAdvertising();
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
let sessionId;
|
|
3157
|
+
try {
|
|
3158
|
+
sessionId = await dmConnectWithObserve(dm, path);
|
|
3159
|
+
} catch (err) {
|
|
3160
|
+
if (isBleDirectConnect && err?._tag === ERROR_TAG.DeviceNotInDiscoveryCache) {
|
|
3161
|
+
throwNotAdvertising();
|
|
3162
|
+
}
|
|
3163
|
+
throw err;
|
|
3164
|
+
}
|
|
3165
|
+
try {
|
|
3166
|
+
this._watchSessionState(sessionId, externalConnectId);
|
|
3167
|
+
} catch (subErr) {
|
|
3168
|
+
debugLog("[DMK] state subscription failed during connect; disconnecting session:", subErr);
|
|
3169
|
+
try {
|
|
3170
|
+
await dm.disconnect(sessionId);
|
|
3171
|
+
} catch {
|
|
3172
|
+
}
|
|
3173
|
+
throw subErr;
|
|
3174
|
+
}
|
|
3175
|
+
const info = dm.getDiscoveredDeviceInfo(path);
|
|
2339
3176
|
const session = {
|
|
2340
3177
|
sessionId,
|
|
2341
3178
|
deviceInfo: {
|
|
2342
3179
|
vendor: "ledger",
|
|
2343
|
-
model: "unknown",
|
|
3180
|
+
model: info?.model ?? "unknown",
|
|
3181
|
+
modelName: info?.modelName,
|
|
2344
3182
|
firmwareVersion: "unknown",
|
|
2345
3183
|
deviceId: path,
|
|
2346
3184
|
connectId: externalConnectId,
|
|
2347
3185
|
connectionType: this.connectionType,
|
|
3186
|
+
rssi: info?.rssi,
|
|
2348
3187
|
capabilities: { persistentDeviceIdentity: false }
|
|
2349
3188
|
}
|
|
2350
3189
|
};
|
|
2351
|
-
const
|
|
3190
|
+
const name = dm.getDeviceName(path) ?? "Ledger";
|
|
2352
3191
|
this._emit("device-connect", {
|
|
2353
|
-
device: {
|
|
2354
|
-
connectId: externalConnectId,
|
|
2355
|
-
deviceId: path,
|
|
2356
|
-
name: realName
|
|
2357
|
-
}
|
|
3192
|
+
device: { connectId: externalConnectId, deviceId: path, name }
|
|
2358
3193
|
});
|
|
2359
3194
|
return session;
|
|
2360
3195
|
};
|
|
2361
3196
|
try {
|
|
2362
3197
|
return await doConnect(targetPath);
|
|
2363
|
-
} catch {
|
|
3198
|
+
} catch (err) {
|
|
2364
3199
|
this._resetSignersAndSessions();
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
const descriptors = await dm2.enumerate();
|
|
2370
|
-
if (descriptors.length === 0) {
|
|
2371
|
-
throw new Error(
|
|
2372
|
-
`No Ledger device found after retry. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
|
|
2373
|
-
);
|
|
3200
|
+
if (isLedgerBleConnectionType(this.connectionType)) {
|
|
3201
|
+
const tag = err?._tag;
|
|
3202
|
+
if (isKnownConnectionTag(tag)) {
|
|
3203
|
+
throw err;
|
|
2374
3204
|
}
|
|
2375
|
-
|
|
3205
|
+
const wrapped = new Error(
|
|
3206
|
+
"Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
|
|
3207
|
+
);
|
|
3208
|
+
wrapped._tag = ERROR_TAG.BleGattBondingFailed;
|
|
3209
|
+
wrapped.code = HardwareErrorCode7.BlePairingTimeout;
|
|
3210
|
+
wrapped.originalError = err;
|
|
3211
|
+
throw wrapped;
|
|
2376
3212
|
}
|
|
2377
|
-
|
|
3213
|
+
if (err && typeof err === "object" && err._tag) {
|
|
3214
|
+
const taggedError = err instanceof Error ? err : Object.assign(
|
|
3215
|
+
new Error(err.message ?? "Ledger device error"),
|
|
3216
|
+
err
|
|
3217
|
+
);
|
|
3218
|
+
throw taggedError;
|
|
3219
|
+
}
|
|
3220
|
+
throw new Error("Ledger device appears unplugged. Plug in the device and try again.");
|
|
2378
3221
|
}
|
|
2379
3222
|
}
|
|
2380
3223
|
async disconnect(sessionId) {
|
|
2381
3224
|
if (!this._deviceManager) return;
|
|
2382
3225
|
const deviceId = this._deviceManager.getDeviceId(sessionId);
|
|
2383
3226
|
this._signerManager?.invalidate(sessionId);
|
|
3227
|
+
this._unwatchSessionState(sessionId);
|
|
2384
3228
|
await this._deviceManager.disconnect(sessionId);
|
|
2385
3229
|
if (deviceId) {
|
|
2386
3230
|
this._emit("device-disconnect", { connectId: deviceId });
|
|
2387
3231
|
}
|
|
2388
3232
|
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Subscribe to DMK's per-session state observable so that any autonomous
|
|
3235
|
+
* disconnect (USB unplug, BLE drop, device sleep, transport reset) is
|
|
3236
|
+
* surfaced as a `device-disconnect` event — without this, the upstream
|
|
3237
|
+
* `LedgerAdapter._sessions` map would hold a dead session entry until
|
|
3238
|
+
* the next call hit `DeviceSessionNotFound`.
|
|
3239
|
+
*
|
|
3240
|
+
* Subscribe failure is fatal — see the inline note at the subscribe call.
|
|
3241
|
+
*/
|
|
3242
|
+
_watchSessionState(sessionId, externalConnectId) {
|
|
3243
|
+
const dmk = this._dmk;
|
|
3244
|
+
if (!dmk) return;
|
|
3245
|
+
const previous = this._sessionStateSubs.get(sessionId);
|
|
3246
|
+
if (previous) {
|
|
3247
|
+
try {
|
|
3248
|
+
previous.unsubscribe();
|
|
3249
|
+
} catch {
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
const sub = dmk.getDeviceSessionState({ sessionId }).subscribe({
|
|
3253
|
+
next: (state) => {
|
|
3254
|
+
if (state?.deviceStatus === "NOT CONNECTED") {
|
|
3255
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3256
|
+
}
|
|
3257
|
+
},
|
|
3258
|
+
error: () => {
|
|
3259
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3260
|
+
},
|
|
3261
|
+
complete: () => {
|
|
3262
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
this._sessionStateSubs.set(sessionId, sub);
|
|
3266
|
+
}
|
|
3267
|
+
_unwatchSessionState(sessionId) {
|
|
3268
|
+
const sub = this._sessionStateSubs.get(sessionId);
|
|
3269
|
+
if (!sub) return;
|
|
3270
|
+
try {
|
|
3271
|
+
sub.unsubscribe();
|
|
3272
|
+
} catch {
|
|
3273
|
+
}
|
|
3274
|
+
this._sessionStateSubs.delete(sessionId);
|
|
3275
|
+
}
|
|
3276
|
+
_handleAutonomousDisconnect(sessionId, externalConnectId) {
|
|
3277
|
+
if (!this._sessionStateSubs.has(sessionId)) return;
|
|
3278
|
+
debugLog(
|
|
3279
|
+
"[DMK] autonomous disconnect detected \u2014 sessionId:",
|
|
3280
|
+
sessionId,
|
|
3281
|
+
"connectId:",
|
|
3282
|
+
externalConnectId
|
|
3283
|
+
);
|
|
3284
|
+
this._unwatchSessionState(sessionId);
|
|
3285
|
+
this._signerManager?.invalidate(sessionId);
|
|
3286
|
+
this._cancellers.get(sessionId)?.({
|
|
3287
|
+
code: HardwareErrorCode7.DeviceDisconnected,
|
|
3288
|
+
tag: "DeviceDisconnected",
|
|
3289
|
+
message: "Device disconnected"
|
|
3290
|
+
});
|
|
3291
|
+
this._cancellers.delete(sessionId);
|
|
3292
|
+
this._emit("device-disconnect", { connectId: externalConnectId });
|
|
3293
|
+
}
|
|
2389
3294
|
// ---------------------------------------------------------------------------
|
|
2390
3295
|
// IConnector -- Method dispatch
|
|
2391
3296
|
// ---------------------------------------------------------------------------
|
|
2392
3297
|
async call(sessionId, method, params) {
|
|
2393
3298
|
debugLog("[DMK] call:", method, JSON.stringify(params));
|
|
3299
|
+
try {
|
|
3300
|
+
return await this._dispatch(sessionId, method, params);
|
|
3301
|
+
} catch (err) {
|
|
3302
|
+
if (isAppStuckByApdu(err)) {
|
|
3303
|
+
throw Object.assign(new Error("Ledger app is unresponsive"), {
|
|
3304
|
+
code: HardwareErrorCode7.DeviceAppStuck,
|
|
3305
|
+
_tag: ERROR_TAG.DeviceAppStuck,
|
|
3306
|
+
originalError: err
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
if (isTransportStuck(err)) {
|
|
3310
|
+
throw Object.assign(new Error("Device communication interrupted, please retry"), {
|
|
3311
|
+
code: HardwareErrorCode7.TransportError,
|
|
3312
|
+
_tag: ERROR_TAG.DeviceTransportStuck,
|
|
3313
|
+
originalError: err
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
throw err;
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
async _dispatch(sessionId, method, params) {
|
|
2394
3320
|
const ctx = this._ctxForMethod(method);
|
|
2395
3321
|
switch (method) {
|
|
2396
3322
|
// EVM
|
|
@@ -2530,14 +3456,13 @@ var LedgerConnectorBase = class {
|
|
|
2530
3456
|
}
|
|
2531
3457
|
/**
|
|
2532
3458
|
* Replace an old session with a new one after app switch.
|
|
2533
|
-
*
|
|
2534
|
-
* and emits device-connect so the adapter updates its _sessions Map.
|
|
3459
|
+
* Emits device-connect so the adapter updates its _sessions Map.
|
|
2535
3460
|
*/
|
|
2536
3461
|
_replaceSession(oldSessionId, _newSessionId) {
|
|
2537
3462
|
const dm = this._deviceManager;
|
|
2538
3463
|
if (!dm) return;
|
|
2539
3464
|
const oldDeviceId = dm.getDeviceId(oldSessionId);
|
|
2540
|
-
const connectId = oldDeviceId
|
|
3465
|
+
const connectId = oldDeviceId;
|
|
2541
3466
|
this._signerManager?.invalidate(oldSessionId);
|
|
2542
3467
|
if (connectId) {
|
|
2543
3468
|
this._emit("device-connect", {
|
|
@@ -2550,13 +3475,18 @@ var LedgerConnectorBase = class {
|
|
|
2550
3475
|
}
|
|
2551
3476
|
}
|
|
2552
3477
|
/**
|
|
2553
|
-
* Light reset: clear signer/session state but keep DMK
|
|
3478
|
+
* Light reset: clear signer/session state but keep DMK alive.
|
|
2554
3479
|
* Used by connect() retry — we want to re-discover with the same transport.
|
|
3480
|
+
*
|
|
3481
|
+
* Note: drops the device manager but tears down its RxJS subs first via
|
|
3482
|
+
* disposeKeepingDmk(). Bare null-assignment would leak _listenSub /
|
|
3483
|
+
* _discoverySub and double-scan after the next _initManagers().
|
|
2555
3484
|
*/
|
|
2556
3485
|
_resetSignersAndSessions() {
|
|
2557
3486
|
debugLog("[DMK] _resetSignersAndSessions called");
|
|
2558
3487
|
this._signerManager?.clearAll();
|
|
2559
3488
|
this._signerManager = null;
|
|
3489
|
+
this._deviceManager?.disposeKeepingDmk();
|
|
2560
3490
|
this._deviceManager = null;
|
|
2561
3491
|
}
|
|
2562
3492
|
_resetAll() {
|
|
@@ -2568,13 +3498,18 @@ var LedgerConnectorBase = class {
|
|
|
2568
3498
|
}
|
|
2569
3499
|
}
|
|
2570
3500
|
this._cancellers.clear();
|
|
3501
|
+
for (const sub of this._sessionStateSubs.values()) {
|
|
3502
|
+
try {
|
|
3503
|
+
sub.unsubscribe();
|
|
3504
|
+
} catch {
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
this._sessionStateSubs.clear();
|
|
2571
3508
|
this._signerManager?.clearAll();
|
|
2572
3509
|
this._deviceManager?.dispose();
|
|
2573
3510
|
this._deviceManager = null;
|
|
2574
3511
|
this._signerManager = null;
|
|
2575
3512
|
this._dmk = null;
|
|
2576
|
-
this._connectIdToPath.clear();
|
|
2577
|
-
this._pathToConnectId.clear();
|
|
2578
3513
|
}
|
|
2579
3514
|
// ---------------------------------------------------------------------------
|
|
2580
3515
|
// Private -- Events
|
|
@@ -2608,15 +3543,21 @@ var LedgerConnectorBase = class {
|
|
|
2608
3543
|
};
|
|
2609
3544
|
}
|
|
2610
3545
|
_wrapError(err, opts) {
|
|
2611
|
-
const mapped = mapLedgerError(err, opts);
|
|
2612
|
-
const error = new Error(mapped.message);
|
|
2613
3546
|
const src = err && typeof err === "object" ? err : {};
|
|
3547
|
+
const hasSerializedCode = typeof src.code === "number" && HARDWARE_ERROR_CODE_VALUES.has(src.code);
|
|
3548
|
+
const mapped = hasSerializedCode ? {
|
|
3549
|
+
code: src.code,
|
|
3550
|
+
message: typeof src.message === "string" ? src.message : "Unknown Ledger error",
|
|
3551
|
+
appName: typeof src.appName === "string" ? src.appName : opts?.defaultAppName
|
|
3552
|
+
} : mapLedgerError(err, opts);
|
|
3553
|
+
const error = new Error(mapped.message);
|
|
2614
3554
|
Object.assign(error, {
|
|
2615
3555
|
code: mapped.code,
|
|
2616
3556
|
appName: mapped.appName,
|
|
2617
3557
|
_tag: src._tag,
|
|
2618
3558
|
errorCode: src.errorCode,
|
|
2619
|
-
_lastStep: src._lastStep
|
|
3559
|
+
_lastStep: src._lastStep,
|
|
3560
|
+
_deviceActionSteps: src._deviceActionSteps
|
|
2620
3561
|
});
|
|
2621
3562
|
Object.defineProperty(error, "originalError", {
|
|
2622
3563
|
value: err,
|
|
@@ -2627,13 +3568,6 @@ var LedgerConnectorBase = class {
|
|
|
2627
3568
|
return error;
|
|
2628
3569
|
}
|
|
2629
3570
|
};
|
|
2630
|
-
|
|
2631
|
-
// src/utils/bleIdentity.ts
|
|
2632
|
-
function extractBleHexId(name) {
|
|
2633
|
-
if (!name) return void 0;
|
|
2634
|
-
const match = name.match(/\b([0-9A-Fa-f]{4})$/);
|
|
2635
|
-
return match ? match[1].toUpperCase() : void 0;
|
|
2636
|
-
}
|
|
2637
3571
|
export {
|
|
2638
3572
|
AppManager,
|
|
2639
3573
|
DmkTransport,
|
|
@@ -2644,12 +3578,15 @@ export {
|
|
|
2644
3578
|
SignerEth,
|
|
2645
3579
|
SignerManager,
|
|
2646
3580
|
SignerSol,
|
|
3581
|
+
debugLog,
|
|
2647
3582
|
deviceActionToPromise,
|
|
2648
|
-
extractBleHexId,
|
|
2649
|
-
isDebugEnabled,
|
|
2650
3583
|
isDeviceLockedError,
|
|
3584
|
+
isLedgerBleConnectionType,
|
|
3585
|
+
isLedgerBleDescriptor,
|
|
3586
|
+
isLedgerDmkBleTransport,
|
|
2651
3587
|
ledgerFailure,
|
|
2652
3588
|
mapLedgerError,
|
|
2653
|
-
|
|
3589
|
+
offSdkEvent,
|
|
3590
|
+
onSdkEvent
|
|
2654
3591
|
};
|
|
2655
3592
|
//# sourceMappingURL=index.mjs.map
|