From 18a3c6155981e9ca4badbe9a2fbf97dd5d5a6f28 Mon Sep 17 00:00:00 2001
From: pablomendezroyo
Date: Mon, 3 Feb 2025 12:23:46 +0100
Subject: [PATCH 01/19] feat: dappnode features
---
assets/icons/down-arrow.svg | 6 +
assets/icons/eye-off.svg | 7 +
assets/icons/eye-on.svg | 4 +
consts/csm-constants.ts | 6 +
consts/urls.ts | 2 +
dappnode/components/modal.tsx | 85 ++++
dappnode/components/text-wrappers.ts | 16 +
dappnode/fallbacks/ec-no-logs-page.tsx | 29 ++
dappnode/fallbacks/ec-not-installed-page.tsx | 27 ++
dappnode/fallbacks/ec-scanning-events.tsx | 23 +
dappnode/fallbacks/ec-syncing-page.tsx | 23 +
.../fallbacks/welcome-section-component.tsx | 31 ++
dappnode/hooks/use-brain-keystore-api.ts | 45 ++
dappnode/hooks/use-brain-launchpad-api.ts | 72 +++
dappnode/hooks/use-check-deposit-keys.ts | 51 +++
dappnode/hooks/use-dappnode-urls.ts | 99 ++++
dappnode/hooks/use-ec-sanity-check.ts | 99 ++++
...use-exit-requested-keys-from-events-api.ts | 73 +++
dappnode/hooks/use-get-exit-requests.ts | 49 ++
dappnode/hooks/use-get-infra-status.ts | 114 +++++
dappnode/hooks/use-get-next-report.ts | 28 ++
.../hooks/use-get-operator-performance.ts | 62 +++
dappnode/hooks/use-get-pending-reports.ts | 50 +++
.../hooks/use-get-performance-by-range.ts | 181 ++++++++
dappnode/hooks/use-get-relays-data.ts | 161 +++++++
dappnode/hooks/use-get-telegram-data.ts | 43 ++
.../hooks/use-invites-events-fetcher-api.ts | 155 +++++++
dappnode/hooks/use-missing-keys.ts | 96 ++++
...-node-operators-fetcher-from-events-api.ts | 182 ++++++++
...use-node-operators-with-locked-bond-api.ts | 114 +++++
dappnode/hooks/use-post-telegram-data.ts | 64 +++
...e-withdrawn-key-indexes-from-events-api.ts | 84 ++++
.../import-keys/import-keys-confirm-modal.tsx | 46 ++
dappnode/import-keys/keys-input-form.tsx | 85 ++++
dappnode/import-keys/password-input.tsx | 33 ++
dappnode/import-keys/styles.ts | 42 ++
dappnode/import-keys/use-keystore-drop.ts | 79 ++++
dappnode/notifications/input-tg.tsx | 71 +++
.../notifications/notifications-component.tsx | 175 ++++++++
.../notifications/notifications-modal.tsx | 46 ++
dappnode/notifications/notifications-page.tsx | 17 +
.../notifications-setup-steps.tsx | 39 ++
.../notifications/notifications-types.tsx | 87 ++++
dappnode/notifications/styles.ts | 39 ++
.../components/performance-card.tsx | 37 ++
.../components/performance-table.tsx | 80 ++++
.../performance/components/range-selector.tsx | 49 ++
dappnode/performance/components/styles.ts | 227 ++++++++++
dappnode/performance/index.ts | 1 +
.../performance/performance-cards-section.tsx | 47 ++
.../performance/performance-chart-section.tsx | 261 +++++++++++
dappnode/performance/performance-page.tsx | 49 ++
.../performance/performance-table-section.tsx | 33 ++
dappnode/performance/types.ts | 8 +
dappnode/starter-pack/step-wrapper.tsx | 14 +
dappnode/starter-pack/steps.tsx | 424 ++++++++++++++++++
dappnode/starter-pack/styles.ts | 143 ++++++
dappnode/status/InfraItem.tsx | 48 ++
dappnode/status/import-keys-warning-modal.tsx | 44 ++
dappnode/status/status-section.tsx | 80 ++++
dappnode/status/styles.tsx | 84 ++++
dappnode/status/types.ts | 15 +
dappnode/status/warnings.tsx | 226 ++++++++++
dappnode/utils/capitalize-first-char.ts | 3 +
dappnode/utils/dappnode-docs-urls.ts | 12 +
dappnode/utils/is-tg-bot-token.ts | 4 +
dappnode/utils/is-tg-user-id.ts | 4 +
dappnode/utils/sanitize-urls.ts | 26 ++
.../context/add-keys-form-provider.tsx | 25 +-
features/add-keys/add-keys/context/types.ts | 8 +
.../add-keys/add-keys/controls/keys-input.tsx | 12 +
.../context/submit-keys-form-provider.tsx | 24 +-
.../submit-keys-form/context/types.ts | 3 +
.../submit-keys-form/controls/keys-input.tsx | 12 +
.../locked-section/locked-section.tsx | 4 +-
features/welcome/try-csm/try-csm.tsx | 14 +
.../welcome-section/welcome-section.tsx | 1 -
global.d.ts | 3 +
package.json | 2 +
.../use-get-active-node-operator.ts | 34 ++
shared/hooks/use-csm-node-operators.ts | 7 +-
shared/hooks/use-invites.ts | 4 +-
shared/hooks/use-keys-with-status.ts | 7 +-
shared/navigate/gates/gate-loaded.tsx | 42 +-
yarn.lock | 218 ++++++++-
85 files changed, 5160 insertions(+), 19 deletions(-)
create mode 100644 assets/icons/down-arrow.svg
create mode 100644 assets/icons/eye-off.svg
create mode 100644 assets/icons/eye-on.svg
create mode 100644 dappnode/components/modal.tsx
create mode 100644 dappnode/components/text-wrappers.ts
create mode 100644 dappnode/fallbacks/ec-no-logs-page.tsx
create mode 100644 dappnode/fallbacks/ec-not-installed-page.tsx
create mode 100644 dappnode/fallbacks/ec-scanning-events.tsx
create mode 100644 dappnode/fallbacks/ec-syncing-page.tsx
create mode 100644 dappnode/fallbacks/welcome-section-component.tsx
create mode 100644 dappnode/hooks/use-brain-keystore-api.ts
create mode 100644 dappnode/hooks/use-brain-launchpad-api.ts
create mode 100644 dappnode/hooks/use-check-deposit-keys.ts
create mode 100644 dappnode/hooks/use-dappnode-urls.ts
create mode 100644 dappnode/hooks/use-ec-sanity-check.ts
create mode 100644 dappnode/hooks/use-exit-requested-keys-from-events-api.ts
create mode 100644 dappnode/hooks/use-get-exit-requests.ts
create mode 100644 dappnode/hooks/use-get-infra-status.ts
create mode 100644 dappnode/hooks/use-get-next-report.ts
create mode 100644 dappnode/hooks/use-get-operator-performance.ts
create mode 100644 dappnode/hooks/use-get-pending-reports.ts
create mode 100644 dappnode/hooks/use-get-performance-by-range.ts
create mode 100644 dappnode/hooks/use-get-relays-data.ts
create mode 100644 dappnode/hooks/use-get-telegram-data.ts
create mode 100644 dappnode/hooks/use-invites-events-fetcher-api.ts
create mode 100644 dappnode/hooks/use-missing-keys.ts
create mode 100644 dappnode/hooks/use-node-operators-fetcher-from-events-api.ts
create mode 100644 dappnode/hooks/use-node-operators-with-locked-bond-api.ts
create mode 100644 dappnode/hooks/use-post-telegram-data.ts
create mode 100644 dappnode/hooks/use-withdrawn-key-indexes-from-events-api.ts
create mode 100644 dappnode/import-keys/import-keys-confirm-modal.tsx
create mode 100644 dappnode/import-keys/keys-input-form.tsx
create mode 100644 dappnode/import-keys/password-input.tsx
create mode 100644 dappnode/import-keys/styles.ts
create mode 100644 dappnode/import-keys/use-keystore-drop.ts
create mode 100644 dappnode/notifications/input-tg.tsx
create mode 100644 dappnode/notifications/notifications-component.tsx
create mode 100644 dappnode/notifications/notifications-modal.tsx
create mode 100644 dappnode/notifications/notifications-page.tsx
create mode 100644 dappnode/notifications/notifications-setup-steps.tsx
create mode 100644 dappnode/notifications/notifications-types.tsx
create mode 100644 dappnode/notifications/styles.ts
create mode 100644 dappnode/performance/components/performance-card.tsx
create mode 100644 dappnode/performance/components/performance-table.tsx
create mode 100644 dappnode/performance/components/range-selector.tsx
create mode 100644 dappnode/performance/components/styles.ts
create mode 100644 dappnode/performance/index.ts
create mode 100644 dappnode/performance/performance-cards-section.tsx
create mode 100644 dappnode/performance/performance-chart-section.tsx
create mode 100644 dappnode/performance/performance-page.tsx
create mode 100644 dappnode/performance/performance-table-section.tsx
create mode 100644 dappnode/performance/types.ts
create mode 100644 dappnode/starter-pack/step-wrapper.tsx
create mode 100644 dappnode/starter-pack/steps.tsx
create mode 100644 dappnode/starter-pack/styles.ts
create mode 100644 dappnode/status/InfraItem.tsx
create mode 100644 dappnode/status/import-keys-warning-modal.tsx
create mode 100644 dappnode/status/status-section.tsx
create mode 100644 dappnode/status/styles.tsx
create mode 100644 dappnode/status/types.ts
create mode 100644 dappnode/status/warnings.tsx
create mode 100644 dappnode/utils/capitalize-first-char.ts
create mode 100644 dappnode/utils/dappnode-docs-urls.ts
create mode 100644 dappnode/utils/is-tg-bot-token.ts
create mode 100644 dappnode/utils/is-tg-user-id.ts
create mode 100644 dappnode/utils/sanitize-urls.ts
diff --git a/assets/icons/down-arrow.svg b/assets/icons/down-arrow.svg
new file mode 100644
index 00000000..4ba9c987
--- /dev/null
+++ b/assets/icons/down-arrow.svg
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/assets/icons/eye-off.svg b/assets/icons/eye-off.svg
new file mode 100644
index 00000000..a390ff06
--- /dev/null
+++ b/assets/icons/eye-off.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/icons/eye-on.svg b/assets/icons/eye-on.svg
new file mode 100644
index 00000000..844a3ce4
--- /dev/null
+++ b/assets/icons/eye-on.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/consts/csm-constants.ts b/consts/csm-constants.ts
index 744483ae..3dc4a709 100644
--- a/consts/csm-constants.ts
+++ b/consts/csm-constants.ts
@@ -21,6 +21,8 @@ type CsmConstants = {
stakingModuleId: number;
withdrawalCredentials: Address;
retentionPeriodMins: number;
+ lidoFeeRecipient: Address; // DAPPNODE
+ reportTimestamp: number; // DAPPNODE, the timestamp from an epoch where a report was distributed
};
export const CONSTANTS_BY_NETWORK: Partial> = {
@@ -39,6 +41,8 @@ export const CONSTANTS_BY_NETWORK: Partial> = {
stakingModuleId: 3,
withdrawalCredentials: '0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f',
retentionPeriodMins: 80_640, // 8 weeks
+ lidoFeeRecipient: '0x388C818CA8B9251b393131C08a736A67ccB19297', // DAPPNODE
+ reportTimestamp: 1732282199, // DAPPNODE, epoch 326714
},
[CHAINS.Holesky]: {
contracts: {
@@ -55,6 +59,8 @@ export const CONSTANTS_BY_NETWORK: Partial> = {
stakingModuleId: 4,
withdrawalCredentials: '0xF0179dEC45a37423EAD4FaD5fCb136197872EAd9',
retentionPeriodMins: 80_640, // 8 weeks
+ lidoFeeRecipient: '0xE73a3602b99f1f913e72F8bdcBC235e206794Ac8', // DAPPNODE
+ reportTimestamp: 1734371136, // DAPPNODE, epoch 100179
},
};
diff --git a/consts/urls.ts b/consts/urls.ts
index bae3eb6d..4818c793 100644
--- a/consts/urls.ts
+++ b/consts/urls.ts
@@ -13,10 +13,12 @@ export const PATH = {
BOND_CLAIM: '/bond/claim',
BOND_ADD: '/bond/add',
BOND_UNLOCK: '/bond/unlock',
+ PERFORMANCE: '/performance', // DAPPNODE
ROLES: '/roles',
ROLES_REWARDS: '/roles/reward-address',
ROLES_MANAGER: '/roles/manager-address',
ROLES_INBOX: '/roles/inbox',
+ NOTIFICATIONS: '/notifications', // DAPPNODE
STEALING: '/stealing',
STEALING_REPORT: '/stealing/report',
STEALING_CANCEL: '/stealing/cancel',
diff --git a/dappnode/components/modal.tsx b/dappnode/components/modal.tsx
new file mode 100644
index 00000000..ab49ba92
--- /dev/null
+++ b/dappnode/components/modal.tsx
@@ -0,0 +1,85 @@
+import React, { ReactNode } from 'react';
+import styled from 'styled-components';
+
+interface ModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ children: ReactNode;
+ closeOnOverlayClick?: boolean;
+}
+
+const Overlay = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+`;
+
+const Card = styled.div`
+ position: relative;
+ background: var(--lido-color-foreground);
+ color: var(--lido-color-textSecondary);
+ padding: 1.5rem;
+ border-radius: 0.5rem;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ z-index: 1001;
+ max-width: 33rem;
+`;
+
+const CloseButton = styled.button`
+ position: absolute;
+ top: 0.5rem; /* Adjust to fine-tune positioning */
+ right: 0.5rem; /* Adjust to fine-tune positioning */
+ background: none;
+ border: none;
+ color: aliceblue;
+ font-weight: bold;
+ cursor: pointer;
+ font-size: 1.25rem;
+`;
+
+const ChildreWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ text-align: left;
+ > h3 {
+ font-size: 18px;
+ }
+ > div,
+ > p {
+ font-size: 14px;
+ }
+`;
+
+export const LinkWrapper = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+`;
+
+const Modal: React.FC = ({
+ isOpen,
+ onClose,
+ children,
+ closeOnOverlayClick = true,
+}) => {
+ if (!isOpen) return null;
+
+ return (
+
+ e.stopPropagation()}>
+ ×
+ {children}
+
+
+ );
+};
+
+export default Modal;
diff --git a/dappnode/components/text-wrappers.ts b/dappnode/components/text-wrappers.ts
new file mode 100644
index 00000000..89c199a6
--- /dev/null
+++ b/dappnode/components/text-wrappers.ts
@@ -0,0 +1,16 @@
+import styled from 'styled-components';
+
+export const ErrorWrapper = styled.div`
+ color: var(--lido-color-error);
+ font-weight: 600;
+`;
+
+export const WarningWrapper = styled.div`
+ color: var(--lido-color-warning);
+ font-weight: 600;
+`;
+
+export const SuccessWrapper = styled.div`
+ color: var(--lido-color-success);
+ font-weight: 600;
+`;
diff --git a/dappnode/fallbacks/ec-no-logs-page.tsx b/dappnode/fallbacks/ec-no-logs-page.tsx
new file mode 100644
index 00000000..91bf1316
--- /dev/null
+++ b/dappnode/fallbacks/ec-no-logs-page.tsx
@@ -0,0 +1,29 @@
+import { Link } from '@lidofinance/lido-ui';
+import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls';
+import { FC } from 'react';
+import { Layout } from 'shared/layout';
+import { WelcomeSection } from './welcome-section-component';
+import { ErrorWrapper } from 'dappnode/components/text-wrappers';
+import { Note } from 'shared/components';
+
+export const ECNoLogsPage: FC = () => {
+ const { stakersUiUrl } = useDappnodeUrls();
+ return (
+
+
+
+ Execution Client not storing logs
+
+ Your Execution Client is not configured to store log receipts.
+
+ Please either enable log receipt storage on your current client or
+ switch to an Execution Client that supports this feature by default.
+
+ Switch your Execution client{' '}
+
+ Clients like Besu, Geth, and Nethermind store log receipts by default.
+
+ {' '}
+
+ );
+};
diff --git a/dappnode/fallbacks/ec-not-installed-page.tsx b/dappnode/fallbacks/ec-not-installed-page.tsx
new file mode 100644
index 00000000..3df0db9f
--- /dev/null
+++ b/dappnode/fallbacks/ec-not-installed-page.tsx
@@ -0,0 +1,27 @@
+import { Link } from '@lidofinance/lido-ui';
+import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls';
+import { FC } from 'react';
+import { Layout } from 'shared/layout';
+import { WelcomeSection } from './welcome-section-component';
+import { ErrorWrapper } from 'dappnode/components/text-wrappers';
+
+export const ECNotInstalledPage: FC = () => {
+ const { stakersUiUrl } = useDappnodeUrls();
+
+ return (
+
+
+
+ Execution client is not installed
+
+
+ This UI requires an execution client synced to function properly.{' '}
+
+ Please select and Execution client and wait until it's synced
+ before continuing.
+
+ Set up your node
+
+
+ );
+};
diff --git a/dappnode/fallbacks/ec-scanning-events.tsx b/dappnode/fallbacks/ec-scanning-events.tsx
new file mode 100644
index 00000000..85d6801d
--- /dev/null
+++ b/dappnode/fallbacks/ec-scanning-events.tsx
@@ -0,0 +1,23 @@
+import { FC } from 'react';
+import { Layout } from 'shared/layout';
+import { WelcomeSection } from './welcome-section-component';
+import { WarningWrapper } from 'dappnode/components/text-wrappers';
+import { LoaderBanner } from 'shared/navigate/splash/loader-banner';
+
+export const ECScanningPage: FC = () => {
+ return (
+
+
+
+ Execution client scanning blocks
+
+ To retrieve data, this UI needs to scan the blockchain events.
+
+ This may take up to 10 minutes during your first login, depending on
+ your execution client.
+
+
+
+
+ );
+};
diff --git a/dappnode/fallbacks/ec-syncing-page.tsx b/dappnode/fallbacks/ec-syncing-page.tsx
new file mode 100644
index 00000000..ac9589b3
--- /dev/null
+++ b/dappnode/fallbacks/ec-syncing-page.tsx
@@ -0,0 +1,23 @@
+import { FC } from 'react';
+import { Layout } from 'shared/layout';
+import { WelcomeSection } from './welcome-section-component';
+import { WarningWrapper } from 'dappnode/components/text-wrappers';
+import { LoaderBanner } from 'shared/navigate/splash/loader-banner';
+
+export const ECSyncingPage: FC = () => {
+ return (
+
+
+
+ Execution client is syncing
+
+
+ This UI requires an execution client synced to function properly.{' '}
+
+ Please, Wait until it's synced before continuing.
+
+
+
+
+ );
+};
diff --git a/dappnode/fallbacks/welcome-section-component.tsx b/dappnode/fallbacks/welcome-section-component.tsx
new file mode 100644
index 00000000..529ebb5b
--- /dev/null
+++ b/dappnode/fallbacks/welcome-section-component.tsx
@@ -0,0 +1,31 @@
+import {
+ BlockStyled,
+ CSMLogo,
+ Header,
+ Heading,
+} from 'features/welcome/welcome-section/styles';
+import { FC, PropsWithChildren } from 'react';
+import styled from 'styled-components';
+
+const ContentWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spaceMap.md}px;
+
+ text-align: center;
+ color: var(--lido-color-text);
+ font-size: ${({ theme }) => theme.fontSizesMap.xs}px;
+ line-height: ${({ theme }) => theme.fontSizesMap.xl}px;
+`;
+
+export const WelcomeSection: FC = ({ children }) => {
+ return (
+
+
+
+
+
+ {children}
+
+ );
+};
diff --git a/dappnode/hooks/use-brain-keystore-api.ts b/dappnode/hooks/use-brain-keystore-api.ts
new file mode 100644
index 00000000..bf12443d
--- /dev/null
+++ b/dappnode/hooks/use-brain-keystore-api.ts
@@ -0,0 +1,45 @@
+import { useState, useEffect, useCallback } from 'react';
+import useDappnodeUrls from './use-dappnode-urls';
+
+const useApiBrain = (interval = 60000) => {
+ const { brainKeysUrl } = useDappnodeUrls();
+
+ const [pubkeys, setPubkeys] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState();
+
+ const fetchPubkeys = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(brainKeysUrl, { method: 'GET' });
+
+ if (response.ok) {
+ const data = await response.json();
+ setPubkeys(data.lido || []);
+ setError(undefined);
+ } else {
+ console.error('Error fetching brain keys:', response);
+ setError(response);
+ }
+ } catch (e) {
+ console.error(e);
+ setError(e);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [brainKeysUrl]);
+
+ useEffect(() => {
+ void fetchPubkeys();
+
+ const intervalId = setInterval(() => {
+ void fetchPubkeys();
+ }, interval);
+
+ return () => clearInterval(intervalId);
+ }, [fetchPubkeys, interval]);
+
+ return { pubkeys, isLoading, error };
+};
+
+export default useApiBrain;
diff --git a/dappnode/hooks/use-brain-launchpad-api.ts b/dappnode/hooks/use-brain-launchpad-api.ts
new file mode 100644
index 00000000..e4661cbf
--- /dev/null
+++ b/dappnode/hooks/use-brain-launchpad-api.ts
@@ -0,0 +1,72 @@
+import { useState, useCallback } from 'react';
+import useDappnodeUrls from './use-dappnode-urls';
+import { CONSTANTS_BY_NETWORK } from 'consts/csm-constants';
+import { useChainId } from 'wagmi';
+
+const useBrainLaunchpadApi = () => {
+ const { brainLaunchpadUrl } = useDappnodeUrls();
+ const [isLoading, setIsLoading] = useState(false);
+ const [isSuccess, setIsSuccess] = useState(false);
+ const [error, setError] = useState(undefined);
+
+ const chainId = useChainId() as keyof typeof CONSTANTS_BY_NETWORK;
+
+ const lidoFeeRecipient =
+ CONSTANTS_BY_NETWORK[chainId]?.lidoFeeRecipient ?? '';
+
+ const submitKeystores = useCallback(
+ async ({
+ keystores,
+ password,
+ }: {
+ keystores: object[];
+ password: string;
+ }) => {
+ setIsLoading(true);
+ setIsSuccess(false);
+ setError(undefined);
+
+ // Push same pass and tag for every entry in keystores
+ const passwords: string[] = [];
+ const tags: string[] = [];
+ const feeRecipients: string[] = [];
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ for (const keystore of keystores) {
+ passwords.push(password);
+ tags.push('lido');
+ feeRecipients.push(lidoFeeRecipient);
+ }
+
+ const stringifiedKeystores = keystores.map((keystore) =>
+ JSON.stringify(keystore),
+ );
+
+ const keystoresData = {
+ keystores: stringifiedKeystores,
+ passwords,
+ tags,
+ feeRecipients,
+ };
+
+ try {
+ const response = await fetch(`${brainLaunchpadUrl}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json; charset=UTF-8' },
+ body: JSON.stringify(keystoresData),
+ });
+
+ if (response.ok) setIsSuccess(true);
+ } catch (error) {
+ console.error(error);
+ setError(error as Error);
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [brainLaunchpadUrl, lidoFeeRecipient],
+ );
+
+ return { submitKeystores, isLoading, isSuccess, error };
+};
+
+export default useBrainLaunchpadApi;
diff --git a/dappnode/hooks/use-check-deposit-keys.ts b/dappnode/hooks/use-check-deposit-keys.ts
new file mode 100644
index 00000000..74515706
--- /dev/null
+++ b/dappnode/hooks/use-check-deposit-keys.ts
@@ -0,0 +1,51 @@
+import { useEffect, useState } from 'react';
+import { DepositData } from 'types';
+import useApiBrain from './use-brain-keystore-api';
+
+const useCheckImportedDepositKeys = (depositData: DepositData[]) => {
+ const { pubkeys: brainKeys, isLoading: brainKeysLoading } = useApiBrain();
+
+ const [keysLoading, setKeysLoading] = useState(false);
+
+ const [keysInDeposit, setKeysInDeposit] = useState([]);
+ const [missingKeys, setMissingKeys] = useState([]);
+
+ useEffect(() => {
+ const keys = [];
+ for (const key of depositData) {
+ keys.push(key.pubkey);
+ }
+ setKeysInDeposit(keys);
+ }, [depositData]);
+
+ useEffect(() => {
+ if (brainKeysLoading) {
+ setKeysLoading(true);
+ } else {
+ setKeysLoading(false);
+ }
+ }, [brainKeysLoading]);
+
+ // Filtering lido keys by status and brain imported
+ useEffect(() => {
+ const missingEntries: string[] = [];
+ if (brainKeys) {
+ const formattedBrainKeys = brainKeys.map((key) => key.toLowerCase());
+
+ for (const key of keysInDeposit) {
+ if (!formattedBrainKeys.includes('0x' + key)) {
+ missingEntries.push(key);
+ }
+ }
+ } else {
+ for (const key of keysInDeposit) {
+ missingEntries.push(key);
+ }
+ }
+ setMissingKeys(missingEntries);
+ }, [keysInDeposit, brainKeys]);
+
+ return { missingKeys, keysLoading };
+};
+
+export default useCheckImportedDepositKeys;
diff --git a/dappnode/hooks/use-dappnode-urls.ts b/dappnode/hooks/use-dappnode-urls.ts
new file mode 100644
index 00000000..e4eab30a
--- /dev/null
+++ b/dappnode/hooks/use-dappnode-urls.ts
@@ -0,0 +1,99 @@
+import { CHAINS } from '@lido-sdk/constants';
+import { useAccount } from 'shared/hooks';
+
+interface DappnodeUrls {
+ brainUrl: string;
+ brainKeysUrl: string;
+ brainLaunchpadUrl: string;
+ signerUrl: string;
+ sentinelUrl: string;
+ stakersUiUrl: string;
+ backendUrl: string;
+ ECApiUrl: string;
+ CCVersionApiUrl: string;
+ CCStatusApiUrl: string;
+ keysStatusUrl: string;
+ installerTabUrl: string;
+ MEVApiUrl: string;
+ MEVPackageConfig: string;
+}
+
+const useDappnodeUrls = () => {
+ const { chainId } = useAccount();
+
+ const urlsByChain: Partial> = {
+ [CHAINS.Mainnet]: {
+ brainUrl: 'http://brain.web3signer.dappnode',
+ brainKeysUrl: '/api/brain-keys-mainnet',
+ brainLaunchpadUrl: '/api/brain-launchpad-mainnet',
+ signerUrl: 'http://web3signer.web3signer.dappnode',
+ sentinelUrl: 'https://t.me/CSMSentinel_bot',
+ stakersUiUrl: 'http://my.dappnode/stakers/ethereum',
+ backendUrl: 'http://lido-events.lido-csm-mainnet.dappnode:8080',
+ ECApiUrl: 'http://execution.mainnet.dncore.dappnode:8545',
+ CCVersionApiUrl: '/api/consensus-version-mainnet',
+ CCStatusApiUrl: '/api/consensus-status-mainnet',
+ keysStatusUrl: '/api/keys-status-mainnet',
+ installerTabUrl:
+ 'http://my.dappnode/installer/dnp/lido-csm-holesky.dnp.dappnode.eth',
+ MEVApiUrl: '/api/mev-status-mainnet',
+ MEVPackageConfig:
+ 'http://my.dappnode/packages/my/mev-boost.dnp.dappnode.eth/config',
+ },
+ [CHAINS.Holesky]: {
+ brainUrl: 'http://brain.web3signer-holesky.dappnode',
+ brainKeysUrl: '/api/brain-keys-holesky',
+ brainLaunchpadUrl: '/api/brain-launchpad-holesky',
+ signerUrl: 'http://web3signer.web3signer-holesky.dappnode',
+ sentinelUrl: 'https://t.me/CSMSentinelHolesky_bot',
+ stakersUiUrl: 'http://my.dappnode/stakers/holesky',
+ backendUrl: 'http://lido-events.lido-csm-holesky.dappnode:8080',
+ ECApiUrl: 'http://execution.holesky.dncore.dappnode:8545',
+ CCVersionApiUrl: '/api/consensus-version-holesky',
+ CCStatusApiUrl: '/api/consensus-status-holesky',
+ keysStatusUrl: '/api/keys-status-holesky',
+ installerTabUrl:
+ 'http://my.dappnode/installer/dnp/lido-csm-mainnet.dnp.dappnode.eth',
+ MEVApiUrl: '/api/mev-status-holesky',
+ MEVPackageConfig:
+ 'http://my.dappnode/packages/my/mev-boost-holesky.dnp.dappnode.eth/config',
+ },
+ };
+
+ const brainUrl = urlsByChain[chainId as CHAINS]?.brainUrl || '';
+ const brainKeysUrl = urlsByChain[chainId as CHAINS]?.brainKeysUrl || '';
+ const brainLaunchpadUrl =
+ urlsByChain[chainId as CHAINS]?.brainLaunchpadUrl || '';
+ const signerUrl = urlsByChain[chainId as CHAINS]?.signerUrl || '';
+ const sentinelUrl = urlsByChain[chainId as CHAINS]?.sentinelUrl || '';
+ const stakersUiUrl = urlsByChain[chainId as CHAINS]?.stakersUiUrl || '';
+ const backendUrl = urlsByChain[chainId as CHAINS]?.backendUrl || '';
+ const ECApiUrl = urlsByChain[chainId as CHAINS]?.ECApiUrl || '';
+ const CCVersionApiUrl = urlsByChain[chainId as CHAINS]?.CCVersionApiUrl || '';
+ const CCStatusApiUrl = urlsByChain[chainId as CHAINS]?.CCStatusApiUrl || '';
+ const keysStatusUrl = urlsByChain[chainId as CHAINS]?.keysStatusUrl || '';
+ const installerTabUrl = (isMainnet: boolean) =>
+ urlsByChain[isMainnet ? 1 : 17000]?.installerTabUrl;
+ const MEVApiUrl = urlsByChain[chainId as CHAINS]?.MEVApiUrl || '';
+ const MEVPackageConfig =
+ urlsByChain[chainId as CHAINS]?.MEVPackageConfig || '';
+
+ return {
+ brainUrl,
+ brainKeysUrl,
+ brainLaunchpadUrl,
+ signerUrl,
+ sentinelUrl,
+ stakersUiUrl,
+ backendUrl,
+ ECApiUrl,
+ CCVersionApiUrl,
+ CCStatusApiUrl,
+ keysStatusUrl,
+ installerTabUrl,
+ MEVApiUrl,
+ MEVPackageConfig,
+ };
+};
+
+export default useDappnodeUrls;
diff --git a/dappnode/hooks/use-ec-sanity-check.ts b/dappnode/hooks/use-ec-sanity-check.ts
new file mode 100644
index 00000000..53cdf3ef
--- /dev/null
+++ b/dappnode/hooks/use-ec-sanity-check.ts
@@ -0,0 +1,99 @@
+import { CHAINS } from '@lido-sdk/constants';
+import getConfig from 'next/config';
+import { useEffect, useMemo, useState } from 'react';
+
+export const useECSanityCheck = () => {
+ const [isInstalled, setIsInstalled] = useState(false);
+ const [isSynced, setIsSynced] = useState(false);
+ const [hasLogs, setHasLogs] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+ const { publicRuntimeConfig } = getConfig();
+
+ const chainId = publicRuntimeConfig.defaultChain;
+
+ const contractTx = useMemo(
+ () => ({
+ [CHAINS.Mainnet]: `0xf5330dbcf09885ed145c4435e356b5d8a10054751bb8009d3a2605d476ac173f`,
+ [CHAINS.Holesky]: `0x1475719ecbb73b28bc531bb54b37695df1bf6b71c6d2bf1d28b4efa404867e26`,
+ }),
+ [],
+ );
+
+ const rpcUrl =
+ chainId == 1
+ ? publicRuntimeConfig.rpcUrls_1
+ : publicRuntimeConfig.rpcUrls_17000;
+
+ useEffect(() => {
+ const getSyncStatus = async () => {
+ try {
+ setIsLoading(true);
+ const syncResponse = await fetch(`${rpcUrl}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_syncing',
+ params: [],
+ id: 0,
+ }),
+ });
+ if (!syncResponse.ok) {
+ chainId;
+ throw new Error(`HTTP error! Status: ${syncResponse.status}`);
+ }
+
+ const syncData = await syncResponse.json();
+
+ setIsInstalled(true);
+ setIsSynced(syncData.result ? false : true);
+ setIsLoading(false);
+ } catch (e) {
+ console.error(`Error getting EC data: ${e}`);
+ setIsInstalled(false);
+ setIsLoading(false);
+ }
+ };
+
+ void getSyncStatus();
+ }, [chainId, rpcUrl]);
+
+ useEffect(() => {
+ const getTxStatus = async () => {
+ try {
+ setIsLoading(true);
+
+ const txResponse = await fetch(`${rpcUrl}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_getTransactionReceipt',
+ params: [contractTx[chainId as keyof typeof contractTx]],
+ id: 0,
+ }),
+ });
+
+ if (!txResponse.ok) {
+ throw new Error(`HTTP error! Status: ${txResponse.status}`);
+ }
+
+ const txData = await txResponse.json();
+
+ setHasLogs(txData.result ? true : false);
+ setIsLoading(false);
+ } catch (e) {
+ console.error(`Error getting EC data: ${e}`);
+ setIsLoading(false);
+ }
+ };
+
+ void getTxStatus();
+ }, [chainId, contractTx, isSynced, rpcUrl]);
+
+ return { isSynced, isInstalled, hasLogs, isLoading };
+};
diff --git a/dappnode/hooks/use-exit-requested-keys-from-events-api.ts b/dappnode/hooks/use-exit-requested-keys-from-events-api.ts
new file mode 100644
index 00000000..f9a61974
--- /dev/null
+++ b/dappnode/hooks/use-exit-requested-keys-from-events-api.ts
@@ -0,0 +1,73 @@
+import { useLidoSWR } from '@lido-sdk/react';
+import { STRATEGY_LAZY } from 'consts/swr-strategies';
+import { useNodeOperatorId } from 'providers/node-operator-provider';
+import { useCallback } from 'react';
+import { useAccount } from 'shared/hooks';
+import useDappnodeUrls from './use-dappnode-urls';
+
+interface ExitRequest {
+ event: {
+ [key: string]: any;
+ };
+ [key: string]: any;
+ validator_pubkey_hex: string;
+}
+type ExitRequests = Record;
+
+const parseEvents = (data: ExitRequests) => {
+ return Object.values(data).map((event: ExitRequest) => ({
+ validatorPubkey: event.validator_pubkey_hex.toLowerCase(),
+ blockNumber: parseInt(event.event.Raw.blockNumber, 16),
+ }));
+};
+
+const restoreEvents = (
+ events: { validatorPubkey: string; blockNumber: number }[],
+) =>
+ events
+ .sort((a, b) => a.blockNumber - b.blockNumber)
+ .map((e) => e.validatorPubkey);
+
+export const useExitRequestedKeysFromEvents = () => {
+ const { backendUrl } = useDappnodeUrls();
+ const { chainId } = useAccount();
+ const nodeOperatorId = useNodeOperatorId();
+
+ const fetcher = useCallback(async () => {
+ if (!nodeOperatorId) {
+ console.error('Node Operator ID is required to fetch exit requests');
+ return [];
+ }
+
+ try {
+ console.debug(
+ `Fetching exit requests for Node Operator ID: ${nodeOperatorId}`,
+ );
+ const url = `${backendUrl}/api/v0/events_indexer/exit_requests?operatorId=${nodeOperatorId}`;
+ const options = {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ };
+
+ const response = await fetch(url, options);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data: ExitRequests = await response.json();
+ const events = parseEvents(data);
+
+ return restoreEvents(events);
+ } catch (e) {
+ console.error(`Error fetching exit request events: ${e}`);
+ return [];
+ }
+ }, [backendUrl, nodeOperatorId]);
+
+ return useLidoSWR(
+ ['exit-requested-keys', nodeOperatorId, chainId],
+ nodeOperatorId ? fetcher : null,
+ STRATEGY_LAZY,
+ );
+};
diff --git a/dappnode/hooks/use-get-exit-requests.ts b/dappnode/hooks/use-get-exit-requests.ts
new file mode 100644
index 00000000..6b8c8ba7
--- /dev/null
+++ b/dappnode/hooks/use-get-exit-requests.ts
@@ -0,0 +1,49 @@
+import { useState } from 'react';
+import useDappnodeUrls from './use-dappnode-urls';
+import { useActiveNodeOperator } from 'providers/node-operator-provider';
+
+const useGetExitRequests = () => {
+ const { backendUrl } = useDappnodeUrls();
+ const [exitRequests, setExitRequests] = useState();
+
+ const nodeOperator = useActiveNodeOperator();
+
+ interface ExitRequest {
+ event: {
+ [key: string]: any;
+ };
+ [key: string]: any;
+ validator_pubkey_hex: string;
+ }
+ type ExitRequests = Record;
+
+ const getExitRequests = async () => {
+ try {
+ console.debug(`GETting validators exit requests from indexer API`);
+ const response = await fetch(
+ `${backendUrl}/api/v0/events_indexer/exit_requests?operatorId=${nodeOperator?.id}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ setExitRequests(data);
+ } catch (e) {
+ console.error(
+ `Error GETting validators exit requests from indexer API: ${e}`,
+ );
+ }
+ };
+
+ return { exitRequests, getExitRequests };
+};
+
+export default useGetExitRequests;
diff --git a/dappnode/hooks/use-get-infra-status.ts b/dappnode/hooks/use-get-infra-status.ts
new file mode 100644
index 00000000..990e8b92
--- /dev/null
+++ b/dappnode/hooks/use-get-infra-status.ts
@@ -0,0 +1,114 @@
+import { useEffect, useState } from 'react';
+import useDappnodeUrls from './use-dappnode-urls';
+import { InfraStatus } from 'dappnode/status/types';
+
+export const useGetInfraStatus = () => {
+ const { ECApiUrl, CCStatusApiUrl, CCVersionApiUrl } = useDappnodeUrls();
+ const [ECStatus, setECStatus] = useState();
+ const [CCStatus, setCCStatus] = useState();
+
+ const [ECName, setECName] = useState();
+ const [CCName, setCCName] = useState();
+
+ const [isECLoading, setIsECLoading] = useState(true);
+ const [isCCLoading, setIsCCLoading] = useState(true);
+
+ useEffect(() => {
+ const getCCData = async () => {
+ setIsCCLoading(true);
+ try {
+ const versionResponse = await fetch(CCVersionApiUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ if (!versionResponse.ok) {
+ throw new Error(`HTTP error! Status: ${versionResponse.status}`);
+ }
+ const versionData = await versionResponse.json();
+ setCCName(versionData.data.version.split('/')[0]);
+
+ const syncResponse = await fetch(CCStatusApiUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!syncResponse.ok) {
+ throw new Error(`HTTP error! Status: ${syncResponse.status}`);
+ }
+
+ const data = await syncResponse.json();
+ setCCStatus(data.data.is_syncing ? 'Syncing' : 'Synced');
+ setIsCCLoading(false);
+ } catch (e) {
+ console.error(`Error getting CC data: ${e}`);
+ setCCStatus('Not installed');
+ setIsCCLoading(false);
+ }
+ };
+
+ const getECData = async () => {
+ setIsECLoading(true);
+ try {
+ const versionResponse = await fetch(`${ECApiUrl}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'web3_clientVersion',
+ params: [],
+ id: 1,
+ }),
+ });
+
+ if (!versionResponse.ok) {
+ throw new Error(`HTTP error! Status: ${versionResponse.status}`);
+ }
+ const versionData = await versionResponse.json();
+ setECName(versionData.result.split('/')[0]);
+
+ const syncResponse = await fetch(`${ECApiUrl}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_syncing',
+ params: [],
+ id: 0,
+ }),
+ });
+
+ if (!syncResponse.ok) {
+ throw new Error(`HTTP error! Status: ${syncResponse.status}`);
+ }
+
+ const syncData = await syncResponse.json();
+ setECStatus(syncData.result ? 'Syncing' : 'Synced');
+ setIsECLoading(false);
+ } catch (e) {
+ console.error(`Error getting EC data: ${e}`);
+ setECStatus('Not installed');
+ setIsECLoading(false);
+ }
+ };
+
+ void getECData();
+ void getCCData();
+ }, [CCStatusApiUrl, CCVersionApiUrl, ECApiUrl]);
+
+ return {
+ CCName,
+ CCStatus,
+ ECName,
+ ECStatus,
+ isECLoading,
+ isCCLoading,
+ };
+};
diff --git a/dappnode/hooks/use-get-next-report.ts b/dappnode/hooks/use-get-next-report.ts
new file mode 100644
index 00000000..3b6b2e0a
--- /dev/null
+++ b/dappnode/hooks/use-get-next-report.ts
@@ -0,0 +1,28 @@
+import { useChainId } from 'wagmi';
+import { CONSTANTS_BY_NETWORK, getCsmConstants } from 'consts/csm-constants';
+
+export const useGetNextReport = () => {
+ const chainId = useChainId() as keyof typeof CONSTANTS_BY_NETWORK;
+ const currentTimestamp = Math.floor(Date.now() / 1000);
+
+ const deploymentTimestamp = getCsmConstants(chainId).reportTimestamp;
+
+ const reportsIntervalDays = chainId === 1 ? 28 : 7;
+ const reportsIntervalSeconds = reportsIntervalDays * 24 * 60 * 60;
+
+ const secondsSinceDeployment = currentTimestamp - deploymentTimestamp;
+
+ const reportsCompleted = Math.floor(
+ secondsSinceDeployment / reportsIntervalSeconds,
+ );
+
+ const nextReportTimestamp =
+ deploymentTimestamp + (reportsCompleted + 1) * reportsIntervalSeconds; // +1 because we want the next report
+
+ const secondsUntilNextReport = nextReportTimestamp - currentTimestamp;
+ const daysUntilNextReport = Math.ceil(
+ secondsUntilNextReport / (24 * 60 * 60),
+ );
+
+ return daysUntilNextReport;
+};
diff --git a/dappnode/hooks/use-get-operator-performance.ts b/dappnode/hooks/use-get-operator-performance.ts
new file mode 100644
index 00000000..b279b61a
--- /dev/null
+++ b/dappnode/hooks/use-get-operator-performance.ts
@@ -0,0 +1,62 @@
+import { useNodeOperatorsList } from 'providers/node-operator-provider/use-node-operators-list';
+import useDappnodeUrls from './use-dappnode-urls';
+import { useGetActiveNodeOperator } from 'providers/node-operator-provider/use-get-active-node-operator';
+import { useEffect, useState } from 'react';
+
+export const useGetOperatorPerformance = () => {
+ const { backendUrl } = useDappnodeUrls();
+ const { list } = useNodeOperatorsList();
+ const { active: activeNO } = useGetActiveNodeOperator(list);
+ const [operatorData, setOperatorData] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ const getDataWithRetry = async () => {
+ const url = `${backendUrl}/api/v0/events_indexer/operator_performance?operatorId=${activeNO?.id}`;
+ const options = {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ };
+ const retryInterval = 5000; // 5 seconds
+
+ setIsLoading(true);
+
+ const shouldRetry = true;
+ while (shouldRetry) {
+ try {
+ console.debug(
+ `Fetching operator performance data from events indexer API...`,
+ );
+ const response = await fetch(url, options);
+
+ if (response.status === 202) {
+ console.debug(
+ `Received status 202. Retrying in ${retryInterval / 1000} seconds...`,
+ );
+ await new Promise((resolve) => setTimeout(resolve, retryInterval));
+ continue;
+ }
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ setOperatorData(data);
+ break; // Exit loop when successful
+ } catch (e) {
+ console.error(`Error fetching operator performance data: ${e}`);
+ break; // Stop retrying on other errors
+ }
+ }
+
+ setIsLoading(false);
+ };
+
+ if (activeNO) {
+ void getDataWithRetry();
+ }
+ }, [activeNO, backendUrl]);
+
+ return { operatorData, isLoading };
+};
diff --git a/dappnode/hooks/use-get-pending-reports.ts b/dappnode/hooks/use-get-pending-reports.ts
new file mode 100644
index 00000000..bf944a56
--- /dev/null
+++ b/dappnode/hooks/use-get-pending-reports.ts
@@ -0,0 +1,50 @@
+import { useEffect, useState } from 'react';
+import useDappnodeUrls from './use-dappnode-urls';
+import { useNodeOperatorsList } from 'providers/node-operator-provider/use-node-operators-list';
+import { useGetActiveNodeOperator } from 'providers/node-operator-provider/use-get-active-node-operator';
+
+export const useGetPendingReports = () => {
+ const { backendUrl } = useDappnodeUrls();
+ const { list } = useNodeOperatorsList();
+ const { active: activeNO } = useGetActiveNodeOperator(list);
+
+ const [pendingReports, setPendingReports] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ const getPendingReports = async () => {
+ setIsLoading(true);
+ try {
+ console.debug(`GETting pending reports from events indexer API`);
+ const response = await fetch(
+ `${backendUrl}/api/v0/events_indexer/pending_hashes?operatorId=${activeNO?.id}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ setPendingReports(data.length);
+ setIsLoading(false);
+ } catch (e) {
+ console.error(
+ `Error GETting pending reports from events indexer API: ${e}`,
+ );
+ setIsLoading(false);
+ }
+ };
+
+ if (activeNO) {
+ void getPendingReports();
+ }
+ }, [activeNO, backendUrl]);
+
+ return { isLoading, pendingReports };
+};
diff --git a/dappnode/hooks/use-get-performance-by-range.ts b/dappnode/hooks/use-get-performance-by-range.ts
new file mode 100644
index 00000000..4bd4d536
--- /dev/null
+++ b/dappnode/hooks/use-get-performance-by-range.ts
@@ -0,0 +1,181 @@
+import { useEffect, useState } from 'react';
+import { useGetOperatorPerformance } from './use-get-operator-performance';
+import { Range, ValidatorStats } from '../performance/types';
+import { useAccount } from 'shared/hooks';
+
+export const useGetPerformanceByRange = (range: Range) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const { operatorData } = useGetOperatorPerformance();
+ const [operatorDataByRange, setOperatorDataByRange] = useState<
+ Record
+ >({});
+ const [validatorsStats, setValidatorsStats] = useState([]);
+ const [threshold, setThreshold] = useState(0);
+ const [thresholdsByEpoch, setThresholdsByEpoch] = useState([]);
+
+ const { chainId } = useAccount();
+
+ const epochRanges: Record = {
+ week: 1575, // 7 days * 225 epochs / day
+ month: 7650, // 30 days * 225 epochs / day
+ year: 82125, // 365 days * 225 epochs / day
+ ever: Infinity,
+ };
+
+ const epochsInRange = epochRanges[range];
+
+ useEffect(() => {
+ if (!operatorData) return;
+
+ setIsLoading(true);
+ const sortedKeys = Object.keys(operatorData).sort((a, b) => {
+ const [startA] = a.split('-').map(Number);
+ const [startB] = b.split('-').map(Number);
+ return startB - startA; // Sort descending by epoch start
+ });
+
+ let totalEpochs = 0;
+ const filteredData: Record = {};
+ let previousEntry: string | null = null;
+
+ // Filter data based on range for operatorDataByRange
+ for (const key of sortedKeys) {
+ const [startEpoch, endEpoch] = key.split('-').map(Number);
+ const epochDiff = endEpoch - startEpoch;
+
+ if (totalEpochs + epochDiff > epochsInRange) {
+ if (chainId === 1) {
+ if (range === 'month' && !previousEntry) {
+ previousEntry = key;
+ }
+ } else {
+ if (range === 'week' && !previousEntry) {
+ previousEntry = key;
+ }
+ }
+ break;
+ }
+
+ filteredData[key] = operatorData[key];
+ totalEpochs += epochDiff;
+ }
+
+ setOperatorDataByRange(filteredData);
+
+ // Filter data for thresholdsByEpoch
+ const thresholdsData = { ...filteredData };
+ if (chainId === 1) {
+ if (range === 'month' && previousEntry) {
+ thresholdsData[previousEntry] = operatorData[previousEntry];
+ }
+ } else {
+ if (range === 'week' && previousEntry && !thresholdsData[previousEntry]) {
+ thresholdsData[previousEntry] = operatorData[previousEntry];
+ }
+ }
+
+ setThresholdsByEpoch(
+ Object.entries(thresholdsData)
+ .map(([_, value]) => {
+ const endFrame = value.frame[1].toString();
+ const lidoThreshold = value.threshold * 100; // Convert to percentage
+
+ const validatorRatios = Object.entries(value.data.validators).reduce(
+ (acc, [validatorId, validatorData]) => {
+ const validatorPerf = (validatorData as any).perf;
+ acc[validatorId] =
+ (validatorPerf.included / validatorPerf.assigned) * 100; // Convert to percentage
+ return acc;
+ },
+ {} as Record,
+ );
+
+ return {
+ name: endFrame,
+ lidoThreshold,
+ ...validatorRatios,
+ };
+ })
+ .reverse(), // Reverse for oldest first
+ );
+ }, [chainId, epochsInRange, operatorData, range]);
+
+ useEffect(() => {
+ if (!operatorDataByRange) return;
+
+ const statsPerValidator: { [key: string]: ValidatorStats[] } = {};
+ const thresholds: number[] = [];
+
+ // Process data for validatorsStats
+ for (const key of Object.keys(operatorDataByRange)) {
+ const validatorsData = operatorDataByRange[key]?.data?.validators || {};
+ thresholds.push(operatorDataByRange[key]?.threshold);
+
+ for (const validator of Object.keys(validatorsData)) {
+ if (!statsPerValidator[validator]) {
+ statsPerValidator[validator] = [];
+ }
+
+ const validatorPerf = validatorsData[validator].perf;
+ const attestations = {
+ included: validatorPerf.included,
+ assigned: validatorPerf.assigned,
+ };
+
+ statsPerValidator[validator].push({
+ index: parseInt(validator),
+ attestations,
+ efficiency: validatorPerf.included / validatorPerf.assigned,
+ });
+ }
+ }
+
+ // Calculate average threshold
+ setThreshold(
+ thresholds.reduce((sum, value) => sum + value, 0) / thresholds.length,
+ );
+
+ const getValidatorStats = (
+ data: Record,
+ ): ValidatorStats[] => {
+ return Object.entries(data).map(([key, entries]) => {
+ const totalAssigned = entries.reduce(
+ (sum, entry) => sum + entry.attestations.assigned,
+ 0,
+ );
+ const totalIncluded = entries.reduce(
+ (sum, entry) => sum + entry.attestations.included,
+ 0,
+ );
+ const totalEfficiency = entries.reduce(
+ (sum, entry) => sum + (entry.efficiency || 0),
+ 0,
+ );
+
+ return {
+ index: parseInt(key, 10),
+ attestations: {
+ assigned: totalAssigned,
+ included: totalIncluded,
+ },
+ efficiency: totalEfficiency / entries.length,
+ };
+ });
+ };
+
+ // Calculate stats for validators
+ const result = getValidatorStats(statsPerValidator);
+ setValidatorsStats(result);
+ }, [operatorDataByRange]);
+
+ useEffect(() => {
+ setIsLoading(false);
+ }, [validatorsStats]);
+
+ return {
+ isLoading,
+ validatorsStats,
+ threshold,
+ thresholdsByEpoch,
+ };
+};
diff --git a/dappnode/hooks/use-get-relays-data.ts b/dappnode/hooks/use-get-relays-data.ts
new file mode 100644
index 00000000..d3135efd
--- /dev/null
+++ b/dappnode/hooks/use-get-relays-data.ts
@@ -0,0 +1,161 @@
+import { useEffect, useState } from 'react';
+import useDappnodeUrls from './use-dappnode-urls';
+import { AllowedRelay } from 'dappnode/status/types';
+import { sanitizeUrl } from 'dappnode/utils/sanitize-urls';
+
+const useGetRelaysData = () => {
+ const { backendUrl, MEVApiUrl } = useDappnodeUrls();
+
+ const [isLoading, setIsLoading] = useState(true);
+
+ const [allowedRelays, setAllowedRelays] = useState();
+ const [usedRelays, setUsedRelays] = useState<[]>();
+ const [relaysError, setRelaysError] = useState();
+ const [isMEVRunning, setIsMEVRunning] = useState();
+ const [mandatoryRelays, setMandatoryRelays] = useState();
+ const [hasMandatoryRelay, setHasMandatoryRelay] = useState();
+ const [usedBlacklistedRelays, setUsedBlacklistedRelays] = useState(
+ [],
+ );
+
+ useEffect(() => {
+ const getMEVStatus = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(MEVApiUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ await response.json();
+ setIsMEVRunning(true);
+ setIsLoading(false);
+ } catch (e) {
+ setIsMEVRunning(false);
+ console.error('Error GETting MEV PKG Status:', e);
+ setIsLoading(false);
+ }
+ };
+
+ void getMEVStatus();
+ }, [MEVApiUrl]);
+
+ useEffect(() => {
+ const getAllowedRelays = async () => {
+ try {
+ console.debug(`GETting allowed relays from events indexer API`);
+ const response = await fetch(
+ `${backendUrl}/api/v0/events_indexer/relays_allowed`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ const cleanedData = data.map((relay: { Uri: string }) => ({
+ ...relay,
+ Uri: sanitizeUrl(relay.Uri),
+ }));
+
+ setAllowedRelays(cleanedData);
+ } catch (e) {
+ console.error(`Error GETting allowed relays from indexer API: ${e}`);
+ }
+ };
+
+ const getUsedRelays = async () => {
+ try {
+ console.debug(`GETting used relays from events indexer API`);
+ const response = await fetch(
+ `${backendUrl}/api/v0/events_indexer/relays_used`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ const cleanedData = data.map((url: string) => sanitizeUrl(url));
+ setUsedRelays(cleanedData);
+ } catch (e) {
+ setRelaysError(e);
+ console.error(`Error GETting used relays from indexer API: ${e}`);
+ }
+ };
+
+ void getAllowedRelays();
+ void getUsedRelays();
+ }, [isMEVRunning, backendUrl]);
+
+ useEffect(() => {
+ if (allowedRelays) {
+ setMandatoryRelays(allowedRelays.filter((relay) => relay.IsMandatory));
+ }
+ }, [allowedRelays]);
+
+ useEffect(() => {
+ const filterMandatoryRelays = (
+ mandatoryRelays: AllowedRelay[],
+ usedRelays: string[],
+ ) => {
+ return usedRelays.some((usedRelay) =>
+ mandatoryRelays.some((relay) => relay.Uri === usedRelay),
+ );
+ };
+
+ if (mandatoryRelays && usedRelays) {
+ const hasMandatoryRelay = filterMandatoryRelays(
+ mandatoryRelays,
+ usedRelays,
+ );
+ setHasMandatoryRelay(hasMandatoryRelay);
+ }
+ }, [mandatoryRelays, usedRelays]);
+
+ useEffect(() => {
+ if (allowedRelays && usedRelays) {
+ const allowedUris = allowedRelays.map((relay) => relay.Uri);
+
+ // Filter out uris from usedRelays that are not in allowedRelays
+ const blacklisted = usedRelays.filter(
+ (uri) => !allowedUris.includes(uri),
+ );
+
+ setUsedBlacklistedRelays(blacklisted);
+ }
+ }, [usedRelays, allowedRelays]);
+
+ return {
+ allowedRelays,
+ usedRelays,
+ relaysError,
+ isMEVRunning,
+ hasMandatoryRelay,
+ mandatoryRelays,
+ isLoading,
+ usedBlacklistedRelays,
+ };
+};
+
+export default useGetRelaysData;
diff --git a/dappnode/hooks/use-get-telegram-data.ts b/dappnode/hooks/use-get-telegram-data.ts
new file mode 100644
index 00000000..857a92c0
--- /dev/null
+++ b/dappnode/hooks/use-get-telegram-data.ts
@@ -0,0 +1,43 @@
+import { useState } from 'react';
+import useDappnodeUrls from './use-dappnode-urls';
+
+const useGetTelegramData = () => {
+ const { backendUrl } = useDappnodeUrls();
+ const [telegramId, setTelegramData] = useState();
+ const [botToken, setBotToken] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+
+ const getTelegramData = async () => {
+ setIsLoading(true);
+ try {
+ console.debug(`GETting telegram data from events indexer API`);
+ const response = await fetch(
+ `${backendUrl}/api/v0/events_indexer/telegramConfig`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ setTelegramData(data.userId);
+ setBotToken(data.token);
+ setIsLoading(false);
+ } catch (e) {
+ console.error(
+ `Error GETting telegram data from events indexer API: ${e}`,
+ );
+ setIsLoading(false);
+ }
+ };
+
+ return { telegramId, botToken, getTelegramData, isLoading };
+};
+
+export default useGetTelegramData;
diff --git a/dappnode/hooks/use-invites-events-fetcher-api.ts b/dappnode/hooks/use-invites-events-fetcher-api.ts
new file mode 100644
index 00000000..4e724975
--- /dev/null
+++ b/dappnode/hooks/use-invites-events-fetcher-api.ts
@@ -0,0 +1,155 @@
+import { ROLES } from 'consts/roles';
+import { useCallback } from 'react';
+import { useAccount, useAddressCompare } from 'shared/hooks';
+import { getInviteId } from 'shared/node-operator';
+import { NodeOperatorInvite } from 'types';
+import { getNodeOperatorIdFromEvent } from 'utils';
+import useDappnodeUrls from './use-dappnode-urls';
+import {
+ NodeOperatorManagerAddressChangeProposedEvent,
+ NodeOperatorRewardAddressChangeProposedEvent,
+ NodeOperatorManagerAddressChangedEvent,
+ NodeOperatorRewardAddressChangedEvent,
+} from 'generated/CSModule';
+
+type AddressChangeProposedEvents =
+ | NodeOperatorManagerAddressChangeProposedEvent
+ | NodeOperatorRewardAddressChangeProposedEvent
+ | NodeOperatorManagerAddressChangedEvent
+ | NodeOperatorRewardAddressChangedEvent;
+
+const fetchWithRetry = async (
+ url: string,
+ options: RequestInit,
+ timeout: number,
+): Promise => {
+ const shouldRetry = true;
+ while (shouldRetry) {
+ const response = await fetch(url, options);
+ if (response.status === 202) {
+ console.debug(
+ `Received status 202. Retrying in ${timeout / 1000} seconds...`,
+ );
+ await new Promise((resolve) => setTimeout(resolve, timeout));
+ } else {
+ return response;
+ }
+ }
+
+ return new Response(null);
+};
+
+const parseEvents = (data: any) => {
+ return [
+ ...(data.nodeOperatorManagerAddressChangeProposed || []).map(
+ (event: any) => ({
+ event: 'NodeOperatorManagerAddressChangeProposed',
+ nodeOperatorId: `${parseInt(event.NodeOperatorId)}`,
+ newAddress: event.NewAddress,
+ blockNumber: parseInt(event.Raw.blockNumber, 16),
+ }),
+ ),
+ ...(data.nodeOperatorRewardAddressChangeProposed || []).map(
+ (event: any) => ({
+ event: 'NodeOperatorRewardAddressChangeProposed',
+ nodeOperatorId: `${parseInt(event.NodeOperatorId)}`,
+ newAddress: event.NewAddress,
+ blockNumber: parseInt(event.Raw.blockNumber, 16),
+ }),
+ ),
+ ...(data.nodeOperatorManagerAddressChanged || []).map((event: any) => ({
+ event: 'NodeOperatorManagerAddressChanged',
+ nodeOperatorId: `${parseInt(event.NodeOperatorId)}`,
+ oldAddress: event.OldAddress,
+ newAddress: event.NewAddress,
+ blockNumber: parseInt(event.Raw.blockNumber, 16),
+ })),
+ ...(data.nodeOperatorRewardAddressChanged || []).map((event: any) => ({
+ event: 'NodeOperatorRewardAddressChanged',
+ nodeOperatorId: `${parseInt(event.NodeOperatorId)}`,
+ oldAddress: event.OldAddress,
+ newAddress: event.NewAddress,
+ blockNumber: parseInt(event.Raw.blockNumber, 16),
+ })),
+ ];
+};
+
+export const useInvitesEventsFetcher = () => {
+ const { address } = useAccount();
+ const { backendUrl } = useDappnodeUrls();
+ const isUserAddress = useAddressCompare();
+
+ const restoreEvents = useCallback(
+ (events: AddressChangeProposedEvents[]) => {
+ const invitesMap: Map = new Map();
+
+ const updateRoles = (invite: NodeOperatorInvite, add = true) => {
+ const id = getInviteId(invite);
+ if (add) {
+ invitesMap.set(id, invite);
+ } else {
+ invitesMap.delete(id);
+ }
+ };
+
+ events
+ .sort((a, b) => a.blockNumber - b.blockNumber)
+ .forEach((e) => {
+ const id = getNodeOperatorIdFromEvent(e);
+ switch (e.event) {
+ case 'NodeOperatorManagerAddressChangeProposed':
+ return isUserAddress(e.args[2])
+ ? updateRoles({ id, role: ROLES.MANAGER })
+ : updateRoles({ id, role: ROLES.MANAGER }, false);
+ case 'NodeOperatorRewardAddressChangeProposed':
+ return isUserAddress(e.args[2])
+ ? updateRoles({ id, role: ROLES.REWARDS })
+ : updateRoles({ id, role: ROLES.REWARDS }, false);
+ case 'NodeOperatorManagerAddressChanged':
+ return updateRoles({ id, role: ROLES.MANAGER }, false);
+ case 'NodeOperatorRewardAddressChanged':
+ return updateRoles({ id, role: ROLES.REWARDS }, false);
+ default:
+ return;
+ }
+ });
+
+ return Array.from(invitesMap.values()).sort(
+ (a, b) =>
+ parseInt(a.id, 10) - parseInt(b.id, 10) ||
+ -Number(b.role === ROLES.REWARDS) - Number(a.role === ROLES.REWARDS),
+ );
+ },
+ [isUserAddress],
+ );
+
+ const fetcher = useCallback(async () => {
+ try {
+ console.debug(`Fetching invite events for address: ${address}`);
+ const url = `${backendUrl}/api/v0/events_indexer/address_events?address=${address}`;
+ const options = {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ };
+
+ // Retry logic for 202 status
+ const response = await fetchWithRetry(url, options, 5000);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ const events = parseEvents(data);
+
+ console.debug('Parsed invite events:', events);
+
+ return restoreEvents(events);
+ } catch (e) {
+ console.error(`Error fetching invite events: ${e}`);
+ return [];
+ }
+ }, [backendUrl, address, restoreEvents]);
+
+ return fetcher;
+};
diff --git a/dappnode/hooks/use-missing-keys.ts b/dappnode/hooks/use-missing-keys.ts
new file mode 100644
index 00000000..4883a02d
--- /dev/null
+++ b/dappnode/hooks/use-missing-keys.ts
@@ -0,0 +1,96 @@
+import { useEffect, useState } from 'react';
+import { useKeysWithStatus } from 'shared/hooks';
+import useApiBrain from './use-brain-keystore-api';
+import useDappnodeUrls from './use-dappnode-urls';
+
+const useMissingKeys = () => {
+ const [missingKeys, setMissingKeys] = useState([]);
+ const [keysLoading, setKeysLoading] = useState(false);
+
+ const {
+ pubkeys: brainKeys,
+ isLoading: brainKeysLoading,
+ error,
+ } = useApiBrain();
+ const { data: lidoKeys, initialLoading: lidoKeysLoading } =
+ useKeysWithStatus();
+
+ const { keysStatusUrl } = useDappnodeUrls();
+
+ useEffect(() => {
+ if (lidoKeysLoading || brainKeysLoading) {
+ setKeysLoading(true);
+ } else {
+ setKeysLoading(false);
+ }
+ }, [lidoKeysLoading, brainKeysLoading]);
+
+ useEffect(() => {
+ const setActiveKeys = async (missingKeys_: string[]): Promise => {
+ if (missingKeys_.length > 0) {
+ const params = new URLSearchParams({
+ id: missingKeys_.join(','),
+ status:
+ 'withdrawal_done,withdrawal_possible,exited_unslashed,exited_slashed',
+ });
+
+ const inactiveKeys: string[] = [];
+ try {
+ const response = await fetch(
+ `${keysStatusUrl}?${params.toString()}`,
+ {
+ method: 'GET',
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ for (const key of data.data) {
+ inactiveKeys.push(key.validator.pubkey);
+ }
+
+ const lidoActiveKeys = missingKeys_.filter(
+ (key) => !inactiveKeys.includes(key),
+ );
+
+ setMissingKeys(lidoActiveKeys);
+ } catch (error) {
+ console.error('Error fetching validators:', error);
+ }
+ } else {
+ setMissingKeys([]);
+ }
+ };
+
+ const filterLidoKeys = async () => {
+ if (lidoKeys) {
+ const formattedLidoKeys = [];
+ for (const key of lidoKeys) {
+ formattedLidoKeys.push(key.key.toLowerCase());
+ }
+
+ const formattedBrainKeys = brainKeys
+ ? brainKeys.map((key) => key.toLowerCase())
+ : [];
+
+ const missingLidoKeys = formattedLidoKeys.filter(
+ (lidoKey) => !formattedBrainKeys.includes(lidoKey),
+ );
+
+ await setActiveKeys(missingLidoKeys);
+ }
+ };
+
+ if (brainKeys && lidoKeys) {
+ void filterLidoKeys();
+ }
+ }, [brainKeys, keysStatusUrl, lidoKeys]);
+
+ return { missingKeys, keysLoading, error };
+};
+
+export default useMissingKeys;
diff --git a/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts b/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts
new file mode 100644
index 00000000..9e44bb6c
--- /dev/null
+++ b/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts
@@ -0,0 +1,182 @@
+import { useCallback } from 'react';
+import useDappnodeUrls from './use-dappnode-urls';
+import { NodeOperator } from 'types';
+import { compareLowercase, mergeRoles } from 'utils';
+
+/**
+ * This hook acts as an alternative to the hook `useNodeOperatorsFetcherFromEvents`.
+ * Fetching the events "NodeOperatorAdded", "NodeOperatorManagerAddressChanged" and "NodeOperatorRewardAddressChanged"
+ * directly to an RPC endpoint can take several minutes specially in ethereum mainnet.
+ *
+ * This hook fetches the events from an API endpoint of the lido-events backend that fetches the events and indexes them for
+ * faster retrieval.
+ *
+ * In the first login of the user this fetch can take several minutes, but in the next logins the events are fetched from the cache til the latest block.
+ *
+ * The lido-events backend will return http code 202 if the address is still being processed, in that case the hook will retry the fetch every 5 seconds and must never
+ * resolve the promise to keep the isListLoading being true.
+ *
+ * stats:
+ * - Geth: around 1.5 minutes
+ * - Besu: around 7 minutes
+ */
+
+type NodeOperatorRoleEvent =
+ | {
+ event: 'NodeOperatorAdded';
+ nodeOperatorId: string;
+ managerAddress: string;
+ rewardAddress: string;
+ blockNumber: number;
+ }
+ | {
+ event: 'NodeOperatorManagerAddressChanged';
+ nodeOperatorId: string;
+ oldAddress: string;
+ newAddress: string;
+ blockNumber: number;
+ }
+ | {
+ event: 'NodeOperatorRewardAddressChanged';
+ nodeOperatorId: string;
+ oldAddress: string;
+ newAddress: string;
+ blockNumber: number;
+ };
+
+const restoreEvents = (
+ events: NodeOperatorRoleEvent[],
+ address?: string,
+): NodeOperator[] => {
+ const isUserAddress = (value: string) => compareLowercase(address, value);
+
+ return events
+ .sort((a, b) => a.blockNumber - b.blockNumber)
+ .reduce((prev, e) => {
+ const id: `${number}` = `${parseInt(e.nodeOperatorId)}`;
+ switch (e.event) {
+ case 'NodeOperatorAdded':
+ return mergeRoles(prev, {
+ id,
+ manager: isUserAddress(e.managerAddress),
+ rewards: isUserAddress(e.rewardAddress),
+ });
+ case 'NodeOperatorManagerAddressChanged':
+ return mergeRoles(prev, {
+ id,
+ manager: isUserAddress(e.newAddress),
+ });
+ case 'NodeOperatorRewardAddressChanged':
+ return mergeRoles(prev, {
+ id,
+ rewards: isUserAddress(e.newAddress),
+ });
+ default:
+ return prev;
+ }
+ }, [] as NodeOperator[]);
+};
+
+const fetchWithRetry = async (
+ url: string,
+ options: RequestInit,
+ timeout: number,
+): Promise => {
+ const shouldRetry = true;
+ while (shouldRetry) {
+ const response = await fetch(url, options);
+ if (response.status === 202) {
+ console.debug(
+ `Received status 202. Retrying in ${timeout / 1000} seconds...`,
+ );
+ await new Promise((resolve) => setTimeout(resolve, timeout));
+ } else {
+ return response;
+ }
+ }
+
+ return new Response();
+};
+const parseEvents = (data: any): NodeOperatorRoleEvent[] => {
+ const {
+ nodeOperatorAdded = [],
+ nodeOperatorManagerAddressChanged = [],
+ nodeOperatorRewardAddressChanged = [],
+ } = data;
+
+ const parsedAddedEvents = nodeOperatorAdded.map((event: any) => ({
+ event: 'NodeOperatorAdded',
+ nodeOperatorId: `${parseInt(event.NodeOperatorId)}`,
+ managerAddress: event.ManagerAddress,
+ rewardAddress: event.RewardAddress,
+ blockNumber: parseInt(event.Raw.blockNumber, 16),
+ }));
+
+ const parsedManagerChangedEvents = nodeOperatorManagerAddressChanged.map(
+ (event: any) => ({
+ event: 'NodeOperatorManagerAddressChanged',
+ nodeOperatorId: `${parseInt(event.NodeOperatorId)}`,
+ oldAddress: event.OldAddress,
+ newAddress: event.NewAddress,
+ blockNumber: parseInt(event.Raw.blockNumber, 16),
+ }),
+ );
+
+ const parsedRewardChangedEvents = nodeOperatorRewardAddressChanged.map(
+ (event: any) => ({
+ event: 'NodeOperatorRewardAddressChanged',
+ nodeOperatorId: `${parseInt(event.NodeOperatorId)}`,
+ oldAddress: event.OldAddress,
+ newAddress: event.NewAddress,
+ blockNumber: parseInt(event.Raw.blockNumber, 16),
+ }),
+ );
+
+ return [
+ ...parsedAddedEvents,
+ ...parsedManagerChangedEvents,
+ ...parsedRewardChangedEvents,
+ ];
+};
+
+export const useNodeOperatorsFetcherFromAPI = (address?: string) => {
+ const { backendUrl } = useDappnodeUrls();
+
+ return useCallback(async () => {
+ if (!address) {
+ console.error('Address is required to fetch Node Operator events');
+ return [];
+ }
+
+ try {
+ console.debug(
+ `Fetching events associated with Node Operator address: ${address}`,
+ );
+
+ const url = `${backendUrl}/api/v0/events_indexer/address_events?address=${address}`;
+ const options = {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ // Retry logic for 202 status
+ const response = await fetchWithRetry(url, options, 5000);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ const events = parseEvents(data);
+
+ console.debug('Parsed events:', events);
+
+ return restoreEvents(events, address);
+ } catch (e) {
+ console.error(`Error fetching Node Operator events from API: ${e}`);
+ return [];
+ }
+ }, [backendUrl, address]);
+};
diff --git a/dappnode/hooks/use-node-operators-with-locked-bond-api.ts b/dappnode/hooks/use-node-operators-with-locked-bond-api.ts
new file mode 100644
index 00000000..7b1036f9
--- /dev/null
+++ b/dappnode/hooks/use-node-operators-with-locked-bond-api.ts
@@ -0,0 +1,114 @@
+import { useLidoSWR } from '@lido-sdk/react';
+import { STRATEGY_IMMUTABLE } from 'consts/swr-strategies';
+import { BigNumber } from 'ethers';
+import { ELRewardsStealingPenaltyReportedEvent } from 'generated/CSModule';
+import { useCallback } from 'react';
+import { useAccount, useCSAccountingRPC, useMergeSwr } from 'shared/hooks';
+import { NodeOperatorId } from 'types';
+import useDappnodeUrls from './use-dappnode-urls';
+
+const fetchWithRetry = async (
+ url: string,
+ options: RequestInit,
+ timeout: number,
+): Promise => {
+ const shouldRetry = true;
+ while (shouldRetry) {
+ const response = await fetch(url, options);
+ if (response.status === 202) {
+ console.debug(
+ `Received status 202. Retrying in ${timeout / 1000} seconds...`,
+ );
+ await new Promise((resolve) => setTimeout(resolve, timeout));
+ } else {
+ return response;
+ }
+ }
+
+ return new Response();
+};
+
+const parseEvents = (data: any): ELRewardsStealingPenaltyReportedEvent[] => {
+ return data.map((event: any) => ({
+ nodeOperatorId: `${parseInt(event.NodeOperatorId)}`,
+ proposedBlockHash: event.ProposedBlockHash as string,
+ stolenAmount: BigNumber.from(event.StolenAmount),
+ blockNumber: parseInt(event.Raw.blockNumber, 16),
+ }));
+};
+
+const restoreEvents = (
+ events: ELRewardsStealingPenaltyReportedEvent[],
+): string[] =>
+ events
+ .map((e) => e.args.nodeOperatorId.toString())
+ .filter((value, index, array) => array.indexOf(value) === index)
+ .sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
+
+const useLockedNodeOperatorsFromAPI = () => {
+ const { backendUrl } = useDappnodeUrls();
+ const { chainId } = useAccount();
+
+ const fetcher = useCallback(async (): Promise => {
+ try {
+ console.debug('Fetching EL rewards stealing penalties...');
+ const url = `${backendUrl}/api/v0/events_indexer/el_rewards_stealing_penalties_reported`;
+ const options = {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ };
+
+ // Retry logic for 202 status
+ const response = await fetchWithRetry(url, options, 5000);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ const events = parseEvents(data);
+
+ console.debug('Parsed EL rewards stealing penalties:', events);
+
+ return restoreEvents(events);
+ } catch (e) {
+ console.error(`Error fetching EL rewards stealing penalties: ${e}`);
+ return [];
+ }
+ }, [backendUrl]);
+
+ return useLidoSWR(
+ ['locked-node-operators-ids', chainId],
+ fetcher,
+ STRATEGY_IMMUTABLE,
+ );
+};
+
+export type LockedOperator = [NodeOperatorId, BigNumber];
+
+export const useNodeOperatorsWithLockedBond = () => {
+ const swrNodeOperators = useLockedNodeOperatorsFromAPI();
+ const contract = useCSAccountingRPC();
+ const { chainId } = useAccount();
+
+ const nodeOperatorIds = swrNodeOperators.data;
+
+ const fetcher = useCallback(async (): Promise => {
+ if (!nodeOperatorIds) return [];
+
+ const promises = nodeOperatorIds.map(
+ async (id) =>
+ [id, await contract.getActualLockedBond(id)] as LockedOperator,
+ );
+ const result = await Promise.all(promises);
+ return result.filter((r) => r[1].gt(0));
+ }, [contract, nodeOperatorIds]);
+
+ const swrList = useLidoSWR(
+ ['locked-node-operators', chainId, nodeOperatorIds],
+ nodeOperatorIds && chainId ? fetcher : null,
+ STRATEGY_IMMUTABLE,
+ );
+
+ return useMergeSwr([swrNodeOperators, swrList], swrList.data);
+};
diff --git a/dappnode/hooks/use-post-telegram-data.ts b/dappnode/hooks/use-post-telegram-data.ts
new file mode 100644
index 00000000..53cde451
--- /dev/null
+++ b/dappnode/hooks/use-post-telegram-data.ts
@@ -0,0 +1,64 @@
+import { useState } from 'react';
+import useDappnodeUrls from './use-dappnode-urls';
+
+const usePostTelegramData = ({
+ userId,
+ botToken,
+}: {
+ userId: number;
+ botToken: string;
+}) => {
+ const { backendUrl } = useDappnodeUrls();
+ const [postTgError, setPostTgError] = useState();
+ const [isLoading, setIsLoading] = useState();
+ const [isSuccess, setIsSuccess] = useState();
+
+ const errorMessages = {
+ default:
+ 'Error while posting the Telegram data. Double-check the provided data.',
+ chatNotFound:
+ 'Error setting Telegram data. Ensure to start the chat with your Bot and check your user ID.',
+ };
+
+ const postTelegramData = async () => {
+ setIsLoading(true);
+ setIsSuccess(false);
+ try {
+ setPostTgError(undefined);
+
+ console.debug(`POSTing telegram data to events indexer API`);
+ const response = await fetch(
+ `${backendUrl}/api/v0/events_indexer/telegramConfig`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ userId, token: botToken }),
+ },
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+
+ if (error.error.includes('Bad Request: chat not found')) {
+ setPostTgError(errorMessages.chatNotFound);
+ } else {
+ setPostTgError(errorMessages.default);
+ }
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ localStorage.setItem('isTgSeen', 'true');
+ setIsLoading(false);
+ setIsSuccess(true);
+ } catch (e) {
+ console.error(`Error POSTing telegram data to events indexer API: ${e}`);
+ setIsLoading(false);
+ }
+ };
+
+ return { postTelegramData, postTgError, isLoading, isSuccess };
+};
+
+export default usePostTelegramData;
diff --git a/dappnode/hooks/use-withdrawn-key-indexes-from-events-api.ts b/dappnode/hooks/use-withdrawn-key-indexes-from-events-api.ts
new file mode 100644
index 00000000..f8a4362a
--- /dev/null
+++ b/dappnode/hooks/use-withdrawn-key-indexes-from-events-api.ts
@@ -0,0 +1,84 @@
+import { useLidoSWR } from '@lido-sdk/react';
+import { STRATEGY_LAZY } from 'consts/swr-strategies';
+import { useNodeOperatorId } from 'providers/node-operator-provider';
+import { useCallback } from 'react';
+import { useAccount } from 'shared/hooks';
+import useDappnodeUrls from './use-dappnode-urls';
+
+const fetchWithRetry = async (
+ url: string,
+ options: RequestInit,
+ timeout: number,
+): Promise => {
+ const shouldRetry = true;
+ while (shouldRetry) {
+ const response = await fetch(url, options);
+ if (response.status === 202) {
+ console.debug(
+ `Received status 202. Retrying in ${timeout / 1000} seconds...`,
+ );
+ await new Promise((resolve) => setTimeout(resolve, timeout));
+ } else {
+ return response;
+ }
+ }
+
+ return new Response();
+};
+
+const parseEvents = (data: any) => {
+ return (data?.withdrawals || []).map((event: any) => ({
+ keyIndex: parseInt(event.KeyIndex, 10),
+ blockNumber: parseInt(event.Raw.blockNumber, 16),
+ }));
+};
+
+const restoreEvents = (events: { keyIndex: number; blockNumber: number }[]) =>
+ events.sort((a, b) => a.blockNumber - b.blockNumber).map((e) => e.keyIndex);
+
+export const useWithdrawnKeyIndexesFromEvents = () => {
+ const { backendUrl } = useDappnodeUrls();
+ const { chainId } = useAccount();
+ const nodeOperatorId = useNodeOperatorId();
+
+ const fetcher = useCallback(async () => {
+ if (!nodeOperatorId) {
+ console.error('Node Operator ID is required to fetch withdrawals');
+ return [];
+ }
+
+ try {
+ console.debug(
+ `Fetching withdrawals for Node Operator ID: ${nodeOperatorId}`,
+ );
+ const url = `${backendUrl}/api/v0/events_indexer/withdrawals_submitted?operatorId=${nodeOperatorId}`;
+ const options = {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ };
+
+ // Retry logic for 202 status
+ const response = await fetchWithRetry(url, options, 5000);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ const events = parseEvents(data);
+
+ console.debug('Parsed withdrawal events:', events);
+
+ return restoreEvents(events);
+ } catch (e) {
+ console.error(`Error fetching withdrawal events: ${e}`);
+ return [];
+ }
+ }, [backendUrl, nodeOperatorId]);
+
+ return useLidoSWR(
+ ['withdrawn-keys', nodeOperatorId, chainId],
+ nodeOperatorId ? fetcher : null,
+ STRATEGY_LAZY,
+ );
+};
diff --git a/dappnode/import-keys/import-keys-confirm-modal.tsx b/dappnode/import-keys/import-keys-confirm-modal.tsx
new file mode 100644
index 00000000..8d935f86
--- /dev/null
+++ b/dappnode/import-keys/import-keys-confirm-modal.tsx
@@ -0,0 +1,46 @@
+import { Button, Checkbox } from '@lidofinance/lido-ui';
+import Modal from 'dappnode/components/modal';
+import { useState } from 'react';
+
+interface ImportKeysWarningModalProps {
+ isOpen: boolean;
+ setIsOpen: (isOpen: boolean) => void;
+ setKeys: (keys: []) => void;
+}
+export default function ImportKeysWarningModal({
+ isOpen,
+ setIsOpen,
+ setKeys,
+}: ImportKeysWarningModalProps) {
+ const [checked, setChecked] = useState(false);
+ const handleClose = () => {
+ setKeys([]);
+ setIsOpen(false);
+ };
+ return (
+
+ Key Import Advisory
+
+ It is crucial that the keys you are about to use are not active or
+ running on any other machine. Running the same keys in multiple
+ locations can lead to conflicts, loss of funds, or security
+ vulnerabilities.
+
+ Please confirm your understanding by checking the box below:
+ setChecked(e.target.checked)}
+ label="I understand it and promise I don't have these keys running somewhere else"
+ checked={checked}
+ />
+
+ setIsOpen(false)}
+ disabled={!checked}
+ >
+ Confirm
+
+
+ );
+}
diff --git a/dappnode/import-keys/keys-input-form.tsx b/dappnode/import-keys/keys-input-form.tsx
new file mode 100644
index 00000000..2396625f
--- /dev/null
+++ b/dappnode/import-keys/keys-input-form.tsx
@@ -0,0 +1,85 @@
+import { Address, Link } from '@lidofinance/lido-ui';
+import { DropZoneContainer, KeystoreFileRow } from './styles';
+import { PasswordInput } from './password-input';
+import useKeystoreDrop from './use-keystore-drop';
+import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls';
+import { useFormDepositData } from 'shared/hook-form/form-controller';
+import { SubmitKeysFormInputType } from 'features/create-node-operator/submit-keys-form/context';
+import { useForm } from 'react-hook-form';
+import { useEffect, useState } from 'react';
+import ImportKeysConfirmModal from './import-keys-confirm-modal';
+
+type KeysBrainUploadProps = {
+ label?: string;
+ fieldName?: string;
+ showErrorMessage?: boolean;
+ missingKeys: string[];
+ error: boolean;
+};
+
+export const KeysBrainUpload = ({
+ label = 'Drop keystores JSON files here, or click to select files',
+ showErrorMessage,
+ error: errorProp,
+ missingKeys,
+}: KeysBrainUploadProps) => {
+ const { brainUrl } = useDappnodeUrls();
+ const { getRootProps, keysFiles, removeFile, setKeysFiles } =
+ useKeystoreDrop(missingKeys);
+ const [isImportModalOpen, setIsImportModalOpen] = useState(false);
+
+ useEffect(() => {
+ if (keysFiles.length === 1) {
+ setIsImportModalOpen(true);
+ }
+ }, [keysFiles]);
+
+ const formObject = useForm({
+ mode: 'onChange',
+ });
+ useFormDepositData(formObject);
+
+ return (
+ <>
+ Upload keystores
+
+ The following pubkeys are not uploaded in the{' '}
+ Staking Brain:
+
+
+ {missingKeys.map((key, i) => (
+
+ ))}
+
+
+ {label}
+
+ {showErrorMessage && (
+ {}
+ )}
+
+ {keysFiles.length > 0 && (
+ <>
+
+
Uploaded Files:
+ {keysFiles.map((file, index) => (
+
+ {file.name}
+ removeFile(file.name)}>x
+
+ ))}
+
+ Password:
+
+ >
+ )}
+ >
+ );
+};
diff --git a/dappnode/import-keys/password-input.tsx b/dappnode/import-keys/password-input.tsx
new file mode 100644
index 00000000..a9e2b93e
--- /dev/null
+++ b/dappnode/import-keys/password-input.tsx
@@ -0,0 +1,33 @@
+import { Input } from '@lidofinance/lido-ui';
+import { EyeIcon } from 'dappnode/notifications/styles';
+import { useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { ReactComponent as EyeOn } from 'assets/icons/eye-on.svg';
+import { ReactComponent as EyeOff } from 'assets/icons/eye-off.svg';
+
+export const PasswordInput = () => {
+ const { setValue, watch } = useFormContext();
+ const password = watch('password');
+
+ const [showValue, setShowValue] = useState(false);
+
+ const toggleShowPass = () => {
+ setShowValue((prev) => !prev);
+ };
+
+ return (
+ {
+ setValue('password', e.target.value);
+ }}
+ rightDecorator={
+
+ {showValue ? : }
+
+ }
+ />
+ );
+};
diff --git a/dappnode/import-keys/styles.ts b/dappnode/import-keys/styles.ts
new file mode 100644
index 00000000..f726f7ad
--- /dev/null
+++ b/dappnode/import-keys/styles.ts
@@ -0,0 +1,42 @@
+import { ThemeName } from '@lidofinance/lido-ui';
+import styled from 'styled-components';
+
+export const DropZoneContainer = styled.div<{ hasError: boolean }>`
+ border: ${({ hasError, theme }) =>
+ hasError
+ ? `1px dashed ${theme.colors.error}`
+ : `1px dashed ${theme.colors.primary}`};
+ padding: 3em;
+ border-radius: 8px;
+ text-align: center;
+ cursor: pointer;
+ font-family: monospace;
+ font-size: ${({ theme }) => theme.fontSizesMap.xxs}px;
+ color: ${({ theme }) => theme.colors.textSecondary};
+
+ background-color: var(--lido-color-controlBg);
+
+ &:hover {
+ border: ${({ hasError, theme }) =>
+ hasError
+ ? `1px solid ${theme.colors.error}`
+ : `1px solid ${theme.colors.primary}`};
+ background-color: ${({ theme }) =>
+ theme.name === ThemeName.light ? '#F6F8FA' : '#252a2e'};
+ }
+`;
+
+export const KeystoreFileRow = styled.div`
+ display: flex;
+ align-items: center;
+
+ > div {
+ cursor: pointer;
+ color: ${({ theme }) => theme.colors.error};
+ font-weight: 700;
+ }
+
+ > p {
+ margin-right: 8px;
+ }
+`;
diff --git a/dappnode/import-keys/use-keystore-drop.ts b/dappnode/import-keys/use-keystore-drop.ts
new file mode 100644
index 00000000..68d3cafb
--- /dev/null
+++ b/dappnode/import-keys/use-keystore-drop.ts
@@ -0,0 +1,79 @@
+import { KeysFile } from 'features/add-keys/add-keys/context';
+import { useCallback, useEffect, useState } from 'react';
+import { useDropzone } from 'react-dropzone';
+import { useFormContext } from 'react-hook-form';
+
+const useKeystoreDrop = (missingKeys: string[]) => {
+ const [keysFiles, setKeysFiles] = useState([]);
+ const { setValue } = useFormContext();
+
+ useEffect(() => {
+ const keystores = [];
+ for (const keystore of keysFiles) {
+ keystores.push(keystore.content);
+ }
+ setValue('keystores', keystores, {
+ shouldValidate: false,
+ shouldDirty: true,
+ });
+ }, [keysFiles, setValue]);
+
+ const onDrop = useCallback(
+ (acceptedFiles: File[]) => {
+ acceptedFiles.forEach((file) => {
+ const reader = new FileReader();
+
+ reader.onloadend = () => {
+ try {
+ const parsedContent = JSON.parse(reader.result as string) as {
+ pubkey: string;
+ };
+
+ if (
+ !parsedContent.pubkey ||
+ !missingKeys.includes(parsedContent.pubkey)
+ ) {
+ alert(
+ `The file "${file.name}" is missing a valid pubkey or it's not in the missing keys list.`,
+ );
+ return;
+ }
+
+ // Remove duplicated keys
+ if (keysFiles.some((f) => f.name === file.name)) {
+ alert(`File "${file.name}" is already uploaded.`);
+ return;
+ }
+
+ setKeysFiles((prevFiles) => [
+ ...prevFiles,
+ { name: file.name, content: parsedContent },
+ ]);
+ } catch (e) {
+ alert(`Failed to parse JSON in file "${file.name}"`);
+ }
+ };
+ reader.readAsText(file);
+ });
+ },
+ [keysFiles, missingKeys],
+ );
+
+ const removeFile = useCallback((fileName: string) => {
+ setKeysFiles((prevFiles) =>
+ prevFiles.filter((file) => file.name !== fileName),
+ );
+ }, []);
+
+ const { getRootProps } = useDropzone({
+ maxFiles: missingKeys.length,
+ onDrop,
+ noKeyboard: true,
+ multiple: true,
+ accept: { 'application/json': ['.json'] },
+ });
+
+ return { getRootProps, keysFiles, removeFile, setKeysFiles };
+};
+
+export default useKeystoreDrop;
diff --git a/dappnode/notifications/input-tg.tsx b/dappnode/notifications/input-tg.tsx
new file mode 100644
index 00000000..ec47ad83
--- /dev/null
+++ b/dappnode/notifications/input-tg.tsx
@@ -0,0 +1,71 @@
+import {
+ ChangeEvent,
+ forwardRef,
+ useCallback,
+ useImperativeHandle,
+ useRef,
+ useState,
+} from 'react';
+import { StyledInput } from './styles';
+import { InputAddressProps } from 'shared/components/input-address/types';
+import { ReactComponent as EyeOn } from 'assets/icons/eye-on.svg';
+import { ReactComponent as EyeOff } from 'assets/icons/eye-off.svg';
+import { EyeIcon } from './styles';
+
+interface InputTgProps extends InputAddressProps {
+ isPassword?: boolean;
+}
+
+export const InputTelegram = forwardRef(
+ (
+ {
+ onChange,
+ value,
+ isLocked,
+ rightDecorator,
+ label,
+ isPassword = false,
+ ...props
+ },
+ ref,
+ ) => {
+ const inputRef = useRef(null);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+
+ const [showValue, setShowValue] = useState(false);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ useImperativeHandle(ref, () => inputRef.current!, []);
+
+ const handleChange = useCallback(
+ (e: ChangeEvent) => {
+ const currentValue = e.currentTarget.value;
+ onChange?.(currentValue);
+ },
+ [onChange],
+ );
+
+ const toggleShowPass = () => {
+ setShowValue((prev) => !prev);
+ };
+
+ return (
+ {label}>}
+ ref={inputRef}
+ value={value}
+ onChange={handleChange}
+ rightDecorator={
+ isPassword && (
+
+ {showValue ? : }
+
+ )
+ }
+ disabled={props.disabled || isLocked}
+ spellCheck="false"
+ />
+ );
+ },
+);
diff --git a/dappnode/notifications/notifications-component.tsx b/dappnode/notifications/notifications-component.tsx
new file mode 100644
index 00000000..8f93ad85
--- /dev/null
+++ b/dappnode/notifications/notifications-component.tsx
@@ -0,0 +1,175 @@
+import { FormBlock, FormTitle, Latice, Note, Stack } from 'shared/components';
+import { useCallback, useEffect, useState } from 'react';
+import { Button, Link, Loader } from '@lidofinance/lido-ui';
+import isTelegramUserId from 'dappnode/utils/is-tg-user-id';
+import isTelegramBotToken from 'dappnode/utils/is-tg-bot-token';
+import { BotTokenWrapper, InfoWrapper } from './styles';
+import { InputTelegram } from './input-tg';
+import { dappnodeLidoDocsUrls } from 'dappnode/utils/dappnode-docs-urls';
+import useGetTelegramData from 'dappnode/hooks/use-get-telegram-data';
+import usePostTelegramData from 'dappnode/hooks/use-post-telegram-data';
+import { ReactComponent as EyeOn } from 'assets/icons/eye-on.svg';
+import { ReactComponent as EyeOff } from 'assets/icons/eye-off.svg';
+import { EyeIcon } from './styles';
+import {
+ ErrorWrapper,
+ SuccessWrapper,
+} from 'dappnode/components/text-wrappers';
+import { NotificationsSteps } from './notifications-setup-steps';
+import { LoaderWrapperStyle } from 'shared/navigate/splash/loader-banner/styles';
+
+export const NotificationsComponent = () => {
+ const [newTgUserId, setNewTgUserId] = useState('');
+ const [isUserIdValid, setIsUserIDValid] = useState(false);
+
+ const {
+ telegramId,
+ botToken,
+ getTelegramData,
+ isLoading: isTgGetLoading,
+ } = useGetTelegramData();
+
+ const [newTgBotToken, setNewTgBotToken] = useState('');
+ const [isBotTokenValid, setIsBotTokenValid] = useState(false);
+ const [showCurrentBotToken, setShowCurrentBotToken] = useState(false);
+
+ const { postTelegramData, postTgError, isLoading, isSuccess } =
+ usePostTelegramData({
+ userId: Number(newTgUserId ? newTgUserId : telegramId),
+ botToken: newTgBotToken ? newTgBotToken : botToken || '',
+ });
+
+ const fetchData = useCallback(async () => {
+ await getTelegramData();
+ }, [getTelegramData]);
+
+ useEffect(() => {
+ void fetchData();
+ }, [fetchData]);
+
+ useEffect(() => {
+ setIsUserIDValid(isTelegramUserId(newTgUserId));
+ }, [newTgUserId]);
+
+ useEffect(() => {
+ setIsBotTokenValid(isTelegramBotToken(newTgBotToken));
+ }, [newTgBotToken]);
+
+ const sameAsCurrentUserId = newTgUserId == telegramId;
+ const sameAsCurrentBotToken = newTgBotToken === botToken;
+
+ const userValidationError = () => {
+ if (!newTgUserId) return null;
+ if (!isUserIdValid) return 'Specify a valid user ID';
+ if (sameAsCurrentUserId)
+ return 'Should not be the same as the current user ID';
+ return null;
+ };
+
+ const botTokenValidationError = () => {
+ if (!newTgBotToken) return null;
+ if (!isBotTokenValid) return 'Specify a valid bot token';
+ if (sameAsCurrentBotToken)
+ return 'Should not be the same as the current bot token';
+ return null;
+ };
+
+ const handleSubmit = async () => {
+ if (isUserIdValid || isBotTokenValid) {
+ await postTelegramData();
+ await fetchData();
+ setNewTgUserId('');
+ setNewTgBotToken('');
+ }
+ };
+
+ return (
+ <>
+
+ {!isTgGetLoading && (!telegramId || !botToken) && (
+
+ )}
+ Current Telegram Data:
+
+
+ User ID
+ {telegramId ? telegramId : '-'}
+
+
+
+ Bot Token
+ setShowCurrentBotToken(!showCurrentBotToken)}
+ >
+ {showCurrentBotToken ? : }
+
+
+
+ {botToken
+ ? showCurrentBotToken
+ ? botToken
+ : '*******************************'
+ : '-'}
+
+
+
+ Insert New Telegram Data:
+ setNewTgUserId(newValue)}
+ />
+ setNewTgBotToken(newValue)}
+ />
+ {postTgError && {postTgError} }
+ {isSuccess && (
+
+ Notifications configuration set! Ensure that test alert was sent to
+ your Telegram!
+
+ )}
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+ 'Update Telegram Data'
+ )}
+
+
+ You can find a guide on how to get this data in{' '}
+
+ our Documentation
+
+ .
+
+
+ >
+ );
+};
diff --git a/dappnode/notifications/notifications-modal.tsx b/dappnode/notifications/notifications-modal.tsx
new file mode 100644
index 00000000..34a44338
--- /dev/null
+++ b/dappnode/notifications/notifications-modal.tsx
@@ -0,0 +1,46 @@
+import { FC, useEffect, useState } from 'react';
+import Modal, { LinkWrapper } from 'dappnode/components/modal';
+import Link from 'next/link';
+import { PATH } from 'consts/urls';
+import useGetTelegramData from 'dappnode/hooks/use-get-telegram-data';
+
+export const NotificationsModal: FC = () => {
+ const { botToken, telegramId, getTelegramData } = useGetTelegramData();
+ const [isOpen, setIsOpen] = useState(false);
+ useEffect(() => {
+ const fetchData = async () => {
+ await getTelegramData();
+ };
+ void fetchData();
+ }, [getTelegramData]);
+
+ useEffect(() => {
+ if ((!botToken || !telegramId) && !localStorage.getItem('isTgSeen')) {
+ setIsOpen(true);
+ } else {
+ setIsOpen(false);
+ }
+ }, [botToken, telegramId]);
+
+ const handleClose = () => {
+ localStorage.setItem('isTgSeen', 'true');
+ setIsOpen(false);
+ };
+
+ return (
+ <>
+
+ Set up Notifications!
+
+ In order to get notifications about penalties, when your validators
+ exited, and other important events, you need to set up notifications.
+
+
+
+ Navigate here!
+
+
+
+ >
+ );
+};
diff --git a/dappnode/notifications/notifications-page.tsx b/dappnode/notifications/notifications-page.tsx
new file mode 100644
index 00000000..9c00767d
--- /dev/null
+++ b/dappnode/notifications/notifications-page.tsx
@@ -0,0 +1,17 @@
+import { FC } from 'react';
+import { Faq } from 'shared/components';
+
+import { NotificationsComponent } from './notifications-component';
+import { Layout } from 'shared/layout';
+import NotificationsTypes from './notifications-types';
+
+export const NotificationsPage: FC = () => (
+
+
+
+
+
+);
diff --git a/dappnode/notifications/notifications-setup-steps.tsx b/dappnode/notifications/notifications-setup-steps.tsx
new file mode 100644
index 00000000..b380cfaa
--- /dev/null
+++ b/dappnode/notifications/notifications-setup-steps.tsx
@@ -0,0 +1,39 @@
+import { FC } from 'react';
+import styled from 'styled-components';
+
+export const StepsList = styled.ol`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ align-items: start;
+ justify-content: start;
+ font-size: 14px;
+ text-align: left;
+
+ > li::marker {
+ color: var(--lido-color-primary);
+ font-weight: bold;
+ }
+`;
+
+const StepsWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ gap: 10px;
+ width: 100%;
+`;
+
+export const NotificationsSteps: FC = () => {
+ return (
+ <>
+
+
+ Get Telegram user Id
+ Create and get a Telegram Bot token
+ Start the chat with your bot
+
+
+ >
+ );
+};
diff --git a/dappnode/notifications/notifications-types.tsx b/dappnode/notifications/notifications-types.tsx
new file mode 100644
index 00000000..963ed188
--- /dev/null
+++ b/dappnode/notifications/notifications-types.tsx
@@ -0,0 +1,87 @@
+import { Section } from 'shared/components';
+import { AccordionNavigatable } from 'shared/components/accordion-navigatable';
+import { NotificationsList } from './styles';
+
+const avaliableNotifications = {
+ Exits: [
+ {
+ title: 'Validator requires exit 🚨',
+ value:
+ 'Your validator is automatically requested to exit due to certain conditions.',
+ },
+ {
+ title: 'Validator failed to exit, manual exit required 🚪',
+ value:
+ 'Your validator failed to exit automatically and requires manual intervention.',
+ },
+ {
+ title: 'Validator successfully exited 🚪',
+ value:
+ 'Your validator has successfully entered the exit queue without requiring manual action.',
+ },
+ ],
+ Performance: [
+ {
+ title: 'Operator in status stuck in latest report 🚨',
+ value:
+ "An operator is in a 'stuck' state for the specified epoch range. Performance should be checked.",
+ },
+ {
+ title: 'Operator bad performance in latest report 🚨',
+ value:
+ "The operator's performance was below the acceptable threshold during the specified epoch range.",
+ },
+ {
+ title: 'Operator good performance in latest report ✅',
+ value:
+ "The operator's performance exceeded the threshold during the specified epoch range.",
+ },
+ ],
+ Relays: [
+ {
+ title: 'Blacklisted relay 🚨',
+ value:
+ 'A blacklisted relay is currently being used, which is not allowed.',
+ },
+ {
+ title: 'Missing mandatory relay ⚠️',
+ value:
+ 'No mandatory relays are currently in use. Add at least one mandatory relay in the stakers UI.',
+ },
+ ],
+ Others: [
+ {
+ title: 'New distribution log updated 📦',
+ value:
+ 'A new distribution log has been updated and will be used for validator performance visualization.',
+ },
+ {
+ title: 'Execution client does not have logs receipts 🚨',
+ value:
+ 'The execution client is missing log receipts, preventing event scanning. Update your configuration or switch to a compatible client.',
+ },
+ {
+ title: 'CsModule events notifications 📋',
+ value:
+ 'Covers updates on rewards, penalties, new keys, and manager address proposals for the Lido CSModule smart contract.',
+ },
+ ],
+};
+
+export default function NotificationsTypes() {
+ return (
+
+ {Object.entries(avaliableNotifications).map(([key, value]) => (
+
+
+ {value.map((notification, i) => (
+
+ {notification.title} - {notification.value}
+
+ ))}
+
+
+ ))}
+
+ );
+}
diff --git a/dappnode/notifications/styles.ts b/dappnode/notifications/styles.ts
new file mode 100644
index 00000000..b775a627
--- /dev/null
+++ b/dappnode/notifications/styles.ts
@@ -0,0 +1,39 @@
+import styled from 'styled-components';
+import { Input } from '@lidofinance/lido-ui';
+import { TitledAddressStyle } from 'shared/components/titled-address/style';
+
+export const InfoWrapper = styled(TitledAddressStyle)`
+ padding: 22px 16px;
+ width: 100%;
+ > p {
+ font-weight: lighter;
+ font-size: 14px;
+ text-align: right;
+ }
+`;
+
+export const BotTokenWrapper = styled.div`
+ display: flex;
+ flex-direction: row;
+ gap: 12px;
+`;
+
+export const StyledInput = styled(Input)`
+ width: 100%;
+ input + span {
+ overflow: visible;
+ }
+`;
+
+export const EyeIcon = styled.div`
+ cursor: pointer;
+ display: flex;
+ margin-right: 5px;
+`;
+
+export const NotificationsList = styled.ul`
+ font-size: 14px;
+ > li {
+ margin-bottom: 20px;
+ }
+`;
diff --git a/dappnode/performance/components/performance-card.tsx b/dappnode/performance/components/performance-card.tsx
new file mode 100644
index 00000000..4f44554f
--- /dev/null
+++ b/dappnode/performance/components/performance-card.tsx
@@ -0,0 +1,37 @@
+import { Text } from '@lidofinance/lido-ui';
+import { Tooltip } from '@lidofinance/lido-ui';
+import {
+ CardTooltipWrapper,
+ PerformanceCardStyle,
+ TooltipIcon,
+} from './styles';
+
+interface PerformanceCardProps {
+ title: string;
+ tooltip?: React.ReactNode;
+ children: React.ReactNode;
+}
+
+export const PerformanceCard = ({
+ title,
+ tooltip,
+ children,
+}: PerformanceCardProps) => {
+ return (
+
+
+ {tooltip && (
+
+ ?
+
+ )}
+
+ {title}
+
+
+ {children}
+
+
+
+ );
+};
diff --git a/dappnode/performance/components/performance-table.tsx b/dappnode/performance/components/performance-table.tsx
new file mode 100644
index 00000000..cbaf2079
--- /dev/null
+++ b/dappnode/performance/components/performance-table.tsx
@@ -0,0 +1,80 @@
+import { Tbody, Td, Text, Th, Thead, Tr } from '@lidofinance/lido-ui';
+import { FC } from 'react';
+import { BeaconchainPubkeyLink } from 'shared/components';
+import { AddressRow, TableStyle, TooltipIcon } from './styles';
+import { ValidatorStats } from '../types';
+import { Tooltip } from '@lidofinance/lido-ui';
+
+interface PerformanceTableProps {
+ data: ValidatorStats[];
+ threshold: number;
+ offset?: number;
+}
+
+const tooltips = {
+ validator: "Your validator's index",
+ attestations:
+ 'Shows the number of attestations your validator has included compared to the number of attestations assigned to it for the selected range.',
+ efficiency:
+ "Shows your validator's attestation rate compared to Lido's threshold. Green indicates it's above the average; red means it's below.",
+};
+
+export const PerformanceTable: FC = ({
+ data,
+ threshold,
+}) => (
+
+
+
+
+
+ Validator ?
+
+
+
+
+ Attestations ?
+
+
+
+
+ Efficiency ?
+
+
+
+
+
+
+ {data?.map((validator, _) => (
+
+
+
+
+ {validator.index}
+
+
+
+
+
+
+ {validator.attestations.included +
+ ' / ' +
+ validator.attestations.assigned}
+
+
+
+ threshold ? 'success' : 'error'}
+ >
+ {validator.efficiency
+ ? (Number(validator.efficiency.toFixed(4)) * 100).toFixed(2) +
+ ' %'
+ : '-'}
+
+
+
+ ))}
+
+
+);
diff --git a/dappnode/performance/components/range-selector.tsx b/dappnode/performance/components/range-selector.tsx
new file mode 100644
index 00000000..c156db5e
--- /dev/null
+++ b/dappnode/performance/components/range-selector.tsx
@@ -0,0 +1,49 @@
+import { useState } from 'react';
+import { Dropdown, RangeDropdown, RangeWrapper } from './styles';
+import { Range } from '../types';
+import { ReactComponent as RoundedArrowIcon } from 'assets/icons/down-arrow.svg';
+
+interface RangeSelectorProps {
+ setRange: (value: Range) => void;
+ range: Range;
+ chainId: number;
+}
+
+export const RangeSelector = ({
+ chainId,
+ range,
+ setRange,
+}: RangeSelectorProps) => {
+ const [showDropdown, setShowDropdown] = useState(false);
+ const dropDownOptions: Range[] =
+ chainId !== 1
+ ? ['week', 'month', 'year', 'ever']
+ : ['month', 'year', 'ever'];
+
+ const handleRangeSelect = (value: Range) => {
+ setRange(value);
+ setShowDropdown(false);
+ };
+
+ return (
+
+ RANGE:
+ setShowDropdown(!showDropdown)}
+ >
+ {range.substring(0, 1).toUpperCase() + range.substring(1)}
+
+ {showDropdown && (
+
+ {dropDownOptions.map((r) => (
+ handleRangeSelect(r)}>
+ {r.substring(0, 1).toUpperCase() + r.substring(1)}
+
+ ))}
+
+ )}
+
+
+ );
+};
diff --git a/dappnode/performance/components/styles.ts b/dappnode/performance/components/styles.ts
new file mode 100644
index 00000000..a39fe171
--- /dev/null
+++ b/dappnode/performance/components/styles.ts
@@ -0,0 +1,227 @@
+import { Block, Table } from '@lidofinance/lido-ui';
+import { StackStyle } from 'shared/components/stack/style';
+import styled from 'styled-components';
+
+/**
+ * TABLE STYLES
+ */
+export const ViewKeysBlock = styled(Block)`
+ display: flex;
+ gap: ${({ theme }) => theme.spaceMap.md}px;
+ flex-direction: column;
+`;
+
+export const TableStyle = styled(Table)`
+ margin: -32px -32px;
+
+ thead tr::before,
+ thead tr::after,
+ th {
+ border-top: none;
+ }
+
+ th {
+ padding: 24px 8px 16px 8px;
+ min-width: 40px;
+ }
+ tr {
+ padding: 0;
+ }
+
+ td,
+ th {
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ td {
+ padding: 15px 0;
+ }
+
+ th > div {
+ text-align: center;
+ }
+
+ td {
+ border-bottom: none;
+ }
+
+ tbody tr:nth-child(odd) {
+ background-color: var(--lido-color-accentControlBg);
+ }
+
+ td > div > p {
+ text-align: center;
+ }
+`;
+
+export const TooltipIcon = styled.span`
+ color: var(--lido-color-textSecondary);
+ opacity: 0.5;
+ border: solid 1px var(--lido-color-textSecondary);
+ font-size: 10px;
+ border-radius: 100%;
+ padding: 0 4px;
+ margin-left: 2px;
+`;
+
+export const AddressRow = styled(StackStyle).attrs({ $gap: 'xs' })`
+ align-items: center;
+ justify-content: center;
+`;
+
+/**
+ * RANGE SELECTOR STYLES
+ */
+export const SelectedRangeWrapper = styled.div`
+ display: flex;
+ flex-direction: row;
+ gap: 5px;
+ align-items: center;
+ > div {
+ background-color: var(--lido-color-foreground);
+ padding: 4px 8px;
+ border-radius: 4px;
+ }
+`;
+
+export const RangeWrapper = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ gap: 15px;
+ color: var(--lido-color-textSecondary);
+ font-size: 14px;
+ font-weight: 400;
+ margin-top: 10px;
+`;
+
+export const RangeDropdown = styled.div`
+ min-width: 70px;
+ background-color: var(--lido-color-foreground);
+ padding: 8px 16px;
+ border-radius: 8px;
+ cursor: pointer;
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 10px;
+
+ svg {
+ transition: transform 0.3s ease-in-out;
+ transform: ${({ isOpen }: { isOpen: boolean }) =>
+ isOpen ? 'rotate(180deg)' : 'rotate(0deg)'};
+ }
+`;
+
+export const Dropdown = styled.div`
+ position: absolute;
+ top: 100%;
+ left: 0;
+ background-color: var(--lido-color-foreground);
+ border: 1px solid var(--lido-color-border);
+ border-radius: 8px;
+ margin-top: 8px;
+ padding: 8px 0;
+ width: 100%;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ z-index: 10;
+
+ div {
+ padding: 8px 16px;
+ cursor: pointer;
+ &:hover {
+ opacity: 0.6;
+ }
+ }
+`;
+
+/**
+ * CHART STYLES
+ */
+export const ChartSectionWrapper = styled.div`
+ width: 150%;
+`;
+
+export const ChartWrapper = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+`;
+
+// Generate unique colors for validator lines
+const colorPalette = ['#82ca9d', '#ff7300', '#8884d8', '#ffc658', '#0088FE'];
+export const getColor = (index: number) =>
+ colorPalette[index % colorPalette.length];
+
+export const LegendWrapper = styled.div`
+ display: flex;
+ gap: 20px;
+ flex-direction: column;
+ height: 400px;
+ overflow-y: auto;
+`;
+
+export const LegendItem = styled.div<{ color: string }>`
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ color: ${({ color }) => color};
+ cursor: pointer;
+
+ &::before {
+ content: '';
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ background-color: ${({ color }) => color};
+ border-radius: 50%;
+ }
+
+ :hover {
+ opacity: 0.8;
+ }
+`;
+
+export const ChartControlsWrapper = styled.div`
+ color: var(--lido-color-textSecondary);
+ margin: 20px 0px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+`;
+export const ChartControls = styled.div`
+ display: flex;
+ flex-direction: row;
+ gap: 5px;
+ width: 50%;
+ > input {
+ width: 100%;
+ }
+`;
+
+export const NoteWrapper = styled.div`
+ width: 70%;
+`;
+
+/**
+ * PERFORMANCE CARDS STYLES
+ */
+
+export const PerformanceCardStyle = styled(StackStyle).attrs({ $gap: 'sm' })`
+ border-radius: ${({ theme }) => theme.borderRadiusesMap.md}px;
+ padding: 16px 22px;
+ background: var(--lido-color-foreground);
+ display: flex;
+ flex-direction: column;
+ position: relative;
+`;
+
+export const CardTooltipWrapper = styled.div`
+ position: absolute;
+ top: 6px;
+ right: 10px;
+`;
diff --git a/dappnode/performance/index.ts b/dappnode/performance/index.ts
new file mode 100644
index 00000000..2f885303
--- /dev/null
+++ b/dappnode/performance/index.ts
@@ -0,0 +1 @@
+export * from './performance-page';
diff --git a/dappnode/performance/performance-cards-section.tsx b/dappnode/performance/performance-cards-section.tsx
new file mode 100644
index 00000000..f1150a97
--- /dev/null
+++ b/dappnode/performance/performance-cards-section.tsx
@@ -0,0 +1,47 @@
+import { Stack } from 'shared/components';
+import { PerformanceCard } from './components/performance-card';
+import { useGetNextReport } from 'dappnode/hooks/use-get-next-report';
+import { Link, Loader } from '@lidofinance/lido-ui';
+import { dappnodeLidoDocsUrls } from 'dappnode/utils/dappnode-docs-urls';
+import { useGetPendingReports } from 'dappnode/hooks/use-get-pending-reports';
+import { LoaderWrapperStyle } from 'shared/navigate/splash/loader-banner/styles';
+
+export const PerformanceCardsSection = () => {
+ const daysUntilNextReport = useGetNextReport();
+ const { pendingReports, isLoading } = useGetPendingReports();
+
+ return (
+
+
+ {daysUntilNextReport} {daysUntilNextReport === 1 ? 'day' : 'days'}
+
+
+ This represents the number of reports yet to be processed. If you
+ have active validators, it may include the performance of them.
+ These reports will be parsed automatically within the next hours.
+ Learn more about it in our{' '}
+
+ our Documentation
+
+
+ }
+ >
+ {isLoading ? (
+
+
+
+ ) : (
+
+ {pendingReports} {pendingReports === 1 ? 'report' : 'reports'}
+
+ )}
+
+
+ );
+};
diff --git a/dappnode/performance/performance-chart-section.tsx b/dappnode/performance/performance-chart-section.tsx
new file mode 100644
index 00000000..652e67d2
--- /dev/null
+++ b/dappnode/performance/performance-chart-section.tsx
@@ -0,0 +1,261 @@
+import { FC, useState, useEffect } from 'react';
+import {
+ LineChart,
+ Line,
+ CartesianGrid,
+ XAxis,
+ YAxis,
+ Tooltip as ChartTooltip,
+ ResponsiveContainer,
+} from 'recharts';
+import {
+ LegendWrapper,
+ LegendItem,
+ getColor,
+ ChartControls,
+ ChartControlsWrapper,
+ SelectedRangeWrapper,
+ ChartWrapper,
+ ChartSectionWrapper,
+ TooltipIcon,
+} from './components/styles';
+import { Range } from './types';
+import { WhenLoaded } from 'shared/components';
+import { Link, Tooltip, Text } from '@lidofinance/lido-ui';
+
+interface PerformanceChartProps {
+ isLoading: boolean;
+ thresholdsByEpoch: any[];
+ range: Range;
+ chainId: number;
+}
+
+export const PerformanceChartSection: FC = ({
+ isLoading,
+ thresholdsByEpoch,
+ range,
+ chainId,
+}) => {
+ const [reportsRange, setReportsRange] = useState(2); // Default value for 'week'
+ const [visibleValidators, setVisibleValidators] = useState([]);
+
+ // Since the minimum granularity in holesky is 'week' and in mainnet 'month', do not display the slider controls. Also, if there are less than 3 reports, do not display the slider controls
+ const displayChartControls =
+ thresholdsByEpoch.length > 2
+ ? chainId === 1 && range === 'month'
+ ? false
+ : chainId !== 1 && range === 'week'
+ ? false
+ : true
+ : false;
+
+ // Sets the report range based on the network (since reports are distrubuted differently) and also checks if db has less reports than available in range time
+ useEffect(() => {
+ if (range === 'month') {
+ setReportsRange(
+ chainId === 1
+ ? 2
+ : thresholdsByEpoch.length < 4
+ ? thresholdsByEpoch.length
+ : 4,
+ );
+ } else if (range === 'week') {
+ setReportsRange(2);
+ } else if (range === 'year') {
+ setReportsRange(
+ chainId === 1
+ ? thresholdsByEpoch.length < 12
+ ? thresholdsByEpoch.length
+ : 12
+ : thresholdsByEpoch.length < 52
+ ? thresholdsByEpoch.length
+ : 52,
+ );
+ } else {
+ setReportsRange(thresholdsByEpoch.length);
+ }
+ }, [chainId, range, thresholdsByEpoch]);
+
+ // Initialize `visibleValidators` to include all validator keys by default
+ useEffect(() => {
+ const allValidators = Array.from(
+ thresholdsByEpoch.reduce>((keys, entry) => {
+ Object.keys(entry).forEach((key) => {
+ if (key !== 'name' && key !== 'lidoThreshold') {
+ keys.add(key);
+ }
+ });
+ return keys;
+ }, new Set()),
+ );
+ setVisibleValidators(allValidators);
+ }, [thresholdsByEpoch]);
+
+ const visibleData = thresholdsByEpoch.slice(-reportsRange);
+
+ const handleRangeChange = (event: React.ChangeEvent) => {
+ const newRange = Math.max(2, parseInt(event.target.value, 10)); // Ensure the value is not below 2 since at least 2 reports needed to display the chart
+ setReportsRange(newRange);
+ };
+
+ const handleValidatorToggle = (validatorKey: string) => () => {
+ if (visibleValidators.includes(validatorKey)) {
+ setVisibleValidators((prev) =>
+ prev.filter((visibleValidator) => visibleValidator !== validatorKey),
+ );
+ } else {
+ setVisibleValidators((prev) => [...prev, validatorKey]);
+ }
+ };
+
+ return (
+ <>
+
+ Node Operator Efficiency vs Lido Average Efficiency
+
+
+
+
+
+
+
+ {/* Always render the line for lidoThreshold */}
+
+
+ {/* Render a line for each visibleValidator */}
+ {thresholdsByEpoch.length > 0 &&
+ Array.from(
+ thresholdsByEpoch.reduce>((keys, entry) => {
+ Object.keys(entry).forEach((key) => {
+ if (key !== 'name' && key !== 'lidoThreshold') {
+ keys.add(key);
+ }
+ });
+ return keys;
+ }, new Set()),
+ ).map(
+ (validatorKey, index) =>
+ visibleValidators.includes(validatorKey) && (
+
+ ),
+ )}
+
+
+
+
+
+
+
+
+
+
+ Lido Threshold
+ {thresholdsByEpoch.length > 0 &&
+ Array.from(
+ thresholdsByEpoch.reduce>((keys, entry) => {
+ Object.keys(entry).forEach((key) => {
+ if (key !== 'name' && key !== 'lidoThreshold') {
+ keys.add(key);
+ }
+ });
+ return keys;
+ }, new Set()),
+ ).map((validatorKey, index) => (
+
+ {validatorKey}
+
+ ))}
+
+
+
+ {displayChartControls && (
+
+
+
+ A frame is the period of time between the Lido CSM
+ reports. In each report, a new Lido threshold is
+ propagated. For more information, check out{' '}
+
+ Lido's Documentation
+
+ .
+ >
+ }
+ >
+
+ Frames to Display? :{' '}
+
+
+
+ {reportsRange}
+
+
+
+ 2
+
+ {thresholdsByEpoch.length}
+
+
+
+ )}
+
+
+ >
+ );
+};
diff --git a/dappnode/performance/performance-page.tsx b/dappnode/performance/performance-page.tsx
new file mode 100644
index 00000000..2f25557b
--- /dev/null
+++ b/dappnode/performance/performance-page.tsx
@@ -0,0 +1,49 @@
+import { FC, useState } from 'react';
+import { Faq } from 'shared/components';
+import { Layout } from 'shared/layout';
+import { useGetPerformanceByRange } from 'dappnode/hooks/use-get-performance-by-range';
+import { Range } from './types';
+
+import { PerformanceTableSection } from './performance-table-section';
+import { PerformanceChartSection } from './performance-chart-section';
+import { useAccount } from 'shared/hooks';
+import { RangeSelector } from './components/range-selector';
+import { PerformanceCardsSection } from './performance-cards-section';
+import { getConfig } from 'config';
+
+export const SummaryTablePage: FC = () => {
+ const { chainId } = useAccount();
+ const { defaultChain } = getConfig();
+ const [range, setRange] = useState('ever');
+ const { isLoading, validatorsStats, threshold, thresholdsByEpoch } =
+ useGetPerformanceByRange(range);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/dappnode/performance/performance-table-section.tsx b/dappnode/performance/performance-table-section.tsx
new file mode 100644
index 00000000..1411e19e
--- /dev/null
+++ b/dappnode/performance/performance-table-section.tsx
@@ -0,0 +1,33 @@
+import { FC } from 'react';
+import { WhenLoaded, Section } from 'shared/components';
+import { ViewKeysBlock } from './components/styles';
+import { PerformanceTable } from './components/performance-table';
+import { ValidatorStats } from './types';
+
+interface PerformanceTableProps {
+ isLoading: boolean;
+ validatorsStats: ValidatorStats[];
+ threshold: number;
+}
+
+export const PerformanceTableSection: FC = ({
+ isLoading,
+ validatorsStats,
+ threshold,
+}) => {
+ return (
+
+ );
+};
diff --git a/dappnode/performance/types.ts b/dappnode/performance/types.ts
new file mode 100644
index 00000000..cdaa725d
--- /dev/null
+++ b/dappnode/performance/types.ts
@@ -0,0 +1,8 @@
+export type Range = 'week' | 'month' | 'year' | 'ever';
+
+export interface ValidatorStats {
+ index: number;
+ attestations: { assigned: number; included: number };
+ // proposals: number;
+ efficiency: number;
+}
diff --git a/dappnode/starter-pack/step-wrapper.tsx b/dappnode/starter-pack/step-wrapper.tsx
new file mode 100644
index 00000000..cbd36d8c
--- /dev/null
+++ b/dappnode/starter-pack/step-wrapper.tsx
@@ -0,0 +1,14 @@
+import { FC, PropsWithChildren } from 'react';
+import { Number, StepContent, StepTitle, StepWrapper } from './styles';
+
+export const Step: FC<
+ PropsWithChildren<{ stepNum: string; title: string }>
+> = ({ stepNum: number, title, children }) => {
+ return (
+
+ {number}
+ {title}
+ {children}
+
+ );
+};
diff --git a/dappnode/starter-pack/steps.tsx b/dappnode/starter-pack/steps.tsx
new file mode 100644
index 00000000..f91ad5f2
--- /dev/null
+++ b/dappnode/starter-pack/steps.tsx
@@ -0,0 +1,424 @@
+import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events';
+import NextLink from 'next/link';
+import {
+ Dispatch,
+ FC,
+ SetStateAction,
+ useCallback,
+ useEffect,
+ useState,
+} from 'react';
+import { MatomoLink, Note } from 'shared/components';
+import { Step2InfraRow, InfraInstalledLabel, ButtonsRow } from './styles';
+import { Step } from './step-wrapper';
+import { Button, Link } from '@lidofinance/lido-ui';
+import { CONSTANTS_BY_NETWORK } from 'consts/csm-constants';
+import { PATH } from 'consts/urls';
+import { trackMatomoEvent } from 'utils';
+import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls';
+import { InputTelegram } from 'dappnode/notifications/input-tg';
+import { dappnodeLidoDocsUrls } from 'dappnode/utils/dappnode-docs-urls';
+import isTelegramUserID from 'dappnode/utils/is-tg-user-id';
+import isTelegramBotToken from 'dappnode/utils/is-tg-bot-token';
+import { useChainId } from 'wagmi';
+import usePostTelegramData from 'dappnode/hooks/use-post-telegram-data';
+import useApiBrain from 'dappnode/hooks/use-brain-keystore-api';
+import { useGetInfraStatus } from 'dappnode/hooks/use-get-infra-status';
+import useGetTelegramData from 'dappnode/hooks/use-get-telegram-data';
+import { Loader } from '@lidofinance/lido-ui';
+import { ErrorWrapper } from 'dappnode/components/text-wrappers';
+import { LoaderWrapperStyle } from 'shared/navigate/splash/loader-banner/styles';
+import { NotificationsSteps } from 'dappnode/notifications/notifications-setup-steps';
+import { CHAINS } from '@lido-sdk/constants';
+import useGetRelaysData from 'dappnode/hooks/use-get-relays-data';
+
+export const Steps: FC = () => {
+ const StepsTitles: Record = {
+ 1: 'Have Tokens for Bond',
+ 2: 'Set Up your node',
+ 3: 'Set up Notifications',
+ 4: 'Generate Keys',
+ };
+
+ const [step, setStep] = useState(1);
+
+ return step === 1 ? (
+
+ ) : step === 2 ? (
+
+ ) : step === 3 ? (
+
+ ) : step === 4 ? (
+
+ ) : (
+ 'Error: Please, reload the page!'
+ );
+};
+
+interface StepsProps {
+ step: number;
+ title: string;
+ setStep: Dispatch>;
+}
+
+const Step1: FC = ({ step, title, setStep }: StepsProps) => (
+ <>
+
+
+ Bond is a security collateral submitted by Node Operators
+ before uploading validator keys, covering potential losses from
+ inappropriate actions.
+
+
+
+
+ Learn more
+
+
+ {
+ setStep((prevState) => prevState + 1);
+ }}
+ >
+ Next
+
+ >
+);
+
+const Step2: FC = ({ step, title, setStep }: StepsProps) => {
+ const { ECStatus, CCStatus, isECLoading, isCCLoading } = useGetInfraStatus();
+ const { error: brainError, isLoading: brainLoading } = useApiBrain();
+ const { isMEVRunning, isLoading: relaysLoading } = useGetRelaysData();
+
+ const isECSynced: boolean = ECStatus === 'Synced';
+ const isCCSynced: boolean = CCStatus === 'Synced';
+
+ const isSignerInstalled: boolean = brainError ? false : true;
+
+ const isMEVInstalled: boolean = isMEVRunning ?? false;
+
+ const isNextBtnDisabled: boolean =
+ !isECSynced || !isCCSynced || !isSignerInstalled || !isMEVInstalled;
+
+ const { stakersUiUrl: stakersUrl } = useDappnodeUrls();
+
+ const chainId = useChainId();
+ return (
+ <>
+
+
+ In order to be a Node Operator you must have a synced{' '}
+ {CHAINS[chainId]} Node and run MEV Boost.
+
+
+ Execution Client
+ {'->'}
+
+ {isECLoading ? (
+
+
+
+ ) : (
+
+ {ECStatus}
+
+ )}
+
+
+
+ Consensus Client
+ {'->'}
+
+
+ {isCCLoading ? (
+
+
+
+ ) : (
+
+ {CCStatus}
+
+ )}
+
+
+
+ Web3signer
+ {'->'}
+
+ {brainLoading ? (
+
+
+
+ ) : (
+
+ {isSignerInstalled ? 'Installed' : 'Not installed'}
+
+ )}
+
+
+
+ MEV Boost
+ {'->'}
+
+ {relaysLoading ? (
+
+
+
+ ) : (
+
+ {isMEVInstalled ? 'Installed' : 'Not installed'}
+
+ )}
+
+
+ {!isECSynced ||
+ (!isCCSynced && (
+
+
+ You must have a synced {CHAINS[chainId]} Node and run MEV Boost.
+
+
+ ))}
+ {!!brainError && (
+
+ You must have Web3Signer installed.
+
+ )}
+ Set up your node{' '}
+
+
+
+ {
+ setStep((prevState) => prevState - 1);
+ }}
+ >
+ Back
+
+
+ {
+ setStep((prevState) => prevState + 1);
+ }}
+ disabled={
+ isNextBtnDisabled || isECLoading || isCCLoading || brainLoading
+ }
+ >
+ {'Next'}
+
+
+ >
+ );
+};
+
+const Step3: FC = ({ step, title, setStep }: StepsProps) => {
+ const {
+ botToken,
+ telegramId,
+ getTelegramData,
+ isLoading: isTgGetLoading,
+ } = useGetTelegramData();
+
+ const [tgUserId, setTgUserId] = useState('');
+ const [tgBotToken, setTgBotToken] = useState('');
+
+ const [isUserIdValid, setIsUserIDValid] = useState(false);
+ const [isBotTokenValid, setIsBotTokenValid] = useState(false);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ await getTelegramData();
+ };
+
+ void fetchData();
+ }, [getTelegramData]);
+
+ useEffect(() => {
+ setTgUserId(telegramId || '');
+ setTgBotToken(botToken || '');
+ }, [telegramId, botToken]);
+
+ const { postTelegramData, postTgError, isLoading, isSuccess } =
+ usePostTelegramData({
+ userId: Number(tgUserId),
+ botToken: tgBotToken,
+ });
+
+ useEffect(() => {
+ setIsUserIDValid(isTelegramUserID(tgUserId));
+ }, [tgUserId]);
+
+ useEffect(() => {
+ setIsBotTokenValid(isTelegramBotToken(tgBotToken));
+ }, [tgBotToken]);
+
+ const userValidationError = () => {
+ if (!tgUserId) return null;
+ if (!isUserIdValid) return 'Specify a valid user ID';
+ return null;
+ };
+
+ const botTokenValidationError = () => {
+ if (!tgBotToken) return null;
+ if (!isBotTokenValid) return 'Specify a valid bot token';
+ return null;
+ };
+
+ const handleNext = async () => {
+ if (isBotTokenValid && isUserIdValid) {
+ await postTelegramData();
+ } else {
+ setStep((prevState) => prevState + 1);
+ }
+ };
+
+ useEffect(() => {
+ if (isSuccess) {
+ void setStep((prevState) => prevState + 1);
+ }
+ }, [isSuccess, setStep]);
+ return (
+ <>
+
+
+ Dappnode's Notification system allows you to receive alerts
+ regardidng your node and validators directly to your telegram.
+
+ Both inputs are needed to set up alerts ensuring your privacy.
+ {!isTgGetLoading && (!telegramId || !botToken) && (
+
+ )}
+ setTgUserId(newValue)}
+ />
+ setTgBotToken(newValue)}
+ />
+
+
+ We highly recommend setting up these notifications to quickly detect
+ underperformance and avoid penalties.
+
+
+
+ You can find a guide on how to set notifications in{' '}
+
+ our Documentation
+
+ .
+
+ {postTgError && {postTgError} }
+
+ {
+ setStep((prevState) => prevState - 1);
+ }}
+ >
+ Back
+
+
+
+ {tgUserId || tgBotToken ? (
+ isLoading ? (
+
+
+
+ ) : (
+ 'Next'
+ )
+ ) : (
+ 'Skip'
+ )}
+
+
+ >
+ );
+};
+const Step4: FC = ({ step, title, setStep }: StepsProps) => {
+ const chainId = useChainId() as keyof typeof CONSTANTS_BY_NETWORK;
+
+ const withdrawalByAddres =
+ CONSTANTS_BY_NETWORK[chainId]?.withdrawalCredentials;
+
+ const handleClick = useCallback(() => {
+ trackMatomoEvent(MATOMO_CLICK_EVENTS_TYPES.starterPackCreateNodeOperator);
+ }, []);
+ return (
+ <>
+
+
+ In order to run a validator, you need to generate the necessary
+ keystores and deposit data.
+
+
+
+ Set {withdrawalByAddres} as the withdrawal address while
+ generating the keystores. This is the Lido Withdrawal Vault on Holesky{' '}
+
+
+ Prepare your deposit data (.json file) for submitting your keys in the
+ next step.
+
+
+ Just generate the keys, do NOT execute the deposits.
+
+
+
+ You can find a guide on how to generate keys in{' '}
+ our Documentation.
+
+
+ {
+ setStep((prevState) => prevState - 1);
+ }}
+ >
+ Back
+
+
+
+ Create Node Operator
+
+
+ >
+ );
+};
diff --git a/dappnode/starter-pack/styles.ts b/dappnode/starter-pack/styles.ts
new file mode 100644
index 00000000..4f747dfd
--- /dev/null
+++ b/dappnode/starter-pack/styles.ts
@@ -0,0 +1,143 @@
+import { Block, ThemeName } from '@lidofinance/lido-ui';
+import styled from 'styled-components';
+
+export const Header = styled.h1`
+ font-size: 48px; // @style
+ line-height: 52px; // @style
+ font-weight: 600;
+`;
+
+export const Heading = styled.header`
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ h2 {
+ font-size: 20px;
+ font-weight: 700;
+ line-height: 28px;
+ }
+
+ p {
+ color: var(--lido-color-textSecondary);
+ font-size: 12px;
+ line-height: 20px;
+ }
+`;
+
+export const ContentWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spaceMap.md}px;
+
+ text-align: left;
+ color: var(--lido-color-text);
+ font-size: ${({ theme }) => theme.fontSizesMap.xs}px;
+ line-height: ${({ theme }) => theme.fontSizesMap.xl}px;
+
+ ul {
+ color: var(--lido-color-primary);
+ padding-inline-start: 22px;
+ }
+`;
+
+export const BlockStyled = styled(Block)`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spaceMap.md}px;
+ border-radius: 32px; // @style
+
+ text-align: center;
+ color: var(--lido-color-text);
+ font-size: ${({ theme }) => theme.fontSizesMap.xxs}px;
+ line-height: ${({ theme }) => theme.fontSizesMap.lg}px;
+`;
+
+export const StepWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-height: 450px;
+ padding: 16px 40px; // @style
+ border-radius: 10px;
+ background: ${({ theme }) =>
+ theme.name === ThemeName.light ? '#eff2f6' : '#3e3d46'};
+`;
+
+export const Number = styled.div`
+ display: flex;
+ width: 60px;
+ height: 60px;
+ margin-bottom: 30px;
+ justify-content: center;
+ align-items: center;
+ border-radius: 100%;
+ background: var(--lido-color-foreground);
+
+ color: var(--lido-color-textSecondary);
+ font-size: 32px;
+ font-weight: 700;
+`;
+export const StepTitle = styled.h3`
+ font-size: 20px;
+ font-weight: 500;
+`;
+
+export const StepContent = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex: 1 0 40%;
+ align-items: center;
+ text-align: center;
+ justify-content: space-evenly;
+ gap: 10px;
+ padding-top: 10px;
+
+ p {
+ color: var(--lido-color-textSecondary);
+ font-size: 15px;
+ font-weight: 400;
+ text-align: left;
+ width: 100%;
+ line-height: 20px;
+ }
+`;
+
+export const Step2InfraRow = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+
+ ${({ theme }) => theme.mediaQueries.md} {
+ width: 100%;
+ }
+ & > p {
+ flex: 1;
+ width: 100%;
+ font-size: 14px;
+ text-align: center;
+ }
+`;
+
+export const InfraInstalledLabel = styled.span<{ $isInstalled: boolean }>`
+ color: var(
+ ${({ $isInstalled }) =>
+ $isInstalled ? '--lido-color-success' : '--lido-color-error'}
+ );
+ font-weight: 600;
+`;
+
+export const ButtonsRow = styled.div`
+ width: 100%;
+
+ display: flex;
+ flex-direction: row;
+ gap: ${({ theme }) => theme.spaceMap.md}px;
+
+ Button,
+ .button-wrapper {
+ flex: 1 1 0%;
+ }
+`;
diff --git a/dappnode/status/InfraItem.tsx b/dappnode/status/InfraItem.tsx
new file mode 100644
index 00000000..2e734ef5
--- /dev/null
+++ b/dappnode/status/InfraItem.tsx
@@ -0,0 +1,48 @@
+import { FC } from 'react';
+import { TitleStyled, ItemStyled, SubtitleStyled } from './styles';
+import { Loader, Tooltip } from '@lidofinance/lido-ui';
+import { StatusChip } from 'shared/components';
+import { InfraStatus } from './types';
+import { LoaderWrapperStyle } from 'shared/navigate/splash/loader-banner/styles';
+
+export type InfraItemProps = {
+ title: string;
+ subtitle: string;
+ tooltip?: string;
+ status: InfraStatus;
+ isLoading: boolean;
+};
+
+export const InfraItem: FC = ({
+ title,
+ tooltip,
+ subtitle,
+ status,
+ isLoading,
+}) => {
+ const body = (
+
+
+ {title}
+ {subtitle}
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+ );
+
+ if (tooltip) {
+ return (
+
+ {body}
+
+ );
+ }
+ return body;
+};
diff --git a/dappnode/status/import-keys-warning-modal.tsx b/dappnode/status/import-keys-warning-modal.tsx
new file mode 100644
index 00000000..0fa127cc
--- /dev/null
+++ b/dappnode/status/import-keys-warning-modal.tsx
@@ -0,0 +1,44 @@
+import { Checkbox, Link } from '@lidofinance/lido-ui';
+import Modal, { LinkWrapper } from 'dappnode/components/modal';
+import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls';
+import { useState } from 'react';
+
+interface ImportKeysWarningModalProps {
+ isOpen: boolean;
+ setIsOpen: (isOpen: boolean) => void;
+}
+export default function ImportKeysWarningModal({
+ isOpen,
+ setIsOpen,
+}: ImportKeysWarningModalProps) {
+ const { brainUrl } = useDappnodeUrls();
+ const [checked, setChecked] = useState(false);
+ const handleClose = () => {
+ setIsOpen(false);
+ };
+ return (
+
+ Key Import Advisory
+
+ It is crucial that the keys you are about to use are not active or
+ running on any other machine. Running the same keys in multiple
+ locations can lead to conflicts, loss of funds, or security
+ vulnerabilities.
+
+ Please confirm your understanding by checking the box below:
+ setChecked(e.target.checked)}
+ label="I understand it and promise I don't have these keys running somewhere else"
+ checked={checked}
+ />
+
+ {checked && (
+
+
+ Import keys
+
+
+ )}
+
+ );
+}
diff --git a/dappnode/status/status-section.tsx b/dappnode/status/status-section.tsx
new file mode 100644
index 00000000..6f400bc0
--- /dev/null
+++ b/dappnode/status/status-section.tsx
@@ -0,0 +1,80 @@
+import { FC } from 'react';
+import { SectionBlock, Stack } from 'shared/components';
+import { InfraItem, InfraItemProps } from './InfraItem';
+import { Card, Row } from './styles';
+import { Warnings } from './warnings';
+import { useGetInfraStatus } from 'dappnode/hooks/use-get-infra-status';
+import useApiBrain from 'dappnode/hooks/use-brain-keystore-api';
+import { capitalizeFirstChar } from 'dappnode/utils/capitalize-first-char';
+import { Link } from '@lidofinance/lido-ui';
+import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls';
+import useGetRelaysData from 'dappnode/hooks/use-get-relays-data';
+
+export const StatusSection: FC = () => {
+ const { ECName, ECStatus, CCName, CCStatus, isCCLoading, isECLoading } =
+ useGetInfraStatus();
+
+ const { isMEVRunning, isLoading: relaysLoading } = useGetRelaysData();
+ const {
+ pubkeys: brainKeys,
+ isLoading: brainLoading,
+ error: brainError,
+ } = useApiBrain();
+
+ const { stakersUiUrl } = useDappnodeUrls();
+
+ const infraItems: InfraItemProps[] = [
+ {
+ title: ECName ? capitalizeFirstChar(ECName) : '-',
+ subtitle: 'Execution Client',
+ status: ECStatus || 'Not installed',
+ isLoading: isECLoading,
+ },
+ {
+ title: CCName ? capitalizeFirstChar(CCName) : '-',
+ subtitle: 'Consensus Client',
+ status: CCStatus || 'Not installed',
+ isLoading: isCCLoading,
+ },
+ {
+ title: 'Web3signer',
+ subtitle: 'Signer',
+ status: brainKeys ? 'Installed' : 'Not installed',
+ isLoading: brainLoading,
+ },
+ {
+ title: 'MEV Boost',
+ subtitle: 'Relays',
+ status: isMEVRunning ? 'Installed' : 'Not installed',
+ isLoading: relaysLoading,
+ },
+ ];
+
+ return (
+
+
+
+
+ {infraItems.map((infra, i) => (
+
+ ))}
+
+
+ {!!brainError ||
+ ECStatus === 'Not installed' ||
+ CCStatus === 'Not installed' ||
+ !isMEVRunning ? (
+ Set up your node
+ ) : null}
+
+
+
+
+ );
+};
diff --git a/dappnode/status/styles.tsx b/dappnode/status/styles.tsx
new file mode 100644
index 00000000..bc787765
--- /dev/null
+++ b/dappnode/status/styles.tsx
@@ -0,0 +1,84 @@
+import { StackStyle } from 'shared/components/stack/style';
+import styled from 'styled-components';
+
+export const Card = styled(StackStyle).attrs({ $gap: 'sm' })`
+ border-radius: ${({ theme }) => theme.borderRadiusesMap.md}px;
+ padding: 12px 16px;
+ background: var(--lido-color-backgroundSecondary);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
+export const Row = styled(StackStyle).attrs({ $gap: 'sm' })`
+ width: 100%;
+ ${({ theme }) => theme.mediaQueries.lg} {
+ flex-direction: column;
+ gap: 12px;
+ }
+`;
+
+export const ItemStyled = styled(StackStyle).attrs({
+ $direction: 'column',
+ $gap: 'xs',
+})<{ $warning?: boolean }>`
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+ flex: 1 0 20%;
+ row-gap: 8px;
+ font-size: 14px;
+
+ color: var(
+ ${({ $warning }) => ($warning ? '--lido-color-error' : '--lido-color-text')}
+ );
+ text-align: center;
+ align-items: center;
+
+ ${({ theme }) => theme.mediaQueries.lg} {
+ flex-direction: row;
+ justify-content: space-between;
+ }
+`;
+
+export const TitleStyled = styled.b`
+ font-size: 14px;
+ font-weight: 700;
+`;
+export const SubtitleStyled = styled.p`
+ font-size: 12px;
+`;
+
+export const WarningCard = styled(StackStyle).attrs<{ $hasWarning?: boolean }>(
+ (props) => ({
+ $hasWarning: props.$hasWarning ?? true,
+ }),
+)<{ $hasWarning?: boolean }>`
+ justify-content: center;
+ text-align: center;
+ border-radius: ${({ theme }) => theme.borderRadiusesMap.md}px;
+ padding: 12px 16px;
+ background: ${({ $hasWarning }) =>
+ $hasWarning
+ ? 'color-mix(in srgb, var(--lido-color-error) 15%, transparent)'
+ : 'var(--lido-color-backgroundSecondary)'};
+`;
+
+export const NumWarningsLabel = styled.span`
+ font-weight: 600;
+ color: red;
+`;
+
+export const ValidatorMapStack = styled(StackStyle)`
+ ${({ theme }) => theme.mediaQueries.lg} {
+ width: 100%;
+ }
+`;
+
+export const AddressRow = styled(StackStyle).attrs({ $gap: 'xs' })`
+ align-items: center;
+ justify-content: center;
+`;
+
+export const Center = styled(StackStyle).attrs({ $gap: 'sm' })`
+ justify-content: center;
+ text-align: center;
+`;
diff --git a/dappnode/status/types.ts b/dappnode/status/types.ts
new file mode 100644
index 00000000..5053ed08
--- /dev/null
+++ b/dappnode/status/types.ts
@@ -0,0 +1,15 @@
+export type InfraStatus =
+ | 'Synced'
+ | 'Installed'
+ | 'Syncing'
+ | 'Not allowed'
+ | 'Not installed';
+
+export type AllowedRelay = {
+ Description: string;
+ IsMandatory: boolean;
+ Operator: string;
+ Uri: string;
+};
+
+export type WarnedValidator = { index: string; pubkey: string };
diff --git a/dappnode/status/warnings.tsx b/dappnode/status/warnings.tsx
new file mode 100644
index 00000000..46fa70cf
--- /dev/null
+++ b/dappnode/status/warnings.tsx
@@ -0,0 +1,226 @@
+import { FC, useEffect, useState } from 'react';
+import { BeaconchainPubkeyLink, Stack } from 'shared/components';
+import {
+ AddressRow,
+ NumWarningsLabel,
+ ValidatorMapStack,
+ WarningCard,
+} from './styles';
+import { LoaderWrapperStyle } from 'shared/navigate/splash/loader-banner/styles';
+import { Link, Loader, Tooltip } from '@lidofinance/lido-ui';
+import { Address } from '@lidofinance/address';
+import { WarnedValidator } from './types';
+import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls';
+import useMissingKeys from 'dappnode/hooks/use-missing-keys';
+import useGetExitRequests from 'dappnode/hooks/use-get-exit-requests';
+import ImportKeysWarningModal from './import-keys-warning-modal';
+import useGetRelaysData from 'dappnode/hooks/use-get-relays-data';
+import { useGetInfraStatus } from 'dappnode/hooks/use-get-infra-status';
+
+export const Warnings: FC = () => {
+ const { brainUrl, stakersUiUrl, MEVPackageConfig } = useDappnodeUrls();
+ const { missingKeys, keysLoading, error: errorBrain } = useMissingKeys();
+ const { exitRequests, getExitRequests } = useGetExitRequests();
+ const [isImportModalOpen, setIsImportModalOpen] = useState(false);
+ const { ECStatus, CCStatus, isCCLoading, isECLoading } = useGetInfraStatus();
+ const {
+ isMEVRunning,
+ hasMandatoryRelay,
+ mandatoryRelays,
+ usedBlacklistedRelays,
+ isLoading: relaysLoading,
+ } = useGetRelaysData();
+
+ const [validatorsExitRequests, setValidatorsExitRequests] = useState<
+ WarnedValidator[]
+ >([]);
+
+ const [numWarnings, setNumWarnings] = useState(0);
+ useEffect(() => {
+ void getExitRequests();
+ }, [getExitRequests]);
+
+ useEffect(() => {
+ if (exitRequests) {
+ Object.keys(exitRequests).forEach((key) => {
+ setValidatorsExitRequests((prevState) => [
+ ...prevState,
+ {
+ index: exitRequests[key].event.ValidatorIndex,
+ pubkey: exitRequests[key].validator_pubkey_hex,
+ },
+ ]);
+ });
+ }
+ }, [exitRequests]);
+
+ useEffect(() => {
+ setNumWarnings(
+ validatorsExitRequests.length +
+ (errorBrain ? 1 : missingKeys.length) +
+ (ECStatus === 'Not installed' ? 1 : 0) +
+ (CCStatus === 'Not installed' ? 1 : 0) +
+ (isMEVRunning ? 0 : 1) +
+ (isMEVRunning && mandatoryRelays && !hasMandatoryRelay ? 1 : 0) +
+ (isMEVRunning && usedBlacklistedRelays.length > 0 ? 1 : 0),
+ );
+ }, [
+ validatorsExitRequests,
+ errorBrain,
+ missingKeys,
+ ECStatus,
+ CCStatus,
+ isMEVRunning,
+ mandatoryRelays,
+ hasMandatoryRelay,
+ usedBlacklistedRelays,
+ ]);
+
+ return (
+
+ {keysLoading || isCCLoading || isECLoading || relaysLoading ? (
+
+
+
+ ) : (
+ <>
+ 0}>
+
+ {numWarnings > 0 ? (
+
+ You have {numWarnings} {' '}
+ warning/s
+
+ ) : (
+ "You don't have any warnings"
+ )}
+
+
+
+ {ECStatus === 'Not installed' && (
+
+ Your Execution Client is not installed!
+ Please, select and sync a client from the Stakers tab.
+ Set Execution Client
+
+ )}
+
+ {CCStatus === 'Not installed' && (
+
+ Your Consensus Client is not installed!
+ Please, select and sync a client from the Stakers tab.
+ Set Consensus Client
+
+ )}
+
+ {!errorBrain ? (
+ <>
+ {missingKeys.length > 0 && (
+
+ {missingKeys.length > 0 && (
+
+
+ {' '}
+
+ {missingKeys.length}
+ {' '}
+ keys are not imported in Web3Signer
+
+ {missingKeys.map((key, _) => (
+
+
+
+
+ ))}
+ setIsImportModalOpen(true)}>
+ Import keys
+
+
+
+
+ )}
+
+ )}
+ >
+ ) : (
+
+ Your Brain API is not Up!
+ Please, if Web3Signer is already installed, re-install it
+ Set Web3Signer
+
+ )}
+
+ {validatorsExitRequests.length > 0 && (
+
+
+
+
+
+ {validatorsExitRequests.length}
+ {' '}
+ Validator/s requested to exit
+
+
+ {validatorsExitRequests.map((val, _) => (
+
+ {val.index}
+
+
+ ))}
+ Exit validators
+
+
+ )}
+
+ {!isMEVRunning && (
+
+
+ MEV Boost Package is not running
+ Install or restart your MEV Boost Package
+ Set MEV Boost
+
+
+ )}
+
+ {isMEVRunning && mandatoryRelays && !hasMandatoryRelay && (
+
+
+ No mandatory Relays found
+
+ Select at least one of the mandatory MEV relays requested by
+ Lido
+
+ {mandatoryRelays.map((relay, i) => (
+ {'- ' + relay.Operator}
+ ))}
+ Set up Relays
+
+
+ )}
+
+ {isMEVRunning && usedBlacklistedRelays.length > 0 && (
+
+
+ Blacklisted Relays found
+ The following selected relays are blacklisted by Lido:
+ {usedBlacklistedRelays.map((relay, i) => (
+
+ ))}
+ Please, remove the Relays above from your MEV config
+ Remove blacklisted relays
+
+
+ )}
+ >
+ )}
+
+ );
+};
diff --git a/dappnode/utils/capitalize-first-char.ts b/dappnode/utils/capitalize-first-char.ts
new file mode 100644
index 00000000..e7e7bae0
--- /dev/null
+++ b/dappnode/utils/capitalize-first-char.ts
@@ -0,0 +1,3 @@
+export const capitalizeFirstChar = (str: string): string => {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+};
diff --git a/dappnode/utils/dappnode-docs-urls.ts b/dappnode/utils/dappnode-docs-urls.ts
new file mode 100644
index 00000000..5a448e4f
--- /dev/null
+++ b/dappnode/utils/dappnode-docs-urls.ts
@@ -0,0 +1,12 @@
+const baseUrl =
+ 'https://docs.dappnode.io/docs/user/staking/ethereum/lsd-pools/lido/';
+
+export const dappnodeLidoDocsUrls = {
+ register:
+ baseUrl + 'register/#first-steps-to-create-a-node-operator-in-dappnode',
+ generateKeys: baseUrl + 'register/#2-create-the-keystores--deposit-data',
+ notificationsOperatorExists:
+ baseUrl + 'already-node-operator#3-configuring-telegram-notifications',
+ notificationsNewOperator: baseUrl + 'register/#5-setup-notifications',
+ pendingHashes: baseUrl + 'performance',
+};
diff --git a/dappnode/utils/is-tg-bot-token.ts b/dappnode/utils/is-tg-bot-token.ts
new file mode 100644
index 00000000..b19edff6
--- /dev/null
+++ b/dappnode/utils/is-tg-bot-token.ts
@@ -0,0 +1,4 @@
+export default function isTelegramBotToken(token: string): boolean {
+ const telegramBotTokenPattern = /^\d{7,10}:[a-zA-Z0-9_-]{35}$/;
+ return telegramBotTokenPattern.test(token);
+}
diff --git a/dappnode/utils/is-tg-user-id.ts b/dappnode/utils/is-tg-user-id.ts
new file mode 100644
index 00000000..a6cc0e81
--- /dev/null
+++ b/dappnode/utils/is-tg-user-id.ts
@@ -0,0 +1,4 @@
+export default function isTelegramUserID(id: string): boolean {
+ const telegramUserIDPattern = /^\d+$/;
+ return telegramUserIDPattern.test(id);
+}
diff --git a/dappnode/utils/sanitize-urls.ts b/dappnode/utils/sanitize-urls.ts
new file mode 100644
index 00000000..12cdaf98
--- /dev/null
+++ b/dappnode/utils/sanitize-urls.ts
@@ -0,0 +1,26 @@
+export const sanitizeUrl = (url: string): string => {
+ if (!url) return '';
+
+ // Trim spaces
+ let sanitizedUrl = url.trim();
+
+ // Remove trailing slash unless it's just a root "/"
+ sanitizedUrl =
+ sanitizedUrl.endsWith('/') && sanitizedUrl !== '/'
+ ? sanitizedUrl.slice(0, -1)
+ : sanitizedUrl;
+
+ // Remove duplicate slashes (except in protocol)
+ sanitizedUrl = sanitizedUrl.replace(/([^:]\/)\/+/g, '$1');
+
+ // Encode special characters in the URL
+ try {
+ const urlObject = new URL(sanitizedUrl);
+ sanitizedUrl = urlObject.toString();
+ } catch (error) {
+ console.error('Invalid URL:', sanitizedUrl);
+ return '';
+ }
+
+ return sanitizedUrl;
+};
diff --git a/features/add-keys/add-keys/context/add-keys-form-provider.tsx b/features/add-keys/add-keys/context/add-keys-form-provider.tsx
index 33b13e32..d700d564 100644
--- a/features/add-keys/add-keys/context/add-keys-form-provider.tsx
+++ b/features/add-keys/add-keys/context/add-keys-form-provider.tsx
@@ -1,4 +1,4 @@
-import { FC, PropsWithChildren, useMemo } from 'react';
+import { FC, PropsWithChildren, useCallback, useMemo } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import {
FormControllerContext,
@@ -14,6 +14,8 @@ import { useAddKeysSubmit } from './use-add-keys-submit';
import { useAddKeysValidation } from './use-add-keys-validation';
import { useFormBondAmount } from './use-form-bond-amount';
import { useGetDefaultValues } from './use-get-default-values';
+// DAPPNODE
+import useBrainLaunchpadApi from 'dappnode/hooks/use-brain-launchpad-api';
export const useAddKeysFormData = useFormData;
@@ -23,6 +25,9 @@ export const AddKeysFormProvider: FC = ({ children }) => {
const asyncDefaultValues = useGetDefaultValues(networkData);
+ // DAPPNODE
+ const { submitKeystores: submitKeysToBrain } = useBrainLaunchpadApi();
+
const formObject = useForm({
defaultValues: asyncDefaultValues,
resolver: validationResolver,
@@ -39,15 +44,29 @@ export const AddKeysFormProvider: FC = ({ children }) => {
onRetry: retryFire,
});
+ // DAPPNODE
+ const handleSubmit = useCallback(
+ async (
+ input: AddKeysFormInputType,
+ networkData: AddKeysFormNetworkData,
+ ) => {
+ const { keystores, password } = formObject.getValues();
+ if (keystores && password)
+ await submitKeysToBrain({ keystores, password });
+ return await addKeys(input, networkData);
+ },
+ [addKeys, formObject, submitKeysToBrain], // dependencies
+ );
+
const formControllerValue: FormControllerContextValueType<
AddKeysFormInputType,
AddKeysFormNetworkData
> = useMemo(
() => ({
- onSubmit: addKeys,
+ onSubmit: handleSubmit,
retryEvent,
}),
- [addKeys, retryEvent],
+ [handleSubmit, retryEvent],
);
return (
diff --git a/features/add-keys/add-keys/context/types.ts b/features/add-keys/add-keys/context/types.ts
index d0feb472..c1cf006c 100644
--- a/features/add-keys/add-keys/context/types.ts
+++ b/features/add-keys/add-keys/context/types.ts
@@ -4,9 +4,17 @@ import { DepositDataInputType } from 'shared/hook-form/form-controller';
import { KeysAvailable, ShareLimitInfo } from 'shared/hooks';
import { BondBalance, LoadingRecord, NodeOperatorId } from 'types';
+// DAPPNODE
+export interface KeysFile {
+ name: string;
+ content: { pubkey: string };
+}
+
export type AddKeysFormInputType = {
token: TOKENS;
bondAmount?: BigNumber;
+ keystores?: KeysFile[]; //dappnode
+ password?: string; //dappnode
} & DepositDataInputType;
export type AddKeysFormNetworkData = {
diff --git a/features/add-keys/add-keys/controls/keys-input.tsx b/features/add-keys/add-keys/controls/keys-input.tsx
index 91332778..a5e18a98 100644
--- a/features/add-keys/add-keys/controls/keys-input.tsx
+++ b/features/add-keys/add-keys/controls/keys-input.tsx
@@ -4,6 +4,10 @@ import { useFormState } from 'react-hook-form';
import { FormTitle, MatomoLink } from 'shared/components';
import { DepositDataInputHookForm } from 'shared/hook-form/controls';
import { AddKeysFormInputType } from '../context';
+// DAPPNODE
+import useCheckImportedDepositKeys from 'dappnode/hooks/use-check-deposit-keys';
+import { KeysBrainUpload } from 'dappnode/import-keys/keys-input-form';
+import { useFormContext } from 'react-hook-form';
export const KeysInput = () => {
const { errors } = useFormState({
@@ -11,6 +15,11 @@ export const KeysInput = () => {
});
const error = errors.rawDepositData?.message || errors.depositData?.message;
+ // DAPPNODE
+ const { watch } = useFormContext();
+ const depositDataValue = watch('depositData');
+ const { missingKeys } = useCheckImportedDepositKeys(depositDataValue);
+
return (
<>
{
Upload deposit data
+ {missingKeys.length > 0 && (
+
+ )}
>
);
};
diff --git a/features/create-node-operator/submit-keys-form/context/submit-keys-form-provider.tsx b/features/create-node-operator/submit-keys-form/context/submit-keys-form-provider.tsx
index e766ab02..db0827f8 100644
--- a/features/create-node-operator/submit-keys-form/context/submit-keys-form-provider.tsx
+++ b/features/create-node-operator/submit-keys-form/context/submit-keys-form-provider.tsx
@@ -1,5 +1,5 @@
import { useModifyContext } from 'providers/modify-provider';
-import { FC, PropsWithChildren, useMemo } from 'react';
+import { FC, PropsWithChildren, useCallback, useMemo } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import {
FormControllerContext,
@@ -18,6 +18,8 @@ import { useGetDefaultValues } from './use-get-default-values';
import { useSubmitKeysFormNetworkData } from './use-submit-keys-form-network-data';
import { useSubmitKeysSubmit } from './use-submit-keys-submit';
import { useSubmitKeysValidation } from './use-submit-keys-validation';
+// DAPPNODE
+import useBrainLaunchpadApi from 'dappnode/hooks/use-brain-launchpad-api';
export const useSubmitKeysFormData = useFormData;
@@ -39,21 +41,37 @@ export const SubmitKeysFormProvider: FC = ({ children }) => {
useFormDepositData(formObject);
const { retryEvent, retryFire } = useFormControllerRetry();
+ // DAPPNODE
+ const { submitKeystores: submitKeysToBrain } = useBrainLaunchpadApi();
const submitKeys = useSubmitKeysSubmit({
onConfirm: revalidate,
onRetry: retryFire,
});
+ // DAPPNODE
+ const handleSubmit = useCallback(
+ async (
+ input: SubmitKeysFormInputType,
+ networkData: SubmitKeysFormNetworkData,
+ ) => {
+ const { keystores, password } = formObject.getValues();
+ if (keystores && password)
+ await submitKeysToBrain({ keystores, password });
+ return await submitKeys(input, networkData);
+ },
+ [formObject, submitKeys, submitKeysToBrain], // dependencies
+ );
+
const formControllerValue: FormControllerContextValueType<
SubmitKeysFormInputType,
SubmitKeysFormNetworkData
> = useMemo(
() => ({
- onSubmit: submitKeys,
+ onSubmit: handleSubmit,
retryEvent,
}),
- [submitKeys, retryEvent],
+ [handleSubmit, retryEvent],
);
return (
diff --git a/features/create-node-operator/submit-keys-form/context/types.ts b/features/create-node-operator/submit-keys-form/context/types.ts
index 6391b1ae..8e081b30 100644
--- a/features/create-node-operator/submit-keys-form/context/types.ts
+++ b/features/create-node-operator/submit-keys-form/context/types.ts
@@ -1,5 +1,6 @@
import { type TOKENS } from 'consts/tokens';
import { BigNumber } from 'ethers';
+import { KeysFile } from 'features/add-keys/add-keys/context/types';
import { DepositDataInputType } from 'shared/hook-form/form-controller';
import { KeysAvailable, ShareLimitInfo } from 'shared/hooks';
import { LoadingRecord, Proof } from 'types';
@@ -14,6 +15,8 @@ export type SubmitKeysFormInputType = {
extendedManagerPermissions: boolean;
specifyCustomAddresses: boolean;
specifyReferrrer: boolean;
+ keystores?: KeysFile[]; // DAPPNODE
+ password?: string; // DAPPNODE
} & DepositDataInputType;
export type SubmitKeysFormNetworkData = {
diff --git a/features/create-node-operator/submit-keys-form/controls/keys-input.tsx b/features/create-node-operator/submit-keys-form/controls/keys-input.tsx
index 95a53ef2..b3233016 100644
--- a/features/create-node-operator/submit-keys-form/controls/keys-input.tsx
+++ b/features/create-node-operator/submit-keys-form/controls/keys-input.tsx
@@ -4,6 +4,10 @@ import { useFormState } from 'react-hook-form';
import { FormTitle, MatomoLink } from 'shared/components';
import { DepositDataInputHookForm } from 'shared/hook-form/controls';
import { SubmitKeysFormInputType } from '../context';
+// DAPPNODE
+import useCheckImportedDepositKeys from 'dappnode/hooks/use-check-deposit-keys';
+import { KeysBrainUpload } from 'dappnode/import-keys/keys-input-form';
+import { useFormContext } from 'react-hook-form';
export const KeysInput = () => {
const { errors } = useFormState({
@@ -11,6 +15,11 @@ export const KeysInput = () => {
});
const error = errors.rawDepositData?.message || errors.depositData?.message;
+ // DAPPNODE
+ const { watch } = useFormContext();
+ const depositDataValue = watch('depositData');
+ const { missingKeys } = useCheckImportedDepositKeys(depositDataValue);
+
return (
<>
{
Upload deposit data
+ {missingKeys.length > 0 && (
+
+ )}
>
);
};
diff --git a/features/stealing/locked-section/locked-section.tsx b/features/stealing/locked-section/locked-section.tsx
index e73f1f1a..39c7c5e5 100644
--- a/features/stealing/locked-section/locked-section.tsx
+++ b/features/stealing/locked-section/locked-section.tsx
@@ -1,8 +1,10 @@
import { Block } from '@lidofinance/lido-ui';
import { FC } from 'react';
import { WhenLoaded } from 'shared/components';
-import { useNodeOperatorsWithLockedBond } from 'shared/hooks';
+// import { useNodeOperatorsWithLockedBond } from 'shared/hooks';
import { LockedTable } from './locked-table';
+// DAPPNODE
+import { useNodeOperatorsWithLockedBond } from 'dappnode/hooks/use-node-operators-with-locked-bond-api';
export const LockedSection: FC = () => {
const { data, initialLoading: loading } = useNodeOperatorsWithLockedBond();
diff --git a/features/welcome/try-csm/try-csm.tsx b/features/welcome/try-csm/try-csm.tsx
index a092b7b1..c5417868 100644
--- a/features/welcome/try-csm/try-csm.tsx
+++ b/features/welcome/try-csm/try-csm.tsx
@@ -6,11 +6,15 @@ import { FC } from 'react';
import { MatomoLink } from 'shared/components';
import { StyledBlock, StyledStack } from './styles';
import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events';
+// DAPPNODE
+import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls';
const { defaultChain } = getConfig();
export const TryCSM: FC = () => {
const isMainnet = defaultChain === CHAINS.Mainnet;
+ // DAPPNODE
+ const { installerTabUrl } = useDappnodeUrls();
if (isMainnet)
return (
@@ -19,6 +23,16 @@ export const TryCSM: FC = () => {
Try CSM on Holesky
+ {/* DAPPNODE */}
+
+
+ Join CSM Testnet
+
+
{
const [active, setActive] = useState();
const [cachedId, setCachedId] = useCachedId();
+ // DAPPNODE
+ const { backendUrl } = useDappnodeUrls();
useEffect(() => {
if (list) {
@@ -21,6 +25,36 @@ export const useGetActiveNodeOperator = (list?: NodeOperator[]) => {
active && setCachedId(active.id);
}, [active, setCachedId]);
+ useEffect(() => {
+ const postNodeOperatorId = async () => {
+ if (cachedId) {
+ try {
+ console.debug(`POSTing node operator id to events indexer API`);
+ const response = await fetch(
+ `${backendUrl}/api/v0/events_indexer/operatorId`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ operatorId: cachedId }),
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+ } catch (e) {
+ console.error(
+ `Error POSTing node operator id to events indexer API: ${e}`,
+ );
+ }
+ }
+ };
+
+ void postNodeOperatorId();
+ }, [backendUrl, cachedId]);
+
const switchActive = useCallback(
(id: NodeOperatorId) => {
const fromList = list?.find((item) => item.id === id);
diff --git a/shared/hooks/use-csm-node-operators.ts b/shared/hooks/use-csm-node-operators.ts
index 322944ab..31d35926 100644
--- a/shared/hooks/use-csm-node-operators.ts
+++ b/shared/hooks/use-csm-node-operators.ts
@@ -1,11 +1,14 @@
import { useLidoSWR } from '@lido-sdk/react';
import { STRATEGY_LAZY } from 'consts/swr-strategies';
-import { useNodeOperatorsFetcherFromEvents } from './use-node-operators-fetcher-from-events';
+// import { useNodeOperatorsFetcherFromEvents } from './use-node-operators-fetcher-from-events';
import { useAccount } from './use-account';
+// DAPPNODE
+import { useNodeOperatorsFetcherFromAPI } from 'dappnode/hooks/use-node-operators-fetcher-from-events-api';
export const useCsmNodeOperators = () => {
const { chainId, address } = useAccount();
- const fetcher = useNodeOperatorsFetcherFromEvents(address, chainId);
+ // const fetcher = useNodeOperatorsFetcherFromEvents(address, chainId);
+ const fetcher = useNodeOperatorsFetcherFromAPI(address);
return useLidoSWR(
['no-list', address, chainId],
diff --git a/shared/hooks/use-invites.ts b/shared/hooks/use-invites.ts
index 8dfa884d..57a36d12 100644
--- a/shared/hooks/use-invites.ts
+++ b/shared/hooks/use-invites.ts
@@ -1,7 +1,9 @@
import { useLidoSWR } from '@lido-sdk/react';
import { STRATEGY_LAZY } from 'consts/swr-strategies';
import { useAccount } from 'shared/hooks';
-import { useInvitesEventsFetcher } from './use-invites-events-fetcher';
+// import { useInvitesEventsFetcher } from './use-invites-events-fetcher';
+// DAPPNODE
+import { useInvitesEventsFetcher } from 'dappnode/hooks/use-invites-events-fetcher-api';
export const useInvites = (config = STRATEGY_LAZY) => {
const { chainId, address } = useAccount();
diff --git a/shared/hooks/use-keys-with-status.ts b/shared/hooks/use-keys-with-status.ts
index d84ebb35..bb9e8a39 100644
--- a/shared/hooks/use-keys-with-status.ts
+++ b/shared/hooks/use-keys-with-status.ts
@@ -4,14 +4,17 @@ import { useCallback, useMemo } from 'react';
import { HexString } from 'shared/keys';
import invariant from 'tiny-invariant';
import { compareLowercase, hasNoInterception } from 'utils';
-import { useExitRequestedKeysFromEvents } from './use-exit-requested-keys-from-events';
+//import { useExitRequestedKeysFromEvents } from './use-exit-requested-keys-from-events';
import { useKeysCLStatus } from './use-keys-cl-status';
import { useNetworkDuplicates } from './use-network-duplicates';
-import { useWithdrawnKeyIndexesFromEvents } from './use-withdrawn-key-indexes-from-events';
+//import { useWithdrawnKeyIndexesFromEvents } from './use-withdrawn-key-indexes-from-events';
import { useMergeSwr } from './useMergeSwr';
import { useNodeOperatorInfo } from './useNodeOperatorInfo';
import { useNodeOperatorKeys } from './useNodeOperatorKeys';
import { useNodeOperatorUnbondedKeys } from './useNodeOperatorUnbondedKeys';
+// DAPPNODE
+import { useWithdrawnKeyIndexesFromEvents } from 'dappnode/hooks/use-withdrawn-key-indexes-from-events-api';
+import { useExitRequestedKeysFromEvents } from 'dappnode/hooks/use-exit-requested-keys-from-events-api';
export type KeyWithStatus = {
key: HexString;
diff --git a/shared/navigate/gates/gate-loaded.tsx b/shared/navigate/gates/gate-loaded.tsx
index 9fc2bce2..cd510ebf 100644
--- a/shared/navigate/gates/gate-loaded.tsx
+++ b/shared/navigate/gates/gate-loaded.tsx
@@ -3,6 +3,12 @@ import { FC, PropsWithChildren, ReactNode } from 'react';
import { useAccount, useCsmPaused, useCsmPublicRelease } from 'shared/hooks';
import { useCsmEarlyAdoption } from 'shared/hooks/useCsmEarlyAdoption';
import { SplashPage } from '../splash';
+// DAPPNODE
+import { useECSanityCheck } from 'dappnode/hooks/use-ec-sanity-check';
+import { ECNotInstalledPage } from 'dappnode/fallbacks/ec-not-installed-page';
+import { ECNoLogsPage } from 'dappnode/fallbacks/ec-no-logs-page';
+import { ECSyncingPage } from 'dappnode/fallbacks/ec-syncing-page';
+import { ECScanningPage } from 'dappnode/fallbacks/ec-scanning-events';
type Props = {
fallback?: ReactNode;
@@ -10,7 +16,7 @@ type Props = {
};
export const GateLoaded: FC> = ({
- fallback = ,
+ fallback,
additional,
children,
}) => {
@@ -19,13 +25,43 @@ export const GateLoaded: FC> = ({
const { isConnecting } = useAccount();
const { isListLoading, active } = useNodeOperatorContext();
const { initialLoading: isEaLoading } = useCsmEarlyAdoption();
+ // DAPPNODE
+ const {
+ isInstalled,
+ isLoading: isECLoading,
+ isSynced,
+ hasLogs,
+ } = useECSanityCheck();
const loading =
isPublicReleaseLoading ||
isPausedLoading ||
isConnecting ||
isListLoading ||
- (!active && isEaLoading);
+ (!active && isEaLoading) ||
+ isECLoading; // DAPPNODE
- return <>{loading || additional ? fallback : children}>;
+ // DAPPNODE
+ isECLoading ? (
+
+ ) : !isInstalled ? (
+
+ ) : !isSynced ? (
+
+ ) : !hasLogs ? (
+
+ ) : isListLoading ? (
+
+ ) : (
+
+ );
+
+ // DAPPNODE
+ return (
+ <>
+ {loading || !isInstalled || !isSynced || !hasLogs || additional
+ ? fallback
+ : children}
+ >
+ );
};
diff --git a/yarn.lock b/yarn.lock
index c0add1b3..82c3c397 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3760,6 +3760,69 @@
dependencies:
"@types/node" "*"
+"@types/d3-array@^3.0.3":
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
+ integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
+
+"@types/d3-color@*":
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
+ integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
+
+"@types/d3-ease@^3.0.0":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
+ integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
+
+"@types/d3-interpolate@^3.0.1":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
+ integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
+ dependencies:
+ "@types/d3-color" "*"
+
+"@types/d3-path@*":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a"
+ integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==
+
+"@types/d3-path@^1":
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.11.tgz#45420fee2d93387083b34eae4fe6d996edf482bc"
+ integrity sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==
+
+"@types/d3-scale@^4.0.2":
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb"
+ integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==
+ dependencies:
+ "@types/d3-time" "*"
+
+"@types/d3-shape@^1":
+ version "1.3.12"
+ resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.12.tgz#8f2f9f7a12e631ce6700d6d55b84795ce2c8b259"
+ integrity sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==
+ dependencies:
+ "@types/d3-path" "^1"
+
+"@types/d3-shape@^3.1.0":
+ version "3.1.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555"
+ integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==
+ dependencies:
+ "@types/d3-path" "*"
+
+"@types/d3-time@*", "@types/d3-time@^3.0.0":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f"
+ integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==
+
+"@types/d3-timer@^3.0.0":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
+ integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
+
"@types/debug@^4.1.7":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917"
@@ -3935,6 +3998,14 @@
"@types/prop-types" "*"
csstype "^3.0.2"
+"@types/recharts@^1.8.29":
+ version "1.8.29"
+ resolved "https://registry.yarnpkg.com/@types/recharts/-/recharts-1.8.29.tgz#5e117521a65bf015b808350b45b65553ff5011f3"
+ integrity sha512-ulKklaVsnFIIhTQsQw226TnOibrddW1qUQNFVhoQEyY1Z7FRQrNecFCGt7msRuJseudzE9czVawZb17dK/aPXw==
+ dependencies:
+ "@types/d3-shape" "^1"
+ "@types/react" "*"
+
"@types/scheduler@*":
version "0.23.0"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.23.0.tgz#0a6655b3e2708eaabca00b7372fafd7a792a7b09"
@@ -5426,6 +5497,11 @@ clsx@^1.1.0, clsx@^1.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+clsx@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
+ integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
+
cluster-key-slot@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
@@ -5743,6 +5819,77 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
+ integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+ dependencies:
+ internmap "1 - 2"
+
+"d3-color@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+ integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-ease@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+ integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+"d3-format@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
+ integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+ integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+ dependencies:
+ d3-color "1 - 3"
+
+d3-path@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
+ integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-scale@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+ integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+ dependencies:
+ d3-array "2.10.0 - 3"
+ d3-format "1 - 3"
+ d3-interpolate "1.2.0 - 3"
+ d3-time "2.1.1 - 3"
+ d3-time-format "2 - 4"
+
+d3-shape@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
+ integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+ dependencies:
+ d3-path "^3.1.0"
+
+"d3-time-format@2 - 4":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+ integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+ dependencies:
+ d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
+ integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+ dependencies:
+ d3-array "2 - 3"
+
+d3-timer@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+ integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
damerau-levenshtein@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -5812,6 +5959,11 @@ decamelize@^1.1.0, decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+decimal.js-light@^2.4.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
+ integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
+
decode-uri-component@^0.2.0, decode-uri-component@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
@@ -6639,7 +6791,7 @@ event-target-shim@^5.0.0:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
-eventemitter3@^4.0.0, eventemitter3@^4.0.7:
+eventemitter3@^4.0.0, eventemitter3@^4.0.1, eventemitter3@^4.0.7:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
@@ -6746,6 +6898,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+fast-equals@^5.0.1:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.2.2.tgz#885d7bfb079fac0ce0e8450374bce29e9b742484"
+ integrity sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==
+
fast-glob@^3.2.9, fast-glob@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
@@ -7423,6 +7580,11 @@ internal-slot@^1.0.4, internal-slot@^1.0.7:
hasown "^2.0.0"
side-channel "^1.0.4"
+"internmap@1 - 2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+ integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
invariant@2, invariant@^2.2.2:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -9855,6 +10017,15 @@ react-jazzicon@^1.0.4:
dependencies:
mersenne-twister "^1.1.0"
+react-smooth@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.4.tgz#a5875f8bb61963ca61b819cedc569dc2453894b4"
+ integrity sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==
+ dependencies:
+ fast-equals "^5.0.1"
+ prop-types "^15.8.1"
+ react-transition-group "^4.4.5"
+
react-toastify@7.0.4:
version "7.0.4"
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-7.0.4.tgz#7d0b743f2b96f65754264ca6eae31911a82378db"
@@ -9862,7 +10033,7 @@ react-toastify@7.0.4:
dependencies:
clsx "^1.1.1"
-react-transition-group@4, react-transition-group@^4.4.2:
+react-transition-group@4, react-transition-group@^4.4.2, react-transition-group@^4.4.5:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
@@ -9935,6 +10106,27 @@ real-require@^0.2.0:
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
+recharts-scale@^0.4.4:
+ version "0.4.5"
+ resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9"
+ integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==
+ dependencies:
+ decimal.js-light "^2.4.1"
+
+recharts@^2.15.1:
+ version "2.15.1"
+ resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.1.tgz#0941adf0402528d54f6d81997eb15840c893aa3c"
+ integrity sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==
+ dependencies:
+ clsx "^2.0.0"
+ eventemitter3 "^4.0.1"
+ lodash "^4.17.21"
+ react-is "^18.3.1"
+ react-smooth "^4.0.4"
+ recharts-scale "^0.4.4"
+ tiny-invariant "^1.3.1"
+ victory-vendor "^36.6.8"
+
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
@@ -10969,7 +11161,7 @@ tiny-async-pool@^2.1.0:
resolved "https://registry.yarnpkg.com/tiny-async-pool/-/tiny-async-pool-2.1.0.tgz#3ec126568c18a7916912fb9fbecf812337ec6b84"
integrity sha512-ltAHPh/9k0STRQqaoUX52NH4ZQYAJz24ZAEwf1Zm+HYg3l9OXTWeqWKyYsHu40wF/F0rxd2N2bk5sLvX2qlSvg==
-tiny-invariant@^1.1.0:
+tiny-invariant@^1.1.0, tiny-invariant@^1.3.1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
@@ -11553,6 +11745,26 @@ vfile@^4.0.0:
unist-util-stringify-position "^2.0.0"
vfile-message "^2.0.0"
+victory-vendor@^36.6.8:
+ version "36.9.2"
+ resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801"
+ integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==
+ dependencies:
+ "@types/d3-array" "^3.0.3"
+ "@types/d3-ease" "^3.0.0"
+ "@types/d3-interpolate" "^3.0.1"
+ "@types/d3-scale" "^4.0.2"
+ "@types/d3-shape" "^3.1.0"
+ "@types/d3-time" "^3.0.0"
+ "@types/d3-timer" "^3.0.0"
+ d3-array "^3.1.6"
+ d3-ease "^3.0.1"
+ d3-interpolate "^3.0.1"
+ d3-scale "^4.0.2"
+ d3-shape "^3.1.0"
+ d3-time "^3.0.0"
+ d3-timer "^3.0.1"
+
wagmi@0.12.19:
version "0.12.19"
resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-0.12.19.tgz#5f5038330907f70c033ea51ef8a9136289567256"
From 56d23a74ccf9705082478f84b30425e689dfa879 Mon Sep 17 00:00:00 2001
From: pablomendezroyo
Date: Mon, 3 Feb 2025 13:04:22 +0100
Subject: [PATCH 02/19] fix: remove lido ci
---
.github/CODEOWNERS | 2 -
.github/PULL_REQUEST_TEMPLATE.md | 25 --------
.github/dependabot.yml | 7 ---
.github/workflows/checks.yml | 20 ------
.github/workflows/ci-dev.yml | 29 ---------
.github/workflows/ci-preview-demolish.yml | 27 --------
.github/workflows/ci-preview-deploy.yml | 68 ---------------------
.github/workflows/ci-prod.yml | 25 --------
.github/workflows/ci-staging.yml | 26 --------
.github/workflows/prepare-release-draft.yml | 14 -----
dappnode/hooks/use-dappnode-urls.ts | 10 ++-
11 files changed, 8 insertions(+), 245 deletions(-)
delete mode 100644 .github/CODEOWNERS
delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md
delete mode 100644 .github/dependabot.yml
delete mode 100644 .github/workflows/checks.yml
delete mode 100644 .github/workflows/ci-dev.yml
delete mode 100644 .github/workflows/ci-preview-demolish.yml
delete mode 100644 .github/workflows/ci-preview-deploy.yml
delete mode 100644 .github/workflows/ci-prod.yml
delete mode 100644 .github/workflows/ci-staging.yml
delete mode 100644 .github/workflows/prepare-release-draft.yml
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
deleted file mode 100644
index 9acf268e..00000000
--- a/.github/CODEOWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-* @lidofinance/community-staking
-.github @lidofinance/review-gh-workflows
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
deleted file mode 100644
index f5cea03c..00000000
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ /dev/null
@@ -1,25 +0,0 @@
-## Description
-
-
-
-## Related Issue
-
-
-
-
-
-
-## How Has This Been Tested?
-
-
-
-
-
-## Checklist
-
-- [ ] Requires other services change
-- [ ] Affects to other services
-- [ ] Requires dependency update
-- [ ] Automated tests
-- [ ] Looks good on large screens
-- [ ] Looks good on mobile
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 583decfd..00000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-version: 2
-updates:
- # Maintain dependencies for GitHub Actions
- - package-ecosystem: "github-actions"
- directory: "/"
- schedule:
- interval: "daily"
\ No newline at end of file
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
deleted file mode 100644
index d61e38fc..00000000
--- a/.github/workflows/checks.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-name: Tests and Checks
-
-on:
- pull_request:
-
-jobs:
- security:
- uses: lidofinance/linters/.github/workflows/security.yml@master
- permissions:
- security-events: write
- contents: read
-
- docker:
- uses: lidofinance/linters/.github/workflows/docker.yml@master
-
- todos:
- uses: lidofinance/linters/.github/workflows/todos.yml@master
-
- actions:
- uses: lidofinance/linters/.github/workflows/actions.yml@master
diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml
deleted file mode 100644
index dfe72ff2..00000000
--- a/.github/workflows/ci-dev.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: CI Dev
-
-on:
- workflow_dispatch:
- push:
- branches:
- - develop
- paths-ignore:
- - ".github/**"
-
-permissions: {}
-
-jobs:
- # test:
- # ...
-
- deploy:
- runs-on: ubuntu-latest
- # needs: test
- name: Build and deploy
- steps:
- - name: Testnet deploy
- uses: lidofinance/dispatch-workflow@v1
- env:
- APP_ID: ${{ secrets.APP_ID }}
- APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
- TARGET_REPO: "lidofinance/infra-mainnet"
- TARGET_WORKFLOW: "deploy_testnet_csm_widget.yaml"
- TARGET: "develop"
diff --git a/.github/workflows/ci-preview-demolish.yml b/.github/workflows/ci-preview-demolish.yml
deleted file mode 100644
index 0056c15f..00000000
--- a/.github/workflows/ci-preview-demolish.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-name: CI Preview stand demolish
-
-on:
- workflow_dispatch:
- pull_request:
- types:
- [converted_to_draft, closed]
- branches-ignore:
- - main
-
-permissions: {}
-
-jobs:
- deploy:
- runs-on: ubuntu-latest
- name: Build and deploy
- steps:
- - name: Preview stand deploying
- uses: lidofinance/dispatch-workflow@v1
- env:
- APP_ID: ${{ secrets.APP_ID }}
- APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
- TARGET_REPO: "lidofinance/infra-mainnet"
- TARGET: ${{ github.head_ref }}
- TARGET_WORKFLOW: "preview_stand_demolish.yaml"
- INPUTS_REPO_NAME: ${{ github.repository }}
- INPUTS_PR_ID: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/ci-preview-deploy.yml b/.github/workflows/ci-preview-deploy.yml
deleted file mode 100644
index f2bd30ce..00000000
--- a/.github/workflows/ci-preview-deploy.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-name: CI Preview stand deploy
-
-on:
- workflow_dispatch:
- inputs:
- inventory:
- description: inventory to be used for preview stand deploying
- default: testnet
- required: false
- type: choice
- options:
- - staging
- - testnet
-
- pull_request:
- types:
- [opened, synchronize, reopened, ready_for_review]
- branches-ignore:
- - main
-
-permissions:
- contents: read
- pull-requests: read
-
-jobs:
- deploy:
- runs-on: ubuntu-latest
- if: ${{ github.event.pull_request.draft == false }}
- name: Build and deploy
- outputs:
- stand_url: ${{ steps.stand.outputs.url }}
- steps:
- - uses: lidofinance/gh-find-current-pr@v1
- id: pr
-
- - name: Set ref
- id: ref
- run: echo "short_ref=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
-
- - name: Preview stand deploying
- uses: lidofinance/dispatch-workflow@v1
- env:
- APP_ID: ${{ secrets.APP_ID }}
- APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
- TARGET_REPO: "lidofinance/infra-mainnet"
- TARGET: ${{ github.head_ref || steps.ref.outputs.short_ref }}
- TARGET_WORKFLOW: "preview_stand_deploy.yaml"
- INPUTS_REPO_NAME: ${{ github.repository }}
- INPUTS_PR_ID: ${{ github.event.pull_request.number || steps.pr.outputs.number }}
- INPUTS_INVENTORY: "${{ inputs.inventory || 'testnet' }}"
-
- - name: Define repo short name
- run: echo "short_name=$(echo ${{ github.repository }} | cut -d "/" -f 2)" >> $GITHUB_OUTPUT
- id: repo
-
- - name: Define branch hash
- run: echo "hash=$(echo "$HEAD_REF" | shasum -a 256 | cut -c -10)" >> $GITHUB_OUTPUT
- id: branch
- env:
- HEAD_REF: ${{ github.head_ref || steps.ref.outputs.short_ref }}
-
- - name: Extract stand url
- if: always()
- run: echo "url=https://$SHORT_NAME-$BRANCH_HASH.branch-preview.org" >> $GITHUB_OUTPUT
- id: stand
- env:
- SHORT_NAME: ${{ steps.repo.outputs.short_name }}
- BRANCH_HASH: ${{ steps.branch.outputs.hash }}
diff --git a/.github/workflows/ci-prod.yml b/.github/workflows/ci-prod.yml
deleted file mode 100644
index b4800633..00000000
--- a/.github/workflows/ci-prod.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: CI Build prod image
-
-on:
- release:
- types: [released]
-
-permissions: {}
-
-jobs:
- # test:
- # ...
-
- deploy:
- runs-on: ubuntu-latest
- # needs: test
- name: Build and deploy
- steps:
- - name: Build prod image
- uses: lidofinance/dispatch-workflow@v1
- env:
- APP_ID: ${{ secrets.APP_ID }}
- APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
- TARGET_REPO: "lidofinance/infra-mainnet"
- TAG: '${{ github.event.release.tag_name }}'
- TARGET_WORKFLOW: "build_mainnet_csm_widget.yaml"
diff --git a/.github/workflows/ci-staging.yml b/.github/workflows/ci-staging.yml
deleted file mode 100644
index 4a85830a..00000000
--- a/.github/workflows/ci-staging.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: CI Staging
-
-on:
- workflow_dispatch:
- push:
- branches:
- - main
- paths-ignore:
- - ".github/**"
-
-permissions: {}
-
-jobs:
- deploy:
- if: ${{ github.repository != 'lidofinance/lido-frontend-template' }}
- runs-on: ubuntu-latest
- name: Build and deploy
- steps:
- - name: Staging deploy
- uses: lidofinance/dispatch-workflow@v1
- env:
- APP_ID: ${{ secrets.APP_ID }}
- APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
- TARGET_REPO: "lidofinance/infra-mainnet"
- TARGET_WORKFLOW: "deploy_staging_mainnet_csm_widget.yaml"
- TARGET: "main"
diff --git a/.github/workflows/prepare-release-draft.yml b/.github/workflows/prepare-release-draft.yml
deleted file mode 100644
index 8ea14dd0..00000000
--- a/.github/workflows/prepare-release-draft.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: Prepare release draft
-on:
- push:
- branches:
- - main
-
-permissions:
- contents: write
-
-jobs:
- prepare-release-draft:
- uses: lidofinance/actions/.github/workflows/prepare-release-draft.yml@main
- with:
- target: main
diff --git a/dappnode/hooks/use-dappnode-urls.ts b/dappnode/hooks/use-dappnode-urls.ts
index e4eab30a..c5e41c93 100644
--- a/dappnode/hooks/use-dappnode-urls.ts
+++ b/dappnode/hooks/use-dappnode-urls.ts
@@ -1,4 +1,5 @@
import { CHAINS } from '@lido-sdk/constants';
+import getConfig from 'next/config';
import { useAccount } from 'shared/hooks';
interface DappnodeUrls {
@@ -20,6 +21,7 @@ interface DappnodeUrls {
const useDappnodeUrls = () => {
const { chainId } = useAccount();
+ const { publicRuntimeConfig } = getConfig();
const urlsByChain: Partial> = {
[CHAINS.Mainnet]: {
@@ -30,7 +32,9 @@ const useDappnodeUrls = () => {
sentinelUrl: 'https://t.me/CSMSentinel_bot',
stakersUiUrl: 'http://my.dappnode/stakers/ethereum',
backendUrl: 'http://lido-events.lido-csm-mainnet.dappnode:8080',
- ECApiUrl: 'http://execution.mainnet.dncore.dappnode:8545',
+ ECApiUrl:
+ publicRuntimeConfig.rpcUrls_1 ||
+ 'http://execution.mainnet.dncore.dappnode:8545',
CCVersionApiUrl: '/api/consensus-version-mainnet',
CCStatusApiUrl: '/api/consensus-status-mainnet',
keysStatusUrl: '/api/keys-status-mainnet',
@@ -48,7 +52,9 @@ const useDappnodeUrls = () => {
sentinelUrl: 'https://t.me/CSMSentinelHolesky_bot',
stakersUiUrl: 'http://my.dappnode/stakers/holesky',
backendUrl: 'http://lido-events.lido-csm-holesky.dappnode:8080',
- ECApiUrl: 'http://execution.holesky.dncore.dappnode:8545',
+ ECApiUrl:
+ publicRuntimeConfig.rpcUrls_17000 ||
+ 'http://execution.holesky.dncore.dappnode:8545',
CCVersionApiUrl: '/api/consensus-version-holesky',
CCStatusApiUrl: '/api/consensus-status-holesky',
keysStatusUrl: '/api/keys-status-holesky',
From 6a0eb02280534b2fa81a6e780f3591bda54ea594 Mon Sep 17 00:00:00 2001
From: pablomendezroyo
Date: Mon, 3 Feb 2025 13:04:46 +0100
Subject: [PATCH 03/19] feat: add dappnode settings in next config
---
global.d.ts | 2 ++
next.config.mjs | 71 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 73 insertions(+)
diff --git a/global.d.ts b/global.d.ts
index a4d9b877..36d2f0da 100644
--- a/global.d.ts
+++ b/global.d.ts
@@ -52,9 +52,11 @@ declare module 'next/config' {
publicRuntimeConfig: {
basePath: string | undefined;
developmentMode: boolean;
+ // DAPPNODE
rpcUrls_1: string | undefined;
rpcUrls_17000: string | undefined;
defaultChain: number | undefined;
+ supportedChains: number[] | undefined;
};
};
diff --git a/next.config.mjs b/next.config.mjs
index 1a5f7f32..5d957aff 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -115,6 +115,70 @@ export default withBundleAnalyzer({
return config;
},
+
+ // DAPPNODE Rewrites to proxy the request
+ async rewrites() {
+ return [
+ {
+ source: '/api/consensus-version-mainnet',
+ destination:
+ 'http://beacon-chain.mainnet.dncore.dappnode:3500/eth/v1/node/version',
+ },
+ {
+ source: '/api/consensus-status-mainnet',
+ destination:
+ 'http://beacon-chain.mainnet.dncore.dappnode:3500/eth/v1/node/syncing',
+ },
+ {
+ source: '/api/consensus-version-holesky',
+ destination:
+ 'http://beacon-chain.holesky.dncore.dappnode:3500/eth/v1/node/version',
+ },
+ {
+ source: '/api/consensus-status-holesky',
+ destination:
+ 'http://beacon-chain.holesky.dncore.dappnode:3500/eth/v1/node/syncing',
+ },
+ {
+ source: '/api/keys-status-mainnet',
+ destination:
+ 'http://beacon-chain.mainnet.dncore.dappnode:3500/eth/v1/beacon/states/head/validators',
+ },
+ {
+ source: '/api/keys-status-holesky',
+ destination:
+ 'http://beacon-chain.holesky.dncore.dappnode:3500/eth/v1/beacon/states/head/validators',
+ },
+ {
+ source: '/api/brain-keys-mainnet',
+ destination:
+ 'http://brain.web3signer.dappnode:5000/api/v0/brain/validators?tag=lido&format=pubkey',
+ },
+ {
+ source: '/api/brain-keys-holesky',
+ destination:
+ 'http://brain.web3signer-holesky.dappnode:5000/api/v0/brain/validators?tag=lido&format=pubkey',
+ },
+ {
+ source: '/api/brain-launchpad-mainnet',
+ destination: 'http://brain.web3signer.dappnode:3000/eth/v1/keystores',
+ },
+ {
+ source: '/api/brain-launchpad-holesky',
+ destination:
+ 'http://brain.web3signer-holesky.dappnode:3000/eth/v1/keystores',
+ },
+ {
+ source: '/api/mev-status-mainnet',
+ destination: 'http://mev-boost.dappnode:18550/',
+ },
+ {
+ source: '/api/mev-status-holesky',
+ destination: 'http://mev-boost-holesky.dappnode:18550/',
+ },
+ ];
+ },
+
async headers() {
return [
{
@@ -189,5 +253,12 @@ export default withBundleAnalyzer({
publicRuntimeConfig: {
basePath,
developmentMode,
+ // DAPPNODE
+ rpcUrls_1: process.env.EL_RPC_URLS_1,
+ rpcUrls_17000: process.env.EL_RPC_URLS_17000,
+ defaultChain: parseInt(process.env.DEFAULT_CHAIN),
+ supportedChains: process.env?.SUPPORTED_CHAINS?.split(',').map((chainId) =>
+ parseInt(chainId, 10),
+ ),
},
});
From c408c783eb95b43deb2117ae1b87ef33e18e8064 Mon Sep 17 00:00:00 2001
From: pablomendezroyo
Date: Mon, 3 Feb 2025 13:07:28 +0100
Subject: [PATCH 04/19] fix: dockerfile adapt to dappnode needs
---
Dockerfile | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 9b66c081..4311ef82 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,22 +16,25 @@ RUN rm -rf /app/public/runtime && mkdir /app/public/runtime && chown node /app/p
FROM node:20-alpine as base
ARG BASE_PATH=""
-ARG SUPPORTED_CHAINS="1"
-ARG DEFAULT_CHAIN="1"
+# SUPPORTED_CHAINS and DEFAULT_CHAIN will be set in dappnode Lido CSM generic repository
+#ARG DEFAULT_CHAIN="1"
ENV NEXT_TELEMETRY_DISABLED=1 \
- BASE_PATH=$BASE_PATH \
- SUPPORTED_CHAINS=$SUPPORTED_CHAINS \
- DEFAULT_CHAIN=$DEFAULT_CHAIN
+ BASE_PATH=$BASE_PATH
+#SUPPORTED_CHAINS=$SUPPORTED_CHAINS \
+#DEFAULT_CHAIN=$DEFAULT_CHAIN
WORKDIR /app
RUN apk add --no-cache curl=~8
COPY --from=build /app /app
USER node
-EXPOSE 3000
+#EXPOSE 3000
+# DAPPNODE
+EXPOSE 80
HEALTHCHECK --interval=10s --timeout=3s \
- CMD curl -f http://localhost:3000/api/health || exit 1
+ CMD curl -f http://localhost/api/health || exit 1
+#CMD curl -f http://localhost:3000/api/health || exit 1
CMD ["yarn", "start"]
From e568ab265a9d2efaac7ce71f49066d4741555111 Mon Sep 17 00:00:00 2001
From: pablomendezroyo
Date: Mon, 3 Feb 2025 13:10:50 +0100
Subject: [PATCH 05/19] feat: add dappnode ci
---
.github/workflows/dappnode-release.yml | 85 ++++++++++++++++++++++++++
1 file changed, 85 insertions(+)
create mode 100644 .github/workflows/dappnode-release.yml
diff --git a/.github/workflows/dappnode-release.yml b/.github/workflows/dappnode-release.yml
new file mode 100644
index 00000000..58d7c7e2
--- /dev/null
+++ b/.github/workflows/dappnode-release.yml
@@ -0,0 +1,85 @@
+name: Release and Publish Docker Image
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version to release (optional, defaults to patch increment)'
+ required: false
+ default: ''
+
+jobs:
+ release:
+ name: Release and Publish Docker Image
+ runs-on: ubuntu-latest
+
+ steps:
+ # Checkout the repository
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ # Fetch all tags
+ - name: Fetch tags
+ run: git fetch --tags
+
+ # Determine the next version
+ - name: Determine release version
+ id: determine_version
+ run: |
+ # Get the input version
+ INPUT_VERSION="${{ github.event.inputs.version }}"
+
+ # Find the latest tag
+ LATEST_TAG=$(git tag --sort=-v:refname | head -n 1)
+
+ # If an input version is provided, use it
+ if [[ -n "$INPUT_VERSION" ]]; then
+ NEW_VERSION="$INPUT_VERSION"
+ else
+ # Increment the patch version by default
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_TAG"
+ NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
+ fi
+
+ # If no releases exist, default to 0.1.0
+ if [[ "$LATEST_TAG" == "0.0.0" ]]; then
+ NEW_VERSION="0.1.0"
+ fi
+
+ echo "New version: $NEW_VERSION"
+ echo "::set-output name=version::$NEW_VERSION"
+
+ # Create a GitHub release
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: ${{ steps.determine_version.outputs.version }}
+ name: Release ${{ steps.determine_version.outputs.version }}
+ body: |
+ Automatically generated release ${{ steps.determine_version.outputs.version }}.
+ draft: false
+ prerelease: false
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ # Push the new tag to the repository
+ - name: Push new tag
+ run: |
+ git tag ${{ steps.determine_version.outputs.version }}
+ git push origin ${{ steps.determine_version.outputs.version }}
+
+ # Log in to GitHub Docker Registry
+ - name: Log in to GitHub Docker Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ # Build and push Docker image
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ tags: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ steps.determine_version.outputs.version }}
From fa9589c5037249dbb5d5f1d3424a934e7d365277 Mon Sep 17 00:00:00 2001
From: mateumiralles
Date: Mon, 3 Feb 2025 13:44:09 +0100
Subject: [PATCH 06/19] fix: filter exits requests by validators status
---
dappnode/hooks/use-get-exit-requests.ts | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/dappnode/hooks/use-get-exit-requests.ts b/dappnode/hooks/use-get-exit-requests.ts
index 6b8c8ba7..9745c4b5 100644
--- a/dappnode/hooks/use-get-exit-requests.ts
+++ b/dappnode/hooks/use-get-exit-requests.ts
@@ -34,8 +34,18 @@ const useGetExitRequests = () => {
throw new Error(`HTTP error! Status: ${response.status}`);
}
- const data = await response.json();
- setExitRequests(data);
+ const data: ExitRequests = await response.json();
+
+ // Statuses to include if have not started/ended the exit process
+ const includedStatuses = ['active_ongoing', 'active_slashed'];
+
+ const filteredData = Object.fromEntries(
+ Object.entries(data).filter(([, exitRequest]) =>
+ includedStatuses.includes(exitRequest.status),
+ ),
+ );
+
+ setExitRequests(filteredData);
} catch (e) {
console.error(
`Error GETting validators exit requests from indexer API: ${e}`,
From c2dd6d16a142cd4ee56ae69f073db7264571bf05 Mon Sep 17 00:00:00 2001
From: mateumiralles
Date: Mon, 3 Feb 2025 16:34:14 +0100
Subject: [PATCH 07/19] fix: spinner on performance loading
---
dappnode/hooks/use-get-performance-by-range.ts | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/dappnode/hooks/use-get-performance-by-range.ts b/dappnode/hooks/use-get-performance-by-range.ts
index 4bd4d536..cc875e7b 100644
--- a/dappnode/hooks/use-get-performance-by-range.ts
+++ b/dappnode/hooks/use-get-performance-by-range.ts
@@ -4,8 +4,7 @@ import { Range, ValidatorStats } from '../performance/types';
import { useAccount } from 'shared/hooks';
export const useGetPerformanceByRange = (range: Range) => {
- const [isLoading, setIsLoading] = useState(true);
- const { operatorData } = useGetOperatorPerformance();
+ const { operatorData, isLoading } = useGetOperatorPerformance();
const [operatorDataByRange, setOperatorDataByRange] = useState<
Record
>({});
@@ -27,7 +26,6 @@ export const useGetPerformanceByRange = (range: Range) => {
useEffect(() => {
if (!operatorData) return;
- setIsLoading(true);
const sortedKeys = Object.keys(operatorData).sort((a, b) => {
const [startA] = a.split('-').map(Number);
const [startB] = b.split('-').map(Number);
@@ -168,10 +166,6 @@ export const useGetPerformanceByRange = (range: Range) => {
setValidatorsStats(result);
}, [operatorDataByRange]);
- useEffect(() => {
- setIsLoading(false);
- }, [validatorsStats]);
-
return {
isLoading,
validatorsStats,
From 0c99e76690f7ef5709e7a56e7aecb053d91a83a1 Mon Sep 17 00:00:00 2001
From: mateumiralles
Date: Mon, 3 Feb 2025 19:02:53 +0100
Subject: [PATCH 08/19] fix: rely on wallet provider instead of envs
---
dappnode/hooks/use-ec-sanity-check.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/dappnode/hooks/use-ec-sanity-check.ts b/dappnode/hooks/use-ec-sanity-check.ts
index 53cdf3ef..19a18067 100644
--- a/dappnode/hooks/use-ec-sanity-check.ts
+++ b/dappnode/hooks/use-ec-sanity-check.ts
@@ -1,6 +1,7 @@
import { CHAINS } from '@lido-sdk/constants';
import getConfig from 'next/config';
import { useEffect, useMemo, useState } from 'react';
+import { useAccount } from 'shared/hooks';
export const useECSanityCheck = () => {
const [isInstalled, setIsInstalled] = useState(false);
@@ -9,7 +10,7 @@ export const useECSanityCheck = () => {
const [isLoading, setIsLoading] = useState(true);
const { publicRuntimeConfig } = getConfig();
- const chainId = publicRuntimeConfig.defaultChain;
+ const { chainId } = useAccount();
const contractTx = useMemo(
() => ({
From cf1c485189b975852053035715a6ba28f63b07b9 Mon Sep 17 00:00:00 2001
From: pablomendezroyo
Date: Mon, 3 Feb 2025 21:48:20 +0100
Subject: [PATCH 09/19] fix: get exit requests with fetchretry
---
...use-exit-requested-keys-from-events-api.ts | 3 ++-
dappnode/hooks/use-get-exit-requests.ts | 16 ++++++--------
.../hooks/use-invites-events-fetcher-api.ts | 22 +------------------
...-node-operators-fetcher-from-events-api.ts | 21 +-----------------
...use-node-operators-with-locked-bond-api.ts | 22 +------------------
...e-withdrawn-key-indexes-from-events-api.ts | 22 +------------------
dappnode/utils/fetchWithRetry.ts | 20 +++++++++++++++++
7 files changed, 33 insertions(+), 93 deletions(-)
create mode 100644 dappnode/utils/fetchWithRetry.ts
diff --git a/dappnode/hooks/use-exit-requested-keys-from-events-api.ts b/dappnode/hooks/use-exit-requested-keys-from-events-api.ts
index f9a61974..28b76e1f 100644
--- a/dappnode/hooks/use-exit-requested-keys-from-events-api.ts
+++ b/dappnode/hooks/use-exit-requested-keys-from-events-api.ts
@@ -4,6 +4,7 @@ import { useNodeOperatorId } from 'providers/node-operator-provider';
import { useCallback } from 'react';
import { useAccount } from 'shared/hooks';
import useDappnodeUrls from './use-dappnode-urls';
+import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry';
interface ExitRequest {
event: {
@@ -49,7 +50,7 @@ export const useExitRequestedKeysFromEvents = () => {
headers: { 'Content-Type': 'application/json' },
};
- const response = await fetch(url, options);
+ const response = await fetchWithRetry(url, options, 5000);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
diff --git a/dappnode/hooks/use-get-exit-requests.ts b/dappnode/hooks/use-get-exit-requests.ts
index 9745c4b5..bcac7831 100644
--- a/dappnode/hooks/use-get-exit-requests.ts
+++ b/dappnode/hooks/use-get-exit-requests.ts
@@ -1,6 +1,7 @@
import { useState } from 'react';
import useDappnodeUrls from './use-dappnode-urls';
import { useActiveNodeOperator } from 'providers/node-operator-provider';
+import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry';
const useGetExitRequests = () => {
const { backendUrl } = useDappnodeUrls();
@@ -20,15 +21,12 @@ const useGetExitRequests = () => {
const getExitRequests = async () => {
try {
console.debug(`GETting validators exit requests from indexer API`);
- const response = await fetch(
- `${backendUrl}/api/v0/events_indexer/exit_requests?operatorId=${nodeOperator?.id}`,
- {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- },
- },
- );
+ const url = `${backendUrl}/api/v0/events_indexer/exit_requests?operatorId=${nodeOperator?.id}`;
+ const options = {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ };
+ const response = await fetchWithRetry(url, options, 5000);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
diff --git a/dappnode/hooks/use-invites-events-fetcher-api.ts b/dappnode/hooks/use-invites-events-fetcher-api.ts
index 4e724975..87565024 100644
--- a/dappnode/hooks/use-invites-events-fetcher-api.ts
+++ b/dappnode/hooks/use-invites-events-fetcher-api.ts
@@ -11,6 +11,7 @@ import {
NodeOperatorManagerAddressChangedEvent,
NodeOperatorRewardAddressChangedEvent,
} from 'generated/CSModule';
+import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry';
type AddressChangeProposedEvents =
| NodeOperatorManagerAddressChangeProposedEvent
@@ -18,27 +19,6 @@ type AddressChangeProposedEvents =
| NodeOperatorManagerAddressChangedEvent
| NodeOperatorRewardAddressChangedEvent;
-const fetchWithRetry = async (
- url: string,
- options: RequestInit,
- timeout: number,
-): Promise => {
- const shouldRetry = true;
- while (shouldRetry) {
- const response = await fetch(url, options);
- if (response.status === 202) {
- console.debug(
- `Received status 202. Retrying in ${timeout / 1000} seconds...`,
- );
- await new Promise((resolve) => setTimeout(resolve, timeout));
- } else {
- return response;
- }
- }
-
- return new Response(null);
-};
-
const parseEvents = (data: any) => {
return [
...(data.nodeOperatorManagerAddressChangeProposed || []).map(
diff --git a/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts b/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts
index 9e44bb6c..1703d7b7 100644
--- a/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts
+++ b/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
import useDappnodeUrls from './use-dappnode-urls';
import { NodeOperator } from 'types';
import { compareLowercase, mergeRoles } from 'utils';
+import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry';
/**
* This hook acts as an alternative to the hook `useNodeOperatorsFetcherFromEvents`.
@@ -77,26 +78,6 @@ const restoreEvents = (
}, [] as NodeOperator[]);
};
-const fetchWithRetry = async (
- url: string,
- options: RequestInit,
- timeout: number,
-): Promise => {
- const shouldRetry = true;
- while (shouldRetry) {
- const response = await fetch(url, options);
- if (response.status === 202) {
- console.debug(
- `Received status 202. Retrying in ${timeout / 1000} seconds...`,
- );
- await new Promise((resolve) => setTimeout(resolve, timeout));
- } else {
- return response;
- }
- }
-
- return new Response();
-};
const parseEvents = (data: any): NodeOperatorRoleEvent[] => {
const {
nodeOperatorAdded = [],
diff --git a/dappnode/hooks/use-node-operators-with-locked-bond-api.ts b/dappnode/hooks/use-node-operators-with-locked-bond-api.ts
index 7b1036f9..51cd5610 100644
--- a/dappnode/hooks/use-node-operators-with-locked-bond-api.ts
+++ b/dappnode/hooks/use-node-operators-with-locked-bond-api.ts
@@ -6,27 +6,7 @@ import { useCallback } from 'react';
import { useAccount, useCSAccountingRPC, useMergeSwr } from 'shared/hooks';
import { NodeOperatorId } from 'types';
import useDappnodeUrls from './use-dappnode-urls';
-
-const fetchWithRetry = async (
- url: string,
- options: RequestInit,
- timeout: number,
-): Promise => {
- const shouldRetry = true;
- while (shouldRetry) {
- const response = await fetch(url, options);
- if (response.status === 202) {
- console.debug(
- `Received status 202. Retrying in ${timeout / 1000} seconds...`,
- );
- await new Promise((resolve) => setTimeout(resolve, timeout));
- } else {
- return response;
- }
- }
-
- return new Response();
-};
+import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry';
const parseEvents = (data: any): ELRewardsStealingPenaltyReportedEvent[] => {
return data.map((event: any) => ({
diff --git a/dappnode/hooks/use-withdrawn-key-indexes-from-events-api.ts b/dappnode/hooks/use-withdrawn-key-indexes-from-events-api.ts
index f8a4362a..24e404ef 100644
--- a/dappnode/hooks/use-withdrawn-key-indexes-from-events-api.ts
+++ b/dappnode/hooks/use-withdrawn-key-indexes-from-events-api.ts
@@ -4,27 +4,7 @@ import { useNodeOperatorId } from 'providers/node-operator-provider';
import { useCallback } from 'react';
import { useAccount } from 'shared/hooks';
import useDappnodeUrls from './use-dappnode-urls';
-
-const fetchWithRetry = async (
- url: string,
- options: RequestInit,
- timeout: number,
-): Promise => {
- const shouldRetry = true;
- while (shouldRetry) {
- const response = await fetch(url, options);
- if (response.status === 202) {
- console.debug(
- `Received status 202. Retrying in ${timeout / 1000} seconds...`,
- );
- await new Promise((resolve) => setTimeout(resolve, timeout));
- } else {
- return response;
- }
- }
-
- return new Response();
-};
+import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry';
const parseEvents = (data: any) => {
return (data?.withdrawals || []).map((event: any) => ({
diff --git a/dappnode/utils/fetchWithRetry.ts b/dappnode/utils/fetchWithRetry.ts
new file mode 100644
index 00000000..74b37784
--- /dev/null
+++ b/dappnode/utils/fetchWithRetry.ts
@@ -0,0 +1,20 @@
+export const fetchWithRetry = async (
+ url: string,
+ options: RequestInit,
+ timeout: number,
+): Promise => {
+ const shouldRetry = true;
+ while (shouldRetry) {
+ const response = await fetch(url, options);
+ if (response.status === 202) {
+ console.debug(
+ `Received status 202. Retrying in ${timeout / 1000} seconds...`,
+ );
+ await new Promise((resolve) => setTimeout(resolve, timeout));
+ } else {
+ return response;
+ }
+ }
+
+ return new Response();
+};
From 91ad721467e7782a7f1a256c652c45e738023dbe Mon Sep 17 00:00:00 2001
From: Mateu Miralles <52827122+mateumiralles@users.noreply.github.com>
Date: Tue, 4 Feb 2025 08:49:11 +0100
Subject: [PATCH 10/19] fix: one spinner for each loading warning in
`/dashboard` (#6)
---
dappnode/status/styles.tsx | 5 +
dappnode/status/warnings.tsx | 286 +++++++++++++++++++----------------
2 files changed, 157 insertions(+), 134 deletions(-)
diff --git a/dappnode/status/styles.tsx b/dappnode/status/styles.tsx
index bc787765..f5726d45 100644
--- a/dappnode/status/styles.tsx
+++ b/dappnode/status/styles.tsx
@@ -60,6 +60,11 @@ export const WarningCard = styled(StackStyle).attrs<{ $hasWarning?: boolean }>(
$hasWarning
? 'color-mix(in srgb, var(--lido-color-error) 15%, transparent)'
: 'var(--lido-color-backgroundSecondary)'};
+
+ button {
+ border: none;
+ background: none;
+ }
`;
export const NumWarningsLabel = styled.span`
diff --git a/dappnode/status/warnings.tsx b/dappnode/status/warnings.tsx
index 46fa70cf..70e9a58e 100644
--- a/dappnode/status/warnings.tsx
+++ b/dappnode/status/warnings.tsx
@@ -76,151 +76,169 @@ export const Warnings: FC = () => {
usedBlacklistedRelays,
]);
+ const WarningWrapper: FC<{
+ isLoading?: boolean;
+ showIf: boolean;
+ children: React.ReactNode;
+ }> = ({ isLoading = false, showIf, children }) => {
+ return isLoading ? (
+
+
+
+ ) : (
+ showIf && <>{children}>
+ );
+ };
+
return (
- {keysLoading || isCCLoading || isECLoading || relaysLoading ? (
-
-
-
- ) : (
- <>
- 0}>
-
- {numWarnings > 0 ? (
-
- You have {numWarnings} {' '}
- warning/s
-
- ) : (
- "You don't have any warnings"
- )}
-
-
+
+ 0}>
+
+ {numWarnings > 0 ? (
+
+ You have {numWarnings} {' '}
+ warning/s
+
+ ) : (
+ "You don't have any warnings"
+ )}
+
+
+
- {ECStatus === 'Not installed' && (
-
- Your Execution Client is not installed!
- Please, select and sync a client from the Stakers tab.
- Set Execution Client
-
- )}
+
+
+ Your Execution Client is not installed!
+ Please, select and sync a client from the Stakers tab.
+ Set Execution Client
+
+
- {CCStatus === 'Not installed' && (
-
- Your Consensus Client is not installed!
- Please, select and sync a client from the Stakers tab.
- Set Consensus Client
-
- )}
+
+
+ Your Consensus Client is not installed!
+ Please, select and sync a client from the Stakers tab.
+ Set Consensus Client
+ {' '}
+
- {!errorBrain ? (
- <>
- {missingKeys.length > 0 && (
-
- {missingKeys.length > 0 && (
-
-
- {' '}
-
- {missingKeys.length}
- {' '}
- keys are not imported in Web3Signer
-
- {missingKeys.map((key, _) => (
-
-
-
-
- ))}
- setIsImportModalOpen(true)}>
- Import keys
-
-
-
-
- )}
-
- )}
- >
- ) : (
+
+
+ 0 && !errorBrain}>
- Your Brain API is not Up!
- Please, if Web3Signer is already installed, re-install it
- Set Web3Signer
-
- )}
+
+ {' '}
+ {missingKeys.length} keys
+ are not imported in Web3Signer
+
+ {missingKeys.map((key) => (
+
+
+
+
+ ))}
+ setIsImportModalOpen(true)}>
+ Import keys
+
- {validatorsExitRequests.length > 0 && (
-
-
-
-
-
- {validatorsExitRequests.length}
- {' '}
- Validator/s requested to exit
-
-
- {validatorsExitRequests.map((val, _) => (
-
- {val.index}
-
-
- ))}
- Exit validators
-
+
- )}
+
+
- {!isMEVRunning && (
-
-
- MEV Boost Package is not running
- Install or restart your MEV Boost Package
- Set MEV Boost
-
-
- )}
+
+
+ Your Brain API is not Up!
+ Please, if Web3Signer is already installed, re-install it
+ Set Web3Signer
+
+
- {isMEVRunning && mandatoryRelays && !hasMandatoryRelay && (
-
-
- No mandatory Relays found
-
- Select at least one of the mandatory MEV relays requested by
- Lido
-
- {mandatoryRelays.map((relay, i) => (
- {'- ' + relay.Operator}
- ))}
- Set up Relays
-
-
- )}
+ 0}>
+
+
+
+
+
+ {validatorsExitRequests.length}
+ {' '}
+ Validator/s requested to exit
+
+
+ {validatorsExitRequests.map((val) => (
+
+ {val.index}
+
+
+ ))}
+ Exit validators
+
+
+
+
- {isMEVRunning && usedBlacklistedRelays.length > 0 && (
-
-
- Blacklisted Relays found
- The following selected relays are blacklisted by Lido:
- {usedBlacklistedRelays.map((relay, i) => (
-
- ))}
- Please, remove the Relays above from your MEV config
- Remove blacklisted relays
-
-
- )}
- >
- )}
+
+
+
+
+ MEV Boost Package is not running
+ Install or restart your MEV Boost Package
+ Set MEV Boost
+
+
+
+
+
+
+ No mandatory Relays found
+
+ Select at least one of the mandatory MEV relays requested by
+ Lido
+
+ {mandatoryRelays?.map((relay, i) => (
+ {'- ' + relay.Operator}
+ ))}
+ Set up Relays
+
+
+
+
+ 0)}
+ >
+
+
+ Blacklisted Relays found
+ The following selected relays are blacklisted by Lido:
+ {usedBlacklistedRelays.map((relay, i) => (
+
+ ))}
+ Please, remove the Relays above from your MEV config
+ Remove blacklisted relays
+
+
+
+
);
};
From 180d5e87d91027affe5e8bc7fd4a4eb5146416fe Mon Sep 17 00:00:00 2001
From: pablomendezroyo
Date: Tue, 4 Feb 2025 08:55:53 +0100
Subject: [PATCH 11/19] fix: useffects dependencies
---
dappnode/hooks/use-brain-keystore-api.ts | 2 +-
dappnode/hooks/use-dappnode-urls.ts | 45 ++++++++++++++++--------
dappnode/hooks/use-ec-sanity-check.ts | 26 +++++++-------
dappnode/status/warnings.tsx | 2 +-
4 files changed, 44 insertions(+), 31 deletions(-)
diff --git a/dappnode/hooks/use-brain-keystore-api.ts b/dappnode/hooks/use-brain-keystore-api.ts
index bf12443d..02baf7f1 100644
--- a/dappnode/hooks/use-brain-keystore-api.ts
+++ b/dappnode/hooks/use-brain-keystore-api.ts
@@ -37,7 +37,7 @@ const useApiBrain = (interval = 60000) => {
}, interval);
return () => clearInterval(intervalId);
- }, [fetchPubkeys, interval]);
+ });
return { pubkeys, isLoading, error };
};
diff --git a/dappnode/hooks/use-dappnode-urls.ts b/dappnode/hooks/use-dappnode-urls.ts
index c5e41c93..a934a6aa 100644
--- a/dappnode/hooks/use-dappnode-urls.ts
+++ b/dappnode/hooks/use-dappnode-urls.ts
@@ -1,6 +1,5 @@
import { CHAINS } from '@lido-sdk/constants';
import getConfig from 'next/config';
-import { useAccount } from 'shared/hooks';
interface DappnodeUrls {
brainUrl: string;
@@ -20,7 +19,7 @@ interface DappnodeUrls {
}
const useDappnodeUrls = () => {
- const { chainId } = useAccount();
+ // Rely on runtime config to get the chainId and avoid nullish values when wallet is not connected from chainId
const { publicRuntimeConfig } = getConfig();
const urlsByChain: Partial> = {
@@ -66,23 +65,39 @@ const useDappnodeUrls = () => {
},
};
- const brainUrl = urlsByChain[chainId as CHAINS]?.brainUrl || '';
- const brainKeysUrl = urlsByChain[chainId as CHAINS]?.brainKeysUrl || '';
+ const brainUrl =
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.brainUrl || '';
+ const brainKeysUrl =
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.brainKeysUrl || '';
const brainLaunchpadUrl =
- urlsByChain[chainId as CHAINS]?.brainLaunchpadUrl || '';
- const signerUrl = urlsByChain[chainId as CHAINS]?.signerUrl || '';
- const sentinelUrl = urlsByChain[chainId as CHAINS]?.sentinelUrl || '';
- const stakersUiUrl = urlsByChain[chainId as CHAINS]?.stakersUiUrl || '';
- const backendUrl = urlsByChain[chainId as CHAINS]?.backendUrl || '';
- const ECApiUrl = urlsByChain[chainId as CHAINS]?.ECApiUrl || '';
- const CCVersionApiUrl = urlsByChain[chainId as CHAINS]?.CCVersionApiUrl || '';
- const CCStatusApiUrl = urlsByChain[chainId as CHAINS]?.CCStatusApiUrl || '';
- const keysStatusUrl = urlsByChain[chainId as CHAINS]?.keysStatusUrl || '';
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]
+ ?.brainLaunchpadUrl || '';
+ const signerUrl =
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.signerUrl || '';
+ const sentinelUrl =
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.sentinelUrl || '';
+ const stakersUiUrl =
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.stakersUiUrl || '';
+ const backendUrl =
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.backendUrl || '';
+ const ECApiUrl =
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.ECApiUrl || '';
+ const CCVersionApiUrl =
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.CCVersionApiUrl ||
+ '';
+ const CCStatusApiUrl =
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.CCStatusApiUrl ||
+ '';
+ const keysStatusUrl =
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.keysStatusUrl ||
+ '';
const installerTabUrl = (isMainnet: boolean) =>
urlsByChain[isMainnet ? 1 : 17000]?.installerTabUrl;
- const MEVApiUrl = urlsByChain[chainId as CHAINS]?.MEVApiUrl || '';
+ const MEVApiUrl =
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.MEVApiUrl || '';
const MEVPackageConfig =
- urlsByChain[chainId as CHAINS]?.MEVPackageConfig || '';
+ urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.MEVPackageConfig ||
+ '';
return {
brainUrl,
diff --git a/dappnode/hooks/use-ec-sanity-check.ts b/dappnode/hooks/use-ec-sanity-check.ts
index 19a18067..3f3d9ad5 100644
--- a/dappnode/hooks/use-ec-sanity-check.ts
+++ b/dappnode/hooks/use-ec-sanity-check.ts
@@ -1,17 +1,16 @@
import { CHAINS } from '@lido-sdk/constants';
import getConfig from 'next/config';
import { useEffect, useMemo, useState } from 'react';
-import { useAccount } from 'shared/hooks';
+import useDappnodeUrls from './use-dappnode-urls';
export const useECSanityCheck = () => {
const [isInstalled, setIsInstalled] = useState(false);
const [isSynced, setIsSynced] = useState(false);
const [hasLogs, setHasLogs] = useState(false);
const [isLoading, setIsLoading] = useState(true);
+ const { ECApiUrl } = useDappnodeUrls();
const { publicRuntimeConfig } = getConfig();
- const { chainId } = useAccount();
-
const contractTx = useMemo(
() => ({
[CHAINS.Mainnet]: `0xf5330dbcf09885ed145c4435e356b5d8a10054751bb8009d3a2605d476ac173f`,
@@ -20,16 +19,11 @@ export const useECSanityCheck = () => {
[],
);
- const rpcUrl =
- chainId == 1
- ? publicRuntimeConfig.rpcUrls_1
- : publicRuntimeConfig.rpcUrls_17000;
-
useEffect(() => {
const getSyncStatus = async () => {
try {
setIsLoading(true);
- const syncResponse = await fetch(`${rpcUrl}`, {
+ const syncResponse = await fetch(`${ECApiUrl}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -42,7 +36,7 @@ export const useECSanityCheck = () => {
}),
});
if (!syncResponse.ok) {
- chainId;
+ publicRuntimeConfig.defaultChain;
throw new Error(`HTTP error! Status: ${syncResponse.status}`);
}
@@ -59,14 +53,14 @@ export const useECSanityCheck = () => {
};
void getSyncStatus();
- }, [chainId, rpcUrl]);
+ });
useEffect(() => {
const getTxStatus = async () => {
try {
setIsLoading(true);
- const txResponse = await fetch(`${rpcUrl}`, {
+ const txResponse = await fetch(`${ECApiUrl}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -74,7 +68,11 @@ export const useECSanityCheck = () => {
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_getTransactionReceipt',
- params: [contractTx[chainId as keyof typeof contractTx]],
+ params: [
+ contractTx[
+ publicRuntimeConfig.defaultChain as keyof typeof contractTx
+ ],
+ ],
id: 0,
}),
});
@@ -94,7 +92,7 @@ export const useECSanityCheck = () => {
};
void getTxStatus();
- }, [chainId, contractTx, isSynced, rpcUrl]);
+ }, [ECApiUrl, contractTx, isSynced, publicRuntimeConfig.defaultChain]);
return { isSynced, isInstalled, hasLogs, isLoading };
};
diff --git a/dappnode/status/warnings.tsx b/dappnode/status/warnings.tsx
index 70e9a58e..27ab7de8 100644
--- a/dappnode/status/warnings.tsx
+++ b/dappnode/status/warnings.tsx
@@ -38,7 +38,7 @@ export const Warnings: FC = () => {
const [numWarnings, setNumWarnings] = useState(0);
useEffect(() => {
void getExitRequests();
- }, [getExitRequests]);
+ });
useEffect(() => {
if (exitRequests) {
From 8f1446f607bc5de9082f2deca5bb88e85ed18fe0 Mon Sep 17 00:00:00 2001
From: pablomendezroyo
Date: Tue, 4 Feb 2025 08:58:26 +0100
Subject: [PATCH 12/19] fix: change default values for use state hooks to avoid
frontend flickering
---
dappnode/hooks/use-ec-sanity-check.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/dappnode/hooks/use-ec-sanity-check.ts b/dappnode/hooks/use-ec-sanity-check.ts
index 3f3d9ad5..f57c99ac 100644
--- a/dappnode/hooks/use-ec-sanity-check.ts
+++ b/dappnode/hooks/use-ec-sanity-check.ts
@@ -4,10 +4,10 @@ import { useEffect, useMemo, useState } from 'react';
import useDappnodeUrls from './use-dappnode-urls';
export const useECSanityCheck = () => {
- const [isInstalled, setIsInstalled] = useState(false);
- const [isSynced, setIsSynced] = useState(false);
- const [hasLogs, setHasLogs] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
+ const [isInstalled, setIsInstalled] = useState(true); // Use default true to avoid frontend flickering with IsInstalled component
+ const [isSynced, setIsSynced] = useState(true); // Use default true to avoid frontend flickering with IsSynced component
+ const [hasLogs, setHasLogs] = useState(true); // Use default true to avoid frontend flickering with HasLogs component
+ const [isLoading, setIsLoading] = useState(false);
const { ECApiUrl } = useDappnodeUrls();
const { publicRuntimeConfig } = getConfig();
From 1a777a6400275dcdfa0286ab1369416311e87616 Mon Sep 17 00:00:00 2001
From: mateumiralles
Date: Tue, 4 Feb 2025 10:46:11 +0100
Subject: [PATCH 13/19] fix: remove spinner on keys warning
---
dappnode/status/warnings.tsx | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/dappnode/status/warnings.tsx b/dappnode/status/warnings.tsx
index 27ab7de8..e7b22bc0 100644
--- a/dappnode/status/warnings.tsx
+++ b/dappnode/status/warnings.tsx
@@ -93,8 +93,7 @@ export const Warnings: FC = () => {
return (
0}>
@@ -132,7 +131,7 @@ export const Warnings: FC = () => {
{' '}
-
+
0 && !errorBrain}>
From 2319a76324a6e0ae31491a37b44cd65fad717dba Mon Sep 17 00:00:00 2001
From: mateumiralles
Date: Tue, 4 Feb 2025 11:07:04 +0100
Subject: [PATCH 14/19] fix: exitsLoader warning
---
dappnode/hooks/use-get-exit-requests.ts | 6 +-
dappnode/status/warnings.tsx | 125 +++++++++++++-----------
2 files changed, 71 insertions(+), 60 deletions(-)
diff --git a/dappnode/hooks/use-get-exit-requests.ts b/dappnode/hooks/use-get-exit-requests.ts
index bcac7831..05a50568 100644
--- a/dappnode/hooks/use-get-exit-requests.ts
+++ b/dappnode/hooks/use-get-exit-requests.ts
@@ -6,6 +6,7 @@ import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry';
const useGetExitRequests = () => {
const { backendUrl } = useDappnodeUrls();
const [exitRequests, setExitRequests] = useState();
+ const [isLoading, setIsLoading] = useState(true);
const nodeOperator = useActiveNodeOperator();
@@ -20,6 +21,7 @@ const useGetExitRequests = () => {
const getExitRequests = async () => {
try {
+ setIsLoading(true);
console.debug(`GETting validators exit requests from indexer API`);
const url = `${backendUrl}/api/v0/events_indexer/exit_requests?operatorId=${nodeOperator?.id}`;
const options = {
@@ -44,14 +46,16 @@ const useGetExitRequests = () => {
);
setExitRequests(filteredData);
+ setIsLoading(false);
} catch (e) {
console.error(
`Error GETting validators exit requests from indexer API: ${e}`,
);
+ setIsLoading(false);
}
};
- return { exitRequests, getExitRequests };
+ return { exitRequests, getExitRequests, isLoading };
};
export default useGetExitRequests;
diff --git a/dappnode/status/warnings.tsx b/dappnode/status/warnings.tsx
index e7b22bc0..4b16a067 100644
--- a/dappnode/status/warnings.tsx
+++ b/dappnode/status/warnings.tsx
@@ -20,7 +20,11 @@ import { useGetInfraStatus } from 'dappnode/hooks/use-get-infra-status';
export const Warnings: FC = () => {
const { brainUrl, stakersUiUrl, MEVPackageConfig } = useDappnodeUrls();
const { missingKeys, keysLoading, error: errorBrain } = useMissingKeys();
- const { exitRequests, getExitRequests } = useGetExitRequests();
+ const {
+ exitRequests,
+ getExitRequests,
+ isLoading: exitsLoading,
+ } = useGetExitRequests();
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const { ECStatus, CCStatus, isCCLoading, isECLoading } = useGetInfraStatus();
const {
@@ -93,13 +97,19 @@ export const Warnings: FC = () => {
return (
0}>
{numWarnings > 0 ? (
- You have {numWarnings} {' '}
+ You have {numWarnings}
warning/s
) : (
@@ -128,68 +138,65 @@ export const Warnings: FC = () => {
Your Consensus Client is not installed!
Please, select and sync a client from the Stakers tab.
Set Consensus Client
- {' '}
+
-
-
- 0 && !errorBrain}>
-
-
- {' '}
- {missingKeys.length} keys
- are not imported in Web3Signer
-
- {missingKeys.map((key) => (
-
-
-
-
- ))}
- setIsImportModalOpen(true)}>
- Import keys
-
-
-
-
-
-
-
-
+
+ 0 && !errorBrain}>
- Your Brain API is not Up!
- Please, if Web3Signer is already installed, re-install it
- Set Web3Signer
-
-
+
+ {missingKeys.length} keys are
+ not imported in Web3Signer
+
+ {missingKeys.map((key) => (
+
+
+
+
+ ))}
+ setIsImportModalOpen(true)}>
+ Import keys
+
- 0}>
-
-
-
-
-
- {validatorsExitRequests.length}
- {' '}
- Validator/s requested to exit
-
-
- {validatorsExitRequests.map((val) => (
-
- {val.index}
-
-
- ))}
- Exit validators
-
+
+
+
+
+
+ Your Brain API is not Up!
+ Please, if Web3Signer is already installed, re-install it
+ Set Web3Signer
+
+
+
+ 0}>
+
+
+
+
+
+ {validatorsExitRequests.length}
+
+ Validator/s requested to exit
+
+
+ {validatorsExitRequests.map((val) => (
+
+ {val.index}
+
+
+ ))}
+ Exit validators
+
+
From c6490b1d1a718423e4dabcfe7bda8040c79ec4a6 Mon Sep 17 00:00:00 2001
From: mateumiralles
Date: Tue, 4 Feb 2025 12:18:42 +0100
Subject: [PATCH 15/19] fix: exit requests hook default loading state value to
false
---
dappnode/hooks/use-get-exit-requests.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dappnode/hooks/use-get-exit-requests.ts b/dappnode/hooks/use-get-exit-requests.ts
index 05a50568..e22e34ce 100644
--- a/dappnode/hooks/use-get-exit-requests.ts
+++ b/dappnode/hooks/use-get-exit-requests.ts
@@ -6,7 +6,7 @@ import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry';
const useGetExitRequests = () => {
const { backendUrl } = useDappnodeUrls();
const [exitRequests, setExitRequests] = useState();
- const [isLoading, setIsLoading] = useState(true);
+ const [isLoading, setIsLoading] = useState(false);
const nodeOperator = useActiveNodeOperator();
From 02abf5e6674fe185299bbd1676efc8444feb9c6d Mon Sep 17 00:00:00 2001
From: Mateu Miralles <52827122+mateumiralles@users.noreply.github.com>
Date: Tue, 4 Feb 2025 14:21:22 +0100
Subject: [PATCH 16/19] fix: execution gates on loading (#5)
---
pages/index.tsx | 20 +++++++++----
shared/hooks/use-show-rule.ts | 23 ++++++++++++++-
shared/navigate/gates/gate-loaded.tsx | 41 +++------------------------
3 files changed, 41 insertions(+), 43 deletions(-)
diff --git a/pages/index.tsx b/pages/index.tsx
index 87b735a4..ec49dc8a 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -1,4 +1,7 @@
import { SecretConfigType } from 'config';
+import { ECNoLogsPage } from 'dappnode/fallbacks/ec-no-logs-page';
+import { ECNotInstalledPage } from 'dappnode/fallbacks/ec-not-installed-page';
+import { ECSyncingPage } from 'dappnode/fallbacks/ec-syncing-page';
import { DashboardPage } from 'features/dashboard';
import { StarterPackPage } from 'features/starter-pack';
import { WelcomePage } from 'features/welcome';
@@ -14,13 +17,20 @@ const Page: FC = ({ maintenance }) => {
if (maintenance) return ;
return (
-
- }>
- }>
-
+ // DAPPNODE GATES: IS_EXECUTION_INSTALLED, IS_EXECUTION_SYNCED, EXECUTION_HAS_LOGS
+ }>
+ }>
+ }>
+
+ }>
+ }>
+
+
+
+
-
+
);
};
diff --git a/shared/hooks/use-show-rule.ts b/shared/hooks/use-show-rule.ts
index 92d8a41e..7cf36fce 100644
--- a/shared/hooks/use-show-rule.ts
+++ b/shared/hooks/use-show-rule.ts
@@ -1,3 +1,4 @@
+import { useECSanityCheck } from 'dappnode/hooks/use-ec-sanity-check';
import { useNodeOperatorContext } from 'providers/node-operator-provider';
import { useCallback } from 'react';
import {
@@ -17,7 +18,13 @@ export type ShowRule =
| 'HAS_REWARDS_ROLE'
| 'HAS_LOCKED_BOND'
| 'CAN_CREATE'
- | 'EL_STEALING_REPORTER';
+ | 'EL_STEALING_REPORTER'
+
+ // DAPPNODE
+ | 'IS_EXECUTION_LOADING'
+ | 'IS_EXECUTION_INSTALLED'
+ | 'IS_EXECUTION_SYNCED'
+ | 'EXECUTION_HAS_LOGS';
export const useShowRule = () => {
const { active: isConnectedWallet } = useAccount();
@@ -27,6 +34,9 @@ export const useShowRule = () => {
const { data: lockedBond } = useNodeOperatorLockAmount(nodeOperator?.id);
const canCreateNO = useCanCreateNodeOperator();
+ // DAPPNODE
+ const { isInstalled, isSynced, hasLogs } = useECSanityCheck();
+
return useCallback(
(condition: ShowRule): boolean => {
switch (condition) {
@@ -48,6 +58,14 @@ export const useShowRule = () => {
return !!lockedBond?.gt(0);
case 'EL_STEALING_REPORTER':
return !!isReportingRole;
+
+ // DAPPNODE
+ case 'IS_EXECUTION_INSTALLED':
+ return isInstalled;
+ case 'IS_EXECUTION_SYNCED':
+ return isSynced;
+ case 'EXECUTION_HAS_LOGS':
+ return hasLogs;
default:
return false;
}
@@ -59,6 +77,9 @@ export const useShowRule = () => {
invites?.length,
lockedBond,
isReportingRole,
+ isInstalled,
+ isSynced,
+ hasLogs,
],
);
};
diff --git a/shared/navigate/gates/gate-loaded.tsx b/shared/navigate/gates/gate-loaded.tsx
index cd510ebf..110380e3 100644
--- a/shared/navigate/gates/gate-loaded.tsx
+++ b/shared/navigate/gates/gate-loaded.tsx
@@ -3,11 +3,6 @@ import { FC, PropsWithChildren, ReactNode } from 'react';
import { useAccount, useCsmPaused, useCsmPublicRelease } from 'shared/hooks';
import { useCsmEarlyAdoption } from 'shared/hooks/useCsmEarlyAdoption';
import { SplashPage } from '../splash';
-// DAPPNODE
-import { useECSanityCheck } from 'dappnode/hooks/use-ec-sanity-check';
-import { ECNotInstalledPage } from 'dappnode/fallbacks/ec-not-installed-page';
-import { ECNoLogsPage } from 'dappnode/fallbacks/ec-no-logs-page';
-import { ECSyncingPage } from 'dappnode/fallbacks/ec-syncing-page';
import { ECScanningPage } from 'dappnode/fallbacks/ec-scanning-events';
type Props = {
@@ -16,7 +11,6 @@ type Props = {
};
export const GateLoaded: FC> = ({
- fallback,
additional,
children,
}) => {
@@ -25,43 +19,16 @@ export const GateLoaded: FC> = ({
const { isConnecting } = useAccount();
const { isListLoading, active } = useNodeOperatorContext();
const { initialLoading: isEaLoading } = useCsmEarlyAdoption();
+
// DAPPNODE
- const {
- isInstalled,
- isLoading: isECLoading,
- isSynced,
- hasLogs,
- } = useECSanityCheck();
+ const fallback = isListLoading ? : ;
const loading =
isPublicReleaseLoading ||
isPausedLoading ||
isConnecting ||
isListLoading ||
- (!active && isEaLoading) ||
- isECLoading; // DAPPNODE
-
- // DAPPNODE
- isECLoading ? (
-
- ) : !isInstalled ? (
-
- ) : !isSynced ? (
-
- ) : !hasLogs ? (
-
- ) : isListLoading ? (
-
- ) : (
-
- );
+ (!active && isEaLoading);
- // DAPPNODE
- return (
- <>
- {loading || !isInstalled || !isSynced || !hasLogs || additional
- ? fallback
- : children}
- >
- );
+ return <>{loading || additional ? fallback : children}>;
};
From b0a2566a9b3a6b9e52084a26b3da8ca8e0bed2f2 Mon Sep 17 00:00:00 2001
From: mateumiralles
Date: Wed, 5 Feb 2025 15:19:08 +0100
Subject: [PATCH 17/19] fix: adding `/performance` tab faqs
---
dappnode/performance/performance-page.tsx | 2 +-
faq/performance-1.md | 7 +++++++
faq/performance-2.md | 5 +++++
faq/performance-3.md | 5 +++++
faq/performance-4.md | 5 +++++
faq/performance-5.md | 5 +++++
faq/testnet-performance-1.md | 7 +++++++
faq/testnet-performance-2.md | 5 +++++
faq/testnet-performance-3.md | 5 +++++
faq/testnet-performance-4.md | 5 +++++
faq/testnet-performance-5.md | 5 +++++
lib/getFaq.ts | 14 ++++++++++++--
12 files changed, 67 insertions(+), 3 deletions(-)
create mode 100644 faq/performance-1.md
create mode 100644 faq/performance-2.md
create mode 100644 faq/performance-3.md
create mode 100644 faq/performance-4.md
create mode 100644 faq/performance-5.md
create mode 100644 faq/testnet-performance-1.md
create mode 100644 faq/testnet-performance-2.md
create mode 100644 faq/testnet-performance-3.md
create mode 100644 faq/testnet-performance-4.md
create mode 100644 faq/testnet-performance-5.md
diff --git a/dappnode/performance/performance-page.tsx b/dappnode/performance/performance-page.tsx
index 2f25557b..eb569065 100644
--- a/dappnode/performance/performance-page.tsx
+++ b/dappnode/performance/performance-page.tsx
@@ -21,7 +21,7 @@ export const SummaryTablePage: FC = () => {
return (
'keys-8',
'keys-9',
'keys-10',
- 'keys-13',
'keys-11',
- 'keys-12',
]);
export const getFaqBond = () =>
@@ -86,3 +84,15 @@ export const getFaqLocked = () =>
export const getFaqRoles = () =>
readFaqFiles(['roles-1', 'roles-2', 'roles-3', 'roles-4', 'roles-5']);
+
+export const getFaqNotifications = () =>
+ readFaqFiles(['notifications-1', 'notifications-2', 'notifications-3']);
+
+export const getFaqPerformance = () =>
+ readFaqFiles([
+ 'performance-1',
+ 'performance-2',
+ 'performance-3',
+ 'performance-4',
+ 'performance-5',
+ ]);
From afdd23d83fda15dd674de5e6c6ffed8c130222ac Mon Sep 17 00:00:00 2001
From: mateumiralles
Date: Wed, 5 Feb 2025 16:48:12 +0100
Subject: [PATCH 18/19] fix: adding `/notifications` faqs
---
faq/notifications-1.md | 9 +++++++++
faq/notifications-2.md | 7 +++++++
faq/notifications-3.md | 10 ++++++++++
faq/testnet-notifications-1.md | 9 +++++++++
faq/testnet-notifications-2.md | 7 +++++++
faq/testnet-notifications-3.md | 10 ++++++++++
6 files changed, 52 insertions(+)
create mode 100644 faq/notifications-1.md
create mode 100644 faq/notifications-2.md
create mode 100644 faq/notifications-3.md
create mode 100644 faq/testnet-notifications-1.md
create mode 100644 faq/testnet-notifications-2.md
create mode 100644 faq/testnet-notifications-3.md
diff --git a/faq/notifications-1.md b/faq/notifications-1.md
new file mode 100644
index 00000000..0b8df581
--- /dev/null
+++ b/faq/notifications-1.md
@@ -0,0 +1,9 @@
+---
+title: Why are notifications crucial?
+---
+
+Notifications are essential for staying informed about critical events within the Lido CSM protocol. By receiving alerts about exit requests, deposits, penalties, slashing incidents, and smart contract events, you can proactively manage your staking operations and address issues promptly.
+
+Staying informed helps reduce risks while maintaining transparency and control over your activities, ensuring smooth and efficient participation in the protocol.
+
+Learn more about this notifications in [our documentation](https://docs.dappnode.io/docs/user/staking/ethereum/lsd-pools/lido/notifications).
diff --git a/faq/notifications-2.md b/faq/notifications-2.md
new file mode 100644
index 00000000..d307dcbb
--- /dev/null
+++ b/faq/notifications-2.md
@@ -0,0 +1,7 @@
+---
+title: How to Get Your Telegram User ID
+---
+
+1. Open [Telegram](https://web.telegram.org/a/) and search for [`@userinfobot`](https://web.telegram.org/a/#52504489) or [`@raw_data_bot`](https://web.telegram.org/a/#1533228735).
+2. Start a chat with the bot by clicking Start.
+3. The bot will reply with your Telegram ID.
diff --git a/faq/notifications-3.md b/faq/notifications-3.md
new file mode 100644
index 00000000..db5fa1da
--- /dev/null
+++ b/faq/notifications-3.md
@@ -0,0 +1,10 @@
+---
+title: How to Create a Telegram Bot and Get the Bot Token
+---
+
+1. Open Telegram and search for [`@BotFather`](https://web.telegram.org/a/#93372553).
+2. Start a chat with BotFather and type `/newbot`.
+3. Follow the instructions to name your bot and choose a username (must end with "bot").
+4. Once created, BotFather will send you the bot token.
+ - Example: `123456789:ABCDefghIJKLMNOPQRSTuvwxYZ`.
+5. Open the chat with your bot and clib the "`Start`" button.
diff --git a/faq/testnet-notifications-1.md b/faq/testnet-notifications-1.md
new file mode 100644
index 00000000..0b8df581
--- /dev/null
+++ b/faq/testnet-notifications-1.md
@@ -0,0 +1,9 @@
+---
+title: Why are notifications crucial?
+---
+
+Notifications are essential for staying informed about critical events within the Lido CSM protocol. By receiving alerts about exit requests, deposits, penalties, slashing incidents, and smart contract events, you can proactively manage your staking operations and address issues promptly.
+
+Staying informed helps reduce risks while maintaining transparency and control over your activities, ensuring smooth and efficient participation in the protocol.
+
+Learn more about this notifications in [our documentation](https://docs.dappnode.io/docs/user/staking/ethereum/lsd-pools/lido/notifications).
diff --git a/faq/testnet-notifications-2.md b/faq/testnet-notifications-2.md
new file mode 100644
index 00000000..d307dcbb
--- /dev/null
+++ b/faq/testnet-notifications-2.md
@@ -0,0 +1,7 @@
+---
+title: How to Get Your Telegram User ID
+---
+
+1. Open [Telegram](https://web.telegram.org/a/) and search for [`@userinfobot`](https://web.telegram.org/a/#52504489) or [`@raw_data_bot`](https://web.telegram.org/a/#1533228735).
+2. Start a chat with the bot by clicking Start.
+3. The bot will reply with your Telegram ID.
diff --git a/faq/testnet-notifications-3.md b/faq/testnet-notifications-3.md
new file mode 100644
index 00000000..db5fa1da
--- /dev/null
+++ b/faq/testnet-notifications-3.md
@@ -0,0 +1,10 @@
+---
+title: How to Create a Telegram Bot and Get the Bot Token
+---
+
+1. Open Telegram and search for [`@BotFather`](https://web.telegram.org/a/#93372553).
+2. Start a chat with BotFather and type `/newbot`.
+3. Follow the instructions to name your bot and choose a username (must end with "bot").
+4. Once created, BotFather will send you the bot token.
+ - Example: `123456789:ABCDefghIJKLMNOPQRSTuvwxYZ`.
+5. Open the chat with your bot and clib the "`Start`" button.
From b67c4d774ab86a839f0d67bde89ced41c2202649 Mon Sep 17 00:00:00 2001
From: mateumiralles
Date: Mon, 10 Feb 2025 14:43:41 +0100
Subject: [PATCH 19/19] fix: dappnode `/dashboard` features + initial calls
fixed
---
dappnode/hooks/use-brain-keystore-api.ts | 2 +-
dappnode/hooks/use-ec-sanity-check.ts | 2 +-
dappnode/hooks/use-get-exit-requests.ts | 6 ++---
dappnode/hooks/use-get-infra-status.ts | 14 +++++------
dappnode/hooks/use-get-telegram-data.ts | 6 ++---
dappnode/starter-pack/steps.tsx | 4 ++--
dappnode/status/InfraItem.tsx | 4 ++--
dappnode/status/status-section.tsx | 12 +++++-----
dappnode/status/types.ts | 15 +++++++-----
dappnode/status/warnings.tsx | 10 ++++----
features/dashboard/dashboard.tsx | 6 +++++
shared/components/status-chip/status-chip.tsx | 23 ++++++++++++++++---
12 files changed, 65 insertions(+), 39 deletions(-)
diff --git a/dappnode/hooks/use-brain-keystore-api.ts b/dappnode/hooks/use-brain-keystore-api.ts
index 02baf7f1..bf12443d 100644
--- a/dappnode/hooks/use-brain-keystore-api.ts
+++ b/dappnode/hooks/use-brain-keystore-api.ts
@@ -37,7 +37,7 @@ const useApiBrain = (interval = 60000) => {
}, interval);
return () => clearInterval(intervalId);
- });
+ }, [fetchPubkeys, interval]);
return { pubkeys, isLoading, error };
};
diff --git a/dappnode/hooks/use-ec-sanity-check.ts b/dappnode/hooks/use-ec-sanity-check.ts
index f57c99ac..5be07524 100644
--- a/dappnode/hooks/use-ec-sanity-check.ts
+++ b/dappnode/hooks/use-ec-sanity-check.ts
@@ -53,7 +53,7 @@ export const useECSanityCheck = () => {
};
void getSyncStatus();
- });
+ }, [ECApiUrl, publicRuntimeConfig.defaultChain]);
useEffect(() => {
const getTxStatus = async () => {
diff --git a/dappnode/hooks/use-get-exit-requests.ts b/dappnode/hooks/use-get-exit-requests.ts
index e22e34ce..2a96701f 100644
--- a/dappnode/hooks/use-get-exit-requests.ts
+++ b/dappnode/hooks/use-get-exit-requests.ts
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useCallback, useState } from 'react';
import useDappnodeUrls from './use-dappnode-urls';
import { useActiveNodeOperator } from 'providers/node-operator-provider';
import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry';
@@ -19,7 +19,7 @@ const useGetExitRequests = () => {
}
type ExitRequests = Record;
- const getExitRequests = async () => {
+ const getExitRequests = useCallback(async () => {
try {
setIsLoading(true);
console.debug(`GETting validators exit requests from indexer API`);
@@ -53,7 +53,7 @@ const useGetExitRequests = () => {
);
setIsLoading(false);
}
- };
+ }, [backendUrl, nodeOperator]);
return { exitRequests, getExitRequests, isLoading };
};
diff --git a/dappnode/hooks/use-get-infra-status.ts b/dappnode/hooks/use-get-infra-status.ts
index 990e8b92..caf5ce40 100644
--- a/dappnode/hooks/use-get-infra-status.ts
+++ b/dappnode/hooks/use-get-infra-status.ts
@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react';
import useDappnodeUrls from './use-dappnode-urls';
-import { InfraStatus } from 'dappnode/status/types';
+import { INFRA_STATUS } from 'dappnode/status/types';
export const useGetInfraStatus = () => {
const { ECApiUrl, CCStatusApiUrl, CCVersionApiUrl } = useDappnodeUrls();
- const [ECStatus, setECStatus] = useState();
- const [CCStatus, setCCStatus] = useState();
+ const [ECStatus, setECStatus] = useState();
+ const [CCStatus, setCCStatus] = useState();
const [ECName, setECName] = useState();
const [CCName, setCCName] = useState();
@@ -41,11 +41,11 @@ export const useGetInfraStatus = () => {
}
const data = await syncResponse.json();
- setCCStatus(data.data.is_syncing ? 'Syncing' : 'Synced');
+ setCCStatus(data.data.is_syncing ? 'SYNCING' : 'SYNCED');
setIsCCLoading(false);
} catch (e) {
console.error(`Error getting CC data: ${e}`);
- setCCStatus('Not installed');
+ setCCStatus('NOT_INSTALLED');
setIsCCLoading(false);
}
};
@@ -90,11 +90,11 @@ export const useGetInfraStatus = () => {
}
const syncData = await syncResponse.json();
- setECStatus(syncData.result ? 'Syncing' : 'Synced');
+ setECStatus(syncData.result ? 'SYNCING' : 'SYNCED');
setIsECLoading(false);
} catch (e) {
console.error(`Error getting EC data: ${e}`);
- setECStatus('Not installed');
+ setECStatus('NOT_INSTALLED');
setIsECLoading(false);
}
};
diff --git a/dappnode/hooks/use-get-telegram-data.ts b/dappnode/hooks/use-get-telegram-data.ts
index 857a92c0..f35c7db2 100644
--- a/dappnode/hooks/use-get-telegram-data.ts
+++ b/dappnode/hooks/use-get-telegram-data.ts
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useCallback, useState } from 'react';
import useDappnodeUrls from './use-dappnode-urls';
const useGetTelegramData = () => {
@@ -7,7 +7,7 @@ const useGetTelegramData = () => {
const [botToken, setBotToken] = useState();
const [isLoading, setIsLoading] = useState(false);
- const getTelegramData = async () => {
+ const getTelegramData = useCallback(async () => {
setIsLoading(true);
try {
console.debug(`GETting telegram data from events indexer API`);
@@ -35,7 +35,7 @@ const useGetTelegramData = () => {
);
setIsLoading(false);
}
- };
+ }, [backendUrl]);
return { telegramId, botToken, getTelegramData, isLoading };
};
diff --git a/dappnode/starter-pack/steps.tsx b/dappnode/starter-pack/steps.tsx
index f91ad5f2..798c93dc 100644
--- a/dappnode/starter-pack/steps.tsx
+++ b/dappnode/starter-pack/steps.tsx
@@ -109,8 +109,8 @@ const Step2: FC = ({ step, title, setStep }: StepsProps) => {
const { error: brainError, isLoading: brainLoading } = useApiBrain();
const { isMEVRunning, isLoading: relaysLoading } = useGetRelaysData();
- const isECSynced: boolean = ECStatus === 'Synced';
- const isCCSynced: boolean = CCStatus === 'Synced';
+ const isECSynced: boolean = ECStatus === 'SYNCED';
+ const isCCSynced: boolean = CCStatus === 'SYNCED';
const isSignerInstalled: boolean = brainError ? false : true;
diff --git a/dappnode/status/InfraItem.tsx b/dappnode/status/InfraItem.tsx
index 2e734ef5..ec24e34d 100644
--- a/dappnode/status/InfraItem.tsx
+++ b/dappnode/status/InfraItem.tsx
@@ -2,14 +2,14 @@ import { FC } from 'react';
import { TitleStyled, ItemStyled, SubtitleStyled } from './styles';
import { Loader, Tooltip } from '@lidofinance/lido-ui';
import { StatusChip } from 'shared/components';
-import { InfraStatus } from './types';
import { LoaderWrapperStyle } from 'shared/navigate/splash/loader-banner/styles';
+import { INFRA_STATUS } from './types';
export type InfraItemProps = {
title: string;
subtitle: string;
tooltip?: string;
- status: InfraStatus;
+ status: INFRA_STATUS;
isLoading: boolean;
};
diff --git a/dappnode/status/status-section.tsx b/dappnode/status/status-section.tsx
index 6f400bc0..f29d94fe 100644
--- a/dappnode/status/status-section.tsx
+++ b/dappnode/status/status-section.tsx
@@ -27,25 +27,25 @@ export const StatusSection: FC = () => {
{
title: ECName ? capitalizeFirstChar(ECName) : '-',
subtitle: 'Execution Client',
- status: ECStatus || 'Not installed',
+ status: ECStatus || 'NOT_INSTALLED',
isLoading: isECLoading,
},
{
title: CCName ? capitalizeFirstChar(CCName) : '-',
subtitle: 'Consensus Client',
- status: CCStatus || 'Not installed',
+ status: CCStatus || 'NOT_INSTALLED',
isLoading: isCCLoading,
},
{
title: 'Web3signer',
subtitle: 'Signer',
- status: brainKeys ? 'Installed' : 'Not installed',
+ status: brainKeys ? 'INSTALLED' : 'NOT_INSTALLED',
isLoading: brainLoading,
},
{
title: 'MEV Boost',
subtitle: 'Relays',
- status: isMEVRunning ? 'Installed' : 'Not installed',
+ status: isMEVRunning ? 'INSTALLED' : 'NOT_INSTALLED',
isLoading: relaysLoading,
},
];
@@ -67,8 +67,8 @@ export const StatusSection: FC = () => {
{!!brainError ||
- ECStatus === 'Not installed' ||
- CCStatus === 'Not installed' ||
+ ECStatus === 'NOT_INSTALLED' ||
+ CCStatus === 'NOT_INSTALLED' ||
!isMEVRunning ? (
Set up your node
) : null}
diff --git a/dappnode/status/types.ts b/dappnode/status/types.ts
index 5053ed08..de5f2467 100644
--- a/dappnode/status/types.ts
+++ b/dappnode/status/types.ts
@@ -1,9 +1,12 @@
-export type InfraStatus =
- | 'Synced'
- | 'Installed'
- | 'Syncing'
- | 'Not allowed'
- | 'Not installed';
+export const INFRA_STATUS = {
+ SYNCED: 'SYNCED',
+ SYNCING: 'SYNCING',
+ NOT_ALLOWED: 'NOT_ALLOWED',
+ NOT_INSTALLED: 'NOT_INSTALLED',
+ INSTALLED: 'INSTALLED',
+} as const;
+
+export type INFRA_STATUS = keyof typeof INFRA_STATUS;
export type AllowedRelay = {
Description: string;
diff --git a/dappnode/status/warnings.tsx b/dappnode/status/warnings.tsx
index 4b16a067..0530521e 100644
--- a/dappnode/status/warnings.tsx
+++ b/dappnode/status/warnings.tsx
@@ -42,7 +42,7 @@ export const Warnings: FC = () => {
const [numWarnings, setNumWarnings] = useState(0);
useEffect(() => {
void getExitRequests();
- });
+ }, [getExitRequests]);
useEffect(() => {
if (exitRequests) {
@@ -62,8 +62,8 @@ export const Warnings: FC = () => {
setNumWarnings(
validatorsExitRequests.length +
(errorBrain ? 1 : missingKeys.length) +
- (ECStatus === 'Not installed' ? 1 : 0) +
- (CCStatus === 'Not installed' ? 1 : 0) +
+ (ECStatus === 'NOT_INSTALLED' ? 1 : 0) +
+ (CCStatus === 'NOT_INSTALLED' ? 1 : 0) +
(isMEVRunning ? 0 : 1) +
(isMEVRunning && mandatoryRelays && !hasMandatoryRelay ? 1 : 0) +
(isMEVRunning && usedBlacklistedRelays.length > 0 ? 1 : 0),
@@ -121,7 +121,7 @@ export const Warnings: FC = () => {
Your Execution Client is not installed!
@@ -132,7 +132,7 @@ export const Warnings: FC = () => {
Your Consensus Client is not installed!
diff --git a/features/dashboard/dashboard.tsx b/features/dashboard/dashboard.tsx
index 411a5d94..dba85145 100644
--- a/features/dashboard/dashboard.tsx
+++ b/features/dashboard/dashboard.tsx
@@ -3,10 +3,16 @@ import { BondSection } from './bond';
import { KeysSection } from './keys';
import { RolesSection } from './roles';
import { ExternalSection } from './external';
+import { StatusSection } from 'dappnode/status/status-section';
+import { NotificationsModal } from 'dappnode/notifications/notifications-modal';
export const Dashboard: FC = () => {
return (
<>
+ {/* DAPPNODE */}
+
+
+
diff --git a/shared/components/status-chip/status-chip.tsx b/shared/components/status-chip/status-chip.tsx
index 92cf3169..ee77aa0c 100644
--- a/shared/components/status-chip/status-chip.tsx
+++ b/shared/components/status-chip/status-chip.tsx
@@ -2,11 +2,14 @@ import { FC } from 'react';
import { StatusStyle, Variants } from './style';
import { KEY_STATUS } from 'consts/key-status';
+// DAPPNODE
+import { INFRA_STATUS } from 'dappnode/status/types';
+
type Props = {
- status?: KEY_STATUS;
+ status?: KEY_STATUS | INFRA_STATUS;
};
-const variants: Record = {
+const variants: Record = {
[KEY_STATUS.NON_QUEUED]: 'warning',
[KEY_STATUS.DEPOSITABLE]: 'default',
[KEY_STATUS.ACTIVATION_PENDING]: 'default',
@@ -23,9 +26,16 @@ const variants: Record = {
[KEY_STATUS.EXIT_REQUESTED]: 'warning',
[KEY_STATUS.STUCK]: 'error',
[KEY_STATUS.SLASHED]: 'secondary',
+
+ //DAPPNODE
+ [INFRA_STATUS.SYNCED]: 'success',
+ [INFRA_STATUS.SYNCING]: 'warning',
+ [INFRA_STATUS.NOT_ALLOWED]: 'error',
+ [INFRA_STATUS.NOT_INSTALLED]: 'error',
+ [INFRA_STATUS.INSTALLED]: 'success',
};
-export const StatusTitle: Record = {
+export const StatusTitle: Record = {
[KEY_STATUS.NON_QUEUED]: 'Non queued',
[KEY_STATUS.DEPOSITABLE]: 'Depositable',
[KEY_STATUS.ACTIVATION_PENDING]: 'Activation pending',
@@ -42,6 +52,13 @@ export const StatusTitle: Record = {
[KEY_STATUS.EXIT_REQUESTED]: 'Exit requested',
[KEY_STATUS.STUCK]: 'Stuck',
[KEY_STATUS.SLASHED]: 'Slashed',
+
+ //DAPPNODE
+ [INFRA_STATUS.SYNCED]: 'Synced',
+ [INFRA_STATUS.SYNCING]: 'Syncing',
+ [INFRA_STATUS.NOT_ALLOWED]: 'Not allowed',
+ [INFRA_STATUS.NOT_INSTALLED]: 'Not installed',
+ [INFRA_STATUS.INSTALLED]: 'Installed',
};
export const StatusChip: FC = ({ status }) => (