├── hypersync.gif ├── lib ├── index.js ├── config.js └── scanner.js ├── .gitignore ├── .npmignore ├── package.json ├── .networks-cache.json ├── README.md └── bin └── logtui.js /hypersync.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moose-code/logtui/HEAD/hypersync.gif -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LogTUI - Main Export File 3 | * 4 | * This file exports the public API for the LogTUI package 5 | */ 6 | 7 | // Export scanner functionality 8 | export { createScanner } from "./scanner.js"; 9 | 10 | // Export configuration utilities 11 | export { 12 | getNetworkUrl, 13 | getEventSignatures, 14 | hasPreset, 15 | listPresets, 16 | NETWORKS, 17 | EVENT_PRESETS, 18 | } from "./config.js"; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | package-lock.json 7 | pnpm-lock.yaml 8 | 9 | # Environment variables 10 | .env 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | # Build output 17 | dist/ 18 | build/ 19 | 20 | # Editor directories 21 | .idea/ 22 | .vscode/ 23 | *.swp 24 | *.swo 25 | 26 | # OS specific 27 | .DS_Store 28 | Thumbs.db 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Development files 2 | .git 3 | .github 4 | .gitignore 5 | node_modules 6 | 7 | # Test files 8 | test/ 9 | tests/ 10 | __tests__/ 11 | 12 | # Miscellaneous 13 | .DS_Store 14 | .env 15 | .env.* 16 | *.log 17 | .npmrc 18 | .vscode/ 19 | .idea/ 20 | 21 | # Build files 22 | coverage/ 23 | 24 | # Documentation drafts or internal notes 25 | docs-internal/ 26 | 27 | # Exclude large media files (keep in Git but not in npm) 28 | *.gif 29 | *.png 30 | *.jpg 31 | *.jpeg 32 | hypersync.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logtui", 3 | "version": "0.2.4", 4 | "description": "A terminal-based UI for monitoring blockchain events using Hypersync", 5 | "main": "lib/index.js", 6 | "bin": { 7 | "logtui": "./bin/logtui.js" 8 | }, 9 | "scripts": { 10 | "start": "node ./bin/logtui.js", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "blockchain", 15 | "ethereum", 16 | "events", 17 | "hypersync", 18 | "terminal", 19 | "ui", 20 | "dashboard", 21 | "uniswap" 22 | ], 23 | "author": "moose-code envio.dev", 24 | "license": "MIT", 25 | "dependencies": { 26 | "@envio-dev/hypersync-client": "^0.6.3", 27 | "blessed": "^0.1.81", 28 | "blessed-contrib": "^4.11.0", 29 | "chalk": "^5.4.1", 30 | "commander": "^12.0.0", 31 | "figlet": "^1.8.0", 32 | "node-fetch": "^3.3.2", 33 | "supports-color": "^10.0.0", 34 | "viem": "^2.9.6" 35 | }, 36 | "type": "module", 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/moose-code/logtui" 40 | }, 41 | "homepage": "https://envio.dev", 42 | "engines": { 43 | "node": ">=18.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.networks-cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "metall2": "http://metall2.hypersync.xyz", 3 | "curtis": "http://curtis.hypersync.xyz", 4 | "abstract": "http://abstract.hypersync.xyz", 5 | "hyperliquid": "http://hyperliquid.hypersync.xyz", 6 | "soneium": "http://soneium.hypersync.xyz", 7 | "megaeth-testnet": "http://megaeth-testnet.hypersync.xyz", 8 | "unichain": "http://unichain.hypersync.xyz", 9 | "taraxa": "http://taraxa.hypersync.xyz", 10 | "xdc": "http://xdc.hypersync.xyz", 11 | "chiliz": "http://chiliz.hypersync.xyz", 12 | "gnosis-traces": "http://gnosis-traces.hypersync.xyz", 13 | "xdc-testnet": "http://xdc-testnet.hypersync.xyz", 14 | "arbitrum-sepolia": "http://arbitrum-sepolia.hypersync.xyz", 15 | "worldchain": "http://worldchain.hypersync.xyz", 16 | "arbitrum-nova": "http://arbitrum-nova.hypersync.xyz", 17 | "ink": "http://ink.hypersync.xyz", 18 | "unichain-sepolia": "http://unichain-sepolia.hypersync.xyz", 19 | "optimism": "http://optimism.hypersync.xyz", 20 | "mantle": "http://mantle.hypersync.xyz", 21 | "swell": "http://swell.hypersync.xyz", 22 | "cyber": "http://cyber.hypersync.xyz", 23 | "saakuru": "http://saakuru.hypersync.xyz", 24 | "arbitrum": "http://arbitrum.hypersync.xyz", 25 | "base": "http://base.hypersync.xyz", 26 | "blast-sepolia": "http://blast-sepolia.hypersync.xyz", 27 | "fraxtal": "http://fraxtal.hypersync.xyz", 28 | "aurora": "http://aurora.hypersync.xyz", 29 | "scroll": "http://scroll.hypersync.xyz", 30 | "eth": "http://eth.hypersync.xyz", 31 | "moonbeam": "http://moonbeam.hypersync.xyz", 32 | "polygon-amoy": "http://polygon-amoy.hypersync.xyz", 33 | "morph": "http://morph.hypersync.xyz", 34 | "fantom": "http://fantom.hypersync.xyz", 35 | "polygon-zkevm": "http://polygon-zkevm.hypersync.xyz", 36 | "base-sepolia": "http://base-sepolia.hypersync.xyz", 37 | "shimmer-evm": "http://shimmer-evm.hypersync.xyz", 38 | "blast": "http://blast.hypersync.xyz", 39 | "boba": "http://boba.hypersync.xyz", 40 | "sonic": "http://sonic.hypersync.xyz", 41 | "citrea-testnet": "http://citrea-testnet.hypersync.xyz", 42 | "rootstock": "http://rootstock.hypersync.xyz", 43 | "bsc-testnet": "http://bsc-testnet.hypersync.xyz", 44 | "optimism-sepolia": "http://optimism-sepolia.hypersync.xyz", 45 | "manta": "http://manta.hypersync.xyz", 46 | "merlin": "http://merlin.hypersync.xyz", 47 | "kroma": "http://kroma.hypersync.xyz", 48 | "bsc": "http://bsc.hypersync.xyz", 49 | "zksync": "http://zksync.hypersync.xyz", 50 | "polygon": "http://polygon.hypersync.xyz", 51 | "mode": "http://mode.hypersync.xyz", 52 | "monad-testnet": "http://monad-testnet.hypersync.xyz", 53 | "flare": "http://flare.hypersync.xyz", 54 | "avalanche": "http://avalanche.hypersync.xyz", 55 | "sepolia": "http://sepolia.hypersync.xyz", 56 | "moonbase-alpha": "http://moonbase-alpha.hypersync.xyz", 57 | "sophon-testnet": "http://sophon-testnet.hypersync.xyz", 58 | "lisk": "http://lisk.hypersync.xyz", 59 | "gnosis-chiado": "http://gnosis-chiado.hypersync.xyz", 60 | "eth-traces": "http://eth-traces.hypersync.xyz", 61 | "opbnb": "http://opbnb.hypersync.xyz", 62 | "zeta": "http://zeta.hypersync.xyz", 63 | "berachain-bartio": "http://berachain-bartio.hypersync.xyz", 64 | "zora": "http://zora.hypersync.xyz", 65 | "fuji": "http://fuji.hypersync.xyz", 66 | "mev-commit": "http://mev-commit.hypersync.xyz", 67 | "lukso": "http://lukso.hypersync.xyz", 68 | "holesky": "http://holesky.hypersync.xyz", 69 | "galadriel-devnet": "http://galadriel-devnet.hypersync.xyz", 70 | "linea": "http://linea.hypersync.xyz", 71 | "zircuit": "http://zircuit.hypersync.xyz", 72 | "gnosis": "http://gnosis.hypersync.xyz", 73 | "lukso-testnet": "http://lukso-testnet.hypersync.xyz", 74 | "berachain": "http://berachain.hypersync.xyz", 75 | "sophon": "http://sophon.hypersync.xyz", 76 | "celo": "http://celo.hypersync.xyz", 77 | "superseed": "http://superseed.hypersync.xyz", 78 | "harmony-shard-0": "http://harmony-shard-0.hypersync.xyz" 79 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LogTUI 2 | 3 | A terminal-based UI for monitoring blockchain events using Hypersync. 4 | 5 | ![LogTUI gif](./hypersync.gif) 6 | 7 | ## Quickstart 8 | 9 | Try it with a single command: 10 | 11 | ```bash 12 | # Scan Uniswapv4 events on Unichain (using pnpx) 13 | pnpx logtui uniswap-v4 unichain 14 | 15 | # Monitor Aave events on Arbitrum (using npx) 16 | npx logtui aave arbitrum 17 | 18 | # See all available options 19 | pnpx logtui --help 20 | ``` 21 | 22 | ## Features 23 | 24 | - Real-time monitoring of blockchain events with a beautiful terminal UI 25 | - Supports **all Hypersync-enabled networks** (Ethereum, Arbitrum, Optimism, etc.) 26 | - **Extensive preset collection** covering DeFi, Oracles, NFTs, L2s, and more 27 | - Built-in presets for 20+ protocols (Uniswap, Chainlink, Aave, ENS, etc.) 28 | - Custom event signature support 29 | - Event distribution visualization 30 | - Progress tracking and statistics 31 | - Automatic network discovery from Hypersync API with persistent caching 32 | 33 | ## Installation 34 | 35 | ### Global Installation 36 | 37 | ```bash 38 | npm install -g logtui 39 | # or 40 | yarn global add logtui 41 | # or 42 | pnpm add -g logtui 43 | ``` 44 | 45 | ### Local Installation 46 | 47 | ```bash 48 | npm install logtui 49 | # or 50 | yarn add logtui 51 | # or 52 | pnpm add logtui 53 | ``` 54 | 55 | ## Usage 56 | 57 | ### CLI 58 | 59 | ```bash 60 | # Default: Monitor Uniswap V3 events on Ethereum 61 | logtui 62 | 63 | # Track Uniswap V4 events 64 | logtui uniswap-v4 65 | 66 | # Monitor Chainlink price feed updates 67 | logtui chainlink-price-feeds 68 | 69 | # Track AAVE lending events on Arbitrum 70 | logtui aave arbitrum 71 | 72 | # Watch LayerZero cross-chain messages on Optimism 73 | logtui layerzero optimism 74 | 75 | # Monitor ENS registry events 76 | logtui ens 77 | 78 | # Monitor on a testnet 79 | logtui chainlink-vrf arbitrum-sepolia 80 | 81 | # List all available presets 82 | logtui --list-presets 83 | 84 | # List all available networks 85 | logtui --list-networks 86 | 87 | # Force refresh the network list from Hypersync API (updates cache) 88 | logtui --refresh-networks 89 | 90 | # Custom events 91 | logtui -e "Transfer(address,address,uint256)" "Approval(address,address,uint256)" -n eth 92 | ``` 93 | 94 | ### Network Discovery 95 | 96 | LogTUI automatically discovers and caches all networks supported by Hypersync: 97 | 98 | 1. On first run, it loads the default networks 99 | 2. It then attempts to fetch all available networks from the Hypersync API 100 | 3. Networks are cached locally for future use, even when offline 101 | 4. Use `--refresh-networks` to force update the cached network list 102 | 103 | This ensures you always have access to all supported networks, even when working offline. 104 | 105 | ### CLI Options 106 | 107 | ``` 108 | Usage: logtui [options] [preset] [network] 109 | 110 | Arguments: 111 | preset Event preset to use (e.g., uniswap-v3, erc20, erc721) (default: "uniswap-v3") 112 | network Network to connect to (e.g., eth, arbitrum, optimism) (default: "eth") 113 | 114 | Options: 115 | -V, --version output the version number 116 | -e, --events Custom event signatures to monitor 117 | -n, --network Network to connect to 118 | -t, --title Custom title for the scanner (default: "Blockchain Event Scanner") 119 | -l, --list-presets List available event presets and exit 120 | -N, --list-networks List all available networks and exit 121 | --refresh-networks Force refresh network list from API 122 | -v, --verbose Show additional info in the console 123 | -h, --help display help for command 124 | ``` 125 | 126 | ### Programmatic Usage 127 | 128 | You can also use LogTUI as a library in your Node.js applications: 129 | 130 | ```javascript 131 | import { 132 | createScanner, 133 | getNetworkUrl, 134 | getEventSignatures, 135 | fetchNetworks, 136 | } from "logtui"; 137 | 138 | // Refresh the network list (optional, will use cache by default) 139 | // Pass true to force refresh from API: fetchNetworks(true) 140 | await fetchNetworks(); 141 | 142 | // Option 1: Using direct parameters 143 | createScanner({ 144 | networkUrl: "http://eth.hypersync.xyz", 145 | eventSignatures: [ 146 | "Transfer(address,address,uint256)", 147 | "Approval(address,address,uint256)", 148 | ], 149 | title: "My Custom Scanner", 150 | }); 151 | 152 | // Option 2: Using helper functions 153 | const networkUrl = getNetworkUrl("arbitrum"); 154 | const eventSignatures = getEventSignatures("uniswap-v3"); 155 | 156 | createScanner({ 157 | networkUrl, 158 | eventSignatures, 159 | title: "Uniswap V3 Scanner", 160 | }); 161 | ``` 162 | 163 | ## Supported Networks 164 | 165 | LogTUI automatically discovers all networks supported by Hypersync. The following are some commonly used networks: 166 | 167 | ### Mainnets 168 | 169 | - `eth`: Ethereum Mainnet 170 | - `arbitrum`: Arbitrum One 171 | - `optimism`: Optimism 172 | - `base`: Base 173 | - `polygon`: Polygon PoS 174 | - And many more... 175 | 176 | ### Testnets 177 | 178 | - `arbitrum-sepolia`: Arbitrum Sepolia 179 | - `optimism-sepolia`: Optimism Sepolia 180 | - And more... 181 | 182 | Run `logtui --list-networks` to see the complete, up-to-date list of all supported networks. 183 | 184 | ## Built-in Event Presets 185 | 186 | ### Core Presets 187 | 188 | - `uniswap-v3`: Core Uniswap V3 events (PoolCreated, Swap, Mint, Burn, Initialize) 189 | - `uniswap-v4`: Uniswap V4 PoolManager events (Swap, ModifyLiquidity, Initialize, Donate, and more) 190 | - `erc20`: Standard ERC-20 token events (Transfer, Approval) 191 | - `erc721`: Standard ERC-721 NFT events (Transfer, Approval, ApprovalForAll) 192 | 193 | ### Oracles 194 | 195 | - `chainlink-price-feeds`: Chainlink price oracle events (AnswerUpdated, NewRound) 196 | - `chainlink-vrf`: Chainlink Verifiable Random Function events 197 | - `pyth`: Pyth Network oracle events 198 | - `uma`: UMA Oracle events (PriceProposed, PriceDisputed, PriceSettled) 199 | 200 | ### DeFi Protocols 201 | 202 | - `aave`: Aave V3 lending protocol events (Supply, Withdraw, Borrow, Repay) 203 | - `curve`: Curve Finance pool events (TokenExchange, AddLiquidity) 204 | - `weth`: Wrapped Ether events (Deposit, Withdrawal, Transfer) 205 | - `usdc`: USD Coin stablecoin events 206 | 207 | ### Cross-chain & L2 208 | 209 | - `layerzero`: LayerZero cross-chain messaging events 210 | - `arbitrum`: Arbitrum sequencer and bridge events 211 | 212 | ### Gaming & NFTs 213 | 214 | - `blur`: Blur NFT marketplace events 215 | - `axie`: Axie Infinity game events 216 | - `ens`: Ethereum Name Service registry events 217 | 218 | ### Emerging Tech 219 | 220 | - `erc4337`: Account Abstraction (ERC-4337) events 221 | - `universalRouter`: Uniswap's intent-based Universal Router events 222 | 223 | ## Development 224 | 225 | ```bash 226 | # Clone the repository 227 | git clone https://github.com/yourusername/logtui.git 228 | cd logtui 229 | 230 | # Install dependencies 231 | npm install 232 | 233 | # Run the development version 234 | node bin/logtui.js 235 | ``` 236 | 237 | ## Acknowledgements 238 | 239 | - Built with [Hypersync](https://docs.envio.dev/docs/HyperIndex/overview) by Envio 240 | - Terminal UI powered by [blessed](https://github.com/chjj/blessed) 241 | -------------------------------------------------------------------------------- /bin/logtui.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * LogTUI - Command Line Interface 5 | * 6 | * A terminal-based UI for monitoring blockchain events using Hypersync 7 | */ 8 | 9 | // Force terminal compatibility mode 10 | process.env.FORCE_COLOR = "3"; 11 | process.env.NCURSES_NO_UTF8_ACS = "1"; 12 | 13 | // Handle terminal capability errors before imports 14 | const originalError = console.error; 15 | console.error = function (msg) { 16 | // Ignore specific terminal capability errors 17 | if ( 18 | typeof msg === "string" && 19 | (msg.includes("Error on xterm") || msg.includes("Setulc")) 20 | ) { 21 | return; 22 | } 23 | originalError.apply(console, arguments); 24 | }; 25 | 26 | // Disable debug logging 27 | console.debug = () => {}; 28 | 29 | import { Command } from "commander"; 30 | import chalk from "chalk"; 31 | import { createScanner } from "../lib/scanner.js"; 32 | import { 33 | getNetworkUrl, 34 | getEventSignatures, 35 | hasPreset, 36 | NETWORKS, 37 | DEFAULT_NETWORKS, 38 | fetchNetworks, 39 | listPresets, 40 | } from "../lib/config.js"; 41 | 42 | // Create a new command instance 43 | const program = new Command(); 44 | 45 | // Setup program metadata 46 | program 47 | .name("logtui") 48 | .description("A terminal UI for monitoring blockchain events with Hypersync") 49 | .version("0.1.0"); 50 | 51 | // Main command 52 | program 53 | .argument( 54 | "[preset]", 55 | "Event preset to use (e.g., uniswap-v3, erc20, erc721)", 56 | "uniswap-v3" 57 | ) 58 | .argument( 59 | "[network]", 60 | "Network to connect to (e.g., eth, arbitrum, optimism)", 61 | "eth" 62 | ) 63 | .option("-e, --events <events...>", "Custom event signatures to monitor") 64 | .option("-n, --network <network>", "Network to connect to") 65 | .option( 66 | "-t, --title <title>", 67 | "Custom title for the scanner", 68 | "Blockchain Event Scanner" 69 | ) 70 | .option("-l, --list-presets", "List available event presets and exit") 71 | .option("-N, --list-networks", "List all available networks and exit") 72 | .option("-v, --verbose", "Show additional info in the console") 73 | .option("--refresh-networks", "Force refresh network list from API") 74 | .action(async (presetArg, networkArg, options) => { 75 | try { 76 | // Always fetch networks at startup to ensure we have the latest 77 | // This uses the cache by default unless --refresh-networks is specified 78 | if (options.refreshNetworks) { 79 | console.log(chalk.blue("Refreshing networks from API...")); 80 | await fetchNetworks(true); 81 | console.log(chalk.green("Networks refreshed successfully!")); 82 | } else { 83 | // Silently ensure networks are loaded (uses cache if available) 84 | await fetchNetworks(); 85 | } 86 | 87 | // If the user requested to list networks, show them and exit 88 | if (options.listNetworks) { 89 | console.log(chalk.bold.blue("\nAvailable Networks:")); 90 | console.log(chalk.blue("──────────────────────────────────────────")); 91 | 92 | // Separate into categories for better display 93 | const mainnetNetworks = []; 94 | const testnetNetworks = []; 95 | const otherNetworks = []; 96 | 97 | Object.entries(NETWORKS).forEach(([name, url]) => { 98 | // Categorize networks by name patterns 99 | if ( 100 | name.includes("sepolia") || 101 | name.includes("goerli") || 102 | name.includes("testnet") || 103 | name.includes("test") 104 | ) { 105 | testnetNetworks.push({ name, url }); 106 | } else if (Object.keys(DEFAULT_NETWORKS).includes(name)) { 107 | mainnetNetworks.push({ name, url }); 108 | } else { 109 | otherNetworks.push({ name, url }); 110 | } 111 | }); 112 | 113 | console.log(chalk.yellow("\nPopular Mainnets:")); 114 | mainnetNetworks.forEach(({ name, url }) => { 115 | console.log(`${chalk.green(name)}: ${url}`); 116 | }); 117 | 118 | console.log(chalk.yellow("\nTestnets:")); 119 | testnetNetworks.forEach(({ name, url }) => { 120 | console.log(`${chalk.green(name)}: ${url}`); 121 | }); 122 | 123 | console.log(chalk.yellow("\nOther Networks:")); 124 | otherNetworks.forEach(({ name, url }) => { 125 | console.log(`${chalk.green(name)}: ${url}`); 126 | }); 127 | 128 | console.log( 129 | chalk.yellow( 130 | `\nTotal ${Object.keys(NETWORKS).length} networks available` 131 | ) 132 | ); 133 | 134 | console.log(chalk.blue("\nUsage Examples:")); 135 | console.log( 136 | `${chalk.yellow("logtui uniswap-v3 arbitrum")} - Use Arbitrum network` 137 | ); 138 | console.log( 139 | `${chalk.yellow( 140 | "logtui -n optimism-sepolia" 141 | )} - Use Optimism Sepolia testnet` 142 | ); 143 | console.log(); 144 | process.exit(0); 145 | } 146 | 147 | // If the user requested to list presets, show them and exit 148 | if (options.listPresets) { 149 | console.log(chalk.bold.blue("\nAvailable Event Presets:")); 150 | console.log(chalk.blue("──────────────────────────────────────────")); 151 | 152 | listPresets().forEach((preset) => { 153 | console.log( 154 | `${chalk.green(preset.id)}: ${chalk.yellow(preset.name)} - ${ 155 | preset.description 156 | }` 157 | ); 158 | }); 159 | 160 | console.log(chalk.blue("\nAvailable Networks:")); 161 | console.log(chalk.blue("──────────────────────────────────────────")); 162 | // Show just the default networks for simplicity 163 | Object.keys(DEFAULT_NETWORKS).forEach((network) => { 164 | console.log(`${chalk.green(network)}: ${DEFAULT_NETWORKS[network]}`); 165 | }); 166 | console.log( 167 | chalk.yellow( 168 | `(${ 169 | Object.keys(NETWORKS).length - 170 | Object.keys(DEFAULT_NETWORKS).length 171 | } more networks available. Run with --list-networks to see all)` 172 | ) 173 | ); 174 | 175 | console.log(chalk.blue("\nUsage Examples:")); 176 | console.log(chalk.blue("──────────────────────────────────────────")); 177 | console.log( 178 | `${chalk.yellow("logtui")} - Scan for Uniswap V3 events on Ethereum` 179 | ); 180 | console.log( 181 | `${chalk.yellow( 182 | "logtui uniswap-v3 arbitrum" 183 | )} - Scan for Uniswap V3 events on Arbitrum` 184 | ); 185 | console.log( 186 | `${chalk.yellow( 187 | "logtui erc20 optimism" 188 | )} - Scan for ERC-20 events on Optimism` 189 | ); 190 | console.log( 191 | `${chalk.yellow( 192 | 'logtui -e "Transfer(address,address,uint256)" -n eth' 193 | )} - Scan for custom events` 194 | ); 195 | console.log(); 196 | process.exit(0); 197 | } 198 | 199 | // Determine the network to use 200 | const network = options.network || networkArg || "eth"; 201 | let networkUrl; 202 | 203 | try { 204 | networkUrl = getNetworkUrl(network); 205 | } catch (err) { 206 | console.error(chalk.red(`Error: ${err.message}`)); 207 | console.log( 208 | chalk.yellow( 209 | "Run 'logtui --list-networks' to see all available networks." 210 | ) 211 | ); 212 | process.exit(1); 213 | } 214 | 215 | // Determine the event signatures to use 216 | let eventSignatures = []; 217 | 218 | if (options.events && options.events.length > 0) { 219 | // Use custom event signatures 220 | eventSignatures = options.events; 221 | if (options.verbose) { 222 | console.log(chalk.blue("Using custom event signatures:")); 223 | eventSignatures.forEach((sig) => console.log(`- ${sig}`)); 224 | } 225 | } else { 226 | // Use preset 227 | const preset = presetArg || "uniswap-v3"; 228 | 229 | if (!hasPreset(preset)) { 230 | console.error(chalk.red(`Error: Preset '${preset}' not found.`)); 231 | console.log( 232 | chalk.yellow( 233 | `Run 'logtui --list-presets' to see available presets.` 234 | ) 235 | ); 236 | process.exit(1); 237 | } 238 | 239 | eventSignatures = getEventSignatures(preset); 240 | if (options.verbose) { 241 | console.log( 242 | chalk.blue( 243 | `Using '${preset}' preset with ${eventSignatures.length} event signatures` 244 | ) 245 | ); 246 | } 247 | } 248 | 249 | // Set the title 250 | const title = `${options.title} (${network})`; 251 | 252 | if (options.verbose) { 253 | console.log( 254 | chalk.blue(`Starting scanner on ${network}: ${networkUrl}`) 255 | ); 256 | console.log( 257 | chalk.blue(`Monitoring ${eventSignatures.length} event types`) 258 | ); 259 | } 260 | 261 | // Start the scanner 262 | await createScanner({ 263 | networkUrl, 264 | eventSignatures, 265 | title, 266 | }); 267 | } catch (err) { 268 | console.error(chalk.red(`Error: ${err.message}`)); 269 | if (err.stack) { 270 | console.error(chalk.red(err.stack)); 271 | } 272 | process.exit(1); 273 | } 274 | }); 275 | 276 | // Execute the CLI 277 | async function main() { 278 | try { 279 | // Ensure networks are loaded before parsing arguments 280 | await fetchNetworks(); 281 | program.parse(process.argv); 282 | } catch (err) { 283 | console.error(chalk.red(`Fatal error: ${err.message}`)); 284 | process.exit(1); 285 | } 286 | } 287 | 288 | main(); 289 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration module for logtui 3 | * Provides network endpoints and event signature presets 4 | */ 5 | import fetch from "node-fetch"; 6 | import fs from "fs"; 7 | import path from "path"; 8 | import { fileURLToPath } from "url"; 9 | 10 | // Get the directory of the current module 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | // Path to store cached networks 15 | const CACHE_FILE = path.join(__dirname, "../.networks-cache.json"); 16 | 17 | // Default network endpoint configuration (used as fallback if API fetch fails) 18 | export const DEFAULT_NETWORKS = { 19 | eth: "http://eth.hypersync.xyz", 20 | arbitrum: "http://arbitrum.hypersync.xyz", 21 | optimism: "http://optimism.hypersync.xyz", 22 | base: "http://base.hypersync.xyz", 23 | polygon: "http://polygon.hypersync.xyz", 24 | }; 25 | 26 | // Runtime networks object that will be populated 27 | export let NETWORKS = { ...DEFAULT_NETWORKS }; 28 | 29 | // API endpoint to fetch available networks 30 | const NETWORKS_API_URL = "https://chains.hyperquery.xyz/active_chains"; 31 | 32 | /** 33 | * Load networks from cache file 34 | * @returns {Object} Cached networks or default networks if cache not found 35 | */ 36 | function loadNetworksFromCache() { 37 | try { 38 | if (fs.existsSync(CACHE_FILE)) { 39 | const data = fs.readFileSync(CACHE_FILE, "utf8"); 40 | const networks = JSON.parse(data); 41 | console.debug("Loaded networks from cache"); 42 | return networks; 43 | } 44 | } catch (error) { 45 | console.warn(`Failed to load networks from cache: ${error.message}`); 46 | } 47 | return DEFAULT_NETWORKS; 48 | } 49 | 50 | /** 51 | * Save networks to cache file 52 | * @param {Object} networks - Networks to cache 53 | */ 54 | function saveNetworksToCache(networks) { 55 | try { 56 | const data = JSON.stringify(networks, null, 2); 57 | fs.writeFileSync(CACHE_FILE, data); 58 | console.debug("Saved networks to cache"); 59 | } catch (error) { 60 | console.warn(`Failed to save networks to cache: ${error.message}`); 61 | } 62 | } 63 | 64 | /** 65 | * Fetches all available networks from the Hypersync API 66 | * @param {boolean} forceRefresh - Whether to force a refresh from API 67 | * @returns {Promise<Object>} Object with network names as keys and URLs as values 68 | */ 69 | export async function fetchNetworks(forceRefresh = false) { 70 | // If not forcing refresh, try to load from cache first 71 | if (!forceRefresh) { 72 | const cachedNetworks = loadNetworksFromCache(); 73 | if ( 74 | Object.keys(cachedNetworks).length > Object.keys(DEFAULT_NETWORKS).length 75 | ) { 76 | // If cache has more networks than default, use it 77 | NETWORKS = { ...cachedNetworks }; 78 | return cachedNetworks; 79 | } 80 | } 81 | 82 | try { 83 | console.debug("Fetching networks from API..."); 84 | const response = await fetch(NETWORKS_API_URL); 85 | if (!response.ok) { 86 | throw new Error(`API responded with status: ${response.status}`); 87 | } 88 | 89 | const networks = await response.json(); 90 | const result = {}; 91 | 92 | // Process the API response into our format 93 | networks.forEach((network) => { 94 | // Skip non-EVM networks for now 95 | if (network.ecosystem !== "evm") return; 96 | 97 | // Convert network name to URL format 98 | const url = `http://${network.name}.hypersync.xyz`; 99 | result[network.name] = url; 100 | }); 101 | 102 | // Update the NETWORKS object 103 | NETWORKS = { ...result }; 104 | 105 | // Save to cache 106 | saveNetworksToCache(result); 107 | 108 | return result; 109 | } catch (error) { 110 | console.warn(`Warning: Failed to fetch networks: ${error.message}`); 111 | console.warn("Using previously cached or default networks instead."); 112 | 113 | // Fall back to cached networks if available, or defaults 114 | const cachedNetworks = loadNetworksFromCache(); 115 | NETWORKS = { ...cachedNetworks }; 116 | return cachedNetworks; 117 | } 118 | } 119 | 120 | // Try to load networks from cache immediately 121 | try { 122 | const cachedNetworks = loadNetworksFromCache(); 123 | if (Object.keys(cachedNetworks).length > 0) { 124 | NETWORKS = { ...cachedNetworks }; 125 | } 126 | } catch (err) { 127 | console.warn(`Failed to load networks from cache: ${err.message}`); 128 | } 129 | 130 | // Event signature presets 131 | export const EVENT_PRESETS = { 132 | "uniswap-v3": { 133 | name: "Uniswap V3", 134 | description: "Uniswap V3 core events", 135 | signatures: [ 136 | "PoolCreated(address,address,uint24,int24,address)", 137 | "Burn(address,int24,int24,uint128,uint256,uint256)", 138 | "Initialize(uint160,int24)", 139 | "Mint(address,address,int24,int24,uint128,uint256,uint256)", 140 | "Swap(address,address,int256,int256,uint160,uint128,int24)", 141 | ], 142 | }, 143 | "uniswap-v4": { 144 | name: "Uniswap V4", 145 | description: "Uniswap V4 PoolManager events", 146 | signatures: [ 147 | "Donate(bytes32,address,uint256,uint256)", 148 | "Initialize(bytes32,address,address,uint24,int24,address,uint160,int24)", 149 | "ModifyLiquidity(bytes32,address,int24,int24,int256,bytes32)", 150 | "Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)", 151 | "Transfer(address,address,address,uint256,uint256)", 152 | ], 153 | }, 154 | erc20: { 155 | name: "ERC-20", 156 | description: "Standard ERC-20 token events", 157 | signatures: [ 158 | "Transfer(address,address,uint256)", 159 | "Approval(address,address,uint256)", 160 | ], 161 | }, 162 | erc721: { 163 | name: "ERC-721", 164 | description: "Standard ERC-721 NFT events", 165 | signatures: [ 166 | "Transfer(address,address,uint256)", 167 | "Approval(address,address,uint256)", 168 | "ApprovalForAll(address,address,bool)", 169 | ], 170 | }, 171 | // Oracle Presets 172 | "chainlink-price-feeds": { 173 | name: "Chainlink Price Feeds", 174 | description: "Chainlink price oracle events", 175 | signatures: [ 176 | "AnswerUpdated(int256,uint256,uint256)", 177 | "NewRound(uint256,address,uint256)", 178 | "ResponseReceived(int256,uint256,address)", 179 | "AggregatorConfigSet(address,address,address)", 180 | "RoundDetailsUpdated(uint128,uint32,int192,uint32)", 181 | ], 182 | }, 183 | "chainlink-vrf": { 184 | name: "Chainlink VRF", 185 | description: "Chainlink Verifiable Random Function events", 186 | signatures: [ 187 | "RandomWordsRequested(bytes32,uint256,uint256,uint64,uint16,uint32,uint32,address)", 188 | "RandomWordsFulfilled(uint256,uint256,uint96,bool)", 189 | "ConfigSet(uint16,uint32,uint32,uint32,uint32)", 190 | "SubscriptionCreated(uint64,address)", 191 | "SubscriptionFunded(uint64,uint256,uint256)", 192 | ], 193 | }, 194 | pyth: { 195 | name: "Pyth Network", 196 | description: "Pyth Network oracle events", 197 | signatures: [ 198 | "BatchPriceFeedUpdate(bytes32[],bytes[])", 199 | "PriceFeedUpdate(bytes32,bytes)", 200 | "BatchPriceUpdate(bytes32[],int64[],uint64[],int32[],uint32[])", 201 | "PriceUpdate(bytes32,int64,uint64,int32,uint32)", 202 | ], 203 | }, 204 | uma: { 205 | name: "UMA Protocol", 206 | description: "UMA Oracle events", 207 | signatures: [ 208 | "PriceProposed(address,uint256,uint256,int256)", 209 | "PriceDisputed(address,uint256,uint256,int256)", 210 | "PriceSettled(address,uint256,int256,uint256)", 211 | ], 212 | }, 213 | // DeFi Presets 214 | aave: { 215 | name: "Aave V3", 216 | description: "Aave V3 lending protocol events", 217 | signatures: [ 218 | "Supply(address,address,address,uint256,uint16)", 219 | "Withdraw(address,address,address,uint256)", 220 | "Borrow(address,address,address,uint256,uint256,uint16)", 221 | "Repay(address,address,address,uint256,bool)", 222 | "LiquidationCall(address,address,address,uint256,uint256,address,bool)", 223 | ], 224 | }, 225 | curve: { 226 | name: "Curve Finance", 227 | description: "Curve pool events", 228 | signatures: [ 229 | "TokenExchange(address,int128,uint256,int128,uint256)", 230 | "AddLiquidity(address,uint256[],uint256,uint256)", 231 | "RemoveLiquidity(address,uint256,uint256[])", 232 | "RemoveLiquidityOne(address,uint256,uint256)", 233 | ], 234 | }, 235 | // Cross-chain & Bridges 236 | layerzero: { 237 | name: "LayerZero", 238 | description: "LayerZero cross-chain messaging events", 239 | signatures: [ 240 | "MessageSent(bytes,uint64,bytes32,bytes)", 241 | "PacketReceived(uint16,bytes,address,uint64,bytes32)", 242 | "RelayerAdded(address,uint16)", 243 | "RelayerRemoved(address,uint16)", 244 | ], 245 | }, 246 | weth: { 247 | name: "WETH", 248 | description: "Wrapped Ether events", 249 | signatures: [ 250 | "Deposit(address,uint256)", 251 | "Withdrawal(address,uint256)", 252 | "Transfer(address,address,uint256)", 253 | "Approval(address,address,uint256)", 254 | ], 255 | }, 256 | // L2 Infrastructure 257 | arbitrum: { 258 | name: "Arbitrum", 259 | description: "Arbitrum sequencer and bridge events", 260 | signatures: [ 261 | "SequencerBatchDelivered(uint256,bytes32,address,uint256)", 262 | "MessageDelivered(uint256,bytes32,address,uint8,address,bytes32)", 263 | "BridgeCallTriggered(address,address,uint256,bytes)", 264 | ], 265 | }, 266 | // Gaming/NFTs 267 | blur: { 268 | name: "Blur", 269 | description: "Blur NFT marketplace events", 270 | signatures: [ 271 | "OrdersMatched(address,address,bytes32,bytes32)", 272 | "NonceIncremented(address,uint256)", 273 | "NewBlurExchange(address)", 274 | "NewExecutionDelegate(address)", 275 | ], 276 | }, 277 | axie: { 278 | name: "Axie Infinity", 279 | description: "Axie Infinity game events", 280 | signatures: [ 281 | "AxieSpawned(uint256,uint256,uint256,uint256,uint256)", 282 | "AxieBred(address,uint256,uint256,uint256)", 283 | "Transfer(address,address,uint256)", 284 | "BreedingApproval(address,uint256,address)", 285 | ], 286 | }, 287 | // Stablecoins 288 | usdc: { 289 | name: "USDC", 290 | description: "USD Coin stablecoin events", 291 | signatures: [ 292 | "Transfer(address,address,uint256)", 293 | "Approval(address,address,uint256)", 294 | "Mint(address,uint256)", 295 | "Burn(address,uint256)", 296 | "BlacklistAdded(address)", 297 | "BlacklistRemoved(address)", 298 | ], 299 | }, 300 | // DAOs/Governance 301 | ens: { 302 | name: "ENS", 303 | description: "Ethereum Name Service registry events", 304 | signatures: [ 305 | "NewOwner(bytes32,bytes32,address)", 306 | "Transfer(bytes32,address)", 307 | "NewResolver(bytes32,address)", 308 | "NewTTL(bytes32,uint64)", 309 | "NameRegistered(string,bytes32,address,uint256,uint256)", 310 | ], 311 | }, 312 | // Emerging/Trending 313 | erc4337: { 314 | name: "ERC-4337", 315 | description: "Account Abstraction events", 316 | signatures: [ 317 | "UserOperationEvent(bytes32,address,address,uint256,bool,uint256,uint256)", 318 | "AccountDeployed(bytes32,address,address,address)", 319 | ], 320 | }, 321 | universalRouter: { 322 | name: "Uniswap Universal Router", 323 | description: "Uniswap's intent-based router events", 324 | signatures: [ 325 | "RewardsSent(address,uint256)", 326 | "ERC20Transferred(address,address,uint256)", 327 | "ERC721Transferred(address,address,address,uint256)", 328 | "ERC1155Transferred(address,address,address,uint256,uint256)", 329 | ], 330 | }, 331 | }; 332 | 333 | /** 334 | * Get network URL from network name 335 | * @param {string} network - Network name 336 | * @returns {string} Network URL 337 | */ 338 | export function getNetworkUrl(network) { 339 | if (!NETWORKS[network]) { 340 | throw new Error( 341 | `Network '${network}' not supported. Available networks: ${Object.keys( 342 | NETWORKS 343 | ) 344 | .slice(0, 10) 345 | .join(", ")}... (Use --list-networks to see all)` 346 | ); 347 | } 348 | return NETWORKS[network]; 349 | } 350 | 351 | /** 352 | * Get event signatures from preset name 353 | * @param {string} presetName - Preset name 354 | * @returns {Array<string>} Array of event signatures 355 | */ 356 | export function getEventSignatures(presetName) { 357 | if (!EVENT_PRESETS[presetName]) { 358 | throw new Error( 359 | `Preset '${presetName}' not found. Available presets: ${Object.keys( 360 | EVENT_PRESETS 361 | ).join(", ")}` 362 | ); 363 | } 364 | return EVENT_PRESETS[presetName].signatures; 365 | } 366 | 367 | /** 368 | * Check if a preset exists 369 | * @param {string} presetName - Preset name 370 | * @returns {boolean} Whether the preset exists 371 | */ 372 | export function hasPreset(presetName) { 373 | return Boolean(EVENT_PRESETS[presetName]); 374 | } 375 | 376 | /** 377 | * List all available presets 378 | * @returns {Object} All presets with name and description 379 | */ 380 | export function listPresets() { 381 | return Object.entries(EVENT_PRESETS).map(([id, preset]) => ({ 382 | id, 383 | name: preset.name, 384 | description: preset.description, 385 | })); 386 | } 387 | 388 | /** 389 | * Force refresh networks from API 390 | * @returns {Promise<Object>} Updated networks list 391 | */ 392 | export async function refreshNetworks() { 393 | return await fetchNetworks(true); 394 | } 395 | -------------------------------------------------------------------------------- /lib/scanner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Core scanner module for logtui 3 | * Handles connecting to Hypersync and displaying the TUI 4 | */ 5 | import { keccak256, toHex } from "viem"; 6 | import { 7 | HypersyncClient, 8 | Decoder, 9 | LogField, 10 | JoinMode, 11 | BlockField, 12 | TransactionField, 13 | } from "@envio-dev/hypersync-client"; 14 | import blessed from "blessed"; 15 | import contrib from "blessed-contrib"; 16 | import chalk from "chalk"; 17 | import figlet from "figlet"; 18 | import supportsColor from "supports-color"; 19 | 20 | // Force terminal compatibility mode - stronger settings 21 | // process.env.FORCE_COLOR = "3"; // Force full true color support - REMOVING THIS 22 | process.env.NCURSES_NO_UTF8_ACS = "1"; 23 | process.env.TERM = "xterm-256color"; // Use more compatible terminal type 24 | 25 | // Ensure chalk uses normal level for auto-detection 26 | // chalk.level = 3; // REMOVING FORCED LEVEL 27 | 28 | // Detect actual terminal color support 29 | const hasColorSupport = !!supportsColor.stdout; 30 | const has256ColorSupport = !!( 31 | supportsColor.stdout && supportsColor.stdout.has256 32 | ); 33 | const hasTrueColorSupport = !!( 34 | supportsColor.stdout && supportsColor.stdout.has16m 35 | ); 36 | 37 | // Log color support detection for verbose mode 38 | if (process.env.DEBUG) { 39 | console.log(`Terminal color support detected: 40 | - Basic colors: ${hasColorSupport} 41 | - 256 colors: ${has256ColorSupport} 42 | - True colors: ${hasTrueColorSupport} 43 | `); 44 | } 45 | 46 | // Apply completely silent error handling for Blessed/Terminal issues 47 | const originalConsoleError = console.error; 48 | console.error = function (...args) { 49 | // Check if this is a terminal capability error 50 | if (args.length > 0 && typeof args[0] === "string") { 51 | const errorMsg = args[0]; 52 | if ( 53 | errorMsg.includes("Error on xterm") || 54 | errorMsg.includes("Setulc") || 55 | errorMsg.includes("stack") || 56 | errorMsg.includes("term") || 57 | errorMsg.includes("escape sequence") 58 | ) { 59 | return; // Silently ignore these errors 60 | } 61 | } 62 | originalConsoleError.apply(console, args); 63 | }; 64 | 65 | // Apply monkey patch to process.stderr.write to catch any remaining errors 66 | const originalStderrWrite = process.stderr.write; 67 | process.stderr.write = function (buffer, encoding, fd) { 68 | const str = buffer.toString(); 69 | if ( 70 | str.includes("Error on xterm") || 71 | str.includes("Setulc") || 72 | str.includes("stack") || 73 | str.includes("var v") || 74 | str.includes("terminal capability") || 75 | str.includes("xterm-256color") || 76 | str.toLowerCase().includes("setulc") 77 | ) { 78 | return true; // Pretend we wrote it but don't actually write 79 | } 80 | return originalStderrWrite.apply(process.stderr, arguments); 81 | }; 82 | 83 | /** 84 | * Format numbers with commas 85 | * @param {number|string} num - Number to format 86 | * @returns {string} Formatted number 87 | */ 88 | const formatNumber = (num) => { 89 | if (num === null || num === undefined) return "0"; 90 | return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 91 | }; 92 | 93 | /** 94 | * Safe JSON stringify that handles circular references 95 | * @param {Object} obj - Object to stringify 96 | * @param {number} maxLength - Maximum length before truncating 97 | * @returns {string} Stringified object 98 | */ 99 | const safeStringify = (obj, maxLength = 100) => { 100 | try { 101 | if (!obj) return "null"; 102 | const str = JSON.stringify(obj); 103 | if (str.length <= maxLength) return str; 104 | return str.substring(0, maxLength) + "..."; 105 | } catch (err) { 106 | return `[Object: stringify failed]`; 107 | } 108 | }; 109 | 110 | /** 111 | * Create and run the scanner with TUI 112 | * @param {Object} options - Scanner options 113 | * @param {string} options.networkUrl - Hypersync network URL 114 | * @param {Array<string>} options.eventSignatures - Event signatures to scan for 115 | * @param {string} options.title - Title for the TUI 116 | * @returns {Promise<void>} 117 | */ 118 | export async function createScanner({ 119 | networkUrl, 120 | eventSignatures, 121 | title = "Event Scanner", 122 | }) { 123 | // Initialize Hypersync client 124 | const client = HypersyncClient.new({ 125 | url: networkUrl, 126 | bearerToken: 127 | process.env.HYPERSYNC_BEARER_TOKEN || 128 | "74e5e8e9-5bb7-43fc-856e-4a2dbb9fc237", // Default token if not provided 129 | }); 130 | 131 | // Create topic0 hashes from event signatures 132 | const topic0_list = eventSignatures.map((sig) => keccak256(toHex(sig))); 133 | 134 | // Define the Hypersync query to get events we're interested in 135 | let query = { 136 | fromBlock: 0, 137 | logs: [ 138 | { 139 | // Get all events that have any of the topic0 values we want 140 | topics: [topic0_list], 141 | }, 142 | ], 143 | fieldSelection: { 144 | log: [LogField.Topic0], 145 | }, 146 | joinMode: JoinMode.JoinTransactions, 147 | }; 148 | 149 | // Track event counts - will be populated dynamically 150 | const eventCounts = { 151 | Total: 0, 152 | Unknown: 0, 153 | }; 154 | 155 | // Create a mapping of topic0 hash to event name 156 | const topic0ToName = {}; 157 | 158 | // Initialize event counts for each signature 159 | eventSignatures.forEach((sig) => { 160 | const name = sig.split("(")[0]; 161 | const topic0 = keccak256(toHex(sig)); 162 | topic0ToName[topic0] = name; 163 | eventCounts[name] = 0; 164 | }); 165 | 166 | //============================================================================= 167 | // TUI SETUP 168 | //============================================================================= 169 | 170 | // Create blessed screen with improved compatibility settings 171 | const screen = blessed.screen({ 172 | smartCSR: true, 173 | title, 174 | dockBorders: true, 175 | fullUnicode: true, 176 | forceUnicode: true, 177 | autoPadding: true, 178 | terminal: "xterm-color", // Use simpler terminal type 179 | fastCSR: true, 180 | useBCE: true, // Use Background Color Erase for better rendering 181 | }); 182 | 183 | // Define UI color scheme with multiple fallback options tailored to detected capabilities 184 | // A selection of beautiful baby blue options 185 | const colorOptions = { 186 | trueColor: { 187 | primary: "#FFFFFF", // Pure white for logo 188 | secondary: "#00BFFF", // Keep baby blue for borders and other elements 189 | tertiary: "#87CEFA", // Light sky blue as another option 190 | }, 191 | ansi256: { 192 | // These are the closest ANSI 256 color codes 193 | primary: 15, // White 194 | secondary: 39, // Closest to deep sky blue 195 | tertiary: 45, // A lighter baby blue 196 | }, 197 | basic: { 198 | // When only basic ANSI colors are available 199 | primary: "white", // Simple white 200 | secondary: "cyanBright", // For non-logo elements 201 | fallback: "cyan", 202 | }, 203 | }; 204 | 205 | // Select the best color scheme based on terminal capabilities 206 | const getOptimalColorScheme = () => { 207 | if (hasTrueColorSupport) { 208 | return { 209 | type: "hex", 210 | primary: colorOptions.trueColor.primary, 211 | secondary: colorOptions.trueColor.secondary, 212 | }; 213 | } else if (has256ColorSupport) { 214 | return { 215 | type: "ansi256", 216 | primary: colorOptions.ansi256.primary, 217 | secondary: colorOptions.ansi256.secondary, 218 | }; 219 | } else { 220 | return { 221 | type: "basic", 222 | primary: colorOptions.basic.primary, 223 | secondary: colorOptions.basic.secondary, 224 | }; 225 | } 226 | }; 227 | 228 | // Get the best color scheme for this terminal 229 | const colorScheme = getOptimalColorScheme(); 230 | 231 | // Set the active color for border references - still using baby blue for borders 232 | let uiColor = 233 | colorScheme.type === "hex" 234 | ? colorOptions.trueColor.secondary 235 | : colorOptions.trueColor.secondary; 236 | 237 | // Create enhanced color function with multiple fallbacks based on terminal capabilities 238 | const safeHexColor = (text) => { 239 | try { 240 | if (colorScheme.type === "hex") { 241 | return chalk.hex(colorScheme.primary)(text); 242 | } else if (colorScheme.type === "ansi256") { 243 | return chalk.ansi256(colorScheme.primary)(text); 244 | } else { 245 | // Basic color mode 246 | return chalk[colorScheme.primary](text); 247 | } 248 | } catch (e) { 249 | // Ultimate fallback - use white which should be available everywhere 250 | return chalk.white(text); 251 | } 252 | }; 253 | 254 | // Create specific color function for borders and other non-logo elements 255 | const borderColor = (text) => { 256 | try { 257 | if (colorScheme.type === "hex") { 258 | return chalk.hex(colorScheme.secondary)(text); 259 | } else if (colorScheme.type === "ansi256") { 260 | return chalk.ansi256(colorScheme.secondary)(text); 261 | } else { 262 | return chalk[colorScheme.secondary](text); 263 | } 264 | } catch (e) { 265 | return chalk.cyanBright(text); 266 | } 267 | }; 268 | 269 | // Create a grid layout 270 | const grid = new contrib.grid({ 271 | rows: 12, 272 | cols: 12, 273 | screen: screen, 274 | }); 275 | 276 | // Create ASCII logo box 277 | const logo = grid.set(0, 0, 3, 12, blessed.box, { 278 | tags: true, 279 | align: "center", 280 | valign: "middle", 281 | border: { 282 | type: "line", 283 | fg: uiColor, 284 | }, 285 | }); 286 | 287 | // Define a direct neon cyan color using ANSI escape sequences 288 | // This should be universally visible in all terminals 289 | const NEON_CYAN = "\x1b[38;5;51m"; // Bright neon cyan (ANSI 256 color) 290 | const NEON_CYAN_BG = "\x1b[48;5;51m"; // Bright neon cyan background 291 | const RESET = "\x1b[0m"; // Reset to default color 292 | 293 | // Create a function to apply the neon cyan color to text - for consistency 294 | const neonCyanText = (text) => `${NEON_CYAN}${text}${RESET}`; 295 | 296 | // Create ASCII logo with direct approach 297 | try { 298 | // Generate the text first 299 | const logoText = figlet.textSync("ENVIO.DEV", { 300 | font: "ANSI Shadow", 301 | horizontalLayout: "full", 302 | }); 303 | 304 | // Color it directly with neon cyan - should work universally 305 | const coloredLogo = NEON_CYAN + logoText + RESET; 306 | 307 | // Set the content directly 308 | logo.setContent(coloredLogo); 309 | } catch (error) { 310 | // Emergency fallback to plain text if all else fails 311 | logo.setContent(chalk.cyan.bold("ENVIO.DEV")); 312 | } 313 | 314 | // Create subtitle 315 | const subtitle = grid.set(3, 0, 1, 12, blessed.box, { 316 | content: chalk.yellow(` ${title} - Powered by Envio `), 317 | tags: true, 318 | align: "center", 319 | valign: "middle", 320 | style: { 321 | fg: "yellow", 322 | bold: true, 323 | }, 324 | }); 325 | 326 | // Create a custom progress bar 327 | const progressBox = grid.set(4, 0, 1, 12, blessed.box, { 328 | label: " Scanning Progress ", 329 | tags: true, 330 | border: { 331 | type: "line", 332 | fg: uiColor, 333 | }, 334 | style: { 335 | fg: "white", 336 | }, 337 | }); 338 | 339 | // Create stats display 340 | const stats = grid.set(5, 0, 2, 6, blessed.box, { 341 | label: "Stats", 342 | tags: true, 343 | border: { 344 | type: "line", 345 | fg: uiColor, 346 | }, 347 | style: { 348 | fg: "white", 349 | }, 350 | }); 351 | 352 | // Create event distribution display 353 | const eventDistribution = grid.set(5, 6, 2, 6, blessed.box, { 354 | label: "Event Distribution", 355 | tags: true, 356 | border: { 357 | type: "line", 358 | fg: uiColor, 359 | }, 360 | style: { 361 | fg: "white", 362 | }, 363 | }); 364 | 365 | // Create log window 366 | const logWindow = grid.set(7, 0, 4, 12, contrib.log, { 367 | label: "Event Log", 368 | tags: true, 369 | border: { 370 | type: "line", 371 | fg: uiColor, 372 | }, 373 | style: { 374 | fg: "green", 375 | }, 376 | bufferLength: 30, 377 | }); 378 | 379 | // Exit on Escape, q, or Ctrl+C 380 | screen.key(["escape", "q", "C-c"], function (ch, key) { 381 | return process.exit(0); 382 | }); 383 | 384 | // Custom function to update the progress bar display 385 | const updateProgressBar = (progress, label = "") => { 386 | try { 387 | // Calculate the width of the progress bar (accounting for borders and label) 388 | const width = progressBox.width - 4; 389 | const filledWidth = Math.floor(width * progress); 390 | const emptyWidth = width - filledWidth; 391 | 392 | // Create the progress bar with our neon cyan color 393 | const filledBar = NEON_CYAN_BG + " ".repeat(filledWidth) + RESET; 394 | const emptyBar = chalk.bgBlack(" ".repeat(emptyWidth)); 395 | 396 | // Update the progress box content 397 | progressBox.setContent( 398 | `${filledBar}${emptyBar} ${(progress * 100).toFixed(2)}% ${label}` 399 | ); 400 | } catch (err) { 401 | // Silently handle errors 402 | } 403 | }; 404 | 405 | // Function to update event distribution display using ASCII bars 406 | const updateEventDistribution = (eventCounts) => { 407 | try { 408 | // Make sure we have some events before calculating percentages 409 | const hasEvents = eventCounts.Total > 0; 410 | 411 | // Get all event names (excluding Unknown and Total) 412 | const eventNames = Object.keys(eventCounts).filter( 413 | (key) => key !== "Unknown" && key !== "Total" 414 | ); 415 | 416 | // Calculate the total (excluding unknown and total) 417 | const knownTotal = Math.max( 418 | eventNames.reduce((sum, name) => sum + eventCounts[name], 0), 419 | 1 // Ensure we don't divide by zero 420 | ); 421 | 422 | // Find max count for scaling 423 | const maxCount = Math.max( 424 | ...eventNames.map((name) => eventCounts[name]), 425 | 1 426 | ); 427 | 428 | // Create percentage bars for each event type - using simpler color approach 429 | const createBar = (count, color, maxWidth = 30) => { 430 | // Calculate scaled width to ensure small values are visible 431 | let width = 0; 432 | let percentage = 0; 433 | 434 | if (hasEvents) { 435 | percentage = count / knownTotal; 436 | 437 | // Use a logarithmic scale for better visualization when values are lopsided 438 | if (maxCount > 0 && count > 0) { 439 | // Ensure small values get at least a small bar 440 | const logScale = Math.log(count + 1) / Math.log(maxCount + 1); 441 | width = Math.max(1, Math.floor(logScale * maxWidth)); 442 | } 443 | } 444 | 445 | // Use a more visible character for the bar 446 | const barChar = "■"; 447 | // Use safe color approach 448 | return ( 449 | color(barChar.repeat(width)) + 450 | ` ${formatNumber(count)} (${(percentage * 100).toFixed(1)}%)` 451 | ); 452 | }; 453 | 454 | // Add extra spacing for better readability 455 | const labelWidth = 12; 456 | 457 | // Use basic colors for better terminal compatibility 458 | const colors = [ 459 | chalk.green, // Green 460 | chalk.yellow, // Yellow 461 | chalk.blue, // Blue 462 | chalk.red, // Red 463 | chalk.magenta, // Magenta 464 | chalk.cyan, // Cyan 465 | chalk.white, // White 466 | ]; 467 | 468 | // Set the content with each bar on its own line 469 | const content = eventNames 470 | .map((name, index) => { 471 | const color = colors[index % colors.length]; 472 | // Use neonCyanText for event names for consistency with other labels 473 | return `${neonCyanText(name.padEnd(labelWidth))} ${createBar( 474 | eventCounts[name], 475 | color 476 | )}`; 477 | }) 478 | .join("\n"); 479 | 480 | eventDistribution.setContent(content); 481 | } catch (err) { 482 | // Silently handle errors 483 | } 484 | }; 485 | 486 | // Render the screen 487 | screen.render(); 488 | 489 | //============================================================================= 490 | // MAIN FUNCTION 491 | //============================================================================= 492 | 493 | const startTime = performance.now(); 494 | 495 | // Log startup 496 | logWindow.log(chalk.yellow(`Initializing Event Scanner...`)); 497 | screen.render(); 498 | 499 | try { 500 | //========================================================================= 501 | // STEP 1: Get blockchain height using Hypersync 502 | //========================================================================= 503 | const height = await client.getHeight(); 504 | logWindow.log( 505 | `Starting scan from block ${safeHexColor("0")} to ${safeHexColor( 506 | formatNumber(height) 507 | )}` 508 | ); 509 | screen.render(); 510 | 511 | //========================================================================= 512 | // STEP 2: Create a decoder for the event signatures 513 | //========================================================================= 514 | const decoder = Decoder.fromSignatures(eventSignatures); 515 | logWindow.log("Event decoder initialized"); 516 | screen.render(); 517 | 518 | //========================================================================= 519 | // STEP 3: Stream events from Hypersync 520 | //========================================================================= 521 | logWindow.log(chalk.green("Starting event stream...")); 522 | screen.render(); 523 | const stream = await client.stream(query, {}); 524 | 525 | // Update subtitle to show network 526 | subtitle.setContent( 527 | chalk.yellow(` ${title} - Block Height: ${formatNumber(height)} `) 528 | ); 529 | screen.render(); 530 | 531 | //========================================================================= 532 | // STEP 4: Process streaming data 533 | //========================================================================= 534 | let lastLogUpdate = 0; 535 | let lastDistributionUpdate = 0; 536 | 537 | // Initialize progress bar 538 | updateProgressBar(0, `Block: 0/${formatNumber(height)}`); 539 | 540 | // Initialize distribution display 541 | updateEventDistribution(eventCounts); 542 | 543 | screen.render(); 544 | 545 | while (true) { 546 | // Get the next batch of data from Hypersync 547 | const res = await stream.recv(); 548 | 549 | // Quit if we reached the tip of the blockchain 550 | if (res === null) { 551 | logWindow.log(chalk.green("✓ Reached the tip of the blockchain!")); 552 | updateProgressBar( 553 | 1, 554 | `Block: ${formatNumber(height)}/${formatNumber(height)}` 555 | ); 556 | screen.render(); 557 | break; 558 | } 559 | 560 | // Make sure we have a nextBlock value 561 | if (!res.nextBlock) { 562 | logWindow.log(chalk.yellow("Warning: Missing nextBlock in response")); 563 | continue; 564 | } 565 | 566 | // Process logs if any exist in this batch 567 | if ( 568 | res.data && 569 | res.data.logs && 570 | Array.isArray(res.data.logs) && 571 | res.data.logs.length > 0 572 | ) { 573 | // Process logs based on their topic0 value 574 | res.data.logs.forEach((log) => { 575 | if (!log) return; // Skip if log is null 576 | 577 | eventCounts.Total++; 578 | 579 | if (!log.topics || !Array.isArray(log.topics) || !log.topics[0]) { 580 | eventCounts.Unknown++; 581 | return; 582 | } 583 | 584 | const topic0 = log.topics[0]; 585 | const eventName = topic0ToName[topic0] || "Unknown"; 586 | 587 | if (eventName === "Unknown") { 588 | eventCounts.Unknown++; 589 | } else { 590 | eventCounts[eventName] = (eventCounts[eventName] || 0) + 1; 591 | } 592 | }); 593 | 594 | // Log a decoded event sample occasionally 595 | try { 596 | if (eventCounts.Total % 1000 === 0 && res.data.logs[0]) { 597 | const decodedLogs = await decoder.decodeLogs([res.data.logs[0]]); 598 | if ( 599 | decodedLogs && 600 | Array.isArray(decodedLogs) && 601 | decodedLogs.length > 0 && 602 | decodedLogs[0] 603 | ) { 604 | const eventInfo = decodedLogs[0].event 605 | ? safeStringify(decodedLogs[0].event) 606 | : "No event data"; 607 | logWindow.log( 608 | neonCyanText( 609 | `Sample event at block ${res.nextBlock}: ${eventInfo}` 610 | ) 611 | ); 612 | screen.render(); 613 | } 614 | } 615 | } catch (decodeError) { 616 | logWindow.log(chalk.yellow(`Decode warning: ${decodeError.message}`)); 617 | } 618 | } 619 | 620 | // Update the fromBlock for the next iteration 621 | if (res.nextBlock) { 622 | query.fromBlock = res.nextBlock; 623 | } 624 | 625 | // Calculate time stats 626 | const currentTime = performance.now(); 627 | const seconds = Math.max((currentTime - startTime) / 1000, 0.1); // Avoid division by zero 628 | const eventsPerSecond = (eventCounts.Total / seconds).toFixed(1); 629 | 630 | // Calculate progress 631 | const progress = Math.min(res.nextBlock / height, 1); 632 | 633 | // Update the progress bar 634 | updateProgressBar( 635 | progress, 636 | `Block: ${formatNumber(res.nextBlock)}/${formatNumber(height)}` 637 | ); 638 | 639 | // Update stats display 640 | try { 641 | stats.setContent( 642 | `${neonCyanText("Current Block")}: ${formatNumber(res.nextBlock)}\n` + 643 | `${neonCyanText("Progress")}: ${(progress * 100).toFixed(2)}%\n` + 644 | `${neonCyanText("Total Events")}: ${formatNumber( 645 | eventCounts.Total 646 | )}\n` + 647 | `${neonCyanText("Elapsed Time")}: ${seconds.toFixed(1)}s\n` + 648 | `${neonCyanText("Speed")}: ${formatNumber( 649 | eventsPerSecond 650 | )} events/s` 651 | ); 652 | } catch (statsError) { 653 | // Silently handle errors 654 | } 655 | 656 | // Update event distribution periodically 657 | if (res.nextBlock - lastDistributionUpdate >= 10000) { 658 | updateEventDistribution(eventCounts); 659 | lastDistributionUpdate = res.nextBlock; 660 | } 661 | 662 | // Log progress periodically to avoid too many updates 663 | if (res.nextBlock - lastLogUpdate >= 50000) { 664 | logWindow.log( 665 | `${neonCyanText("Block")} ${formatNumber( 666 | res.nextBlock 667 | )} | ${formatNumber( 668 | eventCounts.Total 669 | )} events | ${eventsPerSecond} events/s` 670 | ); 671 | lastLogUpdate = res.nextBlock; 672 | } 673 | 674 | // Render the updated screen 675 | screen.render(); 676 | } 677 | 678 | //========================================================================= 679 | // Final summary 680 | //========================================================================= 681 | const totalTime = Math.max((performance.now() - startTime) / 1000, 0.1); // Avoid division by zero 682 | 683 | // Update final stats 684 | stats.setContent( 685 | `${neonCyanText("Blocks Scanned")}: ${formatNumber(height)}\n` + 686 | `${neonCyanText("Total Events")}: ${formatNumber( 687 | eventCounts.Total 688 | )}\n` + 689 | `${neonCyanText("Elapsed Time")}: ${totalTime.toFixed(1)}s\n` + 690 | `${neonCyanText("Avg Speed")}: ${formatNumber( 691 | Math.round(eventCounts.Total / totalTime) 692 | )} events/s` 693 | ); 694 | 695 | // Final distribution update 696 | updateEventDistribution(eventCounts); 697 | 698 | // Log completion 699 | logWindow.log(chalk.green("✓ Scan complete!")); 700 | logWindow.log( 701 | chalk.yellow(`Total processing time: ${totalTime.toFixed(2)} seconds`) 702 | ); 703 | logWindow.log( 704 | chalk.yellow( 705 | `Average speed: ${formatNumber( 706 | Math.round(eventCounts.Total / totalTime) 707 | )} events/second` 708 | ) 709 | ); 710 | 711 | // Bold final message 712 | subtitle.setContent(chalk.green.bold(" Scan Complete - Press Q to Exit ")); 713 | 714 | // Render final screen 715 | screen.render(); 716 | 717 | // Wait for user to exit 718 | await new Promise((resolve) => setTimeout(resolve, 1000000000)); 719 | } catch (error) { 720 | logWindow.log(chalk.red(`Error: ${error.message}`)); 721 | screen.render(); 722 | await new Promise((resolve) => setTimeout(resolve, 5000)); 723 | process.exit(1); 724 | } 725 | } 726 | --------------------------------------------------------------------------------