├── .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 | ![Terminal UI Screenshot](./preview.png) 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 --------------------------------------------------------------------------------