@onekeyfe/hardware-cli 1.2.0-alpha.1 → 1.2.0-alpha.100
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 +83 -1
- package/dist/cli.js +501 -142
- 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/pinentry.d.ts +1 -0
- package/dist/sdk.d.ts +12 -2
- package/dist/sdk.js +22 -11
- 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 +371 -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 +101 -0
- package/src/__tests__/noble-ble-plugin.test.ts +370 -0
- package/src/__tests__/wallet-session.test.ts +64 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +46 -0
- package/src/cli.ts +687 -173
- package/src/deviceSelection.ts +13 -0
- package/src/deviceStateCommands.ts +71 -0
- package/src/pinentry.ts +1 -0
- package/src/sdk.ts +29 -13
- package/src/session.ts +2 -24
- package/src/transports/nobleBlePlugin.ts +487 -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,81 @@ 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('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
632
|
+
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
633
|
+
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
634
|
+
.option('--application-p2 <path>', 'FW_MGMT_TARGET_APPLICATION_P2 binary path')
|
|
635
|
+
.option('--coprocessor <path>', 'FW_MGMT_TARGET_COPROCESSOR binary path')
|
|
636
|
+
.option('--se01 <path>', 'FW_MGMT_TARGET_SE01 binary path')
|
|
637
|
+
.option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
|
|
638
|
+
.option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
|
|
639
|
+
.option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
|
|
640
|
+
.option('--resource-archive <path>', 'Complete signed Protocol V2 resource ZIP path')
|
|
641
|
+
.option('--forced-update-res', 'Force resource update')
|
|
642
|
+
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
643
|
+
.action(opts =>
|
|
644
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
645
|
+
const params = buildFirmwareUpdateV4Params(opts);
|
|
646
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
647
|
+
sdk,
|
|
648
|
+
globalOpts,
|
|
649
|
+
params,
|
|
650
|
+
retries: opts.retries ? safeParseInt(opts.retries, '--retries') : undefined,
|
|
651
|
+
});
|
|
652
|
+
outputResult(globalOpts, result);
|
|
653
|
+
})
|
|
654
|
+
);
|
|
655
|
+
|
|
555
656
|
program
|
|
556
657
|
.command('bootloader-check')
|
|
557
658
|
.description('Check bootloader version and status')
|
|
@@ -692,7 +793,7 @@ const sessionCmd = program.command('session').description('Manage device passphr
|
|
|
692
793
|
|
|
693
794
|
sessionCmd
|
|
694
795
|
.command('connect')
|
|
695
|
-
.description('Connect device and
|
|
796
|
+
.description('Connect device and select a hidden wallet for this invocation')
|
|
696
797
|
.action(() =>
|
|
697
798
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
698
799
|
// 1. Search for device
|
|
@@ -704,7 +805,17 @@ sessionCmd
|
|
|
704
805
|
});
|
|
705
806
|
return;
|
|
706
807
|
}
|
|
707
|
-
const device =
|
|
808
|
+
const device = selectSearchDevice(
|
|
809
|
+
searchResult.payload as Array<SearchDevice & { features?: Features }>,
|
|
810
|
+
globalOpts.connectId
|
|
811
|
+
);
|
|
812
|
+
if (!device) {
|
|
813
|
+
outputResult(globalOpts, {
|
|
814
|
+
success: false,
|
|
815
|
+
payload: { error: 'No matching device found', code: 'NO_DEVICE' },
|
|
816
|
+
});
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
708
819
|
const connectId = device.connectId || globalOpts.connectId;
|
|
709
820
|
|
|
710
821
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
@@ -715,62 +826,35 @@ sessionCmd
|
|
|
715
826
|
await unlockWithRetry(sdk, connectId);
|
|
716
827
|
}
|
|
717
828
|
|
|
718
|
-
// 3.
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
useEmptyPassphrase: false,
|
|
829
|
+
// 3. Open a hidden wallet session (triggers 1/2/3 selection).
|
|
830
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
831
|
+
mode: 'select-hidden',
|
|
722
832
|
});
|
|
723
|
-
if (!
|
|
724
|
-
outputResult(globalOpts,
|
|
833
|
+
if (!sessionResult.success) {
|
|
834
|
+
outputResult(globalOpts, sessionResult);
|
|
725
835
|
return;
|
|
726
836
|
}
|
|
727
|
-
|
|
728
|
-
psResult.payload
|
|
729
|
-
);
|
|
730
|
-
if (!passphraseState) {
|
|
837
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
731
838
|
outputResult(globalOpts, {
|
|
732
839
|
success: false,
|
|
733
|
-
payload: { error: '
|
|
840
|
+
payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
|
|
734
841
|
});
|
|
735
842
|
return;
|
|
736
843
|
}
|
|
844
|
+
const { deviceId, passphraseState } = sessionResult.payload;
|
|
737
845
|
|
|
738
846
|
// 4. Get address to verify + extract deviceId
|
|
739
|
-
const addrResult = await sdk.evmGetAddress(connectId,
|
|
847
|
+
const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
|
|
740
848
|
path: "m/44'/60'/0'/0/0",
|
|
741
849
|
showOnOneKey: false,
|
|
742
850
|
passphraseState,
|
|
743
851
|
});
|
|
744
852
|
|
|
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
853
|
outputResult(globalOpts, {
|
|
769
854
|
success: true,
|
|
770
855
|
payload: {
|
|
771
856
|
passphraseState,
|
|
772
857
|
deviceId,
|
|
773
|
-
...(sessionId ? { sessionId } : {}),
|
|
774
858
|
...(addrResult?.success ? { address: addrResult.payload.address } : {}),
|
|
775
859
|
},
|
|
776
860
|
});
|
|
@@ -783,8 +867,10 @@ sessionCmd
|
|
|
783
867
|
.action(() =>
|
|
784
868
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
785
869
|
const searchResult = await sdk.searchDevices();
|
|
786
|
-
const device =
|
|
787
|
-
(searchResult?.payload as
|
|
870
|
+
const device = selectSearchDevice(
|
|
871
|
+
(searchResult?.payload as Array<SearchDevice & { features?: Features }>) ?? [],
|
|
872
|
+
globalOpts.connectId
|
|
873
|
+
);
|
|
788
874
|
const deviceId = device?.deviceId || device?.features?.device_id;
|
|
789
875
|
if (deviceId) {
|
|
790
876
|
await clearSessionFromKeychain(deviceId);
|
|
@@ -879,12 +965,12 @@ async function unlockWithRetry(
|
|
|
879
965
|
* Prepare passphrase session before SDK calls.
|
|
880
966
|
*
|
|
881
967
|
* 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
|
|
882
|
-
* 2. Try keychain → preloadSessionCache → use cached session
|
|
883
|
-
* 3. Keychain miss →
|
|
968
|
+
* 2. Try a legacy keychain entry → preloadSessionCache → use cached session
|
|
969
|
+
* 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
|
|
884
970
|
*
|
|
885
971
|
* After this, globalOpts.passphraseState is set and getCommonParams will include it.
|
|
886
972
|
*/
|
|
887
|
-
async function prepareSession(
|
|
973
|
+
export async function prepareSession(
|
|
888
974
|
sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default,
|
|
889
975
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
890
976
|
globalOpts: Record<string, any>
|
|
@@ -895,7 +981,7 @@ async function prepareSession(
|
|
|
895
981
|
}
|
|
896
982
|
|
|
897
983
|
// Errors from the SDK calls below (PIN cancelled, transport broken,
|
|
898
|
-
//
|
|
984
|
+
// openWalletSession rejection) intentionally propagate to runCommand's
|
|
899
985
|
// catch block, which renders them as structured `{ success: false,
|
|
900
986
|
// payload: { error, code } }` output instead of silently falling through
|
|
901
987
|
// to a confusing downstream error 112 / 114.
|
|
@@ -910,9 +996,30 @@ async function prepareSession(
|
|
|
910
996
|
return undefined;
|
|
911
997
|
}
|
|
912
998
|
|
|
913
|
-
const device =
|
|
999
|
+
const device = selectSearchDevice(
|
|
1000
|
+
searchResult.payload as Array<{
|
|
1001
|
+
connectId?: string;
|
|
1002
|
+
deviceId?: string;
|
|
1003
|
+
deviceType?: string;
|
|
1004
|
+
features?: {
|
|
1005
|
+
deviceId?: string | null;
|
|
1006
|
+
deviceType?: string;
|
|
1007
|
+
sessionId?: string | null;
|
|
1008
|
+
passphraseProtection?: boolean | null;
|
|
1009
|
+
unlocked?: boolean | null;
|
|
1010
|
+
};
|
|
1011
|
+
}>,
|
|
1012
|
+
globalOpts.connectId
|
|
1013
|
+
);
|
|
1014
|
+
|
|
1015
|
+
if (!device) {
|
|
1016
|
+
throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const selectedDevice = device as {
|
|
914
1020
|
connectId?: string;
|
|
915
1021
|
deviceId?: string;
|
|
1022
|
+
deviceType?: string;
|
|
916
1023
|
features?: {
|
|
917
1024
|
deviceId?: string | null;
|
|
918
1025
|
deviceType?: string;
|
|
@@ -921,7 +1028,7 @@ async function prepareSession(
|
|
|
921
1028
|
unlocked?: boolean | null;
|
|
922
1029
|
};
|
|
923
1030
|
};
|
|
924
|
-
const connectId =
|
|
1031
|
+
const connectId = selectedDevice.connectId || globalOpts.connectId || '';
|
|
925
1032
|
if (!globalOpts.connectId && connectId) {
|
|
926
1033
|
globalOpts.connectId = connectId;
|
|
927
1034
|
}
|
|
@@ -929,10 +1036,11 @@ async function prepareSession(
|
|
|
929
1036
|
// ── Step 2: Get features if searchDevices didn't populate them ──
|
|
930
1037
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
931
1038
|
// which will fail with a clearer error if the device is truly unreachable.
|
|
932
|
-
let deviceId =
|
|
933
|
-
let deviceType =
|
|
934
|
-
|
|
935
|
-
let
|
|
1039
|
+
let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
|
|
1040
|
+
let deviceType =
|
|
1041
|
+
selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? EDeviceType.Unknown;
|
|
1042
|
+
let unlocked = selectedDevice.features?.unlocked;
|
|
1043
|
+
let passphraseProtection = selectedDevice.features?.passphraseProtection;
|
|
936
1044
|
|
|
937
1045
|
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
938
1046
|
try {
|
|
@@ -971,7 +1079,7 @@ async function prepareSession(
|
|
|
971
1079
|
return undefined;
|
|
972
1080
|
}
|
|
973
1081
|
|
|
974
|
-
// ── Step 5: Try keychain session reuse
|
|
1082
|
+
// ── Step 5: Try legacy keychain session reuse ────────────────────
|
|
975
1083
|
// Only attempt if device was already unlocked — locking invalidates
|
|
976
1084
|
// all passphrase sessions, so cached session_id is useless after unlock.
|
|
977
1085
|
if (!wasLocked && deviceId) {
|
|
@@ -982,40 +1090,19 @@ async function prepareSession(
|
|
|
982
1090
|
}
|
|
983
1091
|
}
|
|
984
1092
|
|
|
985
|
-
// ── Step 6: Keychain miss →
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
useEmptyPassphrase: false,
|
|
1093
|
+
// ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
|
|
1094
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
1095
|
+
mode: 'select-hidden',
|
|
989
1096
|
});
|
|
990
1097
|
|
|
991
|
-
if (
|
|
992
|
-
|
|
993
|
-
psResult.payload
|
|
994
|
-
);
|
|
995
|
-
if (!passphraseState) {
|
|
1098
|
+
if (sessionResult.success && sessionResult.payload) {
|
|
1099
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
996
1100
|
return undefined;
|
|
997
1101
|
}
|
|
1102
|
+
const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
|
|
1103
|
+
globalOpts.deviceId = sessionDeviceId;
|
|
998
1104
|
globalOpts.passphraseState = passphraseState;
|
|
999
1105
|
|
|
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
1106
|
return passphraseState;
|
|
1020
1107
|
}
|
|
1021
1108
|
return undefined;
|
|
@@ -1032,8 +1119,8 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1032
1119
|
) {
|
|
1033
1120
|
process.exitCode = 1;
|
|
1034
1121
|
}
|
|
1035
|
-
// No process.exit here — runCommand()
|
|
1036
|
-
//
|
|
1122
|
+
// No process.exit here — runCommand() waits for SDK cleanup, then lets Node
|
|
1123
|
+
// exit naturally so leaked USB handles remain observable.
|
|
1037
1124
|
}
|
|
1038
1125
|
|
|
1039
1126
|
/**
|
|
@@ -1045,7 +1132,7 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1045
1132
|
* 3. run the handler (which calls outputResult on success)
|
|
1046
1133
|
* 4. report uncaught errors as a structured failure result
|
|
1047
1134
|
* 5. dispose SDK
|
|
1048
|
-
* 6.
|
|
1135
|
+
* 6. let Node exit naturally after all SDK resources are released
|
|
1049
1136
|
*
|
|
1050
1137
|
* This fixes three previous bugs:
|
|
1051
1138
|
* - Most signing commands skipped prepareSession, so keychain sessions
|
|
@@ -1071,6 +1158,9 @@ async function runCommand(
|
|
|
1071
1158
|
): Promise<void> {
|
|
1072
1159
|
const globalOpts = program.opts();
|
|
1073
1160
|
try {
|
|
1161
|
+
if (globalOpts.transport !== 'usb' && globalOpts.transport !== 'ble') {
|
|
1162
|
+
throw new Error(`Unsupported transport: ${globalOpts.transport}. Use "usb" or "ble".`);
|
|
1163
|
+
}
|
|
1074
1164
|
const sdk = await createSDK(globalOpts);
|
|
1075
1165
|
if (options.needsSession) {
|
|
1076
1166
|
await prepareSession(sdk, globalOpts);
|
|
@@ -1094,9 +1184,7 @@ async function runCommand(
|
|
|
1094
1184
|
// promise reference. Idempotent, safe to call even if init failed.
|
|
1095
1185
|
await disposeSDK();
|
|
1096
1186
|
}
|
|
1097
|
-
//
|
|
1098
|
-
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
1099
|
-
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
1187
|
+
// disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
|
|
1100
1188
|
}
|
|
1101
1189
|
|
|
1102
1190
|
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
@@ -1119,6 +1207,428 @@ function safeJsonParse(input: string, label: string): unknown {
|
|
|
1119
1207
|
}
|
|
1120
1208
|
}
|
|
1121
1209
|
|
|
1210
|
+
function readBinaryParam(path: string): ArrayBuffer {
|
|
1211
|
+
const buffer = readFileSync(path);
|
|
1212
|
+
return new Uint8Array(buffer).buffer;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
async function resolveLegacyFirmwareConnectId(
|
|
1216
|
+
sdk: AnySdk,
|
|
1217
|
+
explicitConnectId?: string,
|
|
1218
|
+
deviceName?: string
|
|
1219
|
+
): Promise<string> {
|
|
1220
|
+
if (explicitConnectId && !deviceName) return explicitConnectId;
|
|
1221
|
+
|
|
1222
|
+
const searchResult = await sdk.searchDevices();
|
|
1223
|
+
if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
|
|
1224
|
+
throw new Error('Unable to scan BLE devices');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const devices = searchResult.payload as EnrichedSearchDevice[];
|
|
1228
|
+
const normalizedName = deviceName?.trim().toLowerCase();
|
|
1229
|
+
const matches = normalizedName
|
|
1230
|
+
? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
|
|
1231
|
+
: devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
|
|
1232
|
+
|
|
1233
|
+
if (matches.length === 0) {
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
normalizedName
|
|
1236
|
+
? `BLE device not found by name: ${deviceName}`
|
|
1237
|
+
: 'No Classic/Pure BLE device found'
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
if (matches.length > 1) {
|
|
1241
|
+
throw new Error(
|
|
1242
|
+
normalizedName
|
|
1243
|
+
? `Multiple BLE devices found by name: ${deviceName}`
|
|
1244
|
+
: 'Multiple Classic/Pure BLE devices found; specify --device-name'
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const [{ connectId, name }] = matches;
|
|
1249
|
+
if (!connectId) throw new Error(`BLE device has no connect ID: ${name}`);
|
|
1250
|
+
return connectId;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function getFirmwareUpdateV4TotalBytes(params: ReturnType<typeof buildFirmwareUpdateV4Params>) {
|
|
1254
|
+
return [
|
|
1255
|
+
params.bootloaderBinary,
|
|
1256
|
+
params.applicationP1Binary,
|
|
1257
|
+
params.applicationP2Binary,
|
|
1258
|
+
params.coprocessorBinary,
|
|
1259
|
+
params.se01Binary,
|
|
1260
|
+
params.se02Binary,
|
|
1261
|
+
params.se03Binary,
|
|
1262
|
+
params.se04Binary,
|
|
1263
|
+
].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function getFirmwareUpdateV4ErrorText(result: unknown) {
|
|
1267
|
+
if (!result || typeof result !== 'object') return '';
|
|
1268
|
+
const { payload } = result as { payload?: unknown };
|
|
1269
|
+
if (!payload || typeof payload !== 'object') return '';
|
|
1270
|
+
const { error } = payload as { error?: unknown };
|
|
1271
|
+
return typeof error === 'string' ? error : '';
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function isProtocolV2UsbProbeTransientResult(result: unknown) {
|
|
1275
|
+
const error = getFirmwareUpdateV4ErrorText(result);
|
|
1276
|
+
return (
|
|
1277
|
+
error.includes('Device protocol mismatch') &&
|
|
1278
|
+
error.includes('expected V2') &&
|
|
1279
|
+
error.includes('did not respond to expected protocol')
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function isSuccessResult(result: unknown) {
|
|
1284
|
+
return (
|
|
1285
|
+
!!result && typeof result === 'object' && (result as { success?: boolean }).success === true
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function getFirmwareUpdatePayload(message: unknown) {
|
|
1290
|
+
if (!message || typeof message !== 'object') return undefined;
|
|
1291
|
+
return (message as { payload?: Record<string, unknown> }).payload;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function formatFirmwareProgress(progress: number) {
|
|
1295
|
+
if (!Number.isFinite(progress)) return '0%';
|
|
1296
|
+
return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function formatFirmwareBytes(bytes: number) {
|
|
1300
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '';
|
|
1301
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
export function buildWallpaperUploadMetrics({
|
|
1305
|
+
totalBytes,
|
|
1306
|
+
transferredBytes,
|
|
1307
|
+
startedAt,
|
|
1308
|
+
endedAt,
|
|
1309
|
+
lastProgress,
|
|
1310
|
+
}: {
|
|
1311
|
+
totalBytes: number;
|
|
1312
|
+
transferredBytes: number;
|
|
1313
|
+
startedAt: number;
|
|
1314
|
+
endedAt: number;
|
|
1315
|
+
lastProgress: number;
|
|
1316
|
+
}) {
|
|
1317
|
+
const elapsedMs = Math.max(endedAt - startedAt, 0);
|
|
1318
|
+
return {
|
|
1319
|
+
totalBytes,
|
|
1320
|
+
transferredBytes,
|
|
1321
|
+
totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
|
|
1322
|
+
transferKiBPerSecond:
|
|
1323
|
+
elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
|
|
1324
|
+
lastProgress,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function maybePrintFirmwareProgress({
|
|
1329
|
+
progressType,
|
|
1330
|
+
progress,
|
|
1331
|
+
payload,
|
|
1332
|
+
lastPrintedProgress,
|
|
1333
|
+
}: {
|
|
1334
|
+
progressType: string;
|
|
1335
|
+
progress: number;
|
|
1336
|
+
payload: Record<string, unknown>;
|
|
1337
|
+
lastPrintedProgress: number;
|
|
1338
|
+
}) {
|
|
1339
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
1340
|
+
if (printableProgress <= lastPrintedProgress && progress < 100) {
|
|
1341
|
+
return lastPrintedProgress;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const transferredBytes = Number(payload.transferredBytes);
|
|
1345
|
+
const totalBytes = Number(payload.totalBytes);
|
|
1346
|
+
const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
|
|
1347
|
+
const sizeText =
|
|
1348
|
+
Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
|
|
1349
|
+
? ` ${formatFirmwareBytes(transferredBytes)}/${formatFirmwareBytes(totalBytes)}`
|
|
1350
|
+
: '';
|
|
1351
|
+
const speedText =
|
|
1352
|
+
Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
|
|
1353
|
+
? ` ${(rateBytesPerSecond / 1024).toFixed(2)} KiB/s`
|
|
1354
|
+
: '';
|
|
1355
|
+
|
|
1356
|
+
process.stderr.write(
|
|
1357
|
+
`[onekey-hw] Firmware ${progressType}: ${formatFirmwareProgress(
|
|
1358
|
+
progress
|
|
1359
|
+
)}${sizeText}${speedText}\n`
|
|
1360
|
+
);
|
|
1361
|
+
return progress >= 100 ? 100 : printableProgress;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function buildFirmwareUpdateV4Metrics({
|
|
1365
|
+
attempt,
|
|
1366
|
+
maxAttempts,
|
|
1367
|
+
totalBytes,
|
|
1368
|
+
totalStartedAt,
|
|
1369
|
+
transferStartedAt,
|
|
1370
|
+
transferEndedAt,
|
|
1371
|
+
installStartedAt,
|
|
1372
|
+
installEndedAt,
|
|
1373
|
+
progressEvents,
|
|
1374
|
+
lastProgress,
|
|
1375
|
+
installProgressEvents,
|
|
1376
|
+
lastInstallProgress,
|
|
1377
|
+
retried,
|
|
1378
|
+
}: {
|
|
1379
|
+
attempt: number;
|
|
1380
|
+
maxAttempts: number;
|
|
1381
|
+
totalBytes: number;
|
|
1382
|
+
totalStartedAt: number;
|
|
1383
|
+
transferStartedAt?: number;
|
|
1384
|
+
transferEndedAt?: number;
|
|
1385
|
+
installStartedAt?: number;
|
|
1386
|
+
installEndedAt?: number;
|
|
1387
|
+
progressEvents: number;
|
|
1388
|
+
lastProgress: number;
|
|
1389
|
+
installProgressEvents: number;
|
|
1390
|
+
lastInstallProgress: number;
|
|
1391
|
+
retried: boolean;
|
|
1392
|
+
}) {
|
|
1393
|
+
const totalElapsedMs = Date.now() - totalStartedAt;
|
|
1394
|
+
const transferElapsedMs =
|
|
1395
|
+
transferStartedAt !== undefined && transferEndedAt !== undefined
|
|
1396
|
+
? transferEndedAt - transferStartedAt
|
|
1397
|
+
: undefined;
|
|
1398
|
+
const installElapsedMs =
|
|
1399
|
+
installStartedAt !== undefined && installEndedAt !== undefined
|
|
1400
|
+
? installEndedAt - installStartedAt
|
|
1401
|
+
: undefined;
|
|
1402
|
+
|
|
1403
|
+
return {
|
|
1404
|
+
attempt,
|
|
1405
|
+
maxAttempts,
|
|
1406
|
+
retried,
|
|
1407
|
+
totalBytes,
|
|
1408
|
+
progressEvents,
|
|
1409
|
+
lastProgress,
|
|
1410
|
+
installProgressEvents,
|
|
1411
|
+
lastInstallProgress,
|
|
1412
|
+
transferSeconds:
|
|
1413
|
+
transferElapsedMs !== undefined ? Number((transferElapsedMs / 1000).toFixed(2)) : null,
|
|
1414
|
+
transferKiBPerSecond:
|
|
1415
|
+
transferElapsedMs !== undefined && transferElapsedMs > 0
|
|
1416
|
+
? Number((totalBytes / 1024 / (transferElapsedMs / 1000)).toFixed(2))
|
|
1417
|
+
: null,
|
|
1418
|
+
installSeconds:
|
|
1419
|
+
installElapsedMs !== undefined ? Number((installElapsedMs / 1000).toFixed(2)) : null,
|
|
1420
|
+
totalSeconds: Number((totalElapsedMs / 1000).toFixed(2)),
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
export async function runFirmwareUpdateV4WithRetry({
|
|
1425
|
+
sdk,
|
|
1426
|
+
globalOpts,
|
|
1427
|
+
params,
|
|
1428
|
+
retries,
|
|
1429
|
+
}: {
|
|
1430
|
+
sdk: AnySdk;
|
|
1431
|
+
globalOpts: Record<string, any>;
|
|
1432
|
+
params: ReturnType<typeof buildFirmwareUpdateV4Params>;
|
|
1433
|
+
retries?: number;
|
|
1434
|
+
}) {
|
|
1435
|
+
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1436
|
+
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1437
|
+
let currentSdk = sdk;
|
|
1438
|
+
let retried = false;
|
|
1439
|
+
let attempt = 1;
|
|
1440
|
+
let { connectId } = globalOpts;
|
|
1441
|
+
|
|
1442
|
+
if (globalOpts.transport === 'usb') {
|
|
1443
|
+
for (; attempt <= maxAttempts; attempt += 1) {
|
|
1444
|
+
const probeResult = await currentSdk.getDeviceState(connectId, {
|
|
1445
|
+
scope: 'runtime',
|
|
1446
|
+
connectProtocol: 'V2',
|
|
1447
|
+
retryCount: 0,
|
|
1448
|
+
});
|
|
1449
|
+
if (isSuccessResult(probeResult)) break;
|
|
1450
|
+
if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
|
|
1451
|
+
return probeResult;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
retried = true;
|
|
1455
|
+
process.stderr.write(
|
|
1456
|
+
`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`
|
|
1457
|
+
);
|
|
1458
|
+
await disposeSDK();
|
|
1459
|
+
await new Promise(resolve => {
|
|
1460
|
+
setTimeout(resolve, 3000);
|
|
1461
|
+
});
|
|
1462
|
+
currentSdk = await createSDK(globalOpts);
|
|
1463
|
+
if (globalOpts.connectId) connectId = undefined;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
let progressEvents = 0;
|
|
1468
|
+
let lastProgress = -1;
|
|
1469
|
+
let transferStartedAt: number | undefined;
|
|
1470
|
+
let transferEndedAt: number | undefined;
|
|
1471
|
+
let installProgressEvents = 0;
|
|
1472
|
+
let lastInstallProgress = -1;
|
|
1473
|
+
let installStartedAt: number | undefined;
|
|
1474
|
+
let installEndedAt: number | undefined;
|
|
1475
|
+
let lastPrintedTransferProgress = -10;
|
|
1476
|
+
let lastPrintedInstallProgress = -10;
|
|
1477
|
+
const totalStartedAt = Date.now();
|
|
1478
|
+
|
|
1479
|
+
const onUiEvent = (message: unknown) => {
|
|
1480
|
+
if (!message || typeof message !== 'object') return;
|
|
1481
|
+
const messageType = (message as { type?: string }).type;
|
|
1482
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1483
|
+
|
|
1484
|
+
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1485
|
+
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1486
|
+
if (typeof tipMessage === 'string') {
|
|
1487
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1488
|
+
}
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1493
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1494
|
+
process.stderr.write(
|
|
1495
|
+
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1496
|
+
);
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
|
|
1501
|
+
const progress = Number(payload.progress);
|
|
1502
|
+
if (!Number.isFinite(progress)) return;
|
|
1503
|
+
|
|
1504
|
+
if (payload.progressType === 'transferData') {
|
|
1505
|
+
progressEvents += 1;
|
|
1506
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1507
|
+
transferStartedAt ??= Date.now();
|
|
1508
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1509
|
+
progressType: 'transfer',
|
|
1510
|
+
progress,
|
|
1511
|
+
payload,
|
|
1512
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1513
|
+
});
|
|
1514
|
+
if (progress >= 100) {
|
|
1515
|
+
transferEndedAt ??= Date.now();
|
|
1516
|
+
}
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1521
|
+
installProgressEvents += 1;
|
|
1522
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1523
|
+
installStartedAt ??= Date.now();
|
|
1524
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1525
|
+
progressType: 'install',
|
|
1526
|
+
progress,
|
|
1527
|
+
payload,
|
|
1528
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1529
|
+
});
|
|
1530
|
+
if (progress >= 100) {
|
|
1531
|
+
installEndedAt ??= Date.now();
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1537
|
+
let result: unknown;
|
|
1538
|
+
try {
|
|
1539
|
+
result = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1540
|
+
} finally {
|
|
1541
|
+
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1542
|
+
}
|
|
1543
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1544
|
+
installEndedAt = Date.now();
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1548
|
+
attempt,
|
|
1549
|
+
maxAttempts,
|
|
1550
|
+
totalBytes,
|
|
1551
|
+
totalStartedAt,
|
|
1552
|
+
transferStartedAt,
|
|
1553
|
+
transferEndedAt,
|
|
1554
|
+
installStartedAt,
|
|
1555
|
+
installEndedAt,
|
|
1556
|
+
progressEvents,
|
|
1557
|
+
lastProgress,
|
|
1558
|
+
installProgressEvents,
|
|
1559
|
+
lastInstallProgress,
|
|
1560
|
+
retried,
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
if (result && typeof result === 'object') {
|
|
1564
|
+
const payload = ((result as { payload?: unknown }).payload ?? {}) as Record<string, unknown>;
|
|
1565
|
+
return {
|
|
1566
|
+
...(result as Record<string, unknown>),
|
|
1567
|
+
payload: {
|
|
1568
|
+
...payload,
|
|
1569
|
+
metrics,
|
|
1570
|
+
},
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
return result;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function buildFirmwareUpdateV4Params(opts: {
|
|
1578
|
+
chunkSize?: string;
|
|
1579
|
+
romloader?: string;
|
|
1580
|
+
bootloader?: string;
|
|
1581
|
+
applicationP1?: string;
|
|
1582
|
+
applicationP2?: string;
|
|
1583
|
+
coprocessor?: string;
|
|
1584
|
+
se01?: string;
|
|
1585
|
+
se02?: string;
|
|
1586
|
+
se03?: string;
|
|
1587
|
+
se04?: string;
|
|
1588
|
+
resourceArchive?: string;
|
|
1589
|
+
forcedUpdateRes?: boolean;
|
|
1590
|
+
}) {
|
|
1591
|
+
const params = {
|
|
1592
|
+
platform: 'desktop' as const,
|
|
1593
|
+
connectProtocol: 'V2' as const,
|
|
1594
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1595
|
+
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1596
|
+
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1597
|
+
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1598
|
+
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
1599
|
+
applicationP2Binary: opts.applicationP2 ? readBinaryParam(opts.applicationP2) : undefined,
|
|
1600
|
+
coprocessorBinary: opts.coprocessor ? readBinaryParam(opts.coprocessor) : undefined,
|
|
1601
|
+
se01Binary: opts.se01 ? readBinaryParam(opts.se01) : undefined,
|
|
1602
|
+
se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
|
|
1603
|
+
se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
|
|
1604
|
+
se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
|
|
1605
|
+
resourceArchiveBinary: opts.resourceArchive ? readBinaryParam(opts.resourceArchive) : undefined,
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
const hasPayload = [
|
|
1609
|
+
params.romloaderBinary,
|
|
1610
|
+
params.bootloaderBinary,
|
|
1611
|
+
params.applicationP1Binary,
|
|
1612
|
+
params.applicationP2Binary,
|
|
1613
|
+
params.coprocessorBinary,
|
|
1614
|
+
params.se01Binary,
|
|
1615
|
+
params.se02Binary,
|
|
1616
|
+
params.se03Binary,
|
|
1617
|
+
params.se04Binary,
|
|
1618
|
+
params.resourceArchiveBinary,
|
|
1619
|
+
].some(Boolean);
|
|
1620
|
+
|
|
1621
|
+
if (!hasPayload) {
|
|
1622
|
+
const err = new Error(
|
|
1623
|
+
'firmware-update-v4 requires at least one firmware binary or resource archive path'
|
|
1624
|
+
);
|
|
1625
|
+
(err as Error & { code?: string }).code = 'MISSING_FIRMWARE_BINARY';
|
|
1626
|
+
throw err;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
return params;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1122
1632
|
/**
|
|
1123
1633
|
* #9 FIX: Safe parseInt with NaN check
|
|
1124
1634
|
*/
|
|
@@ -1130,4 +1640,8 @@ function safeParseInt(input: string, label: string): number {
|
|
|
1130
1640
|
return num;
|
|
1131
1641
|
}
|
|
1132
1642
|
|
|
1133
|
-
program
|
|
1643
|
+
export { program };
|
|
1644
|
+
|
|
1645
|
+
if (require.main === module) {
|
|
1646
|
+
program.parse();
|
|
1647
|
+
}
|