@darksol/terminal 0.13.1 → 0.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.13.1",
3
+ "version": "0.14.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": {
package/src/cli.js CHANGED
@@ -18,6 +18,7 @@ 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
20
  import { aiDiscoverPairs, aiTuneThresholds, aiStrategyBriefing, aiLearn } from './trading/arb-ai.js';
21
+ import { listApprovals, revokeApproval, checkSpecificApproval } from './services/approvals.js';
21
22
  import { executeLifiSwap, executeLifiBridge, checkBridgeStatus, showSupportedChains } from './services/lifi.js';
22
23
  import { topMovers, tokenDetail, compareTokens } from './services/market.js';
23
24
  import { oracleFlip, oracleDice, oracleNumber, oracleShuffle, oracleHealth } from './services/oracle.js';
@@ -773,6 +774,33 @@ export function cli(argv) {
773
774
  .description('Settle payment on-chain')
774
775
  .action((payment) => facilitatorSettle(payment));
775
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
+
776
804
  // ═══════════════════════════════════════
777
805
  // MAIL COMMANDS
778
806
  // ═══════════════════════════════════════
@@ -2045,6 +2073,7 @@ function showCommandList() {
2045
2073
  ['builders', 'ERC-8021 builder index'],
2046
2074
  ['mail', 'AgentMail - email for your agent'],
2047
2075
  ['facilitator', 'x402 payment facilitator'],
2076
+ ['approvals', 'Token approval manager'],
2048
2077
  ['skills', 'Agent skill directory'],
2049
2078
  ['browser', 'Playwright browser automation'],
2050
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
+ }