hodl-wallet 1.8.4 โ†’ 1.8.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.
package/index.js CHANGED
@@ -8,7 +8,6 @@ import { fileURLToPath } from 'url';
8
8
  import { dirname } from 'path';
9
9
  import Table from 'cli-table3';
10
10
  import os from 'os';
11
- import inquirerFuzzyPath from 'inquirer-fuzzy-path';
12
11
  import ora from 'ora';
13
12
 
14
13
  const __filename = fileURLToPath(import.meta.url);
@@ -16,7 +15,6 @@ const __dirname = dirname(__filename);
16
15
 
17
16
  import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
18
17
  inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
19
- inquirer.registerPrompt('fuzzypath', inquirerFuzzyPath);
20
18
 
21
19
  process.on('SIGINT', () => {
22
20
  process.exit();
@@ -39,20 +37,20 @@ class Wallet {
39
37
 
40
38
  formatAmount(num) {
41
39
  num = parseFloat(num);
42
-
40
+
43
41
  // Handle integers - add .00
44
42
  if (num === Math.floor(num)) {
45
43
  return num.toString() + '.00';
46
44
  }
47
-
45
+
48
46
  // For decimals, format to max 3 decimal places, then remove trailing zeros
49
47
  let formatted = num.toFixed(3);
50
-
48
+
51
49
  // Remove trailing zeros, but keep at least 2 decimal places
52
50
  while (formatted.endsWith('0') && formatted.split('.')[1].length > 2) {
53
51
  formatted = formatted.slice(0, -1);
54
52
  }
55
-
53
+
56
54
  return formatted;
57
55
  }
58
56
 
@@ -84,6 +82,9 @@ class Wallet {
84
82
  }
85
83
 
86
84
  setAccount(account) {
85
+ if (!account) {
86
+ return;
87
+ }
87
88
  this.db.set('account', this.network.constructor.name, account);
88
89
  if (account.mnemonic) {
89
90
  this.db.set('mnemonic', account.mnemonic);
@@ -92,8 +93,8 @@ class Wallet {
92
93
 
93
94
  async getAccount() {
94
95
  const account = this.db.get('account', this.network.constructor.name);
95
-
96
-
96
+
97
+
97
98
  return account;
98
99
  }
99
100
 
@@ -113,7 +114,7 @@ class Wallet {
113
114
  head: ['green']
114
115
  }
115
116
  });
116
-
117
+
117
118
  table.push([await this.getAddress()]);
118
119
  console.log(table.toString());
119
120
  }
@@ -135,18 +136,18 @@ class Wallet {
135
136
  const sortedNetworks = networkPlugins.sort((a, b) => {
136
137
  const aUsage = this.networkUsage[a.name];
137
138
  const bUsage = this.networkUsage[b.name];
138
-
139
+
139
140
  // Handle old format (number) vs new format (object)
140
141
  const aLastUsed = typeof aUsage === 'object' ? aUsage.lastUsed || 0 : 0;
141
142
  const bLastUsed = typeof bUsage === 'object' ? bUsage.lastUsed || 0 : 0;
142
-
143
+
143
144
  // If neither has lastUsed timestamp, sort by old count format
144
145
  if (aLastUsed === 0 && bLastUsed === 0) {
145
146
  const aCount = typeof aUsage === 'number' ? aUsage : (aUsage?.count || 0);
146
147
  const bCount = typeof bUsage === 'number' ? bUsage : (bUsage?.count || 0);
147
148
  return bCount - aCount;
148
149
  }
149
-
150
+
150
151
  return bLastUsed - aLastUsed;
151
152
  });
152
153
 
@@ -169,19 +170,19 @@ class Wallet {
169
170
  // Update usage info for the selected network
170
171
  // Handle migration from old format (number) to new format (object)
171
172
  const currentUsage = this.networkUsage[selectedNetwork.name];
172
-
173
+
173
174
  if (!currentUsage || typeof currentUsage === 'number') {
174
175
  // Old format (number) or doesn't exist - create new object
175
- this.networkUsage[selectedNetwork.name] = {
176
- count: typeof currentUsage === 'number' ? currentUsage + 1 : 1,
177
- lastUsed: Date.now()
176
+ this.networkUsage[selectedNetwork.name] = {
177
+ count: typeof currentUsage === 'number' ? currentUsage + 1 : 1,
178
+ lastUsed: Date.now()
178
179
  };
179
180
  } else {
180
181
  // New format (object) - update values
181
182
  this.networkUsage[selectedNetwork.name].count = (currentUsage.count || 0) + 1;
182
183
  this.networkUsage[selectedNetwork.name].lastUsed = Date.now();
183
184
  }
184
-
185
+
185
186
  await this.db.set('networkUsage', this.networkUsage);
186
187
 
187
188
  this.selectedNetwork = selectedNetwork;
@@ -199,9 +200,9 @@ class Wallet {
199
200
  mainChoices.push('Manage Address Book');
200
201
  mainChoices.push('Go Back');
201
202
  } else {
202
- mainChoices.push('Import Mnemonic (12 words)');
203
- mainChoices.push('Import Private-key');
204
203
  mainChoices.push('Import HODL File');
204
+ mainChoices.push('Import Mnemonic (12 or 24 words)');
205
+ mainChoices.push('Import Private-key');
205
206
  }
206
207
 
207
208
  if (!account || loggedIn) {
@@ -238,7 +239,7 @@ class Wallet {
238
239
  }
239
240
 
240
241
  if (accountAction === 'Import Options') {
241
- const importChoices = ['Import Mnemonic (12 words)', 'Import Private-key', 'Import HODL File', 'Go Back'];
242
+ const importChoices = ['Import HODL File', 'Import Mnemonic (12 or 24 words)', 'Import Private-key', 'Go Back'];
242
243
  const { importAction } = await inquirer.prompt({
243
244
  type: 'list',
244
245
  name: 'importAction',
@@ -257,7 +258,7 @@ class Wallet {
257
258
  }
258
259
 
259
260
  switch (accountAction) {
260
- case 'Import Mnemonic (12 words)':
261
+ case 'Import Mnemonic (12 or 24 words)':
261
262
  account = await this.importFromMnemonic();
262
263
  if (!account) {
263
264
  // Wallet.displayError('Invalid mnemonic.');
@@ -270,13 +271,16 @@ class Wallet {
270
271
  await this.importPrivateKey();
271
272
  break;
272
273
  case 'Import HODL File':
273
- this.setAccount(await this.importHODLFile());
274
- await this.displayAccountAddress();
274
+ const importedAccount = await this.importHODLFile();
275
+ if (importedAccount) {
276
+ this.setAccount(importedAccount);
277
+ await this.displayAccountAddress();
278
+ }
275
279
  break;
276
280
  }
277
281
 
278
282
  if (accountAction === 'Export Options') {
279
- const exportChoices = ['Export Private-key', 'Export HODL File', 'Go Back'];
283
+ const exportChoices = ['Export HODL File', 'Export Private-key', 'Go Back'];
280
284
  const { exportAction } = await inquirer.prompt({
281
285
  type: 'list',
282
286
  name: 'exportAction',
@@ -335,7 +339,7 @@ class Wallet {
335
339
  const { mnemonic } = await inquirer.prompt({
336
340
  type: 'password',
337
341
  name: 'mnemonic',
338
- message: 'Enter your 12-word mnemonic phrase:',
342
+ message: 'Enter your mnemonic phrase (12 or 24 words):',
339
343
  mask: '*',
340
344
  validate: (input) => {
341
345
  if (input.trim() === '') return true;
@@ -468,17 +472,17 @@ class Wallet {
468
472
  let currentBalance = 0;
469
473
  try {
470
474
  const walletAddress = await this.getAddress();
471
-
475
+
472
476
  // Get the balance AFTER transaction (not before)
473
477
  if (token === this.selectedNetwork.nativeToken) {
474
478
  currentBalance = await this.network.getBalance(walletAddress);
475
479
  } else {
476
480
  currentBalance = await this.network.getTokenBalance(walletAddress, token);
477
481
  }
478
-
482
+
479
483
  // Round to avoid floating point precision issues
480
484
  currentBalance = Math.round(currentBalance * 100000000) / 100000000;
481
-
485
+
482
486
  } catch (error) {
483
487
  console.error('Error getting post-transaction balance:', error.message);
484
488
  }
@@ -726,21 +730,58 @@ class Wallet {
726
730
  }
727
731
 
728
732
  async importHODLFile() {
733
+ // Scan current directory for .HODL files
734
+ const currentDir = process.cwd();
735
+ let hodlFiles = [];
736
+
737
+ try {
738
+ const files = fs.readdirSync(currentDir);
739
+ hodlFiles = files.filter(file => file.endsWith('.HODL'));
740
+ } catch (error) {
741
+ console.error('Error reading directory:', error.message);
742
+ }
743
+
744
+ // Create file options array for autocomplete
745
+ const fileOptions = hodlFiles.map(file => ({
746
+ name: file,
747
+ value: path.join(currentDir, file)
748
+ }));
749
+
750
+ // Use autocomplete pattern similar to transferFunds
729
751
  const { filePath } = await inquirer.prompt({
730
- type: 'fuzzypath',
752
+ type: 'autocomplete',
731
753
  name: 'filePath',
732
- message: 'Select the .HODL file:',
733
- rootPath: '.',
734
- itemType: 'file',
735
- suggestOnly: false,
736
- depthLimit: 5,
737
- excludePath: nodePath => nodePath.startsWith('node_modules'),
738
- excludeFilter: nodePath => !nodePath.endsWith('.HODL'),
754
+ message: 'HODL file path:',
755
+ source: (answersSoFar, input) => {
756
+ input = input || '';
757
+
758
+ const filenameOptions = fileOptions
759
+ .filter(entry => entry.name.toLowerCase().includes(input.toLowerCase()) || entry.value.toLowerCase().includes(input.toLowerCase()))
760
+ .map(entry => ({
761
+ name: entry.name,
762
+ value: entry.value
763
+ }));
764
+
765
+ // Put path options first, then filename options, then manual input
766
+ return [].concat([{ name: input, value: input }])
767
+ .concat(filenameOptions)
768
+ },
739
769
  });
740
770
 
771
+ // If user leaves empty, skip the operation
772
+ if (!filePath || filePath.trim() === '') {
773
+ return null;
774
+ }
775
+
776
+ // Validate the file path
777
+ if (!filePath.endsWith('.HODL')) {
778
+ Wallet.displayError('File must have .HODL extension');
779
+ return null;
780
+ }
781
+
741
782
  if (!fs.existsSync(filePath)) {
742
783
  Wallet.displayError('File not found.');
743
- return;
784
+ return null;
744
785
  }
745
786
 
746
787
  const encryptedData = fs.readFileSync(filePath, 'utf8');
@@ -751,6 +792,7 @@ class Wallet {
751
792
  return this.getAccount();
752
793
  } catch (error) {
753
794
  Wallet.displayError('Failed to import HODL file.', 'The password is incorrect.');
795
+ return null;
754
796
  }
755
797
  }
756
798
 
@@ -805,7 +847,19 @@ class Wallet {
805
847
  });
806
848
 
807
849
  if (createWithMnemonic) {
808
- this.setAccount(await this.network.createAccountFromMnemonic());
850
+ // Ask for mnemonic word count
851
+ const { wordCount } = await inquirer.prompt({
852
+ type: 'list',
853
+ name: 'wordCount',
854
+ message: 'Choose mnemonic phrase length:',
855
+ choices: [
856
+ { name: '12 words (standard)', value: 12 },
857
+ { name: '24 words (more secure)', value: 24 }
858
+ ],
859
+ default: 12
860
+ });
861
+
862
+ this.setAccount(await this.network.createAccountFromMnemonic(wordCount));
809
863
  } else {
810
864
  this.setAccount(await this.network.createAccount());
811
865
  }
@@ -185,9 +185,11 @@ export default class BitcoinNetwork extends BaseNetwork {
185
185
  };
186
186
  }
187
187
 
188
- async createAccountFromMnemonic() {
188
+ async createAccountFromMnemonic(wordCount = 12) {
189
189
  try {
190
- const mnemonic = bip39.generateMnemonic();
190
+ // Support both 12 and 24 word mnemonics
191
+ const strength = wordCount === 24 ? 256 : 128; // 256 bits = 24 words, 128 bits = 12 words
192
+ const mnemonic = bip39.generateMnemonic(strength);
191
193
  return this.accountFromMnemonic(mnemonic);
192
194
  } catch (error) {
193
195
  throw new Error('Failed to create account from mnemonic: ' + error.message);
@@ -86,40 +86,84 @@ export default class TONNetwork extends BaseNetwork {
86
86
  }
87
87
 
88
88
  async createAccount() {
89
- const mnemonic = await this.generateMnemonic();
90
- return this.accountFromMnemonic(mnemonic);
89
+ try {
90
+ const mnemonic = await this.generateMnemonic();
91
+ const account = await this.accountFromMnemonic(mnemonic);
92
+
93
+ // Ensure all required fields are present
94
+ if (!account.address || !account.privateKey) {
95
+ throw new Error('Failed to create complete account');
96
+ }
97
+
98
+ return account;
99
+ } catch (error) {
100
+ console.error('Error creating new account:', error.message);
101
+ throw error;
102
+ }
91
103
  }
92
104
 
93
105
  async accountFromMnemonic(mnemonic) {
94
- const mnemonicArray = Array.isArray(mnemonic) ? mnemonic : mnemonic.split(' ');
95
- const keyPair = await mnemonicToPrivateKey(mnemonicArray);
96
- const wallet = WalletContractV4.create({
97
- publicKey: keyPair.publicKey,
98
- workchain: 0
99
- });
100
-
101
- return {
102
- address: wallet.address.toString(),
103
- publicKey: keyPair.publicKey,
104
- // Store secretKey as hex string to avoid Buffer serialization issues
105
- secretKey: keyPair.secretKey.toString('hex'),
106
- mnemonic: Array.isArray(mnemonic) ? mnemonic.join(' ') : mnemonic
107
- };
106
+ try {
107
+ const mnemonicArray = Array.isArray(mnemonic) ? mnemonic : mnemonic.split(' ');
108
+
109
+ // Validate mnemonic first
110
+ if (!mnemonicValidate(mnemonicArray)) {
111
+ throw new Error('Invalid mnemonic phrase');
112
+ }
113
+
114
+ const keyPair = await mnemonicToPrivateKey(mnemonicArray);
115
+ const wallet = WalletContractV4.create({
116
+ publicKey: keyPair.publicKey,
117
+ workchain: 0
118
+ });
119
+
120
+ const account = {
121
+ address: wallet.address.toString(),
122
+ publicKey: keyPair.publicKey,
123
+ // Store secretKey as hex string to avoid Buffer serialization issues
124
+ secretKey: keyPair.secretKey.toString('hex'),
125
+ privateKey: keyPair.secretKey.toString('hex'), // Add privateKey for compatibility
126
+ mnemonic: Array.isArray(mnemonic) ? mnemonic.join(' ') : mnemonic
127
+ };
128
+
129
+ return account;
130
+ } catch (error) {
131
+ console.error('Error creating account from mnemonic:', error.message);
132
+ throw error;
133
+ }
108
134
  }
109
135
 
110
- async createAccountFromMnemonic() {
111
- const mnemonic = await this.generateMnemonic();
112
- return this.accountFromMnemonic(mnemonic);
136
+ async createAccountFromMnemonic(wordCount = 12) {
137
+ try {
138
+ const mnemonic = await this.generateMnemonic(wordCount);
139
+ const account = await this.accountFromMnemonic(mnemonic);
140
+
141
+ // Ensure all required fields are present including mnemonic
142
+ if (!account.address || !account.privateKey || !account.mnemonic) {
143
+ throw new Error('Failed to create complete account with mnemonic');
144
+ }
145
+
146
+ return account;
147
+ } catch (error) {
148
+ console.error('Error creating account with mnemonic:', error.message);
149
+ throw error;
150
+ }
113
151
  }
114
152
 
115
- async generateMnemonic() {
116
- const mnemonic = await mnemonicNew();
153
+ async generateMnemonic(wordCount = 12) {
154
+ const mnemonic = await mnemonicNew(wordCount);
117
155
  return mnemonic.join(' ');
118
156
  }
119
157
 
120
158
  validateMnemonic(mnemonic) {
121
159
  try {
122
- return mnemonicValidate(mnemonic.split(' '));
160
+ const mnemonicArray = mnemonic.split(' ');
161
+ // Check if it has exactly 12 or 24 words
162
+ if (mnemonicArray.length !== 12 && mnemonicArray.length !== 24) {
163
+ return false;
164
+ }
165
+ // Use TON's built-in validation
166
+ return mnemonicValidate(mnemonicArray);
123
167
  } catch {
124
168
  return false;
125
169
  }
@@ -89,10 +89,11 @@ export default class Web3Network extends BaseNetwork {
89
89
 
90
90
  async handleNativeTransfer(account, recipient, amount) {
91
91
  const gasPrice = await this.getGasPrice();
92
+ const valueInWei = this.web3.utils.toWei(amount.toString(), 'ether');
92
93
  const gasLimit = await this.estimateGas({
93
94
  from: account.address,
94
95
  to: recipient,
95
- value: amount
96
+ value: valueInWei
96
97
  });
97
98
 
98
99
  return this.transfer(account, recipient, amount, { gasLimit, gasPrice });
@@ -144,9 +145,11 @@ export default class Web3Network extends BaseNetwork {
144
145
  return account;
145
146
  }
146
147
 
147
- async createAccountFromMnemonic() {
148
+ async createAccountFromMnemonic(wordCount = 12) {
148
149
  try {
149
- const mnemonic = bip39.generateMnemonic();
150
+ // Support both 12 and 24 word mnemonics
151
+ const strength = wordCount === 24 ? 256 : 128; // 256 bits = 24 words, 128 bits = 12 words
152
+ const mnemonic = bip39.generateMnemonic(strength);
150
153
  return this.accountFromMnemonic(mnemonic);
151
154
  } catch (error) {
152
155
  throw new Error('Failed to create account from mnemonic: ' + error.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hodl-wallet",
3
- "version": "1.8.4",
3
+ "version": "1.8.8",
4
4
  "description": "๐ŸงŠ HODL Wallet - Fast CLI crypto wallet!",
5
5
  "author": "Martin Clasen",
6
6
  "repository": {
@@ -18,7 +18,10 @@
18
18
  },
19
19
  "scripts": {
20
20
  "start": "node ./index.js",
21
- "test": "echo \"Error: no test specified\" && exit 1"
21
+ "test": "node ./test/test-all.js",
22
+ "test:network": "node ./test/test.js",
23
+ "test:integration": "node ./test/test-integration.js",
24
+ "test:quick": "node ./test/test.js --quick"
22
25
  },
23
26
  "keywords": [
24
27
  "hodl",
@@ -60,12 +63,18 @@
60
63
  "crypto-js": "^4.2.0",
61
64
  "deepbase": "^1.5.2",
62
65
  "ecpair": "^2.1.0",
66
+ "external-editor": "^3.1.0",
63
67
  "hdkey": "^2.1.0",
64
68
  "inquirer": "^9.3.7",
65
69
  "inquirer-autocomplete-prompt": "^3.0.1",
66
70
  "inquirer-fuzzy-path": "^2.3.0",
71
+ "ora": "^8.1.1",
67
72
  "tiny-secp256k1": "^2.2.3",
73
+ "tmp": "^0.2.4",
68
74
  "web3": "^4.14.0"
69
75
  },
76
+ "overrides": {
77
+ "tmp": "^0.2.4"
78
+ },
70
79
  "type": "module"
71
80
  }
package/test/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # ๐Ÿงช Test Directory - HODL Wallet
2
+
3
+ This directory contains the complete test suite to validate the functionality of the HODL Wallet library.
4
+
5
+ ## ๐Ÿ“ File Structure
6
+
7
+ ```
8
+ test/
9
+ โ”œโ”€โ”€ test.js # Main network tests
10
+ โ”œโ”€โ”€ test-integration.js # Cross-network integration tests
11
+ โ”œโ”€โ”€ test-all.js # Complete runner with interactive menu
12
+ โ”œโ”€โ”€ TESTING.md # Complete testing documentation
13
+ โ”œโ”€โ”€ TEST_RESULTS.md # Latest execution results
14
+ โ””โ”€โ”€ README.md # This file
15
+ ```
16
+
17
+ ## ๐Ÿš€ Quick Start
18
+
19
+ ### Run all tests:
20
+ ```bash
21
+ npm test
22
+ ```
23
+
24
+ ### Run specific tests:
25
+ ```bash
26
+ npm run test:network # Network tests only
27
+ npm run test:integration # Integration tests only
28
+ npm run test:quick # Quick validation
29
+ ```
30
+
31
+ ## ๐Ÿ“Š Current Status
32
+
33
+ - **โœ… 9/9 networks fully functional** (100% success)
34
+ - **โœ… 79 individual tests**
35
+ - **โœ… Complete functionality coverage**
36
+
37
+ ### Validated Networks:
38
+ - โœ… Ethereum (ETH)
39
+ - โœ… Binance Smart Chain (BNB)
40
+ - โœ… Arbitrum One (ARB)
41
+ - โœ… Avalanche C-Chain (AVAX)
42
+ - โœ… Fantom (FTM)
43
+ - โœ… Optimism (OP)
44
+ - โœ… Polygon (MATIC)
45
+ - โœ… Bitcoin (BTC)
46
+ - โœ… TON (fully fixed and functional)
47
+
48
+ ## ๐Ÿ“– Documentation
49
+
50
+ For detailed information on how to use the tests, see:
51
+ - **[TESTING.md](./TESTING.md)** - Complete testing guide
52
+ - **[TEST_RESULTS.md](./TEST_RESULTS.md)** - Detailed results
53
+
54
+ ## ๐ŸŽฏ Purpose
55
+
56
+ This test suite validates:
57
+ 1. **Correct configuration** of all networks
58
+ 2. **Complete implementation** of required methods
59
+ 3. **Consistency between networks** EVM
60
+ 4. **Account functionality** (creation, import)
61
+ 5. **Network connectivity** and validations
62
+
63
+ ---
64
+
65
+ *Keep this directory updated when adding new networks or functionality.*