@onekeyfe/hd-core 1.2.2-alpha.100 → 1.2.2-alpha.3
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 +2288 -1439
- 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 +5 -2
- 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 +130 -14
- 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) {
|
|
@@ -393,14 +393,17 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
393
393
|
// the root fingerprint, so each nested chain method must resume the
|
|
394
394
|
// requested standard or hidden wallet before sending its device command.
|
|
395
395
|
const useEmptyPassphrase = this.payload.useEmptyPassphrase === true;
|
|
396
|
-
|
|
396
|
+
// false would AskPassphrase with [Standard] only and permanently drop Cardano
|
|
397
|
+
// from a reused session. Omit the flag for non-Cardano nested methods.
|
|
398
|
+
const deriveCardano = method.name.startsWith('cardano') ? true : undefined;
|
|
397
399
|
const shouldResumeWalletSession = useEmptyPassphrase || !!this.payload.passphraseState;
|
|
398
400
|
if (this.device.isProtocolV2() && shouldResumeWalletSession) {
|
|
399
401
|
const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
|
|
400
402
|
this.payload.passphraseState,
|
|
401
403
|
useEmptyPassphrase,
|
|
402
404
|
this.payload.skipPassphraseCheck,
|
|
403
|
-
deriveCardano
|
|
405
|
+
deriveCardano,
|
|
406
|
+
this.protocolV2UnlockContext?.preflightMainPinSelected
|
|
404
407
|
);
|
|
405
408
|
if (!passphraseStateSafety) {
|
|
406
409
|
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
|
@@ -92,7 +92,7 @@ const preWarmDoneAt = new Map<string, number>();
|
|
|
92
92
|
|
|
93
93
|
export type CoreContext = ReturnType<Core['getCoreContext']>;
|
|
94
94
|
|
|
95
|
-
function
|
|
95
|
+
function resolveDeriveCardano(method: BaseMethod): boolean | undefined {
|
|
96
96
|
if (
|
|
97
97
|
method.name.startsWith('allNetworkGetAddress') &&
|
|
98
98
|
method.payload &&
|
|
@@ -102,15 +102,20 @@ function hasDeriveCardano(method: BaseMethod): boolean {
|
|
|
102
102
|
) {
|
|
103
103
|
return true;
|
|
104
104
|
}
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
if (method.name.startsWith('cardano')) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
if (typeof method.payload?.deriveCardano === 'boolean') {
|
|
109
|
+
return method.payload.deriveCardano;
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
const parseInitOptions = (method?: BaseMethod): InitOptions => ({
|
|
110
115
|
initSession: method?.payload.initSession,
|
|
111
116
|
passphraseState: method?.payload.useEmptyPassphrase ? undefined : method?.payload.passphraseState,
|
|
112
117
|
deviceId: method?.payload.deviceId,
|
|
113
|
-
deriveCardano: method
|
|
118
|
+
deriveCardano: method ? resolveDeriveCardano(method) : undefined,
|
|
114
119
|
connectProtocol: method?.payload.connectProtocol,
|
|
115
120
|
forceProtocolDetection: method?.payload.forceProtocolDetection,
|
|
116
121
|
protocolV2DeviceInfoTimeoutMs: method?.payload.protocolV2DeviceInfoTimeoutMs,
|
|
@@ -415,6 +420,13 @@ const onCallDevice = async (
|
|
|
415
420
|
if (method.payload?.onlyConnectBleDevice) {
|
|
416
421
|
preWarmCallbackTask?.resolve();
|
|
417
422
|
Log.debug('Call API - only connect ble device: ', device?.mainId);
|
|
423
|
+
// This early return bypasses the normal-path bookkeeping at the end of the
|
|
424
|
+
// call. Without it the task leaks and haunts every later queue snapshot
|
|
425
|
+
// and cancel sweep (field log: a completed task lingered for 6 minutes),
|
|
426
|
+
// and the request stays in the active maps, so repeated preconnects pile
|
|
427
|
+
// up phantom work in diagnostics.
|
|
428
|
+
completeMethodRequestContext(method);
|
|
429
|
+
requestQueue.releaseTask(method.responseID);
|
|
418
430
|
return createResponseMessage(method.responseID, true, null);
|
|
419
431
|
}
|
|
420
432
|
|
|
@@ -644,7 +656,8 @@ const onCallDevice = async (
|
|
|
644
656
|
method.payload?.passphraseState,
|
|
645
657
|
method.payload?.useEmptyPassphrase,
|
|
646
658
|
method.payload?.skipPassphraseCheck,
|
|
647
|
-
|
|
659
|
+
resolveDeriveCardano(method),
|
|
660
|
+
method.protocolV2UnlockContext?.preflightMainPinSelected
|
|
648
661
|
);
|
|
649
662
|
|
|
650
663
|
// Double check, handles the special case of Touch/Pro
|
|
@@ -950,11 +963,72 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
|
|
|
950
963
|
);
|
|
951
964
|
}
|
|
952
965
|
|
|
966
|
+
export function isProtocolV2PeerRemovedPairingError(method: BaseMethod, error: unknown) {
|
|
967
|
+
return (
|
|
968
|
+
method.payload.connectProtocol === 'V2' &&
|
|
969
|
+
(error as { errorCode?: unknown })?.errorCode ===
|
|
970
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
953
974
|
/**
|
|
954
975
|
* If the Bluetooth connection times out, retry up to 6 times
|
|
955
976
|
* @param retryCount - Current retry count (default 0)
|
|
956
977
|
*/
|
|
957
|
-
|
|
978
|
+
// device.acquire awaits a transport reply with no deadline of its own; a
|
|
979
|
+
// transport that never settles (field case: Electron main lost an IPC reply,
|
|
980
|
+
// "reply was never sent" after 5 minutes) hangs the call forever and cancel()
|
|
981
|
+
// only takes effect at poll checkpoints. Race acquire against a deadline and
|
|
982
|
+
// the caller's abort signal so the hang is bounded and cancel is immediate.
|
|
983
|
+
const BLE_ACQUIRE_DEADLINE_MS = 60 * 1000;
|
|
984
|
+
|
|
985
|
+
function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal): Promise<T> {
|
|
986
|
+
return new Promise<T>((resolve, reject) => {
|
|
987
|
+
let settled = false;
|
|
988
|
+
const settle = (fn: () => void) => {
|
|
989
|
+
if (settled) return;
|
|
990
|
+
settled = true;
|
|
991
|
+
clearTimeout(deadline);
|
|
992
|
+
abortSignal?.removeEventListener('abort', onAbort);
|
|
993
|
+
fn();
|
|
994
|
+
};
|
|
995
|
+
const onAbort = () =>
|
|
996
|
+
settle(() => reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)));
|
|
997
|
+
const deadline = setTimeout(
|
|
998
|
+
() =>
|
|
999
|
+
settle(() =>
|
|
1000
|
+
reject(
|
|
1001
|
+
ERRORS.TypedError(
|
|
1002
|
+
HardwareErrorCode.BleTimeoutError,
|
|
1003
|
+
`BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
|
|
1004
|
+
)
|
|
1005
|
+
)
|
|
1006
|
+
),
|
|
1007
|
+
BLE_ACQUIRE_DEADLINE_MS
|
|
1008
|
+
);
|
|
1009
|
+
// Attach before any early return so a late settlement of acquirePromise
|
|
1010
|
+
// is always consumed — an abort or deadline must never leave the acquire
|
|
1011
|
+
// rejection unhandled.
|
|
1012
|
+
acquirePromise.then(
|
|
1013
|
+
value => settle(() => resolve(value)),
|
|
1014
|
+
error => settle(() => reject(error))
|
|
1015
|
+
);
|
|
1016
|
+
if (abortSignal) {
|
|
1017
|
+
if (abortSignal.aborted) {
|
|
1018
|
+
onAbort();
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
abortSignal.addEventListener('abort', onAbort);
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function connectDeviceForBle(
|
|
1027
|
+
method: BaseMethod,
|
|
1028
|
+
device: Device,
|
|
1029
|
+
abortSignal?: AbortSignal,
|
|
1030
|
+
retryCount = 0
|
|
1031
|
+
) {
|
|
958
1032
|
try {
|
|
959
1033
|
if (device.wasInterruptedByUser()) {
|
|
960
1034
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
@@ -968,9 +1042,43 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
|
|
|
968
1042
|
!device.commands ||
|
|
969
1043
|
device.commands.disposed;
|
|
970
1044
|
if (shouldAcquire) {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1045
|
+
// The deadline/abort guards are scoped to the desktop electron
|
|
1046
|
+
// transport: its IPC acquire is the only path with a proven
|
|
1047
|
+
// never-settling failure mode, while react-native/lowlevel acquire may
|
|
1048
|
+
// legitimately block on a user-driven system bonding prompt for longer
|
|
1049
|
+
// than any sane deadline. Other envs keep the plain acquire unchanged.
|
|
1050
|
+
const useAcquireGuards = DataManager.getSettings('env') === 'desktop-web-ble';
|
|
1051
|
+
// A cancel landing during the retry backoff must not start a new acquire.
|
|
1052
|
+
if (useAcquireGuards && abortSignal?.aborted) {
|
|
1053
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
1054
|
+
}
|
|
1055
|
+
if (!useAcquireGuards) {
|
|
1056
|
+
await device.acquire(method.payload.connectProtocol, {
|
|
1057
|
+
forceProtocolDetection: method.payload.forceProtocolDetection,
|
|
1058
|
+
});
|
|
1059
|
+
} else {
|
|
1060
|
+
try {
|
|
1061
|
+
await raceBleAcquire(
|
|
1062
|
+
device.acquire(method.payload.connectProtocol, {
|
|
1063
|
+
forceProtocolDetection: method.payload.forceProtocolDetection,
|
|
1064
|
+
}),
|
|
1065
|
+
abortSignal
|
|
1066
|
+
);
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
// A deadline hit means the transport is wedged mid-acquire; drop the
|
|
1069
|
+
// link before the retry so it cold-connects instead of stacking a
|
|
1070
|
+
// second connect onto the half-open one.
|
|
1071
|
+
if (
|
|
1072
|
+
err.errorCode === HardwareErrorCode.BleTimeoutError &&
|
|
1073
|
+
device.mainId &&
|
|
1074
|
+
device.deviceConnector
|
|
1075
|
+
) {
|
|
1076
|
+
await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
|
|
1077
|
+
device.markTransportDisconnected();
|
|
1078
|
+
}
|
|
1079
|
+
throw err;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
974
1082
|
}
|
|
975
1083
|
if (method.payload?.onlyConnectBleDevice) {
|
|
976
1084
|
if (shouldAcquire) {
|
|
@@ -1010,7 +1118,7 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
|
|
|
1010
1118
|
const nextRetry = retryCount + 1;
|
|
1011
1119
|
Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
|
|
1012
1120
|
await wait(3000);
|
|
1013
|
-
await connectDeviceForBle(method, device, nextRetry);
|
|
1121
|
+
await connectDeviceForBle(method, device, abortSignal, nextRetry);
|
|
1014
1122
|
} else {
|
|
1015
1123
|
throw err;
|
|
1016
1124
|
}
|
|
@@ -1120,7 +1228,7 @@ const ensureConnected = async (
|
|
|
1120
1228
|
if (tryCount === 1) {
|
|
1121
1229
|
device.beginConnectionAttempt();
|
|
1122
1230
|
}
|
|
1123
|
-
await connectDeviceForBle(method, device);
|
|
1231
|
+
await connectDeviceForBle(method, device, abortSignal);
|
|
1124
1232
|
}
|
|
1125
1233
|
resolve(device);
|
|
1126
1234
|
return;
|
|
@@ -1164,7 +1272,8 @@ const ensureConnected = async (
|
|
|
1164
1272
|
HardwareErrorCode.BridgeNeedsPermission,
|
|
1165
1273
|
HardwareErrorCode.DeviceInterruptedFromUser,
|
|
1166
1274
|
HardwareErrorCode.CallQueueActionCancelled,
|
|
1167
|
-
].includes(error.errorCode)
|
|
1275
|
+
].includes(error.errorCode) ||
|
|
1276
|
+
isProtocolV2PeerRemovedPairingError(method, error)
|
|
1168
1277
|
) {
|
|
1169
1278
|
reject(error);
|
|
1170
1279
|
return;
|
|
@@ -1220,6 +1329,9 @@ export const cancel = (context: CoreContext, connectId?: string) => {
|
|
|
1220
1329
|
','
|
|
1221
1330
|
)}`
|
|
1222
1331
|
);
|
|
1332
|
+
// Abort before rejecting: rejectRequest releases the task and would make
|
|
1333
|
+
// its AbortController unreachable to an in-flight method loop.
|
|
1334
|
+
requestQueue.abortRequestsByConnectId(connectId);
|
|
1223
1335
|
const canceledDevices: Device[] = [];
|
|
1224
1336
|
const interruptDevice = (device: Device | undefined, deviceConnectId: string) => {
|
|
1225
1337
|
if (!device || canceledDevices.includes(device)) {
|
|
@@ -1244,7 +1356,6 @@ export const cancel = (context: CoreContext, connectId?: string) => {
|
|
|
1244
1356
|
}
|
|
1245
1357
|
}
|
|
1246
1358
|
interruptDevice(deviceCacheMap.get(connectId), connectId);
|
|
1247
|
-
requestQueue.abortRequestsByConnectId(connectId);
|
|
1248
1359
|
pollingManager.stop(connectId);
|
|
1249
1360
|
} catch (e) {
|
|
1250
1361
|
Log.error('Cancel API Error: ', e);
|
|
@@ -1253,6 +1364,8 @@ export const cancel = (context: CoreContext, connectId?: string) => {
|
|
|
1253
1364
|
const env = DataManager.getSettings('env');
|
|
1254
1365
|
if (DataManager.isBleConnect(env)) {
|
|
1255
1366
|
Log.debug('Cancel Api all _deviceList: ');
|
|
1367
|
+
// Keep method abort signals observable until every active task is rejected.
|
|
1368
|
+
requestQueue.abortAllRequests();
|
|
1256
1369
|
const canceledDevices: Device[] = [];
|
|
1257
1370
|
const interruptDevice = (device?: Device) => {
|
|
1258
1371
|
if (!device || canceledDevices.includes(device)) {
|
|
@@ -1277,7 +1390,6 @@ export const cancel = (context: CoreContext, connectId?: string) => {
|
|
|
1277
1390
|
}
|
|
1278
1391
|
}
|
|
1279
1392
|
deviceCacheMap.forEach(interruptDevice);
|
|
1280
|
-
requestQueue.abortAllRequests();
|
|
1281
1393
|
pollingManager.stopAll();
|
|
1282
1394
|
} else {
|
|
1283
1395
|
_deviceList?.allDevices().forEach(device => {
|
|
@@ -1328,6 +1440,10 @@ const checkPassphraseEnableState = (method: BaseMethod, features?: Features) =>
|
|
|
1328
1440
|
const shouldCheckPassphraseState = (method: BaseMethod, device: Device) => {
|
|
1329
1441
|
if (!method.useDevicePassphraseState) return false;
|
|
1330
1442
|
|
|
1443
|
+
if (device.isProtocolV2() && method.payload?.useEmptyPassphrase === true) {
|
|
1444
|
+
return true;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1331
1447
|
return device.hasUsePassphrase();
|
|
1332
1448
|
};
|
|
1333
1449
|
|