@onekeyfe/hwk-keystone-adapter 1.2.3-alpha.4 → 1.2.5-alpha.0

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
@@ -27,6 +27,7 @@ var TronSignRequest = class _TronSignRequest extends RegistryItem {
27
27
  this.getSignData = () => this.signData;
28
28
  this.getSignType = () => this.signType;
29
29
  this.getDerivationPath = () => this.derivationPath.getPath();
30
+ this.getOrigin = () => this.origin;
30
31
  this.toDataItem = () => {
31
32
  const map = {};
32
33
  if (this.requestId) {
@@ -105,6 +106,9 @@ var TronSignature = class _TronSignature extends RegistryItem2 {
105
106
  static fromDataItem(dataItem) {
106
107
  const map = dataItem.getData();
107
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
+ }
108
112
  const requestIdItem = map[1 /* requestId */];
109
113
  const requestId = requestIdItem?.getData();
110
114
  return new _TronSignature(signature, requestId);
@@ -135,14 +139,22 @@ function toSdkUr(ur) {
135
139
  function fromSdkUr(ur) {
136
140
  return { urType: ur.type, urData: ur.cbor.toString("hex") };
137
141
  }
138
- function splitSignature65(hex) {
139
- if (hex.length !== 130) {
142
+ function splitEvmSignature(hex) {
143
+ if (hex.length < 130 || hex.length % 2 !== 0) {
140
144
  throw new Error(
141
- `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)`
142
146
  );
143
147
  }
144
148
  return { r: hex.slice(0, 64), s: hex.slice(64, 128), v: hex.slice(128) };
145
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
+ }
146
158
  function requireBip32MasterFingerprint(value) {
147
159
  const fingerprint = parseBip32MasterFingerprint(value);
148
160
  if (!fingerprint) {
@@ -150,12 +162,24 @@ function requireBip32MasterFingerprint(value) {
150
162
  }
151
163
  return fingerprint;
152
164
  }
165
+ function toParsedAccount(key) {
166
+ return {
167
+ chain: key.chain,
168
+ path: key.path,
169
+ publicKey: key.publicKey,
170
+ extendedPublicKey: key.extendedPublicKey,
171
+ xfp: key.xfp ? requireBip32MasterFingerprint(key.xfp) : void 0,
172
+ name: key.name
173
+ };
174
+ }
175
+ function relativeHdPath(relativeDerivePath) {
176
+ return `m/${relativeDerivePath.replace(/^m\//i, "")}`;
177
+ }
153
178
  var KeystoneUrEngine = class {
154
179
  constructor(origin = "OneKey") {
155
180
  this.sdk = new KeystoneSDK({ origin });
156
181
  }
157
- // --- Account sync (works for both a device-initiated QR export and a
158
- // wallet-initiated KeyDerivation request/response over either channel) ---
182
+ // --- Account sync (device-initiated export or KeyDerivation response) ---
159
183
  parseMultiAccounts(ur) {
160
184
  const parsed = this.sdk.parseMultiAccounts(toSdkUr(ur));
161
185
  return {
@@ -163,46 +187,15 @@ var KeystoneUrEngine = class {
163
187
  device: parsed.device,
164
188
  deviceId: parsed.deviceId,
165
189
  deviceVersion: parsed.deviceVersion,
166
- accounts: parsed.keys.map(
167
- (key) => ({
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
- )
190
+ accounts: parsed.keys.map(toParsedAccount)
176
191
  };
177
192
  }
178
193
  parseHDKey(ur) {
179
- const key = this.sdk.parseHDKey(toSdkUr(ur));
180
- return {
181
- chain: key.chain,
182
- path: key.path,
183
- publicKey: key.publicKey,
184
- extendedPublicKey: key.extendedPublicKey,
185
- xfp: key.xfp ? requireBip32MasterFingerprint(key.xfp) : void 0,
186
- name: key.name
187
- };
194
+ return toParsedAccount(this.sdk.parseHDKey(toSdkUr(ur)));
188
195
  }
189
196
  /**
190
- * Build a `qr-hardware-call` (KeyDerivation) request: the host asks for
191
- * specific paths instead of waiting for whatever the device happens to be
192
- * showing. The device replies with a `crypto-multi-accounts` UR — parse it
193
- * with `parseMultiAccounts`. Used for the implicit "sync this wallet's xfp
194
- * before the first sign" round trip as well as an explicit account import.
195
- *
196
- * `version: V1` is required — verified against real Keystone hardware and
197
- * `keystone3-firmware`'s `CheckHardwareCallRequestIsLegal` source: an
198
- * unversioned/V0 request is validated as a legacy Cardano-only request
199
- * (`m/1852'/1815'/...`) and firmware rejects every other chain's path with
200
- * `PRS_PARSING_ERROR` (device-shown message: "路径不受支持" / "path not
201
- * supported"), regardless of the path's shape. V1 is what actually enables
202
- * the general per-chain path whitelist (includes `m/44'/60'` for ETH, the
203
- * standard BTC purposes, etc.). The SDK itself defaults to V0 unless a
204
- * truthy `version` is passed — omitting this silently produces a request
205
- * every non-Cardano device rejects.
197
+ * Builds a KeyDerivation request. `version: V1` is required: firmware validates V0 (the SDK
198
+ * default) as Cardano-only and rejects other paths with `PRS_PARSING_ERROR`.
206
199
  */
207
200
  buildKeyDerivationRequest(input) {
208
201
  const ur = this.sdk.generateKeyDerivationCall({
@@ -216,14 +209,8 @@ var KeystoneUrEngine = class {
216
209
  return fromSdkUr(ur);
217
210
  }
218
211
  /**
219
- * Parse the response to a KeyDerivation request (or a device-initiated
220
- * account export): `crypto-multi-accounts` for a multi-schema request,
221
- * `crypto-hdkey` for a single-key response some firmware paths use instead.
222
- * Both are normalized to the same `KeystoneParsedMultiAccounts` shape — a
223
- * single `crypto-hdkey` becomes a one-entry account list, with its own
224
- * `origin.sourceFingerprint` promoted to `masterFingerprint` (correct for a
225
- * key derived directly from the seed, which every request this engine
226
- * builds asks for).
212
+ * Normalizes `crypto-multi-accounts` and the single-key `crypto-hdkey` some firmware returns. The
213
+ * hdkey source fingerprint is the mfp only because every request here derives from the seed.
227
214
  */
228
215
  parseAccountResponse(ur) {
229
216
  if (ur.urType === "crypto-hdkey") {
@@ -254,37 +241,19 @@ var KeystoneUrEngine = class {
254
241
  }
255
242
  parseEthSignature(ur) {
256
243
  const signature = this.sdk.eth.parseSignature(toSdkUr(ur));
257
- return { requestId: signature.requestId, ...splitSignature65(signature.signature) };
244
+ return { requestId: signature.requestId, ...splitEvmSignature(signature.signature) };
258
245
  }
259
- /**
260
- * Derive one EVM address offline from an already-synced account xpub —
261
- * verified against the same `@keystonehq/bc-ur-registry-eth` helper the
262
- * Keystone-based OneKey air-gap demo uses in production
263
- * (`generateAddressFromXpub`), so no unverified assumption about what a
264
- * leaf-path KeyDerivation request would return. `relativeDerivePath` is
265
- * relative to the xpub's own depth, e.g. `'0/0'` for an account xpub.
266
- */
246
+ /** Derives an EVM address offline; `relativeDerivePath` is relative to the xpub, e.g. `'0/0'`. */
267
247
  deriveEvmAddressFromXpub(xpub, relativeDerivePath) {
268
- return generateAddressFromXpub(xpub, `m/${relativeDerivePath.replace(/^m\//i, "")}`);
248
+ return generateAddressFromXpub(xpub, relativeHdPath(relativeDerivePath));
269
249
  }
270
250
  // --- BTC (PSBT transaction signing + plain message signing) ---
271
251
  /**
272
- * Derive one BTC address offline from an already-synced account xpub, the
273
- * same way `deriveEvmAddressFromXpub` does. Keystone's `CryptoHDKey`
274
- * always emits standard mainnet-xpub version bytes (`0488B21E`) regardless
275
- * of the account's purpose/script type (verified against
276
- * `@keystonehq/bc-ur-registry`'s `CryptoHDKey.getBip32Key`, which hardcodes
277
- * that version rather than switching to a SLIP-132 ypub/zpub prefix per
278
- * script type) — so `hdkey.fromExtendedKey` parses it correctly for every
279
- * `scriptType` without needing custom version bytes configured.
280
- *
281
- * `p2tr` is deliberately not handled: taproot output-key tweaking (BIP-341)
282
- * needs an elliptic-curve library wired via bitcoinjs-lib's `initEccLib`,
283
- * which this package doesn't set up yet — every other payment function
284
- * here needs no such library.
252
+ * `CryptoHDKey` always emits mainnet xpub version bytes (`0488B21E`), so `hdkey` parses it as is.
253
+ * `p2tr` needs an ECC lib for BIP-341 tweaking, not wired here.
285
254
  */
286
255
  deriveBtcAddressFromXpub(xpub, relativeDerivePath, scriptType) {
287
- const node = HDKey.fromExtendedKey(xpub).derive(`m/${relativeDerivePath.replace(/^m\//i, "")}`);
256
+ const node = HDKey.fromExtendedKey(xpub).derive(relativeHdPath(relativeDerivePath));
288
257
  if (!node.publicKey) throw new Error("HDKey derivation did not produce a public key");
289
258
  return this.deriveBtcAddressFromPublicKey(
290
259
  Buffer.from(node.publicKey).toString("hex"),
@@ -337,7 +306,7 @@ var KeystoneUrEngine = class {
337
306
  requestId: params.requestId,
338
307
  signData: params.messageHex,
339
308
  dataType: 1,
340
- // BtcSignRequest.DataType.message — PSBT signing never goes through this path.
309
+ // BtcSignRequest.DataType.message; PSBT signing never goes through this path.
341
310
  accounts: params.accounts,
342
311
  origin: params.origin
343
312
  });
@@ -366,7 +335,10 @@ var KeystoneUrEngine = class {
366
335
  }
367
336
  parseSolSignature(ur) {
368
337
  const signature = this.sdk.sol.parseSignature(toSdkUr(ur));
369
- return { requestId: signature.requestId, signature: signature.signature };
338
+ return {
339
+ requestId: signature.requestId,
340
+ signature: requireSignatureBytes(signature.signature, 64, "SOL")
341
+ };
370
342
  }
371
343
  // --- TRON ---
372
344
  /**
@@ -391,37 +363,22 @@ var KeystoneUrEngine = class {
391
363
  const requestId = signature.getRequestId();
392
364
  return {
393
365
  requestId: requestId ? uuidStringify(requestId) : void 0,
394
- signature: signature.getSignature().toString("hex")
366
+ signature: requireSignatureBytes(signature.getSignature().toString("hex"), 65, "TRON")
395
367
  };
396
368
  }
397
369
  /**
398
- * Derive one TRON address offline from an already-synced account xpub.
399
- * TRON reuses EVM's exact secp256k1-pubkey → keccak256 → last-20-bytes
400
- * derivation (verified against Keystone's own `formatAddress()` in
401
- * `keystone-sdk`'s TRON chain source) — only the final text encoding
402
- * differs (base58check with a `0x41` version byte, not checksummed hex).
403
- * Reusing `generateAddressFromXpub` here means no new hashing dependency:
404
- * strip its "0x" and re-encode the same 20 bytes.
370
+ * TRON uses the same 20 address bytes as EVM (Keystone's `formatAddress()`), re-encoded as
371
+ * base58check with the `0x41` version byte.
405
372
  */
406
373
  deriveTronAddressFromXpub(xpub, relativeDerivePath) {
407
- const evmStyleHex = generateAddressFromXpub(
408
- xpub,
409
- `m/${relativeDerivePath.replace(/^m\//i, "")}`
410
- );
374
+ const evmStyleHex = generateAddressFromXpub(xpub, relativeHdPath(relativeDerivePath));
411
375
  const addressBytes = Buffer.concat([
412
376
  Buffer.from([TRON_ADDRESS_PREFIX]),
413
- Buffer.from(evmStyleHex.replace(/^0x/i, ""), "hex")
377
+ Buffer.from(stripHexPrefix(evmStyleHex), "hex")
414
378
  ]);
415
379
  return bs58check.encode(addressBytes);
416
380
  }
417
- /**
418
- * Split an account-level xpub back into the BIP-32 fields a host needs to
419
- * treat it as a real extended key (`BtcPublicKey`). Everything here is
420
- * carried inside the xpub's own serialization — depth, the PARENT key
421
- * fingerprint (not the seed's master fingerprint), the chain code and the
422
- * compressed public key — so this is a pure decode with no device round
423
- * trip; the xpub itself was already device-verified when it was synced.
424
- */
381
+ /** Decodes an xpub's BIP-32 fields; `parentFingerprint` is the parent key's, not the mfp. */
425
382
  parseXpubMeta(xpub) {
426
383
  const raw = Buffer.from(bs58check.decode(xpub));
427
384
  if (raw.length !== 78) {
@@ -461,8 +418,10 @@ import {
461
418
  getAllNetworkMethodChain,
462
419
  hasHardwareRuntimeIdPrefix,
463
420
  isAllNetworkMethodName,
464
- isHardwareOperationId,
421
+ isConnectionLost,
422
+ isHardwareOperationId as isHardwareOperationId2,
465
423
  isHwkRecoveryHint,
424
+ isUserRefusal,
466
425
  operationMayHaveCompletedParams,
467
426
  parseBip32MasterFingerprint as parseBip32MasterFingerprint2,
468
427
  rehydrateConnectorError,
@@ -581,21 +540,47 @@ function placeholderDeviceInfo() {
581
540
  };
582
541
  }
583
542
 
543
+ // src/utils/queueKey.ts
544
+ import { isHardwareOperationId } from "@onekeyfe/hwk-adapter-core";
545
+ var KEYSTONE_COLD_START_JOB_LABEL = "keystone-cold-start";
546
+ function keystoneQueueKey(connectId, deviceId) {
547
+ return isHardwareOperationId(connectId) ? connectId : deviceId || connectId || KEYSTONE_COLD_START_JOB_LABEL;
548
+ }
549
+ function keystoneWalletIdFromIdentifier(identifier) {
550
+ const value = identifier.startsWith(KEYSTONE_WALLET_CONNECT_ID_PREFIX) ? identifier.slice(KEYSTONE_WALLET_CONNECT_ID_PREFIX.length) : identifier;
551
+ return /^[0-9a-f]{64}$/i.test(value) ? value.toLowerCase() : void 0;
552
+ }
553
+ function keystoneCancelQueueKeys(identifiers) {
554
+ const keys = /* @__PURE__ */ new Set();
555
+ for (const identifier of identifiers) {
556
+ if (identifier) {
557
+ keys.add(identifier);
558
+ const walletId = keystoneWalletIdFromIdentifier(identifier);
559
+ if (walletId) keys.add(walletId);
560
+ }
561
+ }
562
+ return keys;
563
+ }
564
+
584
565
  // src/adapter/KeystoneAdapter.ts
585
- var COLD_START_JOB_LABEL = "keystone-cold-start";
586
566
  var KEYSTONE_USB_REATTACH_PROBE_ATTEMPTS = 4;
587
567
  var KEYSTONE_USB_REATTACH_PROBE_INTERVAL_MS = 500;
588
568
  function resolveHardwareOperationTarget(positionalTargetId, operationId) {
589
- const result = resolveGenericHardwareOperationTarget(positionalTargetId, operationId, "keystone");
590
- return result;
569
+ return resolveGenericHardwareOperationTarget(positionalTargetId, operationId, "keystone");
570
+ }
571
+ function keySchema(chain, path) {
572
+ return { path, curve: chain === "sol" ? "ed25519" : "secp256k1" };
591
573
  }
592
574
  function waitForKeystoneUsbReattachProbe() {
593
575
  return new Promise((resolve) => {
594
576
  setTimeout(resolve, KEYSTONE_USB_REATTACH_PROBE_INTERVAL_MS);
595
577
  });
596
578
  }
597
- function getKeystoneJobId(connectId, deviceId) {
598
- return isHardwareOperationId(connectId) ? connectId : deviceId || connectId || COLD_START_JOB_LABEL;
579
+ function buildCancelledFailure(signal) {
580
+ const reason = signal.reason;
581
+ const code = typeof reason?.code === "number" ? reason.code : HardwareErrorCode.UserAborted;
582
+ const message = typeof reason?.message === "string" ? reason.message : "";
583
+ return failure(code, message || "All-network get-address cancelled");
599
584
  }
600
585
  var BIP44_COIN_TYPE_TO_CHAIN = {
601
586
  60: "evm",
@@ -610,6 +595,9 @@ function inferHwkChainFromPath(path) {
610
595
  var KeystoneAdapter = class _KeystoneAdapter {
611
596
  constructor(options) {
612
597
  this.vendor = "keystone";
598
+ // No protocol-level cancel exists over Keystone USB, and QR has no transport
599
+ // to interrupt at all. Cancelling ends the wait, not the device's prompt.
600
+ this.cancelCapability = "stops-waiting";
613
601
  this.emitter = new TypedEventEmitter();
614
602
  this._operationRoutes = /* @__PURE__ */ new Map();
615
603
  this._operations = new OperationRegistry({
@@ -635,13 +623,13 @@ var KeystoneAdapter = class _KeystoneAdapter {
635
623
  this._usbConnectTail = Promise.resolve();
636
624
  this._unsettledUsbOperations = /* @__PURE__ */ new Map();
637
625
  this._usbIdleWaiters = /* @__PURE__ */ new Set();
626
+ this._abandonedUsbConnects = 0;
638
627
  this._usbTeardownTail = Promise.resolve();
639
628
  this._pendingUsbTeardowns = 0;
640
629
  this._handleUsbDisconnect = ({ connectId }) => {
641
630
  const record = Array.from(this._devices.values()).find((item) => item.usbSessionId === connectId);
642
631
  if (!record) return;
643
632
  this._operations.endByConnectionKey(connectId, "disconnect");
644
- this._operations.endByConnectionKey(record.connectId, "disconnect");
645
633
  record.usbSessionId = void 0;
646
634
  const info = toDeviceInfo(record);
647
635
  if (record.qrSynced) {
@@ -655,27 +643,35 @@ var KeystoneAdapter = class _KeystoneAdapter {
655
643
  this.emitter.emit("ui-event", event);
656
644
  };
657
645
  // ---------------------------------------------------------------------------
658
- // All-network bundle — dispatches to the same per-chain methods below, so
659
- // there's exactly one implementation of each chain's address logic.
646
+ // All-network bundle, dispatched to the per-chain methods below.
660
647
  // ---------------------------------------------------------------------------
661
648
  this.allNetworkGetAddress = async (connectId, deviceId, params) => {
662
649
  const operationTarget = resolveHardwareOperationTarget(connectId, params.operationId);
663
650
  if (!operationTarget.success) return operationTarget;
664
651
  const effectiveConnectId = operationTarget.payload.targetId ?? "";
665
652
  const { operationId } = operationTarget.payload;
653
+ let cancelScope;
666
654
  try {
667
- const prefetched = await this._prefetchAllNetworkAccounts(
668
- effectiveConnectId,
669
- deviceId,
670
- params
671
- );
655
+ const prefetched = await this._prefetchAllNetworkAccounts(effectiveConnectId, deviceId, {
656
+ ...params,
657
+ operationId
658
+ });
672
659
  const { book } = prefetched;
673
660
  const itemDeviceId = deviceId || prefetched.walletId || "";
661
+ const scope = this._jobQueue.createCancelScope(
662
+ keystoneQueueKey(effectiveConnectId, itemDeviceId)
663
+ );
664
+ cancelScope = scope;
665
+ const cancelledIndexes = /* @__PURE__ */ new Set();
674
666
  return await runAllNetworkGetAddress({
675
667
  connectId: effectiveConnectId,
676
668
  deviceId: itemDeviceId,
677
669
  params,
678
- callItem: async ({ method, item }) => {
670
+ callItem: async ({ method, item, index }) => {
671
+ if (scope.signal.aborted) {
672
+ cancelledIndexes.add(index);
673
+ return buildCancelledFailure(scope.signal);
674
+ }
679
675
  const commonArgs = {
680
676
  path: item.path,
681
677
  showOnDevice: item.showOnDevice,
@@ -714,10 +710,18 @@ var KeystoneAdapter = class _KeystoneAdapter {
714
710
  } : void 0
715
711
  }
716
712
  });
713
+ },
714
+ // Cancel, refusal or a lost session ends the batch; other per-item
715
+ // failures carry on with the next chain.
716
+ shouldAbortBundle: (response, { index }) => {
717
+ const { code } = response.payload ?? {};
718
+ return cancelledIndexes.has(index) || isUserRefusal(code) || isConnectionLost(code);
717
719
  }
718
720
  });
719
721
  } catch (err) {
720
722
  return this._errorToFailure(err);
723
+ } finally {
724
+ cancelScope?.release();
721
725
  }
722
726
  };
723
727
  this._origin = options?.origin ?? "OneKey";
@@ -739,7 +743,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
739
743
  getAvailableTransports() {
740
744
  return this._usbConnector ? ["qr", "usb"] : ["qr"];
741
745
  }
742
- /** 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. */
746
+ /** Pins calls to 'qr' or 'usb'; any other value clears the pin back to auto routing. */
743
747
  switchTransport(type) {
744
748
  this._forcedTransport = type === "qr" || type === "usb" ? type : void 0;
745
749
  return Promise.resolve();
@@ -752,6 +756,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
752
756
  this._uiRegistry.cancel();
753
757
  this._jobQueue.clear();
754
758
  this._searchDeviceTargets.clear();
759
+ await this._usbConnectTail;
755
760
  await this._resetUsbSessions();
756
761
  this._usbConnector?.off("device-disconnect", this._handleUsbDisconnect);
757
762
  this._usbConnector?.off("ui-event", this._handleUsbUiEvent);
@@ -763,14 +768,8 @@ var KeystoneAdapter = class _KeystoneAdapter {
763
768
  // Device table
764
769
  // ---------------------------------------------------------------------------
765
770
  /**
766
- * Unscoped discovery includes QR-synced wallets already known to this
767
- * adapter. Explicit QR discovery returns a virtual connection target because
768
- * there is no physical descriptor to enumerate. When a USB connector is
769
- * configured, its raw scan results are appended as-is: a USB descriptor has
770
- * no mfp until `connectDevice()` actually opens+claims it (see
771
- * `KeystoneUsbConnectorBase.searchDevices`), so these entries carry an
772
- * empty `deviceId` and exist purely so a host can list "plugged in, click
773
- * to connect" candidates.
771
+ * Returns known wallets plus USB scan results; USB entries have no mfp until `connectDevice()`
772
+ * claims them. Explicit QR discovery returns one virtual target.
774
773
  */
775
774
  async searchDevices(options) {
776
775
  const searchGeneration = /* @__PURE__ */ Symbol("keystone-device-search");
@@ -806,18 +805,15 @@ var KeystoneAdapter = class _KeystoneAdapter {
806
805
  if (this._activeSearchGeneration !== searchGeneration) return [];
807
806
  const placeholders = usbDevices.map((d) => ({
808
807
  vendor: "keystone",
809
- // Empty, not 'unknown': enumeration genuinely cannot tell the model
810
- // apart (every Keystone shares one vid/pid), and a literal placeholder
811
- // string would beat the host's own default-name fallback and surface as
812
- // a device called "unknown".
808
+ // Empty, not 'unknown': all models share one vid/pid, and a placeholder
809
+ // string would beat the host's default-name fallback.
813
810
  model: d.model ?? "",
814
811
  modelName: d.modelName,
815
- // Placeholders — neither is knowable until the device is opened.
812
+ // Unknown until the device is opened.
816
813
  firmwareVersion: "0.0.0",
817
814
  deviceId: d.deviceId,
818
815
  connectId: d.connectId,
819
816
  connectionType: "usb",
820
- // The USB product name, which IS readable at enumeration time.
821
817
  label: d.name,
822
818
  serialNumber: d.serialNumber,
823
819
  capabilities: d.capabilities ?? { persistentDeviceIdentity: false },
@@ -837,20 +833,25 @@ var KeystoneAdapter = class _KeystoneAdapter {
837
833
  Array.from(this._devices.values()).map((record) => record.usbSessionId).filter((sessionId) => Boolean(sessionId))
838
834
  );
839
835
  for (const sessionId of sessionIds) {
840
- try {
841
- await this._usbConnector.disconnect(sessionId);
842
- } catch {
843
- }
844
- this._handleUsbDisconnect({ connectId: sessionId });
836
+ await this._retireUsbSession(sessionId);
845
837
  }
846
838
  });
847
839
  }
840
+ /** Best-effort disconnect; the session is retired either way so it cannot be selected again. */
841
+ async _retireUsbSession(sessionId) {
842
+ if (this._usbConnector) {
843
+ try {
844
+ await this._usbConnector.disconnect(sessionId);
845
+ } catch {
846
+ }
847
+ }
848
+ this._handleUsbDisconnect({ connectId: sessionId });
849
+ }
848
850
  async searchDeviceTargets(options) {
849
851
  const devices = await this.searchDevices(options);
850
852
  return devices.map((device) => ({
851
853
  searchTargetId: device.connectId,
852
- // Keystone's wallet id is stable only after the USB/QR handshake. The
853
- // selectable handle remains owned by the current discovery snapshot.
854
+ // The wallet id is stable only after the USB/QR handshake.
854
855
  searchTargetReusePolicy: "current-discovery",
855
856
  vendor: device.vendor,
856
857
  connectionType: device.connectionType,
@@ -869,13 +870,23 @@ var KeystoneAdapter = class _KeystoneAdapter {
869
870
  }));
870
871
  }
871
872
  async connectDevice(searchTargetId) {
873
+ const scope = this._jobQueue.createCancelScope(searchTargetId);
872
874
  try {
873
875
  let connected;
874
876
  let selectedConnectionType;
875
877
  const discoveredConnectionType = this._searchDeviceTargets.get(searchTargetId);
876
878
  if (discoveredConnectionType === "qr") {
877
879
  selectedConnectionType = "qr";
878
- connected = await this.importFromQr();
880
+ const abortColdStart = () => this._jobQueue.cancelActiveAndPending(
881
+ KEYSTONE_COLD_START_JOB_LABEL,
882
+ scope.signal.reason
883
+ );
884
+ scope.signal.addEventListener("abort", abortColdStart, { once: true });
885
+ try {
886
+ connected = await this.importFromQr();
887
+ } finally {
888
+ scope.signal.removeEventListener("abort", abortColdStart);
889
+ }
879
890
  } else if (discoveredConnectionType === "usb") {
880
891
  selectedConnectionType = "usb";
881
892
  if (!this._usbConnector) {
@@ -884,7 +895,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
884
895
  "No USB connector configured for this Keystone adapter"
885
896
  );
886
897
  }
887
- connected = await this._connectUsb({}, searchTargetId);
898
+ connected = await this._connectUsb({}, searchTargetId, scope.signal);
888
899
  } else if (hasHardwareRuntimeIdPrefix(searchTargetId)) {
889
900
  return failure(
890
901
  HardwareErrorCode.DeviceNotFound,
@@ -905,9 +916,10 @@ var KeystoneAdapter = class _KeystoneAdapter {
905
916
  );
906
917
  } else {
907
918
  selectedConnectionType = "usb";
908
- connected = await this._connectUsb(target);
919
+ connected = await this._connectUsb(target, void 0, scope.signal);
909
920
  }
910
921
  }
922
+ _KeystoneAdapter._throwIfAborted(scope.signal);
911
923
  if (!connected.success) return connected;
912
924
  const record = this._devices.get(connected.payload.deviceId);
913
925
  this._operations.endByConnectionKey(connected.payload.connectId, "explicit");
@@ -915,6 +927,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
915
927
  searchTargetId,
916
928
  connectId: connected.payload.connectId,
917
929
  device: connected.payload,
930
+ connectionType: selectedConnectionType,
918
931
  connectionKeys: selectedConnectionType === "usb" && record?.usbSessionId ? [record.usbSessionId] : []
919
932
  });
920
933
  this._operationRoutes.set(connected.payload.connectId, {
@@ -924,14 +937,13 @@ var KeystoneAdapter = class _KeystoneAdapter {
924
937
  return success(operation.operationId);
925
938
  } catch (err) {
926
939
  return this._errorToFailure(err);
940
+ } finally {
941
+ scope.release();
927
942
  }
928
943
  }
929
944
  /**
930
- * QR has no persistent connection to tear down — the account cache
931
- * survives so a later call resumes without re-syncing. For a USB session,
932
- * this closes the connector session and either demotes the record back to
933
- * QR-only (if it was ever QR-synced) or removes it entirely (pure-USB
934
- * wallet that was never seen over QR) — see §4.2 of the design doc.
945
+ * QR has nothing to tear down. A USB session is closed, and the record is demoted to QR-only or
946
+ * removed if it was never QR-synced.
935
947
  */
936
948
  async releaseOperation(operationId) {
937
949
  const operation = this._operations.find(operationId);
@@ -955,19 +967,11 @@ var KeystoneAdapter = class _KeystoneAdapter {
955
967
  return;
956
968
  }
957
969
  const { usbSessionId } = record;
958
- await this._runUsbTeardown(async () => {
959
- if (this._usbConnector) {
960
- try {
961
- await this._usbConnector.disconnect(usbSessionId);
962
- } catch {
963
- }
964
- }
965
- this._handleUsbDisconnect({ connectId: usbSessionId });
966
- });
970
+ await this._runUsbTeardown(() => this._retireUsbSession(usbSessionId));
967
971
  }
968
972
  getDeviceInfo(connectIdOrOperationId, deviceId) {
969
973
  try {
970
- const connectId = isHardwareOperationId(connectIdOrOperationId) ? this._operations.resolve(connectIdOrOperationId).connectId : connectIdOrOperationId;
974
+ const connectId = isHardwareOperationId2(connectIdOrOperationId) ? this._operations.resolve(connectIdOrOperationId).connectId : connectIdOrOperationId;
971
975
  const { record } = this._resolveTarget(connectId, deviceId);
972
976
  if (!record) {
973
977
  return Promise.resolve(
@@ -983,25 +987,35 @@ var KeystoneAdapter = class _KeystoneAdapter {
983
987
  }
984
988
  }
985
989
  cancel(connectId) {
990
+ let namedOperationConnectId;
991
+ if (isHardwareOperationId2(connectId)) {
992
+ try {
993
+ namedOperationConnectId = this._operations.resolve(connectId).connectId;
994
+ } catch {
995
+ return;
996
+ }
997
+ }
986
998
  const reason = createHwkError({
987
999
  code: HardwareErrorCode.UserAborted,
988
1000
  message: "User aborted operation"
989
1001
  });
990
- const activeJobId = this._jobQueue.getActiveJob()?.deviceId;
991
1002
  if (connectId) {
992
- if (!activeJobId || activeJobId !== connectId && activeJobId !== this._walletIdFromIdentifier(connectId)) {
993
- this._jobQueue.cancelActiveAndPending(connectId, reason);
994
- const walletId = this._walletIdFromIdentifier(connectId);
995
- if (walletId && walletId !== connectId) {
996
- this._jobQueue.cancelActiveAndPending(walletId, reason);
997
- }
998
- return;
1003
+ const queueKeys = keystoneCancelQueueKeys([
1004
+ connectId,
1005
+ this._operationRoutes.get(connectId)?.operationId,
1006
+ namedOperationConnectId
1007
+ ]);
1008
+ for (const key of queueKeys) {
1009
+ this._jobQueue.cancelActiveAndPending(key, reason);
1010
+ }
1011
+ const scopedOperationId = isHardwareOperationId2(connectId) ? connectId : this._operationRouteForIdentifier(connectId)?.operationId;
1012
+ if (scopedOperationId) {
1013
+ this._uiRegistry.cancel(void 0, void 0, scopedOperationId);
999
1014
  }
1000
- this._jobQueue.cancelActiveAndPending(activeJobId, reason);
1001
1015
  } else {
1002
1016
  this._jobQueue.cancelActiveAndPending(void 0, reason);
1017
+ this._uiRegistry.cancel();
1003
1018
  }
1004
- this._uiRegistry.cancel();
1005
1019
  }
1006
1020
  getChainFingerprint(connectId, deviceId, chain) {
1007
1021
  try {
@@ -1031,25 +1045,25 @@ var KeystoneAdapter = class _KeystoneAdapter {
1031
1045
  this.emitter.off(event, listener);
1032
1046
  }
1033
1047
  // ---------------------------------------------------------------------------
1034
- // Explicit account import — the recommended entry point before signing, so
1035
- // signing calls don't each pay for their own cold-sync round trip.
1048
+ // Explicit account import: call before signing to avoid a cold sync per call.
1036
1049
  // ---------------------------------------------------------------------------
1037
1050
  async importFromQr(options = {}) {
1038
1051
  try {
1039
- return await this._jobQueue.enqueue(COLD_START_JOB_LABEL, async (signal) => {
1052
+ return await this._jobQueue.enqueue(KEYSTONE_COLD_START_JOB_LABEL, async (signal) => {
1040
1053
  const displayDevice = placeholderDeviceInfo();
1041
1054
  let responseUr;
1042
1055
  if (options.mode === "scan") {
1043
- responseUr = await this._requestQrScanAndAwaitResponse(displayDevice);
1056
+ responseUr = await this._requestQrScanAndAwaitResponse(displayDevice, signal);
1044
1057
  } else {
1045
1058
  const requestUr = this.urEngine.buildKeyDerivationRequest({
1046
- schemas: [{ path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" }],
1059
+ schemas: [keySchema("evm", KEYSTONE_WALLET_ID_PATH)],
1047
1060
  origin: this._origin
1048
1061
  });
1049
- responseUr = await this._requestQrDisplayAndAwaitResponse(displayDevice, {
1050
- ...requestUr,
1051
- animated: false
1052
- });
1062
+ responseUr = await this._requestQrDisplayAndAwaitResponse(
1063
+ displayDevice,
1064
+ { ...requestUr, animated: false },
1065
+ signal
1066
+ );
1053
1067
  }
1054
1068
  _KeystoneAdapter._throwIfAborted(signal);
1055
1069
  const parsed = this.urEngine.parseAccountResponse(responseUr);
@@ -1063,12 +1077,11 @@ var KeystoneAdapter = class _KeystoneAdapter {
1063
1077
  // ---------------------------------------------------------------------------
1064
1078
  // EVM
1065
1079
  // ---------------------------------------------------------------------------
1066
- async evmGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1067
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1080
+ async evmGetAddress(connectIdArg, deviceIdArg, params, book) {
1081
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1068
1082
  if (!operationTarget.success) return operationTarget;
1069
1083
  const connectId = operationTarget.payload.targetId;
1070
1084
  const deviceId = deviceIdArg ?? void 0;
1071
- const params = paramsArg;
1072
1085
  if (!params) return failure(HardwareErrorCode.InvalidParams, "evmGetAddress requires params");
1073
1086
  if (!params.path)
1074
1087
  return failure(HardwareErrorCode.InvalidParams, "evmGetAddress requires params.path");
@@ -1079,39 +1092,24 @@ var KeystoneAdapter = class _KeystoneAdapter {
1079
1092
  "evmGetAddress requires a full leaf path, e.g. m/44'/60'/0'/0/0"
1080
1093
  );
1081
1094
  }
1082
- try {
1083
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1084
- const { account } = await this._fetchAccount(
1085
- connectId,
1086
- deviceId,
1087
- "evm",
1088
- accountPath,
1089
- signal,
1090
- { book }
1091
- );
1092
- _KeystoneAdapter._throwIfAborted(signal);
1093
- if (!account.extendedPublicKey) {
1094
- throw createHwkError({
1095
- code: HardwareErrorCode.MethodNotSupported,
1096
- message: "Keystone did not return an extended public key for this account path"
1097
- });
1098
- }
1099
- const address = this.urEngine.deriveEvmAddressFromXpub(
1100
- account.extendedPublicKey,
1101
- relativeDerivePath
1102
- );
1103
- return success({ address, path: normalizePath(params.path) });
1104
- });
1105
- } catch (err) {
1106
- return this._errorToFailure(err);
1107
- }
1095
+ return this._runJob(connectId, deviceId, async (signal) => {
1096
+ const xpub = await this._fetchAccountXpub(
1097
+ connectId,
1098
+ deviceId,
1099
+ "evm",
1100
+ accountPath,
1101
+ signal,
1102
+ book
1103
+ );
1104
+ const address = this.urEngine.deriveEvmAddressFromXpub(xpub, relativeDerivePath);
1105
+ return success({ address, path: normalizePath(params.path) });
1106
+ });
1108
1107
  }
1109
- async evmSignTransaction(connectIdArg, deviceIdArg, paramsArg) {
1110
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1108
+ async evmSignTransaction(connectIdArg, deviceIdArg, params) {
1109
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1111
1110
  if (!operationTarget.success) return operationTarget;
1112
1111
  const connectId = operationTarget.payload.targetId;
1113
1112
  const deviceId = deviceIdArg ?? void 0;
1114
- const params = paramsArg;
1115
1113
  if (!params)
1116
1114
  return failure(HardwareErrorCode.InvalidParams, "evmSignTransaction requires params");
1117
1115
  if (!params.path) {
@@ -1125,47 +1123,34 @@ var KeystoneAdapter = class _KeystoneAdapter {
1125
1123
  }
1126
1124
  const rawTxHex = stripHex(params.serializedTx);
1127
1125
  const path = normalizePath(params.path);
1128
- try {
1129
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1130
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "evm", signal);
1131
- _KeystoneAdapter._throwIfAborted(signal);
1132
- const requestId = uuidv4();
1133
- const dataType = parseInt(rawTxHex.slice(0, 2), 16) < 192 ? "typedTransaction" : "transaction";
1134
- const requestUr = this.urEngine.buildEthSignRequest({
1135
- requestId,
1136
- unsignedTxHex: rawTxHex,
1137
- dataType,
1138
- path,
1139
- xfp: record.masterFingerprint,
1140
- chainId: params.chainId
1141
- });
1142
- const responseUr = await this._resolveUr(
1143
- record,
1144
- requestUr,
1145
- true,
1146
- connectId,
1147
- signal,
1148
- "evmSignTransaction"
1149
- );
1150
- _KeystoneAdapter._throwIfAborted(signal);
1151
- const sig = this.urEngine.parseEthSignature(responseUr);
1152
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1153
- return success({
1154
- v: ensure0x(sig.v),
1155
- r: ensure0x(sig.r),
1156
- s: ensure0x(sig.s)
1157
- });
1158
- });
1159
- } catch (err) {
1160
- return this._errorToFailure(err);
1161
- }
1126
+ const dataType = parseInt(rawTxHex.slice(0, 2), 16) < 192 ? "typedTransaction" : "transaction";
1127
+ return this._signWithWallet({
1128
+ connectId,
1129
+ deviceId,
1130
+ chain: "evm",
1131
+ operationName: "evmSignTransaction",
1132
+ buildRequest: (requestId, xfp) => this.urEngine.buildEthSignRequest({
1133
+ requestId,
1134
+ unsignedTxHex: rawTxHex,
1135
+ dataType,
1136
+ path,
1137
+ xfp,
1138
+ chainId: params.chainId,
1139
+ origin: this._origin
1140
+ }),
1141
+ parseResponse: (ur) => this.urEngine.parseEthSignature(ur),
1142
+ toPayload: (sig) => ({
1143
+ v: ensure0x(sig.v),
1144
+ r: ensure0x(sig.r),
1145
+ s: ensure0x(sig.s)
1146
+ })
1147
+ });
1162
1148
  }
1163
- async evmSignMessage(connectIdArg, deviceIdArg, paramsArg) {
1164
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1149
+ async evmSignMessage(connectIdArg, deviceIdArg, params) {
1150
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1165
1151
  if (!operationTarget.success) return operationTarget;
1166
1152
  const connectId = operationTarget.payload.targetId;
1167
1153
  const deviceId = deviceIdArg ?? void 0;
1168
- const params = paramsArg;
1169
1154
  if (!params) return failure(HardwareErrorCode.InvalidParams, "evmSignMessage requires params");
1170
1155
  if (!params.path || params.message === void 0) {
1171
1156
  return failure(
@@ -1175,42 +1160,29 @@ var KeystoneAdapter = class _KeystoneAdapter {
1175
1160
  }
1176
1161
  const messageHex = params.hex ? stripHex(params.message) : Buffer.from(params.message, "utf8").toString("hex");
1177
1162
  const path = normalizePath(params.path);
1178
- try {
1179
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1180
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "evm", signal);
1181
- _KeystoneAdapter._throwIfAborted(signal);
1182
- const requestId = uuidv4();
1183
- const requestUr = this.urEngine.buildEthSignRequest({
1184
- requestId,
1185
- unsignedTxHex: messageHex,
1186
- dataType: "personalMessage",
1187
- path,
1188
- xfp: record.masterFingerprint,
1189
- chainId: params.chainId
1190
- });
1191
- const responseUr = await this._resolveUr(
1192
- record,
1193
- requestUr,
1194
- true,
1195
- connectId,
1196
- signal,
1197
- "evmSignMessage"
1198
- );
1199
- _KeystoneAdapter._throwIfAborted(signal);
1200
- const sig = this.urEngine.parseEthSignature(responseUr);
1201
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1202
- return success({ signature: ensure0x(sig.r + sig.s + sig.v) });
1203
- });
1204
- } catch (err) {
1205
- return this._errorToFailure(err);
1206
- }
1163
+ return this._signWithWallet({
1164
+ connectId,
1165
+ deviceId,
1166
+ chain: "evm",
1167
+ operationName: "evmSignMessage",
1168
+ buildRequest: (requestId, xfp) => this.urEngine.buildEthSignRequest({
1169
+ requestId,
1170
+ unsignedTxHex: messageHex,
1171
+ dataType: "personalMessage",
1172
+ path,
1173
+ xfp,
1174
+ chainId: params.chainId,
1175
+ origin: this._origin
1176
+ }),
1177
+ parseResponse: (ur) => this.urEngine.parseEthSignature(ur),
1178
+ toPayload: (sig) => ({ signature: ensure0x(sig.r + sig.s + sig.v) })
1179
+ });
1207
1180
  }
1208
- async evmSignTypedData(connectIdArg, deviceIdArg, paramsArg) {
1209
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1181
+ async evmSignTypedData(connectIdArg, deviceIdArg, params) {
1182
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1210
1183
  if (!operationTarget.success) return operationTarget;
1211
1184
  const connectId = operationTarget.payload.targetId;
1212
1185
  const deviceId = deviceIdArg ?? void 0;
1213
- const params = paramsArg;
1214
1186
  if (!params)
1215
1187
  return failure(HardwareErrorCode.InvalidParams, "evmSignTypedData requires params");
1216
1188
  if (!params.path) {
@@ -1227,48 +1199,33 @@ var KeystoneAdapter = class _KeystoneAdapter {
1227
1199
  "utf8"
1228
1200
  ).toString("hex");
1229
1201
  const path = normalizePath(params.path);
1230
- try {
1231
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1232
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "evm", signal);
1233
- _KeystoneAdapter._throwIfAborted(signal);
1234
- const requestId = uuidv4();
1235
- const requestUr = this.urEngine.buildEthSignRequest({
1236
- requestId,
1237
- unsignedTxHex: signDataHex,
1238
- dataType: "typedData",
1239
- path,
1240
- xfp: record.masterFingerprint,
1241
- chainId: params.chainId
1242
- });
1243
- const responseUr = await this._resolveUr(
1244
- record,
1245
- requestUr,
1246
- true,
1247
- connectId,
1248
- signal,
1249
- "evmSignTypedData"
1250
- );
1251
- _KeystoneAdapter._throwIfAborted(signal);
1252
- const sig = this.urEngine.parseEthSignature(responseUr);
1253
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1254
- return success({ signature: ensure0x(sig.r + sig.s + sig.v) });
1255
- });
1256
- } catch (err) {
1257
- return this._errorToFailure(err);
1258
- }
1202
+ return this._signWithWallet({
1203
+ connectId,
1204
+ deviceId,
1205
+ chain: "evm",
1206
+ operationName: "evmSignTypedData",
1207
+ buildRequest: (requestId, xfp) => this.urEngine.buildEthSignRequest({
1208
+ requestId,
1209
+ unsignedTxHex: signDataHex,
1210
+ dataType: "typedData",
1211
+ path,
1212
+ xfp,
1213
+ chainId: params.chainId,
1214
+ origin: this._origin
1215
+ }),
1216
+ parseResponse: (ur) => this.urEngine.parseEthSignature(ur),
1217
+ toPayload: (sig) => ({ signature: ensure0x(sig.r + sig.s + sig.v) })
1218
+ });
1259
1219
  }
1260
1220
  // ---------------------------------------------------------------------------
1261
- // BTC — PSBT signing and message signing only for now. Address/pubkey
1262
- // derivation needs script-type-aware xpub decoding (P2WPKH/P2TR/…) this
1263
- // phase doesn't wire in yet; structured-field tx signing needs host-side
1264
- // PSBT construction. Both are real, bounded follow-ups, not silent gaps.
1221
+ // BTC: 44'/49'/84' addresses derive offline from the account xpub; P2TR addresses
1222
+ // and structured-field tx signing are unsupported (the host builds a PSBT instead).
1265
1223
  // ---------------------------------------------------------------------------
1266
- async btcGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1267
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1224
+ async btcGetAddress(connectIdArg, deviceIdArg, params, book) {
1225
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1268
1226
  if (!operationTarget.success) return operationTarget;
1269
1227
  const connectId = operationTarget.payload.targetId;
1270
1228
  const deviceId = deviceIdArg ?? void 0;
1271
- const params = paramsArg;
1272
1229
  if (!params) return failure(HardwareErrorCode.InvalidParams, "btcGetAddress requires params");
1273
1230
  if (!params.path)
1274
1231
  return failure(HardwareErrorCode.InvalidParams, "btcGetAddress requires params.path");
@@ -1296,49 +1253,28 @@ var KeystoneAdapter = class _KeystoneAdapter {
1296
1253
  if (!isKeystoneSignableBtcAccountPath(accountPath)) {
1297
1254
  return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1298
1255
  }
1299
- try {
1300
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1301
- const { account } = await this._fetchAccount(
1302
- connectId,
1303
- deviceId,
1304
- "btc",
1305
- accountPath,
1306
- signal,
1307
- { book }
1308
- );
1309
- _KeystoneAdapter._throwIfAborted(signal);
1310
- if (!account.extendedPublicKey) {
1311
- throw createHwkError({
1312
- code: HardwareErrorCode.MethodNotSupported,
1313
- message: "Keystone did not return an extended public key for this account path"
1314
- });
1315
- }
1316
- const address = this.urEngine.deriveBtcAddressFromXpub(
1317
- account.extendedPublicKey,
1318
- relativeDerivePath,
1319
- scriptType
1320
- );
1321
- return success({ address, path: normalizedPath });
1322
- });
1323
- } catch (err) {
1324
- return this._errorToFailure(err);
1325
- }
1256
+ return this._runJob(connectId, deviceId, async (signal) => {
1257
+ const xpub = await this._fetchAccountXpub(
1258
+ connectId,
1259
+ deviceId,
1260
+ "btc",
1261
+ accountPath,
1262
+ signal,
1263
+ book
1264
+ );
1265
+ const address = this.urEngine.deriveBtcAddressFromXpub(xpub, relativeDerivePath, scriptType);
1266
+ return success({ address, path: normalizedPath });
1267
+ });
1326
1268
  }
1327
1269
  /**
1328
- * Returns the account-level extended public key. `params.path` must be an
1329
- * ACCOUNT path (`m/84'/0'/0'`), not a leaf — that is the level Keystone
1330
- * actually syncs, and it is what a host needs to derive a whole account's
1331
- * addresses offline. Unlike `btcGetAddress` this has no script-type
1332
- * restriction: an xpub is script-type agnostic, so `86'` (taproot) works
1333
- * here even though deriving a taproot ADDRESS still needs an EC library
1334
- * this package doesn't wire in.
1270
+ * `params.path` must be an account path (`m/84'/0'/0'`), the level Keystone syncs. An xpub is
1271
+ * script-type agnostic, so `86'` works here even though `btcGetAddress` rejects it.
1335
1272
  */
1336
- async btcGetPublicKey(connectIdArg, deviceIdArg, paramsArg, book) {
1337
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1273
+ async btcGetPublicKey(connectIdArg, deviceIdArg, params, book) {
1274
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1338
1275
  if (!operationTarget.success) return operationTarget;
1339
1276
  const connectId = operationTarget.payload.targetId;
1340
1277
  const deviceId = deviceIdArg ?? void 0;
1341
- const params = paramsArg;
1342
1278
  if (!params) return failure(HardwareErrorCode.InvalidParams, "btcGetPublicKey requires params");
1343
1279
  if (!params.path)
1344
1280
  return failure(HardwareErrorCode.InvalidParams, "btcGetPublicKey requires params.path");
@@ -1352,36 +1288,25 @@ var KeystoneAdapter = class _KeystoneAdapter {
1352
1288
  if (!isKeystoneSignableBtcAccountPath(accountPath)) {
1353
1289
  return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1354
1290
  }
1355
- try {
1356
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1357
- const { account } = await this._fetchAccount(
1358
- connectId,
1359
- deviceId,
1360
- "btc",
1361
- accountPath,
1362
- signal,
1363
- { book }
1364
- );
1365
- _KeystoneAdapter._throwIfAborted(signal);
1366
- if (!account.extendedPublicKey) {
1367
- throw createHwkError({
1368
- code: HardwareErrorCode.MethodNotSupported,
1369
- message: "Keystone did not return an extended public key for this account path"
1370
- });
1371
- }
1372
- const meta = this.urEngine.parseXpubMeta(account.extendedPublicKey);
1373
- return success({
1374
- xpub: account.extendedPublicKey,
1375
- publicKey: meta.publicKey,
1376
- chainCode: meta.chainCode,
1377
- depth: meta.depth,
1378
- fingerprint: meta.parentFingerprint,
1379
- path: normalizePath(accountPath)
1380
- });
1291
+ return this._runJob(connectId, deviceId, async (signal) => {
1292
+ const xpub = await this._fetchAccountXpub(
1293
+ connectId,
1294
+ deviceId,
1295
+ "btc",
1296
+ accountPath,
1297
+ signal,
1298
+ book
1299
+ );
1300
+ const meta = this.urEngine.parseXpubMeta(xpub);
1301
+ return success({
1302
+ xpub,
1303
+ publicKey: meta.publicKey,
1304
+ chainCode: meta.chainCode,
1305
+ depth: meta.depth,
1306
+ fingerprint: meta.parentFingerprint,
1307
+ path: normalizePath(accountPath)
1381
1308
  });
1382
- } catch (err) {
1383
- return this._errorToFailure(err);
1384
- }
1309
+ });
1385
1310
  }
1386
1311
  async btcSignTransaction(_connectId, _deviceId, _params) {
1387
1312
  return this._unsupported(
@@ -1389,12 +1314,11 @@ var KeystoneAdapter = class _KeystoneAdapter {
1389
1314
  "use btcSignPsbt \u2014 structured input/output signing needs host-side PSBT construction, not yet wired in"
1390
1315
  );
1391
1316
  }
1392
- async btcSignPsbt(connectIdArg, deviceIdArg, paramsArg) {
1393
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1317
+ async btcSignPsbt(connectIdArg, deviceIdArg, params) {
1318
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1394
1319
  if (!operationTarget.success) return operationTarget;
1395
1320
  const connectId = operationTarget.payload.targetId;
1396
1321
  const deviceId = deviceIdArg ?? void 0;
1397
- const params = paramsArg;
1398
1322
  if (!params) return failure(HardwareErrorCode.InvalidParams, "btcSignPsbt requires params");
1399
1323
  if (!params.psbt)
1400
1324
  return failure(HardwareErrorCode.InvalidParams, "btcSignPsbt requires params.psbt");
@@ -1403,33 +1327,28 @@ var KeystoneAdapter = class _KeystoneAdapter {
1403
1327
  if (!isKeystoneSignableBtcAccountPath(splitAccountPath(params.path).accountPath)) {
1404
1328
  return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1405
1329
  }
1406
- try {
1407
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1408
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1409
- _KeystoneAdapter._throwIfAborted(signal);
1410
- const requestUr = this.urEngine.buildBtcPsbtRequest(stripHex(params.psbt));
1411
- const responseUr = await this._resolveUr(
1412
- record,
1413
- requestUr,
1414
- true,
1415
- connectId,
1416
- signal,
1417
- "btcSignPsbt"
1418
- );
1419
- _KeystoneAdapter._throwIfAborted(signal);
1420
- const signedPsbt = this.urEngine.parseBtcPsbt(responseUr);
1421
- return success({ signedPsbt });
1422
- });
1423
- } catch (err) {
1424
- return this._errorToFailure(err);
1425
- }
1330
+ return this._runJob(connectId, deviceId, async (signal) => {
1331
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1332
+ _KeystoneAdapter._throwIfAborted(signal);
1333
+ const requestUr = this.urEngine.buildBtcPsbtRequest(stripHex(params.psbt));
1334
+ const responseUr = await this._resolveUr(
1335
+ record,
1336
+ requestUr,
1337
+ true,
1338
+ connectId,
1339
+ signal,
1340
+ "btcSignPsbt"
1341
+ );
1342
+ _KeystoneAdapter._throwIfAborted(signal);
1343
+ const signedPsbt = this.urEngine.parseBtcPsbt(responseUr);
1344
+ return success({ signedPsbt });
1345
+ });
1426
1346
  }
1427
- async btcSignMessage(connectIdArg, deviceIdArg, paramsArg) {
1428
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1347
+ async btcSignMessage(connectIdArg, deviceIdArg, params) {
1348
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1429
1349
  if (!operationTarget.success) return operationTarget;
1430
1350
  const connectId = operationTarget.payload.targetId;
1431
1351
  const deviceId = deviceIdArg ?? void 0;
1432
- const params = paramsArg;
1433
1352
  if (!params) return failure(HardwareErrorCode.InvalidParams, "btcSignMessage requires params");
1434
1353
  if (!params.path || params.message === void 0) {
1435
1354
  return failure(
@@ -1442,79 +1361,55 @@ var KeystoneAdapter = class _KeystoneAdapter {
1442
1361
  if (!isKeystoneSignableBtcAccountPath(splitAccountPath(path).accountPath)) {
1443
1362
  return failure(HardwareErrorCode.DevicePathForbidden, KEYSTONE_BTC_ACCOUNT_FORBIDDEN_MESSAGE);
1444
1363
  }
1445
- try {
1446
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1447
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1448
- _KeystoneAdapter._throwIfAborted(signal);
1449
- const requestId = uuidv4();
1450
- const requestUr = this.urEngine.buildBtcMessageSignRequest({
1451
- requestId,
1452
- messageHex,
1453
- accounts: [{ path, xfp: record.masterFingerprint }]
1454
- });
1455
- const responseUr = await this._resolveUr(
1456
- record,
1457
- requestUr,
1458
- true,
1459
- connectId,
1460
- signal,
1461
- "btcSignMessage"
1462
- );
1463
- _KeystoneAdapter._throwIfAborted(signal);
1464
- const sig = this.urEngine.parseBtcSignature(responseUr);
1465
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1466
- return success({ signature: sig.signature });
1467
- });
1468
- } catch (err) {
1469
- return this._errorToFailure(err);
1470
- }
1364
+ return this._signWithWallet({
1365
+ connectId,
1366
+ deviceId,
1367
+ chain: "btc",
1368
+ operationName: "btcSignMessage",
1369
+ buildRequest: (requestId, xfp) => this.urEngine.buildBtcMessageSignRequest({
1370
+ requestId,
1371
+ messageHex,
1372
+ accounts: [{ path, xfp }],
1373
+ origin: this._origin
1374
+ }),
1375
+ parseResponse: (ur) => this.urEngine.parseBtcSignature(ur),
1376
+ toPayload: (sig) => ({ signature: sig.signature })
1377
+ });
1471
1378
  }
1472
1379
  async btcGetMasterFingerprint(connectIdArg, deviceIdArg, paramsArg) {
1473
1380
  const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1474
1381
  if (!operationTarget.success) return operationTarget;
1475
1382
  const connectId = operationTarget.payload.targetId;
1476
1383
  const deviceId = deviceIdArg ?? void 0;
1477
- try {
1478
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1479
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1480
- return success({ masterFingerprint: record.masterFingerprint });
1481
- });
1482
- } catch (err) {
1483
- return this._errorToFailure(err);
1484
- }
1384
+ return this._runJob(connectId, deviceId, async (signal) => {
1385
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, "btc", signal);
1386
+ return success({ masterFingerprint: record.masterFingerprint });
1387
+ });
1485
1388
  }
1486
1389
  // ---------------------------------------------------------------------------
1487
1390
  // SOL
1488
1391
  // ---------------------------------------------------------------------------
1489
- async solGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1490
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1392
+ async solGetAddress(connectIdArg, deviceIdArg, params, book) {
1393
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1491
1394
  if (!operationTarget.success) return operationTarget;
1492
1395
  const connectId = operationTarget.payload.targetId;
1493
1396
  const deviceId = deviceIdArg ?? void 0;
1494
- const params = paramsArg;
1495
1397
  if (!params) return failure(HardwareErrorCode.InvalidParams, "solGetAddress requires params");
1496
1398
  if (!params.path)
1497
1399
  return failure(HardwareErrorCode.InvalidParams, "solGetAddress requires params.path");
1498
1400
  const path = normalizePath(params.path);
1499
- try {
1500
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1501
- const { account } = await this._fetchAccount(connectId, deviceId, "sol", path, signal, {
1502
- book
1503
- });
1504
- _KeystoneAdapter._throwIfAborted(signal);
1505
- const address = bs58.encode(Buffer.from(account.publicKey, "hex"));
1506
- return success({ address, path });
1507
- });
1508
- } catch (err) {
1509
- return this._errorToFailure(err);
1510
- }
1401
+ return this._runJob(connectId, deviceId, async (signal) => {
1402
+ const { account } = await this._fetchAccount(connectId, deviceId, "sol", path, signal, book);
1403
+ _KeystoneAdapter._throwIfAborted(signal);
1404
+ const address = bs58.encode(Buffer.from(account.publicKey, "hex"));
1405
+ return success({ address, path });
1406
+ });
1511
1407
  }
1512
- async solSignTransaction(connectIdArg, deviceIdArg, paramsArg) {
1513
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1408
+ async solSignTransaction(connectIdArg, deviceIdArg, params) {
1409
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1514
1410
  if (!operationTarget.success) return operationTarget;
1515
1411
  const connectId = operationTarget.payload.targetId;
1516
1412
  const deviceId = deviceIdArg ?? void 0;
1517
- const params = paramsArg;
1518
1413
  if (!params)
1519
1414
  return failure(HardwareErrorCode.InvalidParams, "solSignTransaction requires params");
1520
1415
  if (!params.path || !params.serializedTx) {
@@ -1524,41 +1419,28 @@ var KeystoneAdapter = class _KeystoneAdapter {
1524
1419
  );
1525
1420
  }
1526
1421
  const path = normalizePath(params.path);
1527
- try {
1528
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1529
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "sol", signal);
1530
- _KeystoneAdapter._throwIfAborted(signal);
1531
- const requestId = uuidv4();
1532
- const requestUr = this.urEngine.buildSolSignRequest({
1533
- requestId,
1534
- unsignedPayloadHex: stripHex(params.serializedTx),
1535
- dataType: "transaction",
1536
- path,
1537
- xfp: record.masterFingerprint
1538
- });
1539
- const responseUr = await this._resolveUr(
1540
- record,
1541
- requestUr,
1542
- true,
1543
- connectId,
1544
- signal,
1545
- "solSignTransaction"
1546
- );
1547
- _KeystoneAdapter._throwIfAborted(signal);
1548
- const sig = this.urEngine.parseSolSignature(responseUr);
1549
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1550
- return success({ signature: sig.signature });
1551
- });
1552
- } catch (err) {
1553
- return this._errorToFailure(err);
1554
- }
1422
+ return this._signWithWallet({
1423
+ connectId,
1424
+ deviceId,
1425
+ chain: "sol",
1426
+ operationName: "solSignTransaction",
1427
+ buildRequest: (requestId, xfp) => this.urEngine.buildSolSignRequest({
1428
+ requestId,
1429
+ unsignedPayloadHex: stripHex(params.serializedTx),
1430
+ dataType: "transaction",
1431
+ path,
1432
+ xfp,
1433
+ origin: this._origin
1434
+ }),
1435
+ parseResponse: (ur) => this.urEngine.parseSolSignature(ur),
1436
+ toPayload: (sig) => ({ signature: sig.signature })
1437
+ });
1555
1438
  }
1556
- async solSignMessage(connectIdArg, deviceIdArg, paramsArg) {
1557
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1439
+ async solSignMessage(connectIdArg, deviceIdArg, params) {
1440
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1558
1441
  if (!operationTarget.success) return operationTarget;
1559
1442
  const connectId = operationTarget.payload.targetId;
1560
1443
  const deviceId = deviceIdArg ?? void 0;
1561
- const params = paramsArg;
1562
1444
  if (!params) return failure(HardwareErrorCode.InvalidParams, "solSignMessage requires params");
1563
1445
  if (!params.path || !params.message) {
1564
1446
  return failure(
@@ -1567,49 +1449,32 @@ var KeystoneAdapter = class _KeystoneAdapter {
1567
1449
  );
1568
1450
  }
1569
1451
  const path = normalizePath(params.path);
1570
- try {
1571
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1572
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "sol", signal);
1573
- _KeystoneAdapter._throwIfAborted(signal);
1574
- const requestId = uuidv4();
1575
- const requestUr = this.urEngine.buildSolSignRequest({
1576
- requestId,
1577
- unsignedPayloadHex: stripHex(params.message),
1578
- dataType: "message",
1579
- path,
1580
- xfp: record.masterFingerprint
1581
- });
1582
- const responseUr = await this._resolveUr(
1583
- record,
1584
- requestUr,
1585
- true,
1586
- connectId,
1587
- signal,
1588
- "solSignMessage"
1589
- );
1590
- _KeystoneAdapter._throwIfAborted(signal);
1591
- const sig = this.urEngine.parseSolSignature(responseUr);
1592
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1593
- return success({ signature: sig.signature });
1594
- });
1595
- } catch (err) {
1596
- return this._errorToFailure(err);
1597
- }
1452
+ return this._signWithWallet({
1453
+ connectId,
1454
+ deviceId,
1455
+ chain: "sol",
1456
+ operationName: "solSignMessage",
1457
+ buildRequest: (requestId, xfp) => this.urEngine.buildSolSignRequest({
1458
+ requestId,
1459
+ unsignedPayloadHex: stripHex(params.message),
1460
+ dataType: "message",
1461
+ path,
1462
+ xfp,
1463
+ origin: this._origin
1464
+ }),
1465
+ parseResponse: (ur) => this.urEngine.parseSolSignature(ur),
1466
+ toPayload: (sig) => ({ signature: sig.signature })
1467
+ });
1598
1468
  }
1599
1469
  // ---------------------------------------------------------------------------
1600
- // TRON — routed through `TronSignRequest`/`TronSignature` (see
1601
- // urEngine/TronSignRequest.ts), whose key layout follows keystone-sdk-rust
1602
- // (tags 5201/5202, firmware >= 2.5.0). The OneKey Pro air-gap encoding under
1603
- // the same tag is a different layout the firmware rejects. Not keystone-sdk's
1604
- // bundled `sdk.tron` module either (protobuf-based, unverified response
1605
- // semantics).
1470
+ // TRON: keystone-sdk-rust layout (tags 5201/5202, firmware >= 2.5.0), not sdk.tron's
1471
+ // protobuf module; OneKey Pro's encoding under the same tag is rejected by the firmware.
1606
1472
  // ---------------------------------------------------------------------------
1607
- async tronGetAddress(connectIdArg, deviceIdArg, paramsArg, book) {
1608
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1473
+ async tronGetAddress(connectIdArg, deviceIdArg, params, book) {
1474
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1609
1475
  if (!operationTarget.success) return operationTarget;
1610
1476
  const connectId = operationTarget.payload.targetId;
1611
1477
  const deviceId = deviceIdArg ?? void 0;
1612
- const params = paramsArg;
1613
1478
  if (!params) return failure(HardwareErrorCode.InvalidParams, "tronGetAddress requires params");
1614
1479
  if (!params.path)
1615
1480
  return failure(HardwareErrorCode.InvalidParams, "tronGetAddress requires params.path");
@@ -1620,39 +1485,24 @@ var KeystoneAdapter = class _KeystoneAdapter {
1620
1485
  "tronGetAddress requires a full leaf path, e.g. m/44'/195'/0'/0/0"
1621
1486
  );
1622
1487
  }
1623
- try {
1624
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1625
- const { account } = await this._fetchAccount(
1626
- connectId,
1627
- deviceId,
1628
- "tron",
1629
- accountPath,
1630
- signal,
1631
- { book }
1632
- );
1633
- _KeystoneAdapter._throwIfAborted(signal);
1634
- if (!account.extendedPublicKey) {
1635
- throw createHwkError({
1636
- code: HardwareErrorCode.MethodNotSupported,
1637
- message: "Keystone did not return an extended public key for this account path"
1638
- });
1639
- }
1640
- const address = this.urEngine.deriveTronAddressFromXpub(
1641
- account.extendedPublicKey,
1642
- relativeDerivePath
1643
- );
1644
- return success({ address, path: normalizePath(params.path) });
1645
- });
1646
- } catch (err) {
1647
- return this._errorToFailure(err);
1648
- }
1488
+ return this._runJob(connectId, deviceId, async (signal) => {
1489
+ const xpub = await this._fetchAccountXpub(
1490
+ connectId,
1491
+ deviceId,
1492
+ "tron",
1493
+ accountPath,
1494
+ signal,
1495
+ book
1496
+ );
1497
+ const address = this.urEngine.deriveTronAddressFromXpub(xpub, relativeDerivePath);
1498
+ return success({ address, path: normalizePath(params.path) });
1499
+ });
1649
1500
  }
1650
- async tronSignTransaction(connectIdArg, deviceIdArg, paramsArg) {
1651
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1501
+ async tronSignTransaction(connectIdArg, deviceIdArg, params) {
1502
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1652
1503
  if (!operationTarget.success) return operationTarget;
1653
1504
  const connectId = operationTarget.payload.targetId;
1654
1505
  const deviceId = deviceIdArg ?? void 0;
1655
- const params = paramsArg;
1656
1506
  if (!params)
1657
1507
  return failure(HardwareErrorCode.InvalidParams, "tronSignTransaction requires params");
1658
1508
  if (!params.path) {
@@ -1666,40 +1516,28 @@ var KeystoneAdapter = class _KeystoneAdapter {
1666
1516
  }
1667
1517
  const rawTxHex = stripHex(params.rawTxHex);
1668
1518
  const path = normalizePath(params.path);
1669
- try {
1670
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1671
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "tron", signal);
1672
- _KeystoneAdapter._throwIfAborted(signal);
1673
- const requestId = uuidv4();
1674
- const requestUr = this.urEngine.buildTronSignRequest({
1675
- requestId,
1676
- rawTxHex,
1677
- path,
1678
- xfp: record.masterFingerprint
1679
- });
1680
- const responseUr = await this._resolveUr(
1681
- record,
1682
- requestUr,
1683
- true,
1684
- connectId,
1685
- signal,
1686
- "tronSignTransaction"
1687
- );
1688
- _KeystoneAdapter._throwIfAborted(signal);
1689
- const sig = this.urEngine.parseTronSignature(responseUr);
1690
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1691
- return success({ signature: sig.signature });
1692
- });
1693
- } catch (err) {
1694
- return this._errorToFailure(err);
1695
- }
1519
+ return this._signWithWallet({
1520
+ connectId,
1521
+ deviceId,
1522
+ chain: "tron",
1523
+ operationName: "tronSignTransaction",
1524
+ buildRequest: (requestId, xfp) => this.urEngine.buildTronSignRequest({
1525
+ requestId,
1526
+ rawTxHex,
1527
+ path,
1528
+ xfp,
1529
+ origin: this._origin
1530
+ }),
1531
+ parseResponse: (ur) => this.urEngine.parseTronSignature(ur),
1532
+ // Keystone signs rawTxHex as given and returns only the signature, so no serializedTx.
1533
+ toPayload: (sig) => ({ signature: sig.signature })
1534
+ });
1696
1535
  }
1697
- async tronSignMessage(connectIdArg, deviceIdArg, paramsArg) {
1698
- const operationTarget = resolveHardwareOperationTarget(connectIdArg, paramsArg?.operationId);
1536
+ async tronSignMessage(connectIdArg, deviceIdArg, params) {
1537
+ const operationTarget = resolveHardwareOperationTarget(connectIdArg, params?.operationId);
1699
1538
  if (!operationTarget.success) return operationTarget;
1700
1539
  const connectId = operationTarget.payload.targetId;
1701
1540
  const deviceId = deviceIdArg ?? void 0;
1702
- const params = paramsArg;
1703
1541
  if (!params) return failure(HardwareErrorCode.InvalidParams, "tronSignMessage requires params");
1704
1542
  if (!params.path) {
1705
1543
  return failure(HardwareErrorCode.InvalidParams, "tronSignMessage requires params.path");
@@ -1715,41 +1553,75 @@ var KeystoneAdapter = class _KeystoneAdapter {
1715
1553
  }
1716
1554
  const messageHex = stripHex(params.messageHex);
1717
1555
  const path = normalizePath(params.path);
1556
+ return this._signWithWallet({
1557
+ connectId,
1558
+ deviceId,
1559
+ chain: "tron",
1560
+ operationName: "tronSignMessage",
1561
+ buildRequest: (requestId, xfp) => this.urEngine.buildTronSignRequest({
1562
+ requestId,
1563
+ rawTxHex: messageHex,
1564
+ path,
1565
+ xfp,
1566
+ signType: 2 /* PersonalMessage */,
1567
+ origin: this._origin
1568
+ }),
1569
+ parseResponse: (ur) => this.urEngine.parseTronSignature(ur),
1570
+ toPayload: (sig) => ({ signature: sig.signature })
1571
+ });
1572
+ }
1573
+ // ---------------------------------------------------------------------------
1574
+ // Internals
1575
+ // ---------------------------------------------------------------------------
1576
+ async _runJob(connectId, deviceId, job) {
1718
1577
  try {
1719
- return await this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1720
- const { record } = await this._ensureWalletKnown(connectId, deviceId, "tron", signal);
1721
- _KeystoneAdapter._throwIfAborted(signal);
1722
- const requestId = uuidv4();
1723
- const requestUr = this.urEngine.buildTronSignRequest({
1724
- requestId,
1725
- rawTxHex: messageHex,
1726
- path,
1727
- xfp: record.masterFingerprint,
1728
- signType: 2 /* PersonalMessage */
1729
- });
1730
- const responseUr = await this._resolveUr(
1731
- record,
1732
- requestUr,
1733
- true,
1734
- connectId,
1735
- signal,
1736
- "tronSignMessage"
1737
- );
1738
- _KeystoneAdapter._throwIfAborted(signal);
1739
- const sig = this.urEngine.parseTronSignature(responseUr);
1740
- _KeystoneAdapter._assertRequestIdMatches(requestId, sig.requestId);
1741
- return success({ signature: sig.signature });
1742
- });
1578
+ return await this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), job);
1743
1579
  } catch (err) {
1744
1580
  return this._errorToFailure(err);
1745
1581
  }
1746
1582
  }
1747
- // ---------------------------------------------------------------------------
1748
- // Internals
1749
- // ---------------------------------------------------------------------------
1750
- _walletIdFromIdentifier(identifier) {
1751
- const value = identifier.startsWith(KEYSTONE_WALLET_CONNECT_ID_PREFIX) ? identifier.slice(KEYSTONE_WALLET_CONNECT_ID_PREFIX.length) : identifier;
1752
- return /^[0-9a-f]{64}$/i.test(value) ? value.toLowerCase() : void 0;
1583
+ async _fetchAccountXpub(connectId, deviceId, hwkChain, accountPath, signal, book) {
1584
+ const { account } = await this._fetchAccount(
1585
+ connectId,
1586
+ deviceId,
1587
+ hwkChain,
1588
+ accountPath,
1589
+ signal,
1590
+ book
1591
+ );
1592
+ _KeystoneAdapter._throwIfAborted(signal);
1593
+ if (!account.extendedPublicKey) {
1594
+ throw createHwkError({
1595
+ code: HardwareErrorCode.MethodNotSupported,
1596
+ message: "Keystone did not return an extended public key for this account path"
1597
+ });
1598
+ }
1599
+ return account.extendedPublicKey;
1600
+ }
1601
+ /**
1602
+ * Signing needs only the wallet's mfp: the device re-derives the key from path+xfp, so no
1603
+ * per-path account lookup happens here.
1604
+ */
1605
+ _signWithWallet(args) {
1606
+ const { connectId, deviceId } = args;
1607
+ return this._runJob(connectId, deviceId, async (signal) => {
1608
+ const { record } = await this._ensureWalletKnown(connectId, deviceId, args.chain, signal);
1609
+ _KeystoneAdapter._throwIfAborted(signal);
1610
+ const requestId = uuidv4();
1611
+ const requestUr = args.buildRequest(requestId, record.masterFingerprint);
1612
+ const responseUr = await this._resolveUr(
1613
+ record,
1614
+ requestUr,
1615
+ true,
1616
+ connectId,
1617
+ signal,
1618
+ args.operationName
1619
+ );
1620
+ _KeystoneAdapter._throwIfAborted(signal);
1621
+ const signature = args.parseResponse(responseUr);
1622
+ _KeystoneAdapter._assertRequestIdMatches(requestId, signature.requestId);
1623
+ return success(args.toPayload(signature));
1624
+ });
1753
1625
  }
1754
1626
  _allNetworkSyncSchema(item) {
1755
1627
  if (!isAllNetworkMethodName(String(item.methodName)) || !item.path) return void 0;
@@ -1772,16 +1644,13 @@ var KeystoneAdapter = class _KeystoneAdapter {
1772
1644
  }
1773
1645
  }
1774
1646
  /**
1775
- * QR is an interactive batch transport: one account-creation action must
1776
- * produce one request QR and one response scan, regardless of how many
1777
- * chains or derivation paths the host bundled. USB keeps its proven
1778
- * one-path-at-a-time export flow because the device limits that channel.
1647
+ * QR answers the whole bundle with one request QR and one scan. USB exports one path per
1648
+ * request because the device limits that channel.
1779
1649
  */
1780
1650
  async _prefetchAllNetworkAccounts(connectId, deviceId, params) {
1781
- return this._jobQueue.enqueue(getKeystoneJobId(connectId, deviceId), async (signal) => {
1651
+ return this._jobQueue.enqueue(keystoneQueueKey(connectId, deviceId), async (signal) => {
1782
1652
  const target = this._resolveTarget(connectId, deviceId);
1783
- const { record: targetRecord } = target;
1784
- let record = targetRecord;
1653
+ let { record } = target;
1785
1654
  const requestedByKey = /* @__PURE__ */ new Map();
1786
1655
  for (const item of params.bundle) {
1787
1656
  const schema = this._allNetworkSyncSchema(item);
@@ -1791,11 +1660,23 @@ var KeystoneAdapter = class _KeystoneAdapter {
1791
1660
  if (!requestedByKey.size) {
1792
1661
  return { book };
1793
1662
  }
1794
- const operation = isHardwareOperationId(params.operationId) ? this._operations.find(params.operationId) : void 0;
1663
+ const requestedSchemas = Array.from(requestedByKey.values());
1664
+ const assertAllBooked = () => {
1665
+ for (const schema of requestedSchemas) {
1666
+ if (!book.has(accountKey(schema.hwkChain, schema.path))) {
1667
+ throw createHwkError({
1668
+ code: HardwareErrorCode.DeviceMismatch,
1669
+ message: `Keystone did not return the requested derivation path (${schema.path})`
1670
+ });
1671
+ }
1672
+ }
1673
+ };
1674
+ const operation = isHardwareOperationId2(params.operationId) ? this._operations.find(params.operationId) : void 0;
1795
1675
  const operationRoute = operation ? this._operationRoutes.get(operation.connectId) : void 0;
1796
1676
  const routedToQr = operationRoute?.operationId === operation?.operationId && operationRoute?.connectionType === "qr";
1797
1677
  if (this._forcedTransport !== "qr" && !routedToQr && !record?.usbSessionId && this._usbConnector && target.expectedWalletId) {
1798
1678
  record = await this._tryUsbAttach(
1679
+ signal,
1799
1680
  target.expectedWalletId,
1800
1681
  record?.masterFingerprint ?? target.expectedMasterFingerprint
1801
1682
  ) ?? record;
@@ -1811,8 +1692,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
1811
1692
  });
1812
1693
  }
1813
1694
  };
1814
- const missingSchemas2 = Array.from(requestedByKey.values());
1815
- for (const schema of missingSchemas2) {
1695
+ for (const schema of requestedSchemas) {
1816
1696
  if (!book.has(accountKey(schema.hwkChain, schema.path))) {
1817
1697
  assertBundleSession();
1818
1698
  const synced = await this._fetchAccount(
@@ -1821,20 +1701,13 @@ var KeystoneAdapter = class _KeystoneAdapter {
1821
1701
  schema.hwkChain,
1822
1702
  schema.path,
1823
1703
  signal,
1824
- { book }
1704
+ book
1825
1705
  );
1826
1706
  record = synced.record;
1827
1707
  assertBundleSession();
1828
1708
  }
1829
1709
  }
1830
- for (const schema of missingSchemas2) {
1831
- if (!book.has(accountKey(schema.hwkChain, schema.path))) {
1832
- throw createHwkError({
1833
- code: HardwareErrorCode.DeviceMismatch,
1834
- message: `Keystone did not return the requested derivation path (${schema.path})`
1835
- });
1836
- }
1837
- }
1710
+ assertAllBooked();
1838
1711
  return { book, walletId: record.walletId };
1839
1712
  }
1840
1713
  if (this._forcedTransport === "usb") {
@@ -1843,21 +1716,20 @@ var KeystoneAdapter = class _KeystoneAdapter {
1843
1716
  message: "USB channel is not connected for this Keystone wallet"
1844
1717
  });
1845
1718
  }
1846
- const missingSchemas = Array.from(requestedByKey.values());
1847
1719
  const schemas = [
1848
1720
  { hwkChain: "evm", path: KEYSTONE_WALLET_ID_PATH },
1849
- ...missingSchemas.filter((schema) => normalizePath(schema.path) !== KEYSTONE_WALLET_ID_PATH)
1721
+ ...requestedSchemas.filter(
1722
+ (schema) => normalizePath(schema.path) !== KEYSTONE_WALLET_ID_PATH
1723
+ )
1850
1724
  ];
1851
1725
  const requestUr = this.urEngine.buildKeyDerivationRequest({
1852
- schemas: schemas.map((schema) => ({
1853
- path: schema.path,
1854
- curve: schema.hwkChain === "sol" ? "ed25519" : "secp256k1"
1855
- })),
1726
+ schemas: schemas.map((schema) => keySchema(schema.hwkChain, schema.path)),
1856
1727
  origin: this._origin
1857
1728
  });
1858
1729
  const responseUr = await this._requestQrDisplayAndAwaitResponse(
1859
1730
  record ? toDeviceInfo(record) : placeholderDeviceInfo(),
1860
- { ...requestUr, animated: false }
1731
+ { ...requestUr, animated: false },
1732
+ signal
1861
1733
  );
1862
1734
  _KeystoneAdapter._throwIfAborted(signal);
1863
1735
  const parsed = this.urEngine.parseAccountResponse(responseUr);
@@ -1870,20 +1742,10 @@ var KeystoneAdapter = class _KeystoneAdapter {
1870
1742
  const normalizedPath = normalizePath(account.path);
1871
1743
  const hwkChain = requestedChainByPath.get(normalizedPath) ?? inferHwkChainFromPath(normalizedPath);
1872
1744
  if (hwkChain) {
1873
- book.set(accountKey(hwkChain, normalizedPath), {
1874
- ...account,
1875
- hwkChain
1876
- });
1877
- }
1878
- }
1879
- for (const schema of missingSchemas) {
1880
- if (!book.has(accountKey(schema.hwkChain, schema.path))) {
1881
- throw createHwkError({
1882
- code: HardwareErrorCode.DeviceMismatch,
1883
- message: `Keystone did not return the requested derivation path (${schema.path})`
1884
- });
1745
+ book.set(accountKey(hwkChain, normalizedPath), { ...account, hwkChain });
1885
1746
  }
1886
1747
  }
1748
+ assertAllBooked();
1887
1749
  return { book, walletId: record.walletId };
1888
1750
  });
1889
1751
  }
@@ -1892,15 +1754,12 @@ var KeystoneAdapter = class _KeystoneAdapter {
1892
1754
  * fingerprint is protocol metadata and is never accepted as a lookup key.
1893
1755
  */
1894
1756
  _resolveTarget(connectId, deviceId) {
1895
- let resolvedConnectId = connectId;
1896
- if (isHardwareOperationId(connectId)) {
1897
- resolvedConnectId = this._operations.resolve(connectId).connectId;
1898
- }
1757
+ const resolvedConnectId = isHardwareOperationId2(connectId) ? this._operations.resolve(connectId).connectId : connectId;
1899
1758
  const identifiers = [deviceId, resolvedConnectId].filter(
1900
1759
  (identifier) => Boolean(identifier)
1901
1760
  );
1902
1761
  if (!identifiers.length) return {};
1903
- const walletIds = identifiers.map((identifier) => this._walletIdFromIdentifier(identifier)).filter((walletId) => Boolean(walletId));
1762
+ const walletIds = identifiers.map((identifier) => keystoneWalletIdFromIdentifier(identifier)).filter((walletId) => Boolean(walletId));
1904
1763
  if (!walletIds.length) {
1905
1764
  throw createHwkError({
1906
1765
  code: HardwareErrorCode.DeviceNotFound,
@@ -1914,7 +1773,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
1914
1773
  message: "Keystone connectId and deviceId identify different wallets"
1915
1774
  });
1916
1775
  }
1917
- if (identifiers.some((identifier) => !this._walletIdFromIdentifier(identifier))) {
1776
+ if (identifiers.some((identifier) => !keystoneWalletIdFromIdentifier(identifier))) {
1918
1777
  throw createHwkError({
1919
1778
  code: HardwareErrorCode.DeviceMismatch,
1920
1779
  message: "Keystone connectId and deviceId are inconsistent"
@@ -1944,13 +1803,8 @@ var KeystoneAdapter = class _KeystoneAdapter {
1944
1803
  return walletId;
1945
1804
  }
1946
1805
  /**
1947
- * Folds a parsed account-response UR into the device table. `viaUsb`
1948
- * (defaults false) says which channel actually carried this round trip —
1949
- * `_resolveUr` routes a KeyDerivation sync over USB when the target record
1950
- * already has a live session, so this must NOT unconditionally mark
1951
- * `qrSynced`, or a USB-only wallet would wrongly survive a later USB
1952
- * disconnect as a "QR-synced, demote to QR-only" entry instead of being
1953
- * dropped outright (see `releaseOperation`).
1806
+ * Folds a parsed account response into the device table. Only non-USB round trips mark
1807
+ * `qrSynced`, so a USB-only wallet is dropped on disconnect.
1954
1808
  */
1955
1809
  _upsertDeviceRecord(parsed, options) {
1956
1810
  const walletId = deriveKeystoneWalletId(parsed.accounts);
@@ -1976,34 +1830,38 @@ var KeystoneAdapter = class _KeystoneAdapter {
1976
1830
  return record;
1977
1831
  }
1978
1832
  /**
1979
- * Opens+claims whatever Keystone the USB connector currently has
1980
- * permission for, learns its protocol mfp via `getAppConfig`, then requests
1981
- * one fixed account-level xpub and derives the stable wallet id. A
1982
- * QR-synced entry becomes
1983
- * `{qr, usb}`-capable in place (one `device-changed`, not a second
1984
- * `device-connect`); a wallet never seen before becomes a new USB-only
1985
- * entry. See §4.2 of the design doc.
1833
+ * Opens a permitted USB Keystone, reads its mfp, and derives the wallet id from the identity
1834
+ * xpub. A known wallet gains USB in place (`device-changed`); an unseen one becomes USB-only.
1986
1835
  */
1987
- async _connectUsb(expected, searchTargetId) {
1988
- if (this._pendingUsbTeardowns > 0) {
1836
+ async _connectUsb(expected, searchTargetId, signal) {
1837
+ if (this._pendingUsbTeardowns > 0 || this._abandonedUsbConnects > 0) {
1989
1838
  return failure(
1990
1839
  HardwareErrorCode.DeviceBusyInternal,
1991
1840
  "Keystone USB session is being released"
1992
1841
  );
1993
1842
  }
1994
- const previous = this._usbConnectTail;
1995
- let release;
1996
- this._usbConnectTail = new Promise((resolve) => {
1997
- release = resolve;
1843
+ _KeystoneAdapter._throwIfAborted(signal);
1844
+ let abandoned = false;
1845
+ const onAbort = () => {
1846
+ abandoned = true;
1847
+ this._abandonedUsbConnects += 1;
1848
+ };
1849
+ signal?.addEventListener("abort", onAbort, { once: true });
1850
+ const pending = this._usbConnectTail.then(async () => {
1851
+ _KeystoneAdapter._throwIfAborted(signal);
1852
+ return this._connectUsbExclusive(expected, searchTargetId, signal);
1998
1853
  });
1999
- await previous;
2000
- try {
2001
- return await this._connectUsbExclusive(expected, searchTargetId);
2002
- } finally {
2003
- release?.();
2004
- }
1854
+ const settled = pending.finally(() => {
1855
+ signal?.removeEventListener("abort", onAbort);
1856
+ if (abandoned) this._abandonedUsbConnects -= 1;
1857
+ });
1858
+ this._usbConnectTail = settled.then(
1859
+ () => void 0,
1860
+ () => void 0
1861
+ );
1862
+ return signal ? _KeystoneAdapter._abortable(signal, settled) : settled;
2005
1863
  }
2006
- async _connectUsbExclusive(expected, searchTargetId) {
1864
+ async _connectUsbExclusive(expected, searchTargetId, signal) {
2007
1865
  if (!this._usbConnector) {
2008
1866
  return failure(
2009
1867
  HardwareErrorCode.TransportNotAvailable,
@@ -2024,6 +1882,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
2024
1882
  const connector = this._usbConnector;
2025
1883
  const session = await (connector.connectTarget ? connector.connectTarget(connectTarget) : connector.connect(searchTargetId ?? expected.expectedMasterFingerprint));
2026
1884
  sessionId = session.sessionId;
1885
+ _KeystoneAdapter._throwIfAborted(signal);
2027
1886
  const raw = session.deviceInfo.raw;
2028
1887
  const mfpValue = parseBip32MasterFingerprint2(raw?.masterFingerprint);
2029
1888
  if (!mfpValue) {
@@ -2040,7 +1899,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
2040
1899
  });
2041
1900
  }
2042
1901
  const identityRequest = this.urEngine.buildKeyDerivationRequest({
2043
- schemas: [{ path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" }],
1902
+ schemas: [keySchema("evm", KEYSTONE_WALLET_ID_PATH)],
2044
1903
  origin: this._origin
2045
1904
  });
2046
1905
  const identityResult = await this._callUsbConnector(
@@ -2048,6 +1907,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
2048
1907
  "resolveUr",
2049
1908
  identityRequest
2050
1909
  );
1910
+ _KeystoneAdapter._throwIfAborted(signal);
2051
1911
  if (!identityResult.success) {
2052
1912
  throw rehydrateConnectorError(identityResult.error);
2053
1913
  }
@@ -2058,21 +1918,22 @@ var KeystoneAdapter = class _KeystoneAdapter {
2058
1918
  });
2059
1919
  const walletId = deriveKeystoneWalletId(parsed.accounts);
2060
1920
  const previousUsbSessionId = this._devices.get(walletId)?.usbSessionId;
2061
- const record = this._upsertDeviceRecord(parsed, {
2062
- viaUsb: true,
2063
- usbSessionId: session.sessionId
2064
- });
2065
1921
  if (previousUsbSessionId && previousUsbSessionId !== session.sessionId) {
2066
1922
  try {
2067
1923
  await this._usbConnector.disconnect(previousUsbSessionId);
2068
1924
  } catch {
2069
1925
  }
2070
1926
  }
1927
+ _KeystoneAdapter._throwIfAborted(signal);
1928
+ const record = this._upsertDeviceRecord(parsed, {
1929
+ viaUsb: true,
1930
+ usbSessionId: session.sessionId
1931
+ });
1932
+ _KeystoneAdapter._throwIfAborted(signal);
2071
1933
  record.hadUsbSession = true;
2072
1934
  record.model = session.deviceInfo.modelName ?? session.deviceInfo.model ?? record.model;
2073
1935
  record.deviceVersion = session.deviceInfo.firmwareVersion ?? record.deviceVersion;
2074
- const info = toDeviceInfo(record);
2075
- return success(info);
1936
+ return success(toDeviceInfo(record));
2076
1937
  } catch (err) {
2077
1938
  if (sessionId) {
2078
1939
  try {
@@ -2084,13 +1945,11 @@ var KeystoneAdapter = class _KeystoneAdapter {
2084
1945
  }
2085
1946
  }
2086
1947
  /**
2087
- * Probe already-authorized USB devices without opening a permission picker,
2088
- * then attach and verify the requested wallet when one is present. This is
2089
- * used both after an adapter restart and after a QR-only period, so plugging
2090
- * USB back in affects the next business call without changing an in-flight
2091
- * QR operation.
1948
+ * Probes already-authorized USB devices without a permission picker, then attaches and verifies
1949
+ * the requested wallet if present.
2092
1950
  */
2093
- async _tryUsbAttach(expectedWalletId, expectedMasterFingerprint, options) {
1951
+ async _tryUsbAttach(signal, expectedWalletId, expectedMasterFingerprint, options) {
1952
+ _KeystoneAdapter._throwIfAborted(signal);
2094
1953
  if (!expectedWalletId || this._forcedTransport === "qr" || !this._usbConnector) {
2095
1954
  return void 0;
2096
1955
  }
@@ -2098,17 +1957,22 @@ var KeystoneAdapter = class _KeystoneAdapter {
2098
1957
  let availableDevices = [];
2099
1958
  let lastSearchError;
2100
1959
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1960
+ _KeystoneAdapter._throwIfAborted(signal);
2101
1961
  try {
2102
- availableDevices = await this._usbConnector.searchDevices({ purpose: "availability" });
1962
+ availableDevices = await _KeystoneAdapter._abortable(
1963
+ signal,
1964
+ this._usbConnector.searchDevices({ purpose: "availability" })
1965
+ );
2103
1966
  lastSearchError = void 0;
2104
1967
  } catch (error) {
2105
1968
  lastSearchError = error;
2106
1969
  }
1970
+ _KeystoneAdapter._throwIfAborted(signal);
2107
1971
  if (availableDevices.length) {
2108
1972
  break;
2109
1973
  }
2110
1974
  if (attempt < maxAttempts) {
2111
- await waitForKeystoneUsbReattachProbe();
1975
+ await _KeystoneAdapter._abortable(signal, waitForKeystoneUsbReattachProbe());
2112
1976
  }
2113
1977
  }
2114
1978
  if (!availableDevices.length) {
@@ -2123,8 +1987,16 @@ var KeystoneAdapter = class _KeystoneAdapter {
2123
1987
  for (const device of candidates) {
2124
1988
  attached = await this._connectUsb(
2125
1989
  { expectedWalletId, expectedMasterFingerprint },
2126
- device?.connectId
1990
+ device?.connectId,
1991
+ signal
2127
1992
  );
1993
+ _KeystoneAdapter._throwIfAborted(signal);
1994
+ if (!attached.success && attached.payload.code === HardwareErrorCode.DeviceBusyInternal) {
1995
+ throw createHwkError({
1996
+ ...attached.payload,
1997
+ message: attached.payload.error
1998
+ });
1999
+ }
2128
2000
  if (attached.success) {
2129
2001
  record = this._devices.get(expectedWalletId);
2130
2002
  if (record) break;
@@ -2133,17 +2005,11 @@ var KeystoneAdapter = class _KeystoneAdapter {
2133
2005
  return record;
2134
2006
  }
2135
2007
  /**
2136
- * The one place that decides QR vs. USB for a UR round trip and carries it
2137
- * out. `record` is the (possibly not-yet-existing, for a true cold start)
2138
- * device row for the target wallet. USB is the preferred channel: a known
2139
- * wallet record with no live session gets ONE best-effort re-attach here,
2140
- * and anything that goes wrong just leaves the call on QR. A
2141
- * `switchTransport('qr')` pin forces QR even for a USB-attached wallet;
2142
- * `switchTransport('usb')` on a wallet with no live USB session fails
2143
- * closed rather than silently falling back to QR.
2008
+ * Decides QR vs USB for one UR round trip. A pinned 'usb' with no live session fails closed;
2009
+ * otherwise a failed USB reattach leaves the call on QR.
2144
2010
  */
2145
2011
  async _resolveUr(record, requestUr, animated, operationId, signal, operationName) {
2146
- const releaseOperationRetention = isHardwareOperationId(operationId) ? this._operations.retain(operationId) : void 0;
2012
+ const releaseOperationRetention = isHardwareOperationId2(operationId) ? this._operations.retain(operationId) : void 0;
2147
2013
  try {
2148
2014
  return await this._resolveUrWithRoute(
2149
2015
  record,
@@ -2159,21 +2025,19 @@ var KeystoneAdapter = class _KeystoneAdapter {
2159
2025
  }
2160
2026
  async _resolveUrWithRoute(record, requestUr, animated, operationId, signal, operationName) {
2161
2027
  let interactionRoute;
2162
- if (isHardwareOperationId(operationId)) {
2028
+ if (isHardwareOperationId2(operationId)) {
2163
2029
  const operation = this._operations.resolve(operationId);
2164
2030
  const route = this._operationRoutes.get(operation.connectId);
2165
2031
  if (!route || route.operationId !== operationId) {
2166
- this._operations.end(operationId, "disconnect");
2167
- throw createHwkError({
2168
- code: HardwareErrorCode.OperationEnded,
2169
- message: "Keystone hardware operation is no longer connected",
2170
- params: { operationId, reason: "disconnect" }
2171
- });
2032
+ throw this._endDisconnectedOperation(
2033
+ operationId,
2034
+ "Keystone hardware operation is no longer connected"
2035
+ );
2172
2036
  }
2173
2037
  interactionRoute = route;
2174
2038
  }
2175
2039
  if (!interactionRoute && !this._forcedTransport && record && !record.usbSessionId && this._usbConnector) {
2176
- await this._tryUsbAttach(record.walletId, record.masterFingerprint, {
2040
+ await this._tryUsbAttach(signal, record.walletId, record.masterFingerprint, {
2177
2041
  waitForReenumeration: record.hadUsbSession
2178
2042
  });
2179
2043
  }
@@ -2181,15 +2045,10 @@ var KeystoneAdapter = class _KeystoneAdapter {
2181
2045
  if (wantUsb) {
2182
2046
  if (!record?.usbSessionId || !this._usbConnector) {
2183
2047
  if (interactionRoute) {
2184
- this._operations.end(interactionRoute.operationId, "disconnect");
2185
- throw createHwkError({
2186
- code: HardwareErrorCode.OperationEnded,
2187
- message: "Keystone operation USB connection was lost",
2188
- params: {
2189
- operationId: interactionRoute.operationId,
2190
- reason: "disconnect"
2191
- }
2192
- });
2048
+ throw this._endDisconnectedOperation(
2049
+ interactionRoute.operationId,
2050
+ "Keystone operation USB connection was lost"
2051
+ );
2193
2052
  }
2194
2053
  throw createHwkError({
2195
2054
  code: HardwareErrorCode.TransportNotAvailable,
@@ -2204,14 +2063,17 @@ var KeystoneAdapter = class _KeystoneAdapter {
2204
2063
  );
2205
2064
  if (result.success) return result.payload;
2206
2065
  const usbError = rehydrateConnectorError(result.error);
2207
- const usbErrorOrigin = usbError.origin;
2208
- const usbErrorCode = usbError.code;
2209
- if (!interactionRoute && !this._forcedTransport && usbErrorCode === HardwareErrorCode.PayloadTooLarge && usbErrorOrigin !== "device") {
2210
- const displayDevice2 = toDeviceInfo(record);
2211
- return this._requestQrDisplayAndAwaitResponse(displayDevice2, {
2212
- ...requestUr,
2213
- animated
2214
- });
2066
+ const { origin: usbErrorOrigin, code: usbErrorCode } = usbError;
2067
+ if (usbErrorCode === HardwareErrorCode.PayloadTooLarge && usbErrorOrigin !== "device") {
2068
+ if (!interactionRoute && !this._forcedTransport) {
2069
+ const displayDevice2 = toDeviceInfo(record);
2070
+ return this._requestQrDisplayAndAwaitResponse(
2071
+ displayDevice2,
2072
+ { ...requestUr, animated },
2073
+ signal
2074
+ );
2075
+ }
2076
+ throw usbError;
2215
2077
  }
2216
2078
  const deviceAnswered = usbErrorOrigin !== void 0 ? usbErrorOrigin === "device" : usbErrorCode === HardwareErrorCode.UserRejected || usbErrorCode === HardwareErrorCode.UserAborted || usbErrorCode === HardwareErrorCode.DeviceLocked || usbErrorCode === HardwareErrorCode.DeviceMismatch;
2217
2079
  if (!deviceAnswered) {
@@ -2221,19 +2083,11 @@ var KeystoneAdapter = class _KeystoneAdapter {
2221
2083
  }
2222
2084
  record.usbSessionId = void 0;
2223
2085
  if (interactionRoute) {
2224
- this._operations.end(interactionRoute.operationId, "disconnect");
2225
- throw createHwkError({
2226
- code: HardwareErrorCode.OperationEnded,
2227
- message: operationName ? `Keystone ${operationName} may have completed before the USB connection was lost` : "Keystone operation USB connection was lost",
2228
- recovery: operationName ? { scope: "unknown" } : void 0,
2229
- params: operationName ? operationMayHaveCompletedParams(operationName, {
2230
- operationId: interactionRoute.operationId,
2231
- reason: "disconnect"
2232
- }) : {
2233
- operationId: interactionRoute.operationId,
2234
- reason: "disconnect"
2235
- }
2236
- });
2086
+ throw this._endDisconnectedOperation(
2087
+ interactionRoute.operationId,
2088
+ operationName ? `Keystone ${operationName} may have completed before the USB connection was lost` : "Keystone operation USB connection was lost",
2089
+ operationName
2090
+ );
2237
2091
  }
2238
2092
  if (operationName) {
2239
2093
  throw createHwkError({
@@ -2249,10 +2103,11 @@ var KeystoneAdapter = class _KeystoneAdapter {
2249
2103
  }
2250
2104
  const displayDevice = record ? toDeviceInfo(record) : placeholderDeviceInfo();
2251
2105
  try {
2252
- return await this._requestQrDisplayAndAwaitResponse(displayDevice, {
2253
- ...requestUr,
2254
- animated
2255
- });
2106
+ return await this._requestQrDisplayAndAwaitResponse(
2107
+ displayDevice,
2108
+ { ...requestUr, animated },
2109
+ signal
2110
+ );
2256
2111
  } catch (error) {
2257
2112
  const { code, _tag: tag } = error;
2258
2113
  if (operationName && (code === HardwareErrorCode.OperationTimeout || tag === UI_REQUEST_TIMEOUT_TAG)) {
@@ -2266,51 +2121,49 @@ var KeystoneAdapter = class _KeystoneAdapter {
2266
2121
  throw error;
2267
2122
  }
2268
2123
  }
2124
+ /** Ends the operation as disconnected and returns the error to throw. */
2125
+ _endDisconnectedOperation(operationId, message, operationName) {
2126
+ this._operations.end(operationId, "disconnect");
2127
+ const params = { operationId, reason: "disconnect" };
2128
+ return createHwkError({
2129
+ code: HardwareErrorCode.OperationEnded,
2130
+ message,
2131
+ recovery: operationName ? { scope: "unknown" } : void 0,
2132
+ params: operationName ? operationMayHaveCompletedParams(operationName, params) : params
2133
+ });
2134
+ }
2269
2135
  /**
2270
- * Fetch one account's key material from the device for this operation only.
2271
- * `book` lets earlier fetches of the same operation be reused; nothing is
2272
- * retained on the record. A failed USB export ends this operation without
2273
- * replaying the request or switching transports.
2136
+ * Fetches one account's key material for this operation only; `book` reuses earlier fetches. A
2137
+ * failed USB export ends the operation without replaying or switching transports.
2274
2138
  */
2275
- async _fetchAccount(connectId, deviceId, hwkChain, syncPath, signal, options) {
2139
+ async _fetchAccount(connectId, deviceId, hwkChain, syncPath, signal, book) {
2276
2140
  const target = this._resolveTarget(connectId, deviceId);
2141
+ const operationConnectionType = isHardwareOperationId2(connectId) ? this._operations.resolve(connectId).connectionType : void 0;
2277
2142
  const key = accountKey(hwkChain, syncPath);
2278
2143
  let existingRecord = target.record;
2279
2144
  if (!existingRecord) {
2280
- const attached = await this._tryUsbAttach(target.expectedWalletId);
2145
+ existingRecord = await this._tryUsbAttach(signal, target.expectedWalletId);
2281
2146
  _KeystoneAdapter._throwIfAborted(signal);
2282
- existingRecord = attached;
2283
2147
  }
2284
- const book = options?.book;
2285
2148
  const booked = book?.get(key);
2286
2149
  if (existingRecord && booked) {
2287
2150
  return { record: existingRecord, account: booked };
2288
2151
  }
2289
- if (existingRecord && !existingRecord.usbSessionId && this._forcedTransport === "usb") {
2290
- await this._tryUsbAttach(existingRecord.walletId, existingRecord.masterFingerprint, {
2152
+ if (!operationConnectionType && existingRecord && !existingRecord.usbSessionId && this._forcedTransport === "usb") {
2153
+ await this._tryUsbAttach(signal, existingRecord.walletId, existingRecord.masterFingerprint, {
2291
2154
  waitForReenumeration: existingRecord.hadUsbSession
2292
2155
  });
2293
2156
  _KeystoneAdapter._throwIfAborted(signal);
2294
2157
  existingRecord = this._devices.get(existingRecord.walletId) ?? existingRecord;
2295
2158
  }
2296
- const useSinglePathUsbExport = this._forcedTransport !== "qr" && Boolean(existingRecord?.usbSessionId);
2297
- const requestedSchemaPaths = [
2298
- { hwkChain, path: syncPath }
2299
- ];
2300
- const schemaPaths = useSinglePathUsbExport || requestedSchemaPaths.some((schema) => normalizePath(schema.path) === KEYSTONE_WALLET_ID_PATH) ? requestedSchemaPaths : [{ hwkChain: "evm", path: KEYSTONE_WALLET_ID_PATH }, ...requestedSchemaPaths];
2301
- const requestUr = this.urEngine.buildKeyDerivationRequest({
2302
- schemas: schemaPaths.map((s) => ({
2303
- path: s.path,
2304
- curve: s.hwkChain === "sol" ? "ed25519" : "secp256k1"
2305
- })),
2306
- origin: this._origin
2307
- });
2308
- const resolvedViaUsb = useSinglePathUsbExport;
2159
+ const useSinglePathUsbExport = operationConnectionType ? operationConnectionType === "usb" : this._forcedTransport !== "qr" && Boolean(existingRecord?.usbSessionId);
2160
+ const schemas = useSinglePathUsbExport || normalizePath(syncPath) === KEYSTONE_WALLET_ID_PATH ? [keySchema(hwkChain, syncPath)] : [keySchema("evm", KEYSTONE_WALLET_ID_PATH), keySchema(hwkChain, syncPath)];
2161
+ const requestUr = this.urEngine.buildKeyDerivationRequest({ schemas, origin: this._origin });
2309
2162
  const responseUr = await this._resolveUr(existingRecord, requestUr, false, connectId, signal);
2310
2163
  _KeystoneAdapter._throwIfAborted(signal);
2311
2164
  const parsed = this.urEngine.parseAccountResponse(responseUr);
2312
2165
  let record;
2313
- if (resolvedViaUsb && existingRecord) {
2166
+ if (useSinglePathUsbExport && existingRecord) {
2314
2167
  if (parsed.masterFingerprint !== existingRecord.masterFingerprint) {
2315
2168
  throw createHwkError({
2316
2169
  code: HardwareErrorCode.DeviceMismatch,
@@ -2321,7 +2174,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
2321
2174
  } else {
2322
2175
  this._assertParsedIdentity(parsed, target);
2323
2176
  record = this._upsertDeviceRecord(parsed, {
2324
- viaUsb: Boolean(existingRecord?.usbSessionId)
2177
+ viaUsb: operationConnectionType !== "qr" && Boolean(existingRecord?.usbSessionId)
2325
2178
  });
2326
2179
  }
2327
2180
  const account = parsed.accounts.find((a) => normalizePath(a.path) === syncPath);
@@ -2347,68 +2200,90 @@ var KeystoneAdapter = class _KeystoneAdapter {
2347
2200
  return { record, account: entry };
2348
2201
  }
2349
2202
  /**
2350
- * Like `_fetchAccount`, but for operations (PSBT signing, master
2351
- * fingerprint) that only need to know WHICH wallet is attached, not a
2352
- * specific cached path. Syncs the account-level path for `chain` as a
2353
- * throwaway probe when the wallet record isn't already known.
2354
- *
2355
- * `CHAIN_FINGERPRINT_PATHS[chain]` is a 5-segment LEAF path for `evm`
2356
- * (`m/44'/60'/0'/0/0`) — sending that verbatim as a KeyDerivation request
2357
- * asks Keystone for a non-standard path. Keystone's own docs
2358
- * (dev.keyst.one's multichain KeyDerivation example) show the ETH
2359
- * account-level path as `m/44'/60'/0'` (3 segments), same as what
2360
- * `_fetchAccount` already requests — so
2361
- * truncate through `splitAccountPath` here too instead of using the raw
2362
- * fingerprint leaf path. `btc`/`sol` are already 3-segment account paths
2363
- * and pass through unchanged.
2203
+ * Resolves which wallet is attached, without a per-path account. `CHAIN_FINGERPRINT_PATHS.evm` is
2204
+ * a leaf path, so it is cut to the account path Keystone expects.
2364
2205
  */
2365
2206
  async _ensureWalletKnown(connectId, deviceId, chain, signal) {
2366
2207
  const target = this._resolveTarget(connectId, deviceId);
2367
2208
  if (target.record) return { record: target.record };
2368
- const attached = await this._tryUsbAttach(target.expectedWalletId);
2209
+ const attached = await this._tryUsbAttach(signal, target.expectedWalletId);
2369
2210
  _KeystoneAdapter._throwIfAborted(signal);
2370
2211
  if (attached) return { record: attached };
2371
2212
  const { accountPath } = splitAccountPath(CHAIN_FINGERPRINT_PATHS[chain]);
2372
- const schemas = accountPath === KEYSTONE_WALLET_ID_PATH ? [{ path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" }] : [
2373
- { path: KEYSTONE_WALLET_ID_PATH, curve: "secp256k1" },
2374
- {
2375
- path: accountPath,
2376
- curve: chain === "sol" ? "ed25519" : "secp256k1"
2377
- }
2378
- ];
2379
- const requestUr = this.urEngine.buildKeyDerivationRequest({
2380
- schemas,
2381
- origin: this._origin
2382
- });
2213
+ const schemas = accountPath === KEYSTONE_WALLET_ID_PATH ? [keySchema("evm", KEYSTONE_WALLET_ID_PATH)] : [keySchema("evm", KEYSTONE_WALLET_ID_PATH), keySchema(chain, accountPath)];
2214
+ const requestUr = this.urEngine.buildKeyDerivationRequest({ schemas, origin: this._origin });
2383
2215
  const responseUr = await this._resolveUr(void 0, requestUr, false, connectId, signal);
2384
2216
  _KeystoneAdapter._throwIfAborted(signal);
2385
2217
  const parsed = this.urEngine.parseAccountResponse(responseUr);
2386
2218
  this._assertParsedIdentity(parsed, target);
2387
- const record = this._upsertDeviceRecord(parsed);
2388
- return { record };
2219
+ return { record: this._upsertDeviceRecord(parsed) };
2220
+ }
2221
+ /**
2222
+ * The operation owning the running job, so a mid-call UI request can name it. Unpinned jobs map
2223
+ * through the operation route; a cold start returns undefined.
2224
+ */
2225
+ _activeOperationId() {
2226
+ const activeJobId = this._jobQueue.getActiveJob()?.deviceId;
2227
+ if (!activeJobId) return void 0;
2228
+ if (isHardwareOperationId2(activeJobId)) return activeJobId;
2229
+ return this._operationRouteForIdentifier(activeJobId)?.operationId;
2230
+ }
2231
+ /** Routes are keyed by connectId; a job key may be the bare wallet id. */
2232
+ _operationRouteForIdentifier(identifier) {
2233
+ for (const key of keystoneCancelQueueKeys([identifier])) {
2234
+ const route = this._operationRoutes.get(key);
2235
+ if (route) return route;
2236
+ }
2237
+ const walletId = keystoneWalletIdFromIdentifier(identifier);
2238
+ return walletId ? this._operationRoutes.get(walletConnectId(walletId)) : void 0;
2389
2239
  }
2390
- async _requestQrDisplayAndAwaitResponse(device, data) {
2240
+ /**
2241
+ * A QR prompt has no transport to interrupt; releasing the registry slot on abort is what lets
2242
+ * a cancel reach the wait before the QR timeout.
2243
+ */
2244
+ async _awaitQrResponse(requestType, pending, signal) {
2245
+ if (!signal) return pending;
2246
+ let settled = false;
2247
+ const onAbort = () => {
2248
+ if (!settled) this._uiRegistry.cancel(requestType);
2249
+ };
2250
+ if (signal.aborted) onAbort();
2251
+ else signal.addEventListener("abort", onAbort, { once: true });
2252
+ try {
2253
+ return await pending;
2254
+ } finally {
2255
+ settled = true;
2256
+ signal.removeEventListener("abort", onAbort);
2257
+ }
2258
+ }
2259
+ async _requestQrDisplayAndAwaitResponse(device, data, signal) {
2260
+ const operationId = this._activeOperationId();
2391
2261
  const waitPromise = this._uiRegistry.wait(
2392
2262
  UI_REQUEST.REQUEST_QR_DISPLAY,
2393
- { timeoutMs: this._qrTimeoutMs }
2263
+ { timeoutMs: this._qrTimeoutMs, operationId }
2394
2264
  );
2395
2265
  this.emitter.emit(UI_REQUEST.REQUEST_QR_DISPLAY, {
2396
2266
  type: UI_REQUEST.REQUEST_QR_DISPLAY,
2397
- payload: { device, data }
2267
+ payload: { device, data, operationId }
2398
2268
  });
2399
- const response = await waitPromise;
2269
+ const response = await this._awaitQrResponse(
2270
+ UI_REQUEST.REQUEST_QR_DISPLAY,
2271
+ waitPromise,
2272
+ signal
2273
+ );
2400
2274
  return { urType: response.urType, urData: response.urData };
2401
2275
  }
2402
- async _requestQrScanAndAwaitResponse(device) {
2276
+ async _requestQrScanAndAwaitResponse(device, signal) {
2277
+ const operationId = this._activeOperationId();
2403
2278
  const waitPromise = this._uiRegistry.wait(
2404
2279
  UI_REQUEST.REQUEST_QR_SCAN,
2405
- { timeoutMs: this._qrTimeoutMs }
2280
+ { timeoutMs: this._qrTimeoutMs, operationId }
2406
2281
  );
2407
2282
  this.emitter.emit(UI_REQUEST.REQUEST_QR_SCAN, {
2408
2283
  type: UI_REQUEST.REQUEST_QR_SCAN,
2409
- payload: { device }
2284
+ payload: { device, operationId }
2410
2285
  });
2411
- const response = await waitPromise;
2286
+ const response = await this._awaitQrResponse(UI_REQUEST.REQUEST_QR_SCAN, waitPromise, signal);
2412
2287
  return { urType: response.urType, urData: response.urData };
2413
2288
  }
2414
2289
  async _callUsbConnector(sessionId, method, params, signal) {
@@ -2424,11 +2299,16 @@ var KeystoneAdapter = class _KeystoneAdapter {
2424
2299
  message: `Keystone USB is busy while calling ${method}`
2425
2300
  });
2426
2301
  }
2302
+ if (signal?.aborted) {
2303
+ throw signal.reason instanceof Error ? signal.reason : createHwkError({
2304
+ code: HardwareErrorCode.UserAborted,
2305
+ message: "User aborted operation"
2306
+ });
2307
+ }
2427
2308
  const releaseOperation = this._retainUsbOperation(`call:${sessionId}`);
2428
2309
  let rawCall;
2429
2310
  try {
2430
- const connector = this._usbConnector;
2431
- rawCall = connector.call(sessionId, method, params).finally(releaseOperation);
2311
+ rawCall = this._usbConnector.call(sessionId, method, params).finally(releaseOperation);
2432
2312
  } catch (error) {
2433
2313
  releaseOperation();
2434
2314
  throw error;
@@ -2478,13 +2358,10 @@ var KeystoneAdapter = class _KeystoneAdapter {
2478
2358
  };
2479
2359
  }
2480
2360
  static _abortable(signal, promise) {
2481
- if (signal.aborted) {
2482
- return Promise.reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted"));
2483
- }
2361
+ const abortReason = () => signal.reason instanceof Error ? signal.reason : new Error("Aborted");
2362
+ if (signal.aborted) return Promise.reject(abortReason());
2484
2363
  return new Promise((resolve, reject) => {
2485
- const onAbort = () => {
2486
- reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted"));
2487
- };
2364
+ const onAbort = () => reject(abortReason());
2488
2365
  signal.addEventListener("abort", onAbort, { once: true });
2489
2366
  promise.then(
2490
2367
  (value) => {
@@ -2499,7 +2376,7 @@ var KeystoneAdapter = class _KeystoneAdapter {
2499
2376
  });
2500
2377
  }
2501
2378
  static _throwIfAborted(signal) {
2502
- if (signal.aborted) {
2379
+ if (signal?.aborted) {
2503
2380
  throw signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
2504
2381
  }
2505
2382
  }