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