@onekeyfe/hwk-ledger-adapter 1.1.26-alpha.8 → 1.1.26
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 +274 -170
- package/dist/index.d.ts +274 -170
- package/dist/index.js +1534 -568
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1535 -570
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -3,9 +3,11 @@ import {
|
|
|
3
3
|
CHAIN_FINGERPRINT_PATHS,
|
|
4
4
|
DEVICE,
|
|
5
5
|
DeviceJobQueue,
|
|
6
|
+
EConnectorInteraction,
|
|
6
7
|
HardwareErrorCode as HardwareErrorCode2,
|
|
7
8
|
TypedEventEmitter,
|
|
8
9
|
UI_REQUEST,
|
|
10
|
+
UI_REQUEST_PREEMPTED_TAG,
|
|
9
11
|
UiRequestRegistry,
|
|
10
12
|
deriveDeviceFingerprint,
|
|
11
13
|
failure,
|
|
@@ -14,24 +16,26 @@ import {
|
|
|
14
16
|
|
|
15
17
|
// src/errors.ts
|
|
16
18
|
import { HardwareErrorCode, enrichErrorMessage } from "@onekeyfe/hwk-adapter-core";
|
|
17
|
-
function ledgerFailure(code, error, appName) {
|
|
19
|
+
function ledgerFailure(code, error, appName, tag, params) {
|
|
18
20
|
const payload = { error, code };
|
|
19
21
|
if (appName !== void 0) payload.appName = appName;
|
|
22
|
+
if (tag !== void 0) payload._tag = tag;
|
|
23
|
+
if (params !== void 0) payload.params = params;
|
|
20
24
|
return { success: false, payload };
|
|
21
25
|
}
|
|
22
26
|
var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
|
|
23
27
|
var USER_REJECTED_CODES = /* @__PURE__ */ new Set(["6985", "27013"]);
|
|
24
28
|
var WRONG_APP_CODES = /* @__PURE__ */ new Set(["6e00", "28160", "6d00", "27904", "6a83", "27267"]);
|
|
25
29
|
var APP_NOT_INSTALLED_CODES = /* @__PURE__ */ new Set(["6807", "26631"]);
|
|
26
|
-
var
|
|
30
|
+
var STEP_BLIND_SIGN_TRANSACTION_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
|
|
27
31
|
function getEthAppErrorCode(err) {
|
|
28
32
|
if (!err || typeof err !== "object") return null;
|
|
29
33
|
const e = err;
|
|
30
|
-
if (e._tag ===
|
|
34
|
+
if (e._tag === ERROR_TAG.EthAppCommand && typeof e.errorCode === "string") {
|
|
31
35
|
return e.errorCode.toLowerCase();
|
|
32
36
|
}
|
|
33
37
|
const orig = e.originalError;
|
|
34
|
-
if (orig?._tag ===
|
|
38
|
+
if (orig?._tag === ERROR_TAG.EthAppCommand && typeof orig.errorCode === "string") {
|
|
35
39
|
return orig.errorCode.toLowerCase();
|
|
36
40
|
}
|
|
37
41
|
return null;
|
|
@@ -88,13 +92,8 @@ function mapBtcAppError(hex) {
|
|
|
88
92
|
return null;
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
|
-
function
|
|
95
|
+
function mapEthAppErrorCode(ethCode) {
|
|
92
96
|
switch (ethCode) {
|
|
93
|
-
case "6a80":
|
|
94
|
-
if (lastStep === STEP_BLIND_SIGN_FALLBACK) {
|
|
95
|
-
return HardwareErrorCode.EvmBlindSigningRequired;
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
97
|
case "6984":
|
|
99
98
|
return HardwareErrorCode.EvmClearSignPluginMissing;
|
|
100
99
|
case "6a84":
|
|
@@ -107,16 +106,133 @@ function mapEthAppError(ethCode, lastStep) {
|
|
|
107
106
|
return null;
|
|
108
107
|
}
|
|
109
108
|
}
|
|
109
|
+
function classifyEthAppError(err) {
|
|
110
|
+
const ethCode = getEthAppErrorCode(err);
|
|
111
|
+
if (!ethCode) return null;
|
|
112
|
+
if (ethCode === "6a80") {
|
|
113
|
+
return hasBlindSignFallbackStep(err) ? HardwareErrorCode.EvmBlindSigningRequired : null;
|
|
114
|
+
}
|
|
115
|
+
return mapEthAppErrorCode(ethCode);
|
|
116
|
+
}
|
|
117
|
+
var ERROR_TAG = {
|
|
118
|
+
// SDK-mint
|
|
119
|
+
DeviceNotAdvertising: "DeviceNotAdvertisingError",
|
|
120
|
+
// BLE scan miss
|
|
121
|
+
DeviceNotInDiscoveryCache: "DeviceNotInDiscoveryCacheError",
|
|
122
|
+
// dm.connect() before enumerate
|
|
123
|
+
BlePairingTimeout: "BlePairingTimeoutError",
|
|
124
|
+
// SMP 30s timeout
|
|
125
|
+
BleGattBondingFailed: "BleGattBondingFailedError",
|
|
126
|
+
// other GATT failure
|
|
127
|
+
UserAborted: "UserAborted",
|
|
128
|
+
DeviceAppStuck: "DeviceAppStuck",
|
|
129
|
+
// chain app wedged (APDU 0x6901)
|
|
130
|
+
DeviceTransportStuck: "DeviceTransportStuck",
|
|
131
|
+
// DMK transport queue wedged
|
|
132
|
+
// DMK-reuse (DMK throws same string; we synthesize too)
|
|
133
|
+
DeviceLocked: "DeviceLockedError",
|
|
134
|
+
DeviceNotRecognized: "DeviceNotRecognizedError",
|
|
135
|
+
DeviceSessionNotFound: "DeviceSessionNotFound",
|
|
136
|
+
OpenAppCommand: "OpenAppCommandError",
|
|
137
|
+
// DMK-only (read only)
|
|
138
|
+
EthAppCommand: "EthAppCommandError",
|
|
139
|
+
UserRefusedOnDevice: "UserRefusedOnDevice",
|
|
140
|
+
WrongAppOpened: "WrongAppOpenedError",
|
|
141
|
+
InvalidStatusWord: "InvalidStatusWordError",
|
|
142
|
+
AlreadySendingApdu: "AlreadySendingApduError",
|
|
143
|
+
UnknownDeviceExchange: "UnknownDeviceExchangeError",
|
|
144
|
+
NoAccessibleDevice: "NoAccessibleDeviceError",
|
|
145
|
+
UnknownDevice: "UnknownDeviceError",
|
|
146
|
+
DeviceSessionRefresher: "DeviceSessionRefresherError",
|
|
147
|
+
DeviceNotInitialized: "DeviceNotInitializedError",
|
|
148
|
+
OpeningConnection: "OpeningConnectionError",
|
|
149
|
+
DeviceDisconnectedBeforeSendingApdu: "DeviceDisconnectedBeforeSendingApdu",
|
|
150
|
+
DeviceDisconnectedWhileSending: "DeviceDisconnectedWhileSendingError",
|
|
151
|
+
Disconnect: "DisconnectError",
|
|
152
|
+
ReconnectionFailed: "ReconnectionFailedError",
|
|
153
|
+
WebHIDDisconnect: "WebHIDDisconnectError",
|
|
154
|
+
// ble-plx surfaces this when GATT notification setup fails — typical
|
|
155
|
+
// outcome when the user doesn't confirm pairing on the device, or the
|
|
156
|
+
// existing bond is invalid. Observed in production after ~30s.
|
|
157
|
+
PairingRefused: "PairingRefusedError"
|
|
158
|
+
};
|
|
110
159
|
function isDeviceLockedError(err) {
|
|
111
160
|
if (!err || typeof err !== "object") return false;
|
|
112
161
|
const e = err;
|
|
113
162
|
if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
|
|
114
163
|
if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
|
|
115
|
-
if (e._tag ===
|
|
164
|
+
if (e._tag === ERROR_TAG.DeviceLocked) return true;
|
|
116
165
|
if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
|
|
117
166
|
if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
|
|
118
167
|
return false;
|
|
119
168
|
}
|
|
169
|
+
function isDeviceNotAdvertisingError(err) {
|
|
170
|
+
if (!err || typeof err !== "object") return false;
|
|
171
|
+
return err._tag === ERROR_TAG.DeviceNotAdvertising;
|
|
172
|
+
}
|
|
173
|
+
var PAIRING_FAILURE_TAGS = /* @__PURE__ */ new Set([
|
|
174
|
+
ERROR_TAG.BlePairingTimeout,
|
|
175
|
+
ERROR_TAG.BleGattBondingFailed,
|
|
176
|
+
ERROR_TAG.PairingRefused
|
|
177
|
+
]);
|
|
178
|
+
function isBlePairingFailureError(err) {
|
|
179
|
+
if (!err || typeof err !== "object") return false;
|
|
180
|
+
const tag = err._tag;
|
|
181
|
+
if (tag && PAIRING_FAILURE_TAGS.has(tag)) return true;
|
|
182
|
+
const orig = err.originalError;
|
|
183
|
+
if (orig != null && isBlePairingFailureError(orig)) return true;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
|
|
187
|
+
ERROR_TAG.DeviceLocked,
|
|
188
|
+
ERROR_TAG.DeviceNotAdvertising,
|
|
189
|
+
ERROR_TAG.BlePairingTimeout,
|
|
190
|
+
ERROR_TAG.BleGattBondingFailed,
|
|
191
|
+
ERROR_TAG.PairingRefused,
|
|
192
|
+
ERROR_TAG.DeviceNotRecognized,
|
|
193
|
+
ERROR_TAG.NoAccessibleDevice,
|
|
194
|
+
ERROR_TAG.UnknownDevice,
|
|
195
|
+
ERROR_TAG.DeviceSessionNotFound,
|
|
196
|
+
ERROR_TAG.DeviceSessionRefresher,
|
|
197
|
+
ERROR_TAG.DeviceNotInitialized,
|
|
198
|
+
ERROR_TAG.OpeningConnection,
|
|
199
|
+
ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
|
|
200
|
+
ERROR_TAG.DeviceDisconnectedWhileSending,
|
|
201
|
+
ERROR_TAG.Disconnect,
|
|
202
|
+
ERROR_TAG.ReconnectionFailed,
|
|
203
|
+
ERROR_TAG.WebHIDDisconnect
|
|
204
|
+
]);
|
|
205
|
+
var DEVICE_NOT_FOUND_TAGS = /* @__PURE__ */ new Set([
|
|
206
|
+
ERROR_TAG.NoAccessibleDevice,
|
|
207
|
+
ERROR_TAG.UnknownDevice,
|
|
208
|
+
ERROR_TAG.DeviceNotInitialized,
|
|
209
|
+
// SDK-internal: dm.connect() called before _discovered was populated.
|
|
210
|
+
// Map to DeviceNotFound so non-BLE-direct paths get a sensible error code.
|
|
211
|
+
ERROR_TAG.DeviceNotInDiscoveryCache
|
|
212
|
+
]);
|
|
213
|
+
var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OpeningConnection]);
|
|
214
|
+
var DEVICE_DISCONNECTED_TAGS = /* @__PURE__ */ new Set([
|
|
215
|
+
ERROR_TAG.DeviceNotRecognized,
|
|
216
|
+
ERROR_TAG.DeviceSessionNotFound,
|
|
217
|
+
ERROR_TAG.DeviceSessionRefresher,
|
|
218
|
+
ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
|
|
219
|
+
ERROR_TAG.DeviceDisconnectedWhileSending,
|
|
220
|
+
ERROR_TAG.Disconnect,
|
|
221
|
+
ERROR_TAG.ReconnectionFailed,
|
|
222
|
+
ERROR_TAG.WebHIDDisconnect
|
|
223
|
+
]);
|
|
224
|
+
function isConnectionLevelError(err) {
|
|
225
|
+
if (!err || typeof err !== "object") return false;
|
|
226
|
+
const tag = err._tag;
|
|
227
|
+
if (tag && CONNECTION_LEVEL_TAGS.has(tag)) return true;
|
|
228
|
+
const e = err;
|
|
229
|
+
if (e.originalError != null && isConnectionLevelError(e.originalError)) return true;
|
|
230
|
+
if (e.error != null && e._tag && isConnectionLevelError(e.error)) return true;
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
function isKnownConnectionTag(tag) {
|
|
234
|
+
return typeof tag === "string" && CONNECTION_LEVEL_TAGS.has(tag);
|
|
235
|
+
}
|
|
120
236
|
function hasStatusCode(err, codeSet) {
|
|
121
237
|
if (!err || typeof err !== "object") return false;
|
|
122
238
|
const e = err;
|
|
@@ -126,10 +242,39 @@ function hasStatusCode(err, codeSet) {
|
|
|
126
242
|
if (e.error != null && e._tag && hasStatusCode(e.error, codeSet)) return true;
|
|
127
243
|
return false;
|
|
128
244
|
}
|
|
245
|
+
function hasBlindSignFallbackStep(err) {
|
|
246
|
+
if (!err || typeof err !== "object") return false;
|
|
247
|
+
const e = err;
|
|
248
|
+
if (e._lastStep === STEP_BLIND_SIGN_TRANSACTION_FALLBACK) return true;
|
|
249
|
+
if (Array.isArray(e._deviceActionSteps) && e._deviceActionSteps.includes(STEP_BLIND_SIGN_TRANSACTION_FALLBACK)) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (e.originalError != null && hasBlindSignFallbackStep(e.originalError)) return true;
|
|
253
|
+
if (e.error != null && e._tag && hasBlindSignFallbackStep(e.error)) return true;
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
function isDeviceNotFoundError(err) {
|
|
257
|
+
if (!err || typeof err !== "object") return false;
|
|
258
|
+
const tag = err._tag;
|
|
259
|
+
if (tag && DEVICE_NOT_FOUND_TAGS.has(tag)) return true;
|
|
260
|
+
const e = err;
|
|
261
|
+
if (e.originalError != null && isDeviceNotFoundError(e.originalError)) return true;
|
|
262
|
+
if (e.error != null && e._tag && isDeviceNotFoundError(e.error)) return true;
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
function isDeviceBusyError(err) {
|
|
266
|
+
if (!err || typeof err !== "object") return false;
|
|
267
|
+
const tag = err._tag;
|
|
268
|
+
if (tag && DEVICE_BUSY_TAGS.has(tag)) return true;
|
|
269
|
+
const e = err;
|
|
270
|
+
if (e.originalError != null && isDeviceBusyError(e.originalError)) return true;
|
|
271
|
+
if (e.error != null && e._tag && isDeviceBusyError(e.error)) return true;
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
129
274
|
function isUserRejectedError(err) {
|
|
130
275
|
if (!err || typeof err !== "object") return false;
|
|
131
276
|
const e = err;
|
|
132
|
-
if (e._tag ===
|
|
277
|
+
if (e._tag === ERROR_TAG.UserRefusedOnDevice) return true;
|
|
133
278
|
if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
|
|
134
279
|
if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
|
|
135
280
|
return false;
|
|
@@ -137,7 +282,7 @@ function isUserRejectedError(err) {
|
|
|
137
282
|
function isWrongAppError(err) {
|
|
138
283
|
if (!err || typeof err !== "object") return false;
|
|
139
284
|
const e = err;
|
|
140
|
-
if (e._tag ===
|
|
285
|
+
if (e._tag === ERROR_TAG.WrongAppOpened || e._tag === ERROR_TAG.InvalidStatusWord) {
|
|
141
286
|
if (hasStatusCode(err, WRONG_APP_CODES)) return true;
|
|
142
287
|
}
|
|
143
288
|
if (typeof e.message === "string") {
|
|
@@ -151,7 +296,6 @@ function isWrongAppError(err) {
|
|
|
151
296
|
function isAppNotInstalledError(err) {
|
|
152
297
|
if (!err || typeof err !== "object") return false;
|
|
153
298
|
const e = err;
|
|
154
|
-
if (e._tag === "OpenAppCommandError") return true;
|
|
155
299
|
if (typeof e.message === "string" && /unknown application/i.test(e.message)) return true;
|
|
156
300
|
if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
|
|
157
301
|
return false;
|
|
@@ -159,7 +303,8 @@ function isAppNotInstalledError(err) {
|
|
|
159
303
|
function isDeviceDisconnectedError(err) {
|
|
160
304
|
if (!err || typeof err !== "object") return false;
|
|
161
305
|
const e = err;
|
|
162
|
-
|
|
306
|
+
const tag = e._tag;
|
|
307
|
+
if (typeof tag === "string" && DEVICE_DISCONNECTED_TAGS.has(tag)) return true;
|
|
163
308
|
if (typeof e.message === "string") {
|
|
164
309
|
const msg = e.message.toLowerCase();
|
|
165
310
|
if (msg.includes("disconnected") || msg.includes("not found") || msg.includes("no device") || msg.includes("unplugged"))
|
|
@@ -179,7 +324,22 @@ function isTimeoutError(err) {
|
|
|
179
324
|
if (e.code === HardwareErrorCode.OperationTimeout) return true;
|
|
180
325
|
return false;
|
|
181
326
|
}
|
|
182
|
-
function
|
|
327
|
+
function isAppStuckByApdu(err) {
|
|
328
|
+
if (!err || typeof err !== "object") return false;
|
|
329
|
+
const tag = err._tag;
|
|
330
|
+
if (tag === ERROR_TAG.DeviceAppStuck) return true;
|
|
331
|
+
return extractApduHex(err) === "6901";
|
|
332
|
+
}
|
|
333
|
+
function isTransportStuck(err) {
|
|
334
|
+
if (!err || typeof err !== "object") return false;
|
|
335
|
+
const tag = err._tag;
|
|
336
|
+
return tag === ERROR_TAG.DeviceTransportStuck || // already wrapped form
|
|
337
|
+
tag === ERROR_TAG.UnknownDeviceExchange || tag === ERROR_TAG.AlreadySendingApdu;
|
|
338
|
+
}
|
|
339
|
+
function isStuckAppStateError(err) {
|
|
340
|
+
return isAppStuckByApdu(err) || isTransportStuck(err);
|
|
341
|
+
}
|
|
342
|
+
function mapLedgerError(err, opts) {
|
|
183
343
|
let originalMessage = "Unknown Ledger error";
|
|
184
344
|
if (err instanceof Error) {
|
|
185
345
|
originalMessage = err.message;
|
|
@@ -190,60 +350,127 @@ function mapLedgerError(err) {
|
|
|
190
350
|
let code;
|
|
191
351
|
if (isDeviceLockedError(err)) {
|
|
192
352
|
code = HardwareErrorCode.DeviceLocked;
|
|
353
|
+
} else if (isDeviceNotAdvertisingError(err) || isDeviceNotFoundError(err)) {
|
|
354
|
+
code = HardwareErrorCode.DeviceNotFound;
|
|
355
|
+
} else if (isDeviceBusyError(err)) {
|
|
356
|
+
code = HardwareErrorCode.DeviceBusy;
|
|
357
|
+
} else if (isBlePairingFailureError(err)) {
|
|
358
|
+
code = HardwareErrorCode.BlePairingTimeout;
|
|
193
359
|
} else if (isUserRejectedError(err)) {
|
|
194
360
|
code = HardwareErrorCode.UserRejected;
|
|
195
361
|
} else if (isWrongAppError(err)) {
|
|
196
362
|
code = HardwareErrorCode.WrongApp;
|
|
197
363
|
} else if (isAppNotInstalledError(err)) {
|
|
198
|
-
code = HardwareErrorCode.
|
|
364
|
+
code = HardwareErrorCode.AppNotInstalled;
|
|
199
365
|
} else if (isDeviceDisconnectedError(err)) {
|
|
200
366
|
code = HardwareErrorCode.DeviceDisconnected;
|
|
201
367
|
} else if (isTimeoutError(err)) {
|
|
202
368
|
code = HardwareErrorCode.OperationTimeout;
|
|
203
369
|
} else {
|
|
204
|
-
const
|
|
205
|
-
const lastStep = err && typeof err === "object" ? err._lastStep : void 0;
|
|
206
|
-
const ethMapped = ethCode ? mapEthAppError(ethCode, lastStep) : null;
|
|
370
|
+
const ethMapped = classifyEthAppError(err);
|
|
207
371
|
const apduHex = ethMapped ? null : extractApduHex(err);
|
|
208
372
|
const chainMapped = apduHex ? mapSolanaAppError(apduHex) ?? mapTronAppError(apduHex) ?? mapBtcAppError(apduHex) : null;
|
|
209
373
|
code = ethMapped ?? chainMapped ?? HardwareErrorCode.UnknownError;
|
|
210
374
|
}
|
|
211
|
-
const
|
|
375
|
+
const errAppName = err && typeof err === "object" ? err.appName : void 0;
|
|
376
|
+
const appName = errAppName ?? opts?.defaultAppName;
|
|
212
377
|
return { code, message: enrichErrorMessage(code, originalMessage), appName };
|
|
213
378
|
}
|
|
214
379
|
|
|
215
|
-
// src/utils/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
380
|
+
// src/utils/ledgerDmkTransport.ts
|
|
381
|
+
function isLedgerDmkBleTransport(transport) {
|
|
382
|
+
const normalized = transport?.toUpperCase();
|
|
383
|
+
return normalized === "BLE" || normalized?.endsWith("_BLE") === true;
|
|
219
384
|
}
|
|
220
|
-
function
|
|
221
|
-
return
|
|
385
|
+
function isLedgerBleConnectionType(connectionType) {
|
|
386
|
+
return connectionType === "ble";
|
|
222
387
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
388
|
+
function isLedgerBleDescriptor(connectionType, descriptor) {
|
|
389
|
+
return isLedgerBleConnectionType(connectionType) || isLedgerDmkBleTransport(descriptor.transport);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/utils/sdkEventBus.ts
|
|
393
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
394
|
+
function onSdkEvent(listener) {
|
|
395
|
+
listeners.add(listener);
|
|
396
|
+
return () => {
|
|
397
|
+
listeners.delete(listener);
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function offSdkEvent(listener) {
|
|
401
|
+
listeners.delete(listener);
|
|
402
|
+
}
|
|
403
|
+
function emitSdkEvent(event) {
|
|
404
|
+
if (listeners.size === 0) return;
|
|
405
|
+
for (const listener of listeners) {
|
|
406
|
+
try {
|
|
407
|
+
listener(event);
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
226
410
|
}
|
|
227
411
|
}
|
|
228
|
-
function
|
|
229
|
-
if (
|
|
230
|
-
|
|
412
|
+
function emitLog(level, ...args) {
|
|
413
|
+
if (listeners.size === 0) return;
|
|
414
|
+
const message = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
|
|
415
|
+
emitSdkEvent({ type: "log", level, message });
|
|
416
|
+
}
|
|
417
|
+
function safeStringify(value) {
|
|
418
|
+
if (value instanceof Error) {
|
|
419
|
+
return value.stack ? `${value.message}
|
|
420
|
+
${value.stack}` : value.message;
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
return JSON.stringify(value);
|
|
424
|
+
} catch {
|
|
425
|
+
return String(value);
|
|
231
426
|
}
|
|
232
427
|
}
|
|
233
428
|
|
|
429
|
+
// src/utils/debugLog.ts
|
|
430
|
+
function debugLog(...args) {
|
|
431
|
+
emitLog("debug", ...args);
|
|
432
|
+
}
|
|
433
|
+
function debugError(...args) {
|
|
434
|
+
emitLog("error", ...args);
|
|
435
|
+
}
|
|
436
|
+
|
|
234
437
|
// src/adapter/LedgerAdapter.ts
|
|
235
438
|
function formatDeviceMismatchError(expected, actual) {
|
|
236
439
|
return `Wrong device: expected ${expected}, got ${actual}`;
|
|
237
440
|
}
|
|
441
|
+
var BTC_HIGH_INDEX_THRESHOLD = 100;
|
|
442
|
+
function btcAccountIndexFromPath(path) {
|
|
443
|
+
const segments = path.replace(/^m\//, "").split("/");
|
|
444
|
+
if (segments.length < 3) return null;
|
|
445
|
+
const accountSeg = segments[2].replace(/['h]$/i, "");
|
|
446
|
+
const accountIndex = parseInt(accountSeg, 10);
|
|
447
|
+
return Number.isFinite(accountIndex) ? accountIndex : null;
|
|
448
|
+
}
|
|
238
449
|
var _LedgerAdapter = class _LedgerAdapter {
|
|
239
450
|
constructor(connector, options) {
|
|
240
451
|
this.vendor = "ledger";
|
|
241
452
|
this.emitter = new TypedEventEmitter();
|
|
242
|
-
// Device cache: tracks discovered devices from connector events
|
|
243
453
|
this._discoveredDevices = /* @__PURE__ */ new Map();
|
|
244
|
-
// Session tracking: maps connectId -> sessionId
|
|
245
454
|
this._sessions = /* @__PURE__ */ new Map();
|
|
246
455
|
this._uiRegistry = new UiRequestRegistry();
|
|
456
|
+
// BTC App rejects account index >= 100 unless display=true. Cached per
|
|
457
|
+
// adapter instance: first 100+ path asks the user once via UI request,
|
|
458
|
+
// subsequent 100+ paths in the same session auto-promote silently.
|
|
459
|
+
// The Ledger device itself still requires a per-call confirmation — that's
|
|
460
|
+
// the Ledger app's safety boundary, not ours to bypass.
|
|
461
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
462
|
+
// Shared across concurrent callers — only `cancel()` aborts.
|
|
463
|
+
this._doConnectAbortController = null;
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// Private helpers
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
/**
|
|
468
|
+
* Ensure at least one device is connected and return a valid connectId.
|
|
469
|
+
*
|
|
470
|
+
* - If a session already exists for the given connectId, reuse it.
|
|
471
|
+
* - If ANY session exists (Ledger IDs are ephemeral), reuse it.
|
|
472
|
+
* - Otherwise: search → 1 device: auto-connect, multiple: ask user, 0: throw.
|
|
473
|
+
*/
|
|
247
474
|
// Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
|
|
248
475
|
this._connectingPromise = null;
|
|
249
476
|
// ---------------------------------------------------------------------------
|
|
@@ -266,47 +493,31 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
266
493
|
payload: { connectId: data.connectId }
|
|
267
494
|
});
|
|
268
495
|
};
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this.
|
|
273
|
-
this.
|
|
496
|
+
// Forward low-level connector 'ui-event' (the four EConnectorInteraction values)
|
|
497
|
+
// to the public hw.emitter so consumers only need to subscribe in one place
|
|
498
|
+
// (hw.on instead of also reaching into connector.on).
|
|
499
|
+
this.uiEventForwarder = (event) => {
|
|
500
|
+
this.emitter.emit("ui-event", event);
|
|
274
501
|
};
|
|
275
502
|
this.connector = connector;
|
|
276
503
|
this._handleSelectDevice = options?.handleSelectDevice ?? false;
|
|
277
|
-
this._jobQueue = new DeviceJobQueue(
|
|
278
|
-
emit: (event, data) => this.emitter.emit(event, data),
|
|
279
|
-
uiRegistry: this._uiRegistry
|
|
280
|
-
});
|
|
504
|
+
this._jobQueue = new DeviceJobQueue();
|
|
281
505
|
this.registerEventListeners();
|
|
282
506
|
}
|
|
283
|
-
/**
|
|
284
|
-
* Classify a method's interruptibility.
|
|
285
|
-
* - Signing / typed data / transaction → 'confirm' (user may decide via preemption UI)
|
|
286
|
-
* - Read-only queries (getAddress / getPublicKey / getMasterFingerprint) → 'safe'
|
|
287
|
-
* (auto-cancels any pending read for the same device)
|
|
288
|
-
*/
|
|
289
|
-
static _getInterruptibility(method) {
|
|
290
|
-
if (method.toLowerCase().includes("sign")) return "confirm";
|
|
291
|
-
return "safe";
|
|
292
|
-
}
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
507
|
// Transport
|
|
295
|
-
// ---------------------------------------------------------------------------
|
|
296
|
-
// Transport is decided at connector creation time. These methods
|
|
297
|
-
// satisfy the IHardwareWallet interface with sensible defaults.
|
|
298
508
|
get activeTransport() {
|
|
299
|
-
return this.connector.connectionType
|
|
509
|
+
return isLedgerBleConnectionType(this.connector.connectionType) ? "ble" : "hid";
|
|
300
510
|
}
|
|
301
511
|
getAvailableTransports() {
|
|
302
512
|
return this.activeTransport ? [this.activeTransport] : [];
|
|
303
513
|
}
|
|
304
|
-
|
|
514
|
+
// Connector is bound at construction; switching requires a new adapter.
|
|
515
|
+
switchTransport(_type) {
|
|
516
|
+
return Promise.resolve();
|
|
305
517
|
}
|
|
306
|
-
// ---------------------------------------------------------------------------
|
|
307
518
|
// Lifecycle
|
|
308
|
-
|
|
309
|
-
|
|
519
|
+
init(_config) {
|
|
520
|
+
return Promise.resolve();
|
|
310
521
|
}
|
|
311
522
|
/**
|
|
312
523
|
* Clear cached device/session state without tearing down the adapter.
|
|
@@ -319,6 +530,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
319
530
|
this._connectingPromise = null;
|
|
320
531
|
this._uiRegistry.reset();
|
|
321
532
|
this._jobQueue.clear();
|
|
533
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
322
534
|
}
|
|
323
535
|
async dispose() {
|
|
324
536
|
this._uiRegistry.reset();
|
|
@@ -327,6 +539,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
327
539
|
this.connector.reset();
|
|
328
540
|
this._discoveredDevices.clear();
|
|
329
541
|
this._sessions.clear();
|
|
542
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
330
543
|
this.emitter.removeAllListeners();
|
|
331
544
|
}
|
|
332
545
|
uiResponse(response) {
|
|
@@ -335,10 +548,17 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
335
548
|
// ---------------------------------------------------------------------------
|
|
336
549
|
// Device management
|
|
337
550
|
// ---------------------------------------------------------------------------
|
|
338
|
-
async searchDevices() {
|
|
551
|
+
async searchDevices(options) {
|
|
552
|
+
if (options?.resetSession) {
|
|
553
|
+
this._doConnectAbortController?.abort();
|
|
554
|
+
this._sessions.clear();
|
|
555
|
+
this._connectingPromise = null;
|
|
556
|
+
this._doConnectAbortController = null;
|
|
557
|
+
this._btcHighIndexConfirmedThisSession = false;
|
|
558
|
+
}
|
|
339
559
|
await this._ensureDevicePermission();
|
|
340
560
|
const devices = await this.connector.searchDevices();
|
|
341
|
-
|
|
561
|
+
this._discoveredDevices.clear();
|
|
342
562
|
for (const d of devices) {
|
|
343
563
|
if (d.connectId) {
|
|
344
564
|
this._discoveredDevices.set(d.connectId, this.connectorDeviceToDeviceInfo(d));
|
|
@@ -347,11 +567,26 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
347
567
|
if (this._discoveredDevices.size === 0) {
|
|
348
568
|
await this._ensureDevicePermission();
|
|
349
569
|
}
|
|
570
|
+
debugLog(
|
|
571
|
+
`[LedgerAdapter] searchDevices() return count=${this._discoveredDevices.size} ids=[${[
|
|
572
|
+
...this._discoveredDevices.keys()
|
|
573
|
+
].join(",")}]`
|
|
574
|
+
);
|
|
350
575
|
return Array.from(this._discoveredDevices.values());
|
|
351
576
|
}
|
|
577
|
+
static _createDeviceBusyError(method) {
|
|
578
|
+
return Object.assign(new Error(`Ledger device is busy while calling ${method}`), {
|
|
579
|
+
code: HardwareErrorCode2.DeviceBusy
|
|
580
|
+
});
|
|
581
|
+
}
|
|
352
582
|
async connectDevice(connectId) {
|
|
353
|
-
await this._ensureDevicePermission(connectId);
|
|
354
583
|
try {
|
|
584
|
+
if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
|
|
585
|
+
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
586
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
await this._ensureDevicePermission(connectId);
|
|
355
590
|
const session = await this.connector.connect(connectId);
|
|
356
591
|
this._sessions.set(connectId, session.sessionId);
|
|
357
592
|
if (session.deviceInfo) {
|
|
@@ -387,7 +622,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
387
622
|
// Chain call helper
|
|
388
623
|
// ---------------------------------------------------------------------------
|
|
389
624
|
async callChain(connectId, deviceId, chain, method, params, skipFingerprint = false) {
|
|
390
|
-
await this._ensureDevicePermission(connectId, deviceId);
|
|
391
625
|
try {
|
|
392
626
|
const result = await this.connectorCall(connectId, method, params, {
|
|
393
627
|
chain,
|
|
@@ -399,45 +633,12 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
399
633
|
return this.errorToFailure(err);
|
|
400
634
|
}
|
|
401
635
|
}
|
|
402
|
-
/**
|
|
403
|
-
* Batch version of callChain — checks permission once,
|
|
404
|
-
* fingerprint is verified on the first call inside connectorCall.
|
|
405
|
-
*/
|
|
406
|
-
async callChainBatch(connectId, deviceId, chain, method, params, onProgress, skipFingerprint = false) {
|
|
407
|
-
await this._ensureDevicePermission(connectId, deviceId);
|
|
408
|
-
const results = [];
|
|
409
|
-
for (let i = 0; i < params.length; i++) {
|
|
410
|
-
try {
|
|
411
|
-
const result = await this.connectorCall(connectId, method, params[i], {
|
|
412
|
-
chain,
|
|
413
|
-
deviceId,
|
|
414
|
-
// Only verify fingerprint on the first call in the batch
|
|
415
|
-
skipFingerprint: skipFingerprint || i > 0
|
|
416
|
-
});
|
|
417
|
-
results.push(result);
|
|
418
|
-
onProgress?.({ index: i, total: params.length });
|
|
419
|
-
} catch (err) {
|
|
420
|
-
return this.errorToFailure(err);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return success(results);
|
|
424
|
-
}
|
|
425
636
|
// ---------------------------------------------------------------------------
|
|
426
637
|
// EVM chain methods
|
|
427
638
|
// ---------------------------------------------------------------------------
|
|
428
639
|
evmGetAddress(connectId, deviceId, params) {
|
|
429
640
|
return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
|
|
430
641
|
}
|
|
431
|
-
evmGetAddresses(connectId, deviceId, params, onProgress) {
|
|
432
|
-
return this.callChainBatch(
|
|
433
|
-
connectId,
|
|
434
|
-
deviceId,
|
|
435
|
-
"evm",
|
|
436
|
-
"evmGetAddress",
|
|
437
|
-
params,
|
|
438
|
-
onProgress
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
642
|
evmSignTransaction(connectId, deviceId, params) {
|
|
442
643
|
return this.callChain(connectId, deviceId, "evm", "evmSignTransaction", params);
|
|
443
644
|
}
|
|
@@ -453,16 +654,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
453
654
|
btcGetAddress(connectId, deviceId, params) {
|
|
454
655
|
return this.callChain(connectId, deviceId, "btc", "btcGetAddress", params);
|
|
455
656
|
}
|
|
456
|
-
btcGetAddresses(connectId, deviceId, params, onProgress) {
|
|
457
|
-
return this.callChainBatch(
|
|
458
|
-
connectId,
|
|
459
|
-
deviceId,
|
|
460
|
-
"btc",
|
|
461
|
-
"btcGetAddress",
|
|
462
|
-
params,
|
|
463
|
-
onProgress
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
657
|
btcGetPublicKey(connectId, deviceId, params) {
|
|
467
658
|
return this.callChain(connectId, deviceId, "btc", "btcGetPublicKey", params);
|
|
468
659
|
}
|
|
@@ -490,16 +681,6 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
490
681
|
solGetAddress(connectId, deviceId, params) {
|
|
491
682
|
return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
|
|
492
683
|
}
|
|
493
|
-
solGetAddresses(connectId, deviceId, params, onProgress) {
|
|
494
|
-
return this.callChainBatch(
|
|
495
|
-
connectId,
|
|
496
|
-
deviceId,
|
|
497
|
-
"sol",
|
|
498
|
-
"solGetAddress",
|
|
499
|
-
params,
|
|
500
|
-
onProgress
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
684
|
solSignTransaction(connectId, deviceId, params) {
|
|
504
685
|
return this.callChain(connectId, deviceId, "sol", "solSignTransaction", params);
|
|
505
686
|
}
|
|
@@ -510,38 +691,13 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
510
691
|
// TRON chain methods
|
|
511
692
|
// ---------------------------------------------------------------------------
|
|
512
693
|
tronGetAddress(connectId, deviceId, params) {
|
|
513
|
-
return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params
|
|
514
|
-
}
|
|
515
|
-
tronGetAddresses(connectId, deviceId, params, onProgress) {
|
|
516
|
-
return this.callChainBatch(
|
|
517
|
-
connectId,
|
|
518
|
-
deviceId,
|
|
519
|
-
"tron",
|
|
520
|
-
"tronGetAddress",
|
|
521
|
-
params,
|
|
522
|
-
onProgress,
|
|
523
|
-
true
|
|
524
|
-
);
|
|
694
|
+
return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params);
|
|
525
695
|
}
|
|
526
696
|
tronSignTransaction(connectId, deviceId, params) {
|
|
527
|
-
return this.callChain(
|
|
528
|
-
connectId,
|
|
529
|
-
deviceId,
|
|
530
|
-
"tron",
|
|
531
|
-
"tronSignTransaction",
|
|
532
|
-
params,
|
|
533
|
-
true
|
|
534
|
-
);
|
|
697
|
+
return this.callChain(connectId, deviceId, "tron", "tronSignTransaction", params);
|
|
535
698
|
}
|
|
536
699
|
tronSignMessage(connectId, deviceId, params) {
|
|
537
|
-
return this.callChain(
|
|
538
|
-
connectId,
|
|
539
|
-
deviceId,
|
|
540
|
-
"tron",
|
|
541
|
-
"tronSignMessage",
|
|
542
|
-
params,
|
|
543
|
-
true
|
|
544
|
-
);
|
|
700
|
+
return this.callChain(connectId, deviceId, "tron", "tronSignMessage", params);
|
|
545
701
|
}
|
|
546
702
|
on(event, listener) {
|
|
547
703
|
this.emitter.on(event, listener);
|
|
@@ -550,30 +706,41 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
550
706
|
this.emitter.off(event, listener);
|
|
551
707
|
}
|
|
552
708
|
cancel(connectId) {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
709
|
+
const userAbortReason = Object.assign(new Error("User aborted operation"), {
|
|
710
|
+
code: HardwareErrorCode2.UserAborted,
|
|
711
|
+
_tag: ERROR_TAG.UserAborted
|
|
712
|
+
});
|
|
713
|
+
this._lastCancelReason = userAbortReason;
|
|
714
|
+
setTimeout(() => {
|
|
715
|
+
if (this._lastCancelReason === userAbortReason) {
|
|
716
|
+
this._lastCancelReason = void 0;
|
|
717
|
+
}
|
|
718
|
+
}, 2e3);
|
|
719
|
+
this._uiRegistry.cancel();
|
|
720
|
+
if (connectId) {
|
|
721
|
+
this._jobQueue.cancelActiveAndPending(connectId, userAbortReason);
|
|
722
|
+
} else {
|
|
723
|
+
this._jobQueue.cancelActiveAndPending(void 0, userAbortReason);
|
|
724
|
+
}
|
|
725
|
+
if (connectId) {
|
|
726
|
+
const sessionId = this._sessions.get(connectId) ?? connectId;
|
|
727
|
+
void this.connector.cancel(sessionId);
|
|
728
|
+
} else {
|
|
729
|
+
for (const sid of this._sessions.values()) void this.connector.cancel(sid);
|
|
730
|
+
}
|
|
731
|
+
if (this._connectingPromise) {
|
|
732
|
+
this._doConnectAbortController?.abort(userAbortReason);
|
|
733
|
+
}
|
|
556
734
|
}
|
|
557
735
|
// ---------------------------------------------------------------------------
|
|
558
736
|
// Chain fingerprint
|
|
559
737
|
// ---------------------------------------------------------------------------
|
|
560
738
|
async getChainFingerprint(connectId, deviceId, chain) {
|
|
561
|
-
debugLog(
|
|
562
|
-
"[LedgerAdapter] getChainFingerprint called, chain:",
|
|
563
|
-
chain,
|
|
564
|
-
"connectId:",
|
|
565
|
-
connectId || "(empty)",
|
|
566
|
-
"sessions:",
|
|
567
|
-
this._sessions.size
|
|
568
|
-
);
|
|
569
|
-
await this._ensureDevicePermission(connectId, deviceId);
|
|
570
|
-
debugLog("[LedgerAdapter] getChainFingerprint permission ok, computing fingerprint");
|
|
571
739
|
try {
|
|
572
740
|
const fingerprint = await this._computeChainFingerprint(
|
|
573
741
|
chain,
|
|
574
|
-
(method, params) => this.connectorCall(connectId, method, params)
|
|
742
|
+
(method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
|
|
575
743
|
);
|
|
576
|
-
debugLog("[LedgerAdapter] getChainFingerprint result:", fingerprint?.substring(0, 20));
|
|
577
744
|
return success(fingerprint);
|
|
578
745
|
} catch (err) {
|
|
579
746
|
debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
|
|
@@ -644,77 +811,226 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
644
811
|
if (signal?.aborted) {
|
|
645
812
|
_LedgerAdapter._throwIfAborted(signal);
|
|
646
813
|
}
|
|
814
|
+
const waitPromise = this._uiRegistry.wait(
|
|
815
|
+
UI_REQUEST.REQUEST_DEVICE_CONNECT
|
|
816
|
+
);
|
|
647
817
|
this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_CONNECT, {
|
|
648
818
|
type: UI_REQUEST.REQUEST_DEVICE_CONNECT,
|
|
649
819
|
payload: {
|
|
820
|
+
vendor: "ledger",
|
|
821
|
+
reason: "device-not-found",
|
|
650
822
|
message: "Please connect and unlock your Ledger device"
|
|
651
823
|
}
|
|
652
824
|
});
|
|
653
|
-
const waitPromise = this._uiRegistry.wait(
|
|
654
|
-
UI_REQUEST.REQUEST_DEVICE_CONNECT
|
|
655
|
-
);
|
|
656
825
|
let payload;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
826
|
+
try {
|
|
827
|
+
if (signal) {
|
|
828
|
+
const onAbort = () => {
|
|
829
|
+
this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_CONNECT);
|
|
830
|
+
};
|
|
831
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
832
|
+
try {
|
|
833
|
+
payload = await waitPromise;
|
|
834
|
+
} finally {
|
|
835
|
+
signal.removeEventListener("abort", onAbort);
|
|
836
|
+
}
|
|
837
|
+
} else {
|
|
663
838
|
payload = await waitPromise;
|
|
664
|
-
} finally {
|
|
665
|
-
signal.removeEventListener("abort", onAbort);
|
|
666
839
|
}
|
|
667
|
-
}
|
|
668
|
-
|
|
840
|
+
} catch (err) {
|
|
841
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
842
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
843
|
+
payload: {}
|
|
844
|
+
});
|
|
845
|
+
throw Object.assign(new Error("User cancelled Ledger connection"), {
|
|
846
|
+
_tag: ERROR_TAG.UserAborted,
|
|
847
|
+
code: HardwareErrorCode2.UserAborted,
|
|
848
|
+
cause: err
|
|
849
|
+
});
|
|
669
850
|
}
|
|
670
851
|
if (!payload?.confirmed) {
|
|
671
852
|
throw Object.assign(new Error("User cancelled Ledger connection"), {
|
|
672
|
-
_tag:
|
|
853
|
+
_tag: ERROR_TAG.UserAborted,
|
|
854
|
+
code: HardwareErrorCode2.UserAborted
|
|
673
855
|
});
|
|
674
856
|
}
|
|
857
|
+
await new Promise((resolve) => {
|
|
858
|
+
setTimeout(resolve, 800);
|
|
859
|
+
});
|
|
675
860
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
861
|
+
/**
|
|
862
|
+
* Decide whether a BTC pubkey call needs `showOnDevice=true` because of
|
|
863
|
+
* the BTC App's account-index policy, asking the user once per session.
|
|
864
|
+
*
|
|
865
|
+
* Returns the params to pass through (with `showOnDevice` possibly
|
|
866
|
+
* promoted), or `null` if the user declined.
|
|
867
|
+
*/
|
|
868
|
+
async _gateBtcHighIndex(params) {
|
|
869
|
+
const accountIndex = btcAccountIndexFromPath(params.path);
|
|
870
|
+
if (params.showOnDevice) return params;
|
|
871
|
+
if (accountIndex === null || accountIndex < BTC_HIGH_INDEX_THRESHOLD) {
|
|
872
|
+
return params;
|
|
683
873
|
}
|
|
684
|
-
if (this.
|
|
685
|
-
return
|
|
874
|
+
if (this._btcHighIndexConfirmedThisSession) {
|
|
875
|
+
return { ...params, showOnDevice: true };
|
|
686
876
|
}
|
|
687
|
-
|
|
877
|
+
const confirmed = await this._waitForBtcHighIndexConfirm(params.path, accountIndex);
|
|
878
|
+
if (!confirmed) return null;
|
|
879
|
+
this._btcHighIndexConfirmedThisSession = true;
|
|
880
|
+
return { ...params, showOnDevice: true };
|
|
881
|
+
}
|
|
882
|
+
async _waitForBtcHighIndexConfirm(path, accountIndex) {
|
|
883
|
+
const waitPromise = this._uiRegistry.wait(
|
|
884
|
+
UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM
|
|
885
|
+
);
|
|
886
|
+
this.emitter.emit(UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM, {
|
|
887
|
+
type: UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
|
|
888
|
+
payload: {
|
|
889
|
+
vendor: "ledger",
|
|
890
|
+
path,
|
|
891
|
+
accountIndex
|
|
892
|
+
}
|
|
893
|
+
});
|
|
688
894
|
try {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
895
|
+
const payload = await waitPromise;
|
|
896
|
+
return !!payload?.confirmed;
|
|
897
|
+
} catch (err) {
|
|
898
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
899
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
900
|
+
payload: {}
|
|
901
|
+
});
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
// Layer 1 entry. Caller signal only races the outer awaiter; the shared
|
|
906
|
+
// `_doConnect` runs under its own internal controller so caller A's cancel
|
|
907
|
+
// doesn't kill caller B's await.
|
|
908
|
+
async ensureConnected(connectId, signal) {
|
|
909
|
+
if (signal.aborted) _LedgerAdapter._throwIfAborted(signal);
|
|
910
|
+
if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
|
|
911
|
+
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
912
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
if (connectId && this._sessions.has(connectId)) return connectId;
|
|
916
|
+
if (this._sessions.size > 0) {
|
|
917
|
+
return this._sessions.keys().next().value;
|
|
918
|
+
}
|
|
919
|
+
if (!this._connectingPromise) {
|
|
920
|
+
this._doConnectAbortController = new AbortController();
|
|
921
|
+
const innerSignal = this._doConnectAbortController.signal;
|
|
922
|
+
this._connectingPromise = (async () => {
|
|
923
|
+
try {
|
|
924
|
+
return await this._doConnect(innerSignal, connectId);
|
|
925
|
+
} finally {
|
|
926
|
+
this._connectingPromise = null;
|
|
927
|
+
this._doConnectAbortController = null;
|
|
928
|
+
}
|
|
929
|
+
})();
|
|
692
930
|
}
|
|
931
|
+
return this._abortable(signal, this._connectingPromise);
|
|
693
932
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
933
|
+
// Layer 1 main loop — the ONLY place in SDK that emits unlock dialog.
|
|
934
|
+
// Bounded by MAX_DOCONNECT_CONFIRMS — after N Confirms with no progress,
|
|
935
|
+
// throw DeviceNotFound so the user is kicked out of the loop.
|
|
936
|
+
async _doConnect(internalSignal, targetConnectId) {
|
|
937
|
+
if (isLedgerBleConnectionType(this.connector.connectionType) && targetConnectId) {
|
|
938
|
+
try {
|
|
939
|
+
return await this._connectDeviceOrThrow(targetConnectId);
|
|
940
|
+
} catch (err) {
|
|
941
|
+
if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
|
|
942
|
+
throw err;
|
|
943
|
+
}
|
|
944
|
+
this._discoveredDevices.delete(targetConnectId);
|
|
945
|
+
if (isDeviceDisconnectedError(err)) {
|
|
946
|
+
try {
|
|
947
|
+
this.connector.reset?.();
|
|
948
|
+
} catch {
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
let confirms = 0;
|
|
954
|
+
while (!internalSignal.aborted) {
|
|
955
|
+
this.emitter.emit("ui-event", {
|
|
956
|
+
type: EConnectorInteraction.Searching,
|
|
957
|
+
payload: { sessionId: "" }
|
|
958
|
+
});
|
|
959
|
+
let devices = await this.searchDevices();
|
|
960
|
+
if (devices.length === 0) {
|
|
961
|
+
for (let i = 0; i < 3 && !internalSignal.aborted; i += 1) {
|
|
962
|
+
await new Promise((resolve) => {
|
|
963
|
+
setTimeout(resolve, 1e3);
|
|
964
|
+
});
|
|
965
|
+
devices = await this.searchDevices();
|
|
966
|
+
if (devices.length > 0) break;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
697
969
|
if (devices.length > 0) {
|
|
698
|
-
|
|
970
|
+
try {
|
|
971
|
+
return await this._connectFirstOrSelect(devices, targetConnectId);
|
|
972
|
+
} catch (err) {
|
|
973
|
+
if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
|
|
974
|
+
throw err;
|
|
975
|
+
}
|
|
976
|
+
this._discoveredDevices.clear();
|
|
977
|
+
if (isDeviceDisconnectedError(err)) {
|
|
978
|
+
try {
|
|
979
|
+
this.connector.reset?.();
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
699
984
|
}
|
|
700
|
-
if (
|
|
701
|
-
|
|
985
|
+
if (confirms >= _LedgerAdapter.MAX_DOCONNECT_CONFIRMS) {
|
|
986
|
+
throw Object.assign(
|
|
987
|
+
new Error(
|
|
988
|
+
"Device not connected after multiple attempts. Please ensure your Ledger is awake, unlocked, and in range, then try again."
|
|
989
|
+
),
|
|
990
|
+
{ code: HardwareErrorCode2.DeviceNotFound }
|
|
991
|
+
);
|
|
702
992
|
}
|
|
993
|
+
await this._waitForDeviceConnect(internalSignal);
|
|
994
|
+
confirms += 1;
|
|
703
995
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
"No Ledger device found after multiple attempts. Please connect and unlock your device."
|
|
707
|
-
),
|
|
708
|
-
{ _tag: "DeviceNotRecognizedError" }
|
|
709
|
-
);
|
|
996
|
+
_LedgerAdapter._throwIfAborted(internalSignal);
|
|
997
|
+
throw new Error("_doConnect aborted");
|
|
710
998
|
}
|
|
711
|
-
async _connectFirstOrSelect(devices) {
|
|
999
|
+
async _connectFirstOrSelect(devices, targetConnectId) {
|
|
1000
|
+
if (targetConnectId) {
|
|
1001
|
+
const target = devices.find(
|
|
1002
|
+
(d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
|
|
1003
|
+
);
|
|
1004
|
+
if (!target) {
|
|
1005
|
+
const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
|
|
1006
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
1007
|
+
});
|
|
1008
|
+
if (isLedgerBleConnectionType(this.connector.connectionType)) {
|
|
1009
|
+
err._tag = ERROR_TAG.DeviceNotAdvertising;
|
|
1010
|
+
}
|
|
1011
|
+
throw err;
|
|
1012
|
+
}
|
|
1013
|
+
return this._connectDeviceOrThrow(target.connectId);
|
|
1014
|
+
}
|
|
1015
|
+
if (isLedgerBleConnectionType(this.connector.connectionType)) {
|
|
1016
|
+
throw Object.assign(new Error("Ledger BLE connectId is required."), {
|
|
1017
|
+
code: HardwareErrorCode2.DeviceNotFound
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
712
1020
|
const chosenConnectId = devices.length === 1 ? devices[0].connectId : await this._chooseDeviceFromList(devices);
|
|
1021
|
+
return this._connectDeviceOrThrow(chosenConnectId);
|
|
1022
|
+
}
|
|
1023
|
+
async _connectDeviceOrThrow(chosenConnectId) {
|
|
713
1024
|
const result = await this.connectDevice(chosenConnectId);
|
|
714
1025
|
if (!result.success) {
|
|
715
|
-
|
|
716
|
-
|
|
1026
|
+
const payload = result.payload;
|
|
1027
|
+
const rethrow = Object.assign(new Error(payload.error), {
|
|
1028
|
+
code: payload.code
|
|
717
1029
|
});
|
|
1030
|
+
if (payload._tag !== void 0) {
|
|
1031
|
+
rethrow._tag = payload._tag;
|
|
1032
|
+
}
|
|
1033
|
+
throw rethrow;
|
|
718
1034
|
}
|
|
719
1035
|
return chosenConnectId;
|
|
720
1036
|
}
|
|
@@ -725,18 +1041,35 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
725
1041
|
);
|
|
726
1042
|
return devices[0].connectId;
|
|
727
1043
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
UI_REQUEST.REQUEST_SELECT_DEVICE
|
|
734
|
-
|
|
1044
|
+
let response;
|
|
1045
|
+
try {
|
|
1046
|
+
const waitPromise = this._uiRegistry.wait(
|
|
1047
|
+
UI_REQUEST.REQUEST_SELECT_DEVICE
|
|
1048
|
+
);
|
|
1049
|
+
this.emitter.emit(UI_REQUEST.REQUEST_SELECT_DEVICE, {
|
|
1050
|
+
type: UI_REQUEST.REQUEST_SELECT_DEVICE,
|
|
1051
|
+
payload: { devices }
|
|
1052
|
+
});
|
|
1053
|
+
response = await waitPromise;
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
if (err?._tag === UI_REQUEST_PREEMPTED_TAG) {
|
|
1056
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
1057
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
1058
|
+
payload: {}
|
|
1059
|
+
});
|
|
1060
|
+
throw Object.assign(new Error("Device selection superseded"), {
|
|
1061
|
+
_tag: ERROR_TAG.UserAborted,
|
|
1062
|
+
code: HardwareErrorCode2.UserAborted,
|
|
1063
|
+
cause: err
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
throw err;
|
|
1067
|
+
}
|
|
735
1068
|
const chosen = devices.find((d) => d.connectId === response?.sdkConnectId);
|
|
736
1069
|
if (!chosen) {
|
|
737
1070
|
throw Object.assign(
|
|
738
1071
|
new Error(`Selected sdkConnectId '${response?.sdkConnectId}' not in discovered list`),
|
|
739
|
-
{ _tag:
|
|
1072
|
+
{ _tag: ERROR_TAG.DeviceNotRecognized }
|
|
740
1073
|
);
|
|
741
1074
|
}
|
|
742
1075
|
return chosen.connectId;
|
|
@@ -749,30 +1082,31 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
749
1082
|
* 3. Calls connector.call()
|
|
750
1083
|
* 4. On disconnect error: clears stale session, re-connects, retries once
|
|
751
1084
|
*/
|
|
752
|
-
async connectorCall(connectId, method, params, fingerprint) {
|
|
1085
|
+
async connectorCall(connectId, method, params, fingerprint, permissionDeviceId) {
|
|
753
1086
|
debugLog("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
|
|
754
1087
|
const queueKey = connectId || "__ledger_default__";
|
|
755
|
-
const interruptibility = _LedgerAdapter._getInterruptibility(method);
|
|
756
1088
|
return this._jobQueue.enqueue(
|
|
757
1089
|
queueKey,
|
|
758
|
-
async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint),
|
|
759
|
-
{
|
|
1090
|
+
async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId),
|
|
1091
|
+
{
|
|
1092
|
+
label: method,
|
|
1093
|
+
rejectIfBusy: true,
|
|
1094
|
+
busyError: _LedgerAdapter._createDeviceBusyError(method)
|
|
1095
|
+
}
|
|
760
1096
|
);
|
|
761
1097
|
}
|
|
762
1098
|
/**
|
|
763
|
-
* Race a promise against an abort signal.
|
|
764
|
-
* signal
|
|
765
|
-
* actually be cancelled on Ledger DMK, but the caller gets the abort immediately.
|
|
1099
|
+
* Race a promise against an abort signal. On abort, rejects with
|
|
1100
|
+
* signal.reason → instance _lastCancelReason → generic Error('Aborted').
|
|
766
1101
|
*/
|
|
767
|
-
|
|
1102
|
+
_abortable(signal, promise) {
|
|
1103
|
+
const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
|
|
768
1104
|
if (signal.aborted) {
|
|
769
|
-
return Promise.reject(
|
|
770
|
-
signal.reason ?? new Error("Aborted")
|
|
771
|
-
);
|
|
1105
|
+
return Promise.reject(getAbortReason());
|
|
772
1106
|
}
|
|
773
1107
|
return new Promise((resolve, reject) => {
|
|
774
1108
|
const onAbort = () => {
|
|
775
|
-
reject(
|
|
1109
|
+
reject(getAbortReason());
|
|
776
1110
|
};
|
|
777
1111
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
778
1112
|
promise.then(
|
|
@@ -794,40 +1128,49 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
794
1128
|
}
|
|
795
1129
|
}
|
|
796
1130
|
/** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
|
|
797
|
-
async _runConnectorCall(connectId, method, params, signal, fingerprint) {
|
|
1131
|
+
async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId) {
|
|
798
1132
|
_LedgerAdapter._throwIfAborted(signal);
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1133
|
+
await this._ensureDevicePermission(
|
|
1134
|
+
connectId,
|
|
1135
|
+
permissionDeviceId ?? fingerprint?.deviceId,
|
|
1136
|
+
signal
|
|
802
1137
|
);
|
|
803
1138
|
_LedgerAdapter._throwIfAborted(signal);
|
|
1139
|
+
let effectiveParams = params;
|
|
1140
|
+
if (method === "btcGetPublicKey") {
|
|
1141
|
+
const gatedParams = await this._gateBtcHighIndex(params);
|
|
1142
|
+
if (gatedParams === null) {
|
|
1143
|
+
throw Object.assign(new Error("User cancelled BTC high-index confirmation"), {
|
|
1144
|
+
_tag: ERROR_TAG.UserAborted,
|
|
1145
|
+
code: HardwareErrorCode2.UserAborted
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
effectiveParams = gatedParams;
|
|
1149
|
+
}
|
|
1150
|
+
const resolvedConnectId = await this.ensureConnected(connectId, signal);
|
|
804
1151
|
const sessionId = this._sessions.get(resolvedConnectId);
|
|
805
|
-
debugLog(
|
|
806
|
-
"[LedgerAdapter] connectorCall resolved:",
|
|
807
|
-
method,
|
|
808
|
-
"resolvedConnectId:",
|
|
809
|
-
resolvedConnectId,
|
|
810
|
-
"sessionId:",
|
|
811
|
-
sessionId
|
|
812
|
-
);
|
|
813
1152
|
if (!sessionId) {
|
|
814
1153
|
throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
|
|
815
|
-
_tag:
|
|
1154
|
+
_tag: ERROR_TAG.DeviceSessionNotFound
|
|
816
1155
|
});
|
|
817
1156
|
}
|
|
818
|
-
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
819
|
-
const fp = await _LedgerAdapter._abortable(
|
|
820
|
-
signal,
|
|
821
|
-
this._verifyDeviceFingerprintWithSession(sessionId, fingerprint.deviceId, fingerprint.chain)
|
|
822
|
-
);
|
|
823
|
-
if (!fp.success) {
|
|
824
|
-
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
825
|
-
code: HardwareErrorCode2.DeviceMismatch
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
1157
|
try {
|
|
830
|
-
|
|
1158
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1159
|
+
const fp = await this._abortable(
|
|
1160
|
+
signal,
|
|
1161
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1162
|
+
sessionId,
|
|
1163
|
+
fingerprint.deviceId,
|
|
1164
|
+
fingerprint.chain
|
|
1165
|
+
)
|
|
1166
|
+
);
|
|
1167
|
+
if (!fp.success) {
|
|
1168
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1169
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return await this._abortable(signal, this.connector.call(sessionId, method, effectiveParams));
|
|
831
1174
|
} catch (err) {
|
|
832
1175
|
if (signal.aborted) throw err;
|
|
833
1176
|
const errObj = err;
|
|
@@ -837,37 +1180,243 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
837
1180
|
errorCode: errObj?.errorCode,
|
|
838
1181
|
statusCode: errObj?.statusCode,
|
|
839
1182
|
isDisconnected: isDeviceDisconnectedError(err),
|
|
840
|
-
isLocked: isDeviceLockedError(err)
|
|
1183
|
+
isLocked: isDeviceLockedError(err),
|
|
1184
|
+
isNotAdvertising: isDeviceNotAdvertisingError(err),
|
|
1185
|
+
isStuckApp: isStuckAppStateError(err)
|
|
841
1186
|
});
|
|
842
|
-
if (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1187
|
+
if (isStuckAppStateError(err)) {
|
|
1188
|
+
try {
|
|
1189
|
+
this._sessions.delete(resolvedConnectId);
|
|
1190
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
1191
|
+
this.connector.reset?.();
|
|
1192
|
+
} catch {
|
|
1193
|
+
}
|
|
1194
|
+
debugLog(
|
|
1195
|
+
"[LedgerAdapter] stuck-app retry: method=",
|
|
1196
|
+
method,
|
|
1197
|
+
"delayMs=",
|
|
1198
|
+
_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
|
|
1199
|
+
"_tag=",
|
|
1200
|
+
err?._tag
|
|
1201
|
+
);
|
|
1202
|
+
try {
|
|
1203
|
+
const retryResult = await this._retryAfterStuckApp(
|
|
1204
|
+
resolvedConnectId,
|
|
1205
|
+
method,
|
|
1206
|
+
effectiveParams,
|
|
1207
|
+
signal,
|
|
1208
|
+
err,
|
|
1209
|
+
fingerprint
|
|
1210
|
+
);
|
|
1211
|
+
debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
|
|
1212
|
+
return retryResult;
|
|
1213
|
+
} catch (retryErr) {
|
|
1214
|
+
if (signal.aborted) throw retryErr;
|
|
1215
|
+
if (isStuckAppStateError(retryErr)) {
|
|
1216
|
+
debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
|
|
1217
|
+
throw err;
|
|
1218
|
+
}
|
|
1219
|
+
debugLog(
|
|
1220
|
+
"[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
|
|
1221
|
+
method,
|
|
1222
|
+
"retryErrTag=",
|
|
1223
|
+
retryErr?._tag
|
|
1224
|
+
);
|
|
1225
|
+
throw retryErr;
|
|
1226
|
+
}
|
|
846
1227
|
}
|
|
847
|
-
if (isDeviceLockedError(err)) {
|
|
848
|
-
|
|
849
|
-
_LedgerAdapter.
|
|
850
|
-
|
|
1228
|
+
if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
|
|
1229
|
+
let lastErr = err;
|
|
1230
|
+
for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
|
|
1231
|
+
if (signal.aborted) throw lastErr;
|
|
1232
|
+
try {
|
|
1233
|
+
this._sessions.delete(resolvedConnectId);
|
|
1234
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
1235
|
+
if (isDeviceDisconnectedError(lastErr)) {
|
|
1236
|
+
try {
|
|
1237
|
+
this.connector.reset?.();
|
|
1238
|
+
} catch {
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
const retryConnectId = isLedgerBleConnectionType(this.connector.connectionType) ? resolvedConnectId : void 0;
|
|
1242
|
+
const reConnectId = await this.ensureConnected(retryConnectId, signal);
|
|
1243
|
+
const reSessionId = this._sessions.get(reConnectId);
|
|
1244
|
+
if (!reSessionId) throw lastErr;
|
|
1245
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1246
|
+
const fp = await this._abortable(
|
|
1247
|
+
signal,
|
|
1248
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1249
|
+
reSessionId,
|
|
1250
|
+
fingerprint.deviceId,
|
|
1251
|
+
fingerprint.chain
|
|
1252
|
+
)
|
|
1253
|
+
);
|
|
1254
|
+
if (!fp.success) {
|
|
1255
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1256
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return await this._abortable(
|
|
1261
|
+
signal,
|
|
1262
|
+
this.connector.call(reSessionId, method, effectiveParams)
|
|
1263
|
+
);
|
|
1264
|
+
} catch (retryErr) {
|
|
1265
|
+
if (signal.aborted) throw retryErr;
|
|
1266
|
+
lastErr = retryErr;
|
|
1267
|
+
const canRetry = attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET - 1 && (isDeviceLockedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isDeviceDisconnectedError(retryErr));
|
|
1268
|
+
if (!canRetry) {
|
|
1269
|
+
throw retryErr;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
throw lastErr;
|
|
851
1274
|
}
|
|
852
1275
|
if (isTimeoutError(err)) {
|
|
853
1276
|
debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
|
|
854
1277
|
this._discoveredDevices.delete(resolvedConnectId);
|
|
855
|
-
return this._retryWithFreshConnection(
|
|
1278
|
+
return this._retryWithFreshConnection(
|
|
1279
|
+
resolvedConnectId,
|
|
1280
|
+
method,
|
|
1281
|
+
effectiveParams,
|
|
1282
|
+
signal,
|
|
1283
|
+
err,
|
|
1284
|
+
fingerprint
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
if (isConnectionLevelError(err)) {
|
|
1288
|
+
debugLog("[LedgerAdapter] connection-level fail-closed reset");
|
|
1289
|
+
this._sessions.delete(resolvedConnectId);
|
|
1290
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
1291
|
+
this.connector.reset();
|
|
1292
|
+
const codeNum = err?.code;
|
|
1293
|
+
throw Object.assign(err, {
|
|
1294
|
+
code: codeNum ?? HardwareErrorCode2.DeviceDisconnected
|
|
1295
|
+
});
|
|
856
1296
|
}
|
|
857
1297
|
throw err;
|
|
858
1298
|
}
|
|
859
1299
|
}
|
|
860
|
-
/**
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1300
|
+
/**
|
|
1301
|
+
* Stuck-app recovery: pause for the device's UI transition, then retry once.
|
|
1302
|
+
*
|
|
1303
|
+
* Caller has already cleared the session + reset connector. We wait so Stax
|
|
1304
|
+
* finishes its post-CloseApp animation, then go through ensureConnected +
|
|
1305
|
+
* fingerprint check + call exactly once. Caller decides what to do on a
|
|
1306
|
+
* second stuck-app hit.
|
|
1307
|
+
*/
|
|
1308
|
+
async _retryAfterStuckApp(resolvedConnectId, method, params, signal, originalErr, fingerprint) {
|
|
1309
|
+
await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
|
|
1310
|
+
const retryTargetConnectId = isLedgerBleConnectionType(this.connector.connectionType) ? resolvedConnectId : void 0;
|
|
1311
|
+
const retryConnectId = await this.ensureConnected(retryTargetConnectId, signal);
|
|
1312
|
+
const retrySessionId = this._sessions.get(retryConnectId);
|
|
1313
|
+
if (!retrySessionId) throw originalErr;
|
|
1314
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1315
|
+
const fp = await this._abortable(
|
|
1316
|
+
signal,
|
|
1317
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1318
|
+
retrySessionId,
|
|
1319
|
+
fingerprint.deviceId,
|
|
1320
|
+
fingerprint.chain
|
|
1321
|
+
)
|
|
1322
|
+
);
|
|
1323
|
+
if (!fp.success) {
|
|
1324
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1325
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return this._abortable(signal, this.connector.call(retrySessionId, method, params));
|
|
1330
|
+
}
|
|
1331
|
+
_sleepAbortable(ms, signal) {
|
|
1332
|
+
return new Promise((resolve, reject) => {
|
|
1333
|
+
if (signal.aborted) {
|
|
1334
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const timer = setTimeout(() => {
|
|
1338
|
+
signal.removeEventListener("abort", onAbort);
|
|
1339
|
+
resolve();
|
|
1340
|
+
}, ms);
|
|
1341
|
+
const onAbort = () => {
|
|
1342
|
+
clearTimeout(timer);
|
|
1343
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
1344
|
+
};
|
|
1345
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Clear stale session, reconnect, and retry the call.
|
|
1350
|
+
*
|
|
1351
|
+
* Timeout recovery starts with a full connector reset. After an APDU
|
|
1352
|
+
* timeout, DMK/transport state may still emit malformed responses; retrying
|
|
1353
|
+
* on the same DMK can poison the next chain switch.
|
|
1354
|
+
*/
|
|
1355
|
+
async _retryWithFreshConnection(targetConnectId, method, params, signal, originalErr, fingerprint) {
|
|
1356
|
+
this.connector.reset();
|
|
1357
|
+
this._sessions.clear();
|
|
1358
|
+
this._discoveredDevices.clear();
|
|
1359
|
+
this._connectingPromise = null;
|
|
1360
|
+
const retryConnectId = await this.ensureConnected(targetConnectId, signal);
|
|
866
1361
|
const retrySessionId = this._sessions.get(retryConnectId);
|
|
867
1362
|
if (!retrySessionId) {
|
|
868
1363
|
throw originalErr;
|
|
869
1364
|
}
|
|
870
|
-
|
|
1365
|
+
try {
|
|
1366
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1367
|
+
const fp = await this._abortable(
|
|
1368
|
+
signal,
|
|
1369
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1370
|
+
retrySessionId,
|
|
1371
|
+
fingerprint.deviceId,
|
|
1372
|
+
fingerprint.chain
|
|
1373
|
+
)
|
|
1374
|
+
);
|
|
1375
|
+
if (!fp.success) {
|
|
1376
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1377
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return await this._abortable(signal, this.connector.call(retrySessionId, method, params));
|
|
1382
|
+
} catch (retryErr) {
|
|
1383
|
+
if (signal.aborted) throw retryErr;
|
|
1384
|
+
this.connector.reset();
|
|
1385
|
+
this._sessions.clear();
|
|
1386
|
+
this._discoveredDevices.clear();
|
|
1387
|
+
this._connectingPromise = null;
|
|
1388
|
+
if (!isDeviceDisconnectedError(retryErr) && !isTimeoutError(retryErr)) {
|
|
1389
|
+
throw retryErr;
|
|
1390
|
+
}
|
|
1391
|
+
debugLog(
|
|
1392
|
+
"[LedgerAdapter] fresh-session retry still failed; resetting connector and rebuilding DMK"
|
|
1393
|
+
);
|
|
1394
|
+
this.connector.reset();
|
|
1395
|
+
this._sessions.clear();
|
|
1396
|
+
this._discoveredDevices.clear();
|
|
1397
|
+
this._connectingPromise = null;
|
|
1398
|
+
const finalConnectId = await this.ensureConnected(targetConnectId, signal);
|
|
1399
|
+
const finalSessionId = this._sessions.get(finalConnectId);
|
|
1400
|
+
if (!finalSessionId) {
|
|
1401
|
+
throw originalErr;
|
|
1402
|
+
}
|
|
1403
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
1404
|
+
const fp = await this._abortable(
|
|
1405
|
+
signal,
|
|
1406
|
+
this._verifyDeviceFingerprintWithSession(
|
|
1407
|
+
finalSessionId,
|
|
1408
|
+
fingerprint.deviceId,
|
|
1409
|
+
fingerprint.chain
|
|
1410
|
+
)
|
|
1411
|
+
);
|
|
1412
|
+
if (!fp.success) {
|
|
1413
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
1414
|
+
code: HardwareErrorCode2.DeviceMismatch
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return this._abortable(signal, this.connector.call(finalSessionId, method, params));
|
|
1419
|
+
}
|
|
871
1420
|
}
|
|
872
1421
|
/**
|
|
873
1422
|
* Ensure OS-level device permission (Bluetooth / USB) before proceeding.
|
|
@@ -881,7 +1430,10 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
881
1430
|
* - No connectId (searchDevices): environment-level permission
|
|
882
1431
|
* - With connectId (business methods): device-level permission
|
|
883
1432
|
*/
|
|
884
|
-
async _ensureDevicePermission(connectId, deviceId) {
|
|
1433
|
+
async _ensureDevicePermission(connectId, deviceId, signal) {
|
|
1434
|
+
if (signal?.aborted) {
|
|
1435
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
1436
|
+
}
|
|
885
1437
|
const transportType = this.activeTransport ?? "hid";
|
|
886
1438
|
const waitPromise = this._uiRegistry.wait(
|
|
887
1439
|
UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
@@ -891,10 +1443,37 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
891
1443
|
type: UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
892
1444
|
payload: { transportType, connectId, deviceId }
|
|
893
1445
|
});
|
|
894
|
-
|
|
1446
|
+
let response;
|
|
1447
|
+
const onAbort = () => {
|
|
1448
|
+
this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_PERMISSION);
|
|
1449
|
+
};
|
|
1450
|
+
try {
|
|
1451
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1452
|
+
response = await waitPromise;
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
if (signal?.aborted) {
|
|
1455
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
1456
|
+
}
|
|
1457
|
+
if (err?._tag === UI_REQUEST_PREEMPTED_TAG) {
|
|
1458
|
+
this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
|
|
1459
|
+
type: UI_REQUEST.CLOSE_UI_WINDOW,
|
|
1460
|
+
payload: {}
|
|
1461
|
+
});
|
|
1462
|
+
throw Object.assign(new Error("Device permission request superseded"), {
|
|
1463
|
+
_tag: ERROR_TAG.UserAborted,
|
|
1464
|
+
code: HardwareErrorCode2.UserAborted,
|
|
1465
|
+
cause: err
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
throw err;
|
|
1469
|
+
} finally {
|
|
1470
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1471
|
+
}
|
|
1472
|
+
const { granted, reason, message } = response;
|
|
895
1473
|
if (!granted) {
|
|
896
|
-
throw Object.assign(new Error("Device permission denied"), {
|
|
897
|
-
code: HardwareErrorCode2.DevicePermissionDenied
|
|
1474
|
+
throw Object.assign(new Error(message ?? "Device permission denied"), {
|
|
1475
|
+
code: HardwareErrorCode2.DevicePermissionDenied,
|
|
1476
|
+
reason
|
|
898
1477
|
});
|
|
899
1478
|
}
|
|
900
1479
|
}
|
|
@@ -904,89 +1483,61 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
904
1483
|
*/
|
|
905
1484
|
errorToFailure(err) {
|
|
906
1485
|
debugError("[LedgerAdapter] error:", err);
|
|
1486
|
+
const tag = err && typeof err === "object" ? err._tag : void 0;
|
|
907
1487
|
if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
|
|
908
1488
|
const e = err;
|
|
909
|
-
|
|
1489
|
+
const params = e.code === HardwareErrorCode2.DevicePermissionDenied && e.reason ? { permissionDeniedReason: e.reason } : void 0;
|
|
1490
|
+
return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName, tag, params);
|
|
910
1491
|
}
|
|
911
1492
|
const mapped = mapLedgerError(err);
|
|
912
|
-
return ledgerFailure(mapped.code, mapped.message, mapped.appName);
|
|
1493
|
+
return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
|
|
913
1494
|
}
|
|
914
1495
|
registerEventListeners() {
|
|
915
1496
|
this.connector.on("device-connect", this.deviceConnectHandler);
|
|
916
1497
|
this.connector.on("device-disconnect", this.deviceDisconnectHandler);
|
|
917
|
-
this.connector.on("ui-
|
|
918
|
-
this.connector.on("ui-event", this.uiEventHandler);
|
|
1498
|
+
this.connector.on("ui-event", this.uiEventForwarder);
|
|
919
1499
|
}
|
|
920
1500
|
unregisterEventListeners() {
|
|
921
1501
|
this.connector.off("device-connect", this.deviceConnectHandler);
|
|
922
1502
|
this.connector.off("device-disconnect", this.deviceDisconnectHandler);
|
|
923
|
-
this.connector.off("ui-
|
|
924
|
-
this.connector.off("ui-event", this.uiEventHandler);
|
|
925
|
-
}
|
|
926
|
-
handleUiEvent(event) {
|
|
927
|
-
if (!event.type) return;
|
|
928
|
-
const payload = event.payload;
|
|
929
|
-
const deviceInfo = payload ? this.extractDeviceInfoFromPayload(payload) : this.unknownDevice();
|
|
930
|
-
switch (event.type) {
|
|
931
|
-
case "ui-request_confirmation":
|
|
932
|
-
this.emitter.emit(UI_REQUEST.REQUEST_BUTTON, {
|
|
933
|
-
type: UI_REQUEST.REQUEST_BUTTON,
|
|
934
|
-
payload: { device: deviceInfo }
|
|
935
|
-
});
|
|
936
|
-
break;
|
|
937
|
-
}
|
|
1503
|
+
this.connector.off("ui-event", this.uiEventForwarder);
|
|
938
1504
|
}
|
|
939
1505
|
// ---------------------------------------------------------------------------
|
|
940
1506
|
// Device info mapping
|
|
941
1507
|
// ---------------------------------------------------------------------------
|
|
942
1508
|
connectorDeviceToDeviceInfo(device) {
|
|
943
|
-
const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
|
|
944
1509
|
return {
|
|
945
1510
|
vendor: "ledger",
|
|
946
1511
|
model: device.model ?? "unknown",
|
|
1512
|
+
modelName: device.modelName,
|
|
947
1513
|
firmwareVersion: "",
|
|
948
1514
|
deviceId: device.deviceId,
|
|
949
1515
|
connectId: device.connectId,
|
|
950
1516
|
label: device.name,
|
|
951
|
-
connectionType:
|
|
1517
|
+
connectionType: this.connector.connectionType,
|
|
1518
|
+
rssi: device.rssi,
|
|
1519
|
+
isConnectable: device.isConnectable,
|
|
1520
|
+
serialNumber: device.serialNumber,
|
|
952
1521
|
capabilities: device.capabilities
|
|
953
1522
|
};
|
|
954
1523
|
}
|
|
955
|
-
extractDeviceInfoFromPayload(payload) {
|
|
956
|
-
return {
|
|
957
|
-
vendor: "ledger",
|
|
958
|
-
model: payload.model ?? "unknown",
|
|
959
|
-
firmwareVersion: "",
|
|
960
|
-
deviceId: payload.deviceId ?? payload.id ?? "",
|
|
961
|
-
connectId: payload.connectId ?? payload.path ?? "",
|
|
962
|
-
label: payload.label,
|
|
963
|
-
connectionType: "usb"
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
unknownDevice() {
|
|
967
|
-
return {
|
|
968
|
-
vendor: "ledger",
|
|
969
|
-
model: "unknown",
|
|
970
|
-
firmwareVersion: "",
|
|
971
|
-
deviceId: "",
|
|
972
|
-
connectId: "",
|
|
973
|
-
connectionType: "usb"
|
|
974
|
-
};
|
|
975
|
-
}
|
|
976
1524
|
};
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
_LedgerAdapter.
|
|
1525
|
+
// Layer 2 retry budget after connection-class error. Each round delegates
|
|
1526
|
+
// to Layer 1 (which owns the unlock prompt). Layer 2 never emits UI itself.
|
|
1527
|
+
_LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET = 3;
|
|
1528
|
+
// Stuck-app (APDU 0x6901) retry pause. Ledger Stax often returns 6901 when
|
|
1529
|
+
// OpenAppCommand lands mid-transition right after CloseApp; a short wait
|
|
1530
|
+
// lets the device finish its UI animation before we retry once.
|
|
1531
|
+
_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS = 500;
|
|
1532
|
+
// Layer 1 confirm budget. After N failed Confirm cycles (user keeps clicking
|
|
1533
|
+
// Confirm but device never shows up / never unlocks), throw DeviceNotFound
|
|
1534
|
+
// instead of looping forever.
|
|
1535
|
+
_LedgerAdapter.MAX_DOCONNECT_CONFIRMS = 3;
|
|
988
1536
|
var LedgerAdapter = _LedgerAdapter;
|
|
989
1537
|
|
|
1538
|
+
// src/connector/LedgerConnectorBase.ts
|
|
1539
|
+
import { HardwareErrorCode as HardwareErrorCode7 } from "@onekeyfe/hwk-adapter-core";
|
|
1540
|
+
|
|
990
1541
|
// src/device/LedgerDeviceManager.ts
|
|
991
1542
|
var LedgerDeviceManager = class {
|
|
992
1543
|
constructor(dmk) {
|
|
@@ -1023,7 +1574,9 @@ var LedgerDeviceManager = class {
|
|
|
1023
1574
|
if (resolved) return;
|
|
1024
1575
|
resolved = true;
|
|
1025
1576
|
this._discovered.clear();
|
|
1026
|
-
debugLog(
|
|
1577
|
+
debugLog(
|
|
1578
|
+
`[DMK] enumerate count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
|
|
1579
|
+
);
|
|
1027
1580
|
for (const d of devices) {
|
|
1028
1581
|
this._discovered.set(d.id, d);
|
|
1029
1582
|
}
|
|
@@ -1033,8 +1586,10 @@ var LedgerDeviceManager = class {
|
|
|
1033
1586
|
devices.map((d) => ({
|
|
1034
1587
|
path: d.id,
|
|
1035
1588
|
type: d.deviceModel.model,
|
|
1589
|
+
modelName: d.deviceModel.name,
|
|
1036
1590
|
name: d.name,
|
|
1037
|
-
transport: d.transport
|
|
1591
|
+
transport: d.transport,
|
|
1592
|
+
rssi: d.rssi
|
|
1038
1593
|
}))
|
|
1039
1594
|
);
|
|
1040
1595
|
} else {
|
|
@@ -1055,8 +1610,10 @@ var LedgerDeviceManager = class {
|
|
|
1055
1610
|
devices.map((d) => ({
|
|
1056
1611
|
path: d.id,
|
|
1057
1612
|
type: d.deviceModel.model,
|
|
1613
|
+
modelName: d.deviceModel.name,
|
|
1058
1614
|
name: d.name,
|
|
1059
|
-
transport: d.transport
|
|
1615
|
+
transport: d.transport,
|
|
1616
|
+
rssi: d.rssi
|
|
1060
1617
|
}))
|
|
1061
1618
|
);
|
|
1062
1619
|
}
|
|
@@ -1080,8 +1637,10 @@ var LedgerDeviceManager = class {
|
|
|
1080
1637
|
descriptor: {
|
|
1081
1638
|
path: d.id,
|
|
1082
1639
|
type: d.deviceModel.model,
|
|
1640
|
+
modelName: d.deviceModel.name,
|
|
1083
1641
|
name: d.name,
|
|
1084
|
-
transport: d.transport
|
|
1642
|
+
transport: d.transport,
|
|
1643
|
+
rssi: d.rssi
|
|
1085
1644
|
}
|
|
1086
1645
|
});
|
|
1087
1646
|
}
|
|
@@ -1100,6 +1659,54 @@ var LedgerDeviceManager = class {
|
|
|
1100
1659
|
this._listenSub?.unsubscribe();
|
|
1101
1660
|
this._listenSub = null;
|
|
1102
1661
|
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Resolve to the latest snapshot of advertising devices observed within
|
|
1664
|
+
* `timeoutMs`. Rewrites `_discovered` so it tracks the live list.
|
|
1665
|
+
*
|
|
1666
|
+
* `listenToAvailableDevices` is a BehaviorSubject — it emits the cached
|
|
1667
|
+
* list on subscribe and *only* re-emits when the list changes. A stable
|
|
1668
|
+
* list of currently-advertising devices therefore produces only the
|
|
1669
|
+
* subscription frame; treating that frame as "fossil to be discarded"
|
|
1670
|
+
* caused devices to flicker in/out of the UI as searches alternated
|
|
1671
|
+
* between "saw a change frame" and "saw only the cached frame".
|
|
1672
|
+
*
|
|
1673
|
+
* The window still gives a chance for the active scan to update the list
|
|
1674
|
+
* (e.g. drop a peripheral that just stopped broadcasting), but if nothing
|
|
1675
|
+
* changes we trust the snapshot — DMK silence on a stable list means
|
|
1676
|
+
* "everything's still there".
|
|
1677
|
+
*/
|
|
1678
|
+
getLiveDevices(timeoutMs = 1500) {
|
|
1679
|
+
debugLog(`[DMK] getLiveDevices() called, timeoutMs=${timeoutMs}`);
|
|
1680
|
+
void this.requestDevice();
|
|
1681
|
+
return new Promise((resolve) => {
|
|
1682
|
+
let done = false;
|
|
1683
|
+
let lastSeen = [];
|
|
1684
|
+
let sub = null;
|
|
1685
|
+
const finish = (devices) => {
|
|
1686
|
+
if (done) return;
|
|
1687
|
+
done = true;
|
|
1688
|
+
sub?.unsubscribe();
|
|
1689
|
+
clearTimeout(timer);
|
|
1690
|
+
this._discovered.clear();
|
|
1691
|
+
for (const d of devices) this._discovered.set(d.id, d);
|
|
1692
|
+
debugLog(
|
|
1693
|
+
`[DMK] getLiveDevices() resolved count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
|
|
1694
|
+
);
|
|
1695
|
+
resolve(devices);
|
|
1696
|
+
};
|
|
1697
|
+
const timer = setTimeout(() => finish(lastSeen), timeoutMs);
|
|
1698
|
+
sub = this._dmk.listenToAvailableDevices({}).subscribe({
|
|
1699
|
+
next: (devices) => {
|
|
1700
|
+
lastSeen = devices;
|
|
1701
|
+
},
|
|
1702
|
+
error: (err) => {
|
|
1703
|
+
debugError("[DMK] getLiveDevices() error:", err);
|
|
1704
|
+
finish(lastSeen);
|
|
1705
|
+
},
|
|
1706
|
+
complete: () => finish(lastSeen)
|
|
1707
|
+
});
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1103
1710
|
requestDevice() {
|
|
1104
1711
|
if (this._discoverySub) {
|
|
1105
1712
|
return Promise.resolve();
|
|
@@ -1117,11 +1724,33 @@ var LedgerDeviceManager = class {
|
|
|
1117
1724
|
});
|
|
1118
1725
|
return Promise.resolve();
|
|
1119
1726
|
}
|
|
1727
|
+
/** Lookup a previously-discovered device's display name. */
|
|
1728
|
+
getDeviceName(deviceId) {
|
|
1729
|
+
return this._discovered.get(deviceId)?.name;
|
|
1730
|
+
}
|
|
1731
|
+
/** Lookup minimal model/signal info from a previously-discovered device. */
|
|
1732
|
+
getDiscoveredDeviceInfo(deviceId) {
|
|
1733
|
+
const d = this._discovered.get(deviceId);
|
|
1734
|
+
if (!d) return void 0;
|
|
1735
|
+
return {
|
|
1736
|
+
model: d.deviceModel?.model,
|
|
1737
|
+
modelName: d.deviceModel?.name,
|
|
1738
|
+
name: d.name,
|
|
1739
|
+
rssi: d.rssi
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
hasDiscoveredDevice(deviceId) {
|
|
1743
|
+
return this._discovered.has(deviceId);
|
|
1744
|
+
}
|
|
1120
1745
|
/** Connect to a previously discovered device. Returns sessionId. */
|
|
1121
1746
|
async connect(deviceId) {
|
|
1122
1747
|
const device = this._discovered.get(deviceId);
|
|
1123
1748
|
if (!device) {
|
|
1124
|
-
|
|
1749
|
+
const err = new Error(
|
|
1750
|
+
`Device "${deviceId}" not found in discovery cache. Call enumerate() or listen() first.`
|
|
1751
|
+
);
|
|
1752
|
+
err._tag = ERROR_TAG.DeviceNotInDiscoveryCache;
|
|
1753
|
+
throw err;
|
|
1125
1754
|
}
|
|
1126
1755
|
const sessionId = await this._dmk.connect({ device });
|
|
1127
1756
|
this._sessions.set(deviceId, sessionId);
|
|
@@ -1160,6 +1789,19 @@ var LedgerDeviceManager = class {
|
|
|
1160
1789
|
this._sessionToDevice.clear();
|
|
1161
1790
|
this._dmk.close();
|
|
1162
1791
|
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Tear down RxJS subscriptions and local state without closing the DMK.
|
|
1794
|
+
* For light-reset paths that want to drop session state but keep the
|
|
1795
|
+
* underlying transport alive for retry. Call this instead of orphaning
|
|
1796
|
+
* the manager — bare null assignment leaks _listenSub / _discoverySub.
|
|
1797
|
+
*/
|
|
1798
|
+
disposeKeepingDmk() {
|
|
1799
|
+
this.stopListening();
|
|
1800
|
+
this.stopDiscovery();
|
|
1801
|
+
this._discovered.clear();
|
|
1802
|
+
this._sessions.clear();
|
|
1803
|
+
this._sessionToDevice.clear();
|
|
1804
|
+
}
|
|
1163
1805
|
};
|
|
1164
1806
|
|
|
1165
1807
|
// src/signer/SignerManager.ts
|
|
@@ -1171,11 +1813,12 @@ import { hexToBytes } from "@onekeyfe/hwk-adapter-core";
|
|
|
1171
1813
|
|
|
1172
1814
|
// src/signer/deviceActionToPromise.ts
|
|
1173
1815
|
import { DeviceActionStatus } from "@ledgerhq/device-management-kit";
|
|
1174
|
-
var
|
|
1175
|
-
function deviceActionToPromise(action, onInteraction, timeoutMs =
|
|
1816
|
+
var IDLE_WATCHDOG_MS = 65e3;
|
|
1817
|
+
function deviceActionToPromise(action, onInteraction, timeoutMs = IDLE_WATCHDOG_MS, onRegisterCanceller) {
|
|
1176
1818
|
return new Promise((resolve, reject) => {
|
|
1177
1819
|
let settled = false;
|
|
1178
1820
|
let lastStep;
|
|
1821
|
+
const observedSteps = [];
|
|
1179
1822
|
let sub;
|
|
1180
1823
|
let timer = null;
|
|
1181
1824
|
const cancelAction = () => {
|
|
@@ -1188,44 +1831,59 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1188
1831
|
} catch {
|
|
1189
1832
|
}
|
|
1190
1833
|
};
|
|
1191
|
-
const
|
|
1834
|
+
const armIdleWatchdog = () => {
|
|
1192
1835
|
if (timer) clearTimeout(timer);
|
|
1193
1836
|
if (timeoutMs > 0) {
|
|
1194
1837
|
timer = setTimeout(() => {
|
|
1195
1838
|
if (!settled) {
|
|
1196
1839
|
settled = true;
|
|
1197
1840
|
cancelAction();
|
|
1198
|
-
reject(
|
|
1841
|
+
reject(
|
|
1842
|
+
new Error(
|
|
1843
|
+
"Ledger transport unresponsive: no DMK state change within the watchdog window. The device may have lost connection."
|
|
1844
|
+
)
|
|
1845
|
+
);
|
|
1199
1846
|
}
|
|
1200
1847
|
}, timeoutMs);
|
|
1201
1848
|
}
|
|
1202
1849
|
};
|
|
1203
|
-
|
|
1850
|
+
armIdleWatchdog();
|
|
1204
1851
|
if (onRegisterCanceller) {
|
|
1205
|
-
onRegisterCanceller(() => {
|
|
1206
|
-
if (settled)
|
|
1852
|
+
onRegisterCanceller((reason) => {
|
|
1853
|
+
if (settled) {
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1207
1856
|
settled = true;
|
|
1208
1857
|
if (timer) clearTimeout(timer);
|
|
1209
1858
|
cancelAction();
|
|
1210
|
-
|
|
1859
|
+
const err = new Error(reason?.message ?? "Device action cancelled");
|
|
1860
|
+
if (reason?.code !== void 0) {
|
|
1861
|
+
err.code = reason.code;
|
|
1862
|
+
}
|
|
1863
|
+
if (reason?.tag) {
|
|
1864
|
+
Object.defineProperty(err, "_tag", {
|
|
1865
|
+
value: reason.tag,
|
|
1866
|
+
configurable: true,
|
|
1867
|
+
enumerable: false,
|
|
1868
|
+
writable: true
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
reject(err);
|
|
1211
1872
|
});
|
|
1212
1873
|
}
|
|
1213
|
-
debugLog("[DMK-Observable] subscribing to action.observable...");
|
|
1214
1874
|
sub = action.observable.subscribe({
|
|
1215
1875
|
next: (state) => {
|
|
1216
|
-
if (settled)
|
|
1217
|
-
|
|
1876
|
+
if (settled) {
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
armIdleWatchdog();
|
|
1218
1880
|
const step = state?.intermediateValue?.step;
|
|
1219
|
-
if (step)
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
hasOutput: state.status === DeviceActionStatus.Completed,
|
|
1226
|
-
hasError: state.status === DeviceActionStatus.Error
|
|
1227
|
-
})
|
|
1228
|
-
);
|
|
1881
|
+
if (step) {
|
|
1882
|
+
lastStep = step;
|
|
1883
|
+
if (observedSteps[observedSteps.length - 1] !== step) {
|
|
1884
|
+
observedSteps.push(step);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1229
1887
|
if (state.status === DeviceActionStatus.Completed) {
|
|
1230
1888
|
settled = true;
|
|
1231
1889
|
if (timer) clearTimeout(timer);
|
|
@@ -1237,10 +1895,14 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1237
1895
|
if (timer) clearTimeout(timer);
|
|
1238
1896
|
onInteraction?.("interaction-complete");
|
|
1239
1897
|
sub?.unsubscribe();
|
|
1240
|
-
|
|
1898
|
+
rejectWithStepContext(state.error, lastStep, observedSteps, reject);
|
|
1241
1899
|
} else if (state.status === DeviceActionStatus.Pending && onInteraction) {
|
|
1242
1900
|
const interaction = state.intermediateValue?.requiredUserInteraction;
|
|
1243
1901
|
if (interaction && interaction !== "none") {
|
|
1902
|
+
if (interaction === "unlock-device" && timer) {
|
|
1903
|
+
clearTimeout(timer);
|
|
1904
|
+
timer = null;
|
|
1905
|
+
}
|
|
1244
1906
|
onInteraction(String(interaction));
|
|
1245
1907
|
}
|
|
1246
1908
|
}
|
|
@@ -1250,7 +1912,7 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1250
1912
|
settled = true;
|
|
1251
1913
|
if (timer) clearTimeout(timer);
|
|
1252
1914
|
sub?.unsubscribe();
|
|
1253
|
-
|
|
1915
|
+
rejectWithStepContext(err, lastStep, observedSteps, reject);
|
|
1254
1916
|
}
|
|
1255
1917
|
},
|
|
1256
1918
|
complete: () => {
|
|
@@ -1263,15 +1925,25 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
|
|
|
1263
1925
|
});
|
|
1264
1926
|
});
|
|
1265
1927
|
}
|
|
1266
|
-
function
|
|
1267
|
-
if (err && typeof err === "object" && lastStep) {
|
|
1928
|
+
function rejectWithStepContext(err, lastStep, observedSteps, reject) {
|
|
1929
|
+
if (err && typeof err === "object" && (lastStep || observedSteps.length > 0)) {
|
|
1268
1930
|
try {
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1931
|
+
if (lastStep) {
|
|
1932
|
+
Object.defineProperty(err, "_lastStep", {
|
|
1933
|
+
value: lastStep,
|
|
1934
|
+
configurable: true,
|
|
1935
|
+
enumerable: false,
|
|
1936
|
+
writable: true
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
if (observedSteps.length > 0) {
|
|
1940
|
+
Object.defineProperty(err, "_deviceActionSteps", {
|
|
1941
|
+
value: [...observedSteps],
|
|
1942
|
+
configurable: true,
|
|
1943
|
+
enumerable: false,
|
|
1944
|
+
writable: true
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1275
1947
|
} catch {
|
|
1276
1948
|
}
|
|
1277
1949
|
}
|
|
@@ -1337,7 +2009,8 @@ var SignerManager = class _SignerManager {
|
|
|
1337
2009
|
async getOrCreate(sessionId) {
|
|
1338
2010
|
debugLog("[DMK] SignerManager.getOrCreate:", { sessionId });
|
|
1339
2011
|
const builder = await this._builderFn({ dmk: this._dmk, sessionId });
|
|
1340
|
-
|
|
2012
|
+
const builderWithContext = builder.withContextModule ? builder.withContextModule(_SignerManager._createContextModule()) : builder;
|
|
2013
|
+
return new SignerEth(builderWithContext.build());
|
|
1341
2014
|
}
|
|
1342
2015
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
|
|
1343
2016
|
invalidate(_sessionId) {
|
|
@@ -1346,10 +2019,24 @@ var SignerManager = class _SignerManager {
|
|
|
1346
2019
|
clearAll() {
|
|
1347
2020
|
}
|
|
1348
2021
|
static _defaultBuilder() {
|
|
1349
|
-
return (args) =>
|
|
1350
|
-
|
|
1351
|
-
|
|
2022
|
+
return (args) => new SignerEthBuilder(args);
|
|
2023
|
+
}
|
|
2024
|
+
static _createContextModule() {
|
|
2025
|
+
const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
|
|
2026
|
+
return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
|
|
2027
|
+
}
|
|
2028
|
+
static wrapBlindSigningReportNonBlocking(contextModule) {
|
|
2029
|
+
const report = contextModule.report.bind(contextModule);
|
|
2030
|
+
contextModule.report = async (params) => {
|
|
2031
|
+
try {
|
|
2032
|
+
void report(params).catch((err) => {
|
|
2033
|
+
debugLog("[DMK] ContextModule.report failed:", err);
|
|
2034
|
+
});
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
debugLog("[DMK] ContextModule.report failed:", err);
|
|
2037
|
+
}
|
|
1352
2038
|
};
|
|
2039
|
+
return contextModule;
|
|
1353
2040
|
}
|
|
1354
2041
|
};
|
|
1355
2042
|
|
|
@@ -1357,20 +2044,20 @@ var SignerManager = class _SignerManager {
|
|
|
1357
2044
|
import { HardwareErrorCode as HardwareErrorCode3, padHex64, stripHex } from "@onekeyfe/hwk-adapter-core";
|
|
1358
2045
|
|
|
1359
2046
|
// src/connector/chains/utils.ts
|
|
1360
|
-
import { EConnectorInteraction } from "@onekeyfe/hwk-adapter-core";
|
|
2047
|
+
import { EConnectorInteraction as EConnectorInteraction2 } from "@onekeyfe/hwk-adapter-core";
|
|
1361
2048
|
function normalizePath(path) {
|
|
1362
2049
|
return path.startsWith("m/") ? path.slice(2) : path;
|
|
1363
2050
|
}
|
|
1364
2051
|
function collapseSignerInteraction(interaction) {
|
|
1365
2052
|
switch (interaction) {
|
|
1366
2053
|
case "confirm-open-app":
|
|
1367
|
-
return
|
|
2054
|
+
return EConnectorInteraction2.ConfirmOpenApp;
|
|
1368
2055
|
case "unlock-device":
|
|
1369
|
-
return
|
|
2056
|
+
return EConnectorInteraction2.UnlockDevice;
|
|
1370
2057
|
case "interaction-complete":
|
|
1371
|
-
return
|
|
2058
|
+
return EConnectorInteraction2.InteractionComplete;
|
|
1372
2059
|
default:
|
|
1373
|
-
return
|
|
2060
|
+
return EConnectorInteraction2.ConfirmOnDevice;
|
|
1374
2061
|
}
|
|
1375
2062
|
}
|
|
1376
2063
|
|
|
@@ -1459,6 +2146,7 @@ async function _getEthSigner(ctx, sessionId) {
|
|
|
1459
2146
|
const signerManager = await ctx.getSignerManager();
|
|
1460
2147
|
const signer = await signerManager.getOrCreate(sessionId);
|
|
1461
2148
|
signer.onInteraction = (interaction) => {
|
|
2149
|
+
debugLog("[LedgerConnector] evm.onInteraction:", interaction);
|
|
1462
2150
|
ctx.emit("ui-event", {
|
|
1463
2151
|
type: collapseSignerInteraction(interaction),
|
|
1464
2152
|
payload: { sessionId }
|
|
@@ -1659,6 +2347,7 @@ async function btcSignTransaction(ctx, sessionId, params) {
|
|
|
1659
2347
|
const path = normalizePath(params.path || "84'/0'/0'");
|
|
1660
2348
|
const purpose = path.split("/")[0]?.replace(/'/g, "");
|
|
1661
2349
|
const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
|
|
2350
|
+
debugLog("[LedgerConnector] btcSignTransaction wallet:", { path, purpose, template });
|
|
1662
2351
|
const wallet = new DefaultWallet(path, template);
|
|
1663
2352
|
const signedTxHex = await btcSigner.signTransaction(wallet, params.psbt);
|
|
1664
2353
|
return { serializedTx: stripHex2(signedTxHex) };
|
|
@@ -1683,8 +2372,10 @@ async function btcSignPsbt(ctx, sessionId, params) {
|
|
|
1683
2372
|
const path = normalizePath(params.path || "84'/0'/0'");
|
|
1684
2373
|
const purpose = path.split("/")[0]?.replace(/'/g, "");
|
|
1685
2374
|
const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
|
|
2375
|
+
debugLog("[LedgerConnector] btcSignPsbt wallet:", { path, purpose, template });
|
|
1686
2376
|
const wallet = new DefaultWallet(path, template);
|
|
1687
2377
|
const signatures = await btcSigner.signPsbt(wallet, params.psbt);
|
|
2378
|
+
debugLog("[LedgerConnector] btcSignPsbt signatures received:", signatures.length);
|
|
1688
2379
|
const signedPsbtHex = _applySignaturesToPsbt(params.psbt, signatures);
|
|
1689
2380
|
return { signedPsbt: signedPsbtHex };
|
|
1690
2381
|
} catch (err) {
|
|
@@ -1731,6 +2422,7 @@ async function _createBtcSigner(ctx, sessionId) {
|
|
|
1731
2422
|
const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
|
|
1732
2423
|
const signer = new SignerBtc(sdkSigner);
|
|
1733
2424
|
signer.onInteraction = (interaction) => {
|
|
2425
|
+
debugLog("[LedgerConnector] btc.onInteraction:", interaction);
|
|
1734
2426
|
ctx.emit("ui-event", {
|
|
1735
2427
|
type: collapseSignerInteraction(interaction),
|
|
1736
2428
|
payload: { sessionId }
|
|
@@ -1901,6 +2593,7 @@ async function _createSolSigner(ctx, sessionId) {
|
|
|
1901
2593
|
const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
|
|
1902
2594
|
const signer = new SignerSol(sdkSigner);
|
|
1903
2595
|
signer.onInteraction = (interaction) => {
|
|
2596
|
+
debugLog("[LedgerConnector] sol.onInteraction:", interaction);
|
|
1904
2597
|
ctx.emit("ui-event", {
|
|
1905
2598
|
type: collapseSignerInteraction(interaction),
|
|
1906
2599
|
payload: { sessionId }
|
|
@@ -1913,11 +2606,11 @@ async function _createSolSigner(ctx, sessionId) {
|
|
|
1913
2606
|
}
|
|
1914
2607
|
|
|
1915
2608
|
// src/connector/chains/tron.ts
|
|
1916
|
-
import {
|
|
2609
|
+
import { HardwareErrorCode as HardwareErrorCode6 } from "@onekeyfe/hwk-adapter-core";
|
|
1917
2610
|
import Trx from "@ledgerhq/hw-app-trx";
|
|
1918
2611
|
|
|
1919
|
-
// src/connector/chains/
|
|
1920
|
-
import { EConnectorInteraction as
|
|
2612
|
+
// src/connector/chains/legacyChainCall.ts
|
|
2613
|
+
import { EConnectorInteraction as EConnectorInteraction3 } from "@onekeyfe/hwk-adapter-core";
|
|
1921
2614
|
|
|
1922
2615
|
// src/app/AppManager.ts
|
|
1923
2616
|
import {
|
|
@@ -1926,6 +2619,7 @@ import {
|
|
|
1926
2619
|
OpenAppCommand,
|
|
1927
2620
|
isSuccessCommandResult
|
|
1928
2621
|
} from "@ledgerhq/device-management-kit";
|
|
2622
|
+
import { HardwareErrorCode as HardwareErrorCode5 } from "@onekeyfe/hwk-adapter-core";
|
|
1929
2623
|
var APP_NAME_MAP = {
|
|
1930
2624
|
ETH: "Ethereum",
|
|
1931
2625
|
BTC: "Bitcoin",
|
|
@@ -1961,9 +2655,16 @@ var AppManager = class {
|
|
|
1961
2655
|
* 5. Poll until the device confirms the target app is running.
|
|
1962
2656
|
*/
|
|
1963
2657
|
/**
|
|
1964
|
-
* @param onConfirmOnDevice Called
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
2658
|
+
* @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued — the
|
|
2659
|
+
* device is about to display "Open <app>" on screen and wait for the
|
|
2660
|
+
* user's button press. UI consumers should show their "open app" prompt
|
|
2661
|
+
* in response. NOT called when the target app is already open (no user
|
|
2662
|
+
* interaction needed in that case).
|
|
2663
|
+
*
|
|
2664
|
+
* Important: OpenAppCommand is blocking. It does not resolve until the user
|
|
2665
|
+
* has physically confirmed on the device, so anything that runs AFTER
|
|
2666
|
+
* `await this._openApp(...)` lands AFTER the prompt is already gone.
|
|
2667
|
+
* Hence the callback must fire BEFORE that await.
|
|
1967
2668
|
*/
|
|
1968
2669
|
async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
|
|
1969
2670
|
const currentApp = await this._getCurrentApp(sessionId);
|
|
@@ -1974,8 +2675,8 @@ var AppManager = class {
|
|
|
1974
2675
|
await this._closeCurrentApp(sessionId);
|
|
1975
2676
|
await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
|
|
1976
2677
|
}
|
|
1977
|
-
await this._openApp(sessionId, targetAppName);
|
|
1978
2678
|
onConfirmOnDevice?.();
|
|
2679
|
+
await this._openApp(sessionId, targetAppName);
|
|
1979
2680
|
await this._waitForApp(sessionId, targetAppName);
|
|
1980
2681
|
}
|
|
1981
2682
|
// ---------------------------------------------------------------------------
|
|
@@ -1987,9 +2688,34 @@ var AppManager = class {
|
|
|
1987
2688
|
command: new GetAppAndVersionCommand()
|
|
1988
2689
|
});
|
|
1989
2690
|
if (isSuccessCommandResult(result)) {
|
|
2691
|
+
debugLog("[AppManager] currentApp:", result.data.name);
|
|
1990
2692
|
return result.data.name;
|
|
1991
2693
|
}
|
|
1992
|
-
|
|
2694
|
+
const errResult = result;
|
|
2695
|
+
const dmkErr = errResult.error ?? {};
|
|
2696
|
+
const original = dmkErr.originalError;
|
|
2697
|
+
debugLog(
|
|
2698
|
+
"[AppManager] _getCurrentApp failed sessionId=",
|
|
2699
|
+
sessionId,
|
|
2700
|
+
"tag=",
|
|
2701
|
+
dmkErr._tag,
|
|
2702
|
+
"errorCode=",
|
|
2703
|
+
dmkErr.errorCode,
|
|
2704
|
+
"message=",
|
|
2705
|
+
dmkErr.message,
|
|
2706
|
+
"originalErrorMessage=",
|
|
2707
|
+
original?.message ?? String(original ?? "")
|
|
2708
|
+
);
|
|
2709
|
+
throw Object.assign(
|
|
2710
|
+
new Error(
|
|
2711
|
+
dmkErr.message ?? "Failed to get current app from device"
|
|
2712
|
+
),
|
|
2713
|
+
{
|
|
2714
|
+
_tag: dmkErr._tag,
|
|
2715
|
+
errorCode: dmkErr.errorCode,
|
|
2716
|
+
originalError: original
|
|
2717
|
+
}
|
|
2718
|
+
);
|
|
1993
2719
|
}
|
|
1994
2720
|
async _openApp(sessionId, appName) {
|
|
1995
2721
|
const result = await this._dmk.sendCommand({
|
|
@@ -1998,8 +2724,11 @@ var AppManager = class {
|
|
|
1998
2724
|
});
|
|
1999
2725
|
if (!isSuccessCommandResult(result)) {
|
|
2000
2726
|
const { statusCode } = result;
|
|
2727
|
+
const hasStatusCode2 = statusCode != null && statusCode !== "";
|
|
2728
|
+
debugLog("[AppManager] openApp failed:", appName, "statusCode:", statusCode);
|
|
2001
2729
|
throw Object.assign(new Error(`Failed to open "${appName}"`), {
|
|
2002
|
-
_tag:
|
|
2730
|
+
_tag: ERROR_TAG.OpenAppCommand,
|
|
2731
|
+
code: hasStatusCode2 ? void 0 : HardwareErrorCode5.AppNotInstalled,
|
|
2003
2732
|
errorCode: String(statusCode ?? ""),
|
|
2004
2733
|
statusCode,
|
|
2005
2734
|
appName
|
|
@@ -2007,6 +2736,7 @@ var AppManager = class {
|
|
|
2007
2736
|
}
|
|
2008
2737
|
}
|
|
2009
2738
|
async _closeCurrentApp(sessionId) {
|
|
2739
|
+
debugLog("[AppManager] closeCurrentApp");
|
|
2010
2740
|
await this._dmk.sendCommand({
|
|
2011
2741
|
sessionId,
|
|
2012
2742
|
command: new CloseAppCommand()
|
|
@@ -2017,15 +2747,23 @@ var AppManager = class {
|
|
|
2017
2747
|
* or throw after `_maxRetries` attempts.
|
|
2018
2748
|
*/
|
|
2019
2749
|
async _waitForApp(sessionId, expectedAppName) {
|
|
2750
|
+
let lastSeen = "";
|
|
2020
2751
|
for (let i = 0; i < this._maxRetries; i++) {
|
|
2021
2752
|
await this._wait();
|
|
2022
2753
|
const current = await this._getCurrentApp(sessionId);
|
|
2754
|
+
lastSeen = current;
|
|
2023
2755
|
if (current === expectedAppName) {
|
|
2024
2756
|
return;
|
|
2025
2757
|
}
|
|
2026
2758
|
}
|
|
2759
|
+
debugLog(
|
|
2760
|
+
"[AppManager] waitForApp exhausted: expected=",
|
|
2761
|
+
expectedAppName,
|
|
2762
|
+
"lastSeen=",
|
|
2763
|
+
lastSeen
|
|
2764
|
+
);
|
|
2027
2765
|
throw new Error(
|
|
2028
|
-
`Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries`
|
|
2766
|
+
`Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
|
|
2029
2767
|
);
|
|
2030
2768
|
}
|
|
2031
2769
|
_isDashboard(appName) {
|
|
@@ -2036,46 +2774,92 @@ var AppManager = class {
|
|
|
2036
2774
|
}
|
|
2037
2775
|
};
|
|
2038
2776
|
|
|
2039
|
-
// src/connector/chains/
|
|
2777
|
+
// src/connector/chains/legacyChainCall.ts
|
|
2040
2778
|
function isLegacyWrongAppError(err, _appName) {
|
|
2041
2779
|
return isWrongAppError(err);
|
|
2042
2780
|
}
|
|
2043
|
-
async function
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2781
|
+
async function withLegacyChainCall(ctx, sessionId, options, action) {
|
|
2782
|
+
const { appName, needsConfirmation } = options;
|
|
2783
|
+
let openAppPromptShown = false;
|
|
2784
|
+
const onAppOpenPrompt = () => {
|
|
2785
|
+
openAppPromptShown = true;
|
|
2047
2786
|
ctx.emit("ui-event", {
|
|
2048
|
-
type:
|
|
2787
|
+
type: EConnectorInteraction3.ConfirmOpenApp,
|
|
2049
2788
|
payload: { sessionId }
|
|
2050
2789
|
});
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2790
|
+
};
|
|
2791
|
+
const closeOpenAppUiIfShown = () => {
|
|
2792
|
+
if (openAppPromptShown) {
|
|
2793
|
+
ctx.emit("ui-event", {
|
|
2794
|
+
type: EConnectorInteraction3.InteractionComplete,
|
|
2795
|
+
payload: { sessionId }
|
|
2796
|
+
});
|
|
2797
|
+
openAppPromptShown = false;
|
|
2798
|
+
}
|
|
2799
|
+
};
|
|
2800
|
+
try {
|
|
2801
|
+
await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
|
|
2802
|
+
} catch (err) {
|
|
2803
|
+
debugLog(
|
|
2804
|
+
"[LegacyChainCall] pre-flight ensureAppOpen failed:",
|
|
2805
|
+
appName,
|
|
2806
|
+
err?.message
|
|
2807
|
+
);
|
|
2808
|
+
closeOpenAppUiIfShown();
|
|
2809
|
+
throw ctx.wrapError(err, { defaultAppName: appName });
|
|
2810
|
+
}
|
|
2811
|
+
const runOnce = async () => {
|
|
2812
|
+
let confirmEmitted = false;
|
|
2813
|
+
if (needsConfirmation) {
|
|
2069
2814
|
ctx.emit("ui-event", {
|
|
2070
|
-
type:
|
|
2815
|
+
type: EConnectorInteraction3.ConfirmOnDevice,
|
|
2071
2816
|
payload: { sessionId }
|
|
2072
2817
|
});
|
|
2818
|
+
confirmEmitted = true;
|
|
2819
|
+
}
|
|
2820
|
+
try {
|
|
2073
2821
|
return await action(sessionId);
|
|
2822
|
+
} finally {
|
|
2823
|
+
if (confirmEmitted || openAppPromptShown) {
|
|
2824
|
+
ctx.emit("ui-event", {
|
|
2825
|
+
type: EConnectorInteraction3.InteractionComplete,
|
|
2826
|
+
payload: { sessionId }
|
|
2827
|
+
});
|
|
2828
|
+
openAppPromptShown = false;
|
|
2829
|
+
}
|
|
2074
2830
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2831
|
+
};
|
|
2832
|
+
try {
|
|
2833
|
+
return await runOnce();
|
|
2834
|
+
} catch (err) {
|
|
2835
|
+
if (!isLegacyWrongAppError(err, appName)) {
|
|
2836
|
+
debugLog("[LegacyChainCall] non-wrong-app failure:", appName, err?.message);
|
|
2837
|
+
ctx.invalidateSession(sessionId);
|
|
2838
|
+
throw ctx.wrapError(err, { defaultAppName: appName });
|
|
2839
|
+
}
|
|
2840
|
+
debugLog("[LegacyChainCall] wrong-app detected, retrying:", appName);
|
|
2841
|
+
try {
|
|
2842
|
+
await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
|
|
2843
|
+
} catch (switchErr) {
|
|
2844
|
+
debugLog(
|
|
2845
|
+
"[LegacyChainCall] retry ensureAppOpen failed:",
|
|
2846
|
+
appName,
|
|
2847
|
+
switchErr?.message
|
|
2848
|
+
);
|
|
2849
|
+
closeOpenAppUiIfShown();
|
|
2850
|
+
throw ctx.wrapError(switchErr, { defaultAppName: appName });
|
|
2851
|
+
}
|
|
2852
|
+
ctx.clearAllSigners();
|
|
2853
|
+
const result = await runOnce();
|
|
2854
|
+
debugLog("[LegacyChainCall] retry succeeded:", appName);
|
|
2855
|
+
return result;
|
|
2077
2856
|
}
|
|
2078
2857
|
}
|
|
2858
|
+
async function _ensureAppOpen(ctx, sessionId, appName, onPrompt) {
|
|
2859
|
+
const dmk = await ctx.getOrCreateDmk();
|
|
2860
|
+
const appManager = new AppManager(dmk);
|
|
2861
|
+
await appManager.ensureAppOpen(sessionId, appName, onPrompt);
|
|
2862
|
+
}
|
|
2079
2863
|
|
|
2080
2864
|
// src/transport/DmkTransport.ts
|
|
2081
2865
|
import Transport from "@ledgerhq/hw-transport";
|
|
@@ -2105,83 +2889,75 @@ var DmkTransport = class extends Transport {
|
|
|
2105
2889
|
// src/connector/chains/tron.ts
|
|
2106
2890
|
async function tronGetAddress(ctx, sessionId, params) {
|
|
2107
2891
|
const path = normalizePath(params.path);
|
|
2108
|
-
|
|
2109
|
-
return
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2892
|
+
const showOnDevice = params.showOnDevice ?? false;
|
|
2893
|
+
return withLegacyChainCall(
|
|
2894
|
+
ctx,
|
|
2895
|
+
sessionId,
|
|
2896
|
+
{
|
|
2897
|
+
appName: "Tron",
|
|
2898
|
+
// Only show "confirm on device" UI when the device is actually going
|
|
2899
|
+
// to display the address for the user to verify.
|
|
2900
|
+
needsConfirmation: showOnDevice
|
|
2901
|
+
},
|
|
2902
|
+
async (sid) => {
|
|
2903
|
+
const trx = await _createTrx(ctx, sid);
|
|
2904
|
+
const result = await trx.getAddress(path, showOnDevice);
|
|
2905
|
+
return { address: result.address, publicKey: result.publicKey, path: params.path };
|
|
2116
2906
|
}
|
|
2117
|
-
|
|
2118
|
-
ctx.emit("ui-event", {
|
|
2119
|
-
type: EConnectorInteraction3.InteractionComplete,
|
|
2120
|
-
payload: { sessionId: sid }
|
|
2121
|
-
});
|
|
2122
|
-
return { address: result.address, publicKey: result.publicKey, path: params.path };
|
|
2123
|
-
});
|
|
2907
|
+
);
|
|
2124
2908
|
}
|
|
2125
2909
|
async function tronSignTransaction(ctx, sessionId, params) {
|
|
2126
2910
|
if (!params.rawTxHex) {
|
|
2127
2911
|
throw Object.assign(
|
|
2128
2912
|
new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
|
|
2129
|
-
{ code:
|
|
2913
|
+
{ code: HardwareErrorCode6.InvalidParams }
|
|
2130
2914
|
);
|
|
2131
2915
|
}
|
|
2132
2916
|
const path = normalizePath(params.path);
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
payload: { sessionId: sid }
|
|
2148
|
-
});
|
|
2149
|
-
return { signature };
|
|
2150
|
-
});
|
|
2917
|
+
return withLegacyChainCall(
|
|
2918
|
+
ctx,
|
|
2919
|
+
sessionId,
|
|
2920
|
+
{ appName: "Tron", needsConfirmation: true },
|
|
2921
|
+
async (sid) => {
|
|
2922
|
+
const trx = await _createTrx(ctx, sid);
|
|
2923
|
+
const signature = await trx.signTransaction(
|
|
2924
|
+
path,
|
|
2925
|
+
params.rawTxHex,
|
|
2926
|
+
params.tokenSignatures ?? []
|
|
2927
|
+
);
|
|
2928
|
+
return { signature };
|
|
2929
|
+
}
|
|
2930
|
+
);
|
|
2151
2931
|
}
|
|
2152
2932
|
async function tronSignMessage(ctx, sessionId, params) {
|
|
2153
2933
|
const path = normalizePath(params.path);
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
payload: { sessionId: sid }
|
|
2165
|
-
});
|
|
2166
|
-
return { signature };
|
|
2167
|
-
});
|
|
2934
|
+
return withLegacyChainCall(
|
|
2935
|
+
ctx,
|
|
2936
|
+
sessionId,
|
|
2937
|
+
{ appName: "Tron", needsConfirmation: true },
|
|
2938
|
+
async (sid) => {
|
|
2939
|
+
const trx = await _createTrx(ctx, sid);
|
|
2940
|
+
const signature = await trx.signPersonalMessage(path, params.messageHex);
|
|
2941
|
+
return { signature };
|
|
2942
|
+
}
|
|
2943
|
+
);
|
|
2168
2944
|
}
|
|
2169
2945
|
async function _createTrx(ctx, sessionId) {
|
|
2170
2946
|
const dmk = await ctx.getOrCreateDmk();
|
|
2171
2947
|
return new Trx(new DmkTransport(dmk, sessionId));
|
|
2172
2948
|
}
|
|
2173
|
-
async function _ensureTronAppOpen(ctx, sessionId) {
|
|
2174
|
-
const dmk = await ctx.getOrCreateDmk();
|
|
2175
|
-
const appManager = new AppManager(dmk);
|
|
2176
|
-
await appManager.ensureAppOpen(sessionId, "Tron", () => {
|
|
2177
|
-
ctx.emit("ui-event", {
|
|
2178
|
-
type: EConnectorInteraction3.ConfirmOpenApp,
|
|
2179
|
-
payload: { sessionId }
|
|
2180
|
-
});
|
|
2181
|
-
});
|
|
2182
|
-
}
|
|
2183
2949
|
|
|
2184
2950
|
// src/connector/LedgerConnectorBase.ts
|
|
2951
|
+
var METHOD_PREFIX_TO_APP_NAME = {
|
|
2952
|
+
evm: "Ethereum",
|
|
2953
|
+
btc: "Bitcoin",
|
|
2954
|
+
sol: "Solana",
|
|
2955
|
+
tron: "Tron"
|
|
2956
|
+
};
|
|
2957
|
+
var HARDWARE_ERROR_CODE_VALUES = new Set(
|
|
2958
|
+
Object.values(HardwareErrorCode7).filter((value) => typeof value === "number")
|
|
2959
|
+
);
|
|
2960
|
+
var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
2185
2961
|
async function defaultLedgerKitImporter(pkg) {
|
|
2186
2962
|
switch (pkg) {
|
|
2187
2963
|
case "@ledgerhq/device-management-kit":
|
|
@@ -2205,16 +2981,6 @@ var LedgerConnectorBase = class {
|
|
|
2205
2981
|
this._dmk = null;
|
|
2206
2982
|
this._eventHandlers = /* @__PURE__ */ new Map();
|
|
2207
2983
|
// ---------------------------------------------------------------------------
|
|
2208
|
-
// ConnectId <-> DMK path mapping
|
|
2209
|
-
//
|
|
2210
|
-
// DMK uses internal paths (BLE MAC, USB UUID) that may change across sessions.
|
|
2211
|
-
// _resolveConnectId() maps these to stable external IDs (BLE: "A58F", USB: same).
|
|
2212
|
-
// This bidirectional map is the SINGLE SOURCE OF TRUTH for all connectId usage.
|
|
2213
|
-
// ---------------------------------------------------------------------------
|
|
2214
|
-
this._connectIdToPath = /* @__PURE__ */ new Map();
|
|
2215
|
-
// "A58F" -> "D5:75:7D:4B:51:E8"
|
|
2216
|
-
this._pathToConnectId = /* @__PURE__ */ new Map();
|
|
2217
|
-
// ---------------------------------------------------------------------------
|
|
2218
2984
|
// Per-session DeviceAction cancellers
|
|
2219
2985
|
//
|
|
2220
2986
|
// Each chain handler registers its active DeviceAction's canceller via
|
|
@@ -2223,17 +2989,30 @@ var LedgerConnectorBase = class {
|
|
|
2223
2989
|
// unsubscribes the observable and releases DMK's IntentQueue slot.
|
|
2224
2990
|
// ---------------------------------------------------------------------------
|
|
2225
2991
|
this._cancellers = /* @__PURE__ */ new Map();
|
|
2992
|
+
// ---------------------------------------------------------------------------
|
|
2993
|
+
// Per-session DMK state subscriptions
|
|
2994
|
+
//
|
|
2995
|
+
// DMK only emits `device-disconnect` on the connector when `disconnect()`
|
|
2996
|
+
// is called explicitly. To pick up autonomous disconnects (USB unplug,
|
|
2997
|
+
// BLE drop, device sleep, DMK transport reset) we subscribe to
|
|
2998
|
+
// `_dmk.getDeviceSessionState({sessionId})` per active session and emit
|
|
2999
|
+
// `device-disconnect` ourselves the moment DMK reports the device went
|
|
3000
|
+
// to NOT_CONNECTED. The subscription is torn down on disconnect /
|
|
3001
|
+
// reset / observable completion to avoid leaks.
|
|
3002
|
+
// ---------------------------------------------------------------------------
|
|
3003
|
+
this._sessionStateSubs = /* @__PURE__ */ new Map();
|
|
2226
3004
|
this._createTransport = createTransport;
|
|
2227
3005
|
this.connectionType = options?.connectionType ?? "usb";
|
|
2228
3006
|
this._providedDmk = options?.dmk;
|
|
2229
3007
|
this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
|
|
3008
|
+
this._requirePreFlightScan = options?.requirePreFlightScan ?? false;
|
|
2230
3009
|
if (this._providedDmk) {
|
|
2231
3010
|
this._initManagers(this._providedDmk);
|
|
2232
3011
|
}
|
|
2233
3012
|
this._ctx = {
|
|
2234
3013
|
emit: (event, data) => this._emit(event, data),
|
|
2235
3014
|
invalidateSession: (sid) => this._invalidateSession(sid),
|
|
2236
|
-
wrapError: (err) => this._wrapError(err),
|
|
3015
|
+
wrapError: (err, opts) => this._wrapError(err, opts),
|
|
2237
3016
|
getOrCreateDmk: () => this._getOrCreateDmk(),
|
|
2238
3017
|
getDeviceManager: () => this._getDeviceManager(),
|
|
2239
3018
|
getSignerManager: () => this._getSignerManager(),
|
|
@@ -2244,38 +3023,23 @@ var LedgerConnectorBase = class {
|
|
|
2244
3023
|
importLedgerKit: this._importLedgerKit
|
|
2245
3024
|
};
|
|
2246
3025
|
}
|
|
2247
|
-
// "D5:75:7D:4B:51:E8" -> "A58F"
|
|
2248
|
-
/** Register a connectId <-> path mapping from a device descriptor. */
|
|
2249
|
-
_registerDeviceId(descriptor) {
|
|
2250
|
-
const connectId = this._resolveConnectId(descriptor);
|
|
2251
|
-
this._connectIdToPath.set(connectId, descriptor.path);
|
|
2252
|
-
this._pathToConnectId.set(descriptor.path, connectId);
|
|
2253
|
-
return connectId;
|
|
2254
|
-
}
|
|
2255
|
-
/** Get DMK path from external connectId. Falls back to connectId itself. */
|
|
2256
|
-
_getPathForConnectId(connectId) {
|
|
2257
|
-
return this._connectIdToPath.get(connectId) ?? connectId;
|
|
2258
|
-
}
|
|
2259
|
-
/** Get external connectId from DMK path. Falls back to path itself. */
|
|
2260
|
-
_getConnectIdForPath(path) {
|
|
2261
|
-
return this._pathToConnectId.get(path) ?? path;
|
|
2262
|
-
}
|
|
2263
3026
|
// ---------------------------------------------------------------------------
|
|
2264
3027
|
// Protected — hooks for subclasses
|
|
2265
3028
|
// ---------------------------------------------------------------------------
|
|
2266
3029
|
/**
|
|
2267
3030
|
* Resolve the connectId for a discovered device descriptor.
|
|
2268
3031
|
* Default: use the DMK path (ephemeral UUID).
|
|
2269
|
-
* Override in subclasses
|
|
3032
|
+
* Override in subclasses only when the public connectId differs from the transport path.
|
|
2270
3033
|
*/
|
|
2271
3034
|
_resolveConnectId(descriptor) {
|
|
2272
3035
|
return descriptor.path;
|
|
2273
3036
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
3037
|
+
/**
|
|
3038
|
+
* Authoritative discovery for this transport. Default = enumerate snapshot
|
|
3039
|
+
* (USB/HID). BLE overrides to drive from a live scan window since DMK's
|
|
3040
|
+
* enumerate() cache survives peripherals going offline.
|
|
3041
|
+
*/
|
|
3042
|
+
async _discoverDescriptors(dm) {
|
|
2279
3043
|
let descriptors = await dm.enumerate();
|
|
2280
3044
|
if (descriptors.length === 0) {
|
|
2281
3045
|
try {
|
|
@@ -2284,137 +3048,313 @@ var LedgerConnectorBase = class {
|
|
|
2284
3048
|
}
|
|
2285
3049
|
descriptors = await dm.enumerate();
|
|
2286
3050
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
3051
|
+
return descriptors;
|
|
3052
|
+
}
|
|
3053
|
+
// ---------------------------------------------------------------------------
|
|
3054
|
+
// IConnector -- Device discovery
|
|
3055
|
+
// ---------------------------------------------------------------------------
|
|
3056
|
+
async searchDevices() {
|
|
3057
|
+
const dm = await this._getDeviceManager();
|
|
3058
|
+
const descriptors = await this._discoverDescriptors(dm);
|
|
3059
|
+
const resolvedDescriptors = descriptors.map((d) => ({
|
|
3060
|
+
descriptor: d,
|
|
3061
|
+
connectId: this._resolveConnectId(d)
|
|
3062
|
+
}));
|
|
3063
|
+
const result = resolvedDescriptors.map(({ descriptor: d, connectId }) => ({
|
|
3064
|
+
connectId,
|
|
3065
|
+
deviceId: d.path,
|
|
3066
|
+
name: d.name || d.type || "Ledger",
|
|
3067
|
+
model: d.type,
|
|
3068
|
+
modelName: d.modelName,
|
|
3069
|
+
rssi: d.rssi,
|
|
3070
|
+
isConnectable: d.isConnectable,
|
|
3071
|
+
serialNumber: d.serialNumber
|
|
3072
|
+
}));
|
|
3073
|
+
debugLog(
|
|
3074
|
+
`[DMK] connector.searchDevices() return count=${result.length} ids=[${result.map((r) => r.connectId).join(",")}] paths=[${result.map((r) => r.deviceId).join(",")}]`
|
|
3075
|
+
);
|
|
2296
3076
|
return result;
|
|
2297
3077
|
}
|
|
2298
3078
|
// ---------------------------------------------------------------------------
|
|
2299
3079
|
// IConnector -- Connection
|
|
2300
3080
|
// ---------------------------------------------------------------------------
|
|
2301
3081
|
async connect(deviceId) {
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
const dmkPath = deviceId ? this._getPathForConnectId(deviceId) : void 0;
|
|
2305
|
-
let targetPath = dmkPath;
|
|
3082
|
+
const callerSuppliedConnectId = Boolean(deviceId);
|
|
3083
|
+
let targetPath = deviceId;
|
|
2306
3084
|
if (!targetPath) {
|
|
2307
|
-
const
|
|
2308
|
-
if (
|
|
3085
|
+
const discovered = await this.searchDevices();
|
|
3086
|
+
if (discovered.length === 0) {
|
|
2309
3087
|
throw new Error(
|
|
2310
|
-
`No Ledger device found. Make sure the device is connected${this.connectionType
|
|
3088
|
+
`No Ledger device found. Make sure the device is connected${isLedgerBleConnectionType(this.connectionType) ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
|
|
2311
3089
|
);
|
|
2312
3090
|
}
|
|
2313
|
-
targetPath =
|
|
3091
|
+
targetPath = discovered[0].deviceId;
|
|
2314
3092
|
}
|
|
2315
|
-
const externalConnectId =
|
|
3093
|
+
const externalConnectId = targetPath;
|
|
3094
|
+
const HANG_CEILING_MS = 5 * 6e4;
|
|
3095
|
+
const dmConnectWithObserve = async (dm, path) => {
|
|
3096
|
+
if (!isLedgerBleConnectionType(this.connectionType)) return dm.connect(path);
|
|
3097
|
+
let timeoutId;
|
|
3098
|
+
let timedOut = false;
|
|
3099
|
+
const connectPromise = dm.connect(path);
|
|
3100
|
+
const hangPromise = new Promise((_, reject) => {
|
|
3101
|
+
timeoutId = setTimeout(() => {
|
|
3102
|
+
timedOut = true;
|
|
3103
|
+
const err = new Error(
|
|
3104
|
+
`Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
|
|
3105
|
+
);
|
|
3106
|
+
err._tag = ERROR_TAG.BlePairingTimeout;
|
|
3107
|
+
err.code = HardwareErrorCode7.BlePairingTimeout;
|
|
3108
|
+
reject(err);
|
|
3109
|
+
}, HANG_CEILING_MS);
|
|
3110
|
+
});
|
|
3111
|
+
try {
|
|
3112
|
+
return await Promise.race([connectPromise, hangPromise]);
|
|
3113
|
+
} catch (err) {
|
|
3114
|
+
const e = err;
|
|
3115
|
+
debugLog("[DMK] dm.connect rejected \u2014 observed:", {
|
|
3116
|
+
_tag: e?._tag,
|
|
3117
|
+
message: e?.message,
|
|
3118
|
+
errorCode: e?.errorCode,
|
|
3119
|
+
statusCode: e?.statusCode,
|
|
3120
|
+
originalTag: e?.originalError?._tag,
|
|
3121
|
+
originalMessage: e?.originalError?.message
|
|
3122
|
+
});
|
|
3123
|
+
if (timedOut) {
|
|
3124
|
+
void connectPromise.then(
|
|
3125
|
+
(sessionId) => dm.disconnect(sessionId).catch(() => void 0),
|
|
3126
|
+
() => void 0
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
throw err;
|
|
3130
|
+
} finally {
|
|
3131
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
const isBleDirectConnect = isLedgerBleConnectionType(this.connectionType) && callerSuppliedConnectId;
|
|
3135
|
+
const throwNotAdvertising = () => {
|
|
3136
|
+
const err = new Error(
|
|
3137
|
+
"Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
|
|
3138
|
+
);
|
|
3139
|
+
err._tag = ERROR_TAG.DeviceNotAdvertising;
|
|
3140
|
+
err.code = HardwareErrorCode7.DeviceNotFound;
|
|
3141
|
+
throw err;
|
|
3142
|
+
};
|
|
2316
3143
|
const doConnect = async (path) => {
|
|
2317
|
-
const
|
|
3144
|
+
const dm = await this._getDeviceManager();
|
|
3145
|
+
if (isBleDirectConnect && !dm.hasDiscoveredDevice(path)) {
|
|
3146
|
+
const live = await dm.getLiveDevices(BLE_CONNECT_SCAN_TIMEOUT_MS);
|
|
3147
|
+
if (this._requirePreFlightScan && !live.some((d) => d.id === path)) {
|
|
3148
|
+
throwNotAdvertising();
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
let sessionId;
|
|
3152
|
+
try {
|
|
3153
|
+
sessionId = await dmConnectWithObserve(dm, path);
|
|
3154
|
+
} catch (err) {
|
|
3155
|
+
if (isBleDirectConnect && err?._tag === ERROR_TAG.DeviceNotInDiscoveryCache) {
|
|
3156
|
+
throwNotAdvertising();
|
|
3157
|
+
}
|
|
3158
|
+
throw err;
|
|
3159
|
+
}
|
|
3160
|
+
this._watchSessionState(sessionId, externalConnectId);
|
|
3161
|
+
const info = dm.getDiscoveredDeviceInfo(path);
|
|
2318
3162
|
const session = {
|
|
2319
3163
|
sessionId,
|
|
2320
3164
|
deviceInfo: {
|
|
2321
3165
|
vendor: "ledger",
|
|
2322
|
-
model: "unknown",
|
|
3166
|
+
model: info?.model ?? "unknown",
|
|
3167
|
+
modelName: info?.modelName,
|
|
2323
3168
|
firmwareVersion: "unknown",
|
|
2324
3169
|
deviceId: path,
|
|
2325
3170
|
connectId: externalConnectId,
|
|
2326
3171
|
connectionType: this.connectionType,
|
|
3172
|
+
rssi: info?.rssi,
|
|
2327
3173
|
capabilities: { persistentDeviceIdentity: false }
|
|
2328
3174
|
}
|
|
2329
3175
|
};
|
|
3176
|
+
const name = dm.getDeviceName(path) ?? "Ledger";
|
|
2330
3177
|
this._emit("device-connect", {
|
|
2331
|
-
device: {
|
|
2332
|
-
connectId: externalConnectId,
|
|
2333
|
-
deviceId: path,
|
|
2334
|
-
name: "Ledger"
|
|
2335
|
-
}
|
|
3178
|
+
device: { connectId: externalConnectId, deviceId: path, name }
|
|
2336
3179
|
});
|
|
2337
3180
|
return session;
|
|
2338
3181
|
};
|
|
2339
3182
|
try {
|
|
2340
3183
|
return await doConnect(targetPath);
|
|
2341
|
-
} catch {
|
|
3184
|
+
} catch (err) {
|
|
2342
3185
|
this._resetSignersAndSessions();
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
const descriptors = await dm2.enumerate();
|
|
2348
|
-
if (descriptors.length === 0) {
|
|
2349
|
-
throw new Error(
|
|
2350
|
-
`No Ledger device found after retry. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
|
|
2351
|
-
);
|
|
3186
|
+
if (isLedgerBleConnectionType(this.connectionType)) {
|
|
3187
|
+
const tag = err?._tag;
|
|
3188
|
+
if (isKnownConnectionTag(tag)) {
|
|
3189
|
+
throw err;
|
|
2352
3190
|
}
|
|
2353
|
-
|
|
3191
|
+
const wrapped = new Error(
|
|
3192
|
+
"Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
|
|
3193
|
+
);
|
|
3194
|
+
wrapped._tag = ERROR_TAG.BleGattBondingFailed;
|
|
3195
|
+
wrapped.code = HardwareErrorCode7.BlePairingTimeout;
|
|
3196
|
+
wrapped.originalError = err;
|
|
3197
|
+
throw wrapped;
|
|
3198
|
+
}
|
|
3199
|
+
if (err && typeof err === "object" && err._tag) {
|
|
3200
|
+
const taggedError = err instanceof Error ? err : Object.assign(
|
|
3201
|
+
new Error(err.message ?? "Ledger device error"),
|
|
3202
|
+
err
|
|
3203
|
+
);
|
|
3204
|
+
throw taggedError;
|
|
2354
3205
|
}
|
|
2355
|
-
|
|
3206
|
+
throw new Error("Ledger device appears unplugged. Plug in the device and try again.");
|
|
2356
3207
|
}
|
|
2357
3208
|
}
|
|
2358
3209
|
async disconnect(sessionId) {
|
|
2359
3210
|
if (!this._deviceManager) return;
|
|
2360
3211
|
const deviceId = this._deviceManager.getDeviceId(sessionId);
|
|
2361
3212
|
this._signerManager?.invalidate(sessionId);
|
|
3213
|
+
this._unwatchSessionState(sessionId);
|
|
2362
3214
|
await this._deviceManager.disconnect(sessionId);
|
|
2363
3215
|
if (deviceId) {
|
|
2364
3216
|
this._emit("device-disconnect", { connectId: deviceId });
|
|
2365
3217
|
}
|
|
2366
3218
|
}
|
|
3219
|
+
/**
|
|
3220
|
+
* Subscribe to DMK's per-session state observable so that any autonomous
|
|
3221
|
+
* disconnect (USB unplug, BLE drop, device sleep, transport reset) is
|
|
3222
|
+
* surfaced as a `device-disconnect` event — without this, the upstream
|
|
3223
|
+
* `LedgerAdapter._sessions` map would hold a dead session entry until
|
|
3224
|
+
* the next call hit `DeviceSessionNotFound`.
|
|
3225
|
+
*
|
|
3226
|
+
* Best-effort: any error subscribing is swallowed so a flaky DMK
|
|
3227
|
+
* doesn't break the connect path.
|
|
3228
|
+
*/
|
|
3229
|
+
_watchSessionState(sessionId, externalConnectId) {
|
|
3230
|
+
const dmk = this._dmk;
|
|
3231
|
+
if (!dmk) return;
|
|
3232
|
+
const previous = this._sessionStateSubs.get(sessionId);
|
|
3233
|
+
if (previous) {
|
|
3234
|
+
try {
|
|
3235
|
+
previous.unsubscribe();
|
|
3236
|
+
} catch {
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
try {
|
|
3240
|
+
const sub = dmk.getDeviceSessionState({ sessionId }).subscribe({
|
|
3241
|
+
next: (state) => {
|
|
3242
|
+
if (state?.deviceStatus === "NOT CONNECTED") {
|
|
3243
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3244
|
+
}
|
|
3245
|
+
},
|
|
3246
|
+
error: () => {
|
|
3247
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3248
|
+
},
|
|
3249
|
+
complete: () => {
|
|
3250
|
+
this._handleAutonomousDisconnect(sessionId, externalConnectId);
|
|
3251
|
+
}
|
|
3252
|
+
});
|
|
3253
|
+
this._sessionStateSubs.set(sessionId, sub);
|
|
3254
|
+
} catch (err) {
|
|
3255
|
+
debugLog("[DMK] _watchSessionState subscribe failed:", err);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
_unwatchSessionState(sessionId) {
|
|
3259
|
+
const sub = this._sessionStateSubs.get(sessionId);
|
|
3260
|
+
if (!sub) return;
|
|
3261
|
+
try {
|
|
3262
|
+
sub.unsubscribe();
|
|
3263
|
+
} catch {
|
|
3264
|
+
}
|
|
3265
|
+
this._sessionStateSubs.delete(sessionId);
|
|
3266
|
+
}
|
|
3267
|
+
_handleAutonomousDisconnect(sessionId, externalConnectId) {
|
|
3268
|
+
if (!this._sessionStateSubs.has(sessionId)) return;
|
|
3269
|
+
debugLog(
|
|
3270
|
+
"[DMK] autonomous disconnect detected \u2014 sessionId:",
|
|
3271
|
+
sessionId,
|
|
3272
|
+
"connectId:",
|
|
3273
|
+
externalConnectId
|
|
3274
|
+
);
|
|
3275
|
+
this._unwatchSessionState(sessionId);
|
|
3276
|
+
this._signerManager?.invalidate(sessionId);
|
|
3277
|
+
this._cancellers.get(sessionId)?.({
|
|
3278
|
+
code: HardwareErrorCode7.DeviceDisconnected,
|
|
3279
|
+
tag: "DeviceDisconnected",
|
|
3280
|
+
message: "Device disconnected"
|
|
3281
|
+
});
|
|
3282
|
+
this._cancellers.delete(sessionId);
|
|
3283
|
+
this._emit("device-disconnect", { connectId: externalConnectId });
|
|
3284
|
+
}
|
|
2367
3285
|
// ---------------------------------------------------------------------------
|
|
2368
3286
|
// IConnector -- Method dispatch
|
|
2369
3287
|
// ---------------------------------------------------------------------------
|
|
2370
3288
|
async call(sessionId, method, params) {
|
|
2371
3289
|
debugLog("[DMK] call:", method, JSON.stringify(params));
|
|
3290
|
+
try {
|
|
3291
|
+
return await this._dispatch(sessionId, method, params);
|
|
3292
|
+
} catch (err) {
|
|
3293
|
+
if (isAppStuckByApdu(err)) {
|
|
3294
|
+
throw Object.assign(new Error("Ledger app is unresponsive"), {
|
|
3295
|
+
code: HardwareErrorCode7.DeviceAppStuck,
|
|
3296
|
+
_tag: ERROR_TAG.DeviceAppStuck,
|
|
3297
|
+
originalError: err
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
if (isTransportStuck(err)) {
|
|
3301
|
+
throw Object.assign(new Error("Device communication interrupted, please retry"), {
|
|
3302
|
+
code: HardwareErrorCode7.TransportError,
|
|
3303
|
+
_tag: ERROR_TAG.DeviceTransportStuck,
|
|
3304
|
+
originalError: err
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
throw err;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
async _dispatch(sessionId, method, params) {
|
|
3311
|
+
const ctx = this._ctxForMethod(method);
|
|
2372
3312
|
switch (method) {
|
|
2373
3313
|
// EVM
|
|
2374
3314
|
case "evmGetAddress":
|
|
2375
|
-
return evmGetAddress(
|
|
3315
|
+
return evmGetAddress(ctx, sessionId, params);
|
|
2376
3316
|
case "evmSignTransaction":
|
|
2377
|
-
return evmSignTransaction(
|
|
3317
|
+
return evmSignTransaction(ctx, sessionId, params);
|
|
2378
3318
|
case "evmSignMessage":
|
|
2379
|
-
return evmSignMessage(
|
|
3319
|
+
return evmSignMessage(ctx, sessionId, params);
|
|
2380
3320
|
case "evmSignTypedData":
|
|
2381
|
-
return evmSignTypedData(
|
|
3321
|
+
return evmSignTypedData(ctx, sessionId, params);
|
|
2382
3322
|
// BTC
|
|
2383
3323
|
case "btcGetAddress":
|
|
2384
|
-
return btcGetAddress(
|
|
3324
|
+
return btcGetAddress(ctx, sessionId, params);
|
|
2385
3325
|
case "btcGetPublicKey":
|
|
2386
|
-
return btcGetPublicKey(
|
|
3326
|
+
return btcGetPublicKey(ctx, sessionId, params);
|
|
2387
3327
|
case "btcSignTransaction":
|
|
2388
|
-
return btcSignTransaction(
|
|
3328
|
+
return btcSignTransaction(ctx, sessionId, params);
|
|
2389
3329
|
case "btcSignPsbt":
|
|
2390
|
-
return btcSignPsbt(
|
|
3330
|
+
return btcSignPsbt(ctx, sessionId, params);
|
|
2391
3331
|
case "btcSignMessage":
|
|
2392
|
-
return btcSignMessage(
|
|
3332
|
+
return btcSignMessage(ctx, sessionId, params);
|
|
2393
3333
|
case "btcGetMasterFingerprint":
|
|
2394
3334
|
return btcGetMasterFingerprint(
|
|
2395
|
-
|
|
3335
|
+
ctx,
|
|
2396
3336
|
sessionId,
|
|
2397
3337
|
params
|
|
2398
3338
|
);
|
|
2399
3339
|
// SOL
|
|
2400
3340
|
case "solGetAddress":
|
|
2401
|
-
return solGetAddress(
|
|
3341
|
+
return solGetAddress(ctx, sessionId, params);
|
|
2402
3342
|
case "solSignTransaction":
|
|
2403
|
-
return solSignTransaction(
|
|
3343
|
+
return solSignTransaction(ctx, sessionId, params);
|
|
2404
3344
|
case "solSignMessage":
|
|
2405
|
-
return solSignMessage(
|
|
3345
|
+
return solSignMessage(ctx, sessionId, params);
|
|
2406
3346
|
// TRON
|
|
2407
3347
|
case "tronGetAddress":
|
|
2408
|
-
return tronGetAddress(
|
|
3348
|
+
return tronGetAddress(ctx, sessionId, params);
|
|
2409
3349
|
case "tronSignTransaction":
|
|
2410
|
-
return tronSignTransaction(
|
|
3350
|
+
return tronSignTransaction(ctx, sessionId, params);
|
|
2411
3351
|
case "tronSignMessage": {
|
|
2412
3352
|
const p = params;
|
|
2413
3353
|
const internalParams = {
|
|
2414
3354
|
path: p.path,
|
|
2415
3355
|
messageHex: p.messageHex
|
|
2416
3356
|
};
|
|
2417
|
-
return tronSignMessage(
|
|
3357
|
+
return tronSignMessage(ctx, sessionId, internalParams);
|
|
2418
3358
|
}
|
|
2419
3359
|
default:
|
|
2420
3360
|
throw new Error(`LedgerConnector: unknown method "${method}"`);
|
|
@@ -2507,14 +3447,13 @@ var LedgerConnectorBase = class {
|
|
|
2507
3447
|
}
|
|
2508
3448
|
/**
|
|
2509
3449
|
* Replace an old session with a new one after app switch.
|
|
2510
|
-
*
|
|
2511
|
-
* and emits device-connect so the adapter updates its _sessions Map.
|
|
3450
|
+
* Emits device-connect so the adapter updates its _sessions Map.
|
|
2512
3451
|
*/
|
|
2513
3452
|
_replaceSession(oldSessionId, _newSessionId) {
|
|
2514
3453
|
const dm = this._deviceManager;
|
|
2515
3454
|
if (!dm) return;
|
|
2516
3455
|
const oldDeviceId = dm.getDeviceId(oldSessionId);
|
|
2517
|
-
const connectId = oldDeviceId
|
|
3456
|
+
const connectId = oldDeviceId;
|
|
2518
3457
|
this._signerManager?.invalidate(oldSessionId);
|
|
2519
3458
|
if (connectId) {
|
|
2520
3459
|
this._emit("device-connect", {
|
|
@@ -2527,13 +3466,18 @@ var LedgerConnectorBase = class {
|
|
|
2527
3466
|
}
|
|
2528
3467
|
}
|
|
2529
3468
|
/**
|
|
2530
|
-
* Light reset: clear signer/session state but keep DMK
|
|
3469
|
+
* Light reset: clear signer/session state but keep DMK alive.
|
|
2531
3470
|
* Used by connect() retry — we want to re-discover with the same transport.
|
|
3471
|
+
*
|
|
3472
|
+
* Note: drops the device manager but tears down its RxJS subs first via
|
|
3473
|
+
* disposeKeepingDmk(). Bare null-assignment would leak _listenSub /
|
|
3474
|
+
* _discoverySub and double-scan after the next _initManagers().
|
|
2532
3475
|
*/
|
|
2533
3476
|
_resetSignersAndSessions() {
|
|
2534
3477
|
debugLog("[DMK] _resetSignersAndSessions called");
|
|
2535
3478
|
this._signerManager?.clearAll();
|
|
2536
3479
|
this._signerManager = null;
|
|
3480
|
+
this._deviceManager?.disposeKeepingDmk();
|
|
2537
3481
|
this._deviceManager = null;
|
|
2538
3482
|
}
|
|
2539
3483
|
_resetAll() {
|
|
@@ -2545,13 +3489,18 @@ var LedgerConnectorBase = class {
|
|
|
2545
3489
|
}
|
|
2546
3490
|
}
|
|
2547
3491
|
this._cancellers.clear();
|
|
3492
|
+
for (const sub of this._sessionStateSubs.values()) {
|
|
3493
|
+
try {
|
|
3494
|
+
sub.unsubscribe();
|
|
3495
|
+
} catch {
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
this._sessionStateSubs.clear();
|
|
2548
3499
|
this._signerManager?.clearAll();
|
|
2549
3500
|
this._deviceManager?.dispose();
|
|
2550
3501
|
this._deviceManager = null;
|
|
2551
3502
|
this._signerManager = null;
|
|
2552
3503
|
this._dmk = null;
|
|
2553
|
-
this._connectIdToPath.clear();
|
|
2554
|
-
this._pathToConnectId.clear();
|
|
2555
3504
|
}
|
|
2556
3505
|
// ---------------------------------------------------------------------------
|
|
2557
3506
|
// Private -- Events
|
|
@@ -2570,16 +3519,36 @@ var LedgerConnectorBase = class {
|
|
|
2570
3519
|
// ---------------------------------------------------------------------------
|
|
2571
3520
|
// Private -- Error handling
|
|
2572
3521
|
// ---------------------------------------------------------------------------
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3522
|
+
/**
|
|
3523
|
+
* Return a per-call ctx with the chain's Ledger app name pre-bound to
|
|
3524
|
+
* wrapError, so chain handlers don't need to repeat `{ defaultAppName: 'X' }`
|
|
3525
|
+
* at every catch site. Falls through unchanged for unknown methods.
|
|
3526
|
+
*/
|
|
3527
|
+
_ctxForMethod(method) {
|
|
3528
|
+
const prefix = /^(evm|btc|sol|tron)/.exec(method)?.[1];
|
|
3529
|
+
const defaultAppName = prefix ? METHOD_PREFIX_TO_APP_NAME[prefix] : void 0;
|
|
3530
|
+
if (!defaultAppName) return this._ctx;
|
|
3531
|
+
return {
|
|
3532
|
+
...this._ctx,
|
|
3533
|
+
wrapError: (err, opts) => this._wrapError(err, { defaultAppName, ...opts })
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
_wrapError(err, opts) {
|
|
2576
3537
|
const src = err && typeof err === "object" ? err : {};
|
|
3538
|
+
const hasSerializedCode = typeof src.code === "number" && HARDWARE_ERROR_CODE_VALUES.has(src.code);
|
|
3539
|
+
const mapped = hasSerializedCode ? {
|
|
3540
|
+
code: src.code,
|
|
3541
|
+
message: typeof src.message === "string" ? src.message : "Unknown Ledger error",
|
|
3542
|
+
appName: typeof src.appName === "string" ? src.appName : opts?.defaultAppName
|
|
3543
|
+
} : mapLedgerError(err, opts);
|
|
3544
|
+
const error = new Error(mapped.message);
|
|
2577
3545
|
Object.assign(error, {
|
|
2578
3546
|
code: mapped.code,
|
|
2579
3547
|
appName: mapped.appName,
|
|
2580
3548
|
_tag: src._tag,
|
|
2581
3549
|
errorCode: src.errorCode,
|
|
2582
|
-
_lastStep: src._lastStep
|
|
3550
|
+
_lastStep: src._lastStep,
|
|
3551
|
+
_deviceActionSteps: src._deviceActionSteps
|
|
2583
3552
|
});
|
|
2584
3553
|
Object.defineProperty(error, "originalError", {
|
|
2585
3554
|
value: err,
|
|
@@ -2590,13 +3559,6 @@ var LedgerConnectorBase = class {
|
|
|
2590
3559
|
return error;
|
|
2591
3560
|
}
|
|
2592
3561
|
};
|
|
2593
|
-
|
|
2594
|
-
// src/utils/bleIdentity.ts
|
|
2595
|
-
function extractBleHexId(name) {
|
|
2596
|
-
if (!name) return void 0;
|
|
2597
|
-
const match = name.match(/\b([0-9A-Fa-f]{4})$/);
|
|
2598
|
-
return match ? match[1].toUpperCase() : void 0;
|
|
2599
|
-
}
|
|
2600
3562
|
export {
|
|
2601
3563
|
AppManager,
|
|
2602
3564
|
DmkTransport,
|
|
@@ -2607,12 +3569,15 @@ export {
|
|
|
2607
3569
|
SignerEth,
|
|
2608
3570
|
SignerManager,
|
|
2609
3571
|
SignerSol,
|
|
3572
|
+
debugLog,
|
|
2610
3573
|
deviceActionToPromise,
|
|
2611
|
-
extractBleHexId,
|
|
2612
|
-
isDebugEnabled,
|
|
2613
3574
|
isDeviceLockedError,
|
|
3575
|
+
isLedgerBleConnectionType,
|
|
3576
|
+
isLedgerBleDescriptor,
|
|
3577
|
+
isLedgerDmkBleTransport,
|
|
2614
3578
|
ledgerFailure,
|
|
2615
3579
|
mapLedgerError,
|
|
2616
|
-
|
|
3580
|
+
offSdkEvent,
|
|
3581
|
+
onSdkEvent
|
|
2617
3582
|
};
|
|
2618
3583
|
//# sourceMappingURL=index.mjs.map
|