@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,249 @@
|
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { getSigner } from '../wallet/manager.js';
|
|
6
|
+
import { getConfig } from '../config/store.js';
|
|
7
|
+
import { theme } from '../ui/theme.js';
|
|
8
|
+
import { spinner, kvDisplay, success, error, warn, table } from '../ui/components.js';
|
|
9
|
+
import { showSection } from '../ui/banner.js';
|
|
10
|
+
import { resolveToken, getTokenInfo } from './swap.js';
|
|
11
|
+
import inquirer from 'inquirer';
|
|
12
|
+
|
|
13
|
+
const DCA_DIR = join(homedir(), '.darksol', 'dca');
|
|
14
|
+
const DCA_FILE = join(DCA_DIR, 'orders.json');
|
|
15
|
+
|
|
16
|
+
function ensureDir() {
|
|
17
|
+
if (!existsSync(DCA_DIR)) mkdirSync(DCA_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadOrders() {
|
|
21
|
+
ensureDir();
|
|
22
|
+
if (!existsSync(DCA_FILE)) return [];
|
|
23
|
+
return JSON.parse(readFileSync(DCA_FILE, 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveOrders(orders) {
|
|
27
|
+
ensureDir();
|
|
28
|
+
writeFileSync(DCA_FILE, JSON.stringify(orders, null, 2));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create a new DCA order
|
|
32
|
+
export async function createDCA(opts = {}) {
|
|
33
|
+
const chain = getConfig('chain') || 'base';
|
|
34
|
+
|
|
35
|
+
showSection('CREATE DCA ORDER');
|
|
36
|
+
|
|
37
|
+
const answers = await inquirer.prompt([
|
|
38
|
+
{
|
|
39
|
+
type: 'input',
|
|
40
|
+
name: 'tokenIn',
|
|
41
|
+
message: theme.gold('Spend token (e.g. ETH, USDC):'),
|
|
42
|
+
default: 'ETH',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'input',
|
|
46
|
+
name: 'tokenOut',
|
|
47
|
+
message: theme.gold('Buy token (symbol or address):'),
|
|
48
|
+
validate: v => v.length > 0 || 'Required',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'input',
|
|
52
|
+
name: 'amountPerOrder',
|
|
53
|
+
message: theme.gold('Amount per order:'),
|
|
54
|
+
validate: v => parseFloat(v) > 0 || 'Must be positive',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'list',
|
|
58
|
+
name: 'interval',
|
|
59
|
+
message: theme.gold('Interval:'),
|
|
60
|
+
choices: [
|
|
61
|
+
{ name: 'Every 1 hour', value: 3600 },
|
|
62
|
+
{ name: 'Every 4 hours', value: 14400 },
|
|
63
|
+
{ name: 'Every 12 hours', value: 43200 },
|
|
64
|
+
{ name: 'Every 24 hours', value: 86400 },
|
|
65
|
+
{ name: 'Every 7 days', value: 604800 },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: 'input',
|
|
70
|
+
name: 'totalOrders',
|
|
71
|
+
message: theme.gold('Total number of orders:'),
|
|
72
|
+
default: '10',
|
|
73
|
+
validate: v => parseInt(v) > 0 || 'Must be positive',
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const tokenInAddr = resolveToken(answers.tokenIn, chain);
|
|
78
|
+
const tokenOutAddr = resolveToken(answers.tokenOut, chain);
|
|
79
|
+
|
|
80
|
+
if (!tokenInAddr) {
|
|
81
|
+
error(`Unknown token: ${answers.tokenIn}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!tokenOutAddr) {
|
|
85
|
+
error(`Unknown token: ${answers.tokenOut}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const order = {
|
|
90
|
+
id: `dca_${Date.now()}`,
|
|
91
|
+
chain,
|
|
92
|
+
tokenIn: answers.tokenIn.toUpperCase(),
|
|
93
|
+
tokenInAddress: tokenInAddr,
|
|
94
|
+
tokenOut: answers.tokenOut.toUpperCase(),
|
|
95
|
+
tokenOutAddress: tokenOutAddr,
|
|
96
|
+
amountPerOrder: answers.amountPerOrder,
|
|
97
|
+
interval: answers.interval,
|
|
98
|
+
totalOrders: parseInt(answers.totalOrders),
|
|
99
|
+
executedOrders: 0,
|
|
100
|
+
status: 'active',
|
|
101
|
+
createdAt: new Date().toISOString(),
|
|
102
|
+
nextExecution: new Date(Date.now() + answers.interval * 1000).toISOString(),
|
|
103
|
+
history: [],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const totalSpend = parseFloat(answers.amountPerOrder) * parseInt(answers.totalOrders);
|
|
107
|
+
|
|
108
|
+
console.log('');
|
|
109
|
+
kvDisplay([
|
|
110
|
+
['Order ID', order.id],
|
|
111
|
+
['Buy', `${answers.tokenOut} with ${answers.tokenIn}`],
|
|
112
|
+
['Per Order', `${answers.amountPerOrder} ${answers.tokenIn}`],
|
|
113
|
+
['Interval', formatInterval(answers.interval)],
|
|
114
|
+
['Total Orders', answers.totalOrders],
|
|
115
|
+
['Total Spend', `${totalSpend} ${answers.tokenIn}`],
|
|
116
|
+
['First Exec', order.nextExecution],
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const { confirm } = await inquirer.prompt([{
|
|
120
|
+
type: 'confirm',
|
|
121
|
+
name: 'confirm',
|
|
122
|
+
message: theme.gold('Create DCA order?'),
|
|
123
|
+
default: true,
|
|
124
|
+
}]);
|
|
125
|
+
|
|
126
|
+
if (!confirm) {
|
|
127
|
+
warn('DCA order cancelled');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const orders = loadOrders();
|
|
132
|
+
orders.push(order);
|
|
133
|
+
saveOrders(orders);
|
|
134
|
+
|
|
135
|
+
success(`DCA order created: ${order.id}`);
|
|
136
|
+
console.log(theme.dim(' Run the DCA executor to start: darksol dca run'));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// List DCA orders
|
|
140
|
+
export function listDCA() {
|
|
141
|
+
const orders = loadOrders();
|
|
142
|
+
|
|
143
|
+
if (orders.length === 0) {
|
|
144
|
+
warn('No DCA orders. Create one with: darksol dca create');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
showSection('DCA ORDERS');
|
|
149
|
+
|
|
150
|
+
const rows = orders.map(o => [
|
|
151
|
+
o.id.slice(4, 17),
|
|
152
|
+
`${o.tokenIn} → ${o.tokenOut}`,
|
|
153
|
+
o.amountPerOrder,
|
|
154
|
+
formatInterval(o.interval),
|
|
155
|
+
`${o.executedOrders}/${o.totalOrders}`,
|
|
156
|
+
o.status === 'active' ? theme.success('Active') : theme.dim(o.status),
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
table(['ID', 'Pair', 'Amount', 'Interval', 'Progress', 'Status'], rows);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Cancel a DCA order
|
|
163
|
+
export async function cancelDCA(orderId) {
|
|
164
|
+
const orders = loadOrders();
|
|
165
|
+
const idx = orders.findIndex(o => o.id === orderId || o.id.includes(orderId));
|
|
166
|
+
|
|
167
|
+
if (idx === -1) {
|
|
168
|
+
error(`Order not found: ${orderId}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
orders[idx].status = 'cancelled';
|
|
173
|
+
saveOrders(orders);
|
|
174
|
+
success(`DCA order cancelled: ${orders[idx].id}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Execute pending DCA orders
|
|
178
|
+
export async function runDCA(opts = {}) {
|
|
179
|
+
const orders = loadOrders();
|
|
180
|
+
const active = orders.filter(o =>
|
|
181
|
+
o.status === 'active' &&
|
|
182
|
+
o.executedOrders < o.totalOrders &&
|
|
183
|
+
new Date(o.nextExecution) <= new Date()
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (active.length === 0) {
|
|
187
|
+
console.log(theme.dim(' No DCA orders ready for execution'));
|
|
188
|
+
const nextOrder = orders
|
|
189
|
+
.filter(o => o.status === 'active')
|
|
190
|
+
.sort((a, b) => new Date(a.nextExecution) - new Date(b.nextExecution))[0];
|
|
191
|
+
|
|
192
|
+
if (nextOrder) {
|
|
193
|
+
console.log(theme.dim(` Next execution: ${nextOrder.nextExecution}`));
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
showSection(`DCA EXECUTION — ${active.length} order(s) ready`);
|
|
199
|
+
|
|
200
|
+
if (!opts.password) {
|
|
201
|
+
const { password } = await inquirer.prompt([{
|
|
202
|
+
type: 'password',
|
|
203
|
+
name: 'password',
|
|
204
|
+
message: theme.gold('Wallet password (for all orders):'),
|
|
205
|
+
mask: '●',
|
|
206
|
+
}]);
|
|
207
|
+
opts.password = password;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const order of active) {
|
|
211
|
+
const spin = spinner(`Executing DCA: ${order.tokenIn} → ${order.tokenOut}`).start();
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
// TODO: integrate with swap execution
|
|
215
|
+
// For now, mark as executed and log
|
|
216
|
+
order.executedOrders++;
|
|
217
|
+
order.history.push({
|
|
218
|
+
timestamp: new Date().toISOString(),
|
|
219
|
+
amount: order.amountPerOrder,
|
|
220
|
+
status: 'simulated', // Change to 'executed' when wired
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (order.executedOrders >= order.totalOrders) {
|
|
224
|
+
order.status = 'completed';
|
|
225
|
+
} else {
|
|
226
|
+
order.nextExecution = new Date(Date.now() + order.interval * 1000).toISOString();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
spin.succeed(`DCA ${order.executedOrders}/${order.totalOrders}: ${order.amountPerOrder} ${order.tokenIn} → ${order.tokenOut}`);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
spin.fail(`DCA failed: ${err.message}`);
|
|
232
|
+
order.history.push({
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
234
|
+
amount: order.amountPerOrder,
|
|
235
|
+
status: 'failed',
|
|
236
|
+
error: err.message,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
saveOrders(orders);
|
|
242
|
+
success('DCA execution complete');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function formatInterval(seconds) {
|
|
246
|
+
if (seconds < 3600) return `${seconds / 60}m`;
|
|
247
|
+
if (seconds < 86400) return `${seconds / 3600}h`;
|
|
248
|
+
return `${seconds / 86400}d`;
|
|
249
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
|
+
import { getSigner } from '../wallet/manager.js';
|
|
3
|
+
import { getConfig, getRPC } from '../config/store.js';
|
|
4
|
+
import { theme } from '../ui/theme.js';
|
|
5
|
+
import { spinner, kvDisplay, success, error, warn } from '../ui/components.js';
|
|
6
|
+
import { showSection } from '../ui/banner.js';
|
|
7
|
+
import { resolveToken, getTokenInfo } from './swap.js';
|
|
8
|
+
import inquirer from 'inquirer';
|
|
9
|
+
|
|
10
|
+
// Uniswap V2 Factory ABI (for pair creation events)
|
|
11
|
+
const FACTORY_ABI = [
|
|
12
|
+
'event PairCreated(address indexed token0, address indexed token1, address pair, uint)',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// Uniswap V2 Router ABI
|
|
16
|
+
const ROUTER_V2_ABI = [
|
|
17
|
+
'function swapExactETHForTokens(uint amountOutMin, address[] path, address to, uint deadline) payable returns (uint[] amounts)',
|
|
18
|
+
'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] path, address to, uint deadline) returns (uint[] amounts)',
|
|
19
|
+
'function getAmountsOut(uint amountIn, address[] path) view returns (uint[] amounts)',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const V2_ROUTERS = {
|
|
23
|
+
base: '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24', // Uniswap V2 on Base
|
|
24
|
+
ethereum: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const WETH = {
|
|
28
|
+
base: '0x4200000000000000000000000000000000000006',
|
|
29
|
+
ethereum: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Snipe a token — buy immediately with ETH
|
|
33
|
+
export async function snipeToken(tokenAddress, amount, opts = {}) {
|
|
34
|
+
const chain = getConfig('chain') || 'base';
|
|
35
|
+
const maxSlippage = opts.slippage || getConfig('slippage') || 1.0;
|
|
36
|
+
const gasMultiplier = opts.gas || getConfig('gasMultiplier') || 1.5;
|
|
37
|
+
|
|
38
|
+
if (!tokenAddress || !tokenAddress.startsWith('0x')) {
|
|
39
|
+
error('Provide a valid token contract address');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!amount || parseFloat(amount) <= 0) {
|
|
44
|
+
error('Provide an ETH amount to spend');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get password
|
|
49
|
+
const { password } = await inquirer.prompt([{
|
|
50
|
+
type: 'password',
|
|
51
|
+
name: 'password',
|
|
52
|
+
message: theme.gold('Wallet password:'),
|
|
53
|
+
mask: '●',
|
|
54
|
+
}]);
|
|
55
|
+
|
|
56
|
+
const spin = spinner('Preparing snipe...').start();
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const { signer, provider, address } = await getSigner(opts.wallet, password);
|
|
60
|
+
|
|
61
|
+
const routerAddr = V2_ROUTERS[chain];
|
|
62
|
+
if (!routerAddr) {
|
|
63
|
+
spin.fail('No V2 router for this chain');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const wethAddr = WETH[chain];
|
|
68
|
+
const router = new ethers.Contract(routerAddr, ROUTER_V2_ABI, signer);
|
|
69
|
+
const amountIn = ethers.parseEther(amount.toString());
|
|
70
|
+
|
|
71
|
+
// Check ETH balance
|
|
72
|
+
const balance = await provider.getBalance(address);
|
|
73
|
+
if (balance < amountIn) {
|
|
74
|
+
spin.fail('Insufficient ETH');
|
|
75
|
+
error(`Need ${amount} ETH, have ${ethers.formatEther(balance)}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get token info
|
|
80
|
+
let tokenInfo;
|
|
81
|
+
try {
|
|
82
|
+
tokenInfo = await getTokenInfo(tokenAddress, provider);
|
|
83
|
+
} catch {
|
|
84
|
+
tokenInfo = { symbol: 'UNKNOWN', name: 'Unknown Token', decimals: 18 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Get estimated output
|
|
88
|
+
let estimatedOut;
|
|
89
|
+
try {
|
|
90
|
+
const amounts = await router.getAmountsOut(amountIn, [wethAddr, tokenAddress]);
|
|
91
|
+
estimatedOut = amounts[1];
|
|
92
|
+
} catch {
|
|
93
|
+
estimatedOut = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
spin.succeed('Snipe ready');
|
|
97
|
+
|
|
98
|
+
showSection('SNIPE PREVIEW');
|
|
99
|
+
kvDisplay([
|
|
100
|
+
['Token', `${tokenInfo.symbol} (${tokenInfo.name})`],
|
|
101
|
+
['Contract', tokenAddress],
|
|
102
|
+
['Spend', `${amount} ETH`],
|
|
103
|
+
['Est. Output', estimatedOut ? ethers.formatUnits(estimatedOut, tokenInfo.decimals) + ' ' + tokenInfo.symbol : 'Unable to estimate'],
|
|
104
|
+
['Slippage', `${maxSlippage}%`],
|
|
105
|
+
['Gas Boost', `${gasMultiplier}x`],
|
|
106
|
+
['Chain', chain],
|
|
107
|
+
]);
|
|
108
|
+
console.log('');
|
|
109
|
+
|
|
110
|
+
const { confirm } = await inquirer.prompt([{
|
|
111
|
+
type: 'confirm',
|
|
112
|
+
name: 'confirm',
|
|
113
|
+
message: theme.accent('Execute snipe? This is HIGH RISK.'),
|
|
114
|
+
default: false,
|
|
115
|
+
}]);
|
|
116
|
+
|
|
117
|
+
if (!confirm) {
|
|
118
|
+
warn('Snipe cancelled');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const snipeSpin = spinner('Executing snipe...').start();
|
|
123
|
+
|
|
124
|
+
const deadline = Math.floor(Date.now() / 1000) + 120; // 2 min tight deadline
|
|
125
|
+
const minOut = estimatedOut
|
|
126
|
+
? (estimatedOut * BigInt(Math.floor((100 - maxSlippage) * 100))) / 10000n
|
|
127
|
+
: 0n;
|
|
128
|
+
|
|
129
|
+
// Boost gas for priority
|
|
130
|
+
const feeData = await provider.getFeeData();
|
|
131
|
+
const maxFeePerGas = feeData.maxFeePerGas
|
|
132
|
+
? (feeData.maxFeePerGas * BigInt(Math.floor(gasMultiplier * 100))) / 100n
|
|
133
|
+
: undefined;
|
|
134
|
+
const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas
|
|
135
|
+
? (feeData.maxPriorityFeePerGas * BigInt(Math.floor(gasMultiplier * 100))) / 100n
|
|
136
|
+
: undefined;
|
|
137
|
+
|
|
138
|
+
const tx = await router.swapExactETHForTokens(
|
|
139
|
+
minOut,
|
|
140
|
+
[wethAddr, tokenAddress],
|
|
141
|
+
address,
|
|
142
|
+
deadline,
|
|
143
|
+
{
|
|
144
|
+
value: amountIn,
|
|
145
|
+
maxFeePerGas,
|
|
146
|
+
maxPriorityFeePerGas,
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
snipeSpin.text = 'Waiting for confirmation...';
|
|
151
|
+
const receipt = await tx.wait();
|
|
152
|
+
|
|
153
|
+
snipeSpin.succeed(theme.success('Snipe executed!'));
|
|
154
|
+
|
|
155
|
+
console.log('');
|
|
156
|
+
showSection('SNIPE RESULT');
|
|
157
|
+
kvDisplay([
|
|
158
|
+
['TX Hash', receipt.hash],
|
|
159
|
+
['Block', receipt.blockNumber.toString()],
|
|
160
|
+
['Gas Used', receipt.gasUsed.toString()],
|
|
161
|
+
['Status', receipt.status === 1 ? theme.success('✓ Success') : theme.error('✗ Failed')],
|
|
162
|
+
]);
|
|
163
|
+
console.log('');
|
|
164
|
+
warn('Check your token balance with: darksol wallet balance');
|
|
165
|
+
|
|
166
|
+
} catch (err) {
|
|
167
|
+
spin.fail('Snipe failed');
|
|
168
|
+
error(err.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Watch for new pairs and auto-snipe (monitor mode)
|
|
173
|
+
export async function watchSnipe(opts = {}) {
|
|
174
|
+
const chain = getConfig('chain') || 'base';
|
|
175
|
+
const rpc = getRPC(chain);
|
|
176
|
+
|
|
177
|
+
showSection('SNIPE WATCHER');
|
|
178
|
+
console.log(theme.accent(' ⚡ Monitoring for new token pairs...'));
|
|
179
|
+
console.log(theme.dim(` Chain: ${chain} | RPC: ${rpc}`));
|
|
180
|
+
console.log(theme.dim(' Press Ctrl+C to stop'));
|
|
181
|
+
console.log('');
|
|
182
|
+
|
|
183
|
+
const provider = new ethers.WebSocketProvider(
|
|
184
|
+
rpc.replace('https://', 'wss://').replace('http://', 'ws://')
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Listen for pending transactions to the factory
|
|
188
|
+
// This is a simplified version — production would use mempool monitoring
|
|
189
|
+
console.log(theme.warning(' ⚠ Watch mode is experimental. Use at your own risk.'));
|
|
190
|
+
console.log(theme.dim(' Auto-snipe requires --auto flag and pre-set amount'));
|
|
191
|
+
|
|
192
|
+
provider.on('block', (blockNumber) => {
|
|
193
|
+
process.stdout.write(theme.dim(` Block: ${blockNumber}\r`));
|
|
194
|
+
});
|
|
195
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
|
+
import { getSigner } from '../wallet/manager.js';
|
|
3
|
+
import { getConfig, getRPC } from '../config/store.js';
|
|
4
|
+
import { theme } from '../ui/theme.js';
|
|
5
|
+
import { spinner, kvDisplay, success, error, warn, formatAddress } from '../ui/components.js';
|
|
6
|
+
import { showSection } from '../ui/banner.js';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
|
|
9
|
+
// Known DEX router addresses
|
|
10
|
+
const ROUTERS = {
|
|
11
|
+
base: {
|
|
12
|
+
uniswapV3: '0x2626664c2603336E57B271c5C0b26F421741e481',
|
|
13
|
+
aerodrome: '0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43',
|
|
14
|
+
},
|
|
15
|
+
ethereum: {
|
|
16
|
+
uniswapV3: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
|
|
17
|
+
},
|
|
18
|
+
arbitrum: {
|
|
19
|
+
uniswapV3: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Common token addresses per chain
|
|
24
|
+
const TOKENS = {
|
|
25
|
+
base: {
|
|
26
|
+
ETH: ethers.ZeroAddress,
|
|
27
|
+
WETH: '0x4200000000000000000000000000000000000006',
|
|
28
|
+
USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
29
|
+
USDbC: '0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA',
|
|
30
|
+
DAI: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb',
|
|
31
|
+
AERO: '0x940181a94A35A4569E4529A3CDfB74e38FD98631',
|
|
32
|
+
VIRTUAL: '0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b',
|
|
33
|
+
},
|
|
34
|
+
ethereum: {
|
|
35
|
+
ETH: ethers.ZeroAddress,
|
|
36
|
+
WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
|
|
37
|
+
USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
38
|
+
USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
|
39
|
+
DAI: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ERC20 ABI for approvals and balance checks
|
|
44
|
+
const ERC20_ABI = [
|
|
45
|
+
'function approve(address spender, uint256 amount) returns (bool)',
|
|
46
|
+
'function allowance(address owner, address spender) view returns (uint256)',
|
|
47
|
+
'function balanceOf(address) view returns (uint256)',
|
|
48
|
+
'function decimals() view returns (uint8)',
|
|
49
|
+
'function symbol() view returns (string)',
|
|
50
|
+
'function name() view returns (string)',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Uniswap V3 SwapRouter ABI (exactInputSingle)
|
|
54
|
+
const SWAP_ROUTER_ABI = [
|
|
55
|
+
'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',
|
|
56
|
+
'function multicall(uint256 deadline, bytes[] data) external payable returns (bytes[])',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Resolve token symbol to address
|
|
60
|
+
export function resolveToken(symbol, chain) {
|
|
61
|
+
const upper = symbol.toUpperCase();
|
|
62
|
+
const chainTokens = TOKENS[chain] || TOKENS.base;
|
|
63
|
+
if (chainTokens[upper]) return chainTokens[upper];
|
|
64
|
+
// If it looks like an address, use it directly
|
|
65
|
+
if (symbol.startsWith('0x') && symbol.length === 42) return symbol;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get token info
|
|
70
|
+
export async function getTokenInfo(address, provider) {
|
|
71
|
+
if (address === ethers.ZeroAddress) {
|
|
72
|
+
return { symbol: 'ETH', name: 'Ether', decimals: 18, address };
|
|
73
|
+
}
|
|
74
|
+
const contract = new ethers.Contract(address, ERC20_ABI, provider);
|
|
75
|
+
const [symbol, name, decimals] = await Promise.all([
|
|
76
|
+
contract.symbol(),
|
|
77
|
+
contract.name(),
|
|
78
|
+
contract.decimals(),
|
|
79
|
+
]);
|
|
80
|
+
return { symbol, name, decimals: Number(decimals), address };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Execute a swap via Uniswap V3
|
|
84
|
+
export async function executeSwap(opts = {}) {
|
|
85
|
+
const {
|
|
86
|
+
tokenIn: tokenInSymbol,
|
|
87
|
+
tokenOut: tokenOutSymbol,
|
|
88
|
+
amount,
|
|
89
|
+
wallet: walletName,
|
|
90
|
+
slippage,
|
|
91
|
+
} = opts;
|
|
92
|
+
|
|
93
|
+
const chain = getConfig('chain') || 'base';
|
|
94
|
+
const maxSlippage = slippage || getConfig('slippage') || 0.5;
|
|
95
|
+
|
|
96
|
+
// Resolve tokens
|
|
97
|
+
const tokenInAddr = resolveToken(tokenInSymbol, chain);
|
|
98
|
+
const tokenOutAddr = resolveToken(tokenOutSymbol, chain);
|
|
99
|
+
|
|
100
|
+
if (!tokenInAddr) {
|
|
101
|
+
error(`Unknown token: ${tokenInSymbol}. Use symbol (ETH, USDC) or contract address.`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!tokenOutAddr) {
|
|
105
|
+
error(`Unknown token: ${tokenOutSymbol}. Use symbol (ETH, USDC) or contract address.`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Get password for wallet
|
|
110
|
+
const { password } = await inquirer.prompt([{
|
|
111
|
+
type: 'password',
|
|
112
|
+
name: 'password',
|
|
113
|
+
message: theme.gold('Wallet password:'),
|
|
114
|
+
mask: '●',
|
|
115
|
+
}]);
|
|
116
|
+
|
|
117
|
+
const spin = spinner('Preparing swap...').start();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const { signer, provider, address } = await getSigner(walletName, password);
|
|
121
|
+
const router = ROUTERS[chain]?.uniswapV3;
|
|
122
|
+
if (!router) {
|
|
123
|
+
spin.fail('No router available');
|
|
124
|
+
error(`No DEX router configured for ${chain}`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Get token info
|
|
129
|
+
const isNativeIn = tokenInAddr === ethers.ZeroAddress;
|
|
130
|
+
const actualTokenIn = isNativeIn ? TOKENS[chain]?.WETH : tokenInAddr;
|
|
131
|
+
const tokenOutInfo = await getTokenInfo(tokenOutAddr === ethers.ZeroAddress ? TOKENS[chain]?.WETH : tokenOutAddr, provider);
|
|
132
|
+
const tokenInInfo = await getTokenInfo(actualTokenIn, provider);
|
|
133
|
+
|
|
134
|
+
const amountIn = ethers.parseUnits(amount.toString(), isNativeIn ? 18 : tokenInInfo.decimals);
|
|
135
|
+
|
|
136
|
+
// Check balance
|
|
137
|
+
if (isNativeIn) {
|
|
138
|
+
const balance = await provider.getBalance(address);
|
|
139
|
+
if (balance < amountIn) {
|
|
140
|
+
spin.fail('Insufficient balance');
|
|
141
|
+
error(`Need ${amount} ETH, have ${ethers.formatEther(balance)}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
const token = new ethers.Contract(actualTokenIn, ERC20_ABI, signer);
|
|
146
|
+
const balance = await token.balanceOf(address);
|
|
147
|
+
if (balance < amountIn) {
|
|
148
|
+
spin.fail('Insufficient balance');
|
|
149
|
+
error(`Need ${amount} ${tokenInInfo.symbol}, have ${ethers.formatUnits(balance, tokenInInfo.decimals)}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
spin.text = 'Swap details ready';
|
|
155
|
+
spin.succeed();
|
|
156
|
+
|
|
157
|
+
// Show swap details
|
|
158
|
+
showSection('SWAP PREVIEW');
|
|
159
|
+
kvDisplay([
|
|
160
|
+
['From', `${amount} ${isNativeIn ? 'ETH' : tokenInInfo.symbol}`],
|
|
161
|
+
['To', tokenOutInfo.symbol],
|
|
162
|
+
['Router', formatAddress(router)],
|
|
163
|
+
['Chain', chain],
|
|
164
|
+
['Slippage', `${maxSlippage}%`],
|
|
165
|
+
]);
|
|
166
|
+
console.log('');
|
|
167
|
+
|
|
168
|
+
const { confirm } = await inquirer.prompt([{
|
|
169
|
+
type: 'confirm',
|
|
170
|
+
name: 'confirm',
|
|
171
|
+
message: theme.gold('Execute swap?'),
|
|
172
|
+
default: false,
|
|
173
|
+
}]);
|
|
174
|
+
|
|
175
|
+
if (!confirm) {
|
|
176
|
+
warn('Swap cancelled');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const swapSpin = spinner('Executing swap...').start();
|
|
181
|
+
|
|
182
|
+
// Approve if needed (non-native)
|
|
183
|
+
if (!isNativeIn) {
|
|
184
|
+
const token = new ethers.Contract(actualTokenIn, ERC20_ABI, signer);
|
|
185
|
+
const allowance = await token.allowance(address, router);
|
|
186
|
+
if (allowance < amountIn) {
|
|
187
|
+
swapSpin.text = 'Approving token...';
|
|
188
|
+
const approveTx = await token.approve(router, ethers.MaxUint256);
|
|
189
|
+
await approveTx.wait();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Execute swap
|
|
194
|
+
swapSpin.text = 'Sending swap transaction...';
|
|
195
|
+
const swapRouter = new ethers.Contract(router, SWAP_ROUTER_ABI, signer);
|
|
196
|
+
|
|
197
|
+
const deadline = Math.floor(Date.now() / 1000) + 300; // 5 min
|
|
198
|
+
const amountOutMin = 0; // TODO: get quote for proper slippage protection
|
|
199
|
+
|
|
200
|
+
const swapParams = {
|
|
201
|
+
tokenIn: actualTokenIn,
|
|
202
|
+
tokenOut: tokenOutAddr === ethers.ZeroAddress ? TOKENS[chain]?.WETH : tokenOutAddr,
|
|
203
|
+
fee: 3000, // 0.3% fee tier
|
|
204
|
+
recipient: address,
|
|
205
|
+
deadline,
|
|
206
|
+
amountIn,
|
|
207
|
+
amountOutMinimum: amountOutMin,
|
|
208
|
+
sqrtPriceLimitX96: 0,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const txOpts = isNativeIn ? { value: amountIn } : {};
|
|
212
|
+
const tx = await swapRouter.exactInputSingle(swapParams, txOpts);
|
|
213
|
+
|
|
214
|
+
swapSpin.text = 'Waiting for confirmation...';
|
|
215
|
+
const receipt = await tx.wait();
|
|
216
|
+
|
|
217
|
+
swapSpin.succeed(theme.success('Swap executed'));
|
|
218
|
+
|
|
219
|
+
console.log('');
|
|
220
|
+
showSection('SWAP RESULT');
|
|
221
|
+
kvDisplay([
|
|
222
|
+
['TX Hash', receipt.hash],
|
|
223
|
+
['Block', receipt.blockNumber.toString()],
|
|
224
|
+
['Gas Used', receipt.gasUsed.toString()],
|
|
225
|
+
['Status', receipt.status === 1 ? theme.success('Success') : theme.error('Failed')],
|
|
226
|
+
]);
|
|
227
|
+
console.log('');
|
|
228
|
+
|
|
229
|
+
} catch (err) {
|
|
230
|
+
spin.fail('Swap failed');
|
|
231
|
+
error(err.message);
|
|
232
|
+
}
|
|
233
|
+
}
|