@dfinity/hardware-wallet-cli 0.2.2 → 0.3.0-next-2023-08-28

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/index.ts DELETED
@@ -1,552 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * A CLI tool for testing the Ledger hardware wallet integration.
5
- */
6
- import { Command, Option, InvalidArgumentError } from "commander";
7
- import { LedgerIdentity } from "./src/ledger/identity";
8
- import {
9
- AccountIdentifier,
10
- LedgerCanister,
11
- GenesisTokenCanister,
12
- GovernanceCanister,
13
- GovernanceError,
14
- ICP,
15
- InsufficientAmountError,
16
- InsufficientFundsError,
17
- } from "@dfinity/nns";
18
- import { Principal } from "@dfinity/principal";
19
- import type { Secp256k1PublicKey } from "src/ledger/secp256k1";
20
- import { Agent, AnonymousIdentity, HttpAgent, Identity } from "@dfinity/agent";
21
- import chalk from "chalk";
22
-
23
- // Add polyfill for `window` for `TransportWebHID` checks to work.
24
- import "node-window-polyfill/register";
25
-
26
- // Add polyfill for `window.fetch` for agent-js to work.
27
- // @ts-ignore (no types are available)
28
- import fetch from "node-fetch";
29
-
30
- global.fetch = fetch;
31
- window.fetch = fetch;
32
-
33
- const program = new Command();
34
- const log = console.log;
35
-
36
- const SECONDS_PER_MINUTE = 60;
37
- const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
38
- const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
39
- const SECONDS_PER_YEAR = 365 * SECONDS_PER_DAY + 6 * SECONDS_PER_HOUR;
40
-
41
- async function getAgent(identity: Identity): Promise<Agent> {
42
- const network = program.opts().network;
43
-
44
- // Only fetch the rootkey if the network isn't mainnet.
45
- const fetchRootKey = new URL(network).host == "ic0.app" ? false : true;
46
-
47
- const agent = new HttpAgent({
48
- host: program.opts().network,
49
- identity: identity,
50
- });
51
-
52
- if (fetchRootKey) {
53
- await agent.fetchRootKey();
54
- }
55
-
56
- return agent;
57
- }
58
-
59
- async function getLedgerIdentity(): Promise<LedgerIdentity> {
60
- const principalPath = tryParseInt(program.opts().principal);
61
- if (principalPath < 0 || principalPath > 255) {
62
- throw new InvalidArgumentError(
63
- "Principal path must be between 0 and 255 inclusive."
64
- );
65
- }
66
- return LedgerIdentity.create(`m/44'/223'/0'/0/${principalPath}`);
67
- }
68
-
69
- /**
70
- * Fetches the balance of the main account on the wallet.
71
- */
72
- async function getBalance() {
73
- const identity = await getLedgerIdentity();
74
- const accountIdentifier = AccountIdentifier.fromPrincipal({
75
- principal: identity.getPrincipal(),
76
- });
77
-
78
- const ledger = LedgerCanister.create({
79
- agent: await getAgent(new AnonymousIdentity()),
80
- hardwareWallet: true,
81
- });
82
-
83
- const balance = await ledger.accountBalance({
84
- accountIdentifier: accountIdentifier,
85
- });
86
-
87
- ok(`Account ${accountIdentifier.toHex()} has balance ${balance.toE8s()} e8s`);
88
- }
89
-
90
- /**
91
- * Send ICP to another address.
92
- *
93
- * @param to The account identifier in hex.
94
- * @param amount Amount to send in e8s.
95
- */
96
- async function sendICP(to: AccountIdentifier, amount: ICP) {
97
- const identity = await getLedgerIdentity();
98
- const ledger = LedgerCanister.create({
99
- agent: await getAgent(identity),
100
- hardwareWallet: true,
101
- });
102
-
103
- const blockHeight = await ledger.transfer({
104
- to: to,
105
- amount: amount,
106
- memo: BigInt(0),
107
- });
108
-
109
- ok(`Transaction completed at block height ${blockHeight}.`);
110
- }
111
-
112
- /**
113
- * Shows the principal and account idenifier on the terminal and on the wallet's screen.
114
- */
115
- async function showInfo(showOnDevice?: boolean) {
116
- const identity = await getLedgerIdentity();
117
- const accountIdentifier = AccountIdentifier.fromPrincipal({
118
- principal: identity.getPrincipal(),
119
- });
120
- const publicKey = identity.getPublicKey() as Secp256k1PublicKey;
121
-
122
- log(chalk.bold(`Principal: `) + identity.getPrincipal());
123
- log(
124
- chalk.bold(`Address (${identity.derivePath}): `) + accountIdentifier.toHex()
125
- );
126
- log(
127
- chalk.bold('Public key: ') + publicKey.toHex()
128
- )
129
-
130
- if (showOnDevice) {
131
- log("Displaying the principal and the address on the device...");
132
- await identity.showAddressAndPubKeyOnDevice();
133
- }
134
- }
135
-
136
- /**
137
- * Stakes a new neuron.
138
- *
139
- * @param amount Amount to stake in e8s.
140
- */
141
- async function stakeNeuron(stake: ICP) {
142
- const identity = await getLedgerIdentity();
143
- const ledger = LedgerCanister.create({
144
- agent: await getAgent(identity),
145
- });
146
- const governance = GovernanceCanister.create({
147
- agent: await getAgent(new AnonymousIdentity()),
148
- hardwareWallet: true,
149
- });
150
-
151
- // Flag that an upcoming stake neuron transaction is coming to distinguish
152
- // it from a "send ICP" transaction on the device.
153
- identity.flagUpcomingStakeNeuron();
154
-
155
- try {
156
- const stakedNeuronId = await governance.stakeNeuron({
157
- stake: stake,
158
- principal: identity.getPrincipal(),
159
- ledgerCanister: ledger,
160
- });
161
-
162
- ok(`Staked neuron with ID: ${stakedNeuronId}`);
163
- } catch (error) {
164
- if (error instanceof InsufficientAmountError) {
165
- err(`Cannot stake less than ${error.minimumAmount.toE8s()} e8s`);
166
- } else if (error instanceof InsufficientFundsError) {
167
- err(`Your account has insufficient funds (${error.balance.toE8s()} e8s)`);
168
- } else {
169
- console.log(error);
170
- }
171
- }
172
- }
173
-
174
- async function increaseDissolveDelay(
175
- neuronId: bigint,
176
- years: number,
177
- days: number,
178
- minutes: number,
179
- seconds: number
180
- ) {
181
- const identity = await getLedgerIdentity();
182
- const governance = GovernanceCanister.create({
183
- agent: await getAgent(identity),
184
- hardwareWallet: true,
185
- });
186
-
187
- const additionalDissolveDelaySeconds =
188
- years * SECONDS_PER_YEAR +
189
- days * SECONDS_PER_DAY +
190
- minutes * SECONDS_PER_MINUTE +
191
- seconds;
192
-
193
- await governance.increaseDissolveDelay({
194
- neuronId: neuronId,
195
- additionalDissolveDelaySeconds: additionalDissolveDelaySeconds,
196
- });
197
-
198
- ok();
199
- }
200
-
201
- async function disburseNeuron(neuronId: bigint, to?: string, amount?: bigint) {
202
- const identity = await getLedgerIdentity();
203
- const governance = GovernanceCanister.create({
204
- agent: await getAgent(identity),
205
- hardwareWallet: true,
206
- });
207
-
208
- await governance.disburse({
209
- neuronId: BigInt(neuronId),
210
- toAccountId: to,
211
- amount: amount,
212
- });
213
-
214
- ok();
215
- }
216
-
217
- async function spawnNeuron(neuronId: string, controller?: Principal) {
218
- const identity = await getLedgerIdentity();
219
- const governance = GovernanceCanister.create({
220
- agent: await getAgent(identity),
221
- hardwareWallet: true,
222
- });
223
-
224
- const spawnedNeuronId = await governance.spawnNeuron({
225
- neuronId: BigInt(neuronId),
226
- newController: controller,
227
- });
228
- ok(`Spawned neuron with ID ${spawnedNeuronId}`);
229
- }
230
-
231
- async function startDissolving(neuronId: bigint) {
232
- const identity = await getLedgerIdentity();
233
- const governance = GovernanceCanister.create({
234
- agent: await getAgent(identity),
235
- hardwareWallet: true,
236
- });
237
-
238
- await governance.startDissolving(neuronId);
239
-
240
- ok();
241
- }
242
-
243
- async function stopDissolving(neuronId: bigint) {
244
- const identity = await getLedgerIdentity();
245
- const governance = GovernanceCanister.create({
246
- agent: await getAgent(identity),
247
- hardwareWallet: true,
248
- });
249
-
250
- await governance.stopDissolving(neuronId);
251
-
252
- ok();
253
- }
254
-
255
- async function addHotkey(neuronId: bigint, principal: Principal) {
256
- const identity = await getLedgerIdentity();
257
- const governance = GovernanceCanister.create({
258
- agent: await getAgent(identity),
259
- hardwareWallet: true,
260
- });
261
-
262
- await governance.addHotkey({
263
- neuronId: BigInt(neuronId),
264
- principal: principal,
265
- });
266
-
267
- ok();
268
- }
269
-
270
- async function removeHotkey(neuronId: bigint, principal: Principal) {
271
- const identity = await getLedgerIdentity();
272
- const governance = GovernanceCanister.create({
273
- agent: await getAgent(identity),
274
- hardwareWallet: true,
275
- });
276
-
277
- await governance.removeHotkey({
278
- neuronId: BigInt(neuronId),
279
- principal: principal,
280
- });
281
-
282
- ok();
283
- }
284
-
285
- async function listNeurons() {
286
- const identity = await getLedgerIdentity();
287
- const governance = GovernanceCanister.create({
288
- agent: await getAgent(identity),
289
- hardwareWallet: true,
290
- });
291
-
292
- // We filter neurons with no ICP, as they'll be garbage collected by the governance canister.
293
- const neurons = await governance.listNeurons({
294
- certified: true,
295
- });
296
-
297
- if (neurons.length > 0) {
298
- neurons.forEach((n) => {
299
- log(`Neuron ID: ${n.neuronId}`);
300
- });
301
- } else {
302
- ok("No neurons found.");
303
- }
304
- }
305
-
306
- /**
307
- * Fetches the balance of the main account on the wallet.
308
- */
309
- async function claimNeurons() {
310
- const identity = await getLedgerIdentity();
311
-
312
- const publicKey = identity.getPublicKey() as Secp256k1PublicKey;
313
- const hexPubKey = publicKey.toHex();
314
-
315
- const governance = await GenesisTokenCanister.create({
316
- agent: await getAgent(identity),
317
- });
318
-
319
- const claimedNeuronIds = await governance.claimNeurons({
320
- hexPubKey,
321
- });
322
-
323
- ok(`Successfully claimed the following neurons: ${claimedNeuronIds}`);
324
- }
325
-
326
- /**
327
- * Runs a function with a try/catch block.
328
- */
329
- async function run(f: () => void) {
330
- try {
331
- await f();
332
- } catch (error: any) {
333
- err(error);
334
- }
335
- }
336
-
337
- function ok(message?: string) {
338
- if (message) {
339
- log(`${chalk.green(chalk.bold("OK"))}: ${message}`);
340
- } else {
341
- log(`${chalk.green(chalk.bold("OK"))}`);
342
- }
343
- }
344
-
345
- function err(error: any) {
346
- const message =
347
- error instanceof GovernanceError
348
- ? error.detail.error_message
349
- : error instanceof Error
350
- ? error.message
351
- : error;
352
- log(`${chalk.bold(chalk.red("Error:"))} ${message}`);
353
- }
354
-
355
- function tryParseInt(value: string): number {
356
- const parsedValue = parseInt(value, 10);
357
- if (isNaN(parsedValue)) {
358
- throw new InvalidArgumentError("Not a number.");
359
- }
360
- return parsedValue;
361
- }
362
-
363
- function tryParseBigInt(value: string): bigint {
364
- try {
365
- return BigInt(value);
366
- } catch (err: any) {
367
- throw new InvalidArgumentError(err.toString());
368
- }
369
- }
370
-
371
- function tryParsePrincipal(value: string): Principal {
372
- try {
373
- return Principal.fromText(value);
374
- } catch (err: any) {
375
- throw new InvalidArgumentError(err.toString());
376
- }
377
- }
378
-
379
- function tryParseE8s(e8s: string): ICP {
380
- try {
381
- return ICP.fromE8s(tryParseBigInt(e8s));
382
- } catch (err: any) {
383
- throw new InvalidArgumentError(err.toString());
384
- }
385
- }
386
-
387
- function tryParseAccountIdentifier(
388
- accountIdentifier: string
389
- ): AccountIdentifier {
390
- try {
391
- return AccountIdentifier.fromHex(accountIdentifier);
392
- } catch (err: any) {
393
- throw new InvalidArgumentError(err.toString());
394
- }
395
- }
396
-
397
- async function main() {
398
- const neuron = new Command("neuron")
399
- .description("Commands for managing neurons.")
400
- .showSuggestionAfterError()
401
- .addCommand(
402
- new Command("stake")
403
- .requiredOption(
404
- "--amount <amount>",
405
- "Amount to stake in e8s.",
406
- tryParseE8s
407
- )
408
- .action((args) => run(() => stakeNeuron(args.amount)))
409
- )
410
- .addCommand(
411
- new Command("increase-dissolve-delay")
412
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
413
- .option("--years <years>", "Number of years", tryParseInt)
414
- .option("--days <days>", "Number of days", tryParseInt)
415
- .option("--minutes <minutes>", "Number of minutes", tryParseInt)
416
- .option("--seconds <seconds>", "Number of seconds", tryParseInt)
417
- .action((args) =>
418
- run(() =>
419
- increaseDissolveDelay(
420
- args.neuronId,
421
- args.years || 0,
422
- args.days || 0,
423
- args.minutes || 0,
424
- args.seconds || 0
425
- )
426
- )
427
- )
428
- )
429
- .addCommand(
430
- new Command("disburse")
431
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
432
- .option("--to <account-identifier>")
433
- .option(
434
- "--amount <amount>",
435
- "Amount to disburse (empty to disburse all)",
436
- tryParseBigInt
437
- )
438
- .action((args) => {
439
- run(() => disburseNeuron(args.neuronId, args.to, args.amount));
440
- })
441
- )
442
- .addCommand(
443
- new Command("spawn")
444
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
445
- .option(
446
- "--controller <new-controller>",
447
- "Controller",
448
- tryParsePrincipal
449
- )
450
- .action((args) => {
451
- run(() => spawnNeuron(args.neuronId, args.controller));
452
- })
453
- )
454
- .addCommand(
455
- new Command("start-dissolving")
456
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
457
- .action((args) => {
458
- run(() => startDissolving(args.neuronId));
459
- })
460
- )
461
- .addCommand(
462
- new Command("stop-dissolving")
463
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
464
- .action((args) => {
465
- run(() => stopDissolving(args.neuronId));
466
- })
467
- )
468
- .addCommand(new Command("list").action(() => run(listNeurons)))
469
- .addCommand(
470
- new Command("add-hotkey")
471
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
472
- .requiredOption(
473
- "--principal <principal>",
474
- "Principal",
475
- tryParsePrincipal
476
- )
477
- .action((args) => run(() => addHotkey(args.neuronId, args.principal)))
478
- )
479
- .addCommand(
480
- new Command("remove-hotkey")
481
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
482
- .requiredOption(
483
- "--principal <principal>",
484
- "Principal",
485
- tryParsePrincipal
486
- )
487
- .action((args) =>
488
- run(() => removeHotkey(args.neuronId, args.principal))
489
- )
490
- )
491
- .addCommand(
492
- new Command("claim")
493
- .description(
494
- "Claim the caller's GTC neurons."
495
- )
496
- .action((args) => run(() => claimNeurons()))
497
- );
498
-
499
- const icp = new Command("icp")
500
- .description("Commands for managing ICP.")
501
- .showSuggestionAfterError()
502
- .addCommand(
503
- new Command("balance")
504
- .description("Fetch current balance.")
505
- .action(() => {
506
- run(getBalance);
507
- })
508
- )
509
- .addCommand(
510
- new Command("transfer")
511
- .requiredOption(
512
- "--to <account-identifier>",
513
- "Account identifier to transfer to.",
514
- tryParseAccountIdentifier
515
- )
516
- .requiredOption(
517
- "--amount <amount>",
518
- "Amount to transfer in e8s.",
519
- tryParseE8s
520
- )
521
- .action((args) => run(() => sendICP(args.to, args.amount)))
522
- );
523
-
524
- program
525
- .description("A CLI for the Ledger hardware wallet.")
526
- .enablePositionalOptions()
527
- .showSuggestionAfterError()
528
- .addOption(
529
- new Option("--network <network>", "The IC network to talk to.")
530
- .default("https://ic0.app")
531
- .env("IC_NETWORK")
532
- )
533
- .addOption(
534
- new Option("--principal <principal>", "The derivation path to use for the principal.\n(e.g. --principal 123 will result in a derivation path of m/44'/223'/0'/0/123)\nMust be >= 0 && <= 255").default(
535
- 0
536
- )
537
- )
538
- .addCommand(
539
- new Command("info")
540
- .option("-n --no-show-on-device")
541
- .description("Show the wallet's principal, address, and balance.")
542
- .action((args) => {
543
- run(() => showInfo(args.showOnDevice));
544
- })
545
- )
546
- .addCommand(icp)
547
- .addCommand(neuron);
548
-
549
- await program.parseAsync(process.argv);
550
- }
551
-
552
- main();