hodl-wallet 1.4.2 โ†’ 1.4.6

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,18 @@ 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 both BEP-20 (Binance Smart Chain) and ERC-20 (Ethereum and Polygon) networks. Switch between networks with ease!
45
+
46
+ ### ๐Ÿ’พ Export and Import HODL Files
47
+
48
+ HODL Wallet now supports exporting and importing encrypted .HODL files, which securely store your wallet information.
49
+
50
+ - **Export HODL File**: Save your wallet data (including private keys and addresses) to an encrypted .HODL file.
51
+ - **Import HODL File**: Restore your wallet from a previously exported .HODL file.
52
+
53
+ These files are encrypted using your wallet password, providing an additional layer of security for storing and transferring your wallet information.
54
+
55
+ 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
56
 
46
57
  ## ๐Ÿ”’ Security
47
58
 
@@ -65,6 +76,7 @@ We've carefully selected trusted and well-maintained dependencies for this proje
65
76
  - **deepbase**: For persistent storage.
66
77
  - **crypto-js**: For encryption.
67
78
  - **inquirer** and **inquirer-autocomplete-prompt**: For interactive command-line interfaces.
79
+ - **inquirer-fuzzy-path**: For fuzzy searching and selecting file paths during HODL file import.
68
80
  - **cli-table3**: For creating formatted CLI tables.
69
81
  - **bip39**: For generating and handling mnemonic phrases.
70
82
  - **hdkey**: For handling hierarchical deterministic (HD) keys.
package/index.mjs CHANGED
@@ -11,12 +11,18 @@ 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);
22
+
23
+ process.on('SIGINT', () => {
24
+ process.exit();
25
+ });
20
26
 
21
27
  class Wallet {
22
28
  constructor(encryptionKey) {
@@ -93,113 +99,101 @@ class Wallet {
93
99
  async loadAccount(forceReload = false) {
94
100
  let account = this.db.secureGet('account');
95
101
 
96
- const choices = ['Create New Account', 'Import Mnemonic (12 words)', 'Import Private-key'];
102
+ const mainChoices = ['Create New Account'];
97
103
 
98
104
  if (forceReload) {
99
- choices.push('Export Account');
100
- choices.push('Switch Network');
101
- choices.push('Go Back');
105
+ mainChoices.push('Import Options');
106
+ mainChoices.push('Export Options');
107
+ mainChoices.push('Switch Network');
108
+ mainChoices.push('Go Back');
109
+ } else {
110
+ mainChoices.push('Import Mnemonic (12 words)');
111
+ mainChoices.push('Import Private-key');
112
+ mainChoices.push('Import HODL File');
102
113
  }
103
114
 
104
115
  if (!account || forceReload) {
105
- const { accountAction } = await inquirer.prompt({
116
+ let { accountAction } = await inquirer.prompt({
106
117
  type: 'list',
107
118
  name: 'accountAction',
108
119
  message: 'Select an account option:',
109
- choices,
120
+ choices: mainChoices,
110
121
  });
111
122
 
112
123
  if (accountAction === 'Go Back') {
113
124
  return;
114
125
  }
115
126
 
116
- if (account && accountAction !== 'Export Account' && accountAction !== 'Switch Network') {
117
- const { confirmOverwrite } = await inquirer.prompt({
118
- type: 'confirm',
119
- name: 'confirmOverwrite',
120
- message: 'This action will overwrite the existing account. Are you sure you want to continue?',
121
- default: false,
127
+ if (accountAction === 'Import Options') {
128
+ const importChoices = ['Import Mnemonic (12 words)', 'Import Private-key', 'Import HODL File', 'Go Back'];
129
+ const { importAction } = await inquirer.prompt({
130
+ type: 'list',
131
+ name: 'importAction',
132
+ message: 'Select an import option:',
133
+ choices: importChoices,
122
134
  });
123
135
 
124
- if (!confirmOverwrite) {
125
- return;
136
+ if (importAction === 'Go Back') {
137
+ return this.loadAccount(forceReload);
126
138
  }
127
- }
128
139
 
129
- if (accountAction === 'Create New Account') {
130
- const { createWithMnemonic } = await inquirer.prompt({
131
- type: 'confirm',
132
- name: 'createWithMnemonic',
133
- message: 'Create account with mnemonic?',
134
- default: true
135
- });
136
-
137
- let message = 'Do you want to display sensitive information (private key';
138
- if (createWithMnemonic) {
139
- const mnemonic = bip39.generateMnemonic();
140
- const seed = await bip39.mnemonicToSeed(mnemonic);
141
- const root = hdkey.fromMasterSeed(seed);
142
- const addrNode = root.derive("m/44'/60'/0'/0/0");
143
- const privateKey = addrNode.privateKey.toString('hex');
144
- this.account = this.web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
145
- this.account.mnemonic = mnemonic;
146
- message += ' and mnemonic';
147
- } else {
148
- this.account = this.web3.eth.accounts.create();
140
+ if (!await this.confirmOverwrite()) {
141
+ return;
149
142
  }
150
143
 
151
- message += ')?';
152
-
153
- await this.db.secureSet('account', this.account);
154
-
155
- const { showSensitive } = await inquirer.prompt({
156
- type: 'confirm',
157
- name: 'showSensitive',
158
- message,
159
- default: false,
160
- });
144
+ accountAction = importAction;
145
+ }
161
146
 
162
- if (showSensitive) {
163
- this.displayAccountDetails();
164
- } else {
147
+ switch (accountAction) {
148
+ case 'Import Mnemonic (12 words)':
149
+ account = await this.importFrom12Words();
150
+ if (!account) {
151
+ Wallet.displayError('Invalid mnemonic.');
152
+ return;
153
+ }
154
+ this.account = account;
165
155
  this.displayAccountAddress();
166
- }
156
+ break;
157
+ case 'Import Private-key':
158
+ await this.importPrivateKey();
159
+ break;
160
+ case 'Import HODL File':
161
+ this.account = await this.importHODLFile();
162
+ this.displayAccountAddress();
163
+ break;
167
164
  }
168
165
 
169
- if (accountAction === 'Import Private-key') {
170
- const { privateKey } = await inquirer.prompt({
171
- type: 'password',
172
- name: 'privateKey',
173
- message: 'Private-key:',
174
- mask: '*',
166
+ if (accountAction === 'Export Options') {
167
+ const exportChoices = ['Export Private-key', 'Export HODL File', 'Go Back'];
168
+ const { exportAction } = await inquirer.prompt({
169
+ type: 'list',
170
+ name: 'exportAction',
171
+ message: 'Select an export option:',
172
+ choices: exportChoices,
175
173
  });
176
174
 
177
- if (!privateKey.trim()) {
178
- Wallet.displayError('Private-key is empty.');
179
- return;
175
+ if (exportAction === 'Go Back') {
176
+ return this.loadAccount(forceReload);
180
177
  }
181
178
 
182
- try {
183
- this.account = this.web3.eth.accounts.privateKeyToAccount(privateKey);
184
- await this.db.secureSet('account', this.account);
185
- this.displayAccountAddress();
186
- } catch (error) {
187
- Wallet.displayError('Invalid private-key.');
179
+ switch (exportAction) {
180
+ case 'Export Private-key':
181
+ await this.displayAccountDetails();
182
+ break;
183
+ case 'Export HODL File':
184
+ await this.exportHODLFile();
185
+ break;
188
186
  }
187
+ return;
189
188
  }
190
189
 
191
- if (accountAction === 'Import Mnemonic (12 words)') {
192
- account = await this.importFrom12Words();
193
- if (!account) {
194
- Wallet.displayError('Invalid mnemonic.');
190
+
191
+ if (accountAction === 'Create New Account') {
192
+ if (!await this.confirmOverwrite()) {
195
193
  return;
196
194
  }
197
- this.account = account;
198
- this.displayAccountAddress();
199
- }
200
195
 
201
- if (accountAction === 'Export Account') {
202
- await this.displayAccountDetails();
196
+ await this.createNewAccount();
203
197
  }
204
198
 
205
199
  if (accountAction === 'Switch Network') {
@@ -212,6 +206,20 @@ class Wallet {
212
206
  this.account = account;
213
207
  }
214
208
 
209
+ async confirmOverwrite() {
210
+ if (this.account) {
211
+ const { confirmOverwrite } = await inquirer.prompt({
212
+ type: 'confirm',
213
+ name: 'confirmOverwrite',
214
+ message: 'This action will overwrite the existing account. Are you sure you want to continue?',
215
+ default: false,
216
+ });
217
+
218
+ return confirmOverwrite;
219
+ }
220
+ return false;
221
+ }
222
+
215
223
  async importFrom12Words() {
216
224
  const { mnemonic } = await inquirer.prompt({
217
225
  type: 'password',
@@ -224,13 +232,7 @@ class Wallet {
224
232
  return null;
225
233
  }
226
234
 
227
- const seed = await bip39.mnemonicToSeed(mnemonic);
228
- const root = hdkey.fromMasterSeed(seed);
229
- const addrNode = root.derive("m/44'/60'/0'/0/0");
230
- const privateKey = addrNode.privateKey.toString('hex');
231
- const account = this.web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
232
-
233
- account.mnemonic = mnemonic;
235
+ const account = await this.accountFromMnemonic(mnemonic);
234
236
  await this.db.secureSet('account', account);
235
237
  return account;
236
238
  }
@@ -581,6 +583,134 @@ class Wallet {
581
583
  await this.selectNetwork(networkPlugins);
582
584
  this.web3 = new Web3(this.selectedNetwork.rpcUrl);
583
585
  }
586
+
587
+ async exportHODLFile() {
588
+ const defaultFileName = `${this.account.address.slice(-6).toUpperCase()}`;
589
+ let { fileName } = await inquirer.prompt({
590
+ type: 'input',
591
+ name: 'fileName',
592
+ message: 'Enter the name for the HODL file:',
593
+ default: defaultFileName
594
+ });
595
+
596
+ fileName += '.HODL';
597
+
598
+ const data = this.db.get();
599
+ const encryptedData = this.db.encrypt(data);
600
+
601
+ fs.writeFileSync(fileName, encryptedData);
602
+
603
+ const table = new Table({
604
+ head: ['HODL File Exported'],
605
+ style: { head: ['green'] }
606
+ });
607
+ table.push([`File saved as: ${fileName}`]);
608
+ console.log(table.toString());
609
+ }
610
+
611
+ async importHODLFile() {
612
+ const { filePath } = await inquirer.prompt({
613
+ type: 'fuzzypath',
614
+ name: 'filePath',
615
+ message: 'Select the .HODL file:',
616
+ rootPath: '.',
617
+ itemType: 'file',
618
+ suggestOnly: false,
619
+ depthLimit: 5,
620
+ excludePath: nodePath => nodePath.startsWith('node_modules'),
621
+ excludeFilter: nodePath => !nodePath.endsWith('.HODL'),
622
+ });
623
+
624
+ if (!fs.existsSync(filePath)) {
625
+ Wallet.displayError('File not found.');
626
+ return;
627
+ }
628
+
629
+ const encryptedData = fs.readFileSync(filePath, 'utf8');
630
+
631
+ try {
632
+ const decryptedData = this.db.decrypt(encryptedData);
633
+ this.db.set(decryptedData);
634
+ return this.db.secureGet('account');
635
+ } catch (error) {
636
+ Wallet.displayError('Failed to import HODL file.', 'The file may be corrupted or the encryption key is incorrect.');
637
+ }
638
+ }
639
+
640
+ async importPrivateKey() {
641
+ const { privateKey } = await inquirer.prompt({
642
+ type: 'password',
643
+ name: 'privateKey',
644
+ message: 'Private-key:',
645
+ mask: '*',
646
+ });
647
+
648
+ if (!privateKey.trim()) {
649
+ Wallet.displayError('Private-key is empty.');
650
+ return;
651
+ }
652
+
653
+ try {
654
+ this.account = this.web3.eth.accounts.privateKeyToAccount(privateKey);
655
+ await this.db.secureSet('account', this.account);
656
+ this.displayAccountAddress();
657
+ } catch (error) {
658
+ Wallet.displayError('Invalid private-key.');
659
+ }
660
+ }
661
+
662
+ async createNewAccount() {
663
+ const { createWithMnemonic } = await inquirer.prompt({
664
+ type: 'confirm',
665
+ name: 'createWithMnemonic',
666
+ message: 'Create account with mnemonic?',
667
+ default: true
668
+ });
669
+
670
+ let message = 'Do you want to display sensitive information (private key';
671
+ if (createWithMnemonic) {
672
+ this.account = await this.createAccountFromMnemonic();
673
+ message += ' and mnemonic';
674
+ } else {
675
+ this.account = this.web3.eth.accounts.create();
676
+ }
677
+
678
+ message += ')?';
679
+
680
+ await this.db.secureSet('account', this.account);
681
+
682
+ const { showSensitive } = await inquirer.prompt({
683
+ type: 'confirm',
684
+ name: 'showSensitive',
685
+ message,
686
+ default: false,
687
+ });
688
+
689
+ if (showSensitive) {
690
+ this.displayAccountDetails();
691
+ } else {
692
+ this.displayAccountAddress();
693
+ }
694
+ }
695
+
696
+ async accountFromMnemonic(mnemonic) {
697
+ const seed = await bip39.mnemonicToSeed(mnemonic);
698
+ const root = hdkey.fromMasterSeed(seed);
699
+ const addrNode = root.derive("m/44'/60'/0'/0/0");
700
+ const privateKey = addrNode.privateKey.toString('hex');
701
+ const account = this.web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
702
+ account.mnemonic = mnemonic;
703
+ return account;
704
+ }
705
+
706
+ async createAccountFromMnemonic() {
707
+ try {
708
+ const mnemonic = bip39.generateMnemonic();
709
+ return this.accountFromMnemonic(mnemonic);
710
+ } catch (error) {
711
+ Wallet.displayError('Failed to create account from mnemonic.', error);
712
+ }
713
+ }
584
714
  }
585
715
 
586
716
  class UIManager {
@@ -681,7 +811,6 @@ try {
681
811
  process.exit(1);
682
812
  }
683
813
 
684
- // Register the displayExitPhrase method and account clearing to be called on process exit
685
814
  process.on('exit', () => {
686
815
  wallet.clearAccountData();
687
816
  UIManager.displayExitPhrase();
@@ -718,8 +847,3 @@ while (true) {
718
847
  process.exit();
719
848
  }
720
849
  }
721
-
722
- process.on('SIGINT', () => {
723
- console.log('\n');
724
- process.exit();
725
- });
package/network/pol.js ADDED
@@ -0,0 +1,10 @@
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', // Direcciรณn del contrato USDT en Polygon
9
+ },
10
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hodl-wallet",
3
- "version": "1.4.2",
3
+ "version": "1.4.6",
4
4
  "description": "๐ŸงŠ HODL Wallet - Fast CLI crypto wallet!",
5
5
  "author": "Martin Clasen",
6
6
  "repository": {
@@ -37,16 +37,20 @@
37
37
  "stable",
38
38
  "coin",
39
39
  "evm",
40
+ "polygon",
41
+ "matic",
42
+ "pol",
40
43
  "clasen"
41
44
  ],
42
45
  "dependencies": {
43
46
  "bip39": "^3.1.0",
44
47
  "cli-table3": "^0.6.5",
45
48
  "crypto-js": "^4.2.0",
46
- "deepbase": "^1.2.2",
49
+ "deepbase": "^1.2.4",
47
50
  "hdkey": "^2.1.0",
48
51
  "inquirer": "^9.3.7",
49
52
  "inquirer-autocomplete-prompt": "^3.0.1",
53
+ "inquirer-fuzzy-path": "^2.3.0",
50
54
  "web3": "^4.13.0"
51
55
  }
52
56
  }
File without changes