@onekeyfe/hd-core 1.2.0-alpha.35 → 1.2.0-alpha.36
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/__tests__/AllNetworkGetAddressBase.tracing.test.ts +74 -0
- package/__tests__/DeviceEventRegistration.test.ts +6 -0
- package/__tests__/get-device-state.test.ts +104 -12
- package/__tests__/open-wallet-session.test.ts +131 -7
- package/__tests__/protocol-v2-ui-lifecycle.test.ts +56 -0
- package/__tests__/protocol-v2.test.ts +290 -39
- package/dist/api/FirmwareUpdateV4.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/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
- package/dist/core/deviceEventRegistration.d.ts +2 -0
- package/dist/core/deviceEventRegistration.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +22 -4
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/events/device.d.ts +4 -0
- package/dist/events/device.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +27 -2
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +56 -7
- package/dist/index.js +692 -397
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/uiInteraction.d.ts +3 -3
- package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts +4 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/package.json +4 -4
- package/src/api/BaseMethod.ts +1 -1
- package/src/api/FirmwareUpdateV4.ts +13 -6
- package/src/api/GetPassphraseState.ts +7 -1
- package/src/api/OpenWalletSession.ts +18 -3
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +24 -2
- package/src/api/device/DeviceUnlock.ts +1 -1
- package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
- package/src/core/deviceEventRegistration.ts +4 -0
- package/src/core/index.ts +46 -4
- package/src/data/messages/messages-protocol-v2.json +15 -0
- package/src/device/Device.ts +221 -24
- package/src/events/device.ts +4 -0
- package/src/events/ui-request.ts +32 -2
- package/src/protocols/protocol-v2/features.ts +9 -2
- package/src/protocols/protocol-v2/uiInteraction.ts +31 -5
- package/src/protocols/protocol-v2/unlockRetry.ts +33 -11
- package/src/protocols/protocol-v2/walletSession.ts +140 -32
- package/src/utils/deviceFeaturesUtils.ts +4 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
|
|
2
3
|
|
|
3
4
|
import { deviceWalletSessionStore } from '../device/DeviceWalletSessionStore';
|
|
4
5
|
import { getProtocolV2WalletSession } from '../protocols/protocol-v2/walletSession';
|
|
@@ -105,7 +106,12 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
|
|
|
105
106
|
};
|
|
106
107
|
const unlockProtocolV2IfLocked = async () => {
|
|
107
108
|
if (isProtocolV2 && state.status.unlocked === false) {
|
|
108
|
-
await this.device.unlockDevice(
|
|
109
|
+
await this.device.unlockDevice(DeviceSessionPinType.Main, {
|
|
110
|
+
source: 'unlock-coordinator',
|
|
111
|
+
reason: 'device-locked',
|
|
112
|
+
deviceOnly: true,
|
|
113
|
+
method: 'openWalletSession',
|
|
114
|
+
});
|
|
109
115
|
}
|
|
110
116
|
};
|
|
111
117
|
|
|
@@ -114,7 +120,12 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
|
|
|
114
120
|
if (this.params.mode === OpenWalletSessionMode.Standard) {
|
|
115
121
|
this.device.passphraseState = undefined;
|
|
116
122
|
const session = isProtocolV2
|
|
117
|
-
? await getProtocolV2WalletSession(this.device, {
|
|
123
|
+
? await getProtocolV2WalletSession(this.device, {
|
|
124
|
+
onlyMainPin: true,
|
|
125
|
+
deriveCardano: this.payload.deriveCardano,
|
|
126
|
+
selectMainWalletBeforeRestore:
|
|
127
|
+
state.status.unlocked === false || state.status.unlockedAttachPin === true,
|
|
128
|
+
})
|
|
118
129
|
: await getPassphraseStateWithRefreshDeviceInfo(this.device, {
|
|
119
130
|
onlyMainPin: true,
|
|
120
131
|
initSession: this.payload.initSession,
|
|
@@ -166,6 +177,7 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
|
|
|
166
177
|
const session = isProtocolV2
|
|
167
178
|
? await getProtocolV2WalletSession(this.device, {
|
|
168
179
|
expectedPassphraseState: this.params.passphraseState,
|
|
180
|
+
deriveCardano: this.payload.deriveCardano,
|
|
169
181
|
})
|
|
170
182
|
: await getPassphraseStateWithRefreshDeviceInfo(this.device, {
|
|
171
183
|
expectPassphraseState: this.params.passphraseState,
|
|
@@ -187,7 +199,10 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
|
|
|
187
199
|
this.device.passphraseState = undefined;
|
|
188
200
|
await unlockProtocolV2IfLocked();
|
|
189
201
|
const session = isProtocolV2
|
|
190
|
-
? await getProtocolV2WalletSession(this.device, {
|
|
202
|
+
? await getProtocolV2WalletSession(this.device, {
|
|
203
|
+
forceWalletSelection: true,
|
|
204
|
+
deriveCardano: this.payload.deriveCardano,
|
|
205
|
+
})
|
|
191
206
|
: await getPassphraseStateWithRefreshDeviceInfo(this.device, { initSession: true });
|
|
192
207
|
const deviceId = isProtocolV2 ? await refreshProtocolV2DeviceId() : requireDeviceId();
|
|
193
208
|
if (isProtocolV2 && this.device.getCurrentPassphraseProtection() !== true) {
|
|
@@ -14,6 +14,8 @@ import { findMethod } from '../utils';
|
|
|
14
14
|
import { DEVICE, IFRAME, createUiMessage } from '../../events';
|
|
15
15
|
import { UI_REQUEST } from '../../constants/ui-request';
|
|
16
16
|
import { onDeviceButtonHandler } from '../../core';
|
|
17
|
+
import { runMethodWithUnlockRetry } from '../../protocols/protocol-v2/unlockRetry';
|
|
18
|
+
import { ensureProtocolV2WalletSessionUnlocked } from '../../protocols/protocol-v2/walletSession';
|
|
17
19
|
import {
|
|
18
20
|
completeRequestContext,
|
|
19
21
|
createRequestContext,
|
|
@@ -384,7 +386,25 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
384
386
|
}
|
|
385
387
|
}
|
|
386
388
|
|
|
387
|
-
|
|
389
|
+
// Protocol V2 hands a wallet session to exactly one blockchain request.
|
|
390
|
+
// The parent all-network call consumes its first handoff while fetching
|
|
391
|
+
// the root fingerprint, so each nested chain method must resume the
|
|
392
|
+
// requested hidden wallet before it sends its own device command.
|
|
393
|
+
if (this.device.isProtocolV2() && this.payload.passphraseState) {
|
|
394
|
+
await ensureProtocolV2WalletSessionUnlocked(this.device);
|
|
395
|
+
const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
|
|
396
|
+
this.payload.passphraseState,
|
|
397
|
+
false,
|
|
398
|
+
this.payload.skipPassphraseCheck
|
|
399
|
+
);
|
|
400
|
+
if (!passphraseStateSafety) {
|
|
401
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const response = this.device.isProtocolV2()
|
|
406
|
+
? await runMethodWithUnlockRetry(method, this.device)
|
|
407
|
+
: await method.run();
|
|
388
408
|
|
|
389
409
|
if (!Array.isArray(response) || response.length === 0) {
|
|
390
410
|
throw new Error('No response');
|
|
@@ -439,7 +459,9 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
439
459
|
show_display: false,
|
|
440
460
|
});
|
|
441
461
|
|
|
442
|
-
this.
|
|
462
|
+
if (!this.device.isProtocolV2()) {
|
|
463
|
+
this.postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_PIN_WINDOW));
|
|
464
|
+
}
|
|
443
465
|
|
|
444
466
|
if (res.message.root_fingerprint == null) {
|
|
445
467
|
throw ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter);
|
|
@@ -13,7 +13,9 @@ export default class ProtocolInfoRequest extends BaseMethod {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
async run() {
|
|
16
|
-
const res = await this.device.commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
16
|
+
const res = await this.device.commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
17
|
+
eventless_wallet_session: true,
|
|
18
|
+
});
|
|
17
19
|
return Promise.resolve(res.message);
|
|
18
20
|
}
|
|
19
21
|
}
|
|
@@ -4,6 +4,8 @@ import type { Device, DeviceEvents } from '../device/Device';
|
|
|
4
4
|
|
|
5
5
|
export type HardwareUiEventHandlers = {
|
|
6
6
|
pin: (...event: DeviceEvents[typeof DEVICE.PIN]) => void;
|
|
7
|
+
pinOnDevice: (...event: DeviceEvents[typeof DEVICE.PIN_ON_DEVICE]) => void;
|
|
8
|
+
pinOnDeviceComplete: (...event: DeviceEvents[typeof DEVICE.PIN_ON_DEVICE_COMPLETE]) => void;
|
|
7
9
|
button: (...event: DeviceEvents[typeof DEVICE.BUTTON]) => void;
|
|
8
10
|
passphrase: (...event: DeviceEvents[typeof DEVICE.PASSPHRASE]) => void;
|
|
9
11
|
passphraseOnDevice: (...event: DeviceEvents[typeof DEVICE.PASSPHRASE_ON_DEVICE]) => void;
|
|
@@ -15,6 +17,8 @@ export function registerHardwareUiEventListeners(
|
|
|
15
17
|
handlers: HardwareUiEventHandlers
|
|
16
18
|
) {
|
|
17
19
|
device.on(DEVICE.PIN, handlers.pin);
|
|
20
|
+
device.on(DEVICE.PIN_ON_DEVICE, handlers.pinOnDevice);
|
|
21
|
+
device.on(DEVICE.PIN_ON_DEVICE_COMPLETE, handlers.pinOnDeviceComplete);
|
|
18
22
|
device.on(DEVICE.BUTTON, handlers.button);
|
|
19
23
|
device.on(DEVICE.PASSPHRASE, handlers.passphrase);
|
|
20
24
|
device.on(DEVICE.PASSPHRASE_ON_DEVICE, handlers.passphraseOnDevice);
|
package/src/core/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import semver from 'semver';
|
|
2
2
|
import EventEmitter from 'events';
|
|
3
|
-
import { TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
|
|
3
|
+
import { DeviceSessionPinType, TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
|
|
4
4
|
import {
|
|
5
5
|
ERRORS,
|
|
6
6
|
ERROR_CODES_REQUIRE_RELEASE,
|
|
@@ -56,6 +56,7 @@ import RequestQueue from './RequestQueue';
|
|
|
56
56
|
import { registerHardwareUiEventListeners } from './deviceEventRegistration';
|
|
57
57
|
import { getSynchronize } from '../utils/getSynchronize';
|
|
58
58
|
import { runMethodWithUnlockRetry } from '../protocols/protocol-v2/unlockRetry';
|
|
59
|
+
import { ensureProtocolV2WalletSessionUnlocked } from '../protocols/protocol-v2/walletSession';
|
|
59
60
|
import {
|
|
60
61
|
ProtocolV2UiInteractionCoordinator,
|
|
61
62
|
isProtocolV2UiEnabled,
|
|
@@ -406,6 +407,8 @@ const onCallDevice = async (
|
|
|
406
407
|
|
|
407
408
|
registerHardwareUiEventListeners(device, {
|
|
408
409
|
pin: onDevicePinHandler,
|
|
410
|
+
pinOnDevice: onEnterPinOnDeviceHandler,
|
|
411
|
+
pinOnDeviceComplete: onPinOnDeviceCompleteHandler,
|
|
409
412
|
button: onDeviceButtonHandler,
|
|
410
413
|
passphrase: message.payload.useEmptyPassphrase
|
|
411
414
|
? onEmptyPassphraseHandler
|
|
@@ -421,6 +424,7 @@ const onCallDevice = async (
|
|
|
421
424
|
);
|
|
422
425
|
|
|
423
426
|
const protocolV2UiCoordinator = new ProtocolV2UiInteractionCoordinator(device, postMessage);
|
|
427
|
+
device.beginProtocolV2UiInteraction();
|
|
424
428
|
device.on(
|
|
425
429
|
DEVICE.SELECT_DEVICE_FOR_SWITCH_FIRMWARE_WEB_DEVICE,
|
|
426
430
|
onSelectDeviceForSwitchFirmwareWebDeviceHandler
|
|
@@ -587,11 +591,15 @@ const onCallDevice = async (
|
|
|
587
591
|
);
|
|
588
592
|
}
|
|
589
593
|
|
|
594
|
+
// Wallet-session recovery is part of the protected business call, not onboarding.
|
|
595
|
+
await ensureProtocolV2WalletSessionUnlocked(device);
|
|
596
|
+
|
|
590
597
|
// Check Device passphrase State
|
|
591
598
|
const passphraseStateSafety = await device.checkPassphraseStateSafety(
|
|
592
599
|
method.payload?.passphraseState,
|
|
593
600
|
method.payload?.useEmptyPassphrase,
|
|
594
|
-
method.payload?.skipPassphraseCheck
|
|
601
|
+
method.payload?.skipPassphraseCheck,
|
|
602
|
+
hasDeriveCardano(method)
|
|
595
603
|
);
|
|
596
604
|
|
|
597
605
|
// Double check, handles the special case of Touch/Pro
|
|
@@ -604,8 +612,11 @@ const onCallDevice = async (
|
|
|
604
612
|
);
|
|
605
613
|
}
|
|
606
614
|
|
|
607
|
-
//
|
|
608
|
-
|
|
615
|
+
// Protocol V2 emits the PIN phase completion from DeviceSessionAskPin.
|
|
616
|
+
// Keep the legacy compatibility close for Protocol V1 only.
|
|
617
|
+
if (!device.isProtocolV2()) {
|
|
618
|
+
postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_PIN_WINDOW));
|
|
619
|
+
}
|
|
609
620
|
}
|
|
610
621
|
|
|
611
622
|
// Automatic check safety_check level for Kovan, Ropsten, Rinkeby, Goerli test networks.
|
|
@@ -1279,6 +1290,7 @@ const onDevicePassphraseHandler = async (
|
|
|
1279
1290
|
source: requestPayload.source,
|
|
1280
1291
|
reason: requestPayload.reason,
|
|
1281
1292
|
expectedPassphraseState: requestPayload.expectedPassphraseState,
|
|
1293
|
+
...(requestPayload.interaction ? { interaction: requestPayload.interaction } : {}),
|
|
1282
1294
|
})
|
|
1283
1295
|
);
|
|
1284
1296
|
// wait for passphrase
|
|
@@ -1308,6 +1320,7 @@ const onEnterPassphraseOnDeviceHandler = (
|
|
|
1308
1320
|
passphraseState: device.passphraseState,
|
|
1309
1321
|
source: requestPayload?.source,
|
|
1310
1322
|
reason: requestPayload?.reason,
|
|
1323
|
+
...(requestPayload?.interaction ? { interaction: requestPayload.interaction } : {}),
|
|
1311
1324
|
})
|
|
1312
1325
|
);
|
|
1313
1326
|
};
|
|
@@ -1321,10 +1334,39 @@ const onEnterAttachPinOnDeviceHandler = (
|
|
|
1321
1334
|
type: 'ButtonRequest_AttachPin',
|
|
1322
1335
|
source: requestPayload?.source,
|
|
1323
1336
|
reason: requestPayload?.reason,
|
|
1337
|
+
...(requestPayload?.interaction ? { interaction: requestPayload.interaction } : {}),
|
|
1324
1338
|
})
|
|
1325
1339
|
);
|
|
1326
1340
|
};
|
|
1327
1341
|
|
|
1342
|
+
const onEnterPinOnDeviceHandler = (
|
|
1343
|
+
...[device, pinType, metadata]: [...DeviceEvents['pin_on_device']]
|
|
1344
|
+
) => {
|
|
1345
|
+
postMessage(
|
|
1346
|
+
createUiMessage(UI_REQUEST.REQUEST_PIN, {
|
|
1347
|
+
device: device.toMessageObject() as KnownDevice,
|
|
1348
|
+
type:
|
|
1349
|
+
pinType === DeviceSessionPinType.AttachToPin
|
|
1350
|
+
? 'ButtonRequest_AttachPin'
|
|
1351
|
+
: 'ButtonRequest_PinEntry',
|
|
1352
|
+
source: metadata?.source,
|
|
1353
|
+
reason: metadata?.reason,
|
|
1354
|
+
deviceOnly: metadata?.deviceOnly ?? true,
|
|
1355
|
+
completion: metadata?.completion,
|
|
1356
|
+
method: metadata?.method,
|
|
1357
|
+
page: metadata?.page,
|
|
1358
|
+
operation: metadata?.operation,
|
|
1359
|
+
...(metadata?.interaction ? { interaction: metadata.interaction } : {}),
|
|
1360
|
+
})
|
|
1361
|
+
);
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
const onPinOnDeviceCompleteHandler = (
|
|
1365
|
+
...[_, metadata]: [...DeviceEvents['pin_on_device_complete']]
|
|
1366
|
+
) => {
|
|
1367
|
+
postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_PIN_WINDOW, metadata));
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1328
1370
|
const onSelectDeviceInBootloaderForWebDeviceHandler = async (
|
|
1329
1371
|
...[device, callback]: [...DeviceEvents['select_device_in_bootloader_for_web_device']]
|
|
1330
1372
|
) => {
|
|
@@ -12200,11 +12200,26 @@
|
|
|
12200
12200
|
"DeviceSessionError_Busy": 5
|
|
12201
12201
|
}
|
|
12202
12202
|
},
|
|
12203
|
+
"DeviceSessionSeedDomain": {
|
|
12204
|
+
"values": {
|
|
12205
|
+
"SeedDomain_Standard": 1,
|
|
12206
|
+
"SeedDomain_Cardano": 2
|
|
12207
|
+
}
|
|
12208
|
+
},
|
|
12203
12209
|
"DeviceSessionGet": {
|
|
12204
12210
|
"fields": {
|
|
12205
12211
|
"session_id": {
|
|
12206
12212
|
"type": "bytes",
|
|
12207
12213
|
"id": 1
|
|
12214
|
+
},
|
|
12215
|
+
"btc_test_address": {
|
|
12216
|
+
"type": "string",
|
|
12217
|
+
"id": 2
|
|
12218
|
+
},
|
|
12219
|
+
"seed_domains": {
|
|
12220
|
+
"rule": "repeated",
|
|
12221
|
+
"type": "DeviceSessionSeedDomain",
|
|
12222
|
+
"id": 3
|
|
12208
12223
|
}
|
|
12209
12224
|
}
|
|
12210
12225
|
},
|
package/src/device/Device.ts
CHANGED
|
@@ -74,7 +74,9 @@ import type { DeviceStateReadOptions } from '../types/api/getDeviceState';
|
|
|
74
74
|
import type {
|
|
75
75
|
DeviceButtonRequestPayload,
|
|
76
76
|
DeviceFeaturesPayload,
|
|
77
|
+
HardwareUiInteractionMeta,
|
|
77
78
|
PassphraseRequestPayload,
|
|
79
|
+
ProtocolV2UiEventMetadata,
|
|
78
80
|
} from '../events';
|
|
79
81
|
import type { PassphrasePromptResponse } from './DeviceCommands';
|
|
80
82
|
import type { Deferred, HardwareConnectProtocol } from '@onekeyfe/hd-shared';
|
|
@@ -110,8 +112,30 @@ const parseRunOptions = (options?: RunOptions): RunOptions => {
|
|
|
110
112
|
|
|
111
113
|
const Log = getLogger(LoggerNames.Device);
|
|
112
114
|
|
|
115
|
+
const isProtocolV2DeviceStatusUnsupportedError = (error: unknown) => {
|
|
116
|
+
if (error instanceof HardwareError) {
|
|
117
|
+
if (error.errorCode === HardwareErrorCode.DeviceNotSupportMethod) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (error.params?.failureCode === 'Failure_UnexpectedMessage') {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const message =
|
|
126
|
+
error instanceof Error
|
|
127
|
+
? error.message
|
|
128
|
+
: String((error as { message?: unknown } | null)?.message ?? error ?? '');
|
|
129
|
+
return (
|
|
130
|
+
/^Failure_UnexpectedMessage(?:,|\b)/i.test(message) ||
|
|
131
|
+
/\b(?:unsupported message|handler not registered|message handler not found)\b/i.test(message)
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
113
135
|
export interface DeviceEvents {
|
|
114
136
|
[DEVICE.PIN]: [Device, PROTO.PinMatrixRequestType | undefined, (err: any, pin: string) => void];
|
|
137
|
+
[DEVICE.PIN_ON_DEVICE]: [Device, DeviceSessionPinType, ProtocolV2UiEventMetadata?];
|
|
138
|
+
[DEVICE.PIN_ON_DEVICE_COMPLETE]: [Device, HardwareUiInteractionMeta];
|
|
115
139
|
[DEVICE.PASSPHRASE_ON_DEVICE]: [Device, PassphraseRequestPayload?];
|
|
116
140
|
[DEVICE.ATTACH_PIN_ON_DEVICE]: [Device, PassphraseRequestPayload?];
|
|
117
141
|
[DEVICE.BUTTON]: [Device, DeviceButtonRequestPayload];
|
|
@@ -210,6 +234,24 @@ export class Device extends EventEmitter {
|
|
|
210
234
|
/** Force the next initialization to reload DeviceInfo after reconnect or reboot. */
|
|
211
235
|
private protocolV2StateNeedsReload = false;
|
|
212
236
|
|
|
237
|
+
/** Runtime context negotiated once per active Protocol V2 link. */
|
|
238
|
+
private protocolV2RuntimeContext?: ProtocolInfo;
|
|
239
|
+
|
|
240
|
+
/** Coalesces concurrent first-use runtime negotiation on the same active link. */
|
|
241
|
+
private protocolV2RuntimeContextPromise?: Promise<ProtocolInfo>;
|
|
242
|
+
|
|
243
|
+
/** Invalidates an in-flight runtime-context response without adding a transport epoch. */
|
|
244
|
+
private protocolV2RuntimeContextRequestToken?: object;
|
|
245
|
+
|
|
246
|
+
private protocolV2UiInteraction?: {
|
|
247
|
+
interactionId: string;
|
|
248
|
+
phaseCounter: number;
|
|
249
|
+
sequence: number;
|
|
250
|
+
opened: boolean;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
private protocolV2UiInteractionCounter = 0;
|
|
254
|
+
|
|
213
255
|
get state() {
|
|
214
256
|
return this.stateStore.getState();
|
|
215
257
|
}
|
|
@@ -967,13 +1009,7 @@ export class Device extends EventEmitter {
|
|
|
967
1009
|
}
|
|
968
1010
|
|
|
969
1011
|
if (refresh.has('status') && !initializedWithDeviceInfo) {
|
|
970
|
-
|
|
971
|
-
refreshedDeviceInfo ??
|
|
972
|
-
(await requestProtocolV2DeviceInfo({
|
|
973
|
-
commands: this.commands,
|
|
974
|
-
request: getProtocolV2DeviceInfoRequest(),
|
|
975
|
-
}));
|
|
976
|
-
await this.probeProtocolV2RuntimeState(deviceInfo);
|
|
1012
|
+
await this.probeProtocolV2RuntimeState(refreshedDeviceInfo);
|
|
977
1013
|
}
|
|
978
1014
|
|
|
979
1015
|
if (refresh.has('settings') && this.state?.status.mode === 'normal') {
|
|
@@ -1044,15 +1080,58 @@ export class Device extends EventEmitter {
|
|
|
1044
1080
|
return this.features;
|
|
1045
1081
|
}
|
|
1046
1082
|
|
|
1047
|
-
async
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1083
|
+
async ensureProtocolV2RuntimeContext(timeoutMs?: number): Promise<ProtocolInfo> {
|
|
1084
|
+
const cachedProtocolInfo =
|
|
1085
|
+
this.protocolV2RuntimeContext ??
|
|
1086
|
+
(!this.protocolV2StateNeedsReload
|
|
1087
|
+
? this.state?.raw?.protocolV2ProtocolInfo ?? undefined
|
|
1088
|
+
: undefined);
|
|
1089
|
+
if (cachedProtocolInfo) {
|
|
1090
|
+
this.protocolV2RuntimeContext = cachedProtocolInfo;
|
|
1091
|
+
return cachedProtocolInfo;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (this.protocolV2RuntimeContextPromise) {
|
|
1095
|
+
return this.protocolV2RuntimeContextPromise;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const requestToken = {};
|
|
1099
|
+
const pendingRequest = (async () => {
|
|
1100
|
+
const protocolInfo = await requestProtocolV2ProtocolInfo({
|
|
1101
|
+
commands: this.commands,
|
|
1102
|
+
timeoutMs,
|
|
1103
|
+
});
|
|
1104
|
+
if (this.protocolV2RuntimeContextRequestToken !== requestToken) {
|
|
1105
|
+
throw ERRORS.TypedError(
|
|
1106
|
+
HardwareErrorCode.DeviceInitializeFailed,
|
|
1107
|
+
'Protocol V2 runtime context was invalidated while loading.'
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
this.protocolV2RuntimeContext = protocolInfo;
|
|
1111
|
+
return protocolInfo;
|
|
1112
|
+
})();
|
|
1113
|
+
this.protocolV2RuntimeContextRequestToken = requestToken;
|
|
1114
|
+
this.protocolV2RuntimeContextPromise = pendingRequest;
|
|
1115
|
+
|
|
1116
|
+
try {
|
|
1117
|
+
return await pendingRequest;
|
|
1118
|
+
} finally {
|
|
1119
|
+
if (this.protocolV2RuntimeContextPromise === pendingRequest) {
|
|
1120
|
+
this.protocolV2RuntimeContextPromise = undefined;
|
|
1121
|
+
}
|
|
1122
|
+
if (this.protocolV2RuntimeContextRequestToken === requestToken) {
|
|
1123
|
+
this.protocolV2RuntimeContextRequestToken = undefined;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
async probeProtocolV2RuntimeState(deviceInfo?: ProtocolV2DeviceInfo, timeoutMs?: number) {
|
|
1129
|
+
const protocolInfo = await this.ensureProtocolV2RuntimeContext(timeoutMs);
|
|
1052
1130
|
const runtimeMode = getProtocolV2RuntimeMode(protocolInfo);
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1131
|
+
const runtimeDeviceInfo = deviceInfo ?? this.state?.raw?.protocolV2DeviceInfo;
|
|
1132
|
+
const protocolV2DeviceType = runtimeDeviceInfo
|
|
1133
|
+
? resolveProtocolV2DeviceIdentity(runtimeDeviceInfo.hw?.Device_type).deviceType
|
|
1134
|
+
: this.getCurrentDeviceType();
|
|
1056
1135
|
if (runtimeMode === 'romloader' && protocolV2DeviceType !== EDeviceType.Pro2) {
|
|
1057
1136
|
throw ERRORS.TypedError(
|
|
1058
1137
|
HardwareErrorCode.DeviceInitializeFailed,
|
|
@@ -1078,10 +1157,18 @@ export class Device extends EventEmitter {
|
|
|
1078
1157
|
);
|
|
1079
1158
|
}
|
|
1080
1159
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1160
|
+
let deviceStatus: DeviceStatus;
|
|
1161
|
+
try {
|
|
1162
|
+
deviceStatus = await requestProtocolV2DeviceStatus({
|
|
1163
|
+
commands: this.commands,
|
|
1164
|
+
timeoutMs,
|
|
1165
|
+
});
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
if (runtimeMode === undefined && isProtocolV2DeviceStatusUnsupportedError(error)) {
|
|
1168
|
+
return this.updateProtocolV2Features(deviceInfo, null, 'bootloader', protocolInfo);
|
|
1169
|
+
}
|
|
1170
|
+
throw error;
|
|
1171
|
+
}
|
|
1085
1172
|
return this.updateProtocolV2Features(deviceInfo, deviceStatus, 'normal', protocolInfo);
|
|
1086
1173
|
}
|
|
1087
1174
|
|
|
@@ -1119,6 +1206,9 @@ export class Device extends EventEmitter {
|
|
|
1119
1206
|
this.deviceAcquired = false;
|
|
1120
1207
|
if (!this.isProtocolV2()) return;
|
|
1121
1208
|
this.protocolV2StateNeedsReload = true;
|
|
1209
|
+
this.protocolV2RuntimeContext = undefined;
|
|
1210
|
+
this.protocolV2RuntimeContextPromise = undefined;
|
|
1211
|
+
this.protocolV2RuntimeContextRequestToken = undefined;
|
|
1122
1212
|
this.clearPreInitialized();
|
|
1123
1213
|
}
|
|
1124
1214
|
|
|
@@ -1127,9 +1217,14 @@ export class Device extends EventEmitter {
|
|
|
1127
1217
|
if (deviceId) {
|
|
1128
1218
|
deviceWalletSessionStore.deleteDevice(deviceId);
|
|
1129
1219
|
}
|
|
1130
|
-
if (this.isProtocolV2()
|
|
1131
|
-
|
|
1220
|
+
if (this.isProtocolV2()) {
|
|
1221
|
+
if (this.originalDescriptor.path !== deviceId) {
|
|
1222
|
+
deviceWalletSessionStore.deleteDevice(this.originalDescriptor.path);
|
|
1223
|
+
}
|
|
1132
1224
|
this.protocolV2StateNeedsReload = true;
|
|
1225
|
+
this.protocolV2RuntimeContext = undefined;
|
|
1226
|
+
this.protocolV2RuntimeContextPromise = undefined;
|
|
1227
|
+
this.protocolV2RuntimeContextRequestToken = undefined;
|
|
1133
1228
|
}
|
|
1134
1229
|
|
|
1135
1230
|
this.passphraseState = undefined;
|
|
@@ -1142,6 +1237,9 @@ export class Device extends EventEmitter {
|
|
|
1142
1237
|
if (!this.isProtocolV2()) return;
|
|
1143
1238
|
|
|
1144
1239
|
this.protocolV2StateNeedsReload = true;
|
|
1240
|
+
this.protocolV2RuntimeContext = undefined;
|
|
1241
|
+
this.protocolV2RuntimeContextPromise = undefined;
|
|
1242
|
+
this.protocolV2RuntimeContextRequestToken = undefined;
|
|
1145
1243
|
this.clearPreInitialized();
|
|
1146
1244
|
let loaderMode: 'bootloader' | 'romloader' | undefined;
|
|
1147
1245
|
if (rebootType === DeviceRebootType.Bootloader) {
|
|
@@ -1494,6 +1592,75 @@ export class Device extends EventEmitter {
|
|
|
1494
1592
|
return res.message;
|
|
1495
1593
|
}
|
|
1496
1594
|
|
|
1595
|
+
beginProtocolV2UiInteraction() {
|
|
1596
|
+
if (!this.isProtocolV2()) return;
|
|
1597
|
+
this.protocolV2UiInteraction = {
|
|
1598
|
+
interactionId: `${this.instanceId}:${Date.now()}:${++this.protocolV2UiInteractionCounter}`,
|
|
1599
|
+
phaseCounter: 0,
|
|
1600
|
+
sequence: 0,
|
|
1601
|
+
opened: false,
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
createProtocolV2UiPhaseMetadata(
|
|
1606
|
+
phase: HardwareUiInteractionMeta['phase'],
|
|
1607
|
+
transition: HardwareUiInteractionMeta['transition'],
|
|
1608
|
+
options?: {
|
|
1609
|
+
phaseId?: string;
|
|
1610
|
+
outcome?: HardwareUiInteractionMeta['outcome'];
|
|
1611
|
+
}
|
|
1612
|
+
): HardwareUiInteractionMeta | undefined {
|
|
1613
|
+
if (!this.isProtocolV2()) return undefined;
|
|
1614
|
+
if (!this.protocolV2UiInteraction) this.beginProtocolV2UiInteraction();
|
|
1615
|
+
|
|
1616
|
+
const interaction = this.protocolV2UiInteraction;
|
|
1617
|
+
if (!interaction) return undefined;
|
|
1618
|
+
const phaseId =
|
|
1619
|
+
options?.phaseId ?? `${interaction.interactionId}:phase-${++interaction.phaseCounter}`;
|
|
1620
|
+
interaction.opened = true;
|
|
1621
|
+
interaction.sequence += 1;
|
|
1622
|
+
|
|
1623
|
+
return {
|
|
1624
|
+
interactionId: interaction.interactionId,
|
|
1625
|
+
phaseId,
|
|
1626
|
+
sequence: interaction.sequence,
|
|
1627
|
+
phase,
|
|
1628
|
+
transition,
|
|
1629
|
+
...(options?.outcome ? { outcome: options.outcome } : {}),
|
|
1630
|
+
protocol: 'V2',
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
completeProtocolV2UiPhase(
|
|
1635
|
+
phase: HardwareUiInteractionMeta,
|
|
1636
|
+
outcome: HardwareUiInteractionMeta['outcome'] = 'succeeded'
|
|
1637
|
+
) {
|
|
1638
|
+
return this.createProtocolV2UiPhaseMetadata(phase.phase, 'complete', {
|
|
1639
|
+
phaseId: phase.phaseId,
|
|
1640
|
+
outcome,
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
finishProtocolV2UiInteraction(outcome: HardwareUiInteractionMeta['outcome'] = 'succeeded') {
|
|
1645
|
+
const interaction = this.protocolV2UiInteraction;
|
|
1646
|
+
if (!interaction?.opened) {
|
|
1647
|
+
this.protocolV2UiInteraction = undefined;
|
|
1648
|
+
return undefined;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const phaseId = `${interaction.interactionId}:phase-${Math.max(interaction.phaseCounter, 1)}`;
|
|
1652
|
+
const metadata = this.createProtocolV2UiPhaseMetadata('processing', 'finish', {
|
|
1653
|
+
phaseId,
|
|
1654
|
+
outcome,
|
|
1655
|
+
});
|
|
1656
|
+
this.protocolV2UiInteraction = undefined;
|
|
1657
|
+
return metadata;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
hasOpenProtocolV2UiInteraction() {
|
|
1661
|
+
return this.protocolV2UiInteraction?.opened === true;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1497
1664
|
supportUnlockVersionRange(): DeviceFirmwareRange {
|
|
1498
1665
|
// This range applies to Protocol V1 Pro devices; Pro2 has a dedicated unlock flow.
|
|
1499
1666
|
return {
|
|
@@ -1503,10 +1670,33 @@ export class Device extends EventEmitter {
|
|
|
1503
1670
|
};
|
|
1504
1671
|
}
|
|
1505
1672
|
|
|
1506
|
-
async unlockDevice(
|
|
1673
|
+
async unlockDevice(
|
|
1674
|
+
pinType?: DeviceSessionPinType,
|
|
1675
|
+
options?: ProtocolV2UiEventMetadata & { emitUiEvent?: boolean }
|
|
1676
|
+
) {
|
|
1507
1677
|
if (this.isProtocolV2()) {
|
|
1678
|
+
const requestedPinType = pinType ?? DeviceSessionPinType.Main;
|
|
1679
|
+
const interaction =
|
|
1680
|
+
options?.interaction ??
|
|
1681
|
+
(options?.emitUiEvent === false
|
|
1682
|
+
? undefined
|
|
1683
|
+
: this.createProtocolV2UiPhaseMetadata('pin', 'start'));
|
|
1684
|
+
if (options?.emitUiEvent !== false) {
|
|
1685
|
+
this.emit(DEVICE.PIN_ON_DEVICE, this, requestedPinType, {
|
|
1686
|
+
source: options?.source ?? 'unlock-coordinator',
|
|
1687
|
+
reason: options?.reason ?? 'device-unlock',
|
|
1688
|
+
deviceOnly: options?.deviceOnly ?? true,
|
|
1689
|
+
completion: options?.completion,
|
|
1690
|
+
method: options?.method,
|
|
1691
|
+
page: options?.page,
|
|
1692
|
+
operation: options?.operation,
|
|
1693
|
+
interaction: options?.interaction ?? interaction,
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1508
1696
|
try {
|
|
1509
|
-
await this.commands.typedCall('DeviceSessionAskPin', 'Success', {
|
|
1697
|
+
await this.commands.typedCall('DeviceSessionAskPin', 'Success', {
|
|
1698
|
+
type: requestedPinType,
|
|
1699
|
+
});
|
|
1510
1700
|
} catch (error) {
|
|
1511
1701
|
const errorText =
|
|
1512
1702
|
error instanceof Error
|
|
@@ -1518,6 +1708,11 @@ export class Device extends EventEmitter {
|
|
|
1518
1708
|
throw error;
|
|
1519
1709
|
}
|
|
1520
1710
|
|
|
1711
|
+
const completion = interaction ? this.completeProtocolV2UiPhase(interaction) : undefined;
|
|
1712
|
+
if (completion) {
|
|
1713
|
+
this.emit(DEVICE.PIN_ON_DEVICE_COMPLETE, this, completion);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1521
1716
|
const status = await requestProtocolV2DeviceStatus({ commands: this.commands });
|
|
1522
1717
|
return this.updateProtocolV2Status(status);
|
|
1523
1718
|
}
|
|
@@ -1579,7 +1774,8 @@ export class Device extends EventEmitter {
|
|
|
1579
1774
|
async checkPassphraseStateSafety(
|
|
1580
1775
|
passphraseState?: string,
|
|
1581
1776
|
useEmptyPassphrase?: boolean,
|
|
1582
|
-
skipPassphraseCheck?: boolean
|
|
1777
|
+
skipPassphraseCheck?: boolean,
|
|
1778
|
+
deriveCardano?: boolean
|
|
1583
1779
|
) {
|
|
1584
1780
|
if (this.isUnacquired()) return false;
|
|
1585
1781
|
|
|
@@ -1588,6 +1784,7 @@ export class Device extends EventEmitter {
|
|
|
1588
1784
|
await getPassphraseStateWithRefreshDeviceInfo(this, {
|
|
1589
1785
|
expectPassphraseState: expectedPassphraseState,
|
|
1590
1786
|
onlyMainPin: useEmptyPassphrase,
|
|
1787
|
+
deriveCardano,
|
|
1591
1788
|
});
|
|
1592
1789
|
|
|
1593
1790
|
// Main wallet and unlock Attach Pin, throw safe error
|
package/src/events/device.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MessageFactoryFn } from './utils';
|
|
2
|
+
import type { HardwareUiInteractionMeta } from './ui-request';
|
|
2
3
|
import type { PROTO } from '../constants';
|
|
3
4
|
import type {
|
|
4
5
|
KnownDevice as Device,
|
|
@@ -26,6 +27,8 @@ export const DEVICE = {
|
|
|
26
27
|
// onekey-transport events in protobuf format
|
|
27
28
|
BUTTON: 'button',
|
|
28
29
|
PIN: 'pin',
|
|
30
|
+
PIN_ON_DEVICE: 'pin_on_device',
|
|
31
|
+
PIN_ON_DEVICE_COMPLETE: 'pin_on_device_complete',
|
|
29
32
|
PASSPHRASE: 'passphrase',
|
|
30
33
|
PASSPHRASE_ON_DEVICE: 'passphrase_on_device',
|
|
31
34
|
ATTACH_PIN_ON_DEVICE: 'attach_pin_on_device',
|
|
@@ -58,6 +61,7 @@ export type PassphraseRequestPayload = {
|
|
|
58
61
|
source?: 'wallet-session-coordinator';
|
|
59
62
|
reason?: 'open-wallet' | 'session-recovery';
|
|
60
63
|
expectedPassphraseState?: string;
|
|
64
|
+
interaction?: HardwareUiInteractionMeta;
|
|
61
65
|
};
|
|
62
66
|
|
|
63
67
|
export interface DeviceButtonRequest {
|