@darksol/terminal 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,7 @@ darksol bridge status 0xTxHash...
57
57
  darksol bridge chains
58
58
 
59
59
  # Cross-DEX arbitrage
60
- darksol arb scan --chain base # one-shot price comparison
60
+ darksol arb scan --chain base # AI-scored DEX price comparison
61
61
  darksol arb monitor --chain base --execute # real-time block-by-block scanning
62
62
  darksol arb config # set thresholds, dry-run, DEXes
63
63
  darksol arb add-endpoint base wss://your-quicknode # faster with WSS endpoints
@@ -65,6 +65,12 @@ darksol arb add-pair WETH AERO # add pairs to scan
65
65
  darksol arb stats --days 7 # PnL history
66
66
  darksol arb info # setup guide + risk warnings
67
67
 
68
+ # AI arbitrage intelligence
69
+ darksol arb ai # strategy briefing + recommendations
70
+ darksol arb discover --chain base # AI pair discovery
71
+ darksol arb tune --chain base # AI threshold optimization
72
+ darksol arb learn --chain base # learn from history patterns
73
+
68
74
  # Set up your agent identity
69
75
  darksol soul
70
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
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": {
package/src/cli.js CHANGED
@@ -17,6 +17,8 @@ import { executeSwap } from './trading/swap.js';
17
17
  import { snipeToken, watchSnipe } from './trading/snipe.js';
18
18
  import { createDCA, listDCA, cancelDCA, runDCA } from './trading/dca.js';
19
19
  import { arbScan, arbMonitor, arbExecute, arbStats, arbConfig, arbAddEndpoint, arbAddPair, arbRemovePair, arbInfo } from './trading/arb.js';
20
+ import { aiDiscoverPairs, aiTuneThresholds, aiStrategyBriefing, aiLearn } from './trading/arb-ai.js';
21
+ import { listApprovals, revokeApproval, checkSpecificApproval } from './services/approvals.js';
20
22
  import { executeLifiSwap, executeLifiBridge, checkBridgeStatus, showSupportedChains } from './services/lifi.js';
21
23
  import { topMovers, tokenDetail, compareTokens } from './services/market.js';
22
24
  import { oracleFlip, oracleDice, oracleNumber, oracleShuffle, oracleHealth } from './services/oracle.js';
@@ -400,6 +402,30 @@ export function cli(argv) {
400
402
  .description('How arbitrage works, setup guide, and risk warnings')
401
403
  .action(() => arbInfo());
402
404
 
405
+ arb
406
+ .command('ai')
407
+ .description('AI strategy briefing — assessment, recommendations, next actions')
408
+ .option('-c, --chain <chain>', 'Target chain', 'base')
409
+ .action((opts) => aiStrategyBriefing({ chain: opts.chain }));
410
+
411
+ arb
412
+ .command('discover')
413
+ .description('AI-powered pair discovery — find new opportunities, drop dead pairs')
414
+ .option('-c, --chain <chain>', 'Target chain', 'base')
415
+ .action((opts) => aiDiscoverPairs({ chain: opts.chain }));
416
+
417
+ arb
418
+ .command('tune')
419
+ .description('AI threshold tuning — optimize min profit, trade size, gas ceiling')
420
+ .option('-c, --chain <chain>', 'Target chain', 'base')
421
+ .action((opts) => aiTuneThresholds({ chain: opts.chain }));
422
+
423
+ arb
424
+ .command('learn')
425
+ .description('Run AI learning cycle — analyze history and update patterns')
426
+ .option('-c, --chain <chain>', 'Target chain', 'base')
427
+ .action((opts) => aiLearn({ chain: opts.chain }));
428
+
403
429
  const auto = program
404
430
  .command('auto')
405
431
  .description('Autonomous trader mode - goal-based automated execution');
@@ -748,6 +774,33 @@ export function cli(argv) {
748
774
  .description('Settle payment on-chain')
749
775
  .action((payment) => facilitatorSettle(payment));
750
776
 
777
+ // ═══════════════════════════════════════
778
+ // APPROVALS COMMANDS
779
+ // ═══════════════════════════════════════
780
+ const approvals = program
781
+ .command('approvals')
782
+ .description('🔐 Token approval manager — view and revoke ERC-20 approvals');
783
+
784
+ approvals
785
+ .command('list')
786
+ .alias('ls')
787
+ .description('List all active token approvals')
788
+ .option('-c, --chain <chain>', 'Target chain', 'base')
789
+ .action((opts) => listApprovals(opts));
790
+
791
+ approvals
792
+ .command('revoke')
793
+ .description('Interactively revoke token approvals')
794
+ .option('-c, --chain <chain>', 'Target chain', 'base')
795
+ .option('-a, --all', 'Revoke ALL approvals')
796
+ .action((opts) => revokeApproval(opts));
797
+
798
+ approvals
799
+ .command('check <token> <spender>')
800
+ .description('Check specific token + spender approval')
801
+ .option('-c, --chain <chain>', 'Target chain', 'base')
802
+ .action((token, spender, opts) => checkSpecificApproval(token, spender, opts));
803
+
751
804
  // ═══════════════════════════════════════
752
805
  // MAIL COMMANDS
753
806
  // ═══════════════════════════════════════
@@ -2020,6 +2073,7 @@ function showCommandList() {
2020
2073
  ['builders', 'ERC-8021 builder index'],
2021
2074
  ['mail', 'AgentMail - email for your agent'],
2022
2075
  ['facilitator', 'x402 payment facilitator'],
2076
+ ['approvals', 'Token approval manager'],
2023
2077
  ['skills', 'Agent skill directory'],
2024
2078
  ['browser', 'Playwright browser automation'],
2025
2079
  ['daemon', 'Background service daemon'],
package/src/llm/intent.js CHANGED
@@ -68,6 +68,7 @@ ACTIONS (use the most specific one):
68
68
  - "casino" — play a casino game (coinflip, dice, hilo, slots). All bets are $1 USDC. (e.g. "flip a coin", "bet on heads", "play slots", "roll dice over 3")
69
69
  - "arb_scan" — scan for cross-DEX arbitrage opportunities (e.g. "find arb opportunities", "scan for price differences", "check arb on base")
70
70
  - "arb_monitor" — start real-time arbitrage monitoring (e.g. "monitor arb", "watch for arb opportunities")
71
+ - "approvals" — check or revoke ERC-20 token approvals (e.g. "check my approvals", "revoke approvals", "what tokens have I approved", "show unlimited approvals")
71
72
  - "unknown" — can't determine what the user wants
72
73
 
73
74
  CASINO GAMES:
@@ -680,6 +681,18 @@ export async function executeIntent(intent, opts = {}) {
680
681
  return { success: false, reason: 'Tell me which token to look at.' };
681
682
  }
682
683
 
684
+ case 'approvals': {
685
+ const { listApprovals, revokeApproval } = await import('../services/approvals.js');
686
+ const chain = intent.chain || opts.chain || 'base';
687
+ // If they mention "revoke", go to revoke flow
688
+ if (intent.raw && /revoke|remove|clear/i.test(intent.raw)) {
689
+ await revokeApproval({ chain });
690
+ } else {
691
+ await listApprovals({ chain });
692
+ }
693
+ return { success: true, action: 'approvals' };
694
+ }
695
+
683
696
  default:
684
697
  warn(`I don't know how to do "${intent.action}" yet.`);
685
698
  if (intent.command) {
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Token Approvals Manager
3
+ * View, analyze, and revoke ERC-20 token approvals for the active wallet.
4
+ * Security-first: identifies unlimited approvals, risky spenders, and stale approvals.
5
+ *
6
+ * Built by DARKSOL 🌑
7
+ */
8
+
9
+ import { ethers } from 'ethers';
10
+ import { getConfig, getRPC } from '../config/store.js';
11
+ import { theme } from '../ui/theme.js';
12
+ import { success, error, warn, info, kvDisplay } from '../ui/components.js';
13
+ import { getKey as getKeyAuto } from '../config/keys.js';
14
+ import inquirer from 'inquirer';
15
+
16
+ // Minimal ERC-20 ABI for approval operations
17
+ const ERC20_ABI = [
18
+ 'function allowance(address owner, address spender) view returns (uint256)',
19
+ 'function approve(address spender, uint256 amount) returns (bool)',
20
+ 'function symbol() view returns (string)',
21
+ 'function decimals() view returns (uint8)',
22
+ 'function name() view returns (string)',
23
+ 'function balanceOf(address) view returns (uint256)',
24
+ 'event Approval(address indexed owner, address indexed spender, uint256 value)',
25
+ ];
26
+
27
+ // Known spender labels
28
+ const KNOWN_SPENDERS = {
29
+ // Uniswap
30
+ '0x2626664c2603336E57B271c5C0b26F421741e481': { name: 'Uniswap SwapRouter02', risk: 'low' },
31
+ '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD': { name: 'Uniswap Universal Router', risk: 'low' },
32
+ '0x000000000022D473030F116dDEE9F6B43aC78BA3': { name: 'Permit2', risk: 'low' },
33
+ // Aerodrome
34
+ '0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43': { name: 'Aerodrome Router', risk: 'low' },
35
+ // SushiSwap
36
+ '0xFB7eF66a7e61224DD6FcD0D7d9C3Ae5B8B1fF3B5': { name: 'SushiSwap V3 Router', risk: 'low' },
37
+ // LI.FI
38
+ '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE': { name: 'LI.FI Diamond', risk: 'low' },
39
+ // 1inch
40
+ '0x111111125421cA6dc452d289314280a0f8842A65': { name: '1inch Router v6', risk: 'low' },
41
+ // Aave
42
+ '0xA238Dd80C259a72e81d7e4664a9801593F98d1c5': { name: 'Aave V3 Pool (Base)', risk: 'low' },
43
+ };
44
+
45
+ // Chain-specific block explorer API base URLs
46
+ const EXPLORER_APIS = {
47
+ base: 'https://api.basescan.org/api',
48
+ ethereum: 'https://api.etherscan.io/api',
49
+ arbitrum: 'https://api.arbiscan.io/api',
50
+ optimism: 'https://api-optimistic.etherscan.io/api',
51
+ polygon: 'https://api.polygonscan.com/api',
52
+ };
53
+
54
+ const UNLIMITED_THRESHOLD = ethers.MaxUint256 / 2n;
55
+
56
+ /**
57
+ * Get provider for the specified chain
58
+ */
59
+ function getProvider(chain = 'base') {
60
+ const rpc = getRPC(chain);
61
+ if (!rpc) throw new Error(`No RPC configured for ${chain}. Run: darksol config rpc ${chain} <url>`);
62
+ return new ethers.JsonRpcProvider(rpc);
63
+ }
64
+
65
+ /**
66
+ * Get wallet signer
67
+ */
68
+ async function getSigner(chain = 'base') {
69
+ const provider = getProvider(chain);
70
+ const activeWallet = getConfig('activeWallet');
71
+ if (!activeWallet) throw new Error('No active wallet. Run: darksol wallet use <name>');
72
+
73
+ const wallets = getConfig('wallets') || {};
74
+ const walletData = wallets[activeWallet];
75
+ if (!walletData) throw new Error(`Wallet "${activeWallet}" not found`);
76
+
77
+ // Prompt for password to decrypt
78
+ const { password } = await inquirer.prompt([{
79
+ type: 'password',
80
+ name: 'password',
81
+ message: theme.gold('Wallet password:'),
82
+ mask: '•',
83
+ }]);
84
+
85
+ const decrypted = await ethers.Wallet.fromEncryptedJson(
86
+ JSON.stringify(walletData.keystore),
87
+ password
88
+ );
89
+
90
+ return decrypted.connect(provider);
91
+ }
92
+
93
+ /**
94
+ * Fetch approval events from block explorer API
95
+ */
96
+ async function fetchApprovalEvents(address, chain = 'base') {
97
+ const apiBase = EXPLORER_APIS[chain];
98
+ if (!apiBase) {
99
+ warn(`No block explorer API for ${chain} — falling back to manual check`);
100
+ return [];
101
+ }
102
+
103
+ const apiKey = getKeyAuto('etherscan') || getKeyAuto('basescan') || '';
104
+ const url = `${apiBase}?module=account&action=tokentx&address=${address}&startblock=0&endblock=99999999&sort=desc&apikey=${apiKey}`;
105
+
106
+ try {
107
+ const response = await fetch(url);
108
+ const data = await response.json();
109
+ if (data.status !== '1' || !data.result) return [];
110
+
111
+ // Extract unique token contracts the wallet has interacted with
112
+ const tokens = new Set();
113
+ for (const tx of data.result) {
114
+ tokens.add(tx.contractAddress.toLowerCase());
115
+ }
116
+ return [...tokens];
117
+ } catch (err) {
118
+ warn(`Explorer API error: ${err.message}`);
119
+ return [];
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Check approval amount for a specific token + spender
125
+ */
126
+ async function checkApproval(provider, tokenAddress, owner, spender) {
127
+ try {
128
+ const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
129
+ const allowance = await contract.allowance(owner, spender);
130
+ return allowance;
131
+ } catch {
132
+ return 0n;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Get token info (symbol, decimals, name)
138
+ */
139
+ async function getTokenInfo(provider, tokenAddress) {
140
+ try {
141
+ const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
142
+ const [symbol, decimals, name] = await Promise.all([
143
+ contract.symbol().catch(() => '???'),
144
+ contract.decimals().catch(() => 18),
145
+ contract.name().catch(() => 'Unknown'),
146
+ ]);
147
+ return { symbol, decimals: Number(decimals), name, address: tokenAddress };
148
+ } catch {
149
+ return { symbol: '???', decimals: 18, name: 'Unknown', address: tokenAddress };
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Format approval amount for display
155
+ */
156
+ function formatApproval(amount, decimals) {
157
+ if (amount >= UNLIMITED_THRESHOLD) {
158
+ return theme.error('♾️ UNLIMITED');
159
+ }
160
+ const formatted = ethers.formatUnits(amount, decimals);
161
+ const num = parseFloat(formatted);
162
+ if (num > 1e12) return theme.warn(`${(num / 1e12).toFixed(2)}T`);
163
+ if (num > 1e9) return theme.warn(`${(num / 1e9).toFixed(2)}B`);
164
+ if (num > 1e6) return `${(num / 1e6).toFixed(2)}M`;
165
+ if (num > 1e3) return `${(num / 1e3).toFixed(2)}K`;
166
+ return formatted;
167
+ }
168
+
169
+ /**
170
+ * Get risk label for a spender
171
+ */
172
+ function getSpenderInfo(spenderAddress) {
173
+ const normalized = ethers.getAddress(spenderAddress);
174
+ const known = KNOWN_SPENDERS[normalized];
175
+ if (known) return known;
176
+ return { name: `Unknown (${normalized.slice(0, 6)}...${normalized.slice(-4)})`, risk: 'unknown' };
177
+ }
178
+
179
+ /**
180
+ * Risk color
181
+ */
182
+ function riskColor(risk) {
183
+ switch (risk) {
184
+ case 'low': return theme.success;
185
+ case 'medium': return theme.warn;
186
+ case 'high': return theme.error;
187
+ case 'unknown': return theme.error;
188
+ default: return theme.dim;
189
+ }
190
+ }
191
+
192
+ // Common router/spender addresses to check per chain
193
+ const COMMON_SPENDERS = {
194
+ base: [
195
+ '0x2626664c2603336E57B271c5C0b26F421741e481', // Uniswap SwapRouter02
196
+ '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', // Uniswap Universal Router
197
+ '0x000000000022D473030F116dDEE9F6B43aC78BA3', // Permit2
198
+ '0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43', // Aerodrome Router
199
+ '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', // LI.FI
200
+ ],
201
+ ethereum: [
202
+ '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', // Uniswap SwapRouter02
203
+ '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', // Universal Router
204
+ '0x000000000022D473030F116dDEE9F6B43aC78BA3', // Permit2
205
+ '0x111111125421cA6dc452d289314280a0f8842A65', // 1inch v6
206
+ '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', // LI.FI
207
+ ],
208
+ arbitrum: [
209
+ '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', // Uniswap SwapRouter02
210
+ '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', // Universal Router
211
+ '0x000000000022D473030F116dDEE9F6B43aC78BA3', // Permit2
212
+ '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', // LI.FI
213
+ ],
214
+ optimism: [
215
+ '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', // Uniswap SwapRouter02
216
+ '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', // Universal Router
217
+ '0x000000000022D473030F116dDEE9F6B43aC78BA3', // Permit2
218
+ '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', // LI.FI
219
+ ],
220
+ polygon: [
221
+ '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', // Uniswap SwapRouter02
222
+ '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', // Universal Router
223
+ '0x000000000022D473030F116dDEE9F6B43aC78BA3', // Permit2
224
+ '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', // LI.FI
225
+ ],
226
+ };
227
+
228
+ /**
229
+ * List all token approvals for the active wallet
230
+ */
231
+ export async function listApprovals(opts = {}) {
232
+ const chain = opts.chain || getConfig('defaultChain') || 'base';
233
+ const activeWallet = getConfig('activeWallet');
234
+ if (!activeWallet) return error('No active wallet. Run: darksol wallet use <name>');
235
+
236
+ const wallets = getConfig('wallets') || {};
237
+ const walletData = wallets[activeWallet];
238
+ if (!walletData) return error(`Wallet "${activeWallet}" not found`);
239
+
240
+ const address = walletData.address;
241
+ const provider = getProvider(chain);
242
+
243
+ console.log(theme.gold('\n🔍 Scanning Token Approvals'));
244
+ console.log(theme.dim(` Wallet: ${address}`));
245
+ console.log(theme.dim(` Chain: ${chain}\n`));
246
+
247
+ // Step 1: Get token contracts the wallet has interacted with
248
+ info('Fetching token interactions from block explorer...');
249
+ const tokenAddresses = await fetchApprovalEvents(address, chain);
250
+
251
+ if (tokenAddresses.length === 0) {
252
+ warn('No token transactions found via explorer. Checking common tokens...');
253
+ }
254
+
255
+ // Add common tokens (USDC, WETH, etc.) per chain
256
+ const commonTokens = {
257
+ base: ['0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', '0x4200000000000000000000000000000000000006'],
258
+ ethereum: ['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'],
259
+ arbitrum: ['0xaf88d065e77c8cC2239327C5EDb3A432268e5831', '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1'],
260
+ optimism: ['0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', '0x4200000000000000000000000000000000000006'],
261
+ polygon: ['0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270'],
262
+ };
263
+
264
+ const allTokens = new Set([...tokenAddresses, ...(commonTokens[chain] || []).map(t => t.toLowerCase())]);
265
+ const spenders = COMMON_SPENDERS[chain] || COMMON_SPENDERS.base;
266
+
267
+ // Step 2: Check each token against known spenders
268
+ const approvals = [];
269
+ let checked = 0;
270
+ const total = allTokens.size * spenders.length;
271
+
272
+ for (const tokenAddr of allTokens) {
273
+ const tokenInfo = await getTokenInfo(provider, tokenAddr);
274
+
275
+ for (const spender of spenders) {
276
+ checked++;
277
+ process.stdout.write(`\r${theme.dim(` Checking ${checked}/${total}...`)}`);
278
+
279
+ const allowance = await checkApproval(provider, tokenAddr, address, spender);
280
+ if (allowance > 0n) {
281
+ const spenderInfo = getSpenderInfo(spender);
282
+ approvals.push({
283
+ token: tokenInfo,
284
+ spender: spender,
285
+ spenderInfo,
286
+ allowance,
287
+ isUnlimited: allowance >= UNLIMITED_THRESHOLD,
288
+ });
289
+ }
290
+ }
291
+ }
292
+
293
+ console.log('\r' + ' '.repeat(50)); // Clear progress line
294
+
295
+ if (approvals.length === 0) {
296
+ success('No active approvals found. Your wallet is clean! ✨');
297
+ return { approvals: [], stats: { total: 0, unlimited: 0, unknown: 0 } };
298
+ }
299
+
300
+ // Step 3: Display results
301
+ console.log(theme.gold(`\n📋 Active Approvals (${approvals.length}):\n`));
302
+
303
+ const unlimited = approvals.filter(a => a.isUnlimited);
304
+ const unknownSpenders = approvals.filter(a => a.spenderInfo.risk === 'unknown');
305
+
306
+ for (const a of approvals) {
307
+ const riskFn = riskColor(a.spenderInfo.risk);
308
+ const amount = formatApproval(a.allowance, a.token.decimals);
309
+ console.log(` ${theme.gold(a.token.symbol.padEnd(8))} → ${riskFn(a.spenderInfo.name)}`);
310
+ console.log(` ${theme.dim('Amount:')} ${amount} ${theme.dim('Risk:')} ${riskFn(a.spenderInfo.risk.toUpperCase())}`);
311
+ console.log(` ${theme.dim('Token:')} ${a.token.address}`);
312
+ console.log(` ${theme.dim('Spender:')} ${a.spender}\n`);
313
+ }
314
+
315
+ // Summary
316
+ console.log(theme.gold('─'.repeat(50)));
317
+ console.log(` ${theme.gold('Total approvals:')} ${approvals.length}`);
318
+ if (unlimited.length > 0) {
319
+ console.log(` ${theme.error('♾️ Unlimited:')} ${unlimited.length}`);
320
+ }
321
+ if (unknownSpenders.length > 0) {
322
+ console.log(` ${theme.error('⚠️ Unknown spenders:')} ${unknownSpenders.length}`);
323
+ }
324
+
325
+ if (unlimited.length > 0) {
326
+ console.log(`\n${theme.warn('⚠️ You have unlimited approvals. Consider revoking with:')}`)
327
+ console.log(theme.dim(' darksol approvals revoke'));
328
+ }
329
+
330
+ return {
331
+ approvals,
332
+ stats: {
333
+ total: approvals.length,
334
+ unlimited: unlimited.length,
335
+ unknown: unknownSpenders.length,
336
+ },
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Revoke a specific or all approvals
342
+ */
343
+ export async function revokeApproval(opts = {}) {
344
+ const chain = opts.chain || getConfig('defaultChain') || 'base';
345
+
346
+ // First list current approvals
347
+ const { approvals } = await listApprovals({ chain });
348
+ if (!approvals || approvals.length === 0) return;
349
+
350
+ // Build choices
351
+ const choices = approvals.map((a, i) => ({
352
+ name: `${a.token.symbol} → ${a.spenderInfo.name} (${a.isUnlimited ? '♾️ UNLIMITED' : formatApproval(a.allowance, a.token.decimals)})`,
353
+ value: i,
354
+ }));
355
+
356
+ if (opts.all) {
357
+ // Revoke all
358
+ const { confirm } = await inquirer.prompt([{
359
+ type: 'confirm',
360
+ name: 'confirm',
361
+ message: theme.warn(`Revoke ALL ${approvals.length} approvals? This will cost gas for each transaction.`),
362
+ default: false,
363
+ }]);
364
+ if (!confirm) return info('Cancelled.');
365
+
366
+ const signer = await getSigner(chain);
367
+ let revoked = 0;
368
+ for (const a of approvals) {
369
+ try {
370
+ const contract = new ethers.Contract(a.token.address, ERC20_ABI, signer);
371
+ const tx = await contract.approve(a.spender, 0);
372
+ console.log(` ${theme.dim('TX:')} ${tx.hash}`);
373
+ await tx.wait();
374
+ success(`Revoked ${a.token.symbol} → ${a.spenderInfo.name}`);
375
+ revoked++;
376
+ } catch (err) {
377
+ error(`Failed to revoke ${a.token.symbol}: ${err.message}`);
378
+ }
379
+ }
380
+ success(`\nRevoked ${revoked}/${approvals.length} approvals.`);
381
+ return;
382
+ }
383
+
384
+ // Interactive selection
385
+ const { selected } = await inquirer.prompt([{
386
+ type: 'checkbox',
387
+ name: 'selected',
388
+ message: theme.gold('Select approvals to revoke:'),
389
+ choices,
390
+ }]);
391
+
392
+ if (selected.length === 0) return info('Nothing selected.');
393
+
394
+ const { confirm } = await inquirer.prompt([{
395
+ type: 'confirm',
396
+ name: 'confirm',
397
+ message: theme.warn(`Revoke ${selected.length} approval(s)? Each costs one transaction.`),
398
+ default: true,
399
+ }]);
400
+
401
+ if (!confirm) return info('Cancelled.');
402
+
403
+ const signer = await getSigner(chain);
404
+ let revoked = 0;
405
+
406
+ for (const idx of selected) {
407
+ const a = approvals[idx];
408
+ try {
409
+ const contract = new ethers.Contract(a.token.address, ERC20_ABI, signer);
410
+ info(`Revoking ${a.token.symbol} → ${a.spenderInfo.name}...`);
411
+ const tx = await contract.approve(a.spender, 0);
412
+ console.log(` ${theme.dim('TX:')} ${tx.hash}`);
413
+ await tx.wait();
414
+ success(`✓ Revoked ${a.token.symbol} → ${a.spenderInfo.name}`);
415
+ revoked++;
416
+ } catch (err) {
417
+ error(`✗ Failed: ${err.message}`);
418
+ }
419
+ }
420
+
421
+ success(`\n${revoked}/${selected.length} approvals revoked.`);
422
+ }
423
+
424
+ /**
425
+ * Check a specific token + spender approval
426
+ */
427
+ export async function checkSpecificApproval(tokenAddress, spenderAddress, opts = {}) {
428
+ const chain = opts.chain || getConfig('defaultChain') || 'base';
429
+ const activeWallet = getConfig('activeWallet');
430
+ if (!activeWallet) return error('No active wallet.');
431
+
432
+ const wallets = getConfig('wallets') || {};
433
+ const walletData = wallets[activeWallet];
434
+ if (!walletData) return error(`Wallet "${activeWallet}" not found`);
435
+
436
+ const provider = getProvider(chain);
437
+ const tokenInfo = await getTokenInfo(provider, tokenAddress);
438
+ const allowance = await checkApproval(provider, tokenAddress, walletData.address, spenderAddress);
439
+ const spenderInfo = getSpenderInfo(spenderAddress);
440
+
441
+ console.log(theme.gold('\n🔍 Approval Check'));
442
+ kvDisplay({
443
+ 'Token': `${tokenInfo.symbol} (${tokenInfo.name})`,
444
+ 'Token Address': tokenAddress,
445
+ 'Spender': spenderInfo.name,
446
+ 'Spender Address': spenderAddress,
447
+ 'Allowance': allowance > 0n ? formatApproval(allowance, tokenInfo.decimals) : theme.success('None (0)'),
448
+ 'Risk': riskColor(spenderInfo.risk)(spenderInfo.risk.toUpperCase()),
449
+ });
450
+
451
+ return { allowance, tokenInfo, spenderInfo };
452
+ }