@onekeyfe/hwk-keystone-adapter 1.2.3-alpha.8 → 1.2.5-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -106,6 +106,9 @@ var TronSignature = class _TronSignature extends RegistryItem2 {
106
106
  static fromDataItem(dataItem) {
107
107
  const map = dataItem.getData();
108
108
  const signature = map[2 /* signature */];
109
+ if (!Buffer.isBuffer(signature)) {
110
+ throw new Error("Keystone tron-signature UR is missing its signature bytes");
111
+ }
109
112
  const requestIdItem = map[1 /* requestId */];
110
113
  const requestId = requestIdItem?.getData();
111
114
  return new _TronSignature(signature, requestId);
@@ -136,27 +139,48 @@ function toSdkUr(ur) {
136
139
  function fromSdkUr(ur) {
137
140
  return { urType: ur.type, urData: ur.cbor.toString("hex") };
138
141
  }
139
- function splitSignature65(hex) {
140
- if (hex.length !== 130) {
142
+ function splitEvmSignature(hex) {
143
+ if (hex.length < 130 || hex.length % 2 !== 0) {
141
144
  throw new Error(
142
- `Keystone returned a ${hex.length / 2}-byte signature; expected 65 bytes (r|s|v)`
145
+ `Keystone returned a ${hex.length / 2}-byte signature; expected at least 65 bytes (r|s|v)`
143
146
  );
144
147
  }
145
148
  return { r: hex.slice(0, 64), s: hex.slice(64, 128), v: hex.slice(128) };
146
149
  }
150
+ function requireSignatureBytes(hex, expectedBytes, chain) {
151
+ if (hex.length !== expectedBytes * 2) {
152
+ throw new Error(
153
+ `Keystone returned a ${hex.length / 2}-byte ${chain} signature; expected ${expectedBytes} bytes`
154
+ );
155
+ }
156
+ return hex;
157
+ }
158
+ var MISSING_MASTER_FINGERPRINT = "00000000";
147
159
  function requireBip32MasterFingerprint(value) {
148
160
  const fingerprint = parseBip32MasterFingerprint(value);
149
- if (!fingerprint) {
161
+ if (!fingerprint || fingerprint === MISSING_MASTER_FINGERPRINT) {
150
162
  throw new Error("Keystone master fingerprint must be exactly 4 bytes (8 hex characters)");
151
163
  }
152
164
  return fingerprint;
153
165
  }
166
+ function toParsedAccount(key) {
167
+ return {
168
+ chain: key.chain,
169
+ path: key.path,
170
+ publicKey: key.publicKey,
171
+ extendedPublicKey: key.extendedPublicKey,
172
+ xfp: key.xfp ? requireBip32MasterFingerprint(key.xfp) : void 0,
173
+ name: key.name
174
+ };
175
+ }
176
+ function relativeHdPath(relativeDerivePath) {
177
+ return `m/${relativeDerivePath.replace(/^m\//i, "")}`;
178
+ }
154
179
  var KeystoneUrEngine = class {
155
180
  constructor(origin = "OneKey") {
156
181
  this.sdk = new KeystoneSDK({ origin });
157
182
  }
158
- // --- Account sync (works for both a device-initiated QR export and a
159
- // wallet-initiated KeyDerivation request/response over either channel) ---
183
+ // --- Account sync (device-initiated export or KeyDerivation response) ---
160
184
  parseMultiAccounts(ur) {
161
185
  const parsed = this.sdk.parseMultiAccounts(toSdkUr(ur));
162
186
  return {
@@ -164,46 +188,15 @@ var KeystoneUrEngine = class {
164
188
  device: parsed.device,
165
189
  deviceId: parsed.deviceId,
166
190
  deviceVersion: parsed.deviceVersion,
167
- accounts: parsed.keys.map(
168
- (key) => ({
169
- chain: key.chain,
170
- path: key.path,
171
- publicKey: key.publicKey,
172
- extendedPublicKey: key.extendedPublicKey,
173
- xfp: key.xfp ? requireBip32MasterFingerprint(key.xfp) : void 0,
174
- name: key.name
175
- })
176
- )
191
+ accounts: parsed.keys.map(toParsedAccount)
177
192
  };
178
193
  }
179
194
  parseHDKey(ur) {
180
- const key = this.sdk.parseHDKey(toSdkUr(ur));
181
- return {
182
- chain: key.chain,
183
- path: key.path,
184
- publicKey: key.publicKey,
185
- extendedPublicKey: key.extendedPublicKey,
186
- xfp: key.xfp ? requireBip32MasterFingerprint(key.xfp) : void 0,
187
- name: key.name
188
- };
195
+ return toParsedAccount(this.sdk.parseHDKey(toSdkUr(ur)));
189
196
  }
190
197
  /**
191
- * Build a `qr-hardware-call` (KeyDerivation) request: the host asks for
192
- * specific paths instead of waiting for whatever the device happens to be
193
- * showing. The device replies with a `crypto-multi-accounts` UR — parse it
194
- * with `parseMultiAccounts`. Used for the implicit "sync this wallet's xfp
195
- * before the first sign" round trip as well as an explicit account import.
196
- *
197
- * `version: V1` is required — verified against real Keystone hardware and
198
- * `keystone3-firmware`'s `CheckHardwareCallRequestIsLegal` source: an
199
- * unversioned/V0 request is validated as a legacy Cardano-only request
200
- * (`m/1852'/1815'/...`) and firmware rejects every other chain's path with
201
- * `PRS_PARSING_ERROR` (device-shown message: "路径不受支持" / "path not
202
- * supported"), regardless of the path's shape. V1 is what actually enables
203
- * the general per-chain path whitelist (includes `m/44'/60'` for ETH, the
204
- * standard BTC purposes, etc.). The SDK itself defaults to V0 unless a
205
- * truthy `version` is passed — omitting this silently produces a request
206
- * every non-Cardano device rejects.
198
+ * Builds a KeyDerivation request. `version: V1` is required: firmware validates V0 (the SDK
199
+ * default) as Cardano-only and rejects other paths with `PRS_PARSING_ERROR`.
207
200
  */
208
201
  buildKeyDerivationRequest(input) {
209
202
  const ur = this.sdk.generateKeyDerivationCall({
@@ -217,14 +210,8 @@ var KeystoneUrEngine = class {
217
210
  return fromSdkUr(ur);
218
211
  }
219
212
  /**
220
- * Parse the response to a KeyDerivation request (or a device-initiated
221
- * account export): `crypto-multi-accounts` for a multi-schema request,
222
- * `crypto-hdkey` for a single-key response some firmware paths use instead.
223
- * Both are normalized to the same `KeystoneParsedMultiAccounts` shape — a
224
- * single `crypto-hdkey` becomes a one-entry account list, with its own
225
- * `origin.sourceFingerprint` promoted to `masterFingerprint` (correct for a
226
- * key derived directly from the seed, which every request this engine
227
- * builds asks for).
213
+ * Normalizes `crypto-multi-accounts` and the single-key `crypto-hdkey` some firmware returns. The
214
+ * hdkey source fingerprint is the mfp only because every request here derives from the seed.
228
215
  */
229
216
  parseAccountResponse(ur) {
230
217
  if (ur.urType === "crypto-hdkey") {
@@ -255,37 +242,19 @@ var KeystoneUrEngine = class {
255
242
  }
256
243
  parseEthSignature(ur) {
257
244
  const signature = this.sdk.eth.parseSignature(toSdkUr(ur));
258
- return { requestId: signature.requestId, ...splitSignature65(signature.signature) };
245
+ return { requestId: signature.requestId, ...splitEvmSignature(signature.signature) };
259
246
  }
260
- /**
261
- * Derive one EVM address offline from an already-synced account xpub —
262
- * verified against the same `@keystonehq/bc-ur-registry-eth` helper the
263
- * Keystone-based OneKey air-gap demo uses in production
264
- * (`generateAddressFromXpub`), so no unverified assumption about what a
265
- * leaf-path KeyDerivation request would return. `relativeDerivePath` is
266
- * relative to the xpub's own depth, e.g. `'0/0'` for an account xpub.
267
- */
247
+ /** Derives an EVM address offline; `relativeDerivePath` is relative to the xpub, e.g. `'0/0'`. */
268
248
  deriveEvmAddressFromXpub(xpub, relativeDerivePath) {
269
- return generateAddressFromXpub(xpub, `m/${relativeDerivePath.replace(/^m\//i, "")}`);
249
+ return generateAddressFromXpub(xpub, relativeHdPath(relativeDerivePath));
270
250
  }
271
251
  // --- BTC (PSBT transaction signing + plain message signing) ---
272
252
  /**
273
- * Derive one BTC address offline from an already-synced account xpub, the
274
- * same way `deriveEvmAddressFromXpub` does. Keystone's `CryptoHDKey`
275
- * always emits standard mainnet-xpub version bytes (`0488B21E`) regardless
276
- * of the account's purpose/script type (verified against
277
- * `@keystonehq/bc-ur-registry`'s `CryptoHDKey.getBip32Key`, which hardcodes
278
- * that version rather than switching to a SLIP-132 ypub/zpub prefix per
279
- * script type) — so `hdkey.fromExtendedKey` parses it correctly for every
280
- * `scriptType` without needing custom version bytes configured.
281
- *
282
- * `p2tr` is deliberately not handled: taproot output-key tweaking (BIP-341)
283
- * needs an elliptic-curve library wired via bitcoinjs-lib's `initEccLib`,
284
- * which this package doesn't set up yet — every other payment function
285
- * here needs no such library.
253
+ * `CryptoHDKey` always emits mainnet xpub version bytes (`0488B21E`), so `hdkey` parses it as is.
254
+ * `p2tr` needs an ECC lib for BIP-341 tweaking, not wired here.
286
255
  */
287
256
  deriveBtcAddressFromXpub(xpub, relativeDerivePath, scriptType) {
288
- const node = HDKey.fromExtendedKey(xpub).derive(`m/${relativeDerivePath.replace(/^m\//i, "")}`);
257
+ const node = HDKey.fromExtendedKey(xpub).derive(relativeHdPath(relativeDerivePath));
289
258
  if (!node.publicKey) throw new Error("HDKey derivation did not produce a public key");
290
259
  return this.deriveBtcAddressFromPublicKey(
291
260
  Buffer.from(node.publicKey).toString("hex"),
@@ -338,7 +307,7 @@ var KeystoneUrEngine = class {
338
307
  requestId: params.requestId,
339
308
  signData: params.messageHex,
340
309
  dataType: 1,
341
- // BtcSignRequest.DataType.message — PSBT signing never goes through this path.
310
+ // BtcSignRequest.DataType.message; PSBT signing never goes through this path.
342
311
  accounts: params.accounts,
343
312
  origin: params.origin
344
313
  });
@@ -367,7 +336,10 @@ var KeystoneUrEngine = class {
367
336
  }
368
337
  parseSolSignature(ur) {
369
338
  const signature = this.sdk.sol.parseSignature(toSdkUr(ur));
370
- return { requestId: signature.requestId, signature: signature.signature };
339
+ return {
340
+ requestId: signature.requestId,
341
+ signature: requireSignatureBytes(signature.signature, 64, "SOL")
342
+ };
371
343
  }
372
344
  // --- TRON ---
373
345
  /**
@@ -392,37 +364,22 @@ var KeystoneUrEngine = class {
392
364
  const requestId = signature.getRequestId();
393
365
  return {
394
366
  requestId: requestId ? uuidStringify(requestId) : void 0,
395
- signature: signature.getSignature().toString("hex")
367
+ signature: requireSignatureBytes(signature.getSignature().toString("hex"), 65, "TRON")
396
368
  };
397
369
  }
398
370
  /**
399
- * Derive one TRON address offline from an already-synced account xpub.
400
- * TRON reuses EVM's exact secp256k1-pubkey → keccak256 → last-20-bytes
401
- * derivation (verified against Keystone's own `formatAddress()` in
402
- * `keystone-sdk`'s TRON chain source) — only the final text encoding
403
- * differs (base58check with a `0x41` version byte, not checksummed hex).
404
- * Reusing `generateAddressFromXpub` here means no new hashing dependency:
405
- * strip its "0x" and re-encode the same 20 bytes.
371
+ * TRON uses the same 20 address bytes as EVM (Keystone's `formatAddress()`), re-encoded as
372
+ * base58check with the `0x41` version byte.
406
373
  */
407
374
  deriveTronAddressFromXpub(xpub, relativeDerivePath) {
408
- const evmStyleHex = generateAddressFromXpub(
409
- xpub,
410
- `m/${relativeDerivePath.replace(/^m\//i, "")}`
411
- );
375
+ const evmStyleHex = generateAddressFromXpub(xpub, relativeHdPath(relativeDerivePath));
412
376
  const addressBytes = Buffer.concat([
413
377
  Buffer.from([TRON_ADDRESS_PREFIX]),
414
- Buffer.from(evmStyleHex.replace(/^0x/i, ""), "hex")
378
+ Buffer.from(stripHexPrefix(evmStyleHex), "hex")
415
379
  ]);
416
380
  return bs58check.encode(addressBytes);
417
381
  }
418
- /**
419
- * Split an account-level xpub back into the BIP-32 fields a host needs to
420
- * treat it as a real extended key (`BtcPublicKey`). Everything here is
421
- * carried inside the xpub's own serialization — depth, the PARENT key
422
- * fingerprint (not the seed's master fingerprint), the chain code and the
423
- * compressed public key — so this is a pure decode with no device round
424
- * trip; the xpub itself was already device-verified when it was synced.
425
- */
382
+ /** Decodes an xpub's BIP-32 fields; `parentFingerprint` is the parent key's, not the mfp. */
426
383
  parseXpubMeta(xpub) {
427
384
  const raw = Buffer.from(bs58check.decode(xpub));
428
385
  if (raw.length !== 78) {
@@ -467,7 +424,7 @@ import {
467
424
  isHwkRecoveryHint,
468
425
  isUserRefusal,
469
426
  operationMayHaveCompletedParams,
470
- parseBip32MasterFingerprint as parseBip32MasterFingerprint2,
427
+ parseBip32MasterFingerprint as parseBip32MasterFingerprint3,
471
428
  rehydrateConnectorError,
472
429
  resolveHardwareOperationTarget as resolveGenericHardwareOperationTarget,
473
430
  runAllNetworkGetAddress,
@@ -475,9 +432,6 @@ import {
475
432
  success
476
433
  } from "@onekeyfe/hwk-adapter-core";
477
434
 
478
- // src/adapter/deviceTable.ts
479
- import { deriveWalletId } from "@onekeyfe/hwk-adapter-core";
480
-
481
435
  // src/adapter/pathUtils.ts
482
436
  function normalizePath(path) {
483
437
  const trimmed = path.trim();
@@ -522,31 +476,17 @@ var KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE = "Keystone BTC is limited to mainnet
522
476
 
523
477
  // src/adapter/deviceTable.ts
524
478
  var KEYSTONE_WALLET_CONNECT_ID_PREFIX = "keystone-wallet:";
525
- var KEYSTONE_WALLET_ID_PATH = "m/44'/60'/0'";
526
- function walletConnectId(walletId) {
527
- return `${KEYSTONE_WALLET_CONNECT_ID_PREFIX}${walletId}`;
479
+ var KEYSTONE_COLD_START_PATH = "m/44'/60'/0'";
480
+ function walletConnectId(masterFingerprint) {
481
+ return `${KEYSTONE_WALLET_CONNECT_ID_PREFIX}${masterFingerprint}`;
528
482
  }
529
483
  function accountKey(hwkChain, path) {
530
484
  return `${hwkChain}:${normalizePath(path)}`;
531
485
  }
532
- function deriveKeystoneWalletId(accounts) {
533
- const identityAccount = accounts.find(
534
- (account) => normalizePath(account.path) === KEYSTONE_WALLET_ID_PATH
535
- );
536
- if (!identityAccount?.extendedPublicKey) {
537
- throw new Error(
538
- `Keystone identity response is missing the extended public key at ${KEYSTONE_WALLET_ID_PATH}`
539
- );
540
- }
541
- return deriveWalletId(
542
- `keystone:secp256k1:${KEYSTONE_WALLET_ID_PATH}:${identityAccount.extendedPublicKey.trim()}`
543
- );
544
- }
545
- function createDeviceRecord(walletId, masterFingerprint) {
486
+ function createDeviceRecord(masterFingerprint) {
546
487
  return {
547
- walletId,
548
488
  masterFingerprint,
549
- connectId: walletConnectId(walletId),
489
+ connectId: walletConnectId(masterFingerprint),
550
490
  importedAt: Date.now()
551
491
  };
552
492
  }
@@ -561,7 +501,7 @@ function toDeviceInfo(record) {
561
501
  model: record.model ?? "unknown",
562
502
  modelName: record.model,
563
503
  firmwareVersion: record.deviceVersion ?? "0.0.0",
564
- deviceId: record.walletId,
504
+ deviceId: record.masterFingerprint,
565
505
  connectId: record.connectId,
566
506
  connectionType: record.usbSessionId ? "usb" : "qr",
567
507
  capabilities: CAPABILITIES,
@@ -585,22 +525,22 @@ function placeholderDeviceInfo() {
585
525
  }
586
526
 
587
527
  // src/utils/queueKey.ts
588
- import { isHardwareOperationId } from "@onekeyfe/hwk-adapter-core";
528
+ import { isHardwareOperationId, parseBip32MasterFingerprint as parseBip32MasterFingerprint2 } from "@onekeyfe/hwk-adapter-core";
589
529
  var KEYSTONE_COLD_START_JOB_LABEL = "keystone-cold-start";
590
530
  function keystoneQueueKey(connectId, deviceId) {
591
531
  return isHardwareOperationId(connectId) ? connectId : deviceId || connectId || KEYSTONE_COLD_START_JOB_LABEL;
592
532
  }
593
- function keystoneWalletIdFromIdentifier(identifier) {
533
+ function keystoneMfpFromIdentifier(identifier) {
594
534
  const value = identifier.startsWith(KEYSTONE_WALLET_CONNECT_ID_PREFIX) ? identifier.slice(KEYSTONE_WALLET_CONNECT_ID_PREFIX.length) : identifier;
595
- return /^[0-9a-f]{64}$/i.test(value) ? value.toLowerCase() : void 0;
535
+ return parseBip32MasterFingerprint2(value);
596
536
  }
597
537
  function keystoneCancelQueueKeys(identifiers) {
598
538
  const keys = /* @__PURE__ */ new Set();
599
539
  for (const identifier of identifiers) {
600
540
  if (identifier) {
601
541
  keys.add(identifier);
602
- const walletId = keystoneWalletIdFromIdentifier(identifier);
603
- if (walletId) keys.add(walletId);
542
+ const masterFingerprint = keystoneMfpFromIdentifier(identifier);
543
+ if (masterFingerprint) keys.add(masterFingerprint);
604
544
  }
605
545
  }
606
546
  return keys;
@@ -610,8 +550,10 @@ function keystoneCancelQueueKeys(identifiers) {
610
550
  var KEYSTONE_USB_REATTACH_PROBE_ATTEMPTS = 4;
611
551
  var KEYSTONE_USB_REATTACH_PROBE_INTERVAL_MS = 500;
612
552
  function resolveHardwareOperationTarget(positionalTargetId, operationId) {
613
- const result = resolveGenericHardwareOperationTarget(positionalTargetId, operationId, "keystone");
614
- return result;
553
+ return resolveGenericHardwareOperationTarget(positionalTargetId, operationId, "keystone");
554
+ }
555
+ function keySchema(chain, path) {
556
+ return { path, curve: chain === "sol" ? "ed25519" : "secp256k1" };
615
557
  }
616
558
  function waitForKeystoneUsbReattachProbe() {
617
559
  return new Promise((resolve) => {
@@ -665,19 +607,19 @@ var KeystoneAdapter = class _KeystoneAdapter {
665
607
  this._usbConnectTail = Promise.resolve();
666
608
  this._unsettledUsbOperations = /* @__PURE__ */ new Map();
667
609
  this._usbIdleWaiters = /* @__PURE__ */ new Set();
610
+ this._abandonedUsbConnects = 0;
668
611
  this._usbTeardownTail = Promise.resolve();
669
612
  this._pendingUsbTeardowns = 0;
670
613
  this._handleUsbDisconnect = ({ connectId }) => {
671
614
  const record = Array.from(this._devices.values()).find((item) => item.usbSessionId === connectId);
672
615
  if (!record) return;
673
616
  this._operations.endByConnectionKey(connectId, "disconnect");
674
- this._operations.endByConnectionKey(record.connectId, "disconnect");
675
617
  record.usbSessionId = void 0;
676
618
  const info = toDeviceInfo(record);
677
619
  if (record.qrSynced) {
678
620
  this.emitter.emit(DEVICE.CHANGED, { type: DEVICE.CHANGED, payload: info });
679
621
  } else {
680
- this._devices.delete(record.walletId);
622
+ this._devices.delete(record.masterFingerprint);
681
623
  this.emitter.emit(DEVICE.DISCONNECT, { type: DEVICE.DISCONNECT, payload: info });
682
624
  }
683
625
  };
@@ -685,8 +627,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
685
627
  this.emitter.emit("ui-event", event);
686
628
  };
687
629
  // ---------------------------------------------------------------------------
688
- // All-network bundle — dispatches to the same per-chain methods below, so
689
- // there's exactly one implementation of each chain's address logic.
630
+ // All-network bundle, dispatched to the per-chain methods below.
690
631
  // ---------------------------------------------------------------------------
691
632
  this.allNetworkGetAddress = async (connectId, deviceId, params) => {
692
633
  const operationTarget = resolveHardwareOperationTarget(connectId, params.operationId);
@@ -695,13 +636,12 @@ var KeystoneAdapter = class _KeystoneAdapter {
695
636
  const { operationId } = operationTarget.payload;
696
637
  let cancelScope;
697
638
  try {
698
- const prefetched = await this._prefetchAllNetworkAccounts(
699
- effectiveConnectId,
700
- deviceId,
701
- params
702
- );
639
+ const prefetched = await this._prefetchAllNetworkAccounts(effectiveConnectId, deviceId, {
640
+ ...params,
641
+ operationId
642
+ });
703
643
  const { book } = prefetched;
704
- const itemDeviceId = deviceId || prefetched.walletId || "";
644
+ const itemDeviceId = deviceId || prefetched.masterFingerprint || "";
705
645
  const scope = this._jobQueue.createCancelScope(
706
646
  keystoneQueueKey(effectiveConnectId, itemDeviceId)
707
647
  );
@@ -750,14 +690,13 @@ var KeystoneAdapter = class _KeystoneAdapter {
750
690
  deviceIdentity: record ? {
751
691
  vendor: "keystone",
752
692
  type: "walletId",
753
- value: record.walletId
693
+ value: record.masterFingerprint
754
694
  } : void 0
755
695
  }
756
696
  });
757
697
  },
758
- // The gap check plus the shared top-level table: a refusal or a lost
759
- // session ends the batch, every other per-item failure keeps its
760
- // existing "carry on with the next chain" behaviour.
698
+ // Cancel, refusal or a lost session ends the batch; other per-item
699
+ // failures carry on with the next chain.
761
700
  shouldAbortBundle: (response, { index }) => {
762
701
  const { code } = response.payload ?? {};
763
702
  return cancelledIndexes.has(index) || isUserRefusal(code) || isConnectionLost(code);
@@ -788,7 +727,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
788
727
  getAvailableTransports() {
789
728
  return this._usbConnector ? ["qr", "usb"] : ["qr"];
790
729
  }
791
- /** Pins subsequent calls to 'qr' or 'usb' (routing otherwise defaults to "USB when the target wallet has a live session, else QR" — see `_resolveUr`). Any other value clears the pin back to auto. */
730
+ /** Pins calls to 'qr' or 'usb'; any other value clears the pin back to auto routing. */
792
731
  switchTransport(type) {
793
732
  this._forcedTransport = type === "qr" || type === "usb" ? type : void 0;
794
733
  return Promise.resolve();
@@ -801,6 +740,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
801
740
  this._uiRegistry.cancel();
802
741
  this._jobQueue.clear();
803
742
  this._searchDeviceTargets.clear();
743
+ await this._usbConnectTail;
804
744
  await this._resetUsbSessions();
805
745
  this._usbConnector?.off("device-disconnect", this._handleUsbDisconnect);
806
746
  this._usbConnector?.off("ui-event", this._handleUsbUiEvent);
@@ -812,14 +752,8 @@ var KeystoneAdapter = class _KeystoneAdapter {
812
752
  // Device table
813
753
  // ---------------------------------------------------------------------------
814
754
  /**
815
- * Unscoped discovery includes QR-synced wallets already known to this
816
- * adapter. Explicit QR discovery returns a virtual connection target because
817
- * there is no physical descriptor to enumerate. When a USB connector is
818
- * configured, its raw scan results are appended as-is: a USB descriptor has
819
- * no mfp until `connectDevice()` actually opens+claims it (see
820
- * `KeystoneUsbConnectorBase.searchDevices`), so these entries carry an
821
- * empty `deviceId` and exist purely so a host can list "plugged in, click
822
- * to connect" candidates.
755
+ * Returns known wallets plus USB scan results; USB entries have no mfp until `connectDevice()`
756
+ * claims them. Explicit QR discovery returns one virtual target.
823
757
  */
824
758
  async searchDevices(options) {
825
759
  const searchGeneration = /* @__PURE__ */ Symbol("keystone-device-search");
@@ -855,18 +789,15 @@ var KeystoneAdapter = class _KeystoneAdapter {
855
789
  if (this._activeSearchGeneration !== searchGeneration) return [];
856
790
  const placeholders = usbDevices.map((d) => ({
857
791
  vendor: "keystone",
858
- // Empty, not 'unknown': enumeration genuinely cannot tell the model
859
- // apart (every Keystone shares one vid/pid), and a literal placeholder
860
- // string would beat the host's own default-name fallback and surface as
861
- // a device called "unknown".
792
+ // Empty, not 'unknown': all models share one vid/pid, and a placeholder
793
+ // string would beat the host's default-name fallback.
862
794
  model: d.model ?? "",
863
795
  modelName: d.modelName,
864
- // Placeholders — neither is knowable until the device is opened.
796
+ // Unknown until the device is opened.
865
797
  firmwareVersion: "0.0.0",
866
798
  deviceId: d.deviceId,
867
799
  connectId: d.connectId,
868
800
  connectionType: "usb",
869
- // The USB product name, which IS readable at enumeration time.
870
801
  label: d.name,
871
802
  serialNumber: d.serialNumber,
872
803
  capabilities: d.capabilities ?? { persistentDeviceIdentity: false },
@@ -886,20 +817,25 @@ var KeystoneAdapter = class _KeystoneAdapter {
886
817
  Array.from(this._devices.values()).map((record) => record.usbSessionId).filter((sessionId) => Boolean(sessionId))
887
818
  );
888
819
  for (const sessionId of sessionIds) {
889
- try {
890
- await this._usbConnector.disconnect(sessionId);
891
- } catch {
892
- }
893
- this._handleUsbDisconnect({ connectId: sessionId });
820
+ await this._retireUsbSession(sessionId);
894
821
  }
895
822
  });
896
823
  }
824
+ /** Best-effort disconnect; the session is retired either way so it cannot be selected again. */
825
+ async _retireUsbSession(sessionId) {
826
+ if (this._usbConnector) {
827
+ try {
828
+ await this._usbConnector.disconnect(sessionId);
829
+ } catch {
830
+ }
831
+ }
832
+ this._handleUsbDisconnect({ connectId: sessionId });
833
+ }
897
834
  async searchDeviceTargets(options) {
898
835
  const devices = await this.searchDevices(options);
899
836
  return devices.map((device) => ({
900
837
  searchTargetId: device.connectId,
901
- // Keystone's wallet id is stable only after the USB/QR handshake. The
902
- // selectable handle remains owned by the current discovery snapshot.
838
+ // The master fingerprint is known only after the USB/QR handshake.
903
839
  searchTargetReusePolicy: "current-discovery",
904
840
  vendor: device.vendor,
905
841
  connectionType: device.connectionType,
@@ -918,13 +854,23 @@ var KeystoneAdapter = class _KeystoneAdapter {
918
854
  }));
919
855
  }
920
856
  async connectDevice(searchTargetId) {
857
+ const scope = this._jobQueue.createCancelScope(searchTargetId);
921
858
  try {
922
859
  let connected;
923
860
  let selectedConnectionType;
924
861
  const discoveredConnectionType = this._searchDeviceTargets.get(searchTargetId);
925
862
  if (discoveredConnectionType === "qr") {
926
863
  selectedConnectionType = "qr";
927
- connected = await this.importFromQr();
864
+ const abortColdStart = () => this._jobQueue.cancelActiveAndPending(
865
+ KEYSTONE_COLD_START_JOB_LABEL,
866
+ scope.signal.reason
867
+ );
868
+ scope.signal.addEventListener("abort", abortColdStart, { once: true });
869
+ try {
870
+ connected = await this.importFromQr();
871
+ } finally {
872
+ scope.signal.removeEventListener("abort", abortColdStart);
873
+ }
928
874
  } else if (discoveredConnectionType === "usb") {
929
875
  selectedConnectionType = "usb";
930
876
  if (!this._usbConnector) {
@@ -933,7 +879,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
933
879
  "No USB connector configured for this Keystone adapter"
934
880
  );
935
881
  }
936
- connected = await this._connectUsb({}, searchTargetId);
882
+ connected = await this._connectUsb({}, searchTargetId, scope.signal);
937
883
  } else if (hasHardwareRuntimeIdPrefix(searchTargetId)) {
938
884
  return failure(
939
885
  HardwareErrorCode.DeviceNotFound,
@@ -954,9 +900,10 @@ var KeystoneAdapter = class _KeystoneAdapter {
954
900
  );
955
901
  } else {
956
902
  selectedConnectionType = "usb";
957
- connected = await this._connectUsb(target);
903
+ connected = await this._connectUsb(target, void 0, scope.signal);
958
904
  }
959
905
  }
906
+ _KeystoneAdapter._throwIfAborted(scope.signal);
960
907
  if (!connected.success) return connected;
961
908
  const record = this._devices.get(connected.payload.deviceId);
962
909
  this._operations.endByConnectionKey(connected.payload.connectId, "explicit");
@@ -974,14 +921,13 @@ var KeystoneAdapter = class _KeystoneAdapter {
974
921
  return success(operation.operationId);
975
922
  } catch (err) {
976
923
  return this._errorToFailure(err);
924
+ } finally {
925
+ scope.release();
977
926
  }
978
927
  }
979
928
  /**
980
- * QR has no persistent connection to tear down — the account cache
981
- * survives so a later call resumes without re-syncing. For a USB session,
982
- * this closes the connector session and either demotes the record back to
983
- * QR-only (if it was ever QR-synced) or removes it entirely (pure-USB
984
- * wallet that was never seen over QR) — see §4.2 of the design doc.
929
+ * QR has nothing to tear down. A USB session is closed, and the record is demoted to QR-only or
930
+ * removed if it was never QR-synced.
985
931
  */
986
932
  async releaseOperation(operationId) {
987
933
  const operation = this._operations.find(operationId);
@@ -1005,15 +951,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
1005
951
  return;
1006
952
  }
1007
953
  const { usbSessionId } = record;
1008
- await this._runUsbTeardown(async () => {
1009
- if (this._usbConnector) {
1010
- try {
1011
- await this._usbConnector.disconnect(usbSessionId);
1012
- } catch {
1013
- }
1014
- }
1015
- this._handleUsbDisconnect({ connectId: usbSessionId });
1016
- });
954
+ await this._runUsbTeardown(() => this._retireUsbSession(usbSessionId));
1017
955
  }
1018
956
  getDeviceInfo(connectIdOrOperationId, deviceId) {
1019
957
  try {
@@ -1066,13 +1004,15 @@ var KeystoneAdapter = class _KeystoneAdapter {
1066
1004
  getChainFingerprint(connectId, deviceId, chain) {
1067
1005
  try {
1068
1006
  const target = this._resolveTarget(connectId, deviceId);
1069
- const walletId = target.record?.walletId ?? target.expectedWalletId;
1070
- if (!walletId) {
1007
+ const masterFingerprint = target.expectedMasterFingerprint;
1008
+ if (!masterFingerprint) {
1071
1009
  return Promise.resolve(
1072
1010
  failure(HardwareErrorCode.DeviceNotFound, "Unknown Keystone wallet identity")
1073
1011
  );
1074
1012
  }
1075
- return Promise.resolve(success(deriveDeviceFingerprint(`keystone:${chain}:${walletId}`)));
1013
+ return Promise.resolve(
1014
+ success(deriveDeviceFingerprint(`keystone:${chain}:${masterFingerprint}`))
1015
+ );
1076
1016
  } catch (err) {
1077
1017
  return Promise.resolve(this._errorToFailure(err));
1078
1018
  }
@@ -1091,8 +1031,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
1091
1031
  this.emitter.off(event, listener);
1092
1032
  }
1093
1033
  // ---------------------------------------------------------------------------
1094
- // Explicit account import — the recommended entry point before signing, so
1095
- // signing calls don't each pay for their own cold-sync round trip.
1034
+ // Explicit account import: call before signing to avoid a cold sync per call.
1096
1035
  // ---------------------------------------------------------------------------
1097
1036
  async importFromQr(options = {}) {
1098
1037
  try {
@@ -1103,7 +1042,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
1103
1042
  responseUr = await this._requestQrScanAndAwaitResponse(displayDevice, signal);
1104
1043
  } else {
1105
1044
  const requestUr = this.urEngine.buildKeyDerivationRequest({
1106
- schemas: [{ path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" }],
1045
+ schemas: [keySchema("evm", KEYSTONE_COLD_START_PATH)],
1107
1046
  origin: this._origin
1108
1047
  });
1109
1048
  responseUr = await this._requestQrDisplayAndAwaitResponse(
@@ -1124,12 +1063,11 @@ var KeystoneAdapter = class _KeystoneAdapter {
1124
1063
  // ---------------------------------------------------------------------------
1125
1064
  // EVM
1126
1065
  // ---------------------------------------------------------------------------
1127
- async evmGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1128
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1066
+ async evmGetAddress(connectIdArg, deviceIdArg, params, book) {
1067
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1129
1068
  if (!operationTarget.success) return operationTarget;
1130
1069
  const connectId = operationTarget.payload.targetId;
1131
1070
  const deviceId = deviceIdArg ?? void 0;
1132
- const params = paramsArg;
1133
1071
  if (!params) return failure(HardwareErrorCode.InvalidParams, "evmGetAddress requires params");
1134
1072
  if (!params.path)
1135
1073
  return failure(HardwareErrorCode.InvalidParams, "evmGetAddress requires params.path");
@@ -1140,39 +1078,24 @@ var KeystoneAdapter = class _KeystoneAdapter {
1140
1078
  "evmGetAddress requires a full leaf path, e.g. m/44'/60'/0'/0/0"
1141
1079
  );
1142
1080
  }
1143
- try {
1144
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1145
- const { account } = await this._fetchAccount(
1146
- connectId,
1147
- deviceId,
1148
- "evm",
1149
- accountPath,
1150
- signal,
1151
- { book }
1152
- );
1153
- _KeystoneAdapter._throwIfAborted(signal);
1154
- if (!account.extendedPublicKey) {
1155
- throw createHwkError({
1156
- code: HardwareErrorCode.MethodNotSupported,
1157
- message: "Keystone did not return an extended public key for this account path"
1158
- });
1159
- }
1160
- const address = this.urEngine.deriveEvmAddressFromXpub(
1161
- account.extendedPublicKey,
1162
- relativeDerivePath
1163
- );
1164
- return success({ address, path: normalizePath(params.path) });
1165
- });
1166
- } catch (err) {
1167
- return this._errorToFailure(err);
1168
- }
1081
+ return this._runJob(connectId, deviceId, async (signal) => {
1082
+ const xpub = await this._fetchAccountXpub(
1083
+ connectId,
1084
+ deviceId,
1085
+ "evm",
1086
+ accountPath,
1087
+ signal,
1088
+ book
1089
+ );
1090
+ const address = this.urEngine.deriveEvmAddressFromXpub(xpub, relativeDerivePath);
1091
+ return success({ address, path: normalizePath(params.path) });
1092
+ });
1169
1093
  }
1170
- async evmSignTransaction(connectIdArg, deviceIdArg, paramsArg) {
1171
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1094
+ async evmSignTransaction(connectIdArg, deviceIdArg, params) {
1095
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1172
1096
  if (!operationTarget.success) return operationTarget;
1173
1097
  const connectId = operationTarget.payload.targetId;
1174
1098
  const deviceId = deviceIdArg ?? void 0;
1175
- const params = paramsArg;
1176
1099
  if (!params)
1177
1100
  return failure(HardwareErrorCode.InvalidParams, "evmSignTransaction requires params");
1178
1101
  if (!params.path) {
@@ -1186,48 +1109,34 @@ var KeystoneAdapter = class _KeystoneAdapter {
1186
1109
  }
1187
1110
  const rawTxHex = stripHex(params.serializedTx);
1188
1111
  const path = normalizePath(params.path);
1189
- try {
1190
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1191
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "evm", signal);
1192
- _KeystoneAdapter._throwIfAborted(signal);
1193
- const requestId = uuidv4();
1194
- const dataType = parseInt(rawTxHex.slice(0, 2), 16) < 192 ? "typedTransaction" : "transaction";
1195
- const requestUr = this.urEngine.buildEthSignRequest({
1196
- requestId,
1197
- unsignedTxHex: rawTxHex,
1198
- dataType,
1199
- path,
1200
- xfp: record.masterFingerprint,
1201
- chainId: params.chainId,
1202
- origin: this._origin
1203
- });
1204
- const responseUr = await this._resolveUr(
1205
- record,
1206
- requestUr,
1207
- true,
1208
- connectId,
1209
- signal,
1210
- "evmSignTransaction"
1211
- );
1212
- _KeystoneAdapter._throwIfAborted(signal);
1213
- const sig = this.urEngine.parseEthSignature(responseUr);
1214
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1215
- return success({
1216
- v: ensure0x(sig.v),
1217
- r: ensure0x(sig.r),
1218
- s: ensure0x(sig.s)
1219
- });
1220
- });
1221
- } catch (err) {
1222
- return this._errorToFailure(err);
1223
- }
1112
+ const dataType = parseInt(rawTxHex.slice(0, 2), 16) < 192 ? "typedTransaction" : "transaction";
1113
+ return this._signWithWallet({
1114
+ connectId,
1115
+ deviceId,
1116
+ chain: "evm",
1117
+ operationName: "evmSignTransaction",
1118
+ buildRequest: (requestId, xfp) => this.urEngine.buildEthSignRequest({
1119
+ requestId,
1120
+ unsignedTxHex: rawTxHex,
1121
+ dataType,
1122
+ path,
1123
+ xfp,
1124
+ chainId: params.chainId,
1125
+ origin: this._origin
1126
+ }),
1127
+ parseResponse: (ur) => this.urEngine.parseEthSignature(ur),
1128
+ toPayload: (sig) => ({
1129
+ v: ensure0x(sig.v),
1130
+ r: ensure0x(sig.r),
1131
+ s: ensure0x(sig.s)
1132
+ })
1133
+ });
1224
1134
  }
1225
- async evmSignMessage(connectIdArg, deviceIdArg, paramsArg) {
1226
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1135
+ async evmSignMessage(connectIdArg, deviceIdArg, params) {
1136
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1227
1137
  if (!operationTarget.success) return operationTarget;
1228
1138
  const connectId = operationTarget.payload.targetId;
1229
1139
  const deviceId = deviceIdArg ?? void 0;
1230
- const params = paramsArg;
1231
1140
  if (!params) return failure(HardwareErrorCode.InvalidParams, "evmSignMessage requires params");
1232
1141
  if (!params.path || params.message === void 0) {
1233
1142
  return failure(
@@ -1237,43 +1146,29 @@ var KeystoneAdapter = class _KeystoneAdapter {
1237
1146
  }
1238
1147
  const messageHex = params.hex ? stripHex(params.message) : Buffer.from(params.message, "utf8").toString("hex");
1239
1148
  const path = normalizePath(params.path);
1240
- try {
1241
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1242
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "evm", signal);
1243
- _KeystoneAdapter._throwIfAborted(signal);
1244
- const requestId = uuidv4();
1245
- const requestUr = this.urEngine.buildEthSignRequest({
1246
- requestId,
1247
- unsignedTxHex: messageHex,
1248
- dataType: "personalMessage",
1249
- path,
1250
- xfp: record.masterFingerprint,
1251
- chainId: params.chainId,
1252
- origin: this._origin
1253
- });
1254
- const responseUr = await this._resolveUr(
1255
- record,
1256
- requestUr,
1257
- true,
1258
- connectId,
1259
- signal,
1260
- "evmSignMessage"
1261
- );
1262
- _KeystoneAdapter._throwIfAborted(signal);
1263
- const sig = this.urEngine.parseEthSignature(responseUr);
1264
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1265
- return success({ signature: ensure0x(sig.r + sig.s + sig.v) });
1266
- });
1267
- } catch (err) {
1268
- return this._errorToFailure(err);
1269
- }
1149
+ return this._signWithWallet({
1150
+ connectId,
1151
+ deviceId,
1152
+ chain: "evm",
1153
+ operationName: "evmSignMessage",
1154
+ buildRequest: (requestId, xfp) => this.urEngine.buildEthSignRequest({
1155
+ requestId,
1156
+ unsignedTxHex: messageHex,
1157
+ dataType: "personalMessage",
1158
+ path,
1159
+ xfp,
1160
+ chainId: params.chainId,
1161
+ origin: this._origin
1162
+ }),
1163
+ parseResponse: (ur) => this.urEngine.parseEthSignature(ur),
1164
+ toPayload: (sig) => ({ signature: ensure0x(sig.r + sig.s + sig.v) })
1165
+ });
1270
1166
  }
1271
- async evmSignTypedData(connectIdArg, deviceIdArg, paramsArg) {
1272
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1167
+ async evmSignTypedData(connectIdArg, deviceIdArg, params) {
1168
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1273
1169
  if (!operationTarget.success) return operationTarget;
1274
1170
  const connectId = operationTarget.payload.targetId;
1275
1171
  const deviceId = deviceIdArg ?? void 0;
1276
- const params = paramsArg;
1277
1172
  if (!params)
1278
1173
  return failure(HardwareErrorCode.InvalidParams, "evmSignTypedData requires params");
1279
1174
  if (!params.path) {
@@ -1290,49 +1185,33 @@ var KeystoneAdapter = class _KeystoneAdapter {
1290
1185
  "utf8"
1291
1186
  ).toString("hex");
1292
1187
  const path = normalizePath(params.path);
1293
- try {
1294
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1295
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "evm", signal);
1296
- _KeystoneAdapter._throwIfAborted(signal);
1297
- const requestId = uuidv4();
1298
- const requestUr = this.urEngine.buildEthSignRequest({
1299
- requestId,
1300
- unsignedTxHex: signDataHex,
1301
- dataType: "typedData",
1302
- path,
1303
- xfp: record.masterFingerprint,
1304
- chainId: params.chainId,
1305
- origin: this._origin
1306
- });
1307
- const responseUr = await this._resolveUr(
1308
- record,
1309
- requestUr,
1310
- true,
1311
- connectId,
1312
- signal,
1313
- "evmSignTypedData"
1314
- );
1315
- _KeystoneAdapter._throwIfAborted(signal);
1316
- const sig = this.urEngine.parseEthSignature(responseUr);
1317
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1318
- return success({ signature: ensure0x(sig.r + sig.s + sig.v) });
1319
- });
1320
- } catch (err) {
1321
- return this._errorToFailure(err);
1322
- }
1188
+ return this._signWithWallet({
1189
+ connectId,
1190
+ deviceId,
1191
+ chain: "evm",
1192
+ operationName: "evmSignTypedData",
1193
+ buildRequest: (requestId, xfp) => this.urEngine.buildEthSignRequest({
1194
+ requestId,
1195
+ unsignedTxHex: signDataHex,
1196
+ dataType: "typedData",
1197
+ path,
1198
+ xfp,
1199
+ chainId: params.chainId,
1200
+ origin: this._origin
1201
+ }),
1202
+ parseResponse: (ur) => this.urEngine.parseEthSignature(ur),
1203
+ toPayload: (sig) => ({ signature: ensure0x(sig.r + sig.s + sig.v) })
1204
+ });
1323
1205
  }
1324
1206
  // ---------------------------------------------------------------------------
1325
- // BTC — PSBT signing and message signing only for now. Address/pubkey
1326
- // derivation needs script-type-aware xpub decoding (P2WPKH/P2TR/…) this
1327
- // phase doesn't wire in yet; structured-field tx signing needs host-side
1328
- // PSBT construction. Both are real, bounded follow-ups, not silent gaps.
1207
+ // BTC: 44'/49'/84' addresses derive offline from the account xpub; P2TR addresses
1208
+ // and structured-field tx signing are unsupported (the host builds a PSBT instead).
1329
1209
  // ---------------------------------------------------------------------------
1330
- async btcGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1331
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1210
+ async btcGetAddress(connectIdArg, deviceIdArg, params, book) {
1211
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1332
1212
  if (!operationTarget.success) return operationTarget;
1333
1213
  const connectId = operationTarget.payload.targetId;
1334
1214
  const deviceId = deviceIdArg ?? void 0;
1335
- const params = paramsArg;
1336
1215
  if (!params) return failure(HardwareErrorCode.InvalidParams, "btcGetAddress requires params");
1337
1216
  if (!params.path)
1338
1217
  return failure(HardwareErrorCode.InvalidParams, "btcGetAddress requires params.path");
@@ -1360,49 +1239,28 @@ var KeystoneAdapter = class _KeystoneAdapter {
1360
1239
  if (!isKeystoneSignableBtcAccountPath(accountPath)) {
1361
1240
  return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1362
1241
  }
1363
- try {
1364
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1365
- const { account } = await this._fetchAccount(
1366
- connectId,
1367
- deviceId,
1368
- "btc",
1369
- accountPath,
1370
- signal,
1371
- { book }
1372
- );
1373
- _KeystoneAdapter._throwIfAborted(signal);
1374
- if (!account.extendedPublicKey) {
1375
- throw createHwkError({
1376
- code: HardwareErrorCode.MethodNotSupported,
1377
- message: "Keystone did not return an extended public key for this account path"
1378
- });
1379
- }
1380
- const address = this.urEngine.deriveBtcAddressFromXpub(
1381
- account.extendedPublicKey,
1382
- relativeDerivePath,
1383
- scriptType
1384
- );
1385
- return success({ address, path: normalizedPath });
1386
- });
1387
- } catch (err) {
1388
- return this._errorToFailure(err);
1389
- }
1242
+ return this._runJob(connectId, deviceId, async (signal) => {
1243
+ const xpub = await this._fetchAccountXpub(
1244
+ connectId,
1245
+ deviceId,
1246
+ "btc",
1247
+ accountPath,
1248
+ signal,
1249
+ book
1250
+ );
1251
+ const address = this.urEngine.deriveBtcAddressFromXpub(xpub, relativeDerivePath, scriptType);
1252
+ return success({ address, path: normalizedPath });
1253
+ });
1390
1254
  }
1391
1255
  /**
1392
- * Returns the account-level extended public key. `params.path` must be an
1393
- * ACCOUNT path (`m/84'/0'/0'`), not a leaf — that is the level Keystone
1394
- * actually syncs, and it is what a host needs to derive a whole account's
1395
- * addresses offline. Unlike `btcGetAddress` this has no script-type
1396
- * restriction: an xpub is script-type agnostic, so `86'` (taproot) works
1397
- * here even though deriving a taproot ADDRESS still needs an EC library
1398
- * this package doesn't wire in.
1256
+ * `params.path` must be an account path (`m/84'/0'/0'`), the level Keystone syncs. An xpub is
1257
+ * script-type agnostic, so `86'` works here even though `btcGetAddress` rejects it.
1399
1258
  */
1400
- async btcGetPublicKey(connectIdArg, deviceIdArg, paramsArg, book) {
1401
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1259
+ async btcGetPublicKey(connectIdArg, deviceIdArg, params, book) {
1260
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1402
1261
  if (!operationTarget.success) return operationTarget;
1403
1262
  const connectId = operationTarget.payload.targetId;
1404
1263
  const deviceId = deviceIdArg ?? void 0;
1405
- const params = paramsArg;
1406
1264
  if (!params) return failure(HardwareErrorCode.InvalidParams, "btcGetPublicKey requires params");
1407
1265
  if (!params.path)
1408
1266
  return failure(HardwareErrorCode.InvalidParams, "btcGetPublicKey requires params.path");
@@ -1416,36 +1274,25 @@ var KeystoneAdapter = class _KeystoneAdapter {
1416
1274
  if (!isKeystoneSignableBtcAccountPath(accountPath)) {
1417
1275
  return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1418
1276
  }
1419
- try {
1420
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1421
- const { account } = await this._fetchAccount(
1422
- connectId,
1423
- deviceId,
1424
- "btc",
1425
- accountPath,
1426
- signal,
1427
- { book }
1428
- );
1429
- _KeystoneAdapter._throwIfAborted(signal);
1430
- if (!account.extendedPublicKey) {
1431
- throw createHwkError({
1432
- code: HardwareErrorCode.MethodNotSupported,
1433
- message: "Keystone did not return an extended public key for this account path"
1434
- });
1435
- }
1436
- const meta = this.urEngine.parseXpubMeta(account.extendedPublicKey);
1437
- return success({
1438
- xpub: account.extendedPublicKey,
1439
- publicKey: meta.publicKey,
1440
- chainCode: meta.chainCode,
1441
- depth: meta.depth,
1442
- fingerprint: meta.parentFingerprint,
1443
- path: normalizePath(accountPath)
1444
- });
1277
+ return this._runJob(connectId, deviceId, async (signal) => {
1278
+ const xpub = await this._fetchAccountXpub(
1279
+ connectId,
1280
+ deviceId,
1281
+ "btc",
1282
+ accountPath,
1283
+ signal,
1284
+ book
1285
+ );
1286
+ const meta = this.urEngine.parseXpubMeta(xpub);
1287
+ return success({
1288
+ xpub,
1289
+ publicKey: meta.publicKey,
1290
+ chainCode: meta.chainCode,
1291
+ depth: meta.depth,
1292
+ fingerprint: meta.parentFingerprint,
1293
+ path: normalizePath(accountPath)
1445
1294
  });
1446
- } catch (err) {
1447
- return this._errorToFailure(err);
1448
- }
1295
+ });
1449
1296
  }
1450
1297
  async btcSignTransaction(_connectId, _deviceId, _params) {
1451
1298
  return this._unsupported(
@@ -1453,12 +1300,11 @@ var KeystoneAdapter = class _KeystoneAdapter {
1453
1300
  "use btcSignPsbt \u2014 structured input/output signing needs host-side PSBT construction, not yet wired in"
1454
1301
  );
1455
1302
  }
1456
- async btcSignPsbt(connectIdArg, deviceIdArg, paramsArg) {
1457
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1303
+ async btcSignPsbt(connectIdArg, deviceIdArg, params) {
1304
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1458
1305
  if (!operationTarget.success) return operationTarget;
1459
1306
  const connectId = operationTarget.payload.targetId;
1460
1307
  const deviceId = deviceIdArg ?? void 0;
1461
- const params = paramsArg;
1462
1308
  if (!params) return failure(HardwareErrorCode.InvalidParams, "btcSignPsbt requires params");
1463
1309
  if (!params.psbt)
1464
1310
  return failure(HardwareErrorCode.InvalidParams, "btcSignPsbt requires params.psbt");
@@ -1467,33 +1313,28 @@ var KeystoneAdapter = class _KeystoneAdapter {
1467
1313
  if (!isKeystoneSignableBtcAccountPath(splitAccountPath(params.path).accountPath)) {
1468
1314
  return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1469
1315
  }
1470
- try {
1471
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1472
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1473
- _KeystoneAdapter._throwIfAborted(signal);
1474
- const requestUr = this.urEngine.buildBtcPsbtRequest(stripHex(params.psbt));
1475
- const responseUr = await this._resolveUr(
1476
- record,
1477
- requestUr,
1478
- true,
1479
- connectId,
1480
- signal,
1481
- "btcSignPsbt"
1482
- );
1483
- _KeystoneAdapter._throwIfAborted(signal);
1484
- const signedPsbt = this.urEngine.parseBtcPsbt(responseUr);
1485
- return success({ signedPsbt });
1486
- });
1487
- } catch (err) {
1488
- return this._errorToFailure(err);
1489
- }
1316
+ return this._runJob(connectId, deviceId, async (signal) => {
1317
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1318
+ _KeystoneAdapter._throwIfAborted(signal);
1319
+ const requestUr = this.urEngine.buildBtcPsbtRequest(stripHex(params.psbt));
1320
+ const responseUr = await this._resolveUr(
1321
+ record,
1322
+ requestUr,
1323
+ true,
1324
+ connectId,
1325
+ signal,
1326
+ "btcSignPsbt"
1327
+ );
1328
+ _KeystoneAdapter._throwIfAborted(signal);
1329
+ const signedPsbt = this.urEngine.parseBtcPsbt(responseUr);
1330
+ return success({ signedPsbt });
1331
+ });
1490
1332
  }
1491
- async btcSignMessage(connectIdArg, deviceIdArg, paramsArg) {
1492
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1333
+ async btcSignMessage(connectIdArg, deviceIdArg, params) {
1334
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1493
1335
  if (!operationTarget.success) return operationTarget;
1494
1336
  const connectId = operationTarget.payload.targetId;
1495
1337
  const deviceId = deviceIdArg ?? void 0;
1496
- const params = paramsArg;
1497
1338
  if (!params) return failure(HardwareErrorCode.InvalidParams, "btcSignMessage requires params");
1498
1339
  if (!params.path || params.message === void 0) {
1499
1340
  return failure(
@@ -1506,80 +1347,55 @@ var KeystoneAdapter = class _KeystoneAdapter {
1506
1347
  if (!isKeystoneSignableBtcAccountPath(splitAccountPath(path).accountPath)) {
1507
1348
  return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1508
1349
  }
1509
- try {
1510
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1511
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1512
- _KeystoneAdapter._throwIfAborted(signal);
1513
- const requestId = uuidv4();
1514
- const requestUr = this.urEngine.buildBtcMessageSignRequest({
1515
- requestId,
1516
- messageHex,
1517
- accounts: [{ path, xfp: record.masterFingerprint }],
1518
- origin: this._origin
1519
- });
1520
- const responseUr = await this._resolveUr(
1521
- record,
1522
- requestUr,
1523
- true,
1524
- connectId,
1525
- signal,
1526
- "btcSignMessage"
1527
- );
1528
- _KeystoneAdapter._throwIfAborted(signal);
1529
- const sig = this.urEngine.parseBtcSignature(responseUr);
1530
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1531
- return success({ signature: sig.signature });
1532
- });
1533
- } catch (err) {
1534
- return this._errorToFailure(err);
1535
- }
1350
+ return this._signWithWallet({
1351
+ connectId,
1352
+ deviceId,
1353
+ chain: "btc",
1354
+ operationName: "btcSignMessage",
1355
+ buildRequest: (requestId, xfp) => this.urEngine.buildBtcMessageSignRequest({
1356
+ requestId,
1357
+ messageHex,
1358
+ accounts: [{ path, xfp }],
1359
+ origin: this._origin
1360
+ }),
1361
+ parseResponse: (ur) => this.urEngine.parseBtcSignature(ur),
1362
+ toPayload: (sig) => ({ signature: sig.signature })
1363
+ });
1536
1364
  }
1537
1365
  async btcGetMasterFingerprint(connectIdArg, deviceIdArg, paramsArg) {
1538
1366
  const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1539
1367
  if (!operationTarget.success) return operationTarget;
1540
1368
  const connectId = operationTarget.payload.targetId;
1541
1369
  const deviceId = deviceIdArg ?? void 0;
1542
- try {
1543
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1544
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1545
- return success({ masterFingerprint: record.masterFingerprint });
1546
- });
1547
- } catch (err) {
1548
- return this._errorToFailure(err);
1549
- }
1370
+ return this._runJob(connectId, deviceId, async (signal) => {
1371
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1372
+ return success({ masterFingerprint: record.masterFingerprint });
1373
+ });
1550
1374
  }
1551
1375
  // ---------------------------------------------------------------------------
1552
1376
  // SOL
1553
1377
  // ---------------------------------------------------------------------------
1554
- async solGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1555
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1378
+ async solGetAddress(connectIdArg, deviceIdArg, params, book) {
1379
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1556
1380
  if (!operationTarget.success) return operationTarget;
1557
1381
  const connectId = operationTarget.payload.targetId;
1558
1382
  const deviceId = deviceIdArg ?? void 0;
1559
- const params = paramsArg;
1560
1383
  if (!params) return failure(HardwareErrorCode.InvalidParams, "solGetAddress requires params");
1561
1384
  if (!params.path)
1562
1385
  return failure(HardwareErrorCode.InvalidParams, "solGetAddress requires params.path");
1563
1386
  const path = normalizePath(params.path);
1564
- try {
1565
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1566
- const { account } = await this._fetchAccount(connectId, deviceId, "sol", path, signal, {
1567
- book
1568
- });
1569
- _KeystoneAdapter._throwIfAborted(signal);
1570
- const address = bs58.encode(Buffer.from(account.publicKey, "hex"));
1571
- return success({ address, path });
1572
- });
1573
- } catch (err) {
1574
- return this._errorToFailure(err);
1575
- }
1387
+ return this._runJob(connectId, deviceId, async (signal) => {
1388
+ const { account } = await this._fetchAccount(connectId, deviceId, "sol", path, signal, book);
1389
+ _KeystoneAdapter._throwIfAborted(signal);
1390
+ const address = bs58.encode(Buffer.from(account.publicKey, "hex"));
1391
+ return success({ address, path });
1392
+ });
1576
1393
  }
1577
- async solSignTransaction(connectIdArg, deviceIdArg, paramsArg) {
1578
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1394
+ async solSignTransaction(connectIdArg, deviceIdArg, params) {
1395
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1579
1396
  if (!operationTarget.success) return operationTarget;
1580
1397
  const connectId = operationTarget.payload.targetId;
1581
1398
  const deviceId = deviceIdArg ?? void 0;
1582
- const params = paramsArg;
1583
1399
  if (!params)
1584
1400
  return failure(HardwareErrorCode.InvalidParams, "solSignTransaction requires params");
1585
1401
  if (!params.path || !params.serializedTx) {
@@ -1589,42 +1405,28 @@ var KeystoneAdapter = class _KeystoneAdapter {
1589
1405
  );
1590
1406
  }
1591
1407
  const path = normalizePath(params.path);
1592
- try {
1593
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1594
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "sol", signal);
1595
- _KeystoneAdapter._throwIfAborted(signal);
1596
- const requestId = uuidv4();
1597
- const requestUr = this.urEngine.buildSolSignRequest({
1598
- requestId,
1599
- unsignedPayloadHex: stripHex(params.serializedTx),
1600
- dataType: "transaction",
1601
- path,
1602
- xfp: record.masterFingerprint,
1603
- origin: this._origin
1604
- });
1605
- const responseUr = await this._resolveUr(
1606
- record,
1607
- requestUr,
1608
- true,
1609
- connectId,
1610
- signal,
1611
- "solSignTransaction"
1612
- );
1613
- _KeystoneAdapter._throwIfAborted(signal);
1614
- const sig = this.urEngine.parseSolSignature(responseUr);
1615
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1616
- return success({ signature: sig.signature });
1617
- });
1618
- } catch (err) {
1619
- return this._errorToFailure(err);
1620
- }
1408
+ return this._signWithWallet({
1409
+ connectId,
1410
+ deviceId,
1411
+ chain: "sol",
1412
+ operationName: "solSignTransaction",
1413
+ buildRequest: (requestId, xfp) => this.urEngine.buildSolSignRequest({
1414
+ requestId,
1415
+ unsignedPayloadHex: stripHex(params.serializedTx),
1416
+ dataType: "transaction",
1417
+ path,
1418
+ xfp,
1419
+ origin: this._origin
1420
+ }),
1421
+ parseResponse: (ur) => this.urEngine.parseSolSignature(ur),
1422
+ toPayload: (sig) => ({ signature: sig.signature })
1423
+ });
1621
1424
  }
1622
- async solSignMessage(connectIdArg, deviceIdArg, paramsArg) {
1623
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1425
+ async solSignMessage(connectIdArg, deviceIdArg, params) {
1426
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1624
1427
  if (!operationTarget.success) return operationTarget;
1625
1428
  const connectId = operationTarget.payload.targetId;
1626
1429
  const deviceId = deviceIdArg ?? void 0;
1627
- const params = paramsArg;
1628
1430
  if (!params) return failure(HardwareErrorCode.InvalidParams, "solSignMessage requires params");
1629
1431
  if (!params.path || !params.message) {
1630
1432
  return failure(
@@ -1633,50 +1435,32 @@ var KeystoneAdapter = class _KeystoneAdapter {
1633
1435
  );
1634
1436
  }
1635
1437
  const path = normalizePath(params.path);
1636
- try {
1637
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1638
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "sol", signal);
1639
- _KeystoneAdapter._throwIfAborted(signal);
1640
- const requestId = uuidv4();
1641
- const requestUr = this.urEngine.buildSolSignRequest({
1642
- requestId,
1643
- unsignedPayloadHex: stripHex(params.message),
1644
- dataType: "message",
1645
- path,
1646
- xfp: record.masterFingerprint,
1647
- origin: this._origin
1648
- });
1649
- const responseUr = await this._resolveUr(
1650
- record,
1651
- requestUr,
1652
- true,
1653
- connectId,
1654
- signal,
1655
- "solSignMessage"
1656
- );
1657
- _KeystoneAdapter._throwIfAborted(signal);
1658
- const sig = this.urEngine.parseSolSignature(responseUr);
1659
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1660
- return success({ signature: sig.signature });
1661
- });
1662
- } catch (err) {
1663
- return this._errorToFailure(err);
1664
- }
1438
+ return this._signWithWallet({
1439
+ connectId,
1440
+ deviceId,
1441
+ chain: "sol",
1442
+ operationName: "solSignMessage",
1443
+ buildRequest: (requestId, xfp) => this.urEngine.buildSolSignRequest({
1444
+ requestId,
1445
+ unsignedPayloadHex: stripHex(params.message),
1446
+ dataType: "message",
1447
+ path,
1448
+ xfp,
1449
+ origin: this._origin
1450
+ }),
1451
+ parseResponse: (ur) => this.urEngine.parseSolSignature(ur),
1452
+ toPayload: (sig) => ({ signature: sig.signature })
1453
+ });
1665
1454
  }
1666
1455
  // ---------------------------------------------------------------------------
1667
- // TRON — routed through `TronSignRequest`/`TronSignature` (see
1668
- // urEngine/TronSignRequest.ts), whose key layout follows keystone-sdk-rust
1669
- // (tags 5201/5202, firmware >= 2.5.0). The OneKey Pro air-gap encoding under
1670
- // the same tag is a different layout the firmware rejects. Not keystone-sdk's
1671
- // bundled `sdk.tron` module either (protobuf-based, unverified response
1672
- // semantics).
1456
+ // TRON: keystone-sdk-rust layout (tags 5201/5202, firmware >= 2.5.0), not sdk.tron's
1457
+ // protobuf module; OneKey Pro's encoding under the same tag is rejected by the firmware.
1673
1458
  // ---------------------------------------------------------------------------
1674
- async tronGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1675
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1459
+ async tronGetAddress(connectIdArg, deviceIdArg, params, book) {
1460
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1676
1461
  if (!operationTarget.success) return operationTarget;
1677
1462
  const connectId = operationTarget.payload.targetId;
1678
1463
  const deviceId = deviceIdArg ?? void 0;
1679
- const params = paramsArg;
1680
1464
  if (!params) return failure(HardwareErrorCode.InvalidParams, "tronGetAddress requires params");
1681
1465
  if (!params.path)
1682
1466
  return failure(HardwareErrorCode.InvalidParams, "tronGetAddress requires params.path");
@@ -1687,39 +1471,24 @@ var KeystoneAdapter = class _KeystoneAdapter {
1687
1471
  "tronGetAddress requires a full leaf path, e.g. m/44'/195'/0'/0/0"
1688
1472
  );
1689
1473
  }
1690
- try {
1691
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1692
- const { account } = await this._fetchAccount(
1693
- connectId,
1694
- deviceId,
1695
- "tron",
1696
- accountPath,
1697
- signal,
1698
- { book }
1699
- );
1700
- _KeystoneAdapter._throwIfAborted(signal);
1701
- if (!account.extendedPublicKey) {
1702
- throw createHwkError({
1703
- code: HardwareErrorCode.MethodNotSupported,
1704
- message: "Keystone did not return an extended public key for this account path"
1705
- });
1706
- }
1707
- const address = this.urEngine.deriveTronAddressFromXpub(
1708
- account.extendedPublicKey,
1709
- relativeDerivePath
1710
- );
1711
- return success({ address, path: normalizePath(params.path) });
1712
- });
1713
- } catch (err) {
1714
- return this._errorToFailure(err);
1715
- }
1474
+ return this._runJob(connectId, deviceId, async (signal) => {
1475
+ const xpub = await this._fetchAccountXpub(
1476
+ connectId,
1477
+ deviceId,
1478
+ "tron",
1479
+ accountPath,
1480
+ signal,
1481
+ book
1482
+ );
1483
+ const address = this.urEngine.deriveTronAddressFromXpub(xpub, relativeDerivePath);
1484
+ return success({ address, path: normalizePath(params.path) });
1485
+ });
1716
1486
  }
1717
- async tronSignTransaction(connectIdArg, deviceIdArg, paramsArg) {
1718
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1487
+ async tronSignTransaction(connectIdArg, deviceIdArg, params) {
1488
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1719
1489
  if (!operationTarget.success) return operationTarget;
1720
1490
  const connectId = operationTarget.payload.targetId;
1721
1491
  const deviceId = deviceIdArg ?? void 0;
1722
- const params = paramsArg;
1723
1492
  if (!params)
1724
1493
  return failure(HardwareErrorCode.InvalidParams, "tronSignTransaction requires params");
1725
1494
  if (!params.path) {
@@ -1733,41 +1502,28 @@ var KeystoneAdapter = class _KeystoneAdapter {
1733
1502
  }
1734
1503
  const rawTxHex = stripHex(params.rawTxHex);
1735
1504
  const path = normalizePath(params.path);
1736
- try {
1737
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1738
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "tron", signal);
1739
- _KeystoneAdapter._throwIfAborted(signal);
1740
- const requestId = uuidv4();
1741
- const requestUr = this.urEngine.buildTronSignRequest({
1742
- requestId,
1743
- rawTxHex,
1744
- path,
1745
- xfp: record.masterFingerprint,
1746
- origin: this._origin
1747
- });
1748
- const responseUr = await this._resolveUr(
1749
- record,
1750
- requestUr,
1751
- true,
1752
- connectId,
1753
- signal,
1754
- "tronSignTransaction"
1755
- );
1756
- _KeystoneAdapter._throwIfAborted(signal);
1757
- const sig = this.urEngine.parseTronSignature(responseUr);
1758
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1759
- return success({ signature: sig.signature });
1760
- });
1761
- } catch (err) {
1762
- return this._errorToFailure(err);
1763
- }
1505
+ return this._signWithWallet({
1506
+ connectId,
1507
+ deviceId,
1508
+ chain: "tron",
1509
+ operationName: "tronSignTransaction",
1510
+ buildRequest: (requestId, xfp) => this.urEngine.buildTronSignRequest({
1511
+ requestId,
1512
+ rawTxHex,
1513
+ path,
1514
+ xfp,
1515
+ origin: this._origin
1516
+ }),
1517
+ parseResponse: (ur) => this.urEngine.parseTronSignature(ur),
1518
+ // Keystone signs rawTxHex as given and returns only the signature, so no serializedTx.
1519
+ toPayload: (sig) => ({ signature: sig.signature })
1520
+ });
1764
1521
  }
1765
- async tronSignMessage(connectIdArg, deviceIdArg, paramsArg) {
1766
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1522
+ async tronSignMessage(connectIdArg, deviceIdArg, params) {
1523
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1767
1524
  if (!operationTarget.success) return operationTarget;
1768
1525
  const connectId = operationTarget.payload.targetId;
1769
1526
  const deviceId = deviceIdArg ?? void 0;
1770
- const params = paramsArg;
1771
1527
  if (!params) return failure(HardwareErrorCode.InvalidParams, "tronSignMessage requires params");
1772
1528
  if (!params.path) {
1773
1529
  return failure(HardwareErrorCode.InvalidParams, "tronSignMessage requires params.path");
@@ -1783,41 +1539,75 @@ var KeystoneAdapter = class _KeystoneAdapter {
1783
1539
  }
1784
1540
  const messageHex = stripHex(params.messageHex);
1785
1541
  const path = normalizePath(params.path);
1542
+ return this._signWithWallet({
1543
+ connectId,
1544
+ deviceId,
1545
+ chain: "tron",
1546
+ operationName: "tronSignMessage",
1547
+ buildRequest: (requestId, xfp) => this.urEngine.buildTronSignRequest({
1548
+ requestId,
1549
+ rawTxHex: messageHex,
1550
+ path,
1551
+ xfp,
1552
+ signType: 2 /* PersonalMessage */,
1553
+ origin: this._origin
1554
+ }),
1555
+ parseResponse: (ur) => this.urEngine.parseTronSignature(ur),
1556
+ toPayload: (sig) => ({ signature: sig.signature })
1557
+ });
1558
+ }
1559
+ // ---------------------------------------------------------------------------
1560
+ // Internals
1561
+ // ---------------------------------------------------------------------------
1562
+ async _runJob(connectId, deviceId, job) {
1786
1563
  try {
1787
- return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1788
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "tron", signal);
1789
- _KeystoneAdapter._throwIfAborted(signal);
1790
- const requestId = uuidv4();
1791
- const requestUr = this.urEngine.buildTronSignRequest({
1792
- requestId,
1793
- rawTxHex: messageHex,
1794
- path,
1795
- xfp: record.masterFingerprint,
1796
- signType: 2 /* PersonalMessage */,
1797
- origin: this._origin
1798
- });
1799
- const responseUr = await this._resolveUr(
1800
- record,
1801
- requestUr,
1802
- true,
1803
- connectId,
1804
- signal,
1805
- "tronSignMessage"
1806
- );
1807
- _KeystoneAdapter._throwIfAborted(signal);
1808
- const sig = this.urEngine.parseTronSignature(responseUr);
1809
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1810
- return success({ signature: sig.signature });
1811
- });
1564
+ return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), job);
1812
1565
  } catch (err) {
1813
1566
  return this._errorToFailure(err);
1814
1567
  }
1815
1568
  }
1816
- // ---------------------------------------------------------------------------
1817
- // Internals
1818
- // ---------------------------------------------------------------------------
1819
- _walletIdFromIdentifier(identifier) {
1820
- return keystoneWalletIdFromIdentifier(identifier);
1569
+ async _fetchAccountXpub(connectId, deviceId, hwkChain, accountPath, signal, book) {
1570
+ const { account } = await this._fetchAccount(
1571
+ connectId,
1572
+ deviceId,
1573
+ hwkChain,
1574
+ accountPath,
1575
+ signal,
1576
+ book
1577
+ );
1578
+ _KeystoneAdapter._throwIfAborted(signal);
1579
+ if (!account.extendedPublicKey) {
1580
+ throw createHwkError({
1581
+ code: HardwareErrorCode.MethodNotSupported,
1582
+ message: "Keystone did not return an extended public key for this account path"
1583
+ });
1584
+ }
1585
+ return account.extendedPublicKey;
1586
+ }
1587
+ /**
1588
+ * Signing needs only the wallet's mfp: the device re-derives the key from path+xfp, so no
1589
+ * per-path account lookup happens here.
1590
+ */
1591
+ _signWithWallet(args) {
1592
+ const { connectId, deviceId } = args;
1593
+ return this._runJob(connectId, deviceId, async (signal) => {
1594
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, args.chain, signal);
1595
+ _KeystoneAdapter._throwIfAborted(signal);
1596
+ const requestId = uuidv4();
1597
+ const requestUr = args.buildRequest(requestId, record.masterFingerprint);
1598
+ const responseUr = await this._resolveUr(
1599
+ record,
1600
+ requestUr,
1601
+ true,
1602
+ connectId,
1603
+ signal,
1604
+ args.operationName
1605
+ );
1606
+ _KeystoneAdapter._throwIfAborted(signal);
1607
+ const signature = args.parseResponse(responseUr);
1608
+ _KeystoneAdapter._assertRequestIdMatches(requestId, signature.requestId);
1609
+ return success(args.toPayload(signature));
1610
+ });
1821
1611
  }
1822
1612
  _allNetworkSyncSchema(item) {
1823
1613
  if (!isAllNetworkMethodName(String(item.methodName)) || !item.path) return void 0;
@@ -1840,16 +1630,13 @@ var KeystoneAdapter = class _KeystoneAdapter {
1840
1630
  }
1841
1631
  }
1842
1632
  /**
1843
- * QR is an interactive batch transport: one account-creation action must
1844
- * produce one request QR and one response scan, regardless of how many
1845
- * chains or derivation paths the host bundled. USB keeps its proven
1846
- * one-path-at-a-time export flow because the device limits that channel.
1633
+ * QR answers the whole bundle with one request QR and one scan. USB exports one path per
1634
+ * request because the device limits that channel.
1847
1635
  */
1848
1636
  async _prefetchAllNetworkAccounts(connectId, deviceId, params) {
1849
1637
  return this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1850
1638
  const target = this._resolveTarget(connectId, deviceId);
1851
- const { record: targetRecord } = target;
1852
- let record = targetRecord;
1639
+ let { record } = target;
1853
1640
  const requestedByKey = /* @__PURE__ */ new Map();
1854
1641
  for (const item of params.bundle) {
1855
1642
  const schema = this._allNetworkSyncSchema(item);
@@ -1859,14 +1646,22 @@ var KeystoneAdapter = class _KeystoneAdapter {
1859
1646
  if (!requestedByKey.size) {
1860
1647
  return { book };
1861
1648
  }
1649
+ const requestedSchemas = Array.from(requestedByKey.values());
1650
+ const assertAllBooked = () => {
1651
+ for (const schema of requestedSchemas) {
1652
+ if (!book.has(accountKey(schema.hwkChain, schema.path))) {
1653
+ throw createHwkError({
1654
+ code: HardwareErrorCode.DeviceMismatch,
1655
+ message: `Keystone did not return the requested derivation path (${schema.path})`
1656
+ });
1657
+ }
1658
+ }
1659
+ };
1862
1660
  const operation = isHardwareOperationId2(params.operationId) ? this._operations.find(params.operationId) : void 0;
1863
1661
  const operationRoute = operation ? this._operationRoutes.get(operation.connectId) : void 0;
1864
1662
  const routedToQr = operationRoute?.operationId === operation?.operationId && operationRoute?.connectionType === "qr";
1865
- if (this._forcedTransport !== "qr" && !routedToQr && !record?.usbSessionId && this._usbConnector && target.expectedWalletId) {
1866
- record = await this._tryUsbAttach(
1867
- target.expectedWalletId,
1868
- record?.masterFingerprint ?? target.expectedMasterFingerprint
1869
- ) ?? record;
1663
+ if (this._forcedTransport !== "qr" && !routedToQr && !record?.usbSessionId && this._usbConnector && target.expectedMasterFingerprint) {
1664
+ record = await this._tryUsbAttach(signal, target.expectedMasterFingerprint) ?? record;
1870
1665
  _KeystoneAdapter._throwIfAborted(signal);
1871
1666
  }
1872
1667
  if (this._forcedTransport !== "qr" && !routedToQr && record?.usbSessionId) {
@@ -1879,8 +1674,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
1879
1674
  });
1880
1675
  }
1881
1676
  };
1882
- const missingSchemas2 = Array.from(requestedByKey.values());
1883
- for (const schema of missingSchemas2) {
1677
+ for (const schema of requestedSchemas) {
1884
1678
  if (!book.has(accountKey(schema.hwkChain, schema.path))) {
1885
1679
  assertBundleSession();
1886
1680
  const synced = await this._fetchAccount(
@@ -1889,21 +1683,14 @@ var KeystoneAdapter = class _KeystoneAdapter {
1889
1683
  schema.hwkChain,
1890
1684
  schema.path,
1891
1685
  signal,
1892
- { book }
1686
+ book
1893
1687
  );
1894
1688
  record = synced.record;
1895
1689
  assertBundleSession();
1896
1690
  }
1897
1691
  }
1898
- for (const schema of missingSchemas2) {
1899
- if (!book.has(accountKey(schema.hwkChain, schema.path))) {
1900
- throw createHwkError({
1901
- code: HardwareErrorCode.DeviceMismatch,
1902
- message: `Keystone did not return the requested derivation path (${schema.path})`
1903
- });
1904
- }
1905
- }
1906
- return { book, walletId: record.walletId };
1692
+ assertAllBooked();
1693
+ return { book, masterFingerprint: record.masterFingerprint };
1907
1694
  }
1908
1695
  if (this._forcedTransport === "usb") {
1909
1696
  throw createHwkError({
@@ -1911,16 +1698,9 @@ var KeystoneAdapter = class _KeystoneAdapter {
1911
1698
  message: "USB channel is not connected for this Keystone wallet"
1912
1699
  });
1913
1700
  }
1914
- const missingSchemas = Array.from(requestedByKey.values());
1915
- const schemas = [
1916
- { hwkChain: "evm", path: KEYSTONE_WALLET_ID_PATH },
1917
- ...missingSchemas.filter((schema) => normalizePath(schema.path) !== KEYSTONE_WALLET_ID_PATH)
1918
- ];
1701
+ const schemas = requestedSchemas;
1919
1702
  const requestUr = this.urEngine.buildKeyDerivationRequest({
1920
- schemas: schemas.map((schema) => ({
1921
- path: schema.path,
1922
- curve: schema.hwkChain === "sol" ? "ed25519" : "secp256k1"
1923
- })),
1703
+ schemas: schemas.map((schema) => keySchema(schema.hwkChain, schema.path)),
1924
1704
  origin: this._origin
1925
1705
  });
1926
1706
  const responseUr = await this._requestQrDisplayAndAwaitResponse(
@@ -1939,100 +1719,58 @@ var KeystoneAdapter = class _KeystoneAdapter {
1939
1719
  const normalizedPath = normalizePath(account.path);
1940
1720
  const hwkChain = requestedChainByPath.get(normalizedPath) ?? inferHwkChainFromPath(normalizedPath);
1941
1721
  if (hwkChain) {
1942
- book.set(accountKey(hwkChain, normalizedPath), {
1943
- ...account,
1944
- hwkChain
1945
- });
1946
- }
1947
- }
1948
- for (const schema of missingSchemas) {
1949
- if (!book.has(accountKey(schema.hwkChain, schema.path))) {
1950
- throw createHwkError({
1951
- code: HardwareErrorCode.DeviceMismatch,
1952
- message: `Keystone did not return the requested derivation path (${schema.path})`
1953
- });
1722
+ book.set(accountKey(hwkChain, normalizedPath), { ...account, hwkChain });
1954
1723
  }
1955
1724
  }
1956
- return { book, walletId: record.walletId };
1725
+ assertAllBooked();
1726
+ return { book, masterFingerprint: record.masterFingerprint };
1957
1727
  });
1958
1728
  }
1959
- /**
1960
- * Resolve only the stable 64-hex wallet identity. The 8-hex BIP32 master
1961
- * fingerprint is protocol metadata and is never accepted as a lookup key.
1962
- */
1729
+ /** Resolve the wallet's master fingerprint from the connectId and/or deviceId the caller passed. */
1963
1730
  _resolveTarget(connectId, deviceId) {
1964
- let resolvedConnectId = connectId;
1965
- if (isHardwareOperationId2(connectId)) {
1966
- resolvedConnectId = this._operations.resolve(connectId).connectId;
1967
- }
1731
+ const resolvedConnectId = isHardwareOperationId2(connectId) ? this._operations.resolve(connectId).connectId : connectId;
1968
1732
  const identifiers = [deviceId, resolvedConnectId].filter(
1969
1733
  (identifier) => Boolean(identifier)
1970
1734
  );
1971
1735
  if (!identifiers.length) return {};
1972
- const walletIds = identifiers.map((identifier) => this._walletIdFromIdentifier(identifier)).filter((walletId) => Boolean(walletId));
1973
- if (!walletIds.length) {
1736
+ const fingerprints = identifiers.map((identifier) => keystoneMfpFromIdentifier(identifier));
1737
+ const expectedMasterFingerprint = fingerprints[0];
1738
+ if (!expectedMasterFingerprint || fingerprints.some((value) => !value)) {
1974
1739
  throw createHwkError({
1975
1740
  code: HardwareErrorCode.DeviceNotFound,
1976
- message: "Keystone device lookup requires a 64-hex wallet identity"
1741
+ message: "Keystone device lookup requires the wallet master fingerprint"
1977
1742
  });
1978
1743
  }
1979
- const expectedWalletId = walletIds[0];
1980
- if (walletIds.some((walletId) => walletId !== expectedWalletId)) {
1744
+ if (fingerprints.some((value) => value !== expectedMasterFingerprint)) {
1981
1745
  throw createHwkError({
1982
1746
  code: HardwareErrorCode.DeviceMismatch,
1983
1747
  message: "Keystone connectId and deviceId identify different wallets"
1984
1748
  });
1985
1749
  }
1986
- if (identifiers.some((identifier) => !this._walletIdFromIdentifier(identifier))) {
1987
- throw createHwkError({
1988
- code: HardwareErrorCode.DeviceMismatch,
1989
- message: "Keystone connectId and deviceId are inconsistent"
1990
- });
1991
- }
1992
- const record = this._devices.get(expectedWalletId);
1993
- return record ? {
1994
- record,
1995
- expectedWalletId,
1996
- expectedMasterFingerprint: record.masterFingerprint
1997
- } : { expectedWalletId };
1750
+ return {
1751
+ record: this._devices.get(expectedMasterFingerprint),
1752
+ expectedMasterFingerprint
1753
+ };
1998
1754
  }
1999
1755
  _assertParsedIdentity(parsed, expected) {
2000
- const walletId = deriveKeystoneWalletId(parsed.accounts);
2001
1756
  if (expected.expectedMasterFingerprint && parsed.masterFingerprint !== expected.expectedMasterFingerprint) {
2002
1757
  throw createHwkError({
2003
1758
  code: HardwareErrorCode.DeviceMismatch,
2004
1759
  message: `Connected Keystone wallet (mfp ${parsed.masterFingerprint}) does not match the requested wallet fingerprint (${expected.expectedMasterFingerprint})`
2005
1760
  });
2006
1761
  }
2007
- if (expected.expectedWalletId && walletId !== expected.expectedWalletId) {
2008
- throw createHwkError({
2009
- code: HardwareErrorCode.DeviceMismatch,
2010
- message: "Connected Keystone wallet does not match the requested wallet identity"
2011
- });
2012
- }
2013
- return walletId;
2014
1762
  }
2015
1763
  /**
2016
- * Folds a parsed account-response UR into the device table. `viaUsb`
2017
- * (defaults false) says which channel actually carried this round trip —
2018
- * `_resolveUr` routes a KeyDerivation sync over USB when the target record
2019
- * already has a live session, so this must NOT unconditionally mark
2020
- * `qrSynced`, or a USB-only wallet would wrongly survive a later USB
2021
- * disconnect as a "QR-synced, demote to QR-only" entry instead of being
2022
- * dropped outright (see `releaseOperation`).
1764
+ * Folds a parsed account response into the device table. Only non-USB round trips mark
1765
+ * `qrSynced`, so a USB-only wallet is dropped on disconnect.
2023
1766
  */
2024
1767
  _upsertDeviceRecord(parsed, options) {
2025
- const walletId = deriveKeystoneWalletId(parsed.accounts);
2026
- let record = this._devices.get(walletId);
1768
+ const { masterFingerprint } = parsed;
1769
+ let record = this._devices.get(masterFingerprint);
2027
1770
  const isNew = !record;
2028
1771
  if (!record) {
2029
- record = createDeviceRecord(walletId, parsed.masterFingerprint);
2030
- this._devices.set(walletId, record);
2031
- } else if (record.masterFingerprint !== parsed.masterFingerprint) {
2032
- throw createHwkError({
2033
- code: HardwareErrorCode.DeviceMismatch,
2034
- message: "Keystone wallet identity returned an inconsistent master fingerprint"
2035
- });
1772
+ record = createDeviceRecord(masterFingerprint);
1773
+ this._devices.set(masterFingerprint, record);
2036
1774
  }
2037
1775
  record.model = parsed.device ?? record.model;
2038
1776
  record.deviceVersion = parsed.deviceVersion ?? record.deviceVersion;
@@ -2045,34 +1783,38 @@ var KeystoneAdapter = class _KeystoneAdapter {
2045
1783
  return record;
2046
1784
  }
2047
1785
  /**
2048
- * Opens+claims whatever Keystone the USB connector currently has
2049
- * permission for, learns its protocol mfp via `getAppConfig`, then requests
2050
- * one fixed account-level xpub and derives the stable wallet id. A
2051
- * QR-synced entry becomes
2052
- * `{qr, usb}`-capable in place (one `device-changed`, not a second
2053
- * `device-connect`); a wallet never seen before becomes a new USB-only
2054
- * entry. See §4.2 of the design doc.
1786
+ * Opens a permitted USB Keystone and takes its mfp as the wallet identity. A known wallet gains
1787
+ * USB in place (`device-changed`); an unseen one becomes USB-only.
2055
1788
  */
2056
- async _connectUsb(expected, searchTargetId) {
2057
- if (this._pendingUsbTeardowns > 0) {
1789
+ async _connectUsb(expected, searchTargetId, signal) {
1790
+ if (this._pendingUsbTeardowns > 0 || this._abandonedUsbConnects > 0) {
2058
1791
  return failure(
2059
1792
  HardwareErrorCode.DeviceBusyInternal,
2060
1793
  "Keystone USB session is being released"
2061
1794
  );
2062
1795
  }
2063
- const previous = this._usbConnectTail;
2064
- let release;
2065
- this._usbConnectTail = new Promise((resolve) => {
2066
- release = resolve;
1796
+ _KeystoneAdapter._throwIfAborted(signal);
1797
+ let abandoned = false;
1798
+ const onAbort = () => {
1799
+ abandoned = true;
1800
+ this._abandonedUsbConnects += 1;
1801
+ };
1802
+ signal?.addEventListener("abort", onAbort, { once: true });
1803
+ const pending = this._usbConnectTail.then(async () => {
1804
+ _KeystoneAdapter._throwIfAborted(signal);
1805
+ return this._connectUsbExclusive(expected, searchTargetId, signal);
2067
1806
  });
2068
- await previous;
2069
- try {
2070
- return await this._connectUsbExclusive(expected, searchTargetId);
2071
- } finally {
2072
- release?.();
2073
- }
1807
+ const settled = pending.finally(() => {
1808
+ signal?.removeEventListener("abort", onAbort);
1809
+ if (abandoned) this._abandonedUsbConnects -= 1;
1810
+ });
1811
+ this._usbConnectTail = settled.then(
1812
+ () => void 0,
1813
+ () => void 0
1814
+ );
1815
+ return signal ? _KeystoneAdapter._abortable(signal, settled) : settled;
2074
1816
  }
2075
- async _connectUsbExclusive(expected, searchTargetId) {
1817
+ async _connectUsbExclusive(expected, searchTargetId, signal) {
2076
1818
  if (!this._usbConnector) {
2077
1819
  return failure(
2078
1820
  HardwareErrorCode.TransportNotAvailable,
@@ -2093,8 +1835,9 @@ var KeystoneAdapter = class _KeystoneAdapter {
2093
1835
  const connector = this._usbConnector;
2094
1836
  const session = await (connector.connectTarget ? connector.connectTarget(connectTarget) : connector.connect(searchTargetId ?? expected.expectedMasterFingerprint));
2095
1837
  sessionId = session.sessionId;
1838
+ _KeystoneAdapter._throwIfAborted(signal);
2096
1839
  const raw = session.deviceInfo.raw;
2097
- const mfpValue = parseBip32MasterFingerprint2(raw?.masterFingerprint);
1840
+ const mfpValue = parseBip32MasterFingerprint3(raw?.masterFingerprint);
2098
1841
  if (!mfpValue) {
2099
1842
  throw createHwkError({
2100
1843
  code: HardwareErrorCode.DeviceMismatch,
@@ -2108,40 +1851,26 @@ var KeystoneAdapter = class _KeystoneAdapter {
2108
1851
  message: `Connected Keystone wallet (mfp ${masterFingerprint}) does not match the requested wallet fingerprint (${expected.expectedMasterFingerprint})`
2109
1852
  });
2110
1853
  }
2111
- const identityRequest = this.urEngine.buildKeyDerivationRequest({
2112
- schemas: [{ path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" }],
2113
- origin: this._origin
2114
- });
2115
- const identityResult = await this._callUsbConnector(
2116
- session.sessionId,
2117
- "resolveUr",
2118
- identityRequest
2119
- );
2120
- if (!identityResult.success) {
2121
- throw rehydrateConnectorError(identityResult.error);
2122
- }
2123
- const parsed = this.urEngine.parseAccountResponse(identityResult.payload);
2124
- this._assertParsedIdentity(parsed, {
2125
- ...expected,
2126
- expectedMasterFingerprint: expected.expectedMasterFingerprint ?? masterFingerprint
2127
- });
2128
- const walletId = deriveKeystoneWalletId(parsed.accounts);
2129
- const previousUsbSessionId = this._devices.get(walletId)?.usbSessionId;
2130
- const record = this._upsertDeviceRecord(parsed, {
2131
- viaUsb: true,
2132
- usbSessionId: session.sessionId
2133
- });
1854
+ const previousUsbSessionId = this._devices.get(masterFingerprint)?.usbSessionId;
2134
1855
  if (previousUsbSessionId && previousUsbSessionId !== session.sessionId) {
2135
1856
  try {
2136
1857
  await this._usbConnector.disconnect(previousUsbSessionId);
2137
1858
  } catch {
2138
1859
  }
2139
1860
  }
1861
+ _KeystoneAdapter._throwIfAborted(signal);
1862
+ const record = this._upsertDeviceRecord(
1863
+ { masterFingerprint, accounts: [] },
1864
+ {
1865
+ viaUsb: true,
1866
+ usbSessionId: session.sessionId
1867
+ }
1868
+ );
1869
+ _KeystoneAdapter._throwIfAborted(signal);
2140
1870
  record.hadUsbSession = true;
2141
1871
  record.model = session.deviceInfo.modelName ?? session.deviceInfo.model ?? record.model;
2142
1872
  record.deviceVersion = session.deviceInfo.firmwareVersion ?? record.deviceVersion;
2143
- const info = toDeviceInfo(record);
2144
- return success(info);
1873
+ return success(toDeviceInfo(record));
2145
1874
  } catch (err) {
2146
1875
  if (sessionId) {
2147
1876
  try {
@@ -2153,31 +1882,34 @@ var KeystoneAdapter = class _KeystoneAdapter {
2153
1882
  }
2154
1883
  }
2155
1884
  /**
2156
- * Probe already-authorized USB devices without opening a permission picker,
2157
- * then attach and verify the requested wallet when one is present. This is
2158
- * used both after an adapter restart and after a QR-only period, so plugging
2159
- * USB back in affects the next business call without changing an in-flight
2160
- * QR operation.
1885
+ * Probes already-authorized USB devices without a permission picker, then attaches and verifies
1886
+ * the requested wallet if present.
2161
1887
  */
2162
- async _tryUsbAttach(expectedWalletId, expectedMasterFingerprint, options) {
2163
- if (!expectedWalletId || this._forcedTransport === "qr" || !this._usbConnector) {
1888
+ async _tryUsbAttach(signal, expectedMasterFingerprint, options) {
1889
+ _KeystoneAdapter._throwIfAborted(signal);
1890
+ if (!expectedMasterFingerprint || this._forcedTransport === "qr" || !this._usbConnector) {
2164
1891
  return void 0;
2165
1892
  }
2166
1893
  const maxAttempts = options?.waitForReenumeration ? KEYSTONE_USB_REATTACH_PROBE_ATTEMPTS : 1;
2167
1894
  let availableDevices = [];
2168
1895
  let lastSearchError;
2169
1896
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1897
+ _KeystoneAdapter._throwIfAborted(signal);
2170
1898
  try {
2171
- availableDevices = await this._usbConnector.searchDevices({ purpose: "availability" });
1899
+ availableDevices = await _KeystoneAdapter._abortable(
1900
+ signal,
1901
+ this._usbConnector.searchDevices({ purpose: "availability" })
1902
+ );
2172
1903
  lastSearchError = void 0;
2173
1904
  } catch (error) {
2174
1905
  lastSearchError = error;
2175
1906
  }
1907
+ _KeystoneAdapter._throwIfAborted(signal);
2176
1908
  if (availableDevices.length) {
2177
1909
  break;
2178
1910
  }
2179
1911
  if (attempt < maxAttempts) {
2180
- await waitForKeystoneUsbReattachProbe();
1912
+ await _KeystoneAdapter._abortable(signal, waitForKeystoneUsbReattachProbe());
2181
1913
  }
2182
1914
  }
2183
1915
  if (!availableDevices.length) {
@@ -2186,30 +1918,19 @@ var KeystoneAdapter = class _KeystoneAdapter {
2186
1918
  }
2187
1919
  return void 0;
2188
1920
  }
2189
- let attached;
2190
- let record;
2191
- const candidates = expectedMasterFingerprint ? [void 0] : availableDevices;
2192
- for (const device of candidates) {
2193
- attached = await this._connectUsb(
2194
- { expectedWalletId, expectedMasterFingerprint },
2195
- device?.connectId
2196
- );
2197
- if (attached.success) {
2198
- record = this._devices.get(expectedWalletId);
2199
- if (record) break;
2200
- }
1921
+ const attached = await this._connectUsb({ expectedMasterFingerprint }, void 0, signal);
1922
+ _KeystoneAdapter._throwIfAborted(signal);
1923
+ if (!attached.success && attached.payload.code === HardwareErrorCode.DeviceBusyInternal) {
1924
+ throw createHwkError({
1925
+ ...attached.payload,
1926
+ message: attached.payload.error
1927
+ });
2201
1928
  }
2202
- return record;
1929
+ return attached.success ? this._devices.get(expectedMasterFingerprint) : void 0;
2203
1930
  }
2204
1931
  /**
2205
- * The one place that decides QR vs. USB for a UR round trip and carries it
2206
- * out. `record` is the (possibly not-yet-existing, for a true cold start)
2207
- * device row for the target wallet. USB is the preferred channel: a known
2208
- * wallet record with no live session gets ONE best-effort re-attach here,
2209
- * and anything that goes wrong just leaves the call on QR. A
2210
- * `switchTransport('qr')` pin forces QR even for a USB-attached wallet;
2211
- * `switchTransport('usb')` on a wallet with no live USB session fails
2212
- * closed rather than silently falling back to QR.
1932
+ * Decides QR vs USB for one UR round trip. A pinned 'usb' with no live session fails closed;
1933
+ * otherwise a failed USB reattach leaves the call on QR.
2213
1934
  */
2214
1935
  async _resolveUr(record, requestUr, animated, operationId, signal, operationName) {
2215
1936
  const releaseOperationRetention = isHardwareOperationId2(operationId) ? this._operations.retain(operationId) : void 0;
@@ -2226,39 +1947,33 @@ var KeystoneAdapter = class _KeystoneAdapter {
2226
1947
  releaseOperationRetention?.();
2227
1948
  }
2228
1949
  }
2229
- async _resolveUrWithRoute(record, requestUr, animated, operationId, signal, operationName) {
1950
+ async _resolveUrWithRoute(callerRecord, requestUr, animated, operationId, signal, operationName) {
1951
+ let record = callerRecord;
2230
1952
  let interactionRoute;
2231
1953
  if (isHardwareOperationId2(operationId)) {
2232
1954
  const operation = this._operations.resolve(operationId);
2233
1955
  const route = this._operationRoutes.get(operation.connectId);
2234
1956
  if (!route || route.operationId !== operationId) {
2235
- this._operations.end(operationId, "disconnect");
2236
- throw createHwkError({
2237
- code: HardwareErrorCode.OperationEnded,
2238
- message: "Keystone hardware operation is no longer connected",
2239
- params: { operationId, reason: "disconnect" }
2240
- });
1957
+ throw this._endDisconnectedOperation(
1958
+ operationId,
1959
+ "Keystone hardware operation is no longer connected"
1960
+ );
2241
1961
  }
2242
1962
  interactionRoute = route;
2243
1963
  }
2244
1964
  if (!interactionRoute && !this._forcedTransport && record && !record.usbSessionId && this._usbConnector) {
2245
- await this._tryUsbAttach(record.walletId, record.masterFingerprint, {
1965
+ record = await this._tryUsbAttach(signal, record.masterFingerprint, {
2246
1966
  waitForReenumeration: record.hadUsbSession
2247
- });
1967
+ }) ?? record;
2248
1968
  }
2249
1969
  const wantUsb = interactionRoute ? interactionRoute.connectionType === "usb" : this._forcedTransport === "usb" || this._forcedTransport !== "qr" && Boolean(record?.usbSessionId);
2250
1970
  if (wantUsb) {
2251
1971
  if (!record?.usbSessionId || !this._usbConnector) {
2252
1972
  if (interactionRoute) {
2253
- this._operations.end(interactionRoute.operationId, "disconnect");
2254
- throw createHwkError({
2255
- code: HardwareErrorCode.OperationEnded,
2256
- message: "Keystone operation USB connection was lost",
2257
- params: {
2258
- operationId: interactionRoute.operationId,
2259
- reason: "disconnect"
2260
- }
2261
- });
1973
+ throw this._endDisconnectedOperation(
1974
+ interactionRoute.operationId,
1975
+ "Keystone operation USB connection was lost"
1976
+ );
2262
1977
  }
2263
1978
  throw createHwkError({
2264
1979
  code: HardwareErrorCode.TransportNotAvailable,
@@ -2273,15 +1988,17 @@ var KeystoneAdapter = class _KeystoneAdapter {
2273
1988
  );
2274
1989
  if (result.success) return result.payload;
2275
1990
  const usbError = rehydrateConnectorError(result.error);
2276
- const usbErrorOrigin = usbError.origin;
2277
- const usbErrorCode = usbError.code;
2278
- if (!interactionRoute && !this._forcedTransport && usbErrorCode === HardwareErrorCode.PayloadTooLarge && usbErrorOrigin !== "device") {
2279
- const displayDevice2 = toDeviceInfo(record);
2280
- return this._requestQrDisplayAndAwaitResponse(
2281
- displayDevice2,
2282
- { ...requestUr, animated },
2283
- signal
2284
- );
1991
+ const { origin: usbErrorOrigin, code: usbErrorCode } = usbError;
1992
+ if (usbErrorCode === HardwareErrorCode.PayloadTooLarge && usbErrorOrigin !== "device") {
1993
+ if (!interactionRoute && !this._forcedTransport) {
1994
+ const displayDevice2 = toDeviceInfo(record);
1995
+ return this._requestQrDisplayAndAwaitResponse(
1996
+ displayDevice2,
1997
+ { ...requestUr, animated },
1998
+ signal
1999
+ );
2000
+ }
2001
+ throw usbError;
2285
2002
  }
2286
2003
  const deviceAnswered = usbErrorOrigin !== void 0 ? usbErrorOrigin === "device" : usbErrorCode === HardwareErrorCode.UserRejected || usbErrorCode === HardwareErrorCode.UserAborted || usbErrorCode === HardwareErrorCode.DeviceLocked || usbErrorCode === HardwareErrorCode.DeviceMismatch;
2287
2004
  if (!deviceAnswered) {
@@ -2291,19 +2008,11 @@ var KeystoneAdapter = class _KeystoneAdapter {
2291
2008
  }
2292
2009
  record.usbSessionId = void 0;
2293
2010
  if (interactionRoute) {
2294
- this._operations.end(interactionRoute.operationId, "disconnect");
2295
- throw createHwkError({
2296
- code: HardwareErrorCode.OperationEnded,
2297
- message: operationName ? `Keystone ${operationName} may have completed before the USB connection was lost` : "Keystone operation USB connection was lost",
2298
- recovery: operationName ? { scope: "unknown" } : void 0,
2299
- params: operationName ? operationMayHaveCompletedParams(operationName, {
2300
- operationId: interactionRoute.operationId,
2301
- reason: "disconnect"
2302
- }) : {
2303
- operationId: interactionRoute.operationId,
2304
- reason: "disconnect"
2305
- }
2306
- });
2011
+ throw this._endDisconnectedOperation(
2012
+ interactionRoute.operationId,
2013
+ operationName ? `Keystone ${operationName} may have completed before the USB connection was lost` : "Keystone operation USB connection was lost",
2014
+ operationName
2015
+ );
2307
2016
  }
2308
2017
  if (operationName) {
2309
2018
  throw createHwkError({
@@ -2337,51 +2046,49 @@ var KeystoneAdapter = class _KeystoneAdapter {
2337
2046
  throw error;
2338
2047
  }
2339
2048
  }
2049
+ /** Ends the operation as disconnected and returns the error to throw. */
2050
+ _endDisconnectedOperation(operationId, message, operationName) {
2051
+ this._operations.end(operationId, "disconnect");
2052
+ const params = { operationId, reason: "disconnect" };
2053
+ return createHwkError({
2054
+ code: HardwareErrorCode.OperationEnded,
2055
+ message,
2056
+ recovery: operationName ? { scope: "unknown" } : void 0,
2057
+ params: operationName ? operationMayHaveCompletedParams(operationName, params) : params
2058
+ });
2059
+ }
2340
2060
  /**
2341
- * Fetch one account's key material from the device for this operation only.
2342
- * `book` lets earlier fetches of the same operation be reused; nothing is
2343
- * retained on the record. A failed USB export ends this operation without
2344
- * replaying the request or switching transports.
2061
+ * Fetches one account's key material for this operation only; `book` reuses earlier fetches. A
2062
+ * failed USB export ends the operation without replaying or switching transports.
2345
2063
  */
2346
- async _fetchAccount(connectId, deviceId, hwkChain, syncPath, signal, options) {
2064
+ async _fetchAccount(connectId, deviceId, hwkChain, syncPath, signal, book) {
2347
2065
  const target = this._resolveTarget(connectId, deviceId);
2066
+ const operationConnectionType = isHardwareOperationId2(connectId) ? this._operations.resolve(connectId).connectionType : void 0;
2348
2067
  const key = accountKey(hwkChain, syncPath);
2349
2068
  let existingRecord = target.record;
2350
2069
  if (!existingRecord) {
2351
- const attached = await this._tryUsbAttach(target.expectedWalletId);
2070
+ existingRecord = await this._tryUsbAttach(signal, target.expectedMasterFingerprint);
2352
2071
  _KeystoneAdapter._throwIfAborted(signal);
2353
- existingRecord = attached;
2354
2072
  }
2355
- const book = options?.book;
2356
2073
  const booked = book?.get(key);
2357
2074
  if (existingRecord && booked) {
2358
2075
  return { record: existingRecord, account: booked };
2359
2076
  }
2360
- if (existingRecord && !existingRecord.usbSessionId && this._forcedTransport === "usb") {
2361
- await this._tryUsbAttach(existingRecord.walletId, existingRecord.masterFingerprint, {
2077
+ if (!operationConnectionType && existingRecord && !existingRecord.usbSessionId && this._forcedTransport === "usb") {
2078
+ await this._tryUsbAttach(signal, existingRecord.masterFingerprint, {
2362
2079
  waitForReenumeration: existingRecord.hadUsbSession
2363
2080
  });
2364
2081
  _KeystoneAdapter._throwIfAborted(signal);
2365
- existingRecord = this._devices.get(existingRecord.walletId) ?? existingRecord;
2082
+ existingRecord = this._devices.get(existingRecord.masterFingerprint) ?? existingRecord;
2366
2083
  }
2367
- const useSinglePathUsbExport = this._forcedTransport !== "qr" && Boolean(existingRecord?.usbSessionId);
2368
- const requestedSchemaPaths = [
2369
- { hwkChain, path: syncPath }
2370
- ];
2371
- const schemaPaths = useSinglePathUsbExport || requestedSchemaPaths.some((schema) => normalizePath(schema.path) === KEYSTONE_WALLET_ID_PATH) ? requestedSchemaPaths : [{ hwkChain: "evm", path: KEYSTONE_WALLET_ID_PATH }, ...requestedSchemaPaths];
2372
- const requestUr = this.urEngine.buildKeyDerivationRequest({
2373
- schemas: schemaPaths.map((s) => ({
2374
- path: s.path,
2375
- curve: s.hwkChain === "sol" ? "ed25519" : "secp256k1"
2376
- })),
2377
- origin: this._origin
2378
- });
2379
- const resolvedViaUsb = useSinglePathUsbExport;
2084
+ const useSinglePathUsbExport = operationConnectionType ? operationConnectionType === "usb" : this._forcedTransport !== "qr" && Boolean(existingRecord?.usbSessionId);
2085
+ const schemas = [keySchema(hwkChain, syncPath)];
2086
+ const requestUr = this.urEngine.buildKeyDerivationRequest({ schemas, origin: this._origin });
2380
2087
  const responseUr = await this._resolveUr(existingRecord, requestUr, false, connectId, signal);
2381
2088
  _KeystoneAdapter._throwIfAborted(signal);
2382
2089
  const parsed = this.urEngine.parseAccountResponse(responseUr);
2383
2090
  let record;
2384
- if (resolvedViaUsb && existingRecord) {
2091
+ if (useSinglePathUsbExport && existingRecord) {
2385
2092
  if (parsed.masterFingerprint !== existingRecord.masterFingerprint) {
2386
2093
  throw createHwkError({
2387
2094
  code: HardwareErrorCode.DeviceMismatch,
@@ -2392,7 +2099,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
2392
2099
  } else {
2393
2100
  this._assertParsedIdentity(parsed, target);
2394
2101
  record = this._upsertDeviceRecord(parsed, {
2395
- viaUsb: Boolean(existingRecord?.usbSessionId)
2102
+ viaUsb: operationConnectionType !== "qr" && Boolean(existingRecord?.usbSessionId)
2396
2103
  });
2397
2104
  }
2398
2105
  const account = parsed.accounts.find((a) => normalizePath(a.path) === syncPath);
@@ -2418,52 +2125,30 @@ var KeystoneAdapter = class _KeystoneAdapter {
2418
2125
  return { record, account: entry };
2419
2126
  }
2420
2127
  /**
2421
- * Like `_fetchAccount`, but for operations (PSBT signing, master
2422
- * fingerprint) that only need to know WHICH wallet is attached, not a
2423
- * specific cached path. Syncs the account-level path for `chain` as a
2424
- * throwaway probe when the wallet record isn't already known.
2425
- *
2426
- * `CHAIN_FINGERPRINT_PATHS[chain]` is a 5-segment LEAF path for `evm`
2427
- * (`m/44'/60'/0'/0/0`) — sending that verbatim as a KeyDerivation request
2428
- * asks Keystone for a non-standard path. Keystone's own docs
2429
- * (dev.keyst.one's multichain KeyDerivation example) show the ETH
2430
- * account-level path as `m/44'/60'/0'` (3 segments), same as what
2431
- * `_fetchAccount` already requests — so
2432
- * truncate through `splitAccountPath` here too instead of using the raw
2433
- * fingerprint leaf path. `btc`/`sol` are already 3-segment account paths
2434
- * and pass through unchanged.
2128
+ * Resolves which wallet is attached, without a per-path account. `CHAIN_FINGERPRINT_PATHS.evm` is
2129
+ * a leaf path, so it is cut to the account path Keystone expects.
2435
2130
  */
2436
2131
  async _ensureWalletKnown(connectId, deviceId, chain, signal) {
2437
2132
  const target = this._resolveTarget(connectId, deviceId);
2438
2133
  if (target.record) return { record: target.record };
2439
- const attached = await this._tryUsbAttach(target.expectedWalletId);
2134
+ const attached = await this._tryUsbAttach(signal, target.expectedMasterFingerprint);
2440
2135
  _KeystoneAdapter._throwIfAborted(signal);
2441
2136
  if (attached) return { record: attached };
2137
+ if (target.expectedMasterFingerprint) {
2138
+ return { record: createDeviceRecord(target.expectedMasterFingerprint) };
2139
+ }
2442
2140
  const { accountPath } = splitAccountPath(CHAIN_FINGERPRINT_PATHS[chain]);
2443
- const schemas = accountPath === KEYSTONE_WALLET_ID_PATH ? [{ path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" }] : [
2444
- { path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" },
2445
- {
2446
- path: accountPath,
2447
- curve: chain === "sol" ? "ed25519" : "secp256k1"
2448
- }
2449
- ];
2450
2141
  const requestUr = this.urEngine.buildKeyDerivationRequest({
2451
- schemas,
2142
+ schemas: [keySchema(chain, accountPath)],
2452
2143
  origin: this._origin
2453
2144
  });
2454
2145
  const responseUr = await this._resolveUr(void 0, requestUr, false, connectId, signal);
2455
2146
  _KeystoneAdapter._throwIfAborted(signal);
2456
- const parsed = this.urEngine.parseAccountResponse(responseUr);
2457
- this._assertParsedIdentity(parsed, target);
2458
- const record = this._upsertDeviceRecord(parsed);
2459
- return { record };
2147
+ return { record: this._upsertDeviceRecord(this.urEngine.parseAccountResponse(responseUr)) };
2460
2148
  }
2461
2149
  /**
2462
- * The operation the running device job belongs to, so a UI request raised
2463
- * mid-call can name it. A call pinned to an operation queues under its id
2464
- * (`keystoneQueueKey`); a call without one queues under the wallet id or the
2465
- * wallet connectId, and the live operation routed to that connection is still
2466
- * the owner. Only a cold start comes back undefined.
2150
+ * The operation owning the running job, so a mid-call UI request can name it. Unpinned jobs map
2151
+ * through the operation route; a cold start returns undefined.
2467
2152
  */
2468
2153
  _activeOperationId() {
2469
2154
  const activeJobId = this._jobQueue.getActiveJob()?.deviceId;
@@ -2471,19 +2156,18 @@ var KeystoneAdapter = class _KeystoneAdapter {
2471
2156
  if (isHardwareOperationId2(activeJobId)) return activeJobId;
2472
2157
  return this._operationRouteForIdentifier(activeJobId)?.operationId;
2473
2158
  }
2474
- /** Routes are keyed by connectId; a job key may be the bare wallet id. */
2159
+ /** Routes are keyed by connectId; a job key may be the bare master fingerprint. */
2475
2160
  _operationRouteForIdentifier(identifier) {
2476
2161
  for (const key of keystoneCancelQueueKeys([identifier])) {
2477
2162
  const route = this._operationRoutes.get(key);
2478
2163
  if (route) return route;
2479
2164
  }
2480
- const walletId = keystoneWalletIdFromIdentifier(identifier);
2481
- return walletId ? this._operationRoutes.get(walletConnectId(walletId)) : void 0;
2165
+ const masterFingerprint = keystoneMfpFromIdentifier(identifier);
2166
+ return masterFingerprint ? this._operationRoutes.get(walletConnectId(masterFingerprint)) : void 0;
2482
2167
  }
2483
2168
  /**
2484
- * A QR prompt has no transport to interrupt, so an aborted job would sit here
2485
- * until the QR timeout. Releasing the registry slot on abort is what lets a
2486
- * cancel reach the wait at all.
2169
+ * A QR prompt has no transport to interrupt; releasing the registry slot on abort is what lets
2170
+ * a cancel reach the wait before the QR timeout.
2487
2171
  */
2488
2172
  async _awaitQrResponse(requestType, pending, signal) {
2489
2173
  if (!signal) return pending;
@@ -2552,8 +2236,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
2552
2236
  const releaseOperation = this._retainUsbOperation(`call:${sessionId}`);
2553
2237
  let rawCall;
2554
2238
  try {
2555
- const connector = this._usbConnector;
2556
- rawCall = connector.call(sessionId, method, params).finally(releaseOperation);
2239
+ rawCall = this._usbConnector.call(sessionId, method, params).finally(releaseOperation);
2557
2240
  } catch (error) {
2558
2241
  releaseOperation();
2559
2242
  throw error;
@@ -2603,13 +2286,10 @@ var KeystoneAdapter = class _KeystoneAdapter {
2603
2286
  };
2604
2287
  }
2605
2288
  static _abortable(signal, promise) {
2606
- if (signal.aborted) {
2607
- return Promise.reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted"));
2608
- }
2289
+ const abortReason = () => signal.reason instanceof Error ? signal.reason : new Error("Aborted");
2290
+ if (signal.aborted) return Promise.reject(abortReason());
2609
2291
  return new Promise((resolve, reject) => {
2610
- const onAbort = () => {
2611
- reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted"));
2612
- };
2292
+ const onAbort = () => reject(abortReason());
2613
2293
  signal.addEventListener("abort", onAbort, { once: true });
2614
2294
  promise.then(
2615
2295
  (value) => {
@@ -2624,7 +2304,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
2624
2304
  });
2625
2305
  }
2626
2306
  static _throwIfAborted(signal) {
2627
- if (signal.aborted) {
2307
+ if (signal?.aborted) {
2628
2308
  throw signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
2629
2309
  }
2630
2310
  }
@@ -2676,14 +2356,13 @@ var KeystoneAdapter = class _KeystoneAdapter {
2676
2356
  }
2677
2357
  };
2678
2358
  export {
2359
+ KEYSTONE_COLD_START_PATH,
2679
2360
  KEYSTONE_WALLET_CONNECT_ID_PREFIX,
2680
- KEYSTONE_WALLET_ID_PATH,
2681
2361
  KeystoneAdapter,
2682
2362
  KeystoneUrEngine,
2683
2363
  accountKey,
2684
2364
  btcScriptTypeFromPath,
2685
2365
  createDeviceRecord,
2686
- deriveKeystoneWalletId,
2687
2366
  normalizePath,
2688
2367
  placeholderDeviceInfo,
2689
2368
  splitAccountPath,