@dfinity/hardware-wallet-cli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,3 +16,16 @@ Live desktop app running on your computer. If you are facing connection issues
16
16
  when doing so, Ledger provides platform-specific
17
17
  [troubleshooting instructions](https://support.ledger.com/hc/en-us/articles/115005165269-Fix-USB-connection-issues-with-Ledger-Live?support=true)
18
18
  on their support site.
19
+
20
+ ## Development
21
+
22
+ Clone the repository.
23
+
24
+ Install dependencies with `npm install`.
25
+
26
+ To execute a command, you can use `npm run execute -- <args>`.
27
+
28
+ For example
29
+
30
+ * The command `ic-hardware-wallet --network https://nnsdapp.dfinity.network icp balance`.
31
+ * Would be `npm run execute -- --network https://nnsdapp.dfinity.network icp balance` for development.
package/index.ts CHANGED
@@ -15,6 +15,8 @@ import {
15
15
  InsufficientAmountError,
16
16
  InsufficientFundsError,
17
17
  } from "@dfinity/nns";
18
+ import { Principal } from "@dfinity/principal";
19
+ import type { Secp256k1PublicKey } from "src/ledger/secp256k1";
18
20
  import { Agent, AnonymousIdentity, HttpAgent, Identity } from "@dfinity/agent";
19
21
  import chalk from "chalk";
20
22
 
@@ -24,7 +26,7 @@ import "node-window-polyfill/register";
24
26
  // Add polyfill for `window.fetch` for agent-js to work.
25
27
  // @ts-ignore (no types are available)
26
28
  import fetch from "node-fetch";
27
- import { Principal } from "@dfinity/principal";
29
+
28
30
  global.fetch = fetch;
29
31
  window.fetch = fetch;
30
32
 
@@ -285,10 +287,20 @@ async function listNeurons() {
285
287
  }
286
288
  }
287
289
 
290
+ const buf2hex = (buffer: ArrayBuffer): string => {
291
+ return [...new Uint8Array(buffer)]
292
+ .map((x) => x.toString(16).padStart(2, "0"))
293
+ .join("");
294
+ };
295
+
288
296
  /**
289
297
  * Fetches the balance of the main account on the wallet.
290
298
  */
291
- async function claimNeurons(hexPubKey: string) {
299
+ async function claimNeurons() {
300
+ const identity = await LedgerIdentity.create();
301
+
302
+ const bufferKey = identity.getPublicKey() as Secp256k1PublicKey;
303
+ const hexPubKey = buf2hex(bufferKey.toRaw());
292
304
  const isHex = hexPubKey.match("^[0-9a-fA-F]+$");
293
305
  if (!isHex) {
294
306
  throw new Error(`${hexPubKey} is not a hex string.`);
@@ -298,7 +310,6 @@ async function claimNeurons(hexPubKey: string) {
298
310
  throw new Error(`The key must be >= 130 characters and <= 150 characters.`);
299
311
  }
300
312
 
301
- const identity = await LedgerIdentity.create();
302
313
  const governance = await GenesisTokenCanister.create({
303
314
  agent: await getAgent(identity),
304
315
  });
@@ -477,11 +488,10 @@ async function main() {
477
488
  )
478
489
  .addCommand(
479
490
  new Command("claim")
480
- .requiredOption(
481
- "--hex-public-key <public-key>",
491
+ .description(
482
492
  "Claim the caller's GTC neurons."
483
493
  )
484
- .action((args) => run(() => claimNeurons(args.hexPublicKey)))
494
+ .action((args) => run(() => claimNeurons()))
485
495
  );
486
496
 
487
497
  const icp = new Command("icp")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfinity/hardware-wallet-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
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": {
@@ -8,7 +8,8 @@
8
8
  "test": "echo \"Error: no test specified\" && exit 1",
9
9
  "build": "tsc --build",
10
10
  "clean": "tsc --build --clean",
11
- "refresh": "rm -rf ./node_modules ./package-lock.json && npm install"
11
+ "refresh": "rm -rf ./node_modules ./package-lock.json && npm install",
12
+ "execute": "ts-node ./index.ts"
12
13
  },
13
14
  "repository": {
14
15
  "type": "git",
@@ -35,7 +36,8 @@
35
36
  "@types/google-protobuf": "^3.15.6",
36
37
  "@types/node": "^17.0.16",
37
38
  "@types/node-hid": "^1.3.1",
38
- "prettier": "^2.6.2"
39
+ "prettier": "^2.6.2",
40
+ "ts-node": "^10.8.0"
39
41
  },
40
42
  "bin": {
41
43
  "ic-hardware-wallet": "./build/index.js"
package/build/index.js DELETED
@@ -1,394 +0,0 @@
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 agent_1 = require("@dfinity/agent");
14
- const chalk_1 = __importDefault(require("chalk"));
15
- // Add polyfill for `window` for `TransportWebHID` checks to work.
16
- require("node-window-polyfill/register");
17
- // Add polyfill for `window.fetch` for agent-js to work.
18
- // @ts-ignore (no types are available)
19
- const node_fetch_1 = __importDefault(require("node-fetch"));
20
- const principal_1 = require("@dfinity/principal");
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
- /**
231
- * Fetches the balance of the main account on the wallet.
232
- */
233
- async function claimNeurons(hexPubKey) {
234
- const isHex = hexPubKey.match("^[0-9a-fA-F]+$");
235
- if (!isHex) {
236
- throw new Error(`${hexPubKey} is not a hex string.`);
237
- }
238
- if (hexPubKey.length < 130 || hexPubKey.length > 150) {
239
- throw new Error(`The key must be >= 130 characters and <= 150 characters.`);
240
- }
241
- const identity = await identity_1.LedgerIdentity.create();
242
- const governance = await nns_1.GenesisTokenCanister.create({
243
- agent: await getAgent(identity),
244
- });
245
- const claimedNeuronIds = await governance.claimNeurons({
246
- hexPubKey: hexPubKey,
247
- });
248
- ok(`Successfully claimed the following neurons: ${claimedNeuronIds}`);
249
- }
250
- /**
251
- * Runs a function with a try/catch block.
252
- */
253
- async function run(f) {
254
- try {
255
- await f();
256
- }
257
- catch (error) {
258
- err(error);
259
- }
260
- }
261
- function ok(message) {
262
- if (message) {
263
- log(`${chalk_1.default.green(chalk_1.default.bold("OK"))}: ${message}`);
264
- }
265
- else {
266
- log(`${chalk_1.default.green(chalk_1.default.bold("OK"))}`);
267
- }
268
- }
269
- function err(error) {
270
- const message = error instanceof nns_1.GovernanceError
271
- ? error.detail.error_message
272
- : error instanceof Error
273
- ? error.message
274
- : error;
275
- log(`${chalk_1.default.bold(chalk_1.default.red("Error:"))} ${message}`);
276
- }
277
- function tryParseInt(value) {
278
- const parsedValue = parseInt(value, 10);
279
- if (isNaN(parsedValue)) {
280
- throw new commander_1.InvalidArgumentError("Not a number.");
281
- }
282
- return parsedValue;
283
- }
284
- function tryParseBigInt(value) {
285
- try {
286
- return BigInt(value);
287
- }
288
- catch (err) {
289
- throw new commander_1.InvalidArgumentError(err.toString());
290
- }
291
- }
292
- function tryParsePrincipal(value) {
293
- try {
294
- return principal_1.Principal.fromText(value);
295
- }
296
- catch (err) {
297
- throw new commander_1.InvalidArgumentError(err.toString());
298
- }
299
- }
300
- function tryParseE8s(e8s) {
301
- try {
302
- return nns_1.ICP.fromE8s(tryParseBigInt(e8s));
303
- }
304
- catch (err) {
305
- throw new commander_1.InvalidArgumentError(err.toString());
306
- }
307
- }
308
- function tryParseAccountIdentifier(accountIdentifier) {
309
- try {
310
- return nns_1.AccountIdentifier.fromHex(accountIdentifier);
311
- }
312
- catch (err) {
313
- throw new commander_1.InvalidArgumentError(err.toString());
314
- }
315
- }
316
- async function main() {
317
- const neuron = new commander_1.Command("neuron")
318
- .description("Commands for managing neurons.")
319
- .showSuggestionAfterError()
320
- .addCommand(new commander_1.Command("stake")
321
- .requiredOption("--amount <amount>", "Amount to stake in e8s.", tryParseE8s)
322
- .action((args) => run(() => stakeNeuron(args.amount))))
323
- .addCommand(new commander_1.Command("increase-dissolve-delay")
324
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
325
- .option("--years <years>", "Number of years", tryParseInt)
326
- .option("--days <days>", "Number of days", tryParseInt)
327
- .option("--minutes <minutes>", "Number of minutes", tryParseInt)
328
- .option("--seconds <seconds>", "Number of seconds", tryParseInt)
329
- .action((args) => run(() => increaseDissolveDelay(args.neuronId, args.years || 0, args.days || 0, args.minutes || 0, args.seconds || 0))))
330
- .addCommand(new commander_1.Command("disburse")
331
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
332
- .option("--to <account-identifier>")
333
- .option("--amount <amount>", "Amount to disburse (empty to disburse all)", tryParseBigInt)
334
- .action((args) => {
335
- run(() => disburseNeuron(args.neuronId, args.to, args.amount));
336
- }))
337
- .addCommand(new commander_1.Command("spawn")
338
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
339
- .option("--controller <new-controller>", "Controller", tryParsePrincipal)
340
- .action((args) => {
341
- run(() => spawnNeuron(args.neuronId, args.controller));
342
- }))
343
- .addCommand(new commander_1.Command("start-dissolving")
344
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
345
- .action((args) => {
346
- run(() => startDissolving(args.neuronId));
347
- }))
348
- .addCommand(new commander_1.Command("stop-dissolving")
349
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
350
- .action((args) => {
351
- run(() => stopDissolving(args.neuronId));
352
- }))
353
- .addCommand(new commander_1.Command("list").action(() => run(listNeurons)))
354
- .addCommand(new commander_1.Command("add-hotkey")
355
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
356
- .requiredOption("--principal <principal>", "Principal", tryParsePrincipal)
357
- .action((args) => run(() => addHotkey(args.neuronId, args.principal))))
358
- .addCommand(new commander_1.Command("remove-hotkey")
359
- .requiredOption("--neuron-id <neuron-id>", "Neuron ID", tryParseBigInt)
360
- .requiredOption("--principal <principal>", "Principal", tryParsePrincipal)
361
- .action((args) => run(() => removeHotkey(args.neuronId, args.principal))))
362
- .addCommand(new commander_1.Command("claim")
363
- .requiredOption("--hex-public-key <public-key>", "Claim the caller's GTC neurons.")
364
- .action((args) => run(() => claimNeurons(args.hexPublicKey))));
365
- const icp = new commander_1.Command("icp")
366
- .description("Commands for managing ICP.")
367
- .showSuggestionAfterError()
368
- .addCommand(new commander_1.Command("balance")
369
- .description("Fetch current balance.")
370
- .action(() => {
371
- run(getBalance);
372
- }))
373
- .addCommand(new commander_1.Command("transfer")
374
- .requiredOption("--to <account-identifier>", "Account identifier to transfer to.", tryParseAccountIdentifier)
375
- .requiredOption("--amount <amount>", "Amount to transfer in e8s.", tryParseE8s)
376
- .action((args) => run(() => sendICP(args.to, args.amount))));
377
- program
378
- .description("A CLI for the Ledger hardware wallet.")
379
- .enablePositionalOptions()
380
- .showSuggestionAfterError()
381
- .addOption(new commander_1.Option("--network <network>", "The IC network to talk to.")
382
- .default("https://ic0.app")
383
- .env("IC_NETWORK"))
384
- .addCommand(new commander_1.Command("info")
385
- .option("-n --no-show-on-device")
386
- .description("Show the wallet's principal, address, and balance.")
387
- .action((args) => {
388
- run(() => showInfo(args.showOnDevice));
389
- }))
390
- .addCommand(icp)
391
- .addCommand(neuron);
392
- await program.parseAsync(process.argv);
393
- }
394
- main();
@@ -1,214 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
- }) : (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- o[k2] = m[k];
8
- }));
9
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
- Object.defineProperty(o, "default", { enumerable: true, value: v });
11
- }) : function(o, v) {
12
- o["default"] = v;
13
- });
14
- var __importStar = (this && this.__importStar) || function (mod) {
15
- if (mod && mod.__esModule) return mod;
16
- var result = {};
17
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
- __setModuleDefault(result, mod);
19
- return result;
20
- };
21
- var __importDefault = (this && this.__importDefault) || function (mod) {
22
- return (mod && mod.__esModule) ? mod : { "default": mod };
23
- };
24
- Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.LedgerIdentity = void 0;
26
- const agent_1 = require("@dfinity/agent");
27
- const principal_1 = require("@dfinity/principal");
28
- const ledger_icp_1 = __importStar(require("@zondax/ledger-icp"));
29
- const secp256k1_1 = require("./secp256k1");
30
- // @ts-ignore (no types are available)
31
- const hw_transport_webhid_1 = __importDefault(require("@ledgerhq/hw-transport-webhid"));
32
- const hw_transport_node_hid_noevents_1 = __importDefault(require("@ledgerhq/hw-transport-node-hid-noevents"));
33
- // Add polyfill for `window.fetch` for agent-js to work.
34
- // @ts-ignore (no types are available)
35
- const node_fetch_1 = __importDefault(require("node-fetch"));
36
- global.fetch = node_fetch_1.default;
37
- /**
38
- * Convert the HttpAgentRequest body into cbor which can be signed by the Ledger Hardware Wallet.
39
- * @param request - body of the HttpAgentRequest
40
- */
41
- function _prepareCborForLedger(request) {
42
- return agent_1.Cbor.encode({ content: request });
43
- }
44
- /**
45
- * A Hardware Ledger Internet Computer Agent identity.
46
- */
47
- class LedgerIdentity extends agent_1.SignIdentity {
48
- constructor(derivePath, _publicKey) {
49
- super();
50
- this.derivePath = derivePath;
51
- this._publicKey = _publicKey;
52
- // A flag to signal that the next transaction to be signed will be
53
- // a "stake neuron" transaction.
54
- this._neuronStakeFlag = false;
55
- }
56
- /**
57
- * Create a LedgerIdentity using the Web USB transport.
58
- * @param derivePath The derivation path.
59
- */
60
- static async create(derivePath = `m/44'/223'/0'/0/0`) {
61
- const [app, transport] = await this._connect();
62
- try {
63
- const publicKey = await this._fetchPublicKeyFromDevice(app, derivePath);
64
- return new this(derivePath, publicKey);
65
- }
66
- finally {
67
- // Always close the transport.
68
- transport.close();
69
- }
70
- }
71
- /**
72
- * Connect to a ledger hardware wallet.
73
- */
74
- static async _connect() {
75
- async function getTransport() {
76
- if (await hw_transport_webhid_1.default.isSupported()) {
77
- // We're in a web browser.
78
- return hw_transport_webhid_1.default.create();
79
- }
80
- else if (await hw_transport_node_hid_noevents_1.default.isSupported()) {
81
- // Maybe we're in a CLI.
82
- return hw_transport_node_hid_noevents_1.default.create();
83
- }
84
- else {
85
- // Unknown environment.
86
- throw Error();
87
- }
88
- }
89
- try {
90
- const transport = await getTransport();
91
- const app = new ledger_icp_1.default(transport);
92
- return [app, transport];
93
- }
94
- catch (err) {
95
- // @ts-ignore
96
- if (err.id && err.id == "NoDeviceFound") {
97
- throw "No Ledger device found. Is the wallet connected and unlocked?";
98
- }
99
- else if (
100
- // @ts-ignore
101
- err.message &&
102
- // @ts-ignore
103
- err.message.includes("cannot open device with path")) {
104
- throw "Cannot connect to Ledger device. Please close all other wallet applications (e.g. Ledger Live) and try again.";
105
- }
106
- else {
107
- // Unsupported browser. Data on browser compatibility is taken from https://caniuse.com/webhid
108
- 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}`;
109
- }
110
- }
111
- }
112
- static async _fetchPublicKeyFromDevice(app, derivePath) {
113
- const resp = await app.getAddressAndPubKey(derivePath);
114
- // @ts-ignore
115
- if (resp.returnCode == 28161) {
116
- throw "Please open the Internet Computer app on your wallet and try again.";
117
- }
118
- else if (resp.returnCode == ledger_icp_1.LedgerError.TransactionRejected) {
119
- throw "Ledger Wallet is locked. Unlock it and try again.";
120
- // @ts-ignore
121
- }
122
- else if (resp.returnCode == 65535) {
123
- throw "Unable to fetch the public key. Please try again.";
124
- }
125
- // This type doesn't have the right fields in it, so we have to manually type it.
126
- const principal = resp
127
- .principalText;
128
- const publicKey = secp256k1_1.Secp256k1PublicKey.fromRaw(new Uint8Array(resp.publicKey));
129
- if (principal !==
130
- principal_1.Principal.selfAuthenticating(new Uint8Array(publicKey.toDer())).toText()) {
131
- throw new Error("Principal returned by device does not match public key.");
132
- }
133
- return publicKey;
134
- }
135
- /**
136
- * Required by Ledger.com that the user should be able to press a Button in UI
137
- * and verify the address/pubkey are the same as on the device screen.
138
- */
139
- async showAddressAndPubKeyOnDevice() {
140
- this._executeWithApp(async (app) => {
141
- await app.showAddressAndPubKey(this.derivePath);
142
- });
143
- }
144
- /**
145
- * @returns The verion of the `Internet Computer' app installed on the Ledger device.
146
- */
147
- async getVersion() {
148
- return this._executeWithApp(async (app) => {
149
- const res = await app.getVersion();
150
- return {
151
- major: res.major,
152
- minor: res.minor,
153
- patch: res.patch,
154
- };
155
- });
156
- }
157
- getPublicKey() {
158
- return this._publicKey;
159
- }
160
- async sign(blob) {
161
- return await this._executeWithApp(async (app) => {
162
- const resp = await app.sign(this.derivePath, Buffer.from(blob), this._neuronStakeFlag ? 1 : 0);
163
- // Remove the "neuron stake" flag, since we already signed the transaction.
164
- this._neuronStakeFlag = false;
165
- const signatureRS = resp.signatureRS;
166
- if (!signatureRS) {
167
- throw new Error(`A ledger error happened during signature:\n` +
168
- `Code: ${resp.returnCode}\n` +
169
- `Message: ${JSON.stringify(resp.errorMessage)}\n`);
170
- }
171
- if (signatureRS?.byteLength !== 64) {
172
- throw new Error(`Signature must be 64 bytes long (is ${signatureRS.length})`);
173
- }
174
- return bufferToArrayBuffer(signatureRS);
175
- });
176
- }
177
- /**
178
- * Signals that the upcoming transaction to be signed will be a "stake neuron" transaction.
179
- */
180
- flagUpcomingStakeNeuron() {
181
- this._neuronStakeFlag = true;
182
- }
183
- async transformRequest(request) {
184
- const { body, ...fields } = request;
185
- const signature = await this.sign(_prepareCborForLedger(body));
186
- return {
187
- ...fields,
188
- body: {
189
- content: body,
190
- sender_pubkey: this._publicKey.toDer(),
191
- sender_sig: signature,
192
- },
193
- };
194
- }
195
- async _executeWithApp(func) {
196
- const [app, transport] = await LedgerIdentity._connect();
197
- try {
198
- // Verify that the public key of the device matches the public key of this identity.
199
- const devicePublicKey = await LedgerIdentity._fetchPublicKeyFromDevice(app, this.derivePath);
200
- if (JSON.stringify(devicePublicKey) !== JSON.stringify(this._publicKey)) {
201
- throw new Error("Found unexpected public key. Are you sure you're using the right wallet?");
202
- }
203
- // Run the provided function.
204
- return await func(app);
205
- }
206
- finally {
207
- transport.close();
208
- }
209
- }
210
- }
211
- exports.LedgerIdentity = LedgerIdentity;
212
- function bufferToArrayBuffer(buffer) {
213
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
214
- }
@@ -1,74 +0,0 @@
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
- ]);