@onekeyfe/hd-core 1.2.0-alpha.107 → 1.2.0-alpha.108
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__/DeviceCommands.test.ts +140 -26
- package/__tests__/base64Data.test.ts +70 -0
- package/__tests__/device-lifecycle-events.test.ts +113 -2
- package/__tests__/device-pool-state.test.ts +25 -0
- package/__tests__/deviceSettings.test.ts +0 -1
- package/__tests__/deviceUploadNft.test.ts +33 -4
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +178 -0
- package/__tests__/get-device-state.test.ts +52 -13
- package/__tests__/logBlockEvent.test.ts +13 -1
- package/__tests__/protocol-v2.test.ts +762 -124
- package/__tests__/refresh-device-state.test.ts +3 -1
- package/__tests__/resourceBase64Boundary.test.ts +48 -0
- package/__tests__/ton-sign-message.test.ts +84 -0
- package/dist/api/FirmwareUpdateV4.d.ts +10 -3
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/helpers/base64Data.d.ts +16 -0
- package/dist/api/helpers/base64Data.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +7 -2
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +11 -14
- package/dist/index.js +455 -200
- package/dist/protocols/protocol-v2/features.d.ts +9 -1
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/index.d.ts +2 -2
- package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +1 -1
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/utils/pro2Nft.d.ts +7 -0
- package/dist/utils/pro2Nft.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/api/FirmwareUpdateV4.ts +326 -174
- package/src/api/UploadPortfolio.ts +9 -2
- package/src/api/helpers/base64Data.ts +85 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +56 -8
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +37 -18
- package/src/api/ton/TonSignMessage.ts +5 -3
- package/src/core/index.ts +6 -2
- package/src/device/Device.ts +53 -31
- package/src/device/DeviceCommands.ts +4 -14
- package/src/device/DevicePool.ts +6 -2
- package/src/events/logBlockEvent.ts +14 -1
- package/src/protocols/protocol-v2/features.ts +19 -2
- package/src/protocols/protocol-v2/index.ts +2 -0
- package/src/types/api/protocolV2.ts +1 -1
- package/src/utils/deviceSettings.ts +1 -1
- package/src/utils/pro2Nft.ts +31 -8
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { createDeviceNotSupportMethodError } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import { supportsProtocolV2Message } from '../protocols/protocol-v2/features';
|
|
4
|
+
import { decodeCanonicalBase64 } from './helpers/base64Data';
|
|
4
5
|
import FileWrite from './FileWrite';
|
|
5
6
|
|
|
6
7
|
export type UploadPortfolioParams = {
|
|
7
|
-
|
|
8
|
+
packageBase64: string;
|
|
8
9
|
timeoutMs?: number | string;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
const PORTFOLIO_PENDING_PATH = 'vol1:/portfolio/portfolio.okpkg.pending';
|
|
12
13
|
const PORTFOLIO_CHUNK_SIZE = 2048;
|
|
14
|
+
const PORTFOLIO_PACKAGE_MAX_BYTES = 128 * 1024;
|
|
13
15
|
const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
|
|
14
16
|
const PORTFOLIO_UPDATE_MESSAGE_TYPE = 61400;
|
|
15
17
|
|
|
16
18
|
export default class UploadPortfolio extends FileWrite {
|
|
17
19
|
init() {
|
|
18
|
-
const {
|
|
20
|
+
const { packageBase64, timeoutMs } = this.payload as UploadPortfolioParams;
|
|
21
|
+
const packageBytes = decodeCanonicalBase64({
|
|
22
|
+
value: packageBase64,
|
|
23
|
+
parameterName: 'packageBase64',
|
|
24
|
+
maxBytes: PORTFOLIO_PACKAGE_MAX_BYTES,
|
|
25
|
+
});
|
|
19
26
|
this.payload = {
|
|
20
27
|
...this.payload,
|
|
21
28
|
path: PORTFOLIO_PENDING_PATH,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { decode as decodeJpeg } from 'jpeg-js';
|
|
3
|
+
|
|
4
|
+
import { invalidParameter } from './filesystemValidation';
|
|
5
|
+
|
|
6
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
7
|
+
const JPEG_CONTAINER_OVERHEAD_BYTES = 64 * 1024;
|
|
8
|
+
const JPEG_MAX_MEMORY_USAGE_IN_MB = 32;
|
|
9
|
+
const JPEG_MAX_RESOLUTION_IN_MP = 1;
|
|
10
|
+
|
|
11
|
+
export function decodeCanonicalBase64({
|
|
12
|
+
value,
|
|
13
|
+
parameterName,
|
|
14
|
+
maxBytes,
|
|
15
|
+
}: {
|
|
16
|
+
value: unknown;
|
|
17
|
+
parameterName: string;
|
|
18
|
+
maxBytes: number;
|
|
19
|
+
}): Uint8Array {
|
|
20
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
21
|
+
throw invalidParameter(`Parameter [${parameterName}] must be a non-empty Base64 string.`);
|
|
22
|
+
}
|
|
23
|
+
if (value.length > Math.ceil(maxBytes / 3) * 4) {
|
|
24
|
+
throw invalidParameter(`Parameter [${parameterName}] exceeds the maximum supported size.`);
|
|
25
|
+
}
|
|
26
|
+
if (value.length % 4 !== 0 || !BASE64_PATTERN.test(value)) {
|
|
27
|
+
throw invalidParameter(`Parameter [${parameterName}] must use canonical Base64 encoding.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const decoded = Buffer.from(value, 'base64');
|
|
31
|
+
if (decoded.byteLength === 0 || decoded.byteLength > maxBytes) {
|
|
32
|
+
throw invalidParameter(`Parameter [${parameterName}] exceeds the maximum supported size.`);
|
|
33
|
+
}
|
|
34
|
+
if (decoded.toString('base64') !== value) {
|
|
35
|
+
throw invalidParameter(`Parameter [${parameterName}] must use canonical Base64 encoding.`);
|
|
36
|
+
}
|
|
37
|
+
return Uint8Array.from(decoded);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function decodeJpegBase64ToRgba({
|
|
41
|
+
jpegBase64,
|
|
42
|
+
parameterName,
|
|
43
|
+
expectedWidth,
|
|
44
|
+
expectedHeight,
|
|
45
|
+
}: {
|
|
46
|
+
jpegBase64: unknown;
|
|
47
|
+
parameterName: string;
|
|
48
|
+
expectedWidth: number;
|
|
49
|
+
expectedHeight: number;
|
|
50
|
+
}): { width: number; height: number; data: Uint8Array } {
|
|
51
|
+
const jpegBytes = decodeCanonicalBase64({
|
|
52
|
+
value: jpegBase64,
|
|
53
|
+
parameterName,
|
|
54
|
+
maxBytes: expectedWidth * expectedHeight * 8 + JPEG_CONTAINER_OVERHEAD_BYTES,
|
|
55
|
+
});
|
|
56
|
+
if (jpegBytes[0] !== 0xff || jpegBytes[1] !== 0xd8) {
|
|
57
|
+
throw invalidParameter(`Parameter [${parameterName}] must contain a JPEG image.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let decoded: { width: number; height: number; data: Uint8Array };
|
|
61
|
+
try {
|
|
62
|
+
decoded = decodeJpeg(jpegBytes, {
|
|
63
|
+
useTArray: true,
|
|
64
|
+
formatAsRGBA: true,
|
|
65
|
+
tolerantDecoding: false,
|
|
66
|
+
maxResolutionInMP: JPEG_MAX_RESOLUTION_IN_MP,
|
|
67
|
+
maxMemoryUsageInMB: JPEG_MAX_MEMORY_USAGE_IN_MB,
|
|
68
|
+
});
|
|
69
|
+
} catch {
|
|
70
|
+
throw invalidParameter(`Parameter [${parameterName}] must contain a valid JPEG image.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (decoded.width !== expectedWidth || decoded.height !== expectedHeight) {
|
|
74
|
+
throw invalidParameter(
|
|
75
|
+
`Parameter [${parameterName}] must contain a ${expectedWidth}x${expectedHeight} JPEG image.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const expectedLength = expectedWidth * expectedHeight * 4;
|
|
79
|
+
if (decoded.data.byteLength !== expectedLength) {
|
|
80
|
+
throw invalidParameter(
|
|
81
|
+
`Decoded parameter [${parameterName}] must contain ${expectedLength} RGBA bytes.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return decoded;
|
|
85
|
+
}
|
|
@@ -7,21 +7,26 @@ import {
|
|
|
7
7
|
PRO2_NFT_DEFAULT_PACE_MS,
|
|
8
8
|
PRO2_NFT_DEFAULT_TIMEOUT_MS,
|
|
9
9
|
PRO2_NFT_DIRECTORY,
|
|
10
|
+
PRO2_NFT_IMAGE_HEIGHT,
|
|
11
|
+
PRO2_NFT_IMAGE_WIDTH,
|
|
10
12
|
PRO2_NFT_MAX_CHUNK_SIZE,
|
|
11
13
|
PRO2_NFT_MAX_ITEMS,
|
|
12
14
|
PRO2_NFT_MIN_CHUNK_SIZE,
|
|
15
|
+
PRO2_NFT_THUMBNAIL_HEIGHT,
|
|
16
|
+
PRO2_NFT_THUMBNAIL_WIDTH,
|
|
13
17
|
type Pro2NftBundle,
|
|
14
|
-
|
|
15
|
-
buildPro2NftBundle,
|
|
18
|
+
buildPro2NftBundleFromEncodedImages,
|
|
16
19
|
getCompletePro2NftBasenames,
|
|
17
20
|
} from '../../utils/pro2Nft';
|
|
21
|
+
import { encodePro2Image } from '../../utils/pro2Wallpaper';
|
|
18
22
|
import { BaseMethod } from '../BaseMethod';
|
|
23
|
+
import { decodeJpegBase64ToRgba } from '../helpers/base64Data';
|
|
19
24
|
import { invalidParameter } from '../helpers/filesystemValidation';
|
|
20
25
|
import { writeProtocolV2File } from '../helpers/protocolV2FileWrite';
|
|
21
26
|
|
|
22
27
|
export type DeviceUploadNftParams = {
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
imageJpegBase64: string;
|
|
29
|
+
thumbnailJpegBase64: string;
|
|
25
30
|
title: string;
|
|
26
31
|
subtitle: string;
|
|
27
32
|
timestampMs?: number;
|
|
@@ -54,8 +59,8 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
54
59
|
|
|
55
60
|
init() {
|
|
56
61
|
const {
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
imageJpegBase64,
|
|
63
|
+
thumbnailJpegBase64,
|
|
59
64
|
title,
|
|
60
65
|
subtitle,
|
|
61
66
|
timestampMs = Date.now(),
|
|
@@ -79,8 +84,51 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
79
84
|
throw invalidParameter('Parameter [timeoutMs] must be a positive integer.');
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const encodedImage = (() => {
|
|
88
|
+
const decoded = decodeJpegBase64ToRgba({
|
|
89
|
+
jpegBase64: imageJpegBase64,
|
|
90
|
+
parameterName: 'imageJpegBase64',
|
|
91
|
+
expectedWidth: PRO2_NFT_IMAGE_WIDTH,
|
|
92
|
+
expectedHeight: PRO2_NFT_IMAGE_HEIGHT,
|
|
93
|
+
});
|
|
94
|
+
return encodePro2Image({
|
|
95
|
+
width: PRO2_NFT_IMAGE_WIDTH,
|
|
96
|
+
height: PRO2_NFT_IMAGE_HEIGHT,
|
|
97
|
+
rgba: decoded.data,
|
|
98
|
+
alphaMode: 'black-background',
|
|
99
|
+
}).data;
|
|
100
|
+
})();
|
|
101
|
+
const encodedThumbnail = (() => {
|
|
102
|
+
const decoded = decodeJpegBase64ToRgba({
|
|
103
|
+
jpegBase64: thumbnailJpegBase64,
|
|
104
|
+
parameterName: 'thumbnailJpegBase64',
|
|
105
|
+
expectedWidth: PRO2_NFT_THUMBNAIL_WIDTH,
|
|
106
|
+
expectedHeight: PRO2_NFT_THUMBNAIL_HEIGHT,
|
|
107
|
+
});
|
|
108
|
+
return encodePro2Image({
|
|
109
|
+
width: PRO2_NFT_THUMBNAIL_WIDTH,
|
|
110
|
+
height: PRO2_NFT_THUMBNAIL_HEIGHT,
|
|
111
|
+
rgba: decoded.data,
|
|
112
|
+
alphaMode: 'black-background',
|
|
113
|
+
}).data;
|
|
114
|
+
})();
|
|
115
|
+
this.bundle = buildPro2NftBundleFromEncodedImages({
|
|
116
|
+
image: encodedImage,
|
|
117
|
+
thumbnail: encodedThumbnail,
|
|
118
|
+
title,
|
|
119
|
+
subtitle,
|
|
120
|
+
timestampMs,
|
|
121
|
+
});
|
|
122
|
+
this.params = {
|
|
123
|
+
imageJpegBase64,
|
|
124
|
+
thumbnailJpegBase64,
|
|
125
|
+
title,
|
|
126
|
+
subtitle,
|
|
127
|
+
timestampMs,
|
|
128
|
+
chunkSize,
|
|
129
|
+
paceMs,
|
|
130
|
+
timeoutMs,
|
|
131
|
+
};
|
|
84
132
|
this.unlockPolicy = 'none';
|
|
85
133
|
this.skipForceUpdateCheck = true;
|
|
86
134
|
this.useDevicePassphraseState = false;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { blake2s } from '@noble/hashes/blake2s';
|
|
2
2
|
import { bytesToHex } from '@noble/hashes/utils';
|
|
3
|
+
import { createDeviceNotSupportMethodError } from '@onekeyfe/hd-shared';
|
|
3
4
|
|
|
4
5
|
import { BaseMethod } from '../BaseMethod';
|
|
6
|
+
import { decodeJpegBase64ToRgba } from '../helpers/base64Data';
|
|
5
7
|
import { invalidParameter } from '../helpers/filesystemValidation';
|
|
6
8
|
import { writeProtocolV2File } from '../helpers/protocolV2FileWrite';
|
|
7
9
|
import { UI_REQUEST, createUiMessage } from '../../events/ui-request';
|
|
10
|
+
import { supportsProtocolV2Message } from '../../protocols/protocol-v2/features';
|
|
8
11
|
import {
|
|
9
12
|
PRO2_WALLPAPER_HEIGHT,
|
|
10
13
|
PRO2_WALLPAPER_WIDTH,
|
|
@@ -13,9 +16,7 @@ import {
|
|
|
13
16
|
} from '../../utils/pro2Wallpaper';
|
|
14
17
|
|
|
15
18
|
export type DeviceUploadWallpaperParams = {
|
|
16
|
-
|
|
17
|
-
height: number;
|
|
18
|
-
rgba: Uint8Array | ArrayBuffer;
|
|
19
|
+
jpegBase64: string;
|
|
19
20
|
fileName?: string;
|
|
20
21
|
chunkSize?: number;
|
|
21
22
|
};
|
|
@@ -29,6 +30,9 @@ export type DeviceUploadWallpaperResponse = {
|
|
|
29
30
|
|
|
30
31
|
const WALLPAPER_DIRECTORY = 'vol1:/wallpapers';
|
|
31
32
|
const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/;
|
|
33
|
+
const DEVICE_SETTINGS_SET_MESSAGE_TYPE = 60412;
|
|
34
|
+
const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
|
|
35
|
+
const FILESYSTEM_DIR_MAKE_MESSAGE_TYPE = 60809;
|
|
32
36
|
|
|
33
37
|
function normalizeFileName(fileName: string | undefined, data: Uint8Array): string {
|
|
34
38
|
if (fileName !== undefined && (!fileName || !SAFE_FILE_NAME.test(fileName))) {
|
|
@@ -54,31 +58,45 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
|
|
|
54
58
|
private path = '';
|
|
55
59
|
|
|
56
60
|
init() {
|
|
57
|
-
const {
|
|
58
|
-
if (width !== PRO2_WALLPAPER_WIDTH || height !== PRO2_WALLPAPER_HEIGHT) {
|
|
59
|
-
throw invalidParameter(
|
|
60
|
-
`Pro2 wallpaper dimensions must be ${PRO2_WALLPAPER_WIDTH}x${PRO2_WALLPAPER_HEIGHT}.`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
if (!(rgba instanceof ArrayBuffer) && !ArrayBuffer.isView(rgba)) {
|
|
64
|
-
throw invalidParameter('Parameter [rgba] must be an ArrayBuffer or Uint8Array.');
|
|
65
|
-
}
|
|
61
|
+
const { jpegBase64, fileName, chunkSize } = this.payload;
|
|
66
62
|
if (chunkSize !== undefined && (!Number.isInteger(chunkSize) || chunkSize <= 0)) {
|
|
67
63
|
throw invalidParameter('Parameter [chunkSize] must be a positive integer.');
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
const decoded = decodeJpegBase64ToRgba({
|
|
67
|
+
jpegBase64,
|
|
68
|
+
parameterName: 'jpegBase64',
|
|
69
|
+
expectedWidth: PRO2_WALLPAPER_WIDTH,
|
|
70
|
+
expectedHeight: PRO2_WALLPAPER_HEIGHT,
|
|
71
|
+
});
|
|
72
|
+
this.encoded = encodePro2Wallpaper({
|
|
73
|
+
width: PRO2_WALLPAPER_WIDTH,
|
|
74
|
+
height: PRO2_WALLPAPER_HEIGHT,
|
|
75
|
+
rgba: decoded.data,
|
|
76
|
+
});
|
|
75
77
|
this.path = `${WALLPAPER_DIRECTORY}/${normalizeFileName(fileName, this.encoded.data)}`;
|
|
76
|
-
this.params = {
|
|
78
|
+
this.params = { jpegBase64, fileName, chunkSize };
|
|
77
79
|
this.unlockPolicy = 'none';
|
|
78
80
|
this.skipForceUpdateCheck = true;
|
|
79
81
|
this.useDevicePassphraseState = false;
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
private async assertCapabilities() {
|
|
85
|
+
const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
|
|
86
|
+
const requiredMessageTypes = [
|
|
87
|
+
DEVICE_SETTINGS_SET_MESSAGE_TYPE,
|
|
88
|
+
FILESYSTEM_FILE_WRITE_MESSAGE_TYPE,
|
|
89
|
+
FILESYSTEM_DIR_MAKE_MESSAGE_TYPE,
|
|
90
|
+
];
|
|
91
|
+
if (
|
|
92
|
+
requiredMessageTypes.some(
|
|
93
|
+
messageType => !supportsProtocolV2Message(protocolInfo, messageType)
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
82
100
|
private async ensureDirectory() {
|
|
83
101
|
if (this.directoryReady) return;
|
|
84
102
|
try {
|
|
@@ -119,6 +137,7 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
|
|
|
119
137
|
async run(): Promise<DeviceUploadWallpaperResponse> {
|
|
120
138
|
const { encoded } = this;
|
|
121
139
|
if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.');
|
|
140
|
+
await this.assertCapabilities();
|
|
122
141
|
await this.ensureDirectory();
|
|
123
142
|
await this.upload();
|
|
124
143
|
const response = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
|
|
@@ -174,12 +174,14 @@ export default class TonSignMessage extends BaseMethod<HardwareTonSignMessage> {
|
|
|
174
174
|
if (!request.init_data_length) {
|
|
175
175
|
const deviceType = this.device.getCurrentDeviceType();
|
|
176
176
|
const hasClassic = DeviceModelToTypes.model_classic1s.includes(deviceType);
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
const signingMessage = request.signing_message ?? request.signning_message;
|
|
178
|
+
// Blind signing omits the signing message in both protocol schemas.
|
|
179
|
+
const shouldSkipValidation = signingMessage == null;
|
|
179
180
|
|
|
180
181
|
return Promise.resolve({
|
|
181
182
|
...request,
|
|
182
|
-
|
|
183
|
+
signing_message: signingMessage,
|
|
184
|
+
skip_validate: hasClassic || shouldSkipValidation,
|
|
183
185
|
});
|
|
184
186
|
}
|
|
185
187
|
|
package/src/core/index.ts
CHANGED
|
@@ -136,6 +136,9 @@ const toError = (error: unknown): Error | undefined => {
|
|
|
136
136
|
}
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
+
const isExpectedCompatibilityError = (error: unknown) =>
|
|
140
|
+
error instanceof HardwareError && error.errorCode === HardwareErrorCode.DeviceNotSupportMethod;
|
|
141
|
+
|
|
139
142
|
const updateMethodRequestContext = (method: BaseMethod, updates: any) => {
|
|
140
143
|
if (method.requestContext) {
|
|
141
144
|
updateRequestContext(method.requestContext.responseID, updates);
|
|
@@ -693,7 +696,9 @@ const onCallDevice = async (
|
|
|
693
696
|
try {
|
|
694
697
|
return await task.callPromise.promise;
|
|
695
698
|
} catch (e) {
|
|
696
|
-
|
|
699
|
+
if (!isExpectedCompatibilityError(e)) {
|
|
700
|
+
Log.debug('Device Run Error: ', e);
|
|
701
|
+
}
|
|
697
702
|
completeMethodRequestContext(method, e);
|
|
698
703
|
return createResponseMessage(method.responseID, false, { error: e });
|
|
699
704
|
}
|
|
@@ -1248,7 +1253,6 @@ const cleanup = () => {
|
|
|
1248
1253
|
pendingUiPromises,
|
|
1249
1254
|
ERRORS.TypedError(HardwareErrorCode.ActionCancelled, 'UI request was cancelled')
|
|
1250
1255
|
);
|
|
1251
|
-
Log.debug('Cleanup...');
|
|
1252
1256
|
};
|
|
1253
1257
|
|
|
1254
1258
|
const removeDeviceListener = (device: Device) => {
|
package/src/device/Device.ts
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
|
|
62
62
|
type ProtocolV2RuntimeMode,
|
|
63
63
|
getProtocolV2RuntimeMode,
|
|
64
|
+
isLegacyProtocolV2ProtocolInfo,
|
|
64
65
|
requestProtocolV2DeviceInfo,
|
|
65
66
|
requestProtocolV2DeviceStatus,
|
|
66
67
|
requestProtocolV2ProtocolInfo,
|
|
@@ -485,6 +486,10 @@ export class Device extends EventEmitter {
|
|
|
485
486
|
}
|
|
486
487
|
|
|
487
488
|
this.commands = new DeviceCommands(this, this.mainId ?? '');
|
|
489
|
+
// Protocol V2 runtime metadata belongs to one active transport link. A
|
|
490
|
+
// successful acquire creates a fresh link/session, so never carry cached
|
|
491
|
+
// ProtocolInfo or pre-initialize state across that boundary.
|
|
492
|
+
this.invalidateProtocolV2RuntimeState();
|
|
488
493
|
} catch (error) {
|
|
489
494
|
if (options?.forceProtocolDetection) {
|
|
490
495
|
this.originalDescriptor.protocolType = previousProtocol;
|
|
@@ -977,11 +982,7 @@ export class Device extends EventEmitter {
|
|
|
977
982
|
});
|
|
978
983
|
// The default request excludes SE/hash data and therefore uses basic scope.
|
|
979
984
|
// Full version and verification data require getDeviceState({ scope: 'firmware' }).
|
|
980
|
-
|
|
981
|
-
deviceInfo,
|
|
982
|
-
options?.protocolV2DeviceInfoTimeoutMs
|
|
983
|
-
);
|
|
984
|
-
Log.debug('Protocol V2 features:', features);
|
|
985
|
+
await this.probeProtocolV2RuntimeState(deviceInfo, options?.protocolV2DeviceInfoTimeoutMs);
|
|
985
986
|
} catch (error) {
|
|
986
987
|
Log.error('Protocol V2 initialization failed:', error);
|
|
987
988
|
throw error;
|
|
@@ -1053,7 +1054,13 @@ export class Device extends EventEmitter {
|
|
|
1053
1054
|
}
|
|
1054
1055
|
|
|
1055
1056
|
if (refresh.has('status') && !initializedWithDeviceInfo) {
|
|
1056
|
-
|
|
1057
|
+
const cachedMode = this.state?.status.mode;
|
|
1058
|
+
await this.probeProtocolV2RuntimeState(refreshedDeviceInfo, undefined, {
|
|
1059
|
+
// Loader firmware does not support DeviceStatusGet. Renegotiate ProtocolInfo
|
|
1060
|
+
// during an explicit refresh so a device rebooted into application firmware
|
|
1061
|
+
// can leave the cached loader state.
|
|
1062
|
+
forceRuntimeContextRefresh: cachedMode === 'bootloader' || cachedMode === 'romloader',
|
|
1063
|
+
});
|
|
1057
1064
|
}
|
|
1058
1065
|
|
|
1059
1066
|
if (refresh.has('settings') && this.state?.status.mode === 'normal') {
|
|
@@ -1101,9 +1108,10 @@ export class Device extends EventEmitter {
|
|
|
1101
1108
|
source,
|
|
1102
1109
|
changedKeys: result.changedKeys,
|
|
1103
1110
|
};
|
|
1104
|
-
Log.debug('Device state
|
|
1111
|
+
Log.debug('Device state updated', {
|
|
1105
1112
|
source,
|
|
1106
|
-
|
|
1113
|
+
revision: result.revision,
|
|
1114
|
+
changedKeyCount: result.changedKeys.length,
|
|
1107
1115
|
});
|
|
1108
1116
|
this.emit(DEVICE.STATE, this, event);
|
|
1109
1117
|
if (result.state.protocol === 'V1') {
|
|
@@ -1124,12 +1132,17 @@ export class Device extends EventEmitter {
|
|
|
1124
1132
|
return this.features;
|
|
1125
1133
|
}
|
|
1126
1134
|
|
|
1127
|
-
async ensureProtocolV2RuntimeContext(
|
|
1135
|
+
async ensureProtocolV2RuntimeContext(
|
|
1136
|
+
timeoutMs?: number,
|
|
1137
|
+
options?: { forceRefresh?: boolean }
|
|
1138
|
+
): Promise<ProtocolInfo> {
|
|
1128
1139
|
const cachedProtocolInfo =
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1140
|
+
options?.forceRefresh === true
|
|
1141
|
+
? undefined
|
|
1142
|
+
: this.protocolV2RuntimeContext ??
|
|
1143
|
+
(!this.protocolV2StateNeedsReload
|
|
1144
|
+
? this.state?.raw?.protocolV2ProtocolInfo ?? undefined
|
|
1145
|
+
: undefined);
|
|
1133
1146
|
if (cachedProtocolInfo) {
|
|
1134
1147
|
this.protocolV2RuntimeContext = cachedProtocolInfo;
|
|
1135
1148
|
return cachedProtocolInfo;
|
|
@@ -1169,10 +1182,19 @@ export class Device extends EventEmitter {
|
|
|
1169
1182
|
}
|
|
1170
1183
|
}
|
|
1171
1184
|
|
|
1172
|
-
async probeProtocolV2RuntimeState(
|
|
1173
|
-
|
|
1174
|
-
|
|
1185
|
+
async probeProtocolV2RuntimeState(
|
|
1186
|
+
deviceInfo?: ProtocolV2DeviceInfo,
|
|
1187
|
+
timeoutMs?: number,
|
|
1188
|
+
options?: {
|
|
1189
|
+
forceRuntimeContextRefresh?: boolean;
|
|
1190
|
+
}
|
|
1191
|
+
) {
|
|
1192
|
+
const protocolInfo = await this.ensureProtocolV2RuntimeContext(timeoutMs, {
|
|
1193
|
+
forceRefresh: options?.forceRuntimeContextRefresh,
|
|
1194
|
+
});
|
|
1175
1195
|
const runtimeDeviceInfo = deviceInfo ?? this.state?.raw?.protocolV2DeviceInfo;
|
|
1196
|
+
const runtimeMode = getProtocolV2RuntimeMode(protocolInfo, runtimeDeviceInfo);
|
|
1197
|
+
const legacyProtocolInfo = isLegacyProtocolV2ProtocolInfo(protocolInfo);
|
|
1176
1198
|
const protocolV2DeviceType = runtimeDeviceInfo
|
|
1177
1199
|
? resolveProtocolV2DeviceIdentity(runtimeDeviceInfo.hw?.Device_type).deviceType
|
|
1178
1200
|
: this.getCurrentDeviceType();
|
|
@@ -1186,10 +1208,9 @@ export class Device extends EventEmitter {
|
|
|
1186
1208
|
'Protocol V2 romloader mode is only supported for Pro2 and Neo.'
|
|
1187
1209
|
);
|
|
1188
1210
|
}
|
|
1189
|
-
const deviceStatusSupported =
|
|
1190
|
-
|
|
1191
|
-
PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE
|
|
1192
|
-
);
|
|
1211
|
+
const deviceStatusSupported =
|
|
1212
|
+
legacyProtocolInfo ||
|
|
1213
|
+
supportsProtocolV2Message(protocolInfo, PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE);
|
|
1193
1214
|
|
|
1194
1215
|
if (runtimeMode === 'bootloader' || runtimeMode === 'romloader') {
|
|
1195
1216
|
return this.updateProtocolV2Features(deviceInfo, null, runtimeMode, protocolInfo);
|
|
@@ -1250,8 +1271,7 @@ export class Device extends EventEmitter {
|
|
|
1250
1271
|
});
|
|
1251
1272
|
}
|
|
1252
1273
|
|
|
1253
|
-
|
|
1254
|
-
this.deviceAcquired = false;
|
|
1274
|
+
private invalidateProtocolV2RuntimeState() {
|
|
1255
1275
|
if (!this.isProtocolV2()) return;
|
|
1256
1276
|
this.protocolV2StateNeedsReload = true;
|
|
1257
1277
|
this.protocolV2RuntimeContext = undefined;
|
|
@@ -1260,6 +1280,11 @@ export class Device extends EventEmitter {
|
|
|
1260
1280
|
this.clearPreInitialized();
|
|
1261
1281
|
}
|
|
1262
1282
|
|
|
1283
|
+
markTransportDisconnected() {
|
|
1284
|
+
this.deviceAcquired = false;
|
|
1285
|
+
this.invalidateProtocolV2RuntimeState();
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1263
1288
|
invalidateAfterWipe() {
|
|
1264
1289
|
const deviceId = this.getCurrentDeviceId();
|
|
1265
1290
|
if (deviceId) {
|
|
@@ -1269,10 +1294,7 @@ export class Device extends EventEmitter {
|
|
|
1269
1294
|
if (this.originalDescriptor.path !== deviceId) {
|
|
1270
1295
|
deviceWalletSessionStore.deleteDevice(this.originalDescriptor.path);
|
|
1271
1296
|
}
|
|
1272
|
-
this.
|
|
1273
|
-
this.protocolV2RuntimeContext = undefined;
|
|
1274
|
-
this.protocolV2RuntimeContextPromise = undefined;
|
|
1275
|
-
this.protocolV2RuntimeContextRequestToken = undefined;
|
|
1297
|
+
this.invalidateProtocolV2RuntimeState();
|
|
1276
1298
|
}
|
|
1277
1299
|
|
|
1278
1300
|
this.passphraseState = undefined;
|
|
@@ -1284,11 +1306,7 @@ export class Device extends EventEmitter {
|
|
|
1284
1306
|
markProtocolV2Reboot(rebootType: DeviceRebootType) {
|
|
1285
1307
|
if (!this.isProtocolV2()) return;
|
|
1286
1308
|
|
|
1287
|
-
this.
|
|
1288
|
-
this.protocolV2RuntimeContext = undefined;
|
|
1289
|
-
this.protocolV2RuntimeContextPromise = undefined;
|
|
1290
|
-
this.protocolV2RuntimeContextRequestToken = undefined;
|
|
1291
|
-
this.clearPreInitialized();
|
|
1309
|
+
this.invalidateProtocolV2RuntimeState();
|
|
1292
1310
|
let loaderMode: 'bootloader' | 'romloader' | undefined;
|
|
1293
1311
|
if (rebootType === DeviceRebootType.Bootloader) {
|
|
1294
1312
|
loaderMode = 'bootloader';
|
|
@@ -1362,6 +1380,10 @@ export class Device extends EventEmitter {
|
|
|
1362
1380
|
if (device.features) {
|
|
1363
1381
|
this._updateFeatures(device.features);
|
|
1364
1382
|
}
|
|
1383
|
+
// Adopting another Device instance's command channel also crosses an active
|
|
1384
|
+
// link boundary. Renegotiate runtime metadata on that channel instead of
|
|
1385
|
+
// retaining ProtocolInfo from the previous instance/session.
|
|
1386
|
+
this.invalidateProtocolV2RuntimeState();
|
|
1365
1387
|
}
|
|
1366
1388
|
|
|
1367
1389
|
async run(fn?: () => Promise<void>, options?: RunOptions) {
|
|
@@ -304,13 +304,6 @@ export class DeviceCommands {
|
|
|
304
304
|
const promise = this.transport.call(this.mainId, type, msg ?? {}, options) as any;
|
|
305
305
|
this.callPromise = promise;
|
|
306
306
|
const res = await promise;
|
|
307
|
-
if (!shouldReduceDebug) {
|
|
308
|
-
LogCore.debug(
|
|
309
|
-
'[DeviceCommands] [call] Received',
|
|
310
|
-
res.type,
|
|
311
|
-
getSafeTransportLogPayload(res.message, res.type)
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
307
|
return res;
|
|
315
308
|
} catch (error) {
|
|
316
309
|
LogCore.debug('[DeviceCommands] [call] Received error', {
|
|
@@ -445,13 +438,10 @@ export class DeviceCommands {
|
|
|
445
438
|
if (!shouldReduceDebugForCall(callType)) {
|
|
446
439
|
Log.debug('_filterCommonTypes: ', {
|
|
447
440
|
request: callType,
|
|
448
|
-
response:
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
message: getSafeTransportLogPayload(res.message, res.type),
|
|
453
|
-
}
|
|
454
|
-
: res.type,
|
|
441
|
+
response: {
|
|
442
|
+
type: res.type,
|
|
443
|
+
message: getSafeTransportLogPayload(res.message, res.type),
|
|
444
|
+
},
|
|
455
445
|
});
|
|
456
446
|
}
|
|
457
447
|
} catch (error) {
|
package/src/device/DevicePool.ts
CHANGED
|
@@ -218,9 +218,13 @@ export class DevicePool extends EventEmitter {
|
|
|
218
218
|
* Device.getDeviceState skips unsupported status/settings calls in loader mode.
|
|
219
219
|
*/
|
|
220
220
|
static async _refreshProtocolV2DiscoveryState(device: Device) {
|
|
221
|
-
await device.getDeviceState({
|
|
221
|
+
await device.getDeviceState({
|
|
222
|
+
refreshSections: ['status'],
|
|
223
|
+
});
|
|
222
224
|
try {
|
|
223
|
-
await device.getDeviceState({
|
|
225
|
+
await device.getDeviceState({
|
|
226
|
+
refreshSections: ['settings'],
|
|
227
|
+
});
|
|
224
228
|
} catch (error) {
|
|
225
229
|
Log.debug('Unable to refresh Protocol V2 device label during discovery', error);
|
|
226
230
|
}
|
|
@@ -17,6 +17,14 @@ const LogLabelMethod: Set<string> = new Set([
|
|
|
17
17
|
'fileRead',
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
|
+
// 资源上传参数可能包含很大的 Base64 字符串。这里按方法整段跳过,避免日志层
|
|
21
|
+
// 递归复制和序列化这些数据;资源 API 与传输内容本身保持不变。
|
|
22
|
+
const LogPayloadBlockMethod: Set<string> = new Set([
|
|
23
|
+
'deviceUploadNft',
|
|
24
|
+
'deviceUploadWallpaper',
|
|
25
|
+
'uploadPortfolio',
|
|
26
|
+
]);
|
|
27
|
+
|
|
20
28
|
const SensitiveLogKeys: Set<string> = new Set([
|
|
21
29
|
'devicestate',
|
|
22
30
|
'entropy',
|
|
@@ -86,7 +94,12 @@ export function getLogBlockLabel(message: unknown): string | undefined {
|
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
export function getSafeLogPayload(value: unknown, blockLabel?: string): unknown {
|
|
89
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
blockLabel &&
|
|
99
|
+
(LogBlockEvent.has(blockLabel) ||
|
|
100
|
+
LogPayloadBlockMethod.has(blockLabel) ||
|
|
101
|
+
isSigningMethod(blockLabel))
|
|
102
|
+
) {
|
|
90
103
|
return { method: blockLabel, payload: '[REDACTED]' };
|
|
91
104
|
}
|
|
92
105
|
|
|
@@ -56,6 +56,16 @@ export const getProtocolV2SeType = (se?: DeviceSEInfo): string | null =>
|
|
|
56
56
|
|
|
57
57
|
export type ProtocolV2RuntimeMode = 'normal' | 'bootloader' | 'romloader';
|
|
58
58
|
|
|
59
|
+
export type ProtocolV2ProtocolInfo = ProtocolInfo & {
|
|
60
|
+
/** Present only when hd-transport decoded the pre-build-fingerprint wire layout. */
|
|
61
|
+
protobuf_definition?: string | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const isLegacyProtocolV2ProtocolInfo = (
|
|
65
|
+
protocolInfo: ProtocolInfo
|
|
66
|
+
): protocolInfo is ProtocolV2ProtocolInfo & { protobuf_definition: string | null } =>
|
|
67
|
+
Object.prototype.hasOwnProperty.call(protocolInfo, 'protobuf_definition');
|
|
68
|
+
|
|
59
69
|
/**
|
|
60
70
|
* Protocol V2 fingerprints use:
|
|
61
71
|
* <binary>__<version>__<commit>__<PROD|DEV>__<DEBUG|RELEASE>
|
|
@@ -87,11 +97,18 @@ export const parseProtocolV2BuildFingerprint = (
|
|
|
87
97
|
};
|
|
88
98
|
|
|
89
99
|
export const getProtocolV2RuntimeMode = (
|
|
90
|
-
protocolInfo: ProtocolInfo
|
|
100
|
+
protocolInfo: ProtocolInfo,
|
|
101
|
+
deviceInfo?: ProtocolV2DeviceInfo
|
|
91
102
|
): ProtocolV2RuntimeMode | undefined => {
|
|
92
103
|
const binary = parseProtocolV2BuildFingerprint(protocolInfo.build_fingerprint)?.binary;
|
|
93
104
|
if (binary === 'application') return 'normal';
|
|
94
|
-
return binary;
|
|
105
|
+
if (binary) return binary;
|
|
106
|
+
|
|
107
|
+
if (isLegacyProtocolV2ProtocolInfo(protocolInfo) && !deviceInfo?.fw?.application) {
|
|
108
|
+
if (deviceInfo?.fw?.romloader) return 'romloader';
|
|
109
|
+
if (deviceInfo?.fw?.bootloader) return 'bootloader';
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
95
112
|
};
|
|
96
113
|
|
|
97
114
|
// MessageType_DeviceStatusGet in the Protocol V2 protobuf registry.
|
|
@@ -7,6 +7,7 @@ export {
|
|
|
7
7
|
getProtocolV2RuntimeMode,
|
|
8
8
|
getProtocolV2SeState,
|
|
9
9
|
getProtocolV2SeType,
|
|
10
|
+
isLegacyProtocolV2ProtocolInfo,
|
|
10
11
|
parseProtocolV2BuildFingerprint,
|
|
11
12
|
requestProtocolV2ProtocolInfo,
|
|
12
13
|
supportsProtocolV2Message,
|
|
@@ -18,6 +19,7 @@ export type {
|
|
|
18
19
|
ProtocolV2SEInfo,
|
|
19
20
|
ProtocolV2SeStateLabel,
|
|
20
21
|
ProtocolV2RuntimeMode,
|
|
22
|
+
ProtocolV2ProtocolInfo,
|
|
21
23
|
} from './features';
|
|
22
24
|
export * from './firmware';
|
|
23
25
|
export * from './walletSession';
|