@onekeyfe/hwk-ledger-adapter 1.1.26-alpha.100
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 +529 -0
- package/dist/index.d.ts +529 -0
- package/dist/index.js +2338 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2309 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2309 @@
|
|
|
1
|
+
// src/adapter/LedgerAdapter.ts
|
|
2
|
+
import {
|
|
3
|
+
success,
|
|
4
|
+
failure,
|
|
5
|
+
HardwareErrorCode as HardwareErrorCode2,
|
|
6
|
+
TypedEventEmitter,
|
|
7
|
+
DEVICE,
|
|
8
|
+
UI_REQUEST,
|
|
9
|
+
CHAIN_FINGERPRINT_PATHS,
|
|
10
|
+
deriveDeviceFingerprint
|
|
11
|
+
} from "@onekeyfe/hwk-adapter-core";
|
|
12
|
+
|
|
13
|
+
// src/errors.ts
|
|
14
|
+
import { HardwareErrorCode, enrichErrorMessage } from "@onekeyfe/hwk-adapter-core";
|
|
15
|
+
var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
|
|
16
|
+
var USER_REJECTED_CODES = /* @__PURE__ */ new Set(["6985", "27013"]);
|
|
17
|
+
var WRONG_APP_CODES = /* @__PURE__ */ new Set(["6e00", "28160", "6d00", "27904", "6a83", "27267"]);
|
|
18
|
+
var APP_NOT_INSTALLED_CODES = /* @__PURE__ */ new Set(["6807", "26631"]);
|
|
19
|
+
function isDeviceLockedError(err) {
|
|
20
|
+
if (!err || typeof err !== "object") return false;
|
|
21
|
+
const e = err;
|
|
22
|
+
if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
|
|
23
|
+
if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
|
|
24
|
+
if (e._tag === "DeviceLockedError") return true;
|
|
25
|
+
if (typeof e.message === "string" && /locked|device exchange error/i.test(e.message)) return true;
|
|
26
|
+
if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
|
|
27
|
+
if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function hasStatusCode(err, codeSet) {
|
|
31
|
+
if (!err || typeof err !== "object") return false;
|
|
32
|
+
const e = err;
|
|
33
|
+
if (e.errorCode != null && codeSet.has(String(e.errorCode))) return true;
|
|
34
|
+
if (e.statusCode != null && codeSet.has(String(e.statusCode))) return true;
|
|
35
|
+
if (e.originalError != null && hasStatusCode(e.originalError, codeSet)) return true;
|
|
36
|
+
if (e.error != null && e._tag && hasStatusCode(e.error, codeSet)) return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
function isUserRejectedError(err) {
|
|
40
|
+
if (!err || typeof err !== "object") return false;
|
|
41
|
+
const e = err;
|
|
42
|
+
if (e._tag === "UserRefusedOnDevice") return true;
|
|
43
|
+
if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
|
|
44
|
+
if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
function isWrongAppError(err) {
|
|
48
|
+
if (!err || typeof err !== "object") return false;
|
|
49
|
+
const e = err;
|
|
50
|
+
if (e._tag === "WrongAppOpenedError" || e._tag === "InvalidStatusWordError") {
|
|
51
|
+
if (hasStatusCode(err, WRONG_APP_CODES)) return true;
|
|
52
|
+
}
|
|
53
|
+
if (typeof e.message === "string" && /wrong app|open the .* app|CLA not supported/i.test(e.message))
|
|
54
|
+
return true;
|
|
55
|
+
if (hasStatusCode(err, WRONG_APP_CODES)) return true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
function isAppNotInstalledError(err) {
|
|
59
|
+
if (!err || typeof err !== "object") return false;
|
|
60
|
+
const e = err;
|
|
61
|
+
if (e._tag === "OpenAppCommandError") return true;
|
|
62
|
+
if (typeof e.message === "string" && /unknown application/i.test(e.message)) return true;
|
|
63
|
+
if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
function isDeviceDisconnectedError(err) {
|
|
67
|
+
if (!err || typeof err !== "object") return false;
|
|
68
|
+
const e = err;
|
|
69
|
+
if (e._tag === "DeviceNotRecognizedError" || e._tag === "DeviceSessionNotFound") return true;
|
|
70
|
+
if (typeof e.message === "string" && /disconnected|not found|no device|unplugged|session.*not.*found|timed out.*locked/i.test(
|
|
71
|
+
e.message
|
|
72
|
+
))
|
|
73
|
+
return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
var TIMEOUT_TAGS = /* @__PURE__ */ new Set([
|
|
77
|
+
"DeviceExchangeTimeoutError",
|
|
78
|
+
"SendApduTimeoutError",
|
|
79
|
+
"SendCommandTimeoutError"
|
|
80
|
+
]);
|
|
81
|
+
function isTimeoutError(err) {
|
|
82
|
+
if (!err || typeof err !== "object") return false;
|
|
83
|
+
const e = err;
|
|
84
|
+
if (typeof e._tag === "string" && TIMEOUT_TAGS.has(e._tag)) return true;
|
|
85
|
+
if (e.code === HardwareErrorCode.OperationTimeout) return true;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
function mapLedgerError(err) {
|
|
89
|
+
let originalMessage = "Unknown Ledger error";
|
|
90
|
+
if (err instanceof Error) {
|
|
91
|
+
originalMessage = err.message;
|
|
92
|
+
} else if (err && typeof err === "object") {
|
|
93
|
+
const e = err;
|
|
94
|
+
originalMessage = String(e.message ?? e._tag ?? e.type ?? JSON.stringify(err));
|
|
95
|
+
}
|
|
96
|
+
let code;
|
|
97
|
+
if (isDeviceLockedError(err)) {
|
|
98
|
+
code = HardwareErrorCode.DeviceLocked;
|
|
99
|
+
} else if (isUserRejectedError(err)) {
|
|
100
|
+
code = HardwareErrorCode.UserRejected;
|
|
101
|
+
} else if (isWrongAppError(err)) {
|
|
102
|
+
code = HardwareErrorCode.WrongApp;
|
|
103
|
+
} else if (isAppNotInstalledError(err)) {
|
|
104
|
+
code = HardwareErrorCode.AppNotOpen;
|
|
105
|
+
} else if (isDeviceDisconnectedError(err)) {
|
|
106
|
+
code = HardwareErrorCode.DeviceDisconnected;
|
|
107
|
+
} else if (isTimeoutError(err)) {
|
|
108
|
+
code = HardwareErrorCode.OperationTimeout;
|
|
109
|
+
} else {
|
|
110
|
+
code = HardwareErrorCode.UnknownError;
|
|
111
|
+
}
|
|
112
|
+
return { code, message: enrichErrorMessage(code, originalMessage) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/adapter/LedgerAdapter.ts
|
|
116
|
+
var _LedgerAdapter = class _LedgerAdapter {
|
|
117
|
+
constructor(connector) {
|
|
118
|
+
this.vendor = "ledger";
|
|
119
|
+
this.emitter = new TypedEventEmitter();
|
|
120
|
+
this._uiHandler = null;
|
|
121
|
+
// Device cache: tracks discovered devices from connector events
|
|
122
|
+
this._discoveredDevices = /* @__PURE__ */ new Map();
|
|
123
|
+
// Session tracking: maps connectId -> sessionId
|
|
124
|
+
this._sessions = /* @__PURE__ */ new Map();
|
|
125
|
+
// Pending device-connect resolve — set by _waitForDeviceConnect, resolved by uiResponse
|
|
126
|
+
this._deviceConnectResolve = null;
|
|
127
|
+
// Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
|
|
128
|
+
this._connectingPromise = null;
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Event translation
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
this.deviceConnectHandler = (data) => {
|
|
133
|
+
const deviceInfo = this.connectorDeviceToDeviceInfo(data.device);
|
|
134
|
+
this._discoveredDevices.set(deviceInfo.connectId, deviceInfo);
|
|
135
|
+
this._sessions.delete(deviceInfo.connectId);
|
|
136
|
+
this.emitter.emit(DEVICE.CONNECT, {
|
|
137
|
+
type: DEVICE.CONNECT,
|
|
138
|
+
payload: deviceInfo
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
this.deviceDisconnectHandler = (data) => {
|
|
142
|
+
this._discoveredDevices.delete(data.connectId);
|
|
143
|
+
this._sessions.delete(data.connectId);
|
|
144
|
+
this.emitter.emit(DEVICE.DISCONNECT, {
|
|
145
|
+
type: DEVICE.DISCONNECT,
|
|
146
|
+
payload: { connectId: data.connectId }
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
this.uiRequestHandler = (data) => {
|
|
150
|
+
this.handleUiEvent(data);
|
|
151
|
+
};
|
|
152
|
+
this.uiEventHandler = (data) => {
|
|
153
|
+
this.handleUiEvent(data);
|
|
154
|
+
};
|
|
155
|
+
this.connector = connector;
|
|
156
|
+
this.registerEventListeners();
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Transport
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Transport is decided at connector creation time. These methods
|
|
162
|
+
// satisfy the IHardwareWallet interface with sensible defaults.
|
|
163
|
+
get activeTransport() {
|
|
164
|
+
return "hid";
|
|
165
|
+
}
|
|
166
|
+
getAvailableTransports() {
|
|
167
|
+
return ["hid"];
|
|
168
|
+
}
|
|
169
|
+
async switchTransport(_type) {
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// UI handler
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
setUiHandler(handler) {
|
|
175
|
+
this._uiHandler = handler;
|
|
176
|
+
}
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Lifecycle
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
async init(_config) {
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Clear cached device/session state without tearing down the adapter.
|
|
184
|
+
* Call before retrying after errors or when the device state may be stale.
|
|
185
|
+
* The next operation will re-discover and re-connect automatically.
|
|
186
|
+
*/
|
|
187
|
+
resetState() {
|
|
188
|
+
this._discoveredDevices.clear();
|
|
189
|
+
this._sessions.clear();
|
|
190
|
+
this._connectingPromise = null;
|
|
191
|
+
}
|
|
192
|
+
async dispose() {
|
|
193
|
+
this._deviceConnectResolve?.(true);
|
|
194
|
+
this._deviceConnectResolve = null;
|
|
195
|
+
this.unregisterEventListeners();
|
|
196
|
+
this.connector.reset();
|
|
197
|
+
this._uiHandler = null;
|
|
198
|
+
this._discoveredDevices.clear();
|
|
199
|
+
this._sessions.clear();
|
|
200
|
+
this.emitter.removeAllListeners();
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Device management
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
async searchDevices() {
|
|
206
|
+
await this._ensureDevicePermission();
|
|
207
|
+
const devices = await this.connector.searchDevices();
|
|
208
|
+
console.log("[DMK] adapter.searchDevices raw:", JSON.stringify(devices));
|
|
209
|
+
for (const d of devices) {
|
|
210
|
+
if (d.connectId) {
|
|
211
|
+
this._discoveredDevices.set(d.connectId, this.connectorDeviceToDeviceInfo(d));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (this._discoveredDevices.size === 0) {
|
|
215
|
+
await this._ensureDevicePermission();
|
|
216
|
+
}
|
|
217
|
+
return Array.from(this._discoveredDevices.values());
|
|
218
|
+
}
|
|
219
|
+
async connectDevice(connectId) {
|
|
220
|
+
await this._ensureDevicePermission(connectId);
|
|
221
|
+
try {
|
|
222
|
+
const session = await this.connector.connect(connectId);
|
|
223
|
+
this._sessions.set(connectId, session.sessionId);
|
|
224
|
+
if (session.deviceInfo) {
|
|
225
|
+
this._discoveredDevices.set(connectId, session.deviceInfo);
|
|
226
|
+
}
|
|
227
|
+
return success(connectId);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
return this.errorToFailure(err);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async disconnectDevice(connectId) {
|
|
233
|
+
const sessionId = this._sessions.get(connectId);
|
|
234
|
+
if (sessionId) {
|
|
235
|
+
await this.connector.disconnect(sessionId);
|
|
236
|
+
this._sessions.delete(connectId);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async getDeviceInfo(connectId, deviceId) {
|
|
240
|
+
await this._ensureDevicePermission(connectId, deviceId);
|
|
241
|
+
const cached = this._discoveredDevices.get(connectId) ?? Array.from(this._discoveredDevices.values()).find((d) => d.deviceId === deviceId);
|
|
242
|
+
if (cached) {
|
|
243
|
+
return success(cached);
|
|
244
|
+
}
|
|
245
|
+
return failure(
|
|
246
|
+
HardwareErrorCode2.DeviceNotFound,
|
|
247
|
+
"Device not found in cache. Call searchDevices() or wait for a device-connected event first."
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
getSupportedChains() {
|
|
251
|
+
return ["evm", "btc", "sol", "tron"];
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Chain call helper
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
async callChain(connectId, deviceId, chain, method, params, skipFingerprint = false) {
|
|
257
|
+
await this._ensureDevicePermission(connectId, deviceId);
|
|
258
|
+
if (!skipFingerprint && !await this._verifyDeviceFingerprint(connectId, deviceId, chain)) {
|
|
259
|
+
return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const result = await this.connectorCall(connectId, method, params);
|
|
263
|
+
return success(result);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
return this.errorToFailure(err);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Batch version of callChain — checks permission and fingerprint once,
|
|
270
|
+
* then calls the connector for each param sequentially.
|
|
271
|
+
*/
|
|
272
|
+
async callChainBatch(connectId, deviceId, chain, method, params, onProgress, skipFingerprint = false) {
|
|
273
|
+
await this._ensureDevicePermission(connectId, deviceId);
|
|
274
|
+
if (!skipFingerprint && !await this._verifyDeviceFingerprint(connectId, deviceId, chain)) {
|
|
275
|
+
return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
|
|
276
|
+
}
|
|
277
|
+
const results = [];
|
|
278
|
+
for (let i = 0; i < params.length; i++) {
|
|
279
|
+
try {
|
|
280
|
+
const result = await this.connectorCall(connectId, method, params[i]);
|
|
281
|
+
results.push(result);
|
|
282
|
+
onProgress?.({ index: i, total: params.length });
|
|
283
|
+
} catch (err) {
|
|
284
|
+
return this.errorToFailure(err);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return success(results);
|
|
288
|
+
}
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// EVM chain methods
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
evmGetAddress(connectId, deviceId, params) {
|
|
293
|
+
return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
|
|
294
|
+
}
|
|
295
|
+
evmGetAddresses(connectId, deviceId, params, onProgress) {
|
|
296
|
+
return this.callChainBatch(
|
|
297
|
+
connectId,
|
|
298
|
+
deviceId,
|
|
299
|
+
"evm",
|
|
300
|
+
"evmGetAddress",
|
|
301
|
+
params,
|
|
302
|
+
onProgress
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
evmGetPublicKey(connectId, deviceId, params) {
|
|
306
|
+
return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
|
|
307
|
+
}
|
|
308
|
+
evmSignTransaction(connectId, deviceId, params) {
|
|
309
|
+
return this.callChain(connectId, deviceId, "evm", "evmSignTransaction", params);
|
|
310
|
+
}
|
|
311
|
+
evmSignMessage(connectId, deviceId, params) {
|
|
312
|
+
return this.callChain(connectId, deviceId, "evm", "evmSignMessage", params);
|
|
313
|
+
}
|
|
314
|
+
evmSignTypedData(connectId, deviceId, params) {
|
|
315
|
+
return this.callChain(connectId, deviceId, "evm", "evmSignTypedData", params);
|
|
316
|
+
}
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// BTC chain methods
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
btcGetAddress(connectId, deviceId, params) {
|
|
321
|
+
return this.callChain(connectId, deviceId, "btc", "btcGetAddress", params);
|
|
322
|
+
}
|
|
323
|
+
btcGetAddresses(connectId, deviceId, params, onProgress) {
|
|
324
|
+
return this.callChainBatch(
|
|
325
|
+
connectId,
|
|
326
|
+
deviceId,
|
|
327
|
+
"btc",
|
|
328
|
+
"btcGetAddress",
|
|
329
|
+
params,
|
|
330
|
+
onProgress
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
btcGetPublicKey(connectId, deviceId, params) {
|
|
334
|
+
return this.callChain(connectId, deviceId, "btc", "btcGetPublicKey", params);
|
|
335
|
+
}
|
|
336
|
+
btcSignTransaction(connectId, deviceId, params) {
|
|
337
|
+
return this.callChain(connectId, deviceId, "btc", "btcSignTransaction", params);
|
|
338
|
+
}
|
|
339
|
+
btcSignMessage(connectId, deviceId, params) {
|
|
340
|
+
return this.callChain(connectId, deviceId, "btc", "btcSignMessage", params);
|
|
341
|
+
}
|
|
342
|
+
btcGetMasterFingerprint(connectId, deviceId) {
|
|
343
|
+
return this.callChain(
|
|
344
|
+
connectId,
|
|
345
|
+
deviceId,
|
|
346
|
+
"btc",
|
|
347
|
+
"btcGetMasterFingerprint",
|
|
348
|
+
{}
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// SOL chain methods
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
solGetAddress(connectId, deviceId, params) {
|
|
355
|
+
return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
|
|
356
|
+
}
|
|
357
|
+
solGetAddresses(connectId, deviceId, params, onProgress) {
|
|
358
|
+
return this.callChainBatch(
|
|
359
|
+
connectId,
|
|
360
|
+
deviceId,
|
|
361
|
+
"sol",
|
|
362
|
+
"solGetAddress",
|
|
363
|
+
params,
|
|
364
|
+
onProgress
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
solGetPublicKey(connectId, deviceId, params) {
|
|
368
|
+
return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
|
|
369
|
+
}
|
|
370
|
+
solSignTransaction(connectId, deviceId, params) {
|
|
371
|
+
return this.callChain(connectId, deviceId, "sol", "solSignTransaction", params);
|
|
372
|
+
}
|
|
373
|
+
solSignMessage(connectId, deviceId, params) {
|
|
374
|
+
return this.callChain(connectId, deviceId, "sol", "solSignMessage", params);
|
|
375
|
+
}
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// TRON chain methods
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
tronGetAddress(connectId, deviceId, params) {
|
|
380
|
+
return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params, true);
|
|
381
|
+
}
|
|
382
|
+
tronGetAddresses(connectId, deviceId, params, onProgress) {
|
|
383
|
+
return this.callChainBatch(
|
|
384
|
+
connectId,
|
|
385
|
+
deviceId,
|
|
386
|
+
"tron",
|
|
387
|
+
"tronGetAddress",
|
|
388
|
+
params,
|
|
389
|
+
onProgress,
|
|
390
|
+
true
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
tronSignTransaction(connectId, deviceId, params) {
|
|
394
|
+
return this.callChain(
|
|
395
|
+
connectId,
|
|
396
|
+
deviceId,
|
|
397
|
+
"tron",
|
|
398
|
+
"tronSignTransaction",
|
|
399
|
+
params,
|
|
400
|
+
true
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
tronSignMessage(connectId, deviceId, params) {
|
|
404
|
+
return this.callChain(
|
|
405
|
+
connectId,
|
|
406
|
+
deviceId,
|
|
407
|
+
"tron",
|
|
408
|
+
"tronSignMessage",
|
|
409
|
+
params,
|
|
410
|
+
true
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
on(event, listener) {
|
|
414
|
+
this.emitter.on(event, listener);
|
|
415
|
+
}
|
|
416
|
+
off(event, listener) {
|
|
417
|
+
this.emitter.off(event, listener);
|
|
418
|
+
}
|
|
419
|
+
cancel(connectId) {
|
|
420
|
+
const sessionId = this._sessions.get(connectId) ?? connectId;
|
|
421
|
+
void this.connector.cancel(sessionId);
|
|
422
|
+
}
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// Chain fingerprint
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
async getChainFingerprint(connectId, deviceId, chain) {
|
|
427
|
+
console.log(
|
|
428
|
+
"[LedgerAdapter] getChainFingerprint called, chain:",
|
|
429
|
+
chain,
|
|
430
|
+
"connectId:",
|
|
431
|
+
connectId || "(empty)",
|
|
432
|
+
"sessions:",
|
|
433
|
+
this._sessions.size
|
|
434
|
+
);
|
|
435
|
+
await this._ensureDevicePermission(connectId, deviceId);
|
|
436
|
+
console.log(
|
|
437
|
+
"[LedgerAdapter] getChainFingerprint permission ok, calling _deriveAddressForFingerprint"
|
|
438
|
+
);
|
|
439
|
+
try {
|
|
440
|
+
const address = await this._deriveAddressForFingerprint(connectId, chain);
|
|
441
|
+
console.log("[LedgerAdapter] getChainFingerprint address:", address?.substring(0, 20));
|
|
442
|
+
return success(deriveDeviceFingerprint(address));
|
|
443
|
+
} catch (err) {
|
|
444
|
+
console.error(
|
|
445
|
+
"[LedgerAdapter] getChainFingerprint error in _deriveAddressForFingerprint:",
|
|
446
|
+
chain,
|
|
447
|
+
err
|
|
448
|
+
);
|
|
449
|
+
return this.errorToFailure(err);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Verify that the connected device matches the expected fingerprint.
|
|
454
|
+
*
|
|
455
|
+
* - If deviceId is empty, verification is skipped (returns true).
|
|
456
|
+
* - deviceId is used here as the stored fingerprint to compare against.
|
|
457
|
+
*/
|
|
458
|
+
async _verifyDeviceFingerprint(connectId, deviceId, chain) {
|
|
459
|
+
if (!deviceId) return true;
|
|
460
|
+
try {
|
|
461
|
+
const address = await this._deriveAddressForFingerprint(connectId, chain);
|
|
462
|
+
const fingerprint = deriveDeviceFingerprint(address);
|
|
463
|
+
return fingerprint === deviceId;
|
|
464
|
+
} catch (err) {
|
|
465
|
+
const mapped = mapLedgerError(err);
|
|
466
|
+
if (mapped.code === HardwareErrorCode2.WrongApp || mapped.code === HardwareErrorCode2.DeviceLocked) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
throw err;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Derive an address at the fixed testnet path for fingerprint generation.
|
|
474
|
+
*/
|
|
475
|
+
async _deriveAddressForFingerprint(connectId, chain) {
|
|
476
|
+
const path = CHAIN_FINGERPRINT_PATHS[chain];
|
|
477
|
+
if (chain === "evm") {
|
|
478
|
+
const result = await this.connectorCall(connectId, "evmGetAddress", {
|
|
479
|
+
path,
|
|
480
|
+
showOnDevice: false
|
|
481
|
+
});
|
|
482
|
+
return result.address;
|
|
483
|
+
}
|
|
484
|
+
if (chain === "btc") {
|
|
485
|
+
const result = await this.connectorCall(connectId, "btcGetPublicKey", {
|
|
486
|
+
path,
|
|
487
|
+
showOnDevice: false
|
|
488
|
+
});
|
|
489
|
+
return result.xpub;
|
|
490
|
+
}
|
|
491
|
+
if (chain === "sol") {
|
|
492
|
+
const result = await this.connectorCall(connectId, "solGetAddress", {
|
|
493
|
+
path,
|
|
494
|
+
showOnDevice: false
|
|
495
|
+
});
|
|
496
|
+
return result.address;
|
|
497
|
+
}
|
|
498
|
+
if (chain === "tron") {
|
|
499
|
+
const result = await this.connectorCall(connectId, "tronGetAddress", {
|
|
500
|
+
path,
|
|
501
|
+
showOnDevice: false
|
|
502
|
+
});
|
|
503
|
+
return result.address;
|
|
504
|
+
}
|
|
505
|
+
throw new Error(`Unsupported chain for fingerprint: ${chain}`);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Wait for user to connect and unlock device.
|
|
509
|
+
* Emits 'ui-request' event via the adapter's own emitter.
|
|
510
|
+
* The consumer (monorepo adapter wrapper) listens for this and shows UI.
|
|
511
|
+
* When user confirms, they call adapter.deviceConnectResponse() which resolves this promise.
|
|
512
|
+
* Times out after 60 seconds if no response is received.
|
|
513
|
+
*/
|
|
514
|
+
_waitForDeviceConnect(attempt) {
|
|
515
|
+
return new Promise((resolve, reject) => {
|
|
516
|
+
let settled = false;
|
|
517
|
+
const timer = setTimeout(() => {
|
|
518
|
+
if (!settled) {
|
|
519
|
+
settled = true;
|
|
520
|
+
this._deviceConnectResolve = null;
|
|
521
|
+
reject(new Error("Ledger device connect timed out after 60 seconds"));
|
|
522
|
+
}
|
|
523
|
+
}, _LedgerAdapter.DEVICE_CONNECT_TIMEOUT_MS);
|
|
524
|
+
this._deviceConnectResolve = (cancelled) => {
|
|
525
|
+
if (settled) return;
|
|
526
|
+
settled = true;
|
|
527
|
+
clearTimeout(timer);
|
|
528
|
+
this._deviceConnectResolve = null;
|
|
529
|
+
if (cancelled) {
|
|
530
|
+
reject(
|
|
531
|
+
Object.assign(new Error("User cancelled Ledger connection"), {
|
|
532
|
+
_tag: "DeviceNotRecognizedError"
|
|
533
|
+
})
|
|
534
|
+
);
|
|
535
|
+
} else {
|
|
536
|
+
resolve();
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_CONNECT, {
|
|
540
|
+
type: UI_REQUEST.REQUEST_DEVICE_CONNECT,
|
|
541
|
+
payload: {
|
|
542
|
+
message: "Please connect and unlock your Ledger device",
|
|
543
|
+
retryCount: attempt,
|
|
544
|
+
maxRetries: _LedgerAdapter.MAX_DEVICE_RETRY
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Called by consumer to respond to ui-request-device-connect.
|
|
551
|
+
* type='confirm' → retry search, type='cancel' → abort.
|
|
552
|
+
*/
|
|
553
|
+
deviceConnectResponse(type) {
|
|
554
|
+
if (this._deviceConnectResolve) {
|
|
555
|
+
this._deviceConnectResolve(type === "cancel");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async ensureConnected(connectId) {
|
|
559
|
+
if (connectId && this._sessions.has(connectId)) {
|
|
560
|
+
return connectId;
|
|
561
|
+
}
|
|
562
|
+
if (this._sessions.size > 0) {
|
|
563
|
+
return this._sessions.keys().next().value;
|
|
564
|
+
}
|
|
565
|
+
if (this._connectingPromise) {
|
|
566
|
+
return this._connectingPromise;
|
|
567
|
+
}
|
|
568
|
+
this._connectingPromise = this._doConnect();
|
|
569
|
+
try {
|
|
570
|
+
return await this._connectingPromise;
|
|
571
|
+
} finally {
|
|
572
|
+
this._connectingPromise = null;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async _doConnect() {
|
|
576
|
+
for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
|
|
577
|
+
const devices = await this.searchDevices();
|
|
578
|
+
if (devices.length > 0) {
|
|
579
|
+
return this._connectFirstOrSelect(devices);
|
|
580
|
+
}
|
|
581
|
+
if (attempt < _LedgerAdapter.MAX_DEVICE_RETRY - 1) {
|
|
582
|
+
await this._waitForDeviceConnect(attempt + 1);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
throw Object.assign(
|
|
586
|
+
new Error(
|
|
587
|
+
"No Ledger device found after multiple attempts. Please connect and unlock your device."
|
|
588
|
+
),
|
|
589
|
+
{ _tag: "DeviceNotRecognizedError" }
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
async _connectFirstOrSelect(devices) {
|
|
593
|
+
if (devices.length === 1) {
|
|
594
|
+
const result2 = await this.connectDevice(devices[0].connectId);
|
|
595
|
+
if (!result2.success) {
|
|
596
|
+
throw Object.assign(new Error(result2.payload.error), { _tag: "DeviceNotRecognizedError" });
|
|
597
|
+
}
|
|
598
|
+
return devices[0].connectId;
|
|
599
|
+
}
|
|
600
|
+
if (this._uiHandler?.onSelectDevice) {
|
|
601
|
+
const selectedConnectId = await this._uiHandler.onSelectDevice(devices);
|
|
602
|
+
const result2 = await this.connectDevice(selectedConnectId);
|
|
603
|
+
if (!result2.success) {
|
|
604
|
+
throw Object.assign(new Error(result2.payload.error), { _tag: "DeviceNotRecognizedError" });
|
|
605
|
+
}
|
|
606
|
+
return selectedConnectId;
|
|
607
|
+
}
|
|
608
|
+
const result = await this.connectDevice(devices[0].connectId);
|
|
609
|
+
if (!result.success) {
|
|
610
|
+
throw Object.assign(new Error(result.payload.error), { _tag: "DeviceNotRecognizedError" });
|
|
611
|
+
}
|
|
612
|
+
return devices[0].connectId;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Call the connector with automatic session resolution and disconnect retry.
|
|
616
|
+
*
|
|
617
|
+
* 1. Resolves a valid connectId via ensureConnected()
|
|
618
|
+
* 2. Looks up sessionId from _sessions
|
|
619
|
+
* 3. Calls connector.call()
|
|
620
|
+
* 4. On disconnect error: clears stale session, re-connects, retries once
|
|
621
|
+
*/
|
|
622
|
+
async connectorCall(connectId, method, params) {
|
|
623
|
+
console.log("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
|
|
624
|
+
const resolvedConnectId = await this.ensureConnected(connectId);
|
|
625
|
+
const sessionId = this._sessions.get(resolvedConnectId);
|
|
626
|
+
console.log(
|
|
627
|
+
"[LedgerAdapter] connectorCall resolved:",
|
|
628
|
+
method,
|
|
629
|
+
"resolvedConnectId:",
|
|
630
|
+
resolvedConnectId,
|
|
631
|
+
"sessionId:",
|
|
632
|
+
sessionId
|
|
633
|
+
);
|
|
634
|
+
if (!sessionId) {
|
|
635
|
+
throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
|
|
636
|
+
_tag: "DeviceSessionNotFound"
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
return await this.connector.call(sessionId, method, params);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
const errObj = err;
|
|
643
|
+
console.log("[LedgerAdapter] connectorCall error:", method, {
|
|
644
|
+
message: errObj?.message,
|
|
645
|
+
_tag: errObj?._tag,
|
|
646
|
+
errorCode: errObj?.errorCode,
|
|
647
|
+
statusCode: errObj?.statusCode,
|
|
648
|
+
isDisconnected: isDeviceDisconnectedError(err),
|
|
649
|
+
isLocked: isDeviceLockedError(err)
|
|
650
|
+
});
|
|
651
|
+
if (isDeviceDisconnectedError(err)) {
|
|
652
|
+
console.log("[LedgerAdapter] disconnected, retrying with fresh connection...");
|
|
653
|
+
this._discoveredDevices.clear();
|
|
654
|
+
return this._retryWithFreshConnection(resolvedConnectId, method, params, err);
|
|
655
|
+
}
|
|
656
|
+
if (isDeviceLockedError(err)) {
|
|
657
|
+
await this._waitForDeviceConnect(0);
|
|
658
|
+
return this.connector.call(sessionId, method, params);
|
|
659
|
+
}
|
|
660
|
+
if (isTimeoutError(err)) {
|
|
661
|
+
console.log("[LedgerAdapter] timeout, retrying with fresh connection...");
|
|
662
|
+
this._discoveredDevices.delete(resolvedConnectId);
|
|
663
|
+
return this._retryWithFreshConnection(resolvedConnectId, method, params, err);
|
|
664
|
+
}
|
|
665
|
+
throw err;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/** Clear stale session, reconnect, and retry the call. */
|
|
669
|
+
async _retryWithFreshConnection(resolvedConnectId, method, params, originalErr) {
|
|
670
|
+
this._sessions.delete(resolvedConnectId);
|
|
671
|
+
const retryConnectId = await this.ensureConnected();
|
|
672
|
+
const retrySessionId = this._sessions.get(retryConnectId);
|
|
673
|
+
if (!retrySessionId) {
|
|
674
|
+
throw originalErr;
|
|
675
|
+
}
|
|
676
|
+
return this.connector.call(retrySessionId, method, params);
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Ensure device permission before proceeding.
|
|
680
|
+
* - No connectId (searchDevices): check environment-level permission
|
|
681
|
+
* - With connectId (business methods): check device-level permission
|
|
682
|
+
* If not granted, calls onDevicePermission so the consumer can request access.
|
|
683
|
+
*/
|
|
684
|
+
async _ensureDevicePermission(connectId, deviceId) {
|
|
685
|
+
const transportType = "hid";
|
|
686
|
+
let granted = false;
|
|
687
|
+
let context;
|
|
688
|
+
if (this._uiHandler?.checkDevicePermission) {
|
|
689
|
+
try {
|
|
690
|
+
const result = await this._uiHandler.checkDevicePermission({
|
|
691
|
+
transportType,
|
|
692
|
+
connectId,
|
|
693
|
+
deviceId
|
|
694
|
+
});
|
|
695
|
+
granted = result.granted;
|
|
696
|
+
context = result.context;
|
|
697
|
+
} catch {
|
|
698
|
+
granted = false;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (!granted) {
|
|
702
|
+
try {
|
|
703
|
+
await this._uiHandler?.onDevicePermission?.({ transportType, context });
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Convert a thrown error to a Response failure.
|
|
710
|
+
* Uses mapLedgerError to parse Ledger DMK error codes into HardwareErrorCode values.
|
|
711
|
+
*/
|
|
712
|
+
errorToFailure(err) {
|
|
713
|
+
console.error("[LedgerAdapter] error:", err);
|
|
714
|
+
if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
|
|
715
|
+
const e = err;
|
|
716
|
+
return failure(e.code, e.message ?? "Unknown error");
|
|
717
|
+
}
|
|
718
|
+
const mapped = mapLedgerError(err);
|
|
719
|
+
return failure(mapped.code, mapped.message);
|
|
720
|
+
}
|
|
721
|
+
registerEventListeners() {
|
|
722
|
+
this.connector.on("device-connect", this.deviceConnectHandler);
|
|
723
|
+
this.connector.on("device-disconnect", this.deviceDisconnectHandler);
|
|
724
|
+
this.connector.on("ui-request", this.uiRequestHandler);
|
|
725
|
+
this.connector.on("ui-event", this.uiEventHandler);
|
|
726
|
+
}
|
|
727
|
+
unregisterEventListeners() {
|
|
728
|
+
this.connector.off("device-connect", this.deviceConnectHandler);
|
|
729
|
+
this.connector.off("device-disconnect", this.deviceDisconnectHandler);
|
|
730
|
+
this.connector.off("ui-request", this.uiRequestHandler);
|
|
731
|
+
this.connector.off("ui-event", this.uiEventHandler);
|
|
732
|
+
}
|
|
733
|
+
handleUiEvent(event) {
|
|
734
|
+
if (!event.type) return;
|
|
735
|
+
const payload = event.payload;
|
|
736
|
+
const deviceInfo = payload ? this.extractDeviceInfoFromPayload(payload) : this.unknownDevice();
|
|
737
|
+
switch (event.type) {
|
|
738
|
+
case "ui-request_confirmation":
|
|
739
|
+
this.emitter.emit(UI_REQUEST.REQUEST_BUTTON, {
|
|
740
|
+
type: UI_REQUEST.REQUEST_BUTTON,
|
|
741
|
+
payload: { device: deviceInfo }
|
|
742
|
+
});
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// ---------------------------------------------------------------------------
|
|
747
|
+
// Device info mapping
|
|
748
|
+
// ---------------------------------------------------------------------------
|
|
749
|
+
connectorDeviceToDeviceInfo(device) {
|
|
750
|
+
const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
|
|
751
|
+
return {
|
|
752
|
+
vendor: "ledger",
|
|
753
|
+
model: device.model ?? "unknown",
|
|
754
|
+
firmwareVersion: "",
|
|
755
|
+
deviceId: device.deviceId,
|
|
756
|
+
connectId: device.connectId,
|
|
757
|
+
label: device.name,
|
|
758
|
+
connectionType: isBle ? "ble" : "usb",
|
|
759
|
+
capabilities: device.capabilities
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
extractDeviceInfoFromPayload(payload) {
|
|
763
|
+
return {
|
|
764
|
+
vendor: "ledger",
|
|
765
|
+
model: payload["model"] ?? "unknown",
|
|
766
|
+
firmwareVersion: "",
|
|
767
|
+
deviceId: payload["deviceId"] ?? payload["id"] ?? "",
|
|
768
|
+
connectId: payload["connectId"] ?? payload["path"] ?? "",
|
|
769
|
+
label: payload["label"],
|
|
770
|
+
connectionType: "usb"
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
unknownDevice() {
|
|
774
|
+
return {
|
|
775
|
+
vendor: "ledger",
|
|
776
|
+
model: "unknown",
|
|
777
|
+
firmwareVersion: "",
|
|
778
|
+
deviceId: "",
|
|
779
|
+
connectId: "",
|
|
780
|
+
connectionType: "usb"
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
// ---------------------------------------------------------------------------
|
|
785
|
+
// Private helpers
|
|
786
|
+
// ---------------------------------------------------------------------------
|
|
787
|
+
/**
|
|
788
|
+
* Ensure at least one device is connected and return a valid connectId.
|
|
789
|
+
*
|
|
790
|
+
* - If a session already exists for the given connectId, reuse it.
|
|
791
|
+
* - If ANY session exists (Ledger IDs are ephemeral), reuse it.
|
|
792
|
+
* - Otherwise: search → 1 device: auto-connect, multiple: ask user, 0: throw.
|
|
793
|
+
*/
|
|
794
|
+
_LedgerAdapter.MAX_DEVICE_RETRY = 3;
|
|
795
|
+
_LedgerAdapter.DEVICE_CONNECT_TIMEOUT_MS = 6e4;
|
|
796
|
+
var LedgerAdapter = _LedgerAdapter;
|
|
797
|
+
|
|
798
|
+
// src/device/LedgerDeviceManager.ts
|
|
799
|
+
var LedgerDeviceManager = class {
|
|
800
|
+
constructor(dmk) {
|
|
801
|
+
this._discovered = /* @__PURE__ */ new Map();
|
|
802
|
+
this._sessions = /* @__PURE__ */ new Map();
|
|
803
|
+
// deviceId → sessionId
|
|
804
|
+
this._sessionToDevice = /* @__PURE__ */ new Map();
|
|
805
|
+
// sessionId → deviceId
|
|
806
|
+
this._listenSub = null;
|
|
807
|
+
/**
|
|
808
|
+
* Trigger browser device selection (WebHID requestDevice).
|
|
809
|
+
* Starts discovery for a short period, then stops.
|
|
810
|
+
*/
|
|
811
|
+
/**
|
|
812
|
+
* Start BLE discovery if not already running.
|
|
813
|
+
* Does NOT stop — DMK keeps scanning in the background so that
|
|
814
|
+
* listenToAvailableDevices() always has fresh data.
|
|
815
|
+
*/
|
|
816
|
+
this._discoverySub = null;
|
|
817
|
+
this._dmk = dmk;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* One-shot enumeration: subscribe to listenToAvailableDevices,
|
|
821
|
+
* take the first emission, unsubscribe, return DeviceDescriptors.
|
|
822
|
+
*/
|
|
823
|
+
enumerate() {
|
|
824
|
+
console.log(
|
|
825
|
+
"[DMK] enumerate() called, dmk exists:",
|
|
826
|
+
!!this._dmk,
|
|
827
|
+
"listenToAvailableDevices exists:",
|
|
828
|
+
typeof this._dmk?.listenToAvailableDevices
|
|
829
|
+
);
|
|
830
|
+
return new Promise((resolve) => {
|
|
831
|
+
let resolved = false;
|
|
832
|
+
let syncResult = null;
|
|
833
|
+
let sub = null;
|
|
834
|
+
sub = this._dmk.listenToAvailableDevices({}).subscribe({
|
|
835
|
+
next: (devices) => {
|
|
836
|
+
if (resolved) return;
|
|
837
|
+
resolved = true;
|
|
838
|
+
this._discovered.clear();
|
|
839
|
+
console.log(
|
|
840
|
+
"[DMK] enumerate raw devices:",
|
|
841
|
+
JSON.stringify(
|
|
842
|
+
devices.map((d) => ({
|
|
843
|
+
id: d.id,
|
|
844
|
+
deviceModel: d.deviceModel,
|
|
845
|
+
transport: d.transport,
|
|
846
|
+
name: d.name,
|
|
847
|
+
rssi: d.rssi,
|
|
848
|
+
// Dump all keys to see what else is available
|
|
849
|
+
_keys: Object.keys(d)
|
|
850
|
+
}))
|
|
851
|
+
)
|
|
852
|
+
);
|
|
853
|
+
for (const d of devices) {
|
|
854
|
+
this._discovered.set(d.id, d);
|
|
855
|
+
}
|
|
856
|
+
if (sub) {
|
|
857
|
+
sub.unsubscribe();
|
|
858
|
+
resolve(
|
|
859
|
+
devices.map((d) => ({
|
|
860
|
+
path: d.id,
|
|
861
|
+
type: d.deviceModel.model,
|
|
862
|
+
name: d.name,
|
|
863
|
+
transport: d.transport
|
|
864
|
+
}))
|
|
865
|
+
);
|
|
866
|
+
} else {
|
|
867
|
+
syncResult = devices;
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
error: () => {
|
|
871
|
+
if (!resolved) {
|
|
872
|
+
resolved = true;
|
|
873
|
+
resolve([]);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
if (syncResult !== null) {
|
|
878
|
+
sub.unsubscribe();
|
|
879
|
+
const devices = syncResult;
|
|
880
|
+
resolve(
|
|
881
|
+
devices.map((d) => ({
|
|
882
|
+
path: d.id,
|
|
883
|
+
type: d.deviceModel.model,
|
|
884
|
+
name: d.name,
|
|
885
|
+
transport: d.transport
|
|
886
|
+
}))
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Continuous listening: tracks device connect/disconnect via diffing.
|
|
893
|
+
*/
|
|
894
|
+
listen(onChange) {
|
|
895
|
+
this.stopListening();
|
|
896
|
+
let previousIds = /* @__PURE__ */ new Set();
|
|
897
|
+
this._listenSub = this._dmk.listenToAvailableDevices({}).subscribe({
|
|
898
|
+
next: (devices) => {
|
|
899
|
+
const currentIds = new Set(devices.map((d) => d.id));
|
|
900
|
+
for (const d of devices) {
|
|
901
|
+
this._discovered.set(d.id, d);
|
|
902
|
+
console.log(
|
|
903
|
+
"[DMK] listen device:",
|
|
904
|
+
JSON.stringify({
|
|
905
|
+
id: d.id,
|
|
906
|
+
deviceModel: d.deviceModel,
|
|
907
|
+
name: d.name
|
|
908
|
+
})
|
|
909
|
+
);
|
|
910
|
+
if (!previousIds.has(d.id)) {
|
|
911
|
+
onChange({
|
|
912
|
+
type: "device-connected",
|
|
913
|
+
descriptor: {
|
|
914
|
+
path: d.id,
|
|
915
|
+
type: d.deviceModel.model,
|
|
916
|
+
name: d.name,
|
|
917
|
+
transport: d.transport
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
for (const id of previousIds) {
|
|
923
|
+
if (!currentIds.has(id)) {
|
|
924
|
+
this._discovered.delete(id);
|
|
925
|
+
onChange({ type: "device-disconnected", descriptor: { path: id } });
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
previousIds = currentIds;
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
stopListening() {
|
|
933
|
+
this._listenSub?.unsubscribe();
|
|
934
|
+
this._listenSub = null;
|
|
935
|
+
}
|
|
936
|
+
requestDevice() {
|
|
937
|
+
if (this._discoverySub) {
|
|
938
|
+
return Promise.resolve();
|
|
939
|
+
}
|
|
940
|
+
console.log("[DMK] requestDevice() starting persistent BLE scan");
|
|
941
|
+
this._discoverySub = this._dmk.startDiscovering({}).subscribe({
|
|
942
|
+
next: (d) => {
|
|
943
|
+
console.log("[DMK] BLE discovered:", d.name || d.id);
|
|
944
|
+
this._discovered.set(d.id, d);
|
|
945
|
+
},
|
|
946
|
+
error: (err) => {
|
|
947
|
+
console.error("[DMK] BLE scan error:", err);
|
|
948
|
+
this._discoverySub = null;
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
return Promise.resolve();
|
|
952
|
+
}
|
|
953
|
+
/** Connect to a previously discovered device. Returns sessionId. */
|
|
954
|
+
async connect(deviceId) {
|
|
955
|
+
const device = this._discovered.get(deviceId);
|
|
956
|
+
if (!device) {
|
|
957
|
+
throw new Error(`Device "${deviceId}" not found. Call enumerate() or listen() first.`);
|
|
958
|
+
}
|
|
959
|
+
const sessionId = await this._dmk.connect({ device });
|
|
960
|
+
this._sessions.set(deviceId, sessionId);
|
|
961
|
+
this._sessionToDevice.set(sessionId, deviceId);
|
|
962
|
+
return sessionId;
|
|
963
|
+
}
|
|
964
|
+
/** Disconnect a session. */
|
|
965
|
+
async disconnect(sessionId) {
|
|
966
|
+
await this._dmk.disconnect({ sessionId });
|
|
967
|
+
const deviceId = this._sessionToDevice.get(sessionId);
|
|
968
|
+
if (deviceId) this._sessions.delete(deviceId);
|
|
969
|
+
this._sessionToDevice.delete(sessionId);
|
|
970
|
+
}
|
|
971
|
+
getSessionId(deviceId) {
|
|
972
|
+
return this._sessions.get(deviceId);
|
|
973
|
+
}
|
|
974
|
+
getDeviceId(sessionId) {
|
|
975
|
+
return this._sessionToDevice.get(sessionId);
|
|
976
|
+
}
|
|
977
|
+
/** Get the underlying DMK instance (needed by SignerManager). */
|
|
978
|
+
getDmk() {
|
|
979
|
+
return this._dmk;
|
|
980
|
+
}
|
|
981
|
+
stopDiscovery() {
|
|
982
|
+
if (this._discoverySub) {
|
|
983
|
+
this._discoverySub.unsubscribe();
|
|
984
|
+
this._discoverySub = null;
|
|
985
|
+
this._dmk.stopDiscovering();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
dispose() {
|
|
989
|
+
this.stopListening();
|
|
990
|
+
this.stopDiscovery();
|
|
991
|
+
this._discovered.clear();
|
|
992
|
+
this._sessions.clear();
|
|
993
|
+
this._sessionToDevice.clear();
|
|
994
|
+
this._dmk.close();
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
// src/signer/deviceActionToPromise.ts
|
|
999
|
+
import { DeviceActionStatus } from "@ledgerhq/device-management-kit";
|
|
1000
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1001
|
+
function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1002
|
+
return new Promise((resolve, reject) => {
|
|
1003
|
+
let settled = false;
|
|
1004
|
+
let sub;
|
|
1005
|
+
let timer = null;
|
|
1006
|
+
const resetTimer = () => {
|
|
1007
|
+
if (timer) clearTimeout(timer);
|
|
1008
|
+
if (timeoutMs > 0) {
|
|
1009
|
+
timer = setTimeout(() => {
|
|
1010
|
+
if (!settled) {
|
|
1011
|
+
settled = true;
|
|
1012
|
+
sub?.unsubscribe();
|
|
1013
|
+
reject(new Error("Device action timed out \u2014 device may be locked or disconnected"));
|
|
1014
|
+
}
|
|
1015
|
+
}, timeoutMs);
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
resetTimer();
|
|
1019
|
+
console.log("[DMK-Observable] subscribing to action.observable...");
|
|
1020
|
+
sub = action.observable.subscribe({
|
|
1021
|
+
next: (state) => {
|
|
1022
|
+
console.log("[DMK-Observable] next received");
|
|
1023
|
+
resetTimer();
|
|
1024
|
+
console.log(
|
|
1025
|
+
"[DMK-Observable] state:",
|
|
1026
|
+
JSON.stringify({
|
|
1027
|
+
status: state.status,
|
|
1028
|
+
intermediateValue: state.status === DeviceActionStatus.Pending ? state.intermediateValue : void 0,
|
|
1029
|
+
hasOutput: state.status === DeviceActionStatus.Completed,
|
|
1030
|
+
hasError: state.status === DeviceActionStatus.Error
|
|
1031
|
+
})
|
|
1032
|
+
);
|
|
1033
|
+
if (settled) return;
|
|
1034
|
+
if (state.status === DeviceActionStatus.Completed) {
|
|
1035
|
+
settled = true;
|
|
1036
|
+
if (timer) clearTimeout(timer);
|
|
1037
|
+
onInteraction?.("interaction-complete");
|
|
1038
|
+
sub?.unsubscribe();
|
|
1039
|
+
resolve(state.output);
|
|
1040
|
+
} else if (state.status === DeviceActionStatus.Error) {
|
|
1041
|
+
settled = true;
|
|
1042
|
+
if (timer) clearTimeout(timer);
|
|
1043
|
+
onInteraction?.("interaction-complete");
|
|
1044
|
+
sub?.unsubscribe();
|
|
1045
|
+
reject(state.error);
|
|
1046
|
+
} else if (state.status === DeviceActionStatus.Pending && onInteraction) {
|
|
1047
|
+
const interaction = state.intermediateValue?.requiredUserInteraction;
|
|
1048
|
+
if (interaction && interaction !== "none") {
|
|
1049
|
+
onInteraction(String(interaction));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
error: (err) => {
|
|
1054
|
+
if (!settled) {
|
|
1055
|
+
settled = true;
|
|
1056
|
+
if (timer) clearTimeout(timer);
|
|
1057
|
+
sub?.unsubscribe();
|
|
1058
|
+
reject(err);
|
|
1059
|
+
}
|
|
1060
|
+
},
|
|
1061
|
+
complete: () => {
|
|
1062
|
+
if (!settled) {
|
|
1063
|
+
settled = true;
|
|
1064
|
+
if (timer) clearTimeout(timer);
|
|
1065
|
+
reject(new Error("Device action completed without result"));
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/signer/SignerEth.ts
|
|
1073
|
+
function hexToBytes(hex) {
|
|
1074
|
+
const h = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1075
|
+
const bytes = new Uint8Array(h.length / 2);
|
|
1076
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1077
|
+
bytes[i] = parseInt(h.substring(i * 2, i * 2 + 2), 16);
|
|
1078
|
+
}
|
|
1079
|
+
return bytes;
|
|
1080
|
+
}
|
|
1081
|
+
var INTERACTIVE_TIMEOUT_MS = 5 * 6e4;
|
|
1082
|
+
var SignerEth = class {
|
|
1083
|
+
constructor(_sdk) {
|
|
1084
|
+
this._sdk = _sdk;
|
|
1085
|
+
}
|
|
1086
|
+
async getAddress(derivationPath, options) {
|
|
1087
|
+
const checkOnDevice = options?.checkOnDevice ?? false;
|
|
1088
|
+
console.log("[DMK] getAddress \u2192 DMK:", { derivationPath, checkOnDevice });
|
|
1089
|
+
const action = this._sdk.getAddress(derivationPath, {
|
|
1090
|
+
checkOnDevice
|
|
1091
|
+
});
|
|
1092
|
+
const timeout = checkOnDevice ? INTERACTIVE_TIMEOUT_MS : void 0;
|
|
1093
|
+
return deviceActionToPromise(action, this.onInteraction, timeout);
|
|
1094
|
+
}
|
|
1095
|
+
async signTransaction(derivationPath, serializedTxHex) {
|
|
1096
|
+
const action = this._sdk.signTransaction(derivationPath, hexToBytes(serializedTxHex));
|
|
1097
|
+
return deviceActionToPromise(
|
|
1098
|
+
action,
|
|
1099
|
+
this.onInteraction,
|
|
1100
|
+
INTERACTIVE_TIMEOUT_MS
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
async signMessage(derivationPath, message) {
|
|
1104
|
+
const action = this._sdk.signMessage(derivationPath, hexToBytes(message));
|
|
1105
|
+
return deviceActionToPromise(
|
|
1106
|
+
action,
|
|
1107
|
+
this.onInteraction,
|
|
1108
|
+
INTERACTIVE_TIMEOUT_MS
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
async signTypedData(derivationPath, data) {
|
|
1112
|
+
const action = this._sdk.signTypedData(derivationPath, data);
|
|
1113
|
+
return deviceActionToPromise(
|
|
1114
|
+
action,
|
|
1115
|
+
this.onInteraction,
|
|
1116
|
+
INTERACTIVE_TIMEOUT_MS
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
// src/signer/SignerManager.ts
|
|
1122
|
+
var SignerManager = class _SignerManager {
|
|
1123
|
+
constructor(dmk, builderFn) {
|
|
1124
|
+
this._cache = /* @__PURE__ */ new Map();
|
|
1125
|
+
this._dmk = dmk;
|
|
1126
|
+
this._builderFn = builderFn ?? _SignerManager._defaultBuilder();
|
|
1127
|
+
}
|
|
1128
|
+
async getOrCreate(sessionId) {
|
|
1129
|
+
const hadCached = this._cache.has(sessionId);
|
|
1130
|
+
this._cache.delete(sessionId);
|
|
1131
|
+
console.log("[DMK] SignerManager.getOrCreate:", { sessionId, hadCached, creating: true });
|
|
1132
|
+
const builder = await this._builderFn({ dmk: this._dmk, sessionId });
|
|
1133
|
+
const sdkSigner = builder.build();
|
|
1134
|
+
console.log("[DMK] SignerManager: new signer built");
|
|
1135
|
+
const signer = new SignerEth(sdkSigner);
|
|
1136
|
+
this._cache.set(sessionId, signer);
|
|
1137
|
+
return signer;
|
|
1138
|
+
}
|
|
1139
|
+
invalidate(sessionId) {
|
|
1140
|
+
this._cache.delete(sessionId);
|
|
1141
|
+
}
|
|
1142
|
+
clearAll() {
|
|
1143
|
+
this._cache.clear();
|
|
1144
|
+
}
|
|
1145
|
+
static _defaultBuilder() {
|
|
1146
|
+
let BuilderClass = null;
|
|
1147
|
+
return async (args) => {
|
|
1148
|
+
if (!BuilderClass) {
|
|
1149
|
+
const mod = await import("@ledgerhq/device-signer-kit-ethereum");
|
|
1150
|
+
BuilderClass = mod.SignerEthBuilder;
|
|
1151
|
+
}
|
|
1152
|
+
return new BuilderClass(args);
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
// src/connector/chains/evm.ts
|
|
1158
|
+
import { stripHex, padHex64, HardwareErrorCode as HardwareErrorCode3 } from "@onekeyfe/hwk-adapter-core";
|
|
1159
|
+
|
|
1160
|
+
// src/connector/chains/utils.ts
|
|
1161
|
+
function normalizePath(path) {
|
|
1162
|
+
return path.startsWith("m/") ? path.slice(2) : path;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/connector/chains/evm.ts
|
|
1166
|
+
async function evmGetAddress(ctx, sessionId, params) {
|
|
1167
|
+
const signer = await _getEthSigner(ctx, sessionId);
|
|
1168
|
+
const path = normalizePath(params.path);
|
|
1169
|
+
const checkOnDevice = params.showOnDevice ?? false;
|
|
1170
|
+
console.log("[DMK] evmGetAddress -> signer.getAddress:", { path, checkOnDevice });
|
|
1171
|
+
try {
|
|
1172
|
+
const result = await signer.getAddress(path, { checkOnDevice });
|
|
1173
|
+
return { address: result.address, publicKey: result.publicKey };
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
ctx.invalidateSession(sessionId);
|
|
1176
|
+
throw ctx.wrapError(err);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
async function evmSignTransaction(ctx, sessionId, params) {
|
|
1180
|
+
if (!params.serializedTx) {
|
|
1181
|
+
throw Object.assign(
|
|
1182
|
+
new Error(
|
|
1183
|
+
"Ledger requires a pre-serialized transaction (serializedTx). Provide an RLP-encoded hex string."
|
|
1184
|
+
),
|
|
1185
|
+
{ code: HardwareErrorCode3.InvalidParams }
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
const signer = await _getEthSigner(ctx, sessionId);
|
|
1189
|
+
const path = normalizePath(params.path);
|
|
1190
|
+
try {
|
|
1191
|
+
const result = await signer.signTransaction(path, params.serializedTx);
|
|
1192
|
+
return {
|
|
1193
|
+
v: `0x${result.v.toString(16)}`,
|
|
1194
|
+
r: padHex64(result.r),
|
|
1195
|
+
s: padHex64(result.s)
|
|
1196
|
+
};
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
ctx.invalidateSession(sessionId);
|
|
1199
|
+
throw ctx.wrapError(err);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
async function evmSignMessage(ctx, sessionId, params) {
|
|
1203
|
+
const signer = await _getEthSigner(ctx, sessionId);
|
|
1204
|
+
const path = normalizePath(params.path);
|
|
1205
|
+
try {
|
|
1206
|
+
const result = await signer.signMessage(path, params.message);
|
|
1207
|
+
const rHex = stripHex(result.r).padStart(64, "0");
|
|
1208
|
+
const sHex = stripHex(result.s).padStart(64, "0");
|
|
1209
|
+
const vHex = result.v.toString(16).padStart(2, "0");
|
|
1210
|
+
return { signature: `0x${rHex}${sHex}${vHex}` };
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
ctx.invalidateSession(sessionId);
|
|
1213
|
+
throw ctx.wrapError(err);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
async function evmSignTypedData(ctx, sessionId, params) {
|
|
1217
|
+
if (params.mode === "hash") {
|
|
1218
|
+
throw Object.assign(
|
|
1219
|
+
new Error(
|
|
1220
|
+
'Ledger does not support hash-only EIP-712 signing. Use mode "full" with the complete typed data structure.'
|
|
1221
|
+
),
|
|
1222
|
+
{ code: HardwareErrorCode3.MethodNotSupported }
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
const signer = await _getEthSigner(ctx, sessionId);
|
|
1226
|
+
const path = normalizePath(params.path);
|
|
1227
|
+
try {
|
|
1228
|
+
const result = await signer.signTypedData(path, params.data);
|
|
1229
|
+
const rHex = stripHex(result.r).padStart(64, "0");
|
|
1230
|
+
const sHex = stripHex(result.s).padStart(64, "0");
|
|
1231
|
+
const vHex = result.v.toString(16).padStart(2, "0");
|
|
1232
|
+
return { signature: `0x${rHex}${sHex}${vHex}` };
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
ctx.invalidateSession(sessionId);
|
|
1235
|
+
throw ctx.wrapError(err);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
async function _getEthSigner(ctx, sessionId) {
|
|
1239
|
+
const signerManager = await ctx.getSignerManager();
|
|
1240
|
+
const signer = await signerManager.getOrCreate(sessionId);
|
|
1241
|
+
signer.onInteraction = (interaction) => {
|
|
1242
|
+
ctx.emit("ui-event", {
|
|
1243
|
+
type: interaction,
|
|
1244
|
+
payload: { sessionId }
|
|
1245
|
+
});
|
|
1246
|
+
};
|
|
1247
|
+
return signer;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// src/connector/chains/btc.ts
|
|
1251
|
+
import {
|
|
1252
|
+
stripHex as stripHex2,
|
|
1253
|
+
hexToBytes as hexToBytes2,
|
|
1254
|
+
bytesToHex,
|
|
1255
|
+
HardwareErrorCode as HardwareErrorCode4
|
|
1256
|
+
} from "@onekeyfe/hwk-adapter-core";
|
|
1257
|
+
|
|
1258
|
+
// src/signer/SignerBtc.ts
|
|
1259
|
+
function hexToUtf8(hex) {
|
|
1260
|
+
const h = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1261
|
+
const bytes = new Uint8Array(h.length / 2);
|
|
1262
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1263
|
+
bytes[i] = parseInt(h.substring(i * 2, i * 2 + 2), 16);
|
|
1264
|
+
}
|
|
1265
|
+
return new TextDecoder().decode(bytes);
|
|
1266
|
+
}
|
|
1267
|
+
var INTERACTIVE_TIMEOUT_MS2 = 5 * 6e4;
|
|
1268
|
+
var SignerBtc = class {
|
|
1269
|
+
constructor(_sdk) {
|
|
1270
|
+
this._sdk = _sdk;
|
|
1271
|
+
}
|
|
1272
|
+
async getWalletAddress(wallet, addressIndex, options) {
|
|
1273
|
+
const action = this._sdk.getWalletAddress(wallet, addressIndex, {
|
|
1274
|
+
checkOnDevice: options?.checkOnDevice ?? false,
|
|
1275
|
+
change: options?.change ?? false
|
|
1276
|
+
});
|
|
1277
|
+
return deviceActionToPromise(action, this.onInteraction);
|
|
1278
|
+
}
|
|
1279
|
+
async getExtendedPublicKey(derivationPath, options) {
|
|
1280
|
+
console.log(
|
|
1281
|
+
"[SignerBtc] getExtendedPublicKey called, path:",
|
|
1282
|
+
derivationPath,
|
|
1283
|
+
"options:",
|
|
1284
|
+
JSON.stringify(options)
|
|
1285
|
+
);
|
|
1286
|
+
const action = this._sdk.getExtendedPublicKey(derivationPath, {
|
|
1287
|
+
checkOnDevice: options?.checkOnDevice ?? false
|
|
1288
|
+
});
|
|
1289
|
+
try {
|
|
1290
|
+
const result = await deviceActionToPromise(
|
|
1291
|
+
action,
|
|
1292
|
+
this.onInteraction
|
|
1293
|
+
);
|
|
1294
|
+
console.log(
|
|
1295
|
+
"[SignerBtc] getExtendedPublicKey result type:",
|
|
1296
|
+
typeof result,
|
|
1297
|
+
"value:",
|
|
1298
|
+
typeof result === "string" ? result.substring(0, 20) + "..." : JSON.stringify(result).substring(0, 50)
|
|
1299
|
+
);
|
|
1300
|
+
if (typeof result === "string") return result;
|
|
1301
|
+
return result.extendedPublicKey;
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
console.error("[SignerBtc] getExtendedPublicKey error:", err);
|
|
1304
|
+
throw err;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
async getMasterFingerprint(options) {
|
|
1308
|
+
const action = this._sdk.getMasterFingerprint(options);
|
|
1309
|
+
const result = await deviceActionToPromise(
|
|
1310
|
+
action,
|
|
1311
|
+
this.onInteraction
|
|
1312
|
+
);
|
|
1313
|
+
return result.masterFingerprint;
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Sign a PSBT and return the array of partial signatures.
|
|
1317
|
+
* The `wallet` param is a DefaultWallet or WalletPolicy instance.
|
|
1318
|
+
* The `psbt` param can be a hex string, base64 string, or Uint8Array.
|
|
1319
|
+
*/
|
|
1320
|
+
async signPsbt(wallet, psbt, options) {
|
|
1321
|
+
const action = this._sdk.signPsbt(wallet, psbt, options);
|
|
1322
|
+
return deviceActionToPromise(action, this.onInteraction, INTERACTIVE_TIMEOUT_MS2);
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Sign a PSBT and return the fully extracted raw transaction as a hex string.
|
|
1326
|
+
* Like signPsbt, but also finalises the PSBT and extracts the transaction.
|
|
1327
|
+
*/
|
|
1328
|
+
async signTransaction(wallet, psbt, options) {
|
|
1329
|
+
const action = this._sdk.signTransaction(wallet, psbt, options);
|
|
1330
|
+
return deviceActionToPromise(action, this.onInteraction, INTERACTIVE_TIMEOUT_MS2);
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Sign a message with the BTC app (BIP-137 / "Bitcoin Signed Message").
|
|
1334
|
+
* Returns `{ r, s, v }` signature object.
|
|
1335
|
+
*/
|
|
1336
|
+
async signMessage(derivationPath, message, options) {
|
|
1337
|
+
const action = this._sdk.signMessage(derivationPath, hexToUtf8(message), options);
|
|
1338
|
+
return deviceActionToPromise(
|
|
1339
|
+
action,
|
|
1340
|
+
this.onInteraction,
|
|
1341
|
+
INTERACTIVE_TIMEOUT_MS2
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
// src/connector/chains/btc.ts
|
|
1347
|
+
function readCompactSize(data, pos) {
|
|
1348
|
+
const first = data[pos];
|
|
1349
|
+
if (first < 253) return [first, 1];
|
|
1350
|
+
if (first === 253) return [data[pos + 1] | data[pos + 2] << 8, 3];
|
|
1351
|
+
return [0, 1];
|
|
1352
|
+
}
|
|
1353
|
+
function writeCompactSize(out, val) {
|
|
1354
|
+
if (val < 253) {
|
|
1355
|
+
out.push(val);
|
|
1356
|
+
} else {
|
|
1357
|
+
out.push(253, val & 255, val >>> 8 & 255);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
function readKeyValue(data, pos) {
|
|
1361
|
+
const start = pos;
|
|
1362
|
+
const [keyLen, keyLenSize] = readCompactSize(data, pos);
|
|
1363
|
+
pos += keyLenSize;
|
|
1364
|
+
const keyType = data[pos];
|
|
1365
|
+
pos += keyLen;
|
|
1366
|
+
const [valLen, valLenSize] = readCompactSize(data, pos);
|
|
1367
|
+
pos += valLenSize;
|
|
1368
|
+
const value = data.slice(pos, pos + valLen);
|
|
1369
|
+
pos += valLen;
|
|
1370
|
+
return { bytes: data.slice(start, pos), end: pos, keyType, value };
|
|
1371
|
+
}
|
|
1372
|
+
function writeKv(out, key, value) {
|
|
1373
|
+
writeCompactSize(out, key.length);
|
|
1374
|
+
key.forEach((b) => out.push(b));
|
|
1375
|
+
writeCompactSize(out, value.length);
|
|
1376
|
+
value.forEach((b) => out.push(b));
|
|
1377
|
+
}
|
|
1378
|
+
async function btcGetAddress(ctx, sessionId, params) {
|
|
1379
|
+
const btcSigner = await _createBtcSigner(ctx, sessionId);
|
|
1380
|
+
const path = normalizePath(params.path);
|
|
1381
|
+
try {
|
|
1382
|
+
const { DefaultWallet, DefaultDescriptorTemplate } = await ctx.importLedgerKit(
|
|
1383
|
+
"@ledgerhq/device-signer-kit-bitcoin"
|
|
1384
|
+
);
|
|
1385
|
+
const purpose = path.split("/")[0]?.replace("'", "");
|
|
1386
|
+
let template = DefaultDescriptorTemplate.NATIVE_SEGWIT;
|
|
1387
|
+
if (purpose === "44") template = DefaultDescriptorTemplate.LEGACY;
|
|
1388
|
+
else if (purpose === "49") template = DefaultDescriptorTemplate.NESTED_SEGWIT;
|
|
1389
|
+
else if (purpose === "86") template = DefaultDescriptorTemplate.TAPROOT;
|
|
1390
|
+
const wallet = new DefaultWallet(path, template);
|
|
1391
|
+
console.log("[LedgerConnector] btcGetAddress params:", {
|
|
1392
|
+
path,
|
|
1393
|
+
purpose,
|
|
1394
|
+
template,
|
|
1395
|
+
addressIndex: params.addressIndex,
|
|
1396
|
+
change: params.change,
|
|
1397
|
+
showOnDevice: params.showOnDevice,
|
|
1398
|
+
rawParams: JSON.stringify(params)
|
|
1399
|
+
});
|
|
1400
|
+
const result = await btcSigner.getWalletAddress(wallet, params.addressIndex ?? 0, {
|
|
1401
|
+
checkOnDevice: params.showOnDevice ?? false,
|
|
1402
|
+
change: params.change ?? false
|
|
1403
|
+
});
|
|
1404
|
+
return { address: result.address, path: params.path };
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
ctx.invalidateSession(sessionId);
|
|
1407
|
+
throw ctx.wrapError(err);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
async function btcGetPublicKey(ctx, sessionId, params) {
|
|
1411
|
+
const btcSigner = await _createBtcSigner(ctx, sessionId);
|
|
1412
|
+
const path = normalizePath(params.path);
|
|
1413
|
+
console.log("[LedgerConnector] btcGetPublicKey called, path:", path, "sessionId:", sessionId);
|
|
1414
|
+
try {
|
|
1415
|
+
const xpub = await btcSigner.getExtendedPublicKey(path, {
|
|
1416
|
+
checkOnDevice: params.showOnDevice ?? false
|
|
1417
|
+
});
|
|
1418
|
+
console.log("[LedgerConnector] btcGetPublicKey success, xpub:", xpub?.substring(0, 20) + "...");
|
|
1419
|
+
return { xpub, path: params.path };
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
console.error("[LedgerConnector] btcGetPublicKey error, path:", path, "err:", err);
|
|
1422
|
+
ctx.invalidateSession(sessionId);
|
|
1423
|
+
throw ctx.wrapError(err);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
async function btcSignTransaction(ctx, sessionId, params) {
|
|
1427
|
+
if (!params.psbt) {
|
|
1428
|
+
throw Object.assign(
|
|
1429
|
+
new Error("Ledger requires PSBT format for BTC transaction signing. Provide params.psbt."),
|
|
1430
|
+
{ code: HardwareErrorCode4.InvalidParams }
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
const btcSigner = await _createBtcSigner(ctx, sessionId);
|
|
1434
|
+
try {
|
|
1435
|
+
const { DefaultWallet, DefaultDescriptorTemplate } = await ctx.importLedgerKit(
|
|
1436
|
+
"@ledgerhq/device-signer-kit-bitcoin"
|
|
1437
|
+
);
|
|
1438
|
+
const path = normalizePath(params.path || "84'/0'/0'");
|
|
1439
|
+
const purpose = path.split("/")[0]?.replace("'", "");
|
|
1440
|
+
let template = DefaultDescriptorTemplate.NATIVE_SEGWIT;
|
|
1441
|
+
if (purpose === "44") template = DefaultDescriptorTemplate.LEGACY;
|
|
1442
|
+
else if (purpose === "49") template = DefaultDescriptorTemplate.NESTED_SEGWIT;
|
|
1443
|
+
else if (purpose === "86") template = DefaultDescriptorTemplate.TAPROOT;
|
|
1444
|
+
const wallet = new DefaultWallet(path, template);
|
|
1445
|
+
let psbtToSign = params.psbt;
|
|
1446
|
+
if (purpose === "86" && params.inputDerivations?.length) {
|
|
1447
|
+
psbtToSign = await _enrichTaprootPsbt(btcSigner, psbtToSign, params.inputDerivations);
|
|
1448
|
+
}
|
|
1449
|
+
const signedTxHex = await btcSigner.signTransaction(wallet, psbtToSign);
|
|
1450
|
+
return { signedPsbt: stripHex2(signedTxHex) };
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
ctx.invalidateSession(sessionId);
|
|
1453
|
+
throw ctx.wrapError(err);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
async function btcSignMessage(ctx, sessionId, params) {
|
|
1457
|
+
const btcSigner = await _createBtcSigner(ctx, sessionId);
|
|
1458
|
+
const path = normalizePath(params.path);
|
|
1459
|
+
try {
|
|
1460
|
+
const result = await btcSigner.signMessage(path, params.message);
|
|
1461
|
+
const vHex = result.v.toString(16).padStart(2, "0");
|
|
1462
|
+
const rHex = stripHex2(result.r).padStart(64, "0");
|
|
1463
|
+
const sHex = stripHex2(result.s).padStart(64, "0");
|
|
1464
|
+
return { signature: `${vHex}${rHex}${sHex}`, address: "" };
|
|
1465
|
+
} catch (err) {
|
|
1466
|
+
ctx.invalidateSession(sessionId);
|
|
1467
|
+
throw ctx.wrapError(err);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
async function btcGetMasterFingerprint(ctx, sessionId, params) {
|
|
1471
|
+
const btcSigner = await _createBtcSigner(ctx, sessionId);
|
|
1472
|
+
try {
|
|
1473
|
+
const fingerprint = await btcSigner.getMasterFingerprint({
|
|
1474
|
+
skipOpenApp: params?.skipOpenApp
|
|
1475
|
+
});
|
|
1476
|
+
const hex = Array.from(fingerprint).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1477
|
+
return { masterFingerprint: hex };
|
|
1478
|
+
} catch (err) {
|
|
1479
|
+
ctx.invalidateSession(sessionId);
|
|
1480
|
+
throw ctx.wrapError(err);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
async function _createBtcSigner(ctx, sessionId) {
|
|
1484
|
+
const dmk = await ctx.getOrCreateDmk();
|
|
1485
|
+
const { SignerBtcBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-bitcoin");
|
|
1486
|
+
const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
|
|
1487
|
+
const signer = new SignerBtc(sdkSigner);
|
|
1488
|
+
signer.onInteraction = (interaction) => {
|
|
1489
|
+
ctx.emit("ui-event", {
|
|
1490
|
+
type: interaction,
|
|
1491
|
+
payload: { sessionId }
|
|
1492
|
+
});
|
|
1493
|
+
};
|
|
1494
|
+
return signer;
|
|
1495
|
+
}
|
|
1496
|
+
async function _enrichTaprootPsbt(btcSigner, psbtHex, inputDerivations) {
|
|
1497
|
+
const masterFp = await btcSigner.getMasterFingerprint({ skipOpenApp: true });
|
|
1498
|
+
const fpBytes = masterFp.length === 4 ? masterFp : new Uint8Array([0, 0, 0, 0]);
|
|
1499
|
+
const raw = hexToBytes2(psbtHex);
|
|
1500
|
+
const result = [];
|
|
1501
|
+
let pos = 0;
|
|
1502
|
+
for (let i = 0; i < 5; i++) result.push(raw[pos++]);
|
|
1503
|
+
while (pos < raw.length && raw[pos] !== 0) {
|
|
1504
|
+
const { bytes: kv, end } = readKeyValue(raw, pos);
|
|
1505
|
+
kv.forEach((b) => result.push(b));
|
|
1506
|
+
pos = end;
|
|
1507
|
+
}
|
|
1508
|
+
result.push(raw[pos++]);
|
|
1509
|
+
for (let inputIdx = 0; pos < raw.length; inputIdx++) {
|
|
1510
|
+
const inputKvs = [];
|
|
1511
|
+
let witnessUtxoScript = null;
|
|
1512
|
+
while (pos < raw.length && raw[pos] !== 0) {
|
|
1513
|
+
const { bytes: kv, end, keyType, value } = readKeyValue(raw, pos);
|
|
1514
|
+
inputKvs.push(kv);
|
|
1515
|
+
if (keyType === 1 && value) {
|
|
1516
|
+
const scriptStart = 8;
|
|
1517
|
+
const scriptLen = value[scriptStart];
|
|
1518
|
+
witnessUtxoScript = value.slice(scriptStart + 1, scriptStart + 1 + scriptLen);
|
|
1519
|
+
}
|
|
1520
|
+
pos = end;
|
|
1521
|
+
}
|
|
1522
|
+
inputKvs.forEach((kv) => kv.forEach((b) => result.push(b)));
|
|
1523
|
+
if (witnessUtxoScript && witnessUtxoScript.length === 34 && witnessUtxoScript[0] === 81 && // OP_1
|
|
1524
|
+
witnessUtxoScript[1] === 32 && // PUSH 32
|
|
1525
|
+
inputDerivations[inputIdx]) {
|
|
1526
|
+
const xOnlyKey = witnessUtxoScript.slice(2, 34);
|
|
1527
|
+
const fullPath = inputDerivations[inputIdx].path;
|
|
1528
|
+
writeKv(result, new Uint8Array([23]), xOnlyKey);
|
|
1529
|
+
const pathComponents = fullPath.replace(/^m\//, "").split("/");
|
|
1530
|
+
const pathBuf = new Uint8Array(1 + 4 + pathComponents.length * 4);
|
|
1531
|
+
pathBuf[0] = 0;
|
|
1532
|
+
pathBuf.set(fpBytes, 1);
|
|
1533
|
+
for (let i = 0; i < pathComponents.length; i++) {
|
|
1534
|
+
const comp = pathComponents[i];
|
|
1535
|
+
const hardened = comp.endsWith("'");
|
|
1536
|
+
let val = parseInt(hardened ? comp.slice(0, -1) : comp, 10);
|
|
1537
|
+
if (hardened) val += 2147483648;
|
|
1538
|
+
const off = 5 + i * 4;
|
|
1539
|
+
pathBuf[off] = val & 255;
|
|
1540
|
+
pathBuf[off + 1] = val >>> 8 & 255;
|
|
1541
|
+
pathBuf[off + 2] = val >>> 16 & 255;
|
|
1542
|
+
pathBuf[off + 3] = val >>> 24 & 255;
|
|
1543
|
+
}
|
|
1544
|
+
const tapBipKey = new Uint8Array(1 + 32);
|
|
1545
|
+
tapBipKey[0] = 22;
|
|
1546
|
+
tapBipKey.set(xOnlyKey, 1);
|
|
1547
|
+
writeKv(result, tapBipKey, pathBuf);
|
|
1548
|
+
}
|
|
1549
|
+
result.push(raw[pos++]);
|
|
1550
|
+
}
|
|
1551
|
+
while (pos < raw.length) {
|
|
1552
|
+
result.push(raw[pos++]);
|
|
1553
|
+
}
|
|
1554
|
+
return bytesToHex(new Uint8Array(result));
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/connector/chains/sol.ts
|
|
1558
|
+
import { hexToBytes as hexToBytes3, bytesToHex as bytesToHex2 } from "@onekeyfe/hwk-adapter-core";
|
|
1559
|
+
|
|
1560
|
+
// src/signer/SignerSol.ts
|
|
1561
|
+
var SignerSol = class {
|
|
1562
|
+
constructor(_sdk) {
|
|
1563
|
+
this._sdk = _sdk;
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Get the Solana address (base58-encoded Ed25519 public key) at the given derivation path.
|
|
1567
|
+
*/
|
|
1568
|
+
async getAddress(derivationPath, options) {
|
|
1569
|
+
const action = this._sdk.getAddress(derivationPath, {
|
|
1570
|
+
checkOnDevice: options?.checkOnDevice ?? false
|
|
1571
|
+
});
|
|
1572
|
+
return deviceActionToPromise(action, this.onInteraction);
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Sign a Solana transaction.
|
|
1576
|
+
*/
|
|
1577
|
+
async signTransaction(derivationPath, transaction, options) {
|
|
1578
|
+
const action = this._sdk.signTransaction(derivationPath, transaction, options);
|
|
1579
|
+
return deviceActionToPromise(action, this.onInteraction);
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Sign a message with the Solana app.
|
|
1583
|
+
* DMK returns { signature: string } for signMessage (unlike signTransaction which returns Uint8Array).
|
|
1584
|
+
*/
|
|
1585
|
+
async signMessage(derivationPath, message, options) {
|
|
1586
|
+
const action = this._sdk.signMessage(derivationPath, message, options);
|
|
1587
|
+
return deviceActionToPromise(action, this.onInteraction);
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
// src/connector/chains/sol.ts
|
|
1592
|
+
async function solGetAddress(ctx, sessionId, params) {
|
|
1593
|
+
const solSigner = await _createSolSigner(ctx, sessionId);
|
|
1594
|
+
const path = normalizePath(params.path);
|
|
1595
|
+
try {
|
|
1596
|
+
const publicKey = await solSigner.getAddress(path, {
|
|
1597
|
+
checkOnDevice: params.showOnDevice ?? false
|
|
1598
|
+
});
|
|
1599
|
+
return { address: publicKey, path: params.path };
|
|
1600
|
+
} catch (err) {
|
|
1601
|
+
ctx.invalidateSession(sessionId);
|
|
1602
|
+
throw ctx.wrapError(err);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
async function solSignTransaction(ctx, sessionId, params) {
|
|
1606
|
+
const solSigner = await _createSolSigner(ctx, sessionId);
|
|
1607
|
+
const path = normalizePath(params.path);
|
|
1608
|
+
const txBytes = hexToBytes3(params.serializedTx);
|
|
1609
|
+
try {
|
|
1610
|
+
const result = await solSigner.signTransaction(path, txBytes);
|
|
1611
|
+
return { signature: bytesToHex2(result) };
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
ctx.invalidateSession(sessionId);
|
|
1614
|
+
throw ctx.wrapError(err);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
async function solSignMessage(ctx, sessionId, params) {
|
|
1618
|
+
const solSigner = await _createSolSigner(ctx, sessionId);
|
|
1619
|
+
const path = normalizePath(params.path);
|
|
1620
|
+
const messageBytes = hexToBytes3(params.message);
|
|
1621
|
+
try {
|
|
1622
|
+
const result = await solSigner.signMessage(path, messageBytes);
|
|
1623
|
+
return { signature: result.signature };
|
|
1624
|
+
} catch (err) {
|
|
1625
|
+
ctx.invalidateSession(sessionId);
|
|
1626
|
+
throw ctx.wrapError(err);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
async function _createSolSigner(ctx, sessionId) {
|
|
1630
|
+
const dmk = await ctx.getOrCreateDmk();
|
|
1631
|
+
const { SignerSolanaBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-solana");
|
|
1632
|
+
const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).build();
|
|
1633
|
+
const signer = new SignerSol(sdkSigner);
|
|
1634
|
+
signer.onInteraction = (interaction) => {
|
|
1635
|
+
ctx.emit("ui-event", {
|
|
1636
|
+
type: interaction,
|
|
1637
|
+
payload: { sessionId }
|
|
1638
|
+
});
|
|
1639
|
+
};
|
|
1640
|
+
return signer;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// src/connector/chains/tron.ts
|
|
1644
|
+
import { EConnectorInteraction as EConnectorInteraction5, HardwareErrorCode as HardwareErrorCode5 } from "@onekeyfe/hwk-adapter-core";
|
|
1645
|
+
import Trx from "@ledgerhq/hw-app-trx";
|
|
1646
|
+
|
|
1647
|
+
// src/connector/chains/legacyAppRetry.ts
|
|
1648
|
+
import { EConnectorInteraction as EConnectorInteraction4 } from "@onekeyfe/hwk-adapter-core";
|
|
1649
|
+
|
|
1650
|
+
// src/app/AppManager.ts
|
|
1651
|
+
import {
|
|
1652
|
+
GetAppAndVersionCommand,
|
|
1653
|
+
OpenAppCommand,
|
|
1654
|
+
CloseAppCommand,
|
|
1655
|
+
isSuccessCommandResult
|
|
1656
|
+
} from "@ledgerhq/device-management-kit";
|
|
1657
|
+
var APP_NAME_MAP = {
|
|
1658
|
+
ETH: "Ethereum",
|
|
1659
|
+
BTC: "Bitcoin",
|
|
1660
|
+
SOL: "Solana",
|
|
1661
|
+
TRX: "Tron",
|
|
1662
|
+
XRP: "XRP",
|
|
1663
|
+
ADA: "Cardano",
|
|
1664
|
+
DOT: "Polkadot",
|
|
1665
|
+
ATOM: "Cosmos"
|
|
1666
|
+
};
|
|
1667
|
+
var DASHBOARD_APP_NAME = "BOLOS";
|
|
1668
|
+
var AppManager = class {
|
|
1669
|
+
constructor(dmk, options) {
|
|
1670
|
+
this._dmk = dmk;
|
|
1671
|
+
this._waitMs = options?.waitMs ?? 1e3;
|
|
1672
|
+
this._maxRetries = options?.maxRetries ?? 10;
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Return the Ledger app name for a given chain ticker,
|
|
1676
|
+
* or undefined if the chain is not supported.
|
|
1677
|
+
*/
|
|
1678
|
+
static getAppName(chain) {
|
|
1679
|
+
return APP_NAME_MAP[chain];
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Ensure the target app is open on the device identified by `sessionId`.
|
|
1683
|
+
*
|
|
1684
|
+
* Flow:
|
|
1685
|
+
* 1. Check the currently running app.
|
|
1686
|
+
* 2. If it is already the target, return immediately.
|
|
1687
|
+
* 3. If a different app is running (not dashboard), close it first.
|
|
1688
|
+
* 4. Open the target app.
|
|
1689
|
+
* 5. Poll until the device confirms the target app is running.
|
|
1690
|
+
*/
|
|
1691
|
+
/**
|
|
1692
|
+
* @param onConfirmOnDevice Called after OpenAppCommand succeeds —
|
|
1693
|
+
* the device is now showing a confirmation prompt to the user.
|
|
1694
|
+
* NOT called if the app is already open or if the app is not installed.
|
|
1695
|
+
*/
|
|
1696
|
+
async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
|
|
1697
|
+
const currentApp = await this._getCurrentApp(sessionId);
|
|
1698
|
+
if (currentApp === targetAppName) {
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
if (!this._isDashboard(currentApp)) {
|
|
1702
|
+
await this._closeCurrentApp(sessionId);
|
|
1703
|
+
await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
|
|
1704
|
+
}
|
|
1705
|
+
await this._openApp(sessionId, targetAppName);
|
|
1706
|
+
onConfirmOnDevice?.();
|
|
1707
|
+
await this._waitForApp(sessionId, targetAppName);
|
|
1708
|
+
}
|
|
1709
|
+
// ---------------------------------------------------------------------------
|
|
1710
|
+
// Private helpers
|
|
1711
|
+
// ---------------------------------------------------------------------------
|
|
1712
|
+
async _getCurrentApp(sessionId) {
|
|
1713
|
+
const result = await this._dmk.sendCommand({
|
|
1714
|
+
sessionId,
|
|
1715
|
+
command: new GetAppAndVersionCommand()
|
|
1716
|
+
});
|
|
1717
|
+
if (isSuccessCommandResult(result)) {
|
|
1718
|
+
return result.data.name;
|
|
1719
|
+
}
|
|
1720
|
+
throw new Error("Failed to get current app from device");
|
|
1721
|
+
}
|
|
1722
|
+
async _openApp(sessionId, appName) {
|
|
1723
|
+
const result = await this._dmk.sendCommand({
|
|
1724
|
+
sessionId,
|
|
1725
|
+
command: new OpenAppCommand({ appName })
|
|
1726
|
+
});
|
|
1727
|
+
if (!isSuccessCommandResult(result)) {
|
|
1728
|
+
const statusCode = result.statusCode;
|
|
1729
|
+
throw Object.assign(
|
|
1730
|
+
new Error(`Failed to open "${appName}": app may not be installed`),
|
|
1731
|
+
{ _tag: "OpenAppCommandError", errorCode: String(statusCode ?? ""), statusCode }
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
async _closeCurrentApp(sessionId) {
|
|
1736
|
+
await this._dmk.sendCommand({
|
|
1737
|
+
sessionId,
|
|
1738
|
+
command: new CloseAppCommand()
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Poll the device until the expected app is reported as running,
|
|
1743
|
+
* or throw after `_maxRetries` attempts.
|
|
1744
|
+
*/
|
|
1745
|
+
async _waitForApp(sessionId, expectedAppName) {
|
|
1746
|
+
for (let i = 0; i < this._maxRetries; i++) {
|
|
1747
|
+
await this._wait();
|
|
1748
|
+
const current = await this._getCurrentApp(sessionId);
|
|
1749
|
+
if (current === expectedAppName) {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
throw new Error(
|
|
1754
|
+
`Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
_isDashboard(appName) {
|
|
1758
|
+
return appName === DASHBOARD_APP_NAME;
|
|
1759
|
+
}
|
|
1760
|
+
_wait() {
|
|
1761
|
+
return new Promise((resolve) => setTimeout(resolve, this._waitMs));
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
// src/connector/chains/legacyAppRetry.ts
|
|
1766
|
+
var KNOWN_APP_CODES = {
|
|
1767
|
+
Tron: /* @__PURE__ */ new Set([
|
|
1768
|
+
27013,
|
|
1769
|
+
// user denied
|
|
1770
|
+
21781,
|
|
1771
|
+
// device locked
|
|
1772
|
+
27274,
|
|
1773
|
+
// invalid BIP32 path
|
|
1774
|
+
27275,
|
|
1775
|
+
27276,
|
|
1776
|
+
27277,
|
|
1777
|
+
// app-specific data errors
|
|
1778
|
+
27264,
|
|
1779
|
+
// invalid data
|
|
1780
|
+
27392,
|
|
1781
|
+
// wrong parameter
|
|
1782
|
+
26368
|
|
1783
|
+
// wrong length
|
|
1784
|
+
])
|
|
1785
|
+
};
|
|
1786
|
+
function isLegacyWrongAppError(err, appName) {
|
|
1787
|
+
if (isWrongAppError(err)) return true;
|
|
1788
|
+
const msg = err instanceof Error ? err.message : "";
|
|
1789
|
+
const match = msg.match(/0x([0-9a-fA-F]{4})/);
|
|
1790
|
+
if (!match) return false;
|
|
1791
|
+
const sw = parseInt(match[1], 16);
|
|
1792
|
+
const knownCodes = KNOWN_APP_CODES[appName];
|
|
1793
|
+
if (knownCodes?.has(sw)) return false;
|
|
1794
|
+
return true;
|
|
1795
|
+
}
|
|
1796
|
+
async function withLegacyAppRetry(ctx, sessionId, appName, action) {
|
|
1797
|
+
try {
|
|
1798
|
+
return await action(sessionId);
|
|
1799
|
+
} catch (err) {
|
|
1800
|
+
ctx.emit("ui-event", {
|
|
1801
|
+
type: EConnectorInteraction4.InteractionComplete,
|
|
1802
|
+
payload: { sessionId }
|
|
1803
|
+
});
|
|
1804
|
+
if (isLegacyWrongAppError(err, appName)) {
|
|
1805
|
+
const dmk = await ctx.getOrCreateDmk();
|
|
1806
|
+
const appManager = new AppManager(dmk);
|
|
1807
|
+
try {
|
|
1808
|
+
await appManager.ensureAppOpen(sessionId, appName, () => {
|
|
1809
|
+
ctx.emit("ui-event", {
|
|
1810
|
+
type: EConnectorInteraction4.ConfirmOpenApp,
|
|
1811
|
+
payload: { sessionId }
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
} catch (switchErr) {
|
|
1815
|
+
ctx.emit("ui-event", {
|
|
1816
|
+
type: EConnectorInteraction4.InteractionComplete,
|
|
1817
|
+
payload: { sessionId }
|
|
1818
|
+
});
|
|
1819
|
+
throw ctx.wrapError(switchErr);
|
|
1820
|
+
}
|
|
1821
|
+
ctx.clearAllSigners();
|
|
1822
|
+
ctx.emit("ui-event", {
|
|
1823
|
+
type: EConnectorInteraction4.InteractionComplete,
|
|
1824
|
+
payload: { sessionId }
|
|
1825
|
+
});
|
|
1826
|
+
return await action(sessionId);
|
|
1827
|
+
}
|
|
1828
|
+
ctx.invalidateSession(sessionId);
|
|
1829
|
+
throw ctx.wrapError(err);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// src/transport/DmkTransport.ts
|
|
1834
|
+
import Transport from "@ledgerhq/hw-transport";
|
|
1835
|
+
var DmkTransport = class extends Transport {
|
|
1836
|
+
constructor(dmk, sessionId) {
|
|
1837
|
+
super();
|
|
1838
|
+
this._dmk = dmk;
|
|
1839
|
+
this._sessionId = sessionId;
|
|
1840
|
+
}
|
|
1841
|
+
async exchange(apdu) {
|
|
1842
|
+
const response = await this._dmk.sendApdu({
|
|
1843
|
+
sessionId: this._sessionId,
|
|
1844
|
+
apdu: new Uint8Array(apdu)
|
|
1845
|
+
});
|
|
1846
|
+
const result = Buffer.alloc(response.data.length + 2);
|
|
1847
|
+
if (response.data.length > 0) {
|
|
1848
|
+
result.set(response.data, 0);
|
|
1849
|
+
}
|
|
1850
|
+
result.set(response.statusCode, response.data.length);
|
|
1851
|
+
return result;
|
|
1852
|
+
}
|
|
1853
|
+
async close() {
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
|
|
1857
|
+
// src/connector/chains/tron.ts
|
|
1858
|
+
async function tronGetAddress(ctx, sessionId, params) {
|
|
1859
|
+
const path = normalizePath(params.path);
|
|
1860
|
+
return withLegacyAppRetry(ctx, sessionId, "Tron", async (sid) => {
|
|
1861
|
+
const trx = await _createTrx(ctx, sid);
|
|
1862
|
+
if (params.showOnDevice) {
|
|
1863
|
+
ctx.emit("ui-event", {
|
|
1864
|
+
type: EConnectorInteraction5.ConfirmOnDevice,
|
|
1865
|
+
payload: { sessionId: sid }
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
const result = await trx.getAddress(path, params.showOnDevice ?? false);
|
|
1869
|
+
ctx.emit("ui-event", {
|
|
1870
|
+
type: EConnectorInteraction5.InteractionComplete,
|
|
1871
|
+
payload: { sessionId: sid }
|
|
1872
|
+
});
|
|
1873
|
+
return { address: result.address, publicKey: result.publicKey, path: params.path };
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
async function tronSignTransaction(ctx, sessionId, params) {
|
|
1877
|
+
if (!params.rawTxHex) {
|
|
1878
|
+
throw Object.assign(
|
|
1879
|
+
new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
|
|
1880
|
+
{ code: HardwareErrorCode5.InvalidParams }
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
const path = normalizePath(params.path);
|
|
1884
|
+
return withLegacyAppRetry(ctx, sessionId, "Tron", async (sid) => {
|
|
1885
|
+
const trx = await _createTrx(ctx, sid);
|
|
1886
|
+
ctx.emit("ui-event", {
|
|
1887
|
+
type: EConnectorInteraction5.ConfirmOnDevice,
|
|
1888
|
+
payload: { sessionId: sid }
|
|
1889
|
+
});
|
|
1890
|
+
const signature = await trx.signTransaction(path, params.rawTxHex, []);
|
|
1891
|
+
ctx.emit("ui-event", {
|
|
1892
|
+
type: EConnectorInteraction5.InteractionComplete,
|
|
1893
|
+
payload: { sessionId: sid }
|
|
1894
|
+
});
|
|
1895
|
+
return { signature };
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
async function tronSignMessage(ctx, sessionId, params) {
|
|
1899
|
+
const path = normalizePath(params.path);
|
|
1900
|
+
return withLegacyAppRetry(ctx, sessionId, "Tron", async (sid) => {
|
|
1901
|
+
const trx = await _createTrx(ctx, sid);
|
|
1902
|
+
ctx.emit("ui-event", {
|
|
1903
|
+
type: EConnectorInteraction5.ConfirmOnDevice,
|
|
1904
|
+
payload: { sessionId: sid }
|
|
1905
|
+
});
|
|
1906
|
+
const signature = await trx.signPersonalMessage(path, params.messageHex);
|
|
1907
|
+
ctx.emit("ui-event", {
|
|
1908
|
+
type: EConnectorInteraction5.InteractionComplete,
|
|
1909
|
+
payload: { sessionId: sid }
|
|
1910
|
+
});
|
|
1911
|
+
return { signature };
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
async function _createTrx(ctx, sessionId) {
|
|
1915
|
+
const dmk = await ctx.getOrCreateDmk();
|
|
1916
|
+
return new Trx(new DmkTransport(dmk, sessionId));
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// src/connector/LedgerConnectorBase.ts
|
|
1920
|
+
async function defaultLedgerKitImporter(pkg) {
|
|
1921
|
+
switch (pkg) {
|
|
1922
|
+
case "@ledgerhq/device-management-kit":
|
|
1923
|
+
return import("@ledgerhq/device-management-kit");
|
|
1924
|
+
case "@ledgerhq/device-signer-kit-ethereum":
|
|
1925
|
+
return import("@ledgerhq/device-signer-kit-ethereum");
|
|
1926
|
+
case "@ledgerhq/device-signer-kit-bitcoin":
|
|
1927
|
+
return import("@ledgerhq/device-signer-kit-bitcoin");
|
|
1928
|
+
case "@ledgerhq/device-signer-kit-solana":
|
|
1929
|
+
return import("@ledgerhq/device-signer-kit-solana");
|
|
1930
|
+
default:
|
|
1931
|
+
throw new Error(`Unknown Ledger kit package: ${pkg}`);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
var LedgerConnectorBase = class {
|
|
1935
|
+
constructor(createTransport, options) {
|
|
1936
|
+
this._deviceManager = null;
|
|
1937
|
+
this._signerManager = null;
|
|
1938
|
+
this._dmk = null;
|
|
1939
|
+
this._eventHandlers = /* @__PURE__ */ new Map();
|
|
1940
|
+
// ---------------------------------------------------------------------------
|
|
1941
|
+
// ConnectId <-> DMK path mapping
|
|
1942
|
+
//
|
|
1943
|
+
// DMK uses internal paths (BLE MAC, USB UUID) that may change across sessions.
|
|
1944
|
+
// _resolveConnectId() maps these to stable external IDs (BLE: "A58F", USB: same).
|
|
1945
|
+
// This bidirectional map is the SINGLE SOURCE OF TRUTH for all connectId usage.
|
|
1946
|
+
// ---------------------------------------------------------------------------
|
|
1947
|
+
this._connectIdToPath = /* @__PURE__ */ new Map();
|
|
1948
|
+
// "A58F" -> "D5:75:7D:4B:51:E8"
|
|
1949
|
+
this._pathToConnectId = /* @__PURE__ */ new Map();
|
|
1950
|
+
this._createTransport = createTransport;
|
|
1951
|
+
this._connectionType = options?.connectionType ?? "usb";
|
|
1952
|
+
this._providedDmk = options?.dmk;
|
|
1953
|
+
this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
|
|
1954
|
+
if (this._providedDmk) {
|
|
1955
|
+
this._initManagers(this._providedDmk);
|
|
1956
|
+
}
|
|
1957
|
+
this._ctx = {
|
|
1958
|
+
emit: (event, data) => this._emit(event, data),
|
|
1959
|
+
invalidateSession: (sid) => this._invalidateSession(sid),
|
|
1960
|
+
wrapError: (err) => this._wrapError(err),
|
|
1961
|
+
getOrCreateDmk: () => this._getOrCreateDmk(),
|
|
1962
|
+
getDeviceManager: () => this._getDeviceManager(),
|
|
1963
|
+
getSignerManager: () => this._getSignerManager(),
|
|
1964
|
+
clearAllSigners: () => this._signerManager?.clearAll(),
|
|
1965
|
+
replaceSession: (oldSid, newSid) => this._replaceSession(oldSid, newSid),
|
|
1966
|
+
importLedgerKit: this._importLedgerKit
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
// "D5:75:7D:4B:51:E8" -> "A58F"
|
|
1970
|
+
/** Register a connectId <-> path mapping from a device descriptor. */
|
|
1971
|
+
_registerDeviceId(descriptor) {
|
|
1972
|
+
const connectId = this._resolveConnectId(descriptor);
|
|
1973
|
+
this._connectIdToPath.set(connectId, descriptor.path);
|
|
1974
|
+
this._pathToConnectId.set(descriptor.path, connectId);
|
|
1975
|
+
return connectId;
|
|
1976
|
+
}
|
|
1977
|
+
/** Get DMK path from external connectId. Falls back to connectId itself. */
|
|
1978
|
+
_getPathForConnectId(connectId) {
|
|
1979
|
+
return this._connectIdToPath.get(connectId) ?? connectId;
|
|
1980
|
+
}
|
|
1981
|
+
/** Get external connectId from DMK path. Falls back to path itself. */
|
|
1982
|
+
_getConnectIdForPath(path) {
|
|
1983
|
+
return this._pathToConnectId.get(path) ?? path;
|
|
1984
|
+
}
|
|
1985
|
+
// ---------------------------------------------------------------------------
|
|
1986
|
+
// Protected — hooks for subclasses
|
|
1987
|
+
// ---------------------------------------------------------------------------
|
|
1988
|
+
/**
|
|
1989
|
+
* Resolve the connectId for a discovered device descriptor.
|
|
1990
|
+
* Default: use the DMK path (ephemeral UUID).
|
|
1991
|
+
* Override in subclasses to extract stable identifiers (e.g. BLE hex ID).
|
|
1992
|
+
*/
|
|
1993
|
+
_resolveConnectId(descriptor) {
|
|
1994
|
+
return descriptor.path;
|
|
1995
|
+
}
|
|
1996
|
+
// ---------------------------------------------------------------------------
|
|
1997
|
+
// IConnector -- Device discovery
|
|
1998
|
+
// ---------------------------------------------------------------------------
|
|
1999
|
+
async searchDevices() {
|
|
2000
|
+
const dm = await this._getDeviceManager();
|
|
2001
|
+
let descriptors = await dm.enumerate();
|
|
2002
|
+
if (descriptors.length === 0) {
|
|
2003
|
+
try {
|
|
2004
|
+
await dm.requestDevice();
|
|
2005
|
+
} catch {
|
|
2006
|
+
}
|
|
2007
|
+
descriptors = await dm.enumerate();
|
|
2008
|
+
}
|
|
2009
|
+
const result = descriptors.map((d) => {
|
|
2010
|
+
const connectId = this._registerDeviceId(d);
|
|
2011
|
+
return {
|
|
2012
|
+
connectId,
|
|
2013
|
+
deviceId: d.path,
|
|
2014
|
+
name: d.name || d.type || "Ledger",
|
|
2015
|
+
model: d.type
|
|
2016
|
+
};
|
|
2017
|
+
});
|
|
2018
|
+
return result;
|
|
2019
|
+
}
|
|
2020
|
+
// ---------------------------------------------------------------------------
|
|
2021
|
+
// IConnector -- Connection
|
|
2022
|
+
// ---------------------------------------------------------------------------
|
|
2023
|
+
async connect(deviceId) {
|
|
2024
|
+
const dm = await this._getDeviceManager();
|
|
2025
|
+
await this.searchDevices();
|
|
2026
|
+
const dmkPath = deviceId ? this._getPathForConnectId(deviceId) : void 0;
|
|
2027
|
+
let targetPath = dmkPath;
|
|
2028
|
+
if (!targetPath) {
|
|
2029
|
+
const descriptors = await dm.enumerate();
|
|
2030
|
+
if (descriptors.length === 0) {
|
|
2031
|
+
throw new Error(
|
|
2032
|
+
`No Ledger device found. Make sure the device is connected${this._connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
targetPath = descriptors[0].path;
|
|
2036
|
+
}
|
|
2037
|
+
const externalConnectId = this._getConnectIdForPath(targetPath);
|
|
2038
|
+
const doConnect = async (path) => {
|
|
2039
|
+
const sessionId = await dm.connect(path);
|
|
2040
|
+
const session = {
|
|
2041
|
+
sessionId,
|
|
2042
|
+
deviceInfo: {
|
|
2043
|
+
vendor: "ledger",
|
|
2044
|
+
model: "unknown",
|
|
2045
|
+
firmwareVersion: "unknown",
|
|
2046
|
+
deviceId: path,
|
|
2047
|
+
connectId: externalConnectId,
|
|
2048
|
+
connectionType: this._connectionType,
|
|
2049
|
+
capabilities: { persistentDeviceIdentity: false }
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
this._emit("device-connect", {
|
|
2053
|
+
device: {
|
|
2054
|
+
connectId: externalConnectId,
|
|
2055
|
+
deviceId: path,
|
|
2056
|
+
name: "Ledger"
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
2059
|
+
return session;
|
|
2060
|
+
};
|
|
2061
|
+
try {
|
|
2062
|
+
return await doConnect(targetPath);
|
|
2063
|
+
} catch {
|
|
2064
|
+
this._resetSignersAndSessions();
|
|
2065
|
+
const dm2 = await this._getDeviceManager();
|
|
2066
|
+
await this.searchDevices();
|
|
2067
|
+
const retryPath = this._getPathForConnectId(externalConnectId);
|
|
2068
|
+
if (!retryPath || retryPath === externalConnectId) {
|
|
2069
|
+
const descriptors = await dm2.enumerate();
|
|
2070
|
+
if (descriptors.length === 0) {
|
|
2071
|
+
throw new Error(
|
|
2072
|
+
`No Ledger device found after retry. Make sure the device is connected${this._connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
return doConnect(descriptors[0].path);
|
|
2076
|
+
}
|
|
2077
|
+
return doConnect(retryPath);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
async disconnect(sessionId) {
|
|
2081
|
+
if (!this._deviceManager) return;
|
|
2082
|
+
const deviceId = this._deviceManager.getDeviceId(sessionId);
|
|
2083
|
+
this._signerManager?.invalidate(sessionId);
|
|
2084
|
+
await this._deviceManager.disconnect(sessionId);
|
|
2085
|
+
if (deviceId) {
|
|
2086
|
+
this._emit("device-disconnect", { connectId: deviceId });
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
// ---------------------------------------------------------------------------
|
|
2090
|
+
// IConnector -- Method dispatch
|
|
2091
|
+
// ---------------------------------------------------------------------------
|
|
2092
|
+
async call(sessionId, method, params) {
|
|
2093
|
+
console.log("[DMK] call:", method, JSON.stringify(params));
|
|
2094
|
+
switch (method) {
|
|
2095
|
+
// EVM
|
|
2096
|
+
case "evmGetAddress":
|
|
2097
|
+
return evmGetAddress(this._ctx, sessionId, params);
|
|
2098
|
+
case "evmSignTransaction":
|
|
2099
|
+
return evmSignTransaction(this._ctx, sessionId, params);
|
|
2100
|
+
case "evmSignMessage":
|
|
2101
|
+
return evmSignMessage(this._ctx, sessionId, params);
|
|
2102
|
+
case "evmSignTypedData":
|
|
2103
|
+
return evmSignTypedData(this._ctx, sessionId, params);
|
|
2104
|
+
// BTC
|
|
2105
|
+
case "btcGetAddress":
|
|
2106
|
+
return btcGetAddress(this._ctx, sessionId, params);
|
|
2107
|
+
case "btcGetPublicKey":
|
|
2108
|
+
return btcGetPublicKey(this._ctx, sessionId, params);
|
|
2109
|
+
case "btcSignTransaction":
|
|
2110
|
+
return btcSignTransaction(this._ctx, sessionId, params);
|
|
2111
|
+
case "btcSignMessage":
|
|
2112
|
+
return btcSignMessage(this._ctx, sessionId, params);
|
|
2113
|
+
case "btcGetMasterFingerprint":
|
|
2114
|
+
return btcGetMasterFingerprint(
|
|
2115
|
+
this._ctx,
|
|
2116
|
+
sessionId,
|
|
2117
|
+
params
|
|
2118
|
+
);
|
|
2119
|
+
// SOL
|
|
2120
|
+
case "solGetAddress":
|
|
2121
|
+
return solGetAddress(this._ctx, sessionId, params);
|
|
2122
|
+
case "solSignTransaction":
|
|
2123
|
+
return solSignTransaction(this._ctx, sessionId, params);
|
|
2124
|
+
case "solSignMessage":
|
|
2125
|
+
return solSignMessage(this._ctx, sessionId, params);
|
|
2126
|
+
// TRON
|
|
2127
|
+
case "tronGetAddress":
|
|
2128
|
+
return tronGetAddress(this._ctx, sessionId, params);
|
|
2129
|
+
case "tronSignTransaction":
|
|
2130
|
+
return tronSignTransaction(this._ctx, sessionId, params);
|
|
2131
|
+
case "tronSignMessage":
|
|
2132
|
+
return tronSignMessage(this._ctx, sessionId, params);
|
|
2133
|
+
default:
|
|
2134
|
+
throw new Error(`LedgerConnector: unknown method "${method}"`);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
async cancel(_sessionId) {
|
|
2138
|
+
}
|
|
2139
|
+
uiResponse(_response) {
|
|
2140
|
+
}
|
|
2141
|
+
// ---------------------------------------------------------------------------
|
|
2142
|
+
// IConnector -- Events
|
|
2143
|
+
// ---------------------------------------------------------------------------
|
|
2144
|
+
on(event, handler) {
|
|
2145
|
+
if (!this._eventHandlers.has(event)) {
|
|
2146
|
+
this._eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
2147
|
+
}
|
|
2148
|
+
this._eventHandlers.get(event).add(handler);
|
|
2149
|
+
}
|
|
2150
|
+
off(event, handler) {
|
|
2151
|
+
this._eventHandlers.get(event)?.delete(handler);
|
|
2152
|
+
}
|
|
2153
|
+
// ---------------------------------------------------------------------------
|
|
2154
|
+
// IConnector -- Reset
|
|
2155
|
+
// ---------------------------------------------------------------------------
|
|
2156
|
+
reset() {
|
|
2157
|
+
this._resetAll();
|
|
2158
|
+
}
|
|
2159
|
+
// ---------------------------------------------------------------------------
|
|
2160
|
+
// Private -- DMK / Manager lifecycle
|
|
2161
|
+
// ---------------------------------------------------------------------------
|
|
2162
|
+
/**
|
|
2163
|
+
* Lazily create or return the DMK instance.
|
|
2164
|
+
* If a DMK was provided via constructor, it is used directly.
|
|
2165
|
+
* Otherwise, one is created via the transport factory.
|
|
2166
|
+
*/
|
|
2167
|
+
async _getOrCreateDmk() {
|
|
2168
|
+
console.log(
|
|
2169
|
+
"[DMK] _getOrCreateDmk called, _dmk exists:",
|
|
2170
|
+
!!this._dmk,
|
|
2171
|
+
"_providedDmk exists:",
|
|
2172
|
+
!!this._providedDmk,
|
|
2173
|
+
"stack:",
|
|
2174
|
+
new Error().stack?.split("\n").slice(1, 4).join(" | ")
|
|
2175
|
+
);
|
|
2176
|
+
if (this._dmk) return this._dmk;
|
|
2177
|
+
if (this._providedDmk) {
|
|
2178
|
+
this._dmk = this._providedDmk;
|
|
2179
|
+
return this._dmk;
|
|
2180
|
+
}
|
|
2181
|
+
const { DeviceManagementKitBuilder } = await this._importLedgerKit(
|
|
2182
|
+
"@ledgerhq/device-management-kit"
|
|
2183
|
+
);
|
|
2184
|
+
const transportFactory = await this._createTransport();
|
|
2185
|
+
console.log(
|
|
2186
|
+
"[DMK] _getOrCreateDmk: transportFactory type:",
|
|
2187
|
+
typeof transportFactory,
|
|
2188
|
+
"value:",
|
|
2189
|
+
String(transportFactory).substring(0, 80)
|
|
2190
|
+
);
|
|
2191
|
+
const dmk = new DeviceManagementKitBuilder().addTransport(transportFactory).build();
|
|
2192
|
+
this._dmk = dmk;
|
|
2193
|
+
console.log(
|
|
2194
|
+
"[DMK] _getOrCreateDmk: DMK created, methods:",
|
|
2195
|
+
Object.getOwnPropertyNames(Object.getPrototypeOf(dmk)).join(", ")
|
|
2196
|
+
);
|
|
2197
|
+
return dmk;
|
|
2198
|
+
}
|
|
2199
|
+
_initManagers(dmk) {
|
|
2200
|
+
this._dmk = dmk;
|
|
2201
|
+
this._deviceManager = new LedgerDeviceManager(dmk);
|
|
2202
|
+
const importKit = this._importLedgerKit;
|
|
2203
|
+
this._signerManager = new SignerManager(dmk, async (args) => {
|
|
2204
|
+
const mod = await importKit("@ledgerhq/device-signer-kit-ethereum");
|
|
2205
|
+
return new mod.SignerEthBuilder(args);
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
async _getDeviceManager() {
|
|
2209
|
+
if (this._deviceManager) return this._deviceManager;
|
|
2210
|
+
const dmk = await this._getOrCreateDmk();
|
|
2211
|
+
this._initManagers(dmk);
|
|
2212
|
+
return this._deviceManager;
|
|
2213
|
+
}
|
|
2214
|
+
async _getSignerManager() {
|
|
2215
|
+
if (!this._signerManager) {
|
|
2216
|
+
const dmk = await this._getOrCreateDmk();
|
|
2217
|
+
this._initManagers(dmk);
|
|
2218
|
+
}
|
|
2219
|
+
return this._signerManager;
|
|
2220
|
+
}
|
|
2221
|
+
_invalidateSession(sessionId) {
|
|
2222
|
+
this._signerManager?.invalidate(sessionId);
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Replace an old session with a new one after app switch.
|
|
2226
|
+
* Updates the connectId→path mapping so subsequent calls() use the new session,
|
|
2227
|
+
* and emits device-connect so the adapter updates its _sessions Map.
|
|
2228
|
+
*/
|
|
2229
|
+
_replaceSession(oldSessionId, _newSessionId) {
|
|
2230
|
+
const dm = this._deviceManager;
|
|
2231
|
+
if (!dm) return;
|
|
2232
|
+
const oldDeviceId = dm.getDeviceId(oldSessionId);
|
|
2233
|
+
const connectId = oldDeviceId ? this._pathToConnectId.get(oldDeviceId) : void 0;
|
|
2234
|
+
this._signerManager?.invalidate(oldSessionId);
|
|
2235
|
+
if (connectId) {
|
|
2236
|
+
this._emit("device-connect", {
|
|
2237
|
+
device: {
|
|
2238
|
+
connectId,
|
|
2239
|
+
deviceId: connectId,
|
|
2240
|
+
name: "Ledger"
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Light reset: clear signer/session state but keep DMK, BLE scan, and ID mapping alive.
|
|
2247
|
+
* Used by connect() retry — we want to re-discover with the same transport.
|
|
2248
|
+
*/
|
|
2249
|
+
_resetSignersAndSessions() {
|
|
2250
|
+
console.log(
|
|
2251
|
+
"[DMK] _resetSignersAndSessions called, stack:",
|
|
2252
|
+
new Error().stack?.split("\n").slice(1, 3).join(" | ")
|
|
2253
|
+
);
|
|
2254
|
+
this._signerManager?.clearAll();
|
|
2255
|
+
this._signerManager = null;
|
|
2256
|
+
this._deviceManager = null;
|
|
2257
|
+
}
|
|
2258
|
+
_resetAll() {
|
|
2259
|
+
console.log(
|
|
2260
|
+
"[DMK] _resetAll called, stack:",
|
|
2261
|
+
new Error().stack?.split("\n").slice(1, 3).join(" | ")
|
|
2262
|
+
);
|
|
2263
|
+
this._signerManager?.clearAll();
|
|
2264
|
+
this._deviceManager?.dispose();
|
|
2265
|
+
this._deviceManager = null;
|
|
2266
|
+
this._signerManager = null;
|
|
2267
|
+
this._dmk = null;
|
|
2268
|
+
this._connectIdToPath.clear();
|
|
2269
|
+
this._pathToConnectId.clear();
|
|
2270
|
+
}
|
|
2271
|
+
// ---------------------------------------------------------------------------
|
|
2272
|
+
// Private -- Events
|
|
2273
|
+
// ---------------------------------------------------------------------------
|
|
2274
|
+
_emit(event, data) {
|
|
2275
|
+
const handlers = this._eventHandlers.get(event);
|
|
2276
|
+
if (handlers) {
|
|
2277
|
+
for (const handler of handlers) {
|
|
2278
|
+
try {
|
|
2279
|
+
handler(data);
|
|
2280
|
+
} catch {
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
// ---------------------------------------------------------------------------
|
|
2286
|
+
// Private -- Error handling
|
|
2287
|
+
// ---------------------------------------------------------------------------
|
|
2288
|
+
_wrapError(err) {
|
|
2289
|
+
const mapped = mapLedgerError(err);
|
|
2290
|
+
const error = new Error(mapped.message);
|
|
2291
|
+
Object.assign(error, { code: mapped.code });
|
|
2292
|
+
return error;
|
|
2293
|
+
}
|
|
2294
|
+
};
|
|
2295
|
+
export {
|
|
2296
|
+
AppManager,
|
|
2297
|
+
DmkTransport,
|
|
2298
|
+
LedgerAdapter,
|
|
2299
|
+
LedgerConnectorBase,
|
|
2300
|
+
LedgerDeviceManager,
|
|
2301
|
+
SignerBtc,
|
|
2302
|
+
SignerEth,
|
|
2303
|
+
SignerManager,
|
|
2304
|
+
SignerSol,
|
|
2305
|
+
deviceActionToPromise,
|
|
2306
|
+
isDeviceLockedError,
|
|
2307
|
+
mapLedgerError
|
|
2308
|
+
};
|
|
2309
|
+
//# sourceMappingURL=index.mjs.map
|