hodl-wallet 1.4.4 → 1.5.2

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
@@ -41,7 +41,28 @@ Keep your favorite addresses handy. No more copy-pasting!
41
41
 
42
42
  ### 🌐 Multi-Network Support
43
43
 
44
- Seamlessly manage your assets on both BEP-20 (Binance Smart Chain) and ERC-20 (Ethereum) networks. Switch between networks with ease!
44
+ Seamlessly manage your assets on multiple networks. HODL Wallet supports the following networks:
45
+
46
+ - Binance Smart Chain
47
+ - Ethereum
48
+ - Polygon
49
+ - Optimism
50
+ - Arbitrum One
51
+ - Fantom
52
+ - Avalanche C-Chain
53
+
54
+ Each network supports its native token and popular tokens like USDT. You can easily add more tokens as needed.
55
+
56
+ ## 💾 Export and Import HODL Files
57
+
58
+ HODL Wallet now supports exporting and importing encrypted .HODL files, which securely store your wallet information.
59
+
60
+ - **Export HODL File**: Save your wallet data (including private keys and addresses) to an encrypted .HODL file.
61
+ - **Import HODL File**: Restore your wallet from a previously exported .HODL file.
62
+
63
+ These files are encrypted using your wallet password, providing an additional layer of security for storing and transferring your wallet information.
64
+
65
+ The main advantage of exporting a HODL file is that to access the private key, you need BOTH the file AND the password. This two-factor approach significantly enhances security. However, keep in mind that this solution is only compatible with HODL Wallet.
45
66
 
46
67
  ## 🔒 Security
47
68
 
@@ -65,12 +86,25 @@ We've carefully selected trusted and well-maintained dependencies for this proje
65
86
  - **deepbase**: For persistent storage.
66
87
  - **crypto-js**: For encryption.
67
88
  - **inquirer** and **inquirer-autocomplete-prompt**: For interactive command-line interfaces.
89
+ - **inquirer-fuzzy-path**: For fuzzy searching and selecting file paths during HODL file import.
68
90
  - **cli-table3**: For creating formatted CLI tables.
69
91
  - **bip39**: For generating and handling mnemonic phrases.
70
92
  - **hdkey**: For handling hierarchical deterministic (HD) keys.
71
93
 
72
94
  ⚠️ **Important Notice**: HODL Wallet is a personal project created with the best intentions. While we strive for security, it may contain security flaws or vulnerabilities. Use at your own risk and always exercise caution with your crypto assets.
73
95
 
96
+ ## 📘 What HODL means
97
+
98
+ The term "HODL" is a cornerstone of crypto culture, and it's worth understanding its origins:
99
+
100
+ - 🎂 Born on December 18, 2013, in a Bitcoin Talk forum post
101
+ - 🍺 Originally a typo for "HOLD" in a drunk, impassioned rant about not selling Bitcoin
102
+ - 🔤 Later backronymed to mean "Hold On for Dear Life"
103
+ - 💎 Symbolizes a long-term investment strategy and resistance to panic selling
104
+ - 🌍 Now used across various cryptocurrency communities as a rallying cry
105
+
106
+ HODL embodies the belief in the long-term potential of cryptocurrencies, often in the face of short-term market volatility. It's more than just a misspelling; it's a philosophy that has shaped the crypto landscape.
107
+
74
108
  ## 🤝 Contributing
75
109
 
76
110
  Found a bug? Want to add a feature? We're all ears! Open an issue or submit a PR. Let's make crypto easier together.
package/index.mjs CHANGED
@@ -11,12 +11,14 @@ import Table from 'cli-table3';
11
11
  import bip39 from 'bip39';
12
12
  import hdkey from 'hdkey';
13
13
  import os from 'os';
14
+ import inquirerFuzzyPath from 'inquirer-fuzzy-path';
14
15
 
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = dirname(__filename);
17
18
 
18
19
  import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
19
20
  inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
21
+ inquirer.registerPrompt('fuzzypath', inquirerFuzzyPath);
20
22
 
21
23
  process.on('SIGINT', () => {
22
24
  process.exit();
@@ -34,6 +36,7 @@ class Wallet {
34
36
  this.web3 = null;
35
37
  this.selectedNetwork = null;
36
38
  this.account = null;
39
+ this.networkUsage = this.db.get('networkUsage') || {};
37
40
  }
38
41
 
39
42
  async initialize() {
@@ -82,14 +85,23 @@ class Wallet {
82
85
  }
83
86
 
84
87
  async selectNetwork(networkPlugins) {
88
+ // Sort networks by usage count (descending)
89
+ const sortedNetworks = networkPlugins.sort((a, b) =>
90
+ (this.networkUsage[b.name] || 0) - (this.networkUsage[a.name] || 0)
91
+ );
92
+
85
93
  const { network } = await inquirer.prompt({
86
94
  type: 'list',
87
95
  name: 'network',
88
96
  message: 'Select the network:',
89
- choices: networkPlugins.map(plugin => plugin.name),
97
+ choices: sortedNetworks.map(plugin => plugin.name),
90
98
  });
91
99
 
92
- const selectedNetwork = networkPlugins.find(plugin => plugin.name === network);
100
+ const selectedNetwork = sortedNetworks.find(plugin => plugin.name === network);
101
+
102
+ // Increment usage count for the selected network
103
+ this.networkUsage[network] = (this.networkUsage[network] || 0) + 1;
104
+ this.db.set('networkUsage', this.networkUsage);
93
105
 
94
106
  this.selectedNetwork = selectedNetwork;
95
107
  }
@@ -97,113 +109,101 @@ class Wallet {
97
109
  async loadAccount(forceReload = false) {
98
110
  let account = this.db.secureGet('account');
99
111
 
100
- const choices = ['Create New Account', 'Import Mnemonic (12 words)', 'Import Private-key'];
112
+ const mainChoices = ['Create New Account'];
101
113
 
102
114
  if (forceReload) {
103
- choices.push('Export Account');
104
- choices.push('Switch Network');
105
- choices.push('Go Back');
115
+ mainChoices.push('Import Options');
116
+ mainChoices.push('Export Options');
117
+ mainChoices.push('Switch Network');
118
+ mainChoices.push('Go Back');
119
+ } else {
120
+ mainChoices.push('Import Mnemonic (12 words)');
121
+ mainChoices.push('Import Private-key');
122
+ mainChoices.push('Import HODL File');
106
123
  }
107
124
 
108
125
  if (!account || forceReload) {
109
- const { accountAction } = await inquirer.prompt({
126
+ let { accountAction } = await inquirer.prompt({
110
127
  type: 'list',
111
128
  name: 'accountAction',
112
129
  message: 'Select an account option:',
113
- choices,
130
+ choices: mainChoices,
114
131
  });
115
132
 
116
133
  if (accountAction === 'Go Back') {
117
134
  return;
118
135
  }
119
136
 
120
- if (account && accountAction !== 'Export Account' && accountAction !== 'Switch Network') {
121
- const { confirmOverwrite } = await inquirer.prompt({
122
- type: 'confirm',
123
- name: 'confirmOverwrite',
124
- message: 'This action will overwrite the existing account. Are you sure you want to continue?',
125
- default: false,
137
+ if (accountAction === 'Import Options') {
138
+ const importChoices = ['Import Mnemonic (12 words)', 'Import Private-key', 'Import HODL File', 'Go Back'];
139
+ const { importAction } = await inquirer.prompt({
140
+ type: 'list',
141
+ name: 'importAction',
142
+ message: 'Select an import option:',
143
+ choices: importChoices,
126
144
  });
127
145
 
128
- if (!confirmOverwrite) {
129
- return;
146
+ if (importAction === 'Go Back') {
147
+ return this.loadAccount(forceReload);
130
148
  }
131
- }
132
149
 
133
- if (accountAction === 'Create New Account') {
134
- const { createWithMnemonic } = await inquirer.prompt({
135
- type: 'confirm',
136
- name: 'createWithMnemonic',
137
- message: 'Create account with mnemonic?',
138
- default: true
139
- });
140
-
141
- let message = 'Do you want to display sensitive information (private key';
142
- if (createWithMnemonic) {
143
- const mnemonic = bip39.generateMnemonic();
144
- const seed = await bip39.mnemonicToSeed(mnemonic);
145
- const root = hdkey.fromMasterSeed(seed);
146
- const addrNode = root.derive("m/44'/60'/0'/0/0");
147
- const privateKey = addrNode.privateKey.toString('hex');
148
- this.account = this.web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
149
- this.account.mnemonic = mnemonic;
150
- message += ' and mnemonic';
151
- } else {
152
- this.account = this.web3.eth.accounts.create();
150
+ if (!await this.confirmOverwrite()) {
151
+ return;
153
152
  }
154
153
 
155
- message += ')?';
156
-
157
- await this.db.secureSet('account', this.account);
158
-
159
- const { showSensitive } = await inquirer.prompt({
160
- type: 'confirm',
161
- name: 'showSensitive',
162
- message,
163
- default: false,
164
- });
154
+ accountAction = importAction;
155
+ }
165
156
 
166
- if (showSensitive) {
167
- this.displayAccountDetails();
168
- } else {
157
+ switch (accountAction) {
158
+ case 'Import Mnemonic (12 words)':
159
+ account = await this.importFrom12Words();
160
+ if (!account) {
161
+ Wallet.displayError('Invalid mnemonic.');
162
+ return;
163
+ }
164
+ this.account = account;
169
165
  this.displayAccountAddress();
170
- }
166
+ break;
167
+ case 'Import Private-key':
168
+ await this.importPrivateKey();
169
+ break;
170
+ case 'Import HODL File':
171
+ this.account = await this.importHODLFile();
172
+ this.displayAccountAddress();
173
+ break;
171
174
  }
172
175
 
173
- if (accountAction === 'Import Private-key') {
174
- const { privateKey } = await inquirer.prompt({
175
- type: 'password',
176
- name: 'privateKey',
177
- message: 'Private-key:',
178
- mask: '*',
176
+ if (accountAction === 'Export Options') {
177
+ const exportChoices = ['Export Private-key', 'Export HODL File', 'Go Back'];
178
+ const { exportAction } = await inquirer.prompt({
179
+ type: 'list',
180
+ name: 'exportAction',
181
+ message: 'Select an export option:',
182
+ choices: exportChoices,
179
183
  });
180
184
 
181
- if (!privateKey.trim()) {
182
- Wallet.displayError('Private-key is empty.');
183
- return;
185
+ if (exportAction === 'Go Back') {
186
+ return this.loadAccount(forceReload);
184
187
  }
185
188
 
186
- try {
187
- this.account = this.web3.eth.accounts.privateKeyToAccount(privateKey);
188
- await this.db.secureSet('account', this.account);
189
- this.displayAccountAddress();
190
- } catch (error) {
191
- Wallet.displayError('Invalid private-key.');
189
+ switch (exportAction) {
190
+ case 'Export Private-key':
191
+ await this.displayAccountDetails();
192
+ break;
193
+ case 'Export HODL File':
194
+ await this.exportHODLFile();
195
+ break;
192
196
  }
197
+ return;
193
198
  }
194
199
 
195
- if (accountAction === 'Import Mnemonic (12 words)') {
196
- account = await this.importFrom12Words();
197
- if (!account) {
198
- Wallet.displayError('Invalid mnemonic.');
200
+
201
+ if (accountAction === 'Create New Account') {
202
+ if (!await this.confirmOverwrite()) {
199
203
  return;
200
204
  }
201
- this.account = account;
202
- this.displayAccountAddress();
203
- }
204
205
 
205
- if (accountAction === 'Export Account') {
206
- await this.displayAccountDetails();
206
+ await this.createNewAccount();
207
207
  }
208
208
 
209
209
  if (accountAction === 'Switch Network') {
@@ -216,6 +216,20 @@ class Wallet {
216
216
  this.account = account;
217
217
  }
218
218
 
219
+ async confirmOverwrite() {
220
+ if (this.account) {
221
+ const { confirmOverwrite } = await inquirer.prompt({
222
+ type: 'confirm',
223
+ name: 'confirmOverwrite',
224
+ message: 'This action will overwrite the existing account. Are you sure you want to continue?',
225
+ default: false,
226
+ });
227
+
228
+ return confirmOverwrite;
229
+ }
230
+ return false;
231
+ }
232
+
219
233
  async importFrom12Words() {
220
234
  const { mnemonic } = await inquirer.prompt({
221
235
  type: 'password',
@@ -228,13 +242,7 @@ class Wallet {
228
242
  return null;
229
243
  }
230
244
 
231
- const seed = await bip39.mnemonicToSeed(mnemonic);
232
- const root = hdkey.fromMasterSeed(seed);
233
- const addrNode = root.derive("m/44'/60'/0'/0/0");
234
- const privateKey = addrNode.privateKey.toString('hex');
235
- const account = this.web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
236
-
237
- account.mnemonic = mnemonic;
245
+ const account = await this.accountFromMnemonic(mnemonic);
238
246
  await this.db.secureSet('account', account);
239
247
  return account;
240
248
  }
@@ -329,7 +337,7 @@ class Wallet {
329
337
  const { amount } = await inquirer.prompt({
330
338
  type: 'input',
331
339
  name: 'amount',
332
- message: `Amount to transfer [${token}] (leave empty to cancel):`,
340
+ message: `Amount to transfer:`,
333
341
  validate: value => {
334
342
  if (value === '') return true;
335
343
  return !isNaN(value) && Number(value) > 0 ? true : 'Please enter a valid number or leave empty to cancel.';
@@ -340,6 +348,18 @@ class Wallet {
340
348
  return;
341
349
  }
342
350
 
351
+ // Add confirmation step
352
+ const { confirmTransaction } = await inquirer.prompt({
353
+ type: 'confirm',
354
+ name: 'confirmTransaction',
355
+ message: `Confirm transfer?`,
356
+ default: true
357
+ });
358
+
359
+ if (!confirmTransaction) {
360
+ return;
361
+ }
362
+
343
363
  try {
344
364
  let signedTx;
345
365
  if (tokens[token] && token !== this.selectedNetwork.nativeToken) {
@@ -585,6 +605,134 @@ class Wallet {
585
605
  await this.selectNetwork(networkPlugins);
586
606
  this.web3 = new Web3(this.selectedNetwork.rpcUrl);
587
607
  }
608
+
609
+ async exportHODLFile() {
610
+ const defaultFileName = `${this.account.address.slice(-6).toUpperCase()}`;
611
+ let { fileName } = await inquirer.prompt({
612
+ type: 'input',
613
+ name: 'fileName',
614
+ message: 'Enter the name for the HODL file:',
615
+ default: defaultFileName
616
+ });
617
+
618
+ fileName += '.HODL';
619
+
620
+ const data = this.db.get();
621
+ const encryptedData = this.db.encrypt(data);
622
+
623
+ fs.writeFileSync(fileName, encryptedData);
624
+
625
+ const table = new Table({
626
+ head: ['HODL File Exported'],
627
+ style: { head: ['green'] }
628
+ });
629
+ table.push([`File saved as: ${fileName}`]);
630
+ console.log(table.toString());
631
+ }
632
+
633
+ async importHODLFile() {
634
+ const { filePath } = await inquirer.prompt({
635
+ type: 'fuzzypath',
636
+ name: 'filePath',
637
+ message: 'Select the .HODL file:',
638
+ rootPath: '.',
639
+ itemType: 'file',
640
+ suggestOnly: false,
641
+ depthLimit: 5,
642
+ excludePath: nodePath => nodePath.startsWith('node_modules'),
643
+ excludeFilter: nodePath => !nodePath.endsWith('.HODL'),
644
+ });
645
+
646
+ if (!fs.existsSync(filePath)) {
647
+ Wallet.displayError('File not found.');
648
+ return;
649
+ }
650
+
651
+ const encryptedData = fs.readFileSync(filePath, 'utf8');
652
+
653
+ try {
654
+ const decryptedData = this.db.decrypt(encryptedData);
655
+ this.db.set(decryptedData);
656
+ return this.db.secureGet('account');
657
+ } catch (error) {
658
+ Wallet.displayError('Failed to import HODL file.', 'The file may be corrupted or the encryption key is incorrect.');
659
+ }
660
+ }
661
+
662
+ async importPrivateKey() {
663
+ const { privateKey } = await inquirer.prompt({
664
+ type: 'password',
665
+ name: 'privateKey',
666
+ message: 'Private-key:',
667
+ mask: '*',
668
+ });
669
+
670
+ if (!privateKey.trim()) {
671
+ Wallet.displayError('Private-key is empty.');
672
+ return;
673
+ }
674
+
675
+ try {
676
+ this.account = this.web3.eth.accounts.privateKeyToAccount(privateKey);
677
+ await this.db.secureSet('account', this.account);
678
+ this.displayAccountAddress();
679
+ } catch (error) {
680
+ Wallet.displayError('Invalid private-key.');
681
+ }
682
+ }
683
+
684
+ async createNewAccount() {
685
+ const { createWithMnemonic } = await inquirer.prompt({
686
+ type: 'confirm',
687
+ name: 'createWithMnemonic',
688
+ message: 'Create account with mnemonic?',
689
+ default: true
690
+ });
691
+
692
+ let message = 'Do you want to display sensitive information (private key';
693
+ if (createWithMnemonic) {
694
+ this.account = await this.createAccountFromMnemonic();
695
+ message += ' and mnemonic';
696
+ } else {
697
+ this.account = this.web3.eth.accounts.create();
698
+ }
699
+
700
+ message += ')?';
701
+
702
+ await this.db.secureSet('account', this.account);
703
+
704
+ const { showSensitive } = await inquirer.prompt({
705
+ type: 'confirm',
706
+ name: 'showSensitive',
707
+ message,
708
+ default: false,
709
+ });
710
+
711
+ if (showSensitive) {
712
+ this.displayAccountDetails();
713
+ } else {
714
+ this.displayAccountAddress();
715
+ }
716
+ }
717
+
718
+ async accountFromMnemonic(mnemonic) {
719
+ const seed = await bip39.mnemonicToSeed(mnemonic);
720
+ const root = hdkey.fromMasterSeed(seed);
721
+ const addrNode = root.derive("m/44'/60'/0'/0/0");
722
+ const privateKey = addrNode.privateKey.toString('hex');
723
+ const account = this.web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
724
+ account.mnemonic = mnemonic;
725
+ return account;
726
+ }
727
+
728
+ async createAccountFromMnemonic() {
729
+ try {
730
+ const mnemonic = bip39.generateMnemonic();
731
+ return this.accountFromMnemonic(mnemonic);
732
+ } catch (error) {
733
+ Wallet.displayError('Failed to create account from mnemonic.', error);
734
+ }
735
+ }
588
736
  }
589
737
 
590
738
  class UIManager {
package/network/arb.js ADDED
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ name: '[ERC-20] Arbitrum One',
3
+ explorer: 'https://arbiscan.io/tx/',
4
+ rpcUrl: 'https://arb1.arbitrum.io/rpc',
5
+ nativeToken: 'ETH',
6
+ tokens: {
7
+ 'USDT': '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', // USDT on Arbitrum
8
+ 'ARB': '0x0000000000000000000000000000000000000000', // Native ETH on Arbitrum
9
+ },
10
+ };
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ name: '[ERC-20] Avalanche C-Chain',
3
+ explorer: 'https://snowtrace.io/tx/',
4
+ rpcUrl: 'https://api.avax.network/ext/bc/C/rpc',
5
+ nativeToken: 'AVAX',
6
+ tokens: {
7
+ 'USDT': '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7', // USDT.e on Avalanche C-Chain
8
+ 'AVAX': '0x0000000000000000000000000000000000000000', // Native AVAX
9
+ },
10
+ };
package/network/ftm.js ADDED
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ name: '[ERC-20] Fantom',
3
+ explorer: 'https://ftmscan.com/tx/',
4
+ rpcUrl: 'https://rpc.ftm.tools/',
5
+ nativeToken: 'FTM',
6
+ tokens: {
7
+ 'USDT': '0x049d68029688eAbF473097a2fC38ef61633A3C7A', // USDT on Fantom
8
+ 'FTM': '0x0000000000000000000000000000000000000000', // Native FTM
9
+ },
10
+ };
package/network/op.js ADDED
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ name: '[ERC-20] Optimism',
3
+ explorer: 'https://optimistic.etherscan.io/tx/',
4
+ rpcUrl: 'https://mainnet.optimism.io',
5
+ nativeToken: 'ETH',
6
+ tokens: {
7
+ 'USDT': '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58', // USDT on Optimism
8
+ 'OP': '0x0000000000000000000000000000000000000000',
9
+ },
10
+ };
package/network/pol.js ADDED
@@ -0,0 +1,11 @@
1
+ // network/matic.js
2
+ module.exports = {
3
+ name: '[ERC-20] Polygon',
4
+ explorer: 'https://polygonscan.com/tx/',
5
+ rpcUrl: 'https://polygon-rpc.com/',
6
+ nativeToken: 'POL',
7
+ tokens: {
8
+ 'USDT': '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
9
+ 'POL': '0x0000000000000000000000000000000000000000',
10
+ },
11
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hodl-wallet",
3
- "version": "1.4.4",
3
+ "version": "1.5.2",
4
4
  "description": "🧊 HODL Wallet - Fast CLI crypto wallet!",
5
5
  "author": "Martin Clasen",
6
6
  "repository": {
@@ -37,16 +37,24 @@
37
37
  "stable",
38
38
  "coin",
39
39
  "evm",
40
+ "polygon",
41
+ "matic",
42
+ "arbitrum",
43
+ "fantom",
44
+ "optimism",
45
+ "avalanche",
46
+ "avax",
40
47
  "clasen"
41
48
  ],
42
49
  "dependencies": {
43
50
  "bip39": "^3.1.0",
44
51
  "cli-table3": "^0.6.5",
45
52
  "crypto-js": "^4.2.0",
46
- "deepbase": "^1.2.2",
53
+ "deepbase": "^1.2.4",
47
54
  "hdkey": "^2.1.0",
48
55
  "inquirer": "^9.3.7",
49
56
  "inquirer-autocomplete-prompt": "^3.0.1",
57
+ "inquirer-fuzzy-path": "^2.3.0",
50
58
  "web3": "^4.13.0"
51
59
  }
52
- }
60
+ }
File without changes