├── .nvmrc ├── src ├── assets │ ├── .gitkeep │ └── images │ │ ├── dots.png │ │ ├── launchpad.png │ │ ├── sidebar-eth.png │ │ ├── initialize │ │ └── PrysmStripe.png │ │ ├── ethereum.svg │ │ └── skeletons │ │ ├── info_box.svg │ │ └── chart.svg ├── _redirects ├── app │ ├── modules │ │ ├── wallet │ │ │ ├── pages │ │ │ │ ├── account-backup │ │ │ │ │ ├── account-backup.component.scss │ │ │ │ │ └── account-backup.component.html │ │ │ │ ├── account-voluntary-exit │ │ │ │ │ └── account-voluntary-exit.component.scss │ │ │ │ ├── import │ │ │ │ │ └── import.component.html │ │ │ │ ├── wallet-details │ │ │ │ │ ├── wallet-details.component.html │ │ │ │ │ └── wallet-details.component.ts │ │ │ │ └── accounts │ │ │ │ │ └── accounts.component.html │ │ │ ├── components │ │ │ │ ├── account-delete │ │ │ │ │ └── account-delete.component.scss │ │ │ │ ├── accounts-form-selection │ │ │ │ │ ├── accounts-form-selection.component.scss │ │ │ │ │ ├── accounts-form-selection.component.html │ │ │ │ │ └── accounts-form-selection.component.spec.ts │ │ │ │ ├── wallet-help │ │ │ │ │ ├── wallet-help.component.html │ │ │ │ │ ├── wallet-help.component.spec.ts │ │ │ │ │ └── wallet-help.component.ts │ │ │ │ ├── icon-trigger-select │ │ │ │ │ ├── icon-trigger-select.component.html │ │ │ │ │ ├── icon-trigger-select.component.ts │ │ │ │ │ └── icon-trigger-select.component.spec.ts │ │ │ │ ├── account-selections │ │ │ │ │ ├── account-selections.component.html │ │ │ │ │ ├── account-selections.component.ts │ │ │ │ │ └── account-selections.component.spec.ts │ │ │ │ ├── files-and-directories │ │ │ │ │ ├── files-and-directories.component.ts │ │ │ │ │ ├── files-and-directories.component.spec.ts │ │ │ │ │ └── files-and-directories.component.html │ │ │ │ ├── wallet-kind │ │ │ │ │ ├── wallet-kind.component.html │ │ │ │ │ ├── wallet-kind.component.spec.ts │ │ │ │ │ └── wallet-kind.component.ts │ │ │ │ ├── accounts-table │ │ │ │ │ └── accounts-table.component.spec.ts │ │ │ │ └── account-actions │ │ │ │ │ ├── account-actions.component.ts │ │ │ │ │ ├── account-actions.component.spec.ts │ │ │ │ │ └── account-actions.component.html │ │ │ └── wallet.component.ts │ │ ├── shared │ │ │ ├── components │ │ │ │ ├── import-dropzone │ │ │ │ │ ├── import-dropzone.component.scss │ │ │ │ │ ├── import-dropzone.component.spec.ts │ │ │ │ │ └── import-dropzone.component.html │ │ │ │ ├── import-protection │ │ │ │ │ ├── import-protection.component.scss │ │ │ │ │ ├── model │ │ │ │ │ │ ├── interface.ts │ │ │ │ │ │ └── interface 2.ts │ │ │ │ │ ├── import-protection.component.spec.ts │ │ │ │ │ └── import-protection.component.html │ │ │ │ ├── base.component.ts │ │ │ │ ├── create-accounts-form │ │ │ │ │ ├── create-accounts-form.component.ts │ │ │ │ │ └── create-accounts-form.component.html │ │ │ │ ├── breadcrumb │ │ │ │ │ ├── breadcrumb.component.ts │ │ │ │ │ └── breadcrumb.component.html │ │ │ │ ├── password-form │ │ │ │ │ └── password-form.component.ts │ │ │ │ └── import-accounts-form │ │ │ │ │ └── import-accounts-form.component.spec.ts │ │ │ ├── types │ │ │ │ ├── select-list-item.ts │ │ │ │ └── user.ts │ │ │ ├── services │ │ │ │ ├── enums.ts │ │ │ │ ├── extensions.ts │ │ │ │ ├── user.service.spec.ts │ │ │ │ ├── utility.service.spec.ts │ │ │ │ ├── notification.service.spec.ts │ │ │ │ ├── notification.service.ts │ │ │ │ ├── utility.service.ts │ │ │ │ ├── breadcrumb.service.ts │ │ │ │ └── user.service.ts │ │ │ ├── pipes │ │ │ │ ├── balance.pipe.ts │ │ │ │ ├── pretty-json.pipe.ts │ │ │ │ ├── format-slot.pipe.ts │ │ │ │ ├── format-slot.pipe.spec.ts │ │ │ │ ├── format-epoch.pipe.ts │ │ │ │ ├── ordinal.pipe.ts │ │ │ │ ├── filename.pipe.spec.ts │ │ │ │ ├── ordinal.pipe.spec.ts │ │ │ │ └── filename.pipe.ts │ │ │ ├── loading │ │ │ │ ├── loading.component.html │ │ │ │ ├── loading.component.spec.ts │ │ │ │ └── loading.component.ts │ │ │ └── directives │ │ │ │ └── external-link.directive.ts │ │ ├── onboarding │ │ │ ├── pages │ │ │ │ └── wallet-recover-wizard │ │ │ │ │ ├── wallet-recover-wizard.component.scss │ │ │ │ │ └── templates │ │ │ │ │ └── mnemonic-form │ │ │ │ │ ├── mnemonic-form.component.scss │ │ │ │ │ └── mnemonic-form.component.spec.ts │ │ │ ├── types │ │ │ │ └── wallet.ts │ │ │ ├── components │ │ │ │ ├── generate-mnemonic │ │ │ │ │ ├── generate-mnemonic.component.ts │ │ │ │ │ ├── generate-mnemonic.component.html │ │ │ │ │ └── generate-mnemonic.component.spec.ts │ │ │ │ ├── wallet-directory-form │ │ │ │ │ ├── wallet-directory-form.component.ts │ │ │ │ │ ├── wallet-directory-form.component.html │ │ │ │ │ └── wallet-directory-form.component.spec.ts │ │ │ │ ├── choose-wallet-kind │ │ │ │ │ ├── choose-wallet-kind.component.ts │ │ │ │ │ └── choose-wallet-kind.component.html │ │ │ │ └── confirm-mnemonic │ │ │ │ │ ├── confirm-mnemonic.component.ts │ │ │ │ │ └── confirm-mnemonic.component.html │ │ │ ├── directives │ │ │ │ └── block-copy-paste.directive.ts │ │ │ ├── onboarding.component.html │ │ │ ├── validators │ │ │ │ └── utility.validator.ts │ │ │ └── onboarding.component.spec.ts │ │ ├── system-process │ │ │ ├── components │ │ │ │ ├── pie-chart │ │ │ │ │ ├── pie-chart.component.html │ │ │ │ │ └── pie-chart.component.ts │ │ │ │ ├── balances-chart │ │ │ │ │ └── balances-chart.component.html │ │ │ │ ├── double-bar-chart │ │ │ │ │ ├── double-bar-chart.component.html │ │ │ │ │ └── double-bar-chart.component.ts │ │ │ │ ├── proposed-missed-chart │ │ │ │ │ └── proposed-missed-chart.component.html │ │ │ │ └── logs-stream │ │ │ │ │ ├── logs-stream.component.html │ │ │ │ │ ├── logs-stream.component.spec.ts │ │ │ │ │ └── logs-stream.component.ts │ │ │ ├── pages │ │ │ │ ├── peer-locations-map │ │ │ │ │ ├── peer-locations-map.component.html │ │ │ │ │ └── peer-locations-map.component.spec.ts │ │ │ │ ├── metrics │ │ │ │ │ ├── metrics.component.ts │ │ │ │ │ └── metrics.component.spec.ts │ │ │ │ └── logs │ │ │ │ │ ├── logs.component.spec.ts │ │ │ │ │ └── logs.component.html │ │ │ └── system-process.module.ts │ │ ├── core │ │ │ ├── components │ │ │ │ ├── global-dialog │ │ │ │ │ ├── model │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ ├── global-dialog.service.ts │ │ │ │ │ ├── global-dialog.component.ts │ │ │ │ │ └── global-dialog.component.html │ │ │ │ └── loading-overlay │ │ │ │ │ └── loading-overlay.component.html │ │ │ ├── utils │ │ │ │ ├── intersect.ts │ │ │ │ ├── range.ts │ │ │ │ ├── deep-freeze.ts │ │ │ │ ├── simple-store.ts │ │ │ │ ├── range.spec.ts │ │ │ │ ├── select$.ts │ │ │ │ ├── select$.spec.ts │ │ │ │ ├── intersect.spec.ts │ │ │ │ └── simple-store.spec.ts │ │ │ ├── services │ │ │ │ ├── environmenter.service.ts │ │ │ │ ├── events.service.ts │ │ │ │ ├── events.service.spec.ts │ │ │ │ ├── logs.service.spec.ts │ │ │ │ ├── beacon-node.service.spec.ts │ │ │ │ ├── geo-locator.service.spec.ts │ │ │ │ ├── geo-locator.service.ts │ │ │ │ ├── logs.service.ts │ │ │ │ └── validator.service.spec.ts │ │ │ ├── constants.ts │ │ │ ├── interceptors │ │ │ │ ├── mock.interceptor.spec.ts │ │ │ │ └── jwt.interceptor.ts │ │ │ ├── core.module.ts │ │ │ └── validators │ │ │ │ └── password.validator.ts │ │ ├── dashboard │ │ │ ├── components │ │ │ │ ├── latest-gist-feature │ │ │ │ │ ├── templates │ │ │ │ │ │ └── git-info │ │ │ │ │ │ │ ├── git-info.component.scss │ │ │ │ │ │ │ ├── git-info.component.spec.ts │ │ │ │ │ │ │ ├── git-info.component.ts │ │ │ │ │ │ │ └── git-info.component.html │ │ │ │ │ ├── latest-gist-feature.component.scss │ │ │ │ │ ├── latest-gist-feature.component.spec.ts │ │ │ │ │ ├── latest-gist-feature.component.ts │ │ │ │ │ └── latest-gist-feature.component.html │ │ │ │ ├── version │ │ │ │ │ ├── version.component.html │ │ │ │ │ ├── version.component.ts │ │ │ │ │ └── version.component.spec.ts │ │ │ │ ├── sidebar │ │ │ │ │ ├── sidebar.component.ts │ │ │ │ │ └── sidebar.component.spec.ts │ │ │ │ ├── sidebar-expandable-link │ │ │ │ │ ├── sidebar-expandable-link.component.ts │ │ │ │ │ ├── sidebar-expandable-link.component.spec.ts │ │ │ │ │ └── sidebar-expandable-link.component.html │ │ │ │ └── beacon-node-status │ │ │ │ │ └── beacon-node-status.component.ts │ │ │ ├── types │ │ │ │ ├── sidebar-link.ts │ │ │ │ └── git-response.ts │ │ │ ├── pages │ │ │ │ └── gains-and-losses │ │ │ │ │ ├── gains-and-losses.component.ts │ │ │ │ │ └── gains-and-losses.component.html │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.spec.ts │ │ │ └── dashboard.module.ts │ │ └── auth │ │ │ ├── error_pages │ │ │ ├── notfound.component.ts │ │ │ └── notfound.component.html │ │ │ ├── auth.module.ts │ │ │ ├── services │ │ │ ├── authentication.service.spec.ts │ │ │ └── authentication.service.ts │ │ │ ├── guards │ │ │ └── auth.guard.ts │ │ │ └── initialize │ │ │ └── initialize.component.ts │ ├── proto │ │ └── validator │ │ │ └── accounts │ │ │ └── v2 │ │ │ └── web_api_keymanager-api.ts │ ├── app.component.spec.ts │ ├── app.component.html │ ├── app.module.ts │ └── app.component.ts ├── styles │ ├── layouts │ │ ├── _footer.scss │ │ ├── _index.scss │ │ ├── _layout.scss │ │ └── _table.scss │ ├── tailwind.scss │ ├── views │ │ ├── _notfound.scss │ │ ├── _peer-locations-map.scss │ │ ├── _index.scss │ │ ├── _security.scss │ │ ├── _wallet.scss │ │ └── _sessions.scss │ ├── utilities │ │ ├── _functions.scss │ │ ├── _shadow.scss │ │ ├── _utilities.scss │ │ ├── _common.scss │ │ └── _animations.scss │ ├── components │ │ ├── _index.scss │ │ ├── _loader.scss │ │ ├── _loading.component.scss │ │ └── _pulsating.scss │ ├── _mixins.scss │ └── app.scss ├── favicon.ico ├── environments │ ├── environment.prod.ts │ ├── environment.staging.ts │ ├── token.ts │ └── environment.ts ├── main.ts ├── index.html └── test.ts ├── .dockerignore ├── e2e ├── src │ ├── gains │ │ ├── gains.e2e-specs.ts │ │ └── gains.po.ts │ ├── app.po.ts │ └── app.e2e-spec.ts ├── tsconfig.json └── protractor.conf.js ├── SECURITY.md ├── scripts ├── update-ts-pbs.sh └── common.sh ├── .editorconfig ├── CHANGELOG.md ├── tsconfig.app.json ├── tsconfig.spec.json ├── Dockerfile ├── tailwind.config.js ├── tsconfig.json ├── ng-tailwind.js ├── .gitignore ├── .browserslistrc ├── .eslintrc.json ├── .github ├── workflows │ └── node.js.yml └── PULL_REQUEST_TEMPLATE.md ├── third_party └── leaflet │ └── leaflet.ts └── karma.conf.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | -------------------------------------------------------------------------------- /src/app/modules/wallet/pages/account-backup/account-backup.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/account-delete/account-delete.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/import-dropzone/import-dropzone.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/layouts/_footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | min-height: $topbar-height; 3 | } -------------------------------------------------------------------------------- /e2e/src/gains/gains.e2e-specs.ts: -------------------------------------------------------------------------------- 1 | import {GainsPage} from './gains.po'; 2 | 3 | // TODO 4 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/pages/wallet-recover-wizard/wallet-recover-wizard.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OffchainLabs/prysm-web-ui/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/styles/tailwind.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; -------------------------------------------------------------------------------- /src/styles/views/_notfound.scss: -------------------------------------------------------------------------------- 1 | .notfound { 2 | margin: 0 auto; 3 | text-align: center; 4 | } -------------------------------------------------------------------------------- /src/app/modules/onboarding/pages/wallet-recover-wizard/templates/mnemonic-form/mnemonic-form.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OffchainLabs/prysm-web-ui/HEAD/src/assets/images/dots.png -------------------------------------------------------------------------------- /src/app/modules/system-process/components/pie-chart/pie-chart.component.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/styles/layouts/_index.scss: -------------------------------------------------------------------------------- 1 | @import "layout"; 2 | @import "sidenav"; 3 | @import "footer"; 4 | @import "table"; 5 | -------------------------------------------------------------------------------- /src/assets/images/launchpad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OffchainLabs/prysm-web-ui/HEAD/src/assets/images/launchpad.png -------------------------------------------------------------------------------- /src/styles/utilities/_functions.scss: -------------------------------------------------------------------------------- 1 | @function bezier() { 2 | @return cubic-bezier(0.17, 0.67, 0.83, 0.67); 3 | } 4 | -------------------------------------------------------------------------------- /src/app/modules/system-process/components/balances-chart/balances-chart.component.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/assets/images/sidebar-eth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OffchainLabs/prysm-web-ui/HEAD/src/assets/images/sidebar-eth.png -------------------------------------------------------------------------------- /src/app/modules/shared/types/select-list-item.ts: -------------------------------------------------------------------------------- 1 | export interface ISelectListItem { 2 | text: string; 3 | value: any; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/modules/system-process/components/double-bar-chart/double-bar-chart.component.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/app/modules/system-process/pages/peer-locations-map/peer-locations-map.component.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/app/modules/system-process/components/proposed-missed-chart/proposed-missed-chart.component.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/app/modules/wallet/components/accounts-form-selection/accounts-form-selection.component.scss: -------------------------------------------------------------------------------- 1 | .example-viewport { 2 | height: 300px; 3 | } -------------------------------------------------------------------------------- /src/app/modules/wallet/pages/account-voluntary-exit/account-voluntary-exit.component.scss: -------------------------------------------------------------------------------- 1 | .example-viewport { 2 | height: 300px; 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/utilities/_shadow.scss: -------------------------------------------------------------------------------- 1 | @for $i from 0 through 24 { 2 | .elevation-z#{$i} { 3 | box-shadow: var(--elevation-z#{$i}); 4 | } 5 | } -------------------------------------------------------------------------------- /src/assets/images/initialize/PrysmStripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OffchainLabs/prysm-web-ui/HEAD/src/assets/images/initialize/PrysmStripe.png -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Please see the [Prysm Security Policy](https://github.com/prysmaticlabs/prysm/security/policy) for more information. 4 | -------------------------------------------------------------------------------- /src/styles/components/_index.scss: -------------------------------------------------------------------------------- 1 | @import "./loader.scss"; 2 | @import "./loading.component.scss"; 3 | @import "./pulsating.scss"; 4 | @import "./shapes.scss"; 5 | -------------------------------------------------------------------------------- /src/styles/utilities/_utilities.scss: -------------------------------------------------------------------------------- 1 | @import "../mixins"; 2 | @import "./functions"; 3 | @import "./animations"; 4 | @import "./shadow"; 5 | @import "./common"; 6 | -------------------------------------------------------------------------------- /src/styles/views/_peer-locations-map.scss: -------------------------------------------------------------------------------- 1 | #peer-locations-map { 2 | position: absolute; 3 | top: 0; 4 | height: calc(100% - 64px); 5 | right: 0; 6 | left: 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/modules/core/components/global-dialog/model/types.ts: -------------------------------------------------------------------------------- 1 | export enum DialogContentAlertType { 2 | ERROR = 'ERROR', 3 | WARNING = 'WARNING', 4 | INFO = 'INFO' 5 | } 6 | -------------------------------------------------------------------------------- /scripts/update-ts-pbs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . $(dirname "$0")/common.sh 3 | 4 | protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/app/proto -Ithird_party -I$1 $2 -------------------------------------------------------------------------------- /src/app/modules/core/utils/intersect.ts: -------------------------------------------------------------------------------- 1 | export default function intersect(a: Set, b: Set): Set { 2 | return new Set( 3 | [...a].filter(x => b.has(x)), 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/latest-gist-feature/templates/git-info/git-info.component.scss: -------------------------------------------------------------------------------- 1 | .pre-wrap { 2 | white-space: pre-line; 3 | word-break: break-all; 4 | ; 5 | } -------------------------------------------------------------------------------- /src/app/modules/shared/services/enums.ts: -------------------------------------------------------------------------------- 1 | export enum FileStatus { 2 | default = 1, 3 | validating = 10, 4 | validated = 20, 5 | uploading = 30, 6 | error = 40, 7 | uploaded = 50, 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/views/_index.scss: -------------------------------------------------------------------------------- 1 | @import "./sessions"; 2 | @import "./dashboard"; 3 | @import "./onboarding"; 4 | @import "./peer-locations-map"; 5 | @import "./security"; 6 | @import "./wallet"; 7 | @import "./notfound"; 8 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/import-protection/import-protection.component.scss: -------------------------------------------------------------------------------- 1 | .mr-2 { 2 | margin-right: .5rem; 3 | } 4 | .overlay{ 5 | position: absolute; 6 | height: 100%; 7 | width: 100%; 8 | opacity: .5; 9 | } -------------------------------------------------------------------------------- /src/app/modules/dashboard/types/sidebar-link.ts: -------------------------------------------------------------------------------- 1 | export default interface SidebarLink { 2 | name: string; 3 | icon: string; 4 | path?: string; 5 | externalUrl?: string; 6 | comingSoon?: boolean; 7 | children?: SidebarLink[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/version/version.component.html: -------------------------------------------------------------------------------- 1 |
2 |
Beacon: {{data.beacon}}
3 |
Validator: {{data.validator}}
4 |
-------------------------------------------------------------------------------- /src/app/modules/shared/types/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | constructor(init?: Partial) { 3 | Object.assign(this, init); 4 | } 5 | acountsPerPage = 5; 6 | gainAndLosesPageSize = 5; 7 | pageSizeOptions: number[] = [5, 10, 50, 100, 250]; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/latest-gist-feature/latest-gist-feature.component.scss: -------------------------------------------------------------------------------- 1 | .mb-0 { 2 | margin-bottom: 0 !important; 3 | } 4 | 5 | .align-itens-end { 6 | align-items: flex-end; 7 | } 8 | 9 | .mat-card-header-text { 10 | margin: 0 !important; 11 | } -------------------------------------------------------------------------------- /src/app/modules/auth/error_pages/notfound.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-notfound', 5 | templateUrl: 'notfound.component.html' 6 | }) 7 | 8 | export class NotFoundComponent { 9 | constructor() { } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/modules/core/utils/range.ts: -------------------------------------------------------------------------------- 1 | export default function range(start: number, end: number): number[] { 2 | if (end < start) { 3 | throw new Error('Upper limit cannot be smaller than lower limit'); 4 | } 5 | return Array(end - start).fill(null).map((_, idx) => start + idx); 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/views/_security.scss: -------------------------------------------------------------------------------- 1 | .security { 2 | position: relative; 3 | .mat-card:not(.bg-primary) { 4 | background-color: $paper; 5 | } 6 | .mat-card.bg-primary { 7 | background-color: $primary; 8 | } 9 | .mat-form-field { 10 | width: 100% !important; 11 | } 12 | } -------------------------------------------------------------------------------- /src/app/modules/onboarding/types/wallet.ts: -------------------------------------------------------------------------------- 1 | export enum WalletKind { 2 | Imported, 3 | Derived, 4 | Remote, 5 | } 6 | 7 | export interface WalletSelection { 8 | kind: WalletKind; 9 | name: string; 10 | description: string; 11 | image: string; 12 | comingSoon?: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/modules/wallet/wallet.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | @Component({ 3 | selector: 'app-wallet-component', 4 | styles: [''], 5 | template: ` `, 6 | }) 7 | export class WalletComponent { 8 | constructor() {} 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import { IEnvironment } from './token'; 2 | 3 | // please update this configuration if api version upgrades 4 | export const environment: IEnvironment = { 5 | production: true, 6 | validatorEndpoint: '/api/v2/validator', 7 | keymanagerEndpoint: '/eth/v1', 8 | }; 9 | -------------------------------------------------------------------------------- /src/environments/environment.staging.ts: -------------------------------------------------------------------------------- 1 | import { IEnvironment } from './token'; 2 | 3 | export const environment: IEnvironment = { 4 | production: false, 5 | validatorEndpoint: 'http://127.0.0.1:7500/api/v2/validator', 6 | keymanagerEndpoint: 'http://127.0.0.1:7500/eth/v1', 7 | mockInterceptor: false 8 | }; -------------------------------------------------------------------------------- /src/app/modules/system-process/pages/metrics/metrics.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-metrics', 5 | templateUrl: './metrics.component.html', 6 | }) 7 | export class MetricsComponent { 8 | 9 | constructor() { } 10 | 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/pages/gains-and-losses/gains-and-losses.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-gains-and-losses', 5 | templateUrl: './gains-and-losses.component.html', 6 | }) 7 | export class GainsAndLossesComponent { 8 | constructor() { } 9 | } 10 | -------------------------------------------------------------------------------- /src/environments/token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export interface IEnvironment { 4 | production: boolean; 5 | validatorEndpoint: string; 6 | keymanagerEndpoint: string; 7 | mockInterceptor?: boolean; 8 | } 9 | 10 | export const ENVIRONMENT = new InjectionToken('ENVIRONMENT'); 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import SidebarLink from '../../types/sidebar-link'; 3 | 4 | @Component({ 5 | selector: 'app-sidebar', 6 | templateUrl: './sidebar.component.html', 7 | }) 8 | export class SidebarComponent { 9 | @Input() links: SidebarLink[] | null = null; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/modules/shared/pipes/balance.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | /* 4 | * Formats a validator balance, showing n/a if 0. 5 | */ 6 | @Pipe({name: 'balance'}) 7 | export class BalancePipe implements PipeTransform { 8 | transform(n: string): string { 9 | if (n === '0') { 10 | return 'n/a'; 11 | } 12 | return n; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/modules/auth/error_pages/notfound.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

404 - Page not found

4 |

oops... we can't find the page you were looking for.

5 | arrow_backGo back to home 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/app/modules/shared/pipes/pretty-json.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'prettyjson' 5 | }) 6 | export class PrettyjsonPipe implements PipeTransform { 7 | transform(value: any, ...args: any[]): any { 8 | return JSON.stringify(value, null, 2) 9 | .replace(/ /g, ' ') 10 | .replace(/\n/g, '
'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/modules/shared/services/extensions.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Date { 3 | toDateTimeString(this: Date): string; 4 | } 5 | } 6 | 7 | Date.prototype.toDateTimeString = function(): string { 8 | const d = this; 9 | return `${d.getMonth()}-${d.getDate()}-${d.getFullYear()}-${d.getHours()}-${d.getMinutes()}-${d.getSeconds()}`; 10 | }; 11 | 12 | export class extensions {} 13 | -------------------------------------------------------------------------------- /src/styles/utilities/_common.scss: -------------------------------------------------------------------------------- 1 | .bg-dotted { 2 | background: url("/assets/images/dots.png"), 3 | linear-gradient(90deg, #7467ef -19.83%, #ada5f6 189.85%); 4 | background-repeat: no-repeat; 5 | background-size: 100%; 6 | } 7 | 8 | .min-w-sm { 9 | min-width: 110px; 10 | } 11 | 12 | .truncate { 13 | white-space: nowrap; 14 | overflow: hidden; 15 | text-overflow: ellipsis; 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin media($width) { 2 | @media screen and (max-width: $width) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin keyframeMaker($name) { 8 | @keyframes #{$name} { 9 | @content; 10 | } 11 | @-webkit-keyframes #{$name} { 12 | @content; 13 | } 14 | @-o-keyframes #{$name} { 15 | @content; 16 | } 17 | @-moz-keyframes #{$name} { 18 | @content; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## What's Changed 2 | 3 | **WEB UI IS STILL DEPRECATED AND WILL BE FROZEN UNTIL NEXT STEPS ARE DETERMINED** 4 | 5 | - updates to fix vulnerabilities 6 | - removal of hex to base64 as APIs update from https://github.com/prysmaticlabs/prysm-web-ui/pull/249, depends on https://github.com/prysmaticlabs/prysm/pull/13191 7 | 8 | **Full Changelog**: https://github.com/prysmaticlabs/prysm-web-ui/compare/v2.0.4...v2.0.5 -------------------------------------------------------------------------------- /src/styles/utilities/_animations.scss: -------------------------------------------------------------------------------- 1 | .fade-in { 2 | @include keyframeMaker(fade-in) { 3 | from { 4 | opacity: 0; 5 | } 6 | to { 7 | opacity: 1; 8 | } 9 | } 10 | animation: fade-in 1s #{bezier()}; 11 | } 12 | 13 | @keyframes spin { 14 | 0% {transform: rotate(0)} 15 | 100% {transform: rotate(360deg)} 16 | } 17 | 18 | .spin { 19 | animation: spin 3s infinite linear; 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "baseUrl": "./", 7 | "types": ["node"], 8 | "strict": true 9 | }, 10 | "files": [ 11 | "src/main.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/app/modules/core/services/environmenter.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { ENVIRONMENT, IEnvironment } from '../../../../environments/token'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class EnvironmenterService { 8 | constructor( 9 | @Inject(ENVIRONMENT) private environment: IEnvironment, 10 | ) {} 11 | public readonly env = this.environment; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/wallet-help/wallet-help.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{item.title}} 6 | 7 | 8 |

9 |

10 |
11 |
12 | -------------------------------------------------------------------------------- /src/app/modules/core/components/loading-overlay/loading-overlay.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Uploading files... 4 |
5 |
6 | Hang in there while we upload your files... 7 |
8 |
9 | 10 |
11 |
-------------------------------------------------------------------------------- /src/app/modules/core/services/events.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Event, NavigationEnd } from '@angular/router'; 3 | import { BehaviorSubject } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class EventsService { 9 | routeChanged$ = new BehaviorSubject({} as ActivatedRouteSnapshot); 10 | constructor() { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/modules/core/utils/deep-freeze.ts: -------------------------------------------------------------------------------- 1 | export type primitive = string | number | boolean | undefined | null; 2 | export type DeepReadonly = 3 | T extends primitive ? T : 4 | T extends Array ? DeepReadonlyArray : 5 | DeepReadonlyObject; 6 | 7 | export interface DeepReadonlyArray extends ReadonlyArray> {} 8 | 9 | export type DeepReadonlyObject = { 10 | readonly [P in keyof T]: DeepReadonly 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/base.component.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnDestroy } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | @Injectable({ providedIn: 'root' }) 5 | export class BaseComponent implements OnDestroy { 6 | destroyed$ = new Subject(); 7 | 8 | ngOnDestroy(): void { 9 | this.destroyed$.next(); 10 | this.destroyed$.complete(); 11 | } 12 | 13 | back(): void { 14 | history.back(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/modules/shared/services/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserService } from './user.service'; 4 | 5 | describe('UserService', () => { 6 | let service: UserService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UserService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "strict": true, 6 | "outDir": "./out-tsc/spec", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test.ts", 14 | "src/polyfills.ts" 15 | ], 16 | "include": [ 17 | "src/**/*.spec.ts", 18 | "src/**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/app/modules/core/services/events.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { EventsService } from './events.service'; 4 | 5 | describe('EventsService', () => { 6 | let service: EventsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(EventsService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/modules/shared/services/utility.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UtilityService } from './utility.service'; 4 | 5 | describe('UtilityService', () => { 6 | let service: UtilityService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UtilityService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | # set working directory 4 | WORKDIR /app 5 | ENV PORT 8080 6 | EXPOSE $PORT 7 | 8 | # install and cache app dependencies 9 | COPY package.json . 10 | COPY package-lock.json . 11 | RUN npm install 12 | 13 | # add `/app/node_modules/.bin` to $PATH 14 | ENV PATH /app/node_modules/.bin:$PATH 15 | 16 | # Build 17 | COPY . /app 18 | RUN npm run build:prod 19 | 20 | # Run on $PORT 21 | CMD ng serve --host 0.0.0.0 --port $PORT --disableHostCheck 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | import './app/modules/shared/services/extensions'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic() 13 | .bootstrapModule(AppModule) 14 | .catch((err) => console.error(err)); 15 | -------------------------------------------------------------------------------- /src/app/modules/system-process/components/logs-stream/logs-stream.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
{{title}}
4 |
{{url}}
5 |
6 |
7 |
12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/version/version.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { ValidatorService } from 'src/app/modules/core/services/validator.service'; 4 | 5 | @Component({ 6 | selector: 'app-version', 7 | templateUrl: './version.component.html', 8 | }) 9 | export class VersionComponent { 10 | constructor( 11 | private validatorService: ValidatorService, 12 | ) { } 13 | 14 | version$ = this.validatorService.version$; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/modules/shared/pipes/format-slot.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | /** 4 | * Formats a slot, such that negative values are interpreted to 'awaiting genesis,' 5 | * and 0 to 'genesis' 6 | */ 7 | @Pipe({name: 'slot'}) 8 | export class SlotPipe implements PipeTransform { 9 | transform(n: number): string { 10 | if (!n && n !== 0) { 11 | return 'n/a'; 12 | } 13 | if (n < 0) { 14 | return 'awaiting genesis'; 15 | } 16 | return n.toString(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /e2e/src/gains/gains.po.ts: -------------------------------------------------------------------------------- 1 | import {browser, by, element} from 'protractor'; 2 | 3 | /** 4 | * This represents the Gains and Losses Page POM 5 | */ 6 | export class GainsPage { 7 | 8 | private static readonly PAGE_RELATIVE_URL = 'dashboard/gains-and-losses'; 9 | private static readonly PAGE_TITLE = 'PrysmWebUi'; 10 | 11 | static get relativeUrl(): string { 12 | return GainsPage.PAGE_RELATIVE_URL; 13 | } 14 | 15 | static get completeUrl(): string { 16 | return `${browser.baseUrl}${GainsPage.PAGE_RELATIVE_URL}`; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/sidebar-expandable-link/sidebar-expandable-link.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import SidebarLink from '../../types/sidebar-link'; 3 | 4 | @Component({ 5 | selector: 'app-sidebar-expandable-link', 6 | templateUrl: './sidebar-expandable-link.component.html', 7 | }) 8 | export class SidebarExpandableLinkComponent { 9 | @Input() link: SidebarLink | null = null; 10 | collapsed = true; 11 | 12 | toggleCollapsed(): void { 13 | this.collapsed = !this.collapsed; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/components/generate-mnemonic/generate-mnemonic.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { WalletService } from 'src/app/modules/core/services/wallet.service'; 4 | 5 | @Component({ 6 | selector: 'app-generate-mnemonic', 7 | templateUrl: './generate-mnemonic.component.html', 8 | }) 9 | export class GenerateMnemonicComponent { 10 | mnemonic$: Observable = this.walletService.generateMnemonic$; 11 | constructor(private walletService: WalletService) { } 12 | } 13 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | theme: { 4 | colors: { 5 | primary: '#7467ef', 6 | secondary: '#ff9e43', 7 | error: '#e95455', 8 | default: '#1a2038', 9 | paper: '#222A45', 10 | paperlight: '#30345b', 11 | muted: 'rgba(255, 255, 255, 0.7)', 12 | hint: 'rgba(255, 255, 255, 0.5)', 13 | white: '#fff', 14 | }, 15 | extend: { 16 | colors: { 17 | success: 'rgba(51, 217, 178, 1)', 18 | } 19 | }, 20 | }, 21 | variants: {}, 22 | plugins: [], 23 | } 24 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/icon-trigger-select/icon-trigger-select.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/app/modules/core/utils/simple-store.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs'; 2 | 3 | export class Store extends BehaviorSubject { 4 | constructor(initialData: T) { 5 | super(initialData); 6 | } 7 | 8 | next(newData: T): void { 9 | const frozenData: T = newData; 10 | if (!naiveObjectComparison(frozenData, this.getValue())) { 11 | super.next(frozenData); 12 | } 13 | } 14 | } 15 | 16 | export function naiveObjectComparison(objOne: T, objTwo: R): boolean { 17 | return JSON.stringify(objOne) === JSON.stringify(objTwo); 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "strict": true, 6 | "baseUrl": "./", 7 | "outDir": "./dist/out-tsc", 8 | "sourceMap": true, 9 | "declaration": false, 10 | "downlevelIteration": true, 11 | "experimentalDecorators": true, 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "target": "es2018", 15 | "module": "esnext", 16 | "lib": [ 17 | "es2018", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ng-tailwind.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Tailwind Paths 3 | configJS: 'tailwind.config.js', 4 | sourceCSS: 'src/styles/tailwind.scss', 5 | outputCSS: 'src/styles/tailwind-generated.scss', 6 | watchRelatedFiles: [], 7 | // Sass 8 | sass: true, 9 | // PurgeCSS Settings 10 | purge: false, 11 | keyframes: false, 12 | fontFace: false, 13 | rejected: false, 14 | whitelist: [], 15 | whitelistPatterns: [], 16 | whitelistPatternsChildren: [], 17 | extensions: [ 18 | '.ts', 19 | '.html', 20 | '.js' 21 | ], 22 | extractors: [], 23 | content: [] 24 | } 25 | -------------------------------------------------------------------------------- /src/app/modules/core/components/global-dialog/model/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse } from '@angular/common/http'; 2 | import { DialogContentAlertType } from './types'; 3 | 4 | export interface DialogConfigMessage { 5 | payload: DialogConfig; 6 | } 7 | 8 | export interface DialogConfig { 9 | title: string; 10 | content: string; 11 | alert?: DialogContentAlert; 12 | } 13 | 14 | export interface DialogContentAlert { 15 | type: DialogContentAlertType; 16 | title: string; 17 | description: string; 18 | message: string | Error | HttpErrorResponse; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/modules/shared/services/notification.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { NotificationService } from './notification.service'; 4 | import { SharedModule } from '../shared.module'; 5 | 6 | describe('NotificationService', () => { 7 | let service: NotificationService; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [SharedModule], 12 | }); 13 | service = TestBed.inject(NotificationService); 14 | }); 15 | 16 | it('should be created', () => { 17 | expect(service).toBeTruthy(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/directives/block-copy-paste.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, HostListener } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appBlockCopyPaste]' 5 | }) 6 | export class BlockCopyPasteDirective { 7 | constructor() { } 8 | 9 | @HostListener('paste', ['$event']) blockPaste(e: KeyboardEvent): void { 10 | e.preventDefault(); 11 | } 12 | 13 | @HostListener('copy', ['$event']) blockCopy(e: KeyboardEvent): void { 14 | e.preventDefault(); 15 | } 16 | 17 | @HostListener('cut', ['$event']) blockCut(e: KeyboardEvent): void { 18 | e.preventDefault(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/modules/shared/pipes/format-slot.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { SlotPipe } from './format-slot.pipe'; 2 | 3 | describe('SlotPipe', () => { 4 | it('properly transforms a positive slot number to its string representation', () => { 5 | const pipe = new SlotPipe(); 6 | const original = 1; 7 | expect(pipe.transform(original)).toEqual('1'); 8 | }); 9 | it('properly transforms a negative slot number to its string representation \'awaiting genesis\'', () => { 10 | const pipe = new SlotPipe(); 11 | const original = -1; 12 | expect(pipe.transform(original)).toEqual('awaiting genesis'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/components/generate-mnemonic/generate-mnemonic.component.html: -------------------------------------------------------------------------------- 1 |
2 | Creating an HD Wallet 3 |
4 |
5 | {{mnemonic$ | async}} 6 |
7 |
8 | Write down the above mnemonic offline, and keep it secret! It is the only way you can recover your wallet if you lose it and anyone who gains access to it will be able to steal all your keys. 9 |
-------------------------------------------------------------------------------- /src/app/modules/shared/pipes/format-epoch.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { FAR_FUTURE_EPOCH } from 'src/app/modules/core/constants'; 3 | 4 | /* 5 | * Formats an epoch, such as 0 to 'genesis' and FAR_FUTURE_EPOCH to 'n/a' 6 | */ 7 | @Pipe({name: 'epoch'}) 8 | export class EpochPipe implements PipeTransform { 9 | transform(n: number): string { 10 | if (!n) { 11 | return 'n/a'; 12 | } 13 | if (n === 0) { 14 | return 'genesis'; 15 | } else if (n.toString() === FAR_FUTURE_EPOCH) { 16 | return 'n/a'; 17 | } 18 | return n.toString(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/modules/core/utils/range.spec.ts: -------------------------------------------------------------------------------- 1 | import range from './range'; 2 | 3 | describe('range', () => { 4 | it('should create range of numbers upper limit exclusive', () => { 5 | const arr = range(0, 2); 6 | expect(arr.length).toEqual(2); 7 | expect(arr).toEqual([0, 1]); 8 | }); 9 | 10 | it('should create an empty array if range is 0, 0', () => { 11 | const arr = range(0, 0); 12 | expect(arr.length).toEqual(0); 13 | }); 14 | 15 | it('should throw error if end < start', () => { 16 | const badCall = () => { 17 | range(0, -100); 18 | }; 19 | expect(badCall).toThrowError(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/components/wallet-directory-form/wallet-directory-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'app-wallet-directory-form', 6 | templateUrl: './wallet-directory-form.component.html', 7 | }) 8 | export class WalletDirectoryFormComponent { 9 | @Input() formGroup: FormGroup | null = null; 10 | linux = '$HOME/.eth2validators/prysm-wallet-v2'; 11 | macos = '$HOME/Library/Eth2Validators/prysm-wallet-v2'; 12 | windows = '%LOCALAPPDATA%\\Eth2Validators\\prysm-wallet-v2'; 13 | constructor() { } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/views/_wallet.scss: -------------------------------------------------------------------------------- 1 | .search-bar { 2 | width: 700px; 3 | } 4 | 5 | .mat-raised-button.large-btn { 6 | margin-top: -20px; 7 | padding: 8px 16px; 8 | font-size: 16px; 9 | } 10 | 11 | .table-container { 12 | min-height: 300px; 13 | } 14 | 15 | .table-loading-shade { 16 | height: 100%; 17 | min-height: 300px; 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | right: 0; 22 | background: rgba(0, 0, 0, 0.15); 23 | z-index: 1; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | } 28 | 29 | @media (max-width: 640px) { 30 | .search-bar { 31 | width: 100%; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/modules/shared/pipes/ordinal.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | const ordinals: string[] = ['th', 'st', 'nd', 'rd']; 4 | 5 | /* 6 | * Append ordinal to number (e.g. '1st' position) 7 | * Usage: 8 | * value | ordinal 9 | * Example: 10 | * {{ 23 | ordinal}} 11 | * formats to: '23rd' 12 | * Example: 13 | * {{ 23 | ordinal:false}} 14 | * formats to: 'rd' 15 | */ 16 | @Pipe({name: 'ordinal'}) 17 | export class OrdinalPipe implements PipeTransform { 18 | transform(n: number): string { 19 | const v = n % 100; 20 | return n + (ordinals[(v - 20) % 10] || ordinals[v] || ordinals[0]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/modules/shared/pipes/filename.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { FileNamePipe } from "./filename.pipe"; 2 | 3 | describe('FileNamePipe', () => { 4 | it('properly truncates a filename when setting max length to 22', () => { 5 | const pipe = new FileNamePipe(); 6 | const original = 'keystore-m_12381_3600_0_0_0-1636471397.json'; 7 | expect(pipe.transform(original,22)).toEqual('keystor...6471397.json'); 8 | }); 9 | 10 | it('properly returns a filename if it is too short', () => { 11 | const pipe = new FileNamePipe(); 12 | const original = 'key.json'; 13 | expect(pipe.transform(original,22)).toEqual('key.json'); 14 | }); 15 | }); -------------------------------------------------------------------------------- /src/app/modules/core/constants.ts: -------------------------------------------------------------------------------- 1 | export const GWEI_PER_ETHER = 1000000000; 2 | export const FAR_FUTURE_EPOCH = '18446744073709551615'; 3 | export const MILLISECONDS_PER_SLOT = 12000; 4 | export const SLOTS_PER_EPOCH = 32; 5 | export const MILLISECONDS_PER_EPOCH = SLOTS_PER_EPOCH * MILLISECONDS_PER_SLOT; 6 | export const SECONDS_PER_EPOCH = MILLISECONDS_PER_EPOCH / 1000; 7 | export const DIALOG_WIDTH = '600px'; 8 | export const BEACONCHAIN_EXPLORER = 'https://beaconcha.in'; 9 | export const WS_RECONNECT_INTERVAL = 3000; 10 | export const GEO_COORDINATES_API = 'http://ip-api.com/batch'; 11 | 12 | export const LANDING_URL = 'dashboard'; 13 | export const ONBOARDING_URL = 'onboarding'; 14 | -------------------------------------------------------------------------------- /src/app/modules/shared/pipes/ordinal.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { OrdinalPipe } from './ordinal.pipe'; 2 | 3 | describe('OrdinalPipe', () => { 4 | it('transforms unique cases such as 1, 2, and 3 into ordinal form', () => { 5 | const pipe = new OrdinalPipe(); 6 | expect(pipe.transform(1)).toEqual('1st'); 7 | expect(pipe.transform(2)).toEqual('2nd'); 8 | expect(pipe.transform(3)).toEqual('3rd'); 9 | }); 10 | it('transforms other numbers into ordinal form', () => { 11 | const pipe = new OrdinalPipe(); 12 | expect(pipe.transform(20)).toEqual('20th'); 13 | expect(pipe.transform(25)).toEqual('25th'); 14 | expect(pipe.transform(110)).toEqual('110th'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PrysmWebUi 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/styles/layouts/_layout.scss: -------------------------------------------------------------------------------- 1 | .layout-full { 2 | .container { 3 | padding-left: 30px; 4 | padding-right: 30px; 5 | } 6 | } 7 | 8 | .layout-contained, .layout-boxed { 9 | .container { 10 | padding-left: 30px; 11 | padding-right: 30px; 12 | } 13 | } 14 | 15 | 16 | .layout-contained { 17 | .container { 18 | max-width: 1000px; 19 | margin: auto; 20 | width: 100%; 21 | @include media(767px) { 22 | max-width: 100%; 23 | } 24 | } 25 | } 26 | 27 | .layout-boxed { 28 | max-width: 1000px; 29 | margin: auto; 30 | box-shadow: $elevation-z12; 31 | background: $white; 32 | @include media(767px) { 33 | max-width: 100%; 34 | box-shadow: none; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/icon-trigger-select/icon-trigger-select.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { TableData } from '../accounts-table/accounts-table.component'; 3 | 4 | export interface MenuItem { 5 | disabled?: boolean; 6 | danger?: boolean; 7 | name: string; 8 | icon: string; 9 | action: (row: TableData) => void; 10 | } 11 | 12 | @Component({ 13 | selector: 'app-icon-trigger-select', 14 | templateUrl: './icon-trigger-select.component.html', 15 | }) 16 | export class IconTriggerSelectComponent { 17 | @Input() data: TableData | null = null; 18 | @Input() icon: string | null = null; 19 | @Input() menuItems: MenuItem[] | null = null; 20 | constructor() { } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/onboarding.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /src/app/proto/validator/accounts/v2/web_api_keymanager-api.ts: -------------------------------------------------------------------------------- 1 | /* Keymanager API standard */ 2 | 3 | 4 | export interface DeleteAccountsRequest { 5 | /** 6 | * Public keys to delete. 7 | */ 8 | pubkeys: string[]; 9 | } 10 | 11 | export interface DeleteAccountsResponse { 12 | data: DeleteAccountsData[], 13 | slashing_protection: string 14 | } 15 | 16 | export interface DeleteAccountsData { 17 | status: string, 18 | message: string 19 | } 20 | 21 | export interface ListFeeRecipientResponse { 22 | data: FeeRecipientData 23 | } 24 | 25 | export interface FeeRecipientData { 26 | pubkey: string, 27 | ethaddress: string 28 | } 29 | 30 | export interface SetFeeRecipientRequest { 31 | ethaddress: string 32 | } -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('prysm-web-ui app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/create-accounts-form/create-accounts-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'app-create-accounts-form', 6 | templateUrl: './create-accounts-form.component.html', 7 | }) 8 | export class CreateAccountsFormComponent { 9 | @Input() formGroup: FormGroup | null = null; 10 | @Input() title = 'Create new validator accounts'; 11 | @Input() subtitle = `Generate new accounts in your wallet and obtain the deposit 12 | data you need to activate them into the deposit contract via the 13 | eth2 launchpad`; 14 | constructor() { } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/components/choose-wallet-kind/choose-wallet-kind.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { WalletKind, WalletSelection } from '../../types/wallet'; 3 | import { Subject } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'app-choose-wallet-kind', 7 | templateUrl: './choose-wallet-kind.component.html', 8 | }) 9 | export class ChooseWalletKindComponent { 10 | @Input() walletSelections: WalletSelection[] | null = null; 11 | @Input() selectedWallet$: Subject | null = null; 12 | selectedCard = 1; // We select card with index 1 as the default. 13 | constructor() { } 14 | 15 | // Update the currently selected UI card. 16 | selectCard(index: number): void { 17 | this.selectedCard = index; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | company-logo 8 |
9 |
10 | 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/modules/shared/loading/loading.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
{{getMessage()}}
4 | error 5 | loading background 6 |
7 | 8 |
9 |
10 |
11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/components/wallet-directory-form/wallet-directory-form.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Pick a Wallet Directory 4 |
5 |
6 | Enter the directory where you want to store your wallet. By default, a wallet will be created at {{linux}} for Linux machines, {{macos}} for MacOS, or {{windows}} for windows computers, leave blank to use the default value. 7 |
8 | 9 | Enter desired wallet directory 10 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/account-selections/account-selections.component.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/beacon-node-status/beacon-node-status.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { BeaconNodeService } from 'src/app/modules/core/services/beacon-node.service'; 3 | 4 | @Component({ 5 | selector: 'app-beacon-node-status', 6 | templateUrl: './beacon-node-status.component.html', 7 | styles: [ 8 | ] 9 | }) 10 | export class BeaconNodeStatusComponent { 11 | constructor( 12 | private beaconNodeService: BeaconNodeService, 13 | ) { } 14 | endpoint$ = this.beaconNodeService.nodeEndpoint$; 15 | connected$ = this.beaconNodeService.connected$; 16 | syncing$ = this.beaconNodeService.syncing$; 17 | chainHead$ = this.beaconNodeService.chainHead$; 18 | latestClockSlotPoll$ = this.beaconNodeService.latestClockSlotPoll$; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/modules/shared/loading/loading.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { LoadingComponent } from './loading.component'; 4 | 5 | describe('LoadingComponent', () => { 6 | let component: LoadingComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoadingComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoadingComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/latest-gist-feature/templates/git-info/git-info.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { GitInfoComponent } from './git-info.component'; 4 | 5 | describe('GitInfoComponent', () => { 6 | let component: GitInfoComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ GitInfoComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(GitInfoComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/latest-gist-feature/templates/git-info/git-info.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input} from '@angular/core'; 2 | import { GitResponse } from '../../../../types/git-response'; 3 | 4 | @Component({ 5 | selector: 'app-git-info', 6 | templateUrl: './git-info.component.html', 7 | styleUrls: ['./git-info.component.scss'], 8 | }) 9 | export class GitInfoComponent { 10 | descriptionLength = 300; 11 | displayReadMore = false; 12 | 13 | private gitInfo: GitResponse | undefined; 14 | @Input() 15 | get GitInfo(): GitResponse | undefined { 16 | return this.gitInfo; 17 | } 18 | 19 | set GitInfo(val: GitResponse | undefined) { 20 | this.gitInfo = val; 21 | if (val) { 22 | this.displayReadMore = val.body.length > this.descriptionLength; 23 | } 24 | } 25 | constructor() {} 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/breadcrumb/breadcrumb.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BreadcrumbService } from '../../services/breadcrumb.service'; 3 | import { ActivatedRouteSnapshot } from '@angular/router'; 4 | import { switchMap } from 'rxjs/operators'; 5 | import { EventsService } from 'src/app/modules/core/services/events.service'; 6 | 7 | @Component({ 8 | selector: 'app-breadcrumb', 9 | templateUrl: './breadcrumb.component.html', 10 | }) 11 | export class BreadcrumbComponent { 12 | constructor( 13 | private breadcrumbService: BreadcrumbService, 14 | private eventsService: EventsService, 15 | ) { 16 | } 17 | breadcrumbs$ = this.eventsService.routeChanged$.pipe( 18 | switchMap((route: ActivatedRouteSnapshot) => 19 | this.breadcrumbService.create(route) 20 | ), 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/password-form/password-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | import { PasswordValidator } from 'src/app/modules/core/validators/password.validator'; 4 | 5 | @Component({ 6 | selector: 'app-password-form', 7 | templateUrl: './password-form.component.html', 8 | }) 9 | export class PasswordFormComponent { 10 | passwordValidator = new PasswordValidator(); 11 | @Input() title: string | null = null; 12 | @Input() subtitle: string | null = null; 13 | @Input() label: string | null = null; 14 | @Input() confirmationLabel: string | null = null; 15 | @Input() formGroup: FormGroup | null = null; 16 | @Input() showSubmitButton: boolean | null = null; 17 | @Input() submit: () => void = () => {}; 18 | constructor( 19 | ) { } 20 | } 21 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/latest-gist-feature/templates/git-info/git-info.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Version
4 |
{{GitInfo.name}}
5 |
6 |
7 |
Release date
8 |
{{GitInfo.published_at|date}}
9 |
10 |
11 |
Description
12 |
{{GitInfo.body|slice:0:400}}
13 |
14 | 20 |
-------------------------------------------------------------------------------- /src/app/modules/shared/components/import-dropzone/import-dropzone.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { ImportDropzoneComponent } from './import-dropzone.component'; 4 | 5 | describe('ImportDropzoneComponent', () => { 6 | let component: ImportDropzoneComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ImportDropzoneComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ImportDropzoneComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | debug.log 43 | /typings 44 | /.angular 45 | 46 | # System Files 47 | .DS_Store 48 | Thumbs.db 49 | /.vs 50 | -------------------------------------------------------------------------------- /src/app/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | 6 | import { SharedModule } from '../../modules/shared/shared.module'; 7 | import { InitializeComponent } from './initialize/initialize.component'; 8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 9 | import { NotFoundComponent } from './error_pages/notfound.component'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | InitializeComponent, 14 | NotFoundComponent 15 | ], 16 | imports: [ 17 | CommonModule, 18 | BrowserAnimationsModule, 19 | FormsModule, 20 | ReactiveFormsModule, 21 | RouterModule, 22 | SharedModule, 23 | ] 24 | }) 25 | export class AuthModule { } 26 | -------------------------------------------------------------------------------- /src/app/modules/core/services/logs.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { ENVIRONMENT } from 'src/environments/token'; 4 | 5 | import { LogsService } from './logs.service'; 6 | 7 | describe('LogsService', () => { 8 | let service: LogsService; 9 | 10 | beforeEach(() => { 11 | const envSpy = jasmine.createSpyObj('EnvironmenterService', ['env']); 12 | TestBed.configureTestingModule({ 13 | imports: [ 14 | HttpClientTestingModule 15 | ], 16 | providers: [ 17 | { provide: ENVIRONMENT, useValue: jasmine.createSpyObj('EnvironmenterService', ['env']) }, 18 | ] 19 | }); 20 | service = TestBed.inject(LogsService); 21 | }); 22 | 23 | it('should be created', () => { 24 | expect(service).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/import-protection/model/interface.ts: -------------------------------------------------------------------------------- 1 | // interface naming and format follows the format defined in the following location on the Prysm repo 2 | // validator/slashing-protection/local/standard-protection-format/format/format.go 3 | 4 | export interface EIPSlashingProtectionFormat { 5 | metadata: Metadata, 6 | data: ProtectionData[] 7 | } 8 | 9 | interface Metadata { 10 | interchange_format_version: string; 11 | genesis_validators_root: string; 12 | } 13 | 14 | interface ProtectionData { 15 | pubkey: string; 16 | signed_blocks: SignedBlock[]; 17 | signed_attestations: SignedAttestation[]; 18 | } 19 | 20 | interface SignedAttestation { 21 | source_epoch: string; 22 | target_epoch: string; 23 | signing_root?: string; 24 | } 25 | 26 | interface SignedBlock { 27 | slot: string; 28 | signing_root?: string; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/assets/images/ethereum.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/modules/core/services/beacon-node.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | 4 | import { BeaconNodeService } from './beacon-node.service'; 5 | import { EnvironmenterService } from './environmenter.service'; 6 | 7 | describe('BeaconNodeService', () => { 8 | const envSpy = jasmine.createSpyObj('EnvironmenterService', ['env']); 9 | let service: BeaconNodeService; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ 14 | HttpClientTestingModule, 15 | ], 16 | providers: [ 17 | { provide: EnvironmenterService, useValue: envSpy }, 18 | ] 19 | }); 20 | service = TestBed.inject(BeaconNodeService); 21 | }); 22 | 23 | it('should be created', () => { 24 | expect(service).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/modules/core/services/geo-locator.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { EnvironmenterService } from './environmenter.service'; 4 | 5 | import { GeoLocatorService } from './geo-locator.service'; 6 | 7 | describe('GeoLocatorService', () => { 8 | const envSpy = jasmine.createSpyObj('EnvironmenterService', ['env']); 9 | let service: GeoLocatorService; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ 14 | HttpClientTestingModule, 15 | ], 16 | providers: [ 17 | { provide: EnvironmenterService, useValue: envSpy }, 18 | ] 19 | }); 20 | service = TestBed.inject(GeoLocatorService); 21 | }); 22 | 23 | it('should be created', () => { 24 | expect(service).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/import-protection/model/interface 2.ts: -------------------------------------------------------------------------------- 1 | // interface naming and format follows the format defined in the following location on the Prysm repo 2 | // validator/slashing-protection/local/standard-protection-format/format/format.go 3 | 4 | export interface EIPSlashingProtectionFormat { 5 | metadata: Metadata, 6 | data: ProtectionData[] 7 | } 8 | 9 | interface Metadata { 10 | interchange_format_version: string; 11 | genesis_validators_root: string; 12 | } 13 | 14 | interface ProtectionData { 15 | pubkey: string; 16 | signed_blocks: SignedBlock[]; 17 | signed_attestations: SignedAttestation[]; 18 | } 19 | 20 | interface SignedAttestation { 21 | source_epoch: string; 22 | target_epoch: string; 23 | signing_root?: string; 24 | } 25 | 26 | interface SignedBlock { 27 | slot: string; 28 | signing_root?: string; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { ENVIRONMENT } from 'src/environments/token'; 4 | import { AppComponent } from './app.component'; 5 | 6 | describe('AppComponent', () => { 7 | beforeEach(waitForAsync(() => { 8 | TestBed.configureTestingModule({ 9 | imports: [ 10 | RouterTestingModule 11 | ], 12 | declarations: [ 13 | AppComponent 14 | ], 15 | providers: [ 16 | { provide: ENVIRONMENT, useValue: jasmine.createSpyObj('EnvironmenterService', ['env']) }, 17 | ] 18 | }).compileComponents(); 19 | })); 20 | 21 | it('should create the app', () => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | const app = fixture.componentInstance; 24 | expect(app).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /src/styles/layouts/_table.scss: -------------------------------------------------------------------------------- 1 | .table-container { 2 | overflow-x: auto; 3 | } 4 | 5 | table.mat-table { 6 | mat-checkbox { 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | 12 | .mat-column-correctlyVotedSource { 13 | max-width: 70px; 14 | } 15 | .mat-column-correctlyVotedTarget { 16 | max-width: 50px; 17 | } 18 | .mat-column-correctlyVotedHead { 19 | max-width: 50px; 20 | } 21 | .mat-column-gains{ 22 | max-width: 75px; 23 | } 24 | 25 | th.mat-header-cell:first-of-type, td.mat-cell:first-of-type, td.mat-footer-cell:first-of-type { 26 | padding-left: 12px; 27 | padding-right: 20px; 28 | } 29 | 30 | th .mat-header-cell, td.mat-cell, td.mat-footer-cel { 31 | max-width: 100px; 32 | padding-left: 2px; 33 | padding-right: 2px; 34 | 35 | @media screen and (max-width: 1000px) { 36 | max-width: 70px; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scripts/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function color() { 4 | # Usage: color "31;5" "string" 5 | # Some valid values for color: 6 | # - 5 blink, 1 strong, 4 underlined 7 | # - fg: 31 red, 32 green, 33 yellow, 34 blue, 35 purple, 36 cyan, 37 white 8 | # - bg: 40 black, 41 red, 44 blue, 45 purple 9 | printf '\033[%sm%s\033[0m\n' "$@" 10 | } 11 | 12 | system="" 13 | case "$OSTYPE" in 14 | darwin*) system="darwin" ;; 15 | linux*) system="linux" ;; 16 | msys*) system="windows" ;; 17 | cygwin*) system="windows" ;; 18 | *) exit 1 ;; 19 | esac 20 | readonly system 21 | 22 | findutil="find" 23 | # On OSX `find` is not GNU find compatible, so require "findutils" package. 24 | if [ "$system" == "darwin" ]; then 25 | if [[ ! -x "/usr/local/bin/gfind" ]]; then 26 | color 31 "Make sure that GNU 'findutils' package is installed: brew install findutils" 27 | exit 1 28 | else 29 | findutil="gfind" 30 | fi 31 | fi -------------------------------------------------------------------------------- /src/app/modules/wallet/components/files-and-directories/files-and-directories.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | import { WalletResponse } from 'src/app/proto/validator/accounts/v2/web_api'; 3 | 4 | @Component({ 5 | selector: 'app-files-and-directories', 6 | templateUrl: './files-and-directories.component.html', 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | }) 9 | export class FilesAndDirectoriesComponent { 10 | @Input() wallet: WalletResponse | null = null; 11 | constructor() { } 12 | walletDirTooltip = 'The directory on disk which your validator client uses to determine the location of your' + 13 | ' validating keys and accounts configuration'; 14 | keystoreTooltip = 'An EIP-2335 compliant, JSON file storing all your validating keys encrypted by a strong password'; 15 | encryptedSeedTooltip = 'An EIP-2335 compliant JSON file containing your encrypted wallet seed'; 16 | } 17 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | import { IEnvironment } from './token'; 5 | 6 | export const environment: IEnvironment = { 7 | production: false, 8 | validatorEndpoint: 'http://127.0.0.1:7500/api/v2/validator', 9 | keymanagerEndpoint: 'http://127.0.0.1:7500/eth/v1', 10 | mockInterceptor: true 11 | }; 12 | 13 | /* 14 | * For easier debugging in development mode, you can import the following file 15 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 16 | * 17 | * This import should be commented out in production mode because it will have a negative impact 18 | * on performance if an error is thrown. 19 | */ 20 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 21 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/breadcrumb/breadcrumb.component.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /src/styles/views/_sessions.scss: -------------------------------------------------------------------------------- 1 | .signup { 2 | @extend .bg-default; 3 | .signup-card { 4 | padding: 0; 5 | width: 600px; 6 | img { 7 | width: 160px; 8 | height: 120px; 9 | } 10 | .signup-form-container { 11 | background-color: rgba(0, 0, 0, 0.08); 12 | .mat-form-field { 13 | width: 100%; 14 | } 15 | .mat-button-disabled { 16 | @extend .text-hint; 17 | } 18 | .mat-form-field-appearance-outline .mat-form-field-outline { 19 | color: $primary; 20 | } 21 | .mat-form-field-label { 22 | color: $white; 23 | } 24 | } 25 | } 26 | } 27 | 28 | @media (max-width: 640px) { 29 | .signup { 30 | padding-left: 20px; 31 | padding-right: 20px; 32 | .signup-img { 33 | padding-left: 16px; 34 | padding-right: 16px; 35 | img { 36 | width: 100%; 37 | } 38 | } 39 | .signup-card { 40 | width: 100%; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/modules/shared/directives/external-link.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostBinding, Input, OnChanges } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: 'a[href]' 5 | }) 6 | export class ExternalLinkDirective implements OnChanges { 7 | 8 | constructor(private elementRef: ElementRef){} 9 | 10 | @HostBinding('attr.rel') relAttr: string | null = null; 11 | @HostBinding('attr.target') targetAttr: string | null = null; 12 | @Input() href: string | null = null; 13 | 14 | ngOnChanges(): void { 15 | 16 | this.elementRef.nativeElement.href = this.href; 17 | 18 | if (this.isLinkExternal()) { 19 | this.relAttr = 'noopener'; 20 | this.targetAttr = '_blank'; 21 | } else { 22 | this.relAttr = ''; 23 | this.targetAttr = ''; 24 | } 25 | } 26 | 27 | private isLinkExternal(): boolean { 28 | return !this.elementRef.nativeElement.hostname.includes(location.hostname); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/modules/core/utils/select$.ts: -------------------------------------------------------------------------------- 1 | import { naiveObjectComparison } from './simple-store'; 2 | import { Observable } from 'rxjs'; 3 | import { map, distinctUntilChanged, shareReplay } from 'rxjs/operators'; 4 | 5 | type MappingFunction = (mappable: T) => R; 6 | type MemoizationFunction = (previousResult: R, currentResult: R) => boolean; 7 | 8 | function defaultMemoization(previousValue: T, currentValue: T): boolean { 9 | if (typeof previousValue === 'object' && typeof currentValue === 'object') { 10 | return naiveObjectComparison(previousValue, currentValue); 11 | } 12 | return previousValue === currentValue; 13 | } 14 | 15 | export function select$( 16 | source$: Observable, 17 | mappingFunction: MappingFunction, 18 | memoizationFunction?: MemoizationFunction 19 | ): Observable { 20 | return source$.pipe( 21 | map(mappingFunction), 22 | distinctUntilChanged(memoizationFunction || defaultMemoization), 23 | shareReplay(1) 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/wallet-kind/wallet-kind.component.html: -------------------------------------------------------------------------------- 1 | 2 |
5 |
6 |

7 | YOUR WALLET KIND 8 |

9 |
10 | {{info[kind].name}} 11 |
12 |

13 | {{info[kind].description}} 14 |

15 | 22 |
23 |
24 | wallet 28 |
29 |
30 |
-------------------------------------------------------------------------------- /src/app/modules/dashboard/components/latest-gist-feature/latest-gist-feature.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { LatestGistFeatureComponent } from './latest-gist-feature.component'; 5 | 6 | 7 | describe('LatestGistFeatureComponent', () => { 8 | let component: LatestGistFeatureComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [LatestGistFeatureComponent], 14 | imports: [HttpClientTestingModule], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(LatestGistFeatureComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ 31 | spec: { 32 | displayStacktrace: StacktraceOption.PRETTY 33 | } 34 | })); 35 | } 36 | }; -------------------------------------------------------------------------------- /src/app/modules/wallet/components/account-selections/account-selections.component.ts: -------------------------------------------------------------------------------- 1 | import { SelectionModel } from '@angular/cdk/collections'; 2 | import { Component, Input } from '@angular/core'; 3 | import { MatDialog } from '@angular/material/dialog'; 4 | import { BEACONCHAIN_EXPLORER } from 'src/app/modules/core/constants'; 5 | 6 | import { TableData } from '../accounts-table/accounts-table.component'; 7 | 8 | @Component({ 9 | selector: 'app-account-selections', 10 | templateUrl: './account-selections.component.html', 11 | }) 12 | export class AccountSelectionsComponent { 13 | @Input() selection: SelectionModel | null = null; 14 | constructor( 15 | private dialog: MatDialog, 16 | ) { } 17 | 18 | openExplorer(): void { 19 | if (window !== undefined) { 20 | const indices = this.selection?.selected.map((d: TableData) => d.index).join(','); 21 | if (indices) { 22 | window.open(`${BEACONCHAIN_EXPLORER}/dashboard?validators=${indices}`, '_blank'); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/modules/core/interceptors/mock.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | 6 | import { MockInterceptor} from './mock.interceptor'; 7 | import { EnvironmenterService } from '../services/environmenter.service'; 8 | 9 | class MockEnv { 10 | env = { production: false }; 11 | } 12 | 13 | describe('MockInterceptor', () => { 14 | beforeEach(() => { 15 | TestBed.configureTestingModule({ 16 | imports: [ 17 | HttpClientTestingModule, 18 | RouterTestingModule, 19 | ], 20 | providers: [ 21 | { 22 | provide: HTTP_INTERCEPTORS, 23 | useClass: MockInterceptor, 24 | multi: true 25 | }, 26 | { 27 | provide: EnvironmenterService, useValue: new MockEnv(), 28 | }, 29 | ] 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/accounts-form-selection/accounts-form-selection.component.html: -------------------------------------------------------------------------------- 1 |
2 | Selected {{accountList.selectedOptions.selected.length}} Accounts 3 |
4 | 8 | 9 | Unselect all 10 | 11 | 12 | Select all 13 | 14 | 15 | 17 | 20 | 23 | {{item.validating_public_key|slice:0:16}}... 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/app/modules/system-process/components/logs-stream/logs-stream.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { SharedModule } from 'src/app/modules/shared/shared.module'; 4 | 5 | import { LogsStreamComponent } from './logs-stream.component'; 6 | 7 | describe('LogsStreamComponent', () => { 8 | let component: LogsStreamComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [LogsStreamComponent], 14 | imports: [BrowserAnimationsModule, SharedModule], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(LogsStreamComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/validators/utility.validator.ts: -------------------------------------------------------------------------------- 1 | import { ValidationErrors, AbstractControl, FormGroup } from '@angular/forms'; 2 | export abstract class UtilityValidator { 3 | static BiggerThanZero(ctrl: AbstractControl): ValidationErrors | null { 4 | if (!ctrl.value || !+ctrl.value) { 5 | return { BiggerThanZero: true }; 6 | } 7 | const val = +ctrl.value; 8 | if (val > 0) { 9 | return null; 10 | } 11 | return { BiggerThanZero: true }; 12 | } 13 | 14 | static MustBe(value: any): ValidationErrors | null { 15 | return (ctrl: AbstractControl) => { 16 | if (ctrl.value === value) { 17 | return null; 18 | } 19 | return { 20 | incorectValue: true, 21 | }; 22 | }; 23 | } 24 | 25 | static LengthMustBeBiggerThanOrEqual(length = 0): ValidationErrors | null { 26 | return (grp: FormGroup) => { 27 | if (Object.keys(grp.controls).length >= length) { 28 | return null; 29 | } 30 | return { 31 | mustSelectOne: true, 32 | }; 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/wallet-help/wallet-help.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { SharedModule } from 'src/app/modules/shared/shared.module'; 3 | 4 | import { WalletHelpComponent } from './wallet-help.component'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | describe('WalletHelpComponent', () => { 8 | let component: WalletHelpComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [ WalletHelpComponent ], 14 | imports: [ 15 | SharedModule, 16 | BrowserAnimationsModule 17 | ] 18 | }) 19 | .compileComponents(); 20 | })); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(WalletHelpComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/wallet-kind/wallet-kind.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { SharedModule } from 'src/app/modules/shared/shared.module'; 3 | 4 | import { WalletKindComponent } from './wallet-kind.component'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | describe('WalletKindComponent', () => { 8 | let component: WalletKindComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [ WalletKindComponent ], 14 | imports: [ 15 | SharedModule, 16 | BrowserAnimationsModule 17 | ] 18 | }) 19 | .compileComponents(); 20 | })); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(WalletKindComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/modules/core/utils/select$.spec.ts: -------------------------------------------------------------------------------- 1 | import { take } from 'rxjs/operators'; 2 | 3 | import { Store } from './simple-store'; 4 | import { select$ } from './select$'; 5 | 6 | describe('select$', () => { 7 | it('should share data across replays', (done) => { 8 | const item = { 9 | title: 'Foo', 10 | subtitle: 'Bar', 11 | }; 12 | const store = new Store(item); 13 | const title$ = select$( 14 | store, 15 | res => res.title, 16 | ); 17 | title$.pipe( 18 | take(1), 19 | ).subscribe(title => { 20 | expect(title).toEqual(item.title); 21 | done(); 22 | }); 23 | title$.pipe( 24 | take(1), 25 | ).subscribe(title => { 26 | expect(title).toEqual(item.title); 27 | done(); 28 | }); 29 | 30 | // We expect no events to fire in the super class (BehaviorSubject) 31 | // if the data remains the same. 32 | const superSpy = spyOn(Object.getPrototypeOf(Object.getPrototypeOf(store)), 'next'); 33 | store.next(item); 34 | expect(superSpy).toHaveBeenCalledTimes(0); 35 | store.complete(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/account-selections/account-selections.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { SharedModule } from 'src/app/modules/shared/shared.module'; 3 | 4 | import { AccountSelectionsComponent } from './account-selections.component'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | describe('AccountSelectionsComponent', () => { 8 | let component: AccountSelectionsComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [AccountSelectionsComponent], 14 | imports: [SharedModule, BrowserAnimationsModule], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(AccountSelectionsComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/modules/core/components/global-dialog/global-dialog.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatDialog } from '@angular/material/dialog'; 3 | import { GlobalDialogComponent } from './global-dialog.component'; 4 | import { DialogConfigMessage } from './model/interfaces'; 5 | @Injectable() 6 | export class GlobalDialogService { 7 | 8 | queue: DialogConfigMessage[] = []; 9 | constructor(public dialog: MatDialog) { 10 | this.dialog.afterAllClosed.subscribe(event => { 11 | if (this.queue.length){ 12 | this.dialog.open(GlobalDialogComponent, { 13 | data: this.queue.shift() 14 | }); 15 | } 16 | }); 17 | } 18 | 19 | open(message: DialogConfigMessage): void { 20 | if (!this.dialog.openDialogs || !this.dialog.openDialogs.length){ 21 | this.dialog.open(GlobalDialogComponent, { 22 | data: message 23 | }); 24 | } else { 25 | this.queue.push(message); 26 | } 27 | 28 | } 29 | 30 | close(): void { 31 | this.dialog.closeAll(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/create-accounts-form/create-accounts-form.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{title}} 4 |
5 |
6 |
7 | 8 | Number of accounts* 9 | 16 | 17 | Num accounts is required 18 | 19 | 20 | Must create at least 1 account(s) 21 | 22 | 23 | Max {{maxAccounts}} accounts allowed to create at the same time 24 | 25 | 26 |
27 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/latest-gist-feature/latest-gist-feature.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { finalize, map, tap } from 'rxjs/operators'; 4 | import { GitResponse } from '../../types/git-response'; 5 | import { Observable } from 'rxjs'; 6 | 7 | @Component({ 8 | selector: 'app-latest-gist-feature', 9 | templateUrl: './latest-gist-feature.component.html', 10 | styleUrls: ['./latest-gist-feature.component.scss'], 11 | }) 12 | export class LatestGistFeatureComponent implements OnInit { 13 | constructor(private httpClient: HttpClient) {} 14 | 15 | loading = false; 16 | gitResponse$: Observable | undefined; 17 | 18 | ngOnInit(): void { 19 | this.loading = true; 20 | this.gitResponse$ = this.httpClient.get( 21 | 'https://api.github.com/repos/prysmaticlabs/prysm/releases' 22 | ).pipe( 23 | map((val: any) => { 24 | return val[0]; 25 | }), 26 | finalize(() => { 27 | this.loading = false; 28 | }) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/accounts-table/accounts-table.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { SharedModule } from 'src/app/modules/shared/shared.module'; 3 | 4 | import { AccountsTableComponent } from './accounts-table.component'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | describe('AccountsTableComponent', () => { 8 | let component: AccountsTableComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [AccountsTableComponent], 14 | imports: [ 15 | SharedModule, 16 | BrowserAnimationsModule 17 | ] 18 | }) 19 | .compileComponents(); 20 | })); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(AccountsTableComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/icon-trigger-select/icon-trigger-select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { SharedModule } from 'src/app/modules/shared/shared.module'; 3 | 4 | import { IconTriggerSelectComponent } from './icon-trigger-select.component'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | describe('IconTriggerSelectComponent', () => { 8 | let component: IconTriggerSelectComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [IconTriggerSelectComponent], 14 | imports: [ 15 | SharedModule, 16 | BrowserAnimationsModule 17 | ] 18 | }) 19 | .compileComponents(); 20 | })); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(IconTriggerSelectComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | The Prysm UI is marked for DEPRECATION and will be FROZEN until next steps are determined. 4 | Existing features will continue to function as is, but newer features will not be added until a strategy is determined.
5 | Please check our web UI documentation for more details 6 |
7 |
8 |
9 |
10 | Warning! You are running the web UI in development mode, meaning it will show **fake** data for testing purposes. Do not run real validators this way. If you want to run the web UI with your real Prysm node and validator, follow our instructions here. 11 |
12 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /src/app/modules/system-process/components/pie-chart/pie-chart.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { mixinColor } from '@angular/material/core'; 3 | 4 | @Component({ 5 | selector: 'app-pie-chart', 6 | templateUrl: './pie-chart.component.html', 7 | }) 8 | export class PieChartComponent { 9 | options = { 10 | title: { 11 | text: 'Validator data breakdown', 12 | subtext: 'Some sample pie chart data', 13 | x: 'center', 14 | color: 'white' 15 | }, 16 | tooltip: { 17 | trigger: 'item', 18 | formatter: '{a}
{b} : {c} ({d}%)' 19 | }, 20 | legend: { 21 | x: 'center', 22 | y: 'bottom', 23 | data: ['item1', 'item2', 'item3', 'item4'] 24 | }, 25 | calculable: true, 26 | series: [ 27 | { 28 | name: 'area', 29 | type: 'pie', 30 | radius: [30, 110], 31 | roseType: 'area', 32 | data: [ 33 | { value: 10, name: 'item1' }, 34 | { value: 30, name: 'item2' }, 35 | { value: 45, name: 'item3' }, 36 | { value: 15, name: 'item4' }, 37 | ] 38 | } 39 | ] 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/files-and-directories/files-and-directories.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { SharedModule } from 'src/app/modules/shared/shared.module'; 3 | 4 | import { FilesAndDirectoriesComponent } from './files-and-directories.component'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | describe('FilesAndDirectoriesComponent', () => { 8 | let component: FilesAndDirectoriesComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [ FilesAndDirectoriesComponent ], 14 | imports: [ 15 | SharedModule, 16 | BrowserAnimationsModule 17 | ] 18 | }) 19 | .compileComponents(); 20 | })); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(FilesAndDirectoriesComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/latest-gist-feature/latest-gist-feature.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | Latest Software Updates 5 |
6 |
7 |
8 | 15 |
16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 |

24 | Please wait whie we retrieve the information 25 |

26 |
27 | 28 |
29 |
-------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json", 14 | "e2e/tsconfig.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "extends": [ 19 | "plugin:@angular-eslint/recommended", 20 | "plugin:@angular-eslint/template/process-inline-templates" 21 | ], 22 | "rules": { 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "prefix": "app", 27 | "style": "kebab-case", 28 | "type": "element" 29 | } 30 | ], 31 | "@angular-eslint/directive-selector": [ 32 | "error", 33 | { 34 | "style": "camelCase", 35 | "type": "attribute" 36 | } 37 | ] 38 | } 39 | }, 40 | { 41 | "files": [ 42 | "*.html" 43 | ], 44 | "extends": [ 45 | "plugin:@angular-eslint/template/recommended" 46 | ], 47 | "rules": {} 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /src/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import "./variables"; 2 | @import "./mixins"; 3 | @import "./utilities/utilities"; 4 | @import "./components/index"; 5 | @import "./layouts/index"; 6 | @import "./views/index"; 7 | @import "./tailwind-generated"; 8 | 9 | // override framework layout 10 | 11 | .override .mat-list-item { 12 | height: auto !important; 13 | } 14 | 15 | .override .mat-list-item .mat-list-item-content { 16 | padding:0px !important; 17 | } 18 | 19 | .override .mat-form-field { 20 | display: flex; 21 | } 22 | 23 | .override .mat-form-field .mat-form-field-wrapper { 24 | display: flex; 25 | flex: 1 1 0%; 26 | padding-bottom:0px !important; 27 | } 28 | 29 | .override .mat-button-toggle-group-appearance-standard{ 30 | box-shadow: none; 31 | border:0px !important; 32 | } 33 | 34 | .override mat-button-toggle{ 35 | background-color: #434190 !important; 36 | } 37 | 38 | .override mat-button-toggle:hover { 39 | background-color: #3c366b !important; 40 | } 41 | 42 | .override .mat-button-toggle-checked { 43 | background-color: #3c366b !important; 44 | } 45 | 46 | .override .mat-button-toggle-input { 47 | background-color: none !important; 48 | padding-left: 32px !important; 49 | } -------------------------------------------------------------------------------- /src/app/modules/wallet/pages/import/import.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | 7 |
8 |
9 | 10 |
11 |
12 |
13 | 20 |
21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/sidebar/sidebar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { SidebarComponent } from './sidebar.component'; 4 | import { SharedModule } from '../../../shared/shared.module'; 5 | import { SidebarExpandableLinkComponent } from '../sidebar-expandable-link/sidebar-expandable-link.component'; 6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7 | 8 | describe('SidebarComponent', () => { 9 | let component: SidebarComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(waitForAsync(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [ 15 | SharedModule, 16 | BrowserAnimationsModule 17 | ], 18 | declarations: [ 19 | SidebarExpandableLinkComponent, 20 | SidebarComponent, 21 | ] 22 | }) 23 | .compileComponents(); 24 | })); 25 | 26 | beforeEach(() => { 27 | fixture = TestBed.createComponent(SidebarComponent); 28 | component = fixture.componentInstance; 29 | fixture.detectChanges(); 30 | }); 31 | 32 | it('should create', () => { 33 | expect(component).toBeTruthy(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [14.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | 22 | - name: Cache node modules 23 | uses: actions/cache@v1 24 | with: 25 | path: ~/.npm 26 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 27 | restore-keys: | 28 | ${{ runner.os }}-node- 29 | 30 | - name: Node ${{ matrix.node-version }} 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | 35 | - name: npm ci and npm run build:dev 36 | run: | 37 | npm ci 38 | npm run build:ci 39 | 40 | - name: npm run lint 41 | run: | 42 | npm run lint 43 | -------------------------------------------------------------------------------- /src/app/modules/core/utils/intersect.spec.ts: -------------------------------------------------------------------------------- 1 | import intersect from './intersect'; 2 | 3 | describe('intersect', () => { 4 | it('intersect A(1) and B() should yield C()', () => { 5 | const a = new Set(); 6 | const b = new Set(); 7 | b.add(1); 8 | const expected = new Set(); 9 | expect(intersect(a, b)).toEqual(expected); 10 | }); 11 | it('intersect A(1) and B(1) should yield C(1)', () => { 12 | const a = new Set(); 13 | const b = new Set(); 14 | a.add(1); 15 | b.add(1); 16 | const expected = new Set(); 17 | expected.add(1); 18 | expect(intersect(a, b)).toEqual(expected); 19 | }); 20 | it('intersect A(1, 2) and B(3, 4) should yield C()', () => { 21 | const a = new Set(); 22 | const b = new Set(); 23 | a.add(1); 24 | a.add(2); 25 | b.add(3); 26 | b.add(4); 27 | const expected = new Set(); 28 | expect(intersect(a, b)).toEqual(expected); 29 | }); 30 | it('intersect A(1, 2) and B(2, 2) should yield C(2)', () => { 31 | const a = new Set(); 32 | const b = new Set(); 33 | a.add(1); 34 | a.add(2); 35 | b.add(2); 36 | b.add(3); 37 | const expected = new Set(); 38 | expected.add(2); 39 | expect(intersect(a, b)).toEqual(expected); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/components/confirm-mnemonic/confirm-mnemonic.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, NgZone, ViewChild, OnDestroy } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | import { CdkTextareaAutosize } from '@angular/cdk/text-field'; 4 | import { tap, takeUntil } from 'rxjs/operators'; 5 | import { Subject } from 'rxjs'; 6 | 7 | @Component({ 8 | selector: 'app-confirm-mnemonic', 9 | templateUrl: './confirm-mnemonic.component.html', 10 | }) 11 | export class ConfirmMnemonicComponent implements OnInit, OnDestroy { 12 | @Input() formGroup: FormGroup | null = null; 13 | @ViewChild('autosize') autosize?: CdkTextareaAutosize; 14 | 15 | destroyed$$ = new Subject(); 16 | 17 | constructor( 18 | private ngZone: NgZone, 19 | ) { } 20 | 21 | ngOnInit(): void { 22 | this.triggerResize(); 23 | } 24 | 25 | ngOnDestroy(): void { 26 | this.destroyed$$.next(); 27 | this.destroyed$$.complete(); 28 | } 29 | 30 | triggerResize(): void { 31 | // Wait for changes to be applied, then trigger textarea resize. 32 | this.ngZone.onStable.pipe( 33 | tap(() => this.autosize?.resizeToFitContent(true)), 34 | takeUntil(this.destroyed$$), 35 | ).subscribe(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/modules/shared/services/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from '@angular/cdk/portal'; 2 | import { Injectable } from '@angular/core'; 3 | import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class NotificationService { 9 | constructor(private snackBar: MatSnackBar) {} 10 | 11 | private readonly DURATION = 6000; 12 | 13 | notifySuccess(msg: string, duration = this.DURATION): void { 14 | this.snackBar.open(msg, 'Success', this.getSnackBarConfig(duration)); 15 | } 16 | 17 | notifyError(msg: string, duration = this.DURATION): void { 18 | this.snackBar.open(msg, 'Close', { 19 | duration: this.DURATION, 20 | panelClass: 'snackbar-warn', 21 | }); 22 | } 23 | 24 | notifyWithComponent(component: ComponentType, duration = this.DURATION): void { 25 | this.snackBar.openFromComponent( 26 | component, 27 | this.getSnackBarConfig(duration) 28 | ); 29 | } 30 | 31 | getSnackBarConfig(duration: number): MatSnackBarConfig { 32 | return { 33 | duration: (duration), 34 | horizontalPosition: 'right', 35 | verticalPosition: 'top', 36 | politeness: 'polite' 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/modules/shared/loading/loading.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, TemplateRef } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-loading', 5 | templateUrl: './loading.component.html' 6 | }) 7 | export class LoadingComponent { 8 | 9 | @Input() loading = false; 10 | @Input() hasError = false; 11 | @Input() noData = false; 12 | 13 | @Input() loadingMessage: string | null = null; 14 | @Input() errorMessage: string | null = null; 15 | @Input() noDataMessage: string | null = null; 16 | 17 | @Input() errorImage: string | null = null; 18 | @Input() noDataImage: string | null = null; 19 | @Input() loadingImage: string | null = null; 20 | 21 | @Input() loadingTemplate: TemplateRef | null = null; 22 | 23 | @Input() minHeight = '200px'; 24 | @Input() minWidth = '100%'; 25 | 26 | 27 | constructor() { } 28 | 29 | getMessage(): string | null { 30 | let message: string | null = null; 31 | if (this.loading) { 32 | message = this.loadingMessage; 33 | } 34 | else if (this.errorMessage) { 35 | message = this.errorMessage || 'An error occured.'; 36 | } 37 | else if (this.noData) { 38 | message = this.noDataMessage || 'No data was loaded'; 39 | } 40 | return message; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/app/modules/core/services/geo-locator.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { map, switchMap, take } from 'rxjs/operators'; 5 | import { Peers } from '../../../proto/eth/v1alpha1/node'; 6 | import { BeaconNodeService } from './beacon-node.service'; 7 | import { GEO_COORDINATES_API } from '../../core/constants'; 8 | 9 | 10 | export interface GeoCoordinate { 11 | city: string; 12 | lon: number; 13 | lat: number; 14 | } 15 | 16 | @Injectable({ 17 | providedIn: 'root' 18 | }) 19 | export class GeoLocatorService { 20 | 21 | constructor( 22 | private http: HttpClient, 23 | private beaconService: BeaconNodeService, 24 | ) { } 25 | 26 | // http request 27 | getPeerCoordinates(): Observable { 28 | return this.beaconService.peers$.pipe( 29 | map((peers: Peers) => { 30 | return peers.peers.filter(peer => { 31 | return peer.address.match(/^\/ip(4|6)\//); 32 | }).map(peer => { 33 | return peer.address.split('/')[2]; 34 | }); 35 | }), 36 | switchMap(ips => this.http.post(GEO_COORDINATES_API, JSON.stringify(ips.slice(0, 100)))) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/modules/auth/services/authentication.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { EnvironmenterService } from '../../core/services/environmenter.service'; 5 | import { AuthenticationService } from './authentication.service'; 6 | 7 | describe('AuthenticationService', () => { 8 | let environmenter: EnvironmenterService; 9 | let service: AuthenticationService; 10 | let httpMock: HttpTestingController; 11 | const serviceSpy = jasmine.createSpyObj('EnvironmenterService', ['env']); 12 | 13 | beforeEach(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [ 16 | HttpClientTestingModule, 17 | RouterTestingModule, 18 | ], 19 | providers: [ 20 | AuthenticationService, 21 | { provide: EnvironmenterService, useValue: serviceSpy } 22 | ] 23 | }); 24 | environmenter = TestBed.inject(EnvironmenterService); 25 | service = TestBed.inject(AuthenticationService); 26 | httpMock = TestBed.inject(HttpTestingController); 27 | }); 28 | 29 | afterEach(() => { 30 | httpMock.verify(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/modules/shared/services/utility.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, of } from 'rxjs'; 3 | import { ISelectListItem } from '../types/select-list-item'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class UtilityService { 9 | constructor() {} 10 | 11 | languages$(): Observable { 12 | return of([ 13 | { 14 | text: 'English', 15 | value: 'english', 16 | }, 17 | { 18 | text: '日本語', 19 | value: 'japanese', 20 | }, 21 | { 22 | text: 'Español', 23 | value: 'spanish', 24 | }, 25 | { 26 | text: '中文(简体)', 27 | value: 'simplified chinese', 28 | }, 29 | { 30 | text: '中文(繁體)', 31 | value: 'traditional chinese', 32 | }, 33 | { 34 | text: 'Français', 35 | value: 'french', 36 | }, 37 | { 38 | text: 'Italiano', 39 | value: 'italian', 40 | }, 41 | { 42 | text: '한국어', 43 | value: 'korean', 44 | }, 45 | { 46 | text: 'Čeština', 47 | value: 'czech', 48 | }, 49 | { 50 | text: 'Português', 51 | value: 'portuguese', 52 | }, 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/styles/components/_loader.scss: -------------------------------------------------------------------------------- 1 | .btn-container { 2 | position: relative; 3 | .btn-progress { 4 | position: absolute; 5 | top: 5px; 6 | left: 56px; 7 | margin-top: -4; 8 | margin-left: -24; 9 | } 10 | } 11 | 12 | .loader-bounce { 13 | height: 100vh !important; 14 | width: 100%; 15 | display: flex; 16 | align-items: center; 17 | } 18 | 19 | .spinner { 20 | width: 40px; 21 | height: 40px; 22 | position: relative; 23 | margin: auto; 24 | } 25 | 26 | .double-bounce1, 27 | .double-bounce2 { 28 | width: 100%; 29 | height: 100%; 30 | border-radius: 50%; 31 | opacity: 0.6; 32 | position: absolute; 33 | top: 0; 34 | left: 0; 35 | -webkit-animation: sk-bounce 2s infinite ease-in-out; 36 | animation: sk-bounce 2s infinite ease-in-out; 37 | } 38 | 39 | .double-bounce2 { 40 | -webkit-animation-delay: -1s; 41 | animation-delay: -1s; 42 | } 43 | 44 | @-webkit-keyframes sk-bounce { 45 | 0%, 46 | 100% { 47 | -webkit-transform: scale(0); 48 | } 49 | 50% { 50 | -webkit-transform: scale(1); 51 | } 52 | } 53 | 54 | @keyframes sk-bounce { 55 | 0%, 56 | 100% { 57 | transform: scale(0); 58 | -webkit-transform: scale(0); 59 | } 60 | 50% { 61 | transform: scale(1); 62 | -webkit-transform: scale(1); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /third_party/leaflet/leaflet.ts: -------------------------------------------------------------------------------- 1 | export interface ILMarker { 2 | bindTooltip(toolTip: string): ILMarker; 3 | addTo(map: ILMap): ILMarker; 4 | } 5 | export interface ILMap { 6 | setView(coord: number[], scale: number): ILMap; 7 | } 8 | 9 | export interface ILIcon { 10 | iconUrl: string; 11 | iconSize: number[]; 12 | } 13 | 14 | export interface ILIconConfig { 15 | iconUrl: string; 16 | iconSize: number[]; 17 | } 18 | 19 | export class LIconConfig implements ILIconConfig { 20 | constructor(public iconUrl: string, public iconSize: number[]) { } 21 | } 22 | 23 | export interface ILMarkerConfig { 24 | icon: ILIcon; 25 | } 26 | 27 | export class LMarkerConfig implements ILMarkerConfig { 28 | constructor(public icon: ILIcon) { } 29 | } 30 | 31 | export interface ILTileLayer { 32 | addTo(map: ILMap): ILTileLayer; 33 | } 34 | 35 | export interface ILTileLayerConfig { 36 | attribution: string; 37 | } 38 | 39 | export class LTileLayerConfig implements ILTileLayerConfig { 40 | constructor(public attribution: string) { } 41 | } 42 | 43 | export interface IL { 44 | map(name: string): ILMap; 45 | icon(config: ILIconConfig): ILIcon; 46 | marker(coords: number[], markerConfig: ILMarkerConfig): ILMarker; 47 | tileLayer(path: string, config: ILTileLayerConfig): ILTileLayer; 48 | } 49 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/sidebar-expandable-link/sidebar-expandable-link.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { SharedModule } from '../../../shared/shared.module'; 4 | import { SidebarExpandableLinkComponent } from './sidebar-expandable-link.component'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | describe('SidebarExpandableLinkComponent', () => { 8 | let component: SidebarExpandableLinkComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ 14 | SharedModule, 15 | BrowserAnimationsModule 16 | ], 17 | declarations: [ 18 | SidebarExpandableLinkComponent, 19 | ] 20 | }) 21 | .compileComponents(); 22 | })); 23 | 24 | beforeEach(() => { 25 | fixture = TestBed.createComponent(SidebarExpandableLinkComponent); 26 | component = fixture.componentInstance; 27 | component.link = { 28 | name: 'Wallet', 29 | icon: 'whatshot', 30 | }; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/modules/core/interceptors/jwt.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { AuthenticationService } from '../../auth/services/authentication.service'; 6 | import { EnvironmenterService } from '../services/environmenter.service'; 7 | 8 | @Injectable() 9 | export class JwtInterceptor implements HttpInterceptor { 10 | constructor( 11 | private authenticationService: AuthenticationService, 12 | private environmenterService: EnvironmenterService 13 | ) { } 14 | 15 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 16 | // Add authorization header to all HTTP requests with 17 | // a jwt token if available from the auth service.. 18 | const token = this.authenticationService.getToken(); 19 | if ((token && request.url.indexOf(this.environmenterService.env.validatorEndpoint) !== -1) || 20 | (token && request.url.indexOf(this.environmenterService.env.keymanagerEndpoint) !== -1)) 21 | { 22 | request = request.clone({ 23 | setHeaders: { 24 | Authorization: `Bearer ${token}` 25 | } 26 | }); 27 | } 28 | 29 | return next.handle(request); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/account-actions/account-actions.component.ts: -------------------------------------------------------------------------------- 1 | import { SelectionModel } from '@angular/cdk/collections'; 2 | import { Component, Input } from '@angular/core'; 3 | import { MatDialog } from '@angular/material/dialog'; 4 | import { WalletService } from 'src/app/modules/core/services/wallet.service'; 5 | import { TableData } from '../accounts-table/accounts-table.component'; 6 | import { AccountDeleteComponent } from '../account-delete/account-delete.component'; 7 | import { LANDING_URL } from 'src/app/modules/core/constants'; 8 | 9 | 10 | 11 | @Component({ 12 | selector: 'app-account-actions', 13 | templateUrl: './account-actions.component.html', 14 | styles: [], 15 | }) 16 | export class AccountActionsComponent { 17 | 18 | readonly LANDING_URL = '/' + LANDING_URL; 19 | 20 | constructor( 21 | private walletService: WalletService, 22 | private dialog: MatDialog 23 | ) {} 24 | walletConfig$ = this.walletService.walletConfig$; 25 | @Input() selection: SelectionModel | null = null; 26 | openDelete(): void { 27 | if (!this.selection) { 28 | return; 29 | } 30 | const keys = this.selection.selected.map((x) => x.publicKey); 31 | this.dialog.open(AccountDeleteComponent, { 32 | width: '600px', 33 | data: keys, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/modules/shared/pipes/filename.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | /* 4 | * partial truncate a file name based on max length 5 | */ 6 | @Pipe({name: 'filename'}) 7 | export class FileNamePipe implements PipeTransform { 8 | transform(name: string, maxLength: number): string { 9 | const ext: string = 10 | name.substring(name.lastIndexOf('.') + 1, name.length).toLowerCase(); 11 | let newName: string = name.replace('.' + ext, ''); 12 | if (name.length <= maxLength || maxLength < 8) { 13 | // if file name length is less than maxLength do not format 14 | // return same name 15 | return name; 16 | } else { 17 | // if file name length is greater than maxLength 18 | const fileName = name.substring(0, name.lastIndexOf('.')); 19 | const firstHalf = fileName.substring(0, Math.floor(fileName.length / 2)); 20 | const secondHalf = fileName.substring(Math.floor(fileName.length / 2), fileName.length); 21 | 22 | const fileNamePartMaxLength = Math.floor((maxLength - ext.length - 3)/2); 23 | 24 | const newName = firstHalf.substring(0,fileNamePartMaxLength) + '...' + secondHalf.substring(secondHalf.length-fileNamePartMaxLength,secondHalf.length); 25 | 26 | return newName + '.' + ext; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/styles/components/_loading.component.scss: -------------------------------------------------------------------------------- 1 | app-loading { 2 | position: relative; 3 | display: block; 4 | 5 | .overlay { 6 | position: absolute; 7 | left: 0; 8 | right: 0; 9 | top: 0; 10 | bottom: 0; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: center; 15 | background: #222A45; 16 | z-index: 999; 17 | padding: 10px; 18 | 19 | .loadingBackground { 20 | position: absolute; 21 | left: 0; 22 | right: 0; 23 | top: 0; 24 | bottom: 0; 25 | z-index: -1; 26 | height: 100%; 27 | width: 100%; 28 | object-fit: fill; 29 | opacity: 0.1; 30 | margin: auto; 31 | } 32 | 33 | .message { 34 | font-size: 1.25rem; 35 | padding: 10px; 36 | } 37 | 38 | img { 39 | max-height: 80%; 40 | max-width: 80%; 41 | } 42 | } 43 | 44 | .content-container { 45 | opacity: 1; 46 | transition: opacity 1s; 47 | 48 | &.loading { 49 | opacity: 0; 50 | } 51 | } 52 | 53 | .loading-template-container { 54 | position: absolute; 55 | left: 0; 56 | right: 0; 57 | top: 0; 58 | bottom: 0; 59 | z-index: -1; 60 | height: 100%; 61 | width: 100%; 62 | max-height: 100%; 63 | opacity: 0.2; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/modules/core/utils/simple-store.spec.ts: -------------------------------------------------------------------------------- 1 | import { Store } from './simple-store'; 2 | 3 | describe('Store', () => { 4 | it('should only fire an event over the behavior subject if data has changed', (done) => { 5 | const item = { 6 | title: 'Foo', 7 | subtitle: 'Bar', 8 | }; 9 | const store = new Store(item); 10 | store.subscribe(res => { 11 | expect(res).toEqual(item); 12 | done(); 13 | }); 14 | store.subscribe(res => { 15 | expect(res).toEqual(item); 16 | done(); 17 | }); 18 | expect(store.observers.length).toEqual(2); 19 | 20 | // We spy on the superclass 'next' method (BehaviorSubject). 21 | const superSpy = spyOn(Object.getPrototypeOf(Object.getPrototypeOf(store)), 'next'); 22 | // We send the item over the store 2 times 23 | // but expect 0 calls to the superclass next() 24 | // method given the data has not changed. 25 | store.next(item); 26 | store.next(item); 27 | expect(superSpy).toHaveBeenCalledTimes(0); 28 | 29 | // Next, we change the item and fire again and now 30 | // we should have seen the super class's next() method get called. 31 | store.next({ 32 | title: 'Hello', 33 | subtitle: 'World', 34 | }); 35 | expect(superSpy).toHaveBeenCalledTimes(1); 36 | store.complete(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 14 | 15 | **What type of PR is this?** 16 | 17 | > Uncomment one line below and remove others. 18 | > 19 | > Bug fix 20 | > Feature 21 | > Documentation 22 | > Other 23 | 24 | **What does this PR do? Why is it needed?** 25 | 26 | **Which issues(s) does this PR fix?** 27 | 28 | Fixes # 29 | 30 | **Other notes for review** 31 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { HttpClientXsrfModule } from '@angular/common/http'; 6 | 7 | import { AppRoutingModule } from './app-routing.module'; 8 | import { AppComponent } from './app.component'; 9 | import { DashboardModule } from './modules/dashboard/dashboard.module'; 10 | import { AuthModule } from './modules/auth/auth.module'; 11 | import { WalletModule } from './modules/wallet/wallet.module'; 12 | import { OnboardingModule } from './modules/onboarding/onboarding.module'; 13 | import { SystemProcessModule } from './modules/system-process/system-process.module'; 14 | import { CoreModule } from './modules/core/core.module'; 15 | 16 | 17 | const frameworkModules = [ 18 | BrowserModule, 19 | BrowserAnimationsModule, 20 | HttpClientXsrfModule 21 | ]; 22 | 23 | const prysmModules = [ 24 | CoreModule, 25 | AppRoutingModule, 26 | AuthModule, 27 | DashboardModule, 28 | WalletModule, 29 | OnboardingModule, 30 | SystemProcessModule 31 | ]; 32 | 33 | @NgModule({ 34 | declarations: [AppComponent], 35 | imports: [ 36 | ...frameworkModules, 37 | ...prysmModules 38 | ], 39 | bootstrap: [AppComponent] 40 | }) 41 | export class AppModule {} 42 | -------------------------------------------------------------------------------- /src/app/modules/auth/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { AuthenticationService } from '../services/authentication.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AuthGuard implements CanActivate { 10 | constructor(private authService: AuthenticationService, private router: Router) { } 11 | 12 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): 13 | Observable 14 | | Promise 15 | | boolean | UrlTree { 16 | const accessToken = this.authService.getToken(); 17 | const accessTokenExpiration = this.authService.getTokenExpiration(); 18 | // validate if user is authenticated and if the token expiration exists 19 | if (!accessToken || (accessTokenExpiration && !this.validateAccessTokenExpiration(accessTokenExpiration))){ 20 | return this.router.parseUrl('/initialize'); 21 | } else { 22 | return true; 23 | } 24 | } 25 | 26 | private validateAccessTokenExpiration(tokenExpiration: number): boolean{ 27 | // logic should be added here for checking token expiration 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/modules/wallet/pages/wallet-details/wallet-details.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | Wallet Information 6 |
7 |

8 | Information about your current wallet and its configuration options 9 |

10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 | help 20 | Help 21 | 22 | 23 | 24 | 25 | 26 | folder 27 | Files 28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core'; 2 | import { NavigationEnd, Router } from '@angular/router'; 3 | import { Subject } from 'rxjs'; 4 | import { filter, takeUntil, tap } from 'rxjs/operators'; 5 | import { EventsService } from 'src/app/modules/core/services/events.service'; 6 | import { EnvironmenterService } from './modules/core/services/environmenter.service'; 7 | 8 | 9 | @Component({ 10 | selector: 'app-root', 11 | templateUrl: './app.component.html', 12 | }) 13 | export class AppComponent implements OnInit, OnDestroy { 14 | title = 'prysm-web-ui'; 15 | constructor( 16 | private router: Router, 17 | private eventsService: EventsService, 18 | private environmenterService: EnvironmenterService 19 | ) { } 20 | private destroyed$$ = new Subject(); 21 | isDevelopment = !this.environmenterService.env.production; 22 | 23 | 24 | ngOnInit(): void { 25 | // dispatch action for initialize 26 | this.router.events.pipe( 27 | filter(event => event instanceof NavigationEnd), 28 | tap(() => { 29 | this.eventsService.routeChanged$.next(this.router.routerState.root.snapshot); 30 | }), 31 | takeUntil(this.destroyed$$), 32 | ).subscribe(); 33 | } 34 | 35 | ngOnDestroy(): void { 36 | this.destroyed$$.next(); 37 | this.destroyed$$.complete(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/styles/components/_pulsating.scss: -------------------------------------------------------------------------------- 1 | .pulsating-circle { 2 | background: rgba(255, 82, 82, 1); 3 | box-shadow: 0 0 0 0 rgba(255, 82, 82, 1); 4 | border-radius: 50%; 5 | margin: 10px; 6 | height: 20px; 7 | width: 20px; 8 | transform: scale(1); 9 | animation: pulse-red 2s infinite; 10 | } 11 | 12 | .pulsating-circle.red { 13 | background: rgba(255, 82, 82, 1); 14 | box-shadow: 0 0 0 0 rgba(255, 82, 82, 1); 15 | animation: pulse-red 2s infinite; 16 | } 17 | 18 | .pulsating-circle.green { 19 | background: rgba(51, 217, 178, 1); 20 | box-shadow: 0 0 0 0 rgba(51, 217, 178, 1); 21 | animation: pulse-green 2s infinite; 22 | } 23 | 24 | @keyframes pulse-red { 25 | 0% { 26 | transform: scale(1.1); 27 | box-shadow: 0 0 0 0 rgba(255, 82, 82, 0.7); 28 | } 29 | 30 | 70% { 31 | transform: scale(1.3); 32 | box-shadow: 0 0 0 15px rgba(255, 82, 82, 0); 33 | } 34 | 35 | 100% { 36 | transform: scale(1.1); 37 | box-shadow: 0 0 0 0 rgba(255, 82, 82, 0); 38 | } 39 | } 40 | 41 | @keyframes pulse-green { 42 | 0% { 43 | transform: scale(0.95); 44 | box-shadow: 0 0 0 0 rgba(51, 217, 178, 0.7); 45 | } 46 | 47 | 70% { 48 | transform: scale(1); 49 | box-shadow: 0 0 0 10px rgba(51, 217, 178, 0); 50 | } 51 | 52 | 100% { 53 | transform: scale(0.95); 54 | box-shadow: 0 0 0 0 rgba(51, 217, 178, 0); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/components/confirm-mnemonic/confirm-mnemonic.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Confirm your mnemonic 4 |
5 |
6 | Enter all 24 words for your mnemonic from the previous step to proceed. Remember this is the only way you can recover your validator keys if you lose your wallet. 7 |
8 | 9 | Confirm your mnemonic 10 | 18 | 19 | Mnemonic is required 20 | 21 | 22 | Must contain 24 words separated by spaces 23 | 24 | 25 | Entered mnemonic does not match original 26 | 27 | 28 |
29 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | customLaunchers: { 19 | ChromeHeadlessCustom: { 20 | base: 'ChromeHeadless', 21 | flags: ['--no-sandbox', '--disable-gpu'] 22 | } 23 | }, 24 | coverageIstanbulReporter: { 25 | dir: require('path').join(__dirname, './coverage/prysm-web-ui'), 26 | reports: ['html', 'lcovonly', 'text-summary'], 27 | fixWebpackSourcePaths: true 28 | }, 29 | files: [ 30 | "./node_modules/leaflet/dist/leaflet.js" 31 | ], 32 | reporters: ['progress', 'kjhtml'], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | browsers: ['ChromeHeadlessCustom'], 38 | singleRun: false, 39 | restartOnFileChange: true 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/types/git-response.ts: -------------------------------------------------------------------------------- 1 | export interface GitResponse { 2 | url: string; 3 | assets_url: string; 4 | upload_url: string; 5 | html_url: string; 6 | id: number; 7 | author: GitAuthor; 8 | node_id: string; 9 | tag_name: string; 10 | target_commitish: string; 11 | name: string; 12 | draft: boolean; 13 | prerelease: boolean; 14 | created_at: Date | string; 15 | published_at: Date | string; 16 | assets: GitAssets[]; 17 | tarball_url: string; 18 | zipball_url: string; 19 | body: string; 20 | } 21 | 22 | export interface GitAuthor { 23 | login: string; 24 | id: number; 25 | node_id: string; 26 | avatar_url: string; 27 | gravatar_id: string; 28 | url: string; 29 | html_url: string; 30 | followers_url: string; 31 | following_url: string; 32 | gists_url: string; 33 | starred_url: string; 34 | subscriptions_url: string; 35 | organizations_url: string; 36 | repos_url: string; 37 | events_url: string; 38 | received_events_url: string; 39 | type: string; 40 | site_admin: boolean; 41 | } 42 | 43 | export interface GitAssets { 44 | url: string; 45 | id: number; 46 | node_id: string; 47 | name: string; 48 | label: string; 49 | content_type: string; 50 | state: string; 51 | size: number; 52 | download_count: number; 53 | created_at: Date | string; 54 | updated_at: Date | string; 55 | browser_download_url: string; 56 | uploader: GitAuthor; 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/images/skeletons/info_box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Layer 1 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/import-dropzone/import-dropzone.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | 10 | 11 | {{uploadedFiles[0].name || 'invalid file'}} 12 | 13 | 14 | 16 | 17 | 18 |
19 |
20 | 21 | Not adding these files: 22 |
    23 |
  • {{file}}
  • 24 |
25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/account-actions/account-actions.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { MockService } from 'ng-mocks'; 3 | import { of } from 'rxjs'; 4 | import { WalletService } from 'src/app/modules/core/services/wallet.service'; 5 | import { SharedModule } from 'src/app/modules/shared/shared.module'; 6 | import { WalletResponse } from 'src/app/proto/validator/accounts/v2/web_api'; 7 | 8 | import { AccountActionsComponent } from './account-actions.component'; 9 | 10 | describe('AccountActionsComponent', () => { 11 | let component: AccountActionsComponent; 12 | let fixture: ComponentFixture; 13 | let service: WalletService = MockService(WalletService); 14 | service.walletConfig$ = of({ 15 | keymanager_kind: 'DERIVED', 16 | } as WalletResponse); 17 | 18 | beforeEach(waitForAsync(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [AccountActionsComponent], 21 | imports: [ 22 | SharedModule, 23 | ], 24 | providers: [ 25 | { provide: WalletService, useValue: service } 26 | ] 27 | }) 28 | .compileComponents(); 29 | service = TestBed.inject(WalletService); 30 | })); 31 | 32 | beforeEach(() => { 33 | fixture = TestBed.createComponent(AccountActionsComponent); 34 | component = fixture.componentInstance; 35 | fixture.detectChanges(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/modules/core/components/global-dialog/global-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse } from '@angular/common/http'; 2 | import { Component, Inject, OnInit } from '@angular/core'; 3 | import { MAT_DIALOG_DATA } from '@angular/material/dialog'; 4 | import { DialogConfig, DialogConfigMessage, DialogContentAlert } from './model/interfaces'; 5 | 6 | @Component({ 7 | selector: 'app-global-dialog', 8 | templateUrl: 'global-dialog.component.html' 9 | }) 10 | export class GlobalDialogComponent implements OnInit { 11 | constructor(@Inject(MAT_DIALOG_DATA) public data: DialogConfigMessage) { } 12 | 13 | panelOpenState = false; 14 | copyButtonText = 'Copy'; 15 | 16 | title = ''; 17 | content = ''; 18 | 19 | alert: DialogContentAlert | null | undefined = null; 20 | 21 | ngOnInit(): void { 22 | const dialogConfig: DialogConfig = this.data.payload; 23 | this.title = dialogConfig.title; 24 | this.content = dialogConfig.content; 25 | this.alert = dialogConfig.alert; 26 | } 27 | 28 | isInstanceOfError(): boolean { 29 | if (this.alert){ 30 | return this.alert.message instanceof Error || this.alert.message instanceof HttpErrorResponse; 31 | } 32 | return false; 33 | } 34 | 35 | changeCopyText(): void { 36 | this.copyButtonText = 'Copied'; 37 | setTimeout(() => {this.copyButtonText = 'Copy'; }, 1500); 38 | } 39 | 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/version/version.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { MockService } from 'ng-mocks'; 4 | import { of } from 'rxjs'; 5 | import { ValidatorService } from 'src/app/modules/core/services/validator.service'; 6 | import { SharedModule } from '../../../shared/shared.module'; 7 | import { VersionComponent } from './version.component'; 8 | 9 | describe('VersionComponent', () => { 10 | const validatorService = MockService(ValidatorService); 11 | let component: VersionComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async(() => { 15 | TestBed.configureTestingModule({ 16 | imports: [ 17 | SharedModule, 18 | BrowserAnimationsModule 19 | ], 20 | providers: [ 21 | {provide: ValidatorService, useValue: validatorService} 22 | ], 23 | declarations: [ 24 | VersionComponent, 25 | ] 26 | }) 27 | .compileComponents(); 28 | })); 29 | 30 | beforeEach(() => { 31 | validatorService.version$ = of({beacon: 'test', validator: 'test'}); 32 | fixture = TestBed.createComponent(VersionComponent); 33 | component = fixture.componentInstance; 34 | fixture.detectChanges(); 35 | }); 36 | 37 | it('should create', () => { 38 | expect(component).toBeTruthy(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/app/modules/system-process/pages/peer-locations-map/peer-locations-map.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { MockService } from 'ng-mocks'; 3 | import { of } from 'rxjs'; 4 | import { GeoCoordinate, GeoLocatorService } from '../../../core/services/geo-locator.service'; 5 | 6 | import { PeerLocationsMapComponent } from './peer-locations-map.component'; 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | declare let L: any; 9 | 10 | describe('PeerLocationsMapComponent', () => { 11 | let component: PeerLocationsMapComponent; 12 | let fixture: ComponentFixture; 13 | const service: GeoLocatorService = MockService(GeoLocatorService); 14 | 15 | beforeEach(waitForAsync(() => { 16 | TestBed.configureTestingModule({ 17 | imports: [ 18 | BrowserAnimationsModule 19 | ], 20 | declarations: [PeerLocationsMapComponent], 21 | providers: [ 22 | { provide: GeoLocatorService, useValue: service }, 23 | ] 24 | }) 25 | .compileComponents(); 26 | })); 27 | 28 | beforeEach(() => { 29 | spyOn(service, 'getPeerCoordinates').and.returnValue(of()); 30 | fixture = TestBed.createComponent(PeerLocationsMapComponent); 31 | component = fixture.componentInstance; 32 | fixture.detectChanges(); 33 | }); 34 | 35 | it('should create', () => { 36 | expect(component).toBeTruthy(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/app/modules/system-process/system-process.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { NgxEchartsModule } from 'ngx-echarts'; 5 | 6 | import { SharedModule } from '../../modules/shared/shared.module'; 7 | import { LogsComponent } from './pages/logs/logs.component'; 8 | import { MetricsComponent } from './pages/metrics/metrics.component'; 9 | import { BalancesChartComponent } from './components/balances-chart/balances-chart.component'; 10 | import { ProposedMissedChartComponent } from './components/proposed-missed-chart/proposed-missed-chart.component'; 11 | import { DoubleBarChartComponent } from './components/double-bar-chart/double-bar-chart.component'; 12 | import { PieChartComponent } from './components/pie-chart/pie-chart.component'; 13 | import { LogsStreamComponent } from './components/logs-stream/logs-stream.component'; 14 | import { PeerLocationsMapComponent } from './pages/peer-locations-map/peer-locations-map.component'; 15 | 16 | @NgModule({ 17 | declarations: [ 18 | LogsComponent, 19 | MetricsComponent, 20 | BalancesChartComponent, 21 | ProposedMissedChartComponent, 22 | DoubleBarChartComponent, 23 | PieChartComponent, 24 | LogsStreamComponent, 25 | PeerLocationsMapComponent, 26 | ], 27 | imports: [ 28 | CommonModule, 29 | SharedModule, 30 | NgxEchartsModule.forRoot({ 31 | echarts: () => import('echarts'), 32 | }) 33 | ] 34 | }) 35 | export class SystemProcessModule { } 36 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/account-actions/account-actions.component.html: -------------------------------------------------------------------------------- 1 | 45 | -------------------------------------------------------------------------------- /src/app/modules/wallet/pages/account-backup/account-backup.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | Back Up Selected Account(s) 7 |
8 |
9 | We'll convert your selected accounts into individual, EIP-2335 10 | compliant, password protected files compressed into a zip file 11 |
12 | 13 | 14 | 19 |
20 | 21 |
22 | 23 | 27 | 28 |
29 |
30 |
31 |
-------------------------------------------------------------------------------- /src/app/modules/shared/services/breadcrumb.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | Route, 4 | ActivatedRouteSnapshot, 5 | } from '@angular/router'; 6 | import { Observable, of } from 'rxjs'; 7 | 8 | export interface Breadcrumb { 9 | displayName: string; 10 | url: string; 11 | route?: Route; 12 | } 13 | 14 | @Injectable() 15 | export class BreadcrumbService { 16 | constructor() { } 17 | 18 | create(route: ActivatedRouteSnapshot): Observable { 19 | let url = ''; 20 | const newCrumbs: Breadcrumb[] = []; 21 | 22 | while (route.firstChild) { 23 | route = route.firstChild; 24 | if (!route.routeConfig) { 25 | continue; 26 | } 27 | if (!route.routeConfig.path) { 28 | continue; 29 | } 30 | url += `/${this.createUrl(route)}`; 31 | // Only include route paths with defined breadcrumb label. 32 | if (!route.data.breadcrumb) { 33 | continue; 34 | } 35 | const newCrumb = this.initializeBreadcrumb(route, url); 36 | newCrumbs.push(newCrumb); 37 | } 38 | return of(newCrumbs); 39 | } 40 | 41 | private initializeBreadcrumb(route: ActivatedRouteSnapshot, url: string): Breadcrumb { 42 | const breadcrumb: Breadcrumb = { 43 | displayName: route.data.breadcrumb, 44 | url, 45 | }; 46 | if (route.routeConfig) { 47 | breadcrumb.route = route.routeConfig; 48 | } 49 | return breadcrumb; 50 | } 51 | 52 | private createUrl(route: ActivatedRouteSnapshot): string { 53 | return route && route.url.map(String).join('/'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/modules/shared/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { User } from '../types/user'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class UserService { 9 | constructor() { 10 | this.setUser(this.getUser()); 11 | } 12 | 13 | private userKeyStore = 'user-prysm'; 14 | 15 | private user: User | null | undefined; 16 | private onUserChange = new BehaviorSubject((null as any) as User); 17 | user$ = this.onUserChange.asObservable(); 18 | 19 | changeAccountListPerPage(accountListPerPage: number): void { 20 | if (!this.user) { 21 | return; 22 | } 23 | this.user.acountsPerPage = accountListPerPage; 24 | this.saveChanges(); 25 | } 26 | 27 | changeGainsAndLosesPageSize(pageSize: number): void { 28 | if (!this.user) { 29 | return; 30 | } 31 | this.user.gainAndLosesPageSize = pageSize; 32 | this.saveChanges(); 33 | } 34 | 35 | private saveChanges(): void { 36 | if (this.user) { 37 | localStorage.setItem(this.userKeyStore, JSON.stringify(this.user)); 38 | this.onUserChange.next(this.user); 39 | } 40 | } 41 | 42 | private getUser(): User { 43 | const userStr = localStorage.getItem(this.userKeyStore); 44 | if (!userStr) { 45 | const user = new User(); 46 | return user; 47 | } else { 48 | const user = new User(JSON.parse(userStr)); 49 | return user; 50 | } 51 | } 52 | 53 | private setUser(user: User): void { 54 | this.user = user; 55 | this.saveChanges(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/modules/wallet/pages/wallet-details/wallet-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { Subject, throwError } from 'rxjs'; 3 | import { catchError, takeUntil, tap } from 'rxjs/operators'; 4 | import { WalletService } from 'src/app/modules/core/services/wallet.service'; 5 | import { WalletResponse } from 'src/app/proto/validator/accounts/v2/web_api'; 6 | 7 | @Component({ 8 | selector: 'app-wallet-details', 9 | templateUrl: './wallet-details.component.html', 10 | }) 11 | export class WalletDetailsComponent implements OnInit, OnDestroy { 12 | constructor( 13 | private walletService: WalletService, 14 | ) { } 15 | private destroyed$ = new Subject(); 16 | loading = false; 17 | wallet: WalletResponse | null = null; 18 | keymanagerKind = 'UNKNOWN'; 19 | 20 | ngOnInit(): void { 21 | this.fetchData(); 22 | } 23 | 24 | ngOnDestroy(): void { 25 | this.destroyed$.next(); 26 | this.destroyed$.complete(); 27 | } 28 | 29 | private fetchData(): void { 30 | this.loading = true; 31 | this.walletService.walletConfig$.pipe( 32 | takeUntil(this.destroyed$), 33 | tap((res: WalletResponse) => { 34 | this.loading = false; 35 | this.wallet = res; 36 | if (!this.wallet.keymanager_kind) { 37 | this.keymanagerKind = 'DERIVED'; 38 | } else { 39 | this.keymanagerKind = this.wallet.keymanager_kind; 40 | } 41 | }), 42 | catchError((err) => { 43 | this.loading = false; 44 | return throwError(err); 45 | }), 46 | ).subscribe(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/modules/system-process/components/logs-stream/logs-stream.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | Component, 4 | ElementRef, 5 | Input, 6 | QueryList, 7 | ViewChild, 8 | ViewChildren 9 | } from '@angular/core'; 10 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; 11 | 12 | import { 13 | default as AnsiUp 14 | } from 'ansi_up'; 15 | 16 | @Component({ 17 | selector: 'app-logs-stream', 18 | templateUrl: './logs-stream.component.html', 19 | }) 20 | export class LogsStreamComponent implements AfterViewInit { 21 | @Input() messages: string[] | null = null; 22 | @Input() title: string | null = null; 23 | @Input() url: string | null = null; 24 | @ViewChild('scrollFrame', {static: false}) scrollFrame: ElementRef | null = null; 25 | @ViewChildren('item') itemElements: QueryList | null = null; 26 | constructor( 27 | private sanitizer: DomSanitizer, 28 | ) { } 29 | 30 | private scrollContainer: any; 31 | private ansiUp = new AnsiUp(); 32 | 33 | ngAfterViewInit(): void { 34 | this.scrollContainer = this.scrollFrame?.nativeElement; 35 | this.itemElements?.changes.subscribe(_ => this.onItemElementsChanged()); 36 | } 37 | 38 | formatLog(msg: string): SafeHtml { 39 | return this.sanitizer.bypassSecurityTrustHtml(this.ansiUp.ansi_to_html(msg)); 40 | } 41 | 42 | private onItemElementsChanged(): void { 43 | this.scrollToBottom(); 44 | } 45 | 46 | private scrollToBottom(): void { 47 | this.scrollContainer.scroll({ 48 | top: this.scrollContainer.scrollHeight, 49 | left: 0, 50 | behavior: 'smooth' 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/wallet-kind/wallet-kind.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | 3 | interface KeymanagerInfo { 4 | [key: string]: { 5 | name: string; 6 | description: string; 7 | docsLink: string; 8 | }; 9 | } 10 | 11 | @Component({ 12 | selector: 'app-wallet-kind', 13 | templateUrl: './wallet-kind.component.html', 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | }) 16 | export class WalletKindComponent { 17 | @Input() kind = 'UNKNOWN'; 18 | constructor() { } 19 | info: KeymanagerInfo = { 20 | IMPORTED: { 21 | name: 'Imported Wallet', 22 | description: 'Imported (non-deterministic) wallets are the recommended wallets to use with Prysm when coming from the official eth2 launchpad', 23 | docsLink: 'https://docs.prylabs.network/docs/wallet/nondeterministic', 24 | }, 25 | DERIVED: { 26 | name: 'HD Wallet', 27 | description: 'Hierarchical-deterministic (HD) wallets are secure blockchain wallets derived from a seed phrase (a 24 word mnemonic)', 28 | docsLink: 'https://docs.prylabs.network/docs/wallet/deterministic', 29 | }, 30 | REMOTE: { 31 | name: 'Remote Signing Wallet', 32 | description: 'Remote wallets are the most secure, as they keep your private keys away from your validator', 33 | docsLink: 'https://docs.prylabs.network/docs/wallet/remote', 34 | }, 35 | UNKNOWN: { 36 | name: 'Unknown', 37 | description: 'Could not determine your wallet kind', 38 | docsLink: 'https://docs.prylabs.network/docs/wallet/remote', 39 | } 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/assets/images/skeletons/chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Layer 1 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/onboarding.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { MockComponent } from 'ng-mocks'; 3 | 4 | import { WalletKind } from './types/wallet'; 5 | import { OnboardingComponent, OnboardingState } from './onboarding.component'; 6 | import { ChooseWalletKindComponent } from './components/choose-wallet-kind/choose-wallet-kind.component'; 7 | import { HdWalletWizardComponent } from './pages/hd-wallet-wizard/hd-wallet-wizard.component'; 8 | 9 | describe('OnboardingComponent', () => { 10 | let component: OnboardingComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(waitForAsync(() => { 14 | TestBed.configureTestingModule({ 15 | declarations: [ 16 | OnboardingComponent, 17 | MockComponent(ChooseWalletKindComponent), 18 | MockComponent(HdWalletWizardComponent), 19 | ] 20 | }) 21 | .compileComponents(); 22 | })); 23 | 24 | beforeEach(() => { 25 | fixture = TestBed.createComponent(OnboardingComponent); 26 | component = fixture.componentInstance; 27 | component.onboardingState = OnboardingState.PickingWallet; 28 | fixture.detectChanges(); 29 | }); 30 | 31 | it('should update onboarding state based on wallet selection', done => { 32 | expect(component.onboardingState).toEqual(OnboardingState.PickingWallet); 33 | component.selectedWallet$.subscribe(kind => { 34 | expect(component.onboardingState).toEqual(OnboardingState.ImportWizard); 35 | done(); 36 | }); 37 | component.selectedWallet$.next(WalletKind.Imported); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/app/modules/system-process/pages/logs/logs.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { LogsComponent } from './logs.component'; 4 | import { SharedModule } from '../../../shared/shared.module'; 5 | import { LogsService } from '../../../core/services/logs.service'; 6 | import { MockComponent, MockService } from 'ng-mocks'; 7 | import { of } from 'rxjs'; 8 | import { LogsStreamComponent } from '../../components/logs-stream/logs-stream.component'; 9 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 10 | 11 | describe('LogsComponent', () => { 12 | let component: LogsComponent; 13 | let fixture: ComponentFixture; 14 | let service: LogsService = MockService(LogsService); 15 | 16 | beforeEach(waitForAsync(() => { 17 | TestBed.configureTestingModule({ 18 | imports: [ 19 | SharedModule, 20 | BrowserAnimationsModule 21 | ], 22 | declarations: [ 23 | LogsComponent, 24 | MockComponent(LogsStreamComponent), 25 | ], 26 | providers: [ 27 | { provide: LogsService, useValue: service }, 28 | ] 29 | }) 30 | .compileComponents(); 31 | service = TestBed.inject(LogsService); 32 | })); 33 | 34 | beforeEach(() => { 35 | service.beaconLogs = () => of(''); 36 | service.validatorLogs = () => of(''); 37 | fixture = TestBed.createComponent(LogsComponent); 38 | component = fixture.componentInstance; 39 | fixture.detectChanges(); 40 | }); 41 | 42 | it('should create', () => { 43 | expect(component).toBeTruthy(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/pages/wallet-recover-wizard/templates/mnemonic-form/mnemonic-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { SharedModule } from 'src/app/modules/shared/shared.module'; 6 | 7 | import { MnemonicFormComponent } from './mnemonic-form.component'; 8 | 9 | describe('MnemonicFormComponent', () => { 10 | let component: MnemonicFormComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(waitForAsync(() => { 14 | TestBed.configureTestingModule({ 15 | declarations: [MnemonicFormComponent], 16 | imports: [ 17 | FormsModule, 18 | ReactiveFormsModule, 19 | SharedModule, 20 | BrowserAnimationsModule, 21 | HttpClientTestingModule, 22 | ], 23 | }).compileComponents(); 24 | })); 25 | 26 | beforeEach(() => { 27 | fixture = TestBed.createComponent(MnemonicFormComponent); 28 | component = fixture.componentInstance; 29 | fixture.detectChanges(); 30 | }); 31 | 32 | it('should error if less mnemonic less than 1 account', () => { 33 | component.numAccountsFg.controls.numAccounts.setValue(-1); 34 | const errors = component.numAccountsFg.controls.numAccounts.errors || {}; 35 | expect(errors['min']).toBeTruthy(); 36 | }); 37 | 38 | it('should create', () => { 39 | expect(component).toBeTruthy(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/import-protection/import-protection.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { ImportProtectionComponent } from './import-protection.component'; 4 | import { WalletService } from '../../../core/services/wallet.service'; 5 | import { NotificationService } from '../../services/notification.service'; 6 | import { MockService } from 'ng-mocks'; 7 | import { SharedModule } from '../../shared.module'; 8 | 9 | describe('ImportProtectionComponent', () => { 10 | let component: ImportProtectionComponent; 11 | let fixture: ComponentFixture; 12 | let walletService: WalletService; 13 | let notificationService: NotificationService; 14 | 15 | beforeEach(waitForAsync(() => { 16 | walletService = MockService(WalletService); 17 | notificationService = MockService(NotificationService); 18 | TestBed.configureTestingModule({ 19 | declarations: [ImportProtectionComponent], 20 | imports: [SharedModule], 21 | providers: [ 22 | { provide: WalletService, useValue: walletService }, 23 | { provide: NotificationService, notificationService }, 24 | ], 25 | }).compileComponents(); 26 | walletService = TestBed.inject(WalletService); 27 | notificationService = TestBed.inject(NotificationService); 28 | })); 29 | 30 | beforeEach(() => { 31 | fixture = TestBed.createComponent(ImportProtectionComponent); 32 | component = fixture.componentInstance; 33 | fixture.detectChanges(); 34 | }); 35 | 36 | it('should create', () => { 37 | expect(component).toBeTruthy(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/wallet-help/wallet-help.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | interface HelpPanel { 4 | title: string; 5 | content: string; 6 | } 7 | 8 | @Component({ 9 | selector: 'app-wallet-help', 10 | templateUrl: './wallet-help.component.html', 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | }) 13 | export class WalletHelpComponent { 14 | constructor() { } 15 | items: HelpPanel[] = [ 16 | { 17 | title: 'How are my private keys stored?', 18 | content: ` 19 | Private keys are encrypted using the EIP-2334 keystore standard for BLS-12381 private keys, which is implemented by all eth2 client teams.

The internal representation Prysm uses, however, is quite different. For optimization purposes, we store a single EIP-2335 keystore called all-accounts.keystore.json which stores your private keys encrypted by a strong password.

This file is still compliant with EIP-2335. 20 | `, 21 | }, 22 | { 23 | title: 'Is my wallet password stored?', 24 | content: 'We do not store your wallet password', 25 | }, 26 | { 27 | title: 'How can I recover my wallet?', 28 | content: 'Currently, you cannot recover an HD wallet from the web interface. If you wish to recover your wallet, you can use Prysm from the command line to accomplish this goal. You can see our detailed documentation on recovering HD wallets here', 29 | }, 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/components/choose-wallet-kind/choose-wallet-kind.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Create a Prysm Wallet
4 |
5 | To get started, you will need to create a new wallet in Prysm
to hold 6 | your validator keys 7 |
8 |
9 |
11 |
17 |
18 | 19 |
20 |
21 |

22 | {{selection.name}} 23 |

24 |

25 | {{selection.description}} 26 |

27 |

28 | 33 | 37 |

38 |
39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/components/wallet-directory-form/wallet-directory-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { MockService } from 'ng-mocks'; 5 | import { WalletService } from 'src/app/modules/core/services/wallet.service'; 6 | import { SharedModule } from 'src/app/modules/shared/shared.module'; 7 | 8 | import { WalletDirectoryFormComponent } from './wallet-directory-form.component'; 9 | 10 | describe('WalletDirectoryFormComponent', () => { 11 | let component: WalletDirectoryFormComponent; 12 | let fixture: ComponentFixture; 13 | const service: WalletService = MockService(WalletService); 14 | 15 | beforeEach(waitForAsync(() => { 16 | TestBed.configureTestingModule({ 17 | imports: [ 18 | SharedModule, 19 | BrowserAnimationsModule, 20 | FormsModule, 21 | ReactiveFormsModule, 22 | ], 23 | declarations: [ WalletDirectoryFormComponent ], 24 | providers: [ 25 | { provide: WalletService, useValue: service } 26 | ] 27 | }) 28 | .compileComponents(); 29 | })); 30 | 31 | beforeEach(() => { 32 | fixture = TestBed.createComponent(WalletDirectoryFormComponent); 33 | component = fixture.componentInstance; 34 | component.formGroup = new FormBuilder().group({ 35 | walletDir: ['', Validators.required] 36 | }); 37 | fixture.detectChanges(); 38 | }); 39 | 40 | it('should create', () => { 41 | expect(component).toBeTruthy(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/app/modules/core/components/global-dialog/global-dialog.component.html: -------------------------------------------------------------------------------- 1 |

{{title}}

2 |
3 |

4 | {{content}} 5 |

6 |

7 | For more information and common error solutions, you can look at our Documentation for the web-ui
8 | or create an issue on Github 9 |

10 | 11 | 13 | 14 | 15 | {{alert.title}} 16 | 17 | 18 | {{alert.description}} 19 | 20 | 21 |
22 |

{{alert.message}}

23 |
{{alert.message | json}}
24 |
25 |
26 |
27 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/app/modules/auth/services/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { tap } from 'rxjs/operators'; 5 | import { InitializeAuthResponse } from 'src/app/proto/validator/accounts/v2/web_api'; 6 | import { EnvironmenterService } from '../../core/services/environmenter.service'; 7 | 8 | 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class AuthenticationService { 14 | constructor( 15 | private http: HttpClient, 16 | private environmenter: EnvironmenterService 17 | ) { 18 | } 19 | 20 | shortLivedToken = ''; 21 | private apiUrl = this.environmenter.env.validatorEndpoint; 22 | 23 | private TOKENNAME = 'prysm_access_token'; 24 | private TOKENEXPIRATIONNAME = 'prysm_access_token_expiration'; 25 | 26 | cacheToken(token: string, tokenExpiration: number): void{ 27 | this.clearCachedToken(); 28 | window.localStorage.setItem(this.TOKENNAME, token); 29 | if (tokenExpiration){ 30 | window.localStorage.setItem(this.TOKENEXPIRATIONNAME, tokenExpiration.toString()); 31 | } 32 | } 33 | 34 | clearCachedToken(): void{ 35 | window.localStorage.removeItem(this.TOKENNAME); 36 | window.localStorage.removeItem(this.TOKENEXPIRATIONNAME); 37 | } 38 | 39 | checkHasUsedWeb(): Observable { 40 | return this.http.get(`${this.apiUrl}/initialize`); 41 | } 42 | 43 | getToken(): string | null{ 44 | return window.localStorage.getItem(this.TOKENNAME); 45 | } 46 | 47 | getTokenExpiration(): number | null { 48 | const tokenExpiration = window.localStorage.getItem(this.TOKENEXPIRATIONNAME); 49 | return Number(tokenExpiration); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/app/modules/auth/initialize/initialize.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { LANDING_URL } from '../../core/constants'; 4 | import { AuthenticationService } from '../services/authentication.service'; 5 | 6 | @Component({ 7 | selector: 'app-initialize', 8 | templateUrl: './initialize.component.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class InitializeComponent implements OnInit { 12 | 13 | constructor( 14 | private authenticationService: AuthenticationService, 15 | private router: Router, 16 | private route: ActivatedRoute, 17 | private changeDetectorRef: ChangeDetectorRef 18 | ) { 19 | // override the route reuse strategy 20 | this.router.routeReuseStrategy.shouldReuseRoute = (): boolean => { 21 | return false; 22 | }; 23 | 24 | } 25 | 26 | displayWarning = false; 27 | 28 | ngOnInit(): void { 29 | const accessToken = this.route.snapshot.queryParams['token']; 30 | const accessTokenExpiration = this.route.snapshot.queryParams['expiration']; 31 | // cache the token and token expiration for use 32 | if (accessToken ) { 33 | this.authenticationService.cacheToken(accessToken, accessTokenExpiration); 34 | } 35 | 36 | // redirect users to dashboard if token is already cached 37 | if (this.authenticationService.getToken()){ 38 | console.log('redirecting'); 39 | this.displayWarning = false; 40 | this.router.navigate([LANDING_URL]); 41 | } else { 42 | console.log('Warning: unauthorized'); 43 | this.displayWarning = true; 44 | this.changeDetectorRef.detectChanges(); 45 | } 46 | 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/app/modules/core/services/logs.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, of } from 'rxjs'; 3 | import { concatMap, delay, map, mergeAll } from 'rxjs/operators'; 4 | 5 | import { stream } from '../../core/utils/ndjson'; 6 | import { EnvironmenterService } from '../../core/services/environmenter.service'; 7 | import { mockBeaconLogs, mockValidatorLogs } from 'src/app/modules/core/mocks/logs'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class LogsService { 13 | constructor( 14 | private environmenter: EnvironmenterService, 15 | ) { } 16 | private apiUrl = this.environmenter.env.validatorEndpoint; 17 | 18 | validatorLogs(): Observable { 19 | // Use mock data in development mode. 20 | if (this.environmenter.env.mockInterceptor) { 21 | const data = mockValidatorLogs.split('\n').map((v, _) => v); 22 | return of(data).pipe( 23 | mergeAll(), 24 | concatMap(x => of(x).pipe( 25 | delay(1500), 26 | )) 27 | ); 28 | } 29 | return stream(`${this.apiUrl}/health/logs/validator/stream`).pipe( 30 | map((obj: any) => obj ? obj.logs : ''), 31 | mergeAll(), 32 | ) as Observable; 33 | } 34 | 35 | beaconLogs(): Observable { 36 | // Use mock data in development mode. 37 | if (this.environmenter.env.mockInterceptor) { 38 | const data = mockBeaconLogs.split('\n').map((v, _) => v); 39 | return of(data).pipe( 40 | mergeAll(), 41 | concatMap(x => of(x).pipe( 42 | delay(1500), 43 | )) 44 | ); 45 | } 46 | return stream(`${this.apiUrl}/health/logs/beacon/stream`).pipe( 47 | map((obj: any) => obj ? obj.logs : ''), 48 | mergeAll(), 49 | ) as Observable; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/components/sidebar-expandable-link/sidebar-expandable-link.component.html: -------------------------------------------------------------------------------- 1 | 40 | 41 | -------------------------------------------------------------------------------- /src/app/modules/system-process/pages/metrics/metrics.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { NgxEchartsModule } from 'ngx-echarts'; 4 | 5 | import { SharedModule } from '../../../shared/shared.module'; 6 | import { BalancesChartComponent } from '../../components/balances-chart/balances-chart.component'; 7 | import { ProposedMissedChartComponent } from '../../components/proposed-missed-chart/proposed-missed-chart.component'; 8 | import { DoubleBarChartComponent } from '../../components/double-bar-chart/double-bar-chart.component'; 9 | import { PieChartComponent } from '../../components/pie-chart/pie-chart.component'; 10 | import { MetricsComponent } from './metrics.component'; 11 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 12 | 13 | describe('MetricsComponent', () => { 14 | let component: MetricsComponent; 15 | let fixture: ComponentFixture; 16 | 17 | beforeEach(waitForAsync(() => { 18 | TestBed.configureTestingModule({ 19 | imports: [ 20 | SharedModule, 21 | BrowserAnimationsModule, 22 | NgxEchartsModule.forRoot({ 23 | echarts: () => import('echarts'), 24 | }) 25 | ], 26 | declarations: [ 27 | BalancesChartComponent, 28 | ProposedMissedChartComponent, 29 | DoubleBarChartComponent, 30 | PieChartComponent, 31 | MetricsComponent 32 | ] 33 | }) 34 | .compileComponents(); 35 | })); 36 | 37 | beforeEach(() => { 38 | fixture = TestBed.createComponent(MetricsComponent); 39 | component = fixture.componentInstance; 40 | fixture.detectChanges(); 41 | }); 42 | 43 | it('should create', () => { 44 | expect(component).toBeTruthy(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/modules/wallet/pages/accounts/accounts.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | Your Validator Accounts List 6 |
7 |

8 | Full list of all validating public keys managed by your Prysm wallet 9 |

10 |
11 | 12 | 14 | 15 |
19 | 20 | Filter rows by pubkey, validator index, or name 21 | 26 | search 27 | 28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | 40 |
41 | 46 | 47 |
48 |
49 | -------------------------------------------------------------------------------- /src/app/modules/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { GlobalErrorHandler } from './services/global-error-handler'; 5 | 6 | import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; 7 | import { ENVIRONMENT } from '../../../environments/token'; 8 | import { environment } from '../../../environments/environment'; 9 | import { JwtInterceptor } from './interceptors/jwt.interceptor'; 10 | import { MockInterceptor } from './interceptors/mock.interceptor'; 11 | 12 | import { GlobalDialogComponent } from './components/global-dialog/global-dialog.component'; 13 | 14 | import { GlobalDialogService } from './components/global-dialog/global-dialog.service'; 15 | import { SharedModule } from '../shared/shared.module'; 16 | 17 | const components = [ 18 | GlobalDialogComponent 19 | ]; 20 | 21 | const commonProviders = [ 22 | { 23 | // processes all errors 24 | provide: ErrorHandler, 25 | useClass: GlobalErrorHandler, 26 | }, 27 | GlobalDialogService, 28 | { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, 29 | { provide: ENVIRONMENT, useValue: environment }, 30 | ]; 31 | 32 | const mockProviders = [ 33 | { provide: HTTP_INTERCEPTORS, useClass: MockInterceptor, multi: true } 34 | ]; 35 | 36 | const prysmModules = [ 37 | SharedModule 38 | ]; 39 | 40 | @NgModule({ 41 | declarations: [ 42 | ...components 43 | ], 44 | imports: [ 45 | CommonModule, 46 | HttpClientModule, 47 | ... prysmModules 48 | ], 49 | exports: [ 50 | ...components 51 | ], 52 | providers: [ 53 | ... commonProviders, 54 | ... environment.mockInterceptor ? mockProviders : [] 55 | ], 56 | }) 57 | export class CoreModule {} 58 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/pages/gains-and-losses/gains-and-losses.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 |
8 |
9 | Your Prysm web dashboard 10 |
11 |

12 | Manage your Prysm validator and beacon node, analyze your 13 | validator performance, and control your wallet all from a simple 14 | web interface 15 |

16 |
17 | designer 21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 | 32 |
33 | 34 |

35 | Want to monitor your system process such as logs from your beacon node 36 | and validator? Our logs page has you covered 37 |

38 | 40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /src/app/modules/core/validators/password.validator.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, Validators } from '@angular/forms'; 2 | export class StaticPasswordValidator { 3 | static strongPassword = Validators.pattern( 4 | /(?=.*[A-Za-z])(?=.*\d)(?=.*[^A-Za-z\d]).{8,}/ 5 | ); 6 | static matchingPasswordConfirmation(control: AbstractControl): void { 7 | const password: string = control.get('password')?.value; 8 | const confirmPassword: string = control.get('passwordConfirmation')?.value; 9 | if (password !== confirmPassword) { 10 | control 11 | .get('passwordConfirmation') 12 | ?.setErrors({ passwordMismatch: true }); 13 | } 14 | } 15 | } 16 | // PasswordValidator contains form validation for strong 17 | // password protection in the Prysm web UI. 18 | export class PasswordValidator { 19 | constructor() {} 20 | 21 | errorMessage = { 22 | required: 'Password is required', 23 | minLength: 'Password must be at least 8 characters', 24 | pattern: 'Requires at least 1 letter, number, and special character', 25 | passwordMismatch: 'Passwords do not match', 26 | }; 27 | 28 | // Ensures a password has at least: 29 | // 1 letter (?=.*[A-Za-z]) 30 | // 1 number (?=.*\d) 31 | // 1 of any other character (?=.*[^A-Za-z\d]) 32 | // with at least 8 total characters .{8,} 33 | strongPassword = Validators.pattern( 34 | /(?=.*[A-Za-z])(?=.*\d)(?=.*[^A-Za-z\d]).{8,}/ 35 | ); 36 | 37 | // Ensure password and password confirmation field values match. 38 | matchingPasswordConfirmation(control: AbstractControl): void { 39 | const password: string = control.get('password')?.value; 40 | const confirmPassword: string = control.get('passwordConfirmation')?.value; 41 | if (password !== confirmPassword) { 42 | control 43 | .get('passwordConfirmation') 44 | ?.setErrors({ passwordMismatch: true }); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/modules/system-process/components/double-bar-chart/double-bar-chart.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-double-bar-chart', 5 | templateUrl: './double-bar-chart.component.html', 6 | }) 7 | export class DoubleBarChartComponent { 8 | options = { 9 | grid: { 10 | top: '10%', 11 | bottom: '10%', 12 | // left: '5%', 13 | right: '5%' 14 | }, 15 | legend: { 16 | show: false 17 | }, 18 | color: [ 19 | '#7467ef', 20 | '#ff9e43', 21 | ], 22 | barGap: 0, 23 | barMaxWidth: '64px', 24 | tooltip: {}, 25 | dataset: { 26 | source: [ 27 | ['Month', 'Website', 'App'], 28 | ['Jan', 2200, 1200], 29 | ['Feb', 800, 500], 30 | ['Mar', 700, 1350], 31 | ['Apr', 1500, 1250], 32 | ['May', 2450, 450], 33 | ['June', 1700, 1250] 34 | ] 35 | }, 36 | xAxis: { 37 | type: 'category', 38 | axisLine: { 39 | show: false 40 | }, 41 | splitLine: { 42 | show: false 43 | }, 44 | axisTick: { 45 | show: false 46 | }, 47 | axisLabel: { 48 | color: 'white', 49 | fontSize: 13, 50 | fontFamily: 'roboto' 51 | } 52 | }, 53 | yAxis: { 54 | axisLine: { 55 | show: false 56 | }, 57 | axisTick: { 58 | show: false 59 | }, 60 | splitLine: { 61 | // show: false 62 | lineStyle: { 63 | color: 'white', 64 | opacity: 0.15 65 | } 66 | }, 67 | axisLabel: { 68 | color: 'white', 69 | fontSize: 13, 70 | fontFamily: 'roboto' 71 | } 72 | }, 73 | // Declare several bar series, each will be mapped 74 | // to a column of dataset.source by default. 75 | series: [{ type: 'bar' }, { type: 'bar' }] 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/import-accounts-form/import-accounts-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { NgxFileDropModule } from 'ngx-file-drop'; 6 | import { EnvironmenterService } from 'src/app/modules/core/services/environmenter.service'; 7 | import { SharedModule } from '../../shared.module'; 8 | 9 | import { ImportAccountsFormComponent } from './import-accounts-form.component'; 10 | 11 | describe('ImportAccountsFormComponent', () => { 12 | let component: ImportAccountsFormComponent; 13 | let fixture: ComponentFixture; 14 | const serviceSpy = jasmine.createSpyObj('EnvironmenterService', ['env']); 15 | const fb:FormBuilder = new FormBuilder(); 16 | beforeEach(waitForAsync(() => { 17 | TestBed.configureTestingModule({ 18 | declarations: [ImportAccountsFormComponent], 19 | imports: [ 20 | FormsModule, 21 | ReactiveFormsModule, 22 | SharedModule.forRoot(), 23 | NgxFileDropModule, 24 | BrowserAnimationsModule, 25 | HttpClientTestingModule, 26 | 27 | ], 28 | providers: [ 29 | { provide: EnvironmenterService, useValue: serviceSpy }, 30 | { provide: FormBuilder, useValue: fb }, 31 | ] 32 | }) 33 | .compileComponents(); 34 | })); 35 | 36 | beforeEach(() => { 37 | fixture = TestBed.createComponent(ImportAccountsFormComponent); 38 | component = fixture.componentInstance; 39 | component.formGroup = fb.group({}); 40 | fixture.detectChanges(); 41 | }); 42 | 43 | it('should create', () => { 44 | expect(component).toBeTruthy(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/files-and-directories/files-and-directories.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
Wallet Directory
6 | 11 | help_outline 12 | 13 |
14 |
15 | {{wallet?.wallet_path}} 16 |
17 |
18 |
19 |
20 |
Accounts Keystore File
21 | 26 | help_outline 27 | 28 |
29 |
30 | {{wallet?.wallet_path}}/direct/accounts/all-accounts.keystore.json 31 |
32 |
33 |
34 |
35 |
Encrypted Seed File
36 | 41 | help_outline 42 | 43 |
44 |
45 | {{wallet?.wallet_path}}/derived/seed.encrypted.json 46 |
47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { MockService } from 'ng-mocks'; 5 | import { of } from 'rxjs'; 6 | 7 | import { SharedModule } from '../../modules/shared/shared.module'; 8 | import { SidebarExpandableLinkComponent } from './components/sidebar-expandable-link/sidebar-expandable-link.component'; 9 | import { SidebarComponent } from './components/sidebar/sidebar.component'; 10 | import { DashboardComponent } from './dashboard.component'; 11 | import { BeaconNodeService } from '../core/services/beacon-node.service'; 12 | import { BeaconStatusResponse } from 'src/app/proto/validator/accounts/v2/web_api'; 13 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 14 | 15 | describe('DashboardComponent', () => { 16 | let component: DashboardComponent; 17 | let fixture: ComponentFixture; 18 | const serviceMock = MockService(BeaconNodeService); 19 | (serviceMock as any)['nodeStatusPoll$'] = of({} as BeaconStatusResponse); 20 | 21 | beforeEach(waitForAsync(() => { 22 | TestBed.configureTestingModule({ 23 | imports: [ 24 | RouterTestingModule, 25 | BrowserAnimationsModule, 26 | SharedModule, 27 | ], 28 | providers: [ 29 | { provide: BeaconNodeService, useValue: serviceMock }, 30 | ], 31 | declarations: [ 32 | SidebarExpandableLinkComponent, 33 | SidebarComponent, 34 | DashboardComponent, 35 | ] 36 | }) 37 | .compileComponents(); 38 | })); 39 | 40 | beforeEach(() => { 41 | fixture = TestBed.createComponent(DashboardComponent); 42 | component = fixture.componentInstance; 43 | fixture.detectChanges(); 44 | }); 45 | 46 | it('should create', () => { 47 | expect(component).toBeTruthy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/app/modules/shared/components/import-protection/import-protection.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | Import slashing protection data? (recommended)*
7 | only for previously-used keystores 8 |
9 |
10 |
11 | 12 | Yes 13 | No 14 | 15 |
16 |
17 | 18 |
Upload 19 | your slashing protection file to protect your keystore(s). To read more about 20 | how to obtain this file, see our documentation portal 21 | 24 | here 25 | 26 |
27 | 29 |
30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { SharedModule } from '../../modules/shared/shared.module'; 6 | import { DashboardComponent } from './dashboard.component'; 7 | import { GainsAndLossesComponent } from './pages/gains-and-losses/gains-and-losses.component'; 8 | import { ValidatorPerformanceListComponent } from './components/validator-performance-list/validator-performance-list.component'; 9 | import { SidebarComponent } from './components/sidebar/sidebar.component'; 10 | import { SidebarExpandableLinkComponent } from './components/sidebar-expandable-link/sidebar-expandable-link.component'; 11 | import { BeaconNodeStatusComponent } from './components/beacon-node-status/beacon-node-status.component'; 12 | import { ValidatorPerformanceSummaryComponent } from './components/validator-performance-summary/validator-performance-summary.component'; 13 | import { VersionComponent } from './components/version/version.component'; 14 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 15 | import { LatestGistFeatureComponent } from './components/latest-gist-feature/latest-gist-feature.component'; 16 | import { GitInfoComponent } from './components/latest-gist-feature/templates/git-info/git-info.component'; 17 | 18 | @NgModule({ 19 | declarations: [ 20 | DashboardComponent, 21 | GainsAndLossesComponent, 22 | ValidatorPerformanceListComponent, 23 | SidebarComponent, 24 | SidebarExpandableLinkComponent, 25 | BeaconNodeStatusComponent, 26 | ValidatorPerformanceSummaryComponent, 27 | VersionComponent, 28 | LatestGistFeatureComponent, 29 | GitInfoComponent, 30 | ], 31 | imports: [ 32 | CommonModule, 33 | BrowserAnimationsModule, 34 | SharedModule.forRoot(), 35 | RouterModule 36 | ], 37 | }) 38 | export class DashboardModule {} 39 | -------------------------------------------------------------------------------- /src/app/modules/onboarding/components/generate-mnemonic/generate-mnemonic.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, inject, fakeAsync, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { GenerateMnemonicComponent } from './generate-mnemonic.component'; 4 | import { SharedModule } from 'src/app/modules/shared/shared.module'; 5 | import { WalletService } from 'src/app/modules/core/services/wallet.service'; 6 | import { of } from 'rxjs'; 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | 9 | describe('GenerateMnemonicComponent', () => { 10 | let walletService: WalletService; 11 | const spy = jasmine.createSpyObj('WalletService', ['generateMnemonic$']); 12 | 13 | let component: GenerateMnemonicComponent; 14 | let fixture: ComponentFixture; 15 | 16 | beforeEach(waitForAsync(() => { 17 | TestBed.configureTestingModule({ 18 | declarations: [ GenerateMnemonicComponent ], 19 | imports: [ 20 | SharedModule, 21 | BrowserAnimationsModule 22 | ], 23 | providers: [ 24 | { provide: WalletService, useValue: spy }, 25 | ] 26 | }) 27 | .compileComponents(); 28 | walletService = TestBed.inject(WalletService); 29 | })); 30 | 31 | beforeEach(() => { 32 | fixture = TestBed.createComponent(GenerateMnemonicComponent); 33 | component = fixture.componentInstance; 34 | walletService.generateMnemonic$ = of('hello'); 35 | component.mnemonic$ = walletService.generateMnemonic$; 36 | fixture.detectChanges(); 37 | }); 38 | 39 | it('should create', () => { 40 | expect(component).toBeTruthy(); 41 | }); 42 | 43 | it('should display generated mnemonic from service', fakeAsync(() => { 44 | fixture.whenStable().then(() => { 45 | fixture.detectChanges(); 46 | const text: HTMLElement = fixture.nativeElement; 47 | expect(text.textContent).toContain('hello'); 48 | }); 49 | })); 50 | }); 51 | -------------------------------------------------------------------------------- /src/app/modules/core/services/validator.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | 4 | import { MockService } from 'ng-mocks'; 5 | 6 | import { ValidatorService } from './validator.service'; 7 | import { BeaconNodeService } from './beacon-node.service'; 8 | import { WalletService } from './wallet.service'; 9 | import { Observable, of } from 'rxjs'; 10 | import { Account, ListAccountsResponse } from 'src/app/proto/validator/accounts/v2/web_api'; 11 | import { ENVIRONMENT } from 'src/environments/token'; 12 | 13 | describe('ValidatorService', () => { 14 | let service: ValidatorService = MockService(ValidatorService); 15 | let beaconNodeService: BeaconNodeService = MockService(BeaconNodeService); 16 | let walletService: WalletService = MockService(WalletService); 17 | (beaconNodeService as any)['nodeEndpoint$'] = of('endpoint'); 18 | 19 | beforeEach(() => { 20 | TestBed.configureTestingModule({ 21 | imports: [ 22 | HttpClientTestingModule, 23 | ], 24 | providers: [ 25 | { provide: ENVIRONMENT, useValue: jasmine.createSpyObj('EnvironmenterService', ['env']) }, 26 | { provide: BeaconNodeService, useValue: beaconNodeService }, 27 | { provide: WalletService, useValue: walletService }, 28 | { provide: ValidatorService, useValue: service }, 29 | ] 30 | }); 31 | service = TestBed.inject(ValidatorService); 32 | beaconNodeService = TestBed.inject(BeaconNodeService); 33 | walletService = TestBed.inject(WalletService); 34 | walletService.validatingPublicKeys$ = of([] as string[]); 35 | walletService.accounts = (pageIndex?: number, pageSize?: number): Observable => of({ 36 | accounts: [] as Account[] 37 | } as ListAccountsResponse); 38 | spyOn(walletService, 'accounts').and.returnValue(of({ 39 | accounts: [] as Account[], 40 | } as ListAccountsResponse)); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/app/modules/wallet/components/accounts-form-selection/accounts-form-selection.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 3 | 4 | import { AccountsFormSelectionComponent } from './accounts-form-selection.component'; 5 | import { WalletService } from '../../../core/services/wallet.service'; 6 | import { MockService } from 'ng-mocks'; 7 | import { SharedModule } from '../../../shared/shared.module'; 8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 9 | import { of } from 'rxjs'; 10 | import { Account, ListAccountsResponse } from '../../../../proto/validator/accounts/v2/web_api'; 11 | 12 | describe('AccountsFormSelectionComponent', () => { 13 | let component: AccountsFormSelectionComponent; 14 | let fixture: ComponentFixture; 15 | let walletService: WalletService; 16 | 17 | beforeEach(waitForAsync(() => { 18 | walletService = MockService(WalletService); 19 | walletService.accounts = () => { 20 | return of(({ 21 | accounts: [] as Account[], 22 | } as unknown) as ListAccountsResponse); 23 | }; 24 | TestBed.configureTestingModule({ 25 | declarations: [AccountsFormSelectionComponent], 26 | providers: [ 27 | { 28 | provide: WalletService, 29 | useValue: walletService, 30 | }, 31 | ], 32 | imports: [ 33 | SharedModule, 34 | FormsModule, 35 | ReactiveFormsModule, 36 | BrowserAnimationsModule, 37 | ], 38 | }).compileComponents(); 39 | walletService = TestBed.inject(WalletService); 40 | })); 41 | 42 | beforeEach(() => { 43 | fixture = TestBed.createComponent(AccountsFormSelectionComponent); 44 | component = fixture.componentInstance; 45 | fixture.detectChanges(); 46 | }); 47 | 48 | it('should create', () => { 49 | expect(component).toBeTruthy(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/app/modules/system-process/pages/logs/logs.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | Your Prysm Process' Logs 6 |
7 |

8 | Stream of logs from both the beacon node and validator client 9 |

10 |
11 |
12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 |
Log Percentages
20 |
21 |
22 | 23 |
24 |
25 | {{metrics.percentInfo}}% Info Logs 26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 | {{metrics.percentWarn}}% Warn Logs 34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 | {{metrics.percentError}}% Error Logs 42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------