@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
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { ethers } from 'ethers';
|
|
5
|
+
import { decryptKey, loadWallet, listWallets } from '../wallet/keystore.js';
|
|
6
|
+
import { getConfig, getRPC } from '../config/store.js';
|
|
7
|
+
import { resolveToken } from '../trading/swap.js';
|
|
8
|
+
import { theme } from '../ui/theme.js';
|
|
9
|
+
import { spinner, kvDisplay, success, error, warn, table, info } from '../ui/components.js';
|
|
10
|
+
import { showSection, showDivider } from '../ui/banner.js';
|
|
11
|
+
import inquirer from 'inquirer';
|
|
12
|
+
|
|
13
|
+
const SCRIPTS_DIR = join(homedir(), '.darksol', 'scripts');
|
|
14
|
+
|
|
15
|
+
function ensureDir() {
|
|
16
|
+
if (!existsSync(SCRIPTS_DIR)) mkdirSync(SCRIPTS_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ──────────────────────────────────────────────────
|
|
20
|
+
// SCRIPT TEMPLATES
|
|
21
|
+
// ──────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const TEMPLATES = {
|
|
24
|
+
'buy-token': {
|
|
25
|
+
name: 'Buy Token',
|
|
26
|
+
description: 'Buy a token with ETH at current price',
|
|
27
|
+
params: ['token', 'amountETH'],
|
|
28
|
+
template: `// Buy Token Script
|
|
29
|
+
// Buys {token} with {amountETH} ETH via Uniswap V2
|
|
30
|
+
module.exports = async function({ signer, provider, ethers, config, params }) {
|
|
31
|
+
const WETH = '0x4200000000000000000000000000000000000006';
|
|
32
|
+
const ROUTER = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
|
|
33
|
+
|
|
34
|
+
const router = new ethers.Contract(ROUTER, [
|
|
35
|
+
'function swapExactETHForTokens(uint amountOutMin, address[] path, address to, uint deadline) payable returns (uint[] amounts)',
|
|
36
|
+
'function getAmountsOut(uint amountIn, address[] path) view returns (uint[] amounts)',
|
|
37
|
+
], signer);
|
|
38
|
+
|
|
39
|
+
const amountIn = ethers.parseEther(params.amountETH);
|
|
40
|
+
const path = [WETH, params.token];
|
|
41
|
+
const deadline = Math.floor(Date.now() / 1000) + 300;
|
|
42
|
+
|
|
43
|
+
// Get estimated output
|
|
44
|
+
const amounts = await router.getAmountsOut(amountIn, path);
|
|
45
|
+
const minOut = (amounts[1] * 95n) / 100n; // 5% slippage
|
|
46
|
+
|
|
47
|
+
console.log('Estimated output:', ethers.formatUnits(amounts[1], 18));
|
|
48
|
+
console.log('Min output (5% slippage):', ethers.formatUnits(minOut, 18));
|
|
49
|
+
|
|
50
|
+
const tx = await router.swapExactETHForTokens(minOut, path, signer.address, deadline, { value: amountIn });
|
|
51
|
+
const receipt = await tx.wait();
|
|
52
|
+
|
|
53
|
+
return { txHash: receipt.hash, block: receipt.blockNumber, gasUsed: receipt.gasUsed.toString() };
|
|
54
|
+
};`,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
'sell-token': {
|
|
58
|
+
name: 'Sell Token',
|
|
59
|
+
description: 'Sell a token for ETH',
|
|
60
|
+
params: ['token', 'amountPercent'],
|
|
61
|
+
template: `// Sell Token Script
|
|
62
|
+
// Sells {amountPercent}% of {token} balance for ETH
|
|
63
|
+
module.exports = async function({ signer, provider, ethers, config, params }) {
|
|
64
|
+
const WETH = '0x4200000000000000000000000000000000000006';
|
|
65
|
+
const ROUTER = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
|
|
66
|
+
|
|
67
|
+
const token = new ethers.Contract(params.token, [
|
|
68
|
+
'function balanceOf(address) view returns (uint256)',
|
|
69
|
+
'function approve(address, uint256) returns (bool)',
|
|
70
|
+
'function decimals() view returns (uint8)',
|
|
71
|
+
'function symbol() view returns (string)',
|
|
72
|
+
], signer);
|
|
73
|
+
|
|
74
|
+
const balance = await token.balanceOf(signer.address);
|
|
75
|
+
const decimals = await token.decimals();
|
|
76
|
+
const symbol = await token.symbol();
|
|
77
|
+
const sellAmount = (balance * BigInt(params.amountPercent)) / 100n;
|
|
78
|
+
|
|
79
|
+
console.log('Token:', symbol);
|
|
80
|
+
console.log('Balance:', ethers.formatUnits(balance, decimals));
|
|
81
|
+
console.log('Selling:', ethers.formatUnits(sellAmount, decimals), '(' + params.amountPercent + '%)');
|
|
82
|
+
|
|
83
|
+
// Approve
|
|
84
|
+
const approveTx = await token.approve(ROUTER, sellAmount);
|
|
85
|
+
await approveTx.wait();
|
|
86
|
+
|
|
87
|
+
const router = new ethers.Contract(ROUTER, [
|
|
88
|
+
'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] path, address to, uint deadline) returns (uint[] amounts)',
|
|
89
|
+
], signer);
|
|
90
|
+
|
|
91
|
+
const deadline = Math.floor(Date.now() / 1000) + 300;
|
|
92
|
+
const tx = await router.swapExactTokensForETH(sellAmount, 0, [params.token, WETH], signer.address, deadline);
|
|
93
|
+
const receipt = await tx.wait();
|
|
94
|
+
|
|
95
|
+
return { txHash: receipt.hash, block: receipt.blockNumber, gasUsed: receipt.gasUsed.toString() };
|
|
96
|
+
};`,
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
'limit-buy': {
|
|
100
|
+
name: 'Limit Buy',
|
|
101
|
+
description: 'Buy token when price drops to target (polling)',
|
|
102
|
+
params: ['token', 'targetPrice', 'amountETH', 'pollSeconds'],
|
|
103
|
+
template: `// Limit Buy Script
|
|
104
|
+
// Watches {token} and buys with {amountETH} ETH when price <= {targetPrice}
|
|
105
|
+
module.exports = async function({ signer, provider, ethers, config, params }) {
|
|
106
|
+
const WETH = '0x4200000000000000000000000000000000000006';
|
|
107
|
+
const ROUTER = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
|
|
108
|
+
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
109
|
+
const pollMs = (parseInt(params.pollSeconds) || 30) * 1000;
|
|
110
|
+
|
|
111
|
+
const router = new ethers.Contract(ROUTER, [
|
|
112
|
+
'function swapExactETHForTokens(uint amountOutMin, address[] path, address to, uint deadline) payable returns (uint[] amounts)',
|
|
113
|
+
'function getAmountsOut(uint amountIn, address[] path) view returns (uint[] amounts)',
|
|
114
|
+
], signer);
|
|
115
|
+
|
|
116
|
+
console.log('Limit buy active — polling every', pollMs/1000, 'seconds');
|
|
117
|
+
console.log('Target price: $' + params.targetPrice);
|
|
118
|
+
|
|
119
|
+
while (true) {
|
|
120
|
+
try {
|
|
121
|
+
// Check price via WETH->Token->USDC path estimate
|
|
122
|
+
const oneETH = ethers.parseEther('1');
|
|
123
|
+
const amounts = await router.getAmountsOut(oneETH, [WETH, params.token]);
|
|
124
|
+
const tokenPerETH = amounts[1];
|
|
125
|
+
|
|
126
|
+
// Rough price calc (assumes 18 decimals)
|
|
127
|
+
const priceEstimate = parseFloat(ethers.formatUnits(tokenPerETH, 18));
|
|
128
|
+
process.stdout.write('\\rPrice check: ~' + priceEstimate.toFixed(6) + ' tokens/ETH ');
|
|
129
|
+
|
|
130
|
+
if (priceEstimate >= parseFloat(params.targetPrice)) {
|
|
131
|
+
console.log('\\nTarget hit! Executing buy...');
|
|
132
|
+
|
|
133
|
+
const amountIn = ethers.parseEther(params.amountETH);
|
|
134
|
+
const deadline = Math.floor(Date.now() / 1000) + 300;
|
|
135
|
+
const minOut = (amounts[1] * BigInt(Math.floor(parseFloat(params.amountETH) * 1e18))) / oneETH;
|
|
136
|
+
const minOutSlipped = (minOut * 90n) / 100n;
|
|
137
|
+
|
|
138
|
+
const tx = await router.swapExactETHForTokens(minOutSlipped, [WETH, params.token], signer.address, deadline, { value: amountIn });
|
|
139
|
+
const receipt = await tx.wait();
|
|
140
|
+
return { txHash: receipt.hash, filled: true };
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.log('\\nPrice check error:', err.message);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await new Promise(r => setTimeout(r, pollMs));
|
|
147
|
+
}
|
|
148
|
+
};`,
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
'stop-loss': {
|
|
152
|
+
name: 'Stop Loss',
|
|
153
|
+
description: 'Auto-sell token if value drops below threshold',
|
|
154
|
+
params: ['token', 'stopPrice', 'sellPercent', 'pollSeconds'],
|
|
155
|
+
template: `// Stop Loss Script
|
|
156
|
+
// Sells {sellPercent}% of {token} if price drops below {stopPrice}
|
|
157
|
+
module.exports = async function({ signer, provider, ethers, config, params }) {
|
|
158
|
+
const WETH = '0x4200000000000000000000000000000000000006';
|
|
159
|
+
const ROUTER = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
|
|
160
|
+
const pollMs = (parseInt(params.pollSeconds) || 15) * 1000;
|
|
161
|
+
|
|
162
|
+
const token = new ethers.Contract(params.token, [
|
|
163
|
+
'function balanceOf(address) view returns (uint256)',
|
|
164
|
+
'function approve(address, uint256) returns (bool)',
|
|
165
|
+
'function decimals() view returns (uint8)',
|
|
166
|
+
'function symbol() view returns (string)',
|
|
167
|
+
], signer);
|
|
168
|
+
|
|
169
|
+
const router = new ethers.Contract(ROUTER, [
|
|
170
|
+
'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] path, address to, uint deadline) returns (uint[] amounts)',
|
|
171
|
+
'function getAmountsOut(uint amountIn, address[] path) view returns (uint[] amounts)',
|
|
172
|
+
], signer);
|
|
173
|
+
|
|
174
|
+
const symbol = await token.symbol();
|
|
175
|
+
const decimals = await token.decimals();
|
|
176
|
+
console.log('Stop loss active for', symbol);
|
|
177
|
+
console.log('Stop price: $' + params.stopPrice);
|
|
178
|
+
console.log('Will sell', params.sellPercent + '% on trigger');
|
|
179
|
+
|
|
180
|
+
while (true) {
|
|
181
|
+
try {
|
|
182
|
+
const balance = await token.balanceOf(signer.address);
|
|
183
|
+
if (balance === 0n) {
|
|
184
|
+
console.log('\\nNo token balance remaining. Exiting.');
|
|
185
|
+
return { triggered: false, reason: 'zero_balance' };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Get price estimate
|
|
189
|
+
const testAmount = balance / 100n || 1n;
|
|
190
|
+
const amounts = await router.getAmountsOut(testAmount, [params.token, WETH]);
|
|
191
|
+
const ethValue = parseFloat(ethers.formatEther(amounts[1]));
|
|
192
|
+
|
|
193
|
+
process.stdout.write('\\rMonitoring ' + symbol + ' — value estimate active ');
|
|
194
|
+
|
|
195
|
+
// Simple threshold (this is approximate — production would use oracle)
|
|
196
|
+
if (ethValue < parseFloat(params.stopPrice)) {
|
|
197
|
+
console.log('\\n⚠ STOP LOSS TRIGGERED');
|
|
198
|
+
|
|
199
|
+
const sellAmount = (balance * BigInt(params.sellPercent)) / 100n;
|
|
200
|
+
await (await token.approve(ROUTER, sellAmount)).wait();
|
|
201
|
+
|
|
202
|
+
const deadline = Math.floor(Date.now() / 1000) + 120;
|
|
203
|
+
const tx = await router.swapExactTokensForETH(sellAmount, 0, [params.token, WETH], signer.address, deadline);
|
|
204
|
+
const receipt = await tx.wait();
|
|
205
|
+
|
|
206
|
+
return { triggered: true, txHash: receipt.hash, sold: ethers.formatUnits(sellAmount, decimals) };
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.log('\\nMonitor error:', err.message);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await new Promise(r => setTimeout(r, pollMs));
|
|
213
|
+
}
|
|
214
|
+
};`,
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
'multi-buy': {
|
|
218
|
+
name: 'Multi Buy',
|
|
219
|
+
description: 'Buy multiple tokens in one execution',
|
|
220
|
+
params: ['tokens', 'amountETHEach'],
|
|
221
|
+
template: `// Multi Buy Script
|
|
222
|
+
// Buys multiple tokens, splitting ETH equally
|
|
223
|
+
// tokens param should be comma-separated addresses
|
|
224
|
+
module.exports = async function({ signer, provider, ethers, config, params }) {
|
|
225
|
+
const WETH = '0x4200000000000000000000000000000000000006';
|
|
226
|
+
const ROUTER = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
|
|
227
|
+
const tokens = params.tokens.split(',').map(t => t.trim());
|
|
228
|
+
const amountPerToken = ethers.parseEther(params.amountETHEach);
|
|
229
|
+
|
|
230
|
+
const router = new ethers.Contract(ROUTER, [
|
|
231
|
+
'function swapExactETHForTokens(uint amountOutMin, address[] path, address to, uint deadline) payable returns (uint[] amounts)',
|
|
232
|
+
], signer);
|
|
233
|
+
|
|
234
|
+
const results = [];
|
|
235
|
+
|
|
236
|
+
for (const tokenAddr of tokens) {
|
|
237
|
+
console.log('\\nBuying token:', tokenAddr);
|
|
238
|
+
try {
|
|
239
|
+
const deadline = Math.floor(Date.now() / 1000) + 300;
|
|
240
|
+
const tx = await router.swapExactETHForTokens(0, [WETH, tokenAddr], signer.address, deadline, { value: amountPerToken });
|
|
241
|
+
const receipt = await tx.wait();
|
|
242
|
+
results.push({ token: tokenAddr, txHash: receipt.hash, status: 'success' });
|
|
243
|
+
console.log('✓ Bought:', receipt.hash);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
results.push({ token: tokenAddr, error: err.message, status: 'failed' });
|
|
246
|
+
console.log('✗ Failed:', err.message);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { results, totalSpent: ethers.formatEther(amountPerToken * BigInt(tokens.length)) + ' ETH' };
|
|
251
|
+
};`,
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
'transfer': {
|
|
255
|
+
name: 'Transfer',
|
|
256
|
+
description: 'Transfer ETH or tokens to another address',
|
|
257
|
+
params: ['to', 'amount', 'token'],
|
|
258
|
+
template: `// Transfer Script
|
|
259
|
+
// Sends {amount} of {token} (or ETH if token is 'ETH') to {to}
|
|
260
|
+
module.exports = async function({ signer, provider, ethers, config, params }) {
|
|
261
|
+
const toAddress = params.to;
|
|
262
|
+
|
|
263
|
+
if (!params.token || params.token.toUpperCase() === 'ETH') {
|
|
264
|
+
// ETH transfer
|
|
265
|
+
const value = ethers.parseEther(params.amount);
|
|
266
|
+
console.log('Sending', params.amount, 'ETH to', toAddress);
|
|
267
|
+
const tx = await signer.sendTransaction({ to: toAddress, value });
|
|
268
|
+
const receipt = await tx.wait();
|
|
269
|
+
return { txHash: receipt.hash, type: 'ETH', amount: params.amount };
|
|
270
|
+
} else {
|
|
271
|
+
// ERC20 transfer
|
|
272
|
+
const token = new ethers.Contract(params.token, [
|
|
273
|
+
'function transfer(address, uint256) returns (bool)',
|
|
274
|
+
'function decimals() view returns (uint8)',
|
|
275
|
+
'function symbol() view returns (string)',
|
|
276
|
+
], signer);
|
|
277
|
+
|
|
278
|
+
const decimals = await token.decimals();
|
|
279
|
+
const symbol = await token.symbol();
|
|
280
|
+
const amount = ethers.parseUnits(params.amount, decimals);
|
|
281
|
+
|
|
282
|
+
console.log('Sending', params.amount, symbol, 'to', toAddress);
|
|
283
|
+
const tx = await token.transfer(toAddress, amount);
|
|
284
|
+
const receipt = await tx.wait();
|
|
285
|
+
return { txHash: receipt.hash, type: symbol, amount: params.amount };
|
|
286
|
+
}
|
|
287
|
+
};`,
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
'empty': {
|
|
291
|
+
name: 'Custom Script',
|
|
292
|
+
description: 'Empty template for custom logic',
|
|
293
|
+
params: [],
|
|
294
|
+
template: `// Custom DARKSOL Script
|
|
295
|
+
// Available in context: { signer, provider, ethers, config, params }
|
|
296
|
+
//
|
|
297
|
+
// signer — ethers.Wallet connected to provider (your unlocked wallet)
|
|
298
|
+
// provider — ethers.JsonRpcProvider for the active chain
|
|
299
|
+
// ethers — the ethers library
|
|
300
|
+
// config — { chain, slippage, gasMultiplier, rpcs, ... }
|
|
301
|
+
// params — your custom parameters (from script config)
|
|
302
|
+
//
|
|
303
|
+
// Return an object with results. Throw to signal failure.
|
|
304
|
+
module.exports = async function({ signer, provider, ethers, config, params }) {
|
|
305
|
+
console.log('Wallet:', signer.address);
|
|
306
|
+
console.log('Chain:', config.chain);
|
|
307
|
+
|
|
308
|
+
// Your logic here
|
|
309
|
+
|
|
310
|
+
return { status: 'ok' };
|
|
311
|
+
};`,
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// ──────────────────────────────────────────────────
|
|
316
|
+
// SCRIPT MANAGEMENT
|
|
317
|
+
// ──────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
function getScriptPath(name) {
|
|
320
|
+
return join(SCRIPTS_DIR, `${name}.json`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function loadScript(name) {
|
|
324
|
+
const path = getScriptPath(name);
|
|
325
|
+
if (!existsSync(path)) throw new Error(`Script "${name}" not found`);
|
|
326
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function saveScript(script) {
|
|
330
|
+
ensureDir();
|
|
331
|
+
writeFileSync(getScriptPath(script.name), JSON.stringify(script, null, 2));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function getAllScripts() {
|
|
335
|
+
ensureDir();
|
|
336
|
+
return readdirSync(SCRIPTS_DIR)
|
|
337
|
+
.filter(f => f.endsWith('.json'))
|
|
338
|
+
.map(f => JSON.parse(readFileSync(join(SCRIPTS_DIR, f), 'utf8')));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Create a new script from template or custom
|
|
342
|
+
export async function createScript(opts = {}) {
|
|
343
|
+
showSection('CREATE EXECUTION SCRIPT');
|
|
344
|
+
|
|
345
|
+
const templateChoices = Object.entries(TEMPLATES).map(([key, t]) => ({
|
|
346
|
+
name: `${t.name} — ${t.description}`,
|
|
347
|
+
value: key,
|
|
348
|
+
}));
|
|
349
|
+
|
|
350
|
+
const { templateKey } = await inquirer.prompt([{
|
|
351
|
+
type: 'list',
|
|
352
|
+
name: 'templateKey',
|
|
353
|
+
message: theme.gold('Select template:'),
|
|
354
|
+
choices: templateChoices,
|
|
355
|
+
}]);
|
|
356
|
+
|
|
357
|
+
const template = TEMPLATES[templateKey];
|
|
358
|
+
|
|
359
|
+
const { scriptName } = await inquirer.prompt([{
|
|
360
|
+
type: 'input',
|
|
361
|
+
name: 'scriptName',
|
|
362
|
+
message: theme.gold('Script name:'),
|
|
363
|
+
default: templateKey,
|
|
364
|
+
validate: v => /^[a-zA-Z0-9_-]+$/.test(v) || 'Alphanumeric, dashes, underscores only',
|
|
365
|
+
}]);
|
|
366
|
+
|
|
367
|
+
// Collect params
|
|
368
|
+
const paramValues = {};
|
|
369
|
+
for (const param of template.params) {
|
|
370
|
+
const { value } = await inquirer.prompt([{
|
|
371
|
+
type: 'input',
|
|
372
|
+
name: 'value',
|
|
373
|
+
message: theme.gold(`${param}:`),
|
|
374
|
+
}]);
|
|
375
|
+
paramValues[param] = value;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const { walletName } = await inquirer.prompt([{
|
|
379
|
+
type: 'input',
|
|
380
|
+
name: 'walletName',
|
|
381
|
+
message: theme.gold('Wallet to use:'),
|
|
382
|
+
default: getConfig('activeWallet') || '',
|
|
383
|
+
}]);
|
|
384
|
+
|
|
385
|
+
const script = {
|
|
386
|
+
name: scriptName,
|
|
387
|
+
template: templateKey,
|
|
388
|
+
description: template.description,
|
|
389
|
+
wallet: walletName,
|
|
390
|
+
chain: getConfig('chain') || 'base',
|
|
391
|
+
params: paramValues,
|
|
392
|
+
code: template.template,
|
|
393
|
+
createdAt: new Date().toISOString(),
|
|
394
|
+
lastRun: null,
|
|
395
|
+
runCount: 0,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
saveScript(script);
|
|
399
|
+
|
|
400
|
+
console.log('');
|
|
401
|
+
success(`Script created: ${scriptName}`);
|
|
402
|
+
kvDisplay([
|
|
403
|
+
['Name', scriptName],
|
|
404
|
+
['Template', template.name],
|
|
405
|
+
['Wallet', walletName],
|
|
406
|
+
['Chain', script.chain],
|
|
407
|
+
['Params', Object.entries(paramValues).map(([k, v]) => `${k}=${v}`).join(', ') || '(none)'],
|
|
408
|
+
['Stored', getScriptPath(scriptName)],
|
|
409
|
+
]);
|
|
410
|
+
console.log('');
|
|
411
|
+
info('Run with: darksol script run ' + scriptName);
|
|
412
|
+
info('Edit code: darksol script edit ' + scriptName);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// List all scripts
|
|
416
|
+
export function listScripts() {
|
|
417
|
+
const scripts = getAllScripts();
|
|
418
|
+
|
|
419
|
+
if (scripts.length === 0) {
|
|
420
|
+
warn('No scripts found. Create one with: darksol script create');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
showSection('EXECUTION SCRIPTS');
|
|
425
|
+
|
|
426
|
+
const rows = scripts.map(s => [
|
|
427
|
+
theme.gold(s.name),
|
|
428
|
+
s.description || s.template,
|
|
429
|
+
s.wallet || theme.dim('(default)'),
|
|
430
|
+
s.chain,
|
|
431
|
+
s.runCount.toString(),
|
|
432
|
+
s.lastRun ? new Date(s.lastRun).toLocaleString() : theme.dim('never'),
|
|
433
|
+
]);
|
|
434
|
+
|
|
435
|
+
table(['Name', 'Type', 'Wallet', 'Chain', 'Runs', 'Last Run'], rows);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Run a script
|
|
439
|
+
export async function runScript(name, opts = {}) {
|
|
440
|
+
let script;
|
|
441
|
+
try {
|
|
442
|
+
script = loadScript(name);
|
|
443
|
+
} catch {
|
|
444
|
+
error(`Script "${name}" not found`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
showSection(`EXECUTING: ${name}`);
|
|
449
|
+
kvDisplay([
|
|
450
|
+
['Script', script.name],
|
|
451
|
+
['Type', script.template],
|
|
452
|
+
['Wallet', script.wallet],
|
|
453
|
+
['Chain', script.chain],
|
|
454
|
+
['Params', Object.entries(script.params).map(([k, v]) => `${k}=${v}`).join(', ') || '(none)'],
|
|
455
|
+
]);
|
|
456
|
+
console.log('');
|
|
457
|
+
|
|
458
|
+
// Get wallet password (unless --password provided for automation)
|
|
459
|
+
let password = opts.password;
|
|
460
|
+
if (!password) {
|
|
461
|
+
const { pw } = await inquirer.prompt([{
|
|
462
|
+
type: 'password',
|
|
463
|
+
name: 'pw',
|
|
464
|
+
message: theme.gold('Wallet password:'),
|
|
465
|
+
mask: '●',
|
|
466
|
+
}]);
|
|
467
|
+
password = pw;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Confirm unless --yes flag
|
|
471
|
+
if (!opts.yes) {
|
|
472
|
+
const { confirm } = await inquirer.prompt([{
|
|
473
|
+
type: 'confirm',
|
|
474
|
+
name: 'confirm',
|
|
475
|
+
message: theme.accent('Execute script? This will use your private key for transactions.'),
|
|
476
|
+
default: false,
|
|
477
|
+
}]);
|
|
478
|
+
if (!confirm) {
|
|
479
|
+
warn('Execution cancelled');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const spin = spinner('Unlocking wallet...').start();
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
// Unlock wallet
|
|
488
|
+
const walletData = loadWallet(script.wallet || getConfig('activeWallet'));
|
|
489
|
+
const privateKey = decryptKey(walletData.keystore, password);
|
|
490
|
+
const rpcUrl = getRPC(script.chain);
|
|
491
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
492
|
+
const signer = new ethers.Wallet(privateKey, provider);
|
|
493
|
+
|
|
494
|
+
spin.text = 'Running script...';
|
|
495
|
+
|
|
496
|
+
// Build execution context
|
|
497
|
+
const context = {
|
|
498
|
+
signer,
|
|
499
|
+
provider,
|
|
500
|
+
ethers,
|
|
501
|
+
config: {
|
|
502
|
+
chain: script.chain,
|
|
503
|
+
slippage: getConfig('slippage'),
|
|
504
|
+
gasMultiplier: getConfig('gasMultiplier'),
|
|
505
|
+
rpcs: getConfig('rpcs'),
|
|
506
|
+
},
|
|
507
|
+
params: script.params,
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// Execute the script
|
|
511
|
+
// We use Function constructor to run the script code in a sandboxed-ish context
|
|
512
|
+
// The script code uses module.exports pattern, so we wrap it
|
|
513
|
+
const wrappedCode = `
|
|
514
|
+
const module = { exports: null };
|
|
515
|
+
${script.code}
|
|
516
|
+
return module.exports;
|
|
517
|
+
`;
|
|
518
|
+
|
|
519
|
+
const scriptFn = new Function(wrappedCode)();
|
|
520
|
+
|
|
521
|
+
if (typeof scriptFn !== 'function') {
|
|
522
|
+
throw new Error('Script must export an async function via module.exports');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const startTime = Date.now();
|
|
526
|
+
const result = await scriptFn(context);
|
|
527
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
528
|
+
|
|
529
|
+
spin.succeed(theme.success('Script completed'));
|
|
530
|
+
|
|
531
|
+
// Show results
|
|
532
|
+
console.log('');
|
|
533
|
+
showSection('RESULTS');
|
|
534
|
+
if (result && typeof result === 'object') {
|
|
535
|
+
kvDisplay(Object.entries(result).map(([k, v]) =>
|
|
536
|
+
[k, typeof v === 'object' ? JSON.stringify(v) : String(v)]
|
|
537
|
+
));
|
|
538
|
+
}
|
|
539
|
+
console.log('');
|
|
540
|
+
info(`Execution time: ${elapsed}s`);
|
|
541
|
+
|
|
542
|
+
// Update script metadata
|
|
543
|
+
script.lastRun = new Date().toISOString();
|
|
544
|
+
script.runCount++;
|
|
545
|
+
saveScript(script);
|
|
546
|
+
|
|
547
|
+
} catch (err) {
|
|
548
|
+
spin.fail('Script failed');
|
|
549
|
+
error(err.message);
|
|
550
|
+
if (opts.verbose) {
|
|
551
|
+
console.log(theme.dim(err.stack));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Show script details
|
|
557
|
+
export async function showScript(name) {
|
|
558
|
+
let script;
|
|
559
|
+
try {
|
|
560
|
+
script = loadScript(name);
|
|
561
|
+
} catch {
|
|
562
|
+
error(`Script "${name}" not found`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
showSection(`SCRIPT: ${name}`);
|
|
567
|
+
kvDisplay([
|
|
568
|
+
['Name', script.name],
|
|
569
|
+
['Template', script.template],
|
|
570
|
+
['Description', script.description],
|
|
571
|
+
['Wallet', script.wallet],
|
|
572
|
+
['Chain', script.chain],
|
|
573
|
+
['Run Count', script.runCount.toString()],
|
|
574
|
+
['Last Run', script.lastRun || 'never'],
|
|
575
|
+
['Created', script.createdAt],
|
|
576
|
+
]);
|
|
577
|
+
|
|
578
|
+
if (Object.keys(script.params).length > 0) {
|
|
579
|
+
console.log('');
|
|
580
|
+
showSection('PARAMETERS');
|
|
581
|
+
kvDisplay(Object.entries(script.params).map(([k, v]) => [k, v]));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
console.log('');
|
|
585
|
+
showSection('CODE');
|
|
586
|
+
console.log(theme.dim(script.code));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Edit script params
|
|
590
|
+
export async function editScript(name) {
|
|
591
|
+
let script;
|
|
592
|
+
try {
|
|
593
|
+
script = loadScript(name);
|
|
594
|
+
} catch {
|
|
595
|
+
error(`Script "${name}" not found`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
showSection(`EDIT: ${name}`);
|
|
600
|
+
|
|
601
|
+
const { what } = await inquirer.prompt([{
|
|
602
|
+
type: 'list',
|
|
603
|
+
name: 'what',
|
|
604
|
+
message: theme.gold('What to edit:'),
|
|
605
|
+
choices: [
|
|
606
|
+
{ name: 'Parameters', value: 'params' },
|
|
607
|
+
{ name: 'Wallet', value: 'wallet' },
|
|
608
|
+
{ name: 'Chain', value: 'chain' },
|
|
609
|
+
{ name: 'Description', value: 'description' },
|
|
610
|
+
],
|
|
611
|
+
}]);
|
|
612
|
+
|
|
613
|
+
if (what === 'params') {
|
|
614
|
+
for (const [key, currentVal] of Object.entries(script.params)) {
|
|
615
|
+
const { value } = await inquirer.prompt([{
|
|
616
|
+
type: 'input',
|
|
617
|
+
name: 'value',
|
|
618
|
+
message: theme.gold(`${key}:`),
|
|
619
|
+
default: currentVal,
|
|
620
|
+
}]);
|
|
621
|
+
script.params[key] = value;
|
|
622
|
+
}
|
|
623
|
+
} else if (what === 'wallet') {
|
|
624
|
+
const wallets = listWallets();
|
|
625
|
+
const { wallet } = await inquirer.prompt([{
|
|
626
|
+
type: 'list',
|
|
627
|
+
name: 'wallet',
|
|
628
|
+
message: theme.gold('Wallet:'),
|
|
629
|
+
choices: wallets.map(w => w.name),
|
|
630
|
+
}]);
|
|
631
|
+
script.wallet = wallet;
|
|
632
|
+
} else if (what === 'chain') {
|
|
633
|
+
const { chain } = await inquirer.prompt([{
|
|
634
|
+
type: 'list',
|
|
635
|
+
name: 'chain',
|
|
636
|
+
message: theme.gold('Chain:'),
|
|
637
|
+
choices: ['base', 'ethereum', 'polygon', 'arbitrum', 'optimism'],
|
|
638
|
+
}]);
|
|
639
|
+
script.chain = chain;
|
|
640
|
+
} else if (what === 'description') {
|
|
641
|
+
const { desc } = await inquirer.prompt([{
|
|
642
|
+
type: 'input',
|
|
643
|
+
name: 'desc',
|
|
644
|
+
message: theme.gold('Description:'),
|
|
645
|
+
default: script.description,
|
|
646
|
+
}]);
|
|
647
|
+
script.description = desc;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
saveScript(script);
|
|
651
|
+
success('Script updated');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Delete a script
|
|
655
|
+
export async function deleteScript(name) {
|
|
656
|
+
const path = getScriptPath(name);
|
|
657
|
+
if (!existsSync(path)) {
|
|
658
|
+
error(`Script "${name}" not found`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const { confirm } = await inquirer.prompt([{
|
|
663
|
+
type: 'confirm',
|
|
664
|
+
name: 'confirm',
|
|
665
|
+
message: theme.accent(`Delete script "${name}"?`),
|
|
666
|
+
default: false,
|
|
667
|
+
}]);
|
|
668
|
+
|
|
669
|
+
if (!confirm) return;
|
|
670
|
+
|
|
671
|
+
unlinkSync(path);
|
|
672
|
+
success(`Script "${name}" deleted`);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Clone a script
|
|
676
|
+
export async function cloneScript(name, newName) {
|
|
677
|
+
let script;
|
|
678
|
+
try {
|
|
679
|
+
script = loadScript(name);
|
|
680
|
+
} catch {
|
|
681
|
+
error(`Script "${name}" not found`);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (!newName) {
|
|
686
|
+
const { n } = await inquirer.prompt([{
|
|
687
|
+
type: 'input',
|
|
688
|
+
name: 'n',
|
|
689
|
+
message: theme.gold('New name:'),
|
|
690
|
+
validate: v => /^[a-zA-Z0-9_-]+$/.test(v) || 'Alphanumeric, dashes, underscores only',
|
|
691
|
+
}]);
|
|
692
|
+
newName = n;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
script.name = newName;
|
|
696
|
+
script.createdAt = new Date().toISOString();
|
|
697
|
+
script.lastRun = null;
|
|
698
|
+
script.runCount = 0;
|
|
699
|
+
saveScript(script);
|
|
700
|
+
|
|
701
|
+
success(`Script cloned: ${name} → ${newName}`);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// List available templates
|
|
705
|
+
export function listTemplates() {
|
|
706
|
+
showSection('SCRIPT TEMPLATES');
|
|
707
|
+
|
|
708
|
+
const rows = Object.entries(TEMPLATES).map(([key, t]) => [
|
|
709
|
+
theme.gold(key),
|
|
710
|
+
t.name,
|
|
711
|
+
t.description,
|
|
712
|
+
t.params.join(', ') || '(none)',
|
|
713
|
+
]);
|
|
714
|
+
|
|
715
|
+
table(['Key', 'Name', 'Description', 'Parameters'], rows);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export { SCRIPTS_DIR, TEMPLATES };
|