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