@onekeyfe/hd-core 1.2.3-alpha.1 → 1.2.3-alpha.11
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/api/SearchDevices.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
|
|
1
|
+
import { EDeviceType, canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import { BaseMethod } from './BaseMethod';
|
|
4
4
|
import TransportManager from '../data-manager/TransportManager';
|
|
@@ -6,10 +6,80 @@ import { DataManager } from '../data-manager';
|
|
|
6
6
|
import { LoggerNames, getDeviceTypeByBleName, getLogger } from '../utils';
|
|
7
7
|
import { DevicePool } from '../device/DevicePool';
|
|
8
8
|
|
|
9
|
+
import type { SearchDevice } from '../types/device';
|
|
9
10
|
import type DeviceConnector from '../device/DeviceConnector';
|
|
11
|
+
import type { OneKeyDeviceInfo as DeviceDescriptor } from '@onekeyfe/hd-transport';
|
|
12
|
+
import type { Device } from '../device/Device';
|
|
10
13
|
|
|
11
14
|
const Log = getLogger(LoggerNames.DevicePool);
|
|
12
15
|
|
|
16
|
+
type RequestQueueLookup = {
|
|
17
|
+
getRequestTasksIdByConnectId: (connectId: string) => number[];
|
|
18
|
+
getTask: (requestId: number) => { method: { device?: Device } } | undefined;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getDescriptorKeys = (descriptor: DeviceDescriptor) =>
|
|
22
|
+
[descriptor.path, descriptor.id].filter(
|
|
23
|
+
(key): key is string => typeof key === 'string' && key.length > 0
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const isOwnedByActiveWebUsbRequest = (
|
|
27
|
+
descriptor: DeviceDescriptor,
|
|
28
|
+
requestQueue?: RequestQueueLookup
|
|
29
|
+
) => {
|
|
30
|
+
if (!requestQueue) return false;
|
|
31
|
+
return getDescriptorKeys(descriptor).some(
|
|
32
|
+
key => requestQueue.getRequestTasksIdByConnectId(key).length > 0
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getUsbHandle = (descriptor?: DeviceDescriptor) =>
|
|
37
|
+
(descriptor as { device?: unknown } | undefined)?.device;
|
|
38
|
+
|
|
39
|
+
const getOwningRequestDevice = (
|
|
40
|
+
descriptor: DeviceDescriptor,
|
|
41
|
+
requestQueue?: RequestQueueLookup
|
|
42
|
+
) => {
|
|
43
|
+
if (!requestQueue) return undefined;
|
|
44
|
+
const usbHandle = getUsbHandle(descriptor);
|
|
45
|
+
// A path alone can be shared (the all-zero bootloader path, a synthesized serial-less
|
|
46
|
+
// path), so the owner's identity is only reported for the USB handle it actually bound.
|
|
47
|
+
if (usbHandle === undefined) return undefined;
|
|
48
|
+
for (const key of getDescriptorKeys(descriptor)) {
|
|
49
|
+
for (const requestId of requestQueue.getRequestTasksIdByConnectId(key)) {
|
|
50
|
+
const device = requestQueue.getTask(requestId)?.method.device;
|
|
51
|
+
if (
|
|
52
|
+
device?.features &&
|
|
53
|
+
(device.mainId === key || device.originalDescriptor?.path === key) &&
|
|
54
|
+
getUsbHandle(device.originalDescriptor) === usbHandle
|
|
55
|
+
) {
|
|
56
|
+
return device;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// A USB path is a routing key (the USB serial, a synthesized usb-vid-pid-name, or the
|
|
64
|
+
// bootloader placeholder), not a hardware identity, so it never fills uuid/serialNo.
|
|
65
|
+
const toSearchDeviceFromDescriptor = (descriptor: DeviceDescriptor): SearchDevice => {
|
|
66
|
+
const connectId = descriptor.path || descriptor.id || null;
|
|
67
|
+
return {
|
|
68
|
+
connectId,
|
|
69
|
+
uuid: '',
|
|
70
|
+
serialNo: null,
|
|
71
|
+
deviceId: null,
|
|
72
|
+
deviceType: EDeviceType.Unknown,
|
|
73
|
+
name: descriptor.name || connectId || '',
|
|
74
|
+
commType: descriptor.commType,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const toSearchDevice = (device: Device): SearchDevice | null => {
|
|
79
|
+
const message = device.toMessageObject();
|
|
80
|
+
return message ? (message as SearchDevice) : null;
|
|
81
|
+
};
|
|
82
|
+
|
|
13
83
|
export default class SearchDevices extends BaseMethod {
|
|
14
84
|
connector?: DeviceConnector;
|
|
15
85
|
|
|
@@ -19,19 +89,31 @@ export default class SearchDevices extends BaseMethod {
|
|
|
19
89
|
this.skipForceUpdateCheck = true;
|
|
20
90
|
}
|
|
21
91
|
|
|
22
|
-
async run() {
|
|
23
|
-
|
|
92
|
+
async run(): Promise<SearchDevice[]> {
|
|
93
|
+
const env = DataManager.getSettings('env');
|
|
94
|
+
const isWebUsb = env === 'webusb' || env === 'desktop-webusb';
|
|
95
|
+
const requestQueue = this.context?.requestQueue;
|
|
96
|
+
const hasActiveWebUsbRequest = isWebUsb && (requestQueue?.getRequestTasksId().length ?? 0) > 0;
|
|
97
|
+
// Bring up WebUSB even when schema configuration is deferred while a
|
|
98
|
+
// business request owns the discovery lock. Failures here used to be
|
|
99
|
+
// swallowed by configure(); keep searchDevices resolving empty.
|
|
100
|
+
if (isWebUsb) {
|
|
101
|
+
try {
|
|
102
|
+
await TransportManager.ensureInitialized();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
Log.debug('WebUSB bring-up unavailable', error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!hasActiveWebUsbRequest) await TransportManager.configure();
|
|
24
108
|
const deviceDiff = await this.connector?.enumerate();
|
|
25
109
|
const devicesDescriptor = deviceDiff?.descriptors ?? [];
|
|
26
110
|
|
|
27
|
-
const env = DataManager.getSettings('env');
|
|
28
|
-
|
|
29
111
|
/**
|
|
30
112
|
* No need to call features during Bluetooth scaning
|
|
31
113
|
* to avoid device pairing
|
|
32
114
|
*/
|
|
33
115
|
if (DataManager.isBleConnect(env)) {
|
|
34
|
-
const devices = [];
|
|
116
|
+
const devices: SearchDevice[] = [];
|
|
35
117
|
const seenIds = new Set<string>();
|
|
36
118
|
|
|
37
119
|
for (const device of devicesDescriptor) {
|
|
@@ -56,30 +138,42 @@ export default class SearchDevices extends BaseMethod {
|
|
|
56
138
|
return devices;
|
|
57
139
|
}
|
|
58
140
|
|
|
59
|
-
const deviceList = [];
|
|
141
|
+
const deviceList: SearchDevice[] = [];
|
|
60
142
|
for (const descriptor of devicesDescriptor) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
143
|
+
if (hasActiveWebUsbRequest && isOwnedByActiveWebUsbRequest(descriptor, requestQueue)) {
|
|
144
|
+
const cached = DevicePool.getDeviceByPath(descriptor.path);
|
|
145
|
+
const known = cached?.features ? cached : getOwningRequestDevice(descriptor, requestQueue);
|
|
146
|
+
const message = known ? toSearchDevice(known) : null;
|
|
147
|
+
// Do not probe a path an in-flight request already owns. A cache miss is still a
|
|
148
|
+
// connected device: report the owning request's device, or the path with no identity.
|
|
149
|
+
deviceList.push(message ?? toSearchDeviceFromDescriptor(descriptor));
|
|
150
|
+
} else {
|
|
151
|
+
try {
|
|
152
|
+
// Discovery is best effort. Browsers may retain WebUSB grants for devices that
|
|
153
|
+
// are offline, busy, or not ready, so one descriptor must not abort the scan.
|
|
154
|
+
const result = await DevicePool.getDevices([descriptor], descriptor.path, {
|
|
155
|
+
// Discovery must actively identify the protocol instead of trusting a caller hint.
|
|
156
|
+
connectProtocol: undefined,
|
|
157
|
+
forceProtocolDetection: true,
|
|
158
|
+
refreshRuntimeState: true,
|
|
159
|
+
});
|
|
160
|
+
for (const device of result.deviceList) {
|
|
161
|
+
const message = toSearchDevice(device);
|
|
162
|
+
if (message) deviceList.push(message);
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const errorCode =
|
|
166
|
+
error && typeof error === 'object' && 'errorCode' in error
|
|
167
|
+
? (error as { errorCode?: unknown }).errorCode
|
|
168
|
+
: undefined;
|
|
169
|
+
Log.debug('Skip unavailable device during search', {
|
|
170
|
+
path: descriptor.path,
|
|
171
|
+
...(errorCode !== undefined ? { errorCode } : {}),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
80
174
|
}
|
|
81
175
|
}
|
|
82
176
|
|
|
83
|
-
return deviceList
|
|
177
|
+
return deviceList;
|
|
84
178
|
}
|
|
85
179
|
}
|
|
@@ -7,21 +7,74 @@ import AllNetworkGetAddressBase from './AllNetworkGetAddressBase';
|
|
|
7
7
|
import type { CoreApi } from '../../types';
|
|
8
8
|
import type {
|
|
9
9
|
AllNetworkAddress,
|
|
10
|
-
AllNetworkAddressParams,
|
|
11
10
|
AllNetworkGetAddressParams,
|
|
12
11
|
} from '../../types/api/allNetworkGetAddress';
|
|
13
12
|
|
|
14
|
-
type MethodParams =
|
|
15
|
-
methodName: keyof CoreApi;
|
|
16
|
-
params: Parameters<CoreApi[keyof CoreApi]>[0];
|
|
17
|
-
_originRequestParams: AllNetworkAddressParams;
|
|
18
|
-
_originalIndex: number;
|
|
19
|
-
};
|
|
13
|
+
type MethodParams = ReturnType<AllNetworkGetAddressBase['generateMethodName']>;
|
|
20
14
|
|
|
21
15
|
export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
|
|
16
|
+
private checkAborted() {
|
|
17
|
+
if (this.abortController?.signal.aborted) {
|
|
18
|
+
throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private async callAddressGroup(
|
|
23
|
+
methodName: keyof CoreApi,
|
|
24
|
+
params: MethodParams[],
|
|
25
|
+
rootFingerprint: number
|
|
26
|
+
): Promise<AllNetworkAddress[]> {
|
|
27
|
+
const methodCallParams = { bundle: params.map(param => ({ ...param.params })) };
|
|
28
|
+
if (!this.device.isProtocolV2() || params.length === 1) {
|
|
29
|
+
return this.callMethod(methodName, methodCallParams, rootFingerprint);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const postedAddressCounts = new Map<string, number>();
|
|
33
|
+
let runningIndividually = false;
|
|
34
|
+
const postMessage: typeof this.postMessage = message => {
|
|
35
|
+
if (message.type === UI_REQUEST.PREVIOUS_ADDRESS_RESULT) {
|
|
36
|
+
const { path, address } = message.payload.data;
|
|
37
|
+
const key = JSON.stringify([path, address]);
|
|
38
|
+
const count = postedAddressCounts.get(key) ?? 0;
|
|
39
|
+
if (runningIndividually && count > 0) {
|
|
40
|
+
postedAddressCounts.set(key, count - 1);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!runningIndividually) postedAddressCounts.set(key, count + 1);
|
|
44
|
+
}
|
|
45
|
+
this.postMessage(message);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Only silent reads may be replayed. Forward their notifications immediately,
|
|
49
|
+
// then suppress matching retry copies by count so repeated inputs still emit.
|
|
50
|
+
if (params.every(param => param._originRequestParams.showOnOneKey === false)) {
|
|
51
|
+
const response = await this.callMethod(
|
|
52
|
+
methodName,
|
|
53
|
+
methodCallParams,
|
|
54
|
+
rootFingerprint,
|
|
55
|
+
postMessage
|
|
56
|
+
);
|
|
57
|
+
// Skippable errors become failed items; link, cancellation and wallet errors throw.
|
|
58
|
+
if (response.some(item => item.success)) return response;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
runningIndividually = true;
|
|
62
|
+
const responses: AllNetworkAddress[] = [];
|
|
63
|
+
for (const param of params) {
|
|
64
|
+
this.checkAborted();
|
|
65
|
+
const response = await this.callMethod(
|
|
66
|
+
methodName,
|
|
67
|
+
{ bundle: [{ ...param.params }] },
|
|
68
|
+
rootFingerprint,
|
|
69
|
+
postMessage
|
|
70
|
+
);
|
|
71
|
+
responses.push(...response);
|
|
72
|
+
}
|
|
73
|
+
return responses;
|
|
74
|
+
}
|
|
75
|
+
|
|
22
76
|
async getAllNetworkAddress(rootFingerprint: number) {
|
|
23
77
|
const responses: AllNetworkAddress[] = [];
|
|
24
|
-
const resultMap: Record<string, AllNetworkAddress> = {};
|
|
25
78
|
const { bundle } = this.payload as AllNetworkGetAddressParams;
|
|
26
79
|
|
|
27
80
|
const methodParams = bundle.map((param, index) =>
|
|
@@ -31,58 +84,39 @@ export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
|
|
|
31
84
|
originalIndex: index,
|
|
32
85
|
})
|
|
33
86
|
);
|
|
34
|
-
|
|
87
|
+
// Protocol V2 DeviceSessionGet is the Initialize(session_id) equivalent: the
|
|
88
|
+
// SE wallet stays selected until the next Ask/Get or lock. Nested chain
|
|
89
|
+
// methods still resume once in callMethod; same-method addresses can share
|
|
90
|
+
// that session the way Protocol V1 bundles do.
|
|
91
|
+
const methodGroups = methodParams.reduce((groups, param) => {
|
|
35
92
|
const group = groups.get(param.methodName) ?? [];
|
|
36
93
|
group.push(param);
|
|
37
94
|
groups.set(param.methodName, group);
|
|
38
95
|
return groups;
|
|
39
96
|
}, new Map<keyof CoreApi, MethodParams[]>());
|
|
40
|
-
const requiresProtocolV2WalletHandoff =
|
|
41
|
-
this.device.isProtocolV2() &&
|
|
42
|
-
(this.payload.useEmptyPassphrase === true || !!this.payload.passphraseState);
|
|
43
|
-
const methodGroups: [keyof CoreApi, MethodParams[]][] = requiresProtocolV2WalletHandoff
|
|
44
|
-
? methodParams.map(param => [param.methodName, [param]])
|
|
45
|
-
: Array.from(groupedMethodParams.entries());
|
|
46
97
|
|
|
47
|
-
let
|
|
48
|
-
for (const [methodName, params] of methodGroups) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
})),
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
if (this.abortController?.signal.aborted) {
|
|
56
|
-
throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
|
|
57
|
-
}
|
|
58
|
-
// call method
|
|
59
|
-
const response = await this.callMethod(methodName, methodParams, rootFingerprint);
|
|
98
|
+
let processed = 0;
|
|
99
|
+
for (const [methodName, params] of methodGroups.entries()) {
|
|
100
|
+
this.checkAborted();
|
|
101
|
+
const response = await this.callAddressGroup(methodName, params, rootFingerprint);
|
|
102
|
+
this.checkAborted();
|
|
60
103
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for (let i = 0; i < params.length; i++) {
|
|
66
|
-
const { _originRequestParams, _originalIndex } = params[i];
|
|
67
|
-
const responseKey = `${_originalIndex}`;
|
|
68
|
-
resultMap[responseKey] = {
|
|
104
|
+
for (let index = 0; index < params.length; index++) {
|
|
105
|
+
const { _originRequestParams, _originalIndex } = params[index];
|
|
106
|
+
responses[_originalIndex] = {
|
|
69
107
|
..._originRequestParams,
|
|
70
|
-
...response[
|
|
108
|
+
...response[index],
|
|
71
109
|
};
|
|
72
110
|
}
|
|
73
111
|
|
|
74
|
-
|
|
75
|
-
|
|
112
|
+
processed += params.length;
|
|
113
|
+
if (bundle.length > 1) {
|
|
114
|
+
const progress = Math.round((processed / bundle.length) * 100);
|
|
76
115
|
this.postMessage(createUiMessage(UI_REQUEST.DEVICE_PROGRESS, { progress }));
|
|
77
116
|
}
|
|
78
|
-
i++;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
for (let i = 0; i < bundle.length; i++) {
|
|
82
|
-
responses.push(resultMap[i]);
|
|
83
117
|
}
|
|
84
118
|
|
|
85
119
|
this.abortController = null;
|
|
86
|
-
return
|
|
120
|
+
return responses;
|
|
87
121
|
}
|
|
88
122
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import semver from 'semver';
|
|
2
2
|
import {
|
|
3
|
+
EDeviceType,
|
|
3
4
|
ERRORS,
|
|
4
5
|
HardwareError,
|
|
5
6
|
HardwareErrorCode,
|
|
@@ -15,6 +16,7 @@ import { DEVICE, IFRAME, createUiMessage } from '../../events';
|
|
|
15
16
|
import { UI_REQUEST } from '../../constants/ui-request';
|
|
16
17
|
import { onDeviceButtonHandler } from '../../core';
|
|
17
18
|
import { runMethodWithUnlockPolicy } from '../../protocols/protocol-v2/unlockPolicyRunner';
|
|
19
|
+
import { supportsProtocolV2Message } from '../../protocols/protocol-v2/features';
|
|
18
20
|
import {
|
|
19
21
|
completeRequestContext,
|
|
20
22
|
createRequestContext,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
} from '../../utils/tracing';
|
|
23
25
|
|
|
24
26
|
import type { Device, DeviceEvents } from '../../device/Device';
|
|
27
|
+
import type { DeviceCommands } from '../../device/DeviceCommands';
|
|
25
28
|
import type { CoreApi } from '../../types';
|
|
26
29
|
import type {
|
|
27
30
|
AllNetworkAddress,
|
|
@@ -266,6 +269,16 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
266
269
|
|
|
267
270
|
abortController: AbortController | null = null;
|
|
268
271
|
|
|
272
|
+
protected loadingCleanupInBackground = false;
|
|
273
|
+
|
|
274
|
+
private loadingCommands?: DeviceCommands;
|
|
275
|
+
|
|
276
|
+
// DeviceSessionGet selects the SE wallet like Initialize(session_id). Nested
|
|
277
|
+
// all-network methods skip callAPI, so the first chain call still resumes;
|
|
278
|
+
// later same-domain calls reuse that session. Cardano may Ask [Standard,
|
|
279
|
+
// Cardano] once, which also covers later non-Cardano commands.
|
|
280
|
+
private protocolV2ResumedSeedDomains = new Set<'standard' | 'cardano'>();
|
|
281
|
+
|
|
269
282
|
init() {
|
|
270
283
|
this.checkDeviceId = true;
|
|
271
284
|
this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
|
|
@@ -313,12 +326,30 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
313
326
|
};
|
|
314
327
|
}
|
|
315
328
|
|
|
329
|
+
private hasProtocolV2WalletResume(deriveCardano?: boolean) {
|
|
330
|
+
if (deriveCardano) {
|
|
331
|
+
return this.protocolV2ResumedSeedDomains.has('cardano');
|
|
332
|
+
}
|
|
333
|
+
return (
|
|
334
|
+
this.protocolV2ResumedSeedDomains.has('standard') ||
|
|
335
|
+
this.protocolV2ResumedSeedDomains.has('cardano')
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private markProtocolV2WalletResumed(deriveCardano?: boolean) {
|
|
340
|
+
this.protocolV2ResumedSeedDomains.add('standard');
|
|
341
|
+
if (deriveCardano) {
|
|
342
|
+
this.protocolV2ResumedSeedDomains.add('cardano');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
316
346
|
async callMethod(
|
|
317
347
|
methodName: keyof CoreApi,
|
|
318
348
|
params: any & {
|
|
319
349
|
bundle: (any & { _originRequestParams: CommonResponseParams })[];
|
|
320
350
|
},
|
|
321
|
-
rootFingerprint: number
|
|
351
|
+
rootFingerprint: number,
|
|
352
|
+
postMessage = this.postMessage
|
|
322
353
|
) {
|
|
323
354
|
const method: BaseMethod = findMethod({
|
|
324
355
|
event: IFRAME.CALL,
|
|
@@ -332,7 +363,7 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
332
363
|
});
|
|
333
364
|
|
|
334
365
|
method.connector = this.connector;
|
|
335
|
-
method.postMessage =
|
|
366
|
+
method.postMessage = postMessage;
|
|
336
367
|
if (this.context) {
|
|
337
368
|
method.setContext?.(this.context);
|
|
338
369
|
}
|
|
@@ -388,16 +419,17 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
388
419
|
}
|
|
389
420
|
}
|
|
390
421
|
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
// requested standard or hidden wallet before sending its device command.
|
|
422
|
+
// Nested chain methods skip callAPI's session gate. Resume the requested
|
|
423
|
+
// wallet once per seed domain; DeviceSessionGet is sticky like V1
|
|
424
|
+
// Initialize, so later addresses and chains reuse it.
|
|
395
425
|
const useEmptyPassphrase = this.payload.useEmptyPassphrase === true;
|
|
396
|
-
// Nested Cardano methods opt in to [Standard, Cardano] if Ask rebuilds.
|
|
397
|
-
// Other chains stay Standard-only.
|
|
398
426
|
const deriveCardano = method.name.startsWith('cardano') ? true : undefined;
|
|
399
427
|
const shouldResumeWalletSession = useEmptyPassphrase || !!this.payload.passphraseState;
|
|
400
|
-
if (
|
|
428
|
+
if (
|
|
429
|
+
this.device.isProtocolV2() &&
|
|
430
|
+
shouldResumeWalletSession &&
|
|
431
|
+
!this.hasProtocolV2WalletResume(deriveCardano)
|
|
432
|
+
) {
|
|
401
433
|
const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
|
|
402
434
|
this.payload.passphraseState,
|
|
403
435
|
useEmptyPassphrase,
|
|
@@ -408,6 +440,7 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
408
440
|
if (!passphraseStateSafety) {
|
|
409
441
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
|
|
410
442
|
}
|
|
443
|
+
this.markProtocolV2WalletResumed(deriveCardano);
|
|
411
444
|
}
|
|
412
445
|
},
|
|
413
446
|
});
|
|
@@ -457,30 +490,68 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
457
490
|
|
|
458
491
|
abstract getAllNetworkAddress(rootFingerprint: number): Promise<AllNetworkAddress[]>;
|
|
459
492
|
|
|
493
|
+
protected async stopAllNetworkLoading(canSend = true) {
|
|
494
|
+
const commands = this.loadingCommands;
|
|
495
|
+
this.loadingCommands = undefined;
|
|
496
|
+
// Never send cleanup through a replacement or disposed connection.
|
|
497
|
+
if (!canSend || !commands || commands.disposed || commands !== this.device.commands) return;
|
|
498
|
+
try {
|
|
499
|
+
await commands.typedCall('DeviceAnimationControl', 'Success', {
|
|
500
|
+
action: PROTO.DeviceAnimationAction.AnimationAction_Stop,
|
|
501
|
+
});
|
|
502
|
+
} catch {
|
|
503
|
+
// Cleanup must not mask the operation result. Firmware also has an idle timeout.
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
460
507
|
async run() {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
508
|
+
this.loadingCleanupInBackground = false;
|
|
509
|
+
try {
|
|
510
|
+
if (
|
|
511
|
+
this.device.isProtocolV2() &&
|
|
512
|
+
(this.device.getCurrentDeviceType() === EDeviceType.Pro2 ||
|
|
513
|
+
this.device.getCurrentDeviceType() === EDeviceType.Neo)
|
|
514
|
+
) {
|
|
515
|
+
const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
|
|
516
|
+
if (supportsProtocolV2Message(protocolInfo, 60461)) {
|
|
517
|
+
const { commands } = this.device;
|
|
518
|
+
await commands.typedCall('DeviceAnimationControl', 'Success', {
|
|
519
|
+
action: PROTO.DeviceAnimationAction.AnimationAction_Start,
|
|
520
|
+
});
|
|
521
|
+
this.loadingCommands = commands;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
467
524
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
525
|
+
const res = await this.device.commands.typedCall('GetPublicKey', 'PublicKey', {
|
|
526
|
+
address_n: [toHardened(44), toHardened(1), toHardened(0)],
|
|
527
|
+
coin_name: 'Testnet',
|
|
528
|
+
script_type: 'SPENDADDRESS',
|
|
529
|
+
show_display: false,
|
|
530
|
+
});
|
|
471
531
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
532
|
+
if (!this.device.isProtocolV2()) {
|
|
533
|
+
this.postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_PIN_WINDOW));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (res.message.root_fingerprint == null) {
|
|
537
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter);
|
|
538
|
+
}
|
|
475
539
|
|
|
476
|
-
|
|
540
|
+
this.abortController = new AbortController();
|
|
477
541
|
|
|
478
|
-
|
|
542
|
+
return await this.getAllNetworkAddress(res.message.root_fingerprint);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
// A failed call may have invalidated the transport link without disposing
|
|
545
|
+
// DeviceCommands. Let firmware time out instead of reconnecting for Stop.
|
|
546
|
+
await this.stopAllNetworkLoading(false);
|
|
479
547
|
if (e instanceof HardwareError && e.errorCode === HardwareErrorCode.RepeatUnlocking) {
|
|
480
548
|
throw ERRORS.TypedError(HardwareErrorCode.RepeatUnlocking, e.message);
|
|
481
549
|
}
|
|
482
550
|
throw e;
|
|
483
|
-
}
|
|
551
|
+
} finally {
|
|
552
|
+
// The callback API returns before its chain requests finish.
|
|
553
|
+
if (!this.loadingCleanupInBackground) await this.stopAllNetworkLoading();
|
|
554
|
+
}
|
|
484
555
|
}
|
|
485
556
|
}
|
|
486
557
|
|
|
@@ -28,6 +28,7 @@ export default class AllNetworkGetAddressByLoop extends AllNetworkGetAddressBase
|
|
|
28
28
|
const bundle = this.payload.bundle || [this.payload];
|
|
29
29
|
|
|
30
30
|
// process callbacks in background
|
|
31
|
+
this.loadingCleanupInBackground = true;
|
|
31
32
|
const callbackPromise = this.processCallbacksInBackground(
|
|
32
33
|
bundle,
|
|
33
34
|
rootFingerprint,
|
|
@@ -95,6 +96,7 @@ export default class AllNetworkGetAddressByLoop extends AllNetworkGetAddressBase
|
|
|
95
96
|
data: allResults,
|
|
96
97
|
});
|
|
97
98
|
} catch (error: any) {
|
|
99
|
+
await this.stopAllNetworkLoading(false);
|
|
98
100
|
let errorCode = error.errorCode || error.code;
|
|
99
101
|
let errorMessage = error.message;
|
|
100
102
|
|
|
@@ -121,6 +123,7 @@ export default class AllNetworkGetAddressByLoop extends AllNetworkGetAddressBase
|
|
|
121
123
|
},
|
|
122
124
|
});
|
|
123
125
|
} finally {
|
|
126
|
+
await this.stopAllNetworkLoading();
|
|
124
127
|
this.context?.cancelCallbackTasks(this.payload.connectId);
|
|
125
128
|
this.abortController = null;
|
|
126
129
|
}
|
|
@@ -22,6 +22,14 @@ export default class DeviceVerify extends BaseMethod<BixinVerifyDeviceRequest> {
|
|
|
22
22
|
// the main PIN or an Attach PIN may authorize them.
|
|
23
23
|
this.protocolV2PreUnlockPinType = DeviceSessionPinType.Any;
|
|
24
24
|
this.useDevicePassphraseState = false;
|
|
25
|
+
this.protocolV2UiInteraction = {
|
|
26
|
+
request: 'button',
|
|
27
|
+
source: 'method-lifecycle',
|
|
28
|
+
reason: 'device-management',
|
|
29
|
+
completion: 'operation-completed',
|
|
30
|
+
deviceOnly: true,
|
|
31
|
+
operation: 'deviceVerify',
|
|
32
|
+
};
|
|
25
33
|
|
|
26
34
|
// check payload
|
|
27
35
|
validateParams(this.payload, [{ name: 'dataHex', type: 'hexString' }]);
|
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;
|