├── .gitignore ├── LICENSE ├── README.md ├── commandLineOptions.js ├── ethereum_client_scripts ├── configureBGPeers.js ├── geth.js ├── install.js ├── lighthouse.js ├── prysm.js └── reth.js ├── ethereum_clients └── .gitkeep ├── getSystemStats.js ├── helpers.js ├── index.js ├── monitor.js ├── monitor_components ├── bandwidthGauge.js ├── chainInfoBox.js ├── consensusLog.js ├── cpuLine.js ├── diskLine.js ├── executionLog.js ├── gethStageGauge.js ├── header.js ├── helperFunctions.js ├── networkLine.js ├── peerCountGauge.js ├── pixelBgLogo.png ├── pixelBgLogo40.png ├── rethStageGauge.js ├── rpcInfoBox.js ├── statusBox.js ├── systemStatsGauge.js ├── updateLogic.js └── viemClients.js ├── package-lock.json ├── package.json ├── web_socket_connection └── webSocketConnection.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Reth, Geth, Lighthouse, and Prysm executables, logs, and databases 133 | ethereum_clients/* 134 | !ethereum_clients/.gitkeep 135 | 136 | # Debug log 137 | debug.log 138 | 139 | # Command line options json (loaded for secondary dashboard views) 140 | options.json 141 | 142 | # Mac-specific finder file 143 | .DS_Store 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 BuidlGuidl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📡 BuidlGuidl Client 2 | This project will download client executables, start a execution + consensus client pair, and provide a terminal dashboard view of client and machine info. 3 | 4 |   5 |   6 | ## Hardware Requirements 7 | See this [Rocket Pool Hardware Guide](https://docs.rocketpool.net/guides/node/local/hardware) for a nice rundown of node hardware requirements. 8 | 9 | - Node operation doesn't require too much CPU power. We've ran the BG Client using both i3 and i5 versions of the [ASUS NUC 13 PRO](https://www.asus.com/us/displays-desktops/nucs/nuc-mini-pcs/asus-nuc-13-pro/). Note that there are some gotchas if you plan to use a Celeron processor. ([Rocket Pool Hardware Guide](https://docs.rocketpool.net/guides/node/local/hardware)). 10 | - 32 GB of RAM works well with plenty of overhead. 11 | - Selecting a suitable NVMe SSD is the trickiest part. You will need at least a 2 TB drive that includes a DRAM cache and DOES NOT use a Quad-level cell (QLC) architecture. The [Rocket Pool Hardware Guide](https://docs.rocketpool.net/guides/node/local/hardware) goes into more detail. This [SSD List GitHub Gist](https://gist.github.com/yorickdowne/f3a3e79a573bf35767cd002cc977b038) has a nice list of SSDs that have been tested and confirmed to work for running nodes. 12 | 13 |   14 |   15 | ## Dependencies 16 | For Linux & MacOS: 17 | - node (https://nodejs.org/en) 18 | - yarn (https://yarnpkg.com/migration/overview) 19 | - GNU Make (https://www.gnu.org/software/make/) 20 | 21 | Additional MacOS Specifics: 22 | - gnupg (https://gnupg.org/) 23 | - Perl-Digest-SHA (https://metacpan.org/pod/Digest::SHA) 24 | 25 | Hint: See the one line command below if you don't want to install the dependencies manually. 26 | 27 |   28 |   29 | ## Quickstart 30 | To get a full node started using a Reth + Lighthouse client pair: 31 | ```bash 32 | git clone https://github.com/BuidlGuidl/buidlguidl-client.git 33 | cd buidlguidl-client 34 | yarn install 35 | node index.js 36 | ``` 37 | 38 | ------------ OR ------------ 39 | 40 | Run this fancy one line command to check for/install dependencies and clone/run this repo (see https://client.buidlguidl.com/): 41 | ```bash 42 | /bin/bash -c "$(curl -fsSL https://bgclient.io)" 43 | ``` 44 | 45 |   46 |   47 | 48 | By default, client executables, databases, and logs will be established within buidlguidl-client/ethereum_clients. After initialization steps, the script displays a terminal view with scrolling client logs and some plots showing some machine and chain stats. Full client logs are located in buidlguidl-client/ethereum_clients/reth/logs and buidlguidl-client/ethereum_clients/lighthouse/logs. Exiting the terminal view (control-c or q) will also gracefully close your clients (can take 15 seconds or so). 49 | 50 |   51 |   52 | 53 | ## Startup Options 54 | 55 | Use the --archive flag to perform an archive sync for the execution client: 56 | ```bash 57 | node index.js --archive 58 | ``` 59 | 60 | Omitting the --archive flag will make the execution clients perform a pruned sync that will give you full access to data from the last 10,064 blocks for Reth or the last 128 blocks for Geth. 61 | 62 |   63 |   64 | 65 | You can opt in to the BuidlGuidl distributed RPC system and earn credits for serving RPC requests to the BuidlGuidl network by passing your eth address to the --owner (-o) option: 66 | ```bash 67 | node index.js --owner 68 | ``` 69 | 70 |   71 |   72 | 73 | If you want to specify a non-standard location for the ethereum_clients directory, pass a --directory (-d) option to index.js: 74 | ```bash 75 | node index.js --directory path/for/directory/containing/ethereum_clients 76 | ``` 77 | 78 |   79 |   80 | 81 | If you want to use a Geth + Prysm client pair, pass those as --executionclient (-e) and --consensusclient (-c) options to index.js: 82 | ```bash 83 | node index.js --executionclient geth --consensusclient prysm 84 | ``` 85 | 86 |   87 |   88 | 89 | Pass the --update option to update the execution and consensus clients to the latest versions (that have been tested with the BG Client): 90 | ```bash 91 | node index.js --update 92 | ``` 93 | 94 |   95 |   96 | 97 | Use the --help (-h) option to see all command line options: 98 | ```bash 99 | node index.js --help 100 | 101 | -e, --executionclient Specify the execution client ('reth' or 'geth') 102 | Default: reth 103 | 104 | -c, --consensusclient Specify the consensus client ('lighthouse' or 'prysm') 105 | Default: lighthouse 106 | 107 | --archive Perform an archive sync for the execution client 108 | 109 | -ep, --executionpeerport Specify the execution peer port (must be a number) 110 | Default: 30303 111 | 112 | -cp, --consensuspeerports , Specify the execution peer ports (must be two comma-separated numbers) 113 | lighthouse defaults: 9000,9001. prysm defaults: 12000,13000 114 | 115 | -cc, --consensuscheckpoint Specify the consensus checkpoint server URL 116 | Lighthouse default: https://mainnet-checkpoint-sync.stakely.io/ 117 | Prysm default: https://mainnet-checkpoint-sync.attestant.io/ 118 | 119 | -d, --directory Specify ethereum client executable, database, and logs directory 120 | Default: buidlguidl-client/ethereum_clients 121 | 122 | -o, --owner Specify a owner eth address to opt in to the points system and distributed RPC network 123 | 124 | --update Update the execution and consensus clients to the latest version. 125 | Latest versions: Reth: 1.0.0, Geth: 1.14.12, Lighthouse: 5.3.0, (Prysm is handled by its executable automatically) 126 | 127 | -h, --help Display this help message and exit 128 | ``` 129 | 130 |   131 |   132 | ## Common Questions and Issues 133 | The consensus clients (Lighthouse and Prysm) require a checkpoint sync server URL to initiate sync. Connection to checkpoint servers can fail depending on your location. If the consensus client fails to start the sync and you see an error message in the Lighthouse/Prysm logs like this: 134 | 135 | ```bash 136 | Nov 21 17:45:41.833 INFO Starting checkpoint sync remote_url: https://mainnet-checkpoint-sync.stakely.io/, service: beacon 137 | Nov 21 17:45:51.842 CRIT Failed to start beacon node reason: Error loading checkpoint state from remote: HttpClient(, kind: timeout, detail: operation timed out) 138 | Nov 21 17:45:51.843 INFO Internal shutdown received reason: Failed to start beacon node 139 | Nov 21 17:45:51.843 INFO Shutting down.. reason: Failure("Failed to start beacon node") 140 | Failed to start beacon node 141 | ``` 142 | 143 | You will need to specify a different checkpoint server URL using the --consensuscheckpoint (-cc) option. See https://eth-clients.github.io/checkpoint-sync-endpoints/ for a list of public checkpoint sync servers. 144 | 145 |   146 |   147 | 148 | The consensus client logs can output many warnings while syncing (see below for some Lighthouse examples). These warnings can be ignored and will resolve after the execution client has synced. They look scary but it's expected behavior. 149 | 150 | ```bash 151 | Nov 21 20:58:53.309 INFO Block production disabled reason: no eth1 backend configured 152 | Nov 21 21:01:16.144 WARN Blocks and blobs request for range received invalid data, error: MissingBlobs, sender_id: BackfillSync { batch_id: Epoch(326557) }, peer_id: 16Uiu2HAkv5priPv8S7bawF8u96aAMgAbtkh95x4PkDvm7WSdH3ER, service: sync 153 | Nov 21 21:01:17.001 WARN Head is optimistic execution_block_hash: 0x16410f3d5cb5044dcf596b301a34ec88ffce09dd4346f04aea95d442b1456e62, info: chain not fully verified, block and attestation production disabled until execution engine syncs, service: slot_notifier 154 | Nov 21 21:01:44.997 WARN Execution engine call failed error: InvalidClientVersion("Input must be exactly 8 characters long (excluding any '0x' prefix)"), service: exec 155 | Nov 21 21:01:59.013 WARN Error signalling fork choice waiter slot: 10449907, error: ForkChoiceSignalOutOfOrder { current: Slot(10449908), latest: Slot(10449907) }, service: beacon 156 | ``` -------------------------------------------------------------------------------- /commandLineOptions.js: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | import fs from "fs"; 3 | import minimist from "minimist"; 4 | import readlineSync from "readline-sync"; 5 | import { fileURLToPath } from "url"; 6 | import { dirname, join } from "path"; 7 | import { 8 | installMacLinuxClient, 9 | getVersionNumber, 10 | compareClientVersions, 11 | removeClient, 12 | latestGethVer, 13 | latestRethVer, 14 | latestLighthouseVer, 15 | } from "./ethereum_client_scripts/install.js"; 16 | import { debugToFile } from "./helpers.js"; 17 | 18 | debugToFile( 19 | `\n\n\n\n\n\n--------------------------------------------------------------------------` 20 | ); 21 | debugToFile( 22 | `---------------------------- CLIENT STARTED ----------------------------` 23 | ); 24 | 25 | /// Set default command line option values 26 | let executionClient = "reth"; 27 | let executionType = "full"; 28 | let consensusClient = "lighthouse"; 29 | let executionPeerPort = 30303; 30 | let consensusPeerPorts = [null, null]; 31 | let consensusCheckpoint = null; 32 | let owner = null; 33 | 34 | const filename = fileURLToPath(import.meta.url); 35 | let installDir = dirname(filename); 36 | 37 | const optionsFilePath = join(installDir, "options.json"); 38 | 39 | function showHelp() { 40 | console.log(""); 41 | console.log( 42 | " -e, --executionclient Specify the execution client ('reth' or 'geth')" 43 | ); 44 | console.log(" Default: reth\n"); 45 | console.log( 46 | " -c, --consensusclient Specify the consensus client ('lighthouse' or 'prysm')" 47 | ); 48 | console.log( 49 | " Default: lighthouse\n" 50 | ); 51 | console.log( 52 | " --archive Perform an archive sync for the execution client\n" 53 | ); 54 | console.log( 55 | " -ep, --executionpeerport Specify the execution peer port (must be a number)" 56 | ); 57 | console.log(" Default: 30303\n"); 58 | console.log( 59 | " -cp, --consensuspeerports , Specify the execution peer ports (must be two comma-separated numbers)" 60 | ); 61 | console.log( 62 | " lighthouse defaults: 9000,9001. prysm defaults: 12000,13000\n" 63 | ); 64 | console.log( 65 | " -cc, --consensuscheckpoint Specify the consensus checkpoint server URL" 66 | ); 67 | console.log( 68 | " Lighthouse default: https://mainnet-checkpoint-sync.stakely.io/" 69 | ); 70 | console.log( 71 | " Prysm default: https://mainnet-checkpoint-sync.attestant.io/\n" 72 | ); 73 | console.log( 74 | " -d, --directory Specify ethereum client executable, database, and logs directory" 75 | ); 76 | console.log( 77 | " Default: buidlguidl-client/ethereum_clients\n" 78 | ); 79 | console.log( 80 | " -o, --owner Specify a owner eth address to opt in to the points system and distributed RPC network\n" 81 | ); 82 | console.log( 83 | " --update Update the execution and consensus clients to the latest version." 84 | ); 85 | console.log( 86 | ` Latest versions: Reth: ${latestRethVer}, Geth: ${latestGethVer}, Lighthouse: ${latestLighthouseVer}, (Prysm is handled by its executable automatically)\n` 87 | ); 88 | console.log( 89 | " -h, --help Display this help message and exit" 90 | ); 91 | console.log(""); 92 | } 93 | 94 | function isValidPath(p) { 95 | try { 96 | return fs.existsSync(p) && fs.statSync(p).isDirectory(); 97 | } catch (err) { 98 | return false; 99 | } 100 | } 101 | 102 | // Function to save options to a file 103 | function saveOptionsToFile() { 104 | const options = { 105 | executionClient, 106 | consensusClient, 107 | executionPeerPort, 108 | consensusPeerPorts, 109 | consensusCheckpoint, 110 | installDir, 111 | owner, 112 | }; 113 | fs.writeFileSync(optionsFilePath, JSON.stringify(options), "utf8"); 114 | } 115 | 116 | // Function to load options from a file 117 | function loadOptionsFromFile() { 118 | if (fs.existsSync(optionsFilePath)) { 119 | const options = JSON.parse(fs.readFileSync(optionsFilePath, "utf8")); 120 | return options; 121 | } else { 122 | debugToFile(`loadOptionsFromFile(): Options file not found`); 123 | } 124 | } 125 | 126 | // Check if the options file already exists 127 | let optionsLoaded = false; 128 | if (fs.existsSync(optionsFilePath)) { 129 | try { 130 | const options = loadOptionsFromFile(); 131 | executionClient = options.executionClient; 132 | consensusClient = options.consensusClient; 133 | executionPeerPort = options.executionPeerPort; 134 | consensusPeerPorts = options.consensusPeerPorts; 135 | consensusCheckpoint = options.consensusCheckpoint; 136 | installDir = options.installDir; 137 | owner = options.owner; 138 | optionsLoaded = true; 139 | } catch (error) { 140 | debugToFile(`Failed to load options from file: ${error}`); 141 | } 142 | } 143 | 144 | function deleteOptionsFile() { 145 | try { 146 | if (fs.existsSync(optionsFilePath)) { 147 | fs.unlinkSync(optionsFilePath); 148 | } 149 | } catch (error) { 150 | debugToFile(`deleteOptionsFile(): ${error}`); 151 | } 152 | } 153 | 154 | // Preprocess arguments to handle "-ep" and "-cp" as aliases 155 | const args = process.argv.slice(2).flatMap((arg) => { 156 | if (arg === "-ep") { 157 | return "--executionpeerport"; 158 | } else if (arg === "-cp") { 159 | return "--consensuspeerports"; 160 | } else if (arg === "-cc") { 161 | return "--consensuscheckpoint"; 162 | } 163 | return arg; 164 | }); 165 | 166 | // If options were not loaded from the file, process command-line arguments 167 | if (!optionsLoaded) { 168 | const argv = minimist(args, { 169 | string: [ 170 | "e", 171 | "executionclient", 172 | "c", 173 | "consensusclient", 174 | "executionpeerport", 175 | "consensuspeerports", 176 | "consensuscheckpoint", 177 | "d", 178 | "directory", 179 | "o", 180 | "owner", 181 | ], 182 | alias: { 183 | e: "executionclient", 184 | c: "consensusclient", 185 | d: "directory", 186 | o: "owner", 187 | h: "help", 188 | }, 189 | boolean: ["h", "help", "update", "archive"], 190 | unknown: (option) => { 191 | console.log(`Invalid option: ${option}`); 192 | showHelp(); 193 | process.exit(1); 194 | }, 195 | }); 196 | 197 | if (argv.executionclient) { 198 | executionClient = argv.executionclient; 199 | if (executionClient !== "reth" && executionClient !== "geth") { 200 | console.log( 201 | "Invalid option for --executionclient (-e). Use 'reth' or 'geth'." 202 | ); 203 | process.exit(1); 204 | } 205 | } 206 | 207 | if (argv.archive) { 208 | executionType = "archive"; 209 | } 210 | 211 | if (argv.consensusclient) { 212 | consensusClient = argv.consensusclient; 213 | if (consensusClient !== "lighthouse" && consensusClient !== "prysm") { 214 | console.log( 215 | "Invalid option for --consensusclient (-c). Use 'lighthouse' or 'prysm'." 216 | ); 217 | process.exit(1); 218 | } 219 | } 220 | 221 | if (argv.executionpeerport) { 222 | executionPeerPort = parseInt(argv.executionpeerport, 10); 223 | if (executionPeerPort === "number" && !isNaN(executionPeerPort)) { 224 | console.log( 225 | "Invalid option for --executionpeerport (-ep). Must be a number." 226 | ); 227 | process.exit(1); 228 | } 229 | } 230 | 231 | if (argv.consensuspeerports) { 232 | consensusPeerPorts = argv.consensuspeerports 233 | .split(",") 234 | .map((port) => parseInt(port.trim(), 10)); 235 | 236 | // Check if there are exactly two ports and if both are valid numbers 237 | if (consensusPeerPorts.length !== 2 || consensusPeerPorts.some(isNaN)) { 238 | console.log( 239 | "Invalid option for --consensuspeerports (-cp). Must be two comma-separated numbers (e.g., 9000,9001)." 240 | ); 241 | process.exit(1); 242 | } 243 | } 244 | 245 | if (argv.consensuscheckpoint) { 246 | consensusCheckpoint = argv.consensuscheckpoint; 247 | } 248 | 249 | if (argv.directory) { 250 | installDir = argv.directory; 251 | if (!isValidPath(installDir)) { 252 | console.log( 253 | `Invalid option for --directory (-d). '${installDir}' is not a valid path.` 254 | ); 255 | process.exit(1); 256 | } 257 | } 258 | 259 | if (argv.owner) { 260 | owner = argv.owner; 261 | } 262 | 263 | if (argv.update) { 264 | // Get list of installed clients from directory 265 | const clientsDir = join(installDir, "ethereum_clients"); 266 | const clients = fs.existsSync(clientsDir) 267 | ? fs 268 | .readdirSync(clientsDir) 269 | .filter((dir) => fs.statSync(join(clientsDir, dir)).isDirectory()) 270 | : []; 271 | 272 | for (const client of clients) { 273 | if (client !== "prysm" && client !== "jwt") { 274 | const installedVersion = getVersionNumber(client); 275 | 276 | // Skip if no version number found 277 | if (!installedVersion) { 278 | console.log( 279 | `⚠️ Could not determine version for ${client}, skipping update check.` 280 | ); 281 | continue; 282 | } 283 | 284 | const [isLatest, latestVersion] = compareClientVersions( 285 | client, 286 | installedVersion 287 | ); 288 | if (isLatest) { 289 | console.log( 290 | `\n✅ The currently installed ${client} version (${installedVersion}) is the latest available.` 291 | ); 292 | } else { 293 | console.log( 294 | `\n❓ An updated version of ${client} is available. ${installedVersion} is currently installed. Would you like to update to ${latestVersion}? (y/yes)` 295 | ); 296 | 297 | const answer = readlineSync.question(""); 298 | if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") { 299 | console.log(`Removing old version of ${client}`); 300 | removeClient(client); 301 | 302 | const platform = os.platform(); 303 | if (["darwin", "linux"].includes(platform)) { 304 | installMacLinuxClient(client, platform); 305 | } 306 | console.log(""); 307 | console.log(`👍 Updated ${client} to ${latestVersion}`); 308 | } else { 309 | console.log("Update cancelled."); 310 | } 311 | } 312 | } 313 | } 314 | process.exit(0); 315 | } 316 | 317 | if (argv.help) { 318 | showHelp(); 319 | process.exit(0); 320 | } 321 | 322 | if ( 323 | consensusPeerPorts.every((port) => port === null) && 324 | consensusClient === "lighthouse" 325 | ) { 326 | consensusPeerPorts = [9000, 9001]; 327 | } 328 | 329 | if ( 330 | consensusPeerPorts.every((port) => port === null) && 331 | consensusClient === "prysm" 332 | ) { 333 | consensusPeerPorts = [12000, 13000]; 334 | } 335 | } 336 | 337 | export { 338 | executionClient, 339 | executionType, 340 | consensusClient, 341 | executionPeerPort, 342 | consensusPeerPorts, 343 | consensusCheckpoint, 344 | installDir, 345 | owner, 346 | saveOptionsToFile, 347 | deleteOptionsFile, 348 | }; 349 | -------------------------------------------------------------------------------- /ethereum_client_scripts/configureBGPeers.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import { getPublicIPAddress } from "../getSystemStats.js"; 3 | import { debugToFile } from "../helpers.js"; 4 | import { executionPeerPort } from "../commandLineOptions.js"; 5 | import os from "os"; 6 | import { getMacAddress } from "../getSystemStats.js"; 7 | import { consensusClient } from "../commandLineOptions.js"; 8 | 9 | export async function fetchBGExecutionPeers() { 10 | try { 11 | const publicIP = await getPublicIPAddress(); 12 | const response = await fetch( 13 | "https://pool.mainnet.rpc.buidlguidl.com:48546/enodes" 14 | ); 15 | const data = await response.json(); 16 | 17 | const filteredEnodes = data.enodes.filter((node) => { 18 | const nodeUrl = new URL(node.enode); 19 | return !( 20 | nodeUrl.hostname === publicIP && 21 | nodeUrl.port === executionPeerPort.toString() 22 | ); 23 | }); 24 | 25 | const filteredEnodeValues = filteredEnodes.map((node) => node.enode); 26 | 27 | debugToFile( 28 | "fetchBGExecutionPeers(): Filtered enodes:\n" + 29 | filteredEnodeValues.join("\n") 30 | ); 31 | 32 | return filteredEnodeValues; 33 | } catch (error) { 34 | debugToFile("fetchBGExecutionPeers() error:", error); 35 | return []; 36 | } 37 | } 38 | 39 | export async function configureBGExecutionPeers(bgPeers) { 40 | try { 41 | for (const enode of bgPeers) { 42 | const curlCommandAddPeer = `curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","id":1,"method":"admin_addPeer","params":["${enode}"]}' http://localhost:8545`; 43 | const curlCommandAddTrustedPeer = `curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","id":1,"method":"admin_addTrustedPeer","params":["${enode}"]}' http://localhost:8545`; 44 | 45 | const { exec } = await import("child_process"); 46 | 47 | exec(curlCommandAddPeer, (error, stdout, stderr) => { 48 | if (error) { 49 | debugToFile( 50 | `configureBGExecutionPeers(): AddPeer: Error executing curl command: ${error}` 51 | ); 52 | return; 53 | } 54 | if (stderr) { 55 | debugToFile( 56 | `configureBGExecutionPeers(): AddPeer: Curl command stderr: ${stderr}` 57 | ); 58 | return; 59 | } 60 | debugToFile( 61 | `configureBGExecutionPeers(): AddPeer: Curl command stdout: ${stdout}` 62 | ); 63 | }); 64 | 65 | exec(curlCommandAddTrustedPeer, (error, stdout, stderr) => { 66 | if (error) { 67 | debugToFile( 68 | `configureBGExecutionPeers(): AddTrustedPeer: Error executing curl command: ${error}` 69 | ); 70 | return; 71 | } 72 | if (stderr) { 73 | debugToFile( 74 | `configureBGExecutionPeers(): AddTrustedPeer: Curl command stderr: ${stderr}` 75 | ); 76 | return; 77 | } 78 | debugToFile( 79 | `configureBGExecutionPeers(): AddTrustedPeer: Curl command stdout: ${stdout}` 80 | ); 81 | }); 82 | } 83 | } catch (error) { 84 | debugToFile( 85 | `configureBGExecutionPeers() error: ${error.message}\nStack: ${error.stack}` 86 | ); 87 | } 88 | } 89 | 90 | export async function fetchBGConsensusPeers() { 91 | try { 92 | const response = await fetch( 93 | "https://pool.mainnet.rpc.buidlguidl.com:48546/peerids" 94 | ); 95 | const data = await response.json(); 96 | 97 | const peerIDValues = data.peerids 98 | .map((peer) => peer.peerid) 99 | .filter((peerid) => peerid && peerid !== "null"); // Filter out falsy values and "null" strings 100 | 101 | return peerIDValues; 102 | } catch (error) { 103 | debugToFile("fetchBGConsensusPeers() error:", error); 104 | return []; 105 | } 106 | } 107 | 108 | export async function configureBGConsensusPeers() { 109 | try { 110 | const response = await fetch( 111 | "https://pool.mainnet.rpc.buidlguidl.com:48546/consensuspeeraddr" 112 | ); 113 | const data = await response.json(); 114 | 115 | const macAddress = await getMacAddress(); 116 | const thisMachineID = `${os.hostname()}-${macAddress}-${os.platform()}-${os.arch()}`; 117 | 118 | const filteredPeers = data.consensusPeerAddrs.filter( 119 | (peer) => 120 | peer.consensusClient === consensusClient && 121 | peer.machineID !== thisMachineID 122 | ); 123 | 124 | const peerAddresses = filteredPeers.flatMap((peer) => 125 | peer.consensusPeerAddr.split(",") 126 | ); 127 | 128 | const result = peerAddresses.join(","); 129 | 130 | // debugToFile( 131 | // `configureBGConsensusPeers(): Filtered peer addresses:\n${result}` 132 | // ); 133 | 134 | return result; 135 | } catch (error) { 136 | debugToFile("configureBGConsensusPeers() error:", error); 137 | return ""; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /ethereum_client_scripts/geth.js: -------------------------------------------------------------------------------- 1 | import pty from "node-pty"; 2 | import fs from "fs"; 3 | import os from "os"; 4 | import path from "path"; 5 | import { debugToFile } from "../helpers.js"; 6 | import { stripAnsiCodes, getFormattedDateTime } from "../helpers.js"; 7 | import minimist from "minimist"; 8 | 9 | let installDir = os.homedir(); 10 | 11 | const argv = minimist(process.argv.slice(2)); 12 | 13 | const executionPeerPort = argv.executionpeerport; 14 | 15 | const executionType = argv.executiontype; 16 | debugToFile(`From geth.js: executionType: ${executionType}`); 17 | 18 | // Check if a different install directory was provided via the `--directory` option 19 | if (argv.directory) { 20 | installDir = argv.directory; 21 | } 22 | 23 | const jwtPath = path.join(installDir, "ethereum_clients", "jwt", "jwt.hex"); 24 | 25 | let gethCommand; 26 | const platform = os.platform(); 27 | if (["darwin", "linux"].includes(platform)) { 28 | gethCommand = path.join(installDir, "ethereum_clients", "geth", "geth"); 29 | } else if (platform === "win32") { 30 | gethCommand = path.join(installDir, "ethereum_clients", "geth", "geth.exe"); 31 | } 32 | 33 | const logFilePath = path.join( 34 | installDir, 35 | "ethereum_clients", 36 | "geth", 37 | "logs", 38 | `geth_${getFormattedDateTime()}.log` 39 | ); 40 | 41 | const logStream = fs.createWriteStream(logFilePath, { flags: "a" }); 42 | 43 | const execution = pty.spawn( 44 | gethCommand, 45 | [ 46 | "--mainnet", 47 | "--syncmode", 48 | // "snap", 49 | ...(executionType === "full" 50 | ? ["snap"] 51 | : executionType === "archive" 52 | ? ["full", "--gcmode", "archive"] 53 | : []), 54 | "--port", 55 | executionPeerPort, 56 | "--discovery.port", 57 | executionPeerPort, 58 | "--http", 59 | "--http.api", 60 | "eth,net,engine,admin", 61 | "--http.addr", 62 | "0.0.0.0", 63 | "--http.port", 64 | "8545", 65 | "--http.corsdomain", 66 | "*", 67 | "--datadir", 68 | path.join(installDir, "ethereum_clients", "geth", "database"), 69 | "--authrpc.jwtsecret", 70 | jwtPath, 71 | "--metrics", 72 | "--metrics.addr", 73 | "127.0.0.1", 74 | ], 75 | { 76 | name: "xterm-color", 77 | cols: 80, 78 | rows: 30, 79 | cwd: process.env.HOME, 80 | env: { ...process.env, INSTALL_DIR: installDir }, 81 | } 82 | ); 83 | 84 | // Pipe stdout and stderr to the log file and to the parent process 85 | execution.on("data", (data) => { 86 | logStream.write(stripAnsiCodes(data)); 87 | if (process.send) { 88 | process.send({ log: data }); 89 | } 90 | }); 91 | 92 | execution.on("exit", (code) => { 93 | // const exitMessage = `geth process exited with code ${code}`; 94 | // logStream.write(exitMessage); 95 | logStream.end(); 96 | }); 97 | 98 | execution.on("error", (err) => { 99 | const errorMessage = `Error: ${err.message}`; 100 | logStream.write(errorMessage); 101 | if (process.send) { 102 | process.send({ log: errorMessage }); // Send error message to parent process 103 | } 104 | debugToFile(`From geth.js: ${errorMessage}`); 105 | }); 106 | 107 | process.on("SIGINT", () => { 108 | execution.kill("SIGINT"); 109 | }); 110 | -------------------------------------------------------------------------------- /ethereum_client_scripts/install.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { execSync } from "child_process"; 4 | import os from "os"; 5 | import { installDir } from "../commandLineOptions.js"; 6 | import { debugToFile } from "./../helpers.js"; 7 | 8 | export const latestGethVer = "1.15.11"; 9 | export const latestRethVer = "1.4.3"; 10 | export const latestLighthouseVer = "7.0.1"; 11 | 12 | export function installMacLinuxClient(clientName, platform) { 13 | const arch = os.arch(); 14 | 15 | const gethHash = { 16 | "1.14.3": "ab48ba42", 17 | "1.14.12": "293a300d", 18 | "1.15.10": "2bf8a789", 19 | "1.15.11": "36b2371c", 20 | }; 21 | 22 | const configs = { 23 | darwin: { 24 | x64: { 25 | geth: `geth-darwin-amd64-${latestGethVer}-${gethHash[latestGethVer]}`, 26 | reth: `reth-v${latestRethVer}-x86_64-apple-darwin`, 27 | lighthouse: `lighthouse-v${latestLighthouseVer}-x86_64-apple-darwin`, 28 | prysm: "prysm.sh", 29 | }, 30 | arm64: { 31 | geth: `geth-darwin-arm64-${latestGethVer}-${gethHash[latestGethVer]}`, 32 | reth: `reth-v${latestRethVer}-aarch64-apple-darwin`, 33 | lighthouse: `lighthouse-v${latestLighthouseVer}-x86_64-apple-darwin`, 34 | prysm: "prysm.sh", 35 | }, 36 | }, 37 | linux: { 38 | x64: { 39 | geth: `geth-linux-amd64-${latestGethVer}-${gethHash[latestGethVer]}`, 40 | reth: `reth-v${latestRethVer}-x86_64-unknown-linux-gnu`, 41 | lighthouse: `lighthouse-v${latestLighthouseVer}-x86_64-unknown-linux-gnu`, 42 | prysm: "prysm.sh", 43 | }, 44 | arm64: { 45 | geth: `geth-linux-arm64-${latestGethVer}-${gethHash[latestGethVer]}`, 46 | reth: `reth-v${latestRethVer}-aarch64-unknown-linux-gnu`, 47 | lighthouse: `lighthouse-v${latestLighthouseVer}-aarch64-unknown-linux-gnu`, 48 | prysm: "prysm.sh", 49 | }, 50 | }, 51 | }; 52 | 53 | const fileName = configs[platform][arch][clientName]; 54 | const clientDir = path.join(installDir, "ethereum_clients", clientName); 55 | const clientScript = path.join( 56 | clientDir, 57 | clientName === "prysm" ? "prysm.sh" : clientName 58 | ); 59 | 60 | if (!fs.existsSync(clientScript)) { 61 | console.log(`\nInstalling ${clientName}.`); 62 | if (!fs.existsSync(clientDir)) { 63 | console.log(`Creating '${clientDir}'`); 64 | fs.mkdirSync(`${clientDir}/database`, { recursive: true }); 65 | fs.mkdirSync(`${clientDir}/logs`, { recursive: true }); 66 | } 67 | 68 | const downloadUrls = { 69 | geth: `https://gethstore.blob.core.windows.net/builds/${fileName}.tar.gz`, 70 | reth: `https://github.com/paradigmxyz/reth/releases/download/v${latestRethVer}/${fileName}.tar.gz`, 71 | lighthouse: `https://github.com/sigp/lighthouse/releases/download/v${latestLighthouseVer}/${fileName}.tar.gz`, 72 | prysm: 73 | "https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.sh", 74 | }; 75 | 76 | if (clientName === "prysm") { 77 | console.log("Downloading Prysm."); 78 | execSync( 79 | `cd "${clientDir}" && curl -L -O -# ${downloadUrls.prysm} && chmod +x prysm.sh`, 80 | { stdio: "inherit" } 81 | ); 82 | } else { 83 | console.log(`Downloading ${clientName}.`); 84 | execSync( 85 | `cd "${clientDir}" && curl -L -O -# ${downloadUrls[clientName]}`, 86 | { stdio: "inherit" } 87 | ); 88 | console.log(`Uncompressing ${clientName}.`); 89 | execSync(`cd "${clientDir}" && tar -xzvf "${fileName}.tar.gz"`, { 90 | stdio: "inherit", 91 | }); 92 | 93 | if (clientName === "geth") { 94 | execSync(`cd "${clientDir}/${fileName}" && mv geth ..`, { 95 | stdio: "inherit", 96 | }); 97 | execSync(`cd "${clientDir}" && rm -r "${fileName}"`, { 98 | stdio: "inherit", 99 | }); 100 | } 101 | 102 | console.log(`Cleaning up ${clientName} directory.`); 103 | execSync(`cd "${clientDir}" && rm "${fileName}.tar.gz"`, { 104 | stdio: "inherit", 105 | }); 106 | } 107 | } else { 108 | console.log(`${clientName} is already installed.`); 109 | } 110 | } 111 | 112 | export function getVersionNumber(client) { 113 | const platform = os.platform(); 114 | let clientCommand; 115 | let argument; 116 | let versionOutput; 117 | let versionMatch; 118 | 119 | if (client === "reth" || client === "lighthouse" || client === "geth") { 120 | argument = "--version"; 121 | } else if (client === "prysm") { 122 | argument = "beacon-chain --version"; 123 | } 124 | 125 | if (["darwin", "linux"].includes(platform)) { 126 | clientCommand = path.join( 127 | installDir, 128 | "ethereum_clients", 129 | `${client}`, 130 | client === "prysm" ? `${client}.sh` : `${client}` 131 | ); 132 | } else if (platform === "win32") { 133 | console.log("getVersionNumber() for windows is yet not implemented"); 134 | process.exit(1); 135 | } 136 | 137 | try { 138 | const versionCommand = execSync( 139 | `${clientCommand} ${argument} 2>/dev/null`, 140 | { 141 | encoding: "utf-8", 142 | stdio: ["pipe", "pipe", "ignore"], 143 | } 144 | ); 145 | versionOutput = versionCommand.trim(); 146 | 147 | if (client === "reth") { 148 | versionMatch = versionOutput.match( 149 | /reth(?:-ethereum-cli)? Version: (\d+\.\d+\.\d+)/ 150 | ); 151 | } else if (client === "lighthouse") { 152 | versionMatch = versionOutput.match(/Lighthouse v(\d+\.\d+\.\d+)/); 153 | } else if (client === "geth") { 154 | versionMatch = versionOutput.match(/geth version (\d+\.\d+\.\d+)/); 155 | } else if (client === "prysm") { 156 | versionMatch = versionOutput.match(/beacon-chain-v(\d+\.\d+\.\d+)-/); 157 | } 158 | 159 | const parsedVersion = versionMatch ? versionMatch[1] : null; 160 | 161 | if (parsedVersion) { 162 | return parsedVersion; 163 | } else { 164 | debugToFile(`Unable to parse version number for ${client}`); 165 | return null; 166 | } 167 | } catch (error) { 168 | debugToFile(`Error getting version for ${client}:`, error.message); 169 | return null; 170 | } 171 | } 172 | 173 | export function compareClientVersions(client, installedVersion) { 174 | let isLatest = true; 175 | let latestVersion; 176 | 177 | if (client === "reth") { 178 | latestVersion = latestRethVer; 179 | } else if (client === "geth") { 180 | latestVersion = latestGethVer; 181 | } else if (client === "lighthouse") { 182 | latestVersion = latestLighthouseVer; 183 | } 184 | if (compareVersions(installedVersion, latestVersion) < 0) { 185 | isLatest = false; 186 | } 187 | return [isLatest, latestVersion]; 188 | } 189 | 190 | export function removeClient(client) { 191 | const clientDir = path.join(installDir, "ethereum_clients", client, client); 192 | if (fs.existsSync(clientDir)) { 193 | fs.rmSync(clientDir, { recursive: true }); 194 | } 195 | } 196 | 197 | function compareVersions(v1, v2) { 198 | const parts1 = v1.split(".").map(Number); 199 | const parts2 = v2.split(".").map(Number); 200 | 201 | for (let i = 0; i < 3; i++) { 202 | if (parts1[i] > parts2[i]) return 1; 203 | if (parts1[i] < parts2[i]) return -1; 204 | } 205 | 206 | return 0; 207 | } 208 | -------------------------------------------------------------------------------- /ethereum_client_scripts/lighthouse.js: -------------------------------------------------------------------------------- 1 | import pty from "node-pty"; 2 | import fs from "fs"; 3 | import os from "os"; 4 | import path from "path"; 5 | import { debugToFile } from "../helpers.js"; 6 | import { stripAnsiCodes, getFormattedDateTime } from "../helpers.js"; 7 | import minimist from "minimist"; 8 | 9 | let installDir = os.homedir(); 10 | let consensusCheckpoint = "https://mainnet-checkpoint-sync.stakely.io/"; 11 | let bgConsensusPeers; 12 | let bgConsensusAddrs; 13 | 14 | const argv = minimist(process.argv.slice(2)); 15 | 16 | const consensusPeerPorts = argv.consensuspeerports 17 | .split(",") 18 | .map((port) => parseInt(port.trim(), 10)); 19 | 20 | // Check if a different install directory was provided via the `--directory` option 21 | if (argv.directory) { 22 | installDir = argv.directory; 23 | } 24 | 25 | if (argv.consensuscheckpoint) { 26 | consensusCheckpoint = argv.consensuscheckpoint; 27 | } 28 | 29 | if (argv.bgconsensuspeers) { 30 | bgConsensusPeers = argv.bgconsensuspeers 31 | .split(",") 32 | .map((peer) => peer.trim()) 33 | .filter((peer) => peer) // Filter out any empty strings 34 | .join(","); 35 | } 36 | 37 | if (argv.bgconsensusaddrs) { 38 | bgConsensusAddrs = argv.bgconsensusaddrs; 39 | } 40 | 41 | const jwtPath = path.join(installDir, "ethereum_clients", "jwt", "jwt.hex"); 42 | 43 | let lighthouseCommand; 44 | const platform = os.platform(); 45 | if (["darwin", "linux"].includes(platform)) { 46 | lighthouseCommand = path.join( 47 | installDir, 48 | "ethereum_clients", 49 | "lighthouse", 50 | "lighthouse" 51 | ); 52 | } else if (platform === "win32") { 53 | lighthouseCommand = path.join( 54 | installDir, 55 | "ethereum_clients", 56 | "lighthouse", 57 | "lighthouse.exe" 58 | ); 59 | } 60 | 61 | const logFilePath = path.join( 62 | installDir, 63 | "ethereum_clients", 64 | "lighthouse", 65 | "logs", 66 | `lighthouse_${getFormattedDateTime()}.log` 67 | ); 68 | 69 | const logStream = fs.createWriteStream(logFilePath, { flags: "a" }); 70 | 71 | const consensusArgs = [ 72 | "bn", 73 | "--network", 74 | "mainnet", 75 | "--port", 76 | consensusPeerPorts[0], 77 | "--quic-port", 78 | consensusPeerPorts[1], 79 | "--execution-endpoint", 80 | "http://localhost:8551", 81 | "--checkpoint-sync-url", 82 | consensusCheckpoint, 83 | "--checkpoint-sync-url-timeout", 84 | "1200", 85 | "--disable-deposit-contract-sync", 86 | "--datadir", 87 | path.join(installDir, "ethereum_clients", "lighthouse", "database"), 88 | "--execution-jwt", 89 | `${jwtPath}`, 90 | "--metrics", 91 | "--metrics-address", 92 | "127.0.0.1", 93 | "--metrics-port", 94 | "5054", 95 | "--http", 96 | "--disable-upnp", // There is currently a bug in the p2p-lib that causes panics with this enabled 97 | "--disable-enr-auto-update", // This is causing a loop of ENR updates that crashes lighthouse 98 | // "--libp2p-addresses", 99 | // "/ip4/76.155.211.156/tcp/26617/p2p/16Uiu2HAkw5RWctJguL1CPRyvgwuF4GsqTKUBW7qXdNrX3t6k4CH9,/ip4/76.155.211.156/udp/26617/quic-v1/p2p/16Uiu2HAkw5RWctJguL1CPRyvgwuF4GsqTKUBW7qXdNrX3t6k4CH9,/ip4/76.155.211.156/tcp/10000/p2p/16Uiu2HAmT4mjLEPrwStrRvorexA3rH9FLJLS367N1KJYUCPWTSio,/ip4/76.155.211.156/udp/10000/quic-v1/p2p/16Uiu2HAmT4mjLEPrwStrRvorexA3rH9FLJLS367N1KJYUCPWTSio", 100 | // "/ip4/76.155.211.156/tcp/9000/p2p/16Uiu2HAkw5RWctJguL1CPRyvgwuF4GsqTKUBW7qXdNrX3t6k4CH9,/ip4/76.155.211.156/udp/9001/quic-v1/p2p/16Uiu2HAkw5RWctJguL1CPRyvgwuF4GsqTKUBW7qXdNrX3t6k4CH9,/ip4/76.155.211.156/tcp/10000/p2p/16Uiu2HAmT4mjLEPrwStrRvorexA3rH9FLJLS367N1KJYUCPWTSio,/ip4/76.155.211.156/udp/10001/quic-v1/p2p/16Uiu2HAmT4mjLEPrwStrRvorexA3rH9FLJLS367N1KJYUCPWTSio,/ip4/140.228.255.200/tcp/9000/p2p/16Uiu2HAmUxRVA7mHdJdt8QeauaiFU9ifHUuqANs6BAPcU3nWbyAu,/ip4/140.228.255.200/udp/9001/quic-v1/p2p/16Uiu2HAmUxRVA7mHdJdt8QeauaiFU9ifHUuqANs6BAPcU3nWbyAu", 101 | ]; 102 | 103 | if (argv.bgconsensuspeers) { 104 | debugToFile(`Lighthouse: Added Trusted BG Peers: ${bgConsensusPeers}`); 105 | consensusArgs.push("--trusted-peers", bgConsensusPeers); 106 | } 107 | 108 | if (argv.bgconsensusaddrs) { 109 | debugToFile(`Lighthouse: Added BG Peer Addresses: ${bgConsensusAddrs}`); 110 | consensusArgs.push("--libp2p-addresses", bgConsensusAddrs); 111 | } 112 | 113 | const consensus = pty.spawn(`${lighthouseCommand}`, consensusArgs, { 114 | name: "xterm-color", 115 | cols: 80, 116 | rows: 30, 117 | cwd: process.env.HOME, 118 | env: { ...process.env, INSTALL_DIR: installDir }, 119 | }); 120 | 121 | // Pipe stdout and stderr to the log file and to the parent process 122 | consensus.on("data", (data) => { 123 | logStream.write(stripAnsiCodes(data)); 124 | if (process.send) { 125 | process.send({ log: data }); // No need for .toString(), pty preserves colors 126 | } 127 | }); 128 | 129 | consensus.on("exit", (code) => { 130 | // const exitMessage = `prysm process exited with code ${code}`; 131 | // logStream.write(exitMessage); 132 | logStream.end(); 133 | }); 134 | 135 | consensus.on("error", (err) => { 136 | const errorMessage = `Error: ${err.message}`; 137 | logStream.write(errorMessage); 138 | if (process.send) { 139 | process.send({ log: errorMessage }); // Send error message to parent process 140 | } 141 | debugToFile(`From lighthouse.js: ${errorMessage}`); 142 | }); 143 | 144 | process.on("SIGINT", () => { 145 | consensus.kill("SIGINT"); 146 | }); 147 | -------------------------------------------------------------------------------- /ethereum_client_scripts/prysm.js: -------------------------------------------------------------------------------- 1 | import pty from "node-pty"; 2 | import fs from "fs"; 3 | import os from "os"; 4 | import path from "path"; 5 | import { debugToFile } from "../helpers.js"; 6 | import { stripAnsiCodes, getFormattedDateTime } from "../helpers.js"; 7 | import minimist from "minimist"; 8 | 9 | let installDir = os.homedir(); 10 | let consensusCheckpoint = "https://mainnet-checkpoint-sync.attestant.io/"; 11 | let bgConsensusAddrs; 12 | 13 | const argv = minimist(process.argv.slice(2)); 14 | 15 | const consensusPeerPorts = argv.consensuspeerports 16 | .split(",") 17 | .map((port) => parseInt(port.trim(), 10)); 18 | 19 | // Check if a different install directory was provided via the `--directory` option 20 | if (argv.directory) { 21 | installDir = argv.directory; 22 | } 23 | 24 | if (argv.consensuscheckpoint) { 25 | consensusCheckpoint = argv.consensuscheckpoint; 26 | } 27 | 28 | if (argv.bgconsensusaddrs) { 29 | bgConsensusAddrs = argv.bgconsensusaddrs 30 | .split(",") 31 | .map((addr) => addr.trim()); 32 | } 33 | 34 | const jwtPath = path.join(installDir, "ethereum_clients", "jwt", "jwt.hex"); 35 | 36 | let prysmCommand; 37 | const platform = os.platform(); 38 | if (["darwin", "linux"].includes(platform)) { 39 | prysmCommand = path.join(installDir, "ethereum_clients", "prysm", "prysm.sh"); 40 | } else if (platform === "win32") { 41 | prysmCommand = path.join( 42 | installDir, 43 | "ethereum_clients", 44 | "prysm", 45 | "prysm.exe" 46 | ); 47 | } 48 | 49 | const logFilePath = path.join( 50 | installDir, 51 | "ethereum_clients", 52 | "prysm", 53 | "logs", 54 | `prysm_${getFormattedDateTime()}.log` 55 | ); 56 | 57 | const logStream = fs.createWriteStream(logFilePath, { flags: "a" }); 58 | 59 | const consensusArgs = [ 60 | "beacon-chain", 61 | "--mainnet", 62 | "--p2p-udp-port", 63 | consensusPeerPorts[1], 64 | "--p2p-quic-port", 65 | consensusPeerPorts[0], 66 | "--p2p-tcp-port", 67 | consensusPeerPorts[0], 68 | "--execution-endpoint", 69 | "http://localhost:8551", 70 | "--grpc-gateway-host=0.0.0.0", 71 | "--grpc-gateway-port=5052", 72 | `--checkpoint-sync-url=${consensusCheckpoint}`, 73 | `--genesis-beacon-api-url=${consensusCheckpoint}`, 74 | "--datadir", 75 | path.join(installDir, "ethereum_clients", "prysm", "database"), 76 | "--accept-terms-of-use=true", 77 | "--jwt-secret", 78 | jwtPath, 79 | "--monitoring-host", 80 | "127.0.0.1", 81 | "--monitoring-port", 82 | "5054", 83 | // "--peer", 84 | // "enr:-MK4QFKbF8xjEtSUT8mGKGHujC-NrlgX_-FPF0PuMmeZYzuePneu7Kf78RMhY0XyDOMb9mfOd7GwS_XSeC1LeCM81tyGAZIRObQZh2F0dG5ldHOIABgAAAAAAACEZXRoMpBqlaGpBQAAAP__________gmlkgnY0gmlwhAoAAEiJc2VjcDI1NmsxoQIxBPPTLz6I7hjG94FZDpSfm4UzdJPKjs2zB7OmGCs2dIhzeW5jbmV0cwCDdGNwghueg3VkcIIbOQ", 85 | ]; 86 | 87 | if (argv.bgconsensusaddrs) { 88 | bgConsensusAddrs.forEach((peer) => { 89 | debugToFile(`Prysm: Adding BG peer: ${peer}`); 90 | consensusArgs.push("--peer", peer); 91 | }); 92 | } 93 | 94 | const consensus = pty.spawn(`${prysmCommand}`, consensusArgs, { 95 | name: "xterm-color", 96 | cols: 80, 97 | rows: 30, 98 | cwd: process.env.HOME, 99 | env: { ...process.env, INSTALL_DIR: installDir }, 100 | }); 101 | 102 | // Pipe stdout and stderr to the log file and to the parent process 103 | consensus.on("data", (data) => { 104 | logStream.write(stripAnsiCodes(data)); 105 | if (process.send) { 106 | process.send({ log: data }); // No need for .toString(), pty preserves colors 107 | } 108 | }); 109 | 110 | consensus.on("exit", (code) => { 111 | // const exitMessage = `prysm process exited with code ${code}`; 112 | // logStream.write(exitMessage); 113 | logStream.end(); 114 | }); 115 | 116 | consensus.on("error", (err) => { 117 | const errorMessage = `Error: ${err.message}`; 118 | logStream.write(errorMessage); 119 | if (process.send) { 120 | process.send({ log: errorMessage }); // Send error message to parent process 121 | } 122 | debugToFile(`From prysm.js: ${errorMessage}`); 123 | }); 124 | 125 | process.on("SIGINT", () => { 126 | consensus.kill("SIGINT"); 127 | }); 128 | -------------------------------------------------------------------------------- /ethereum_client_scripts/reth.js: -------------------------------------------------------------------------------- 1 | import pty from "node-pty"; 2 | import fs from "fs"; 3 | import os from "os"; 4 | import path from "path"; 5 | import { debugToFile } from "../helpers.js"; 6 | import { stripAnsiCodes, getFormattedDateTime } from "../helpers.js"; 7 | import minimist from "minimist"; 8 | 9 | let installDir = os.homedir(); 10 | 11 | const argv = minimist(process.argv.slice(2)); 12 | 13 | const executionPeerPort = argv.executionpeerport; 14 | 15 | const executionType = argv.executiontype; 16 | 17 | // Check if a different install directory was provided via the `--directory` option 18 | if (argv.directory) { 19 | installDir = argv.directory; 20 | } 21 | 22 | const jwtPath = path.join(installDir, "ethereum_clients", "jwt", "jwt.hex"); 23 | 24 | let rethCommand; 25 | 26 | const platform = os.platform(); 27 | if (["darwin", "linux"].includes(platform)) { 28 | rethCommand = path.join(installDir, "ethereum_clients", "reth", "reth"); 29 | } else if (platform === "win32") { 30 | rethCommand = path.join(installDir, "ethereum_clients", "reth", "reth.exe"); 31 | } 32 | 33 | const logFilePath = path.join( 34 | installDir, 35 | "ethereum_clients", 36 | "reth", 37 | "logs", 38 | `reth_${getFormattedDateTime()}.log` 39 | ); 40 | 41 | const logStream = fs.createWriteStream(logFilePath, { flags: "a" }); 42 | 43 | const execution = pty.spawn( 44 | `${rethCommand}`, 45 | [ 46 | "node", 47 | ...(executionType === "archive" ? [] : ["--full"]), 48 | "--port", 49 | executionPeerPort, 50 | "--discovery.port", 51 | executionPeerPort, 52 | "--http", 53 | "--http.addr", 54 | "0.0.0.0", 55 | "--http.api", 56 | "eth,net,admin", 57 | "--authrpc.addr", 58 | "127.0.0.1", 59 | "--authrpc.port", 60 | "8551", 61 | "--datadir", 62 | path.join(installDir, "ethereum_clients", "reth", "database"), 63 | "--authrpc.jwtsecret", 64 | `${jwtPath}`, 65 | "--metrics", 66 | "127.0.0.1:9001", 67 | "--ws", 68 | "--ws.api", 69 | "eth,net,admin", 70 | "--ws.origins", 71 | "*", 72 | "--ws.addr", 73 | "127.0.0.1", 74 | "--ws.port", 75 | "8546", 76 | ], 77 | { 78 | name: "xterm-color", 79 | cols: 80, 80 | rows: 30, 81 | cwd: process.env.HOME, 82 | env: { ...process.env, INSTALL_DIR: installDir }, 83 | } 84 | ); 85 | 86 | // Pipe stdout and stderr to the log file and to the parent process 87 | execution.on("data", (data) => { 88 | logStream.write(stripAnsiCodes(data)); 89 | if (process.send) { 90 | process.send({ log: data }); 91 | } 92 | }); 93 | 94 | execution.on("exit", (code) => { 95 | // const exitMessage = `geth process exited with code ${code}`; 96 | // logStream.write(exitMessage); 97 | logStream.end(); 98 | }); 99 | 100 | execution.on("error", (err) => { 101 | const errorMessage = `Error: ${err.message}`; 102 | logStream.write(errorMessage); 103 | if (process.send) { 104 | process.send({ log: errorMessage }); // Send error message to parent process 105 | } 106 | debugToFile(`From reth.js: ${errorMessage}`); 107 | }); 108 | 109 | process.on("SIGINT", () => { 110 | execution.kill("SIGINT"); 111 | }); 112 | -------------------------------------------------------------------------------- /ethereum_clients/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/buidlguidl-client/3240645e94c9022aabed12cb9455200380aabced/ethereum_clients/.gitkeep -------------------------------------------------------------------------------- /getSystemStats.js: -------------------------------------------------------------------------------- 1 | import si from "systeminformation"; 2 | import { debugToFile } from "./helpers.js"; 3 | import axios from "axios"; 4 | import macaddress from "macaddress"; 5 | 6 | export function getMemoryUsage() { 7 | return new Promise((resolve, reject) => { 8 | si.mem() 9 | .then((memory) => { 10 | const totalMemory = memory.total; 11 | const usedMemory = memory.active; // 'active' is usually what's actually used 12 | const memoryUsagePercent = (usedMemory / totalMemory) * 100; 13 | resolve(memoryUsagePercent.toFixed(1)); // Return memory usage as a percentage 14 | }) 15 | .catch((error) => { 16 | debugToFile(`getMemoryUsage(): ${error}`); 17 | reject(error); 18 | }); 19 | }); 20 | } 21 | 22 | export function getCpuUsage() { 23 | return new Promise((resolve, reject) => { 24 | si.currentLoad() 25 | .then((load) => { 26 | const currentLoad = load.currentLoad; 27 | resolve(currentLoad); 28 | }) 29 | .catch((error) => { 30 | debugToFile(`getCpuUsage(): ${error}`); 31 | reject(error); 32 | }); 33 | }); 34 | } 35 | 36 | export function getDiskUsage(installDir) { 37 | return new Promise((resolve, reject) => { 38 | si.fsSize() 39 | .then((drives) => { 40 | let diskUsagePercent = 0; 41 | 42 | // Sort drives by the length of their mount point, descending 43 | drives.sort((a, b) => b.mount.length - a.mount.length); 44 | 45 | // Find the drive with the longest mount point that is a prefix of installDir 46 | const installDrive = drives.find((drive) => { 47 | return installDir.startsWith(drive.mount); 48 | }); 49 | 50 | if (installDrive) { 51 | // debugToFile(`Drive info: ${JSON.stringify(installDrive, null, 2)}`); 52 | 53 | if (process.platform === "darwin") { 54 | // macOS (installDrive.use is weird on macOS) 55 | diskUsagePercent = 56 | ((installDrive.size - installDrive.available) / 57 | installDrive.size) * 58 | 100; 59 | } else { 60 | // Linux and others 61 | diskUsagePercent = installDrive.use; 62 | } 63 | } else { 64 | debugToFile( 65 | `getDiskUsage(): Drive for ${installDir} not found.`, 66 | () => {} 67 | ); 68 | } 69 | 70 | resolve(diskUsagePercent.toFixed(1)); 71 | }) 72 | .catch((error) => { 73 | debugToFile(`getDiskUsage(): ${error}`); 74 | reject(error); 75 | }); 76 | }); 77 | } 78 | 79 | export function getCpuTemperature() { 80 | return new Promise((resolve, reject) => { 81 | si.cpuTemperature() 82 | .then((data) => { 83 | // debugToFile(`CPU data: ${JSON.stringify(data, null, 2)}`); 84 | 85 | const cpuTemp = data.main; 86 | if (cpuTemp !== null && cpuTemp !== undefined) { 87 | resolve(cpuTemp.toFixed(1)); // Return CPU temperature as a fixed-point number 88 | } else { 89 | resolve(0); 90 | } 91 | }) 92 | .catch((error) => { 93 | debugToFile(`Error fetching CPU temperature: ${error}`); 94 | reject(error); 95 | }); 96 | }); 97 | } 98 | 99 | export async function getPublicIPAddress() { 100 | while (true) { 101 | try { 102 | const response = await axios.get("https://api.ipify.org?format=json"); 103 | return response.data.ip; 104 | } catch (error) { 105 | debugToFile(`Error fetching public IP address: ${error}`); 106 | await new Promise((resolve) => setTimeout(resolve, 5000)); 107 | } 108 | } 109 | } 110 | 111 | export function getMacAddress() { 112 | return new Promise((resolve, reject) => { 113 | macaddress.all((err, all) => { 114 | if (err) { 115 | reject(`Error getting MAC address: ${err}`); 116 | return; 117 | } 118 | 119 | // Get the first non-internal MAC address 120 | let macAddress = null; 121 | for (const interfaceName in all) { 122 | const mac = all[interfaceName].mac; 123 | if (mac && mac !== "00:00:00:00:00:00") { 124 | macAddress = mac; 125 | break; 126 | } 127 | } 128 | 129 | resolve(macAddress); 130 | }); 131 | }); 132 | } 133 | -------------------------------------------------------------------------------- /helpers.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | import { dirname } from "path"; 5 | 6 | export function setupDebugLogging(debugLogPath) { 7 | if (fs.existsSync(debugLogPath)) { 8 | fs.unlinkSync(debugLogPath); 9 | } 10 | 11 | function logDebug(message) { 12 | if (typeof message === "object") { 13 | message = JSON.stringify(message, null, 2); 14 | } 15 | fs.appendFileSync( 16 | debugLogPath, 17 | `[${new Date().toISOString()}] ${message}\n` 18 | ); 19 | } 20 | 21 | console.log = function (message, ...optionalParams) { 22 | if (optionalParams.length > 0) { 23 | message += 24 | " " + 25 | optionalParams 26 | .map((param) => 27 | typeof param === "object" ? JSON.stringify(param, null, 2) : param 28 | ) 29 | .join(" "); 30 | } 31 | logDebug(message); 32 | }; 33 | } 34 | 35 | const __filename = fileURLToPath(import.meta.url); 36 | const __dirname = dirname(__filename); 37 | const debugLogPath = path.join(__dirname, "debug.log"); 38 | 39 | export function debugToFile(data) { 40 | const now = new Date(); 41 | const timestamp = `${now.toLocaleDateString()} ${now.toLocaleTimeString()}`; 42 | 43 | let content; 44 | if (typeof data === "object" && data !== null) { 45 | content = `${timestamp} - ${JSON.stringify(data, null, 2)}\n`; 46 | } else { 47 | content = `${timestamp} - ${data}\n`; 48 | } 49 | 50 | fs.appendFile(debugLogPath, content, (err) => { 51 | if (err) { 52 | console.error("Error writing to log file:", err); 53 | } 54 | }); 55 | } 56 | 57 | export function stripAnsiCodes(input) { 58 | return input.replace( 59 | /[\u001b\u009b][[()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nq-uy=><~])/g, 60 | "" 61 | ); 62 | } 63 | 64 | export function getFormattedDateTime() { 65 | const now = new Date(); 66 | 67 | return now 68 | .toISOString() 69 | .replace(/T/, "_") 70 | .replace(/\..+/, "") 71 | .replace(/:/g, "_"); 72 | } 73 | 74 | // let proxyUrl = ""; 75 | 76 | // function setProxyUrl() { 77 | // try { 78 | // const currentBranch = execSync("git rev-parse --abbrev-ref HEAD") 79 | // .toString() 80 | // .trim(); 81 | // proxyUrl = 82 | // currentBranch === "main" 83 | // ? "rpc.buidlguidl.com" 84 | // : "stage.rpc.buidlguidl.com"; 85 | // } catch (error) { 86 | // console.error("Error getting Git branch:", error); 87 | // proxyUrl = "stage.rpc.buidlguidl.com"; // Default to stage if there's an error 88 | // } 89 | // } 90 | 91 | // export function getProxyUrl() { 92 | // return proxyUrl; 93 | // } 94 | 95 | // // Call setProxyUrl when the module is imported 96 | // setProxyUrl(); 97 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { execSync, spawn } from "child_process"; 2 | import os from "os"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | import { fileURLToPath } from "url"; 6 | import { dirname } from "path"; 7 | import { initializeMonitoring } from "./monitor.js"; 8 | import { installMacLinuxClient } from "./ethereum_client_scripts/install.js"; 9 | import { initializeWebSocketConnection } from "./web_socket_connection/webSocketConnection.js"; 10 | import { 11 | executionClient, 12 | executionType, 13 | consensusClient, 14 | executionPeerPort, 15 | consensusPeerPorts, 16 | consensusCheckpoint, 17 | installDir, 18 | owner, 19 | saveOptionsToFile, 20 | deleteOptionsFile, 21 | } from "./commandLineOptions.js"; 22 | import { 23 | fetchBGExecutionPeers, 24 | configureBGExecutionPeers, 25 | fetchBGConsensusPeers, 26 | configureBGConsensusPeers, 27 | } from "./ethereum_client_scripts/configureBGPeers.js"; 28 | import { getVersionNumber } from "./ethereum_client_scripts/install.js"; 29 | import { debugToFile } from "./helpers.js"; 30 | 31 | const __filename = fileURLToPath(import.meta.url); 32 | const __dirname = dirname(__filename); 33 | 34 | const lockFilePath = path.join(installDir, "ethereum_clients", "script.lock"); 35 | 36 | // const CONFIG = { 37 | // debugLogPath: path.join(installDir, "ethereum_clients", "debugIndex.log"), 38 | // }; 39 | 40 | function createJwtSecret(jwtDir) { 41 | if (!fs.existsSync(jwtDir)) { 42 | console.log(`\nCreating '${jwtDir}'`); 43 | fs.mkdirSync(jwtDir, { recursive: true }); 44 | } 45 | 46 | if (!fs.existsSync(`${jwtDir}/jwt.hex`)) { 47 | console.log("Generating JWT.hex file."); 48 | execSync(`cd "${jwtDir}" && openssl rand -hex 32 > jwt.hex`, { 49 | stdio: "inherit", 50 | }); 51 | } 52 | } 53 | 54 | let executionChild; 55 | let consensusChild; 56 | 57 | let executionExited = false; 58 | let consensusExited = false; 59 | 60 | let isExiting = false; 61 | 62 | function handleExit(exitType) { 63 | if (isExiting) return; // Prevent multiple calls 64 | 65 | // Check if the current process PID matches the one in the lockfile 66 | try { 67 | const lockFilePid = fs.readFileSync(lockFilePath, "utf8"); 68 | if (parseInt(lockFilePid) !== process.pid) { 69 | console.log( 70 | `This client process (${process.pid}) is not the first instance launched. Closing dashboard view without killing clients.` 71 | ); 72 | process.exit(0); 73 | } 74 | } catch (error) { 75 | console.error("Error reading lockfile:", error); 76 | process.exit(1); 77 | } 78 | 79 | isExiting = true; 80 | 81 | console.log(`\n\n🛰️ Received exit signal: ${exitType}\n`); 82 | 83 | deleteOptionsFile(); 84 | debugToFile(`handleExit(): deleteOptionsFile() has been called`); 85 | 86 | try { 87 | // Check if both child processes have exited 88 | const checkExit = () => { 89 | if (executionExited && consensusExited) { 90 | console.log("\n👍 Both clients exited!"); 91 | removeLockFile(); 92 | process.exit(0); 93 | } 94 | }; 95 | 96 | // Handle execution client exit 97 | const handleExecutionExit = (code) => { 98 | if (!executionExited) { 99 | executionExited = true; 100 | console.log(`🫡 Execution client exited with code ${code}`); 101 | checkExit(); 102 | } 103 | }; 104 | 105 | // Handle consensus client exit 106 | const handleConsensusExit = (code) => { 107 | if (!consensusExited) { 108 | consensusExited = true; 109 | console.log(`🫡 Consensus client exited with code ${code}`); 110 | checkExit(); 111 | } 112 | }; 113 | 114 | // Handle execution client close 115 | const handleExecutionClose = (code) => { 116 | if (!executionExited) { 117 | executionExited = true; 118 | console.log(`🫡 Execution client closed with code ${code}`); 119 | checkExit(); 120 | } 121 | }; 122 | 123 | // Handle consensus client close 124 | const handleConsensusClose = (code) => { 125 | if (!consensusExited) { 126 | consensusExited = true; 127 | console.log(`🫡 Consensus client closed with code ${code}`); 128 | checkExit(); 129 | } 130 | }; 131 | 132 | // Ensure event listeners are set before killing the processes 133 | if (executionChild && !executionExited) { 134 | executionChild.on("exit", handleExecutionExit); 135 | executionChild.on("close", handleExecutionClose); 136 | } else { 137 | executionExited = true; 138 | } 139 | 140 | if (consensusChild && !consensusExited) { 141 | consensusChild.on("exit", handleConsensusExit); 142 | consensusChild.on("close", handleConsensusClose); 143 | } else { 144 | consensusExited = true; 145 | } 146 | 147 | // Send the kill signals after setting the event listeners 148 | if (executionChild && !executionExited) { 149 | console.log("⌛️ Exiting execution client..."); 150 | setTimeout(() => { 151 | executionChild.kill("SIGINT"); 152 | }, 750); 153 | } 154 | 155 | if (consensusChild && !consensusExited) { 156 | console.log("⌛️ Exiting consensus client..."); 157 | setTimeout(() => { 158 | consensusChild.kill("SIGINT"); 159 | }, 750); 160 | } 161 | 162 | // Initial check in case both children are already not running 163 | checkExit(); 164 | 165 | // Periodically check if both child processes have exited 166 | const intervalId = setInterval(() => { 167 | checkExit(); 168 | // Clear interval if both clients have exited 169 | if (executionExited && consensusExited) { 170 | clearInterval(intervalId); 171 | } 172 | }, 1000); 173 | } catch (error) { 174 | console.log("Error from handleExit()", error); 175 | } 176 | } 177 | 178 | // Modify existing listeners 179 | process.on("SIGINT", () => handleExit("SIGINT")); 180 | process.on("SIGTERM", () => handleExit("SIGTERM")); 181 | process.on("SIGHUP", () => handleExit("SIGHUP")); 182 | process.on("SIGUSR2", () => handleExit("SIGUSR2")); 183 | 184 | // Modify the exit listener 185 | process.on("exit", (code) => { 186 | if (!isExiting) { 187 | handleExit("exit"); 188 | } 189 | }); 190 | 191 | // This helps catch uncaught exceptions 192 | process.on("uncaughtException", (error) => { 193 | console.error("Uncaught Exception:", error); 194 | handleExit("uncaughtException"); 195 | }); 196 | 197 | // This helps catch unhandled promise rejections 198 | process.on("unhandledRejection", (reason, promise) => { 199 | console.error("Unhandled Rejection at:", promise, "reason:", reason); 200 | handleExit("unhandledRejection"); 201 | }); 202 | 203 | let bgConsensusPeers = []; 204 | let bgConsensusAddrs; 205 | 206 | async function startClient(clientName, executionType, installDir) { 207 | let clientCommand, 208 | clientArgs = []; 209 | 210 | if (clientName === "geth") { 211 | clientArgs.push("--executionpeerport", executionPeerPort); 212 | clientArgs.push("--executiontype", executionType); 213 | clientCommand = path.join(__dirname, "ethereum_client_scripts/geth.js"); 214 | } else if (clientName === "reth") { 215 | clientArgs.push("--executionpeerport", executionPeerPort); 216 | clientArgs.push("--executiontype", executionType); 217 | clientCommand = path.join(__dirname, "ethereum_client_scripts/reth.js"); 218 | } else if (clientName === "prysm") { 219 | bgConsensusPeers = await fetchBGConsensusPeers(); 220 | bgConsensusAddrs = await configureBGConsensusPeers(consensusClient); 221 | 222 | if (bgConsensusPeers.length > 0) { 223 | clientArgs.push("--bgconsensuspeers", bgConsensusPeers); 224 | } 225 | 226 | if (bgConsensusAddrs != null) { 227 | clientArgs.push("--bgconsensusaddrs", bgConsensusAddrs); 228 | } 229 | 230 | if (consensusCheckpoint != null) { 231 | clientArgs.push("--consensuscheckpoint", consensusCheckpoint); 232 | } 233 | 234 | clientArgs.push("--consensuspeerports", consensusPeerPorts); 235 | 236 | clientCommand = path.join(__dirname, "ethereum_client_scripts/prysm.js"); 237 | } else if (clientName === "lighthouse") { 238 | bgConsensusPeers = await fetchBGConsensusPeers(); 239 | bgConsensusAddrs = await configureBGConsensusPeers(consensusClient); 240 | 241 | if (bgConsensusPeers.length > 0) { 242 | clientArgs.push("--bgconsensuspeers", bgConsensusPeers); 243 | } 244 | 245 | if (bgConsensusAddrs != null) { 246 | clientArgs.push("--bgconsensusaddrs", bgConsensusAddrs); 247 | } 248 | 249 | if (consensusCheckpoint != null) { 250 | clientArgs.push("--consensuscheckpoint", consensusCheckpoint); 251 | } 252 | clientArgs.push("--consensuspeerports", consensusPeerPorts); 253 | 254 | clientCommand = path.join( 255 | __dirname, 256 | "ethereum_client_scripts/lighthouse.js" 257 | ); 258 | } else { 259 | clientCommand = path.join( 260 | installDir, 261 | "ethereum_clients", 262 | clientName, 263 | clientName 264 | ); 265 | } 266 | 267 | clientArgs.push("--directory", installDir); 268 | 269 | const child = spawn("node", [clientCommand, ...clientArgs], { 270 | stdio: ["inherit", "pipe", "inherit"], 271 | cwd: process.env.HOME, 272 | env: { ...process.env, INSTALL_DIR: installDir }, 273 | }); 274 | 275 | if (clientName === "geth" || clientName === "reth") { 276 | executionChild = child; 277 | } else if (clientName === "prysm" || clientName === "lighthouse") { 278 | consensusChild = child; 279 | } 280 | 281 | child.on("exit", (code) => { 282 | console.log(`🫡 ${clientName} process exited with code ${code}`); 283 | if (clientName === "geth" || clientName === "reth") { 284 | executionExited = true; 285 | } else if (clientName === "prysm" || clientName === "lighthouse") { 286 | consensusExited = true; 287 | } 288 | }); 289 | 290 | child.on("error", (err) => { 291 | console.log(`Error from start client: ${err.message}`); 292 | }); 293 | 294 | console.log(clientName, "started"); 295 | 296 | child.stdout.on("error", (err) => { 297 | console.error(`Error on stdout of ${clientName}: ${err.message}`); 298 | }); 299 | } 300 | 301 | function isAlreadyRunning() { 302 | try { 303 | if (fs.existsSync(lockFilePath)) { 304 | const pid = fs.readFileSync(lockFilePath, "utf8"); 305 | try { 306 | process.kill(pid, 0); 307 | return true; 308 | } catch (e) { 309 | if (e.code === "ESRCH") { 310 | fs.unlinkSync(lockFilePath); 311 | return false; 312 | } 313 | throw e; 314 | } 315 | } 316 | return false; 317 | } catch (err) { 318 | console.error("Error checking for existing process:", err); 319 | return false; 320 | } 321 | } 322 | 323 | function createLockFile() { 324 | fs.writeFileSync(lockFilePath, process.pid.toString(), "utf8"); 325 | // console.log(process.pid.toString()) 326 | } 327 | 328 | function removeLockFile() { 329 | if (fs.existsSync(lockFilePath)) { 330 | fs.unlinkSync(lockFilePath); 331 | } 332 | } 333 | 334 | const jwtDir = path.join(installDir, "ethereum_clients", "jwt"); 335 | const platform = os.platform(); 336 | 337 | if (["darwin", "linux"].includes(platform)) { 338 | installMacLinuxClient(executionClient, platform); 339 | installMacLinuxClient(consensusClient, platform); 340 | } 341 | // } else if (platform === "win32") { 342 | // installWindowsExecutionClient(executionClient); 343 | // installWindowsConsensusClient(consensusClient); 344 | // } 345 | 346 | let messageForHeader = ""; 347 | let runsClient = false; 348 | 349 | createJwtSecret(jwtDir); 350 | 351 | const executionClientVer = getVersionNumber(executionClient); 352 | const consensusClientVer = getVersionNumber(consensusClient); 353 | 354 | const wsConfig = { 355 | executionClient: executionClient, 356 | consensusClient: consensusClient, 357 | executionClientVer: executionClientVer, 358 | consensusClientVer: consensusClientVer, 359 | }; 360 | 361 | if (!isAlreadyRunning()) { 362 | deleteOptionsFile(); 363 | createLockFile(); 364 | 365 | await startClient(executionClient, executionType, installDir); 366 | await startClient(consensusClient, executionType, installDir); 367 | 368 | if (owner !== null) { 369 | initializeWebSocketConnection(wsConfig); 370 | } 371 | 372 | runsClient = true; 373 | saveOptionsToFile(); 374 | } else { 375 | messageForHeader = "Dashboard View (client already running)"; 376 | runsClient = false; 377 | // Initialize WebSocket connection for secondary instances too 378 | if (owner !== null) { 379 | initializeWebSocketConnection(wsConfig); 380 | } 381 | } 382 | 383 | initializeMonitoring( 384 | messageForHeader, 385 | executionClient, 386 | consensusClient, 387 | executionClientVer, 388 | consensusClientVer, 389 | runsClient 390 | ); 391 | 392 | let bgExecutionPeers = []; 393 | 394 | setTimeout(async () => { 395 | bgExecutionPeers = await fetchBGExecutionPeers(); 396 | await configureBGExecutionPeers(bgExecutionPeers); 397 | }, 10000); 398 | 399 | export { bgExecutionPeers, bgConsensusPeers }; 400 | -------------------------------------------------------------------------------- /monitor.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import blessed from "blessed"; 3 | import contrib from "blessed-contrib"; 4 | import { debugToFile } from "./helpers.js"; 5 | import { createSystemStatsGauge } from "./monitor_components/systemStatsGauge.js"; 6 | import { createPeerCountGauge } from "./monitor_components/peerCountGauge.js"; 7 | import { createCpuLine } from "./monitor_components/cpuLine.js"; 8 | import { createNetworkLine } from "./monitor_components/networkLine.js"; 9 | import { createDiskLine } from "./monitor_components/diskLine.js"; 10 | import { createRethStageGauge } from "./monitor_components/rethStageGauge.js"; 11 | import { createGethStageGauge } from "./monitor_components/gethStageGauge.js"; 12 | import { createChainInfoBox } from "./monitor_components/chainInfoBox.js"; 13 | import { createRpcInfoBox } from "./monitor_components/rpcInfoBox.js"; 14 | import { createExecutionLog } from "./monitor_components/executionLog.js"; 15 | import { createStatusBox } from "./monitor_components/statusBox.js"; 16 | import { installDir } from "./commandLineOptions.js"; 17 | 18 | import { 19 | loadProgress, 20 | getLatestLogFile, 21 | } from "./monitor_components/helperFunctions.js"; 22 | 23 | import { 24 | createBandwidthBox, 25 | setBandwidthBox, 26 | startBandwidthMonitoring, 27 | } from "./monitor_components/bandwidthGauge.js"; 28 | 29 | import { 30 | setupLogStreaming, 31 | showHideRethWidgets, 32 | showHideGethWidgets, 33 | } from "./monitor_components/updateLogic.js"; 34 | 35 | import { createConsensusLog } from "./monitor_components/consensusLog.js"; 36 | import { createHeader } from "./monitor_components/header.js"; 37 | 38 | let executionClientGlobal; 39 | let consensusClientGlobal; 40 | 41 | export let statusBox = null; 42 | export let chainInfoBox = null; 43 | export let rpcInfoBox = null; 44 | export let screen = null; 45 | 46 | export async function initializeMonitoring( 47 | messageForHeader, 48 | executionClient, 49 | consensusClient, 50 | executionClientVer, 51 | consensusClientVer, 52 | runsClient 53 | ) { 54 | try { 55 | executionClientGlobal = executionClient; 56 | consensusClientGlobal = consensusClient; 57 | let progress; 58 | 59 | if (executionClient == "geth") { 60 | progress = loadProgress(); 61 | } 62 | 63 | const { screen, components } = setupUI( 64 | progress, 65 | messageForHeader, 66 | executionClientVer, 67 | consensusClientVer, 68 | runsClient 69 | ); 70 | 71 | const executionLogsPath = path.join( 72 | installDir, 73 | "ethereum_clients", 74 | executionClient, 75 | "logs" 76 | ); 77 | 78 | const consensusLogsPath = path.join( 79 | installDir, 80 | "ethereum_clients", 81 | consensusClient, 82 | "logs" 83 | ); 84 | 85 | const logFilePathExecution = path.join( 86 | executionLogsPath, 87 | await getLatestLogFile(executionLogsPath, executionClient) 88 | ); 89 | 90 | // debugToFile( 91 | // `Monitoring ${executionClient} logs from: ${logFilePathExecution}`, 92 | // () => {} 93 | // ); 94 | 95 | setTimeout(() => { 96 | const logFilePathConsensus = path.join( 97 | consensusLogsPath, 98 | getLatestLogFile(consensusLogsPath, consensusClient) 99 | ); 100 | 101 | setupLogStreaming( 102 | consensusClientGlobal, 103 | logFilePathConsensus, 104 | components.consensusLog, 105 | screen, 106 | components.gethStageGauge 107 | ); 108 | 109 | // debugToFile( 110 | // `Monitoring ${consensusClient} logs from: ${logFilePathConsensus}`, 111 | // () => {} 112 | // ); 113 | }, 3000); 114 | 115 | setInterval(() => { 116 | showHideRethWidgets( 117 | screen, 118 | components.rethStageGauge, 119 | components.chainInfoBox, 120 | components.rpcInfoBox 121 | ); 122 | }, 5000); 123 | 124 | setupLogStreaming( 125 | executionClientGlobal, 126 | logFilePathExecution, 127 | components.executionLog, 128 | screen, 129 | components.gethStageGauge 130 | ); 131 | 132 | if (executionClient == "reth") { 133 | setInterval(() => { 134 | showHideRethWidgets( 135 | screen, 136 | components.rethStageGauge, 137 | components.chainInfoBox, 138 | components.rpcInfoBox 139 | ); 140 | }, 5000); 141 | } else if (executionClient == "geth") { 142 | setInterval(() => { 143 | showHideGethWidgets( 144 | screen, 145 | components.gethStageGauge, 146 | components.chainInfoBox, 147 | components.rpcInfoBox 148 | ); 149 | }, 5000); 150 | } 151 | } catch (error) { 152 | debugToFile(`Error initializing monitoring: ${error}`); 153 | } 154 | } 155 | 156 | function setupUI( 157 | progress, 158 | messageForHeader, 159 | executionClientVer, 160 | consensusClientVer, 161 | runsClient 162 | ) { 163 | screen = blessed.screen(); 164 | suppressMouseOutput(screen); 165 | // const grid = new contrib.grid({ rows: 9, cols: 10, screen: screen }); 166 | const grid = new contrib.grid({ rows: 9, cols: 9, screen: screen }); 167 | 168 | let executionClientLabel; 169 | let consensusClientLabel; 170 | 171 | if (executionClientGlobal == "geth") { 172 | executionClientLabel = `Geth v${executionClientVer}`; 173 | } else if (executionClientGlobal == "reth") { 174 | executionClientLabel = `Reth v${executionClientVer}`; 175 | } 176 | 177 | if (consensusClientGlobal == "prysm") { 178 | consensusClientLabel = `Prysm v${consensusClientVer}`; 179 | } else if (consensusClientGlobal == "lighthouse") { 180 | consensusClientLabel = `Lighthouse v${consensusClientVer}`; 181 | } 182 | 183 | const executionLog = createExecutionLog(grid, executionClientLabel, screen); 184 | const consensusLog = createConsensusLog(grid, consensusClientLabel, screen); 185 | const systemStatsGauge = createSystemStatsGauge(grid, installDir); 186 | const peerCountGauge = createPeerCountGauge(grid); 187 | const cpuLine = createCpuLine(grid, screen); 188 | const networkLine = createNetworkLine(grid, screen); 189 | const diskLine = createDiskLine(grid, screen, installDir); 190 | statusBox = createStatusBox(grid); 191 | const bandwidthBox = createBandwidthBox(grid); 192 | chainInfoBox = createChainInfoBox(grid); 193 | rpcInfoBox = createRpcInfoBox(grid); 194 | 195 | let gethStageGauge, rethStageGauge; 196 | 197 | if (executionClientGlobal == "geth") { 198 | gethStageGauge = createGethStageGauge(grid); 199 | } else if (executionClientGlobal == "reth") { 200 | rethStageGauge = createRethStageGauge(grid); 201 | } 202 | 203 | const { pic, bigText, ipAddressBox } = createHeader( 204 | grid, 205 | screen, 206 | messageForHeader 207 | ); 208 | 209 | screen.append(executionLog); 210 | screen.append(consensusLog); 211 | screen.append(cpuLine); 212 | screen.append(networkLine); 213 | screen.append(diskLine); 214 | screen.append(systemStatsGauge); 215 | screen.append(peerCountGauge); 216 | screen.append(statusBox); 217 | screen.append(bandwidthBox); 218 | if (executionClientGlobal == "geth") { 219 | screen.append(gethStageGauge); 220 | } else if (executionClientGlobal == "reth") { 221 | screen.append(rethStageGauge); 222 | } 223 | 224 | setBandwidthBox(bandwidthBox); 225 | startBandwidthMonitoring(screen); 226 | 227 | function fixBottomMargins(screen) { 228 | try { 229 | let executionLogBottom = executionLog.top + executionLog.height - 1; 230 | let executionLogGap = consensusLog.top - executionLogBottom - 1; 231 | if (executionLogGap != 0) { 232 | executionLog.height = executionLog.height + executionLogGap; 233 | } 234 | 235 | let statusBoxBottom = statusBox.top + statusBox.height - 1; 236 | let statusBoxGap = peerCountGauge.top - statusBoxBottom - 1; 237 | if (statusBoxGap != 0) { 238 | statusBox.height = statusBox.height + statusBoxGap; 239 | } 240 | 241 | let peerCountGaugeBottom = peerCountGauge.top + peerCountGauge.height - 1; 242 | let peerCountGaugeGap = bandwidthBox.top - peerCountGaugeBottom - 1; 243 | if (peerCountGaugeGap != 0) { 244 | peerCountGauge.height = peerCountGauge.height + peerCountGaugeGap; 245 | } 246 | 247 | let bandwidthBoxBottom = bandwidthBox.top + bandwidthBox.height - 1; 248 | let bandwidthBoxGap = systemStatsGauge.top - bandwidthBoxBottom - 1; 249 | if (bandwidthBoxGap != 0) { 250 | bandwidthBox.height = bandwidthBox.height + bandwidthBoxGap; 251 | } 252 | 253 | let consensusLogBottom = consensusLog.top + consensusLog.height - 1; 254 | let consensusLogGap = cpuLine.top - consensusLogBottom - 1; 255 | if (consensusLogGap != 0) { 256 | consensusLog.height = consensusLog.height + consensusLogGap; 257 | } 258 | 259 | if (screen.children.includes(rethStageGauge)) { 260 | let rethStageGaugeBottom = 261 | rethStageGauge.top + rethStageGauge.height - 1; 262 | let rethStageGaugeGap = cpuLine.top - rethStageGaugeBottom - 1; 263 | if (rethStageGaugeGap != 0) { 264 | rethStageGauge.height = rethStageGauge.height + rethStageGaugeGap; 265 | } 266 | } 267 | 268 | if (screen.children.includes(gethStageGauge)) { 269 | let gethStageGaugeBottom = 270 | gethStageGauge.top + gethStageGauge.height - 1; 271 | let gethStageGaugeGap = cpuLine.top - gethStageGaugeBottom - 1; 272 | if (gethStageGaugeGap != 0) { 273 | gethStageGauge.height = gethStageGauge.height + gethStageGaugeGap; 274 | } 275 | } 276 | 277 | let chainInfoBoxGap; 278 | 279 | if (screen.children.includes(chainInfoBox)) { 280 | let chainInfoBoxBottom = chainInfoBox.top + chainInfoBox.height - 1; 281 | if (screen.children.includes(rpcInfoBox)) { 282 | chainInfoBoxGap = rpcInfoBox.top - chainInfoBoxBottom - 1; 283 | } else { 284 | chainInfoBoxGap = diskLine.top - chainInfoBoxBottom - 1; 285 | } 286 | if (chainInfoBoxGap != 0) { 287 | chainInfoBox.height = chainInfoBox.height + chainInfoBoxGap; 288 | } 289 | } 290 | 291 | if (screen.children.includes(rpcInfoBox)) { 292 | let rpcInfoBoxBottom = rpcInfoBox.top + rpcInfoBox.height - 1; 293 | let rpcInfoBoxGap = diskLine.top - rpcInfoBoxBottom - 1; 294 | if (rpcInfoBoxGap != 0) { 295 | rpcInfoBox.height = rpcInfoBox.height + rpcInfoBoxGap; 296 | } 297 | } 298 | 299 | let systemStatsGaugeBottom = 300 | systemStatsGauge.top + systemStatsGauge.height - 1; 301 | let systemStatsGaugeGap = cpuLine.top - systemStatsGaugeBottom - 1; 302 | if (systemStatsGaugeGap != 0) { 303 | systemStatsGauge.height = systemStatsGauge.height + systemStatsGaugeGap; 304 | } 305 | 306 | let cpuLineBottom = cpuLine.top + cpuLine.height - 1; 307 | let cpuLineGap = screen.height - cpuLineBottom - 1; 308 | if (cpuLineGap != 0) { 309 | cpuLine.height = cpuLine.height + cpuLineGap; 310 | } 311 | 312 | let networkLineBottom = networkLine.top + networkLine.height - 1; 313 | let networkLineGap = screen.height - networkLineBottom - 1; 314 | if (networkLineGap != 0) { 315 | networkLine.height = networkLine.height + networkLineGap; 316 | } 317 | 318 | let diskLineBottom = diskLine.top + diskLine.height - 1; 319 | let diskLineGap = screen.height - diskLineBottom - 1; 320 | if (diskLineGap != 0) { 321 | diskLine.height = diskLine.height + diskLineGap; 322 | } 323 | } catch (error) { 324 | debugToFile(`fixBottomMargins(): ${error}`); 325 | } 326 | } 327 | 328 | function fixRightMargins(screen) { 329 | try { 330 | // let bigTextRight = bigText.left + bigText.width - 1; 331 | // let bigTextGap = ipAddressBox.left - bigTextRight - 1; 332 | // if (bigTextGap != 0) { 333 | // bigTextGap.width = bigTextGap.width + bigTextGap; 334 | // } 335 | 336 | let ipAddressBoxRight = ipAddressBox.left + ipAddressBox.width - 1; 337 | let ipAddressBoxGap = screen.width - ipAddressBoxRight - 1; 338 | if (ipAddressBoxGap != 0) { 339 | ipAddressBox.width = ipAddressBox.width + ipAddressBoxGap; 340 | } 341 | 342 | let statusBoxRight = statusBox.left + statusBox.width - 1; 343 | let statusBoxGap = screen.width - statusBoxRight - 1; 344 | if (statusBoxGap != 0) { 345 | statusBox.width = statusBox.width + statusBoxGap; 346 | } 347 | 348 | if (screen.children.includes(rethStageGauge)) { 349 | let rethStageGaugeRight = 350 | rethStageGauge.left + rethStageGauge.width - 1; 351 | let rethStageGaugeGap = peerCountGauge.left - rethStageGaugeRight - 1; 352 | if (rethStageGaugeGap != 0) { 353 | rethStageGauge.width = rethStageGauge.width + rethStageGaugeGap; 354 | } 355 | } 356 | 357 | if (screen.children.includes(gethStageGauge)) { 358 | let gethStageGaugeRight = 359 | gethStageGauge.left + gethStageGauge.width - 1; 360 | let gethStageGaugeGap = peerCountGauge.left - gethStageGaugeRight - 1; 361 | if (gethStageGaugeGap != 0) { 362 | gethStageGauge.width = gethStageGauge.width + gethStageGaugeGap; 363 | } 364 | } 365 | 366 | if (screen.children.includes(chainInfoBox)) { 367 | let chainInfoBoxRight = chainInfoBox.left + chainInfoBox.width - 1; 368 | let chainInfoBoxGap = peerCountGauge.left - chainInfoBoxRight - 1; 369 | if (chainInfoBoxGap != 0) { 370 | chainInfoBox.width = chainInfoBox.width + chainInfoBoxGap; 371 | } 372 | } 373 | 374 | if (screen.children.includes(rpcInfoBox)) { 375 | let rpcInfoBoxRight = rpcInfoBox.left + rpcInfoBox.width - 1; 376 | let rpcInfoBoxGap = peerCountGauge.left - rpcInfoBoxRight - 1; 377 | if (rpcInfoBoxGap != 0) { 378 | rpcInfoBox.width = rpcInfoBox.width + rpcInfoBoxGap; 379 | } 380 | } 381 | 382 | let peerCountGaugeRight = peerCountGauge.left + peerCountGauge.width - 1; 383 | let peerCountGaugeGap = screen.width - peerCountGaugeRight - 1; 384 | if (peerCountGaugeGap != 0) { 385 | peerCountGauge.width = peerCountGauge.width + peerCountGaugeGap; 386 | } 387 | 388 | let bandwidthBoxRight = bandwidthBox.left + bandwidthBox.width - 1; 389 | let bandwidthBoxGap = screen.width - bandwidthBoxRight - 1; 390 | if (bandwidthBoxGap != 0) { 391 | bandwidthBox.width = bandwidthBox.width + bandwidthBoxGap; 392 | } 393 | 394 | let systemStatsGaugeRight = 395 | systemStatsGauge.left + systemStatsGauge.width - 1; 396 | let systemStatsGaugeGap = screen.width - systemStatsGaugeRight - 1; 397 | if (systemStatsGaugeGap != 0) { 398 | systemStatsGauge.width = systemStatsGauge.width + systemStatsGaugeGap; 399 | } 400 | 401 | let cpuLineRight = cpuLine.left + cpuLine.width - 1; 402 | let cpuLineGap = networkLine.left - cpuLineRight - 1; 403 | if (cpuLineGap != 0) { 404 | cpuLine.width = cpuLine.width + cpuLineGap; 405 | } 406 | 407 | let networkLineRight = networkLine.left + networkLine.width - 1; 408 | let networkLineGap = diskLine.left - networkLineRight - 1; 409 | if (networkLineGap != 0) { 410 | networkLine.width = networkLine.width + networkLineGap; 411 | } 412 | 413 | let diskLineRight = diskLine.left + diskLine.width - 1; 414 | let diskLineGap = screen.width - diskLineRight - 1; 415 | if (diskLineGap != 0) { 416 | diskLine.width = diskLine.width + diskLineGap; 417 | } 418 | } catch (error) { 419 | debugToFile(`fixRightMargins(): ${error}`); 420 | } 421 | } 422 | 423 | screen.render(); 424 | 425 | setTimeout(() => { 426 | fixBottomMargins(screen); 427 | fixRightMargins(screen); 428 | 429 | cpuLine.emit("attach"); 430 | networkLine.emit("attach"); 431 | diskLine.emit("attach"); 432 | 433 | screen.render(); 434 | }, 250); 435 | 436 | screen.on("resize", () => { 437 | fixBottomMargins(screen); 438 | fixRightMargins(screen); 439 | 440 | cpuLine.emit("attach"); 441 | networkLine.emit("attach"); 442 | diskLine.emit("attach"); 443 | executionLog.emit("attach"); 444 | consensusLog.emit("attach"); 445 | 446 | screen.render(); 447 | }); 448 | 449 | screen.key(["escape", "q", "C-c"], function (ch, key) { 450 | if (runsClient) { 451 | process.kill(process.pid, "SIGUSR2"); 452 | console.log("Clients exited from monitor"); 453 | } else { 454 | process.exit(0); 455 | } 456 | screen.destroy(); 457 | }); 458 | 459 | return { 460 | screen, 461 | components: { 462 | executionLog, 463 | consensusLog, 464 | gethStageGauge, 465 | rethStageGauge, 466 | chainInfoBox, 467 | rpcInfoBox, 468 | }, 469 | }; 470 | } 471 | 472 | function suppressMouseOutput(screen) { 473 | screen.on("element mouse", (el, data) => { 474 | if (data.button === "mouseup" || data.button === "mousedown") { 475 | return false; 476 | } 477 | }); 478 | 479 | screen.on("keypress", (ch, key) => { 480 | if ( 481 | key.name === "up" || 482 | key.name === "down" || 483 | key.name === "left" || 484 | key.name === "right" || 485 | (key.name === "r" && (key.meta || key.ctrl)) 486 | ) { 487 | if (!key.ctrl && !key.meta && !key.shift) { 488 | return false; 489 | } 490 | } 491 | }); 492 | } 493 | -------------------------------------------------------------------------------- /monitor_components/bandwidthGauge.js: -------------------------------------------------------------------------------- 1 | import si from "systeminformation"; 2 | import blessed from "blessed"; 3 | import { debugToFile } from "../helpers.js"; 4 | 5 | let bandwidthBox; 6 | const interval = 60 * 1000; // 1 minute in milliseconds 7 | 8 | let history24 = []; 9 | let history7 = []; 10 | 11 | let previousStats = null; 12 | 13 | let displayMode = "daily"; 14 | let lastSwitchTime = 0; 15 | 16 | async function getNetworkStats() { 17 | try { 18 | const networkStats = await si.networkStats(); 19 | const totalSent = networkStats.reduce( 20 | (sum, iface) => sum + iface.tx_bytes, 21 | 0 22 | ); 23 | const totalReceived = networkStats.reduce( 24 | (sum, iface) => sum + iface.rx_bytes, 25 | 0 26 | ); 27 | return { timestamp: Date.now(), sent: totalSent, received: totalReceived }; 28 | } catch (error) { 29 | debugToFile(`Failed to get network stats: ${error}`); 30 | return { timestamp: Date.now(), sent: 0, received: 0 }; 31 | } 32 | } 33 | 34 | function updateHistory(stats) { 35 | const currentTime = Date.now(); 36 | 37 | if (previousStats) { 38 | const sentDiff = stats.sent - previousStats.sent; 39 | const receivedDiff = stats.received - previousStats.received; 40 | 41 | // Add new entry with timestamp 42 | history24.push({ 43 | timestamp: currentTime, 44 | sent: sentDiff, 45 | received: receivedDiff, 46 | }); 47 | history7.push({ 48 | timestamp: currentTime, 49 | sent: sentDiff, 50 | received: receivedDiff, 51 | }); 52 | 53 | // Remove old entries 54 | history24 = history24.filter( 55 | (entry) => currentTime - entry.timestamp <= 24 * 60 * 60 * 1000 56 | ); 57 | history7 = history7.filter( 58 | (entry) => currentTime - entry.timestamp <= 7 * 24 * 60 * 60 * 1000 59 | ); 60 | } 61 | previousStats = stats; 62 | } 63 | 64 | function calculateStatsForPeriod(history) { 65 | return history.reduce( 66 | (acc, entry) => ({ 67 | sent: acc.sent + entry.sent, 68 | received: acc.received + entry.received, 69 | }), 70 | { sent: 0, received: 0 } 71 | ); 72 | } 73 | 74 | function formatBytes(bytes) { 75 | const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; 76 | if (bytes === 0) return "0 Byte"; 77 | const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); 78 | return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + " " + sizes[i]; 79 | } 80 | 81 | async function updateBandwidthBox(screen) { 82 | try { 83 | const stats = await getNetworkStats(); 84 | updateHistory(stats); 85 | 86 | const dailyStats = calculateStatsForPeriod(history24); 87 | const weeklyStats = calculateStatsForPeriod(history7); 88 | 89 | const dailyText = `{red-fg}▲ 1D: ${formatBytes( 90 | dailyStats.sent 91 | )}\n{blue-fg}▼ 1D: ${formatBytes(dailyStats.received)}`; 92 | const weeklyText = `{red-fg}▲ 7D: ${formatBytes( 93 | weeklyStats.sent 94 | )}\n{blue-fg}▼ 7D: ${formatBytes(weeklyStats.received)}`; 95 | 96 | const currentTime = Date.now(); 97 | if (currentTime - lastSwitchTime >= 10000) { 98 | displayMode = displayMode === "daily" ? "weekly" : "daily"; 99 | lastSwitchTime = currentTime; 100 | } 101 | 102 | let formattedText; 103 | if (bandwidthBox.height < 6) { 104 | formattedText = displayMode === "daily" ? dailyText : weeklyText; 105 | } else { 106 | formattedText = `${dailyText}\n${weeklyText}`; 107 | } 108 | 109 | bandwidthBox.setContent(formattedText); 110 | screen.render(); 111 | } catch (error) { 112 | debugToFile(`Failed to update bandwidth box: ${error}`); 113 | } 114 | } 115 | 116 | export function setBandwidthBox(box) { 117 | bandwidthBox = box; 118 | } 119 | 120 | export function startBandwidthMonitoring(screen) { 121 | async function scheduleNextUpdate() { 122 | const startTime = Date.now(); 123 | await updateBandwidthBox(screen); 124 | const endTime = Date.now(); 125 | const elapsedTime = endTime - startTime; 126 | const nextInterval = Math.max(interval - elapsedTime, 0); 127 | setTimeout(scheduleNextUpdate, nextInterval); 128 | } 129 | 130 | scheduleNextUpdate(); 131 | } 132 | 133 | export function createBandwidthBox(grid) { 134 | // const row = screen.height < layoutHeightThresh ? 3 : 6; 135 | // const rowSpan = screen.height < layoutHeightThresh ? 2 : 1; 136 | 137 | // const box = grid.set(3, 9, 2, 1, blessed.box, { 138 | const box = grid.set(4, 8, 1, 1, blessed.box, { 139 | label: "Bandwidth Usage", 140 | style: { 141 | fg: "blue", 142 | }, 143 | border: { 144 | type: "line", 145 | fg: "cyan", 146 | }, 147 | content: "Calculating...", 148 | tags: true, 149 | }); 150 | return box; 151 | } 152 | 153 | export { updateBandwidthBox }; 154 | -------------------------------------------------------------------------------- /monitor_components/chainInfoBox.js: -------------------------------------------------------------------------------- 1 | import blessed from "blessed"; 2 | import { localClient } from "./viemClients.js"; 3 | import { owner } from "../commandLineOptions.js"; 4 | import { debugToFile } from "../helpers.js"; 5 | 6 | let chainInfoBox; 7 | 8 | export function createChainInfoBox(grid) { 9 | let nRows = 5; 10 | if (owner !== null) { 11 | nRows = 3; 12 | } 13 | chainInfoBox = grid.set(2, 7, nRows, 1, blessed.box, { 14 | label: "Chain Info", 15 | stroke: "cyan", 16 | fill: "white", 17 | border: { 18 | type: "line", 19 | fg: "cyan", 20 | }, 21 | wrap: false, 22 | tags: true, 23 | }); 24 | 25 | return chainInfoBox; 26 | } 27 | 28 | // DAI and WETH contract addresses 29 | const DAI_CONTRACT_ADDRESS = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; 30 | const WETH_CONTRACT_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; 31 | 32 | // ABI to interact with the balanceOf function in an ERC-20 contract 33 | const ERC20_ABI = [ 34 | { 35 | constant: true, 36 | inputs: [{ name: "_owner", type: "address" }], 37 | name: "balanceOf", 38 | outputs: [{ name: "balance", type: "uint256" }], 39 | type: "function", 40 | }, 41 | ]; 42 | 43 | // Address to check 44 | const addressToCheck = "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11"; 45 | 46 | function formatBalance(balance, decimals = 18) { 47 | return (BigInt(balance) / BigInt(10 ** decimals)).toString(); 48 | } 49 | 50 | async function getEthPrice(blockNumber) { 51 | try { 52 | const daiBalance = await localClient.readContract({ 53 | address: DAI_CONTRACT_ADDRESS, 54 | abi: ERC20_ABI, 55 | functionName: "balanceOf", 56 | args: [addressToCheck], 57 | blockNumber: blockNumber, // Specify the block number here 58 | }); 59 | 60 | const wethBalance = await localClient.readContract({ 61 | address: WETH_CONTRACT_ADDRESS, 62 | abi: ERC20_ABI, 63 | functionName: "balanceOf", 64 | args: [addressToCheck], 65 | blockNumber: blockNumber, // Specify the block number here 66 | }); 67 | 68 | const ratio = formatBalance(daiBalance) / formatBalance(wethBalance); 69 | const roundedRatio = ratio.toFixed(2); 70 | 71 | return roundedRatio; 72 | } catch (error) { 73 | debugToFile(`Error fetching token balances: ${error}`); 74 | return null; // Return null or a default value in case of an error 75 | } 76 | } 77 | 78 | async function getBatchBlockInfo() { 79 | try { 80 | const nBlocks = Math.floor((chainInfoBox.height - 3) / 5); 81 | 82 | const currentBlockNumber = await localClient.getBlockNumber(); 83 | 84 | if (currentBlockNumber === 0n) { 85 | return { 86 | blockNumbers: [], 87 | transactionCounts: [], 88 | gasPrices: [], 89 | ethPrices: [], 90 | }; 91 | } 92 | 93 | // Create an array of block numbers for the most current block and the previous blocks 94 | const blockNumbers = []; 95 | for (let i = 0; i < nBlocks; i++) { 96 | const blockNumber = currentBlockNumber - BigInt(i); 97 | if (blockNumber < 0n) break; // Stop if block number is out of range 98 | blockNumbers.push(blockNumber); 99 | } 100 | 101 | // Fetch the blocks concurrently using Promise.all 102 | const blocks = await Promise.all( 103 | blockNumbers.map((blockNumber) => 104 | localClient.getBlock({ 105 | blockNumber: blockNumber, 106 | }) 107 | ) 108 | ); 109 | 110 | // Extract transaction counts, gas prices, and ETH prices from the blocks 111 | const transactionCounts = blocks.map((block) => block.transactions.length); 112 | const gasPrices = blocks.map( 113 | (block) => (Number(block.baseFeePerGas) / 10 ** 9).toFixed(4) // Convert gas prices to Gwei 114 | ); 115 | 116 | // Fetch ETH prices concurrently for each block 117 | const ethPrices = await Promise.all( 118 | blockNumbers.map((blockNumber) => getEthPrice(blockNumber)) 119 | ); 120 | 121 | return { blockNumbers, transactionCounts, gasPrices, ethPrices }; 122 | } catch (error) { 123 | debugToFile(`getBatchBlockInfo(): ${error}`); 124 | return { 125 | blockNumbers: [], 126 | transactionCounts: [], 127 | gasPrices: [], 128 | ethPrices: [], 129 | }; 130 | } 131 | } 132 | 133 | export async function populateChainInfoBox() { 134 | try { 135 | const { blockNumbers, transactionCounts, gasPrices, ethPrices } = 136 | await getBatchBlockInfo(); 137 | 138 | // Check if all arrays are empty 139 | if ( 140 | blockNumbers.length === 0 && 141 | transactionCounts.length === 0 && 142 | gasPrices.length === 0 && 143 | ethPrices.length === 0 144 | ) { 145 | chainInfoBox.setContent("INITIALIZING..."); 146 | return; 147 | } 148 | 149 | // Get the width of the chainInfoBox to properly format the separator line 150 | const boxWidth = chainInfoBox.width - 2; // Adjusting for border padding 151 | const separator = "-".repeat(boxWidth); 152 | 153 | let content = ""; 154 | content += separator + "\n"; 155 | 156 | for (let i = 0; i < blockNumbers.length; i++) { 157 | content += `{center}{bold}{green-fg}${blockNumbers[ 158 | i 159 | ].toLocaleString()}{/green-fg}{/bold}{/center}\n`; 160 | content += `{bold}{blue-fg}ETH $:{/blue-fg}{/bold} ${ethPrices[i]}\n`; 161 | content += `{bold}{blue-fg}GAS:{/blue-fg}{/bold} ${gasPrices[i]}\n`; 162 | content += `{bold}{blue-fg}# TX:{/blue-fg}{/bold} ${transactionCounts[i]}\n`; 163 | content += separator; 164 | 165 | if (i < blockNumbers.length - 1) { 166 | content += "\n"; 167 | } 168 | } 169 | 170 | chainInfoBox.setContent(content); 171 | } catch (error) { 172 | debugToFile(`populateChainInfoBox(): ${error}`); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /monitor_components/consensusLog.js: -------------------------------------------------------------------------------- 1 | import blessed from "blessed"; 2 | 3 | export function createConsensusLog(grid, consensusClientLabel, screen) { 4 | const consensusLog = grid.set(4, 0, 3, 7, blessed.box, { 5 | label: `${consensusClientLabel}`, 6 | content: `Loading ${consensusClientLabel} logs`, 7 | border: { 8 | type: "line", 9 | fg: "cyan", 10 | }, 11 | tags: true, 12 | shrink: true, 13 | wrap: true, 14 | }); 15 | 16 | return consensusLog; 17 | } 18 | -------------------------------------------------------------------------------- /monitor_components/cpuLine.js: -------------------------------------------------------------------------------- 1 | import contrib from "blessed-contrib"; 2 | import { debugToFile } from "../helpers.js"; 3 | import { getCpuUsage } from "../getSystemStats.js"; 4 | 5 | let cpuDataX = []; 6 | let dataCpuUsage = []; 7 | 8 | async function updateCpuLinePlot(cpuLine, screen) { 9 | try { 10 | const currentLoad = await getCpuUsage(); // Get the overall CPU load 11 | 12 | if (currentLoad === undefined || currentLoad === null) { 13 | throw new Error("Failed to fetch CPU usage data or data is empty"); 14 | } 15 | 16 | const now = new Date(); 17 | const timeLabel = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; 18 | 19 | if (!Array.isArray(cpuDataX)) { 20 | cpuDataX = []; 21 | } 22 | if (!Array.isArray(dataCpuUsage)) { 23 | dataCpuUsage = []; 24 | } 25 | 26 | cpuDataX.push(timeLabel); 27 | dataCpuUsage.push(currentLoad); 28 | 29 | // Prepare series data for the overall CPU load 30 | const series = [ 31 | { 32 | title: "", // Use an empty string for the title 33 | x: cpuDataX, 34 | y: dataCpuUsage, 35 | style: { line: "yellow" }, 36 | }, 37 | ]; 38 | 39 | cpuLine.setData(series); 40 | screen.render(); 41 | 42 | // Limit data history to the last 60 points 43 | if (cpuDataX.length > 60) { 44 | cpuDataX.shift(); 45 | dataCpuUsage.shift(); 46 | } 47 | } catch (error) { 48 | debugToFile( 49 | `updateCpuLinePlot() Failed to update CPU usage line chart: ${error}`, 50 | () => {} 51 | ); 52 | } 53 | } 54 | 55 | export function createCpuLine(grid, screen) { 56 | // const cpuLine = grid.set(7, 0, 2, 5, contrib.line, { 57 | const cpuLine = grid.set(7, 0, 2, 3, contrib.line, { 58 | style: { line: "blue", text: "green", baseline: "green" }, 59 | xLabelPadding: 3, 60 | xPadding: 5, 61 | showLegend: false, 62 | wholeNumbersOnly: false, 63 | label: "CPU Load (%)", 64 | border: { 65 | type: "line", 66 | fg: "cyan", 67 | }, 68 | }); 69 | 70 | setInterval(() => updateCpuLinePlot(cpuLine, screen), 1000); 71 | 72 | return cpuLine; 73 | } 74 | -------------------------------------------------------------------------------- /monitor_components/diskLine.js: -------------------------------------------------------------------------------- 1 | import contrib from "blessed-contrib"; 2 | import si from "systeminformation"; 3 | import { debugToFile } from "../helpers.js"; 4 | 5 | let diskDataX = []; 6 | let writeSpeedY = []; 7 | let readSpeedY = []; 8 | let lastStats = { 9 | totalWrite: undefined, 10 | totalRead: undefined, 11 | timestamp: Date.now(), 12 | }; 13 | let firstTime = true; 14 | 15 | function getDiskStats(installDir) { 16 | return new Promise((resolve, reject) => { 17 | si.fsStats() 18 | .then((data) => { 19 | si.fsSize() 20 | .then((drives) => { 21 | // Sort drives by the length of their mount point, descending 22 | drives.sort((a, b) => b.mount.length - a.mount.length); 23 | 24 | // Find the drive with the longest mount point that is a prefix of installDir 25 | const installDrive = drives.find((drive) => { 26 | return installDir.startsWith(drive.mount); 27 | }); 28 | 29 | if (installDrive) { 30 | // Use wx_sec and rx_sec for bytes written and read per second 31 | const writePerSecond = data.wx_sec / (1024 * 1024); // Convert to MB/s 32 | const readPerSecond = data.rx_sec / (1024 * 1024); // Convert to MB/s 33 | 34 | const result = { 35 | writePerSecond: writePerSecond, 36 | readPerSecond: readPerSecond, 37 | }; 38 | 39 | resolve(result); 40 | } else { 41 | reject(new Error(`Drive for ${installDir} not found.`)); 42 | } 43 | }) 44 | .catch((error) => { 45 | debugToFile(`Error fetching drive information: ${error}`); 46 | reject(error); 47 | }); 48 | }) 49 | .catch((error) => { 50 | debugToFile( 51 | `getDiskStats() Error fetching disk stats: ${error}`, 52 | () => {} 53 | ); 54 | reject(error); 55 | }); 56 | }); 57 | } 58 | 59 | async function updateDiskLinePlot(diskLine, screen, installDir) { 60 | try { 61 | const stats = await getDiskStats(installDir); 62 | const now = new Date(); 63 | diskDataX.push( 64 | now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds() 65 | ); 66 | writeSpeedY.push(stats.writePerSecond); 67 | readSpeedY.push(stats.readPerSecond); 68 | 69 | // debugToFile(`writeSpeedY: ${writeSpeedY}`); 70 | // debugToFile(`readSpeedY: ${readSpeedY}`); 71 | 72 | var seriesDiskWrite = { 73 | title: "I", 74 | x: diskDataX, 75 | y: writeSpeedY, 76 | style: { line: "magenta" }, 77 | }; 78 | var seriesDiskRead = { 79 | title: "O", 80 | x: diskDataX, 81 | y: readSpeedY, 82 | style: { line: "cyan" }, 83 | }; 84 | 85 | diskLine.setData([seriesDiskWrite, seriesDiskRead]); 86 | screen.render(); 87 | 88 | // Keep the data arrays from growing indefinitely 89 | if (diskDataX.length > 60) { 90 | diskDataX.shift(); 91 | writeSpeedY.shift(); 92 | readSpeedY.shift(); 93 | } 94 | } catch (error) { 95 | debugToFile(`updateDiskPlot(): ${error}`); 96 | } 97 | } 98 | 99 | export function createDiskLine(grid, screen, installDir) { 100 | const diskLine = grid.set(7, 6, 2, 3, contrib.line, { 101 | style: { line: "yellow", text: "green", baseline: "green" }, 102 | xLabelPadding: 0, 103 | xPadding: 0, 104 | showLegend: false, 105 | wholeNumbersOnly: false, 106 | label: 107 | "Disk I/O (MB/sec) [{magenta-fg}I{/magenta-fg} {cyan-fg}O{/cyan-fg}]", 108 | border: { 109 | type: "line", 110 | fg: "cyan", 111 | }, 112 | tags: true, 113 | }); 114 | 115 | setInterval(() => updateDiskLinePlot(diskLine, screen, installDir), 1000); 116 | 117 | return diskLine; 118 | } 119 | -------------------------------------------------------------------------------- /monitor_components/executionLog.js: -------------------------------------------------------------------------------- 1 | import blessed from "blessed"; 2 | 3 | export function createExecutionLog(grid, executionClientLabel, screen) { 4 | const executionLog = grid.set(1, 0, 3, 7, blessed.box, { 5 | label: `${executionClientLabel}`, 6 | content: `Loading ${executionClientLabel} logs`, 7 | border: { 8 | type: "line", 9 | fg: "cyan", 10 | }, 11 | tags: true, 12 | wrap: true, 13 | shrink: true, 14 | }); 15 | 16 | return executionLog; 17 | } 18 | -------------------------------------------------------------------------------- /monitor_components/gethStageGauge.js: -------------------------------------------------------------------------------- 1 | import blessed from "blessed"; 2 | import { debugToFile } from "../helpers.js"; 3 | 4 | let gethStageGauge; 5 | 6 | export function createGethStageGauge(grid) { 7 | gethStageGauge = grid.set(2, 7, 5, 1, blessed.box, { 8 | label: "Sync Progress", 9 | content: `INITIALIZING...`, 10 | stroke: "cyan", 11 | fill: "white", 12 | border: { 13 | type: "line", 14 | fg: "cyan", 15 | }, 16 | wrap: false, 17 | }); 18 | 19 | return gethStageGauge; 20 | } 21 | 22 | export function populateGethStageGauge(stagePercentages) { 23 | try { 24 | // Define the custom stage names inside the function 25 | const stageNames = ["HEADERS", "CHAIN", "STATE"]; 26 | 27 | // Get the width of the gethStageGauge box 28 | const boxWidth = gethStageGauge.width - 9; // Subtracting 9 for padding/border 29 | if (boxWidth > 0) { 30 | // Initialize the content string 31 | let content = ""; 32 | 33 | // Iterate over each stage's percentage and name 34 | stagePercentages.forEach((percentComplete, index) => { 35 | // Calculate the number of filled bars for this stage 36 | const filledBars = Math.floor(boxWidth * percentComplete); 37 | 38 | // Create the bar string 39 | const bar = "█".repeat(filledBars) + " ".repeat(boxWidth - filledBars); 40 | 41 | // Create the percentage string 42 | const percentString = `${Math.floor(percentComplete * 100)}%`; 43 | 44 | // Append the custom stage title, progress bar, and percentage to the content 45 | content += `${stageNames[index]}\n[${bar}] ${percentString}\n`; 46 | }); 47 | 48 | // Set the content of the gethStageGauge box 49 | gethStageGauge.setContent(content.trim()); 50 | 51 | // Render the screen to reflect the changes 52 | gethStageGauge.screen.render(); 53 | } 54 | } catch (error) { 55 | debugToFile(`populateGethStageGauge(): ${error}`); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /monitor_components/header.js: -------------------------------------------------------------------------------- 1 | import blessed from "blessed"; 2 | import os from "os"; 3 | import axios from "axios"; 4 | import path from "path"; 5 | import { fileURLToPath } from "url"; 6 | import { dirname } from "path"; 7 | import { debugToFile } from "../helpers.js"; 8 | import { execSync } from "child_process"; 9 | import { getPublicIPAddress } from "../getSystemStats.js"; 10 | import { owner } from "../commandLineOptions.js"; 11 | import { isConnected } from "../web_socket_connection/webSocketConnection.js"; 12 | 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = dirname(__filename); 15 | 16 | export function createHeader(grid, screen, messageForHeader) { 17 | // Function to get the local IP address 18 | async function getIPAddress() { 19 | while (true) { 20 | const interfaces = os.networkInterfaces(); 21 | for (const iface in interfaces) { 22 | for (const alias of interfaces[iface]) { 23 | if (alias.family === "IPv4" && !alias.internal) { 24 | return alias.address; 25 | } 26 | } 27 | } 28 | await new Promise((resolve) => setTimeout(resolve, 5000)); 29 | } 30 | } 31 | 32 | // New function to fetch points 33 | async function fetchPoints(owner) { 34 | try { 35 | const response = await axios.get( 36 | `https://pool.mainnet.rpc.buidlguidl.com:48546/yourpoints?owner=${owner}` 37 | ); 38 | return response.data.points; 39 | } catch (error) { 40 | debugToFile(`Error fetching points: ${error}`); 41 | return null; 42 | } 43 | } 44 | 45 | // New function to get the current Git branch 46 | function getCurrentBranch() { 47 | try { 48 | return execSync("git rev-parse --abbrev-ref HEAD").toString().trim(); 49 | } catch (error) { 50 | debugToFile(`Error getting current branch: ${error}`); 51 | return "unknown"; 52 | } 53 | } 54 | 55 | // Updated function to get the full Git commit hash 56 | function getCurrentCommitHash() { 57 | try { 58 | return execSync("git rev-parse HEAD").toString().trim(); 59 | } catch (error) { 60 | debugToFile(`Error getting current commit hash: ${error}`); 61 | return "unknown"; 62 | } 63 | } 64 | 65 | // Updated function to update bigText with points, branch name, and commit hash 66 | async function updatePointsAndBranchDisplay() { 67 | const points = await fetchPoints(owner); 68 | const currentBranch = getCurrentBranch(); 69 | const commitHash = getCurrentCommitHash(); 70 | if (owner !== null) { 71 | bigText.setContent( 72 | `{center}{bold}B u i d l G u i d l C l i e n t{/bold}{/center}\n` + 73 | `{center}Branch: ${currentBranch} (${commitHash}){/center}\n` + 74 | `{center}{cyan-fg}Owner: ${owner}{/cyan-fg} | {green-fg}Credits: ${points}{/green-fg}{/center}\n` + 75 | `{center}{cyan-fg}${messageForHeader}{/cyan-fg}{/center}` 76 | ); 77 | } else { 78 | bigText.setContent( 79 | `{center}{bold}B u i d l G u i d l C l i e n t{/bold}{/center}\n` + 80 | `{center}Branch: ${currentBranch} (${commitHash}){/center}\n` + 81 | `{center}{cyan-fg}${messageForHeader}{/cyan-fg}{/center}` 82 | ); 83 | } 84 | screen.render(); 85 | } 86 | 87 | let pic, logo; 88 | try { 89 | pic = grid.set(0, 0, 1, 2, blessed.box, { 90 | border: false, 91 | valign: "top", 92 | padding: { top: 0, bottom: 0, left: 0, right: 0 }, 93 | align: "center", 94 | }); 95 | 96 | const renderLogo = () => { 97 | const logoHeight = pic.height * 1.1; // Use the height of the pic box 98 | const logoWidth = logoHeight + logoHeight * 1.5; // Adjust width as needed 99 | const leftPosition = Math.floor((pic.width - logoWidth) / 2); 100 | 101 | // If logo already exists, remove it before adding a new one 102 | if (logo) { 103 | pic.remove(logo); 104 | } 105 | 106 | logo = blessed.image({ 107 | parent: pic, 108 | file: path.join(__dirname, "pixelBgLogo.png"), 109 | type: "ansi", // or "overlay" depending on your terminal capabilities 110 | width: logoWidth, 111 | height: logoHeight, 112 | left: leftPosition, 113 | top: -1, 114 | }); 115 | 116 | pic.screen.render(); // Rerender the screen after updating the logo 117 | }; 118 | 119 | // Initial render 120 | renderLogo(); 121 | 122 | // Listen for resize events and rerender the logo 123 | pic.screen.on("resize", () => { 124 | renderLogo(); 125 | }); 126 | } catch (err) { 127 | debugToFile(`pic: ${err}`); 128 | } 129 | 130 | const bigText = grid.set(0, 2, 1, 5, blessed.box, { 131 | content: `{center}{bold}B u i d l G u i d l C l i e n t{/bold}{/center}\n{center}{cyan-fg}${messageForHeader}{/cyan-fg}{/center}`, 132 | tags: true, 133 | align: "center", 134 | valign: "top", 135 | style: { 136 | fg: "white", 137 | border: { 138 | fg: "cyan", 139 | }, 140 | // hover: { 141 | // fg: "cyan", 142 | // }, 143 | }, 144 | // mouse: true, 145 | // clickable: true, 146 | }); 147 | 148 | // bigText.on("click", function () { 149 | // const url = "https://client.buidlguidl.com"; // Replace with your desired URL 150 | // let command; 151 | // switch (process.platform) { 152 | // case "darwin": 153 | // command = `open ${url}`; 154 | // break; 155 | // case "win32": 156 | // command = `start ${url}`; 157 | // break; 158 | // default: 159 | // command = `xdg-open ${url}`; 160 | // } 161 | // exec(command, (error) => { 162 | // if (error) { 163 | // debugToFile(`Error opening URL: ${error}`); 164 | // } 165 | // }); 166 | // }); 167 | 168 | let ipAddressBoxContent = `{center}{bold}Local IP: Fetching...{/bold}\n{center}{bold}Public IP: Fetching...{/bold}{/center}`; 169 | 170 | // Create the IP address box 171 | const ipAddressBox = grid.set(0, 7, 1, 2, blessed.box, { 172 | content: ipAddressBoxContent, 173 | tags: true, 174 | align: "center", 175 | valign: "top", 176 | style: { 177 | fg: "white", 178 | border: { 179 | fg: "cyan", 180 | }, 181 | }, 182 | }); 183 | 184 | let rpcStatusMessage = ""; 185 | let showIPAddresses = true; 186 | let lastToggleTime = Date.now(); 187 | 188 | function updateWSStatusMessage() { 189 | // Update the RPC status message 190 | if (owner !== null) { 191 | if (isConnected(process.pid)) { 192 | rpcStatusMessage = 193 | "{center}{green-fg}RPC Server Connected{/green-fg}{/center}"; 194 | } else { 195 | rpcStatusMessage = 196 | "{center}{red-fg}RPC Server Disconnected{/red-fg}{/center}"; 197 | } 198 | } 199 | 200 | const ipAddressLines = ipAddressBoxContent 201 | .split("\n") 202 | .slice(0, 2) 203 | .join("\n"); 204 | const currentTime = Date.now(); 205 | 206 | if (owner !== null && ipAddressBox.height < 5) { 207 | if (currentTime - lastToggleTime >= 10000) { 208 | showIPAddresses = !showIPAddresses; 209 | lastToggleTime = currentTime; 210 | } 211 | 212 | const contentToShow = showIPAddresses ? ipAddressLines : rpcStatusMessage; 213 | ipAddressBox.setContent(contentToShow); 214 | } else { 215 | // If height is 5 or more, show all information 216 | ipAddressBox.setContent(`${ipAddressLines}\n${rpcStatusMessage}`); 217 | } 218 | 219 | screen.render(); 220 | } 221 | 222 | // Update the IP address fetching part 223 | Promise.all([getIPAddress(), getPublicIPAddress()]).then( 224 | ([localIP, publicIP]) => { 225 | ipAddressBoxContent = `{center}{bold}Local IP: ${localIP}{/bold}\n{center}{bold}Public IP: ${publicIP}{/bold}{/center}`; 226 | updateWSStatusMessage(); // Call this to add the initial RPC status 227 | screen.render(); 228 | } 229 | ); 230 | 231 | updatePointsAndBranchDisplay(); 232 | setInterval(updatePointsAndBranchDisplay, 1 * 60 * 1000); // Every 1 minute 233 | setInterval(updateWSStatusMessage, 1000); // Check every second for smoother transitions 234 | 235 | // Add resize event listener 236 | screen.on("resize", () => { 237 | // Force an immediate update after resize 238 | lastToggleTime = Date.now() - 10000; 239 | updateWSStatusMessage(); 240 | }); 241 | 242 | return { pic, bigText, ipAddressBox }; 243 | } 244 | -------------------------------------------------------------------------------- /monitor_components/helperFunctions.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import os from "os"; 4 | import { debugToFile } from "../helpers.js"; 5 | import { installDir } from "../commandLineOptions.js"; 6 | 7 | const progressFilePath = path.join( 8 | installDir, 9 | "ethereum_clients", 10 | "progressMonitor.json" 11 | ); 12 | 13 | // The cutoff (in terminal lines) for switching the widget layout style 14 | // If screen is < thesh, layout is compact 15 | let layoutHeightThresh; 16 | if (os.platform() == "darwin") { 17 | layoutHeightThresh = 55; 18 | } else if (os.platform() == "linux") { 19 | layoutHeightThresh = 77; 20 | } 21 | export { layoutHeightThresh }; 22 | 23 | export function getLatestLogFile(dir, client) { 24 | try { 25 | const files = fs.readdirSync(dir); 26 | let logFiles; 27 | if (client === "geth") { 28 | logFiles = files.filter( 29 | (file) => file.startsWith("geth_") && file.endsWith(".log") 30 | ); 31 | } else if (client === "reth") { 32 | logFiles = files.filter( 33 | (file) => file.startsWith("reth_") && file.endsWith(".log") 34 | ); 35 | } else if (client === "prysm") { 36 | logFiles = files.filter( 37 | (file) => file.startsWith("prysm_") && file.endsWith(".log") 38 | ); 39 | } else if (client === "lighthouse") { 40 | logFiles = files.filter( 41 | (file) => file.startsWith("lighthouse_") && file.endsWith(".log") 42 | ); 43 | } else { 44 | debugToFile( 45 | `getLatestLogFile(): Invalid client specified. Must be 'geth', 'reth', 'prysm', or 'lighthouse'.`, 46 | () => {} 47 | ); 48 | } 49 | logFiles.sort( 50 | (a, b) => 51 | fs.statSync(path.join(dir, b)).mtime - 52 | fs.statSync(path.join(dir, a)).mtime 53 | ); 54 | 55 | if (logFiles[0]) { 56 | return logFiles[0]; 57 | } else { 58 | debugToFile( 59 | `getLatestLogFile(): No log file found, retrying in 1 second...`, 60 | () => {} 61 | ); 62 | return new Promise((resolve) => { 63 | setTimeout(() => { 64 | resolve(getLatestLogFile(dir, client)); 65 | }, 1000); 66 | }); 67 | } 68 | } catch (error) { 69 | debugToFile(`getLatestLogFile(): ${error}`); 70 | return undefined; 71 | } 72 | } 73 | 74 | export function saveProgress(progress) { 75 | // console.log("Saving progress:", progress); 76 | fs.writeFileSync( 77 | progressFilePath, 78 | JSON.stringify(progress, null, 2), 79 | "utf-8" 80 | ); 81 | } 82 | 83 | export function loadProgress() { 84 | if (fs.existsSync(progressFilePath)) { 85 | const data = fs.readFileSync(progressFilePath, "utf-8"); 86 | return JSON.parse(data); 87 | } 88 | return { 89 | headerDlProgress: 0, 90 | chainDlProgress: 0, 91 | stateDlProgress: 0, 92 | }; 93 | } 94 | 95 | export function formatLogLines(line) { 96 | // Define words which should be highlighted in exec and consensus logs 97 | const highlightRules = [ 98 | { word: "INFO", style: "{bold}{green-fg}" }, 99 | { word: "WARN", style: "{bold}{yellow-fg}" }, 100 | { word: "ERROR", style: "{bold}{red-fg}" }, 101 | { word: "updated", style: "{bold}{yellow-fg}" }, 102 | { word: "latestProcessedSlot", style: "{bold}{green-fg}" }, 103 | // { word: " backfill:", style: "{bold}{blue-fg}" }, 104 | // { word: " blockchain:", style: "{bold}{blue-fg}" }, 105 | // { word: " db:", style: "{bold}{blue-fg}" }, 106 | // { word: " execution:", style: "{bold}{blue-fg}" }, 107 | // { word: " flags:", style: "{bold}{blue-fg}" }, 108 | // { word: " filesystem:", style: "{bold}{blue-fg}" }, 109 | // { word: " gateway:", style: "{bold}{blue-fg}" }, 110 | // { word: " genesis:", style: "{bold}{blue-fg}" }, 111 | // { word: " initial-sync:", style: "{bold}{blue-fg}" }, 112 | // { word: " node:", style: "{bold}{blue-fg}" }, 113 | // { word: " p2p:", style: "{bold}{blue-fg}" }, 114 | // { word: " rpc:", style: "{bold}{blue-fg}" }, 115 | // { word: " state-gen:", style: "{bold}{blue-fg}" }, 116 | // { word: " sync:", style: "{bold}{blue-fg}" }, 117 | // { word: " Syncing:", style: "{bold}{blue-fg}" }, 118 | ]; 119 | 120 | // Apply styles to the words 121 | highlightRules.forEach((rule) => { 122 | const regex = new RegExp(`(${rule.word})`, "g"); 123 | line = line.replace(regex, `${rule.style}$1{/}`); 124 | }); 125 | 126 | // Highlight words followed by "=" in green 127 | line = line.replace(/\b(\w+)(?==)/g, "{bold}{green-fg}$1{/}"); 128 | 129 | // Highlight words followed by ":" and surrounded by spaces in bold blue 130 | line = line.replace(/\s(\w+):\s/g, " {bold}{blue-fg}$1:{/} "); 131 | 132 | // Replace three or more consecutive spaces with two spaces 133 | line = line.replace(/\s{3,}/g, " "); 134 | 135 | return line; 136 | } 137 | -------------------------------------------------------------------------------- /monitor_components/networkLine.js: -------------------------------------------------------------------------------- 1 | import contrib from "blessed-contrib"; 2 | import si from "systeminformation"; 3 | import { debugToFile } from "../helpers.js"; 4 | 5 | let networkDataX = []; 6 | let dataSentY = []; 7 | let dataReceivedY = []; 8 | let lastStats = { 9 | totalSent: 0, 10 | totalReceived: 0, 11 | timestamp: Date.now(), 12 | }; 13 | let firstTime = true; 14 | 15 | function getNetworkStats() { 16 | return new Promise((resolve, reject) => { 17 | si.networkStats() 18 | .then((interfaces) => { 19 | let currentTotalSent = 0; 20 | let currentTotalReceived = 0; 21 | 22 | interfaces.forEach((iface) => { 23 | currentTotalSent += iface.tx_bytes; 24 | currentTotalReceived += iface.rx_bytes; 25 | }); 26 | 27 | // Calculate time difference in seconds 28 | const currentTime = Date.now(); 29 | const timeDiff = (currentTime - lastStats.timestamp) / 1000; 30 | 31 | // Calculate bytes per second 32 | let sentPerSecond = (currentTotalSent - lastStats.totalSent) / timeDiff; 33 | let receivedPerSecond = 34 | (currentTotalReceived - lastStats.totalReceived) / timeDiff; 35 | 36 | // Update last stats for next calculation 37 | lastStats = { 38 | totalSent: currentTotalSent, 39 | totalReceived: currentTotalReceived, 40 | timestamp: currentTime, 41 | }; 42 | 43 | if (sentPerSecond < 0 || sentPerSecond > 1000000000) { 44 | sentPerSecond = 0; 45 | } 46 | 47 | if (receivedPerSecond < 0 || receivedPerSecond > 1000000000) { 48 | receivedPerSecond = 0; 49 | } 50 | 51 | if (firstTime) { 52 | resolve({ 53 | sentPerSecond: 0, 54 | receivedPerSecond: 0, 55 | }); 56 | 57 | firstTime = false; 58 | } else { 59 | resolve({ 60 | sentPerSecond: sentPerSecond / 1000000, 61 | receivedPerSecond: receivedPerSecond / 1000000, 62 | }); 63 | } 64 | }) 65 | .catch((error) => { 66 | debugToFile( 67 | `getNetworkStats() Error fetching network stats: ${error}`, 68 | () => {} 69 | ); 70 | reject(error); 71 | }); 72 | }); 73 | } 74 | 75 | async function updateNetworkLinePlot(networkLine, screen) { 76 | try { 77 | const stats = await getNetworkStats(); 78 | const now = new Date(); 79 | networkDataX.push( 80 | now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds() 81 | ); 82 | dataSentY.push(stats.sentPerSecond); 83 | dataReceivedY.push(stats.receivedPerSecond); 84 | 85 | var seriesNetworkSent = { 86 | title: "Sent", 87 | x: networkDataX, 88 | y: dataSentY, 89 | style: { line: "red" }, 90 | }; 91 | var seriesNetworkReceived = { 92 | title: "Received", 93 | x: networkDataX, 94 | y: dataReceivedY, 95 | style: { line: "blue" }, 96 | }; 97 | 98 | networkLine.setData([seriesNetworkSent, seriesNetworkReceived]); 99 | screen.render(); 100 | 101 | // Keep the data arrays from growing indefinitely 102 | if (networkDataX.length > 60) { 103 | networkDataX.shift(); 104 | dataSentY.shift(); 105 | dataReceivedY.shift(); 106 | } 107 | } catch (error) { 108 | debugToFile(`updateNetworkPlot(): ${error}`); 109 | } 110 | } 111 | 112 | export function createNetworkLine(grid, screen) { 113 | // const networkLine = grid.set(7, 5, 2, 5, contrib.line, { 114 | const networkLine = grid.set(7, 3, 2, 3, contrib.line, { 115 | style: { line: "yellow", text: "green", baseline: "green" }, 116 | xLabelPadding: 3, 117 | xPadding: 5, 118 | showLegend: false, 119 | wholeNumbersOnly: false, 120 | label: 121 | "Network Traffic (MB/sec) [{red-fg}Tx{/red-fg} {blue-fg}Rx{/blue-fg}]", 122 | border: { 123 | type: "line", 124 | fg: "cyan", 125 | }, 126 | tags: true, 127 | }); 128 | 129 | setInterval(() => updateNetworkLinePlot(networkLine, screen), 1000); 130 | 131 | return networkLine; 132 | } 133 | -------------------------------------------------------------------------------- /monitor_components/peerCountGauge.js: -------------------------------------------------------------------------------- 1 | import blessed from "blessed"; 2 | import { exec } from "child_process"; 3 | import { debugToFile } from "../helpers.js"; 4 | import { localClient } from "./viemClients.js"; 5 | import { executionClient, consensusClient } from "../commandLineOptions.js"; 6 | import { bgExecutionPeers, bgConsensusPeers } from "../index.js"; 7 | 8 | let peerCountGauge; 9 | 10 | export function createPeerCountGauge(grid) { 11 | // peerCountGauge = grid.set(2, 9, 1, 1, blessed.box, { 12 | peerCountGauge = grid.set(2, 8, 2, 1, blessed.box, { 13 | label: "Peer Count", 14 | content: `INITIALIZING...`, 15 | stroke: "green", 16 | fill: "white", 17 | border: { 18 | type: "line", 19 | fg: "cyan", 20 | }, 21 | wrap: false, 22 | tags: true, 23 | }); 24 | 25 | populatePeerCountGauge(executionClient, consensusClient); 26 | setInterval( 27 | () => populatePeerCountGauge(executionClient, consensusClient), 28 | 5000 29 | ); 30 | 31 | return peerCountGauge; 32 | } 33 | 34 | // 35 | 36 | export async function getExecutionPeers() { 37 | try { 38 | const peerCountHex = await localClient.request({ 39 | method: "net_peerCount", 40 | }); 41 | // Convert the result from hexadecimal to a decimal number 42 | const peerCount = parseInt(peerCountHex, 16); 43 | 44 | return peerCount; 45 | } catch (error) { 46 | debugToFile(`getExecutionPeers(): ${error}`); 47 | } 48 | } 49 | 50 | export async function getConsensusPeers(consensusClient) { 51 | let searchString; 52 | if (consensusClient == "prysm") { 53 | searchString = 'p2p_peer_count{state="Connected"}'; 54 | } else if (consensusClient == "lighthouse") { 55 | searchString = "libp2p_peers"; 56 | } 57 | return new Promise((resolve) => { 58 | exec( 59 | `curl -s http://localhost:5054/metrics | grep -E '^${searchString} '`, 60 | (error, stdout, stderr) => { 61 | if (error || stderr) { 62 | // debugToFile(`getConsensusPeers(): ${error || stderr}`); 63 | return resolve(null); 64 | } 65 | 66 | const parts = stdout.trim().split(" "); 67 | if (parts.length === 2 && parts[0] === searchString) { 68 | const peerCount = parseInt(parts[1], 10); 69 | resolve(peerCount); 70 | } else { 71 | resolve(null); 72 | } 73 | } 74 | ); 75 | }); 76 | } 77 | 78 | export async function getBGExecutionPeers() { 79 | try { 80 | const curlCommand = `curl -s -X POST --data '{"jsonrpc":"2.0","method":"admin_peers","params":[],"id":1}' -H "Content-Type: application/json" http://localhost:8545`; 81 | 82 | const response = await new Promise((resolve, reject) => { 83 | exec(curlCommand, (error, stdout, stderr) => { 84 | if (error) reject(error); 85 | else resolve(stdout); 86 | }); 87 | }); 88 | 89 | const parsedResponse = JSON.parse(response); 90 | const peerIds = parsedResponse.result.map((peer) => 91 | peer.id.replace(/^0x/, "") 92 | ); 93 | 94 | // debugToFile(`getBGExecutionPeers(): peerIds: ${peerIds}\n`); 95 | 96 | // Parse bgPeerIds correctly 97 | const bgPeerIds = bgExecutionPeers 98 | .map((peer) => { 99 | const match = peer.match(/^enode:\/\/([^@]+)@/); 100 | return match ? match[1] : null; 101 | }) 102 | .filter(Boolean); 103 | 104 | // debugToFile(`getBGExecutionPeers(): bgExecutionPeers: ${bgExecutionPeers}\n`); 105 | // debugToFile(`getBGExecutionPeers(): bgPeerIds: ${bgPeerIds}\n`); 106 | 107 | const matchingPeers = peerIds.filter((id) => bgPeerIds.includes(id)); 108 | 109 | return matchingPeers.length; 110 | } catch (error) { 111 | debugToFile(`getBGExecutionPeers(): ${error}`); 112 | return 0; 113 | } 114 | } 115 | 116 | export async function getBGConsensusPeers() { 117 | try { 118 | // debugToFile( 119 | // `getBGConsensusPeers(): bgConsensusPeers: ${bgConsensusPeers}\n` 120 | // ); 121 | 122 | const curlCommand = `curl -s http://localhost:5052/eth/v1/node/peers`; 123 | 124 | const response = await new Promise((resolve, reject) => { 125 | exec(curlCommand, (error, stdout, stderr) => { 126 | if (error) reject(error); 127 | else resolve(stdout); 128 | }); 129 | }); 130 | 131 | const parsedResponse = JSON.parse(response); 132 | const connectedPeers = parsedResponse.data 133 | .filter((peer) => peer.state === "connected") 134 | .map((peer) => peer.peer_id); 135 | 136 | // debugToFile(`getBGConsensusPeers(): connectedPeers: ${connectedPeers}\n`); 137 | 138 | // Remove duplicates 139 | const uniqueConnectedPeers = [...new Set(connectedPeers)]; 140 | // debugToFile( 141 | // `getBGConsensusPeers(): uniqueConnectedPeers: ${uniqueConnectedPeers}\n` 142 | // ); 143 | 144 | // Compare with bgConsensusPeers 145 | const matchingPeers = uniqueConnectedPeers.filter((peerId) => 146 | bgConsensusPeers.includes(peerId) 147 | ); 148 | 149 | // debugToFile(`getBGConsensusPeers(): matchingPeers: ${matchingPeers}\n\n\n`); 150 | 151 | return matchingPeers.length; 152 | } catch (error) { 153 | // debugToFile(`getBGConsensusPeers(): ${error}`); 154 | return 0; 155 | } 156 | } 157 | 158 | let peerCounts = [0, 0, 0, 0]; 159 | 160 | async function populatePeerCountGauge(executionClient, consensusClient) { 161 | try { 162 | const gaugeNames = [ 163 | `${executionClient.toUpperCase()} All`, 164 | `${executionClient.toUpperCase()} BG`, 165 | `${consensusClient.toUpperCase()} All`, 166 | `${consensusClient.toUpperCase()} BG`, 167 | ]; 168 | const gaugeColors = ["{cyan-fg}", "{cyan-fg}", "{green-fg}", "{green-fg}"]; 169 | const maxPeers = [130, 130, 130, 130]; 170 | 171 | // Get the execution peers count 172 | peerCounts[0] = await getExecutionPeers(); 173 | 174 | // Try to get the consensus peers count, but handle the failure case 175 | try { 176 | peerCounts[1] = await getBGExecutionPeers(); 177 | } catch { 178 | peerCounts[1] = null; // If there's an error, set it to null 179 | } 180 | 181 | try { 182 | peerCounts[2] = await getConsensusPeers(consensusClient); 183 | } catch { 184 | peerCounts[2] = 0; // If there's an error, set it to null 185 | } 186 | 187 | try { 188 | peerCounts[3] = await getBGConsensusPeers(); 189 | } catch { 190 | peerCounts[3] = 0; // If there's an error, set it to null 191 | } 192 | 193 | const boxWidth = peerCountGauge.width - 8; // Subtracting 8 for padding/border 194 | if (boxWidth > 0) { 195 | let content = ""; 196 | 197 | // Only display the first gauge (Execution) if the second one (Consensus) is null 198 | peerCounts.forEach((peerCount, index) => { 199 | // Create the peer count string 200 | const peerCountString = `${peerCount !== null ? peerCount : "0"}`; 201 | 202 | if (peerCount > maxPeers[index]) { 203 | peerCount = maxPeers[index]; 204 | } 205 | 206 | // Calculate the number of filled bars for this stage 207 | const filledBars = Math.floor(boxWidth * (peerCount / maxPeers[index])); 208 | 209 | // Create the bar string 210 | const bar = "█".repeat(filledBars) + " ".repeat(boxWidth - filledBars); 211 | 212 | // Append the custom stage title, progress bar, and peer count to the content 213 | content += `${gaugeColors[index]}${gaugeNames[index]}\n[${bar}] ${peerCountString}{/}\n`; 214 | }); 215 | 216 | peerCountGauge.setContent(content.trim()); 217 | 218 | peerCountGauge.screen.render(); 219 | } 220 | } catch (error) { 221 | debugToFile(`populatePeerCountGauge(): ${error}`); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /monitor_components/pixelBgLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/buidlguidl-client/3240645e94c9022aabed12cb9455200380aabced/monitor_components/pixelBgLogo.png -------------------------------------------------------------------------------- /monitor_components/pixelBgLogo40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/buidlguidl-client/3240645e94c9022aabed12cb9455200380aabced/monitor_components/pixelBgLogo40.png -------------------------------------------------------------------------------- /monitor_components/rethStageGauge.js: -------------------------------------------------------------------------------- 1 | import blessed from "blessed"; 2 | import { debugToFile } from "../helpers.js"; 3 | import { getVersionNumber } from "../ethereum_client_scripts/install.js"; 4 | 5 | // Store Reth version at module level 6 | let rethVersion = null; 7 | 8 | // Function to initialize Reth version 9 | function initRethVersion() { 10 | if (rethVersion === null) { 11 | rethVersion = getVersionNumber("reth"); 12 | } 13 | return rethVersion; 14 | } 15 | 16 | let rethStageGauge; 17 | 18 | export function createRethStageGauge(grid) { 19 | rethStageGauge = grid.set(2, 7, 5, 1, blessed.box, { 20 | label: "Sync Progress", 21 | content: `INITIALIZING...`, 22 | stroke: "cyan", 23 | fill: "white", 24 | border: { 25 | type: "line", 26 | fg: "cyan", 27 | }, 28 | wrap: false, 29 | }); 30 | 31 | return rethStageGauge; 32 | } 33 | 34 | export function populateRethStageGauge(stagePercentages) { 35 | try { 36 | let stageNames; 37 | 38 | // Initialize Reth version if not already done 39 | initRethVersion(); 40 | 41 | if (rethVersion >= "1.3.4") { 42 | stageNames = [ 43 | "HEADERS", 44 | "BODIES", 45 | "SENDER RECOVERY", 46 | "EXECUTION", 47 | "PRUNE SENDER RECOVERY", 48 | "MERKLE UNWIND", 49 | "ACCOUNT HASHING", 50 | "STORAGE HASHING", 51 | "MERKLE EXECUTE", 52 | "TRANSACTION LOOKUP", 53 | "INDEX STORAGE HIST", 54 | "INDEX ACCOUNT HIST", 55 | "PRUNE", 56 | "FINISH", 57 | ]; 58 | } else { 59 | stageNames = [ 60 | "HEADERS", 61 | "BODIES", 62 | "SENDER RECOVERY", 63 | "EXECUTION", 64 | "MERKLE UNWIND", 65 | "ACCOUNT HASHING", 66 | "STORAGE HASHING", 67 | "MERKLE EXECUTE", 68 | "TRANSACTION LOOKUP", 69 | "INDEX STORAGE HIST", 70 | "INDEX ACCOUNT HIST", 71 | "FINISH", 72 | ]; 73 | } 74 | 75 | const boxWidth = rethStageGauge.width - 9; 76 | const boxHeight = rethStageGauge.height - 2; // Subtracting 2 for border 77 | 78 | if (boxWidth > 0 && boxHeight > 0) { 79 | let content = ""; 80 | const maxItems = Math.floor(boxHeight / 2); 81 | 82 | // Display stages based on available space 83 | let startIndex = 0; 84 | let endIndex = Math.min(stagePercentages.length, maxItems); 85 | 86 | if (boxHeight >= 24) { 87 | endIndex = stagePercentages.length; 88 | } 89 | 90 | for (let i = startIndex; i < endIndex; i++) { 91 | let percentComplete = stagePercentages[i]; 92 | if (percentComplete > 1) { 93 | percentComplete = 1; 94 | } 95 | const filledBars = Math.max(0, Math.floor(boxWidth * percentComplete)); 96 | const emptyBars = Math.max(0, boxWidth - filledBars); 97 | const bar = "█".repeat(filledBars) + " ".repeat(emptyBars); 98 | const percentString = `${Math.floor(percentComplete * 100)}%`; 99 | 100 | content += `${stageNames[i]}\n[${bar}] ${percentString}\n`; 101 | } 102 | 103 | rethStageGauge.setContent(content.trim()); 104 | rethStageGauge.screen.render(); 105 | } 106 | } catch (error) { 107 | debugToFile(`populateRethStageGauge(): ${error}`); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /monitor_components/rpcInfoBox.js: -------------------------------------------------------------------------------- 1 | import blessed from "blessed"; 2 | import { debugToFile } from "../helpers.js"; 3 | import { owner } from "../commandLineOptions.js"; 4 | 5 | let rpcInfoBox; 6 | let rpcMethodsHistory = []; 7 | const MAX_HISTORY_LENGTH = 30; 8 | 9 | export function createRpcInfoBox(grid) { 10 | // Create the box configuration but don't add it to grid yet 11 | const boxConfig = { 12 | label: "RPC Requests", 13 | stroke: "cyan", 14 | fill: "white", 15 | border: { 16 | type: "line", 17 | fg: "cyan", 18 | }, 19 | wrap: false, 20 | tags: true, 21 | }; 22 | 23 | // Only create and add to grid if owner exists 24 | if (owner != null) { 25 | rpcInfoBox = grid.set(5, 7, 2, 1, blessed.box, boxConfig); 26 | } else { 27 | // Create the box without adding it to grid 28 | rpcInfoBox = blessed.box(boxConfig); 29 | } 30 | 31 | return rpcInfoBox; 32 | } 33 | 34 | export function populateRpcInfoBox(rpcMethod) { 35 | try { 36 | if (rpcMethod) { 37 | rpcMethodsHistory.unshift(rpcMethod); // Add the new method to the top of the history 38 | if (rpcMethodsHistory.length > MAX_HISTORY_LENGTH) { 39 | rpcMethodsHistory = rpcMethodsHistory.slice(0, MAX_HISTORY_LENGTH); // Keep only the most recent entries 40 | } 41 | } 42 | const content = rpcMethodsHistory.map((method) => `${method}`).join("\n"); 43 | rpcInfoBox.setContent(content); 44 | } catch (error) { 45 | debugToFile(`populateRpcInfoBox(): ${error}`); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /monitor_components/statusBox.js: -------------------------------------------------------------------------------- 1 | import blessed from "blessed"; 2 | import { debugToFile } from "../helpers.js"; 3 | import { installDir } from "../commandLineOptions.js"; 4 | import { synchronizeAndUpdateWidgets } from "./updateLogic.js"; 5 | 6 | export async function updateStatusBox(statusBox) { 7 | try { 8 | const statusMessage = await synchronizeAndUpdateWidgets(installDir); 9 | statusBox.setContent(statusMessage); 10 | } catch (error) { 11 | debugToFile(`updateStatusBox(): ${error}`); 12 | } 13 | } 14 | 15 | export function createStatusBox(grid) { 16 | const statusBox = grid.set(1, 7, 1, 2, blessed.box, { 17 | label: `Status`, 18 | content: "INITIALIZING...", 19 | border: { 20 | type: "line", 21 | fg: "cyan", 22 | }, 23 | tags: true, 24 | }); 25 | 26 | return statusBox; 27 | } 28 | -------------------------------------------------------------------------------- /monitor_components/systemStatsGauge.js: -------------------------------------------------------------------------------- 1 | import blessed from "blessed"; 2 | import { 3 | getMemoryUsage, 4 | getDiskUsage, 5 | getCpuTemperature, 6 | } from "../getSystemStats.js"; 7 | import { debugToFile } from "../helpers.js"; 8 | 9 | let systemStatsGauge; 10 | 11 | export function createSystemStatsGauge(grid, installDir) { 12 | systemStatsGauge = grid.set(5, 8, 2, 1, blessed.box, { 13 | label: "System Stats", 14 | stroke: "green", 15 | fill: "white", 16 | border: { 17 | type: "line", 18 | fg: "cyan", 19 | }, 20 | wrap: false, 21 | tags: true, 22 | }); 23 | 24 | populateSystemStatsGauge(installDir); 25 | setInterval(() => populateSystemStatsGauge(installDir), 5000); 26 | 27 | return systemStatsGauge; 28 | } 29 | 30 | let gaugePercentages = [0, 0, 0]; 31 | 32 | async function populateSystemStatsGauge(installDir) { 33 | try { 34 | gaugePercentages[0] = (await getMemoryUsage()) / 100; 35 | gaugePercentages[1] = (await getDiskUsage(installDir)) / 100; 36 | gaugePercentages[2] = (await getCpuTemperature()) / 100; 37 | 38 | const gaugeNames = ["MEMORY", "STORAGE", "CPU TEMP"]; 39 | const gaugeColors = ["{magenta-fg}", "{green-fg}", "{blue-fg}"]; 40 | const units = ["%", "%", "C"]; 41 | 42 | const boxWidth = systemStatsGauge.width - 9; // Subtracting 9 for padding/border 43 | if (boxWidth > 0) { 44 | let content = ""; 45 | 46 | // Iterate over each stage's percentage and name 47 | gaugePercentages.forEach((percentComplete, index) => { 48 | // Create the percentage string 49 | const percentString = `${Math.floor(percentComplete * 100)}${ 50 | units[index] 51 | }`; 52 | 53 | if (percentComplete > 1) { 54 | percentComplete = 1; 55 | } 56 | 57 | // Calculate the number of filled bars for this stage 58 | const filledBars = Math.floor(boxWidth * percentComplete); 59 | 60 | // Create the bar string 61 | const bar = "█".repeat(filledBars) + " ".repeat(boxWidth - filledBars); 62 | 63 | // Append the custom stage title, progress bar, and percentage to the content 64 | content += `${gaugeColors[index]}${gaugeNames[index]}\n[${bar}] ${percentString}{/}\n`; 65 | }); 66 | 67 | systemStatsGauge.setContent(content.trim()); 68 | 69 | systemStatsGauge.screen.render(); 70 | } 71 | } catch (error) { 72 | debugToFile(`populateSystemStatsGauge(): ${error}`); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /monitor_components/viemClients.js: -------------------------------------------------------------------------------- 1 | import { createPublicClient, http } from "viem"; 2 | import { mainnet } from "viem/chains"; 3 | 4 | export const localClient = createPublicClient({ 5 | name: "localClient", 6 | chain: mainnet, 7 | transport: http("http://localhost:8545"), 8 | }); 9 | 10 | export const mainnetClient = createPublicClient({ 11 | name: "mainnetClient", 12 | chain: mainnet, 13 | transport: http("https://pool.mainnet.rpc.buidlguidl.com:48544/", { 14 | fetchOptions: { 15 | headers: { 16 | Origin: "buidlguidl-client", 17 | }, 18 | }, 19 | }), 20 | }); 21 | 22 | export async function getEthSyncingStatus() { 23 | try { 24 | const syncingStatus = await localClient.request({ 25 | method: "eth_syncing", 26 | params: [], 27 | }); 28 | 29 | return syncingStatus; 30 | } catch (error) { 31 | debugToFile(`getEthSyncingStatus(): ${error}`); 32 | return false; // Return false to indicate not syncing when there's an error 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodes-script", 3 | "version": "1.0.0", 4 | "description": "Assist user in running ethereum node", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "type": "module", 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "axios": "^1.7.2", 15 | "blessed": "^0.1.81", 16 | "blessed-contrib": "^4.11.0", 17 | "https": "^1.0.0", 18 | "macaddress": "^0.5.3", 19 | "minimist": "^1.2.8", 20 | "node-fetch": "^3.3.2", 21 | "node-pty": "^1.0.0", 22 | "open": "^10.1.0", 23 | "readline-sync": "^1.4.10", 24 | "simple-git": "^3.26.0", 25 | "socket.io": "^4.8.1", 26 | "socket.io-client": "^4.8.1", 27 | "systeminformation": "^5.22.8", 28 | "viem": "^2.13.3", 29 | "ws": "^8.15.0" 30 | }, 31 | "optionalDependencies": { 32 | "osx-temperature-sensor": "^1.0.8" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /web_socket_connection/webSocketConnection.js: -------------------------------------------------------------------------------- 1 | import https from "https"; 2 | import os from "os"; 3 | import { debugToFile } from "../helpers.js"; 4 | import { 5 | getMemoryUsage, 6 | getCpuUsage, 7 | getDiskUsage, 8 | } from "../getSystemStats.js"; 9 | import { localClient } from "../monitor_components/viemClients.js"; 10 | import { 11 | installDir, 12 | consensusPeerPorts, 13 | owner, 14 | } from "../commandLineOptions.js"; 15 | import { 16 | getConsensusPeers, 17 | getExecutionPeers, 18 | } from "../monitor_components/peerCountGauge.js"; 19 | import { populateRpcInfoBox } from "../monitor_components/rpcInfoBox.js"; 20 | import simpleGit from "simple-git"; 21 | import { exec } from "child_process"; 22 | import { getPublicIPAddress, getMacAddress } from "../getSystemStats.js"; 23 | import { io } from "socket.io-client"; 24 | import axios from "axios"; 25 | import fs from "fs"; 26 | import path from "path"; 27 | 28 | let socketId; 29 | export let checkIn; 30 | let socket; 31 | const connectionStatus = new Map(); 32 | 33 | export function isConnected(pid) { 34 | return connectionStatus.get(pid) || false; 35 | } 36 | 37 | let isConnecting = false; 38 | let reconnectTimeout; 39 | 40 | const connectionStatusFilePath = path.join( 41 | installDir, 42 | "ethereum_clients", 43 | "websocket_connection_status.json" 44 | ); 45 | 46 | const lockFilePath = path.join(installDir, "ethereum_clients", "script.lock"); 47 | const ethereumClientsDir = path.dirname(lockFilePath); 48 | 49 | // Ensure the ethereum_clients directory exists 50 | if (!fs.existsSync(ethereumClientsDir)) { 51 | fs.mkdirSync(ethereumClientsDir, { recursive: true }); 52 | } 53 | 54 | export function initializeWebSocketConnection(wsConfig) { 55 | let lastCheckInTime = 0; 56 | let lastCheckedBlockNumber = -1; 57 | const minCheckInInterval = 60000; // Minimum 60 seconds between check-ins 58 | 59 | const git = simpleGit(); 60 | 61 | // Run getGitInfo() once and store the result 62 | let gitInfo; 63 | getGitInfo() 64 | .then((info) => { 65 | gitInfo = info; 66 | }) 67 | .catch((error) => { 68 | debugToFile(`Failed to get initial git info: ${error}`); 69 | gitInfo = { branch: "unknown", lastCommitDate: "unknown" }; 70 | }); 71 | 72 | async function getGitInfo() { 73 | try { 74 | const branch = await git.revparse(["--abbrev-ref", "HEAD"]); 75 | const lastCommit = await git.log(["--format=%cd", "--date=iso", "-1"]); 76 | const commitHash = await git.revparse(["HEAD"]); // Add this line to get the commit hash 77 | 78 | let lastCommitDate = "unknown"; 79 | if (lastCommit && lastCommit.latest && lastCommit.latest.hash) { 80 | const commitDateString = lastCommit.latest.hash; 81 | try { 82 | // Directly create a date from the ISO string 83 | const date = new Date(commitDateString); 84 | 85 | if (!isNaN(date)) { 86 | lastCommitDate = date 87 | .toISOString() 88 | .replace(/T/, " ") 89 | .replace(/\..+/, ""); 90 | } else { 91 | throw new Error("Invalid date"); 92 | } 93 | } catch (error) { 94 | debugToFile(`Failed to parse commit date: ${error}`); 95 | debugToFile(`Error stack: ${error.stack}`); 96 | } 97 | } 98 | 99 | return { 100 | branch: branch.trim(), 101 | lastCommitDate: lastCommitDate, 102 | commitHash: commitHash.trim(), // Add this line 103 | }; 104 | } catch (error) { 105 | debugToFile(`Failed to get git info: ${error}`); 106 | debugToFile(`Error stack: ${error.stack}`); 107 | return { 108 | branch: "unknown", 109 | lastCommitDate: "unknown", 110 | commitHash: "unknown", 111 | }; 112 | } 113 | } 114 | 115 | function updateConnectionStatusFile(status) { 116 | fs.writeFileSync( 117 | connectionStatusFilePath, 118 | JSON.stringify({ connected: status }) 119 | ); 120 | } 121 | 122 | function connectWebSocket() { 123 | if (isConnecting) return; 124 | isConnecting = true; 125 | 126 | try { 127 | // Check if this is a primary instance 128 | const isPrimary = 129 | fs.existsSync(lockFilePath) && 130 | fs.readFileSync(lockFilePath, "utf8") === process.pid.toString(); 131 | 132 | // For non-primary instances, we should still connect to the local RPC 133 | if (!isPrimary) { 134 | // Test the RPC connection 135 | axios 136 | .post("http://localhost:8545", { 137 | jsonrpc: "2.0", 138 | method: "eth_blockNumber", 139 | params: [], 140 | id: 1, 141 | }) 142 | .then(() => { 143 | connectionStatus.set(process.pid, true); 144 | updateConnectionStatusFile(true); 145 | }) 146 | .catch(() => { 147 | connectionStatus.set(process.pid, false); 148 | updateConnectionStatusFile(false); 149 | }); 150 | 151 | isConnecting = false; 152 | return; 153 | } 154 | 155 | // Primary instance Socket.IO connection logic 156 | socket = io("wss://pool.mainnet.rpc.buidlguidl.com:48546", { 157 | reconnection: true, 158 | reconnectionDelay: 10000, 159 | reconnectionAttempts: Infinity, 160 | }); 161 | 162 | socket.on("connect", () => { 163 | debugToFile("Socket.IO connection established"); 164 | isConnecting = false; 165 | connectionStatus.set(process.pid, true); 166 | updateConnectionStatusFile(true); 167 | clearTimeout(reconnectTimeout); 168 | }); 169 | 170 | socket.on("init", (id) => { 171 | socketId = id; 172 | debugToFile(`Socket ID: ${socketId}`); 173 | }); 174 | 175 | socket.on("rpc_request", async (request, callback) => { 176 | populateRpcInfoBox(request.method); 177 | 178 | const targetUrl = "http://localhost:8545"; 179 | 180 | try { 181 | const rpcResponse = await axios.post(targetUrl, { 182 | jsonrpc: "2.0", 183 | method: request.method, 184 | params: request.params, 185 | id: request.id, 186 | }); 187 | 188 | callback(rpcResponse.data); 189 | } catch (error) { 190 | debugToFile("Error returning RPC response:", error); 191 | 192 | callback({ 193 | jsonrpc: "2.0", 194 | error: { 195 | code: -70000, 196 | message: "Internal node error", 197 | data: error.message, 198 | }, 199 | id: request.id, 200 | }); 201 | } 202 | }); 203 | 204 | socket.on("disconnect", () => { 205 | socketId = null; 206 | isConnecting = false; 207 | connectionStatus.set(process.pid, false); 208 | updateConnectionStatusFile(false); 209 | debugToFile("Disconnected from Socket.IO server"); 210 | }); 211 | 212 | socket.on("connect_error", (error) => { 213 | debugToFile(`Socket.IO connection error: ${error}`); 214 | isConnecting = false; 215 | connectionStatus.set(process.pid, false); 216 | updateConnectionStatusFile(false); 217 | }); 218 | } catch (error) { 219 | debugToFile(`connectWebSocket error: ${error}`); 220 | isConnecting = false; 221 | connectionStatus.set(process.pid, false); 222 | updateConnectionStatusFile(false); 223 | } 224 | } 225 | 226 | connectWebSocket(); 227 | 228 | checkIn = async function (force = false, blockNumber = null) { 229 | // debugToFile(`checkIn() called`); 230 | const now = Date.now(); 231 | if (!force && now - lastCheckInTime < minCheckInInterval) { 232 | return; 233 | } 234 | 235 | let currentBlockNumber = blockNumber; 236 | if (!currentBlockNumber) { 237 | try { 238 | currentBlockNumber = await localClient.getBlockNumber(); 239 | } catch (error) { 240 | debugToFile(`Failed to get block number: ${error}`); 241 | return; 242 | } 243 | } 244 | 245 | if (!force && currentBlockNumber === lastCheckedBlockNumber) { 246 | return; 247 | } 248 | 249 | lastCheckInTime = now; 250 | lastCheckedBlockNumber = currentBlockNumber; 251 | 252 | let executionClientResponse = 253 | wsConfig.executionClient + " v" + wsConfig.executionClientVer; 254 | let consensusClientResponse = 255 | wsConfig.consensusClient + " v" + wsConfig.consensusClientVer; 256 | 257 | let possibleBlockNumber = currentBlockNumber; 258 | let possibleBlockHash; 259 | try { 260 | const block = await localClient.getBlock(possibleBlockNumber); 261 | possibleBlockHash = block.hash; 262 | } catch (error) { 263 | debugToFile(`Failed to get block hash: ${error}`); 264 | } 265 | 266 | let enode = await getEnodeWithRetry(); 267 | let peerInfo = await getPeerIDWithRetry(); 268 | 269 | let peer_id = null; 270 | let enr = null; 271 | 272 | if (peerInfo) { 273 | peer_id = peerInfo.peer_id; 274 | enr = peerInfo.enr; 275 | } 276 | 277 | // debugToFile(`Checkin() enr: ${enr}`); 278 | // debugToFile(`Checkin() Peer ID: ${peer_id}`); 279 | 280 | try { 281 | const cpuUsage = await getCpuUsage(); 282 | const memoryUsage = await getMemoryUsage(); 283 | const diskUsage = await getDiskUsage(installDir); 284 | const macAddress = await getMacAddress(); 285 | const executionPeers = await getExecutionPeers(wsConfig.executionClient); 286 | const consensusPeers = await getConsensusPeers(wsConfig.consensusClient); 287 | 288 | // Use the stored gitInfo instead of calling getGitInfo() 289 | const params = { 290 | id: `${os.hostname()}-${macAddress}-${os.platform()}-${os.arch()}`, 291 | node_version: `${process.version}`, 292 | execution_client: executionClientResponse, 293 | consensus_client: consensusClientResponse, 294 | cpu_usage: `${cpuUsage.toFixed(1)}`, 295 | memory_usage: `${memoryUsage}`, 296 | storage_usage: `${diskUsage}`, 297 | block_number: possibleBlockNumber ? possibleBlockNumber.toString() : "", 298 | block_hash: possibleBlockHash ? possibleBlockHash : "", 299 | execution_peers: executionPeers.toString(), 300 | consensus_peers: consensusPeers.toString(), 301 | git_branch: gitInfo.branch, 302 | last_commit: gitInfo.lastCommitDate, 303 | commit_hash: gitInfo.commitHash, 304 | enode: enode || "", 305 | peerid: peer_id || "", 306 | enr: enr || "", 307 | consensus_tcp_port: consensusPeerPorts[0].toString(), 308 | consensus_udp_port: consensusPeerPorts[1].toString(), 309 | socket_id: socketId || "", 310 | owner: owner, 311 | }; 312 | 313 | // debugToFile(`Checkin() params: ${JSON.stringify(params)}`); 314 | 315 | if (socket && socket.connected) { 316 | socket.emit("checkin", { 317 | type: "checkin", 318 | params: params, 319 | }); 320 | } else { 321 | debugToFile("Socket.IO is not connected."); 322 | } 323 | } catch (error) { 324 | debugToFile(`checkIn() Error: ${error}`); 325 | } 326 | }; 327 | 328 | // Immediate check-in when monitoring starts 329 | checkIn(true); 330 | 331 | let checkInTimer; 332 | 333 | // Function to schedule next check-in 334 | const scheduleNextCheckIn = () => { 335 | if (checkInTimer) { 336 | clearTimeout(checkInTimer); 337 | } 338 | checkInTimer = setTimeout(() => { 339 | checkIn(true); 340 | scheduleNextCheckIn(); // Schedule next check-in after this one completes 341 | }, 60000); 342 | }; 343 | 344 | // Initial timer setup 345 | scheduleNextCheckIn(); 346 | 347 | // Set up block listener 348 | localClient.watchBlocks( 349 | { 350 | onBlock: (block) => { 351 | if (block.number > 0) { 352 | checkIn(true, block.number); // Check in with new block 353 | scheduleNextCheckIn(); // Reset the timer 354 | } 355 | }, 356 | }, 357 | (error) => { 358 | debugToFile(`Error in block watcher: ${error}`); 359 | } 360 | ); 361 | 362 | setInterval(() => { 363 | try { 364 | const statusData = fs.readFileSync(connectionStatusFilePath, "utf8"); 365 | const { connected } = JSON.parse(statusData); 366 | connectionStatus.set(process.pid, connected); 367 | } catch (error) { 368 | debugToFile(`Error reading connection status file: ${error}`); 369 | } 370 | }, 5000); // Check every 5 seconds 371 | } 372 | 373 | let cachedEnode = null; 374 | let enodeRetries = 0; 375 | 376 | async function getEnodeWithRetry(maxRetries = 60) { 377 | // If we already have a cached enode, return it immediately 378 | if (cachedEnode) { 379 | return cachedEnode; 380 | } 381 | if (enodeRetries < maxRetries) { 382 | try { 383 | const nodeInfo = await getNodeInfo(); 384 | if (nodeInfo.enode) { 385 | let enode = nodeInfo.enode; 386 | const publicIPv4 = await getPublicIPAddress(); 387 | 388 | // Check if the enode contains an IPv6 address 389 | if (enode.includes("[") && enode.includes("]")) { 390 | // Replace IPv6 with public IPv4 391 | enode = enode.replace(/\[.*?\]/, publicIPv4); 392 | } else if (enode.includes("@127.") || enode.includes("@0.0.0.0")) { 393 | // Replace local IPv4 or 0.0.0.0 with public IPv4 394 | enode = enode.replace(/@(127\.[0-9.]+|0\.0\.0\.0)/, `@${publicIPv4}`); 395 | } 396 | // Cache the successful enode 397 | cachedEnode = enode; 398 | return enode; 399 | } 400 | } catch (error) { 401 | debugToFile( 402 | `Failed to get enode (attempt ${enodeRetries + 1}): ${error}`, 403 | () => {} 404 | ); 405 | } 406 | enodeRetries++; 407 | } else { 408 | debugToFile(`Failed to get enode after ${maxRetries} attempts`); 409 | return null; 410 | } 411 | } 412 | 413 | let cachedPeerID = null; 414 | let cachedENR = null; 415 | let peerInfoRetries = 0; 416 | 417 | async function getPeerIDWithRetry(maxRetries = 60) { 418 | // If we already have a cached peer ID and ENR, return them immediately 419 | if (cachedPeerID && cachedENR) { 420 | return { peer_id: cachedPeerID, enr: cachedENR }; 421 | } 422 | 423 | if (peerInfoRetries < maxRetries) { 424 | try { 425 | const { peer_id, enr } = await getConsensusPeerInfo(); 426 | if (peer_id && enr) { 427 | // Cache the successful peer ID and ENR 428 | cachedPeerID = peer_id; 429 | cachedENR = enr; 430 | return { peer_id, enr }; 431 | } 432 | } catch (error) { 433 | debugToFile( 434 | `Failed to get peer info (attempt ${peerInfoRetries + 1}): ${error}`, 435 | () => {} 436 | ); 437 | } 438 | peerInfoRetries++; 439 | } else { 440 | debugToFile(`Failed to get peer info after ${maxRetries} attempts`); 441 | return { peer_id: null, enr: null }; 442 | } 443 | } 444 | 445 | function getConsensusPeerInfo() { 446 | return new Promise((resolve, reject) => { 447 | const command = `curl -s http://localhost:5052/eth/v1/node/identity`; 448 | 449 | exec(command, (error, stdout, stderr) => { 450 | if (error) { 451 | reject(`Error executing curl command: ${error}`); 452 | return; 453 | } 454 | if (stderr) { 455 | reject(`Curl command stderr: ${stderr}`); 456 | return; 457 | } 458 | try { 459 | const response = JSON.parse(stdout); 460 | const peer_id = response.data.peer_id; 461 | const enr = response.data.enr; 462 | if (peer_id && enr) { 463 | resolve({ peer_id, enr }); 464 | } else { 465 | reject("Incomplete peer info received"); 466 | } 467 | } catch (parseError) { 468 | reject(`Error parsing JSON response: ${parseError}`); 469 | } 470 | }); 471 | }); 472 | } 473 | 474 | function getConsensusPeerID() { 475 | return new Promise((resolve, reject) => { 476 | const command = `curl -s http://localhost:5052/eth/v1/node/identity | grep -o '"peer_id":"[^"]*"' | sed 's/"peer_id":"//;s/"//g'`; 477 | 478 | exec(command, (error, stdout, stderr) => { 479 | if (error) { 480 | reject(`Error executing curl command: ${error}`); 481 | return null; 482 | } 483 | if (stderr) { 484 | reject(`Curl command stderr: ${stderr}`); 485 | return null; 486 | } 487 | const peerID = stdout.trim(); 488 | if (peerID) { 489 | resolve(peerID); 490 | } else { 491 | reject("Empty peer ID received"); 492 | } 493 | }); 494 | }); 495 | } 496 | 497 | function getNodeInfo() { 498 | return new Promise((resolve, reject) => { 499 | const command = `curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":1}' http://localhost:8545`; 500 | 501 | exec(command, (error, stdout, stderr) => { 502 | if (error) { 503 | reject(`Error executing curl command: ${error}`); 504 | return null; 505 | } 506 | if (stderr) { 507 | reject(`Curl command stderr: ${stderr}`); 508 | return null; 509 | } 510 | try { 511 | const response = JSON.parse(stdout); 512 | resolve(response.result); 513 | } catch (parseError) { 514 | reject(`Error parsing JSON response: ${parseError}`); 515 | } 516 | }); 517 | }); 518 | } 519 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@adraffy/ens-normalize@1.10.0": 6 | version "1.10.0" 7 | resolved "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz" 8 | integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== 9 | 10 | "@colors/colors@1.5.0": 11 | version "1.5.0" 12 | resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" 13 | integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== 14 | 15 | "@kwsites/file-exists@^1.1.1": 16 | version "1.1.1" 17 | resolved "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz" 18 | integrity sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw== 19 | dependencies: 20 | debug "^4.1.1" 21 | 22 | "@kwsites/promise-deferred@^1.1.1": 23 | version "1.1.1" 24 | resolved "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz" 25 | integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== 26 | 27 | "@noble/curves@1.4.0", "@noble/curves@^1.4.0", "@noble/curves@~1.4.0": 28 | version "1.4.0" 29 | resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz" 30 | integrity sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg== 31 | dependencies: 32 | "@noble/hashes" "1.4.0" 33 | 34 | "@noble/hashes@1.4.0", "@noble/hashes@^1.4.0", "@noble/hashes@~1.4.0": 35 | version "1.4.0" 36 | resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz" 37 | integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== 38 | 39 | "@scure/base@~1.1.6": 40 | version "1.1.7" 41 | resolved "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz" 42 | integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== 43 | 44 | "@scure/bip32@1.4.0": 45 | version "1.4.0" 46 | resolved "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz" 47 | integrity sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg== 48 | dependencies: 49 | "@noble/curves" "~1.4.0" 50 | "@noble/hashes" "~1.4.0" 51 | "@scure/base" "~1.1.6" 52 | 53 | "@scure/bip39@1.3.0": 54 | version "1.3.0" 55 | resolved "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz" 56 | integrity sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ== 57 | dependencies: 58 | "@noble/hashes" "~1.4.0" 59 | "@scure/base" "~1.1.6" 60 | 61 | "@socket.io/component-emitter@~3.1.0": 62 | version "3.1.2" 63 | resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" 64 | integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== 65 | 66 | "@types/cors@^2.8.12": 67 | version "2.8.17" 68 | resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" 69 | integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== 70 | dependencies: 71 | "@types/node" "*" 72 | 73 | "@types/node@*", "@types/node@>=10.0.0": 74 | version "22.13.1" 75 | resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33" 76 | integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew== 77 | dependencies: 78 | undici-types "~6.20.0" 79 | 80 | abbrev@1: 81 | version "1.1.1" 82 | resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" 83 | integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== 84 | 85 | abitype@1.0.5: 86 | version "1.0.5" 87 | resolved "https://registry.npmjs.org/abitype/-/abitype-1.0.5.tgz" 88 | integrity sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw== 89 | 90 | accepts@~1.3.4: 91 | version "1.3.8" 92 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 93 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 94 | dependencies: 95 | mime-types "~2.1.34" 96 | negotiator "0.6.3" 97 | 98 | ansi-escapes@^6.2.0: 99 | version "6.2.1" 100 | resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz" 101 | integrity sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig== 102 | 103 | ansi-regex@^2.0.0: 104 | version "2.1.1" 105 | resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" 106 | integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== 107 | 108 | ansi-regex@^5.0.1: 109 | version "5.0.1" 110 | resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" 111 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 112 | 113 | ansi-styles@^2.2.1: 114 | version "2.2.1" 115 | resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" 116 | integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA== 117 | 118 | ansi-term@>=0.0.2: 119 | version "0.0.2" 120 | resolved "https://registry.npmjs.org/ansi-term/-/ansi-term-0.0.2.tgz" 121 | integrity sha512-jLnGE+n8uAjksTJxiWZf/kcUmXq+cRWSl550B9NmQ8YiqaTM+lILcSe5dHdp8QkJPhaOghDjnMKwyYSMjosgAA== 122 | dependencies: 123 | x256 ">=0.0.1" 124 | 125 | ansicolors@~0.3.2: 126 | version "0.3.2" 127 | resolved "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz" 128 | integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== 129 | 130 | asynckit@^0.4.0: 131 | version "0.4.0" 132 | resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" 133 | integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== 134 | 135 | axios@^1.7.2: 136 | version "1.7.4" 137 | resolved "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz" 138 | integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw== 139 | dependencies: 140 | follow-redirects "^1.15.6" 141 | form-data "^4.0.0" 142 | proxy-from-env "^1.1.0" 143 | 144 | base64id@2.0.0, base64id@~2.0.0: 145 | version "2.0.0" 146 | resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" 147 | integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== 148 | 149 | blessed-contrib@^4.11.0: 150 | version "4.11.0" 151 | resolved "https://registry.npmjs.org/blessed-contrib/-/blessed-contrib-4.11.0.tgz" 152 | integrity sha512-P00Xji3xPp53+FdU9f74WpvnOAn/SS0CKLy4vLAf5Ps7FGDOTY711ruJPZb3/7dpFuP+4i7f4a/ZTZdLlKG9WA== 153 | dependencies: 154 | ansi-term ">=0.0.2" 155 | chalk "^1.1.0" 156 | drawille-canvas-blessed-contrib ">=0.1.3" 157 | lodash "~>=4.17.21" 158 | map-canvas ">=0.1.5" 159 | marked "^4.0.12" 160 | marked-terminal "^5.1.1" 161 | memory-streams "^0.1.0" 162 | memorystream "^0.3.1" 163 | picture-tuber "^1.0.1" 164 | sparkline "^0.1.1" 165 | strip-ansi "^3.0.0" 166 | term-canvas "0.0.5" 167 | x256 ">=0.0.1" 168 | 169 | blessed@^0.1.81: 170 | version "0.1.81" 171 | resolved "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz" 172 | integrity sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ== 173 | 174 | bresenham@0.0.3: 175 | version "0.0.3" 176 | resolved "https://registry.npmjs.org/bresenham/-/bresenham-0.0.3.tgz" 177 | integrity sha512-wbMxoJJM1p3+6G7xEFXYNCJ30h2qkwmVxebkbwIl4OcnWtno5R3UT9VuYLfStlVNAQCmRjkGwjPFdfaPd4iNXw== 178 | 179 | buffers@~0.1.1: 180 | version "0.1.1" 181 | resolved "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz" 182 | integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== 183 | 184 | bundle-name@^4.1.0: 185 | version "4.1.0" 186 | resolved "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz" 187 | integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== 188 | dependencies: 189 | run-applescript "^7.0.0" 190 | 191 | cardinal@^2.1.1: 192 | version "2.1.1" 193 | resolved "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz" 194 | integrity sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw== 195 | dependencies: 196 | ansicolors "~0.3.2" 197 | redeyed "~2.1.0" 198 | 199 | chalk@^1.1.0: 200 | version "1.1.3" 201 | resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" 202 | integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== 203 | dependencies: 204 | ansi-styles "^2.2.1" 205 | escape-string-regexp "^1.0.2" 206 | has-ansi "^2.0.0" 207 | strip-ansi "^3.0.0" 208 | supports-color "^2.0.0" 209 | 210 | chalk@^5.2.0: 211 | version "5.3.0" 212 | resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" 213 | integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== 214 | 215 | charm@~0.1.0: 216 | version "0.1.2" 217 | resolved "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz" 218 | integrity sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ== 219 | 220 | cli-table3@^0.6.3: 221 | version "0.6.5" 222 | resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz" 223 | integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== 224 | dependencies: 225 | string-width "^4.2.0" 226 | optionalDependencies: 227 | "@colors/colors" "1.5.0" 228 | 229 | combined-stream@^1.0.8: 230 | version "1.0.8" 231 | resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" 232 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 233 | dependencies: 234 | delayed-stream "~1.0.0" 235 | 236 | cookie@~0.7.2: 237 | version "0.7.2" 238 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" 239 | integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== 240 | 241 | core-util-is@~1.0.0: 242 | version "1.0.3" 243 | resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" 244 | integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== 245 | 246 | cors@~2.8.5: 247 | version "2.8.5" 248 | resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" 249 | integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== 250 | dependencies: 251 | object-assign "^4" 252 | vary "^1" 253 | 254 | data-uri-to-buffer@^4.0.0: 255 | version "4.0.1" 256 | resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz" 257 | integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== 258 | 259 | debug@^4.1.1, debug@^4.3.5, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: 260 | version "4.3.7" 261 | resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" 262 | integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== 263 | dependencies: 264 | ms "^2.1.3" 265 | 266 | default-browser-id@^5.0.0: 267 | version "5.0.0" 268 | resolved "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz" 269 | integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== 270 | 271 | default-browser@^5.2.1: 272 | version "5.2.1" 273 | resolved "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz" 274 | integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== 275 | dependencies: 276 | bundle-name "^4.1.0" 277 | default-browser-id "^5.0.0" 278 | 279 | define-lazy-prop@^3.0.0: 280 | version "3.0.0" 281 | resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz" 282 | integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== 283 | 284 | delayed-stream@~1.0.0: 285 | version "1.0.0" 286 | resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" 287 | integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== 288 | 289 | drawille-blessed-contrib@>=0.0.1: 290 | version "1.0.0" 291 | resolved "https://registry.npmjs.org/drawille-blessed-contrib/-/drawille-blessed-contrib-1.0.0.tgz" 292 | integrity sha512-WnHMgf5en/hVOsFhxLI8ZX0qTJmerOsVjIMQmn4cR1eI8nLGu+L7w5ENbul+lZ6w827A3JakCuernES5xbHLzQ== 293 | 294 | drawille-canvas-blessed-contrib@>=0.0.1, drawille-canvas-blessed-contrib@>=0.1.3: 295 | version "0.1.3" 296 | resolved "https://registry.npmjs.org/drawille-canvas-blessed-contrib/-/drawille-canvas-blessed-contrib-0.1.3.tgz" 297 | integrity sha512-bdDvVJOxlrEoPLifGDPaxIzFh3cD7QH05ePoQ4fwnqfi08ZSxzEhOUpI5Z0/SQMlWgcCQOEtuw0zrwezacXglw== 298 | dependencies: 299 | ansi-term ">=0.0.2" 300 | bresenham "0.0.3" 301 | drawille-blessed-contrib ">=0.0.1" 302 | gl-matrix "^2.1.0" 303 | x256 ">=0.0.1" 304 | 305 | emoji-regex@^8.0.0: 306 | version "8.0.0" 307 | resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" 308 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 309 | 310 | engine.io-client@~6.6.1: 311 | version "6.6.3" 312 | resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.3.tgz#815393fa24f30b8e6afa8f77ccca2f28146be6de" 313 | integrity sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w== 314 | dependencies: 315 | "@socket.io/component-emitter" "~3.1.0" 316 | debug "~4.3.1" 317 | engine.io-parser "~5.2.1" 318 | ws "~8.17.1" 319 | xmlhttprequest-ssl "~2.1.1" 320 | 321 | engine.io-parser@~5.2.1: 322 | version "5.2.3" 323 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" 324 | integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== 325 | 326 | engine.io@~6.6.0: 327 | version "6.6.4" 328 | resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.4.tgz#0a89a3e6b6c1d4b0c2a2a637495e7c149ec8d8ee" 329 | integrity sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g== 330 | dependencies: 331 | "@types/cors" "^2.8.12" 332 | "@types/node" ">=10.0.0" 333 | accepts "~1.3.4" 334 | base64id "2.0.0" 335 | cookie "~0.7.2" 336 | cors "~2.8.5" 337 | debug "~4.3.1" 338 | engine.io-parser "~5.2.1" 339 | ws "~8.17.1" 340 | 341 | escape-string-regexp@^1.0.2: 342 | version "1.0.5" 343 | resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" 344 | integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== 345 | 346 | esprima@~4.0.0: 347 | version "4.0.1" 348 | resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" 349 | integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== 350 | 351 | event-stream@~0.9.8: 352 | version "0.9.8" 353 | resolved "https://registry.npmjs.org/event-stream/-/event-stream-0.9.8.tgz" 354 | integrity sha512-o5h0Mp1bkoR6B0i7pTCAzRy+VzdsRWH997KQD4Psb0EOPoKEIiaRx/EsOdUl7p1Ktjw7aIWvweI/OY1R9XrlUg== 355 | dependencies: 356 | optimist "0.2" 357 | 358 | fetch-blob@^3.1.2, fetch-blob@^3.1.4: 359 | version "3.2.0" 360 | resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz" 361 | integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== 362 | dependencies: 363 | node-domexception "^1.0.0" 364 | web-streams-polyfill "^3.0.3" 365 | 366 | follow-redirects@^1.15.6: 367 | version "1.15.6" 368 | resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz" 369 | integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== 370 | 371 | form-data@^4.0.0: 372 | version "4.0.0" 373 | resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz" 374 | integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== 375 | dependencies: 376 | asynckit "^0.4.0" 377 | combined-stream "^1.0.8" 378 | mime-types "^2.1.12" 379 | 380 | formdata-polyfill@^4.0.10: 381 | version "4.0.10" 382 | resolved "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz" 383 | integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== 384 | dependencies: 385 | fetch-blob "^3.1.2" 386 | 387 | gl-matrix@^2.1.0: 388 | version "2.8.1" 389 | resolved "https://registry.npmjs.org/gl-matrix/-/gl-matrix-2.8.1.tgz" 390 | integrity sha512-0YCjVpE3pS5XWlN3J4X7AiAx65+nqAI54LndtVFnQZB6G/FVLkZH8y8V6R3cIoOQR4pUdfwQGd1iwyoXHJ4Qfw== 391 | 392 | has-ansi@^2.0.0: 393 | version "2.0.0" 394 | resolved "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" 395 | integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg== 396 | dependencies: 397 | ansi-regex "^2.0.0" 398 | 399 | has-flag@^4.0.0: 400 | version "4.0.0" 401 | resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" 402 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 403 | 404 | here@0.0.2: 405 | version "0.0.2" 406 | resolved "https://registry.npmjs.org/here/-/here-0.0.2.tgz" 407 | integrity sha512-U7VYImCTcPoY27TSmzoiFsmWLEqQFaYNdpsPb9K0dXJhE6kufUqycaz51oR09CW85dDU9iWyy7At8M+p7hb3NQ== 408 | 409 | https@^1.0.0: 410 | version "1.0.0" 411 | resolved "https://registry.npmjs.org/https/-/https-1.0.0.tgz" 412 | integrity sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg== 413 | 414 | inherits@~2.0.1: 415 | version "2.0.4" 416 | resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" 417 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 418 | 419 | is-docker@^3.0.0: 420 | version "3.0.0" 421 | resolved "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz" 422 | integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== 423 | 424 | is-fullwidth-code-point@^3.0.0: 425 | version "3.0.0" 426 | resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" 427 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 428 | 429 | is-inside-container@^1.0.0: 430 | version "1.0.0" 431 | resolved "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz" 432 | integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== 433 | dependencies: 434 | is-docker "^3.0.0" 435 | 436 | is-wsl@^3.1.0: 437 | version "3.1.0" 438 | resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz" 439 | integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== 440 | dependencies: 441 | is-inside-container "^1.0.0" 442 | 443 | isarray@0.0.1: 444 | version "0.0.1" 445 | resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" 446 | integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== 447 | 448 | isows@1.0.4: 449 | version "1.0.4" 450 | resolved "https://registry.npmjs.org/isows/-/isows-1.0.4.tgz" 451 | integrity sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ== 452 | 453 | lodash@^4.17.21, lodash@~>=4.17.21: 454 | version "4.17.21" 455 | resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" 456 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 457 | 458 | macaddress@^0.5.3: 459 | version "0.5.3" 460 | resolved "https://registry.npmjs.org/macaddress/-/macaddress-0.5.3.tgz" 461 | integrity sha512-vGBKTA+jwM4KgjGZ+S/8/Mkj9rWzePyGY6jManXPGhiWu63RYwW8dKPyk5koP+8qNVhPhHgFa1y/MJ4wrjsNrg== 462 | 463 | map-canvas@>=0.1.5: 464 | version "0.1.5" 465 | resolved "https://registry.npmjs.org/map-canvas/-/map-canvas-0.1.5.tgz" 466 | integrity sha512-f7M3sOuL9+up0NCOZbb1rQpWDLZwR/ftCiNbyscjl9LUUEwrRaoumH4sz6swgs58lF21DQ0hsYOCw5C6Zz7hbg== 467 | dependencies: 468 | drawille-canvas-blessed-contrib ">=0.0.1" 469 | xml2js "^0.4.5" 470 | 471 | marked-terminal@^5.1.1: 472 | version "5.2.0" 473 | resolved "https://registry.npmjs.org/marked-terminal/-/marked-terminal-5.2.0.tgz" 474 | integrity sha512-Piv6yNwAQXGFjZSaiNljyNFw7jKDdGrw70FSbtxEyldLsyeuV5ZHm/1wW++kWbrOF1VPnUgYOhB2oLL0ZpnekA== 475 | dependencies: 476 | ansi-escapes "^6.2.0" 477 | cardinal "^2.1.1" 478 | chalk "^5.2.0" 479 | cli-table3 "^0.6.3" 480 | node-emoji "^1.11.0" 481 | supports-hyperlinks "^2.3.0" 482 | 483 | marked@^4.0.12: 484 | version "4.3.0" 485 | resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz" 486 | integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== 487 | 488 | memory-streams@^0.1.0: 489 | version "0.1.3" 490 | resolved "https://registry.npmjs.org/memory-streams/-/memory-streams-0.1.3.tgz" 491 | integrity sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA== 492 | dependencies: 493 | readable-stream "~1.0.2" 494 | 495 | memorystream@^0.3.1: 496 | version "0.3.1" 497 | resolved "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz" 498 | integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== 499 | 500 | mime-db@1.52.0: 501 | version "1.52.0" 502 | resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" 503 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 504 | 505 | mime-types@^2.1.12, mime-types@~2.1.34: 506 | version "2.1.35" 507 | resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" 508 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 509 | dependencies: 510 | mime-db "1.52.0" 511 | 512 | minimist@^1.2.8: 513 | version "1.2.8" 514 | resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" 515 | integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== 516 | 517 | ms@^2.1.3: 518 | version "2.1.3" 519 | resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" 520 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 521 | 522 | nan@^2.17.0: 523 | version "2.20.0" 524 | resolved "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz" 525 | integrity sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw== 526 | 527 | negotiator@0.6.3: 528 | version "0.6.3" 529 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 530 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 531 | 532 | node-domexception@^1.0.0: 533 | version "1.0.0" 534 | resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" 535 | integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== 536 | 537 | node-emoji@^1.11.0: 538 | version "1.11.0" 539 | resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz" 540 | integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== 541 | dependencies: 542 | lodash "^4.17.21" 543 | 544 | node-fetch@^3.3.2: 545 | version "3.3.2" 546 | resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz" 547 | integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== 548 | dependencies: 549 | data-uri-to-buffer "^4.0.0" 550 | fetch-blob "^3.1.4" 551 | formdata-polyfill "^4.0.10" 552 | 553 | node-pty@^1.0.0: 554 | version "1.0.0" 555 | resolved "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz" 556 | integrity sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA== 557 | dependencies: 558 | nan "^2.17.0" 559 | 560 | nopt@~2.1.2: 561 | version "2.1.2" 562 | resolved "https://registry.npmjs.org/nopt/-/nopt-2.1.2.tgz" 563 | integrity sha512-x8vXm7BZ2jE1Txrxh/hO74HTuYZQEbo8edoRcANgdZ4+PCV+pbjd/xdummkmjjC7LU5EjPzlu8zEq/oxWylnKA== 564 | dependencies: 565 | abbrev "1" 566 | 567 | object-assign@^4: 568 | version "4.1.1" 569 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 570 | integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== 571 | 572 | open@^10.1.0: 573 | version "10.1.0" 574 | resolved "https://registry.npmjs.org/open/-/open-10.1.0.tgz" 575 | integrity sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw== 576 | dependencies: 577 | default-browser "^5.2.1" 578 | define-lazy-prop "^3.0.0" 579 | is-inside-container "^1.0.0" 580 | is-wsl "^3.1.0" 581 | 582 | optimist@0.2: 583 | version "0.2.8" 584 | resolved "https://registry.npmjs.org/optimist/-/optimist-0.2.8.tgz" 585 | integrity sha512-Wy7E3cQDpqsTIFyW7m22hSevyTLxw850ahYv7FWsw4G6MIKVTZ8NSA95KBrQ95a4SMsMr1UGUUnwEFKhVaSzIg== 586 | dependencies: 587 | wordwrap ">=0.0.1 <0.1.0" 588 | 589 | optimist@~0.3.4: 590 | version "0.3.7" 591 | resolved "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz" 592 | integrity sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ== 593 | dependencies: 594 | wordwrap "~0.0.2" 595 | 596 | osx-temperature-sensor@^1.0.8: 597 | version "1.0.8" 598 | resolved "https://registry.npmjs.org/osx-temperature-sensor/-/osx-temperature-sensor-1.0.8.tgz" 599 | integrity sha512-Gl3b+bn7+oDDnqPa+4v/cg3yg9lnE8ppS7ivL3opBZh4i7h99JNmkm6zWmo0m2a83UUJu+C9D7lGP0OS8IlehA== 600 | 601 | picture-tuber@^1.0.1: 602 | version "1.0.2" 603 | resolved "https://registry.npmjs.org/picture-tuber/-/picture-tuber-1.0.2.tgz" 604 | integrity sha512-49/xq+wzbwDeI32aPvwQJldM8pr7dKDRuR76IjztrkmiCkAQDaWFJzkmfVqCHmt/iFoPFhHmI9L0oKhthrTOQw== 605 | dependencies: 606 | buffers "~0.1.1" 607 | charm "~0.1.0" 608 | event-stream "~0.9.8" 609 | optimist "~0.3.4" 610 | png-js "~0.1.0" 611 | x256 "~0.0.1" 612 | 613 | png-js@~0.1.0: 614 | version "0.1.1" 615 | resolved "https://registry.npmjs.org/png-js/-/png-js-0.1.1.tgz" 616 | integrity sha512-NTtk2SyfjBm+xYl2/VZJBhFnTQ4kU5qWC7VC4/iGbrgiU4FuB4xC+74erxADYJIqZICOR1HCvRA7EBHkpjTg9g== 617 | 618 | proxy-from-env@^1.1.0: 619 | version "1.1.0" 620 | resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" 621 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 622 | 623 | readable-stream@~1.0.2: 624 | version "1.0.34" 625 | resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" 626 | integrity sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg== 627 | dependencies: 628 | core-util-is "~1.0.0" 629 | inherits "~2.0.1" 630 | isarray "0.0.1" 631 | string_decoder "~0.10.x" 632 | 633 | readline-sync@^1.4.10: 634 | version "1.4.10" 635 | resolved "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz" 636 | integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw== 637 | 638 | redeyed@~2.1.0: 639 | version "2.1.1" 640 | resolved "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz" 641 | integrity sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ== 642 | dependencies: 643 | esprima "~4.0.0" 644 | 645 | run-applescript@^7.0.0: 646 | version "7.0.0" 647 | resolved "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz" 648 | integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== 649 | 650 | sax@>=0.6.0: 651 | version "1.4.1" 652 | resolved "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz" 653 | integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== 654 | 655 | simple-git@^3.26.0: 656 | version "3.27.0" 657 | resolved "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz" 658 | integrity sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA== 659 | dependencies: 660 | "@kwsites/file-exists" "^1.1.1" 661 | "@kwsites/promise-deferred" "^1.1.1" 662 | debug "^4.3.5" 663 | 664 | socket.io-adapter@~2.5.2: 665 | version "2.5.5" 666 | resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" 667 | integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg== 668 | dependencies: 669 | debug "~4.3.4" 670 | ws "~8.17.1" 671 | 672 | socket.io-client@^4.8.1: 673 | version "4.8.1" 674 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.8.1.tgz#1941eca135a5490b94281d0323fe2a35f6f291cb" 675 | integrity sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ== 676 | dependencies: 677 | "@socket.io/component-emitter" "~3.1.0" 678 | debug "~4.3.2" 679 | engine.io-client "~6.6.1" 680 | socket.io-parser "~4.2.4" 681 | 682 | socket.io-parser@~4.2.4: 683 | version "4.2.4" 684 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" 685 | integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== 686 | dependencies: 687 | "@socket.io/component-emitter" "~3.1.0" 688 | debug "~4.3.1" 689 | 690 | socket.io@^4.8.1: 691 | version "4.8.1" 692 | resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a" 693 | integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg== 694 | dependencies: 695 | accepts "~1.3.4" 696 | base64id "~2.0.0" 697 | cors "~2.8.5" 698 | debug "~4.3.2" 699 | engine.io "~6.6.0" 700 | socket.io-adapter "~2.5.2" 701 | socket.io-parser "~4.2.4" 702 | 703 | sparkline@^0.1.1: 704 | version "0.1.2" 705 | resolved "https://registry.npmjs.org/sparkline/-/sparkline-0.1.2.tgz" 706 | integrity sha512-t//aVOiWt9fi/e22ea1vXVWBDX+gp18y+Ch9sKqmHl828bRfvP2VtfTJVEcgWFBQHd0yDPNQRiHdqzCvbcYSDA== 707 | dependencies: 708 | here "0.0.2" 709 | nopt "~2.1.2" 710 | 711 | string-width@^4.2.0: 712 | version "4.2.3" 713 | resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" 714 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== 715 | dependencies: 716 | emoji-regex "^8.0.0" 717 | is-fullwidth-code-point "^3.0.0" 718 | strip-ansi "^6.0.1" 719 | 720 | string_decoder@~0.10.x: 721 | version "0.10.31" 722 | resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" 723 | integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== 724 | 725 | strip-ansi@^3.0.0: 726 | version "3.0.1" 727 | resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" 728 | integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== 729 | dependencies: 730 | ansi-regex "^2.0.0" 731 | 732 | strip-ansi@^6.0.1: 733 | version "6.0.1" 734 | resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" 735 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 736 | dependencies: 737 | ansi-regex "^5.0.1" 738 | 739 | supports-color@^2.0.0: 740 | version "2.0.0" 741 | resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" 742 | integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== 743 | 744 | supports-color@^7.0.0: 745 | version "7.2.0" 746 | resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" 747 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 748 | dependencies: 749 | has-flag "^4.0.0" 750 | 751 | supports-hyperlinks@^2.3.0: 752 | version "2.3.0" 753 | resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz" 754 | integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== 755 | dependencies: 756 | has-flag "^4.0.0" 757 | supports-color "^7.0.0" 758 | 759 | systeminformation@^5.22.8: 760 | version "5.23.4" 761 | resolved "https://registry.npmjs.org/systeminformation/-/systeminformation-5.23.4.tgz" 762 | integrity sha512-mD2R9xnOzKOOmIVtxekosf/ghOE/DGLqAPmsEgQMWJK0pMKxBtX19riz1Ss0tN4omcfS2FQ2RDJ4lkxgADxIPw== 763 | 764 | term-canvas@0.0.5: 765 | version "0.0.5" 766 | resolved "https://registry.npmjs.org/term-canvas/-/term-canvas-0.0.5.tgz" 767 | integrity sha512-eZ3rIWi5yLnKiUcsW8P79fKyooaLmyLWAGqBhFspqMxRNUiB4GmHHk5AzQ4LxvFbJILaXqQZLwbbATLOhCFwkw== 768 | 769 | undici-types@~6.20.0: 770 | version "6.20.0" 771 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" 772 | integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== 773 | 774 | vary@^1: 775 | version "1.1.2" 776 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 777 | integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== 778 | 779 | viem@^2.13.3: 780 | version "2.19.4" 781 | resolved "https://registry.npmjs.org/viem/-/viem-2.19.4.tgz" 782 | integrity sha512-JdhK3ui3uPD2tnpqGNkJaDQV4zTfOeKXcF+VrU8RG88Dn2e0lFjv6l7m0YNmYLsHm+n5vFFfCLcUrTk6xcYv5w== 783 | dependencies: 784 | "@adraffy/ens-normalize" "1.10.0" 785 | "@noble/curves" "1.4.0" 786 | "@noble/hashes" "1.4.0" 787 | "@scure/bip32" "1.4.0" 788 | "@scure/bip39" "1.3.0" 789 | abitype "1.0.5" 790 | isows "1.0.4" 791 | webauthn-p256 "0.0.5" 792 | ws "8.17.1" 793 | 794 | web-streams-polyfill@^3.0.3: 795 | version "3.3.3" 796 | resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz" 797 | integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== 798 | 799 | webauthn-p256@0.0.5: 800 | version "0.0.5" 801 | resolved "https://registry.npmjs.org/webauthn-p256/-/webauthn-p256-0.0.5.tgz" 802 | integrity sha512-drMGNWKdaixZNobeORVIqq7k5DsRC9FnG201K2QjeOoQLmtSDaSsVZdkg6n5jUALJKcAG++zBPJXmv6hy0nWFg== 803 | dependencies: 804 | "@noble/curves" "^1.4.0" 805 | "@noble/hashes" "^1.4.0" 806 | 807 | "wordwrap@>=0.0.1 <0.1.0", wordwrap@~0.0.2: 808 | version "0.0.3" 809 | resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" 810 | integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw== 811 | 812 | ws@8.17.1, ws@~8.17.1: 813 | version "8.17.1" 814 | resolved "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz" 815 | integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== 816 | 817 | ws@^8.15.0: 818 | version "8.18.0" 819 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" 820 | integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== 821 | 822 | x256@>=0.0.1, x256@~0.0.1: 823 | version "0.0.2" 824 | resolved "https://registry.npmjs.org/x256/-/x256-0.0.2.tgz" 825 | integrity sha512-ZsIH+sheoF8YG9YG+QKEEIdtqpHRA9FYuD7MqhfyB1kayXU43RUNBFSxBEnF8ywSUxdg+8no4+bPr5qLbyxKgA== 826 | 827 | xml2js@^0.4.5: 828 | version "0.4.23" 829 | resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz" 830 | integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== 831 | dependencies: 832 | sax ">=0.6.0" 833 | xmlbuilder "~11.0.0" 834 | 835 | xmlbuilder@~11.0.0: 836 | version "11.0.1" 837 | resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz" 838 | integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== 839 | 840 | xmlhttprequest-ssl@~2.1.1: 841 | version "2.1.2" 842 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz#e9e8023b3f29ef34b97a859f584c5e6c61418e23" 843 | integrity sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ== 844 | --------------------------------------------------------------------------------