├── .gitignore
├── .npmignore
├── README.md
├── extraRpcs.js
├── index.js
├── networkCache.json
├── package.json
├── pnpm-lock.yaml
└── preview.png
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | *.log
4 | .npm
5 | .npmrc
6 | npm-debug.log*
7 | package-lock.json
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .git
2 | .github
3 | .gitignore
4 | .DS_Store
5 | *.log
6 | tests/
7 | examples/
8 | docs/
9 | .vscode/
10 | .idea/
11 | *.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Snubb - Multichain Token Approval Scanner
2 |
3 | A beautiful terminal UI for scanning blockchain token approvals and tracking your exposure.
4 |
5 | 
6 |
7 | ## Features
8 |
9 | - 🔍 Scans multiple blockchains for token approvals
10 | - 🔄 Tracks transfers that utilize these approvals
11 | - ⚠️ Highlights unlimited token approvals (∞)
12 | - 📊 Shows remaining allowances after transfers
13 | - 🖥️ Slick terminal user interface with real-time stats
14 | - 🚀 Fast scanning using Hypersync's indexing API
15 | - 📋 Full address details view for copying to blockchain explorers
16 |
17 | ## Installation
18 |
19 | ```bash
20 | # Install globally
21 | npm install -g snubb
22 |
23 | # Or use directly with npx
24 | npx snubb --address YOUR_ETH_ADDRESS
25 | ```
26 |
27 | ## Usage
28 |
29 | ```bash
30 | # Scan for approvals for a specific address
31 | snubb --address 0x7C25a8C86A04f40F2Db0434ab3A24b051FB3cA58
32 |
33 | # Get help
34 | snubb --help
35 | ```
36 |
37 | ## Navigation
38 |
39 | The terminal UI supports keyboard navigation:
40 |
41 | - **n/p**: Navigate through the approval list
42 | - **>/<**: Navigate between pages
43 | - **Enter**: Toggle detailed view for an approval
44 | - **h**: Show help screen
45 | - **q**: Quit the application
46 |
47 | When an approval is selected, the full token and spender addresses are shown in the details panel, allowing you to copy the complete addresses for use in blockchain explorers.
48 |
49 | ## Understanding the Results
50 |
51 | The tool scans for two types of events:
52 |
53 | 1. **Approval events** - When you authorize a contract/address to spend your tokens
54 | 2. **Transfer events** - When tokens move from your wallet (potentially using those approvals)
55 |
56 | The results show:
57 |
58 | - **Token Address** - The contract address of the token
59 | - **Spender** - The address authorized to spend your tokens
60 | - **Approved** - The amount you've approved for spending
61 | - **Used** - How much the spender has already used
62 | - **Remaining** - The current remaining approval (what you're still exposed to)
63 |
64 | ## Security Recommendations
65 |
66 | 1. **Revoke unnecessary approvals**, especially those with unlimited amounts
67 | 2. **Use token allowance services** like revoke.cash or etherscan's token approval tool
68 | 3. **Be cautious with unlimited approvals** (∞) as they represent unlimited access to that token
69 |
70 | ## Development
71 |
72 | ```bash
73 | # Clone the repository
74 | git clone https://github.com/your-username/snubb.git
75 |
76 | # Install dependencies
77 | cd snubb
78 | npm install
79 |
80 | # Run locally
81 | node index.js --address YOUR_ETH_ADDRESS
82 | ```
83 |
84 | ## License
85 |
86 | ISC
87 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { keccak256, toHex, createPublicClient, http, formatUnits } from "viem";
4 | import { mainnet } from "viem/chains";
5 | import {
6 | HypersyncClient,
7 | LogField,
8 | JoinMode,
9 | TransactionField,
10 | Decoder,
11 | } from "@envio-dev/hypersync-client";
12 | import chalk from "chalk";
13 | import figlet from "figlet";
14 | import { Command } from "commander";
15 | import ora from "ora";
16 | import readline from "readline";
17 | import boxen from "boxen";
18 | import fs from "fs";
19 | import path from "path";
20 | import Table from "cli-table3";
21 | import { fileURLToPath } from "url";
22 | import { dirname } from "path";
23 |
24 | // Get directory of current module
25 | const __filename = fileURLToPath(import.meta.url);
26 | const __dirname = dirname(__filename);
27 |
28 | // Import extraRpcs dynamically
29 | let extraRpcs = {};
30 | try {
31 | const extraRpcsPath = path.resolve(__dirname, "./extraRpcs.js");
32 | if (fs.existsSync(extraRpcsPath)) {
33 | const module = await import(extraRpcsPath);
34 | extraRpcs = module.default || {};
35 | }
36 | } catch (error) {
37 | console.warn(
38 | chalk.yellow(`Warning: Could not load extraRpcs.js: ${error.message}`)
39 | );
40 | }
41 |
42 | // List of safe chalk colors for dynamically assigned chains
43 | const SAFE_COLORS = [
44 | "green",
45 | "yellow",
46 | "blue",
47 | "magenta",
48 | "cyan",
49 | "white",
50 | "gray",
51 | "redBright",
52 | "greenBright",
53 | "yellowBright",
54 | "blueBright",
55 | "magentaBright",
56 | "cyanBright",
57 | "whiteBright",
58 | ];
59 |
60 | // Load chain data from networkCache.json or fetch from API
61 | let networkData = [];
62 | try {
63 | const networkCachePath = path.resolve(__dirname, "./networkCache.json");
64 | if (fs.existsSync(networkCachePath)) {
65 | networkData = JSON.parse(fs.readFileSync(networkCachePath, "utf8"));
66 | } else {
67 | // In a production app, we'd fetch from API here
68 | console.warn(chalk.yellow("Warning: Could not find networkCache.json"));
69 | }
70 | } catch (error) {
71 | console.warn(
72 | chalk.yellow(`Warning: Could not load networkCache.json: ${error.message}`)
73 | );
74 | }
75 |
76 | // List of preferred chains to include by default
77 | const PREFERRED_CHAINS = [
78 | "eth",
79 | "optimism",
80 | "arbitrum",
81 | "gnosis",
82 | "xdc",
83 | "unichain",
84 | "avalanche",
85 | ];
86 |
87 | // Create a map of name to chain data for quick lookup
88 | const chainNameToData = {};
89 | networkData.forEach((chain) => {
90 | chainNameToData[chain.name] = chain;
91 | });
92 |
93 | // Create dynamic SUPPORTED_CHAINS object with colors
94 | const SUPPORTED_CHAINS = {};
95 |
96 | // First, add all chains from networkCache.json
97 | networkData.forEach((chain, index) => {
98 | if (chain.ecosystem === "evm" && chain.chain_id) {
99 | // Assign a color from the safe colors array, cycling through them if needed
100 | const colorIndex = index % SAFE_COLORS.length;
101 | const color = SAFE_COLORS[colorIndex];
102 |
103 | // Capitalize first letter of chain name
104 | const displayName =
105 | chain.name.charAt(0).toUpperCase() + chain.name.slice(1);
106 |
107 | SUPPORTED_CHAINS[chain.chain_id] = {
108 | name: displayName,
109 | color: color,
110 | hypersyncUrl: `http://${chain.chain_id}.hypersync.xyz`,
111 | };
112 | }
113 | });
114 |
115 | // Apply special color assignments for well-known chains
116 | if (SUPPORTED_CHAINS[1]) SUPPORTED_CHAINS[1].color = "cyan"; // Ethereum
117 | if (SUPPORTED_CHAINS[10]) SUPPORTED_CHAINS[10].color = "redBright"; // Optimism
118 | if (SUPPORTED_CHAINS[137]) SUPPORTED_CHAINS[137].color = "magenta"; // Polygon
119 | if (SUPPORTED_CHAINS[42161]) SUPPORTED_CHAINS[42161].color = "blue"; // Arbitrum
120 | if (SUPPORTED_CHAINS[8453]) SUPPORTED_CHAINS[8453].color = "blue"; // Base
121 | if (SUPPORTED_CHAINS[100]) SUPPORTED_CHAINS[100].color = "green"; // Gnosis
122 | if (SUPPORTED_CHAINS[43114]) SUPPORTED_CHAINS[43114].color = "red"; // Avalanche
123 |
124 | // If no chains were loaded, provide fallbacks for core chains
125 | if (Object.keys(SUPPORTED_CHAINS).length === 0) {
126 | console.warn(chalk.yellow("Warning: Using fallback chain configuration"));
127 | // Fallback to core chains
128 | const fallbackChains = {
129 | 1: {
130 | name: "Ethereum",
131 | color: "cyan",
132 | hypersyncUrl: "http://1.hypersync.xyz",
133 | },
134 | 10: {
135 | name: "Optimism",
136 | color: "redBright",
137 | hypersyncUrl: "http://10.hypersync.xyz",
138 | },
139 | 137: {
140 | name: "Polygon",
141 | color: "magenta",
142 | hypersyncUrl: "http://137.hypersync.xyz",
143 | },
144 | 42161: {
145 | name: "Arbitrum",
146 | color: "blue",
147 | hypersyncUrl: "http://42161.hypersync.xyz",
148 | },
149 | 8453: {
150 | name: "Base",
151 | color: "greenBright",
152 | hypersyncUrl: "http://8453.hypersync.xyz",
153 | },
154 | 100: {
155 | name: "Gnosis",
156 | color: "green",
157 | hypersyncUrl: "http://100.hypersync.xyz",
158 | },
159 | 43114: {
160 | name: "Avalanche",
161 | color: "red",
162 | hypersyncUrl: "http://43114.hypersync.xyz",
163 | },
164 | };
165 |
166 | Object.assign(SUPPORTED_CHAINS, fallbackChains);
167 | }
168 |
169 | // Get default chain IDs string
170 | const DEFAULT_CHAIN_IDS = Object.keys(SUPPORTED_CHAINS).join(",");
171 |
172 | // Cache for token metadata
173 | const tokenMetadataCache = new Map();
174 |
175 | // ERC20 ABI for token metadata
176 | const ERC20_ABI = [
177 | {
178 | constant: true,
179 | inputs: [],
180 | name: "name",
181 | outputs: [{ name: "", type: "string" }],
182 | payable: false,
183 | stateMutability: "view",
184 | type: "function",
185 | },
186 | {
187 | constant: true,
188 | inputs: [],
189 | name: "symbol",
190 | outputs: [{ name: "", type: "string" }],
191 | payable: false,
192 | stateMutability: "view",
193 | type: "function",
194 | },
195 | {
196 | constant: true,
197 | inputs: [],
198 | name: "decimals",
199 | outputs: [{ name: "", type: "uint8" }],
200 | payable: false,
201 | stateMutability: "view",
202 | type: "function",
203 | },
204 | ];
205 |
206 | // Fetch token metadata from a list of RPCs with improved retry logic
207 | async function fetchTokenMetadata(tokenAddress, chainId = 1) {
208 | // Check cache first
209 | const cacheKey = `${chainId}:${tokenAddress}`;
210 | if (tokenMetadataCache.has(cacheKey)) {
211 | return tokenMetadataCache.get(cacheKey);
212 | }
213 |
214 | // Get RPC URLs for the chain
215 | let rpcUrls = [];
216 | if (extraRpcs[chainId]) {
217 | extraRpcs[chainId].rpcs.forEach((rpc) => {
218 | if (typeof rpc === "string") {
219 | rpcUrls.push(rpc);
220 | } else if (rpc.url) {
221 | rpcUrls.push(rpc.url);
222 | }
223 | });
224 | }
225 |
226 | // If no RPCs available, return default values
227 | if (rpcUrls.length === 0) {
228 | return {
229 | success: false,
230 | name: "Unknown Token",
231 | symbol: "???",
232 | decimals: 18,
233 | formattedName: "Unknown Token (???)",
234 | };
235 | }
236 |
237 | // Shuffle RPC URLs to avoid always hitting the same one first
238 | rpcUrls = shuffleArray([...rpcUrls]);
239 |
240 | let lastError = null;
241 | let retryCount = 0;
242 |
243 | // Try each RPC until one works, with exponential backoff between retries
244 | for (const rpcUrl of rpcUrls) {
245 | try {
246 | if (rpcUrl.startsWith("wss://")) continue; // Skip WebSocket RPCs for now
247 |
248 | // Add a small delay between retries with exponential backoff
249 | if (retryCount > 0) {
250 | await new Promise((resolve) =>
251 | setTimeout(resolve, Math.min(200 * Math.pow(1.5, retryCount), 2000))
252 | );
253 | }
254 | retryCount++;
255 |
256 | // Create a viem client with timeout
257 | const client = createPublicClient({
258 | chain: mainnet, // This is just for typing, we'll override with custom endpoint
259 | transport: http(rpcUrl, {
260 | timeout: 3000, // 3 second timeout for RPC calls
261 | fetchOptions: {
262 | headers: {
263 | "Content-Type": "application/json",
264 | },
265 | },
266 | }),
267 | });
268 |
269 | // Fetch token metadata (name, symbol, decimals) in parallel
270 | const [name, symbol, decimals] = await Promise.all([
271 | client
272 | .readContract({
273 | address: tokenAddress,
274 | abi: ERC20_ABI,
275 | functionName: "name",
276 | })
277 | .catch((e) => null),
278 | client
279 | .readContract({
280 | address: tokenAddress,
281 | abi: ERC20_ABI,
282 | functionName: "symbol",
283 | })
284 | .catch((e) => null),
285 | client
286 | .readContract({
287 | address: tokenAddress,
288 | abi: ERC20_ABI,
289 | functionName: "decimals",
290 | })
291 | .catch((e) => 18),
292 | ]);
293 |
294 | // If we got at least one piece of metadata
295 | if (name !== null || symbol !== null) {
296 | const finalName = name || "Unknown Token";
297 | const finalSymbol = symbol || "???";
298 |
299 | const metadata = {
300 | success: true,
301 | name: finalName,
302 | symbol: finalSymbol,
303 | decimals,
304 | formattedName: `${finalName} (${finalSymbol})`,
305 | };
306 |
307 | // Cache the result
308 | tokenMetadataCache.set(cacheKey, metadata);
309 | return metadata;
310 | }
311 |
312 | // If both name and symbol are null, consider this attempt failed
313 | lastError = new Error("Token metadata not available");
314 | } catch (error) {
315 | lastError = error;
316 | // Continue to the next RPC if this one fails
317 | }
318 | }
319 |
320 | // If we have a default (placeholder) metadata in cache from a previous failed attempt,
321 | // use that instead of creating a new default object every time
322 | const defaultMetadata = {
323 | success: false,
324 | name: "Unknown Token",
325 | symbol: "???",
326 | decimals: 18,
327 | formattedName: "Unknown Token (???)",
328 | };
329 |
330 | // Cache the default result to avoid repeated failed requests
331 | tokenMetadataCache.set(cacheKey, defaultMetadata);
332 | return defaultMetadata;
333 | }
334 |
335 | // Utility to shuffle array (for randomizing RPC order)
336 | function shuffleArray(array) {
337 | for (let i = array.length - 1; i > 0; i--) {
338 | const j = Math.floor(Math.random() * (i + 1));
339 | [array[i], array[j]] = [array[j], array[i]];
340 | }
341 | return array;
342 | }
343 |
344 | // Format token amounts with proper decimals
345 | function formatTokenAmount(amount, decimals) {
346 | if (!amount) return "0";
347 |
348 | try {
349 | return formatUnits(amount, decimals);
350 | } catch (error) {
351 | return amount.toString();
352 | }
353 | }
354 |
355 | // Global variables for interactive mode
356 | let approvalsList = [];
357 | let selectedApprovalIndex = 0;
358 | let currentPage = 0;
359 | const PAGE_SIZE = 8; // Number of approvals to show per page
360 |
361 | // Group approvals by token for better display
362 | let groupedApprovals = {};
363 |
364 | // Scanning stats to preserve after completion
365 | let chainStats = {};
366 |
367 | // Create global readline interface
368 | const rl = readline.createInterface({
369 | input: process.stdin,
370 | output: process.stdout,
371 | terminal: true,
372 | });
373 |
374 | // CLI setup - note the change to use DEFAULT_CHAIN_IDS
375 | const program = new Command();
376 | program
377 | .name("snubb")
378 | .description("Terminal UI for finding and revoking Ethereum token approvals")
379 | .version("1.0.0")
380 | .option("-a, --address
", "Ethereum address to check approvals for")
381 | .option(
382 | "-c, --chains ",
383 | "Comma-separated chain IDs or 'many-networks' to scan multiple networks (default: 1 - Ethereum only)",
384 | "1"
385 | )
386 | .option(
387 | "--list-chains",
388 | "Display a list of all supported chains from networkCache.json"
389 | )
390 | .parse(process.argv);
391 |
392 | const options = program.opts();
393 |
394 | // Check if user wants to list all supported chains
395 | if (options.listChains) {
396 | console.log(
397 | chalk.bold.cyan(figlet.textSync("Supported Chains", { font: "Small" }))
398 | );
399 | console.log(
400 | chalk.bold.cyan("List of all supported chains from networkCache.json\n")
401 | );
402 |
403 | // Create a table for better display
404 | const chainsTable = new Table({
405 | head: [
406 | chalk.cyan.bold("CHAIN ID"),
407 | chalk.cyan.bold("NAME"),
408 | chalk.cyan.bold("TIER"),
409 | ],
410 | colWidths: [12, 25, 12],
411 | style: {
412 | head: [], // No additional styling for headers
413 | border: [], // No additional styling for borders
414 | },
415 | });
416 |
417 | // Sort networkData by chain ID for easier reading
418 | const sortedChains = [...networkData]
419 | .filter((chain) => chain.ecosystem === "evm") // Only show EVM chains
420 | .sort((a, b) => a.chain_id - b.chain_id);
421 |
422 | // Add each chain to the table
423 | sortedChains.forEach((chain) => {
424 | chainsTable.push([chain.chain_id.toString(), chain.name, chain.tier]);
425 | });
426 |
427 | // Display the table
428 | console.log(chainsTable.toString());
429 | console.log(
430 | `\nTo use: ${chalk.green(
431 | "snubb --address --chains "
432 | )}`
433 | );
434 | process.exit(0);
435 | }
436 |
437 | // Check if we have an address
438 | let TARGET_ADDRESS = options.address;
439 | if (!TARGET_ADDRESS) {
440 | console.log(
441 | chalk.bold.cyan(
442 | figlet.textSync("snubb", {
443 | font: "ANSI Shadow",
444 | horizontalLayout: "full",
445 | })
446 | )
447 | );
448 | console.log(
449 | chalk.bold.cyan("multichain token approval scanner") +
450 | " - " +
451 | chalk.cyan("powered by ") +
452 | chalk.cyan.underline("envio.dev") +
453 | "\n"
454 | );
455 |
456 | console.log(chalk.yellow("Usage:"));
457 | console.log(
458 | chalk.green(
459 | " snubb --address 0x7C25a8C86A04f40F2Db0434ab3A24b051FB3cA58\n"
460 | )
461 | );
462 | console.log(chalk.yellow("Options:"));
463 | console.log(
464 | chalk.green(
465 | ` --chains Comma-separated chain IDs to scan (default: 1 - Ethereum only)\n`
466 | )
467 | );
468 | console.log(
469 | chalk.green(
470 | ` --chains many-networks Scan multiple supported networks (${PREFERRED_CHAINS.join(
471 | ", "
472 | )})\n`
473 | )
474 | );
475 | console.log(
476 | chalk.green(` --list-chains Display a list of all supported chains\n`)
477 | );
478 |
479 | process.exit(0);
480 | }
481 |
482 | // Get chain IDs from options
483 | let CHAIN_IDS = [];
484 |
485 | // Check if 'many-networks' keyword is used
486 | if (options.chains.toLowerCase() === "many-networks") {
487 | // Use all preferred networks
488 | for (const chainName of PREFERRED_CHAINS) {
489 | const chain = chainNameToData[chainName];
490 | if (chain) {
491 | CHAIN_IDS.push(chain.chain_id);
492 | }
493 | }
494 | } else {
495 | // Otherwise use the specified chains
496 | const requestedChainIds = options.chains
497 | .split(",")
498 | .map((id) => parseInt(id.trim()));
499 |
500 | for (const chainId of requestedChainIds) {
501 | // Check if this chain ID exists in networkData (networkCache.json)
502 | const chainData = networkData.find(
503 | (chain) => chain.chain_id === chainId && chain.ecosystem === "evm"
504 | );
505 |
506 | if (chainData) {
507 | // If in networkData, check if already added to SUPPORTED_CHAINS
508 | if (!SUPPORTED_CHAINS[chainId]) {
509 | // Get a color from SAFE_COLORS
510 | const colorIndex = Math.floor(Math.random() * SAFE_COLORS.length);
511 | const color = SAFE_COLORS[colorIndex];
512 |
513 | // Add to SUPPORTED_CHAINS
514 | SUPPORTED_CHAINS[chainId] = {
515 | name:
516 | chainData.name.charAt(0).toUpperCase() + chainData.name.slice(1),
517 | color: color,
518 | hypersyncUrl: `http://${chainId}.hypersync.xyz`,
519 | };
520 | }
521 |
522 | // Now add to CHAIN_IDS
523 | CHAIN_IDS.push(chainId);
524 | } else {
525 | // Chain not in networkCache.json - this is an error
526 | console.error(chalk.red(`Error: Chain ID ${chainId} is not supported.`));
527 | console.error(
528 | chalk.yellow(
529 | `Run '${chalk.green(
530 | "snubb --list-chains"
531 | )}' to see all supported chains.`
532 | )
533 | );
534 | process.exit(1);
535 | }
536 | }
537 | }
538 |
539 | // If no valid chains, use Ethereum mainnet
540 | if (CHAIN_IDS.length === 0) {
541 | CHAIN_IDS.push(1); // Fallback to Ethereum mainnet
542 | }
543 |
544 | // Normalize address
545 | TARGET_ADDRESS = TARGET_ADDRESS.toLowerCase();
546 | if (!TARGET_ADDRESS.startsWith("0x")) {
547 | TARGET_ADDRESS = "0x" + TARGET_ADDRESS;
548 | }
549 |
550 | // Address formatting for topic filtering
551 | const TARGET_ADDRESS_NO_PREFIX = TARGET_ADDRESS.substring(2).toLowerCase();
552 | const TARGET_ADDRESS_PADDED =
553 | "0x000000000000000000000000" + TARGET_ADDRESS_NO_PREFIX;
554 |
555 | // Define ERC20 event signatures
556 | const event_signatures = [
557 | "Transfer(address,address,uint256)",
558 | "Approval(address,address,uint256)",
559 | ];
560 |
561 | // Create topic0 hashes from event signatures
562 | const topic0_list = event_signatures.map((sig) => keccak256(toHex(sig)));
563 |
564 | // Store individual topic hashes for easier comparison
565 | const TRANSFER_TOPIC = topic0_list[0];
566 | const APPROVAL_TOPIC = topic0_list[1];
567 |
568 | // Create mapping from topic0 hash to event name
569 | const topic0ToName = {};
570 | topic0ToName[TRANSFER_TOPIC] = "Transfer";
571 | topic0ToName[APPROVAL_TOPIC] = "Approval";
572 |
573 | // Helper functions for UI
574 | const formatNumber = (num) => {
575 | return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
576 | };
577 |
578 | const formatToken = (tokenAddress, tokenMetadata) => {
579 | if (tokenMetadata && tokenMetadata.success) {
580 | return tokenMetadata.formattedName;
581 | }
582 |
583 | if (tokenAddress.length <= 12) return tokenAddress;
584 | return `${tokenAddress.slice(0, 6)}...${tokenAddress.slice(-6)}`;
585 | };
586 |
587 | // Check if an amount is effectively unlimited (close to 2^256-1)
588 | const isEffectivelyUnlimited = (amount) => {
589 | // Common unlimited values (2^256-1 and similar large numbers)
590 | const MAX_UINT256 = BigInt(
591 | "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
592 | );
593 | const LARGE_THRESHOLD = MAX_UINT256 - MAX_UINT256 / BigInt(1000); // Within 0.1% of max
594 |
595 | return amount > LARGE_THRESHOLD;
596 | };
597 |
598 | const formatAmount = (amount, tokenMetadata) => {
599 | if (!amount) return "0";
600 |
601 | // Check for unlimited or very large approval (effectively unlimited)
602 | if (
603 | amount === BigInt(2) ** BigInt(256) - BigInt(1) ||
604 | amount ===
605 | BigInt(
606 | "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
607 | ) ||
608 | isEffectivelyUnlimited(amount)
609 | ) {
610 | return "∞ (Unlimited)";
611 | }
612 |
613 | // Format with decimals if available
614 | if (tokenMetadata && tokenMetadata.success) {
615 | return formatTokenAmount(amount, tokenMetadata.decimals);
616 | }
617 |
618 | // Format large numbers with abbr (fallback)
619 | if (amount > BigInt(1000000000000)) {
620 | return `${Number(amount / BigInt(1000000000000)).toFixed(2)}T`;
621 | } else if (amount > BigInt(1000000000)) {
622 | return `${Number(amount / BigInt(1000000000)).toFixed(2)}B`;
623 | } else if (amount > BigInt(1000000)) {
624 | return `${Number(amount / BigInt(1000000)).toFixed(2)}M`;
625 | } else if (amount > BigInt(1000)) {
626 | return `${Number(amount / BigInt(1000)).toFixed(2)}K`;
627 | }
628 |
629 | return amount.toString();
630 | };
631 |
632 | // Format chain name with color (safely)
633 | const formatChainName = (chainId) => {
634 | if (!SUPPORTED_CHAINS[chainId]) {
635 | return chalk.white(`Chain ${chainId}`);
636 | }
637 |
638 | const chain = SUPPORTED_CHAINS[chainId];
639 | const colorName = chain.color || "white";
640 |
641 | // Safely apply color
642 | try {
643 | if (chalk[colorName]) {
644 | return chalk[colorName](chain.name);
645 | } else {
646 | return chalk.white(chain.name);
647 | }
648 | } catch (error) {
649 | return chalk.white(chain.name);
650 | }
651 | };
652 |
653 | // Draw progress bar with safe color handling
654 | function drawProgressBar(progress, width = 40, colorName = "cyan") {
655 | const filledWidth = Math.floor(width * progress);
656 | const emptyWidth = width - filledWidth;
657 |
658 | // Ensure we draw something even at 100%
659 | const filledChar = "█";
660 | const emptyChar = "░";
661 | const filledBar = filledChar.repeat(Math.max(1, filledWidth));
662 | const emptyBar = emptyChar.repeat(emptyWidth);
663 |
664 | // Safely apply color
665 | try {
666 | if (chalk[colorName]) {
667 | return chalk[colorName](filledBar) + emptyBar;
668 | } else {
669 | return chalk.cyan(filledBar) + emptyBar;
670 | }
671 | } catch (error) {
672 | return chalk.cyan(filledBar) + emptyBar;
673 | }
674 | }
675 |
676 | // Create a query for ERC20 events related to our target address
677 | const createQuery = (fromBlock) => ({
678 | fromBlock,
679 | logs: [
680 | // Filter for Approval events where target address is the owner (topic1)
681 | {
682 | topics: [[APPROVAL_TOPIC], [TARGET_ADDRESS_PADDED], []],
683 | },
684 | // Filter for Transfer events where target address is from (topic1)
685 | {
686 | topics: [[TRANSFER_TOPIC], [TARGET_ADDRESS_PADDED], []],
687 | },
688 | // Also get Transfer events where target address is to (topic2)
689 | {
690 | topics: [[TRANSFER_TOPIC], [], [TARGET_ADDRESS_PADDED]],
691 | },
692 | ],
693 | // Also filter for transactions involving the target address
694 | transactions: [
695 | {
696 | from: [TARGET_ADDRESS],
697 | },
698 | {
699 | to: [TARGET_ADDRESS],
700 | },
701 | ],
702 | fieldSelection: {
703 | log: [
704 | LogField.BlockNumber,
705 | LogField.LogIndex,
706 | LogField.TransactionIndex,
707 | LogField.TransactionHash,
708 | LogField.Data,
709 | LogField.Address,
710 | LogField.Topic0,
711 | LogField.Topic1,
712 | LogField.Topic2,
713 | LogField.Topic3,
714 | ],
715 | transaction: [
716 | TransactionField.From,
717 | TransactionField.To,
718 | TransactionField.Hash,
719 | ],
720 | },
721 | joinMode: JoinMode.JoinTransactions,
722 | });
723 |
724 | // Add a new state variable near the other global variables
725 | let detailsExpanded = false;
726 |
727 | // Function to display the approvals list
728 | async function displayApprovalsList() {
729 | console.clear();
730 |
731 | // Display header with logo and stats
732 | console.log(chalk.bold.cyan(figlet.textSync("snubb", { font: "Doom" })));
733 | console.log(
734 | chalk.bold.cyan("multichain token approval scanner") +
735 | " - " +
736 | chalk.cyan("powered by ") +
737 | chalk.cyan.underline("envio.dev") +
738 | "\n"
739 | );
740 |
741 | // Display scan progress and summary separately
742 | displayScanSummary();
743 |
744 | // Calculate page bounds
745 | const startIdx = currentPage * PAGE_SIZE;
746 | const endIdx = Math.min(startIdx + PAGE_SIZE, approvalsList.length);
747 | const totalPages = Math.ceil(approvalsList.length / PAGE_SIZE);
748 |
749 | // Navigation header with enhanced information
750 | console.log(
751 | boxen(
752 | chalk.bold.cyan(
753 | `OUTSTANDING APPROVALS (${currentPage + 1}/${totalPages}) - Showing ${
754 | startIdx + 1
755 | }-${endIdx} of ${approvalsList.length}`
756 | ),
757 | {
758 | padding: { top: 0, bottom: 0, left: 1, right: 1 },
759 | borderColor: "yellow",
760 | borderStyle: "round",
761 | }
762 | )
763 | );
764 |
765 | // Create a more structured table for approvals with proper hierarchy
766 | displayApprovalsTable(startIdx, endIdx);
767 |
768 | // Display details of the selected approval only if expanded
769 | if (approvalsList.length > 0 && detailsExpanded) {
770 | const approval = approvalsList[selectedApprovalIndex];
771 |
772 | // Use cached token metadata if available
773 | const tokenMetadata = tokenMetadataCache.get(
774 | `${approval.chainId}:${approval.tokenAddress}`
775 | );
776 |
777 | // Display approval details with available metadata
778 | displayApprovalDetails(approval, tokenMetadata || { success: false });
779 | } else if (approvalsList.length > 0) {
780 | // Show a hint to expand details
781 | console.log(
782 | boxen(
783 | chalk.dim(
784 | "Press ENTER to view detailed information for the selected approval"
785 | ),
786 | {
787 | padding: { top: 0, bottom: 0, left: 1, right: 1 },
788 | borderColor: "blue",
789 | borderStyle: "round",
790 | }
791 | )
792 | );
793 | }
794 |
795 | // Add revoke.cash link right above navigation commands
796 | const revokeLink = `https://revoke.cash/address/${TARGET_ADDRESS}`;
797 | console.log(
798 | boxen(
799 | chalk.bold.white(
800 | `⚠️ REVOKE APPROVALS: ${chalk.bold.cyan.underline(revokeLink)}`
801 | ),
802 | {
803 | padding: { top: 0, bottom: 0, left: 2, right: 2 },
804 | margin: { top: 1, bottom: 0 },
805 | borderColor: "red",
806 | borderStyle: "round",
807 | }
808 | )
809 | );
810 |
811 | // Move navigation instructions to the bottom near the input prompt
812 | console.log(
813 | "\n" +
814 | boxen(
815 | [
816 | chalk.cyan("Navigation Commands:"),
817 | `${chalk.yellow("n")} - Next approval ${chalk.yellow(
818 | "p"
819 | )} - Previous approval`,
820 | `${chalk.yellow(">")} - Next page ${chalk.yellow(
821 | "<"
822 | )} - Previous page`,
823 | `${chalk.yellow("ENTER")} - Show/hide details`,
824 | `${chalk.yellow("q")} - Quit ${chalk.yellow("h")} - Help`,
825 | ].join("\n"),
826 | {
827 | padding: { top: 1, bottom: 1, left: 2, right: 2 },
828 | margin: { top: 0, bottom: 1 },
829 | borderColor: "magenta",
830 | borderStyle: "round",
831 | }
832 | )
833 | );
834 |
835 | // Start fetching metadata in the background
836 | fetchTokenMetadataInBackground(startIdx, endIdx);
837 | }
838 |
839 | // Function to display approvals in a professionally formatted table
840 | function displayApprovalsTable(startIdx, endIdx) {
841 | // Create a new table for approvals with clean styling
842 | const approvalsTable = new Table({
843 | head: [
844 | chalk.cyan.bold("CHAIN"),
845 | chalk.cyan.bold("TOKEN"),
846 | chalk.cyan.bold("SPENDER"),
847 | chalk.cyan.bold("AMOUNT"),
848 | ],
849 | colWidths: [10, 18, 23, 35],
850 | style: {
851 | head: [], // No additional styling for headers
852 | border: [], // No additional styling for borders
853 | compact: true, // More compact table
854 | },
855 | chars: {
856 | top: "━",
857 | "top-mid": "┳",
858 | "top-left": "┏",
859 | "top-right": "┓",
860 | bottom: "━",
861 | "bottom-mid": "┻",
862 | "bottom-left": "┗",
863 | "bottom-right": "┛",
864 | left: "┃",
865 | "left-mid": "",
866 | mid: "",
867 | "mid-mid": "",
868 | right: "┃",
869 | "right-mid": "",
870 | middle: "┃",
871 | },
872 | });
873 |
874 | // Keep track of current chain to handle grouping
875 | let currentChainId = null;
876 | let currentTokenAddress = null;
877 |
878 | // Display the approvals with token metadata when available
879 | for (let i = startIdx; i < endIdx; i++) {
880 | const approval = approvalsList[i];
881 | const isSelected = i === selectedApprovalIndex;
882 |
883 | // Check if this is a new chain
884 | const isNewChain = currentChainId !== approval.chainId;
885 | const isNewToken =
886 | currentTokenAddress !== approval.tokenAddress || isNewChain;
887 |
888 | // Get token metadata
889 | const tokenMetadata = tokenMetadataCache.get(
890 | `${approval.chainId}:${approval.tokenAddress}`
891 | );
892 |
893 | // Format token display based on available metadata
894 | const tokenDisplay =
895 | tokenMetadata && tokenMetadata.success
896 | ? `${chalk.cyan(tokenMetadata.symbol)}`
897 | : chalk.cyan(approval.tokenAddress.slice(0, 6) + "...");
898 |
899 | // Format spender display with selection indicator and truncation if needed
900 | const spenderText = formatToken(approval.spender);
901 | // Truncate long spender addresses to fit column
902 | const displaySpender =
903 | spenderText.length > 18
904 | ? spenderText.slice(0, 8) + "..." + spenderText.slice(-8)
905 | : spenderText;
906 |
907 | const spenderDisplay = isSelected
908 | ? chalk.yellow.bold(`→ ${displaySpender}`)
909 | : chalk.yellow(displaySpender);
910 |
911 | // Update unlimited flag for effectively unlimited values
912 | const isEffectiveUnlimited = isEffectivelyUnlimited(
913 | approval.remainingApproval
914 | );
915 | const displayAsUnlimited = approval.isUnlimited || isEffectiveUnlimited;
916 |
917 | // Format amount display
918 | const amountDisplay = displayAsUnlimited
919 | ? isSelected
920 | ? chalk.red.bold("⚠️ UNLIMITED")
921 | : chalk.red.bold("⚠️ ∞")
922 | : chalk.green(formatAmount(approval.remainingApproval, tokenMetadata));
923 |
924 | // Handle chain grouping - only show chain name for the first entry of the chain
925 | const chainCell = isNewChain ? formatChainName(approval.chainId) : "";
926 |
927 | // Add row to table
928 | approvalsTable.push([
929 | chainCell,
930 | tokenDisplay,
931 | spenderDisplay,
932 | amountDisplay,
933 | ]);
934 |
935 | // Update tracking variables
936 | if (isNewChain) {
937 | currentChainId = approval.chainId;
938 | }
939 |
940 | if (isNewToken) {
941 | currentTokenAddress = approval.tokenAddress;
942 | }
943 | }
944 |
945 | // Display the table
946 | console.log(approvalsTable.toString());
947 | }
948 |
949 | // Function to display progress bars and summary table sequentially
950 | function displayScanSummary() {
951 | // Calculate maximum width needed for chain names
952 | const chainNameWidth =
953 | Math.max(
954 | ...CHAIN_IDS.map((id) => formatChainName(id).length),
955 | 10 // Minimum width
956 | ) + 2; // Add some padding
957 |
958 | // Display progress bars header
959 | console.log(chalk.bold.yellow("SCAN PROGRESS"));
960 |
961 | // Display progress bars
962 | for (const chainId of CHAIN_IDS) {
963 | if (chainStats[chainId]) {
964 | const stats = chainStats[chainId];
965 | // Use consistent padding and formatting for all chains
966 | const chainName = formatChainName(chainId);
967 | const paddedChainName = chainName.padEnd(chainNameWidth);
968 |
969 | // Create progress bar line with fixed spacing
970 | console.log(
971 | ` ${paddedChainName}: [${stats.progressBar}] 100.00% ${chalk.green(
972 | "✓ Complete"
973 | )}`
974 | );
975 | }
976 | }
977 |
978 | // Create summary table
979 | console.log(chalk.bold.yellow("\nSUMMARY"));
980 |
981 | const statsTable = new Table({
982 | head: [
983 | chalk.cyan("CHAIN"),
984 | chalk.cyan("HEIGHT"),
985 | chalk.cyan("EVENTS"),
986 | chalk.cyan("TIME"),
987 | chalk.cyan("APPROVALS"),
988 | ],
989 | colWidths: [15, 15, 10, 8, 10],
990 | style: {
991 | head: [], // No additional styling for headers
992 | border: [], // No additional styling for borders
993 | compact: true, // More compact table with less padding
994 | },
995 | });
996 |
997 | // Add rows to the table from chain stats
998 | let totalApprovals = 0;
999 | for (const chainId of CHAIN_IDS) {
1000 | if (chainStats[chainId]) {
1001 | const stats = chainStats[chainId];
1002 |
1003 | // Add a row with colored chain name and right-aligned numeric data
1004 | statsTable.push([
1005 | formatChainName(chainId), // Already has color applied
1006 | formatNumber(stats.height),
1007 | formatNumber(stats.totalEvents),
1008 | `${(stats.endTime / 1000).toFixed(1)}s`,
1009 | stats.approvalsCount.toString(),
1010 | ]);
1011 |
1012 | totalApprovals += stats.approvalsCount;
1013 | }
1014 | }
1015 |
1016 | // Add a totals row
1017 | statsTable.push([
1018 | chalk.bold("TOTAL"),
1019 | "",
1020 | "",
1021 | "",
1022 | chalk.bold.white(totalApprovals.toString()),
1023 | ]);
1024 |
1025 | // Display the table
1026 | console.log(statsTable.toString());
1027 | console.log(""); // Add spacing
1028 | }
1029 |
1030 | // Asynchronous function to fetch token metadata in background
1031 | async function fetchTokenMetadataInBackground(startIdx, endIdx) {
1032 | // Collection of unique token addresses on the current page
1033 | const tokensToFetch = new Set();
1034 |
1035 | // Collect all tokens that need metadata
1036 | for (let i = startIdx; i < endIdx; i++) {
1037 | if (i < approvalsList.length) {
1038 | const approval = approvalsList[i];
1039 | const cacheKey = `${approval.chainId}:${approval.tokenAddress}`;
1040 |
1041 | // Only fetch tokens that aren't already in the cache
1042 | if (!tokenMetadataCache.has(cacheKey)) {
1043 | tokensToFetch.add({
1044 | chainId: approval.chainId,
1045 | tokenAddress: approval.tokenAddress,
1046 | });
1047 | }
1048 | }
1049 | }
1050 |
1051 | // If no tokens to fetch, we're done
1052 | if (tokensToFetch.size === 0) return;
1053 |
1054 | // Fetch token metadata in parallel
1055 | const promises = Array.from(tokensToFetch).map(
1056 | async ({ chainId, tokenAddress }) => {
1057 | await fetchTokenMetadata(tokenAddress, chainId);
1058 | }
1059 | );
1060 |
1061 | // Wait for all fetches to complete then redraw the screen
1062 | await Promise.all(promises);
1063 | displayApprovalsList();
1064 | }
1065 |
1066 | // Function to display approval details
1067 | function displayApprovalDetails(approval, tokenMetadata) {
1068 | // Get chain info
1069 | const chain = SUPPORTED_CHAINS[approval.chainId] || {
1070 | name: `Chain ${approval.chainId}`,
1071 | color: "white",
1072 | };
1073 |
1074 | console.log(
1075 | "\n" +
1076 | boxen(chalk.bold.cyan("APPROVAL DETAILS"), {
1077 | padding: { top: 0, bottom: 0, left: 1, right: 1 },
1078 | borderColor: "green",
1079 | borderStyle: "round",
1080 | })
1081 | );
1082 |
1083 | // Update unlimited flag for effectively unlimited values
1084 | const isEffectiveUnlimited = isEffectivelyUnlimited(
1085 | approval.remainingApproval
1086 | );
1087 | const displayAsUnlimited = approval.isUnlimited || isEffectiveUnlimited;
1088 |
1089 | // Create a more readable single-column display
1090 | const detailsContent = [
1091 | // Chain information
1092 | `${chalk.cyan.bold("Chain:")} ${formatChainName(approval.chainId)}`,
1093 | "",
1094 |
1095 | // Token information
1096 | tokenMetadata && tokenMetadata.success
1097 | ? `${chalk.cyan.bold("Token:")} ${chalk.green(tokenMetadata.name)} (${
1098 | tokenMetadata.symbol
1099 | })`
1100 | : `${chalk.cyan.bold("Token:")} ${chalk.green(approval.tokenAddress)}`,
1101 |
1102 | `${chalk.cyan.bold("Token Address:")} ${chalk.green(
1103 | approval.tokenAddress
1104 | )}`,
1105 | "",
1106 |
1107 | // Spender information
1108 | `${chalk.cyan.bold("Spender Address:")} ${chalk.green(approval.spender)}`,
1109 | "",
1110 |
1111 | // Approval amounts
1112 | chalk.cyan.bold("Approval Details:"),
1113 | `${chalk.yellow("Approved Amount:")} ${chalk.green(
1114 | displayAsUnlimited
1115 | ? "∞ (Unlimited)"
1116 | : formatAmount(approval.approvedAmount, tokenMetadata)
1117 | )}`,
1118 | `${chalk.yellow("Used Amount:")} ${chalk.green(
1119 | formatAmount(approval.transferredAmount, tokenMetadata)
1120 | )}`,
1121 | `${chalk.yellow("Remaining:")} ${
1122 | displayAsUnlimited
1123 | ? chalk.red.bold("∞ (UNLIMITED)")
1124 | : chalk.green(formatAmount(approval.remainingApproval, tokenMetadata))
1125 | }`,
1126 | "",
1127 |
1128 | // Transaction information
1129 | chalk.cyan.bold("Transaction Details:"),
1130 | `${chalk.yellow("Block Number:")} ${approval.blockNumber}`,
1131 | `${chalk.yellow("Transaction Hash:")} ${approval.txHash}`,
1132 | ].join("\n");
1133 |
1134 | // Display the details
1135 | console.log(
1136 | boxen(detailsContent, {
1137 | padding: 1,
1138 | borderColor: "blue",
1139 | borderStyle: "round",
1140 | })
1141 | );
1142 |
1143 | // Display warning for unlimited approvals
1144 | if (displayAsUnlimited) {
1145 | console.log(
1146 | boxen(
1147 | chalk.bold.white(
1148 | "⚠️ UNLIMITED APPROVAL - This contract has unlimited access to this token in your wallet"
1149 | ),
1150 | { padding: 1, borderColor: "red", borderStyle: "round" }
1151 | )
1152 | );
1153 | }
1154 | }
1155 |
1156 | // Help screen to display all commands
1157 | function displayHelpScreen() {
1158 | console.clear();
1159 |
1160 | console.log(chalk.bold.cyan(figlet.textSync("HELP", { font: "Doom" })));
1161 | console.log(
1162 | chalk.bold.cyan("multichain token approval scanner") +
1163 | " - " +
1164 | chalk.cyan("powered by ") +
1165 | chalk.cyan.underline("envio.dev") +
1166 | "\n"
1167 | );
1168 | const helpContent = boxen(
1169 | [
1170 | chalk.bold.yellow("COMMAND REFERENCE"),
1171 | "",
1172 | `${chalk.yellow("n")} - Move to the next approval in the list`,
1173 | `${chalk.yellow("p")} - Move to the previous approval in the list`,
1174 | `${chalk.yellow(">")} - Go to next page of approvals`,
1175 | `${chalk.yellow("<")} - Go to previous page of approvals`,
1176 | `${chalk.yellow("h")} - Show this help screen`,
1177 | `${chalk.yellow("q")} - Quit the application`,
1178 | "",
1179 | chalk.bold.yellow("ABOUT TOKEN APPROVALS"),
1180 | "",
1181 | `${chalk.white(
1182 | "Token approvals give dApps permission to spend your tokens."
1183 | )}`,
1184 | `${chalk.white(
1185 | "Unlimited approvals (∞) are a security risk as they never expire."
1186 | )}`,
1187 | `${chalk.white(
1188 | "Consider revoking unused approvals to improve your wallet security."
1189 | )}`,
1190 | "",
1191 | chalk.bold.yellow("PRESS ANY KEY TO RETURN"),
1192 | ].join("\n"),
1193 | {
1194 | padding: 1,
1195 | borderColor: "cyan",
1196 | borderStyle: "round",
1197 | }
1198 | );
1199 |
1200 | // Add a separate, more prominent box for the revoke.cash link
1201 | const revokeLink = `https://revoke.cash/address/${TARGET_ADDRESS}`;
1202 | const revokeLinkContent = boxen(
1203 | [
1204 | chalk.bold.yellow("⚠️ HOW TO REVOKE APPROVALS ⚠️"),
1205 | "",
1206 | `${chalk.white("To manage and revoke token approvals, visit:")}`,
1207 | "",
1208 | `${chalk.bold.cyan.underline(revokeLink)}`,
1209 | ].join("\n"),
1210 | {
1211 | padding: { top: 1, bottom: 1, left: 3, right: 3 },
1212 | margin: { top: 1, bottom: 1 },
1213 | borderColor: "red",
1214 | borderStyle: "double",
1215 | }
1216 | );
1217 |
1218 | console.log(helpContent);
1219 | console.log(revokeLinkContent);
1220 |
1221 | // Wait for keypress to return
1222 | process.stdin.once("data", () => {
1223 | displayApprovalsList();
1224 | process.stdout.write(chalk.cyan.bold("> "));
1225 | });
1226 | }
1227 |
1228 | // Interactive mode with improved prompting
1229 | function startInteractivePrompt() {
1230 | // Use a visually distinct prompt
1231 | process.stdout.write(chalk.cyan.bold("> "));
1232 |
1233 | // Use a different approach with process.stdin directly
1234 | process.stdin.resume(); // Resume stdin stream
1235 | process.stdin.setEncoding("utf8");
1236 |
1237 | process.stdin.on("data", function (data) {
1238 | const command = data.toString().trim().toLowerCase();
1239 |
1240 | if (command === "q") {
1241 | console.log(chalk.green("Exiting..."));
1242 | rl.close();
1243 | process.exit(0);
1244 | } else if (command === "n") {
1245 | if (selectedApprovalIndex < approvalsList.length - 1) {
1246 | selectedApprovalIndex++;
1247 | // Update current page if selection moves to next page
1248 | if (selectedApprovalIndex >= (currentPage + 1) * PAGE_SIZE) {
1249 | currentPage = Math.floor(selectedApprovalIndex / PAGE_SIZE);
1250 | }
1251 | }
1252 | displayApprovalsList();
1253 | process.stdout.write(chalk.cyan.bold("> "));
1254 | } else if (command === "p") {
1255 | if (selectedApprovalIndex > 0) {
1256 | selectedApprovalIndex--;
1257 | // Update current page if selection moves to previous page
1258 | if (selectedApprovalIndex < currentPage * PAGE_SIZE) {
1259 | currentPage = Math.floor(selectedApprovalIndex / PAGE_SIZE);
1260 | }
1261 | }
1262 | displayApprovalsList();
1263 | process.stdout.write(chalk.cyan.bold("> "));
1264 | } else if (command === ">") {
1265 | // Next page
1266 | if ((currentPage + 1) * PAGE_SIZE < approvalsList.length) {
1267 | currentPage++;
1268 | // Update selected index to first item on new page
1269 | selectedApprovalIndex = currentPage * PAGE_SIZE;
1270 | }
1271 | displayApprovalsList();
1272 | process.stdout.write(chalk.cyan.bold("> "));
1273 | } else if (command === "<") {
1274 | // Previous page
1275 | if (currentPage > 0) {
1276 | currentPage--;
1277 | // Update selected index to first item on new page
1278 | selectedApprovalIndex = currentPage * PAGE_SIZE;
1279 | }
1280 | displayApprovalsList();
1281 | process.stdout.write(chalk.cyan.bold("> "));
1282 | } else if (command === "h") {
1283 | // Show help screen
1284 | displayHelpScreen();
1285 | } else if (command === "") {
1286 | // Enter key - toggle details view
1287 | detailsExpanded = !detailsExpanded;
1288 | displayApprovalsList();
1289 | process.stdout.write(chalk.cyan.bold("> "));
1290 | } else if (command) {
1291 | // Invalid command
1292 | console.log(
1293 | chalk.red(`Invalid command: '${command}'. Type 'h' for help.`)
1294 | );
1295 | process.stdout.write(chalk.cyan.bold("> "));
1296 | } else {
1297 | // Empty command, just redisplay prompt
1298 | process.stdout.write(chalk.cyan.bold("> "));
1299 | }
1300 | });
1301 |
1302 | // Handle Ctrl+C to exit gracefully
1303 | process.on("SIGINT", function () {
1304 | console.log("\nExiting...");
1305 | process.exit(0);
1306 | });
1307 | }
1308 |
1309 | // Main function
1310 | async function main() {
1311 | // Clear the screen and show welcome message
1312 | console.clear();
1313 | console.log(chalk.bold.cyan(figlet.textSync("snubb", { font: "Doom" })));
1314 | console.log(
1315 | chalk.bold.cyan("multichain token approval scanner") +
1316 | " - " +
1317 | chalk.cyan("powered by ") +
1318 | chalk.cyan.underline("envio.dev") +
1319 | "\n"
1320 | );
1321 | console.log(chalk.yellow(`Address: ${chalk.green(TARGET_ADDRESS)}\n`));
1322 |
1323 | // Show which chains will be scanned
1324 | console.log(chalk.yellow("Scanning chains:"));
1325 | for (const chainId of CHAIN_IDS) {
1326 | const chain = SUPPORTED_CHAINS[chainId] || {
1327 | name: `Chain ${chainId}`,
1328 | color: "white",
1329 | };
1330 | console.log(` - ${formatChainName(chainId)}`);
1331 | }
1332 | console.log("");
1333 |
1334 | try {
1335 | // Initialize chain statistics first (without spinner to show real-time progress)
1336 | console.log(chalk.bold.yellow("INITIALIZING CHAINS"));
1337 |
1338 | // Get chain heights for all chains first
1339 | for (const chainId of CHAIN_IDS) {
1340 | // Initialize per-chain stats
1341 | chainStats[chainId] = {
1342 | height: 0,
1343 | totalEvents: 0,
1344 | startTime: 0,
1345 | endTime: 0,
1346 | progressBar: drawProgressBar(0),
1347 | eventsPerSecond: 0,
1348 | approvalsCount: 0,
1349 | isScanning: false,
1350 | isComplete: false,
1351 | };
1352 |
1353 | // Initialize Hypersync client for this chain
1354 | const hypersyncUrl = `http://${chainId}.hypersync.xyz`;
1355 | try {
1356 | const client = HypersyncClient.new({
1357 | url: hypersyncUrl,
1358 | });
1359 |
1360 | // Get chain height
1361 | console.log(` Connecting to ${formatChainName(chainId)}...`);
1362 | const height = await client.getHeight();
1363 | chainStats[chainId].height = height;
1364 | console.log(
1365 | ` ${formatChainName(chainId)} height: ${formatNumber(height)}`
1366 | );
1367 | } catch (error) {
1368 | console.error(
1369 | chalk.red(` Error connecting to ${hypersyncUrl}: ${error.message}`)
1370 | );
1371 | }
1372 | }
1373 |
1374 | console.log("\n" + chalk.bold.yellow("SCANNING PROGRESS"));
1375 |
1376 | // Display initial progress bars
1377 | displayScanProgress();
1378 |
1379 | // Start scanning each chain (in parallel) but with UI updates
1380 | const scanPromises = CHAIN_IDS.map((chainId) => {
1381 | // Mark this chain as scanning
1382 | chainStats[chainId].isScanning = true;
1383 | chainStats[chainId].startTime = performance.now();
1384 |
1385 | // Return the scan promise
1386 | return scanChain(chainId)
1387 | .then((result) => {
1388 | // Mark as complete and update UI
1389 | chainStats[chainId].isScanning = false;
1390 | chainStats[chainId].isComplete = true;
1391 | displayScanProgress();
1392 | return result;
1393 | })
1394 | .catch((error) => {
1395 | // Handle error, mark as complete
1396 | console.error(
1397 | chalk.red(`Error scanning chain ${chainId}: ${error.message}`)
1398 | );
1399 | chainStats[chainId].isScanning = false;
1400 | chainStats[chainId].isComplete = true;
1401 | displayScanProgress();
1402 | return { approvals: {}, transfersUsingApprovals: {} };
1403 | });
1404 | });
1405 |
1406 | // Start UI update interval - refresh every 500ms while scanning
1407 | const uiUpdateInterval = setInterval(() => {
1408 | // Only continue updating while at least one chain is still scanning
1409 | if (Object.values(chainStats).some((stats) => stats.isScanning)) {
1410 | displayScanProgress();
1411 | } else {
1412 | clearInterval(uiUpdateInterval);
1413 | }
1414 | }, 500);
1415 |
1416 | // Wait for all scans to complete
1417 | const results = await Promise.all(scanPromises);
1418 |
1419 | // Clear the UI update interval (if not already cleared)
1420 | clearInterval(uiUpdateInterval);
1421 |
1422 | // Show completion message
1423 | console.log(chalk.green("\nAll chains scanned successfully!\n"));
1424 |
1425 | // Process approvals from all chains
1426 | approvalsList = [];
1427 |
1428 | // Combine results from all chains
1429 | results.forEach(({ approvals, transfersUsingApprovals }, index) => {
1430 | const chainId = CHAIN_IDS[index];
1431 | let chainApprovalsCount = 0;
1432 |
1433 | // Process approvals for this chain
1434 | for (const tokenAddress in approvals) {
1435 | for (const spender in approvals[tokenAddress]) {
1436 | const {
1437 | amount: approvedAmount,
1438 | blockNumber,
1439 | txHash,
1440 | } = approvals[tokenAddress][spender];
1441 | const transferredAmount =
1442 | transfersUsingApprovals[tokenAddress]?.[spender] || BigInt(0);
1443 |
1444 | // Calculate remaining approval
1445 | let remainingApproval;
1446 | let isUnlimited = false;
1447 |
1448 | // Check for unlimited approval (common values)
1449 | if (
1450 | approvedAmount === BigInt(2) ** BigInt(256) - BigInt(1) ||
1451 | approvedAmount ===
1452 | BigInt(
1453 | "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1454 | ) ||
1455 | isEffectivelyUnlimited(approvedAmount)
1456 | ) {
1457 | remainingApproval = approvedAmount;
1458 | isUnlimited = true;
1459 | } else {
1460 | remainingApproval =
1461 | approvedAmount > transferredAmount
1462 | ? approvedAmount - transferredAmount
1463 | : BigInt(0);
1464 | }
1465 |
1466 | // Only show non-zero remaining approvals
1467 | if (remainingApproval > 0) {
1468 | approvalsList.push({
1469 | chainId,
1470 | tokenAddress,
1471 | spender,
1472 | approvedAmount,
1473 | transferredAmount,
1474 | remainingApproval,
1475 | isUnlimited,
1476 | blockNumber,
1477 | txHash,
1478 | });
1479 | chainApprovalsCount++;
1480 | }
1481 | }
1482 | }
1483 |
1484 | // Update chain stats with approval count
1485 | if (chainStats[chainId]) {
1486 | chainStats[chainId].approvalsCount = chainApprovalsCount;
1487 | }
1488 | });
1489 |
1490 | // Sort approvals with priority: by chain, unlimited first across tokens, then largest amounts
1491 | approvalsList.sort((a, b) => {
1492 | // First by chain ID
1493 | if (a.chainId !== b.chainId) {
1494 | return a.chainId - b.chainId;
1495 | }
1496 |
1497 | // Group by token + unlimited status to bring unlimited tokens to the top
1498 | const aIsUnlimitedToken =
1499 | a.isUnlimited || isEffectivelyUnlimited(a.remainingApproval);
1500 | const bIsUnlimitedToken =
1501 | b.isUnlimited || isEffectivelyUnlimited(b.remainingApproval);
1502 |
1503 | // Sort unlimited tokens first within the same chain
1504 | if (aIsUnlimitedToken && !bIsUnlimitedToken) return -1;
1505 | if (!aIsUnlimitedToken && bIsUnlimitedToken) return 1;
1506 |
1507 | // For tokens with the same unlimited status, sort by token address
1508 | if (a.tokenAddress !== b.tokenAddress) {
1509 | return a.tokenAddress.localeCompare(b.tokenAddress);
1510 | }
1511 |
1512 | // Then by unlimited status (unlimited approvals first) for the same token
1513 | if (a.isUnlimited && !b.isUnlimited) return -1;
1514 | if (!a.isUnlimited && b.isUnlimited) return 1;
1515 |
1516 | // Then by remaining approval amount (highest first) for same token, non-unlimited approvals
1517 | if (!a.isUnlimited && !b.isUnlimited) {
1518 | if (b.remainingApproval > a.remainingApproval) return 1;
1519 | if (b.remainingApproval < a.remainingApproval) return -1;
1520 | }
1521 |
1522 | return 0;
1523 | });
1524 |
1525 | // Display summary
1526 | console.log(
1527 | chalk.cyan(
1528 | `Found ${chalk.white(
1529 | approvalsList.length
1530 | )} outstanding approvals for ${chalk.white(TARGET_ADDRESS)}\n`
1531 | )
1532 | );
1533 |
1534 | if (approvalsList.length === 0) {
1535 | console.log(
1536 | chalk.green("No outstanding approvals found. Your wallets are secure!")
1537 | );
1538 | rl.close();
1539 | process.exit(0);
1540 | }
1541 |
1542 | // Display initial approvals list
1543 | displayApprovalsList();
1544 |
1545 | // Start interactive mode
1546 | startInteractivePrompt();
1547 | } catch (error) {
1548 | console.error(chalk.red(`Error: ${error.message}`));
1549 | process.exit(1);
1550 | }
1551 | }
1552 |
1553 | // Function to display ongoing scan progress
1554 | function displayScanProgress() {
1555 | // No need to clear the screen - we want to see continuous updates
1556 |
1557 | // Calculate maximum width needed for chain names
1558 | const chainNameWidth =
1559 | Math.max(
1560 | ...CHAIN_IDS.map((id) => formatChainName(id).length),
1561 | 10 // Minimum width
1562 | ) + 2; // Add some padding
1563 |
1564 | // Display progress for each chain
1565 | for (const chainId of CHAIN_IDS) {
1566 | const stats = chainStats[chainId];
1567 | if (!stats) continue;
1568 |
1569 | // If chain is actively scanning
1570 | if (stats.isScanning) {
1571 | const elapsedTime = (performance.now() - stats.startTime) / 1000;
1572 | const eventsPerSecond =
1573 | stats.totalEvents > 0 ? Math.round(stats.totalEvents / elapsedTime) : 0;
1574 |
1575 | // Format numbers with consistent width
1576 | const blockDisplay = `${formatNumber(
1577 | stats.lastBlockSeen || 0
1578 | )}/${formatNumber(stats.height)}`.padEnd(20);
1579 | const eventsDisplay = formatNumber(stats.totalEvents).padEnd(8);
1580 | const speedDisplay = `${formatNumber(eventsPerSecond)}/s`.padEnd(10);
1581 |
1582 | // Calculate progress percentage - ensure it's greater than 0 if any blocks processed
1583 | const progress = stats.lastBlockSeen
1584 | ? Math.max(0.01, stats.lastBlockSeen / stats.height)
1585 | : 0;
1586 |
1587 | // Update progress bar
1588 | stats.progressBar = drawProgressBar(
1589 | progress,
1590 | 40,
1591 | SUPPORTED_CHAINS[chainId]?.color || "cyan"
1592 | );
1593 |
1594 | // Use a different format for in-progress chains with better alignment
1595 | process.stdout.write(
1596 | `\r${formatChainName(chainId).padEnd(chainNameWidth)}: ${
1597 | stats.progressBar
1598 | } Block: ${blockDisplay} | Events: ${eventsDisplay} | ${speedDisplay} `
1599 | );
1600 | process.stdout.write("\n");
1601 | }
1602 | // If chain scan is complete
1603 | else if (stats.isComplete) {
1604 | const elapsedTime = (stats.endTime / 1000).toFixed(1);
1605 |
1606 | // Format numbers with consistent width
1607 | const eventsDisplay = formatNumber(stats.totalEvents).padEnd(8);
1608 | const timeDisplay = `${elapsedTime}s`.padEnd(6);
1609 |
1610 | // Ensure progress bar shows 100% for completed chains
1611 | stats.progressBar = drawProgressBar(
1612 | 1.0,
1613 | 40,
1614 | SUPPORTED_CHAINS[chainId]?.color || "cyan"
1615 | );
1616 |
1617 | // Show completed chain with checkmark and better alignment
1618 | process.stdout.write(
1619 | `\r${formatChainName(chainId).padEnd(chainNameWidth)}: ${
1620 | stats.progressBar
1621 | } ${chalk.green(
1622 | "✓"
1623 | )} Complete | Events: ${eventsDisplay} in ${timeDisplay} `
1624 | );
1625 | process.stdout.write("\n");
1626 | }
1627 | // If not yet started scanning
1628 | else {
1629 | process.stdout.write(
1630 | `\r${formatChainName(chainId).padEnd(chainNameWidth)}: ${
1631 | stats.progressBar
1632 | } Waiting to begin scan... `
1633 | );
1634 | process.stdout.write("\n");
1635 | }
1636 | }
1637 |
1638 | // Move cursor position back up to overwrite the progress display on next update
1639 | process.stdout.write(`\x1b[${CHAIN_IDS.length}A`);
1640 | }
1641 |
1642 | // Function to scan a single chain
1643 | async function scanChain(chainId) {
1644 | // Initialize per-chain stats (should already be initialized in main)
1645 | const stats = chainStats[chainId];
1646 | const chain = SUPPORTED_CHAINS[chainId] || {
1647 | name: `Chain ${chainId}`,
1648 | color: "white",
1649 | };
1650 | const colorName = chain.color || "white";
1651 |
1652 | // Initialize Hypersync client for this chain
1653 | const hypersyncUrl = `http://${chainId}.hypersync.xyz`;
1654 | const client = HypersyncClient.new({
1655 | url: hypersyncUrl,
1656 | });
1657 |
1658 | // Create decoder for events
1659 | const decoder = Decoder.fromSignatures([
1660 | "Transfer(address indexed from, address indexed to, uint256 amount)",
1661 | "Approval(address indexed owner, address indexed spender, uint256 amount)",
1662 | ]);
1663 |
1664 | // Track approvals by token and spender
1665 | const approvals = {};
1666 | const transfersUsingApprovals = {};
1667 |
1668 | let query = createQuery(0);
1669 | let lastOutputTime = Date.now();
1670 |
1671 | // Start streaming events
1672 | const stream = await client.stream(query, {});
1673 |
1674 | while (true) {
1675 | try {
1676 | const res = await stream.recv();
1677 |
1678 | // Exit if we've reached the end of the chain
1679 | if (res === null) {
1680 | break;
1681 | }
1682 |
1683 | // Track the last block we've seen
1684 | if (res.nextBlock) {
1685 | stats.lastBlockSeen = res.nextBlock;
1686 | }
1687 |
1688 | // Process events
1689 | if (res.data && res.data.logs) {
1690 | stats.totalEvents += res.data.logs.length;
1691 |
1692 | // Decode logs
1693 | const decodedLogs = await decoder.decodeLogs(res.data.logs);
1694 |
1695 | // Process ERC20 events
1696 | for (let i = 0; i < decodedLogs.length; i++) {
1697 | const log = decodedLogs[i];
1698 | if (log === null) continue;
1699 |
1700 | try {
1701 | // Get the original raw log and transaction
1702 | const rawLog = res.data.logs[i];
1703 | if (!rawLog || !rawLog.topics || !rawLog.topics[0]) continue;
1704 |
1705 | const topic0 = rawLog.topics[0];
1706 | const tokenAddress = rawLog.address.toLowerCase();
1707 |
1708 | // Find corresponding transaction for this log
1709 | const txHash = rawLog.transactionHash;
1710 | const transaction = res.data.transactions?.find(
1711 | (tx) => tx.hash === txHash
1712 | );
1713 | const txSender = transaction?.from?.toLowerCase() || null;
1714 |
1715 | if (topic0 === APPROVAL_TOPIC) {
1716 | // Get owner and spender from indexed parameters
1717 | const owner = log.indexed[0]?.val.toString().toLowerCase() || "";
1718 | const spender =
1719 | log.indexed[1]?.val.toString().toLowerCase() || "";
1720 | const amount = log.body[0]?.val || BigInt(0);
1721 |
1722 | // Only track approvals where the target address is the owner
1723 | if (owner === TARGET_ADDRESS.toLowerCase()) {
1724 | // Initialize token in approvals map if needed
1725 | if (!approvals[tokenAddress]) {
1726 | approvals[tokenAddress] = {};
1727 | }
1728 |
1729 | // Store latest approval with block number for chronological ordering
1730 | approvals[tokenAddress][spender] = {
1731 | amount,
1732 | blockNumber: rawLog.blockNumber,
1733 | txHash,
1734 | };
1735 | }
1736 | } else if (topic0 === TRANSFER_TOPIC) {
1737 | // Get from and to from indexed parameters
1738 | const from = log.indexed[0]?.val.toString().toLowerCase() || "";
1739 | const to = log.indexed[1]?.val.toString().toLowerCase() || "";
1740 | const amount = log.body[0]?.val || BigInt(0);
1741 |
1742 | // Track transfers where the target has approved a spender (from = target, to = any)
1743 | if (from === TARGET_ADDRESS.toLowerCase()) {
1744 | // Initialize token in transfers map if needed
1745 | if (!transfersUsingApprovals[tokenAddress]) {
1746 | transfersUsingApprovals[tokenAddress] = {};
1747 | }
1748 |
1749 | // Check two cases:
1750 | // 1. Transaction initiated by spender (typical approval usage)
1751 | // 2. Transaction initiated by owner but sent to a contract with approval
1752 | const isSpenderInitiated =
1753 | txSender && txSender !== from.toLowerCase();
1754 | const isOwnerInitiatedToSpender =
1755 | txSender === from.toLowerCase() && transaction?.to;
1756 |
1757 | if (isSpenderInitiated) {
1758 | // Track against the transaction sender (spender)
1759 | if (!transfersUsingApprovals[tokenAddress][txSender]) {
1760 | transfersUsingApprovals[tokenAddress][txSender] = BigInt(0);
1761 | }
1762 | transfersUsingApprovals[tokenAddress][txSender] += amount;
1763 | } else if (isOwnerInitiatedToSpender) {
1764 | // When owner initiates a transaction to a spender
1765 | const txTo = transaction.to.toLowerCase();
1766 |
1767 | // Check if txTo is an approved spender
1768 | if (approvals[tokenAddress]?.[txTo]) {
1769 | if (!transfersUsingApprovals[tokenAddress][txTo]) {
1770 | transfersUsingApprovals[tokenAddress][txTo] = BigInt(0);
1771 | }
1772 | transfersUsingApprovals[tokenAddress][txTo] += amount;
1773 | }
1774 | }
1775 | }
1776 | }
1777 | } catch (error) {
1778 | // Silently ignore errors to prevent crashing
1779 | }
1780 | }
1781 | }
1782 |
1783 | // Update query for next batch
1784 | if (res.nextBlock) {
1785 | query.fromBlock = res.nextBlock;
1786 | }
1787 |
1788 | // Update progress display periodically
1789 | const now = Date.now();
1790 | if (now - lastOutputTime > 200) {
1791 | // More frequent updates (200ms)
1792 | const progress = Math.min(1, res.nextBlock / stats.height);
1793 | const seconds = (performance.now() - stats.startTime) / 1000;
1794 | stats.eventsPerSecond = Math.round(stats.totalEvents / seconds);
1795 |
1796 | // Update progress bar using safe color
1797 | stats.progressBar = drawProgressBar(progress, 40, colorName);
1798 |
1799 | lastOutputTime = now;
1800 | }
1801 | } catch (error) {
1802 | // Log error but continue processing
1803 | console.error(
1804 | chalk.red(`Error processing chain ${chainId}: ${error.message}`)
1805 | );
1806 | }
1807 | }
1808 |
1809 | // Processing complete
1810 | stats.endTime = performance.now() - stats.startTime;
1811 |
1812 | // Ensure progress is 100% when complete
1813 | stats.progressBar = drawProgressBar(1.0, 40, colorName);
1814 |
1815 | return { approvals, transfersUsingApprovals };
1816 | }
1817 |
1818 | // Run the main function with error handling
1819 | main().catch((error) => {
1820 | console.error(chalk.red(`Fatal error: ${error.message}`));
1821 | process.exit(1);
1822 | });
1823 |
--------------------------------------------------------------------------------
/networkCache.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "metall2",
4 | "tier": "STONE",
5 | "chain_id": 1750,
6 | "ecosystem": "evm"
7 | },
8 | {
9 | "name": "base",
10 | "tier": "GOLD",
11 | "chain_id": 8453,
12 | "ecosystem": "evm"
13 | },
14 | {
15 | "name": "lisk",
16 | "tier": "BRONZE",
17 | "chain_id": 1135,
18 | "ecosystem": "evm"
19 | },
20 | {
21 | "name": "shimmer-evm",
22 | "tier": "STONE",
23 | "chain_id": 148,
24 | "ecosystem": "evm"
25 | },
26 | {
27 | "name": "unichain",
28 | "tier": "STONE",
29 | "chain_id": 130,
30 | "ecosystem": "evm"
31 | },
32 | {
33 | "name": "arbitrum-nova",
34 | "tier": "BRONZE",
35 | "chain_id": 42170,
36 | "ecosystem": "evm"
37 | },
38 | {
39 | "name": "moonbeam",
40 | "tier": "SILVER",
41 | "chain_id": 1284,
42 | "ecosystem": "evm"
43 | },
44 | {
45 | "name": "arbitrum",
46 | "tier": "GOLD",
47 | "chain_id": 42161,
48 | "ecosystem": "evm"
49 | },
50 | {
51 | "name": "xdc",
52 | "tier": "SILVER",
53 | "chain_id": 50,
54 | "ecosystem": "evm"
55 | },
56 | {
57 | "name": "arbitrum-sepolia",
58 | "tier": "TESTNET",
59 | "chain_id": 421614,
60 | "ecosystem": "evm"
61 | },
62 | {
63 | "name": "fuel-mainnet",
64 | "tier": "GOLD",
65 | "chain_id": 9889,
66 | "ecosystem": "fuel"
67 | },
68 | {
69 | "name": "flare",
70 | "tier": "STONE",
71 | "chain_id": 14,
72 | "ecosystem": "evm"
73 | },
74 | {
75 | "name": "fantom",
76 | "tier": "BRONZE",
77 | "chain_id": 250,
78 | "ecosystem": "evm"
79 | },
80 | {
81 | "name": "zircuit",
82 | "tier": "STONE",
83 | "chain_id": 48900,
84 | "ecosystem": "evm"
85 | },
86 | {
87 | "name": "fuji",
88 | "tier": "TESTNET",
89 | "chain_id": 43113,
90 | "ecosystem": "evm"
91 | },
92 | {
93 | "name": "xdc-testnet",
94 | "tier": "TESTNET",
95 | "chain_id": 51,
96 | "ecosystem": "evm"
97 | },
98 | {
99 | "name": "bsc-testnet",
100 | "tier": "TESTNET",
101 | "chain_id": 97,
102 | "ecosystem": "evm"
103 | },
104 | {
105 | "name": "holesky-token-test",
106 | "tier": "HIDDEN",
107 | "chain_id": 17000,
108 | "ecosystem": "evm"
109 | },
110 | {
111 | "name": "ink",
112 | "tier": "STONE",
113 | "chain_id": 57073,
114 | "ecosystem": "evm"
115 | },
116 | {
117 | "name": "holesky",
118 | "tier": "TESTNET",
119 | "chain_id": 17000,
120 | "ecosystem": "evm"
121 | },
122 | {
123 | "name": "megaeth-testnet",
124 | "tier": "STONE",
125 | "chain_id": 6342,
126 | "ecosystem": "evm"
127 | },
128 | {
129 | "name": "zora",
130 | "tier": "STONE",
131 | "chain_id": 7777777,
132 | "ecosystem": "evm"
133 | },
134 | {
135 | "name": "lukso",
136 | "tier": "STONE",
137 | "chain_id": 42,
138 | "ecosystem": "evm"
139 | },
140 | {
141 | "name": "morph-holesky",
142 | "tier": "TESTNET",
143 | "chain_id": 2810,
144 | "ecosystem": "evm"
145 | },
146 | {
147 | "name": "polygon-zkevm",
148 | "tier": "STONE",
149 | "chain_id": 1101,
150 | "ecosystem": "evm"
151 | },
152 | {
153 | "name": "chiliz",
154 | "tier": "BRONZE",
155 | "chain_id": 8888,
156 | "ecosystem": "evm"
157 | },
158 | {
159 | "name": "manta",
160 | "tier": "STONE",
161 | "chain_id": 169,
162 | "ecosystem": "evm"
163 | },
164 | {
165 | "name": "rootstock",
166 | "tier": "BRONZE",
167 | "chain_id": 30,
168 | "ecosystem": "evm"
169 | },
170 | {
171 | "name": "merlin",
172 | "tier": "STONE",
173 | "chain_id": 4200,
174 | "ecosystem": "evm"
175 | },
176 | {
177 | "name": "linea",
178 | "tier": "BRONZE",
179 | "chain_id": 59144,
180 | "ecosystem": "evm"
181 | },
182 | {
183 | "name": "morph",
184 | "tier": "STONE",
185 | "chain_id": 2818,
186 | "ecosystem": "evm"
187 | },
188 | {
189 | "name": "aurora",
190 | "tier": "STONE",
191 | "chain_id": 1313161554,
192 | "ecosystem": "evm"
193 | },
194 | {
195 | "name": "boba",
196 | "tier": "STONE",
197 | "chain_id": 288,
198 | "ecosystem": "evm"
199 | },
200 | {
201 | "name": "saakuru",
202 | "tier": "STONE",
203 | "chain_id": 7225878,
204 | "ecosystem": "evm"
205 | },
206 | {
207 | "name": "extrabud",
208 | "tier": "INTERNAL",
209 | "chain_id": 283027429,
210 | "ecosystem": "evm"
211 | },
212 | {
213 | "name": "metis",
214 | "tier": "STONE",
215 | "chain_id": 1088,
216 | "ecosystem": "evm"
217 | },
218 | {
219 | "name": "citrea-testnet",
220 | "tier": "STONE",
221 | "chain_id": 5115,
222 | "ecosystem": "evm"
223 | },
224 | {
225 | "name": "blast-sepolia",
226 | "tier": "TESTNET",
227 | "chain_id": 168587773,
228 | "ecosystem": "evm"
229 | },
230 | {
231 | "name": "polygon",
232 | "tier": "GOLD",
233 | "chain_id": 137,
234 | "ecosystem": "evm"
235 | },
236 | {
237 | "name": "zeta",
238 | "tier": "STONE",
239 | "chain_id": 7000,
240 | "ecosystem": "evm"
241 | },
242 | {
243 | "name": "avalanche",
244 | "tier": "BRONZE",
245 | "chain_id": 43114,
246 | "ecosystem": "evm"
247 | },
248 | {
249 | "name": "celo",
250 | "tier": "STONE",
251 | "chain_id": 42220,
252 | "ecosystem": "evm"
253 | },
254 | {
255 | "name": "polygon-amoy",
256 | "tier": "BRONZE",
257 | "chain_id": 80002,
258 | "ecosystem": "evm"
259 | },
260 | {
261 | "name": "lukso-testnet",
262 | "tier": "TESTNET",
263 | "chain_id": 4201,
264 | "ecosystem": "evm"
265 | },
266 | {
267 | "name": "cyber",
268 | "tier": "STONE",
269 | "chain_id": 7560,
270 | "ecosystem": "evm"
271 | },
272 | {
273 | "name": "gnosis-traces",
274 | "tier": "BRONZE",
275 | "chain_id": 100,
276 | "ecosystem": "evm"
277 | },
278 | {
279 | "name": "bsc",
280 | "tier": "BRONZE",
281 | "chain_id": 56,
282 | "ecosystem": "evm"
283 | },
284 | {
285 | "name": "abstract",
286 | "tier": "STONE",
287 | "chain_id": 2741,
288 | "ecosystem": "evm"
289 | },
290 | {
291 | "name": "gnosis-chiado",
292 | "tier": "TESTNET",
293 | "chain_id": 10200,
294 | "ecosystem": "evm"
295 | },
296 | {
297 | "name": "internal-test-chain",
298 | "tier": "HIDDEN",
299 | "chain_id": 16858666,
300 | "ecosystem": "evm"
301 | },
302 | {
303 | "name": "berachain",
304 | "tier": "STONE",
305 | "chain_id": 80094,
306 | "ecosystem": "evm"
307 | },
308 | {
309 | "name": "unichain-sepolia",
310 | "tier": "TESTNET",
311 | "chain_id": 1301,
312 | "ecosystem": "evm"
313 | },
314 | {
315 | "name": "mantle",
316 | "tier": "STONE",
317 | "chain_id": 5000,
318 | "ecosystem": "evm"
319 | },
320 | {
321 | "name": "base-sepolia",
322 | "tier": "TESTNET",
323 | "chain_id": 84532,
324 | "ecosystem": "evm"
325 | },
326 | {
327 | "name": "optimism-sepolia",
328 | "tier": "TESTNET",
329 | "chain_id": 11155420,
330 | "ecosystem": "evm"
331 | },
332 | {
333 | "name": "galadriel-devnet",
334 | "tier": "TESTNET",
335 | "chain_id": 696969,
336 | "ecosystem": "evm"
337 | },
338 | {
339 | "name": "fraxtal",
340 | "tier": "STONE",
341 | "chain_id": 252,
342 | "ecosystem": "evm"
343 | },
344 | {
345 | "name": "gnosis",
346 | "tier": "GOLD",
347 | "chain_id": 100,
348 | "ecosystem": "evm"
349 | },
350 | {
351 | "name": "darwinia",
352 | "tier": "STONE",
353 | "chain_id": 46,
354 | "additional_features": ["TRACES"],
355 | "ecosystem": "evm"
356 | },
357 | {
358 | "name": "harmony-shard-0",
359 | "tier": "STONE",
360 | "chain_id": 1666600000,
361 | "ecosystem": "evm"
362 | },
363 | {
364 | "name": "sophon-testnet",
365 | "tier": "TESTNET",
366 | "chain_id": 531050104,
367 | "ecosystem": "evm"
368 | },
369 | {
370 | "name": "eth",
371 | "tier": "GOLD",
372 | "chain_id": 1,
373 | "additional_features": ["TRACES"],
374 | "ecosystem": "evm"
375 | },
376 | {
377 | "name": "mev-commit",
378 | "tier": "STONE",
379 | "chain_id": 17864,
380 | "ecosystem": "evm"
381 | },
382 | {
383 | "name": "opbnb",
384 | "tier": "STONE",
385 | "chain_id": 204,
386 | "ecosystem": "evm"
387 | },
388 | {
389 | "name": "optimism",
390 | "tier": "GOLD",
391 | "chain_id": 10,
392 | "ecosystem": "evm"
393 | },
394 | {
395 | "name": "mode",
396 | "tier": "STONE",
397 | "chain_id": 34443,
398 | "ecosystem": "evm"
399 | },
400 | {
401 | "name": "monad-testnet",
402 | "tier": "GOLD",
403 | "chain_id": 10143,
404 | "ecosystem": "evm"
405 | },
406 | {
407 | "name": "zksync",
408 | "tier": "BRONZE",
409 | "chain_id": 324,
410 | "ecosystem": "evm"
411 | },
412 | {
413 | "name": "blast",
414 | "tier": "BRONZE",
415 | "chain_id": 81457,
416 | "ecosystem": "evm"
417 | },
418 | {
419 | "name": "sophon",
420 | "tier": "BRONZE",
421 | "chain_id": 50104,
422 | "ecosystem": "evm"
423 | },
424 | {
425 | "name": "moonbase-alpha",
426 | "tier": "STONE",
427 | "chain_id": 1287,
428 | "ecosystem": "evm"
429 | },
430 | {
431 | "name": "kroma",
432 | "tier": "STONE",
433 | "chain_id": 255,
434 | "ecosystem": "evm"
435 | },
436 | {
437 | "name": "sepolia",
438 | "tier": "BRONZE",
439 | "chain_id": 11155111,
440 | "ecosystem": "evm"
441 | },
442 | {
443 | "name": "berachain-bartio",
444 | "tier": "TESTNET",
445 | "chain_id": 80084,
446 | "ecosystem": "evm"
447 | },
448 | {
449 | "name": "soneium",
450 | "tier": "STONE",
451 | "chain_id": 1868,
452 | "ecosystem": "evm"
453 | },
454 | {
455 | "name": "scroll",
456 | "tier": "BRONZE",
457 | "chain_id": 534352,
458 | "ecosystem": "evm"
459 | },
460 | {
461 | "name": "hyperliquid",
462 | "tier": "BRONZE",
463 | "chain_id": 645749,
464 | "ecosystem": "evm"
465 | },
466 | {
467 | "name": "hyperliquid-temp",
468 | "tier": "BRONZE",
469 | "chain_id": 645748,
470 | "ecosystem": "evm"
471 | },
472 | {
473 | "name": "pharos-devnet",
474 | "tier": "STONE",
475 | "chain_id": 50002,
476 | "ecosystem": "evm"
477 | }
478 | ]
479 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "snubb",
3 | "version": "1.0.3",
4 | "description": "A beautiful terminal UI for scanning blockchain token approvals and tracking your exposure",
5 | "main": "index.js",
6 | "bin": {
7 | "snubb": "./index.js"
8 | },
9 | "scripts": {
10 | "start": "node index.js",
11 | "test": "echo \"Error: no test specified\" && exit 1"
12 | },
13 | "keywords": [
14 | "ethereum",
15 | "erc20",
16 | "approvals",
17 | "revoke",
18 | "security",
19 | "blockchain",
20 | "terminal-ui",
21 | "defi",
22 | "token-scanner",
23 | "multichain"
24 | ],
25 | "author": "moose-code",
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/moose-code/snubb.git"
29 | },
30 | "homepage": "https://github.com/moose-code/snubb#readme",
31 | "bugs": {
32 | "url": "https://github.com/moose-code/snubb/issues"
33 | },
34 | "type": "module",
35 | "license": "ISC",
36 | "dependencies": {
37 | "@envio-dev/hypersync-client": "^0.6.3",
38 | "blessed": "^0.1.81",
39 | "blessed-contrib": "^4.11.0",
40 | "boxen": "^8.0.1",
41 | "chalk": "^5.4.1",
42 | "cli-spinners": "^2.9.2",
43 | "cli-table3": "^0.6.5",
44 | "commander": "^11.1.0",
45 | "figlet": "^1.8.0",
46 | "log-update": "^6.1.0",
47 | "ora": "^7.0.1",
48 | "viem": "^2.24.1"
49 | },
50 | "engines": {
51 | "node": ">=16"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | dependencies:
11 | '@envio-dev/hypersync-client':
12 | specifier: ^0.6.3
13 | version: 0.6.3
14 | blessed:
15 | specifier: ^0.1.81
16 | version: 0.1.81
17 | blessed-contrib:
18 | specifier: ^4.11.0
19 | version: 4.11.0
20 | boxen:
21 | specifier: ^8.0.1
22 | version: 8.0.1
23 | chalk:
24 | specifier: ^5.4.1
25 | version: 5.4.1
26 | cli-spinners:
27 | specifier: ^2.9.2
28 | version: 2.9.2
29 | cli-table3:
30 | specifier: ^0.6.5
31 | version: 0.6.5
32 | commander:
33 | specifier: ^11.1.0
34 | version: 11.1.0
35 | figlet:
36 | specifier: ^1.8.0
37 | version: 1.8.0
38 | log-update:
39 | specifier: ^6.1.0
40 | version: 6.1.0
41 | ora:
42 | specifier: ^7.0.1
43 | version: 7.0.1
44 | viem:
45 | specifier: ^2.24.1
46 | version: 2.24.1
47 |
48 | packages:
49 |
50 | '@adraffy/ens-normalize@1.11.0':
51 | resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==}
52 |
53 | '@colors/colors@1.5.0':
54 | resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
55 | engines: {node: '>=0.1.90'}
56 |
57 | '@envio-dev/hypersync-client-darwin-arm64@0.6.3':
58 | resolution: {integrity: sha512-w4OLJaq3lD03iXPJxLnPpoxOduvzCfEe2nYtamvIRLPB2SlbxnB5EF5dSyFs6WKh1x4PMsksQOUKeCcOtQPZow==}
59 | engines: {node: '>= 10'}
60 | cpu: [arm64]
61 | os: [darwin]
62 |
63 | '@envio-dev/hypersync-client-darwin-x64@0.6.3':
64 | resolution: {integrity: sha512-zh98zCbm2cse/iIzyMs1YCcA1iI1U/j4Yex1xBVaEa1ogzQSK9peS6M+NygzdmlwPqr7K9rUQ/U56ICRl67fWw==}
65 | engines: {node: '>= 10'}
66 | cpu: [x64]
67 | os: [darwin]
68 |
69 | '@envio-dev/hypersync-client-linux-arm64-gnu@0.6.3':
70 | resolution: {integrity: sha512-5pI0N6W7W0L7LpgN76BAWRsC+NPOvrlvBEq8IjlBUS47vqZALvcJj3gYNkm466tUBQ4q4tF1x48k7MFKtoO8aw==}
71 | engines: {node: '>= 10'}
72 | cpu: [arm64]
73 | os: [linux]
74 |
75 | '@envio-dev/hypersync-client-linux-x64-gnu@0.6.3':
76 | resolution: {integrity: sha512-jL3sxWVyTJuKdO5y/0tu1EtWSox+nJdpmr7rdVW1w0gLk4ewzWORp9cl5pXkpqM4MUsjJz+NkcQKsUquGYBkKg==}
77 | engines: {node: '>= 10'}
78 | cpu: [x64]
79 | os: [linux]
80 |
81 | '@envio-dev/hypersync-client-linux-x64-musl@0.6.3':
82 | resolution: {integrity: sha512-4rAh9x3PEWIweih731lWVEUGm+qrIHouElqZgXfTcZS6KkPeDnSYKVw10K5EyDuDtZla/NccEr0pJmcvpui5Ew==}
83 | engines: {node: '>= 10'}
84 | cpu: [x64]
85 | os: [linux]
86 |
87 | '@envio-dev/hypersync-client-win32-x64-msvc@0.6.3':
88 | resolution: {integrity: sha512-AINzLdjqU+y6ZiHnxorjFlrGNIw/AAJ80YXABYzokvGhDTF9qupjR1gdZo4sQEumgrRyg7fiG8QnsZVEm6V/zA==}
89 | engines: {node: '>= 10'}
90 | cpu: [x64]
91 | os: [win32]
92 |
93 | '@envio-dev/hypersync-client@0.6.3':
94 | resolution: {integrity: sha512-Lr5WyMZBK1cI++kAQLM/zd62NfUM60A3KWTKgeNew4NOghfoY5YZr99C7vo+CMYePXskYBR+ul+5iNPoHqkFrw==}
95 | engines: {node: '>= 10'}
96 |
97 | '@noble/curves@1.8.1':
98 | resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==}
99 | engines: {node: ^14.21.3 || >=16}
100 |
101 | '@noble/hashes@1.7.1':
102 | resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==}
103 | engines: {node: ^14.21.3 || >=16}
104 |
105 | '@scure/base@1.2.4':
106 | resolution: {integrity: sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==}
107 |
108 | '@scure/bip32@1.6.2':
109 | resolution: {integrity: sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==}
110 |
111 | '@scure/bip39@1.5.4':
112 | resolution: {integrity: sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==}
113 |
114 | abbrev@1.1.1:
115 | resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
116 |
117 | abitype@1.0.8:
118 | resolution: {integrity: sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==}
119 | peerDependencies:
120 | typescript: '>=5.0.4'
121 | zod: ^3 >=3.22.0
122 | peerDependenciesMeta:
123 | typescript:
124 | optional: true
125 | zod:
126 | optional: true
127 |
128 | ansi-align@3.0.1:
129 | resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
130 |
131 | ansi-escapes@6.2.1:
132 | resolution: {integrity: sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==}
133 | engines: {node: '>=14.16'}
134 |
135 | ansi-escapes@7.0.0:
136 | resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
137 | engines: {node: '>=18'}
138 |
139 | ansi-regex@2.1.1:
140 | resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
141 | engines: {node: '>=0.10.0'}
142 |
143 | ansi-regex@5.0.1:
144 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
145 | engines: {node: '>=8'}
146 |
147 | ansi-regex@6.1.0:
148 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
149 | engines: {node: '>=12'}
150 |
151 | ansi-styles@2.2.1:
152 | resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
153 | engines: {node: '>=0.10.0'}
154 |
155 | ansi-styles@6.2.1:
156 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
157 | engines: {node: '>=12'}
158 |
159 | ansi-term@0.0.2:
160 | resolution: {integrity: sha512-jLnGE+n8uAjksTJxiWZf/kcUmXq+cRWSl550B9NmQ8YiqaTM+lILcSe5dHdp8QkJPhaOghDjnMKwyYSMjosgAA==}
161 |
162 | ansicolors@0.3.2:
163 | resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==}
164 |
165 | base64-js@1.5.1:
166 | resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
167 |
168 | bl@5.1.0:
169 | resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==}
170 |
171 | blessed-contrib@4.11.0:
172 | resolution: {integrity: sha512-P00Xji3xPp53+FdU9f74WpvnOAn/SS0CKLy4vLAf5Ps7FGDOTY711ruJPZb3/7dpFuP+4i7f4a/ZTZdLlKG9WA==}
173 |
174 | blessed@0.1.81:
175 | resolution: {integrity: sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==}
176 | engines: {node: '>= 0.8.0'}
177 | hasBin: true
178 |
179 | boxen@8.0.1:
180 | resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
181 | engines: {node: '>=18'}
182 |
183 | bresenham@0.0.3:
184 | resolution: {integrity: sha512-wbMxoJJM1p3+6G7xEFXYNCJ30h2qkwmVxebkbwIl4OcnWtno5R3UT9VuYLfStlVNAQCmRjkGwjPFdfaPd4iNXw==}
185 |
186 | buffer@6.0.3:
187 | resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
188 |
189 | buffers@0.1.1:
190 | resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==}
191 | engines: {node: '>=0.2.0'}
192 |
193 | camelcase@8.0.0:
194 | resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
195 | engines: {node: '>=16'}
196 |
197 | cardinal@2.1.1:
198 | resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==}
199 | hasBin: true
200 |
201 | chalk@1.1.3:
202 | resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
203 | engines: {node: '>=0.10.0'}
204 |
205 | chalk@5.4.1:
206 | resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
207 | engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
208 |
209 | charm@0.1.2:
210 | resolution: {integrity: sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==}
211 |
212 | cli-boxes@3.0.0:
213 | resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
214 | engines: {node: '>=10'}
215 |
216 | cli-cursor@4.0.0:
217 | resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
218 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
219 |
220 | cli-cursor@5.0.0:
221 | resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
222 | engines: {node: '>=18'}
223 |
224 | cli-spinners@2.9.2:
225 | resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
226 | engines: {node: '>=6'}
227 |
228 | cli-table3@0.6.5:
229 | resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==}
230 | engines: {node: 10.* || >= 12.*}
231 |
232 | commander@11.1.0:
233 | resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
234 | engines: {node: '>=16'}
235 |
236 | core-util-is@1.0.3:
237 | resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
238 |
239 | drawille-blessed-contrib@1.0.0:
240 | resolution: {integrity: sha512-WnHMgf5en/hVOsFhxLI8ZX0qTJmerOsVjIMQmn4cR1eI8nLGu+L7w5ENbul+lZ6w827A3JakCuernES5xbHLzQ==}
241 |
242 | drawille-canvas-blessed-contrib@0.1.3:
243 | resolution: {integrity: sha512-bdDvVJOxlrEoPLifGDPaxIzFh3cD7QH05ePoQ4fwnqfi08ZSxzEhOUpI5Z0/SQMlWgcCQOEtuw0zrwezacXglw==}
244 |
245 | eastasianwidth@0.2.0:
246 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
247 |
248 | emoji-regex@10.4.0:
249 | resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
250 |
251 | emoji-regex@8.0.0:
252 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
253 |
254 | environment@1.1.0:
255 | resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
256 | engines: {node: '>=18'}
257 |
258 | escape-string-regexp@1.0.5:
259 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
260 | engines: {node: '>=0.8.0'}
261 |
262 | esprima@4.0.1:
263 | resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
264 | engines: {node: '>=4'}
265 | hasBin: true
266 |
267 | event-stream@0.9.8:
268 | resolution: {integrity: sha512-o5h0Mp1bkoR6B0i7pTCAzRy+VzdsRWH997KQD4Psb0EOPoKEIiaRx/EsOdUl7p1Ktjw7aIWvweI/OY1R9XrlUg==}
269 |
270 | eventemitter3@5.0.1:
271 | resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
272 |
273 | figlet@1.8.0:
274 | resolution: {integrity: sha512-chzvGjd+Sp7KUvPHZv6EXV5Ir3Q7kYNpCr4aHrRW79qFtTefmQZNny+W1pW9kf5zeE6dikku2W50W/wAH2xWgw==}
275 | engines: {node: '>= 0.4.0'}
276 | hasBin: true
277 |
278 | get-east-asian-width@1.3.0:
279 | resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
280 | engines: {node: '>=18'}
281 |
282 | gl-matrix@2.8.1:
283 | resolution: {integrity: sha512-0YCjVpE3pS5XWlN3J4X7AiAx65+nqAI54LndtVFnQZB6G/FVLkZH8y8V6R3cIoOQR4pUdfwQGd1iwyoXHJ4Qfw==}
284 |
285 | has-ansi@2.0.0:
286 | resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==}
287 | engines: {node: '>=0.10.0'}
288 |
289 | has-flag@4.0.0:
290 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
291 | engines: {node: '>=8'}
292 |
293 | here@0.0.2:
294 | resolution: {integrity: sha512-U7VYImCTcPoY27TSmzoiFsmWLEqQFaYNdpsPb9K0dXJhE6kufUqycaz51oR09CW85dDU9iWyy7At8M+p7hb3NQ==}
295 |
296 | ieee754@1.2.1:
297 | resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
298 |
299 | inherits@2.0.4:
300 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
301 |
302 | is-fullwidth-code-point@3.0.0:
303 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
304 | engines: {node: '>=8'}
305 |
306 | is-fullwidth-code-point@5.0.0:
307 | resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==}
308 | engines: {node: '>=18'}
309 |
310 | is-interactive@2.0.0:
311 | resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
312 | engines: {node: '>=12'}
313 |
314 | is-unicode-supported@1.3.0:
315 | resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
316 | engines: {node: '>=12'}
317 |
318 | isarray@0.0.1:
319 | resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
320 |
321 | isows@1.0.6:
322 | resolution: {integrity: sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==}
323 | peerDependencies:
324 | ws: '*'
325 |
326 | lodash@4.17.21:
327 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
328 |
329 | log-symbols@5.1.0:
330 | resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==}
331 | engines: {node: '>=12'}
332 |
333 | log-update@6.1.0:
334 | resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
335 | engines: {node: '>=18'}
336 |
337 | map-canvas@0.1.5:
338 | resolution: {integrity: sha512-f7M3sOuL9+up0NCOZbb1rQpWDLZwR/ftCiNbyscjl9LUUEwrRaoumH4sz6swgs58lF21DQ0hsYOCw5C6Zz7hbg==}
339 |
340 | marked-terminal@5.2.0:
341 | resolution: {integrity: sha512-Piv6yNwAQXGFjZSaiNljyNFw7jKDdGrw70FSbtxEyldLsyeuV5ZHm/1wW++kWbrOF1VPnUgYOhB2oLL0ZpnekA==}
342 | engines: {node: '>=14.13.1 || >=16.0.0'}
343 | peerDependencies:
344 | marked: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
345 |
346 | marked@4.3.0:
347 | resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
348 | engines: {node: '>= 12'}
349 | hasBin: true
350 |
351 | memory-streams@0.1.3:
352 | resolution: {integrity: sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA==}
353 |
354 | memorystream@0.3.1:
355 | resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
356 | engines: {node: '>= 0.10.0'}
357 |
358 | mimic-fn@2.1.0:
359 | resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
360 | engines: {node: '>=6'}
361 |
362 | mimic-function@5.0.1:
363 | resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
364 | engines: {node: '>=18'}
365 |
366 | node-emoji@1.11.0:
367 | resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
368 |
369 | nopt@2.1.2:
370 | resolution: {integrity: sha512-x8vXm7BZ2jE1Txrxh/hO74HTuYZQEbo8edoRcANgdZ4+PCV+pbjd/xdummkmjjC7LU5EjPzlu8zEq/oxWylnKA==}
371 | hasBin: true
372 |
373 | onetime@5.1.2:
374 | resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
375 | engines: {node: '>=6'}
376 |
377 | onetime@7.0.0:
378 | resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
379 | engines: {node: '>=18'}
380 |
381 | optimist@0.2.8:
382 | resolution: {integrity: sha512-Wy7E3cQDpqsTIFyW7m22hSevyTLxw850ahYv7FWsw4G6MIKVTZ8NSA95KBrQ95a4SMsMr1UGUUnwEFKhVaSzIg==}
383 |
384 | optimist@0.3.7:
385 | resolution: {integrity: sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==}
386 |
387 | ora@7.0.1:
388 | resolution: {integrity: sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==}
389 | engines: {node: '>=16'}
390 |
391 | ox@0.6.9:
392 | resolution: {integrity: sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==}
393 | peerDependencies:
394 | typescript: '>=5.4.0'
395 | peerDependenciesMeta:
396 | typescript:
397 | optional: true
398 |
399 | picture-tuber@1.0.2:
400 | resolution: {integrity: sha512-49/xq+wzbwDeI32aPvwQJldM8pr7dKDRuR76IjztrkmiCkAQDaWFJzkmfVqCHmt/iFoPFhHmI9L0oKhthrTOQw==}
401 | engines: {node: '>=0.4.0'}
402 | hasBin: true
403 |
404 | png-js@0.1.1:
405 | resolution: {integrity: sha512-NTtk2SyfjBm+xYl2/VZJBhFnTQ4kU5qWC7VC4/iGbrgiU4FuB4xC+74erxADYJIqZICOR1HCvRA7EBHkpjTg9g==}
406 |
407 | readable-stream@1.0.34:
408 | resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==}
409 |
410 | readable-stream@3.6.2:
411 | resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
412 | engines: {node: '>= 6'}
413 |
414 | redeyed@2.1.1:
415 | resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==}
416 |
417 | restore-cursor@4.0.0:
418 | resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
419 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
420 |
421 | restore-cursor@5.1.0:
422 | resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
423 | engines: {node: '>=18'}
424 |
425 | safe-buffer@5.2.1:
426 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
427 |
428 | sax@1.4.1:
429 | resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
430 |
431 | signal-exit@3.0.7:
432 | resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
433 |
434 | signal-exit@4.1.0:
435 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
436 | engines: {node: '>=14'}
437 |
438 | slice-ansi@7.1.0:
439 | resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
440 | engines: {node: '>=18'}
441 |
442 | sparkline@0.1.2:
443 | resolution: {integrity: sha512-t//aVOiWt9fi/e22ea1vXVWBDX+gp18y+Ch9sKqmHl828bRfvP2VtfTJVEcgWFBQHd0yDPNQRiHdqzCvbcYSDA==}
444 | engines: {node: '>= 0.8.0'}
445 | hasBin: true
446 |
447 | stdin-discarder@0.1.0:
448 | resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==}
449 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
450 |
451 | string-width@4.2.3:
452 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
453 | engines: {node: '>=8'}
454 |
455 | string-width@6.1.0:
456 | resolution: {integrity: sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==}
457 | engines: {node: '>=16'}
458 |
459 | string-width@7.2.0:
460 | resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
461 | engines: {node: '>=18'}
462 |
463 | string_decoder@0.10.31:
464 | resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
465 |
466 | string_decoder@1.3.0:
467 | resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
468 |
469 | strip-ansi@3.0.1:
470 | resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
471 | engines: {node: '>=0.10.0'}
472 |
473 | strip-ansi@6.0.1:
474 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
475 | engines: {node: '>=8'}
476 |
477 | strip-ansi@7.1.0:
478 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
479 | engines: {node: '>=12'}
480 |
481 | supports-color@2.0.0:
482 | resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
483 | engines: {node: '>=0.8.0'}
484 |
485 | supports-color@7.2.0:
486 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
487 | engines: {node: '>=8'}
488 |
489 | supports-hyperlinks@2.3.0:
490 | resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
491 | engines: {node: '>=8'}
492 |
493 | term-canvas@0.0.5:
494 | resolution: {integrity: sha512-eZ3rIWi5yLnKiUcsW8P79fKyooaLmyLWAGqBhFspqMxRNUiB4GmHHk5AzQ4LxvFbJILaXqQZLwbbATLOhCFwkw==}
495 |
496 | type-fest@4.38.0:
497 | resolution: {integrity: sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==}
498 | engines: {node: '>=16'}
499 |
500 | util-deprecate@1.0.2:
501 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
502 |
503 | viem@2.24.1:
504 | resolution: {integrity: sha512-xptFlc081SIPz+ZNDeb0XS/Nn5PU28onq+im+UxEAPCXTIuL1kfw1GTnV8NhbUNoEONnrwcZNqoI0AT0ADF5XQ==}
505 | peerDependencies:
506 | typescript: '>=5.0.4'
507 | peerDependenciesMeta:
508 | typescript:
509 | optional: true
510 |
511 | widest-line@5.0.0:
512 | resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
513 | engines: {node: '>=18'}
514 |
515 | wordwrap@0.0.3:
516 | resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==}
517 | engines: {node: '>=0.4.0'}
518 |
519 | wrap-ansi@9.0.0:
520 | resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
521 | engines: {node: '>=18'}
522 |
523 | ws@8.18.1:
524 | resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==}
525 | engines: {node: '>=10.0.0'}
526 | peerDependencies:
527 | bufferutil: ^4.0.1
528 | utf-8-validate: '>=5.0.2'
529 | peerDependenciesMeta:
530 | bufferutil:
531 | optional: true
532 | utf-8-validate:
533 | optional: true
534 |
535 | x256@0.0.2:
536 | resolution: {integrity: sha512-ZsIH+sheoF8YG9YG+QKEEIdtqpHRA9FYuD7MqhfyB1kayXU43RUNBFSxBEnF8ywSUxdg+8no4+bPr5qLbyxKgA==}
537 | engines: {node: '>=0.4.0'}
538 |
539 | xml2js@0.4.23:
540 | resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}
541 | engines: {node: '>=4.0.0'}
542 |
543 | xmlbuilder@11.0.1:
544 | resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
545 | engines: {node: '>=4.0'}
546 |
547 | snapshots:
548 |
549 | '@adraffy/ens-normalize@1.11.0': {}
550 |
551 | '@colors/colors@1.5.0':
552 | optional: true
553 |
554 | '@envio-dev/hypersync-client-darwin-arm64@0.6.3':
555 | optional: true
556 |
557 | '@envio-dev/hypersync-client-darwin-x64@0.6.3':
558 | optional: true
559 |
560 | '@envio-dev/hypersync-client-linux-arm64-gnu@0.6.3':
561 | optional: true
562 |
563 | '@envio-dev/hypersync-client-linux-x64-gnu@0.6.3':
564 | optional: true
565 |
566 | '@envio-dev/hypersync-client-linux-x64-musl@0.6.3':
567 | optional: true
568 |
569 | '@envio-dev/hypersync-client-win32-x64-msvc@0.6.3':
570 | optional: true
571 |
572 | '@envio-dev/hypersync-client@0.6.3':
573 | optionalDependencies:
574 | '@envio-dev/hypersync-client-darwin-arm64': 0.6.3
575 | '@envio-dev/hypersync-client-darwin-x64': 0.6.3
576 | '@envio-dev/hypersync-client-linux-arm64-gnu': 0.6.3
577 | '@envio-dev/hypersync-client-linux-x64-gnu': 0.6.3
578 | '@envio-dev/hypersync-client-linux-x64-musl': 0.6.3
579 | '@envio-dev/hypersync-client-win32-x64-msvc': 0.6.3
580 |
581 | '@noble/curves@1.8.1':
582 | dependencies:
583 | '@noble/hashes': 1.7.1
584 |
585 | '@noble/hashes@1.7.1': {}
586 |
587 | '@scure/base@1.2.4': {}
588 |
589 | '@scure/bip32@1.6.2':
590 | dependencies:
591 | '@noble/curves': 1.8.1
592 | '@noble/hashes': 1.7.1
593 | '@scure/base': 1.2.4
594 |
595 | '@scure/bip39@1.5.4':
596 | dependencies:
597 | '@noble/hashes': 1.7.1
598 | '@scure/base': 1.2.4
599 |
600 | abbrev@1.1.1: {}
601 |
602 | abitype@1.0.8: {}
603 |
604 | ansi-align@3.0.1:
605 | dependencies:
606 | string-width: 4.2.3
607 |
608 | ansi-escapes@6.2.1: {}
609 |
610 | ansi-escapes@7.0.0:
611 | dependencies:
612 | environment: 1.1.0
613 |
614 | ansi-regex@2.1.1: {}
615 |
616 | ansi-regex@5.0.1: {}
617 |
618 | ansi-regex@6.1.0: {}
619 |
620 | ansi-styles@2.2.1: {}
621 |
622 | ansi-styles@6.2.1: {}
623 |
624 | ansi-term@0.0.2:
625 | dependencies:
626 | x256: 0.0.2
627 |
628 | ansicolors@0.3.2: {}
629 |
630 | base64-js@1.5.1: {}
631 |
632 | bl@5.1.0:
633 | dependencies:
634 | buffer: 6.0.3
635 | inherits: 2.0.4
636 | readable-stream: 3.6.2
637 |
638 | blessed-contrib@4.11.0:
639 | dependencies:
640 | ansi-term: 0.0.2
641 | chalk: 1.1.3
642 | drawille-canvas-blessed-contrib: 0.1.3
643 | lodash: 4.17.21
644 | map-canvas: 0.1.5
645 | marked: 4.3.0
646 | marked-terminal: 5.2.0(marked@4.3.0)
647 | memory-streams: 0.1.3
648 | memorystream: 0.3.1
649 | picture-tuber: 1.0.2
650 | sparkline: 0.1.2
651 | strip-ansi: 3.0.1
652 | term-canvas: 0.0.5
653 | x256: 0.0.2
654 |
655 | blessed@0.1.81: {}
656 |
657 | boxen@8.0.1:
658 | dependencies:
659 | ansi-align: 3.0.1
660 | camelcase: 8.0.0
661 | chalk: 5.4.1
662 | cli-boxes: 3.0.0
663 | string-width: 7.2.0
664 | type-fest: 4.38.0
665 | widest-line: 5.0.0
666 | wrap-ansi: 9.0.0
667 |
668 | bresenham@0.0.3: {}
669 |
670 | buffer@6.0.3:
671 | dependencies:
672 | base64-js: 1.5.1
673 | ieee754: 1.2.1
674 |
675 | buffers@0.1.1: {}
676 |
677 | camelcase@8.0.0: {}
678 |
679 | cardinal@2.1.1:
680 | dependencies:
681 | ansicolors: 0.3.2
682 | redeyed: 2.1.1
683 |
684 | chalk@1.1.3:
685 | dependencies:
686 | ansi-styles: 2.2.1
687 | escape-string-regexp: 1.0.5
688 | has-ansi: 2.0.0
689 | strip-ansi: 3.0.1
690 | supports-color: 2.0.0
691 |
692 | chalk@5.4.1: {}
693 |
694 | charm@0.1.2: {}
695 |
696 | cli-boxes@3.0.0: {}
697 |
698 | cli-cursor@4.0.0:
699 | dependencies:
700 | restore-cursor: 4.0.0
701 |
702 | cli-cursor@5.0.0:
703 | dependencies:
704 | restore-cursor: 5.1.0
705 |
706 | cli-spinners@2.9.2: {}
707 |
708 | cli-table3@0.6.5:
709 | dependencies:
710 | string-width: 4.2.3
711 | optionalDependencies:
712 | '@colors/colors': 1.5.0
713 |
714 | commander@11.1.0: {}
715 |
716 | core-util-is@1.0.3: {}
717 |
718 | drawille-blessed-contrib@1.0.0: {}
719 |
720 | drawille-canvas-blessed-contrib@0.1.3:
721 | dependencies:
722 | ansi-term: 0.0.2
723 | bresenham: 0.0.3
724 | drawille-blessed-contrib: 1.0.0
725 | gl-matrix: 2.8.1
726 | x256: 0.0.2
727 |
728 | eastasianwidth@0.2.0: {}
729 |
730 | emoji-regex@10.4.0: {}
731 |
732 | emoji-regex@8.0.0: {}
733 |
734 | environment@1.1.0: {}
735 |
736 | escape-string-regexp@1.0.5: {}
737 |
738 | esprima@4.0.1: {}
739 |
740 | event-stream@0.9.8:
741 | dependencies:
742 | optimist: 0.2.8
743 |
744 | eventemitter3@5.0.1: {}
745 |
746 | figlet@1.8.0: {}
747 |
748 | get-east-asian-width@1.3.0: {}
749 |
750 | gl-matrix@2.8.1: {}
751 |
752 | has-ansi@2.0.0:
753 | dependencies:
754 | ansi-regex: 2.1.1
755 |
756 | has-flag@4.0.0: {}
757 |
758 | here@0.0.2: {}
759 |
760 | ieee754@1.2.1: {}
761 |
762 | inherits@2.0.4: {}
763 |
764 | is-fullwidth-code-point@3.0.0: {}
765 |
766 | is-fullwidth-code-point@5.0.0:
767 | dependencies:
768 | get-east-asian-width: 1.3.0
769 |
770 | is-interactive@2.0.0: {}
771 |
772 | is-unicode-supported@1.3.0: {}
773 |
774 | isarray@0.0.1: {}
775 |
776 | isows@1.0.6(ws@8.18.1):
777 | dependencies:
778 | ws: 8.18.1
779 |
780 | lodash@4.17.21: {}
781 |
782 | log-symbols@5.1.0:
783 | dependencies:
784 | chalk: 5.4.1
785 | is-unicode-supported: 1.3.0
786 |
787 | log-update@6.1.0:
788 | dependencies:
789 | ansi-escapes: 7.0.0
790 | cli-cursor: 5.0.0
791 | slice-ansi: 7.1.0
792 | strip-ansi: 7.1.0
793 | wrap-ansi: 9.0.0
794 |
795 | map-canvas@0.1.5:
796 | dependencies:
797 | drawille-canvas-blessed-contrib: 0.1.3
798 | xml2js: 0.4.23
799 |
800 | marked-terminal@5.2.0(marked@4.3.0):
801 | dependencies:
802 | ansi-escapes: 6.2.1
803 | cardinal: 2.1.1
804 | chalk: 5.4.1
805 | cli-table3: 0.6.5
806 | marked: 4.3.0
807 | node-emoji: 1.11.0
808 | supports-hyperlinks: 2.3.0
809 |
810 | marked@4.3.0: {}
811 |
812 | memory-streams@0.1.3:
813 | dependencies:
814 | readable-stream: 1.0.34
815 |
816 | memorystream@0.3.1: {}
817 |
818 | mimic-fn@2.1.0: {}
819 |
820 | mimic-function@5.0.1: {}
821 |
822 | node-emoji@1.11.0:
823 | dependencies:
824 | lodash: 4.17.21
825 |
826 | nopt@2.1.2:
827 | dependencies:
828 | abbrev: 1.1.1
829 |
830 | onetime@5.1.2:
831 | dependencies:
832 | mimic-fn: 2.1.0
833 |
834 | onetime@7.0.0:
835 | dependencies:
836 | mimic-function: 5.0.1
837 |
838 | optimist@0.2.8:
839 | dependencies:
840 | wordwrap: 0.0.3
841 |
842 | optimist@0.3.7:
843 | dependencies:
844 | wordwrap: 0.0.3
845 |
846 | ora@7.0.1:
847 | dependencies:
848 | chalk: 5.4.1
849 | cli-cursor: 4.0.0
850 | cli-spinners: 2.9.2
851 | is-interactive: 2.0.0
852 | is-unicode-supported: 1.3.0
853 | log-symbols: 5.1.0
854 | stdin-discarder: 0.1.0
855 | string-width: 6.1.0
856 | strip-ansi: 7.1.0
857 |
858 | ox@0.6.9:
859 | dependencies:
860 | '@adraffy/ens-normalize': 1.11.0
861 | '@noble/curves': 1.8.1
862 | '@noble/hashes': 1.7.1
863 | '@scure/bip32': 1.6.2
864 | '@scure/bip39': 1.5.4
865 | abitype: 1.0.8
866 | eventemitter3: 5.0.1
867 | transitivePeerDependencies:
868 | - zod
869 |
870 | picture-tuber@1.0.2:
871 | dependencies:
872 | buffers: 0.1.1
873 | charm: 0.1.2
874 | event-stream: 0.9.8
875 | optimist: 0.3.7
876 | png-js: 0.1.1
877 | x256: 0.0.2
878 |
879 | png-js@0.1.1: {}
880 |
881 | readable-stream@1.0.34:
882 | dependencies:
883 | core-util-is: 1.0.3
884 | inherits: 2.0.4
885 | isarray: 0.0.1
886 | string_decoder: 0.10.31
887 |
888 | readable-stream@3.6.2:
889 | dependencies:
890 | inherits: 2.0.4
891 | string_decoder: 1.3.0
892 | util-deprecate: 1.0.2
893 |
894 | redeyed@2.1.1:
895 | dependencies:
896 | esprima: 4.0.1
897 |
898 | restore-cursor@4.0.0:
899 | dependencies:
900 | onetime: 5.1.2
901 | signal-exit: 3.0.7
902 |
903 | restore-cursor@5.1.0:
904 | dependencies:
905 | onetime: 7.0.0
906 | signal-exit: 4.1.0
907 |
908 | safe-buffer@5.2.1: {}
909 |
910 | sax@1.4.1: {}
911 |
912 | signal-exit@3.0.7: {}
913 |
914 | signal-exit@4.1.0: {}
915 |
916 | slice-ansi@7.1.0:
917 | dependencies:
918 | ansi-styles: 6.2.1
919 | is-fullwidth-code-point: 5.0.0
920 |
921 | sparkline@0.1.2:
922 | dependencies:
923 | here: 0.0.2
924 | nopt: 2.1.2
925 |
926 | stdin-discarder@0.1.0:
927 | dependencies:
928 | bl: 5.1.0
929 |
930 | string-width@4.2.3:
931 | dependencies:
932 | emoji-regex: 8.0.0
933 | is-fullwidth-code-point: 3.0.0
934 | strip-ansi: 6.0.1
935 |
936 | string-width@6.1.0:
937 | dependencies:
938 | eastasianwidth: 0.2.0
939 | emoji-regex: 10.4.0
940 | strip-ansi: 7.1.0
941 |
942 | string-width@7.2.0:
943 | dependencies:
944 | emoji-regex: 10.4.0
945 | get-east-asian-width: 1.3.0
946 | strip-ansi: 7.1.0
947 |
948 | string_decoder@0.10.31: {}
949 |
950 | string_decoder@1.3.0:
951 | dependencies:
952 | safe-buffer: 5.2.1
953 |
954 | strip-ansi@3.0.1:
955 | dependencies:
956 | ansi-regex: 2.1.1
957 |
958 | strip-ansi@6.0.1:
959 | dependencies:
960 | ansi-regex: 5.0.1
961 |
962 | strip-ansi@7.1.0:
963 | dependencies:
964 | ansi-regex: 6.1.0
965 |
966 | supports-color@2.0.0: {}
967 |
968 | supports-color@7.2.0:
969 | dependencies:
970 | has-flag: 4.0.0
971 |
972 | supports-hyperlinks@2.3.0:
973 | dependencies:
974 | has-flag: 4.0.0
975 | supports-color: 7.2.0
976 |
977 | term-canvas@0.0.5: {}
978 |
979 | type-fest@4.38.0: {}
980 |
981 | util-deprecate@1.0.2: {}
982 |
983 | viem@2.24.1:
984 | dependencies:
985 | '@noble/curves': 1.8.1
986 | '@noble/hashes': 1.7.1
987 | '@scure/bip32': 1.6.2
988 | '@scure/bip39': 1.5.4
989 | abitype: 1.0.8
990 | isows: 1.0.6(ws@8.18.1)
991 | ox: 0.6.9
992 | ws: 8.18.1
993 | transitivePeerDependencies:
994 | - bufferutil
995 | - utf-8-validate
996 | - zod
997 |
998 | widest-line@5.0.0:
999 | dependencies:
1000 | string-width: 7.2.0
1001 |
1002 | wordwrap@0.0.3: {}
1003 |
1004 | wrap-ansi@9.0.0:
1005 | dependencies:
1006 | ansi-styles: 6.2.1
1007 | string-width: 7.2.0
1008 | strip-ansi: 7.1.0
1009 |
1010 | ws@8.18.1: {}
1011 |
1012 | x256@0.0.2: {}
1013 |
1014 | xml2js@0.4.23:
1015 | dependencies:
1016 | sax: 1.4.1
1017 | xmlbuilder: 11.0.1
1018 |
1019 | xmlbuilder@11.0.1: {}
1020 |
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moose-code/snubb/af7c1a35bdebc8fe993bc45654a7d17e5428aa46/preview.png
--------------------------------------------------------------------------------