@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.
Files changed (57) hide show
  1. package/__tests__/DeviceCommands.test.ts +140 -26
  2. package/__tests__/base64Data.test.ts +70 -0
  3. package/__tests__/device-lifecycle-events.test.ts +113 -2
  4. package/__tests__/device-pool-state.test.ts +25 -0
  5. package/__tests__/deviceSettings.test.ts +0 -1
  6. package/__tests__/deviceUploadNft.test.ts +33 -4
  7. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +178 -0
  8. package/__tests__/get-device-state.test.ts +52 -13
  9. package/__tests__/logBlockEvent.test.ts +13 -1
  10. package/__tests__/protocol-v2.test.ts +762 -124
  11. package/__tests__/refresh-device-state.test.ts +3 -1
  12. package/__tests__/resourceBase64Boundary.test.ts +48 -0
  13. package/__tests__/ton-sign-message.test.ts +84 -0
  14. package/dist/api/FirmwareUpdateV4.d.ts +10 -3
  15. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  16. package/dist/api/UploadPortfolio.d.ts +1 -1
  17. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  18. package/dist/api/helpers/base64Data.d.ts +16 -0
  19. package/dist/api/helpers/base64Data.d.ts.map +1 -0
  20. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
  21. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  22. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
  23. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  24. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/device/Device.d.ts +7 -2
  27. package/dist/device/Device.d.ts.map +1 -1
  28. package/dist/device/DeviceCommands.d.ts.map +1 -1
  29. package/dist/device/DevicePool.d.ts.map +1 -1
  30. package/dist/events/logBlockEvent.d.ts.map +1 -1
  31. package/dist/index.d.ts +11 -14
  32. package/dist/index.js +455 -200
  33. package/dist/protocols/protocol-v2/features.d.ts +9 -1
  34. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  35. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  36. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  37. package/dist/types/api/protocolV2.d.ts +1 -1
  38. package/dist/types/api/protocolV2.d.ts.map +1 -1
  39. package/dist/utils/pro2Nft.d.ts +7 -0
  40. package/dist/utils/pro2Nft.d.ts.map +1 -1
  41. package/package.json +6 -4
  42. package/src/api/FirmwareUpdateV4.ts +326 -174
  43. package/src/api/UploadPortfolio.ts +9 -2
  44. package/src/api/helpers/base64Data.ts +85 -0
  45. package/src/api/protocol-v2/DeviceUploadNft.ts +56 -8
  46. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +37 -18
  47. package/src/api/ton/TonSignMessage.ts +5 -3
  48. package/src/core/index.ts +6 -2
  49. package/src/device/Device.ts +53 -31
  50. package/src/device/DeviceCommands.ts +4 -14
  51. package/src/device/DevicePool.ts +6 -2
  52. package/src/events/logBlockEvent.ts +14 -1
  53. package/src/protocols/protocol-v2/features.ts +19 -2
  54. package/src/protocols/protocol-v2/index.ts +2 -0
  55. package/src/types/api/protocolV2.ts +1 -1
  56. package/src/utils/deviceSettings.ts +1 -1
  57. 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
- packageBytes: ArrayBuffer | Uint8Array | Blob;
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 { packageBytes, timeoutMs } = this.payload as UploadPortfolioParams;
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
- type Pro2NftImage,
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
- image: Pro2NftImage;
24
- thumbnail: Pro2NftImage;
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
- image,
58
- thumbnail,
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
- this.bundle = buildPro2NftBundle({ image, thumbnail, title, subtitle, timestampMs });
83
- this.params = { image, thumbnail, title, subtitle, timestampMs, chunkSize, paceMs, timeoutMs };
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
- width: number;
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 { width, height, rgba, fileName, chunkSize } = this.payload;
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 rgbaBytes =
71
- rgba instanceof ArrayBuffer
72
- ? rgba
73
- : new Uint8Array(rgba.buffer, rgba.byteOffset, rgba.byteLength);
74
- this.encoded = encodePro2Wallpaper({ width, height, rgba: rgbaBytes });
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 = { width, height, rgba: rgbaBytes, fileName, chunkSize };
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
- // use signing_message_repr sign, not exists signning_message, skip validate
178
- const hasSigningMessageRepr = request.signning_message == null;
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
- skip_validate: hasClassic || hasSigningMessageRepr,
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
- Log.debug('Device Run Error: ', e);
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) => {
@@ -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
- const features = await this.probeProtocolV2RuntimeState(
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
- await this.probeProtocolV2RuntimeState(refreshedDeviceInfo);
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 patch committed', {
1111
+ Log.debug('Device state updated', {
1105
1112
  source,
1106
- keys: result.changedKeys,
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(timeoutMs?: number): Promise<ProtocolInfo> {
1135
+ async ensureProtocolV2RuntimeContext(
1136
+ timeoutMs?: number,
1137
+ options?: { forceRefresh?: boolean }
1138
+ ): Promise<ProtocolInfo> {
1128
1139
  const cachedProtocolInfo =
1129
- this.protocolV2RuntimeContext ??
1130
- (!this.protocolV2StateNeedsReload
1131
- ? this.state?.raw?.protocolV2ProtocolInfo ?? undefined
1132
- : undefined);
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(deviceInfo?: ProtocolV2DeviceInfo, timeoutMs?: number) {
1173
- const protocolInfo = await this.ensureProtocolV2RuntimeContext(timeoutMs);
1174
- const runtimeMode = getProtocolV2RuntimeMode(protocolInfo);
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 = supportsProtocolV2Message(
1190
- protocolInfo,
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
- markTransportDisconnected() {
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.protocolV2StateNeedsReload = true;
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.protocolV2StateNeedsReload = true;
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
- callType === 'DeviceFirmwareUpdateStatusGet'
450
- ? {
451
- type: res.type,
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) {
@@ -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({ refreshSections: ['status'] });
221
+ await device.getDeviceState({
222
+ refreshSections: ['status'],
223
+ });
222
224
  try {
223
- await device.getDeviceState({ refreshSections: ['settings'] });
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 (blockLabel && (LogBlockEvent.has(blockLabel) || isSigningMethod(blockLabel))) {
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';