@onekeyfe/hardware-cli 1.1.26-alpha.106 → 1.1.26-alpha.4
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/.eslintignore +4 -0
- package/dist/chains.d.ts +6 -0
- package/dist/chains.js +191 -87
- package/dist/cli.js +615 -496
- package/dist/index.d.ts +16 -89
- package/dist/index.js +1 -2
- package/dist/sdk.d.ts +15 -5
- package/dist/sdk.js +237 -131
- package/dist/session.d.ts +22 -0
- package/dist/session.js +83 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/process-utils.d.ts +2 -0
- package/dist/storage/process-utils.js +44 -0
- package/dist/storage/secure-storage.linux.d.ts +11 -0
- package/dist/storage/secure-storage.linux.js +59 -0
- package/dist/storage/secure-storage.macos.d.ts +11 -0
- package/dist/storage/secure-storage.macos.js +65 -0
- package/dist/storage/storage-factory.d.ts +3 -0
- package/dist/storage/storage-factory.js +14 -0
- package/dist/storage/types.d.ts +18 -0
- package/dist/storage/types.js +2 -0
- package/package.json +15 -13
- package/src/chains.ts +229 -85
- package/src/cli.ts +620 -297
- package/src/sdk.ts +244 -125
- package/src/session.ts +89 -0
- package/src/storage/index.ts +2 -0
- package/src/storage/process-utils.ts +50 -0
- package/src/storage/secure-storage.linux.ts +68 -0
- package/src/storage/secure-storage.macos.ts +68 -0
- package/src/storage/storage-factory.ts +13 -0
- package/src/storage/types.ts +17 -0
- package/tsconfig.json +5 -7
- package/.claude-plugin/plugin.json +0 -14
- package/AGENTS.md +0 -40
- package/CLAUDE.md +0 -40
- package/README.md +0 -112
- package/evals/cases.json +0 -373
- package/evals/run-evals.sh +0 -136
- package/rollup.config.js +0 -28
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
|
|
3
|
-
import { createSDK } from './sdk';
|
|
3
|
+
import { createSDK, disposeSDK } from './sdk';
|
|
4
|
+
import {
|
|
5
|
+
clearSessionFromKeychain,
|
|
6
|
+
preloadSessionFromKeychain,
|
|
7
|
+
saveSessionToKeychain,
|
|
8
|
+
} from './session';
|
|
4
9
|
import {
|
|
5
10
|
resolveBatchGetAddress,
|
|
6
11
|
resolveGetAddress,
|
|
@@ -9,12 +14,23 @@ import {
|
|
|
9
14
|
resolveSignTransaction,
|
|
10
15
|
} from './chains';
|
|
11
16
|
|
|
17
|
+
import type {
|
|
18
|
+
EthereumSignTypedDataMessage,
|
|
19
|
+
EthereumSignTypedDataTypes,
|
|
20
|
+
Features,
|
|
21
|
+
IDeviceType,
|
|
22
|
+
SearchDevice,
|
|
23
|
+
} from '@onekeyfe/hd-core';
|
|
24
|
+
|
|
25
|
+
/** SearchDevice enriched with features fetched after discovery */
|
|
26
|
+
type EnrichedSearchDevice = SearchDevice & { features?: Features };
|
|
27
|
+
|
|
12
28
|
const program = new Command();
|
|
13
29
|
|
|
14
30
|
program
|
|
15
31
|
.name('onekey-hw')
|
|
16
32
|
.description('OneKey hardware wallet CLI for AI agent integration')
|
|
17
|
-
.version('1.1.
|
|
33
|
+
.version('1.1.26-alpha.1');
|
|
18
34
|
|
|
19
35
|
// ============================================================
|
|
20
36
|
// Global Options
|
|
@@ -35,23 +51,23 @@ program.option('--use-empty-passphrase', 'Use standard wallet (skip passphrase p
|
|
|
35
51
|
program
|
|
36
52
|
.command('search')
|
|
37
53
|
.description('Search for connected OneKey hardware wallet devices')
|
|
38
|
-
.action(
|
|
39
|
-
|
|
40
|
-
const sdk = await createSDK(globalOpts);
|
|
41
|
-
try {
|
|
54
|
+
.action(() =>
|
|
55
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
42
56
|
const result = await sdk.searchDevices();
|
|
43
57
|
|
|
44
58
|
// Auto-fetch features for each discovered device (doesn't require PIN)
|
|
45
59
|
if (result?.success && Array.isArray(result.payload)) {
|
|
46
|
-
for (const device of result.payload) {
|
|
60
|
+
for (const device of result.payload as EnrichedSearchDevice[]) {
|
|
47
61
|
if (device.connectId) {
|
|
48
62
|
try {
|
|
49
63
|
const features = await sdk.getFeatures(device.connectId);
|
|
50
64
|
if (features?.success && features.payload) {
|
|
51
65
|
device.features = features.payload;
|
|
52
66
|
device.name = features.payload.label || features.payload.ble_name || device.name;
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
const devType = features.payload.onekey_device_type?.toLowerCase();
|
|
68
|
+
if (devType) {
|
|
69
|
+
device.deviceType = devType as IDeviceType;
|
|
70
|
+
}
|
|
55
71
|
}
|
|
56
72
|
} catch {
|
|
57
73
|
// Features fetch failed — device may need PIN, continue with basic info
|
|
@@ -61,10 +77,35 @@ program
|
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
outputResult(globalOpts, result);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
program
|
|
84
|
+
.command('get-features')
|
|
85
|
+
.description('Get device features (firmware, unlock state, passphrase protection, etc.)')
|
|
86
|
+
.action(() =>
|
|
87
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
88
|
+
// Resolve connectId: explicit flag wins, else pick the first attached device
|
|
89
|
+
let { connectId } = globalOpts as { connectId?: string };
|
|
90
|
+
if (!connectId) {
|
|
91
|
+
const searchResult = await sdk.searchDevices();
|
|
92
|
+
if (
|
|
93
|
+
!searchResult?.success ||
|
|
94
|
+
!Array.isArray(searchResult.payload) ||
|
|
95
|
+
searchResult.payload.length === 0
|
|
96
|
+
) {
|
|
97
|
+
outputResult(globalOpts, {
|
|
98
|
+
success: false,
|
|
99
|
+
payload: { error: 'No device found', code: 'NO_DEVICE' },
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
connectId = (searchResult.payload[0] as EnrichedSearchDevice).connectId ?? undefined;
|
|
104
|
+
}
|
|
105
|
+
const result = await sdk.getFeatures(connectId || '');
|
|
106
|
+
outputResult(globalOpts, result);
|
|
107
|
+
})
|
|
108
|
+
);
|
|
68
109
|
|
|
69
110
|
// ============================================================
|
|
70
111
|
// Signing Commands
|
|
@@ -76,41 +117,33 @@ program
|
|
|
76
117
|
.requiredOption('--chain <chain>', 'Target blockchain (evm, btc, sol, ...)')
|
|
77
118
|
.option('--path <path>', 'BIP44 derivation path')
|
|
78
119
|
.option('--show-on-device <bool>', 'Display address on device for verification', 'true')
|
|
79
|
-
.action(
|
|
80
|
-
|
|
81
|
-
const sdk = await createSDK(globalOpts);
|
|
82
|
-
try {
|
|
120
|
+
.action(opts =>
|
|
121
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
83
122
|
const result = await resolveGetAddress(sdk, {
|
|
84
123
|
chain: opts.chain,
|
|
85
124
|
path: opts.path,
|
|
86
125
|
showOnDevice: opts.showOnDevice === 'true',
|
|
87
|
-
...
|
|
126
|
+
...params,
|
|
88
127
|
});
|
|
89
128
|
outputResult(globalOpts, result);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
});
|
|
129
|
+
})
|
|
130
|
+
);
|
|
94
131
|
|
|
95
132
|
program
|
|
96
133
|
.command('get-public-key')
|
|
97
134
|
.description('Get public key from the hardware wallet')
|
|
98
135
|
.requiredOption('--chain <chain>', 'Target blockchain')
|
|
99
136
|
.option('--path <path>', 'BIP44 derivation path')
|
|
100
|
-
.action(
|
|
101
|
-
|
|
102
|
-
const sdk = await createSDK(globalOpts);
|
|
103
|
-
try {
|
|
137
|
+
.action(opts =>
|
|
138
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
104
139
|
const result = await resolveGetPublicKey(sdk, {
|
|
105
140
|
chain: opts.chain,
|
|
106
141
|
path: opts.path,
|
|
107
|
-
...
|
|
142
|
+
...params,
|
|
108
143
|
});
|
|
109
144
|
outputResult(globalOpts, result);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
});
|
|
145
|
+
})
|
|
146
|
+
);
|
|
114
147
|
|
|
115
148
|
program
|
|
116
149
|
.command('sign-transaction')
|
|
@@ -118,22 +151,18 @@ program
|
|
|
118
151
|
.requiredOption('--chain <chain>', 'Target blockchain')
|
|
119
152
|
.requiredOption('--tx <json>', 'Transaction data (JSON)')
|
|
120
153
|
.option('--path <path>', 'BIP44 derivation path')
|
|
121
|
-
.action(
|
|
122
|
-
|
|
123
|
-
const sdk = await createSDK(globalOpts);
|
|
124
|
-
try {
|
|
154
|
+
.action(opts =>
|
|
155
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
125
156
|
const tx = safeJsonParse(opts.tx, '--tx') as Record<string, unknown>;
|
|
126
157
|
const result = await resolveSignTransaction(sdk, {
|
|
127
158
|
chain: opts.chain,
|
|
128
159
|
path: opts.path,
|
|
129
160
|
transaction: tx,
|
|
130
|
-
...
|
|
161
|
+
...params,
|
|
131
162
|
});
|
|
132
163
|
outputResult(globalOpts, result);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
});
|
|
164
|
+
})
|
|
165
|
+
);
|
|
137
166
|
|
|
138
167
|
program
|
|
139
168
|
.command('sign-message')
|
|
@@ -141,67 +170,56 @@ program
|
|
|
141
170
|
.requiredOption('--chain <chain>', 'Target blockchain')
|
|
142
171
|
.requiredOption('--message <msg>', 'Message to sign')
|
|
143
172
|
.option('--path <path>', 'BIP44 derivation path')
|
|
144
|
-
.action(
|
|
145
|
-
|
|
146
|
-
const sdk = await createSDK(globalOpts);
|
|
147
|
-
try {
|
|
173
|
+
.action(opts =>
|
|
174
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
148
175
|
const result = await resolveSignMessage(sdk, {
|
|
149
176
|
chain: opts.chain,
|
|
150
177
|
path: opts.path,
|
|
151
178
|
message: opts.message,
|
|
152
|
-
...
|
|
179
|
+
...params,
|
|
153
180
|
});
|
|
154
181
|
outputResult(globalOpts, result);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
});
|
|
182
|
+
})
|
|
183
|
+
);
|
|
159
184
|
|
|
160
185
|
program
|
|
161
186
|
.command('sign-typed-data')
|
|
162
187
|
.description('Sign EIP-712 typed data (EVM only, requires device confirmation)')
|
|
163
188
|
.requiredOption('--data <json>', 'EIP-712 typed data JSON')
|
|
164
189
|
.option('--path <path>', 'BIP44 derivation path')
|
|
165
|
-
.option('--metamask-v4-compat', '
|
|
166
|
-
.action(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
190
|
+
.option('--no-metamask-v4-compat', 'Disable MetaMask V4 compatibility mode')
|
|
191
|
+
.action(opts =>
|
|
192
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
193
|
+
const data = safeJsonParse(
|
|
194
|
+
opts.data,
|
|
195
|
+
'--data'
|
|
196
|
+
) as EthereumSignTypedDataMessage<EthereumSignTypedDataTypes>;
|
|
172
197
|
const path = opts.path || "m/44'/60'/0'/0/0";
|
|
173
198
|
const result = await sdk.evmSignTypedData(params.connectId || '', params.deviceId || '', {
|
|
174
199
|
path,
|
|
175
200
|
metamaskV4Compat: opts.metamaskV4Compat,
|
|
176
201
|
data,
|
|
177
202
|
...params,
|
|
178
|
-
}
|
|
203
|
+
});
|
|
179
204
|
outputResult(globalOpts, result);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
});
|
|
205
|
+
})
|
|
206
|
+
);
|
|
184
207
|
|
|
185
208
|
program
|
|
186
209
|
.command('sign-psbt')
|
|
187
210
|
.description('Sign a Bitcoin PSBT (Pro/Classic1s only, requires device confirmation)')
|
|
188
211
|
.requiredOption('--psbt <hex>', 'Hex-encoded PSBT data')
|
|
189
212
|
.option('--coin <coin>', 'Bitcoin network: btc, ltc, etc.', 'btc')
|
|
190
|
-
.action(
|
|
191
|
-
|
|
192
|
-
const sdk = await createSDK(globalOpts);
|
|
193
|
-
try {
|
|
194
|
-
const params = getCommonParams(globalOpts);
|
|
213
|
+
.action(opts =>
|
|
214
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
195
215
|
const result = await sdk.btcSignPsbt(params.connectId || '', params.deviceId || '', {
|
|
196
216
|
psbt: opts.psbt,
|
|
197
217
|
coin: opts.coin,
|
|
198
218
|
...params,
|
|
199
|
-
}
|
|
219
|
+
});
|
|
200
220
|
outputResult(globalOpts, result);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
});
|
|
221
|
+
})
|
|
222
|
+
);
|
|
205
223
|
|
|
206
224
|
program
|
|
207
225
|
.command('verify-message')
|
|
@@ -210,11 +228,8 @@ program
|
|
|
210
228
|
.requiredOption('--address <addr>', 'Signer address')
|
|
211
229
|
.requiredOption('--message <msg>', 'Original message')
|
|
212
230
|
.requiredOption('--signature <sig>', 'Signature to verify')
|
|
213
|
-
.action(
|
|
214
|
-
|
|
215
|
-
const sdk = await createSDK(globalOpts);
|
|
216
|
-
try {
|
|
217
|
-
const params = getCommonParams(globalOpts);
|
|
231
|
+
.action(opts =>
|
|
232
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
218
233
|
const cid = params.connectId || '';
|
|
219
234
|
const did = params.deviceId || '';
|
|
220
235
|
let result: unknown;
|
|
@@ -226,23 +241,26 @@ program
|
|
|
226
241
|
address: opts.address,
|
|
227
242
|
messageHex: opts.message,
|
|
228
243
|
signature: opts.signature,
|
|
244
|
+
...params,
|
|
229
245
|
});
|
|
230
246
|
break;
|
|
231
247
|
case 'btc':
|
|
232
248
|
case 'bitcoin':
|
|
233
249
|
result = await sdk.btcVerifyMessage(cid, did, {
|
|
234
250
|
address: opts.address,
|
|
235
|
-
|
|
251
|
+
messageHex: opts.message,
|
|
236
252
|
signature: opts.signature,
|
|
237
253
|
coin: 'btc',
|
|
254
|
+
...params,
|
|
238
255
|
});
|
|
239
256
|
break;
|
|
240
257
|
case 'starcoin':
|
|
241
258
|
case 'stc':
|
|
242
259
|
result = await sdk.starcoinVerifyMessage(cid, did, {
|
|
243
260
|
publicKey: opts.address,
|
|
244
|
-
|
|
261
|
+
messageHex: opts.message,
|
|
245
262
|
signature: opts.signature,
|
|
263
|
+
...params,
|
|
246
264
|
});
|
|
247
265
|
break;
|
|
248
266
|
default:
|
|
@@ -251,19 +269,15 @@ program
|
|
|
251
269
|
);
|
|
252
270
|
}
|
|
253
271
|
outputResult(globalOpts, result);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
});
|
|
272
|
+
})
|
|
273
|
+
);
|
|
258
274
|
|
|
259
275
|
program
|
|
260
276
|
.command('batch-get-address')
|
|
261
277
|
.description('Get addresses for multiple chains/paths in a single session')
|
|
262
278
|
.requiredOption('--bundle <json>', 'JSON array of {chain, path, showOnDevice}')
|
|
263
|
-
.action(
|
|
264
|
-
|
|
265
|
-
const sdk = await createSDK(globalOpts);
|
|
266
|
-
try {
|
|
279
|
+
.action(opts =>
|
|
280
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
267
281
|
const bundle = safeJsonParse(opts.bundle, '--bundle') as Array<{
|
|
268
282
|
chain: string;
|
|
269
283
|
path?: string;
|
|
@@ -271,13 +285,11 @@ program
|
|
|
271
285
|
}>;
|
|
272
286
|
const result = await resolveBatchGetAddress(sdk, {
|
|
273
287
|
bundle,
|
|
274
|
-
...
|
|
288
|
+
...params,
|
|
275
289
|
});
|
|
276
290
|
outputResult(globalOpts, result);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
});
|
|
291
|
+
})
|
|
292
|
+
);
|
|
281
293
|
|
|
282
294
|
// ============================================================
|
|
283
295
|
// Chain-Specific Commands
|
|
@@ -289,41 +301,37 @@ program
|
|
|
289
301
|
.requiredOption('--domain-hash <hex>', 'EIP-712 domain separator hash')
|
|
290
302
|
.requiredOption('--message-hash <hex>', 'EIP-712 message hash')
|
|
291
303
|
.option('--path <path>', 'BIP44 derivation path', "m/44'/60'/0'/0/0")
|
|
292
|
-
.action(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
const p = getCommonParams(globalOpts);
|
|
297
|
-
const result = await sdk.evmSignMessageEIP712(p.connectId || '', p.deviceId || '', {
|
|
304
|
+
.action(opts =>
|
|
305
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
306
|
+
const result = await sdk.evmSignMessageEIP712(params.connectId || '', params.deviceId || '', {
|
|
298
307
|
path: opts.path,
|
|
299
308
|
domainHash: opts.domainHash,
|
|
300
309
|
messageHash: opts.messageHash,
|
|
310
|
+
...params,
|
|
301
311
|
});
|
|
302
312
|
outputResult(globalOpts, result);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
});
|
|
313
|
+
})
|
|
314
|
+
);
|
|
307
315
|
|
|
308
316
|
program
|
|
309
317
|
.command('sol-sign-offchain')
|
|
310
318
|
.description('Sign a Solana off-chain message (requires device confirmation)')
|
|
311
319
|
.requiredOption('--message-hex <hex>', 'Off-chain message as hex')
|
|
312
320
|
.option('--path <path>', 'BIP44 derivation path', "m/44'/501'/0'/0'")
|
|
313
|
-
.action(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
321
|
+
.action(opts =>
|
|
322
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
323
|
+
const result = await sdk.solSignOffchainMessage(
|
|
324
|
+
params.connectId || '',
|
|
325
|
+
params.deviceId || '',
|
|
326
|
+
{
|
|
327
|
+
path: opts.path,
|
|
328
|
+
messageHex: opts.messageHex,
|
|
329
|
+
...params,
|
|
330
|
+
}
|
|
331
|
+
);
|
|
322
332
|
outputResult(globalOpts, result);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
});
|
|
333
|
+
})
|
|
334
|
+
);
|
|
327
335
|
|
|
328
336
|
program
|
|
329
337
|
.command('nostr-encrypt')
|
|
@@ -332,22 +340,18 @@ program
|
|
|
332
340
|
.requiredOption('--plaintext <text>', 'Message to encrypt')
|
|
333
341
|
.option('--path <path>', 'BIP44 derivation path', "m/44'/1237'/0'/0/0")
|
|
334
342
|
.option('--show-on-device <bool>', 'Display on device', 'false')
|
|
335
|
-
.action(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
const p = getCommonParams(globalOpts);
|
|
340
|
-
const result = await sdk.nostrEncryptMessage(p.connectId || '', p.deviceId || '', {
|
|
343
|
+
.action(opts =>
|
|
344
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
345
|
+
const result = await sdk.nostrEncryptMessage(params.connectId || '', params.deviceId || '', {
|
|
341
346
|
path: opts.path,
|
|
342
347
|
pubkey: opts.pubkey,
|
|
343
348
|
plaintext: opts.plaintext,
|
|
344
349
|
showOnOneKey: opts.showOnDevice === 'true',
|
|
350
|
+
...params,
|
|
345
351
|
});
|
|
346
352
|
outputResult(globalOpts, result);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
});
|
|
353
|
+
})
|
|
354
|
+
);
|
|
351
355
|
|
|
352
356
|
program
|
|
353
357
|
.command('nostr-decrypt')
|
|
@@ -356,62 +360,50 @@ program
|
|
|
356
360
|
.requiredOption('--ciphertext <text>', 'Encrypted message')
|
|
357
361
|
.option('--path <path>', 'BIP44 derivation path', "m/44'/1237'/0'/0/0")
|
|
358
362
|
.option('--show-on-device <bool>', 'Display on device', 'false')
|
|
359
|
-
.action(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
const p = getCommonParams(globalOpts);
|
|
364
|
-
const result = await sdk.nostrDecryptMessage(p.connectId || '', p.deviceId || '', {
|
|
363
|
+
.action(opts =>
|
|
364
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
365
|
+
const result = await sdk.nostrDecryptMessage(params.connectId || '', params.deviceId || '', {
|
|
365
366
|
path: opts.path,
|
|
366
367
|
pubkey: opts.pubkey,
|
|
367
368
|
ciphertext: opts.ciphertext,
|
|
368
369
|
showOnOneKey: opts.showOnDevice === 'true',
|
|
370
|
+
...params,
|
|
369
371
|
});
|
|
370
372
|
outputResult(globalOpts, result);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
});
|
|
373
|
+
})
|
|
374
|
+
);
|
|
375
375
|
|
|
376
376
|
program
|
|
377
377
|
.command('nostr-sign-schnorr')
|
|
378
378
|
.description('Sign a Schnorr signature for Nostr')
|
|
379
379
|
.requiredOption('--hash <hex>', 'Hash to sign (hex)')
|
|
380
380
|
.option('--path <path>', 'BIP44 derivation path', "m/44'/1237'/0'/0/0")
|
|
381
|
-
.action(
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
try {
|
|
385
|
-
const p = getCommonParams(globalOpts);
|
|
386
|
-
const result = await sdk.nostrSignSchnorr(p.connectId || '', p.deviceId || '', {
|
|
381
|
+
.action(opts =>
|
|
382
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
383
|
+
const result = await sdk.nostrSignSchnorr(params.connectId || '', params.deviceId || '', {
|
|
387
384
|
path: opts.path,
|
|
388
385
|
hash: opts.hash,
|
|
386
|
+
...params,
|
|
389
387
|
});
|
|
390
388
|
outputResult(globalOpts, result);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
});
|
|
389
|
+
})
|
|
390
|
+
);
|
|
395
391
|
|
|
396
392
|
program
|
|
397
393
|
.command('lnurl-auth')
|
|
398
394
|
.description('Authenticate with LNURL (Lightning Network)')
|
|
399
395
|
.requiredOption('--domain <domain>', 'Service domain')
|
|
400
396
|
.requiredOption('--k1 <hex>', 'Challenge k1 parameter')
|
|
401
|
-
.action(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
const p = getCommonParams(globalOpts);
|
|
406
|
-
const result = await sdk.lnurlAuth(p.connectId || '', p.deviceId || '', {
|
|
397
|
+
.action(opts =>
|
|
398
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
399
|
+
const result = await sdk.lnurlAuth(params.connectId || '', params.deviceId || '', {
|
|
407
400
|
domain: opts.domain,
|
|
408
401
|
k1: opts.k1,
|
|
402
|
+
...params,
|
|
409
403
|
});
|
|
410
404
|
outputResult(globalOpts, result);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
});
|
|
405
|
+
})
|
|
406
|
+
);
|
|
415
407
|
|
|
416
408
|
program
|
|
417
409
|
.command('conflux-sign-cip23')
|
|
@@ -419,41 +411,37 @@ program
|
|
|
419
411
|
.requiredOption('--domain-hash <hex>', 'CIP-23 domain hash')
|
|
420
412
|
.requiredOption('--message-hash <hex>', 'CIP-23 message hash')
|
|
421
413
|
.option('--path <path>', 'BIP44 derivation path', "m/44'/503'/0'/0/0")
|
|
422
|
-
.action(
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
414
|
+
.action(opts =>
|
|
415
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
416
|
+
const result = await sdk.confluxSignMessageCIP23(
|
|
417
|
+
params.connectId || '',
|
|
418
|
+
params.deviceId || '',
|
|
419
|
+
{
|
|
420
|
+
path: opts.path,
|
|
421
|
+
domainHash: opts.domainHash,
|
|
422
|
+
messageHash: opts.messageHash,
|
|
423
|
+
...params,
|
|
424
|
+
}
|
|
425
|
+
);
|
|
432
426
|
outputResult(globalOpts, result);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
});
|
|
427
|
+
})
|
|
428
|
+
);
|
|
437
429
|
|
|
438
430
|
program
|
|
439
431
|
.command('aptos-sign-in')
|
|
440
432
|
.description('Sign an Aptos sign-in message')
|
|
441
433
|
.requiredOption('--payload <text>', 'Sign-in payload string')
|
|
442
434
|
.option('--path <path>', 'BIP44 derivation path', "m/44'/637'/0'/0'/0'")
|
|
443
|
-
.action(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
try {
|
|
447
|
-
const p = getCommonParams(globalOpts);
|
|
448
|
-
const result = await sdk.aptosSignInMessage(p.connectId || '', p.deviceId || '', {
|
|
435
|
+
.action(opts =>
|
|
436
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
437
|
+
const result = await sdk.aptosSignInMessage(params.connectId || '', params.deviceId || '', {
|
|
449
438
|
path: opts.path,
|
|
450
439
|
payload: opts.payload,
|
|
440
|
+
...params,
|
|
451
441
|
});
|
|
452
442
|
outputResult(globalOpts, result);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
});
|
|
443
|
+
})
|
|
444
|
+
);
|
|
457
445
|
|
|
458
446
|
program
|
|
459
447
|
.command('ton-sign-proof')
|
|
@@ -462,22 +450,18 @@ program
|
|
|
462
450
|
.requiredOption('--expire-at <timestamp>', 'Proof expiration timestamp')
|
|
463
451
|
.option('--comment <text>', 'Optional comment')
|
|
464
452
|
.option('--path <path>', 'BIP44 derivation path', "m/44'/607'/0'")
|
|
465
|
-
.action(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
try {
|
|
469
|
-
const p = getCommonParams(globalOpts);
|
|
470
|
-
const result = await sdk.tonSignProof(p.connectId || '', p.deviceId || '', {
|
|
453
|
+
.action(opts =>
|
|
454
|
+
runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
|
|
455
|
+
const result = await sdk.tonSignProof(params.connectId || '', params.deviceId || '', {
|
|
471
456
|
path: opts.path,
|
|
472
457
|
appdomain: opts.appdomain,
|
|
473
458
|
expireAt: safeParseInt(opts.expireAt, '--expire-at'),
|
|
474
459
|
...(opts.comment ? { comment: opts.comment } : {}),
|
|
460
|
+
...params,
|
|
475
461
|
});
|
|
476
462
|
outputResult(globalOpts, result);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
});
|
|
463
|
+
})
|
|
464
|
+
);
|
|
481
465
|
|
|
482
466
|
// ============================================================
|
|
483
467
|
// Firmware Commands
|
|
@@ -486,72 +470,60 @@ program
|
|
|
486
470
|
program
|
|
487
471
|
.command('firmware-check')
|
|
488
472
|
.description('Check if firmware updates are available')
|
|
489
|
-
.action(
|
|
490
|
-
|
|
491
|
-
const sdk = await createSDK(globalOpts);
|
|
492
|
-
try {
|
|
473
|
+
.action(() =>
|
|
474
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
493
475
|
const result = await sdk.checkFirmwareRelease(globalOpts.connectId);
|
|
494
476
|
outputResult(globalOpts, result);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
});
|
|
477
|
+
})
|
|
478
|
+
);
|
|
499
479
|
|
|
500
480
|
program
|
|
501
481
|
.command('firmware-check-all')
|
|
502
482
|
.description('Check all firmware components (system, BLE, bootloader)')
|
|
503
|
-
.action(
|
|
504
|
-
|
|
505
|
-
const sdk = await createSDK(globalOpts);
|
|
506
|
-
try {
|
|
483
|
+
.action(() =>
|
|
484
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
507
485
|
const result = await sdk.checkAllFirmwareRelease(globalOpts.connectId);
|
|
508
486
|
outputResult(globalOpts, result);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
});
|
|
487
|
+
})
|
|
488
|
+
);
|
|
513
489
|
|
|
514
490
|
program
|
|
515
491
|
.command('firmware-update')
|
|
516
492
|
.description('Firmware update is not supported via CLI')
|
|
517
|
-
.action(() =>
|
|
518
|
-
|
|
493
|
+
.action(() =>
|
|
494
|
+
respondAndExit({
|
|
519
495
|
success: false,
|
|
520
496
|
payload: {
|
|
521
497
|
error:
|
|
522
498
|
'Firmware update via CLI is not supported. Please use the OneKey App or https://firmware.onekey.so/ to update firmware.',
|
|
523
499
|
code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
|
|
524
500
|
},
|
|
525
|
-
})
|
|
526
|
-
|
|
501
|
+
})
|
|
502
|
+
);
|
|
527
503
|
|
|
528
504
|
program
|
|
529
505
|
.command('firmware-update-ble')
|
|
530
506
|
.description('BLE firmware update is not supported via CLI')
|
|
531
|
-
.action(() =>
|
|
532
|
-
|
|
507
|
+
.action(() =>
|
|
508
|
+
respondAndExit({
|
|
533
509
|
success: false,
|
|
534
510
|
payload: {
|
|
535
511
|
error:
|
|
536
512
|
'BLE firmware update via CLI is not supported. Please use the OneKey App or https://firmware.onekey.so/ to update firmware.',
|
|
537
513
|
code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
|
|
538
514
|
},
|
|
539
|
-
})
|
|
540
|
-
|
|
515
|
+
})
|
|
516
|
+
);
|
|
541
517
|
|
|
542
518
|
program
|
|
543
519
|
.command('bootloader-check')
|
|
544
520
|
.description('Check bootloader version and status')
|
|
545
|
-
.action(
|
|
546
|
-
|
|
547
|
-
const sdk = await createSDK(globalOpts);
|
|
548
|
-
try {
|
|
521
|
+
.action(() =>
|
|
522
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
549
523
|
const result = await sdk.checkBootloaderRelease(globalOpts.connectId);
|
|
550
524
|
outputResult(globalOpts, result);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
});
|
|
525
|
+
})
|
|
526
|
+
);
|
|
555
527
|
|
|
556
528
|
// ============================================================
|
|
557
529
|
// Security / Management Commands
|
|
@@ -561,64 +533,59 @@ program
|
|
|
561
533
|
.command('change-pin')
|
|
562
534
|
.description('Change or set the device PIN code')
|
|
563
535
|
.option('--remove', 'Remove PIN protection instead of changing')
|
|
564
|
-
.action(
|
|
565
|
-
|
|
566
|
-
const sdk = await createSDK(globalOpts);
|
|
567
|
-
try {
|
|
536
|
+
.action(opts =>
|
|
537
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
568
538
|
const result = await sdk.deviceChangePin(globalOpts.connectId, {
|
|
569
539
|
remove: opts.remove ?? false,
|
|
570
|
-
}
|
|
540
|
+
});
|
|
571
541
|
outputResult(globalOpts, result);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
});
|
|
542
|
+
})
|
|
543
|
+
);
|
|
576
544
|
|
|
577
545
|
program
|
|
578
546
|
.command('passphrase-state')
|
|
579
547
|
.description('Get current passphrase state (for hidden wallet session management)')
|
|
580
|
-
.action(
|
|
581
|
-
|
|
582
|
-
const sdk = await createSDK(globalOpts);
|
|
583
|
-
try {
|
|
548
|
+
.action(() =>
|
|
549
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
584
550
|
const result = await sdk.getPassphraseState(globalOpts.connectId, {
|
|
585
551
|
useEmptyPassphrase: globalOpts.useEmptyPassphrase,
|
|
586
|
-
}
|
|
552
|
+
});
|
|
587
553
|
outputResult(globalOpts, result);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
});
|
|
554
|
+
})
|
|
555
|
+
);
|
|
592
556
|
|
|
593
557
|
program
|
|
594
558
|
.command('toggle-passphrase')
|
|
595
559
|
.description('Enable or disable passphrase (hidden wallet) protection')
|
|
596
560
|
.requiredOption('--enable <bool>', 'true to enable, false to disable')
|
|
597
|
-
.action(
|
|
598
|
-
|
|
599
|
-
const sdk = await createSDK(globalOpts);
|
|
600
|
-
try {
|
|
561
|
+
.action(opts =>
|
|
562
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
601
563
|
const result = await sdk.deviceSettings(globalOpts.connectId, {
|
|
602
564
|
usePassphrase: opts.enable === 'true',
|
|
603
565
|
});
|
|
604
566
|
outputResult(globalOpts, result);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
});
|
|
567
|
+
})
|
|
568
|
+
);
|
|
609
569
|
|
|
610
570
|
program
|
|
611
571
|
.command('device-wipe')
|
|
612
|
-
.description('Factory reset — erase ALL data (IRREVERSIBLE)')
|
|
613
|
-
.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
572
|
+
.description('Factory reset — erase ALL data (IRREVERSIBLE, requires --yes)')
|
|
573
|
+
.option('--yes', 'Confirm factory reset (required)')
|
|
574
|
+
.action(opts => {
|
|
575
|
+
if (!opts.yes) {
|
|
576
|
+
respondAndExit({
|
|
577
|
+
success: false,
|
|
578
|
+
payload: {
|
|
579
|
+
error: 'Factory reset requires --yes flag to confirm. This operation is IRREVERSIBLE.',
|
|
580
|
+
code: 'CONFIRMATION_REQUIRED',
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
return runCommand({}, async ({ sdk, globalOpts }) => {
|
|
617
586
|
const result = await sdk.deviceWipe(globalOpts.connectId);
|
|
618
587
|
outputResult(globalOpts, result);
|
|
619
|
-
}
|
|
620
|
-
sdk.dispose();
|
|
621
|
-
}
|
|
588
|
+
});
|
|
622
589
|
});
|
|
623
590
|
|
|
624
591
|
program
|
|
@@ -630,10 +597,8 @@ program
|
|
|
630
597
|
.option('--passphrase-always-on-device <bool>', 'Always enter passphrase on device')
|
|
631
598
|
.option('--haptic-feedback <bool>', 'Enable/disable haptic feedback')
|
|
632
599
|
.option('--auto-shutdown-delay <seconds>', 'Auto shutdown timeout in seconds')
|
|
633
|
-
.action(
|
|
634
|
-
|
|
635
|
-
const sdk = await createSDK(globalOpts);
|
|
636
|
-
try {
|
|
600
|
+
.action(opts =>
|
|
601
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
637
602
|
// Map CLI options to SDK param names (camelCase → snake_case handled by SDK)
|
|
638
603
|
// Reference: packages/core/src/api/device/DeviceSettings.ts
|
|
639
604
|
const settings: Record<string, unknown> = {};
|
|
@@ -659,38 +624,131 @@ program
|
|
|
659
624
|
|
|
660
625
|
const result = await sdk.deviceSettings(globalOpts.connectId, settings);
|
|
661
626
|
outputResult(globalOpts, result);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
});
|
|
627
|
+
})
|
|
628
|
+
);
|
|
666
629
|
|
|
667
630
|
program
|
|
668
631
|
.command('device-verify')
|
|
669
632
|
.description('Verify device is genuine OneKey hardware')
|
|
670
|
-
.action(
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
try {
|
|
674
|
-
const result = await sdk.deviceVerify(globalOpts.connectId);
|
|
633
|
+
.action(() =>
|
|
634
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
635
|
+
const result = await sdk.deviceVerify(globalOpts.connectId, { dataHex: '' });
|
|
675
636
|
outputResult(globalOpts, result);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
});
|
|
637
|
+
})
|
|
638
|
+
);
|
|
680
639
|
|
|
681
640
|
program
|
|
682
641
|
.command('lock')
|
|
683
642
|
.description('Lock the device')
|
|
684
|
-
.action(
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
try {
|
|
688
|
-
const result = await sdk.deviceLock(globalOpts.connectId);
|
|
643
|
+
.action(() =>
|
|
644
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
645
|
+
const result = await sdk.deviceLock(globalOpts.connectId, {});
|
|
689
646
|
outputResult(globalOpts, result);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
647
|
+
})
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
// ============================================================
|
|
651
|
+
// Session Management Commands
|
|
652
|
+
// ============================================================
|
|
653
|
+
|
|
654
|
+
const sessionCmd = program.command('session').description('Manage device passphrase session cache');
|
|
655
|
+
|
|
656
|
+
sessionCmd
|
|
657
|
+
.command('connect')
|
|
658
|
+
.description('Connect device and establish passphrase session (cached for subsequent commands)')
|
|
659
|
+
.action(() =>
|
|
660
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
661
|
+
// 1. Search for device
|
|
662
|
+
const searchResult = await sdk.searchDevices();
|
|
663
|
+
if (!searchResult?.success || !searchResult.payload?.length) {
|
|
664
|
+
outputResult(globalOpts, {
|
|
665
|
+
success: false,
|
|
666
|
+
payload: { error: 'No device found', code: 'NO_DEVICE' },
|
|
667
|
+
});
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const device = searchResult.payload[0] as EnrichedSearchDevice;
|
|
671
|
+
const connectId = device.connectId || globalOpts.connectId;
|
|
672
|
+
|
|
673
|
+
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
674
|
+
// device session, which a locked device will reject with an obscure
|
|
675
|
+
// error. Uses the same PinInvalid retry policy as prepareSession.
|
|
676
|
+
if (device.features?.unlocked === false) {
|
|
677
|
+
process.stderr.write('[onekey-hw] Device is locked. Unlocking (PIN required)...\n');
|
|
678
|
+
await unlockWithRetry(sdk, connectId);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// 3. Get passphraseState (triggers 1/2/3 selection)
|
|
682
|
+
const psResult = await sdk.getPassphraseState(connectId, {
|
|
683
|
+
initSession: true,
|
|
684
|
+
useEmptyPassphrase: false,
|
|
685
|
+
});
|
|
686
|
+
if (!psResult.success) {
|
|
687
|
+
outputResult(globalOpts, psResult);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const passphraseState = psResult.payload;
|
|
691
|
+
|
|
692
|
+
// 4. Get address to verify + extract deviceId
|
|
693
|
+
const addrResult = await sdk.evmGetAddress(connectId, device.deviceId || '', {
|
|
694
|
+
path: "m/44'/60'/0'/0/0",
|
|
695
|
+
showOnOneKey: false,
|
|
696
|
+
passphraseState,
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// 5. Fetch the now-active session_id via getFeatures.
|
|
700
|
+
//
|
|
701
|
+
// IMPORTANT: pass `passphraseState` here. Without it, the SDK's
|
|
702
|
+
// connectStateChange guard (core/index.ts) would see the payload's
|
|
703
|
+
// passphraseState flip from mnNy → undefined, clear the cached Device,
|
|
704
|
+
// and call Initialize again with no passphrase_state / no session_id.
|
|
705
|
+
// That Initialize resets the device to the standard wallet and returns
|
|
706
|
+
// a *standard-wallet* session_id — which we'd then save in the keychain
|
|
707
|
+
// paired with the hidden-wallet passphraseState. On the next CLI run
|
|
708
|
+
// the mismatch would trigger PassphraseRequest (1/2/3 again).
|
|
709
|
+
const featResult = await sdk.getFeatures(connectId, {
|
|
710
|
+
passphraseState,
|
|
711
|
+
skipPassphraseCheck: true,
|
|
712
|
+
});
|
|
713
|
+
const featPayload = featResult?.success ? featResult.payload : undefined;
|
|
714
|
+
const deviceId = featPayload?.device_id || device.deviceId || '';
|
|
715
|
+
const sessionId = featPayload?.session_id || '';
|
|
716
|
+
|
|
717
|
+
// 6. Save to keychain
|
|
718
|
+
if (passphraseState && deviceId && sessionId) {
|
|
719
|
+
await saveSessionToKeychain(deviceId, passphraseState, sessionId);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
outputResult(globalOpts, {
|
|
723
|
+
success: true,
|
|
724
|
+
payload: {
|
|
725
|
+
passphraseState,
|
|
726
|
+
deviceId,
|
|
727
|
+
...(sessionId ? { sessionId } : {}),
|
|
728
|
+
...(addrResult?.success ? { address: addrResult.payload.address } : {}),
|
|
729
|
+
},
|
|
730
|
+
});
|
|
731
|
+
})
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
sessionCmd
|
|
735
|
+
.command('disconnect')
|
|
736
|
+
.description('Clear cached device session')
|
|
737
|
+
.action(() =>
|
|
738
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
739
|
+
const searchResult = await sdk.searchDevices();
|
|
740
|
+
const device = // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
741
|
+
(searchResult?.payload as any)?.[0];
|
|
742
|
+
const deviceId = device?.features?.device_id || device?.deviceId;
|
|
743
|
+
if (deviceId) {
|
|
744
|
+
await clearSessionFromKeychain(deviceId);
|
|
745
|
+
}
|
|
746
|
+
outputResult(globalOpts, {
|
|
747
|
+
success: true,
|
|
748
|
+
payload: deviceId ? { deviceId } : {},
|
|
749
|
+
});
|
|
750
|
+
})
|
|
751
|
+
);
|
|
694
752
|
|
|
695
753
|
// ============================================================
|
|
696
754
|
// Helpers
|
|
@@ -699,45 +757,310 @@ program
|
|
|
699
757
|
/**
|
|
700
758
|
* Extract common device params from global CLI options.
|
|
701
759
|
* These are passed to every SDK method call.
|
|
760
|
+
*
|
|
761
|
+
* skipPassphraseCheck is always true because:
|
|
762
|
+
* - prepareSession handles passphrase selection (1/2/3 + pinentry/device)
|
|
763
|
+
* - Without it, SDK's checkPassphraseStateSafety triggers a SECOND
|
|
764
|
+
* REQUEST_PASSPHRASE (double prompt) even when passphraseState is set
|
|
765
|
+
* - Error 114 (no passphrase state) is also bypassed — our interactive
|
|
766
|
+
* handler in REQUEST_PASSPHRASE handles it correctly
|
|
702
767
|
*/
|
|
768
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
703
769
|
function getCommonParams(globalOpts: Record<string, any>) {
|
|
704
770
|
return {
|
|
705
771
|
connectId: globalOpts.connectId,
|
|
706
772
|
deviceId: globalOpts.deviceId,
|
|
707
773
|
passphraseState: globalOpts.passphraseState,
|
|
708
774
|
useEmptyPassphrase: globalOpts.useEmptyPassphrase,
|
|
775
|
+
skipPassphraseCheck: true,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* HardwareErrorCode.PinInvalid from @onekeyfe/hd-shared. Duplicated here
|
|
781
|
+
* to avoid pulling the shared package's runtime just for one enum value.
|
|
782
|
+
* (PinCancelled is 802 — we don't retry it, any non-PinInvalid failure
|
|
783
|
+
* falls through to the "bail out" branch below.)
|
|
784
|
+
*/
|
|
785
|
+
const HW_ERR_PIN_INVALID = 801;
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Unlock a locked device, retrying up to `maxAttempts` times on
|
|
789
|
+
* `PinInvalid`. On `PinCancelled` or any other failure, throws immediately
|
|
790
|
+
* so the structured error surfaces to the user via runCommand's catch.
|
|
791
|
+
*
|
|
792
|
+
* With `@@ONEKEY_INPUT_PIN_IN_DEVICE` semantics the device firmware
|
|
793
|
+
* normally loops PIN entry internally and only reports back on success
|
|
794
|
+
* or cancel. Retrying here is defensive: if the device ever surfaces
|
|
795
|
+
* `PinInvalid` directly (older firmware, specific models), give the
|
|
796
|
+
* user another chance without forcing a re-run of the whole command.
|
|
797
|
+
*/
|
|
798
|
+
async function unlockWithRetry(
|
|
799
|
+
sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default,
|
|
800
|
+
connectId: string,
|
|
801
|
+
maxAttempts = 3
|
|
802
|
+
): Promise<{
|
|
803
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
804
|
+
payload: any;
|
|
805
|
+
}> {
|
|
806
|
+
let lastPayload: { error?: string; code?: number | string } = {};
|
|
807
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
808
|
+
const result = await sdk.deviceUnlock(connectId, {});
|
|
809
|
+
if (result?.success && result.payload) {
|
|
810
|
+
return { payload: result.payload };
|
|
811
|
+
}
|
|
812
|
+
lastPayload = (result?.payload ?? {}) as {
|
|
813
|
+
error?: string;
|
|
814
|
+
code?: number | string;
|
|
815
|
+
};
|
|
816
|
+
const { code } = lastPayload;
|
|
817
|
+
const isPinInvalid = code === HW_ERR_PIN_INVALID;
|
|
818
|
+
if (!isPinInvalid || attempt >= maxAttempts) {
|
|
819
|
+
// PinCancelled, other error, or PinInvalid on the last attempt — bail out
|
|
820
|
+
const err = new Error(`Device unlock failed: ${lastPayload.error ?? 'unknown error'}`);
|
|
821
|
+
(err as Error & { code?: number | string }).code = code ?? 'UNLOCK_FAILED';
|
|
822
|
+
throw err;
|
|
823
|
+
}
|
|
824
|
+
process.stderr.write(
|
|
825
|
+
`[onekey-hw] Invalid PIN. Retry on your device (attempt ${attempt + 1}/${maxAttempts}).\n`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
// Unreachable — the loop either returns or throws
|
|
829
|
+
throw new Error('Device unlock failed: retry loop exited without a result');
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Prepare passphrase session before SDK calls.
|
|
834
|
+
*
|
|
835
|
+
* 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
|
|
836
|
+
* 2. Try keychain → preloadSessionCache → use cached session
|
|
837
|
+
* 3. Keychain miss → getPassphraseState (triggers 1/2/3 prompt) → save to keychain
|
|
838
|
+
*
|
|
839
|
+
* After this, globalOpts.passphraseState is set and getCommonParams will include it.
|
|
840
|
+
*/
|
|
841
|
+
async function prepareSession(
|
|
842
|
+
sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default,
|
|
843
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
844
|
+
globalOpts: Record<string, any>
|
|
845
|
+
): Promise<string | undefined> {
|
|
846
|
+
// Skip if standard wallet or passphraseState already provided
|
|
847
|
+
if (globalOpts.useEmptyPassphrase || globalOpts.passphraseState) {
|
|
848
|
+
return globalOpts.passphraseState;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Errors from the SDK calls below (PIN cancelled, transport broken,
|
|
852
|
+
// getPassphraseState rejection) intentionally propagate to runCommand's
|
|
853
|
+
// catch block, which renders them as structured `{ success: false,
|
|
854
|
+
// payload: { error, code } }` output instead of silently falling through
|
|
855
|
+
// to a confusing downstream error 112 / 114.
|
|
856
|
+
|
|
857
|
+
// ── Step 1: Discover device ──────────────────────────────────────
|
|
858
|
+
const searchResult = await sdk.searchDevices();
|
|
859
|
+
if (
|
|
860
|
+
!searchResult?.success ||
|
|
861
|
+
!Array.isArray(searchResult.payload) ||
|
|
862
|
+
searchResult.payload.length === 0
|
|
863
|
+
) {
|
|
864
|
+
return undefined;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const device = searchResult.payload[0] as {
|
|
868
|
+
connectId?: string;
|
|
869
|
+
deviceId?: string;
|
|
870
|
+
features?: {
|
|
871
|
+
device_id?: string;
|
|
872
|
+
session_id?: string;
|
|
873
|
+
passphrase_protection?: boolean | null;
|
|
874
|
+
unlocked?: boolean | null;
|
|
875
|
+
};
|
|
709
876
|
};
|
|
877
|
+
const connectId = device.connectId || globalOpts.connectId || '';
|
|
878
|
+
if (!globalOpts.connectId && connectId) {
|
|
879
|
+
globalOpts.connectId = connectId;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ── Step 2: Get features if searchDevices didn't populate them ──
|
|
883
|
+
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
884
|
+
// which will fail with a clearer error if the device is truly unreachable.
|
|
885
|
+
let deviceId = device.features?.device_id || device.deviceId || '';
|
|
886
|
+
let unlocked = device.features?.unlocked;
|
|
887
|
+
let passphraseProtection = device.features?.passphrase_protection;
|
|
888
|
+
|
|
889
|
+
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
890
|
+
try {
|
|
891
|
+
const featResult = await sdk.getFeatures(connectId);
|
|
892
|
+
if (featResult?.success && featResult.payload) {
|
|
893
|
+
deviceId = featResult.payload.device_id || deviceId;
|
|
894
|
+
unlocked = featResult.payload.unlocked;
|
|
895
|
+
passphraseProtection = featResult.payload.passphrase_protection;
|
|
896
|
+
}
|
|
897
|
+
} catch {
|
|
898
|
+
/* non-fatal — Step 3 will surface a clear error if device is gone */
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// ── Step 3: Unlock if locked (matches app-monorepo ServiceHardware flow) ──
|
|
903
|
+
// Track whether device was locked — locking invalidates passphrase sessions,
|
|
904
|
+
// so keychain session reuse is only possible if device was already unlocked.
|
|
905
|
+
// PinInvalid retries inside unlockWithRetry; PinCancelled/other failures
|
|
906
|
+
// throw and propagate to runCommand's catch for structured output.
|
|
907
|
+
const wasLocked = unlocked === false;
|
|
908
|
+
if (wasLocked) {
|
|
909
|
+
process.stderr.write('[onekey-hw] Device is locked. Unlocking (PIN required)...\n');
|
|
910
|
+
const { payload: feat } = await unlockWithRetry(sdk, connectId);
|
|
911
|
+
deviceId = feat.device_id || deviceId;
|
|
912
|
+
unlocked = feat.unlocked;
|
|
913
|
+
passphraseProtection = feat.passphrase_protection;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (!globalOpts.deviceId && deviceId) {
|
|
917
|
+
globalOpts.deviceId = deviceId;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ── Step 4: Check passphrase protection ──────────────────────────
|
|
921
|
+
if (passphraseProtection === false) {
|
|
922
|
+
return undefined;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ── Step 5: Try keychain session reuse ───────────────────────────
|
|
926
|
+
// Only attempt if device was already unlocked — locking invalidates
|
|
927
|
+
// all passphrase sessions, so cached session_id is useless after unlock.
|
|
928
|
+
if (!wasLocked && deviceId) {
|
|
929
|
+
const cached = await preloadSessionFromKeychain(deviceId);
|
|
930
|
+
if (cached) {
|
|
931
|
+
globalOpts.passphraseState = cached;
|
|
932
|
+
return cached;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// ── Step 6: Keychain miss → getPassphraseState (triggers 1/2/3 prompt) ──
|
|
937
|
+
const psResult = await sdk.getPassphraseState(connectId, {
|
|
938
|
+
initSession: true,
|
|
939
|
+
useEmptyPassphrase: false,
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
if (psResult.success && psResult.payload) {
|
|
943
|
+
const passphraseState = psResult.payload;
|
|
944
|
+
globalOpts.passphraseState = passphraseState;
|
|
945
|
+
|
|
946
|
+
// Save session to keychain for next invocation.
|
|
947
|
+
//
|
|
948
|
+
// Pass passphraseState to keep connectStateChange=false — otherwise
|
|
949
|
+
// Initialize would be re-run without passphrase_state, resetting the
|
|
950
|
+
// device to the standard wallet and returning a mismatched session_id.
|
|
951
|
+
// See the matching comment in `session connect`.
|
|
952
|
+
if (deviceId) {
|
|
953
|
+
const featAfter = await sdk.getFeatures(connectId, {
|
|
954
|
+
passphraseState,
|
|
955
|
+
skipPassphraseCheck: true,
|
|
956
|
+
});
|
|
957
|
+
const sessionId = featAfter?.success ? featAfter.payload?.session_id : undefined;
|
|
958
|
+
if (sessionId) {
|
|
959
|
+
await saveSessionToKeychain(deviceId, passphraseState, sessionId);
|
|
960
|
+
await preloadSessionFromKeychain(deviceId);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return passphraseState;
|
|
965
|
+
}
|
|
966
|
+
return undefined;
|
|
710
967
|
}
|
|
711
968
|
|
|
969
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
712
970
|
function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
713
|
-
// #10 FIX: Always use JSON.stringify to avoid [Object] truncation
|
|
714
971
|
console.log(JSON.stringify(result, null, 2));
|
|
972
|
+
if (
|
|
973
|
+
result &&
|
|
974
|
+
typeof result === 'object' &&
|
|
975
|
+
'success' in result &&
|
|
976
|
+
!(result as { success: boolean }).success
|
|
977
|
+
) {
|
|
978
|
+
process.exitCode = 1;
|
|
979
|
+
}
|
|
980
|
+
// No process.exit here — runCommand() below handles dispose + exit so SDK
|
|
981
|
+
// async cleanup (USB release, event listener teardown) finishes first.
|
|
982
|
+
}
|
|
715
983
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
984
|
+
/**
|
|
985
|
+
* Unified command runner. All `.action(...)` handlers should wrap their body
|
|
986
|
+
* in this helper so every command goes through the same lifecycle:
|
|
987
|
+
*
|
|
988
|
+
* 1. (optional) create SDK
|
|
989
|
+
* 2. (optional) prepareSession — unlock + keychain preload + passphraseState
|
|
990
|
+
* 3. run the handler (which calls outputResult on success)
|
|
991
|
+
* 4. report uncaught errors as a structured failure result
|
|
992
|
+
* 5. dispose SDK
|
|
993
|
+
* 6. drain event loop and process.exit with the right code
|
|
994
|
+
*
|
|
995
|
+
* This fixes three previous bugs:
|
|
996
|
+
* - Most signing commands skipped prepareSession, so keychain sessions
|
|
997
|
+
* were never actually reused.
|
|
998
|
+
* - outputResult scheduled process.exit(200ms) before dispose completed,
|
|
999
|
+
* racing USB release on slower transports.
|
|
1000
|
+
* - Errors thrown from SDK calls leaked as stack traces instead of the
|
|
1001
|
+
* CLI's structured `{ success: false, payload: { error } }` shape.
|
|
1002
|
+
*/
|
|
1003
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1004
|
+
type AnySdk = Awaited<ReturnType<typeof createSDK>>;
|
|
1005
|
+
|
|
1006
|
+
interface CommandContext {
|
|
1007
|
+
sdk: AnySdk;
|
|
1008
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1009
|
+
globalOpts: Record<string, any>;
|
|
1010
|
+
params: ReturnType<typeof getCommonParams>;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async function runCommand(
|
|
1014
|
+
options: { needsSession?: boolean },
|
|
1015
|
+
handler: (ctx: CommandContext) => Promise<void>
|
|
1016
|
+
): Promise<void> {
|
|
1017
|
+
const globalOpts = program.opts();
|
|
1018
|
+
try {
|
|
1019
|
+
const sdk = await createSDK(globalOpts);
|
|
1020
|
+
if (options.needsSession) {
|
|
1021
|
+
await prepareSession(sdk, globalOpts);
|
|
1022
|
+
}
|
|
1023
|
+
await handler({
|
|
1024
|
+
sdk,
|
|
1025
|
+
globalOpts,
|
|
1026
|
+
params: getCommonParams(globalOpts),
|
|
1027
|
+
});
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
const code = (err as Error & { code?: string }).code || 'COMMAND_FAILED';
|
|
1030
|
+
outputResult(globalOpts, {
|
|
1031
|
+
success: false,
|
|
1032
|
+
payload: {
|
|
1033
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1034
|
+
code,
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
} finally {
|
|
1038
|
+
// Singleton disposer — awaits the SDK, calls dispose, clears the
|
|
1039
|
+
// promise reference. Idempotent, safe to call even if init failed.
|
|
1040
|
+
await disposeSDK();
|
|
721
1041
|
}
|
|
1042
|
+
// SDK event listeners can keep the event loop alive after dispose.
|
|
1043
|
+
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
1044
|
+
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
1048
|
+
function respondAndExit(result: unknown): void {
|
|
1049
|
+
outputResult({}, result);
|
|
1050
|
+
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
722
1051
|
}
|
|
723
1052
|
|
|
724
1053
|
/**
|
|
725
|
-
*
|
|
1054
|
+
* Safe JSON.parse. Throws on failure — runCommand() catches and renders
|
|
1055
|
+
* the structured error so the SDK still gets disposed cleanly.
|
|
726
1056
|
*/
|
|
727
1057
|
function safeJsonParse(input: string, label: string): unknown {
|
|
728
1058
|
try {
|
|
729
1059
|
return JSON.parse(input);
|
|
730
1060
|
} catch {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
payload: {
|
|
735
|
-
error: `Invalid JSON for ${label}: ${input.slice(0, 100)}`,
|
|
736
|
-
code: 'INVALID_JSON',
|
|
737
|
-
},
|
|
738
|
-
})
|
|
739
|
-
);
|
|
740
|
-
process.exit(1);
|
|
1061
|
+
const err = new Error(`Invalid JSON for ${label}: ${input.slice(0, 100)}`);
|
|
1062
|
+
(err as Error & { code?: string }).code = 'INVALID_JSON';
|
|
1063
|
+
throw err;
|
|
741
1064
|
}
|
|
742
1065
|
}
|
|
743
1066
|
|