@onekeyfe/hwk-ledger-adapter 1.1.26-alpha.10
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 +628 -0
- package/dist/index.d.ts +628 -0
- package/dist/index.js +2720 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2684 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2720 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AppManager: () => AppManager,
|
|
34
|
+
DmkTransport: () => DmkTransport,
|
|
35
|
+
LedgerAdapter: () => LedgerAdapter,
|
|
36
|
+
LedgerConnectorBase: () => LedgerConnectorBase,
|
|
37
|
+
LedgerDeviceManager: () => LedgerDeviceManager,
|
|
38
|
+
SignerBtc: () => SignerBtc,
|
|
39
|
+
SignerEth: () => SignerEth,
|
|
40
|
+
SignerManager: () => SignerManager,
|
|
41
|
+
SignerSol: () => SignerSol,
|
|
42
|
+
deviceActionToPromise: () => deviceActionToPromise,
|
|
43
|
+
extractBleHexId: () => extractBleHexId,
|
|
44
|
+
isDeviceLockedError: () => isDeviceLockedError,
|
|
45
|
+
ledgerFailure: () => ledgerFailure,
|
|
46
|
+
mapLedgerError: () => mapLedgerError,
|
|
47
|
+
offSdkEvent: () => offSdkEvent,
|
|
48
|
+
onSdkEvent: () => onSdkEvent
|
|
49
|
+
});
|
|
50
|
+
module.exports = __toCommonJS(index_exports);
|
|
51
|
+
|
|
52
|
+
// src/adapter/LedgerAdapter.ts
|
|
53
|
+
var import_hwk_adapter_core2 = require("@onekeyfe/hwk-adapter-core");
|
|
54
|
+
|
|
55
|
+
// src/errors.ts
|
|
56
|
+
var import_hwk_adapter_core = require("@onekeyfe/hwk-adapter-core");
|
|
57
|
+
function ledgerFailure(code, error, appName) {
|
|
58
|
+
const payload = { error, code };
|
|
59
|
+
if (appName !== void 0) payload.appName = appName;
|
|
60
|
+
return { success: false, payload };
|
|
61
|
+
}
|
|
62
|
+
var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
|
|
63
|
+
var USER_REJECTED_CODES = /* @__PURE__ */ new Set(["6985", "27013"]);
|
|
64
|
+
var WRONG_APP_CODES = /* @__PURE__ */ new Set(["6e00", "28160", "6d00", "27904", "6a83", "27267"]);
|
|
65
|
+
var APP_NOT_INSTALLED_CODES = /* @__PURE__ */ new Set(["6807", "26631"]);
|
|
66
|
+
var STEP_BLIND_SIGN_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
|
|
67
|
+
function getEthAppErrorCode(err) {
|
|
68
|
+
if (!err || typeof err !== "object") return null;
|
|
69
|
+
const e = err;
|
|
70
|
+
if (e._tag === "EthAppCommandError" && typeof e.errorCode === "string") {
|
|
71
|
+
return e.errorCode.toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
const orig = e.originalError;
|
|
74
|
+
if (orig?._tag === "EthAppCommandError" && typeof orig.errorCode === "string") {
|
|
75
|
+
return orig.errorCode.toLowerCase();
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function extractApduHex(err) {
|
|
80
|
+
if (!err || typeof err !== "object") return null;
|
|
81
|
+
const e = err;
|
|
82
|
+
if (typeof e.errorCode === "string" && /^[0-9a-f]+$/i.test(e.errorCode)) {
|
|
83
|
+
return e.errorCode.toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
if (typeof e.statusCode === "number" && Number.isFinite(e.statusCode)) {
|
|
86
|
+
return e.statusCode.toString(16).padStart(4, "0");
|
|
87
|
+
}
|
|
88
|
+
if (typeof e.statusCode === "string" && /^[0-9a-f]+$/i.test(e.statusCode)) {
|
|
89
|
+
return e.statusCode.toLowerCase();
|
|
90
|
+
}
|
|
91
|
+
if (e.originalError != null) {
|
|
92
|
+
const nested = extractApduHex(e.originalError);
|
|
93
|
+
if (nested) return nested;
|
|
94
|
+
}
|
|
95
|
+
if (e.error != null && typeof e._tag === "string") {
|
|
96
|
+
const nested = extractApduHex(e.error);
|
|
97
|
+
if (nested) return nested;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function mapSolanaAppError(hex) {
|
|
102
|
+
switch (hex) {
|
|
103
|
+
case "6808":
|
|
104
|
+
return import_hwk_adapter_core.HardwareErrorCode.SolanaBlindSigningRequired;
|
|
105
|
+
default:
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function mapTronAppError(hex) {
|
|
110
|
+
switch (hex) {
|
|
111
|
+
case "6a8d":
|
|
112
|
+
return import_hwk_adapter_core.HardwareErrorCode.TronCustomContractRequired;
|
|
113
|
+
case "6a8b":
|
|
114
|
+
return import_hwk_adapter_core.HardwareErrorCode.TronDataSigningRequired;
|
|
115
|
+
case "6a8c":
|
|
116
|
+
return import_hwk_adapter_core.HardwareErrorCode.TronSignByHashRequired;
|
|
117
|
+
default:
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function mapBtcAppError(hex) {
|
|
122
|
+
switch (hex) {
|
|
123
|
+
case "b008":
|
|
124
|
+
return import_hwk_adapter_core.HardwareErrorCode.BtcWalletPolicyHmacMismatch;
|
|
125
|
+
case "b007":
|
|
126
|
+
return import_hwk_adapter_core.HardwareErrorCode.BtcUnexpectedState;
|
|
127
|
+
default:
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function mapEthAppError(ethCode, lastStep) {
|
|
132
|
+
switch (ethCode) {
|
|
133
|
+
case "6a80":
|
|
134
|
+
if (lastStep === STEP_BLIND_SIGN_FALLBACK) {
|
|
135
|
+
return import_hwk_adapter_core.HardwareErrorCode.EvmBlindSigningRequired;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
case "6984":
|
|
139
|
+
return import_hwk_adapter_core.HardwareErrorCode.EvmClearSignPluginMissing;
|
|
140
|
+
case "6a84":
|
|
141
|
+
return import_hwk_adapter_core.HardwareErrorCode.EvmDataTooLarge;
|
|
142
|
+
case "6501":
|
|
143
|
+
return import_hwk_adapter_core.HardwareErrorCode.EvmTxTypeNotSupported;
|
|
144
|
+
case "911c":
|
|
145
|
+
return import_hwk_adapter_core.HardwareErrorCode.AppTooOld;
|
|
146
|
+
default:
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function isDeviceLockedError(err) {
|
|
151
|
+
if (!err || typeof err !== "object") return false;
|
|
152
|
+
const e = err;
|
|
153
|
+
if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
|
|
154
|
+
if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
|
|
155
|
+
if (e._tag === "DeviceLockedError") return true;
|
|
156
|
+
if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
|
|
157
|
+
if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
function hasStatusCode(err, codeSet) {
|
|
161
|
+
if (!err || typeof err !== "object") return false;
|
|
162
|
+
const e = err;
|
|
163
|
+
if (e.errorCode != null && codeSet.has(String(e.errorCode))) return true;
|
|
164
|
+
if (e.statusCode != null && codeSet.has(String(e.statusCode))) return true;
|
|
165
|
+
if (e.originalError != null && hasStatusCode(e.originalError, codeSet)) return true;
|
|
166
|
+
if (e.error != null && e._tag && hasStatusCode(e.error, codeSet)) return true;
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
function isUserRejectedError(err) {
|
|
170
|
+
if (!err || typeof err !== "object") return false;
|
|
171
|
+
const e = err;
|
|
172
|
+
if (e._tag === "UserRefusedOnDevice") return true;
|
|
173
|
+
if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
|
|
174
|
+
if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
function isWrongAppError(err) {
|
|
178
|
+
if (!err || typeof err !== "object") return false;
|
|
179
|
+
const e = err;
|
|
180
|
+
if (e._tag === "WrongAppOpenedError" || e._tag === "InvalidStatusWordError") {
|
|
181
|
+
if (hasStatusCode(err, WRONG_APP_CODES)) return true;
|
|
182
|
+
}
|
|
183
|
+
if (typeof e.message === "string") {
|
|
184
|
+
const msg = e.message.toLowerCase();
|
|
185
|
+
if (msg.includes("wrong app") || msg.includes("open the") || msg.includes("cla not supported"))
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (hasStatusCode(err, WRONG_APP_CODES)) return true;
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
function isAppNotInstalledError(err) {
|
|
192
|
+
if (!err || typeof err !== "object") return false;
|
|
193
|
+
const e = err;
|
|
194
|
+
if (e._tag === "OpenAppCommandError") return true;
|
|
195
|
+
if (typeof e.message === "string" && /unknown application/i.test(e.message)) return true;
|
|
196
|
+
if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
function isDeviceDisconnectedError(err) {
|
|
200
|
+
if (!err || typeof err !== "object") return false;
|
|
201
|
+
const e = err;
|
|
202
|
+
if (e._tag === "DeviceNotRecognizedError" || e._tag === "DeviceSessionNotFound") return true;
|
|
203
|
+
if (typeof e.message === "string") {
|
|
204
|
+
const msg = e.message.toLowerCase();
|
|
205
|
+
if (msg.includes("disconnected") || msg.includes("not found") || msg.includes("no device") || msg.includes("unplugged"))
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
var TIMEOUT_TAGS = /* @__PURE__ */ new Set([
|
|
211
|
+
"DeviceExchangeTimeoutError",
|
|
212
|
+
"SendApduTimeoutError",
|
|
213
|
+
"SendCommandTimeoutError"
|
|
214
|
+
]);
|
|
215
|
+
function isTimeoutError(err) {
|
|
216
|
+
if (!err || typeof err !== "object") return false;
|
|
217
|
+
const e = err;
|
|
218
|
+
if (typeof e._tag === "string" && TIMEOUT_TAGS.has(e._tag)) return true;
|
|
219
|
+
if (e.code === import_hwk_adapter_core.HardwareErrorCode.OperationTimeout) return true;
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
function mapLedgerError(err, opts) {
|
|
223
|
+
let originalMessage = "Unknown Ledger error";
|
|
224
|
+
if (err instanceof Error) {
|
|
225
|
+
originalMessage = err.message;
|
|
226
|
+
} else if (err && typeof err === "object") {
|
|
227
|
+
const e = err;
|
|
228
|
+
originalMessage = String(e.message ?? e._tag ?? e.type ?? JSON.stringify(err));
|
|
229
|
+
}
|
|
230
|
+
let code;
|
|
231
|
+
if (isDeviceLockedError(err)) {
|
|
232
|
+
code = import_hwk_adapter_core.HardwareErrorCode.DeviceLocked;
|
|
233
|
+
} else if (isUserRejectedError(err)) {
|
|
234
|
+
code = import_hwk_adapter_core.HardwareErrorCode.UserRejected;
|
|
235
|
+
} else if (isWrongAppError(err)) {
|
|
236
|
+
code = import_hwk_adapter_core.HardwareErrorCode.WrongApp;
|
|
237
|
+
} else if (isAppNotInstalledError(err)) {
|
|
238
|
+
code = import_hwk_adapter_core.HardwareErrorCode.AppNotOpen;
|
|
239
|
+
} else if (isDeviceDisconnectedError(err)) {
|
|
240
|
+
code = import_hwk_adapter_core.HardwareErrorCode.DeviceDisconnected;
|
|
241
|
+
} else if (isTimeoutError(err)) {
|
|
242
|
+
code = import_hwk_adapter_core.HardwareErrorCode.OperationTimeout;
|
|
243
|
+
} else {
|
|
244
|
+
const ethCode = getEthAppErrorCode(err);
|
|
245
|
+
const lastStep = err && typeof err === "object" ? err._lastStep : void 0;
|
|
246
|
+
const ethMapped = ethCode ? mapEthAppError(ethCode, lastStep) : null;
|
|
247
|
+
const apduHex = ethMapped ? null : extractApduHex(err);
|
|
248
|
+
const chainMapped = apduHex ? mapSolanaAppError(apduHex) ?? mapTronAppError(apduHex) ?? mapBtcAppError(apduHex) : null;
|
|
249
|
+
code = ethMapped ?? chainMapped ?? import_hwk_adapter_core.HardwareErrorCode.UnknownError;
|
|
250
|
+
}
|
|
251
|
+
const errAppName = err && typeof err === "object" ? err.appName : void 0;
|
|
252
|
+
const appName = errAppName ?? opts?.defaultAppName;
|
|
253
|
+
return { code, message: (0, import_hwk_adapter_core.enrichErrorMessage)(code, originalMessage), appName };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/utils/sdkEventBus.ts
|
|
257
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
258
|
+
function onSdkEvent(listener) {
|
|
259
|
+
listeners.add(listener);
|
|
260
|
+
return () => {
|
|
261
|
+
listeners.delete(listener);
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function offSdkEvent(listener) {
|
|
265
|
+
listeners.delete(listener);
|
|
266
|
+
}
|
|
267
|
+
function emitSdkEvent(event) {
|
|
268
|
+
if (listeners.size === 0) return;
|
|
269
|
+
for (const listener of listeners) {
|
|
270
|
+
try {
|
|
271
|
+
listener(event);
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function emitLog(level, ...args) {
|
|
277
|
+
if (listeners.size === 0) return;
|
|
278
|
+
const message = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
|
|
279
|
+
emitSdkEvent({ type: "log", level, message });
|
|
280
|
+
}
|
|
281
|
+
function safeStringify(value) {
|
|
282
|
+
if (value instanceof Error) {
|
|
283
|
+
return value.stack ? `${value.message}
|
|
284
|
+
${value.stack}` : value.message;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
return JSON.stringify(value);
|
|
288
|
+
} catch {
|
|
289
|
+
return String(value);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/utils/debugLog.ts
|
|
294
|
+
function debugLog(...args) {
|
|
295
|
+
emitLog("debug", ...args);
|
|
296
|
+
}
|
|
297
|
+
function debugError(...args) {
|
|
298
|
+
emitLog("error", ...args);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/adapter/LedgerAdapter.ts
|
|
302
|
+
function formatDeviceMismatchError(expected, actual) {
|
|
303
|
+
return `Wrong device: expected ${expected}, got ${actual}`;
|
|
304
|
+
}
|
|
305
|
+
var _LedgerAdapter = class _LedgerAdapter {
|
|
306
|
+
constructor(connector, options) {
|
|
307
|
+
this.vendor = "ledger";
|
|
308
|
+
this.emitter = new import_hwk_adapter_core2.TypedEventEmitter();
|
|
309
|
+
// Device cache: tracks discovered devices from connector events
|
|
310
|
+
this._discoveredDevices = /* @__PURE__ */ new Map();
|
|
311
|
+
// Session tracking: maps connectId -> sessionId
|
|
312
|
+
this._sessions = /* @__PURE__ */ new Map();
|
|
313
|
+
this._uiRegistry = new import_hwk_adapter_core2.UiRequestRegistry();
|
|
314
|
+
// Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
|
|
315
|
+
this._connectingPromise = null;
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Event translation
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
this.deviceConnectHandler = (data) => {
|
|
320
|
+
const deviceInfo = this.connectorDeviceToDeviceInfo(data.device);
|
|
321
|
+
this._discoveredDevices.set(deviceInfo.connectId, deviceInfo);
|
|
322
|
+
this._sessions.delete(deviceInfo.connectId);
|
|
323
|
+
this.emitter.emit(import_hwk_adapter_core2.DEVICE.CONNECT, {
|
|
324
|
+
type: import_hwk_adapter_core2.DEVICE.CONNECT,
|
|
325
|
+
payload: deviceInfo
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
this.deviceDisconnectHandler = (data) => {
|
|
329
|
+
this._discoveredDevices.delete(data.connectId);
|
|
330
|
+
this._sessions.delete(data.connectId);
|
|
331
|
+
this.emitter.emit(import_hwk_adapter_core2.DEVICE.DISCONNECT, {
|
|
332
|
+
type: import_hwk_adapter_core2.DEVICE.DISCONNECT,
|
|
333
|
+
payload: { connectId: data.connectId }
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
// Forward low-level connector 'ui-event' (the four EConnectorInteraction values)
|
|
337
|
+
// to the public hw.emitter so consumers only need to subscribe in one place
|
|
338
|
+
// (hw.on instead of also reaching into connector.on).
|
|
339
|
+
this.uiEventForwarder = (event) => {
|
|
340
|
+
this.emitter.emit("ui-event", event);
|
|
341
|
+
};
|
|
342
|
+
this.connector = connector;
|
|
343
|
+
this._handleSelectDevice = options?.handleSelectDevice ?? false;
|
|
344
|
+
this._jobQueue = new import_hwk_adapter_core2.DeviceJobQueue({
|
|
345
|
+
emit: (event, data) => this.emitter.emit(event, data),
|
|
346
|
+
uiRegistry: this._uiRegistry
|
|
347
|
+
});
|
|
348
|
+
this.registerEventListeners();
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Classify a method's interruptibility.
|
|
352
|
+
* - Signing / typed data / transaction → 'confirm' (user may decide via preemption UI)
|
|
353
|
+
* - Read-only queries (getAddress / getPublicKey / getMasterFingerprint) → 'safe'
|
|
354
|
+
* (auto-cancels any pending read for the same device)
|
|
355
|
+
*/
|
|
356
|
+
static _getInterruptibility(method) {
|
|
357
|
+
if (method.toLowerCase().includes("sign")) return "confirm";
|
|
358
|
+
return "safe";
|
|
359
|
+
}
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Transport
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// Transport is decided at connector creation time. These methods
|
|
364
|
+
// satisfy the IHardwareWallet interface with sensible defaults.
|
|
365
|
+
get activeTransport() {
|
|
366
|
+
return this.connector.connectionType === "ble" ? "ble" : "hid";
|
|
367
|
+
}
|
|
368
|
+
getAvailableTransports() {
|
|
369
|
+
return this.activeTransport ? [this.activeTransport] : [];
|
|
370
|
+
}
|
|
371
|
+
async switchTransport(_type) {
|
|
372
|
+
}
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// Lifecycle
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
async init(_config) {
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Clear cached device/session state without tearing down the adapter.
|
|
380
|
+
* Call before retrying after errors or when the device state may be stale.
|
|
381
|
+
* The next operation will re-discover and re-connect automatically.
|
|
382
|
+
*/
|
|
383
|
+
resetState() {
|
|
384
|
+
this._discoveredDevices.clear();
|
|
385
|
+
this._sessions.clear();
|
|
386
|
+
this._connectingPromise = null;
|
|
387
|
+
this._uiRegistry.reset();
|
|
388
|
+
this._jobQueue.clear();
|
|
389
|
+
}
|
|
390
|
+
async dispose() {
|
|
391
|
+
this._uiRegistry.reset();
|
|
392
|
+
this._jobQueue.clear();
|
|
393
|
+
this.unregisterEventListeners();
|
|
394
|
+
this.connector.reset();
|
|
395
|
+
this._discoveredDevices.clear();
|
|
396
|
+
this._sessions.clear();
|
|
397
|
+
this.emitter.removeAllListeners();
|
|
398
|
+
}
|
|
399
|
+
uiResponse(response) {
|
|
400
|
+
this._uiRegistry.resolve(response.type, response.payload);
|
|
401
|
+
}
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// Device management
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
async searchDevices() {
|
|
406
|
+
await this._ensureDevicePermission();
|
|
407
|
+
const devices = await this.connector.searchDevices();
|
|
408
|
+
debugLog("[DMK] adapter.searchDevices raw:", JSON.stringify(devices));
|
|
409
|
+
this._discoveredDevices.clear();
|
|
410
|
+
for (const d of devices) {
|
|
411
|
+
if (d.connectId) {
|
|
412
|
+
this._discoveredDevices.set(d.connectId, this.connectorDeviceToDeviceInfo(d));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (this._discoveredDevices.size === 0) {
|
|
416
|
+
await this._ensureDevicePermission();
|
|
417
|
+
}
|
|
418
|
+
return Array.from(this._discoveredDevices.values());
|
|
419
|
+
}
|
|
420
|
+
async connectDevice(connectId) {
|
|
421
|
+
await this._ensureDevicePermission(connectId);
|
|
422
|
+
try {
|
|
423
|
+
const session = await this.connector.connect(connectId);
|
|
424
|
+
this._sessions.set(connectId, session.sessionId);
|
|
425
|
+
if (session.deviceInfo) {
|
|
426
|
+
this._discoveredDevices.set(connectId, session.deviceInfo);
|
|
427
|
+
}
|
|
428
|
+
return (0, import_hwk_adapter_core2.success)(connectId);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
return this.errorToFailure(err);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async disconnectDevice(connectId) {
|
|
434
|
+
const sessionId = this._sessions.get(connectId);
|
|
435
|
+
if (sessionId) {
|
|
436
|
+
await this.connector.disconnect(sessionId);
|
|
437
|
+
this._sessions.delete(connectId);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async getDeviceInfo(connectId, deviceId) {
|
|
441
|
+
await this._ensureDevicePermission(connectId, deviceId);
|
|
442
|
+
const cached = this._discoveredDevices.get(connectId) ?? Array.from(this._discoveredDevices.values()).find((d) => d.deviceId === deviceId);
|
|
443
|
+
if (cached) {
|
|
444
|
+
return (0, import_hwk_adapter_core2.success)(cached);
|
|
445
|
+
}
|
|
446
|
+
return (0, import_hwk_adapter_core2.failure)(
|
|
447
|
+
import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound,
|
|
448
|
+
"Device not found in cache. Call searchDevices() or wait for a device-connected event first."
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
getSupportedChains() {
|
|
452
|
+
return ["evm", "btc", "sol", "tron"];
|
|
453
|
+
}
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// Chain call helper
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
async callChain(connectId, deviceId, chain, method, params, skipFingerprint = false) {
|
|
458
|
+
await this._ensureDevicePermission(connectId, deviceId);
|
|
459
|
+
try {
|
|
460
|
+
const result = await this.connectorCall(connectId, method, params, {
|
|
461
|
+
chain,
|
|
462
|
+
deviceId,
|
|
463
|
+
skipFingerprint
|
|
464
|
+
});
|
|
465
|
+
return (0, import_hwk_adapter_core2.success)(result);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
return this.errorToFailure(err);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Batch version of callChain — checks permission once,
|
|
472
|
+
* fingerprint is verified on the first call inside connectorCall.
|
|
473
|
+
*/
|
|
474
|
+
async callChainBatch(connectId, deviceId, chain, method, params, onProgress, skipFingerprint = false) {
|
|
475
|
+
await this._ensureDevicePermission(connectId, deviceId);
|
|
476
|
+
const results = [];
|
|
477
|
+
for (let i = 0; i < params.length; i++) {
|
|
478
|
+
try {
|
|
479
|
+
const result = await this.connectorCall(connectId, method, params[i], {
|
|
480
|
+
chain,
|
|
481
|
+
deviceId,
|
|
482
|
+
// Only verify fingerprint on the first call in the batch
|
|
483
|
+
skipFingerprint: skipFingerprint || i > 0
|
|
484
|
+
});
|
|
485
|
+
results.push(result);
|
|
486
|
+
onProgress?.({ index: i, total: params.length });
|
|
487
|
+
} catch (err) {
|
|
488
|
+
return this.errorToFailure(err);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return (0, import_hwk_adapter_core2.success)(results);
|
|
492
|
+
}
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
// EVM chain methods
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
evmGetAddress(connectId, deviceId, params) {
|
|
497
|
+
return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
|
|
498
|
+
}
|
|
499
|
+
evmGetAddresses(connectId, deviceId, params, onProgress) {
|
|
500
|
+
return this.callChainBatch(
|
|
501
|
+
connectId,
|
|
502
|
+
deviceId,
|
|
503
|
+
"evm",
|
|
504
|
+
"evmGetAddress",
|
|
505
|
+
params,
|
|
506
|
+
onProgress
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
evmSignTransaction(connectId, deviceId, params) {
|
|
510
|
+
return this.callChain(connectId, deviceId, "evm", "evmSignTransaction", params);
|
|
511
|
+
}
|
|
512
|
+
evmSignMessage(connectId, deviceId, params) {
|
|
513
|
+
return this.callChain(connectId, deviceId, "evm", "evmSignMessage", params);
|
|
514
|
+
}
|
|
515
|
+
evmSignTypedData(connectId, deviceId, params) {
|
|
516
|
+
return this.callChain(connectId, deviceId, "evm", "evmSignTypedData", params);
|
|
517
|
+
}
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// BTC chain methods
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
btcGetAddress(connectId, deviceId, params) {
|
|
522
|
+
return this.callChain(connectId, deviceId, "btc", "btcGetAddress", params);
|
|
523
|
+
}
|
|
524
|
+
btcGetAddresses(connectId, deviceId, params, onProgress) {
|
|
525
|
+
return this.callChainBatch(
|
|
526
|
+
connectId,
|
|
527
|
+
deviceId,
|
|
528
|
+
"btc",
|
|
529
|
+
"btcGetAddress",
|
|
530
|
+
params,
|
|
531
|
+
onProgress
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
btcGetPublicKey(connectId, deviceId, params) {
|
|
535
|
+
return this.callChain(connectId, deviceId, "btc", "btcGetPublicKey", params);
|
|
536
|
+
}
|
|
537
|
+
btcSignTransaction(connectId, deviceId, params) {
|
|
538
|
+
return this.callChain(connectId, deviceId, "btc", "btcSignTransaction", params);
|
|
539
|
+
}
|
|
540
|
+
btcSignPsbt(connectId, deviceId, params) {
|
|
541
|
+
return this.callChain(connectId, deviceId, "btc", "btcSignPsbt", params);
|
|
542
|
+
}
|
|
543
|
+
btcSignMessage(connectId, deviceId, params) {
|
|
544
|
+
return this.callChain(connectId, deviceId, "btc", "btcSignMessage", params);
|
|
545
|
+
}
|
|
546
|
+
btcGetMasterFingerprint(connectId, deviceId) {
|
|
547
|
+
return this.callChain(
|
|
548
|
+
connectId,
|
|
549
|
+
deviceId,
|
|
550
|
+
"btc",
|
|
551
|
+
"btcGetMasterFingerprint",
|
|
552
|
+
{}
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
// SOL chain methods
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
solGetAddress(connectId, deviceId, params) {
|
|
559
|
+
return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
|
|
560
|
+
}
|
|
561
|
+
solGetAddresses(connectId, deviceId, params, onProgress) {
|
|
562
|
+
return this.callChainBatch(
|
|
563
|
+
connectId,
|
|
564
|
+
deviceId,
|
|
565
|
+
"sol",
|
|
566
|
+
"solGetAddress",
|
|
567
|
+
params,
|
|
568
|
+
onProgress
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
solSignTransaction(connectId, deviceId, params) {
|
|
572
|
+
return this.callChain(connectId, deviceId, "sol", "solSignTransaction", params);
|
|
573
|
+
}
|
|
574
|
+
solSignMessage(connectId, deviceId, params) {
|
|
575
|
+
return this.callChain(connectId, deviceId, "sol", "solSignMessage", params);
|
|
576
|
+
}
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
// TRON chain methods
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
tronGetAddress(connectId, deviceId, params) {
|
|
581
|
+
return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params, true);
|
|
582
|
+
}
|
|
583
|
+
tronGetAddresses(connectId, deviceId, params, onProgress) {
|
|
584
|
+
return this.callChainBatch(
|
|
585
|
+
connectId,
|
|
586
|
+
deviceId,
|
|
587
|
+
"tron",
|
|
588
|
+
"tronGetAddress",
|
|
589
|
+
params,
|
|
590
|
+
onProgress,
|
|
591
|
+
true
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
tronSignTransaction(connectId, deviceId, params) {
|
|
595
|
+
return this.callChain(
|
|
596
|
+
connectId,
|
|
597
|
+
deviceId,
|
|
598
|
+
"tron",
|
|
599
|
+
"tronSignTransaction",
|
|
600
|
+
params,
|
|
601
|
+
true
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
tronSignMessage(connectId, deviceId, params) {
|
|
605
|
+
return this.callChain(
|
|
606
|
+
connectId,
|
|
607
|
+
deviceId,
|
|
608
|
+
"tron",
|
|
609
|
+
"tronSignMessage",
|
|
610
|
+
params,
|
|
611
|
+
true
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
on(event, listener) {
|
|
615
|
+
this.emitter.on(event, listener);
|
|
616
|
+
}
|
|
617
|
+
off(event, listener) {
|
|
618
|
+
this.emitter.off(event, listener);
|
|
619
|
+
}
|
|
620
|
+
cancel(connectId) {
|
|
621
|
+
const sessionId = this._sessions.get(connectId) ?? connectId;
|
|
622
|
+
this._jobQueue.forceCancelActive(connectId || "__ledger_default__");
|
|
623
|
+
void this.connector.cancel(sessionId);
|
|
624
|
+
}
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// Chain fingerprint
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
async getChainFingerprint(connectId, deviceId, chain) {
|
|
629
|
+
debugLog(
|
|
630
|
+
"[LedgerAdapter] getChainFingerprint called, chain:",
|
|
631
|
+
chain,
|
|
632
|
+
"connectId:",
|
|
633
|
+
connectId || "(empty)",
|
|
634
|
+
"sessions:",
|
|
635
|
+
this._sessions.size
|
|
636
|
+
);
|
|
637
|
+
await this._ensureDevicePermission(connectId, deviceId);
|
|
638
|
+
debugLog("[LedgerAdapter] getChainFingerprint permission ok, computing fingerprint");
|
|
639
|
+
try {
|
|
640
|
+
const fingerprint = await this._computeChainFingerprint(
|
|
641
|
+
chain,
|
|
642
|
+
(method, params) => this.connectorCall(connectId, method, params)
|
|
643
|
+
);
|
|
644
|
+
debugLog("[LedgerAdapter] getChainFingerprint result:", fingerprint?.substring(0, 20));
|
|
645
|
+
return (0, import_hwk_adapter_core2.success)(fingerprint);
|
|
646
|
+
} catch (err) {
|
|
647
|
+
debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
|
|
648
|
+
return this.errorToFailure(err);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Verify fingerprint using an existing sessionId directly.
|
|
653
|
+
* Safe to call inside connectorCall without causing queue deadlock.
|
|
654
|
+
*/
|
|
655
|
+
async _verifyDeviceFingerprintWithSession(sessionId, deviceId, chain) {
|
|
656
|
+
if (!deviceId) return { success: true };
|
|
657
|
+
try {
|
|
658
|
+
const fingerprint = await this._computeChainFingerprint(
|
|
659
|
+
chain,
|
|
660
|
+
(method, params) => this.connector.call(sessionId, method, params)
|
|
661
|
+
);
|
|
662
|
+
if (fingerprint === deviceId) {
|
|
663
|
+
return { success: true };
|
|
664
|
+
}
|
|
665
|
+
return { success: false, expected: deviceId, actual: fingerprint };
|
|
666
|
+
} catch (err) {
|
|
667
|
+
const mapped = mapLedgerError(err);
|
|
668
|
+
if (mapped.code === import_hwk_adapter_core2.HardwareErrorCode.WrongApp || mapped.code === import_hwk_adapter_core2.HardwareErrorCode.DeviceLocked) {
|
|
669
|
+
return { success: true };
|
|
670
|
+
}
|
|
671
|
+
throw err;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Compute the chain fingerprint via a caller-supplied call strategy.
|
|
676
|
+
*
|
|
677
|
+
* Chains with a native device-side identity primitive (BTC → BIP32 master
|
|
678
|
+
* fingerprint) short-circuit at the top and return it verbatim, so the value
|
|
679
|
+
* stays reusable for higher-level use (BIP380 descriptors, PSBT signing).
|
|
680
|
+
*
|
|
681
|
+
* All other chains derive a fixed-path address and run it through
|
|
682
|
+
* `deriveDeviceFingerprint` to produce an opaque seed identifier.
|
|
683
|
+
*
|
|
684
|
+
* The two callers (`getChainFingerprint` / `_verifyDeviceFingerprintWithSession`)
|
|
685
|
+
* differ only in the underlying call mechanism, which is injected as `callMethod`
|
|
686
|
+
* to avoid queue deadlocks when running inside `connectorCall`.
|
|
687
|
+
*/
|
|
688
|
+
async _computeChainFingerprint(chain, callMethod) {
|
|
689
|
+
if (chain === "btc") {
|
|
690
|
+
const result = await callMethod("btcGetMasterFingerprint", {});
|
|
691
|
+
return result.masterFingerprint;
|
|
692
|
+
}
|
|
693
|
+
const path = import_hwk_adapter_core2.CHAIN_FINGERPRINT_PATHS[chain];
|
|
694
|
+
let address;
|
|
695
|
+
if (chain === "evm") {
|
|
696
|
+
address = (await callMethod("evmGetAddress", { path, showOnDevice: false })).address.toLowerCase();
|
|
697
|
+
} else if (chain === "sol") {
|
|
698
|
+
address = (await callMethod("solGetAddress", { path, showOnDevice: false })).address;
|
|
699
|
+
} else if (chain === "tron") {
|
|
700
|
+
address = (await callMethod("tronGetAddress", { path, showOnDevice: false })).address;
|
|
701
|
+
} else {
|
|
702
|
+
throw new Error(`Unsupported chain for fingerprint: ${chain}`);
|
|
703
|
+
}
|
|
704
|
+
return (0, import_hwk_adapter_core2.deriveDeviceFingerprint)(address);
|
|
705
|
+
}
|
|
706
|
+
// Ledger WebUSB won't expose a locked device, so we can't auto-detect unlock.
|
|
707
|
+
// The user must press Confirm after unlocking, which triggers a search retry.
|
|
708
|
+
// If `signal` is provided, an abort cancels the pending UI request so the
|
|
709
|
+
// registry slot is released and a stale RECEIVE_DEVICE_CONNECT won't land in
|
|
710
|
+
// a future request.
|
|
711
|
+
async _waitForDeviceConnect(signal) {
|
|
712
|
+
if (signal?.aborted) {
|
|
713
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
714
|
+
}
|
|
715
|
+
this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT, {
|
|
716
|
+
type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT,
|
|
717
|
+
payload: {
|
|
718
|
+
message: "Please connect and unlock your Ledger device"
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
const waitPromise = this._uiRegistry.wait(
|
|
722
|
+
import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT
|
|
723
|
+
);
|
|
724
|
+
let payload;
|
|
725
|
+
if (signal) {
|
|
726
|
+
const onAbort = () => {
|
|
727
|
+
this._uiRegistry.cancel(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT);
|
|
728
|
+
};
|
|
729
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
730
|
+
try {
|
|
731
|
+
payload = await waitPromise;
|
|
732
|
+
} finally {
|
|
733
|
+
signal.removeEventListener("abort", onAbort);
|
|
734
|
+
}
|
|
735
|
+
} else {
|
|
736
|
+
payload = await waitPromise;
|
|
737
|
+
}
|
|
738
|
+
if (!payload?.confirmed) {
|
|
739
|
+
throw Object.assign(new Error("User cancelled Ledger connection"), {
|
|
740
|
+
_tag: "DeviceNotRecognizedError"
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async ensureConnected(connectId) {
|
|
745
|
+
if (connectId && this._sessions.has(connectId)) {
|
|
746
|
+
return connectId;
|
|
747
|
+
}
|
|
748
|
+
if (this._sessions.size > 0) {
|
|
749
|
+
const firstKey = this._sessions.keys().next().value;
|
|
750
|
+
return firstKey;
|
|
751
|
+
}
|
|
752
|
+
if (this._connectingPromise) {
|
|
753
|
+
return this._connectingPromise;
|
|
754
|
+
}
|
|
755
|
+
this._connectingPromise = this._doConnect();
|
|
756
|
+
try {
|
|
757
|
+
return await this._connectingPromise;
|
|
758
|
+
} finally {
|
|
759
|
+
this._connectingPromise = null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async _doConnect() {
|
|
763
|
+
for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
|
|
764
|
+
const devices = await this.searchDevices();
|
|
765
|
+
if (devices.length > 0) {
|
|
766
|
+
return this._connectFirstOrSelect(devices);
|
|
767
|
+
}
|
|
768
|
+
if (attempt < _LedgerAdapter.MAX_DEVICE_RETRY - 1) {
|
|
769
|
+
await this._waitForDeviceConnect();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
throw Object.assign(
|
|
773
|
+
new Error(
|
|
774
|
+
"No Ledger device found after multiple attempts. Please connect and unlock your device."
|
|
775
|
+
),
|
|
776
|
+
{ _tag: "DeviceNotRecognizedError" }
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
async _connectFirstOrSelect(devices) {
|
|
780
|
+
const chosenConnectId = devices.length === 1 ? devices[0].connectId : await this._chooseDeviceFromList(devices);
|
|
781
|
+
const result = await this.connectDevice(chosenConnectId);
|
|
782
|
+
if (!result.success) {
|
|
783
|
+
throw Object.assign(new Error(result.payload.error), {
|
|
784
|
+
_tag: "DeviceNotRecognizedError"
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
return chosenConnectId;
|
|
788
|
+
}
|
|
789
|
+
async _chooseDeviceFromList(devices) {
|
|
790
|
+
if (!this._handleSelectDevice) {
|
|
791
|
+
debugLog(
|
|
792
|
+
`[DMK] Multiple Ledger devices found (${devices.length}); handleSelectDevice=false, picking first (${devices[0].connectId}).`
|
|
793
|
+
);
|
|
794
|
+
return devices[0].connectId;
|
|
795
|
+
}
|
|
796
|
+
this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE, {
|
|
797
|
+
type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE,
|
|
798
|
+
payload: { devices }
|
|
799
|
+
});
|
|
800
|
+
const response = await this._uiRegistry.wait(
|
|
801
|
+
import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE
|
|
802
|
+
);
|
|
803
|
+
const chosen = devices.find((d) => d.connectId === response?.sdkConnectId);
|
|
804
|
+
if (!chosen) {
|
|
805
|
+
throw Object.assign(
|
|
806
|
+
new Error(`Selected sdkConnectId '${response?.sdkConnectId}' not in discovered list`),
|
|
807
|
+
{ _tag: "DeviceNotRecognizedError" }
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
return chosen.connectId;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Call the connector with automatic session resolution and disconnect retry.
|
|
814
|
+
*
|
|
815
|
+
* 1. Resolves a valid connectId via ensureConnected()
|
|
816
|
+
* 2. Looks up sessionId from _sessions
|
|
817
|
+
* 3. Calls connector.call()
|
|
818
|
+
* 4. On disconnect error: clears stale session, re-connects, retries once
|
|
819
|
+
*/
|
|
820
|
+
async connectorCall(connectId, method, params, fingerprint) {
|
|
821
|
+
debugLog("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
|
|
822
|
+
const queueKey = connectId || "__ledger_default__";
|
|
823
|
+
const interruptibility = _LedgerAdapter._getInterruptibility(method);
|
|
824
|
+
return this._jobQueue.enqueue(
|
|
825
|
+
queueKey,
|
|
826
|
+
async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint),
|
|
827
|
+
{ interruptibility, label: method }
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Race a promise against an abort signal. If the signal fires, rejects with the
|
|
832
|
+
* signal's reason (or a generic Error). The underlying connector.call() cannot
|
|
833
|
+
* actually be cancelled on Ledger DMK, but the caller gets the abort immediately.
|
|
834
|
+
*/
|
|
835
|
+
static _abortable(signal, promise) {
|
|
836
|
+
if (signal.aborted) {
|
|
837
|
+
return Promise.reject(
|
|
838
|
+
signal.reason ?? new Error("Aborted")
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
return new Promise((resolve, reject) => {
|
|
842
|
+
const onAbort = () => {
|
|
843
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
844
|
+
};
|
|
845
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
846
|
+
promise.then(
|
|
847
|
+
(value) => {
|
|
848
|
+
signal.removeEventListener("abort", onAbort);
|
|
849
|
+
resolve(value);
|
|
850
|
+
},
|
|
851
|
+
(err) => {
|
|
852
|
+
signal.removeEventListener("abort", onAbort);
|
|
853
|
+
reject(err);
|
|
854
|
+
}
|
|
855
|
+
);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
/** Throw an AbortError if signal is already aborted. */
|
|
859
|
+
static _throwIfAborted(signal) {
|
|
860
|
+
if (signal.aborted) {
|
|
861
|
+
throw signal.reason ?? new Error("Aborted");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
|
|
865
|
+
async _runConnectorCall(connectId, method, params, signal, fingerprint) {
|
|
866
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
867
|
+
const resolvedConnectId = await _LedgerAdapter._abortable(
|
|
868
|
+
signal,
|
|
869
|
+
this.ensureConnected(connectId)
|
|
870
|
+
);
|
|
871
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
872
|
+
const sessionId = this._sessions.get(resolvedConnectId);
|
|
873
|
+
debugLog(
|
|
874
|
+
"[LedgerAdapter] connectorCall resolved:",
|
|
875
|
+
method,
|
|
876
|
+
"resolvedConnectId:",
|
|
877
|
+
resolvedConnectId,
|
|
878
|
+
"sessionId:",
|
|
879
|
+
sessionId
|
|
880
|
+
);
|
|
881
|
+
if (!sessionId) {
|
|
882
|
+
throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
|
|
883
|
+
_tag: "DeviceSessionNotFound"
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
|
|
887
|
+
const fp = await _LedgerAdapter._abortable(
|
|
888
|
+
signal,
|
|
889
|
+
this._verifyDeviceFingerprintWithSession(sessionId, fingerprint.deviceId, fingerprint.chain)
|
|
890
|
+
);
|
|
891
|
+
if (!fp.success) {
|
|
892
|
+
throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
|
|
893
|
+
code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
try {
|
|
898
|
+
return await _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
|
|
899
|
+
} catch (err) {
|
|
900
|
+
if (signal.aborted) throw err;
|
|
901
|
+
const errObj = err;
|
|
902
|
+
debugLog("[LedgerAdapter] connectorCall error:", method, {
|
|
903
|
+
message: errObj?.message,
|
|
904
|
+
_tag: errObj?._tag,
|
|
905
|
+
errorCode: errObj?.errorCode,
|
|
906
|
+
statusCode: errObj?.statusCode,
|
|
907
|
+
isDisconnected: isDeviceDisconnectedError(err),
|
|
908
|
+
isLocked: isDeviceLockedError(err)
|
|
909
|
+
});
|
|
910
|
+
if (isDeviceDisconnectedError(err)) {
|
|
911
|
+
debugLog("[LedgerAdapter] disconnected, retrying with fresh connection...");
|
|
912
|
+
this._discoveredDevices.clear();
|
|
913
|
+
return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
|
|
914
|
+
}
|
|
915
|
+
if (isDeviceLockedError(err)) {
|
|
916
|
+
await this._waitForDeviceConnect(signal);
|
|
917
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
918
|
+
return _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
|
|
919
|
+
}
|
|
920
|
+
if (isTimeoutError(err)) {
|
|
921
|
+
debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
|
|
922
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
923
|
+
return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
|
|
924
|
+
}
|
|
925
|
+
throw err;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/** Clear stale session, reconnect, and retry the call. */
|
|
929
|
+
async _retryWithFreshConnection(resolvedConnectId, method, params, signal, originalErr) {
|
|
930
|
+
this._sessions.delete(resolvedConnectId);
|
|
931
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
932
|
+
const retryConnectId = await this.ensureConnected();
|
|
933
|
+
_LedgerAdapter._throwIfAborted(signal);
|
|
934
|
+
const retrySessionId = this._sessions.get(retryConnectId);
|
|
935
|
+
if (!retrySessionId) {
|
|
936
|
+
throw originalErr;
|
|
937
|
+
}
|
|
938
|
+
return _LedgerAdapter._abortable(signal, this.connector.call(retrySessionId, method, params));
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Ensure OS-level device permission (Bluetooth / USB) before proceeding.
|
|
942
|
+
*
|
|
943
|
+
* Emits `REQUEST_DEVICE_PERMISSION` and awaits the consumer's
|
|
944
|
+
* `RECEIVE_DEVICE_PERMISSION` reply (60s budget covers "probe → system
|
|
945
|
+
* prompt → user tap" plus a generous margin). If the consumer never wires
|
|
946
|
+
* a handler or never replies, the wait times out and the operation fails
|
|
947
|
+
* fast so scanners/callers don't hang silently.
|
|
948
|
+
*
|
|
949
|
+
* - No connectId (searchDevices): environment-level permission
|
|
950
|
+
* - With connectId (business methods): device-level permission
|
|
951
|
+
*/
|
|
952
|
+
async _ensureDevicePermission(connectId, deviceId) {
|
|
953
|
+
const transportType = this.activeTransport ?? "hid";
|
|
954
|
+
const waitPromise = this._uiRegistry.wait(
|
|
955
|
+
import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
956
|
+
{ timeoutMs: 6e4 }
|
|
957
|
+
);
|
|
958
|
+
this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION, {
|
|
959
|
+
type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION,
|
|
960
|
+
payload: { transportType, connectId, deviceId }
|
|
961
|
+
});
|
|
962
|
+
const { granted } = await waitPromise;
|
|
963
|
+
if (!granted) {
|
|
964
|
+
throw Object.assign(new Error("Device permission denied"), {
|
|
965
|
+
code: import_hwk_adapter_core2.HardwareErrorCode.DevicePermissionDenied
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Convert a thrown error to a Response failure.
|
|
971
|
+
* Uses mapLedgerError to parse Ledger DMK error codes into HardwareErrorCode values.
|
|
972
|
+
*/
|
|
973
|
+
errorToFailure(err) {
|
|
974
|
+
debugError("[LedgerAdapter] error:", err);
|
|
975
|
+
if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
|
|
976
|
+
const e = err;
|
|
977
|
+
return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName);
|
|
978
|
+
}
|
|
979
|
+
const mapped = mapLedgerError(err);
|
|
980
|
+
return ledgerFailure(mapped.code, mapped.message, mapped.appName);
|
|
981
|
+
}
|
|
982
|
+
registerEventListeners() {
|
|
983
|
+
this.connector.on("device-connect", this.deviceConnectHandler);
|
|
984
|
+
this.connector.on("device-disconnect", this.deviceDisconnectHandler);
|
|
985
|
+
this.connector.on("ui-event", this.uiEventForwarder);
|
|
986
|
+
}
|
|
987
|
+
unregisterEventListeners() {
|
|
988
|
+
this.connector.off("device-connect", this.deviceConnectHandler);
|
|
989
|
+
this.connector.off("device-disconnect", this.deviceDisconnectHandler);
|
|
990
|
+
this.connector.off("ui-event", this.uiEventForwarder);
|
|
991
|
+
}
|
|
992
|
+
// ---------------------------------------------------------------------------
|
|
993
|
+
// Device info mapping
|
|
994
|
+
// ---------------------------------------------------------------------------
|
|
995
|
+
connectorDeviceToDeviceInfo(device) {
|
|
996
|
+
const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
|
|
997
|
+
return {
|
|
998
|
+
vendor: "ledger",
|
|
999
|
+
model: device.model ?? "unknown",
|
|
1000
|
+
firmwareVersion: "",
|
|
1001
|
+
deviceId: device.deviceId,
|
|
1002
|
+
connectId: device.connectId,
|
|
1003
|
+
label: device.name,
|
|
1004
|
+
connectionType: isBle ? "ble" : "usb",
|
|
1005
|
+
capabilities: device.capabilities
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
// ---------------------------------------------------------------------------
|
|
1010
|
+
// Private helpers
|
|
1011
|
+
// ---------------------------------------------------------------------------
|
|
1012
|
+
/**
|
|
1013
|
+
* Ensure at least one device is connected and return a valid connectId.
|
|
1014
|
+
*
|
|
1015
|
+
* - If a session already exists for the given connectId, reuse it.
|
|
1016
|
+
* - If ANY session exists (Ledger IDs are ephemeral), reuse it.
|
|
1017
|
+
* - Otherwise: search → 1 device: auto-connect, multiple: ask user, 0: throw.
|
|
1018
|
+
*/
|
|
1019
|
+
_LedgerAdapter.MAX_DEVICE_RETRY = 3;
|
|
1020
|
+
var LedgerAdapter = _LedgerAdapter;
|
|
1021
|
+
|
|
1022
|
+
// src/device/LedgerDeviceManager.ts
|
|
1023
|
+
var LedgerDeviceManager = class {
|
|
1024
|
+
constructor(dmk) {
|
|
1025
|
+
this._discovered = /* @__PURE__ */ new Map();
|
|
1026
|
+
this._sessions = /* @__PURE__ */ new Map();
|
|
1027
|
+
// deviceId → sessionId
|
|
1028
|
+
this._sessionToDevice = /* @__PURE__ */ new Map();
|
|
1029
|
+
// sessionId → deviceId
|
|
1030
|
+
this._listenSub = null;
|
|
1031
|
+
/**
|
|
1032
|
+
* Trigger browser device selection (WebHID requestDevice).
|
|
1033
|
+
* Starts discovery for a short period, then stops.
|
|
1034
|
+
*/
|
|
1035
|
+
/**
|
|
1036
|
+
* Start BLE discovery if not already running.
|
|
1037
|
+
* Does NOT stop — DMK keeps scanning in the background so that
|
|
1038
|
+
* listenToAvailableDevices() always has fresh data.
|
|
1039
|
+
*/
|
|
1040
|
+
this._discoverySub = null;
|
|
1041
|
+
this._dmk = dmk;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* One-shot enumeration: subscribe to listenToAvailableDevices,
|
|
1045
|
+
* take the first emission, unsubscribe, return DeviceDescriptors.
|
|
1046
|
+
*/
|
|
1047
|
+
enumerate() {
|
|
1048
|
+
debugLog("[DMK] enumerate() called, dmk exists:", !!this._dmk);
|
|
1049
|
+
return new Promise((resolve) => {
|
|
1050
|
+
let resolved = false;
|
|
1051
|
+
let syncResult = null;
|
|
1052
|
+
let sub = null;
|
|
1053
|
+
sub = this._dmk.listenToAvailableDevices({}).subscribe({
|
|
1054
|
+
next: (devices) => {
|
|
1055
|
+
if (resolved) return;
|
|
1056
|
+
resolved = true;
|
|
1057
|
+
this._discovered.clear();
|
|
1058
|
+
debugLog("[DMK] enumerate raw devices:", devices.length);
|
|
1059
|
+
for (const d of devices) {
|
|
1060
|
+
this._discovered.set(d.id, d);
|
|
1061
|
+
}
|
|
1062
|
+
if (sub) {
|
|
1063
|
+
sub.unsubscribe();
|
|
1064
|
+
resolve(
|
|
1065
|
+
devices.map((d) => ({
|
|
1066
|
+
path: d.id,
|
|
1067
|
+
type: d.deviceModel.model,
|
|
1068
|
+
name: d.name,
|
|
1069
|
+
transport: d.transport
|
|
1070
|
+
}))
|
|
1071
|
+
);
|
|
1072
|
+
} else {
|
|
1073
|
+
syncResult = devices;
|
|
1074
|
+
}
|
|
1075
|
+
},
|
|
1076
|
+
error: () => {
|
|
1077
|
+
if (!resolved) {
|
|
1078
|
+
resolved = true;
|
|
1079
|
+
resolve([]);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
if (syncResult !== null) {
|
|
1084
|
+
sub.unsubscribe();
|
|
1085
|
+
const devices = syncResult;
|
|
1086
|
+
resolve(
|
|
1087
|
+
devices.map((d) => ({
|
|
1088
|
+
path: d.id,
|
|
1089
|
+
type: d.deviceModel.model,
|
|
1090
|
+
name: d.name,
|
|
1091
|
+
transport: d.transport
|
|
1092
|
+
}))
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Continuous listening: tracks device connect/disconnect via diffing.
|
|
1099
|
+
*/
|
|
1100
|
+
listen(onChange) {
|
|
1101
|
+
this.stopListening();
|
|
1102
|
+
let previousIds = /* @__PURE__ */ new Set();
|
|
1103
|
+
this._listenSub = this._dmk.listenToAvailableDevices({}).subscribe({
|
|
1104
|
+
next: (devices) => {
|
|
1105
|
+
const currentIds = new Set(devices.map((d) => d.id));
|
|
1106
|
+
for (const d of devices) {
|
|
1107
|
+
this._discovered.set(d.id, d);
|
|
1108
|
+
debugLog("[DMK] listen device:", d.id, d.name);
|
|
1109
|
+
if (!previousIds.has(d.id)) {
|
|
1110
|
+
onChange({
|
|
1111
|
+
type: "device-connected",
|
|
1112
|
+
descriptor: {
|
|
1113
|
+
path: d.id,
|
|
1114
|
+
type: d.deviceModel.model,
|
|
1115
|
+
name: d.name,
|
|
1116
|
+
transport: d.transport
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
for (const id of previousIds) {
|
|
1122
|
+
if (!currentIds.has(id)) {
|
|
1123
|
+
this._discovered.delete(id);
|
|
1124
|
+
onChange({ type: "device-disconnected", descriptor: { path: id } });
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
previousIds = currentIds;
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
stopListening() {
|
|
1132
|
+
this._listenSub?.unsubscribe();
|
|
1133
|
+
this._listenSub = null;
|
|
1134
|
+
}
|
|
1135
|
+
requestDevice() {
|
|
1136
|
+
if (this._discoverySub) {
|
|
1137
|
+
return Promise.resolve();
|
|
1138
|
+
}
|
|
1139
|
+
debugLog("[DMK] requestDevice() starting persistent BLE scan");
|
|
1140
|
+
this._discoverySub = this._dmk.startDiscovering({}).subscribe({
|
|
1141
|
+
next: (d) => {
|
|
1142
|
+
debugLog("[DMK] BLE discovered:", d.name || d.id);
|
|
1143
|
+
this._discovered.set(d.id, d);
|
|
1144
|
+
},
|
|
1145
|
+
error: (err) => {
|
|
1146
|
+
debugError("[DMK] BLE scan error:", err);
|
|
1147
|
+
this._discoverySub = null;
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
return Promise.resolve();
|
|
1151
|
+
}
|
|
1152
|
+
/** Connect to a previously discovered device. Returns sessionId. */
|
|
1153
|
+
async connect(deviceId) {
|
|
1154
|
+
const device = this._discovered.get(deviceId);
|
|
1155
|
+
if (!device) {
|
|
1156
|
+
throw new Error(`Device "${deviceId}" not found. Call enumerate() or listen() first.`);
|
|
1157
|
+
}
|
|
1158
|
+
const sessionId = await this._dmk.connect({ device });
|
|
1159
|
+
this._sessions.set(deviceId, sessionId);
|
|
1160
|
+
this._sessionToDevice.set(sessionId, deviceId);
|
|
1161
|
+
return sessionId;
|
|
1162
|
+
}
|
|
1163
|
+
/** Disconnect a session. */
|
|
1164
|
+
async disconnect(sessionId) {
|
|
1165
|
+
await this._dmk.disconnect({ sessionId });
|
|
1166
|
+
const deviceId = this._sessionToDevice.get(sessionId);
|
|
1167
|
+
if (deviceId) this._sessions.delete(deviceId);
|
|
1168
|
+
this._sessionToDevice.delete(sessionId);
|
|
1169
|
+
}
|
|
1170
|
+
getSessionId(deviceId) {
|
|
1171
|
+
return this._sessions.get(deviceId);
|
|
1172
|
+
}
|
|
1173
|
+
getDeviceId(sessionId) {
|
|
1174
|
+
return this._sessionToDevice.get(sessionId);
|
|
1175
|
+
}
|
|
1176
|
+
/** Get the underlying DMK instance (needed by SignerManager). */
|
|
1177
|
+
getDmk() {
|
|
1178
|
+
return this._dmk;
|
|
1179
|
+
}
|
|
1180
|
+
stopDiscovery() {
|
|
1181
|
+
if (this._discoverySub) {
|
|
1182
|
+
this._discoverySub.unsubscribe();
|
|
1183
|
+
this._discoverySub = null;
|
|
1184
|
+
this._dmk.stopDiscovering();
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
dispose() {
|
|
1188
|
+
this.stopListening();
|
|
1189
|
+
this.stopDiscovery();
|
|
1190
|
+
this._discovered.clear();
|
|
1191
|
+
this._sessions.clear();
|
|
1192
|
+
this._sessionToDevice.clear();
|
|
1193
|
+
this._dmk.close();
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
// src/signer/SignerManager.ts
|
|
1198
|
+
var import_device_signer_kit_ethereum = require("@ledgerhq/device-signer-kit-ethereum");
|
|
1199
|
+
var import_context_module = require("@ledgerhq/context-module");
|
|
1200
|
+
|
|
1201
|
+
// src/signer/SignerEth.ts
|
|
1202
|
+
var import_hwk_adapter_core3 = require("@onekeyfe/hwk-adapter-core");
|
|
1203
|
+
|
|
1204
|
+
// src/signer/deviceActionToPromise.ts
|
|
1205
|
+
var import_device_management_kit = require("@ledgerhq/device-management-kit");
|
|
1206
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1207
|
+
function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOUT_MS, onRegisterCanceller) {
|
|
1208
|
+
return new Promise((resolve, reject) => {
|
|
1209
|
+
let settled = false;
|
|
1210
|
+
let lastStep;
|
|
1211
|
+
let sub;
|
|
1212
|
+
let timer = null;
|
|
1213
|
+
const cancelAction = () => {
|
|
1214
|
+
try {
|
|
1215
|
+
sub?.unsubscribe();
|
|
1216
|
+
} catch {
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
action.cancel();
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
const resetTimer = () => {
|
|
1224
|
+
if (timer) clearTimeout(timer);
|
|
1225
|
+
if (timeoutMs > 0) {
|
|
1226
|
+
timer = setTimeout(() => {
|
|
1227
|
+
if (!settled) {
|
|
1228
|
+
settled = true;
|
|
1229
|
+
cancelAction();
|
|
1230
|
+
reject(new Error("Device action timed out \u2014 device may be locked or disconnected"));
|
|
1231
|
+
}
|
|
1232
|
+
}, timeoutMs);
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
resetTimer();
|
|
1236
|
+
if (onRegisterCanceller) {
|
|
1237
|
+
onRegisterCanceller(() => {
|
|
1238
|
+
if (settled) return;
|
|
1239
|
+
settled = true;
|
|
1240
|
+
if (timer) clearTimeout(timer);
|
|
1241
|
+
cancelAction();
|
|
1242
|
+
reject(new Error("Device action cancelled"));
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
debugLog("[DMK-Observable] subscribing to action.observable...");
|
|
1246
|
+
sub = action.observable.subscribe({
|
|
1247
|
+
next: (state) => {
|
|
1248
|
+
if (settled) return;
|
|
1249
|
+
resetTimer();
|
|
1250
|
+
const step = state?.intermediateValue?.step;
|
|
1251
|
+
if (step) lastStep = step;
|
|
1252
|
+
debugLog(
|
|
1253
|
+
"[DMK-Observable] state:",
|
|
1254
|
+
JSON.stringify({
|
|
1255
|
+
status: state.status,
|
|
1256
|
+
intermediateValue: state.status === import_device_management_kit.DeviceActionStatus.Pending ? state.intermediateValue : void 0,
|
|
1257
|
+
hasOutput: state.status === import_device_management_kit.DeviceActionStatus.Completed,
|
|
1258
|
+
hasError: state.status === import_device_management_kit.DeviceActionStatus.Error
|
|
1259
|
+
})
|
|
1260
|
+
);
|
|
1261
|
+
if (state.status === import_device_management_kit.DeviceActionStatus.Completed) {
|
|
1262
|
+
settled = true;
|
|
1263
|
+
if (timer) clearTimeout(timer);
|
|
1264
|
+
onInteraction?.("interaction-complete");
|
|
1265
|
+
sub?.unsubscribe();
|
|
1266
|
+
resolve(state.output);
|
|
1267
|
+
} else if (state.status === import_device_management_kit.DeviceActionStatus.Error) {
|
|
1268
|
+
settled = true;
|
|
1269
|
+
if (timer) clearTimeout(timer);
|
|
1270
|
+
onInteraction?.("interaction-complete");
|
|
1271
|
+
sub?.unsubscribe();
|
|
1272
|
+
rejectWithLastStep(state.error, lastStep, reject);
|
|
1273
|
+
} else if (state.status === import_device_management_kit.DeviceActionStatus.Pending && onInteraction) {
|
|
1274
|
+
const interaction = state.intermediateValue?.requiredUserInteraction;
|
|
1275
|
+
if (interaction && interaction !== "none") {
|
|
1276
|
+
onInteraction(String(interaction));
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
},
|
|
1280
|
+
error: (err) => {
|
|
1281
|
+
if (!settled) {
|
|
1282
|
+
settled = true;
|
|
1283
|
+
if (timer) clearTimeout(timer);
|
|
1284
|
+
sub?.unsubscribe();
|
|
1285
|
+
rejectWithLastStep(err, lastStep, reject);
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1288
|
+
complete: () => {
|
|
1289
|
+
if (!settled) {
|
|
1290
|
+
settled = true;
|
|
1291
|
+
if (timer) clearTimeout(timer);
|
|
1292
|
+
reject(new Error("Device action completed without result"));
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
function rejectWithLastStep(err, lastStep, reject) {
|
|
1299
|
+
if (err && typeof err === "object" && lastStep) {
|
|
1300
|
+
try {
|
|
1301
|
+
Object.defineProperty(err, "_lastStep", {
|
|
1302
|
+
value: lastStep,
|
|
1303
|
+
configurable: true,
|
|
1304
|
+
enumerable: false,
|
|
1305
|
+
writable: true
|
|
1306
|
+
});
|
|
1307
|
+
} catch {
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
reject(err);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/signer/SignerEth.ts
|
|
1314
|
+
var INTERACTIVE_TIMEOUT_MS = 5 * 6e4;
|
|
1315
|
+
var SignerEth = class {
|
|
1316
|
+
// eslint-disable-next-line no-useless-constructor, no-empty-function
|
|
1317
|
+
constructor(_sdk) {
|
|
1318
|
+
this._sdk = _sdk;
|
|
1319
|
+
}
|
|
1320
|
+
async getAddress(derivationPath, options) {
|
|
1321
|
+
const checkOnDevice = options?.checkOnDevice ?? false;
|
|
1322
|
+
debugLog("[DMK] getAddress \u2192 DMK:", { derivationPath, checkOnDevice });
|
|
1323
|
+
const action = this._sdk.getAddress(derivationPath, {
|
|
1324
|
+
checkOnDevice
|
|
1325
|
+
});
|
|
1326
|
+
const timeout = checkOnDevice ? INTERACTIVE_TIMEOUT_MS : void 0;
|
|
1327
|
+
return deviceActionToPromise(
|
|
1328
|
+
action,
|
|
1329
|
+
this.onInteraction,
|
|
1330
|
+
timeout,
|
|
1331
|
+
this.onRegisterCanceller
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
async signTransaction(derivationPath, serializedTxHex) {
|
|
1335
|
+
const action = this._sdk.signTransaction(derivationPath, (0, import_hwk_adapter_core3.hexToBytes)(serializedTxHex));
|
|
1336
|
+
return deviceActionToPromise(
|
|
1337
|
+
action,
|
|
1338
|
+
this.onInteraction,
|
|
1339
|
+
INTERACTIVE_TIMEOUT_MS,
|
|
1340
|
+
this.onRegisterCanceller
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
async signMessage(derivationPath, message) {
|
|
1344
|
+
const action = this._sdk.signMessage(derivationPath, (0, import_hwk_adapter_core3.hexToBytes)(message));
|
|
1345
|
+
return deviceActionToPromise(
|
|
1346
|
+
action,
|
|
1347
|
+
this.onInteraction,
|
|
1348
|
+
INTERACTIVE_TIMEOUT_MS,
|
|
1349
|
+
this.onRegisterCanceller
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
async signTypedData(derivationPath, data) {
|
|
1353
|
+
const action = this._sdk.signTypedData(derivationPath, data);
|
|
1354
|
+
return deviceActionToPromise(
|
|
1355
|
+
action,
|
|
1356
|
+
this.onInteraction,
|
|
1357
|
+
INTERACTIVE_TIMEOUT_MS,
|
|
1358
|
+
this.onRegisterCanceller
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
// src/signer/SignerManager.ts
|
|
1364
|
+
var SignerManager = class _SignerManager {
|
|
1365
|
+
constructor(dmk, builderFn) {
|
|
1366
|
+
this._dmk = dmk;
|
|
1367
|
+
this._builderFn = builderFn ?? _SignerManager._defaultBuilder();
|
|
1368
|
+
}
|
|
1369
|
+
async getOrCreate(sessionId) {
|
|
1370
|
+
debugLog("[DMK] SignerManager.getOrCreate:", { sessionId });
|
|
1371
|
+
const builder = await this._builderFn({ dmk: this._dmk, sessionId });
|
|
1372
|
+
return new SignerEth(builder.build());
|
|
1373
|
+
}
|
|
1374
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
|
|
1375
|
+
invalidate(_sessionId) {
|
|
1376
|
+
}
|
|
1377
|
+
// eslint-disable-next-line class-methods-use-this
|
|
1378
|
+
clearAll() {
|
|
1379
|
+
}
|
|
1380
|
+
static _defaultBuilder() {
|
|
1381
|
+
return (args) => {
|
|
1382
|
+
const contextModule = new import_context_module.ContextModuleBuilder({}).removeDefaultLoaders().build();
|
|
1383
|
+
return new import_device_signer_kit_ethereum.SignerEthBuilder(args).withContextModule(contextModule);
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
// src/connector/chains/evm.ts
|
|
1389
|
+
var import_hwk_adapter_core5 = require("@onekeyfe/hwk-adapter-core");
|
|
1390
|
+
|
|
1391
|
+
// src/connector/chains/utils.ts
|
|
1392
|
+
var import_hwk_adapter_core4 = require("@onekeyfe/hwk-adapter-core");
|
|
1393
|
+
function normalizePath(path) {
|
|
1394
|
+
return path.startsWith("m/") ? path.slice(2) : path;
|
|
1395
|
+
}
|
|
1396
|
+
function collapseSignerInteraction(interaction) {
|
|
1397
|
+
switch (interaction) {
|
|
1398
|
+
case "confirm-open-app":
|
|
1399
|
+
return import_hwk_adapter_core4.EConnectorInteraction.ConfirmOpenApp;
|
|
1400
|
+
case "unlock-device":
|
|
1401
|
+
return import_hwk_adapter_core4.EConnectorInteraction.UnlockDevice;
|
|
1402
|
+
case "interaction-complete":
|
|
1403
|
+
return import_hwk_adapter_core4.EConnectorInteraction.InteractionComplete;
|
|
1404
|
+
default:
|
|
1405
|
+
return import_hwk_adapter_core4.EConnectorInteraction.ConfirmOnDevice;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// src/connector/chains/evm.ts
|
|
1410
|
+
async function evmGetAddress(ctx, sessionId, params) {
|
|
1411
|
+
const signer = await _getEthSigner(ctx, sessionId);
|
|
1412
|
+
const path = normalizePath(params.path);
|
|
1413
|
+
const checkOnDevice = params.showOnDevice ?? false;
|
|
1414
|
+
debugLog("[DMK] evmGetAddress -> signer.getAddress:", { path, checkOnDevice });
|
|
1415
|
+
try {
|
|
1416
|
+
const result = await signer.getAddress(path, { checkOnDevice });
|
|
1417
|
+
return { address: result.address, publicKey: result.publicKey };
|
|
1418
|
+
} catch (err) {
|
|
1419
|
+
ctx.invalidateSession(sessionId);
|
|
1420
|
+
throw ctx.wrapError(err);
|
|
1421
|
+
} finally {
|
|
1422
|
+
ctx.clearCanceller(sessionId);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
async function evmSignTransaction(ctx, sessionId, params) {
|
|
1426
|
+
if (!params.serializedTx) {
|
|
1427
|
+
throw Object.assign(
|
|
1428
|
+
new Error(
|
|
1429
|
+
"Ledger requires a pre-serialized transaction (serializedTx). Provide an RLP-encoded hex string."
|
|
1430
|
+
),
|
|
1431
|
+
{ code: import_hwk_adapter_core5.HardwareErrorCode.InvalidParams }
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
const signer = await _getEthSigner(ctx, sessionId);
|
|
1435
|
+
const path = normalizePath(params.path);
|
|
1436
|
+
try {
|
|
1437
|
+
const result = await signer.signTransaction(path, params.serializedTx);
|
|
1438
|
+
return {
|
|
1439
|
+
v: `0x${result.v.toString(16)}`,
|
|
1440
|
+
r: (0, import_hwk_adapter_core5.padHex64)(result.r),
|
|
1441
|
+
s: (0, import_hwk_adapter_core5.padHex64)(result.s)
|
|
1442
|
+
};
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
ctx.invalidateSession(sessionId);
|
|
1445
|
+
throw ctx.wrapError(err);
|
|
1446
|
+
} finally {
|
|
1447
|
+
ctx.clearCanceller(sessionId);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async function evmSignMessage(ctx, sessionId, params) {
|
|
1451
|
+
const signer = await _getEthSigner(ctx, sessionId);
|
|
1452
|
+
const path = normalizePath(params.path);
|
|
1453
|
+
try {
|
|
1454
|
+
const result = await signer.signMessage(path, params.message);
|
|
1455
|
+
const rHex = (0, import_hwk_adapter_core5.stripHex)(result.r).padStart(64, "0");
|
|
1456
|
+
const sHex = (0, import_hwk_adapter_core5.stripHex)(result.s).padStart(64, "0");
|
|
1457
|
+
const vHex = result.v.toString(16).padStart(2, "0");
|
|
1458
|
+
return { signature: `0x${rHex}${sHex}${vHex}` };
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
ctx.invalidateSession(sessionId);
|
|
1461
|
+
throw ctx.wrapError(err);
|
|
1462
|
+
} finally {
|
|
1463
|
+
ctx.clearCanceller(sessionId);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
async function evmSignTypedData(ctx, sessionId, params) {
|
|
1467
|
+
if (params.mode === "hash") {
|
|
1468
|
+
throw Object.assign(
|
|
1469
|
+
new Error(
|
|
1470
|
+
'Ledger does not support hash-only EIP-712 signing. Use mode "full" with the complete typed data structure.'
|
|
1471
|
+
),
|
|
1472
|
+
{ code: import_hwk_adapter_core5.HardwareErrorCode.MethodNotSupported }
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
const signer = await _getEthSigner(ctx, sessionId);
|
|
1476
|
+
const path = normalizePath(params.path);
|
|
1477
|
+
try {
|
|
1478
|
+
const result = await signer.signTypedData(path, params.data);
|
|
1479
|
+
const rHex = (0, import_hwk_adapter_core5.stripHex)(result.r).padStart(64, "0");
|
|
1480
|
+
const sHex = (0, import_hwk_adapter_core5.stripHex)(result.s).padStart(64, "0");
|
|
1481
|
+
const vHex = result.v.toString(16).padStart(2, "0");
|
|
1482
|
+
return { signature: `0x${rHex}${sHex}${vHex}` };
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
ctx.invalidateSession(sessionId);
|
|
1485
|
+
throw ctx.wrapError(err);
|
|
1486
|
+
} finally {
|
|
1487
|
+
ctx.clearCanceller(sessionId);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
async function _getEthSigner(ctx, sessionId) {
|
|
1491
|
+
const signerManager = await ctx.getSignerManager();
|
|
1492
|
+
const signer = await signerManager.getOrCreate(sessionId);
|
|
1493
|
+
signer.onInteraction = (interaction) => {
|
|
1494
|
+
debugLog("[LedgerConnector] evm.onInteraction:", interaction);
|
|
1495
|
+
ctx.emit("ui-event", {
|
|
1496
|
+
type: collapseSignerInteraction(interaction),
|
|
1497
|
+
payload: { sessionId }
|
|
1498
|
+
});
|
|
1499
|
+
};
|
|
1500
|
+
signer.onRegisterCanceller = (cancel) => {
|
|
1501
|
+
ctx.registerCanceller(sessionId, cancel);
|
|
1502
|
+
};
|
|
1503
|
+
return signer;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/connector/chains/btc.ts
|
|
1507
|
+
var import_hwk_adapter_core7 = require("@onekeyfe/hwk-adapter-core");
|
|
1508
|
+
var import_bitcoinjs_lib = require("bitcoinjs-lib");
|
|
1509
|
+
|
|
1510
|
+
// src/signer/SignerBtc.ts
|
|
1511
|
+
var import_hwk_adapter_core6 = require("@onekeyfe/hwk-adapter-core");
|
|
1512
|
+
function hexToUtf8(hex) {
|
|
1513
|
+
return new TextDecoder().decode((0, import_hwk_adapter_core6.hexToBytes)(hex));
|
|
1514
|
+
}
|
|
1515
|
+
var INTERACTIVE_TIMEOUT_MS2 = 5 * 6e4;
|
|
1516
|
+
var SignerBtc = class {
|
|
1517
|
+
// eslint-disable-next-line no-useless-constructor, no-empty-function
|
|
1518
|
+
constructor(_sdk) {
|
|
1519
|
+
this._sdk = _sdk;
|
|
1520
|
+
}
|
|
1521
|
+
async getWalletAddress(wallet, addressIndex, options) {
|
|
1522
|
+
const action = this._sdk.getWalletAddress(wallet, addressIndex, {
|
|
1523
|
+
checkOnDevice: options?.checkOnDevice ?? false,
|
|
1524
|
+
change: options?.change ?? false
|
|
1525
|
+
});
|
|
1526
|
+
return deviceActionToPromise(
|
|
1527
|
+
action,
|
|
1528
|
+
this.onInteraction,
|
|
1529
|
+
void 0,
|
|
1530
|
+
this.onRegisterCanceller
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
async getExtendedPublicKey(derivationPath, options) {
|
|
1534
|
+
debugLog(
|
|
1535
|
+
"[SignerBtc] getExtendedPublicKey called, path:",
|
|
1536
|
+
derivationPath,
|
|
1537
|
+
"options:",
|
|
1538
|
+
JSON.stringify(options)
|
|
1539
|
+
);
|
|
1540
|
+
const action = this._sdk.getExtendedPublicKey(derivationPath, {
|
|
1541
|
+
checkOnDevice: options?.checkOnDevice ?? false
|
|
1542
|
+
});
|
|
1543
|
+
try {
|
|
1544
|
+
const result = await deviceActionToPromise(
|
|
1545
|
+
action,
|
|
1546
|
+
this.onInteraction,
|
|
1547
|
+
void 0,
|
|
1548
|
+
this.onRegisterCanceller
|
|
1549
|
+
);
|
|
1550
|
+
debugLog(
|
|
1551
|
+
"[SignerBtc] getExtendedPublicKey result type:",
|
|
1552
|
+
typeof result,
|
|
1553
|
+
"value:",
|
|
1554
|
+
typeof result === "string" ? `${result.substring(0, 20)}...` : JSON.stringify(result).substring(0, 50)
|
|
1555
|
+
);
|
|
1556
|
+
if (typeof result === "string") return result;
|
|
1557
|
+
return result.extendedPublicKey;
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
debugError("[SignerBtc] getExtendedPublicKey error:", err);
|
|
1560
|
+
throw err;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
async getMasterFingerprint(options) {
|
|
1564
|
+
const action = this._sdk.getMasterFingerprint(options);
|
|
1565
|
+
const result = await deviceActionToPromise(
|
|
1566
|
+
action,
|
|
1567
|
+
this.onInteraction,
|
|
1568
|
+
void 0,
|
|
1569
|
+
this.onRegisterCanceller
|
|
1570
|
+
);
|
|
1571
|
+
return result.masterFingerprint;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Sign a PSBT and return the array of partial signatures.
|
|
1575
|
+
* The `wallet` param is a DefaultWallet or WalletPolicy instance.
|
|
1576
|
+
* The `psbt` param can be a hex string, base64 string, or Uint8Array.
|
|
1577
|
+
*/
|
|
1578
|
+
async signPsbt(wallet, psbt, options) {
|
|
1579
|
+
const action = this._sdk.signPsbt(wallet, psbt, options);
|
|
1580
|
+
return deviceActionToPromise(
|
|
1581
|
+
action,
|
|
1582
|
+
this.onInteraction,
|
|
1583
|
+
INTERACTIVE_TIMEOUT_MS2,
|
|
1584
|
+
this.onRegisterCanceller
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Sign a PSBT and return the fully extracted raw transaction as a hex string.
|
|
1589
|
+
* Like signPsbt, but also finalises the PSBT and extracts the transaction.
|
|
1590
|
+
*/
|
|
1591
|
+
async signTransaction(wallet, psbt, options) {
|
|
1592
|
+
const action = this._sdk.signTransaction(wallet, psbt, options);
|
|
1593
|
+
return deviceActionToPromise(
|
|
1594
|
+
action,
|
|
1595
|
+
this.onInteraction,
|
|
1596
|
+
INTERACTIVE_TIMEOUT_MS2,
|
|
1597
|
+
this.onRegisterCanceller
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Sign a message with the BTC app (BIP-137 / "Bitcoin Signed Message").
|
|
1602
|
+
* Returns `{ r, s, v }` signature object.
|
|
1603
|
+
*/
|
|
1604
|
+
async signMessage(derivationPath, message, options) {
|
|
1605
|
+
const action = this._sdk.signMessage(derivationPath, hexToUtf8(message), options);
|
|
1606
|
+
return deviceActionToPromise(
|
|
1607
|
+
action,
|
|
1608
|
+
this.onInteraction,
|
|
1609
|
+
INTERACTIVE_TIMEOUT_MS2,
|
|
1610
|
+
this.onRegisterCanceller
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
// src/connector/chains/btc.ts
|
|
1616
|
+
function _purposeToTemplate(purpose, DDT) {
|
|
1617
|
+
switch (purpose) {
|
|
1618
|
+
case "44":
|
|
1619
|
+
return DDT.LEGACY;
|
|
1620
|
+
case "49":
|
|
1621
|
+
return DDT.NESTED_SEGWIT;
|
|
1622
|
+
case "84":
|
|
1623
|
+
return DDT.NATIVE_SEGWIT;
|
|
1624
|
+
case "86":
|
|
1625
|
+
return DDT.TAPROOT;
|
|
1626
|
+
default:
|
|
1627
|
+
throw Object.assign(new Error(`Unsupported BTC purpose: m/${purpose ?? "<undefined>"}'`), {
|
|
1628
|
+
code: import_hwk_adapter_core7.HardwareErrorCode.InvalidParams
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
async function btcGetAddress(ctx, sessionId, params) {
|
|
1633
|
+
const btcSigner = await _createBtcSigner(ctx, sessionId);
|
|
1634
|
+
const path = normalizePath(params.path);
|
|
1635
|
+
try {
|
|
1636
|
+
const { DefaultWallet, DefaultDescriptorTemplate } = await ctx.importLedgerKit(
|
|
1637
|
+
"@ledgerhq/device-signer-kit-bitcoin"
|
|
1638
|
+
);
|
|
1639
|
+
const purpose = path.split("/")[0]?.replace(/'/g, "");
|
|
1640
|
+
const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
|
|
1641
|
+
const wallet = new DefaultWallet(path, template);
|
|
1642
|
+
debugLog("[LedgerConnector] btcGetAddress params:", {
|
|
1643
|
+
path,
|
|
1644
|
+
purpose,
|
|
1645
|
+
template,
|
|
1646
|
+
addressIndex: params.addressIndex,
|
|
1647
|
+
change: params.change,
|
|
1648
|
+
showOnDevice: params.showOnDevice
|
|
1649
|
+
});
|
|
1650
|
+
const result = await btcSigner.getWalletAddress(wallet, params.addressIndex ?? 0, {
|
|
1651
|
+
checkOnDevice: params.showOnDevice ?? false,
|
|
1652
|
+
change: params.change ?? false
|
|
1653
|
+
});
|
|
1654
|
+
return { address: result.address, path: params.path };
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
ctx.invalidateSession(sessionId);
|
|
1657
|
+
throw ctx.wrapError(err);
|
|
1658
|
+
} finally {
|
|
1659
|
+
ctx.clearCanceller(sessionId);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
async function btcGetPublicKey(ctx, sessionId, params) {
|
|
1663
|
+
const btcSigner = await _createBtcSigner(ctx, sessionId);
|
|
1664
|
+
const path = normalizePath(params.path);
|
|
1665
|
+
debugLog("[LedgerConnector] btcGetPublicKey called, path:", path, "sessionId:", sessionId);
|
|
1666
|
+
try {
|
|
1667
|
+
const xpub = await btcSigner.getExtendedPublicKey(path, {
|
|
1668
|
+
checkOnDevice: params.showOnDevice ?? false
|
|
1669
|
+
});
|
|
1670
|
+
debugLog("[LedgerConnector] btcGetPublicKey success, xpub:", `${xpub?.substring(0, 20)}...`);
|
|
1671
|
+
return { xpub, path: params.path };
|
|
1672
|
+
} catch (err) {
|
|
1673
|
+
debugError("[LedgerConnector] btcGetPublicKey error, path:", path, "err:", err);
|
|
1674
|
+
ctx.invalidateSession(sessionId);
|
|
1675
|
+
throw ctx.wrapError(err);
|
|
1676
|
+
} finally {
|
|
1677
|
+
ctx.clearCanceller(sessionId);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
async function btcSignTransaction(ctx, sessionId, params) {
|
|
1681
|
+
if (!params.psbt) {
|
|
1682
|
+
throw Object.assign(
|
|
1683
|
+
new Error("Ledger requires PSBT format for BTC transaction signing. Provide params.psbt."),
|
|
1684
|
+
{ code: import_hwk_adapter_core7.HardwareErrorCode.InvalidParams }
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
const btcSigner = await _createBtcSigner(ctx, sessionId);
|
|
1688
|
+
try {
|
|
1689
|
+
const { DefaultWallet, DefaultDescriptorTemplate } = await ctx.importLedgerKit(
|
|
1690
|
+
"@ledgerhq/device-signer-kit-bitcoin"
|
|
1691
|
+
);
|
|
1692
|
+
const path = normalizePath(params.path || "84'/0'/0'");
|
|
1693
|
+
const purpose = path.split("/")[0]?.replace(/'/g, "");
|
|
1694
|
+
const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
|
|
1695
|
+
debugLog("[LedgerConnector] btcSignTransaction wallet:", { path, purpose, template });
|
|
1696
|
+
const wallet = new DefaultWallet(path, template);
|
|
1697
|
+
const signedTxHex = await btcSigner.signTransaction(wallet, params.psbt);
|
|
1698
|
+
return { serializedTx: (0, import_hwk_adapter_core7.stripHex)(signedTxHex) };
|
|
1699
|
+
} catch (err) {
|
|
1700
|
+
ctx.invalidateSession(sessionId);
|
|
1701
|
+
throw ctx.wrapError(err);
|
|
1702
|
+
} finally {
|
|
1703
|
+
ctx.clearCanceller(sessionId);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
async function btcSignPsbt(ctx, sessionId, params) {
|
|
1707
|
+
if (!params.psbt) {
|
|
1708
|
+
throw Object.assign(new Error("btcSignPsbt requires params.psbt"), {
|
|
1709
|
+
code: import_hwk_adapter_core7.HardwareErrorCode.InvalidParams
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
const btcSigner = await _createBtcSigner(ctx, sessionId);
|
|
1713
|
+
try {
|
|
1714
|
+
const { DefaultWallet, DefaultDescriptorTemplate } = await ctx.importLedgerKit(
|
|
1715
|
+
"@ledgerhq/device-signer-kit-bitcoin"
|
|
1716
|
+
);
|
|
1717
|
+
const path = normalizePath(params.path || "84'/0'/0'");
|
|
1718
|
+
const purpose = path.split("/")[0]?.replace(/'/g, "");
|
|
1719
|
+
const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
|
|
1720
|
+
debugLog("[LedgerConnector] btcSignPsbt wallet:", { path, purpose, template });
|
|
1721
|
+
const wallet = new DefaultWallet(path, template);
|
|
1722
|
+
const signatures = await btcSigner.signPsbt(wallet, params.psbt);
|
|
1723
|
+
debugLog("[LedgerConnector] btcSignPsbt signatures received:", signatures.length);
|
|
1724
|
+
const signedPsbtHex = _applySignaturesToPsbt(params.psbt, signatures);
|
|
1725
|
+
return { signedPsbt: signedPsbtHex };
|
|
1726
|
+
} catch (err) {
|
|
1727
|
+
ctx.invalidateSession(sessionId);
|
|
1728
|
+
throw ctx.wrapError(err);
|
|
1729
|
+
} finally {
|
|
1730
|
+
ctx.clearCanceller(sessionId);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
async function btcSignMessage(ctx, sessionId, params) {
|
|
1734
|
+
const btcSigner = await _createBtcSigner(ctx, sessionId);
|
|
1735
|
+
const path = normalizePath(params.path);
|
|
1736
|
+
try {
|
|
1737
|
+
const result = await btcSigner.signMessage(path, params.message);
|
|
1738
|
+
const vHex = result.v.toString(16).padStart(2, "0");
|
|
1739
|
+
const rHex = (0, import_hwk_adapter_core7.stripHex)(result.r).padStart(64, "0");
|
|
1740
|
+
const sHex = (0, import_hwk_adapter_core7.stripHex)(result.s).padStart(64, "0");
|
|
1741
|
+
return { signature: `${vHex}${rHex}${sHex}` };
|
|
1742
|
+
} catch (err) {
|
|
1743
|
+
ctx.invalidateSession(sessionId);
|
|
1744
|
+
throw ctx.wrapError(err);
|
|
1745
|
+
} finally {
|
|
1746
|
+
ctx.clearCanceller(sessionId);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
async function btcGetMasterFingerprint(ctx, sessionId, params) {
|
|
1750
|
+
const btcSigner = await _createBtcSigner(ctx, sessionId);
|
|
1751
|
+
try {
|
|
1752
|
+
const fingerprint = await btcSigner.getMasterFingerprint({
|
|
1753
|
+
skipOpenApp: params?.skipOpenApp
|
|
1754
|
+
});
|
|
1755
|
+
const hex = Array.from(fingerprint).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1756
|
+
return { masterFingerprint: hex };
|
|
1757
|
+
} catch (err) {
|
|
1758
|
+
ctx.invalidateSession(sessionId);
|
|
1759
|
+
throw ctx.wrapError(err);
|
|
1760
|
+
} finally {
|
|
1761
|
+
ctx.clearCanceller(sessionId);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
async function _createBtcSigner(ctx, sessionId) {
|
|
1765
|
+
const dmk = await ctx.getOrCreateDmk();
|
|
1766
|
+
const { SignerBtcBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-bitcoin");
|
|
1767
|
+
const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
|
|
1768
|
+
const signer = new SignerBtc(sdkSigner);
|
|
1769
|
+
signer.onInteraction = (interaction) => {
|
|
1770
|
+
debugLog("[LedgerConnector] btc.onInteraction:", interaction);
|
|
1771
|
+
ctx.emit("ui-event", {
|
|
1772
|
+
type: collapseSignerInteraction(interaction),
|
|
1773
|
+
payload: { sessionId }
|
|
1774
|
+
});
|
|
1775
|
+
};
|
|
1776
|
+
signer.onRegisterCanceller = (cancel) => {
|
|
1777
|
+
ctx.registerCanceller(sessionId, cancel);
|
|
1778
|
+
};
|
|
1779
|
+
return signer;
|
|
1780
|
+
}
|
|
1781
|
+
function _isTaprootInput(input) {
|
|
1782
|
+
if (input.tapInternalKey || input.tapKeySig || input.tapLeafScript || input.tapMerkleRoot || input.tapBip32Derivation || input.tapScriptSig)
|
|
1783
|
+
return true;
|
|
1784
|
+
const script = input.witnessUtxo?.script;
|
|
1785
|
+
return !!(script && script.length === 34 && script[0] === 81 && script[1] === 32);
|
|
1786
|
+
}
|
|
1787
|
+
function _isScriptPathInput(input) {
|
|
1788
|
+
return !!(input.tapLeafScript && input.tapLeafScript.length > 0);
|
|
1789
|
+
}
|
|
1790
|
+
function _applySignaturesToPsbt(psbtHex, signatures) {
|
|
1791
|
+
const psbt = import_bitcoinjs_lib.Psbt.fromHex(psbtHex);
|
|
1792
|
+
for (const sig of signatures) {
|
|
1793
|
+
const input = psbt.data.inputs[sig.inputIndex];
|
|
1794
|
+
if (!input) {
|
|
1795
|
+
throw new Error(`_applySignaturesToPsbt: no PSBT input at index ${sig.inputIndex}`);
|
|
1796
|
+
}
|
|
1797
|
+
const taproot = _isTaprootInput(input);
|
|
1798
|
+
const scriptPath = taproot && _isScriptPathInput(input);
|
|
1799
|
+
const hasLeafHash = !!(sig.tapleafHash && sig.tapleafHash.length > 0);
|
|
1800
|
+
if (scriptPath) {
|
|
1801
|
+
if (sig.pubkey.length !== 32)
|
|
1802
|
+
throw new Error(
|
|
1803
|
+
`input ${sig.inputIndex}: taproot script-path pubkey must be 32B, got ${sig.pubkey.length}`
|
|
1804
|
+
);
|
|
1805
|
+
if (!hasLeafHash)
|
|
1806
|
+
throw new Error(
|
|
1807
|
+
`input ${sig.inputIndex}: taproot script-path signature missing tapleafHash`
|
|
1808
|
+
);
|
|
1809
|
+
psbt.updateInput(sig.inputIndex, {
|
|
1810
|
+
tapScriptSig: [
|
|
1811
|
+
{ pubkey: sig.pubkey, leafHash: sig.tapleafHash, signature: sig.signature }
|
|
1812
|
+
]
|
|
1813
|
+
});
|
|
1814
|
+
} else if (taproot) {
|
|
1815
|
+
if (sig.pubkey.length !== 32)
|
|
1816
|
+
throw new Error(
|
|
1817
|
+
`input ${sig.inputIndex}: taproot key-path pubkey must be 32B, got ${sig.pubkey.length}`
|
|
1818
|
+
);
|
|
1819
|
+
if (hasLeafHash)
|
|
1820
|
+
throw new Error(
|
|
1821
|
+
`input ${sig.inputIndex}: taproot key-path signature must not carry tapleafHash`
|
|
1822
|
+
);
|
|
1823
|
+
psbt.updateInput(sig.inputIndex, { tapKeySig: sig.signature });
|
|
1824
|
+
} else {
|
|
1825
|
+
if (sig.pubkey.length !== 33 && sig.pubkey.length !== 65)
|
|
1826
|
+
throw new Error(
|
|
1827
|
+
`input ${sig.inputIndex}: ECDSA pubkey must be 33B or 65B, got ${sig.pubkey.length}`
|
|
1828
|
+
);
|
|
1829
|
+
if (hasLeafHash)
|
|
1830
|
+
throw new Error(`input ${sig.inputIndex}: ECDSA signature must not carry tapleafHash`);
|
|
1831
|
+
psbt.updateInput(sig.inputIndex, {
|
|
1832
|
+
partialSig: [{ pubkey: sig.pubkey, signature: sig.signature }]
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return psbt.toHex();
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// src/connector/chains/sol.ts
|
|
1840
|
+
var import_hwk_adapter_core8 = require("@onekeyfe/hwk-adapter-core");
|
|
1841
|
+
|
|
1842
|
+
// src/signer/SignerSol.ts
|
|
1843
|
+
var SignerSol = class {
|
|
1844
|
+
// eslint-disable-next-line no-useless-constructor, no-empty-function
|
|
1845
|
+
constructor(_sdk) {
|
|
1846
|
+
this._sdk = _sdk;
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Get the Solana address (base58-encoded Ed25519 public key) at the given derivation path.
|
|
1850
|
+
*/
|
|
1851
|
+
async getAddress(derivationPath, options) {
|
|
1852
|
+
const action = this._sdk.getAddress(derivationPath, {
|
|
1853
|
+
checkOnDevice: options?.checkOnDevice ?? false
|
|
1854
|
+
});
|
|
1855
|
+
return deviceActionToPromise(
|
|
1856
|
+
action,
|
|
1857
|
+
this.onInteraction,
|
|
1858
|
+
void 0,
|
|
1859
|
+
this.onRegisterCanceller
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Sign a Solana transaction.
|
|
1864
|
+
*/
|
|
1865
|
+
async signTransaction(derivationPath, transaction, options) {
|
|
1866
|
+
const action = this._sdk.signTransaction(derivationPath, transaction, options);
|
|
1867
|
+
return deviceActionToPromise(
|
|
1868
|
+
action,
|
|
1869
|
+
this.onInteraction,
|
|
1870
|
+
void 0,
|
|
1871
|
+
this.onRegisterCanceller
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Sign a message with the Solana app.
|
|
1876
|
+
* DMK returns { signature: string } for signMessage (unlike signTransaction which returns Uint8Array).
|
|
1877
|
+
*/
|
|
1878
|
+
async signMessage(derivationPath, message, options) {
|
|
1879
|
+
const action = this._sdk.signMessage(derivationPath, message, options);
|
|
1880
|
+
return deviceActionToPromise(
|
|
1881
|
+
action,
|
|
1882
|
+
this.onInteraction,
|
|
1883
|
+
void 0,
|
|
1884
|
+
this.onRegisterCanceller
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
|
|
1889
|
+
// src/connector/chains/sol.ts
|
|
1890
|
+
async function solGetAddress(ctx, sessionId, params) {
|
|
1891
|
+
const solSigner = await _createSolSigner(ctx, sessionId);
|
|
1892
|
+
const path = normalizePath(params.path);
|
|
1893
|
+
try {
|
|
1894
|
+
const publicKey = await solSigner.getAddress(path, {
|
|
1895
|
+
checkOnDevice: params.showOnDevice ?? false
|
|
1896
|
+
});
|
|
1897
|
+
return { address: publicKey, path: params.path };
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
ctx.invalidateSession(sessionId);
|
|
1900
|
+
throw ctx.wrapError(err);
|
|
1901
|
+
} finally {
|
|
1902
|
+
ctx.clearCanceller(sessionId);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
async function solSignTransaction(ctx, sessionId, params) {
|
|
1906
|
+
const solSigner = await _createSolSigner(ctx, sessionId);
|
|
1907
|
+
const path = normalizePath(params.path);
|
|
1908
|
+
const txBytes = (0, import_hwk_adapter_core8.hexToBytes)(params.serializedTx);
|
|
1909
|
+
try {
|
|
1910
|
+
const result = await solSigner.signTransaction(path, txBytes);
|
|
1911
|
+
return { signature: (0, import_hwk_adapter_core8.bytesToHex)(result) };
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
ctx.invalidateSession(sessionId);
|
|
1914
|
+
throw ctx.wrapError(err);
|
|
1915
|
+
} finally {
|
|
1916
|
+
ctx.clearCanceller(sessionId);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
async function solSignMessage(ctx, sessionId, params) {
|
|
1920
|
+
const solSigner = await _createSolSigner(ctx, sessionId);
|
|
1921
|
+
const path = normalizePath(params.path);
|
|
1922
|
+
const messageBytes = (0, import_hwk_adapter_core8.hexToBytes)(params.message);
|
|
1923
|
+
try {
|
|
1924
|
+
const result = await solSigner.signMessage(path, messageBytes);
|
|
1925
|
+
return { signature: result.signature };
|
|
1926
|
+
} catch (err) {
|
|
1927
|
+
ctx.invalidateSession(sessionId);
|
|
1928
|
+
throw ctx.wrapError(err);
|
|
1929
|
+
} finally {
|
|
1930
|
+
ctx.clearCanceller(sessionId);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
async function _createSolSigner(ctx, sessionId) {
|
|
1934
|
+
const dmk = await ctx.getOrCreateDmk();
|
|
1935
|
+
const { ContextModuleBuilder: ContextModuleBuilder2 } = await ctx.importLedgerKit("@ledgerhq/context-module");
|
|
1936
|
+
const { SignerSolanaBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-solana");
|
|
1937
|
+
const contextModule = new ContextModuleBuilder2({}).removeDefaultLoaders().build();
|
|
1938
|
+
const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
|
|
1939
|
+
const signer = new SignerSol(sdkSigner);
|
|
1940
|
+
signer.onInteraction = (interaction) => {
|
|
1941
|
+
debugLog("[LedgerConnector] sol.onInteraction:", interaction);
|
|
1942
|
+
ctx.emit("ui-event", {
|
|
1943
|
+
type: collapseSignerInteraction(interaction),
|
|
1944
|
+
payload: { sessionId }
|
|
1945
|
+
});
|
|
1946
|
+
};
|
|
1947
|
+
signer.onRegisterCanceller = (cancel) => {
|
|
1948
|
+
ctx.registerCanceller(sessionId, cancel);
|
|
1949
|
+
};
|
|
1950
|
+
return signer;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/connector/chains/tron.ts
|
|
1954
|
+
var import_hwk_adapter_core10 = require("@onekeyfe/hwk-adapter-core");
|
|
1955
|
+
var import_hw_app_trx = __toESM(require("@ledgerhq/hw-app-trx"));
|
|
1956
|
+
|
|
1957
|
+
// src/connector/chains/legacyChainCall.ts
|
|
1958
|
+
var import_hwk_adapter_core9 = require("@onekeyfe/hwk-adapter-core");
|
|
1959
|
+
|
|
1960
|
+
// src/app/AppManager.ts
|
|
1961
|
+
var import_device_management_kit2 = require("@ledgerhq/device-management-kit");
|
|
1962
|
+
var APP_NAME_MAP = {
|
|
1963
|
+
ETH: "Ethereum",
|
|
1964
|
+
BTC: "Bitcoin",
|
|
1965
|
+
SOL: "Solana",
|
|
1966
|
+
TRX: "Tron",
|
|
1967
|
+
XRP: "XRP",
|
|
1968
|
+
ADA: "Cardano",
|
|
1969
|
+
DOT: "Polkadot",
|
|
1970
|
+
ATOM: "Cosmos"
|
|
1971
|
+
};
|
|
1972
|
+
var DASHBOARD_APP_NAME = "BOLOS";
|
|
1973
|
+
var AppManager = class {
|
|
1974
|
+
constructor(dmk, options) {
|
|
1975
|
+
this._dmk = dmk;
|
|
1976
|
+
this._waitMs = options?.waitMs ?? 1e3;
|
|
1977
|
+
this._maxRetries = options?.maxRetries ?? 10;
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Return the Ledger app name for a given chain ticker,
|
|
1981
|
+
* or undefined if the chain is not supported.
|
|
1982
|
+
*/
|
|
1983
|
+
static getAppName(chain) {
|
|
1984
|
+
return APP_NAME_MAP[chain];
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Ensure the target app is open on the device identified by `sessionId`.
|
|
1988
|
+
*
|
|
1989
|
+
* Flow:
|
|
1990
|
+
* 1. Check the currently running app.
|
|
1991
|
+
* 2. If it is already the target, return immediately.
|
|
1992
|
+
* 3. If a different app is running (not dashboard), close it first.
|
|
1993
|
+
* 4. Open the target app.
|
|
1994
|
+
* 5. Poll until the device confirms the target app is running.
|
|
1995
|
+
*/
|
|
1996
|
+
/**
|
|
1997
|
+
* @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued — the
|
|
1998
|
+
* device is about to display "Open <app>" on screen and wait for the
|
|
1999
|
+
* user's button press. UI consumers should show their "open app" prompt
|
|
2000
|
+
* in response. NOT called when the target app is already open (no user
|
|
2001
|
+
* interaction needed in that case).
|
|
2002
|
+
*
|
|
2003
|
+
* Important: OpenAppCommand is blocking. It does not resolve until the user
|
|
2004
|
+
* has physically confirmed on the device, so anything that runs AFTER
|
|
2005
|
+
* `await this._openApp(...)` lands AFTER the prompt is already gone.
|
|
2006
|
+
* Hence the callback must fire BEFORE that await.
|
|
2007
|
+
*/
|
|
2008
|
+
async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
|
|
2009
|
+
const currentApp = await this._getCurrentApp(sessionId);
|
|
2010
|
+
if (currentApp === targetAppName) {
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
if (!this._isDashboard(currentApp)) {
|
|
2014
|
+
await this._closeCurrentApp(sessionId);
|
|
2015
|
+
await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
|
|
2016
|
+
}
|
|
2017
|
+
onConfirmOnDevice?.();
|
|
2018
|
+
await this._openApp(sessionId, targetAppName);
|
|
2019
|
+
await this._waitForApp(sessionId, targetAppName);
|
|
2020
|
+
}
|
|
2021
|
+
// ---------------------------------------------------------------------------
|
|
2022
|
+
// Private helpers
|
|
2023
|
+
// ---------------------------------------------------------------------------
|
|
2024
|
+
async _getCurrentApp(sessionId) {
|
|
2025
|
+
const result = await this._dmk.sendCommand({
|
|
2026
|
+
sessionId,
|
|
2027
|
+
command: new import_device_management_kit2.GetAppAndVersionCommand()
|
|
2028
|
+
});
|
|
2029
|
+
if ((0, import_device_management_kit2.isSuccessCommandResult)(result)) {
|
|
2030
|
+
debugLog("[AppManager] currentApp:", result.data.name);
|
|
2031
|
+
return result.data.name;
|
|
2032
|
+
}
|
|
2033
|
+
throw new Error("Failed to get current app from device");
|
|
2034
|
+
}
|
|
2035
|
+
async _openApp(sessionId, appName) {
|
|
2036
|
+
const result = await this._dmk.sendCommand({
|
|
2037
|
+
sessionId,
|
|
2038
|
+
command: new import_device_management_kit2.OpenAppCommand({ appName })
|
|
2039
|
+
});
|
|
2040
|
+
if (!(0, import_device_management_kit2.isSuccessCommandResult)(result)) {
|
|
2041
|
+
const { statusCode } = result;
|
|
2042
|
+
debugLog("[AppManager] openApp failed:", appName, "statusCode:", statusCode);
|
|
2043
|
+
throw Object.assign(new Error(`Failed to open "${appName}"`), {
|
|
2044
|
+
_tag: "OpenAppCommandError",
|
|
2045
|
+
errorCode: String(statusCode ?? ""),
|
|
2046
|
+
statusCode,
|
|
2047
|
+
appName
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
async _closeCurrentApp(sessionId) {
|
|
2052
|
+
debugLog("[AppManager] closeCurrentApp");
|
|
2053
|
+
await this._dmk.sendCommand({
|
|
2054
|
+
sessionId,
|
|
2055
|
+
command: new import_device_management_kit2.CloseAppCommand()
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Poll the device until the expected app is reported as running,
|
|
2060
|
+
* or throw after `_maxRetries` attempts.
|
|
2061
|
+
*/
|
|
2062
|
+
async _waitForApp(sessionId, expectedAppName) {
|
|
2063
|
+
let lastSeen = "";
|
|
2064
|
+
for (let i = 0; i < this._maxRetries; i++) {
|
|
2065
|
+
await this._wait();
|
|
2066
|
+
const current = await this._getCurrentApp(sessionId);
|
|
2067
|
+
lastSeen = current;
|
|
2068
|
+
if (current === expectedAppName) {
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
debugLog(
|
|
2073
|
+
"[AppManager] waitForApp exhausted: expected=",
|
|
2074
|
+
expectedAppName,
|
|
2075
|
+
"lastSeen=",
|
|
2076
|
+
lastSeen
|
|
2077
|
+
);
|
|
2078
|
+
throw new Error(
|
|
2079
|
+
`Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
_isDashboard(appName) {
|
|
2083
|
+
return appName === DASHBOARD_APP_NAME;
|
|
2084
|
+
}
|
|
2085
|
+
_wait() {
|
|
2086
|
+
return new Promise((resolve) => setTimeout(resolve, this._waitMs));
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
// src/connector/chains/legacyChainCall.ts
|
|
2091
|
+
function isLegacyWrongAppError(err, _appName) {
|
|
2092
|
+
return isWrongAppError(err);
|
|
2093
|
+
}
|
|
2094
|
+
async function withLegacyChainCall(ctx, sessionId, options, action) {
|
|
2095
|
+
const { appName, needsConfirmation } = options;
|
|
2096
|
+
let openAppPromptShown = false;
|
|
2097
|
+
const onAppOpenPrompt = () => {
|
|
2098
|
+
openAppPromptShown = true;
|
|
2099
|
+
ctx.emit("ui-event", {
|
|
2100
|
+
type: import_hwk_adapter_core9.EConnectorInteraction.ConfirmOpenApp,
|
|
2101
|
+
payload: { sessionId }
|
|
2102
|
+
});
|
|
2103
|
+
};
|
|
2104
|
+
const closeOpenAppUiIfShown = () => {
|
|
2105
|
+
if (openAppPromptShown) {
|
|
2106
|
+
ctx.emit("ui-event", {
|
|
2107
|
+
type: import_hwk_adapter_core9.EConnectorInteraction.InteractionComplete,
|
|
2108
|
+
payload: { sessionId }
|
|
2109
|
+
});
|
|
2110
|
+
openAppPromptShown = false;
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
try {
|
|
2114
|
+
await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
|
|
2115
|
+
} catch (err) {
|
|
2116
|
+
debugLog(
|
|
2117
|
+
"[LegacyChainCall] pre-flight ensureAppOpen failed:",
|
|
2118
|
+
appName,
|
|
2119
|
+
err?.message
|
|
2120
|
+
);
|
|
2121
|
+
closeOpenAppUiIfShown();
|
|
2122
|
+
throw ctx.wrapError(err, { defaultAppName: appName });
|
|
2123
|
+
}
|
|
2124
|
+
const runOnce = async () => {
|
|
2125
|
+
let confirmEmitted = false;
|
|
2126
|
+
if (needsConfirmation) {
|
|
2127
|
+
ctx.emit("ui-event", {
|
|
2128
|
+
type: import_hwk_adapter_core9.EConnectorInteraction.ConfirmOnDevice,
|
|
2129
|
+
payload: { sessionId }
|
|
2130
|
+
});
|
|
2131
|
+
confirmEmitted = true;
|
|
2132
|
+
}
|
|
2133
|
+
try {
|
|
2134
|
+
return await action(sessionId);
|
|
2135
|
+
} finally {
|
|
2136
|
+
if (confirmEmitted || openAppPromptShown) {
|
|
2137
|
+
ctx.emit("ui-event", {
|
|
2138
|
+
type: import_hwk_adapter_core9.EConnectorInteraction.InteractionComplete,
|
|
2139
|
+
payload: { sessionId }
|
|
2140
|
+
});
|
|
2141
|
+
openAppPromptShown = false;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
try {
|
|
2146
|
+
return await runOnce();
|
|
2147
|
+
} catch (err) {
|
|
2148
|
+
if (!isLegacyWrongAppError(err, appName)) {
|
|
2149
|
+
debugLog("[LegacyChainCall] non-wrong-app failure:", appName, err?.message);
|
|
2150
|
+
ctx.invalidateSession(sessionId);
|
|
2151
|
+
throw ctx.wrapError(err, { defaultAppName: appName });
|
|
2152
|
+
}
|
|
2153
|
+
debugLog("[LegacyChainCall] wrong-app detected, retrying:", appName);
|
|
2154
|
+
try {
|
|
2155
|
+
await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
|
|
2156
|
+
} catch (switchErr) {
|
|
2157
|
+
debugLog(
|
|
2158
|
+
"[LegacyChainCall] retry ensureAppOpen failed:",
|
|
2159
|
+
appName,
|
|
2160
|
+
switchErr?.message
|
|
2161
|
+
);
|
|
2162
|
+
closeOpenAppUiIfShown();
|
|
2163
|
+
throw ctx.wrapError(switchErr, { defaultAppName: appName });
|
|
2164
|
+
}
|
|
2165
|
+
ctx.clearAllSigners();
|
|
2166
|
+
const result = await runOnce();
|
|
2167
|
+
debugLog("[LegacyChainCall] retry succeeded:", appName);
|
|
2168
|
+
return result;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
async function _ensureAppOpen(ctx, sessionId, appName, onPrompt) {
|
|
2172
|
+
const dmk = await ctx.getOrCreateDmk();
|
|
2173
|
+
const appManager = new AppManager(dmk);
|
|
2174
|
+
await appManager.ensureAppOpen(sessionId, appName, onPrompt);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/transport/DmkTransport.ts
|
|
2178
|
+
var import_hw_transport = __toESM(require("@ledgerhq/hw-transport"));
|
|
2179
|
+
var DmkTransport = class extends import_hw_transport.default {
|
|
2180
|
+
constructor(dmk, sessionId) {
|
|
2181
|
+
super();
|
|
2182
|
+
this._dmk = dmk;
|
|
2183
|
+
this._sessionId = sessionId;
|
|
2184
|
+
}
|
|
2185
|
+
async exchange(apdu) {
|
|
2186
|
+
const response = await this._dmk.sendApdu({
|
|
2187
|
+
sessionId: this._sessionId,
|
|
2188
|
+
apdu: new Uint8Array(apdu)
|
|
2189
|
+
});
|
|
2190
|
+
const { data, statusCode } = response;
|
|
2191
|
+
const result = Buffer.alloc(data.length + 2);
|
|
2192
|
+
if (data.length > 0) {
|
|
2193
|
+
result.set(data, 0);
|
|
2194
|
+
}
|
|
2195
|
+
result.set(statusCode, data.length);
|
|
2196
|
+
return result;
|
|
2197
|
+
}
|
|
2198
|
+
async close() {
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
|
|
2202
|
+
// src/connector/chains/tron.ts
|
|
2203
|
+
async function tronGetAddress(ctx, sessionId, params) {
|
|
2204
|
+
const path = normalizePath(params.path);
|
|
2205
|
+
const showOnDevice = params.showOnDevice ?? false;
|
|
2206
|
+
return withLegacyChainCall(
|
|
2207
|
+
ctx,
|
|
2208
|
+
sessionId,
|
|
2209
|
+
{
|
|
2210
|
+
appName: "Tron",
|
|
2211
|
+
// Only show "confirm on device" UI when the device is actually going
|
|
2212
|
+
// to display the address for the user to verify.
|
|
2213
|
+
needsConfirmation: showOnDevice
|
|
2214
|
+
},
|
|
2215
|
+
async (sid) => {
|
|
2216
|
+
const trx = await _createTrx(ctx, sid);
|
|
2217
|
+
const result = await trx.getAddress(path, showOnDevice);
|
|
2218
|
+
return { address: result.address, publicKey: result.publicKey, path: params.path };
|
|
2219
|
+
}
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
async function tronSignTransaction(ctx, sessionId, params) {
|
|
2223
|
+
if (!params.rawTxHex) {
|
|
2224
|
+
throw Object.assign(
|
|
2225
|
+
new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
|
|
2226
|
+
{ code: import_hwk_adapter_core10.HardwareErrorCode.InvalidParams }
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
const path = normalizePath(params.path);
|
|
2230
|
+
return withLegacyChainCall(
|
|
2231
|
+
ctx,
|
|
2232
|
+
sessionId,
|
|
2233
|
+
{ appName: "Tron", needsConfirmation: true },
|
|
2234
|
+
async (sid) => {
|
|
2235
|
+
const trx = await _createTrx(ctx, sid);
|
|
2236
|
+
const signature = await trx.signTransaction(
|
|
2237
|
+
path,
|
|
2238
|
+
params.rawTxHex,
|
|
2239
|
+
params.tokenSignatures ?? []
|
|
2240
|
+
);
|
|
2241
|
+
return { signature };
|
|
2242
|
+
}
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
async function tronSignMessage(ctx, sessionId, params) {
|
|
2246
|
+
const path = normalizePath(params.path);
|
|
2247
|
+
return withLegacyChainCall(
|
|
2248
|
+
ctx,
|
|
2249
|
+
sessionId,
|
|
2250
|
+
{ appName: "Tron", needsConfirmation: true },
|
|
2251
|
+
async (sid) => {
|
|
2252
|
+
const trx = await _createTrx(ctx, sid);
|
|
2253
|
+
const signature = await trx.signPersonalMessage(path, params.messageHex);
|
|
2254
|
+
return { signature };
|
|
2255
|
+
}
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
async function _createTrx(ctx, sessionId) {
|
|
2259
|
+
const dmk = await ctx.getOrCreateDmk();
|
|
2260
|
+
return new import_hw_app_trx.default(new DmkTransport(dmk, sessionId));
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// src/connector/LedgerConnectorBase.ts
|
|
2264
|
+
var METHOD_PREFIX_TO_APP_NAME = {
|
|
2265
|
+
evm: "Ethereum",
|
|
2266
|
+
btc: "Bitcoin",
|
|
2267
|
+
sol: "Solana",
|
|
2268
|
+
tron: "Tron"
|
|
2269
|
+
};
|
|
2270
|
+
async function defaultLedgerKitImporter(pkg) {
|
|
2271
|
+
switch (pkg) {
|
|
2272
|
+
case "@ledgerhq/device-management-kit":
|
|
2273
|
+
return import("@ledgerhq/device-management-kit");
|
|
2274
|
+
case "@ledgerhq/device-signer-kit-ethereum":
|
|
2275
|
+
return import("@ledgerhq/device-signer-kit-ethereum");
|
|
2276
|
+
case "@ledgerhq/device-signer-kit-bitcoin":
|
|
2277
|
+
return import("@ledgerhq/device-signer-kit-bitcoin");
|
|
2278
|
+
case "@ledgerhq/device-signer-kit-solana":
|
|
2279
|
+
return import("@ledgerhq/device-signer-kit-solana");
|
|
2280
|
+
case "@ledgerhq/context-module":
|
|
2281
|
+
return import("@ledgerhq/context-module");
|
|
2282
|
+
default:
|
|
2283
|
+
throw new Error(`Unknown Ledger kit package: ${pkg}`);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
var LedgerConnectorBase = class {
|
|
2287
|
+
constructor(createTransport, options) {
|
|
2288
|
+
this._deviceManager = null;
|
|
2289
|
+
this._signerManager = null;
|
|
2290
|
+
this._dmk = null;
|
|
2291
|
+
this._eventHandlers = /* @__PURE__ */ new Map();
|
|
2292
|
+
// ---------------------------------------------------------------------------
|
|
2293
|
+
// ConnectId <-> DMK path mapping
|
|
2294
|
+
//
|
|
2295
|
+
// DMK uses internal paths (BLE MAC, USB UUID) that may change across sessions.
|
|
2296
|
+
// _resolveConnectId() maps these to stable external IDs (BLE: "A58F", USB: same).
|
|
2297
|
+
// This bidirectional map is the SINGLE SOURCE OF TRUTH for all connectId usage.
|
|
2298
|
+
// ---------------------------------------------------------------------------
|
|
2299
|
+
this._connectIdToPath = /* @__PURE__ */ new Map();
|
|
2300
|
+
// "A58F" -> "D5:75:7D:4B:51:E8"
|
|
2301
|
+
this._pathToConnectId = /* @__PURE__ */ new Map();
|
|
2302
|
+
// ---------------------------------------------------------------------------
|
|
2303
|
+
// Per-session DeviceAction cancellers
|
|
2304
|
+
//
|
|
2305
|
+
// Each chain handler registers its active DeviceAction's canceller via
|
|
2306
|
+
// ctx.registerCanceller(sessionId, cancel) and clears it on completion.
|
|
2307
|
+
// IConnector.cancel(sessionId) invokes the registered canceller, which
|
|
2308
|
+
// unsubscribes the observable and releases DMK's IntentQueue slot.
|
|
2309
|
+
// ---------------------------------------------------------------------------
|
|
2310
|
+
this._cancellers = /* @__PURE__ */ new Map();
|
|
2311
|
+
this._createTransport = createTransport;
|
|
2312
|
+
this.connectionType = options?.connectionType ?? "usb";
|
|
2313
|
+
this._providedDmk = options?.dmk;
|
|
2314
|
+
this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
|
|
2315
|
+
if (this._providedDmk) {
|
|
2316
|
+
this._initManagers(this._providedDmk);
|
|
2317
|
+
}
|
|
2318
|
+
this._ctx = {
|
|
2319
|
+
emit: (event, data) => this._emit(event, data),
|
|
2320
|
+
invalidateSession: (sid) => this._invalidateSession(sid),
|
|
2321
|
+
wrapError: (err, opts) => this._wrapError(err, opts),
|
|
2322
|
+
getOrCreateDmk: () => this._getOrCreateDmk(),
|
|
2323
|
+
getDeviceManager: () => this._getDeviceManager(),
|
|
2324
|
+
getSignerManager: () => this._getSignerManager(),
|
|
2325
|
+
clearAllSigners: () => this._signerManager?.clearAll(),
|
|
2326
|
+
replaceSession: (oldSid, newSid) => this._replaceSession(oldSid, newSid),
|
|
2327
|
+
registerCanceller: (sid, cancel) => this._cancellers.set(sid, cancel),
|
|
2328
|
+
clearCanceller: (sid) => this._cancellers.delete(sid),
|
|
2329
|
+
importLedgerKit: this._importLedgerKit
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
// "D5:75:7D:4B:51:E8" -> "A58F"
|
|
2333
|
+
/** Register a connectId <-> path mapping from a device descriptor. */
|
|
2334
|
+
_registerDeviceId(descriptor) {
|
|
2335
|
+
const connectId = this._resolveConnectId(descriptor);
|
|
2336
|
+
this._connectIdToPath.set(connectId, descriptor.path);
|
|
2337
|
+
this._pathToConnectId.set(descriptor.path, connectId);
|
|
2338
|
+
return connectId;
|
|
2339
|
+
}
|
|
2340
|
+
/** Get DMK path from external connectId. Falls back to connectId itself. */
|
|
2341
|
+
_getPathForConnectId(connectId) {
|
|
2342
|
+
return this._connectIdToPath.get(connectId) ?? connectId;
|
|
2343
|
+
}
|
|
2344
|
+
/** Get external connectId from DMK path. Falls back to path itself. */
|
|
2345
|
+
_getConnectIdForPath(path) {
|
|
2346
|
+
return this._pathToConnectId.get(path) ?? path;
|
|
2347
|
+
}
|
|
2348
|
+
// ---------------------------------------------------------------------------
|
|
2349
|
+
// Protected — hooks for subclasses
|
|
2350
|
+
// ---------------------------------------------------------------------------
|
|
2351
|
+
/**
|
|
2352
|
+
* Resolve the connectId for a discovered device descriptor.
|
|
2353
|
+
* Default: use the DMK path (ephemeral UUID).
|
|
2354
|
+
* Override in subclasses to extract stable identifiers (e.g. BLE hex ID).
|
|
2355
|
+
*/
|
|
2356
|
+
_resolveConnectId(descriptor) {
|
|
2357
|
+
return descriptor.path;
|
|
2358
|
+
}
|
|
2359
|
+
// ---------------------------------------------------------------------------
|
|
2360
|
+
// IConnector -- Device discovery
|
|
2361
|
+
// ---------------------------------------------------------------------------
|
|
2362
|
+
async searchDevices() {
|
|
2363
|
+
const dm = await this._getDeviceManager();
|
|
2364
|
+
let descriptors = await dm.enumerate();
|
|
2365
|
+
if (descriptors.length === 0) {
|
|
2366
|
+
try {
|
|
2367
|
+
await dm.requestDevice();
|
|
2368
|
+
} catch {
|
|
2369
|
+
}
|
|
2370
|
+
descriptors = await dm.enumerate();
|
|
2371
|
+
}
|
|
2372
|
+
const result = descriptors.map((d) => {
|
|
2373
|
+
const connectId = this._registerDeviceId(d);
|
|
2374
|
+
return {
|
|
2375
|
+
connectId,
|
|
2376
|
+
deviceId: d.path,
|
|
2377
|
+
name: d.name || d.type || "Ledger",
|
|
2378
|
+
model: d.type
|
|
2379
|
+
};
|
|
2380
|
+
});
|
|
2381
|
+
return result;
|
|
2382
|
+
}
|
|
2383
|
+
// ---------------------------------------------------------------------------
|
|
2384
|
+
// IConnector -- Connection
|
|
2385
|
+
// ---------------------------------------------------------------------------
|
|
2386
|
+
async connect(deviceId) {
|
|
2387
|
+
const dm = await this._getDeviceManager();
|
|
2388
|
+
let discovered = await this.searchDevices();
|
|
2389
|
+
const dmkPath = deviceId ? this._getPathForConnectId(deviceId) : void 0;
|
|
2390
|
+
let targetPath = dmkPath;
|
|
2391
|
+
if (!targetPath) {
|
|
2392
|
+
const descriptors = await dm.enumerate();
|
|
2393
|
+
if (descriptors.length === 0) {
|
|
2394
|
+
throw new Error(
|
|
2395
|
+
`No Ledger device found. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
targetPath = descriptors[0].path;
|
|
2399
|
+
}
|
|
2400
|
+
const externalConnectId = this._getConnectIdForPath(targetPath);
|
|
2401
|
+
const doConnect = async (path) => {
|
|
2402
|
+
const sessionId = await dm.connect(path);
|
|
2403
|
+
const session = {
|
|
2404
|
+
sessionId,
|
|
2405
|
+
deviceInfo: {
|
|
2406
|
+
vendor: "ledger",
|
|
2407
|
+
model: "unknown",
|
|
2408
|
+
firmwareVersion: "unknown",
|
|
2409
|
+
deviceId: path,
|
|
2410
|
+
connectId: externalConnectId,
|
|
2411
|
+
connectionType: this.connectionType,
|
|
2412
|
+
capabilities: { persistentDeviceIdentity: false }
|
|
2413
|
+
}
|
|
2414
|
+
};
|
|
2415
|
+
const realName = discovered.find((d) => d.connectId === externalConnectId)?.name ?? "Ledger";
|
|
2416
|
+
this._emit("device-connect", {
|
|
2417
|
+
device: {
|
|
2418
|
+
connectId: externalConnectId,
|
|
2419
|
+
deviceId: path,
|
|
2420
|
+
name: realName
|
|
2421
|
+
}
|
|
2422
|
+
});
|
|
2423
|
+
return session;
|
|
2424
|
+
};
|
|
2425
|
+
try {
|
|
2426
|
+
return await doConnect(targetPath);
|
|
2427
|
+
} catch {
|
|
2428
|
+
this._resetSignersAndSessions();
|
|
2429
|
+
const dm2 = await this._getDeviceManager();
|
|
2430
|
+
discovered = await this.searchDevices();
|
|
2431
|
+
const retryPath = this._getPathForConnectId(externalConnectId);
|
|
2432
|
+
if (!retryPath || retryPath === externalConnectId) {
|
|
2433
|
+
const descriptors = await dm2.enumerate();
|
|
2434
|
+
if (descriptors.length === 0) {
|
|
2435
|
+
throw new Error(
|
|
2436
|
+
`No Ledger device found after retry. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
return doConnect(descriptors[0].path);
|
|
2440
|
+
}
|
|
2441
|
+
return doConnect(retryPath);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
async disconnect(sessionId) {
|
|
2445
|
+
if (!this._deviceManager) return;
|
|
2446
|
+
const deviceId = this._deviceManager.getDeviceId(sessionId);
|
|
2447
|
+
this._signerManager?.invalidate(sessionId);
|
|
2448
|
+
await this._deviceManager.disconnect(sessionId);
|
|
2449
|
+
if (deviceId) {
|
|
2450
|
+
this._emit("device-disconnect", { connectId: deviceId });
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
// ---------------------------------------------------------------------------
|
|
2454
|
+
// IConnector -- Method dispatch
|
|
2455
|
+
// ---------------------------------------------------------------------------
|
|
2456
|
+
async call(sessionId, method, params) {
|
|
2457
|
+
debugLog("[DMK] call:", method, JSON.stringify(params));
|
|
2458
|
+
const ctx = this._ctxForMethod(method);
|
|
2459
|
+
switch (method) {
|
|
2460
|
+
// EVM
|
|
2461
|
+
case "evmGetAddress":
|
|
2462
|
+
return evmGetAddress(ctx, sessionId, params);
|
|
2463
|
+
case "evmSignTransaction":
|
|
2464
|
+
return evmSignTransaction(ctx, sessionId, params);
|
|
2465
|
+
case "evmSignMessage":
|
|
2466
|
+
return evmSignMessage(ctx, sessionId, params);
|
|
2467
|
+
case "evmSignTypedData":
|
|
2468
|
+
return evmSignTypedData(ctx, sessionId, params);
|
|
2469
|
+
// BTC
|
|
2470
|
+
case "btcGetAddress":
|
|
2471
|
+
return btcGetAddress(ctx, sessionId, params);
|
|
2472
|
+
case "btcGetPublicKey":
|
|
2473
|
+
return btcGetPublicKey(ctx, sessionId, params);
|
|
2474
|
+
case "btcSignTransaction":
|
|
2475
|
+
return btcSignTransaction(ctx, sessionId, params);
|
|
2476
|
+
case "btcSignPsbt":
|
|
2477
|
+
return btcSignPsbt(ctx, sessionId, params);
|
|
2478
|
+
case "btcSignMessage":
|
|
2479
|
+
return btcSignMessage(ctx, sessionId, params);
|
|
2480
|
+
case "btcGetMasterFingerprint":
|
|
2481
|
+
return btcGetMasterFingerprint(
|
|
2482
|
+
ctx,
|
|
2483
|
+
sessionId,
|
|
2484
|
+
params
|
|
2485
|
+
);
|
|
2486
|
+
// SOL
|
|
2487
|
+
case "solGetAddress":
|
|
2488
|
+
return solGetAddress(ctx, sessionId, params);
|
|
2489
|
+
case "solSignTransaction":
|
|
2490
|
+
return solSignTransaction(ctx, sessionId, params);
|
|
2491
|
+
case "solSignMessage":
|
|
2492
|
+
return solSignMessage(ctx, sessionId, params);
|
|
2493
|
+
// TRON
|
|
2494
|
+
case "tronGetAddress":
|
|
2495
|
+
return tronGetAddress(ctx, sessionId, params);
|
|
2496
|
+
case "tronSignTransaction":
|
|
2497
|
+
return tronSignTransaction(ctx, sessionId, params);
|
|
2498
|
+
case "tronSignMessage": {
|
|
2499
|
+
const p = params;
|
|
2500
|
+
const internalParams = {
|
|
2501
|
+
path: p.path,
|
|
2502
|
+
messageHex: p.messageHex
|
|
2503
|
+
};
|
|
2504
|
+
return tronSignMessage(ctx, sessionId, internalParams);
|
|
2505
|
+
}
|
|
2506
|
+
default:
|
|
2507
|
+
throw new Error(`LedgerConnector: unknown method "${method}"`);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
async cancel(sessionId) {
|
|
2511
|
+
const cancel = this._cancellers.get(sessionId);
|
|
2512
|
+
if (cancel) {
|
|
2513
|
+
this._cancellers.delete(sessionId);
|
|
2514
|
+
try {
|
|
2515
|
+
cancel();
|
|
2516
|
+
} catch {
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
uiResponse(_response) {
|
|
2521
|
+
}
|
|
2522
|
+
// ---------------------------------------------------------------------------
|
|
2523
|
+
// IConnector -- Events
|
|
2524
|
+
// ---------------------------------------------------------------------------
|
|
2525
|
+
on(event, handler) {
|
|
2526
|
+
if (!this._eventHandlers.has(event)) {
|
|
2527
|
+
this._eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
2528
|
+
}
|
|
2529
|
+
this._eventHandlers.get(event).add(handler);
|
|
2530
|
+
}
|
|
2531
|
+
off(event, handler) {
|
|
2532
|
+
this._eventHandlers.get(event)?.delete(handler);
|
|
2533
|
+
}
|
|
2534
|
+
// ---------------------------------------------------------------------------
|
|
2535
|
+
// IConnector -- Reset
|
|
2536
|
+
// ---------------------------------------------------------------------------
|
|
2537
|
+
reset() {
|
|
2538
|
+
this._resetAll();
|
|
2539
|
+
}
|
|
2540
|
+
// ---------------------------------------------------------------------------
|
|
2541
|
+
// Private -- DMK / Manager lifecycle
|
|
2542
|
+
// ---------------------------------------------------------------------------
|
|
2543
|
+
/**
|
|
2544
|
+
* Lazily create or return the DMK instance.
|
|
2545
|
+
* If a DMK was provided via constructor, it is used directly.
|
|
2546
|
+
* Otherwise, one is created via the transport factory.
|
|
2547
|
+
*/
|
|
2548
|
+
async _getOrCreateDmk() {
|
|
2549
|
+
debugLog(
|
|
2550
|
+
"[DMK] _getOrCreateDmk called, _dmk exists:",
|
|
2551
|
+
!!this._dmk,
|
|
2552
|
+
"_providedDmk exists:",
|
|
2553
|
+
!!this._providedDmk
|
|
2554
|
+
);
|
|
2555
|
+
if (this._dmk) return this._dmk;
|
|
2556
|
+
if (this._providedDmk) {
|
|
2557
|
+
this._dmk = this._providedDmk;
|
|
2558
|
+
return this._dmk;
|
|
2559
|
+
}
|
|
2560
|
+
const { DeviceManagementKitBuilder } = await this._importLedgerKit(
|
|
2561
|
+
"@ledgerhq/device-management-kit"
|
|
2562
|
+
);
|
|
2563
|
+
const transportFactory = await this._createTransport();
|
|
2564
|
+
debugLog("[DMK] _getOrCreateDmk: transportFactory type:", typeof transportFactory);
|
|
2565
|
+
const dmk = new DeviceManagementKitBuilder().addTransport(transportFactory).build();
|
|
2566
|
+
this._dmk = dmk;
|
|
2567
|
+
debugLog("[DMK] _getOrCreateDmk: DMK created");
|
|
2568
|
+
return dmk;
|
|
2569
|
+
}
|
|
2570
|
+
_initManagers(dmk) {
|
|
2571
|
+
this._dmk = dmk;
|
|
2572
|
+
this._deviceManager = new LedgerDeviceManager(dmk);
|
|
2573
|
+
const importKit = this._importLedgerKit;
|
|
2574
|
+
this._signerManager = new SignerManager(dmk, async (args) => {
|
|
2575
|
+
const mod = await importKit("@ledgerhq/device-signer-kit-ethereum");
|
|
2576
|
+
return new mod.SignerEthBuilder(args);
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
async _getDeviceManager() {
|
|
2580
|
+
if (this._deviceManager) return this._deviceManager;
|
|
2581
|
+
const dmk = await this._getOrCreateDmk();
|
|
2582
|
+
this._initManagers(dmk);
|
|
2583
|
+
return this._deviceManager;
|
|
2584
|
+
}
|
|
2585
|
+
async _getSignerManager() {
|
|
2586
|
+
if (!this._signerManager) {
|
|
2587
|
+
const dmk = await this._getOrCreateDmk();
|
|
2588
|
+
this._initManagers(dmk);
|
|
2589
|
+
}
|
|
2590
|
+
return this._signerManager;
|
|
2591
|
+
}
|
|
2592
|
+
_invalidateSession(sessionId) {
|
|
2593
|
+
this._signerManager?.invalidate(sessionId);
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Replace an old session with a new one after app switch.
|
|
2597
|
+
* Updates the connectId→path mapping so subsequent calls() use the new session,
|
|
2598
|
+
* and emits device-connect so the adapter updates its _sessions Map.
|
|
2599
|
+
*/
|
|
2600
|
+
_replaceSession(oldSessionId, _newSessionId) {
|
|
2601
|
+
const dm = this._deviceManager;
|
|
2602
|
+
if (!dm) return;
|
|
2603
|
+
const oldDeviceId = dm.getDeviceId(oldSessionId);
|
|
2604
|
+
const connectId = oldDeviceId ? this._pathToConnectId.get(oldDeviceId) : void 0;
|
|
2605
|
+
this._signerManager?.invalidate(oldSessionId);
|
|
2606
|
+
if (connectId) {
|
|
2607
|
+
this._emit("device-connect", {
|
|
2608
|
+
device: {
|
|
2609
|
+
connectId,
|
|
2610
|
+
deviceId: connectId,
|
|
2611
|
+
name: "Ledger"
|
|
2612
|
+
}
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Light reset: clear signer/session state but keep DMK, BLE scan, and ID mapping alive.
|
|
2618
|
+
* Used by connect() retry — we want to re-discover with the same transport.
|
|
2619
|
+
*/
|
|
2620
|
+
_resetSignersAndSessions() {
|
|
2621
|
+
debugLog("[DMK] _resetSignersAndSessions called");
|
|
2622
|
+
this._signerManager?.clearAll();
|
|
2623
|
+
this._signerManager = null;
|
|
2624
|
+
this._deviceManager = null;
|
|
2625
|
+
}
|
|
2626
|
+
_resetAll() {
|
|
2627
|
+
debugLog("[DMK] _resetAll called");
|
|
2628
|
+
for (const cancel of this._cancellers.values()) {
|
|
2629
|
+
try {
|
|
2630
|
+
cancel();
|
|
2631
|
+
} catch {
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
this._cancellers.clear();
|
|
2635
|
+
this._signerManager?.clearAll();
|
|
2636
|
+
this._deviceManager?.dispose();
|
|
2637
|
+
this._deviceManager = null;
|
|
2638
|
+
this._signerManager = null;
|
|
2639
|
+
this._dmk = null;
|
|
2640
|
+
this._connectIdToPath.clear();
|
|
2641
|
+
this._pathToConnectId.clear();
|
|
2642
|
+
}
|
|
2643
|
+
// ---------------------------------------------------------------------------
|
|
2644
|
+
// Private -- Events
|
|
2645
|
+
// ---------------------------------------------------------------------------
|
|
2646
|
+
_emit(event, data) {
|
|
2647
|
+
const handlers = this._eventHandlers.get(event);
|
|
2648
|
+
if (handlers) {
|
|
2649
|
+
for (const handler of handlers) {
|
|
2650
|
+
try {
|
|
2651
|
+
handler(data);
|
|
2652
|
+
} catch {
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
// ---------------------------------------------------------------------------
|
|
2658
|
+
// Private -- Error handling
|
|
2659
|
+
// ---------------------------------------------------------------------------
|
|
2660
|
+
/**
|
|
2661
|
+
* Return a per-call ctx with the chain's Ledger app name pre-bound to
|
|
2662
|
+
* wrapError, so chain handlers don't need to repeat `{ defaultAppName: 'X' }`
|
|
2663
|
+
* at every catch site. Falls through unchanged for unknown methods.
|
|
2664
|
+
*/
|
|
2665
|
+
_ctxForMethod(method) {
|
|
2666
|
+
const prefix = /^(evm|btc|sol|tron)/.exec(method)?.[1];
|
|
2667
|
+
const defaultAppName = prefix ? METHOD_PREFIX_TO_APP_NAME[prefix] : void 0;
|
|
2668
|
+
if (!defaultAppName) return this._ctx;
|
|
2669
|
+
return {
|
|
2670
|
+
...this._ctx,
|
|
2671
|
+
wrapError: (err, opts) => this._wrapError(err, { defaultAppName, ...opts })
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
_wrapError(err, opts) {
|
|
2675
|
+
const mapped = mapLedgerError(err, opts);
|
|
2676
|
+
const error = new Error(mapped.message);
|
|
2677
|
+
const src = err && typeof err === "object" ? err : {};
|
|
2678
|
+
Object.assign(error, {
|
|
2679
|
+
code: mapped.code,
|
|
2680
|
+
appName: mapped.appName,
|
|
2681
|
+
_tag: src._tag,
|
|
2682
|
+
errorCode: src.errorCode,
|
|
2683
|
+
_lastStep: src._lastStep
|
|
2684
|
+
});
|
|
2685
|
+
Object.defineProperty(error, "originalError", {
|
|
2686
|
+
value: err,
|
|
2687
|
+
configurable: true,
|
|
2688
|
+
enumerable: false,
|
|
2689
|
+
writable: true
|
|
2690
|
+
});
|
|
2691
|
+
return error;
|
|
2692
|
+
}
|
|
2693
|
+
};
|
|
2694
|
+
|
|
2695
|
+
// src/utils/bleIdentity.ts
|
|
2696
|
+
function extractBleHexId(name) {
|
|
2697
|
+
if (!name) return void 0;
|
|
2698
|
+
const match = name.match(/\b([0-9A-Fa-f]{4})$/);
|
|
2699
|
+
return match ? match[1].toUpperCase() : void 0;
|
|
2700
|
+
}
|
|
2701
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2702
|
+
0 && (module.exports = {
|
|
2703
|
+
AppManager,
|
|
2704
|
+
DmkTransport,
|
|
2705
|
+
LedgerAdapter,
|
|
2706
|
+
LedgerConnectorBase,
|
|
2707
|
+
LedgerDeviceManager,
|
|
2708
|
+
SignerBtc,
|
|
2709
|
+
SignerEth,
|
|
2710
|
+
SignerManager,
|
|
2711
|
+
SignerSol,
|
|
2712
|
+
deviceActionToPromise,
|
|
2713
|
+
extractBleHexId,
|
|
2714
|
+
isDeviceLockedError,
|
|
2715
|
+
ledgerFailure,
|
|
2716
|
+
mapLedgerError,
|
|
2717
|
+
offSdkEvent,
|
|
2718
|
+
onSdkEvent
|
|
2719
|
+
});
|
|
2720
|
+
//# sourceMappingURL=index.js.map
|