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 +13 -1
- package/index.mjs +210 -86
- package/network/pol.js +10 -0
- package/package.json +6 -2
- /package/network/{erc.js โ eth.js} +0 -0
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
|
|
102
|
+
const mainChoices = ['Create New Account'];
|
|
97
103
|
|
|
98
104
|
if (forceReload) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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 (
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 (
|
|
125
|
-
return;
|
|
136
|
+
if (importAction === 'Go Back') {
|
|
137
|
+
return this.loadAccount(forceReload);
|
|
126
138
|
}
|
|
127
|
-
}
|
|
128
139
|
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 === '
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 (
|
|
178
|
-
|
|
179
|
-
return;
|
|
175
|
+
if (exportAction === 'Go Back') {
|
|
176
|
+
return this.loadAccount(forceReload);
|
|
180
177
|
}
|
|
181
178
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|