@darksol/terminal 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -184,6 +184,105 @@ JSON output mode for programmatic use:
184
184
  darksol config set output json
185
185
  ```
186
186
 
187
+ ## Helper Functions
188
+
189
+ When writing custom execution scripts, you have access to powerful helper utilities:
190
+
191
+ ```javascript
192
+ import {
193
+ // Providers & Chain
194
+ getProvider, // Get ethers provider for any chain
195
+ CHAIN_IDS, // { base: 8453, ethereum: 1, ... }
196
+ EXPLORERS, // Block explorer URLs per chain
197
+ txUrl, addressUrl, // Generate explorer links
198
+
199
+ // Tokens
200
+ getERC20, // Get ERC20 contract instance
201
+ getFullTokenInfo, // Name, symbol, decimals, totalSupply
202
+ getTokenBalance, // Formatted balance for any token
203
+ ensureApproval, // Check & approve token spending
204
+ TOKENS, // All known token addresses per chain
205
+ getUSDC, getWETH, // Quick chain-specific lookups
206
+
207
+ // Gas
208
+ estimateGasCost, // Estimate gas in ETH
209
+ getBoostedGas, // Priority gas settings for snipes
210
+
211
+ // Formatting
212
+ formatCompact, // 1234567 → "1.23M"
213
+ formatUSD, // Format as $1,234.56
214
+ formatETH, // Format wei to ETH string
215
+ formatTokenAmount, // Format with symbol
216
+ shortAddress, // 0x1234...5678
217
+ formatDuration, // Seconds → "2h 30m"
218
+
219
+ // Validation
220
+ isValidAddress, // Check Ethereum address
221
+ isValidPrivateKey, // Check private key format
222
+ isValidAmount, // Check positive number
223
+ parseTokenAmount, // String → bigint with decimals
224
+
225
+ // Async
226
+ sleep, // await sleep(1000)
227
+ retry, // Retry with exponential backoff
228
+ waitForTx, // Wait for tx with timeout
229
+
230
+ // Price
231
+ quickPrice, // DexScreener price lookup
232
+ hasLiquidity, // Check minimum liquidity
233
+ } from './utils/helpers.js';
234
+ ```
235
+
236
+ ### Example: Custom Script Using Helpers
237
+
238
+ ```javascript
239
+ module.exports = async function({ signer, provider, ethers, config, params }) {
240
+ // Import helpers (available in script context)
241
+ const helpers = await import('@darksol/terminal/src/utils/helpers.js');
242
+
243
+ // Check if token has enough liquidity
244
+ const liquid = await helpers.hasLiquidity(params.token, 5000);
245
+ if (!liquid) throw new Error('Insufficient liquidity');
246
+
247
+ // Get price
248
+ const price = await helpers.quickPrice(params.token);
249
+ console.log(`Price: ${helpers.formatUSD(price.price)}`);
250
+
251
+ // Get boosted gas for priority
252
+ const gas = await helpers.getBoostedGas(provider, 1.5);
253
+
254
+ // Execute trade with retry
255
+ const result = await helpers.retry(async () => {
256
+ const tx = await signer.sendTransaction({ ...txParams, ...gas });
257
+ return helpers.waitForTx(tx, 60000);
258
+ }, 3, 2000);
259
+
260
+ return { txHash: result.hash, price: price.price };
261
+ };
262
+ ```
263
+
264
+ ## Tips & Reference
265
+
266
+ ```bash
267
+ # Trading tips (slippage, MEV protection, etc.)
268
+ darksol tips --trading
269
+
270
+ # Script writing tips
271
+ darksol tips --scripts
272
+
273
+ # Both
274
+ darksol tips
275
+
276
+ # Network reference (chains, IDs, explorers, USDC addresses)
277
+ darksol networks
278
+
279
+ # Getting started guide
280
+ darksol quickstart
281
+
282
+ # Look up any address (auto-detects token vs wallet)
283
+ darksol lookup 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
284
+ ```
285
+
187
286
  ## Development
188
287
 
189
288
  ```bash
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "darksol": "./bin/darksol.js"
7
+ "darksol": "bin/darksol.js"
8
8
  },
9
9
  "main": "./src/cli.js",
10
10
  "scripts": {
@@ -12,7 +12,16 @@
12
12
  "dev": "node bin/darksol.js dashboard",
13
13
  "test": "node --test tests/*.test.js"
14
14
  },
15
- "keywords": ["darksol", "crypto", "trading", "cli", "x402", "base", "ethereum", "defi"],
15
+ "keywords": [
16
+ "darksol",
17
+ "crypto",
18
+ "trading",
19
+ "cli",
20
+ "x402",
21
+ "base",
22
+ "ethereum",
23
+ "defi"
24
+ ],
16
25
  "author": "DARKSOL <chris00claw@gmail.com>",
17
26
  "license": "MIT",
18
27
  "dependencies": {
package/src/cli.js CHANGED
@@ -14,6 +14,7 @@ import { cardsCatalog, cardsOrder, cardsStatus } from './services/cards.js';
14
14
  import { facilitatorHealth, facilitatorVerify, facilitatorSettle } from './services/facilitator.js';
15
15
  import { buildersLeaderboard, buildersLookup, buildersFeed } from './services/builders.js';
16
16
  import { createScript, listScripts, runScript, showScript, editScript, deleteScript, cloneScript, listTemplates } from './scripts/engine.js';
17
+ import { showTradingTips, showScriptTips, showNetworkReference, showQuickStart, showWalletSummary, showTokenInfo, showTxResult } from './utils/helpers.js';
17
18
 
18
19
  export function cli(argv) {
19
20
  const program = new Command();
@@ -288,6 +289,61 @@ export function cli(argv) {
288
289
  .description('Settle payment on-chain')
289
290
  .action((payment) => facilitatorSettle(payment));
290
291
 
292
+ // ═══════════════════════════════════════
293
+ // TIPS & REFERENCE COMMANDS
294
+ // ═══════════════════════════════════════
295
+ program
296
+ .command('tips')
297
+ .description('Show trading and script writing tips')
298
+ .option('-t, --trading', 'Trading tips only')
299
+ .option('-s, --scripts', 'Script writing tips only')
300
+ .action((opts) => {
301
+ showMiniBanner();
302
+ if (opts.scripts) {
303
+ showScriptTips();
304
+ } else if (opts.trading) {
305
+ showTradingTips();
306
+ } else {
307
+ showTradingTips();
308
+ showScriptTips();
309
+ }
310
+ });
311
+
312
+ program
313
+ .command('networks')
314
+ .description('Show supported networks and chain info')
315
+ .action(() => {
316
+ showMiniBanner();
317
+ showNetworkReference();
318
+ });
319
+
320
+ program
321
+ .command('quickstart')
322
+ .description('Show getting started guide')
323
+ .action(() => {
324
+ showMiniBanner();
325
+ showQuickStart();
326
+ });
327
+
328
+ program
329
+ .command('lookup <address>')
330
+ .description('Look up a token or wallet address on-chain')
331
+ .option('-c, --chain <chain>', 'Chain to query')
332
+ .action(async (address, opts) => {
333
+ showMiniBanner();
334
+ if (address.length === 42 && address.startsWith('0x')) {
335
+ // Could be token or wallet — try token first
336
+ try {
337
+ await showTokenInfo(address, opts.chain);
338
+ } catch {
339
+ await showWalletSummary(address, opts.chain);
340
+ }
341
+ } else {
342
+ const { error } = await import('./ui/components.js');
343
+ error('Provide a valid 0x address');
344
+ }
345
+ });
346
+
291
347
  // ═══════════════════════════════════════
292
348
  // SCRIPT COMMANDS
293
349
  // ═══════════════════════════════════════
@@ -418,6 +474,10 @@ export function cli(argv) {
418
474
  ['builders', 'ERC-8021 builder index'],
419
475
  ['facilitator', 'x402 payment facilitator'],
420
476
  ['config', 'Terminal configuration'],
477
+ ['tips', 'Trading & scripting tips'],
478
+ ['networks', 'Chain reference & explorers'],
479
+ ['quickstart', 'Getting started guide'],
480
+ ['lookup', 'Look up any address on-chain'],
421
481
  ];
422
482
 
423
483
  commands.forEach(([cmd, desc]) => {
package/src/ui/banner.js CHANGED
@@ -26,7 +26,7 @@ export function showBanner(opts = {}) {
26
26
  );
27
27
  console.log(
28
28
  theme.dim(' ║ ') +
29
- theme.subtle(' v0.1.0') +
29
+ theme.subtle(' v0.1.1') +
30
30
  theme.dim(' ') +
31
31
  theme.gold('🌑') +
32
32
  theme.dim(' ║')
@@ -44,7 +44,7 @@ export function showBanner(opts = {}) {
44
44
 
45
45
  export function showMiniBanner() {
46
46
  console.log('');
47
- console.log(theme.gold.bold(' 🌑 DARKSOL TERMINAL') + theme.dim(' v0.1.0'));
47
+ console.log(theme.gold.bold(' 🌑 DARKSOL TERMINAL') + theme.dim(' v0.1.1'));
48
48
  console.log(theme.dim(' ─────────────────────────────'));
49
49
  console.log('');
50
50
  }
@@ -58,3 +58,4 @@ export function showSection(title) {
58
58
  export function showDivider() {
59
59
  console.log(theme.dim(' ' + '─'.repeat(50)));
60
60
  }
61
+
@@ -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
+ }