@onekeyfe/hardware-cli 1.2.0-alpha.17 → 1.2.0-alpha.170

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/src/cli.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
2
3
  import { Command } from 'commander';
3
4
  import { UI_EVENT, UI_REQUEST, getDeviceType } from '@onekeyfe/hd-core';
4
- import { EDeviceType } from '@onekeyfe/hd-shared';
5
+ import { EDeviceType, isSameOnekeyBleName } from '@onekeyfe/hd-shared';
5
6
 
6
7
  import {
7
8
  resolveBatchGetAddress,
@@ -11,65 +12,30 @@ import {
11
12
  resolveSignTransaction,
12
13
  } from './chains';
13
14
  import { selectSearchDevice } from './deviceSelection';
15
+ import { getCanonicalDeviceState, getCompatibleFeatures } from './deviceStateCommands';
14
16
  import { createSDK, disposeSDK } from './sdk';
15
- import {
16
- clearSessionFromKeychain,
17
- preloadSessionFromKeychain,
18
- saveSessionToKeychain,
19
- } from './session';
17
+ import { clearSessionFromKeychain, preloadSessionFromKeychain } from './session';
20
18
 
21
19
  import type {
20
+ DeviceStateScope,
22
21
  EthereumSignTypedDataMessage,
23
22
  EthereumSignTypedDataTypes,
24
23
  Features,
25
- IDeviceType,
26
24
  SearchDevice,
27
25
  } from '@onekeyfe/hd-core';
28
26
 
29
27
  /** SearchDevice enriched with features fetched after discovery */
30
28
  type EnrichedSearchDevice = SearchDevice & { features?: Features };
31
29
 
32
- function extractPassphraseSession(payload: unknown): {
33
- passphraseState?: string;
34
- sessionId?: string;
35
- } {
36
- if (typeof payload === 'string') {
37
- return { passphraseState: payload };
38
- }
39
- if (!payload || typeof payload !== 'object') {
40
- return {};
41
- }
42
-
43
- const statePayload = payload as {
44
- passphrase_state?: unknown;
45
- passphraseState?: unknown;
46
- session_id?: unknown;
47
- sessionId?: unknown;
48
- };
49
-
50
- let passphraseState: string | undefined;
51
- if (typeof statePayload.passphrase_state === 'string') {
52
- passphraseState = statePayload.passphrase_state;
53
- } else if (typeof statePayload.passphraseState === 'string') {
54
- passphraseState = statePayload.passphraseState;
55
- }
56
-
57
- let sessionId: string | undefined;
58
- if (typeof statePayload.session_id === 'string') {
59
- sessionId = statePayload.session_id;
60
- } else if (typeof statePayload.sessionId === 'string') {
61
- sessionId = statePayload.sessionId;
62
- }
63
-
64
- return { passphraseState, sessionId };
65
- }
66
-
67
30
  const program = new Command();
31
+ const { version: cliVersion } = JSON.parse(
32
+ readFileSync(resolve(__dirname, '../package.json'), 'utf8')
33
+ ) as { version: string };
68
34
 
69
35
  program
70
36
  .name('onekey-hw')
71
37
  .description('OneKey hardware wallet CLI for AI agent integration')
72
- .version('1.1.26-alpha.1');
38
+ .version(cliVersion);
73
39
 
74
40
  // ============================================================
75
41
  // Global Options
@@ -95,28 +61,6 @@ program
95
61
  .action(() =>
96
62
  runCommand({}, async ({ sdk, globalOpts }) => {
97
63
  const result = await sdk.searchDevices();
98
-
99
- // USB 下自动读取 features 成本低;BLE 搜索阶段只做枚举,避免批量连接导致超时。
100
- if (globalOpts.transport !== 'ble' && result?.success && Array.isArray(result.payload)) {
101
- for (const device of result.payload as EnrichedSearchDevice[]) {
102
- if (device.connectId) {
103
- try {
104
- const features = await sdk.getFeatures(device.connectId);
105
- if (features?.success && features.payload) {
106
- device.features = features.payload;
107
- device.name = features.payload.label || features.payload.bleName || device.name;
108
- const devType = features.payload.deviceType?.toLowerCase();
109
- if (devType) {
110
- device.deviceType = devType as IDeviceType;
111
- }
112
- }
113
- } catch {
114
- // Features fetch failed — device may need PIN, continue with basic info
115
- }
116
- }
117
- }
118
- }
119
-
120
64
  outputResult(globalOpts, result);
121
65
  })
122
66
  );
@@ -126,24 +70,28 @@ program
126
70
  .description('Get device features (firmware, unlock state, passphrase protection, etc.)')
127
71
  .action(() =>
128
72
  runCommand({}, async ({ sdk, globalOpts }) => {
129
- // Resolve connectId: explicit flag wins, else pick the first attached device
130
- let { connectId } = globalOpts as { connectId?: string };
131
- if (!connectId) {
132
- const searchResult = await sdk.searchDevices();
133
- if (
134
- !searchResult?.success ||
135
- !Array.isArray(searchResult.payload) ||
136
- searchResult.payload.length === 0
137
- ) {
138
- outputResult(globalOpts, {
139
- success: false,
140
- payload: { error: 'No device found', code: 'NO_DEVICE' },
141
- });
142
- return;
143
- }
144
- connectId = (searchResult.payload[0] as EnrichedSearchDevice).connectId ?? undefined;
73
+ const result = await getCompatibleFeatures(sdk, globalOpts.connectId);
74
+ outputResult(globalOpts, result);
75
+ })
76
+ );
77
+
78
+ program
79
+ .command('get-state')
80
+ .description('Get canonical device state for Protocol V1 and Protocol V2 devices')
81
+ .option('--scope <scope>', 'State refresh scope: runtime, settings, or firmware', 'runtime')
82
+ .action((opts: { scope: string }) =>
83
+ runCommand({}, async ({ sdk, globalOpts }) => {
84
+ const supportedScopes: DeviceStateScope[] = ['runtime', 'settings', 'firmware'];
85
+ if (!supportedScopes.includes(opts.scope as DeviceStateScope)) {
86
+ const error = new Error(`Unsupported device state scope: ${opts.scope}`);
87
+ (error as Error & { code?: string }).code = 'INVALID_DEVICE_STATE_SCOPE';
88
+ throw error;
145
89
  }
146
- const result = await sdk.getFeatures(connectId || '');
90
+ const result = await getCanonicalDeviceState(
91
+ sdk,
92
+ globalOpts.connectId,
93
+ opts.scope as DeviceStateScope
94
+ );
147
95
  outputResult(globalOpts, result);
148
96
  })
149
97
  );
@@ -151,18 +99,12 @@ program
151
99
  program
152
100
  .command('upload-wallpaper')
153
101
  .description('Upload and activate a Pro2 wallpaper')
154
- .requiredOption('--rgba <path>', '604x1024 raw RGBA file')
102
+ .requiredOption('--jpeg <path>', '604x1024 JPEG file')
155
103
  .option('--file-name <name>', 'Device wallpaper file name', 'wallpaper-cli')
156
104
  .option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
157
105
  .action(opts =>
158
106
  runCommand({}, async ({ sdk, globalOpts, params }) => {
159
- const rgba = readBinaryParam(opts.rgba);
160
- const expectedBytes = 604 * 1024 * 4;
161
- if (rgba.byteLength !== expectedBytes) {
162
- throw new Error(
163
- `Invalid RGBA size: expected ${expectedBytes} bytes, received ${rgba.byteLength}`
164
- );
165
- }
107
+ const jpegBase64 = readFileSync(opts.jpeg).toString('base64');
166
108
 
167
109
  let transferStartedAt: number | undefined;
168
110
  let transferEndedAt: number | undefined;
@@ -211,9 +153,7 @@ program
211
153
  try {
212
154
  result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
213
155
  ...params,
214
- width: 604,
215
- height: 1024,
216
- rgba,
156
+ jpegBase64,
217
157
  fileName: opts.fileName,
218
158
  chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
219
159
  });
@@ -680,10 +620,6 @@ program
680
620
  .command('firmware-update-v4')
681
621
  .description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
682
622
  .option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
683
- .option(
684
- '--resource-bundle <spec...>',
685
- 'RESC bundle direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg'
686
- )
687
623
  .option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
688
624
  .option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
689
625
  .option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
@@ -693,6 +629,7 @@ program
693
629
  .option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
694
630
  .option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
695
631
  .option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
632
+ .option('--resource-archive <path>', 'Complete signed Protocol V2 resource ZIP path')
696
633
  .option('--forced-update-res', 'Force resource update')
697
634
  .option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
698
635
  .action(opts =>
@@ -848,7 +785,7 @@ const sessionCmd = program.command('session').description('Manage device passphr
848
785
 
849
786
  sessionCmd
850
787
  .command('connect')
851
- .description('Connect device and establish passphrase session (cached for subsequent commands)')
788
+ .description('Connect device and select a hidden wallet for this invocation')
852
789
  .action(() =>
853
790
  runCommand({}, async ({ sdk, globalOpts }) => {
854
791
  // 1. Search for device
@@ -860,7 +797,17 @@ sessionCmd
860
797
  });
861
798
  return;
862
799
  }
863
- const device = searchResult.payload[0] as EnrichedSearchDevice;
800
+ const device = selectSearchDevice(
801
+ searchResult.payload as Array<SearchDevice & { features?: Features }>,
802
+ globalOpts.connectId
803
+ );
804
+ if (!device) {
805
+ outputResult(globalOpts, {
806
+ success: false,
807
+ payload: { error: 'No matching device found', code: 'NO_DEVICE' },
808
+ });
809
+ return;
810
+ }
864
811
  const connectId = device.connectId || globalOpts.connectId;
865
812
 
866
813
  // 2. Unlock if locked — getPassphraseState below talks to a live
@@ -871,62 +818,35 @@ sessionCmd
871
818
  await unlockWithRetry(sdk, connectId);
872
819
  }
873
820
 
874
- // 3. Get passphraseState (triggers 1/2/3 selection)
875
- const psResult = await sdk.getPassphraseState(connectId, {
876
- initSession: true,
877
- useEmptyPassphrase: false,
821
+ // 3. Open a hidden wallet session (triggers 1/2/3 selection).
822
+ const sessionResult = await sdk.openWalletSession(connectId, {
823
+ mode: 'select-hidden',
878
824
  });
879
- if (!psResult.success) {
880
- outputResult(globalOpts, psResult);
825
+ if (!sessionResult.success) {
826
+ outputResult(globalOpts, sessionResult);
881
827
  return;
882
828
  }
883
- const { passphraseState, sessionId: passphraseSessionId } = extractPassphraseSession(
884
- psResult.payload
885
- );
886
- if (!passphraseState) {
829
+ if (sessionResult.payload.walletType !== 'hidden') {
887
830
  outputResult(globalOpts, {
888
831
  success: false,
889
- payload: { error: 'getPassphraseState did not return passphraseState' },
832
+ payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
890
833
  });
891
834
  return;
892
835
  }
836
+ const { deviceId, passphraseState } = sessionResult.payload;
893
837
 
894
838
  // 4. Get address to verify + extract deviceId
895
- const addrResult = await sdk.evmGetAddress(connectId, device.deviceId || '', {
839
+ const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
896
840
  path: "m/44'/60'/0'/0/0",
897
841
  showOnOneKey: false,
898
842
  passphraseState,
899
843
  });
900
844
 
901
- // 5. Fetch the now-active session_id via getFeatures.
902
- //
903
- // IMPORTANT: pass `passphraseState` here. Without it, the SDK's
904
- // connectStateChange guard (core/index.ts) would see the payload's
905
- // passphraseState flip from mnNy → undefined, clear the cached Device,
906
- // and call Initialize again with no passphrase_state / no session_id.
907
- // That Initialize resets the device to the standard wallet and returns
908
- // a *standard-wallet* session_id — which we'd then save in the keychain
909
- // paired with the hidden-wallet passphraseState. On the next CLI run
910
- // the mismatch would trigger PassphraseRequest (1/2/3 again).
911
- const featResult = await sdk.getFeatures(connectId, {
912
- passphraseState,
913
- skipPassphraseCheck: true,
914
- });
915
- const featPayload = featResult?.success ? featResult.payload : undefined;
916
- const deviceId = featPayload?.deviceId || device.deviceId || '';
917
- const sessionId = passphraseSessionId || featPayload?.sessionId || '';
918
-
919
- // 6. Save to keychain
920
- if (passphraseState && deviceId && sessionId) {
921
- await saveSessionToKeychain(deviceId, passphraseState, sessionId);
922
- }
923
-
924
845
  outputResult(globalOpts, {
925
846
  success: true,
926
847
  payload: {
927
848
  passphraseState,
928
849
  deviceId,
929
- ...(sessionId ? { sessionId } : {}),
930
850
  ...(addrResult?.success ? { address: addrResult.payload.address } : {}),
931
851
  },
932
852
  });
@@ -939,8 +859,10 @@ sessionCmd
939
859
  .action(() =>
940
860
  runCommand({}, async ({ sdk, globalOpts }) => {
941
861
  const searchResult = await sdk.searchDevices();
942
- const device = // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
943
- (searchResult?.payload as any)?.[0];
862
+ const device = selectSearchDevice(
863
+ (searchResult?.payload as Array<SearchDevice & { features?: Features }>) ?? [],
864
+ globalOpts.connectId
865
+ );
944
866
  const deviceId = device?.deviceId || device?.features?.device_id;
945
867
  if (deviceId) {
946
868
  await clearSessionFromKeychain(deviceId);
@@ -1035,12 +957,12 @@ async function unlockWithRetry(
1035
957
  * Prepare passphrase session before SDK calls.
1036
958
  *
1037
959
  * 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
1038
- * 2. Try keychain → preloadSessionCache → use cached session
1039
- * 3. Keychain miss → getPassphraseState (triggers 1/2/3 prompt) → save to keychain
960
+ * 2. Try a legacy keychain entry → preloadSessionCache → use cached session
961
+ * 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
1040
962
  *
1041
963
  * After this, globalOpts.passphraseState is set and getCommonParams will include it.
1042
964
  */
1043
- async function prepareSession(
965
+ export async function prepareSession(
1044
966
  sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default,
1045
967
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1046
968
  globalOpts: Record<string, any>
@@ -1051,7 +973,7 @@ async function prepareSession(
1051
973
  }
1052
974
 
1053
975
  // Errors from the SDK calls below (PIN cancelled, transport broken,
1054
- // getPassphraseState rejection) intentionally propagate to runCommand's
976
+ // openWalletSession rejection) intentionally propagate to runCommand's
1055
977
  // catch block, which renders them as structured `{ success: false,
1056
978
  // payload: { error, code } }` output instead of silently falling through
1057
979
  // to a confusing downstream error 112 / 114.
@@ -1070,6 +992,7 @@ async function prepareSession(
1070
992
  searchResult.payload as Array<{
1071
993
  connectId?: string;
1072
994
  deviceId?: string;
995
+ deviceType?: string;
1073
996
  features?: {
1074
997
  deviceId?: string | null;
1075
998
  deviceType?: string;
@@ -1088,6 +1011,7 @@ async function prepareSession(
1088
1011
  const selectedDevice = device as {
1089
1012
  connectId?: string;
1090
1013
  deviceId?: string;
1014
+ deviceType?: string;
1091
1015
  features?: {
1092
1016
  deviceId?: string | null;
1093
1017
  deviceType?: string;
@@ -1105,7 +1029,8 @@ async function prepareSession(
1105
1029
  // getFeatures failures here are non-fatal — we fall through to Step 3
1106
1030
  // which will fail with a clearer error if the device is truly unreachable.
1107
1031
  let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
1108
- let deviceType = getDeviceType(selectedDevice.features as Features | undefined);
1032
+ let deviceType =
1033
+ selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? EDeviceType.Unknown;
1109
1034
  let unlocked = selectedDevice.features?.unlocked;
1110
1035
  let passphraseProtection = selectedDevice.features?.passphraseProtection;
1111
1036
 
@@ -1146,7 +1071,7 @@ async function prepareSession(
1146
1071
  return undefined;
1147
1072
  }
1148
1073
 
1149
- // ── Step 5: Try keychain session reuse ───────────────────────────
1074
+ // ── Step 5: Try legacy keychain session reuse ────────────────────
1150
1075
  // Only attempt if device was already unlocked — locking invalidates
1151
1076
  // all passphrase sessions, so cached session_id is useless after unlock.
1152
1077
  if (!wasLocked && deviceId) {
@@ -1157,40 +1082,19 @@ async function prepareSession(
1157
1082
  }
1158
1083
  }
1159
1084
 
1160
- // ── Step 6: Keychain miss → getPassphraseState (triggers 1/2/3 prompt) ──
1161
- const psResult = await sdk.getPassphraseState(connectId, {
1162
- initSession: true,
1163
- useEmptyPassphrase: false,
1085
+ // ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
1086
+ const sessionResult = await sdk.openWalletSession(connectId, {
1087
+ mode: 'select-hidden',
1164
1088
  });
1165
1089
 
1166
- if (psResult.success && psResult.payload) {
1167
- const { passphraseState, sessionId: passphraseSessionId } = extractPassphraseSession(
1168
- psResult.payload
1169
- );
1170
- if (!passphraseState) {
1090
+ if (sessionResult.success && sessionResult.payload) {
1091
+ if (sessionResult.payload.walletType !== 'hidden') {
1171
1092
  return undefined;
1172
1093
  }
1094
+ const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
1095
+ globalOpts.deviceId = sessionDeviceId;
1173
1096
  globalOpts.passphraseState = passphraseState;
1174
1097
 
1175
- // Save session to keychain for next invocation.
1176
- //
1177
- // Pass passphraseState to keep connectStateChange=false — otherwise
1178
- // Initialize would be re-run without passphrase_state, resetting the
1179
- // device to the standard wallet and returning a mismatched session_id.
1180
- // See the matching comment in `session connect`.
1181
- if (deviceId) {
1182
- const featAfter = await sdk.getFeatures(connectId, {
1183
- passphraseState,
1184
- skipPassphraseCheck: true,
1185
- });
1186
- const sessionId =
1187
- passphraseSessionId || (featAfter?.success ? featAfter.payload?.sessionId : undefined);
1188
- if (sessionId) {
1189
- await saveSessionToKeychain(deviceId, passphraseState, sessionId);
1190
- await preloadSessionFromKeychain(deviceId);
1191
- }
1192
- }
1193
-
1194
1098
  return passphraseState;
1195
1099
  }
1196
1100
  return undefined;
@@ -1207,8 +1111,8 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
1207
1111
  ) {
1208
1112
  process.exitCode = 1;
1209
1113
  }
1210
- // No process.exit here — runCommand() below handles dispose + exit so SDK
1211
- // async cleanup (USB release, event listener teardown) finishes first.
1114
+ // No process.exit here — runCommand() waits for SDK cleanup, then lets Node
1115
+ // exit naturally so leaked USB handles remain observable.
1212
1116
  }
1213
1117
 
1214
1118
  /**
@@ -1220,7 +1124,7 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
1220
1124
  * 3. run the handler (which calls outputResult on success)
1221
1125
  * 4. report uncaught errors as a structured failure result
1222
1126
  * 5. dispose SDK
1223
- * 6. drain event loop and process.exit with the right code
1127
+ * 6. let Node exit naturally after all SDK resources are released
1224
1128
  *
1225
1129
  * This fixes three previous bugs:
1226
1130
  * - Most signing commands skipped prepareSession, so keychain sessions
@@ -1272,9 +1176,7 @@ async function runCommand(
1272
1176
  // promise reference. Idempotent, safe to call even if init failed.
1273
1177
  await disposeSDK();
1274
1178
  }
1275
- // SDK event listeners can keep the event loop alive after dispose.
1276
- // setImmediate lets any trailing stdout/stderr writes flush first.
1277
- setImmediate(() => process.exit(process.exitCode ?? 0));
1179
+ // disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
1278
1180
  }
1279
1181
 
1280
1182
  /** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
@@ -1315,52 +1217,37 @@ async function resolveLegacyFirmwareConnectId(
1315
1217
  }
1316
1218
 
1317
1219
  const devices = searchResult.payload as EnrichedSearchDevice[];
1318
- const normalizedName = deviceName?.trim().toLowerCase();
1319
- const matches = normalizedName
1320
- ? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
1220
+ const requestedName = deviceName?.trim();
1221
+ const matches = requestedName
1222
+ ? devices.filter(
1223
+ device =>
1224
+ isSameOnekeyBleName(device.name, requestedName) ||
1225
+ device.name?.trim().toLowerCase() === requestedName.toLowerCase()
1226
+ )
1321
1227
  : devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
1322
1228
 
1323
1229
  if (matches.length === 0) {
1324
1230
  throw new Error(
1325
- normalizedName ? `BLE device not found by name: ${deviceName}` : 'No Classic/Pure BLE device found'
1231
+ requestedName
1232
+ ? `BLE device not found by name: ${deviceName}`
1233
+ : 'No Classic/Pure BLE device found'
1326
1234
  );
1327
1235
  }
1328
1236
  if (matches.length > 1) {
1329
1237
  throw new Error(
1330
- normalizedName
1238
+ requestedName
1331
1239
  ? `Multiple BLE devices found by name: ${deviceName}`
1332
1240
  : 'Multiple Classic/Pure BLE devices found; specify --device-name'
1333
1241
  );
1334
1242
  }
1335
1243
 
1336
- const connectId = matches[0].connectId;
1337
- if (!connectId) throw new Error(`BLE device has no connect ID: ${matches[0].name}`);
1244
+ const [{ connectId, name }] = matches;
1245
+ if (!connectId) throw new Error(`BLE device has no connect ID: ${name}`);
1338
1246
  return connectId;
1339
1247
  }
1340
1248
 
1341
- function parseResourceBundleParam(spec: string): { binary: ArrayBuffer; devicePath: string } {
1342
- const sep = spec.indexOf(':');
1343
- if (sep <= 0 || sep === spec.length - 1) {
1344
- throw new Error(
1345
- `Invalid --resource-bundle value: "${spec}". Expected <localPath>:<devicePath>`
1346
- );
1347
- }
1348
- const localPath = spec.slice(0, sep);
1349
- const devicePath = spec.slice(sep + 1);
1350
- if (!devicePath.startsWith('vol')) {
1351
- throw new Error(
1352
- `Invalid --resource-bundle device path: "${devicePath}". Expected a vol*:/... path`
1353
- );
1354
- }
1355
- return {
1356
- binary: readBinaryParam(localPath),
1357
- devicePath,
1358
- };
1359
- }
1360
-
1361
1249
  function getFirmwareUpdateV4TotalBytes(params: ReturnType<typeof buildFirmwareUpdateV4Params>) {
1362
1250
  return [
1363
- ...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
1364
1251
  params.bootloaderBinary,
1365
1252
  params.applicationP1Binary,
1366
1253
  params.applicationP2Binary,
@@ -1429,9 +1316,7 @@ export function buildWallpaperUploadMetrics({
1429
1316
  transferredBytes,
1430
1317
  totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
1431
1318
  transferKiBPerSecond:
1432
- elapsedMs > 0
1433
- ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2))
1434
- : null,
1319
+ elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
1435
1320
  lastProgress,
1436
1321
  };
1437
1322
  }
@@ -1532,7 +1417,7 @@ function buildFirmwareUpdateV4Metrics({
1532
1417
  };
1533
1418
  }
1534
1419
 
1535
- async function runFirmwareUpdateV4WithRetry({
1420
+ export async function runFirmwareUpdateV4WithRetry({
1536
1421
  sdk,
1537
1422
  globalOpts,
1538
1423
  params,
@@ -1546,152 +1431,147 @@ async function runFirmwareUpdateV4WithRetry({
1546
1431
  const totalBytes = getFirmwareUpdateV4TotalBytes(params);
1547
1432
  const maxAttempts = Math.max((retries ?? 2) + 1, 1);
1548
1433
  let currentSdk = sdk;
1549
- let lastResult: unknown;
1550
1434
  let retried = false;
1551
-
1552
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1553
- let progressEvents = 0;
1554
- let lastProgress = -1;
1555
- let transferStartedAt: number | undefined;
1556
- let transferEndedAt: number | undefined;
1557
- let installProgressEvents = 0;
1558
- let lastInstallProgress = -1;
1559
- let installStartedAt: number | undefined;
1560
- let installEndedAt: number | undefined;
1561
- let lastPrintedTransferProgress = -10;
1562
- let lastPrintedInstallProgress = -10;
1563
- const totalStartedAt = Date.now();
1564
- const connectId =
1565
- retried && globalOpts.transport === 'usb' && globalOpts.connectId
1566
- ? undefined
1567
- : globalOpts.connectId;
1568
-
1569
- const onUiEvent = (message: unknown) => {
1570
- if (!message || typeof message !== 'object') return;
1571
- const messageType = (message as { type?: string }).type;
1572
- const payload = getFirmwareUpdatePayload(message);
1573
-
1574
- if (messageType === UI_REQUEST.FIRMWARE_TIP) {
1575
- const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
1576
- if (typeof tipMessage === 'string') {
1577
- process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1578
- }
1579
- return;
1580
- }
1581
-
1582
- if (messageType === UI_REQUEST.REQUEST_BUTTON) {
1583
- const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1584
- process.stderr.write(
1585
- `[onekey-hw] Please confirm the firmware update on your device${code}.\n`
1586
- );
1587
- return;
1435
+ let attempt = 1;
1436
+ let { connectId } = globalOpts;
1437
+
1438
+ if (globalOpts.transport === 'usb') {
1439
+ for (; attempt <= maxAttempts; attempt += 1) {
1440
+ const probeResult = await currentSdk.getDeviceState(connectId, {
1441
+ scope: 'runtime',
1442
+ connectProtocol: 'V2',
1443
+ retryCount: 0,
1444
+ });
1445
+ if (isSuccessResult(probeResult)) break;
1446
+ if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
1447
+ return probeResult;
1588
1448
  }
1589
1449
 
1590
- if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
1591
- const progress = Number(payload.progress);
1592
- if (!Number.isFinite(progress)) return;
1593
-
1594
- if (payload.progressType === 'transferData') {
1595
- progressEvents += 1;
1596
- lastProgress = Math.max(lastProgress, progress);
1597
- transferStartedAt ??= Date.now();
1598
- lastPrintedTransferProgress = maybePrintFirmwareProgress({
1599
- progressType: 'transfer',
1600
- progress,
1601
- payload,
1602
- lastPrintedProgress: lastPrintedTransferProgress,
1603
- });
1604
- if (progress >= 100) {
1605
- transferEndedAt ??= Date.now();
1606
- }
1607
- return;
1608
- }
1450
+ retried = true;
1451
+ process.stderr.write(
1452
+ `[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`
1453
+ );
1454
+ await disposeSDK();
1455
+ await new Promise(resolve => {
1456
+ setTimeout(resolve, 3000);
1457
+ });
1458
+ currentSdk = await createSDK(globalOpts);
1459
+ if (globalOpts.connectId) connectId = undefined;
1460
+ }
1461
+ }
1609
1462
 
1610
- if (payload.progressType === 'installingFirmware') {
1611
- installProgressEvents += 1;
1612
- lastInstallProgress = Math.max(lastInstallProgress, progress);
1613
- installStartedAt ??= Date.now();
1614
- lastPrintedInstallProgress = maybePrintFirmwareProgress({
1615
- progressType: 'install',
1616
- progress,
1617
- payload,
1618
- lastPrintedProgress: lastPrintedInstallProgress,
1619
- });
1620
- if (progress >= 100) {
1621
- installEndedAt ??= Date.now();
1622
- }
1463
+ let progressEvents = 0;
1464
+ let lastProgress = -1;
1465
+ let transferStartedAt: number | undefined;
1466
+ let transferEndedAt: number | undefined;
1467
+ let installProgressEvents = 0;
1468
+ let lastInstallProgress = -1;
1469
+ let installStartedAt: number | undefined;
1470
+ let installEndedAt: number | undefined;
1471
+ let lastPrintedTransferProgress = -10;
1472
+ let lastPrintedInstallProgress = -10;
1473
+ const totalStartedAt = Date.now();
1474
+
1475
+ const onUiEvent = (message: unknown) => {
1476
+ if (!message || typeof message !== 'object') return;
1477
+ const messageType = (message as { type?: string }).type;
1478
+ const payload = getFirmwareUpdatePayload(message);
1479
+
1480
+ if (messageType === UI_REQUEST.FIRMWARE_TIP) {
1481
+ const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
1482
+ if (typeof tipMessage === 'string') {
1483
+ process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1623
1484
  }
1624
- };
1625
-
1626
- currentSdk.on(UI_EVENT, onUiEvent);
1627
- try {
1628
- lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
1629
- } finally {
1630
- currentSdk.off?.(UI_EVENT, onUiEvent);
1631
- }
1632
- if (installStartedAt !== undefined && installEndedAt === undefined) {
1633
- installEndedAt = Date.now();
1485
+ return;
1634
1486
  }
1635
1487
 
1636
- const metrics = buildFirmwareUpdateV4Metrics({
1637
- attempt,
1638
- maxAttempts,
1639
- totalBytes,
1640
- totalStartedAt,
1641
- transferStartedAt,
1642
- transferEndedAt,
1643
- installStartedAt,
1644
- installEndedAt,
1645
- progressEvents,
1646
- lastProgress,
1647
- installProgressEvents,
1648
- lastInstallProgress,
1649
- retried,
1650
- });
1651
-
1652
- if (lastResult && typeof lastResult === 'object') {
1653
- const payload = ((lastResult as { payload?: unknown }).payload ?? {}) as Record<
1654
- string,
1655
- unknown
1656
- >;
1657
- lastResult = {
1658
- ...(lastResult as Record<string, unknown>),
1659
- payload: {
1660
- ...payload,
1661
- metrics,
1662
- },
1663
- };
1488
+ if (messageType === UI_REQUEST.REQUEST_BUTTON) {
1489
+ const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1490
+ process.stderr.write(
1491
+ `[onekey-hw] Please confirm the firmware update on your device${code}.\n`
1492
+ );
1493
+ return;
1664
1494
  }
1665
1495
 
1666
- if (isSuccessResult(lastResult)) {
1667
- return lastResult;
1496
+ if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
1497
+ const progress = Number(payload.progress);
1498
+ if (!Number.isFinite(progress)) return;
1499
+
1500
+ if (payload.progressType === 'transferData') {
1501
+ progressEvents += 1;
1502
+ lastProgress = Math.max(lastProgress, progress);
1503
+ transferStartedAt ??= Date.now();
1504
+ lastPrintedTransferProgress = maybePrintFirmwareProgress({
1505
+ progressType: 'transfer',
1506
+ progress,
1507
+ payload,
1508
+ lastPrintedProgress: lastPrintedTransferProgress,
1509
+ });
1510
+ if (progress >= 100) {
1511
+ transferEndedAt ??= Date.now();
1512
+ }
1513
+ return;
1668
1514
  }
1669
1515
 
1670
- if (
1671
- attempt >= maxAttempts ||
1672
- globalOpts.transport !== 'usb' ||
1673
- !isProtocolV2UsbProbeTransientResult(lastResult)
1674
- ) {
1675
- return lastResult;
1516
+ if (payload.progressType === 'installingFirmware') {
1517
+ installProgressEvents += 1;
1518
+ lastInstallProgress = Math.max(lastInstallProgress, progress);
1519
+ installStartedAt ??= Date.now();
1520
+ lastPrintedInstallProgress = maybePrintFirmwareProgress({
1521
+ progressType: 'install',
1522
+ progress,
1523
+ payload,
1524
+ lastPrintedProgress: lastPrintedInstallProgress,
1525
+ });
1526
+ if (progress >= 100) {
1527
+ installEndedAt ??= Date.now();
1528
+ }
1676
1529
  }
1530
+ };
1677
1531
 
1678
- retried = true;
1679
- process.stderr.write(
1680
- `[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`
1681
- );
1682
- await disposeSDK();
1683
- await new Promise(resolve => {
1684
- setTimeout(resolve, 3000);
1685
- });
1686
- currentSdk = await createSDK(globalOpts);
1532
+ currentSdk.on(UI_EVENT, onUiEvent);
1533
+ let result: unknown;
1534
+ try {
1535
+ result = await currentSdk.firmwareUpdateV4(connectId, params);
1536
+ } finally {
1537
+ currentSdk.off?.(UI_EVENT, onUiEvent);
1538
+ }
1539
+ if (installStartedAt !== undefined && installEndedAt === undefined) {
1540
+ installEndedAt = Date.now();
1541
+ }
1542
+
1543
+ const metrics = buildFirmwareUpdateV4Metrics({
1544
+ attempt,
1545
+ maxAttempts,
1546
+ totalBytes,
1547
+ totalStartedAt,
1548
+ transferStartedAt,
1549
+ transferEndedAt,
1550
+ installStartedAt,
1551
+ installEndedAt,
1552
+ progressEvents,
1553
+ lastProgress,
1554
+ installProgressEvents,
1555
+ lastInstallProgress,
1556
+ retried,
1557
+ });
1558
+
1559
+ if (result && typeof result === 'object') {
1560
+ const payload = ((result as { payload?: unknown }).payload ?? {}) as Record<string, unknown>;
1561
+ return {
1562
+ ...(result as Record<string, unknown>),
1563
+ payload: {
1564
+ ...payload,
1565
+ metrics,
1566
+ },
1567
+ };
1687
1568
  }
1688
1569
 
1689
- return lastResult;
1570
+ return result;
1690
1571
  }
1691
1572
 
1692
1573
  function buildFirmwareUpdateV4Params(opts: {
1693
1574
  chunkSize?: string;
1694
- resourceBundle?: string[];
1695
1575
  romloader?: string;
1696
1576
  bootloader?: string;
1697
1577
  applicationP1?: string;
@@ -1701,6 +1581,7 @@ function buildFirmwareUpdateV4Params(opts: {
1701
1581
  se02?: string;
1702
1582
  se03?: string;
1703
1583
  se04?: string;
1584
+ resourceArchive?: string;
1704
1585
  forcedUpdateRes?: boolean;
1705
1586
  }) {
1706
1587
  const params = {
@@ -1708,7 +1589,6 @@ function buildFirmwareUpdateV4Params(opts: {
1708
1589
  connectProtocol: 'V2' as const,
1709
1590
  chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
1710
1591
  forcedUpdateRes: opts.forcedUpdateRes,
1711
- resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
1712
1592
  romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
1713
1593
  bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
1714
1594
  applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
@@ -1718,10 +1598,10 @@ function buildFirmwareUpdateV4Params(opts: {
1718
1598
  se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
1719
1599
  se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
1720
1600
  se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
1601
+ resourceArchiveBinary: opts.resourceArchive ? readBinaryParam(opts.resourceArchive) : undefined,
1721
1602
  };
1722
1603
 
1723
1604
  const hasPayload = [
1724
- params.resourceBundleFiles,
1725
1605
  params.romloaderBinary,
1726
1606
  params.bootloaderBinary,
1727
1607
  params.applicationP1Binary,
@@ -1731,10 +1611,13 @@ function buildFirmwareUpdateV4Params(opts: {
1731
1611
  params.se02Binary,
1732
1612
  params.se03Binary,
1733
1613
  params.se04Binary,
1614
+ params.resourceArchiveBinary,
1734
1615
  ].some(Boolean);
1735
1616
 
1736
1617
  if (!hasPayload) {
1737
- const err = new Error('firmware-update-v4 requires at least one binary path');
1618
+ const err = new Error(
1619
+ 'firmware-update-v4 requires at least one firmware binary or resource archive path'
1620
+ );
1738
1621
  (err as Error & { code?: string }).code = 'MISSING_FIRMWARE_BINARY';
1739
1622
  throw err;
1740
1623
  }