@onekeyfe/hd-core 1.2.0-alpha.10 → 1.2.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/__tests__/DeviceCommands.test.ts +25 -0
  2. package/__tests__/RequestQueue.test.ts +34 -0
  3. package/__tests__/pro2Wallpaper.test.ts +50 -0
  4. package/__tests__/protocol-v2.test.ts +426 -60
  5. package/dist/api/BaseMethod.d.ts +4 -0
  6. package/dist/api/BaseMethod.d.ts.map +1 -1
  7. package/dist/api/FileWrite.d.ts.map +1 -1
  8. package/dist/api/FirmwareUpdateV4.d.ts +0 -4
  9. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  10. package/dist/api/GetPassphraseState.d.ts +1 -6
  11. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  12. package/dist/api/UploadPortfolio.d.ts +18 -0
  13. package/dist/api/UploadPortfolio.d.ts.map +1 -0
  14. package/dist/api/index.d.ts +5 -0
  15. package/dist/api/index.d.ts.map +1 -1
  16. package/dist/api/protocol-v2/DeviceSessionGet.d.ts +1 -4
  17. package/dist/api/protocol-v2/DeviceSessionGet.d.ts.map +1 -1
  18. package/dist/api/protocol-v2/DeviceSettingsGet.d.ts +6 -0
  19. package/dist/api/protocol-v2/DeviceSettingsGet.d.ts.map +1 -0
  20. package/dist/api/protocol-v2/DeviceSettingsPageShow.d.ts +16 -0
  21. package/dist/api/protocol-v2/DeviceSettingsPageShow.d.ts.map +1 -0
  22. package/dist/api/protocol-v2/DeviceSettingsSet.d.ts +12 -0
  23. package/dist/api/protocol-v2/DeviceSettingsSet.d.ts.map +1 -0
  24. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +26 -0
  25. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -0
  26. package/dist/core/RequestQueue.d.ts +2 -0
  27. package/dist/core/RequestQueue.d.ts.map +1 -1
  28. package/dist/core/index.d.ts +1 -0
  29. package/dist/core/index.d.ts.map +1 -1
  30. package/dist/device/Device.d.ts.map +1 -1
  31. package/dist/device/DeviceCommands.d.ts +4 -4
  32. package/dist/device/DeviceCommands.d.ts.map +1 -1
  33. package/dist/events/call.d.ts +7 -0
  34. package/dist/events/call.d.ts.map +1 -1
  35. package/dist/events/core.d.ts +2 -2
  36. package/dist/events/core.d.ts.map +1 -1
  37. package/dist/events/iframe.d.ts +1 -0
  38. package/dist/events/iframe.d.ts.map +1 -1
  39. package/dist/index.d.ts +76 -21
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +694 -253
  42. package/dist/inject.d.ts +3 -2
  43. package/dist/inject.d.ts.map +1 -1
  44. package/dist/lowLevelInject.d.ts +2 -1
  45. package/dist/lowLevelInject.d.ts.map +1 -1
  46. package/dist/protocols/protocol-v2/unlockRetry.d.ts +9 -0
  47. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -0
  48. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  49. package/dist/topLevelInject.d.ts.map +1 -1
  50. package/dist/types/api/firmwareUpdate.d.ts +2 -0
  51. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  52. package/dist/types/api/getPassphraseState.d.ts +2 -10
  53. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  54. package/dist/types/api/index.d.ts +8 -2
  55. package/dist/types/api/index.d.ts.map +1 -1
  56. package/dist/types/api/protocolV2.d.ts +19 -4
  57. package/dist/types/api/protocolV2.d.ts.map +1 -1
  58. package/dist/utils/deviceFeaturesUtils.d.ts +0 -2
  59. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  60. package/dist/utils/index.d.ts +2 -0
  61. package/dist/utils/index.d.ts.map +1 -1
  62. package/dist/utils/patch.d.ts +1 -1
  63. package/dist/utils/patch.d.ts.map +1 -1
  64. package/dist/utils/pro2Wallpaper.d.ts +12 -0
  65. package/dist/utils/pro2Wallpaper.d.ts.map +1 -0
  66. package/package.json +4 -4
  67. package/src/api/BaseMethod.ts +18 -0
  68. package/src/api/FileWrite.ts +9 -1
  69. package/src/api/FirmwareUpdateV4.ts +21 -91
  70. package/src/api/GetPassphraseState.ts +9 -18
  71. package/src/api/UploadPortfolio.ts +37 -0
  72. package/src/api/index.ts +5 -0
  73. package/src/api/protocol-v2/DeviceSessionGet.ts +3 -14
  74. package/src/api/protocol-v2/DeviceSettingsGet.ts +16 -0
  75. package/src/api/protocol-v2/DeviceSettingsPageShow.ts +59 -0
  76. package/src/api/protocol-v2/DeviceSettingsSet.ts +39 -0
  77. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +160 -0
  78. package/src/core/RequestQueue.ts +19 -0
  79. package/src/core/index.ts +10 -1
  80. package/src/data/messages/messages-protocol-v2.json +92 -8
  81. package/src/device/Device.ts +17 -3
  82. package/src/device/DeviceCommands.ts +9 -3
  83. package/src/events/call.ts +6 -0
  84. package/src/events/core.ts +2 -0
  85. package/src/events/iframe.ts +1 -0
  86. package/src/index.ts +4 -0
  87. package/src/inject.ts +13 -0
  88. package/src/lowLevelInject.ts +3 -0
  89. package/src/protocols/protocol-v2/unlockRetry.ts +46 -0
  90. package/src/protocols/protocol-v2/walletSession.ts +14 -5
  91. package/src/topLevelInject.ts +2 -0
  92. package/src/types/api/firmwareUpdate.ts +12 -0
  93. package/src/types/api/getPassphraseState.ts +2 -11
  94. package/src/types/api/index.ts +12 -1
  95. package/src/types/api/protocolV2.ts +46 -3
  96. package/src/utils/deviceFeaturesUtils.ts +3 -8
  97. package/src/utils/index.ts +6 -0
  98. package/src/utils/pro2Wallpaper.ts +111 -0
@@ -30,7 +30,7 @@ import {
30
30
  isProtocolV2DeviceDisconnectedError,
31
31
  } from './protocol-v2/helpers';
32
32
 
33
- import type { FirmwareUpdateV4Params } from '../types/api/firmwareUpdate';
33
+ import type { FirmwareUpdateV4Params, FirmwareUpdateV4Target } from '../types/api/firmwareUpdate';
34
34
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
35
35
  import type { PROTO } from '../constants';
36
36
  import type { TypedResponseMessage } from '../device/DeviceCommands';
@@ -188,6 +188,17 @@ const PROTOCOL_V2_REMOTE_COMPONENT_TARGETS: Readonly<
188
188
  },
189
189
  };
190
190
 
191
+ const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<number, FirmwareUpdateV4Target>([
192
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER, 'boot'],
193
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, 'app_v1'],
194
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, 'app_v2'],
195
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR, 'coprocessor'],
196
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01, 'se01'],
197
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02, 'se02'],
198
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03, 'se03'],
199
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, 'se04'],
200
+ ]);
201
+
191
202
  const PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE =
192
203
  'FW_MGMT_TARGET_ROMLOADER is not accepted by the current Pro2 bootloader update request. Flash romloader with the loader-specific flow instead of firmwareUpdateV4.';
193
204
 
@@ -282,13 +293,6 @@ const versionArrayToNumber = (version?: IVersionArray) => {
282
293
  return version[0] * 0x10000 + version[1] * 0x100 + version[2];
283
294
  };
284
295
 
285
- const versionStringToArray = (version?: string | null): IVersionArray | undefined => {
286
- if (!version) return undefined;
287
- const parts = version.split('.').map(part => Number(part));
288
- if (parts.length !== 3 || parts.some(part => !Number.isFinite(part))) return undefined;
289
- return parts as IVersionArray;
290
- };
291
-
292
296
  const compareProtocolV2Versions = (current?: IVersionArray, target?: IVersionArray) => {
293
297
  const currentNumber = versionArrayToNumber(current);
294
298
  const targetNumber = versionArrayToNumber(target);
@@ -418,6 +422,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
418
422
  { name: 'se03Binary', type: 'buffer' },
419
423
  { name: 'se04Binary', type: 'buffer' },
420
424
  { name: 'firmwareType', type: 'string' },
425
+ { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
421
426
  { name: 'platform', type: 'string' },
422
427
  { name: 'resourceBundleFiles', type: 'array', allowEmpty: true },
423
428
  ]);
@@ -436,6 +441,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
436
441
  se04Binary: payload.se04Binary,
437
442
  resourceBundleFiles: payload.resourceBundleFiles,
438
443
  firmwareType: payload.firmwareType,
444
+ targetsToUpdate: payload.targetsToUpdate,
439
445
  platform: payload.platform,
440
446
  };
441
447
  }
@@ -601,63 +607,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
601
607
  return target;
602
608
  }
603
609
 
604
- private getProtocolV2ComponentTargetVersion(
605
- release: IFirmwareReleaseInfo,
606
- component: IProtocolV2FirmwareComponent,
607
- target: ProtocolV2RemoteComponentTarget
608
- ) {
609
- if (component.version) return component.version;
610
- if (target.targetId === ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER) {
611
- return release.bootloaderVersion;
612
- }
613
- if (
614
- target.targetId === ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1 ||
615
- target.targetId === ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2
616
- ) {
617
- return release.version;
618
- }
619
- return undefined;
620
- }
621
-
622
- private getProtocolV2ComponentCurrentVersion(
623
- features: Features,
624
- target: ProtocolV2RemoteComponentTarget
625
- ) {
626
- switch (target.targetId) {
627
- case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER:
628
- return getDeviceBootloaderVersion(features);
629
- case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1:
630
- case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2:
631
- return getDeviceFirmwareVersion(features);
632
- case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR:
633
- return getDeviceBLEFirmwareVersion(features);
634
- case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01:
635
- return versionStringToArray(features.se01Version);
636
- case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02:
637
- return versionStringToArray(features.se02Version);
638
- case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03:
639
- return versionStringToArray(features.se03Version);
640
- case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04:
641
- return versionStringToArray(features.se04Version);
642
- default:
643
- return undefined;
644
- }
645
- }
646
-
647
- private isProtocolV2ComponentVersionSatisfied(
648
- release: IFirmwareReleaseInfo,
649
- component: IProtocolV2FirmwareComponent,
650
- target: ProtocolV2RemoteComponentTarget,
651
- features: Features
652
- ) {
653
- const targetVersion = this.getProtocolV2ComponentTargetVersion(release, component, target);
654
- if (!targetVersion) return false;
655
-
656
- const currentVersion = this.getProtocolV2ComponentCurrentVersion(features, target);
657
- const compareResult = compareProtocolV2Versions(currentVersion, targetVersion);
658
- return compareResult !== undefined && compareResult >= 0;
659
- }
660
-
661
610
  private getProtocolV2ResourceFilePath(path: string) {
662
611
  if (path.startsWith('vol')) return path;
663
612
  if (path.startsWith('/')) return `vol0:${path}`;
@@ -709,25 +658,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
709
658
  return parseProtocolV2OkppHeader(headerBytes);
710
659
  }
711
660
 
712
- private shouldInstallRemoteProtocolV2Component(
713
- release: IFirmwareReleaseInfo,
714
- key: string,
715
- component: IProtocolV2FirmwareComponent,
716
- target: ProtocolV2RemoteComponentTarget,
717
- features: Features
718
- ) {
719
- const versionSatisfied = this.isProtocolV2ComponentVersionSatisfied(
720
- release,
721
- component,
722
- target,
723
- features
724
- );
725
- if (versionSatisfied) {
726
- Log.log(`[FirmwareUpdateV4] skip Protocol V2 component ${key}; version is up to date`);
727
- }
728
- return !versionSatisfied;
729
- }
730
-
731
661
  private async downloadRemoteProtocolV2Component(
732
662
  key: string,
733
663
  component: IProtocolV2FirmwareComponent
@@ -763,16 +693,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
763
693
  }
764
694
 
765
695
  const entries = this.getRemoteComponentEntries(release);
696
+ const targetsToUpdate = new Set(this.params.targetsToUpdate ?? []);
766
697
 
767
698
  for (const [key, component] of entries) {
768
699
  const target = this.getRemoteComponentTarget(key, component);
769
- const shouldInstall = this.shouldInstallRemoteProtocolV2Component(
770
- release,
771
- key,
772
- component,
773
- target,
774
- features
775
- );
700
+ const updateTarget = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId);
701
+ const shouldInstall = updateTarget ? targetsToUpdate.has(updateTarget) : false;
776
702
  if (shouldInstall) {
777
703
  const remoteBinary = await this.downloadRemoteProtocolV2Component(key, component);
778
704
  if (remoteBinary.kind === 'bootloader') {
@@ -827,6 +753,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
827
753
  }));
828
754
  }
829
755
 
756
+ if (!this.params.targetsToUpdate?.includes('resource')) {
757
+ return undefined;
758
+ }
759
+
830
760
  // 模式2:远端配置模式,后续按需下载 + 比对
831
761
  const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
832
762
  if (!release?.resourceBundles?.length) return undefined;
@@ -11,27 +11,18 @@ export default class GetPassphraseState extends BaseMethod {
11
11
  }
12
12
 
13
13
  async run() {
14
- const { passphraseState, newSession, unlockedAttachPin } =
15
- await getPassphraseStateWithRefreshDeviceInfo(this.device, {
16
- expectPassphraseState: this.payload.passphraseState,
17
- onlyMainPin: this.payload.useEmptyPassphrase,
18
- allowCreateAttachPin: this.payload.allowCreateAttachPin,
19
- initSession: this.payload.initSession,
20
- });
14
+ const { passphraseState } = await getPassphraseStateWithRefreshDeviceInfo(this.device, {
15
+ expectPassphraseState: this.payload.passphraseState,
16
+ onlyMainPin: this.payload.useEmptyPassphrase,
17
+ initSession: this.payload.initSession,
18
+ });
21
19
 
22
- const { features } = this.device;
23
- const passphraseProtection = this.device.getCurrentPassphraseProtection() ?? null;
20
+ const passphraseProtection = this.device.getCurrentPassphraseProtection();
24
21
  const deviceType = this.device.getCurrentDeviceType();
25
22
  const isProSeries = deviceType === EDeviceType.Pro || deviceType === EDeviceType.Pro2;
26
23
 
27
- // refresh device info
28
- return Promise.resolve({
29
- passphraseState: isProSeries || passphraseProtection === true ? passphraseState : undefined,
30
- sessionId: this.payload.initSession
31
- ? newSession ?? undefined
32
- : newSession ?? features?.sessionId ?? undefined,
33
- unlockedAttachPin: unlockedAttachPin ?? features?.unlockedAttachPin,
34
- passphraseProtection,
35
- });
24
+ return Promise.resolve(
25
+ isProSeries || passphraseProtection === true ? passphraseState : undefined
26
+ );
36
27
  }
37
28
  }
@@ -0,0 +1,37 @@
1
+ import FileWrite from './FileWrite';
2
+
3
+ export type UploadPortfolioParams = {
4
+ packageBytes: ArrayBuffer | Uint8Array | Blob;
5
+ operationId?: string;
6
+ timeoutMs?: number | string;
7
+ };
8
+
9
+ const PORTFOLIO_PENDING_PATH = 'vol1:/portfolio/portfolio.pfol.pending';
10
+ const PORTFOLIO_CHUNK_SIZE = 2048;
11
+
12
+ export default class UploadPortfolio extends FileWrite {
13
+ init() {
14
+ const { packageBytes, timeoutMs } = this.payload as UploadPortfolioParams;
15
+ this.payload = {
16
+ ...this.payload,
17
+ path: PORTFOLIO_PENDING_PATH,
18
+ offset: 0,
19
+ data: packageBytes,
20
+ chunkSize: PORTFOLIO_CHUNK_SIZE,
21
+ overwrite: true,
22
+ append: false,
23
+ timeoutMs,
24
+ };
25
+ super.init();
26
+ }
27
+
28
+ async run() {
29
+ const stagedFile = await super.run();
30
+ this.throwIfAborted();
31
+ await this.device.commands.typedCall('PortfolioUpdate', 'Success', {});
32
+ return {
33
+ ...stagedFile,
34
+ portfolioUpdated: true,
35
+ };
36
+ }
37
+ }
package/src/api/index.ts CHANGED
@@ -54,6 +54,10 @@ export { default as deviceFirmwareUpdate } from './protocol-v2/DeviceFirmwareUpd
54
54
  export { default as deviceGetFirmwareUpdateStatus } from './protocol-v2/DeviceGetFirmwareUpdateStatus';
55
55
  export { default as deviceFactoryInfoSet } from './protocol-v2/DeviceFactoryInfoSet';
56
56
  export { default as deviceFactoryInfoGet } from './protocol-v2/DeviceFactoryInfoGet';
57
+ export { default as deviceSettingsGet } from './protocol-v2/DeviceSettingsGet';
58
+ export { default as deviceSettingsSet } from './protocol-v2/DeviceSettingsSet';
59
+ export { default as deviceSettingsPageShow } from './protocol-v2/DeviceSettingsPageShow';
60
+ export { default as deviceUploadWallpaper } from './protocol-v2/DeviceUploadWallpaper';
57
61
  export { default as filesystemPermissionFix } from './protocol-v2/FilesystemPermissionFix';
58
62
  export { default as filesystemFormat } from './protocol-v2/FilesystemFormat';
59
63
  export { default as filesystemDiskControl } from './protocol-v2/FilesystemDiskControl';
@@ -66,6 +70,7 @@ export { default as dirRemove } from './DirRemove';
66
70
  export { default as pathInfo } from './PathInfo';
67
71
  export { default as filesystemFileRead } from './FileRead';
68
72
  export { default as filesystemFileWrite } from './FileWrite';
73
+ export { default as uploadPortfolio } from './UploadPortfolio';
69
74
  export { default as filesystemFileDelete } from './FileDelete';
70
75
  export { default as filesystemDirList } from './DirList';
71
76
  export { default as filesystemDirMake } from './DirMake';
@@ -1,26 +1,15 @@
1
1
  import { BaseMethod } from '../BaseMethod';
2
2
 
3
- export type DeviceSessionGetParams = {
4
- sessionId?: string;
5
- };
6
-
7
- export default class DeviceSessionGet extends BaseMethod<DeviceSessionGetParams> {
3
+ export default class DeviceSessionGet extends BaseMethod {
8
4
  init() {
9
5
  this.requireProtocolV2 = true;
10
6
  this.useDevicePassphraseState = false;
11
7
  this.skipForceUpdateCheck = true;
12
- this.params = {
13
- sessionId: this.payload.sessionId,
14
- };
8
+ this.params = undefined;
15
9
  }
16
10
 
17
11
  async run() {
18
- const payload = this.params.sessionId ? { session_id: this.params.sessionId } : {};
19
- const { message } = await this.device.commands.typedCall(
20
- 'DeviceSessionGet',
21
- 'DeviceSession',
22
- payload
23
- );
12
+ const { message } = await this.device.commands.typedCall('DeviceSessionGet', 'DeviceSession', {});
24
13
  return message;
25
14
  }
26
15
  }
@@ -0,0 +1,16 @@
1
+ import { BaseMethod } from '../BaseMethod';
2
+
3
+ export default class DeviceSettingsGet extends BaseMethod {
4
+ init() {
5
+ this.requireProtocolV2 = true;
6
+ this.unlockPolicy = 'retry-on-locked';
7
+ this.skipForceUpdateCheck = true;
8
+ this.useDevicePassphraseState = false;
9
+ this.params = undefined;
10
+ }
11
+
12
+ async run() {
13
+ const res = await this.device.commands.typedCall('DeviceSettingsGet', 'DeviceSettings', {});
14
+ return Promise.resolve(res.message);
15
+ }
16
+ }
@@ -0,0 +1,59 @@
1
+ import { DeviceSettingsPage } from '@onekeyfe/hd-transport';
2
+
3
+ import { BaseMethod } from '../BaseMethod';
4
+ import { invalidParameter } from '../helpers/filesystemValidation';
5
+
6
+ export type SupportedDeviceSettingsPage = DeviceSettingsPage;
7
+
8
+ export type DeviceSettingsPageShowParams = {
9
+ page?:
10
+ | SupportedDeviceSettingsPage
11
+ | 'DeviceReset'
12
+ | 'DevicePinChange'
13
+ | 'DevicePassphrase'
14
+ | 'DeviceAirgap';
15
+ fieldName?: string;
16
+ field_name?: string;
17
+ };
18
+
19
+ const SUPPORTED_PAGES = new Set<number>([
20
+ DeviceSettingsPage.DeviceReset,
21
+ DeviceSettingsPage.DevicePinChange,
22
+ DeviceSettingsPage.DevicePassphrase,
23
+ DeviceSettingsPage.DeviceAirgap,
24
+ ]);
25
+
26
+ function normalizePage(value: DeviceSettingsPageShowParams['page']): SupportedDeviceSettingsPage {
27
+ const page = typeof value === 'string' ? DeviceSettingsPage[value] : value;
28
+ if (page !== undefined && SUPPORTED_PAGES.has(page)) {
29
+ return page as SupportedDeviceSettingsPage;
30
+ }
31
+ throw invalidParameter(
32
+ 'Parameter [page] must be DeviceReset, DevicePinChange, DevicePassphrase, or DeviceAirgap.'
33
+ );
34
+ }
35
+
36
+ export default class DeviceSettingsPageShow extends BaseMethod<{
37
+ page: SupportedDeviceSettingsPage;
38
+ field_name?: string;
39
+ }> {
40
+ init() {
41
+ this.requireProtocolV2 = true;
42
+ this.unlockPolicy = 'retry-on-locked';
43
+ this.skipForceUpdateCheck = true;
44
+ this.useDevicePassphraseState = false;
45
+ this.params = {
46
+ page: normalizePage(this.payload.page),
47
+ field_name: this.payload.field_name ?? this.payload.fieldName,
48
+ };
49
+ }
50
+
51
+ async run() {
52
+ const res = await this.device.commands.typedCall(
53
+ 'DeviceSettingsPageShow',
54
+ 'Success',
55
+ this.params
56
+ );
57
+ return Promise.resolve(res.message);
58
+ }
59
+ }
@@ -0,0 +1,39 @@
1
+ import { BaseMethod } from '../BaseMethod';
2
+ import { invalidParameter } from '../helpers/filesystemValidation';
3
+
4
+ import type { DeviceSettings } from '@onekeyfe/hd-transport';
5
+
6
+ export type DeviceSettingsSetParams = {
7
+ settings?: Omit<DeviceSettings, 'passphrase_enable' | 'airgap_mode'>;
8
+ };
9
+
10
+ export default class DeviceSettingsSet extends BaseMethod<{
11
+ settings: Omit<DeviceSettings, 'passphrase_enable' | 'airgap_mode'>;
12
+ }> {
13
+ init() {
14
+ const settings = this.payload.settings;
15
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
16
+ throw invalidParameter('Parameter [settings] must be an object.');
17
+ }
18
+
19
+ const {
20
+ passphrase_enable: _passphraseEnable,
21
+ airgap_mode: _airgapMode,
22
+ ...supported
23
+ } = settings as DeviceSettings;
24
+ if (Object.keys(supported).length === 0) {
25
+ throw invalidParameter('Parameter [settings] must contain at least one supported setting.');
26
+ }
27
+
28
+ this.requireProtocolV2 = true;
29
+ this.unlockPolicy = 'retry-on-locked';
30
+ this.skipForceUpdateCheck = true;
31
+ this.useDevicePassphraseState = false;
32
+ this.params = { settings: supported };
33
+ }
34
+
35
+ async run() {
36
+ const res = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', this.params);
37
+ return Promise.resolve(res.message);
38
+ }
39
+ }
@@ -0,0 +1,160 @@
1
+ import { blake2s } from '@noble/hashes/blake2s';
2
+ import { bytesToHex } from '@noble/hashes/utils';
3
+ import {
4
+ PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
5
+ PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
6
+ WallpaperTarget,
7
+ } from '@onekeyfe/hd-transport';
8
+
9
+ import { BaseMethod } from '../BaseMethod';
10
+ import { invalidParameter } from '../helpers/filesystemValidation';
11
+ import { DataManager } from '../../data-manager';
12
+ import {
13
+ encodePro2Wallpaper,
14
+ PRO2_WALLPAPER_HEIGHT,
15
+ PRO2_WALLPAPER_WIDTH,
16
+ type Pro2WallpaperColorFormat,
17
+ } from '../../utils/pro2Wallpaper';
18
+
19
+ export type DeviceUploadWallpaperParams = {
20
+ width: number;
21
+ height: number;
22
+ rgba: Uint8Array | ArrayBuffer;
23
+ fileName?: string;
24
+ chunkSize?: number;
25
+ };
26
+
27
+ export type DeviceUploadWallpaperResponse = {
28
+ path: string;
29
+ size: number;
30
+ colorFormat: Pro2WallpaperColorFormat;
31
+ message?: string;
32
+ };
33
+
34
+ const WALLPAPER_DIRECTORY = 'vol0:/wallpapers/user';
35
+ const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/;
36
+
37
+ function getDefaultChunkSize(): number {
38
+ const env = DataManager.getSettings('env');
39
+ return env && DataManager.isBleConnect(env)
40
+ ? PROTOCOL_V2_BLE_FILE_CHUNK_SIZE
41
+ : PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
42
+ }
43
+
44
+ function normalizeFileName(fileName: string | undefined, data: Uint8Array): string {
45
+ if (fileName !== undefined && (!fileName || !SAFE_FILE_NAME.test(fileName))) {
46
+ throw invalidParameter(
47
+ 'Parameter [fileName] may only contain letters, numbers, underscores, hyphens and an optional .bin suffix.'
48
+ );
49
+ }
50
+ const baseName = fileName ?? `wallpaper-${bytesToHex(blake2s(data)).slice(0, 12)}`;
51
+ return baseName.endsWith('.bin') ? baseName : `${baseName}.bin`;
52
+ }
53
+
54
+ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpaperParams> {
55
+ private encoded?: { data: Uint8Array; colorFormat: Pro2WallpaperColorFormat };
56
+
57
+ private directoryReady = false;
58
+
59
+ private uploaded = false;
60
+
61
+ private path = '';
62
+
63
+ init() {
64
+ const { width, height, rgba, fileName, chunkSize } = this.payload;
65
+ if (width !== PRO2_WALLPAPER_WIDTH || height !== PRO2_WALLPAPER_HEIGHT) {
66
+ throw invalidParameter(
67
+ `Pro2 wallpaper dimensions must be ${PRO2_WALLPAPER_WIDTH}x${PRO2_WALLPAPER_HEIGHT}.`
68
+ );
69
+ }
70
+ if (!(rgba instanceof ArrayBuffer) && !ArrayBuffer.isView(rgba)) {
71
+ throw invalidParameter('Parameter [rgba] must be an ArrayBuffer or Uint8Array.');
72
+ }
73
+ if (chunkSize !== undefined && (!Number.isInteger(chunkSize) || chunkSize <= 0)) {
74
+ throw invalidParameter('Parameter [chunkSize] must be a positive integer.');
75
+ }
76
+
77
+ const rgbaBytes =
78
+ rgba instanceof ArrayBuffer
79
+ ? rgba
80
+ : new Uint8Array(rgba.buffer, rgba.byteOffset, rgba.byteLength);
81
+ this.encoded = encodePro2Wallpaper({ width, height, rgba: rgbaBytes });
82
+ this.path = `${WALLPAPER_DIRECTORY}/${normalizeFileName(fileName, this.encoded.data)}`;
83
+ this.params = { width, height, rgba: rgbaBytes, fileName, chunkSize };
84
+ this.requireProtocolV2 = true;
85
+ this.unlockPolicy = 'retry-on-locked';
86
+ this.skipForceUpdateCheck = true;
87
+ this.useDevicePassphraseState = false;
88
+ }
89
+
90
+ private async ensureDirectory() {
91
+ if (this.directoryReady) return;
92
+ try {
93
+ await this.device.commands.typedCall('FilesystemDirMake', 'Success', {
94
+ path: WALLPAPER_DIRECTORY,
95
+ });
96
+ } catch (error) {
97
+ const message = error instanceof Error ? error.message : String(error);
98
+ if (!/exist/i.test(message)) throw error;
99
+ }
100
+ this.directoryReady = true;
101
+ }
102
+
103
+ private async upload() {
104
+ if (this.uploaded) return;
105
+ const encoded = this.encoded;
106
+ if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.');
107
+
108
+ const maxChunkSize = getDefaultChunkSize();
109
+ const requestedChunkSize = this.params.chunkSize ?? maxChunkSize;
110
+ const chunkSize = Math.min(Math.max(64, Math.floor(requestedChunkSize)), maxChunkSize);
111
+ let offset = 0;
112
+ while (offset < encoded.data.byteLength) {
113
+ const chunk = encoded.data.slice(offset, Math.min(offset + chunkSize, encoded.data.byteLength));
114
+ const response = await this.device.commands.typedCall(
115
+ 'FilesystemFileWrite',
116
+ 'FilesystemFile',
117
+ {
118
+ file: {
119
+ path: this.path,
120
+ offset,
121
+ total_size: encoded.data.byteLength,
122
+ data: chunk,
123
+ },
124
+ overwrite: offset === 0,
125
+ append: false,
126
+ ui_percentage: Math.min(
127
+ Math.ceil(((offset + chunk.byteLength) / encoded.data.byteLength) * 100),
128
+ 100
129
+ ),
130
+ },
131
+ { timeoutMs: undefined }
132
+ );
133
+ const processedByte = Number(response.message?.processed_byte);
134
+ offset = Number.isFinite(processedByte) && processedByte > offset
135
+ ? processedByte
136
+ : offset + chunk.byteLength;
137
+ if (offset > encoded.data.byteLength) {
138
+ throw invalidParameter(`Invalid processed_byte returned by device: ${processedByte}.`);
139
+ }
140
+ }
141
+ this.uploaded = true;
142
+ }
143
+
144
+ async run(): Promise<DeviceUploadWallpaperResponse> {
145
+ const encoded = this.encoded;
146
+ if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.');
147
+ await this.ensureDirectory();
148
+ await this.upload();
149
+ const response = await this.device.commands.typedCall('SetWallpaper', 'Success', {
150
+ target: WallpaperTarget.Lock,
151
+ path: this.path,
152
+ });
153
+ return {
154
+ path: this.path,
155
+ size: encoded.data.byteLength,
156
+ colorFormat: encoded.colorFormat,
157
+ message: response.message?.message,
158
+ };
159
+ }
160
+ }
@@ -14,6 +14,8 @@ export type RequestTask = {
14
14
  export default class RequestQueue {
15
15
  private requestQueue = new Map<number, RequestTask>();
16
16
 
17
+ private operationRequestIds = new Map<string, number>();
18
+
17
19
  private pendingCallbackTasks = new Map<string, Deferred<void>>();
18
20
 
19
21
  // 生成唯一请求ID
@@ -30,11 +32,21 @@ export default class RequestQueue {
30
32
  method.responseID = requestId;
31
33
  }
32
34
  const abortController = new AbortController();
35
+ method.abortSignal = abortController.signal;
33
36
  const task = { id: requestId, method, abortController };
34
37
  this.requestQueue.set(requestId, task);
38
+ const operationId = method.payload?.operationId;
39
+ if (typeof operationId === 'string' && operationId) {
40
+ this.operationRequestIds.set(operationId, requestId);
41
+ }
35
42
  return task;
36
43
  }
37
44
 
45
+ public abortOperation(operationId: string) {
46
+ const requestId = this.operationRequestIds.get(operationId);
47
+ return requestId === undefined ? false : this.abortRequest(requestId);
48
+ }
49
+
38
50
  public getTask(requestId: number): RequestTask | undefined {
39
51
  return this.requestQueue.get(requestId);
40
52
  }
@@ -105,6 +117,13 @@ export default class RequestQueue {
105
117
 
106
118
  // 删除请求
107
119
  public releaseTask(requestId: number) {
120
+ const operationId = this.requestQueue.get(requestId)?.method.payload?.operationId;
121
+ if (
122
+ typeof operationId === 'string' &&
123
+ this.operationRequestIds.get(operationId) === requestId
124
+ ) {
125
+ this.operationRequestIds.delete(operationId);
126
+ }
108
127
  this.requestQueue.delete(requestId);
109
128
  }
110
129
 
package/src/core/index.ts CHANGED
@@ -56,6 +56,7 @@ import TransportManager from '../data-manager/TransportManager';
56
56
  import DeviceConnector from '../device/DeviceConnector';
57
57
  import RequestQueue from './RequestQueue';
58
58
  import { getSynchronize } from '../utils/getSynchronize';
59
+ import { runMethodWithUnlockRetry } from '../protocols/protocol-v2/unlockRetry';
59
60
 
60
61
  import type { ConnectSettings, Features, KnownDevice } from '../types';
61
62
  import type { CoreMessage, IFrameCallMessage, UiPromise, UiPromiseResponse } from '../events';
@@ -616,7 +617,7 @@ const onCallDevice = async (
616
617
  method.device?.commands?.checkDisposed();
617
618
 
618
619
  try {
619
- const response: object = await method.run();
620
+ const response: object = await runMethodWithUnlockRetry(method, device);
620
621
  messageResponse = createResponseMessage(method.responseID, true, response);
621
622
  requestQueue.resolveRequest(method.responseID, messageResponse);
622
623
  completeMethodRequestContext(method);
@@ -1341,6 +1342,10 @@ export default class Core extends EventEmitter {
1341
1342
  Log.debug(`[Core] Created SDK instance: ${this.sdkInstanceId}`);
1342
1343
  }
1343
1344
 
1345
+ cancelOperation(operationId: string) {
1346
+ this.requestQueue.abortOperation(operationId);
1347
+ }
1348
+
1344
1349
  private getCoreContext() {
1345
1350
  return {
1346
1351
  sdkInstanceId: this.sdkInstanceId,
@@ -1409,6 +1414,10 @@ export default class Core extends EventEmitter {
1409
1414
  cancel(this.getCoreContext(), message.payload.connectId);
1410
1415
  break;
1411
1416
  }
1417
+ case IFRAME.CANCEL_OPERATION: {
1418
+ this.cancelOperation(message.payload.operationId);
1419
+ break;
1420
+ }
1412
1421
  case IFRAME.CALLBACK: {
1413
1422
  Log.log('callback message: ', message);
1414
1423
  postMessage(message);