hodl-wallet 1.9.2 → 1.9.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 +1 -2
- package/network/btc.js +1 -1
- package/network/lib/BitcoinNetwork.js +29 -10
- package/package.json +72 -74
- package/persist.js +81 -8
- package/pnpm-workspace.yaml +7 -0
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ We've carefully selected trusted and well-maintained dependencies for this proje
|
|
|
92
92
|
- **cli-table3**: For creating formatted CLI tables.
|
|
93
93
|
- **ora**: For displaying progress bars.
|
|
94
94
|
- **deepbase**: For persistent storage.
|
|
95
|
-
- **crypto
|
|
95
|
+
- **node:crypto**: Native encryption for JSON storage.
|
|
96
96
|
- **bip39**: For generating and handling mnemonic phrases.
|
|
97
97
|
- Web3
|
|
98
98
|
- **web3**: The Ethereum JavaScript API for blockchain interactions.
|
|
@@ -102,7 +102,6 @@ We've carefully selected trusted and well-maintained dependencies for this proje
|
|
|
102
102
|
- **bip32**: For handling hierarchical deterministic (HD) keys.
|
|
103
103
|
- **ecpair**: For elliptic curve pairings.
|
|
104
104
|
- **tiny-secp256k1**: For elliptic curve secp256k1 operations.
|
|
105
|
-
- **axios**: For making HTTP requests.
|
|
106
105
|
|
|
107
106
|
⚠️ **Important Notice**: HODL Wallet is a personal project created with the best intentions. While we strive for security, it may contain security flaws or vulnerabilities. Use at your own risk and always exercise caution with your crypto assets.
|
|
108
107
|
|
package/network/btc.js
CHANGED
|
@@ -4,7 +4,6 @@ import bip39 from 'bip39';
|
|
|
4
4
|
import * as ecc from 'tiny-secp256k1';
|
|
5
5
|
import { BIP32Factory } from 'bip32';
|
|
6
6
|
import { ECPairFactory } from 'ecpair';
|
|
7
|
-
import axios from 'axios';
|
|
8
7
|
|
|
9
8
|
const bip32 = BIP32Factory(ecc);
|
|
10
9
|
const ECPair = ECPairFactory(ecc);
|
|
@@ -19,9 +18,9 @@ export default class BitcoinNetwork extends BaseNetwork {
|
|
|
19
18
|
|
|
20
19
|
async getBalance(address) {
|
|
21
20
|
try {
|
|
22
|
-
const
|
|
23
|
-
const chainStats =
|
|
24
|
-
const mempoolStats =
|
|
21
|
+
const data = await this.fetchFromApi(`/address/${address}`);
|
|
22
|
+
const chainStats = data.chain_stats;
|
|
23
|
+
const mempoolStats = data.mempool_stats;
|
|
25
24
|
const balance = (chainStats.funded_txo_sum - chainStats.spent_txo_sum) +
|
|
26
25
|
(mempoolStats.funded_txo_sum - mempoolStats.spent_txo_sum);
|
|
27
26
|
return this.satoshisToBTC(balance);
|
|
@@ -118,8 +117,7 @@ export default class BitcoinNetwork extends BaseNetwork {
|
|
|
118
117
|
// Add this helper method to get full transaction data
|
|
119
118
|
async getTransaction(txid) {
|
|
120
119
|
try {
|
|
121
|
-
|
|
122
|
-
return response.data;
|
|
120
|
+
return await this.fetchFromApi(`/tx/${txid}/hex`, {}, 'text');
|
|
123
121
|
} catch (error) {
|
|
124
122
|
throw new Error(`Failed to get transaction: ${error.message}`);
|
|
125
123
|
}
|
|
@@ -211,8 +209,7 @@ export default class BitcoinNetwork extends BaseNetwork {
|
|
|
211
209
|
|
|
212
210
|
async getUTXOs(address) {
|
|
213
211
|
try {
|
|
214
|
-
|
|
215
|
-
return response.data;
|
|
212
|
+
return await this.fetchFromApi(`/address/${address}/utxo`);
|
|
216
213
|
} catch (error) {
|
|
217
214
|
throw new Error(`Failed to get UTXOs: ${error.message}`);
|
|
218
215
|
}
|
|
@@ -224,10 +221,32 @@ export default class BitcoinNetwork extends BaseNetwork {
|
|
|
224
221
|
|
|
225
222
|
async sendSignedTransaction(signedTx) {
|
|
226
223
|
try {
|
|
227
|
-
const
|
|
228
|
-
|
|
224
|
+
const txHash = await this.fetchFromApi(
|
|
225
|
+
'/tx',
|
|
226
|
+
{
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: {
|
|
229
|
+
'Content-Type': 'text/plain'
|
|
230
|
+
},
|
|
231
|
+
body: signedTx
|
|
232
|
+
},
|
|
233
|
+
'text'
|
|
234
|
+
);
|
|
235
|
+
return { transactionHash: txHash };
|
|
229
236
|
} catch (error) {
|
|
230
237
|
throw new Error(`Failed to broadcast transaction: ${error.message}`);
|
|
231
238
|
}
|
|
232
239
|
}
|
|
240
|
+
|
|
241
|
+
async fetchFromApi(path, options = {}, responseType = 'json') {
|
|
242
|
+
const response = await fetch(`${this.url}${path}`, options);
|
|
243
|
+
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
const errorBody = await response.text();
|
|
246
|
+
const details = errorBody ? ` - ${errorBody}` : '';
|
|
247
|
+
throw new Error(`${response.status} ${response.statusText}${details}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return responseType === 'text' ? response.text() : response.json();
|
|
251
|
+
}
|
|
233
252
|
}
|
package/package.json
CHANGED
|
@@ -1,75 +1,73 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"type": "module"
|
|
75
|
-
}
|
|
2
|
+
"name": "hodl-wallet",
|
|
3
|
+
"version": "1.9.6",
|
|
4
|
+
"description": "🧊 HODL Wallet - Fast CLI crypto wallet!",
|
|
5
|
+
"author": "Martin Clasen",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/clasen/HODL.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/clasen/HODL/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/clasen/HODL#readme",
|
|
15
|
+
"main": "index.js",
|
|
16
|
+
"bin": {
|
|
17
|
+
"hodl": "index.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"hodl",
|
|
21
|
+
"wallet",
|
|
22
|
+
"crypto",
|
|
23
|
+
"ethereum",
|
|
24
|
+
"bsc20",
|
|
25
|
+
"bep20",
|
|
26
|
+
"bnb",
|
|
27
|
+
"eth",
|
|
28
|
+
"binance",
|
|
29
|
+
"trust",
|
|
30
|
+
"blockchain",
|
|
31
|
+
"bitcoin",
|
|
32
|
+
"usdt",
|
|
33
|
+
"stable",
|
|
34
|
+
"coin",
|
|
35
|
+
"evm",
|
|
36
|
+
"polygon",
|
|
37
|
+
"matic",
|
|
38
|
+
"arbitrum",
|
|
39
|
+
"fantom",
|
|
40
|
+
"optimism",
|
|
41
|
+
"avalanche",
|
|
42
|
+
"avax",
|
|
43
|
+
"clasen"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"bip32": "^4.0.0",
|
|
47
|
+
"bip39": "3.1.0",
|
|
48
|
+
"bitcoinjs-lib": "6.1.7",
|
|
49
|
+
"cli-table3": "0.6.5",
|
|
50
|
+
"deepbase": "1.5.2",
|
|
51
|
+
"ecpair": "2.1.0",
|
|
52
|
+
"external-editor": "3.1.0",
|
|
53
|
+
"hdkey": "^0.6.0",
|
|
54
|
+
"inquirer": "9.3.7",
|
|
55
|
+
"inquirer-autocomplete-prompt": "3.0.1",
|
|
56
|
+
"inquirer-fuzzy-path": "2.3.0",
|
|
57
|
+
"ora": "8.1.1",
|
|
58
|
+
"tiny-secp256k1": "2.2.3",
|
|
59
|
+
"tmp": "0.2.4",
|
|
60
|
+
"web3": "4.14.0"
|
|
61
|
+
},
|
|
62
|
+
"overrides": {
|
|
63
|
+
"tmp": "0.2.4"
|
|
64
|
+
},
|
|
65
|
+
"type": "module",
|
|
66
|
+
"scripts": {
|
|
67
|
+
"start": "node ./index.js",
|
|
68
|
+
"test": "node ./test/test-all.js",
|
|
69
|
+
"test:network": "node ./test/test.js",
|
|
70
|
+
"test:integration": "node ./test/test-integration.js",
|
|
71
|
+
"test:quick": "node ./test/test.js --quick"
|
|
72
|
+
}
|
|
73
|
+
}
|
package/persist.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import Deepbase from 'deepbase';
|
|
2
|
-
import
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
5
|
+
const KEY_LENGTH = 32;
|
|
6
|
+
const IV_LENGTH = 12;
|
|
7
|
+
const SALT_LENGTH = 16;
|
|
3
8
|
|
|
4
9
|
class Persist extends Deepbase {
|
|
5
10
|
constructor(opts) {
|
|
@@ -10,16 +15,84 @@ class Persist extends Deepbase {
|
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
static encrypt(obj, encryptionKey) {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
18
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
19
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
20
|
+
const key = crypto.scryptSync(encryptionKey, salt, KEY_LENGTH);
|
|
21
|
+
|
|
22
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
23
|
+
const encrypted = Buffer.concat([
|
|
24
|
+
cipher.update(JSON.stringify(obj), 'utf8'),
|
|
25
|
+
cipher.final()
|
|
26
|
+
]);
|
|
27
|
+
const authTag = cipher.getAuthTag();
|
|
28
|
+
|
|
29
|
+
return `v2:${salt.toString('hex')}:${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
|
|
16
30
|
}
|
|
17
31
|
|
|
18
32
|
static decrypt(encryptedData, encryptionKey) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
const parts = encryptedData.split(':');
|
|
34
|
+
|
|
35
|
+
if (parts[0] === 'v2' && parts.length === 5) {
|
|
36
|
+
const [, saltHex, ivHex, authTagHex, encryptedHex] = parts;
|
|
37
|
+
const salt = Buffer.from(saltHex, 'hex');
|
|
38
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
39
|
+
const authTag = Buffer.from(authTagHex, 'hex');
|
|
40
|
+
const encrypted = Buffer.from(encryptedHex, 'hex');
|
|
41
|
+
const key = crypto.scryptSync(encryptionKey, salt, KEY_LENGTH);
|
|
42
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
43
|
+
decipher.setAuthTag(authTag);
|
|
44
|
+
|
|
45
|
+
const decrypted = Buffer.concat([
|
|
46
|
+
decipher.update(encrypted),
|
|
47
|
+
decipher.final()
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
return JSON.parse(decrypted.toString('utf8'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Persist.decryptLegacy(encryptedData, encryptionKey);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static decryptLegacy(encryptedData, encryptionKey) {
|
|
57
|
+
const [, encrypted] = encryptedData.split(':');
|
|
58
|
+
const payload = Buffer.from(encrypted, 'base64');
|
|
59
|
+
|
|
60
|
+
const saltedPrefix = payload.subarray(0, 8).toString('utf8');
|
|
61
|
+
if (saltedPrefix !== 'Salted__') {
|
|
62
|
+
throw new Error('Unsupported encrypted payload format');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const salt = payload.subarray(8, 16);
|
|
66
|
+
const ciphertext = payload.subarray(16);
|
|
67
|
+
const { key, iv } = Persist.evpBytesToKey(
|
|
68
|
+
Buffer.from(encryptionKey, 'utf8'),
|
|
69
|
+
salt,
|
|
70
|
+
KEY_LENGTH,
|
|
71
|
+
16
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
75
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
76
|
+
return JSON.parse(decrypted.toString('utf8'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static evpBytesToKey(password, salt, keyLen, ivLen) {
|
|
80
|
+
let derived = Buffer.alloc(0);
|
|
81
|
+
let block = Buffer.alloc(0);
|
|
82
|
+
|
|
83
|
+
while (derived.length < keyLen + ivLen) {
|
|
84
|
+
const hash = crypto.createHash('md5');
|
|
85
|
+
hash.update(block);
|
|
86
|
+
hash.update(password);
|
|
87
|
+
hash.update(salt);
|
|
88
|
+
block = hash.digest();
|
|
89
|
+
derived = Buffer.concat([derived, block]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
key: derived.subarray(0, keyLen),
|
|
94
|
+
iv: derived.subarray(keyLen, keyLen + ivLen)
|
|
95
|
+
};
|
|
23
96
|
}
|
|
24
97
|
}
|
|
25
98
|
|