@onekeyfe/hd-core 1.2.3-alpha.1 → 1.2.3-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +503 -10
- package/__tests__/core-error-output.test.ts +169 -1
- package/__tests__/device-lifecycle-events.test.ts +350 -2
- package/__tests__/open-wallet-session-error-response.test.ts +2 -2
- package/__tests__/open-wallet-session.test.ts +8 -411
- package/__tests__/protocol-v2.test.ts +51 -0
- package/__tests__/public-device-state-api.test.ts +2 -7
- package/__tests__/search-devices.test.ts +196 -8
- package/__tests__/sol-sign-offchain-message.test.ts +0 -8
- package/dist/api/GetFeatures.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts +2 -15
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddress.d.ts +2 -0
- package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +7 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressByLoop.d.ts.map +1 -1
- package/dist/api/device/DeviceVerify.d.ts.map +1 -1
- package/dist/core/RequestQueue.d.ts +1 -0
- package/dist/core/RequestQueue.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/data-manager/TransportManager.d.ts +2 -0
- package/dist/data-manager/TransportManager.d.ts.map +1 -1
- package/dist/device/Device.d.ts +2 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/index.d.ts +17 -19
- package/dist/index.js +449 -184
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/getFeatures.d.ts.map +1 -1
- package/dist/types/api/getPassphraseState.d.ts.map +1 -1
- package/dist/types/api/openWalletSession.d.ts +1 -10
- package/dist/types/api/openWalletSession.d.ts.map +1 -1
- package/dist/types/params.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/GetFeatures.ts +1 -0
- package/src/api/GetPassphraseState.ts +1 -0
- package/src/api/OpenWalletSession.ts +7 -77
- package/src/api/SearchDevices.ts +121 -27
- package/src/api/allnetwork/AllNetworkGetAddress.ts +79 -45
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +95 -24
- package/src/api/allnetwork/AllNetworkGetAddressByLoop.ts +3 -0
- package/src/api/device/DeviceVerify.ts +8 -0
- package/src/core/RequestQueue.ts +20 -0
- package/src/core/index.ts +135 -43
- package/src/data/messages/messages-protocol-v2.json +21 -0
- package/src/data-manager/TransportManager.ts +14 -3
- package/src/device/Device.ts +58 -27
- package/src/protocols/protocol-v2/walletSession.ts +7 -0
- package/src/types/api/getFeatures.ts +2 -1
- package/src/types/api/getPassphraseState.ts +2 -5
- package/src/types/api/openWalletSession.ts +7 -19
- package/src/types/params.ts +7 -0
package/src/core/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isProtocolV2LinkDisabledError,
|
|
11
11
|
} from '@onekeyfe/hd-transport';
|
|
12
12
|
import {
|
|
13
|
+
EDeviceType,
|
|
13
14
|
ERRORS,
|
|
14
15
|
ERROR_CODES_REQUIRE_DISCONNECT,
|
|
15
16
|
ERROR_CODES_REQUIRE_RELEASE,
|
|
@@ -83,7 +84,8 @@ import type { BaseMethod } from '../api/BaseMethod';
|
|
|
83
84
|
|
|
84
85
|
const Log = getLogger(LoggerNames.Core);
|
|
85
86
|
const PRE_INITIALIZE_TTL_MS = 60 * 1000;
|
|
86
|
-
const PRE_PENDING_CALL_TIMEOUT_MS =
|
|
87
|
+
const PRE_PENDING_CALL_TIMEOUT_MS = 5 * 1000;
|
|
88
|
+
const PRO2_USB_SIGNING_COOLDOWN_MS = 1000;
|
|
87
89
|
|
|
88
90
|
// Dedup/coalesce state for "pre-warm signal" methods (isPreWarmSignal),
|
|
89
91
|
// keyed by getPreWarmKey(): coalesce in-flight, skip if warmed within TTL.
|
|
@@ -181,6 +183,7 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
|
|
|
181
183
|
}
|
|
182
184
|
};
|
|
183
185
|
method.setContext?.(context);
|
|
186
|
+
method.context = context;
|
|
184
187
|
|
|
185
188
|
method.requestContext = createRequestContext(method.responseID, method.name, {
|
|
186
189
|
sdkInstanceId: context.sdkInstanceId,
|
|
@@ -201,7 +204,12 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
|
|
|
201
204
|
if (!method.useDevice) {
|
|
202
205
|
updateMethodRequestContext(method, { status: 'running' });
|
|
203
206
|
try {
|
|
204
|
-
const
|
|
207
|
+
const env = DataManager.getSettings('env');
|
|
208
|
+
const response =
|
|
209
|
+
method.name === 'searchDevices' &&
|
|
210
|
+
(DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env))
|
|
211
|
+
? await context.methodSynchronize(() => method.run(), 'webusb-discovery')
|
|
212
|
+
: await method.run();
|
|
205
213
|
completeMethodRequestContext(method);
|
|
206
214
|
return createResponseMessage(method.responseID, true, response);
|
|
207
215
|
} catch (error) {
|
|
@@ -314,6 +322,7 @@ const waitForPendingPromise = async (
|
|
|
314
322
|
Log.debug('pre pending call promise before call method, wait for it');
|
|
315
323
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
316
324
|
let timedOut = false;
|
|
325
|
+
let completed = false;
|
|
317
326
|
try {
|
|
318
327
|
await Promise.race([
|
|
319
328
|
pendingPromise,
|
|
@@ -324,11 +333,12 @@ const waitForPendingPromise = async (
|
|
|
324
333
|
}, PRE_PENDING_CALL_TIMEOUT_MS);
|
|
325
334
|
}),
|
|
326
335
|
]);
|
|
327
|
-
|
|
328
|
-
// Cancellation is best-effort; the transport teardown owns recovery.
|
|
336
|
+
completed = !timedOut;
|
|
329
337
|
} finally {
|
|
330
338
|
if (timer) clearTimeout(timer);
|
|
331
|
-
|
|
339
|
+
// Keep a rejected cleanup barrier for the next caller's safety check;
|
|
340
|
+
// only a completed cleanup or a timeout may clear it.
|
|
341
|
+
if (timedOut || completed) removePrePendingCallPromise?.(connectId, pendingPromise);
|
|
332
342
|
}
|
|
333
343
|
if (timedOut) {
|
|
334
344
|
Log.warn('pre pending call promise timed out before call method', { connectId });
|
|
@@ -337,6 +347,20 @@ const waitForPendingPromise = async (
|
|
|
337
347
|
}
|
|
338
348
|
};
|
|
339
349
|
|
|
350
|
+
export function getPostCallPendingPromise(method: BaseMethod, device: Device): Promise<void> {
|
|
351
|
+
const cleanupPromise = device.waitForRunCleanup();
|
|
352
|
+
const env = DataManager.getSettings('env');
|
|
353
|
+
const deviceType = device.getCurrentDeviceType();
|
|
354
|
+
const requiresSigningCooldown =
|
|
355
|
+
method.name.includes('Sign') &&
|
|
356
|
+
(deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) &&
|
|
357
|
+
(DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'node-usb');
|
|
358
|
+
|
|
359
|
+
return requiresSigningCooldown
|
|
360
|
+
? cleanupPromise.then(() => wait(PRO2_USB_SIGNING_COOLDOWN_MS)).then(() => undefined)
|
|
361
|
+
: cleanupPromise;
|
|
362
|
+
}
|
|
363
|
+
|
|
340
364
|
const onCallDevice = async (
|
|
341
365
|
context: CoreContext,
|
|
342
366
|
message: CoreMessage,
|
|
@@ -367,42 +391,57 @@ const onCallDevice = async (
|
|
|
367
391
|
DevicePool.clearDeviceCache(method.payload.connectId);
|
|
368
392
|
}
|
|
369
393
|
|
|
370
|
-
//
|
|
371
|
-
if (method.connectId) {
|
|
372
|
-
await context.waitForCallbackTasks(method.connectId);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
await waitForPendingPromise(
|
|
376
|
-
method.connectId ?? '',
|
|
377
|
-
getPrePendingCallPromise,
|
|
378
|
-
removePrePendingCallPromise
|
|
379
|
-
);
|
|
380
|
-
|
|
394
|
+
// Register before waiting so cancellation also covers queued requests.
|
|
381
395
|
const task = requestQueue.createTask(method);
|
|
382
396
|
|
|
383
397
|
// Pre-warm holds the device as a per-connectId callback task so a concurrent
|
|
384
398
|
// real call waits (before ensureConnected) instead of racing its Initialize.
|
|
385
399
|
// Only covers pre-warm -> real-call ordering; the reverse is fail-closed.
|
|
386
400
|
let preWarmCallbackTask: Deferred<void> | undefined;
|
|
387
|
-
if (method.isPreWarmSignal && method.connectId) {
|
|
388
|
-
preWarmCallbackTask = createDeferred<void>();
|
|
389
|
-
context.registerCallbackTask(method.connectId, preWarmCallbackTask);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
401
|
let device: Device;
|
|
393
402
|
try {
|
|
403
|
+
const connectId = method.connectId ?? '';
|
|
404
|
+
if (connectId) {
|
|
405
|
+
await requestQueue.waitForTask(task, () => context.waitForCallbackTasks(connectId));
|
|
406
|
+
}
|
|
407
|
+
await requestQueue.waitForTask(task, () =>
|
|
408
|
+
waitForPendingPromise(
|
|
409
|
+
method.connectId ?? '',
|
|
410
|
+
getPrePendingCallPromise,
|
|
411
|
+
removePrePendingCallPromise
|
|
412
|
+
)
|
|
413
|
+
);
|
|
414
|
+
if (method.isPreWarmSignal && method.connectId) {
|
|
415
|
+
preWarmCallbackTask = createDeferred<void>();
|
|
416
|
+
context.registerCallbackTask(method.connectId, preWarmCallbackTask);
|
|
417
|
+
}
|
|
394
418
|
/**
|
|
395
419
|
* Polling to ensure successful connection
|
|
396
420
|
*/
|
|
397
|
-
const connectId = method.connectId ?? '';
|
|
398
421
|
const pollingId = pollingManager.start(connectId);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
method,
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
422
|
+
const env = DataManager.getSettings('env');
|
|
423
|
+
const connect = () =>
|
|
424
|
+
ensureConnected(context, method, connectId, pollingId, method.abortSignal);
|
|
425
|
+
// Discovery may acquire USB endpoints. Finish it before initializing a public
|
|
426
|
+
// request; once registered, that request makes discovery use cached state only.
|
|
427
|
+
if (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env)) {
|
|
428
|
+
// Synchronization can keep the connect action queued after the caller is
|
|
429
|
+
// cancelled. Observe that promise so a late device-not-found error does
|
|
430
|
+
// not become an unhandled rejection.
|
|
431
|
+
const connectPromise = context.methodSynchronize(async () => {
|
|
432
|
+
if (method.abortSignal?.aborted) {
|
|
433
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
434
|
+
}
|
|
435
|
+
return connect();
|
|
436
|
+
}, 'webusb-discovery');
|
|
437
|
+
connectPromise.catch(() => undefined);
|
|
438
|
+
device = await requestQueue.waitForTask(task, () => connectPromise);
|
|
439
|
+
} else {
|
|
440
|
+
device = await connect();
|
|
441
|
+
}
|
|
442
|
+
if (method.abortSignal?.aborted) {
|
|
443
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
444
|
+
}
|
|
406
445
|
} catch (e) {
|
|
407
446
|
preWarmCallbackTask?.resolve();
|
|
408
447
|
Log.debug('ensureConnected error: ', e);
|
|
@@ -478,14 +517,19 @@ const onCallDevice = async (
|
|
|
478
517
|
|
|
479
518
|
try {
|
|
480
519
|
// Wait for any pending task except our own (self-wait would deadlock).
|
|
481
|
-
|
|
482
|
-
|
|
520
|
+
const { connectId } = method;
|
|
521
|
+
if (connectId) {
|
|
522
|
+
await requestQueue.waitForTask(task, () =>
|
|
523
|
+
context.waitForCallbackTasks(connectId, preWarmCallbackTask)
|
|
524
|
+
);
|
|
483
525
|
}
|
|
484
526
|
|
|
485
|
-
await
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
527
|
+
await requestQueue.waitForTask(task, () =>
|
|
528
|
+
waitForPendingPromise(
|
|
529
|
+
method.connectId ?? '',
|
|
530
|
+
getPrePendingCallPromise,
|
|
531
|
+
removePrePendingCallPromise
|
|
532
|
+
)
|
|
489
533
|
);
|
|
490
534
|
|
|
491
535
|
const inner = async (): Promise<void> => {
|
|
@@ -690,6 +734,13 @@ const onCallDevice = async (
|
|
|
690
734
|
},
|
|
691
735
|
});
|
|
692
736
|
messageResponse = createResponseMessage(method.responseID, true, response);
|
|
737
|
+
// Preserve the acknowledged result while the next call waits for cleanup and cooldown.
|
|
738
|
+
if (method.connectId) {
|
|
739
|
+
context.setPrePendingCallPromise(
|
|
740
|
+
method.connectId,
|
|
741
|
+
getPostCallPendingPromise(method, device)
|
|
742
|
+
);
|
|
743
|
+
}
|
|
693
744
|
requestQueue.resolveRequest(method.responseID, messageResponse);
|
|
694
745
|
completeMethodRequestContext(method);
|
|
695
746
|
} catch (error) {
|
|
@@ -701,6 +752,12 @@ const onCallDevice = async (
|
|
|
701
752
|
}
|
|
702
753
|
Log.debug(`Call API - Inner Method Run Error`, error);
|
|
703
754
|
messageResponse = createResponseMessage(method.responseID, false, { error });
|
|
755
|
+
if (method.connectId) {
|
|
756
|
+
context.setPrePendingCallPromise(
|
|
757
|
+
method.connectId,
|
|
758
|
+
getPostCallPendingPromise(method, device)
|
|
759
|
+
);
|
|
760
|
+
}
|
|
704
761
|
requestQueue.resolveRequest(method.responseID, messageResponse);
|
|
705
762
|
completeMethodRequestContext(method, error);
|
|
706
763
|
|
|
@@ -727,6 +784,9 @@ const onCallDevice = async (
|
|
|
727
784
|
skipInitialize: canSkipInitialize(method, device),
|
|
728
785
|
...parseInitOptions(method),
|
|
729
786
|
};
|
|
787
|
+
if (method.abortSignal?.aborted) {
|
|
788
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
789
|
+
}
|
|
730
790
|
const deviceRun = () => device.run(inner, runOptions);
|
|
731
791
|
task.callPromise = createDeferred<any>(deviceRun);
|
|
732
792
|
|
|
@@ -747,6 +807,7 @@ const onCallDevice = async (
|
|
|
747
807
|
);
|
|
748
808
|
Log.debug('Call API - Run Error: ', error);
|
|
749
809
|
completeMethodRequestContext(method, error);
|
|
810
|
+
return messageResponse;
|
|
750
811
|
} finally {
|
|
751
812
|
// Release the pre-warm callback task so the next real call can proceed.
|
|
752
813
|
preWarmCallbackTask?.resolve();
|
|
@@ -1044,7 +1105,7 @@ async function connectDeviceForBle(
|
|
|
1044
1105
|
retryCount = 0
|
|
1045
1106
|
) {
|
|
1046
1107
|
try {
|
|
1047
|
-
if (device.wasInterruptedByUser()) {
|
|
1108
|
+
if (abortSignal?.aborted || device.wasInterruptedByUser()) {
|
|
1048
1109
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
1049
1110
|
}
|
|
1050
1111
|
if (method.payload.forceProtocolDetection && device.hasDeviceAcquire()) {
|
|
@@ -1113,6 +1174,9 @@ async function connectDeviceForBle(
|
|
|
1113
1174
|
DevicePool.emitter.emit(DEVICE.CONNECT, device);
|
|
1114
1175
|
}
|
|
1115
1176
|
} catch (err) {
|
|
1177
|
+
if (abortSignal?.aborted || device.wasInterruptedByUser()) {
|
|
1178
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
1179
|
+
}
|
|
1116
1180
|
const requiresColdReconnect = isMissingDetectedProtocolV2Error(method, err);
|
|
1117
1181
|
// Device.run()'s REQUIRE_DISCONNECT handling never sees acquire/initialize
|
|
1118
1182
|
// failures, so with keep-alive a wedged link would be reused by every retry
|
|
@@ -1163,6 +1227,7 @@ const ensureConnected = async (
|
|
|
1163
1227
|
const POLL_INTERVAL_TIME = (method.payload && method.payload.pollIntervalTime) || 1000;
|
|
1164
1228
|
const TIME_OUT = (method.payload && method.payload.timeout) || 10000;
|
|
1165
1229
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
1230
|
+
let lastInitializeError: any;
|
|
1166
1231
|
Log.debug(
|
|
1167
1232
|
`EnsureConnected function start, MAX_RETRY_COUNT=${MAX_RETRY_COUNT}, POLL_INTERVAL_TIME=${POLL_INTERVAL_TIME} `
|
|
1168
1233
|
);
|
|
@@ -1203,7 +1268,9 @@ const ensureConnected = async (
|
|
|
1203
1268
|
Log.debug('EnsureConnected function try count: ', tryCount, ' poll interval time: ', time);
|
|
1204
1269
|
try {
|
|
1205
1270
|
await initDeviceList(method);
|
|
1271
|
+
lastInitializeError = undefined;
|
|
1206
1272
|
} catch (error) {
|
|
1273
|
+
lastInitializeError = error;
|
|
1207
1274
|
Log.debug('device list error: ', error);
|
|
1208
1275
|
if (
|
|
1209
1276
|
[
|
|
@@ -1218,6 +1285,7 @@ const ensureConnected = async (
|
|
|
1218
1285
|
}
|
|
1219
1286
|
if (error.errorCode === HardwareErrorCode.TransportNotConfigured) {
|
|
1220
1287
|
await TransportManager.configure();
|
|
1288
|
+
lastInitializeError = undefined;
|
|
1221
1289
|
}
|
|
1222
1290
|
}
|
|
1223
1291
|
|
|
@@ -1286,6 +1354,8 @@ const ensureConnected = async (
|
|
|
1286
1354
|
HardwareErrorCode.BleDeviceBondedCanceled,
|
|
1287
1355
|
HardwareErrorCode.BleCharacteristicNotifyError,
|
|
1288
1356
|
HardwareErrorCode.BleTimeoutError,
|
|
1357
|
+
// A transport setup reset must also stop this outer poll.
|
|
1358
|
+
HardwareErrorCode.PollingTimeout,
|
|
1289
1359
|
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1290
1360
|
HardwareErrorCode.BleAlreadyConnected,
|
|
1291
1361
|
HardwareErrorCode.FirmwareUpdateLimitOneDevice,
|
|
@@ -1296,6 +1366,10 @@ const ensureConnected = async (
|
|
|
1296
1366
|
HardwareErrorCode.DeviceInterruptedFromUser,
|
|
1297
1367
|
HardwareErrorCode.CallQueueActionCancelled,
|
|
1298
1368
|
].includes(error.errorCode) ||
|
|
1369
|
+
(env === 'react-native' &&
|
|
1370
|
+
[HardwareErrorCode.BleDeviceDisconnected, HardwareErrorCode.PollingTimeout].includes(
|
|
1371
|
+
error.errorCode
|
|
1372
|
+
)) ||
|
|
1299
1373
|
isTerminalBleStaleBondError(error) ||
|
|
1300
1374
|
isDeviceIdentityMismatchError(error)
|
|
1301
1375
|
) {
|
|
@@ -1309,14 +1383,22 @@ const ensureConnected = async (
|
|
|
1309
1383
|
clearTimeout(timer);
|
|
1310
1384
|
}
|
|
1311
1385
|
Log.debug('EnsureConnected get to max try count, will return: ', tryCount);
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1386
|
+
const preserveWebUsbInitError =
|
|
1387
|
+
DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env);
|
|
1388
|
+
const needsPermissionPrompt =
|
|
1389
|
+
DataManager.isBrowserWebUsb(env) && !method.payload?.skipWebDevicePrompt;
|
|
1390
|
+
const fallbackError = needsPermissionPrompt
|
|
1391
|
+
? ERRORS.TypedError(HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission)
|
|
1392
|
+
: ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
|
|
1393
|
+
const errorToReject =
|
|
1394
|
+
preserveWebUsbInitError && lastInitializeError ? lastInitializeError : fallbackError;
|
|
1395
|
+
// Only ask the host for a WebUSB grant when the failure is actually
|
|
1396
|
+
// "not found / needs permission". A preserved initialize error must
|
|
1397
|
+
// not fire that prompt with a different public code.
|
|
1398
|
+
if (needsPermissionPrompt && errorToReject === fallbackError) {
|
|
1315
1399
|
postMessage(createUiMessage(UI_REQUEST.WEB_DEVICE_PROMPT_ACCESS_PERMISSION));
|
|
1316
|
-
reject(ERRORS.TypedError(HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission));
|
|
1317
|
-
} else {
|
|
1318
|
-
reject(ERRORS.TypedError(HardwareErrorCode.DeviceNotFound));
|
|
1319
1400
|
}
|
|
1401
|
+
reject(errorToReject);
|
|
1320
1402
|
return;
|
|
1321
1403
|
}
|
|
1322
1404
|
|
|
@@ -1774,7 +1856,17 @@ export default class Core extends EventEmitter {
|
|
|
1774
1856
|
this.prePendingCallPromises.delete(connectId);
|
|
1775
1857
|
return;
|
|
1776
1858
|
}
|
|
1777
|
-
this.prePendingCallPromises.
|
|
1859
|
+
const previous = this.prePendingCallPromises.get(connectId);
|
|
1860
|
+
const cleanupPromise =
|
|
1861
|
+
previous && previous !== promise
|
|
1862
|
+
? Promise.all([previous, promise]).then(() => undefined)
|
|
1863
|
+
: promise;
|
|
1864
|
+
this.prePendingCallPromises.set(connectId, cleanupPromise);
|
|
1865
|
+
// cancel() is fire-and-forget. Observe failures immediately, while
|
|
1866
|
+
// preserving the rejected barrier for the next caller's safety check.
|
|
1867
|
+
cleanupPromise.catch(error => {
|
|
1868
|
+
Log.warn('Device cancellation cleanup failed', { errorCode: error?.errorCode });
|
|
1869
|
+
});
|
|
1778
1870
|
},
|
|
1779
1871
|
removePrePendingCallPromise: (connectId: string, promise: Promise<void>) => {
|
|
1780
1872
|
if (this.prePendingCallPromises.get(connectId) === promise) {
|
|
@@ -11483,6 +11483,7 @@
|
|
|
11483
11483
|
"MessageType_DeviceFindMyTokenState": 60450,
|
|
11484
11484
|
"MessageType_DeviceFindMyTokenUpdate": 60451,
|
|
11485
11485
|
"MessageType_DeviceFindMyTokenStateGet": 60452,
|
|
11486
|
+
"MessageType_DeviceAnimationControl": 60461,
|
|
11486
11487
|
"MessageType_DeviceInfoGet": 60600,
|
|
11487
11488
|
"MessageType_DeviceInfo": 60601,
|
|
11488
11489
|
"MessageType_DeviceStatusGet": 60602,
|
|
@@ -11806,6 +11807,26 @@
|
|
|
11806
11807
|
}
|
|
11807
11808
|
}
|
|
11808
11809
|
},
|
|
11810
|
+
"DeviceAnimationAction": {
|
|
11811
|
+
"values": {
|
|
11812
|
+
"AnimationAction_Unknown": 0,
|
|
11813
|
+
"AnimationAction_Start": 1,
|
|
11814
|
+
"AnimationAction_Stop": 2
|
|
11815
|
+
}
|
|
11816
|
+
},
|
|
11817
|
+
"DeviceAnimationControl": {
|
|
11818
|
+
"fields": {
|
|
11819
|
+
"action": {
|
|
11820
|
+
"rule": "required",
|
|
11821
|
+
"type": "DeviceAnimationAction",
|
|
11822
|
+
"id": 1
|
|
11823
|
+
},
|
|
11824
|
+
"timeout_ms": {
|
|
11825
|
+
"type": "uint32",
|
|
11826
|
+
"id": 2
|
|
11827
|
+
}
|
|
11828
|
+
}
|
|
11829
|
+
},
|
|
11809
11830
|
"DeviceFactoryAck": {
|
|
11810
11831
|
"values": {
|
|
11811
11832
|
"FACTORY_ACK_SUCCESS": 0,
|
|
@@ -33,6 +33,8 @@ export default class TransportManager {
|
|
|
33
33
|
|
|
34
34
|
static reactNativeInit = false;
|
|
35
35
|
|
|
36
|
+
static webUsbInit = false;
|
|
37
|
+
|
|
36
38
|
static protocolV1MessageSchema: ProtocolV1MessageSchema = 'v1CurrentSchema';
|
|
37
39
|
|
|
38
40
|
static plugin: LowlevelTransportSharedPlugin | null = null;
|
|
@@ -42,6 +44,17 @@ export default class TransportManager {
|
|
|
42
44
|
this.defaultMessages = DataManager.getProtobufMessages();
|
|
43
45
|
this.currentMessages = this.defaultMessages;
|
|
44
46
|
this.protocolV1MessageSchema = 'v1CurrentSchema';
|
|
47
|
+
this.webUsbInit = false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static async ensureInitialized() {
|
|
51
|
+
const env = DataManager.getSettings('env');
|
|
52
|
+
if (env !== 'webusb' && env !== 'desktop-webusb') return;
|
|
53
|
+
if (this.webUsbInit) return;
|
|
54
|
+
// The emitter registers USB disconnect events; this must happen even when
|
|
55
|
+
// schema configuration is intentionally deferred during discovery.
|
|
56
|
+
await this.transport.init(WebUsbLogger, DevicePool.emitter);
|
|
57
|
+
this.webUsbInit = true;
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
static async configure() {
|
|
@@ -69,9 +82,7 @@ export default class TransportManager {
|
|
|
69
82
|
} else if (env === 'desktop-web-ble') {
|
|
70
83
|
await this.transport.init(WebBleLogger, DevicePool.emitter);
|
|
71
84
|
} else if (env === 'webusb' || env === 'desktop-webusb') {
|
|
72
|
-
|
|
73
|
-
// DEVICE.DISCONNECT; without it WebUSB never reports device removal.
|
|
74
|
-
await this.transport.init(WebUsbLogger, DevicePool.emitter);
|
|
85
|
+
await this.ensureInitialized();
|
|
75
86
|
} else {
|
|
76
87
|
await this.transport.init(HttpLogger);
|
|
77
88
|
}
|
package/src/device/Device.ts
CHANGED
|
@@ -297,6 +297,8 @@ export class Device extends EventEmitter {
|
|
|
297
297
|
/** Resolves only after the active run has completed its release path. */
|
|
298
298
|
private runCleanupPromise?: Promise<void>;
|
|
299
299
|
|
|
300
|
+
private userInterruption?: { attempt: number; promise: Promise<void> };
|
|
301
|
+
|
|
300
302
|
externalState: string[] = [];
|
|
301
303
|
|
|
302
304
|
unavailableCapabilities: UnavailableCapabilities = {};
|
|
@@ -1462,6 +1464,10 @@ export class Device extends EventEmitter {
|
|
|
1462
1464
|
this.invalidateProtocolV2RuntimeState();
|
|
1463
1465
|
}
|
|
1464
1466
|
|
|
1467
|
+
waitForRunCleanup(): Promise<void> {
|
|
1468
|
+
return this.runCleanupPromise ?? Promise.resolve();
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1465
1471
|
async run(fn?: () => Promise<void>, options?: RunOptions) {
|
|
1466
1472
|
if (this.runPromise) {
|
|
1467
1473
|
await this.interruptionFromOutside();
|
|
@@ -1596,45 +1602,70 @@ export class Device extends EventEmitter {
|
|
|
1596
1602
|
}
|
|
1597
1603
|
}
|
|
1598
1604
|
|
|
1599
|
-
|
|
1605
|
+
interruptionFromUser(): Promise<void> {
|
|
1606
|
+
const attempt = this.connectionAttempt;
|
|
1607
|
+
if (this.userInterruption?.attempt === attempt) {
|
|
1608
|
+
return this.userInterruption.promise;
|
|
1609
|
+
}
|
|
1600
1610
|
const error = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
1601
|
-
this.interruptedAttempt =
|
|
1611
|
+
this.interruptedAttempt = attempt;
|
|
1602
1612
|
const cleanupPromise = this.runCleanupPromise;
|
|
1603
|
-
const { cancelableAction } = this;
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1613
|
+
const { cancelableAction, commands, runPromise, deviceConnector, mainId } = this;
|
|
1614
|
+
const sendFallbackCancel = this.shouldSendFallbackProtocolCancel();
|
|
1615
|
+
const acquired = this.hasDeviceAcquire();
|
|
1616
|
+
const promise = (async () => {
|
|
1617
|
+
try {
|
|
1618
|
+
if (cancelableAction) {
|
|
1619
|
+
await cancelableAction(error);
|
|
1620
|
+
} else if (sendFallbackCancel) {
|
|
1621
|
+
await commands?.cancelDevice?.().catch(cancelError => {
|
|
1622
|
+
Log.debug('Protocol V2 fallback cancel error', cancelError);
|
|
1623
|
+
});
|
|
1624
|
+
} else if (!acquired) {
|
|
1625
|
+
// Abort setup without acquiring a session just to send Cancel.
|
|
1626
|
+
if (mainId && deviceConnector?.disconnect) {
|
|
1627
|
+
// The connector drops the link silently, so without this line a
|
|
1628
|
+
// user-cancel teardown is indistinguishable in field logs from an
|
|
1629
|
+
// idle keep-alive release or a device that left on its own.
|
|
1630
|
+
Log.debug(
|
|
1631
|
+
'interruptionFromUser: disconnecting device without acquire, mainId:',
|
|
1632
|
+
mainId
|
|
1633
|
+
);
|
|
1634
|
+
await deviceConnector.disconnect(mainId);
|
|
1635
|
+
}
|
|
1636
|
+
if (this.connectionAttempt === attempt) this.markTransportDisconnected();
|
|
1637
|
+
}
|
|
1638
|
+
await commands?.cancel();
|
|
1639
|
+
} catch (cleanupError) {
|
|
1640
|
+
Log.warn('User cancellation cleanup failed; disconnecting device', cleanupError);
|
|
1641
|
+
if (mainId && deviceConnector?.disconnect) {
|
|
1642
|
+
await deviceConnector.disconnect(mainId).catch(disconnectError => {
|
|
1643
|
+
Log.debug('User cancellation fallback disconnect failed', disconnectError);
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
if (this.connectionAttempt === attempt) this.markTransportDisconnected();
|
|
1647
|
+
} finally {
|
|
1648
|
+
runPromise?.reject(error);
|
|
1649
|
+
if (this.runPromise === runPromise) this.runPromise = null;
|
|
1650
|
+
await cleanupPromise?.catch(() => undefined);
|
|
1617
1651
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
this.runPromise.reject(error);
|
|
1624
|
-
this.runPromise = null;
|
|
1625
|
-
}
|
|
1626
|
-
await cleanupPromise?.catch(() => undefined);
|
|
1652
|
+
})();
|
|
1653
|
+
// Keep the settled promise for this attempt so a duplicate close/finally
|
|
1654
|
+
// cannot send a second wire Cancel. A new attempt gets its own cleanup.
|
|
1655
|
+
this.userInterruption = { attempt, promise };
|
|
1656
|
+
return promise;
|
|
1627
1657
|
}
|
|
1628
1658
|
|
|
1629
1659
|
setCancelableAction(callback: (err?: Error) => Promise<unknown>) {
|
|
1630
|
-
|
|
1660
|
+
const action = (e?: Error) =>
|
|
1631
1661
|
callback(e)
|
|
1632
1662
|
.catch(e2 => {
|
|
1633
1663
|
Log.debug('cancelableAction error', e2);
|
|
1634
1664
|
})
|
|
1635
1665
|
.finally(() => {
|
|
1636
|
-
this.clearCancelableAction();
|
|
1666
|
+
if (this.cancelableAction === action) this.clearCancelableAction();
|
|
1637
1667
|
});
|
|
1668
|
+
this.cancelableAction = action;
|
|
1638
1669
|
}
|
|
1639
1670
|
|
|
1640
1671
|
clearCancelableAction() {
|
|
@@ -268,6 +268,7 @@ export async function getProtocolV2WalletSession(
|
|
|
268
268
|
? device.getInternalState()
|
|
269
269
|
: undefined;
|
|
270
270
|
let response;
|
|
271
|
+
let walletSelectionRequested = false;
|
|
271
272
|
let resumed = false;
|
|
272
273
|
let walletStatusRefreshed = false;
|
|
273
274
|
const markWalletStatusRefreshed = () => {
|
|
@@ -482,6 +483,7 @@ export async function getProtocolV2WalletSession(
|
|
|
482
483
|
throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
|
|
483
484
|
}
|
|
484
485
|
await lockAttachPinBeforePassphraseSelection();
|
|
486
|
+
walletSelectionRequested = true;
|
|
485
487
|
response = await selectDeviceSession(
|
|
486
488
|
device,
|
|
487
489
|
expectedPassphraseState,
|
|
@@ -508,6 +510,11 @@ export async function getProtocolV2WalletSession(
|
|
|
508
510
|
device.clearInternalState();
|
|
509
511
|
throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
|
|
510
512
|
}
|
|
513
|
+
// Report a fresh selection mismatch before asking for input again.
|
|
514
|
+
if (walletSelectionRequested) {
|
|
515
|
+
clearCurrentWalletSession();
|
|
516
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
|
|
517
|
+
}
|
|
511
518
|
if (options?.onlyMainPin) {
|
|
512
519
|
device.clearStandardInternalState?.();
|
|
513
520
|
await selectStandardWallet(true);
|
|
@@ -2,6 +2,7 @@ import type { CommonParams, Response } from '../params';
|
|
|
2
2
|
import type { Features } from '../device';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* @deprecated Use `getDeviceState` for both Protocol V1 and Protocol V2
|
|
5
|
+
* @deprecated Use `getDeviceState` for both Protocol V1 and Protocol V2.
|
|
6
|
+
* Existing calls still work. Read `payload.identity.deviceId` from `getDeviceState`.
|
|
6
7
|
*/
|
|
7
8
|
export declare function getFeatures(connectId?: string, params?: CommonParams): Response<Features>;
|
|
@@ -3,11 +3,8 @@ import type { CommonParams, Response } from '../params';
|
|
|
3
3
|
export type GetPassphraseStateParams = CommonParams;
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Protocol V1 keeps its parameterless firmware flow. Protocol V2 maps the existing
|
|
9
|
-
* `useEmptyPassphrase` and `initSession` intent to the device-only wallet-session flow.
|
|
10
|
-
* New integrations should prefer `openWalletSession` for explicit wallet intent.
|
|
6
|
+
* @deprecated Use `openWalletSession` for both Protocol V1 and Protocol V2.
|
|
7
|
+
* Existing calls still work. Persist `deviceId + passphraseState`; do not persist firmware `session_id`.
|
|
11
8
|
*/
|
|
12
9
|
export declare function getPassphraseState(
|
|
13
10
|
connectId?: string,
|
|
@@ -3,28 +3,16 @@ import type { CommonParams, Response } from '../params';
|
|
|
3
3
|
export const OpenWalletSessionMode = {
|
|
4
4
|
Standard: 'standard',
|
|
5
5
|
SelectHidden: 'select-hidden',
|
|
6
|
-
ResumeHidden: 'resume-hidden',
|
|
7
6
|
} as const;
|
|
8
7
|
|
|
9
8
|
export type OpenWalletSessionModeValue =
|
|
10
9
|
(typeof OpenWalletSessionMode)[keyof typeof OpenWalletSessionMode];
|
|
11
10
|
|
|
12
|
-
export type OpenWalletSessionParams =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
| {
|
|
19
|
-
mode: typeof OpenWalletSessionMode.SelectHidden;
|
|
20
|
-
deviceId?: never;
|
|
21
|
-
passphraseState?: never;
|
|
22
|
-
}
|
|
23
|
-
| {
|
|
24
|
-
mode: typeof OpenWalletSessionMode.ResumeHidden;
|
|
25
|
-
deviceId: string;
|
|
26
|
-
passphraseState: string;
|
|
27
|
-
};
|
|
11
|
+
export type OpenWalletSessionParams = {
|
|
12
|
+
mode: OpenWalletSessionModeValue;
|
|
13
|
+
deviceId?: never;
|
|
14
|
+
passphraseState?: never;
|
|
15
|
+
};
|
|
28
16
|
|
|
29
17
|
type OpenWalletSessionPayloadBase = {
|
|
30
18
|
protocol: 'V1' | 'V2';
|
|
@@ -45,8 +33,8 @@ export type OpenWalletSessionPayload = OpenWalletSessionPayloadBase &
|
|
|
45
33
|
);
|
|
46
34
|
|
|
47
35
|
/**
|
|
48
|
-
* Opens the standard
|
|
49
|
-
*
|
|
36
|
+
* Opens the standard or hidden wallet through a unified Protocol V1/V2 API.
|
|
37
|
+
* Resume a bound hidden wallet by passing passphraseState on later methods.
|
|
50
38
|
*/
|
|
51
39
|
export declare function openWalletSession(
|
|
52
40
|
connectId: string,
|
package/src/types/params.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { HardwareConnectProtocol } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
export interface CommonParams {
|
|
4
|
+
/**
|
|
5
|
+
* Keep the transport session after this call instead of releasing it.
|
|
6
|
+
* This is a Device.run() hold flag, not a wallet identity. It does not replace
|
|
7
|
+
* `useEmptyPassphrase`, `passphraseState`, or `openWalletSession`.
|
|
8
|
+
* USB multi-step flows and Electron BLE firmware windows still need this to
|
|
9
|
+
* avoid dropping the link between calls.
|
|
10
|
+
*/
|
|
4
11
|
keepSession?: boolean;
|
|
5
12
|
/**
|
|
6
13
|
* polling connect max retry count
|