@onekeyfe/hwk-ledger-adapter 1.2.3-alpha.7 → 1.2.3-alpha.8
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 +86 -29
- package/dist/index.d.ts +86 -29
- package/dist/index.js +1781 -826
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1789 -814
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -11
package/dist/index.mjs
CHANGED
|
@@ -6,30 +6,41 @@ import {
|
|
|
6
6
|
DeviceJobQueue,
|
|
7
7
|
EConnectorInteraction,
|
|
8
8
|
HardwareErrorCode as HardwareErrorCode3,
|
|
9
|
+
OperationRegistry,
|
|
10
|
+
SDK,
|
|
9
11
|
TypedEventEmitter,
|
|
10
12
|
UI_REQUEST,
|
|
11
13
|
UI_REQUEST_PREEMPTED_TAG,
|
|
12
14
|
UiRequestRegistry,
|
|
15
|
+
canReplayHardwareMethodAfterTransportFailure,
|
|
13
16
|
createHwkError,
|
|
14
17
|
deriveDeviceFingerprint,
|
|
15
18
|
failure as failure2,
|
|
19
|
+
isHardwareOperationId,
|
|
20
|
+
isHwkRecoveryHint,
|
|
21
|
+
operationMayHaveCompletedParams,
|
|
16
22
|
rehydrateConnectorError,
|
|
23
|
+
requestBleDeviceSelection,
|
|
24
|
+
requestSaveDeviceBinding,
|
|
25
|
+
resolveSearchTargetReusePolicy,
|
|
17
26
|
success
|
|
18
27
|
} from "@onekeyfe/hwk-adapter-core";
|
|
19
28
|
|
|
20
29
|
// src/errors.ts
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
function ledgerFailure(code, error, appName, tag, params) {
|
|
30
|
+
import {
|
|
31
|
+
HardwareErrorCode,
|
|
32
|
+
defaultOriginForCode,
|
|
33
|
+
defaultRecoveryForCode,
|
|
34
|
+
enrichErrorMessage
|
|
35
|
+
} from "@onekeyfe/hwk-adapter-core";
|
|
36
|
+
function ledgerFailure(code, error, appName, tag, params, origin, recovery) {
|
|
29
37
|
const payload = { error, code };
|
|
30
38
|
if (appName !== void 0) payload.appName = appName;
|
|
31
39
|
if (tag !== void 0) payload._tag = tag;
|
|
32
40
|
if (params !== void 0) payload.params = params;
|
|
41
|
+
const resolvedOrigin = origin ?? defaultOriginForCode(code);
|
|
42
|
+
if (resolvedOrigin !== void 0) payload.origin = resolvedOrigin;
|
|
43
|
+
payload.recovery = recovery ?? defaultRecoveryForCode(code);
|
|
33
44
|
return { success: false, payload };
|
|
34
45
|
}
|
|
35
46
|
var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
|
|
@@ -170,7 +181,12 @@ var ERROR_TAG = {
|
|
|
170
181
|
UnknownDevice: "UnknownDeviceError",
|
|
171
182
|
DeviceSessionRefresher: "DeviceSessionRefresherError",
|
|
172
183
|
DeviceNotInitialized: "DeviceNotInitializedError",
|
|
173
|
-
|
|
184
|
+
// DMK's class is named OpeningConnectionError but its `_tag` reads
|
|
185
|
+
// "ConnectionOpeningError" — and its typings widen `_tag` to `string`, which
|
|
186
|
+
// is why the class name looked authoritative. Both spellings are kept so a
|
|
187
|
+
// DMK version that aligns them does not silently drop back to UnknownError.
|
|
188
|
+
OpeningConnection: "ConnectionOpeningError",
|
|
189
|
+
OpeningConnectionLegacy: "OpeningConnectionError",
|
|
174
190
|
DeviceDisconnectedBeforeSendingApdu: "DeviceDisconnectedBeforeSendingApdu",
|
|
175
191
|
DeviceDisconnectedWhileSending: "DeviceDisconnectedWhileSendingError",
|
|
176
192
|
Disconnect: "DisconnectError",
|
|
@@ -183,7 +199,18 @@ var ERROR_TAG = {
|
|
|
183
199
|
// DMK remote-network failures (manager-api HTTP / secure-channel WS).
|
|
184
200
|
WebSocketConnection: "WebSocketConnectionError",
|
|
185
201
|
HttpFetch: "FetchError",
|
|
186
|
-
|
|
202
|
+
NetworkDA: "NetworkDAError",
|
|
203
|
+
InvalidFirmwareMetadataResponse: "InvalidGetFirmwareMetadataResponseError",
|
|
204
|
+
ApplicationsMetadataTask: "GetApplicationsMetadataTaskError",
|
|
205
|
+
// DMK OS/secure-channel device actions. `SecureChannelError` is the residual
|
|
206
|
+
// bucket left by SecureChannelError.mapInstallDAErrors() after the locked /
|
|
207
|
+
// refused / already-installed / OOM cases have been split out, so it means
|
|
208
|
+
// "the relay itself broke", not "the device answered".
|
|
209
|
+
SecureChannel: "SecureChannelError",
|
|
210
|
+
RefusedByUserDA: "RefusedByUserDAError",
|
|
211
|
+
AppAlreadyInstalledDA: "AppAlreadyInstalledDAError",
|
|
212
|
+
OutOfMemoryDA: "OutOfMemoryDAError",
|
|
213
|
+
DeviceNotOnboarded: "DeviceNotOnboardedError"
|
|
187
214
|
};
|
|
188
215
|
function isDeviceLockedError(err) {
|
|
189
216
|
if (!err || typeof err !== "object") return false;
|
|
@@ -213,7 +240,6 @@ function isBlePairingFailureError(err) {
|
|
|
213
240
|
return false;
|
|
214
241
|
}
|
|
215
242
|
var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
|
|
216
|
-
ERROR_TAG.DeviceLocked,
|
|
217
243
|
ERROR_TAG.DeviceNotAdvertising,
|
|
218
244
|
ERROR_TAG.BlePairingTimeout,
|
|
219
245
|
ERROR_TAG.BleGattBondingFailed,
|
|
@@ -225,6 +251,7 @@ var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
|
|
|
225
251
|
ERROR_TAG.DeviceSessionRefresher,
|
|
226
252
|
ERROR_TAG.DeviceNotInitialized,
|
|
227
253
|
ERROR_TAG.OpeningConnection,
|
|
254
|
+
ERROR_TAG.OpeningConnectionLegacy,
|
|
228
255
|
ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
|
|
229
256
|
ERROR_TAG.DeviceDisconnectedWhileSending,
|
|
230
257
|
ERROR_TAG.Disconnect,
|
|
@@ -239,7 +266,10 @@ var DEVICE_NOT_FOUND_TAGS = /* @__PURE__ */ new Set([
|
|
|
239
266
|
// Map to DeviceNotFound so non-BLE-direct paths get a sensible error code.
|
|
240
267
|
ERROR_TAG.DeviceNotInDiscoveryCache
|
|
241
268
|
]);
|
|
242
|
-
var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([
|
|
269
|
+
var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([
|
|
270
|
+
ERROR_TAG.OpeningConnection,
|
|
271
|
+
ERROR_TAG.OpeningConnectionLegacy
|
|
272
|
+
]);
|
|
243
273
|
var DEVICE_DISCONNECTED_TAGS = /* @__PURE__ */ new Set([
|
|
244
274
|
ERROR_TAG.DeviceNotRecognized,
|
|
245
275
|
ERROR_TAG.DeviceSessionNotFound,
|
|
@@ -262,6 +292,13 @@ function isConnectionLevelError(err) {
|
|
|
262
292
|
function isKnownConnectionTag(tag) {
|
|
263
293
|
return typeof tag === "string" && CONNECTION_LEVEL_TAGS.has(tag);
|
|
264
294
|
}
|
|
295
|
+
var CONNECTION_OPENING_TAGS = /* @__PURE__ */ new Set([
|
|
296
|
+
ERROR_TAG.OpeningConnection,
|
|
297
|
+
ERROR_TAG.OpeningConnectionLegacy
|
|
298
|
+
]);
|
|
299
|
+
function isConnectionOpeningTag(tag) {
|
|
300
|
+
return typeof tag === "string" && CONNECTION_OPENING_TAGS.has(tag);
|
|
301
|
+
}
|
|
265
302
|
function hasStatusCode(err, codeSet) {
|
|
266
303
|
if (!err || typeof err !== "object") return false;
|
|
267
304
|
const e = err;
|
|
@@ -297,6 +334,14 @@ function hasInvalidArgumentCode(err) {
|
|
|
297
334
|
if (e.error != null && e._tag && hasInvalidArgumentCode(e.error)) return true;
|
|
298
335
|
return false;
|
|
299
336
|
}
|
|
337
|
+
function hasErrorTag(err, tags) {
|
|
338
|
+
if (!err || typeof err !== "object") return false;
|
|
339
|
+
const e = err;
|
|
340
|
+
if (typeof e._tag === "string" && tags.has(e._tag)) return true;
|
|
341
|
+
if (e.originalError != null && hasErrorTag(e.originalError, tags)) return true;
|
|
342
|
+
if (e.error != null && e._tag && hasErrorTag(e.error, tags)) return true;
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
300
345
|
function isDeviceNotFoundError(err) {
|
|
301
346
|
if (!err || typeof err !== "object") return false;
|
|
302
347
|
const tag = err._tag;
|
|
@@ -315,10 +360,14 @@ function isDeviceBusyError(err) {
|
|
|
315
360
|
if (e.error != null && e._tag && isDeviceBusyError(e.error)) return true;
|
|
316
361
|
return false;
|
|
317
362
|
}
|
|
363
|
+
var USER_REJECTED_TAGS = /* @__PURE__ */ new Set([
|
|
364
|
+
ERROR_TAG.UserRefusedOnDevice,
|
|
365
|
+
ERROR_TAG.RefusedByUserDA
|
|
366
|
+
]);
|
|
318
367
|
function isUserRejectedError(err) {
|
|
319
368
|
if (!err || typeof err !== "object") return false;
|
|
320
369
|
const e = err;
|
|
321
|
-
if (
|
|
370
|
+
if (hasErrorTag(err, USER_REJECTED_TAGS)) return true;
|
|
322
371
|
if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
|
|
323
372
|
if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
|
|
324
373
|
return false;
|
|
@@ -349,15 +398,37 @@ function isAppNotInstalledError(err) {
|
|
|
349
398
|
if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
|
|
350
399
|
return false;
|
|
351
400
|
}
|
|
401
|
+
var OUT_OF_MEMORY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OutOfMemoryDA]);
|
|
352
402
|
function isOutOfMemoryError(err) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
403
|
+
return hasErrorTag(err, OUT_OF_MEMORY_TAGS);
|
|
404
|
+
}
|
|
405
|
+
var APP_ALREADY_INSTALLED_TAGS = /* @__PURE__ */ new Set([
|
|
406
|
+
ERROR_TAG.AppAlreadyInstalledDA
|
|
407
|
+
]);
|
|
408
|
+
function isAppAlreadyInstalledError(err) {
|
|
409
|
+
return hasErrorTag(err, APP_ALREADY_INSTALLED_TAGS);
|
|
410
|
+
}
|
|
411
|
+
var DEVICE_NOT_ONBOARDED_TAGS = /* @__PURE__ */ new Set([
|
|
412
|
+
ERROR_TAG.DeviceNotOnboarded
|
|
413
|
+
]);
|
|
414
|
+
function isDeviceNotOnboardedError(err) {
|
|
415
|
+
return hasErrorTag(err, DEVICE_NOT_ONBOARDED_TAGS);
|
|
416
|
+
}
|
|
417
|
+
var FIRMWARE_METADATA_TAGS = /* @__PURE__ */ new Set([
|
|
418
|
+
ERROR_TAG.InvalidFirmwareMetadataResponse,
|
|
419
|
+
ERROR_TAG.ApplicationsMetadataTask
|
|
420
|
+
]);
|
|
421
|
+
function isFirmwareMetadataError(err) {
|
|
422
|
+
return hasErrorTag(err, FIRMWARE_METADATA_TAGS);
|
|
423
|
+
}
|
|
424
|
+
var SECURE_CHANNEL_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.SecureChannel]);
|
|
425
|
+
function isSecureChannelError(err) {
|
|
426
|
+
return hasErrorTag(err, SECURE_CHANNEL_TAGS);
|
|
356
427
|
}
|
|
357
428
|
var NETWORK_ERROR_TAGS = /* @__PURE__ */ new Set([
|
|
358
429
|
ERROR_TAG.WebSocketConnection,
|
|
359
430
|
ERROR_TAG.HttpFetch,
|
|
360
|
-
ERROR_TAG.
|
|
431
|
+
ERROR_TAG.NetworkDA
|
|
361
432
|
]);
|
|
362
433
|
function isNetworkError(err) {
|
|
363
434
|
if (!err || typeof err !== "object") return false;
|
|
@@ -407,23 +478,34 @@ function isTransportStuck(err) {
|
|
|
407
478
|
function isStuckAppStateError(err) {
|
|
408
479
|
return isAppStuckByApdu(err) || isTransportStuck(err);
|
|
409
480
|
}
|
|
481
|
+
var DMK_PLACEHOLDER_MESSAGE = "Unknown error.";
|
|
482
|
+
function nestedErrorMessage(nested) {
|
|
483
|
+
if (!nested || typeof nested !== "object") return void 0;
|
|
484
|
+
const { message } = nested;
|
|
485
|
+
if (typeof message !== "string") return void 0;
|
|
486
|
+
const trimmed = message.trim();
|
|
487
|
+
if (!trimmed || trimmed === DMK_PLACEHOLDER_MESSAGE) return void 0;
|
|
488
|
+
return message;
|
|
489
|
+
}
|
|
410
490
|
function mapLedgerError(err, opts) {
|
|
411
491
|
let originalMessage = "Unknown Ledger error";
|
|
412
492
|
if (err instanceof Error) {
|
|
413
493
|
originalMessage = err.message;
|
|
414
494
|
} else if (err && typeof err === "object") {
|
|
415
495
|
const e = err;
|
|
416
|
-
originalMessage = String(
|
|
496
|
+
originalMessage = String(
|
|
497
|
+
e.message ?? nestedErrorMessage(e.originalError) ?? e._tag ?? e.type ?? JSON.stringify(err)
|
|
498
|
+
);
|
|
417
499
|
}
|
|
418
500
|
let code;
|
|
419
501
|
if (isDeviceLockedError(err)) {
|
|
420
502
|
code = HardwareErrorCode.DeviceLocked;
|
|
421
503
|
} else if (isDeviceNotAdvertisingError(err) || isDeviceNotFoundError(err)) {
|
|
422
504
|
code = HardwareErrorCode.DeviceNotFound;
|
|
423
|
-
} else if (isDeviceBusyError(err)) {
|
|
424
|
-
code = HardwareErrorCode.DeviceBusy;
|
|
425
505
|
} else if (isBlePairingFailureError(err)) {
|
|
426
506
|
code = HardwareErrorCode.BlePairingTimeout;
|
|
507
|
+
} else if (isDeviceBusyError(err)) {
|
|
508
|
+
code = HardwareErrorCode.DeviceBusy;
|
|
427
509
|
} else if (isUserAbortedError(err)) {
|
|
428
510
|
code = HardwareErrorCode.UserAborted;
|
|
429
511
|
} else if (isUserRejectedError(err)) {
|
|
@@ -432,10 +514,18 @@ function mapLedgerError(err, opts) {
|
|
|
432
514
|
code = HardwareErrorCode.WrongApp;
|
|
433
515
|
} else if (isAppNotInstalledError(err)) {
|
|
434
516
|
code = HardwareErrorCode.AppNotInstalled;
|
|
517
|
+
} else if (isAppAlreadyInstalledError(err)) {
|
|
518
|
+
code = HardwareErrorCode.AppAlreadyInstalled;
|
|
435
519
|
} else if (isOutOfMemoryError(err)) {
|
|
436
520
|
code = HardwareErrorCode.DeviceOutOfMemory;
|
|
521
|
+
} else if (isDeviceNotOnboardedError(err)) {
|
|
522
|
+
code = HardwareErrorCode.DeviceNotInitialized;
|
|
523
|
+
} else if (isFirmwareMetadataError(err)) {
|
|
524
|
+
code = HardwareErrorCode.LedgerFirmwareMetadataError;
|
|
437
525
|
} else if (isNetworkError(err)) {
|
|
438
526
|
code = HardwareErrorCode.NetworkError;
|
|
527
|
+
} else if (isSecureChannelError(err)) {
|
|
528
|
+
code = HardwareErrorCode.LedgerSecureChannelError;
|
|
439
529
|
} else if (isDeviceDisconnectedError(err)) {
|
|
440
530
|
code = HardwareErrorCode.DeviceDisconnected;
|
|
441
531
|
} else if (isTimeoutError(err)) {
|
|
@@ -448,11 +538,32 @@ function mapLedgerError(err, opts) {
|
|
|
448
538
|
}
|
|
449
539
|
const errAppName = err && typeof err === "object" ? err.appName : void 0;
|
|
450
540
|
const appName = errAppName ?? opts?.defaultAppName;
|
|
451
|
-
return {
|
|
541
|
+
return {
|
|
542
|
+
code,
|
|
543
|
+
message: enrichErrorMessage(code, originalMessage),
|
|
544
|
+
origin: defaultOriginForCode(code),
|
|
545
|
+
appName
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/utils/queueKey.ts
|
|
550
|
+
var LEDGER_DEFAULT_QUEUE_KEY = "__ledger_default__";
|
|
551
|
+
function ledgerQueueKey({
|
|
552
|
+
operationId,
|
|
553
|
+
connectId
|
|
554
|
+
}) {
|
|
555
|
+
return (operationId ?? connectId) || LEDGER_DEFAULT_QUEUE_KEY;
|
|
452
556
|
}
|
|
453
557
|
|
|
454
558
|
// src/adapter/methods/allNetworkGetAddress.ts
|
|
455
|
-
import {
|
|
559
|
+
import {
|
|
560
|
+
HardwareErrorCode as HardwareErrorCode2,
|
|
561
|
+
failure,
|
|
562
|
+
isConnectionLost,
|
|
563
|
+
isUserRefusal,
|
|
564
|
+
resolveHardwareOperationTarget,
|
|
565
|
+
runAllNetworkGetAddress
|
|
566
|
+
} from "@onekeyfe/hwk-adapter-core";
|
|
456
567
|
|
|
457
568
|
// src/utils/sdkEventBus.ts
|
|
458
569
|
var listeners = /* @__PURE__ */ new Set();
|
|
@@ -509,65 +620,102 @@ var LEDGER_BTC_NETWORK_COIN_MAP = {
|
|
|
509
620
|
var LEDGER_UNSUPPORTED_ALLNETWORK_NETWORKS = /* @__PURE__ */ new Set(["doge", "dogecoin"]);
|
|
510
621
|
function createAllNetworkGetAddress({
|
|
511
622
|
callChain,
|
|
512
|
-
getChainFingerprint
|
|
623
|
+
getChainFingerprint,
|
|
624
|
+
retainOperation,
|
|
625
|
+
errorToFailure,
|
|
626
|
+
createCancelScope
|
|
513
627
|
}) {
|
|
514
628
|
return async function allNetworkGetAddress(connectId, _deviceId, params) {
|
|
515
|
-
debugLog("[LedgerAdapter][REQ]", {
|
|
629
|
+
debugLog("[LedgerAdapter][REQ]", {
|
|
630
|
+
method: "allNetworkGetAddress",
|
|
631
|
+
connectId,
|
|
632
|
+
itemCount: params.bundle.length
|
|
633
|
+
});
|
|
634
|
+
const target = resolveHardwareOperationTarget(connectId, params.operationId, "ledger");
|
|
635
|
+
if (!target.success) return target;
|
|
636
|
+
const effectiveTargetId = target.payload.targetId ?? "";
|
|
637
|
+
let releaseOperationRetention;
|
|
638
|
+
try {
|
|
639
|
+
releaseOperationRetention = target.payload.operationId ? retainOperation(target.payload.operationId) : void 0;
|
|
640
|
+
} catch (error) {
|
|
641
|
+
return errorToFailure(error);
|
|
642
|
+
}
|
|
643
|
+
const cancelScope = createCancelScope(
|
|
644
|
+
ledgerQueueKey({ operationId: target.payload.operationId, connectId: effectiveTargetId })
|
|
645
|
+
);
|
|
516
646
|
const installContext = {};
|
|
517
647
|
const commonParams = {
|
|
518
|
-
autoInstallApp: params.autoInstallApp
|
|
648
|
+
autoInstallApp: params.autoInstallApp,
|
|
649
|
+
operationId: target.payload.operationId,
|
|
650
|
+
knownConnections: params.knownConnections,
|
|
651
|
+
extra: params.extra,
|
|
652
|
+
allowDeviceSelection: params.allowDeviceSelection
|
|
519
653
|
};
|
|
520
654
|
const chainFingerprints = /* @__PURE__ */ new Map();
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
655
|
+
try {
|
|
656
|
+
const result = await runAllNetworkGetAddress({
|
|
657
|
+
connectId: effectiveTargetId,
|
|
658
|
+
deviceId: _deviceId,
|
|
659
|
+
params,
|
|
660
|
+
normalizeItem: normalizeLedgerAllNetworkItem,
|
|
661
|
+
buildUnsupportedNetworkResponse: (item) => isUnsupportedLedgerAllNetworkNetwork(item) ? buildUnsupportedNetworkResponse(item) : void 0,
|
|
662
|
+
callItem: async ({ method, chain, item }) => {
|
|
663
|
+
if (cancelScope.signal.aborted) return buildCancelledFailure(cancelScope.signal);
|
|
664
|
+
const itemDeviceId = getItemDeviceId(item) ?? chainFingerprints.get(chain) ?? "";
|
|
665
|
+
return callAllNetworkMethod(
|
|
666
|
+
callChain,
|
|
667
|
+
effectiveTargetId,
|
|
668
|
+
itemDeviceId,
|
|
669
|
+
method,
|
|
670
|
+
item,
|
|
671
|
+
commonParams,
|
|
672
|
+
installContext
|
|
673
|
+
);
|
|
674
|
+
},
|
|
675
|
+
attachIdentity: async ({ item, chain, payload }) => attachLedgerIdentity(
|
|
676
|
+
getChainFingerprint,
|
|
677
|
+
effectiveTargetId,
|
|
534
678
|
item,
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
});
|
|
562
|
-
return result;
|
|
679
|
+
chain,
|
|
680
|
+
payload,
|
|
681
|
+
chainFingerprints,
|
|
682
|
+
installContext,
|
|
683
|
+
cancelScope.signal
|
|
684
|
+
),
|
|
685
|
+
shouldAbortBundle: isTopLevelAllNetworkFailure,
|
|
686
|
+
buildTopLevelFailure: (response) => {
|
|
687
|
+
const code = response.payload?.code ?? HardwareErrorCode2.DeviceMismatch;
|
|
688
|
+
return failure(
|
|
689
|
+
code,
|
|
690
|
+
response.payload?.error ?? "All-network get-address aborted",
|
|
691
|
+
response.payload?.params
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
debugLog("[LedgerAdapter][RES]", {
|
|
696
|
+
method: "allNetworkGetAddress",
|
|
697
|
+
success: result.success,
|
|
698
|
+
payload: result
|
|
699
|
+
});
|
|
700
|
+
return result;
|
|
701
|
+
} finally {
|
|
702
|
+
cancelScope.release();
|
|
703
|
+
releaseOperationRetention?.();
|
|
704
|
+
}
|
|
563
705
|
};
|
|
564
706
|
}
|
|
707
|
+
function buildCancelledFailure(signal) {
|
|
708
|
+
const reason = signal.reason;
|
|
709
|
+
const code = typeof reason?.code === "number" ? reason.code : HardwareErrorCode2.UserAborted;
|
|
710
|
+
const message = typeof reason?.message === "string" ? reason.message : "";
|
|
711
|
+
return failure(code, message || "All-network get-address cancelled");
|
|
712
|
+
}
|
|
565
713
|
function isTopLevelAllNetworkFailure(response) {
|
|
566
714
|
if (response.success) {
|
|
567
715
|
return false;
|
|
568
716
|
}
|
|
569
717
|
const code = response.payload?.code;
|
|
570
|
-
return code === HardwareErrorCode2.DeviceMismatch || code
|
|
718
|
+
return code === HardwareErrorCode2.DeviceMismatch || isUserRefusal(code) || isConnectionLost(code);
|
|
571
719
|
}
|
|
572
720
|
function getItemDeviceId(item) {
|
|
573
721
|
const { deviceId } = item;
|
|
@@ -597,8 +745,18 @@ function normalizeLedgerAllNetworkItem(method, item) {
|
|
|
597
745
|
const coin = LEDGER_BTC_NETWORK_COIN_MAP[item.network];
|
|
598
746
|
return coin ? { ...item, coin } : item;
|
|
599
747
|
}
|
|
600
|
-
async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain, payload, chainFingerprints) {
|
|
601
|
-
const
|
|
748
|
+
async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain, payload, chainFingerprints, context, cancelSignal) {
|
|
749
|
+
const knownFingerprint = getItemDeviceId(item) || chainFingerprints.get(chain) || "";
|
|
750
|
+
if (!knownFingerprint && cancelSignal.aborted) {
|
|
751
|
+
const cancelled = buildCancelledFailure(cancelSignal);
|
|
752
|
+
return { ...item, success: false, payload: cancelled.payload };
|
|
753
|
+
}
|
|
754
|
+
const fingerprint = knownFingerprint || await bootstrapChainFingerprint(
|
|
755
|
+
getChainFingerprint,
|
|
756
|
+
context.connection?.connectId ?? connectId,
|
|
757
|
+
chain,
|
|
758
|
+
context
|
|
759
|
+
);
|
|
602
760
|
if (!fingerprint) {
|
|
603
761
|
return buildFingerprintBootstrapFailure(item, chain);
|
|
604
762
|
}
|
|
@@ -619,8 +777,8 @@ async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain,
|
|
|
619
777
|
}
|
|
620
778
|
};
|
|
621
779
|
}
|
|
622
|
-
async function bootstrapChainFingerprint(getChainFingerprint, connectId, chain) {
|
|
623
|
-
const response = await getChainFingerprint(connectId, chain);
|
|
780
|
+
async function bootstrapChainFingerprint(getChainFingerprint, connectId, chain, context) {
|
|
781
|
+
const response = await getChainFingerprint(connectId, chain, context);
|
|
624
782
|
return response.success ? response.payload : "";
|
|
625
783
|
}
|
|
626
784
|
function buildFingerprintBootstrapFailure(item, chain) {
|
|
@@ -724,9 +882,32 @@ function btcAccountIndexFromPath(path) {
|
|
|
724
882
|
var _LedgerAdapter = class _LedgerAdapter {
|
|
725
883
|
constructor(connector, options) {
|
|
726
884
|
this.vendor = "ledger";
|
|
885
|
+
// A cancel releases the DMK device action and its intent-queue slot, so the
|
|
886
|
+
// next call is not blocked behind it. It still cannot retract a confirmation
|
|
887
|
+
// screen the device is already showing.
|
|
888
|
+
this.cancelCapability = "stops-waiting";
|
|
727
889
|
this.emitter = new TypedEventEmitter();
|
|
890
|
+
this._operations = new OperationRegistry({
|
|
891
|
+
vendor: "ledger",
|
|
892
|
+
onEnded: (operation, reason) => {
|
|
893
|
+
const binding = this._pendingOperationBindings.get(operation.operationId);
|
|
894
|
+
if (binding?.selectedConnection?.requestId === this._bindingSelectionRequestId) {
|
|
895
|
+
this._finishBleBinding("cancelled");
|
|
896
|
+
}
|
|
897
|
+
this._pendingOperationBindings.delete(operation.operationId);
|
|
898
|
+
this.emitter.emit(SDK.OPERATION_ENDED, {
|
|
899
|
+
type: SDK.OPERATION_ENDED,
|
|
900
|
+
payload: { operationId: operation.operationId, reason }
|
|
901
|
+
});
|
|
902
|
+
if (reason === "timeout") {
|
|
903
|
+
void this._releaseOperationConnection(operation);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
});
|
|
728
907
|
this._discoveredDevices = /* @__PURE__ */ new Map();
|
|
729
908
|
this._sessions = /* @__PURE__ */ new Map();
|
|
909
|
+
this._pendingOperationBindings = /* @__PURE__ */ new Map();
|
|
910
|
+
this._verifiedBleReconnectTargets = /* @__PURE__ */ new Map();
|
|
730
911
|
this._uiRegistry = new UiRequestRegistry();
|
|
731
912
|
// BTC App rejects account index >= 100 unless display=true. Cached per
|
|
732
913
|
// adapter instance: first 100+ path asks the user once via UI request,
|
|
@@ -740,10 +921,31 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
740
921
|
this._deviceAuthenticityQueueTail = Promise.resolve();
|
|
741
922
|
// Shared across concurrent callers — only `cancel()` aborts.
|
|
742
923
|
this._doConnectAbortController = null;
|
|
924
|
+
this._unsettledConnectorOperations = /* @__PURE__ */ new Map();
|
|
925
|
+
this._connectorIdleWaiters = /* @__PURE__ */ new Set();
|
|
926
|
+
this._resetPromise = null;
|
|
927
|
+
this._stateGeneration = 0;
|
|
928
|
+
this._connectorTeardownTail = Promise.resolve();
|
|
929
|
+
this._pendingConnectorTeardowns = 0;
|
|
930
|
+
this._activeOperationJobs = /* @__PURE__ */ new Set();
|
|
931
|
+
this._pendingOperationDisconnects = /* @__PURE__ */ new Set();
|
|
743
932
|
this._installProgressLastEmittedValue = -Infinity;
|
|
744
933
|
this.allNetworkGetAddress = createAllNetworkGetAddress({
|
|
745
934
|
callChain: this.callChain.bind(this),
|
|
746
|
-
getChainFingerprint: (connectId, chain) =>
|
|
935
|
+
getChainFingerprint: async (connectId, chain, context) => {
|
|
936
|
+
try {
|
|
937
|
+
const fingerprint = await this._computeChainFingerprint(
|
|
938
|
+
chain,
|
|
939
|
+
(method, params) => this.connectorCall(connectId, method, params, void 0, void 0, void 0, context)
|
|
940
|
+
);
|
|
941
|
+
return success(fingerprint);
|
|
942
|
+
} catch (error) {
|
|
943
|
+
return this.errorToFailure(error);
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
retainOperation: (operationId) => this._operations.retain(operationId),
|
|
947
|
+
errorToFailure: (error) => this.errorToFailure(error),
|
|
948
|
+
createCancelScope: (queueKey) => this._jobQueue.createCancelScope(queueKey)
|
|
747
949
|
});
|
|
748
950
|
// ---------------------------------------------------------------------------
|
|
749
951
|
// Private helpers
|
|
@@ -753,7 +955,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
753
955
|
*
|
|
754
956
|
* - If a session already exists for the given connectId, reuse it.
|
|
755
957
|
* - If ANY session exists (Ledger IDs are ephemeral), reuse it.
|
|
756
|
-
* - Otherwise: search →
|
|
958
|
+
* - Otherwise: search → one USB device auto-connects; multiple asks the host to choose.
|
|
757
959
|
*/
|
|
758
960
|
// Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
|
|
759
961
|
this._connectingPromise = null;
|
|
@@ -770,6 +972,12 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
770
972
|
});
|
|
771
973
|
};
|
|
772
974
|
this.deviceDisconnectHandler = (data) => {
|
|
975
|
+
const activeOperation = this._operations.findActiveByConnectionKey(data.connectId);
|
|
976
|
+
if (activeOperation && this._activeOperationJobs.has(activeOperation.operationId)) {
|
|
977
|
+
this._pendingOperationDisconnects.add(activeOperation.operationId);
|
|
978
|
+
} else {
|
|
979
|
+
this._operations.endByConnectionKey(data.connectId, "disconnect");
|
|
980
|
+
}
|
|
773
981
|
this._discoveredDevices.delete(data.connectId);
|
|
774
982
|
this._sessions.delete(data.connectId);
|
|
775
983
|
this.emitter.emit(DEVICE.DISCONNECT, {
|
|
@@ -826,11 +1034,28 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
826
1034
|
this._jobQueue = new DeviceJobQueue();
|
|
827
1035
|
this.registerEventListeners();
|
|
828
1036
|
}
|
|
1037
|
+
_finishBleBinding(status) {
|
|
1038
|
+
const selectionRequestId = this._bindingSelectionRequestId;
|
|
1039
|
+
this._bindingSelectionRequestId = void 0;
|
|
1040
|
+
if (selectionRequestId)
|
|
1041
|
+
this.emitter.emit(UI_REQUEST.DEVICE_BINDING_STATUS, {
|
|
1042
|
+
type: UI_REQUEST.DEVICE_BINDING_STATUS,
|
|
1043
|
+
payload: { selectionRequestId, status }
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
_isBleConnection() {
|
|
1047
|
+
return isLedgerBleConnectionType(this._activeConnectionType ?? this.connector.connectionType);
|
|
1048
|
+
}
|
|
829
1049
|
// Transport
|
|
830
1050
|
get activeTransport() {
|
|
831
|
-
return
|
|
1051
|
+
return this._isBleConnection() ? "ble" : "hid";
|
|
832
1052
|
}
|
|
833
1053
|
getAvailableTransports() {
|
|
1054
|
+
if (this.connector.availableTransports) {
|
|
1055
|
+
return this.connector.availableTransports.map(
|
|
1056
|
+
(transport) => transport === "ble" ? "ble" : "hid"
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
834
1059
|
return this.activeTransport ? [this.activeTransport] : [];
|
|
835
1060
|
}
|
|
836
1061
|
// Connector is bound at construction; switching requires a new adapter.
|
|
@@ -847,21 +1072,39 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
847
1072
|
* The next operation will re-discover and re-connect automatically.
|
|
848
1073
|
*/
|
|
849
1074
|
resetState() {
|
|
1075
|
+
void this._resetStateAndDisconnectSessions();
|
|
1076
|
+
}
|
|
1077
|
+
_resetStateAndDisconnectSessions() {
|
|
1078
|
+
if (this._resetPromise) return this._resetPromise;
|
|
1079
|
+
const sessionIds = new Set(this._sessions.values());
|
|
1080
|
+
this._stateGeneration += 1;
|
|
1081
|
+
this._finishBleBinding("cancelled");
|
|
1082
|
+
this._operations.endAll("runtime-reset");
|
|
1083
|
+
this._doConnectAbortController?.abort();
|
|
850
1084
|
this._discoveredDevices.clear();
|
|
851
1085
|
this._sessions.clear();
|
|
1086
|
+
this._verifiedBleReconnectTargets.clear();
|
|
852
1087
|
this._connectingPromise = null;
|
|
1088
|
+
this._doConnectAbortController = null;
|
|
853
1089
|
this._uiRegistry.reset();
|
|
854
1090
|
this._jobQueue.clear();
|
|
855
1091
|
this._btcHighIndexConfirmedThisSession = false;
|
|
1092
|
+
const resetPromise = this._runConnectorTeardown(async () => {
|
|
1093
|
+
for (const sessionId of sessionIds) {
|
|
1094
|
+
await this.connector.disconnect(sessionId).catch(() => void 0);
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
this._resetPromise = resetPromise;
|
|
1098
|
+
return resetPromise.finally(() => {
|
|
1099
|
+
if (this._resetPromise === resetPromise) {
|
|
1100
|
+
this._resetPromise = null;
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
856
1103
|
}
|
|
857
1104
|
async dispose() {
|
|
858
|
-
this.
|
|
859
|
-
this._jobQueue.clear();
|
|
1105
|
+
await this._resetStateAndDisconnectSessions();
|
|
860
1106
|
this.unregisterEventListeners();
|
|
861
1107
|
this.connector.reset();
|
|
862
|
-
this._discoveredDevices.clear();
|
|
863
|
-
this._sessions.clear();
|
|
864
|
-
this._btcHighIndexConfirmedThisSession = false;
|
|
865
1108
|
this.emitter.removeAllListeners();
|
|
866
1109
|
}
|
|
867
1110
|
uiResponse(response) {
|
|
@@ -871,17 +1114,32 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
871
1114
|
// Device management
|
|
872
1115
|
// ---------------------------------------------------------------------------
|
|
873
1116
|
async searchDevices(options) {
|
|
1117
|
+
return this._searchDevices(options);
|
|
1118
|
+
}
|
|
1119
|
+
async _searchDevices(options, signal) {
|
|
874
1120
|
debugLog("[LedgerAdapter][REQ]", { method: "searchDevices", params: options });
|
|
875
1121
|
try {
|
|
876
1122
|
if (options?.resetSession) {
|
|
877
|
-
this.
|
|
878
|
-
|
|
879
|
-
this.
|
|
880
|
-
|
|
881
|
-
|
|
1123
|
+
await this._resetStateAndDisconnectSessions();
|
|
1124
|
+
} else {
|
|
1125
|
+
await this._connectorTeardownTail;
|
|
1126
|
+
}
|
|
1127
|
+
await this._ensureDevicePermission(void 0, void 0, signal);
|
|
1128
|
+
if (signal) _LedgerAdapter._throwIfAborted(signal);
|
|
1129
|
+
const stateGeneration = this._stateGeneration;
|
|
1130
|
+
const devices = await this.connector.searchDevices(
|
|
1131
|
+
options?.transportType ? {
|
|
1132
|
+
transportType: options.transportType,
|
|
1133
|
+
waitForAll: options.waitForAllTransports
|
|
1134
|
+
} : void 0
|
|
1135
|
+
);
|
|
1136
|
+
if (signal) _LedgerAdapter._throwIfAborted(signal);
|
|
1137
|
+
if (stateGeneration !== this._stateGeneration) {
|
|
1138
|
+
throw createHwkError({
|
|
1139
|
+
code: HardwareErrorCode3.UserAborted,
|
|
1140
|
+
message: "Ledger discovery was reset"
|
|
1141
|
+
});
|
|
882
1142
|
}
|
|
883
|
-
await this._ensureDevicePermission();
|
|
884
|
-
const devices = await this.connector.searchDevices();
|
|
885
1143
|
this._discoveredDevices.clear();
|
|
886
1144
|
for (const d of devices) {
|
|
887
1145
|
if (d.connectId) {
|
|
@@ -889,7 +1147,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
889
1147
|
}
|
|
890
1148
|
}
|
|
891
1149
|
if (this._discoveredDevices.size === 0) {
|
|
892
|
-
await this._ensureDevicePermission();
|
|
1150
|
+
await this._ensureDevicePermission(void 0, void 0, signal);
|
|
893
1151
|
}
|
|
894
1152
|
const result = Array.from(this._discoveredDevices.values());
|
|
895
1153
|
debugLog("[LedgerAdapter][RES]", {
|
|
@@ -908,41 +1166,287 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
908
1166
|
throw err;
|
|
909
1167
|
}
|
|
910
1168
|
}
|
|
1169
|
+
async searchDeviceTargets(options) {
|
|
1170
|
+
const devices = await this.searchDevices(options);
|
|
1171
|
+
return devices.map((device) => ({
|
|
1172
|
+
searchTargetId: device.connectId,
|
|
1173
|
+
searchTargetReusePolicy: resolveSearchTargetReusePolicy(device),
|
|
1174
|
+
vendor: "ledger",
|
|
1175
|
+
connectionType: device.connectionType,
|
|
1176
|
+
kind: "physical",
|
|
1177
|
+
label: device.label,
|
|
1178
|
+
model: device.model,
|
|
1179
|
+
modelName: device.modelName,
|
|
1180
|
+
serialNumber: device.serialNumber
|
|
1181
|
+
}));
|
|
1182
|
+
}
|
|
1183
|
+
async listConnectionTargets(options) {
|
|
1184
|
+
const targets = await this.searchDeviceTargets(options);
|
|
1185
|
+
return targets.map(({ searchTargetId, ...target }) => ({
|
|
1186
|
+
...target,
|
|
1187
|
+
targetId: searchTargetId
|
|
1188
|
+
}));
|
|
1189
|
+
}
|
|
911
1190
|
// USB single-session invariant: evict all sessions, best-effort (see connectDevice).
|
|
912
|
-
async _evictAllSessions() {
|
|
1191
|
+
async _evictAllSessions(preserveOperationId) {
|
|
1192
|
+
this._operations.endAll("explicit", preserveOperationId);
|
|
913
1193
|
if (this._sessions.size === 0) return;
|
|
914
1194
|
const stale = [...this._sessions.values()];
|
|
915
1195
|
this._sessions.clear();
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1196
|
+
await this._runConnectorTeardown(async () => {
|
|
1197
|
+
for (const sid of stale) {
|
|
1198
|
+
try {
|
|
1199
|
+
await this.connector.disconnect(sid);
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
920
1202
|
}
|
|
921
|
-
}
|
|
1203
|
+
});
|
|
922
1204
|
}
|
|
923
1205
|
static _createDeviceBusyError(method) {
|
|
924
1206
|
return Object.assign(new Error(`Ledger device is busy while calling ${method}`), {
|
|
925
1207
|
code: HardwareErrorCode3.DeviceBusy
|
|
926
1208
|
});
|
|
927
1209
|
}
|
|
928
|
-
async connectDevice(
|
|
1210
|
+
async connectDevice(searchTargetId) {
|
|
1211
|
+
try {
|
|
1212
|
+
return await this._jobQueue.enqueue(
|
|
1213
|
+
searchTargetId || "__ledger_connect__",
|
|
1214
|
+
async () => {
|
|
1215
|
+
const connected = await this._connectTarget(searchTargetId);
|
|
1216
|
+
if (!connected.success) return connected;
|
|
1217
|
+
return this._createOperation(searchTargetId, connected.payload);
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
label: "connectDevice",
|
|
1221
|
+
rejectIfBusy: true,
|
|
1222
|
+
busyError: _LedgerAdapter._createDeviceBusyError("connectDevice")
|
|
1223
|
+
}
|
|
1224
|
+
);
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
return this.errorToFailure(error);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
async bindBleDevice(params) {
|
|
1230
|
+
if (params.identity.vendor !== "ledger" || !params.identity.value) {
|
|
1231
|
+
return failure2(HardwareErrorCode3.InvalidParams, "Ledger wallet identity is required");
|
|
1232
|
+
}
|
|
1233
|
+
const { chain, value: expectedFingerprint } = params.identity;
|
|
1234
|
+
try {
|
|
1235
|
+
return await this._jobQueue.enqueue(
|
|
1236
|
+
expectedFingerprint,
|
|
1237
|
+
async (signal) => {
|
|
1238
|
+
if (!this.getAvailableTransports().includes("ble")) {
|
|
1239
|
+
throw createHwkError({
|
|
1240
|
+
code: HardwareErrorCode3.TransportNotAvailable,
|
|
1241
|
+
message: "Ledger Bluetooth transport is not available"
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
this._activeConnectionType = "ble";
|
|
1245
|
+
await this._ensureDevicePermission(void 0, void 0, signal);
|
|
1246
|
+
const attempt = {
|
|
1247
|
+
extra: params.extra,
|
|
1248
|
+
bindingReason: "manual-rebind"
|
|
1249
|
+
};
|
|
1250
|
+
try {
|
|
1251
|
+
for (; ; ) {
|
|
1252
|
+
const connectId = await this._connectFirstOrSelect(
|
|
1253
|
+
[],
|
|
1254
|
+
void 0,
|
|
1255
|
+
true,
|
|
1256
|
+
void 0,
|
|
1257
|
+
attempt,
|
|
1258
|
+
signal
|
|
1259
|
+
);
|
|
1260
|
+
const sessionId = this._sessions.get(connectId);
|
|
1261
|
+
if (!sessionId) {
|
|
1262
|
+
throw createHwkError({
|
|
1263
|
+
code: HardwareErrorCode3.DeviceDisconnected,
|
|
1264
|
+
message: "Selected Ledger connection ended"
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
let saved = false;
|
|
1268
|
+
try {
|
|
1269
|
+
const installContext = {
|
|
1270
|
+
connection: { connectId, sessionId }
|
|
1271
|
+
};
|
|
1272
|
+
const fingerprint = await this._computeChainFingerprint(
|
|
1273
|
+
chain,
|
|
1274
|
+
(method, callParams) => this._runConnectorCall(
|
|
1275
|
+
connectId,
|
|
1276
|
+
method,
|
|
1277
|
+
callParams,
|
|
1278
|
+
signal,
|
|
1279
|
+
void 0,
|
|
1280
|
+
void 0,
|
|
1281
|
+
{ autoInstallApp: true },
|
|
1282
|
+
installContext
|
|
1283
|
+
)
|
|
1284
|
+
);
|
|
1285
|
+
if (fingerprint !== expectedFingerprint) {
|
|
1286
|
+
attempt.rejectedConnectIds ?? (attempt.rejectedConnectIds = /* @__PURE__ */ new Set());
|
|
1287
|
+
attempt.rejectedConnectIds.add(connectId);
|
|
1288
|
+
attempt.rejectedConnectId = connectId;
|
|
1289
|
+
} else {
|
|
1290
|
+
const persisted = await this._publishVerifiedBleBinding(
|
|
1291
|
+
connectId,
|
|
1292
|
+
chain,
|
|
1293
|
+
fingerprint,
|
|
1294
|
+
attempt,
|
|
1295
|
+
void 0,
|
|
1296
|
+
signal
|
|
1297
|
+
);
|
|
1298
|
+
if (!persisted) {
|
|
1299
|
+
throw createHwkError({
|
|
1300
|
+
code: HardwareErrorCode3.UnknownError,
|
|
1301
|
+
message: "Bluetooth binding could not be saved",
|
|
1302
|
+
origin: "host"
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
saved = true;
|
|
1306
|
+
return success(connectId);
|
|
1307
|
+
}
|
|
1308
|
+
} finally {
|
|
1309
|
+
if (!saved && this._sessions.get(connectId) === sessionId) {
|
|
1310
|
+
this._sessions.delete(connectId);
|
|
1311
|
+
const teardown = this._runConnectorTeardown(
|
|
1312
|
+
() => this.connector.disconnect(sessionId)
|
|
1313
|
+
).catch(() => void 0);
|
|
1314
|
+
if (!signal.aborted) await teardown;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
this._finishBleBinding(signal.aborted ? "cancelled" : "failed");
|
|
1320
|
+
throw error;
|
|
1321
|
+
}
|
|
1322
|
+
},
|
|
1323
|
+
{
|
|
1324
|
+
label: "bindBleDevice",
|
|
1325
|
+
rejectIfBusy: true,
|
|
1326
|
+
busyError: _LedgerAdapter._createDeviceBusyError("bindBleDevice")
|
|
1327
|
+
}
|
|
1328
|
+
);
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
return this.errorToFailure(error);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
async acquireOperation(connectId, context) {
|
|
1334
|
+
try {
|
|
1335
|
+
return await this._jobQueue.enqueue(
|
|
1336
|
+
connectId || "__ledger_acquire__",
|
|
1337
|
+
async (signal) => {
|
|
1338
|
+
const transport = this._isBleConnection() ? "ble" : "usb";
|
|
1339
|
+
const hint = context.knownConnections?.find(
|
|
1340
|
+
(connection) => connection.transport === transport
|
|
1341
|
+
);
|
|
1342
|
+
const target = hint && hint.transport !== "qr" ? hint.connectId : connectId;
|
|
1343
|
+
await this._ensureDevicePermission(target, void 0, signal);
|
|
1344
|
+
const attempt = {
|
|
1345
|
+
...context,
|
|
1346
|
+
extra: context.extra ? { ...context.extra } : void 0
|
|
1347
|
+
};
|
|
1348
|
+
try {
|
|
1349
|
+
const resolvedConnectId = await this.ensureConnected(
|
|
1350
|
+
target,
|
|
1351
|
+
signal,
|
|
1352
|
+
true,
|
|
1353
|
+
void 0,
|
|
1354
|
+
attempt
|
|
1355
|
+
);
|
|
1356
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
1357
|
+
const result = this._createOperation(connectId, resolvedConnectId);
|
|
1358
|
+
if (result.success && attempt.selectedConnection) {
|
|
1359
|
+
this._pendingOperationBindings.set(result.payload, attempt);
|
|
1360
|
+
}
|
|
1361
|
+
return result;
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
this._finishBleBinding(signal.aborted ? "cancelled" : "failed");
|
|
1364
|
+
throw error;
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
label: "acquireOperation",
|
|
1369
|
+
rejectIfBusy: true,
|
|
1370
|
+
busyError: _LedgerAdapter._createDeviceBusyError("acquireOperation")
|
|
1371
|
+
}
|
|
1372
|
+
);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
return this.errorToFailure(error);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
_createOperation(searchTargetId, resolvedConnectId) {
|
|
1378
|
+
this._operations.endByConnectionKey(resolvedConnectId, "explicit");
|
|
1379
|
+
const sessionId = this._sessions.get(resolvedConnectId);
|
|
1380
|
+
if (sessionId) this._operations.endByConnectionKey(sessionId, "explicit");
|
|
1381
|
+
const connectionType = this._isBleConnection() ? "ble" : "usb";
|
|
1382
|
+
const device = this._discoveredDevices.get(resolvedConnectId) ?? {
|
|
1383
|
+
vendor: "ledger",
|
|
1384
|
+
model: "unknown",
|
|
1385
|
+
firmwareVersion: "",
|
|
1386
|
+
deviceId: "",
|
|
1387
|
+
connectId: resolvedConnectId,
|
|
1388
|
+
connectionType
|
|
1389
|
+
};
|
|
1390
|
+
const operation = this._operations.create({
|
|
1391
|
+
searchTargetId,
|
|
1392
|
+
connectId: resolvedConnectId,
|
|
1393
|
+
device,
|
|
1394
|
+
connectionType,
|
|
1395
|
+
connectionKeys: [this._sessions.get(resolvedConnectId) ?? ""]
|
|
1396
|
+
});
|
|
1397
|
+
return success(operation.operationId);
|
|
1398
|
+
}
|
|
1399
|
+
async _connectTarget(connectId, preserveOperationId, signal) {
|
|
929
1400
|
debugLog("[LedgerAdapter][REQ]", { method: "connectDevice", connectId, params: { connectId } });
|
|
930
1401
|
try {
|
|
931
|
-
|
|
1402
|
+
this._assertConnectorReady("connectDevice");
|
|
1403
|
+
const discoveredType = this._discoveredDevices.get(connectId)?.connectionType;
|
|
1404
|
+
if (discoveredType === "usb" || discoveredType === "ble") {
|
|
1405
|
+
this._activeConnectionType = discoveredType;
|
|
1406
|
+
}
|
|
1407
|
+
if (this._isBleConnection() && !connectId) {
|
|
932
1408
|
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
933
1409
|
code: HardwareErrorCode3.DeviceNotFound
|
|
934
1410
|
});
|
|
935
1411
|
}
|
|
936
|
-
|
|
937
|
-
|
|
1412
|
+
await this._ensureDevicePermission(connectId, void 0, signal);
|
|
1413
|
+
if (signal) _LedgerAdapter._throwIfAborted(signal);
|
|
1414
|
+
if (this.connector.availableTransports && this.connector.availableTransports.length > 1) {
|
|
1415
|
+
await this._evictAllSessions(preserveOperationId);
|
|
1416
|
+
} else if (this._isBleConnection()) {
|
|
1417
|
+
const previousSessionId = this._sessions.get(connectId);
|
|
1418
|
+
this._operations.endByConnectionKey(connectId, "explicit", preserveOperationId);
|
|
1419
|
+
if (previousSessionId) {
|
|
1420
|
+
this._operations.endByConnectionKey(previousSessionId, "explicit", preserveOperationId);
|
|
1421
|
+
this._sessions.delete(connectId);
|
|
1422
|
+
await this.connector.disconnect(previousSessionId).catch(() => void 0);
|
|
1423
|
+
}
|
|
1424
|
+
} else {
|
|
1425
|
+
await this._evictAllSessions(preserveOperationId);
|
|
1426
|
+
}
|
|
1427
|
+
if (signal) _LedgerAdapter._throwIfAborted(signal);
|
|
1428
|
+
const stateGeneration = this._stateGeneration;
|
|
1429
|
+
const releaseOperation = this._retainConnectorOperation(`connect:${connectId}`);
|
|
1430
|
+
let session;
|
|
1431
|
+
try {
|
|
1432
|
+
session = this.connector.availableTransports?.length ? await this.connector.connect(connectId, {
|
|
1433
|
+
transportType: this._activeConnectionType ?? "usb"
|
|
1434
|
+
}) : await this.connector.connect(connectId);
|
|
1435
|
+
if (signal?.aborted || stateGeneration !== this._stateGeneration) {
|
|
1436
|
+
await this.connector.disconnect(session.sessionId).catch(() => void 0);
|
|
1437
|
+
throw Object.assign(new Error("Ledger connection aborted"), {
|
|
1438
|
+
code: HardwareErrorCode3.UserAborted
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
} finally {
|
|
1442
|
+
releaseOperation();
|
|
938
1443
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
this._sessions.set(connectId, session.sessionId);
|
|
1444
|
+
const resolvedConnectId = session.deviceInfo?.connectId || connectId;
|
|
1445
|
+
this._sessions.set(resolvedConnectId, session.sessionId);
|
|
942
1446
|
if (session.deviceInfo) {
|
|
943
|
-
this._discoveredDevices.set(
|
|
1447
|
+
this._discoveredDevices.set(resolvedConnectId, session.deviceInfo);
|
|
944
1448
|
}
|
|
945
|
-
const result = success(
|
|
1449
|
+
const result = success(resolvedConnectId);
|
|
946
1450
|
debugLog("[LedgerAdapter][RES]", { method: "connectDevice", success: true, payload: result });
|
|
947
1451
|
return result;
|
|
948
1452
|
} catch (err) {
|
|
@@ -955,30 +1459,60 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
955
1459
|
return failureResult;
|
|
956
1460
|
}
|
|
957
1461
|
}
|
|
958
|
-
async
|
|
1462
|
+
async releaseOperation(operationId) {
|
|
1463
|
+
const operation = this._operations.find(operationId);
|
|
1464
|
+
if (!operation) {
|
|
1465
|
+
this._operations.resolve(operationId);
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
const endedOperation = this._operations.end(operationId, "explicit");
|
|
1469
|
+
if (!endedOperation) return;
|
|
1470
|
+
const { connectId } = operation;
|
|
959
1471
|
debugLog("[LedgerAdapter][REQ]", {
|
|
960
|
-
method: "
|
|
1472
|
+
method: "releaseOperation",
|
|
961
1473
|
connectId,
|
|
962
1474
|
params: { connectId }
|
|
963
1475
|
});
|
|
964
1476
|
try {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
await this.connector.disconnect(sessionId);
|
|
968
|
-
this._sessions.delete(connectId);
|
|
969
|
-
}
|
|
970
|
-
debugLog("[LedgerAdapter][RES]", { method: "disconnectDevice", success: true });
|
|
1477
|
+
await this._releaseOperationConnection(endedOperation);
|
|
1478
|
+
debugLog("[LedgerAdapter][RES]", { method: "releaseOperation", success: true });
|
|
971
1479
|
} catch (err) {
|
|
972
1480
|
const e = err;
|
|
973
1481
|
debugLog("[LedgerAdapter][RES]", {
|
|
974
|
-
method: "
|
|
1482
|
+
method: "releaseOperation",
|
|
975
1483
|
success: false,
|
|
976
1484
|
error: { message: e?.message, _tag: e?._tag, code: e?.code ?? e?.errorCode }
|
|
977
1485
|
});
|
|
978
1486
|
throw err;
|
|
979
1487
|
}
|
|
980
1488
|
}
|
|
981
|
-
async
|
|
1489
|
+
async _releaseOperationConnection(operation) {
|
|
1490
|
+
const sessionIds = /* @__PURE__ */ new Set();
|
|
1491
|
+
for (const [connectId, sessionId] of this._sessions) {
|
|
1492
|
+
if (connectId === operation.connectId || operation.connectionKeys.includes(connectId) || operation.connectionKeys.includes(sessionId)) {
|
|
1493
|
+
this._sessions.delete(connectId);
|
|
1494
|
+
sessionIds.add(sessionId);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
for (const sessionId of sessionIds) {
|
|
1498
|
+
await this._runConnectorTeardown(
|
|
1499
|
+
() => this.connector.disconnect(sessionId).catch(() => void 0)
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
async _releaseLostOperationConnection(operationId) {
|
|
1504
|
+
const endedOperation = this._operations.end(operationId, "disconnect");
|
|
1505
|
+
if (!endedOperation) return;
|
|
1506
|
+
this._discoveredDevices.delete(endedOperation.connectId);
|
|
1507
|
+
await this._releaseOperationConnection(endedOperation);
|
|
1508
|
+
}
|
|
1509
|
+
async getDeviceInfo(connectIdOrOperationId, deviceId) {
|
|
1510
|
+
let connectId;
|
|
1511
|
+
try {
|
|
1512
|
+
connectId = isHardwareOperationId(connectIdOrOperationId) ? this._operations.resolve(connectIdOrOperationId).connectId : connectIdOrOperationId;
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
return this.errorToFailure(error);
|
|
1515
|
+
}
|
|
982
1516
|
debugLog("[LedgerAdapter][REQ]", {
|
|
983
1517
|
method: "getDeviceInfo",
|
|
984
1518
|
connectId,
|
|
@@ -1016,9 +1550,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1016
1550
|
throw err;
|
|
1017
1551
|
}
|
|
1018
1552
|
}
|
|
1019
|
-
getSupportedChains() {
|
|
1020
|
-
return ["evm", "btc", "sol", "tron"];
|
|
1021
|
-
}
|
|
1022
1553
|
// ---------------------------------------------------------------------------
|
|
1023
1554
|
// Chain call helper
|
|
1024
1555
|
// ---------------------------------------------------------------------------
|
|
@@ -1053,13 +1584,21 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1053
1584
|
if (params && typeof params === "object") {
|
|
1054
1585
|
const {
|
|
1055
1586
|
autoInstallApp,
|
|
1587
|
+
operationId,
|
|
1056
1588
|
passphraseState: _passphraseState,
|
|
1057
1589
|
useEmptyPassphrase: _useEmptyPassphrase,
|
|
1590
|
+
knownConnections,
|
|
1591
|
+
extra,
|
|
1592
|
+
allowDeviceSelection,
|
|
1058
1593
|
...rest
|
|
1059
1594
|
} = params;
|
|
1060
1595
|
return {
|
|
1061
1596
|
commonParams: {
|
|
1062
|
-
autoInstallApp: typeof autoInstallApp === "boolean" ? autoInstallApp : void 0
|
|
1597
|
+
autoInstallApp: typeof autoInstallApp === "boolean" ? autoInstallApp : void 0,
|
|
1598
|
+
operationId: typeof operationId === "string" ? operationId : void 0,
|
|
1599
|
+
knownConnections,
|
|
1600
|
+
extra,
|
|
1601
|
+
allowDeviceSelection: typeof allowDeviceSelection === "boolean" ? allowDeviceSelection : void 0
|
|
1063
1602
|
},
|
|
1064
1603
|
rest
|
|
1065
1604
|
};
|
|
@@ -1235,6 +1774,27 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1235
1774
|
);
|
|
1236
1775
|
}
|
|
1237
1776
|
// ---------------------------------------------------------------------------
|
|
1777
|
+
// Zcash chain methods (viewing key + shielded address reads; Zcash app >= 3.8.0)
|
|
1778
|
+
// ---------------------------------------------------------------------------
|
|
1779
|
+
zcashGetFullViewingKey(connectId, deviceId, params) {
|
|
1780
|
+
return this.callChainWithMergedParams(
|
|
1781
|
+
connectId,
|
|
1782
|
+
deviceId,
|
|
1783
|
+
"zcash",
|
|
1784
|
+
"zcashGetFullViewingKey",
|
|
1785
|
+
params
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
zcashGetShieldedAddress(connectId, deviceId, params) {
|
|
1789
|
+
return this.callChainWithMergedParams(
|
|
1790
|
+
connectId,
|
|
1791
|
+
deviceId,
|
|
1792
|
+
"zcash",
|
|
1793
|
+
"zcashGetShieldedAddress",
|
|
1794
|
+
params
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
// ---------------------------------------------------------------------------
|
|
1238
1798
|
// App management — OS-level Ledger app install / list. Bypasses fingerprint
|
|
1239
1799
|
// and chain-handler dispatch; installApp progress is forwarded to the adapter
|
|
1240
1800
|
// emitter via 'ui-event' AppInstallProgress events.
|
|
@@ -1323,7 +1883,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1323
1883
|
);
|
|
1324
1884
|
}
|
|
1325
1885
|
await this.connector.configure({ ledgerGenuineCheckWebSocketUrl: relayUrl });
|
|
1326
|
-
this.
|
|
1886
|
+
await this._resetStateAndDisconnectSessions();
|
|
1327
1887
|
}
|
|
1328
1888
|
const result = await this.connectorCall(connectId, "getDeviceGenuineCheck", {});
|
|
1329
1889
|
if (!result.isGenuine) {
|
|
@@ -1349,10 +1909,10 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1349
1909
|
if (relayUrl) {
|
|
1350
1910
|
try {
|
|
1351
1911
|
await this.connector.configure?.({ ledgerGenuineCheckWebSocketUrl: void 0 });
|
|
1352
|
-
this.
|
|
1912
|
+
await this._resetStateAndDisconnectSessions();
|
|
1353
1913
|
} catch {
|
|
1354
1914
|
this.connector.reset();
|
|
1355
|
-
this.
|
|
1915
|
+
await this._resetStateAndDisconnectSessions();
|
|
1356
1916
|
}
|
|
1357
1917
|
}
|
|
1358
1918
|
}
|
|
@@ -1364,6 +1924,20 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1364
1924
|
this.emitter.off(event, listener);
|
|
1365
1925
|
}
|
|
1366
1926
|
cancel(connectId) {
|
|
1927
|
+
const namedOperationIsLive = (id) => {
|
|
1928
|
+
try {
|
|
1929
|
+
this._operations.resolve(id);
|
|
1930
|
+
return true;
|
|
1931
|
+
} catch {
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
if (isHardwareOperationId(connectId) && !namedOperationIsLive(connectId)) {
|
|
1936
|
+
debugLog("[LedgerAdapter] cancel target already ended; nothing to cancel", {
|
|
1937
|
+
connectId
|
|
1938
|
+
});
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1367
1941
|
const userAbortReason = Object.assign(new Error("User aborted operation"), {
|
|
1368
1942
|
code: HardwareErrorCode3.UserAborted,
|
|
1369
1943
|
_tag: ERROR_TAG.UserAborted
|
|
@@ -1374,14 +1948,50 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1374
1948
|
this._lastCancelReason = void 0;
|
|
1375
1949
|
}
|
|
1376
1950
|
}, 2e3);
|
|
1377
|
-
this.
|
|
1378
|
-
|
|
1379
|
-
|
|
1951
|
+
const activeJobId = this._jobQueue.getActiveJob()?.deviceId;
|
|
1952
|
+
let operationId;
|
|
1953
|
+
if (isHardwareOperationId(connectId)) {
|
|
1954
|
+
operationId = connectId;
|
|
1955
|
+
} else if (!connectId && isHardwareOperationId(activeJobId)) {
|
|
1956
|
+
operationId = activeJobId;
|
|
1957
|
+
}
|
|
1958
|
+
let resolvedConnectId = connectId;
|
|
1959
|
+
if (operationId) {
|
|
1960
|
+
try {
|
|
1961
|
+
resolvedConnectId = this._operations.resolve(operationId).connectId;
|
|
1962
|
+
} catch {
|
|
1963
|
+
resolvedConnectId = void 0;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
const interactionForPhysicalId = !operationId && connectId ? this._operations.findActiveByConnectionKey(connectId) : void 0;
|
|
1967
|
+
const pendingOperationId = operationId ?? interactionForPhysicalId?.operationId;
|
|
1968
|
+
if (!connectId) {
|
|
1969
|
+
this._uiRegistry.cancel();
|
|
1970
|
+
} else if (pendingOperationId) {
|
|
1971
|
+
this._uiRegistry.cancel(void 0, void 0, pendingOperationId);
|
|
1972
|
+
}
|
|
1973
|
+
this._finishBleBinding("cancelled");
|
|
1974
|
+
if (!connectId) this._pendingOperationBindings.clear();
|
|
1975
|
+
else if (pendingOperationId) {
|
|
1976
|
+
this._pendingOperationBindings.delete(pendingOperationId);
|
|
1977
|
+
}
|
|
1978
|
+
const queueKeys = /* @__PURE__ */ new Set();
|
|
1979
|
+
if (connectId) queueKeys.add(ledgerQueueKey({ connectId }));
|
|
1980
|
+
if (pendingOperationId) queueKeys.add(ledgerQueueKey({ operationId: pendingOperationId }));
|
|
1981
|
+
if (queueKeys.size) {
|
|
1982
|
+
let cancelledAnyJob = false;
|
|
1983
|
+
for (const key of queueKeys) {
|
|
1984
|
+
cancelledAnyJob = this._jobQueue.cancelActiveAndPending(key, userAbortReason) || cancelledAnyJob;
|
|
1985
|
+
}
|
|
1986
|
+
debugLog("[LedgerAdapter] cancel routed to queue keys", {
|
|
1987
|
+
queueKeys: [...queueKeys],
|
|
1988
|
+
cancelledAnyJob
|
|
1989
|
+
});
|
|
1380
1990
|
} else {
|
|
1381
1991
|
this._jobQueue.cancelActiveAndPending(void 0, userAbortReason);
|
|
1382
1992
|
}
|
|
1383
|
-
if (
|
|
1384
|
-
const sessionId = this._sessions.get(
|
|
1993
|
+
if (resolvedConnectId) {
|
|
1994
|
+
const sessionId = this._sessions.get(resolvedConnectId) ?? resolvedConnectId;
|
|
1385
1995
|
void this.connector.cancel(sessionId);
|
|
1386
1996
|
} else {
|
|
1387
1997
|
for (const sid of this._sessions.values()) void this.connector.cancel(sid);
|
|
@@ -1393,44 +2003,102 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1393
2003
|
// ---------------------------------------------------------------------------
|
|
1394
2004
|
// Chain fingerprint
|
|
1395
2005
|
// ---------------------------------------------------------------------------
|
|
2006
|
+
/** A non-empty deviceId is an expected chain fingerprint, not a transport identifier. */
|
|
1396
2007
|
async getChainFingerprint(connectId, deviceId, chain) {
|
|
1397
2008
|
try {
|
|
1398
2009
|
const fingerprint = await this._computeChainFingerprint(
|
|
1399
2010
|
chain,
|
|
1400
2011
|
(method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
|
|
1401
2012
|
);
|
|
2013
|
+
if (deviceId) {
|
|
2014
|
+
if (fingerprint !== deviceId) {
|
|
2015
|
+
return failure2(
|
|
2016
|
+
HardwareErrorCode3.DeviceMismatch,
|
|
2017
|
+
formatDeviceMismatchError(deviceId, fingerprint)
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
if (isHardwareOperationId(connectId)) {
|
|
2021
|
+
const operation = this._operations.resolve(connectId);
|
|
2022
|
+
await this._publishVerifiedBleBinding(
|
|
2023
|
+
operation.connectId,
|
|
2024
|
+
chain,
|
|
2025
|
+
fingerprint,
|
|
2026
|
+
void 0,
|
|
2027
|
+
connectId
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
1402
2031
|
return success(fingerprint);
|
|
1403
2032
|
} catch (err) {
|
|
1404
2033
|
debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
|
|
1405
2034
|
return this.errorToFailure(err);
|
|
1406
2035
|
}
|
|
1407
2036
|
}
|
|
1408
|
-
/**
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
2037
|
+
/** Discovery may select a BLE target before a later call verifies its wallet. */
|
|
2038
|
+
async _publishVerifiedBleBinding(connectId, chain, fingerprint, attempt, operationId, signal) {
|
|
2039
|
+
const binding = operationId ? this._pendingOperationBindings.get(operationId) : attempt;
|
|
2040
|
+
if (!this._isBleConnection() || binding?.selectedConnection?.connectId !== connectId) {
|
|
2041
|
+
return false;
|
|
2042
|
+
}
|
|
1414
2043
|
try {
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
|
|
2044
|
+
const outcome = await requestSaveDeviceBinding(
|
|
2045
|
+
this.emitter,
|
|
2046
|
+
this._uiRegistry,
|
|
2047
|
+
{
|
|
2048
|
+
selectionRequestId: binding.selectedConnection.requestId,
|
|
2049
|
+
connection: { transport: "ble", connectId },
|
|
2050
|
+
identity: { vendor: "ledger", type: "chainFingerprint", chain, value: fingerprint },
|
|
2051
|
+
extra: binding.extra,
|
|
2052
|
+
operationId
|
|
2053
|
+
},
|
|
2054
|
+
signal
|
|
1418
2055
|
);
|
|
1419
|
-
if (
|
|
1420
|
-
|
|
2056
|
+
if (operationId) this._operations.resolve(operationId);
|
|
2057
|
+
if (!outcome.saved) {
|
|
2058
|
+
debugLog("[LedgerAdapter] BLE binding not persisted by host", {
|
|
2059
|
+
connectId,
|
|
2060
|
+
reason: outcome.reason
|
|
2061
|
+
});
|
|
1421
2062
|
}
|
|
1422
|
-
return
|
|
1423
|
-
} catch (
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
2063
|
+
return outcome.saved;
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
if (operationId) {
|
|
2066
|
+
this._pendingOperationBindings.delete(operationId);
|
|
2067
|
+
this._operations.end(operationId, "explicit");
|
|
1427
2068
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
2069
|
+
const sessionId = this._sessions.get(connectId);
|
|
2070
|
+
this._sessions.delete(connectId);
|
|
2071
|
+
if (sessionId) {
|
|
2072
|
+
const teardown = this._runConnectorTeardown(
|
|
2073
|
+
() => this.connector.disconnect(sessionId)
|
|
2074
|
+
).catch(() => void 0);
|
|
2075
|
+
if (!signal?.aborted) await teardown;
|
|
2076
|
+
}
|
|
2077
|
+
throw error;
|
|
2078
|
+
} finally {
|
|
2079
|
+
if (this._bindingSelectionRequestId === binding.selectedConnection.requestId) {
|
|
2080
|
+
this._bindingSelectionRequestId = void 0;
|
|
2081
|
+
}
|
|
2082
|
+
if (operationId) {
|
|
2083
|
+
this._pendingOperationBindings.delete(operationId);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
/** Verify on the acquired session without re-entering the job queue. */
|
|
2088
|
+
async _verifyDeviceFingerprintWithSession(sessionId, deviceId, chain) {
|
|
2089
|
+
if (!deviceId) return { success: true };
|
|
2090
|
+
const fingerprint = await this._computeChainFingerprint(
|
|
2091
|
+
chain,
|
|
2092
|
+
(method, params) => this._callConnector(sessionId, method, params)
|
|
2093
|
+
);
|
|
2094
|
+
if (fingerprint === deviceId) {
|
|
2095
|
+
return { success: true };
|
|
2096
|
+
}
|
|
2097
|
+
return { success: false, expected: deviceId, actual: fingerprint };
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Compute the chain fingerprint via a caller-supplied call strategy.
|
|
2101
|
+
*
|
|
1434
2102
|
* Chains with a native device-side identity primitive (BTC → BIP32 master
|
|
1435
2103
|
* fingerprint) short-circuit at the top and return it verbatim, so the value
|
|
1436
2104
|
* stays reusable for higher-level use (BIP380 descriptors, PSBT signing).
|
|
@@ -1455,12 +2123,27 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1455
2123
|
address = (await callMethod("solGetAddress", { path, showOnDevice: false })).address;
|
|
1456
2124
|
} else if (chain === "tron") {
|
|
1457
2125
|
address = (await callMethod("tronGetAddress", { path, showOnDevice: false })).address;
|
|
2126
|
+
} else if (chain === "zcash") {
|
|
2127
|
+
address = (await callMethod("zcashGetShieldedAddress", { path, showOnDevice: false })).address;
|
|
1458
2128
|
} else {
|
|
1459
2129
|
throw new Error(`Unsupported chain for fingerprint: ${chain}`);
|
|
1460
2130
|
}
|
|
1461
2131
|
return deriveDeviceFingerprint(address);
|
|
1462
2132
|
}
|
|
1463
2133
|
// Ledger WebUSB won't expose a locked device, so we can't auto-detect unlock.
|
|
2134
|
+
/**
|
|
2135
|
+
* The operation the running device job belongs to, so a UI request raised
|
|
2136
|
+
* mid-call can name it. A call pinned to an operation queues under its id
|
|
2137
|
+
* (`ledgerQueueKey`); a call without one queues under the connectId, and the
|
|
2138
|
+
* live operation on that connection is still the owner. Only work with no
|
|
2139
|
+
* operation at all (cold start) comes back undefined.
|
|
2140
|
+
*/
|
|
2141
|
+
_activeOperationId() {
|
|
2142
|
+
const activeJobId = this._jobQueue.getActiveJob()?.deviceId;
|
|
2143
|
+
if (!activeJobId) return void 0;
|
|
2144
|
+
if (isHardwareOperationId(activeJobId)) return activeJobId;
|
|
2145
|
+
return this._operations.findActiveByConnectionKey(activeJobId)?.operationId;
|
|
2146
|
+
}
|
|
1464
2147
|
// The user must press Confirm after unlocking, which triggers a search retry.
|
|
1465
2148
|
// If `signal` is provided, an abort cancels the pending UI request so the
|
|
1466
2149
|
// registry slot is released and a stale RECEIVE_DEVICE_CONNECT won't land in
|
|
@@ -1469,15 +2152,18 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1469
2152
|
if (signal?.aborted) {
|
|
1470
2153
|
_LedgerAdapter._throwIfAborted(signal);
|
|
1471
2154
|
}
|
|
2155
|
+
const operationId = this._activeOperationId();
|
|
1472
2156
|
const waitPromise = this._uiRegistry.wait(
|
|
1473
|
-
UI_REQUEST.REQUEST_DEVICE_CONNECT
|
|
2157
|
+
UI_REQUEST.REQUEST_DEVICE_CONNECT,
|
|
2158
|
+
{ operationId }
|
|
1474
2159
|
);
|
|
1475
2160
|
this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_CONNECT, {
|
|
1476
2161
|
type: UI_REQUEST.REQUEST_DEVICE_CONNECT,
|
|
1477
2162
|
payload: {
|
|
1478
2163
|
vendor: "ledger",
|
|
1479
2164
|
reason: "device-not-found",
|
|
1480
|
-
message: "Please connect and unlock your Ledger device"
|
|
2165
|
+
message: "Please connect and unlock your Ledger device",
|
|
2166
|
+
operationId
|
|
1481
2167
|
}
|
|
1482
2168
|
});
|
|
1483
2169
|
let payload;
|
|
@@ -1538,15 +2224,18 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1538
2224
|
return { ...params, showOnDevice: true };
|
|
1539
2225
|
}
|
|
1540
2226
|
async _waitForBtcHighIndexConfirm(path, accountIndex) {
|
|
2227
|
+
const operationId = this._activeOperationId();
|
|
1541
2228
|
const waitPromise = this._uiRegistry.wait(
|
|
1542
|
-
UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM
|
|
2229
|
+
UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
|
|
2230
|
+
{ operationId }
|
|
1543
2231
|
);
|
|
1544
2232
|
this.emitter.emit(UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM, {
|
|
1545
2233
|
type: UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
|
|
1546
2234
|
payload: {
|
|
1547
2235
|
vendor: "ledger",
|
|
1548
2236
|
path,
|
|
1549
|
-
accountIndex
|
|
2237
|
+
accountIndex,
|
|
2238
|
+
operationId
|
|
1550
2239
|
}
|
|
1551
2240
|
});
|
|
1552
2241
|
try {
|
|
@@ -1563,12 +2252,14 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1563
2252
|
// Ask the user whether to install a missing app (autoInstallApp flow).
|
|
1564
2253
|
// Same register-then-emit ordering as the BTC high-index gate.
|
|
1565
2254
|
async _waitForInstallAppConfirm(appName) {
|
|
2255
|
+
const operationId = this._activeOperationId();
|
|
1566
2256
|
const waitPromise = this._uiRegistry.wait(
|
|
1567
|
-
UI_REQUEST.REQUEST_INSTALL_APP
|
|
2257
|
+
UI_REQUEST.REQUEST_INSTALL_APP,
|
|
2258
|
+
{ operationId }
|
|
1568
2259
|
);
|
|
1569
2260
|
this.emitter.emit(UI_REQUEST.REQUEST_INSTALL_APP, {
|
|
1570
2261
|
type: UI_REQUEST.REQUEST_INSTALL_APP,
|
|
1571
|
-
payload: { vendor: "ledger", appName }
|
|
2262
|
+
payload: { vendor: "ledger", appName, operationId }
|
|
1572
2263
|
});
|
|
1573
2264
|
try {
|
|
1574
2265
|
const payload = await waitPromise;
|
|
@@ -1584,16 +2275,17 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1584
2275
|
// Layer 1 entry. Caller signal only races the outer awaiter; the shared
|
|
1585
2276
|
// `_doConnect` runs under its own internal controller so caller A's cancel
|
|
1586
2277
|
// doesn't kill caller B's await.
|
|
1587
|
-
async ensureConnected(connectId, signal, allowUsbEphemeralFallback = false) {
|
|
2278
|
+
async ensureConnected(connectId, signal, allowUsbEphemeralFallback = false, preserveOperationId, context) {
|
|
1588
2279
|
if (signal.aborted) _LedgerAdapter._throwIfAborted(signal);
|
|
1589
|
-
|
|
2280
|
+
this._assertConnectorReady("connectDevice");
|
|
2281
|
+
if (this._isBleConnection() && !connectId && !allowUsbEphemeralFallback) {
|
|
1590
2282
|
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
1591
2283
|
code: HardwareErrorCode3.DeviceNotFound
|
|
1592
2284
|
});
|
|
1593
2285
|
}
|
|
1594
2286
|
if (connectId && this._sessions.has(connectId)) return connectId;
|
|
1595
2287
|
if (!connectId && this._sessions.size > 0) {
|
|
1596
|
-
if (!
|
|
2288
|
+
if (!this._isBleConnection() && this._sessions.size > 1) {
|
|
1597
2289
|
throw Object.assign(
|
|
1598
2290
|
new Error(
|
|
1599
2291
|
"Ledger USB session invariant violated: more than one session is active. Please reconnect the device."
|
|
@@ -1603,15 +2295,24 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1603
2295
|
}
|
|
1604
2296
|
return this._sessions.keys().next().value;
|
|
1605
2297
|
}
|
|
1606
|
-
if (!this._connectingPromise) {
|
|
1607
|
-
|
|
1608
|
-
|
|
2298
|
+
if (!this._connectingPromise || this._doConnectAbortController?.signal.aborted) {
|
|
2299
|
+
const controller = new AbortController();
|
|
2300
|
+
this._doConnectAbortController = controller;
|
|
2301
|
+
const innerSignal = controller.signal;
|
|
1609
2302
|
this._connectingPromise = (async () => {
|
|
1610
2303
|
try {
|
|
1611
|
-
return await this._doConnect(
|
|
2304
|
+
return await this._doConnect(
|
|
2305
|
+
innerSignal,
|
|
2306
|
+
connectId,
|
|
2307
|
+
allowUsbEphemeralFallback,
|
|
2308
|
+
preserveOperationId,
|
|
2309
|
+
context
|
|
2310
|
+
);
|
|
1612
2311
|
} finally {
|
|
1613
|
-
this.
|
|
1614
|
-
|
|
2312
|
+
if (this._doConnectAbortController === controller) {
|
|
2313
|
+
this._connectingPromise = null;
|
|
2314
|
+
this._doConnectAbortController = null;
|
|
2315
|
+
}
|
|
1615
2316
|
}
|
|
1616
2317
|
})();
|
|
1617
2318
|
}
|
|
@@ -1620,22 +2321,51 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1620
2321
|
// Layer 1 main loop — the ONLY place in SDK that emits unlock dialog.
|
|
1621
2322
|
// Bounded by MAX_DOCONNECT_CONFIRMS — after N Confirms with no progress,
|
|
1622
2323
|
// throw DeviceNotFound so the user is kicked out of the loop.
|
|
1623
|
-
async _doConnect(internalSignal, targetConnectId, allowUsbEphemeralFallback = false) {
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
2324
|
+
async _doConnect(internalSignal, targetConnectId, allowUsbEphemeralFallback = false, preserveOperationId, context) {
|
|
2325
|
+
_LedgerAdapter._throwIfAborted(internalSignal);
|
|
2326
|
+
if (this.connector.availableTransports?.includes("usb") && this.connector.availableTransports.includes("ble")) {
|
|
2327
|
+
this._activeConnectionType = "usb";
|
|
2328
|
+
const usbDevices = await this._searchDevices({ transportType: "usb" }, internalSignal);
|
|
2329
|
+
_LedgerAdapter._throwIfAborted(internalSignal);
|
|
2330
|
+
if (usbDevices.length > 0) {
|
|
2331
|
+
const knownUsb = context?.knownConnections?.find(
|
|
2332
|
+
(connection) => connection.transport === "usb"
|
|
2333
|
+
);
|
|
2334
|
+
const usbTarget = knownUsb?.transport === "usb" ? knownUsb.connectId : usbDevices.find((device) => device.connectId === targetConnectId)?.connectId;
|
|
2335
|
+
return this._connectFirstOrSelect(
|
|
2336
|
+
usbDevices,
|
|
2337
|
+
usbTarget,
|
|
2338
|
+
allowUsbEphemeralFallback,
|
|
2339
|
+
preserveOperationId,
|
|
2340
|
+
context,
|
|
2341
|
+
internalSignal
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
this._activeConnectionType = "ble";
|
|
2345
|
+
const knownBle = context?.knownConnections?.find(
|
|
2346
|
+
(connection) => connection.transport === "ble"
|
|
2347
|
+
);
|
|
2348
|
+
if (knownBle?.transport === "ble") {
|
|
2349
|
+
return this._connectDeviceOrThrow(knownBle.connectId, preserveOperationId, internalSignal);
|
|
2350
|
+
}
|
|
2351
|
+
if (targetConnectId && context?.knownConnections === void 0) {
|
|
2352
|
+
throw createHwkError({
|
|
2353
|
+
code: HardwareErrorCode3.DeviceNotFound,
|
|
2354
|
+
message: "Ledger connection metadata is required before starting Bluetooth binding"
|
|
2355
|
+
});
|
|
1638
2356
|
}
|
|
2357
|
+
const bleDevices = await this._searchDevices({ transportType: "ble" }, internalSignal);
|
|
2358
|
+
return this._connectFirstOrSelect(
|
|
2359
|
+
bleDevices,
|
|
2360
|
+
void 0,
|
|
2361
|
+
allowUsbEphemeralFallback,
|
|
2362
|
+
preserveOperationId,
|
|
2363
|
+
context,
|
|
2364
|
+
internalSignal
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2367
|
+
if (this._isBleConnection() && targetConnectId) {
|
|
2368
|
+
return this._connectDeviceOrThrow(targetConnectId, preserveOperationId, internalSignal);
|
|
1639
2369
|
}
|
|
1640
2370
|
let confirms = 0;
|
|
1641
2371
|
while (!internalSignal.aborted) {
|
|
@@ -1643,22 +2373,28 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1643
2373
|
type: EConnectorInteraction.Searching,
|
|
1644
2374
|
payload: { sessionId: "" }
|
|
1645
2375
|
});
|
|
1646
|
-
let devices = await this.
|
|
2376
|
+
let devices = await this._searchDevices(void 0, internalSignal);
|
|
2377
|
+
_LedgerAdapter._throwIfAborted(internalSignal);
|
|
1647
2378
|
if (devices.length === 0) {
|
|
1648
2379
|
for (let i = 0; i < 3 && !internalSignal.aborted; i += 1) {
|
|
1649
2380
|
await new Promise((resolve) => {
|
|
1650
2381
|
setTimeout(resolve, DEVICE_CONNECT_RETRY_DELAY_MS);
|
|
1651
2382
|
});
|
|
1652
|
-
|
|
2383
|
+
_LedgerAdapter._throwIfAborted(internalSignal);
|
|
2384
|
+
devices = await this._searchDevices(void 0, internalSignal);
|
|
2385
|
+
_LedgerAdapter._throwIfAborted(internalSignal);
|
|
1653
2386
|
if (devices.length > 0) break;
|
|
1654
2387
|
}
|
|
1655
2388
|
}
|
|
1656
|
-
if (devices.length > 0) {
|
|
2389
|
+
if (devices.length > 0 || this._isBleConnection() && allowUsbEphemeralFallback) {
|
|
1657
2390
|
try {
|
|
1658
2391
|
return await this._connectFirstOrSelect(
|
|
1659
2392
|
devices,
|
|
1660
2393
|
targetConnectId,
|
|
1661
|
-
allowUsbEphemeralFallback
|
|
2394
|
+
allowUsbEphemeralFallback,
|
|
2395
|
+
preserveOperationId,
|
|
2396
|
+
context,
|
|
2397
|
+
internalSignal
|
|
1662
2398
|
);
|
|
1663
2399
|
} catch (err) {
|
|
1664
2400
|
if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
|
|
@@ -1687,45 +2423,136 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1687
2423
|
_LedgerAdapter._throwIfAborted(internalSignal);
|
|
1688
2424
|
throw new Error("_doConnect aborted");
|
|
1689
2425
|
}
|
|
1690
|
-
async _connectFirstOrSelect(devices, targetConnectId, allowUsbEphemeralFallback
|
|
2426
|
+
async _connectFirstOrSelect(devices, targetConnectId, allowUsbEphemeralFallback, preserveOperationId, context, signal) {
|
|
2427
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
1691
2428
|
if (targetConnectId) {
|
|
1692
2429
|
const target = devices.find(
|
|
1693
2430
|
(d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
|
|
1694
2431
|
);
|
|
1695
2432
|
if (target) {
|
|
1696
|
-
return this._connectDeviceOrThrow(target.connectId);
|
|
2433
|
+
return this._connectDeviceOrThrow(target.connectId, preserveOperationId, signal);
|
|
1697
2434
|
}
|
|
1698
|
-
if (!
|
|
2435
|
+
if (!this._isBleConnection() && devices.length === 1 && allowUsbEphemeralFallback) {
|
|
1699
2436
|
debugLog(
|
|
1700
2437
|
`[LedgerAdapter] target ${targetConnectId} not in fresh enumeration; accepting sole USB device ${devices[0].connectId} for fingerprint-verified recovery`
|
|
1701
2438
|
);
|
|
1702
|
-
return this._connectDeviceOrThrow(devices[0].connectId);
|
|
2439
|
+
return this._connectDeviceOrThrow(devices[0].connectId, preserveOperationId, signal);
|
|
1703
2440
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
2441
|
+
if (!this._isBleConnection() || !allowUsbEphemeralFallback) {
|
|
2442
|
+
const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
|
|
2443
|
+
code: HardwareErrorCode3.DeviceNotFound
|
|
2444
|
+
});
|
|
2445
|
+
if (this._isBleConnection()) {
|
|
2446
|
+
err._tag = ERROR_TAG.DeviceNotAdvertising;
|
|
2447
|
+
}
|
|
2448
|
+
throw err;
|
|
1709
2449
|
}
|
|
1710
|
-
throw err;
|
|
1711
2450
|
}
|
|
1712
|
-
|
|
2451
|
+
const requiresBleSelection = this._isBleConnection();
|
|
2452
|
+
if (requiresBleSelection && !allowUsbEphemeralFallback) {
|
|
1713
2453
|
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
1714
2454
|
code: HardwareErrorCode3.DeviceNotFound
|
|
1715
2455
|
});
|
|
1716
2456
|
}
|
|
1717
|
-
if (
|
|
1718
|
-
|
|
2457
|
+
if (requiresBleSelection && context?.allowDeviceSelection !== false) {
|
|
2458
|
+
const bindingSessionId = context?.bindingSessionId ?? this._uiRegistry.createRequestId();
|
|
2459
|
+
if (context) context.bindingSessionId = bindingSessionId;
|
|
2460
|
+
const allowUsbFallback = context?.bindingReason !== "manual-rebind" && Boolean(this.connector.availableTransports?.includes("usb"));
|
|
2461
|
+
const knownUsb = context?.knownConnections?.find(
|
|
2462
|
+
(connection) => connection.transport === "usb"
|
|
2463
|
+
);
|
|
2464
|
+
const usbConnectId = knownUsb?.transport === "usb" ? knownUsb.connectId : targetConnectId;
|
|
2465
|
+
const { device, requestId } = await requestBleDeviceSelection({
|
|
2466
|
+
emitter: this.emitter,
|
|
2467
|
+
registry: this._uiRegistry,
|
|
2468
|
+
signal,
|
|
2469
|
+
allowUsbFallback,
|
|
2470
|
+
scan: async () => {
|
|
2471
|
+
if (allowUsbFallback) {
|
|
2472
|
+
const usbDevices = await this._searchDevices({ transportType: "usb" }, signal);
|
|
2473
|
+
const candidate = usbDevices.find(
|
|
2474
|
+
(device2) => device2.connectionType === "usb" && device2.connectId === usbConnectId
|
|
2475
|
+
) ?? (usbDevices.length === 1 && usbDevices[0].connectionType === "usb" ? usbDevices[0] : void 0);
|
|
2476
|
+
if (candidate) return [candidate];
|
|
2477
|
+
}
|
|
2478
|
+
return (await this._searchDevices({ transportType: "ble", waitForAllTransports: true }, signal)).filter((device2) => !context?.rejectedConnectIds?.has(device2.connectId));
|
|
2479
|
+
},
|
|
2480
|
+
request: {
|
|
2481
|
+
devices: devices.filter((device2) => !context?.rejectedConnectIds?.has(device2.connectId)),
|
|
2482
|
+
bindingSessionId,
|
|
2483
|
+
rejectedConnectId: context?.rejectedConnectId,
|
|
2484
|
+
context: {
|
|
2485
|
+
kind: "bind-connection",
|
|
2486
|
+
transport: "ble",
|
|
2487
|
+
reason: context?.bindingReason ?? (targetConnectId ? "known-connection-unavailable" : "missing-binding")
|
|
2488
|
+
},
|
|
2489
|
+
extra: context?.extra,
|
|
2490
|
+
operationId: preserveOperationId
|
|
2491
|
+
}
|
|
2492
|
+
});
|
|
2493
|
+
if (device.connectionType === "usb") {
|
|
2494
|
+
this._activeConnectionType = "usb";
|
|
2495
|
+
if (context) context.selectedConnection = void 0;
|
|
2496
|
+
if (preserveOperationId) this._pendingOperationBindings.delete(preserveOperationId);
|
|
2497
|
+
this._bindingSelectionRequestId = void 0;
|
|
2498
|
+
this.emitter.emit(UI_REQUEST.DEVICE_BINDING_STATUS, {
|
|
2499
|
+
type: UI_REQUEST.DEVICE_BINDING_STATUS,
|
|
2500
|
+
payload: { selectionRequestId: requestId, status: "cancelled" }
|
|
2501
|
+
});
|
|
2502
|
+
return this._connectDeviceOrThrow(device.connectId, preserveOperationId, signal);
|
|
2503
|
+
}
|
|
2504
|
+
if (context) context.selectedConnection = { connectId: device.connectId, requestId };
|
|
2505
|
+
this._bindingSelectionRequestId = requestId;
|
|
2506
|
+
return this._connectDeviceOrThrow(device.connectId, preserveOperationId, signal);
|
|
2507
|
+
}
|
|
2508
|
+
if (devices.length > 0 && (devices.length > 1 || requiresBleSelection)) {
|
|
2509
|
+
if (context?.allowDeviceSelection === false || !this.emitter.listenerCount(UI_REQUEST.REQUEST_SELECT_DEVICE)) {
|
|
2510
|
+
throw createHwkError({
|
|
2511
|
+
code: HardwareErrorCode3.DeviceNotFound,
|
|
2512
|
+
message: "Select a Ledger device before continuing"
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
const requestId = this._uiRegistry.createRequestId();
|
|
2516
|
+
const operationId = this._activeOperationId();
|
|
2517
|
+
const waitPromise = this._uiRegistry.wait(
|
|
2518
|
+
UI_REQUEST.REQUEST_SELECT_DEVICE,
|
|
2519
|
+
{ requestId, operationId }
|
|
2520
|
+
);
|
|
2521
|
+
this.emitter.emit(UI_REQUEST.REQUEST_SELECT_DEVICE, {
|
|
2522
|
+
type: UI_REQUEST.REQUEST_SELECT_DEVICE,
|
|
2523
|
+
payload: {
|
|
2524
|
+
devices,
|
|
2525
|
+
requestId,
|
|
2526
|
+
operationId,
|
|
2527
|
+
context: requiresBleSelection ? {
|
|
2528
|
+
kind: "bind-connection",
|
|
2529
|
+
transport: "ble",
|
|
2530
|
+
reason: targetConnectId ? "known-connection-unavailable" : "missing-binding"
|
|
2531
|
+
} : { kind: "select-device", transport: "usb", reason: "multiple-candidates" },
|
|
2532
|
+
extra: context?.extra
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
const { sdkConnectId } = await (signal ? this._abortable(signal, waitPromise) : waitPromise);
|
|
2536
|
+
if (signal) _LedgerAdapter._throwIfAborted(signal);
|
|
2537
|
+
const selected = devices.find((device) => device.connectId === sdkConnectId);
|
|
2538
|
+
if (!selected) {
|
|
2539
|
+
throw Object.assign(new Error("Selected Ledger is no longer available"), {
|
|
2540
|
+
code: HardwareErrorCode3.DeviceNotFound
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
if (context && requiresBleSelection)
|
|
2544
|
+
context.selectedConnection = { connectId: selected.connectId, requestId };
|
|
2545
|
+
return this._connectDeviceOrThrow(selected.connectId, preserveOperationId, signal);
|
|
1719
2546
|
}
|
|
1720
2547
|
if (devices.length !== 1) {
|
|
1721
2548
|
throw Object.assign(new Error("Ledger device not found."), {
|
|
1722
2549
|
code: HardwareErrorCode3.DeviceNotFound
|
|
1723
2550
|
});
|
|
1724
2551
|
}
|
|
1725
|
-
return this._connectDeviceOrThrow(devices[0].connectId);
|
|
2552
|
+
return this._connectDeviceOrThrow(devices[0].connectId, preserveOperationId, signal);
|
|
1726
2553
|
}
|
|
1727
|
-
async _connectDeviceOrThrow(chosenConnectId) {
|
|
1728
|
-
const result = await this.
|
|
2554
|
+
async _connectDeviceOrThrow(chosenConnectId, preserveOperationId, signal) {
|
|
2555
|
+
const result = await this._connectTarget(chosenConnectId, preserveOperationId, signal);
|
|
1729
2556
|
if (!result.success) {
|
|
1730
2557
|
const payload = result.payload;
|
|
1731
2558
|
const rethrow = Object.assign(new Error(payload.error), {
|
|
@@ -1736,7 +2563,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1736
2563
|
}
|
|
1737
2564
|
throw rethrow;
|
|
1738
2565
|
}
|
|
1739
|
-
return
|
|
2566
|
+
return result.payload;
|
|
1740
2567
|
}
|
|
1741
2568
|
/**
|
|
1742
2569
|
* Call the connector with automatic session resolution and disconnect retry.
|
|
@@ -1766,26 +2593,122 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1766
2593
|
* lives in one place.
|
|
1767
2594
|
*/
|
|
1768
2595
|
async _callConnector(sessionId, method, params, signal) {
|
|
1769
|
-
|
|
2596
|
+
this._assertConnectorReady(method);
|
|
2597
|
+
if (signal?.aborted) throw this._abortReason(signal);
|
|
2598
|
+
const releaseOperation = this._retainConnectorOperation(`call:${sessionId}`);
|
|
2599
|
+
let promise;
|
|
2600
|
+
try {
|
|
2601
|
+
promise = this.connector.call(sessionId, method, params).finally(releaseOperation);
|
|
2602
|
+
} catch (error) {
|
|
2603
|
+
releaseOperation();
|
|
2604
|
+
throw error;
|
|
2605
|
+
}
|
|
1770
2606
|
const result = signal ? await this._abortable(signal, promise) : await promise;
|
|
1771
2607
|
return this._unwrapConnectorResult(result);
|
|
1772
2608
|
}
|
|
2609
|
+
_assertConnectorReady(method) {
|
|
2610
|
+
if (this._resetPromise || this._pendingConnectorTeardowns > 0 || this._unsettledConnectorOperations.size > 0) {
|
|
2611
|
+
throw _LedgerAdapter._createDeviceBusyError(method);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
_runConnectorTeardown(task) {
|
|
2615
|
+
const previous = this._connectorTeardownTail;
|
|
2616
|
+
let releaseTail = () => void 0;
|
|
2617
|
+
this._connectorTeardownTail = new Promise((resolve) => {
|
|
2618
|
+
releaseTail = resolve;
|
|
2619
|
+
});
|
|
2620
|
+
this._pendingConnectorTeardowns += 1;
|
|
2621
|
+
return (async () => {
|
|
2622
|
+
try {
|
|
2623
|
+
await previous;
|
|
2624
|
+
await this._waitForConnectorOperationsToDrain();
|
|
2625
|
+
await task();
|
|
2626
|
+
} finally {
|
|
2627
|
+
this._pendingConnectorTeardowns -= 1;
|
|
2628
|
+
releaseTail();
|
|
2629
|
+
}
|
|
2630
|
+
})();
|
|
2631
|
+
}
|
|
2632
|
+
_waitForConnectorOperationsToDrain() {
|
|
2633
|
+
if (this._unsettledConnectorOperations.size === 0) {
|
|
2634
|
+
return Promise.resolve();
|
|
2635
|
+
}
|
|
2636
|
+
return new Promise((resolve) => {
|
|
2637
|
+
this._connectorIdleWaiters.add(resolve);
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
_retainConnectorOperation(key) {
|
|
2641
|
+
this._unsettledConnectorOperations.set(
|
|
2642
|
+
key,
|
|
2643
|
+
(this._unsettledConnectorOperations.get(key) ?? 0) + 1
|
|
2644
|
+
);
|
|
2645
|
+
let released = false;
|
|
2646
|
+
return () => {
|
|
2647
|
+
if (released) return;
|
|
2648
|
+
released = true;
|
|
2649
|
+
const remaining = (this._unsettledConnectorOperations.get(key) ?? 1) - 1;
|
|
2650
|
+
if (remaining > 0) {
|
|
2651
|
+
this._unsettledConnectorOperations.set(key, remaining);
|
|
2652
|
+
} else {
|
|
2653
|
+
this._unsettledConnectorOperations.delete(key);
|
|
2654
|
+
}
|
|
2655
|
+
if (this._unsettledConnectorOperations.size === 0) {
|
|
2656
|
+
for (const resolve of this._connectorIdleWaiters) resolve();
|
|
2657
|
+
this._connectorIdleWaiters.clear();
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
1773
2661
|
async connectorCall(connectId, method, params, fingerprint, permissionDeviceId, commonParams, installContext) {
|
|
1774
|
-
|
|
1775
|
-
|
|
2662
|
+
const positionalOperationId = isHardwareOperationId(connectId) ? connectId : void 0;
|
|
2663
|
+
if (positionalOperationId && commonParams?.operationId && positionalOperationId !== commonParams.operationId) {
|
|
2664
|
+
throw createHwkError({
|
|
2665
|
+
code: HardwareErrorCode3.InvalidParams,
|
|
2666
|
+
message: "Conflicting Ledger operation ids",
|
|
2667
|
+
params: {
|
|
2668
|
+
positionalOperationId,
|
|
2669
|
+
commonOperationId: commonParams.operationId
|
|
2670
|
+
}
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
const operationId = commonParams?.operationId ?? positionalOperationId;
|
|
2674
|
+
const operation = operationId ? this._operations.resolve(operationId) : void 0;
|
|
2675
|
+
const releaseOperationRetention = operationId ? this._operations.retain(operationId) : void 0;
|
|
2676
|
+
const effectiveConnectId = operation?.connectId ?? connectId;
|
|
2677
|
+
debugLog("[LedgerAdapter][REQ]", {
|
|
2678
|
+
method,
|
|
2679
|
+
connectId: effectiveConnectId || "(empty)",
|
|
2680
|
+
params
|
|
2681
|
+
});
|
|
2682
|
+
const queueKey = ledgerQueueKey({ operationId, connectId: effectiveConnectId });
|
|
1776
2683
|
try {
|
|
1777
2684
|
const result = await this._jobQueue.enqueue(
|
|
1778
2685
|
queueKey,
|
|
1779
|
-
async (signal) =>
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2686
|
+
async (signal) => {
|
|
2687
|
+
if (operationId) this._activeOperationJobs.add(operationId);
|
|
2688
|
+
try {
|
|
2689
|
+
return await this._runConnectorCall(
|
|
2690
|
+
effectiveConnectId,
|
|
2691
|
+
method,
|
|
2692
|
+
params,
|
|
2693
|
+
signal,
|
|
2694
|
+
fingerprint,
|
|
2695
|
+
permissionDeviceId,
|
|
2696
|
+
commonParams,
|
|
2697
|
+
installContext ?? {},
|
|
2698
|
+
operationId
|
|
2699
|
+
);
|
|
2700
|
+
} catch (error) {
|
|
2701
|
+
this._finishBleBinding(signal.aborted ? "cancelled" : "failed");
|
|
2702
|
+
throw error;
|
|
2703
|
+
} finally {
|
|
2704
|
+
if (operationId) {
|
|
2705
|
+
this._activeOperationJobs.delete(operationId);
|
|
2706
|
+
if (this._pendingOperationDisconnects.delete(operationId)) {
|
|
2707
|
+
await this._releaseLostOperationConnection(operationId);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
},
|
|
1789
2712
|
{
|
|
1790
2713
|
label: method,
|
|
1791
2714
|
rejectIfBusy: true,
|
|
@@ -1806,15 +2729,22 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1806
2729
|
}
|
|
1807
2730
|
});
|
|
1808
2731
|
throw err;
|
|
2732
|
+
} finally {
|
|
2733
|
+
releaseOperationRetention?.();
|
|
1809
2734
|
}
|
|
1810
2735
|
}
|
|
1811
2736
|
/**
|
|
1812
2737
|
* Race a promise against an abort signal. On abort, rejects with
|
|
1813
2738
|
* signal.reason → instance _lastCancelReason → generic Error('Aborted').
|
|
1814
2739
|
*/
|
|
2740
|
+
/** Hermes/RN polyfills don't always populate signal.reason; fall back. */
|
|
2741
|
+
_abortReason(signal) {
|
|
2742
|
+
return signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
|
|
2743
|
+
}
|
|
1815
2744
|
_abortable(signal, promise) {
|
|
1816
|
-
const getAbortReason = () =>
|
|
2745
|
+
const getAbortReason = () => this._abortReason(signal);
|
|
1817
2746
|
if (signal.aborted) {
|
|
2747
|
+
void promise.catch(() => void 0);
|
|
1818
2748
|
return Promise.reject(getAbortReason());
|
|
1819
2749
|
}
|
|
1820
2750
|
return new Promise((resolve, reject) => {
|
|
@@ -1841,13 +2771,15 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1841
2771
|
}
|
|
1842
2772
|
}
|
|
1843
2773
|
/** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
|
|
1844
|
-
async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId, commonParams, installContext) {
|
|
2774
|
+
async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId, commonParams, installContext, operationId, lockedRetryBudget = _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET) {
|
|
1845
2775
|
_LedgerAdapter._throwIfAborted(signal);
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2776
|
+
if (!operationId) {
|
|
2777
|
+
await this._ensureDevicePermission(
|
|
2778
|
+
connectId,
|
|
2779
|
+
permissionDeviceId ?? fingerprint?.deviceId,
|
|
2780
|
+
signal
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
1851
2783
|
_LedgerAdapter._throwIfAborted(signal);
|
|
1852
2784
|
let effectiveParams = params;
|
|
1853
2785
|
if (method === "btcGetPublicKey") {
|
|
@@ -1861,36 +2793,135 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1861
2793
|
effectiveParams = gatedParams;
|
|
1862
2794
|
}
|
|
1863
2795
|
const allowUsbEphemeralFallback = !!fingerprint?.deviceId && !fingerprint.skipFingerprint;
|
|
1864
|
-
|
|
1865
|
-
|
|
2796
|
+
let businessCallStarted = false;
|
|
2797
|
+
const verifiedBleTarget = connectId ? this._verifiedBleReconnectTargets.get(connectId) : void 0;
|
|
2798
|
+
const knownTransport = this._isBleConnection() ? "ble" : "usb";
|
|
2799
|
+
const hintedConnectId = commonParams?.knownConnections?.find(
|
|
2800
|
+
(connection) => connection.transport === knownTransport
|
|
2801
|
+
);
|
|
2802
|
+
const inputConnectId = hintedConnectId && hintedConnectId.transport !== "qr" ? hintedConnectId.connectId : connectId;
|
|
2803
|
+
const preferredConnectId = fingerprint && !fingerprint.skipFingerprint && verifiedBleTarget?.chain === fingerprint.chain && verifiedBleTarget.fingerprint === fingerprint.deviceId ? verifiedBleTarget.connectId : inputConnectId;
|
|
2804
|
+
const connectionAttempt = { ...commonParams };
|
|
2805
|
+
const bundleConnection = installContext?.connection;
|
|
2806
|
+
if (bundleConnection && this._sessions.get(bundleConnection.connectId) !== bundleConnection.sessionId) {
|
|
2807
|
+
throw createHwkError({
|
|
2808
|
+
code: HardwareErrorCode3.DeviceDisconnected,
|
|
2809
|
+
message: "Ledger all-network connection ended"
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
let resolvedConnectId = operationId ? this._operations.resolve(operationId).connectId : bundleConnection?.connectId ?? await this.ensureConnected(
|
|
2813
|
+
preferredConnectId,
|
|
1866
2814
|
signal,
|
|
1867
|
-
allowUsbEphemeralFallback
|
|
2815
|
+
allowUsbEphemeralFallback,
|
|
2816
|
+
void 0,
|
|
2817
|
+
connectionAttempt
|
|
1868
2818
|
);
|
|
1869
|
-
|
|
2819
|
+
let sessionId = this._sessions.get(resolvedConnectId);
|
|
2820
|
+
if (sessionId && installContext && !installContext.connection) {
|
|
2821
|
+
installContext.connection = { connectId: resolvedConnectId, sessionId };
|
|
2822
|
+
}
|
|
1870
2823
|
if (!sessionId) {
|
|
2824
|
+
if (operationId) {
|
|
2825
|
+
this._operations.end(operationId, "disconnect");
|
|
2826
|
+
throw createHwkError({
|
|
2827
|
+
code: HardwareErrorCode3.OperationEnded,
|
|
2828
|
+
message: "Ledger operation connection is no longer active",
|
|
2829
|
+
params: { operationId, reason: "disconnect" }
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
1871
2832
|
throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
|
|
1872
2833
|
_tag: ERROR_TAG.DeviceSessionNotFound
|
|
1873
2834
|
});
|
|
1874
2835
|
}
|
|
1875
2836
|
try {
|
|
1876
2837
|
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
2838
|
+
for (; ; ) {
|
|
2839
|
+
const fp = await this._abortable(
|
|
2840
|
+
signal,
|
|
2841
|
+
this._verifyDeviceFingerprintWithSession(
|
|
2842
|
+
sessionId,
|
|
2843
|
+
fingerprint.deviceId,
|
|
2844
|
+
fingerprint.chain
|
|
2845
|
+
)
|
|
2846
|
+
);
|
|
2847
|
+
if (fp.success) break;
|
|
2848
|
+
const binding = operationId ? this._pendingOperationBindings.get(operationId) : connectionAttempt;
|
|
2849
|
+
if (!this._isBleConnection() || binding?.selectedConnection?.connectId !== resolvedConnectId) {
|
|
2850
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
2851
|
+
code: HardwareErrorCode3.DeviceMismatch
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
binding.rejectedConnectIds ?? (binding.rejectedConnectIds = /* @__PURE__ */ new Set());
|
|
2855
|
+
binding.rejectedConnectIds.add(resolvedConnectId);
|
|
2856
|
+
binding.rejectedConnectId = resolvedConnectId;
|
|
2857
|
+
this._sessions.delete(resolvedConnectId);
|
|
2858
|
+
await this.connector.disconnect(sessionId);
|
|
2859
|
+
if (operationId) this._pendingOperationDisconnects.delete(operationId);
|
|
2860
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
2861
|
+
resolvedConnectId = await this._connectFirstOrSelect(
|
|
2862
|
+
[],
|
|
2863
|
+
void 0,
|
|
2864
|
+
true,
|
|
2865
|
+
operationId,
|
|
2866
|
+
binding,
|
|
2867
|
+
signal
|
|
2868
|
+
);
|
|
2869
|
+
const selectedSession = this._sessions.get(resolvedConnectId);
|
|
2870
|
+
const selectedDevice = this._discoveredDevices.get(resolvedConnectId);
|
|
2871
|
+
if (!selectedSession || !selectedDevice)
|
|
2872
|
+
throw createHwkError({
|
|
2873
|
+
code: HardwareErrorCode3.DeviceDisconnected,
|
|
2874
|
+
message: "Selected Ledger connection ended"
|
|
2875
|
+
});
|
|
2876
|
+
sessionId = selectedSession;
|
|
2877
|
+
if (operationId)
|
|
2878
|
+
this._operations.rebind(operationId, {
|
|
2879
|
+
connectId: resolvedConnectId,
|
|
2880
|
+
device: selectedDevice,
|
|
2881
|
+
// Same source as `_createOperation`: the selected transport, not
|
|
2882
|
+
// the device snapshot a session connect overwrote.
|
|
2883
|
+
connectionType: this._isBleConnection() ? "ble" : "usb",
|
|
2884
|
+
connectionKeys: [sessionId]
|
|
2885
|
+
});
|
|
2886
|
+
if (installContext)
|
|
2887
|
+
installContext.connection = { connectId: resolvedConnectId, sessionId };
|
|
2888
|
+
}
|
|
2889
|
+
await this._publishVerifiedBleBinding(
|
|
2890
|
+
resolvedConnectId,
|
|
2891
|
+
fingerprint.chain,
|
|
2892
|
+
fingerprint.deviceId,
|
|
2893
|
+
connectionAttempt,
|
|
2894
|
+
operationId,
|
|
2895
|
+
signal
|
|
1884
2896
|
);
|
|
1885
|
-
if (!
|
|
1886
|
-
|
|
1887
|
-
|
|
2897
|
+
if (!operationId && connectionAttempt.selectedConnection?.connectId === resolvedConnectId && this._isBleConnection()) {
|
|
2898
|
+
this._verifiedBleReconnectTargets.set(connectId, {
|
|
2899
|
+
connectId: resolvedConnectId,
|
|
2900
|
+
chain: fingerprint.chain,
|
|
2901
|
+
fingerprint: fingerprint.deviceId
|
|
1888
2902
|
});
|
|
1889
2903
|
}
|
|
1890
2904
|
}
|
|
2905
|
+
businessCallStarted = true;
|
|
1891
2906
|
return await this._callConnector(sessionId, method, effectiveParams, signal);
|
|
1892
2907
|
} catch (err) {
|
|
1893
2908
|
if (signal.aborted) throw err;
|
|
2909
|
+
if (isDeviceDisconnectedError(err) || isDeviceNotAdvertisingError(err) || isTimeoutError(err) || isConnectionLevelError(err)) {
|
|
2910
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
2911
|
+
if (operationId) await this._releaseLostOperationConnection(operationId);
|
|
2912
|
+
else {
|
|
2913
|
+
this._sessions.delete(resolvedConnectId);
|
|
2914
|
+
await this.connector.disconnect(sessionId).catch(() => void 0);
|
|
2915
|
+
}
|
|
2916
|
+
const ambiguous = businessCallStarted && !canReplayHardwareMethodAfterTransportFailure(method);
|
|
2917
|
+
const interactionParams = operationId ? { operationId, reason: "disconnect" } : void 0;
|
|
2918
|
+
throw createHwkError({
|
|
2919
|
+
code: operationId ? HardwareErrorCode3.OperationEnded : mapLedgerError(err).code,
|
|
2920
|
+
message: "Ledger operation connection was lost; start a new operation",
|
|
2921
|
+
params: ambiguous ? operationMayHaveCompletedParams(method, { operationId }) : interactionParams,
|
|
2922
|
+
recovery: ambiguous ? { scope: "unknown" } : void 0
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
1894
2925
|
const errObj = err;
|
|
1895
2926
|
debugLog("[LedgerAdapter] connectorCall error:", method, {
|
|
1896
2927
|
message: errObj?.message,
|
|
@@ -1902,6 +2933,65 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1902
2933
|
isNotAdvertising: isDeviceNotAdvertisingError(err),
|
|
1903
2934
|
isStuckApp: isStuckAppStateError(err)
|
|
1904
2935
|
});
|
|
2936
|
+
if ((isDeviceLockedError(err) || errObj?.code === HardwareErrorCode3.DeviceLocked) && lockedRetryBudget > 0) {
|
|
2937
|
+
await this._waitForDeviceConnect(signal);
|
|
2938
|
+
if (this._sessions.get(resolvedConnectId) !== sessionId) {
|
|
2939
|
+
throw createHwkError({
|
|
2940
|
+
code: operationId ? HardwareErrorCode3.OperationEnded : HardwareErrorCode3.DeviceDisconnected,
|
|
2941
|
+
message: "Ledger connection ended while waiting for unlock"
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
return this._runConnectorCall(
|
|
2945
|
+
resolvedConnectId,
|
|
2946
|
+
method,
|
|
2947
|
+
effectiveParams,
|
|
2948
|
+
signal,
|
|
2949
|
+
fingerprint,
|
|
2950
|
+
permissionDeviceId,
|
|
2951
|
+
commonParams,
|
|
2952
|
+
installContext,
|
|
2953
|
+
operationId,
|
|
2954
|
+
lockedRetryBudget - 1
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
if (businessCallStarted && isStuckAppStateError(err)) {
|
|
2958
|
+
await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
|
|
2959
|
+
if (this._sessions.get(resolvedConnectId) !== sessionId) {
|
|
2960
|
+
throw createHwkError({
|
|
2961
|
+
code: operationId ? HardwareErrorCode3.OperationEnded : HardwareErrorCode3.DeviceDisconnected,
|
|
2962
|
+
message: "Ledger connection ended during the app transition"
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
try {
|
|
2966
|
+
return await this._callConnector(sessionId, method, effectiveParams, signal);
|
|
2967
|
+
} catch (retryErr) {
|
|
2968
|
+
if (isStuckAppStateError(retryErr)) throw err;
|
|
2969
|
+
if (isDeviceDisconnectedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isTimeoutError(retryErr) || isConnectionLevelError(retryErr)) {
|
|
2970
|
+
if (operationId) await this._releaseLostOperationConnection(operationId);
|
|
2971
|
+
else {
|
|
2972
|
+
this._sessions.delete(resolvedConnectId);
|
|
2973
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
2974
|
+
await this.connector.disconnect(sessionId).catch(() => void 0);
|
|
2975
|
+
}
|
|
2976
|
+
throw createHwkError({
|
|
2977
|
+
code: operationId ? HardwareErrorCode3.OperationEnded : mapLedgerError(retryErr).code,
|
|
2978
|
+
message: `Ledger ${method} may have completed before the connection was lost`,
|
|
2979
|
+
params: !canReplayHardwareMethodAfterTransportFailure(method) ? operationMayHaveCompletedParams(method, {
|
|
2980
|
+
operationId,
|
|
2981
|
+
reason: "disconnect"
|
|
2982
|
+
}) : { operationId, reason: "disconnect" },
|
|
2983
|
+
recovery: !canReplayHardwareMethodAfterTransportFailure(method) ? { scope: "unknown" } : void 0
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
throw retryErr;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
if (!operationId && err?.code === HardwareErrorCode3.DeviceMismatch) {
|
|
2990
|
+
this._sessions.delete(resolvedConnectId);
|
|
2991
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
2992
|
+
await this.connector.disconnect(sessionId).catch(() => void 0);
|
|
2993
|
+
throw err;
|
|
2994
|
+
}
|
|
1905
2995
|
const autoInstallApp = commonParams?.autoInstallApp ?? this._defaultAutoInstallApp;
|
|
1906
2996
|
const isAppMissing = isAppNotInstalledError(err) || err?.code === HardwareErrorCode3.AppNotInstalled;
|
|
1907
2997
|
if (autoInstallApp && isAppMissing) {
|
|
@@ -1957,157 +3047,14 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
1957
3047
|
fingerprint,
|
|
1958
3048
|
permissionDeviceId,
|
|
1959
3049
|
commonParams,
|
|
1960
|
-
installContext
|
|
1961
|
-
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
if (isStuckAppStateError(err)) {
|
|
1965
|
-
try {
|
|
1966
|
-
this._sessions.delete(resolvedConnectId);
|
|
1967
|
-
this._discoveredDevices.delete(resolvedConnectId);
|
|
1968
|
-
this.connector.reset?.();
|
|
1969
|
-
} catch {
|
|
1970
|
-
}
|
|
1971
|
-
debugLog(
|
|
1972
|
-
"[LedgerAdapter] stuck-app retry: method=",
|
|
1973
|
-
method,
|
|
1974
|
-
"delayMs=",
|
|
1975
|
-
_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
|
|
1976
|
-
"_tag=",
|
|
1977
|
-
err?._tag
|
|
1978
|
-
);
|
|
1979
|
-
try {
|
|
1980
|
-
const retryResult = await this._retryAfterStuckApp(
|
|
1981
|
-
resolvedConnectId,
|
|
1982
|
-
method,
|
|
1983
|
-
effectiveParams,
|
|
1984
|
-
signal,
|
|
1985
|
-
err,
|
|
1986
|
-
fingerprint
|
|
1987
|
-
);
|
|
1988
|
-
debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
|
|
1989
|
-
return retryResult;
|
|
1990
|
-
} catch (retryErr) {
|
|
1991
|
-
if (signal.aborted) throw retryErr;
|
|
1992
|
-
if (isStuckAppStateError(retryErr)) {
|
|
1993
|
-
debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
|
|
1994
|
-
throw err;
|
|
1995
|
-
}
|
|
1996
|
-
debugLog(
|
|
1997
|
-
"[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
|
|
1998
|
-
method,
|
|
1999
|
-
"retryErrTag=",
|
|
2000
|
-
retryErr?._tag
|
|
3050
|
+
installContext,
|
|
3051
|
+
operationId
|
|
2001
3052
|
);
|
|
2002
|
-
throw retryErr;
|
|
2003
3053
|
}
|
|
2004
3054
|
}
|
|
2005
|
-
if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
|
|
2006
|
-
let lastErr = err;
|
|
2007
|
-
for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
|
|
2008
|
-
if (signal.aborted) throw lastErr;
|
|
2009
|
-
try {
|
|
2010
|
-
this._sessions.delete(resolvedConnectId);
|
|
2011
|
-
this._discoveredDevices.delete(resolvedConnectId);
|
|
2012
|
-
if (isDeviceDisconnectedError(lastErr)) {
|
|
2013
|
-
try {
|
|
2014
|
-
this.connector.reset?.();
|
|
2015
|
-
} catch {
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
const reConnectId = await this.ensureConnected(
|
|
2019
|
-
resolvedConnectId,
|
|
2020
|
-
signal,
|
|
2021
|
-
allowUsbEphemeralFallback
|
|
2022
|
-
);
|
|
2023
|
-
const reSessionId = this._sessions.get(reConnectId);
|
|
2024
|
-
if (!reSessionId) throw lastErr;
|
|
2025
|
-
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
2026
|
-
const fp = await this._abortable(
|
|
2027
|
-
signal,
|
|
2028
|
-
this._verifyDeviceFingerprintWithSession(
|
|
2029
|
-
reSessionId,
|
|
2030
|
-
fingerprint.deviceId,
|
|
2031
|
-
fingerprint.chain
|
|
2032
|
-
)
|
|
2033
|
-
);
|
|
2034
|
-
if (!fp.success) {
|
|
2035
|
-
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
2036
|
-
code: HardwareErrorCode3.DeviceMismatch
|
|
2037
|
-
});
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
return await this._callConnector(reSessionId, method, effectiveParams, signal);
|
|
2041
|
-
} catch (retryErr) {
|
|
2042
|
-
if (signal.aborted) throw retryErr;
|
|
2043
|
-
lastErr = retryErr;
|
|
2044
|
-
const canRetry = attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET - 1 && (isDeviceLockedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isDeviceDisconnectedError(retryErr));
|
|
2045
|
-
if (!canRetry) {
|
|
2046
|
-
throw retryErr;
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
throw lastErr;
|
|
2051
|
-
}
|
|
2052
|
-
if (isTimeoutError(err)) {
|
|
2053
|
-
debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
|
|
2054
|
-
this._discoveredDevices.delete(resolvedConnectId);
|
|
2055
|
-
return this._retryWithFreshConnection(
|
|
2056
|
-
resolvedConnectId,
|
|
2057
|
-
method,
|
|
2058
|
-
effectiveParams,
|
|
2059
|
-
signal,
|
|
2060
|
-
err,
|
|
2061
|
-
fingerprint
|
|
2062
|
-
);
|
|
2063
|
-
}
|
|
2064
|
-
if (isConnectionLevelError(err)) {
|
|
2065
|
-
debugLog("[LedgerAdapter] connection-level fail-closed reset");
|
|
2066
|
-
this._sessions.delete(resolvedConnectId);
|
|
2067
|
-
this._discoveredDevices.delete(resolvedConnectId);
|
|
2068
|
-
this.connector.reset();
|
|
2069
|
-
const codeNum = err?.code;
|
|
2070
|
-
throw Object.assign(err, {
|
|
2071
|
-
code: codeNum ?? HardwareErrorCode3.DeviceDisconnected
|
|
2072
|
-
});
|
|
2073
|
-
}
|
|
2074
3055
|
throw err;
|
|
2075
3056
|
}
|
|
2076
3057
|
}
|
|
2077
|
-
/**
|
|
2078
|
-
* Stuck-app recovery: pause for the device's UI transition, then retry once.
|
|
2079
|
-
*
|
|
2080
|
-
* Caller has already cleared the session + reset connector. We wait so Stax
|
|
2081
|
-
* finishes its post-CloseApp animation, then go through ensureConnected +
|
|
2082
|
-
* fingerprint check + call exactly once. Caller decides what to do on a
|
|
2083
|
-
* second stuck-app hit.
|
|
2084
|
-
*/
|
|
2085
|
-
async _retryAfterStuckApp(resolvedConnectId, method, params, signal, originalErr, fingerprint) {
|
|
2086
|
-
await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
|
|
2087
|
-
const retryConnectId = await this.ensureConnected(
|
|
2088
|
-
resolvedConnectId,
|
|
2089
|
-
signal,
|
|
2090
|
-
!!fingerprint?.deviceId && !fingerprint.skipFingerprint
|
|
2091
|
-
);
|
|
2092
|
-
const retrySessionId = this._sessions.get(retryConnectId);
|
|
2093
|
-
if (!retrySessionId) throw originalErr;
|
|
2094
|
-
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
2095
|
-
const fp = await this._abortable(
|
|
2096
|
-
signal,
|
|
2097
|
-
this._verifyDeviceFingerprintWithSession(
|
|
2098
|
-
retrySessionId,
|
|
2099
|
-
fingerprint.deviceId,
|
|
2100
|
-
fingerprint.chain
|
|
2101
|
-
)
|
|
2102
|
-
);
|
|
2103
|
-
if (!fp.success) {
|
|
2104
|
-
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
2105
|
-
code: HardwareErrorCode3.DeviceMismatch
|
|
2106
|
-
});
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
return this._callConnector(retrySessionId, method, params, signal);
|
|
2110
|
-
}
|
|
2111
3058
|
_sleepAbortable(ms, signal) {
|
|
2112
3059
|
return new Promise((resolve, reject) => {
|
|
2113
3060
|
if (signal.aborted) {
|
|
@@ -2126,111 +3073,30 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
2126
3073
|
});
|
|
2127
3074
|
}
|
|
2128
3075
|
/**
|
|
2129
|
-
*
|
|
3076
|
+
* Ensure OS-level device permission (Bluetooth / USB) before proceeding.
|
|
3077
|
+
*
|
|
3078
|
+
* Emits `REQUEST_DEVICE_PERMISSION` and awaits the consumer's
|
|
3079
|
+
* `RECEIVE_DEVICE_PERMISSION` reply (60s budget covers "probe → system
|
|
3080
|
+
* prompt → user tap" plus a generous margin). If the consumer never wires
|
|
3081
|
+
* a handler or never replies, the wait times out and the operation fails
|
|
3082
|
+
* fast so scanners/callers don't hang silently.
|
|
2130
3083
|
*
|
|
2131
|
-
*
|
|
2132
|
-
*
|
|
2133
|
-
* on the same DMK can poison the next chain switch.
|
|
3084
|
+
* - No connectId (searchDevices): environment-level permission
|
|
3085
|
+
* - With connectId (business methods): device-level permission
|
|
2134
3086
|
*/
|
|
2135
|
-
async
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
this._discoveredDevices.clear();
|
|
2139
|
-
this._connectingPromise = null;
|
|
2140
|
-
const allowUsbEphemeralFallback = !!fingerprint?.deviceId && !fingerprint.skipFingerprint;
|
|
2141
|
-
const retryConnectId = await this.ensureConnected(
|
|
2142
|
-
targetConnectId,
|
|
2143
|
-
signal,
|
|
2144
|
-
allowUsbEphemeralFallback
|
|
2145
|
-
);
|
|
2146
|
-
const retrySessionId = this._sessions.get(retryConnectId);
|
|
2147
|
-
if (!retrySessionId) {
|
|
2148
|
-
throw originalErr;
|
|
2149
|
-
}
|
|
2150
|
-
try {
|
|
2151
|
-
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
2152
|
-
const fp = await this._abortable(
|
|
2153
|
-
signal,
|
|
2154
|
-
this._verifyDeviceFingerprintWithSession(
|
|
2155
|
-
retrySessionId,
|
|
2156
|
-
fingerprint.deviceId,
|
|
2157
|
-
fingerprint.chain
|
|
2158
|
-
)
|
|
2159
|
-
);
|
|
2160
|
-
if (!fp.success) {
|
|
2161
|
-
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
2162
|
-
code: HardwareErrorCode3.DeviceMismatch
|
|
2163
|
-
});
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
return await this._callConnector(retrySessionId, method, params, signal);
|
|
2167
|
-
} catch (retryErr) {
|
|
2168
|
-
if (signal.aborted) throw retryErr;
|
|
2169
|
-
this.connector.reset();
|
|
2170
|
-
this._sessions.clear();
|
|
2171
|
-
this._discoveredDevices.clear();
|
|
2172
|
-
this._connectingPromise = null;
|
|
2173
|
-
if (!isDeviceDisconnectedError(retryErr) && !isTimeoutError(retryErr)) {
|
|
2174
|
-
throw retryErr;
|
|
2175
|
-
}
|
|
2176
|
-
debugLog(
|
|
2177
|
-
"[LedgerAdapter] fresh-session retry still failed; resetting connector and rebuilding DMK"
|
|
2178
|
-
);
|
|
2179
|
-
this.connector.reset();
|
|
2180
|
-
this._sessions.clear();
|
|
2181
|
-
this._discoveredDevices.clear();
|
|
2182
|
-
this._connectingPromise = null;
|
|
2183
|
-
const finalConnectId = await this.ensureConnected(
|
|
2184
|
-
targetConnectId,
|
|
2185
|
-
signal,
|
|
2186
|
-
allowUsbEphemeralFallback
|
|
2187
|
-
);
|
|
2188
|
-
const finalSessionId = this._sessions.get(finalConnectId);
|
|
2189
|
-
if (!finalSessionId) {
|
|
2190
|
-
throw originalErr;
|
|
2191
|
-
}
|
|
2192
|
-
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
2193
|
-
const fp = await this._abortable(
|
|
2194
|
-
signal,
|
|
2195
|
-
this._verifyDeviceFingerprintWithSession(
|
|
2196
|
-
finalSessionId,
|
|
2197
|
-
fingerprint.deviceId,
|
|
2198
|
-
fingerprint.chain
|
|
2199
|
-
)
|
|
2200
|
-
);
|
|
2201
|
-
if (!fp.success) {
|
|
2202
|
-
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
2203
|
-
code: HardwareErrorCode3.DeviceMismatch
|
|
2204
|
-
});
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
return this._callConnector(finalSessionId, method, params, signal);
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
/**
|
|
2211
|
-
* Ensure OS-level device permission (Bluetooth / USB) before proceeding.
|
|
2212
|
-
*
|
|
2213
|
-
* Emits `REQUEST_DEVICE_PERMISSION` and awaits the consumer's
|
|
2214
|
-
* `RECEIVE_DEVICE_PERMISSION` reply (60s budget covers "probe → system
|
|
2215
|
-
* prompt → user tap" plus a generous margin). If the consumer never wires
|
|
2216
|
-
* a handler or never replies, the wait times out and the operation fails
|
|
2217
|
-
* fast so scanners/callers don't hang silently.
|
|
2218
|
-
*
|
|
2219
|
-
* - No connectId (searchDevices): environment-level permission
|
|
2220
|
-
* - With connectId (business methods): device-level permission
|
|
2221
|
-
*/
|
|
2222
|
-
async _ensureDevicePermission(connectId, deviceId, signal) {
|
|
2223
|
-
if (signal?.aborted) {
|
|
2224
|
-
_LedgerAdapter._throwIfAborted(signal);
|
|
3087
|
+
async _ensureDevicePermission(connectId, deviceId, signal) {
|
|
3088
|
+
if (signal?.aborted) {
|
|
3089
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
2225
3090
|
}
|
|
2226
3091
|
const transportType = this.activeTransport ?? "hid";
|
|
3092
|
+
const operationId = this._activeOperationId();
|
|
2227
3093
|
const waitPromise = this._uiRegistry.wait(
|
|
2228
3094
|
UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
2229
|
-
{ timeoutMs: 6e4 }
|
|
3095
|
+
{ timeoutMs: 6e4, operationId }
|
|
2230
3096
|
);
|
|
2231
3097
|
this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_PERMISSION, {
|
|
2232
3098
|
type: UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
2233
|
-
payload: { transportType, connectId, deviceId }
|
|
3099
|
+
payload: { transportType, connectId, deviceId, operationId }
|
|
2234
3100
|
});
|
|
2235
3101
|
let response;
|
|
2236
3102
|
const onAbort = () => {
|
|
@@ -2276,7 +3142,15 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
2276
3142
|
if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
|
|
2277
3143
|
const e = err;
|
|
2278
3144
|
const params = e.code === HardwareErrorCode3.DevicePermissionDenied && e.reason ? { permissionDeniedReason: e.reason } : e.params;
|
|
2279
|
-
return ledgerFailure(
|
|
3145
|
+
return ledgerFailure(
|
|
3146
|
+
e.code,
|
|
3147
|
+
e.message ?? "Unknown error",
|
|
3148
|
+
e.appName,
|
|
3149
|
+
tag,
|
|
3150
|
+
params,
|
|
3151
|
+
void 0,
|
|
3152
|
+
isHwkRecoveryHint(e.recovery) ? e.recovery : void 0
|
|
3153
|
+
);
|
|
2280
3154
|
}
|
|
2281
3155
|
const mapped = mapLedgerError(err);
|
|
2282
3156
|
return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
|
|
@@ -2303,7 +3177,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
2303
3177
|
deviceId: device.deviceId,
|
|
2304
3178
|
connectId: device.connectId,
|
|
2305
3179
|
label: device.name,
|
|
2306
|
-
connectionType: this.connector.connectionType,
|
|
3180
|
+
connectionType: device.connectionType ?? this.connector.connectionType,
|
|
2307
3181
|
rssi: device.rssi,
|
|
2308
3182
|
isConnectable: device.isConnectable,
|
|
2309
3183
|
serialNumber: device.serialNumber,
|
|
@@ -2329,8 +3203,8 @@ var LedgerAdapter = _LedgerAdapter;
|
|
|
2329
3203
|
|
|
2330
3204
|
// src/connector/LedgerConnectorBase.ts
|
|
2331
3205
|
import {
|
|
2332
|
-
EConnectorInteraction as
|
|
2333
|
-
HardwareErrorCode as
|
|
3206
|
+
EConnectorInteraction as EConnectorInteraction3,
|
|
3207
|
+
HardwareErrorCode as HardwareErrorCode7,
|
|
2334
3208
|
isKnownNonTargetHardwareVendor,
|
|
2335
3209
|
serializeConnectorError
|
|
2336
3210
|
} from "@onekeyfe/hwk-adapter-core";
|
|
@@ -2603,7 +3477,7 @@ var LedgerDeviceManager = class {
|
|
|
2603
3477
|
|
|
2604
3478
|
// src/signer/SignerManager.ts
|
|
2605
3479
|
import { SignerEthBuilder } from "@ledgerhq/device-signer-kit-ethereum";
|
|
2606
|
-
import { ContextModuleBuilder } from "@ledgerhq/context-module";
|
|
3480
|
+
import { ContextModuleBuilder, ContextModuleChainID } from "@ledgerhq/context-module";
|
|
2607
3481
|
|
|
2608
3482
|
// src/signer/SignerEth.ts
|
|
2609
3483
|
import { hexToBytes } from "@onekeyfe/hwk-adapter-core";
|
|
@@ -2869,7 +3743,7 @@ var SignerManager = class _SignerManager {
|
|
|
2869
3743
|
return (args) => new SignerEthBuilder(args);
|
|
2870
3744
|
}
|
|
2871
3745
|
static _createContextModule() {
|
|
2872
|
-
const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
|
|
3746
|
+
const contextModule = new ContextModuleBuilder({}).setChain(ContextModuleChainID.Ethereum).removeDefaultLoaders().build();
|
|
2873
3747
|
return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
|
|
2874
3748
|
}
|
|
2875
3749
|
static wrapBlindSigningReportNonBlocking(contextModule) {
|
|
@@ -3339,7 +4213,8 @@ function _applySignaturesToPsbt(psbtHex, signatures) {
|
|
|
3339
4213
|
}
|
|
3340
4214
|
|
|
3341
4215
|
// src/connector/chains/sol.ts
|
|
3342
|
-
import { bytesToHex, hexToBytes as hexToBytes3 } from "@onekeyfe/hwk-adapter-core";
|
|
4216
|
+
import { bytesToHex, hexToBytes as hexToBytes3, prepareSolanaOffchainMessageV1 } from "@onekeyfe/hwk-adapter-core";
|
|
4217
|
+
import bs58 from "bs58";
|
|
3343
4218
|
|
|
3344
4219
|
// src/signer/SignerSol.ts
|
|
3345
4220
|
var SignerSol = class {
|
|
@@ -3423,8 +4298,21 @@ async function solSignMessage(ctx, sessionId, params) {
|
|
|
3423
4298
|
const path = normalizePath(params.path);
|
|
3424
4299
|
const messageBytes = hexToBytes3(params.message);
|
|
3425
4300
|
try {
|
|
4301
|
+
if (params.messageVersion === 1) {
|
|
4302
|
+
const preparedMessage = prepareSolanaOffchainMessageV1({
|
|
4303
|
+
message: messageBytes,
|
|
4304
|
+
requiredSigners: params.requiredSigners
|
|
4305
|
+
});
|
|
4306
|
+
const { SignMessageVersion } = await ctx.importLedgerKit(
|
|
4307
|
+
"@ledgerhq/device-signer-kit-solana"
|
|
4308
|
+
);
|
|
4309
|
+
const result2 = await solSigner.signMessage(path, preparedMessage.serializedMessage, {
|
|
4310
|
+
version: SignMessageVersion.Raw
|
|
4311
|
+
});
|
|
4312
|
+
return { signature: decodeBase58Signature(result2.signature) };
|
|
4313
|
+
}
|
|
3426
4314
|
const result = await solSigner.signMessage(path, messageBytes);
|
|
3427
|
-
return { signature: result.signature };
|
|
4315
|
+
return { signature: decodeBase58EnvelopeSignature(result.signature) };
|
|
3428
4316
|
} catch (err) {
|
|
3429
4317
|
ctx.invalidateSession(sessionId);
|
|
3430
4318
|
throw ctx.wrapError(err);
|
|
@@ -3432,11 +4320,27 @@ async function solSignMessage(ctx, sessionId, params) {
|
|
|
3432
4320
|
ctx.clearCanceller(sessionId);
|
|
3433
4321
|
}
|
|
3434
4322
|
}
|
|
4323
|
+
function decodeBase58Signature(signature) {
|
|
4324
|
+
const bytes = bs58.decode(signature);
|
|
4325
|
+
if (bytes.length !== 64) {
|
|
4326
|
+
throw new Error(`Ledger Solana signature must be 64 bytes, received ${bytes.length}`);
|
|
4327
|
+
}
|
|
4328
|
+
return bytesToHex(bytes);
|
|
4329
|
+
}
|
|
4330
|
+
function decodeBase58EnvelopeSignature(envelope) {
|
|
4331
|
+
const bytes = bs58.decode(envelope);
|
|
4332
|
+
if (bytes.length < 65 || bytes[0] !== 1) {
|
|
4333
|
+
throw new Error("Ledger Solana signature envelope is invalid");
|
|
4334
|
+
}
|
|
4335
|
+
return bytesToHex(bytes.subarray(1, 65));
|
|
4336
|
+
}
|
|
3435
4337
|
async function _createSolSigner(ctx, sessionId) {
|
|
3436
4338
|
const dmk = await ctx.getOrCreateDmk();
|
|
3437
|
-
const { ContextModuleBuilder: ContextModuleBuilder2 } = await ctx.importLedgerKit(
|
|
4339
|
+
const { ContextModuleBuilder: ContextModuleBuilder2, ContextModuleChainID: ContextModuleChainID2 } = await ctx.importLedgerKit(
|
|
4340
|
+
"@ledgerhq/context-module"
|
|
4341
|
+
);
|
|
3438
4342
|
const { SignerSolanaBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-solana");
|
|
3439
|
-
const contextModule = new ContextModuleBuilder2({}).removeDefaultLoaders().build();
|
|
4343
|
+
const contextModule = new ContextModuleBuilder2({}).setChain(ContextModuleChainID2.Solana).removeDefaultLoaders().build();
|
|
3440
4344
|
const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
|
|
3441
4345
|
const signer = new SignerSol(sdkSigner);
|
|
3442
4346
|
signer.onInteraction = (interaction) => {
|
|
@@ -3452,361 +4356,192 @@ async function _createSolSigner(ctx, sessionId) {
|
|
|
3452
4356
|
return signer;
|
|
3453
4357
|
}
|
|
3454
4358
|
|
|
3455
|
-
// src/connector/chains/
|
|
3456
|
-
import {
|
|
3457
|
-
import Trx from "@ledgerhq/hw-app-trx";
|
|
3458
|
-
|
|
3459
|
-
// src/connector/chains/legacyChainCall.ts
|
|
3460
|
-
import { EConnectorInteraction as EConnectorInteraction3 } from "@onekeyfe/hwk-adapter-core";
|
|
4359
|
+
// src/connector/chains/zcash.ts
|
|
4360
|
+
import { bytesToHex as bytesToHex2 } from "@onekeyfe/hwk-adapter-core";
|
|
3461
4361
|
|
|
3462
|
-
// src/
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
isSuccessCommandResult
|
|
3468
|
-
} from "@ledgerhq/device-management-kit";
|
|
3469
|
-
import { HardwareErrorCode as HardwareErrorCode6 } from "@onekeyfe/hwk-adapter-core";
|
|
3470
|
-
var APP_NAME_MAP = {
|
|
3471
|
-
ETH: "Ethereum",
|
|
3472
|
-
BTC: "Bitcoin",
|
|
3473
|
-
SOL: "Solana",
|
|
3474
|
-
TRX: "Tron",
|
|
3475
|
-
XRP: "XRP",
|
|
3476
|
-
ADA: "Cardano",
|
|
3477
|
-
DOT: "Polkadot",
|
|
3478
|
-
ATOM: "Cosmos"
|
|
3479
|
-
};
|
|
3480
|
-
var DASHBOARD_APP_NAME = "BOLOS";
|
|
3481
|
-
var AppManager = class {
|
|
3482
|
-
constructor(dmk, options) {
|
|
3483
|
-
this._dmk = dmk;
|
|
3484
|
-
this._waitMs = options?.waitMs ?? 1e3;
|
|
3485
|
-
this._maxRetries = options?.maxRetries ?? 10;
|
|
3486
|
-
}
|
|
3487
|
-
/**
|
|
3488
|
-
* Return the Ledger app name for a given chain ticker,
|
|
3489
|
-
* or undefined if the chain is not supported.
|
|
3490
|
-
*/
|
|
3491
|
-
static getAppName(chain) {
|
|
3492
|
-
return APP_NAME_MAP[chain];
|
|
3493
|
-
}
|
|
3494
|
-
/**
|
|
3495
|
-
* Ensure the target app is open on the device identified by `sessionId`.
|
|
3496
|
-
*
|
|
3497
|
-
* Flow:
|
|
3498
|
-
* 1. Check the currently running app.
|
|
3499
|
-
* 2. If it is already the target, return immediately.
|
|
3500
|
-
* 3. If a different app is running (not dashboard), close it first.
|
|
3501
|
-
* 4. Open the target app.
|
|
3502
|
-
* 5. Poll until the device confirms the target app is running.
|
|
3503
|
-
*/
|
|
3504
|
-
/**
|
|
3505
|
-
* @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued — the
|
|
3506
|
-
* device is about to display "Open <app>" on screen and wait for the
|
|
3507
|
-
* user's button press. UI consumers should show their "open app" prompt
|
|
3508
|
-
* in response. NOT called when the target app is already open (no user
|
|
3509
|
-
* interaction needed in that case).
|
|
3510
|
-
*
|
|
3511
|
-
* Important: OpenAppCommand is blocking. It does not resolve until the user
|
|
3512
|
-
* has physically confirmed on the device, so anything that runs AFTER
|
|
3513
|
-
* `await this._openApp(...)` lands AFTER the prompt is already gone.
|
|
3514
|
-
* Hence the callback must fire BEFORE that await.
|
|
3515
|
-
*/
|
|
3516
|
-
async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
|
|
3517
|
-
const currentApp = await this._getCurrentApp(sessionId);
|
|
3518
|
-
if (currentApp === targetAppName) {
|
|
3519
|
-
return;
|
|
3520
|
-
}
|
|
3521
|
-
if (!this._isDashboard(currentApp)) {
|
|
3522
|
-
await this._closeCurrentApp(sessionId);
|
|
3523
|
-
await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
|
|
3524
|
-
}
|
|
3525
|
-
onConfirmOnDevice?.();
|
|
3526
|
-
await this._openApp(sessionId, targetAppName);
|
|
3527
|
-
await this._waitForApp(sessionId, targetAppName);
|
|
4362
|
+
// src/signer/SignerZcash.ts
|
|
4363
|
+
var SignerZcash = class {
|
|
4364
|
+
// eslint-disable-next-line no-useless-constructor, no-empty-function
|
|
4365
|
+
constructor(_sdk) {
|
|
4366
|
+
this._sdk = _sdk;
|
|
3528
4367
|
}
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
if (isSuccessCommandResult(result)) {
|
|
3538
|
-
debugLog("[AppManager] currentApp:", result.data.name);
|
|
3539
|
-
return result.data.name;
|
|
3540
|
-
}
|
|
3541
|
-
const errResult = result;
|
|
3542
|
-
const dmkErr = errResult.error ?? {};
|
|
3543
|
-
const original = dmkErr.originalError;
|
|
3544
|
-
debugLog(
|
|
3545
|
-
"[AppManager] _getCurrentApp failed sessionId=",
|
|
3546
|
-
sessionId,
|
|
3547
|
-
"tag=",
|
|
3548
|
-
dmkErr._tag,
|
|
3549
|
-
"errorCode=",
|
|
3550
|
-
dmkErr.errorCode,
|
|
3551
|
-
"message=",
|
|
3552
|
-
dmkErr.message,
|
|
3553
|
-
"originalErrorMessage=",
|
|
3554
|
-
original?.message ?? String(original ?? "")
|
|
3555
|
-
);
|
|
3556
|
-
throw Object.assign(
|
|
3557
|
-
new Error(
|
|
3558
|
-
dmkErr.message ?? "Failed to get current app from device"
|
|
3559
|
-
),
|
|
3560
|
-
{
|
|
3561
|
-
_tag: dmkErr._tag,
|
|
3562
|
-
errorCode: dmkErr.errorCode,
|
|
3563
|
-
originalError: original
|
|
3564
|
-
}
|
|
4368
|
+
/** GET_VK: UFVK string (default) or raw 96-byte Orchard FVK. */
|
|
4369
|
+
async getFullViewingKey(derivationPath, options) {
|
|
4370
|
+
const action = this._sdk.getFullViewingKey(derivationPath, options);
|
|
4371
|
+
return deviceActionToPromise(
|
|
4372
|
+
action,
|
|
4373
|
+
this.onInteraction,
|
|
4374
|
+
void 0,
|
|
4375
|
+
this.onRegisterCanceller
|
|
3565
4376
|
);
|
|
3566
4377
|
}
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
const message = "message" in dmkErr && typeof dmkErr.message === "string" ? dmkErr.message : "";
|
|
3576
|
-
debugLog(
|
|
3577
|
-
"[AppManager] openApp failed:",
|
|
3578
|
-
appName,
|
|
3579
|
-
"errorCode:",
|
|
3580
|
-
errorCode,
|
|
3581
|
-
"tag:",
|
|
3582
|
-
dmkErr._tag
|
|
3583
|
-
);
|
|
3584
|
-
let code;
|
|
3585
|
-
if (errorCode === "6807" || /unknown application/i.test(message)) {
|
|
3586
|
-
code = HardwareErrorCode6.AppNotInstalled;
|
|
3587
|
-
} else if (errorCode === "5501" || dmkErr._tag === "ActionRefusedError") {
|
|
3588
|
-
code = HardwareErrorCode6.UserRejected;
|
|
3589
|
-
}
|
|
3590
|
-
throw Object.assign(new Error(`Failed to open "${appName}"`), {
|
|
3591
|
-
_tag: ERROR_TAG.OpenAppCommand,
|
|
3592
|
-
code,
|
|
3593
|
-
errorCode,
|
|
3594
|
-
statusCode: errorCode,
|
|
3595
|
-
appName,
|
|
3596
|
-
originalError: dmkErr
|
|
3597
|
-
});
|
|
3598
|
-
}
|
|
3599
|
-
}
|
|
3600
|
-
async _closeCurrentApp(sessionId) {
|
|
3601
|
-
debugLog("[AppManager] closeCurrentApp");
|
|
3602
|
-
await this._dmk.sendCommand({
|
|
3603
|
-
sessionId,
|
|
3604
|
-
command: new CloseAppCommand()
|
|
3605
|
-
});
|
|
3606
|
-
}
|
|
3607
|
-
/**
|
|
3608
|
-
* Poll the device until the expected app is reported as running,
|
|
3609
|
-
* or throw after `_maxRetries` attempts.
|
|
3610
|
-
*/
|
|
3611
|
-
async _waitForApp(sessionId, expectedAppName) {
|
|
3612
|
-
let lastSeen = "";
|
|
3613
|
-
for (let i = 0; i < this._maxRetries; i++) {
|
|
3614
|
-
await this._wait();
|
|
3615
|
-
const current = await this._getCurrentApp(sessionId);
|
|
3616
|
-
lastSeen = current;
|
|
3617
|
-
if (current === expectedAppName) {
|
|
3618
|
-
return;
|
|
3619
|
-
}
|
|
3620
|
-
}
|
|
3621
|
-
debugLog(
|
|
3622
|
-
"[AppManager] waitForApp exhausted: expected=",
|
|
3623
|
-
expectedAppName,
|
|
3624
|
-
"lastSeen=",
|
|
3625
|
-
lastSeen
|
|
3626
|
-
);
|
|
3627
|
-
throw new Error(
|
|
3628
|
-
`Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
|
|
4378
|
+
/** GET_SHIELDED_ADDRESS: single-Orchard-receiver UA for a 5-level transparent path. */
|
|
4379
|
+
async getShieldedAddress(derivationPath, options) {
|
|
4380
|
+
const action = this._sdk.getShieldedAddress(derivationPath, options);
|
|
4381
|
+
return deviceActionToPromise(
|
|
4382
|
+
action,
|
|
4383
|
+
this.onInteraction,
|
|
4384
|
+
void 0,
|
|
4385
|
+
this.onRegisterCanceller
|
|
3629
4386
|
);
|
|
3630
4387
|
}
|
|
3631
|
-
_isDashboard(appName) {
|
|
3632
|
-
return appName === DASHBOARD_APP_NAME;
|
|
3633
|
-
}
|
|
3634
|
-
_wait() {
|
|
3635
|
-
return new Promise((resolve) => setTimeout(resolve, this._waitMs));
|
|
3636
|
-
}
|
|
3637
4388
|
};
|
|
3638
4389
|
|
|
3639
|
-
// src/connector/chains/
|
|
3640
|
-
function
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
const { appName, needsConfirmation } = options;
|
|
3645
|
-
let openAppPromptShown = false;
|
|
3646
|
-
const onAppOpenPrompt = () => {
|
|
3647
|
-
openAppPromptShown = true;
|
|
3648
|
-
ctx.emit("ui-event", {
|
|
3649
|
-
type: EConnectorInteraction3.ConfirmOpenApp,
|
|
3650
|
-
payload: { sessionId }
|
|
3651
|
-
});
|
|
3652
|
-
};
|
|
3653
|
-
const closeOpenAppUiIfShown = () => {
|
|
3654
|
-
if (openAppPromptShown) {
|
|
3655
|
-
ctx.emit("ui-event", {
|
|
3656
|
-
type: EConnectorInteraction3.InteractionComplete,
|
|
3657
|
-
payload: { sessionId }
|
|
3658
|
-
});
|
|
3659
|
-
openAppPromptShown = false;
|
|
3660
|
-
}
|
|
3661
|
-
};
|
|
4390
|
+
// src/connector/chains/zcash.ts
|
|
4391
|
+
async function zcashGetFullViewingKey(ctx, sessionId, params) {
|
|
4392
|
+
const signer = await _createZcashSigner(ctx, sessionId);
|
|
4393
|
+
const path = normalizePath(params.path);
|
|
4394
|
+
const mode = params.mode ?? "ufvk";
|
|
3662
4395
|
try {
|
|
3663
|
-
await
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
"[LegacyChainCall] pre-flight ensureAppOpen failed:",
|
|
3667
|
-
appName,
|
|
3668
|
-
err?.message
|
|
3669
|
-
);
|
|
3670
|
-
closeOpenAppUiIfShown();
|
|
3671
|
-
throw ctx.wrapError(err, { defaultAppName: appName });
|
|
3672
|
-
}
|
|
3673
|
-
const runOnce = async () => {
|
|
3674
|
-
let confirmEmitted = false;
|
|
3675
|
-
if (needsConfirmation) {
|
|
3676
|
-
ctx.emit("ui-event", {
|
|
3677
|
-
type: EConnectorInteraction3.ConfirmOnDevice,
|
|
3678
|
-
payload: { sessionId }
|
|
3679
|
-
});
|
|
3680
|
-
confirmEmitted = true;
|
|
3681
|
-
}
|
|
3682
|
-
try {
|
|
3683
|
-
return await action(sessionId);
|
|
3684
|
-
} finally {
|
|
3685
|
-
if (confirmEmitted || openAppPromptShown) {
|
|
3686
|
-
ctx.emit("ui-event", {
|
|
3687
|
-
type: EConnectorInteraction3.InteractionComplete,
|
|
3688
|
-
payload: { sessionId }
|
|
3689
|
-
});
|
|
3690
|
-
openAppPromptShown = false;
|
|
3691
|
-
}
|
|
4396
|
+
const result = await signer.getFullViewingKey(path, { mode });
|
|
4397
|
+
if (result.mode === "ufvk") {
|
|
4398
|
+
return { path: params.path, mode, ufvk: result.fullViewingKey };
|
|
3692
4399
|
}
|
|
3693
|
-
|
|
4400
|
+
return { path: params.path, mode, orchardFvk: bytesToHex2(result.fullViewingKey) };
|
|
4401
|
+
} catch (err) {
|
|
4402
|
+
ctx.invalidateSession(sessionId);
|
|
4403
|
+
throw ctx.wrapError(err);
|
|
4404
|
+
} finally {
|
|
4405
|
+
ctx.clearCanceller(sessionId);
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
async function zcashGetShieldedAddress(ctx, sessionId, params) {
|
|
4409
|
+
const signer = await _createZcashSigner(ctx, sessionId);
|
|
4410
|
+
const path = normalizePath(params.path);
|
|
3694
4411
|
try {
|
|
3695
|
-
|
|
4412
|
+
const result = await signer.getShieldedAddress(path, {
|
|
4413
|
+
checkOnDevice: params.showOnDevice ?? false
|
|
4414
|
+
});
|
|
4415
|
+
return { address: result.address, path: params.path };
|
|
3696
4416
|
} catch (err) {
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
}
|
|
3702
|
-
debugLog("[LegacyChainCall] wrong-app detected, retrying:", appName);
|
|
3703
|
-
try {
|
|
3704
|
-
await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
|
|
3705
|
-
} catch (switchErr) {
|
|
3706
|
-
debugLog(
|
|
3707
|
-
"[LegacyChainCall] retry ensureAppOpen failed:",
|
|
3708
|
-
appName,
|
|
3709
|
-
switchErr?.message
|
|
3710
|
-
);
|
|
3711
|
-
closeOpenAppUiIfShown();
|
|
3712
|
-
throw ctx.wrapError(switchErr, { defaultAppName: appName });
|
|
3713
|
-
}
|
|
3714
|
-
ctx.clearAllSigners();
|
|
3715
|
-
const result = await runOnce();
|
|
3716
|
-
debugLog("[LegacyChainCall] retry succeeded:", appName);
|
|
3717
|
-
return result;
|
|
4417
|
+
ctx.invalidateSession(sessionId);
|
|
4418
|
+
throw ctx.wrapError(err);
|
|
4419
|
+
} finally {
|
|
4420
|
+
ctx.clearCanceller(sessionId);
|
|
3718
4421
|
}
|
|
3719
4422
|
}
|
|
3720
|
-
async function
|
|
4423
|
+
async function _createZcashSigner(ctx, sessionId) {
|
|
3721
4424
|
const dmk = await ctx.getOrCreateDmk();
|
|
3722
|
-
const
|
|
3723
|
-
|
|
4425
|
+
const { SignerZcashBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-zcash");
|
|
4426
|
+
const sdkSigner = new SignerZcashBuilder({ dmk, sessionId }).build();
|
|
4427
|
+
const signer = new SignerZcash(sdkSigner);
|
|
4428
|
+
signer.onInteraction = (interaction) => {
|
|
4429
|
+
debugLog("[LedgerConnector] zcash.onInteraction:", interaction);
|
|
4430
|
+
ctx.emit("ui-event", {
|
|
4431
|
+
type: collapseSignerInteraction(interaction),
|
|
4432
|
+
payload: { sessionId }
|
|
4433
|
+
});
|
|
4434
|
+
};
|
|
4435
|
+
signer.onRegisterCanceller = (cancel) => ctx.registerCanceller(sessionId, cancel);
|
|
4436
|
+
return signer;
|
|
3724
4437
|
}
|
|
3725
4438
|
|
|
3726
|
-
// src/
|
|
3727
|
-
import
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
4439
|
+
// src/connector/chains/tron.ts
|
|
4440
|
+
import { HardwareErrorCode as HardwareErrorCode6, bytesToHex as bytesToHex3, hexToBytes as hexToBytes4 } from "@onekeyfe/hwk-adapter-core";
|
|
4441
|
+
|
|
4442
|
+
// src/signer/SignerTron.ts
|
|
4443
|
+
var SignerTron = class {
|
|
4444
|
+
// eslint-disable-next-line no-useless-constructor, no-empty-function
|
|
4445
|
+
constructor(_sdk) {
|
|
4446
|
+
this._sdk = _sdk;
|
|
3733
4447
|
}
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
}
|
|
3744
|
-
result.set(statusCode, data.length);
|
|
3745
|
-
return result;
|
|
4448
|
+
/** Base58 Tron address plus the uncompressed public key at `derivationPath`. */
|
|
4449
|
+
async getAddress(derivationPath, options) {
|
|
4450
|
+
const action = this._sdk.getAddress(derivationPath, options);
|
|
4451
|
+
const result = await deviceActionToPromise(
|
|
4452
|
+
action,
|
|
4453
|
+
this.onInteraction,
|
|
4454
|
+
void 0,
|
|
4455
|
+
this.onRegisterCanceller
|
|
4456
|
+
);
|
|
4457
|
+
return { address: result.address, publicKey: result.publicKey };
|
|
3746
4458
|
}
|
|
3747
|
-
|
|
4459
|
+
/** Sign a protobuf-encoded raw transaction. */
|
|
4460
|
+
async signTransaction(derivationPath, transaction, options) {
|
|
4461
|
+
const action = this._sdk.signTransaction(derivationPath, transaction, options);
|
|
4462
|
+
return deviceActionToPromise(
|
|
4463
|
+
action,
|
|
4464
|
+
this.onInteraction,
|
|
4465
|
+
void 0,
|
|
4466
|
+
this.onRegisterCanceller
|
|
4467
|
+
);
|
|
4468
|
+
}
|
|
4469
|
+
/** Sign a personal message (TIP-191). */
|
|
4470
|
+
async signPersonalMessage(derivationPath, message, options) {
|
|
4471
|
+
const action = this._sdk.signPersonalMessage(derivationPath, message, options);
|
|
4472
|
+
return deviceActionToPromise(
|
|
4473
|
+
action,
|
|
4474
|
+
this.onInteraction,
|
|
4475
|
+
void 0,
|
|
4476
|
+
this.onRegisterCanceller
|
|
4477
|
+
);
|
|
3748
4478
|
}
|
|
3749
4479
|
};
|
|
3750
4480
|
|
|
3751
4481
|
// src/connector/chains/tron.ts
|
|
3752
4482
|
async function tronGetAddress(ctx, sessionId, params) {
|
|
4483
|
+
const tronSigner = await _createTronSigner(ctx, sessionId);
|
|
3753
4484
|
const path = normalizePath(params.path);
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
{
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
const trx = await _createTrx(ctx, sid);
|
|
3766
|
-
const result = await trx.getAddress(path, showOnDevice);
|
|
3767
|
-
return { address: result.address, publicKey: result.publicKey, path: params.path };
|
|
3768
|
-
}
|
|
3769
|
-
);
|
|
4485
|
+
try {
|
|
4486
|
+
const result = await tronSigner.getAddress(path, {
|
|
4487
|
+
checkOnDevice: params.showOnDevice ?? false
|
|
4488
|
+
});
|
|
4489
|
+
return { address: result.address, publicKey: result.publicKey, path: params.path };
|
|
4490
|
+
} catch (err) {
|
|
4491
|
+
ctx.invalidateSession(sessionId);
|
|
4492
|
+
throw ctx.wrapError(err);
|
|
4493
|
+
} finally {
|
|
4494
|
+
ctx.clearCanceller(sessionId);
|
|
4495
|
+
}
|
|
3770
4496
|
}
|
|
3771
4497
|
async function tronSignTransaction(ctx, sessionId, params) {
|
|
3772
4498
|
if (!params.rawTxHex) {
|
|
3773
4499
|
throw Object.assign(
|
|
3774
4500
|
new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
|
|
3775
|
-
{ code:
|
|
4501
|
+
{ code: HardwareErrorCode6.InvalidParams }
|
|
3776
4502
|
);
|
|
3777
4503
|
}
|
|
4504
|
+
const tronSigner = await _createTronSigner(ctx, sessionId);
|
|
3778
4505
|
const path = normalizePath(params.path);
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
params.tokenSignatures ?? []
|
|
3789
|
-
);
|
|
3790
|
-
return { signature };
|
|
3791
|
-
}
|
|
3792
|
-
);
|
|
4506
|
+
try {
|
|
4507
|
+
const signature = await tronSigner.signTransaction(path, hexToBytes4(params.rawTxHex));
|
|
4508
|
+
return { signature: bytesToHex3(signature) };
|
|
4509
|
+
} catch (err) {
|
|
4510
|
+
ctx.invalidateSession(sessionId);
|
|
4511
|
+
throw ctx.wrapError(err);
|
|
4512
|
+
} finally {
|
|
4513
|
+
ctx.clearCanceller(sessionId);
|
|
4514
|
+
}
|
|
3793
4515
|
}
|
|
3794
4516
|
async function tronSignMessage(ctx, sessionId, params) {
|
|
4517
|
+
const tronSigner = await _createTronSigner(ctx, sessionId);
|
|
3795
4518
|
const path = normalizePath(params.path);
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
);
|
|
4519
|
+
try {
|
|
4520
|
+
const signature = await tronSigner.signPersonalMessage(path, hexToBytes4(params.messageHex));
|
|
4521
|
+
return { signature: bytesToHex3(signature) };
|
|
4522
|
+
} catch (err) {
|
|
4523
|
+
ctx.invalidateSession(sessionId);
|
|
4524
|
+
throw ctx.wrapError(err);
|
|
4525
|
+
} finally {
|
|
4526
|
+
ctx.clearCanceller(sessionId);
|
|
4527
|
+
}
|
|
3806
4528
|
}
|
|
3807
|
-
async function
|
|
4529
|
+
async function _createTronSigner(ctx, sessionId) {
|
|
3808
4530
|
const dmk = await ctx.getOrCreateDmk();
|
|
3809
|
-
|
|
4531
|
+
const { SignerTrxBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-tron");
|
|
4532
|
+
const sdkSigner = new SignerTrxBuilder({ dmk, sessionId }).build();
|
|
4533
|
+
const signer = new SignerTron(sdkSigner);
|
|
4534
|
+
signer.onInteraction = (interaction) => {
|
|
4535
|
+
debugLog("[LedgerConnector] tron.onInteraction:", interaction);
|
|
4536
|
+
ctx.emit("ui-event", {
|
|
4537
|
+
type: collapseSignerInteraction(interaction),
|
|
4538
|
+
payload: { sessionId }
|
|
4539
|
+
});
|
|
4540
|
+
};
|
|
4541
|
+
signer.onRegisterCanceller = (cancel) => {
|
|
4542
|
+
ctx.registerCanceller(sessionId, cancel);
|
|
4543
|
+
};
|
|
4544
|
+
return signer;
|
|
3810
4545
|
}
|
|
3811
4546
|
|
|
3812
4547
|
// src/device-apps/customActions.ts
|
|
@@ -3985,7 +4720,7 @@ var DeviceApps = class {
|
|
|
3985
4720
|
seTargetId: v.seTargetId,
|
|
3986
4721
|
mcuTargetId: v.mcuTargetId,
|
|
3987
4722
|
seVersion: v.seVersion,
|
|
3988
|
-
seFlagsHex:
|
|
4723
|
+
seFlagsHex: bytesToHex4(v.seFlags),
|
|
3989
4724
|
mcuSephVersion: v.mcuSephVersion,
|
|
3990
4725
|
mcuBootloaderVersion: v.mcuBootloaderVersion,
|
|
3991
4726
|
hwVersion: v.hwVersion
|
|
@@ -4059,7 +4794,7 @@ var DeviceApps = class {
|
|
|
4059
4794
|
}
|
|
4060
4795
|
}
|
|
4061
4796
|
};
|
|
4062
|
-
function
|
|
4797
|
+
function bytesToHex4(bytes) {
|
|
4063
4798
|
if (!bytes) return "";
|
|
4064
4799
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
4065
4800
|
}
|
|
@@ -4102,10 +4837,11 @@ var METHOD_PREFIX_TO_APP_NAME = {
|
|
|
4102
4837
|
evm: "Ethereum",
|
|
4103
4838
|
btc: "Bitcoin",
|
|
4104
4839
|
sol: "Solana",
|
|
4105
|
-
tron: "Tron"
|
|
4840
|
+
tron: "Tron",
|
|
4841
|
+
zcash: "Zcash"
|
|
4106
4842
|
};
|
|
4107
4843
|
var HARDWARE_ERROR_CODE_VALUES = new Set(
|
|
4108
|
-
Object.values(
|
|
4844
|
+
Object.values(HardwareErrorCode7).filter((value) => typeof value === "number")
|
|
4109
4845
|
);
|
|
4110
4846
|
var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
4111
4847
|
var LEDGER_RELAY_ALLOWED_ROOT_DOMAINS = ["onekeytest.com", "onekey.com"];
|
|
@@ -4142,6 +4878,10 @@ async function defaultLedgerKitImporter(pkg) {
|
|
|
4142
4878
|
return import("@ledgerhq/device-signer-kit-bitcoin");
|
|
4143
4879
|
case "@ledgerhq/device-signer-kit-solana":
|
|
4144
4880
|
return import("@ledgerhq/device-signer-kit-solana");
|
|
4881
|
+
case "@ledgerhq/device-signer-kit-tron":
|
|
4882
|
+
return import("@ledgerhq/device-signer-kit-tron");
|
|
4883
|
+
case "@ledgerhq/device-signer-kit-zcash":
|
|
4884
|
+
return import("@ledgerhq/device-signer-kit-zcash");
|
|
4145
4885
|
case "@ledgerhq/context-module":
|
|
4146
4886
|
return import("@ledgerhq/context-module");
|
|
4147
4887
|
default:
|
|
@@ -4201,6 +4941,7 @@ var LedgerConnectorBase = class {
|
|
|
4201
4941
|
this._ctx = {
|
|
4202
4942
|
emit: (event, data) => this._emit(event, data),
|
|
4203
4943
|
invalidateSession: (sid) => this._invalidateSession(sid),
|
|
4944
|
+
teardownSecureChannelSession: (sid) => this._teardownSecureChannelSession(sid),
|
|
4204
4945
|
wrapError: (err, opts) => this._wrapError(err, opts),
|
|
4205
4946
|
getOrCreateDmk: () => this._getOrCreateDmk(),
|
|
4206
4947
|
getDeviceManager: () => this._getDeviceManager(),
|
|
@@ -4300,7 +5041,7 @@ var LedgerConnectorBase = class {
|
|
|
4300
5041
|
`Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
|
|
4301
5042
|
);
|
|
4302
5043
|
err._tag = ERROR_TAG.BlePairingTimeout;
|
|
4303
|
-
err.code =
|
|
5044
|
+
err.code = HardwareErrorCode7.BlePairingTimeout;
|
|
4304
5045
|
reject(err);
|
|
4305
5046
|
}, HANG_CEILING_MS);
|
|
4306
5047
|
});
|
|
@@ -4333,7 +5074,7 @@ var LedgerConnectorBase = class {
|
|
|
4333
5074
|
"Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
|
|
4334
5075
|
);
|
|
4335
5076
|
err._tag = ERROR_TAG.DeviceNotAdvertising;
|
|
4336
|
-
err.code =
|
|
5077
|
+
err.code = HardwareErrorCode7.DeviceNotFound;
|
|
4337
5078
|
throw err;
|
|
4338
5079
|
};
|
|
4339
5080
|
const doConnect = async (path) => {
|
|
@@ -4390,14 +5131,14 @@ var LedgerConnectorBase = class {
|
|
|
4390
5131
|
this._resetSignersAndSessions();
|
|
4391
5132
|
if (isLedgerBleConnectionType(this.connectionType)) {
|
|
4392
5133
|
const tag = err?._tag;
|
|
4393
|
-
if (isKnownConnectionTag(tag)) {
|
|
5134
|
+
if (isKnownConnectionTag(tag) && !isConnectionOpeningTag(tag)) {
|
|
4394
5135
|
throw err;
|
|
4395
5136
|
}
|
|
4396
5137
|
const wrapped = new Error(
|
|
4397
5138
|
"Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
|
|
4398
5139
|
);
|
|
4399
5140
|
wrapped._tag = ERROR_TAG.BleGattBondingFailed;
|
|
4400
|
-
wrapped.code =
|
|
5141
|
+
wrapped.code = HardwareErrorCode7.BlePairingTimeout;
|
|
4401
5142
|
wrapped.originalError = err;
|
|
4402
5143
|
throw wrapped;
|
|
4403
5144
|
}
|
|
@@ -4475,7 +5216,7 @@ var LedgerConnectorBase = class {
|
|
|
4475
5216
|
this._unwatchSessionState(sessionId);
|
|
4476
5217
|
this._signerManager?.invalidate(sessionId);
|
|
4477
5218
|
this._cancellers.get(sessionId)?.({
|
|
4478
|
-
code:
|
|
5219
|
+
code: HardwareErrorCode7.DeviceDisconnected,
|
|
4479
5220
|
tag: "DeviceDisconnected",
|
|
4480
5221
|
message: "Device disconnected"
|
|
4481
5222
|
});
|
|
@@ -4501,7 +5242,7 @@ var LedgerConnectorBase = class {
|
|
|
4501
5242
|
success: false,
|
|
4502
5243
|
error: serializeConnectorError(
|
|
4503
5244
|
Object.assign(new Error("Ledger app is unresponsive"), {
|
|
4504
|
-
code:
|
|
5245
|
+
code: HardwareErrorCode7.DeviceAppStuck,
|
|
4505
5246
|
_tag: ERROR_TAG.DeviceAppStuck,
|
|
4506
5247
|
originalError: err
|
|
4507
5248
|
})
|
|
@@ -4513,7 +5254,7 @@ var LedgerConnectorBase = class {
|
|
|
4513
5254
|
success: false,
|
|
4514
5255
|
error: serializeConnectorError(
|
|
4515
5256
|
Object.assign(new Error("Device communication interrupted, please retry"), {
|
|
4516
|
-
code:
|
|
5257
|
+
code: HardwareErrorCode7.TransportError,
|
|
4517
5258
|
_tag: ERROR_TAG.DeviceTransportStuck,
|
|
4518
5259
|
originalError: err
|
|
4519
5260
|
})
|
|
@@ -4559,6 +5300,11 @@ var LedgerConnectorBase = class {
|
|
|
4559
5300
|
return solSignTransaction(ctx, sessionId, params);
|
|
4560
5301
|
case "solSignMessage":
|
|
4561
5302
|
return solSignMessage(ctx, sessionId, params);
|
|
5303
|
+
// ZCASH
|
|
5304
|
+
case "zcashGetFullViewingKey":
|
|
5305
|
+
return zcashGetFullViewingKey(ctx, sessionId, params);
|
|
5306
|
+
case "zcashGetShieldedAddress":
|
|
5307
|
+
return zcashGetShieldedAddress(ctx, sessionId, params);
|
|
4562
5308
|
// TRON
|
|
4563
5309
|
case "tronGetAddress":
|
|
4564
5310
|
return tronGetAddress(ctx, sessionId, params);
|
|
@@ -4583,7 +5329,7 @@ var LedgerConnectorBase = class {
|
|
|
4583
5329
|
try {
|
|
4584
5330
|
return await apps.install(p.appName, ({ progress }) => {
|
|
4585
5331
|
ctx.emit("ui-event", {
|
|
4586
|
-
type:
|
|
5332
|
+
type: EConnectorInteraction3.AppInstallProgress,
|
|
4587
5333
|
payload: {
|
|
4588
5334
|
sessionId,
|
|
4589
5335
|
appName: p.appName,
|
|
@@ -4592,7 +5338,7 @@ var LedgerConnectorBase = class {
|
|
|
4592
5338
|
});
|
|
4593
5339
|
});
|
|
4594
5340
|
} catch (err) {
|
|
4595
|
-
ctx.
|
|
5341
|
+
ctx.teardownSecureChannelSession(sessionId);
|
|
4596
5342
|
throw ctx.wrapError(err);
|
|
4597
5343
|
} finally {
|
|
4598
5344
|
ctx.clearCanceller(sessionId);
|
|
@@ -4682,7 +5428,7 @@ var LedgerConnectorBase = class {
|
|
|
4682
5428
|
);
|
|
4683
5429
|
return { isGenuine: output.isGenuine, deviceId };
|
|
4684
5430
|
} catch (err) {
|
|
4685
|
-
ctx.
|
|
5431
|
+
ctx.teardownSecureChannelSession(sessionId);
|
|
4686
5432
|
throw ctx.wrapError(err);
|
|
4687
5433
|
} finally {
|
|
4688
5434
|
ctx.clearCanceller(sessionId);
|
|
@@ -4801,9 +5547,34 @@ var LedgerConnectorBase = class {
|
|
|
4801
5547
|
}
|
|
4802
5548
|
return this._deviceAppsManager;
|
|
4803
5549
|
}
|
|
5550
|
+
// DeviceAppsManager is intentionally absent here: it is a per-call factory
|
|
5551
|
+
// with no cached session state, so there is nothing to invalidate. Cancelling
|
|
5552
|
+
// the device action is what closes the secure channel.
|
|
4804
5553
|
_invalidateSession(sessionId) {
|
|
4805
5554
|
this._signerManager?.invalidate(sessionId);
|
|
4806
5555
|
}
|
|
5556
|
+
/**
|
|
5557
|
+
* Teardown for the OS-level actions that open a manager-api secure channel
|
|
5558
|
+
* (install, uninstall, genuine check). Cancelling the device action is what
|
|
5559
|
+
* stops DMK's xstate actor, which unsubscribes the secure-channel observable
|
|
5560
|
+
* and closes its WebSocket; dropping the canceller without firing it leaves
|
|
5561
|
+
* that teardown to run only if the action settled on its own.
|
|
5562
|
+
*
|
|
5563
|
+
* The DMK device session is deliberately kept: the secure channel is a
|
|
5564
|
+
* separate WebSocket per call, and disconnecting here would break the bounded
|
|
5565
|
+
* unlock/retry recovery the adapter runs on the original session.
|
|
5566
|
+
*/
|
|
5567
|
+
_teardownSecureChannelSession(sessionId) {
|
|
5568
|
+
const cancel = this._cancellers.get(sessionId);
|
|
5569
|
+
this._cancellers.delete(sessionId);
|
|
5570
|
+
if (cancel) {
|
|
5571
|
+
try {
|
|
5572
|
+
cancel();
|
|
5573
|
+
} catch {
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
this._invalidateSession(sessionId);
|
|
5577
|
+
}
|
|
4807
5578
|
/**
|
|
4808
5579
|
* Replace an old session with a new one after app switch.
|
|
4809
5580
|
* Emits device-connect so the adapter updates its _sessions Map.
|
|
@@ -4888,7 +5659,7 @@ var LedgerConnectorBase = class {
|
|
|
4888
5659
|
* at every catch site. Falls through unchanged for unknown methods.
|
|
4889
5660
|
*/
|
|
4890
5661
|
_ctxForMethod(method) {
|
|
4891
|
-
const prefix = /^(evm|btc|sol|tron)/.exec(method)?.[1];
|
|
5662
|
+
const prefix = /^(evm|btc|sol|tron|zcash)/.exec(method)?.[1];
|
|
4892
5663
|
const defaultAppName = prefix ? METHOD_PREFIX_TO_APP_NAME[prefix] : void 0;
|
|
4893
5664
|
if (!defaultAppName) return this._ctx;
|
|
4894
5665
|
return {
|
|
@@ -4922,6 +5693,209 @@ var LedgerConnectorBase = class {
|
|
|
4922
5693
|
return error;
|
|
4923
5694
|
}
|
|
4924
5695
|
};
|
|
5696
|
+
|
|
5697
|
+
// src/transport/DmkTransport.ts
|
|
5698
|
+
import Transport from "@ledgerhq/hw-transport";
|
|
5699
|
+
var DmkTransport = class extends Transport {
|
|
5700
|
+
constructor(dmk, sessionId) {
|
|
5701
|
+
super();
|
|
5702
|
+
this._dmk = dmk;
|
|
5703
|
+
this._sessionId = sessionId;
|
|
5704
|
+
}
|
|
5705
|
+
async exchange(apdu) {
|
|
5706
|
+
const response = await this._dmk.sendApdu({
|
|
5707
|
+
sessionId: this._sessionId,
|
|
5708
|
+
apdu: new Uint8Array(apdu)
|
|
5709
|
+
});
|
|
5710
|
+
const { data, statusCode } = response;
|
|
5711
|
+
const result = Buffer.alloc(data.length + 2);
|
|
5712
|
+
if (data.length > 0) {
|
|
5713
|
+
result.set(data, 0);
|
|
5714
|
+
}
|
|
5715
|
+
result.set(statusCode, data.length);
|
|
5716
|
+
return result;
|
|
5717
|
+
}
|
|
5718
|
+
async close() {
|
|
5719
|
+
}
|
|
5720
|
+
};
|
|
5721
|
+
|
|
5722
|
+
// src/app/AppManager.ts
|
|
5723
|
+
import {
|
|
5724
|
+
CloseAppCommand,
|
|
5725
|
+
GetAppAndVersionCommand,
|
|
5726
|
+
OpenAppCommand,
|
|
5727
|
+
isSuccessCommandResult
|
|
5728
|
+
} from "@ledgerhq/device-management-kit";
|
|
5729
|
+
import { HardwareErrorCode as HardwareErrorCode8 } from "@onekeyfe/hwk-adapter-core";
|
|
5730
|
+
var APP_NAME_MAP = {
|
|
5731
|
+
ETH: "Ethereum",
|
|
5732
|
+
BTC: "Bitcoin",
|
|
5733
|
+
SOL: "Solana",
|
|
5734
|
+
TRX: "Tron",
|
|
5735
|
+
ZEC: "Zcash",
|
|
5736
|
+
XRP: "XRP",
|
|
5737
|
+
ADA: "Cardano",
|
|
5738
|
+
DOT: "Polkadot",
|
|
5739
|
+
ATOM: "Cosmos"
|
|
5740
|
+
};
|
|
5741
|
+
var DASHBOARD_APP_NAME = "BOLOS";
|
|
5742
|
+
var AppManager = class {
|
|
5743
|
+
constructor(dmk, options) {
|
|
5744
|
+
this._dmk = dmk;
|
|
5745
|
+
this._waitMs = options?.waitMs ?? 1e3;
|
|
5746
|
+
this._maxRetries = options?.maxRetries ?? 10;
|
|
5747
|
+
}
|
|
5748
|
+
/**
|
|
5749
|
+
* Return the Ledger app name for a given chain ticker,
|
|
5750
|
+
* or undefined if the chain is not supported.
|
|
5751
|
+
*/
|
|
5752
|
+
static getAppName(chain) {
|
|
5753
|
+
return APP_NAME_MAP[chain];
|
|
5754
|
+
}
|
|
5755
|
+
/**
|
|
5756
|
+
* Ensure the target app is open on the device identified by `sessionId`.
|
|
5757
|
+
*
|
|
5758
|
+
* Flow:
|
|
5759
|
+
* 1. Check the currently running app.
|
|
5760
|
+
* 2. If it is already the target, return immediately.
|
|
5761
|
+
* 3. If a different app is running (not dashboard), close it first.
|
|
5762
|
+
* 4. Open the target app.
|
|
5763
|
+
* 5. Poll until the device confirms the target app is running.
|
|
5764
|
+
*/
|
|
5765
|
+
/**
|
|
5766
|
+
* @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued — the
|
|
5767
|
+
* device is about to display "Open <app>" on screen and wait for the
|
|
5768
|
+
* user's button press. UI consumers should show their "open app" prompt
|
|
5769
|
+
* in response. NOT called when the target app is already open (no user
|
|
5770
|
+
* interaction needed in that case).
|
|
5771
|
+
*
|
|
5772
|
+
* Important: OpenAppCommand is blocking. It does not resolve until the user
|
|
5773
|
+
* has physically confirmed on the device, so anything that runs AFTER
|
|
5774
|
+
* `await this._openApp(...)` lands AFTER the prompt is already gone.
|
|
5775
|
+
* Hence the callback must fire BEFORE that await.
|
|
5776
|
+
*/
|
|
5777
|
+
async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
|
|
5778
|
+
const currentApp = await this._getCurrentApp(sessionId);
|
|
5779
|
+
if (currentApp === targetAppName) {
|
|
5780
|
+
return;
|
|
5781
|
+
}
|
|
5782
|
+
if (!this._isDashboard(currentApp)) {
|
|
5783
|
+
await this._closeCurrentApp(sessionId);
|
|
5784
|
+
await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
|
|
5785
|
+
}
|
|
5786
|
+
onConfirmOnDevice?.();
|
|
5787
|
+
await this._openApp(sessionId, targetAppName);
|
|
5788
|
+
await this._waitForApp(sessionId, targetAppName);
|
|
5789
|
+
}
|
|
5790
|
+
// ---------------------------------------------------------------------------
|
|
5791
|
+
// Private helpers
|
|
5792
|
+
// ---------------------------------------------------------------------------
|
|
5793
|
+
async _getCurrentApp(sessionId) {
|
|
5794
|
+
const result = await this._dmk.sendCommand({
|
|
5795
|
+
sessionId,
|
|
5796
|
+
command: new GetAppAndVersionCommand()
|
|
5797
|
+
});
|
|
5798
|
+
if (isSuccessCommandResult(result)) {
|
|
5799
|
+
debugLog("[AppManager] currentApp:", result.data.name);
|
|
5800
|
+
return result.data.name;
|
|
5801
|
+
}
|
|
5802
|
+
const errResult = result;
|
|
5803
|
+
const dmkErr = errResult.error ?? {};
|
|
5804
|
+
const original = dmkErr.originalError;
|
|
5805
|
+
debugLog(
|
|
5806
|
+
"[AppManager] _getCurrentApp failed sessionId=",
|
|
5807
|
+
sessionId,
|
|
5808
|
+
"tag=",
|
|
5809
|
+
dmkErr._tag,
|
|
5810
|
+
"errorCode=",
|
|
5811
|
+
dmkErr.errorCode,
|
|
5812
|
+
"message=",
|
|
5813
|
+
dmkErr.message,
|
|
5814
|
+
"originalErrorMessage=",
|
|
5815
|
+
original?.message ?? String(original ?? "")
|
|
5816
|
+
);
|
|
5817
|
+
throw Object.assign(
|
|
5818
|
+
new Error(
|
|
5819
|
+
dmkErr.message ?? "Failed to get current app from device"
|
|
5820
|
+
),
|
|
5821
|
+
{
|
|
5822
|
+
_tag: dmkErr._tag,
|
|
5823
|
+
errorCode: dmkErr.errorCode,
|
|
5824
|
+
originalError: original
|
|
5825
|
+
}
|
|
5826
|
+
);
|
|
5827
|
+
}
|
|
5828
|
+
async _openApp(sessionId, appName) {
|
|
5829
|
+
const result = await this._dmk.sendCommand({
|
|
5830
|
+
sessionId,
|
|
5831
|
+
command: new OpenAppCommand({ appName })
|
|
5832
|
+
});
|
|
5833
|
+
if (!isSuccessCommandResult(result)) {
|
|
5834
|
+
const dmkErr = result.error;
|
|
5835
|
+
const errorCode = "errorCode" in dmkErr && dmkErr.errorCode != null ? String(dmkErr.errorCode) : "";
|
|
5836
|
+
const message = "message" in dmkErr && typeof dmkErr.message === "string" ? dmkErr.message : "";
|
|
5837
|
+
debugLog(
|
|
5838
|
+
"[AppManager] openApp failed:",
|
|
5839
|
+
appName,
|
|
5840
|
+
"errorCode:",
|
|
5841
|
+
errorCode,
|
|
5842
|
+
"tag:",
|
|
5843
|
+
dmkErr._tag
|
|
5844
|
+
);
|
|
5845
|
+
let code;
|
|
5846
|
+
if (errorCode === "6807" || /unknown application/i.test(message)) {
|
|
5847
|
+
code = HardwareErrorCode8.AppNotInstalled;
|
|
5848
|
+
} else if (errorCode === "5501" || dmkErr._tag === "ActionRefusedError") {
|
|
5849
|
+
code = HardwareErrorCode8.UserRejected;
|
|
5850
|
+
}
|
|
5851
|
+
throw Object.assign(new Error(`Failed to open "${appName}"`), {
|
|
5852
|
+
_tag: ERROR_TAG.OpenAppCommand,
|
|
5853
|
+
code,
|
|
5854
|
+
errorCode,
|
|
5855
|
+
statusCode: errorCode,
|
|
5856
|
+
appName,
|
|
5857
|
+
originalError: dmkErr
|
|
5858
|
+
});
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
async _closeCurrentApp(sessionId) {
|
|
5862
|
+
debugLog("[AppManager] closeCurrentApp");
|
|
5863
|
+
await this._dmk.sendCommand({
|
|
5864
|
+
sessionId,
|
|
5865
|
+
command: new CloseAppCommand()
|
|
5866
|
+
});
|
|
5867
|
+
}
|
|
5868
|
+
/**
|
|
5869
|
+
* Poll the device until the expected app is reported as running,
|
|
5870
|
+
* or throw after `_maxRetries` attempts.
|
|
5871
|
+
*/
|
|
5872
|
+
async _waitForApp(sessionId, expectedAppName) {
|
|
5873
|
+
let lastSeen = "";
|
|
5874
|
+
for (let i = 0; i < this._maxRetries; i++) {
|
|
5875
|
+
await this._wait();
|
|
5876
|
+
const current = await this._getCurrentApp(sessionId);
|
|
5877
|
+
lastSeen = current;
|
|
5878
|
+
if (current === expectedAppName) {
|
|
5879
|
+
return;
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
debugLog(
|
|
5883
|
+
"[AppManager] waitForApp exhausted: expected=",
|
|
5884
|
+
expectedAppName,
|
|
5885
|
+
"lastSeen=",
|
|
5886
|
+
lastSeen
|
|
5887
|
+
);
|
|
5888
|
+
throw new Error(
|
|
5889
|
+
`Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
|
|
5890
|
+
);
|
|
5891
|
+
}
|
|
5892
|
+
_isDashboard(appName) {
|
|
5893
|
+
return appName === DASHBOARD_APP_NAME;
|
|
5894
|
+
}
|
|
5895
|
+
_wait() {
|
|
5896
|
+
return new Promise((resolve) => setTimeout(resolve, this._waitMs));
|
|
5897
|
+
}
|
|
5898
|
+
};
|
|
4925
5899
|
export {
|
|
4926
5900
|
AppManager,
|
|
4927
5901
|
DmkTransport,
|
|
@@ -4932,6 +5906,7 @@ export {
|
|
|
4932
5906
|
SignerEth,
|
|
4933
5907
|
SignerManager,
|
|
4934
5908
|
SignerSol,
|
|
5909
|
+
SignerZcash,
|
|
4935
5910
|
debugLog,
|
|
4936
5911
|
deviceActionToPromise,
|
|
4937
5912
|
isDeviceLockedError,
|