@onekeyfe/hd-core 1.2.2-alpha.100 → 1.2.2-alpha.2
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 +20 -4
- package/__tests__/DeviceCommands.test.ts +13 -0
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -2
- package/__tests__/check-firmware-release-protocol-v2.test.ts +0 -2
- package/__tests__/device-lifecycle-events.test.ts +19 -0
- package/__tests__/deviceUploadNft.test.ts +68 -0
- package/__tests__/evmSignTypedData.test.ts +42 -0
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +825 -12
- package/__tests__/open-wallet-session.test.ts +144 -37
- package/__tests__/pro2HostAssetPackage.test.ts +58 -0
- package/__tests__/protocol-v2-resources.test.ts +7 -4
- package/__tests__/protocol-v2-unlock-policy.test.ts +35 -1
- package/__tests__/protocol-v2.test.ts +720 -51
- package/dist/api/FirmwareUpdateV4.d.ts +4 -0
- package/dist/api/FirmwareUpdateV4.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/evm/EVMSignTypedData.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/getBinary.d.ts +2 -2
- package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/api/protocol-v2/helpers.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 +1 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceConnector.d.ts +1 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +3 -0
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +21 -5
- package/dist/index.js +2278 -1435
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +2 -0
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/settings.d.ts +2 -2
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/dist/utils/pro2HostAssetPackage.d.ts +8 -0
- package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +434 -75
- package/src/api/OpenWalletSession.ts +39 -9
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
- package/src/api/evm/EVMSignTypedData.ts +1 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
- package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
- package/src/api/protocol-v2/DeviceUploadNft.ts +31 -8
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +27 -8
- package/src/api/protocol-v2/helpers.ts +8 -0
- package/src/core/index.ts +121 -10
- package/src/data/messages/messages-protocol-v2.json +1541 -1332
- package/src/device/Device.ts +3 -1
- package/src/device/DeviceConnector.ts +7 -3
- package/src/events/ui-request.ts +3 -0
- package/src/protocols/protocol-v2/resources.ts +9 -1
- package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
- package/src/protocols/protocol-v2/walletSession.ts +88 -50
- package/src/types/settings.ts +4 -2
- package/src/utils/deviceFeaturesUtils.ts +4 -0
- package/src/utils/pro2HostAssetPackage.ts +354 -0
|
@@ -29,6 +29,12 @@ const wasResumed = (session: unknown) =>
|
|
|
29
29
|
'resumed' in session &&
|
|
30
30
|
(session as { resumed?: unknown }).resumed === true;
|
|
31
31
|
|
|
32
|
+
const wasWalletStatusRefreshed = (session: unknown) =>
|
|
33
|
+
!!session &&
|
|
34
|
+
typeof session === 'object' &&
|
|
35
|
+
'walletStatusRefreshed' in session &&
|
|
36
|
+
(session as { walletStatusRefreshed?: unknown }).walletStatusRefreshed === true;
|
|
37
|
+
|
|
32
38
|
const requireHiddenWalletResponse = (session: { passphraseState?: string }) => {
|
|
33
39
|
if (!session.passphraseState) {
|
|
34
40
|
throw ERRORS.TypedError(
|
|
@@ -87,12 +93,20 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
|
|
|
87
93
|
this.unlockPolicy = 'unlock-before-run';
|
|
88
94
|
this.skipForceUpdateCheck = true;
|
|
89
95
|
this.params = normalizeParams(this.payload as unknown as Record<string, unknown>);
|
|
96
|
+
this.protocolV2PreUnlockPinType =
|
|
97
|
+
this.params.mode === OpenWalletSessionMode.SelectHidden
|
|
98
|
+
? DeviceSessionPinType.Any
|
|
99
|
+
: DeviceSessionPinType.Main;
|
|
90
100
|
this.payload.useEmptyPassphrase = this.params.mode === OpenWalletSessionMode.Standard;
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
async run(): Promise<OpenWalletSessionPayload> {
|
|
94
104
|
const isProtocolV2 = this.device.isProtocolV2();
|
|
95
|
-
|
|
105
|
+
const reusePreflightStatus =
|
|
106
|
+
isProtocolV2 && this.protocolV2UnlockContext?.preflightStatusRefreshed === true;
|
|
107
|
+
let state = reusePreflightStatus
|
|
108
|
+
? await this.device.getDeviceState()
|
|
109
|
+
: await this.device.getDeviceState({ refreshSections: ['status'] });
|
|
96
110
|
let currentDeviceId = state.identity.deviceId;
|
|
97
111
|
const hasAuthoritativeProtocolV2WalletStatus = (candidate: typeof state) =>
|
|
98
112
|
candidate.status.unlocked === true &&
|
|
@@ -115,14 +129,25 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
|
|
|
115
129
|
currentDeviceId = state.identity.deviceId;
|
|
116
130
|
return state;
|
|
117
131
|
};
|
|
132
|
+
const resolveProtocolV2DeviceStateAfterSession = async (session: unknown) => {
|
|
133
|
+
if (!wasWalletStatusRefreshed(session)) {
|
|
134
|
+
return refreshProtocolV2DeviceState();
|
|
135
|
+
}
|
|
136
|
+
state = await this.device.getDeviceState();
|
|
137
|
+
currentDeviceId = state.identity.deviceId;
|
|
138
|
+
return state;
|
|
139
|
+
};
|
|
118
140
|
const ensureProtocolV2WalletStatus = async () => {
|
|
119
141
|
if (isProtocolV2 && !hasAuthoritativeProtocolV2WalletStatus(state)) {
|
|
120
|
-
await this.device.unlockDevice(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
142
|
+
await this.device.unlockDevice(
|
|
143
|
+
this.protocolV2PreUnlockPinType ?? DeviceSessionPinType.Main,
|
|
144
|
+
{
|
|
145
|
+
source: 'unlock-coordinator',
|
|
146
|
+
reason: 'device-locked',
|
|
147
|
+
deviceOnly: true,
|
|
148
|
+
method: 'openWalletSession',
|
|
149
|
+
}
|
|
150
|
+
);
|
|
126
151
|
requireAuthoritativeProtocolV2WalletStatus(await refreshProtocolV2DeviceState());
|
|
127
152
|
}
|
|
128
153
|
return state;
|
|
@@ -139,13 +164,16 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
|
|
|
139
164
|
selectMainWalletBeforeRestore:
|
|
140
165
|
!hasAuthoritativeProtocolV2WalletStatus(state) ||
|
|
141
166
|
state.status.unlockedAttachPin === true,
|
|
167
|
+
mainPinSelected: this.protocolV2UnlockContext?.preflightMainPinSelected,
|
|
142
168
|
})
|
|
143
169
|
: await getPassphraseStateWithRefreshDeviceInfo(this.device, {
|
|
144
170
|
onlyMainPin: true,
|
|
145
171
|
initSession: this.payload.initSession,
|
|
146
172
|
});
|
|
147
173
|
const refreshedState = isProtocolV2
|
|
148
|
-
? requireAuthoritativeProtocolV2WalletStatus(
|
|
174
|
+
? requireAuthoritativeProtocolV2WalletStatus(
|
|
175
|
+
await resolveProtocolV2DeviceStateAfterSession(session)
|
|
176
|
+
)
|
|
149
177
|
: state;
|
|
150
178
|
const deviceId = requireDeviceId();
|
|
151
179
|
if (
|
|
@@ -233,7 +261,9 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
|
|
|
233
261
|
})
|
|
234
262
|
: await getPassphraseStateWithRefreshDeviceInfo(this.device, { initSession: true });
|
|
235
263
|
const refreshedState = isProtocolV2
|
|
236
|
-
? requireAuthoritativeProtocolV2WalletStatus(
|
|
264
|
+
? requireAuthoritativeProtocolV2WalletStatus(
|
|
265
|
+
await resolveProtocolV2DeviceStateAfterSession(session)
|
|
266
|
+
)
|
|
237
267
|
: state;
|
|
238
268
|
const deviceId = requireDeviceId();
|
|
239
269
|
if (isProtocolV2 && refreshedState.status.passphraseProtection !== true) {
|
|
@@ -400,7 +400,8 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
400
400
|
this.payload.passphraseState,
|
|
401
401
|
useEmptyPassphrase,
|
|
402
402
|
this.payload.skipPassphraseCheck,
|
|
403
|
-
deriveCardano
|
|
403
|
+
deriveCardano,
|
|
404
|
+
this.protocolV2UnlockContext?.preflightMainPinSelected
|
|
404
405
|
);
|
|
405
406
|
if (!passphraseStateSafety) {
|
|
406
407
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
|
|
@@ -360,6 +360,7 @@ export default class EVMSignTypedData extends BaseMethod<EVMSignTypedDataParams>
|
|
|
360
360
|
|
|
361
361
|
const supportBiggerData =
|
|
362
362
|
DeviceModelToTypes.model_classic1s.includes(currentDeviceType) ||
|
|
363
|
+
DeviceModelToTypes.model_pro2.includes(currentDeviceType) ||
|
|
363
364
|
(DeviceModelToTypes.model_touch.includes(currentDeviceType) &&
|
|
364
365
|
semver.gte(currentVersion, supportBiggerDataVersion));
|
|
365
366
|
|
|
@@ -41,9 +41,15 @@ const FIRMWARE_UPDATE_CONFIRM = 'Firmware install confirmed';
|
|
|
41
41
|
*/
|
|
42
42
|
export const BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS = 5000;
|
|
43
43
|
|
|
44
|
-
type
|
|
44
|
+
type FirmwareProgressMetadata = Pick<
|
|
45
45
|
FirmwareProgress['payload'],
|
|
46
|
-
|
|
46
|
+
| 'transferredBytes'
|
|
47
|
+
| 'totalBytes'
|
|
48
|
+
| 'rateBytesPerSecond'
|
|
49
|
+
| 'elapsedMs'
|
|
50
|
+
| 'installTargetId'
|
|
51
|
+
| 'installPhase'
|
|
52
|
+
| 'installPhaseProgress'
|
|
47
53
|
>;
|
|
48
54
|
|
|
49
55
|
const isDeviceDisconnectedError = (error: unknown) => {
|
|
@@ -103,7 +109,7 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
|
|
|
103
109
|
postProgressMessage = (
|
|
104
110
|
progress: number,
|
|
105
111
|
progressType: IFirmwareUpdateProgressType,
|
|
106
|
-
metrics?:
|
|
112
|
+
metrics?: FirmwareProgressMetadata
|
|
107
113
|
) => {
|
|
108
114
|
this.postMessage(
|
|
109
115
|
createUiMessage(UI_REQUEST.FIRMWARE_PROGRESS, {
|
|
@@ -4,7 +4,13 @@ import { invalidParameter } from '../helpers/filesystemValidation';
|
|
|
4
4
|
import type { DeviceFirmwareUpdateStatusGetParams } from './helpers';
|
|
5
5
|
import type { DeviceFirmwareUpdateRecordFields } from '@onekeyfe/hd-transport';
|
|
6
6
|
|
|
7
|
-
const DEVICE_FIRMWARE_UPDATE_STATUS_FIELDS = [
|
|
7
|
+
const DEVICE_FIRMWARE_UPDATE_STATUS_FIELDS = [
|
|
8
|
+
'status',
|
|
9
|
+
'progress_percent',
|
|
10
|
+
'phase_info',
|
|
11
|
+
'payload_version',
|
|
12
|
+
'path',
|
|
13
|
+
] as const;
|
|
8
14
|
|
|
9
15
|
function normalizeStatusFields(
|
|
10
16
|
fields: DeviceFirmwareUpdateStatusGetParams['fields']
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
getCompletePro2NftBasenames,
|
|
21
21
|
} from '../../utils/pro2Nft';
|
|
22
22
|
import { encodePro2Image } from '../../utils/pro2Wallpaper';
|
|
23
|
+
import {
|
|
24
|
+
buildPro2HostAssetPackage,
|
|
25
|
+
supportsPro2HostAssetPackage,
|
|
26
|
+
} from '../../utils/pro2HostAssetPackage';
|
|
23
27
|
import { BaseMethod } from '../BaseMethod';
|
|
24
28
|
import { decodeJpegBase64ToRgba } from '../helpers/base64Data';
|
|
25
29
|
import { invalidParameter } from '../helpers/filesystemValidation';
|
|
@@ -38,8 +42,11 @@ export type DeviceUploadNftParams = {
|
|
|
38
42
|
|
|
39
43
|
export type DeviceUploadNftResponse = {
|
|
40
44
|
basename: string;
|
|
45
|
+
/** Final image path after firmware extracts a host-asset package. */
|
|
41
46
|
imagePath: string;
|
|
47
|
+
/** Final thumbnail path after firmware extracts a host-asset package. */
|
|
42
48
|
thumbnailPath: string;
|
|
49
|
+
/** Final metadata path after firmware extracts a host-asset package. */
|
|
43
50
|
metadataPath: string;
|
|
44
51
|
totalSize: number;
|
|
45
52
|
nftUpdated: true;
|
|
@@ -138,7 +145,7 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
138
145
|
this.useDevicePassphraseState = false;
|
|
139
146
|
}
|
|
140
147
|
|
|
141
|
-
private async assertCapabilities() {
|
|
148
|
+
private async assertCapabilities(useHostAssetPackage: boolean) {
|
|
142
149
|
const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
|
|
143
150
|
const hasFileWrite = supportsProtocolV2Message(
|
|
144
151
|
protocolInfo,
|
|
@@ -150,7 +157,7 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
150
157
|
);
|
|
151
158
|
const hasDirList = supportsProtocolV2Message(protocolInfo, FILESYSTEM_DIR_LIST_MESSAGE_TYPE);
|
|
152
159
|
const hasNftUpdate = supportsProtocolV2Message(protocolInfo, NFT_UPDATE_MESSAGE_TYPE);
|
|
153
|
-
if (!hasFileWrite || !
|
|
160
|
+
if (!hasFileWrite || !hasNftUpdate || (!useHostAssetPackage && (!hasPathInfo || !hasDirList))) {
|
|
154
161
|
throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType());
|
|
155
162
|
}
|
|
156
163
|
}
|
|
@@ -192,16 +199,32 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
192
199
|
const { bundle } = this;
|
|
193
200
|
if (!bundle) throw invalidParameter('NFT data has not been initialized.');
|
|
194
201
|
|
|
195
|
-
|
|
202
|
+
const useHostAssetPackage = supportsPro2HostAssetPackage(
|
|
203
|
+
this.device.state?.versions.firmware ?? undefined
|
|
204
|
+
);
|
|
205
|
+
await this.assertCapabilities(useHostAssetPackage);
|
|
196
206
|
this.throwIfAborted();
|
|
197
|
-
await this.assertStorageCapacity(bundle.basename);
|
|
207
|
+
if (!useHostAssetPackage) await this.assertStorageCapacity(bundle.basename);
|
|
198
208
|
this.throwIfAborted();
|
|
199
209
|
|
|
200
|
-
const
|
|
210
|
+
const extractedFiles = [
|
|
201
211
|
{ path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.bin`, data: bundle.image },
|
|
202
212
|
{ path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}_m.bin`, data: bundle.thumbnail },
|
|
203
213
|
{ path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.json`, data: bundle.metadata },
|
|
204
214
|
];
|
|
215
|
+
const files = useHostAssetPackage
|
|
216
|
+
? [
|
|
217
|
+
{
|
|
218
|
+
path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.okpkg`,
|
|
219
|
+
data: buildPro2HostAssetPackage(
|
|
220
|
+
extractedFiles.map(file => ({
|
|
221
|
+
name: file.path.slice(`${PRO2_NFT_DIRECTORY}/`.length),
|
|
222
|
+
data: file.data,
|
|
223
|
+
}))
|
|
224
|
+
),
|
|
225
|
+
},
|
|
226
|
+
]
|
|
227
|
+
: extractedFiles;
|
|
205
228
|
const totalSize = files.reduce((sum, file) => sum + file.data.byteLength, 0);
|
|
206
229
|
let transferredBeforeFile = 0;
|
|
207
230
|
|
|
@@ -238,9 +261,9 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
238
261
|
const response = await this.updateNft(bundle.basename);
|
|
239
262
|
return {
|
|
240
263
|
basename: bundle.basename,
|
|
241
|
-
imagePath:
|
|
242
|
-
thumbnailPath:
|
|
243
|
-
metadataPath:
|
|
264
|
+
imagePath: extractedFiles[0].path,
|
|
265
|
+
thumbnailPath: extractedFiles[1].path,
|
|
266
|
+
metadataPath: extractedFiles[2].path,
|
|
244
267
|
totalSize,
|
|
245
268
|
nftUpdated: true,
|
|
246
269
|
message: response.message?.message,
|
|
@@ -16,14 +16,26 @@ import {
|
|
|
16
16
|
type Pro2WallpaperColorFormat,
|
|
17
17
|
encodePro2Wallpaper,
|
|
18
18
|
} from '../../utils/pro2Wallpaper';
|
|
19
|
+
import {
|
|
20
|
+
buildPro2HostAssetPackage,
|
|
21
|
+
supportsPro2HostAssetPackage,
|
|
22
|
+
} from '../../utils/pro2HostAssetPackage';
|
|
19
23
|
|
|
20
24
|
export type DeviceUploadWallpaperParams = {
|
|
21
25
|
jpegBase64: string;
|
|
26
|
+
/**
|
|
27
|
+
* Legacy firmware uses this name for the uploaded `.bin` file. Firmware
|
|
28
|
+
* 1.0.1+ always consumes the fixed `wallpaper.okpkg` package path.
|
|
29
|
+
*/
|
|
22
30
|
fileName?: string;
|
|
23
31
|
chunkSize?: number;
|
|
24
32
|
};
|
|
25
33
|
|
|
26
34
|
export type DeviceUploadWallpaperResponse = {
|
|
35
|
+
/**
|
|
36
|
+
* Filesystem path sent to `DeviceSettingsSet`. On firmware 1.0.1+ this is
|
|
37
|
+
* the temporary package path; firmware extracts and persists wallpaper.bin.
|
|
38
|
+
*/
|
|
27
39
|
path: string;
|
|
28
40
|
size: number;
|
|
29
41
|
colorFormat: Pro2WallpaperColorFormat;
|
|
@@ -31,6 +43,8 @@ export type DeviceUploadWallpaperResponse = {
|
|
|
31
43
|
};
|
|
32
44
|
|
|
33
45
|
const WALLPAPER_DIRECTORY = 'vol1:/wallpapers';
|
|
46
|
+
const WALLPAPER_PACKAGE_PATH = `${WALLPAPER_DIRECTORY}/wallpaper.okpkg`;
|
|
47
|
+
const WALLPAPER_PACKAGE_ENTRY = 'wallpaper.bin';
|
|
34
48
|
const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/;
|
|
35
49
|
const DEVICE_SETTINGS_SET_MESSAGE_TYPE = 60412;
|
|
36
50
|
const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
|
|
@@ -116,16 +130,14 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
|
|
|
116
130
|
this.directoryReady = true;
|
|
117
131
|
}
|
|
118
132
|
|
|
119
|
-
private async upload() {
|
|
133
|
+
private async upload(path: string, data: Uint8Array) {
|
|
120
134
|
if (this.uploaded) return;
|
|
121
|
-
const { encoded } = this;
|
|
122
|
-
if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.');
|
|
123
135
|
|
|
124
136
|
await writeProtocolV2File({
|
|
125
137
|
commands: this.device.commands,
|
|
126
|
-
path
|
|
127
|
-
data
|
|
128
|
-
totalSize:
|
|
138
|
+
path,
|
|
139
|
+
data,
|
|
140
|
+
totalSize: data.byteLength,
|
|
129
141
|
chunkSize: this.params.chunkSize,
|
|
130
142
|
maxChunkRetries: 3,
|
|
131
143
|
overwrite: true,
|
|
@@ -145,7 +157,14 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
|
|
|
145
157
|
if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.');
|
|
146
158
|
await this.assertCapabilities();
|
|
147
159
|
await this.ensureDirectory();
|
|
148
|
-
|
|
160
|
+
const useHostAssetPackage = supportsPro2HostAssetPackage(
|
|
161
|
+
this.device.state?.versions.firmware ?? undefined
|
|
162
|
+
);
|
|
163
|
+
const data = useHostAssetPackage
|
|
164
|
+
? buildPro2HostAssetPackage([{ name: WALLPAPER_PACKAGE_ENTRY, data: encoded.data }])
|
|
165
|
+
: encoded.data;
|
|
166
|
+
if (useHostAssetPackage) this.path = WALLPAPER_PACKAGE_PATH;
|
|
167
|
+
await this.upload(this.path, data);
|
|
149
168
|
const response = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
|
|
150
169
|
settings: { wallpaper_path: this.path },
|
|
151
170
|
});
|
|
@@ -160,7 +179,7 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
|
|
|
160
179
|
}
|
|
161
180
|
return {
|
|
162
181
|
path: this.path,
|
|
163
|
-
size:
|
|
182
|
+
size: data.byteLength,
|
|
164
183
|
colorFormat: encoded.colorFormat,
|
|
165
184
|
message: response.message?.message,
|
|
166
185
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DeviceRebootType } from '@onekeyfe/hd-transport';
|
|
2
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
3
|
|
|
3
4
|
import { invalidParameter, validateNonEmptyString } from '../helpers/filesystemValidation';
|
|
4
5
|
import { ProtocolV2FirmwareTargetType } from '../../protocols/protocol-v2/firmware';
|
|
@@ -103,6 +104,13 @@ export const getProtocolV2UnknownErrorText = (error: unknown) => {
|
|
|
103
104
|
};
|
|
104
105
|
|
|
105
106
|
export const isProtocolV2DeviceDisconnectedError = (error: unknown) => {
|
|
107
|
+
const errorCode =
|
|
108
|
+
error && typeof error === 'object'
|
|
109
|
+
? (error as { errorCode?: number; code?: number }).errorCode ??
|
|
110
|
+
(error as { errorCode?: number; code?: number }).code
|
|
111
|
+
: undefined;
|
|
112
|
+
if (errorCode === HardwareErrorCode.BleDeviceDisconnected) return true;
|
|
113
|
+
|
|
106
114
|
const message = getProtocolV2UnknownErrorText(error).toLowerCase();
|
|
107
115
|
const compactMessage = message.replace(/\s+/g, '');
|
|
108
116
|
return (
|
package/src/core/index.ts
CHANGED
|
@@ -415,6 +415,13 @@ const onCallDevice = async (
|
|
|
415
415
|
if (method.payload?.onlyConnectBleDevice) {
|
|
416
416
|
preWarmCallbackTask?.resolve();
|
|
417
417
|
Log.debug('Call API - only connect ble device: ', device?.mainId);
|
|
418
|
+
// This early return bypasses the normal-path bookkeeping at the end of the
|
|
419
|
+
// call. Without it the task leaks and haunts every later queue snapshot
|
|
420
|
+
// and cancel sweep (field log: a completed task lingered for 6 minutes),
|
|
421
|
+
// and the request stays in the active maps, so repeated preconnects pile
|
|
422
|
+
// up phantom work in diagnostics.
|
|
423
|
+
completeMethodRequestContext(method);
|
|
424
|
+
requestQueue.releaseTask(method.responseID);
|
|
418
425
|
return createResponseMessage(method.responseID, true, null);
|
|
419
426
|
}
|
|
420
427
|
|
|
@@ -644,7 +651,8 @@ const onCallDevice = async (
|
|
|
644
651
|
method.payload?.passphraseState,
|
|
645
652
|
method.payload?.useEmptyPassphrase,
|
|
646
653
|
method.payload?.skipPassphraseCheck,
|
|
647
|
-
hasDeriveCardano(method)
|
|
654
|
+
hasDeriveCardano(method),
|
|
655
|
+
method.protocolV2UnlockContext?.preflightMainPinSelected
|
|
648
656
|
);
|
|
649
657
|
|
|
650
658
|
// Double check, handles the special case of Touch/Pro
|
|
@@ -950,11 +958,72 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
|
|
|
950
958
|
);
|
|
951
959
|
}
|
|
952
960
|
|
|
961
|
+
export function isProtocolV2PeerRemovedPairingError(method: BaseMethod, error: unknown) {
|
|
962
|
+
return (
|
|
963
|
+
method.payload.connectProtocol === 'V2' &&
|
|
964
|
+
(error as { errorCode?: unknown })?.errorCode ===
|
|
965
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
|
|
953
969
|
/**
|
|
954
970
|
* If the Bluetooth connection times out, retry up to 6 times
|
|
955
971
|
* @param retryCount - Current retry count (default 0)
|
|
956
972
|
*/
|
|
957
|
-
|
|
973
|
+
// device.acquire awaits a transport reply with no deadline of its own; a
|
|
974
|
+
// transport that never settles (field case: Electron main lost an IPC reply,
|
|
975
|
+
// "reply was never sent" after 5 minutes) hangs the call forever and cancel()
|
|
976
|
+
// only takes effect at poll checkpoints. Race acquire against a deadline and
|
|
977
|
+
// the caller's abort signal so the hang is bounded and cancel is immediate.
|
|
978
|
+
const BLE_ACQUIRE_DEADLINE_MS = 60 * 1000;
|
|
979
|
+
|
|
980
|
+
function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal): Promise<T> {
|
|
981
|
+
return new Promise<T>((resolve, reject) => {
|
|
982
|
+
let settled = false;
|
|
983
|
+
const settle = (fn: () => void) => {
|
|
984
|
+
if (settled) return;
|
|
985
|
+
settled = true;
|
|
986
|
+
clearTimeout(deadline);
|
|
987
|
+
abortSignal?.removeEventListener('abort', onAbort);
|
|
988
|
+
fn();
|
|
989
|
+
};
|
|
990
|
+
const onAbort = () =>
|
|
991
|
+
settle(() => reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)));
|
|
992
|
+
const deadline = setTimeout(
|
|
993
|
+
() =>
|
|
994
|
+
settle(() =>
|
|
995
|
+
reject(
|
|
996
|
+
ERRORS.TypedError(
|
|
997
|
+
HardwareErrorCode.BleTimeoutError,
|
|
998
|
+
`BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
|
|
999
|
+
)
|
|
1000
|
+
)
|
|
1001
|
+
),
|
|
1002
|
+
BLE_ACQUIRE_DEADLINE_MS
|
|
1003
|
+
);
|
|
1004
|
+
// Attach before any early return so a late settlement of acquirePromise
|
|
1005
|
+
// is always consumed — an abort or deadline must never leave the acquire
|
|
1006
|
+
// rejection unhandled.
|
|
1007
|
+
acquirePromise.then(
|
|
1008
|
+
value => settle(() => resolve(value)),
|
|
1009
|
+
error => settle(() => reject(error))
|
|
1010
|
+
);
|
|
1011
|
+
if (abortSignal) {
|
|
1012
|
+
if (abortSignal.aborted) {
|
|
1013
|
+
onAbort();
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
abortSignal.addEventListener('abort', onAbort);
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async function connectDeviceForBle(
|
|
1022
|
+
method: BaseMethod,
|
|
1023
|
+
device: Device,
|
|
1024
|
+
abortSignal?: AbortSignal,
|
|
1025
|
+
retryCount = 0
|
|
1026
|
+
) {
|
|
958
1027
|
try {
|
|
959
1028
|
if (device.wasInterruptedByUser()) {
|
|
960
1029
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
@@ -968,9 +1037,43 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
|
|
|
968
1037
|
!device.commands ||
|
|
969
1038
|
device.commands.disposed;
|
|
970
1039
|
if (shouldAcquire) {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1040
|
+
// The deadline/abort guards are scoped to the desktop electron
|
|
1041
|
+
// transport: its IPC acquire is the only path with a proven
|
|
1042
|
+
// never-settling failure mode, while react-native/lowlevel acquire may
|
|
1043
|
+
// legitimately block on a user-driven system bonding prompt for longer
|
|
1044
|
+
// than any sane deadline. Other envs keep the plain acquire unchanged.
|
|
1045
|
+
const useAcquireGuards = DataManager.getSettings('env') === 'desktop-web-ble';
|
|
1046
|
+
// A cancel landing during the retry backoff must not start a new acquire.
|
|
1047
|
+
if (useAcquireGuards && abortSignal?.aborted) {
|
|
1048
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
1049
|
+
}
|
|
1050
|
+
if (!useAcquireGuards) {
|
|
1051
|
+
await device.acquire(method.payload.connectProtocol, {
|
|
1052
|
+
forceProtocolDetection: method.payload.forceProtocolDetection,
|
|
1053
|
+
});
|
|
1054
|
+
} else {
|
|
1055
|
+
try {
|
|
1056
|
+
await raceBleAcquire(
|
|
1057
|
+
device.acquire(method.payload.connectProtocol, {
|
|
1058
|
+
forceProtocolDetection: method.payload.forceProtocolDetection,
|
|
1059
|
+
}),
|
|
1060
|
+
abortSignal
|
|
1061
|
+
);
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
// A deadline hit means the transport is wedged mid-acquire; drop the
|
|
1064
|
+
// link before the retry so it cold-connects instead of stacking a
|
|
1065
|
+
// second connect onto the half-open one.
|
|
1066
|
+
if (
|
|
1067
|
+
err.errorCode === HardwareErrorCode.BleTimeoutError &&
|
|
1068
|
+
device.mainId &&
|
|
1069
|
+
device.deviceConnector
|
|
1070
|
+
) {
|
|
1071
|
+
await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
|
|
1072
|
+
device.markTransportDisconnected();
|
|
1073
|
+
}
|
|
1074
|
+
throw err;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
974
1077
|
}
|
|
975
1078
|
if (method.payload?.onlyConnectBleDevice) {
|
|
976
1079
|
if (shouldAcquire) {
|
|
@@ -1010,7 +1113,7 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
|
|
|
1010
1113
|
const nextRetry = retryCount + 1;
|
|
1011
1114
|
Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
|
|
1012
1115
|
await wait(3000);
|
|
1013
|
-
await connectDeviceForBle(method, device, nextRetry);
|
|
1116
|
+
await connectDeviceForBle(method, device, abortSignal, nextRetry);
|
|
1014
1117
|
} else {
|
|
1015
1118
|
throw err;
|
|
1016
1119
|
}
|
|
@@ -1120,7 +1223,7 @@ const ensureConnected = async (
|
|
|
1120
1223
|
if (tryCount === 1) {
|
|
1121
1224
|
device.beginConnectionAttempt();
|
|
1122
1225
|
}
|
|
1123
|
-
await connectDeviceForBle(method, device);
|
|
1226
|
+
await connectDeviceForBle(method, device, abortSignal);
|
|
1124
1227
|
}
|
|
1125
1228
|
resolve(device);
|
|
1126
1229
|
return;
|
|
@@ -1164,7 +1267,8 @@ const ensureConnected = async (
|
|
|
1164
1267
|
HardwareErrorCode.BridgeNeedsPermission,
|
|
1165
1268
|
HardwareErrorCode.DeviceInterruptedFromUser,
|
|
1166
1269
|
HardwareErrorCode.CallQueueActionCancelled,
|
|
1167
|
-
].includes(error.errorCode)
|
|
1270
|
+
].includes(error.errorCode) ||
|
|
1271
|
+
isProtocolV2PeerRemovedPairingError(method, error)
|
|
1168
1272
|
) {
|
|
1169
1273
|
reject(error);
|
|
1170
1274
|
return;
|
|
@@ -1220,6 +1324,9 @@ export const cancel = (context: CoreContext, connectId?: string) => {
|
|
|
1220
1324
|
','
|
|
1221
1325
|
)}`
|
|
1222
1326
|
);
|
|
1327
|
+
// Abort before rejecting: rejectRequest releases the task and would make
|
|
1328
|
+
// its AbortController unreachable to an in-flight method loop.
|
|
1329
|
+
requestQueue.abortRequestsByConnectId(connectId);
|
|
1223
1330
|
const canceledDevices: Device[] = [];
|
|
1224
1331
|
const interruptDevice = (device: Device | undefined, deviceConnectId: string) => {
|
|
1225
1332
|
if (!device || canceledDevices.includes(device)) {
|
|
@@ -1244,7 +1351,6 @@ export const cancel = (context: CoreContext, connectId?: string) => {
|
|
|
1244
1351
|
}
|
|
1245
1352
|
}
|
|
1246
1353
|
interruptDevice(deviceCacheMap.get(connectId), connectId);
|
|
1247
|
-
requestQueue.abortRequestsByConnectId(connectId);
|
|
1248
1354
|
pollingManager.stop(connectId);
|
|
1249
1355
|
} catch (e) {
|
|
1250
1356
|
Log.error('Cancel API Error: ', e);
|
|
@@ -1253,6 +1359,8 @@ export const cancel = (context: CoreContext, connectId?: string) => {
|
|
|
1253
1359
|
const env = DataManager.getSettings('env');
|
|
1254
1360
|
if (DataManager.isBleConnect(env)) {
|
|
1255
1361
|
Log.debug('Cancel Api all _deviceList: ');
|
|
1362
|
+
// Keep method abort signals observable until every active task is rejected.
|
|
1363
|
+
requestQueue.abortAllRequests();
|
|
1256
1364
|
const canceledDevices: Device[] = [];
|
|
1257
1365
|
const interruptDevice = (device?: Device) => {
|
|
1258
1366
|
if (!device || canceledDevices.includes(device)) {
|
|
@@ -1277,7 +1385,6 @@ export const cancel = (context: CoreContext, connectId?: string) => {
|
|
|
1277
1385
|
}
|
|
1278
1386
|
}
|
|
1279
1387
|
deviceCacheMap.forEach(interruptDevice);
|
|
1280
|
-
requestQueue.abortAllRequests();
|
|
1281
1388
|
pollingManager.stopAll();
|
|
1282
1389
|
} else {
|
|
1283
1390
|
_deviceList?.allDevices().forEach(device => {
|
|
@@ -1328,6 +1435,10 @@ const checkPassphraseEnableState = (method: BaseMethod, features?: Features) =>
|
|
|
1328
1435
|
const shouldCheckPassphraseState = (method: BaseMethod, device: Device) => {
|
|
1329
1436
|
if (!method.useDevicePassphraseState) return false;
|
|
1330
1437
|
|
|
1438
|
+
if (device.isProtocolV2() && method.payload?.useEmptyPassphrase === true) {
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1331
1442
|
return device.hasUsePassphrase();
|
|
1332
1443
|
};
|
|
1333
1444
|
|