hodl-wallet 1.8.6 → 1.9.0
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 +3 -9
- package/index.js +92 -36
- package/network/lib/BitcoinNetwork.js +4 -2
- package/network/lib/TONNetwork.js +4 -4
- package/network/lib/Web3Network.js +6 -3
- package/package.json +19 -24
- package/test/test-all.js +3 -3
- package/test/test-integration.js +3 -3
- package/test/test.js +35 -6
- package/.claude/settings.local.json +0 -11
- package/CLAUDE.md +0 -90
- package/network/disabled/rsk.js +0 -14
- /package/network/{ton.js → disabled/ton.js} +0 -0
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Let's face it, Trust Wallet's sluggishness and annoying ads are so last season.
|
|
|
17
17
|
- 🚫 Zero ads, zero BS
|
|
18
18
|
- 🔒 Create wallets offline (because paranoia is just good sense in crypto)
|
|
19
19
|
- 🔍 Fully transparent, open-source code
|
|
20
|
-
- 🌐 Support for Bitcoin and Ethereum. Binance Smart Chain,
|
|
20
|
+
- 🌐 Support for Bitcoin and Ethereum. Binance Smart Chain, Polygon, Avalanche, Optimism, Arbitrum, and Fantom.
|
|
21
21
|
|
|
22
22
|
That's it! Follow the prompts and you're in crypto heaven.
|
|
23
23
|
|
|
@@ -44,9 +44,6 @@ Keep your favorite addresses handy. No more copy-pasting!
|
|
|
44
44
|
Seamlessly manage your assets on multiple networks. HODL Wallet supports the following networks:
|
|
45
45
|
|
|
46
46
|
- Bitcoin
|
|
47
|
-
- TON (The Open Network)
|
|
48
|
-
- Native TON transfers
|
|
49
|
-
- Jetton support (USDT)
|
|
50
47
|
- EVM
|
|
51
48
|
- Ethereum
|
|
52
49
|
- Binance Smart Chain
|
|
@@ -75,6 +72,8 @@ The main advantage of exporting a HODL file is that to access the private key, y
|
|
|
75
72
|
|
|
76
73
|
We encourage users to perform their own security audits. One easy way to do this is to copy the entire codebase into ChatGPT or another AI assistant and ask if the code appears secure or if there are any malicious intentions. This is a good practice for any open-source project you're considering using.
|
|
77
74
|
|
|
75
|
+
**For a deeper understanding**: [HODL DeepWiki](https://deepwiki.com/clasen/HODL)
|
|
76
|
+
|
|
78
77
|
### 🔑 Private Key Storage
|
|
79
78
|
|
|
80
79
|
Your private key is securely stored in a JSON file, encrypted with a password of your choice. The encryption adds an extra layer of security, making it significantly harder for unauthorized parties to access your private key even if they gain access to the JSON file.
|
|
@@ -98,11 +97,6 @@ We've carefully selected trusted and well-maintained dependencies for this proje
|
|
|
98
97
|
- Web3
|
|
99
98
|
- **web3**: The Ethereum JavaScript API for blockchain interactions.
|
|
100
99
|
- **hdkey**: For handling hierarchical deterministic (HD) keys.
|
|
101
|
-
- TON
|
|
102
|
-
- **@ton/ton**: The TON JavaScript SDK for blockchain interactions.
|
|
103
|
-
- **@ton/core**: Core TON blockchain operations and data structures.
|
|
104
|
-
- **@ton/crypto**: Cryptographic functions for TON blockchain.
|
|
105
|
-
- **@orbs-network/ton-access**: For reliable TON network access.
|
|
106
100
|
- Bitcoin
|
|
107
101
|
- **bitcoinjs-lib**: For Bitcoin-specific operations.
|
|
108
102
|
- **bip32**: For handling hierarchical deterministic (HD) keys.
|
package/index.js
CHANGED
|
@@ -37,20 +37,20 @@ class Wallet {
|
|
|
37
37
|
|
|
38
38
|
formatAmount(num) {
|
|
39
39
|
num = parseFloat(num);
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
// Handle integers - add .00
|
|
42
42
|
if (num === Math.floor(num)) {
|
|
43
43
|
return num.toString() + '.00';
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
// For decimals, format to max 3 decimal places, then remove trailing zeros
|
|
47
47
|
let formatted = num.toFixed(3);
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
// Remove trailing zeros, but keep at least 2 decimal places
|
|
50
50
|
while (formatted.endsWith('0') && formatted.split('.')[1].length > 2) {
|
|
51
51
|
formatted = formatted.slice(0, -1);
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
return formatted;
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -82,6 +82,9 @@ class Wallet {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
setAccount(account) {
|
|
85
|
+
if (!account) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
85
88
|
this.db.set('account', this.network.constructor.name, account);
|
|
86
89
|
if (account.mnemonic) {
|
|
87
90
|
this.db.set('mnemonic', account.mnemonic);
|
|
@@ -90,8 +93,8 @@ class Wallet {
|
|
|
90
93
|
|
|
91
94
|
async getAccount() {
|
|
92
95
|
const account = this.db.get('account', this.network.constructor.name);
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
|
|
97
|
+
|
|
95
98
|
return account;
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -111,7 +114,7 @@ class Wallet {
|
|
|
111
114
|
head: ['green']
|
|
112
115
|
}
|
|
113
116
|
});
|
|
114
|
-
|
|
117
|
+
|
|
115
118
|
table.push([await this.getAddress()]);
|
|
116
119
|
console.log(table.toString());
|
|
117
120
|
}
|
|
@@ -133,18 +136,18 @@ class Wallet {
|
|
|
133
136
|
const sortedNetworks = networkPlugins.sort((a, b) => {
|
|
134
137
|
const aUsage = this.networkUsage[a.name];
|
|
135
138
|
const bUsage = this.networkUsage[b.name];
|
|
136
|
-
|
|
139
|
+
|
|
137
140
|
// Handle old format (number) vs new format (object)
|
|
138
141
|
const aLastUsed = typeof aUsage === 'object' ? aUsage.lastUsed || 0 : 0;
|
|
139
142
|
const bLastUsed = typeof bUsage === 'object' ? bUsage.lastUsed || 0 : 0;
|
|
140
|
-
|
|
143
|
+
|
|
141
144
|
// If neither has lastUsed timestamp, sort by old count format
|
|
142
145
|
if (aLastUsed === 0 && bLastUsed === 0) {
|
|
143
146
|
const aCount = typeof aUsage === 'number' ? aUsage : (aUsage?.count || 0);
|
|
144
147
|
const bCount = typeof bUsage === 'number' ? bUsage : (bUsage?.count || 0);
|
|
145
148
|
return bCount - aCount;
|
|
146
149
|
}
|
|
147
|
-
|
|
150
|
+
|
|
148
151
|
return bLastUsed - aLastUsed;
|
|
149
152
|
});
|
|
150
153
|
|
|
@@ -167,19 +170,19 @@ class Wallet {
|
|
|
167
170
|
// Update usage info for the selected network
|
|
168
171
|
// Handle migration from old format (number) to new format (object)
|
|
169
172
|
const currentUsage = this.networkUsage[selectedNetwork.name];
|
|
170
|
-
|
|
173
|
+
|
|
171
174
|
if (!currentUsage || typeof currentUsage === 'number') {
|
|
172
175
|
// Old format (number) or doesn't exist - create new object
|
|
173
|
-
this.networkUsage[selectedNetwork.name] = {
|
|
174
|
-
count: typeof currentUsage === 'number' ? currentUsage + 1 : 1,
|
|
175
|
-
lastUsed: Date.now()
|
|
176
|
+
this.networkUsage[selectedNetwork.name] = {
|
|
177
|
+
count: typeof currentUsage === 'number' ? currentUsage + 1 : 1,
|
|
178
|
+
lastUsed: Date.now()
|
|
176
179
|
};
|
|
177
180
|
} else {
|
|
178
181
|
// New format (object) - update values
|
|
179
182
|
this.networkUsage[selectedNetwork.name].count = (currentUsage.count || 0) + 1;
|
|
180
183
|
this.networkUsage[selectedNetwork.name].lastUsed = Date.now();
|
|
181
184
|
}
|
|
182
|
-
|
|
185
|
+
|
|
183
186
|
await this.db.set('networkUsage', this.networkUsage);
|
|
184
187
|
|
|
185
188
|
this.selectedNetwork = selectedNetwork;
|
|
@@ -197,9 +200,9 @@ class Wallet {
|
|
|
197
200
|
mainChoices.push('Manage Address Book');
|
|
198
201
|
mainChoices.push('Go Back');
|
|
199
202
|
} else {
|
|
200
|
-
mainChoices.push('Import Mnemonic (12 words)');
|
|
201
|
-
mainChoices.push('Import Private-key');
|
|
202
203
|
mainChoices.push('Import HODL File');
|
|
204
|
+
mainChoices.push('Import Mnemonic (12 or 24 words)');
|
|
205
|
+
mainChoices.push('Import Private-key');
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
if (!account || loggedIn) {
|
|
@@ -236,7 +239,7 @@ class Wallet {
|
|
|
236
239
|
}
|
|
237
240
|
|
|
238
241
|
if (accountAction === 'Import Options') {
|
|
239
|
-
const importChoices = ['Import Mnemonic (12 words)', 'Import Private-key', '
|
|
242
|
+
const importChoices = ['Import HODL File', 'Import Mnemonic (12 or 24 words)', 'Import Private-key', 'Go Back'];
|
|
240
243
|
const { importAction } = await inquirer.prompt({
|
|
241
244
|
type: 'list',
|
|
242
245
|
name: 'importAction',
|
|
@@ -255,7 +258,7 @@ class Wallet {
|
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
switch (accountAction) {
|
|
258
|
-
case 'Import Mnemonic (12 words)':
|
|
261
|
+
case 'Import Mnemonic (12 or 24 words)':
|
|
259
262
|
account = await this.importFromMnemonic();
|
|
260
263
|
if (!account) {
|
|
261
264
|
// Wallet.displayError('Invalid mnemonic.');
|
|
@@ -268,13 +271,16 @@ class Wallet {
|
|
|
268
271
|
await this.importPrivateKey();
|
|
269
272
|
break;
|
|
270
273
|
case 'Import HODL File':
|
|
271
|
-
|
|
272
|
-
|
|
274
|
+
const importedAccount = await this.importHODLFile();
|
|
275
|
+
if (importedAccount) {
|
|
276
|
+
this.setAccount(importedAccount);
|
|
277
|
+
await this.displayAccountAddress();
|
|
278
|
+
}
|
|
273
279
|
break;
|
|
274
280
|
}
|
|
275
281
|
|
|
276
282
|
if (accountAction === 'Export Options') {
|
|
277
|
-
const exportChoices = ['Export
|
|
283
|
+
const exportChoices = ['Export HODL File', 'Export Private-key', 'Go Back'];
|
|
278
284
|
const { exportAction } = await inquirer.prompt({
|
|
279
285
|
type: 'list',
|
|
280
286
|
name: 'exportAction',
|
|
@@ -333,7 +339,7 @@ class Wallet {
|
|
|
333
339
|
const { mnemonic } = await inquirer.prompt({
|
|
334
340
|
type: 'password',
|
|
335
341
|
name: 'mnemonic',
|
|
336
|
-
message: 'Enter your 12
|
|
342
|
+
message: 'Enter your mnemonic phrase (12 or 24 words):',
|
|
337
343
|
mask: '*',
|
|
338
344
|
validate: (input) => {
|
|
339
345
|
if (input.trim() === '') return true;
|
|
@@ -466,17 +472,17 @@ class Wallet {
|
|
|
466
472
|
let currentBalance = 0;
|
|
467
473
|
try {
|
|
468
474
|
const walletAddress = await this.getAddress();
|
|
469
|
-
|
|
475
|
+
|
|
470
476
|
// Get the balance AFTER transaction (not before)
|
|
471
477
|
if (token === this.selectedNetwork.nativeToken) {
|
|
472
478
|
currentBalance = await this.network.getBalance(walletAddress);
|
|
473
479
|
} else {
|
|
474
480
|
currentBalance = await this.network.getTokenBalance(walletAddress, token);
|
|
475
481
|
}
|
|
476
|
-
|
|
482
|
+
|
|
477
483
|
// Round to avoid floating point precision issues
|
|
478
484
|
currentBalance = Math.round(currentBalance * 100000000) / 100000000;
|
|
479
|
-
|
|
485
|
+
|
|
480
486
|
} catch (error) {
|
|
481
487
|
console.error('Error getting post-transaction balance:', error.message);
|
|
482
488
|
}
|
|
@@ -724,21 +730,58 @@ class Wallet {
|
|
|
724
730
|
}
|
|
725
731
|
|
|
726
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
|
|
727
751
|
const { filePath } = await inquirer.prompt({
|
|
728
|
-
type: '
|
|
752
|
+
type: 'autocomplete',
|
|
729
753
|
name: 'filePath',
|
|
730
|
-
message: '
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
+
},
|
|
737
769
|
});
|
|
738
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
|
+
|
|
739
782
|
if (!fs.existsSync(filePath)) {
|
|
740
783
|
Wallet.displayError('File not found.');
|
|
741
|
-
return;
|
|
784
|
+
return null;
|
|
742
785
|
}
|
|
743
786
|
|
|
744
787
|
const encryptedData = fs.readFileSync(filePath, 'utf8');
|
|
@@ -749,6 +792,7 @@ class Wallet {
|
|
|
749
792
|
return this.getAccount();
|
|
750
793
|
} catch (error) {
|
|
751
794
|
Wallet.displayError('Failed to import HODL file.', 'The password is incorrect.');
|
|
795
|
+
return null;
|
|
752
796
|
}
|
|
753
797
|
}
|
|
754
798
|
|
|
@@ -803,7 +847,19 @@ class Wallet {
|
|
|
803
847
|
});
|
|
804
848
|
|
|
805
849
|
if (createWithMnemonic) {
|
|
806
|
-
|
|
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));
|
|
807
863
|
} else {
|
|
808
864
|
this.setAccount(await this.network.createAccount());
|
|
809
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
|
-
|
|
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);
|
|
@@ -133,9 +133,9 @@ export default class TONNetwork extends BaseNetwork {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
async createAccountFromMnemonic() {
|
|
136
|
+
async createAccountFromMnemonic(wordCount = 12) {
|
|
137
137
|
try {
|
|
138
|
-
const mnemonic = await this.generateMnemonic();
|
|
138
|
+
const mnemonic = await this.generateMnemonic(wordCount);
|
|
139
139
|
const account = await this.accountFromMnemonic(mnemonic);
|
|
140
140
|
|
|
141
141
|
// Ensure all required fields are present including mnemonic
|
|
@@ -150,8 +150,8 @@ export default class TONNetwork extends BaseNetwork {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
async generateMnemonic() {
|
|
154
|
-
const mnemonic = await mnemonicNew();
|
|
153
|
+
async generateMnemonic(wordCount = 12) {
|
|
154
|
+
const mnemonic = await mnemonicNew(wordCount);
|
|
155
155
|
return mnemonic.join(' ');
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -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:
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "🧊 HODL Wallet - Fast CLI crypto wallet!",
|
|
5
5
|
"author": "Martin Clasen",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"bep20",
|
|
33
33
|
"bnb",
|
|
34
34
|
"eth",
|
|
35
|
-
"ton",
|
|
36
35
|
"binance",
|
|
37
36
|
"trust",
|
|
38
37
|
"blockchain",
|
|
@@ -51,30 +50,26 @@
|
|
|
51
50
|
"clasen"
|
|
52
51
|
],
|
|
53
52
|
"dependencies": {
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"ora": "^8.1.1",
|
|
72
|
-
"tiny-secp256k1": "^2.2.3",
|
|
73
|
-
"tmp": "^0.2.4",
|
|
74
|
-
"web3": "^4.14.0"
|
|
53
|
+
"axios": "1.8.4",
|
|
54
|
+
"bip32": "5.0.0-rc.0",
|
|
55
|
+
"bip39": "3.1.0",
|
|
56
|
+
"bitcoinjs-lib": "6.1.7",
|
|
57
|
+
"cli-table3": "0.6.5",
|
|
58
|
+
"crypto-js": "4.2.0",
|
|
59
|
+
"deepbase": "1.5.2",
|
|
60
|
+
"ecpair": "2.1.0",
|
|
61
|
+
"external-editor": "3.1.0",
|
|
62
|
+
"hdkey": "2.1.0",
|
|
63
|
+
"inquirer": "9.3.7",
|
|
64
|
+
"inquirer-autocomplete-prompt": "3.0.1",
|
|
65
|
+
"inquirer-fuzzy-path": "2.3.0",
|
|
66
|
+
"ora": "8.1.1",
|
|
67
|
+
"tiny-secp256k1": "2.2.3",
|
|
68
|
+
"tmp": "0.2.4",
|
|
69
|
+
"web3": "4.14.0"
|
|
75
70
|
},
|
|
76
71
|
"overrides": {
|
|
77
|
-
"tmp": "
|
|
72
|
+
"tmp": "0.2.4"
|
|
78
73
|
},
|
|
79
74
|
"type": "module"
|
|
80
75
|
}
|
package/test/test-all.js
CHANGED
|
@@ -14,9 +14,9 @@ class ComprehensiveTester {
|
|
|
14
14
|
displayWelcome() {
|
|
15
15
|
console.log('\x1b[32m'); // Set text color to green
|
|
16
16
|
console.log(`
|
|
17
|
-
|
|
18
|
-
░░░░░░░░░░░░░░ 🧊 HODL WALLET - COMPREHENSIVE TEST SUITE
|
|
19
|
-
|
|
17
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
18
|
+
░░░░░░░░░░░░░░ 🧊 HODL WALLET - COMPREHENSIVE TEST SUITE ░░░░░░░░░░░░░░░
|
|
19
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
20
20
|
`);
|
|
21
21
|
console.log('\x1b[0m'); // Reset text color
|
|
22
22
|
console.log('This test suite will validate all network implementations and integrations.\n');
|
package/test/test-integration.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
3
|
import { fileURLToPath } from 'url';
|
|
6
4
|
import { dirname } from 'path';
|
|
7
5
|
import Table from 'cli-table3';
|
|
@@ -99,7 +97,9 @@ class IntegrationTester extends NetworkTester {
|
|
|
99
97
|
this.crossNetworkResults.push({
|
|
100
98
|
test: 'Network Type Distribution',
|
|
101
99
|
status: totalNetworks === allNetworks.length ? 'PASS' : 'FAIL',
|
|
102
|
-
message:
|
|
100
|
+
message: tonNetworks.length === 0 ?
|
|
101
|
+
`Found ${totalNetworks} categorized networks out of ${allNetworks.length} total (TON network disabled)` :
|
|
102
|
+
`Found ${totalNetworks} categorized networks out of ${allNetworks.length} total`,
|
|
103
103
|
details: results
|
|
104
104
|
});
|
|
105
105
|
|
package/test/test.js
CHANGED
|
@@ -197,22 +197,51 @@ class NetworkTester {
|
|
|
197
197
|
}
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
-
// Test 8: Create account from new mnemonic
|
|
200
|
+
// Test 8: Create account from new mnemonic (12 words)
|
|
201
201
|
tests.push({
|
|
202
|
-
name: 'Create Account From New Mnemonic',
|
|
202
|
+
name: 'Create Account From New Mnemonic (12 words)',
|
|
203
203
|
test: async () => {
|
|
204
|
-
const account = await instance.createAccountFromMnemonic();
|
|
204
|
+
const account = await instance.createAccountFromMnemonic(12);
|
|
205
205
|
|
|
206
206
|
if (!account || !account.address || !account.privateKey || !account.mnemonic) {
|
|
207
|
-
throw new Error('Account creation with mnemonic failed');
|
|
207
|
+
throw new Error('Account creation with 12-word mnemonic failed');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const wordCount = account.mnemonic.split(' ').length;
|
|
211
|
+
if (wordCount !== 12) {
|
|
212
|
+
throw new Error(`Expected 12 words, got: ${wordCount}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Verify the mnemonic is valid
|
|
216
|
+
if (!instance.validateMnemonic(account.mnemonic)) {
|
|
217
|
+
throw new Error('Generated 12-word mnemonic is invalid');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return `12-word account: ${account.address.substring(0, 10)}...`;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Test 8b: Create account from new mnemonic (24 words)
|
|
225
|
+
tests.push({
|
|
226
|
+
name: 'Create Account From New Mnemonic (24 words)',
|
|
227
|
+
test: async () => {
|
|
228
|
+
const account = await instance.createAccountFromMnemonic(24);
|
|
229
|
+
|
|
230
|
+
if (!account || !account.address || !account.privateKey || !account.mnemonic) {
|
|
231
|
+
throw new Error('Account creation with 24-word mnemonic failed');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const wordCount = account.mnemonic.split(' ').length;
|
|
235
|
+
if (wordCount !== 24) {
|
|
236
|
+
throw new Error(`Expected 24 words, got: ${wordCount}`);
|
|
208
237
|
}
|
|
209
238
|
|
|
210
239
|
// Verify the mnemonic is valid
|
|
211
240
|
if (!instance.validateMnemonic(account.mnemonic)) {
|
|
212
|
-
throw new Error('Generated mnemonic is invalid');
|
|
241
|
+
throw new Error('Generated 24-word mnemonic is invalid');
|
|
213
242
|
}
|
|
214
243
|
|
|
215
|
-
return `
|
|
244
|
+
return `24-word account: ${account.address.substring(0, 10)}...`;
|
|
216
245
|
}
|
|
217
246
|
});
|
|
218
247
|
|
package/CLAUDE.md
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
HODL Wallet is a CLI-based multi-network cryptocurrency wallet written in Node.js. It supports Bitcoin, TON (The Open Network), and multiple EVM-compatible networks (Ethereum, BSC, Polygon, Arbitrum, Optimism, Fantom, Avalanche).
|
|
8
|
-
|
|
9
|
-
## Key Commands
|
|
10
|
-
|
|
11
|
-
- **Start the application**: `npm start` or `node index.js`
|
|
12
|
-
- **Install globally**: `npm install -g hodl-wallet` then run `hodl`
|
|
13
|
-
- **No test suite configured**: The project uses `echo "Error: no test specified" && exit 1` for tests
|
|
14
|
-
|
|
15
|
-
## Architecture Overview
|
|
16
|
-
|
|
17
|
-
### Core Components
|
|
18
|
-
|
|
19
|
-
- **index.js**: Main application entry point containing the `Wallet` class and `UIManager` class
|
|
20
|
-
- **persist.js**: Encrypted data persistence layer using Deepbase with AES encryption
|
|
21
|
-
- **network/**: Network implementations following a plugin architecture
|
|
22
|
-
|
|
23
|
-
### Network Plugin System
|
|
24
|
-
|
|
25
|
-
The application uses a modular network plugin system:
|
|
26
|
-
|
|
27
|
-
- **BaseNetwork.js**: Abstract base class defining the interface all networks must implement
|
|
28
|
-
- **Web3Network.js**: EVM-compatible network implementation extending BaseNetwork
|
|
29
|
-
- **BitcoinNetwork.js**: Bitcoin-specific network implementation
|
|
30
|
-
- **TONNetwork.js**: TON network implementation
|
|
31
|
-
|
|
32
|
-
Each network plugin exports:
|
|
33
|
-
- `NetworkClass`: The implementation class
|
|
34
|
-
- `name`: Display name for the network
|
|
35
|
-
- `url`: RPC endpoint URL
|
|
36
|
-
- `nativeToken`: Native token symbol (e.g., 'ETH', 'BTC')
|
|
37
|
-
- `explorer`: Block explorer URL template
|
|
38
|
-
- `tokens`: Object mapping token symbols to contract addresses
|
|
39
|
-
|
|
40
|
-
### Data Storage
|
|
41
|
-
|
|
42
|
-
- User data stored in `~/.HODL/` directory
|
|
43
|
-
- All data encrypted using user-provided password
|
|
44
|
-
- Supports mnemonic phrases, private keys, address book, and transaction history
|
|
45
|
-
- Network usage tracking for auto-selection of last-used network
|
|
46
|
-
|
|
47
|
-
### Key Features
|
|
48
|
-
|
|
49
|
-
- Multi-network wallet with unified interface
|
|
50
|
-
- Encrypted local storage with password protection
|
|
51
|
-
- Address book with autocomplete
|
|
52
|
-
- Transaction history tracking
|
|
53
|
-
- HODL file export/import for wallet backup
|
|
54
|
-
- Mnemonic and private key import/export
|
|
55
|
-
|
|
56
|
-
### Security Considerations
|
|
57
|
-
|
|
58
|
-
- Private keys and mnemonics are encrypted at rest
|
|
59
|
-
- Password required for all operations
|
|
60
|
-
- Support for offline account creation
|
|
61
|
-
- Transparent open-source codebase encourages security audits
|
|
62
|
-
|
|
63
|
-
## Common Development Patterns
|
|
64
|
-
|
|
65
|
-
- Network implementations extend either `Web3Network`, `BitcoinNetwork`, or `BaseNetwork` (for TON)
|
|
66
|
-
- All user interactions use the `inquirer` library for CLI prompts
|
|
67
|
-
- Tables displayed using `cli-table3` for consistent formatting
|
|
68
|
-
- Async/await pattern used throughout
|
|
69
|
-
- ES6 modules with `.js` extensions
|
|
70
|
-
|
|
71
|
-
## TON Network Integration
|
|
72
|
-
|
|
73
|
-
The TON network has been integrated with the following features:
|
|
74
|
-
- Native TON balance checking
|
|
75
|
-
- TON transfers using WalletContractV4
|
|
76
|
-
- Mnemonic phrase support (TON uses 24-word mnemonics)
|
|
77
|
-
- Integration with @ton/ton, @ton/crypto, and @ton/core libraries
|
|
78
|
-
- Fallback RPC endpoint if @orbs-network/ton-access is unavailable
|
|
79
|
-
|
|
80
|
-
**TON Features:**
|
|
81
|
-
- Native TON balance checking and transfers ✅
|
|
82
|
-
- Jetton (token) balance checking ✅
|
|
83
|
-
- Jetton transfers ✅
|
|
84
|
-
- Uses standard jetton master contract methods
|
|
85
|
-
- Supports TEP-74 jetton standard
|
|
86
|
-
|
|
87
|
-
**TON Limitations:**
|
|
88
|
-
- Private key import not supported (use mnemonic instead)
|
|
89
|
-
- Gas estimation uses fixed approximation
|
|
90
|
-
- Jetton decimals assumed to be 9 (standard for most tokens)
|
package/network/disabled/rsk.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import Web3Network from './lib/Web3Network.js';
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
NetworkClass: Web3Network,
|
|
5
|
-
name: '[ERC-20] Rootstock',
|
|
6
|
-
url: 'https://public-node.rsk.co',
|
|
7
|
-
nativeToken: 'RBTC',
|
|
8
|
-
explorer: 'https://explorer.rootstock.io/tx/',
|
|
9
|
-
tokens: {
|
|
10
|
-
// 'USDT': {
|
|
11
|
-
// address: '0x833589fCD6eDb6E08B1Daf2d5F90D7ae4Dfd9F59',
|
|
12
|
-
// }
|
|
13
|
-
}
|
|
14
|
-
};
|
|
File without changes
|