@darksol/terminal 0.1.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 +198 -0
- package/assets/darksol-banner.png +0 -0
- package/bin/darksol.js +5 -0
- package/package.json +39 -0
- package/src/cli.js +434 -0
- package/src/config/store.js +75 -0
- package/src/scripts/engine.js +718 -0
- package/src/services/builders.js +70 -0
- package/src/services/cards.js +67 -0
- package/src/services/casino.js +94 -0
- package/src/services/facilitator.js +60 -0
- package/src/services/market.js +179 -0
- package/src/services/oracle.js +92 -0
- package/src/trading/dca.js +249 -0
- package/src/trading/index.js +3 -0
- package/src/trading/snipe.js +195 -0
- package/src/trading/swap.js +233 -0
- package/src/ui/banner.js +60 -0
- package/src/ui/components.js +126 -0
- package/src/ui/theme.js +46 -0
- package/src/wallet/keystore.js +127 -0
- package/src/wallet/manager.js +287 -0
- package/tests/cli.test.js +72 -0
- package/tests/config.test.js +75 -0
- package/tests/dca.test.js +141 -0
- package/tests/keystore.test.js +94 -0
- package/tests/scripts.test.js +136 -0
- package/tests/trading.test.js +21 -0
- package/tests/ui.test.js +27 -0
package/src/ui/banner.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import figlet from 'figlet';
|
|
2
|
+
import gradient from 'gradient-string';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { theme } from './theme.js';
|
|
5
|
+
|
|
6
|
+
const darksol_gradient = gradient(['#B8860B', '#FFD700', '#FFF8DC', '#FFD700', '#B8860B']);
|
|
7
|
+
|
|
8
|
+
export function showBanner(opts = {}) {
|
|
9
|
+
const banner = figlet.textSync('DARKSOL', {
|
|
10
|
+
font: 'ANSI Shadow',
|
|
11
|
+
horizontalLayout: 'fitted',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
console.log('');
|
|
15
|
+
console.log(darksol_gradient(banner));
|
|
16
|
+
console.log('');
|
|
17
|
+
console.log(
|
|
18
|
+
theme.dim(' ╔══════════════════════════════════════════════════════════╗')
|
|
19
|
+
);
|
|
20
|
+
console.log(
|
|
21
|
+
theme.dim(' ║ ') +
|
|
22
|
+
theme.gold.bold(' DARKSOL TERMINAL') +
|
|
23
|
+
theme.dim(' — ') +
|
|
24
|
+
theme.subtle('Ghost in the machine with teeth') +
|
|
25
|
+
theme.dim(' ║')
|
|
26
|
+
);
|
|
27
|
+
console.log(
|
|
28
|
+
theme.dim(' ║ ') +
|
|
29
|
+
theme.subtle(' v0.1.0') +
|
|
30
|
+
theme.dim(' ') +
|
|
31
|
+
theme.gold('🌑') +
|
|
32
|
+
theme.dim(' ║')
|
|
33
|
+
);
|
|
34
|
+
console.log(
|
|
35
|
+
theme.dim(' ╚══════════════════════════════════════════════════════════╝')
|
|
36
|
+
);
|
|
37
|
+
console.log('');
|
|
38
|
+
|
|
39
|
+
if (opts.tagline !== false) {
|
|
40
|
+
console.log(theme.subtle(' All services. One terminal. Zero trust required.'));
|
|
41
|
+
console.log('');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function showMiniBanner() {
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(theme.gold.bold(' 🌑 DARKSOL TERMINAL') + theme.dim(' v0.1.0'));
|
|
48
|
+
console.log(theme.dim(' ─────────────────────────────'));
|
|
49
|
+
console.log('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function showSection(title) {
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(theme.gold(' ◆ ') + theme.header(title));
|
|
55
|
+
console.log(theme.dim(' ' + '─'.repeat(50)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function showDivider() {
|
|
59
|
+
console.log(theme.dim(' ' + '─'.repeat(50)));
|
|
60
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import boxen from 'boxen';
|
|
3
|
+
import Table from 'cli-table3';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { theme } from './theme.js';
|
|
6
|
+
|
|
7
|
+
// Branded spinner
|
|
8
|
+
export function spinner(text) {
|
|
9
|
+
return ora({
|
|
10
|
+
text: theme.dim(text),
|
|
11
|
+
spinner: 'dots',
|
|
12
|
+
color: 'yellow',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Info card
|
|
17
|
+
export function card(title, content, opts = {}) {
|
|
18
|
+
const box = boxen(content, {
|
|
19
|
+
title: theme.gold.bold(` ${title} `),
|
|
20
|
+
titleAlignment: 'left',
|
|
21
|
+
padding: 1,
|
|
22
|
+
margin: { top: 0, bottom: 0, left: 2, right: 0 },
|
|
23
|
+
borderStyle: 'round',
|
|
24
|
+
borderColor: '#FFD700',
|
|
25
|
+
...opts,
|
|
26
|
+
});
|
|
27
|
+
console.log(box);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Key-value display
|
|
31
|
+
export function kvDisplay(pairs, opts = {}) {
|
|
32
|
+
const maxKey = Math.max(...pairs.map(([k]) => k.length));
|
|
33
|
+
const lines = pairs.map(([key, value]) => {
|
|
34
|
+
const paddedKey = key.padEnd(maxKey);
|
|
35
|
+
return ` ${theme.label(paddedKey)} ${theme.value(value)}`;
|
|
36
|
+
});
|
|
37
|
+
if (opts.title) {
|
|
38
|
+
console.log('');
|
|
39
|
+
console.log(theme.gold(' ◆ ') + theme.header(opts.title));
|
|
40
|
+
console.log(theme.dim(' ' + '─'.repeat(50)));
|
|
41
|
+
}
|
|
42
|
+
lines.forEach(l => console.log(l));
|
|
43
|
+
if (opts.footer) {
|
|
44
|
+
console.log(theme.dim(' ' + '─'.repeat(50)));
|
|
45
|
+
console.log(theme.subtle(` ${opts.footer}`));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Styled table
|
|
50
|
+
export function table(headers, rows, opts = {}) {
|
|
51
|
+
const t = new Table({
|
|
52
|
+
head: headers.map(h => theme.gold.bold(h)),
|
|
53
|
+
chars: {
|
|
54
|
+
'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐',
|
|
55
|
+
'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘',
|
|
56
|
+
'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼',
|
|
57
|
+
'right': '│', 'right-mid': '┤', 'middle': '│'
|
|
58
|
+
},
|
|
59
|
+
style: {
|
|
60
|
+
head: [],
|
|
61
|
+
border: ['grey'],
|
|
62
|
+
...opts.style,
|
|
63
|
+
},
|
|
64
|
+
...(opts.colWidths ? { colWidths: opts.colWidths } : {}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
rows.forEach(row => t.push(row));
|
|
68
|
+
console.log(t.toString());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Price formatting with color
|
|
72
|
+
export function formatPrice(price, opts = {}) {
|
|
73
|
+
const num = parseFloat(price);
|
|
74
|
+
if (isNaN(num)) return theme.dim('N/A');
|
|
75
|
+
|
|
76
|
+
const formatted = opts.decimals !== undefined
|
|
77
|
+
? num.toFixed(opts.decimals)
|
|
78
|
+
: num < 0.01 ? num.toPrecision(4) : num.toFixed(2);
|
|
79
|
+
|
|
80
|
+
return `$${formatted}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatChange(change) {
|
|
84
|
+
const num = parseFloat(change);
|
|
85
|
+
if (isNaN(num)) return theme.dim('N/A');
|
|
86
|
+
const sign = num >= 0 ? '+' : '';
|
|
87
|
+
const color = num > 0 ? theme.price.up : num < 0 ? theme.price.down : theme.price.neutral;
|
|
88
|
+
return color(`${sign}${num.toFixed(2)}%`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatAddress(address, chars = 6) {
|
|
92
|
+
if (!address) return theme.dim('N/A');
|
|
93
|
+
return `${address.slice(0, chars)}...${address.slice(-4)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatETH(wei, decimals = 6) {
|
|
97
|
+
const eth = parseFloat(wei) / 1e18;
|
|
98
|
+
return `${eth.toFixed(decimals)} ETH`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function formatUSDC(raw, decimals = 2) {
|
|
102
|
+
const usdc = parseFloat(raw) / 1e6;
|
|
103
|
+
return `$${usdc.toFixed(decimals)} USDC`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Success/error messages
|
|
107
|
+
export function success(msg) {
|
|
108
|
+
console.log(theme.success(' ✓ ') + msg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function error(msg) {
|
|
112
|
+
console.log(theme.error(' ✗ ') + msg);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function warn(msg) {
|
|
116
|
+
console.log(theme.warning(' ⚠ ') + msg);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function info(msg) {
|
|
120
|
+
console.log(theme.info(' ℹ ') + msg);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Confirmation prompt formatting
|
|
124
|
+
export function confirmLine(label, value) {
|
|
125
|
+
return `${theme.label(label.padEnd(16))} ${theme.value(value)}`;
|
|
126
|
+
}
|
package/src/ui/theme.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
// DARKSOL Terminal Theme — gold/dark palette
|
|
4
|
+
export const theme = {
|
|
5
|
+
// Primary colors
|
|
6
|
+
gold: chalk.hex('#FFD700'),
|
|
7
|
+
darkGold: chalk.hex('#B8860B'),
|
|
8
|
+
dim: chalk.hex('#666666'),
|
|
9
|
+
bright: chalk.hex('#FFFFFF'),
|
|
10
|
+
dark: chalk.hex('#1a1a2e'),
|
|
11
|
+
accent: chalk.hex('#e94560'),
|
|
12
|
+
success: chalk.hex('#00ff88'),
|
|
13
|
+
warning: chalk.hex('#ffaa00'),
|
|
14
|
+
error: chalk.hex('#ff4444'),
|
|
15
|
+
info: chalk.hex('#4488ff'),
|
|
16
|
+
muted: chalk.hex('#555555'),
|
|
17
|
+
|
|
18
|
+
// Semantic
|
|
19
|
+
price: {
|
|
20
|
+
up: chalk.hex('#00ff88'),
|
|
21
|
+
down: chalk.hex('#ff4444'),
|
|
22
|
+
neutral: chalk.hex('#888888'),
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// Box styles
|
|
26
|
+
border: chalk.hex('#FFD700'),
|
|
27
|
+
|
|
28
|
+
// Format helpers
|
|
29
|
+
header: (text) => chalk.hex('#FFD700').bold(text),
|
|
30
|
+
label: (text) => chalk.hex('#B8860B')(text),
|
|
31
|
+
value: (text) => chalk.white.bold(text),
|
|
32
|
+
subtle: (text) => chalk.hex('#666666')(text),
|
|
33
|
+
link: (text) => chalk.hex('#4488ff').underline(text),
|
|
34
|
+
badge: (text) => chalk.bgHex('#FFD700').hex('#000000').bold(` ${text} `),
|
|
35
|
+
errorBadge: (text) => chalk.bgHex('#ff4444').white.bold(` ${text} `),
|
|
36
|
+
successBadge: (text) => chalk.bgHex('#00ff88').hex('#000000').bold(` ${text} `),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Box drawing chars for custom borders
|
|
40
|
+
export const box = {
|
|
41
|
+
tl: '╔', tr: '╗', bl: '╚', br: '╝',
|
|
42
|
+
h: '═', v: '║',
|
|
43
|
+
t: '╦', b: '╩', l: '╠', r: '╣', x: '╬',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default theme;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
|
|
6
|
+
const WALLET_DIR = join(homedir(), '.darksol', 'wallets');
|
|
7
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
8
|
+
const SCRYPT_N = 2 ** 18;
|
|
9
|
+
const SCRYPT_r = 8;
|
|
10
|
+
const SCRYPT_p = 1;
|
|
11
|
+
const KEY_LENGTH = 32;
|
|
12
|
+
const SALT_LENGTH = 32;
|
|
13
|
+
const IV_LENGTH = 16;
|
|
14
|
+
const SCRYPT_MAXMEM = 512 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
function ensureDir() {
|
|
17
|
+
if (!existsSync(WALLET_DIR)) {
|
|
18
|
+
mkdirSync(WALLET_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Encrypt a private key with a password
|
|
23
|
+
export function encryptKey(privateKey, password) {
|
|
24
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
25
|
+
const iv = randomBytes(IV_LENGTH);
|
|
26
|
+
const key = scryptSync(password, salt, KEY_LENGTH, {
|
|
27
|
+
N: SCRYPT_N,
|
|
28
|
+
r: SCRYPT_r,
|
|
29
|
+
p: SCRYPT_p,
|
|
30
|
+
maxmem: SCRYPT_MAXMEM,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
34
|
+
let encrypted = cipher.update(privateKey, 'utf8', 'hex');
|
|
35
|
+
encrypted += cipher.final('hex');
|
|
36
|
+
const tag = cipher.getAuthTag();
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
version: 1,
|
|
40
|
+
algorithm: ALGORITHM,
|
|
41
|
+
kdf: 'scrypt',
|
|
42
|
+
kdfParams: { N: SCRYPT_N, r: SCRYPT_r, p: SCRYPT_p },
|
|
43
|
+
salt: salt.toString('hex'),
|
|
44
|
+
iv: iv.toString('hex'),
|
|
45
|
+
tag: tag.toString('hex'),
|
|
46
|
+
ciphertext: encrypted,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Decrypt a private key with a password
|
|
51
|
+
export function decryptKey(keystore, password) {
|
|
52
|
+
const salt = Buffer.from(keystore.salt, 'hex');
|
|
53
|
+
const iv = Buffer.from(keystore.iv, 'hex');
|
|
54
|
+
const tag = Buffer.from(keystore.tag, 'hex');
|
|
55
|
+
|
|
56
|
+
const key = scryptSync(password, salt, KEY_LENGTH, {
|
|
57
|
+
N: keystore.kdfParams.N,
|
|
58
|
+
r: keystore.kdfParams.r,
|
|
59
|
+
p: keystore.kdfParams.p,
|
|
60
|
+
maxmem: SCRYPT_MAXMEM,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
64
|
+
decipher.setAuthTag(tag);
|
|
65
|
+
let decrypted = decipher.update(keystore.ciphertext, 'hex', 'utf8');
|
|
66
|
+
decrypted += decipher.final('utf8');
|
|
67
|
+
|
|
68
|
+
return decrypted;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Save wallet to disk
|
|
72
|
+
export function saveWallet(name, address, keystoreData, metadata = {}) {
|
|
73
|
+
ensureDir();
|
|
74
|
+
const walletFile = join(WALLET_DIR, `${name}.json`);
|
|
75
|
+
|
|
76
|
+
const wallet = {
|
|
77
|
+
name,
|
|
78
|
+
address,
|
|
79
|
+
keystore: keystoreData,
|
|
80
|
+
chain: metadata.chain || 'base',
|
|
81
|
+
createdAt: new Date().toISOString(),
|
|
82
|
+
...metadata,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
writeFileSync(walletFile, JSON.stringify(wallet, null, 2));
|
|
86
|
+
return walletFile;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Load wallet from disk
|
|
90
|
+
export function loadWallet(name) {
|
|
91
|
+
const walletFile = join(WALLET_DIR, `${name}.json`);
|
|
92
|
+
if (!existsSync(walletFile)) {
|
|
93
|
+
throw new Error(`Wallet "${name}" not found`);
|
|
94
|
+
}
|
|
95
|
+
return JSON.parse(readFileSync(walletFile, 'utf8'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// List all wallets
|
|
99
|
+
export function listWallets() {
|
|
100
|
+
ensureDir();
|
|
101
|
+
const files = readdirSync(WALLET_DIR).filter(f => f.endsWith('.json'));
|
|
102
|
+
return files.map(f => {
|
|
103
|
+
const data = JSON.parse(readFileSync(join(WALLET_DIR, f), 'utf8'));
|
|
104
|
+
return {
|
|
105
|
+
name: data.name,
|
|
106
|
+
address: data.address,
|
|
107
|
+
chain: data.chain,
|
|
108
|
+
createdAt: data.createdAt,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Delete wallet
|
|
114
|
+
export function deleteWallet(name) {
|
|
115
|
+
const walletFile = join(WALLET_DIR, `${name}.json`);
|
|
116
|
+
if (!existsSync(walletFile)) {
|
|
117
|
+
throw new Error(`Wallet "${name}" not found`);
|
|
118
|
+
}
|
|
119
|
+
unlinkSync(walletFile);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if wallet exists
|
|
123
|
+
export function walletExists(name) {
|
|
124
|
+
return existsSync(join(WALLET_DIR, `${name}.json`));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { WALLET_DIR };
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
|
+
import { encryptKey, decryptKey, saveWallet, loadWallet, listWallets, walletExists, WALLET_DIR } from './keystore.js';
|
|
3
|
+
import { getConfig, setConfig, getRPC } from '../config/store.js';
|
|
4
|
+
import { theme } from '../ui/theme.js';
|
|
5
|
+
import { card, kvDisplay, success, error, warn, spinner, table } from '../ui/components.js';
|
|
6
|
+
import { showSection } from '../ui/banner.js';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
|
|
9
|
+
// Create a new wallet
|
|
10
|
+
export async function createWallet(name, opts = {}) {
|
|
11
|
+
if (!name) {
|
|
12
|
+
const { walletName } = await inquirer.prompt([{
|
|
13
|
+
type: 'input',
|
|
14
|
+
name: 'walletName',
|
|
15
|
+
message: theme.gold('Wallet name:'),
|
|
16
|
+
validate: (v) => v.length > 0 || 'Name required',
|
|
17
|
+
}]);
|
|
18
|
+
name = walletName;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (walletExists(name)) {
|
|
22
|
+
error(`Wallet "${name}" already exists`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { password } = await inquirer.prompt([{
|
|
27
|
+
type: 'password',
|
|
28
|
+
name: 'password',
|
|
29
|
+
message: theme.gold('Encryption password:'),
|
|
30
|
+
mask: '●',
|
|
31
|
+
validate: (v) => v.length >= 8 || 'Minimum 8 characters',
|
|
32
|
+
}]);
|
|
33
|
+
|
|
34
|
+
const { confirmPassword } = await inquirer.prompt([{
|
|
35
|
+
type: 'password',
|
|
36
|
+
name: 'confirmPassword',
|
|
37
|
+
message: theme.gold('Confirm password:'),
|
|
38
|
+
mask: '●',
|
|
39
|
+
}]);
|
|
40
|
+
|
|
41
|
+
if (password !== confirmPassword) {
|
|
42
|
+
error('Passwords do not match');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const spin = spinner('Generating wallet...').start();
|
|
47
|
+
|
|
48
|
+
const wallet = ethers.Wallet.createRandom();
|
|
49
|
+
const keystoreData = encryptKey(wallet.privateKey, password);
|
|
50
|
+
const chain = opts.chain || getConfig('chain') || 'base';
|
|
51
|
+
saveWallet(name, wallet.address, keystoreData, { chain });
|
|
52
|
+
|
|
53
|
+
// Set as active if first wallet
|
|
54
|
+
const wallets = listWallets();
|
|
55
|
+
if (wallets.length === 1) {
|
|
56
|
+
setConfig('activeWallet', name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
spin.succeed(theme.success('Wallet created'));
|
|
60
|
+
|
|
61
|
+
console.log('');
|
|
62
|
+
showSection('NEW WALLET');
|
|
63
|
+
kvDisplay([
|
|
64
|
+
['Name', name],
|
|
65
|
+
['Address', wallet.address],
|
|
66
|
+
['Chain', chain],
|
|
67
|
+
['Stored', WALLET_DIR],
|
|
68
|
+
]);
|
|
69
|
+
console.log('');
|
|
70
|
+
warn('Back up your password — there is NO recovery if lost.');
|
|
71
|
+
warn('Private key is AES-256-GCM encrypted with scrypt KDF.');
|
|
72
|
+
console.log('');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Import wallet from private key
|
|
76
|
+
export async function importWallet(name, opts = {}) {
|
|
77
|
+
if (!name) {
|
|
78
|
+
const { walletName } = await inquirer.prompt([{
|
|
79
|
+
type: 'input',
|
|
80
|
+
name: 'walletName',
|
|
81
|
+
message: theme.gold('Wallet name:'),
|
|
82
|
+
validate: (v) => v.length > 0 || 'Name required',
|
|
83
|
+
}]);
|
|
84
|
+
name = walletName;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (walletExists(name)) {
|
|
88
|
+
error(`Wallet "${name}" already exists`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { privateKey } = await inquirer.prompt([{
|
|
93
|
+
type: 'password',
|
|
94
|
+
name: 'privateKey',
|
|
95
|
+
message: theme.gold('Private key (0x...):'),
|
|
96
|
+
mask: '●',
|
|
97
|
+
validate: (v) => {
|
|
98
|
+
try {
|
|
99
|
+
new ethers.Wallet(v);
|
|
100
|
+
return true;
|
|
101
|
+
} catch {
|
|
102
|
+
return 'Invalid private key';
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
}]);
|
|
106
|
+
|
|
107
|
+
const { password } = await inquirer.prompt([{
|
|
108
|
+
type: 'password',
|
|
109
|
+
name: 'password',
|
|
110
|
+
message: theme.gold('Encryption password:'),
|
|
111
|
+
mask: '●',
|
|
112
|
+
validate: (v) => v.length >= 8 || 'Minimum 8 characters',
|
|
113
|
+
}]);
|
|
114
|
+
|
|
115
|
+
const spin = spinner('Encrypting and storing...').start();
|
|
116
|
+
|
|
117
|
+
const wallet = new ethers.Wallet(privateKey);
|
|
118
|
+
const keystoreData = encryptKey(privateKey, password);
|
|
119
|
+
const chain = opts.chain || getConfig('chain') || 'base';
|
|
120
|
+
saveWallet(name, wallet.address, keystoreData, { chain });
|
|
121
|
+
|
|
122
|
+
spin.succeed(theme.success('Wallet imported'));
|
|
123
|
+
|
|
124
|
+
console.log('');
|
|
125
|
+
showSection('IMPORTED WALLET');
|
|
126
|
+
kvDisplay([
|
|
127
|
+
['Name', name],
|
|
128
|
+
['Address', wallet.address],
|
|
129
|
+
['Chain', chain],
|
|
130
|
+
]);
|
|
131
|
+
console.log('');
|
|
132
|
+
success('Private key encrypted and stored securely.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// List wallets
|
|
136
|
+
export async function showWallets() {
|
|
137
|
+
const wallets = listWallets();
|
|
138
|
+
const active = getConfig('activeWallet');
|
|
139
|
+
|
|
140
|
+
if (wallets.length === 0) {
|
|
141
|
+
warn('No wallets found. Create one with: darksol wallet create');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
showSection('WALLETS');
|
|
146
|
+
|
|
147
|
+
const rows = wallets.map(w => [
|
|
148
|
+
w.name === active ? theme.gold('► ' + w.name) : ' ' + w.name,
|
|
149
|
+
w.address,
|
|
150
|
+
w.chain,
|
|
151
|
+
new Date(w.createdAt).toLocaleDateString(),
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
table(['Name', 'Address', 'Chain', 'Created'], rows);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Get a signer (unlocked wallet) for transactions
|
|
158
|
+
export async function getSigner(walletName, password) {
|
|
159
|
+
const name = walletName || getConfig('activeWallet');
|
|
160
|
+
if (!name) {
|
|
161
|
+
throw new Error('No active wallet. Set one with: darksol wallet use <name>');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const walletData = loadWallet(name);
|
|
165
|
+
const privateKey = decryptKey(walletData.keystore, password);
|
|
166
|
+
const rpcUrl = getRPC(walletData.chain);
|
|
167
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
168
|
+
const signer = new ethers.Wallet(privateKey, provider);
|
|
169
|
+
|
|
170
|
+
return { signer, provider, address: walletData.address, chain: walletData.chain };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Get wallet balance
|
|
174
|
+
export async function getBalance(walletName) {
|
|
175
|
+
const name = walletName || getConfig('activeWallet');
|
|
176
|
+
if (!name) {
|
|
177
|
+
error('No active wallet. Set one with: darksol wallet use <name>');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const walletData = loadWallet(name);
|
|
182
|
+
const rpcUrl = getRPC(walletData.chain);
|
|
183
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
184
|
+
|
|
185
|
+
const spin = spinner('Fetching balance...').start();
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const balance = await provider.getBalance(walletData.address);
|
|
189
|
+
const ethBalance = ethers.formatEther(balance);
|
|
190
|
+
|
|
191
|
+
// Also check USDC balance
|
|
192
|
+
const usdcAddresses = {
|
|
193
|
+
base: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
194
|
+
ethereum: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
195
|
+
polygon: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
|
|
196
|
+
arbitrum: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
|
|
197
|
+
optimism: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85',
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
let usdcBalance = '0.00';
|
|
201
|
+
const usdcAddr = usdcAddresses[walletData.chain];
|
|
202
|
+
if (usdcAddr) {
|
|
203
|
+
const usdc = new ethers.Contract(usdcAddr, [
|
|
204
|
+
'function balanceOf(address) view returns (uint256)',
|
|
205
|
+
'function decimals() view returns (uint8)',
|
|
206
|
+
], provider);
|
|
207
|
+
try {
|
|
208
|
+
const raw = await usdc.balanceOf(walletData.address);
|
|
209
|
+
const decimals = await usdc.decimals();
|
|
210
|
+
usdcBalance = ethers.formatUnits(raw, decimals);
|
|
211
|
+
} catch { }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
spin.succeed('Balance fetched');
|
|
215
|
+
|
|
216
|
+
console.log('');
|
|
217
|
+
showSection(`BALANCE — ${name}`);
|
|
218
|
+
kvDisplay([
|
|
219
|
+
['Address', walletData.address],
|
|
220
|
+
['Chain', walletData.chain],
|
|
221
|
+
['Native', `${parseFloat(ethBalance).toFixed(6)} ETH`],
|
|
222
|
+
['USDC', `$${parseFloat(usdcBalance).toFixed(2)}`],
|
|
223
|
+
]);
|
|
224
|
+
console.log('');
|
|
225
|
+
} catch (err) {
|
|
226
|
+
spin.fail('Failed to fetch balance');
|
|
227
|
+
error(err.message);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Set active wallet
|
|
232
|
+
export function useWallet(name) {
|
|
233
|
+
if (!walletExists(name)) {
|
|
234
|
+
error(`Wallet "${name}" not found`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
setConfig('activeWallet', name);
|
|
238
|
+
success(`Active wallet set to "${name}"`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Export wallet (show address only, never PK without password)
|
|
242
|
+
export async function exportWallet(name) {
|
|
243
|
+
if (!name) {
|
|
244
|
+
name = getConfig('activeWallet');
|
|
245
|
+
}
|
|
246
|
+
if (!name || !walletExists(name)) {
|
|
247
|
+
error('Wallet not found');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const walletData = loadWallet(name);
|
|
252
|
+
|
|
253
|
+
showSection(`WALLET — ${name}`);
|
|
254
|
+
kvDisplay([
|
|
255
|
+
['Address', walletData.address],
|
|
256
|
+
['Chain', walletData.chain],
|
|
257
|
+
['Created', walletData.createdAt],
|
|
258
|
+
['Keystore', 'AES-256-GCM + scrypt'],
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
const { confirm } = await inquirer.prompt([{
|
|
262
|
+
type: 'confirm',
|
|
263
|
+
name: 'confirm',
|
|
264
|
+
message: theme.accent('Export private key? (requires password)'),
|
|
265
|
+
default: false,
|
|
266
|
+
}]);
|
|
267
|
+
|
|
268
|
+
if (!confirm) return;
|
|
269
|
+
|
|
270
|
+
const { password } = await inquirer.prompt([{
|
|
271
|
+
type: 'password',
|
|
272
|
+
name: 'password',
|
|
273
|
+
message: theme.gold('Password:'),
|
|
274
|
+
mask: '●',
|
|
275
|
+
}]);
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const pk = decryptKey(walletData.keystore, password);
|
|
279
|
+
console.log('');
|
|
280
|
+
warn('PRIVATE KEY — DO NOT SHARE');
|
|
281
|
+
console.log(theme.accent(` ${pk}`));
|
|
282
|
+
console.log('');
|
|
283
|
+
warn('This key controls all funds. Keep it safe.');
|
|
284
|
+
} catch {
|
|
285
|
+
error('Wrong password');
|
|
286
|
+
}
|
|
287
|
+
}
|