hodl-wallet 1.0.6 → 1.1.8

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.
Files changed (3) hide show
  1. package/README.md +16 -17
  2. package/index.mjs +197 -133
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -2,9 +2,16 @@
2
2
 
3
3
  ![HODL Wallet](https://raw.githubusercontent.com/clasen/HODL/refs/heads/master/example.jpg)
4
4
 
5
+ ## 🚀 Install and try!
6
+
7
+ ```bash
8
+ npm install -g hodl-wallet
9
+ hodl
10
+ ```
11
+
5
12
  ## 🚀 Why HODL Wallet?
6
13
 
7
- Let's face it, Trust Wallet's sluggishness and annoying ads are so last season. HODL Wallet is here to revolutionize your crypto experience:
14
+ Let's face it, Trust Wallet's sluggishness and annoying ads are so last season. HODL Wallet is here to agilize your crypto experience.
8
15
 
9
16
  - 🏎️ Lightning-fast operations
10
17
  - 🧊 Cool, minimalist CLI interface
@@ -12,18 +19,6 @@ Let's face it, Trust Wallet's sluggishness and annoying ads are so last season.
12
19
  - 🔒 Create wallets offline (because paranoia is just good sense in crypto)
13
20
  - 🔍 Fully transparent, open-source code
14
21
 
15
- ## 🛠️ Installation
16
-
17
- ```bash
18
- npm install -g hodl-wallet
19
- ```
20
-
21
- ## 🚀 Quick Start
22
-
23
- ```bash
24
- hodl
25
- ```
26
-
27
22
  That's it! Follow the prompts and you're in crypto heaven.
28
23
 
29
24
  ## 🎮 Features
@@ -40,17 +35,21 @@ Smoother than sliding into your crush's DMs.
40
35
 
41
36
  Because constantly checking your balance is totally healthy.
42
37
 
38
+ ### 📘 Address Book
39
+
40
+ Keep your favorite addresses handy. No more copy-pasting!
41
+
43
42
  ## 🔒 Security
44
43
 
45
- ### Private Key Storage
44
+ ### 🔑 Private Key Storage
46
45
 
47
- Your private key is securely stored in a JSON file, encrypted with your first password. This ensures that your sensitive information remains protected while still being accessible when you need it.
46
+ Your private key is securely stored in a JSON file, encrypted with a password of your choice. The encryption adds an extra layer of security, making it significantly harder for unauthorized parties to access your private key even if they gain access to the JSON file.
48
47
 
49
- ## 🔬 Transparency
48
+ ### 🔬 Transparency
50
49
 
51
50
  We're as transparent as your ex's excuses. Our code is open-source, and we encourage you to dive in, explore, and contribute. Trust isn't given; it's earned and verified.
52
51
 
53
- ## 🔍 Security Audit
52
+ ### 🔍 Security Audit
54
53
 
55
54
  We encourage users to perform their own security audits. One easy way to do this is to copy the entire codebase into ChatGPT or another AI assistant and ask if the code appears secure or if there are any malicious intentions. This is a good practice for any open-source project you're considering using.
56
55
 
package/index.mjs CHANGED
@@ -29,7 +29,7 @@ class Wallet {
29
29
  try {
30
30
  const networkPlugins = await this.loadNetworkPlugins();
31
31
  if (networkPlugins.length === 0) {
32
- console.log('No valid network plugins found.');
32
+ Wallet.displayError('No valid network plugins found.');
33
33
  process.exit(1);
34
34
  }
35
35
  await this.selectNetwork(networkPlugins);
@@ -37,11 +37,12 @@ class Wallet {
37
37
  await this.loadAccount();
38
38
 
39
39
  if (!this.account) {
40
- throw new Error('Failed to initialize account');
40
+ Wallet.displayError('Failed to initialize account.');
41
+ process.exit(1);
41
42
  }
42
43
 
43
44
  } catch (error) {
44
- console.error('Initialization failed:', error);
45
+ Wallet.displayError('Initialization failed', error);
45
46
  process.exit(1);
46
47
  }
47
48
  }
@@ -85,56 +86,88 @@ class Wallet {
85
86
  async loadAccount(forceReload = false) {
86
87
  let account = this.db.secureGet('account');
87
88
 
89
+ const choices = ['Create New Account', 'Import Mnemonic (12 words)', 'Import private-key'];
90
+
91
+ if (forceReload) {
92
+ choices.push('Export Account');
93
+ choices.push('Go back');
94
+ }
95
+
88
96
  if (!account || forceReload) {
89
97
  const { accountAction } = await inquirer.prompt({
90
98
  type: 'list',
91
99
  name: 'accountAction',
92
100
  message: 'Select an account option:',
93
- choices: ['Import private key', 'Import from 12 words', 'Create new account'],
101
+ choices,
94
102
  });
95
103
 
96
- if (accountAction === 'Import private key') {
97
- const { privateKey } = await inquirer.prompt({
98
- type: 'password',
99
- name: 'privateKey',
100
- message: 'Private key:',
101
- mask: '*',
104
+ if (accountAction === 'Go back') {
105
+ return;
106
+ }
107
+
108
+ if (account && accountAction !== 'Export Account') {
109
+ const { confirmOverwrite } = await inquirer.prompt({
110
+ type: 'confirm',
111
+ name: 'confirmOverwrite',
112
+ message: 'This action will overwrite the existing account. Are you sure you want to continue?',
113
+ default: false,
102
114
  });
103
- account = this.web3.eth.accounts.privateKeyToAccount(privateKey);
104
- await this.db.secureSet('account', { privateKey: account.privateKey });
105
115
 
106
- this.account = account;
107
- this.displayAccountAddress();
108
- } else if (accountAction === 'Import from 12 words') {
109
- account = await this.importFrom12Words();
110
- this.account = account;
111
- this.displayAccountAddress();
112
- } else {
116
+ if (!confirmOverwrite) {
117
+ return;
118
+ }
119
+ }
120
+
121
+ if (accountAction === 'Create New Account') {
113
122
  const mnemonic = bip39.generateMnemonic();
114
123
  const seed = await bip39.mnemonicToSeed(mnemonic);
115
124
  const root = hdkey.fromMasterSeed(seed);
116
125
  const addrNode = root.derive("m/44'/60'/0'/0/0");
117
126
  const privateKey = addrNode.privateKey.toString('hex');
118
- account = this.web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
119
- this.db.secureSet('account', { privateKey: account.privateKey });
127
+ this.account = this.web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
128
+ this.account.mnemonic = mnemonic;
129
+ await this.db.secureSet('account', this.account);
130
+ this.displayAccountDetails();
131
+ }
120
132
 
121
- const table = new Table({
122
- head: [{ colSpan: 2, content: 'New Account Created' }],
123
- style: { head: ['green'] },
124
- wordWrap: true
133
+ if (accountAction === 'Import private-key') {
134
+ const { privateKey } = await inquirer.prompt({
135
+ type: 'password',
136
+ name: 'privateKey',
137
+ message: 'Private-key:',
138
+ mask: '*',
125
139
  });
126
140
 
127
- table.push(
128
- ['Address', account.address],
129
- ['Private Key', account.privateKey],
130
- ['Mnemonic Phrase', mnemonic],
131
- ['WARNING', "Please write down your mnemonic phrase and keep it in a safe place. \nIt\'s crucial for recovering your account."]
132
- );
141
+ if (!privateKey.trim()) {
142
+ Wallet.displayError('Private-key is empty.');
143
+ return;
144
+ }
145
+
146
+ try {
147
+ this.account = this.web3.eth.accounts.privateKeyToAccount(privateKey);
148
+ await this.db.secureSet('account', this.account);
149
+ this.displayAccountAddress();
150
+ } catch (error) {
151
+ Wallet.displayError('Invalid private-key.');
152
+ return;
153
+ }
154
+ }
133
155
 
134
- console.log('\n' + table.toString());
156
+ if (accountAction === 'Import Mnemonic (12 words)') {
157
+ account = await this.importFrom12Words();
158
+ if (!account) {
159
+ Wallet.displayError('Invalid mnemonic.');
160
+ return;
161
+ }
162
+ this.account = account;
163
+ this.displayAccountAddress();
135
164
  }
136
- } else {
137
- account = this.web3.eth.accounts.privateKeyToAccount(account.privateKey);
165
+
166
+ if (accountAction === 'Export Account') {
167
+ await this.displayAccountDetails();
168
+ return;
169
+ }
170
+ return;
138
171
  }
139
172
 
140
173
  this.account = account;
@@ -149,7 +182,6 @@ class Wallet {
149
182
  });
150
183
 
151
184
  if (!bip39.validateMnemonic(mnemonic)) {
152
- console.error('Invalid mnemonic phrase');
153
185
  return null;
154
186
  }
155
187
 
@@ -159,14 +191,14 @@ class Wallet {
159
191
  const privateKey = addrNode.privateKey.toString('hex');
160
192
  const account = this.web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
161
193
 
162
- await this.db.secureSet('account', { privateKey: account.privateKey });
163
-
194
+ account.mnemonic = mnemonic;
195
+ await this.db.secureSet('account', account);
164
196
  return account;
165
197
  }
166
198
 
167
199
  async showBalance() {
168
200
  if (!this.account) {
169
- console.error('Account not initialized. Please try restarting the application.');
201
+ Wallet.displayError('Account not initialized.', Please try restarting the application.');
170
202
  return;
171
203
  }
172
204
 
@@ -179,9 +211,8 @@ class Wallet {
179
211
  colWidths: [21, 22]
180
212
  });
181
213
 
182
- // Add address as the top row
183
214
  // table.push([{ colSpan: 2, content: this.account.address }]);
184
- table.push([this.selectedNetwork.nativeToken, parseFloat(balance).toFixed(8)]);
215
+ table.push([this.selectedNetwork.nativeToken, parseFloat(balance).toFixed(3)]);
185
216
 
186
217
  // Check if USDT is available on the selected network
187
218
  if (this.selectedNetwork.tokens['USDT']) {
@@ -212,7 +243,7 @@ class Wallet {
212
243
  (BigInt(usdtBalance) * 100n) / (10n ** BigInt(decimals))
213
244
  ) / 100;
214
245
 
215
- table.push(['USDT', formattedUsdtBalance.toFixed(8)]);
246
+ table.push(['USDT', formattedUsdtBalance.toFixed(3)]);
216
247
  }
217
248
 
218
249
  console.log(table.toString());
@@ -258,7 +289,6 @@ class Wallet {
258
289
  });
259
290
 
260
291
  if (amount === '') {
261
- console.log('Transaction cancelled.');
262
292
  return;
263
293
  }
264
294
 
@@ -271,7 +301,7 @@ class Wallet {
271
301
  }
272
302
 
273
303
  const receipt = await this.web3.eth.sendSignedTransaction(signedTx.rawTransaction);
274
- await this.displayTransactionResult(receipt);
304
+ this.displayTransactionResult(address, token, amount, receipt);
275
305
 
276
306
  // Add transaction to history
277
307
  this.addToTransactions(address, token, amount, receipt);
@@ -281,10 +311,40 @@ class Wallet {
281
311
  }
282
312
 
283
313
  } catch (error) {
284
- await this.displayTransactionResult(null, error);
314
+ this.displayTransactionError(error);
285
315
  }
286
316
  }
287
317
 
318
+ displayTransactionError(error) {
319
+ const data = error.reason ? error.reason.replace(/(\w+):/g, "\n$1:").trim() : null;
320
+ Wallet.displayError(error.message, data);
321
+ }
322
+
323
+ static displayError(message, data) {
324
+ const table = new Table({
325
+ head: [message],
326
+ style: { head: ['red'] },
327
+ wordWrap: true,
328
+ });
329
+
330
+ if (data) {
331
+ table.push([data]);
332
+ }
333
+
334
+ console.log(table.toString());
335
+ }
336
+
337
+ formatDate(date) {
338
+ return new Date(date).toLocaleString('en-GB', {
339
+ year: 'numeric',
340
+ month: '2-digit',
341
+ day: '2-digit',
342
+ hour: '2-digit',
343
+ minute: '2-digit',
344
+ hour12: false
345
+ }).replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$2-$1').replace(",", "");
346
+ }
347
+
288
348
  addToTransactions(recipient, token, amount, receipt) {
289
349
  const transaction = {
290
350
  timestamp: new Date().toISOString(),
@@ -309,15 +369,8 @@ class Wallet {
309
369
  }
310
370
 
311
371
  history.forEach(tx => {
312
- const date = new Date(tx.timestamp).toLocaleString('en-GB', {
313
- year: 'numeric',
314
- month: '2-digit',
315
- day: '2-digit',
316
- hour: '2-digit',
317
- minute: '2-digit',
318
- hour12: false
319
- }).replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$2-$1').replace(",", "");
320
- table.push([date, tx.recipient, tx.token, tx.amount]);
372
+ const date = this.formatDate(tx.timestamp);
373
+ table.push([date, tx.recipient, tx.token, parseFloat(tx.amount).toFixed(3)]);
321
374
  table.push([{ colSpan: 4, content: this.selectedNetwork.explorer + tx.hash }]);
322
375
  });
323
376
 
@@ -389,14 +442,7 @@ class Wallet {
389
442
 
390
443
  return this.web3.eth.accounts.signTransaction(tx, this.account.privateKey);
391
444
  } catch (error) {
392
- if (error instanceof Web3ValidatorError) {
393
- const table = new Table({
394
- head: [{ colSpan: 2, content: 'Error: Invalid Address' }],
395
- style: { head: ['red'] }
396
- });
397
- table.push(['Address', recipient]);
398
- console.log(table.toString());
399
- }
445
+ this.displayTransactionError(error);
400
446
  }
401
447
  }
402
448
 
@@ -416,13 +462,12 @@ class Wallet {
416
462
  if (balance < amountWei + gasCost) {
417
463
  const maxAmount = this.web3.utils.fromWei((balance - gasCost).toString(), 'ether');
418
464
  const table = new Table({
419
- head: [{ colSpan: 2, content: 'Insufficient balance for this transaction' }],
465
+ head: [{ colSpan: 2, content: 'Insufficient balance for this transaction.' }],
420
466
  style: { head: ['red'] },
421
467
  wordWrap: true
422
468
  });
423
469
  table.push(['Maximum Amount', `${maxAmount} ${this.selectedNetwork.nativeToken}`]);
424
470
  console.log(table.toString());
425
- return;
426
471
  }
427
472
 
428
473
  const tx = {
@@ -436,43 +481,56 @@ class Wallet {
436
481
  return this.web3.eth.accounts.signTransaction(tx, this.account.privateKey);
437
482
  }
438
483
 
439
- async displayTransactionResult(receipt, error = null) {
440
- if (receipt) {
441
- const gasUsed = receipt.gasUsed;
442
- const gasPrice = await this.web3.eth.getGasPrice();
443
- const transactionFee = this.web3.utils.fromWei((BigInt(gasUsed) * BigInt(gasPrice)).toString(), 'ether');
484
+ displayTransactionResult(address, token, amount, hash) {
444
485
 
445
- const table = new Table({
446
- head: [{ colSpan: 2, content: 'Transaction successful, share this link with the recipient' }],
447
- style: { head: ['green'] }
448
- });
449
- table.push(
450
- ['Explorer', this.selectedNetwork.explorer + receipt.transactionHash],
451
- ['Fee', `${transactionFee} ${this.selectedNetwork.nativeToken}`],
452
- );
486
+ const table = new Table({
487
+ head: ['Date', 'Recipient', 'Token', 'Amount'],
488
+ style: { head: ['green'] },
489
+ });
453
490
 
454
- console.log('\n' + table.toString());
455
- } else if (error) {
456
- const table = new Table({
457
- head: [error.message],
458
- style: { head: ['red'] },
459
- wordWrap: true,
460
- });
491
+ const date = this.formatDate(new Date());
461
492
 
462
- if (error.reason) {
463
- table.push([error.reason.replace(/(\w+):/g, "\n$1:").trim()]);
464
- }
493
+ table.push([
494
+ date,
495
+ address,
496
+ token,
497
+ amount.toFixed(3)
498
+ ]);
499
+ table.push([{ colSpan: 4, content: this.selectedNetwork.explorer + hash }]);
465
500
 
466
- console.log('\n' + table.toString());
467
- }
501
+ console.log('\n' + table.toString());
468
502
  }
469
503
 
470
504
  clearAccountData() {
471
505
  if (this.account) {
472
506
  this.account.privateKey = '0'.repeat(64);
507
+ this.account.mnemonic = 'x'.repeat(256);
473
508
  this.account = null;
474
509
  }
475
510
  }
511
+
512
+ displayAccountDetails() {
513
+
514
+ const table = new Table({
515
+ head: [{ colSpan: 2, content: 'Account Details' }],
516
+ style: { head: ['green'] },
517
+ wordWrap: true
518
+ });
519
+
520
+ table.push(
521
+ ['Address', this.account.address],
522
+ ['Private-key', this.account.privateKey]
523
+ );
524
+
525
+ if (this.account.mnemonic) {
526
+ table.push(['Mnemonic Phrase', this.account.mnemonic]);
527
+ table.push(['WARNING', "Please keep your private-key and mnemonic phrase secure. Never share it."]);
528
+ } else {
529
+ table.push(['WARNING', "Please keep your private-key secure. Never share it."]);
530
+ }
531
+
532
+ console.log('\n' + table.toString());
533
+ }
476
534
  }
477
535
 
478
536
  class UIManager {
@@ -546,66 +604,72 @@ class UIManager {
546
604
  }
547
605
  }
548
606
 
549
- async function main() {
550
- UIManager.displayWelcome();
551
- const encryptionKey = await UIManager.getEncryptionKey();
552
- const wallet = new Wallet(encryptionKey);
607
+ UIManager.displayWelcome();
608
+ const encryptionKey = await UIManager.getEncryptionKey();
609
+ const wallet = new Wallet(encryptionKey);
610
+
611
+ // Check if an account exists
612
+ const accountExists = wallet.db.secureGet('account');
613
+ if (accountExists === null) {
614
+ Wallet.displayError('Wrong password.');
615
+ process.exit(1);
616
+ }
553
617
 
554
- // Check if an account exists
555
- const accountExists = wallet.db.secureGet('account');
556
- if (accountExists === null) {
557
- console.error('Wrong password.');
618
+ if (!accountExists) {
619
+ const confirmedKey = await UIManager.confirmEncryptionKey(encryptionKey);
620
+ if (confirmedKey !== encryptionKey) {
621
+ Wallet.displayError('Passwords do not match. Please try again.');
558
622
  process.exit(1);
559
623
  }
624
+ }
560
625
 
561
- if (!accountExists) {
562
- const confirmedKey = await UIManager.confirmEncryptionKey(encryptionKey);
563
- if (confirmedKey !== encryptionKey) {
564
- console.error('Passwords do not match. Please try again.');
565
- process.exit(1);
566
- }
567
- }
626
+ try {
627
+ await wallet.initialize();
628
+ if (accountExists) wallet.displayAccountAddress();
629
+ } catch (error) {
630
+ Wallet.displayError('Failed to initialize wallet.', error);
631
+ process.exit(1);
632
+ }
568
633
 
569
- try {
570
- await wallet.initialize();
571
- if (accountExists) wallet.displayAccountAddress();
572
- } catch (error) {
573
- console.error('Failed to initialize wallet:', error);
574
- process.exit(1);
575
- }
634
+ // Register the displayExitPhrase method and account clearing to be called on process exit
635
+ process.on('exit', () => {
636
+ wallet.clearAccountData();
637
+ UIManager.displayExitPhrase();
638
+ });
576
639
 
577
- // Register the displayExitPhrase method and account clearing to be called on process exit
578
- process.on('exit', () => {
579
- wallet.clearAccountData();
580
- UIManager.displayExitPhrase();
640
+ while (true) {
641
+ const { action } = await inquirer.prompt({
642
+ type: 'list',
643
+ name: 'action',
644
+ message: 'What would you like to do?',
645
+ choices: [
646
+ { name: 'Transfer Funds', value: 'transfer' },
647
+ { name: 'Show Balance', value: 'balance' },
648
+ { name: 'Show Sents', value: 'history' },
649
+ { name: 'Account', value: 'account' },
650
+ { name: 'Exit', value: 'exit' }
651
+ ],
581
652
  });
582
653
 
583
- while (true) {
584
- const { action } = await inquirer.prompt({
585
- type: 'list',
586
- name: 'action',
587
- message: 'What would you like to do?',
588
- choices: ['Transfer funds', 'Show balance', 'Show transactions', 'Change account', 'Exit'],
589
- });
590
-
591
- if (action === 'Transfer funds') {
592
- await wallet.transferFunds();
593
- } else if (action === 'Show balance') {
654
+ switch (action) {
655
+ case 'balance':
594
656
  await wallet.showBalance();
595
- } else if (action === 'Show transactions') {
657
+ break;
658
+ case 'transfer':
659
+ await wallet.transferFunds();
660
+ break;
661
+ case 'history':
596
662
  await wallet.showTransactions();
597
- } else if (action === 'Change account') {
663
+ break;
664
+ case 'account':
598
665
  await wallet.loadAccount(true);
599
- } else {
666
+ break;
667
+ case 'exit':
600
668
  process.exit();
601
- }
602
669
  }
603
670
  }
604
671
 
605
- main().catch(error => {
606
- console.error('An unexpected error occurred:', error);
607
- process.exit(1);
608
- });
672
+
609
673
 
610
674
  process.on('SIGINT', () => {
611
675
  console.log('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hodl-wallet",
3
- "version": "1.0.6",
3
+ "version": "1.1.8",
4
4
  "description": "🧊 HODL Wallet - Blazing-fast, transparent, and ad-free crypto wallet!",
5
5
  "main": "index.mjs",
6
6
  "bin": {
@@ -17,13 +17,16 @@
17
17
  "ethereum",
18
18
  "bsc20",
19
19
  "bep20",
20
- "evm",
21
20
  "bnb",
22
21
  "eth",
23
22
  "binance",
24
23
  "trust",
25
24
  "blockchain",
26
25
  "bitcoin",
26
+ "usdt",
27
+ "stable",
28
+ "coin",
29
+ "evm",
27
30
  "clasen"
28
31
  ],
29
32
  "author": "Hodl Wallet",