@onekeyfe/hd-core 1.2.2-alpha.105 → 1.2.2-alpha.106
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 +2 -2
- package/__tests__/core-error-output.test.ts +92 -1
- package/__tests__/device-lifecycle-events.test.ts +313 -2
- package/__tests__/open-wallet-session-error-response.test.ts +2 -2
- package/__tests__/open-wallet-session.test.ts +8 -411
- package/__tests__/public-device-state-api.test.ts +2 -7
- 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/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/device/Device.d.ts +2 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/index.d.ts +16 -18
- package/dist/index.js +156 -124
- 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/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/core/RequestQueue.ts +20 -0
- package/src/core/index.ts +95 -42
- package/src/device/Device.ts +51 -27
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-core",
|
|
3
|
-
"version": "1.2.2-alpha.
|
|
3
|
+
"version": "1.2.2-alpha.106",
|
|
4
4
|
"description": "Core processes and APIs for communicating with OneKey hardware devices.",
|
|
5
5
|
"author": "OneKey",
|
|
6
6
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@onekeyfe/hd-shared": "1.2.2-alpha.
|
|
29
|
-
"@onekeyfe/hd-transport": "1.2.2-alpha.
|
|
28
|
+
"@onekeyfe/hd-shared": "1.2.2-alpha.106",
|
|
29
|
+
"@onekeyfe/hd-transport": "1.2.2-alpha.106",
|
|
30
30
|
"axios": "1.15.2",
|
|
31
31
|
"bignumber.js": "^9.0.2",
|
|
32
32
|
"buffer": "^6.0.3",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"@types/w3c-web-usb": "^1.0.10",
|
|
47
47
|
"@types/web-bluetooth": "^0.0.21"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "e52ad091098aaab6b2845154457ace5e2f8c9098"
|
|
50
50
|
}
|
package/src/api/GetFeatures.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { UI_REQUEST } from '../constants/ui-request';
|
|
|
4
4
|
import { projectFeatures } from '../device/DeviceStateProjector';
|
|
5
5
|
import { BaseMethod } from './BaseMethod';
|
|
6
6
|
|
|
7
|
+
/** @deprecated Use GetDeviceState / HardwareSDK.getDeviceState. */
|
|
7
8
|
export default class GetFeatures extends BaseMethod {
|
|
8
9
|
init() {
|
|
9
10
|
this.unlockPolicy = 'none';
|
|
@@ -6,6 +6,7 @@ import { refreshProtocolV2DeviceStatus } from '../protocols/protocol-v2/walletSe
|
|
|
6
6
|
import { getPassphraseStateWithRefreshDeviceInfo } from '../utils/deviceFeaturesUtils';
|
|
7
7
|
import { BaseMethod } from './BaseMethod';
|
|
8
8
|
|
|
9
|
+
/** @deprecated Use OpenWalletSession / HardwareSDK.openWalletSession. */
|
|
9
10
|
export default class GetPassphraseState extends BaseMethod {
|
|
10
11
|
getSupportedProtocols() {
|
|
11
12
|
return ['V1', 'V2'] as const;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
2
|
import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
|
|
3
3
|
|
|
4
|
-
import { deviceWalletSessionStore } from '../device/DeviceWalletSessionStore';
|
|
5
4
|
import { getProtocolV2WalletSession } from '../protocols/protocol-v2/walletSession';
|
|
6
5
|
import { getPassphraseStateWithRefreshDeviceInfo } from '../utils/deviceFeaturesUtils';
|
|
7
6
|
import { BaseMethod } from './BaseMethod';
|
|
@@ -13,16 +12,6 @@ import type {
|
|
|
13
12
|
OpenWalletSessionPayload,
|
|
14
13
|
} from '../types/api/openWalletSession';
|
|
15
14
|
|
|
16
|
-
const requiredString = (value: unknown, name: string) => {
|
|
17
|
-
if (value === undefined || value === null) {
|
|
18
|
-
throw invalidParameter(`Missing required parameter: ${name}`);
|
|
19
|
-
}
|
|
20
|
-
if (typeof value !== 'string' || !value.trim()) {
|
|
21
|
-
throw invalidParameter(`Parameter [${name}] must be a non-empty string.`);
|
|
22
|
-
}
|
|
23
|
-
return value.trim();
|
|
24
|
-
};
|
|
25
|
-
|
|
26
15
|
const wasResumed = (session: unknown) =>
|
|
27
16
|
!!session &&
|
|
28
17
|
typeof session === 'object' &&
|
|
@@ -53,34 +42,21 @@ const normalizeParams = (payload: Record<string, unknown>): OpenWalletSessionPar
|
|
|
53
42
|
}
|
|
54
43
|
if (
|
|
55
44
|
payload.mode !== OpenWalletSessionMode.Standard &&
|
|
56
|
-
payload.mode !== OpenWalletSessionMode.SelectHidden
|
|
57
|
-
payload.mode !== OpenWalletSessionMode.ResumeHidden
|
|
45
|
+
payload.mode !== OpenWalletSessionMode.SelectHidden
|
|
58
46
|
) {
|
|
59
|
-
throw invalidParameter(
|
|
60
|
-
'Parameter [mode] must be one of standard, select-hidden, or resume-hidden.'
|
|
61
|
-
);
|
|
47
|
+
throw invalidParameter('Parameter [mode] must be one of standard or select-hidden.');
|
|
62
48
|
}
|
|
63
49
|
if (payload.useEmptyPassphrase !== undefined || payload.initSession !== undefined) {
|
|
64
50
|
throw invalidParameter(
|
|
65
51
|
'Legacy parameters [useEmptyPassphrase] and [initSession] are not supported by openWalletSession.'
|
|
66
52
|
);
|
|
67
53
|
}
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (payload.deviceId !== undefined || payload.passphraseState !== undefined) {
|
|
73
|
-
throw invalidParameter(
|
|
74
|
-
'Parameters [deviceId] and [passphraseState] are only allowed with mode [resume-hidden].'
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
return { mode: payload.mode };
|
|
54
|
+
if (payload.deviceId !== undefined || payload.passphraseState !== undefined) {
|
|
55
|
+
throw invalidParameter(
|
|
56
|
+
'Parameters [deviceId] and [passphraseState] are not supported by openWalletSession. Pass passphraseState on later address and signing calls.'
|
|
57
|
+
);
|
|
78
58
|
}
|
|
79
|
-
return {
|
|
80
|
-
mode: OpenWalletSessionMode.ResumeHidden,
|
|
81
|
-
deviceId: requiredString(payload.deviceId, 'deviceId'),
|
|
82
|
-
passphraseState: requiredString(payload.passphraseState, 'passphraseState'),
|
|
83
|
-
};
|
|
59
|
+
return { mode: payload.mode };
|
|
84
60
|
};
|
|
85
61
|
|
|
86
62
|
export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParams> {
|
|
@@ -196,52 +172,6 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
|
|
|
196
172
|
};
|
|
197
173
|
}
|
|
198
174
|
|
|
199
|
-
if (this.params.mode === OpenWalletSessionMode.ResumeHidden) {
|
|
200
|
-
if (isProtocolV2) {
|
|
201
|
-
await ensureProtocolV2WalletStatus();
|
|
202
|
-
const refreshedDeviceId = requireDeviceId();
|
|
203
|
-
if (refreshedDeviceId !== this.params.deviceId) {
|
|
204
|
-
deviceWalletSessionStore.delete(this.params.deviceId, this.params.passphraseState);
|
|
205
|
-
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
|
|
206
|
-
}
|
|
207
|
-
} else if (requireDeviceId() !== this.params.deviceId) {
|
|
208
|
-
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
|
|
209
|
-
}
|
|
210
|
-
this.device.passphraseState = this.params.passphraseState;
|
|
211
|
-
const cachedSessionId = deviceWalletSessionStore.get(
|
|
212
|
-
this.params.deviceId,
|
|
213
|
-
this.params.passphraseState
|
|
214
|
-
);
|
|
215
|
-
if (!cachedSessionId && !isProtocolV2) {
|
|
216
|
-
throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
|
|
217
|
-
}
|
|
218
|
-
if (!isProtocolV2) {
|
|
219
|
-
await this.device.initialize({
|
|
220
|
-
deviceId: this.params.deviceId,
|
|
221
|
-
passphraseState: this.params.passphraseState,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
const session = isProtocolV2
|
|
225
|
-
? await getProtocolV2WalletSession(this.device, {
|
|
226
|
-
expectedPassphraseState: this.params.passphraseState,
|
|
227
|
-
})
|
|
228
|
-
: await getPassphraseStateWithRefreshDeviceInfo(this.device, {
|
|
229
|
-
expectPassphraseState: this.params.passphraseState,
|
|
230
|
-
});
|
|
231
|
-
const deviceId = requireDeviceId();
|
|
232
|
-
if (session.passphraseState !== this.params.passphraseState) {
|
|
233
|
-
this.device.clearInternalState();
|
|
234
|
-
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
|
|
235
|
-
}
|
|
236
|
-
return {
|
|
237
|
-
protocol,
|
|
238
|
-
walletType: 'hidden',
|
|
239
|
-
deviceId,
|
|
240
|
-
...requireHiddenWalletResponse(session),
|
|
241
|
-
resumed: wasResumed(session) || (!isProtocolV2 && session.newSession === cachedSessionId),
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
175
|
this.device.passphraseState = undefined;
|
|
246
176
|
const walletStatus = await ensureProtocolV2WalletStatus();
|
|
247
177
|
if (isProtocolV2 && walletStatus.status.passphraseProtection !== true) {
|
package/src/core/RequestQueue.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
1
3
|
import { LoggerNames, getLogger } from '../utils';
|
|
2
4
|
|
|
3
5
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
@@ -40,6 +42,24 @@ export default class RequestQueue {
|
|
|
40
42
|
return this.requestQueue.get(requestId);
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
public async waitForTask<T>(task: RequestTask, pending: () => Promise<T>): Promise<T> {
|
|
46
|
+
const signal = task.method.abortSignal;
|
|
47
|
+
const cancellationError = () => ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
48
|
+
if (signal?.aborted) throw cancellationError();
|
|
49
|
+
let onAbort: (() => void) | undefined;
|
|
50
|
+
try {
|
|
51
|
+
const cancelled = new Promise<never>((_, reject) => {
|
|
52
|
+
onAbort = () => reject(cancellationError());
|
|
53
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
54
|
+
});
|
|
55
|
+
const result = await Promise.race([pending(), cancelled]);
|
|
56
|
+
if (signal?.aborted) throw cancellationError();
|
|
57
|
+
return result;
|
|
58
|
+
} finally {
|
|
59
|
+
if (onAbort) signal?.removeEventListener('abort', onAbort);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
// 获取请求的AbortController
|
|
44
64
|
public getAbortController(requestId: number) {
|
|
45
65
|
return this.requestQueue.get(requestId)?.abortController;
|
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,
|
|
@@ -84,6 +85,7 @@ import type { BaseMethod } from '../api/BaseMethod';
|
|
|
84
85
|
const Log = getLogger(LoggerNames.Core);
|
|
85
86
|
const PRE_INITIALIZE_TTL_MS = 60 * 1000;
|
|
86
87
|
const PRE_PENDING_CALL_TIMEOUT_MS = 15 * 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.
|
|
@@ -313,30 +315,44 @@ const waitForPendingPromise = async (
|
|
|
313
315
|
if (pendingPromise) {
|
|
314
316
|
Log.debug('pre pending call promise before call method, wait for it');
|
|
315
317
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
316
|
-
let timedOut = false;
|
|
317
318
|
try {
|
|
318
319
|
await Promise.race([
|
|
319
320
|
pendingPromise,
|
|
320
|
-
new Promise<void>(
|
|
321
|
+
new Promise<void>((_, reject) => {
|
|
321
322
|
timer = setTimeout(() => {
|
|
322
|
-
|
|
323
|
-
|
|
323
|
+
reject(
|
|
324
|
+
ERRORS.TypedError(
|
|
325
|
+
HardwareErrorCode.DeviceBusy,
|
|
326
|
+
'Previous device cancellation is still draining'
|
|
327
|
+
)
|
|
328
|
+
);
|
|
324
329
|
}, PRE_PENDING_CALL_TIMEOUT_MS);
|
|
325
330
|
}),
|
|
326
331
|
]);
|
|
327
|
-
|
|
328
|
-
//
|
|
332
|
+
// A deadline is not evidence that old I/O is safe to reuse. Keep the
|
|
333
|
+
// barrier on failure; a later call may proceed only after cleanup settles.
|
|
334
|
+
removePrePendingCallPromise?.(connectId, pendingPromise);
|
|
329
335
|
} finally {
|
|
330
336
|
if (timer) clearTimeout(timer);
|
|
331
|
-
removePrePendingCallPromise?.(connectId, pendingPromise);
|
|
332
|
-
}
|
|
333
|
-
if (timedOut) {
|
|
334
|
-
Log.warn('pre pending call promise timed out before call method', { connectId });
|
|
335
337
|
}
|
|
336
338
|
Log.debug('pre pending call promise before call method done');
|
|
337
339
|
}
|
|
338
340
|
};
|
|
339
341
|
|
|
342
|
+
export function getPostCallPendingPromise(method: BaseMethod, device: Device): Promise<void> {
|
|
343
|
+
const cleanupPromise = device.waitForRunCleanup();
|
|
344
|
+
const env = DataManager.getSettings('env');
|
|
345
|
+
const deviceType = device.getCurrentDeviceType();
|
|
346
|
+
const requiresSigningCooldown =
|
|
347
|
+
method.name.includes('Sign') &&
|
|
348
|
+
(deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) &&
|
|
349
|
+
(DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'node-usb');
|
|
350
|
+
|
|
351
|
+
return requiresSigningCooldown
|
|
352
|
+
? cleanupPromise.then(() => wait(PRO2_USB_SIGNING_COOLDOWN_MS)).then(() => undefined)
|
|
353
|
+
: cleanupPromise;
|
|
354
|
+
}
|
|
355
|
+
|
|
340
356
|
const onCallDevice = async (
|
|
341
357
|
context: CoreContext,
|
|
342
358
|
message: CoreMessage,
|
|
@@ -367,42 +383,38 @@ const onCallDevice = async (
|
|
|
367
383
|
DevicePool.clearDeviceCache(method.payload.connectId);
|
|
368
384
|
}
|
|
369
385
|
|
|
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
|
-
|
|
386
|
+
// Register before waiting so cancellation also covers queued requests.
|
|
381
387
|
const task = requestQueue.createTask(method);
|
|
382
388
|
|
|
383
389
|
// Pre-warm holds the device as a per-connectId callback task so a concurrent
|
|
384
390
|
// real call waits (before ensureConnected) instead of racing its Initialize.
|
|
385
391
|
// Only covers pre-warm -> real-call ordering; the reverse is fail-closed.
|
|
386
392
|
let preWarmCallbackTask: Deferred<void> | undefined;
|
|
387
|
-
if (method.isPreWarmSignal && method.connectId) {
|
|
388
|
-
preWarmCallbackTask = createDeferred<void>();
|
|
389
|
-
context.registerCallbackTask(method.connectId, preWarmCallbackTask);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
393
|
let device: Device;
|
|
393
394
|
try {
|
|
395
|
+
const connectId = method.connectId ?? '';
|
|
396
|
+
if (connectId) {
|
|
397
|
+
await requestQueue.waitForTask(task, () => context.waitForCallbackTasks(connectId));
|
|
398
|
+
}
|
|
399
|
+
await requestQueue.waitForTask(task, () =>
|
|
400
|
+
waitForPendingPromise(
|
|
401
|
+
method.connectId ?? '',
|
|
402
|
+
getPrePendingCallPromise,
|
|
403
|
+
removePrePendingCallPromise
|
|
404
|
+
)
|
|
405
|
+
);
|
|
406
|
+
if (method.isPreWarmSignal && method.connectId) {
|
|
407
|
+
preWarmCallbackTask = createDeferred<void>();
|
|
408
|
+
context.registerCallbackTask(method.connectId, preWarmCallbackTask);
|
|
409
|
+
}
|
|
394
410
|
/**
|
|
395
411
|
* Polling to ensure successful connection
|
|
396
412
|
*/
|
|
397
|
-
const connectId = method.connectId ?? '';
|
|
398
413
|
const pollingId = pollingManager.start(connectId);
|
|
399
|
-
device = await ensureConnected(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
pollingId,
|
|
404
|
-
task.abortController?.signal
|
|
405
|
-
);
|
|
414
|
+
device = await ensureConnected(context, method, connectId, pollingId, method.abortSignal);
|
|
415
|
+
if (method.abortSignal?.aborted) {
|
|
416
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
417
|
+
}
|
|
406
418
|
} catch (e) {
|
|
407
419
|
preWarmCallbackTask?.resolve();
|
|
408
420
|
Log.debug('ensureConnected error: ', e);
|
|
@@ -478,14 +490,19 @@ const onCallDevice = async (
|
|
|
478
490
|
|
|
479
491
|
try {
|
|
480
492
|
// Wait for any pending task except our own (self-wait would deadlock).
|
|
481
|
-
|
|
482
|
-
|
|
493
|
+
const { connectId } = method;
|
|
494
|
+
if (connectId) {
|
|
495
|
+
await requestQueue.waitForTask(task, () =>
|
|
496
|
+
context.waitForCallbackTasks(connectId, preWarmCallbackTask)
|
|
497
|
+
);
|
|
483
498
|
}
|
|
484
499
|
|
|
485
|
-
await
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
500
|
+
await requestQueue.waitForTask(task, () =>
|
|
501
|
+
waitForPendingPromise(
|
|
502
|
+
method.connectId ?? '',
|
|
503
|
+
getPrePendingCallPromise,
|
|
504
|
+
removePrePendingCallPromise
|
|
505
|
+
)
|
|
489
506
|
);
|
|
490
507
|
|
|
491
508
|
const inner = async (): Promise<void> => {
|
|
@@ -690,6 +707,13 @@ const onCallDevice = async (
|
|
|
690
707
|
},
|
|
691
708
|
});
|
|
692
709
|
messageResponse = createResponseMessage(method.responseID, true, response);
|
|
710
|
+
// Preserve the acknowledged result while the next call waits for cleanup and cooldown.
|
|
711
|
+
if (method.connectId) {
|
|
712
|
+
context.setPrePendingCallPromise(
|
|
713
|
+
method.connectId,
|
|
714
|
+
getPostCallPendingPromise(method, device)
|
|
715
|
+
);
|
|
716
|
+
}
|
|
693
717
|
requestQueue.resolveRequest(method.responseID, messageResponse);
|
|
694
718
|
completeMethodRequestContext(method);
|
|
695
719
|
} catch (error) {
|
|
@@ -701,6 +725,12 @@ const onCallDevice = async (
|
|
|
701
725
|
}
|
|
702
726
|
Log.debug(`Call API - Inner Method Run Error`, error);
|
|
703
727
|
messageResponse = createResponseMessage(method.responseID, false, { error });
|
|
728
|
+
if (method.connectId) {
|
|
729
|
+
context.setPrePendingCallPromise(
|
|
730
|
+
method.connectId,
|
|
731
|
+
getPostCallPendingPromise(method, device)
|
|
732
|
+
);
|
|
733
|
+
}
|
|
704
734
|
requestQueue.resolveRequest(method.responseID, messageResponse);
|
|
705
735
|
completeMethodRequestContext(method, error);
|
|
706
736
|
|
|
@@ -727,6 +757,9 @@ const onCallDevice = async (
|
|
|
727
757
|
skipInitialize: canSkipInitialize(method, device),
|
|
728
758
|
...parseInitOptions(method),
|
|
729
759
|
};
|
|
760
|
+
if (method.abortSignal?.aborted) {
|
|
761
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
762
|
+
}
|
|
730
763
|
const deviceRun = () => device.run(inner, runOptions);
|
|
731
764
|
task.callPromise = createDeferred<any>(deviceRun);
|
|
732
765
|
|
|
@@ -747,6 +780,7 @@ const onCallDevice = async (
|
|
|
747
780
|
);
|
|
748
781
|
Log.debug('Call API - Run Error: ', error);
|
|
749
782
|
completeMethodRequestContext(method, error);
|
|
783
|
+
return messageResponse;
|
|
750
784
|
} finally {
|
|
751
785
|
// Release the pre-warm callback task so the next real call can proceed.
|
|
752
786
|
preWarmCallbackTask?.resolve();
|
|
@@ -1044,7 +1078,7 @@ async function connectDeviceForBle(
|
|
|
1044
1078
|
retryCount = 0
|
|
1045
1079
|
) {
|
|
1046
1080
|
try {
|
|
1047
|
-
if (device.wasInterruptedByUser()) {
|
|
1081
|
+
if (abortSignal?.aborted || device.wasInterruptedByUser()) {
|
|
1048
1082
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
1049
1083
|
}
|
|
1050
1084
|
if (method.payload.forceProtocolDetection && device.hasDeviceAcquire()) {
|
|
@@ -1113,6 +1147,9 @@ async function connectDeviceForBle(
|
|
|
1113
1147
|
DevicePool.emitter.emit(DEVICE.CONNECT, device);
|
|
1114
1148
|
}
|
|
1115
1149
|
} catch (err) {
|
|
1150
|
+
if (abortSignal?.aborted || device.wasInterruptedByUser()) {
|
|
1151
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
1152
|
+
}
|
|
1116
1153
|
const requiresColdReconnect = isMissingDetectedProtocolV2Error(method, err);
|
|
1117
1154
|
// Device.run()'s REQUIRE_DISCONNECT handling never sees acquire/initialize
|
|
1118
1155
|
// failures, so with keep-alive a wedged link would be reused by every retry
|
|
@@ -1286,6 +1323,8 @@ const ensureConnected = async (
|
|
|
1286
1323
|
HardwareErrorCode.BleDeviceBondedCanceled,
|
|
1287
1324
|
HardwareErrorCode.BleCharacteristicNotifyError,
|
|
1288
1325
|
HardwareErrorCode.BleTimeoutError,
|
|
1326
|
+
// A transport setup reset must also stop this outer poll.
|
|
1327
|
+
HardwareErrorCode.PollingTimeout,
|
|
1289
1328
|
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1290
1329
|
HardwareErrorCode.BleAlreadyConnected,
|
|
1291
1330
|
HardwareErrorCode.FirmwareUpdateLimitOneDevice,
|
|
@@ -1296,6 +1335,10 @@ const ensureConnected = async (
|
|
|
1296
1335
|
HardwareErrorCode.DeviceInterruptedFromUser,
|
|
1297
1336
|
HardwareErrorCode.CallQueueActionCancelled,
|
|
1298
1337
|
].includes(error.errorCode) ||
|
|
1338
|
+
(env === 'react-native' &&
|
|
1339
|
+
[HardwareErrorCode.BleDeviceDisconnected, HardwareErrorCode.PollingTimeout].includes(
|
|
1340
|
+
error.errorCode
|
|
1341
|
+
)) ||
|
|
1299
1342
|
isTerminalBleStaleBondError(error) ||
|
|
1300
1343
|
isDeviceIdentityMismatchError(error)
|
|
1301
1344
|
) {
|
|
@@ -1774,7 +1817,17 @@ export default class Core extends EventEmitter {
|
|
|
1774
1817
|
this.prePendingCallPromises.delete(connectId);
|
|
1775
1818
|
return;
|
|
1776
1819
|
}
|
|
1777
|
-
this.prePendingCallPromises.
|
|
1820
|
+
const previous = this.prePendingCallPromises.get(connectId);
|
|
1821
|
+
const cleanupPromise =
|
|
1822
|
+
previous && previous !== promise
|
|
1823
|
+
? Promise.all([previous, promise]).then(() => undefined)
|
|
1824
|
+
: promise;
|
|
1825
|
+
this.prePendingCallPromises.set(connectId, cleanupPromise);
|
|
1826
|
+
// cancel() is fire-and-forget. Observe failures immediately, while
|
|
1827
|
+
// preserving the rejected barrier for the next caller's safety check.
|
|
1828
|
+
cleanupPromise.catch(error => {
|
|
1829
|
+
Log.warn('Device cancellation cleanup failed', { errorCode: error?.errorCode });
|
|
1830
|
+
});
|
|
1778
1831
|
},
|
|
1779
1832
|
removePrePendingCallPromise: (connectId: string, promise: Promise<void>) => {
|
|
1780
1833
|
if (this.prePendingCallPromises.get(connectId) === promise) {
|
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,63 @@ 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
|
+
await deviceConnector.disconnect(mainId);
|
|
1628
|
+
}
|
|
1629
|
+
if (this.connectionAttempt === attempt) this.markTransportDisconnected();
|
|
1630
|
+
}
|
|
1631
|
+
await commands?.cancel();
|
|
1632
|
+
} catch (cleanupError) {
|
|
1633
|
+
Log.warn('User cancellation cleanup failed; disconnecting device', cleanupError);
|
|
1634
|
+
if (mainId && deviceConnector?.disconnect) {
|
|
1635
|
+
await deviceConnector.disconnect(mainId).catch(disconnectError => {
|
|
1636
|
+
Log.debug('User cancellation fallback disconnect failed', disconnectError);
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
if (this.connectionAttempt === attempt) this.markTransportDisconnected();
|
|
1640
|
+
} finally {
|
|
1641
|
+
runPromise?.reject(error);
|
|
1642
|
+
if (this.runPromise === runPromise) this.runPromise = null;
|
|
1643
|
+
await cleanupPromise?.catch(() => undefined);
|
|
1617
1644
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
this.runPromise.reject(error);
|
|
1624
|
-
this.runPromise = null;
|
|
1625
|
-
}
|
|
1626
|
-
await cleanupPromise?.catch(() => undefined);
|
|
1645
|
+
})();
|
|
1646
|
+
// Keep the settled promise for this attempt so a duplicate close/finally
|
|
1647
|
+
// cannot send a second wire Cancel. A new attempt gets its own cleanup.
|
|
1648
|
+
this.userInterruption = { attempt, promise };
|
|
1649
|
+
return promise;
|
|
1627
1650
|
}
|
|
1628
1651
|
|
|
1629
1652
|
setCancelableAction(callback: (err?: Error) => Promise<unknown>) {
|
|
1630
|
-
|
|
1653
|
+
const action = (e?: Error) =>
|
|
1631
1654
|
callback(e)
|
|
1632
1655
|
.catch(e2 => {
|
|
1633
1656
|
Log.debug('cancelableAction error', e2);
|
|
1634
1657
|
})
|
|
1635
1658
|
.finally(() => {
|
|
1636
|
-
this.clearCancelableAction();
|
|
1659
|
+
if (this.cancelableAction === action) this.clearCancelableAction();
|
|
1637
1660
|
});
|
|
1661
|
+
this.cancelableAction = action;
|
|
1638
1662
|
}
|
|
1639
1663
|
|
|
1640
1664
|
clearCancelableAction() {
|
|
@@ -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
|