@onekeyfe/hd-core 1.2.0-alpha.36 → 1.2.0-alpha.39

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 (79) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +5 -1
  2. package/__tests__/deviceUploadNft.test.ts +187 -0
  3. package/__tests__/homescreen.test.ts +41 -1
  4. package/__tests__/logBlockEvent.test.ts +1 -1
  5. package/__tests__/open-wallet-session.test.ts +161 -44
  6. package/__tests__/pro2Nft.test.ts +108 -0
  7. package/__tests__/protocol-v2-unlock-policy.test.ts +86 -35
  8. package/__tests__/protocol-v2.test.ts +168 -444
  9. package/__tests__/public-pro2-api-boundary.test.ts +1 -0
  10. package/__tests__/uiProgressThrottle.test.ts +74 -0
  11. package/dist/api/BaseMethod.d.ts +3 -1
  12. package/dist/api/BaseMethod.d.ts.map +1 -1
  13. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  14. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  15. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +0 -1
  16. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  17. package/dist/api/helpers/protocolV2FileWrite.d.ts +2 -0
  18. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  19. package/dist/api/index.d.ts +1 -0
  20. package/dist/api/index.d.ts.map +1 -1
  21. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +30 -0
  22. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/events/logBlockEvent.d.ts.map +1 -1
  25. package/dist/index.d.ts +34 -2
  26. package/dist/index.js +544 -496
  27. package/dist/inject.d.ts.map +1 -1
  28. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
  29. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  30. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
  31. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
  32. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  33. package/dist/types/api/index.d.ts +2 -1
  34. package/dist/types/api/index.d.ts.map +1 -1
  35. package/dist/types/api/protocolV2.d.ts +3 -0
  36. package/dist/types/api/protocolV2.d.ts.map +1 -1
  37. package/dist/utils/homescreen.d.ts +4 -0
  38. package/dist/utils/homescreen.d.ts.map +1 -1
  39. package/dist/utils/index.d.ts +1 -1
  40. package/dist/utils/index.d.ts.map +1 -1
  41. package/dist/utils/patch.d.ts +1 -1
  42. package/dist/utils/patch.d.ts.map +1 -1
  43. package/dist/utils/pro2Nft.d.ts +29 -0
  44. package/dist/utils/pro2Nft.d.ts.map +1 -0
  45. package/dist/utils/pro2Wallpaper.d.ts +10 -0
  46. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  47. package/dist/utils/uiProgressThrottle.d.ts +3 -0
  48. package/dist/utils/uiProgressThrottle.d.ts.map +1 -0
  49. package/package.json +4 -4
  50. package/src/api/BaseMethod.ts +7 -9
  51. package/src/api/OpenWalletSession.ts +37 -15
  52. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +29 -30
  53. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +0 -7
  54. package/src/api/helpers/protocolV2FileWrite.ts +11 -4
  55. package/src/api/index.ts +1 -0
  56. package/src/api/protocol-v2/DeviceUploadNft.ts +166 -0
  57. package/src/core/index.ts +83 -82
  58. package/src/data/messages/messages-protocol-v2.json +10 -15
  59. package/src/events/logBlockEvent.ts +1 -0
  60. package/src/inject.ts +1 -0
  61. package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
  62. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +117 -0
  63. package/src/protocols/protocol-v2/walletSession.ts +18 -51
  64. package/src/types/api/index.ts +2 -0
  65. package/src/types/api/protocolV2.ts +13 -0
  66. package/src/utils/homescreen.ts +32 -6
  67. package/src/utils/index.ts +6 -1
  68. package/src/utils/pro2Nft.ts +91 -0
  69. package/src/utils/pro2Wallpaper.ts +35 -8
  70. package/src/utils/uiProgressThrottle.ts +63 -0
  71. package/dist/api/firmware/progressThrottle.d.ts +0 -3
  72. package/dist/api/firmware/progressThrottle.d.ts.map +0 -1
  73. package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
  74. package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
  75. package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
  76. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
  77. package/src/api/firmware/progressThrottle.ts +0 -44
  78. package/src/protocols/protocol-v2/lockedError.ts +0 -10
  79. package/src/protocols/protocol-v2/unlockRetry.ts +0 -131
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pro2Nft.d.ts","sourceRoot":"","sources":["../../src/utils/pro2Nft.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAC5C,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAC9C,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAC/C,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAC3C,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAClD,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,GAAG,WAAW,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,EAAE,UAAU,CAAC;IACtB,QAAQ,EAAE,UAAU,CAAC;CACtB,CAAC;AAsBF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,EAAE,YAAY,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,aAAa,CAkChB"}
@@ -1,6 +1,16 @@
1
1
  export declare const PRO2_WALLPAPER_WIDTH = 604;
2
2
  export declare const PRO2_WALLPAPER_HEIGHT = 1024;
3
3
  export type Pro2WallpaperColorFormat = 'RGB565' | 'RGB565A8';
4
+ export type Pro2ImageAlphaMode = 'preserve' | 'black-background';
5
+ export declare function encodePro2Image(options: {
6
+ width: number;
7
+ height: number;
8
+ rgba: Uint8Array | ArrayBuffer;
9
+ alphaMode?: Pro2ImageAlphaMode;
10
+ }): {
11
+ data: Uint8Array;
12
+ colorFormat: Pro2WallpaperColorFormat;
13
+ };
4
14
  export declare function encodePro2Wallpaper(options: {
5
15
  width: number;
6
16
  height: number;
@@ -1 +1 @@
1
- {"version":3,"file":"pro2Wallpaper.d.ts","sourceRoot":"","sources":["../../src/utils/pro2Wallpaper.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,UAAU,CAAC;AA8B7D,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,GAAG,WAAW,CAAC;CAChC,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,WAAW,EAAE,wBAAwB,CAAA;CAAE,CA4D9D"}
1
+ {"version":3,"file":"pro2Wallpaper.d.ts","sourceRoot":"","sources":["../../src/utils/pro2Wallpaper.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE7D,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,kBAAkB,CAAC;AA8BjE,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,GAAG,WAAW,CAAC;IAC/B,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAChC,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,WAAW,EAAE,wBAAwB,CAAA;CAAE,CA4E9D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,GAAG,WAAW,CAAC;CAChC,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,WAAW,EAAE,wBAAwB,CAAA;CAAE,CAE9D"}
@@ -0,0 +1,3 @@
1
+ import type { CoreMessage } from '../events';
2
+ export declare const createUiProgressMessageFilter: (intervalMs?: number, now?: () => number) => (message: CoreMessage) => boolean;
3
+ //# sourceMappingURL=uiProgressThrottle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uiProgressThrottle.d.ts","sourceRoot":"","sources":["../../src/utils/uiProgressThrottle.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAiB7C,eAAO,MAAM,6BAA6B,yDAMvB,WAAW,YAqC7B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-core",
3
- "version": "1.2.0-alpha.36",
3
+ "version": "1.2.0-alpha.39",
4
4
  "description": "Core processes and APIs for communicating with OneKey hardware devices.",
5
5
  "author": "OneKey",
6
6
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
@@ -25,8 +25,8 @@
25
25
  "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-shared": "1.2.0-alpha.36",
29
- "@onekeyfe/hd-transport": "1.2.0-alpha.36",
28
+ "@onekeyfe/hd-shared": "1.2.0-alpha.39",
29
+ "@onekeyfe/hd-transport": "1.2.0-alpha.39",
30
30
  "axios": "1.15.2",
31
31
  "bignumber.js": "^9.0.2",
32
32
  "bytebuffer": "^5.0.1",
@@ -44,5 +44,5 @@
44
44
  "@types/w3c-web-usb": "^1.0.10",
45
45
  "@types/web-bluetooth": "^0.0.21"
46
46
  },
47
- "gitHead": "94f0fec6ea1dfd4ffea2dae8ea2b0b4a2a6cf2be"
47
+ "gitHead": "2cb8f692689327fc7e77cb848223cbeaa740dcad"
48
48
  }
@@ -16,16 +16,14 @@ import { getBleFirmwareReleaseInfo, getFirmwareReleaseInfo } from './firmware/re
16
16
  import { LoggerNames, getLogger } from '../utils';
17
17
  import { generateInstanceId } from '../utils/tracing';
18
18
  import { DeviceModelToTypes } from '../types';
19
- import {
20
- type UnlockPolicy,
21
- getProtocolV2UnlockPolicy,
22
- } from '../protocols/protocol-v2/unlockPolicy';
23
19
 
24
20
  import type { Device } from '../device/Device';
25
21
  import type DeviceConnector from '../device/DeviceConnector';
26
22
  import type { DeviceFirmwareRange, KnownDevice } from '../types';
27
23
  import type { CoreMessage } from '../events';
24
+ import type { UnlockPolicy } from '../protocols/protocol-v2/unlockPolicy';
28
25
  import type { ProtocolV2InteractionDescriptor } from '../protocols/protocol-v2/uiInteraction';
26
+ import type { ProtocolV2UnlockContext } from '../protocols/protocol-v2/unlockPolicyRunner';
29
27
  import type { RequestContext } from '../utils/tracing';
30
28
  import type { CoreContext } from '../core';
31
29
 
@@ -128,9 +126,7 @@ export abstract class BaseMethod<Params = undefined> {
128
126
  */
129
127
  checkDeviceId = false;
130
128
 
131
- /**
132
- * 该方法是否需要校验 passphrase state
133
- */
129
+ /** Whether the method requires wallet-session selection and Protocol V2 pre-unlock. */
134
130
  useDevicePassphraseState = true;
135
131
 
136
132
  /**
@@ -168,9 +164,12 @@ export abstract class BaseMethod<Params = undefined> {
168
164
  */
169
165
  strictCheckDeviceSupport = false;
170
166
 
171
- /** Core unlocks and retries only explicitly replay-safe Protocol V2 methods. */
167
+ /** Non-wallet methods may explicitly require Protocol V2 pre-unlock. */
172
168
  unlockPolicy: UnlockPolicy = 'none';
173
169
 
170
+ /** Shared by composite methods so nested calls do not repeat the root preflight. */
171
+ protocolV2UnlockContext?: ProtocolV2UnlockContext;
172
+
174
173
  getSupportedProtocols(): readonly ProtocolType[] {
175
174
  return ['V1'];
176
175
  }
@@ -217,7 +216,6 @@ export abstract class BaseMethod<Params = undefined> {
217
216
  this.useDevice = true;
218
217
  this.allowDeviceMode = [UI_REQUEST.NOT_INITIALIZE];
219
218
  this.requireDeviceMode = [];
220
- this.unlockPolicy = getProtocolV2UnlockPolicy(this.name);
221
219
  }
222
220
 
223
221
  abstract init(): void;
@@ -91,28 +91,40 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
91
91
 
92
92
  async run(): Promise<OpenWalletSessionPayload> {
93
93
  const isProtocolV2 = this.device.isProtocolV2();
94
- const state = await this.device.getDeviceState({ refreshSections: ['status'] });
94
+ let state = await this.device.getDeviceState({ refreshSections: ['status'] });
95
95
  let currentDeviceId = state.identity.deviceId;
96
+ const hasAuthoritativeProtocolV2WalletStatus = (candidate: typeof state) =>
97
+ candidate.status.unlocked === true &&
98
+ typeof candidate.status.passphraseProtection === 'boolean' &&
99
+ typeof candidate.status.unlockedAttachPin === 'boolean';
100
+ const requireAuthoritativeProtocolV2WalletStatus = (candidate: typeof state) => {
101
+ if (!hasAuthoritativeProtocolV2WalletStatus(candidate)) {
102
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInitializeFailed);
103
+ }
104
+ return candidate;
105
+ };
96
106
  const requireDeviceId = () => {
97
107
  if (!currentDeviceId) {
98
108
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInitializeFailed);
99
109
  }
100
110
  return currentDeviceId;
101
111
  };
102
- const refreshProtocolV2DeviceId = async () => {
103
- const refreshedState = await this.device.getDeviceState({ refreshSections: ['status'] });
104
- currentDeviceId = refreshedState.identity.deviceId;
105
- return requireDeviceId();
112
+ const refreshProtocolV2DeviceState = async () => {
113
+ state = await this.device.getDeviceState({ refreshSections: ['status'] });
114
+ currentDeviceId = state.identity.deviceId;
115
+ return state;
106
116
  };
107
- const unlockProtocolV2IfLocked = async () => {
108
- if (isProtocolV2 && state.status.unlocked === false) {
117
+ const ensureProtocolV2WalletStatus = async () => {
118
+ if (isProtocolV2 && !hasAuthoritativeProtocolV2WalletStatus(state)) {
109
119
  await this.device.unlockDevice(DeviceSessionPinType.Main, {
110
120
  source: 'unlock-coordinator',
111
121
  reason: 'device-locked',
112
122
  deviceOnly: true,
113
123
  method: 'openWalletSession',
114
124
  });
125
+ requireAuthoritativeProtocolV2WalletStatus(await refreshProtocolV2DeviceState());
115
126
  }
127
+ return state;
116
128
  };
117
129
 
118
130
  const protocol = isProtocolV2 ? 'V2' : 'V1';
@@ -124,14 +136,21 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
124
136
  onlyMainPin: true,
125
137
  deriveCardano: this.payload.deriveCardano,
126
138
  selectMainWalletBeforeRestore:
127
- state.status.unlocked === false || state.status.unlockedAttachPin === true,
139
+ !hasAuthoritativeProtocolV2WalletStatus(state) ||
140
+ state.status.unlockedAttachPin === true,
128
141
  })
129
142
  : await getPassphraseStateWithRefreshDeviceInfo(this.device, {
130
143
  onlyMainPin: true,
131
144
  initSession: this.payload.initSession,
132
145
  });
133
- const deviceId = isProtocolV2 ? await refreshProtocolV2DeviceId() : requireDeviceId();
134
- if (session.unlockedAttachPin) {
146
+ const refreshedState = isProtocolV2
147
+ ? requireAuthoritativeProtocolV2WalletStatus(await refreshProtocolV2DeviceState())
148
+ : state;
149
+ const deviceId = requireDeviceId();
150
+ if (
151
+ session.unlockedAttachPin ||
152
+ (isProtocolV2 && refreshedState.status.unlockedAttachPin === true)
153
+ ) {
135
154
  try {
136
155
  await this.device.lockDevice();
137
156
  } catch {
@@ -151,8 +170,8 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
151
170
 
152
171
  if (this.params.mode === OpenWalletSessionMode.ResumeHidden) {
153
172
  if (isProtocolV2) {
154
- await unlockProtocolV2IfLocked();
155
- const refreshedDeviceId = await refreshProtocolV2DeviceId();
173
+ await ensureProtocolV2WalletStatus();
174
+ const refreshedDeviceId = requireDeviceId();
156
175
  if (refreshedDeviceId !== this.params.deviceId) {
157
176
  deviceWalletSessionStore.delete(this.params.deviceId, this.params.passphraseState);
158
177
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
@@ -197,15 +216,18 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
197
216
  }
198
217
 
199
218
  this.device.passphraseState = undefined;
200
- await unlockProtocolV2IfLocked();
219
+ await ensureProtocolV2WalletStatus();
201
220
  const session = isProtocolV2
202
221
  ? await getProtocolV2WalletSession(this.device, {
203
222
  forceWalletSelection: true,
204
223
  deriveCardano: this.payload.deriveCardano,
205
224
  })
206
225
  : await getPassphraseStateWithRefreshDeviceInfo(this.device, { initSession: true });
207
- const deviceId = isProtocolV2 ? await refreshProtocolV2DeviceId() : requireDeviceId();
208
- if (isProtocolV2 && this.device.getCurrentPassphraseProtection() !== true) {
226
+ const refreshedState = isProtocolV2
227
+ ? requireAuthoritativeProtocolV2WalletStatus(await refreshProtocolV2DeviceState())
228
+ : state;
229
+ const deviceId = requireDeviceId();
230
+ if (isProtocolV2 && refreshedState.status.passphraseProtection !== true) {
209
231
  this.device.clearInternalState();
210
232
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotOpenedPassphrase);
211
233
  }
@@ -14,8 +14,7 @@ import { findMethod } from '../utils';
14
14
  import { DEVICE, IFRAME, createUiMessage } from '../../events';
15
15
  import { UI_REQUEST } from '../../constants/ui-request';
16
16
  import { onDeviceButtonHandler } from '../../core';
17
- import { runMethodWithUnlockRetry } from '../../protocols/protocol-v2/unlockRetry';
18
- import { ensureProtocolV2WalletSessionUnlocked } from '../../protocols/protocol-v2/walletSession';
17
+ import { runMethodWithUnlockPolicy } from '../../protocols/protocol-v2/unlockPolicyRunner';
19
18
  import {
20
19
  completeRequestContext,
21
20
  createRequestContext,
@@ -377,34 +376,34 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
377
376
  this.device.on(DEVICE.PASSPHRASE, onSignalAbort);
378
377
 
379
378
  preCheckDeviceSupport(this.device, method);
380
- if (this.temporarySafetyCheckPrompted) {
381
- method.temporarySafetyCheckPrompted = true;
382
- } else {
383
- const appliedTemporarySafetyCheck = await method.checkSafetyLevelOnTestNet();
384
- if (appliedTemporarySafetyCheck) {
385
- this.temporarySafetyCheckPrompted = true;
386
- }
387
- }
388
-
389
- // Protocol V2 hands a wallet session to exactly one blockchain request.
390
- // The parent all-network call consumes its first handoff while fetching
391
- // the root fingerprint, so each nested chain method must resume the
392
- // requested hidden wallet before it sends its own device command.
393
- if (this.device.isProtocolV2() && this.payload.passphraseState) {
394
- await ensureProtocolV2WalletSessionUnlocked(this.device);
395
- const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
396
- this.payload.passphraseState,
397
- false,
398
- this.payload.skipPassphraseCheck
399
- );
400
- if (!passphraseStateSafety) {
401
- throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
402
- }
403
- }
404
-
405
- const response = this.device.isProtocolV2()
406
- ? await runMethodWithUnlockRetry(method, this.device)
407
- : await method.run();
379
+ const response = await runMethodWithUnlockPolicy<any[]>(method, this.device, {
380
+ context: this.protocolV2UnlockContext,
381
+ prepare: async () => {
382
+ if (this.temporarySafetyCheckPrompted) {
383
+ method.temporarySafetyCheckPrompted = true;
384
+ } else {
385
+ const appliedTemporarySafetyCheck = await method.checkSafetyLevelOnTestNet();
386
+ if (appliedTemporarySafetyCheck) {
387
+ this.temporarySafetyCheckPrompted = true;
388
+ }
389
+ }
390
+
391
+ // Protocol V2 hands a wallet session to exactly one blockchain request.
392
+ // The parent all-network call consumes its first handoff while fetching
393
+ // the root fingerprint, so each nested chain method must resume the
394
+ // requested hidden wallet before it sends its own device command.
395
+ if (this.device.isProtocolV2() && this.payload.passphraseState) {
396
+ const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
397
+ this.payload.passphraseState,
398
+ false,
399
+ this.payload.skipPassphraseCheck
400
+ );
401
+ if (!passphraseStateSafety) {
402
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
403
+ }
404
+ }
405
+ },
406
+ });
408
407
 
409
408
  if (!Array.isArray(response) || response.length === 0) {
410
409
  throw new Error('No response');
@@ -13,7 +13,6 @@ import { DeviceModelToTypes } from '../../types';
13
13
  import { DataManager } from '../../data-manager';
14
14
  import { BaseMethod } from '../BaseMethod';
15
15
  import { DEVICE } from '../../events';
16
- import { createFirmwareProgressThrottle } from './progressThrottle';
17
16
 
18
17
  import type {
19
18
  IFirmwareUpdateProgressType,
@@ -41,8 +40,6 @@ const isDeviceDisconnectedError = (error: unknown) => {
41
40
  export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
42
41
  checkPromise: Deferred<any> | null = null;
43
42
 
44
- private shouldPostFirmwareProgress = createFirmwareProgressThrottle();
45
-
46
43
  init(): void {}
47
44
 
48
45
  run(): Promise<any> {
@@ -86,10 +83,6 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
86
83
  * @param progress Post the percentage of the progress
87
84
  */
88
85
  postProgressMessage = (progress: number, progressType: IFirmwareUpdateProgressType) => {
89
- if (!this.shouldPostFirmwareProgress(progress, progressType)) {
90
- return;
91
- }
92
-
93
86
  this.postMessage(
94
87
  createUiMessage(UI_REQUEST.FIRMWARE_PROGRESS, {
95
88
  device: this.device.toMessageObject() as KnownDevice,
@@ -31,19 +31,21 @@ export type ProtocolV2FileWriteOptions = {
31
31
  uiPercentage?: number;
32
32
  timeoutMs?: number;
33
33
  maxChunkRetries?: number;
34
+ paceMs?: number;
34
35
  throwIfAborted?: () => void;
35
36
  onProgress?: (progress: ProtocolV2FileWriteProgress) => void;
36
37
  };
37
38
 
38
39
  const MIN_FILE_CHUNK_SIZE = 64;
39
40
 
40
- function isRetryableFileWriteTimeout(error: unknown) {
41
+ export function isProtocolV2ResponseTimeout(error: unknown) {
41
42
  if (!error || typeof error !== 'object') return false;
42
- const candidate = error as { errorCode?: number; code?: number; message?: string };
43
+ const candidate = error as { errorCode?: number; code?: number | string; message?: string };
43
44
  const code = candidate.errorCode ?? candidate.code;
44
45
  return (
45
46
  code === HardwareErrorCode.BleTimeoutError ||
46
- /Lowlevel response timeout/i.test(candidate.message ?? '')
47
+ code === 'response-timeout' ||
48
+ /(?:BLE|Lowlevel|Protocol V2) response timeout/i.test(candidate.message ?? '')
47
49
  );
48
50
  }
49
51
 
@@ -147,7 +149,7 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
147
149
  );
148
150
  isWritePending = false;
149
151
  } catch (error) {
150
- if (retryCount >= maxChunkRetries || !isRetryableFileWriteTimeout(error)) throw error;
152
+ if (retryCount >= maxChunkRetries || !isProtocolV2ResponseTimeout(error)) throw error;
151
153
  retryCount += 1;
152
154
  options.throwIfAborted?.();
153
155
  }
@@ -186,6 +188,11 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
186
188
  elapsedMs > 0 ? Math.round((transferredBytes / elapsedMs) * 1000) : undefined,
187
189
  elapsedMs,
188
190
  });
191
+ if (options.paceMs && options.paceMs > 0) {
192
+ await new Promise(resolve => {
193
+ setTimeout(resolve, options.paceMs);
194
+ });
195
+ }
189
196
  }
190
197
 
191
198
  return {
package/src/api/index.ts CHANGED
@@ -49,6 +49,7 @@ export { default as promptWebDeviceAccess } from './PromptWebDeviceAccess';
49
49
  export { default as deviceReboot } from './protocol-v2/DeviceReboot';
50
50
  export { default as deviceGetOnboardingStatus } from './protocol-v2/DeviceGetOnboardingStatus';
51
51
  export { default as deviceUploadWallpaper } from './protocol-v2/DeviceUploadWallpaper';
52
+ export { default as deviceUploadNft } from './protocol-v2/DeviceUploadNft';
52
53
  export { default as uploadPortfolio } from './UploadPortfolio';
53
54
 
54
55
  export { default as cipherKeyValue } from './CipherKeyValue';
@@ -0,0 +1,166 @@
1
+ import { createDeviceNotSupportMethodError } from '@onekeyfe/hd-shared';
2
+
3
+ import { UI_REQUEST, createUiMessage } from '../../events/ui-request';
4
+ import { supportsProtocolV2Message } from '../../protocols/protocol-v2/features';
5
+ import {
6
+ PRO2_NFT_DEFAULT_CHUNK_SIZE,
7
+ PRO2_NFT_DEFAULT_PACE_MS,
8
+ PRO2_NFT_DEFAULT_TIMEOUT_MS,
9
+ PRO2_NFT_DIRECTORY,
10
+ PRO2_NFT_MAX_CHUNK_SIZE,
11
+ PRO2_NFT_MIN_CHUNK_SIZE,
12
+ type Pro2NftBundle,
13
+ type Pro2NftImage,
14
+ buildPro2NftBundle,
15
+ } from '../../utils/pro2Nft';
16
+ import { BaseMethod } from '../BaseMethod';
17
+ import { invalidParameter } from '../helpers/filesystemValidation';
18
+ import { isProtocolV2ResponseTimeout, writeProtocolV2File } from '../helpers/protocolV2FileWrite';
19
+
20
+ export type DeviceUploadNftParams = {
21
+ image: Pro2NftImage;
22
+ thumbnail: Pro2NftImage;
23
+ title: string;
24
+ subtitle: string;
25
+ timestampMs?: number;
26
+ chunkSize?: number;
27
+ paceMs?: number;
28
+ timeoutMs?: number;
29
+ };
30
+
31
+ export type DeviceUploadNftResponse = {
32
+ basename: string;
33
+ imagePath: string;
34
+ thumbnailPath: string;
35
+ metadataPath: string;
36
+ totalSize: number;
37
+ nftUpdated: true;
38
+ message?: string;
39
+ };
40
+
41
+ const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
42
+ const NFT_UPDATE_MESSAGE_TYPE = 61500;
43
+
44
+ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
45
+ private bundle?: Pro2NftBundle;
46
+
47
+ getSupportedProtocols() {
48
+ return ['V2'] as const;
49
+ }
50
+
51
+ init() {
52
+ const {
53
+ image,
54
+ thumbnail,
55
+ title,
56
+ subtitle,
57
+ timestampMs = Date.now(),
58
+ chunkSize = PRO2_NFT_DEFAULT_CHUNK_SIZE,
59
+ paceMs = PRO2_NFT_DEFAULT_PACE_MS,
60
+ timeoutMs = PRO2_NFT_DEFAULT_TIMEOUT_MS,
61
+ } = this.payload;
62
+ if (
63
+ !Number.isInteger(chunkSize) ||
64
+ chunkSize < PRO2_NFT_MIN_CHUNK_SIZE ||
65
+ chunkSize > PRO2_NFT_MAX_CHUNK_SIZE
66
+ ) {
67
+ throw invalidParameter(
68
+ `Parameter [chunkSize] must be an integer between ${PRO2_NFT_MIN_CHUNK_SIZE} and ${PRO2_NFT_MAX_CHUNK_SIZE}.`
69
+ );
70
+ }
71
+ if (!Number.isInteger(paceMs) || paceMs < 0) {
72
+ throw invalidParameter('Parameter [paceMs] must be a non-negative integer.');
73
+ }
74
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
75
+ throw invalidParameter('Parameter [timeoutMs] must be a positive integer.');
76
+ }
77
+
78
+ this.bundle = buildPro2NftBundle({ image, thumbnail, title, subtitle, timestampMs });
79
+ this.params = { image, thumbnail, title, subtitle, timestampMs, chunkSize, paceMs, timeoutMs };
80
+ this.unlockPolicy = 'none';
81
+ this.skipForceUpdateCheck = true;
82
+ this.useDevicePassphraseState = false;
83
+ }
84
+
85
+ private async assertCapabilities() {
86
+ const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
87
+ const hasFileWrite = supportsProtocolV2Message(
88
+ protocolInfo,
89
+ FILESYSTEM_FILE_WRITE_MESSAGE_TYPE
90
+ );
91
+ const hasNftUpdate = supportsProtocolV2Message(protocolInfo, NFT_UPDATE_MESSAGE_TYPE);
92
+ if (!hasFileWrite || !hasNftUpdate) {
93
+ throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType());
94
+ }
95
+ }
96
+
97
+ private async updateNft(basename: string) {
98
+ const params = { file_name_no_ext: basename };
99
+ const options = { timeoutMs: this.params.timeoutMs };
100
+ try {
101
+ return await this.device.commands.typedCall('NftUpdate', 'Success', params, options);
102
+ } catch (error) {
103
+ if (!isProtocolV2ResponseTimeout(error)) {
104
+ throw error;
105
+ }
106
+ this.throwIfAborted();
107
+ return this.device.commands.typedCall('NftUpdate', 'Success', params, options);
108
+ }
109
+ }
110
+
111
+ async run(): Promise<DeviceUploadNftResponse> {
112
+ const { bundle } = this;
113
+ if (!bundle) throw invalidParameter('NFT data has not been initialized.');
114
+
115
+ await this.assertCapabilities();
116
+
117
+ const files = [
118
+ { path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.bin`, data: bundle.image },
119
+ { path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}_m.bin`, data: bundle.thumbnail },
120
+ { path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.json`, data: bundle.metadata },
121
+ ];
122
+ const totalSize = files.reduce((sum, file) => sum + file.data.byteLength, 0);
123
+ let transferredBeforeFile = 0;
124
+
125
+ for (const file of files) {
126
+ const transferredAtFileStart = transferredBeforeFile;
127
+ await writeProtocolV2File({
128
+ commands: this.device.commands,
129
+ path: file.path,
130
+ data: file.data,
131
+ totalSize: file.data.byteLength,
132
+ chunkSize: this.params.chunkSize,
133
+ timeoutMs: this.params.timeoutMs,
134
+ paceMs: this.params.paceMs,
135
+ overwrite: true,
136
+ append: false,
137
+ throwIfAborted: () => this.throwIfAborted(),
138
+ onProgress: progress => {
139
+ if (typeof this.postMessage !== 'function') return;
140
+ const transferredBytes = transferredAtFileStart + progress.transferredBytes;
141
+ this.postMessage(
142
+ createUiMessage(UI_REQUEST.DEVICE_PROGRESS, {
143
+ ...progress,
144
+ progress: Math.floor((transferredBytes / totalSize) * 100),
145
+ transferredBytes,
146
+ totalBytes: totalSize,
147
+ })
148
+ );
149
+ },
150
+ });
151
+ transferredBeforeFile += file.data.byteLength;
152
+ }
153
+
154
+ this.throwIfAborted();
155
+ const response = await this.updateNft(bundle.basename);
156
+ return {
157
+ basename: bundle.basename,
158
+ imagePath: files[0].path,
159
+ thumbnailPath: files[1].path,
160
+ metadataPath: files[2].path,
161
+ totalSize,
162
+ nftUpdated: true,
163
+ message: response.message?.message,
164
+ };
165
+ }
166
+ }