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