mirror of
https://github.com/stake-house/wagyu-installer.git
synced 2026-01-10 05:37:56 -05:00
Compare commits
63 Commits
cs/rp_upda
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c68c0a3b2 | ||
|
|
359f79b560 | ||
|
|
e38a7ec2a6 | ||
|
|
44f0406b50 | ||
|
|
d71eb42be2 | ||
|
|
98cd2751e4 | ||
|
|
e1c50a6f05 | ||
|
|
b391000f50 | ||
|
|
d512bd7d39 | ||
|
|
48c7a52f88 | ||
|
|
c11f801bba | ||
|
|
66a547af8f | ||
|
|
d522adb697 | ||
|
|
2dd4597de8 | ||
|
|
045da87f80 | ||
|
|
12e67c2a62 | ||
|
|
cd88d67e14 | ||
|
|
2c13ec4e0f | ||
|
|
fc0458adfd | ||
|
|
4b90e3523d | ||
|
|
aabe8336e2 | ||
|
|
dc9dda928f | ||
|
|
a69cc00779 | ||
|
|
2252d3fba0 | ||
|
|
ff61301dce | ||
|
|
0dd573bf72 | ||
|
|
b052fdf9bb | ||
|
|
92aec97fb2 | ||
|
|
41add19701 | ||
|
|
9f8043fe9e | ||
|
|
f642d95423 | ||
|
|
dbf3dca826 | ||
|
|
7e220d21fd | ||
|
|
efdee8d745 | ||
|
|
31ce474ac8 | ||
|
|
1e5d45ad08 | ||
|
|
afbc0faebf | ||
|
|
4342302edf | ||
|
|
cda841e514 | ||
|
|
65943961b6 | ||
|
|
afe25c38fb | ||
|
|
46e1e3dc28 | ||
|
|
d09083f02d | ||
|
|
5dc792eb8b | ||
|
|
b2e17ef176 | ||
|
|
2db500f7e1 | ||
|
|
6fa91ec166 | ||
|
|
370dd53579 | ||
|
|
041f3548b6 | ||
|
|
754d292872 | ||
|
|
f236bca658 | ||
|
|
e221ab7432 | ||
|
|
24683cd39f | ||
|
|
52a7d8e0d7 | ||
|
|
a6a9f16e1f | ||
|
|
7e3c866cbf | ||
|
|
c7e460d370 | ||
|
|
53a93c31d9 | ||
|
|
b51f3b36bf | ||
|
|
ad8033b2c7 | ||
|
|
0ee5a5d05a | ||
|
|
a1777f6ae5 | ||
|
|
624fd51ec8 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,2 +1,11 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
build/
|
||||
13
.prettierrc
13
.prettierrc
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"useTabs": false
|
||||
}
|
||||
|
||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"workbench.tree.renderIndentGuides": "always",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"eslint.options": {
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"prefer-const": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
768
.yarn/releases/yarn-berry.cjs
vendored
Executable file
768
.yarn/releases/yarn-berry.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
2
.yarnrc.yml
Normal file
2
.yarnrc.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
yarnPath: ".yarn/releases/yarn-berry.cjs"
|
||||
nodeLinker: node-modules
|
||||
42
Dockerfile
Normal file
42
Dockerfile
Normal file
@@ -0,0 +1,42 @@
|
||||
# credit https://trigodev.com/blog/develop-electron-in-docker
|
||||
FROM node:16.15.0
|
||||
|
||||
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install \
|
||||
build-essential clang policykit-1 \
|
||||
git libx11-xcb1 libxcb-dri3-0 libcups2-dev libxtst-dev libatk-bridge2.0-0 libdbus-1-dev libgtk-3-dev libxss1 libnotify-dev libasound2-dev libcap-dev libdrm2 libice6 libsm6 \
|
||||
xorg openbox libatk-adaptor \
|
||||
gperf bison python3-dbusmock \
|
||||
libnss3-dev gcc-multilib g++-multilib curl sudo \
|
||||
-yq --no-install-suggests --no-install-recommends \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
lsb-release
|
||||
|
||||
RUN curl -fsSL https://get.docker.com | sh
|
||||
# RUN echo 'alias docker-compose="docker compose"' >> /home/node/.bashrc
|
||||
RUN apt-get install docker-compose
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN chown -R node /app
|
||||
|
||||
# install node modules and perform an electron rebuild
|
||||
USER node
|
||||
RUN npm install
|
||||
RUN npx electron-rebuild
|
||||
|
||||
USER root
|
||||
RUN chown root /app/node_modules/electron/dist/chrome-sandbox
|
||||
RUN chmod 4755 /app/node_modules/electron/dist/chrome-sandbox
|
||||
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
RUN usermod -aG sudo node
|
||||
RUN usermod -aG docker node
|
||||
|
||||
|
||||
USER node
|
||||
ENTRYPOINT [ "bash" ]
|
||||
# docker run --privileged -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$DISPLAY -v `pwd`/src:/app/src --rm -it electron-wrapper bash
|
||||
64
README.md
64
README.md
@@ -1,13 +1,16 @@
|
||||
# Stakehouse
|
||||
Stakehouse is an application aimed at lowering the technical bar to staking on Ethereum 2.0.
|
||||
# Wagyu
|
||||
|
||||
## Download at [https://wagyu.gg](https://wagyu.gg)
|
||||
|
||||
Wagyu (formerly known as StakeHouse) is an application aimed at lowering the technical bar to staking on Ethereum 2.0.
|
||||
|
||||
|
||||
Dubbed a 'one-click installer', it provides a clean UI automating the setup and management of all the infrastructure necessary to stake without the user needing to have any technical knowledge.
|
||||
|
||||
[](https://www.youtube.com/watch?v=-KKeZwI8EII&ab_channel=ColfaxSelby)
|
||||
[](https://www.youtube.com/watch?v=-KKeZwI8EII&ab_channel=ColfaxSelby)
|
||||
|
||||
## Disclaimer
|
||||
Stakehouse:
|
||||
Wagyu:
|
||||
- only runs on Ubuntu (see below for more system requirements)
|
||||
- is not audited - use at your own risk
|
||||
- is currently experimental and still under development
|
||||
@@ -15,16 +18,29 @@ Stakehouse:
|
||||
- does not currently do anything with real ETH or mainnet (__DO NOT USE REAL ETH__)
|
||||
|
||||
## Demo
|
||||
StakeHouse did a demo at the EthStaker Validator Workshop - take a look [here](https://youtu.be/cxP9gwapXJ0).
|
||||
Wagyu (while it was known as StakeHouse) did a demo at the EthStaker Validator Workshop - take a look [here](https://youtu.be/cxP9gwapXJ0).
|
||||
|
||||
## Staking Resources
|
||||
We are going to assume if you've gotten this far you already know about Ethereum and Ethereum 2.0, if not we suggest starting with [Superphiz's overview](https://www.youtube.com/watch?v=tpkpW031RCI) then heading over to [the EthStaker reddit](https://www.reddit.com/r/ethstaker/comments/jjdxvw/welcome_to_rethstaker_the_home_for_ethereum/) and [checking out our Studymaster program](https://www.reddit.com/r/ethstaker/wiki/studymaster).
|
||||
|
||||
The key to the Eth2 upgrades is the introduction of staking and the transition from Proof-of-Work to Proof-of-Stake. This transition, referred to as "The Merge", will make Ethereum more secure and sustainable while unlocking what is needed to achieve scalability through sharding. By becoming an Ethereum validator and staking your Ethereum you can do a public good, securing the network, while earning rewards.
|
||||
|
||||
Learn More:
|
||||
|
||||
- [A Comprehensive overview of Eth 2.0 and Staking for beginners](https://www.youtube.com/watch?v=tpkpW031RCI) by Superphiz
|
||||
- [Proof-of-Stake FAQs](https://eth.wiki/en/concepts/proof-of-stake-faqs) Ethereum Wiki by the Ethereum Foundation
|
||||
- [Staking FAQs](https://www.reddit.com/r/ethstaker/comments/ju61pf/ethstaker_faq/) by Lamboshi
|
||||
- [Staking hardware guide](https://ethstaker.cc/a-comprehensive-look-at-hardware-for-staking-by-u-lamboshinakaghini/) by Lamboshi
|
||||
- [A country's worth of power, no more!](https://blog.ethereum.org/2021/05/18/country-power-no-more/) by Carl Beekhuizen of the Ethereum Foundation
|
||||
|
||||
## Usage
|
||||
There are no releases yet. Please see Development section.
|
||||
There are no releases yet, please see the [setup](#setup) section under development to run the latest version.
|
||||
|
||||
## Getting Involved
|
||||
Theres plenty left to do with StakeHouse. If you'd like to help out come join us on the [EthStaker](http://invite.gg/ethstaker) discord, channel #stakehouse, or reach out to Colfax (discord username: colfax#1983) directly.
|
||||
Theres plenty left to do with Wagyu. If you'd like to help out come join us on the [EthStaker](http://invite.gg/ethstaker) discord, channel #stakehouse, or reach out to Colfax (discord username: colfax#1983) directly.
|
||||
|
||||
|
||||
Also try it out (feedback welcome!), take a look our demo above, and/or browse our open [issues](https://github.com/ethstaker-core/stakehouse/issues) (start by filtering by the label 'small').
|
||||
Also try it out (feedback welcome!), take a look our demo above, and/or browse our open [issues](https://github.com/stake-house/wagyu/issues) (start by filtering by the label 'small').
|
||||
|
||||
## Development
|
||||
### Requirements
|
||||
@@ -34,19 +50,33 @@ Also try it out (feedback welcome!), take a look our demo above, and/or browse o
|
||||
- root access
|
||||
|
||||
### Setup
|
||||
Stakehouse is a React app running in Electron and currently *only runs on Ubuntu* (tested on version 20.04). See `src/electron/` for the simple electron app and `src/react/` for where the magic happens. Feedback and help is much encouraged so please reach out!
|
||||
Wagyu is a React app running in Electron and currently *only runs on Ubuntu* (tested on version 20.04). See `src/electron/` for the simple electron app and `src/react/` for where the magic happens. Feedback and help is much encouraged so please reach out!
|
||||
|
||||
Start by cloning this repo and enter the directory by running `git clone https://github.com/stake-house/wagyu.git` and `cd wagyu`. Then run the following:
|
||||
- `yarn install`
|
||||
- `yarn run build:watch` in one terminal - this continually watches and builds your ts code and puts it in `./dist`
|
||||
- `yarn start` in another terminal - this runs your app (the code in `./dist`)
|
||||
|
||||
_If you make changes, save them and they will automatically build. In order to get them to show in the app press `ctrl+r` or `cmd+r`_
|
||||
- `yarn build` (or run `yarn run build:watch` in a separate terminal to hot reload your changes)
|
||||
- _If you are running with `build:watch` after saving your changes will automatically build. In order to get them to show in the app press `ctrl+r` or `cmd+r`._
|
||||
- `yarn start`
|
||||
|
||||
### Installing Yarn on Ubuntu
|
||||
Run the following commands:
|
||||
1) `sudo apt remove cmdtest yarn`
|
||||
2) `sudo apt install npm`
|
||||
3) `sudo npm install -g yarn`
|
||||
|
||||
1) `sudo apt update`
|
||||
2) `sudo apt remove cmdtest yarn`
|
||||
3) `sudo apt install npm`
|
||||
4) `sudo npm install -g yarn`
|
||||
|
||||
### Contribution Guidelines
|
||||
To streamline contributions to React code within the Wagyu codebase, a small set of guidelines encapsulating our opinions is included here:
|
||||
1) Libraries - Use `styled-components` for css encapsulation, `rem` units for any pixel sizes (potentially via the utility library `polished`).
|
||||
2) Typescript - Make generous use of everything typescript has to offer (don't use `any`... ever).
|
||||
3) Formatting - This codebase uses `prettier` (rules defined in `.prettierrc`) for standardized code formatting. If using vscode, the formatter will run on save, if not, be sure to run `npx prettier --write "**/*.ts*"` to format the code.
|
||||
4) Common components - Extract commonly used typography or UI elements into the `typography` directory.
|
||||
5) Utility Functions - Keep utility functions that don't deal with rendering outside of components.
|
||||
6) Other Opinions - Refrain from using 'render' functions within functional components. Extract this logic out into a separate component instead. Refrain from using `<br />` and use padding and/or margin instead.
|
||||
|
||||
Refer to this [React style guide](https://alexkondov.com/tao-of-react/) for more information.
|
||||
|
||||
|
||||
## Support
|
||||
Reach out to the EthStaker community:
|
||||
@@ -57,6 +87,6 @@ Reach out to the EthStaker community:
|
||||
[GPL](LICENSE)
|
||||
|
||||
## Credits
|
||||
- Stakehouse uses the [Rocket Pool](https://www.rocketpool.net/) installer to manage eth1/eth2 clients.
|
||||
- Wagyu uses the [Rocket Pool](https://www.rocketpool.net/) installer to manage eth1/eth2 clients.
|
||||
- [EthStaker](https://www.reddit.com/r/ethstaker/) for the incredible guidance and support.
|
||||
- [Somer Esat](https://someresat.medium.com/)'s guides for the introductory knowledge.
|
||||
|
||||
86
package.json
86
package.json
@@ -1,38 +1,68 @@
|
||||
{
|
||||
"name": "stakehouse",
|
||||
"version": "1.0.0",
|
||||
"description": "Stakehouse: a portal into the eth2 staking world.",
|
||||
"main": "./dist/electron/index.js",
|
||||
"author": "Colfax Selby <colfax.selby@gmail.com>",
|
||||
"license": "MIT",
|
||||
"name": "wagyuinstaller",
|
||||
"productName": "Wagyu Installer",
|
||||
"version": "0.6.0",
|
||||
"description": "Application aimed at lowering the technical bar to staking on Ethereum",
|
||||
"main": "./build/electron/index.js",
|
||||
"author": "Rémy Roy <remyroy@remyroy.com>",
|
||||
"license": "GPL",
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.0",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.1",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/styled-components": "^5.1.7",
|
||||
"electron": "^12.0.0",
|
||||
"ts-loader": "^8.0.17",
|
||||
"typescript": "^4.2.2",
|
||||
"webpack": "^5.24.2",
|
||||
"webpack-cli": "^4.5.0",
|
||||
"webpack-serve": "^3.2.0"
|
||||
"@types/react": "^17.0.14",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"css-loader": "^6.7.0",
|
||||
"electron": "^17.1.0",
|
||||
"electron-builder": "^22.14.5",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-loader": "^9.2.3",
|
||||
"typescript": "^4.3.5",
|
||||
"webpack": "^5.64.3",
|
||||
"webpack-cli": "^4.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.react.config.js --config webpack.electron.config.js",
|
||||
"build:watch": "yarn build -- --watch",
|
||||
"start": "electron ./dist/electron/index.js",
|
||||
"dev:electron": "NODE_ENV=development webpack --config webpack.electron.config.js --mode development && electron .",
|
||||
"dev:react": "NODE_ENV=development webpack-serve --config webpack.react.config.js --mode development"
|
||||
"build:watch": "webpack --config webpack.react.config.js --config webpack.electron.config.js --watch",
|
||||
"start": "electron ./build/electron/index.js",
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "electron-builder"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rauschma/stringio": "^1.4.0",
|
||||
"html-webpack-plugin": "^5.2.0",
|
||||
"js-yaml": "^4.0.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"@emotion/react": "^11.8.1",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@fontsource/roboto": "^4.5.3",
|
||||
"@mui/icons-material": "^5.4.4",
|
||||
"@mui/material": "^5.4.4",
|
||||
"command-join": "^3.0.0",
|
||||
"generate-password": "^1.7.0",
|
||||
"git-revision-webpack-plugin": "^5.0.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"shebang-loader": "^0.0.1",
|
||||
"styled-components": "^5.2.1"
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"tmp-promise": "^3.0.3"
|
||||
},
|
||||
"build": {
|
||||
"appId": "gg.wagyu.installer",
|
||||
"productName": "Wagyu Installer",
|
||||
"files": [
|
||||
"build/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"extraFiles": [
|
||||
"static/icon.png"
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.utilities"
|
||||
},
|
||||
"linux": {
|
||||
"target": "AppImage",
|
||||
"icon": "static/icon.png"
|
||||
},
|
||||
"win": {
|
||||
"target": "portable",
|
||||
"icon": "static/icon.ico"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
73
src/electron/BashUtils.ts
Normal file
73
src/electron/BashUtils.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// BashUtils.ts
|
||||
/**
|
||||
* This BashUtils module provides different file and OS utility functions. Those functions should
|
||||
* work across all our supported operating systems including Linux, macOS and Windows.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { constants, readdir } from 'fs';
|
||||
import { access, stat } from 'fs/promises';
|
||||
|
||||
import path from "path";
|
||||
|
||||
const readdirProm = promisify(readdir);
|
||||
|
||||
/**
|
||||
* Check for the existence of a file or a directory on the filesystem.
|
||||
*
|
||||
* @param filename The path to the file or directory.
|
||||
*
|
||||
* @returns Returns a Promise<boolean> that includes a true value if file or directory exists.
|
||||
*/
|
||||
const doesFileExist = async (filename: string): Promise<boolean> => {
|
||||
try {
|
||||
await access(filename, constants.F_OK);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check for the existence of a directory on the filesystem.
|
||||
*
|
||||
* @param directory The path to the directory.
|
||||
*
|
||||
* @returns Returns a Promise<boolean> that includes a true value if the directory exists.
|
||||
*/
|
||||
const doesDirectoryExist = async (directory: string): Promise<boolean> => {
|
||||
if (await doesFileExist(directory)) {
|
||||
return (await stat(directory)).isDirectory();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first file whom filename starts with some value in a directory.
|
||||
*
|
||||
* @param directory The path to the directory.
|
||||
* @param startsWith Filename match to look for.
|
||||
*
|
||||
* @returns Returns a Promise<string> that includes the path to the file if found. Returns empty
|
||||
* string if not found.
|
||||
*/
|
||||
const findFirstFile = async (directory: string, startsWith: string): Promise<string> => {
|
||||
const entries = await readdirProm(directory, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && entry.name.startsWith(startsWith)) {
|
||||
return path.join(directory, entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export {
|
||||
doesFileExist,
|
||||
doesDirectoryExist,
|
||||
findFirstFile
|
||||
};
|
||||
679
src/electron/EthDockerInstaller.ts
Normal file
679
src/electron/EthDockerInstaller.ts
Normal file
@@ -0,0 +1,679 @@
|
||||
import sudo from 'sudo-prompt';
|
||||
|
||||
import { commandJoin } from "command-join";
|
||||
import { generate as generate_password } from "generate-password";
|
||||
|
||||
import { exec, execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { withDir } from 'tmp-promise';
|
||||
|
||||
import { open, rm, mkdir } from 'fs/promises';
|
||||
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
import {
|
||||
ExecutionClient,
|
||||
ConsensusClient,
|
||||
IMultiClientInstaller,
|
||||
NodeStatus,
|
||||
ValidatorStatus,
|
||||
InstallDetails,
|
||||
OutputLogs
|
||||
} from "./IMultiClientInstaller";
|
||||
import { Network, networkToExecution } from '../react/types';
|
||||
import { doesFileExist, doesDirectoryExist } from './BashUtils';
|
||||
|
||||
const execFileProm = promisify(execFile);
|
||||
const execProm = promisify(exec);
|
||||
|
||||
const dockerServiceName = 'docker.service';
|
||||
const dockerGroupName = 'docker';
|
||||
const installPath = path.join(os.homedir(), '.wagyu-installer');
|
||||
const ethDockerGitRepository = 'https://github.com/eth-educators/eth-docker.git';
|
||||
const prysmWalletPasswordFileName = 'prysm-wallet-password';
|
||||
|
||||
type SystemdServiceDetails = {
|
||||
description: string | undefined;
|
||||
loadState: string | undefined;
|
||||
activeState: string | undefined;
|
||||
subState: string | undefined;
|
||||
unitFileState: string | undefined;
|
||||
}
|
||||
|
||||
const writeOutput = (message: string, outputLogs?: OutputLogs): void => {
|
||||
if (outputLogs) {
|
||||
outputLogs(message);
|
||||
}
|
||||
};
|
||||
|
||||
export class EthDockerInstaller implements IMultiClientInstaller {
|
||||
|
||||
title = 'Electron';
|
||||
|
||||
async preInstall(outputLogs?: OutputLogs): Promise<boolean> {
|
||||
|
||||
let packagesToInstall = new Array<string>();
|
||||
|
||||
// We need git installed
|
||||
const gitPackageName = 'git';
|
||||
|
||||
writeOutput(`Checking if ${gitPackageName} is installed.`, outputLogs);
|
||||
const gitInstalled = await this.checkForInstalledUbuntuPackage(gitPackageName);
|
||||
if (!gitInstalled) {
|
||||
writeOutput(`${gitPackageName} is not installed. We will install it.`, outputLogs);
|
||||
packagesToInstall.push(gitPackageName);
|
||||
} else {
|
||||
writeOutput(`${gitPackageName} is already installed. We will not install it.`, outputLogs);
|
||||
}
|
||||
|
||||
// We need docker installed, enabled and running
|
||||
const dockerPackageName = 'docker-compose';
|
||||
let needToEnableDockerService = true;
|
||||
let needToStartDockerService = false;
|
||||
|
||||
writeOutput(`Checking if ${dockerPackageName} is installed.`, outputLogs);
|
||||
const dockerInstalled = await this.checkForInstalledUbuntuPackage(dockerPackageName);
|
||||
if (!dockerInstalled) {
|
||||
writeOutput(`${dockerPackageName} is not installed. We will install it.`, outputLogs);
|
||||
packagesToInstall.push(dockerPackageName);
|
||||
} else {
|
||||
writeOutput(`${dockerPackageName} is already installed. We will not install it.`, outputLogs);
|
||||
|
||||
writeOutput(`Checking if we need to enable or start the ${dockerServiceName} service.`, outputLogs);
|
||||
const dockerServiceDetails = await this.getSystemdServiceDetails(dockerServiceName);
|
||||
needToEnableDockerService = dockerServiceDetails.unitFileState != 'enabled';
|
||||
if (needToEnableDockerService) {
|
||||
writeOutput(`The ${dockerServiceName} service is not enabled. We will enable it.`, outputLogs);
|
||||
}
|
||||
needToStartDockerService = dockerServiceDetails.subState != 'running';
|
||||
if (needToStartDockerService) {
|
||||
writeOutput(`The ${dockerServiceName} service is not started. We will start it.`, outputLogs);
|
||||
}
|
||||
}
|
||||
|
||||
// We need our user to be in docker group
|
||||
writeOutput(`Checking if current user is in ${dockerGroupName} group.`, outputLogs);
|
||||
const needUserInDockerGroup = !await this.isUserInGroup(dockerGroupName);
|
||||
if (needUserInDockerGroup) {
|
||||
writeOutput(`Current user is not in ${dockerGroupName} group. We will add it.`, outputLogs);
|
||||
}
|
||||
|
||||
// We need our installPath directory
|
||||
writeOutput(`Creating install directory in ${installPath}.`, outputLogs);
|
||||
await mkdir(installPath, { recursive: true });
|
||||
|
||||
return await this.preInstallAdminScript(
|
||||
packagesToInstall,
|
||||
needUserInDockerGroup,
|
||||
needToEnableDockerService,
|
||||
needToStartDockerService,
|
||||
outputLogs);
|
||||
}
|
||||
|
||||
async getSystemdServiceDetails(serviceName: string): Promise<SystemdServiceDetails> {
|
||||
let resultValue: SystemdServiceDetails = {
|
||||
description: undefined,
|
||||
loadState: undefined,
|
||||
activeState: undefined,
|
||||
subState: undefined,
|
||||
unitFileState: undefined
|
||||
};
|
||||
|
||||
const properties = ['Description', 'LoadState', 'ActiveState', 'SubState', 'UnitFileState'];
|
||||
|
||||
const { stdout, stderr } = await execFileProm('systemctl',
|
||||
['show', serviceName, '--property=' + properties.join(',')]);
|
||||
|
||||
const lines = stdout.split('\n');
|
||||
const lineRegex = /(?<key>[^=]+)=(?<value>.*)/;
|
||||
for (const line of lines) {
|
||||
const found = line.match(lineRegex);
|
||||
if (found) {
|
||||
const key = found.groups?.key;
|
||||
const value = found.groups?.value.trim();
|
||||
|
||||
switch (key) {
|
||||
case "Description":
|
||||
resultValue.description = value;
|
||||
break;
|
||||
case "LoadState":
|
||||
resultValue.loadState = value;
|
||||
break;
|
||||
case "ActiveState":
|
||||
resultValue.activeState = value;
|
||||
break;
|
||||
case "SubState":
|
||||
resultValue.subState = value;
|
||||
break;
|
||||
case "UnitFileState":
|
||||
resultValue.unitFileState = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultValue;
|
||||
}
|
||||
|
||||
async preInstallAdminScript(
|
||||
packagesToInstall: Array<string>,
|
||||
needUserInDockerGroup: boolean,
|
||||
needToEnableDockerService: boolean,
|
||||
needToStartDockerService: boolean,
|
||||
outputLogs?: OutputLogs): Promise<boolean> {
|
||||
|
||||
if (
|
||||
packagesToInstall.length > 0 ||
|
||||
needUserInDockerGroup ||
|
||||
needToEnableDockerService ||
|
||||
needToStartDockerService
|
||||
) {
|
||||
// We need to perform some admin commands.
|
||||
// Create script and execute it with sudo. This will minimize the amount of password prompts.
|
||||
|
||||
let commandResult = false;
|
||||
|
||||
await withDir(async dirResult => {
|
||||
|
||||
const scriptPath = path.join(dirResult.path, 'commands.sh');
|
||||
let scriptContent = '';
|
||||
|
||||
const scriptFile = await open(scriptPath, 'w');
|
||||
await scriptFile.write('#!/bin/bash\n');
|
||||
|
||||
// Install APT packages
|
||||
if (packagesToInstall.length > 0) {
|
||||
const aptUpdate = 'apt -y update\n';
|
||||
const aptInstall = 'apt -y install ' + commandJoin(packagesToInstall) + '\n';
|
||||
|
||||
scriptContent += aptUpdate + aptInstall;
|
||||
|
||||
await scriptFile.write(aptUpdate);
|
||||
await scriptFile.write(aptInstall);
|
||||
}
|
||||
|
||||
// Enable docker service
|
||||
if (needToEnableDockerService) {
|
||||
const systemctlEnable = 'systemctl enable --now ' + commandJoin([dockerServiceName]) + '\n';
|
||||
|
||||
scriptContent += systemctlEnable;
|
||||
|
||||
await scriptFile.write(systemctlEnable);
|
||||
}
|
||||
|
||||
// Start docker service
|
||||
if (needToStartDockerService) {
|
||||
const systemctlStart = 'systemctl start ' + commandJoin([dockerServiceName]) + '\n';
|
||||
|
||||
scriptContent += systemctlStart;
|
||||
|
||||
await scriptFile.write(systemctlStart);
|
||||
}
|
||||
|
||||
// Add user in docker group
|
||||
if (needUserInDockerGroup) {
|
||||
const userName = await this.getUsername();
|
||||
const usermodUser = `usermod -aG ${dockerGroupName} ${userName}\n`;
|
||||
|
||||
scriptContent += usermodUser;
|
||||
|
||||
await scriptFile.write(usermodUser);
|
||||
}
|
||||
|
||||
await scriptFile.chmod(0o500);
|
||||
await scriptFile.close();
|
||||
|
||||
writeOutput(`Running script ${scriptPath} with the following content as root:\n${scriptContent}`, outputLogs);
|
||||
|
||||
const promise = new Promise<boolean>(async (resolve, reject) => {
|
||||
const options = {
|
||||
name: this.title
|
||||
};
|
||||
try {
|
||||
sudo.exec(scriptPath, options,
|
||||
function (error, stdout, stderr) {
|
||||
if (error) reject(error);
|
||||
if (stdout) {
|
||||
writeOutput(stdout.toString(), outputLogs);
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
await promise.then(result => {
|
||||
commandResult = result;
|
||||
}).catch(reason => {
|
||||
commandResult = false;
|
||||
}).finally(async () => {
|
||||
await rm(scriptPath);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return commandResult;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async getUsername(): Promise<string> {
|
||||
const { stdout, stderr } = await execFileProm('whoami');
|
||||
const userName = stdout.trim();
|
||||
|
||||
return userName;
|
||||
}
|
||||
|
||||
async isUserInGroup(groupName: string): Promise<boolean> {
|
||||
const userName = await this.getUsername();
|
||||
|
||||
const { stdout, stderr } = await execFileProm('groups', [userName]);
|
||||
const groups = stdout.trim().split(' ');
|
||||
return groups.findIndex(val => val === groupName) >= 0;
|
||||
}
|
||||
|
||||
async checkForInstalledUbuntuPackage(packageName: string): Promise<boolean> {
|
||||
const { stdout, stderr } = await execFileProm('apt', ['-qq', 'list', packageName]);
|
||||
return stdout.indexOf('[installed]') > 0
|
||||
}
|
||||
|
||||
async install(details: InstallDetails, outputLogs?: OutputLogs): Promise<boolean> {
|
||||
// Install and update eth-docker
|
||||
if (!await this.installUpdateEthDockerCode(details.network)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create .env file with all the configuration details
|
||||
if (!await this.createEthDockerEnvFile(details)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build the clients
|
||||
if (!await this.buildClients(details.network)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async buildClients(network: Network): Promise<boolean> {
|
||||
const networkPath = path.join(installPath, network.toLowerCase());
|
||||
const ethDockerPath = path.join(networkPath, 'eth-docker');
|
||||
|
||||
const ethdCommand = path.join(ethDockerPath, 'ethd');
|
||||
const bashScript = `
|
||||
/usr/bin/newgrp ${dockerGroupName} <<EONG
|
||||
${ethdCommand} cmd build --pull
|
||||
EONG
|
||||
`;
|
||||
|
||||
const returnProm = execProm(bashScript, { shell: '/bin/bash', cwd: ethDockerPath });
|
||||
const { stdout, stderr } = await returnProm;
|
||||
|
||||
if (returnProm.child.exitCode !== 0) {
|
||||
console.log('We failed to build eth-docker clients.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async createEthDockerEnvFile(details: InstallDetails): Promise<boolean> {
|
||||
const networkPath = path.join(installPath, details.network.toLowerCase());
|
||||
const ethDockerPath = path.join(networkPath, 'eth-docker');
|
||||
|
||||
// Start with the default env file.
|
||||
const defaultEnvPath = path.join(ethDockerPath, 'default.env');
|
||||
const envPath = path.join(ethDockerPath, '.env');
|
||||
|
||||
// Open default env file and update the configs.
|
||||
const defaultEnvFile = await open(defaultEnvPath, 'r');
|
||||
const defaultEnvConfigs = await defaultEnvFile.readFile({ encoding: 'utf8' });
|
||||
await defaultEnvFile.close();
|
||||
|
||||
let envConfigs = defaultEnvConfigs;
|
||||
|
||||
// Writing consensus network
|
||||
const networkValue = details.network.toLowerCase();
|
||||
envConfigs = envConfigs.replace(/NETWORK=(.*)/, `NETWORK=${networkValue}`);
|
||||
|
||||
// Writing execution network
|
||||
const ecNetworkValue = networkToExecution.get(details.network)?.toLowerCase() as string;
|
||||
envConfigs = envConfigs.replace(/EC_NETWORK=(.*)/, `EC_NETWORK=${ecNetworkValue}`);
|
||||
|
||||
let composeFileValues = new Array<string>();
|
||||
|
||||
switch (details.consensusClient) {
|
||||
case ConsensusClient.LIGHTHOUSE:
|
||||
composeFileValues.push('lh-base.yml');
|
||||
break;
|
||||
case ConsensusClient.NIMBUS:
|
||||
composeFileValues.push('nimbus-base.yml');
|
||||
break;
|
||||
case ConsensusClient.PRYSM:
|
||||
composeFileValues.push('prysm-base.yml');
|
||||
break;
|
||||
case ConsensusClient.TEKU:
|
||||
composeFileValues.push('teku-base.yml');
|
||||
break;
|
||||
case ConsensusClient.LODESTAR:
|
||||
composeFileValues.push('lodestar-base.yml');
|
||||
break;
|
||||
}
|
||||
|
||||
switch (details.executionClient) {
|
||||
case ExecutionClient.GETH:
|
||||
composeFileValues.push('geth.yml');
|
||||
break;
|
||||
case ExecutionClient.NETHERMIND:
|
||||
composeFileValues.push('nm.yml');
|
||||
break;
|
||||
case ExecutionClient.BESU:
|
||||
composeFileValues.push('besu.yml');
|
||||
break;
|
||||
case ExecutionClient.ERIGON:
|
||||
composeFileValues.push('erigon.yml');
|
||||
break;
|
||||
}
|
||||
|
||||
const composeFileValue = composeFileValues.join(':');
|
||||
envConfigs = envConfigs.replace(/COMPOSE_FILE=(.*)/, `COMPOSE_FILE=${composeFileValue}`);
|
||||
|
||||
// Write our new env file
|
||||
const envFile = await open(envPath, 'w');
|
||||
envFile.writeFile(envConfigs, { encoding: 'utf8' });
|
||||
await envFile.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async installUpdateEthDockerCode(network: Network): Promise<boolean> {
|
||||
const networkPath = path.join(installPath, network.toLowerCase());
|
||||
|
||||
// Make sure the networkPath is a directory
|
||||
const networkPathExists = await doesFileExist(networkPath);
|
||||
const networkPathIsDir = networkPathExists && await doesDirectoryExist(networkPath);
|
||||
if (!networkPathExists) {
|
||||
await mkdir(networkPath, { recursive: true });
|
||||
} else if (networkPathExists && !networkPathIsDir) {
|
||||
await rm(networkPath);
|
||||
await mkdir(networkPath, { recursive: true });
|
||||
}
|
||||
|
||||
const ethDockerPath = path.join(networkPath, 'eth-docker');
|
||||
|
||||
const ethDockerPathExists = await doesFileExist(ethDockerPath);
|
||||
const ethDockerPathIsDir = ethDockerPathExists && await doesDirectoryExist(ethDockerPath);
|
||||
let needToClone = !ethDockerPathExists;
|
||||
|
||||
if (ethDockerPathExists && !ethDockerPathIsDir) {
|
||||
await rm(ethDockerPath);
|
||||
needToClone = true;
|
||||
} else if (ethDockerPathIsDir) {
|
||||
// Check if eth-docker was already cloned.
|
||||
const returnProm = execFileProm('git', ['remote', 'show', 'origin'], { cwd: ethDockerPath });
|
||||
const { stdout, stderr } = await returnProm;
|
||||
|
||||
if (returnProm.child.exitCode === 0) {
|
||||
// Check for origin being ethDockerGitRepository
|
||||
const remoteMatch = stdout.match(/Fetch URL: (?<remote>.+)/);
|
||||
if (remoteMatch) {
|
||||
if (remoteMatch.groups?.remote.trim() === ethDockerGitRepository) {
|
||||
needToClone = false;
|
||||
} else {
|
||||
// Git repository with the wrong remote.
|
||||
await rm(ethDockerPath, { recursive: true, force: true });
|
||||
needToClone = true;
|
||||
}
|
||||
} else {
|
||||
console.log('Cannot parse `git remote show origin` output.');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Not a git repository or does not have origin remote
|
||||
await rm(ethDockerPath, { recursive: true, force: true });
|
||||
needToClone = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needToClone) {
|
||||
// Clone repository if needed
|
||||
const returnProm = execFileProm('git', ['clone', ethDockerGitRepository], { cwd: networkPath });
|
||||
const { stdout, stderr } = await returnProm;
|
||||
|
||||
if (returnProm.child.exitCode !== 0) {
|
||||
console.log('We failed to clone eth-docker repository.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate Prysm wallet password and store it in plain text
|
||||
const walletPassword = generate_password({
|
||||
length: 32,
|
||||
numbers: true
|
||||
});
|
||||
const walletPasswordPath = path.join(ethDockerPath, prysmWalletPasswordFileName);
|
||||
const walletPasswordFile = await open(walletPasswordPath, 'w');
|
||||
await walletPasswordFile.write(walletPassword);
|
||||
await walletPasswordFile.close();
|
||||
} else {
|
||||
// Update repository
|
||||
const returnProm = execFileProm('git', ['pull'], { cwd: ethDockerPath });
|
||||
const { stdout, stderr } = await returnProm;
|
||||
|
||||
if (returnProm.child.exitCode !== 0) {
|
||||
console.log('We failed to update eth-docker repository.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async postInstall(network: Network, outputLogs?: OutputLogs): Promise<boolean> {
|
||||
return this.startNodes(network, outputLogs);
|
||||
}
|
||||
|
||||
async stopNodes(network: Network, outputLogs?: OutputLogs): Promise<boolean> {
|
||||
const networkPath = path.join(installPath, network.toLowerCase());
|
||||
const ethDockerPath = path.join(networkPath, 'eth-docker');
|
||||
|
||||
const ethdCommand = path.join(ethDockerPath, 'ethd');
|
||||
const bashScript = `
|
||||
/usr/bin/newgrp ${dockerGroupName} <<EONG
|
||||
${ethdCommand} stop
|
||||
EONG
|
||||
`;
|
||||
|
||||
const returnProm = execProm(bashScript, { shell: '/bin/bash', cwd: ethDockerPath });
|
||||
const { stdout, stderr } = await returnProm;
|
||||
|
||||
if (returnProm.child.exitCode !== 0) {
|
||||
console.log('We failed to stop eth-docker clients.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async startNodes(network: Network, outputLogs?: OutputLogs): Promise<boolean> {
|
||||
const networkPath = path.join(installPath, network.toLowerCase());
|
||||
const ethDockerPath = path.join(networkPath, 'eth-docker');
|
||||
|
||||
const ethdCommand = path.join(ethDockerPath, 'ethd');
|
||||
const bashScript = `
|
||||
/usr/bin/newgrp ${dockerGroupName} <<EONG
|
||||
${ethdCommand} start
|
||||
EONG
|
||||
`;
|
||||
|
||||
const returnProm = execProm(bashScript, { shell: '/bin/bash', cwd: ethDockerPath });
|
||||
const { stdout, stderr } = await returnProm;
|
||||
|
||||
if (returnProm.child.exitCode !== 0) {
|
||||
console.log('We failed to start eth-docker clients.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async updateExecutionClient(outputLogs?: OutputLogs): Promise<void> {
|
||||
// TODO: implement
|
||||
console.log("Executing updateExecutionClient");
|
||||
return;
|
||||
}
|
||||
|
||||
async updateConsensusClient(outputLogs?: OutputLogs): Promise<void> {
|
||||
// TODO: implement
|
||||
console.log("Executing updateConsensusClient");
|
||||
return;
|
||||
}
|
||||
|
||||
async importKeys(
|
||||
network: Network,
|
||||
keyStoreDirectoryPath: string,
|
||||
keyStorePassword: string,
|
||||
outputLogs?: OutputLogs): Promise<boolean> {
|
||||
|
||||
const networkPath = path.join(installPath, network.toLowerCase());
|
||||
const ethDockerPath = path.join(networkPath, 'eth-docker');
|
||||
|
||||
const ethdCommand = path.join(ethDockerPath, 'ethd');
|
||||
const argKeyStoreDirectoryPath = commandJoin([keyStoreDirectoryPath]);
|
||||
const argKeyStorePassword = commandJoin([keyStorePassword]);
|
||||
|
||||
const walletPasswordPath = path.join(ethDockerPath, prysmWalletPasswordFileName);
|
||||
const walletPasswordFile = await open(walletPasswordPath, 'r');
|
||||
const walletPassword = commandJoin([await walletPasswordFile.readFile({ encoding: 'utf8' })]);
|
||||
await walletPasswordFile.close();
|
||||
|
||||
const bashScript = `
|
||||
/usr/bin/newgrp ${dockerGroupName} <<EONG
|
||||
WALLET_PASSWORD=${walletPassword} KEYSTORE_PASSWORD=${argKeyStorePassword} ${ethdCommand} keyimport --non-interactive --path ${argKeyStoreDirectoryPath}
|
||||
EONG
|
||||
`;
|
||||
|
||||
const returnProm = execProm(bashScript, { shell: '/bin/bash', cwd: ethDockerPath });
|
||||
const { stdout, stderr } = await returnProm;
|
||||
|
||||
if (returnProm.child.exitCode !== 0) {
|
||||
console.log('We failed to import keys with eth-docker.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async exportKeys(): Promise<void> {
|
||||
// TODO: implement
|
||||
return;
|
||||
}
|
||||
|
||||
async switchExecutionClient(targetClient: ExecutionClient): Promise<boolean> {
|
||||
// TODO: implement
|
||||
console.log("Executing switchExecutionClient");
|
||||
return false;
|
||||
}
|
||||
|
||||
async switchConsensusClient(targetClient: ConsensusClient): Promise<boolean> {
|
||||
// TODO: implement
|
||||
console.log("Executing switchConsensusClient");
|
||||
return false;
|
||||
}
|
||||
|
||||
async uninstall(): Promise<boolean> {
|
||||
// TODO: implement
|
||||
console.log("Executing uninstall");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Data
|
||||
async getCurrentExecutionClient(): Promise<ExecutionClient> {
|
||||
// TODO: implement
|
||||
console.log("Executing getCurrentExecutionClient");
|
||||
return ExecutionClient.GETH;
|
||||
}
|
||||
|
||||
async getCurrentConsensusClient(): Promise<ConsensusClient> {
|
||||
// TODO: implement
|
||||
console.log("Executing getCurrentConsensusClient");
|
||||
return ConsensusClient.LIGHTHOUSE;
|
||||
}
|
||||
|
||||
async getCurrentExecutionClientVersion(): Promise<string> {
|
||||
// TODO: implement
|
||||
console.log("Executing getCurrentExecutionClientVersion");
|
||||
return "0.1";
|
||||
}
|
||||
|
||||
async getCurrentConsensusClientVersion(): Promise<string> {
|
||||
// TODO: implement
|
||||
console.log("Executing getCurrentConsensusClientVersion");
|
||||
return "0.1";
|
||||
}
|
||||
|
||||
async getLatestExecutionClientVersion(client: ExecutionClient): Promise<string> {
|
||||
// TODO: implement
|
||||
console.log("Executing getLatestExecutionClientVersion");
|
||||
return "0.1";
|
||||
}
|
||||
|
||||
async getLatestConsensusClientVersion(client: ConsensusClient): Promise<string> {
|
||||
// TODO: implement
|
||||
console.log("Executing getLatestConsensusClientVersion");
|
||||
return "0.1";
|
||||
}
|
||||
|
||||
async executionClientStatus(): Promise<NodeStatus> {
|
||||
// TODO: implement
|
||||
console.log("Executing executionClientStatus");
|
||||
return NodeStatus.UNKNOWN;
|
||||
}
|
||||
|
||||
async consensusBeaconNodeStatus(): Promise<NodeStatus> {
|
||||
// TODO: implement
|
||||
console.log("Executing consensusBeaconNodeStatus");
|
||||
return NodeStatus.UNKNOWN;
|
||||
}
|
||||
|
||||
async consensusValidatorStatus(): Promise<ValidatorStatus> {
|
||||
// TODO: implement
|
||||
console.log("Executing consensusValidatorStatus");
|
||||
return ValidatorStatus.UNKNOWN;
|
||||
}
|
||||
|
||||
async consensusValidatorCount(): Promise<number> {
|
||||
// TODO: implement
|
||||
console.log("Executing consensusValidatorCount");
|
||||
return -1;
|
||||
}
|
||||
|
||||
async executionClientPeerCount(): Promise<number> {
|
||||
// TODO: implement
|
||||
console.log("Executing executionClientPeerCount");
|
||||
return -1;
|
||||
}
|
||||
|
||||
async consensusClientPeerCount(): Promise<number> {
|
||||
// TODO: implement
|
||||
console.log("Executing consensusClientPeerCount");
|
||||
return -1;
|
||||
}
|
||||
|
||||
async executionClientLatestBlock(): Promise<number> {
|
||||
// TODO: implement
|
||||
console.log("Executing executionClientLatestBlock");
|
||||
return -1;
|
||||
}
|
||||
|
||||
async consensusClientLatestBlock(): Promise<number> {
|
||||
// TODO: implement
|
||||
console.log("Executing consensusClientLatestBlock");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
92
src/electron/IMultiClientInstaller.ts
Normal file
92
src/electron/IMultiClientInstaller.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Network } from "../react/types";
|
||||
|
||||
export interface IMultiClientInstaller {
|
||||
|
||||
// Functionality
|
||||
preInstall: (outputLogs?: OutputLogs) => Promise<boolean>,
|
||||
install: (details: InstallDetails, outputLogs?: OutputLogs) => Promise<boolean>,
|
||||
postInstall: (network: Network, outputLogs?: OutputLogs) => Promise<boolean>,
|
||||
|
||||
stopNodes: (network: Network, outputLogs?: OutputLogs) => Promise<boolean>,
|
||||
startNodes: (network: Network, outputLogs?: OutputLogs) => Promise<boolean>,
|
||||
|
||||
updateExecutionClient: (outputLogs?: OutputLogs) => Promise<void>,
|
||||
updateConsensusClient: (outputLogs?: OutputLogs) => Promise<void>,
|
||||
|
||||
importKeys: (
|
||||
network: Network,
|
||||
keyStoreDirectoryPath: string,
|
||||
keyStorePassword: string,
|
||||
outputLogs?: OutputLogs) => Promise<boolean>,
|
||||
exportKeys: () => Promise<void>,
|
||||
|
||||
switchExecutionClient: (targetClient: ExecutionClient, outputLogs?: OutputLogs) => Promise<boolean>,
|
||||
switchConsensusClient: (targetClient: ConsensusClient, outputLogs?: OutputLogs) => Promise<boolean>,
|
||||
|
||||
uninstall: (outputLogs?: OutputLogs) => Promise<boolean>,
|
||||
|
||||
|
||||
// Data
|
||||
getCurrentExecutionClient: () => Promise<ExecutionClient>,
|
||||
getCurrentConsensusClient: () => Promise<ConsensusClient>,
|
||||
|
||||
getCurrentExecutionClientVersion: () => Promise<string>,
|
||||
getCurrentConsensusClientVersion: () => Promise<string>,
|
||||
|
||||
getLatestExecutionClientVersion: (client: ExecutionClient) => Promise<string>,
|
||||
getLatestConsensusClientVersion: (client: ConsensusClient) => Promise<string>,
|
||||
|
||||
executionClientStatus: () => Promise<NodeStatus>,
|
||||
consensusBeaconNodeStatus: () => Promise<NodeStatus>,
|
||||
consensusValidatorStatus: () => Promise<ValidatorStatus>,
|
||||
|
||||
consensusValidatorCount: () => Promise<number>,
|
||||
|
||||
executionClientPeerCount: () => Promise<number>,
|
||||
consensusClientPeerCount: () => Promise<number>,
|
||||
|
||||
executionClientLatestBlock: () => Promise<number>,
|
||||
consensusClientLatestBlock: () => Promise<number>,
|
||||
|
||||
// TODO: logs stream
|
||||
}
|
||||
|
||||
export interface OutputLogs {
|
||||
(message: string): void;
|
||||
}
|
||||
|
||||
export type InstallDetails = {
|
||||
network: Network,
|
||||
executionClient: ExecutionClient,
|
||||
consensusClient: ConsensusClient,
|
||||
}
|
||||
|
||||
export enum NodeStatus {
|
||||
UP_AND_SYNCED,
|
||||
UP_AND_SYNCING,
|
||||
DOWN,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
export enum ValidatorStatus {
|
||||
UP_AND_VALIDATING,
|
||||
UP_NOT_VALIDATING,
|
||||
DOWN,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
// Supported clients
|
||||
export enum ExecutionClient {
|
||||
GETH = "geth",
|
||||
NETHERMIND = "nethermind",
|
||||
BESU = "besu",
|
||||
ERIGON = "erigon"
|
||||
}
|
||||
|
||||
export enum ConsensusClient {
|
||||
TEKU = "teku",
|
||||
NIMBUS = "nimbus",
|
||||
LIGHTHOUSE = "lighthouse",
|
||||
PRYSM = "prysm",
|
||||
LODESTAR = "lodestar"
|
||||
}
|
||||
@@ -1,30 +1,101 @@
|
||||
import { BrowserWindow, app, globalShortcut } from "electron";
|
||||
// index.ts
|
||||
/**
|
||||
* This typescript file contains the Electron app which renders the React app.
|
||||
*/
|
||||
|
||||
import { BrowserWindow, app, globalShortcut, ipcMain, dialog, clipboard } from "electron";
|
||||
import path from "path";
|
||||
|
||||
import { accessSync, constants } from "fs";
|
||||
import { OpenDialogOptions } from "electron/common";
|
||||
|
||||
/**
|
||||
* VERSION and COMMITHASH are set by the git-revision-webpack-plugin module.
|
||||
*/
|
||||
declare var VERSION: string;
|
||||
declare var COMMITHASH: string;
|
||||
|
||||
const doesFileExist = (filename: string): boolean => {
|
||||
try {
|
||||
accessSync(filename, constants.F_OK);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
app.on("ready", () => {
|
||||
// once electron has started up, create a window.
|
||||
var iconPath = path.join("static", "icon.png");
|
||||
const bundledIconPath = path.join(process.resourcesPath, "..", "static", "icon.png");
|
||||
|
||||
if (doesFileExist(bundledIconPath)) {
|
||||
iconPath = bundledIconPath;
|
||||
}
|
||||
|
||||
const title = `${app.getName()} ${VERSION}-${COMMITHASH}`;
|
||||
|
||||
/**
|
||||
* Create the window in which to render the React app
|
||||
*/
|
||||
const window = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 720,
|
||||
icon: 'src/images/ethstaker_icon_1.png',
|
||||
width: 950,
|
||||
height: 750,
|
||||
icon: iconPath,
|
||||
title: title,
|
||||
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
|
||||
// TODO: is it a problem to disable this?
|
||||
// https://www.electronjs.org/docs/tutorial/context-isolation#security-considerations
|
||||
contextIsolation: false
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
// hide the default menu bar that comes with the browser window
|
||||
/**
|
||||
* Hide the default menu bar that comes with the browser window
|
||||
*/
|
||||
window.setMenuBarVisibility(false);
|
||||
|
||||
/**
|
||||
* Set the Permission Request Handler to deny all permissions requests
|
||||
*/
|
||||
window.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
|
||||
return callback(false);
|
||||
});
|
||||
|
||||
globalShortcut.register('CommandOrControl+R', function() {
|
||||
console.log('CommandOrControl+R is pressed')
|
||||
window.reload()
|
||||
})
|
||||
/**
|
||||
* Allow for refreshing of the React app within Electron without reopening.
|
||||
* This feature is used for development and will be disabled before production deployment.
|
||||
*/
|
||||
globalShortcut.register('CommandOrControl+R', function () {
|
||||
console.log('CommandOrControl+R was pressed, refreshing the React app within Electron.')
|
||||
window.reload()
|
||||
})
|
||||
|
||||
// load a website to display
|
||||
/**
|
||||
* This logic closes the application when the window is closed, explicitly.
|
||||
* On MacOS this is not a default feature.
|
||||
*/
|
||||
ipcMain.on('close', (evt, arg) => {
|
||||
app.quit();
|
||||
})
|
||||
|
||||
/**
|
||||
* Provides the renderer a way to call the dialog.showOpenDialog function using IPC.
|
||||
*/
|
||||
ipcMain.handle('showOpenDialog', async (event, options) => {
|
||||
return await dialog.showOpenDialog(<OpenDialogOptions>options);
|
||||
});
|
||||
|
||||
/**
|
||||
* Load the react app
|
||||
*/
|
||||
window.loadURL(`file://${__dirname}/../react/index.html`);
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
/**
|
||||
* Clear clipboard on quit to avoid access to any mnemonic or password that was copied during
|
||||
* application use.
|
||||
*/
|
||||
clipboard.clear();
|
||||
})
|
||||
|
||||
74
src/electron/preload.ts
Normal file
74
src/electron/preload.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// preload.ts
|
||||
/**
|
||||
* This typescript file contains the API used by the UI to call the electron modules.
|
||||
*/
|
||||
|
||||
import {
|
||||
contextBridge,
|
||||
shell,
|
||||
clipboard,
|
||||
ipcRenderer,
|
||||
OpenDialogOptions,
|
||||
OpenDialogReturnValue
|
||||
} from "electron";
|
||||
import { Network } from "../react/types";
|
||||
|
||||
import { doesDirectoryExist, findFirstFile } from './BashUtils';
|
||||
|
||||
import { EthDockerInstaller } from './EthDockerInstaller';
|
||||
import { InstallDetails, OutputLogs } from "./IMultiClientInstaller";
|
||||
|
||||
import { Writable } from 'stream';
|
||||
|
||||
const ethDockerInstaller = new EthDockerInstaller();
|
||||
const ethDockerPreInstall = async (outputLogs?: OutputLogs): Promise<boolean> => {
|
||||
return ethDockerInstaller.preInstall(outputLogs);
|
||||
};
|
||||
const ethDockerInstall = async (details: InstallDetails): Promise<boolean> => {
|
||||
return ethDockerInstaller.install(details);
|
||||
};
|
||||
const ethDockerImportKeys = async (
|
||||
network: Network,
|
||||
keyStoreDirectoryPath: string,
|
||||
keyStorePassword: string): Promise<boolean> => {
|
||||
return ethDockerInstaller.importKeys(network, keyStoreDirectoryPath, keyStorePassword);
|
||||
};
|
||||
const ethDockerPostInstall = async (network: Network): Promise<boolean> => {
|
||||
return ethDockerInstaller.postInstall(network);
|
||||
};
|
||||
const ethDockerStartNodes = async (network: Network): Promise<boolean> => {
|
||||
return ethDockerInstaller.startNodes(network);
|
||||
};
|
||||
const ethDockerStopNodes = async (network: Network): Promise<boolean> => {
|
||||
return ethDockerInstaller.stopNodes(network);
|
||||
};
|
||||
|
||||
const ipcRendererSendClose = () => {
|
||||
ipcRenderer.send('close');
|
||||
};
|
||||
|
||||
const invokeShowOpenDialog = (options: OpenDialogOptions): Promise<OpenDialogReturnValue> => {
|
||||
return ipcRenderer.invoke('showOpenDialog', options);
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
'shellOpenExternal': shell.openExternal,
|
||||
'shellShowItemInFolder': shell.showItemInFolder,
|
||||
'clipboardWriteText': clipboard.writeText,
|
||||
'ipcRendererSendClose': ipcRendererSendClose,
|
||||
'invokeShowOpenDialog': invokeShowOpenDialog
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('bashUtils', {
|
||||
'doesDirectoryExist': doesDirectoryExist,
|
||||
'findFirstFile': findFirstFile
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('ethDocker', {
|
||||
'preInstall': ethDockerPreInstall,
|
||||
'install': ethDockerInstall,
|
||||
'importKeys': ethDockerImportKeys,
|
||||
'postInstall': ethDockerPostInstall,
|
||||
'startNodes': ethDockerStartNodes,
|
||||
'stopNodes': ethDockerStopNodes
|
||||
});
|
||||
64
src/electron/renderer.d.ts
vendored
Normal file
64
src/electron/renderer.d.ts
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
// renderer.d.ts
|
||||
/**
|
||||
* This file contains the typescript type hinting for the preload.ts API.
|
||||
*/
|
||||
|
||||
import {
|
||||
OpenDialogOptions,
|
||||
OpenDialogReturnValue
|
||||
} from "electron";
|
||||
|
||||
import {
|
||||
FileOptions,
|
||||
FileResult
|
||||
} from "tmp";
|
||||
|
||||
import {
|
||||
PathLike,
|
||||
Stats,
|
||||
Dirent
|
||||
} from "fs"
|
||||
|
||||
import {
|
||||
ChildProcess
|
||||
} from "child_process"
|
||||
|
||||
import {
|
||||
EthDockerInstaller
|
||||
} from './EthDockerInstaller'
|
||||
import { InstallDetails, OutputLogs } from "./IMultiClientInstaller";
|
||||
|
||||
export interface IElectronAPI {
|
||||
shellOpenExternal: (url: string, options?: Electron.OpenExternalOptions | undefined) => Promise<void>,
|
||||
shellShowItemInFolder: (fullPath: string) => void,
|
||||
clipboardWriteText: (ext: string, type?: "selection" | "clipboard" | undefined) => void,
|
||||
ipcRendererSendClose: () => void,
|
||||
invokeShowOpenDialog: (options: OpenDialogOptions) => Promise<OpenDialogReturnValue>
|
||||
}
|
||||
|
||||
export interface IBashUtilsAPI {
|
||||
doesDirectoryExist: (directory: string) => Promise<boolean>,
|
||||
isDirectoryWritable: (directory: string) => Promise<boolean>,
|
||||
findFirstFile: (directory: string, startsWith: string) => Promise<string>
|
||||
}
|
||||
|
||||
|
||||
export interface IEthDockerAPI {
|
||||
preInstall: (outputLogs?: OutputLogs) => Promise<boolean>,
|
||||
install: (details: InstallDetails) => Promise<boolean>,
|
||||
importKeys: (
|
||||
network: Network,
|
||||
keyStoreDirectoryPath: string,
|
||||
keyStorePassword: string) => Promise<boolean>,
|
||||
postInstall: (network: Network) => Promise<boolean>,
|
||||
startNodes: (network: Network) => Promise<boolean>,
|
||||
stopNodes: (network: Network) => Promise<boolean>,
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: IElectronAPI,
|
||||
bashUtils: IBashUtilsAPI,
|
||||
ethDocker: IEthDockerAPI
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
@@ -1,37 +1,47 @@
|
||||
import { HashRouter, Route, Switch } from "react-router-dom";
|
||||
|
||||
import { Background } from "./colors";
|
||||
import Deposit from "./pages/Deposit";
|
||||
import { HashRouter, Route, Routes } from "react-router-dom";
|
||||
import React, { FC, ReactElement, useState } from "react";
|
||||
import styled from '@emotion/styled';
|
||||
import Home from "./pages/Home";
|
||||
import InstallFailed from "./pages/InstallFailed";
|
||||
import Installing from "./pages/Installing";
|
||||
import React from "react";
|
||||
import Status from "./pages/Status";
|
||||
import SystemCheck from "./pages/SystemCheck";
|
||||
import styled from "styled-components";
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import { ThemeProvider } from "@mui/material";
|
||||
import '@fontsource/roboto';
|
||||
import MainWizard from "./pages/MainWizard";
|
||||
import theme from "./theme";
|
||||
import { Network } from './types';
|
||||
import SystemOverview from "./pages/SystemOverview";
|
||||
import { ConsensusClient, ExecutionClient, InstallDetails } from "../electron/IMultiClientInstaller";
|
||||
|
||||
const Container = styled.main`
|
||||
font-family: 'PT Mono', monospace;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: ${Background};
|
||||
display: block;
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
const App = () => {
|
||||
/**
|
||||
* The React app top level including theme and routing.
|
||||
*
|
||||
* @returns the react element containing the app
|
||||
*/
|
||||
const App: FC = (): ReactElement => {
|
||||
// const [network, setNetwork] = useState<Network>(Network.PRATER);
|
||||
const [installationDetails, setInstallationDetails] = useState<InstallDetails>({
|
||||
consensusClient: ConsensusClient.PRYSM,
|
||||
executionClient: ExecutionClient.GETH,
|
||||
network: Network.PRATER
|
||||
})
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<Container>
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route exact path="/systemcheck" component={SystemCheck} />
|
||||
<Route exact path="/installing" component={Installing} />
|
||||
<Route exact path="/installfailed" component={InstallFailed} />
|
||||
<Route exact path="/status" component={Status} />
|
||||
<Route exact path="/deposit" component={Deposit} />
|
||||
</Switch>
|
||||
</Container>
|
||||
</HashRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<HashRouter>
|
||||
<Container>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home installationDetails={installationDetails} setInstallationDetails={setInstallationDetails} />} />
|
||||
<Route path="/wizard/:stepSequenceKey" element={<MainWizard installationDetails={installationDetails} setInstallationDetails={setInstallationDetails} />} />
|
||||
<Route path="/systemOverview" element={<SystemOverview installationDetails={installationDetails} />} />
|
||||
</Routes>
|
||||
</Container>
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@ export const Red = "#fa1e0e";
|
||||
|
||||
export const LightGreen = "#52b788";
|
||||
|
||||
export const Background = "#1b262c";
|
||||
export const Button = LightBlue;
|
||||
export const Background = "#1B262C";
|
||||
export const BackgroundLight = "#354D5A";
|
||||
export const ButtonColor = LightBlue;
|
||||
export const ButtonHover = LightGreen;
|
||||
export const Heading = MediumBlue;
|
||||
export const MainContent = Gray3;
|
||||
@@ -27,4 +28,5 @@ export const MainContentAlert = Red;
|
||||
export const Black = "#000000"
|
||||
export const DisabledButton = DarkGray;
|
||||
|
||||
|
||||
export const Yellow = "#FFA600";
|
||||
export const Orange = "#F2994A";
|
||||
@@ -1,24 +0,0 @@
|
||||
import { executeCommandSync, executeCommandSyncReturnStdout } from "./ExecuteCommand";
|
||||
|
||||
const doesFileExist = (filename: string): boolean => {
|
||||
const cmd = "test -f " + filename;
|
||||
const result = executeCommandSync(cmd);
|
||||
return result == 0;
|
||||
};
|
||||
|
||||
//TODO: add error handling
|
||||
const readlink = (file: string): string => {
|
||||
return executeCommandSyncReturnStdout("readlink -f " + file).trim();
|
||||
}
|
||||
|
||||
const which = (tool: string): boolean => {
|
||||
const cmd = "which " + tool;
|
||||
const result = executeCommandSync(cmd);
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
export {
|
||||
doesFileExist,
|
||||
readlink,
|
||||
which
|
||||
};
|
||||
@@ -1,146 +0,0 @@
|
||||
import { exec, execSync, spawn } from 'child_process';
|
||||
import { streamEnd, streamWrite } from '@rauschma/stringio';
|
||||
|
||||
import { Writable } from 'stream';
|
||||
|
||||
// TODO: better error handling and logging
|
||||
// TODO: remove console.log
|
||||
|
||||
// TODO: make this work for different operating systems
|
||||
const UBUNTU_TERMINAL_COMMAND = "/usr/bin/gnome-terminal";
|
||||
|
||||
type StdoutCallback = (text: string) => void;
|
||||
|
||||
const executeCommandAsync = async (cmd: string): Promise<any> => {
|
||||
console.log("running command async with: " + cmd);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = exec(cmd);
|
||||
|
||||
child.once('exit', function (code) {
|
||||
resolve(code);
|
||||
});
|
||||
|
||||
child.on('error', function (err) {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const executeCommandInNewTerminal = (cmd: string, title: string): number => {
|
||||
return executeCommandSync(UBUNTU_TERMINAL_COMMAND + " --title=\"" + title + "\" -- bash -c '" + cmd + "'");
|
||||
}
|
||||
|
||||
const executeCommandSync = (cmd: string): number => {
|
||||
console.log("running command sync with: " + cmd);
|
||||
|
||||
try {
|
||||
execSync(cmd, {stdio: 'inherit'});
|
||||
return 0;
|
||||
}
|
||||
catch (error) {
|
||||
// TODO: more robust error handling
|
||||
error.status;
|
||||
error.message;
|
||||
error.stderr;
|
||||
error.stdout;
|
||||
console.log(error.message);
|
||||
return error.status;
|
||||
}
|
||||
}
|
||||
|
||||
const executeCommandSyncReturnStdout = (cmd: string): string => {
|
||||
console.log("running command sync stdout with: " + cmd);
|
||||
|
||||
try {
|
||||
return execSync(cmd).toString();
|
||||
}
|
||||
catch (error) {
|
||||
// TODO: more robust error handling
|
||||
error.status;
|
||||
error.message;
|
||||
error.stderr;
|
||||
error.stdout;
|
||||
console.log(error.message);
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
const executeCommandStream = (cmd: string, stdoutCallback: StdoutCallback): Promise<any> => {
|
||||
console.log("running command stream with: " + cmd);
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(cmd, {
|
||||
shell: true
|
||||
});
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
stdoutCallback(data.toString());
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
stdoutCallback(data.toString());
|
||||
});
|
||||
|
||||
child.once('exit', function (code) {
|
||||
resolve(code);
|
||||
});
|
||||
|
||||
child.on('error', function (err) {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// good resource for this: https://2ality.com/2018/05/child-process-streams.html
|
||||
const executeCommandWithPromptsAsync = (cmd: string, responses: string[], stdoutCallback: StdoutCallback): Promise<any> => {
|
||||
console.log("running command with prompts async with: " + cmd + " and responses " + responses.join());
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(cmd, {
|
||||
stdio: ['pipe', 'pipe', process.stderr],
|
||||
shell: true,
|
||||
});
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
stdoutCallback(data.toString());
|
||||
});
|
||||
|
||||
writeToWritable(child.stdin, responses);
|
||||
|
||||
child.once('exit', function (code) {
|
||||
resolve(code);
|
||||
});
|
||||
|
||||
child.on('error', function (err) {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: using this sync wait to get the prompt responses to work properly is
|
||||
// probably not the best - come up with alternative solution
|
||||
const syncWait = (ms: number) => {
|
||||
const end = Date.now() + ms
|
||||
while (Date.now() < end) continue
|
||||
}
|
||||
|
||||
async function writeToWritable(writable: Writable, responses: string[]) {
|
||||
syncWait(1000);
|
||||
for (const response of responses) {
|
||||
console.log("writing '" + response + "'");
|
||||
await streamWrite(writable, response);
|
||||
syncWait(1000);
|
||||
}
|
||||
|
||||
await streamEnd(writable);
|
||||
}
|
||||
|
||||
export {
|
||||
executeCommandAsync,
|
||||
executeCommandInNewTerminal,
|
||||
executeCommandStream,
|
||||
executeCommandSync,
|
||||
executeCommandSyncReturnStdout,
|
||||
executeCommandWithPromptsAsync,
|
||||
};
|
||||
@@ -1,200 +0,0 @@
|
||||
import { doesFileExist, readlink } from "./BashUtils";
|
||||
import { executeCommandInNewTerminal, executeCommandStream, executeCommandSync, executeCommandSyncReturnStdout, executeCommandWithPromptsAsync } from "./ExecuteCommand";
|
||||
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
const ASKPASS_PATH = "src/scripts/askpass.sh";
|
||||
|
||||
const ROCKET_POOL_EXECUTABLE = "~/bin/rocketpool";
|
||||
const ROCKET_POOL_DIR = "~/.rocketpool"
|
||||
const ROCKET_POOL_INSTALL_COMMAND = "mkdir -p ~/bin && wget https://github.com/rocket-pool/smartnode-install/releases/latest/download/rocketpool-cli-linux-amd64 -O " + ROCKET_POOL_EXECUTABLE + " && chmod +x " + ROCKET_POOL_EXECUTABLE;
|
||||
|
||||
const GETH_SYNC_STATUS_DOCKER_CMD = "docker exec rocketpool_eth1 geth --exec 'eth.syncing' attach ipc:ethclient/geth/geth.ipc";
|
||||
const GETH_PEERS_DOCKER_CMD = "docker exec rocketpool_eth1 geth --exec 'admin.peers.length' attach ipc:ethclient/geth/geth.ipc";
|
||||
|
||||
// TODO: make an installer interface and implement it here, so we can easily extend
|
||||
// to utilize multiple different installers
|
||||
|
||||
// TODO: better error handling/logging
|
||||
|
||||
type Callback = (success: boolean) => void;
|
||||
type NodeStatusCallback = (status: number) => void;
|
||||
type StdoutCallback = (text: string[]) => void;
|
||||
|
||||
const wrapCommandInDockerGroup = (command: string) => {
|
||||
return "sg docker \"" + command + "\"";
|
||||
}
|
||||
|
||||
// TODO: make this better, it is pretty brittle and peeks into the RP settings implementation
|
||||
// this is required because we select the client at random, so we need to show the user what is running
|
||||
const getEth2ClientName = (): string => {
|
||||
try {
|
||||
const rpSettings: any = yaml.load(fs.readFileSync(readlink(ROCKET_POOL_DIR + '/settings.yml'), 'utf8'));
|
||||
const selectedClient = rpSettings["chains"]["eth2"]["client"]["selected"];
|
||||
|
||||
return selectedClient;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const installAndStartRocketPool = async (callback: Callback, stdoutCallback: StdoutCallback) => {
|
||||
// Used for reporting back log messages to caller
|
||||
// TODO: there has to be a better way to do this...
|
||||
const consoleMessages: string[] = [];
|
||||
const internalStdoutCallback = (text: string) => {
|
||||
consoleMessages.push(text);
|
||||
stdoutCallback(consoleMessages);
|
||||
}
|
||||
|
||||
// cache sudo credentials to be used for install later
|
||||
const passwordRc = await executeCommandStream("export SUDO_ASKPASS='" + ASKPASS_PATH + "' && sudo -A echo 'Authentication successful.'", internalStdoutCallback);
|
||||
if (passwordRc != 0) {
|
||||
console.log("password failed");
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cliRc = await executeCommandStream(ROCKET_POOL_INSTALL_COMMAND, internalStdoutCallback);
|
||||
if (cliRc != 0) {
|
||||
console.log("cli failed to install");
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceRc = await executeCommandStream(ROCKET_POOL_EXECUTABLE + " service install --yes --network pyrmont", internalStdoutCallback);
|
||||
if (serviceRc != 0) {
|
||||
console.log("service install failed");
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// For some reason executeCommandWithPromptsAsync needs the full path, so fetching it here
|
||||
const rocketPoolExecutableFullPath = readlink(ROCKET_POOL_EXECUTABLE);
|
||||
console.log("full path");
|
||||
console.log(rocketPoolExecutableFullPath);
|
||||
|
||||
const promptResponses = [
|
||||
"1\n", // which eth1 client? 1 geth, 2 infura, 3 custom
|
||||
"\n", // ethstats label
|
||||
"\n", // ethstats login
|
||||
"\n", // Cache size
|
||||
"\n", // Max peers
|
||||
"\n", // P2P port
|
||||
"y\n", // random eth2 client? y/n
|
||||
"\n", // graffiti
|
||||
"\n", // Max peers
|
||||
"\n", // P2P port
|
||||
]
|
||||
|
||||
const serviceConfigRc = await executeCommandWithPromptsAsync(rocketPoolExecutableFullPath + " service config", promptResponses, internalStdoutCallback);
|
||||
if (serviceConfigRc != 0) {
|
||||
console.log("service config failed");
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Just in case nodes were running - pick up new config (might happen anyway on start, not sure)
|
||||
const stopNodesRc = await executeCommandStream(wrapCommandInDockerGroup(ROCKET_POOL_EXECUTABLE + " service stop -y"), internalStdoutCallback);
|
||||
if (stopNodesRc != 0) {
|
||||
console.log("stop nodes failed");
|
||||
callback(false);
|
||||
}
|
||||
|
||||
const startNodesRc = await executeCommandStream(wrapCommandInDockerGroup(ROCKET_POOL_EXECUTABLE + " service start"), internalStdoutCallback);
|
||||
if (startNodesRc != 0) {
|
||||
console.log("start nodes failed");
|
||||
callback(false);
|
||||
}
|
||||
|
||||
await executeCommandStream("echo 'Install complete - redirecting...'", internalStdoutCallback);
|
||||
|
||||
callback(true);
|
||||
}
|
||||
|
||||
const isRocketPoolInstalled = (): boolean => {
|
||||
return doesFileExist(ROCKET_POOL_EXECUTABLE)
|
||||
}
|
||||
|
||||
const openEth1Logs = () => {
|
||||
const openEth1LogsRc = executeCommandInNewTerminal(wrapCommandInDockerGroup("docker container logs -f rocketpool_eth1"), "eth1 (geth) logs");
|
||||
if (openEth1LogsRc != 0) {
|
||||
console.log("failed to open eth1 logs");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const openEth2BeaconLogs = () => {
|
||||
const openEth2BeaconLogsRc = executeCommandInNewTerminal(wrapCommandInDockerGroup("docker container logs -f rocketpool_eth2"), "eth2 beacon node (" + getEth2ClientName() + ") logs");
|
||||
if (openEth2BeaconLogsRc != 0) {
|
||||
console.log("failed to open eth2 beacon logs");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const openEth2ValidatorLogs = () => {
|
||||
const openEth2ValidatorLogsRc = executeCommandInNewTerminal(wrapCommandInDockerGroup("docker container logs -f rocketpool_validator"), "eth2 validator (" + getEth2ClientName() + ") logs");
|
||||
if (openEth2ValidatorLogsRc != 0) {
|
||||
console.log("failed to open eth2 validator logs");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const startNodes = (): number => {
|
||||
return executeCommandSync(wrapCommandInDockerGroup(ROCKET_POOL_EXECUTABLE + " service start"));
|
||||
}
|
||||
|
||||
const stopNodes = (): number => {
|
||||
return executeCommandSync(wrapCommandInDockerGroup(ROCKET_POOL_EXECUTABLE + " service stop -y"));
|
||||
}
|
||||
|
||||
const queryEth1PeerCount = (): number => {
|
||||
const numPeers = executeCommandSyncReturnStdout(wrapCommandInDockerGroup(GETH_PEERS_DOCKER_CMD));
|
||||
const numPeersNumber = parseInt(numPeers.trim());
|
||||
return isNaN(numPeersNumber) ? 0 : numPeersNumber;
|
||||
}
|
||||
|
||||
const queryEth1Status = (nodeStatusCallback: NodeStatusCallback) => {
|
||||
dockerContainerStatus("rocketpool_eth1", nodeStatusCallback);
|
||||
}
|
||||
|
||||
const queryEth1Syncing = (): boolean => {
|
||||
const syncValue = executeCommandSyncReturnStdout(wrapCommandInDockerGroup(GETH_SYNC_STATUS_DOCKER_CMD));
|
||||
return !syncValue.includes("false");
|
||||
}
|
||||
|
||||
const queryEth2BeaconStatus = (nodeStatusCallback: NodeStatusCallback) => {
|
||||
dockerContainerStatus("rocketpool_eth2", nodeStatusCallback);
|
||||
}
|
||||
|
||||
const queryEth2ValidatorStatus = (nodeStatusCallback: NodeStatusCallback) => {
|
||||
dockerContainerStatus("rocketpool_validator", nodeStatusCallback);
|
||||
}
|
||||
|
||||
const dockerContainerStatus = async (containerName: string, nodeStatusCallback: NodeStatusCallback) => {
|
||||
const containerId = executeCommandSyncReturnStdout(wrapCommandInDockerGroup("docker ps -q -f name=" + containerName));
|
||||
|
||||
if (containerId.trim()) {
|
||||
nodeStatusCallback(0); // online
|
||||
} else {
|
||||
nodeStatusCallback(2); // offline
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getEth2ClientName,
|
||||
installAndStartRocketPool,
|
||||
isRocketPoolInstalled,
|
||||
openEth1Logs,
|
||||
openEth2BeaconLogs,
|
||||
openEth2ValidatorLogs,
|
||||
startNodes,
|
||||
stopNodes,
|
||||
queryEth1PeerCount,
|
||||
queryEth1Status,
|
||||
queryEth1Syncing,
|
||||
queryEth2BeaconStatus,
|
||||
queryEth2ValidatorStatus,
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { Black, Button, ButtonHover } from "../colors";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type FooterProps = {
|
||||
backLink: string;
|
||||
backLabel: string;
|
||||
nextLink: string;
|
||||
nextLabel: string;
|
||||
}
|
||||
|
||||
const FooterContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-self: flex-end;
|
||||
height: 70;
|
||||
flex-grow:1;
|
||||
min-width:100vw;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Link)`
|
||||
color: ${Black};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-self:flex-end;
|
||||
height: 24;
|
||||
background-color: ${Button};
|
||||
padding: 16 24;
|
||||
border-radius: 10%;
|
||||
text-decoration: none;
|
||||
|
||||
transition: 250ms background-color ease;
|
||||
cursor: pointer;
|
||||
margin: 60;
|
||||
|
||||
&:hover {
|
||||
background-color: ${ButtonHover};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const Footer = (props: FooterProps) => {
|
||||
return(
|
||||
<FooterContainer>
|
||||
{ props.backLink ? <StyledButton to={props.backLink}>{props.backLabel}</StyledButton> : <div></div> }
|
||||
{ props.nextLink ? <StyledButton to={props.nextLink}>{props.nextLabel}</StyledButton> : <div></div> }
|
||||
</FooterContainer>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
150
src/react/components/ImportKeystore.tsx
Normal file
150
src/react/components/ImportKeystore.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { BackgroundLight, } from '../colors';
|
||||
import { Button, Typography, Box, Grid, Modal, InputAdornment, TextField, styled } from '@mui/material';
|
||||
import React, { FC, ChangeEvent, Dispatch, ReactElement, SetStateAction, MutableRefObject } from 'react';
|
||||
|
||||
import { FileCopy, LockOpen } from '@mui/icons-material'
|
||||
import { InstallDetails } from '../../electron/IMultiClientInstaller';
|
||||
|
||||
|
||||
const ModalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 600,
|
||||
padding: '20px',
|
||||
borderRadius: '20px',
|
||||
background: BackgroundLight,
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
const FileUploadField = styled(TextField)({
|
||||
'& label.Mui-focused': {
|
||||
},
|
||||
'& .MuiInput-underline:after': {
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
paddingLeft: '0',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type ImportKeystoreProps = {
|
||||
setModalOpen: Dispatch<SetStateAction<boolean>>
|
||||
isModalOpen : boolean
|
||||
keyStorePath: string
|
||||
keystorePassword: string
|
||||
setKeystorePath: Dispatch<SetStateAction<string>>
|
||||
setKeystorePassword: Dispatch<SetStateAction<string>>
|
||||
closing: MutableRefObject<((arg: () => Promise<boolean>) => void) | undefined>
|
||||
installationDetails: InstallDetails
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the network picker modal component where the user selects the desired network.
|
||||
*
|
||||
* @param props.isModalOpen the current open state of the modal
|
||||
* @param props.setModalOpen a function to set the modal open state
|
||||
* @param props.keyStorePath the path to the directory where the validator keys are stored
|
||||
* @param props.setKeystorePath set the path to the directory where the validator keys are stored
|
||||
* @param props.keystorePassword the password to unlock the validator key
|
||||
* @param props.setKeystorePassword sets the password used to unlock the validator keys
|
||||
* @param props.closing sets the function that will be called after the modal is closed
|
||||
* @param props.installationDetails the current installation details
|
||||
* @returns the import validator key modal
|
||||
*/
|
||||
export const ImportKeystore: FC<ImportKeystoreProps> = (props): ReactElement => {
|
||||
|
||||
const handleKeystorePathChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
props.setKeystorePath(ev.target.value)
|
||||
}
|
||||
const handlePasswordChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
props.setKeystorePassword(ev.target.value)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.isModalOpen}
|
||||
onClose={() => props.setModalOpen(false)}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description"
|
||||
>
|
||||
<Box sx={ModalStyle}>
|
||||
<Typography id="modal-modal-title" align='center' variant="h4" component="h2">
|
||||
Import Validator Keys
|
||||
</Typography>
|
||||
<hr style={{ borderColor: 'orange' }} />
|
||||
<Grid container>
|
||||
<Grid xs={12} item container justifyContent={'flex-start'} direction={'column'}>
|
||||
<Grid item container alignItems={'center'} p={2} spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<span>Keystore Directory</span>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FileUploadField
|
||||
placeholder='/validator_keys/'
|
||||
sx={{ my: 2, minWidth: '215', cursor: 'pointer !important' }}
|
||||
variant="outlined"
|
||||
onChange={handleKeystorePathChange}
|
||||
value={props.keyStorePath}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment sx={{paddingLeft: '14px'}} onClick={(ev) => {
|
||||
ev.preventDefault()
|
||||
window.electronAPI.invokeShowOpenDialog({
|
||||
|
||||
properties: ['openDirectory']
|
||||
}).then(DialogResponse => {
|
||||
if (DialogResponse.filePaths && DialogResponse.filePaths.length) {
|
||||
props.setKeystorePath(DialogResponse.filePaths[0])
|
||||
}
|
||||
})
|
||||
}} position="start"><FileCopy /></InputAdornment>,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item container alignItems={'center'} p={2} spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<span>Keystore Password</span>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
type={'password'}
|
||||
sx={{ my: 2, minWidth: '215' }}
|
||||
// label="Fallback URL"
|
||||
variant="outlined"
|
||||
value={props.keystorePassword}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start"><LockOpen /></InputAdornment>,
|
||||
}}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} textAlign='center' my={2}>
|
||||
<Button
|
||||
variant="contained" color="primary" onClick={() => {
|
||||
props.setModalOpen(false)
|
||||
if (props.closing && props.closing.current) {
|
||||
props.closing.current(() => window.ethDocker.importKeys(props.installationDetails.network, props.keyStorePath, props.keystorePassword))
|
||||
}
|
||||
}
|
||||
}>Import</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
49
src/react/components/InstallFlow/0-SystemCheck.tsx
Normal file
49
src/react/components/InstallFlow/0-SystemCheck.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { FC, ReactElement } from 'react';
|
||||
import { Grid, Typography } from '@mui/material';
|
||||
import StepNavigation from '../StepNavigation';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type SystemCheckProps = {
|
||||
onStepBack: () => void,
|
||||
onStepForward: () => void,
|
||||
}
|
||||
|
||||
const ContentGrid = styled(Grid)`
|
||||
height: 320px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
/**
|
||||
* This page is the first step of the install process where system checks are run.
|
||||
*
|
||||
* @param props the data and functions passed in, they are self documenting
|
||||
* @returns
|
||||
*/
|
||||
const SystemCheck: FC<SystemCheckProps> = (props): ReactElement => {
|
||||
|
||||
return (
|
||||
<Grid item container direction="column" spacing={2}>
|
||||
<Grid item>
|
||||
<Typography variant="h1" align='center'>
|
||||
System Check
|
||||
</Typography>
|
||||
</Grid>
|
||||
<ContentGrid>
|
||||
|
||||
</ContentGrid>
|
||||
{/* props.children is the stepper */}
|
||||
{props.children}
|
||||
<StepNavigation
|
||||
onPrev={props.onStepBack}
|
||||
onNext={props.onStepForward}
|
||||
backLabel={"Back"}
|
||||
nextLabel={"Next"}
|
||||
disableBack={false}
|
||||
disableNext={false}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default SystemCheck;
|
||||
224
src/react/components/InstallFlow/1-Configuration.tsx
Normal file
224
src/react/components/InstallFlow/1-Configuration.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import React, { ChangeEvent, Dispatch, FC, ReactElement, SetStateAction, useState } from 'react';
|
||||
import { Grid, Typography, FormControl, Select, MenuItem, SelectChangeEvent, Modal, Box, Button, TextField, InputAdornment } from '@mui/material';
|
||||
import { Folder, Link } from '@mui/icons-material'
|
||||
import StepNavigation from '../StepNavigation';
|
||||
import styled from '@emotion/styled';
|
||||
import { ConsensusClients, ExecutionClients, IConsensusClient, IExecutionClient } from '../../constants'
|
||||
import { ConsensusClient, ExecutionClient, InstallDetails } from '../../../electron/IMultiClientInstaller';
|
||||
import { BackgroundLight, } from '../../colors';
|
||||
|
||||
type ConfigurationProps = {
|
||||
onStepBack: () => void,
|
||||
onStepForward: () => void,
|
||||
installationDetails: InstallDetails,
|
||||
setInstallationDetails: Dispatch<SetStateAction<InstallDetails>>
|
||||
}
|
||||
|
||||
const ContentGrid = styled(Grid)`
|
||||
height: 320px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
|
||||
const ModalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 600,
|
||||
padding: '20px',
|
||||
borderRadius: '20px',
|
||||
background: BackgroundLight,
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
/**
|
||||
* This page is the second step of the install process where the user inputs their configuration.
|
||||
*
|
||||
* @param props the data and functions passed in, they are self documenting
|
||||
* @returns
|
||||
*/
|
||||
const Configuration: FC<ConfigurationProps> = (props): ReactElement => {
|
||||
const [consensusClient, setConsensusClient] = useState<ConsensusClient>(props.installationDetails.consensusClient);
|
||||
const [executionClient, setExecutionClient] = useState<ExecutionClient>(props.installationDetails.executionClient);
|
||||
const [isModalOpen, setModalOpen] = useState(false)
|
||||
const [checkpointSync, setCheckpointSync] = useState('');
|
||||
const [executionClientFallback, setExecutionClientFallback] = useState('');
|
||||
const [installationPath, setInstallationPath] = useState('');
|
||||
|
||||
const handleConsensusClientChange = (ev: SelectChangeEvent<string>) => {
|
||||
setConsensusClient(ev.target.value as ConsensusClient)
|
||||
}
|
||||
|
||||
const handleExecutionClientChange = (ev: SelectChangeEvent<string>) => {
|
||||
setExecutionClient(ev.target.value as ExecutionClient)
|
||||
}
|
||||
|
||||
const handleCheckpointSyncChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
setCheckpointSync(ev.target.value)
|
||||
}
|
||||
const handleExecutionClientFallbackChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
setExecutionClientFallback(ev.target.value)
|
||||
}
|
||||
const handleInstallationPathChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
setInstallationPath(ev.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid item container direction="column" spacing={2}>
|
||||
<Grid item>
|
||||
<Typography variant="h1" align='center'>
|
||||
Configuration
|
||||
</Typography>
|
||||
</Grid>
|
||||
<ContentGrid item container justifyContent={'center'}>
|
||||
<Grid xs={11} style={{ border: '1px solid orange' }} item container justifyContent={'center'} direction={'column'}>
|
||||
<Grid item container alignItems={'center'} p={2} spacing={2}>
|
||||
<Grid item xs={1}></Grid>
|
||||
<Grid item xs={4}>
|
||||
<span>Consensus Client</span>
|
||||
</Grid>
|
||||
<Grid item xs={2}></Grid>
|
||||
<Grid item xs={4}>
|
||||
<FormControl sx={{ my: 2, minWidth: '215' }}>
|
||||
<Select
|
||||
id="consensus-client"
|
||||
value={consensusClient}
|
||||
onChange={handleConsensusClientChange}
|
||||
>
|
||||
{ConsensusClients.map((c: IConsensusClient) => {
|
||||
return (
|
||||
<MenuItem key={c.key} value={c.key}>{c.label}</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={1}></Grid>
|
||||
</Grid>
|
||||
<Grid item container alignItems={'center'} p={2} spacing={2}>
|
||||
<Grid item xs={1}></Grid>
|
||||
<Grid item xs={4}>
|
||||
<span>Execution Client</span>
|
||||
</Grid>
|
||||
<Grid item xs={2}></Grid>
|
||||
<Grid item xs={4}>
|
||||
<FormControl sx={{ my: 2, minWidth: '215' }}>
|
||||
<Select
|
||||
id="execution-client"
|
||||
value={executionClient}
|
||||
onChange={handleExecutionClientChange}
|
||||
>
|
||||
{ExecutionClients.map((c: IExecutionClient) => {
|
||||
return (
|
||||
<MenuItem key={c.key} value={c.key}>{c.label}</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={1}></Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Button disabled onClick={() => setModalOpen(true)}>Advanced Options</Button>
|
||||
<Modal
|
||||
open={isModalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description"
|
||||
>
|
||||
<Box sx={ModalStyle}>
|
||||
<Typography id="modal-modal-title" align='center' variant="h4" component="h2">
|
||||
Advanced options
|
||||
</Typography>
|
||||
<hr style={{ borderColor: 'orange' }} />
|
||||
<Grid container>
|
||||
<Grid xs={12} item container justifyContent={'flex-start'} direction={'column'}>
|
||||
<Grid item container alignItems={'center'} p={2} spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<span>Checkpoint Sync</span>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
placeholder="https://beaconcha.in/checkpoint"
|
||||
type={'url'}
|
||||
sx={{ my: 2, minWidth: '215' }}
|
||||
variant="outlined"
|
||||
value={checkpointSync}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start"><Link /></InputAdornment>,
|
||||
}}
|
||||
onChange={handleCheckpointSyncChange}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item container alignItems={'center'} p={2} spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<span>Execution Client Fallback</span>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
placeholder="http://localhost:8545"
|
||||
type={'url'}
|
||||
sx={{ my: 2, minWidth: '215' }}
|
||||
// label="Fallback URL"
|
||||
variant="outlined"
|
||||
value={executionClientFallback}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start"><Link /></InputAdornment>,
|
||||
}}
|
||||
onChange={handleExecutionClientFallbackChange}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item container alignItems={'center'} p={2} spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<span>Installation Path</span>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
placeholder='.wagyu/'
|
||||
onClick={(ev) => { ev.preventDefault(); }}
|
||||
sx={{ my: 2, minWidth: '215' }}
|
||||
variant="outlined"
|
||||
disabled
|
||||
value={installationPath}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start"><Folder /></InputAdornment>,
|
||||
}}
|
||||
onChange={handleInstallationPathChange}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Modal>
|
||||
</ContentGrid>
|
||||
{props.children}
|
||||
<StepNavigation
|
||||
onPrev={props.onStepBack}
|
||||
onNext={() => {
|
||||
|
||||
let installationDetails: InstallDetails = {
|
||||
consensusClient,
|
||||
executionClient,
|
||||
network: props.installationDetails.network
|
||||
}
|
||||
|
||||
props.setInstallationDetails(installationDetails)
|
||||
|
||||
props.onStepForward()
|
||||
}}
|
||||
backLabel={"Back"}
|
||||
nextLabel={"Install"}
|
||||
disableBack={false}
|
||||
disableNext={false}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default Configuration;
|
||||
426
src/react/components/InstallFlow/2-Install.tsx
Normal file
426
src/react/components/InstallFlow/2-Install.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
import React, { Dispatch, FC, ReactElement, SetStateAction, useState, useRef, useEffect } from 'react';
|
||||
import { Grid, Typography, Fab, CircularProgress, Box } from '@mui/material';
|
||||
import StepNavigation from '../StepNavigation';
|
||||
import { DoneOutline, DownloadingOutlined, ComputerOutlined, RocketLaunchOutlined, KeyOutlined, ErrorOutline } from '@mui/icons-material';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { green, red } from '@mui/material/colors';
|
||||
import { InstallDetails } from '../../../electron/IMultiClientInstaller';
|
||||
import { ImportKeystore } from '../ImportKeystore'
|
||||
|
||||
type InstallProps = {
|
||||
onStepBack: () => void,
|
||||
onStepForward: () => void,
|
||||
installationDetails: InstallDetails,
|
||||
setInstallationDetails: Dispatch<SetStateAction<InstallDetails>>
|
||||
}
|
||||
|
||||
const ContentGrid = styled(Grid)`
|
||||
height: 320px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
|
||||
/**
|
||||
* This page is the third step of the install process where the software is being installed.
|
||||
*
|
||||
* @param props the data and functions passed in, they are self documenting
|
||||
* @returns
|
||||
*/
|
||||
const Install: FC<InstallProps> = (props): ReactElement => {
|
||||
|
||||
const [loadingPreInstall, setLoadingPreInstall] = useState(false);
|
||||
const [loadingInstall, setLoadingInstall] = useState(false);
|
||||
const [loadingKeyImport, setLoadingKeyImport] = useState(false);
|
||||
const [loadingPostInstall, setLoadingPostInstall] = useState(false);
|
||||
|
||||
const [failedPreInstall, setFailedPreInstall] = useState(false);
|
||||
const [failedInstall, setFailedInstall] = useState(false);
|
||||
const [failedKeyImport, setFailedKeyImport] = useState(false);
|
||||
const [failedPostInstall, setFailedPostInstall] = useState(false);
|
||||
|
||||
const [successPreInstall, setSuccessPreInstall] = useState(false);
|
||||
const [successInstall, setSuccessInstall] = useState(false);
|
||||
const [successKeyImport, setSuccessKeyImport] = useState(false);
|
||||
const [successPostInstall, setSuccessPostInstall] = useState(false);
|
||||
const resolveModal = useRef<(arg: () => Promise<boolean>) => void>();
|
||||
|
||||
const [disableBack, setDisableBack] = useState<boolean>(true)
|
||||
const [disableForward, setDisableForward] = useState<boolean>(true)
|
||||
|
||||
const buttonPreInstallSx = {
|
||||
...(failedPreInstall ? {
|
||||
bgcolor: '#ffc107',
|
||||
'&:hover': {
|
||||
bgcolor: '#ffc107',
|
||||
},
|
||||
} : successPreInstall && {
|
||||
bgcolor: green[500],
|
||||
'&:hover': {
|
||||
bgcolor: green[700],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const buttonInstallSx = {
|
||||
...(failedInstall ? {
|
||||
bgcolor: '#ffc107',
|
||||
'&:hover': {
|
||||
bgcolor: '#ffc107',
|
||||
},
|
||||
} :successInstall && {
|
||||
bgcolor: green[500],
|
||||
'&:hover': {
|
||||
bgcolor: green[700],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const buttonKeyImportSx = {
|
||||
...(failedKeyImport ? {
|
||||
bgcolor: '#ffc107',
|
||||
'&:hover': {
|
||||
bgcolor: '#ffc107',
|
||||
},
|
||||
} : successKeyImport && {
|
||||
bgcolor: green[500],
|
||||
'&:hover': {
|
||||
bgcolor: green[700],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const buttonPostInstallSx = {
|
||||
...(failedPostInstall ? {
|
||||
bgcolor: '#ffc107',
|
||||
'&:hover': {
|
||||
bgcolor: '#ffc107',
|
||||
},
|
||||
} : successPostInstall && {
|
||||
bgcolor: green[500],
|
||||
'&:hover': {
|
||||
bgcolor: green[700],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
const [isModalOpen, setModalOpen] = useState<boolean>(false)
|
||||
const [keyStorePath, setKeystorePath] = useState<string>('')
|
||||
const [keystorePassword, setKeystorePassword] = useState<string>('')
|
||||
|
||||
const bufferLoad:() => Promise<boolean> = () => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(true), 1500)
|
||||
})
|
||||
}
|
||||
|
||||
const handlePreInstall: () => Promise<boolean> = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (!loadingPreInstall) {
|
||||
setSuccessPreInstall(false);
|
||||
setLoadingPreInstall(true);
|
||||
|
||||
Promise.all([window.ethDocker.preInstall(), bufferLoad()]).then((res) => {
|
||||
setSuccessPreInstall(true);
|
||||
setLoadingPreInstall(false);
|
||||
resolve(res[0])
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const handleInstall: () => Promise<boolean> = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (!loadingInstall) {
|
||||
setSuccessInstall(false);
|
||||
setLoadingInstall(true);
|
||||
|
||||
Promise.all([window.ethDocker.install(props.installationDetails), bufferLoad()]).then((res) => {
|
||||
setSuccessInstall(true);
|
||||
setLoadingInstall(false);
|
||||
resolve(res[0])
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const handleKeyImportModal: () => Promise<boolean> = () => {
|
||||
return new Promise((resolve: (arg: () => Promise<boolean>) => void) => {
|
||||
setSuccessKeyImport(false)
|
||||
setLoadingKeyImport(true);
|
||||
setModalOpen(true)
|
||||
resolveModal.current = resolve
|
||||
}).then((keyImp: () => Promise<boolean>) => {
|
||||
return new Promise((resolve: (arg: boolean) => void ) => {
|
||||
Promise.all([keyImp(), bufferLoad()]).then(res => {
|
||||
setSuccessKeyImport(true)
|
||||
setLoadingKeyImport(false);
|
||||
resolve(res[0])
|
||||
}).catch(err => {
|
||||
console.error('error importing key: ',err)
|
||||
setLoadingKeyImport(false);
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}).catch(err => {
|
||||
console.error('error filling out key import modal: ',err)
|
||||
setLoadingKeyImport(false);
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
const handlePostInstall: () => Promise<boolean> = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (!loadingPostInstall) {
|
||||
setSuccessPostInstall(false);
|
||||
setLoadingPostInstall(true);
|
||||
|
||||
Promise.all([ window.ethDocker.postInstall(props.installationDetails.network), bufferLoad()]).then((res) => {
|
||||
setSuccessPostInstall(true);
|
||||
setLoadingPostInstall(false);
|
||||
resolve(res[0])
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const install = async (step: number) => {
|
||||
if (!step) {
|
||||
step = 0
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 0:
|
||||
setFailedPreInstall(false)
|
||||
let preInstallResult = await handlePreInstall()
|
||||
if (!preInstallResult) {
|
||||
setFailedPreInstall(true)
|
||||
setDisableBack(false)
|
||||
return
|
||||
}
|
||||
step = 1
|
||||
case 1:
|
||||
setFailedInstall(false)
|
||||
let installResult = await handleInstall()
|
||||
if (!installResult) {
|
||||
setFailedInstall(true)
|
||||
setDisableBack(false)
|
||||
return
|
||||
}
|
||||
step += 1
|
||||
|
||||
case 2:
|
||||
setFailedKeyImport(false)
|
||||
let keyImportResult = await handleKeyImportModal()
|
||||
if (!keyImportResult) {
|
||||
setFailedKeyImport(true)
|
||||
setDisableBack(false)
|
||||
return
|
||||
}
|
||||
step += 1
|
||||
|
||||
case 3:
|
||||
setFailedPostInstall(false)
|
||||
let postInstallResult = await handlePostInstall()
|
||||
if (!postInstallResult) {
|
||||
setFailedPostInstall(true)
|
||||
setDisableBack(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setDisableForward(false)
|
||||
setDisableBack(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
install(0)
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Grid item container direction="column" spacing={2}>
|
||||
<Grid item>
|
||||
<Typography variant="h1" align='center'>
|
||||
Installing
|
||||
</Typography>
|
||||
</Grid>
|
||||
<ContentGrid item container>
|
||||
<Grid item container>
|
||||
<Grid item xs={3}></Grid>
|
||||
<Grid item container justifyContent="center" alignItems="center" xs={2}>
|
||||
<Box sx={{ m: 1, position: 'relative' }}>
|
||||
<Fab
|
||||
aria-label="save"
|
||||
color="primary"
|
||||
sx={buttonPreInstallSx}
|
||||
disabled={!failedPreInstall}
|
||||
onClick={failedPreInstall ? () => install(0) : () => {}}
|
||||
>
|
||||
{
|
||||
!failedPreInstall ? successPreInstall ? <DoneOutline sx={{
|
||||
color: green[500],
|
||||
}}/> : <DownloadingOutlined /> : <ErrorOutline sx={{ color: red[500] }} />
|
||||
}
|
||||
</Fab>
|
||||
{loadingPreInstall && (
|
||||
<CircularProgress
|
||||
size={68}
|
||||
sx={{
|
||||
color: green[500],
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
left: -6,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={1}></Grid>
|
||||
<Grid item container justifyContent="flex-start" alignItems="center" xs={3}>
|
||||
<span>Downloading dependencies</span>
|
||||
</Grid>
|
||||
<Grid item xs={3}></Grid>
|
||||
</Grid>
|
||||
<Grid item container>
|
||||
<Grid item xs={3}></Grid>
|
||||
<Grid item container justifyContent="center" alignItems="center" xs={2}>
|
||||
<Box sx={{ m: 1, position: 'relative' }}>
|
||||
<Fab
|
||||
aria-label="save"
|
||||
color="primary"
|
||||
sx={buttonInstallSx}
|
||||
disabled={!failedInstall}
|
||||
onClick={failedInstall ? () => install(1) : () => {}}
|
||||
>
|
||||
{
|
||||
!failedInstall ? successInstall ? <DoneOutline sx={{
|
||||
color: green[500],
|
||||
}} /> : <ComputerOutlined /> : <ErrorOutline sx={{ color: red[500] }} />
|
||||
}
|
||||
</Fab>
|
||||
{loadingInstall && (
|
||||
<CircularProgress
|
||||
size={68}
|
||||
sx={{
|
||||
color: green[500],
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
left: -6,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={1}></Grid>
|
||||
<Grid item container justifyContent="flex-start" alignItems="center" xs={4}>
|
||||
<span>Installing services</span>
|
||||
</Grid>
|
||||
<Grid item xs={3}></Grid>
|
||||
</Grid>
|
||||
<Grid item container>
|
||||
<Grid item xs={3}></Grid>
|
||||
<Grid item container justifyContent="center" alignItems="center" xs={2}>
|
||||
<Box sx={{ m: 1, position: 'relative' }}>
|
||||
<Fab
|
||||
aria-label="save"
|
||||
color="primary"
|
||||
sx={buttonKeyImportSx}
|
||||
disabled={!failedKeyImport}
|
||||
onClick={failedKeyImport ? () => install(2) : () => {}}
|
||||
>
|
||||
{
|
||||
!failedKeyImport ? successKeyImport ? <DoneOutline sx={{
|
||||
color: green[500],
|
||||
}} /> : <KeyOutlined />: <ErrorOutline sx={{ color: red[500] }} />
|
||||
}
|
||||
</Fab>
|
||||
{loadingKeyImport && (
|
||||
<CircularProgress
|
||||
size={68}
|
||||
sx={{
|
||||
color: green[500],
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
left: -6,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={1}></Grid>
|
||||
<Grid item container justifyContent="flex-start" alignItems="center" xs={4}>
|
||||
<span>Key Import</span>
|
||||
</Grid>
|
||||
<Grid item xs={3}></Grid>
|
||||
</Grid>
|
||||
<Grid item container>
|
||||
<Grid item xs={3}></Grid>
|
||||
<Grid item container justifyContent="center" alignItems="center" xs={2}>
|
||||
<Box sx={{ m: 1, position: 'relative' }}>
|
||||
<Fab
|
||||
aria-label="save"
|
||||
color="primary"
|
||||
sx={buttonPostInstallSx}
|
||||
disabled={!failedPostInstall}
|
||||
onClick={failedPostInstall ? () => install(3) : () => {}}
|
||||
>
|
||||
{
|
||||
!failedPostInstall ? successPostInstall ?
|
||||
<DoneOutline sx={{
|
||||
color: green[500],
|
||||
}} /> : <RocketLaunchOutlined /> : <ErrorOutline sx={{ color: red[500] }} />
|
||||
}
|
||||
</Fab>
|
||||
{loadingPostInstall && (
|
||||
<CircularProgress
|
||||
size={68}
|
||||
sx={{
|
||||
color: green[500],
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
left: -6,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={1}></Grid>
|
||||
<Grid item container justifyContent="flex-start" alignItems="center" xs={4}>
|
||||
<span>Configuring and launching</span>
|
||||
</Grid>
|
||||
<Grid item xs={3}></Grid>
|
||||
</Grid>
|
||||
</ContentGrid>
|
||||
{/* props.children is the stepper */}
|
||||
{props.children}
|
||||
<StepNavigation
|
||||
onPrev={props.onStepBack}
|
||||
onNext={props.onStepForward}
|
||||
backLabel={"Back"}
|
||||
nextLabel={"Finish"}
|
||||
disableBack={disableBack}
|
||||
disableNext={disableForward}
|
||||
/>
|
||||
<ImportKeystore
|
||||
setModalOpen={setModalOpen}
|
||||
isModalOpen={isModalOpen}
|
||||
setKeystorePassword={setKeystorePassword}
|
||||
setKeystorePath={setKeystorePath}
|
||||
keyStorePath={keyStorePath}
|
||||
keystorePassword={keystorePassword}
|
||||
closing={resolveModal}
|
||||
installationDetails={props.installationDetails}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default Install;
|
||||
72
src/react/components/NetworkPicker.tsx
Normal file
72
src/react/components/NetworkPicker.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { BackgroundLight, } from '../colors';
|
||||
import { FormControl, FormControlLabel, Radio, RadioGroup, Button, Typography } from '@mui/material';
|
||||
import React, { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import { Network } from '../types';
|
||||
import styled from '@emotion/styled';
|
||||
import { InstallDetails } from '../../electron/IMultiClientInstaller';
|
||||
|
||||
const Container = styled.div`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 20px;
|
||||
width: 350px;
|
||||
border-radius: 20px;
|
||||
background: ${BackgroundLight};
|
||||
`;
|
||||
|
||||
const Submit = styled(Button)`
|
||||
margin: 8px auto 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
type NetworkPickerProps = {
|
||||
handleCloseNetworkModal: (event: object, reason: string) => void,
|
||||
setInstallationDetails: Dispatch<SetStateAction<InstallDetails>>,
|
||||
installationDetails: InstallDetails,
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the network picker modal component where the user selects the desired network.
|
||||
*
|
||||
* @param props.handleCloseNetworkModal function to handle closing the network modal
|
||||
* @param props.setInstallationDetails the currently set installation details
|
||||
* @param props.installationDetails the current installation details
|
||||
* @returns the network picker element to render
|
||||
*/
|
||||
export const NetworkPicker = (props: NetworkPickerProps) => {
|
||||
|
||||
const closePicker = (evt: React.FormEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
props.handleCloseNetworkModal({}, 'submitClick');
|
||||
}
|
||||
|
||||
const networkChanged = (selected: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
let network = selected.target.value as Network
|
||||
let details = { ...props.installationDetails, network }
|
||||
|
||||
props.setInstallationDetails(details);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Typography id="modal-modal-title" align='center' variant="h4" component="h2">
|
||||
Network
|
||||
</Typography>
|
||||
<hr style={{ borderColor: 'orange' }} />
|
||||
<form onSubmit={closePicker}>
|
||||
<FormControl fullWidth focused style={{padding: '16px'}}>
|
||||
<RadioGroup aria-label="network" name="network" value={props.installationDetails.network} onChange={networkChanged}>
|
||||
<FormControlLabel sx={{ my: 1 }} value={Network.PRATER} control={<Radio />} label={Network.PRATER} />
|
||||
<FormControlLabel sx={{ my: 1 }} value={Network.MAINNET} control={<Radio />} label={Network.MAINNET} />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<Submit variant="contained" color="primary" type="submit" tabIndex={1}>OK</Submit>
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
48
src/react/components/StepNavigation.tsx
Normal file
48
src/react/components/StepNavigation.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { FC, ReactElement } from "react";
|
||||
import { Grid, Button } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
onPrev: () => void,
|
||||
onNext: () => void,
|
||||
disableBack?: boolean,
|
||||
disableNext?: boolean,
|
||||
hideBack?: boolean,
|
||||
hideNext?: boolean,
|
||||
backLabel?: string,
|
||||
nextLabel?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* This contains the navigation components (back, next) that the user uses to navigate through the process
|
||||
*
|
||||
* @param props.onPrev the function to execute when the user hits previous
|
||||
* @param props.onNext the function to execute when the user hits next
|
||||
* @param props.disableBack whether or not to disable the back button
|
||||
* @param props.disableNext whether or not to disable the next button
|
||||
* @param props.hideBack whether or not to hide the back button
|
||||
* @param props.hideNext whether or not to hide the next button
|
||||
* @param props.backLabel the label for the back button
|
||||
* @param props.nextLabel the label for the next button
|
||||
* @returns react component to render
|
||||
*/
|
||||
const StepNavigation: FC<Props> = (props): ReactElement => {
|
||||
return (
|
||||
<Grid item container justifyContent="space-between">
|
||||
<Grid item xs={2} />
|
||||
<Grid item xs={2} justifyContent={'flex-start'} display={'flex'}>
|
||||
{!props.hideBack && (
|
||||
<Button variant="contained" color="primary" disabled={props.disableBack} onClick={props.onPrev} tabIndex={3}>{props.backLabel}</Button>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={4} />
|
||||
<Grid item xs={2} justifyContent={'flex-end'} display={'flex'}>
|
||||
{!props.hideNext && (
|
||||
<Button variant="contained" color="primary" disabled={props.disableNext} onClick={props.onNext} tabIndex={2}>{props.nextLabel}</Button>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={2} />
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default StepNavigation;
|
||||
37
src/react/components/VersionFooter.tsx
Normal file
37
src/react/components/VersionFooter.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Grid, Typography } from '@mui/material';
|
||||
import React from "react";
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
declare var VERSION: string;
|
||||
declare var COMMITHASH: string;
|
||||
|
||||
const SoftText = styled(Typography)`
|
||||
color: gray;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
position: fixed;
|
||||
bottom: 35;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
/**
|
||||
* This component is a footer used to display the version and commit hash.
|
||||
*
|
||||
* @returns the footer component containing the version and commit hash
|
||||
*/
|
||||
const VersionFooter = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Grid container direction="column">
|
||||
<Grid item xs={12}>
|
||||
<SoftText>Version: {VERSION} - Commit Hash: {COMMITHASH}</SoftText>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default VersionFooter;
|
||||
17
src/react/components/icons/HomeIcon.tsx
Normal file
17
src/react/components/icons/HomeIcon.tsx
Normal file
File diff suppressed because one or more lines are too long
17
src/react/components/icons/KeyIcon.tsx
Normal file
17
src/react/components/icons/KeyIcon.tsx
Normal file
File diff suppressed because one or more lines are too long
63
src/react/constants.ts
Normal file
63
src/react/constants.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ConsensusClient, ExecutionClient } from '../electron/IMultiClientInstaller';
|
||||
import { StepKey } from './types';
|
||||
|
||||
export const errors = {
|
||||
FOLDER: "Please select a folder.",
|
||||
FOLDER_DOES_NOT_EXISTS: "Folder does not exist. Select an existing folder.",
|
||||
FOLDER_IS_NOT_WRITABLE: "Cannot write in this folder. Select a folder in which you have write permission.",
|
||||
};
|
||||
|
||||
export const stepLabels = {
|
||||
[StepKey.SystemCheck]: 'System Check',
|
||||
[StepKey.Configuration]: 'Configuration',
|
||||
[StepKey.Installing]: 'Install',
|
||||
};
|
||||
|
||||
|
||||
export interface IExecutionClient {
|
||||
key: ExecutionClient;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface IConsensusClient {
|
||||
key: ConsensusClient;
|
||||
label: string;
|
||||
}
|
||||
|
||||
|
||||
export const ConsensusClients: IConsensusClient[] = [{
|
||||
key: ConsensusClient.PRYSM,
|
||||
label: 'Prysm',
|
||||
},
|
||||
{
|
||||
key: ConsensusClient.LIGHTHOUSE,
|
||||
label: 'Lighthouse',
|
||||
}, {
|
||||
key: ConsensusClient.NIMBUS,
|
||||
label: 'Nimbus',
|
||||
}, {
|
||||
key: ConsensusClient.TEKU,
|
||||
label: 'Teku'
|
||||
},
|
||||
{
|
||||
key: ConsensusClient.LODESTAR,
|
||||
label: 'Lodestar'
|
||||
},
|
||||
]
|
||||
export const ExecutionClients: IExecutionClient[] = [{
|
||||
key: ExecutionClient.GETH,
|
||||
label: 'Geth',
|
||||
},
|
||||
{
|
||||
key: ExecutionClient.ERIGON,
|
||||
label: 'Erigon'
|
||||
},
|
||||
{
|
||||
key: ExecutionClient.BESU,
|
||||
label: 'Besu'
|
||||
},
|
||||
{
|
||||
key: ExecutionClient.NETHERMIND,
|
||||
label: 'Nethermind'
|
||||
},
|
||||
]
|
||||
@@ -1,7 +1,4 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Stakehouse</title>
|
||||
</head>
|
||||
<body style="width:100%;height:100%;margin:0">
|
||||
<div id="app" style="width:100%;height:100%">
|
||||
</div>
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import {
|
||||
Black,
|
||||
DisabledButton,
|
||||
Heading,
|
||||
MainContent
|
||||
} from "../colors";
|
||||
|
||||
import Footer from "../components/Footer";
|
||||
import React from "react";
|
||||
import { shell } from "electron";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height:100vh;
|
||||
`;
|
||||
|
||||
const LandingHeader = styled.div`
|
||||
font-weight: 700;
|
||||
font-size: 35;
|
||||
margin-top: 50;
|
||||
color: ${Heading};
|
||||
max-width: 550;
|
||||
flex-grow:1;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
color: ${MainContent};
|
||||
margin-top: 20;
|
||||
width: 650;
|
||||
flex-grow: 6;
|
||||
`;
|
||||
|
||||
const StyledLink = styled.span`
|
||||
color: ${Heading};
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const ButtonContainer = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const ImportKeysButton = styled.div`
|
||||
color: ${Black};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
background-color: ${DisabledButton};
|
||||
padding: 16 24;
|
||||
border-radius: 20px;
|
||||
text-decoration: none;
|
||||
|
||||
transition: 250ms background-color ease;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const sendToLaunchpad = () => {
|
||||
shell.openExternal("https://pyrmont.launchpad.ethereum.org/");
|
||||
}
|
||||
|
||||
const Deposit = () => {
|
||||
// TODO: add browse
|
||||
// TODO: run validator then go to status page on Run click
|
||||
return (
|
||||
<Container>
|
||||
<LandingHeader>Deposit</LandingHeader>
|
||||
<Content>
|
||||
1) Head to the <StyledLink onClick={sendToLaunchpad}>launchpad</StyledLink> to deposit 32 Goerli ETH.
|
||||
<br />
|
||||
<br />
|
||||
<em>Note: Your nodes are set up already, so ignore those steps.</em>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
2) After depositing on the launchpad, import validator key here:
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<ButtonContainer>
|
||||
<ImportKeysButton>
|
||||
Import Validator Keys `keystore-*.json` file
|
||||
<br />
|
||||
(still in development)
|
||||
</ImportKeysButton>
|
||||
</ButtonContainer>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
3) Click Run.
|
||||
</Content>
|
||||
<Footer backLink={"/status"} backLabel={"Back"} nextLink={"/status"} nextLabel={"Run"} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Deposit;
|
||||
@@ -1,106 +1,190 @@
|
||||
import {
|
||||
Black,
|
||||
Button,
|
||||
ButtonHover,
|
||||
Heading,
|
||||
MainContent,
|
||||
Red
|
||||
} from "../colors";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import React, { FC, ReactElement, useState, Dispatch, SetStateAction } from "react";
|
||||
import styled from '@emotion/styled';
|
||||
import { Container, Grid, Modal, Typography } from '@mui/material';
|
||||
import { Button } from '@mui/material';
|
||||
import { HomeIcon } from "../components/icons/HomeIcon";
|
||||
import { NetworkPicker } from "../components/NetworkPicker";
|
||||
import { StepSequenceKey } from '../types'
|
||||
import VersionFooter from "../components/VersionFooter";
|
||||
import { InstallDetails } from "../../electron/IMultiClientInstaller";
|
||||
|
||||
import { Link, withRouter } from "react-router-dom";
|
||||
import React from "react";
|
||||
import { shell } from "electron";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { History } from "history";
|
||||
|
||||
import { isRocketPoolInstalled } from "../commands/RocketPool";
|
||||
|
||||
const Container = styled.div`
|
||||
const StyledMuiContainer = styled(Container)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const LandingHeader = styled.div`
|
||||
font-weight: 700;
|
||||
font-size: 35;
|
||||
margin-top: 120;
|
||||
color: ${Heading};
|
||||
max-width: 550;
|
||||
text-align: center;
|
||||
const NetworkDiv = styled.div`
|
||||
margin-top: 35px;
|
||||
margin-left: 35px;
|
||||
align-self: flex-start;
|
||||
color: gray;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
color: ${MainContent};
|
||||
margin-top: 40;
|
||||
max-width: 650;
|
||||
const LandingHeader = styled(Typography)`
|
||||
font-size: 36px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const StartButton = styled(Link)`
|
||||
color: ${Black};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
const SubHeader = styled(Typography)`
|
||||
margin-top: 20px;
|
||||
`;
|
||||
|
||||
const Links = styled.div`
|
||||
margin-top: 20px;
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Typography)`
|
||||
cursor: pointer;
|
||||
display: inline;
|
||||
`;
|
||||
|
||||
const InfoLabel = styled.span`
|
||||
color: gray;
|
||||
`;
|
||||
|
||||
const EnterGrid = styled(Grid)`
|
||||
margin-top: 35px;
|
||||
align-items: center;
|
||||
height: 24;
|
||||
background-color: ${Button};
|
||||
padding: 16 24;
|
||||
border-radius: 10%;
|
||||
text-decoration: none;
|
||||
|
||||
transition: 250ms background-color ease;
|
||||
cursor: pointer;
|
||||
margin-top: 60;
|
||||
|
||||
&:hover {
|
||||
background-color: ${ButtonHover};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled.em`
|
||||
color: ${Heading};
|
||||
cursor: pointer;
|
||||
const ContentGrid = styled(Grid)`
|
||||
height: 320px;
|
||||
margin-top: 16px;
|
||||
`;
|
||||
|
||||
const Testnet = styled.b`
|
||||
color: ${Red}
|
||||
`;
|
||||
type HomeProps = {
|
||||
installationDetails: InstallDetails,
|
||||
setInstallationDetails: Dispatch<SetStateAction<InstallDetails>>
|
||||
}
|
||||
|
||||
const Home = ({ history }: {history: History}) => {
|
||||
if (isRocketPoolInstalled()) {
|
||||
history.push('/status');
|
||||
/**
|
||||
* Home page and entry point of the app. This page displays general information
|
||||
* and options for a user to create a new secret recovery phrase or use an
|
||||
* existing one.
|
||||
*
|
||||
* @param props passed in data for the component to use
|
||||
* @returns the react element to render
|
||||
*/
|
||||
const Home: FC<HomeProps> = (props): ReactElement => {
|
||||
const [showNetworkModal, setShowNetworkModal] = useState(false);
|
||||
const [networkModalWasOpened, setNetworkModalWasOpened] = useState(false);
|
||||
const [enterSelected, setEnterSelected] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOpenNetworkModal = () => {
|
||||
setShowNetworkModal(true);
|
||||
setNetworkModalWasOpened(true);
|
||||
}
|
||||
|
||||
const sendToRocketpool = () => {
|
||||
shell.openExternal("https://www.rocketpool.net/");
|
||||
const handleCloseNetworkModal = (event: object, reason: string) => {
|
||||
if (reason !== 'backdropClick') {
|
||||
setShowNetworkModal(false);
|
||||
|
||||
if (enterSelected) {
|
||||
handleEnter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sendToGithub = () => {
|
||||
window.electronAPI.shellOpenExternal("https://github.com/stake-house/wagyu-installer");
|
||||
}
|
||||
|
||||
const sendToDiscord = () => {
|
||||
window.electronAPI.shellOpenExternal("https://discord.io/ethstaker");
|
||||
}
|
||||
|
||||
const handleEnter = () => {
|
||||
|
||||
// Backend usage example
|
||||
// const consoleWrite: OutputLogs = (message: string): void => {
|
||||
// console.log(message);
|
||||
// };
|
||||
|
||||
// window.ethDocker.preInstall(consoleWrite).then(preInstallResult => {
|
||||
// console.log(`preInstall ${preInstallResult}`);
|
||||
/*if (preInstallResult) {
|
||||
const installationDetails: InstallDetails = {
|
||||
network: props.network,
|
||||
executionClient: ExecutionClient.GETH,
|
||||
consensusClient: ConsensusClient.LIGHTHOUSE
|
||||
};
|
||||
|
||||
window.ethDocker.install(installationDetails).then(installResult => {
|
||||
console.log(`install ${installResult}`);
|
||||
if (installResult) {
|
||||
window.ethDocker.importKeys(
|
||||
props.network,
|
||||
'/home/remy/keys',
|
||||
'password').then(importKeysResult => {
|
||||
console.log(`importKeys ${importKeysResult}`);
|
||||
|
||||
if (importKeysResult) {
|
||||
window.ethDocker.postInstall(props.network).then(postInstallResult => {
|
||||
console.log(`postInstall ${postInstallResult}`);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}*/
|
||||
// });
|
||||
|
||||
setEnterSelected(true);
|
||||
|
||||
if (!networkModalWasOpened) {
|
||||
handleOpenNetworkModal();
|
||||
} else {
|
||||
const location = {
|
||||
pathname: `/wizard/${StepSequenceKey.Install}`
|
||||
}
|
||||
|
||||
navigate(location);
|
||||
}
|
||||
}
|
||||
|
||||
const tabIndex = (priority: number) => showNetworkModal ? -1 : priority;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<LandingHeader>Welcome to Stakehouse</LandingHeader>
|
||||
<Content>
|
||||
This is your portal into the Eth2 world - welcome.
|
||||
<br />
|
||||
<br/>
|
||||
<br />
|
||||
A one-click staking installer for the <Testnet>pyrmont testnet</Testnet>.
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
It will configure the following for you*:
|
||||
<ul>
|
||||
<li>Eth 1 Node</li>
|
||||
<li>Eth 2 Beacon Node</li>
|
||||
<li>Eth 2 Validator</li>
|
||||
</ul>
|
||||
<br/>
|
||||
<br/>
|
||||
*Note: we use the Rocket Pool install infrastructure which runs everything in docker, more info <StyledLink onClick={sendToRocketpool}>here</StyledLink>
|
||||
</Content>
|
||||
<StartButton to="/systemcheck">Enter</StartButton>
|
||||
</Container>
|
||||
<StyledMuiContainer>
|
||||
<NetworkDiv>
|
||||
Select Network: <Button color="primary" onClick={handleOpenNetworkModal} tabIndex={tabIndex(1)}>{props.installationDetails.network}</Button>
|
||||
</NetworkDiv>
|
||||
<Modal
|
||||
open={showNetworkModal}
|
||||
onClose={handleCloseNetworkModal}
|
||||
>
|
||||
{/* Added <div> here per the following link to fix error https://stackoverflow.com/a/63521049/5949270 */}
|
||||
<div>
|
||||
<NetworkPicker handleCloseNetworkModal={handleCloseNetworkModal} setInstallationDetails={props.setInstallationDetails} installationDetails={props.installationDetails}></NetworkPicker>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<LandingHeader variant="h1">Welcome!</LandingHeader>
|
||||
<HomeIcon />
|
||||
<SubHeader>Your installer for staking on Ethereum</SubHeader>
|
||||
|
||||
<Links>
|
||||
<StyledLink display="inline" color="primary" onClick={sendToGithub} tabIndex={tabIndex(0)}>Github</StyledLink>
|
||||
|
|
||||
<StyledLink display="inline" color="primary" onClick={sendToDiscord} tabIndex={tabIndex(0)}>Discord</StyledLink>
|
||||
</Links>
|
||||
|
||||
<EnterGrid>
|
||||
<Button variant="contained" color="primary" onClick={handleEnter} tabIndex={tabIndex(1)}>
|
||||
Enter
|
||||
</Button>
|
||||
</EnterGrid>
|
||||
<VersionFooter />
|
||||
</StyledMuiContainer>
|
||||
);
|
||||
};
|
||||
export default withRouter(Home);
|
||||
|
||||
export default Home;
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Heading, MainContent } from '../colors';
|
||||
|
||||
import Footer from '../components/Footer';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height:100vh;
|
||||
`;
|
||||
|
||||
const LandingHeader = styled.div`
|
||||
font-weight: 700;
|
||||
font-size: 35;
|
||||
margin-top: 50;
|
||||
color: ${Heading};
|
||||
max-width: 550;
|
||||
flex-grow:1;
|
||||
`;
|
||||
|
||||
|
||||
const Content = styled.div`
|
||||
color: ${MainContent};
|
||||
margin-top: 20;
|
||||
width: 650;
|
||||
flex-grow: 6;
|
||||
`;
|
||||
|
||||
const InstallFailed = () => {
|
||||
return (
|
||||
<Container>
|
||||
<LandingHeader>Install Failed</LandingHeader>
|
||||
<Content>
|
||||
Unfortunately your install failed. At this time we cannot provide any additonal info.
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
Please reach out to the ethstaker community for help.
|
||||
</Content>
|
||||
<Footer backLink={"/"} backLabel={"Home"} nextLink={""} nextLabel={""} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
export default InstallFailed;
|
||||
@@ -1,154 +0,0 @@
|
||||
import {
|
||||
Black,
|
||||
Button,
|
||||
ButtonHover,
|
||||
Heading,
|
||||
MainContent
|
||||
} from "../colors";
|
||||
import { Link, withRouter } from "react-router-dom";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
|
||||
import { History } from "history";
|
||||
import { installAndStartRocketPool } from "../commands/RocketPool";
|
||||
|
||||
const ENTER_KEYCODE = 13;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height:100vh;
|
||||
`;
|
||||
|
||||
const LandingHeader = styled.div`
|
||||
font-weight: 700;
|
||||
font-size: 35;
|
||||
margin-top: 50;
|
||||
color: ${Heading};
|
||||
max-width: 550;
|
||||
flex-grow:1;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
color: ${MainContent};
|
||||
margin-top: 20;
|
||||
width: 650;
|
||||
flex-grow: 6;
|
||||
`;
|
||||
|
||||
const rotate = keyframes`
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
`;
|
||||
|
||||
const SpinnerContainer = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const LoadingSpinner = styled.div`
|
||||
border: 16px solid #f3f3f3; /* Light grey */
|
||||
border-top: 16px solid #3498db; /* Blue */
|
||||
border-radius: 50%;
|
||||
margin-top: 30px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
animation: ${rotate} 2s linear infinite;
|
||||
`;
|
||||
|
||||
const LogsContainer = styled.div`
|
||||
height: 250px;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
border-style: groove;
|
||||
color: black;
|
||||
`;
|
||||
|
||||
const LogsList = styled.ul`
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
`;
|
||||
|
||||
const LogsListItem = styled.li`
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const LogsContainerAnchor = styled.div`
|
||||
`;
|
||||
|
||||
const Installing = ({ history }: {history: History}) => {
|
||||
const anchorRef = useRef(document.createElement("div"));
|
||||
|
||||
const [stdoutText, setStdoutText] = useState([""]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
installAndStartRocketPool(installCallback, stdoutCallback);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
anchorRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [stdoutText]);
|
||||
|
||||
const stdoutCallback = (text: string[]) => {
|
||||
console.log("installing cb with " + text.join());
|
||||
setStdoutText(stdoutText.concat(text));
|
||||
}
|
||||
|
||||
const installCallback = (success: boolean) => {
|
||||
if (success) {
|
||||
console.log("install succeeded")
|
||||
|
||||
// wait 5 seconds before redirecting to make sure everythings up
|
||||
setTimeout(() => {
|
||||
history.push('/status');
|
||||
}, 5000);
|
||||
} else {
|
||||
console.log("install failed");
|
||||
history.push("/installfailed");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<LandingHeader>Install</LandingHeader>
|
||||
<Content>
|
||||
Installing...
|
||||
<br />
|
||||
<br />
|
||||
May take 2-4 minutes depending on internet speed.
|
||||
<SpinnerContainer>
|
||||
<LoadingSpinner />
|
||||
</SpinnerContainer>
|
||||
<br/>
|
||||
{ // Only show logs container if there are some
|
||||
stdoutText.length > 1
|
||||
&&
|
||||
<div>
|
||||
Install logs:
|
||||
<LogsContainer>
|
||||
<LogsList>
|
||||
{stdoutText.map((line, i) => {
|
||||
return (<LogsListItem key={i}>{line}</LogsListItem>)
|
||||
})}
|
||||
</LogsList>
|
||||
<LogsContainerAnchor ref={anchorRef}></LogsContainerAnchor>
|
||||
</LogsContainer>
|
||||
</div>
|
||||
}
|
||||
|
||||
</Content>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(Installing);
|
||||
140
src/react/pages/MainWizard.tsx
Normal file
140
src/react/pages/MainWizard.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { FC, ReactElement, SetStateAction, useState, Dispatch } from 'react';
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { Stepper, Step, StepLabel, Grid, Typography } from '@mui/material';
|
||||
import styled from '@emotion/styled';
|
||||
import { StepKey } from '../types';
|
||||
import { stepLabels } from '../constants';
|
||||
import { StepSequenceKey } from '../types';
|
||||
import VersionFooter from '../components/VersionFooter';
|
||||
import Install from '../components/InstallFlow/2-Install';
|
||||
import Configuration from '../components/InstallFlow/1-Configuration';
|
||||
import SystemCheck from '../components/InstallFlow/0-SystemCheck';
|
||||
import { InstallDetails } from '../../electron/IMultiClientInstaller';
|
||||
|
||||
const stepSequenceMap: Record<string, StepKey[]> = {
|
||||
install: [
|
||||
// StepKey.SystemCheck,
|
||||
StepKey.Configuration,
|
||||
StepKey.Installing,
|
||||
]
|
||||
}
|
||||
|
||||
const MainGrid = styled(Grid)`
|
||||
`;
|
||||
|
||||
const StyledStepper = styled(Stepper)`
|
||||
background-color: transparent;
|
||||
`
|
||||
|
||||
type RouteParams = {
|
||||
stepSequenceKey: StepSequenceKey;
|
||||
};
|
||||
|
||||
type WizardProps = {
|
||||
installationDetails: InstallDetails,
|
||||
setInstallationDetails: Dispatch<SetStateAction<InstallDetails>>
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main wizard through which each piece of functionality for the app runs.
|
||||
*
|
||||
* This wizard manages the global stepper showing the user where they are in the process.
|
||||
*
|
||||
* @param props passed in data for the component to use
|
||||
* @returns the react element to render
|
||||
*/
|
||||
const Wizard: FC<WizardProps> = (props): ReactElement => {
|
||||
const { stepSequenceKey } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [activeStepIndex, setActiveStepIndex] = useState(0);
|
||||
|
||||
const stepSequence = stepSequenceMap[stepSequenceKey as string];
|
||||
const activeStepKey = stepSequence[activeStepIndex];
|
||||
|
||||
|
||||
const onStepForward = () => {
|
||||
if (activeStepIndex === stepSequence.length - 1) {
|
||||
const location = {
|
||||
pathname: `/systemOverview`
|
||||
}
|
||||
|
||||
navigate(location);
|
||||
}
|
||||
setActiveStepIndex(activeStepIndex + 1);
|
||||
}
|
||||
|
||||
const onStepBack = () => {
|
||||
if (activeStepIndex === 0) {
|
||||
navigate("/");
|
||||
} else {
|
||||
setActiveStepIndex(activeStepIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the UI stepper component rendering where the user is in the process
|
||||
*/
|
||||
const stepper = (
|
||||
<Grid item my={3}>
|
||||
<StyledStepper activeStep={activeStepIndex} alternativeLabel>
|
||||
{stepSequence.map((stepKey: StepKey) => (
|
||||
<Step key={stepKey}>
|
||||
<StepLabel>{stepLabels[stepKey]}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</StyledStepper>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
|
||||
const commonProps = {
|
||||
onStepForward,
|
||||
onStepBack,
|
||||
installationDetails: props.installationDetails,
|
||||
setInstallationDetails: props.setInstallationDetails,
|
||||
children: stepper,
|
||||
};
|
||||
|
||||
/**
|
||||
* This switch returns the correct react components based on the active step.
|
||||
* @returns the component to render
|
||||
*/
|
||||
const stepComponentSwitch = (): ReactElement => {
|
||||
switch (activeStepKey) {
|
||||
case StepKey.SystemCheck:
|
||||
return (
|
||||
<SystemCheck {...{ ...commonProps }} />
|
||||
);
|
||||
case StepKey.Configuration:
|
||||
return (
|
||||
<Configuration {...{ ...commonProps }} />
|
||||
);
|
||||
case StepKey.Installing:
|
||||
return (
|
||||
<Install {...{ ...commonProps }} />
|
||||
);
|
||||
default:
|
||||
return <div>No component for this step</div>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MainGrid container direction="column">
|
||||
<Grid item container>
|
||||
<Grid item xs={10} />
|
||||
<Grid item xs={2}>
|
||||
<Typography variant="caption" style={{ color: "gray" }}>
|
||||
Network: {props.installationDetails.network}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item container>
|
||||
{stepComponentSwitch()}
|
||||
</Grid>
|
||||
<VersionFooter />
|
||||
</MainGrid>
|
||||
);
|
||||
}
|
||||
|
||||
export default Wizard;
|
||||
@@ -1,282 +0,0 @@
|
||||
import {
|
||||
Black,
|
||||
Button,
|
||||
ButtonHover,
|
||||
DarkGray,
|
||||
Gray4,
|
||||
Heading,
|
||||
MainContent
|
||||
} from "../colors";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getEth2ClientName, openEth1Logs, openEth2BeaconLogs, openEth2ValidatorLogs, queryEth1PeerCount, queryEth1Status, queryEth1Syncing, queryEth2BeaconStatus, queryEth2ValidatorStatus } from "../commands/RocketPool";
|
||||
|
||||
import Footer from "../components/Footer";
|
||||
import { shell } from "electron";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height:100vh;
|
||||
`;
|
||||
|
||||
const LandingHeader = styled.div`
|
||||
font-weight: 700;
|
||||
font-size: 35;
|
||||
margin-top: 50;
|
||||
color: ${Heading};
|
||||
max-width: 550;
|
||||
flex-grow:1;
|
||||
`;
|
||||
|
||||
|
||||
const Content = styled.div`
|
||||
color: ${MainContent};
|
||||
margin-top: 20;
|
||||
width: 650;
|
||||
flex-grow: 6;
|
||||
`;
|
||||
|
||||
const ResultsTable = styled.table`
|
||||
border: 2px solid gray;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
color: white;
|
||||
`;
|
||||
|
||||
const StyledLink = styled.span`
|
||||
color: ${Heading};
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const LogsButton = styled.button`
|
||||
color: ${Black};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${Button};
|
||||
border-radius: 10px;
|
||||
text-decoration: none;
|
||||
|
||||
transition: 250ms background-color ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: ${ButtonHover};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
color: ${DarkGray};
|
||||
background-color: ${Gray4};
|
||||
}
|
||||
`;
|
||||
|
||||
// TODO: turn this into an enum?
|
||||
const NodeStatus: [string, string, string][] = [
|
||||
["Online", "\u2B24", "green"], // 0
|
||||
["Syncing", "\u2B24", "yellow"], // 1
|
||||
["Offline", "\u2B24", "red"], // 2
|
||||
["Loading...", "", ""] // 3
|
||||
]
|
||||
|
||||
// TODO: right after install, while nodes are starting up, this page says everything is "online"
|
||||
// while things are looking for peers. Need to improve that logic.
|
||||
|
||||
const Status = () => {
|
||||
const [eth1ContainerStatus, setEth1ContainerStatus] = useState(3);
|
||||
const [eth1PeerCount, setEth1PeerCount] = useState(0);
|
||||
const [eth1Syncing, setEth1Syncing] = useState(false);
|
||||
const [eth2ClientName, setEth2ClientName] = useState("");
|
||||
const [eth2BeaconContainerStatus, setEth2BeaconContainerStatus] = useState(3);
|
||||
const [eth2ValidatorContainerStatus, setEth2ValidatorContainerStatus] = useState(3);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
queryStatuses();
|
||||
setEth2ClientName(getEth2ClientName());
|
||||
setInterval(queryStatuses, 5000);
|
||||
}, 500)
|
||||
}, []);
|
||||
|
||||
const queryStatuses = () => {
|
||||
queryEth1Status(setEth1ContainerStatus);
|
||||
setEth1Syncing(queryEth1Syncing());
|
||||
setEth1PeerCount(queryEth1PeerCount());
|
||||
queryEth2BeaconStatus(setEth2BeaconContainerStatus);
|
||||
queryEth2ValidatorStatus(setEth2ValidatorContainerStatus);
|
||||
}
|
||||
|
||||
const formatStatusIcon = (status: number) => {
|
||||
return (
|
||||
<span style={{ color: NodeStatus[status][2]}}>{NodeStatus[status][1]}</span>
|
||||
)
|
||||
}
|
||||
|
||||
const eth1eth2synced = () => {
|
||||
return computeEth1Status() == 0 && computeEth2BeaconStatus() == 0;
|
||||
}
|
||||
|
||||
const computeEth1Status = (): number => {
|
||||
if (eth1ContainerStatus == 3) {
|
||||
return 3;
|
||||
} else if (eth1ContainerStatus == 2) {
|
||||
return 2;
|
||||
} else if (eth1Syncing) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const computeEth2BeaconStatus = () => {
|
||||
return eth2BeaconContainerStatus;
|
||||
}
|
||||
|
||||
const computeEth2ValidatorStatus = () => {
|
||||
return eth2ValidatorContainerStatus;
|
||||
}
|
||||
|
||||
const renderNodeStatusTable = () => {
|
||||
return (
|
||||
<ResultsTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Application</th>
|
||||
<th>Status*</th>
|
||||
<th># Peers</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Eth1 Node - geth</td>
|
||||
<td>{formatStatusIcon(computeEth1Status())} {NodeStatus[computeEth1Status()][0]}</td>
|
||||
<td>{eth1PeerCount}</td>
|
||||
<td><LogsButton onClick={openEth1Logs} disabled={eth1ContainerStatus == 2}>View Logs</LogsButton></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Eth2 Beacon Node - {eth2ClientName}</td>
|
||||
<td>{formatStatusIcon(computeEth2BeaconStatus())} {NodeStatus[computeEth2BeaconStatus()][0]}</td>
|
||||
<td>-</td>
|
||||
<td><LogsButton onClick={openEth2BeaconLogs} disabled={eth2BeaconContainerStatus == 2}>View Logs</LogsButton></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Eth2 Validator - {eth2ClientName}</td>
|
||||
<td>{formatStatusIcon(computeEth2ValidatorStatus())} {NodeStatus[computeEth2ValidatorStatus()][0]}</td>
|
||||
<td>-</td>
|
||||
<td><LogsButton onClick={openEth2ValidatorLogs} disabled={eth2ValidatorContainerStatus == 2}>View Logs</LogsButton></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ResultsTable>
|
||||
)
|
||||
}
|
||||
|
||||
const sendToEthStakerDiscord = () => {
|
||||
shell.openExternal("http://invite.gg/ethstaker");
|
||||
}
|
||||
|
||||
const sendToEthStakerSubreddit = () => {
|
||||
shell.openExternal("https://www.reddit.com/r/ethstaker/");
|
||||
}
|
||||
|
||||
const sendToGoerliEtherscan = () => {
|
||||
shell.openExternal("http://goerli.etherscan.io/");
|
||||
}
|
||||
|
||||
const sendToPyrmontBeaconchain = () => {
|
||||
shell.openExternal("https://pyrmont.beaconcha.in/");
|
||||
}
|
||||
|
||||
const sendToGetGoerliEth = () => {
|
||||
shell.openExternal("https://www.reddit.com/r/ethstaker/comments/ij56ox/best_way_to_get_goerli_ether/");
|
||||
}
|
||||
|
||||
const sendToEthereumStudymaster = () => {
|
||||
shell.openExternal("https://ethereumstudymaster.com/");
|
||||
}
|
||||
|
||||
const renderResources = () => {
|
||||
return (
|
||||
<ul>
|
||||
<li>Join the EthStaker <StyledLink onClick={sendToEthStakerDiscord}>discord</StyledLink></li>
|
||||
<li>Check out the EthStaker <StyledLink onClick={sendToEthStakerSubreddit}>subreddit</StyledLink></li>
|
||||
<li>Join the <StyledLink onClick={sendToEthereumStudymaster}>Ethereum Studymaster</StyledLink> program</li>
|
||||
<li>Grab some <StyledLink onClick={sendToGetGoerliEth}>Goerli ETH</StyledLink></li>
|
||||
<li>Familiarize yourself with <StyledLink onClick={sendToGoerliEtherscan}>etherscan</StyledLink> and <StyledLink onClick={sendToPyrmontBeaconchain}>beaconcha.in</StyledLink> </li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSubText = () => {
|
||||
if (eth1eth2synced()) {
|
||||
return (
|
||||
<div>
|
||||
Resources:
|
||||
{ renderResources() }
|
||||
If you have not deposited your Goerli eth yet, click Deposit.
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
Syncing may take a while.. here are a few things to do:
|
||||
{ renderResources() }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
return(
|
||||
<Content>
|
||||
{ renderNodeStatusTable() }
|
||||
<br />
|
||||
<br />
|
||||
*Note: "Syncing" state is only supported for Eth1. Eth1 Beacon/Validator statuses are set based on docker status.
|
||||
<br />
|
||||
Supporting "Syncing" state for Eth2 Becon high priority feature to build.
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
{ renderSubText() }
|
||||
{
|
||||
// TODO: file size for proxy of progress?
|
||||
// TODO: blinking dot for syncing and running
|
||||
// TODO: load right to status if rp is installed
|
||||
// TODO: additional data, maybe even pull some from beaconcha.in
|
||||
// TODO: add buttons to stop/start/update nodes
|
||||
// TODO: add button for logs
|
||||
// TODO: direct user to beaconcha.in to track their validator
|
||||
// TODO: button to configure alerts from beaconcha.in?
|
||||
}
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
const renderFooter = () => {
|
||||
if (eth1eth2synced()) {
|
||||
return (
|
||||
<Footer backLink={"/systemcheck"} backLabel={"Back"} nextLink={"/deposit"} nextLabel={"Deposit"} />
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Footer backLink={"/systemcheck"} backLabel={"Back"} nextLink={""} nextLabel={""} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<LandingHeader>Status</LandingHeader>
|
||||
{renderContent()}
|
||||
{renderFooter()}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Status;
|
||||
@@ -1,202 +0,0 @@
|
||||
import {
|
||||
DarkBlue,
|
||||
Heading,
|
||||
MainContent
|
||||
} from "../colors";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import Footer from "../components/Footer";
|
||||
import { isRocketPoolInstalled } from "../commands/RocketPool";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height:100vh;
|
||||
`;
|
||||
|
||||
const LandingHeader = styled.div`
|
||||
font-weight: 700;
|
||||
font-size: 35;
|
||||
margin-top: 50;
|
||||
color: ${Heading};
|
||||
max-width: 550;
|
||||
flex-grow:1;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
color: ${MainContent};
|
||||
margin-top: 20;
|
||||
width: 650;
|
||||
flex-grow: 6;
|
||||
`;
|
||||
|
||||
const Advanced = styled.div`
|
||||
font-size:15;
|
||||
color: ${DarkBlue};
|
||||
max-width: 550;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const ResultsTable = styled.table`
|
||||
border: 2px solid gray;
|
||||
width: 75%;
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
color: white;
|
||||
`;
|
||||
|
||||
const SystemCheck = () => {
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
const [arbitraryTest, setArbitraryTest] = useState(true);
|
||||
const [rocketPoolInstalled, setRocketPoolInstalled] = useState(false);
|
||||
const [systemStatus, setSystemStatus] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
runSystemCheck();
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setSystemStatus(
|
||||
arbitraryTest &&
|
||||
!rocketPoolInstalled
|
||||
);
|
||||
}, [arbitraryTest, rocketPoolInstalled]);
|
||||
|
||||
const runSystemCheck = () => {
|
||||
// TODO: do more validation here
|
||||
// TODO: check OS
|
||||
// TODO: check hardware (ram/ssd size/speed)
|
||||
// TODO: check for anything that looks like an already running eth1/2 node
|
||||
// TODO: check ports
|
||||
setRocketPoolInstalled(isRocketPoolInstalled());
|
||||
setArbitraryTest(true);
|
||||
|
||||
// TODO: add instructions/links for install/fix if a test fails
|
||||
// TODO: create a loading state and only render once the test is finished
|
||||
}
|
||||
|
||||
const resultToIcon = (result: boolean): string => {
|
||||
// TODO: make these icons - green check and red X
|
||||
return result ? "Pass" : "Fail";
|
||||
}
|
||||
|
||||
const toggleShowAdvanced = () => {
|
||||
setShowAdvanced(!showAdvanced);
|
||||
}
|
||||
|
||||
const renderRocketPoolInstalledContent = () => {
|
||||
return (
|
||||
<Content>
|
||||
It looks like you already have Rocket Pool installed. Let's hop over to check the status of your node.
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
const renderSystemCheckResultsTable = () => {
|
||||
// TODO: make this dynamic
|
||||
return (
|
||||
<ResultsTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Test</th>
|
||||
<th>Result</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Rocket Pool <i>not</i> installed</td>
|
||||
<td>{resultToIcon(!rocketPoolInstalled)}</td>
|
||||
</tr>
|
||||
{/* This test is used for development purposes, so we can toggle different states easily */}
|
||||
{/* <tr>
|
||||
<td>Arbitrary test</td>
|
||||
<td>{resultToIcon(arbitraryTest)}</td>
|
||||
</tr> */}
|
||||
</tbody>
|
||||
</ResultsTable>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSystemReady = () => {
|
||||
return (
|
||||
<div>
|
||||
We are good to go. We will pick a random eth1 and eth2 client to install in order to promote client diversity.
|
||||
<br/>
|
||||
<br/>
|
||||
<Advanced onClick={toggleShowAdvanced}>Advanced</Advanced>
|
||||
{ // TODO: fix jump when this is clicked, or redisgn this alltogether
|
||||
// TODO: add pivoting dropdown arrow to signify menu opening and closing?
|
||||
showAdvanced ?
|
||||
<div>
|
||||
<br />
|
||||
<em>This is where we will allow users to pick their client, customize params, etc, however it is not yet implemented.</em>
|
||||
<br /><br />
|
||||
Come join the team and help out :)
|
||||
</div>
|
||||
:
|
||||
<div></div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderCantProceed = () => {
|
||||
return (
|
||||
<div>
|
||||
Unfortunately your system does not meet the requirements so we cannot proceed :(
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSystemCheckContent = () => {
|
||||
return (
|
||||
<Content>
|
||||
We did some system checks, here are the results:
|
||||
<br />
|
||||
<br />
|
||||
{ renderSystemCheckResultsTable() }
|
||||
<br />
|
||||
<br />
|
||||
{ systemStatus ? renderSystemReady() : renderCantProceed() }
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (rocketPoolInstalled) {
|
||||
return renderRocketPoolInstalledContent();
|
||||
} else {
|
||||
return renderSystemCheckContent();
|
||||
}
|
||||
}
|
||||
|
||||
const renderFooter = () => {
|
||||
if (systemStatus) {
|
||||
return (
|
||||
<Footer backLink={"/"} backLabel={"Back"} nextLink={"/installing"} nextLabel={"Install"} />
|
||||
)
|
||||
} else if (rocketPoolInstalled) {
|
||||
return (
|
||||
<Footer backLink={"/"} backLabel={"Back"} nextLink={"/status"} nextLabel={"Status"} />
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Footer backLink={"/"} backLabel={"Back"} nextLink={""} nextLabel={""} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<LandingHeader>System Check</LandingHeader>
|
||||
{renderContent()}
|
||||
{renderFooter()}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default SystemCheck;
|
||||
44
src/react/pages/SystemOverview.tsx
Normal file
44
src/react/pages/SystemOverview.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { FC, ReactElement } from "react";
|
||||
import { Grid, Typography } from '@mui/material';
|
||||
import styled from '@emotion/styled';
|
||||
import VersionFooter from "../components/VersionFooter";
|
||||
import { InstallDetails } from "../../electron/IMultiClientInstaller";
|
||||
|
||||
const MainGrid = styled(Grid)`
|
||||
width: 100%;
|
||||
margin: 0px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
type SystemOverviewProps = {
|
||||
installationDetails: InstallDetails
|
||||
}
|
||||
|
||||
const SystemOverview: FC<SystemOverviewProps> = (props): ReactElement => {
|
||||
return (
|
||||
<MainGrid container spacing={5} direction="column">
|
||||
<Grid item container>
|
||||
<Grid item xs={10} />
|
||||
<Grid item xs={2}>
|
||||
<Typography variant="caption" style={{ color: "gray" }}>
|
||||
Network: {props.installationDetails.network}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h1">
|
||||
{/* System Overview */}
|
||||
Installation Complete
|
||||
</Typography>
|
||||
<ul style={{ margin: '0 auto', width: '350px', marginTop: '3rem', textAlign: 'left'}}>
|
||||
<li>Network: {props.installationDetails.network}</li>
|
||||
<li>Consensus Client: {props.installationDetails.consensusClient}</li>
|
||||
<li>Execution Client: {props.installationDetails.executionClient}</li>
|
||||
</ul>
|
||||
</Grid>
|
||||
<VersionFooter />
|
||||
</MainGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemOverview;
|
||||
27
src/react/theme.ts
Normal file
27
src/react/theme.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import { amber, blue } from '@mui/material/colors';
|
||||
|
||||
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: "dark",
|
||||
primary: amber,
|
||||
secondary: blue,
|
||||
background: {
|
||||
default: '#1b262c',
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: [
|
||||
'Roboto',
|
||||
'sans-serif'
|
||||
].join(','),
|
||||
h1: {
|
||||
fontSize: "36px"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default theme;
|
||||
24
src/react/types.ts
Normal file
24
src/react/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export enum StepKey {
|
||||
SystemCheck,
|
||||
Configuration,
|
||||
Installing
|
||||
}
|
||||
|
||||
export enum StepSequenceKey {
|
||||
Install = "install"
|
||||
}
|
||||
|
||||
export enum Network {
|
||||
PRATER = "Prater",
|
||||
MAINNET = "Mainnet"
|
||||
}
|
||||
|
||||
export enum ExecutionNetwork {
|
||||
GOERLI = "goerli",
|
||||
MAINNET = "mainnet"
|
||||
}
|
||||
|
||||
export const networkToExecution: Map<Network, ExecutionNetwork> = new Map([
|
||||
[Network.PRATER, ExecutionNetwork.GOERLI],
|
||||
[Network.MAINNET, ExecutionNetwork.MAINNET]
|
||||
]);
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
zenity --password --title=Authentication
|
||||
BIN
static/icon.ico
Normal file
BIN
static/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
BIN
static/icon.png
Normal file
BIN
static/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -14,7 +14,7 @@
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||
"outDir": "./build", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
|
||||
@@ -1,25 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: './src/electron/index.ts',
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
path: path.resolve(__dirname, 'dist/electron')
|
||||
const { GitRevisionPlugin } = require('git-revision-webpack-plugin');
|
||||
const gitRevisionPlugin = new GitRevisionPlugin({
|
||||
branch: true,
|
||||
commithashCommand: 'rev-list --max-count=1 --no-merges --abbrev-commit HEAD',
|
||||
});
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
mode: 'development',
|
||||
entry: './src/electron/index.ts',
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
path: path.resolve(__dirname, 'build/electron')
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.tsx?$/, loader: 'ts-loader' }
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
gitRevisionPlugin,
|
||||
new webpack.DefinePlugin({
|
||||
VERSION: JSON.stringify(gitRevisionPlugin.version()),
|
||||
COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()),
|
||||
BRANCH: JSON.stringify(gitRevisionPlugin.branch()),
|
||||
LASTCOMMITDATETIME: JSON.stringify(gitRevisionPlugin.lastcommitdatetime()),
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js']
|
||||
},
|
||||
// tell webpack that we're building for electron
|
||||
target: 'electron-main',
|
||||
node: {
|
||||
// tell webpack that we actually want a working __dirname value
|
||||
// (ref: https://webpack.js.org/configuration/node/#node-__dirname)
|
||||
__dirname: false
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [{ test: /\.tsx?$/, loader: 'ts-loader' }]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js']
|
||||
},
|
||||
// tell webpack that we're building for electron
|
||||
target: 'electron-main',
|
||||
node: {
|
||||
// tell webpack that we actually want a working __dirname value
|
||||
// (ref: https://webpack.js.org/configuration/node/#node-__dirname)
|
||||
__dirname: false
|
||||
{
|
||||
mode: 'development',
|
||||
entry: './src/electron/preload.ts',
|
||||
output: {
|
||||
filename: 'preload.js',
|
||||
path: path.resolve(__dirname, 'build/electron')
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.tsx?$/, loader: 'ts-loader' }
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js']
|
||||
},
|
||||
target: 'electron-preload'
|
||||
}
|
||||
};
|
||||
];
|
||||
@@ -4,6 +4,13 @@
|
||||
// pull in the 'path' module from node
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const { GitRevisionPlugin } = require('git-revision-webpack-plugin');
|
||||
const gitRevisionPlugin = new GitRevisionPlugin({
|
||||
branch: true,
|
||||
commithashCommand: 'rev-list --max-count=1 --no-merges --abbrev-commit HEAD',
|
||||
});
|
||||
|
||||
// export the configuration as an object
|
||||
module.exports = {
|
||||
@@ -12,10 +19,10 @@ module.exports = {
|
||||
// the entry point is the top of the tree of modules.
|
||||
// webpack will bundle this file and everything it references.
|
||||
entry: './src/react/index.tsx',
|
||||
// we specify we want to put the bundled result in the matching dist/ folder
|
||||
// we specify we want to put the bundled result in the matching build/ folder
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
path: path.resolve(__dirname, 'dist/react'),
|
||||
path: path.resolve(__dirname, 'build/react'),
|
||||
},
|
||||
module: {
|
||||
// rules tell webpack how to handle certain types of files
|
||||
@@ -28,7 +35,13 @@ module.exports = {
|
||||
}, {
|
||||
test: /node_modules\/JSONStream\/index\.js$/,
|
||||
loader: 'shebang-loader'
|
||||
}
|
||||
}, {
|
||||
test: /\.css$/i,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
}, {
|
||||
test: /\.(woff|woff2|eot|ttf|svg)$/,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
@@ -40,7 +53,13 @@ module.exports = {
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/react/index.html',
|
||||
}),
|
||||
gitRevisionPlugin,
|
||||
new webpack.DefinePlugin({
|
||||
VERSION: JSON.stringify(gitRevisionPlugin.version()),
|
||||
COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()),
|
||||
BRANCH: JSON.stringify(gitRevisionPlugin.branch()),
|
||||
LASTCOMMITDATETIME: JSON.stringify(gitRevisionPlugin.lastcommitdatetime()),
|
||||
})
|
||||
],
|
||||
target: 'electron-renderer'
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user