hodl-wallet 2.1.0 → 2.2.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 +1 -1
- package/dist/app/account.d.ts +7 -0
- package/dist/app/account.js +248 -0
- package/dist/app/account.js.map +1 -0
- package/dist/app/format.d.ts +3 -0
- package/dist/app/format.js +27 -0
- package/dist/app/format.js.map +1 -0
- package/dist/app/index.d.ts +8 -0
- package/dist/app/index.js +7 -0
- package/dist/app/index.js.map +1 -0
- package/dist/app/menu.d.ts +12 -0
- package/dist/app/menu.js +78 -0
- package/dist/app/menu.js.map +1 -0
- package/dist/app/output.d.ts +15 -0
- package/dist/app/output.js +81 -0
- package/dist/app/output.js.map +1 -0
- package/dist/app/session.d.ts +105 -0
- package/dist/app/session.js +2 -0
- package/dist/app/session.js.map +1 -0
- package/dist/app/transfer.d.ts +3 -0
- package/dist/app/transfer.js +144 -0
- package/dist/app/transfer.js.map +1 -0
- package/dist/app/ui.d.ts +60 -0
- package/dist/app/ui.js +15 -0
- package/dist/app/ui.js.map +1 -0
- package/dist/browser-core.d.ts +2 -0
- package/dist/browser-core.js +2 -0
- package/dist/browser-core.js.map +1 -1
- package/dist/index.js +24 -1096
- package/dist/index.js.map +1 -1
- package/dist/node-session.d.ts +51 -0
- package/dist/node-session.js +410 -0
- package/dist/node-session.js.map +1 -0
- package/dist/persist.d.ts +6 -0
- package/dist/persist.js +45 -25
- package/dist/persist.js.map +1 -1
- package/dist/ui-inquirer.d.ts +30 -0
- package/dist/ui-inquirer.js +62 -0
- package/dist/ui-inquirer.js.map +1 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
|
-
import { AgentError } from './agent-errors.js';
|
|
5
|
-
import { TransferService } from './transfer-service.js';
|
|
6
3
|
import path from 'path';
|
|
7
|
-
import inquirer from 'inquirer';
|
|
8
|
-
import Persist from './persist.js';
|
|
9
|
-
import { normalizeDecimal } from './amounts.js';
|
|
10
|
-
import { NetworkRegistry, networkStorageName } from './network-registry.js';
|
|
11
4
|
import { ProfileLock, ProfileLockedError } from './profile-lock.js';
|
|
12
|
-
import { SwapService } from './swap/service.js';
|
|
13
|
-
import { routeForNetwork } from './swap/routes.js';
|
|
14
|
-
import { startSwapMenu } from './swap/ui.js';
|
|
15
5
|
import { fileURLToPath } from 'url';
|
|
16
|
-
import Table from 'cli-table3';
|
|
17
6
|
import os from 'os';
|
|
18
|
-
import
|
|
19
|
-
|
|
7
|
+
import { NodeSession } from './node-session.js';
|
|
8
|
+
import { InquirerUi } from './ui-inquirer.js';
|
|
9
|
+
import { errorMessage } from './app/format.js';
|
|
10
|
+
import { farewell, showError, welcome } from './app/output.js';
|
|
11
|
+
import { runWallet } from './app/menu.js';
|
|
20
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const ui = new InquirerUi();
|
|
21
14
|
function suppressPunycodeDeprecationWarning() {
|
|
22
15
|
const warningListeners = process.rawListeners('warning');
|
|
23
16
|
process.removeAllListeners('warning');
|
|
@@ -29,1085 +22,22 @@ function suppressPunycodeDeprecationWarning() {
|
|
|
29
22
|
});
|
|
30
23
|
}
|
|
31
24
|
suppressPunycodeDeprecationWarning();
|
|
32
|
-
|
|
33
|
-
return (typeof value === 'object' &&
|
|
34
|
-
value !== null &&
|
|
35
|
-
!Array.isArray(value) &&
|
|
36
|
-
typeof value.count === 'number');
|
|
37
|
-
}
|
|
38
|
-
function errorMessage(error) {
|
|
39
|
-
return error instanceof Error ? error.message : String(error);
|
|
40
|
-
}
|
|
41
|
-
import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
|
42
|
-
inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
|
|
43
|
-
class Wallet {
|
|
44
|
-
db;
|
|
45
|
-
network;
|
|
46
|
-
selectedNetwork;
|
|
47
|
-
networkUsage;
|
|
48
|
-
databaseExisted;
|
|
49
|
-
profileLock;
|
|
50
|
-
hodlDir;
|
|
51
|
-
constructor(encryptionKey, profileLock) {
|
|
52
|
-
const hodlDir = path.join(os.homedir(), '.HODL');
|
|
53
|
-
this.hodlDir = hodlDir;
|
|
54
|
-
if (!fs.existsSync(hodlDir)) {
|
|
55
|
-
fs.mkdirSync(hodlDir, { recursive: true, mode: 0o700 });
|
|
56
|
-
}
|
|
57
|
-
fs.chmodSync(hodlDir, 0o700);
|
|
58
|
-
this.databaseExisted = fs.existsSync(path.join(hodlDir, 'persist.json'));
|
|
59
|
-
this.profileLock = profileLock;
|
|
60
|
-
try {
|
|
61
|
-
this.db = new Persist({ path: hodlDir, encryptionKey });
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
this.profileLock.release();
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
67
|
-
this.network = null;
|
|
68
|
-
this.selectedNetwork = null;
|
|
69
|
-
this.networkUsage = {};
|
|
70
|
-
}
|
|
71
|
-
async connect() {
|
|
72
|
-
await this.db.connect();
|
|
73
|
-
this.securePersistFile();
|
|
74
|
-
}
|
|
75
|
-
hasStoredDatabase() {
|
|
76
|
-
return this.databaseExisted;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* @param {number | string} num
|
|
80
|
-
* @returns {string}
|
|
81
|
-
*/
|
|
82
|
-
formatAmount(num) {
|
|
83
|
-
num = parseFloat(num.toString());
|
|
84
|
-
// Handle integers - add .00
|
|
85
|
-
if (num === Math.floor(num)) {
|
|
86
|
-
return num.toString() + '.00';
|
|
87
|
-
}
|
|
88
|
-
// For decimals, format to max 3 decimal places, then remove trailing zeros
|
|
89
|
-
let formatted = num.toFixed(3);
|
|
90
|
-
// Remove trailing zeros, but keep at least 2 decimal places
|
|
91
|
-
while (formatted.endsWith('0') && formatted.split('.')[1].length > 2) {
|
|
92
|
-
formatted = formatted.slice(0, -1);
|
|
93
|
-
}
|
|
94
|
-
return formatted;
|
|
95
|
-
}
|
|
96
|
-
async initialize() {
|
|
97
|
-
try {
|
|
98
|
-
const rawNetworkUsage = await this.db.get('networkUsage') || {};
|
|
99
|
-
// Validate and clean networkUsage data
|
|
100
|
-
this.networkUsage = {};
|
|
101
|
-
for (const [networkName, usage] of Object.entries(rawNetworkUsage)) {
|
|
102
|
-
if (typeof usage === 'number') {
|
|
103
|
-
// Old format - convert to new format
|
|
104
|
-
this.networkUsage[networkName] = {
|
|
105
|
-
count: usage,
|
|
106
|
-
lastUsed: 0
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
else if (isNetworkUsageEntry(usage)) {
|
|
110
|
-
// Valid new format
|
|
111
|
-
this.networkUsage[networkName] = {
|
|
112
|
-
count: usage.count,
|
|
113
|
-
lastUsed: usage.lastUsed || 0
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
// Skip corrupted entries (they'll be recreated when needed)
|
|
117
|
-
}
|
|
118
|
-
const networkPlugins = await this.loadNetworkPlugins();
|
|
119
|
-
if (networkPlugins.length === 0) {
|
|
120
|
-
throw new Error('No valid network plugins found.');
|
|
121
|
-
}
|
|
122
|
-
await this.selectNetwork(networkPlugins, { autoSelect: true });
|
|
123
|
-
this.network = new this.selectedNetwork.NetworkClass(this.selectedNetwork);
|
|
124
|
-
this.network.name = this.selectedNetwork.name;
|
|
125
|
-
await this.loadAccount();
|
|
126
|
-
if (!await this.hasAccount()) {
|
|
127
|
-
throw new Error('Failed to initialize account.');
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
throw new Error(`Initialization failed: ${errorMessage(error)}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* @param {WalletAccount | any} account
|
|
136
|
-
* @returns {void}
|
|
137
|
-
*/
|
|
138
|
-
async setAccount(account) {
|
|
139
|
-
if (!account) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
await this.db.set('account', this.network.constructor.name, account);
|
|
143
|
-
if (account.mnemonic) {
|
|
144
|
-
await this.db.set('mnemonic', account.mnemonic);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
async getAccount() {
|
|
148
|
-
const account = await this.db.get('account', this.network.constructor.name);
|
|
149
|
-
return account ?? null;
|
|
150
|
-
}
|
|
151
|
-
async getAddress() {
|
|
152
|
-
return this.db.get('account', this.network.constructor.name, 'address');
|
|
153
|
-
}
|
|
154
|
-
async getMnemonic() {
|
|
155
|
-
return await this.db.get('mnemonic') ?? null;
|
|
156
|
-
}
|
|
157
|
-
async hasAccount() {
|
|
158
|
-
return typeof await this.db.get('account', this.network.constructor.name, 'address') === 'string';
|
|
159
|
-
}
|
|
160
|
-
async displayAccountAddress() {
|
|
161
|
-
const table = new AnyTable({
|
|
162
|
-
head: [`${this.selectedNetwork.name} Address`],
|
|
163
|
-
style: {
|
|
164
|
-
head: ['green']
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
table.push([await this.getAddress()]);
|
|
168
|
-
console.log(table.toString());
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* @returns {Promise<NetworkPlugin[]>}
|
|
172
|
-
*/
|
|
173
|
-
async loadNetworkPlugins() {
|
|
174
|
-
return new NetworkRegistry().list();
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* @param {NetworkPlugin[]} networkPlugins
|
|
178
|
-
* @param {{ autoSelect?: boolean }} [options]
|
|
179
|
-
* @returns {Promise<void>}
|
|
180
|
-
*/
|
|
181
|
-
async selectNetwork(networkPlugins, { autoSelect = false } = {}) {
|
|
182
|
-
// Sort networks by last used timestamp (most recent first)
|
|
183
|
-
const sortedNetworks = networkPlugins.sort((a, b) => {
|
|
184
|
-
const aUsage = this.networkUsage[networkStorageName(a)];
|
|
185
|
-
const bUsage = this.networkUsage[networkStorageName(b)];
|
|
186
|
-
// Handle old format (number) vs new format (object)
|
|
187
|
-
const aLastUsed = typeof aUsage === 'object' ? aUsage.lastUsed || 0 : 0;
|
|
188
|
-
const bLastUsed = typeof bUsage === 'object' ? bUsage.lastUsed || 0 : 0;
|
|
189
|
-
// If neither has lastUsed timestamp, sort by old count format
|
|
190
|
-
if (aLastUsed === 0 && bLastUsed === 0) {
|
|
191
|
-
const aCount = typeof aUsage === 'number' ? aUsage : (aUsage?.count || 0);
|
|
192
|
-
const bCount = typeof bUsage === 'number' ? bUsage : (bUsage?.count || 0);
|
|
193
|
-
return bCount - aCount;
|
|
194
|
-
}
|
|
195
|
-
return bLastUsed - aLastUsed;
|
|
196
|
-
});
|
|
197
|
-
let selectedNetwork;
|
|
198
|
-
if (autoSelect) {
|
|
199
|
-
// Automatically select the first network (most recently used)
|
|
200
|
-
selectedNetwork = sortedNetworks[0];
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
// Let user choose the network
|
|
204
|
-
const { network } = await inquirer.prompt({
|
|
205
|
-
type: 'list',
|
|
206
|
-
name: 'network',
|
|
207
|
-
message: 'Select the network:',
|
|
208
|
-
choices: sortedNetworks.map(plugin => plugin.name),
|
|
209
|
-
});
|
|
210
|
-
selectedNetwork = sortedNetworks.find(plugin => plugin.name === network);
|
|
211
|
-
}
|
|
212
|
-
if (!selectedNetwork) {
|
|
213
|
-
throw new Error('Selected network was not found.');
|
|
214
|
-
}
|
|
215
|
-
// Update usage info for the selected network
|
|
216
|
-
// Handle migration from old format (number) to new format (object)
|
|
217
|
-
const currentUsage = this.networkUsage[networkStorageName(selectedNetwork)];
|
|
218
|
-
if (!currentUsage || typeof currentUsage === 'number' || typeof currentUsage !== 'object' || currentUsage === null || Array.isArray(currentUsage)) {
|
|
219
|
-
// Old format (number), doesn't exist, or corrupted data - create new object
|
|
220
|
-
this.networkUsage[networkStorageName(selectedNetwork)] = {
|
|
221
|
-
count: typeof currentUsage === 'number' ? currentUsage + 1 : 1,
|
|
222
|
-
lastUsed: Date.now()
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
else if (isNetworkUsageEntry(currentUsage)) {
|
|
226
|
-
// New format (object) - update values
|
|
227
|
-
currentUsage.count = (currentUsage.count || 0) + 1;
|
|
228
|
-
currentUsage.lastUsed = Date.now();
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
this.networkUsage[networkStorageName(selectedNetwork)] = {
|
|
232
|
-
count: 1,
|
|
233
|
-
lastUsed: Date.now()
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
await this.db.set('networkUsage', this.networkUsage);
|
|
237
|
-
this.selectedNetwork = selectedNetwork;
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* @param {boolean} [loggedIn]
|
|
241
|
-
* @returns {Promise<void>}
|
|
242
|
-
*/
|
|
243
|
-
async loadAccount(loggedIn = false) {
|
|
244
|
-
let accountExists = await this.hasAccount();
|
|
245
|
-
const mainChoices = ['Create New Account'];
|
|
246
|
-
if (loggedIn) {
|
|
247
|
-
mainChoices.push('Import Options');
|
|
248
|
-
mainChoices.push('Export Options');
|
|
249
|
-
mainChoices.push('Switch Network');
|
|
250
|
-
mainChoices.push('Manage Address Book');
|
|
251
|
-
mainChoices.push('Go Back');
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
mainChoices.push('Import HODL File');
|
|
255
|
-
mainChoices.push('Import Mnemonic (12 or 24 words)');
|
|
256
|
-
mainChoices.push('Import Private-key');
|
|
257
|
-
}
|
|
258
|
-
if (!accountExists || loggedIn) {
|
|
259
|
-
let { accountAction } = await inquirer.prompt({
|
|
260
|
-
type: 'list',
|
|
261
|
-
name: 'accountAction',
|
|
262
|
-
message: 'Select an account option:',
|
|
263
|
-
choices: mainChoices,
|
|
264
|
-
});
|
|
265
|
-
if (accountAction === 'Go Back') {
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
if (accountAction === 'Manage Address Book') {
|
|
269
|
-
const addressBookChoices = ['Delete Address', 'Go Back'];
|
|
270
|
-
const { addressBookAction } = await inquirer.prompt({
|
|
271
|
-
type: 'list',
|
|
272
|
-
name: 'addressBookAction',
|
|
273
|
-
message: 'Select an address book option:',
|
|
274
|
-
choices: addressBookChoices,
|
|
275
|
-
});
|
|
276
|
-
if (addressBookAction === 'Go Back') {
|
|
277
|
-
return this.loadAccount(loggedIn);
|
|
278
|
-
}
|
|
279
|
-
switch (addressBookAction) {
|
|
280
|
-
case 'Delete Address':
|
|
281
|
-
await this.deleteFromAddressBook();
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
if (accountAction === 'Import Options') {
|
|
287
|
-
const importChoices = ['Import HODL File', 'Import Mnemonic (12 or 24 words)', 'Import Private-key', 'Go Back'];
|
|
288
|
-
const { importAction } = await inquirer.prompt({
|
|
289
|
-
type: 'list',
|
|
290
|
-
name: 'importAction',
|
|
291
|
-
message: 'Select an import option:',
|
|
292
|
-
choices: importChoices,
|
|
293
|
-
});
|
|
294
|
-
if (importAction === 'Go Back') {
|
|
295
|
-
return this.loadAccount(loggedIn);
|
|
296
|
-
}
|
|
297
|
-
const confirmOverwrite = await this.confirmOverwrite();
|
|
298
|
-
if (!confirmOverwrite)
|
|
299
|
-
return;
|
|
300
|
-
accountAction = importAction;
|
|
301
|
-
}
|
|
302
|
-
switch (accountAction) {
|
|
303
|
-
case 'Import Mnemonic (12 or 24 words)':
|
|
304
|
-
const account = await this.importFromMnemonic();
|
|
305
|
-
if (!account) {
|
|
306
|
-
// Wallet.displayError('Invalid mnemonic.');
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
await this.setAccount(account);
|
|
310
|
-
Persist.clearSensitiveData(account);
|
|
311
|
-
await this.displayAccountAddress();
|
|
312
|
-
accountExists = true;
|
|
313
|
-
break;
|
|
314
|
-
case 'Import Private-key':
|
|
315
|
-
await this.importPrivateKey();
|
|
316
|
-
break;
|
|
317
|
-
case 'Import HODL File':
|
|
318
|
-
const importedAccount = await this.importHODLFile();
|
|
319
|
-
if (importedAccount) {
|
|
320
|
-
Persist.clearSensitiveData(importedAccount);
|
|
321
|
-
await this.displayAccountAddress();
|
|
322
|
-
accountExists = true;
|
|
323
|
-
}
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
326
|
-
if (accountAction === 'Export Options') {
|
|
327
|
-
const exportChoices = ['Export HODL File', 'Export Private-key', 'Go Back'];
|
|
328
|
-
const { exportAction } = await inquirer.prompt({
|
|
329
|
-
type: 'list',
|
|
330
|
-
name: 'exportAction',
|
|
331
|
-
message: 'Select an export option:',
|
|
332
|
-
choices: exportChoices,
|
|
333
|
-
});
|
|
334
|
-
if (exportAction === 'Go Back') {
|
|
335
|
-
return this.loadAccount(loggedIn);
|
|
336
|
-
}
|
|
337
|
-
switch (exportAction) {
|
|
338
|
-
case 'Export Private-key':
|
|
339
|
-
await this.displayAccountDetails();
|
|
340
|
-
break;
|
|
341
|
-
case 'Export HODL File':
|
|
342
|
-
await this.exportHODLFile();
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
if (accountAction === 'Create New Account') {
|
|
348
|
-
const confirmOverwrite = await this.confirmOverwrite();
|
|
349
|
-
if (!confirmOverwrite)
|
|
350
|
-
return;
|
|
351
|
-
await this.createNewAccount();
|
|
352
|
-
}
|
|
353
|
-
if (accountAction === 'Switch Network') {
|
|
354
|
-
await this.switchNetwork();
|
|
355
|
-
}
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
if (!accountExists) {
|
|
359
|
-
throw new Error('Account was not initialized.');
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* @returns {Promise<boolean>}
|
|
364
|
-
*/
|
|
365
|
-
async confirmOverwrite() {
|
|
366
|
-
if (await this.hasAccount()) {
|
|
367
|
-
const { confirmOverwrite } = await inquirer.prompt({
|
|
368
|
-
type: 'confirm',
|
|
369
|
-
name: 'confirmOverwrite',
|
|
370
|
-
message: 'This action will overwrite the existing account. Are you sure you want to continue?',
|
|
371
|
-
default: false,
|
|
372
|
-
});
|
|
373
|
-
return confirmOverwrite;
|
|
374
|
-
}
|
|
375
|
-
return true;
|
|
376
|
-
}
|
|
377
|
-
/**
|
|
378
|
-
* @returns {Promise<WalletAccount | null>}
|
|
379
|
-
*/
|
|
380
|
-
async importFromMnemonic() {
|
|
381
|
-
const { mnemonic } = await inquirer.prompt({
|
|
382
|
-
type: 'password',
|
|
383
|
-
name: 'mnemonic',
|
|
384
|
-
message: 'Enter your mnemonic phrase (12 or 24 words):',
|
|
385
|
-
mask: '*',
|
|
386
|
-
validate: (input) => {
|
|
387
|
-
if (input.trim() === '')
|
|
388
|
-
return true;
|
|
389
|
-
return this.network.validateMnemonic(input) || 'Please enter a valid mnemonic phrase or leave empty to cancel.';
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
if (!mnemonic.trim())
|
|
393
|
-
return null;
|
|
394
|
-
return this.network.accountFromMnemonic(mnemonic);
|
|
395
|
-
}
|
|
396
|
-
async showBalance() {
|
|
397
|
-
if (!await this.hasAccount()) {
|
|
398
|
-
Wallet.displayError('Account not initialized.');
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
const balances = await this.network.getTokenBalances(await this.getAddress());
|
|
402
|
-
const table = new AnyTable({
|
|
403
|
-
head: ['Token', 'Balance'],
|
|
404
|
-
style: { head: ['blue'] },
|
|
405
|
-
colWidths: [21, 22]
|
|
406
|
-
});
|
|
407
|
-
balances.forEach(([token, balance]) => {
|
|
408
|
-
table.push([token, token === 'USDT' ? Number(balance).toFixed(2) : this.formatAmount(balance)]);
|
|
409
|
-
});
|
|
410
|
-
console.log(table.toString());
|
|
411
|
-
}
|
|
412
|
-
async transferFunds() {
|
|
413
|
-
const service = new TransferService(this.db, this.selectedNetwork, this.network);
|
|
414
|
-
const pending = (await service.list()).filter(transfer => !['confirmed', 'failed'].includes(transfer.status));
|
|
415
|
-
if (pending.length) {
|
|
416
|
-
const { requestId } = await inquirer.prompt({
|
|
417
|
-
type: 'list', name: 'requestId', message: 'A previous transfer is still pending:',
|
|
418
|
-
choices: [
|
|
419
|
-
...pending.map(transfer => ({
|
|
420
|
-
name: `Check / resume ${transfer.amount} ${transfer.asset} to ${transfer.to} (${transfer.status})`,
|
|
421
|
-
value: transfer.requestId
|
|
422
|
-
})),
|
|
423
|
-
{ name: 'New transfer', value: 'new' },
|
|
424
|
-
{ name: 'Go back', value: 'back' }
|
|
425
|
-
]
|
|
426
|
-
});
|
|
427
|
-
if (requestId === 'back')
|
|
428
|
-
return;
|
|
429
|
-
if (requestId !== 'new') {
|
|
430
|
-
const transfer = pending.find(item => item.requestId === requestId);
|
|
431
|
-
await this.submitTransfer(service, transfer.to, transfer.asset, transfer.amount, requestId);
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
const contacts = await this.db.entries('contact', this.storageNetworkName) || [];
|
|
436
|
-
const addressBook = contacts.map(([address, data]) => ({
|
|
437
|
-
address,
|
|
438
|
-
name: data.name
|
|
439
|
-
}));
|
|
440
|
-
addressBook.push({ name: 'Go Back', address: '' });
|
|
441
|
-
// Implement autocomplete for address book
|
|
442
|
-
const { recipient } = await inquirer.prompt({
|
|
443
|
-
type: 'autocomplete',
|
|
444
|
-
name: 'recipient',
|
|
445
|
-
message: 'Recipient address:',
|
|
446
|
-
source: (_answersSoFar, input = '') => {
|
|
447
|
-
input = input || '';
|
|
448
|
-
return addressBook
|
|
449
|
-
.filter(entry => entry.name.toLowerCase().includes(input.toLowerCase()) || entry.address.toLowerCase().includes(input.toLowerCase()))
|
|
450
|
-
.map(entry => ({
|
|
451
|
-
name: entry.address ? `${entry.address} (${entry.name})` : entry.name,
|
|
452
|
-
value: entry.address
|
|
453
|
-
}))
|
|
454
|
-
.concat([{ name: input, value: input }]); // Add the input as a possible choice
|
|
455
|
-
},
|
|
456
|
-
});
|
|
457
|
-
if (!recipient)
|
|
458
|
-
return;
|
|
459
|
-
let address = recipient;
|
|
460
|
-
const choices = Object.keys(this.selectedNetwork.tokens);
|
|
461
|
-
choices.push(this.selectedNetwork.nativeToken);
|
|
462
|
-
let token = this.selectedNetwork.nativeToken;
|
|
463
|
-
if (choices.length > 1) {
|
|
464
|
-
token = (await inquirer.prompt({
|
|
465
|
-
type: 'list',
|
|
466
|
-
name: 'token',
|
|
467
|
-
message: 'Token to transfer:',
|
|
468
|
-
choices,
|
|
469
|
-
})).token;
|
|
470
|
-
}
|
|
471
|
-
const { amount } = await inquirer.prompt({
|
|
472
|
-
type: 'input',
|
|
473
|
-
name: 'amount',
|
|
474
|
-
message: `Amount to transfer (or max):`,
|
|
475
|
-
validate: (value) => {
|
|
476
|
-
if (value.trim() === '' || value.trim().toLowerCase() === 'max')
|
|
477
|
-
return true;
|
|
478
|
-
try {
|
|
479
|
-
normalizeDecimal(value.trim());
|
|
480
|
-
return true;
|
|
481
|
-
}
|
|
482
|
-
catch {
|
|
483
|
-
return 'Please enter a valid decimal amount, max, or leave empty to cancel.';
|
|
484
|
-
}
|
|
485
|
-
},
|
|
486
|
-
});
|
|
487
|
-
if (amount.trim() === '') {
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
let transferAmount = amount.trim();
|
|
491
|
-
if (transferAmount.toLowerCase() === 'max') {
|
|
492
|
-
try {
|
|
493
|
-
const preview = await service.send({ wallet: 'default', to: address, asset: token, amount: 'max', dryRun: true });
|
|
494
|
-
transferAmount = preview.amount;
|
|
495
|
-
console.log(`Maximum: ${preview.amount} ${token} | Estimated fee: ${preview.fee.amount} ${preview.fee.asset}`);
|
|
496
|
-
}
|
|
497
|
-
catch (error) {
|
|
498
|
-
this.displayTransactionError(error);
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
// Add confirmation step
|
|
503
|
-
const { confirmTransaction } = await inquirer.prompt({
|
|
504
|
-
type: 'confirm',
|
|
505
|
-
name: 'confirmTransaction',
|
|
506
|
-
message: amount.trim().toLowerCase() === 'max'
|
|
507
|
-
? `Confirm transfer of maximum available ${token} (estimated ${transferAmount})?`
|
|
508
|
-
: `Confirm transfer of ${transferAmount} ${token}?`,
|
|
509
|
-
default: true
|
|
510
|
-
});
|
|
511
|
-
if (!confirmTransaction) {
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
await this.submitTransfer(service, address, token, amount.trim().toLowerCase() === 'max' ? 'max' : transferAmount, crypto.randomUUID());
|
|
515
|
-
}
|
|
516
|
-
async submitTransfer(service, address, token, amount, requestId) {
|
|
517
|
-
const spinner = ora({ text: 'Checking / sending transaction...', spinner: 'dots' }).start();
|
|
518
|
-
let result;
|
|
519
|
-
try {
|
|
520
|
-
result = await service.send({ wallet: 'default', to: address, asset: token, amount, requestId, dryRun: false });
|
|
521
|
-
}
|
|
522
|
-
catch (error) {
|
|
523
|
-
spinner.fail('Could not complete the transfer request.');
|
|
524
|
-
this.displayTransactionError(error);
|
|
525
|
-
if (error instanceof AgentError && error.code === 'SWAP_IN_PROGRESS') {
|
|
526
|
-
console.log(`Blocking swap: ${error.details?.requestId}`);
|
|
527
|
-
console.log('Open Swap > Track Swaps to refresh its saved status, then retry the transfer once the swap has finished.');
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
if (error instanceof AgentError && error.details?.transactionHash) {
|
|
531
|
-
console.log(`Saved transfer: ${error.details.requestId}`);
|
|
532
|
-
console.log(this.selectedNetwork.explorer + error.details.transactionHash);
|
|
533
|
-
}
|
|
534
|
-
else {
|
|
535
|
-
console.log(`Request ID: ${requestId}`);
|
|
536
|
-
}
|
|
537
|
-
console.log('Open Transfer Funds to check or resume any saved request before creating another payment.');
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
spinner.succeed(result.status === 'confirmed' ? 'Transaction confirmed!' : 'Transaction submitted; awaiting confirmation.');
|
|
541
|
-
console.log(`Request ID: ${requestId}`);
|
|
542
|
-
console.log(this.selectedNetwork.explorer + result.transactionHash);
|
|
543
|
-
try {
|
|
544
|
-
const currentBalance = (await this.network.getAssetBalance(result.from, token)).amount;
|
|
545
|
-
await this.addToTransactions(address, token, result.amount, result.transactionHash, currentBalance);
|
|
546
|
-
await this.displayTransactionResult(address, token, result.amount, result.transactionHash, currentBalance);
|
|
547
|
-
const existingContact = await this.db.get('contact', this.storageNetworkName, address);
|
|
548
|
-
if (!existingContact)
|
|
549
|
-
await this.addToAddressBook(address);
|
|
550
|
-
}
|
|
551
|
-
catch (error) {
|
|
552
|
-
Wallet.displayError('Transfer recorded; could not update balance, history or contact.', errorMessage(error));
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* @param {any} error
|
|
557
|
-
* @returns {void}
|
|
558
|
-
*/
|
|
559
|
-
displayTransactionError(error) {
|
|
560
|
-
const reason = typeof error === 'object' && error !== null && 'reason' in error
|
|
561
|
-
? String(error.reason)
|
|
562
|
-
: null;
|
|
563
|
-
const message = error instanceof Error ? error.message : errorMessage(error);
|
|
564
|
-
const data = reason ? reason.replace(/(\w+):/g, "\n$1:").trim() : null;
|
|
565
|
-
Wallet.displayError(message, data);
|
|
566
|
-
}
|
|
567
|
-
/**
|
|
568
|
-
* @param {string} message
|
|
569
|
-
* @param {unknown} [data]
|
|
570
|
-
* @returns {void}
|
|
571
|
-
*/
|
|
572
|
-
static displayError(message, data) {
|
|
573
|
-
const table = new AnyTable({
|
|
574
|
-
head: [message],
|
|
575
|
-
style: { head: ['red'] },
|
|
576
|
-
wordWrap: true,
|
|
577
|
-
});
|
|
578
|
-
if (data) {
|
|
579
|
-
table.push([data.toString()]);
|
|
580
|
-
}
|
|
581
|
-
console.log(table.toString());
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* @param {string | number | Date} date
|
|
585
|
-
* @returns {string}
|
|
586
|
-
*/
|
|
587
|
-
formatDate(date) {
|
|
588
|
-
return new Date(date).toLocaleString('en-GB', {
|
|
589
|
-
year: 'numeric',
|
|
590
|
-
month: '2-digit',
|
|
591
|
-
day: '2-digit',
|
|
592
|
-
hour: '2-digit',
|
|
593
|
-
minute: '2-digit',
|
|
594
|
-
hour12: false
|
|
595
|
-
}).replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$2-$1').replace(",", "");
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* @param {string} recipient
|
|
599
|
-
* @param {string} token
|
|
600
|
-
* @param {string} amount
|
|
601
|
-
* @param {string} hash
|
|
602
|
-
* @param {string} balance
|
|
603
|
-
* @returns {Promise<void>}
|
|
604
|
-
*/
|
|
605
|
-
async addToTransactions(recipient, token, amount, hash, balance) {
|
|
606
|
-
const transaction = {
|
|
607
|
-
timestamp: new Date().toISOString(),
|
|
608
|
-
recipient,
|
|
609
|
-
token,
|
|
610
|
-
amount,
|
|
611
|
-
hash,
|
|
612
|
-
balance
|
|
613
|
-
};
|
|
614
|
-
const address = await this.getAddress();
|
|
615
|
-
const history = await this.db.values('transactions', address, this.selectedNetwork.nativeToken) || [];
|
|
616
|
-
if (!history.some(entry => entry.hash === hash)) {
|
|
617
|
-
await this.db.add('transactions', address, this.selectedNetwork.nativeToken, transaction);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
async showTransactions() {
|
|
621
|
-
const address = await this.getAddress();
|
|
622
|
-
const history = await this.db.values('transactions', address, this.selectedNetwork.nativeToken) || [];
|
|
623
|
-
const legacyHistoryKey = this.selectedNetwork.id === 'op'
|
|
624
|
-
? 'OP'
|
|
625
|
-
: this.selectedNetwork.id === 'arb'
|
|
626
|
-
? 'ARB'
|
|
627
|
-
: null;
|
|
628
|
-
if (legacyHistoryKey) {
|
|
629
|
-
const legacyHistory = await this.db.values('transactions', address, legacyHistoryKey) || [];
|
|
630
|
-
history.push(...legacyHistory);
|
|
631
|
-
history.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
632
|
-
}
|
|
633
|
-
const transfers = await new TransferService(this.db, this.selectedNetwork, this.network).list();
|
|
634
|
-
for (const transfer of transfers.filter(item => item.from === address)) {
|
|
635
|
-
const existing = history.find(item => item.hash === transfer.transactionHash);
|
|
636
|
-
if (existing)
|
|
637
|
-
existing.status = transfer.status;
|
|
638
|
-
else
|
|
639
|
-
history.push({
|
|
640
|
-
timestamp: transfer.createdAt, recipient: transfer.to, token: transfer.asset,
|
|
641
|
-
amount: transfer.amount, hash: transfer.transactionHash, status: transfer.status
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
history.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
645
|
-
const table = new AnyTable({
|
|
646
|
-
head: ['Date', 'Recipient', 'Contact', 'Token', 'Amount', 'Balance', 'Status'],
|
|
647
|
-
style: { head: ['blue'] },
|
|
648
|
-
});
|
|
649
|
-
if (history.length === 0) {
|
|
650
|
-
table.push([{ colSpan: 7, content: 'No transaction history available.' }]);
|
|
651
|
-
}
|
|
652
|
-
for (const tx of history) {
|
|
653
|
-
const date = this.formatDate(tx.timestamp);
|
|
654
|
-
const contact = await this.db.get('contact', this.storageNetworkName, tx.recipient);
|
|
655
|
-
const contactName = contact ? contact.name : '-';
|
|
656
|
-
const amount = this.formatAmount(tx.amount);
|
|
657
|
-
const balance = tx.balance !== undefined ? this.formatAmount(tx.balance) : '-';
|
|
658
|
-
table.push([date, tx.recipient, contactName, tx.token, amount, balance, tx.status || '-']);
|
|
659
|
-
table.push([{ colSpan: 7, content: this.selectedNetwork.explorer + tx.hash }]);
|
|
660
|
-
}
|
|
661
|
-
console.log(table.toString());
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* @param {string} address
|
|
665
|
-
* @returns {Promise<void>}
|
|
666
|
-
*/
|
|
667
|
-
async addToAddressBook(address) {
|
|
668
|
-
const { name } = await inquirer.prompt({
|
|
669
|
-
type: 'input',
|
|
670
|
-
name: 'name',
|
|
671
|
-
message: 'Name for the address book (leave empty to skip):',
|
|
672
|
-
});
|
|
673
|
-
if (name.trim() !== '') {
|
|
674
|
-
await this.db.set('contact', this.storageNetworkName, address, 'name', name);
|
|
675
|
-
const table = new AnyTable({
|
|
676
|
-
head: [{ colSpan: 2, content: "Recipient saved to the address book." }],
|
|
677
|
-
style: { head: ['green'] },
|
|
678
|
-
});
|
|
679
|
-
table.push([name, address]);
|
|
680
|
-
console.log(table.toString());
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
async deleteFromAddressBook() {
|
|
684
|
-
const contacts = await this.db.get('contact', this.storageNetworkName) || {};
|
|
685
|
-
const addressBook = Object.entries(contacts).map(([address, data]) => ({
|
|
686
|
-
address,
|
|
687
|
-
name: data.name
|
|
688
|
-
}));
|
|
689
|
-
if (addressBook.length === 0) {
|
|
690
|
-
const table = new AnyTable({
|
|
691
|
-
head: ['Address Book'],
|
|
692
|
-
style: { head: ['yellow'] },
|
|
693
|
-
});
|
|
694
|
-
table.push(['No addresses in the address book.']);
|
|
695
|
-
console.log(table.toString());
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
addressBook.push({ name: 'Go Back', address: '' });
|
|
699
|
-
const { addressToDelete } = await inquirer.prompt({
|
|
700
|
-
type: 'list',
|
|
701
|
-
name: 'addressToDelete',
|
|
702
|
-
message: 'Select an address to delete:',
|
|
703
|
-
choices: addressBook.map(entry => ({
|
|
704
|
-
name: entry.address ? `${entry.address} (${entry.name})` : entry.name,
|
|
705
|
-
value: entry.address
|
|
706
|
-
}))
|
|
707
|
-
});
|
|
708
|
-
if (!addressToDelete)
|
|
709
|
-
return;
|
|
710
|
-
const { confirmDelete } = await inquirer.prompt({
|
|
711
|
-
type: 'confirm',
|
|
712
|
-
name: 'confirmDelete',
|
|
713
|
-
message: 'Are you sure you want to delete this address?',
|
|
714
|
-
default: false
|
|
715
|
-
});
|
|
716
|
-
if (confirmDelete) {
|
|
717
|
-
await this.db.del('contact', this.storageNetworkName, addressToDelete);
|
|
718
|
-
const table = new AnyTable({
|
|
719
|
-
head: ['Address Book'],
|
|
720
|
-
style: { head: ['green'] },
|
|
721
|
-
});
|
|
722
|
-
table.push(['Address deleted successfully.']);
|
|
723
|
-
console.log(table.toString());
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* @param {string} address
|
|
728
|
-
* @param {string} token
|
|
729
|
-
* @param {string} amount
|
|
730
|
-
* @param {string} hash
|
|
731
|
-
* @param {string} balance
|
|
732
|
-
* @returns {Promise<void>}
|
|
733
|
-
*/
|
|
734
|
-
async displayTransactionResult(address, token, amount, hash, balance) {
|
|
735
|
-
const table = new AnyTable({
|
|
736
|
-
head: ['Date', 'Recipient', 'Contact', 'Token', 'Amount', 'Balance'],
|
|
737
|
-
style: { head: ['green'] },
|
|
738
|
-
});
|
|
739
|
-
const date = this.formatDate(new Date());
|
|
740
|
-
const contact = await this.db.get('contact', this.storageNetworkName, address);
|
|
741
|
-
const contactName = contact ? contact.name : '-';
|
|
742
|
-
table.push([date, address, contactName, token, this.formatAmount(amount), this.formatAmount(balance)]);
|
|
743
|
-
table.push([{ colSpan: 6, content: this.selectedNetwork.explorer + hash }]);
|
|
744
|
-
console.log(table.toString());
|
|
745
|
-
}
|
|
746
|
-
async clearAccountData() {
|
|
747
|
-
try {
|
|
748
|
-
await this.db.dispose();
|
|
749
|
-
}
|
|
750
|
-
finally {
|
|
751
|
-
this.securePersistFile();
|
|
752
|
-
this.profileLock.release();
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
get storageNetworkName() { return networkStorageName(this.selectedNetwork); }
|
|
756
|
-
getSwapRoute() { return routeForNetwork(this.selectedNetwork.id); }
|
|
757
|
-
async swapFunds() {
|
|
758
|
-
const route = this.getSwapRoute();
|
|
759
|
-
if (!route)
|
|
760
|
-
throw new Error('No swap route for the selected network.');
|
|
761
|
-
await startSwapMenu(new SwapService(this.db, { routeId: route.id }));
|
|
762
|
-
}
|
|
763
|
-
securePersistFile() {
|
|
764
|
-
const persistPath = path.join(this.hodlDir, 'persist.json');
|
|
765
|
-
if (fs.existsSync(persistPath)) {
|
|
766
|
-
fs.chmodSync(persistPath, 0o600);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
async displayAccountDetails() {
|
|
770
|
-
const table = new AnyTable({
|
|
771
|
-
head: [{ colSpan: 2, content: 'Account Details' }],
|
|
772
|
-
style: { head: ['green'] },
|
|
773
|
-
wordWrap: true
|
|
774
|
-
});
|
|
775
|
-
const account = await this.getAccount();
|
|
776
|
-
if (!account) {
|
|
777
|
-
Wallet.displayError('Account not initialized.');
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
table.push(['Address', await this.getAddress()], ['Private-key', account.privateKey]);
|
|
781
|
-
const mnemonic = await this.getMnemonic();
|
|
782
|
-
if (mnemonic) {
|
|
783
|
-
table.push(['Mnemonic Phrase', mnemonic]);
|
|
784
|
-
table.push(['WARNING', "Please keep your private-key and mnemonic phrase secure. Never share it."]);
|
|
785
|
-
}
|
|
786
|
-
else {
|
|
787
|
-
table.push(['WARNING', "Please keep your private-key secure. Never share it."]);
|
|
788
|
-
}
|
|
789
|
-
console.log(table.toString());
|
|
790
|
-
Persist.clearSensitiveData(account);
|
|
791
|
-
}
|
|
792
|
-
async switchNetwork() {
|
|
793
|
-
const networkPlugins = await this.loadNetworkPlugins();
|
|
794
|
-
await this.selectNetwork(networkPlugins);
|
|
795
|
-
this.network = new this.selectedNetwork.NetworkClass(this.selectedNetwork);
|
|
796
|
-
this.network.name = this.selectedNetwork.name;
|
|
797
|
-
if (await this.hasAccount()) {
|
|
798
|
-
await this.displayAccountAddress();
|
|
799
|
-
}
|
|
800
|
-
else {
|
|
801
|
-
console.log(`\nNo account found for ${this.selectedNetwork.name}. Please create or import an account.`);
|
|
802
|
-
await this.loadAccount(true);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
async exportHODLFile() {
|
|
806
|
-
const address = await this.getAddress();
|
|
807
|
-
const defaultFileName = `${address.slice(-6).toUpperCase()}`;
|
|
808
|
-
let { fileName } = await inquirer.prompt({
|
|
809
|
-
type: 'input',
|
|
810
|
-
name: 'fileName',
|
|
811
|
-
message: 'Enter the name for the HODL file:',
|
|
812
|
-
default: defaultFileName
|
|
813
|
-
});
|
|
814
|
-
fileName += '.HODL';
|
|
815
|
-
const data = await this.db.get();
|
|
816
|
-
const encryptionKey = await UIManager.getEncryptionKey();
|
|
817
|
-
try {
|
|
818
|
-
const encryptedData = Persist.encrypt(data, encryptionKey);
|
|
819
|
-
fs.writeFileSync(fileName, encryptedData);
|
|
820
|
-
}
|
|
821
|
-
finally {
|
|
822
|
-
Persist.clearSensitiveData(data);
|
|
823
|
-
}
|
|
824
|
-
const table = new AnyTable({
|
|
825
|
-
head: ['HODL File Exported'],
|
|
826
|
-
style: { head: ['green'] }
|
|
827
|
-
});
|
|
828
|
-
table.push([`File saved as: ${fileName}`]);
|
|
829
|
-
console.log(table.toString());
|
|
830
|
-
}
|
|
831
|
-
async importHODLFile() {
|
|
832
|
-
// Scan current directory for .HODL files
|
|
833
|
-
const currentDir = process.cwd();
|
|
834
|
-
let hodlFiles = [];
|
|
835
|
-
try {
|
|
836
|
-
const files = fs.readdirSync(currentDir);
|
|
837
|
-
hodlFiles = files.filter(file => file.endsWith('.HODL'));
|
|
838
|
-
}
|
|
839
|
-
catch (error) {
|
|
840
|
-
console.error('Error reading directory:', errorMessage(error));
|
|
841
|
-
}
|
|
842
|
-
// Create file options array for autocomplete
|
|
843
|
-
const fileOptions = hodlFiles.map(file => ({
|
|
844
|
-
name: file,
|
|
845
|
-
value: path.join(currentDir, file)
|
|
846
|
-
}));
|
|
847
|
-
// Use autocomplete pattern similar to transferFunds
|
|
848
|
-
const { filePath } = await inquirer.prompt({
|
|
849
|
-
type: 'autocomplete',
|
|
850
|
-
name: 'filePath',
|
|
851
|
-
message: 'HODL file path:',
|
|
852
|
-
source: (_answersSoFar, input = '') => {
|
|
853
|
-
input = input || '';
|
|
854
|
-
const filenameOptions = fileOptions
|
|
855
|
-
.filter(entry => entry.name.toLowerCase().includes(input.toLowerCase()) || entry.value.toLowerCase().includes(input.toLowerCase()))
|
|
856
|
-
.map(entry => ({
|
|
857
|
-
name: entry.name,
|
|
858
|
-
value: entry.value
|
|
859
|
-
}));
|
|
860
|
-
// Put path options first, then filename options, then manual input
|
|
861
|
-
return [{ name: input, value: input }, ...filenameOptions];
|
|
862
|
-
},
|
|
863
|
-
});
|
|
864
|
-
// If user leaves empty, skip the operation
|
|
865
|
-
if (!filePath || filePath.trim() === '') {
|
|
866
|
-
return null;
|
|
867
|
-
}
|
|
868
|
-
// Validate the file path
|
|
869
|
-
if (!filePath.endsWith('.HODL')) {
|
|
870
|
-
Wallet.displayError('File must have .HODL extension');
|
|
871
|
-
return null;
|
|
872
|
-
}
|
|
873
|
-
if (!fs.existsSync(filePath)) {
|
|
874
|
-
Wallet.displayError('File not found.');
|
|
875
|
-
return null;
|
|
876
|
-
}
|
|
877
|
-
const encryptedData = fs.readFileSync(filePath, 'utf8');
|
|
878
|
-
const encryptionKey = await UIManager.getEncryptionKey();
|
|
879
|
-
let importedData;
|
|
880
|
-
try {
|
|
881
|
-
importedData = Persist.decrypt(encryptedData, encryptionKey);
|
|
882
|
-
await this.db.set(importedData);
|
|
883
|
-
return await this.getAccount();
|
|
884
|
-
}
|
|
885
|
-
catch (error) {
|
|
886
|
-
Wallet.displayError('Failed to import HODL file.', 'The password is incorrect.');
|
|
887
|
-
return null;
|
|
888
|
-
}
|
|
889
|
-
finally {
|
|
890
|
-
Persist.clearSensitiveData(importedData);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
async importPrivateKey() {
|
|
894
|
-
const { privateKey } = await inquirer.prompt({
|
|
895
|
-
type: 'password',
|
|
896
|
-
name: 'privateKey',
|
|
897
|
-
message: 'Private-key (leave empty to cancel):',
|
|
898
|
-
mask: '*',
|
|
899
|
-
validate: (input) => {
|
|
900
|
-
if (input.trim() === '')
|
|
901
|
-
return true;
|
|
902
|
-
return this.network.validatePrivateKey(input) ||
|
|
903
|
-
'Please enter a valid private-key for the selected network or leave empty to cancel.';
|
|
904
|
-
}
|
|
905
|
-
});
|
|
906
|
-
if (!privateKey.trim()) {
|
|
907
|
-
return; // Silently return if empty
|
|
908
|
-
}
|
|
909
|
-
try {
|
|
910
|
-
await this.setAccount(await this.network.privateKeyToAccount(privateKey));
|
|
911
|
-
await this.displayAccountAddress();
|
|
912
|
-
}
|
|
913
|
-
catch (error) {
|
|
914
|
-
Wallet.displayError('Invalid private-key.');
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
async createNewAccount() {
|
|
918
|
-
let existingMnemonic = await this.getMnemonic();
|
|
919
|
-
let useMnemonic = false;
|
|
920
|
-
if (existingMnemonic) {
|
|
921
|
-
const { useExistingMnemonic } = await inquirer.prompt({
|
|
922
|
-
type: 'confirm',
|
|
923
|
-
name: 'useExistingMnemonic',
|
|
924
|
-
message: 'Use existing mnemonic to create account?',
|
|
925
|
-
default: true
|
|
926
|
-
});
|
|
927
|
-
if (useExistingMnemonic) {
|
|
928
|
-
useMnemonic = true;
|
|
929
|
-
await this.setAccount(await this.network.accountFromMnemonic(existingMnemonic));
|
|
930
|
-
existingMnemonic = null;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
if (!useMnemonic) {
|
|
934
|
-
const { createWithMnemonic } = await inquirer.prompt({
|
|
935
|
-
type: 'confirm',
|
|
936
|
-
name: 'createWithMnemonic',
|
|
937
|
-
message: 'Create account with mnemonic?',
|
|
938
|
-
default: true
|
|
939
|
-
});
|
|
940
|
-
if (createWithMnemonic) {
|
|
941
|
-
// Ask for mnemonic word count
|
|
942
|
-
const { wordCount } = await inquirer.prompt({
|
|
943
|
-
type: 'list',
|
|
944
|
-
name: 'wordCount',
|
|
945
|
-
message: 'Choose mnemonic phrase length:',
|
|
946
|
-
choices: [
|
|
947
|
-
{ name: '12 words (standard)', value: 12 },
|
|
948
|
-
{ name: '24 words (more secure)', value: 24 }
|
|
949
|
-
],
|
|
950
|
-
default: 12
|
|
951
|
-
});
|
|
952
|
-
await this.setAccount(await this.network.createAccountFromMnemonic(wordCount));
|
|
953
|
-
}
|
|
954
|
-
else {
|
|
955
|
-
await this.setAccount(await this.network.createAccount());
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
// Check if there are addresses in the address book
|
|
959
|
-
const addressBook = await this.db.entries('contact', this.storageNetworkName) || [];
|
|
960
|
-
if (addressBook.length > 0) {
|
|
961
|
-
const { deleteAddresses } = await inquirer.prompt({
|
|
962
|
-
type: 'confirm',
|
|
963
|
-
name: 'deleteAddresses',
|
|
964
|
-
message: `Do you want to delete all ${addressBook.length} addresses from the previous account?`,
|
|
965
|
-
default: false
|
|
966
|
-
});
|
|
967
|
-
if (deleteAddresses) {
|
|
968
|
-
await this.db.del('contact');
|
|
969
|
-
const table = new AnyTable({
|
|
970
|
-
head: ['Address Book'],
|
|
971
|
-
style: { head: ['green'] },
|
|
972
|
-
});
|
|
973
|
-
table.push(['All addresses deleted successfully.']);
|
|
974
|
-
console.log(table.toString());
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
let message = 'Do you want to display sensitive information (private key';
|
|
978
|
-
if (await this.getMnemonic()) {
|
|
979
|
-
message += ' and mnemonic';
|
|
980
|
-
}
|
|
981
|
-
message += ')?';
|
|
982
|
-
const { showSensitive } = await inquirer.prompt({
|
|
983
|
-
type: 'confirm',
|
|
984
|
-
name: 'showSensitive',
|
|
985
|
-
message,
|
|
986
|
-
default: false,
|
|
987
|
-
});
|
|
988
|
-
if (showSensitive) {
|
|
989
|
-
await this.displayAccountDetails();
|
|
990
|
-
}
|
|
991
|
-
else {
|
|
992
|
-
await this.displayAccountAddress();
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
class UIManager {
|
|
997
|
-
static displayWelcome() {
|
|
998
|
-
console.log('\x1b[32m'); // Set text color to green
|
|
999
|
-
console.log(` ░░░░░░░░░░░░░░ █ █ █▀█ █▀▄ █ ░░░░░░░░░░░░░░
|
|
1000
|
-
░░░░░░░░░░░░░░ █▀█ █▄█ █▄▀ █▄▄ ░░░░░░░░░░░░░░
|
|
1001
|
-
░░░░░░░░░░░░░░ ──────── WALLET ░░░░░░░░░░░░░░`);
|
|
1002
|
-
console.log('\x1b[0m'); // Reset text color
|
|
1003
|
-
}
|
|
1004
|
-
static async getEncryptionKey() {
|
|
1005
|
-
const { key } = await inquirer.prompt({
|
|
1006
|
-
type: 'password',
|
|
1007
|
-
name: 'key',
|
|
1008
|
-
message: 'Password:',
|
|
1009
|
-
mask: '*',
|
|
1010
|
-
});
|
|
1011
|
-
return key;
|
|
1012
|
-
}
|
|
1013
|
-
static async confirmEncryptionKey() {
|
|
1014
|
-
const { confirmKey } = await inquirer.prompt({
|
|
1015
|
-
type: 'password',
|
|
1016
|
-
name: 'confirmKey',
|
|
1017
|
-
message: 'Repeat Password:',
|
|
1018
|
-
mask: '*',
|
|
1019
|
-
});
|
|
1020
|
-
return confirmKey;
|
|
1021
|
-
}
|
|
1022
|
-
static displayExitPhrase() {
|
|
1023
|
-
const phrases = [
|
|
1024
|
-
"Buy the rumor, sell the news",
|
|
1025
|
-
"The trend is your friend",
|
|
1026
|
-
"Don't fight the tape",
|
|
1027
|
-
"Cut your losses and let your profits run",
|
|
1028
|
-
"Be fearful when others are greedy, and greedy when others are fearful",
|
|
1029
|
-
"The market can remain irrational longer than you can remain solvent",
|
|
1030
|
-
"Bulls make money, bears make money, pigs get slaughtered",
|
|
1031
|
-
"No one is bigger than the market",
|
|
1032
|
-
"Don't catch a falling knife",
|
|
1033
|
-
"Past performance is not indicative of future results",
|
|
1034
|
-
"The stock market is a device for transferring money from the impatient to the patient",
|
|
1035
|
-
"Time in the market beats timing the market",
|
|
1036
|
-
"Buy low, sell high",
|
|
1037
|
-
"Diversification is the only free lunch in investing",
|
|
1038
|
-
"The four most dangerous words in investing are: 'This time it's different'",
|
|
1039
|
-
"Markets can remain irrational a lot longer than you and I can remain solvent",
|
|
1040
|
-
"Risk comes from not knowing what you're doing",
|
|
1041
|
-
"In the short run, the market is a voting machine. In the long run, it's a weighing machine",
|
|
1042
|
-
"Invest in yourself. Your career is the engine of your wealth",
|
|
1043
|
-
"Who has the gold makes the rules",
|
|
1044
|
-
"The best time to invest was yesterday. The second best time is now",
|
|
1045
|
-
"Don't put all your eggs in one basket",
|
|
1046
|
-
"Knowledge is power in the world of investing",
|
|
1047
|
-
"Patience is a virtue in the stock market",
|
|
1048
|
-
"The market is never wrong, but opinions often are"
|
|
1049
|
-
];
|
|
1050
|
-
const randomPhrase = phrases[Math.floor(Math.random() * phrases.length)];
|
|
1051
|
-
const table = new AnyTable({
|
|
1052
|
-
head: ['✨ Good bye!'],
|
|
1053
|
-
style: { head: ['yellow'] },
|
|
1054
|
-
wordWrap: true,
|
|
1055
|
-
});
|
|
1056
|
-
table.push([randomPhrase]);
|
|
1057
|
-
console.log(table.toString());
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
async function mainMenu(wallet) {
|
|
1061
|
-
const route = wallet.getSwapRoute();
|
|
1062
|
-
const { action } = await inquirer.prompt({
|
|
1063
|
-
type: 'list',
|
|
1064
|
-
name: 'action',
|
|
1065
|
-
message: 'What would you like to do?',
|
|
1066
|
-
choices: [
|
|
1067
|
-
{ name: 'Transfer Funds', value: 'transferFunds' },
|
|
1068
|
-
{ name: 'Show Balance', value: 'balance' },
|
|
1069
|
-
{ name: 'Show Sent Transfers', value: 'showTransactions' },
|
|
1070
|
-
{ name: 'Account Settings', value: 'account' },
|
|
1071
|
-
...(route ? [{ name: 'Swap', value: 'swapFunds' }] : []),
|
|
1072
|
-
{ name: 'Exit', value: 'exit' }
|
|
1073
|
-
],
|
|
1074
|
-
});
|
|
1075
|
-
switch (action) {
|
|
1076
|
-
case 'swapFunds':
|
|
1077
|
-
await wallet.swapFunds();
|
|
1078
|
-
return mainMenu(wallet);
|
|
1079
|
-
case 'transferFunds':
|
|
1080
|
-
await wallet.transferFunds();
|
|
1081
|
-
return mainMenu(wallet);
|
|
1082
|
-
case 'balance':
|
|
1083
|
-
await wallet.showBalance();
|
|
1084
|
-
return mainMenu(wallet);
|
|
1085
|
-
case 'showTransactions':
|
|
1086
|
-
await wallet.showTransactions();
|
|
1087
|
-
return mainMenu(wallet);
|
|
1088
|
-
case 'account':
|
|
1089
|
-
await wallet.loadAccount(true);
|
|
1090
|
-
return mainMenu(wallet);
|
|
1091
|
-
case 'exit':
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
let activeWallet = null;
|
|
25
|
+
let activeSession = null;
|
|
1096
26
|
let shutdownPromise = null;
|
|
1097
27
|
async function shutdown() {
|
|
1098
28
|
if (!shutdownPromise) {
|
|
1099
29
|
shutdownPromise = (async () => {
|
|
1100
|
-
if (
|
|
1101
|
-
await
|
|
1102
|
-
|
|
30
|
+
if (activeSession) {
|
|
31
|
+
await activeSession.close();
|
|
32
|
+
activeSession = null;
|
|
1103
33
|
}
|
|
1104
|
-
|
|
34
|
+
farewell(ui);
|
|
1105
35
|
})();
|
|
1106
36
|
}
|
|
1107
37
|
await shutdownPromise;
|
|
1108
38
|
}
|
|
1109
39
|
async function run() {
|
|
1110
|
-
|
|
40
|
+
welcome(ui);
|
|
1111
41
|
const hodlDir = path.join(os.homedir(), '.HODL');
|
|
1112
42
|
fs.mkdirSync(hodlDir, { recursive: true, mode: 0o700 });
|
|
1113
43
|
const profileLock = new ProfileLock(path.join(hodlDir, '.default.lock'));
|
|
@@ -1118,42 +48,40 @@ async function run() {
|
|
|
1118
48
|
if (!(error instanceof ProfileLockedError)) {
|
|
1119
49
|
throw error;
|
|
1120
50
|
}
|
|
1121
|
-
|
|
51
|
+
showError(ui, error.owner === null
|
|
1122
52
|
? 'The HODL profile is locked. Close the other instance and try again.'
|
|
1123
53
|
: `HODL is already open in another terminal (PID ${error.owner}). Close that instance and try again.`);
|
|
1124
54
|
return;
|
|
1125
55
|
}
|
|
1126
56
|
let encryptionKey = null;
|
|
1127
57
|
try {
|
|
1128
|
-
encryptionKey = await
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
const databaseExisted =
|
|
58
|
+
encryptionKey = await ui.password({ message: 'Password:' });
|
|
59
|
+
const session = new NodeSession(encryptionKey, profileLock);
|
|
60
|
+
activeSession = session;
|
|
61
|
+
const databaseExisted = session.hasStoredDatabase();
|
|
1132
62
|
try {
|
|
1133
|
-
await
|
|
63
|
+
await session.connect();
|
|
1134
64
|
}
|
|
1135
65
|
catch (error) {
|
|
1136
66
|
console.error(errorMessage(error));
|
|
1137
|
-
|
|
67
|
+
showError(ui, 'Wrong password.');
|
|
1138
68
|
process.exitCode = 1;
|
|
1139
69
|
return;
|
|
1140
70
|
}
|
|
1141
71
|
if (!databaseExisted) {
|
|
1142
|
-
const confirmedKey = await
|
|
72
|
+
const confirmedKey = await ui.password({ message: 'Repeat Password:' });
|
|
1143
73
|
if (confirmedKey !== encryptionKey) {
|
|
1144
|
-
|
|
74
|
+
showError(ui, 'Passwords do not match. Please try again.');
|
|
1145
75
|
process.exitCode = 1;
|
|
1146
76
|
return;
|
|
1147
77
|
}
|
|
1148
78
|
}
|
|
1149
79
|
encryptionKey = null;
|
|
1150
80
|
try {
|
|
1151
|
-
await
|
|
1152
|
-
await wallet.displayAccountAddress();
|
|
1153
|
-
await mainMenu(wallet);
|
|
81
|
+
await runWallet(ui, session);
|
|
1154
82
|
}
|
|
1155
83
|
catch (error) {
|
|
1156
|
-
|
|
84
|
+
showError(ui, 'Failed to initialize wallet.', error);
|
|
1157
85
|
process.exitCode = 1;
|
|
1158
86
|
}
|
|
1159
87
|
}
|
|
@@ -1193,7 +121,7 @@ async function runEntrypoint(argv) {
|
|
|
1193
121
|
}
|
|
1194
122
|
catch (error) {
|
|
1195
123
|
if (!(error instanceof Error && error.name === 'ExitPromptError')) {
|
|
1196
|
-
|
|
124
|
+
showError(ui, 'Unexpected error.', error);
|
|
1197
125
|
process.exitCode = 1;
|
|
1198
126
|
}
|
|
1199
127
|
await shutdown();
|