@darksol/terminal 0.1.0 → 0.2.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.
@@ -0,0 +1,677 @@
1
+ import { ethers } from 'ethers';
2
+ import { getConfig, getRPC } from '../config/store.js';
3
+ import { theme } from '../ui/theme.js';
4
+ import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/components.js';
5
+ import { showSection } from '../ui/banner.js';
6
+
7
+ // ──────────────────────────────────────────────────
8
+ // PROVIDER & CHAIN HELPERS
9
+ // ──────────────────────────────────────────────────
10
+
11
+ /**
12
+ * Get an ethers provider for a given chain (or the active chain)
13
+ * @param {string} [chain] - Chain name (base, ethereum, polygon, arbitrum, optimism)
14
+ * @returns {ethers.JsonRpcProvider}
15
+ */
16
+ export function getProvider(chain) {
17
+ const rpc = getRPC(chain || getConfig('chain'));
18
+ return new ethers.JsonRpcProvider(rpc);
19
+ }
20
+
21
+ /**
22
+ * Get the chain ID for a chain name
23
+ */
24
+ export const CHAIN_IDS = {
25
+ ethereum: 1,
26
+ optimism: 10,
27
+ polygon: 137,
28
+ arbitrum: 42161,
29
+ base: 8453,
30
+ };
31
+
32
+ /**
33
+ * Get a block explorer URL for a given chain
34
+ */
35
+ export const EXPLORERS = {
36
+ base: 'https://basescan.org',
37
+ ethereum: 'https://etherscan.io',
38
+ polygon: 'https://polygonscan.com',
39
+ arbitrum: 'https://arbiscan.io',
40
+ optimism: 'https://optimistic.etherscan.io',
41
+ };
42
+
43
+ /**
44
+ * Get the block explorer TX URL
45
+ * @param {string} txHash
46
+ * @param {string} [chain]
47
+ * @returns {string}
48
+ */
49
+ export function txUrl(txHash, chain) {
50
+ const explorer = EXPLORERS[chain || getConfig('chain')] || EXPLORERS.base;
51
+ return `${explorer}/tx/${txHash}`;
52
+ }
53
+
54
+ /**
55
+ * Get the block explorer address URL
56
+ * @param {string} address
57
+ * @param {string} [chain]
58
+ * @returns {string}
59
+ */
60
+ export function addressUrl(address, chain) {
61
+ const explorer = EXPLORERS[chain || getConfig('chain')] || EXPLORERS.base;
62
+ return `${explorer}/address/${address}`;
63
+ }
64
+
65
+ /**
66
+ * Get the block explorer token URL
67
+ * @param {string} tokenAddress
68
+ * @param {string} [chain]
69
+ * @returns {string}
70
+ */
71
+ export function tokenUrl(tokenAddress, chain) {
72
+ const explorer = EXPLORERS[chain || getConfig('chain')] || EXPLORERS.base;
73
+ return `${explorer}/token/${tokenAddress}`;
74
+ }
75
+
76
+
77
+ // ──────────────────────────────────────────────────
78
+ // TOKEN HELPERS
79
+ // ──────────────────────────────────────────────────
80
+
81
+ const ERC20_ABI = [
82
+ 'function name() view returns (string)',
83
+ 'function symbol() view returns (string)',
84
+ 'function decimals() view returns (uint8)',
85
+ 'function totalSupply() view returns (uint256)',
86
+ 'function balanceOf(address) view returns (uint256)',
87
+ 'function allowance(address owner, address spender) view returns (uint256)',
88
+ 'function approve(address spender, uint256 amount) returns (bool)',
89
+ 'function transfer(address to, uint256 amount) returns (bool)',
90
+ ];
91
+
92
+ /**
93
+ * Get a connected ERC20 contract instance
94
+ * @param {string} address - Token contract address
95
+ * @param {ethers.Signer|ethers.Provider} signerOrProvider
96
+ * @returns {ethers.Contract}
97
+ */
98
+ export function getERC20(address, signerOrProvider) {
99
+ return new ethers.Contract(address, ERC20_ABI, signerOrProvider);
100
+ }
101
+
102
+ /**
103
+ * Get full token info: name, symbol, decimals, totalSupply
104
+ * @param {string} address
105
+ * @param {ethers.Provider} provider
106
+ * @returns {Promise<{name: string, symbol: string, decimals: number, totalSupply: bigint, address: string}>}
107
+ */
108
+ export async function getFullTokenInfo(address, provider) {
109
+ const token = getERC20(address, provider);
110
+ const [name, symbol, decimals, totalSupply] = await Promise.all([
111
+ token.name(),
112
+ token.symbol(),
113
+ token.decimals(),
114
+ token.totalSupply(),
115
+ ]);
116
+ return {
117
+ name,
118
+ symbol,
119
+ decimals: Number(decimals),
120
+ totalSupply,
121
+ address,
122
+ formattedSupply: ethers.formatUnits(totalSupply, Number(decimals)),
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Get token balance for an address
128
+ * @param {string} tokenAddress
129
+ * @param {string} walletAddress
130
+ * @param {ethers.Provider} provider
131
+ * @returns {Promise<{raw: bigint, formatted: string, symbol: string}>}
132
+ */
133
+ export async function getTokenBalance(tokenAddress, walletAddress, provider) {
134
+ const token = getERC20(tokenAddress, provider);
135
+ const [balance, decimals, symbol] = await Promise.all([
136
+ token.balanceOf(walletAddress),
137
+ token.decimals(),
138
+ token.symbol(),
139
+ ]);
140
+ return {
141
+ raw: balance,
142
+ formatted: ethers.formatUnits(balance, Number(decimals)),
143
+ symbol,
144
+ decimals: Number(decimals),
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Check and approve token spending if needed
150
+ * @param {ethers.Contract} token - ERC20 contract connected to signer
151
+ * @param {string} spender - Address to approve
152
+ * @param {bigint} amount - Amount to approve
153
+ * @param {ethers.Signer} signer
154
+ * @returns {Promise<boolean>} true if approval tx was sent
155
+ */
156
+ export async function ensureApproval(token, spender, amount, signer) {
157
+ const owner = await signer.getAddress();
158
+ const allowance = await token.allowance(owner, spender);
159
+ if (allowance >= amount) return false;
160
+
161
+ const tx = await token.approve(spender, ethers.MaxUint256);
162
+ await tx.wait();
163
+ return true;
164
+ }
165
+
166
+
167
+ // ──────────────────────────────────────────────────
168
+ // COMMON TOKEN ADDRESSES
169
+ // ──────────────────────────────────────────────────
170
+
171
+ export const TOKENS = {
172
+ base: {
173
+ WETH: '0x4200000000000000000000000000000000000006',
174
+ USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
175
+ USDbC: '0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA',
176
+ DAI: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb',
177
+ AERO: '0x940181a94A35A4569E4529A3CDfB74e38FD98631',
178
+ VIRTUAL: '0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b',
179
+ cbETH: '0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22',
180
+ },
181
+ ethereum: {
182
+ WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
183
+ USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
184
+ USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
185
+ DAI: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
186
+ WBTC: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
187
+ LINK: '0x514910771AF9Ca656af840dff83E8264EcF986CA',
188
+ },
189
+ polygon: {
190
+ WMATIC: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
191
+ USDC: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
192
+ USDT: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
193
+ },
194
+ arbitrum: {
195
+ WETH: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
196
+ USDC: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
197
+ ARB: '0x912CE59144191C1204E64559FE8253a0e49E6548',
198
+ GMX: '0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a',
199
+ },
200
+ };
201
+
202
+ /**
203
+ * Get USDC address for a chain
204
+ */
205
+ export function getUSDC(chain) {
206
+ return TOKENS[chain || getConfig('chain')]?.USDC;
207
+ }
208
+
209
+ /**
210
+ * Get WETH address for a chain
211
+ */
212
+ export function getWETH(chain) {
213
+ const c = chain || getConfig('chain');
214
+ if (c === 'polygon') return TOKENS.polygon.WMATIC;
215
+ return TOKENS[c]?.WETH;
216
+ }
217
+
218
+
219
+ // ──────────────────────────────────────────────────
220
+ // GAS HELPERS
221
+ // ──────────────────────────────────────────────────
222
+
223
+ /**
224
+ * Estimate gas cost in ETH
225
+ * @param {ethers.Provider} provider
226
+ * @param {bigint} gasLimit
227
+ * @returns {Promise<{gwei: string, ethCost: string, maxFee: bigint, priorityFee: bigint}>}
228
+ */
229
+ export async function estimateGasCost(provider, gasLimit = 21000n) {
230
+ const feeData = await provider.getFeeData();
231
+ const maxFee = feeData.maxFeePerGas || feeData.gasPrice || 0n;
232
+ const priorityFee = feeData.maxPriorityFeePerGas || 0n;
233
+ const totalCost = maxFee * gasLimit;
234
+
235
+ return {
236
+ gwei: ethers.formatUnits(maxFee, 'gwei'),
237
+ ethCost: ethers.formatEther(totalCost),
238
+ maxFee,
239
+ priorityFee,
240
+ gasLimit,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Get boosted gas settings (for snipes and priority txs)
246
+ * @param {ethers.Provider} provider
247
+ * @param {number} multiplier - Gas price multiplier (e.g., 1.5)
248
+ * @returns {Promise<{maxFeePerGas: bigint, maxPriorityFeePerGas: bigint}>}
249
+ */
250
+ export async function getBoostedGas(provider, multiplier = 1.5) {
251
+ const feeData = await provider.getFeeData();
252
+ const mult = BigInt(Math.floor(multiplier * 100));
253
+ return {
254
+ maxFeePerGas: feeData.maxFeePerGas ? (feeData.maxFeePerGas * mult) / 100n : undefined,
255
+ maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ? (feeData.maxPriorityFeePerGas * mult) / 100n : undefined,
256
+ };
257
+ }
258
+
259
+
260
+ // ──────────────────────────────────────────────────
261
+ // FORMATTING HELPERS
262
+ // ──────────────────────────────────────────────────
263
+
264
+ /**
265
+ * Format a number with commas (e.g., 1234567 → "1,234,567")
266
+ */
267
+ export function formatNumber(num) {
268
+ return Number(num).toLocaleString('en-US');
269
+ }
270
+
271
+ /**
272
+ * Format a large number compactly (e.g., 1234567 → "1.23M")
273
+ */
274
+ export function formatCompact(num) {
275
+ num = parseFloat(num);
276
+ if (isNaN(num)) return '0';
277
+ if (num >= 1e12) return (num / 1e12).toFixed(2) + 'T';
278
+ if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
279
+ if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
280
+ if (num >= 1e3) return (num / 1e3).toFixed(1) + 'K';
281
+ return num.toFixed(2);
282
+ }
283
+
284
+ /**
285
+ * Format a USD value
286
+ */
287
+ export function formatUSD(num) {
288
+ const n = parseFloat(num);
289
+ if (isNaN(n)) return '$0.00';
290
+ if (n < 0.01 && n > 0) return '$' + n.toPrecision(4);
291
+ return '$' + n.toFixed(2);
292
+ }
293
+
294
+ /**
295
+ * Format ETH amount
296
+ */
297
+ export function formatETH(wei, decimals = 6) {
298
+ return parseFloat(ethers.formatEther(wei)).toFixed(decimals) + ' ETH';
299
+ }
300
+
301
+ /**
302
+ * Format token amount with symbol
303
+ */
304
+ export function formatTokenAmount(raw, decimals, symbol) {
305
+ return parseFloat(ethers.formatUnits(raw, decimals)).toFixed(6) + ' ' + symbol;
306
+ }
307
+
308
+ /**
309
+ * Shorten an address (e.g., 0x1234...5678)
310
+ */
311
+ export function shortAddress(address, chars = 6) {
312
+ if (!address) return 'N/A';
313
+ return `${address.slice(0, chars)}...${address.slice(-4)}`;
314
+ }
315
+
316
+ /**
317
+ * Format a timestamp to local date/time string
318
+ */
319
+ export function formatTime(timestamp) {
320
+ return new Date(timestamp).toLocaleString();
321
+ }
322
+
323
+ /**
324
+ * Format a duration in seconds to human-readable
325
+ */
326
+ export function formatDuration(seconds) {
327
+ if (seconds < 60) return `${seconds}s`;
328
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
329
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
330
+ return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`;
331
+ }
332
+
333
+
334
+ // ──────────────────────────────────────────────────
335
+ // VALIDATION HELPERS
336
+ // ──────────────────────────────────────────────────
337
+
338
+ /**
339
+ * Validate an Ethereum address
340
+ */
341
+ export function isValidAddress(address) {
342
+ try {
343
+ return ethers.isAddress(address);
344
+ } catch {
345
+ return false;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Validate a private key
351
+ */
352
+ export function isValidPrivateKey(key) {
353
+ try {
354
+ new ethers.Wallet(key);
355
+ return true;
356
+ } catch {
357
+ return false;
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Validate a numeric amount (positive, non-zero)
363
+ */
364
+ export function isValidAmount(amount) {
365
+ const n = parseFloat(amount);
366
+ return !isNaN(n) && n > 0;
367
+ }
368
+
369
+ /**
370
+ * Parse a token amount to bigint with decimals
371
+ */
372
+ export function parseTokenAmount(amount, decimals) {
373
+ return ethers.parseUnits(amount.toString(), decimals);
374
+ }
375
+
376
+
377
+ // ──────────────────────────────────────────────────
378
+ // RETRY & TIMING HELPERS
379
+ // ──────────────────────────────────────────────────
380
+
381
+ /**
382
+ * Sleep for a given number of milliseconds
383
+ */
384
+ export function sleep(ms) {
385
+ return new Promise(resolve => setTimeout(resolve, ms));
386
+ }
387
+
388
+ /**
389
+ * Retry a function with exponential backoff
390
+ * @param {Function} fn - Async function to retry
391
+ * @param {number} maxRetries - Max number of retries
392
+ * @param {number} baseDelay - Base delay in ms (doubles each retry)
393
+ * @returns {Promise<any>}
394
+ */
395
+ export async function retry(fn, maxRetries = 3, baseDelay = 1000) {
396
+ let lastError;
397
+ for (let i = 0; i <= maxRetries; i++) {
398
+ try {
399
+ return await fn();
400
+ } catch (err) {
401
+ lastError = err;
402
+ if (i < maxRetries) {
403
+ const delay = baseDelay * Math.pow(2, i);
404
+ await sleep(delay);
405
+ }
406
+ }
407
+ }
408
+ throw lastError;
409
+ }
410
+
411
+ /**
412
+ * Wait for a transaction with timeout
413
+ * @param {ethers.TransactionResponse} tx
414
+ * @param {number} timeoutMs - Timeout in ms (default 120s)
415
+ * @returns {Promise<ethers.TransactionReceipt>}
416
+ */
417
+ export async function waitForTx(tx, timeoutMs = 120000) {
418
+ const receipt = await Promise.race([
419
+ tx.wait(),
420
+ new Promise((_, reject) =>
421
+ setTimeout(() => reject(new Error('Transaction timeout')), timeoutMs)
422
+ ),
423
+ ]);
424
+ return receipt;
425
+ }
426
+
427
+
428
+ // ──────────────────────────────────────────────────
429
+ // DEX / PRICE HELPERS
430
+ // ──────────────────────────────────────────────────
431
+
432
+ const DEXSCREENER_API = 'https://api.dexscreener.com/latest';
433
+
434
+ /**
435
+ * Quick price lookup via DexScreener
436
+ * @param {string} query - Token symbol or address
437
+ * @returns {Promise<{price: string, symbol: string, chain: string, liquidity: number} | null>}
438
+ */
439
+ export async function quickPrice(query) {
440
+ try {
441
+ const resp = await fetch(`${DEXSCREENER_API}/dex/search?q=${encodeURIComponent(query)}`);
442
+ const data = await resp.json();
443
+ const pair = (data.pairs || [])
444
+ .sort((a, b) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0))[0];
445
+ if (!pair) return null;
446
+ return {
447
+ price: pair.priceUsd,
448
+ symbol: pair.baseToken.symbol,
449
+ name: pair.baseToken.name,
450
+ chain: pair.chainId,
451
+ liquidity: pair.liquidity?.usd || 0,
452
+ volume24h: pair.volume?.h24 || 0,
453
+ change24h: pair.priceChange?.h24,
454
+ contract: pair.baseToken.address,
455
+ dex: pair.dexId,
456
+ };
457
+ } catch {
458
+ return null;
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Check if a token has sufficient liquidity for trading
464
+ * @param {string} query - Token symbol or address
465
+ * @param {number} minLiquidity - Minimum liquidity in USD (default $1000)
466
+ */
467
+ export async function hasLiquidity(query, minLiquidity = 1000) {
468
+ const data = await quickPrice(query);
469
+ if (!data) return false;
470
+ return data.liquidity >= minLiquidity;
471
+ }
472
+
473
+
474
+ // ──────────────────────────────────────────────────
475
+ // DISPLAY HELPERS (CLI-specific)
476
+ // ──────────────────────────────────────────────────
477
+
478
+ /**
479
+ * Show a transaction result card
480
+ */
481
+ export function showTxResult(receipt, opts = {}) {
482
+ const chain = opts.chain || getConfig('chain');
483
+
484
+ showSection(opts.title || 'TRANSACTION RESULT');
485
+ kvDisplay([
486
+ ['TX Hash', receipt.hash],
487
+ ['Block', receipt.blockNumber.toString()],
488
+ ['Gas Used', formatNumber(receipt.gasUsed.toString())],
489
+ ['Status', receipt.status === 1 ? theme.success('✓ Success') : theme.error('✗ Failed')],
490
+ ['Explorer', txUrl(receipt.hash, chain)],
491
+ ]);
492
+ }
493
+
494
+ /**
495
+ * Show a wallet summary card
496
+ */
497
+ export async function showWalletSummary(address, chain) {
498
+ const provider = getProvider(chain);
499
+ const spin = spinner('Fetching wallet info...').start();
500
+
501
+ try {
502
+ const balance = await provider.getBalance(address);
503
+ const usdcAddr = getUSDC(chain);
504
+ let usdcBalance = '0.00';
505
+
506
+ if (usdcAddr) {
507
+ try {
508
+ const { formatted } = await getTokenBalance(usdcAddr, address, provider);
509
+ usdcBalance = formatted;
510
+ } catch {}
511
+ }
512
+
513
+ const nonce = await provider.getTransactionCount(address);
514
+
515
+ spin.succeed('Wallet loaded');
516
+
517
+ showSection('WALLET SUMMARY');
518
+ kvDisplay([
519
+ ['Address', address],
520
+ ['Chain', chain || getConfig('chain')],
521
+ ['ETH', parseFloat(ethers.formatEther(balance)).toFixed(6)],
522
+ ['USDC', `$${parseFloat(usdcBalance).toFixed(2)}`],
523
+ ['TX Count', nonce.toString()],
524
+ ['Explorer', addressUrl(address, chain)],
525
+ ]);
526
+ } catch (err) {
527
+ spin.fail('Failed');
528
+ error(err.message);
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Show token info card
534
+ */
535
+ export async function showTokenInfo(tokenAddress, chain) {
536
+ const provider = getProvider(chain);
537
+ const spin = spinner('Fetching token info...').start();
538
+
539
+ try {
540
+ const info_data = await getFullTokenInfo(tokenAddress, provider);
541
+ const priceData = await quickPrice(tokenAddress);
542
+
543
+ spin.succeed('Token loaded');
544
+
545
+ showSection(`${info_data.symbol} — ${info_data.name}`);
546
+ const pairs = [
547
+ ['Contract', tokenAddress],
548
+ ['Symbol', info_data.symbol],
549
+ ['Name', info_data.name],
550
+ ['Decimals', info_data.decimals.toString()],
551
+ ['Total Supply', formatCompact(info_data.formattedSupply)],
552
+ ];
553
+
554
+ if (priceData) {
555
+ pairs.push(
556
+ ['Price', formatUSD(priceData.price)],
557
+ ['24h Change', priceData.change24h ? `${priceData.change24h}%` : 'N/A'],
558
+ ['Liquidity', formatUSD(priceData.liquidity)],
559
+ ['Volume 24h', formatUSD(priceData.volume24h)],
560
+ ['DEX', priceData.dex],
561
+ );
562
+ }
563
+
564
+ pairs.push(['Explorer', tokenUrl(tokenAddress, chain)]);
565
+ kvDisplay(pairs);
566
+ } catch (err) {
567
+ spin.fail('Failed');
568
+ error(err.message);
569
+ }
570
+ }
571
+
572
+
573
+ // ──────────────────────────────────────────────────
574
+ // TIPS & REFERENCE
575
+ // ──────────────────────────────────────────────────
576
+
577
+ /**
578
+ * Show trading tips
579
+ */
580
+ export function showTradingTips() {
581
+ showSection('TRADING TIPS');
582
+ const tips = [
583
+ ['Slippage', 'Use 0.5% for stables, 1-3% for volatile tokens, 5%+ for micro-caps'],
584
+ ['Gas Boost', 'Use 1.5-2x gas multiplier for snipes, 1.1x for normal swaps'],
585
+ ['Approvals', 'First trade of a token requires an approve tx (one-time)'],
586
+ ['Liquidity', 'Check liquidity before trading: darksol market token <SYMBOL>'],
587
+ ['MEV', 'Large swaps on mainnet may get sandwiched. Use private RPCs or L2s'],
588
+ ['Verify', 'Always verify contract addresses on block explorer before trading'],
589
+ ['Test First', 'Test with small amounts before running large scripts'],
590
+ ['Backup', 'Keep your wallet password backed up — no recovery if lost'],
591
+ ['DCA', 'Dollar-cost averaging reduces timing risk: darksol dca create'],
592
+ ['Stop Loss', 'Protect gains with stop-loss scripts: darksol script templates'],
593
+ ];
594
+
595
+ tips.forEach(([label, tip]) => {
596
+ console.log(` ${theme.gold('◆')} ${theme.label(label.padEnd(12))} ${theme.dim(tip)}`);
597
+ });
598
+ console.log('');
599
+ }
600
+
601
+ /**
602
+ * Show script writing tips
603
+ */
604
+ export function showScriptTips() {
605
+ showSection('SCRIPT WRITING TIPS');
606
+ const tips = [
607
+ ['Context', 'Scripts get { signer, provider, ethers, config, params } — full access'],
608
+ ['Signer', 'signer.address gives your wallet address, signer.sendTransaction() sends ETH'],
609
+ ['ERC20', 'Use helpers: getERC20(address, signer) for token interactions'],
610
+ ['Gas', 'Use getBoostedGas(provider, 1.5) for priority transactions'],
611
+ ['Retry', 'Use retry(fn, 3, 1000) for unreliable RPC calls'],
612
+ ['Sleep', 'Use sleep(ms) between polling iterations'],
613
+ ['Validation', 'Use isValidAddress(), isValidAmount() to validate inputs'],
614
+ ['Return', 'Return an object with results — it gets displayed after execution'],
615
+ ['Errors', 'Throw errors to signal failure — they get caught and displayed'],
616
+ ['Logging', 'Use console.log() inside scripts for live progress output'],
617
+ ];
618
+
619
+ tips.forEach(([label, tip]) => {
620
+ console.log(` ${theme.gold('◆')} ${theme.label(label.padEnd(12))} ${theme.dim(tip)}`);
621
+ });
622
+ console.log('');
623
+ }
624
+
625
+ /**
626
+ * Show network reference
627
+ */
628
+ export function showNetworkReference() {
629
+ showSection('NETWORK REFERENCE');
630
+
631
+ const rows = Object.entries(CHAIN_IDS).map(([chain, id]) => [
632
+ theme.gold(chain),
633
+ id.toString(),
634
+ EXPLORERS[chain],
635
+ getUSDC(chain) ? shortAddress(getUSDC(chain)) : theme.dim('N/A'),
636
+ ]);
637
+
638
+ table(['Chain', 'ID', 'Explorer', 'USDC'], rows);
639
+ }
640
+
641
+ /**
642
+ * Show quick-start guide
643
+ */
644
+ export function showQuickStart() {
645
+ showSection('QUICK START GUIDE');
646
+
647
+ console.log('');
648
+ console.log(theme.gold(' 1. Create a wallet'));
649
+ console.log(theme.dim(' darksol wallet create my-wallet'));
650
+ console.log('');
651
+ console.log(theme.gold(' 2. Fund it with ETH'));
652
+ console.log(theme.dim(' Send ETH to your wallet address on Base'));
653
+ console.log('');
654
+ console.log(theme.gold(' 3. Check balance'));
655
+ console.log(theme.dim(' darksol wallet balance'));
656
+ console.log('');
657
+ console.log(theme.gold(' 4. Look up a token'));
658
+ console.log(theme.dim(' darksol market token VIRTUAL'));
659
+ console.log('');
660
+ console.log(theme.gold(' 5. Swap tokens'));
661
+ console.log(theme.dim(' darksol trade swap -i ETH -o USDC -a 0.01'));
662
+ console.log('');
663
+ console.log(theme.gold(' 6. Create a trading script'));
664
+ console.log(theme.dim(' darksol script create'));
665
+ console.log('');
666
+ console.log(theme.gold(' 7. Set up DCA'));
667
+ console.log(theme.dim(' darksol dca create'));
668
+ console.log('');
669
+ console.log(theme.gold(' 8. Configure custom RPC'));
670
+ console.log(theme.dim(' darksol config rpc base https://your-rpc.com'));
671
+ console.log('');
672
+
673
+ info('Run any command with --help for full options');
674
+ info('Run darksol tips for trading tips');
675
+ info('Run darksol networks for chain reference');
676
+ console.log('');
677
+ }