clawncher 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 +494 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2640 -0
- package/dist/cli.js.map +1 -0
- package/dist/wallet.d.ts +144 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +500 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +52 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2640 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Clawncher CLI
|
|
4
|
+
*
|
|
5
|
+
* Command-line interface for deploying tokens on Base with Uniswap V4 pools.
|
|
6
|
+
* Uses ClawnchDeployer from SDK (Clanker-backed infrastructure).
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import Table from 'cli-table3';
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { homedir } from 'os';
|
|
15
|
+
import { createWalletClient, createPublicClient, http, formatEther, parseEther, } from 'viem';
|
|
16
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
17
|
+
import { base, baseSepolia } from 'viem/chains';
|
|
18
|
+
import { ClawnchDeployer, ClawnchReader, ClawnchClient, ClawncherClaimer, ClawnchPortfolio, ClawnchWatcher, ClawnchSwapper, ClawnchLiquidity, ClawnchApiDeployer, getAddresses, NATIVE_TOKEN_ADDRESS, ClawnchFeeLockerABI, } from '@clawnch/clawncher-sdk';
|
|
19
|
+
const VERSION = '0.1.0';
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// UI Toolkit
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Brand colors - orange/red/yellow fire palette
|
|
24
|
+
const c = {
|
|
25
|
+
accent: chalk.hex('#FF6B2B'), // bright orange
|
|
26
|
+
accent2: chalk.hex('#FF8C42'), // warm amber
|
|
27
|
+
muted: chalk.hex('#8B6B5A'), // muted brown
|
|
28
|
+
dim: chalk.hex('#5C4033'), // dark brown
|
|
29
|
+
label: chalk.hex('#CC9966'), // tan/gold for labels
|
|
30
|
+
value: chalk.white, // white for values
|
|
31
|
+
success: chalk.hex('#FFB347'), // warm gold
|
|
32
|
+
warn: chalk.hex('#FFCC00'), // yellow
|
|
33
|
+
error: chalk.hex('#FF3344'), // bright red
|
|
34
|
+
link: chalk.hex('#FFA066'), // peach for links
|
|
35
|
+
highlight: chalk.hex('#FF6B2B').bold, // bold orange
|
|
36
|
+
};
|
|
37
|
+
// Gradient for the big banner - row by row color shift (red -> orange -> yellow)
|
|
38
|
+
function bannerGradient(lines) {
|
|
39
|
+
const rowColors = ['#FF2200', '#FF4400', '#FF6B2B', '#FF8C42', '#FFAA33', '#FFCC00'];
|
|
40
|
+
return lines.map((line, i) => {
|
|
41
|
+
const color = rowColors[Math.min(i, rowColors.length - 1)];
|
|
42
|
+
return chalk.hex(color).bold(line);
|
|
43
|
+
}).join('\n');
|
|
44
|
+
}
|
|
45
|
+
// Section header with line
|
|
46
|
+
function sectionHeader(title) {
|
|
47
|
+
const line = c.dim('\u2500'.repeat(Math.max(0, 50 - title.length)));
|
|
48
|
+
return ` ${c.accent2.bold(title)} ${line}`;
|
|
49
|
+
}
|
|
50
|
+
// Key-value pair with aligned labels
|
|
51
|
+
function kv(label, value, labelWidth = 16) {
|
|
52
|
+
const padded = label.padEnd(labelWidth);
|
|
53
|
+
return ` ${c.label(padded)} ${c.value(value)}`;
|
|
54
|
+
}
|
|
55
|
+
// Dim key-value (for less important info)
|
|
56
|
+
function kvDim(label, value, labelWidth = 16) {
|
|
57
|
+
const padded = label.padEnd(labelWidth);
|
|
58
|
+
return ` ${c.muted(padded)} ${c.muted(value)}`;
|
|
59
|
+
}
|
|
60
|
+
// Status indicator dot
|
|
61
|
+
function dot(color) {
|
|
62
|
+
const colors = { green: '#FFB347', yellow: '#FFCC00', red: '#FF3344' };
|
|
63
|
+
return chalk.hex(colors[color])('\u25CF');
|
|
64
|
+
}
|
|
65
|
+
// Network badge
|
|
66
|
+
function networkBadge(network) {
|
|
67
|
+
if (network === 'mainnet') {
|
|
68
|
+
return chalk.bgHex('#FF6B2B').hex('#000000').bold(` ${network.toUpperCase()} `);
|
|
69
|
+
}
|
|
70
|
+
return chalk.bgHex('#FFCC00').hex('#000000').bold(` ${network.toUpperCase()} `);
|
|
71
|
+
}
|
|
72
|
+
// Progress bar
|
|
73
|
+
function progressBar(percent, width = 20) {
|
|
74
|
+
const filled = Math.round(width * Math.min(percent, 100) / 100);
|
|
75
|
+
const empty = width - filled;
|
|
76
|
+
const bar = c.accent('\u2588'.repeat(filled)) + c.dim('\u2591'.repeat(empty));
|
|
77
|
+
return `${bar} ${c.value(percent.toFixed(1) + '%')}`;
|
|
78
|
+
}
|
|
79
|
+
// Address formatting - highlight 0x prefix and ccc vanity
|
|
80
|
+
function fmtAddr(address, truncate = false) {
|
|
81
|
+
if (truncate) {
|
|
82
|
+
return c.muted('0x') + c.accent(address.slice(2, 6)) + c.dim('...') + c.value(address.slice(-4));
|
|
83
|
+
}
|
|
84
|
+
// Highlight vanity prefix if starts with 0xccc
|
|
85
|
+
if (address.toLowerCase().startsWith('0xccc')) {
|
|
86
|
+
return c.muted('0x') + c.accent(address.slice(2, 5)) + c.value(address.slice(5));
|
|
87
|
+
}
|
|
88
|
+
return c.muted('0x') + c.value(address.slice(2));
|
|
89
|
+
}
|
|
90
|
+
// Styled table with our colors
|
|
91
|
+
function styledTable(heads, rows) {
|
|
92
|
+
const table = new Table({
|
|
93
|
+
head: heads.map(h => c.accent2.bold(h)),
|
|
94
|
+
chars: {
|
|
95
|
+
'top': '\u2500', 'top-mid': '\u252C', 'top-left': '\u250C', 'top-right': '\u2510',
|
|
96
|
+
'bottom': '\u2500', 'bottom-mid': '\u2534', 'bottom-left': '\u2514', 'bottom-right': '\u2518',
|
|
97
|
+
'left': '\u2502', 'left-mid': '\u251C', 'mid': '\u2500', 'mid-mid': '\u253C',
|
|
98
|
+
'right': '\u2502', 'right-mid': '\u2524', 'middle': '\u2502',
|
|
99
|
+
},
|
|
100
|
+
style: {
|
|
101
|
+
head: [],
|
|
102
|
+
border: [],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
rows.forEach(row => table.push(row.map(cell => String(cell))));
|
|
106
|
+
// Dim the border characters
|
|
107
|
+
return table.toString().replace(/[\u2500\u252C\u250C\u2510\u2534\u2514\u2518\u2502\u251C\u253C\u2524]/g, (ch) => c.dim(ch));
|
|
108
|
+
}
|
|
109
|
+
const CONFIG_DIR = join(homedir(), '.clawncher');
|
|
110
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
111
|
+
function loadConfig() {
|
|
112
|
+
try {
|
|
113
|
+
if (existsSync(CONFIG_FILE)) {
|
|
114
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Ignore errors, return empty config
|
|
119
|
+
}
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
function saveConfig(config) {
|
|
123
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
124
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
125
|
+
}
|
|
126
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
127
|
+
}
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Helpers
|
|
130
|
+
// ============================================================================
|
|
131
|
+
function isValidAddress(address) {
|
|
132
|
+
return /^0x[0-9a-fA-F]{40}$/.test(address);
|
|
133
|
+
}
|
|
134
|
+
function validateAddress(address, label) {
|
|
135
|
+
if (!isValidAddress(address)) {
|
|
136
|
+
console.error(c.error(` \u2717 Invalid ${label} address: ${address}`));
|
|
137
|
+
console.error(c.muted(' Expected a 42-character hex string starting with 0x'));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
return address;
|
|
141
|
+
}
|
|
142
|
+
function safeParseInt(value, label) {
|
|
143
|
+
const n = parseInt(value, 10);
|
|
144
|
+
if (isNaN(n)) {
|
|
145
|
+
console.error(c.error(`Invalid ${label}: "${value}" is not a number`));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
return n;
|
|
149
|
+
}
|
|
150
|
+
function handleError(err) {
|
|
151
|
+
if (err && typeof err === 'object' && 'message' in err) {
|
|
152
|
+
const error = err;
|
|
153
|
+
console.error(`\n ${c.error('\u2717')} ${c.error(error.message)}`);
|
|
154
|
+
if (error.errors?.length) {
|
|
155
|
+
error.errors.forEach((e) => console.error(c.muted(` \u2500 ${e}`)));
|
|
156
|
+
}
|
|
157
|
+
console.error();
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.error(`\n ${c.error('\u2717')} ${c.error('An unknown error occurred')}\n`);
|
|
161
|
+
}
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
function getPrivateKey(opts) {
|
|
165
|
+
// Priority: --private-key flag > env var > config file
|
|
166
|
+
const key = opts.privateKey || process.env.CLAWNCHER_PRIVATE_KEY || loadConfig().privateKey;
|
|
167
|
+
if (!key) {
|
|
168
|
+
console.error(c.error('Private key required. Use --private-key, CLAWNCHER_PRIVATE_KEY env var, `clawncher config --private-key`, or `clawncher wallet use <name>`'));
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
return key.startsWith('0x') ? key : `0x${key}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Get private key with wallet support. If no key is provided via flag/env/config,
|
|
175
|
+
* tries to decrypt the active wallet (prompts for password).
|
|
176
|
+
*/
|
|
177
|
+
async function getPrivateKeyOrWallet(opts) {
|
|
178
|
+
// First try the non-interactive sources
|
|
179
|
+
const key = opts.privateKey || process.env.CLAWNCHER_PRIVATE_KEY || loadConfig().privateKey;
|
|
180
|
+
if (key) {
|
|
181
|
+
return key.startsWith('0x') ? key : `0x${key}`;
|
|
182
|
+
}
|
|
183
|
+
// Try active wallet (lazy import to avoid circular issues at module load)
|
|
184
|
+
const { getActiveWallet: getActive, walletExists: exists, decryptWallet: decrypt, promptPassword: prompt } = await import('./wallet.js');
|
|
185
|
+
const activeWallet = getActive();
|
|
186
|
+
if (activeWallet && exists(activeWallet)) {
|
|
187
|
+
const password = await prompt(` Enter password for wallet "${activeWallet}": `);
|
|
188
|
+
try {
|
|
189
|
+
const decrypted = decrypt(activeWallet, password);
|
|
190
|
+
return decrypted.privateKey;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
console.error(c.error(' Incorrect wallet password'));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
console.error(c.error('Private key required. Use --private-key, CLAWNCHER_PRIVATE_KEY env var, `clawncher config --private-key`, or `clawncher wallet use <name>`'));
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
function getNetwork(opts) {
|
|
201
|
+
const network = opts.network || loadConfig().network || 'sepolia';
|
|
202
|
+
if (network !== 'sepolia' && network !== 'mainnet') {
|
|
203
|
+
console.error(c.error('Invalid network. Use "sepolia" or "mainnet"'));
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
return network;
|
|
207
|
+
}
|
|
208
|
+
function getRpcUrl(network, opts) {
|
|
209
|
+
const config = loadConfig();
|
|
210
|
+
if (opts.rpc)
|
|
211
|
+
return opts.rpc;
|
|
212
|
+
if (network === 'sepolia' && config.rpc?.sepolia)
|
|
213
|
+
return config.rpc.sepolia;
|
|
214
|
+
if (network === 'mainnet' && config.rpc?.mainnet)
|
|
215
|
+
return config.rpc.mainnet;
|
|
216
|
+
// Default public RPCs
|
|
217
|
+
return network === 'mainnet'
|
|
218
|
+
? 'https://mainnet.base.org'
|
|
219
|
+
: 'https://sepolia.base.org';
|
|
220
|
+
}
|
|
221
|
+
function getChain(network) {
|
|
222
|
+
return network === 'mainnet' ? base : baseSepolia;
|
|
223
|
+
}
|
|
224
|
+
function createClients(network, privateKey, rpcUrl) {
|
|
225
|
+
const chain = getChain(network);
|
|
226
|
+
const account = privateKeyToAccount(privateKey);
|
|
227
|
+
const wallet = createWalletClient({
|
|
228
|
+
account,
|
|
229
|
+
chain,
|
|
230
|
+
transport: http(rpcUrl),
|
|
231
|
+
});
|
|
232
|
+
const publicClient = createPublicClient({
|
|
233
|
+
chain,
|
|
234
|
+
transport: http(rpcUrl),
|
|
235
|
+
});
|
|
236
|
+
// Cast to any to avoid viem version mismatch between CLI and SDK
|
|
237
|
+
return { wallet: wallet, publicClient: publicClient, account };
|
|
238
|
+
}
|
|
239
|
+
function getClawnchApiUrl() {
|
|
240
|
+
return process.env.CLAWNCHER_API_URL || process.env.CLAWNCH_API_URL || 'https://clawn.ch';
|
|
241
|
+
}
|
|
242
|
+
// Helper to read ERC20 decimals/symbol via any-typed publicClient (avoids viem version mismatch)
|
|
243
|
+
const erc20DecimalsAbi = [{ type: 'function', name: 'decimals', inputs: [], outputs: [{ type: 'uint8' }], stateMutability: 'view' }];
|
|
244
|
+
const erc20SymbolAbi = [{ type: 'function', name: 'symbol', inputs: [], outputs: [{ type: 'string' }], stateMutability: 'view' }];
|
|
245
|
+
async function readDecimals(client, token) {
|
|
246
|
+
return client.readContract({ address: token, abi: erc20DecimalsAbi, functionName: 'decimals' });
|
|
247
|
+
}
|
|
248
|
+
async function readSymbol(client, token) {
|
|
249
|
+
return client.readContract({ address: token, abi: erc20SymbolAbi, functionName: 'symbol' });
|
|
250
|
+
}
|
|
251
|
+
function getClient(options = {}) {
|
|
252
|
+
return new ClawnchClient({
|
|
253
|
+
baseUrl: options.apiUrl || process.env.CLAWNCHER_API_URL || 'https://clawn.ch',
|
|
254
|
+
moltbookKey: options.moltbookKey || process.env.MOLTBOOK_KEY,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// Main Program
|
|
259
|
+
// ============================================================================
|
|
260
|
+
const program = new Command();
|
|
261
|
+
program
|
|
262
|
+
.name('clawncher')
|
|
263
|
+
.description('CLI for Clawncher - token launch toolkit for Base, optimized for agents')
|
|
264
|
+
.version(VERSION)
|
|
265
|
+
.option('--api-url <url>', 'API base URL', 'https://clawn.ch')
|
|
266
|
+
.option('--moltbook-key <key>', 'Moltbook API key for authenticated operations');
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// Deploy Command
|
|
269
|
+
// ============================================================================
|
|
270
|
+
program
|
|
271
|
+
.command('deploy')
|
|
272
|
+
.description('Deploy a new token on Base with Uniswap V4 pool')
|
|
273
|
+
.requiredOption('--name <name>', 'Token name')
|
|
274
|
+
.requiredOption('--symbol <symbol>', 'Token symbol')
|
|
275
|
+
.option('--image <url>', 'Token image URL')
|
|
276
|
+
.option('--description <text>', 'Token description')
|
|
277
|
+
.option('--recipient <address>', 'Fee recipient address (defaults to deployer)')
|
|
278
|
+
.option('--fee-preference <pref>', 'Fee preference: Clawnch (token), Paired (WETH), or Both', 'Clawnch')
|
|
279
|
+
.option('--vault-percent <n>', 'Vault allocation percentage (1-90)')
|
|
280
|
+
.option('--vault-lockup <days>', 'Vault lockup duration in days', '7')
|
|
281
|
+
.option('--vault-vesting <days>', 'Vault vesting duration in days after lockup (0 = instant)', '0')
|
|
282
|
+
.option('--vault-recipient <address>', 'Vault recipient (defaults to deployer)')
|
|
283
|
+
.option('--dev-buy <eth>', 'Dev buy: ETH amount to spend (instant transfer)')
|
|
284
|
+
.option('--dev-buy-recipient <address>', 'Dev buy recipient (defaults to deployer)')
|
|
285
|
+
.option('--no-vanity', 'Disable vanity address (default: enabled)')
|
|
286
|
+
.option('--dry-run', 'Simulate deployment without sending transaction')
|
|
287
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
288
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
289
|
+
.option('--private-key <key>', 'Private key (or use CLAWNCHER_PRIVATE_KEY env)')
|
|
290
|
+
.option('--json', 'Output as JSON')
|
|
291
|
+
.action(async (opts) => {
|
|
292
|
+
const spinner = ora('Preparing deployment...').start();
|
|
293
|
+
try {
|
|
294
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
295
|
+
const network = getNetwork(opts);
|
|
296
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
297
|
+
const { wallet, publicClient, account } = createClients(network, privateKey, rpcUrl);
|
|
298
|
+
spinner.text = `Deploying ${opts.symbol} on ${network}...`;
|
|
299
|
+
const deployer = new ClawnchDeployer({
|
|
300
|
+
wallet,
|
|
301
|
+
publicClient,
|
|
302
|
+
network,
|
|
303
|
+
});
|
|
304
|
+
if (!deployer.isConfigured()) {
|
|
305
|
+
spinner.stop();
|
|
306
|
+
console.error(c.error(`Contracts not deployed on ${network}`));
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
// Parse fee preference (default: Clawnch = receive fees in the token)
|
|
310
|
+
let feePreference = 'Clawnch';
|
|
311
|
+
if (opts.feePreference) {
|
|
312
|
+
const pref = opts.feePreference.toLowerCase();
|
|
313
|
+
if (pref === 'paired')
|
|
314
|
+
feePreference = 'Paired';
|
|
315
|
+
else if (pref === 'both')
|
|
316
|
+
feePreference = 'Both';
|
|
317
|
+
else
|
|
318
|
+
feePreference = 'Clawnch';
|
|
319
|
+
}
|
|
320
|
+
// Build deploy options
|
|
321
|
+
const recipient = opts.recipient ? validateAddress(opts.recipient, 'recipient') : account.address;
|
|
322
|
+
const deployOpts = {
|
|
323
|
+
name: opts.name,
|
|
324
|
+
symbol: opts.symbol,
|
|
325
|
+
tokenAdmin: account.address,
|
|
326
|
+
image: opts.image,
|
|
327
|
+
metadata: opts.description ? { description: opts.description } : undefined,
|
|
328
|
+
rewards: {
|
|
329
|
+
recipients: [{
|
|
330
|
+
recipient,
|
|
331
|
+
admin: recipient,
|
|
332
|
+
bps: 10000, // 100%
|
|
333
|
+
feePreference,
|
|
334
|
+
}],
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
// Add vault if specified
|
|
338
|
+
if (opts.vaultPercent) {
|
|
339
|
+
const vaultPercent = safeParseInt(opts.vaultPercent, 'vault percent');
|
|
340
|
+
if (vaultPercent < 1 || vaultPercent > 90) {
|
|
341
|
+
spinner.stop();
|
|
342
|
+
console.error(c.error('Vault percent must be between 1 and 90'));
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
const lockupDays = safeParseInt(opts.vaultLockup || '7', 'vault lockup days');
|
|
346
|
+
const lockupSeconds = lockupDays * 24 * 60 * 60;
|
|
347
|
+
const vestingDays = safeParseInt(opts.vaultVesting || '0', 'vault vesting days');
|
|
348
|
+
const vestingSeconds = vestingDays * 24 * 60 * 60;
|
|
349
|
+
const vaultRecipient = opts.vaultRecipient ? validateAddress(opts.vaultRecipient, 'vault recipient') : account.address;
|
|
350
|
+
deployOpts.vault = {
|
|
351
|
+
percentage: vaultPercent,
|
|
352
|
+
lockupDuration: lockupSeconds,
|
|
353
|
+
vestingDuration: vestingSeconds,
|
|
354
|
+
recipient: vaultRecipient,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// Add instant dev buy if specified
|
|
358
|
+
if (opts.devBuy) {
|
|
359
|
+
const ethAmount = parseEther(opts.devBuy);
|
|
360
|
+
if (ethAmount <= 0n) {
|
|
361
|
+
spinner.stop();
|
|
362
|
+
console.error(c.error('Dev buy ETH amount must be greater than 0'));
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
const devBuyRecipient = opts.devBuyRecipient ? validateAddress(opts.devBuyRecipient, 'dev buy recipient') : account.address;
|
|
366
|
+
deployOpts.devBuy = {
|
|
367
|
+
ethAmount,
|
|
368
|
+
recipient: devBuyRecipient,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
// Configure vanity address (default: enabled, uses Clanker's remote service)
|
|
372
|
+
deployOpts.vanity = opts.vanity !== false; // --no-vanity sets this to false
|
|
373
|
+
// Dry run mode
|
|
374
|
+
if (opts.dryRun) {
|
|
375
|
+
deployOpts.dryRun = true;
|
|
376
|
+
const result = await deployer.deploy(deployOpts);
|
|
377
|
+
spinner.stop();
|
|
378
|
+
if (result.error) {
|
|
379
|
+
handleError(result.error);
|
|
380
|
+
}
|
|
381
|
+
if (opts.json) {
|
|
382
|
+
console.log(JSON.stringify({
|
|
383
|
+
dryRun: true,
|
|
384
|
+
valid: result.valid,
|
|
385
|
+
estimatedGas: result.estimatedGas?.toString(),
|
|
386
|
+
estimatedCostEth: result.estimatedCostEth,
|
|
387
|
+
translatedConfig: result.translatedConfig,
|
|
388
|
+
}, null, 2));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
console.log();
|
|
392
|
+
console.log(` ${c.success('\u2713')} ${c.highlight('Dry run passed')}`);
|
|
393
|
+
console.log();
|
|
394
|
+
console.log(sectionHeader('Validation'));
|
|
395
|
+
console.log(kv('Valid', result.valid ? `${dot('green')} Yes` : `${dot('red')} No`));
|
|
396
|
+
console.log(kv('Name', opts.name));
|
|
397
|
+
console.log(kv('Symbol', c.highlight(opts.symbol)));
|
|
398
|
+
console.log(kv('Network', networkBadge(network)));
|
|
399
|
+
if (result.estimatedGas) {
|
|
400
|
+
console.log(kv('Est. Gas', c.value(result.estimatedGas.toLocaleString())));
|
|
401
|
+
}
|
|
402
|
+
if (result.estimatedCostEth) {
|
|
403
|
+
console.log(kv('Est. Cost', c.accent(result.estimatedCostEth + ' ETH')));
|
|
404
|
+
}
|
|
405
|
+
console.log();
|
|
406
|
+
console.log(sectionHeader('Translated Config'));
|
|
407
|
+
const configStr = JSON.stringify(result.translatedConfig, null, 2);
|
|
408
|
+
for (const line of configStr.split('\n')) {
|
|
409
|
+
console.log(c.muted(' ' + line));
|
|
410
|
+
}
|
|
411
|
+
console.log();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const result = await deployer.deploy(deployOpts);
|
|
415
|
+
if (result.error) {
|
|
416
|
+
spinner.stop();
|
|
417
|
+
handleError(result.error);
|
|
418
|
+
}
|
|
419
|
+
spinner.text = 'Waiting for transaction confirmation...';
|
|
420
|
+
const { address } = await result.waitForTransaction();
|
|
421
|
+
spinner.stop();
|
|
422
|
+
if (opts.json) {
|
|
423
|
+
console.log(JSON.stringify({
|
|
424
|
+
success: true,
|
|
425
|
+
txHash: result.txHash,
|
|
426
|
+
tokenAddress: address,
|
|
427
|
+
network,
|
|
428
|
+
name: opts.name,
|
|
429
|
+
symbol: opts.symbol,
|
|
430
|
+
deployer: account.address,
|
|
431
|
+
}, null, 2));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
console.log();
|
|
435
|
+
console.log(` ${c.success('\u2713')} ${c.highlight('Token deployed successfully')}`);
|
|
436
|
+
console.log();
|
|
437
|
+
console.log(sectionHeader('Deployment'));
|
|
438
|
+
console.log(kv('Network', networkBadge(network)));
|
|
439
|
+
console.log(kv('Name', opts.name));
|
|
440
|
+
console.log(kv('Symbol', c.highlight(opts.symbol)));
|
|
441
|
+
console.log(kv('Token', address ? fmtAddr(address) : c.warn('Pending...')));
|
|
442
|
+
console.log(kv('Transaction', c.muted(result.txHash)));
|
|
443
|
+
console.log(kv('Deployer', fmtAddr(account.address)));
|
|
444
|
+
console.log(kv('Fee Recipient', fmtAddr(recipient)));
|
|
445
|
+
if (opts.vaultPercent) {
|
|
446
|
+
console.log(kv('Vault', `${c.accent(opts.vaultPercent + '%')} locked for ${c.value((opts.vaultLockup || 7) + ' days')}`));
|
|
447
|
+
}
|
|
448
|
+
const explorerUrl = network === 'mainnet'
|
|
449
|
+
? `https://basescan.org/tx/${result.txHash}`
|
|
450
|
+
: `https://sepolia.basescan.org/tx/${result.txHash}`;
|
|
451
|
+
console.log();
|
|
452
|
+
console.log(` ${c.link(explorerUrl)}`);
|
|
453
|
+
console.log();
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
spinner.stop();
|
|
457
|
+
handleError(err);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
// ============================================================================
|
|
461
|
+
// Info Command (Read from chain)
|
|
462
|
+
// ============================================================================
|
|
463
|
+
program
|
|
464
|
+
.command('info')
|
|
465
|
+
.description('Get full token details from chain (deployment, vault, MEV, rewards)')
|
|
466
|
+
.argument('<address>', 'Token contract address')
|
|
467
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
468
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
469
|
+
.option('--json', 'Output as JSON')
|
|
470
|
+
.action(async (address, opts) => {
|
|
471
|
+
const spinner = ora('Fetching token details...').start();
|
|
472
|
+
try {
|
|
473
|
+
const tokenAddress = validateAddress(address, 'token');
|
|
474
|
+
const network = getNetwork(opts);
|
|
475
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
476
|
+
const chain = getChain(network);
|
|
477
|
+
const publicClient = createPublicClient({
|
|
478
|
+
chain,
|
|
479
|
+
transport: http(rpcUrl),
|
|
480
|
+
});
|
|
481
|
+
const reader = new ClawnchReader({
|
|
482
|
+
publicClient,
|
|
483
|
+
network,
|
|
484
|
+
});
|
|
485
|
+
const details = await reader.getTokenDetails(tokenAddress);
|
|
486
|
+
spinner.stop();
|
|
487
|
+
if (!details) {
|
|
488
|
+
// Fall back to basic ERC20 info if not a Clawnch token
|
|
489
|
+
const info = await reader.getTokenInfo(tokenAddress);
|
|
490
|
+
console.log();
|
|
491
|
+
console.log(` ${c.warn('\u26A0')} ${c.warn(info.symbol + ' is not a Clawnch token')}`);
|
|
492
|
+
console.log();
|
|
493
|
+
console.log(kv('Address', fmtAddr(address)));
|
|
494
|
+
console.log(kv('Name', info.name));
|
|
495
|
+
console.log(kv('Symbol', info.symbol));
|
|
496
|
+
console.log(kv('Total Supply', formatEther(info.totalSupply)));
|
|
497
|
+
console.log(kv('Network', networkBadge(network)));
|
|
498
|
+
console.log();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (opts.json) {
|
|
502
|
+
// Serialize bigints for JSON output
|
|
503
|
+
const serializable = JSON.parse(JSON.stringify(details, (_, v) => typeof v === 'bigint' ? v.toString() : v));
|
|
504
|
+
console.log(JSON.stringify(serializable, null, 2));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
console.log();
|
|
508
|
+
console.log(` ${c.highlight(details.symbol)} ${c.muted('\u00B7 ' + details.name)}`);
|
|
509
|
+
console.log();
|
|
510
|
+
console.log(sectionHeader('Token'));
|
|
511
|
+
console.log(kv('Address', fmtAddr(details.address)));
|
|
512
|
+
console.log(kv('Total Supply', formatEther(details.totalSupply)));
|
|
513
|
+
console.log(kv('Admin', fmtAddr(details.tokenAdmin)));
|
|
514
|
+
console.log(kv('Network', networkBadge(network)));
|
|
515
|
+
if (details.image) {
|
|
516
|
+
console.log(kv('Image', c.link(details.image)));
|
|
517
|
+
}
|
|
518
|
+
// Deployment
|
|
519
|
+
console.log();
|
|
520
|
+
console.log(sectionHeader('Deployment'));
|
|
521
|
+
console.log(kv('Hook', fmtAddr(details.deployment.hook, true)));
|
|
522
|
+
console.log(kv('Locker', fmtAddr(details.deployment.locker, true)));
|
|
523
|
+
if (details.deployment.extensions.length > 0) {
|
|
524
|
+
console.log(kv('Extensions', details.deployment.extensions.map(e => fmtAddr(e, true)).join(c.dim(', '))));
|
|
525
|
+
}
|
|
526
|
+
// Rewards
|
|
527
|
+
if (details.rewards) {
|
|
528
|
+
console.log();
|
|
529
|
+
console.log(sectionHeader('Fee Recipients'));
|
|
530
|
+
for (let i = 0; i < details.rewards.rewardRecipients.length; i++) {
|
|
531
|
+
const bps = details.rewards.rewardBps[i];
|
|
532
|
+
const pct = (bps / 100).toFixed(1);
|
|
533
|
+
console.log(` ${fmtAddr(details.rewards.rewardRecipients[i], true)} ${c.dim('\u2500')} ${c.accent(pct + '%')}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Vault
|
|
537
|
+
if (details.vault) {
|
|
538
|
+
console.log();
|
|
539
|
+
console.log(sectionHeader('Vault'));
|
|
540
|
+
console.log(kv('Vesting', progressBar(details.vault.percentVested)));
|
|
541
|
+
console.log(kv('Total', formatEther(details.vault.amountTotal) + ' tokens'));
|
|
542
|
+
console.log(kv('Claimed', formatEther(details.vault.amountClaimed) + ' tokens'));
|
|
543
|
+
console.log(kv('Available', c.accent(formatEther(details.vault.amountAvailable) + ' tokens')));
|
|
544
|
+
console.log(kv('Unlocked', details.vault.isUnlocked ? `${dot('green')} Yes` : `${dot('yellow')} No`));
|
|
545
|
+
console.log(kv('Admin', fmtAddr(details.vault.admin, true)));
|
|
546
|
+
}
|
|
547
|
+
// Vested Dev Buy (legacy v2 tokens only)
|
|
548
|
+
if (details.vestedDevBuy) {
|
|
549
|
+
console.log();
|
|
550
|
+
console.log(sectionHeader('Vested Dev Buy (Legacy)'));
|
|
551
|
+
console.log(c.warn(' Vested dev buy claiming is not available in v3'));
|
|
552
|
+
console.log(kv('Vesting', progressBar(details.vestedDevBuy.percentVested)));
|
|
553
|
+
console.log(kv('Total', formatEther(details.vestedDevBuy.amountTotal) + ' tokens'));
|
|
554
|
+
console.log(kv('Claimed', formatEther(details.vestedDevBuy.amountClaimed) + ' tokens'));
|
|
555
|
+
console.log(kv('Available', c.accent(formatEther(details.vestedDevBuy.amountAvailable) + ' tokens')));
|
|
556
|
+
console.log(kv('Unlocked', details.vestedDevBuy.isUnlocked ? `${dot('green')} Yes` : `${dot('yellow')} No`));
|
|
557
|
+
}
|
|
558
|
+
// MEV
|
|
559
|
+
if (details.mev) {
|
|
560
|
+
console.log();
|
|
561
|
+
console.log(sectionHeader('MEV Protection'));
|
|
562
|
+
console.log(kv('Current Fee', c.accent((details.mev.currentFee / 10000 * 100).toFixed(2) + '%')));
|
|
563
|
+
console.log(kv('Starting Fee', (details.mev.startingFee / 10000 * 100).toFixed(2) + '%'));
|
|
564
|
+
console.log(kv('Ending Fee', (details.mev.endingFee / 10000 * 100).toFixed(2) + '%'));
|
|
565
|
+
console.log(kv('Decay Done', details.mev.isDecayComplete ? `${dot('green')} Complete` : `${dot('yellow')} Active`));
|
|
566
|
+
}
|
|
567
|
+
// Explorer link
|
|
568
|
+
const explorerUrl = network === 'mainnet'
|
|
569
|
+
? `https://basescan.org/address/${address}`
|
|
570
|
+
: `https://sepolia.basescan.org/address/${address}`;
|
|
571
|
+
console.log();
|
|
572
|
+
console.log(` ${c.link(explorerUrl)}`);
|
|
573
|
+
console.log();
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
spinner.stop();
|
|
577
|
+
handleError(err);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
// ============================================================================
|
|
581
|
+
// Fees Commands
|
|
582
|
+
// ============================================================================
|
|
583
|
+
const fees = program.command('fees').description('Manage trading fees');
|
|
584
|
+
fees
|
|
585
|
+
.command('check')
|
|
586
|
+
.description('Check claimable fees for a wallet')
|
|
587
|
+
.argument('<wallet>', 'Wallet address')
|
|
588
|
+
.option('-t, --tokens <addresses>', 'Comma-separated token addresses to check')
|
|
589
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
590
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
591
|
+
.option('--json', 'Output as JSON')
|
|
592
|
+
.action(async (wallet, opts) => {
|
|
593
|
+
const walletAddr = validateAddress(wallet, 'wallet');
|
|
594
|
+
const spinner = ora('Checking fees...').start();
|
|
595
|
+
try {
|
|
596
|
+
const network = getNetwork(opts);
|
|
597
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
598
|
+
const chain = getChain(network);
|
|
599
|
+
const publicClient = createPublicClient({
|
|
600
|
+
chain,
|
|
601
|
+
transport: http(rpcUrl),
|
|
602
|
+
});
|
|
603
|
+
const addresses = getAddresses(network);
|
|
604
|
+
// If specific tokens provided, validate and check those
|
|
605
|
+
const tokenAddresses = opts.tokens
|
|
606
|
+
? opts.tokens.split(',').map((t) => validateAddress(t.trim(), 'token'))
|
|
607
|
+
: [];
|
|
608
|
+
const results = [];
|
|
609
|
+
// Check fees for each token in FeeLocker
|
|
610
|
+
for (const tokenAddr of tokenAddresses) {
|
|
611
|
+
try {
|
|
612
|
+
const available = await publicClient.readContract({
|
|
613
|
+
address: addresses.clawnch.feeLocker,
|
|
614
|
+
abi: ClawnchFeeLockerABI,
|
|
615
|
+
functionName: 'availableFees',
|
|
616
|
+
args: [walletAddr, tokenAddr],
|
|
617
|
+
});
|
|
618
|
+
results.push({
|
|
619
|
+
token: tokenAddr,
|
|
620
|
+
wethAvailable: available,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
// Token may not have fees
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
spinner.stop();
|
|
628
|
+
if (opts.json) {
|
|
629
|
+
console.log(JSON.stringify({
|
|
630
|
+
wallet,
|
|
631
|
+
network,
|
|
632
|
+
tokens: results.map(r => ({
|
|
633
|
+
token: r.token,
|
|
634
|
+
wethAvailable: r.wethAvailable.toString(),
|
|
635
|
+
wethFormatted: formatEther(r.wethAvailable),
|
|
636
|
+
})),
|
|
637
|
+
}, null, 2));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
console.log();
|
|
641
|
+
console.log(sectionHeader('Claimable Fees'));
|
|
642
|
+
console.log(kv('Wallet', fmtAddr(wallet)));
|
|
643
|
+
console.log(kv('Network', networkBadge(network)));
|
|
644
|
+
console.log();
|
|
645
|
+
if (results.length === 0) {
|
|
646
|
+
if (tokenAddresses.length === 0) {
|
|
647
|
+
console.log(c.warn(' Provide token addresses with --tokens to check fees'));
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
console.log(c.muted(' No claimable fees found'));
|
|
651
|
+
}
|
|
652
|
+
console.log();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
console.log(styledTable(['Token', 'WETH Available'], results.map(r => [fmtAddr(r.token, true), c.accent(formatEther(r.wethAvailable))])));
|
|
656
|
+
console.log();
|
|
657
|
+
}
|
|
658
|
+
catch (err) {
|
|
659
|
+
spinner.stop();
|
|
660
|
+
handleError(err);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
fees
|
|
664
|
+
.command('available')
|
|
665
|
+
.description('Check available fees for a wallet (via API)')
|
|
666
|
+
.argument('<wallet>', 'Wallet address')
|
|
667
|
+
.option('-t, --tokens <addresses>', 'Comma-separated token addresses to check')
|
|
668
|
+
.option('--json', 'Output as JSON')
|
|
669
|
+
.action(async (wallet, opts) => {
|
|
670
|
+
const spinner = ora('Checking fees...').start();
|
|
671
|
+
try {
|
|
672
|
+
const client = getClient(program.opts());
|
|
673
|
+
const result = await client.getAvailableFees(wallet, opts.tokens);
|
|
674
|
+
spinner.stop();
|
|
675
|
+
if (opts.json) {
|
|
676
|
+
console.log(JSON.stringify(result, null, 2));
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
console.log();
|
|
680
|
+
console.log(sectionHeader('Available Fees'));
|
|
681
|
+
console.log(kv('Wallet', fmtAddr(wallet)));
|
|
682
|
+
const totalWeth = result.total_weth || result.weth?.available || '0';
|
|
683
|
+
console.log(kv('Total WETH', c.accent((result.weth?.formatted || formatEther(BigInt(totalWeth))) + ' WETH')));
|
|
684
|
+
console.log();
|
|
685
|
+
if (!result.tokens?.length) {
|
|
686
|
+
console.log(c.muted(' No tokens with claimable fees'));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
console.log(styledTable(['Symbol', 'Address', 'Available'], result.tokens.map((t) => [
|
|
690
|
+
c.highlight(t.symbol),
|
|
691
|
+
fmtAddr(t.address, true),
|
|
692
|
+
c.accent(t.formatted || formatEther(BigInt(t.available || '0'))),
|
|
693
|
+
])));
|
|
694
|
+
console.log();
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
spinner.stop();
|
|
698
|
+
handleError(err);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
fees
|
|
702
|
+
.command('claim')
|
|
703
|
+
.description('Claim trading fees on-chain (collect rewards + claim from FeeLocker)')
|
|
704
|
+
.argument('<token>', 'Token contract address')
|
|
705
|
+
.requiredOption('--fee-owner <address>', 'Fee owner address (the reward recipient configured at deploy)')
|
|
706
|
+
.option('--collect-only', 'Only collect LP rewards (skip fee claims)')
|
|
707
|
+
.option('--skip-collect', 'Skip collecting LP rewards (only claim from FeeLocker)')
|
|
708
|
+
.option('--vault', 'Also claim vault allocation if available')
|
|
709
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
710
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
711
|
+
.option('--private-key <key>', 'Private key (or use CLAWNCHER_PRIVATE_KEY env)')
|
|
712
|
+
.option('--json', 'Output as JSON')
|
|
713
|
+
.action(async (token, opts) => {
|
|
714
|
+
const spinner = ora('Preparing claim...').start();
|
|
715
|
+
try {
|
|
716
|
+
const tokenAddress = validateAddress(token, 'token');
|
|
717
|
+
const feeOwner = validateAddress(opts.feeOwner, 'fee owner');
|
|
718
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
719
|
+
const network = getNetwork(opts);
|
|
720
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
721
|
+
const { wallet, publicClient, account } = createClients(network, privateKey, rpcUrl);
|
|
722
|
+
const addresses = getAddresses(network);
|
|
723
|
+
const claimer = new ClawncherClaimer({
|
|
724
|
+
wallet,
|
|
725
|
+
publicClient,
|
|
726
|
+
network,
|
|
727
|
+
});
|
|
728
|
+
const results = [];
|
|
729
|
+
// Step 1: Collect LP rewards (unless --skip-collect)
|
|
730
|
+
if (!opts.skipCollect) {
|
|
731
|
+
spinner.text = 'Collecting LP rewards...';
|
|
732
|
+
try {
|
|
733
|
+
const collectResult = await claimer.collectRewards(tokenAddress);
|
|
734
|
+
const receipt = await collectResult.wait();
|
|
735
|
+
results.push({ step: 'Collect Rewards', txHash: collectResult.txHash, success: receipt.success });
|
|
736
|
+
}
|
|
737
|
+
catch (err) {
|
|
738
|
+
results.push({ step: 'Collect Rewards', txHash: '', success: false });
|
|
739
|
+
if (opts.collectOnly) {
|
|
740
|
+
spinner.stop();
|
|
741
|
+
handleError(err);
|
|
742
|
+
}
|
|
743
|
+
// Continue to fee claims even if collect fails (may already have unclaimed fees)
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (!opts.collectOnly) {
|
|
747
|
+
// Step 2: Check and claim WETH fees
|
|
748
|
+
spinner.text = 'Checking WETH fees...';
|
|
749
|
+
try {
|
|
750
|
+
const wethAvailable = await publicClient.readContract({
|
|
751
|
+
address: addresses.clawnch.feeLocker,
|
|
752
|
+
abi: ClawnchFeeLockerABI,
|
|
753
|
+
functionName: 'availableFees',
|
|
754
|
+
args: [feeOwner, addresses.infrastructure.weth],
|
|
755
|
+
});
|
|
756
|
+
if (wethAvailable > 0n) {
|
|
757
|
+
spinner.text = `Claiming ${formatEther(wethAvailable)} WETH...`;
|
|
758
|
+
const claimResult = await claimer.claimFees(feeOwner, addresses.infrastructure.weth);
|
|
759
|
+
const receipt = await claimResult.wait();
|
|
760
|
+
results.push({ step: `Claim WETH (${formatEther(wethAvailable)})`, txHash: claimResult.txHash, success: receipt.success });
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
results.push({ step: 'Claim WETH', txHash: '-', success: true });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
results.push({ step: 'Claim WETH', txHash: '', success: false });
|
|
768
|
+
}
|
|
769
|
+
// Step 3: Check and claim token fees
|
|
770
|
+
spinner.text = 'Checking token fees...';
|
|
771
|
+
try {
|
|
772
|
+
const tokenAvailable = await publicClient.readContract({
|
|
773
|
+
address: addresses.clawnch.feeLocker,
|
|
774
|
+
abi: ClawnchFeeLockerABI,
|
|
775
|
+
functionName: 'availableFees',
|
|
776
|
+
args: [feeOwner, tokenAddress],
|
|
777
|
+
});
|
|
778
|
+
if (tokenAvailable > 0n) {
|
|
779
|
+
spinner.text = `Claiming ${formatEther(tokenAvailable)} token fees...`;
|
|
780
|
+
const claimResult = await claimer.claimFees(feeOwner, tokenAddress);
|
|
781
|
+
const receipt = await claimResult.wait();
|
|
782
|
+
results.push({ step: `Claim Token (${formatEther(tokenAvailable)})`, txHash: claimResult.txHash, success: receipt.success });
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
results.push({ step: 'Claim Token', txHash: '-', success: true });
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
results.push({ step: 'Claim Token', txHash: '', success: false });
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// Optional: Claim vault
|
|
793
|
+
if (opts.vault) {
|
|
794
|
+
spinner.text = 'Claiming vault allocation...';
|
|
795
|
+
try {
|
|
796
|
+
const vaultResult = await claimer.claimVault(tokenAddress);
|
|
797
|
+
const receipt = await vaultResult.wait();
|
|
798
|
+
results.push({ step: 'Claim Vault', txHash: vaultResult.txHash, success: receipt.success });
|
|
799
|
+
}
|
|
800
|
+
catch {
|
|
801
|
+
results.push({ step: 'Claim Vault', txHash: '', success: false });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
spinner.stop();
|
|
805
|
+
if (opts.json) {
|
|
806
|
+
console.log(JSON.stringify({ token, feeOwner, network, results }, null, 2));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
console.log();
|
|
810
|
+
console.log(sectionHeader('Fee Claim Results'));
|
|
811
|
+
console.log(kv('Token', fmtAddr(tokenAddress)));
|
|
812
|
+
console.log(kv('Fee Owner', fmtAddr(feeOwner)));
|
|
813
|
+
console.log(kv('Network', networkBadge(network)));
|
|
814
|
+
console.log(kv('Caller', fmtAddr(account.address)));
|
|
815
|
+
console.log();
|
|
816
|
+
for (const r of results) {
|
|
817
|
+
const icon = r.success ? c.success('\u2713') : c.error('\u2717');
|
|
818
|
+
const hash = r.txHash && r.txHash !== '-' && r.txHash !== ''
|
|
819
|
+
? c.muted(r.txHash.slice(0, 10) + '...')
|
|
820
|
+
: r.txHash === '-' ? c.dim('no fees') : c.error('failed');
|
|
821
|
+
console.log(` ${icon} ${c.label(r.step.padEnd(30))} ${hash}`);
|
|
822
|
+
}
|
|
823
|
+
const successCount = results.filter(r => r.success).length;
|
|
824
|
+
console.log();
|
|
825
|
+
console.log(c.dim(` ${successCount}/${results.length} steps completed`));
|
|
826
|
+
console.log();
|
|
827
|
+
}
|
|
828
|
+
catch (err) {
|
|
829
|
+
spinner.stop();
|
|
830
|
+
handleError(err);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
fees
|
|
834
|
+
.command('batch-claim')
|
|
835
|
+
.description('Claim fees for multiple tokens in one session')
|
|
836
|
+
.argument('<tokens...>', 'Token contract addresses (space-separated)')
|
|
837
|
+
.requiredOption('--fee-owner <address>', 'Fee owner address')
|
|
838
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
839
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
840
|
+
.option('--private-key <key>', 'Private key (or use CLAWNCHER_PRIVATE_KEY env)')
|
|
841
|
+
.option('--json', 'Output as JSON')
|
|
842
|
+
.action(async (tokens, opts) => {
|
|
843
|
+
const spinner = ora('Starting batch claim...').start();
|
|
844
|
+
try {
|
|
845
|
+
const feeOwner = validateAddress(opts.feeOwner, 'fee owner');
|
|
846
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
847
|
+
const network = getNetwork(opts);
|
|
848
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
849
|
+
const tokenAddresses = tokens.map((t) => validateAddress(t.trim(), 'token'));
|
|
850
|
+
const { wallet, publicClient, account } = createClients(network, privateKey, rpcUrl);
|
|
851
|
+
const claimer = new ClawncherClaimer({
|
|
852
|
+
wallet,
|
|
853
|
+
publicClient,
|
|
854
|
+
network,
|
|
855
|
+
});
|
|
856
|
+
const result = await claimer.claimBatch(tokenAddresses, feeOwner, {
|
|
857
|
+
onProgress: (token, step) => {
|
|
858
|
+
spinner.text = `${fmtAddr(token, true)}: ${step}`;
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
spinner.stop();
|
|
862
|
+
if (opts.json) {
|
|
863
|
+
console.log(JSON.stringify({
|
|
864
|
+
feeOwner,
|
|
865
|
+
network,
|
|
866
|
+
successCount: result.successCount,
|
|
867
|
+
failureCount: result.failureCount,
|
|
868
|
+
results: result.results.map(r => ({
|
|
869
|
+
token: r.token,
|
|
870
|
+
success: r.success,
|
|
871
|
+
collectRewards: r.collectRewards?.txHash,
|
|
872
|
+
claimFeesWeth: r.claimFeesWeth?.txHash,
|
|
873
|
+
claimFeesToken: r.claimFeesToken?.txHash,
|
|
874
|
+
error: r.error?.message,
|
|
875
|
+
})),
|
|
876
|
+
}, null, 2));
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
console.log();
|
|
880
|
+
console.log(sectionHeader('Batch Claim Results'));
|
|
881
|
+
console.log(kv('Fee Owner', fmtAddr(feeOwner)));
|
|
882
|
+
console.log(kv('Network', networkBadge(network)));
|
|
883
|
+
console.log(kv('Tokens', c.value(tokenAddresses.length.toString())));
|
|
884
|
+
console.log();
|
|
885
|
+
for (const r of result.results) {
|
|
886
|
+
const icon = r.success ? c.success('\u2713') : c.error('\u2717');
|
|
887
|
+
const status = r.success ? c.success('claimed') : c.error(r.error?.message || 'failed');
|
|
888
|
+
console.log(` ${icon} ${fmtAddr(r.token, true)} ${c.dim('\u2500')} ${status}`);
|
|
889
|
+
if (r.collectRewards) {
|
|
890
|
+
console.log(` ${c.muted('collect:')} ${c.dim(r.collectRewards.txHash.slice(0, 14) + '...')}`);
|
|
891
|
+
}
|
|
892
|
+
if (r.claimFeesWeth) {
|
|
893
|
+
console.log(` ${c.muted('weth:')} ${c.dim(r.claimFeesWeth.txHash.slice(0, 14) + '...')}`);
|
|
894
|
+
}
|
|
895
|
+
if (r.claimFeesToken) {
|
|
896
|
+
console.log(` ${c.muted('token:')} ${c.dim(r.claimFeesToken.txHash.slice(0, 14) + '...')}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
console.log();
|
|
900
|
+
console.log(c.dim(` ${result.successCount}/${result.results.length} tokens claimed successfully`));
|
|
901
|
+
console.log();
|
|
902
|
+
}
|
|
903
|
+
catch (err) {
|
|
904
|
+
spinner.stop();
|
|
905
|
+
handleError(err);
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
// ============================================================================
|
|
909
|
+
// Portfolio Command
|
|
910
|
+
// ============================================================================
|
|
911
|
+
program
|
|
912
|
+
.command('portfolio')
|
|
913
|
+
.description('Show all tokens and claimable fees for a wallet')
|
|
914
|
+
.argument('<wallet>', 'Wallet address')
|
|
915
|
+
.option('-t, --tokens <addresses>', 'Comma-separated token addresses to check')
|
|
916
|
+
.option('--discover', 'Discover tokens by scanning factory events (slower)')
|
|
917
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
918
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
919
|
+
.option('--json', 'Output as JSON')
|
|
920
|
+
.action(async (wallet, opts) => {
|
|
921
|
+
const spinner = ora('Building portfolio...').start();
|
|
922
|
+
try {
|
|
923
|
+
const walletAddr = validateAddress(wallet, 'wallet');
|
|
924
|
+
const network = getNetwork(opts);
|
|
925
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
926
|
+
const chain = getChain(network);
|
|
927
|
+
const publicClient = createPublicClient({
|
|
928
|
+
chain,
|
|
929
|
+
transport: http(rpcUrl),
|
|
930
|
+
});
|
|
931
|
+
const portfolio = new ClawnchPortfolio({
|
|
932
|
+
publicClient,
|
|
933
|
+
network,
|
|
934
|
+
});
|
|
935
|
+
// Get token list
|
|
936
|
+
let tokenAddresses = [];
|
|
937
|
+
if (opts.tokens) {
|
|
938
|
+
tokenAddresses = opts.tokens.split(',').map((t) => validateAddress(t.trim(), 'token'));
|
|
939
|
+
}
|
|
940
|
+
else if (opts.discover) {
|
|
941
|
+
spinner.text = 'Scanning for deployed tokens...';
|
|
942
|
+
tokenAddresses = await portfolio.discoverTokens(walletAddr);
|
|
943
|
+
}
|
|
944
|
+
if (tokenAddresses.length === 0) {
|
|
945
|
+
spinner.stop();
|
|
946
|
+
console.log();
|
|
947
|
+
console.log(c.warn(' No tokens to check. Use --tokens or --discover to specify tokens.'));
|
|
948
|
+
console.log(c.muted(' Example: clawncher portfolio 0x... --tokens 0xToken1,0xToken2'));
|
|
949
|
+
console.log(c.muted(' Example: clawncher portfolio 0x... --discover'));
|
|
950
|
+
console.log();
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
spinner.text = `Checking ${tokenAddresses.length} tokens...`;
|
|
954
|
+
const [tokens, claimable] = await Promise.all([
|
|
955
|
+
portfolio.getTokensForWallet(walletAddr, tokenAddresses),
|
|
956
|
+
portfolio.getTotalClaimable(walletAddr, tokenAddresses),
|
|
957
|
+
]);
|
|
958
|
+
spinner.stop();
|
|
959
|
+
if (opts.json) {
|
|
960
|
+
console.log(JSON.stringify({
|
|
961
|
+
wallet,
|
|
962
|
+
network,
|
|
963
|
+
tokens: tokens.map(t => ({
|
|
964
|
+
address: t.address,
|
|
965
|
+
name: t.name,
|
|
966
|
+
symbol: t.symbol,
|
|
967
|
+
bps: t.bps,
|
|
968
|
+
claimableWeth: t.claimableWeth.toString(),
|
|
969
|
+
claimableToken: t.claimableToken.toString(),
|
|
970
|
+
formattedWeth: t.formattedWeth,
|
|
971
|
+
formattedToken: t.formattedToken,
|
|
972
|
+
})),
|
|
973
|
+
totalClaimable: {
|
|
974
|
+
weth: claimable.weth.toString(),
|
|
975
|
+
formattedWeth: claimable.formattedWeth,
|
|
976
|
+
tokens: claimable.tokens.map(t => ({
|
|
977
|
+
address: t.address,
|
|
978
|
+
symbol: t.symbol,
|
|
979
|
+
amount: t.amount.toString(),
|
|
980
|
+
formatted: t.formatted,
|
|
981
|
+
})),
|
|
982
|
+
},
|
|
983
|
+
}, null, 2));
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
console.log();
|
|
987
|
+
console.log(` ${c.highlight('Portfolio')} ${c.muted('for')} ${fmtAddr(wallet, true)}`);
|
|
988
|
+
console.log();
|
|
989
|
+
console.log(sectionHeader('Summary'));
|
|
990
|
+
console.log(kv('Total WETH', c.accent(claimable.formattedWeth + ' WETH')));
|
|
991
|
+
console.log(kv('Tokens Found', c.value(tokens.length.toString())));
|
|
992
|
+
console.log(kv('Network', networkBadge(network)));
|
|
993
|
+
console.log();
|
|
994
|
+
if (tokens.length === 0) {
|
|
995
|
+
console.log(c.muted(' No tokens found where this wallet is a fee recipient'));
|
|
996
|
+
console.log();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
console.log(styledTable(['Symbol', 'Address', 'BPS', 'WETH Fees', 'Token Fees'], tokens.map(t => [
|
|
1000
|
+
c.highlight(t.symbol),
|
|
1001
|
+
fmtAddr(t.address, true),
|
|
1002
|
+
c.value((t.bps / 100).toFixed(1) + '%'),
|
|
1003
|
+
c.accent(t.formattedWeth),
|
|
1004
|
+
c.value(t.formattedToken),
|
|
1005
|
+
])));
|
|
1006
|
+
if (claimable.tokens.length > 0) {
|
|
1007
|
+
console.log();
|
|
1008
|
+
console.log(sectionHeader('Token Fees'));
|
|
1009
|
+
for (const t of claimable.tokens) {
|
|
1010
|
+
console.log(` ${c.highlight(t.symbol)} ${c.dim('\u2500')} ${c.accent(t.formatted)} tokens`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
console.log();
|
|
1014
|
+
}
|
|
1015
|
+
catch (err) {
|
|
1016
|
+
spinner.stop();
|
|
1017
|
+
handleError(err);
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
// ============================================================================
|
|
1021
|
+
// Watch Command
|
|
1022
|
+
// ============================================================================
|
|
1023
|
+
program
|
|
1024
|
+
.command('watch')
|
|
1025
|
+
.description('Watch for new token deployments in real-time')
|
|
1026
|
+
.option('--admin <address>', 'Filter by token admin address')
|
|
1027
|
+
.option('--history <blocks>', 'Also show recent deployments from last N blocks', '100')
|
|
1028
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
1029
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
1030
|
+
.action(async (opts) => {
|
|
1031
|
+
const network = getNetwork(opts);
|
|
1032
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
1033
|
+
const chain = getChain(network);
|
|
1034
|
+
const publicClient = createPublicClient({
|
|
1035
|
+
chain,
|
|
1036
|
+
transport: http(rpcUrl),
|
|
1037
|
+
});
|
|
1038
|
+
const watcher = new ClawnchWatcher({
|
|
1039
|
+
publicClient,
|
|
1040
|
+
network,
|
|
1041
|
+
});
|
|
1042
|
+
const adminFilter = opts.admin ? validateAddress(opts.admin, 'admin') : undefined;
|
|
1043
|
+
console.log();
|
|
1044
|
+
console.log(` ${c.highlight('Watching deployments')} ${networkBadge(network)}`);
|
|
1045
|
+
if (adminFilter) {
|
|
1046
|
+
console.log(kv('Filter', `admin = ${fmtAddr(adminFilter, true)}`));
|
|
1047
|
+
}
|
|
1048
|
+
console.log(c.muted(' Press Ctrl+C to stop'));
|
|
1049
|
+
console.log();
|
|
1050
|
+
// Show recent history first
|
|
1051
|
+
if (opts.history && parseInt(opts.history) > 0) {
|
|
1052
|
+
const blocks = BigInt(parseInt(opts.history));
|
|
1053
|
+
try {
|
|
1054
|
+
const currentBlock = await publicClient.getBlockNumber();
|
|
1055
|
+
const fromBlock = currentBlock > blocks ? currentBlock - blocks : 0n;
|
|
1056
|
+
const historical = await watcher.getHistoricalDeployments({
|
|
1057
|
+
fromBlock,
|
|
1058
|
+
tokenAdmin: adminFilter,
|
|
1059
|
+
});
|
|
1060
|
+
if (historical.length > 0) {
|
|
1061
|
+
console.log(sectionHeader(`Recent (last ${opts.history} blocks)`));
|
|
1062
|
+
for (const event of historical) {
|
|
1063
|
+
console.log(` ${c.dim('block ' + event.blockNumber.toString())} ${c.highlight(event.tokenSymbol)} ${c.muted(event.tokenName)} ${fmtAddr(event.tokenAddress, true)}`);
|
|
1064
|
+
}
|
|
1065
|
+
console.log();
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
// History scan failed, continue with live watching
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
console.log(sectionHeader('Live'));
|
|
1073
|
+
// Start live watching
|
|
1074
|
+
const unwatch = watcher.watchDeployments((event) => {
|
|
1075
|
+
const time = new Date().toLocaleTimeString();
|
|
1076
|
+
console.log(` ${c.dim(time)} ${c.success('\u2713')} ${c.highlight(event.tokenSymbol)} ${c.muted(event.tokenName)}`);
|
|
1077
|
+
console.log(` ${kv('Token', fmtAddr(event.tokenAddress))}`);
|
|
1078
|
+
console.log(` ${kv('Admin', fmtAddr(event.tokenAdmin, true))}`);
|
|
1079
|
+
console.log(` ${kv('Block', c.value(event.blockNumber.toString()))}`);
|
|
1080
|
+
console.log();
|
|
1081
|
+
}, { tokenAdmin: adminFilter });
|
|
1082
|
+
// Keep process alive until Ctrl+C
|
|
1083
|
+
process.on('SIGINT', () => {
|
|
1084
|
+
unwatch();
|
|
1085
|
+
console.log();
|
|
1086
|
+
console.log(c.muted(' Stopped watching.'));
|
|
1087
|
+
console.log();
|
|
1088
|
+
process.exit(0);
|
|
1089
|
+
});
|
|
1090
|
+
// Keep the process alive
|
|
1091
|
+
await new Promise(() => { });
|
|
1092
|
+
});
|
|
1093
|
+
// ============================================================================
|
|
1094
|
+
// Config Command
|
|
1095
|
+
// ============================================================================
|
|
1096
|
+
program
|
|
1097
|
+
.command('config')
|
|
1098
|
+
.description('Show or set CLI configuration')
|
|
1099
|
+
.option('--show', 'Show current configuration')
|
|
1100
|
+
.option('--network <network>', 'Set default network (sepolia or mainnet)')
|
|
1101
|
+
.option('--private-key <key>', 'Set private key')
|
|
1102
|
+
.option('--rpc-sepolia <url>', 'Set Sepolia RPC URL')
|
|
1103
|
+
.option('--rpc-mainnet <url>', 'Set Mainnet RPC URL')
|
|
1104
|
+
.option('--clear', 'Clear all configuration')
|
|
1105
|
+
.action((opts) => {
|
|
1106
|
+
const config = loadConfig();
|
|
1107
|
+
if (opts.clear) {
|
|
1108
|
+
saveConfig({});
|
|
1109
|
+
console.log(` ${c.success('\u2713')} Configuration cleared`);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (opts.network) {
|
|
1113
|
+
if (opts.network !== 'sepolia' && opts.network !== 'mainnet') {
|
|
1114
|
+
console.error(c.error('Invalid network. Use "sepolia" or "mainnet"'));
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
config.network = opts.network;
|
|
1118
|
+
}
|
|
1119
|
+
if (opts.privateKey) {
|
|
1120
|
+
config.privateKey = opts.privateKey.startsWith('0x')
|
|
1121
|
+
? opts.privateKey
|
|
1122
|
+
: `0x${opts.privateKey}`;
|
|
1123
|
+
}
|
|
1124
|
+
if (opts.rpcSepolia) {
|
|
1125
|
+
config.rpc = config.rpc || {};
|
|
1126
|
+
config.rpc.sepolia = opts.rpcSepolia;
|
|
1127
|
+
}
|
|
1128
|
+
if (opts.rpcMainnet) {
|
|
1129
|
+
config.rpc = config.rpc || {};
|
|
1130
|
+
config.rpc.mainnet = opts.rpcMainnet;
|
|
1131
|
+
}
|
|
1132
|
+
// If any option was set, save config
|
|
1133
|
+
if (opts.network || opts.privateKey || opts.rpcSepolia || opts.rpcMainnet) {
|
|
1134
|
+
saveConfig(config);
|
|
1135
|
+
console.log(` ${c.success('\u2713')} Configuration saved`);
|
|
1136
|
+
}
|
|
1137
|
+
// Show config (always, or if --show)
|
|
1138
|
+
if (opts.show || (!opts.network && !opts.privateKey && !opts.rpcSepolia && !opts.rpcMainnet && !opts.clear)) {
|
|
1139
|
+
console.log();
|
|
1140
|
+
console.log(sectionHeader('Configuration'));
|
|
1141
|
+
console.log(kv('Config file', c.muted(CONFIG_FILE)));
|
|
1142
|
+
console.log(kv('Network', config.network ? networkBadge(config.network) : c.muted('sepolia (default)')));
|
|
1143
|
+
console.log(kv('Private key', config.privateKey
|
|
1144
|
+
? `${dot('green')} ${c.muted('********' + config.privateKey.slice(-4))}`
|
|
1145
|
+
: `${dot('red')} ${c.muted('Not set')}`));
|
|
1146
|
+
console.log(kv('RPC Sepolia', config.rpc?.sepolia || c.muted('Default')));
|
|
1147
|
+
console.log(kv('RPC Mainnet', config.rpc?.mainnet || c.muted('Default')));
|
|
1148
|
+
console.log();
|
|
1149
|
+
console.log(sectionHeader('Environment'));
|
|
1150
|
+
const envSet = process.env.CLAWNCHER_PRIVATE_KEY;
|
|
1151
|
+
console.log(kv('CLAWNCHER_PRIVATE_KEY', envSet ? `${dot('green')} Set` : `${dot('red')} Not set`));
|
|
1152
|
+
console.log();
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
// ============================================================================
|
|
1156
|
+
// Tokens Command (API)
|
|
1157
|
+
// ============================================================================
|
|
1158
|
+
program
|
|
1159
|
+
.command('tokens')
|
|
1160
|
+
.description('List all tokens launched via Clawncher (API)')
|
|
1161
|
+
.option('-s, --symbol <symbol>', 'Filter by symbol')
|
|
1162
|
+
.option('-l, --limit <n>', 'Limit results', '20')
|
|
1163
|
+
.option('--json', 'Output as JSON')
|
|
1164
|
+
.action(async (opts) => {
|
|
1165
|
+
const spinner = ora('Fetching tokens...').start();
|
|
1166
|
+
try {
|
|
1167
|
+
const client = getClient(program.opts());
|
|
1168
|
+
const tokens = await client.getTokens();
|
|
1169
|
+
spinner.stop();
|
|
1170
|
+
let filtered = tokens;
|
|
1171
|
+
if (opts.symbol) {
|
|
1172
|
+
filtered = tokens.filter((t) => t.symbol.toLowerCase().includes(opts.symbol.toLowerCase()));
|
|
1173
|
+
}
|
|
1174
|
+
const limited = filtered.slice(0, safeParseInt(opts.limit, 'limit'));
|
|
1175
|
+
if (opts.json) {
|
|
1176
|
+
console.log(JSON.stringify(limited, null, 2));
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (limited.length === 0) {
|
|
1180
|
+
console.log(c.muted('\n No tokens found\n'));
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
console.log();
|
|
1184
|
+
console.log(styledTable(['Symbol', 'Name', 'Address', 'Agent'], limited.map((token) => [
|
|
1185
|
+
c.highlight(token.symbol),
|
|
1186
|
+
c.value(token.name),
|
|
1187
|
+
fmtAddr(token.address, true),
|
|
1188
|
+
token.agent ? c.accent2(token.agent) : c.dim('-'),
|
|
1189
|
+
])));
|
|
1190
|
+
console.log(c.dim(` Showing ${limited.length} of ${filtered.length} tokens`));
|
|
1191
|
+
}
|
|
1192
|
+
catch (err) {
|
|
1193
|
+
spinner.stop();
|
|
1194
|
+
handleError(err);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
// ============================================================================
|
|
1198
|
+
// Launches Command (API)
|
|
1199
|
+
// ============================================================================
|
|
1200
|
+
program
|
|
1201
|
+
.command('launches')
|
|
1202
|
+
.description('View launch history (API)')
|
|
1203
|
+
.option('-a, --agent <name>', 'Filter by agent name')
|
|
1204
|
+
.option('-s, --source <source>', 'Filter by source (moltbook, 4claw, moltx)')
|
|
1205
|
+
.option('-l, --limit <n>', 'Limit results', '20')
|
|
1206
|
+
.option('-o, --offset <n>', 'Offset for pagination', '0')
|
|
1207
|
+
.option('--json', 'Output as JSON')
|
|
1208
|
+
.action(async (opts) => {
|
|
1209
|
+
const spinner = ora('Fetching launches...').start();
|
|
1210
|
+
try {
|
|
1211
|
+
const client = getClient(program.opts());
|
|
1212
|
+
const response = await client.getLaunches({
|
|
1213
|
+
agent: opts.agent,
|
|
1214
|
+
source: opts.source,
|
|
1215
|
+
limit: safeParseInt(opts.limit, 'limit'),
|
|
1216
|
+
offset: safeParseInt(opts.offset, 'offset'),
|
|
1217
|
+
});
|
|
1218
|
+
spinner.stop();
|
|
1219
|
+
if (opts.json) {
|
|
1220
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (response.launches.length === 0) {
|
|
1224
|
+
console.log(c.muted('\n No launches found\n'));
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
console.log();
|
|
1228
|
+
console.log(styledTable(['Symbol', 'Agent', 'Source', 'Address', 'Date'], response.launches.map((launch) => {
|
|
1229
|
+
const agent = launch.agent || launch.agentName || '-';
|
|
1230
|
+
const address = launch.address || launch.contractAddress || '';
|
|
1231
|
+
const date = launch.created_at || launch.launchedAt || '';
|
|
1232
|
+
return [
|
|
1233
|
+
c.highlight(launch.symbol),
|
|
1234
|
+
agent !== '-' ? c.accent2(agent) : c.dim('-'),
|
|
1235
|
+
c.value(launch.source),
|
|
1236
|
+
address ? fmtAddr(address, true) : c.dim('-'),
|
|
1237
|
+
date ? c.muted(new Date(date).toLocaleDateString()) : c.dim('-'),
|
|
1238
|
+
];
|
|
1239
|
+
})));
|
|
1240
|
+
const total = response.total || response.pagination?.total || response.launches.length;
|
|
1241
|
+
console.log(c.dim(` Showing ${response.launches.length} of ${total} launches`));
|
|
1242
|
+
}
|
|
1243
|
+
catch (err) {
|
|
1244
|
+
spinner.stop();
|
|
1245
|
+
handleError(err);
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
// ============================================================================
|
|
1249
|
+
// Stats Command (API)
|
|
1250
|
+
// ============================================================================
|
|
1251
|
+
program
|
|
1252
|
+
.command('stats')
|
|
1253
|
+
.description('Get Clawnch market statistics (API)')
|
|
1254
|
+
.option('--json', 'Output as JSON')
|
|
1255
|
+
.action(async (opts) => {
|
|
1256
|
+
const spinner = ora('Fetching stats...').start();
|
|
1257
|
+
try {
|
|
1258
|
+
const client = getClient(program.opts());
|
|
1259
|
+
const stats = await client.getStats();
|
|
1260
|
+
spinner.stop();
|
|
1261
|
+
if (opts.json) {
|
|
1262
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
console.log();
|
|
1266
|
+
console.log(sectionHeader('Statistics'));
|
|
1267
|
+
console.log(kv('Total Tokens', c.accent((stats.totalTokens || stats.total_tokens || 0).toLocaleString())));
|
|
1268
|
+
console.log(kv('Total Volume', c.value(stats.totalVolume || stats.total_volume || '-')));
|
|
1269
|
+
console.log(kv('$CLAWNCH Price', c.accent(stats.clawnchPrice || '-')));
|
|
1270
|
+
console.log(kv('Market Cap', c.value(stats.clawnchMarketCap || '-')));
|
|
1271
|
+
console.log();
|
|
1272
|
+
}
|
|
1273
|
+
catch (err) {
|
|
1274
|
+
spinner.stop();
|
|
1275
|
+
handleError(err);
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
// ============================================================================
|
|
1279
|
+
// Addresses Command
|
|
1280
|
+
// ============================================================================
|
|
1281
|
+
program
|
|
1282
|
+
.command('addresses')
|
|
1283
|
+
.description('Show contract addresses for a network')
|
|
1284
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
1285
|
+
.option('--json', 'Output as JSON')
|
|
1286
|
+
.action((opts) => {
|
|
1287
|
+
const network = getNetwork(opts);
|
|
1288
|
+
const addresses = getAddresses(network);
|
|
1289
|
+
if (opts.json) {
|
|
1290
|
+
console.log(JSON.stringify(addresses, null, 2));
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
console.log();
|
|
1294
|
+
console.log(` ${c.highlight('Clawnch Contracts')} ${networkBadge(network)}`);
|
|
1295
|
+
console.log();
|
|
1296
|
+
console.log(sectionHeader('Core'));
|
|
1297
|
+
console.log(kv('Factory', fmtAddr(addresses.clawnch.factory), 18));
|
|
1298
|
+
console.log(kv('Hook', fmtAddr(addresses.clawnch.hook), 18));
|
|
1299
|
+
console.log(kv('Locker', fmtAddr(addresses.clawnch.locker), 18));
|
|
1300
|
+
console.log(kv('FeeLocker', fmtAddr(addresses.clawnch.feeLocker), 18));
|
|
1301
|
+
console.log(kv('MevModule', fmtAddr(addresses.clawnch.mevModule), 18));
|
|
1302
|
+
console.log();
|
|
1303
|
+
console.log(sectionHeader('Extensions'));
|
|
1304
|
+
console.log(kv('Vault', fmtAddr(addresses.clawnch.vault), 18));
|
|
1305
|
+
console.log(kv('AirdropV2', fmtAddr(addresses.clawnch.airdropV2), 18));
|
|
1306
|
+
console.log(kv('DevBuy', fmtAddr(addresses.clawnch.devBuy), 18));
|
|
1307
|
+
console.log();
|
|
1308
|
+
console.log(sectionHeader('Infrastructure'));
|
|
1309
|
+
console.log(kv('PoolManager', fmtAddr(addresses.infrastructure.poolManager), 18));
|
|
1310
|
+
console.log(kv('PositionManager', fmtAddr(addresses.infrastructure.positionManager), 18));
|
|
1311
|
+
console.log(kv('UniversalRouter', fmtAddr(addresses.infrastructure.universalRouter), 18));
|
|
1312
|
+
console.log(kv('WETH', fmtAddr(addresses.infrastructure.weth), 18));
|
|
1313
|
+
console.log(kv('Permit2', fmtAddr(addresses.infrastructure.permit2), 18));
|
|
1314
|
+
console.log();
|
|
1315
|
+
});
|
|
1316
|
+
// ============================================================================
|
|
1317
|
+
// Help/Info Command
|
|
1318
|
+
// ============================================================================
|
|
1319
|
+
program
|
|
1320
|
+
.command('about')
|
|
1321
|
+
.description('Show Clawncher information and links')
|
|
1322
|
+
.action(() => {
|
|
1323
|
+
const banner = [
|
|
1324
|
+
' ██████╗██╗ █████╗ ██╗ ██╗███╗ ██╗ ██████╗██╗ ██╗███████╗██████╗ ',
|
|
1325
|
+
' ██╔════╝██║ ██╔══██╗██║ ██║████╗ ██║ ██╔════╝██║ ██║██╔════╝██╔══██╗',
|
|
1326
|
+
' ██║ ██║ ███████║██║ █╗ ██║██╔██╗ ██║ ██║ ███████║█████╗ ██████╔╝',
|
|
1327
|
+
' ██║ ██║ ██╔══██║██║███╗██║██║╚██╗██║ ██║ ██╔══██║██╔══╝ ██╔══██╗',
|
|
1328
|
+
' ╚██████╗███████╗ ██║ ██║╚███╔███╔╝██║ ╚████║ ╚██████╗██║ ██║███████╗██║ ██║',
|
|
1329
|
+
' ╚═════╝╚══════╝ ╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝',
|
|
1330
|
+
];
|
|
1331
|
+
console.log();
|
|
1332
|
+
console.log(bannerGradient(banner));
|
|
1333
|
+
console.log();
|
|
1334
|
+
console.log(c.muted(' Token launch toolkit for Base, optimized for agents.'));
|
|
1335
|
+
console.log(c.dim(' 1% LP fee (80% deployer, 20% protocol) \u00B7 MEV protection \u00B7 Instant tradability'));
|
|
1336
|
+
console.log();
|
|
1337
|
+
console.log(sectionHeader('Links'));
|
|
1338
|
+
console.log(kv('Website', c.link('https://clawn.ch/er')));
|
|
1339
|
+
console.log(kv('Agent Skill', c.link('https://clawn.ch/er/skill')));
|
|
1340
|
+
console.log(kv('Tech Docs', c.link('https://clawn.ch/er/docs')));
|
|
1341
|
+
console.log();
|
|
1342
|
+
console.log(sectionHeader('Install'));
|
|
1343
|
+
console.log(kv('SDK', c.value('npm install @clawnch/clawncher-sdk')));
|
|
1344
|
+
console.log(kv('CLI', c.value('npm install -g clawncher')));
|
|
1345
|
+
console.log();
|
|
1346
|
+
console.log(sectionHeader('Quick Start'));
|
|
1347
|
+
console.log(c.dim(' $ ') + c.accent('clawncher') + c.value(' config --private-key <key>'));
|
|
1348
|
+
console.log(c.dim(' $ ') + c.accent('clawncher') + c.value(' deploy --name "My Token" --symbol MYTKN'));
|
|
1349
|
+
console.log(c.dim(' $ ') + c.accent('clawncher') + c.value(' info <token-address> --network mainnet'));
|
|
1350
|
+
console.log();
|
|
1351
|
+
console.log(c.dim(` v${VERSION}`));
|
|
1352
|
+
console.log();
|
|
1353
|
+
});
|
|
1354
|
+
// ============================================================================
|
|
1355
|
+
// Swap Commands
|
|
1356
|
+
// ============================================================================
|
|
1357
|
+
const swap = program.command('swap').description('Token swaps via 0x aggregation');
|
|
1358
|
+
swap
|
|
1359
|
+
.command('price')
|
|
1360
|
+
.description('Get an indicative swap price (no execution)')
|
|
1361
|
+
.requiredOption('--sell-token <address>', 'Token to sell (address or "ETH")')
|
|
1362
|
+
.requiredOption('--buy-token <address>', 'Token to buy (address or "ETH")')
|
|
1363
|
+
.requiredOption('--amount <amount>', 'Amount to sell (in human-readable units, e.g. "0.01")')
|
|
1364
|
+
.option('--slippage <bps>', 'Slippage tolerance in basis points (default: 100 = 1%)', '100')
|
|
1365
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
1366
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
1367
|
+
.option('--private-key <key>', 'Private key (or use CLAWNCHER_PRIVATE_KEY env)')
|
|
1368
|
+
.option('--json', 'Output as JSON')
|
|
1369
|
+
.action(async (opts) => {
|
|
1370
|
+
const spinner = ora('Fetching price...').start();
|
|
1371
|
+
try {
|
|
1372
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
1373
|
+
const network = getNetwork(opts);
|
|
1374
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
1375
|
+
const { wallet, publicClient } = createClients(network, privateKey, rpcUrl);
|
|
1376
|
+
const swapper = new ClawnchSwapper({
|
|
1377
|
+
wallet,
|
|
1378
|
+
publicClient,
|
|
1379
|
+
network,
|
|
1380
|
+
apiBaseUrl: getClawnchApiUrl(),
|
|
1381
|
+
});
|
|
1382
|
+
// Resolve token addresses
|
|
1383
|
+
const sellToken = opts.sellToken.toLowerCase() === 'eth'
|
|
1384
|
+
? NATIVE_TOKEN_ADDRESS
|
|
1385
|
+
: validateAddress(opts.sellToken, 'sell token');
|
|
1386
|
+
const buyToken = opts.buyToken.toLowerCase() === 'eth'
|
|
1387
|
+
? NATIVE_TOKEN_ADDRESS
|
|
1388
|
+
: validateAddress(opts.buyToken, 'buy token');
|
|
1389
|
+
// Parse amount - get decimals for the sell token
|
|
1390
|
+
const sellDecimals = await swapper.getDecimals(sellToken);
|
|
1391
|
+
const { parseUnits } = await import('viem');
|
|
1392
|
+
const sellAmount = parseUnits(opts.amount, sellDecimals);
|
|
1393
|
+
spinner.text = 'Querying best price...';
|
|
1394
|
+
const price = await swapper.getPrice({
|
|
1395
|
+
sellToken,
|
|
1396
|
+
buyToken,
|
|
1397
|
+
sellAmount,
|
|
1398
|
+
slippageBps: safeParseInt(opts.slippage, 'slippage'),
|
|
1399
|
+
});
|
|
1400
|
+
// Get symbols for display
|
|
1401
|
+
const [sellSymbol, buySymbol, buyDecimals] = await Promise.all([
|
|
1402
|
+
swapper.getSymbol(sellToken),
|
|
1403
|
+
swapper.getSymbol(buyToken),
|
|
1404
|
+
swapper.getDecimals(buyToken),
|
|
1405
|
+
]);
|
|
1406
|
+
spinner.stop();
|
|
1407
|
+
const { formatUnits: fmtUnits } = await import('viem');
|
|
1408
|
+
if (opts.json) {
|
|
1409
|
+
console.log(JSON.stringify({
|
|
1410
|
+
sellToken: opts.sellToken,
|
|
1411
|
+
buyToken: opts.buyToken,
|
|
1412
|
+
sellAmount: sellAmount.toString(),
|
|
1413
|
+
buyAmount: price.buyAmount.toString(),
|
|
1414
|
+
minBuyAmount: price.minBuyAmount.toString(),
|
|
1415
|
+
sellSymbol,
|
|
1416
|
+
buySymbol,
|
|
1417
|
+
sellFormatted: opts.amount,
|
|
1418
|
+
buyFormatted: fmtUnits(price.buyAmount, buyDecimals),
|
|
1419
|
+
minBuyFormatted: fmtUnits(price.minBuyAmount, buyDecimals),
|
|
1420
|
+
gasEstimate: price.gas.toString(),
|
|
1421
|
+
liquidityAvailable: price.liquidityAvailable,
|
|
1422
|
+
route: price.route,
|
|
1423
|
+
}, null, 2));
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
console.log();
|
|
1427
|
+
console.log(` ${c.highlight('Swap Price')}`);
|
|
1428
|
+
console.log();
|
|
1429
|
+
console.log(sectionHeader('Quote'));
|
|
1430
|
+
console.log(kv('Sell', `${c.accent(opts.amount)} ${c.value(sellSymbol)}`));
|
|
1431
|
+
console.log(kv('Buy', `${c.accent(fmtUnits(price.buyAmount, buyDecimals))} ${c.value(buySymbol)}`));
|
|
1432
|
+
console.log(kv('Min Receive', `${c.muted(fmtUnits(price.minBuyAmount, buyDecimals))} ${c.muted(buySymbol)}`));
|
|
1433
|
+
console.log(kv('Liquidity', price.liquidityAvailable ? `${dot('green')} Available` : `${dot('red')} Insufficient`));
|
|
1434
|
+
console.log(kv('Gas Estimate', c.value(price.gas.toLocaleString())));
|
|
1435
|
+
console.log(kv('Network', networkBadge(network)));
|
|
1436
|
+
if (price.route.fills.length > 0) {
|
|
1437
|
+
console.log();
|
|
1438
|
+
console.log(sectionHeader('Route'));
|
|
1439
|
+
for (const fill of price.route.fills) {
|
|
1440
|
+
const pct = (parseInt(fill.proportionBps) / 100).toFixed(1);
|
|
1441
|
+
console.log(` ${c.accent(pct + '%')} ${c.dim('\u2500')} ${c.value(fill.source)}`);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
console.log();
|
|
1445
|
+
}
|
|
1446
|
+
catch (err) {
|
|
1447
|
+
spinner.stop();
|
|
1448
|
+
handleError(err);
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
swap
|
|
1452
|
+
.command('exec')
|
|
1453
|
+
.description('Execute a token swap')
|
|
1454
|
+
.requiredOption('--sell-token <address>', 'Token to sell (address or "ETH")')
|
|
1455
|
+
.requiredOption('--buy-token <address>', 'Token to buy (address or "ETH")')
|
|
1456
|
+
.requiredOption('--amount <amount>', 'Amount to sell (in human-readable units, e.g. "0.01")')
|
|
1457
|
+
.option('--slippage <bps>', 'Slippage tolerance in basis points (default: 100 = 1%)', '100')
|
|
1458
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
1459
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
1460
|
+
.option('--private-key <key>', 'Private key (or use CLAWNCHER_PRIVATE_KEY env)')
|
|
1461
|
+
.option('--json', 'Output as JSON')
|
|
1462
|
+
.action(async (opts) => {
|
|
1463
|
+
const spinner = ora('Preparing swap...').start();
|
|
1464
|
+
try {
|
|
1465
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
1466
|
+
const network = getNetwork(opts);
|
|
1467
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
1468
|
+
const { wallet, publicClient, account } = createClients(network, privateKey, rpcUrl);
|
|
1469
|
+
const swapper = new ClawnchSwapper({
|
|
1470
|
+
wallet,
|
|
1471
|
+
publicClient,
|
|
1472
|
+
network,
|
|
1473
|
+
apiBaseUrl: getClawnchApiUrl(),
|
|
1474
|
+
});
|
|
1475
|
+
const sellToken = opts.sellToken.toLowerCase() === 'eth'
|
|
1476
|
+
? NATIVE_TOKEN_ADDRESS
|
|
1477
|
+
: validateAddress(opts.sellToken, 'sell token');
|
|
1478
|
+
const buyToken = opts.buyToken.toLowerCase() === 'eth'
|
|
1479
|
+
? NATIVE_TOKEN_ADDRESS
|
|
1480
|
+
: validateAddress(opts.buyToken, 'buy token');
|
|
1481
|
+
const sellDecimals = await swapper.getDecimals(sellToken);
|
|
1482
|
+
const { parseUnits } = await import('viem');
|
|
1483
|
+
const sellAmount = parseUnits(opts.amount, sellDecimals);
|
|
1484
|
+
const [sellSymbol, buySymbol, buyDecimals] = await Promise.all([
|
|
1485
|
+
swapper.getSymbol(sellToken),
|
|
1486
|
+
swapper.getSymbol(buyToken),
|
|
1487
|
+
swapper.getDecimals(buyToken),
|
|
1488
|
+
]);
|
|
1489
|
+
spinner.text = `Swapping ${opts.amount} ${sellSymbol} for ${buySymbol}...`;
|
|
1490
|
+
const result = await swapper.swap({
|
|
1491
|
+
sellToken,
|
|
1492
|
+
buyToken,
|
|
1493
|
+
sellAmount,
|
|
1494
|
+
slippageBps: safeParseInt(opts.slippage, 'slippage'),
|
|
1495
|
+
});
|
|
1496
|
+
spinner.stop();
|
|
1497
|
+
const { formatUnits: fmtUnits } = await import('viem');
|
|
1498
|
+
if (opts.json) {
|
|
1499
|
+
console.log(JSON.stringify({
|
|
1500
|
+
success: true,
|
|
1501
|
+
txHash: result.txHash,
|
|
1502
|
+
sellToken: opts.sellToken,
|
|
1503
|
+
buyToken: opts.buyToken,
|
|
1504
|
+
sellAmount: result.sellAmount.toString(),
|
|
1505
|
+
buyAmount: result.buyAmount.toString(),
|
|
1506
|
+
sellFormatted: fmtUnits(result.sellAmount, sellDecimals),
|
|
1507
|
+
buyFormatted: fmtUnits(result.buyAmount, buyDecimals),
|
|
1508
|
+
sellSymbol,
|
|
1509
|
+
buySymbol,
|
|
1510
|
+
gasUsed: result.gasUsed.toString(),
|
|
1511
|
+
network,
|
|
1512
|
+
}, null, 2));
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
console.log();
|
|
1516
|
+
console.log(` ${c.success('\u2713')} ${c.highlight('Swap executed')}`);
|
|
1517
|
+
console.log();
|
|
1518
|
+
console.log(sectionHeader('Result'));
|
|
1519
|
+
console.log(kv('Sold', `${c.value(fmtUnits(result.sellAmount, sellDecimals))} ${c.muted(sellSymbol)}`));
|
|
1520
|
+
console.log(kv('Received', `${c.accent(fmtUnits(result.buyAmount, buyDecimals))} ${c.value(buySymbol)}`));
|
|
1521
|
+
console.log(kv('Transaction', c.muted(result.txHash)));
|
|
1522
|
+
console.log(kv('Gas Used', c.value(result.gasUsed.toLocaleString())));
|
|
1523
|
+
console.log(kv('Wallet', fmtAddr(account.address)));
|
|
1524
|
+
console.log(kv('Network', networkBadge(network)));
|
|
1525
|
+
const explorerUrl = network === 'mainnet'
|
|
1526
|
+
? `https://basescan.org/tx/${result.txHash}`
|
|
1527
|
+
: `https://sepolia.basescan.org/tx/${result.txHash}`;
|
|
1528
|
+
console.log();
|
|
1529
|
+
console.log(` ${c.link(explorerUrl)}`);
|
|
1530
|
+
console.log();
|
|
1531
|
+
}
|
|
1532
|
+
catch (err) {
|
|
1533
|
+
spinner.stop();
|
|
1534
|
+
handleError(err);
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
// ============================================================================
|
|
1538
|
+
// Liquidity Commands
|
|
1539
|
+
// ============================================================================
|
|
1540
|
+
const liquidity = program.command('liquidity').description('Uniswap V3/V4 liquidity management');
|
|
1541
|
+
liquidity
|
|
1542
|
+
.command('positions')
|
|
1543
|
+
.description('List V3 liquidity positions for a wallet')
|
|
1544
|
+
.argument('[wallet]', 'Wallet address (defaults to configured wallet)')
|
|
1545
|
+
.option('--token <address>', 'Filter by token address')
|
|
1546
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
1547
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
1548
|
+
.option('--private-key <key>', 'Private key (or use CLAWNCHER_PRIVATE_KEY env)')
|
|
1549
|
+
.option('--json', 'Output as JSON')
|
|
1550
|
+
.action(async (walletArg, opts) => {
|
|
1551
|
+
const spinner = ora('Fetching positions...').start();
|
|
1552
|
+
try {
|
|
1553
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
1554
|
+
const network = getNetwork(opts);
|
|
1555
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
1556
|
+
const { wallet, publicClient, account } = createClients(network, privateKey, rpcUrl);
|
|
1557
|
+
const liq = new ClawnchLiquidity({
|
|
1558
|
+
wallet,
|
|
1559
|
+
publicClient,
|
|
1560
|
+
network,
|
|
1561
|
+
});
|
|
1562
|
+
const walletAddr = walletArg
|
|
1563
|
+
? validateAddress(walletArg, 'wallet')
|
|
1564
|
+
: account.address;
|
|
1565
|
+
let positions;
|
|
1566
|
+
if (opts.token) {
|
|
1567
|
+
const tokenAddr = validateAddress(opts.token, 'token');
|
|
1568
|
+
spinner.text = `Finding positions for token ${fmtAddr(tokenAddr, true)}...`;
|
|
1569
|
+
positions = await liq.getPositionsForToken(tokenAddr, walletAddr);
|
|
1570
|
+
}
|
|
1571
|
+
else {
|
|
1572
|
+
positions = await liq.v3GetPositionsForWallet(walletAddr);
|
|
1573
|
+
}
|
|
1574
|
+
spinner.stop();
|
|
1575
|
+
if (opts.json) {
|
|
1576
|
+
console.log(JSON.stringify(positions.map(p => ({
|
|
1577
|
+
tokenId: p.tokenId.toString(),
|
|
1578
|
+
version: p.version,
|
|
1579
|
+
token0: p.token0,
|
|
1580
|
+
token1: p.token1,
|
|
1581
|
+
fee: p.fee,
|
|
1582
|
+
tickLower: p.tickLower,
|
|
1583
|
+
tickUpper: p.tickUpper,
|
|
1584
|
+
liquidity: p.liquidity.toString(),
|
|
1585
|
+
unclaimedFees: {
|
|
1586
|
+
token0: p.unclaimedFees.token0.toString(),
|
|
1587
|
+
token1: p.unclaimedFees.token1.toString(),
|
|
1588
|
+
},
|
|
1589
|
+
})), null, 2));
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
console.log();
|
|
1593
|
+
console.log(` ${c.highlight('Liquidity Positions')} ${c.muted('for')} ${fmtAddr(walletAddr, true)}`);
|
|
1594
|
+
console.log(kv('Network', networkBadge(network)));
|
|
1595
|
+
console.log();
|
|
1596
|
+
if (positions.length === 0) {
|
|
1597
|
+
console.log(c.muted(' No V3 positions found'));
|
|
1598
|
+
console.log();
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
for (const pos of positions) {
|
|
1602
|
+
const feeTier = (pos.fee / 10000).toFixed(2) + '%';
|
|
1603
|
+
const hasLiq = pos.liquidity > 0n;
|
|
1604
|
+
const hasFees = pos.unclaimedFees.token0 > 0n || pos.unclaimedFees.token1 > 0n;
|
|
1605
|
+
console.log(` ${hasLiq ? dot('green') : dot('red')} ${c.accent('#' + pos.tokenId.toString())} ${c.muted(pos.version.toUpperCase())} ${c.dim('fee=' + feeTier)}`);
|
|
1606
|
+
console.log(` ${c.label('Pair')} ${fmtAddr(pos.token0, true)} ${c.dim('/')} ${fmtAddr(pos.token1, true)}`);
|
|
1607
|
+
console.log(` ${c.label('Range')} ${c.value(pos.tickLower.toString())} ${c.dim('\u2192')} ${c.value(pos.tickUpper.toString())}`);
|
|
1608
|
+
console.log(` ${c.label('Liquidity')} ${hasLiq ? c.accent(pos.liquidity.toString()) : c.muted('0')}`);
|
|
1609
|
+
if (hasFees) {
|
|
1610
|
+
console.log(` ${c.label('Fees')} ${c.success(pos.unclaimedFees.token0.toString())} ${c.dim('/')} ${c.success(pos.unclaimedFees.token1.toString())}`);
|
|
1611
|
+
}
|
|
1612
|
+
console.log();
|
|
1613
|
+
}
|
|
1614
|
+
console.log(c.dim(` ${positions.length} position${positions.length !== 1 ? 's' : ''} found`));
|
|
1615
|
+
console.log();
|
|
1616
|
+
}
|
|
1617
|
+
catch (err) {
|
|
1618
|
+
spinner.stop();
|
|
1619
|
+
handleError(err);
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
liquidity
|
|
1623
|
+
.command('mint')
|
|
1624
|
+
.description('Create a new V3 liquidity position')
|
|
1625
|
+
.requiredOption('--token0 <address>', 'Token 0 address (lower address)')
|
|
1626
|
+
.requiredOption('--token1 <address>', 'Token 1 address (higher address)')
|
|
1627
|
+
.requiredOption('--fee <tier>', 'Fee tier: 500 (0.05%), 3000 (0.3%), or 10000 (1%)')
|
|
1628
|
+
.requiredOption('--tick-lower <tick>', 'Lower tick boundary')
|
|
1629
|
+
.requiredOption('--tick-upper <tick>', 'Upper tick boundary')
|
|
1630
|
+
.requiredOption('--amount0 <amount>', 'Amount of token0 (human-readable)')
|
|
1631
|
+
.requiredOption('--amount1 <amount>', 'Amount of token1 (human-readable)')
|
|
1632
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
1633
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
1634
|
+
.option('--private-key <key>', 'Private key (or use CLAWNCHER_PRIVATE_KEY env)')
|
|
1635
|
+
.option('--json', 'Output as JSON')
|
|
1636
|
+
.action(async (opts) => {
|
|
1637
|
+
const spinner = ora('Preparing to mint position...').start();
|
|
1638
|
+
try {
|
|
1639
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
1640
|
+
const network = getNetwork(opts);
|
|
1641
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
1642
|
+
const { wallet, publicClient, account } = createClients(network, privateKey, rpcUrl);
|
|
1643
|
+
const liq = new ClawnchLiquidity({
|
|
1644
|
+
wallet,
|
|
1645
|
+
publicClient,
|
|
1646
|
+
network,
|
|
1647
|
+
});
|
|
1648
|
+
const token0 = validateAddress(opts.token0, 'token0');
|
|
1649
|
+
const token1 = validateAddress(opts.token1, 'token1');
|
|
1650
|
+
const fee = safeParseInt(opts.fee, 'fee');
|
|
1651
|
+
const tickLower = safeParseInt(opts.tickLower, 'tick-lower');
|
|
1652
|
+
const tickUpper = safeParseInt(opts.tickUpper, 'tick-upper');
|
|
1653
|
+
// Get decimals for parsing amounts
|
|
1654
|
+
const { parseUnits } = await import('viem');
|
|
1655
|
+
const [dec0, dec1] = await Promise.all([
|
|
1656
|
+
readDecimals(publicClient, token0),
|
|
1657
|
+
readDecimals(publicClient, token1),
|
|
1658
|
+
]);
|
|
1659
|
+
const amount0Desired = parseUnits(opts.amount0, dec0);
|
|
1660
|
+
const amount1Desired = parseUnits(opts.amount1, dec1);
|
|
1661
|
+
spinner.text = 'Minting V3 position...';
|
|
1662
|
+
const result = await liq.v3MintPosition({
|
|
1663
|
+
token0,
|
|
1664
|
+
token1,
|
|
1665
|
+
fee,
|
|
1666
|
+
tickLower,
|
|
1667
|
+
tickUpper,
|
|
1668
|
+
amount0Desired,
|
|
1669
|
+
amount1Desired,
|
|
1670
|
+
});
|
|
1671
|
+
spinner.stop();
|
|
1672
|
+
const { formatUnits: fmtUnits } = await import('viem');
|
|
1673
|
+
if (opts.json) {
|
|
1674
|
+
console.log(JSON.stringify({
|
|
1675
|
+
success: true,
|
|
1676
|
+
txHash: result.txHash,
|
|
1677
|
+
tokenId: result.tokenId.toString(),
|
|
1678
|
+
amount0: result.amount0.toString(),
|
|
1679
|
+
amount1: result.amount1.toString(),
|
|
1680
|
+
liquidity: result.liquidity.toString(),
|
|
1681
|
+
network,
|
|
1682
|
+
}, null, 2));
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
console.log();
|
|
1686
|
+
console.log(` ${c.success('\u2713')} ${c.highlight('V3 position minted')}`);
|
|
1687
|
+
console.log();
|
|
1688
|
+
console.log(sectionHeader('Position'));
|
|
1689
|
+
console.log(kv('Token ID', c.accent('#' + result.tokenId.toString())));
|
|
1690
|
+
console.log(kv('Pair', `${fmtAddr(token0, true)} ${c.dim('/')} ${fmtAddr(token1, true)}`));
|
|
1691
|
+
console.log(kv('Fee Tier', c.value((fee / 10000).toFixed(2) + '%')));
|
|
1692
|
+
console.log(kv('Range', `${c.value(tickLower.toString())} ${c.dim('\u2192')} ${c.value(tickUpper.toString())}`));
|
|
1693
|
+
console.log(kv('Deposited', `${c.accent(fmtUnits(result.amount0, dec0))} + ${c.accent(fmtUnits(result.amount1, dec1))}`));
|
|
1694
|
+
console.log(kv('Liquidity', c.value(result.liquidity.toString())));
|
|
1695
|
+
console.log(kv('Transaction', c.muted(result.txHash)));
|
|
1696
|
+
console.log(kv('Network', networkBadge(network)));
|
|
1697
|
+
const explorerUrl = network === 'mainnet'
|
|
1698
|
+
? `https://basescan.org/tx/${result.txHash}`
|
|
1699
|
+
: `https://sepolia.basescan.org/tx/${result.txHash}`;
|
|
1700
|
+
console.log();
|
|
1701
|
+
console.log(` ${c.link(explorerUrl)}`);
|
|
1702
|
+
console.log();
|
|
1703
|
+
}
|
|
1704
|
+
catch (err) {
|
|
1705
|
+
spinner.stop();
|
|
1706
|
+
handleError(err);
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
liquidity
|
|
1710
|
+
.command('add')
|
|
1711
|
+
.description('Add liquidity to an existing V3 position')
|
|
1712
|
+
.requiredOption('--id <tokenId>', 'Position NFT token ID')
|
|
1713
|
+
.requiredOption('--amount0 <amount>', 'Amount of token0 (human-readable)')
|
|
1714
|
+
.requiredOption('--amount1 <amount>', 'Amount of token1 (human-readable)')
|
|
1715
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
1716
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
1717
|
+
.option('--private-key <key>', 'Private key (or use CLAWNCHER_PRIVATE_KEY env)')
|
|
1718
|
+
.option('--json', 'Output as JSON')
|
|
1719
|
+
.action(async (opts) => {
|
|
1720
|
+
const spinner = ora('Adding liquidity...').start();
|
|
1721
|
+
try {
|
|
1722
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
1723
|
+
const network = getNetwork(opts);
|
|
1724
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
1725
|
+
const { wallet, publicClient } = createClients(network, privateKey, rpcUrl);
|
|
1726
|
+
const liq = new ClawnchLiquidity({
|
|
1727
|
+
wallet,
|
|
1728
|
+
publicClient,
|
|
1729
|
+
network,
|
|
1730
|
+
});
|
|
1731
|
+
const tokenId = BigInt(opts.id);
|
|
1732
|
+
// Get position to know tokens and decimals
|
|
1733
|
+
spinner.text = 'Reading position...';
|
|
1734
|
+
const position = await liq.v3GetPosition(tokenId);
|
|
1735
|
+
const { parseUnits, formatUnits: fmtUnits } = await import('viem');
|
|
1736
|
+
const [dec0, dec1] = await Promise.all([
|
|
1737
|
+
readDecimals(publicClient, position.token0),
|
|
1738
|
+
readDecimals(publicClient, position.token1),
|
|
1739
|
+
]);
|
|
1740
|
+
const amount0Desired = parseUnits(opts.amount0, dec0);
|
|
1741
|
+
const amount1Desired = parseUnits(opts.amount1, dec1);
|
|
1742
|
+
spinner.text = `Adding liquidity to position #${opts.id}...`;
|
|
1743
|
+
const result = await liq.v3AddLiquidity(tokenId, {
|
|
1744
|
+
amount0Desired,
|
|
1745
|
+
amount1Desired,
|
|
1746
|
+
});
|
|
1747
|
+
spinner.stop();
|
|
1748
|
+
if (opts.json) {
|
|
1749
|
+
console.log(JSON.stringify({
|
|
1750
|
+
success: true,
|
|
1751
|
+
txHash: result.txHash,
|
|
1752
|
+
tokenId: opts.id,
|
|
1753
|
+
amount0: result.amount0.toString(),
|
|
1754
|
+
amount1: result.amount1.toString(),
|
|
1755
|
+
network,
|
|
1756
|
+
}, null, 2));
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
console.log();
|
|
1760
|
+
console.log(` ${c.success('\u2713')} ${c.highlight('Liquidity added')}`);
|
|
1761
|
+
console.log();
|
|
1762
|
+
console.log(sectionHeader('Result'));
|
|
1763
|
+
console.log(kv('Position', c.accent('#' + opts.id)));
|
|
1764
|
+
console.log(kv('Deposited', `${c.accent(fmtUnits(result.amount0, dec0))} + ${c.accent(fmtUnits(result.amount1, dec1))}`));
|
|
1765
|
+
console.log(kv('Transaction', c.muted(result.txHash)));
|
|
1766
|
+
console.log(kv('Network', networkBadge(network)));
|
|
1767
|
+
const explorerUrl = network === 'mainnet'
|
|
1768
|
+
? `https://basescan.org/tx/${result.txHash}`
|
|
1769
|
+
: `https://sepolia.basescan.org/tx/${result.txHash}`;
|
|
1770
|
+
console.log();
|
|
1771
|
+
console.log(` ${c.link(explorerUrl)}`);
|
|
1772
|
+
console.log();
|
|
1773
|
+
}
|
|
1774
|
+
catch (err) {
|
|
1775
|
+
spinner.stop();
|
|
1776
|
+
handleError(err);
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
liquidity
|
|
1780
|
+
.command('remove')
|
|
1781
|
+
.description('Remove liquidity from a V3 position')
|
|
1782
|
+
.requiredOption('--id <tokenId>', 'Position NFT token ID')
|
|
1783
|
+
.option('--percent <pct>', 'Percentage to remove (1-100, default: 100)', '100')
|
|
1784
|
+
.option('--burn', 'Burn the position NFT after full removal')
|
|
1785
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
1786
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
1787
|
+
.option('--private-key <key>', 'Private key (or use CLAWNCHER_PRIVATE_KEY env)')
|
|
1788
|
+
.option('--json', 'Output as JSON')
|
|
1789
|
+
.action(async (opts) => {
|
|
1790
|
+
const spinner = ora('Removing liquidity...').start();
|
|
1791
|
+
try {
|
|
1792
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
1793
|
+
const network = getNetwork(opts);
|
|
1794
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
1795
|
+
const { wallet, publicClient } = createClients(network, privateKey, rpcUrl);
|
|
1796
|
+
const liq = new ClawnchLiquidity({
|
|
1797
|
+
wallet,
|
|
1798
|
+
publicClient,
|
|
1799
|
+
network,
|
|
1800
|
+
});
|
|
1801
|
+
const tokenId = BigInt(opts.id);
|
|
1802
|
+
const percent = safeParseInt(opts.percent, 'percent');
|
|
1803
|
+
if (percent < 1 || percent > 100) {
|
|
1804
|
+
spinner.stop();
|
|
1805
|
+
console.error(c.error('Percent must be between 1 and 100'));
|
|
1806
|
+
process.exit(1);
|
|
1807
|
+
}
|
|
1808
|
+
// Get position to know tokens
|
|
1809
|
+
spinner.text = 'Reading position...';
|
|
1810
|
+
const position = await liq.v3GetPosition(tokenId);
|
|
1811
|
+
const { formatUnits: fmtUnits } = await import('viem');
|
|
1812
|
+
const [dec0, dec1] = await Promise.all([
|
|
1813
|
+
readDecimals(publicClient, position.token0),
|
|
1814
|
+
readDecimals(publicClient, position.token1),
|
|
1815
|
+
]);
|
|
1816
|
+
spinner.text = `Removing ${percent}% liquidity from position #${opts.id}...`;
|
|
1817
|
+
const result = await liq.v3RemoveLiquidity(tokenId, {
|
|
1818
|
+
percentageToRemove: percent / 100,
|
|
1819
|
+
burnToken: opts.burn,
|
|
1820
|
+
});
|
|
1821
|
+
spinner.stop();
|
|
1822
|
+
if (opts.json) {
|
|
1823
|
+
console.log(JSON.stringify({
|
|
1824
|
+
success: true,
|
|
1825
|
+
txHash: result.txHash,
|
|
1826
|
+
tokenId: opts.id,
|
|
1827
|
+
percentRemoved: percent,
|
|
1828
|
+
amount0: result.amount0.toString(),
|
|
1829
|
+
amount1: result.amount1.toString(),
|
|
1830
|
+
burned: opts.burn && percent === 100,
|
|
1831
|
+
network,
|
|
1832
|
+
}, null, 2));
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
console.log();
|
|
1836
|
+
console.log(` ${c.success('\u2713')} ${c.highlight('Liquidity removed')}`);
|
|
1837
|
+
console.log();
|
|
1838
|
+
console.log(sectionHeader('Result'));
|
|
1839
|
+
console.log(kv('Position', c.accent('#' + opts.id)));
|
|
1840
|
+
console.log(kv('Removed', c.value(percent + '%')));
|
|
1841
|
+
console.log(kv('Received', `${c.accent(fmtUnits(result.amount0, dec0))} + ${c.accent(fmtUnits(result.amount1, dec1))}`));
|
|
1842
|
+
if (opts.burn && percent === 100) {
|
|
1843
|
+
console.log(kv('NFT', `${dot('red')} Burned`));
|
|
1844
|
+
}
|
|
1845
|
+
console.log(kv('Transaction', c.muted(result.txHash)));
|
|
1846
|
+
console.log(kv('Network', networkBadge(network)));
|
|
1847
|
+
const explorerUrl = network === 'mainnet'
|
|
1848
|
+
? `https://basescan.org/tx/${result.txHash}`
|
|
1849
|
+
: `https://sepolia.basescan.org/tx/${result.txHash}`;
|
|
1850
|
+
console.log();
|
|
1851
|
+
console.log(` ${c.link(explorerUrl)}`);
|
|
1852
|
+
console.log();
|
|
1853
|
+
}
|
|
1854
|
+
catch (err) {
|
|
1855
|
+
spinner.stop();
|
|
1856
|
+
handleError(err);
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
liquidity
|
|
1860
|
+
.command('fees')
|
|
1861
|
+
.description('View or collect unclaimed fees from a V3 position')
|
|
1862
|
+
.requiredOption('--id <tokenId>', 'Position NFT token ID')
|
|
1863
|
+
.option('--collect', 'Collect the fees (otherwise just shows them)')
|
|
1864
|
+
.option('--network <network>', 'Network: sepolia or mainnet', 'sepolia')
|
|
1865
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
1866
|
+
.option('--private-key <key>', 'Private key (or use CLAWNCHER_PRIVATE_KEY env)')
|
|
1867
|
+
.option('--json', 'Output as JSON')
|
|
1868
|
+
.action(async (opts) => {
|
|
1869
|
+
const spinner = ora('Checking position fees...').start();
|
|
1870
|
+
try {
|
|
1871
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
1872
|
+
const network = getNetwork(opts);
|
|
1873
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
1874
|
+
const { wallet, publicClient } = createClients(network, privateKey, rpcUrl);
|
|
1875
|
+
const liq = new ClawnchLiquidity({
|
|
1876
|
+
wallet,
|
|
1877
|
+
publicClient,
|
|
1878
|
+
network,
|
|
1879
|
+
});
|
|
1880
|
+
const tokenId = BigInt(opts.id);
|
|
1881
|
+
const position = await liq.v3GetPosition(tokenId);
|
|
1882
|
+
const { formatUnits: fmtUnits } = await import('viem');
|
|
1883
|
+
const [dec0, dec1, sym0, sym1] = await Promise.all([
|
|
1884
|
+
readDecimals(publicClient, position.token0),
|
|
1885
|
+
readDecimals(publicClient, position.token1),
|
|
1886
|
+
readSymbol(publicClient, position.token0),
|
|
1887
|
+
readSymbol(publicClient, position.token1),
|
|
1888
|
+
]);
|
|
1889
|
+
if (opts.collect) {
|
|
1890
|
+
spinner.text = `Collecting fees from position #${opts.id}...`;
|
|
1891
|
+
const result = await liq.v3CollectFees(tokenId);
|
|
1892
|
+
spinner.stop();
|
|
1893
|
+
if (opts.json) {
|
|
1894
|
+
console.log(JSON.stringify({
|
|
1895
|
+
success: true,
|
|
1896
|
+
txHash: result.txHash,
|
|
1897
|
+
tokenId: opts.id,
|
|
1898
|
+
amount0: result.amount0.toString(),
|
|
1899
|
+
amount1: result.amount1.toString(),
|
|
1900
|
+
symbol0: sym0,
|
|
1901
|
+
symbol1: sym1,
|
|
1902
|
+
formatted0: fmtUnits(result.amount0, dec0),
|
|
1903
|
+
formatted1: fmtUnits(result.amount1, dec1),
|
|
1904
|
+
network,
|
|
1905
|
+
}, null, 2));
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
console.log();
|
|
1909
|
+
console.log(` ${c.success('\u2713')} ${c.highlight('Fees collected')}`);
|
|
1910
|
+
console.log();
|
|
1911
|
+
console.log(sectionHeader('Result'));
|
|
1912
|
+
console.log(kv('Position', c.accent('#' + opts.id)));
|
|
1913
|
+
console.log(kv(sym0, c.accent(fmtUnits(result.amount0, dec0))));
|
|
1914
|
+
console.log(kv(sym1, c.accent(fmtUnits(result.amount1, dec1))));
|
|
1915
|
+
console.log(kv('Transaction', c.muted(result.txHash)));
|
|
1916
|
+
console.log(kv('Network', networkBadge(network)));
|
|
1917
|
+
const explorerUrl = network === 'mainnet'
|
|
1918
|
+
? `https://basescan.org/tx/${result.txHash}`
|
|
1919
|
+
: `https://sepolia.basescan.org/tx/${result.txHash}`;
|
|
1920
|
+
console.log();
|
|
1921
|
+
console.log(` ${c.link(explorerUrl)}`);
|
|
1922
|
+
console.log();
|
|
1923
|
+
}
|
|
1924
|
+
else {
|
|
1925
|
+
spinner.stop();
|
|
1926
|
+
const hasFees = position.unclaimedFees.token0 > 0n || position.unclaimedFees.token1 > 0n;
|
|
1927
|
+
if (opts.json) {
|
|
1928
|
+
console.log(JSON.stringify({
|
|
1929
|
+
tokenId: opts.id,
|
|
1930
|
+
token0: { address: position.token0, symbol: sym0, fees: position.unclaimedFees.token0.toString(), formatted: fmtUnits(position.unclaimedFees.token0, dec0) },
|
|
1931
|
+
token1: { address: position.token1, symbol: sym1, fees: position.unclaimedFees.token1.toString(), formatted: fmtUnits(position.unclaimedFees.token1, dec1) },
|
|
1932
|
+
hasFees,
|
|
1933
|
+
network,
|
|
1934
|
+
}, null, 2));
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
console.log();
|
|
1938
|
+
console.log(` ${c.highlight('Position Fees')} ${c.accent('#' + opts.id)}`);
|
|
1939
|
+
console.log();
|
|
1940
|
+
console.log(sectionHeader('Unclaimed'));
|
|
1941
|
+
console.log(kv(sym0, hasFees ? c.accent(fmtUnits(position.unclaimedFees.token0, dec0)) : c.muted('0')));
|
|
1942
|
+
console.log(kv(sym1, hasFees ? c.accent(fmtUnits(position.unclaimedFees.token1, dec1)) : c.muted('0')));
|
|
1943
|
+
console.log(kv('Network', networkBadge(network)));
|
|
1944
|
+
console.log();
|
|
1945
|
+
if (hasFees) {
|
|
1946
|
+
console.log(c.muted(' Run with --collect to claim these fees'));
|
|
1947
|
+
console.log();
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
catch (err) {
|
|
1952
|
+
spinner.stop();
|
|
1953
|
+
handleError(err);
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
// ============================================================================
|
|
1957
|
+
// Agent Commands (Verified Launches)
|
|
1958
|
+
// ============================================================================
|
|
1959
|
+
const agent = program.command('agent').description('Verified agent registration and management');
|
|
1960
|
+
agent
|
|
1961
|
+
.command('register')
|
|
1962
|
+
.description('Register as a verified agent on Clawnch')
|
|
1963
|
+
.requiredOption('--name <name>', 'Agent display name')
|
|
1964
|
+
.requiredOption('--description <desc>', 'Short description of your agent')
|
|
1965
|
+
.option('--network <network>', 'Network (mainnet/sepolia)', 'mainnet')
|
|
1966
|
+
.option('--private-key <key>', 'Private key')
|
|
1967
|
+
.option('--rpc <url>', 'RPC URL')
|
|
1968
|
+
.option('--api-url <url>', 'Clawnch API URL')
|
|
1969
|
+
.option('--json', 'Output as JSON')
|
|
1970
|
+
.action(async (opts) => {
|
|
1971
|
+
const spinner = ora('Registering agent...').start();
|
|
1972
|
+
try {
|
|
1973
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
1974
|
+
const network = getNetwork(opts);
|
|
1975
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
1976
|
+
const { wallet, publicClient, account } = createClients(network, privateKey, rpcUrl);
|
|
1977
|
+
spinner.text = 'Submitting registration...';
|
|
1978
|
+
const result = await ClawnchApiDeployer.register({
|
|
1979
|
+
wallet,
|
|
1980
|
+
publicClient,
|
|
1981
|
+
apiBaseUrl: opts.apiUrl || getClawnchApiUrl(),
|
|
1982
|
+
}, {
|
|
1983
|
+
name: opts.name,
|
|
1984
|
+
wallet: account.address,
|
|
1985
|
+
description: opts.description,
|
|
1986
|
+
});
|
|
1987
|
+
spinner.succeed('Agent registered and verified');
|
|
1988
|
+
if (opts.json) {
|
|
1989
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1990
|
+
}
|
|
1991
|
+
else {
|
|
1992
|
+
console.log();
|
|
1993
|
+
console.log(kv('Agent ID', c.value(result.agentId)));
|
|
1994
|
+
console.log(kv('Wallet', c.value(result.wallet)));
|
|
1995
|
+
console.log(kv('API Key', c.accent(result.apiKey)));
|
|
1996
|
+
console.log();
|
|
1997
|
+
console.log(c.warn(' Save your API key — it will not be shown again.'));
|
|
1998
|
+
console.log(c.muted(' Next steps:'));
|
|
1999
|
+
console.log(c.muted(' 1. Run: clawncher agent approve --api-key <key>'));
|
|
2000
|
+
console.log(c.muted(' 2. Deploy: clawncher deploy --verified --api-key <key>'));
|
|
2001
|
+
console.log();
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
catch (err) {
|
|
2005
|
+
spinner.stop();
|
|
2006
|
+
handleError(err);
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
agent
|
|
2010
|
+
.command('approve')
|
|
2011
|
+
.description('Approve $CLAWNCH spend for verified launches (one-time)')
|
|
2012
|
+
.requiredOption('--api-key <key>', 'Agent API key')
|
|
2013
|
+
.option('--network <network>', 'Network (mainnet/sepolia)', 'mainnet')
|
|
2014
|
+
.option('--private-key <key>', 'Private key')
|
|
2015
|
+
.option('--rpc <url>', 'RPC URL')
|
|
2016
|
+
.option('--api-url <url>', 'Clawnch API URL')
|
|
2017
|
+
.option('--json', 'Output as JSON')
|
|
2018
|
+
.action(async (opts) => {
|
|
2019
|
+
const spinner = ora('Approving $CLAWNCH spend...').start();
|
|
2020
|
+
try {
|
|
2021
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
2022
|
+
const network = getNetwork(opts);
|
|
2023
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
2024
|
+
const { wallet, publicClient } = createClients(network, privateKey, rpcUrl);
|
|
2025
|
+
const deployer = new ClawnchApiDeployer({
|
|
2026
|
+
apiKey: opts.apiKey,
|
|
2027
|
+
wallet,
|
|
2028
|
+
publicClient,
|
|
2029
|
+
network,
|
|
2030
|
+
apiBaseUrl: opts.apiUrl || getClawnchApiUrl(),
|
|
2031
|
+
});
|
|
2032
|
+
spinner.text = 'Sending approval transaction...';
|
|
2033
|
+
const result = await deployer.approveClawnch();
|
|
2034
|
+
spinner.succeed('$CLAWNCH spend approved');
|
|
2035
|
+
if (opts.json) {
|
|
2036
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2037
|
+
}
|
|
2038
|
+
else {
|
|
2039
|
+
console.log();
|
|
2040
|
+
console.log(kv('Tx Hash', c.value(result.txHash)));
|
|
2041
|
+
console.log(kv('Spender', c.value(result.spender)));
|
|
2042
|
+
console.log(kv('Allowance', c.accent('unlimited')));
|
|
2043
|
+
console.log();
|
|
2044
|
+
console.log(c.muted(' You can now deploy verified tokens with: clawncher deploy --verified'));
|
|
2045
|
+
console.log();
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
catch (err) {
|
|
2049
|
+
spinner.stop();
|
|
2050
|
+
handleError(err);
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
agent
|
|
2054
|
+
.command('status')
|
|
2055
|
+
.description('Show verified agent status, balance, and launch count')
|
|
2056
|
+
.requiredOption('--api-key <key>', 'Agent API key')
|
|
2057
|
+
.option('--api-url <url>', 'Clawnch API URL')
|
|
2058
|
+
.option('--json', 'Output as JSON')
|
|
2059
|
+
.action(async (opts) => {
|
|
2060
|
+
const spinner = ora('Fetching agent status...').start();
|
|
2061
|
+
try {
|
|
2062
|
+
// For status we only need the API key, no wallet needed
|
|
2063
|
+
// But ClawnchApiDeployer requires wallet/publicClient, so we use a direct fetch
|
|
2064
|
+
const baseUrl = (opts.apiUrl || getClawnchApiUrl()).replace(/\/$/, '');
|
|
2065
|
+
const response = await fetch(`${baseUrl}/api/agents/me`, {
|
|
2066
|
+
method: 'GET',
|
|
2067
|
+
headers: {
|
|
2068
|
+
'Accept': 'application/json',
|
|
2069
|
+
'Authorization': `Bearer ${opts.apiKey}`,
|
|
2070
|
+
},
|
|
2071
|
+
});
|
|
2072
|
+
if (!response.ok) {
|
|
2073
|
+
const err = await response.json().catch(() => ({ error: 'Request failed' }));
|
|
2074
|
+
throw new Error(err.error || `HTTP ${response.status}`);
|
|
2075
|
+
}
|
|
2076
|
+
const status = await response.json();
|
|
2077
|
+
spinner.succeed('Agent status retrieved');
|
|
2078
|
+
if (opts.json) {
|
|
2079
|
+
console.log(JSON.stringify(status, null, 2));
|
|
2080
|
+
}
|
|
2081
|
+
else {
|
|
2082
|
+
console.log();
|
|
2083
|
+
console.log(kv('Agent', c.accent(status.name)));
|
|
2084
|
+
console.log(kv('ID', c.value(status.agentId)));
|
|
2085
|
+
console.log(kv('Wallet', c.value(status.wallet)));
|
|
2086
|
+
console.log(kv('Verified', status.verified ? `${dot('green')} Yes` : `${dot('red')} No`));
|
|
2087
|
+
console.log(kv('$CLAWNCH Balance', c.value(status.clawnchBalance)));
|
|
2088
|
+
console.log(kv('$CLAWNCH Allowance', c.value(status.clawnchAllowance)));
|
|
2089
|
+
console.log(kv('Launches', c.value(status.launchCount.toString())));
|
|
2090
|
+
console.log(kv('Registered', c.muted(status.registeredAt)));
|
|
2091
|
+
console.log();
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
catch (err) {
|
|
2095
|
+
spinner.stop();
|
|
2096
|
+
handleError(err);
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
// ============================================================================
|
|
2100
|
+
// Verified Deploy (--verified flag on deploy command)
|
|
2101
|
+
// ============================================================================
|
|
2102
|
+
program
|
|
2103
|
+
.command('deploy-verified')
|
|
2104
|
+
.description('Deploy a token via Clawnch API (verified, gets @clawnch badge)')
|
|
2105
|
+
.requiredOption('--name <name>', 'Token name')
|
|
2106
|
+
.requiredOption('--symbol <symbol>', 'Token symbol (ticker)')
|
|
2107
|
+
.requiredOption('--api-key <key>', 'Agent API key')
|
|
2108
|
+
.option('--image <url>', 'Token image URL')
|
|
2109
|
+
.option('--description <desc>', 'Token description')
|
|
2110
|
+
.option('--recipient <address>', 'Fee recipient address')
|
|
2111
|
+
.option('--fee-preference <pref>', 'Fee preference: Clawnch, Paired, Both', 'Clawnch')
|
|
2112
|
+
.option('--vault-percent <n>', 'Vault allocation percentage (1-90)')
|
|
2113
|
+
.option('--vault-lockup <days>', 'Vault lockup in days')
|
|
2114
|
+
.option('--vault-recipient <address>', 'Vault recipient')
|
|
2115
|
+
.option('--dev-buy <eth>', 'ETH amount for dev buy')
|
|
2116
|
+
.option('--dev-buy-recipient <address>', 'Dev buy recipient')
|
|
2117
|
+
.option('--no-vanity', 'Disable vanity address')
|
|
2118
|
+
.option('--network <network>', 'Network (mainnet/sepolia)', 'mainnet')
|
|
2119
|
+
.option('--private-key <key>', 'Private key')
|
|
2120
|
+
.option('--rpc <url>', 'RPC URL')
|
|
2121
|
+
.option('--api-url <url>', 'Clawnch API URL')
|
|
2122
|
+
.option('--json', 'Output as JSON')
|
|
2123
|
+
.action(async (opts) => {
|
|
2124
|
+
const spinner = ora('Preparing verified deployment...').start();
|
|
2125
|
+
try {
|
|
2126
|
+
const privateKey = await getPrivateKeyOrWallet(opts);
|
|
2127
|
+
const network = getNetwork(opts);
|
|
2128
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
2129
|
+
const { wallet, publicClient, account } = createClients(network, privateKey, rpcUrl);
|
|
2130
|
+
const deployer = new ClawnchApiDeployer({
|
|
2131
|
+
apiKey: opts.apiKey,
|
|
2132
|
+
wallet,
|
|
2133
|
+
publicClient,
|
|
2134
|
+
network,
|
|
2135
|
+
apiBaseUrl: opts.apiUrl || getClawnchApiUrl(),
|
|
2136
|
+
});
|
|
2137
|
+
// Build deploy request
|
|
2138
|
+
const request = {
|
|
2139
|
+
name: opts.name,
|
|
2140
|
+
symbol: opts.symbol,
|
|
2141
|
+
};
|
|
2142
|
+
if (opts.image)
|
|
2143
|
+
request.image = opts.image;
|
|
2144
|
+
if (opts.description)
|
|
2145
|
+
request.description = opts.description;
|
|
2146
|
+
if (opts.vanity === false)
|
|
2147
|
+
request.vanity = false;
|
|
2148
|
+
// Rewards
|
|
2149
|
+
if (opts.recipient) {
|
|
2150
|
+
request.rewards = {
|
|
2151
|
+
recipients: [{
|
|
2152
|
+
recipient: validateAddress(opts.recipient, 'recipient'),
|
|
2153
|
+
admin: account.address,
|
|
2154
|
+
bps: 10000,
|
|
2155
|
+
feePreference: opts.feePreference || 'Clawnch',
|
|
2156
|
+
}],
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
// Vault
|
|
2160
|
+
if (opts.vaultPercent) {
|
|
2161
|
+
const days = parseInt(opts.vaultLockup || '7', 10);
|
|
2162
|
+
request.vault = {
|
|
2163
|
+
percentage: parseInt(opts.vaultPercent, 10),
|
|
2164
|
+
lockupDuration: days * 86400,
|
|
2165
|
+
recipient: opts.vaultRecipient ? validateAddress(opts.vaultRecipient, 'vault recipient') : account.address,
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
// Dev buy
|
|
2169
|
+
if (opts.devBuy) {
|
|
2170
|
+
const { parseEther } = await import('viem');
|
|
2171
|
+
request.devBuy = {
|
|
2172
|
+
ethAmount: parseEther(opts.devBuy).toString(),
|
|
2173
|
+
recipient: opts.devBuyRecipient ? validateAddress(opts.devBuyRecipient, 'dev buy recipient') : account.address,
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
spinner.text = 'Solving captcha challenge...';
|
|
2177
|
+
const result = await deployer.deploy(request);
|
|
2178
|
+
spinner.succeed('Token deployed (verified)');
|
|
2179
|
+
if (opts.json) {
|
|
2180
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2181
|
+
}
|
|
2182
|
+
else {
|
|
2183
|
+
console.log();
|
|
2184
|
+
console.log(kv('Token', c.accent(`${opts.name} (${opts.symbol})`)));
|
|
2185
|
+
console.log(kv('Address', c.value(result.tokenAddress)));
|
|
2186
|
+
console.log(kv('Tx Hash', c.value(result.txHash)));
|
|
2187
|
+
console.log(kv('Deployed From', c.value(result.deployedFrom)));
|
|
2188
|
+
console.log(kv('$CLAWNCH Burned', c.value(result.clawnchBurned)));
|
|
2189
|
+
console.log(kv('Badge', `${dot('green')} @clawnch verified`));
|
|
2190
|
+
console.log();
|
|
2191
|
+
console.log(c.muted(` View: https://clanker.world/clanker/${result.tokenAddress}`));
|
|
2192
|
+
console.log();
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
catch (err) {
|
|
2196
|
+
spinner.stop();
|
|
2197
|
+
handleError(err);
|
|
2198
|
+
}
|
|
2199
|
+
});
|
|
2200
|
+
// ============================================================================
|
|
2201
|
+
// Wallet Management Commands
|
|
2202
|
+
// ============================================================================
|
|
2203
|
+
import { createWallet as createNewWallet, importFromPrivateKey, importFromMnemonic, decryptWallet, listWallets, walletExists, removeWallet, getActiveWallet, setActiveWallet, getWalletInfo, changePassword, promptPassword, promptPasswordConfirm, } from './wallet.js';
|
|
2204
|
+
const walletCmd = program
|
|
2205
|
+
.command('wallet')
|
|
2206
|
+
.description('Manage encrypted wallets');
|
|
2207
|
+
walletCmd
|
|
2208
|
+
.command('create')
|
|
2209
|
+
.description('Create a new wallet with a fresh mnemonic')
|
|
2210
|
+
.argument('<name>', 'Wallet name (alphanumeric, dash, underscore)')
|
|
2211
|
+
.option('--set-active', 'Set as active wallet after creation')
|
|
2212
|
+
.action(async (name, opts) => {
|
|
2213
|
+
try {
|
|
2214
|
+
// Validate name
|
|
2215
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
2216
|
+
console.error(c.error(' Wallet name must be alphanumeric (dashes and underscores allowed)'));
|
|
2217
|
+
process.exit(1);
|
|
2218
|
+
}
|
|
2219
|
+
if (walletExists(name)) {
|
|
2220
|
+
console.error(c.error(` Wallet "${name}" already exists`));
|
|
2221
|
+
process.exit(1);
|
|
2222
|
+
}
|
|
2223
|
+
const password = await promptPasswordConfirm('encryption password');
|
|
2224
|
+
const spinner = ora('Generating wallet...').start();
|
|
2225
|
+
const { keystore, mnemonic, privateKey } = createNewWallet(name, password);
|
|
2226
|
+
spinner.succeed('Wallet created');
|
|
2227
|
+
if (opts.setActive) {
|
|
2228
|
+
setActiveWallet(name);
|
|
2229
|
+
}
|
|
2230
|
+
console.log();
|
|
2231
|
+
console.log(sectionHeader('New Wallet'));
|
|
2232
|
+
console.log(kv('Name', c.accent(name)));
|
|
2233
|
+
console.log(kv('Address', fmtAddr(keystore.address)));
|
|
2234
|
+
console.log();
|
|
2235
|
+
console.log(sectionHeader('Recovery Phrase'));
|
|
2236
|
+
console.log(c.warn(' IMPORTANT: Write down this mnemonic and store it safely.'));
|
|
2237
|
+
console.log(c.warn(' It will NOT be shown again. Anyone with this phrase'));
|
|
2238
|
+
console.log(c.warn(' can access your funds.'));
|
|
2239
|
+
console.log();
|
|
2240
|
+
const words = mnemonic.split(' ');
|
|
2241
|
+
for (let i = 0; i < words.length; i += 4) {
|
|
2242
|
+
const row = words.slice(i, i + 4)
|
|
2243
|
+
.map((w, j) => `${c.muted(String(i + j + 1).padStart(2, ' '))}. ${c.value(w.padEnd(10))}`)
|
|
2244
|
+
.join(' ');
|
|
2245
|
+
console.log(` ${row}`);
|
|
2246
|
+
}
|
|
2247
|
+
console.log();
|
|
2248
|
+
if (opts.setActive) {
|
|
2249
|
+
console.log(` ${c.success('*')} Set as active wallet`);
|
|
2250
|
+
}
|
|
2251
|
+
else {
|
|
2252
|
+
console.log(c.muted(` Run ${c.accent('clawncher wallet use ' + name)} to set as active`));
|
|
2253
|
+
}
|
|
2254
|
+
console.log();
|
|
2255
|
+
}
|
|
2256
|
+
catch (err) {
|
|
2257
|
+
handleError(err);
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
walletCmd
|
|
2261
|
+
.command('import')
|
|
2262
|
+
.description('Import a wallet from a private key or mnemonic')
|
|
2263
|
+
.argument('<name>', 'Wallet name')
|
|
2264
|
+
.option('--private-key <key>', 'Import from private key (hex)')
|
|
2265
|
+
.option('--mnemonic <phrase>', 'Import from mnemonic phrase')
|
|
2266
|
+
.option('--set-active', 'Set as active wallet after import')
|
|
2267
|
+
.action(async (name, opts) => {
|
|
2268
|
+
try {
|
|
2269
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
2270
|
+
console.error(c.error(' Wallet name must be alphanumeric (dashes and underscores allowed)'));
|
|
2271
|
+
process.exit(1);
|
|
2272
|
+
}
|
|
2273
|
+
if (walletExists(name)) {
|
|
2274
|
+
console.error(c.error(` Wallet "${name}" already exists`));
|
|
2275
|
+
process.exit(1);
|
|
2276
|
+
}
|
|
2277
|
+
if (!opts.privateKey && !opts.mnemonic) {
|
|
2278
|
+
console.error(c.error(' Provide --private-key or --mnemonic'));
|
|
2279
|
+
process.exit(1);
|
|
2280
|
+
}
|
|
2281
|
+
const password = await promptPasswordConfirm('encryption password');
|
|
2282
|
+
const spinner = ora('Encrypting wallet...').start();
|
|
2283
|
+
let keystore;
|
|
2284
|
+
if (opts.privateKey) {
|
|
2285
|
+
const key = opts.privateKey.startsWith('0x') ? opts.privateKey : `0x${opts.privateKey}`;
|
|
2286
|
+
keystore = importFromPrivateKey(name, key, password);
|
|
2287
|
+
}
|
|
2288
|
+
else {
|
|
2289
|
+
keystore = importFromMnemonic(name, opts.mnemonic, password);
|
|
2290
|
+
}
|
|
2291
|
+
spinner.succeed('Wallet imported');
|
|
2292
|
+
if (opts.setActive) {
|
|
2293
|
+
setActiveWallet(name);
|
|
2294
|
+
}
|
|
2295
|
+
console.log();
|
|
2296
|
+
console.log(kv('Name', c.accent(name)));
|
|
2297
|
+
console.log(kv('Address', fmtAddr(keystore.address)));
|
|
2298
|
+
console.log(kv('Source', opts.privateKey ? 'Private key' : 'Mnemonic'));
|
|
2299
|
+
if (opts.setActive) {
|
|
2300
|
+
console.log(` ${c.success('*')} Set as active wallet`);
|
|
2301
|
+
}
|
|
2302
|
+
console.log();
|
|
2303
|
+
}
|
|
2304
|
+
catch (err) {
|
|
2305
|
+
handleError(err);
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
walletCmd
|
|
2309
|
+
.command('list')
|
|
2310
|
+
.alias('ls')
|
|
2311
|
+
.description('List all wallets')
|
|
2312
|
+
.option('--json', 'Output as JSON')
|
|
2313
|
+
.action(async (opts) => {
|
|
2314
|
+
try {
|
|
2315
|
+
const wallets = listWallets();
|
|
2316
|
+
if (opts.json) {
|
|
2317
|
+
console.log(JSON.stringify(wallets, null, 2));
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
if (wallets.length === 0) {
|
|
2321
|
+
console.log(c.muted('\n No wallets found. Create one with: clawncher wallet create <name>\n'));
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
console.log();
|
|
2325
|
+
console.log(sectionHeader(`Wallets (${wallets.length})`));
|
|
2326
|
+
for (const w of wallets) {
|
|
2327
|
+
const active = w.isActive ? c.success(' *') : ' ';
|
|
2328
|
+
const mnemonicBadge = w.hasMnemonic ? c.muted(' [mnemonic]') : '';
|
|
2329
|
+
console.log(`${active} ${c.accent(w.name.padEnd(16))} ${fmtAddr(w.address)}${mnemonicBadge}`);
|
|
2330
|
+
}
|
|
2331
|
+
const activeWallet = getActiveWallet();
|
|
2332
|
+
if (activeWallet) {
|
|
2333
|
+
console.log(c.muted(`\n * = active wallet (${activeWallet})`));
|
|
2334
|
+
}
|
|
2335
|
+
console.log();
|
|
2336
|
+
}
|
|
2337
|
+
catch (err) {
|
|
2338
|
+
handleError(err);
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
walletCmd
|
|
2342
|
+
.command('use')
|
|
2343
|
+
.description('Set the active wallet (used when no --private-key given)')
|
|
2344
|
+
.argument('<name>', 'Wallet name')
|
|
2345
|
+
.action(async (name) => {
|
|
2346
|
+
try {
|
|
2347
|
+
if (!walletExists(name)) {
|
|
2348
|
+
console.error(c.error(` Wallet "${name}" not found`));
|
|
2349
|
+
process.exit(1);
|
|
2350
|
+
}
|
|
2351
|
+
setActiveWallet(name);
|
|
2352
|
+
const info = getWalletInfo(name);
|
|
2353
|
+
console.log(` ${c.success('\u2713')} Active wallet: ${c.accent(name)} (${fmtAddr(info.address, true)})`);
|
|
2354
|
+
}
|
|
2355
|
+
catch (err) {
|
|
2356
|
+
handleError(err);
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
walletCmd
|
|
2360
|
+
.command('export')
|
|
2361
|
+
.description('Export wallet private key (requires password)')
|
|
2362
|
+
.argument('<name>', 'Wallet name')
|
|
2363
|
+
.option('--show-mnemonic', 'Also show mnemonic phrase if available')
|
|
2364
|
+
.action(async (name, opts) => {
|
|
2365
|
+
try {
|
|
2366
|
+
if (!walletExists(name)) {
|
|
2367
|
+
console.error(c.error(` Wallet "${name}" not found`));
|
|
2368
|
+
process.exit(1);
|
|
2369
|
+
}
|
|
2370
|
+
const password = await promptPassword(' Enter wallet password: ');
|
|
2371
|
+
const spinner = ora('Decrypting...').start();
|
|
2372
|
+
const decrypted = decryptWallet(name, password);
|
|
2373
|
+
spinner.succeed('Decrypted');
|
|
2374
|
+
console.log();
|
|
2375
|
+
console.log(c.warn(' WARNING: Never share your private key. Anyone with it controls your funds.'));
|
|
2376
|
+
console.log();
|
|
2377
|
+
console.log(kv('Name', c.accent(decrypted.name)));
|
|
2378
|
+
console.log(kv('Address', fmtAddr(decrypted.address)));
|
|
2379
|
+
console.log(kv('Private Key', c.value(decrypted.privateKey)));
|
|
2380
|
+
if (opts.showMnemonic && decrypted.mnemonic) {
|
|
2381
|
+
console.log();
|
|
2382
|
+
console.log(sectionHeader('Recovery Phrase'));
|
|
2383
|
+
const words = decrypted.mnemonic.split(' ');
|
|
2384
|
+
for (let i = 0; i < words.length; i += 4) {
|
|
2385
|
+
const row = words.slice(i, i + 4)
|
|
2386
|
+
.map((w, j) => `${c.muted(String(i + j + 1).padStart(2, ' '))}. ${c.value(w.padEnd(10))}`)
|
|
2387
|
+
.join(' ');
|
|
2388
|
+
console.log(` ${row}`);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
else if (opts.showMnemonic && !decrypted.mnemonic) {
|
|
2392
|
+
console.log(c.muted('\n No mnemonic stored (wallet was imported from private key)'));
|
|
2393
|
+
}
|
|
2394
|
+
console.log();
|
|
2395
|
+
}
|
|
2396
|
+
catch (err) {
|
|
2397
|
+
handleError(err);
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2400
|
+
walletCmd
|
|
2401
|
+
.command('remove')
|
|
2402
|
+
.alias('rm')
|
|
2403
|
+
.description('Remove a wallet')
|
|
2404
|
+
.argument('<name>', 'Wallet name')
|
|
2405
|
+
.option('--force', 'Skip confirmation')
|
|
2406
|
+
.action(async (name, opts) => {
|
|
2407
|
+
try {
|
|
2408
|
+
if (!walletExists(name)) {
|
|
2409
|
+
console.error(c.error(` Wallet "${name}" not found`));
|
|
2410
|
+
process.exit(1);
|
|
2411
|
+
}
|
|
2412
|
+
const info = getWalletInfo(name);
|
|
2413
|
+
if (!opts.force) {
|
|
2414
|
+
console.log(c.warn(`\n This will permanently delete wallet "${name}" (${fmtAddr(info.address, true)})`));
|
|
2415
|
+
console.log(c.warn(' Make sure you have backed up your private key or mnemonic!\n'));
|
|
2416
|
+
const confirm = await promptPassword(' Type wallet name to confirm: ');
|
|
2417
|
+
if (confirm !== name) {
|
|
2418
|
+
console.log(c.muted(' Cancelled'));
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
removeWallet(name);
|
|
2423
|
+
console.log(` ${c.success('\u2713')} Wallet "${name}" removed`);
|
|
2424
|
+
}
|
|
2425
|
+
catch (err) {
|
|
2426
|
+
handleError(err);
|
|
2427
|
+
}
|
|
2428
|
+
});
|
|
2429
|
+
walletCmd
|
|
2430
|
+
.command('password')
|
|
2431
|
+
.description('Change wallet password')
|
|
2432
|
+
.argument('<name>', 'Wallet name')
|
|
2433
|
+
.action(async (name) => {
|
|
2434
|
+
try {
|
|
2435
|
+
if (!walletExists(name)) {
|
|
2436
|
+
console.error(c.error(` Wallet "${name}" not found`));
|
|
2437
|
+
process.exit(1);
|
|
2438
|
+
}
|
|
2439
|
+
const oldPw = await promptPassword(' Enter current password: ');
|
|
2440
|
+
// Verify old password works
|
|
2441
|
+
const spinner = ora('Verifying...').start();
|
|
2442
|
+
decryptWallet(name, oldPw); // Throws if wrong
|
|
2443
|
+
spinner.succeed('Password verified');
|
|
2444
|
+
const newPw = await promptPasswordConfirm('new password');
|
|
2445
|
+
const reEncrypt = ora('Re-encrypting...').start();
|
|
2446
|
+
changePassword(name, oldPw, newPw);
|
|
2447
|
+
reEncrypt.succeed('Password changed');
|
|
2448
|
+
}
|
|
2449
|
+
catch (err) {
|
|
2450
|
+
handleError(err);
|
|
2451
|
+
}
|
|
2452
|
+
});
|
|
2453
|
+
walletCmd
|
|
2454
|
+
.command('balance')
|
|
2455
|
+
.description('Show wallet balances (ETH + tokens)')
|
|
2456
|
+
.argument('[name]', 'Wallet name (default: active wallet)')
|
|
2457
|
+
.option('-n, --network <network>', 'Network (sepolia or mainnet)')
|
|
2458
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
2459
|
+
.option('-t, --token <address>', 'Also show balance of a specific ERC20 token')
|
|
2460
|
+
.action(async (name, opts) => {
|
|
2461
|
+
try {
|
|
2462
|
+
const walletName = name || getActiveWallet();
|
|
2463
|
+
if (!walletName) {
|
|
2464
|
+
console.error(c.error(' No wallet specified and no active wallet set'));
|
|
2465
|
+
console.error(c.muted(' Use: clawncher wallet balance <name>'));
|
|
2466
|
+
console.error(c.muted(' or: clawncher wallet use <name>'));
|
|
2467
|
+
process.exit(1);
|
|
2468
|
+
}
|
|
2469
|
+
if (!walletExists(walletName)) {
|
|
2470
|
+
console.error(c.error(` Wallet "${walletName}" not found`));
|
|
2471
|
+
process.exit(1);
|
|
2472
|
+
}
|
|
2473
|
+
const info = getWalletInfo(walletName);
|
|
2474
|
+
const network = getNetwork(opts);
|
|
2475
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
2476
|
+
const chain = getChain(network);
|
|
2477
|
+
const publicClient = createPublicClient({ chain, transport: http(rpcUrl) });
|
|
2478
|
+
const spinner = ora('Fetching balances...').start();
|
|
2479
|
+
// Get ETH balance
|
|
2480
|
+
const ethBalance = await publicClient.getBalance({ address: info.address });
|
|
2481
|
+
spinner.succeed('Balances fetched');
|
|
2482
|
+
console.log();
|
|
2483
|
+
console.log(sectionHeader(`${walletName} Balances`));
|
|
2484
|
+
console.log(kv('Address', fmtAddr(info.address)));
|
|
2485
|
+
console.log(kv('Network', networkBadge(network)));
|
|
2486
|
+
console.log(kv('ETH', `${formatEther(ethBalance)} ETH`));
|
|
2487
|
+
// Token balance if requested
|
|
2488
|
+
if (opts.token) {
|
|
2489
|
+
const tokenAddr = validateAddress(opts.token, 'token');
|
|
2490
|
+
try {
|
|
2491
|
+
const [balance, decimals, symbol] = await Promise.all([
|
|
2492
|
+
publicClient.readContract({
|
|
2493
|
+
address: tokenAddr,
|
|
2494
|
+
abi: [{ type: 'function', name: 'balanceOf', inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' }],
|
|
2495
|
+
functionName: 'balanceOf',
|
|
2496
|
+
args: [info.address],
|
|
2497
|
+
}),
|
|
2498
|
+
readDecimals(publicClient, tokenAddr),
|
|
2499
|
+
readSymbol(publicClient, tokenAddr),
|
|
2500
|
+
]);
|
|
2501
|
+
const formatted = Number(balance) / (10 ** decimals);
|
|
2502
|
+
console.log(kv(symbol, `${formatted.toLocaleString()} ${symbol}`));
|
|
2503
|
+
}
|
|
2504
|
+
catch {
|
|
2505
|
+
console.log(kv('Token', c.error('Failed to read token balance')));
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
console.log();
|
|
2509
|
+
}
|
|
2510
|
+
catch (err) {
|
|
2511
|
+
handleError(err);
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
walletCmd
|
|
2515
|
+
.command('send')
|
|
2516
|
+
.description('Send ETH or tokens from wallet')
|
|
2517
|
+
.argument('[name]', 'Wallet name (default: active wallet)')
|
|
2518
|
+
.requiredOption('--to <address>', 'Recipient address')
|
|
2519
|
+
.requiredOption('--amount <value>', 'Amount to send (in ETH or token units)')
|
|
2520
|
+
.option('--token <address>', 'ERC20 token address (omit for ETH)')
|
|
2521
|
+
.option('-n, --network <network>', 'Network (sepolia or mainnet)')
|
|
2522
|
+
.option('--rpc <url>', 'Custom RPC URL')
|
|
2523
|
+
.option('--json', 'Output as JSON')
|
|
2524
|
+
.action(async (name, opts) => {
|
|
2525
|
+
try {
|
|
2526
|
+
const walletName = name || getActiveWallet();
|
|
2527
|
+
if (!walletName) {
|
|
2528
|
+
console.error(c.error(' No wallet specified and no active wallet set'));
|
|
2529
|
+
process.exit(1);
|
|
2530
|
+
}
|
|
2531
|
+
if (!walletExists(walletName)) {
|
|
2532
|
+
console.error(c.error(` Wallet "${walletName}" not found`));
|
|
2533
|
+
process.exit(1);
|
|
2534
|
+
}
|
|
2535
|
+
const to = validateAddress(opts.to, 'recipient');
|
|
2536
|
+
const network = getNetwork(opts);
|
|
2537
|
+
const rpcUrl = getRpcUrl(network, opts);
|
|
2538
|
+
// Decrypt wallet
|
|
2539
|
+
const password = await promptPassword(' Enter wallet password: ');
|
|
2540
|
+
const spinner = ora('Decrypting wallet...').start();
|
|
2541
|
+
const decrypted = decryptWallet(walletName, password);
|
|
2542
|
+
spinner.text = 'Preparing transaction...';
|
|
2543
|
+
const { wallet, publicClient } = createClients(network, decrypted.privateKey, rpcUrl);
|
|
2544
|
+
if (opts.token) {
|
|
2545
|
+
// ERC20 transfer
|
|
2546
|
+
const tokenAddr = validateAddress(opts.token, 'token');
|
|
2547
|
+
const [decimals, symbol] = await Promise.all([
|
|
2548
|
+
readDecimals(publicClient, tokenAddr),
|
|
2549
|
+
readSymbol(publicClient, tokenAddr),
|
|
2550
|
+
]);
|
|
2551
|
+
const amount = BigInt(Math.floor(Number(opts.amount) * (10 ** decimals)));
|
|
2552
|
+
spinner.text = `Sending ${opts.amount} ${symbol}...`;
|
|
2553
|
+
const hash = await wallet.writeContract({
|
|
2554
|
+
address: tokenAddr,
|
|
2555
|
+
abi: [{ type: 'function', name: 'transfer', inputs: [{ type: 'address', name: 'to' }, { type: 'uint256', name: 'amount' }], outputs: [{ type: 'bool' }], stateMutability: 'nonpayable' }],
|
|
2556
|
+
functionName: 'transfer',
|
|
2557
|
+
args: [to, amount],
|
|
2558
|
+
});
|
|
2559
|
+
spinner.succeed(`Sent ${opts.amount} ${symbol}`);
|
|
2560
|
+
if (opts.json) {
|
|
2561
|
+
console.log(JSON.stringify({ hash, from: decrypted.address, to, amount: opts.amount, token: tokenAddr, symbol }));
|
|
2562
|
+
}
|
|
2563
|
+
else {
|
|
2564
|
+
console.log();
|
|
2565
|
+
console.log(kv('Tx Hash', c.value(hash)));
|
|
2566
|
+
console.log(kv('From', fmtAddr(decrypted.address, true)));
|
|
2567
|
+
console.log(kv('To', fmtAddr(to, true)));
|
|
2568
|
+
console.log(kv('Amount', `${opts.amount} ${symbol}`));
|
|
2569
|
+
console.log(kv('Network', networkBadge(network)));
|
|
2570
|
+
console.log();
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
else {
|
|
2574
|
+
// ETH transfer
|
|
2575
|
+
const amount = parseEther(opts.amount);
|
|
2576
|
+
spinner.text = `Sending ${opts.amount} ETH...`;
|
|
2577
|
+
const hash = await wallet.sendTransaction({
|
|
2578
|
+
to,
|
|
2579
|
+
value: amount,
|
|
2580
|
+
});
|
|
2581
|
+
spinner.succeed(`Sent ${opts.amount} ETH`);
|
|
2582
|
+
if (opts.json) {
|
|
2583
|
+
console.log(JSON.stringify({ hash, from: decrypted.address, to, amount: opts.amount }));
|
|
2584
|
+
}
|
|
2585
|
+
else {
|
|
2586
|
+
console.log();
|
|
2587
|
+
console.log(kv('Tx Hash', c.value(hash)));
|
|
2588
|
+
console.log(kv('From', fmtAddr(decrypted.address, true)));
|
|
2589
|
+
console.log(kv('To', fmtAddr(to, true)));
|
|
2590
|
+
console.log(kv('Amount', `${opts.amount} ETH`));
|
|
2591
|
+
console.log(kv('Network', networkBadge(network)));
|
|
2592
|
+
console.log();
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
catch (err) {
|
|
2597
|
+
handleError(err);
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
walletCmd
|
|
2601
|
+
.command('sign')
|
|
2602
|
+
.description('Sign a message with wallet')
|
|
2603
|
+
.argument('[name]', 'Wallet name (default: active wallet)')
|
|
2604
|
+
.requiredOption('-m, --message <text>', 'Message to sign')
|
|
2605
|
+
.option('--json', 'Output as JSON')
|
|
2606
|
+
.action(async (name, opts) => {
|
|
2607
|
+
try {
|
|
2608
|
+
const walletName = name || getActiveWallet();
|
|
2609
|
+
if (!walletName) {
|
|
2610
|
+
console.error(c.error(' No wallet specified and no active wallet set'));
|
|
2611
|
+
process.exit(1);
|
|
2612
|
+
}
|
|
2613
|
+
if (!walletExists(walletName)) {
|
|
2614
|
+
console.error(c.error(` Wallet "${walletName}" not found`));
|
|
2615
|
+
process.exit(1);
|
|
2616
|
+
}
|
|
2617
|
+
const password = await promptPassword(' Enter wallet password: ');
|
|
2618
|
+
const spinner = ora('Signing...').start();
|
|
2619
|
+
const decrypted = decryptWallet(walletName, password);
|
|
2620
|
+
const account = privateKeyToAccount(decrypted.privateKey);
|
|
2621
|
+
const signature = await account.signMessage({ message: opts.message });
|
|
2622
|
+
spinner.succeed('Message signed');
|
|
2623
|
+
if (opts.json) {
|
|
2624
|
+
console.log(JSON.stringify({ address: decrypted.address, message: opts.message, signature }));
|
|
2625
|
+
}
|
|
2626
|
+
else {
|
|
2627
|
+
console.log();
|
|
2628
|
+
console.log(kv('Address', fmtAddr(decrypted.address)));
|
|
2629
|
+
console.log(kv('Message', c.value(opts.message)));
|
|
2630
|
+
console.log(kv('Signature', c.value(signature)));
|
|
2631
|
+
console.log();
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
catch (err) {
|
|
2635
|
+
handleError(err);
|
|
2636
|
+
}
|
|
2637
|
+
});
|
|
2638
|
+
// Parse and run
|
|
2639
|
+
program.parse();
|
|
2640
|
+
//# sourceMappingURL=cli.js.map
|