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