@onekeyfe/hwk-ledger-adapter 1.1.26-alpha.9 → 1.1.26
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 +250 -164
- package/dist/index.d.ts +250 -164
- package/dist/index.js +1350 -421
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1351 -423
- 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,26 @@ import {
|
|
|
14
16
|
|
|
15
17
|
// src/errors.ts
|
|
16
18
|
import { HardwareErrorCode, enrichErrorMessage } from "@onekeyfe/hwk-adapter-core";
|
|
17
|
-
function ledgerFailure(code, error, appName) {
|
|
19
|
+
function ledgerFailure(code, error, appName, tag, params) {
|
|
18
20
|
const payload = { error, code };
|
|
19
21
|
if (appName !== void 0) payload.appName = appName;
|
|
22
|
+
if (tag !== void 0) payload._tag = tag;
|
|
23
|
+
if (params !== void 0) payload.params = params;
|
|
20
24
|
return { success: false, payload };
|
|
21
25
|
}
|
|
22
26
|
var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
|
|
23
27
|
var USER_REJECTED_CODES = /* @__PURE__ */ new Set(["6985", "27013"]);
|
|
24
28
|
var WRONG_APP_CODES = /* @__PURE__ */ new Set(["6e00", "28160", "6d00", "27904", "6a83", "27267"]);
|
|
25
29
|
var APP_NOT_INSTALLED_CODES = /* @__PURE__ */ new Set(["6807", "26631"]);
|
|
26
|
-
var
|
|
30
|
+
var STEP_BLIND_SIGN_TRANSACTION_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
|
|
27
31
|
function getEthAppErrorCode(err) {
|
|
28
32
|
if (!err || typeof err !== "object") return null;
|
|
29
33
|
const e = err;
|
|
30
|
-
if (e._tag ===
|
|
34
|
+
if (e._tag === ERROR_TAG.EthAppCommand && typeof e.errorCode === "string") {
|
|
31
35
|
return e.errorCode.toLowerCase();
|
|
32
36
|
}
|
|
33
37
|
const orig = e.originalError;
|
|
34
|
-
if (orig?._tag ===
|
|
38
|
+
if (orig?._tag === ERROR_TAG.EthAppCommand && typeof orig.errorCode === "string") {
|
|
35
39
|
return orig.errorCode.toLowerCase();
|
|
36
40
|
}
|
|
37
41
|
return null;
|
|
@@ -88,13 +92,8 @@ function mapBtcAppError(hex) {
|
|
|
88
92
|
return null;
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
|
-
function
|
|
95
|
+
function mapEthAppErrorCode(ethCode) {
|
|
92
96
|
switch (ethCode) {
|
|
93
|
-
case "6a80":
|
|
94
|
-
if (lastStep === STEP_BLIND_SIGN_FALLBACK) {
|
|
95
|
-
return HardwareErrorCode.EvmBlindSigningRequired;
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
97
|
case "6984":
|
|
99
98
|
return HardwareErrorCode.EvmClearSignPluginMissing;
|
|
100
99
|
case "6a84":
|
|
@@ -107,16 +106,133 @@ function mapEthAppError(ethCode, lastStep) {
|
|
|
107
106
|
return null;
|
|
108
107
|
}
|
|
109
108
|
}
|
|
109
|
+
function classifyEthAppError(err) {
|
|
110
|
+
const ethCode = getEthAppErrorCode(err);
|
|
111
|
+
if (!ethCode) return null;
|
|
112
|
+
if (ethCode === "6a80") {
|
|
113
|
+
return hasBlindSignFallbackStep(err) ? HardwareErrorCode.EvmBlindSigningRequired : null;
|
|
114
|
+
}
|
|
115
|
+
return mapEthAppErrorCode(ethCode);
|
|
116
|
+
}
|
|
117
|
+
var ERROR_TAG = {
|
|
118
|
+
// SDK-mint
|
|
119
|
+
DeviceNotAdvertising: "DeviceNotAdvertisingError",
|
|
120
|
+
// BLE scan miss
|
|
121
|
+
DeviceNotInDiscoveryCache: "DeviceNotInDiscoveryCacheError",
|
|
122
|
+
// dm.connect() before enumerate
|
|
123
|
+
BlePairingTimeout: "BlePairingTimeoutError",
|
|
124
|
+
// SMP 30s timeout
|
|
125
|
+
BleGattBondingFailed: "BleGattBondingFailedError",
|
|
126
|
+
// other GATT failure
|
|
127
|
+
UserAborted: "UserAborted",
|
|
128
|
+
DeviceAppStuck: "DeviceAppStuck",
|
|
129
|
+
// chain app wedged (APDU 0x6901)
|
|
130
|
+
DeviceTransportStuck: "DeviceTransportStuck",
|
|
131
|
+
// DMK transport queue wedged
|
|
132
|
+
// DMK-reuse (DMK throws same string; we synthesize too)
|
|
133
|
+
DeviceLocked: "DeviceLockedError",
|
|
134
|
+
DeviceNotRecognized: "DeviceNotRecognizedError",
|
|
135
|
+
DeviceSessionNotFound: "DeviceSessionNotFound",
|
|
136
|
+
OpenAppCommand: "OpenAppCommandError",
|
|
137
|
+
// DMK-only (read only)
|
|
138
|
+
EthAppCommand: "EthAppCommandError",
|
|
139
|
+
UserRefusedOnDevice: "UserRefusedOnDevice",
|
|
140
|
+
WrongAppOpened: "WrongAppOpenedError",
|
|
141
|
+
InvalidStatusWord: "InvalidStatusWordError",
|
|
142
|
+
AlreadySendingApdu: "AlreadySendingApduError",
|
|
143
|
+
UnknownDeviceExchange: "UnknownDeviceExchangeError",
|
|
144
|
+
NoAccessibleDevice: "NoAccessibleDeviceError",
|
|
145
|
+
UnknownDevice: "UnknownDeviceError",
|
|
146
|
+
DeviceSessionRefresher: "DeviceSessionRefresherError",
|
|
147
|
+
DeviceNotInitialized: "DeviceNotInitializedError",
|
|
148
|
+
OpeningConnection: "OpeningConnectionError",
|
|
149
|
+
DeviceDisconnectedBeforeSendingApdu: "DeviceDisconnectedBeforeSendingApdu",
|
|
150
|
+
DeviceDisconnectedWhileSending: "DeviceDisconnectedWhileSendingError",
|
|
151
|
+
Disconnect: "DisconnectError",
|
|
152
|
+
ReconnectionFailed: "ReconnectionFailedError",
|
|
153
|
+
WebHIDDisconnect: "WebHIDDisconnectError",
|
|
154
|
+
// ble-plx surfaces this when GATT notification setup fails — typical
|
|
155
|
+
// outcome when the user doesn't confirm pairing on the device, or the
|
|
156
|
+
// existing bond is invalid. Observed in production after ~30s.
|
|
157
|
+
PairingRefused: "PairingRefusedError"
|
|
158
|
+
};
|
|
110
159
|
function isDeviceLockedError(err) {
|
|
111
160
|
if (!err || typeof err !== "object") return false;
|
|
112
161
|
const e = err;
|
|
113
162
|
if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
|
|
114
163
|
if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
|
|
115
|
-
if (e._tag ===
|
|
164
|
+
if (e._tag === ERROR_TAG.DeviceLocked) return true;
|
|
116
165
|
if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
|
|
117
166
|
if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
|
|
118
167
|
return false;
|
|
119
168
|
}
|
|
169
|
+
function isDeviceNotAdvertisingError(err) {
|
|
170
|
+
if (!err || typeof err !== "object") return false;
|
|
171
|
+
return err._tag === ERROR_TAG.DeviceNotAdvertising;
|
|
172
|
+
}
|
|
173
|
+
var PAIRING_FAILURE_TAGS = /* @__PURE__ */ new Set([
|
|
174
|
+
ERROR_TAG.BlePairingTimeout,
|
|
175
|
+
ERROR_TAG.BleGattBondingFailed,
|
|
176
|
+
ERROR_TAG.PairingRefused
|
|
177
|
+
]);
|
|
178
|
+
function isBlePairingFailureError(err) {
|
|
179
|
+
if (!err || typeof err !== "object") return false;
|
|
180
|
+
const tag = err._tag;
|
|
181
|
+
if (tag && PAIRING_FAILURE_TAGS.has(tag)) return true;
|
|
182
|
+
const orig = err.originalError;
|
|
183
|
+
if (orig != null && isBlePairingFailureError(orig)) return true;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
|
|
187
|
+
ERROR_TAG.DeviceLocked,
|
|
188
|
+
ERROR_TAG.DeviceNotAdvertising,
|
|
189
|
+
ERROR_TAG.BlePairingTimeout,
|
|
190
|
+
ERROR_TAG.BleGattBondingFailed,
|
|
191
|
+
ERROR_TAG.PairingRefused,
|
|
192
|
+
ERROR_TAG.DeviceNotRecognized,
|
|
193
|
+
ERROR_TAG.NoAccessibleDevice,
|
|
194
|
+
ERROR_TAG.UnknownDevice,
|
|
195
|
+
ERROR_TAG.DeviceSessionNotFound,
|
|
196
|
+
ERROR_TAG.DeviceSessionRefresher,
|
|
197
|
+
ERROR_TAG.DeviceNotInitialized,
|
|
198
|
+
ERROR_TAG.OpeningConnection,
|
|
199
|
+
ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
|
|
200
|
+
ERROR_TAG.DeviceDisconnectedWhileSending,
|
|
201
|
+
ERROR_TAG.Disconnect,
|
|
202
|
+
ERROR_TAG.ReconnectionFailed,
|
|
203
|
+
ERROR_TAG.WebHIDDisconnect
|
|
204
|
+
]);
|
|
205
|
+
var DEVICE_NOT_FOUND_TAGS = /* @__PURE__ */ new Set([
|
|
206
|
+
ERROR_TAG.NoAccessibleDevice,
|
|
207
|
+
ERROR_TAG.UnknownDevice,
|
|
208
|
+
ERROR_TAG.DeviceNotInitialized,
|
|
209
|
+
// SDK-internal: dm.connect() called before _discovered was populated.
|
|
210
|
+
// Map to DeviceNotFound so non-BLE-direct paths get a sensible error code.
|
|
211
|
+
ERROR_TAG.DeviceNotInDiscoveryCache
|
|
212
|
+
]);
|
|
213
|
+
var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OpeningConnection]);
|
|
214
|
+
var DEVICE_DISCONNECTED_TAGS = /* @__PURE__ */ new Set([
|
|
215
|
+
ERROR_TAG.DeviceNotRecognized,
|
|
216
|
+
ERROR_TAG.DeviceSessionNotFound,
|
|
217
|
+
ERROR_TAG.DeviceSessionRefresher,
|
|
218
|
+
ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
|
|
219
|
+
ERROR_TAG.DeviceDisconnectedWhileSending,
|
|
220
|
+
ERROR_TAG.Disconnect,
|
|
221
|
+
ERROR_TAG.ReconnectionFailed,
|
|
222
|
+
ERROR_TAG.WebHIDDisconnect
|
|
223
|
+
]);
|
|
224
|
+
function isConnectionLevelError(err) {
|
|
225
|
+
if (!err || typeof err !== "object") return false;
|
|
226
|
+
const tag = err._tag;
|
|
227
|
+
if (tag && CONNECTION_LEVEL_TAGS.has(tag)) return true;
|
|
228
|
+
const e = err;
|
|
229
|
+
if (e.originalError != null && isConnectionLevelError(e.originalError)) return true;
|
|
230
|
+
if (e.error != null && e._tag && isConnectionLevelError(e.error)) return true;
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
function isKnownConnectionTag(tag) {
|
|
234
|
+
return typeof tag === "string" && CONNECTION_LEVEL_TAGS.has(tag);
|
|
235
|
+
}
|
|
120
236
|
function hasStatusCode(err, codeSet) {
|
|
121
237
|
if (!err || typeof err !== "object") return false;
|
|
122
238
|
const e = err;
|
|
@@ -126,10 +242,39 @@ function hasStatusCode(err, codeSet) {
|
|
|
126
242
|
if (e.error != null && e._tag && hasStatusCode(e.error, codeSet)) return true;
|
|
127
243
|
return false;
|
|
128
244
|
}
|
|
245
|
+
function hasBlindSignFallbackStep(err) {
|
|
246
|
+
if (!err || typeof err !== "object") return false;
|
|
247
|
+
const e = err;
|
|
248
|
+
if (e._lastStep === STEP_BLIND_SIGN_TRANSACTION_FALLBACK) return true;
|
|
249
|
+
if (Array.isArray(e._deviceActionSteps) && e._deviceActionSteps.includes(STEP_BLIND_SIGN_TRANSACTION_FALLBACK)) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (e.originalError != null && hasBlindSignFallbackStep(e.originalError)) return true;
|
|
253
|
+
if (e.error != null && e._tag && hasBlindSignFallbackStep(e.error)) return true;
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
function isDeviceNotFoundError(err) {
|
|
257
|
+
if (!err || typeof err !== "object") return false;
|
|
258
|
+
const tag = err._tag;
|
|
259
|
+
if (tag && DEVICE_NOT_FOUND_TAGS.has(tag)) return true;
|
|
260
|
+
const e = err;
|
|
261
|
+
if (e.originalError != null && isDeviceNotFoundError(e.originalError)) return true;
|
|
262
|
+
if (e.error != null && e._tag && isDeviceNotFoundError(e.error)) return true;
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
function isDeviceBusyError(err) {
|
|
266
|
+
if (!err || typeof err !== "object") return false;
|
|
267
|
+
const tag = err._tag;
|
|
268
|
+
if (tag && DEVICE_BUSY_TAGS.has(tag)) return true;
|
|
269
|
+
const e = err;
|
|
270
|
+
if (e.originalError != null && isDeviceBusyError(e.originalError)) return true;
|
|
271
|
+
if (e.error != null && e._tag && isDeviceBusyError(e.error)) return true;
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
129
274
|
function isUserRejectedError(err) {
|
|
130
275
|
if (!err || typeof err !== "object") return false;
|
|
131
276
|
const e = err;
|
|
132
|
-
if (e._tag ===
|
|
277
|
+
if (e._tag === ERROR_TAG.UserRefusedOnDevice) return true;
|
|
133
278
|
if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
|
|
134
279
|
if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
|
|
135
280
|
return false;
|
|
@@ -137,7 +282,7 @@ function isUserRejectedError(err) {
|
|
|
137
282
|
function isWrongAppError(err) {
|
|
138
283
|
if (!err || typeof err !== "object") return false;
|
|
139
284
|
const e = err;
|
|
140
|
-
if (e._tag ===
|
|
285
|
+
if (e._tag === ERROR_TAG.WrongAppOpened || e._tag === ERROR_TAG.InvalidStatusWord) {
|
|
141
286
|
if (hasStatusCode(err, WRONG_APP_CODES)) return true;
|
|
142
287
|
}
|
|
143
288
|
if (typeof e.message === "string") {
|
|
@@ -151,7 +296,6 @@ function isWrongAppError(err) {
|
|
|
151
296
|
function isAppNotInstalledError(err) {
|
|
152
297
|
if (!err || typeof err !== "object") return false;
|
|
153
298
|
const e = err;
|
|
154
|
-
if (e._tag === "OpenAppCommandError") return true;
|
|
155
299
|
if (typeof e.message === "string" && /unknown application/i.test(e.message)) return true;
|
|
156
300
|
if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
|
|
157
301
|
return false;
|
|
@@ -159,7 +303,8 @@ function isAppNotInstalledError(err) {
|
|
|
159
303
|
function isDeviceDisconnectedError(err) {
|
|
160
304
|
if (!err || typeof err !== "object") return false;
|
|
161
305
|
const e = err;
|
|
162
|
-
|
|
306
|
+
const tag = e._tag;
|
|
307
|
+
if (typeof tag === "string" && DEVICE_DISCONNECTED_TAGS.has(tag)) return true;
|
|
163
308
|
if (typeof e.message === "string") {
|
|
164
309
|
const msg = e.message.toLowerCase();
|
|
165
310
|
if (msg.includes("disconnected") || msg.includes("not found") || msg.includes("no device") || msg.includes("unplugged"))
|
|
@@ -179,6 +324,21 @@ function isTimeoutError(err) {
|
|
|
179
324
|
if (e.code === HardwareErrorCode.OperationTimeout) return true;
|
|
180
325
|
return false;
|
|
181
326
|
}
|
|
327
|
+
function isAppStuckByApdu(err) {
|
|
328
|
+
if (!err || typeof err !== "object") return false;
|
|
329
|
+
const tag = err._tag;
|
|
330
|
+
if (tag === ERROR_TAG.DeviceAppStuck) return true;
|
|
331
|
+
return extractApduHex(err) === "6901";
|
|
332
|
+
}
|
|
333
|
+
function isTransportStuck(err) {
|
|
334
|
+
if (!err || typeof err !== "object") return false;
|
|
335
|
+
const tag = err._tag;
|
|
336
|
+
return tag === ERROR_TAG.DeviceTransportStuck || // already wrapped form
|
|
337
|
+
tag === ERROR_TAG.UnknownDeviceExchange || tag === ERROR_TAG.AlreadySendingApdu;
|
|
338
|
+
}
|
|
339
|
+
function isStuckAppStateError(err) {
|
|
340
|
+
return isAppStuckByApdu(err) || isTransportStuck(err);
|
|
341
|
+
}
|
|
182
342
|
function mapLedgerError(err, opts) {
|
|
183
343
|
let originalMessage = "Unknown Ledger error";
|
|
184
344
|
if (err instanceof Error) {
|
|
@@ -190,20 +350,24 @@ function mapLedgerError(err, opts) {
|
|
|
190
350
|
let code;
|
|
191
351
|
if (isDeviceLockedError(err)) {
|
|
192
352
|
code = HardwareErrorCode.DeviceLocked;
|
|
353
|
+
} else if (isDeviceNotAdvertisingError(err) || isDeviceNotFoundError(err)) {
|
|
354
|
+
code = HardwareErrorCode.DeviceNotFound;
|
|
355
|
+
} else if (isDeviceBusyError(err)) {
|
|
356
|
+
code = HardwareErrorCode.DeviceBusy;
|
|
357
|
+
} else if (isBlePairingFailureError(err)) {
|
|
358
|
+
code = HardwareErrorCode.BlePairingTimeout;
|
|
193
359
|
} else if (isUserRejectedError(err)) {
|
|
194
360
|
code = HardwareErrorCode.UserRejected;
|
|
195
361
|
} else if (isWrongAppError(err)) {
|
|
196
362
|
code = HardwareErrorCode.WrongApp;
|
|
197
363
|
} else if (isAppNotInstalledError(err)) {
|
|
198
|
-
code = HardwareErrorCode.
|
|
364
|
+
code = HardwareErrorCode.AppNotInstalled;
|
|
199
365
|
} else if (isDeviceDisconnectedError(err)) {
|
|
200
366
|
code = HardwareErrorCode.DeviceDisconnected;
|
|
201
367
|
} else if (isTimeoutError(err)) {
|
|
202
368
|
code = HardwareErrorCode.OperationTimeout;
|
|
203
369
|
} else {
|
|
204
|
-
const
|
|
205
|
-
const lastStep = err && typeof err === "object" ? err._lastStep : void 0;
|
|
206
|
-
const ethMapped = ethCode ? mapEthAppError(ethCode, lastStep) : null;
|
|
370
|
+
const ethMapped = classifyEthAppError(err);
|
|
207
371
|
const apduHex = ethMapped ? null : extractApduHex(err);
|
|
208
372
|
const chainMapped = apduHex ? mapSolanaAppError(apduHex) ?? mapTronAppError(apduHex) ?? mapBtcAppError(apduHex) : null;
|
|
209
373
|
code = ethMapped ?? chainMapped ?? HardwareErrorCode.UnknownError;
|
|
@@ -213,38 +377,100 @@ function mapLedgerError(err, opts) {
|
|
|
213
377
|
return { code, message: enrichErrorMessage(code, originalMessage), appName };
|
|
214
378
|
}
|
|
215
379
|
|
|
216
|
-
// src/utils/
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
380
|
+
// src/utils/ledgerDmkTransport.ts
|
|
381
|
+
function isLedgerDmkBleTransport(transport) {
|
|
382
|
+
const normalized = transport?.toUpperCase();
|
|
383
|
+
return normalized === "BLE" || normalized?.endsWith("_BLE") === true;
|
|
220
384
|
}
|
|
221
|
-
function
|
|
222
|
-
return
|
|
385
|
+
function isLedgerBleConnectionType(connectionType) {
|
|
386
|
+
return connectionType === "ble";
|
|
223
387
|
}
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
|
|
388
|
+
function isLedgerBleDescriptor(connectionType, descriptor) {
|
|
389
|
+
return isLedgerBleConnectionType(connectionType) || isLedgerDmkBleTransport(descriptor.transport);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/utils/sdkEventBus.ts
|
|
393
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
394
|
+
function onSdkEvent(listener) {
|
|
395
|
+
listeners.add(listener);
|
|
396
|
+
return () => {
|
|
397
|
+
listeners.delete(listener);
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function offSdkEvent(listener) {
|
|
401
|
+
listeners.delete(listener);
|
|
402
|
+
}
|
|
403
|
+
function emitSdkEvent(event) {
|
|
404
|
+
if (listeners.size === 0) return;
|
|
405
|
+
for (const listener of listeners) {
|
|
406
|
+
try {
|
|
407
|
+
listener(event);
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
227
410
|
}
|
|
228
411
|
}
|
|
229
|
-
function
|
|
230
|
-
if (
|
|
231
|
-
|
|
412
|
+
function emitLog(level, ...args) {
|
|
413
|
+
if (listeners.size === 0) return;
|
|
414
|
+
const message = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
|
|
415
|
+
emitSdkEvent({ type: "log", level, message });
|
|
416
|
+
}
|
|
417
|
+
function safeStringify(value) {
|
|
418
|
+
if (value instanceof Error) {
|
|
419
|
+
return value.stack ? `${value.message}
|
|
420
|
+
${value.stack}` : value.message;
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
return JSON.stringify(value);
|
|
424
|
+
} catch {
|
|
425
|
+
return String(value);
|
|
232
426
|
}
|
|
233
427
|
}
|
|
234
428
|
|
|
429
|
+
// src/utils/debugLog.ts
|
|
430
|
+
function debugLog(...args) {
|
|
431
|
+
emitLog("debug", ...args);
|
|
432
|
+
}
|
|
433
|
+
function debugError(...args) {
|
|
434
|
+
emitLog("error", ...args);
|
|
435
|
+
}
|
|
436
|
+
|
|
235
437
|
// src/adapter/LedgerAdapter.ts
|
|
236
438
|
function formatDeviceMismatchError(expected, actual) {
|
|
237
439
|
return `Wrong device: expected ${expected}, got ${actual}`;
|
|
238
440
|
}
|
|
441
|
+
var BTC_HIGH_INDEX_THRESHOLD = 100;
|
|
442
|
+
function btcAccountIndexFromPath(path) {
|
|
443
|
+
const segments = path.replace(/^m\//, "").split("/");
|
|
444
|
+
if (segments.length < 3) return null;
|
|
445
|
+
const accountSeg = segments[2].replace(/['h]$/i, "");
|
|
446
|
+
const accountIndex = parseInt(accountSeg, 10);
|
|
447
|
+
return Number.isFinite(accountIndex) ? accountIndex : null;
|
|
448
|
+
}
|
|
239
449
|
var _LedgerAdapter = class _LedgerAdapter {
|
|
240
450
|
constructor(connector, options) {
|
|
241
451
|
this.vendor = "ledger";
|
|
242
452
|
this.emitter = new TypedEventEmitter();
|
|
243
|
-
// Device cache: tracks discovered devices from connector events
|
|
244
453
|
this._discoveredDevices = /* @__PURE__ */ new Map();
|
|
245
|
-
// Session tracking: maps connectId -> sessionId
|
|
246
454
|
this._sessions = /* @__PURE__ */ new Map();
|
|
247
455
|
this._uiRegistry = new UiRequestRegistry();
|
|
456
|
+
// BTC App rejects account index >= 100 unless display=true. Cached per
|
|
457
|
+
// adapter instance: first 100+ path asks the user once via UI request,
|
|
458
|
+
// subsequent 100+ paths in the same session auto-promote silently.
|
|
459
|
+
// The Ledger device itself still requires a per-call confirmation — that's
|
|
460
|
+
// the Ledger app's safety boundary, not ours to bypass.
|
|
461
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
462
|
+
// Shared across concurrent callers — only `cancel()` aborts.
|
|
463
|
+
this._doConnectAbortController = null;
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// Private helpers
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
/**
|
|
468
|
+
* Ensure at least one device is connected and return a valid connectId.
|
|
469
|
+
*
|
|
470
|
+
* - If a session already exists for the given connectId, reuse it.
|
|
471
|
+
* - If ANY session exists (Ledger IDs are ephemeral), reuse it.
|
|
472
|
+
* - Otherwise: search → 1 device: auto-connect, multiple: ask user, 0: throw.
|
|
473
|
+
*/
|
|
248
474
|
// Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
|
|
249
475
|
this._connectingPromise = null;
|
|
250
476
|
// ---------------------------------------------------------------------------
|
|
@@ -275,39 +501,23 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
275
501
|
};
|
|
276
502
|
this.connector = connector;
|
|
277
503
|
this._handleSelectDevice = options?.handleSelectDevice ?? false;
|
|
278
|
-
this._jobQueue = new DeviceJobQueue(
|
|
279
|
-
emit: (event, data) => this.emitter.emit(event, data),
|
|
280
|
-
uiRegistry: this._uiRegistry
|
|
281
|
-
});
|
|
504
|
+
this._jobQueue = new DeviceJobQueue();
|
|
282
505
|
this.registerEventListeners();
|
|
283
506
|
}
|
|
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
507
|
// Transport
|
|
296
|
-
// ---------------------------------------------------------------------------
|
|
297
|
-
// Transport is decided at connector creation time. These methods
|
|
298
|
-
// satisfy the IHardwareWallet interface with sensible defaults.
|
|
299
508
|
get activeTransport() {
|
|
300
|
-
return this.connector.connectionType
|
|
509
|
+
return isLedgerBleConnectionType(this.connector.connectionType) ? "ble" : "hid";
|
|
301
510
|
}
|
|
302
511
|
getAvailableTransports() {
|
|
303
512
|
return this.activeTransport ? [this.activeTransport] : [];
|
|
304
513
|
}
|
|
305
|
-
|
|
514
|
+
// Connector is bound at construction; switching requires a new adapter.
|
|
515
|
+
switchTransport(_type) {
|
|
516
|
+
return Promise.resolve();
|
|
306
517
|
}
|
|
307
|
-
// ---------------------------------------------------------------------------
|
|
308
518
|
// Lifecycle
|
|
309
|
-
|
|
310
|
-
|
|
519
|
+
init(_config) {
|
|
520
|
+
return Promise.resolve();
|
|
311
521
|
}
|
|
312
522
|
/**
|
|
313
523
|
* Clear cached device/session state without tearing down the adapter.
|
|
@@ -320,6 +530,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
320
530
|
this._connectingPromise = null;
|
|
321
531
|
this._uiRegistry.reset();
|
|
322
532
|
this._jobQueue.clear();
|
|
533
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
323
534
|
}
|
|
324
535
|
async dispose() {
|
|
325
536
|
this._uiRegistry.reset();
|
|
@@ -328,6 +539,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
328
539
|
this.connector.reset();
|
|
329
540
|
this._discoveredDevices.clear();
|
|
330
541
|
this._sessions.clear();
|
|
542
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
331
543
|
this.emitter.removeAllListeners();
|
|
332
544
|
}
|
|
333
545
|
uiResponse(response) {
|
|
@@ -336,10 +548,16 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
336
548
|
// ---------------------------------------------------------------------------
|
|
337
549
|
// Device management
|
|
338
550
|
// ---------------------------------------------------------------------------
|
|
339
|
-
async searchDevices() {
|
|
551
|
+
async searchDevices(options) {
|
|
552
|
+
if (options?.resetSession) {
|
|
553
|
+
this._doConnectAbortController?.abort();
|
|
554
|
+
this._sessions.clear();
|
|
555
|
+
this._connectingPromise = null;
|
|
556
|
+
this._doConnectAbortController = null;
|
|
557
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
558
|
+
}
|
|
340
559
|
await this._ensureDevicePermission();
|
|
341
560
|
const devices = await this.connector.searchDevices();
|
|
342
|
-
debugLog("[DMK] adapter.searchDevices raw:", JSON.stringify(devices));
|
|
343
561
|
this._discoveredDevices.clear();
|
|
344
562
|
for (const d of devices) {
|
|
345
563
|
if (d.connectId) {
|
|
@@ -349,11 +567,26 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
349
567
|
if (this._discoveredDevices.size === 0) {
|
|
350
568
|
await this._ensureDevicePermission();
|
|
351
569
|
}
|
|
570
|
+
debugLog(
|
|
571
|
+
`[LedgerAdapter] searchDevices() return count=${this._discoveredDevices.size} ids=[${[
|
|
572
|
+
...this._discoveredDevices.keys()
|
|
573
|
+
].join(",")}]`
|
|
574
|
+
);
|
|
352
575
|
return Array.from(this._discoveredDevices.values());
|
|
353
576
|
}
|
|
577
|
+
static _createDeviceBusyError(method) {
|
|
578
|
+
return Object.assign(new Error(`Ledger device is busy while calling ${method}`), {
|
|
579
|
+
code: HardwareErrorCode2.DeviceBusy
|
|
580
|
+
});
|
|
581
|
+
}
|
|
354
582
|
async connectDevice(connectId) {
|
|
355
|
-
await this._ensureDevicePermission(connectId);
|
|
356
583
|
try {
|
|
584
|
+
if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
|
|
585
|
+
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
586
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
await this._ensureDevicePermission(connectId);
|
|
357
590
|
const session = await this.connector.connect(connectId);
|
|
358
591
|
this._sessions.set(connectId, session.sessionId);
|
|
359
592
|
if (session.deviceInfo) {
|
|
@@ -389,7 +622,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
389
622
|
// Chain call helper
|
|
390
623
|
// ---------------------------------------------------------------------------
|
|
391
624
|
async callChain(connectId, deviceId, chain, method, params, skipFingerprint = false) {
|
|
392
|
-
await this._ensureDevicePermission(connectId, deviceId);
|
|
393
625
|
try {
|
|
394
626
|
const result = await this.connectorCall(connectId, method, params, {
|
|
395
627
|
chain,
|
|
@@ -401,45 +633,12 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
401
633
|
return this.errorToFailure(err);
|
|
402
634
|
}
|
|
403
635
|
}
|
|
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
636
|
// ---------------------------------------------------------------------------
|
|
428
637
|
// EVM chain methods
|
|
429
638
|
// ---------------------------------------------------------------------------
|
|
430
639
|
evmGetAddress(connectId, deviceId, params) {
|
|
431
640
|
return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
|
|
432
641
|
}
|
|
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
642
|
evmSignTransaction(connectId, deviceId, params) {
|
|
444
643
|
return this.callChain(connectId, deviceId, "evm", "evmSignTransaction", params);
|
|
445
644
|
}
|
|
@@ -455,16 +654,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
455
654
|
btcGetAddress(connectId, deviceId, params) {
|
|
456
655
|
return this.callChain(connectId, deviceId, "btc", "btcGetAddress", params);
|
|
457
656
|
}
|
|
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
657
|
btcGetPublicKey(connectId, deviceId, params) {
|
|
469
658
|
return this.callChain(connectId, deviceId, "btc", "btcGetPublicKey", params);
|
|
470
659
|
}
|
|
@@ -492,16 +681,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
492
681
|
solGetAddress(connectId, deviceId, params) {
|
|
493
682
|
return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
|
|
494
683
|
}
|
|
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
684
|
solSignTransaction(connectId, deviceId, params) {
|
|
506
685
|
return this.callChain(connectId, deviceId, "sol", "solSignTransaction", params);
|
|
507
686
|
}
|
|
@@ -512,38 +691,13 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
512
691
|
// TRON chain methods
|
|
513
692
|
// ---------------------------------------------------------------------------
|
|
514
693
|
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
|
-
);
|
|
694
|
+
return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params);
|
|
527
695
|
}
|
|
528
696
|
tronSignTransaction(connectId, deviceId, params) {
|
|
529
|
-
return this.callChain(
|
|
530
|
-
connectId,
|
|
531
|
-
deviceId,
|
|
532
|
-
"tron",
|
|
533
|
-
"tronSignTransaction",
|
|
534
|
-
params,
|
|
535
|
-
true
|
|
536
|
-
);
|
|
697
|
+
return this.callChain(connectId, deviceId, "tron", "tronSignTransaction", params);
|
|
537
698
|
}
|
|
538
699
|
tronSignMessage(connectId, deviceId, params) {
|
|
539
|
-
return this.callChain(
|
|
540
|
-
connectId,
|
|
541
|
-
deviceId,
|
|
542
|
-
"tron",
|
|
543
|
-
"tronSignMessage",
|
|
544
|
-
params,
|
|
545
|
-
true
|
|
546
|
-
);
|
|
700
|
+
return this.callChain(connectId, deviceId, "tron", "tronSignMessage", params);
|
|
547
701
|
}
|
|
548
702
|
on(event, listener) {
|
|
549
703
|
this.emitter.on(event, listener);
|
|
@@ -552,30 +706,41 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
552
706
|
this.emitter.off(event, listener);
|
|
553
707
|
}
|
|
554
708
|
cancel(connectId) {
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
709
|
+
const userAbortReason = Object.assign(new Error("User aborted operation"), {
|
|
710
|
+
code: HardwareErrorCode2.UserAborted,
|
|
711
|
+
_tag: ERROR_TAG.UserAborted
|
|
712
|
+
});
|
|
713
|
+
this._lastCancelReason = userAbortReason;
|
|
714
|
+
setTimeout(() => {
|
|
715
|
+
if (this._lastCancelReason === userAbortReason) {
|
|
716
|
+
this._lastCancelReason = void 0;
|
|
717
|
+
}
|
|
718
|
+
}, 2e3);
|
|
719
|
+
this._uiRegistry.cancel();
|
|
720
|
+
if (connectId) {
|
|
721
|
+
this._jobQueue.cancelActiveAndPending(connectId, userAbortReason);
|
|
722
|
+
} else {
|
|
723
|
+
this._jobQueue.cancelActiveAndPending(void 0, userAbortReason);
|
|
724
|
+
}
|
|
725
|
+
if (connectId) {
|
|
726
|
+
const sessionId = this._sessions.get(connectId) ?? connectId;
|
|
727
|
+
void this.connector.cancel(sessionId);
|
|
728
|
+
} else {
|
|
729
|
+
for (const sid of this._sessions.values()) void this.connector.cancel(sid);
|
|
730
|
+
}
|
|
731
|
+
if (this._connectingPromise) {
|
|
732
|
+
this._doConnectAbortController?.abort(userAbortReason);
|
|
733
|
+
}
|
|
558
734
|
}
|
|
559
735
|
// ---------------------------------------------------------------------------
|
|
560
736
|
// Chain fingerprint
|
|
561
737
|
// ---------------------------------------------------------------------------
|
|
562
738
|
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
739
|
try {
|
|
574
740
|
const fingerprint = await this._computeChainFingerprint(
|
|
575
741
|
chain,
|
|
576
|
-
(method, params) => this.connectorCall(connectId, method, params)
|
|
742
|
+
(method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
|
|
577
743
|
);
|
|
578
|
-
debugLog("[LedgerAdapter] getChainFingerprint result:", fingerprint?.substring(0, 20));
|
|
579
744
|
return success(fingerprint);
|
|
580
745
|
} catch (err) {
|
|
581
746
|
debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
|
|
@@ -646,77 +811,226 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
646
811
|
if (signal?.aborted) {
|
|
647
812
|
_LedgerAdapter._throwIfAborted(signal);
|
|
648
813
|
}
|
|
814
|
+
const waitPromise = this._uiRegistry.wait(
|
|
815
|
+
UI_REQUEST.REQUEST_DEVICE_CONNECT
|
|
816
|
+
);
|
|
649
817
|
this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_CONNECT, {
|
|
650
818
|
type: UI_REQUEST.REQUEST_DEVICE_CONNECT,
|
|
651
819
|
payload: {
|
|
820
|
+
vendor: "ledger",
|
|
821
|
+
reason: "device-not-found",
|
|
652
822
|
message: "Please connect and unlock your Ledger device"
|
|
653
823
|
}
|
|
654
824
|
});
|
|
655
|
-
const waitPromise = this._uiRegistry.wait(
|
|
656
|
-
UI_REQUEST.REQUEST_DEVICE_CONNECT
|
|
657
|
-
);
|
|
658
825
|
let payload;
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
826
|
+
try {
|
|
827
|
+
if (signal) {
|
|
828
|
+
const onAbort = () => {
|
|
829
|
+
this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_CONNECT);
|
|
830
|
+
};
|
|
831
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
832
|
+
try {
|
|
833
|
+
payload = await waitPromise;
|
|
834
|
+
} finally {
|
|
835
|
+
signal.removeEventListener("abort", onAbort);
|
|
836
|
+
}
|
|
837
|
+
} else {
|
|
665
838
|
payload = await waitPromise;
|
|
666
|
-
} finally {
|
|
667
|
-
signal.removeEventListener("abort", onAbort);
|
|
668
839
|
}
|
|
669
|
-
}
|
|
670
|
-
|
|
840
|
+
} catch (err) {
|
|
841
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
842
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
843
|
+
payload: {}
|
|
844
|
+
});
|
|
845
|
+
throw Object.assign(new Error("User cancelled Ledger connection"), {
|
|
846
|
+
_tag: ERROR_TAG.UserAborted,
|
|
847
|
+
code: HardwareErrorCode2.UserAborted,
|
|
848
|
+
cause: err
|
|
849
|
+
});
|
|
671
850
|
}
|
|
672
851
|
if (!payload?.confirmed) {
|
|
673
852
|
throw Object.assign(new Error("User cancelled Ledger connection"), {
|
|
674
|
-
_tag:
|
|
853
|
+
_tag: ERROR_TAG.UserAborted,
|
|
854
|
+
code: HardwareErrorCode2.UserAborted
|
|
675
855
|
});
|
|
676
856
|
}
|
|
857
|
+
await new Promise((resolve) => {
|
|
858
|
+
setTimeout(resolve, 800);
|
|
859
|
+
});
|
|
677
860
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
861
|
+
/**
|
|
862
|
+
* Decide whether a BTC pubkey call needs `showOnDevice=true` because of
|
|
863
|
+
* the BTC App's account-index policy, asking the user once per session.
|
|
864
|
+
*
|
|
865
|
+
* Returns the params to pass through (with `showOnDevice` possibly
|
|
866
|
+
* promoted), or `null` if the user declined.
|
|
867
|
+
*/
|
|
868
|
+
async _gateBtcHighIndex(params) {
|
|
869
|
+
const accountIndex = btcAccountIndexFromPath(params.path);
|
|
870
|
+
if (params.showOnDevice) return params;
|
|
871
|
+
if (accountIndex === null || accountIndex < BTC_HIGH_INDEX_THRESHOLD) {
|
|
872
|
+
return params;
|
|
685
873
|
}
|
|
686
|
-
if (this.
|
|
687
|
-
return
|
|
874
|
+
if (this._btcHighIndexConfirmedThisSession) {
|
|
875
|
+
return { ...params, showOnDevice: true };
|
|
688
876
|
}
|
|
689
|
-
|
|
877
|
+
const confirmed = await this._waitForBtcHighIndexConfirm(params.path, accountIndex);
|
|
878
|
+
if (!confirmed) return null;
|
|
879
|
+
this._btcHighIndexConfirmedThisSession = true;
|
|
880
|
+
return { ...params, showOnDevice: true };
|
|
881
|
+
}
|
|
882
|
+
async _waitForBtcHighIndexConfirm(path, accountIndex) {
|
|
883
|
+
const waitPromise = this._uiRegistry.wait(
|
|
884
|
+
UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM
|
|
885
|
+
);
|
|
886
|
+
this.emitter.emit(UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM, {
|
|
887
|
+
type: UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
|
|
888
|
+
payload: {
|
|
889
|
+
vendor: "ledger",
|
|
890
|
+
path,
|
|
891
|
+
accountIndex
|
|
892
|
+
}
|
|
893
|
+
});
|
|
690
894
|
try {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
895
|
+
const payload = await waitPromise;
|
|
896
|
+
return !!payload?.confirmed;
|
|
897
|
+
} catch (err) {
|
|
898
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
899
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
900
|
+
payload: {}
|
|
901
|
+
});
|
|
902
|
+
return false;
|
|
694
903
|
}
|
|
695
904
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
905
|
+
// Layer 1 entry. Caller signal only races the outer awaiter; the shared
|
|
906
|
+
// `_doConnect` runs under its own internal controller so caller A's cancel
|
|
907
|
+
// doesn't kill caller B's await.
|
|
908
|
+
async ensureConnected(connectId, signal) {
|
|
909
|
+
if (signal.aborted) _LedgerAdapter._throwIfAborted(signal);
|
|
910
|
+
if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
|
|
911
|
+
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
912
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
if (connectId && this._sessions.has(connectId)) return connectId;
|
|
916
|
+
if (this._sessions.size > 0) {
|
|
917
|
+
return this._sessions.keys().next().value;
|
|
918
|
+
}
|
|
919
|
+
if (!this._connectingPromise) {
|
|
920
|
+
this._doConnectAbortController = new AbortController();
|
|
921
|
+
const innerSignal = this._doConnectAbortController.signal;
|
|
922
|
+
this._connectingPromise = (async () => {
|
|
923
|
+
try {
|
|
924
|
+
return await this._doConnect(innerSignal, connectId);
|
|
925
|
+
} finally {
|
|
926
|
+
this._connectingPromise = null;
|
|
927
|
+
this._doConnectAbortController = null;
|
|
928
|
+
}
|
|
929
|
+
})();
|
|
930
|
+
}
|
|
931
|
+
return this._abortable(signal, this._connectingPromise);
|
|
932
|
+
}
|
|
933
|
+
// Layer 1 main loop — the ONLY place in SDK that emits unlock dialog.
|
|
934
|
+
// Bounded by MAX_DOCONNECT_CONFIRMS — after N Confirms with no progress,
|
|
935
|
+
// throw DeviceNotFound so the user is kicked out of the loop.
|
|
936
|
+
async _doConnect(internalSignal, targetConnectId) {
|
|
937
|
+
if (isLedgerBleConnectionType(this.connector.connectionType) && targetConnectId) {
|
|
938
|
+
try {
|
|
939
|
+
return await this._connectDeviceOrThrow(targetConnectId);
|
|
940
|
+
} catch (err) {
|
|
941
|
+
if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
|
|
942
|
+
throw err;
|
|
943
|
+
}
|
|
944
|
+
this._discoveredDevices.delete(targetConnectId);
|
|
945
|
+
if (isDeviceDisconnectedError(err)) {
|
|
946
|
+
try {
|
|
947
|
+
this.connector.reset?.();
|
|
948
|
+
} catch {
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
let confirms = 0;
|
|
954
|
+
while (!internalSignal.aborted) {
|
|
955
|
+
this.emitter.emit("ui-event", {
|
|
956
|
+
type: EConnectorInteraction.Searching,
|
|
957
|
+
payload: { sessionId: "" }
|
|
958
|
+
});
|
|
959
|
+
let devices = await this.searchDevices();
|
|
960
|
+
if (devices.length === 0) {
|
|
961
|
+
for (let i = 0; i < 3 && !internalSignal.aborted; i += 1) {
|
|
962
|
+
await new Promise((resolve) => {
|
|
963
|
+
setTimeout(resolve, 1e3);
|
|
964
|
+
});
|
|
965
|
+
devices = await this.searchDevices();
|
|
966
|
+
if (devices.length > 0) break;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
699
969
|
if (devices.length > 0) {
|
|
700
|
-
|
|
970
|
+
try {
|
|
971
|
+
return await this._connectFirstOrSelect(devices, targetConnectId);
|
|
972
|
+
} catch (err) {
|
|
973
|
+
if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
|
|
974
|
+
throw err;
|
|
975
|
+
}
|
|
976
|
+
this._discoveredDevices.clear();
|
|
977
|
+
if (isDeviceDisconnectedError(err)) {
|
|
978
|
+
try {
|
|
979
|
+
this.connector.reset?.();
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
701
984
|
}
|
|
702
|
-
if (
|
|
703
|
-
|
|
985
|
+
if (confirms >= _LedgerAdapter.MAX_DOCONNECT_CONFIRMS) {
|
|
986
|
+
throw Object.assign(
|
|
987
|
+
new Error(
|
|
988
|
+
"Device not connected after multiple attempts. Please ensure your Ledger is awake, unlocked, and in range, then try again."
|
|
989
|
+
),
|
|
990
|
+
{ code: HardwareErrorCode2.DeviceNotFound }
|
|
991
|
+
);
|
|
704
992
|
}
|
|
993
|
+
await this._waitForDeviceConnect(internalSignal);
|
|
994
|
+
confirms += 1;
|
|
705
995
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
"No Ledger device found after multiple attempts. Please connect and unlock your device."
|
|
709
|
-
),
|
|
710
|
-
{ _tag: "DeviceNotRecognizedError" }
|
|
711
|
-
);
|
|
996
|
+
_LedgerAdapter._throwIfAborted(internalSignal);
|
|
997
|
+
throw new Error("_doConnect aborted");
|
|
712
998
|
}
|
|
713
|
-
async _connectFirstOrSelect(devices) {
|
|
999
|
+
async _connectFirstOrSelect(devices, targetConnectId) {
|
|
1000
|
+
if (targetConnectId) {
|
|
1001
|
+
const target = devices.find(
|
|
1002
|
+
(d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
|
|
1003
|
+
);
|
|
1004
|
+
if (!target) {
|
|
1005
|
+
const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
|
|
1006
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
1007
|
+
});
|
|
1008
|
+
if (isLedgerBleConnectionType(this.connector.connectionType)) {
|
|
1009
|
+
err._tag = ERROR_TAG.DeviceNotAdvertising;
|
|
1010
|
+
}
|
|
1011
|
+
throw err;
|
|
1012
|
+
}
|
|
1013
|
+
return this._connectDeviceOrThrow(target.connectId);
|
|
1014
|
+
}
|
|
1015
|
+
if (isLedgerBleConnectionType(this.connector.connectionType)) {
|
|
1016
|
+
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
1017
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
714
1020
|
const chosenConnectId = devices.length === 1 ? devices[0].connectId : await this._chooseDeviceFromList(devices);
|
|
1021
|
+
return this._connectDeviceOrThrow(chosenConnectId);
|
|
1022
|
+
}
|
|
1023
|
+
async _connectDeviceOrThrow(chosenConnectId) {
|
|
715
1024
|
const result = await this.connectDevice(chosenConnectId);
|
|
716
1025
|
if (!result.success) {
|
|
717
|
-
|
|
718
|
-
|
|
1026
|
+
const payload = result.payload;
|
|
1027
|
+
const rethrow = Object.assign(new Error(payload.error), {
|
|
1028
|
+
code: payload.code
|
|
719
1029
|
});
|
|
1030
|
+
if (payload._tag !== void 0) {
|
|
1031
|
+
rethrow._tag = payload._tag;
|
|
1032
|
+
}
|
|
1033
|
+
throw rethrow;
|
|
720
1034
|
}
|
|
721
1035
|
return chosenConnectId;
|
|
722
1036
|
}
|
|
@@ -727,18 +1041,35 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
727
1041
|
);
|
|
728
1042
|
return devices[0].connectId;
|
|
729
1043
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
UI_REQUEST.REQUEST_SELECT_DEVICE
|
|
736
|
-
|
|
1044
|
+
let response;
|
|
1045
|
+
try {
|
|
1046
|
+
const waitPromise = this._uiRegistry.wait(
|
|
1047
|
+
UI_REQUEST.REQUEST_SELECT_DEVICE
|
|
1048
|
+
);
|
|
1049
|
+
this.emitter.emit(UI_REQUEST.REQUEST_SELECT_DEVICE, {
|
|
1050
|
+
type: UI_REQUEST.REQUEST_SELECT_DEVICE,
|
|
1051
|
+
payload: { devices }
|
|
1052
|
+
});
|
|
1053
|
+
response = await waitPromise;
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
if (err?._tag === UI_REQUEST_PREEMPTED_TAG) {
|
|
1056
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
1057
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
1058
|
+
payload: {}
|
|
1059
|
+
});
|
|
1060
|
+
throw Object.assign(new Error("Device selection superseded"), {
|
|
1061
|
+
_tag: ERROR_TAG.UserAborted,
|
|
1062
|
+
code: HardwareErrorCode2.UserAborted,
|
|
1063
|
+
cause: err
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
throw err;
|
|
1067
|
+
}
|
|
737
1068
|
const chosen = devices.find((d) => d.connectId === response?.sdkConnectId);
|
|
738
1069
|
if (!chosen) {
|
|
739
1070
|
throw Object.assign(
|
|
740
1071
|
new Error(`Selected sdkConnectId '${response?.sdkConnectId}' not in discovered list`),
|
|
741
|
-
{ _tag:
|
|
1072
|
+
{ _tag: ERROR_TAG.DeviceNotRecognized }
|
|
742
1073
|
);
|
|
743
1074
|
}
|
|
744
1075
|
return chosen.connectId;
|
|
@@ -751,30 +1082,31 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
751
1082
|
* 3. Calls connector.call()
|
|
752
1083
|
* 4. On disconnect error: clears stale session, re-connects, retries once
|
|
753
1084
|
*/
|
|
754
|
-
async connectorCall(connectId, method, params, fingerprint) {
|
|
1085
|
+
async connectorCall(connectId, method, params, fingerprint, permissionDeviceId) {
|
|
755
1086
|
debugLog("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
|
|
756
1087
|
const queueKey = connectId || "__ledger_default__";
|
|
757
|
-
const interruptibility = _LedgerAdapter._getInterruptibility(method);
|
|
758
1088
|
return this._jobQueue.enqueue(
|
|
759
1089
|
queueKey,
|
|
760
|
-
async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint),
|
|
761
|
-
{
|
|
1090
|
+
async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId),
|
|
1091
|
+
{
|
|
1092
|
+
label: method,
|
|
1093
|
+
rejectIfBusy: true,
|
|
1094
|
+
busyError: _LedgerAdapter._createDeviceBusyError(method)
|
|
1095
|
+
}
|
|
762
1096
|
);
|
|
763
1097
|
}
|
|
764
1098
|
/**
|
|
765
|
-
* Race a promise against an abort signal.
|
|
766
|
-
* signal
|
|
767
|
-
* actually be cancelled on Ledger DMK, but the caller gets the abort immediately.
|
|
1099
|
+
* Race a promise against an abort signal. On abort, rejects with
|
|
1100
|
+
* signal.reason → instance _lastCancelReason → generic Error('Aborted').
|
|
768
1101
|
*/
|
|
769
|
-
|
|
1102
|
+
_abortable(signal, promise) {
|
|
1103
|
+
const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
|
|
770
1104
|
if (signal.aborted) {
|
|
771
|
-
return Promise.reject(
|
|
772
|
-
signal.reason ?? new Error("Aborted")
|
|
773
|
-
);
|
|
1105
|
+
return Promise.reject(getAbortReason());
|
|
774
1106
|
}
|
|
775
1107
|
return new Promise((resolve, reject) => {
|
|
776
1108
|
const onAbort = () => {
|
|
777
|
-
reject(
|
|
1109
|
+
reject(getAbortReason());
|
|
778
1110
|
};
|
|
779
1111
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
780
1112
|
promise.then(
|
|
@@ -796,40 +1128,49 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
796
1128
|
}
|
|
797
1129
|
}
|
|
798
1130
|
/** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
|
|
799
|
-
async _runConnectorCall(connectId, method, params, signal, fingerprint) {
|
|
1131
|
+
async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId) {
|
|
800
1132
|
_LedgerAdapter._throwIfAborted(signal);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1133
|
+
await this._ensureDevicePermission(
|
|
1134
|
+
connectId,
|
|
1135
|
+
permissionDeviceId ?? fingerprint?.deviceId,
|
|
1136
|
+
signal
|
|
804
1137
|
);
|
|
805
1138
|
_LedgerAdapter._throwIfAborted(signal);
|
|
1139
|
+
let effectiveParams = params;
|
|
1140
|
+
if (method === "btcGetPublicKey") {
|
|
1141
|
+
const gatedParams = await this._gateBtcHighIndex(params);
|
|
1142
|
+
if (gatedParams === null) {
|
|
1143
|
+
throw Object.assign(new Error("User cancelled BTC high-index confirmation"), {
|
|
1144
|
+
_tag: ERROR_TAG.UserAborted,
|
|
1145
|
+
code: HardwareErrorCode2.UserAborted
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
effectiveParams = gatedParams;
|
|
1149
|
+
}
|
|
1150
|
+
const resolvedConnectId = await this.ensureConnected(connectId, signal);
|
|
806
1151
|
const sessionId = this._sessions.get(resolvedConnectId);
|
|
807
|
-
debugLog(
|
|
808
|
-
"[LedgerAdapter] connectorCall resolved:",
|
|
809
|
-
method,
|
|
810
|
-
"resolvedConnectId:",
|
|
811
|
-
resolvedConnectId,
|
|
812
|
-
"sessionId:",
|
|
813
|
-
sessionId
|
|
814
|
-
);
|
|
815
1152
|
if (!sessionId) {
|
|
816
1153
|
throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
|
|
817
|
-
_tag:
|
|
1154
|
+
_tag: ERROR_TAG.DeviceSessionNotFound
|
|
818
1155
|
});
|
|
819
1156
|
}
|
|
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
1157
|
try {
|
|
832
|
-
|
|
1158
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1159
|
+
const fp = await this._abortable(
|
|
1160
|
+
signal,
|
|
1161
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1162
|
+
sessionId,
|
|
1163
|
+
fingerprint.deviceId,
|
|
1164
|
+
fingerprint.chain
|
|
1165
|
+
)
|
|
1166
|
+
);
|
|
1167
|
+
if (!fp.success) {
|
|
1168
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1169
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return await this._abortable(signal, this.connector.call(sessionId, method, effectiveParams));
|
|
833
1174
|
} catch (err) {
|
|
834
1175
|
if (signal.aborted) throw err;
|
|
835
1176
|
const errObj = err;
|
|
@@ -839,37 +1180,243 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
839
1180
|
errorCode: errObj?.errorCode,
|
|
840
1181
|
statusCode: errObj?.statusCode,
|
|
841
1182
|
isDisconnected: isDeviceDisconnectedError(err),
|
|
842
|
-
isLocked: isDeviceLockedError(err)
|
|
1183
|
+
isLocked: isDeviceLockedError(err),
|
|
1184
|
+
isNotAdvertising: isDeviceNotAdvertisingError(err),
|
|
1185
|
+
isStuckApp: isStuckAppStateError(err)
|
|
843
1186
|
});
|
|
844
|
-
if (
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1187
|
+
if (isStuckAppStateError(err)) {
|
|
1188
|
+
try {
|
|
1189
|
+
this._sessions.delete(resolvedConnectId);
|
|
1190
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
1191
|
+
this.connector.reset?.();
|
|
1192
|
+
} catch {
|
|
1193
|
+
}
|
|
1194
|
+
debugLog(
|
|
1195
|
+
"[LedgerAdapter] stuck-app retry: method=",
|
|
1196
|
+
method,
|
|
1197
|
+
"delayMs=",
|
|
1198
|
+
_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
|
|
1199
|
+
"_tag=",
|
|
1200
|
+
err?._tag
|
|
1201
|
+
);
|
|
1202
|
+
try {
|
|
1203
|
+
const retryResult = await this._retryAfterStuckApp(
|
|
1204
|
+
resolvedConnectId,
|
|
1205
|
+
method,
|
|
1206
|
+
effectiveParams,
|
|
1207
|
+
signal,
|
|
1208
|
+
err,
|
|
1209
|
+
fingerprint
|
|
1210
|
+
);
|
|
1211
|
+
debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
|
|
1212
|
+
return retryResult;
|
|
1213
|
+
} catch (retryErr) {
|
|
1214
|
+
if (signal.aborted) throw retryErr;
|
|
1215
|
+
if (isStuckAppStateError(retryErr)) {
|
|
1216
|
+
debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
|
|
1217
|
+
throw err;
|
|
1218
|
+
}
|
|
1219
|
+
debugLog(
|
|
1220
|
+
"[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
|
|
1221
|
+
method,
|
|
1222
|
+
"retryErrTag=",
|
|
1223
|
+
retryErr?._tag
|
|
1224
|
+
);
|
|
1225
|
+
throw retryErr;
|
|
1226
|
+
}
|
|
848
1227
|
}
|
|
849
|
-
if (isDeviceLockedError(err)) {
|
|
850
|
-
|
|
851
|
-
_LedgerAdapter.
|
|
852
|
-
|
|
1228
|
+
if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
|
|
1229
|
+
let lastErr = err;
|
|
1230
|
+
for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
|
|
1231
|
+
if (signal.aborted) throw lastErr;
|
|
1232
|
+
try {
|
|
1233
|
+
this._sessions.delete(resolvedConnectId);
|
|
1234
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
1235
|
+
if (isDeviceDisconnectedError(lastErr)) {
|
|
1236
|
+
try {
|
|
1237
|
+
this.connector.reset?.();
|
|
1238
|
+
} catch {
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
const retryConnectId = isLedgerBleConnectionType(this.connector.connectionType) ? resolvedConnectId : void 0;
|
|
1242
|
+
const reConnectId = await this.ensureConnected(retryConnectId, 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 retryTargetConnectId = isLedgerBleConnectionType(this.connector.connectionType) ? resolvedConnectId : void 0;
|
|
1311
|
+
const retryConnectId = await this.ensureConnected(retryTargetConnectId, signal);
|
|
1312
|
+
const retrySessionId = this._sessions.get(retryConnectId);
|
|
1313
|
+
if (!retrySessionId) throw originalErr;
|
|
1314
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1315
|
+
const fp = await this._abortable(
|
|
1316
|
+
signal,
|
|
1317
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1318
|
+
retrySessionId,
|
|
1319
|
+
fingerprint.deviceId,
|
|
1320
|
+
fingerprint.chain
|
|
1321
|
+
)
|
|
1322
|
+
);
|
|
1323
|
+
if (!fp.success) {
|
|
1324
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1325
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return this._abortable(signal, this.connector.call(retrySessionId, method, params));
|
|
1330
|
+
}
|
|
1331
|
+
_sleepAbortable(ms, signal) {
|
|
1332
|
+
return new Promise((resolve, reject) => {
|
|
1333
|
+
if (signal.aborted) {
|
|
1334
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const timer = setTimeout(() => {
|
|
1338
|
+
signal.removeEventListener("abort", onAbort);
|
|
1339
|
+
resolve();
|
|
1340
|
+
}, ms);
|
|
1341
|
+
const onAbort = () => {
|
|
1342
|
+
clearTimeout(timer);
|
|
1343
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
1344
|
+
};
|
|
1345
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Clear stale session, reconnect, and retry the call.
|
|
1350
|
+
*
|
|
1351
|
+
* Timeout recovery starts with a full connector reset. After an APDU
|
|
1352
|
+
* timeout, DMK/transport state may still emit malformed responses; retrying
|
|
1353
|
+
* on the same DMK can poison the next chain switch.
|
|
1354
|
+
*/
|
|
1355
|
+
async _retryWithFreshConnection(targetConnectId, method, params, signal, originalErr, fingerprint) {
|
|
1356
|
+
this.connector.reset();
|
|
1357
|
+
this._sessions.clear();
|
|
1358
|
+
this._discoveredDevices.clear();
|
|
1359
|
+
this._connectingPromise = null;
|
|
1360
|
+
const retryConnectId = await this.ensureConnected(targetConnectId, signal);
|
|
868
1361
|
const retrySessionId = this._sessions.get(retryConnectId);
|
|
869
1362
|
if (!retrySessionId) {
|
|
870
1363
|
throw originalErr;
|
|
871
1364
|
}
|
|
872
|
-
|
|
1365
|
+
try {
|
|
1366
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1367
|
+
const fp = await this._abortable(
|
|
1368
|
+
signal,
|
|
1369
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1370
|
+
retrySessionId,
|
|
1371
|
+
fingerprint.deviceId,
|
|
1372
|
+
fingerprint.chain
|
|
1373
|
+
)
|
|
1374
|
+
);
|
|
1375
|
+
if (!fp.success) {
|
|
1376
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1377
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return await this._abortable(signal, this.connector.call(retrySessionId, method, params));
|
|
1382
|
+
} catch (retryErr) {
|
|
1383
|
+
if (signal.aborted) throw retryErr;
|
|
1384
|
+
this.connector.reset();
|
|
1385
|
+
this._sessions.clear();
|
|
1386
|
+
this._discoveredDevices.clear();
|
|
1387
|
+
this._connectingPromise = null;
|
|
1388
|
+
if (!isDeviceDisconnectedError(retryErr) && !isTimeoutError(retryErr)) {
|
|
1389
|
+
throw retryErr;
|
|
1390
|
+
}
|
|
1391
|
+
debugLog(
|
|
1392
|
+
"[LedgerAdapter] fresh-session retry still failed; resetting connector and rebuilding DMK"
|
|
1393
|
+
);
|
|
1394
|
+
this.connector.reset();
|
|
1395
|
+
this._sessions.clear();
|
|
1396
|
+
this._discoveredDevices.clear();
|
|
1397
|
+
this._connectingPromise = null;
|
|
1398
|
+
const finalConnectId = await this.ensureConnected(targetConnectId, signal);
|
|
1399
|
+
const finalSessionId = this._sessions.get(finalConnectId);
|
|
1400
|
+
if (!finalSessionId) {
|
|
1401
|
+
throw originalErr;
|
|
1402
|
+
}
|
|
1403
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1404
|
+
const fp = await this._abortable(
|
|
1405
|
+
signal,
|
|
1406
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1407
|
+
finalSessionId,
|
|
1408
|
+
fingerprint.deviceId,
|
|
1409
|
+
fingerprint.chain
|
|
1410
|
+
)
|
|
1411
|
+
);
|
|
1412
|
+
if (!fp.success) {
|
|
1413
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1414
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return this._abortable(signal, this.connector.call(finalSessionId, method, params));
|
|
1419
|
+
}
|
|
873
1420
|
}
|
|
874
1421
|
/**
|
|
875
1422
|
* Ensure OS-level device permission (Bluetooth / USB) before proceeding.
|
|
@@ -883,7 +1430,10 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
883
1430
|
* - No connectId (searchDevices): environment-level permission
|
|
884
1431
|
* - With connectId (business methods): device-level permission
|
|
885
1432
|
*/
|
|
886
|
-
async _ensureDevicePermission(connectId, deviceId) {
|
|
1433
|
+
async _ensureDevicePermission(connectId, deviceId, signal) {
|
|
1434
|
+
if (signal?.aborted) {
|
|
1435
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
1436
|
+
}
|
|
887
1437
|
const transportType = this.activeTransport ?? "hid";
|
|
888
1438
|
const waitPromise = this._uiRegistry.wait(
|
|
889
1439
|
UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
@@ -893,10 +1443,37 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
893
1443
|
type: UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
894
1444
|
payload: { transportType, connectId, deviceId }
|
|
895
1445
|
});
|
|
896
|
-
|
|
1446
|
+
let response;
|
|
1447
|
+
const onAbort = () => {
|
|
1448
|
+
this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_PERMISSION);
|
|
1449
|
+
};
|
|
1450
|
+
try {
|
|
1451
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1452
|
+
response = await waitPromise;
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
if (signal?.aborted) {
|
|
1455
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
1456
|
+
}
|
|
1457
|
+
if (err?._tag === UI_REQUEST_PREEMPTED_TAG) {
|
|
1458
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
1459
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
1460
|
+
payload: {}
|
|
1461
|
+
});
|
|
1462
|
+
throw Object.assign(new Error("Device permission request superseded"), {
|
|
1463
|
+
_tag: ERROR_TAG.UserAborted,
|
|
1464
|
+
code: HardwareErrorCode2.UserAborted,
|
|
1465
|
+
cause: err
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
throw err;
|
|
1469
|
+
} finally {
|
|
1470
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1471
|
+
}
|
|
1472
|
+
const { granted, reason, message } = response;
|
|
897
1473
|
if (!granted) {
|
|
898
|
-
throw Object.assign(new Error("Device permission denied"), {
|
|
899
|
-
code: HardwareErrorCode2.DevicePermissionDenied
|
|
1474
|
+
throw Object.assign(new Error(message ?? "Device permission denied"), {
|
|
1475
|
+
code: HardwareErrorCode2.DevicePermissionDenied,
|
|
1476
|
+
reason
|
|
900
1477
|
});
|
|
901
1478
|
}
|
|
902
1479
|
}
|
|
@@ -906,12 +1483,14 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
906
1483
|
*/
|
|
907
1484
|
errorToFailure(err) {
|
|
908
1485
|
debugError("[LedgerAdapter] error:", err);
|
|
1486
|
+
const tag = err && typeof err === "object" ? err._tag : void 0;
|
|
909
1487
|
if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
|
|
910
1488
|
const e = err;
|
|
911
|
-
|
|
1489
|
+
const params = e.code === HardwareErrorCode2.DevicePermissionDenied && e.reason ? { permissionDeniedReason: e.reason } : void 0;
|
|
1490
|
+
return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName, tag, params);
|
|
912
1491
|
}
|
|
913
1492
|
const mapped = mapLedgerError(err);
|
|
914
|
-
return ledgerFailure(mapped.code, mapped.message, mapped.appName);
|
|
1493
|
+
return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
|
|
915
1494
|
}
|
|
916
1495
|
registerEventListeners() {
|
|
917
1496
|
this.connector.on("device-connect", this.deviceConnectHandler);
|
|
@@ -927,32 +1506,38 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
927
1506
|
// Device info mapping
|
|
928
1507
|
// ---------------------------------------------------------------------------
|
|
929
1508
|
connectorDeviceToDeviceInfo(device) {
|
|
930
|
-
const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
|
|
931
1509
|
return {
|
|
932
1510
|
vendor: "ledger",
|
|
933
1511
|
model: device.model ?? "unknown",
|
|
1512
|
+
modelName: device.modelName,
|
|
934
1513
|
firmwareVersion: "",
|
|
935
1514
|
deviceId: device.deviceId,
|
|
936
1515
|
connectId: device.connectId,
|
|
937
1516
|
label: device.name,
|
|
938
|
-
connectionType:
|
|
1517
|
+
connectionType: this.connector.connectionType,
|
|
1518
|
+
rssi: device.rssi,
|
|
1519
|
+
isConnectable: device.isConnectable,
|
|
1520
|
+
serialNumber: device.serialNumber,
|
|
939
1521
|
capabilities: device.capabilities
|
|
940
1522
|
};
|
|
941
1523
|
}
|
|
942
1524
|
};
|
|
943
|
-
//
|
|
944
|
-
//
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
_LedgerAdapter.
|
|
1525
|
+
// Layer 2 retry budget after connection-class error. Each round delegates
|
|
1526
|
+
// to Layer 1 (which owns the unlock prompt). Layer 2 never emits UI itself.
|
|
1527
|
+
_LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET = 3;
|
|
1528
|
+
// Stuck-app (APDU 0x6901) retry pause. Ledger Stax often returns 6901 when
|
|
1529
|
+
// OpenAppCommand lands mid-transition right after CloseApp; a short wait
|
|
1530
|
+
// lets the device finish its UI animation before we retry once.
|
|
1531
|
+
_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS = 500;
|
|
1532
|
+
// Layer 1 confirm budget. After N failed Confirm cycles (user keeps clicking
|
|
1533
|
+
// Confirm but device never shows up / never unlocks), throw DeviceNotFound
|
|
1534
|
+
// instead of looping forever.
|
|
1535
|
+
_LedgerAdapter.MAX_DOCONNECT_CONFIRMS = 3;
|
|
954
1536
|
var LedgerAdapter = _LedgerAdapter;
|
|
955
1537
|
|
|
1538
|
+
// src/connector/LedgerConnectorBase.ts
|
|
1539
|
+
import { HardwareErrorCode as HardwareErrorCode7 } from "@onekeyfe/hwk-adapter-core";
|
|
1540
|
+
|
|
956
1541
|
// src/device/LedgerDeviceManager.ts
|
|
957
1542
|
var LedgerDeviceManager = class {
|
|
958
1543
|
constructor(dmk) {
|
|
@@ -989,7 +1574,9 @@ var LedgerDeviceManager = class {
|
|
|
989
1574
|
if (resolved) return;
|
|
990
1575
|
resolved = true;
|
|
991
1576
|
this._discovered.clear();
|
|
992
|
-
debugLog(
|
|
1577
|
+
debugLog(
|
|
1578
|
+
`[DMK] enumerate count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
|
|
1579
|
+
);
|
|
993
1580
|
for (const d of devices) {
|
|
994
1581
|
this._discovered.set(d.id, d);
|
|
995
1582
|
}
|
|
@@ -999,8 +1586,10 @@ var LedgerDeviceManager = class {
|
|
|
999
1586
|
devices.map((d) => ({
|
|
1000
1587
|
path: d.id,
|
|
1001
1588
|
type: d.deviceModel.model,
|
|
1589
|
+
modelName: d.deviceModel.name,
|
|
1002
1590
|
name: d.name,
|
|
1003
|
-
transport: d.transport
|
|
1591
|
+
transport: d.transport,
|
|
1592
|
+
rssi: d.rssi
|
|
1004
1593
|
}))
|
|
1005
1594
|
);
|
|
1006
1595
|
} else {
|
|
@@ -1021,8 +1610,10 @@ var LedgerDeviceManager = class {
|
|
|
1021
1610
|
devices.map((d) => ({
|
|
1022
1611
|
path: d.id,
|
|
1023
1612
|
type: d.deviceModel.model,
|
|
1613
|
+
modelName: d.deviceModel.name,
|
|
1024
1614
|
name: d.name,
|
|
1025
|
-
transport: d.transport
|
|
1615
|
+
transport: d.transport,
|
|
1616
|
+
rssi: d.rssi
|
|
1026
1617
|
}))
|
|
1027
1618
|
);
|
|
1028
1619
|
}
|
|
@@ -1046,8 +1637,10 @@ var LedgerDeviceManager = class {
|
|
|
1046
1637
|
descriptor: {
|
|
1047
1638
|
path: d.id,
|
|
1048
1639
|
type: d.deviceModel.model,
|
|
1640
|
+
modelName: d.deviceModel.name,
|
|
1049
1641
|
name: d.name,
|
|
1050
|
-
transport: d.transport
|
|
1642
|
+
transport: d.transport,
|
|
1643
|
+
rssi: d.rssi
|
|
1051
1644
|
}
|
|
1052
1645
|
});
|
|
1053
1646
|
}
|
|
@@ -1066,6 +1659,54 @@ var LedgerDeviceManager = class {
|
|
|
1066
1659
|
this._listenSub?.unsubscribe();
|
|
1067
1660
|
this._listenSub = null;
|
|
1068
1661
|
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Resolve to the latest snapshot of advertising devices observed within
|
|
1664
|
+
* `timeoutMs`. Rewrites `_discovered` so it tracks the live list.
|
|
1665
|
+
*
|
|
1666
|
+
* `listenToAvailableDevices` is a BehaviorSubject — it emits the cached
|
|
1667
|
+
* list on subscribe and *only* re-emits when the list changes. A stable
|
|
1668
|
+
* list of currently-advertising devices therefore produces only the
|
|
1669
|
+
* subscription frame; treating that frame as "fossil to be discarded"
|
|
1670
|
+
* caused devices to flicker in/out of the UI as searches alternated
|
|
1671
|
+
* between "saw a change frame" and "saw only the cached frame".
|
|
1672
|
+
*
|
|
1673
|
+
* The window still gives a chance for the active scan to update the list
|
|
1674
|
+
* (e.g. drop a peripheral that just stopped broadcasting), but if nothing
|
|
1675
|
+
* changes we trust the snapshot — DMK silence on a stable list means
|
|
1676
|
+
* "everything's still there".
|
|
1677
|
+
*/
|
|
1678
|
+
getLiveDevices(timeoutMs = 1500) {
|
|
1679
|
+
debugLog(`[DMK] getLiveDevices() called, timeoutMs=${timeoutMs}`);
|
|
1680
|
+
void this.requestDevice();
|
|
1681
|
+
return new Promise((resolve) => {
|
|
1682
|
+
let done = false;
|
|
1683
|
+
let lastSeen = [];
|
|
1684
|
+
let sub = null;
|
|
1685
|
+
const finish = (devices) => {
|
|
1686
|
+
if (done) return;
|
|
1687
|
+
done = true;
|
|
1688
|
+
sub?.unsubscribe();
|
|
1689
|
+
clearTimeout(timer);
|
|
1690
|
+
this._discovered.clear();
|
|
1691
|
+
for (const d of devices) this._discovered.set(d.id, d);
|
|
1692
|
+
debugLog(
|
|
1693
|
+
`[DMK] getLiveDevices() resolved count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
|
|
1694
|
+
);
|
|
1695
|
+
resolve(devices);
|
|
1696
|
+
};
|
|
1697
|
+
const timer = setTimeout(() => finish(lastSeen), timeoutMs);
|
|
1698
|
+
sub = this._dmk.listenToAvailableDevices({}).subscribe({
|
|
1699
|
+
next: (devices) => {
|
|
1700
|
+
lastSeen = devices;
|
|
1701
|
+
},
|
|
1702
|
+
error: (err) => {
|
|
1703
|
+
debugError("[DMK] getLiveDevices() error:", err);
|
|
1704
|
+
finish(lastSeen);
|
|
1705
|
+
},
|
|
1706
|
+
complete: () => finish(lastSeen)
|
|
1707
|
+
});
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1069
1710
|
requestDevice() {
|
|
1070
1711
|
if (this._discoverySub) {
|
|
1071
1712
|
return Promise.resolve();
|
|
@@ -1083,11 +1724,33 @@ var LedgerDeviceManager = class {
|
|
|
1083
1724
|
});
|
|
1084
1725
|
return Promise.resolve();
|
|
1085
1726
|
}
|
|
1727
|
+
/** Lookup a previously-discovered device's display name. */
|
|
1728
|
+
getDeviceName(deviceId) {
|
|
1729
|
+
return this._discovered.get(deviceId)?.name;
|
|
1730
|
+
}
|
|
1731
|
+
/** Lookup minimal model/signal info from a previously-discovered device. */
|
|
1732
|
+
getDiscoveredDeviceInfo(deviceId) {
|
|
1733
|
+
const d = this._discovered.get(deviceId);
|
|
1734
|
+
if (!d) return void 0;
|
|
1735
|
+
return {
|
|
1736
|
+
model: d.deviceModel?.model,
|
|
1737
|
+
modelName: d.deviceModel?.name,
|
|
1738
|
+
name: d.name,
|
|
1739
|
+
rssi: d.rssi
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
hasDiscoveredDevice(deviceId) {
|
|
1743
|
+
return this._discovered.has(deviceId);
|
|
1744
|
+
}
|
|
1086
1745
|
/** Connect to a previously discovered device. Returns sessionId. */
|
|
1087
1746
|
async connect(deviceId) {
|
|
1088
1747
|
const device = this._discovered.get(deviceId);
|
|
1089
1748
|
if (!device) {
|
|
1090
|
-
|
|
1749
|
+
const err = new Error(
|
|
1750
|
+
`Device "${deviceId}" not found in discovery cache. Call enumerate() or listen() first.`
|
|
1751
|
+
);
|
|
1752
|
+
err._tag = ERROR_TAG.DeviceNotInDiscoveryCache;
|
|
1753
|
+
throw err;
|
|
1091
1754
|
}
|
|
1092
1755
|
const sessionId = await this._dmk.connect({ device });
|
|
1093
1756
|
this._sessions.set(deviceId, sessionId);
|
|
@@ -1126,6 +1789,19 @@ var LedgerDeviceManager = class {
|
|
|
1126
1789
|
this._sessionToDevice.clear();
|
|
1127
1790
|
this._dmk.close();
|
|
1128
1791
|
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Tear down RxJS subscriptions and local state without closing the DMK.
|
|
1794
|
+
* For light-reset paths that want to drop session state but keep the
|
|
1795
|
+
* underlying transport alive for retry. Call this instead of orphaning
|
|
1796
|
+
* the manager — bare null assignment leaks _listenSub / _discoverySub.
|
|
1797
|
+
*/
|
|
1798
|
+
disposeKeepingDmk() {
|
|
1799
|
+
this.stopListening();
|
|
1800
|
+
this.stopDiscovery();
|
|
1801
|
+
this._discovered.clear();
|
|
1802
|
+
this._sessions.clear();
|
|
1803
|
+
this._sessionToDevice.clear();
|
|
1804
|
+
}
|
|
1129
1805
|
};
|
|
1130
1806
|
|
|
1131
1807
|
// src/signer/SignerManager.ts
|
|
@@ -1137,11 +1813,12 @@ import { hexToBytes } from "@onekeyfe/hwk-adapter-core";
|
|
|
1137
1813
|
|
|
1138
1814
|
// src/signer/deviceActionToPromise.ts
|
|
1139
1815
|
import { DeviceActionStatus } from "@ledgerhq/device-management-kit";
|
|
1140
|
-
var
|
|
1141
|
-
function deviceActionToPromise(action, onInteraction, timeoutMs =
|
|
1816
|
+
var IDLE_WATCHDOG_MS = 65e3;
|
|
1817
|
+
function deviceActionToPromise(action, onInteraction, timeoutMs = IDLE_WATCHDOG_MS, onRegisterCanceller) {
|
|
1142
1818
|
return new Promise((resolve, reject) => {
|
|
1143
1819
|
let settled = false;
|
|
1144
1820
|
let lastStep;
|
|
1821
|
+
const observedSteps = [];
|
|
1145
1822
|
let sub;
|
|
1146
1823
|
let timer = null;
|
|
1147
1824
|
const cancelAction = () => {
|
|
@@ -1154,44 +1831,59 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1154
1831
|
} catch {
|
|
1155
1832
|
}
|
|
1156
1833
|
};
|
|
1157
|
-
const
|
|
1834
|
+
const armIdleWatchdog = () => {
|
|
1158
1835
|
if (timer) clearTimeout(timer);
|
|
1159
1836
|
if (timeoutMs > 0) {
|
|
1160
1837
|
timer = setTimeout(() => {
|
|
1161
1838
|
if (!settled) {
|
|
1162
1839
|
settled = true;
|
|
1163
1840
|
cancelAction();
|
|
1164
|
-
reject(
|
|
1841
|
+
reject(
|
|
1842
|
+
new Error(
|
|
1843
|
+
"Ledger transport unresponsive: no DMK state change within the watchdog window. The device may have lost connection."
|
|
1844
|
+
)
|
|
1845
|
+
);
|
|
1165
1846
|
}
|
|
1166
1847
|
}, timeoutMs);
|
|
1167
1848
|
}
|
|
1168
1849
|
};
|
|
1169
|
-
|
|
1850
|
+
armIdleWatchdog();
|
|
1170
1851
|
if (onRegisterCanceller) {
|
|
1171
|
-
onRegisterCanceller(() => {
|
|
1172
|
-
if (settled)
|
|
1852
|
+
onRegisterCanceller((reason) => {
|
|
1853
|
+
if (settled) {
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1173
1856
|
settled = true;
|
|
1174
1857
|
if (timer) clearTimeout(timer);
|
|
1175
1858
|
cancelAction();
|
|
1176
|
-
|
|
1859
|
+
const err = new Error(reason?.message ?? "Device action cancelled");
|
|
1860
|
+
if (reason?.code !== void 0) {
|
|
1861
|
+
err.code = reason.code;
|
|
1862
|
+
}
|
|
1863
|
+
if (reason?.tag) {
|
|
1864
|
+
Object.defineProperty(err, "_tag", {
|
|
1865
|
+
value: reason.tag,
|
|
1866
|
+
configurable: true,
|
|
1867
|
+
enumerable: false,
|
|
1868
|
+
writable: true
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
reject(err);
|
|
1177
1872
|
});
|
|
1178
1873
|
}
|
|
1179
|
-
debugLog("[DMK-Observable] subscribing to action.observable...");
|
|
1180
1874
|
sub = action.observable.subscribe({
|
|
1181
1875
|
next: (state) => {
|
|
1182
|
-
if (settled)
|
|
1183
|
-
|
|
1876
|
+
if (settled) {
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
armIdleWatchdog();
|
|
1184
1880
|
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
|
-
);
|
|
1881
|
+
if (step) {
|
|
1882
|
+
lastStep = step;
|
|
1883
|
+
if (observedSteps[observedSteps.length - 1] !== step) {
|
|
1884
|
+
observedSteps.push(step);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1195
1887
|
if (state.status === DeviceActionStatus.Completed) {
|
|
1196
1888
|
settled = true;
|
|
1197
1889
|
if (timer) clearTimeout(timer);
|
|
@@ -1203,10 +1895,14 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1203
1895
|
if (timer) clearTimeout(timer);
|
|
1204
1896
|
onInteraction?.("interaction-complete");
|
|
1205
1897
|
sub?.unsubscribe();
|
|
1206
|
-
|
|
1898
|
+
rejectWithStepContext(state.error, lastStep, observedSteps, reject);
|
|
1207
1899
|
} else if (state.status === DeviceActionStatus.Pending && onInteraction) {
|
|
1208
1900
|
const interaction = state.intermediateValue?.requiredUserInteraction;
|
|
1209
1901
|
if (interaction && interaction !== "none") {
|
|
1902
|
+
if (interaction === "unlock-device" && timer) {
|
|
1903
|
+
clearTimeout(timer);
|
|
1904
|
+
timer = null;
|
|
1905
|
+
}
|
|
1210
1906
|
onInteraction(String(interaction));
|
|
1211
1907
|
}
|
|
1212
1908
|
}
|
|
@@ -1216,7 +1912,7 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1216
1912
|
settled = true;
|
|
1217
1913
|
if (timer) clearTimeout(timer);
|
|
1218
1914
|
sub?.unsubscribe();
|
|
1219
|
-
|
|
1915
|
+
rejectWithStepContext(err, lastStep, observedSteps, reject);
|
|
1220
1916
|
}
|
|
1221
1917
|
},
|
|
1222
1918
|
complete: () => {
|
|
@@ -1229,15 +1925,25 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1229
1925
|
});
|
|
1230
1926
|
});
|
|
1231
1927
|
}
|
|
1232
|
-
function
|
|
1233
|
-
if (err && typeof err === "object" && lastStep) {
|
|
1928
|
+
function rejectWithStepContext(err, lastStep, observedSteps, reject) {
|
|
1929
|
+
if (err && typeof err === "object" && (lastStep || observedSteps.length > 0)) {
|
|
1234
1930
|
try {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1931
|
+
if (lastStep) {
|
|
1932
|
+
Object.defineProperty(err, "_lastStep", {
|
|
1933
|
+
value: lastStep,
|
|
1934
|
+
configurable: true,
|
|
1935
|
+
enumerable: false,
|
|
1936
|
+
writable: true
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
if (observedSteps.length > 0) {
|
|
1940
|
+
Object.defineProperty(err, "_deviceActionSteps", {
|
|
1941
|
+
value: [...observedSteps],
|
|
1942
|
+
configurable: true,
|
|
1943
|
+
enumerable: false,
|
|
1944
|
+
writable: true
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1241
1947
|
} catch {
|
|
1242
1948
|
}
|
|
1243
1949
|
}
|
|
@@ -1303,7 +2009,8 @@ var SignerManager = class _SignerManager {
|
|
|
1303
2009
|
async getOrCreate(sessionId) {
|
|
1304
2010
|
debugLog("[DMK] SignerManager.getOrCreate:", { sessionId });
|
|
1305
2011
|
const builder = await this._builderFn({ dmk: this._dmk, sessionId });
|
|
1306
|
-
|
|
2012
|
+
const builderWithContext = builder.withContextModule ? builder.withContextModule(_SignerManager._createContextModule()) : builder;
|
|
2013
|
+
return new SignerEth(builderWithContext.build());
|
|
1307
2014
|
}
|
|
1308
2015
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
|
|
1309
2016
|
invalidate(_sessionId) {
|
|
@@ -1312,10 +2019,24 @@ var SignerManager = class _SignerManager {
|
|
|
1312
2019
|
clearAll() {
|
|
1313
2020
|
}
|
|
1314
2021
|
static _defaultBuilder() {
|
|
1315
|
-
return (args) =>
|
|
1316
|
-
|
|
1317
|
-
|
|
2022
|
+
return (args) => new SignerEthBuilder(args);
|
|
2023
|
+
}
|
|
2024
|
+
static _createContextModule() {
|
|
2025
|
+
const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
|
|
2026
|
+
return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
|
|
2027
|
+
}
|
|
2028
|
+
static wrapBlindSigningReportNonBlocking(contextModule) {
|
|
2029
|
+
const report = contextModule.report.bind(contextModule);
|
|
2030
|
+
contextModule.report = async (params) => {
|
|
2031
|
+
try {
|
|
2032
|
+
void report(params).catch((err) => {
|
|
2033
|
+
debugLog("[DMK] ContextModule.report failed:", err);
|
|
2034
|
+
});
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
debugLog("[DMK] ContextModule.report failed:", err);
|
|
2037
|
+
}
|
|
1318
2038
|
};
|
|
2039
|
+
return contextModule;
|
|
1319
2040
|
}
|
|
1320
2041
|
};
|
|
1321
2042
|
|
|
@@ -1323,20 +2044,20 @@ var SignerManager = class _SignerManager {
|
|
|
1323
2044
|
import { HardwareErrorCode as HardwareErrorCode3, padHex64, stripHex } from "@onekeyfe/hwk-adapter-core";
|
|
1324
2045
|
|
|
1325
2046
|
// src/connector/chains/utils.ts
|
|
1326
|
-
import { EConnectorInteraction } from "@onekeyfe/hwk-adapter-core";
|
|
2047
|
+
import { EConnectorInteraction as EConnectorInteraction2 } from "@onekeyfe/hwk-adapter-core";
|
|
1327
2048
|
function normalizePath(path) {
|
|
1328
2049
|
return path.startsWith("m/") ? path.slice(2) : path;
|
|
1329
2050
|
}
|
|
1330
2051
|
function collapseSignerInteraction(interaction) {
|
|
1331
2052
|
switch (interaction) {
|
|
1332
2053
|
case "confirm-open-app":
|
|
1333
|
-
return
|
|
2054
|
+
return EConnectorInteraction2.ConfirmOpenApp;
|
|
1334
2055
|
case "unlock-device":
|
|
1335
|
-
return
|
|
2056
|
+
return EConnectorInteraction2.UnlockDevice;
|
|
1336
2057
|
case "interaction-complete":
|
|
1337
|
-
return
|
|
2058
|
+
return EConnectorInteraction2.InteractionComplete;
|
|
1338
2059
|
default:
|
|
1339
|
-
return
|
|
2060
|
+
return EConnectorInteraction2.ConfirmOnDevice;
|
|
1340
2061
|
}
|
|
1341
2062
|
}
|
|
1342
2063
|
|
|
@@ -1425,6 +2146,7 @@ async function _getEthSigner(ctx, sessionId) {
|
|
|
1425
2146
|
const signerManager = await ctx.getSignerManager();
|
|
1426
2147
|
const signer = await signerManager.getOrCreate(sessionId);
|
|
1427
2148
|
signer.onInteraction = (interaction) => {
|
|
2149
|
+
debugLog("[LedgerConnector] evm.onInteraction:", interaction);
|
|
1428
2150
|
ctx.emit("ui-event", {
|
|
1429
2151
|
type: collapseSignerInteraction(interaction),
|
|
1430
2152
|
payload: { sessionId }
|
|
@@ -1700,6 +2422,7 @@ async function _createBtcSigner(ctx, sessionId) {
|
|
|
1700
2422
|
const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
|
|
1701
2423
|
const signer = new SignerBtc(sdkSigner);
|
|
1702
2424
|
signer.onInteraction = (interaction) => {
|
|
2425
|
+
debugLog("[LedgerConnector] btc.onInteraction:", interaction);
|
|
1703
2426
|
ctx.emit("ui-event", {
|
|
1704
2427
|
type: collapseSignerInteraction(interaction),
|
|
1705
2428
|
payload: { sessionId }
|
|
@@ -1870,6 +2593,7 @@ async function _createSolSigner(ctx, sessionId) {
|
|
|
1870
2593
|
const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
|
|
1871
2594
|
const signer = new SignerSol(sdkSigner);
|
|
1872
2595
|
signer.onInteraction = (interaction) => {
|
|
2596
|
+
debugLog("[LedgerConnector] sol.onInteraction:", interaction);
|
|
1873
2597
|
ctx.emit("ui-event", {
|
|
1874
2598
|
type: collapseSignerInteraction(interaction),
|
|
1875
2599
|
payload: { sessionId }
|
|
@@ -1882,11 +2606,11 @@ async function _createSolSigner(ctx, sessionId) {
|
|
|
1882
2606
|
}
|
|
1883
2607
|
|
|
1884
2608
|
// src/connector/chains/tron.ts
|
|
1885
|
-
import { HardwareErrorCode as
|
|
2609
|
+
import { HardwareErrorCode as HardwareErrorCode6 } from "@onekeyfe/hwk-adapter-core";
|
|
1886
2610
|
import Trx from "@ledgerhq/hw-app-trx";
|
|
1887
2611
|
|
|
1888
2612
|
// src/connector/chains/legacyChainCall.ts
|
|
1889
|
-
import { EConnectorInteraction as
|
|
2613
|
+
import { EConnectorInteraction as EConnectorInteraction3 } from "@onekeyfe/hwk-adapter-core";
|
|
1890
2614
|
|
|
1891
2615
|
// src/app/AppManager.ts
|
|
1892
2616
|
import {
|
|
@@ -1895,6 +2619,7 @@ import {
|
|
|
1895
2619
|
OpenAppCommand,
|
|
1896
2620
|
isSuccessCommandResult
|
|
1897
2621
|
} from "@ledgerhq/device-management-kit";
|
|
2622
|
+
import { HardwareErrorCode as HardwareErrorCode5 } from "@onekeyfe/hwk-adapter-core";
|
|
1898
2623
|
var APP_NAME_MAP = {
|
|
1899
2624
|
ETH: "Ethereum",
|
|
1900
2625
|
BTC: "Bitcoin",
|
|
@@ -1966,7 +2691,31 @@ var AppManager = class {
|
|
|
1966
2691
|
debugLog("[AppManager] currentApp:", result.data.name);
|
|
1967
2692
|
return result.data.name;
|
|
1968
2693
|
}
|
|
1969
|
-
|
|
2694
|
+
const errResult = result;
|
|
2695
|
+
const dmkErr = errResult.error ?? {};
|
|
2696
|
+
const original = dmkErr.originalError;
|
|
2697
|
+
debugLog(
|
|
2698
|
+
"[AppManager] _getCurrentApp failed sessionId=",
|
|
2699
|
+
sessionId,
|
|
2700
|
+
"tag=",
|
|
2701
|
+
dmkErr._tag,
|
|
2702
|
+
"errorCode=",
|
|
2703
|
+
dmkErr.errorCode,
|
|
2704
|
+
"message=",
|
|
2705
|
+
dmkErr.message,
|
|
2706
|
+
"originalErrorMessage=",
|
|
2707
|
+
original?.message ?? String(original ?? "")
|
|
2708
|
+
);
|
|
2709
|
+
throw Object.assign(
|
|
2710
|
+
new Error(
|
|
2711
|
+
dmkErr.message ?? "Failed to get current app from device"
|
|
2712
|
+
),
|
|
2713
|
+
{
|
|
2714
|
+
_tag: dmkErr._tag,
|
|
2715
|
+
errorCode: dmkErr.errorCode,
|
|
2716
|
+
originalError: original
|
|
2717
|
+
}
|
|
2718
|
+
);
|
|
1970
2719
|
}
|
|
1971
2720
|
async _openApp(sessionId, appName) {
|
|
1972
2721
|
const result = await this._dmk.sendCommand({
|
|
@@ -1975,9 +2724,11 @@ var AppManager = class {
|
|
|
1975
2724
|
});
|
|
1976
2725
|
if (!isSuccessCommandResult(result)) {
|
|
1977
2726
|
const { statusCode } = result;
|
|
2727
|
+
const hasStatusCode2 = statusCode != null && statusCode !== "";
|
|
1978
2728
|
debugLog("[AppManager] openApp failed:", appName, "statusCode:", statusCode);
|
|
1979
2729
|
throw Object.assign(new Error(`Failed to open "${appName}"`), {
|
|
1980
|
-
_tag:
|
|
2730
|
+
_tag: ERROR_TAG.OpenAppCommand,
|
|
2731
|
+
code: hasStatusCode2 ? void 0 : HardwareErrorCode5.AppNotInstalled,
|
|
1981
2732
|
errorCode: String(statusCode ?? ""),
|
|
1982
2733
|
statusCode,
|
|
1983
2734
|
appName
|
|
@@ -2033,14 +2784,14 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
|
|
|
2033
2784
|
const onAppOpenPrompt = () => {
|
|
2034
2785
|
openAppPromptShown = true;
|
|
2035
2786
|
ctx.emit("ui-event", {
|
|
2036
|
-
type:
|
|
2787
|
+
type: EConnectorInteraction3.ConfirmOpenApp,
|
|
2037
2788
|
payload: { sessionId }
|
|
2038
2789
|
});
|
|
2039
2790
|
};
|
|
2040
2791
|
const closeOpenAppUiIfShown = () => {
|
|
2041
2792
|
if (openAppPromptShown) {
|
|
2042
2793
|
ctx.emit("ui-event", {
|
|
2043
|
-
type:
|
|
2794
|
+
type: EConnectorInteraction3.InteractionComplete,
|
|
2044
2795
|
payload: { sessionId }
|
|
2045
2796
|
});
|
|
2046
2797
|
openAppPromptShown = false;
|
|
@@ -2061,7 +2812,7 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
|
|
|
2061
2812
|
let confirmEmitted = false;
|
|
2062
2813
|
if (needsConfirmation) {
|
|
2063
2814
|
ctx.emit("ui-event", {
|
|
2064
|
-
type:
|
|
2815
|
+
type: EConnectorInteraction3.ConfirmOnDevice,
|
|
2065
2816
|
payload: { sessionId }
|
|
2066
2817
|
});
|
|
2067
2818
|
confirmEmitted = true;
|
|
@@ -2071,7 +2822,7 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
|
|
|
2071
2822
|
} finally {
|
|
2072
2823
|
if (confirmEmitted || openAppPromptShown) {
|
|
2073
2824
|
ctx.emit("ui-event", {
|
|
2074
|
-
type:
|
|
2825
|
+
type: EConnectorInteraction3.InteractionComplete,
|
|
2075
2826
|
payload: { sessionId }
|
|
2076
2827
|
});
|
|
2077
2828
|
openAppPromptShown = false;
|
|
@@ -2159,7 +2910,7 @@ async function tronSignTransaction(ctx, sessionId, params) {
|
|
|
2159
2910
|
if (!params.rawTxHex) {
|
|
2160
2911
|
throw Object.assign(
|
|
2161
2912
|
new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
|
|
2162
|
-
{ code:
|
|
2913
|
+
{ code: HardwareErrorCode6.InvalidParams }
|
|
2163
2914
|
);
|
|
2164
2915
|
}
|
|
2165
2916
|
const path = normalizePath(params.path);
|
|
@@ -2203,6 +2954,10 @@ var METHOD_PREFIX_TO_APP_NAME = {
|
|
|
2203
2954
|
sol: "Solana",
|
|
2204
2955
|
tron: "Tron"
|
|
2205
2956
|
};
|
|
2957
|
+
var HARDWARE_ERROR_CODE_VALUES = new Set(
|
|
2958
|
+
Object.values(HardwareErrorCode7).filter((value) => typeof value === "number")
|
|
2959
|
+
);
|
|
2960
|
+
var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
2206
2961
|
async function defaultLedgerKitImporter(pkg) {
|
|
2207
2962
|
switch (pkg) {
|
|
2208
2963
|
case "@ledgerhq/device-management-kit":
|
|
@@ -2226,16 +2981,6 @@ var LedgerConnectorBase = class {
|
|
|
2226
2981
|
this._dmk = null;
|
|
2227
2982
|
this._eventHandlers = /* @__PURE__ */ new Map();
|
|
2228
2983
|
// ---------------------------------------------------------------------------
|
|
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
2984
|
// Per-session DeviceAction cancellers
|
|
2240
2985
|
//
|
|
2241
2986
|
// Each chain handler registers its active DeviceAction's canceller via
|
|
@@ -2244,10 +2989,23 @@ var LedgerConnectorBase = class {
|
|
|
2244
2989
|
// unsubscribes the observable and releases DMK's IntentQueue slot.
|
|
2245
2990
|
// ---------------------------------------------------------------------------
|
|
2246
2991
|
this._cancellers = /* @__PURE__ */ new Map();
|
|
2992
|
+
// ---------------------------------------------------------------------------
|
|
2993
|
+
// Per-session DMK state subscriptions
|
|
2994
|
+
//
|
|
2995
|
+
// DMK only emits `device-disconnect` on the connector when `disconnect()`
|
|
2996
|
+
// is called explicitly. To pick up autonomous disconnects (USB unplug,
|
|
2997
|
+
// BLE drop, device sleep, DMK transport reset) we subscribe to
|
|
2998
|
+
// `_dmk.getDeviceSessionState({sessionId})` per active session and emit
|
|
2999
|
+
// `device-disconnect` ourselves the moment DMK reports the device went
|
|
3000
|
+
// to NOT_CONNECTED. The subscription is torn down on disconnect /
|
|
3001
|
+
// reset / observable completion to avoid leaks.
|
|
3002
|
+
// ---------------------------------------------------------------------------
|
|
3003
|
+
this._sessionStateSubs = /* @__PURE__ */ new Map();
|
|
2247
3004
|
this._createTransport = createTransport;
|
|
2248
3005
|
this.connectionType = options?.connectionType ?? "usb";
|
|
2249
3006
|
this._providedDmk = options?.dmk;
|
|
2250
3007
|
this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
|
|
3008
|
+
this._requirePreFlightScan = options?.requirePreFlightScan ?? false;
|
|
2251
3009
|
if (this._providedDmk) {
|
|
2252
3010
|
this._initManagers(this._providedDmk);
|
|
2253
3011
|
}
|
|
@@ -2265,38 +3023,23 @@ var LedgerConnectorBase = class {
|
|
|
2265
3023
|
importLedgerKit: this._importLedgerKit
|
|
2266
3024
|
};
|
|
2267
3025
|
}
|
|
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
3026
|
// ---------------------------------------------------------------------------
|
|
2285
3027
|
// Protected — hooks for subclasses
|
|
2286
3028
|
// ---------------------------------------------------------------------------
|
|
2287
3029
|
/**
|
|
2288
3030
|
* Resolve the connectId for a discovered device descriptor.
|
|
2289
3031
|
* Default: use the DMK path (ephemeral UUID).
|
|
2290
|
-
* Override in subclasses
|
|
3032
|
+
* Override in subclasses only when the public connectId differs from the transport path.
|
|
2291
3033
|
*/
|
|
2292
3034
|
_resolveConnectId(descriptor) {
|
|
2293
3035
|
return descriptor.path;
|
|
2294
3036
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
3037
|
+
/**
|
|
3038
|
+
* Authoritative discovery for this transport. Default = enumerate snapshot
|
|
3039
|
+
* (USB/HID). BLE overrides to drive from a live scan window since DMK's
|
|
3040
|
+
* enumerate() cache survives peripherals going offline.
|
|
3041
|
+
*/
|
|
3042
|
+
async _discoverDescriptors(dm) {
|
|
2300
3043
|
let descriptors = await dm.enumerate();
|
|
2301
3044
|
if (descriptors.length === 0) {
|
|
2302
3045
|
try {
|
|
@@ -2305,92 +3048,266 @@ var LedgerConnectorBase = class {
|
|
|
2305
3048
|
}
|
|
2306
3049
|
descriptors = await dm.enumerate();
|
|
2307
3050
|
}
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
3051
|
+
return descriptors;
|
|
3052
|
+
}
|
|
3053
|
+
// ---------------------------------------------------------------------------
|
|
3054
|
+
// IConnector -- Device discovery
|
|
3055
|
+
// ---------------------------------------------------------------------------
|
|
3056
|
+
async searchDevices() {
|
|
3057
|
+
const dm = await this._getDeviceManager();
|
|
3058
|
+
const descriptors = await this._discoverDescriptors(dm);
|
|
3059
|
+
const resolvedDescriptors = descriptors.map((d) => ({
|
|
3060
|
+
descriptor: d,
|
|
3061
|
+
connectId: this._resolveConnectId(d)
|
|
3062
|
+
}));
|
|
3063
|
+
const result = resolvedDescriptors.map(({ descriptor: d, connectId }) => ({
|
|
3064
|
+
connectId,
|
|
3065
|
+
deviceId: d.path,
|
|
3066
|
+
name: d.name || d.type || "Ledger",
|
|
3067
|
+
model: d.type,
|
|
3068
|
+
modelName: d.modelName,
|
|
3069
|
+
rssi: d.rssi,
|
|
3070
|
+
isConnectable: d.isConnectable,
|
|
3071
|
+
serialNumber: d.serialNumber
|
|
3072
|
+
}));
|
|
3073
|
+
debugLog(
|
|
3074
|
+
`[DMK] connector.searchDevices() return count=${result.length} ids=[${result.map((r) => r.connectId).join(",")}] paths=[${result.map((r) => r.deviceId).join(",")}]`
|
|
3075
|
+
);
|
|
2317
3076
|
return result;
|
|
2318
3077
|
}
|
|
2319
3078
|
// ---------------------------------------------------------------------------
|
|
2320
3079
|
// IConnector -- Connection
|
|
2321
3080
|
// ---------------------------------------------------------------------------
|
|
2322
3081
|
async connect(deviceId) {
|
|
2323
|
-
const
|
|
2324
|
-
let
|
|
2325
|
-
const dmkPath = deviceId ? this._getPathForConnectId(deviceId) : void 0;
|
|
2326
|
-
let targetPath = dmkPath;
|
|
3082
|
+
const callerSuppliedConnectId = Boolean(deviceId);
|
|
3083
|
+
let targetPath = deviceId;
|
|
2327
3084
|
if (!targetPath) {
|
|
2328
|
-
const
|
|
2329
|
-
if (
|
|
3085
|
+
const discovered = await this.searchDevices();
|
|
3086
|
+
if (discovered.length === 0) {
|
|
2330
3087
|
throw new Error(
|
|
2331
|
-
`No Ledger device found. Make sure the device is connected${this.connectionType
|
|
3088
|
+
`No Ledger device found. Make sure the device is connected${isLedgerBleConnectionType(this.connectionType) ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
|
|
2332
3089
|
);
|
|
2333
3090
|
}
|
|
2334
|
-
targetPath =
|
|
2335
|
-
}
|
|
2336
|
-
const externalConnectId =
|
|
3091
|
+
targetPath = discovered[0].deviceId;
|
|
3092
|
+
}
|
|
3093
|
+
const externalConnectId = targetPath;
|
|
3094
|
+
const HANG_CEILING_MS = 5 * 6e4;
|
|
3095
|
+
const dmConnectWithObserve = async (dm, path) => {
|
|
3096
|
+
if (!isLedgerBleConnectionType(this.connectionType)) return dm.connect(path);
|
|
3097
|
+
let timeoutId;
|
|
3098
|
+
let timedOut = false;
|
|
3099
|
+
const connectPromise = dm.connect(path);
|
|
3100
|
+
const hangPromise = new Promise((_, reject) => {
|
|
3101
|
+
timeoutId = setTimeout(() => {
|
|
3102
|
+
timedOut = true;
|
|
3103
|
+
const err = new Error(
|
|
3104
|
+
`Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
|
|
3105
|
+
);
|
|
3106
|
+
err._tag = ERROR_TAG.BlePairingTimeout;
|
|
3107
|
+
err.code = HardwareErrorCode7.BlePairingTimeout;
|
|
3108
|
+
reject(err);
|
|
3109
|
+
}, HANG_CEILING_MS);
|
|
3110
|
+
});
|
|
3111
|
+
try {
|
|
3112
|
+
return await Promise.race([connectPromise, hangPromise]);
|
|
3113
|
+
} catch (err) {
|
|
3114
|
+
const e = err;
|
|
3115
|
+
debugLog("[DMK] dm.connect rejected \u2014 observed:", {
|
|
3116
|
+
_tag: e?._tag,
|
|
3117
|
+
message: e?.message,
|
|
3118
|
+
errorCode: e?.errorCode,
|
|
3119
|
+
statusCode: e?.statusCode,
|
|
3120
|
+
originalTag: e?.originalError?._tag,
|
|
3121
|
+
originalMessage: e?.originalError?.message
|
|
3122
|
+
});
|
|
3123
|
+
if (timedOut) {
|
|
3124
|
+
void connectPromise.then(
|
|
3125
|
+
(sessionId) => dm.disconnect(sessionId).catch(() => void 0),
|
|
3126
|
+
() => void 0
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
throw err;
|
|
3130
|
+
} finally {
|
|
3131
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
const isBleDirectConnect = isLedgerBleConnectionType(this.connectionType) && callerSuppliedConnectId;
|
|
3135
|
+
const throwNotAdvertising = () => {
|
|
3136
|
+
const err = new Error(
|
|
3137
|
+
"Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
|
|
3138
|
+
);
|
|
3139
|
+
err._tag = ERROR_TAG.DeviceNotAdvertising;
|
|
3140
|
+
err.code = HardwareErrorCode7.DeviceNotFound;
|
|
3141
|
+
throw err;
|
|
3142
|
+
};
|
|
2337
3143
|
const doConnect = async (path) => {
|
|
2338
|
-
const
|
|
3144
|
+
const dm = await this._getDeviceManager();
|
|
3145
|
+
if (isBleDirectConnect && !dm.hasDiscoveredDevice(path)) {
|
|
3146
|
+
const live = await dm.getLiveDevices(BLE_CONNECT_SCAN_TIMEOUT_MS);
|
|
3147
|
+
if (this._requirePreFlightScan && !live.some((d) => d.id === path)) {
|
|
3148
|
+
throwNotAdvertising();
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
let sessionId;
|
|
3152
|
+
try {
|
|
3153
|
+
sessionId = await dmConnectWithObserve(dm, path);
|
|
3154
|
+
} catch (err) {
|
|
3155
|
+
if (isBleDirectConnect && err?._tag === ERROR_TAG.DeviceNotInDiscoveryCache) {
|
|
3156
|
+
throwNotAdvertising();
|
|
3157
|
+
}
|
|
3158
|
+
throw err;
|
|
3159
|
+
}
|
|
3160
|
+
this._watchSessionState(sessionId, externalConnectId);
|
|
3161
|
+
const info = dm.getDiscoveredDeviceInfo(path);
|
|
2339
3162
|
const session = {
|
|
2340
3163
|
sessionId,
|
|
2341
3164
|
deviceInfo: {
|
|
2342
3165
|
vendor: "ledger",
|
|
2343
|
-
model: "unknown",
|
|
3166
|
+
model: info?.model ?? "unknown",
|
|
3167
|
+
modelName: info?.modelName,
|
|
2344
3168
|
firmwareVersion: "unknown",
|
|
2345
3169
|
deviceId: path,
|
|
2346
3170
|
connectId: externalConnectId,
|
|
2347
3171
|
connectionType: this.connectionType,
|
|
3172
|
+
rssi: info?.rssi,
|
|
2348
3173
|
capabilities: { persistentDeviceIdentity: false }
|
|
2349
3174
|
}
|
|
2350
3175
|
};
|
|
2351
|
-
const
|
|
3176
|
+
const name = dm.getDeviceName(path) ?? "Ledger";
|
|
2352
3177
|
this._emit("device-connect", {
|
|
2353
|
-
device: {
|
|
2354
|
-
connectId: externalConnectId,
|
|
2355
|
-
deviceId: path,
|
|
2356
|
-
name: realName
|
|
2357
|
-
}
|
|
3178
|
+
device: { connectId: externalConnectId, deviceId: path, name }
|
|
2358
3179
|
});
|
|
2359
3180
|
return session;
|
|
2360
3181
|
};
|
|
2361
3182
|
try {
|
|
2362
3183
|
return await doConnect(targetPath);
|
|
2363
|
-
} catch {
|
|
3184
|
+
} catch (err) {
|
|
2364
3185
|
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
|
-
);
|
|
3186
|
+
if (isLedgerBleConnectionType(this.connectionType)) {
|
|
3187
|
+
const tag = err?._tag;
|
|
3188
|
+
if (isKnownConnectionTag(tag)) {
|
|
3189
|
+
throw err;
|
|
2374
3190
|
}
|
|
2375
|
-
|
|
3191
|
+
const wrapped = new Error(
|
|
3192
|
+
"Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
|
|
3193
|
+
);
|
|
3194
|
+
wrapped._tag = ERROR_TAG.BleGattBondingFailed;
|
|
3195
|
+
wrapped.code = HardwareErrorCode7.BlePairingTimeout;
|
|
3196
|
+
wrapped.originalError = err;
|
|
3197
|
+
throw wrapped;
|
|
2376
3198
|
}
|
|
2377
|
-
|
|
3199
|
+
if (err && typeof err === "object" && err._tag) {
|
|
3200
|
+
const taggedError = err instanceof Error ? err : Object.assign(
|
|
3201
|
+
new Error(err.message ?? "Ledger device error"),
|
|
3202
|
+
err
|
|
3203
|
+
);
|
|
3204
|
+
throw taggedError;
|
|
3205
|
+
}
|
|
3206
|
+
throw new Error("Ledger device appears unplugged. Plug in the device and try again.");
|
|
2378
3207
|
}
|
|
2379
3208
|
}
|
|
2380
3209
|
async disconnect(sessionId) {
|
|
2381
3210
|
if (!this._deviceManager) return;
|
|
2382
3211
|
const deviceId = this._deviceManager.getDeviceId(sessionId);
|
|
2383
3212
|
this._signerManager?.invalidate(sessionId);
|
|
3213
|
+
this._unwatchSessionState(sessionId);
|
|
2384
3214
|
await this._deviceManager.disconnect(sessionId);
|
|
2385
3215
|
if (deviceId) {
|
|
2386
3216
|
this._emit("device-disconnect", { connectId: deviceId });
|
|
2387
3217
|
}
|
|
2388
3218
|
}
|
|
3219
|
+
/**
|
|
3220
|
+
* Subscribe to DMK's per-session state observable so that any autonomous
|
|
3221
|
+
* disconnect (USB unplug, BLE drop, device sleep, transport reset) is
|
|
3222
|
+
* surfaced as a `device-disconnect` event — without this, the upstream
|
|
3223
|
+
* `LedgerAdapter._sessions` map would hold a dead session entry until
|
|
3224
|
+
* the next call hit `DeviceSessionNotFound`.
|
|
3225
|
+
*
|
|
3226
|
+
* Best-effort: any error subscribing is swallowed so a flaky DMK
|
|
3227
|
+
* doesn't break the connect path.
|
|
3228
|
+
*/
|
|
3229
|
+
_watchSessionState(sessionId, externalConnectId) {
|
|
3230
|
+
const dmk = this._dmk;
|
|
3231
|
+
if (!dmk) return;
|
|
3232
|
+
const previous = this._sessionStateSubs.get(sessionId);
|
|
3233
|
+
if (previous) {
|
|
3234
|
+
try {
|
|
3235
|
+
previous.unsubscribe();
|
|
3236
|
+
} catch {
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
try {
|
|
3240
|
+
const sub = dmk.getDeviceSessionState({ sessionId }).subscribe({
|
|
3241
|
+
next: (state) => {
|
|
3242
|
+
if (state?.deviceStatus === "NOT CONNECTED") {
|
|
3243
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3244
|
+
}
|
|
3245
|
+
},
|
|
3246
|
+
error: () => {
|
|
3247
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3248
|
+
},
|
|
3249
|
+
complete: () => {
|
|
3250
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3251
|
+
}
|
|
3252
|
+
});
|
|
3253
|
+
this._sessionStateSubs.set(sessionId, sub);
|
|
3254
|
+
} catch (err) {
|
|
3255
|
+
debugLog("[DMK] _watchSessionState subscribe failed:", err);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
_unwatchSessionState(sessionId) {
|
|
3259
|
+
const sub = this._sessionStateSubs.get(sessionId);
|
|
3260
|
+
if (!sub) return;
|
|
3261
|
+
try {
|
|
3262
|
+
sub.unsubscribe();
|
|
3263
|
+
} catch {
|
|
3264
|
+
}
|
|
3265
|
+
this._sessionStateSubs.delete(sessionId);
|
|
3266
|
+
}
|
|
3267
|
+
_handleAutonomousDisconnect(sessionId, externalConnectId) {
|
|
3268
|
+
if (!this._sessionStateSubs.has(sessionId)) return;
|
|
3269
|
+
debugLog(
|
|
3270
|
+
"[DMK] autonomous disconnect detected \u2014 sessionId:",
|
|
3271
|
+
sessionId,
|
|
3272
|
+
"connectId:",
|
|
3273
|
+
externalConnectId
|
|
3274
|
+
);
|
|
3275
|
+
this._unwatchSessionState(sessionId);
|
|
3276
|
+
this._signerManager?.invalidate(sessionId);
|
|
3277
|
+
this._cancellers.get(sessionId)?.({
|
|
3278
|
+
code: HardwareErrorCode7.DeviceDisconnected,
|
|
3279
|
+
tag: "DeviceDisconnected",
|
|
3280
|
+
message: "Device disconnected"
|
|
3281
|
+
});
|
|
3282
|
+
this._cancellers.delete(sessionId);
|
|
3283
|
+
this._emit("device-disconnect", { connectId: externalConnectId });
|
|
3284
|
+
}
|
|
2389
3285
|
// ---------------------------------------------------------------------------
|
|
2390
3286
|
// IConnector -- Method dispatch
|
|
2391
3287
|
// ---------------------------------------------------------------------------
|
|
2392
3288
|
async call(sessionId, method, params) {
|
|
2393
3289
|
debugLog("[DMK] call:", method, JSON.stringify(params));
|
|
3290
|
+
try {
|
|
3291
|
+
return await this._dispatch(sessionId, method, params);
|
|
3292
|
+
} catch (err) {
|
|
3293
|
+
if (isAppStuckByApdu(err)) {
|
|
3294
|
+
throw Object.assign(new Error("Ledger app is unresponsive"), {
|
|
3295
|
+
code: HardwareErrorCode7.DeviceAppStuck,
|
|
3296
|
+
_tag: ERROR_TAG.DeviceAppStuck,
|
|
3297
|
+
originalError: err
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
if (isTransportStuck(err)) {
|
|
3301
|
+
throw Object.assign(new Error("Device communication interrupted, please retry"), {
|
|
3302
|
+
code: HardwareErrorCode7.TransportError,
|
|
3303
|
+
_tag: ERROR_TAG.DeviceTransportStuck,
|
|
3304
|
+
originalError: err
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
throw err;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
async _dispatch(sessionId, method, params) {
|
|
2394
3311
|
const ctx = this._ctxForMethod(method);
|
|
2395
3312
|
switch (method) {
|
|
2396
3313
|
// EVM
|
|
@@ -2530,14 +3447,13 @@ var LedgerConnectorBase = class {
|
|
|
2530
3447
|
}
|
|
2531
3448
|
/**
|
|
2532
3449
|
* Replace an old session with a new one after app switch.
|
|
2533
|
-
*
|
|
2534
|
-
* and emits device-connect so the adapter updates its _sessions Map.
|
|
3450
|
+
* Emits device-connect so the adapter updates its _sessions Map.
|
|
2535
3451
|
*/
|
|
2536
3452
|
_replaceSession(oldSessionId, _newSessionId) {
|
|
2537
3453
|
const dm = this._deviceManager;
|
|
2538
3454
|
if (!dm) return;
|
|
2539
3455
|
const oldDeviceId = dm.getDeviceId(oldSessionId);
|
|
2540
|
-
const connectId = oldDeviceId
|
|
3456
|
+
const connectId = oldDeviceId;
|
|
2541
3457
|
this._signerManager?.invalidate(oldSessionId);
|
|
2542
3458
|
if (connectId) {
|
|
2543
3459
|
this._emit("device-connect", {
|
|
@@ -2550,13 +3466,18 @@ var LedgerConnectorBase = class {
|
|
|
2550
3466
|
}
|
|
2551
3467
|
}
|
|
2552
3468
|
/**
|
|
2553
|
-
* Light reset: clear signer/session state but keep DMK
|
|
3469
|
+
* Light reset: clear signer/session state but keep DMK alive.
|
|
2554
3470
|
* Used by connect() retry — we want to re-discover with the same transport.
|
|
3471
|
+
*
|
|
3472
|
+
* Note: drops the device manager but tears down its RxJS subs first via
|
|
3473
|
+
* disposeKeepingDmk(). Bare null-assignment would leak _listenSub /
|
|
3474
|
+
* _discoverySub and double-scan after the next _initManagers().
|
|
2555
3475
|
*/
|
|
2556
3476
|
_resetSignersAndSessions() {
|
|
2557
3477
|
debugLog("[DMK] _resetSignersAndSessions called");
|
|
2558
3478
|
this._signerManager?.clearAll();
|
|
2559
3479
|
this._signerManager = null;
|
|
3480
|
+
this._deviceManager?.disposeKeepingDmk();
|
|
2560
3481
|
this._deviceManager = null;
|
|
2561
3482
|
}
|
|
2562
3483
|
_resetAll() {
|
|
@@ -2568,13 +3489,18 @@ var LedgerConnectorBase = class {
|
|
|
2568
3489
|
}
|
|
2569
3490
|
}
|
|
2570
3491
|
this._cancellers.clear();
|
|
3492
|
+
for (const sub of this._sessionStateSubs.values()) {
|
|
3493
|
+
try {
|
|
3494
|
+
sub.unsubscribe();
|
|
3495
|
+
} catch {
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
this._sessionStateSubs.clear();
|
|
2571
3499
|
this._signerManager?.clearAll();
|
|
2572
3500
|
this._deviceManager?.dispose();
|
|
2573
3501
|
this._deviceManager = null;
|
|
2574
3502
|
this._signerManager = null;
|
|
2575
3503
|
this._dmk = null;
|
|
2576
|
-
this._connectIdToPath.clear();
|
|
2577
|
-
this._pathToConnectId.clear();
|
|
2578
3504
|
}
|
|
2579
3505
|
// ---------------------------------------------------------------------------
|
|
2580
3506
|
// Private -- Events
|
|
@@ -2608,15 +3534,21 @@ var LedgerConnectorBase = class {
|
|
|
2608
3534
|
};
|
|
2609
3535
|
}
|
|
2610
3536
|
_wrapError(err, opts) {
|
|
2611
|
-
const mapped = mapLedgerError(err, opts);
|
|
2612
|
-
const error = new Error(mapped.message);
|
|
2613
3537
|
const src = err && typeof err === "object" ? err : {};
|
|
3538
|
+
const hasSerializedCode = typeof src.code === "number" && HARDWARE_ERROR_CODE_VALUES.has(src.code);
|
|
3539
|
+
const mapped = hasSerializedCode ? {
|
|
3540
|
+
code: src.code,
|
|
3541
|
+
message: typeof src.message === "string" ? src.message : "Unknown Ledger error",
|
|
3542
|
+
appName: typeof src.appName === "string" ? src.appName : opts?.defaultAppName
|
|
3543
|
+
} : mapLedgerError(err, opts);
|
|
3544
|
+
const error = new Error(mapped.message);
|
|
2614
3545
|
Object.assign(error, {
|
|
2615
3546
|
code: mapped.code,
|
|
2616
3547
|
appName: mapped.appName,
|
|
2617
3548
|
_tag: src._tag,
|
|
2618
3549
|
errorCode: src.errorCode,
|
|
2619
|
-
_lastStep: src._lastStep
|
|
3550
|
+
_lastStep: src._lastStep,
|
|
3551
|
+
_deviceActionSteps: src._deviceActionSteps
|
|
2620
3552
|
});
|
|
2621
3553
|
Object.defineProperty(error, "originalError", {
|
|
2622
3554
|
value: err,
|
|
@@ -2627,13 +3559,6 @@ var LedgerConnectorBase = class {
|
|
|
2627
3559
|
return error;
|
|
2628
3560
|
}
|
|
2629
3561
|
};
|
|
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
3562
|
export {
|
|
2638
3563
|
AppManager,
|
|
2639
3564
|
DmkTransport,
|
|
@@ -2644,12 +3569,15 @@ export {
|
|
|
2644
3569
|
SignerEth,
|
|
2645
3570
|
SignerManager,
|
|
2646
3571
|
SignerSol,
|
|
3572
|
+
debugLog,
|
|
2647
3573
|
deviceActionToPromise,
|
|
2648
|
-
extractBleHexId,
|
|
2649
|
-
isDebugEnabled,
|
|
2650
3574
|
isDeviceLockedError,
|
|
3575
|
+
isLedgerBleConnectionType,
|
|
3576
|
+
isLedgerBleDescriptor,
|
|
3577
|
+
isLedgerDmkBleTransport,
|
|
2651
3578
|
ledgerFailure,
|
|
2652
3579
|
mapLedgerError,
|
|
2653
|
-
|
|
3580
|
+
offSdkEvent,
|
|
3581
|
+
onSdkEvent
|
|
2654
3582
|
};
|
|
2655
3583
|
//# sourceMappingURL=index.mjs.map
|