@onekeyfe/hardware-cli 1.2.0-alpha.3 → 1.2.0-alpha.31
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/dist/cli.d.ts +27 -1
- package/dist/cli.js +502 -139
- package/dist/deviceSelection.d.ts +3 -0
- package/dist/deviceSelection.js +11 -0
- package/dist/deviceStateCommands.d.ts +19 -0
- package/dist/deviceStateCommands.js +62 -0
- package/dist/sdk.d.ts +4 -2
- package/dist/sdk.js +10 -8
- package/dist/session.d.ts +2 -9
- package/dist/session.js +3 -22
- package/dist/transports/nobleBlePlugin.d.ts +2 -0
- package/dist/transports/nobleBlePlugin.js +384 -0
- package/package.json +8 -6
- package/src/__tests__/cli-version.test.ts +8 -0
- package/src/__tests__/device-selection.test.ts +29 -0
- package/src/__tests__/device-state-commands.test.ts +118 -0
- package/src/__tests__/firmware-update-legacy-command.test.ts +18 -0
- package/src/__tests__/firmware-update-v4-command.test.ts +19 -0
- package/src/__tests__/noble-ble-plugin.test.ts +229 -0
- package/src/__tests__/wallet-session.test.ts +55 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +46 -0
- package/src/cli.ts +699 -171
- package/src/deviceSelection.ts +13 -0
- package/src/deviceStateCommands.ts +71 -0
- package/src/sdk.ts +17 -9
- package/src/session.ts +2 -24
- package/src/transports/nobleBlePlugin.ts +498 -0
package/src/cli.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
1
3
|
import { Command } from 'commander';
|
|
4
|
+
import { UI_EVENT, UI_REQUEST, getDeviceType } from '@onekeyfe/hd-core';
|
|
5
|
+
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
2
6
|
|
|
3
|
-
import { createSDK, disposeSDK } from './sdk';
|
|
4
|
-
import {
|
|
5
|
-
clearSessionFromKeychain,
|
|
6
|
-
preloadSessionFromKeychain,
|
|
7
|
-
saveSessionToKeychain,
|
|
8
|
-
} from './session';
|
|
9
7
|
import {
|
|
10
8
|
resolveBatchGetAddress,
|
|
11
9
|
resolveGetAddress,
|
|
@@ -13,61 +11,31 @@ import {
|
|
|
13
11
|
resolveSignMessage,
|
|
14
12
|
resolveSignTransaction,
|
|
15
13
|
} from './chains';
|
|
14
|
+
import { selectSearchDevice } from './deviceSelection';
|
|
15
|
+
import { getCanonicalDeviceState, getCompatibleFeatures } from './deviceStateCommands';
|
|
16
|
+
import { createSDK, disposeSDK } from './sdk';
|
|
17
|
+
import { clearSessionFromKeychain, preloadSessionFromKeychain } from './session';
|
|
16
18
|
|
|
17
|
-
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
18
|
-
import { getDeviceType } from '@onekeyfe/hd-core';
|
|
19
19
|
import type {
|
|
20
|
+
DeviceStateScope,
|
|
20
21
|
EthereumSignTypedDataMessage,
|
|
21
22
|
EthereumSignTypedDataTypes,
|
|
22
23
|
Features,
|
|
23
|
-
IDeviceType,
|
|
24
24
|
SearchDevice,
|
|
25
25
|
} from '@onekeyfe/hd-core';
|
|
26
26
|
|
|
27
27
|
/** SearchDevice enriched with features fetched after discovery */
|
|
28
28
|
type EnrichedSearchDevice = SearchDevice & { features?: Features };
|
|
29
29
|
|
|
30
|
-
function extractPassphraseSession(payload: unknown): {
|
|
31
|
-
passphraseState?: string;
|
|
32
|
-
sessionId?: string;
|
|
33
|
-
} {
|
|
34
|
-
if (typeof payload === 'string') {
|
|
35
|
-
return { passphraseState: payload };
|
|
36
|
-
}
|
|
37
|
-
if (!payload || typeof payload !== 'object') {
|
|
38
|
-
return {};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const statePayload = payload as {
|
|
42
|
-
passphrase_state?: unknown;
|
|
43
|
-
passphraseState?: unknown;
|
|
44
|
-
session_id?: unknown;
|
|
45
|
-
sessionId?: unknown;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
let passphraseState: string | undefined;
|
|
49
|
-
if (typeof statePayload.passphrase_state === 'string') {
|
|
50
|
-
passphraseState = statePayload.passphrase_state;
|
|
51
|
-
} else if (typeof statePayload.passphraseState === 'string') {
|
|
52
|
-
passphraseState = statePayload.passphraseState;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let sessionId: string | undefined;
|
|
56
|
-
if (typeof statePayload.session_id === 'string') {
|
|
57
|
-
sessionId = statePayload.session_id;
|
|
58
|
-
} else if (typeof statePayload.sessionId === 'string') {
|
|
59
|
-
sessionId = statePayload.sessionId;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { passphraseState, sessionId };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
30
|
const program = new Command();
|
|
31
|
+
const { version: cliVersion } = JSON.parse(
|
|
32
|
+
readFileSync(resolve(__dirname, '../package.json'), 'utf8')
|
|
33
|
+
) as { version: string };
|
|
66
34
|
|
|
67
35
|
program
|
|
68
36
|
.name('onekey-hw')
|
|
69
37
|
.description('OneKey hardware wallet CLI for AI agent integration')
|
|
70
|
-
.version(
|
|
38
|
+
.version(cliVersion);
|
|
71
39
|
|
|
72
40
|
// ============================================================
|
|
73
41
|
// Global Options
|
|
@@ -78,8 +46,10 @@ program.option(
|
|
|
78
46
|
'--device-id <id>',
|
|
79
47
|
'Persistent device ID from getFeatures (changes when seed changes)'
|
|
80
48
|
);
|
|
49
|
+
program.option('--transport <transport>', 'Transport to use: usb or ble', 'usb');
|
|
81
50
|
program.option('--passphrase-state <state>', 'Passphrase state for hidden wallet access');
|
|
82
51
|
program.option('--use-empty-passphrase', 'Use standard wallet (skip passphrase prompt)');
|
|
52
|
+
program.option('--debug', 'Enable SDK debug logs');
|
|
83
53
|
|
|
84
54
|
// ============================================================
|
|
85
55
|
// Device Commands
|
|
@@ -91,28 +61,6 @@ program
|
|
|
91
61
|
.action(() =>
|
|
92
62
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
93
63
|
const result = await sdk.searchDevices();
|
|
94
|
-
|
|
95
|
-
// Auto-fetch features for each discovered device (doesn't require PIN)
|
|
96
|
-
if (result?.success && Array.isArray(result.payload)) {
|
|
97
|
-
for (const device of result.payload as EnrichedSearchDevice[]) {
|
|
98
|
-
if (device.connectId) {
|
|
99
|
-
try {
|
|
100
|
-
const features = await sdk.getFeatures(device.connectId);
|
|
101
|
-
if (features?.success && features.payload) {
|
|
102
|
-
device.features = features.payload;
|
|
103
|
-
device.name = features.payload.label || features.payload.bleName || device.name;
|
|
104
|
-
const devType = features.payload.deviceType?.toLowerCase();
|
|
105
|
-
if (devType) {
|
|
106
|
-
device.deviceType = devType as IDeviceType;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch {
|
|
110
|
-
// Features fetch failed — device may need PIN, continue with basic info
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
64
|
outputResult(globalOpts, result);
|
|
117
65
|
})
|
|
118
66
|
);
|
|
@@ -122,28 +70,120 @@ program
|
|
|
122
70
|
.description('Get device features (firmware, unlock state, passphrase protection, etc.)')
|
|
123
71
|
.action(() =>
|
|
124
72
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
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;
|
|
141
89
|
}
|
|
142
|
-
const result = await
|
|
90
|
+
const result = await getCanonicalDeviceState(
|
|
91
|
+
sdk,
|
|
92
|
+
globalOpts.connectId,
|
|
93
|
+
opts.scope as DeviceStateScope
|
|
94
|
+
);
|
|
143
95
|
outputResult(globalOpts, result);
|
|
144
96
|
})
|
|
145
97
|
);
|
|
146
98
|
|
|
99
|
+
program
|
|
100
|
+
.command('upload-wallpaper')
|
|
101
|
+
.description('Upload and activate a Pro2 wallpaper')
|
|
102
|
+
.requiredOption('--rgba <path>', '604x1024 raw RGBA file')
|
|
103
|
+
.option('--file-name <name>', 'Device wallpaper file name', 'wallpaper-cli')
|
|
104
|
+
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
105
|
+
.action(opts =>
|
|
106
|
+
runCommand({}, async ({ sdk, globalOpts, params }) => {
|
|
107
|
+
const rgba = readBinaryParam(opts.rgba);
|
|
108
|
+
const expectedBytes = 604 * 1024 * 4;
|
|
109
|
+
if (rgba.byteLength !== expectedBytes) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Invalid RGBA size: expected ${expectedBytes} bytes, received ${rgba.byteLength}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let transferStartedAt: number | undefined;
|
|
116
|
+
let transferEndedAt: number | undefined;
|
|
117
|
+
let lastProgress = -1;
|
|
118
|
+
let lastPrintedProgress = -10;
|
|
119
|
+
let progressTotalBytes = 0;
|
|
120
|
+
let transferredBytes = 0;
|
|
121
|
+
const totalStartedAt = Date.now();
|
|
122
|
+
const onUiEvent = (message: unknown) => {
|
|
123
|
+
if (!message || typeof message !== 'object') return;
|
|
124
|
+
const event = message as {
|
|
125
|
+
type?: string;
|
|
126
|
+
payload?: {
|
|
127
|
+
progress?: number;
|
|
128
|
+
transferredBytes?: number;
|
|
129
|
+
totalBytes?: number;
|
|
130
|
+
rateBytesPerSecond?: number;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
if (event.type !== UI_REQUEST.DEVICE_PROGRESS || !event.payload) return;
|
|
134
|
+
const progress = Number(event.payload.progress);
|
|
135
|
+
if (!Number.isFinite(progress)) return;
|
|
136
|
+
transferStartedAt ??= Date.now();
|
|
137
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
138
|
+
const totalBytes = Number(event.payload.totalBytes);
|
|
139
|
+
if (Number.isFinite(totalBytes) && totalBytes > 0) progressTotalBytes = totalBytes;
|
|
140
|
+
const confirmedBytes = Number(event.payload.transferredBytes);
|
|
141
|
+
if (Number.isFinite(confirmedBytes) && confirmedBytes >= 0) {
|
|
142
|
+
transferredBytes = Math.max(transferredBytes, confirmedBytes);
|
|
143
|
+
}
|
|
144
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
145
|
+
if (printableProgress > lastPrintedProgress || progress >= 100) {
|
|
146
|
+
const rate = Number(event.payload.rateBytesPerSecond);
|
|
147
|
+
const rateText =
|
|
148
|
+
Number.isFinite(rate) && rate > 0 ? ` ${(rate / 1024).toFixed(2)} KiB/s` : '';
|
|
149
|
+
process.stderr.write(
|
|
150
|
+
`[onekey-hw] Wallpaper transfer: ${Math.round(progress)}%${rateText}\n`
|
|
151
|
+
);
|
|
152
|
+
lastPrintedProgress = progress >= 100 ? 100 : printableProgress;
|
|
153
|
+
}
|
|
154
|
+
if (progress >= 100) transferEndedAt ??= Date.now();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
sdk.on(UI_EVENT, onUiEvent);
|
|
158
|
+
let result: any;
|
|
159
|
+
try {
|
|
160
|
+
result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
|
|
161
|
+
...params,
|
|
162
|
+
width: 604,
|
|
163
|
+
height: 1024,
|
|
164
|
+
rgba,
|
|
165
|
+
fileName: opts.fileName,
|
|
166
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
167
|
+
});
|
|
168
|
+
} finally {
|
|
169
|
+
sdk.off?.(UI_EVENT, onUiEvent);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const endedAt = transferEndedAt ?? Date.now();
|
|
173
|
+
const totalBytes = Number(result?.payload?.size) || progressTotalBytes;
|
|
174
|
+
outputResult(globalOpts, {
|
|
175
|
+
...result,
|
|
176
|
+
metrics: buildWallpaperUploadMetrics({
|
|
177
|
+
totalBytes,
|
|
178
|
+
transferredBytes: result?.success ? totalBytes : transferredBytes,
|
|
179
|
+
startedAt: transferStartedAt ?? totalStartedAt,
|
|
180
|
+
endedAt,
|
|
181
|
+
lastProgress,
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
|
|
147
187
|
// ============================================================
|
|
148
188
|
// Signing Commands
|
|
149
189
|
// ============================================================
|
|
@@ -538,20 +578,84 @@ program
|
|
|
538
578
|
})
|
|
539
579
|
);
|
|
540
580
|
|
|
581
|
+
program
|
|
582
|
+
.command('firmware-update-legacy')
|
|
583
|
+
.description('Update Classic/Pure firmware through the legacy protocol')
|
|
584
|
+
.requiredOption('--binary <path>', 'Local firmware binary path')
|
|
585
|
+
.option('--device-name <name>', 'BLE advertising name, for example K1514')
|
|
586
|
+
.option('--update-type <type>', 'Firmware component: firmware or ble', 'firmware')
|
|
587
|
+
.option('--no-reboot', 'Do not reboot the device after a successful update')
|
|
588
|
+
.action(opts =>
|
|
589
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
590
|
+
if (opts.updateType !== 'firmware' && opts.updateType !== 'ble') {
|
|
591
|
+
throw new Error(`Unsupported --update-type: ${opts.updateType}. Use "firmware" or "ble".`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const connectId = await resolveLegacyFirmwareConnectId(
|
|
595
|
+
sdk,
|
|
596
|
+
globalOpts.connectId,
|
|
597
|
+
opts.deviceName
|
|
598
|
+
);
|
|
599
|
+
const result = await sdk.firmwareUpdate(connectId, {
|
|
600
|
+
binary: readBinaryParam(opts.binary),
|
|
601
|
+
updateType: opts.updateType,
|
|
602
|
+
rebootOnSuccess: opts.reboot,
|
|
603
|
+
timeout: getLegacyFirmwareConnectTimeout(globalOpts.transport),
|
|
604
|
+
});
|
|
605
|
+
outputResult(globalOpts, result);
|
|
606
|
+
})
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
export function getLegacyFirmwareConnectTimeout(transport: 'usb' | 'ble') {
|
|
610
|
+
return transport === 'usb' ? 90_000 : undefined;
|
|
611
|
+
}
|
|
612
|
+
|
|
541
613
|
program
|
|
542
614
|
.command('firmware-update-ble')
|
|
543
|
-
.description('
|
|
615
|
+
.description('Run Protocol V2 firmware update over BLE')
|
|
544
616
|
.action(() =>
|
|
545
617
|
respondAndExit({
|
|
546
618
|
success: false,
|
|
547
619
|
payload: {
|
|
548
620
|
error:
|
|
549
|
-
'
|
|
550
|
-
code: '
|
|
621
|
+
'Use `onekey-hw --transport ble firmware-update-v4` for BLE Protocol V2 firmware updates.',
|
|
622
|
+
code: 'USE_FIRMWARE_UPDATE_V4',
|
|
551
623
|
},
|
|
552
624
|
})
|
|
553
625
|
);
|
|
554
626
|
|
|
627
|
+
program
|
|
628
|
+
.command('firmware-update-v4')
|
|
629
|
+
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
630
|
+
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
631
|
+
.option(
|
|
632
|
+
'--resource-bundle <spec...>',
|
|
633
|
+
'RESC bundle direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg'
|
|
634
|
+
)
|
|
635
|
+
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
636
|
+
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
637
|
+
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
638
|
+
.option('--application-p2 <path>', 'FW_MGMT_TARGET_APPLICATION_P2 binary path')
|
|
639
|
+
.option('--coprocessor <path>', 'FW_MGMT_TARGET_COPROCESSOR binary path')
|
|
640
|
+
.option('--se01 <path>', 'FW_MGMT_TARGET_SE01 binary path')
|
|
641
|
+
.option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
|
|
642
|
+
.option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
|
|
643
|
+
.option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
|
|
644
|
+
.option('--forced-update-res', 'Force resource update')
|
|
645
|
+
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
646
|
+
.action(opts =>
|
|
647
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
648
|
+
const params = buildFirmwareUpdateV4Params(opts);
|
|
649
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
650
|
+
sdk,
|
|
651
|
+
globalOpts,
|
|
652
|
+
params,
|
|
653
|
+
retries: opts.retries ? safeParseInt(opts.retries, '--retries') : undefined,
|
|
654
|
+
});
|
|
655
|
+
outputResult(globalOpts, result);
|
|
656
|
+
})
|
|
657
|
+
);
|
|
658
|
+
|
|
555
659
|
program
|
|
556
660
|
.command('bootloader-check')
|
|
557
661
|
.description('Check bootloader version and status')
|
|
@@ -692,7 +796,7 @@ const sessionCmd = program.command('session').description('Manage device passphr
|
|
|
692
796
|
|
|
693
797
|
sessionCmd
|
|
694
798
|
.command('connect')
|
|
695
|
-
.description('Connect device and
|
|
799
|
+
.description('Connect device and select a hidden wallet for this invocation')
|
|
696
800
|
.action(() =>
|
|
697
801
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
698
802
|
// 1. Search for device
|
|
@@ -704,7 +808,7 @@ sessionCmd
|
|
|
704
808
|
});
|
|
705
809
|
return;
|
|
706
810
|
}
|
|
707
|
-
const device = searchResult.payload[0] as
|
|
811
|
+
const device = searchResult.payload[0] as SearchDevice & { features?: Features };
|
|
708
812
|
const connectId = device.connectId || globalOpts.connectId;
|
|
709
813
|
|
|
710
814
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
@@ -715,62 +819,35 @@ sessionCmd
|
|
|
715
819
|
await unlockWithRetry(sdk, connectId);
|
|
716
820
|
}
|
|
717
821
|
|
|
718
|
-
// 3.
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
useEmptyPassphrase: false,
|
|
822
|
+
// 3. Open a hidden wallet session (triggers 1/2/3 selection).
|
|
823
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
824
|
+
mode: 'select-hidden',
|
|
722
825
|
});
|
|
723
|
-
if (!
|
|
724
|
-
outputResult(globalOpts,
|
|
826
|
+
if (!sessionResult.success) {
|
|
827
|
+
outputResult(globalOpts, sessionResult);
|
|
725
828
|
return;
|
|
726
829
|
}
|
|
727
|
-
|
|
728
|
-
psResult.payload
|
|
729
|
-
);
|
|
730
|
-
if (!passphraseState) {
|
|
830
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
731
831
|
outputResult(globalOpts, {
|
|
732
832
|
success: false,
|
|
733
|
-
payload: { error: '
|
|
833
|
+
payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
|
|
734
834
|
});
|
|
735
835
|
return;
|
|
736
836
|
}
|
|
837
|
+
const { deviceId, passphraseState } = sessionResult.payload;
|
|
737
838
|
|
|
738
839
|
// 4. Get address to verify + extract deviceId
|
|
739
|
-
const addrResult = await sdk.evmGetAddress(connectId,
|
|
840
|
+
const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
|
|
740
841
|
path: "m/44'/60'/0'/0/0",
|
|
741
842
|
showOnOneKey: false,
|
|
742
843
|
passphraseState,
|
|
743
844
|
});
|
|
744
845
|
|
|
745
|
-
// 5. Fetch the now-active session_id via getFeatures.
|
|
746
|
-
//
|
|
747
|
-
// IMPORTANT: pass `passphraseState` here. Without it, the SDK's
|
|
748
|
-
// connectStateChange guard (core/index.ts) would see the payload's
|
|
749
|
-
// passphraseState flip from mnNy → undefined, clear the cached Device,
|
|
750
|
-
// and call Initialize again with no passphrase_state / no session_id.
|
|
751
|
-
// That Initialize resets the device to the standard wallet and returns
|
|
752
|
-
// a *standard-wallet* session_id — which we'd then save in the keychain
|
|
753
|
-
// paired with the hidden-wallet passphraseState. On the next CLI run
|
|
754
|
-
// the mismatch would trigger PassphraseRequest (1/2/3 again).
|
|
755
|
-
const featResult = await sdk.getFeatures(connectId, {
|
|
756
|
-
passphraseState,
|
|
757
|
-
skipPassphraseCheck: true,
|
|
758
|
-
});
|
|
759
|
-
const featPayload = featResult?.success ? featResult.payload : undefined;
|
|
760
|
-
const deviceId = featPayload?.deviceId || device.deviceId || '';
|
|
761
|
-
const sessionId = passphraseSessionId || featPayload?.sessionId || '';
|
|
762
|
-
|
|
763
|
-
// 6. Save to keychain
|
|
764
|
-
if (passphraseState && deviceId && sessionId) {
|
|
765
|
-
await saveSessionToKeychain(deviceId, passphraseState, sessionId);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
846
|
outputResult(globalOpts, {
|
|
769
847
|
success: true,
|
|
770
848
|
payload: {
|
|
771
849
|
passphraseState,
|
|
772
850
|
deviceId,
|
|
773
|
-
...(sessionId ? { sessionId } : {}),
|
|
774
851
|
...(addrResult?.success ? { address: addrResult.payload.address } : {}),
|
|
775
852
|
},
|
|
776
853
|
});
|
|
@@ -879,12 +956,12 @@ async function unlockWithRetry(
|
|
|
879
956
|
* Prepare passphrase session before SDK calls.
|
|
880
957
|
*
|
|
881
958
|
* 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
|
|
882
|
-
* 2. Try keychain → preloadSessionCache → use cached session
|
|
883
|
-
* 3. Keychain miss →
|
|
959
|
+
* 2. Try a legacy keychain entry → preloadSessionCache → use cached session
|
|
960
|
+
* 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
|
|
884
961
|
*
|
|
885
962
|
* After this, globalOpts.passphraseState is set and getCommonParams will include it.
|
|
886
963
|
*/
|
|
887
|
-
async function prepareSession(
|
|
964
|
+
export async function prepareSession(
|
|
888
965
|
sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default,
|
|
889
966
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
890
967
|
globalOpts: Record<string, any>
|
|
@@ -895,7 +972,7 @@ async function prepareSession(
|
|
|
895
972
|
}
|
|
896
973
|
|
|
897
974
|
// Errors from the SDK calls below (PIN cancelled, transport broken,
|
|
898
|
-
//
|
|
975
|
+
// openWalletSession rejection) intentionally propagate to runCommand's
|
|
899
976
|
// catch block, which renders them as structured `{ success: false,
|
|
900
977
|
// payload: { error, code } }` output instead of silently falling through
|
|
901
978
|
// to a confusing downstream error 112 / 114.
|
|
@@ -910,9 +987,30 @@ async function prepareSession(
|
|
|
910
987
|
return undefined;
|
|
911
988
|
}
|
|
912
989
|
|
|
913
|
-
const device =
|
|
990
|
+
const device = selectSearchDevice(
|
|
991
|
+
searchResult.payload as Array<{
|
|
992
|
+
connectId?: string;
|
|
993
|
+
deviceId?: string;
|
|
994
|
+
deviceType?: string;
|
|
995
|
+
features?: {
|
|
996
|
+
deviceId?: string | null;
|
|
997
|
+
deviceType?: string;
|
|
998
|
+
sessionId?: string | null;
|
|
999
|
+
passphraseProtection?: boolean | null;
|
|
1000
|
+
unlocked?: boolean | null;
|
|
1001
|
+
};
|
|
1002
|
+
}>,
|
|
1003
|
+
globalOpts.connectId
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
if (!device) {
|
|
1007
|
+
throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const selectedDevice = device as {
|
|
914
1011
|
connectId?: string;
|
|
915
1012
|
deviceId?: string;
|
|
1013
|
+
deviceType?: string;
|
|
916
1014
|
features?: {
|
|
917
1015
|
deviceId?: string | null;
|
|
918
1016
|
deviceType?: string;
|
|
@@ -921,7 +1019,7 @@ async function prepareSession(
|
|
|
921
1019
|
unlocked?: boolean | null;
|
|
922
1020
|
};
|
|
923
1021
|
};
|
|
924
|
-
const connectId =
|
|
1022
|
+
const connectId = selectedDevice.connectId || globalOpts.connectId || '';
|
|
925
1023
|
if (!globalOpts.connectId && connectId) {
|
|
926
1024
|
globalOpts.connectId = connectId;
|
|
927
1025
|
}
|
|
@@ -929,10 +1027,11 @@ async function prepareSession(
|
|
|
929
1027
|
// ── Step 2: Get features if searchDevices didn't populate them ──
|
|
930
1028
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
931
1029
|
// which will fail with a clearer error if the device is truly unreachable.
|
|
932
|
-
let deviceId =
|
|
933
|
-
let deviceType =
|
|
934
|
-
|
|
935
|
-
let
|
|
1030
|
+
let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
|
|
1031
|
+
let deviceType =
|
|
1032
|
+
selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? EDeviceType.Unknown;
|
|
1033
|
+
let unlocked = selectedDevice.features?.unlocked;
|
|
1034
|
+
let passphraseProtection = selectedDevice.features?.passphraseProtection;
|
|
936
1035
|
|
|
937
1036
|
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
938
1037
|
try {
|
|
@@ -971,7 +1070,7 @@ async function prepareSession(
|
|
|
971
1070
|
return undefined;
|
|
972
1071
|
}
|
|
973
1072
|
|
|
974
|
-
// ── Step 5: Try keychain session reuse
|
|
1073
|
+
// ── Step 5: Try legacy keychain session reuse ────────────────────
|
|
975
1074
|
// Only attempt if device was already unlocked — locking invalidates
|
|
976
1075
|
// all passphrase sessions, so cached session_id is useless after unlock.
|
|
977
1076
|
if (!wasLocked && deviceId) {
|
|
@@ -982,40 +1081,19 @@ async function prepareSession(
|
|
|
982
1081
|
}
|
|
983
1082
|
}
|
|
984
1083
|
|
|
985
|
-
// ── Step 6: Keychain miss →
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
useEmptyPassphrase: false,
|
|
1084
|
+
// ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
|
|
1085
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
1086
|
+
mode: 'select-hidden',
|
|
989
1087
|
});
|
|
990
1088
|
|
|
991
|
-
if (
|
|
992
|
-
|
|
993
|
-
psResult.payload
|
|
994
|
-
);
|
|
995
|
-
if (!passphraseState) {
|
|
1089
|
+
if (sessionResult.success && sessionResult.payload) {
|
|
1090
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
996
1091
|
return undefined;
|
|
997
1092
|
}
|
|
1093
|
+
const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
|
|
1094
|
+
globalOpts.deviceId = sessionDeviceId;
|
|
998
1095
|
globalOpts.passphraseState = passphraseState;
|
|
999
1096
|
|
|
1000
|
-
// Save session to keychain for next invocation.
|
|
1001
|
-
//
|
|
1002
|
-
// Pass passphraseState to keep connectStateChange=false — otherwise
|
|
1003
|
-
// Initialize would be re-run without passphrase_state, resetting the
|
|
1004
|
-
// device to the standard wallet and returning a mismatched session_id.
|
|
1005
|
-
// See the matching comment in `session connect`.
|
|
1006
|
-
if (deviceId) {
|
|
1007
|
-
const featAfter = await sdk.getFeatures(connectId, {
|
|
1008
|
-
passphraseState,
|
|
1009
|
-
skipPassphraseCheck: true,
|
|
1010
|
-
});
|
|
1011
|
-
const sessionId =
|
|
1012
|
-
passphraseSessionId || (featAfter?.success ? featAfter.payload?.sessionId : undefined);
|
|
1013
|
-
if (sessionId) {
|
|
1014
|
-
await saveSessionToKeychain(deviceId, passphraseState, sessionId);
|
|
1015
|
-
await preloadSessionFromKeychain(deviceId);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
1097
|
return passphraseState;
|
|
1020
1098
|
}
|
|
1021
1099
|
return undefined;
|
|
@@ -1032,8 +1110,8 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1032
1110
|
) {
|
|
1033
1111
|
process.exitCode = 1;
|
|
1034
1112
|
}
|
|
1035
|
-
// No process.exit here — runCommand()
|
|
1036
|
-
//
|
|
1113
|
+
// No process.exit here — runCommand() waits for SDK cleanup, then lets Node
|
|
1114
|
+
// exit naturally so leaked USB handles remain observable.
|
|
1037
1115
|
}
|
|
1038
1116
|
|
|
1039
1117
|
/**
|
|
@@ -1045,7 +1123,7 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1045
1123
|
* 3. run the handler (which calls outputResult on success)
|
|
1046
1124
|
* 4. report uncaught errors as a structured failure result
|
|
1047
1125
|
* 5. dispose SDK
|
|
1048
|
-
* 6.
|
|
1126
|
+
* 6. let Node exit naturally after all SDK resources are released
|
|
1049
1127
|
*
|
|
1050
1128
|
* This fixes three previous bugs:
|
|
1051
1129
|
* - Most signing commands skipped prepareSession, so keychain sessions
|
|
@@ -1071,6 +1149,9 @@ async function runCommand(
|
|
|
1071
1149
|
): Promise<void> {
|
|
1072
1150
|
const globalOpts = program.opts();
|
|
1073
1151
|
try {
|
|
1152
|
+
if (globalOpts.transport !== 'usb' && globalOpts.transport !== 'ble') {
|
|
1153
|
+
throw new Error(`Unsupported transport: ${globalOpts.transport}. Use "usb" or "ble".`);
|
|
1154
|
+
}
|
|
1074
1155
|
const sdk = await createSDK(globalOpts);
|
|
1075
1156
|
if (options.needsSession) {
|
|
1076
1157
|
await prepareSession(sdk, globalOpts);
|
|
@@ -1094,9 +1175,7 @@ async function runCommand(
|
|
|
1094
1175
|
// promise reference. Idempotent, safe to call even if init failed.
|
|
1095
1176
|
await disposeSDK();
|
|
1096
1177
|
}
|
|
1097
|
-
//
|
|
1098
|
-
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
1099
|
-
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
1178
|
+
// disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
|
|
1100
1179
|
}
|
|
1101
1180
|
|
|
1102
1181
|
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
@@ -1119,6 +1198,451 @@ function safeJsonParse(input: string, label: string): unknown {
|
|
|
1119
1198
|
}
|
|
1120
1199
|
}
|
|
1121
1200
|
|
|
1201
|
+
function readBinaryParam(path: string): ArrayBuffer {
|
|
1202
|
+
const buffer = readFileSync(path);
|
|
1203
|
+
return new Uint8Array(buffer).buffer;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async function resolveLegacyFirmwareConnectId(
|
|
1207
|
+
sdk: AnySdk,
|
|
1208
|
+
explicitConnectId?: string,
|
|
1209
|
+
deviceName?: string
|
|
1210
|
+
): Promise<string> {
|
|
1211
|
+
if (explicitConnectId && !deviceName) return explicitConnectId;
|
|
1212
|
+
|
|
1213
|
+
const searchResult = await sdk.searchDevices();
|
|
1214
|
+
if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
|
|
1215
|
+
throw new Error('Unable to scan BLE devices');
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const devices = searchResult.payload as EnrichedSearchDevice[];
|
|
1219
|
+
const normalizedName = deviceName?.trim().toLowerCase();
|
|
1220
|
+
const matches = normalizedName
|
|
1221
|
+
? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
|
|
1222
|
+
: devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
|
|
1223
|
+
|
|
1224
|
+
if (matches.length === 0) {
|
|
1225
|
+
throw new Error(
|
|
1226
|
+
normalizedName
|
|
1227
|
+
? `BLE device not found by name: ${deviceName}`
|
|
1228
|
+
: 'No Classic/Pure BLE device found'
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
if (matches.length > 1) {
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
normalizedName
|
|
1234
|
+
? `Multiple BLE devices found by name: ${deviceName}`
|
|
1235
|
+
: 'Multiple Classic/Pure BLE devices found; specify --device-name'
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const [{ connectId, name }] = matches;
|
|
1240
|
+
if (!connectId) throw new Error(`BLE device has no connect ID: ${name}`);
|
|
1241
|
+
return connectId;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function parseResourceBundleParam(spec: string): { binary: ArrayBuffer; devicePath: string } {
|
|
1245
|
+
const sep = spec.indexOf(':');
|
|
1246
|
+
if (sep <= 0 || sep === spec.length - 1) {
|
|
1247
|
+
throw new Error(
|
|
1248
|
+
`Invalid --resource-bundle value: "${spec}". Expected <localPath>:<devicePath>`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
const localPath = spec.slice(0, sep);
|
|
1252
|
+
const devicePath = spec.slice(sep + 1);
|
|
1253
|
+
if (!devicePath.startsWith('vol')) {
|
|
1254
|
+
throw new Error(
|
|
1255
|
+
`Invalid --resource-bundle device path: "${devicePath}". Expected a vol*:/... path`
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
binary: readBinaryParam(localPath),
|
|
1260
|
+
devicePath,
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function getFirmwareUpdateV4TotalBytes(params: ReturnType<typeof buildFirmwareUpdateV4Params>) {
|
|
1265
|
+
return [
|
|
1266
|
+
...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
|
|
1267
|
+
params.bootloaderBinary,
|
|
1268
|
+
params.applicationP1Binary,
|
|
1269
|
+
params.applicationP2Binary,
|
|
1270
|
+
params.coprocessorBinary,
|
|
1271
|
+
params.se01Binary,
|
|
1272
|
+
params.se02Binary,
|
|
1273
|
+
params.se03Binary,
|
|
1274
|
+
params.se04Binary,
|
|
1275
|
+
].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function getFirmwareUpdateV4ErrorText(result: unknown) {
|
|
1279
|
+
if (!result || typeof result !== 'object') return '';
|
|
1280
|
+
const { payload } = result as { payload?: unknown };
|
|
1281
|
+
if (!payload || typeof payload !== 'object') return '';
|
|
1282
|
+
const { error } = payload as { error?: unknown };
|
|
1283
|
+
return typeof error === 'string' ? error : '';
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function isProtocolV2UsbProbeTransientResult(result: unknown) {
|
|
1287
|
+
const error = getFirmwareUpdateV4ErrorText(result);
|
|
1288
|
+
return (
|
|
1289
|
+
error.includes('Device protocol mismatch') &&
|
|
1290
|
+
error.includes('expected V2') &&
|
|
1291
|
+
error.includes('did not respond to expected protocol')
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function isSuccessResult(result: unknown) {
|
|
1296
|
+
return (
|
|
1297
|
+
!!result && typeof result === 'object' && (result as { success?: boolean }).success === true
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function getFirmwareUpdatePayload(message: unknown) {
|
|
1302
|
+
if (!message || typeof message !== 'object') return undefined;
|
|
1303
|
+
return (message as { payload?: Record<string, unknown> }).payload;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function formatFirmwareProgress(progress: number) {
|
|
1307
|
+
if (!Number.isFinite(progress)) return '0%';
|
|
1308
|
+
return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function formatFirmwareBytes(bytes: number) {
|
|
1312
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '';
|
|
1313
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
export function buildWallpaperUploadMetrics({
|
|
1317
|
+
totalBytes,
|
|
1318
|
+
transferredBytes,
|
|
1319
|
+
startedAt,
|
|
1320
|
+
endedAt,
|
|
1321
|
+
lastProgress,
|
|
1322
|
+
}: {
|
|
1323
|
+
totalBytes: number;
|
|
1324
|
+
transferredBytes: number;
|
|
1325
|
+
startedAt: number;
|
|
1326
|
+
endedAt: number;
|
|
1327
|
+
lastProgress: number;
|
|
1328
|
+
}) {
|
|
1329
|
+
const elapsedMs = Math.max(endedAt - startedAt, 0);
|
|
1330
|
+
return {
|
|
1331
|
+
totalBytes,
|
|
1332
|
+
transferredBytes,
|
|
1333
|
+
totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
|
|
1334
|
+
transferKiBPerSecond:
|
|
1335
|
+
elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
|
|
1336
|
+
lastProgress,
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function maybePrintFirmwareProgress({
|
|
1341
|
+
progressType,
|
|
1342
|
+
progress,
|
|
1343
|
+
payload,
|
|
1344
|
+
lastPrintedProgress,
|
|
1345
|
+
}: {
|
|
1346
|
+
progressType: string;
|
|
1347
|
+
progress: number;
|
|
1348
|
+
payload: Record<string, unknown>;
|
|
1349
|
+
lastPrintedProgress: number;
|
|
1350
|
+
}) {
|
|
1351
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
1352
|
+
if (printableProgress <= lastPrintedProgress && progress < 100) {
|
|
1353
|
+
return lastPrintedProgress;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
const transferredBytes = Number(payload.transferredBytes);
|
|
1357
|
+
const totalBytes = Number(payload.totalBytes);
|
|
1358
|
+
const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
|
|
1359
|
+
const sizeText =
|
|
1360
|
+
Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
|
|
1361
|
+
? ` ${formatFirmwareBytes(transferredBytes)}/${formatFirmwareBytes(totalBytes)}`
|
|
1362
|
+
: '';
|
|
1363
|
+
const speedText =
|
|
1364
|
+
Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
|
|
1365
|
+
? ` ${(rateBytesPerSecond / 1024).toFixed(2)} KiB/s`
|
|
1366
|
+
: '';
|
|
1367
|
+
|
|
1368
|
+
process.stderr.write(
|
|
1369
|
+
`[onekey-hw] Firmware ${progressType}: ${formatFirmwareProgress(
|
|
1370
|
+
progress
|
|
1371
|
+
)}${sizeText}${speedText}\n`
|
|
1372
|
+
);
|
|
1373
|
+
return progress >= 100 ? 100 : printableProgress;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function buildFirmwareUpdateV4Metrics({
|
|
1377
|
+
attempt,
|
|
1378
|
+
maxAttempts,
|
|
1379
|
+
totalBytes,
|
|
1380
|
+
totalStartedAt,
|
|
1381
|
+
transferStartedAt,
|
|
1382
|
+
transferEndedAt,
|
|
1383
|
+
installStartedAt,
|
|
1384
|
+
installEndedAt,
|
|
1385
|
+
progressEvents,
|
|
1386
|
+
lastProgress,
|
|
1387
|
+
installProgressEvents,
|
|
1388
|
+
lastInstallProgress,
|
|
1389
|
+
retried,
|
|
1390
|
+
}: {
|
|
1391
|
+
attempt: number;
|
|
1392
|
+
maxAttempts: number;
|
|
1393
|
+
totalBytes: number;
|
|
1394
|
+
totalStartedAt: number;
|
|
1395
|
+
transferStartedAt?: number;
|
|
1396
|
+
transferEndedAt?: number;
|
|
1397
|
+
installStartedAt?: number;
|
|
1398
|
+
installEndedAt?: number;
|
|
1399
|
+
progressEvents: number;
|
|
1400
|
+
lastProgress: number;
|
|
1401
|
+
installProgressEvents: number;
|
|
1402
|
+
lastInstallProgress: number;
|
|
1403
|
+
retried: boolean;
|
|
1404
|
+
}) {
|
|
1405
|
+
const totalElapsedMs = Date.now() - totalStartedAt;
|
|
1406
|
+
const transferElapsedMs =
|
|
1407
|
+
transferStartedAt !== undefined && transferEndedAt !== undefined
|
|
1408
|
+
? transferEndedAt - transferStartedAt
|
|
1409
|
+
: undefined;
|
|
1410
|
+
const installElapsedMs =
|
|
1411
|
+
installStartedAt !== undefined && installEndedAt !== undefined
|
|
1412
|
+
? installEndedAt - installStartedAt
|
|
1413
|
+
: undefined;
|
|
1414
|
+
|
|
1415
|
+
return {
|
|
1416
|
+
attempt,
|
|
1417
|
+
maxAttempts,
|
|
1418
|
+
retried,
|
|
1419
|
+
totalBytes,
|
|
1420
|
+
progressEvents,
|
|
1421
|
+
lastProgress,
|
|
1422
|
+
installProgressEvents,
|
|
1423
|
+
lastInstallProgress,
|
|
1424
|
+
transferSeconds:
|
|
1425
|
+
transferElapsedMs !== undefined ? Number((transferElapsedMs / 1000).toFixed(2)) : null,
|
|
1426
|
+
transferKiBPerSecond:
|
|
1427
|
+
transferElapsedMs !== undefined && transferElapsedMs > 0
|
|
1428
|
+
? Number((totalBytes / 1024 / (transferElapsedMs / 1000)).toFixed(2))
|
|
1429
|
+
: null,
|
|
1430
|
+
installSeconds:
|
|
1431
|
+
installElapsedMs !== undefined ? Number((installElapsedMs / 1000).toFixed(2)) : null,
|
|
1432
|
+
totalSeconds: Number((totalElapsedMs / 1000).toFixed(2)),
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
async function runFirmwareUpdateV4WithRetry({
|
|
1437
|
+
sdk,
|
|
1438
|
+
globalOpts,
|
|
1439
|
+
params,
|
|
1440
|
+
retries,
|
|
1441
|
+
}: {
|
|
1442
|
+
sdk: AnySdk;
|
|
1443
|
+
globalOpts: Record<string, any>;
|
|
1444
|
+
params: ReturnType<typeof buildFirmwareUpdateV4Params>;
|
|
1445
|
+
retries?: number;
|
|
1446
|
+
}) {
|
|
1447
|
+
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1448
|
+
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1449
|
+
let currentSdk = sdk;
|
|
1450
|
+
let lastResult: unknown;
|
|
1451
|
+
let retried = false;
|
|
1452
|
+
|
|
1453
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1454
|
+
let progressEvents = 0;
|
|
1455
|
+
let lastProgress = -1;
|
|
1456
|
+
let transferStartedAt: number | undefined;
|
|
1457
|
+
let transferEndedAt: number | undefined;
|
|
1458
|
+
let installProgressEvents = 0;
|
|
1459
|
+
let lastInstallProgress = -1;
|
|
1460
|
+
let installStartedAt: number | undefined;
|
|
1461
|
+
let installEndedAt: number | undefined;
|
|
1462
|
+
let lastPrintedTransferProgress = -10;
|
|
1463
|
+
let lastPrintedInstallProgress = -10;
|
|
1464
|
+
const totalStartedAt = Date.now();
|
|
1465
|
+
const connectId =
|
|
1466
|
+
retried && globalOpts.transport === 'usb' && globalOpts.connectId
|
|
1467
|
+
? undefined
|
|
1468
|
+
: globalOpts.connectId;
|
|
1469
|
+
|
|
1470
|
+
const onUiEvent = (message: unknown) => {
|
|
1471
|
+
if (!message || typeof message !== 'object') return;
|
|
1472
|
+
const messageType = (message as { type?: string }).type;
|
|
1473
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1474
|
+
|
|
1475
|
+
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1476
|
+
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1477
|
+
if (typeof tipMessage === 'string') {
|
|
1478
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1479
|
+
}
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1484
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1485
|
+
process.stderr.write(
|
|
1486
|
+
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1487
|
+
);
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
|
|
1492
|
+
const progress = Number(payload.progress);
|
|
1493
|
+
if (!Number.isFinite(progress)) return;
|
|
1494
|
+
|
|
1495
|
+
if (payload.progressType === 'transferData') {
|
|
1496
|
+
progressEvents += 1;
|
|
1497
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1498
|
+
transferStartedAt ??= Date.now();
|
|
1499
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1500
|
+
progressType: 'transfer',
|
|
1501
|
+
progress,
|
|
1502
|
+
payload,
|
|
1503
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1504
|
+
});
|
|
1505
|
+
if (progress >= 100) {
|
|
1506
|
+
transferEndedAt ??= Date.now();
|
|
1507
|
+
}
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1512
|
+
installProgressEvents += 1;
|
|
1513
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1514
|
+
installStartedAt ??= Date.now();
|
|
1515
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1516
|
+
progressType: 'install',
|
|
1517
|
+
progress,
|
|
1518
|
+
payload,
|
|
1519
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1520
|
+
});
|
|
1521
|
+
if (progress >= 100) {
|
|
1522
|
+
installEndedAt ??= Date.now();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1528
|
+
try {
|
|
1529
|
+
lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1530
|
+
} finally {
|
|
1531
|
+
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1532
|
+
}
|
|
1533
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1534
|
+
installEndedAt = Date.now();
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1538
|
+
attempt,
|
|
1539
|
+
maxAttempts,
|
|
1540
|
+
totalBytes,
|
|
1541
|
+
totalStartedAt,
|
|
1542
|
+
transferStartedAt,
|
|
1543
|
+
transferEndedAt,
|
|
1544
|
+
installStartedAt,
|
|
1545
|
+
installEndedAt,
|
|
1546
|
+
progressEvents,
|
|
1547
|
+
lastProgress,
|
|
1548
|
+
installProgressEvents,
|
|
1549
|
+
lastInstallProgress,
|
|
1550
|
+
retried,
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
if (lastResult && typeof lastResult === 'object') {
|
|
1554
|
+
const payload = ((lastResult as { payload?: unknown }).payload ?? {}) as Record<
|
|
1555
|
+
string,
|
|
1556
|
+
unknown
|
|
1557
|
+
>;
|
|
1558
|
+
lastResult = {
|
|
1559
|
+
...(lastResult as Record<string, unknown>),
|
|
1560
|
+
payload: {
|
|
1561
|
+
...payload,
|
|
1562
|
+
metrics,
|
|
1563
|
+
},
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (isSuccessResult(lastResult)) {
|
|
1568
|
+
return lastResult;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (
|
|
1572
|
+
attempt >= maxAttempts ||
|
|
1573
|
+
globalOpts.transport !== 'usb' ||
|
|
1574
|
+
!isProtocolV2UsbProbeTransientResult(lastResult)
|
|
1575
|
+
) {
|
|
1576
|
+
return lastResult;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
retried = true;
|
|
1580
|
+
process.stderr.write(
|
|
1581
|
+
`[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`
|
|
1582
|
+
);
|
|
1583
|
+
await disposeSDK();
|
|
1584
|
+
await new Promise(resolve => {
|
|
1585
|
+
setTimeout(resolve, 3000);
|
|
1586
|
+
});
|
|
1587
|
+
currentSdk = await createSDK(globalOpts);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
return lastResult;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function buildFirmwareUpdateV4Params(opts: {
|
|
1594
|
+
chunkSize?: string;
|
|
1595
|
+
resourceBundle?: string[];
|
|
1596
|
+
romloader?: string;
|
|
1597
|
+
bootloader?: string;
|
|
1598
|
+
applicationP1?: string;
|
|
1599
|
+
applicationP2?: string;
|
|
1600
|
+
coprocessor?: string;
|
|
1601
|
+
se01?: string;
|
|
1602
|
+
se02?: string;
|
|
1603
|
+
se03?: string;
|
|
1604
|
+
se04?: string;
|
|
1605
|
+
forcedUpdateRes?: boolean;
|
|
1606
|
+
}) {
|
|
1607
|
+
const params = {
|
|
1608
|
+
platform: 'desktop' as const,
|
|
1609
|
+
connectProtocol: 'V2' as const,
|
|
1610
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1611
|
+
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1612
|
+
resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
|
|
1613
|
+
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1614
|
+
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1615
|
+
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
1616
|
+
applicationP2Binary: opts.applicationP2 ? readBinaryParam(opts.applicationP2) : undefined,
|
|
1617
|
+
coprocessorBinary: opts.coprocessor ? readBinaryParam(opts.coprocessor) : undefined,
|
|
1618
|
+
se01Binary: opts.se01 ? readBinaryParam(opts.se01) : undefined,
|
|
1619
|
+
se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
|
|
1620
|
+
se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
|
|
1621
|
+
se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
const hasPayload = [
|
|
1625
|
+
params.resourceBundleFiles,
|
|
1626
|
+
params.romloaderBinary,
|
|
1627
|
+
params.bootloaderBinary,
|
|
1628
|
+
params.applicationP1Binary,
|
|
1629
|
+
params.applicationP2Binary,
|
|
1630
|
+
params.coprocessorBinary,
|
|
1631
|
+
params.se01Binary,
|
|
1632
|
+
params.se02Binary,
|
|
1633
|
+
params.se03Binary,
|
|
1634
|
+
params.se04Binary,
|
|
1635
|
+
].some(Boolean);
|
|
1636
|
+
|
|
1637
|
+
if (!hasPayload) {
|
|
1638
|
+
const err = new Error('firmware-update-v4 requires at least one binary path');
|
|
1639
|
+
(err as Error & { code?: string }).code = 'MISSING_FIRMWARE_BINARY';
|
|
1640
|
+
throw err;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
return params;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1122
1646
|
/**
|
|
1123
1647
|
* #9 FIX: Safe parseInt with NaN check
|
|
1124
1648
|
*/
|
|
@@ -1130,4 +1654,8 @@ function safeParseInt(input: string, label: string): number {
|
|
|
1130
1654
|
return num;
|
|
1131
1655
|
}
|
|
1132
1656
|
|
|
1133
|
-
program
|
|
1657
|
+
export { program };
|
|
1658
|
+
|
|
1659
|
+
if (require.main === module) {
|
|
1660
|
+
program.parse();
|
|
1661
|
+
}
|