@onekeyfe/hwk-keystone-adapter 1.2.2-alpha.100 → 1.2.3-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -5,6 +5,7 @@ import bs58check from "bs58check";
5
5
  import * as bitcoin from "bitcoinjs-lib";
6
6
  import HDKey from "hdkey";
7
7
  import { parse as uuidParse, stringify as uuidStringify } from "uuid";
8
+ import { parseBip32MasterFingerprint } from "@onekeyfe/hwk-adapter-core";
8
9
 
9
10
  // src/urEngine/TronSignRequest.ts
10
11
  import {
@@ -31,13 +32,13 @@ var TronSignRequest = class _TronSignRequest extends RegistryItem {
31
32
  if (this.requestId) {
32
33
  map[1 /* requestId */] = new DataItem(this.requestId, RegistryTypes.UUID.getTag());
33
34
  }
34
- if (this.address) map[4 /* address */] = this.address;
35
- if (this.origin) map[5 /* origin */] = this.origin;
35
+ if (this.address) map[5 /* address */] = this.address;
36
+ if (this.origin) map[6 /* origin */] = this.origin;
36
37
  map[2 /* signData */] = this.signData;
37
- map[6 /* signType */] = this.signType;
38
+ map[3 /* dataType */] = this.signType;
38
39
  const keyPath = this.derivationPath.toDataItem();
39
40
  keyPath.setTag(this.derivationPath.getRegistryType().getTag());
40
- map[3 /* derivationPath */] = keyPath;
41
+ map[4 /* derivationPath */] = keyPath;
41
42
  return new DataItem(map);
42
43
  };
43
44
  this.requestId = args.requestId;
@@ -50,12 +51,12 @@ var TronSignRequest = class _TronSignRequest extends RegistryItem {
50
51
  static fromDataItem(dataItem) {
51
52
  const map = dataItem.getData();
52
53
  const signData = map[2 /* signData */];
53
- const derivationPath = CryptoKeypath.fromDataItem(map[3 /* derivationPath */]);
54
- const address = map[4 /* address */];
54
+ const derivationPath = CryptoKeypath.fromDataItem(map[4 /* derivationPath */]);
55
+ const address = map[5 /* address */];
55
56
  const requestIdItem = map[1 /* requestId */];
56
57
  const requestId = requestIdItem?.getData();
57
- const origin = map[5 /* origin */];
58
- const signType = map[6 /* signType */];
58
+ const origin = map[6 /* origin */];
59
+ const signType = map[3 /* dataType */];
59
60
  return new _TronSignRequest({ requestId, signData, derivationPath, address, origin, signType });
60
61
  }
61
62
  static fromCBOR(cborPayload) {
@@ -135,8 +136,20 @@ function fromSdkUr(ur) {
135
136
  return { urType: ur.type, urData: ur.cbor.toString("hex") };
136
137
  }
137
138
  function splitSignature65(hex) {
139
+ if (hex.length !== 130) {
140
+ throw new Error(
141
+ `Keystone returned a ${hex.length / 2}-byte signature; expected 65 bytes (r|s|v)`
142
+ );
143
+ }
138
144
  return { r: hex.slice(0, 64), s: hex.slice(64, 128), v: hex.slice(128) };
139
145
  }
146
+ function requireBip32MasterFingerprint(value) {
147
+ const fingerprint = parseBip32MasterFingerprint(value);
148
+ if (!fingerprint) {
149
+ throw new Error("Keystone master fingerprint must be exactly 4 bytes (8 hex characters)");
150
+ }
151
+ return fingerprint;
152
+ }
140
153
  var KeystoneUrEngine = class {
141
154
  constructor(origin = "OneKey") {
142
155
  this.sdk = new KeystoneSDK({ origin });
@@ -146,7 +159,7 @@ var KeystoneUrEngine = class {
146
159
  parseMultiAccounts(ur) {
147
160
  const parsed = this.sdk.parseMultiAccounts(toSdkUr(ur));
148
161
  return {
149
- masterFingerprint: parsed.masterFingerprint.toLowerCase(),
162
+ masterFingerprint: requireBip32MasterFingerprint(parsed.masterFingerprint),
150
163
  device: parsed.device,
151
164
  deviceId: parsed.deviceId,
152
165
  deviceVersion: parsed.deviceVersion,
@@ -156,7 +169,7 @@ var KeystoneUrEngine = class {
156
169
  path: key.path,
157
170
  publicKey: key.publicKey,
158
171
  extendedPublicKey: key.extendedPublicKey,
159
- xfp: key.xfp,
172
+ xfp: key.xfp ? requireBip32MasterFingerprint(key.xfp) : void 0,
160
173
  name: key.name
161
174
  })
162
175
  )
@@ -169,7 +182,7 @@ var KeystoneUrEngine = class {
169
182
  path: key.path,
170
183
  publicKey: key.publicKey,
171
184
  extendedPublicKey: key.extendedPublicKey,
172
- xfp: key.xfp,
185
+ xfp: key.xfp ? requireBip32MasterFingerprint(key.xfp) : void 0,
173
186
  name: key.name
174
187
  };
175
188
  }
@@ -218,7 +231,10 @@ var KeystoneUrEngine = class {
218
231
  if (!account.xfp) {
219
232
  throw new Error("Keystone crypto-hdkey response is missing its source fingerprint");
220
233
  }
221
- return { masterFingerprint: account.xfp.toLowerCase(), accounts: [account] };
234
+ return {
235
+ masterFingerprint: requireBip32MasterFingerprint(account.xfp),
236
+ accounts: [account]
237
+ };
222
238
  }
223
239
  return this.parseMultiAccounts(ur);
224
240
  }
@@ -270,12 +286,18 @@ var KeystoneUrEngine = class {
270
286
  deriveBtcAddressFromXpub(xpub, relativeDerivePath, scriptType) {
271
287
  const node = HDKey.fromExtendedKey(xpub).derive(`m/${relativeDerivePath.replace(/^m\//i, "")}`);
272
288
  if (!node.publicKey) throw new Error("HDKey derivation did not produce a public key");
273
- const pubkey = Buffer.from(node.publicKey);
289
+ return this.deriveBtcAddressFromPublicKey(
290
+ Buffer.from(node.publicKey).toString("hex"),
291
+ scriptType
292
+ );
293
+ }
294
+ deriveBtcAddressFromPublicKey(publicKeyHex, scriptType) {
295
+ const pubkey = Buffer.from(stripHexPrefix(publicKeyHex), "hex");
274
296
  const network = bitcoin.networks.bitcoin;
275
297
  switch (scriptType) {
276
298
  case "p2pkh": {
277
299
  const { address } = bitcoin.payments.p2pkh({ pubkey, network });
278
- if (!address) throw new Error("Failed to derive a P2PKH address from this xpub");
300
+ if (!address) throw new Error("Failed to derive a P2PKH address from this public key");
279
301
  return address;
280
302
  }
281
303
  case "p2sh-p2wpkh": {
@@ -283,12 +305,13 @@ var KeystoneUrEngine = class {
283
305
  redeem: bitcoin.payments.p2wpkh({ pubkey, network }),
284
306
  network
285
307
  });
286
- if (!address) throw new Error("Failed to derive a P2SH-P2WPKH address from this xpub");
308
+ if (!address)
309
+ throw new Error("Failed to derive a P2SH-P2WPKH address from this public key");
287
310
  return address;
288
311
  }
289
312
  case "p2wpkh": {
290
313
  const { address } = bitcoin.payments.p2wpkh({ pubkey, network });
291
- if (!address) throw new Error("Failed to derive a P2WPKH address from this xpub");
314
+ if (!address) throw new Error("Failed to derive a P2WPKH address from this public key");
292
315
  return address;
293
316
  }
294
317
  case "p2tr":
@@ -347,21 +370,14 @@ var KeystoneUrEngine = class {
347
370
  }
348
371
  // --- TRON ---
349
372
  /**
350
- * `@keystonehq/keystone-sdk`'s own bundled `sdk.tron` module is
351
- * deliberately NOT used here — see `TronSignRequest.ts`'s doc comment for
352
- * why: it's a different (gzip/protobuf) protocol with response semantics
353
- * this package has no way to verify, whereas `TronSignRequest`/
354
- * `TronSignature` are a direct port of OneKey's own already-proven
355
- * production QR-wallet TRON implementation (a plain CBOR-native
356
- * sign-request/signature pair, same shape as eth/sol). The device decodes
357
- * `rawTxHex` itself — no client-side contract-type pre-parsing or
358
- * `tokenInfo` needed, unlike the public SDK's module.
373
+ * Keystone's native tron-sign-request/tron-signature pair (bare signature).
374
+ * `sdk.tron` from keystone-sdk is the older protobuf envelope; not used.
359
375
  */
360
376
  buildTronSignRequest(input) {
361
377
  const request = new TronSignRequest({
362
378
  requestId: Buffer.from(uuidParse(input.requestId)),
363
379
  signData: Buffer.from(stripHexPrefix(input.rawTxHex), "hex"),
364
- signType: 0 /* Transaction */,
380
+ signType: input.signType ?? 1 /* Transaction */,
365
381
  derivationPath: TronSignRequest.parsePath(input.path, input.xfp),
366
382
  origin: input.origin
367
383
  });
@@ -398,6 +414,26 @@ var KeystoneUrEngine = class {
398
414
  ]);
399
415
  return bs58check.encode(addressBytes);
400
416
  }
417
+ /**
418
+ * Split an account-level xpub back into the BIP-32 fields a host needs to
419
+ * treat it as a real extended key (`BtcPublicKey`). Everything here is
420
+ * carried inside the xpub's own serialization — depth, the PARENT key
421
+ * fingerprint (not the seed's master fingerprint), the chain code and the
422
+ * compressed public key — so this is a pure decode with no device round
423
+ * trip; the xpub itself was already device-verified when it was synced.
424
+ */
425
+ parseXpubMeta(xpub) {
426
+ const raw = Buffer.from(bs58check.decode(xpub));
427
+ if (raw.length !== 78) {
428
+ throw new Error(`Keystone xpub did not decode to a 78-byte BIP-32 key (got ${raw.length})`);
429
+ }
430
+ return {
431
+ publicKey: raw.subarray(45, 78).toString("hex"),
432
+ chainCode: raw.subarray(13, 45).toString("hex"),
433
+ depth: raw.readUInt8(4),
434
+ parentFingerprint: raw.readUInt32BE(5)
435
+ };
436
+ }
401
437
  };
402
438
 
403
439
  // src/adapter/KeystoneAdapter.ts
@@ -408,6 +444,8 @@ import {
408
444
  DEVICE,
409
445
  DeviceJobQueue,
410
446
  HardwareErrorCode,
447
+ OperationRegistry,
448
+ SDK,
411
449
  TypedEventEmitter,
412
450
  UI_REQUEST,
413
451
  UI_REQUEST_CANCELLED_TAG,
@@ -415,16 +453,28 @@ import {
415
453
  UI_REQUEST_TIMEOUT_TAG,
416
454
  UI_RESPONSE,
417
455
  UiRequestRegistry,
456
+ createHardwareSearchTargetId,
418
457
  createHwkError,
419
458
  deriveDeviceFingerprint,
420
459
  ensure0x,
421
460
  failure,
461
+ getAllNetworkMethodChain,
462
+ hasHardwareRuntimeIdPrefix,
463
+ isAllNetworkMethodName,
464
+ isHardwareOperationId,
465
+ isHwkRecoveryHint,
466
+ operationMayHaveCompletedParams,
467
+ parseBip32MasterFingerprint as parseBip32MasterFingerprint2,
422
468
  rehydrateConnectorError,
469
+ resolveHardwareOperationTarget as resolveGenericHardwareOperationTarget,
423
470
  runAllNetworkGetAddress,
424
471
  stripHex,
425
472
  success
426
473
  } from "@onekeyfe/hwk-adapter-core";
427
474
 
475
+ // src/adapter/deviceTable.ts
476
+ import { deriveWalletId } from "@onekeyfe/hwk-adapter-core";
477
+
428
478
  // src/adapter/pathUtils.ts
429
479
  function normalizePath(path) {
430
480
  const trimmed = path.trim();
@@ -459,20 +509,41 @@ function btcScriptTypeFromPath(path) {
459
509
  return void 0;
460
510
  }
461
511
  }
512
+ function isKeystoneSignableBtcAccountPath(accountPath) {
513
+ const segments = normalizePath(accountPath).slice(2).split("/");
514
+ if (segments.length !== 3) return false;
515
+ const [purpose, coin, account] = segments;
516
+ return ["44'", "49'", "84'", "86'"].includes(purpose) && coin === "0'" && account === "0'";
517
+ }
518
+ var KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE = "Keystone BTC is limited to mainnet account 0 (m/44'|49'|84'|86'/0'/0')";
462
519
 
463
520
  // src/adapter/deviceTable.ts
464
- var QR_CONNECT_ID_PREFIX = "keystone-qr:";
465
- function qrConnectId(masterFingerprint) {
466
- return `${QR_CONNECT_ID_PREFIX}${masterFingerprint}`;
521
+ var KEYSTONE_WALLET_CONNECT_ID_PREFIX = "keystone-wallet:";
522
+ var KEYSTONE_WALLET_ID_PATH = "m/44'/60'/0'";
523
+ function walletConnectId(walletId) {
524
+ return `${KEYSTONE_WALLET_CONNECT_ID_PREFIX}${walletId}`;
467
525
  }
468
526
  function accountKey(hwkChain, path) {
469
527
  return `${hwkChain}:${normalizePath(path)}`;
470
528
  }
471
- function createDeviceRecord(masterFingerprint) {
529
+ function deriveKeystoneWalletId(accounts) {
530
+ const identityAccount = accounts.find(
531
+ (account) => normalizePath(account.path) === KEYSTONE_WALLET_ID_PATH
532
+ );
533
+ if (!identityAccount?.extendedPublicKey) {
534
+ throw new Error(
535
+ `Keystone identity response is missing the extended public key at ${KEYSTONE_WALLET_ID_PATH}`
536
+ );
537
+ }
538
+ return deriveWalletId(
539
+ `keystone:secp256k1:${KEYSTONE_WALLET_ID_PATH}:${identityAccount.extendedPublicKey.trim()}`
540
+ );
541
+ }
542
+ function createDeviceRecord(walletId, masterFingerprint) {
472
543
  return {
544
+ walletId,
473
545
  masterFingerprint,
474
- connectId: qrConnectId(masterFingerprint),
475
- accounts: /* @__PURE__ */ new Map(),
546
+ connectId: walletConnectId(walletId),
476
547
  importedAt: Date.now()
477
548
  };
478
549
  }
@@ -487,11 +558,15 @@ function toDeviceInfo(record) {
487
558
  model: record.model ?? "unknown",
488
559
  modelName: record.model,
489
560
  firmwareVersion: record.deviceVersion ?? "0.0.0",
490
- deviceId: record.masterFingerprint,
561
+ deviceId: record.walletId,
491
562
  connectId: record.connectId,
492
563
  connectionType: record.usbSessionId ? "usb" : "qr",
493
564
  capabilities: CAPABILITIES,
494
- raw: { availableChannels }
565
+ raw: {
566
+ availableChannels,
567
+ masterFingerprint: record.masterFingerprint,
568
+ hardwareDeviceId: record.hardwareDeviceId
569
+ }
495
570
  };
496
571
  }
497
572
  function placeholderDeviceInfo() {
@@ -508,6 +583,20 @@ function placeholderDeviceInfo() {
508
583
 
509
584
  // src/adapter/KeystoneAdapter.ts
510
585
  var COLD_START_JOB_LABEL = "keystone-cold-start";
586
+ var KEYSTONE_USB_REATTACH_PROBE_ATTEMPTS = 4;
587
+ var KEYSTONE_USB_REATTACH_PROBE_INTERVAL_MS = 500;
588
+ function resolveHardwareOperationTarget(positionalTargetId, operationId) {
589
+ const result = resolveGenericHardwareOperationTarget(positionalTargetId, operationId, "keystone");
590
+ return result;
591
+ }
592
+ function waitForKeystoneUsbReattachProbe() {
593
+ return new Promise((resolve) => {
594
+ setTimeout(resolve, KEYSTONE_USB_REATTACH_PROBE_INTERVAL_MS);
595
+ });
596
+ }
597
+ function getKeystoneJobId(connectId, deviceId) {
598
+ return isHardwareOperationId(connectId) ? connectId : deviceId || connectId || COLD_START_JOB_LABEL;
599
+ }
511
600
  var BIP44_COIN_TYPE_TO_CHAIN = {
512
601
  60: "evm",
513
602
  0: "btc",
@@ -518,64 +607,111 @@ function inferHwkChainFromPath(path) {
518
607
  const match = normalizePath(path).match(/^m\/\d+'\/(\d+)'/);
519
608
  return match ? BIP44_COIN_TYPE_TO_CHAIN[Number(match[1])] : void 0;
520
609
  }
521
- var DEFAULT_IMPORT_SCHEMAS = [
522
- { hwkChain: "evm", path: CHAIN_FINGERPRINT_PATHS.evm.replace(/\/0\/0$/, "") },
523
- // The 3 BTC purposes this package can actually derive addresses for
524
- // (btcScriptTypeFromPath: 44'→P2PKH, 49'→P2SH-P2WPKH, 84'→P2WPKH) —
525
- // explicitly requested, not left to chance on whether the device
526
- // volunteers the others unprompted. Still one combined round trip: these
527
- // are 3 more entries in the same qr-hardware-call, not 3 more scans.
528
- // 86' (P2TR/taproot) deliberately excluded — `deriveBtcAddressFromXpub`
529
- // can't use it yet (needs an elliptic-curve library not wired in), and
530
- // unlike app-monorepo's own sync-all bundle (which also requests 86', but
531
- // to compute a "full xfp" — a need this adapter doesn't have, since its
532
- // mfp already comes for free from the response envelope), there's no
533
- // other use for that xpub here — so it stays out until P2TR is real.
534
- { hwkChain: "btc", path: CHAIN_FINGERPRINT_PATHS.btc },
535
- { hwkChain: "btc", path: "m/49'/0'/0'" },
536
- { hwkChain: "btc", path: "m/84'/0'/0'" },
537
- { hwkChain: "sol", path: CHAIN_FINGERPRINT_PATHS.sol },
538
- { hwkChain: "tron", path: CHAIN_FINGERPRINT_PATHS.tron.replace(/\/0\/0$/, "") }
539
- ];
540
610
  var KeystoneAdapter = class _KeystoneAdapter {
541
611
  constructor(options) {
542
612
  this.vendor = "keystone";
543
613
  this.emitter = new TypedEventEmitter();
614
+ this._operationRoutes = /* @__PURE__ */ new Map();
615
+ this._operations = new OperationRegistry({
616
+ vendor: "keystone",
617
+ onEnded: (operation, reason) => {
618
+ const route = this._operationRoutes.get(operation.connectId);
619
+ if (route?.operationId === operation.operationId) {
620
+ this._operationRoutes.delete(operation.connectId);
621
+ }
622
+ this.emitter.emit(SDK.OPERATION_ENDED, {
623
+ type: SDK.OPERATION_ENDED,
624
+ payload: { operationId: operation.operationId, reason }
625
+ });
626
+ if (reason === "timeout") {
627
+ this._releaseOperationConnection(operation).catch(() => void 0);
628
+ }
629
+ }
630
+ });
544
631
  this._uiRegistry = new UiRequestRegistry();
545
632
  this._devices = /* @__PURE__ */ new Map();
633
+ this._searchDeviceTargets = /* @__PURE__ */ new Map();
634
+ /** Serializes USB open/identity handshakes that run outside the job queue. */
635
+ this._usbConnectTail = Promise.resolve();
636
+ this._unsettledUsbOperations = /* @__PURE__ */ new Map();
637
+ this._usbIdleWaiters = /* @__PURE__ */ new Set();
638
+ this._usbTeardownTail = Promise.resolve();
639
+ this._pendingUsbTeardowns = 0;
640
+ this._handleUsbDisconnect = ({ connectId }) => {
641
+ const record = Array.from(this._devices.values()).find((item) => item.usbSessionId === connectId);
642
+ if (!record) return;
643
+ this._operations.endByConnectionKey(connectId, "disconnect");
644
+ this._operations.endByConnectionKey(record.connectId, "disconnect");
645
+ record.usbSessionId = void 0;
646
+ const info = toDeviceInfo(record);
647
+ if (record.qrSynced) {
648
+ this.emitter.emit(DEVICE.CHANGED, { type: DEVICE.CHANGED, payload: info });
649
+ } else {
650
+ this._devices.delete(record.walletId);
651
+ this.emitter.emit(DEVICE.DISCONNECT, { type: DEVICE.DISCONNECT, payload: info });
652
+ }
653
+ };
654
+ this._handleUsbUiEvent = (event) => {
655
+ this.emitter.emit("ui-event", event);
656
+ };
546
657
  // ---------------------------------------------------------------------------
547
658
  // All-network bundle — dispatches to the same per-chain methods below, so
548
659
  // there's exactly one implementation of each chain's address logic.
549
660
  // ---------------------------------------------------------------------------
550
661
  this.allNetworkGetAddress = async (connectId, deviceId, params) => {
662
+ const operationTarget = resolveHardwareOperationTarget(connectId, params.operationId);
663
+ if (!operationTarget.success) return operationTarget;
664
+ const effectiveConnectId = operationTarget.payload.targetId ?? "";
665
+ const { operationId } = operationTarget.payload;
551
666
  try {
552
- return await runAllNetworkGetAddress({
553
- connectId,
667
+ const prefetched = await this._prefetchAllNetworkAccounts(
668
+ effectiveConnectId,
554
669
  deviceId,
670
+ params
671
+ );
672
+ const { book } = prefetched;
673
+ const itemDeviceId = deviceId || prefetched.walletId || "";
674
+ return await runAllNetworkGetAddress({
675
+ connectId: effectiveConnectId,
676
+ deviceId: itemDeviceId,
555
677
  params,
556
- callItem: async ({ chain, item }) => {
557
- const commonArgs = { path: item.path, showOnDevice: item.showOnDevice };
558
- switch (chain) {
559
- case "evm":
560
- return this.evmGetAddress(connectId, deviceId, commonArgs);
561
- case "btc":
562
- return this.btcGetAddress(connectId, deviceId, commonArgs);
563
- case "sol":
564
- return this.solGetAddress(connectId, deviceId, commonArgs);
565
- case "tron":
566
- return this.tronGetAddress(connectId, deviceId, commonArgs);
678
+ callItem: async ({ method, item }) => {
679
+ const commonArgs = {
680
+ path: item.path,
681
+ showOnDevice: item.showOnDevice,
682
+ operationId
683
+ };
684
+ switch (method) {
685
+ case "evmGetAddress":
686
+ return this.evmGetAddress(effectiveConnectId, itemDeviceId, commonArgs, book);
687
+ case "btcGetAddress":
688
+ return this.btcGetAddress(effectiveConnectId, itemDeviceId, commonArgs, book);
689
+ case "btcGetPublicKey":
690
+ return this.btcGetPublicKey(effectiveConnectId, itemDeviceId, commonArgs, book);
691
+ case "solGetAddress":
692
+ return this.solGetAddress(effectiveConnectId, itemDeviceId, commonArgs, book);
693
+ case "tronGetAddress":
694
+ return this.tronGetAddress(effectiveConnectId, itemDeviceId, commonArgs, book);
567
695
  default:
568
- return failure(HardwareErrorCode.MethodNotSupported, `Unsupported chain: ${chain}`);
696
+ return failure(
697
+ HardwareErrorCode.MethodNotSupported,
698
+ `Unsupported method: ${String(method)}`
699
+ );
569
700
  }
570
701
  },
571
702
  attachIdentity: (context) => {
572
- const mfp = deviceId ? deviceId.toLowerCase() : void 0;
703
+ const { record } = this._resolveTarget(effectiveConnectId, itemDeviceId);
573
704
  return Promise.resolve({
574
705
  ...context.item,
575
706
  success: true,
576
707
  payload: {
577
708
  ...context.payload,
578
- deviceIdentity: mfp ? { vendor: "keystone", type: "masterFingerprint", value: mfp } : void 0
709
+ rootFingerprint: record ? Number.parseInt(record.masterFingerprint, 16) : void 0,
710
+ deviceIdentity: record ? {
711
+ vendor: "keystone",
712
+ type: "walletId",
713
+ value: record.walletId
714
+ } : void 0
579
715
  }
580
716
  });
581
717
  }
@@ -589,6 +725,8 @@ var KeystoneAdapter = class _KeystoneAdapter {
589
725
  this.urEngine = new KeystoneUrEngine(this._origin);
590
726
  this._jobQueue = new DeviceJobQueue();
591
727
  this._usbConnector = options?.usbConnector;
728
+ this._usbConnector?.on("device-disconnect", this._handleUsbDisconnect);
729
+ this._usbConnector?.on("ui-event", this._handleUsbUiEvent);
592
730
  }
593
731
  // ---------------------------------------------------------------------------
594
732
  // Lifecycle / transport
@@ -609,56 +747,184 @@ var KeystoneAdapter = class _KeystoneAdapter {
609
747
  init(_config) {
610
748
  return Promise.resolve();
611
749
  }
612
- dispose() {
750
+ async dispose() {
751
+ this._operations.endAll("runtime-reset");
613
752
  this._uiRegistry.cancel();
614
753
  this._jobQueue.clear();
615
- this.emitter.removeAllListeners();
616
- this._devices.clear();
754
+ this._searchDeviceTargets.clear();
755
+ await this._resetUsbSessions();
756
+ this._usbConnector?.off("device-disconnect", this._handleUsbDisconnect);
757
+ this._usbConnector?.off("ui-event", this._handleUsbUiEvent);
617
758
  this._usbConnector?.reset();
618
- return Promise.resolve();
759
+ this._devices.clear();
760
+ this.emitter.removeAllListeners();
619
761
  }
620
762
  // ---------------------------------------------------------------------------
621
763
  // Device table
622
764
  // ---------------------------------------------------------------------------
623
765
  /**
624
- * QR-synced wallets are always included (this instance's own state — no
625
- * enumeration exists for QR). When a USB connector is configured, its raw
626
- * scan results are appended as-is: a USB descriptor has no mfp until
627
- * `connectDevice()` actually opens+claims it (see
766
+ * Unscoped discovery includes QR-synced wallets already known to this
767
+ * adapter. Explicit QR discovery returns a virtual connection target because
768
+ * there is no physical descriptor to enumerate. When a USB connector is
769
+ * configured, its raw scan results are appended as-is: a USB descriptor has
770
+ * no mfp until `connectDevice()` actually opens+claims it (see
628
771
  * `KeystoneUsbConnectorBase.searchDevices`), so these entries carry an
629
772
  * empty `deviceId` and exist purely so a host can list "plugged in, click
630
773
  * to connect" candidates.
631
774
  */
632
- async searchDevices(_options) {
775
+ async searchDevices(options) {
776
+ const searchGeneration = /* @__PURE__ */ Symbol("keystone-device-search");
777
+ this._activeSearchGeneration = searchGeneration;
778
+ this._searchDeviceTargets.clear();
779
+ if (options?.resetSession) {
780
+ await this._resetUsbSessions();
781
+ } else {
782
+ await this._usbTeardownTail;
783
+ }
633
784
  const known = Array.from(this._devices.values()).map(toDeviceInfo);
634
- if (!this._usbConnector) return known;
785
+ if (options?.transportType === "qr") {
786
+ const searchTargetId = createHardwareSearchTargetId("keystone");
787
+ this._searchDeviceTargets.set(searchTargetId, "qr");
788
+ return [
789
+ {
790
+ ...placeholderDeviceInfo(),
791
+ model: "",
792
+ connectId: searchTargetId,
793
+ capabilities: { persistentDeviceIdentity: false }
794
+ }
795
+ ];
796
+ }
797
+ if (options?.transportType === "ble") return [];
798
+ if (!this._usbConnector) return options?.transportType === "usb" ? [] : known;
635
799
  let usbDevices;
636
800
  try {
637
801
  usbDevices = await this._usbConnector.searchDevices();
638
- } catch {
802
+ } catch (error) {
803
+ if (options?.transportType === "usb") throw error;
639
804
  return known;
640
805
  }
806
+ if (this._activeSearchGeneration !== searchGeneration) return [];
641
807
  const placeholders = usbDevices.map((d) => ({
642
808
  vendor: "keystone",
643
- model: d.model ?? "unknown",
809
+ // Empty, not 'unknown': enumeration genuinely cannot tell the model
810
+ // apart (every Keystone shares one vid/pid), and a literal placeholder
811
+ // string would beat the host's own default-name fallback and surface as
812
+ // a device called "unknown".
813
+ model: d.model ?? "",
644
814
  modelName: d.modelName,
815
+ // Placeholders — neither is knowable until the device is opened.
645
816
  firmwareVersion: "0.0.0",
646
817
  deviceId: d.deviceId,
647
818
  connectId: d.connectId,
648
819
  connectionType: "usb",
649
- capabilities: d.capabilities ?? { persistentDeviceIdentity: true }
820
+ // The USB product name, which IS readable at enumeration time.
821
+ label: d.name,
822
+ serialNumber: d.serialNumber,
823
+ capabilities: d.capabilities ?? { persistentDeviceIdentity: false },
824
+ raw: d.raw
825
+ }));
826
+ for (const placeholder of placeholders) {
827
+ this._searchDeviceTargets.set(placeholder.connectId, "usb");
828
+ }
829
+ return options?.transportType === "usb" ? placeholders : [...known, ...placeholders];
830
+ }
831
+ async _resetUsbSessions() {
832
+ await this._runUsbTeardown(async () => {
833
+ await this._usbConnectTail;
834
+ this._operations.endAll("runtime-reset");
835
+ if (!this._usbConnector) return;
836
+ const sessionIds = new Set(
837
+ Array.from(this._devices.values()).map((record) => record.usbSessionId).filter((sessionId) => Boolean(sessionId))
838
+ );
839
+ for (const sessionId of sessionIds) {
840
+ try {
841
+ await this._usbConnector.disconnect(sessionId);
842
+ } catch {
843
+ }
844
+ this._handleUsbDisconnect({ connectId: sessionId });
845
+ }
846
+ });
847
+ }
848
+ async searchDeviceTargets(options) {
849
+ const devices = await this.searchDevices(options);
850
+ return devices.map((device) => ({
851
+ searchTargetId: device.connectId,
852
+ // Keystone's wallet id is stable only after the USB/QR handshake. The
853
+ // selectable handle remains owned by the current discovery snapshot.
854
+ searchTargetReusePolicy: "current-discovery",
855
+ vendor: device.vendor,
856
+ connectionType: device.connectionType,
857
+ kind: device.connectionType === "qr" ? "interactive" : "physical",
858
+ label: device.label,
859
+ model: device.model,
860
+ modelName: device.modelName,
861
+ serialNumber: device.serialNumber
650
862
  }));
651
- return [...known, ...placeholders];
652
863
  }
653
- async connectDevice(connectId) {
654
- const existing = this._findByConnectId(connectId);
655
- if (existing) return success(existing.connectId);
656
- if (!this._usbConnector) {
657
- return failure(HardwareErrorCode.DeviceNotFound, `Unknown Keystone connectId: ${connectId}`);
864
+ async listConnectionTargets(options) {
865
+ const targets = await this.searchDeviceTargets(options);
866
+ return targets.map(({ searchTargetId, ...target }) => ({
867
+ ...target,
868
+ targetId: searchTargetId
869
+ }));
870
+ }
871
+ async connectDevice(searchTargetId) {
872
+ try {
873
+ let connected;
874
+ let selectedConnectionType;
875
+ const discoveredConnectionType = this._searchDeviceTargets.get(searchTargetId);
876
+ if (discoveredConnectionType === "qr") {
877
+ selectedConnectionType = "qr";
878
+ connected = await this.importFromQr();
879
+ } else if (discoveredConnectionType === "usb") {
880
+ selectedConnectionType = "usb";
881
+ if (!this._usbConnector) {
882
+ return failure(
883
+ HardwareErrorCode.TransportNotAvailable,
884
+ "No USB connector configured for this Keystone adapter"
885
+ );
886
+ }
887
+ connected = await this._connectUsb({}, searchTargetId);
888
+ } else if (hasHardwareRuntimeIdPrefix(searchTargetId)) {
889
+ return failure(
890
+ HardwareErrorCode.DeviceNotFound,
891
+ "Keystone search target has expired; search for devices again",
892
+ void 0,
893
+ void 0,
894
+ { scope: "search-target" }
895
+ );
896
+ } else {
897
+ const target = this._resolveTarget(searchTargetId);
898
+ if (target.record) {
899
+ selectedConnectionType = target.record.usbSessionId ? "usb" : "qr";
900
+ connected = success(toDeviceInfo(target.record));
901
+ } else if (!this._usbConnector) {
902
+ return failure(
903
+ HardwareErrorCode.DeviceNotFound,
904
+ `Unknown Keystone device search target: ${searchTargetId}`
905
+ );
906
+ } else {
907
+ selectedConnectionType = "usb";
908
+ connected = await this._connectUsb(target);
909
+ }
910
+ }
911
+ if (!connected.success) return connected;
912
+ const record = this._devices.get(connected.payload.deviceId);
913
+ this._operations.endByConnectionKey(connected.payload.connectId, "explicit");
914
+ const operation = this._operations.create({
915
+ searchTargetId,
916
+ connectId: connected.payload.connectId,
917
+ device: connected.payload,
918
+ connectionKeys: selectedConnectionType === "usb" && record?.usbSessionId ? [record.usbSessionId] : []
919
+ });
920
+ this._operationRoutes.set(connected.payload.connectId, {
921
+ operationId: operation.operationId,
922
+ connectionType: selectedConnectionType
923
+ });
924
+ return success(operation.operationId);
925
+ } catch (err) {
926
+ return this._errorToFailure(err);
658
927
  }
659
- const result = await this._connectUsb();
660
- if (!result.success) return result;
661
- return success(result.payload.connectId);
662
928
  }
663
929
  /**
664
930
  * QR has no persistent connection to tear down — the account cache
@@ -667,54 +933,89 @@ var KeystoneAdapter = class _KeystoneAdapter {
667
933
  * QR-only (if it was ever QR-synced) or removes it entirely (pure-USB
668
934
  * wallet that was never seen over QR) — see §4.2 of the design doc.
669
935
  */
670
- async disconnectDevice(connectId) {
671
- const record = this._findByConnectId(connectId);
672
- if (!record?.usbSessionId) return;
673
- if (this._usbConnector) {
674
- try {
675
- await this._usbConnector.disconnect(record.usbSessionId);
676
- } catch {
677
- }
678
- }
679
- record.usbSessionId = void 0;
680
- if (record.qrSynced) {
681
- const info = toDeviceInfo(record);
682
- this.emitter.emit(DEVICE.CHANGED, { type: DEVICE.CHANGED, payload: info });
683
- } else {
684
- this._devices.delete(record.masterFingerprint);
685
- const info = toDeviceInfo(record);
686
- this.emitter.emit(DEVICE.DISCONNECT, { type: DEVICE.DISCONNECT, payload: info });
936
+ async releaseOperation(operationId) {
937
+ const operation = this._operations.find(operationId);
938
+ if (!operation) {
939
+ this._operations.resolve(operationId);
940
+ return;
687
941
  }
942
+ const endedOperation = this._operations.end(operationId, "explicit");
943
+ if (!endedOperation) return;
944
+ await this._releaseOperationConnection(endedOperation);
688
945
  }
689
- getDeviceInfo(connectId, deviceId) {
690
- const record = this._findByConnectId(connectId) ?? this._devices.get(deviceId.toLowerCase());
691
- if (!record) {
692
- return Promise.resolve(
693
- failure(
694
- HardwareErrorCode.DeviceNotFound,
695
- `Unknown Keystone device: ${connectId || deviceId}`
696
- )
697
- );
946
+ async _releaseOperationConnection(operation) {
947
+ const { connectId } = operation;
948
+ let record;
949
+ try {
950
+ record = this._resolveTarget(connectId).record;
951
+ } catch {
952
+ return;
953
+ }
954
+ if (!record?.usbSessionId || !operation.connectionKeys.includes(record.usbSessionId)) {
955
+ return;
698
956
  }
699
- return Promise.resolve(success(toDeviceInfo(record)));
957
+ const { usbSessionId } = record;
958
+ await this._runUsbTeardown(async () => {
959
+ if (this._usbConnector) {
960
+ try {
961
+ await this._usbConnector.disconnect(usbSessionId);
962
+ } catch {
963
+ }
964
+ }
965
+ this._handleUsbDisconnect({ connectId: usbSessionId });
966
+ });
700
967
  }
701
- getSupportedChains() {
702
- return ["evm", "btc", "sol", "tron"];
968
+ getDeviceInfo(connectIdOrOperationId, deviceId) {
969
+ try {
970
+ const connectId = isHardwareOperationId(connectIdOrOperationId) ? this._operations.resolve(connectIdOrOperationId).connectId : connectIdOrOperationId;
971
+ const { record } = this._resolveTarget(connectId, deviceId);
972
+ if (!record) {
973
+ return Promise.resolve(
974
+ failure(
975
+ HardwareErrorCode.DeviceNotFound,
976
+ `Unknown Keystone device: ${connectId || deviceId}`
977
+ )
978
+ );
979
+ }
980
+ return Promise.resolve(success(toDeviceInfo(record)));
981
+ } catch (err) {
982
+ return Promise.resolve(this._errorToFailure(err));
983
+ }
703
984
  }
704
985
  cancel(connectId) {
705
986
  const reason = createHwkError({
706
987
  code: HardwareErrorCode.UserAborted,
707
988
  message: "User aborted operation"
708
989
  });
990
+ const activeJobId = this._jobQueue.getActiveJob()?.deviceId;
991
+ if (connectId) {
992
+ if (!activeJobId || activeJobId !== connectId && activeJobId !== this._walletIdFromIdentifier(connectId)) {
993
+ this._jobQueue.cancelActiveAndPending(connectId, reason);
994
+ const walletId = this._walletIdFromIdentifier(connectId);
995
+ if (walletId && walletId !== connectId) {
996
+ this._jobQueue.cancelActiveAndPending(walletId, reason);
997
+ }
998
+ return;
999
+ }
1000
+ this._jobQueue.cancelActiveAndPending(activeJobId, reason);
1001
+ } else {
1002
+ this._jobQueue.cancelActiveAndPending(void 0, reason);
1003
+ }
709
1004
  this._uiRegistry.cancel();
710
- this._jobQueue.cancelActiveAndPending(connectId, reason);
711
1005
  }
712
1006
  getChainFingerprint(connectId, deviceId, chain) {
713
- const mfp = deviceId ? deviceId.toLowerCase() : this._mfpFromConnectId(connectId);
714
- if (!mfp) {
715
- return Promise.resolve(failure(HardwareErrorCode.DeviceNotFound, "Unknown Keystone device"));
1007
+ try {
1008
+ const target = this._resolveTarget(connectId, deviceId);
1009
+ const walletId = target.record?.walletId ?? target.expectedWalletId;
1010
+ if (!walletId) {
1011
+ return Promise.resolve(
1012
+ failure(HardwareErrorCode.DeviceNotFound, "Unknown Keystone wallet identity")
1013
+ );
1014
+ }
1015
+ return Promise.resolve(success(deriveDeviceFingerprint(`keystone:${chain}:${walletId}`)));
1016
+ } catch (err) {
1017
+ return Promise.resolve(this._errorToFailure(err));
716
1018
  }
717
- return Promise.resolve(success(deriveDeviceFingerprint(`keystone:${chain}:${mfp}`)));
718
1019
  }
719
1020
  uiResponse(response) {
720
1021
  if (response.type === UI_RESPONSE.CANCEL) {
@@ -738,17 +1039,11 @@ var KeystoneAdapter = class _KeystoneAdapter {
738
1039
  return await this._jobQueue.enqueue(COLD_START_JOB_LABEL, async (signal) => {
739
1040
  const displayDevice = placeholderDeviceInfo();
740
1041
  let responseUr;
741
- let requestedChains;
742
1042
  if (options.mode === "scan") {
743
1043
  responseUr = await this._requestQrScanAndAwaitResponse(displayDevice);
744
1044
  } else {
745
- const schemas = options.paths?.length ? options.paths : DEFAULT_IMPORT_SCHEMAS;
746
- requestedChains = schemas.map((s) => s.hwkChain);
747
1045
  const requestUr = this.urEngine.buildKeyDerivationRequest({
748
- schemas: schemas.map((s) => ({
749
- path: s.path,
750
- curve: s.hwkChain === "sol" ? "ed25519" : "secp256k1"
751
- })),
1046
+ schemas: [{ path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" }],
752
1047
  origin: this._origin
753
1048
  });
754
1049
  responseUr = await this._requestQrDisplayAndAwaitResponse(displayDevice, {
@@ -759,13 +1054,6 @@ var KeystoneAdapter = class _KeystoneAdapter {
759
1054
  _KeystoneAdapter._throwIfAborted(signal);
760
1055
  const parsed = this.urEngine.parseAccountResponse(responseUr);
761
1056
  const record = this._upsertDeviceRecord(parsed);
762
- for (const account of parsed.accounts) {
763
- const hwkChain = requestedChains?.length === 1 ? requestedChains[0] : inferHwkChainFromPath(account.path);
764
- if (hwkChain) {
765
- const entry = { ...account, hwkChain };
766
- record.accounts.set(accountKey(hwkChain, account.path), entry);
767
- }
768
- }
769
1057
  return success(toDeviceInfo(record));
770
1058
  });
771
1059
  } catch (err) {
@@ -775,8 +1063,10 @@ var KeystoneAdapter = class _KeystoneAdapter {
775
1063
  // ---------------------------------------------------------------------------
776
1064
  // EVM
777
1065
  // ---------------------------------------------------------------------------
778
- async evmGetAddress(connectIdArg, deviceIdArg, paramsArg) {
779
- const connectId = connectIdArg ?? void 0;
1066
+ async evmGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1067
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1068
+ if (!operationTarget.success) return operationTarget;
1069
+ const connectId = operationTarget.payload.targetId;
780
1070
  const deviceId = deviceIdArg ?? void 0;
781
1071
  const params = paramsArg;
782
1072
  if (!params) return failure(HardwareErrorCode.InvalidParams, "evmGetAddress requires params");
@@ -790,36 +1080,36 @@ var KeystoneAdapter = class _KeystoneAdapter {
790
1080
  );
791
1081
  }
792
1082
  try {
793
- return await this._jobQueue.enqueue(
794
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
795
- async (signal) => {
796
- const { account } = await this._ensureAccountSynced(
797
- connectId,
798
- deviceId,
799
- "evm",
800
- accountPath,
801
- signal
802
- );
803
- _KeystoneAdapter._throwIfAborted(signal);
804
- if (!account.extendedPublicKey) {
805
- throw createHwkError({
806
- code: HardwareErrorCode.MethodNotSupported,
807
- message: "Keystone did not return an extended public key for this account path"
808
- });
809
- }
810
- const address = this.urEngine.deriveEvmAddressFromXpub(
811
- account.extendedPublicKey,
812
- relativeDerivePath
813
- );
814
- return success({ address, path: normalizePath(params.path) });
1083
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1084
+ const { account } = await this._fetchAccount(
1085
+ connectId,
1086
+ deviceId,
1087
+ "evm",
1088
+ accountPath,
1089
+ signal,
1090
+ { book }
1091
+ );
1092
+ _KeystoneAdapter._throwIfAborted(signal);
1093
+ if (!account.extendedPublicKey) {
1094
+ throw createHwkError({
1095
+ code: HardwareErrorCode.MethodNotSupported,
1096
+ message: "Keystone did not return an extended public key for this account path"
1097
+ });
815
1098
  }
816
- );
1099
+ const address = this.urEngine.deriveEvmAddressFromXpub(
1100
+ account.extendedPublicKey,
1101
+ relativeDerivePath
1102
+ );
1103
+ return success({ address, path: normalizePath(params.path) });
1104
+ });
817
1105
  } catch (err) {
818
1106
  return this._errorToFailure(err);
819
1107
  }
820
1108
  }
821
1109
  async evmSignTransaction(connectIdArg, deviceIdArg, paramsArg) {
822
- const connectId = connectIdArg ?? void 0;
1110
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1111
+ if (!operationTarget.success) return operationTarget;
1112
+ const connectId = operationTarget.payload.targetId;
823
1113
  const deviceId = deviceIdArg ?? void 0;
824
1114
  const params = paramsArg;
825
1115
  if (!params)
@@ -836,38 +1126,44 @@ var KeystoneAdapter = class _KeystoneAdapter {
836
1126
  const rawTxHex = stripHex(params.serializedTx);
837
1127
  const path = normalizePath(params.path);
838
1128
  try {
839
- return await this._jobQueue.enqueue(
840
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
841
- async (signal) => {
842
- const { record } = await this._ensureMfpKnown(connectId, deviceId, "evm", signal);
843
- _KeystoneAdapter._throwIfAborted(signal);
844
- const requestId = uuidv4();
845
- const dataType = parseInt(rawTxHex.slice(0, 2), 16) < 192 ? "typedTransaction" : "transaction";
846
- const requestUr = this.urEngine.buildEthSignRequest({
847
- requestId,
848
- unsignedTxHex: rawTxHex,
849
- dataType,
850
- path,
851
- xfp: record.masterFingerprint,
852
- chainId: params.chainId
853
- });
854
- const responseUr = await this._resolveUr(record, requestUr, true);
855
- _KeystoneAdapter._throwIfAborted(signal);
856
- const sig = this.urEngine.parseEthSignature(responseUr);
857
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
858
- return success({
859
- v: ensure0x(sig.v),
860
- r: ensure0x(sig.r),
861
- s: ensure0x(sig.s)
862
- });
863
- }
864
- );
1129
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1130
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "evm", signal);
1131
+ _KeystoneAdapter._throwIfAborted(signal);
1132
+ const requestId = uuidv4();
1133
+ const dataType = parseInt(rawTxHex.slice(0, 2), 16) < 192 ? "typedTransaction" : "transaction";
1134
+ const requestUr = this.urEngine.buildEthSignRequest({
1135
+ requestId,
1136
+ unsignedTxHex: rawTxHex,
1137
+ dataType,
1138
+ path,
1139
+ xfp: record.masterFingerprint,
1140
+ chainId: params.chainId
1141
+ });
1142
+ const responseUr = await this._resolveUr(
1143
+ record,
1144
+ requestUr,
1145
+ true,
1146
+ connectId,
1147
+ signal,
1148
+ "evmSignTransaction"
1149
+ );
1150
+ _KeystoneAdapter._throwIfAborted(signal);
1151
+ const sig = this.urEngine.parseEthSignature(responseUr);
1152
+ _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1153
+ return success({
1154
+ v: ensure0x(sig.v),
1155
+ r: ensure0x(sig.r),
1156
+ s: ensure0x(sig.s)
1157
+ });
1158
+ });
865
1159
  } catch (err) {
866
1160
  return this._errorToFailure(err);
867
1161
  }
868
1162
  }
869
1163
  async evmSignMessage(connectIdArg, deviceIdArg, paramsArg) {
870
- const connectId = connectIdArg ?? void 0;
1164
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1165
+ if (!operationTarget.success) return operationTarget;
1166
+ const connectId = operationTarget.payload.targetId;
871
1167
  const deviceId = deviceIdArg ?? void 0;
872
1168
  const params = paramsArg;
873
1169
  if (!params) return failure(HardwareErrorCode.InvalidParams, "evmSignMessage requires params");
@@ -880,33 +1176,39 @@ var KeystoneAdapter = class _KeystoneAdapter {
880
1176
  const messageHex = params.hex ? stripHex(params.message) : Buffer.from(params.message, "utf8").toString("hex");
881
1177
  const path = normalizePath(params.path);
882
1178
  try {
883
- return await this._jobQueue.enqueue(
884
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
885
- async (signal) => {
886
- const { record } = await this._ensureMfpKnown(connectId, deviceId, "evm", signal);
887
- _KeystoneAdapter._throwIfAborted(signal);
888
- const requestId = uuidv4();
889
- const requestUr = this.urEngine.buildEthSignRequest({
890
- requestId,
891
- unsignedTxHex: messageHex,
892
- dataType: "personalMessage",
893
- path,
894
- xfp: record.masterFingerprint,
895
- chainId: params.chainId
896
- });
897
- const responseUr = await this._resolveUr(record, requestUr, true);
898
- _KeystoneAdapter._throwIfAborted(signal);
899
- const sig = this.urEngine.parseEthSignature(responseUr);
900
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
901
- return success({ signature: ensure0x(sig.r + sig.s + sig.v) });
902
- }
903
- );
1179
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1180
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "evm", signal);
1181
+ _KeystoneAdapter._throwIfAborted(signal);
1182
+ const requestId = uuidv4();
1183
+ const requestUr = this.urEngine.buildEthSignRequest({
1184
+ requestId,
1185
+ unsignedTxHex: messageHex,
1186
+ dataType: "personalMessage",
1187
+ path,
1188
+ xfp: record.masterFingerprint,
1189
+ chainId: params.chainId
1190
+ });
1191
+ const responseUr = await this._resolveUr(
1192
+ record,
1193
+ requestUr,
1194
+ true,
1195
+ connectId,
1196
+ signal,
1197
+ "evmSignMessage"
1198
+ );
1199
+ _KeystoneAdapter._throwIfAborted(signal);
1200
+ const sig = this.urEngine.parseEthSignature(responseUr);
1201
+ _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1202
+ return success({ signature: ensure0x(sig.r + sig.s + sig.v) });
1203
+ });
904
1204
  } catch (err) {
905
1205
  return this._errorToFailure(err);
906
1206
  }
907
1207
  }
908
1208
  async evmSignTypedData(connectIdArg, deviceIdArg, paramsArg) {
909
- const connectId = connectIdArg ?? void 0;
1209
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1210
+ if (!operationTarget.success) return operationTarget;
1211
+ const connectId = operationTarget.payload.targetId;
910
1212
  const deviceId = deviceIdArg ?? void 0;
911
1213
  const params = paramsArg;
912
1214
  if (!params)
@@ -920,30 +1222,37 @@ var KeystoneAdapter = class _KeystoneAdapter {
920
1222
  "Keystone always displays the full EIP-712 payload for on-device review \u2014 pre-hashed signing is not supported"
921
1223
  );
922
1224
  }
923
- const signDataHex = Buffer.from(JSON.stringify(params.data), "utf8").toString("hex");
1225
+ const signDataHex = Buffer.from(
1226
+ params.dataJson ?? JSON.stringify(params.data),
1227
+ "utf8"
1228
+ ).toString("hex");
924
1229
  const path = normalizePath(params.path);
925
1230
  try {
926
- return await this._jobQueue.enqueue(
927
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
928
- async (signal) => {
929
- const { record } = await this._ensureMfpKnown(connectId, deviceId, "evm", signal);
930
- _KeystoneAdapter._throwIfAborted(signal);
931
- const requestId = uuidv4();
932
- const requestUr = this.urEngine.buildEthSignRequest({
933
- requestId,
934
- unsignedTxHex: signDataHex,
935
- dataType: "typedData",
936
- path,
937
- xfp: record.masterFingerprint,
938
- chainId: params.chainId
939
- });
940
- const responseUr = await this._resolveUr(record, requestUr, true);
941
- _KeystoneAdapter._throwIfAborted(signal);
942
- const sig = this.urEngine.parseEthSignature(responseUr);
943
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
944
- return success({ signature: ensure0x(sig.r + sig.s + sig.v) });
945
- }
946
- );
1231
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1232
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "evm", signal);
1233
+ _KeystoneAdapter._throwIfAborted(signal);
1234
+ const requestId = uuidv4();
1235
+ const requestUr = this.urEngine.buildEthSignRequest({
1236
+ requestId,
1237
+ unsignedTxHex: signDataHex,
1238
+ dataType: "typedData",
1239
+ path,
1240
+ xfp: record.masterFingerprint,
1241
+ chainId: params.chainId
1242
+ });
1243
+ const responseUr = await this._resolveUr(
1244
+ record,
1245
+ requestUr,
1246
+ true,
1247
+ connectId,
1248
+ signal,
1249
+ "evmSignTypedData"
1250
+ );
1251
+ _KeystoneAdapter._throwIfAborted(signal);
1252
+ const sig = this.urEngine.parseEthSignature(responseUr);
1253
+ _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1254
+ return success({ signature: ensure0x(sig.r + sig.s + sig.v) });
1255
+ });
947
1256
  } catch (err) {
948
1257
  return this._errorToFailure(err);
949
1258
  }
@@ -954,8 +1263,10 @@ var KeystoneAdapter = class _KeystoneAdapter {
954
1263
  // phase doesn't wire in yet; structured-field tx signing needs host-side
955
1264
  // PSBT construction. Both are real, bounded follow-ups, not silent gaps.
956
1265
  // ---------------------------------------------------------------------------
957
- async btcGetAddress(connectIdArg, deviceIdArg, paramsArg) {
958
- const connectId = connectIdArg ?? void 0;
1266
+ async btcGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1267
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1268
+ if (!operationTarget.success) return operationTarget;
1269
+ const connectId = operationTarget.payload.targetId;
959
1270
  const deviceId = deviceIdArg ?? void 0;
960
1271
  const params = paramsArg;
961
1272
  if (!params) return failure(HardwareErrorCode.InvalidParams, "btcGetAddress requires params");
@@ -974,48 +1285,103 @@ var KeystoneAdapter = class _KeystoneAdapter {
974
1285
  "P2TR (purpose 86') needs an elliptic-curve library for BIP-341 tweaking, not yet wired in \u2014 44'/49'/84' work"
975
1286
  );
976
1287
  }
977
- const { accountPath, relativeDerivePath } = splitAccountPath(params.path);
1288
+ const normalizedPath = normalizePath(params.path);
1289
+ const { accountPath, relativeDerivePath } = splitAccountPath(normalizedPath);
978
1290
  if (!relativeDerivePath) {
979
1291
  return failure(
980
1292
  HardwareErrorCode.InvalidParams,
981
1293
  "btcGetAddress requires a full leaf path, e.g. m/84'/0'/0'/0/0"
982
1294
  );
983
1295
  }
1296
+ if (!isKeystoneSignableBtcAccountPath(accountPath)) {
1297
+ return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1298
+ }
984
1299
  try {
985
- return await this._jobQueue.enqueue(
986
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
987
- async (signal) => {
988
- const { account } = await this._ensureAccountSynced(
989
- connectId,
990
- deviceId,
991
- "btc",
992
- accountPath,
993
- signal
994
- );
995
- _KeystoneAdapter._throwIfAborted(signal);
996
- if (!account.extendedPublicKey) {
997
- throw createHwkError({
998
- code: HardwareErrorCode.MethodNotSupported,
999
- message: "Keystone did not return an extended public key for this account path"
1000
- });
1001
- }
1002
- const address = this.urEngine.deriveBtcAddressFromXpub(
1003
- account.extendedPublicKey,
1004
- relativeDerivePath,
1005
- scriptType
1006
- );
1007
- return success({ address, path: normalizePath(params.path) });
1300
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1301
+ const { account } = await this._fetchAccount(
1302
+ connectId,
1303
+ deviceId,
1304
+ "btc",
1305
+ accountPath,
1306
+ signal,
1307
+ { book }
1308
+ );
1309
+ _KeystoneAdapter._throwIfAborted(signal);
1310
+ if (!account.extendedPublicKey) {
1311
+ throw createHwkError({
1312
+ code: HardwareErrorCode.MethodNotSupported,
1313
+ message: "Keystone did not return an extended public key for this account path"
1314
+ });
1008
1315
  }
1009
- );
1316
+ const address = this.urEngine.deriveBtcAddressFromXpub(
1317
+ account.extendedPublicKey,
1318
+ relativeDerivePath,
1319
+ scriptType
1320
+ );
1321
+ return success({ address, path: normalizedPath });
1322
+ });
1010
1323
  } catch (err) {
1011
1324
  return this._errorToFailure(err);
1012
1325
  }
1013
1326
  }
1014
- async btcGetPublicKey(_connectId, _deviceId, _params) {
1015
- return this._unsupported(
1016
- "btcGetPublicKey",
1017
- "not yet wired \u2014 the underlying synced xpub/chainCode data already exists in the device table"
1018
- );
1327
+ /**
1328
+ * Returns the account-level extended public key. `params.path` must be an
1329
+ * ACCOUNT path (`m/84'/0'/0'`), not a leaf — that is the level Keystone
1330
+ * actually syncs, and it is what a host needs to derive a whole account's
1331
+ * addresses offline. Unlike `btcGetAddress` this has no script-type
1332
+ * restriction: an xpub is script-type agnostic, so `86'` (taproot) works
1333
+ * here even though deriving a taproot ADDRESS still needs an EC library
1334
+ * this package doesn't wire in.
1335
+ */
1336
+ async btcGetPublicKey(connectIdArg, deviceIdArg, paramsArg, book) {
1337
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1338
+ if (!operationTarget.success) return operationTarget;
1339
+ const connectId = operationTarget.payload.targetId;
1340
+ const deviceId = deviceIdArg ?? void 0;
1341
+ const params = paramsArg;
1342
+ if (!params) return failure(HardwareErrorCode.InvalidParams, "btcGetPublicKey requires params");
1343
+ if (!params.path)
1344
+ return failure(HardwareErrorCode.InvalidParams, "btcGetPublicKey requires params.path");
1345
+ const { accountPath, relativeDerivePath } = splitAccountPath(params.path);
1346
+ if (relativeDerivePath) {
1347
+ return failure(
1348
+ HardwareErrorCode.InvalidParams,
1349
+ "btcGetPublicKey requires an account path, e.g. m/84'/0'/0' (not a leaf path)"
1350
+ );
1351
+ }
1352
+ if (!isKeystoneSignableBtcAccountPath(accountPath)) {
1353
+ return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1354
+ }
1355
+ try {
1356
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1357
+ const { account } = await this._fetchAccount(
1358
+ connectId,
1359
+ deviceId,
1360
+ "btc",
1361
+ accountPath,
1362
+ signal,
1363
+ { book }
1364
+ );
1365
+ _KeystoneAdapter._throwIfAborted(signal);
1366
+ if (!account.extendedPublicKey) {
1367
+ throw createHwkError({
1368
+ code: HardwareErrorCode.MethodNotSupported,
1369
+ message: "Keystone did not return an extended public key for this account path"
1370
+ });
1371
+ }
1372
+ const meta = this.urEngine.parseXpubMeta(account.extendedPublicKey);
1373
+ return success({
1374
+ xpub: account.extendedPublicKey,
1375
+ publicKey: meta.publicKey,
1376
+ chainCode: meta.chainCode,
1377
+ depth: meta.depth,
1378
+ fingerprint: meta.parentFingerprint,
1379
+ path: normalizePath(accountPath)
1380
+ });
1381
+ });
1382
+ } catch (err) {
1383
+ return this._errorToFailure(err);
1384
+ }
1019
1385
  }
1020
1386
  async btcSignTransaction(_connectId, _deviceId, _params) {
1021
1387
  return this._unsupported(
@@ -1024,31 +1390,42 @@ var KeystoneAdapter = class _KeystoneAdapter {
1024
1390
  );
1025
1391
  }
1026
1392
  async btcSignPsbt(connectIdArg, deviceIdArg, paramsArg) {
1027
- const connectId = connectIdArg ?? void 0;
1393
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1394
+ if (!operationTarget.success) return operationTarget;
1395
+ const connectId = operationTarget.payload.targetId;
1028
1396
  const deviceId = deviceIdArg ?? void 0;
1029
1397
  const params = paramsArg;
1030
1398
  if (!params) return failure(HardwareErrorCode.InvalidParams, "btcSignPsbt requires params");
1031
1399
  if (!params.psbt)
1032
1400
  return failure(HardwareErrorCode.InvalidParams, "btcSignPsbt requires params.psbt");
1401
+ if (params.path && !isKeystoneSignableBtcAccountPath(splitAccountPath(params.path).accountPath)) {
1402
+ return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1403
+ }
1033
1404
  try {
1034
- return await this._jobQueue.enqueue(
1035
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
1036
- async (signal) => {
1037
- const { record } = await this._ensureMfpKnown(connectId, deviceId, "btc", signal);
1038
- _KeystoneAdapter._throwIfAborted(signal);
1039
- const requestUr = this.urEngine.buildBtcPsbtRequest(stripHex(params.psbt));
1040
- const responseUr = await this._resolveUr(record, requestUr, true);
1041
- _KeystoneAdapter._throwIfAborted(signal);
1042
- const signedPsbt = this.urEngine.parseBtcPsbt(responseUr);
1043
- return success({ signedPsbt });
1044
- }
1045
- );
1405
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1406
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1407
+ _KeystoneAdapter._throwIfAborted(signal);
1408
+ const requestUr = this.urEngine.buildBtcPsbtRequest(stripHex(params.psbt));
1409
+ const responseUr = await this._resolveUr(
1410
+ record,
1411
+ requestUr,
1412
+ true,
1413
+ connectId,
1414
+ signal,
1415
+ "btcSignPsbt"
1416
+ );
1417
+ _KeystoneAdapter._throwIfAborted(signal);
1418
+ const signedPsbt = this.urEngine.parseBtcPsbt(responseUr);
1419
+ return success({ signedPsbt });
1420
+ });
1046
1421
  } catch (err) {
1047
1422
  return this._errorToFailure(err);
1048
1423
  }
1049
1424
  }
1050
1425
  async btcSignMessage(connectIdArg, deviceIdArg, paramsArg) {
1051
- const connectId = connectIdArg ?? void 0;
1426
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1427
+ if (!operationTarget.success) return operationTarget;
1428
+ const connectId = operationTarget.payload.targetId;
1052
1429
  const deviceId = deviceIdArg ?? void 0;
1053
1430
  const params = paramsArg;
1054
1431
  if (!params) return failure(HardwareErrorCode.InvalidParams, "btcSignMessage requires params");
@@ -1060,40 +1437,46 @@ var KeystoneAdapter = class _KeystoneAdapter {
1060
1437
  }
1061
1438
  const messageHex = params.hex ? stripHex(params.message) : Buffer.from(params.message, "utf8").toString("hex");
1062
1439
  const path = normalizePath(params.path);
1440
+ if (!isKeystoneSignableBtcAccountPath(splitAccountPath(path).accountPath)) {
1441
+ return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1442
+ }
1063
1443
  try {
1064
- return await this._jobQueue.enqueue(
1065
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
1066
- async (signal) => {
1067
- const { record } = await this._ensureMfpKnown(connectId, deviceId, "btc", signal);
1068
- _KeystoneAdapter._throwIfAborted(signal);
1069
- const requestId = uuidv4();
1070
- const requestUr = this.urEngine.buildBtcMessageSignRequest({
1071
- requestId,
1072
- messageHex,
1073
- accounts: [{ path, xfp: record.masterFingerprint }]
1074
- });
1075
- const responseUr = await this._resolveUr(record, requestUr, true);
1076
- _KeystoneAdapter._throwIfAborted(signal);
1077
- const sig = this.urEngine.parseBtcSignature(responseUr);
1078
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1079
- return success({ signature: sig.signature });
1080
- }
1081
- );
1444
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1445
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1446
+ _KeystoneAdapter._throwIfAborted(signal);
1447
+ const requestId = uuidv4();
1448
+ const requestUr = this.urEngine.buildBtcMessageSignRequest({
1449
+ requestId,
1450
+ messageHex,
1451
+ accounts: [{ path, xfp: record.masterFingerprint }]
1452
+ });
1453
+ const responseUr = await this._resolveUr(
1454
+ record,
1455
+ requestUr,
1456
+ true,
1457
+ connectId,
1458
+ signal,
1459
+ "btcSignMessage"
1460
+ );
1461
+ _KeystoneAdapter._throwIfAborted(signal);
1462
+ const sig = this.urEngine.parseBtcSignature(responseUr);
1463
+ _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1464
+ return success({ signature: sig.signature });
1465
+ });
1082
1466
  } catch (err) {
1083
1467
  return this._errorToFailure(err);
1084
1468
  }
1085
1469
  }
1086
- async btcGetMasterFingerprint(connectIdArg, deviceIdArg) {
1087
- const connectId = connectIdArg ?? void 0;
1470
+ async btcGetMasterFingerprint(connectIdArg, deviceIdArg, paramsArg) {
1471
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1472
+ if (!operationTarget.success) return operationTarget;
1473
+ const connectId = operationTarget.payload.targetId;
1088
1474
  const deviceId = deviceIdArg ?? void 0;
1089
1475
  try {
1090
- return await this._jobQueue.enqueue(
1091
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
1092
- async (signal) => {
1093
- const { record } = await this._ensureMfpKnown(connectId, deviceId, "btc", signal);
1094
- return success({ masterFingerprint: record.masterFingerprint });
1095
- }
1096
- );
1476
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1477
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1478
+ return success({ masterFingerprint: record.masterFingerprint });
1479
+ });
1097
1480
  } catch (err) {
1098
1481
  return this._errorToFailure(err);
1099
1482
  }
@@ -1101,8 +1484,10 @@ var KeystoneAdapter = class _KeystoneAdapter {
1101
1484
  // ---------------------------------------------------------------------------
1102
1485
  // SOL
1103
1486
  // ---------------------------------------------------------------------------
1104
- async solGetAddress(connectIdArg, deviceIdArg, paramsArg) {
1105
- const connectId = connectIdArg ?? void 0;
1487
+ async solGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1488
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1489
+ if (!operationTarget.success) return operationTarget;
1490
+ const connectId = operationTarget.payload.targetId;
1106
1491
  const deviceId = deviceIdArg ?? void 0;
1107
1492
  const params = paramsArg;
1108
1493
  if (!params) return failure(HardwareErrorCode.InvalidParams, "solGetAddress requires params");
@@ -1110,27 +1495,22 @@ var KeystoneAdapter = class _KeystoneAdapter {
1110
1495
  return failure(HardwareErrorCode.InvalidParams, "solGetAddress requires params.path");
1111
1496
  const path = normalizePath(params.path);
1112
1497
  try {
1113
- return await this._jobQueue.enqueue(
1114
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
1115
- async (signal) => {
1116
- const { account } = await this._ensureAccountSynced(
1117
- connectId,
1118
- deviceId,
1119
- "sol",
1120
- path,
1121
- signal
1122
- );
1123
- _KeystoneAdapter._throwIfAborted(signal);
1124
- const address = bs58.encode(Buffer.from(account.publicKey, "hex"));
1125
- return success({ address, path });
1126
- }
1127
- );
1498
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1499
+ const { account } = await this._fetchAccount(connectId, deviceId, "sol", path, signal, {
1500
+ book
1501
+ });
1502
+ _KeystoneAdapter._throwIfAborted(signal);
1503
+ const address = bs58.encode(Buffer.from(account.publicKey, "hex"));
1504
+ return success({ address, path });
1505
+ });
1128
1506
  } catch (err) {
1129
1507
  return this._errorToFailure(err);
1130
1508
  }
1131
1509
  }
1132
1510
  async solSignTransaction(connectIdArg, deviceIdArg, paramsArg) {
1133
- const connectId = connectIdArg ?? void 0;
1511
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1512
+ if (!operationTarget.success) return operationTarget;
1513
+ const connectId = operationTarget.payload.targetId;
1134
1514
  const deviceId = deviceIdArg ?? void 0;
1135
1515
  const params = paramsArg;
1136
1516
  if (!params)
@@ -1143,32 +1523,38 @@ var KeystoneAdapter = class _KeystoneAdapter {
1143
1523
  }
1144
1524
  const path = normalizePath(params.path);
1145
1525
  try {
1146
- return await this._jobQueue.enqueue(
1147
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
1148
- async (signal) => {
1149
- const { record } = await this._ensureMfpKnown(connectId, deviceId, "sol", signal);
1150
- _KeystoneAdapter._throwIfAborted(signal);
1151
- const requestId = uuidv4();
1152
- const requestUr = this.urEngine.buildSolSignRequest({
1153
- requestId,
1154
- unsignedPayloadHex: stripHex(params.serializedTx),
1155
- dataType: "transaction",
1156
- path,
1157
- xfp: record.masterFingerprint
1158
- });
1159
- const responseUr = await this._resolveUr(record, requestUr, true);
1160
- _KeystoneAdapter._throwIfAborted(signal);
1161
- const sig = this.urEngine.parseSolSignature(responseUr);
1162
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1163
- return success({ signature: sig.signature });
1164
- }
1165
- );
1526
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1527
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "sol", signal);
1528
+ _KeystoneAdapter._throwIfAborted(signal);
1529
+ const requestId = uuidv4();
1530
+ const requestUr = this.urEngine.buildSolSignRequest({
1531
+ requestId,
1532
+ unsignedPayloadHex: stripHex(params.serializedTx),
1533
+ dataType: "transaction",
1534
+ path,
1535
+ xfp: record.masterFingerprint
1536
+ });
1537
+ const responseUr = await this._resolveUr(
1538
+ record,
1539
+ requestUr,
1540
+ true,
1541
+ connectId,
1542
+ signal,
1543
+ "solSignTransaction"
1544
+ );
1545
+ _KeystoneAdapter._throwIfAborted(signal);
1546
+ const sig = this.urEngine.parseSolSignature(responseUr);
1547
+ _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1548
+ return success({ signature: sig.signature });
1549
+ });
1166
1550
  } catch (err) {
1167
1551
  return this._errorToFailure(err);
1168
1552
  }
1169
1553
  }
1170
1554
  async solSignMessage(connectIdArg, deviceIdArg, paramsArg) {
1171
- const connectId = connectIdArg ?? void 0;
1555
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1556
+ if (!operationTarget.success) return operationTarget;
1557
+ const connectId = operationTarget.payload.targetId;
1172
1558
  const deviceId = deviceIdArg ?? void 0;
1173
1559
  const params = paramsArg;
1174
1560
  if (!params) return failure(HardwareErrorCode.InvalidParams, "solSignMessage requires params");
@@ -1180,26 +1566,30 @@ var KeystoneAdapter = class _KeystoneAdapter {
1180
1566
  }
1181
1567
  const path = normalizePath(params.path);
1182
1568
  try {
1183
- return await this._jobQueue.enqueue(
1184
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
1185
- async (signal) => {
1186
- const { record } = await this._ensureMfpKnown(connectId, deviceId, "sol", signal);
1187
- _KeystoneAdapter._throwIfAborted(signal);
1188
- const requestId = uuidv4();
1189
- const requestUr = this.urEngine.buildSolSignRequest({
1190
- requestId,
1191
- unsignedPayloadHex: stripHex(params.message),
1192
- dataType: "message",
1193
- path,
1194
- xfp: record.masterFingerprint
1195
- });
1196
- const responseUr = await this._resolveUr(record, requestUr, true);
1197
- _KeystoneAdapter._throwIfAborted(signal);
1198
- const sig = this.urEngine.parseSolSignature(responseUr);
1199
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1200
- return success({ signature: sig.signature });
1201
- }
1202
- );
1569
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1570
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "sol", signal);
1571
+ _KeystoneAdapter._throwIfAborted(signal);
1572
+ const requestId = uuidv4();
1573
+ const requestUr = this.urEngine.buildSolSignRequest({
1574
+ requestId,
1575
+ unsignedPayloadHex: stripHex(params.message),
1576
+ dataType: "message",
1577
+ path,
1578
+ xfp: record.masterFingerprint
1579
+ });
1580
+ const responseUr = await this._resolveUr(
1581
+ record,
1582
+ requestUr,
1583
+ true,
1584
+ connectId,
1585
+ signal,
1586
+ "solSignMessage"
1587
+ );
1588
+ _KeystoneAdapter._throwIfAborted(signal);
1589
+ const sig = this.urEngine.parseSolSignature(responseUr);
1590
+ _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1591
+ return success({ signature: sig.signature });
1592
+ });
1203
1593
  } catch (err) {
1204
1594
  return this._errorToFailure(err);
1205
1595
  }
@@ -1211,8 +1601,10 @@ var KeystoneAdapter = class _KeystoneAdapter {
1211
1601
  // bundled `sdk.tron` module (different, protobuf-based protocol with
1212
1602
  // unverified response semantics).
1213
1603
  // ---------------------------------------------------------------------------
1214
- async tronGetAddress(connectIdArg, deviceIdArg, paramsArg) {
1215
- const connectId = connectIdArg ?? void 0;
1604
+ async tronGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1605
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1606
+ if (!operationTarget.success) return operationTarget;
1607
+ const connectId = operationTarget.payload.targetId;
1216
1608
  const deviceId = deviceIdArg ?? void 0;
1217
1609
  const params = paramsArg;
1218
1610
  if (!params) return failure(HardwareErrorCode.InvalidParams, "tronGetAddress requires params");
@@ -1226,36 +1618,36 @@ var KeystoneAdapter = class _KeystoneAdapter {
1226
1618
  );
1227
1619
  }
1228
1620
  try {
1229
- return await this._jobQueue.enqueue(
1230
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
1231
- async (signal) => {
1232
- const { account } = await this._ensureAccountSynced(
1233
- connectId,
1234
- deviceId,
1235
- "tron",
1236
- accountPath,
1237
- signal
1238
- );
1239
- _KeystoneAdapter._throwIfAborted(signal);
1240
- if (!account.extendedPublicKey) {
1241
- throw createHwkError({
1242
- code: HardwareErrorCode.MethodNotSupported,
1243
- message: "Keystone did not return an extended public key for this account path"
1244
- });
1245
- }
1246
- const address = this.urEngine.deriveTronAddressFromXpub(
1247
- account.extendedPublicKey,
1248
- relativeDerivePath
1249
- );
1250
- return success({ address, path: normalizePath(params.path) });
1621
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1622
+ const { account } = await this._fetchAccount(
1623
+ connectId,
1624
+ deviceId,
1625
+ "tron",
1626
+ accountPath,
1627
+ signal,
1628
+ { book }
1629
+ );
1630
+ _KeystoneAdapter._throwIfAborted(signal);
1631
+ if (!account.extendedPublicKey) {
1632
+ throw createHwkError({
1633
+ code: HardwareErrorCode.MethodNotSupported,
1634
+ message: "Keystone did not return an extended public key for this account path"
1635
+ });
1251
1636
  }
1252
- );
1637
+ const address = this.urEngine.deriveTronAddressFromXpub(
1638
+ account.extendedPublicKey,
1639
+ relativeDerivePath
1640
+ );
1641
+ return success({ address, path: normalizePath(params.path) });
1642
+ });
1253
1643
  } catch (err) {
1254
1644
  return this._errorToFailure(err);
1255
1645
  }
1256
1646
  }
1257
1647
  async tronSignTransaction(connectIdArg, deviceIdArg, paramsArg) {
1258
- const connectId = connectIdArg ?? void 0;
1648
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1649
+ if (!operationTarget.success) return operationTarget;
1650
+ const connectId = operationTarget.payload.targetId;
1259
1651
  const deviceId = deviceIdArg ?? void 0;
1260
1652
  const params = paramsArg;
1261
1653
  if (!params)
@@ -1272,50 +1664,278 @@ var KeystoneAdapter = class _KeystoneAdapter {
1272
1664
  const rawTxHex = stripHex(params.rawTxHex);
1273
1665
  const path = normalizePath(params.path);
1274
1666
  try {
1275
- return await this._jobQueue.enqueue(
1276
- deviceId ?? connectId ?? COLD_START_JOB_LABEL,
1277
- async (signal) => {
1278
- const { record } = await this._ensureMfpKnown(connectId, deviceId, "tron", signal);
1279
- _KeystoneAdapter._throwIfAborted(signal);
1280
- const requestId = uuidv4();
1281
- const requestUr = this.urEngine.buildTronSignRequest({
1282
- requestId,
1283
- rawTxHex,
1284
- path,
1285
- xfp: record.masterFingerprint
1286
- });
1287
- const responseUr = await this._resolveUr(record, requestUr, true);
1288
- _KeystoneAdapter._throwIfAborted(signal);
1289
- const sig = this.urEngine.parseTronSignature(responseUr);
1290
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1291
- return success({ signature: sig.signature });
1292
- }
1293
- );
1667
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1668
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "tron", signal);
1669
+ _KeystoneAdapter._throwIfAborted(signal);
1670
+ const requestId = uuidv4();
1671
+ const requestUr = this.urEngine.buildTronSignRequest({
1672
+ requestId,
1673
+ rawTxHex,
1674
+ path,
1675
+ xfp: record.masterFingerprint
1676
+ });
1677
+ const responseUr = await this._resolveUr(
1678
+ record,
1679
+ requestUr,
1680
+ true,
1681
+ connectId,
1682
+ signal,
1683
+ "tronSignTransaction"
1684
+ );
1685
+ _KeystoneAdapter._throwIfAborted(signal);
1686
+ const sig = this.urEngine.parseTronSignature(responseUr);
1687
+ _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1688
+ return success({ signature: sig.signature });
1689
+ });
1294
1690
  } catch (err) {
1295
1691
  return this._errorToFailure(err);
1296
1692
  }
1297
1693
  }
1298
- async tronSignMessage(_connectId, _deviceId, _params) {
1299
- return this._unsupported(
1300
- "tronSignMessage",
1301
- "Keystone TRON message signing needs signType V1-vs-V2 verified against real hardware first \u2014 not wired in"
1302
- );
1694
+ async tronSignMessage(connectIdArg, deviceIdArg, paramsArg) {
1695
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1696
+ if (!operationTarget.success) return operationTarget;
1697
+ const connectId = operationTarget.payload.targetId;
1698
+ const deviceId = deviceIdArg ?? void 0;
1699
+ const params = paramsArg;
1700
+ if (!params) return failure(HardwareErrorCode.InvalidParams, "tronSignMessage requires params");
1701
+ if (!params.path) {
1702
+ return failure(HardwareErrorCode.InvalidParams, "tronSignMessage requires params.path");
1703
+ }
1704
+ if (!params.messageHex) {
1705
+ return failure(HardwareErrorCode.InvalidParams, "tronSignMessage requires params.messageHex");
1706
+ }
1707
+ if (params.messageType !== "V2") {
1708
+ return failure(
1709
+ HardwareErrorCode.MethodNotSupported,
1710
+ 'Keystone signs TRON messages as TIP-191 signMessageV2 only (messageType: "V2")'
1711
+ );
1712
+ }
1713
+ const messageHex = stripHex(params.messageHex);
1714
+ const path = normalizePath(params.path);
1715
+ try {
1716
+ return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1717
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "tron", signal);
1718
+ _KeystoneAdapter._throwIfAborted(signal);
1719
+ const requestId = uuidv4();
1720
+ const requestUr = this.urEngine.buildTronSignRequest({
1721
+ requestId,
1722
+ rawTxHex: messageHex,
1723
+ path,
1724
+ xfp: record.masterFingerprint,
1725
+ signType: 2 /* PersonalMessage */
1726
+ });
1727
+ const responseUr = await this._resolveUr(
1728
+ record,
1729
+ requestUr,
1730
+ true,
1731
+ connectId,
1732
+ signal,
1733
+ "tronSignMessage"
1734
+ );
1735
+ _KeystoneAdapter._throwIfAborted(signal);
1736
+ const sig = this.urEngine.parseTronSignature(responseUr);
1737
+ _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1738
+ return success({ signature: sig.signature });
1739
+ });
1740
+ } catch (err) {
1741
+ return this._errorToFailure(err);
1742
+ }
1303
1743
  }
1304
1744
  // ---------------------------------------------------------------------------
1305
1745
  // Internals
1306
1746
  // ---------------------------------------------------------------------------
1307
- _mfpFromConnectId(connectId) {
1308
- return connectId?.startsWith(QR_CONNECT_ID_PREFIX) ? connectId.slice(QR_CONNECT_ID_PREFIX.length) : void 0;
1747
+ _walletIdFromIdentifier(identifier) {
1748
+ const value = identifier.startsWith(KEYSTONE_WALLET_CONNECT_ID_PREFIX) ? identifier.slice(KEYSTONE_WALLET_CONNECT_ID_PREFIX.length) : identifier;
1749
+ return /^[0-9a-f]{64}$/i.test(value) ? value.toLowerCase() : void 0;
1750
+ }
1751
+ _allNetworkSyncSchema(item) {
1752
+ if (!isAllNetworkMethodName(String(item.methodName)) || !item.path) return void 0;
1753
+ const method = item.methodName;
1754
+ const hwkChain = getAllNetworkMethodChain(method);
1755
+ const path = normalizePath(item.path);
1756
+ if (hwkChain === "btc" && !isKeystoneSignableBtcAccountPath(splitAccountPath(path).accountPath)) {
1757
+ return void 0;
1758
+ }
1759
+ switch (method) {
1760
+ case "evmGetAddress":
1761
+ case "btcGetAddress":
1762
+ case "tronGetAddress":
1763
+ return { hwkChain, path: splitAccountPath(path).accountPath };
1764
+ case "btcGetPublicKey":
1765
+ case "solGetAddress":
1766
+ return { hwkChain, path };
1767
+ default:
1768
+ return void 0;
1769
+ }
1309
1770
  }
1310
1771
  /**
1311
- * Handles both connectId shapes a caller might hand back: the QR-style
1312
- * `keystone-qr:<mfp>` prefix, and a bare mfp — which is exactly what a
1313
- * USB session's `sessionId`/`connectId` is (see `KeystoneUsbConnectorBase`
1314
- * and `_connectUsb`).
1772
+ * QR is an interactive batch transport: one account-creation action must
1773
+ * produce one request QR and one response scan, regardless of how many
1774
+ * chains or derivation paths the host bundled. USB keeps its proven
1775
+ * one-path-at-a-time export flow because the device limits that channel.
1315
1776
  */
1316
- _findByConnectId(connectId) {
1317
- const mfp = this._mfpFromConnectId(connectId) ?? connectId?.toLowerCase();
1318
- return mfp ? this._devices.get(mfp) : void 0;
1777
+ async _prefetchAllNetworkAccounts(connectId, deviceId, params) {
1778
+ return this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1779
+ const target = this._resolveTarget(connectId, deviceId);
1780
+ const { record: targetRecord } = target;
1781
+ let record = targetRecord;
1782
+ const requestedByKey = /* @__PURE__ */ new Map();
1783
+ for (const item of params.bundle) {
1784
+ const schema = this._allNetworkSyncSchema(item);
1785
+ if (schema) requestedByKey.set(accountKey(schema.hwkChain, schema.path), schema);
1786
+ }
1787
+ const book = /* @__PURE__ */ new Map();
1788
+ if (!requestedByKey.size) {
1789
+ return { book };
1790
+ }
1791
+ if (this._forcedTransport !== "qr" && !record?.usbSessionId && this._usbConnector && target.expectedWalletId) {
1792
+ record = await this._tryUsbAttach(
1793
+ target.expectedWalletId,
1794
+ record?.masterFingerprint ?? target.expectedMasterFingerprint
1795
+ ) ?? record;
1796
+ _KeystoneAdapter._throwIfAborted(signal);
1797
+ }
1798
+ if (this._forcedTransport !== "qr" && record?.usbSessionId) {
1799
+ const bundleSessionId = record.usbSessionId;
1800
+ const assertBundleSession = () => {
1801
+ if (record?.usbSessionId !== bundleSessionId) {
1802
+ throw createHwkError({
1803
+ code: HardwareErrorCode.TransportNotAvailable,
1804
+ message: "Keystone USB connection was lost during account export"
1805
+ });
1806
+ }
1807
+ };
1808
+ const missingSchemas2 = Array.from(requestedByKey.values());
1809
+ for (const schema of missingSchemas2) {
1810
+ if (!book.has(accountKey(schema.hwkChain, schema.path))) {
1811
+ assertBundleSession();
1812
+ const synced = await this._fetchAccount(
1813
+ connectId,
1814
+ deviceId,
1815
+ schema.hwkChain,
1816
+ schema.path,
1817
+ signal,
1818
+ { book }
1819
+ );
1820
+ record = synced.record;
1821
+ assertBundleSession();
1822
+ }
1823
+ }
1824
+ for (const schema of missingSchemas2) {
1825
+ if (!book.has(accountKey(schema.hwkChain, schema.path))) {
1826
+ throw createHwkError({
1827
+ code: HardwareErrorCode.DeviceMismatch,
1828
+ message: `Keystone did not return the requested derivation path (${schema.path})`
1829
+ });
1830
+ }
1831
+ }
1832
+ return { book, walletId: record.walletId };
1833
+ }
1834
+ if (this._forcedTransport === "usb") {
1835
+ throw createHwkError({
1836
+ code: HardwareErrorCode.TransportNotAvailable,
1837
+ message: "USB channel is not connected for this Keystone wallet"
1838
+ });
1839
+ }
1840
+ const missingSchemas = Array.from(requestedByKey.values());
1841
+ const schemas = [
1842
+ { hwkChain: "evm", path: KEYSTONE_WALLET_ID_PATH },
1843
+ ...missingSchemas.filter((schema) => normalizePath(schema.path) !== KEYSTONE_WALLET_ID_PATH)
1844
+ ];
1845
+ const requestUr = this.urEngine.buildKeyDerivationRequest({
1846
+ schemas: schemas.map((schema) => ({
1847
+ path: schema.path,
1848
+ curve: schema.hwkChain === "sol" ? "ed25519" : "secp256k1"
1849
+ })),
1850
+ origin: this._origin
1851
+ });
1852
+ const responseUr = await this._requestQrDisplayAndAwaitResponse(
1853
+ record ? toDeviceInfo(record) : placeholderDeviceInfo(),
1854
+ { ...requestUr, animated: false }
1855
+ );
1856
+ _KeystoneAdapter._throwIfAborted(signal);
1857
+ const parsed = this.urEngine.parseAccountResponse(responseUr);
1858
+ this._assertParsedIdentity(parsed, target);
1859
+ record = this._upsertDeviceRecord(parsed);
1860
+ const requestedChainByPath = new Map(
1861
+ schemas.map((schema) => [normalizePath(schema.path), schema.hwkChain])
1862
+ );
1863
+ for (const account of parsed.accounts) {
1864
+ const normalizedPath = normalizePath(account.path);
1865
+ const hwkChain = requestedChainByPath.get(normalizedPath) ?? inferHwkChainFromPath(normalizedPath);
1866
+ if (hwkChain) {
1867
+ book.set(accountKey(hwkChain, normalizedPath), {
1868
+ ...account,
1869
+ hwkChain
1870
+ });
1871
+ }
1872
+ }
1873
+ for (const schema of missingSchemas) {
1874
+ if (!book.has(accountKey(schema.hwkChain, schema.path))) {
1875
+ throw createHwkError({
1876
+ code: HardwareErrorCode.DeviceMismatch,
1877
+ message: `Keystone did not return the requested derivation path (${schema.path})`
1878
+ });
1879
+ }
1880
+ }
1881
+ return { book, walletId: record.walletId };
1882
+ });
1883
+ }
1884
+ /**
1885
+ * Resolve only the stable 64-hex wallet identity. The 8-hex BIP32 master
1886
+ * fingerprint is protocol metadata and is never accepted as a lookup key.
1887
+ */
1888
+ _resolveTarget(connectId, deviceId) {
1889
+ let resolvedConnectId = connectId;
1890
+ if (isHardwareOperationId(connectId)) {
1891
+ resolvedConnectId = this._operations.resolve(connectId).connectId;
1892
+ }
1893
+ const identifiers = [deviceId, resolvedConnectId].filter(
1894
+ (identifier) => Boolean(identifier)
1895
+ );
1896
+ if (!identifiers.length) return {};
1897
+ const walletIds = identifiers.map((identifier) => this._walletIdFromIdentifier(identifier)).filter((walletId) => Boolean(walletId));
1898
+ if (!walletIds.length) {
1899
+ throw createHwkError({
1900
+ code: HardwareErrorCode.DeviceNotFound,
1901
+ message: "Keystone device lookup requires a 64-hex wallet identity"
1902
+ });
1903
+ }
1904
+ const expectedWalletId = walletIds[0];
1905
+ if (walletIds.some((walletId) => walletId !== expectedWalletId)) {
1906
+ throw createHwkError({
1907
+ code: HardwareErrorCode.DeviceMismatch,
1908
+ message: "Keystone connectId and deviceId identify different wallets"
1909
+ });
1910
+ }
1911
+ if (identifiers.some((identifier) => !this._walletIdFromIdentifier(identifier))) {
1912
+ throw createHwkError({
1913
+ code: HardwareErrorCode.DeviceMismatch,
1914
+ message: "Keystone connectId and deviceId are inconsistent"
1915
+ });
1916
+ }
1917
+ const record = this._devices.get(expectedWalletId);
1918
+ return record ? {
1919
+ record,
1920
+ expectedWalletId,
1921
+ expectedMasterFingerprint: record.masterFingerprint
1922
+ } : { expectedWalletId };
1923
+ }
1924
+ _assertParsedIdentity(parsed, expected) {
1925
+ const walletId = deriveKeystoneWalletId(parsed.accounts);
1926
+ if (expected.expectedMasterFingerprint && parsed.masterFingerprint !== expected.expectedMasterFingerprint) {
1927
+ throw createHwkError({
1928
+ code: HardwareErrorCode.DeviceMismatch,
1929
+ message: `Connected Keystone wallet (mfp ${parsed.masterFingerprint}) does not match the requested wallet fingerprint (${expected.expectedMasterFingerprint})`
1930
+ });
1931
+ }
1932
+ if (expected.expectedWalletId && walletId !== expected.expectedWalletId) {
1933
+ throw createHwkError({
1934
+ code: HardwareErrorCode.DeviceMismatch,
1935
+ message: "Connected Keystone wallet does not match the requested wallet identity"
1936
+ });
1937
+ }
1938
+ return walletId;
1319
1939
  }
1320
1940
  /**
1321
1941
  * Folds a parsed account-response UR into the device table. `viaUsb`
@@ -1324,18 +1944,25 @@ var KeystoneAdapter = class _KeystoneAdapter {
1324
1944
  * already has a live session, so this must NOT unconditionally mark
1325
1945
  * `qrSynced`, or a USB-only wallet would wrongly survive a later USB
1326
1946
  * disconnect as a "QR-synced, demote to QR-only" entry instead of being
1327
- * dropped outright (see `disconnectDevice`).
1947
+ * dropped outright (see `releaseOperation`).
1328
1948
  */
1329
1949
  _upsertDeviceRecord(parsed, options) {
1330
- const mfp = parsed.masterFingerprint;
1331
- let record = this._devices.get(mfp);
1950
+ const walletId = deriveKeystoneWalletId(parsed.accounts);
1951
+ let record = this._devices.get(walletId);
1332
1952
  const isNew = !record;
1333
1953
  if (!record) {
1334
- record = createDeviceRecord(mfp);
1335
- this._devices.set(mfp, record);
1954
+ record = createDeviceRecord(walletId, parsed.masterFingerprint);
1955
+ this._devices.set(walletId, record);
1956
+ } else if (record.masterFingerprint !== parsed.masterFingerprint) {
1957
+ throw createHwkError({
1958
+ code: HardwareErrorCode.DeviceMismatch,
1959
+ message: "Keystone wallet identity returned an inconsistent master fingerprint"
1960
+ });
1336
1961
  }
1337
1962
  record.model = parsed.device ?? record.model;
1338
1963
  record.deviceVersion = parsed.deviceVersion ?? record.deviceVersion;
1964
+ record.hardwareDeviceId = parsed.deviceId ?? record.hardwareDeviceId;
1965
+ record.usbSessionId = options?.usbSessionId ?? record.usbSessionId;
1339
1966
  if (!options?.viaUsb) record.qrSynced = true;
1340
1967
  const info = toDeviceInfo(record);
1341
1968
  const eventType = isNew ? DEVICE.CONNECT : DEVICE.CHANGED;
@@ -1344,95 +1971,353 @@ var KeystoneAdapter = class _KeystoneAdapter {
1344
1971
  }
1345
1972
  /**
1346
1973
  * Opens+claims whatever Keystone the USB connector currently has
1347
- * permission for, learns its mfp via `getAppConfig`, and merges it into
1348
- * the device table by that mfp — a QR-synced entry becomes
1974
+ * permission for, learns its protocol mfp via `getAppConfig`, then requests
1975
+ * one fixed account-level xpub and derives the stable wallet id. A
1976
+ * QR-synced entry becomes
1349
1977
  * `{qr, usb}`-capable in place (one `device-changed`, not a second
1350
1978
  * `device-connect`); a wallet never seen before becomes a new USB-only
1351
1979
  * entry. See §4.2 of the design doc.
1352
1980
  */
1353
- async _connectUsb() {
1981
+ async _connectUsb(expected, searchTargetId) {
1982
+ if (this._pendingUsbTeardowns > 0) {
1983
+ return failure(
1984
+ HardwareErrorCode.DeviceBusyInternal,
1985
+ "Keystone USB session is being released"
1986
+ );
1987
+ }
1988
+ const previous = this._usbConnectTail;
1989
+ let release;
1990
+ this._usbConnectTail = new Promise((resolve) => {
1991
+ release = resolve;
1992
+ });
1993
+ await previous;
1994
+ try {
1995
+ return await this._connectUsbExclusive(expected, searchTargetId);
1996
+ } finally {
1997
+ release?.();
1998
+ }
1999
+ }
2000
+ async _connectUsbExclusive(expected, searchTargetId) {
1354
2001
  if (!this._usbConnector) {
1355
2002
  return failure(
1356
2003
  HardwareErrorCode.TransportNotAvailable,
1357
2004
  "No USB connector configured for this Keystone adapter"
1358
2005
  );
1359
2006
  }
2007
+ let sessionId;
1360
2008
  try {
1361
- const session = await this._usbConnector.connect();
1362
- const mfp = session.deviceInfo.deviceId.toLowerCase();
1363
- let record = this._devices.get(mfp);
1364
- const isNew = !record;
1365
- if (!record) {
1366
- record = createDeviceRecord(mfp);
1367
- this._devices.set(mfp, record);
2009
+ let connectTarget = { type: "default" };
2010
+ if (searchTargetId) {
2011
+ connectTarget = { type: "search-target", searchTargetId };
2012
+ } else if (expected.expectedMasterFingerprint) {
2013
+ connectTarget = {
2014
+ type: "expected-device-identity",
2015
+ deviceIdentity: expected.expectedMasterFingerprint
2016
+ };
2017
+ }
2018
+ const connector = this._usbConnector;
2019
+ const session = await (connector.connectTarget ? connector.connectTarget(connectTarget) : connector.connect(searchTargetId ?? expected.expectedMasterFingerprint));
2020
+ sessionId = session.sessionId;
2021
+ const raw = session.deviceInfo.raw;
2022
+ const mfpValue = parseBip32MasterFingerprint2(raw?.masterFingerprint);
2023
+ if (!mfpValue) {
2024
+ throw createHwkError({
2025
+ code: HardwareErrorCode.DeviceMismatch,
2026
+ message: "Keystone USB did not report a master fingerprint"
2027
+ });
1368
2028
  }
2029
+ const masterFingerprint = mfpValue.toLowerCase();
2030
+ if (expected.expectedMasterFingerprint && masterFingerprint !== expected.expectedMasterFingerprint) {
2031
+ throw createHwkError({
2032
+ code: HardwareErrorCode.DeviceMismatch,
2033
+ message: `Connected Keystone wallet (mfp ${masterFingerprint}) does not match the requested wallet fingerprint (${expected.expectedMasterFingerprint})`
2034
+ });
2035
+ }
2036
+ const identityRequest = this.urEngine.buildKeyDerivationRequest({
2037
+ schemas: [{ path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" }],
2038
+ origin: this._origin
2039
+ });
2040
+ const identityResult = await this._callUsbConnector(
2041
+ session.sessionId,
2042
+ "resolveUr",
2043
+ identityRequest
2044
+ );
2045
+ if (!identityResult.success) {
2046
+ throw rehydrateConnectorError(identityResult.error);
2047
+ }
2048
+ const parsed = this.urEngine.parseAccountResponse(identityResult.payload);
2049
+ this._assertParsedIdentity(parsed, {
2050
+ ...expected,
2051
+ expectedMasterFingerprint: expected.expectedMasterFingerprint ?? masterFingerprint
2052
+ });
2053
+ const walletId = deriveKeystoneWalletId(parsed.accounts);
2054
+ const previousUsbSessionId = this._devices.get(walletId)?.usbSessionId;
2055
+ const record = this._upsertDeviceRecord(parsed, {
2056
+ viaUsb: true,
2057
+ usbSessionId: session.sessionId
2058
+ });
2059
+ if (previousUsbSessionId && previousUsbSessionId !== session.sessionId) {
2060
+ try {
2061
+ await this._usbConnector.disconnect(previousUsbSessionId);
2062
+ } catch {
2063
+ }
2064
+ }
2065
+ record.hadUsbSession = true;
1369
2066
  record.model = session.deviceInfo.modelName ?? session.deviceInfo.model ?? record.model;
1370
2067
  record.deviceVersion = session.deviceInfo.firmwareVersion ?? record.deviceVersion;
1371
- record.usbSessionId = session.sessionId;
1372
2068
  const info = toDeviceInfo(record);
1373
- const eventType = isNew ? DEVICE.CONNECT : DEVICE.CHANGED;
1374
- this.emitter.emit(eventType, { type: eventType, payload: info });
1375
2069
  return success(info);
1376
2070
  } catch (err) {
2071
+ if (sessionId) {
2072
+ try {
2073
+ await this._usbConnector.disconnect(sessionId);
2074
+ } catch {
2075
+ }
2076
+ }
1377
2077
  return this._errorToFailure(err);
1378
2078
  }
1379
2079
  }
2080
+ /**
2081
+ * Probe already-authorized USB devices without opening a permission picker,
2082
+ * then attach and verify the requested wallet when one is present. This is
2083
+ * used both after an adapter restart and after a QR-only period, so plugging
2084
+ * USB back in affects the next business call without changing an in-flight
2085
+ * QR operation.
2086
+ */
2087
+ async _tryUsbAttach(expectedWalletId, expectedMasterFingerprint, options) {
2088
+ if (!expectedWalletId || this._forcedTransport === "qr" || !this._usbConnector) {
2089
+ return void 0;
2090
+ }
2091
+ const maxAttempts = options?.waitForReenumeration ? KEYSTONE_USB_REATTACH_PROBE_ATTEMPTS : 1;
2092
+ let availableDevices = [];
2093
+ let lastSearchError;
2094
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2095
+ try {
2096
+ availableDevices = await this._usbConnector.searchDevices({ purpose: "availability" });
2097
+ lastSearchError = void 0;
2098
+ } catch (error) {
2099
+ lastSearchError = error;
2100
+ }
2101
+ if (availableDevices.length) {
2102
+ break;
2103
+ }
2104
+ if (attempt < maxAttempts) {
2105
+ await waitForKeystoneUsbReattachProbe();
2106
+ }
2107
+ }
2108
+ if (!availableDevices.length) {
2109
+ if (this._forcedTransport === "usb" && lastSearchError) {
2110
+ throw lastSearchError instanceof Error ? lastSearchError : new Error(String(lastSearchError));
2111
+ }
2112
+ return void 0;
2113
+ }
2114
+ let attached;
2115
+ let record;
2116
+ const candidates = expectedMasterFingerprint ? [void 0] : availableDevices;
2117
+ for (const device of candidates) {
2118
+ attached = await this._connectUsb(
2119
+ { expectedWalletId, expectedMasterFingerprint },
2120
+ device?.connectId
2121
+ );
2122
+ if (attached.success) {
2123
+ record = this._devices.get(expectedWalletId);
2124
+ if (record) break;
2125
+ }
2126
+ }
2127
+ return record;
2128
+ }
1380
2129
  /**
1381
2130
  * The one place that decides QR vs. USB for a UR round trip and carries it
1382
2131
  * out. `record` is the (possibly not-yet-existing, for a true cold start)
1383
- * device row for the target wallet — USB is only used when `record`
1384
- * already has a live `usbSessionId` (a session comes from an explicit
1385
- * `connectDevice()`, never conjured mid-call — see the class doc). A
2132
+ * device row for the target wallet. USB is the preferred channel: a known
2133
+ * wallet record with no live session gets ONE best-effort re-attach here,
2134
+ * and anything that goes wrong just leaves the call on QR. A
1386
2135
  * `switchTransport('qr')` pin forces QR even for a USB-attached wallet;
1387
2136
  * `switchTransport('usb')` on a wallet with no live USB session fails
1388
2137
  * closed rather than silently falling back to QR.
1389
2138
  */
1390
- async _resolveUr(record, requestUr, animated) {
1391
- const wantUsb = this._forcedTransport === "usb" || this._forcedTransport !== "qr" && Boolean(record?.usbSessionId);
2139
+ async _resolveUr(record, requestUr, animated, operationId, signal, operationName) {
2140
+ const releaseOperationRetention = isHardwareOperationId(operationId) ? this._operations.retain(operationId) : void 0;
2141
+ try {
2142
+ return await this._resolveUrWithRoute(
2143
+ record,
2144
+ requestUr,
2145
+ animated,
2146
+ operationId,
2147
+ signal,
2148
+ operationName
2149
+ );
2150
+ } finally {
2151
+ releaseOperationRetention?.();
2152
+ }
2153
+ }
2154
+ async _resolveUrWithRoute(record, requestUr, animated, operationId, signal, operationName) {
2155
+ let interactionRoute;
2156
+ if (isHardwareOperationId(operationId)) {
2157
+ const operation = this._operations.resolve(operationId);
2158
+ const route = this._operationRoutes.get(operation.connectId);
2159
+ if (!route || route.operationId !== operationId) {
2160
+ this._operations.end(operationId, "disconnect");
2161
+ throw createHwkError({
2162
+ code: HardwareErrorCode.OperationEnded,
2163
+ message: "Keystone hardware operation is no longer connected",
2164
+ params: { operationId, reason: "disconnect" }
2165
+ });
2166
+ }
2167
+ interactionRoute = route;
2168
+ }
2169
+ if (!interactionRoute && !this._forcedTransport && record && !record.usbSessionId && this._usbConnector) {
2170
+ await this._tryUsbAttach(record.walletId, record.masterFingerprint, {
2171
+ waitForReenumeration: record.hadUsbSession
2172
+ });
2173
+ }
2174
+ const wantUsb = interactionRoute ? interactionRoute.connectionType === "usb" : this._forcedTransport === "usb" || this._forcedTransport !== "qr" && Boolean(record?.usbSessionId);
1392
2175
  if (wantUsb) {
1393
2176
  if (!record?.usbSessionId || !this._usbConnector) {
2177
+ if (interactionRoute) {
2178
+ this._operations.end(interactionRoute.operationId, "disconnect");
2179
+ throw createHwkError({
2180
+ code: HardwareErrorCode.OperationEnded,
2181
+ message: "Keystone operation USB connection was lost",
2182
+ params: {
2183
+ operationId: interactionRoute.operationId,
2184
+ reason: "disconnect"
2185
+ }
2186
+ });
2187
+ }
1394
2188
  throw createHwkError({
1395
2189
  code: HardwareErrorCode.TransportNotAvailable,
1396
2190
  message: "USB channel is not connected for this Keystone wallet"
1397
2191
  });
1398
2192
  }
1399
- const result = await this._usbConnector.call(record.usbSessionId, "resolveUr", requestUr);
1400
- if (!result.success) throw rehydrateConnectorError(result.error);
1401
- return result.payload;
2193
+ const result = await this._callUsbConnector(
2194
+ record.usbSessionId,
2195
+ "resolveUr",
2196
+ requestUr,
2197
+ signal
2198
+ );
2199
+ if (result.success) return result.payload;
2200
+ const usbError = rehydrateConnectorError(result.error);
2201
+ const usbErrorOrigin = usbError.origin;
2202
+ const usbErrorCode = usbError.code;
2203
+ if (!interactionRoute && !this._forcedTransport && usbErrorCode === HardwareErrorCode.PayloadTooLarge && usbErrorOrigin !== "device") {
2204
+ const displayDevice2 = toDeviceInfo(record);
2205
+ return this._requestQrDisplayAndAwaitResponse(displayDevice2, {
2206
+ ...requestUr,
2207
+ animated
2208
+ });
2209
+ }
2210
+ const deviceAnswered = usbErrorOrigin !== void 0 ? usbErrorOrigin === "device" : usbErrorCode === HardwareErrorCode.UserRejected || usbErrorCode === HardwareErrorCode.UserAborted || usbErrorCode === HardwareErrorCode.DeviceLocked || usbErrorCode === HardwareErrorCode.DeviceMismatch;
2211
+ if (!deviceAnswered) {
2212
+ const lostSessionId = record.usbSessionId;
2213
+ if (lostSessionId) {
2214
+ this._usbConnector?.disconnect(lostSessionId).catch(() => void 0);
2215
+ }
2216
+ record.usbSessionId = void 0;
2217
+ if (interactionRoute) {
2218
+ this._operations.end(interactionRoute.operationId, "disconnect");
2219
+ throw createHwkError({
2220
+ code: HardwareErrorCode.OperationEnded,
2221
+ message: operationName ? `Keystone ${operationName} may have completed before the USB connection was lost` : "Keystone operation USB connection was lost",
2222
+ recovery: operationName ? { scope: "unknown" } : void 0,
2223
+ params: operationName ? operationMayHaveCompletedParams(operationName, {
2224
+ operationId: interactionRoute.operationId,
2225
+ reason: "disconnect"
2226
+ }) : {
2227
+ operationId: interactionRoute.operationId,
2228
+ reason: "disconnect"
2229
+ }
2230
+ });
2231
+ }
2232
+ if (operationName) {
2233
+ throw createHwkError({
2234
+ code: usbErrorCode ?? HardwareErrorCode.TransportError,
2235
+ message: `Keystone ${operationName} may have completed before the USB connection was lost`,
2236
+ origin: "transport",
2237
+ recovery: { scope: "unknown" },
2238
+ params: operationMayHaveCompletedParams(operationName)
2239
+ });
2240
+ }
2241
+ }
2242
+ throw usbError;
1402
2243
  }
1403
2244
  const displayDevice = record ? toDeviceInfo(record) : placeholderDeviceInfo();
1404
- return this._requestQrDisplayAndAwaitResponse(displayDevice, { ...requestUr, animated });
2245
+ try {
2246
+ return await this._requestQrDisplayAndAwaitResponse(displayDevice, {
2247
+ ...requestUr,
2248
+ animated
2249
+ });
2250
+ } catch (error) {
2251
+ const { code, _tag: tag } = error;
2252
+ if (operationName && (code === HardwareErrorCode.OperationTimeout || tag === UI_REQUEST_TIMEOUT_TAG)) {
2253
+ throw createHwkError({
2254
+ code: HardwareErrorCode.OperationTimeout,
2255
+ message: `Keystone ${operationName} may have completed before the QR response timed out`,
2256
+ recovery: { scope: "unknown" },
2257
+ params: operationMayHaveCompletedParams(operationName)
2258
+ });
2259
+ }
2260
+ throw error;
2261
+ }
1405
2262
  }
1406
2263
  /**
1407
- * Resolve (syncing over QR if needed) the account cached for `hwkChain` at
1408
- * `syncPath`. Drives the "implicit account sync, then the real request" two
1409
- * hop flow the first time a wallet/path pair is seen; a cache hit skips
1410
- * straight to the caller's own round trip.
2264
+ * Fetch one account's key material from the device for this operation only.
2265
+ * `book` lets earlier fetches of the same operation be reused; nothing is
2266
+ * retained on the record. A failed USB export ends this operation without
2267
+ * replaying the request or switching transports.
1411
2268
  */
1412
- async _ensureAccountSynced(connectId, deviceId, hwkChain, syncPath, signal) {
1413
- const wantMfp = deviceId ? deviceId.toLowerCase() : this._mfpFromConnectId(connectId ?? "");
2269
+ async _fetchAccount(connectId, deviceId, hwkChain, syncPath, signal, options) {
2270
+ const target = this._resolveTarget(connectId, deviceId);
1414
2271
  const key = accountKey(hwkChain, syncPath);
1415
- const existingRecord = wantMfp ? this._devices.get(wantMfp) : void 0;
1416
- const cached = existingRecord?.accounts.get(key);
1417
- if (existingRecord && cached) {
1418
- return { record: existingRecord, account: cached };
2272
+ let existingRecord = target.record;
2273
+ if (!existingRecord) {
2274
+ const attached = await this._tryUsbAttach(target.expectedWalletId);
2275
+ _KeystoneAdapter._throwIfAborted(signal);
2276
+ existingRecord = attached;
1419
2277
  }
2278
+ const book = options?.book;
2279
+ const booked = book?.get(key);
2280
+ if (existingRecord && booked) {
2281
+ return { record: existingRecord, account: booked };
2282
+ }
2283
+ if (existingRecord && !existingRecord.usbSessionId && this._forcedTransport === "usb") {
2284
+ await this._tryUsbAttach(existingRecord.walletId, existingRecord.masterFingerprint, {
2285
+ waitForReenumeration: existingRecord.hadUsbSession
2286
+ });
2287
+ _KeystoneAdapter._throwIfAborted(signal);
2288
+ existingRecord = this._devices.get(existingRecord.walletId) ?? existingRecord;
2289
+ }
2290
+ const useSinglePathUsbExport = this._forcedTransport !== "qr" && Boolean(existingRecord?.usbSessionId);
2291
+ const requestedSchemaPaths = [
2292
+ { hwkChain, path: syncPath }
2293
+ ];
2294
+ const schemaPaths = useSinglePathUsbExport || requestedSchemaPaths.some((schema) => normalizePath(schema.path) === KEYSTONE_WALLET_ID_PATH) ? requestedSchemaPaths : [{ hwkChain: "evm", path: KEYSTONE_WALLET_ID_PATH }, ...requestedSchemaPaths];
1420
2295
  const requestUr = this.urEngine.buildKeyDerivationRequest({
1421
- schemas: [{ path: syncPath, curve: hwkChain === "sol" ? "ed25519" : "secp256k1" }],
2296
+ schemas: schemaPaths.map((s) => ({
2297
+ path: s.path,
2298
+ curve: s.hwkChain === "sol" ? "ed25519" : "secp256k1"
2299
+ })),
1422
2300
  origin: this._origin
1423
2301
  });
1424
- const responseUr = await this._resolveUr(existingRecord, requestUr, false);
2302
+ const resolvedViaUsb = useSinglePathUsbExport;
2303
+ const responseUr = await this._resolveUr(existingRecord, requestUr, false, connectId, signal);
1425
2304
  _KeystoneAdapter._throwIfAborted(signal);
1426
2305
  const parsed = this.urEngine.parseAccountResponse(responseUr);
1427
- if (wantMfp && parsed.masterFingerprint !== wantMfp) {
1428
- throw createHwkError({
1429
- code: HardwareErrorCode.DeviceMismatch,
1430
- message: `Scanned Keystone wallet (mfp ${parsed.masterFingerprint}) does not match the requested device (${wantMfp})`
2306
+ let record;
2307
+ if (resolvedViaUsb && existingRecord) {
2308
+ if (parsed.masterFingerprint !== existingRecord.masterFingerprint) {
2309
+ throw createHwkError({
2310
+ code: HardwareErrorCode.DeviceMismatch,
2311
+ message: "Keystone USB account export returned a different master fingerprint"
2312
+ });
2313
+ }
2314
+ record = existingRecord;
2315
+ } else {
2316
+ this._assertParsedIdentity(parsed, target);
2317
+ record = this._upsertDeviceRecord(parsed, {
2318
+ viaUsb: Boolean(existingRecord?.usbSessionId)
1431
2319
  });
1432
2320
  }
1433
- const record = this._upsertDeviceRecord(parsed, {
1434
- viaUsb: Boolean(existingRecord?.usbSessionId)
1435
- });
1436
2321
  const account = parsed.accounts.find((a) => normalizePath(a.path) === syncPath);
1437
2322
  if (!account) {
1438
2323
  throw createHwkError({
@@ -1441,44 +2326,60 @@ var KeystoneAdapter = class _KeystoneAdapter {
1441
2326
  });
1442
2327
  }
1443
2328
  const entry = { ...account, hwkChain };
1444
- record.accounts.set(key, entry);
2329
+ if (book) {
2330
+ for (const returned of parsed.accounts) {
2331
+ const returnedChain = inferHwkChainFromPath(returned.path);
2332
+ if (returnedChain) {
2333
+ book.set(accountKey(returnedChain, returned.path), {
2334
+ ...returned,
2335
+ hwkChain: returnedChain
2336
+ });
2337
+ }
2338
+ }
2339
+ book.set(key, entry);
2340
+ }
1445
2341
  return { record, account: entry };
1446
2342
  }
1447
2343
  /**
1448
- * Like `_ensureAccountSynced`, but for operations (PSBT signing, master
2344
+ * Like `_fetchAccount`, but for operations (PSBT signing, master
1449
2345
  * fingerprint) that only need to know WHICH wallet is attached, not a
1450
2346
  * specific cached path. Syncs the account-level path for `chain` as a
1451
- * throwaway probe when the mfp isn't already known.
2347
+ * throwaway probe when the wallet record isn't already known.
1452
2348
  *
1453
2349
  * `CHAIN_FINGERPRINT_PATHS[chain]` is a 5-segment LEAF path for `evm`
1454
2350
  * (`m/44'/60'/0'/0/0`) — sending that verbatim as a KeyDerivation request
1455
2351
  * asks Keystone for a non-standard path. Keystone's own docs
1456
2352
  * (dev.keyst.one's multichain KeyDerivation example) show the ETH
1457
2353
  * account-level path as `m/44'/60'/0'` (3 segments), same as what
1458
- * `DEFAULT_IMPORT_SCHEMAS`/`_ensureAccountSynced` already request — so
2354
+ * `_fetchAccount` already requests — so
1459
2355
  * truncate through `splitAccountPath` here too instead of using the raw
1460
2356
  * fingerprint leaf path. `btc`/`sol` are already 3-segment account paths
1461
2357
  * and pass through unchanged.
1462
2358
  */
1463
- async _ensureMfpKnown(connectId, deviceId, chain, signal) {
1464
- const wantMfp = deviceId ? deviceId.toLowerCase() : this._mfpFromConnectId(connectId ?? "");
1465
- const existing = wantMfp ? this._devices.get(wantMfp) : void 0;
1466
- if (existing) return { record: existing };
2359
+ async _ensureWalletKnown(connectId, deviceId, chain, signal) {
2360
+ const target = this._resolveTarget(connectId, deviceId);
2361
+ if (target.record) return { record: target.record };
2362
+ const attached = await this._tryUsbAttach(target.expectedWalletId);
2363
+ _KeystoneAdapter._throwIfAborted(signal);
2364
+ if (attached) return { record: attached };
1467
2365
  const { accountPath } = splitAccountPath(CHAIN_FINGERPRINT_PATHS[chain]);
2366
+ const schemas = accountPath === KEYSTONE_WALLET_ID_PATH ? [{ path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" }] : [
2367
+ { path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" },
2368
+ {
2369
+ path: accountPath,
2370
+ curve: chain === "sol" ? "ed25519" : "secp256k1"
2371
+ }
2372
+ ];
1468
2373
  const requestUr = this.urEngine.buildKeyDerivationRequest({
1469
- schemas: [{ path: accountPath, curve: chain === "sol" ? "ed25519" : "secp256k1" }],
2374
+ schemas,
1470
2375
  origin: this._origin
1471
2376
  });
1472
- const responseUr = await this._resolveUr(void 0, requestUr, false);
2377
+ const responseUr = await this._resolveUr(void 0, requestUr, false, connectId, signal);
1473
2378
  _KeystoneAdapter._throwIfAborted(signal);
1474
2379
  const parsed = this.urEngine.parseAccountResponse(responseUr);
1475
- if (wantMfp && parsed.masterFingerprint !== wantMfp) {
1476
- throw createHwkError({
1477
- code: HardwareErrorCode.DeviceMismatch,
1478
- message: `Scanned Keystone wallet (mfp ${parsed.masterFingerprint}) does not match the requested device (${wantMfp})`
1479
- });
1480
- }
1481
- return { record: this._upsertDeviceRecord(parsed) };
2380
+ this._assertParsedIdentity(parsed, target);
2381
+ const record = this._upsertDeviceRecord(parsed);
2382
+ return { record };
1482
2383
  }
1483
2384
  async _requestQrDisplayAndAwaitResponse(device, data) {
1484
2385
  const waitPromise = this._uiRegistry.wait(
@@ -1504,6 +2405,93 @@ var KeystoneAdapter = class _KeystoneAdapter {
1504
2405
  const response = await waitPromise;
1505
2406
  return { urType: response.urType, urData: response.urData };
1506
2407
  }
2408
+ async _callUsbConnector(sessionId, method, params, signal) {
2409
+ if (!this._usbConnector) {
2410
+ throw createHwkError({
2411
+ code: HardwareErrorCode.TransportNotAvailable,
2412
+ message: "No USB connector configured for this Keystone adapter"
2413
+ });
2414
+ }
2415
+ if (this._pendingUsbTeardowns > 0 || this._unsettledUsbOperations.size > 0) {
2416
+ throw createHwkError({
2417
+ code: HardwareErrorCode.DeviceBusyInternal,
2418
+ message: `Keystone USB is busy while calling ${method}`
2419
+ });
2420
+ }
2421
+ const releaseOperation = this._retainUsbOperation(`call:${sessionId}`);
2422
+ let rawCall;
2423
+ try {
2424
+ const connector = this._usbConnector;
2425
+ rawCall = connector.call(sessionId, method, params).finally(releaseOperation);
2426
+ } catch (error) {
2427
+ releaseOperation();
2428
+ throw error;
2429
+ }
2430
+ return signal ? _KeystoneAdapter._abortable(signal, rawCall) : rawCall;
2431
+ }
2432
+ _runUsbTeardown(task) {
2433
+ const previous = this._usbTeardownTail;
2434
+ let releaseTail = () => void 0;
2435
+ this._usbTeardownTail = new Promise((resolve) => {
2436
+ releaseTail = resolve;
2437
+ });
2438
+ this._pendingUsbTeardowns += 1;
2439
+ return (async () => {
2440
+ try {
2441
+ await previous;
2442
+ await this._waitForUsbOperationsToDrain();
2443
+ await task();
2444
+ } finally {
2445
+ this._pendingUsbTeardowns -= 1;
2446
+ releaseTail();
2447
+ }
2448
+ })();
2449
+ }
2450
+ _waitForUsbOperationsToDrain() {
2451
+ if (this._unsettledUsbOperations.size === 0) return Promise.resolve();
2452
+ return new Promise((resolve) => {
2453
+ this._usbIdleWaiters.add(resolve);
2454
+ });
2455
+ }
2456
+ _retainUsbOperation(key) {
2457
+ this._unsettledUsbOperations.set(key, (this._unsettledUsbOperations.get(key) ?? 0) + 1);
2458
+ let released = false;
2459
+ return () => {
2460
+ if (released) return;
2461
+ released = true;
2462
+ const remaining = (this._unsettledUsbOperations.get(key) ?? 1) - 1;
2463
+ if (remaining > 0) {
2464
+ this._unsettledUsbOperations.set(key, remaining);
2465
+ } else {
2466
+ this._unsettledUsbOperations.delete(key);
2467
+ }
2468
+ if (this._unsettledUsbOperations.size === 0) {
2469
+ for (const resolve of this._usbIdleWaiters) resolve();
2470
+ this._usbIdleWaiters.clear();
2471
+ }
2472
+ };
2473
+ }
2474
+ static _abortable(signal, promise) {
2475
+ if (signal.aborted) {
2476
+ return Promise.reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted"));
2477
+ }
2478
+ return new Promise((resolve, reject) => {
2479
+ const onAbort = () => {
2480
+ reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted"));
2481
+ };
2482
+ signal.addEventListener("abort", onAbort, { once: true });
2483
+ promise.then(
2484
+ (value) => {
2485
+ signal.removeEventListener("abort", onAbort);
2486
+ resolve(value);
2487
+ },
2488
+ (error) => {
2489
+ signal.removeEventListener("abort", onAbort);
2490
+ reject(error);
2491
+ }
2492
+ );
2493
+ });
2494
+ }
1507
2495
  static _throwIfAborted(signal) {
1508
2496
  if (signal.aborted) {
1509
2497
  throw signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
@@ -1529,18 +2517,25 @@ var KeystoneAdapter = class _KeystoneAdapter {
1529
2517
  if (err && typeof err === "object") {
1530
2518
  const e = err;
1531
2519
  if (typeof e.code === "number") {
1532
- return failure(e.code, e.message ?? "Unknown error", e.params);
2520
+ const { origin } = e;
2521
+ return failure(
2522
+ e.code,
2523
+ e.message ?? "Unknown error",
2524
+ e.params,
2525
+ origin === "device" || origin === "transport" || origin === "host" ? origin : void 0,
2526
+ isHwkRecoveryHint(e.recovery) ? e.recovery : void 0
2527
+ );
1533
2528
  }
1534
2529
  if (e._tag === UI_REQUEST_CANCELLED_TAG || e._tag === UI_REQUEST_PREEMPTED_TAG) {
1535
2530
  return failure(
1536
2531
  HardwareErrorCode.UserAborted,
1537
- e.message ?? "Keystone QR interaction was cancelled"
2532
+ e.message ?? "Keystone QR operation was cancelled"
1538
2533
  );
1539
2534
  }
1540
2535
  if (e._tag === UI_REQUEST_TIMEOUT_TAG) {
1541
2536
  return failure(
1542
2537
  HardwareErrorCode.OperationTimeout,
1543
- e.message ?? "Keystone QR interaction timed out"
2538
+ e.message ?? "Keystone QR operation timed out"
1544
2539
  );
1545
2540
  }
1546
2541
  }
@@ -1550,16 +2545,18 @@ var KeystoneAdapter = class _KeystoneAdapter {
1550
2545
  }
1551
2546
  };
1552
2547
  export {
2548
+ KEYSTONE_WALLET_CONNECT_ID_PREFIX,
2549
+ KEYSTONE_WALLET_ID_PATH,
1553
2550
  KeystoneAdapter,
1554
2551
  KeystoneUrEngine,
1555
- QR_CONNECT_ID_PREFIX,
1556
2552
  accountKey,
1557
2553
  btcScriptTypeFromPath,
1558
2554
  createDeviceRecord,
2555
+ deriveKeystoneWalletId,
1559
2556
  normalizePath,
1560
2557
  placeholderDeviceInfo,
1561
- qrConnectId,
1562
2558
  splitAccountPath,
1563
- toDeviceInfo
2559
+ toDeviceInfo,
2560
+ walletConnectId
1564
2561
  };
1565
2562
  //# sourceMappingURL=index.mjs.map