@dfinity/hardware-wallet-cli 0.2.0 → 0.2.1
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/build/index.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
/**
|
|
8
|
+
* A CLI tool for testing the Ledger hardware wallet integration.
|
|
9
|
+
*/
|
|
10
|
+
const commander_1 = require("commander");
|
|
11
|
+
const identity_1 = require("./src/ledger/identity");
|
|
12
|
+
const nns_1 = require("@dfinity/nns");
|
|
13
|
+
const principal_1 = require("@dfinity/principal");
|
|
14
|
+
const agent_1 = require("@dfinity/agent");
|
|
15
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
16
|
+
// Add polyfill for `window` for `TransportWebHID` checks to work.
|
|
17
|
+
require("node-window-polyfill/register");
|
|
18
|
+
// Add polyfill for `window.fetch` for agent-js to work.
|
|
19
|
+
// @ts-ignore (no types are available)
|
|
20
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
21
|
+
global.fetch = node_fetch_1.default;
|
|
22
|
+
window.fetch = node_fetch_1.default;
|
|
23
|
+
const program = new commander_1.Command();
|
|
24
|
+
const log = console.log;
|
|
25
|
+
const SECONDS_PER_MINUTE = 60;
|
|
26
|
+
const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
|
|
27
|
+
const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
|
|
28
|
+
const SECONDS_PER_YEAR = 365 * SECONDS_PER_DAY + 6 * SECONDS_PER_HOUR;
|
|
29
|
+
async function getAgent(identity) {
|
|
30
|
+
const network = program.opts().network;
|
|
31
|
+
// Only fetch the rootkey if the network isn't mainnet.
|
|
32
|
+
const fetchRootKey = new URL(network).host == "ic0.app" ? false : true;
|
|
33
|
+
const agent = new agent_1.HttpAgent({
|
|
34
|
+
host: program.opts().network,
|
|
35
|
+
identity: identity,
|
|
36
|
+
});
|
|
37
|
+
if (fetchRootKey) {
|
|
38
|
+
await agent.fetchRootKey();
|
|
39
|
+
}
|
|
40
|
+
return agent;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Fetches the balance of the main account on the wallet.
|
|
44
|
+
*/
|
|
45
|
+
async function getBalance() {
|
|
46
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
47
|
+
const accountIdentifier = nns_1.AccountIdentifier.fromPrincipal({
|
|
48
|
+
principal: identity.getPrincipal(),
|
|
49
|
+
});
|
|
50
|
+
const ledger = nns_1.LedgerCanister.create({
|
|
51
|
+
agent: await getAgent(new agent_1.AnonymousIdentity()),
|
|
52
|
+
});
|
|
53
|
+
const balance = await ledger.accountBalance({
|
|
54
|
+
accountIdentifier: accountIdentifier,
|
|
55
|
+
});
|
|
56
|
+
ok(`Account ${accountIdentifier.toHex()} has balance ${balance.toE8s()} e8s`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Send ICP to another address.
|
|
60
|
+
*
|
|
61
|
+
* @param to The account identifier in hex.
|
|
62
|
+
* @param amount Amount to send in e8s.
|
|
63
|
+
*/
|
|
64
|
+
async function sendICP(to, amount) {
|
|
65
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
66
|
+
const ledger = nns_1.LedgerCanister.create({
|
|
67
|
+
agent: await getAgent(identity),
|
|
68
|
+
});
|
|
69
|
+
const blockHeight = await ledger.transfer({
|
|
70
|
+
to: to,
|
|
71
|
+
amount: amount,
|
|
72
|
+
memo: BigInt(0),
|
|
73
|
+
});
|
|
74
|
+
ok(`Transaction completed at block height ${blockHeight}.`);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Shows the principal and account idenifier on the terminal and on the wallet's screen.
|
|
78
|
+
*/
|
|
79
|
+
async function showInfo(showOnDevice) {
|
|
80
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
81
|
+
const accountIdentifier = nns_1.AccountIdentifier.fromPrincipal({
|
|
82
|
+
principal: identity.getPrincipal(),
|
|
83
|
+
});
|
|
84
|
+
log(chalk_1.default.bold(`Principal: `) + identity.getPrincipal());
|
|
85
|
+
log(chalk_1.default.bold(`Address (${identity.derivePath}): `) + accountIdentifier.toHex());
|
|
86
|
+
if (showOnDevice) {
|
|
87
|
+
log("Displaying the principal and the address on the device...");
|
|
88
|
+
await identity.showAddressAndPubKeyOnDevice();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Stakes a new neuron.
|
|
93
|
+
*
|
|
94
|
+
* @param amount Amount to stake in e8s.
|
|
95
|
+
*/
|
|
96
|
+
async function stakeNeuron(stake) {
|
|
97
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
98
|
+
const ledger = nns_1.LedgerCanister.create({
|
|
99
|
+
agent: await getAgent(identity),
|
|
100
|
+
});
|
|
101
|
+
const governance = nns_1.GovernanceCanister.create({
|
|
102
|
+
agent: await getAgent(new agent_1.AnonymousIdentity()),
|
|
103
|
+
hardwareWallet: true,
|
|
104
|
+
});
|
|
105
|
+
// Flag that an upcoming stake neuron transaction is coming to distinguish
|
|
106
|
+
// it from a "send ICP" transaction on the device.
|
|
107
|
+
identity.flagUpcomingStakeNeuron();
|
|
108
|
+
try {
|
|
109
|
+
const stakedNeuronId = await governance.stakeNeuron({
|
|
110
|
+
stake: stake,
|
|
111
|
+
principal: identity.getPrincipal(),
|
|
112
|
+
ledgerCanister: ledger,
|
|
113
|
+
});
|
|
114
|
+
ok(`Staked neuron with ID: ${stakedNeuronId}`);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error instanceof nns_1.InsufficientAmountError) {
|
|
118
|
+
err(`Cannot stake less than ${error.minimumAmount.toE8s()} e8s`);
|
|
119
|
+
}
|
|
120
|
+
else if (error instanceof nns_1.InsufficientFundsError) {
|
|
121
|
+
err(`Your account has insufficient funds (${error.balance.toE8s()} e8s)`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function increaseDissolveDelay(neuronId, years, days, minutes, seconds) {
|
|
129
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
130
|
+
const governance = nns_1.GovernanceCanister.create({
|
|
131
|
+
agent: await getAgent(identity),
|
|
132
|
+
hardwareWallet: true,
|
|
133
|
+
});
|
|
134
|
+
const additionalDissolveDelaySeconds = years * SECONDS_PER_YEAR +
|
|
135
|
+
days * SECONDS_PER_DAY +
|
|
136
|
+
minutes * SECONDS_PER_MINUTE +
|
|
137
|
+
seconds;
|
|
138
|
+
await governance.increaseDissolveDelay({
|
|
139
|
+
neuronId: neuronId,
|
|
140
|
+
additionalDissolveDelaySeconds: additionalDissolveDelaySeconds,
|
|
141
|
+
});
|
|
142
|
+
ok();
|
|
143
|
+
}
|
|
144
|
+
async function disburseNeuron(neuronId, to, amount) {
|
|
145
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
146
|
+
const governance = nns_1.GovernanceCanister.create({
|
|
147
|
+
agent: await getAgent(identity),
|
|
148
|
+
hardwareWallet: true,
|
|
149
|
+
});
|
|
150
|
+
await governance.disburse({
|
|
151
|
+
neuronId: BigInt(neuronId),
|
|
152
|
+
toAccountId: to,
|
|
153
|
+
amount: amount,
|
|
154
|
+
});
|
|
155
|
+
ok();
|
|
156
|
+
}
|
|
157
|
+
async function spawnNeuron(neuronId, controller) {
|
|
158
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
159
|
+
const governance = nns_1.GovernanceCanister.create({
|
|
160
|
+
agent: await getAgent(identity),
|
|
161
|
+
hardwareWallet: true,
|
|
162
|
+
});
|
|
163
|
+
const spawnedNeuronId = await governance.spawnNeuron({
|
|
164
|
+
neuronId: BigInt(neuronId),
|
|
165
|
+
newController: controller,
|
|
166
|
+
});
|
|
167
|
+
ok(`Spawned neuron with ID ${spawnedNeuronId}`);
|
|
168
|
+
}
|
|
169
|
+
async function startDissolving(neuronId) {
|
|
170
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
171
|
+
const governance = nns_1.GovernanceCanister.create({
|
|
172
|
+
agent: await getAgent(identity),
|
|
173
|
+
hardwareWallet: true,
|
|
174
|
+
});
|
|
175
|
+
await governance.startDissolving(neuronId);
|
|
176
|
+
ok();
|
|
177
|
+
}
|
|
178
|
+
async function stopDissolving(neuronId) {
|
|
179
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
180
|
+
const governance = nns_1.GovernanceCanister.create({
|
|
181
|
+
agent: await getAgent(identity),
|
|
182
|
+
hardwareWallet: true,
|
|
183
|
+
});
|
|
184
|
+
await governance.stopDissolving(neuronId);
|
|
185
|
+
ok();
|
|
186
|
+
}
|
|
187
|
+
async function addHotkey(neuronId, principal) {
|
|
188
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
189
|
+
const governance = nns_1.GovernanceCanister.create({
|
|
190
|
+
agent: await getAgent(identity),
|
|
191
|
+
hardwareWallet: true,
|
|
192
|
+
});
|
|
193
|
+
await governance.addHotkey({
|
|
194
|
+
neuronId: BigInt(neuronId),
|
|
195
|
+
principal: principal,
|
|
196
|
+
});
|
|
197
|
+
ok();
|
|
198
|
+
}
|
|
199
|
+
async function removeHotkey(neuronId, principal) {
|
|
200
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
201
|
+
const governance = nns_1.GovernanceCanister.create({
|
|
202
|
+
agent: await getAgent(identity),
|
|
203
|
+
hardwareWallet: true,
|
|
204
|
+
});
|
|
205
|
+
await governance.removeHotkey({
|
|
206
|
+
neuronId: BigInt(neuronId),
|
|
207
|
+
principal: principal,
|
|
208
|
+
});
|
|
209
|
+
ok();
|
|
210
|
+
}
|
|
211
|
+
async function listNeurons() {
|
|
212
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
213
|
+
const governance = nns_1.GovernanceCanister.create({
|
|
214
|
+
agent: await getAgent(identity),
|
|
215
|
+
hardwareWallet: true,
|
|
216
|
+
});
|
|
217
|
+
// We filter neurons with no ICP, as they'll be garbage collected by the governance canister.
|
|
218
|
+
const neurons = await governance.listNeurons({
|
|
219
|
+
certified: true,
|
|
220
|
+
});
|
|
221
|
+
if (neurons.length > 0) {
|
|
222
|
+
neurons.forEach((n) => {
|
|
223
|
+
log(`Neuron ID: ${n.neuronId}`);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
ok("No neurons found.");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const buf2hex = (buffer) => {
|
|
231
|
+
return [...new Uint8Array(buffer)]
|
|
232
|
+
.map((x) => x.toString(16).padStart(2, "0"))
|
|
233
|
+
.join("");
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Fetches the balance of the main account on the wallet.
|
|
237
|
+
*/
|
|
238
|
+
async function claimNeurons() {
|
|
239
|
+
const identity = await identity_1.LedgerIdentity.create();
|
|
240
|
+
const bufferKey = identity.getPublicKey();
|
|
241
|
+
const hexPubKey = buf2hex(bufferKey.toRaw());
|
|
242
|
+
const isHex = hexPubKey.match("^[0-9a-fA-F]+$");
|
|
243
|
+
if (!isHex) {
|
|
244
|
+
throw new Error(`${hexPubKey} is not a hex string.`);
|
|
245
|
+
}
|
|
246
|
+
if (hexPubKey.length < 130 || hexPubKey.length > 150) {
|
|
247
|
+
throw new Error(`The key must be >= 130 characters and <= 150 characters.`);
|
|
248
|
+
}
|
|
249
|
+
const governance = await nns_1.GenesisTokenCanister.create({
|
|
250
|
+
agent: await getAgent(identity),
|
|
251
|
+
});
|
|
252
|
+
const claimedNeuronIds = await governance.claimNeurons({
|
|
253
|
+
hexPubKey: hexPubKey,
|
|
254
|
+
});
|
|
255
|
+
ok(`Successfully claimed the following neurons: ${claimedNeuronIds}`);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Runs a function with a try/catch block.
|
|
259
|
+
*/
|
|
260
|
+
async function run(f) {
|
|
261
|
+
try {
|
|
262
|
+
await f();
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
err(error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function ok(message) {
|
|
269
|
+
if (message) {
|
|
270
|
+
log(`${chalk_1.default.green(chalk_1.default.bold("OK"))}: ${message}`);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
log(`${chalk_1.default.green(chalk_1.default.bold("OK"))}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function err(error) {
|
|
277
|
+
const message = error instanceof nns_1.GovernanceError
|
|
278
|
+
? error.detail.error_message
|
|
279
|
+
: error instanceof Error
|
|
280
|
+
? error.message
|
|
281
|
+
: error;
|
|
282
|
+
log(`${chalk_1.default.bold(chalk_1.default.red("Error:"))} ${message}`);
|
|
283
|
+
}
|
|
284
|
+
function tryParseInt(value) {
|
|
285
|
+
const parsedValue = parseInt(value, 10);
|
|
286
|
+
if (isNaN(parsedValue)) {
|
|
287
|
+
throw new commander_1.InvalidArgumentError("Not a number.");
|
|
288
|
+
}
|
|
289
|
+
return parsedValue;
|
|
290
|
+
}
|
|
291
|
+
function tryParseBigInt(value) {
|
|
292
|
+
try {
|
|
293
|
+
return BigInt(value);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
throw new commander_1.InvalidArgumentError(err.toString());
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function tryParsePrincipal(value) {
|
|
300
|
+
try {
|
|
301
|
+
return principal_1.Principal.fromText(value);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
throw new commander_1.InvalidArgumentError(err.toString());
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function tryParseE8s(e8s) {
|
|
308
|
+
try {
|
|
309
|
+
return nns_1.ICP.fromE8s(tryParseBigInt(e8s));
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
throw new commander_1.InvalidArgumentError(err.toString());
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function tryParseAccountIdentifier(accountIdentifier) {
|
|
316
|
+
try {
|
|
317
|
+
return nns_1.AccountIdentifier.fromHex(accountIdentifier);
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
throw new commander_1.InvalidArgumentError(err.toString());
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function main() {
|
|
324
|
+
const neuron = new commander_1.Command("neuron")
|
|
325
|
+
.description("Commands for managing neurons.")
|
|
326
|
+
.showSuggestionAfterError()
|
|
327
|
+
.addCommand(new commander_1.Command("stake")
|
|
328
|
+
.requiredOption("--amount <amount>", "Amount to stake in e8s.", tryParseE8s)
|
|
329
|
+
.action((args) => run(() => stakeNeuron(args.amount))))
|
|
330
|
+
.addCommand(new commander_1.Command("increase-dissolve-delay")
|
|
331
|
+
.requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
|
|
332
|
+
.option("--years <years>", "Number of years", tryParseInt)
|
|
333
|
+
.option("--days <days>", "Number of days", tryParseInt)
|
|
334
|
+
.option("--minutes <minutes>", "Number of minutes", tryParseInt)
|
|
335
|
+
.option("--seconds <seconds>", "Number of seconds", tryParseInt)
|
|
336
|
+
.action((args) => run(() => increaseDissolveDelay(args.neuronId, args.years || 0, args.days || 0, args.minutes || 0, args.seconds || 0))))
|
|
337
|
+
.addCommand(new commander_1.Command("disburse")
|
|
338
|
+
.requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
|
|
339
|
+
.option("--to <account-identifier>")
|
|
340
|
+
.option("--amount <amount>", "Amount to disburse (empty to disburse all)", tryParseBigInt)
|
|
341
|
+
.action((args) => {
|
|
342
|
+
run(() => disburseNeuron(args.neuronId, args.to, args.amount));
|
|
343
|
+
}))
|
|
344
|
+
.addCommand(new commander_1.Command("spawn")
|
|
345
|
+
.requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
|
|
346
|
+
.option("--controller <new-controller>", "Controller", tryParsePrincipal)
|
|
347
|
+
.action((args) => {
|
|
348
|
+
run(() => spawnNeuron(args.neuronId, args.controller));
|
|
349
|
+
}))
|
|
350
|
+
.addCommand(new commander_1.Command("start-dissolving")
|
|
351
|
+
.requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
|
|
352
|
+
.action((args) => {
|
|
353
|
+
run(() => startDissolving(args.neuronId));
|
|
354
|
+
}))
|
|
355
|
+
.addCommand(new commander_1.Command("stop-dissolving")
|
|
356
|
+
.requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
|
|
357
|
+
.action((args) => {
|
|
358
|
+
run(() => stopDissolving(args.neuronId));
|
|
359
|
+
}))
|
|
360
|
+
.addCommand(new commander_1.Command("list").action(() => run(listNeurons)))
|
|
361
|
+
.addCommand(new commander_1.Command("add-hotkey")
|
|
362
|
+
.requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
|
|
363
|
+
.requiredOption("--principal <principal>", "Principal", tryParsePrincipal)
|
|
364
|
+
.action((args) => run(() => addHotkey(args.neuronId, args.principal))))
|
|
365
|
+
.addCommand(new commander_1.Command("remove-hotkey")
|
|
366
|
+
.requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
|
|
367
|
+
.requiredOption("--principal <principal>", "Principal", tryParsePrincipal)
|
|
368
|
+
.action((args) => run(() => removeHotkey(args.neuronId, args.principal))))
|
|
369
|
+
.addCommand(new commander_1.Command("claim")
|
|
370
|
+
.description("Claim the caller's GTC neurons.")
|
|
371
|
+
.action((args) => run(() => claimNeurons())));
|
|
372
|
+
const icp = new commander_1.Command("icp")
|
|
373
|
+
.description("Commands for managing ICP.")
|
|
374
|
+
.showSuggestionAfterError()
|
|
375
|
+
.addCommand(new commander_1.Command("balance")
|
|
376
|
+
.description("Fetch current balance.")
|
|
377
|
+
.action(() => {
|
|
378
|
+
run(getBalance);
|
|
379
|
+
}))
|
|
380
|
+
.addCommand(new commander_1.Command("transfer")
|
|
381
|
+
.requiredOption("--to <account-identifier>", "Account identifier to transfer to.", tryParseAccountIdentifier)
|
|
382
|
+
.requiredOption("--amount <amount>", "Amount to transfer in e8s.", tryParseE8s)
|
|
383
|
+
.action((args) => run(() => sendICP(args.to, args.amount))));
|
|
384
|
+
program
|
|
385
|
+
.description("A CLI for the Ledger hardware wallet.")
|
|
386
|
+
.enablePositionalOptions()
|
|
387
|
+
.showSuggestionAfterError()
|
|
388
|
+
.addOption(new commander_1.Option("--network <network>", "The IC network to talk to.")
|
|
389
|
+
.default("https://ic0.app")
|
|
390
|
+
.env("IC_NETWORK"))
|
|
391
|
+
.addCommand(new commander_1.Command("info")
|
|
392
|
+
.option("-n --no-show-on-device")
|
|
393
|
+
.description("Show the wallet's principal, address, and balance.")
|
|
394
|
+
.action((args) => {
|
|
395
|
+
run(() => showInfo(args.showOnDevice));
|
|
396
|
+
}))
|
|
397
|
+
.addCommand(icp)
|
|
398
|
+
.addCommand(neuron);
|
|
399
|
+
await program.parseAsync(process.argv);
|
|
400
|
+
}
|
|
401
|
+
main();
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.LedgerIdentity = void 0;
|
|
30
|
+
const agent_1 = require("@dfinity/agent");
|
|
31
|
+
const principal_1 = require("@dfinity/principal");
|
|
32
|
+
const ledger_icp_1 = __importStar(require("@zondax/ledger-icp"));
|
|
33
|
+
const secp256k1_1 = require("./secp256k1");
|
|
34
|
+
// @ts-ignore (no types are available)
|
|
35
|
+
const hw_transport_webhid_1 = __importDefault(require("@ledgerhq/hw-transport-webhid"));
|
|
36
|
+
const hw_transport_node_hid_noevents_1 = __importDefault(require("@ledgerhq/hw-transport-node-hid-noevents"));
|
|
37
|
+
// Add polyfill for `window.fetch` for agent-js to work.
|
|
38
|
+
// @ts-ignore (no types are available)
|
|
39
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
40
|
+
global.fetch = node_fetch_1.default;
|
|
41
|
+
/**
|
|
42
|
+
* Convert the HttpAgentRequest body into cbor which can be signed by the Ledger Hardware Wallet.
|
|
43
|
+
* @param request - body of the HttpAgentRequest
|
|
44
|
+
*/
|
|
45
|
+
function _prepareCborForLedger(request) {
|
|
46
|
+
return agent_1.Cbor.encode({ content: request });
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A Hardware Ledger Internet Computer Agent identity.
|
|
50
|
+
*/
|
|
51
|
+
class LedgerIdentity extends agent_1.SignIdentity {
|
|
52
|
+
constructor(derivePath, _publicKey) {
|
|
53
|
+
super();
|
|
54
|
+
this.derivePath = derivePath;
|
|
55
|
+
this._publicKey = _publicKey;
|
|
56
|
+
// A flag to signal that the next transaction to be signed will be
|
|
57
|
+
// a "stake neuron" transaction.
|
|
58
|
+
this._neuronStakeFlag = false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create a LedgerIdentity using the Web USB transport.
|
|
62
|
+
* @param derivePath The derivation path.
|
|
63
|
+
*/
|
|
64
|
+
static async create(derivePath = `m/44'/223'/0'/0/0`) {
|
|
65
|
+
const [app, transport] = await this._connect();
|
|
66
|
+
try {
|
|
67
|
+
const publicKey = await this._fetchPublicKeyFromDevice(app, derivePath);
|
|
68
|
+
return new this(derivePath, publicKey);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
// Always close the transport.
|
|
72
|
+
transport.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Connect to a ledger hardware wallet.
|
|
77
|
+
*/
|
|
78
|
+
static async _connect() {
|
|
79
|
+
async function getTransport() {
|
|
80
|
+
if (await hw_transport_webhid_1.default.isSupported()) {
|
|
81
|
+
// We're in a web browser.
|
|
82
|
+
return hw_transport_webhid_1.default.create();
|
|
83
|
+
}
|
|
84
|
+
else if (await hw_transport_node_hid_noevents_1.default.isSupported()) {
|
|
85
|
+
// Maybe we're in a CLI.
|
|
86
|
+
return hw_transport_node_hid_noevents_1.default.create();
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Unknown environment.
|
|
90
|
+
throw Error();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const transport = await getTransport();
|
|
95
|
+
const app = new ledger_icp_1.default(transport);
|
|
96
|
+
return [app, transport];
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// @ts-ignore
|
|
100
|
+
if (err.id && err.id == "NoDeviceFound") {
|
|
101
|
+
throw "No Ledger device found. Is the wallet connected and unlocked?";
|
|
102
|
+
}
|
|
103
|
+
else if (
|
|
104
|
+
// @ts-ignore
|
|
105
|
+
err.message &&
|
|
106
|
+
// @ts-ignore
|
|
107
|
+
err.message.includes("cannot open device with path")) {
|
|
108
|
+
throw "Cannot connect to Ledger device. Please close all other wallet applications (e.g. Ledger Live) and try again.";
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Unsupported browser. Data on browser compatibility is taken from https://caniuse.com/webhid
|
|
112
|
+
throw `Cannot connect to Ledger Wallet. Either you have other wallet applications open (e.g. Ledger Live), or your browser doesn't support WebHID, which is necessary to communicate with your Ledger hardware wallet.\n\nSupported browsers:\n* Chrome (Desktop) v89+\n* Edge v89+\n* Opera v76+\n\nError: ${err}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
static async _fetchPublicKeyFromDevice(app, derivePath) {
|
|
117
|
+
const resp = await app.getAddressAndPubKey(derivePath);
|
|
118
|
+
// @ts-ignore
|
|
119
|
+
if (resp.returnCode == 28161) {
|
|
120
|
+
throw "Please open the Internet Computer app on your wallet and try again.";
|
|
121
|
+
}
|
|
122
|
+
else if (resp.returnCode == ledger_icp_1.LedgerError.TransactionRejected) {
|
|
123
|
+
throw "Ledger Wallet is locked. Unlock it and try again.";
|
|
124
|
+
// @ts-ignore
|
|
125
|
+
}
|
|
126
|
+
else if (resp.returnCode == 65535) {
|
|
127
|
+
throw "Unable to fetch the public key. Please try again.";
|
|
128
|
+
}
|
|
129
|
+
// This type doesn't have the right fields in it, so we have to manually type it.
|
|
130
|
+
const principal = resp
|
|
131
|
+
.principalText;
|
|
132
|
+
const publicKey = secp256k1_1.Secp256k1PublicKey.fromRaw(new Uint8Array(resp.publicKey));
|
|
133
|
+
if (principal !==
|
|
134
|
+
principal_1.Principal.selfAuthenticating(new Uint8Array(publicKey.toDer())).toText()) {
|
|
135
|
+
throw new Error("Principal returned by device does not match public key.");
|
|
136
|
+
}
|
|
137
|
+
return publicKey;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Required by Ledger.com that the user should be able to press a Button in UI
|
|
141
|
+
* and verify the address/pubkey are the same as on the device screen.
|
|
142
|
+
*/
|
|
143
|
+
async showAddressAndPubKeyOnDevice() {
|
|
144
|
+
this._executeWithApp(async (app) => {
|
|
145
|
+
await app.showAddressAndPubKey(this.derivePath);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* @returns The verion of the `Internet Computer' app installed on the Ledger device.
|
|
150
|
+
*/
|
|
151
|
+
async getVersion() {
|
|
152
|
+
return this._executeWithApp(async (app) => {
|
|
153
|
+
const res = await app.getVersion();
|
|
154
|
+
return {
|
|
155
|
+
major: res.major,
|
|
156
|
+
minor: res.minor,
|
|
157
|
+
patch: res.patch,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
getPublicKey() {
|
|
162
|
+
return this._publicKey;
|
|
163
|
+
}
|
|
164
|
+
async sign(blob) {
|
|
165
|
+
return await this._executeWithApp(async (app) => {
|
|
166
|
+
const resp = await app.sign(this.derivePath, Buffer.from(blob), this._neuronStakeFlag ? 1 : 0);
|
|
167
|
+
// Remove the "neuron stake" flag, since we already signed the transaction.
|
|
168
|
+
this._neuronStakeFlag = false;
|
|
169
|
+
const signatureRS = resp.signatureRS;
|
|
170
|
+
if (!signatureRS) {
|
|
171
|
+
throw new Error(`A ledger error happened during signature:\n` +
|
|
172
|
+
`Code: ${resp.returnCode}\n` +
|
|
173
|
+
`Message: ${JSON.stringify(resp.errorMessage)}\n`);
|
|
174
|
+
}
|
|
175
|
+
if (signatureRS?.byteLength !== 64) {
|
|
176
|
+
throw new Error(`Signature must be 64 bytes long (is ${signatureRS.length})`);
|
|
177
|
+
}
|
|
178
|
+
return bufferToArrayBuffer(signatureRS);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Signals that the upcoming transaction to be signed will be a "stake neuron" transaction.
|
|
183
|
+
*/
|
|
184
|
+
flagUpcomingStakeNeuron() {
|
|
185
|
+
this._neuronStakeFlag = true;
|
|
186
|
+
}
|
|
187
|
+
async transformRequest(request) {
|
|
188
|
+
const { body, ...fields } = request;
|
|
189
|
+
const signature = await this.sign(_prepareCborForLedger(body));
|
|
190
|
+
return {
|
|
191
|
+
...fields,
|
|
192
|
+
body: {
|
|
193
|
+
content: body,
|
|
194
|
+
sender_pubkey: this._publicKey.toDer(),
|
|
195
|
+
sender_sig: signature,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async _executeWithApp(func) {
|
|
200
|
+
const [app, transport] = await LedgerIdentity._connect();
|
|
201
|
+
try {
|
|
202
|
+
// Verify that the public key of the device matches the public key of this identity.
|
|
203
|
+
const devicePublicKey = await LedgerIdentity._fetchPublicKeyFromDevice(app, this.derivePath);
|
|
204
|
+
if (JSON.stringify(devicePublicKey) !== JSON.stringify(this._publicKey)) {
|
|
205
|
+
throw new Error("Found unexpected public key. Are you sure you're using the right wallet?");
|
|
206
|
+
}
|
|
207
|
+
// Run the provided function.
|
|
208
|
+
return await func(app);
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
transport.close();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
exports.LedgerIdentity = LedgerIdentity;
|
|
216
|
+
function bufferToArrayBuffer(buffer) {
|
|
217
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
218
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Secp256k1PublicKey = void 0;
|
|
4
|
+
function equals(b1, b2) {
|
|
5
|
+
if (b1.byteLength !== b2.byteLength) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const u1 = new Uint8Array(b1);
|
|
9
|
+
const u2 = new Uint8Array(b2);
|
|
10
|
+
for (let i = 0; i < u1.length; i++) {
|
|
11
|
+
if (u1[i] !== u2[i]) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
// This implementation is adjusted from the Ed25519PublicKey.
|
|
18
|
+
// The RAW_KEY_LENGTH and DER_PREFIX are modified accordingly
|
|
19
|
+
class Secp256k1PublicKey {
|
|
20
|
+
// `fromRaw` and `fromDer` should be used for instantiation, not this constructor.
|
|
21
|
+
constructor(key) {
|
|
22
|
+
this.rawKey = key;
|
|
23
|
+
this.derKey = Secp256k1PublicKey.derEncode(key);
|
|
24
|
+
}
|
|
25
|
+
static fromRaw(rawKey) {
|
|
26
|
+
return new Secp256k1PublicKey(rawKey);
|
|
27
|
+
}
|
|
28
|
+
static fromDer(derKey) {
|
|
29
|
+
return new Secp256k1PublicKey(this.derDecode(derKey));
|
|
30
|
+
}
|
|
31
|
+
static derEncode(publicKey) {
|
|
32
|
+
if (publicKey.byteLength !== Secp256k1PublicKey.RAW_KEY_LENGTH) {
|
|
33
|
+
const bl = publicKey.byteLength;
|
|
34
|
+
throw new TypeError(`secp256k1 public key must be ${Secp256k1PublicKey.RAW_KEY_LENGTH} bytes long (is ${bl})`);
|
|
35
|
+
}
|
|
36
|
+
const derPublicKey = Uint8Array.from([
|
|
37
|
+
...Secp256k1PublicKey.DER_PREFIX,
|
|
38
|
+
...new Uint8Array(publicKey),
|
|
39
|
+
]);
|
|
40
|
+
return derPublicKey.buffer;
|
|
41
|
+
}
|
|
42
|
+
static derDecode(key) {
|
|
43
|
+
const expectedLength = Secp256k1PublicKey.DER_PREFIX.length + Secp256k1PublicKey.RAW_KEY_LENGTH;
|
|
44
|
+
if (key.byteLength !== expectedLength) {
|
|
45
|
+
const bl = key.byteLength;
|
|
46
|
+
throw new TypeError(`secp256k1 DER-encoded public key must be ${expectedLength} bytes long (is ${bl})`);
|
|
47
|
+
}
|
|
48
|
+
const rawKey = key.slice(0, Secp256k1PublicKey.DER_PREFIX.length);
|
|
49
|
+
if (!equals(this.derEncode(rawKey), key)) {
|
|
50
|
+
throw new TypeError("secp256k1 DER-encoded public key is invalid. A valid secp256k1 DER-encoded public key " +
|
|
51
|
+
`must have the following prefix: ${Secp256k1PublicKey.DER_PREFIX}`);
|
|
52
|
+
}
|
|
53
|
+
return rawKey;
|
|
54
|
+
}
|
|
55
|
+
toDer() {
|
|
56
|
+
return this.derKey;
|
|
57
|
+
}
|
|
58
|
+
toRaw() {
|
|
59
|
+
return this.rawKey;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.Secp256k1PublicKey = Secp256k1PublicKey;
|
|
63
|
+
// The length of secp256k1 public keys is always 65 bytes.
|
|
64
|
+
Secp256k1PublicKey.RAW_KEY_LENGTH = 65;
|
|
65
|
+
// Adding this prefix to a raw public key is sufficient to DER-encode it.
|
|
66
|
+
// prettier-ignore
|
|
67
|
+
Secp256k1PublicKey.DER_PREFIX = Uint8Array.from([
|
|
68
|
+
0x30, 0x56,
|
|
69
|
+
0x30, 0x10,
|
|
70
|
+
0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01,
|
|
71
|
+
0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a,
|
|
72
|
+
0x03, 0x42,
|
|
73
|
+
0x00, // no padding
|
|
74
|
+
]);
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfinity/hardware-wallet-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A CLI to interact with the Internet Computer App on Ledger Nano S/X devices.",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"format": "prettier --write .",
|
|
8
8
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
9
|
"build": "tsc --build",
|
|
10
|
+
"prepack": "npm run build",
|
|
10
11
|
"clean": "tsc --build --clean",
|
|
11
12
|
"refresh": "rm -rf ./node_modules ./package-lock.json && npm install",
|
|
12
13
|
"execute": "ts-node ./index.ts"
|