@darksol/terminal 0.13.1 → 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/package.json +1 -1
- package/src/cli.js +29 -0
- package/src/llm/intent.js +13 -0
- package/src/services/approvals.js +452 -0
package/package.json
CHANGED
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
|
+
}
|