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/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