@onekeyfe/hardware-cli 1.1.25-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts ADDED
@@ -0,0 +1,855 @@
1
+ import { Command } from 'commander';
2
+
3
+ import { createSDK } from './sdk';
4
+ import {
5
+ resolveBatchGetAddress,
6
+ resolveGetAddress,
7
+ resolveGetPublicKey,
8
+ resolveSignMessage,
9
+ resolveSignTransaction,
10
+ } from './chains';
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('onekey-hw')
16
+ .description('OneKey hardware wallet CLI for AI agent integration')
17
+ .version('1.1.25-alpha.0');
18
+
19
+ // ============================================================
20
+ // Global Options
21
+ // ============================================================
22
+
23
+ program.option('--json', 'Output in JSON format (for agent consumption)');
24
+ program.option('--connect-id <id>', 'Device connection ID (USB: serial, iOS: uuid, Android: MAC)');
25
+ program.option(
26
+ '--device-id <id>',
27
+ 'Persistent device ID from getFeatures (changes when seed changes)'
28
+ );
29
+ program.option('--passphrase-state <state>', 'Passphrase state for hidden wallet access');
30
+ program.option('--use-empty-passphrase', 'Use standard wallet (skip passphrase prompt)');
31
+
32
+ // ============================================================
33
+ // Device Commands
34
+ // ============================================================
35
+
36
+ program
37
+ .command('search')
38
+ .description('Search for connected OneKey hardware wallet devices')
39
+ .option('--timeout <ms>', 'Search timeout in milliseconds', '10000')
40
+ .action(async opts => {
41
+ const sdk = await createSDK(program.opts());
42
+ try {
43
+ const result = await sdk.searchDevices();
44
+ outputResult(program.opts(), result);
45
+ } finally {
46
+ sdk.dispose();
47
+ }
48
+ });
49
+
50
+ program
51
+ .command('status')
52
+ .description('Get device features and current status')
53
+ .action(async () => {
54
+ const globalOpts = program.opts();
55
+ const sdk = await createSDK(globalOpts);
56
+ try {
57
+ const result = await sdk.getFeatures(globalOpts.connectId);
58
+ outputResult(globalOpts, result);
59
+ } finally {
60
+ sdk.dispose();
61
+ }
62
+ });
63
+
64
+ // ============================================================
65
+ // Signing Commands
66
+ // ============================================================
67
+
68
+ program
69
+ .command('get-address')
70
+ .description('Get a cryptocurrency address from the hardware wallet')
71
+ .requiredOption('--chain <chain>', 'Target blockchain (evm, btc, sol, ...)')
72
+ .option('--path <path>', 'BIP44 derivation path')
73
+ .option('--show-on-device <bool>', 'Display address on device for verification', 'true')
74
+ .action(async opts => {
75
+ const globalOpts = program.opts();
76
+ const sdk = await createSDK(globalOpts);
77
+ try {
78
+ const result = await resolveGetAddress(sdk, {
79
+ chain: opts.chain,
80
+ path: opts.path,
81
+ showOnDevice: opts.showOnDevice === 'true',
82
+ ...getCommonParams(globalOpts),
83
+ });
84
+ outputResult(globalOpts, result);
85
+ } finally {
86
+ sdk.dispose();
87
+ }
88
+ });
89
+
90
+ program
91
+ .command('get-public-key')
92
+ .description('Get public key from the hardware wallet')
93
+ .requiredOption('--chain <chain>', 'Target blockchain')
94
+ .option('--path <path>', 'BIP44 derivation path')
95
+ .action(async opts => {
96
+ const globalOpts = program.opts();
97
+ const sdk = await createSDK(globalOpts);
98
+ try {
99
+ const result = await resolveGetPublicKey(sdk, {
100
+ chain: opts.chain,
101
+ path: opts.path,
102
+ ...getCommonParams(globalOpts),
103
+ });
104
+ outputResult(globalOpts, result);
105
+ } finally {
106
+ sdk.dispose();
107
+ }
108
+ });
109
+
110
+ program
111
+ .command('sign-transaction')
112
+ .description('Sign a blockchain transaction (requires device confirmation)')
113
+ .requiredOption('--chain <chain>', 'Target blockchain')
114
+ .requiredOption('--tx <json>', 'Transaction data (JSON)')
115
+ .option('--path <path>', 'BIP44 derivation path')
116
+ .action(async opts => {
117
+ const globalOpts = program.opts();
118
+ const sdk = await createSDK(globalOpts);
119
+ try {
120
+ const tx = safeJsonParse(opts.tx, '--tx') as Record<string, unknown>;
121
+ const result = await resolveSignTransaction(sdk, {
122
+ chain: opts.chain,
123
+ path: opts.path,
124
+ transaction: tx,
125
+ ...getCommonParams(globalOpts),
126
+ });
127
+ outputResult(globalOpts, result);
128
+ } finally {
129
+ sdk.dispose();
130
+ }
131
+ });
132
+
133
+ program
134
+ .command('sign-message')
135
+ .description('Sign a message (requires device confirmation)')
136
+ .requiredOption('--chain <chain>', 'Target blockchain')
137
+ .requiredOption('--message <msg>', 'Message to sign')
138
+ .option('--path <path>', 'BIP44 derivation path')
139
+ .action(async opts => {
140
+ const globalOpts = program.opts();
141
+ const sdk = await createSDK(globalOpts);
142
+ try {
143
+ const result = await resolveSignMessage(sdk, {
144
+ chain: opts.chain,
145
+ path: opts.path,
146
+ message: opts.message,
147
+ ...getCommonParams(globalOpts),
148
+ });
149
+ outputResult(globalOpts, result);
150
+ } finally {
151
+ sdk.dispose();
152
+ }
153
+ });
154
+
155
+ program
156
+ .command('sign-typed-data')
157
+ .description('Sign EIP-712 typed data (EVM only, requires device confirmation)')
158
+ .requiredOption('--data <json>', 'EIP-712 typed data JSON')
159
+ .option('--path <path>', 'BIP44 derivation path')
160
+ .option('--metamask-v4-compat', 'Use MetaMask V4 compatibility mode', true)
161
+ .action(async opts => {
162
+ const globalOpts = program.opts();
163
+ const sdk = await createSDK(globalOpts);
164
+ try {
165
+ const data = safeJsonParse(opts.data, '--data');
166
+ const params = getCommonParams(globalOpts);
167
+ const path = opts.path || "m/44'/60'/0'/0/0";
168
+ const result = await sdk.evmSignTypedData(params.connectId || '', params.deviceId || '', {
169
+ path,
170
+ metamaskV4Compat: opts.metamaskV4Compat,
171
+ data,
172
+ ...params,
173
+ } as any);
174
+ outputResult(globalOpts, result);
175
+ } finally {
176
+ sdk.dispose();
177
+ }
178
+ });
179
+
180
+ program
181
+ .command('sign-psbt')
182
+ .description('Sign a Bitcoin PSBT (Pro/Classic1s only, requires device confirmation)')
183
+ .requiredOption('--psbt <hex>', 'Hex-encoded PSBT data')
184
+ .option('--coin <coin>', 'Bitcoin network: btc, ltc, etc.', 'btc')
185
+ .action(async opts => {
186
+ const globalOpts = program.opts();
187
+ const sdk = await createSDK(globalOpts);
188
+ try {
189
+ const params = getCommonParams(globalOpts);
190
+ const result = await sdk.btcSignPsbt(params.connectId || '', params.deviceId || '', {
191
+ psbt: opts.psbt,
192
+ coin: opts.coin,
193
+ ...params,
194
+ } as any);
195
+ outputResult(globalOpts, result);
196
+ } finally {
197
+ sdk.dispose();
198
+ }
199
+ });
200
+
201
+ program
202
+ .command('verify-message')
203
+ .description('Verify a signed message on-device (BTC, EVM, Starcoin)')
204
+ .requiredOption('--chain <chain>', 'Target blockchain (btc, evm, starcoin)')
205
+ .requiredOption('--address <addr>', 'Signer address')
206
+ .requiredOption('--message <msg>', 'Original message')
207
+ .requiredOption('--signature <sig>', 'Signature to verify')
208
+ .action(async opts => {
209
+ const globalOpts = program.opts();
210
+ const sdk = await createSDK(globalOpts);
211
+ try {
212
+ const params = getCommonParams(globalOpts);
213
+ const cid = params.connectId || '';
214
+ const did = params.deviceId || '';
215
+ let result: unknown;
216
+ switch (opts.chain.toLowerCase()) {
217
+ case 'evm':
218
+ case 'eth':
219
+ case 'ethereum':
220
+ result = await sdk.evmVerifyMessage(cid, did, {
221
+ address: opts.address,
222
+ messageHex: opts.message,
223
+ signature: opts.signature,
224
+ });
225
+ break;
226
+ case 'btc':
227
+ case 'bitcoin':
228
+ result = await sdk.btcVerifyMessage(cid, did, {
229
+ address: opts.address,
230
+ message: opts.message,
231
+ signature: opts.signature,
232
+ coin: 'btc',
233
+ });
234
+ break;
235
+ case 'starcoin':
236
+ case 'stc':
237
+ result = await sdk.starcoinVerifyMessage(cid, did, {
238
+ publicKey: opts.address,
239
+ message: opts.message,
240
+ signature: opts.signature,
241
+ });
242
+ break;
243
+ default:
244
+ throw new Error(
245
+ `verifyMessage not supported for chain: ${opts.chain}. Supported: evm, btc, starcoin`
246
+ );
247
+ }
248
+ outputResult(globalOpts, result);
249
+ } finally {
250
+ sdk.dispose();
251
+ }
252
+ });
253
+
254
+ program
255
+ .command('batch-get-address')
256
+ .description('Get addresses for multiple chains/paths in a single session')
257
+ .requiredOption('--bundle <json>', 'JSON array of {chain, path, showOnDevice}')
258
+ .action(async opts => {
259
+ const globalOpts = program.opts();
260
+ const sdk = await createSDK(globalOpts);
261
+ try {
262
+ const bundle = safeJsonParse(opts.bundle, '--bundle') as Array<{
263
+ chain: string;
264
+ path?: string;
265
+ showOnDevice?: boolean;
266
+ }>;
267
+ const result = await resolveBatchGetAddress(sdk, {
268
+ bundle,
269
+ ...getCommonParams(globalOpts),
270
+ });
271
+ outputResult(globalOpts, result);
272
+ } finally {
273
+ sdk.dispose();
274
+ }
275
+ });
276
+
277
+ // ============================================================
278
+ // Chain-Specific Commands
279
+ // ============================================================
280
+
281
+ program
282
+ .command('evm-sign-eip712')
283
+ .description('Sign EIP-712 message by domain/message hash (EVM, requires device confirmation)')
284
+ .requiredOption('--domain-hash <hex>', 'EIP-712 domain separator hash')
285
+ .requiredOption('--message-hash <hex>', 'EIP-712 message hash')
286
+ .option('--path <path>', 'BIP44 derivation path', "m/44'/60'/0'/0/0")
287
+ .action(async opts => {
288
+ const globalOpts = program.opts();
289
+ const sdk = await createSDK(globalOpts);
290
+ try {
291
+ const p = getCommonParams(globalOpts);
292
+ const result = await sdk.evmSignMessageEIP712(p.connectId || '', p.deviceId || '', {
293
+ path: opts.path,
294
+ domainHash: opts.domainHash,
295
+ messageHash: opts.messageHash,
296
+ });
297
+ outputResult(globalOpts, result);
298
+ } finally {
299
+ sdk.dispose();
300
+ }
301
+ });
302
+
303
+ program
304
+ .command('sol-sign-offchain')
305
+ .description('Sign a Solana off-chain message (requires device confirmation)')
306
+ .requiredOption('--message-hex <hex>', 'Off-chain message as hex')
307
+ .option('--path <path>', 'BIP44 derivation path', "m/44'/501'/0'/0'")
308
+ .action(async opts => {
309
+ const globalOpts = program.opts();
310
+ const sdk = await createSDK(globalOpts);
311
+ try {
312
+ const p = getCommonParams(globalOpts);
313
+ const result = await sdk.solSignOffchainMessage(p.connectId || '', p.deviceId || '', {
314
+ path: opts.path,
315
+ messageHex: opts.messageHex,
316
+ });
317
+ outputResult(globalOpts, result);
318
+ } finally {
319
+ sdk.dispose();
320
+ }
321
+ });
322
+
323
+ program
324
+ .command('nostr-encrypt')
325
+ .description('Encrypt a message for a Nostr recipient')
326
+ .requiredOption('--pubkey <hex>', 'Recipient Nostr public key')
327
+ .requiredOption('--plaintext <text>', 'Message to encrypt')
328
+ .option('--path <path>', 'BIP44 derivation path', "m/44'/1237'/0'/0/0")
329
+ .option('--show-on-device <bool>', 'Display on device', 'false')
330
+ .action(async opts => {
331
+ const globalOpts = program.opts();
332
+ const sdk = await createSDK(globalOpts);
333
+ try {
334
+ const p = getCommonParams(globalOpts);
335
+ const result = await sdk.nostrEncryptMessage(p.connectId || '', p.deviceId || '', {
336
+ path: opts.path,
337
+ pubkey: opts.pubkey,
338
+ plaintext: opts.plaintext,
339
+ showOnOneKey: opts.showOnDevice === 'true',
340
+ });
341
+ outputResult(globalOpts, result);
342
+ } finally {
343
+ sdk.dispose();
344
+ }
345
+ });
346
+
347
+ program
348
+ .command('nostr-decrypt')
349
+ .description('Decrypt a Nostr encrypted message')
350
+ .requiredOption('--pubkey <hex>', 'Sender Nostr public key')
351
+ .requiredOption('--ciphertext <text>', 'Encrypted message')
352
+ .option('--path <path>', 'BIP44 derivation path', "m/44'/1237'/0'/0/0")
353
+ .option('--show-on-device <bool>', 'Display on device', 'false')
354
+ .action(async opts => {
355
+ const globalOpts = program.opts();
356
+ const sdk = await createSDK(globalOpts);
357
+ try {
358
+ const p = getCommonParams(globalOpts);
359
+ const result = await sdk.nostrDecryptMessage(p.connectId || '', p.deviceId || '', {
360
+ path: opts.path,
361
+ pubkey: opts.pubkey,
362
+ ciphertext: opts.ciphertext,
363
+ showOnOneKey: opts.showOnDevice === 'true',
364
+ });
365
+ outputResult(globalOpts, result);
366
+ } finally {
367
+ sdk.dispose();
368
+ }
369
+ });
370
+
371
+ program
372
+ .command('nostr-sign-schnorr')
373
+ .description('Sign a Schnorr signature for Nostr')
374
+ .requiredOption('--hash <hex>', 'Hash to sign (hex)')
375
+ .option('--path <path>', 'BIP44 derivation path', "m/44'/1237'/0'/0/0")
376
+ .action(async opts => {
377
+ const globalOpts = program.opts();
378
+ const sdk = await createSDK(globalOpts);
379
+ try {
380
+ const p = getCommonParams(globalOpts);
381
+ const result = await sdk.nostrSignSchnorr(p.connectId || '', p.deviceId || '', {
382
+ path: opts.path,
383
+ hash: opts.hash,
384
+ });
385
+ outputResult(globalOpts, result);
386
+ } finally {
387
+ sdk.dispose();
388
+ }
389
+ });
390
+
391
+ program
392
+ .command('lnurl-auth')
393
+ .description('Authenticate with LNURL (Lightning Network)')
394
+ .requiredOption('--domain <domain>', 'Service domain')
395
+ .requiredOption('--k1 <hex>', 'Challenge k1 parameter')
396
+ .action(async opts => {
397
+ const globalOpts = program.opts();
398
+ const sdk = await createSDK(globalOpts);
399
+ try {
400
+ const p = getCommonParams(globalOpts);
401
+ const result = await sdk.lnurlAuth(p.connectId || '', p.deviceId || '', {
402
+ domain: opts.domain,
403
+ k1: opts.k1,
404
+ });
405
+ outputResult(globalOpts, result);
406
+ } finally {
407
+ sdk.dispose();
408
+ }
409
+ });
410
+
411
+ program
412
+ .command('conflux-sign-cip23')
413
+ .description('Sign a Conflux CIP-23 structured message')
414
+ .requiredOption('--domain-hash <hex>', 'CIP-23 domain hash')
415
+ .requiredOption('--message-hash <hex>', 'CIP-23 message hash')
416
+ .option('--path <path>', 'BIP44 derivation path', "m/44'/503'/0'/0/0")
417
+ .action(async opts => {
418
+ const globalOpts = program.opts();
419
+ const sdk = await createSDK(globalOpts);
420
+ try {
421
+ const p = getCommonParams(globalOpts);
422
+ const result = await sdk.confluxSignMessageCIP23(p.connectId || '', p.deviceId || '', {
423
+ path: opts.path,
424
+ domainHash: opts.domainHash,
425
+ messageHash: opts.messageHash,
426
+ });
427
+ outputResult(globalOpts, result);
428
+ } finally {
429
+ sdk.dispose();
430
+ }
431
+ });
432
+
433
+ program
434
+ .command('aptos-sign-in')
435
+ .description('Sign an Aptos sign-in message')
436
+ .requiredOption('--payload <text>', 'Sign-in payload string')
437
+ .option('--path <path>', 'BIP44 derivation path', "m/44'/637'/0'/0'/0'")
438
+ .action(async opts => {
439
+ const globalOpts = program.opts();
440
+ const sdk = await createSDK(globalOpts);
441
+ try {
442
+ const p = getCommonParams(globalOpts);
443
+ const result = await sdk.aptosSignInMessage(p.connectId || '', p.deviceId || '', {
444
+ path: opts.path,
445
+ payload: opts.payload,
446
+ });
447
+ outputResult(globalOpts, result);
448
+ } finally {
449
+ sdk.dispose();
450
+ }
451
+ });
452
+
453
+ program
454
+ .command('ton-sign-proof')
455
+ .description('Sign a TON proof (for wallet authentication)')
456
+ .requiredOption('--appdomain <domain>', 'Application domain')
457
+ .requiredOption('--expire-at <timestamp>', 'Proof expiration timestamp')
458
+ .option('--comment <text>', 'Optional comment')
459
+ .option('--path <path>', 'BIP44 derivation path', "m/44'/607'/0'")
460
+ .action(async opts => {
461
+ const globalOpts = program.opts();
462
+ const sdk = await createSDK(globalOpts);
463
+ try {
464
+ const p = getCommonParams(globalOpts);
465
+ const result = await sdk.tonSignProof(p.connectId || '', p.deviceId || '', {
466
+ path: opts.path,
467
+ appdomain: opts.appdomain,
468
+ expireAt: safeParseInt(opts.expireAt, '--expire-at'),
469
+ ...(opts.comment ? { comment: opts.comment } : {}),
470
+ });
471
+ outputResult(globalOpts, result);
472
+ } finally {
473
+ sdk.dispose();
474
+ }
475
+ });
476
+
477
+ // ============================================================
478
+ // Firmware Commands
479
+ // ============================================================
480
+
481
+ program
482
+ .command('firmware-check')
483
+ .description('Check if firmware updates are available')
484
+ .action(async () => {
485
+ const globalOpts = program.opts();
486
+ const sdk = await createSDK(globalOpts);
487
+ try {
488
+ const result = await sdk.checkFirmwareRelease(globalOpts.connectId);
489
+ outputResult(globalOpts, result);
490
+ } finally {
491
+ sdk.dispose();
492
+ }
493
+ });
494
+
495
+ program
496
+ .command('firmware-check-all')
497
+ .description('Check all firmware components (system, BLE, bootloader)')
498
+ .action(async () => {
499
+ const globalOpts = program.opts();
500
+ const sdk = await createSDK(globalOpts);
501
+ try {
502
+ const result = await sdk.checkAllFirmwareRelease(globalOpts.connectId);
503
+ outputResult(globalOpts, result);
504
+ } finally {
505
+ sdk.dispose();
506
+ }
507
+ });
508
+
509
+ program
510
+ .command('firmware-update')
511
+ .description('Update device firmware')
512
+ .option('--version <ver>', 'Target firmware version (e.g., "4.8.0")')
513
+ .option('--platform <platform>', 'Platform: native | desktop | ext | web', 'desktop')
514
+ .action(async opts => {
515
+ const globalOpts = program.opts();
516
+ const sdk = await createSDK(globalOpts);
517
+ try {
518
+ // firmwareUpdateV2 requires: connectId, deviceId, { updateType, platform, version? }
519
+ const params: Record<string, unknown> = {
520
+ updateType: 'firmware',
521
+ platform: opts.platform,
522
+ };
523
+ if (opts.version) {
524
+ params.version = parseVersion(opts.version);
525
+ }
526
+ // firmwareUpdateV2 signature: (connectId, params) — 2 args only
527
+ const result = await sdk.firmwareUpdateV2(globalOpts.connectId, params as any);
528
+ outputResult(globalOpts, result);
529
+ } finally {
530
+ sdk.dispose();
531
+ }
532
+ });
533
+
534
+ program
535
+ .command('firmware-update-ble')
536
+ .description('Update BLE (Bluetooth) firmware')
537
+ .option('--version <ver>', 'Target BLE firmware version')
538
+ .option('--platform <platform>', 'Platform: native | desktop | ext | web', 'desktop')
539
+ .action(async opts => {
540
+ const globalOpts = program.opts();
541
+ const sdk = await createSDK(globalOpts);
542
+ try {
543
+ const params: Record<string, unknown> = {
544
+ updateType: 'ble',
545
+ platform: opts.platform,
546
+ };
547
+ if (opts.version) {
548
+ params.version = parseVersion(opts.version);
549
+ }
550
+ const result = await sdk.firmwareUpdateV2(globalOpts.connectId, params as any);
551
+ outputResult(globalOpts, result);
552
+ } finally {
553
+ sdk.dispose();
554
+ }
555
+ });
556
+
557
+ program
558
+ .command('bootloader-check')
559
+ .description('Check bootloader version and status')
560
+ .action(async () => {
561
+ const globalOpts = program.opts();
562
+ const sdk = await createSDK(globalOpts);
563
+ try {
564
+ const result = await sdk.checkBootloaderRelease(globalOpts.connectId);
565
+ outputResult(globalOpts, result);
566
+ } finally {
567
+ sdk.dispose();
568
+ }
569
+ });
570
+
571
+ // ============================================================
572
+ // Security / Management Commands
573
+ // ============================================================
574
+
575
+ program
576
+ .command('change-pin')
577
+ .description('Change or set the device PIN code')
578
+ .option('--remove', 'Remove PIN protection instead of changing')
579
+ .action(async opts => {
580
+ const globalOpts = program.opts();
581
+ const sdk = await createSDK(globalOpts);
582
+ try {
583
+ const result = await sdk.deviceChangePin(globalOpts.connectId, {
584
+ remove: opts.remove ?? false,
585
+ } as any);
586
+ outputResult(globalOpts, result);
587
+ } finally {
588
+ sdk.dispose();
589
+ }
590
+ });
591
+
592
+ program
593
+ .command('passphrase-state')
594
+ .description('Get current passphrase state (for hidden wallet session management)')
595
+ .action(async () => {
596
+ const globalOpts = program.opts();
597
+ const sdk = await createSDK(globalOpts);
598
+ try {
599
+ const result = await sdk.getPassphraseState(globalOpts.connectId, {
600
+ useEmptyPassphrase: globalOpts.useEmptyPassphrase,
601
+ } as any);
602
+ outputResult(globalOpts, result);
603
+ } finally {
604
+ sdk.dispose();
605
+ }
606
+ });
607
+
608
+ program
609
+ .command('toggle-passphrase')
610
+ .description('Enable or disable passphrase (hidden wallet) protection')
611
+ .requiredOption('--enable <bool>', 'true to enable, false to disable')
612
+ .action(async opts => {
613
+ const globalOpts = program.opts();
614
+ const sdk = await createSDK(globalOpts);
615
+ try {
616
+ const result = await sdk.deviceSettings(globalOpts.connectId, {
617
+ usePassphrase: opts.enable === 'true',
618
+ });
619
+ outputResult(globalOpts, result);
620
+ } finally {
621
+ sdk.dispose();
622
+ }
623
+ });
624
+
625
+ program
626
+ .command('device-backup')
627
+ .description('Trigger recovery phrase backup on device')
628
+ .action(async () => {
629
+ const globalOpts = program.opts();
630
+ const sdk = await createSDK(globalOpts);
631
+ try {
632
+ const result = await sdk.deviceBackup(globalOpts.connectId);
633
+ outputResult(globalOpts, result);
634
+ } finally {
635
+ sdk.dispose();
636
+ }
637
+ });
638
+
639
+ program
640
+ .command('device-recovery')
641
+ .description('Recover wallet from recovery phrase (entered on device)')
642
+ .option('--word-count <count>', 'Recovery phrase length: 12, 18, or 24', '24')
643
+ .option('--passphrase-protection <bool>', 'Enable passphrase after recovery', 'false')
644
+ .option('--pin-protection <bool>', 'Set PIN after recovery', 'true')
645
+ .option('--label <name>', 'Device label')
646
+ .action(async opts => {
647
+ const globalOpts = program.opts();
648
+ const sdk = await createSDK(globalOpts);
649
+ try {
650
+ const result = await sdk.deviceRecovery(globalOpts.connectId, {
651
+ wordCount: safeParseInt(opts.wordCount, '--word-count'),
652
+ passphraseProtection: opts.passphraseProtection === 'true',
653
+ pinProtection: opts.pinProtection === 'true',
654
+ label: opts.label,
655
+ });
656
+ outputResult(globalOpts, result);
657
+ } finally {
658
+ sdk.dispose();
659
+ }
660
+ });
661
+
662
+ program
663
+ .command('device-reset')
664
+ .description('Initialize device with a new wallet seed (DESTROYS current wallet)')
665
+ .option('--word-count <count>', 'Seed phrase length: 12, 18, or 24', '24')
666
+ .option('--passphrase-protection <bool>', 'Enable passphrase', 'false')
667
+ .option('--pin-protection <bool>', 'Set PIN', 'true')
668
+ .option('--label <name>', 'Device label')
669
+ .action(async opts => {
670
+ const globalOpts = program.opts();
671
+ const sdk = await createSDK(globalOpts);
672
+ try {
673
+ const result = await sdk.deviceReset(globalOpts.connectId, {
674
+ strength: wordCountToStrength(safeParseInt(opts.wordCount, '--word-count')),
675
+ passphraseProtection: opts.passphraseProtection === 'true',
676
+ pinProtection: opts.pinProtection === 'true',
677
+ label: opts.label,
678
+ });
679
+ outputResult(globalOpts, result);
680
+ } finally {
681
+ sdk.dispose();
682
+ }
683
+ });
684
+
685
+ program
686
+ .command('device-wipe')
687
+ .description('Factory reset — erase ALL data (IRREVERSIBLE)')
688
+ .action(async () => {
689
+ const globalOpts = program.opts();
690
+ const sdk = await createSDK(globalOpts);
691
+ try {
692
+ const result = await sdk.deviceWipe(globalOpts.connectId);
693
+ outputResult(globalOpts, result);
694
+ } finally {
695
+ sdk.dispose();
696
+ }
697
+ });
698
+
699
+ program
700
+ .command('device-settings')
701
+ .description('Update device label and settings')
702
+ .option('--label <name>', 'Device display name')
703
+ .option('--auto-lock-delay <seconds>', 'Auto-lock timeout in seconds (0 = disabled)')
704
+ .option('--language <lang>', 'Device language')
705
+ .option('--passphrase-always-on-device <bool>', 'Always enter passphrase on device')
706
+ .option('--haptic-feedback <bool>', 'Enable/disable haptic feedback')
707
+ .option('--auto-shutdown-delay <seconds>', 'Auto shutdown timeout in seconds')
708
+ .action(async opts => {
709
+ const globalOpts = program.opts();
710
+ const sdk = await createSDK(globalOpts);
711
+ try {
712
+ // Map CLI options to SDK param names (camelCase → snake_case handled by SDK)
713
+ // Reference: packages/core/src/api/device/DeviceSettings.ts
714
+ const settings: Record<string, unknown> = {};
715
+ if (opts.label !== undefined) settings.label = opts.label;
716
+ if (opts.autoLockDelay !== undefined)
717
+ settings.autoLockDelayMs = safeParseInt(opts.autoLockDelay, '--auto-lock-delay') * 1000;
718
+ if (opts.language !== undefined) settings.language = opts.language;
719
+ if (opts.passphraseAlwaysOnDevice !== undefined)
720
+ settings.passphraseAlwaysOnDevice = opts.passphraseAlwaysOnDevice === 'true';
721
+ if (opts.hapticFeedback !== undefined)
722
+ settings.hapticFeedback = opts.hapticFeedback === 'true';
723
+ if (opts.autoShutdownDelay !== undefined)
724
+ settings.autoShutdownDelayMs =
725
+ safeParseInt(opts.autoShutdownDelay, '--auto-shutdown-delay') * 1000;
726
+
727
+ if (Object.keys(settings).length === 0) {
728
+ outputResult(globalOpts, {
729
+ success: false,
730
+ error: 'No settings provided. Use --label, --auto-lock-delay, --language, etc.',
731
+ });
732
+ return;
733
+ }
734
+
735
+ const result = await sdk.deviceSettings(globalOpts.connectId, settings);
736
+ outputResult(globalOpts, result);
737
+ } finally {
738
+ sdk.dispose();
739
+ }
740
+ });
741
+
742
+ program
743
+ .command('device-verify')
744
+ .description('Verify device is genuine OneKey hardware')
745
+ .action(async () => {
746
+ const globalOpts = program.opts();
747
+ const sdk = await createSDK(globalOpts);
748
+ try {
749
+ const result = await sdk.deviceVerify(globalOpts.connectId);
750
+ outputResult(globalOpts, result);
751
+ } finally {
752
+ sdk.dispose();
753
+ }
754
+ });
755
+
756
+ program
757
+ .command('lock')
758
+ .description('Lock the device')
759
+ .action(async () => {
760
+ const globalOpts = program.opts();
761
+ const sdk = await createSDK(globalOpts);
762
+ try {
763
+ const result = await sdk.deviceLock(globalOpts.connectId);
764
+ outputResult(globalOpts, result);
765
+ } finally {
766
+ sdk.dispose();
767
+ }
768
+ });
769
+
770
+ // ============================================================
771
+ // Helpers
772
+ // ============================================================
773
+
774
+ /**
775
+ * Extract common device params from global CLI options.
776
+ * These are passed to every SDK method call.
777
+ */
778
+ function getCommonParams(globalOpts: Record<string, any>) {
779
+ return {
780
+ connectId: globalOpts.connectId,
781
+ deviceId: globalOpts.deviceId,
782
+ passphraseState: globalOpts.passphraseState,
783
+ useEmptyPassphrase: globalOpts.useEmptyPassphrase,
784
+ };
785
+ }
786
+
787
+ function outputResult(globalOpts: { json?: boolean }, result: unknown): void {
788
+ // #10 FIX: Always use JSON.stringify to avoid [Object] truncation
789
+ console.log(JSON.stringify(result, null, 2));
790
+
791
+ // #11 FIX: Exit with code 1 on SDK failure
792
+ if (result && typeof result === 'object' && 'success' in result && !(result as any).success) {
793
+ process.exitCode = 1;
794
+ }
795
+ }
796
+
797
+ function wordCountToStrength(wordCount: number): number {
798
+ // #8 FIX: Validate word count is one of the allowed values
799
+ if (![12, 18, 24].includes(wordCount)) {
800
+ throw new Error(`Invalid word count: ${wordCount}. Must be 12, 18, or 24.`);
801
+ }
802
+ switch (wordCount) {
803
+ case 12:
804
+ return 128;
805
+ case 18:
806
+ return 192;
807
+ case 24:
808
+ default:
809
+ return 256;
810
+ }
811
+ }
812
+
813
+ /**
814
+ * #6 FIX: Safe JSON.parse with structured error output
815
+ */
816
+ function safeJsonParse(input: string, label: string): unknown {
817
+ try {
818
+ return JSON.parse(input);
819
+ } catch {
820
+ console.error(
821
+ JSON.stringify({
822
+ success: false,
823
+ payload: {
824
+ error: `Invalid JSON for ${label}: ${input.slice(0, 100)}`,
825
+ code: 'INVALID_JSON',
826
+ },
827
+ })
828
+ );
829
+ process.exit(1);
830
+ }
831
+ }
832
+
833
+ /**
834
+ * #9 FIX: Safe parseInt with NaN check
835
+ */
836
+ function safeParseInt(input: string, label: string): number {
837
+ const num = parseInt(input, 10);
838
+ if (Number.isNaN(num)) {
839
+ throw new Error(`Invalid number for ${label}: "${input}"`);
840
+ }
841
+ return num;
842
+ }
843
+
844
+ /**
845
+ * #15 FIX: Validate firmware version format
846
+ */
847
+ function parseVersion(input: string): number[] {
848
+ const parts = input.split('.').map(Number);
849
+ if (parts.length < 2 || parts.length > 4 || parts.some(Number.isNaN)) {
850
+ throw new Error(`Invalid version format: "${input}". Expected format: "4.8.0"`);
851
+ }
852
+ return parts;
853
+ }
854
+
855
+ program.parse();