63 Commits

Author SHA1 Message Date
Rémy Roy
9c68c0a3b2 Set version to 0.6.0 2022-06-10 16:01:57 -04:00
Rémy Roy
359f79b560 Merge pull request #91 from stake-house/feature/config-page
Add Configuration and Install pages
2022-06-10 13:51:25 -06:00
Stefan
e38a7ec2a6 add lodestar consensus client 2022-06-10 21:39:47 +02:00
Stefan
44f0406b50 add besu and erigon 2022-06-10 21:37:57 +02:00
Stefan
d71eb42be2 remove recommend 2022-06-10 21:36:32 +02:00
Stefan
98cd2751e4 remove unnecessary logging 2022-06-10 21:33:25 +02:00
Stefan
e1c50a6f05 remote unused imports and dangling comments 2022-06-10 21:24:40 +02:00
Stefan
b391000f50 import directroy & improve error handling ui 2022-06-07 20:14:45 +02:00
Stefan
d512bd7d39 add buffer and failed state 2022-06-04 00:51:52 +02:00
Stefan
48c7a52f88 Merge remote-tracking branch 'remy/backend-streaming' into feature/config-page 2022-05-31 22:31:46 +02:00
Rémy Roy
c11f801bba Initial backend streaming for error and messages 2022-05-31 16:27:59 -04:00
Stefan
66a547af8f keystore modal 2022-05-31 22:26:47 +02:00
Stefan
d522adb697 add dockerfile for testing 2022-05-31 20:23:01 +02:00
Stefan
2dd4597de8 Install animation flow 2022-05-30 21:01:10 +02:00
Stefan
045da87f80 add initial install page 2022-05-30 18:52:23 +02:00
Stefan
12e67c2a62 update networkpicker 2022-05-30 18:52:06 +02:00
Stefan
cd88d67e14 add advanced options 2022-05-30 18:51:59 +02:00
Stefan
2c13ec4e0f Merge remote-tracking branch 'origin/main' into feature/config-page 2022-05-30 18:49:47 +02:00
Rémy Roy
fc0458adfd Merge pull request #89 from remyroy/ethdocker-integration
Initial eth-docker integration
2022-05-13 08:35:22 -06:00
Stefan
4b90e3523d add advanced option text fields 2022-05-12 00:47:31 +02:00
Rémy Roy
aabe8336e2 Fix for the missing wallet password during key import with Prysm 2022-05-06 16:31:16 -04:00
Stefan
dc9dda928f add advanced options 2022-05-06 19:42:43 +02:00
Stefan
a69cc00779 installer: add client dropdowns 2022-05-04 21:32:31 +02:00
Rémy Roy
2252d3fba0 Merge pull request #88 from DakotaWeedon/welcomepage
Made main page/welcome page in-line with Figma mockup
2022-03-23 06:46:40 -06:00
Rémy Roy
ff61301dce Comment out backend usage 2022-03-22 10:25:57 -04:00
Rémy Roy
0dd573bf72 Add postInstall, startNodes and stopNodes implementation for eth-docker 2022-03-22 10:10:46 -04:00
Remy Roy
b052fdf9bb Initial implementation for import keys with eth-docker 2022-03-19 20:12:10 -04:00
Remy Roy
92aec97fb2 Fix calling command with docker group 2022-03-19 13:04:19 -06:00
Rémy Roy
41add19701 Build client initial implementation 2022-03-18 20:36:17 -04:00
Dakota
9f8043fe9e Made main page/welcome page in-line with Figma mockup 2022-03-18 17:16:47 -05:00
Rémy Roy
f642d95423 Adding missing keysDirectory 2022-03-18 16:37:21 -04:00
Rémy Roy
dbf3dca826 .env file creation implementation for eth-docker 2022-03-18 16:28:03 -04:00
Rémy Roy
7e220d21fd Reworked clone and pull branch in eth-docker install 2022-03-18 11:45:31 -04:00
Rémy Roy
efdee8d745 Initial install implementation for EthDocker 2022-03-18 11:43:21 -04:00
Rémy Roy
31ce474ac8 Full ethDocker preInstall implementation 2022-03-16 21:32:01 -04:00
Rémy Roy
1e5d45ad08 Initial implementation for EthDocker preInstall on Ubuntu 2022-03-16 15:21:35 -04:00
Rémy Roy
afbc0faebf Merge pull request #85 from remyroy/update-mui
Upgraded mui to v5. Upgraded electron, roboto and react-router-dom
2022-03-09 16:12:13 -05:00
Rémy Roy
4342302edf Removed deprecated file-loader package and use native webpack feature 2022-03-04 16:45:05 -05:00
Rémy Roy
cda841e514 Upgraded mui to v5. Upgraded electron, roboto and react-router-dom 2022-03-04 16:30:40 -05:00
Rémy Roy
65943961b6 Merge pull request #79 from remyroy/backend-interface
Initial backend interface with initial EthDocker template
2022-03-01 10:14:34 -05:00
Rémy Roy
afe25c38fb Reworked IMultiClientInstaller and moved into electron 2022-03-01 10:10:28 -05:00
colfax23
46e1e3dc28 Added scaffolding for the flow (#77) 2022-02-01 12:47:54 -05:00
Colfax Selby
d09083f02d Added installer interface and EthDockerInstaller class scaffold 2022-01-30 14:49:03 -05:00
superphiz
5dc792eb8b add a link to the download site (#74)
* add a link to the download site

* Update README.md

* Update README.md
2022-01-19 21:36:21 -05:00
Rémy Roy
b2e17ef176 Rebase on keygen code (#76)
* Initial rebase with Key Gen code

* Clean up Ken Gen code for installer

* Remove build folder in .gitignore

* Fixed gitignore

* Fix gitignore issue

* Use installer brand for icons
2022-01-19 21:35:40 -05:00
Mike Shaw
2db500f7e1 Update README.md (#72)
Updated the README to include staking resources requested in issue #44  I

I think this could be expanded on by having some type of long form post that explains Eth2, what Eth2 does and why it's important, where EthStaker/Stakehouse fit in. An overview in written form.
2021-08-30 23:54:38 -04:00
Jay Puntham-Baker
6fa91ec166 Merge pull request #70 from peebeejay/wagyu-55-readme-2
[WAGYU-55] Small update to README
2021-08-25 11:06:56 -04:00
Jay Puntham-Baker
370dd53579 add style guide link to README 2021-08-25 11:06:30 -04:00
Jay Puntham-Baker
041f3548b6 [WAGYU-38] System check page refactor (#67)
* update storybook config & mock certain modules

* split up and refactor system check page

* use new system check page

* add devtools

* fix minor styling issues

* fix rocketpool loading flash issue

* refactor installation status and remove render function
2021-08-25 07:35:42 -04:00
Jay Puntham-Baker
754d292872 add contribution guidelines (#69) 2021-08-25 07:15:44 -04:00
Jay Puntham-Baker
f236bca658 [WAGYU-56-59] Update directory structure, housekeeping, storybook, and more (#65)
* run formatter

* remove vscode settings from gitignore

* add storybook

* add polished

* directly import colors

* add common Header typography component

* fix footer export

* fix App import / export

* use new Header

* format main.ts
2021-08-13 15:58:43 -04:00
Jay Puntham-Baker
e221ab7432 [WAGYU-43] Status enum (#52)
* add vscode workspace dir to gitignore

* update prettierrc

* add types file for statuses

* remove unused import

* create utils file for Status

* update imports

* add new NodeStatus

* prettier formatting updates

* use new Status enum

* use Status enum in RocketPool

* move pages to components, run formatter, clean up imports, remove default export

* format footer

* format SystemCheck, clean up imports, consolidate a few render functions

* update imports in App
2021-08-10 19:16:14 -04:00
RomiRand
24683cd39f Start / Stop all (#51)
* Project setup for debugging

* [ADD] start/stop rocketpool service buttons

* [FIX] duplicate start/stop commands removed

* [CHANGE] unified button designs

* [FIX] Updating button on status updates
2021-07-09 10:35:22 -04:00
colfax23
52a7d8e0d7 Update README.md 2021-07-07 15:13:07 -04:00
colfax23
a6a9f16e1f Update README.md 2021-07-07 15:11:44 -04:00
colfax23
7e3c866cbf Update README.md 2021-04-20 11:13:22 -04:00
colfax23
c7e460d370 Update README.md 2021-04-20 10:43:08 -04:00
colfax23
53a93c31d9 Update README.md 2021-04-20 10:41:15 -04:00
colfax23
b51f3b36bf Update README.md 2021-04-20 10:38:14 -04:00
Colfax Selby
ad8033b2c7 New RP version 2021-04-20 10:19:15 -04:00
Colfax Selby
0ee5a5d05a Stakehouse -> Wagyu 2021-04-20 09:51:29 -04:00
colfax23
a1777f6ae5 Update README.md 2021-04-14 13:54:03 -04:00
colfax23
624fd51ec8 Cs/rp update (#46)
* Grap specific rocket pool release, not latest
2021-04-14 08:17:15 -04:00
49 changed files with 9072 additions and 5298 deletions

9
.gitignore vendored
View File

@@ -1,2 +1,11 @@
node_modules/
dist/
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
build/

View File

@@ -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
View 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

File diff suppressed because one or more lines are too long

2
.yarnrc.yml Normal file
View File

@@ -0,0 +1,2 @@
yarnPath: ".yarn/releases/yarn-berry.cjs"
nodeLinker: node-modules

42
Dockerfile Normal file
View 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

View File

@@ -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.
[![stakehouse preview](https://img.youtube.com/vi/-KKeZwI8EII/0.jpg)](https://www.youtube.com/watch?v=-KKeZwI8EII&ab_channel=ColfaxSelby)
[![wagyu preview](https://img.youtube.com/vi/-KKeZwI8EII/0.jpg)](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.

View File

@@ -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
View 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
};

View 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;
}
}

View 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"
}

View File

@@ -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
View 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
View 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

View File

@@ -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>
);
};

View File

@@ -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";

View File

@@ -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
};

View File

@@ -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,
};

View File

@@ -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,
}

View File

@@ -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;

View 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>
)
}

View 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;

View 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;

View 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;

View 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>
)
}

View 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;

View 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;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

63
src/react/constants.ts Normal file
View 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'
},
]

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>
&nbsp;|&nbsp;
<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;

View File

@@ -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;

View File

@@ -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);

View 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;

View File

@@ -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;

View File

@@ -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;

View 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
View 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
View 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]
]);

View File

@@ -1,2 +0,0 @@
#!/bin/bash
zenity --password --title=Authentication

BIN
static/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
static/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -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 */

View File

@@ -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'
}
};
];

View File

@@ -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'
};

9304
yarn.lock

File diff suppressed because it is too large Load Diff