hodl-wallet 1.4.4 โ 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 +206 -80
- 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,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();
|
|
@@ -97,113 +99,101 @@ class Wallet {
|
|
|
97
99
|
async loadAccount(forceReload = false) {
|
|
98
100
|
let account = this.db.secureGet('account');
|
|
99
101
|
|
|
100
|
-
const
|
|
102
|
+
const mainChoices = ['Create New Account'];
|
|
101
103
|
|
|
102
104
|
if (forceReload) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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');
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
if (!account || forceReload) {
|
|
109
|
-
|
|
116
|
+
let { accountAction } = await inquirer.prompt({
|
|
110
117
|
type: 'list',
|
|
111
118
|
name: 'accountAction',
|
|
112
119
|
message: 'Select an account option:',
|
|
113
|
-
choices,
|
|
120
|
+
choices: mainChoices,
|
|
114
121
|
});
|
|
115
122
|
|
|
116
123
|
if (accountAction === 'Go Back') {
|
|
117
124
|
return;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
if (
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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,
|
|
126
134
|
});
|
|
127
135
|
|
|
128
|
-
if (
|
|
129
|
-
return;
|
|
136
|
+
if (importAction === 'Go Back') {
|
|
137
|
+
return this.loadAccount(forceReload);
|
|
130
138
|
}
|
|
131
|
-
}
|
|
132
|
-
|
|
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
139
|
|
|
141
|
-
|
|
142
|
-
|
|
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();
|
|
140
|
+
if (!await this.confirmOverwrite()) {
|
|
141
|
+
return;
|
|
153
142
|
}
|
|
154
143
|
|
|
155
|
-
|
|
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
|
-
});
|
|
144
|
+
accountAction = importAction;
|
|
145
|
+
}
|
|
165
146
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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;
|
|
169
155
|
this.displayAccountAddress();
|
|
170
|
-
|
|
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;
|
|
171
164
|
}
|
|
172
165
|
|
|
173
|
-
if (accountAction === '
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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,
|
|
179
173
|
});
|
|
180
174
|
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
return;
|
|
175
|
+
if (exportAction === 'Go Back') {
|
|
176
|
+
return this.loadAccount(forceReload);
|
|
184
177
|
}
|
|
185
178
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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;
|
|
192
186
|
}
|
|
187
|
+
return;
|
|
193
188
|
}
|
|
194
189
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (!
|
|
198
|
-
Wallet.displayError('Invalid mnemonic.');
|
|
190
|
+
|
|
191
|
+
if (accountAction === 'Create New Account') {
|
|
192
|
+
if (!await this.confirmOverwrite()) {
|
|
199
193
|
return;
|
|
200
194
|
}
|
|
201
|
-
this.account = account;
|
|
202
|
-
this.displayAccountAddress();
|
|
203
|
-
}
|
|
204
195
|
|
|
205
|
-
|
|
206
|
-
await this.displayAccountDetails();
|
|
196
|
+
await this.createNewAccount();
|
|
207
197
|
}
|
|
208
198
|
|
|
209
199
|
if (accountAction === 'Switch Network') {
|
|
@@ -216,6 +206,20 @@ class Wallet {
|
|
|
216
206
|
this.account = account;
|
|
217
207
|
}
|
|
218
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
|
+
|
|
219
223
|
async importFrom12Words() {
|
|
220
224
|
const { mnemonic } = await inquirer.prompt({
|
|
221
225
|
type: 'password',
|
|
@@ -228,13 +232,7 @@ class Wallet {
|
|
|
228
232
|
return null;
|
|
229
233
|
}
|
|
230
234
|
|
|
231
|
-
const
|
|
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;
|
|
235
|
+
const account = await this.accountFromMnemonic(mnemonic);
|
|
238
236
|
await this.db.secureSet('account', account);
|
|
239
237
|
return account;
|
|
240
238
|
}
|
|
@@ -585,6 +583,134 @@ class Wallet {
|
|
|
585
583
|
await this.selectNetwork(networkPlugins);
|
|
586
584
|
this.web3 = new Web3(this.selectedNetwork.rpcUrl);
|
|
587
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
|
+
}
|
|
588
714
|
}
|
|
589
715
|
|
|
590
716
|
class UIManager {
|
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
|