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