hodl-wallet 1.0.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 +65 -0
- package/index.mjs +612 -0
- package/network/bsc.js +12 -0
- package/network/erc.js +11 -0
- package/package.json +39 -0
- package/persist.js +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 🧊 HODL Wallet: Blazing-fast, transparent, and ad-free crypto wallet!
|
|
2
|
+
|
|
3
|
+
## 🚀 Why HODL Wallet?
|
|
4
|
+
|
|
5
|
+
Let's face it, Trust Wallet's sluggishness and annoying ads are so last season. HODL Wallet is here to revolutionize your crypto experience:
|
|
6
|
+
|
|
7
|
+
- 🏎️ Lightning-fast operations
|
|
8
|
+
- 🧊 Cool, minimalist CLI interface
|
|
9
|
+
- 🚫 Zero ads, zero BS
|
|
10
|
+
- 🔒 Create wallets offline (because paranoia is just good sense in crypto)
|
|
11
|
+
- 🔍 Fully transparent, open-source code
|
|
12
|
+
|
|
13
|
+
## 🛠️ Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g hodl-wallet
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 🚀 Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
hodl
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That's it! Follow the prompts and you're in crypto heaven.
|
|
26
|
+
|
|
27
|
+
## 🎮 Features
|
|
28
|
+
|
|
29
|
+
### 💰 Create a Wallet
|
|
30
|
+
|
|
31
|
+
Pro tip: Do this offline if you're feeling extra cautious. We won't judge.
|
|
32
|
+
|
|
33
|
+
### 💸 Send Funds
|
|
34
|
+
|
|
35
|
+
Smoother than sliding into your crush's DMs.
|
|
36
|
+
|
|
37
|
+
### 👀 Check Balance
|
|
38
|
+
|
|
39
|
+
Because constantly checking your balance is totally healthy.
|
|
40
|
+
|
|
41
|
+
## 🔒 Security
|
|
42
|
+
|
|
43
|
+
### Private Key Storage
|
|
44
|
+
|
|
45
|
+
Your private key is securely stored in a JSON file, encrypted with your first password. This ensures that your sensitive information remains protected while still being accessible when you need it.
|
|
46
|
+
|
|
47
|
+
## 🔬 Transparency
|
|
48
|
+
|
|
49
|
+
We're as transparent as your ex's excuses. Our code is open-source, and we encourage you to dive in, explore, and contribute. Trust isn't given; it's earned and verified.
|
|
50
|
+
|
|
51
|
+
## 🔍 Security Audit
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
## 🤝 Contributing
|
|
56
|
+
|
|
57
|
+
Found a bug? Want to add a feature? We're all ears! Open an issue or submit a PR. Let's make crypto easier together.
|
|
58
|
+
|
|
59
|
+
## 📜 License
|
|
60
|
+
|
|
61
|
+
MIT License. Go wild, but don't blame us if you YOLO your life savings into DogeMoonRocket tokens.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
Remember: With great power comes great responsibility. And with crypto, comes great volatility. HODL responsibly! 🚀🌕
|
package/index.mjs
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
// wallet.js
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import Web3 from 'web3';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import Persist from './persist.js';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname } from 'path';
|
|
9
|
+
import Table from 'cli-table3';
|
|
10
|
+
import bip39 from 'bip39';
|
|
11
|
+
import hdkey from 'hdkey';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
|
17
|
+
inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
|
|
18
|
+
|
|
19
|
+
class Wallet {
|
|
20
|
+
constructor(encryptionKey) {
|
|
21
|
+
this.db = new Persist(encryptionKey);
|
|
22
|
+
this.web3 = null;
|
|
23
|
+
this.selectedNetwork = null;
|
|
24
|
+
this.account = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async initialize() {
|
|
28
|
+
try {
|
|
29
|
+
const networkPlugins = await this.loadNetworkPlugins();
|
|
30
|
+
if (networkPlugins.length === 0) {
|
|
31
|
+
console.log('No valid network plugins found.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
await this.selectNetwork(networkPlugins);
|
|
35
|
+
this.web3 = new Web3(this.selectedNetwork.rpcUrl);
|
|
36
|
+
await this.loadAccount();
|
|
37
|
+
|
|
38
|
+
if (!this.account) {
|
|
39
|
+
throw new Error('Failed to initialize account');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Initialization failed:', error);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
displayAccountAddress() {
|
|
49
|
+
const table = new Table({
|
|
50
|
+
head: ['Account Address'],
|
|
51
|
+
style: {
|
|
52
|
+
head: ['green']
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
table.push([this.account.address]);
|
|
56
|
+
console.log(table.toString());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async loadNetworkPlugins() {
|
|
60
|
+
const pluginsDir = path.join(__dirname, 'network');
|
|
61
|
+
const pluginFiles = fs.readdirSync(pluginsDir).filter(file => file.endsWith('.js'));
|
|
62
|
+
|
|
63
|
+
const networks = await Promise.all(pluginFiles.map(async file => {
|
|
64
|
+
const plugin = await import(`./network/${file}`);
|
|
65
|
+
return plugin.default;
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
return networks.filter(network => network && network.name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async selectNetwork(networkPlugins) {
|
|
72
|
+
const { network } = await inquirer.prompt({
|
|
73
|
+
type: 'list',
|
|
74
|
+
name: 'network',
|
|
75
|
+
message: 'Select the network:',
|
|
76
|
+
choices: networkPlugins.map(plugin => plugin.name),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const selectedNetwork = networkPlugins.find(plugin => plugin.name === network);
|
|
80
|
+
|
|
81
|
+
this.selectedNetwork = selectedNetwork;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async loadAccount(forceReload = false) {
|
|
85
|
+
let account = this.db.secureGet('account');
|
|
86
|
+
|
|
87
|
+
if (!account || forceReload) {
|
|
88
|
+
const { accountAction } = await inquirer.prompt({
|
|
89
|
+
type: 'list',
|
|
90
|
+
name: 'accountAction',
|
|
91
|
+
message: 'Select an account option:',
|
|
92
|
+
choices: ['Import private key', 'Import from 12 words', 'Create new account'],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (accountAction === 'Import private key') {
|
|
96
|
+
const { privateKey } = await inquirer.prompt({
|
|
97
|
+
type: 'password',
|
|
98
|
+
name: 'privateKey',
|
|
99
|
+
message: 'Private key:',
|
|
100
|
+
mask: '*',
|
|
101
|
+
});
|
|
102
|
+
account = this.web3.eth.accounts.privateKeyToAccount(privateKey);
|
|
103
|
+
await this.db.secureSet('account', { privateKey: account.privateKey });
|
|
104
|
+
|
|
105
|
+
this.account = account;
|
|
106
|
+
this.displayAccountAddress();
|
|
107
|
+
} else if (accountAction === 'Import from 12 words') {
|
|
108
|
+
account = await this.importFrom12Words();
|
|
109
|
+
this.account = account;
|
|
110
|
+
this.displayAccountAddress();
|
|
111
|
+
} else {
|
|
112
|
+
const mnemonic = bip39.generateMnemonic();
|
|
113
|
+
const seed = await bip39.mnemonicToSeed(mnemonic);
|
|
114
|
+
const root = hdkey.fromMasterSeed(seed);
|
|
115
|
+
const addrNode = root.derive("m/44'/60'/0'/0/0");
|
|
116
|
+
const privateKey = addrNode.privateKey.toString('hex');
|
|
117
|
+
account = this.web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
|
|
118
|
+
this.db.secureSet('account', { privateKey: account.privateKey });
|
|
119
|
+
|
|
120
|
+
const table = new Table({
|
|
121
|
+
head: [{ colSpan: 2, content: 'New Account Created' }],
|
|
122
|
+
style: { head: ['green'] },
|
|
123
|
+
wordWrap: true
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
table.push(
|
|
127
|
+
['Address', account.address],
|
|
128
|
+
['Private Key', account.privateKey],
|
|
129
|
+
['Mnemonic Phrase', mnemonic],
|
|
130
|
+
['WARNING', "Please write down your mnemonic phrase and keep it in a safe place. \nIt\'s crucial for recovering your account."]
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
console.log('\n' + table.toString());
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
account = this.web3.eth.accounts.privateKeyToAccount(account.privateKey);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.account = account;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async importFrom12Words() {
|
|
143
|
+
const { mnemonic } = await inquirer.prompt({
|
|
144
|
+
type: 'password',
|
|
145
|
+
name: 'mnemonic',
|
|
146
|
+
message: 'Enter your 12-word mnemonic phrase:',
|
|
147
|
+
mask: '*',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
151
|
+
console.error('Invalid mnemonic phrase');
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const seed = await bip39.mnemonicToSeed(mnemonic);
|
|
156
|
+
const root = hdkey.fromMasterSeed(seed);
|
|
157
|
+
const addrNode = root.derive("m/44'/60'/0'/0/0");
|
|
158
|
+
const privateKey = addrNode.privateKey.toString('hex');
|
|
159
|
+
const account = this.web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
|
|
160
|
+
|
|
161
|
+
await this.db.secureSet('account', { privateKey: account.privateKey });
|
|
162
|
+
|
|
163
|
+
return account;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async showBalance() {
|
|
167
|
+
if (!this.account) {
|
|
168
|
+
console.error('Account not initialized. Please try restarting the application.');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const balanceWei = await this.web3.eth.getBalance(this.account.address);
|
|
173
|
+
const balance = Web3.utils.fromWei(balanceWei, 'ether');
|
|
174
|
+
|
|
175
|
+
const table = new Table({
|
|
176
|
+
head: ['Token', 'Balance'],
|
|
177
|
+
style: { head: ['blue'] },
|
|
178
|
+
colWidths: [21, 22]
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Add address as the top row
|
|
182
|
+
// table.push([{ colSpan: 2, content: this.account.address }]);
|
|
183
|
+
table.push([this.selectedNetwork.nativeToken, parseFloat(balance).toFixed(8)]);
|
|
184
|
+
|
|
185
|
+
// Check if USDT is available on the selected network
|
|
186
|
+
if (this.selectedNetwork.tokens['USDT']) {
|
|
187
|
+
const usdtAddress = this.selectedNetwork.tokens['USDT'];
|
|
188
|
+
const ERC20_ABI = [
|
|
189
|
+
{
|
|
190
|
+
constant: true,
|
|
191
|
+
inputs: [{ name: "_owner", type: "address" }],
|
|
192
|
+
name: "balanceOf",
|
|
193
|
+
outputs: [{ name: "balance", type: "uint256" }],
|
|
194
|
+
type: "function"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
constant: true,
|
|
198
|
+
inputs: [],
|
|
199
|
+
name: "decimals",
|
|
200
|
+
outputs: [{ name: "", type: "uint8" }],
|
|
201
|
+
type: "function"
|
|
202
|
+
}
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
const usdtContract = new this.web3.eth.Contract(ERC20_ABI, usdtAddress);
|
|
206
|
+
const usdtBalance = await usdtContract.methods.balanceOf(this.account.address).call();
|
|
207
|
+
const decimals = await usdtContract.methods.decimals().call();
|
|
208
|
+
|
|
209
|
+
// Fixed calculation using BigInt consistently
|
|
210
|
+
const formattedUsdtBalance = Number(
|
|
211
|
+
(BigInt(usdtBalance) * 100n) / (10n ** BigInt(decimals))
|
|
212
|
+
) / 100;
|
|
213
|
+
|
|
214
|
+
table.push(['USDT', formattedUsdtBalance.toFixed(8)]);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(table.toString());
|
|
218
|
+
console.log('');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async transferFunds() {
|
|
222
|
+
let addressBook = this.db.get('addressBook') || [];
|
|
223
|
+
|
|
224
|
+
const { token } = await inquirer.prompt({
|
|
225
|
+
type: 'list',
|
|
226
|
+
name: 'token',
|
|
227
|
+
message: 'Token to transfer:',
|
|
228
|
+
choices: Object.keys(this.selectedNetwork.tokens),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const tokens = this.selectedNetwork.tokens;
|
|
232
|
+
|
|
233
|
+
// Implement autocomplete for address book
|
|
234
|
+
const { recipient } = await inquirer.prompt({
|
|
235
|
+
type: 'autocomplete',
|
|
236
|
+
name: 'recipient',
|
|
237
|
+
message: 'Recipient address:',
|
|
238
|
+
source: (answersSoFar, input) => {
|
|
239
|
+
input = input || '';
|
|
240
|
+
return addressBook
|
|
241
|
+
.filter(entry => entry.name.toLowerCase().includes(input.toLowerCase()) || entry.address.toLowerCase().includes(input.toLowerCase()))
|
|
242
|
+
.map(entry => ({ name: `${entry.name} (${entry.address})`, value: entry.address }))
|
|
243
|
+
.concat([{ name: input, value: input }]); // Add the input as a possible choice
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
let address = recipient;
|
|
248
|
+
|
|
249
|
+
const { amount } = await inquirer.prompt({
|
|
250
|
+
type: 'input',
|
|
251
|
+
name: 'amount',
|
|
252
|
+
message: `Amount to transfer [${token}] (leave empty to cancel):`,
|
|
253
|
+
validate: value => {
|
|
254
|
+
if (value === '') return true;
|
|
255
|
+
return !isNaN(value) && Number(value) > 0 ? true : 'Please enter a valid number or leave empty to cancel.';
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (amount === '') {
|
|
260
|
+
console.log('Transaction cancelled.');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
let signedTx;
|
|
266
|
+
if (tokens[token] && token !== this.selectedNetwork.nativeToken) {
|
|
267
|
+
signedTx = await this.handleERC20Transfer(token, address, amount);
|
|
268
|
+
} else {
|
|
269
|
+
signedTx = await this.handleNativeTransfer(address, amount);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const receipt = await this.web3.eth.sendSignedTransaction(signedTx.rawTransaction);
|
|
273
|
+
await this.displayTransactionResult(receipt);
|
|
274
|
+
|
|
275
|
+
// Add transaction to history
|
|
276
|
+
this.addToTransactions(address, token, amount, receipt);
|
|
277
|
+
|
|
278
|
+
if (!addressBook.some(entry => entry.address === address)) {
|
|
279
|
+
await this.addToAddressBook(address);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
} catch (error) {
|
|
283
|
+
await this.displayTransactionResult(null, error);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
addToTransactions(recipient, token, amount, receipt) {
|
|
288
|
+
const transaction = {
|
|
289
|
+
timestamp: new Date().toISOString(),
|
|
290
|
+
recipient,
|
|
291
|
+
token,
|
|
292
|
+
amount,
|
|
293
|
+
hash: receipt.transactionHash
|
|
294
|
+
};
|
|
295
|
+
this.db.add('transactions', this.selectedNetwork.nativeToken, transaction);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async showTransactions() {
|
|
299
|
+
const history = this.db.values('transactions', this.selectedNetwork.nativeToken) || [];
|
|
300
|
+
|
|
301
|
+
const table = new Table({
|
|
302
|
+
head: ['Date', 'Recipient', 'Token', 'Amount'],
|
|
303
|
+
style: { head: ['blue'] },
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (history.length === 0) {
|
|
307
|
+
table.push([{ colSpan: 4, content: 'No transaction history available.' }]);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
history.forEach(tx => {
|
|
311
|
+
const date = new Date(tx.timestamp).toLocaleString('en-GB', {
|
|
312
|
+
year: 'numeric',
|
|
313
|
+
month: '2-digit',
|
|
314
|
+
day: '2-digit',
|
|
315
|
+
hour: '2-digit',
|
|
316
|
+
minute: '2-digit',
|
|
317
|
+
hour12: false
|
|
318
|
+
}).replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$2-$1').replace(",", "");
|
|
319
|
+
table.push([date, tx.recipient, tx.token, tx.amount]);
|
|
320
|
+
table.push([{ colSpan: 4, content: this.selectedNetwork.explorer + tx.hash }]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
console.log(table.toString());
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async addToAddressBook(address) {
|
|
327
|
+
const { name } = await inquirer.prompt({
|
|
328
|
+
type: 'input',
|
|
329
|
+
name: 'name',
|
|
330
|
+
message: 'Name for the address book (leave empty to skip):',
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (name.trim() !== '') {
|
|
334
|
+
let addressBook = this.db.get('addressBook') || [];
|
|
335
|
+
addressBook.push({ name, address });
|
|
336
|
+
this.db.set('addressBook', addressBook);
|
|
337
|
+
|
|
338
|
+
const table = new Table({
|
|
339
|
+
head: [{ colSpan: 2, content: "Recipient saved to the address book." }],
|
|
340
|
+
style: { head: ['green'] },
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
table.push([name, address]);
|
|
344
|
+
console.log('\n' + table.toString());
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async handleERC20Transfer(token, recipient, amount) {
|
|
349
|
+
const ERC20_ABI = [
|
|
350
|
+
// Minimal ABI to interact with ERC20 tokens
|
|
351
|
+
{
|
|
352
|
+
constant: true,
|
|
353
|
+
name: 'decimals',
|
|
354
|
+
inputs: [],
|
|
355
|
+
outputs: [{ name: '', type: 'uint8' }],
|
|
356
|
+
type: 'function',
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
constant: false,
|
|
360
|
+
name: 'transfer',
|
|
361
|
+
inputs: [
|
|
362
|
+
{ name: '_to', type: 'address' },
|
|
363
|
+
{ name: '_value', type: 'uint256' },
|
|
364
|
+
],
|
|
365
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
366
|
+
type: 'function',
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
const contract = new this.web3.eth.Contract(ERC20_ABI, this.selectedNetwork.tokens[token]);
|
|
371
|
+
const decimals = parseInt(await contract.methods.decimals().call(), 10);
|
|
372
|
+
|
|
373
|
+
const weiAmount = BigInt(this.web3.utils.toWei(amount, 'ether'));
|
|
374
|
+
const adjustedAmount = weiAmount / (10n ** BigInt(18 - decimals));
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const data = contract.methods.transfer(recipient, adjustedAmount.toString()).encodeABI();
|
|
378
|
+
const gasLimit = 100000;
|
|
379
|
+
const gasPrice = await this.web3.eth.getGasPrice();
|
|
380
|
+
|
|
381
|
+
const tx = {
|
|
382
|
+
from: this.account.address,
|
|
383
|
+
to: this.selectedNetwork.tokens[token],
|
|
384
|
+
data: data,
|
|
385
|
+
gas: gasLimit,
|
|
386
|
+
gasPrice: gasPrice,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
return this.web3.eth.accounts.signTransaction(tx, this.account.privateKey);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (error instanceof Web3ValidatorError) {
|
|
392
|
+
const table = new Table({
|
|
393
|
+
head: [{ colSpan: 2, content: 'Error: Invalid Address' }],
|
|
394
|
+
style: { head: ['red'] }
|
|
395
|
+
});
|
|
396
|
+
table.push(['Address', recipient]);
|
|
397
|
+
console.log(table.toString());
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async handleNativeTransfer(recipient, amount) {
|
|
403
|
+
const gasPrice = await this.web3.eth.getGasPrice();
|
|
404
|
+
const gasLimit = 21000; // Gas estándar para una transacción simple
|
|
405
|
+
const gasCost = BigInt(gasPrice) * BigInt(gasLimit);
|
|
406
|
+
let amountWei = BigInt(this.web3.utils.toWei(amount, 'ether'));
|
|
407
|
+
|
|
408
|
+
// Obtener el balance actual
|
|
409
|
+
const balance = BigInt(await this.web3.eth.getBalance(this.account.address));
|
|
410
|
+
|
|
411
|
+
if (balance < amountWei + gasCost) {
|
|
412
|
+
amountWei -= gasCost;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (balance < amountWei + gasCost) {
|
|
416
|
+
const maxAmount = this.web3.utils.fromWei((balance - gasCost).toString(), 'ether');
|
|
417
|
+
const table = new Table({
|
|
418
|
+
head: [{ colSpan: 2, content: 'Insufficient balance for this transaction' }],
|
|
419
|
+
style: { head: ['red'] },
|
|
420
|
+
wordWrap: true
|
|
421
|
+
});
|
|
422
|
+
table.push(['Maximum Amount', `${maxAmount} ${this.selectedNetwork.nativeToken}`]);
|
|
423
|
+
console.log(table.toString());
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const tx = {
|
|
428
|
+
from: this.account.address,
|
|
429
|
+
to: recipient,
|
|
430
|
+
value: amountWei.toString(),
|
|
431
|
+
gas: gasLimit,
|
|
432
|
+
gasPrice: gasPrice
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
return this.web3.eth.accounts.signTransaction(tx, this.account.privateKey);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async displayTransactionResult(receipt, error = null) {
|
|
439
|
+
if (receipt) {
|
|
440
|
+
const gasUsed = receipt.gasUsed;
|
|
441
|
+
const gasPrice = await this.web3.eth.getGasPrice();
|
|
442
|
+
const transactionFee = this.web3.utils.fromWei((BigInt(gasUsed) * BigInt(gasPrice)).toString(), 'ether');
|
|
443
|
+
|
|
444
|
+
const table = new Table({
|
|
445
|
+
head: [{ colSpan: 2, content: 'Transaction successful, share this link with the recipient' }],
|
|
446
|
+
style: { head: ['green'] }
|
|
447
|
+
});
|
|
448
|
+
table.push(
|
|
449
|
+
['Explorer', this.selectedNetwork.explorer + receipt.transactionHash],
|
|
450
|
+
['Fee', `${transactionFee} ${this.selectedNetwork.nativeToken}`],
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
console.log('\n' + table.toString());
|
|
454
|
+
} else if (error) {
|
|
455
|
+
const table = new Table({
|
|
456
|
+
head: [error.message],
|
|
457
|
+
style: { head: ['red'] },
|
|
458
|
+
wordWrap: true,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (error.reason) {
|
|
462
|
+
table.push([error.reason.replace(/(\w+):/g, "\n$1:").trim()]);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
console.log('\n' + table.toString());
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
clearAccountData() {
|
|
470
|
+
if (this.account) {
|
|
471
|
+
this.account.privateKey = '0'.repeat(64);
|
|
472
|
+
this.account = null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
class UIManager {
|
|
478
|
+
static displayWelcome() {
|
|
479
|
+
console.log('\x1b[32m'); // Set text color to green
|
|
480
|
+
console.log(` ░░░░░░░░░░░░░░ █ █ █▀█ █▀▄ █ ░░░░░░░░░░░░░░
|
|
481
|
+
░░░░░░░░░░░░░░ █▀█ █▄█ █▄▀ █▄▄ ░░░░░░░░░░░░░░
|
|
482
|
+
░░░░░░░░░░░░░░ ──────── WALLET ░░░░░░░░░░░░░░`);
|
|
483
|
+
console.log('\x1b[0m'); // Reset text color
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
static async getEncryptionKey() {
|
|
487
|
+
const { key } = await inquirer.prompt({
|
|
488
|
+
type: 'password',
|
|
489
|
+
name: 'key',
|
|
490
|
+
message: 'Password:',
|
|
491
|
+
mask: '*',
|
|
492
|
+
});
|
|
493
|
+
return key;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
static async confirmEncryptionKey(originalKey) {
|
|
497
|
+
const { confirmKey } = await inquirer.prompt({
|
|
498
|
+
type: 'password',
|
|
499
|
+
name: 'confirmKey',
|
|
500
|
+
message: 'Repeat Password:',
|
|
501
|
+
mask: '*',
|
|
502
|
+
});
|
|
503
|
+
return confirmKey;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
static displayExitPhrase() {
|
|
507
|
+
const phrases = [
|
|
508
|
+
"Buy the rumor, sell the news",
|
|
509
|
+
"The trend is your friend",
|
|
510
|
+
"Don't fight the tape",
|
|
511
|
+
"Cut your losses and let your profits run",
|
|
512
|
+
"Be fearful when others are greedy, and greedy when others are fearful",
|
|
513
|
+
"The market can remain irrational longer than you can remain solvent",
|
|
514
|
+
"Bulls make money, bears make money, pigs get slaughtered",
|
|
515
|
+
"No one is bigger than the market",
|
|
516
|
+
"Don't catch a falling knife",
|
|
517
|
+
"Past performance is not indicative of future results",
|
|
518
|
+
"The stock market is a device for transferring money from the impatient to the patient",
|
|
519
|
+
"Time in the market beats timing the market",
|
|
520
|
+
"Buy low, sell high",
|
|
521
|
+
"Diversification is the only free lunch in investing",
|
|
522
|
+
"The four most dangerous words in investing are: 'This time it's different'",
|
|
523
|
+
"Markets can remain irrational a lot longer than you and I can remain solvent",
|
|
524
|
+
"Risk comes from not knowing what you're doing",
|
|
525
|
+
"In the short run, the market is a voting machine. In the long run, it's a weighing machine",
|
|
526
|
+
"Invest in yourself. Your career is the engine of your wealth",
|
|
527
|
+
"Who has the gold makes the rules",
|
|
528
|
+
"The best time to invest was yesterday. The second best time is now",
|
|
529
|
+
"Don't put all your eggs in one basket",
|
|
530
|
+
"Knowledge is power in the world of investing",
|
|
531
|
+
"Patience is a virtue in the stock market",
|
|
532
|
+
"The market is never wrong, but opinions often are"
|
|
533
|
+
];
|
|
534
|
+
const randomPhrase = phrases[Math.floor(Math.random() * phrases.length)];
|
|
535
|
+
|
|
536
|
+
const table = new Table({
|
|
537
|
+
head: ['✨ Good bye!'],
|
|
538
|
+
style: { head: ['yellow'] },
|
|
539
|
+
wordWrap: true,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
table.push([randomPhrase]);
|
|
543
|
+
|
|
544
|
+
console.log(table.toString());
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function main() {
|
|
549
|
+
UIManager.displayWelcome();
|
|
550
|
+
const encryptionKey = await UIManager.getEncryptionKey();
|
|
551
|
+
const wallet = new Wallet(encryptionKey);
|
|
552
|
+
|
|
553
|
+
// Check if an account exists
|
|
554
|
+
const accountExists = wallet.db.secureGet('account');
|
|
555
|
+
if (accountExists === null) {
|
|
556
|
+
console.error('Wrong password.');
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!accountExists) {
|
|
561
|
+
const confirmedKey = await UIManager.confirmEncryptionKey(encryptionKey);
|
|
562
|
+
if (confirmedKey !== encryptionKey) {
|
|
563
|
+
console.error('Passwords do not match. Please try again.');
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
try {
|
|
569
|
+
await wallet.initialize();
|
|
570
|
+
if (accountExists) wallet.displayAccountAddress();
|
|
571
|
+
} catch (error) {
|
|
572
|
+
console.error('Failed to initialize wallet:', error);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Register the displayExitPhrase method and account clearing to be called on process exit
|
|
577
|
+
process.on('exit', () => {
|
|
578
|
+
wallet.clearAccountData();
|
|
579
|
+
UIManager.displayExitPhrase();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
while (true) {
|
|
583
|
+
const { action } = await inquirer.prompt({
|
|
584
|
+
type: 'list',
|
|
585
|
+
name: 'action',
|
|
586
|
+
message: 'What would you like to do?',
|
|
587
|
+
choices: ['Transfer funds', 'Show balance', 'Show transactions', 'Change account', 'Exit'],
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
if (action === 'Transfer funds') {
|
|
591
|
+
await wallet.transferFunds();
|
|
592
|
+
} else if (action === 'Show balance') {
|
|
593
|
+
await wallet.showBalance();
|
|
594
|
+
} else if (action === 'Show transactions') {
|
|
595
|
+
await wallet.showTransactions();
|
|
596
|
+
} else if (action === 'Change account') {
|
|
597
|
+
await wallet.loadAccount(true);
|
|
598
|
+
} else {
|
|
599
|
+
process.exit();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
main().catch(error => {
|
|
605
|
+
console.error('An unexpected error occurred:', error);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
process.on('SIGINT', () => {
|
|
610
|
+
console.log('\n');
|
|
611
|
+
process.exit();
|
|
612
|
+
});
|
package/network/bsc.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// plugins/bsc.js
|
|
2
|
+
module.exports = {
|
|
3
|
+
name: '[BEP-20] Binance Smart Chain',
|
|
4
|
+
explorer: 'https://bscscan.com/tx/',
|
|
5
|
+
rpcUrl: 'https://bsc-dataseed.binance.org/',
|
|
6
|
+
nativeToken: 'BNB',
|
|
7
|
+
tokens: {
|
|
8
|
+
'USDT': '0x55d398326f99059fF775485246999027B3197955', // Dirección del contrato USDT en BSC
|
|
9
|
+
'BNB': '0x0000000000000000000000000000000000000000', // Dirección nativa (BNB)
|
|
10
|
+
// Puedes agregar otros tokens BEP20 aquí
|
|
11
|
+
},
|
|
12
|
+
};
|
package/network/erc.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
name: '[ERC-20] Ethereum',
|
|
3
|
+
explorer: 'https://etherscan.io/tx/',
|
|
4
|
+
rpcUrl: 'https://eth.public-rpc.com',
|
|
5
|
+
nativeToken: 'ETH',
|
|
6
|
+
tokens: {
|
|
7
|
+
'USDT': '0xdAC17F958D2ee523a2206206994597C13D831ec7', // Dirección del contrato USDT en Ethereum
|
|
8
|
+
'ETH': '0x0000000000000000000000000000000000000000', // Dirección nativa (ETH)
|
|
9
|
+
// Puedes agregar otros tokens ERC20 aquí
|
|
10
|
+
},
|
|
11
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hodl-wallet",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.mjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hodl": "./index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node ./index.mjs",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"hodl",
|
|
15
|
+
"wallet",
|
|
16
|
+
"crypto",
|
|
17
|
+
"ethereum",
|
|
18
|
+
"bsc20",
|
|
19
|
+
"bep20",
|
|
20
|
+
"evm",
|
|
21
|
+
"bnb",
|
|
22
|
+
"eth",
|
|
23
|
+
"binance",
|
|
24
|
+
"trust",
|
|
25
|
+
"blockchain"
|
|
26
|
+
],
|
|
27
|
+
"author": "Hodl Wallet",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"bip39": "^3.1.0",
|
|
31
|
+
"cli-table3": "^0.6.5",
|
|
32
|
+
"crypto-js": "^4.2.0",
|
|
33
|
+
"deepbase": "^1.1.8",
|
|
34
|
+
"hdkey": "^2.1.0",
|
|
35
|
+
"inquirer": "^9.3.7",
|
|
36
|
+
"inquirer-autocomplete-prompt": "^3.0.1",
|
|
37
|
+
"web3": "^4.13.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/persist.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// db.js
|
|
2
|
+
const Deepbase = require('deepbase');
|
|
3
|
+
const CryptoJS = require('crypto-js');
|
|
4
|
+
|
|
5
|
+
class Persist extends Deepbase {
|
|
6
|
+
constructor(encryptionKey) {
|
|
7
|
+
super();
|
|
8
|
+
this.encryptionKey = encryptionKey;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
encrypt(data) {
|
|
12
|
+
const iv = CryptoJS.lib.WordArray.random(128/8);
|
|
13
|
+
const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data), this.encryptionKey, { iv });
|
|
14
|
+
return iv.toString(CryptoJS.enc.Hex) + ':' + encrypted.toString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
decrypt(encryptedData) {
|
|
18
|
+
const [ivHex, encrypted] = encryptedData.split(':');
|
|
19
|
+
const iv = CryptoJS.enc.Hex.parse(ivHex);
|
|
20
|
+
const bytes = CryptoJS.AES.decrypt(encrypted, this.encryptionKey, { iv });
|
|
21
|
+
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Method to set encrypted account
|
|
25
|
+
secureSet(key, value) {
|
|
26
|
+
const encryptedData = this.encrypt(value);
|
|
27
|
+
return this.set(key, encryptedData);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Method to retrieve encrypted account
|
|
31
|
+
secureGet(key) {
|
|
32
|
+
const data = this.get(key);
|
|
33
|
+
if (!data) return "";
|
|
34
|
+
try {
|
|
35
|
+
// Attempt to decrypt
|
|
36
|
+
return this.decrypt(data);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = Persist;
|