From 9e55b052cdd19249f59cba4a6ac816a9b168b9a7 Mon Sep 17 00:00:00 2001 From: Rami Date: Sun, 23 Mar 2025 22:54:53 -0300 Subject: [PATCH 01/42] Add separate wfs for hoodi and holesky --- .github/workflows/ci-dev-holesky.yml | 29 ++++++++++++++++++++++++++++ .github/workflows/ci-dev.yml | 4 ++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci-dev-holesky.yml diff --git a/.github/workflows/ci-dev-holesky.yml b/.github/workflows/ci-dev-holesky.yml new file mode 100644 index 00000000..ad534f45 --- /dev/null +++ b/.github/workflows/ci-dev-holesky.yml @@ -0,0 +1,29 @@ +name: CI Dev Holesky + +on: + workflow_dispatch: + push: + branches: + - holesky + 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_holesky_testnet_csm_widget.yaml" + TARGET: "holesky" diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml index dfe72ff2..25e81cf3 100644 --- a/.github/workflows/ci-dev.yml +++ b/.github/workflows/ci-dev.yml @@ -1,4 +1,4 @@ -name: CI Dev +name: CI Dev Hoodi on: workflow_dispatch: @@ -25,5 +25,5 @@ jobs: 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_WORKFLOW: "deploy_hoodi_testnet_csm_widget.yaml" TARGET: "develop" From 9cf89e8cc9b8113b9ec457eba5663a52b8962aba Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 18 Mar 2025 07:37:15 +0300 Subject: [PATCH 02/42] fix: missed imports --- utilsApi/api/apiProxyFactory.ts | 14 +++++++------- utilsApi/api/errors.ts | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 utilsApi/api/errors.ts diff --git a/utilsApi/api/apiProxyFactory.ts b/utilsApi/api/apiProxyFactory.ts index fd0833c5..163ce394 100644 --- a/utilsApi/api/apiProxyFactory.ts +++ b/utilsApi/api/apiProxyFactory.ts @@ -1,17 +1,17 @@ import type { ServerLogger } from '@lidofinance/api-logger'; -import { - DEFAULT_API_ERROR_MESSAGE, - HEALTHY_RPC_SERVICES_ARE_OVER, - RpcProviders, - UnsupportedChainIdError, - UnsupportedHTTPMethodError, -} from '@lidofinance/next-pages'; +import { RpcProviders } from '@lidofinance/next-pages'; import { iterateUrls } from '@lidofinance/rpc'; import type { NextApiRequest, NextApiResponse } from 'next'; import { Readable } from 'node:stream'; import { ReadableStream } from 'node:stream/web'; import { Counter, Registry } from 'prom-client'; import type { TrackedFetchApi } from './trackedFetchApiFactory'; +import { + HEALTHY_RPC_SERVICES_ARE_OVER, + UnsupportedChainIdError, + UnsupportedHTTPMethodError, +} from './errors'; +import { DEFAULT_API_ERROR_MESSAGE } from '@lidofinance/next-api-wrapper'; export type ApiFactoryParams = { metrics: { diff --git a/utilsApi/api/errors.ts b/utilsApi/api/errors.ts new file mode 100644 index 00000000..215a1bfc --- /dev/null +++ b/utilsApi/api/errors.ts @@ -0,0 +1,15 @@ +export const HEALTHY_RPC_SERVICES_ARE_OVER = 'Healthy RPC services are over!'; + +export class ClientError extends Error {} + +export class UnsupportedChainIdError extends ClientError { + constructor(message?: string) { + super(message || 'Unsupported chainId'); + } +} + +export class UnsupportedHTTPMethodError extends ClientError { + constructor(message?: string) { + super(message || 'Unsupported HTTP method'); + } +} From 6b0852309a19a764e6ce99634ddd04e6c32524d2 Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 18 Mar 2025 07:11:10 +0300 Subject: [PATCH 03/42] chore: drop unused subgraph code chore: drop consts --- config/get-secret-config.ts | 5 ----- config/groups/cache.ts | 4 ---- consts/aggregator.ts | 19 ------------------- consts/api.ts | 5 ----- consts/metrics.ts | 1 - consts/tx.ts | 10 ---------- global.d.ts | 11 ----------- next.config.mjs | 11 ----------- types/index.ts | 1 - types/subgraph.ts | 3 --- utilsApi/getSubgraphUrl.ts | 12 ------------ utilsApi/index.ts | 1 - utilsApi/metrics/metrics.ts | 2 -- utilsApi/metrics/subgraph.ts | 22 ---------------------- 14 files changed, 107 deletions(-) delete mode 100644 consts/aggregator.ts delete mode 100644 consts/tx.ts delete mode 100644 types/subgraph.ts delete mode 100644 utilsApi/getSubgraphUrl.ts delete mode 100644 utilsApi/metrics/subgraph.ts diff --git a/config/get-secret-config.ts b/config/get-secret-config.ts index ca193c78..84d5e13d 100644 --- a/config/get-secret-config.ts +++ b/config/get-secret-config.ts @@ -16,8 +16,6 @@ export type SecretConfigType = Modify< cspReportOnly: boolean; - subgraphRequestTimeout: number; - rateLimit: number; rateLimitTimeFrame: number; } @@ -54,9 +52,6 @@ export const getSecretConfig = (): SecretConfigType => { cspReportOnly: toBoolean(serverRuntimeConfig.cspReportOnly), - subgraphRequestTimeout: - Number(serverRuntimeConfig.subgraphRequestTimeout) || 5000, - rateLimit: Number(serverRuntimeConfig.rateLimit) || 100, rateLimitTimeFrame: Number(serverRuntimeConfig.rateLimitTimeFrame) || 60, // 1 minute; }; diff --git a/config/groups/cache.ts b/config/groups/cache.ts index 9d0a050e..b71130fc 100644 --- a/config/groups/cache.ts +++ b/config/groups/cache.ts @@ -15,10 +15,6 @@ export const CACHE_LIDO_STATS_TTL = ms('1h'); export const CACHE_LIDO_SHORT_STATS_KEY = 'cache-short-lido-stats'; export const CACHE_LIDO_SHORT_STATS_TTL = ms('1h'); -export const CACHE_LIDO_HOLDERS_VIA_SUBGRAPHS_KEY = - 'cache-lido-holders-via-subgraphs'; -export const CACHE_LIDO_HOLDERS_VIA_SUBGRAPHS_TTL = ms('7d'); - export const CACHE_LDO_STATS_KEY = 'cache-ldo-stats'; export const CACHE_LDO_STATS_TTL = ms('1h'); diff --git a/consts/aggregator.ts b/consts/aggregator.ts deleted file mode 100644 index 5da55d34..00000000 --- a/consts/aggregator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CHAINS } from '@lido-sdk/constants'; -import invariant from 'tiny-invariant'; - -// https://etherscan.io/address/0xcfe54b5cd566ab89272946f602d76ea879cab4a8 -export const AGGREGATOR_STETH_USD_PRICE_FEED_BY_NETWORK: { - [key in CHAINS]?: string; -} = { - [CHAINS.Mainnet]: '0xcfe54b5cd566ab89272946f602d76ea879cab4a8', -}; - -// Chainlink: STETH/USD Price Feed -// https://data.chain.link/ethereum/mainnet/crypto-usd/steth-usd -export const getAggregatorStEthUsdPriceFeedAddress = ( - chainId: CHAINS, -): string => { - const address = AGGREGATOR_STETH_USD_PRICE_FEED_BY_NETWORK[chainId]; - invariant(address, 'chain is not supported'); - return address; -}; diff --git a/consts/api.ts b/consts/api.ts index 45256248..0c8e3eae 100644 --- a/consts/api.ts +++ b/consts/api.ts @@ -1,8 +1,3 @@ -export const ETHPLORER_TOKEN_ENDPOINT = - 'https://api.ethplorer.io/getTokenInfo/'; - -export const HEALTHY_RPC_SERVICES_ARE_OVER = 'Healthy RPC services are over!'; - // TODO: review export const enum API_ROUTES { diff --git a/consts/metrics.ts b/consts/metrics.ts index 353a810d..2b9dfa29 100644 --- a/consts/metrics.ts +++ b/consts/metrics.ts @@ -5,6 +5,5 @@ export const METRICS_PREFIX = 'csm_widget_ui_'; export const enum METRIC_NAMES { REQUESTS_TOTAL = 'requests_total', API_RESPONSE = 'api_response', - SUBGRAPHS_RESPONSE = 'subgraphs_response', ETH_CALL_ADDRESS_TO = 'eth_call_address_to', } diff --git a/consts/tx.ts b/consts/tx.ts deleted file mode 100644 index 246f69f8..00000000 --- a/consts/tx.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BigNumber } from 'ethers'; - -// TODO: review - -export const WSTETH_APPROVE_GAS_LIMIT = BigNumber.from(78000); - -export const WRAP_FROM_ETH_GAS_LIMIT = BigNumber.from(100000); -export const WRAP_GAS_LIMIT = BigNumber.from(140000); -export const WRAP_GAS_LIMIT_GOERLI = BigNumber.from(120000); -export const UNWRAP_GAS_LIMIT = BigNumber.from(115000); diff --git a/global.d.ts b/global.d.ts index 9f0a2244..3c5f7125 100644 --- a/global.d.ts +++ b/global.d.ts @@ -28,26 +28,15 @@ declare module 'next/config' { defaultChain: string; rpcUrls_1: string | undefined; rpcUrls_17000: string | undefined; - ethplorerApiKey: string | undefined; clApiUrls_1: string | undefined; clApiUrls_17000: string | undefined; - oneInchApiKey: string | undefined; - cspTrustedHosts: string | undefined; cspReportUri: string | undefined; cspReportOnly: string | undefined; - subgraphMainnet: string | undefined; - subgraphGoerli: string | undefined; - subgraphHolesky: string | undefined; - subgraphRequestTimeout: string | undefined; - rateLimit: string; rateLimitTimeFrame: string; - - ethAPIBasePath: string; - rewardsBackendAPI: string | undefined; }; publicRuntimeConfig: { basePath: string | undefined; diff --git a/next.config.mjs b/next.config.mjs index 1a5f7f32..d153d321 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -163,26 +163,15 @@ export default withBundleAnalyzer({ defaultChain: process.env.DEFAULT_CHAIN, rpcUrls_1: process.env.EL_RPC_URLS_1, rpcUrls_17000: process.env.EL_RPC_URLS_17000, - ethplorerApiKey: process.env.ETHPLORER_API_KEY, clApiUrls_1: process.env.CL_API_URLS_1, clApiUrls_17000: process.env.CL_API_URLS_17000, - oneInchApiKey: process.env.ONE_INCH_API_KEY, - cspTrustedHosts: process.env.CSP_TRUSTED_HOSTS, cspReportUri: process.env.CSP_REPORT_URI, cspReportOnly: process.env.CSP_REPORT_ONLY, - subgraphMainnet: process.env.SUBGRAPH_MAINNET, - subgraphGoerli: process.env.SUBGRAPH_GOERLI, - subgraphHolesky: process.env.SUBGRAPH_HOLESKY, - subgraphRequestTimeout: process.env.SUBGRAPH_REQUEST_TIMEOUT, - rateLimit: process.env.RATE_LIMIT, rateLimitTimeFrame: process.env.RATE_LIMIT_TIME_FRAME, - - ethAPIBasePath: process.env.ETH_API_BASE_PATH, - rewardsBackendAPI: process.env.REWARDS_BACKEND, }, // ATTENTION: If you add a new variable you should declare it in `global.d.ts` diff --git a/types/index.ts b/types/index.ts index 2bc24b9c..6a4b3a99 100644 --- a/types/index.ts +++ b/types/index.ts @@ -3,4 +3,3 @@ export * from './common'; export * from './components'; export * from './deposit-data'; export * from './node-operator'; -export * from './subgraph'; diff --git a/types/subgraph.ts b/types/subgraph.ts deleted file mode 100644 index 2aed7a1c..00000000 --- a/types/subgraph.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { CHAINS } from 'consts/chains'; - -export type SubgraphChains = CHAINS; diff --git a/utilsApi/getSubgraphUrl.ts b/utilsApi/getSubgraphUrl.ts deleted file mode 100644 index 56614125..00000000 --- a/utilsApi/getSubgraphUrl.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { secretConfig } from 'config'; -import { CHAINS } from 'consts/chains'; -import { SubgraphChains } from 'types'; - -export const SUBGRAPH_URL = { - [CHAINS.Mainnet]: secretConfig.subgraphMainnet, - [CHAINS.Holesky]: secretConfig.subgraphHolesky, -} as const; - -export const getSubgraphUrl = (chainId: SubgraphChains): string | undefined => { - return SUBGRAPH_URL[chainId]; -}; diff --git a/utilsApi/index.ts b/utilsApi/index.ts index 29a3d3a2..4f48443d 100644 --- a/utilsApi/index.ts +++ b/utilsApi/index.ts @@ -1,6 +1,5 @@ export * from './contractAddressesMetricsMap'; export * from './getEthPrice'; -export * from './getSubgraphUrl'; export * from './nextApiWrappers'; export * from './rpcProviders'; export * from './metrics'; diff --git a/utilsApi/metrics/metrics.ts b/utilsApi/metrics/metrics.ts index 18d00cb5..3c5b2337 100644 --- a/utilsApi/metrics/metrics.ts +++ b/utilsApi/metrics/metrics.ts @@ -6,13 +6,11 @@ import { METRICS_PREFIX } from 'consts/metrics'; import buildInfoJson from 'build-info.json'; import { RequestMetrics } from './request'; -import { SubgraphMetrics } from './subgraph'; class Metrics { registry = new Registry(); // compositions of metric types - subgraph = new SubgraphMetrics(this.registry); request = new RequestMetrics(this.registry); constructor() { diff --git a/utilsApi/metrics/subgraph.ts b/utilsApi/metrics/subgraph.ts deleted file mode 100644 index fef942f4..00000000 --- a/utilsApi/metrics/subgraph.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Histogram, Registry } from 'prom-client'; -import { METRICS_PREFIX, METRIC_NAMES } from 'consts/metrics'; - -export class SubgraphMetrics { - subgraphsResponseTime: Histogram<'subgraphs'>; - - constructor(public registry: Registry) { - this.subgraphsResponseTime = this.subgraphsResponseTimeInit(); - } - - subgraphsResponseTimeInit() { - const subgraphsResponseTimeName = - METRICS_PREFIX + METRIC_NAMES.SUBGRAPHS_RESPONSE; - - return new Histogram({ - name: subgraphsResponseTimeName, - help: 'Subgraphs response time seconds', - buckets: [0.1, 0.2, 0.3, 0.6, 1, 1.5, 2, 5], - registers: [this.registry], - }); - } -} From 80b572781285687acd9806b854289a8e2386e3c2 Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 18 Mar 2025 07:00:18 +0300 Subject: [PATCH 04/42] feat: hoodi testnet --- .env.example | 6 ++-- config/get-secret-config.ts | 10 +++++- config/rpc/cl.ts | 1 + config/user-config/types.ts | 1 + config/user-config/utils.ts | 1 + consts/chains.ts | 1 + consts/csm-constants.ts | 32 +++++++++++++++---- consts/external-links.ts | 26 ++++++++++++--- consts/hoodi.ts | 22 +++++++++++++ env-dynamics.mjs | 8 +++-- .../required-bond-amount.tsx | 2 +- features/welcome/try-csm/try-csm.tsx | 4 +-- global.d.ts | 4 +++ next.config.mjs | 4 +++ package.json | 4 +-- pages/api/rpc.ts | 1 + providers/web3.tsx | 3 +- shared/hooks/use-external-links.ts | 4 +-- shared/hooks/useCsmContracts.ts | 1 + .../keys/validate/check-network-duplicates.ts | 13 +++----- shared/keys/validate/constants.ts | 8 ++--- shared/keys/validate/validate.ts | 2 +- utilsApi/clApiUrls.ts | 1 + utilsApi/contractAddressesMetricsMap.ts | 4 +-- yarn.lock | 19 +++++++---- 25 files changed, 134 insertions(+), 48 deletions(-) create mode 100644 consts/hoodi.ts diff --git a/.env.example b/.env.example index 7acc7fe6..d14960ca 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,16 @@ # RPC API urls by network # EL_RPC_URLS_{CHAIN_ID} list or URLs delimeted by commas, first entry is primary, else are fallbacks EL_RPC_URLS_1= -EL_RPC_URLS_17000= +EL_RPC_URLS_560048= # IPFS prefill RPC URLs - list of URLs delimited by commas PREFILL_UNSAFE_EL_RPC_URLS_1= -PREFILL_UNSAFE_EL_RPC_URLS_17000= +PREFILL_UNSAFE_EL_RPC_URLS_560048= # CL API urls by network # CL_API_URLS_{CHAIN_ID} list or URLs delimeted by commas, first entry is primary, else are fallbacks CL_API_URLS_1= -CL_API_URLS_17000= +CL_API_URLS_560048= # MAINTENANCE mode MAINTENANCE= diff --git a/config/get-secret-config.ts b/config/get-secret-config.ts index 84d5e13d..c402182b 100644 --- a/config/get-secret-config.ts +++ b/config/get-secret-config.ts @@ -10,9 +10,11 @@ export type SecretConfigType = Modify< rpcUrls_1: [string, ...string[]]; rpcUrls_17000: [string, ...string[]]; + rpcUrls_560048: [string, ...string[]]; clApiUrls_1: [string, ...string[]]; clApiUrls_17000: [string, ...string[]]; + clApiUrls_560048: [string, ...string[]]; cspReportOnly: boolean; @@ -31,7 +33,7 @@ export const getSecretConfig = (): SecretConfigType => { ...serverRuntimeConfig, // Keep fallback as in 'env-dynamics.mjs' - defaultChain: Number(serverRuntimeConfig.defaultChain) || 17000, + defaultChain: Number(serverRuntimeConfig.defaultChain) || 560048, // Hack: in the current implementation we can treat an empty array as a "tuple" (conditionally) rpcUrls_1: (serverRuntimeConfig.rpcUrls_1?.split(',') ?? []) as [ @@ -42,6 +44,10 @@ export const getSecretConfig = (): SecretConfigType => { string, ...string[], ], + rpcUrls_560048: (serverRuntimeConfig.rpcUrls_560048?.split(',') ?? []) as [ + string, + ...string[], + ], clApiUrls_1: (serverRuntimeConfig.clApiUrls_1?.split(',') ?? []) as [ string, @@ -49,6 +55,8 @@ export const getSecretConfig = (): SecretConfigType => { ], clApiUrls_17000: (serverRuntimeConfig.clApiUrls_17000?.split(',') ?? []) as [string, ...string[]], + clApiUrls_560048: (serverRuntimeConfig.clApiUrls_560048?.split(',') ?? + []) as [string, ...string[]], cspReportOnly: toBoolean(serverRuntimeConfig.cspReportOnly), diff --git a/config/rpc/cl.ts b/config/rpc/cl.ts index 14f0e210..140c4fe9 100644 --- a/config/rpc/cl.ts +++ b/config/rpc/cl.ts @@ -15,6 +15,7 @@ import { config } from '../get-config'; import { useUserConfig } from '../user-config'; export const getBackendApiPath = (chainId: string | number): string => { + if (chainId === CHAINS.Hoodi) return ''; const BASE_URL = typeof window === 'undefined' ? '' : window.location.origin; return `${BASE_URL}/${API_ROUTES.CL}/${chainId}`; }; diff --git a/config/user-config/types.ts b/config/user-config/types.ts index f4c09a4f..1339a6da 100644 --- a/config/user-config/types.ts +++ b/config/user-config/types.ts @@ -6,6 +6,7 @@ export type UserConfigDefaultType = { prefillUnsafeElRpcUrls: { [CHAINS.Mainnet]: string[]; [CHAINS.Holesky]: string[]; + [CHAINS.Hoodi]: string[]; }; walletconnectProjectId: string | undefined; }; diff --git a/config/user-config/utils.ts b/config/user-config/utils.ts index bb8eaeb4..a229f11f 100644 --- a/config/user-config/utils.ts +++ b/config/user-config/utils.ts @@ -15,6 +15,7 @@ export const getUserConfigDefault = (): UserConfigDefaultType => { prefillUnsafeElRpcUrls: { [CHAINS.Mainnet]: config.prefillUnsafeElRpcUrls1, [CHAINS.Holesky]: config.prefillUnsafeElRpcUrls17000, + [CHAINS.Hoodi]: config.prefillUnsafeElRpcUrls560048, }, walletconnectProjectId: config.walletconnectProjectId, }; diff --git a/consts/chains.ts b/consts/chains.ts index 1c5e1012..32aa953e 100644 --- a/consts/chains.ts +++ b/consts/chains.ts @@ -1,4 +1,5 @@ export const enum CHAINS { Mainnet = 1, Holesky = 17000, + Hoodi = 560048, } diff --git a/consts/csm-constants.ts b/consts/csm-constants.ts index e5d9f8d1..172bd9a4 100644 --- a/consts/csm-constants.ts +++ b/consts/csm-constants.ts @@ -1,4 +1,5 @@ -import { CHAINS } from '@lido-sdk/constants'; +import { CHAINS } from 'consts/chains'; +import { CHAINS as ALL_CHAINS } from '@lido-sdk/constants'; import { config } from 'config'; import { HexString } from 'shared/keys'; import { Address } from 'wagmi'; @@ -17,14 +18,14 @@ type CsmContract = type CsmConstants = { contracts: Record; - deploymentBlockNumber: HexString; + deploymentBlockNumber?: HexString; stakingModuleId: number; withdrawalCredentials: Address; retentionPeriodMins: number; slotsPerFrame: number; }; -export const CONSTANTS_BY_NETWORK: Partial> = { +export const CONSTANTS_BY_NETWORK: Record = { [CHAINS.Mainnet]: { contracts: { CSAccounting: '0x4d72BFF1BeaC69925F8Bd12526a39BAAb069e5Da', @@ -42,6 +43,23 @@ export const CONSTANTS_BY_NETWORK: Partial> = { retentionPeriodMins: 80_640, // 8 weeks slotsPerFrame: 32 * 225 * 28, // 28 days }, + [CHAINS.Hoodi]: { + contracts: { + CSAccounting: '0x592FF3c0FEd95909d7770db1659d35B2E1798B21', + CSEarlyAdoption: '0xd9ad1926E1F7bb363E6FA987f720049eDD1F1FA4', + CSFeeDistributor: '0x7D7566db8795015Ff711AD7655e1ED057e8ea155', + CSFeeOracle: '0xCF9230278019830762aC49148Dc9a90981ba157A', + CSModule: '0x5AE927989597213023FfA68D4D3ce109B3959FE4', + CSVerifier: '0x6e51Cb9Ca4D6f918E3d18839ACBe80798068712d', + ExitBusOracle: '0x30308CD8844fb2DB3ec4D056F1d475a802DCA07c', + StakingRouter: '0xCc820558B39ee15C7C45B59390B503b83fb499A8', + }, + deploymentBlockNumber: undefined, + stakingModuleId: 4, + withdrawalCredentials: '0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2', + retentionPeriodMins: 80_640, // 8 weeks + slotsPerFrame: 32 * 225 * 7, // 7 days + }, [CHAINS.Holesky]: { contracts: { CSAccounting: '0xc093e53e8F4b55A223c18A2Da6fA00e60DD5EFE1', @@ -62,9 +80,9 @@ export const CONSTANTS_BY_NETWORK: Partial> = { }; export const getCsmConstants = ( - chainId: CHAINS | undefined = config.defaultChain, + chainId: ALL_CHAINS | undefined = config.defaultChain, ) => { - const constants = CONSTANTS_BY_NETWORK[chainId]; + const constants = CONSTANTS_BY_NETWORK[chainId as unknown as CHAINS]; if (!constants) { throw new Error(`CSM constants for chain [${chainId}] are not specified`); } @@ -72,10 +90,10 @@ export const getCsmConstants = ( }; export const getCsmContractAddress = ( - chainId: CHAINS | undefined, + chainId: ALL_CHAINS | undefined, contract: CsmContract, ): Address => getCsmConstants(chainId).contracts[contract]; export const getCsmContractAddressGetter = - (contract: CsmContract) => (chainId: CHAINS | undefined) => + (contract: CsmContract) => (chainId: ALL_CHAINS) => getCsmContractAddress(chainId, contract); diff --git a/consts/external-links.ts b/consts/external-links.ts index 700bd402..c2315340 100644 --- a/consts/external-links.ts +++ b/consts/external-links.ts @@ -1,4 +1,4 @@ -import { CHAINS } from '@lido-sdk/constants'; +import { CHAINS } from 'consts/chains'; import { config } from 'config'; export const CSM_MAINNET_LINK = 'https://csm.lido.fi/'; @@ -36,9 +36,7 @@ type ExternalLinksConstants = { surveyApi: string; }; -export const EXTERNAL_LINKS_BY_NETWORK: Partial< - Record -> = { +export const EXTERNAL_LINKS_BY_NETWORK: Record = { [CHAINS.Mainnet]: { earlyAdoptionTree: 'https://raw.githubusercontent.com/lidofinance/community-staking-module/v1.0/artifacts/mainnet/early-adoption/merkle-tree.json', @@ -80,6 +78,26 @@ export const EXTERNAL_LINKS_BY_NETWORK: Partial< ratedExplorer: 'https://explorer.rated.network', subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', keysApi: 'https://keys-api-holesky.testnet.fi', + surveyApi: '', + }, + // FIXME: links + [CHAINS.Hoodi]: { + earlyAdoptionTree: '', + rewardsTree: '', + earlyAdoptionSources: + 'https://github.com/lidofinance/community-staking-module/blob/v1.0/artifacts/holesky/early-adoption/addresses.json', + earlyAdoptionAbout: + 'https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22', + feedbackForm: 'https://forms.gle/ZBUqbykaZokJLf4M7', + stakeWidget: 'https://stake-holesky.testnet.fi', + + feesMonitoring: 'https://fees-monitoring-holesky.testnet.fi', + operatorsWidget: 'https://operators-holesky.testnet.fi', + beaconchain: 'https://holesky.beaconcha.in', + beaconchainDashboard: 'https://v2-beta-holesky.beaconcha.in/dashboard', + ratedExplorer: 'https://explorer.rated.network', + subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', + keysApi: 'http://hr6vb81d1ndsx-hoodi-keys-api.valset-01.testnet.fi', surveyApi: 'https://csm-surveys-api-testnet.up.railway.app', }, }; diff --git a/consts/hoodi.ts b/consts/hoodi.ts new file mode 100644 index 00000000..a6a4bbbf --- /dev/null +++ b/consts/hoodi.ts @@ -0,0 +1,22 @@ +import { Chain } from 'wagmi'; + +export const hoodi: Readonly = { + id: 560048, + network: 'hoodi', + name: 'Hoodi', + nativeCurrency: { + name: 'Hoodie Ether', + symbol: 'ETH', + decimals: 18, + }, + rpcUrls: { + default: { + http: ['https://rpc.hoodi.ethpandaops.io'], + }, + public: { + http: ['https://rpc.hoodi.ethpandaops.io'], + }, + }, + contracts: {}, + testnet: true, +}; diff --git a/env-dynamics.mjs b/env-dynamics.mjs index 0061b6e1..a40c198d 100644 --- a/env-dynamics.mjs +++ b/env-dynamics.mjs @@ -18,12 +18,12 @@ const toBoolean = (dataStr) => { /** @type string */ export const matomoHost = process.env.MATOMO_URL; /** @type number */ -export const defaultChain = parseInt(process.env.DEFAULT_CHAIN, 10) || 17000; +export const defaultChain = parseInt(process.env.DEFAULT_CHAIN, 10) || 560048; /** @type number[] */ export const supportedChains = process.env?.SUPPORTED_CHAINS?.split(',').map( (chainId) => parseInt(chainId, 10), -) ?? [17000]; +) ?? [560048]; /** @type string */ export const walletconnectProjectId = process.env.WALLETCONNECT_PROJECT_ID; @@ -38,6 +38,10 @@ export const prefillUnsafeElRpcUrls1 = export const prefillUnsafeElRpcUrls17000 = process.env.PREFILL_UNSAFE_EL_RPC_URLS_17000?.split(',') ?? []; +/** @type string[] */ +export const prefillUnsafeElRpcUrls560048 = + process.env.PREFILL_UNSAFE_EL_RPC_URLS_560048?.split(',') ?? []; + /** @type string */ export const widgetApiBasePathForIpfs = process.env.WIDGET_API_BASE_PATH_FOR_IPFS; diff --git a/features/starter-pack/stacter-pack-section/required-bond-amount.tsx b/features/starter-pack/stacter-pack-section/required-bond-amount.tsx index 85b897e7..068eaea5 100644 --- a/features/starter-pack/stacter-pack-section/required-bond-amount.tsx +++ b/features/starter-pack/stacter-pack-section/required-bond-amount.tsx @@ -26,7 +26,7 @@ export const RequiredBondAmount: FC = () => { ) : ( )} diff --git a/features/welcome/try-csm/try-csm.tsx b/features/welcome/try-csm/try-csm.tsx index a092b7b1..17ffce5c 100644 --- a/features/welcome/try-csm/try-csm.tsx +++ b/features/welcome/try-csm/try-csm.tsx @@ -17,7 +17,7 @@ export const TryCSM: FC = () => { - Try CSM on Holesky + Try CSM on Hoodi { - CSM uses Holesky as a testnet playground for those who want to try the + CSM uses Hoodi as a testnet playground for those who want to try the module in action in a test environment. diff --git a/global.d.ts b/global.d.ts index 3c5f7125..b23d0dd4 100644 --- a/global.d.ts +++ b/global.d.ts @@ -26,10 +26,14 @@ declare module 'next/config' { maintenance: boolean; defaultChain: string; + rpcUrls_1: string | undefined; rpcUrls_17000: string | undefined; + rpcUrls_560048: string | undefined; + clApiUrls_1: string | undefined; clApiUrls_17000: string | undefined; + clApiUrls_560048: string | undefined; cspTrustedHosts: string | undefined; cspReportUri: string | undefined; diff --git a/next.config.mjs b/next.config.mjs index d153d321..b6f98bcc 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -161,10 +161,14 @@ export default withBundleAnalyzer({ maintenance, defaultChain: process.env.DEFAULT_CHAIN, + rpcUrls_1: process.env.EL_RPC_URLS_1, rpcUrls_17000: process.env.EL_RPC_URLS_17000, + rpcUrls_560048: process.env.EL_RPC_URLS_560048, + clApiUrls_1: process.env.CL_API_URLS_1, clApiUrls_17000: process.env.CL_API_URLS_17000, + clApiUrls_560048: process.env.CL_API_URLS_560048, cspTrustedHosts: process.env.CSP_TRUSTED_HOSTS, cspReportUri: process.env.CSP_REPORT_URI, diff --git a/package.json b/package.json index 95877de4..964a0e63 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,9 @@ "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", "@ethersproject/units": "^5.7.0", - "@lido-sdk/constants": "^3.2.1", + "@lido-sdk/constants": "^3.5.0", "@lido-sdk/contracts": "^3.0.4", - "@lido-sdk/fetch": "^2.1.12", + "@lido-sdk/fetch": "^2.3.1", "@lido-sdk/helpers": "^1.5.1", "@lido-sdk/providers": "^1.4.14", "@lido-sdk/react": "^2.0.5", diff --git a/pages/api/rpc.ts b/pages/api/rpc.ts index a7a4f6ec..6ff99037 100644 --- a/pages/api/rpc.ts +++ b/pages/api/rpc.ts @@ -31,6 +31,7 @@ const rpc = rpcFactory({ providers: { [CHAINS.Mainnet]: secretConfig.rpcUrls_1, [CHAINS.Holesky]: secretConfig.rpcUrls_17000, + [CHAINS.Hoodi]: secretConfig.rpcUrls_560048, }, validation: { allowedRPCMethods: [ diff --git a/providers/web3.tsx b/providers/web3.tsx index 9732628a..95079ebf 100644 --- a/providers/web3.tsx +++ b/providers/web3.tsx @@ -4,6 +4,7 @@ import { WagmiConfig, createClient, configureChains, Chain } from 'wagmi'; import * as wagmiChains from 'wagmi/chains'; import { getStaticRpcBatchProvider } from '@lido-sdk/providers'; +import { hoodi } from 'consts/hoodi'; import { useUserConfig } from 'config/user-config'; import { useGetRpcUrlByChainId } from 'config/rpc'; import { CHAINS } from 'consts/chains'; @@ -11,7 +12,7 @@ import { ConnectWalletModal } from 'shared/wallet/connect-wallet-modal'; import { SDKLegacyProvider } from './sdk-legacy'; -const wagmiChainsArray = Object.values({ ...wagmiChains, holesky }); +const wagmiChainsArray = Object.values({ ...wagmiChains, hoodi, holesky }); const Web3Provider: FC = ({ children }) => { const { diff --git a/shared/hooks/use-external-links.ts b/shared/hooks/use-external-links.ts index ad8f572c..4d129f74 100644 --- a/shared/hooks/use-external-links.ts +++ b/shared/hooks/use-external-links.ts @@ -38,13 +38,13 @@ export const useOperatorPortalLink = () => { export const useRatedLink = () => { const nodeOperatorId = useNodeOperatorId(); - const network = defaultChain === CHAINS.Mainnet ? 'mainnet' : 'holesky'; + const network = defaultChain === CHAINS.Mainnet ? 'mainnet' : 'hoodi'; return `${links.ratedExplorer}/o/CSM%20Operator%20${nodeOperatorId}%20-%20Lido%20Community%20Staking%20Module?network=${network}`; }; export const useEthSeerLink = () => { const nodeOperatorId = useNodeOperatorId(); - const network = defaultChain === CHAINS.Mainnet ? 'mainnet' : 'holesky'; + const network = defaultChain === CHAINS.Mainnet ? 'mainnet' : 'hoodi'; if (links.ethseerDashboard) { return `${links.ethseerDashboard}/entity/csm_operator${nodeOperatorId}_lido?network=${network}`; } diff --git a/shared/hooks/useCsmContracts.ts b/shared/hooks/useCsmContracts.ts index b06d9ba0..829d1664 100644 --- a/shared/hooks/useCsmContracts.ts +++ b/shared/hooks/useCsmContracts.ts @@ -19,6 +19,7 @@ const CSModule = contractHooksFactory( export const useCSModuleRPC = CSModule.useContractRPC; export const useCSModuleWeb3 = CSModule.useContractWeb3; +// TODO: drop after removing Holesky const CSModuleOld = contractHooksFactory( CSModuleOld__factory, getCsmContractAddressGetter('CSModule'), diff --git a/shared/keys/validate/check-network-duplicates.ts b/shared/keys/validate/check-network-duplicates.ts index ccb9c59e..0981b4bc 100644 --- a/shared/keys/validate/check-network-duplicates.ts +++ b/shared/keys/validate/check-network-duplicates.ts @@ -18,12 +18,12 @@ type ResponseData = { meta: any; }; -const findDuplicate = async (pubkeys: HexString[], chainId: CHAINS) => { +const findDuplicate = async (pubkeys: HexString[]) => { try { // TODO: timeout // TODO: cache - const url = getExternalLinks(chainId).keysApi; - const response = await fetch(`${url}/v1/keys/find`, { + const { keysApi } = getExternalLinks(); + const response = await fetch(`${keysApi}/v1/keys/find`, { method: 'post', body: JSON.stringify({ pubkeys }), headers: { 'Content-Type': 'application/json' }, @@ -41,14 +41,11 @@ const toHexString = (data: string): HexString => { return `0x${data}`; }; -export const checkNetworkDuplicates = async ( - depositData: DepositData[], - chainId: CHAINS, -) => { +export const checkNetworkDuplicates = async (depositData: DepositData[]) => { const pubkeys = depositData.map((data) => toHexString(data.pubkey.toLowerCase()), ); - const duplicateKey = await findDuplicate(pubkeys, chainId); + const duplicateKey = await findDuplicate(pubkeys); if (duplicateKey) { throw new Error( diff --git a/shared/keys/validate/constants.ts b/shared/keys/validate/constants.ts index 2e3e0869..66f36030 100644 --- a/shared/keys/validate/constants.ts +++ b/shared/keys/validate/constants.ts @@ -12,19 +12,15 @@ export const FIXED_WC_PREFIX = '010000000000000000000000'; export const FIXED_NETWORK: { [key in CHAINS]?: string[]; } = { - [CHAINS.Goerli]: ['goerli', 'prater'], [CHAINS.Mainnet]: ['mainnet'], - [CHAINS.Ropsten]: ['mainnet'], - [CHAINS.Kiln]: ['kiln'], [CHAINS.Holesky]: ['holesky'], + [CHAINS.Hoodi]: ['hoodi'], }; export const FIXED_FORK_VERSION: { [key in CHAINS]?: string; } = { - [CHAINS.Goerli]: '00001020', [CHAINS.Mainnet]: '00000000', [CHAINS.Holesky]: '01017000', - [CHAINS.Ropsten]: '00000000', - [CHAINS.Kiln]: '70000069', + [CHAINS.Hoodi]: '10000910', }; diff --git a/shared/keys/validate/validate.ts b/shared/keys/validate/validate.ts index 39f71192..c9ebd372 100644 --- a/shared/keys/validate/validate.ts +++ b/shared/keys/validate/validate.ts @@ -18,7 +18,7 @@ export const validate = async ( checkLength(depositData, keysUploadLimit); checkDuplicates(depositData); checkPreviouslySubmittedDuplicates(depositData, chainId, blockNumber); - await checkNetworkDuplicates(depositData, chainId); + await checkNetworkDuplicates(depositData); return null; } catch (error) { diff --git a/utilsApi/clApiUrls.ts b/utilsApi/clApiUrls.ts index ffb12a52..ce31525b 100644 --- a/utilsApi/clApiUrls.ts +++ b/utilsApi/clApiUrls.ts @@ -4,4 +4,5 @@ import { CHAINS } from 'consts/chains'; export const clApiUrls: Record = { [CHAINS.Mainnet]: secretConfig.clApiUrls_1, [CHAINS.Holesky]: secretConfig.clApiUrls_17000, + [CHAINS.Hoodi]: secretConfig.clApiUrls_560048, }; diff --git a/utilsApi/contractAddressesMetricsMap.ts b/utilsApi/contractAddressesMetricsMap.ts index bde9295f..4f2420f3 100644 --- a/utilsApi/contractAddressesMetricsMap.ts +++ b/utilsApi/contractAddressesMetricsMap.ts @@ -136,7 +136,7 @@ const aggregatorMainnetAddress = METRIC_CONTRACT_ADDRESS_GETTERS[ ](CHAINS.Mainnet) as HexString; const prefilledAddresses = - config.defaultChain === CHAINS.Holesky && + config.defaultChain === CHAINS.Hoodi && !config.supportedChains.includes(CHAINS.Mainnet) ? ({ [CHAINS.Mainnet]: [aggregatorMainnetAddress], @@ -146,7 +146,7 @@ const prefilledAddresses = const prefilledMetricAddresses: Partial< Record> > = - config.defaultChain === CHAINS.Holesky && + config.defaultChain === CHAINS.Hoodi && !config.supportedChains.includes(CHAINS.Mainnet) ? { [CHAINS.Mainnet]: { diff --git a/yarn.lock b/yarn.lock index 5fc0ddbc..14b66e1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2545,13 +2545,20 @@ bignumber.js "^9.1.2" rxjs "^7.8.1" -"@lido-sdk/constants@3.3.0", "@lido-sdk/constants@^3.2.1": +"@lido-sdk/constants@3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lido-sdk/constants/-/constants-3.3.0.tgz#044b652a000a067b9ee0ae9f58814434779bb108" integrity sha512-R5XINgj/EQvyBfPF+Zv9B/ycFCqARD8rbWDp3J4luAMDbSUP3ncGw0x7Aj836k96eVnrh+jfd2tz/c99Dm6IaA== dependencies: tiny-invariant "^1.1.0" +"@lido-sdk/constants@3.5.0", "@lido-sdk/constants@^3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@lido-sdk/constants/-/constants-3.5.0.tgz#74d7e21a36cba3abce83cc63db0e50a03b92492e" + integrity sha512-Br5md5TP5g2uka3wTtpoUy92wYzTdA+y8U8p+EbZsnm/aMQyrvu0gA7b0pC4l/xODNEEIAGwThCGDb1sP9W39A== + dependencies: + tiny-invariant "^1.1.0" + "@lido-sdk/contracts@3.0.5", "@lido-sdk/contracts@^3.0.4": version "3.0.5" resolved "https://registry.yarnpkg.com/@lido-sdk/contracts/-/contracts-3.0.5.tgz#9778e83258de241e26bcd5c76589ff010856afbb" @@ -2560,12 +2567,12 @@ "@lido-sdk/constants" "3.3.0" tiny-invariant "^1.1.0" -"@lido-sdk/fetch@^2.1.12": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@lido-sdk/fetch/-/fetch-2.2.0.tgz#e0034083edaed471f29163ebfdccc9b10cd58ea7" - integrity sha512-Jwi2W5azP4uuhmsWexkk4al0Y+pMx5PpB74klIBzgX/eoVnK+NOTxLemJa6GDq4KTIlriAM79jscY0TJvJJWQA== +"@lido-sdk/fetch@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@lido-sdk/fetch/-/fetch-2.3.1.tgz#f9fb3359dc76a8922083b3ec91a7112939a4b266" + integrity sha512-ylfyqo1EyB5oykUMBNUx1mumVG2trx/ZrWNOIPBDJVSuDs0GBk9cFPil9dRj8QJh/gQyQdIPUsjv+HlwtdJqjQ== dependencies: - "@lido-sdk/constants" "3.3.0" + "@lido-sdk/constants" "3.5.0" node-fetch "^2.6.7" tiny-invariant "^1.1.0" From 6f32b8bd90ce73502c41f16423869f0a27686326 Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 18 Mar 2025 12:28:54 +0300 Subject: [PATCH 05/42] fix: csm addresses --- consts/csm-constants.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/consts/csm-constants.ts b/consts/csm-constants.ts index 172bd9a4..0fe6f66b 100644 --- a/consts/csm-constants.ts +++ b/consts/csm-constants.ts @@ -45,12 +45,12 @@ export const CONSTANTS_BY_NETWORK: Record = { }, [CHAINS.Hoodi]: { contracts: { - CSAccounting: '0x592FF3c0FEd95909d7770db1659d35B2E1798B21', - CSEarlyAdoption: '0xd9ad1926E1F7bb363E6FA987f720049eDD1F1FA4', - CSFeeDistributor: '0x7D7566db8795015Ff711AD7655e1ED057e8ea155', - CSFeeOracle: '0xCF9230278019830762aC49148Dc9a90981ba157A', - CSModule: '0x5AE927989597213023FfA68D4D3ce109B3959FE4', - CSVerifier: '0x6e51Cb9Ca4D6f918E3d18839ACBe80798068712d', + CSAccounting: '0xA54b90BA34C5f326BC1485054080994e38FB4C60', + CSEarlyAdoption: '0x3281b9E45518F462E594697f8fba1896a8B43939', + CSFeeDistributor: '0xaCd9820b0A2229a82dc1A0770307ce5522FF3582', + CSFeeOracle: '0xe7314f561B2e72f9543F1004e741bab6Fc51028B', + CSModule: '0x79CEf36D84743222f37765204Bec41E92a93E59d', + CSVerifier: '0x16D0f6068D211608e3703323314aa976a6492D09', ExitBusOracle: '0x30308CD8844fb2DB3ec4D056F1d475a802DCA07c', StakingRouter: '0xCc820558B39ee15C7C45B59390B503b83fb499A8', }, From 22e0021622c46edcc69bed37230c1cacc0dac9df Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 18 Mar 2025 13:08:00 +0300 Subject: [PATCH 06/42] chore: vendor @lido-sdk/providers & @lido-sdk/react chore: drop vendor chore: drop vendor --- .../use-add-bond-form-network-data.tsx | 8 +-- .../use-add-keys-form-network-data.tsx | 8 +-- .../use-submit-keys-form-network-data.tsx | 8 +-- package.json | 8 +-- shared/hooks/index.ts | 1 + shared/hooks/use-lido-contracts.ts | 23 +++++++ shared/hooks/use-permit-signature.ts | 12 ++-- shared/hooks/useNodeOperatorRewards.ts | 11 +-- shared/hooks/useStakingLimitInfo.ts | 4 +- shared/hooks/useStethByWsteth.ts | 3 +- shared/hooks/useWstethBySteth.ts | 3 +- yarn.lock | 68 ++++++++++++------- 12 files changed, 98 insertions(+), 59 deletions(-) create mode 100644 shared/hooks/use-lido-contracts.ts diff --git a/features/add-bond/add-bond-form/context/use-add-bond-form-network-data.tsx b/features/add-bond/add-bond-form/context/use-add-bond-form-network-data.tsx index 3326d00e..54aedd5e 100644 --- a/features/add-bond/add-bond-form/context/use-add-bond-form-network-data.tsx +++ b/features/add-bond/add-bond-form/context/use-add-bond-form-network-data.tsx @@ -1,8 +1,4 @@ -import { - useEthereumBalance, - useSTETHBalance, - useWSTETHBalance, -} from '@lido-sdk/react'; +import { useEthereumBalance } from '@lido-sdk/react'; import { STRATEGY_LAZY } from 'consts/swr-strategies'; import { useNodeOperatorId } from 'providers/node-operator-provider'; import { useCallback, useMemo } from 'react'; @@ -10,6 +6,8 @@ import { useCsmPaused, useNodeOperatorBalance, useStakingLimitInfo, + useSTETHBalance, + useWSTETHBalance, } from 'shared/hooks'; import { type AddBondFormNetworkData } from '../context/types'; diff --git a/features/add-keys/add-keys/context/use-add-keys-form-network-data.tsx b/features/add-keys/add-keys/context/use-add-keys-form-network-data.tsx index 1bb8aeb2..6b8e2b79 100644 --- a/features/add-keys/add-keys/context/use-add-keys-form-network-data.tsx +++ b/features/add-keys/add-keys/context/use-add-keys-form-network-data.tsx @@ -1,8 +1,4 @@ -import { - useEthereumBalance, - useSTETHBalance, - useWSTETHBalance, -} from '@lido-sdk/react'; +import { useEthereumBalance } from '@lido-sdk/react'; import { STRATEGY_LAZY } from 'consts/swr-strategies'; import { useNodeOperatorId } from 'providers/node-operator-provider'; import { useCallback, useMemo } from 'react'; @@ -15,6 +11,8 @@ import { useNodeOperatorCurveId, useNonWithdrawnKeysCount, useStakingLimitInfo, + useSTETHBalance, + useWSTETHBalance, } from 'shared/hooks'; import { useBlockNumber } from 'wagmi'; import { type AddKeysFormNetworkData } from './types'; diff --git a/features/create-node-operator/submit-keys-form/context/use-submit-keys-form-network-data.tsx b/features/create-node-operator/submit-keys-form/context/use-submit-keys-form-network-data.tsx index 3aba60b8..16e70653 100644 --- a/features/create-node-operator/submit-keys-form/context/use-submit-keys-form-network-data.tsx +++ b/features/create-node-operator/submit-keys-form/context/use-submit-keys-form-network-data.tsx @@ -1,8 +1,4 @@ -import { - useEthereumBalance, - useSTETHBalance, - useWSTETHBalance, -} from '@lido-sdk/react'; +import { useEthereumBalance } from '@lido-sdk/react'; import { STRATEGY_LAZY } from 'consts/swr-strategies'; import { useCallback, useMemo } from 'react'; import { @@ -14,6 +10,8 @@ import { useKeysAvailable, useKeysUploadLimit, useStakingLimitInfo, + useSTETHBalance, + useWSTETHBalance, } from 'shared/hooks'; import { useBlockNumber } from 'wagmi'; import { type SubmitKeysFormNetworkData } from './types'; diff --git a/package.json b/package.json index 964a0e63..b8a57909 100644 --- a/package.json +++ b/package.json @@ -26,11 +26,11 @@ "@ethersproject/providers": "^5.7.0", "@ethersproject/units": "^5.7.0", "@lido-sdk/constants": "^3.5.0", - "@lido-sdk/contracts": "^3.0.4", + "@lido-sdk/contracts": "^3.1.2", "@lido-sdk/fetch": "^2.3.1", - "@lido-sdk/helpers": "^1.5.1", - "@lido-sdk/providers": "^1.4.14", - "@lido-sdk/react": "^2.0.5", + "@lido-sdk/helpers": "^1.7.0", + "@lido-sdk/providers": "1.4.14", + "@lido-sdk/react": "2.0.5", "@lidofinance/address": "^1.4.0", "@lidofinance/analytics-matomo": "^0.51.0", "@lidofinance/api-logger": "^0.47.0", diff --git a/shared/hooks/index.ts b/shared/hooks/index.ts index e0072a6b..b94a509a 100644 --- a/shared/hooks/index.ts +++ b/shared/hooks/index.ts @@ -22,6 +22,7 @@ export * from './use-keys-available'; export * from './use-keys-cl-status'; export * from './use-keys-upload-limit'; export * from './use-keys-with-status'; +export * from './use-lido-contracts'; export * from './use-mainnet-static-rpc-provider'; export * from './use-network-duplicates'; export * from './use-node-operators-fetcher-from-events'; diff --git a/shared/hooks/use-lido-contracts.ts b/shared/hooks/use-lido-contracts.ts new file mode 100644 index 00000000..67df7019 --- /dev/null +++ b/shared/hooks/use-lido-contracts.ts @@ -0,0 +1,23 @@ +import { getTokenAddress, TOKENS } from '@lido-sdk/constants'; +import { StethAbiFactory, WstethAbiFactory } from '@lido-sdk/contracts'; +import { contractHooksFactory, hooksFactory } from '@lido-sdk/react'; + +const stethContract = contractHooksFactory(StethAbiFactory, (chainId) => + getTokenAddress(chainId, TOKENS.STETH), +); +export const useSTETHContractRPC = stethContract.useContractRPC; + +const stethMethods = hooksFactory((chainId) => + getTokenAddress(chainId, TOKENS.STETH), +); +export const useSTETHBalance = stethMethods.useTokenBalance; + +const wstethContract = contractHooksFactory(WstethAbiFactory, (chainId) => + getTokenAddress(chainId, TOKENS.WSTETH), +); +export const useWSTETHContractRPC = wstethContract.useContractRPC; + +const wstehMethods = hooksFactory((chainId) => + getTokenAddress(chainId, TOKENS.WSTETH), +); +export const useWSTETHBalance = wstehMethods.useTokenBalance; diff --git a/shared/hooks/use-permit-signature.ts b/shared/hooks/use-permit-signature.ts index 2888556f..a978779b 100644 --- a/shared/hooks/use-permit-signature.ts +++ b/shared/hooks/use-permit-signature.ts @@ -1,15 +1,15 @@ import { hexValue, splitSignature } from '@ethersproject/bytes'; import { StethAbi } from '@lido-sdk/contracts'; -import { - useSDK, - useSTETHContractRPC, - useWSTETHContractRPC, -} from '@lido-sdk/react'; +import { useSDK } from '@lido-sdk/react'; import { TOKENS } from 'consts/tokens'; import { getUnixTime, hoursToSeconds } from 'date-fns/fp'; import { BigNumber, BytesLike, TypedDataDomain } from 'ethers'; import { useCallback } from 'react'; -import { useAccount } from 'shared/hooks'; +import { + useAccount, + useSTETHContractRPC, + useWSTETHContractRPC, +} from 'shared/hooks'; import invariant from 'tiny-invariant'; import { Address, useChainId } from 'wagmi'; diff --git a/shared/hooks/useNodeOperatorRewards.ts b/shared/hooks/useNodeOperatorRewards.ts index 2db111b1..acb38bad 100644 --- a/shared/hooks/useNodeOperatorRewards.ts +++ b/shared/hooks/useNodeOperatorRewards.ts @@ -1,14 +1,17 @@ import { Zero } from '@ethersproject/constants'; -import { useContractSWR, useSTETHContractRPC } from '@lido-sdk/react'; +import { useContractSWR } from '@lido-sdk/react'; import { StandardMerkleTree } from '@openzeppelin/merkle-tree'; import { STRATEGY_LAZY } from 'consts/swr-strategies'; import { BigNumber } from 'ethers'; import { useMemo } from 'react'; import { NodeOperatorId, RewardProof, RewardsBalance } from 'types'; import { findIndexAndLeaf } from 'utils'; -import { useCSFeeDistributorRPC } from './useCsmContracts'; -import { useFeeDistributorTree } from './useFeeDistributorTree'; -import { useMergeSwr } from './useMergeSwr'; +import { + useCSFeeDistributorRPC, + useFeeDistributorTree, + useMergeSwr, + useSTETHContractRPC, +} from 'shared/hooks'; export type RewardsTreeLeaf = [NodeOperatorId, string]; diff --git a/shared/hooks/useStakingLimitInfo.ts b/shared/hooks/useStakingLimitInfo.ts index 16634b7e..6542a136 100644 --- a/shared/hooks/useStakingLimitInfo.ts +++ b/shared/hooks/useStakingLimitInfo.ts @@ -1,9 +1,9 @@ -import { useLidoSWR, useSTETHContractRPC } from '@lido-sdk/react'; +import { useLidoSWR } from '@lido-sdk/react'; import { BigNumber } from 'ethers'; import { Zero } from '@ethersproject/constants'; import { STRATEGY_LAZY } from 'consts/swr-strategies'; -import { useAccount } from './use-account'; +import { useAccount, useSTETHContractRPC } from 'shared/hooks'; const getMaxStakeAmount = (limitInfo: { isStakingPaused: boolean; diff --git a/shared/hooks/useStethByWsteth.ts b/shared/hooks/useStethByWsteth.ts index c064f7a3..1fe056d7 100644 --- a/shared/hooks/useStethByWsteth.ts +++ b/shared/hooks/useStethByWsteth.ts @@ -1,6 +1,7 @@ -import { useContractSWR, useWSTETHContractRPC } from '@lido-sdk/react'; +import { useContractSWR } from '@lido-sdk/react'; import { STRATEGY_LAZY } from 'consts/swr-strategies'; import { BigNumber } from 'ethers'; +import { useWSTETHContractRPC } from 'shared/hooks'; export const useStethByWsteth = (wsteth: BigNumber | undefined) => { return useContractSWR({ diff --git a/shared/hooks/useWstethBySteth.ts b/shared/hooks/useWstethBySteth.ts index fce2953f..83cea06d 100644 --- a/shared/hooks/useWstethBySteth.ts +++ b/shared/hooks/useWstethBySteth.ts @@ -1,6 +1,7 @@ -import { useContractSWR, useWSTETHContractRPC } from '@lido-sdk/react'; +import { useContractSWR } from '@lido-sdk/react'; import { STRATEGY_LAZY } from 'consts/swr-strategies'; import { BigNumber } from 'ethers'; +import { useWSTETHContractRPC } from 'shared/hooks'; export const useWstethBySteth = (steth: BigNumber | undefined) => { return useContractSWR({ diff --git a/yarn.lock b/yarn.lock index 14b66e1c..a4187705 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2545,10 +2545,10 @@ bignumber.js "^9.1.2" rxjs "^7.8.1" -"@lido-sdk/constants@3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@lido-sdk/constants/-/constants-3.3.0.tgz#044b652a000a067b9ee0ae9f58814434779bb108" - integrity sha512-R5XINgj/EQvyBfPF+Zv9B/ycFCqARD8rbWDp3J4luAMDbSUP3ncGw0x7Aj836k96eVnrh+jfd2tz/c99Dm6IaA== +"@lido-sdk/constants@3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@lido-sdk/constants/-/constants-3.2.1.tgz#0c4582d7e76e4f8bc42e8f3c0d14dc0fbe481d77" + integrity sha512-zes0Mw0r1nEQYBNHV5fxK2H9Byowejy4haFy9LYDh1nL72aNJzzdh5S5iM+pKlEuLHQJHV5lVO/k9tunNJIKqQ== dependencies: tiny-invariant "^1.1.0" @@ -2559,12 +2559,20 @@ dependencies: tiny-invariant "^1.1.0" -"@lido-sdk/contracts@3.0.5", "@lido-sdk/contracts@^3.0.4": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@lido-sdk/contracts/-/contracts-3.0.5.tgz#9778e83258de241e26bcd5c76589ff010856afbb" - integrity sha512-piu5wKqxs9QS85qG9OrwXfdg5ZCg+xlggj6Q8PMo9qbAk0UzBVuU7CcXUalpKeJpTQMV4SnvcJ0dwZNR2weK8g== +"@lido-sdk/contracts@3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@lido-sdk/contracts/-/contracts-3.0.4.tgz#85e3b203aa0a38841ecf22d7ac4e5f8d70848920" + integrity sha512-oW7gyHKcrss77sEPMmYm38M0CQ5+3GGlNewu9D+UJhtxRpLa+Jh3nWEd5tq/hMdMSN9cGoerVKFfBAhw6zKajg== dependencies: - "@lido-sdk/constants" "3.3.0" + "@lido-sdk/constants" "3.2.1" + tiny-invariant "^1.1.0" + +"@lido-sdk/contracts@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@lido-sdk/contracts/-/contracts-3.1.2.tgz#b534cc536dd8c1bb79854132cd0b14207a7afc22" + integrity sha512-a7sYlegPQ24V6tX1uCneURInnkbRj4QOlkFfzAlc3Fy/+4/JCvt6mq84FD1I0pq6bOaKjDu6nwP9RVr22z4Ojw== + dependencies: + "@lido-sdk/constants" "3.5.0" tiny-invariant "^1.1.0" "@lido-sdk/fetch@^2.3.1": @@ -2576,29 +2584,37 @@ node-fetch "^2.6.7" tiny-invariant "^1.1.0" -"@lido-sdk/helpers@1.6.0", "@lido-sdk/helpers@^1.5.1": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@lido-sdk/helpers/-/helpers-1.6.0.tgz#551cde8aa1251b310d4e1f93f7ec12680992c639" - integrity sha512-rg8sV7l3SWebx8tiagaDf+Q1F+UfgZ2FS31NDPzBUtG++QKP+9V1hTOrpwLNsmJhqQ0zbNhm84+ykHbCmEO+Cw== +"@lido-sdk/helpers@1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@lido-sdk/helpers/-/helpers-1.5.1.tgz#ced13f1df6e34a1d4ad551fde299524dc237b694" + integrity sha512-n8sTliverpxOy7PeTCUyG+bQPIJdg57AOON+6X2tZ19JxU3r6ZhHzo33x/9022aKu0A/Ya7edREDB6MadymdRg== dependencies: - "@lido-sdk/constants" "3.3.0" + "@lido-sdk/constants" "3.2.1" tiny-invariant "^1.1.0" -"@lido-sdk/providers@^1.4.14": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@lido-sdk/providers/-/providers-1.4.15.tgz#2640afbd247cff90952d6710888d8b6a3c2ad0cd" - integrity sha512-155BlcEgKWFlC17itMAeflhfRnF+ejrRUOONfexgIzjcOtam+PB1WNLzpXj8zkhcLbuMW11zYJ0cfIc4QsToag== +"@lido-sdk/helpers@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@lido-sdk/helpers/-/helpers-1.7.0.tgz#ba1d75d9c394bcc27be05eda9755a7d968809b6f" + integrity sha512-adniJaFUHbw5cDxK+cVpuS85/lahYWqaQZo/vpM1dyMgfaYI1SdK7aLKXSMqxJxp6CI5Bx0uq0KS6ceQYqS47g== dependencies: - "@lido-sdk/constants" "3.3.0" + "@lido-sdk/constants" "3.5.0" + tiny-invariant "^1.1.0" -"@lido-sdk/react@^2.0.5": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@lido-sdk/react/-/react-2.0.6.tgz#f1a245803eb7136f396ceff695a598b0c92056dd" - integrity sha512-4Gwnl+6v/3JS6yyfbyyJj4P/r6slcfW8GESSl5XvPQcK3T/Jt54MN3vIv1tmFyPr1RM/vxtiR4doR+jKEItadA== +"@lido-sdk/providers@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@lido-sdk/providers/-/providers-1.4.14.tgz#b7c714aa753d662c0d51f71ee4990b3cb78ce790" + integrity sha512-m422uXuaGoXoUlF8oyFTIQsj8ljVet/x7nK0xF8UoURm/iuaAhTbEXpcxhmkx8JSSDli1928apJRAwxG0McgnQ== + dependencies: + "@lido-sdk/constants" "3.2.1" + +"@lido-sdk/react@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@lido-sdk/react/-/react-2.0.5.tgz#13496354863bcd98f78cf223ac65254e9620419a" + integrity sha512-XRrO1Zg13IJO0TKhqT/TBJYTWuZg+2+8T+FPmegxwe7mfJSrg025lhtRPW8cjLHvPvVsW6RVgV/vW9iIeHvYpA== dependencies: - "@lido-sdk/constants" "3.3.0" - "@lido-sdk/contracts" "3.0.5" - "@lido-sdk/helpers" "1.6.0" + "@lido-sdk/constants" "3.2.1" + "@lido-sdk/contracts" "3.0.4" + "@lido-sdk/helpers" "1.5.1" swr "^1.0.1" tiny-invariant "^1.1.0" tiny-warning "^1.0.3" From 3ceb5289750ca1e65ed90b40e215fc60cf151620 Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 18 Mar 2025 13:11:28 +0300 Subject: [PATCH 07/42] fix: deposit queue with dividing by zero --- .../deposit-queue/use-deposit-queue-graph.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/features/view-keys/deposit-queue/use-deposit-queue-graph.ts b/features/view-keys/deposit-queue/use-deposit-queue-graph.ts index 0e14c885..9f36373d 100644 --- a/features/view-keys/deposit-queue/use-deposit-queue-graph.ts +++ b/features/view-keys/deposit-queue/use-deposit-queue-graph.ts @@ -79,10 +79,13 @@ export const useDepositQueueGraph = (fullView = false) => { const addedSize = ccc(added, queue.add(active)); const limitOffset = extraLow ? 8 : extraHigh ? 95 : cc(capacity); - const koef = queue - .mul(100) - .div(batches?.summ || queue) - .toNumber(); + const koef = + batches?.summ.isZero() || queue.isZero() + ? One.mul(100) + : queue + .mul(100) + .div(batches?.summ || queue) + .toNumber(); const yourBatches = mergeBatches( batches?.list.map((batch) => { From 3eb66a361f1d33c5647ad1b4b26bae5ae596485d Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 18 Mar 2025 13:28:28 +0300 Subject: [PATCH 08/42] chore: disable etherscan link hoodi --- .../external-icon-link/etherscan-address-link.tsx | 6 ++++++ shared/components/tx-link-etherscan/tx-link-etherscan.tsx | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/shared/components/external-icon-link/etherscan-address-link.tsx b/shared/components/external-icon-link/etherscan-address-link.tsx index 21fd787c..e5d68dee 100644 --- a/shared/components/external-icon-link/etherscan-address-link.tsx +++ b/shared/components/external-icon-link/etherscan-address-link.tsx @@ -4,6 +4,7 @@ import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events'; import { FC } from 'react'; import { useAccount } from 'shared/hooks'; import { MatomoLink } from '../matomo-link/matomo-link'; +import { CHAINS } from '@lido-sdk/constants'; type Props = { address: string; @@ -11,6 +12,11 @@ type Props = { export const EtherscanAddressLink: FC = ({ address }) => { const { chainId } = useAccount(); + + if (chainId === CHAINS.Hoodi) { + return null; + } + const href = getEtherscanAddressLink(chainId ?? 0, address); return ( diff --git a/shared/components/tx-link-etherscan/tx-link-etherscan.tsx b/shared/components/tx-link-etherscan/tx-link-etherscan.tsx index cd5c881c..354eb9c0 100644 --- a/shared/components/tx-link-etherscan/tx-link-etherscan.tsx +++ b/shared/components/tx-link-etherscan/tx-link-etherscan.tsx @@ -3,6 +3,7 @@ import { getEtherscanTxLink } from '@lido-sdk/helpers'; import { MatomoLink } from '../matomo-link/matomo-link'; import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events'; import { FC } from 'react'; +import { CHAINS } from '@lido-sdk/constants'; type TxLinkEtherscanProps = { text?: string; @@ -17,6 +18,10 @@ export const TxLinkEtherscan: FC = ({ if (!txHash) return null; + if (chainId === CHAINS.Hoodi) { + return null; + } + return ( Date: Tue, 18 Mar 2025 13:39:03 +0300 Subject: [PATCH 09/42] chore: disable keys-api --- consts/external-links.ts | 125 +++++++++--------- shared/hooks/use-network-duplicates.ts | 2 + shared/hooks/use-operator-in-other-module.ts | 6 +- .../keys/validate/check-network-duplicates.ts | 11 +- 4 files changed, 76 insertions(+), 68 deletions(-) diff --git a/consts/external-links.ts b/consts/external-links.ts index c2315340..6316369e 100644 --- a/consts/external-links.ts +++ b/consts/external-links.ts @@ -36,71 +36,72 @@ type ExternalLinksConstants = { surveyApi: string; }; -export const EXTERNAL_LINKS_BY_NETWORK: Record = { - [CHAINS.Mainnet]: { - earlyAdoptionTree: - 'https://raw.githubusercontent.com/lidofinance/community-staking-module/v1.0/artifacts/mainnet/early-adoption/merkle-tree.json', - rewardsTree: - 'https://raw.githubusercontent.com/lidofinance/csm-rewards/mainnet/tree.json', - earlyAdoptionSources: - 'https://github.com/lidofinance/community-staking-module/blob/v1.0/artifacts/mainnet/early-adoption/addresses.json', - earlyAdoptionAbout: - 'https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22', - feedbackForm: 'https://forms.gle/GL9RYeV2g4px58Sv8', - stakeWidget: 'https://stake.lido.fi', +export const EXTERNAL_LINKS_BY_NETWORK: Record = + { + [CHAINS.Mainnet]: { + earlyAdoptionTree: + 'https://raw.githubusercontent.com/lidofinance/community-staking-module/v1.0/artifacts/mainnet/early-adoption/merkle-tree.json', + rewardsTree: + 'https://raw.githubusercontent.com/lidofinance/csm-rewards/mainnet/tree.json', + earlyAdoptionSources: + 'https://github.com/lidofinance/community-staking-module/blob/v1.0/artifacts/mainnet/early-adoption/addresses.json', + earlyAdoptionAbout: + 'https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22', + feedbackForm: 'https://forms.gle/GL9RYeV2g4px58Sv8', + stakeWidget: 'https://stake.lido.fi', - feesMonitoring: 'https://fees-monitoring.lido.fi', - operatorsWidget: 'https://operators.lido.fi', - beaconchain: 'https://beaconcha.in', - beaconchainDashboard: 'https://v2-beta-mainnet.beaconcha.in/dashboard', - ratedExplorer: 'https://explorer.rated.network', - ethseerDashboard: 'https://ethseer.io', - subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', - keysApi: 'https://keys-api.lido.fi', - surveyApi: 'https://csm-surveys-api-mainnet.up.railway.app', - }, - [CHAINS.Holesky]: { - earlyAdoptionTree: - 'https://raw.githubusercontent.com/lidofinance/community-staking-module/v1.0/artifacts/holesky/early-adoption/merkle-tree.json', - rewardsTree: - 'https://raw.githubusercontent.com/lidofinance/csm-rewards/holesky/tree.json', - earlyAdoptionSources: - 'https://github.com/lidofinance/community-staking-module/blob/v1.0/artifacts/holesky/early-adoption/addresses.json', - earlyAdoptionAbout: - 'https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22', - feedbackForm: 'https://forms.gle/ZBUqbykaZokJLf4M7', - stakeWidget: 'https://stake-holesky.testnet.fi', + feesMonitoring: 'https://fees-monitoring.lido.fi', + operatorsWidget: 'https://operators.lido.fi', + beaconchain: 'https://beaconcha.in', + beaconchainDashboard: 'https://v2-beta-mainnet.beaconcha.in/dashboard', + ratedExplorer: 'https://explorer.rated.network', + ethseerDashboard: 'https://ethseer.io', + subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', + keysApi: 'https://keys-api.lido.fi', + surveyApi: 'https://csm-surveys-api-mainnet.up.railway.app', + }, + [CHAINS.Holesky]: { + earlyAdoptionTree: + 'https://raw.githubusercontent.com/lidofinance/community-staking-module/v1.0/artifacts/holesky/early-adoption/merkle-tree.json', + rewardsTree: + 'https://raw.githubusercontent.com/lidofinance/csm-rewards/holesky/tree.json', + earlyAdoptionSources: + 'https://github.com/lidofinance/community-staking-module/blob/v1.0/artifacts/holesky/early-adoption/addresses.json', + earlyAdoptionAbout: + 'https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22', + feedbackForm: 'https://forms.gle/ZBUqbykaZokJLf4M7', + stakeWidget: 'https://stake-holesky.testnet.fi', - feesMonitoring: 'https://fees-monitoring-holesky.testnet.fi', - operatorsWidget: 'https://operators-holesky.testnet.fi', - beaconchain: 'https://holesky.beaconcha.in', - beaconchainDashboard: 'https://v2-beta-holesky.beaconcha.in/dashboard', - ratedExplorer: 'https://explorer.rated.network', - subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', - keysApi: 'https://keys-api-holesky.testnet.fi', - surveyApi: '', - }, - // FIXME: links - [CHAINS.Hoodi]: { - earlyAdoptionTree: '', - rewardsTree: '', - earlyAdoptionSources: - 'https://github.com/lidofinance/community-staking-module/blob/v1.0/artifacts/holesky/early-adoption/addresses.json', - earlyAdoptionAbout: - 'https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22', - feedbackForm: 'https://forms.gle/ZBUqbykaZokJLf4M7', - stakeWidget: 'https://stake-holesky.testnet.fi', + feesMonitoring: 'https://fees-monitoring-holesky.testnet.fi', + operatorsWidget: 'https://operators-holesky.testnet.fi', + beaconchain: 'https://holesky.beaconcha.in', + beaconchainDashboard: 'https://v2-beta-holesky.beaconcha.in/dashboard', + ratedExplorer: 'https://explorer.rated.network', + subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', + keysApi: 'https://keys-api-holesky.testnet.fi', + surveyApi: '', + }, + // FIXME: links + [CHAINS.Hoodi]: { + earlyAdoptionTree: '', + rewardsTree: '', + earlyAdoptionSources: + 'https://github.com/lidofinance/community-staking-module/blob/v1.0/artifacts/holesky/early-adoption/addresses.json', + earlyAdoptionAbout: + 'https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22', + feedbackForm: 'https://forms.gle/ZBUqbykaZokJLf4M7', + stakeWidget: 'https://stake-holesky.testnet.fi', - feesMonitoring: 'https://fees-monitoring-holesky.testnet.fi', - operatorsWidget: 'https://operators-holesky.testnet.fi', - beaconchain: 'https://holesky.beaconcha.in', - beaconchainDashboard: 'https://v2-beta-holesky.beaconcha.in/dashboard', - ratedExplorer: 'https://explorer.rated.network', - subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', - keysApi: 'http://hr6vb81d1ndsx-hoodi-keys-api.valset-01.testnet.fi', - surveyApi: 'https://csm-surveys-api-testnet.up.railway.app', - }, -}; + feesMonitoring: 'https://fees-monitoring-holesky.testnet.fi', + operatorsWidget: 'https://operators-holesky.testnet.fi', + beaconchain: 'https://holesky.beaconcha.in', + beaconchainDashboard: 'https://v2-beta-holesky.beaconcha.in/dashboard', + ratedExplorer: 'https://explorer.rated.network', + subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', + keysApi: 'http://hr6vb81d1ndsx-hoodi-keys-api.valset-01.testnet.fi', + surveyApi: 'https://csm-surveys-api-testnet.up.railway.app', + }, + }; export const getExternalLinks = ( chainId: CHAINS | undefined = config.defaultChain, diff --git a/shared/hooks/use-network-duplicates.ts b/shared/hooks/use-network-duplicates.ts index ea1f5a95..e6bead31 100644 --- a/shared/hooks/use-network-duplicates.ts +++ b/shared/hooks/use-network-duplicates.ts @@ -91,6 +91,8 @@ export const useNetworkDuplicates = (config = STRATEGY_CONSTANT) => { ['no-keys', nodeOperatorId, chainId], useCallback(async () => { invariant(nodeOperatorId, 'NodeOperatorId is not defined'); + if (!keysApi) return []; + const moduleId = getCsmConstants(chainId).stakingModuleId; const keys = await getKeys(keysApi, moduleId, nodeOperatorId); const rawKeys = await findKeys( diff --git a/shared/hooks/use-operator-in-other-module.ts b/shared/hooks/use-operator-in-other-module.ts index 95c8d30f..98f65e7d 100644 --- a/shared/hooks/use-operator-in-other-module.ts +++ b/shared/hooks/use-operator-in-other-module.ts @@ -73,7 +73,11 @@ const useSROperators = () => { return { operators, modules }; }; - return useLidoSWR(['sr-operators', keysApi], fetcher, STRATEGY_IMMUTABLE); + return useLidoSWR( + ['sr-operators', keysApi], + keysApi ? fetcher : null, + STRATEGY_IMMUTABLE, + ); }; export const useOperatorInOtherModule = () => { diff --git a/shared/keys/validate/check-network-duplicates.ts b/shared/keys/validate/check-network-duplicates.ts index 0981b4bc..016fed0d 100644 --- a/shared/keys/validate/check-network-duplicates.ts +++ b/shared/keys/validate/check-network-duplicates.ts @@ -1,10 +1,9 @@ -import { DepositData } from 'types'; -import { TRIM_LENGTH } from './constants'; import { trimAddress } from '@lidofinance/address'; -import { trimOx } from '../utils'; -import { HexString } from '../types'; -import { CHAINS } from '@lido-sdk/constants'; import { getExternalLinks } from 'consts/external-links'; +import { DepositData } from 'types'; +import { HexString } from '../types'; +import { trimOx } from '../utils'; +import { TRIM_LENGTH } from './constants'; type ResponseData = { data: { @@ -23,6 +22,8 @@ const findDuplicate = async (pubkeys: HexString[]) => { // TODO: timeout // TODO: cache const { keysApi } = getExternalLinks(); + if (!keysApi) return; + const response = await fetch(`${keysApi}/v1/keys/find`, { method: 'post', body: JSON.stringify({ pubkeys }), From 1d007ad854aeb1cf563ee1e5fd2042b384ec80aa Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 18 Mar 2025 13:58:42 +0300 Subject: [PATCH 10/42] fix: dashboard keys if keysCount = zero --- features/dashboard/keys/keys-section.tsx | 2 +- shared/hooks/useNodeOperatorKeys.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/dashboard/keys/keys-section.tsx b/features/dashboard/keys/keys-section.tsx index de7e2680..c3eea72f 100644 --- a/features/dashboard/keys/keys-section.tsx +++ b/features/dashboard/keys/keys-section.tsx @@ -103,7 +103,7 @@ export const KeysSection: FC = () => { tooltip="Keys that have already exited and withdrawn" /> - {keys?.length && ( + {!!keys?.length && ( Date: Tue, 18 Mar 2025 16:44:09 +0300 Subject: [PATCH 11/42] fix: faq with Hoodi WC & feeRecipient --- faq/{ => holesky}/bond-1.md | 0 faq/{testnet-bond-2.md => holesky/bond-2.md} | 0 faq/{testnet-bond-3.md => holesky/bond-3.md} | 0 faq/{ => holesky}/bond-4.md | 0 faq/{testnet-bond-5.md => holesky/bond-5.md} | 0 faq/{testnet-keys-1.md => holesky/keys-1.md} | 0 faq/{ => holesky}/keys-10.md | 0 faq/{ => holesky}/keys-11.md | 0 .../keys-12.md} | 0 .../keys-13.md} | 0 faq/{ => holesky}/keys-2.md | 0 faq/{testnet-keys-3.md => holesky/keys-3.md} | 0 .../keys-3a.md} | 0 faq/{testnet-keys-4.md => holesky/keys-4.md} | 0 .../keys-4a.md} | 0 faq/{ => holesky}/keys-5.md | 0 faq/{testnet-keys-6.md => holesky/keys-6.md} | 0 faq/{ => holesky}/keys-7.md | 0 faq/{ => holesky}/keys-8.md | 0 faq/{ => holesky}/keys-9.md | 0 faq/{ => holesky}/locked-1.md | 0 faq/{ => holesky}/locked-2.md | 0 faq/{ => holesky}/locked-3.md | 0 faq/{ => holesky}/main-1.md | 0 faq/{ => holesky}/main-2.md | 0 faq/{ => holesky}/main-3.md | 0 faq/{ => holesky}/main-4.md | 0 faq/{ => holesky}/main-5.md | 0 faq/{ => holesky}/main-6.md | 0 faq/{testnet-main-7.md => holesky/main-7.md} | 0 .../main-7a.md} | 0 faq/{testnet-main-8.md => holesky/main-8.md} | 0 faq/{ => holesky}/roles-1.md | 0 faq/{ => holesky}/roles-2.md | 0 faq/{ => holesky}/roles-3.md | 0 faq/{ => holesky}/roles-4.md | 0 faq/{ => holesky}/roles-5.md | 0 faq/{testnet-bond-1.md => hoodi/bond-1.md} | 0 faq/hoodi/bond-2.md | 7 ++++++ faq/hoodi/bond-3.md | 9 ++++++++ faq/{testnet-bond-4.md => hoodi/bond-4.md} | 0 faq/hoodi/bond-5.md | 13 +++++++++++ faq/hoodi/keys-1.md | 13 +++++++++++ faq/{testnet-keys-10.md => hoodi/keys-10.md} | 0 faq/{testnet-keys-11.md => hoodi/keys-11.md} | 0 faq/hoodi/keys-12.md | 5 ++++ faq/hoodi/keys-13.md | 18 +++++++++++++++ faq/{testnet-keys-2.md => hoodi/keys-2.md} | 0 faq/hoodi/keys-3.md | 13 +++++++++++ faq/hoodi/keys-3a.md | 13 +++++++++++ faq/hoodi/keys-4.md | 10 ++++++++ faq/hoodi/keys-4a.md | 10 ++++++++ faq/{testnet-keys-5.md => hoodi/keys-5.md} | 0 faq/hoodi/keys-6.md | 16 +++++++++++++ faq/{testnet-keys-7.md => hoodi/keys-7.md} | 0 faq/{testnet-keys-8.md => hoodi/keys-8.md} | 0 faq/{testnet-keys-9.md => hoodi/keys-9.md} | 0 .../locked-1.md} | 0 .../locked-2.md} | 0 .../locked-3.md} | 0 faq/{testnet-main-1.md => hoodi/main-1.md} | 0 faq/{testnet-main-2.md => hoodi/main-2.md} | 0 faq/{testnet-main-3.md => hoodi/main-3.md} | 0 faq/{testnet-main-4.md => hoodi/main-4.md} | 0 faq/{testnet-main-5.md => hoodi/main-5.md} | 0 faq/{testnet-main-6.md => hoodi/main-6.md} | 0 faq/hoodi/main-7.md | 12 ++++++++++ faq/hoodi/main-7a.md | 12 ++++++++++ faq/hoodi/main-8.md | 5 ++++ faq/{testnet-roles-1.md => hoodi/roles-1.md} | 0 faq/{testnet-roles-2.md => hoodi/roles-2.md} | 0 faq/{testnet-roles-3.md => hoodi/roles-3.md} | 0 faq/{testnet-roles-4.md => hoodi/roles-4.md} | 0 faq/{testnet-roles-5.md => hoodi/roles-5.md} | 0 faq/mainnet/bond-1.md | 8 +++++++ faq/{ => mainnet}/bond-2.md | 0 faq/{ => mainnet}/bond-3.md | 0 faq/mainnet/bond-4.md | 9 ++++++++ faq/{ => mainnet}/bond-5.md | 0 faq/{ => mainnet}/keys-1.md | 0 faq/mainnet/keys-10.md | 10 ++++++++ faq/mainnet/keys-11.md | 6 +++++ faq/{ => mainnet}/keys-12.md | 0 faq/{ => mainnet}/keys-13.md | 0 faq/mainnet/keys-2.md | 11 +++++++++ faq/{ => mainnet}/keys-3.md | 0 faq/{ => mainnet}/keys-3a.md | 0 faq/{ => mainnet}/keys-4.md | 0 faq/{ => mainnet}/keys-4a.md | 0 faq/mainnet/keys-5.md | 5 ++++ faq/{ => mainnet}/keys-6.md | 0 faq/mainnet/keys-7.md | 5 ++++ faq/mainnet/keys-8.md | 5 ++++ faq/mainnet/keys-9.md | 5 ++++ faq/mainnet/locked-1.md | 5 ++++ faq/mainnet/locked-2.md | 7 ++++++ faq/mainnet/locked-3.md | 5 ++++ faq/mainnet/main-1.md | 9 ++++++++ faq/mainnet/main-2.md | 10 ++++++++ faq/mainnet/main-3.md | 8 +++++++ faq/mainnet/main-4.md | 12 ++++++++++ faq/mainnet/main-5.md | 5 ++++ faq/mainnet/main-6.md | 11 +++++++++ faq/{ => mainnet}/main-7.md | 0 faq/{ => mainnet}/main-7a.md | 0 faq/{ => mainnet}/main-8.md | 0 faq/mainnet/roles-1.md | 23 +++++++++++++++++++ faq/mainnet/roles-2.md | 7 ++++++ faq/mainnet/roles-3.md | 5 ++++ faq/mainnet/roles-4.md | 5 ++++ faq/mainnet/roles-5.md | 5 ++++ lib/getFaq.ts | 14 +++++------ 112 files changed, 343 insertions(+), 8 deletions(-) rename faq/{ => holesky}/bond-1.md (100%) rename faq/{testnet-bond-2.md => holesky/bond-2.md} (100%) rename faq/{testnet-bond-3.md => holesky/bond-3.md} (100%) rename faq/{ => holesky}/bond-4.md (100%) rename faq/{testnet-bond-5.md => holesky/bond-5.md} (100%) rename faq/{testnet-keys-1.md => holesky/keys-1.md} (100%) rename faq/{ => holesky}/keys-10.md (100%) rename faq/{ => holesky}/keys-11.md (100%) rename faq/{testnet-keys-12.md => holesky/keys-12.md} (100%) rename faq/{testnet-keys-13.md => holesky/keys-13.md} (100%) rename faq/{ => holesky}/keys-2.md (100%) rename faq/{testnet-keys-3.md => holesky/keys-3.md} (100%) rename faq/{testnet-keys-3a.md => holesky/keys-3a.md} (100%) rename faq/{testnet-keys-4.md => holesky/keys-4.md} (100%) rename faq/{testnet-keys-4a.md => holesky/keys-4a.md} (100%) rename faq/{ => holesky}/keys-5.md (100%) rename faq/{testnet-keys-6.md => holesky/keys-6.md} (100%) rename faq/{ => holesky}/keys-7.md (100%) rename faq/{ => holesky}/keys-8.md (100%) rename faq/{ => holesky}/keys-9.md (100%) rename faq/{ => holesky}/locked-1.md (100%) rename faq/{ => holesky}/locked-2.md (100%) rename faq/{ => holesky}/locked-3.md (100%) rename faq/{ => holesky}/main-1.md (100%) rename faq/{ => holesky}/main-2.md (100%) rename faq/{ => holesky}/main-3.md (100%) rename faq/{ => holesky}/main-4.md (100%) rename faq/{ => holesky}/main-5.md (100%) rename faq/{ => holesky}/main-6.md (100%) rename faq/{testnet-main-7.md => holesky/main-7.md} (100%) rename faq/{testnet-main-7a.md => holesky/main-7a.md} (100%) rename faq/{testnet-main-8.md => holesky/main-8.md} (100%) rename faq/{ => holesky}/roles-1.md (100%) rename faq/{ => holesky}/roles-2.md (100%) rename faq/{ => holesky}/roles-3.md (100%) rename faq/{ => holesky}/roles-4.md (100%) rename faq/{ => holesky}/roles-5.md (100%) rename faq/{testnet-bond-1.md => hoodi/bond-1.md} (100%) create mode 100644 faq/hoodi/bond-2.md create mode 100644 faq/hoodi/bond-3.md rename faq/{testnet-bond-4.md => hoodi/bond-4.md} (100%) create mode 100644 faq/hoodi/bond-5.md create mode 100644 faq/hoodi/keys-1.md rename faq/{testnet-keys-10.md => hoodi/keys-10.md} (100%) rename faq/{testnet-keys-11.md => hoodi/keys-11.md} (100%) create mode 100644 faq/hoodi/keys-12.md create mode 100644 faq/hoodi/keys-13.md rename faq/{testnet-keys-2.md => hoodi/keys-2.md} (100%) create mode 100644 faq/hoodi/keys-3.md create mode 100644 faq/hoodi/keys-3a.md create mode 100644 faq/hoodi/keys-4.md create mode 100644 faq/hoodi/keys-4a.md rename faq/{testnet-keys-5.md => hoodi/keys-5.md} (100%) create mode 100644 faq/hoodi/keys-6.md rename faq/{testnet-keys-7.md => hoodi/keys-7.md} (100%) rename faq/{testnet-keys-8.md => hoodi/keys-8.md} (100%) rename faq/{testnet-keys-9.md => hoodi/keys-9.md} (100%) rename faq/{testnet-locked-1.md => hoodi/locked-1.md} (100%) rename faq/{testnet-locked-2.md => hoodi/locked-2.md} (100%) rename faq/{testnet-locked-3.md => hoodi/locked-3.md} (100%) rename faq/{testnet-main-1.md => hoodi/main-1.md} (100%) rename faq/{testnet-main-2.md => hoodi/main-2.md} (100%) rename faq/{testnet-main-3.md => hoodi/main-3.md} (100%) rename faq/{testnet-main-4.md => hoodi/main-4.md} (100%) rename faq/{testnet-main-5.md => hoodi/main-5.md} (100%) rename faq/{testnet-main-6.md => hoodi/main-6.md} (100%) create mode 100644 faq/hoodi/main-7.md create mode 100644 faq/hoodi/main-7a.md create mode 100644 faq/hoodi/main-8.md rename faq/{testnet-roles-1.md => hoodi/roles-1.md} (100%) rename faq/{testnet-roles-2.md => hoodi/roles-2.md} (100%) rename faq/{testnet-roles-3.md => hoodi/roles-3.md} (100%) rename faq/{testnet-roles-4.md => hoodi/roles-4.md} (100%) rename faq/{testnet-roles-5.md => hoodi/roles-5.md} (100%) create mode 100644 faq/mainnet/bond-1.md rename faq/{ => mainnet}/bond-2.md (100%) rename faq/{ => mainnet}/bond-3.md (100%) create mode 100644 faq/mainnet/bond-4.md rename faq/{ => mainnet}/bond-5.md (100%) rename faq/{ => mainnet}/keys-1.md (100%) create mode 100644 faq/mainnet/keys-10.md create mode 100644 faq/mainnet/keys-11.md rename faq/{ => mainnet}/keys-12.md (100%) rename faq/{ => mainnet}/keys-13.md (100%) create mode 100644 faq/mainnet/keys-2.md rename faq/{ => mainnet}/keys-3.md (100%) rename faq/{ => mainnet}/keys-3a.md (100%) rename faq/{ => mainnet}/keys-4.md (100%) rename faq/{ => mainnet}/keys-4a.md (100%) create mode 100644 faq/mainnet/keys-5.md rename faq/{ => mainnet}/keys-6.md (100%) create mode 100644 faq/mainnet/keys-7.md create mode 100644 faq/mainnet/keys-8.md create mode 100644 faq/mainnet/keys-9.md create mode 100644 faq/mainnet/locked-1.md create mode 100644 faq/mainnet/locked-2.md create mode 100644 faq/mainnet/locked-3.md create mode 100644 faq/mainnet/main-1.md create mode 100644 faq/mainnet/main-2.md create mode 100644 faq/mainnet/main-3.md create mode 100644 faq/mainnet/main-4.md create mode 100644 faq/mainnet/main-5.md create mode 100644 faq/mainnet/main-6.md rename faq/{ => mainnet}/main-7.md (100%) rename faq/{ => mainnet}/main-7a.md (100%) rename faq/{ => mainnet}/main-8.md (100%) create mode 100644 faq/mainnet/roles-1.md create mode 100644 faq/mainnet/roles-2.md create mode 100644 faq/mainnet/roles-3.md create mode 100644 faq/mainnet/roles-4.md create mode 100644 faq/mainnet/roles-5.md diff --git a/faq/bond-1.md b/faq/holesky/bond-1.md similarity index 100% rename from faq/bond-1.md rename to faq/holesky/bond-1.md diff --git a/faq/testnet-bond-2.md b/faq/holesky/bond-2.md similarity index 100% rename from faq/testnet-bond-2.md rename to faq/holesky/bond-2.md diff --git a/faq/testnet-bond-3.md b/faq/holesky/bond-3.md similarity index 100% rename from faq/testnet-bond-3.md rename to faq/holesky/bond-3.md diff --git a/faq/bond-4.md b/faq/holesky/bond-4.md similarity index 100% rename from faq/bond-4.md rename to faq/holesky/bond-4.md diff --git a/faq/testnet-bond-5.md b/faq/holesky/bond-5.md similarity index 100% rename from faq/testnet-bond-5.md rename to faq/holesky/bond-5.md diff --git a/faq/testnet-keys-1.md b/faq/holesky/keys-1.md similarity index 100% rename from faq/testnet-keys-1.md rename to faq/holesky/keys-1.md diff --git a/faq/keys-10.md b/faq/holesky/keys-10.md similarity index 100% rename from faq/keys-10.md rename to faq/holesky/keys-10.md diff --git a/faq/keys-11.md b/faq/holesky/keys-11.md similarity index 100% rename from faq/keys-11.md rename to faq/holesky/keys-11.md diff --git a/faq/testnet-keys-12.md b/faq/holesky/keys-12.md similarity index 100% rename from faq/testnet-keys-12.md rename to faq/holesky/keys-12.md diff --git a/faq/testnet-keys-13.md b/faq/holesky/keys-13.md similarity index 100% rename from faq/testnet-keys-13.md rename to faq/holesky/keys-13.md diff --git a/faq/keys-2.md b/faq/holesky/keys-2.md similarity index 100% rename from faq/keys-2.md rename to faq/holesky/keys-2.md diff --git a/faq/testnet-keys-3.md b/faq/holesky/keys-3.md similarity index 100% rename from faq/testnet-keys-3.md rename to faq/holesky/keys-3.md diff --git a/faq/testnet-keys-3a.md b/faq/holesky/keys-3a.md similarity index 100% rename from faq/testnet-keys-3a.md rename to faq/holesky/keys-3a.md diff --git a/faq/testnet-keys-4.md b/faq/holesky/keys-4.md similarity index 100% rename from faq/testnet-keys-4.md rename to faq/holesky/keys-4.md diff --git a/faq/testnet-keys-4a.md b/faq/holesky/keys-4a.md similarity index 100% rename from faq/testnet-keys-4a.md rename to faq/holesky/keys-4a.md diff --git a/faq/keys-5.md b/faq/holesky/keys-5.md similarity index 100% rename from faq/keys-5.md rename to faq/holesky/keys-5.md diff --git a/faq/testnet-keys-6.md b/faq/holesky/keys-6.md similarity index 100% rename from faq/testnet-keys-6.md rename to faq/holesky/keys-6.md diff --git a/faq/keys-7.md b/faq/holesky/keys-7.md similarity index 100% rename from faq/keys-7.md rename to faq/holesky/keys-7.md diff --git a/faq/keys-8.md b/faq/holesky/keys-8.md similarity index 100% rename from faq/keys-8.md rename to faq/holesky/keys-8.md diff --git a/faq/keys-9.md b/faq/holesky/keys-9.md similarity index 100% rename from faq/keys-9.md rename to faq/holesky/keys-9.md diff --git a/faq/locked-1.md b/faq/holesky/locked-1.md similarity index 100% rename from faq/locked-1.md rename to faq/holesky/locked-1.md diff --git a/faq/locked-2.md b/faq/holesky/locked-2.md similarity index 100% rename from faq/locked-2.md rename to faq/holesky/locked-2.md diff --git a/faq/locked-3.md b/faq/holesky/locked-3.md similarity index 100% rename from faq/locked-3.md rename to faq/holesky/locked-3.md diff --git a/faq/main-1.md b/faq/holesky/main-1.md similarity index 100% rename from faq/main-1.md rename to faq/holesky/main-1.md diff --git a/faq/main-2.md b/faq/holesky/main-2.md similarity index 100% rename from faq/main-2.md rename to faq/holesky/main-2.md diff --git a/faq/main-3.md b/faq/holesky/main-3.md similarity index 100% rename from faq/main-3.md rename to faq/holesky/main-3.md diff --git a/faq/main-4.md b/faq/holesky/main-4.md similarity index 100% rename from faq/main-4.md rename to faq/holesky/main-4.md diff --git a/faq/main-5.md b/faq/holesky/main-5.md similarity index 100% rename from faq/main-5.md rename to faq/holesky/main-5.md diff --git a/faq/main-6.md b/faq/holesky/main-6.md similarity index 100% rename from faq/main-6.md rename to faq/holesky/main-6.md diff --git a/faq/testnet-main-7.md b/faq/holesky/main-7.md similarity index 100% rename from faq/testnet-main-7.md rename to faq/holesky/main-7.md diff --git a/faq/testnet-main-7a.md b/faq/holesky/main-7a.md similarity index 100% rename from faq/testnet-main-7a.md rename to faq/holesky/main-7a.md diff --git a/faq/testnet-main-8.md b/faq/holesky/main-8.md similarity index 100% rename from faq/testnet-main-8.md rename to faq/holesky/main-8.md diff --git a/faq/roles-1.md b/faq/holesky/roles-1.md similarity index 100% rename from faq/roles-1.md rename to faq/holesky/roles-1.md diff --git a/faq/roles-2.md b/faq/holesky/roles-2.md similarity index 100% rename from faq/roles-2.md rename to faq/holesky/roles-2.md diff --git a/faq/roles-3.md b/faq/holesky/roles-3.md similarity index 100% rename from faq/roles-3.md rename to faq/holesky/roles-3.md diff --git a/faq/roles-4.md b/faq/holesky/roles-4.md similarity index 100% rename from faq/roles-4.md rename to faq/holesky/roles-4.md diff --git a/faq/roles-5.md b/faq/holesky/roles-5.md similarity index 100% rename from faq/roles-5.md rename to faq/holesky/roles-5.md diff --git a/faq/testnet-bond-1.md b/faq/hoodi/bond-1.md similarity index 100% rename from faq/testnet-bond-1.md rename to faq/hoodi/bond-1.md diff --git a/faq/hoodi/bond-2.md b/faq/hoodi/bond-2.md new file mode 100644 index 00000000..1697d259 --- /dev/null +++ b/faq/hoodi/bond-2.md @@ -0,0 +1,7 @@ +--- +title: How often do I get rewards? +--- + +**Node Operator rewards** on testnet are calculated and made claimable by the CSM Oracle **every 7 days**. Rewards do not have to be claimed during every reporting frame, and can be left to accumulate to be claimed later. + +**Bond rebase part** of the rewards come from stETH being a rebasing token and the bond being stored in stETH. After each Accounting Oracle report that happens on testnet **every 12 epochs (1hr 20min)**, the share rate changes. Hence, the same amount of stETH shares will now be equal to a bigger stETH token balance. diff --git a/faq/hoodi/bond-3.md b/faq/hoodi/bond-3.md new file mode 100644 index 00000000..de49ca7e --- /dev/null +++ b/faq/hoodi/bond-3.md @@ -0,0 +1,9 @@ +--- +title: Why didn’t I get rewards? +anchor: why-did-not-i-get-rewards +--- + +There are two main reasons of you getting no reward within a frame: + +1. If your validator’s performance was below the threshold within the CSM Performance Oracle frame (7 days for testnet) the validator does not receive rewards for the given frame. Read more about [the CSM Performance Oracle](https://operatorportal.lido.fi/modules/community-staking-module#block-c6dc8d00f13243fcb17de3fa07ecc52c). +2. [Your Node Operator has stuck keys](https://operatorportal.lido.fi/modules/community-staking-module#block-0ed61a4c0a5a439bbb4be20e814b4e38) due to not exiting a validator requested for exit timely. diff --git a/faq/testnet-bond-4.md b/faq/hoodi/bond-4.md similarity index 100% rename from faq/testnet-bond-4.md rename to faq/hoodi/bond-4.md diff --git a/faq/hoodi/bond-5.md b/faq/hoodi/bond-5.md new file mode 100644 index 00000000..90af7696 --- /dev/null +++ b/faq/hoodi/bond-5.md @@ -0,0 +1,13 @@ +--- +title: How to claim ETH using a withdrawal NFT +anchor: how-to-claim-eth +--- + +Claiming bond and rewards in a form of ETH constitutes an stETH withdrawal process (unstake). + +The withdrawal process consists of several steps you need to do: + +- **Submit a withdrawal request** by choosing ETH as a token for bond/rewards claim. As a result of this step, you will receive a withdrawal NFT. +- **Claim your ETH** after request fulfilment. The fulfilment process takes 1-5 days (or longer), [depending on a variety of factors](https://help.lido.fi/en/articles/7858315-how-long-does-an-ethereum-withdrawal-take). To know if your ETH is ready to be claimed you, can check its status on the [Claim page](https://stake-holesky.testnet.fi/withdrawals/claim). If your request is marked as “**Ready to claim**”, it is time for you to get your ETH back. + +For more information about withdrawals, [follow the page](https://help.lido.fi/en/collections/3993867-ethereum-withdrawals). diff --git a/faq/hoodi/keys-1.md b/faq/hoodi/keys-1.md new file mode 100644 index 00000000..08ec2b0e --- /dev/null +++ b/faq/hoodi/keys-1.md @@ -0,0 +1,13 @@ +--- +title: How to set up a validator for CSM testnet? +--- + +A detailed guide on preparing all the validation tools for CSM can be found [here](https://dvt-homestaker.stakesaurus.com/bonded-validators-setup/lido-csm). + +A shorter flow of setting up a CSM validator for **testnet** looks as follows: + +1. [Generate new validator keys](https://dvt-homestaker.stakesaurus.com/keystore-generation-and-mev-boost/validator-key-generation) setting the `withdrawal_address` to the [Lido Withdrawal Vault](https://hoodi.cloud.blockscout.com/address/0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2) on **Hoodi:** [`0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2`](https://hoodi.cloud.blockscout.com/address/0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2) and specify the deposit amount of 32 ETH (do **NOT** make a deposit) +2. [Configure your validator client](https://dvt-homestaker.stakesaurus.com/native-solo-staking-setup/validator-client-setup) (and/or beacon node) setting the `fee_recipient` flag to the designated fee recipient address (Lido Execution Layer Rewards Vault) on **Hoodi:** [`0x9b108015fe433F173696Af3Aa0CF7CDb3E104258`](https://hoodi.cloud.blockscout.com/address/0x9b108015fe433F173696Af3Aa0CF7CDb3E104258) and import the newly generated CSM keystores +3. **Do not setup any MEV-Boost** +4. [Upload the newly generated deposit data](https://dvt-homestaker.stakesaurus.com/bonded-validators-setup/lido-csm/upload-remove-view-validator-keys) file pertaining to your CSM keystores onto [the Lido CSM Widget](https://csm.testnet.fi/) and provide the required bond amount in Hoodi ETH/stETH/wstETH. Before uploading, make sure that nodes are synced, running, and ready for the validator activation. +5. Wait for your CSM validator keys to be deposited through the protocol and make sure your node remains online in the meantime! diff --git a/faq/testnet-keys-10.md b/faq/hoodi/keys-10.md similarity index 100% rename from faq/testnet-keys-10.md rename to faq/hoodi/keys-10.md diff --git a/faq/testnet-keys-11.md b/faq/hoodi/keys-11.md similarity index 100% rename from faq/testnet-keys-11.md rename to faq/hoodi/keys-11.md diff --git a/faq/hoodi/keys-12.md b/faq/hoodi/keys-12.md new file mode 100644 index 00000000..1b15238d --- /dev/null +++ b/faq/hoodi/keys-12.md @@ -0,0 +1,5 @@ +--- +title: What to do in case of technical issues? +--- + +For community assistance, join the "[CSM-testnet](https://discord.com/channels/761182643269795850/1255114351120089148)" channel on the [Lido Discord server](https://discord.com/invite/lido) to seek advice and guidance. diff --git a/faq/hoodi/keys-13.md b/faq/hoodi/keys-13.md new file mode 100644 index 00000000..2dd0f972 --- /dev/null +++ b/faq/hoodi/keys-13.md @@ -0,0 +1,18 @@ +--- +title: What is the CSM Stake Share Limit? +anchor: stake-share-limit +--- + +The stake share limit is a parameter defined for each Staking Module based on its risk profile. It determines the percentage of the total stake in the Lido Protocol that can be allocated to the module. Currently, the stake share limit for CSM is set at 15%. Once CSM reaches its stake share limit, new keys can still be uploaded, but deposits to these keys may take a very long time (e.g. months), if they are deposited to at all. These factors affect the possibility of new deposits to your uploaded keys: + +- The number of keys already in the deposit queue, and the position of your keys in this queue +- The number of keys that will exit from CSM +- Changes in the total volume of stake in the Lido Protocol (both net flows as well as whether overall Lido protocol stake increases or not) + +In other words, if keys had not been deposited to before CSM reached its limit, they may still be deposited to later if: + +- The overall stake volume in the Lido Protocol increases +- Keys exit from CSM, freeing up space for new keys +- The DAO decides to increase CSM's stake share limit + +While keys are awaiting deposit, Node Operators continue to receive daily bond rewards based on the bond they submitted. However, they do not receive Node Operator rewards, as the keys remain inactive until they are fully deposited. diff --git a/faq/testnet-keys-2.md b/faq/hoodi/keys-2.md similarity index 100% rename from faq/testnet-keys-2.md rename to faq/hoodi/keys-2.md diff --git a/faq/hoodi/keys-3.md b/faq/hoodi/keys-3.md new file mode 100644 index 00000000..a4c6e0db --- /dev/null +++ b/faq/hoodi/keys-3.md @@ -0,0 +1,13 @@ +--- +title: How much bond is needed? +earlyAdoption: false +anchor: how-bond-is-calculated +--- + +The initial bond requirement for the first validator for the testnet is 2 stETH. However, for [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), this amount is reduced to 1.5 stETH to incentivize early participation. + +Subsequent bond amounts depend on the total number of validators operated by the node operator and follow a specific function known as the “[bond curve](https://operatorportal.lido.fi/modules/community-staking-module#block-2d1c307d95fc4f8ab7c32b7584f795cf)”, which adjusts the bond requirement based on the operator's validator count. + +For the testnet, the values for the bond curve are the following: + +![curve.png](/assets/curve-common.png) diff --git a/faq/hoodi/keys-3a.md b/faq/hoodi/keys-3a.md new file mode 100644 index 00000000..f1ad7a06 --- /dev/null +++ b/faq/hoodi/keys-3a.md @@ -0,0 +1,13 @@ +--- +title: How much bond is needed? +earlyAdoption: true +anchor: how-bond-is-calculated +--- + +The initial bond requirement for the first validator for the testnet is 2 stETH. However, for [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), this amount is reduced to 1.5 stETH to incentivize early participation. + +Subsequent bond amounts depend on the total number of validators operated by the node operator and follow a specific function known as the “[bond curve](https://operatorportal.lido.fi/modules/community-staking-module#block-2d1c307d95fc4f8ab7c32b7584f795cf)”, which adjusts the bond requirement based on the operator's validator count. + +For the testnet, the values for the bond curve are the following: + +![curve.png](/assets/curve-ea.png) diff --git a/faq/hoodi/keys-4.md b/faq/hoodi/keys-4.md new file mode 100644 index 00000000..731c6ded --- /dev/null +++ b/faq/hoodi/keys-4.md @@ -0,0 +1,10 @@ +--- +title: What is the bond curve? +earlyAdoption: false +--- + +[The bond curve](https://operatorportal.lido.fi/modules/community-staking-module#block-2d1c307d95fc4f8ab7c32b7584f795cf) is a function that determines the amount of bond required for each subsequent validator operated by the node operator. For [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), a unique bond curve function is applied to incentivize early participation. + +For the testnet, the values for the bond curve are the following: + +![curve.png](/assets/curve-common.png) diff --git a/faq/hoodi/keys-4a.md b/faq/hoodi/keys-4a.md new file mode 100644 index 00000000..5b43be9b --- /dev/null +++ b/faq/hoodi/keys-4a.md @@ -0,0 +1,10 @@ +--- +title: What is the bond curve? +earlyAdoption: true +--- + +[The bond curve](https://operatorportal.lido.fi/modules/community-staking-module#block-2d1c307d95fc4f8ab7c32b7584f795cf) is a function that determines the amount of bond required for each subsequent validator operated by the node operator. For [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), a unique bond curve function is applied to incentivize early participation. + +For the testnet, the values for the bond curve are the following: + +![curve.png](/assets/curve-ea.png) diff --git a/faq/testnet-keys-5.md b/faq/hoodi/keys-5.md similarity index 100% rename from faq/testnet-keys-5.md rename to faq/hoodi/keys-5.md diff --git a/faq/hoodi/keys-6.md b/faq/hoodi/keys-6.md new file mode 100644 index 00000000..0c2bddda --- /dev/null +++ b/faq/hoodi/keys-6.md @@ -0,0 +1,16 @@ +--- +title: When does a validator become active? +anchor: when-validator-become-active +--- + +After key submission, and if keys have been successfully validated, two actions are required for a validator to be activated: + +1. **Deposit by Lido Protocol**: The time to deposit a validator is unpredictable and depends on factors such as total stake inflows and outflows, gas considerations, module shares, CSM deposit queue size, and the Node Operator's place in the queue. + + You can subscribe to [the important CSM events](https://docs.lido.fi/staking-modules/csm/guides/events) to stay notified about your validator being deposited to. + + Read more information about the deposits flow [here](https://operatorportal.lido.fi/modules/community-staking-module#block-90b8ff95edc64cf7a051584820219616). + +2. **Activation on Ethereum Network**: Once deposited, the validator enters the Beacon Chain activation queue. The time to activation depends on the total number of validators in the queue awaiting activation and the rate of queue processing, which varies based on the total number of active Ethereum validators. + + You can check if the keys are activated on the [Keys tab](https://csm.testnet.fi/keys) or on [beaconcha.in](http://beaconcha.in/) diff --git a/faq/testnet-keys-7.md b/faq/hoodi/keys-7.md similarity index 100% rename from faq/testnet-keys-7.md rename to faq/hoodi/keys-7.md diff --git a/faq/testnet-keys-8.md b/faq/hoodi/keys-8.md similarity index 100% rename from faq/testnet-keys-8.md rename to faq/hoodi/keys-8.md diff --git a/faq/testnet-keys-9.md b/faq/hoodi/keys-9.md similarity index 100% rename from faq/testnet-keys-9.md rename to faq/hoodi/keys-9.md diff --git a/faq/testnet-locked-1.md b/faq/hoodi/locked-1.md similarity index 100% rename from faq/testnet-locked-1.md rename to faq/hoodi/locked-1.md diff --git a/faq/testnet-locked-2.md b/faq/hoodi/locked-2.md similarity index 100% rename from faq/testnet-locked-2.md rename to faq/hoodi/locked-2.md diff --git a/faq/testnet-locked-3.md b/faq/hoodi/locked-3.md similarity index 100% rename from faq/testnet-locked-3.md rename to faq/hoodi/locked-3.md diff --git a/faq/testnet-main-1.md b/faq/hoodi/main-1.md similarity index 100% rename from faq/testnet-main-1.md rename to faq/hoodi/main-1.md diff --git a/faq/testnet-main-2.md b/faq/hoodi/main-2.md similarity index 100% rename from faq/testnet-main-2.md rename to faq/hoodi/main-2.md diff --git a/faq/testnet-main-3.md b/faq/hoodi/main-3.md similarity index 100% rename from faq/testnet-main-3.md rename to faq/hoodi/main-3.md diff --git a/faq/testnet-main-4.md b/faq/hoodi/main-4.md similarity index 100% rename from faq/testnet-main-4.md rename to faq/hoodi/main-4.md diff --git a/faq/testnet-main-5.md b/faq/hoodi/main-5.md similarity index 100% rename from faq/testnet-main-5.md rename to faq/hoodi/main-5.md diff --git a/faq/testnet-main-6.md b/faq/hoodi/main-6.md similarity index 100% rename from faq/testnet-main-6.md rename to faq/hoodi/main-6.md diff --git a/faq/hoodi/main-7.md b/faq/hoodi/main-7.md new file mode 100644 index 00000000..f1e10d2c --- /dev/null +++ b/faq/hoodi/main-7.md @@ -0,0 +1,12 @@ +--- +title: How much bond is needed? +earlyAdoption: false +--- + +The initial bond requirement for the first validator for the testnet is 2 stETH. However, for [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), this amount is reduced to 1.5 stETH to incentivize early participation. + +Subsequent bond amounts depend on the total number of validators operated by the node operator and follow a specific function known as the “[bond curve](https://operatorportal.lido.fi/modules/community-staking-module#block-2d1c307d95fc4f8ab7c32b7584f795cf)”, which adjusts the bond requirement based on the operator's validator count. + +For the testnet, the values for the bond curve are the following: + +![curve.png](/assets/curve-common.png) diff --git a/faq/hoodi/main-7a.md b/faq/hoodi/main-7a.md new file mode 100644 index 00000000..fd008e98 --- /dev/null +++ b/faq/hoodi/main-7a.md @@ -0,0 +1,12 @@ +--- +title: How much bond is needed? +earlyAdoption: true +--- + +The initial bond requirement for the first validator for the testnet is 2 stETH. However, for [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), this amount is reduced to 1.5 stETH to incentivize early participation. + +Subsequent bond amounts depend on the total number of validators operated by the node operator and follow a specific function known as the “[bond curve](https://operatorportal.lido.fi/modules/community-staking-module#block-2d1c307d95fc4f8ab7c32b7584f795cf)”, which adjusts the bond requirement based on the operator's validator count. + +For the testnet, the values for the bond curve are the following: + +![curve.png](/assets/curve-ea.png) diff --git a/faq/hoodi/main-8.md b/faq/hoodi/main-8.md new file mode 100644 index 00000000..35c06f54 --- /dev/null +++ b/faq/hoodi/main-8.md @@ -0,0 +1,5 @@ +--- +title: How can I get help? +--- + +For community assistance, join the "[CSM-testnet](https://discord.com/channels/761182643269795850/1255114351120089148)" channel on the [Lido Discord server](https://discord.com/invite/lido) to seek advice and guidance. diff --git a/faq/testnet-roles-1.md b/faq/hoodi/roles-1.md similarity index 100% rename from faq/testnet-roles-1.md rename to faq/hoodi/roles-1.md diff --git a/faq/testnet-roles-2.md b/faq/hoodi/roles-2.md similarity index 100% rename from faq/testnet-roles-2.md rename to faq/hoodi/roles-2.md diff --git a/faq/testnet-roles-3.md b/faq/hoodi/roles-3.md similarity index 100% rename from faq/testnet-roles-3.md rename to faq/hoodi/roles-3.md diff --git a/faq/testnet-roles-4.md b/faq/hoodi/roles-4.md similarity index 100% rename from faq/testnet-roles-4.md rename to faq/hoodi/roles-4.md diff --git a/faq/testnet-roles-5.md b/faq/hoodi/roles-5.md similarity index 100% rename from faq/testnet-roles-5.md rename to faq/hoodi/roles-5.md diff --git a/faq/mainnet/bond-1.md b/faq/mainnet/bond-1.md new file mode 100644 index 00000000..f4cc4ef4 --- /dev/null +++ b/faq/mainnet/bond-1.md @@ -0,0 +1,8 @@ +--- +title: What rewards do I get in CSM? +--- + +When CSM operators use the Lido protocol to run validators, they can receive two types of rewards: + +- **Node Operator rewards**: a share of rewards from staker locked stake amount, currently calculated pro-rata based on validators operated as a share of total protocol validators, with [possible reductions for bad performance](https://operatorportal.lido.fi/modules/community-staking-module#block-c6dc8d00f13243fcb17de3fa07ecc52c). +- **Bond rebase**: staking rewards pertaining to the bonded tokens (stETH). diff --git a/faq/bond-2.md b/faq/mainnet/bond-2.md similarity index 100% rename from faq/bond-2.md rename to faq/mainnet/bond-2.md diff --git a/faq/bond-3.md b/faq/mainnet/bond-3.md similarity index 100% rename from faq/bond-3.md rename to faq/mainnet/bond-3.md diff --git a/faq/mainnet/bond-4.md b/faq/mainnet/bond-4.md new file mode 100644 index 00000000..4991f37c --- /dev/null +++ b/faq/mainnet/bond-4.md @@ -0,0 +1,9 @@ +--- +title: Why add a bond? +--- + +Adding bond is a prevention measure to avoid Exit Request for your validators if they became unbonded. [Unbonded validators](https://docs.lido.fi/staking-modules/csm/guides/unbonded-validators) appear if the Node Operator's bond is no longer sufficient to cover all the validator keys uploaded to CSM by the Node Operator. + +If a [penalty](https://operatorportal.lido.fi/modules/community-staking-module#block-3951aa72ba1e471bafe95b40fef65d2b) was already applied, there is a relatively short period of time until the next VEBO report, which most likely will contain a validator Exit Request. During this period in between penalty application and the next VEBO report, Node Operators must top up bond to avoid Exit Requests for their validator(s). + +**Warning:** If the unbonded validator has already been requested to exit, Node Operators can only exit it. The bond top-up after the Exit Request will not reverse the Exit Request. diff --git a/faq/bond-5.md b/faq/mainnet/bond-5.md similarity index 100% rename from faq/bond-5.md rename to faq/mainnet/bond-5.md diff --git a/faq/keys-1.md b/faq/mainnet/keys-1.md similarity index 100% rename from faq/keys-1.md rename to faq/mainnet/keys-1.md diff --git a/faq/mainnet/keys-10.md b/faq/mainnet/keys-10.md new file mode 100644 index 00000000..978d3d64 --- /dev/null +++ b/faq/mainnet/keys-10.md @@ -0,0 +1,10 @@ +--- +title: When does a validator become withdrawn? +anchor: when-validator-become-withdrawn +--- + +On the Ethereum network, a validator can be withdrawn after successfully exiting from the consensus layer, but the exact timing of withdrawal depends on several factors related to Ethereum protocol mechanics: + +1. **Exit Queue**: When a validator initiates an exit, it enters an exit queue. The time required to exit depends on the number of validators exiting and the churn limit (the number of validators allowed to exit or enter per epoch). +2. **Withdrawal Process**: After exiting the active validator set, the validator enters a withdrawable state. This state is determined by the withdrawable epoch, which is set to the exit epoch + a minimum delay of 256 epochs (~27 hours). +3. **Finalization of Withdrawal**: Once the withdrawable epoch is reached, the validator balance will be transferred to the validator's withdrawal credentials (in the case of the Lido protocol, the Lido Withdrawal Vault) within the next iteration of the Consensus Layer withdrawal sweep cycle. How long this takes depends on the validator's position in the overall sweep cycle, and time difference between the withdrawable epoch and when its turn will come to be swept. Once the withdrawal has occurred, the fact of the withdrawal can be reported to CSM permissionlessly. Once that occurs, the part of the Node Operator’s bond used for this validator is released. At this point, the Node Operator can claim the bond on the Bond & Rewards Claim tab. diff --git a/faq/mainnet/keys-11.md b/faq/mainnet/keys-11.md new file mode 100644 index 00000000..88e7c56c --- /dev/null +++ b/faq/mainnet/keys-11.md @@ -0,0 +1,6 @@ +--- +title: What is a referrer? +onlyWithReferrer: true +--- + +A referrer is a software provider specializing in node/validator setup that integrated CSM into their tools. When a referrer directs solo stakers to join CSM via its tool, these Node Operators are marked as being referred from this provider. It doesn’t affect the Node Operators rewards in any way and is used just for the funnel-tracking purposes. diff --git a/faq/keys-12.md b/faq/mainnet/keys-12.md similarity index 100% rename from faq/keys-12.md rename to faq/mainnet/keys-12.md diff --git a/faq/keys-13.md b/faq/mainnet/keys-13.md similarity index 100% rename from faq/keys-13.md rename to faq/mainnet/keys-13.md diff --git a/faq/mainnet/keys-2.md b/faq/mainnet/keys-2.md new file mode 100644 index 00000000..fa478fde --- /dev/null +++ b/faq/mainnet/keys-2.md @@ -0,0 +1,11 @@ +--- +title: Why upload a bond? +--- + +Submitting a bond serves as a risk mitigation measure for both the Ethereum network and the Lido protocol. + +There are several major reasons for a CSM Node Operator's bond to be penalized, including: + +- **The validator has been slashed.** In this case, the initial (minimal) slashing penalty is confiscated. `Penalty amount` = `1 ETH (EFFECTIVE_BALANCE / 32)`; +- **The operator has stolen EL rewards (MEV)**. `Penalty amount` = `amount stolen` + `fixed stealing fine`; +- **The validator's withdrawal balance is less than 32 ETH**. `Penalty amount` = `32` - `validator's withdrawal balance.` diff --git a/faq/keys-3.md b/faq/mainnet/keys-3.md similarity index 100% rename from faq/keys-3.md rename to faq/mainnet/keys-3.md diff --git a/faq/keys-3a.md b/faq/mainnet/keys-3a.md similarity index 100% rename from faq/keys-3a.md rename to faq/mainnet/keys-3a.md diff --git a/faq/keys-4.md b/faq/mainnet/keys-4.md similarity index 100% rename from faq/keys-4.md rename to faq/mainnet/keys-4.md diff --git a/faq/keys-4a.md b/faq/mainnet/keys-4a.md similarity index 100% rename from faq/keys-4a.md rename to faq/mainnet/keys-4a.md diff --git a/faq/mainnet/keys-5.md b/faq/mainnet/keys-5.md new file mode 100644 index 00000000..8539e45a --- /dev/null +++ b/faq/mainnet/keys-5.md @@ -0,0 +1,5 @@ +--- +title: Difference between bond types (ETH, stETH, wstETH)? +--- + +Bonds are stored in the form of stETH to so that participation as a Node Operator is more capital efficient than if the bond were un-staked (or could only be staked if sufficient deposits to fill the submitted validators were present). While node operators can submit bond in ETH, stETH, or wstETH, any token other than stETH is converted to stETH for consistency in bond format. diff --git a/faq/keys-6.md b/faq/mainnet/keys-6.md similarity index 100% rename from faq/keys-6.md rename to faq/mainnet/keys-6.md diff --git a/faq/mainnet/keys-7.md b/faq/mainnet/keys-7.md new file mode 100644 index 00000000..2049a4e0 --- /dev/null +++ b/faq/mainnet/keys-7.md @@ -0,0 +1,5 @@ +--- +title: Why pay for key deletion? +--- + +Key deletion incurs a removal fee of 0.05 ETH, which is deducted from the Node Operator's bond per each deleted key to cover the maximal possible operational costs associated with the queue processing. This fee is intended to protect the module from potential DoS attacks by malicious actors who could clog the queue with empty slots by adding and removing keys, and covers the maximal possible operational costs associated with the queue processing. The fee discourages misuse, keeping the system clear of invalid keys or keys that don’t end up being deposited to. diff --git a/faq/mainnet/keys-8.md b/faq/mainnet/keys-8.md new file mode 100644 index 00000000..79c65c68 --- /dev/null +++ b/faq/mainnet/keys-8.md @@ -0,0 +1,5 @@ +--- +title: Can't see the key for deletion? +--- + +Only keys that have not been deposited yet can be deleted. If a key has already been deposited, the only way to retrieve the bond is [to exit the validator on the Consensus Layer (CL)](https://dvt-homestaker.stakesaurus.com/bonded-validators-setup/lido-csm/exiting-csm-validators). Once withdrawn, the node operator can claim the excess bond. diff --git a/faq/mainnet/keys-9.md b/faq/mainnet/keys-9.md new file mode 100644 index 00000000..c1361d38 --- /dev/null +++ b/faq/mainnet/keys-9.md @@ -0,0 +1,5 @@ +--- +title: How to stop validating in CSM? +--- + +Exiting CSM validator keys works the same way as exiting solo staking validator keys. The guide on how to exit the validator can be found [here](https://dvt-homestaker.stakesaurus.com/bonded-validators-setup/lido-csm/exiting-csm-validators#manual-exit). diff --git a/faq/mainnet/locked-1.md b/faq/mainnet/locked-1.md new file mode 100644 index 00000000..ea31fa37 --- /dev/null +++ b/faq/mainnet/locked-1.md @@ -0,0 +1,5 @@ +--- +title: Why is the bond locked? +--- + +Bond may be locked in the case of delayed penalties, typically for MEV stealing event reported by a dedicated committee. This measure ensures that node operators are held accountable for any misbehavior or rule violations. diff --git a/faq/mainnet/locked-2.md b/faq/mainnet/locked-2.md new file mode 100644 index 00000000..4aeaf545 --- /dev/null +++ b/faq/mainnet/locked-2.md @@ -0,0 +1,7 @@ +--- +title: How to unlock the bond? +--- + +To unlock the bond, the penalty amount, which includes the stolen amount and a fixed stealing fine, must be compensated on the "Locked bond" tab. This action can be performed from the Manager Address of the Node Operator. + +If there are disputes regarding the penalty, node operators can start a public discussion about the penalty applied on the Lido research forum under the [CSM Support](https://research.lido.fi/c/csm-support/21) category. diff --git a/faq/mainnet/locked-3.md b/faq/mainnet/locked-3.md new file mode 100644 index 00000000..d811e26f --- /dev/null +++ b/faq/mainnet/locked-3.md @@ -0,0 +1,5 @@ +--- +title: Consequences of not compensating? +--- + +In case of refusal to compensate the penalty, a protocol rule violation occurs which results in the reset of all node operator benefits. The locked bond is burned to compensate all stETH holders for the rewards stolen. diff --git a/faq/mainnet/main-1.md b/faq/mainnet/main-1.md new file mode 100644 index 00000000..d543ade1 --- /dev/null +++ b/faq/mainnet/main-1.md @@ -0,0 +1,9 @@ +--- +title: Why run an Ethereum validator? +--- + +Running an Ethereum validator allows one to: + +1. **Receive Staking Rewards**: Validators get network rewards for performing their duties on the Ethereum blockchain (note: incorrectly or not performing duties incurs penalties). +2. **Support the Network**: By running a validator, you actively contribute to the decentralization and security of the Ethereum network. Validators play a crucial role in reaching consensus and validating transactions, which enhances the network's reliability and resilience. +3. **Learn and Engage with the community**: Operating a validator node provides valuable insights into blockchain technology and consensus mechanisms. Through hands-on experience, individuals can deepen their understanding of Ethereum's inner workings. Moreover, it provides an opportunity to engage with the Ethereum community, share knowledge, and contribute to the network's development. diff --git a/faq/mainnet/main-2.md b/faq/mainnet/main-2.md new file mode 100644 index 00000000..21c6b4a8 --- /dev/null +++ b/faq/mainnet/main-2.md @@ -0,0 +1,10 @@ +--- +title: What is required to be a Node Operator in CSM? +--- + +Node operation in CSM involves a long-term commitment to Ethereum's decentralization. Responsibilities include: + +- **Hardware Setup**: Setting up a computer that meets the system requirements for running validator nodes. +- **Configuration**: Properly configuring nodes and validators, ensuring they are in sync with the Ethereum blockchain and other network participants. +- **Security Measures**: Implementing robust security measures to safeguard against external threats and internal vulnerabilities. +- **Maintenance**: Sustaining ongoing maintenance throughout the validators' lifespan, which involves monitoring performance, troubleshooting issues, and applying necessary updates. diff --git a/faq/mainnet/main-3.md b/faq/mainnet/main-3.md new file mode 100644 index 00000000..32d58fae --- /dev/null +++ b/faq/mainnet/main-3.md @@ -0,0 +1,8 @@ +--- +title: What do node operators receive in CSM? +--- + +Node operators benefit from: + +- **Daily Bond Rebase**: The collateral for CSM NOs is eligible for [rewards through stETH's rebase](https://help.lido.fi/en/articles/5230610-what-is-steth), even before validator activation. +- **Socialized Rewards**: Rewards are smoothed across the largest validator set, mitigating volatility. This means that even in the event of small outages or disruptions, node operators can still receive rewards, reducing the risk of rewards loss. diff --git a/faq/mainnet/main-4.md b/faq/mainnet/main-4.md new file mode 100644 index 00000000..72d4d148 --- /dev/null +++ b/faq/mainnet/main-4.md @@ -0,0 +1,12 @@ +--- +title: What are the risks of running a validator? +--- + +Node operators face several risks, including: + +1. **Technical Risk**: Maintaining reliable and secure hardware and software setups is essential. Any technical failure or vulnerability in the validator setup could lead to penalties. +2. **Penalties for Misbehavior**: Validators can be penalized for various reasons, such as going offline or attempting to manipulate the network. +3. **Market Risk**: The value of ETH can fluctuate significantly, impacting the value of the validators' staked ETH. +4. **Network Risk**: Validators are part of a decentralized network, and the security and stability of the Ethereum network as a whole can affect individual validators. Events such as network attacks or protocol upgrades may impact validator operations, leading to potential disruptions or losses. +5. **Operational Risk**: Validators require ongoing maintenance and monitoring to ensure smooth operation. Any operational issues, such as hardware failures or connectivity issues, could disrupt validator performance and result in rewards losses. +6. **Slashing Risk**: Validators can be slashed, meaning they lose a portion of their staked ETH, for violating network rules or behaving maliciously. Slashing can occur due to actions such as double signing or failing to validate correctly, resulting in significant penalties. diff --git a/faq/mainnet/main-5.md b/faq/mainnet/main-5.md new file mode 100644 index 00000000..68b4053b --- /dev/null +++ b/faq/mainnet/main-5.md @@ -0,0 +1,5 @@ +--- +title: How does CSM work? +--- + +Refer to [the CSM blog post](https://blog.lido.fi/lido-community-staking-an-overview/) for an overview, or [the CSM page](https://operatorportal.lido.fi/modules/community-staking-module) for a more detailed explanation of its mechanics and functionalities. diff --git a/faq/mainnet/main-6.md b/faq/mainnet/main-6.md new file mode 100644 index 00000000..e233beed --- /dev/null +++ b/faq/mainnet/main-6.md @@ -0,0 +1,11 @@ +--- +title: What makes CSM unique? +--- + +CSM provides several unique features to attract and benefit community stakers: + +- **Rewards Smoothing**: Rewards, including Execution Layer (EL) rewards and MEV, are smoothed with other modules to provide more stable rewards comparable to operating validators solo. +- **Low Bond Requirement**: A low bond for node operators makes participation more accessible to a broader range of prospective operators. +- **Exclusive Use of ETH (stETH)**: CSM exclusively uses ETH ((w)stETH) for bond and rewards, eliminating the need for other assets and simplifying the process for node operators. The bond can be submitted as ETH, wstETH, or stETH, and both bond and rewards can be withdrawn in any of the three forms (withdrawals in the form of ETH follow the [normal Lido on Ethereum unstaking process](https://help.lido.fi/en/articles/7858323-how-do-i-unstake-my-steth)). +- **Enhanced User Experience**: Accessible through a multitude of options -- from a web UI to integrations with Dappnode, Stereum, Eth-Docker, Sedge, Stereum, CoinPillar, etc., CSM offers a leading user-friendly experience, with reduced gas fees for on-chain operations and simplified transactions for joining and claiming rewards. +- **Higher Reward Potential**: Node operators are potentially able to earn more rewards compared to vanilla solo staking, making CSM an attractive option for operators looking to run more validators to earn rewards. diff --git a/faq/main-7.md b/faq/mainnet/main-7.md similarity index 100% rename from faq/main-7.md rename to faq/mainnet/main-7.md diff --git a/faq/main-7a.md b/faq/mainnet/main-7a.md similarity index 100% rename from faq/main-7a.md rename to faq/mainnet/main-7a.md diff --git a/faq/main-8.md b/faq/mainnet/main-8.md similarity index 100% rename from faq/main-8.md rename to faq/mainnet/main-8.md diff --git a/faq/mainnet/roles-1.md b/faq/mainnet/roles-1.md new file mode 100644 index 00000000..430183f7 --- /dev/null +++ b/faq/mainnet/roles-1.md @@ -0,0 +1,23 @@ +--- +title: What are rewards and Manager Addresses? +--- + +There are two addresses associated with your Node Operator that have different scope of responsibilities for managing your Node Operator. + +The Rewards Address is used for: + +- Claiming bond and rewards +- Adding extra bond amount +- Proposing a new Rewards Address +- Resetting the Manager Address to the current Rewards Address + +The Manager Address is used for: + +- Adding new keys +- Removing existing keys +- Adding extra bond amount +- Claiming bond and rewards to the Rewards Address +- Covering locked bond +- Proposing a new Manager Address + +Read more about addresses management [here](https://operatorportal.lido.fi/modules/community-staking-module#block-d3ad2b2bd3994a06b19dccc0794ac8d6). diff --git a/faq/mainnet/roles-2.md b/faq/mainnet/roles-2.md new file mode 100644 index 00000000..348752d1 --- /dev/null +++ b/faq/mainnet/roles-2.md @@ -0,0 +1,7 @@ +--- +title: Why should these addresses be different? +--- + +It's recommended to use different addresses for security reasons. For example, a hot wallet may be used for the Manager Address to simplify daily operations, while a cold wallet (or something like a Safe) is preferable for the Rewards Address to enhance security. + +Read more about addresses management [here](https://operatorportal.lido.fi/modules/community-staking-module#block-d3ad2b2bd3994a06b19dccc0794ac8d6). diff --git a/faq/mainnet/roles-3.md b/faq/mainnet/roles-3.md new file mode 100644 index 00000000..893ec291 --- /dev/null +++ b/faq/mainnet/roles-3.md @@ -0,0 +1,5 @@ +--- +title: How to accept a change in address request? +--- + +To accept a change in address request, connect to the CSM widget with the new address, navigate to the "Roles" → "Inbox requests" tab, select and accept the request, and confirm the transaction in your wallet. Changes are made once the transaction is processed. diff --git a/faq/mainnet/roles-4.md b/faq/mainnet/roles-4.md new file mode 100644 index 00000000..1734995e --- /dev/null +++ b/faq/mainnet/roles-4.md @@ -0,0 +1,5 @@ +--- +title: What to do if the change is submitted to a wrong address? +--- + +If a role change was submitted to the wrong address, the change can be revoked. For Rewards Address changes, navigate to "Roles" → "Rewards Address" → "Revoke". For Manager Address changes, go to "Roles" → "Manager Address" → "Revoke" diff --git a/faq/mainnet/roles-5.md b/faq/mainnet/roles-5.md new file mode 100644 index 00000000..90b335a5 --- /dev/null +++ b/faq/mainnet/roles-5.md @@ -0,0 +1,5 @@ +--- +title: What happens to rewards after changing the Rewards Address? +--- + +Rewards claimed to the previous Rewards Address remain there. After changing the Rewards Address, all rewards and excess bond accumulated on the bond balance can be claimed to the new Rewards Address. In the event of validator withdrawal, upon claiming of the bond, it would also be returned to the new Rewards Address. diff --git a/lib/getFaq.ts b/lib/getFaq.ts index c31c3a3e..ec8a3bb9 100644 --- a/lib/getFaq.ts +++ b/lib/getFaq.ts @@ -1,9 +1,10 @@ +/* eslint-disable no-console */ import matter from 'gray-matter'; import remark from 'remark'; import html from 'remark-html'; import externalLinks from 'remark-external-links'; import { getConfig } from 'config'; -import { CHAINS } from 'consts/chains'; +import { CHAINS } from '@lido-sdk/constants'; export type FaqItem = { id: string; @@ -16,8 +17,8 @@ export type FaqItem = { export type FaqGetter = () => Promise; -const readFaqFile = async (id: string): Promise => { - const fileContents = await import(`faq/${id}.md`); +const readFaqFile = async ([scope, id]: string[]): Promise => { + const fileContents = await import(`faq/${scope}/${id}.md`); const matterResult = matter(fileContents.default); const processedContent = await remark() @@ -36,13 +37,10 @@ const readFaqFile = async (id: string): Promise => { }; const { defaultChain } = getConfig(); -const isMainnet = defaultChain === CHAINS.Mainnet; +const chainName = CHAINS[defaultChain].toLowerCase(); export const readFaqFiles = async (fileNames: string[]) => { - const ids = isMainnet - ? fileNames - : fileNames.map((name) => `testnet-${name}`); - + const ids = fileNames.map((name) => [chainName, name]); return Promise.all(ids.map(readFaqFile)); }; From efd38c1ef4d8d1f48393ed4942b8ceab38582075 Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 18 Mar 2025 16:45:22 +0300 Subject: [PATCH 12/42] chore: blockscout explorer --- .../etherscan-address-link.tsx | 31 +++++++++---------- .../tx-link-etherscan/tx-link-etherscan.tsx | 13 +++++--- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/shared/components/external-icon-link/etherscan-address-link.tsx b/shared/components/external-icon-link/etherscan-address-link.tsx index e5d68dee..87665d93 100644 --- a/shared/components/external-icon-link/etherscan-address-link.tsx +++ b/shared/components/external-icon-link/etherscan-address-link.tsx @@ -10,26 +10,25 @@ type Props = { address: string; }; -export const EtherscanAddressLink: FC = ({ address }) => { - const { chainId } = useAccount(); - +const getExplorerAddressLink = (chainId: number, address: string) => { if (chainId === CHAINS.Hoodi) { - return null; + return `https://hoodi.cloud.blockscout.com/address/${address}`; } + return getEtherscanAddressLink(chainId, address); +}; + +export const EtherscanAddressLink: FC = ({ address }) => { + const { chainId } = useAccount(); - const href = getEtherscanAddressLink(chainId ?? 0, address); + if (!address) return null; return ( - <> - {href && ( - - - - )} - + + + ); }; diff --git a/shared/components/tx-link-etherscan/tx-link-etherscan.tsx b/shared/components/tx-link-etherscan/tx-link-etherscan.tsx index 354eb9c0..4f1a21cb 100644 --- a/shared/components/tx-link-etherscan/tx-link-etherscan.tsx +++ b/shared/components/tx-link-etherscan/tx-link-etherscan.tsx @@ -10,6 +10,13 @@ type TxLinkEtherscanProps = { txHash?: string | null; }; +const getExplorerTxLink = (chainId: number, hash: string) => { + if (chainId === CHAINS.Hoodi) { + return `https://hoodi.cloud.blockscout.com/tx/${hash}`; + } + return getEtherscanTxLink(chainId, hash); +}; + export const TxLinkEtherscan: FC = ({ txHash, text = 'View on Etherscan', @@ -18,13 +25,9 @@ export const TxLinkEtherscan: FC = ({ if (!txHash) return null; - if (chainId === CHAINS.Hoodi) { - return null; - } - return ( {text} From c9a611a7140b74cb24628e3c3e690abdb7fca7e1 Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 18 Mar 2025 17:01:05 +0300 Subject: [PATCH 13/42] fix: hoodi CL api --- config/rpc/cl.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/config/rpc/cl.ts b/config/rpc/cl.ts index 140c4fe9..14f0e210 100644 --- a/config/rpc/cl.ts +++ b/config/rpc/cl.ts @@ -15,7 +15,6 @@ import { config } from '../get-config'; import { useUserConfig } from '../user-config'; export const getBackendApiPath = (chainId: string | number): string => { - if (chainId === CHAINS.Hoodi) return ''; const BASE_URL = typeof window === 'undefined' ? '' : window.location.origin; return `${BASE_URL}/${API_ROUTES.CL}/${chainId}`; }; From 6acaefbdefe59cb165fa8a1d31b4f3f17ae4231f Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 18 Mar 2025 18:12:05 +0300 Subject: [PATCH 14/42] fix: deposit queue --- .../view-keys/deposit-queue/use-deposit-queue-graph.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/features/view-keys/deposit-queue/use-deposit-queue-graph.ts b/features/view-keys/deposit-queue/use-deposit-queue-graph.ts index 9f36373d..d180d644 100644 --- a/features/view-keys/deposit-queue/use-deposit-queue-graph.ts +++ b/features/view-keys/deposit-queue/use-deposit-queue-graph.ts @@ -7,7 +7,7 @@ import { DepositDataInputType } from 'shared/hook-form/form-controller'; import { useCSMShareLimitInfo, useNodeOperatorInfo } from 'shared/hooks'; import { useCSMQueueBatches } from 'shared/hooks/useCSMQueueBatches'; -const POTENTIAL_ADDED = BigNumber.from(100); +const POTENTIAL_ADDED = BigNumber.from(25); const BACK = BigNumber.from(30); type Pos = { size: number; offset: number }; @@ -41,7 +41,7 @@ export const useDepositQueueGraph = (fullView = false) => { const isSubmitting = submitting !== undefined; const potential = added.lt(POTENTIAL_ADDED) ? POTENTIAL_ADDED : added; - const m0 = active.isZero() ? queue : active; + const m0 = active.lt(BACK) ? Zero : active; const m1 = m0.sub(BACK).isNegative() ? m0 : m0.sub(BACK); const m2 = active.add(queue).add(potential); const md = m2.sub(m1); @@ -70,7 +70,11 @@ export const useDepositQueueGraph = (fullView = false) => { return value.isZero() ? 0 : Math.max(minSize, cc(value.add(prev)) - p); }; - const queueUnderLimit = queue.lt(activeLeft) ? queue : activeLeft; + const queueUnderLimit = activeLeft.gt(Zero) + ? queue.lt(activeLeft) + ? queue + : activeLeft + : Zero; const queueOverLimit = queue.sub(queueUnderLimit); const activeSize = ccc(active); From 3d170bcb2bff55180f8c869ce779a129e1f18529 Mon Sep 17 00:00:00 2001 From: exromany Date: Wed, 19 Mar 2025 15:52:01 +0300 Subject: [PATCH 15/42] fix: key in activation pending status comment --- shared/components/status-comment/comments.tsx | 14 ++++++++++++++ .../components/status-comment/status-comment.tsx | 10 +++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/shared/components/status-comment/comments.tsx b/shared/components/status-comment/comments.tsx index 1429aeb5..b725cc60 100644 --- a/shared/components/status-comment/comments.tsx +++ b/shared/components/status-comment/comments.tsx @@ -68,6 +68,20 @@ export const CommentExiting: FC = () => ( ); +export const CommentActivationPending: FC = () => { + return ( + + When does the validator become active? + + ); +}; + export const CommentDepositable: FC = () => { const { data } = useCSMShareLimitInfo(); diff --git a/shared/components/status-comment/status-comment.tsx b/shared/components/status-comment/status-comment.tsx index 8e9705f7..5c5bbb39 100644 --- a/shared/components/status-comment/status-comment.tsx +++ b/shared/components/status-comment/status-comment.tsx @@ -1,6 +1,7 @@ import { FC } from 'react'; import { KEY_STATUS } from 'consts/key-status'; import { + CommentActivationPending, CommentDepositable, CommentExiting, CommentExitRequested, @@ -48,11 +49,10 @@ export const StatusComment: FC<{ statuses: KEY_STATUS[] }> = ({ statuses }) => { ) return ; - if ( - statuses.includes(KEY_STATUS.DEPOSITABLE) || - statuses.includes(KEY_STATUS.ACTIVATION_PENDING) - ) - return ; + if (statuses.includes(KEY_STATUS.ACTIVATION_PENDING)) + return ; + + if (statuses.includes(KEY_STATUS.DEPOSITABLE)) return ; return null; }; From ceaeb318cb544fd718a8a60470bbe84768b44ed5 Mon Sep 17 00:00:00 2001 From: exromany Date: Wed, 19 Mar 2025 15:52:22 +0300 Subject: [PATCH 16/42] chore: disable external dashboards for hoodi --- features/dashboard/dashboard.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/features/dashboard/dashboard.tsx b/features/dashboard/dashboard.tsx index 411a5d94..ee7910f8 100644 --- a/features/dashboard/dashboard.tsx +++ b/features/dashboard/dashboard.tsx @@ -3,6 +3,10 @@ import { BondSection } from './bond'; import { KeysSection } from './keys'; import { RolesSection } from './roles'; import { ExternalSection } from './external'; +import { getConfig } from 'config'; +import { CHAINS } from 'consts/chains'; + +const { defaultChain } = getConfig(); export const Dashboard: FC = () => { return ( @@ -10,7 +14,7 @@ export const Dashboard: FC = () => { - + {defaultChain !== CHAINS.Hoodi && } ); }; From 9a518c3e834a6bf8cb1b1778fd34b62a374c76a9 Mon Sep 17 00:00:00 2001 From: exromany Date: Wed, 19 Mar 2025 17:37:22 +0300 Subject: [PATCH 17/42] chore: links --- consts/external-links.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/consts/external-links.ts b/consts/external-links.ts index 6316369e..e874c728 100644 --- a/consts/external-links.ts +++ b/consts/external-links.ts @@ -85,15 +85,13 @@ export const EXTERNAL_LINKS_BY_NETWORK: Record = [CHAINS.Hoodi]: { earlyAdoptionTree: '', rewardsTree: '', - earlyAdoptionSources: - 'https://github.com/lidofinance/community-staking-module/blob/v1.0/artifacts/holesky/early-adoption/addresses.json', - earlyAdoptionAbout: - 'https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22', + earlyAdoptionSources: '', + earlyAdoptionAbout: '', feedbackForm: 'https://forms.gle/ZBUqbykaZokJLf4M7', - stakeWidget: 'https://stake-holesky.testnet.fi', + stakeWidget: 'https://stake-hoodi.testnet.fi', - feesMonitoring: 'https://fees-monitoring-holesky.testnet.fi', - operatorsWidget: 'https://operators-holesky.testnet.fi', + feesMonitoring: 'https://fees-monitoring-hoodi.testnet.fi', + operatorsWidget: 'https://operators-hoodi.testnet.fi', beaconchain: 'https://holesky.beaconcha.in', beaconchainDashboard: 'https://v2-beta-holesky.beaconcha.in/dashboard', ratedExplorer: 'https://explorer.rated.network', From bc6d3d875e0f8b3d71e215e9e915ad3516cc1fae Mon Sep 17 00:00:00 2001 From: exromany Date: Wed, 19 Mar 2025 18:09:51 +0300 Subject: [PATCH 18/42] chore: faq hoodi with correct curve image --- faq/hoodi/keys-3.md | 8 ++++---- faq/hoodi/keys-3a.md | 8 ++++---- faq/hoodi/keys-4.md | 4 ++-- faq/hoodi/keys-4a.md | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/faq/hoodi/keys-3.md b/faq/hoodi/keys-3.md index a4c6e0db..dd1013fa 100644 --- a/faq/hoodi/keys-3.md +++ b/faq/hoodi/keys-3.md @@ -4,10 +4,10 @@ earlyAdoption: false anchor: how-bond-is-calculated --- -The initial bond requirement for the first validator for the testnet is 2 stETH. However, for [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), this amount is reduced to 1.5 stETH to incentivize early participation. +The initial bond requirement for the first validator for the Hoodi testnet is 2.4 stETH. However, for [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), this amount is reduced to 1.5 stETH to incentivize early participation. -Subsequent bond amounts depend on the total number of validators operated by the node operator and follow a specific function known as the “[bond curve](https://operatorportal.lido.fi/modules/community-staking-module#block-2d1c307d95fc4f8ab7c32b7584f795cf)”, which adjusts the bond requirement based on the operator's validator count. +The amount for the second and subsequent validators is 1.3 stETH -For the testnet, the values for the bond curve are the following: +For the Hoodi testnet, the values for the bond curve are the following: -![curve.png](/assets/curve-common.png) +![curve.png](/assets/mainnet-curve-common.png) diff --git a/faq/hoodi/keys-3a.md b/faq/hoodi/keys-3a.md index f1ad7a06..5c7af749 100644 --- a/faq/hoodi/keys-3a.md +++ b/faq/hoodi/keys-3a.md @@ -4,10 +4,10 @@ earlyAdoption: true anchor: how-bond-is-calculated --- -The initial bond requirement for the first validator for the testnet is 2 stETH. However, for [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), this amount is reduced to 1.5 stETH to incentivize early participation. +The initial bond requirement for the first validator for the Hoodi testnet is 2.4 stETH. However, for [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), this amount is reduced to 1.5 stETH to incentivize early participation. -Subsequent bond amounts depend on the total number of validators operated by the node operator and follow a specific function known as the “[bond curve](https://operatorportal.lido.fi/modules/community-staking-module#block-2d1c307d95fc4f8ab7c32b7584f795cf)”, which adjusts the bond requirement based on the operator's validator count. +The amount for the second and subsequent validators is 1.3 stETH -For the testnet, the values for the bond curve are the following: +For the Hoodi testnet, the values for the bond curve are the following: -![curve.png](/assets/curve-ea.png) +![curve.png](/assets/mainnet-curve-ea.png) diff --git a/faq/hoodi/keys-4.md b/faq/hoodi/keys-4.md index 731c6ded..663b9fd4 100644 --- a/faq/hoodi/keys-4.md +++ b/faq/hoodi/keys-4.md @@ -5,6 +5,6 @@ earlyAdoption: false [The bond curve](https://operatorportal.lido.fi/modules/community-staking-module#block-2d1c307d95fc4f8ab7c32b7584f795cf) is a function that determines the amount of bond required for each subsequent validator operated by the node operator. For [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), a unique bond curve function is applied to incentivize early participation. -For the testnet, the values for the bond curve are the following: +For the Hoodi testnet, the values for the bond curve are the following: -![curve.png](/assets/curve-common.png) +![curve.png](/assets/mainnet-curve-common.png) diff --git a/faq/hoodi/keys-4a.md b/faq/hoodi/keys-4a.md index 5b43be9b..810697a1 100644 --- a/faq/hoodi/keys-4a.md +++ b/faq/hoodi/keys-4a.md @@ -5,6 +5,6 @@ earlyAdoption: true [The bond curve](https://operatorportal.lido.fi/modules/community-staking-module#block-2d1c307d95fc4f8ab7c32b7584f795cf) is a function that determines the amount of bond required for each subsequent validator operated by the node operator. For [Early Adopters](https://operatorportal.lido.fi/modules/community-staking-module#block-ef60a1fa96ae4c7995dd7794de2a3e22), a unique bond curve function is applied to incentivize early participation. -For the testnet, the values for the bond curve are the following: +For the Hoodi testnet, the values for the bond curve are the following: -![curve.png](/assets/curve-ea.png) +![curve.png](/assets/mainnet-curve-ea.png) From d941be3c4e8952d319ec976afe43a5e6ab281f07 Mon Sep 17 00:00:00 2001 From: exromany Date: Wed, 19 Mar 2025 19:20:14 +0300 Subject: [PATCH 19/42] fix: exitbus contract address --- consts/csm-constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consts/csm-constants.ts b/consts/csm-constants.ts index 0fe6f66b..0bf66769 100644 --- a/consts/csm-constants.ts +++ b/consts/csm-constants.ts @@ -51,7 +51,7 @@ export const CONSTANTS_BY_NETWORK: Record = { CSFeeOracle: '0xe7314f561B2e72f9543F1004e741bab6Fc51028B', CSModule: '0x79CEf36D84743222f37765204Bec41E92a93E59d', CSVerifier: '0x16D0f6068D211608e3703323314aa976a6492D09', - ExitBusOracle: '0x30308CD8844fb2DB3ec4D056F1d475a802DCA07c', + ExitBusOracle: '0x8664d394C2B3278F26A1B44B967aEf99707eeAB2', StakingRouter: '0xCc820558B39ee15C7C45B59390B503b83fb499A8', }, deploymentBlockNumber: undefined, From afccf200dd9b1db9a60ca0b41fe4a1d0ace5b0e4 Mon Sep 17 00:00:00 2001 From: exromany Date: Fri, 21 Mar 2025 13:34:22 +0300 Subject: [PATCH 20/42] chore: hoodi rawards frame = 1d --- consts/csm-constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consts/csm-constants.ts b/consts/csm-constants.ts index 0bf66769..d9955e6c 100644 --- a/consts/csm-constants.ts +++ b/consts/csm-constants.ts @@ -58,7 +58,7 @@ export const CONSTANTS_BY_NETWORK: Record = { stakingModuleId: 4, withdrawalCredentials: '0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2', retentionPeriodMins: 80_640, // 8 weeks - slotsPerFrame: 32 * 225 * 7, // 7 days + slotsPerFrame: 32 * 225 * 1, // 1 days }, [CHAINS.Holesky]: { contracts: { From 66ed691a0994562e9d2ac0e95b7adaa09d84af73 Mon Sep 17 00:00:00 2001 From: exromany Date: Mon, 24 Mar 2025 16:54:53 +0700 Subject: [PATCH 21/42] chore: refactor rewards frame dates --- abi/HashConsensus.json | 1141 +++++++++++++++++ consts/csm-constants.ts | 4 + .../dashboard/bond/available-to-claim.tsx | 9 +- features/dashboard/bond/last-rewards.tsx | 31 +- shared/hooks/index.ts | 3 +- shared/hooks/useCsmContracts.ts | 8 + ...ewardsFrame.ts => useLastRewardsReport.ts} | 38 - shared/hooks/useRewardsFrame.ts | 62 + utils/format-date.ts | 7 +- utilsApi/contractAddressesMetricsMap.ts | 8 + 10 files changed, 1242 insertions(+), 69 deletions(-) create mode 100644 abi/HashConsensus.json rename shared/hooks/{useLastRewardsFrame.ts => useLastRewardsReport.ts} (78%) create mode 100644 shared/hooks/useRewardsFrame.ts diff --git a/abi/HashConsensus.json b/abi/HashConsensus.json new file mode 100644 index 00000000..00f5a7a8 --- /dev/null +++ b/abi/HashConsensus.json @@ -0,0 +1,1141 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "slotsPerEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "secondsPerSlot", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "genesisTime", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "epochsPerFrame", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "fastLaneLengthSlots", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "admin", + "type": "address", + "internalType": "address" + }, + { + "name": "reportProcessor", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "DEFAULT_ADMIN_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DISABLE_CONSENSUS_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MANAGE_FAST_LANE_CONFIG_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MANAGE_FRAME_CONFIG_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MANAGE_MEMBERS_AND_QUORUM_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MANAGE_REPORT_PROCESSOR_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "addMember", + "inputs": [ + { + "name": "addr", + "type": "address", + "internalType": "address" + }, + { + "name": "quorum", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "disableConsensus", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getChainConfig", + "inputs": [], + "outputs": [ + { + "name": "slotsPerEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "secondsPerSlot", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "genesisTime", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getConsensusState", + "inputs": [], + "outputs": [ + { + "name": "refSlot", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "consensusReport", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "isReportProcessing", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getConsensusStateForMember", + "inputs": [ + { + "name": "addr", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "result", + "type": "tuple", + "internalType": "struct HashConsensus.MemberConsensusState", + "components": [ + { + "name": "currentFrameRefSlot", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "currentFrameConsensusReport", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "isMember", + "type": "bool", + "internalType": "bool" + }, + { + "name": "isFastLane", + "type": "bool", + "internalType": "bool" + }, + { + "name": "canReport", + "type": "bool", + "internalType": "bool" + }, + { + "name": "lastMemberReportRefSlot", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "currentFrameMemberReport", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getCurrentFrame", + "inputs": [], + "outputs": [ + { + "name": "refSlot", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "reportProcessingDeadlineSlot", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getFastLaneMembers", + "inputs": [], + "outputs": [ + { + "name": "addresses", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "lastReportedRefSlots", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getFrameConfig", + "inputs": [], + "outputs": [ + { + "name": "initialEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "epochsPerFrame", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "fastLaneLengthSlots", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getInitialRefSlot", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getIsFastLaneMember", + "inputs": [ + { + "name": "addr", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getIsMember", + "inputs": [ + { + "name": "addr", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getMembers", + "inputs": [], + "outputs": [ + { + "name": "addresses", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "lastReportedRefSlots", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getQuorum", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getReportProcessor", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getReportVariants", + "inputs": [], + "outputs": [ + { + "name": "variants", + "type": "bytes32[]", + "internalType": "bytes32[]" + }, + { + "name": "support", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRoleAdmin", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRoleMember", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRoleMemberCount", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "grantRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "hasRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "removeMember", + "inputs": [ + { + "name": "addr", + "type": "address", + "internalType": "address" + }, + { + "name": "quorum", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "callerConfirmation", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setFastLaneLengthSlots", + "inputs": [ + { + "name": "fastLaneLengthSlots", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setFrameConfig", + "inputs": [ + { + "name": "epochsPerFrame", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "fastLaneLengthSlots", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setQuorum", + "inputs": [ + { + "name": "quorum", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setReportProcessor", + "inputs": [ + { + "name": "newProcessor", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "submitReport", + "inputs": [ + { + "name": "slot", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "report", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "consensusVersion", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "updateInitialEpoch", + "inputs": [ + { + "name": "initialEpoch", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "ConsensusLost", + "inputs": [ + { + "name": "refSlot", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ConsensusReached", + "inputs": [ + { + "name": "refSlot", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "report", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + }, + { + "name": "support", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FastLaneConfigSet", + "inputs": [ + { + "name": "fastLaneLengthSlots", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FrameConfigSet", + "inputs": [ + { + "name": "newInitialEpoch", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newEpochsPerFrame", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MemberAdded", + "inputs": [ + { + "name": "addr", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newTotalMembers", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newQuorum", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MemberRemoved", + "inputs": [ + { + "name": "addr", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newTotalMembers", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newQuorum", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "QuorumSet", + "inputs": [ + { + "name": "newQuorum", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "totalMembers", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "prevQuorum", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ReportProcessorSet", + "inputs": [ + { + "name": "processor", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "prevProcessor", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ReportReceived", + "inputs": [ + { + "name": "refSlot", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "member", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "report", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleAdminChanged", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "previousAdminRole", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "newAdminRole", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleGranted", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleRevoked", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AccessControlBadConfirmation", + "inputs": [] + }, + { + "type": "error", + "name": "AccessControlUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "neededRole", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "AddressCannotBeZero", + "inputs": [] + }, + { + "type": "error", + "name": "AdminCannotBeZero", + "inputs": [] + }, + { + "type": "error", + "name": "ConsensusReportAlreadyProcessing", + "inputs": [] + }, + { + "type": "error", + "name": "DuplicateMember", + "inputs": [] + }, + { + "type": "error", + "name": "DuplicateReport", + "inputs": [] + }, + { + "type": "error", + "name": "EmptyReport", + "inputs": [] + }, + { + "type": "error", + "name": "EpochsPerFrameCannotBeZero", + "inputs": [] + }, + { + "type": "error", + "name": "FastLanePeriodCannotBeLongerThanFrame", + "inputs": [] + }, + { + "type": "error", + "name": "InitialEpochAlreadyArrived", + "inputs": [] + }, + { + "type": "error", + "name": "InitialEpochIsYetToArrive", + "inputs": [] + }, + { + "type": "error", + "name": "InitialEpochRefSlotCannotBeEarlierThanProcessingSlot", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidChainConfig", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidSlot", + "inputs": [] + }, + { + "type": "error", + "name": "NewProcessorCannotBeTheSame", + "inputs": [] + }, + { + "type": "error", + "name": "NonFastLaneMemberCannotReportWithinFastLaneInterval", + "inputs": [] + }, + { + "type": "error", + "name": "NonMember", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "NumericOverflow", + "inputs": [] + }, + { + "type": "error", + "name": "QuorumTooSmall", + "inputs": [ + { + "name": "minQuorum", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receivedQuorum", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ReportProcessorCannotBeZero", + "inputs": [] + }, + { + "type": "error", + "name": "SafeCastOverflowedUintDowncast", + "inputs": [ + { + "name": "bits", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "StaleReport", + "inputs": [] + }, + { + "type": "error", + "name": "UnexpectedConsensusVersion", + "inputs": [ + { + "name": "expected", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "received", + "type": "uint256", + "internalType": "uint256" + } + ] + } +] diff --git a/consts/csm-constants.ts b/consts/csm-constants.ts index d9955e6c..54b7145b 100644 --- a/consts/csm-constants.ts +++ b/consts/csm-constants.ts @@ -13,6 +13,7 @@ type CsmContract = | 'CSFeeOracle' | 'CSModule' | 'CSVerifier' + | 'HashConsensus' | 'ExitBusOracle' | 'StakingRouter'; @@ -34,6 +35,7 @@ export const CONSTANTS_BY_NETWORK: Record = { CSFeeOracle: '0x4D4074628678Bd302921c20573EEa1ed38DdF7FB', CSModule: '0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F', CSVerifier: '0x3Dfc50f22aCA652a0a6F28a0F892ab62074b5583', + HashConsensus: '0x71093efF8D8599b5fA340D665Ad60fA7C80688e4', ExitBusOracle: '0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e', StakingRouter: '0xFdDf38947aFB03C621C71b06C9C70bce73f12999', }, @@ -51,6 +53,7 @@ export const CONSTANTS_BY_NETWORK: Record = { CSFeeOracle: '0xe7314f561B2e72f9543F1004e741bab6Fc51028B', CSModule: '0x79CEf36D84743222f37765204Bec41E92a93E59d', CSVerifier: '0x16D0f6068D211608e3703323314aa976a6492D09', + HashConsensus: '0x54f74a10e4397dDeF85C4854d9dfcA129D72C637', ExitBusOracle: '0x8664d394C2B3278F26A1B44B967aEf99707eeAB2', StakingRouter: '0xCc820558B39ee15C7C45B59390B503b83fb499A8', }, @@ -68,6 +71,7 @@ export const CONSTANTS_BY_NETWORK: Record = { CSFeeOracle: '0xaF57326C7d513085051b50912D51809ECC5d98Ee', CSModule: '0x4562c3e63c2e586cD1651B958C22F88135aCAd4f', CSVerifier: '0x6DcA479178E6Ae41CCEB72a88FfDaa3e10E83CB7', + HashConsensus: '0xbF38618Ea09B503c1dED867156A0ea276Ca1AE37', ExitBusOracle: '0xffDDF7025410412deaa05E3E1cE68FE53208afcb', StakingRouter: '0xd6EbF043D30A7fe46D1Db32BA90a0A51207FE229', }, diff --git a/features/dashboard/bond/available-to-claim.tsx b/features/dashboard/bond/available-to-claim.tsx index f6beb05d..fd80c9c2 100644 --- a/features/dashboard/bond/available-to-claim.tsx +++ b/features/dashboard/bond/available-to-claim.tsx @@ -4,10 +4,9 @@ import { useNodeOperatorId } from 'providers/node-operator-provider'; import { FC } from 'react'; import { Counter, IconTooltip } from 'shared/components'; import { - getNextRewardsFrame, - useLastRewardsSlot, useNodeOperatorBalance, useNodeOperatorRewards, + useRewardsFrame, } from 'shared/hooks'; import { useAvailableToClaim } from 'shared/hooks/useAvailableToClaim'; import { formatDate } from 'utils'; @@ -23,10 +22,8 @@ export const AvailableToClaim: FC = () => { const { data: rewards, initialLoading: isRewardsLoading } = useNodeOperatorRewards(id); - const { data: rewardsSlot } = useLastRewardsSlot(); - const nextRewardsDate = rewardsSlot?.timestamp - ? formatDate(getNextRewardsFrame(rewardsSlot.timestamp)) - : null; + const { data: rewardsFrame } = useRewardsFrame(); + const nextRewardsDate = formatDate(rewardsFrame?.nextRewards); const availableToClaim = useAvailableToClaim({ bond, diff --git a/features/dashboard/bond/last-rewards.tsx b/features/dashboard/bond/last-rewards.tsx index ca4d35ea..f57fd425 100644 --- a/features/dashboard/bond/last-rewards.tsx +++ b/features/dashboard/bond/last-rewards.tsx @@ -6,21 +6,18 @@ import { Text, Tooltip, } from '@lidofinance/lido-ui'; -import { differenceInCalendarDays, fromUnixTime } from 'date-fns'; import { ModalComponentType, useModal } from 'providers/modal-provider'; import { useNodeOperatorId } from 'providers/node-operator-provider'; import { FC, useCallback } from 'react'; import { GrayText, Stack, TextBlock, TxLinkEtherscan } from 'shared/components'; import { FaqElement } from 'shared/components/faq/styles'; import { - getNextRewardsFrame, - getPrevRewardsFrame, useLastOperatorRewards, - useLastRewardsSlot, useLastRewrdsTx, useNodeOperatorInfo, + useRewardsFrame, } from 'shared/hooks'; -import { formatDate, formatPercent } from 'utils'; +import { countDaysLeft, formatDate, formatPercent } from 'utils'; import { Balance } from './balance'; import { AccordionStyle, @@ -38,24 +35,12 @@ export const LastRewards: FC = () => { const id = useNodeOperatorId(); const { data: info } = useNodeOperatorInfo(id); - const { data: rewardsSlot } = useLastRewardsSlot(); + const { data: rewardsFrame } = useRewardsFrame(); - const lastRewardsDate = rewardsSlot?.timestamp - ? formatDate(rewardsSlot.timestamp) - : null; - const prevRewardsDate = rewardsSlot?.timestamp - ? formatDate(getPrevRewardsFrame(rewardsSlot.timestamp)) - : null; - const nextRewardsDate = rewardsSlot?.timestamp - ? formatDate(getNextRewardsFrame(rewardsSlot.timestamp)) - : null; - - const daysLeft = rewardsSlot?.timestamp - ? differenceInCalendarDays( - fromUnixTime(getNextRewardsFrame(rewardsSlot.timestamp)), - new Date(), - ) - : null; + const lastRewardsDate = formatDate(rewardsFrame?.lastRewards); + const prevRewardsDate = formatDate(rewardsFrame?.prevRewards); + const nextRewardsDate = formatDate(rewardsFrame?.nextRewards); + const daysLeft = countDaysLeft(rewardsFrame?.nextRewards); const showThisSection = lastRewards || (info?.totalDepositedKeys ?? 0) > 0; @@ -92,7 +77,7 @@ export const LastRewards: FC = () => { Next rewards distribution - {rewardsSlot?.timestamp ? ( + {lastRewardsDate && nextRewardsDate ? ( Report frame: {lastRewardsDate} — {nextRewardsDate} diff --git a/shared/hooks/index.ts b/shared/hooks/index.ts index b94a509a..cc9a1183 100644 --- a/shared/hooks/index.ts +++ b/shared/hooks/index.ts @@ -60,7 +60,7 @@ export * from './useIsContract'; export * from './useIsMultisig'; export * from './useIsReportStealingRole'; export * from './useKeysCache'; -export * from './useLastRewardsFrame'; +export * from './useLastRewardsReport'; export * from './useMaxGasPrice'; export * from './useMergeSwr'; export * from './useNodeOperatorBalance'; @@ -76,6 +76,7 @@ export * from './useNodeOperatorRewards'; export * from './useNodeOperatorSummary'; export * from './useNodeOperatorUnbondedKeys'; export * from './useNodeOperatorsCount'; +export * from './useRewardsFrame'; export * from './useSearchParams'; export * from './useSessionStorage'; export * from './useStakingLimitInfo'; diff --git a/shared/hooks/useCsmContracts.ts b/shared/hooks/useCsmContracts.ts index 829d1664..668f2248 100644 --- a/shared/hooks/useCsmContracts.ts +++ b/shared/hooks/useCsmContracts.ts @@ -8,6 +8,7 @@ import { CSModule__factory, CSModuleOld__factory, ExitBusOracle__factory, + HashConsensus__factory, StakingRouter__factory, } from 'generated'; @@ -49,6 +50,13 @@ const CSFeeOracle = contractHooksFactory( export const useCSFeeOracleRPC = CSFeeOracle.useContractRPC; +const HashConsesus = contractHooksFactory( + HashConsensus__factory, + getCsmContractAddressGetter('HashConsensus'), +); + +export const useHashConsesusRPC = HashConsesus.useContractRPC; + const CSEarlyAdoption = contractHooksFactory( CSEarlyAdoption__factory, getCsmContractAddressGetter('CSEarlyAdoption'), diff --git a/shared/hooks/useLastRewardsFrame.ts b/shared/hooks/useLastRewardsReport.ts similarity index 78% rename from shared/hooks/useLastRewardsFrame.ts rename to shared/hooks/useLastRewardsReport.ts index 41cc7ba5..0e64e109 100644 --- a/shared/hooks/useLastRewardsFrame.ts +++ b/shared/hooks/useLastRewardsReport.ts @@ -10,44 +10,6 @@ import { useMemo } from 'react'; import { BigNumber } from 'ethers'; import { Zero } from '@ethersproject/constants'; -const SECONDS_PER_SLOT = 12; - -const getRewardsFrameDuration = () => { - const { slotsPerFrame } = getCsmConstants(); - return slotsPerFrame * SECONDS_PER_SLOT; -}; - -export const getNextRewardsFrame = (timestamp: number) => - timestamp + getRewardsFrameDuration(); - -export const getPrevRewardsFrame = (timestamp: number) => - timestamp - getRewardsFrameDuration(); - -export const useLastRewardsSlot = (config = STRATEGY_CONSTANT) => { - const feeOracle = useCSFeeOracleRPC(); - - return useLidoSWR( - ['fee-oracle-slot'], - async () => { - const [refSlot, genesisTime] = await Promise.all([ - feeOracle.getLastProcessingRefSlot(), - feeOracle.GENESIS_TIME(), - ]); - - if (!refSlot || !genesisTime) { - return null; - } - - const timestamp = genesisTime - .add(refSlot.mul(SECONDS_PER_SLOT)) - .toNumber(); - - return { refSlot, timestamp }; - }, - config, - ); -}; - export type RewardsReport = { blockstamp: { block_hash: HexString; diff --git a/shared/hooks/useRewardsFrame.ts b/shared/hooks/useRewardsFrame.ts new file mode 100644 index 00000000..3f629ee3 --- /dev/null +++ b/shared/hooks/useRewardsFrame.ts @@ -0,0 +1,62 @@ +import { useContractSWR } from '@lido-sdk/react'; +import { STRATEGY_CONSTANT, STRATEGY_IMMUTABLE } from 'consts/swr-strategies'; +import { useMemo } from 'react'; +import { useHashConsesusRPC } from './useCsmContracts'; +import { useMergeSwr } from './useMergeSwr'; + +export const useChainConfig = (config = STRATEGY_IMMUTABLE) => { + return useContractSWR({ + contract: useHashConsesusRPC(), + method: 'getChainConfig', + params: [], + config, + }); +}; + +export const useFrameConfig = (config = STRATEGY_IMMUTABLE) => { + return useContractSWR({ + contract: useHashConsesusRPC(), + method: 'getFrameConfig', + params: [], + config, + }); +}; + +export const useCurrentFrame = (config = STRATEGY_CONSTANT) => { + return useContractSWR({ + contract: useHashConsesusRPC(), + method: 'getCurrentFrame', + params: [], + config, + }); +}; + +export const useRewardsFrame = () => { + const chainConfig = useChainConfig(); + const frameConfig = useFrameConfig(); + const currentFrame = useCurrentFrame(); + + return useMergeSwr( + [chainConfig, currentFrame], + useMemo(() => { + if (!chainConfig.data || !frameConfig.data || !currentFrame.data) + return undefined; + + const timestamp = currentFrame.data.refSlot + .mul(chainConfig.data.secondsPerSlot) + .add(chainConfig.data.genesisTime) + .toNumber(); + + const duration = frameConfig.data.epochsPerFrame + .mul(chainConfig.data.slotsPerEpoch) + .mul(chainConfig.data.secondsPerSlot) + .toNumber(); + + return { + lastRewards: timestamp, + nextRewards: timestamp + duration, + prevRewards: timestamp - duration, + }; + }, [chainConfig.data, frameConfig.data, currentFrame.data]), + ); +}; diff --git a/utils/format-date.ts b/utils/format-date.ts index c8001f23..8b1e09cf 100644 --- a/utils/format-date.ts +++ b/utils/format-date.ts @@ -1,4 +1,9 @@ -import { format, fromUnixTime } from 'date-fns'; +import { differenceInCalendarDays, format, fromUnixTime } from 'date-fns'; export const formatDate = (timestamp?: number) => timestamp ? format(fromUnixTime(timestamp), 'MMM dd') : null; + +export const countDaysLeft = (timestamp?: number) => + timestamp + ? differenceInCalendarDays(fromUnixTime(timestamp), new Date()) + : null; diff --git a/utilsApi/contractAddressesMetricsMap.ts b/utilsApi/contractAddressesMetricsMap.ts index 4f2420f3..688a0ea1 100644 --- a/utilsApi/contractAddressesMetricsMap.ts +++ b/utilsApi/contractAddressesMetricsMap.ts @@ -23,6 +23,7 @@ import { CSFeeOracle__factory, CSModule__factory, ExitBusOracle__factory, + HashConsensus__factory, StakingRouter__factory, } from 'generated'; import { HexString } from 'shared/keys'; @@ -55,6 +56,7 @@ export const CONTRACT_NAMES = { CSAccounting: 'CSAccounting', CSFeeDistributor: 'CSFeeDistributor', CSFeeOracle: 'CSFeeOracle', + HashConsensus: 'HashConsensus', CSEarlyAdoption: 'CSEarlyAdoption', ExitBusOracle: 'ExitBusOracle', StakingRouter: 'StakingRouter', @@ -71,6 +73,7 @@ const CONTRACT_LIST_CALL: CONTRACT_NAMES[] = [ CONTRACT_NAMES.CSEarlyAdoption, CONTRACT_NAMES.CSFeeDistributor, CONTRACT_NAMES.CSFeeOracle, + CONTRACT_NAMES.HashConsensus, CONTRACT_NAMES.ExitBusOracle, CONTRACT_NAMES.StakingRouter, ]; @@ -90,6 +93,7 @@ export const METRIC_CONTRACT_ABIS = { [CONTRACT_NAMES.CSAccounting]: CSAccounting__factory.abi, [CONTRACT_NAMES.CSFeeDistributor]: CSFeeDistributor__factory.abi, [CONTRACT_NAMES.CSFeeOracle]: CSFeeOracle__factory.abi, + [CONTRACT_NAMES.HashConsensus]: HashConsensus__factory.abi, [CONTRACT_NAMES.CSEarlyAdoption]: CSEarlyAdoption__factory.abi, [CONTRACT_NAMES.ExitBusOracle]: ExitBusOracle__factory.abi, [CONTRACT_NAMES.StakingRouter]: StakingRouter__factory.abi, @@ -117,6 +121,10 @@ const METRIC_CONTRACT_ADDRESS_GETTERS = { getCsmContractAddress, 'CSFeeOracle', ), + [CONTRACT_NAMES.HashConsensus]: getAddressGetter( + getCsmContractAddress, + 'HashConsensus', + ), [CONTRACT_NAMES.CSEarlyAdoption]: getAddressGetter( getCsmContractAddress, 'CSEarlyAdoption', From 31c37dbeb8b1ddf35f36600f1a4af6cfbcfd46f2 Mon Sep 17 00:00:00 2001 From: exromany Date: Mon, 24 Mar 2025 17:26:09 +0700 Subject: [PATCH 22/42] chore: hoodi banner --- .../welcome/hoodi-banner/hoodi-banner.tsx | 37 +++++++++++++++++++ features/welcome/hoodi-banner/index.ts | 1 + features/welcome/hoodi-banner/style.ts | 20 ++++++++++ features/welcome/welcome.tsx | 2 + 4 files changed, 60 insertions(+) create mode 100644 features/welcome/hoodi-banner/hoodi-banner.tsx create mode 100644 features/welcome/hoodi-banner/index.ts create mode 100644 features/welcome/hoodi-banner/style.ts diff --git a/features/welcome/hoodi-banner/hoodi-banner.tsx b/features/welcome/hoodi-banner/hoodi-banner.tsx new file mode 100644 index 00000000..5669c781 --- /dev/null +++ b/features/welcome/hoodi-banner/hoodi-banner.tsx @@ -0,0 +1,37 @@ +import { DarkThemeProvider, Text } from '@lidofinance/lido-ui'; +import { FC } from 'react'; +import { MatomoLink } from 'shared/components'; +import { StyledAccordion } from './style'; + +export const HoodiBanner: FC<{ open?: boolean }> = ({ open }) => { + return ( + + + CSM is live on the Hoodi testnet! + + } + > + <> + + All testing activities regarding CSM will now be held on the Hoodi + testnet + + + CSM on Holesky is now deprecated. CSM Ui for Holesky is still + available on{' '} + + csm-holesky.testnet.fi + + + + + + ); +}; diff --git a/features/welcome/hoodi-banner/index.ts b/features/welcome/hoodi-banner/index.ts new file mode 100644 index 00000000..7e8e732c --- /dev/null +++ b/features/welcome/hoodi-banner/index.ts @@ -0,0 +1 @@ +export * from './hoodi-banner'; diff --git a/features/welcome/hoodi-banner/style.ts b/features/welcome/hoodi-banner/style.ts new file mode 100644 index 00000000..a713e513 --- /dev/null +++ b/features/welcome/hoodi-banner/style.ts @@ -0,0 +1,20 @@ +import { Accordion } from '@lidofinance/lido-ui'; +import styled from 'styled-components'; + +export const StyledAccordion = styled(Accordion)` + width: 100%; + + --first-color: #bfdbfe; + --second-color: #ccfbf1; + + background: radial-gradient( + 1435.85% 196.07% at 95.46% -44.7%, + rgba(34, 56, 255, 0.8) 0%, + rgba(235, 0, 255, 0.4) 100% + ), + linear-gradient(102deg, #bae6fd -8.89%, #93c5fd 105.62%); + + > div + div p { + margin-bottom: 0.5em; + } +`; diff --git a/features/welcome/welcome.tsx b/features/welcome/welcome.tsx index 5acdd433..25a9bb69 100644 --- a/features/welcome/welcome.tsx +++ b/features/welcome/welcome.tsx @@ -11,6 +11,7 @@ import styled from 'styled-components'; import { HoleskyBanner } from './holesky-banner'; import { getConfig } from 'config'; import { CHAINS } from 'consts/chains'; +import { HoodiBanner } from './hoodi-banner'; const { defaultChain } = getConfig(); @@ -24,6 +25,7 @@ export const Welcome: FC = () => { return ( <> {defaultChain === CHAINS.Holesky && paused && } + {defaultChain === CHAINS.Hoodi && } {isWrongChain && } From 6a3014d55473135dddf9200f46fe4231bca90be5 Mon Sep 17 00:00:00 2001 From: exromany Date: Mon, 24 Mar 2025 17:57:34 +0700 Subject: [PATCH 23/42] fix: metrics for aggregatorPriceFeed --- utilsApi/contractAddressesMetricsMap.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/utilsApi/contractAddressesMetricsMap.ts b/utilsApi/contractAddressesMetricsMap.ts index 688a0ea1..858262e9 100644 --- a/utilsApi/contractAddressesMetricsMap.ts +++ b/utilsApi/contractAddressesMetricsMap.ts @@ -103,8 +103,8 @@ const METRIC_CONTRACT_ADDRESS_GETTERS = { [CONTRACT_NAMES.stETH]: getAddressGetter(getTokenAddress, TOKENS.STETH), [CONTRACT_NAMES.wstETH]: getAddressGetter(getTokenAddress, TOKENS.WSTETH), [CONTRACT_NAMES.WithdrawalQueue]: getAddressGetter(getWithdrawalQueueAddress), - [CONTRACT_NAMES.aggregatorStEthUsdPriceFeed]: - getAddressGetter(getAggregatorAddress), + [CONTRACT_NAMES.aggregatorStEthUsdPriceFeed]: () => + getAddressGetter(getAggregatorAddress)(CHAINS.Mainnet), [CONTRACT_NAMES.CSModule]: getAddressGetter( getCsmContractAddress, 'CSModule', @@ -141,11 +141,10 @@ const METRIC_CONTRACT_ADDRESS_GETTERS = { const aggregatorMainnetAddress = METRIC_CONTRACT_ADDRESS_GETTERS[ CONTRACT_NAMES.aggregatorStEthUsdPriceFeed -](CHAINS.Mainnet) as HexString; +]() as HexString; const prefilledAddresses = - config.defaultChain === CHAINS.Hoodi && - !config.supportedChains.includes(CHAINS.Mainnet) + config.defaultChain !== CHAINS.Mainnet ? ({ [CHAINS.Mainnet]: [aggregatorMainnetAddress], } as Record) @@ -154,8 +153,7 @@ const prefilledAddresses = const prefilledMetricAddresses: Partial< Record> > = - config.defaultChain === CHAINS.Hoodi && - !config.supportedChains.includes(CHAINS.Mainnet) + config.defaultChain !== CHAINS.Mainnet ? { [CHAINS.Mainnet]: { [aggregatorMainnetAddress]: From 17241bac0c7d3131f37d4ba317b959df87e92a82 Mon Sep 17 00:00:00 2001 From: exromany Date: Mon, 24 Mar 2025 17:58:21 +0700 Subject: [PATCH 24/42] chore: correct gate rule for surveys page --- pages/surveys/[[...slug]].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/surveys/[[...slug]].tsx b/pages/surveys/[[...slug]].tsx index e4203f9d..22cfe801 100644 --- a/pages/surveys/[[...slug]].tsx +++ b/pages/surveys/[[...slug]].tsx @@ -40,7 +40,7 @@ const Page = () => { return ( - }> + }> }> {page} From 595b3ed9df811c36d8b405039f7e5a8c725636e6 Mon Sep 17 00:00:00 2001 From: exromany Date: Mon, 24 Mar 2025 18:20:47 +0700 Subject: [PATCH 25/42] chore: holesky-banner fix: banner width --- .../paused-banner/paused-banner.tsx | 9 ---- .../welcome/holesky-banner/holesky-banner.tsx | 41 ++++--------------- features/welcome/welcome.tsx | 2 +- shared/components/banner/styles.ts | 1 + 4 files changed, 11 insertions(+), 42 deletions(-) diff --git a/features/starter-pack/paused-banner/paused-banner.tsx b/features/starter-pack/paused-banner/paused-banner.tsx index ebfa1e5c..bb907fed 100644 --- a/features/starter-pack/paused-banner/paused-banner.tsx +++ b/features/starter-pack/paused-banner/paused-banner.tsx @@ -1,17 +1,8 @@ import { FC } from 'react'; import { BannerHeader, BlockStyled } from './styles'; -import { getConfig } from 'config'; -import { CHAINS } from 'consts/chains'; -import { HoleskyBanner } from 'features/welcome/holesky-banner'; - -const { defaultChain } = getConfig(); export const PausedBanner: FC = () => { - if (defaultChain === CHAINS.Holesky) { - return ; - } - return ( CSM is paused diff --git a/features/welcome/holesky-banner/holesky-banner.tsx b/features/welcome/holesky-banner/holesky-banner.tsx index b1442188..09863a63 100644 --- a/features/welcome/holesky-banner/holesky-banner.tsx +++ b/features/welcome/holesky-banner/holesky-banner.tsx @@ -1,39 +1,16 @@ -import { DarkThemeProvider, Text } from '@lidofinance/lido-ui'; +import { DarkThemeProvider } from '@lidofinance/lido-ui'; import { FC } from 'react'; -import { Stack } from 'shared/components'; -import { StyledAccordion } from './style'; +import { Banner } from 'shared/components'; -export const HoleskyBanner: FC<{ open?: boolean }> = ({ open }) => { +export const HoleskyBanner: FC = () => { return ( - - - CSM is paused on Holesky - - - CSM is transitioning from the Holesky testnet to the Hoodi - testnet, and its operations on Holesky have been paused. - - - } - > - <> - - This means that uploading new keys is currently not possible, but - Node Operator stats can be viewed. - - - This update affects only CSM on Holesky testnet — CSM on Mainnet - remains fully operational. - - - Stay tuned for more details on the Hoodi testnet launch! - - - + +

+ This update affects only CSM on Holesky testnet — CSM on Mainnet + remains fully operational. +

+
); }; diff --git a/features/welcome/welcome.tsx b/features/welcome/welcome.tsx index 25a9bb69..ce725d0b 100644 --- a/features/welcome/welcome.tsx +++ b/features/welcome/welcome.tsx @@ -24,7 +24,7 @@ export const Welcome: FC = () => { return ( <> - {defaultChain === CHAINS.Holesky && paused && } + {defaultChain === CHAINS.Holesky && } {defaultChain === CHAINS.Hoodi && } {isWrongChain && } diff --git a/shared/components/banner/styles.ts b/shared/components/banner/styles.ts index 6fe18fcc..1bb1e692 100644 --- a/shared/components/banner/styles.ts +++ b/shared/components/banner/styles.ts @@ -52,6 +52,7 @@ export const BannerStyled = styled(Block)<{ $variant?: BannerVariant }>` display: flex; flex-direction: column; gap: ${({ theme }) => theme.spaceMap.md}px; + width: 100%; ${({ $variant }) => ($variant ? VARIANTS[$variant] : '')} `; From 9d35b502e9eeed1d522e8a0766acd18d636de94e Mon Sep 17 00:00:00 2001 From: exromany Date: Sat, 15 Mar 2025 19:41:37 +0800 Subject: [PATCH 26/42] feat: integrate shares to stETH conversion in LastRewards component --- features/dashboard/bond/last-rewards.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/features/dashboard/bond/last-rewards.tsx b/features/dashboard/bond/last-rewards.tsx index f57fd425..7d0b5b68 100644 --- a/features/dashboard/bond/last-rewards.tsx +++ b/features/dashboard/bond/last-rewards.tsx @@ -16,6 +16,7 @@ import { useLastRewrdsTx, useNodeOperatorInfo, useRewardsFrame, + useSharesToSteth, } from 'shared/hooks'; import { countDaysLeft, formatDate, formatPercent } from 'utils'; import { Balance } from './balance'; @@ -31,6 +32,8 @@ import { export const LastRewards: FC = () => { const { data: lastRewards, initialLoading: isLoading } = useLastOperatorRewards(); + const { data: distributed, initialLoading: isDistributedLoading } = + useSharesToSteth(lastRewards?.distributed); const id = useNodeOperatorId(); const { data: info } = useNodeOperatorInfo(id); @@ -62,8 +65,8 @@ export const LastRewards: FC = () => { : undefined} /> From e5f747ce3aa5a3d0a612338f8b163a5c392382d1 Mon Sep 17 00:00:00 2001 From: exromany Date: Mon, 24 Mar 2025 23:06:46 +0700 Subject: [PATCH 27/42] chore: optional country fields in setup survey --- features/surveys/survey-setup/survey-setup.tsx | 3 --- features/surveys/survey-setup/transform.tsx | 4 ++-- features/surveys/types.ts | 6 +++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/features/surveys/survey-setup/survey-setup.tsx b/features/surveys/survey-setup/survey-setup.tsx index 9def91de..57ee6ffb 100644 --- a/features/surveys/survey-setup/survey-setup.tsx +++ b/features/surveys/survey-setup/survey-setup.tsx @@ -216,7 +216,6 @@ export const SurveySetup: FC<{ id?: string }> = ({ id }) => { @@ -257,7 +256,6 @@ export const SurveySetup: FC<{ id?: string }> = ({ id }) => { )} @@ -279,7 +277,6 @@ export const SurveySetup: FC<{ id?: string }> = ({ id }) => { fieldName="mevMinBid" label="Min bid" token="ETH" - rules={required} /> Submit diff --git a/features/surveys/survey-setup/transform.tsx b/features/surveys/survey-setup/transform.tsx index 0c0d593c..b6e29600 100644 --- a/features/surveys/survey-setup/transform.tsx +++ b/features/surveys/survey-setup/transform.tsx @@ -3,13 +3,13 @@ import { Setup, SetupRaw } from '../types'; export const transformOutcoming = (data: Setup): SetupRaw => ({ ...data, - mevMinBid: data.mevMinBid?.toString(), + mevMinBid: data.mevMinBid?.toString() || null, validatorClient: data.validatorSameAsCl ? '' : data.validatorClient, validatorServerType: data.validatorSameAsCl ? '' : data.validatorServerType, validatorCountry: data.validatorSameAsCl ? '' : data.validatorCountry, }); export const transformIncoming = (data: SetupRaw): Setup => ({ ...data, - mevMinBid: BigNumber.from(data.mevMinBid), + mevMinBid: data.mevMinBid ? BigNumber.from(data.mevMinBid) : undefined, validatorSameAsCl: data.validatorSameAsCl ?? false, }); diff --git a/features/surveys/types.ts b/features/surveys/types.ts index abc1360d..ce5352e6 100644 --- a/features/surveys/types.ts +++ b/features/surveys/types.ts @@ -37,17 +37,17 @@ export type Setup = { elClient: string; clClient: string; clinetsServerType: string; - clientsCountry: string; + clientsCountry?: string; validatorClient: string; validatorServerType: string; - validatorCountry: string; + validatorCountry?: string; validatorSameAsCl?: boolean; remoteSigner: string; mevMinBid?: BigNumber; }; export type SetupRaw = Omit & { - mevMinBid?: string; + mevMinBid?: string | null; }; export type Summary = { From 62083aed99fe17bb9fef832ca8c984951208c980 Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 25 Mar 2025 01:13:57 +0700 Subject: [PATCH 28/42] feat: surveys cta before april --- features/dashboard/dashboard.tsx | 2 ++ features/dashboard/surveys-cta/index.ts | 1 + .../dashboard/surveys-cta/surveys-cta.tsx | 22 +++++++++++++++++++ shared/components/banner/banner.tsx | 21 +++++++++++++++--- shared/counters/counter-surveys.tsx | 10 +++++++++ shared/counters/index.ts | 1 + shared/hooks/index.ts | 1 + shared/hooks/use-surveys-call.ts | 7 ++++++ .../components/navigation/use-nav-items.tsx | 2 ++ 9 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 features/dashboard/surveys-cta/index.ts create mode 100644 features/dashboard/surveys-cta/surveys-cta.tsx create mode 100644 shared/counters/counter-surveys.tsx create mode 100644 shared/hooks/use-surveys-call.ts diff --git a/features/dashboard/dashboard.tsx b/features/dashboard/dashboard.tsx index ee7910f8..8abdeb5d 100644 --- a/features/dashboard/dashboard.tsx +++ b/features/dashboard/dashboard.tsx @@ -5,12 +5,14 @@ import { RolesSection } from './roles'; import { ExternalSection } from './external'; import { getConfig } from 'config'; import { CHAINS } from 'consts/chains'; +import { SurveysCta } from './surveys-cta'; const { defaultChain } = getConfig(); export const Dashboard: FC = () => { return ( <> + diff --git a/features/dashboard/surveys-cta/index.ts b/features/dashboard/surveys-cta/index.ts new file mode 100644 index 00000000..90f421ae --- /dev/null +++ b/features/dashboard/surveys-cta/index.ts @@ -0,0 +1 @@ +export * from './surveys-cta'; diff --git a/features/dashboard/surveys-cta/surveys-cta.tsx b/features/dashboard/surveys-cta/surveys-cta.tsx new file mode 100644 index 00000000..e4abb8df --- /dev/null +++ b/features/dashboard/surveys-cta/surveys-cta.tsx @@ -0,0 +1,22 @@ +import { FC } from 'react'; +import { Banner } from 'shared/components'; +import { useSurveysCall } from 'shared/hooks'; +import { LocalLink } from 'shared/navigate'; + +export const SurveysCta: FC = () => { + const required = useSurveysCall(); + if (!required) return null; + + return ( + + You're invited to voluntarily submit your validator setup data by + March 31st to help enhance the transparency of the Lido Protocol! Go to + the Surveys tab and fill out the + "Your Setup" form. + + ); +}; diff --git a/shared/components/banner/banner.tsx b/shared/components/banner/banner.tsx index e6655b11..a01a118b 100644 --- a/shared/components/banner/banner.tsx +++ b/shared/components/banner/banner.tsx @@ -1,12 +1,19 @@ -import { FC, PropsWithChildren, ReactNode } from 'react'; +import { ComponentProps, FC, PropsWithChildren, ReactNode } from 'react'; import { BannerContent, BannerHeader, BannerStyled, BannerVariant, } from './styles'; +import { LocalLink } from 'shared/navigate'; +import { Stack } from '../stack/stack'; +import { SectionHeaderLinkStyle } from '../section-title/styles'; -type BannerProps = { +import { ReactComponent as RoundedArrowIcon } from 'assets/icons/rounded-arrow.svg'; + +type BannerProps = Partial< + Pick, 'href' | 'matomoEvent'> +> & { title?: ReactNode; variant?: BannerVariant; }; @@ -14,10 +21,18 @@ type BannerProps = { export const Banner: FC> = ({ title, variant, + href, children, }) => ( - {title} + + {title} + {!!href && ( + + + + )} + {children} ); diff --git a/shared/counters/counter-surveys.tsx b/shared/counters/counter-surveys.tsx new file mode 100644 index 00000000..75eb55ae --- /dev/null +++ b/shared/counters/counter-surveys.tsx @@ -0,0 +1,10 @@ +import { FC } from 'react'; +import { Counter } from 'shared/components'; +import { useSurveysCall } from 'shared/hooks'; + +export const CounterSurveys: FC = () => { + const required = useSurveysCall(); + const count = Number(required); + + return ; +}; diff --git a/shared/counters/index.ts b/shared/counters/index.ts index 0e9741a2..eda4aacb 100644 --- a/shared/counters/index.ts +++ b/shared/counters/index.ts @@ -1,3 +1,4 @@ export * from './counter-invalid-keys'; export * from './counter-invites'; export * from './counter-locked-bond'; +export * from './counter-surveys'; diff --git a/shared/hooks/index.ts b/shared/hooks/index.ts index cc9a1183..01cdd3c7 100644 --- a/shared/hooks/index.ts +++ b/shared/hooks/index.ts @@ -35,6 +35,7 @@ export * from './use-router-path'; export * from './use-send-tx'; export * from './use-show-rule'; export * from './use-sorted-keys'; +export * from './use-surveys-call'; export * from './use-token-max-amount'; export * from './use-tx-cost-in-usd'; export * from './use-withdrawn-key-indexes-from-events'; diff --git a/shared/hooks/use-surveys-call.ts b/shared/hooks/use-surveys-call.ts new file mode 100644 index 00000000..d85bd4f7 --- /dev/null +++ b/shared/hooks/use-surveys-call.ts @@ -0,0 +1,7 @@ +import { isBefore, parseISO } from 'date-fns'; + +export const useSurveysCall = () => { + const endOfSurvey = parseISO('2025-04-01T00:00Z'); + const today = new Date(); + return isBefore(today, endOfSurvey); +}; diff --git a/shared/layout/header/components/navigation/use-nav-items.tsx b/shared/layout/header/components/navigation/use-nav-items.tsx index bf63b32f..2d5d8549 100644 --- a/shared/layout/header/components/navigation/use-nav-items.tsx +++ b/shared/layout/header/components/navigation/use-nav-items.tsx @@ -12,6 +12,7 @@ import { CounterInvalidKeys, CounterInvites, CounterLockedBond, + CounterSurveys, } from 'shared/counters'; import { ShowRule, useShowRule } from 'shared/hooks'; @@ -73,6 +74,7 @@ const routes: Route[] = [ path: PATH.SURVEYS, icon: , showRules: ['IS_SURVEYS_ACTIVE'], + suffix: , }, ]; From c6027c36eccb28c295bc6be761f9e210a3c2447b Mon Sep 17 00:00:00 2001 From: exromany Date: Tue, 25 Mar 2025 16:22:40 +0700 Subject: [PATCH 29/42] fix: hide beaconcha.in on hoodi --- consts/external-links.ts | 4 ++-- shared/hooks/use-external-links.ts | 2 ++ .../tx-stages-parts/after-keys-upload.tsx | 18 +++++++++++------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/consts/external-links.ts b/consts/external-links.ts index e874c728..c3952cae 100644 --- a/consts/external-links.ts +++ b/consts/external-links.ts @@ -92,8 +92,8 @@ export const EXTERNAL_LINKS_BY_NETWORK: Record = feesMonitoring: 'https://fees-monitoring-hoodi.testnet.fi', operatorsWidget: 'https://operators-hoodi.testnet.fi', - beaconchain: 'https://holesky.beaconcha.in', - beaconchainDashboard: 'https://v2-beta-holesky.beaconcha.in/dashboard', + beaconchain: '', + beaconchainDashboard: '', ratedExplorer: 'https://explorer.rated.network', subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', keysApi: 'http://hr6vb81d1ndsx-hoodi-keys-api.valset-01.testnet.fi', diff --git a/shared/hooks/use-external-links.ts b/shared/hooks/use-external-links.ts index 4d129f74..6a0c7465 100644 --- a/shared/hooks/use-external-links.ts +++ b/shared/hooks/use-external-links.ts @@ -15,6 +15,8 @@ export const useBeaconchainDashboardLink = (directKeys?: string[]) => { const { data: keys } = useKeysWithStatus(); const sortedKeys = useSortedKeys(keys, ACTIVE_STATUS_ORDER); + if (!links.beaconchainDashboard) return null; + const keysToShow = ( sortedKeys?.length ? sortedKeys.map(({ key }) => key) : directKeys ) diff --git a/shared/transaction-modal/tx-stages-parts/after-keys-upload.tsx b/shared/transaction-modal/tx-stages-parts/after-keys-upload.tsx index dea05714..e98b1e9a 100644 --- a/shared/transaction-modal/tx-stages-parts/after-keys-upload.tsx +++ b/shared/transaction-modal/tx-stages-parts/after-keys-upload.tsx @@ -30,13 +30,17 @@ export const AfterKeysUpload: FC = ({ keys }) => { > Keys tab - , on{' '} - - beaconcha.in - {' '} + {beaconchain && ( + <> + , on{' '} + + beaconcha.in + + + )}{' '} or subscribe to the{' '} Date: Tue, 25 Mar 2025 18:42:58 +0700 Subject: [PATCH 30/42] chore: hide setups on testnet --- consts/urls.ts | 1 + features/surveys/surveys-home-page.tsx | 4 +-- .../surveys/surveys-home/surverys-home.tsx | 25 ++++++++++++++----- pages/surveys/[[...slug]].tsx | 2 ++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/consts/urls.ts b/consts/urls.ts index 35c5d810..b738260f 100644 --- a/consts/urls.ts +++ b/consts/urls.ts @@ -26,6 +26,7 @@ export const PATH = { SURVEYS_EXPERIENCE: '/surveys/experience', SURVEYS_HOW_DID_YOU_LEARN_CSM: '/surveys/learn-csm', SURVEYS_SETUP: '/surveys/setup', + SURVEYS_ALL: '/surveys/all', }; export type PATH = (typeof PATH)[keyof typeof PATH]; diff --git a/features/surveys/surveys-home-page.tsx b/features/surveys/surveys-home-page.tsx index 09ddb197..778f513c 100644 --- a/features/surveys/surveys-home-page.tsx +++ b/features/surveys/surveys-home-page.tsx @@ -4,10 +4,10 @@ import { NoSSRWrapper } from 'shared/components'; import { Layout } from 'shared/layout'; import { SurveysHome } from './surveys-home/surverys-home'; -export const SurveysHomePage: FC = () => ( +export const SurveysHomePage: FC<{ all?: boolean }> = ({ all }) => ( - + ); diff --git a/features/surveys/surveys-home/surverys-home.tsx b/features/surveys/surveys-home/surverys-home.tsx index 21e5f9c0..64c982e8 100644 --- a/features/surveys/surveys-home/surverys-home.tsx +++ b/features/surveys/surveys-home/surverys-home.tsx @@ -12,8 +12,12 @@ import { useSurveysSWR } from '../shared/use-surveys-swr'; import { useConfirmEraseModal } from './confirm-erase-modal'; import { Divider, Plus, Text } from '@lidofinance/lido-ui'; import { SetupsKeys, Summary } from '../types'; +import { CHAINS } from 'consts/chains'; +import { getConfig } from 'config'; -export const SurveysHome: FC = () => { +const { defaultChain } = getConfig(); + +export const SurveysHome: FC<{ all?: boolean }> = ({ all }) => { const { data, isLoading, remove } = useSurveysSWR('summary'); const { data: keys, mutate: mutateKeys } = useSurveysSWR('setups/keys'); @@ -27,6 +31,18 @@ export const SurveysHome: FC = () => { } }, [confirmModal, mutateKeys, remove]); + const showErase = !!( + data?.contacts || + data?.experience || + data?.howDidYouLearnCsm || + (data?.setups && data.setups.length > 0) + ); + const showSetups = !!( + (all || defaultChain === CHAINS.Mainnet) && + keys && + (keys.total > 0 || keys.filled > 0) + ); + return ( { - {keys && (keys.total > 0 || keys.filled > 0) && ( + {showSetups && ( { )} - {(data?.contacts || - data?.experience || - data?.howDidYouLearnCsm || - (data?.setups && data.setups.length > 0)) && ( + {showErase && ( <> diff --git a/pages/surveys/[[...slug]].tsx b/pages/surveys/[[...slug]].tsx index 22cfe801..f58a423b 100644 --- a/pages/surveys/[[...slug]].tsx +++ b/pages/surveys/[[...slug]].tsx @@ -32,6 +32,8 @@ const Page = () => { return ; case PATH.SURVEYS_SETUP: return ; + case PATH.SURVEYS_ALL: + return ; default: return ; From 6d415897c6fd68aa9c28027747c89d653785030a Mon Sep 17 00:00:00 2001 From: pablomendezroyo <41727368+pablomendezroyo@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:22:01 +0100 Subject: [PATCH 31/42] feat: dappnode features V1 (#1) * feat: dappnode features * fix: remove lido ci * feat: add dappnode settings in next config * fix: dockerfile adapt to dappnode needs * feat: add dappnode ci * fix: filter exits requests by validators status * fix: spinner on performance loading * fix: rely on wallet provider instead of envs * fix: get exit requests with fetchretry * fix: one spinner for each loading warning in `/dashboard` (#6) * fix: useffects dependencies * fix: change default values for use state hooks to avoid frontend flickering * fix: remove spinner on keys warning * fix: exitsLoader warning * fix: exit requests hook default loading state value to false * fix: execution gates on loading (#5) * fix: adding `/performance` tab faqs * fix: adding `/notifications` faqs * fix: display Dappnode's `starter-pack` (#12) * fix: add `/performance` & `/notifications` routes (#11) * fix: dappnode `/dashboard` features + initial calls fixed (#10) * fix: `starter-pack` ux fixes (#16) * fix: implementing dappnode as referral (#15) * fix: dappnode links update (#14) * fix: hardcode `walletConnectProjectId` (#13) * fix: force events scan after new NO created (#17) * fix: modal after NO created updated (#18) * fix: header & footer `min-width: 100%` (#20) * fix: add change `RPC_URL` link to docs (#19) * fix: add change `RPC_URL` link to docs * fix: scanning events copy improved --------- Co-authored-by: mateumiralles Co-authored-by: Mateu Miralles <52827122+mateumiralles@users.noreply.github.com> --- .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/dappnode-release.yml | 85 ++++ .github/workflows/prepare-release-draft.yml | 14 - Dockerfile | 17 +- assets/icons/bell.svg | 3 + assets/icons/down-arrow.svg | 6 + assets/icons/eye-off.svg | 7 + assets/icons/eye-on.svg | 4 + assets/icons/star.svg | 3 + consts/csm-constants.ts | 6 + consts/external-links.ts | 12 +- 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 | 31 ++ 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 | 120 +++++ dappnode/hooks/use-ec-sanity-check.ts | 98 ++++ ...use-exit-requested-keys-from-events-api.ts | 74 +++ dappnode/hooks/use-get-exit-requests.ts | 61 +++ 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 | 175 +++++++ dappnode/hooks/use-get-relays-data.ts | 161 +++++++ dappnode/hooks/use-get-telegram-data.ts | 43 ++ .../hooks/use-invites-events-fetcher-api.ts | 135 ++++++ dappnode/hooks/use-missing-keys.ts | 96 ++++ ...-node-operators-fetcher-from-events-api.ts | 163 +++++++ ...use-node-operators-with-locked-bond-api.ts | 94 ++++ dappnode/hooks/use-post-telegram-data.ts | 64 +++ ...e-withdrawn-key-indexes-from-events-api.ts | 64 +++ .../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/index.tsx | 17 + 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 | 53 +++ .../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.tsx | 17 + .../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 | 428 ++++++++++++++++++ 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 | 89 ++++ dappnode/status/types.ts | 18 + dappnode/status/warnings.tsx | 250 ++++++++++ dappnode/utils/capitalize-first-char.ts | 3 + dappnode/utils/dappnode-docs-urls.ts | 13 + dappnode/utils/fetchWithRetry.ts | 20 + dappnode/utils/is-tg-bot-token.ts | 4 + dappnode/utils/is-tg-user-id.ts | 4 + dappnode/utils/sanitize-urls.ts | 26 ++ env-dynamics.mjs | 2 +- faq/notifications-1.md | 9 + faq/notifications-2.md | 7 + faq/notifications-3.md | 10 + 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-notifications-1.md | 9 + faq/testnet-notifications-2.md | 7 + faq/testnet-notifications-3.md | 10 + 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 + .../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 + .../context/use-submit-keys-submit.ts | 32 +- .../submit-keys-form/controls/keys-input.tsx | 12 + .../hooks/use-tx-modal-stages-submit-keys.tsx | 12 + features/dashboard/dashboard.tsx | 6 + .../starter-pack-section.tsx | 99 ++-- features/starter-pack/starter-pack.tsx | 19 +- .../locked-section/locked-section.tsx | 4 +- features/welcome/try-csm/try-csm.tsx | 14 + .../welcome-section/welcome-section.tsx | 1 - global.d.ts | 5 + lib/getFaq.ts | 14 +- next.config.mjs | 71 +++ package.json | 2 + pages/_app.tsx | 4 +- pages/index.tsx | 20 +- pages/notifications/index.ts | 3 + pages/performance/index.ts | 3 + providers/modify-provider.tsx | 19 +- .../use-get-active-node-operator.ts | 34 ++ shared/components/status-chip/status-chip.tsx | 23 +- shared/hooks/use-csm-node-operators.ts | 7 +- shared/hooks/use-invites.ts | 4 +- shared/hooks/use-keys-with-status.ts | 7 +- shared/hooks/use-show-rule.ts | 22 +- shared/layout/footer/styles.tsx | 2 +- .../header/components/navigation/styles.tsx | 4 +- .../components/navigation/use-nav-items.tsx | 21 +- shared/layout/header/styles.tsx | 1 + shared/navigate/gates/gate-loaded.tsx | 5 +- .../tx-stages-parts/after-keys-upload.tsx | 49 +- yarn.lock | 218 ++++++++- 138 files changed, 5668 insertions(+), 431 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 create mode 100644 .github/workflows/dappnode-release.yml delete mode 100644 .github/workflows/prepare-release-draft.yml create mode 100644 assets/icons/bell.svg 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 assets/icons/star.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/index.tsx 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.tsx 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/fetchWithRetry.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 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/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-notifications-1.md create mode 100644 faq/testnet-notifications-2.md create mode 100644 faq/testnet-notifications-3.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 create mode 100644 pages/notifications/index.ts create mode 100644 pages/performance/index.ts 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 25e81cf3..00000000 --- a/.github/workflows/ci-dev.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: CI Dev Hoodi - -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_hoodi_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/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 }} 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/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"] diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg new file mode 100644 index 00000000..32463588 --- /dev/null +++ b/assets/icons/bell.svg @@ -0,0 +1,3 @@ + + + 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/assets/icons/star.svg b/assets/icons/star.svg new file mode 100644 index 00000000..3956505f --- /dev/null +++ b/assets/icons/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/consts/csm-constants.ts b/consts/csm-constants.ts index 54b7145b..f0e0c815 100644 --- a/consts/csm-constants.ts +++ b/consts/csm-constants.ts @@ -23,6 +23,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 slotsPerFrame: number; }; @@ -43,6 +45,8 @@ export const CONSTANTS_BY_NETWORK: Record = { stakingModuleId: 3, withdrawalCredentials: '0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f', retentionPeriodMins: 80_640, // 8 weeks + lidoFeeRecipient: '0x388C818CA8B9251b393131C08a736A67ccB19297', // DAPPNODE + reportTimestamp: 1732282199, // DAPPNODE, epoch 326714 slotsPerFrame: 32 * 225 * 28, // 28 days }, [CHAINS.Hoodi]: { @@ -79,6 +83,8 @@ export const CONSTANTS_BY_NETWORK: Record = { stakingModuleId: 4, withdrawalCredentials: '0xF0179dEC45a37423EAD4FaD5fCb136197872EAd9', retentionPeriodMins: 80_640, // 8 weeks + lidoFeeRecipient: '0xE73a3602b99f1f913e72F8bdcBC235e206794Ac8', // DAPPNODE + reportTimestamp: 1734371136, // DAPPNODE, epoch 100179 slotsPerFrame: 32 * 225 * 7, // 7 days }, }; diff --git a/consts/external-links.ts b/consts/external-links.ts index c3952cae..b086f8b4 100644 --- a/consts/external-links.ts +++ b/consts/external-links.ts @@ -1,8 +1,10 @@ import { CHAINS } from 'consts/chains'; import { config } from 'config'; -export const CSM_MAINNET_LINK = 'https://csm.lido.fi/'; -export const CSM_TESTNET_LINK = 'https://csm.testnet.fi/'; +export const CSM_MAINNET_LINK = + 'http://my.dappnode/installer/dnp/lido-csm-mainnet.dnp.dappnode.eth'; // DAPPNODE +export const CSM_TESTNET_LINK = + 'http://my.dappnode/installer/dnp/lido-csm-holesky.dnp.dappnode.eth'; // DAPPNODE export const HOW_TO_EXIT_VALIDATOR_LINK = 'https://dvt-homestaker.stakesaurus.com/bonded-validators-setup/lido-csm/exiting-csm-validators'; @@ -57,7 +59,7 @@ export const EXTERNAL_LINKS_BY_NETWORK: Record = ratedExplorer: 'https://explorer.rated.network', ethseerDashboard: 'https://ethseer.io', subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', - keysApi: 'https://keys-api.lido.fi', + keysApi: 'http://lido-events.lido-csm-mainnet.dappnode:8081', // DAPPNODE surveyApi: 'https://csm-surveys-api-mainnet.up.railway.app', }, [CHAINS.Holesky]: { @@ -78,7 +80,7 @@ export const EXTERNAL_LINKS_BY_NETWORK: Record = beaconchainDashboard: 'https://v2-beta-holesky.beaconcha.in/dashboard', ratedExplorer: 'https://explorer.rated.network', subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', - keysApi: 'https://keys-api-holesky.testnet.fi', + keysApi: 'http://lido-events.lido-csm-holesky.dappnode:8081', // DAPPNODE surveyApi: '', }, // FIXME: links @@ -96,7 +98,7 @@ export const EXTERNAL_LINKS_BY_NETWORK: Record = beaconchainDashboard: '', ratedExplorer: 'https://explorer.rated.network', subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', - keysApi: 'http://hr6vb81d1ndsx-hoodi-keys-api.valset-01.testnet.fi', + keysApi: 'http://lido-events.lido-csm-hoodi.dappnode:8081', // DAPPNODE surveyApi: 'https://csm-surveys-api-testnet.up.railway.app', }, }; diff --git a/consts/urls.ts b/consts/urls.ts index b738260f..ae58bf6f 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..29f2d32f --- /dev/null +++ b/dappnode/fallbacks/ec-scanning-events.tsx @@ -0,0 +1,31 @@ +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'; +import { Link } from '@lidofinance/lido-ui'; +import { dappnodeLidoDocsUrls } from 'dappnode/utils/dappnode-docs-urls'; + +export const ECScanningPage: FC = () => { + return ( + + + +

Execution client scanning blocks

+
+

To retrieve data, this UI scans blockchain events.

+

+ The first login may take a few minutes, depending on your execution + client. +

+

+ If you want to reduce the waiting time, use a central RPC node (e.g., + Infura).
+ Learn more in our{' '} + Documentation. +

+ +
+
+ ); +}; 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 ( + + + +
Community Staking Module
+
+ {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..a934a6aa --- /dev/null +++ b/dappnode/hooks/use-dappnode-urls.ts @@ -0,0 +1,120 @@ +import { CHAINS } from '@lido-sdk/constants'; +import getConfig from 'next/config'; + +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 = () => { + // 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> = { + [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: + publicRuntimeConfig.rpcUrls_1 || + '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: + publicRuntimeConfig.rpcUrls_17000 || + '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[publicRuntimeConfig.defaultChain as CHAINS]?.brainUrl || ''; + const brainKeysUrl = + urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.brainKeysUrl || ''; + const brainLaunchpadUrl = + 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[publicRuntimeConfig.defaultChain as CHAINS]?.MEVApiUrl || ''; + const MEVPackageConfig = + urlsByChain[publicRuntimeConfig.defaultChain 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..5be07524 --- /dev/null +++ b/dappnode/hooks/use-ec-sanity-check.ts @@ -0,0 +1,98 @@ +import { CHAINS } from '@lido-sdk/constants'; +import getConfig from 'next/config'; +import { useEffect, useMemo, useState } from 'react'; +import useDappnodeUrls from './use-dappnode-urls'; + +export const useECSanityCheck = () => { + 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(); + + const contractTx = useMemo( + () => ({ + [CHAINS.Mainnet]: `0xf5330dbcf09885ed145c4435e356b5d8a10054751bb8009d3a2605d476ac173f`, + [CHAINS.Holesky]: `0x1475719ecbb73b28bc531bb54b37695df1bf6b71c6d2bf1d28b4efa404867e26`, + }), + [], + ); + + useEffect(() => { + const getSyncStatus = async () => { + try { + setIsLoading(true); + 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) { + publicRuntimeConfig.defaultChain; + 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(); + }, [ECApiUrl, publicRuntimeConfig.defaultChain]); + + useEffect(() => { + const getTxStatus = async () => { + try { + setIsLoading(true); + + const txResponse = await fetch(`${ECApiUrl}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_getTransactionReceipt', + params: [ + contractTx[ + publicRuntimeConfig.defaultChain 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(); + }, [ECApiUrl, contractTx, isSynced, publicRuntimeConfig.defaultChain]); + + 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..28b76e1f --- /dev/null +++ b/dappnode/hooks/use-exit-requested-keys-from-events-api.ts @@ -0,0 +1,74 @@ +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'; +import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry'; + +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 fetchWithRetry(url, options, 5000); + + 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..2a96701f --- /dev/null +++ b/dappnode/hooks/use-get-exit-requests.ts @@ -0,0 +1,61 @@ +import { useCallback, 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(); + const [exitRequests, setExitRequests] = useState(); + const [isLoading, setIsLoading] = useState(false); + + const nodeOperator = useActiveNodeOperator(); + + interface ExitRequest { + event: { + [key: string]: any; + }; + [key: string]: any; + validator_pubkey_hex: string; + } + type ExitRequests = Record; + + const getExitRequests = useCallback(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 = { + 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}`); + } + + 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); + setIsLoading(false); + } catch (e) { + console.error( + `Error GETting validators exit requests from indexer API: ${e}`, + ); + setIsLoading(false); + } + }, [backendUrl, nodeOperator]); + + return { exitRequests, getExitRequests, isLoading }; +}; + +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..caf5ce40 --- /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 { INFRA_STATUS } 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..cc875e7b --- /dev/null +++ b/dappnode/hooks/use-get-performance-by-range.ts @@ -0,0 +1,175 @@ +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 { operatorData, isLoading } = 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; + + 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]); + + 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..f35c7db2 --- /dev/null +++ b/dappnode/hooks/use-get-telegram-data.ts @@ -0,0 +1,43 @@ +import { useCallback, 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 = useCallback(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); + } + }, [backendUrl]); + + 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..87565024 --- /dev/null +++ b/dappnode/hooks/use-invites-events-fetcher-api.ts @@ -0,0 +1,135 @@ +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'; +import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry'; + +type AddressChangeProposedEvents = + | NodeOperatorManagerAddressChangeProposedEvent + | NodeOperatorRewardAddressChangeProposedEvent + | NodeOperatorManagerAddressChangedEvent + | NodeOperatorRewardAddressChangedEvent; + +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..1703d7b7 --- /dev/null +++ b/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts @@ -0,0 +1,163 @@ +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`. + * 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 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..51cd5610 --- /dev/null +++ b/dappnode/hooks/use-node-operators-with-locked-bond-api.ts @@ -0,0 +1,94 @@ +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'; +import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry'; + +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..24e404ef --- /dev/null +++ b/dappnode/hooks/use-withdrawn-key-indexes-from-events-api.ts @@ -0,0 +1,64 @@ +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'; +import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry'; + +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} + /> + + +
+ ); +} 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) => ( +
+ {i + 1}. 0x +
+
+ ))} +
+ +

{label}

+
+ {showErrorMessage && ( +
{}
+ )} + + {keysFiles.length > 0 && ( + <> +
+

Uploaded Files:

+ {keysFiles.map((file, index) => ( + +

{file.name}

+ +
+ ))} +
+

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/index.tsx b/dappnode/notifications/index.tsx new file mode 100644 index 00000000..b88a3d17 --- /dev/null +++ b/dappnode/notifications/index.tsx @@ -0,0 +1,17 @@ +import { PATH } from 'consts/urls'; +import { NotificationsPage } from 'dappnode/notifications/notifications-page'; +import { getFaqNotifications } from 'lib/getFaq'; +import { getProps } from 'lib/getProps'; +import { Gate, GateLoaded, Navigate } from 'shared/navigate'; + +const Page = () => ( + + }> + + + +); + +export default Page; + +export const getStaticProps = getProps(getFaqNotifications); 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! + + )} + + + + 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..4045fd44 --- /dev/null +++ b/dappnode/notifications/notifications-setup-steps.tsx @@ -0,0 +1,53 @@ +import { FC } from 'react'; +import styled from 'styled-components'; +import { Link } from '@lidofinance/lido-ui'; + +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 ( + + @userinfobot + {' '} + or{' '} + + @raw_data_bot + + ) +
  • +
  • + Create and get a Telegram Bot token ( + @BotFather) +
  • +
  • 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 ( + + ); +}; 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) => ( + + ))} + + )} + + + ); +}; 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.tsx b/dappnode/performance/index.tsx new file mode 100644 index 00000000..5abbabd9 --- /dev/null +++ b/dappnode/performance/index.tsx @@ -0,0 +1,17 @@ +import { PATH } from 'consts/urls'; +import { PerformancePage } from 'dappnode/performance/performance-page'; +import { getFaqPerformance } from 'lib/getFaq'; +import { getProps } from 'lib/getProps'; +import { Gate, GateLoaded, Navigate } from 'shared/navigate'; + +const Page = () => ( + + }> + + + +); + +export default Page; + +export const getStaticProps = getProps(getFaqPerformance); 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..3cf8af29 --- /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 PerformancePage: 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..3601b9db --- /dev/null +++ b/dappnode/starter-pack/steps.tsx @@ -0,0 +1,428 @@ +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 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'; +import getConfig from 'next/config'; +import { StatusTitle } from 'shared/components/status-chip/status-chip'; + +const { publicRuntimeConfig } = getConfig(); + +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. +

    +
      +
    • + {' '} +

      + It can be claimed or reused once the validator exits and any losses + are covered. +

      +
    • +
      +
    • + {' '} +

      + 2 {publicRuntimeConfig.defaultChain === 17000 && 'Holesky'} ETH + (stETH / wstETH equivalent) is required for the first validator{' '} +

      +
    • +
    + + + Learn more + +
    + + +); + +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(); + + return ( + <> + +

    + In order to be a Node Operator you must have a synced{' '} + {CHAINS[publicRuntimeConfig.defaultChain as CHAINS]} Node and run MEV + Boost. +

    + +

    Execution Client

    +

    {'->'}

    +

    + {isECLoading ? ( + + + + ) : ( + + {StatusTitle[ECStatus || 'NOT_INSTALLED']} + + )} +

    +
    + +

    Consensus Client

    +

    {'->'}

    + +

    + {isCCLoading ? ( + + + + ) : ( + + {StatusTitle[CCStatus || 'NOT_INSTALLED']} + + )} +

    +
    + +

    Web3signer

    +

    {'->'}

    +

    + {brainLoading ? ( + + + + ) : ( + + {isSignerInstalled ? 'Installed' : 'Not installed'} + + )} +

    +
    + +

    MEV Boost

    +

    {'->'}

    +

    + {relaysLoading ? ( + + + + ) : ( + + {isMEVInstalled ? 'Installed' : 'Not installed'} + + )} +

    +
    + {!isECSynced || + (!isCCSynced && ( +
    + + You must have a synced{' '} + {publicRuntimeConfig.defaultChain as CHAINS} Node and run MEV + Boost. + +
    + ))} + {!!brainError && ( +
    + You must have Web3Signer installed. +
    + )} + Set up your node{' '} +
    + + + + + + + + ); +}; + +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}} + + + + + + + ); +}; +const Step4: FC = ({ step, title, setStep }: StepsProps) => { + const withdrawalByAddres = + CONSTANTS_BY_NETWORK[publicRuntimeConfig.defaultChain as CHAINS] + ?.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. + + + + + + + + + + ); +}; 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..ec24e34d --- /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 { LoaderWrapperStyle } from 'shared/navigate/splash/loader-banner/styles'; +import { INFRA_STATUS } from './types'; + +export type InfraItemProps = { + title: string; + subtitle: string; + tooltip?: string; + status: INFRA_STATUS; + 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 && ( + + + + )} +
    + ); +} diff --git a/dappnode/status/status-section.tsx b/dappnode/status/status-section.tsx new file mode 100644 index 00000000..f29d94fe --- /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..f5726d45 --- /dev/null +++ b/dappnode/status/styles.tsx @@ -0,0 +1,89 @@ +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)'}; + + button { + border: none; + background: none; + } +`; + +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..de5f2467 --- /dev/null +++ b/dappnode/status/types.ts @@ -0,0 +1,18 @@ +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; + 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..0530521e --- /dev/null +++ b/dappnode/status/warnings.tsx @@ -0,0 +1,250 @@ +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, + isLoading: exitsLoading, + } = 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, + ]); + + const WarningWrapper: FC<{ + isLoading?: boolean; + showIf: boolean; + children: React.ReactNode; + }> = ({ isLoading = false, showIf, children }) => { + return isLoading ? ( + + + + ) : ( + showIf && <>{children} + ); + }; + + return ( + + + 0}> +
    + {numWarnings > 0 ? ( +

    + You have {numWarnings} + warning/s +

    + ) : ( + "You don't have any warnings" + )} +
    +
    +
    + + + +

    Your Execution Client is not installed!

    +

    Please, select and sync a client from the Stakers tab.

    + Set Execution Client +
    +
    + + + +

    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) => ( + +
    + + + ))} + + + + + + + + + +

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

    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 +
    +
    +
    +
    + + ); +}; 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..46c1c315 --- /dev/null +++ b/dappnode/utils/dappnode-docs-urls.ts @@ -0,0 +1,13 @@ +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', + changeRPC: baseUrl + 'overview#execution-client-rpc', +}; 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(); +}; 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/env-dynamics.mjs b/env-dynamics.mjs index a40c198d..c8396d1b 100644 --- a/env-dynamics.mjs +++ b/env-dynamics.mjs @@ -25,7 +25,7 @@ export const supportedChains = process.env?.SUPPORTED_CHAINS?.split(',').map( (chainId) => parseInt(chainId, 10), ) ?? [560048]; /** @type string */ -export const walletconnectProjectId = process.env.WALLETCONNECT_PROJECT_ID; +export const walletconnectProjectId = 'd3f589c93f2a2de300741fcd71ed226b'; // DAPPNOPDE: process.env.WALLETCONNECT_PROJECT_ID /** @type boolean */ export const ipfsMode = toBoolean(process.env.IPFS_MODE); 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/performance-1.md b/faq/performance-1.md new file mode 100644 index 00000000..5b60d56e --- /dev/null +++ b/faq/performance-1.md @@ -0,0 +1,7 @@ +--- +title: What is the Lido threshold? +--- + +The Lido threshold is the value that determines whether a validator should receive rewards or not. It is calculated with the average of all the efficiencies (attestation rates) of all validators. + +Validators with an efficiency higher than the threshold will get rewards, while those below it won’t. diff --git a/faq/performance-2.md b/faq/performance-2.md new file mode 100644 index 00000000..794b1e49 --- /dev/null +++ b/faq/performance-2.md @@ -0,0 +1,5 @@ +--- +title: Where does the data come from? +--- + +We obtain the performance data of all Lido operators through its Smart Contract. The Lido CSM team distributes reports from all validators via IPFS hashes and Lido CSM package proccess it. Since this data is provided by Lido, is crucial in determining whether your validators qualify for rewards. diff --git a/faq/performance-3.md b/faq/performance-3.md new file mode 100644 index 00000000..1b5c7d79 --- /dev/null +++ b/faq/performance-3.md @@ -0,0 +1,5 @@ +--- +title: How often is the data updated? +--- + +The Lido CSM team distributes a new report every 28 days, so it can take up to almost a monthly delay when checking your current performance compared to other Lido operators. diff --git a/faq/performance-4.md b/faq/performance-4.md new file mode 100644 index 00000000..e3bcc0a6 --- /dev/null +++ b/faq/performance-4.md @@ -0,0 +1,5 @@ +--- +title: Where is this data stored? +--- + +The data collected from the Lido Smart Contract is stored in the Dappnode Lido package. Note that this data will include all samples from the validator; so the historical data from before the installation will be available. diff --git a/faq/performance-5.md b/faq/performance-5.md new file mode 100644 index 00000000..98eb5ad8 --- /dev/null +++ b/faq/performance-5.md @@ -0,0 +1,5 @@ +--- +title: Is the data accurate? +--- + +To calculate the efficiency, which is used to compare with the Lido threshold, we rely on data from the Lido Smart Contract. This source is considered 100% accurate since its the data that will be used by Lido when allocating the rewards. 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. diff --git a/faq/testnet-performance-1.md b/faq/testnet-performance-1.md new file mode 100644 index 00000000..5b60d56e --- /dev/null +++ b/faq/testnet-performance-1.md @@ -0,0 +1,7 @@ +--- +title: What is the Lido threshold? +--- + +The Lido threshold is the value that determines whether a validator should receive rewards or not. It is calculated with the average of all the efficiencies (attestation rates) of all validators. + +Validators with an efficiency higher than the threshold will get rewards, while those below it won’t. diff --git a/faq/testnet-performance-2.md b/faq/testnet-performance-2.md new file mode 100644 index 00000000..794b1e49 --- /dev/null +++ b/faq/testnet-performance-2.md @@ -0,0 +1,5 @@ +--- +title: Where does the data come from? +--- + +We obtain the performance data of all Lido operators through its Smart Contract. The Lido CSM team distributes reports from all validators via IPFS hashes and Lido CSM package proccess it. Since this data is provided by Lido, is crucial in determining whether your validators qualify for rewards. diff --git a/faq/testnet-performance-3.md b/faq/testnet-performance-3.md new file mode 100644 index 00000000..75d4a96a --- /dev/null +++ b/faq/testnet-performance-3.md @@ -0,0 +1,5 @@ +--- +title: How often is the data updated? +--- + +The Lido CSM team distributes a new report every 7 days, so it can take up to a weekly delay when checking your current performance compared to other Lido operators. diff --git a/faq/testnet-performance-4.md b/faq/testnet-performance-4.md new file mode 100644 index 00000000..e3bcc0a6 --- /dev/null +++ b/faq/testnet-performance-4.md @@ -0,0 +1,5 @@ +--- +title: Where is this data stored? +--- + +The data collected from the Lido Smart Contract is stored in the Dappnode Lido package. Note that this data will include all samples from the validator; so the historical data from before the installation will be available. diff --git a/faq/testnet-performance-5.md b/faq/testnet-performance-5.md new file mode 100644 index 00000000..98eb5ad8 --- /dev/null +++ b/faq/testnet-performance-5.md @@ -0,0 +1,5 @@ +--- +title: Is the data accurate? +--- + +To calculate the efficiency, which is used to compare with the Lido threshold, we rely on data from the Lido Smart Contract. This source is considered 100% accurate since its the data that will be used by Lido when allocating the rewards. 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/context/use-submit-keys-submit.ts b/features/create-node-operator/submit-keys-form/context/use-submit-keys-submit.ts index 0a171f81..2fcc9779 100644 --- a/features/create-node-operator/submit-keys-form/context/use-submit-keys-submit.ts +++ b/features/create-node-operator/submit-keys-form/context/use-submit-keys-submit.ts @@ -22,10 +22,11 @@ import { getAddedNodeOperator, runWithTransactionLogger, } from 'utils'; -import { Address } from 'wagmi'; +import { Address, useAccount } from 'wagmi'; import { useConfirmCustomAddressesModal } from '../hooks/use-confirm-modal'; import { useTxModalStagesSubmitKeys } from '../hooks/use-tx-modal-stages-submit-keys'; import { SubmitKeysFormInputType, SubmitKeysFormNetworkData } from './types'; +import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls'; type SubmitKeysOptions = { onConfirm?: () => Promise | void; @@ -128,6 +129,26 @@ export const useSubmitKeysSubmit = ({ const confirmCustomAddresses = useConfirmCustomAddressesModal(); const { ask } = useAskHowDidYouLearnCsm(); + // DAPPNODE + const { backendUrl } = useDappnodeUrls(); + const { address } = useAccount(); + /** + * `scanEvents` is required to trigger a re-scan of events on the backend after a new node operator is added. + * By default, the backend does not re-scan events if the address is already indexed and the block difference + * since last scann is less than 320 blocks. + */ + const scanEvents = useCallback(async () => { + const url = `${backendUrl}/api/v0/events_indexer/address_events?address=${address}&force=${true}`; + const options = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }; + await fetch(url, options); + }, [backendUrl, address]); + // DAPPNODE + return useCallback( async ( { @@ -198,6 +219,14 @@ export const useSubmitKeysSubmit = ({ waitTx, ); + // DAPPNODE + await scanEvents().catch((e) => { + console.error( + `Failed to trigger forced events re-scan after creating new NO: ${e}`, + ); + }); + // DAPPNODE + const nodeOperator = getAddedNodeOperator(receipt); txModalStages.success( @@ -240,6 +269,7 @@ export const useSubmitKeysSubmit = ({ isUserOrZero, onRetry, ask, + scanEvents, // DAPPNODE ], ); }; 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/create-node-operator/submit-keys-form/hooks/use-tx-modal-stages-submit-keys.tsx b/features/create-node-operator/submit-keys-form/hooks/use-tx-modal-stages-submit-keys.tsx index 2ea4e96c..56c363c7 100644 --- a/features/create-node-operator/submit-keys-form/hooks/use-tx-modal-stages-submit-keys.tsx +++ b/features/create-node-operator/submit-keys-form/hooks/use-tx-modal-stages-submit-keys.tsx @@ -14,6 +14,8 @@ import { TxStageSuccess, } from 'shared/transaction-modal/tx-stages-basic'; import { NodeOperatorId } from 'types'; +import { Button } from '@lidofinance/lido-ui'; +import { PATH } from 'consts/urls'; type Props = { keysCount: number; @@ -79,6 +81,16 @@ const getTxModalStagesSubmitKeys = (

    +
    + ) : undefined } diff --git a/features/dashboard/dashboard.tsx b/features/dashboard/dashboard.tsx index 8abdeb5d..f85174bf 100644 --- a/features/dashboard/dashboard.tsx +++ b/features/dashboard/dashboard.tsx @@ -6,12 +6,18 @@ import { ExternalSection } from './external'; import { getConfig } from 'config'; import { CHAINS } from 'consts/chains'; import { SurveysCta } from './surveys-cta'; +import { StatusSection } from 'dappnode/status/status-section'; +import { NotificationsModal } from 'dappnode/notifications/notifications-modal'; const { defaultChain } = getConfig(); export const Dashboard: FC = () => { return ( <> + {/* DAPPNODE */} + + + diff --git a/features/starter-pack/stacter-pack-section/starter-pack-section.tsx b/features/starter-pack/stacter-pack-section/starter-pack-section.tsx index 16d6ffa1..ad8f8a5c 100644 --- a/features/starter-pack/stacter-pack-section/starter-pack-section.tsx +++ b/features/starter-pack/stacter-pack-section/starter-pack-section.tsx @@ -1,75 +1,30 @@ -import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events'; import { FC, PropsWithChildren } from 'react'; -import { MatomoLink } from 'shared/components'; -import { Partners } from './partners'; -import { Step } from './step'; -import { BlockStyled, Heading, Steps } from './styles'; -import { RequiredBondAmount } from './required-bond-amount'; -import { - ABOUT_DEPOSIT_DATA_LINK, - HOW_TO_GENERATE_DEPOSIT_DATA_LINK, - PREPARE_HARDWARE_LINK, -} from 'consts/external-links'; +import { BlockStyled, Heading } from './styles'; +import { Link } from '@lidofinance/lido-ui'; +import { dappnodeLidoDocsUrls } from 'dappnode/utils/dappnode-docs-urls'; +import { Steps } from 'dappnode/starter-pack/steps'; -export const StarterPackSection: FC = ({ children }) => ( - - -

    CSM node operator starter pack

    -

    - Make sure you’ve completed all the basic steps before joining the{' '} - +export const StarterPackSection: FC = ({ children }) => { + return ( + + +

    CSM in Dappnode starter pack

    +

    + Make sure you've completed all the basic steps before joining the Community Staking Module - -

    -
    - - - (stETH / wstETH equivalent) is required for the - first validator -
    - - Learn more - -
    - - Run{' '} - - your own hardware - {' '} - or use a cloud provider - - - Do it{' '} - - manually - {' '} - or use Plug&Play solutions - - - - Prepare deposit data (.json file) for submitting keys -
    - Follow the{' '} - - generation guide - -
    -
    - {children} -
    -); +

    +

    + You can follow a guide in{' '} + + {' '} + Dappnode's Documentation + +

    + + + + + {children} + + ); +}; diff --git a/features/starter-pack/starter-pack.tsx b/features/starter-pack/starter-pack.tsx index 0649e5dd..d56b70c4 100644 --- a/features/starter-pack/starter-pack.tsx +++ b/features/starter-pack/starter-pack.tsx @@ -1,13 +1,8 @@ -import { Button } from '@lidofinance/lido-ui'; -import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events'; -import { PATH } from 'consts/urls'; import { TryCSM } from 'features/welcome/try-csm'; -import Link from 'next/link'; -import { FC, useCallback } from 'react'; +import { FC } from 'react'; import { Faq } from 'shared/components'; import { useCsmEarlyAdoption } from 'shared/hooks'; import { useCsmPaused, useCsmPublicRelease } from 'shared/hooks/useCsmStatus'; -import { trackMatomoEvent } from 'utils'; import { ConsumedBanner } from './consumed-banner'; import { NotEligibleBanner } from './not-eligible-banner/not-eligible-banner'; import { PausedBanner } from './paused-banner'; @@ -18,17 +13,7 @@ export const StarterPack: FC = () => { const { data: isPublicRelease } = useCsmPublicRelease(); const { data: ea } = useCsmEarlyAdoption(); - const handleClick = useCallback(() => { - trackMatomoEvent(MATOMO_CLICK_EVENTS_TYPES.starterPackCreateNodeOperator); - }, []); - - let content = ( - - - - - - ); + let content = ; if (!isPublicRelease && ea?.consumed) { content = ; 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 17ffce5c..19ab25ee 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 Hoodi + {/* DAPPNODE */} + + + 'keys-8', 'keys-9', 'keys-10', - 'keys-13', 'keys-11', - 'keys-12', ]); export const getFaqBond = () => @@ -84,3 +82,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', + ]); diff --git a/next.config.mjs b/next.config.mjs index b6f98bcc..76296fcf 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 [ { @@ -182,5 +246,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), + ), }, }); diff --git a/package.json b/package.json index b8a57909..6b789679 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "react-hook-form": "^7.52.1", "react-is": "^18.3.1", "react-transition-group": "^4.4.2", + "recharts": "^2.15.1", "reef-knot": "^4.2.0", "remark": "^13.0.0", "remark-external-links": "^8.0.0", @@ -100,6 +101,7 @@ "@types/nprogress": "^0.2.0", "@types/react": "^18.2.22", "@types/react-transition-group": "^4.4.3", + "@types/recharts": "^1.8.29", "@types/styled-components": "^5.1.23", "@types/styled-system": "^5.1.17", "@types/uuid": "^8.3.2", diff --git a/pages/_app.tsx b/pages/_app.tsx index 1b12e12b..b586c269 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -3,7 +3,7 @@ import Head from 'next/head'; import 'nprogress/nprogress.css'; import { memo } from 'react'; -import { CookiesTooltip, ToastContainer } from '@lidofinance/lido-ui'; +import { ToastContainer } from '@lidofinance/lido-ui'; // DAPPNODE: removed {CookiesTooltip} import { config, SecretConfigType } from 'config'; import { withCsp } from 'config/csp'; @@ -55,7 +55,7 @@ const AppWrapper = (props: AppProps): JSX.Element => { /> - + {/* DAPPNODE */} ); diff --git a/pages/index.tsx b/pages/index.tsx index 43570a11..e295be65 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/pages/notifications/index.ts b/pages/notifications/index.ts new file mode 100644 index 00000000..8cac7c55 --- /dev/null +++ b/pages/notifications/index.ts @@ -0,0 +1,3 @@ +//DAPPNODE +import Notifications from 'dappnode/notifications/index'; +export default Notifications; diff --git a/pages/performance/index.ts b/pages/performance/index.ts new file mode 100644 index 00000000..d037c8b4 --- /dev/null +++ b/pages/performance/index.ts @@ -0,0 +1,3 @@ +//DAPPNODE +import Performance from 'dappnode/performance/index'; +export default Performance; diff --git a/providers/modify-provider.tsx b/providers/modify-provider.tsx index 8bc5cb76..f9cde95e 100644 --- a/providers/modify-provider.tsx +++ b/providers/modify-provider.tsx @@ -9,17 +9,15 @@ import { useEffect, useMemo, } from 'react'; -import { useSearchParams, useSessionStorage } from 'shared/hooks'; +import { useSessionStorage } from 'shared/hooks'; import invariant from 'tiny-invariant'; -import { compareLowercase, trackMatomoEvent } from 'utils'; +import { trackMatomoEvent } from 'utils'; import { Address } from 'wagmi'; type ModifyContextValue = { referrer?: Address; }; -const QUERY_REFERRER = 'ref'; - const ModifyContext = createContext(null); ModifyContext.displayName = 'ModifyContext'; @@ -38,23 +36,14 @@ export const ModifyProvider: FC = ({ children }) => { undefined, ); - const query = useSearchParams(); - useEffect(() => { - if (!query) return; - - const refParam = query?.get(QUERY_REFERRER) ?? undefined; - - const ref = - REF_MAPPING.find(({ ref }) => compareLowercase(ref, refParam))?.address || - refParam; + const ref = REF_MAPPING.find(({ ref }) => ref === 'dappnode')?.address; // DAPPNODE if (ref && ref !== referrer && isAddress(ref)) { setReferrer(ref); - trackMatomoEvent(MATOMO_CLICK_EVENTS_TYPES.visitWithReferrer); } - }, [query, referrer, setReferrer]); + }, [referrer, setReferrer]); const value: ModifyContextValue = useMemo( () => ({ diff --git a/providers/node-operator-provider/use-get-active-node-operator.ts b/providers/node-operator-provider/use-get-active-node-operator.ts index afd5eab5..bd2237c3 100644 --- a/providers/node-operator-provider/use-get-active-node-operator.ts +++ b/providers/node-operator-provider/use-get-active-node-operator.ts @@ -1,10 +1,14 @@ import { useCallback, useEffect, useState } from 'react'; import { NodeOperator, NodeOperatorId } from 'types'; import { useCachedId } from './use-cached-id'; +// DAPPNODE +import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls'; export const useGetActiveNodeOperator = (list?: NodeOperator[]) => { 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/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 }) => ( 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/hooks/use-show-rule.ts b/shared/hooks/use-show-rule.ts index 2f259cce..79640cfb 100644 --- a/shared/hooks/use-show-rule.ts +++ b/shared/hooks/use-show-rule.ts @@ -1,4 +1,5 @@ import { getExternalLinks } from 'consts/external-links'; +import { useECSanityCheck } from 'dappnode/hooks/use-ec-sanity-check'; import { useNodeOperatorContext } from 'providers/node-operator-provider'; import { useCallback } from 'react'; import { @@ -19,10 +20,16 @@ export type ShowRule = | 'HAS_LOCKED_BOND' | 'CAN_CREATE' | 'EL_STEALING_REPORTER' - | 'IS_SURVEYS_ACTIVE'; + | 'IS_SURVEYS_ACTIVE' + // DAPPNODE + | 'IS_EXECUTION_LOADING' + | 'IS_EXECUTION_INSTALLED' + | 'IS_EXECUTION_SYNCED' + | 'EXECUTION_HAS_LOGS'; const { surveyApi } = getExternalLinks(); + export const useShowRule = () => { const { active: isConnectedWallet } = useAccount(); const { active: nodeOperator } = useNodeOperatorContext(); @@ -31,6 +38,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) { @@ -54,6 +64,13 @@ export const useShowRule = () => { return !!isReportingRole; case 'IS_SURVEYS_ACTIVE': return !!nodeOperator && !!surveyApi; + // DAPPNODE + case 'IS_EXECUTION_INSTALLED': + return isInstalled; + case 'IS_EXECUTION_SYNCED': + return isSynced; + case 'EXECUTION_HAS_LOGS': + return hasLogs; default: return false; } @@ -65,6 +82,9 @@ export const useShowRule = () => { invites?.length, lockedBond, isReportingRole, + isInstalled, + isSynced, + hasLogs, ], ); }; diff --git a/shared/layout/footer/styles.tsx b/shared/layout/footer/styles.tsx index 1fa8b9c3..60b9b899 100644 --- a/shared/layout/footer/styles.tsx +++ b/shared/layout/footer/styles.tsx @@ -12,7 +12,7 @@ export const FooterStyle = styled(Container)` flex-wrap: wrap; column-gap: 32px; - width: 100%; + min-width: 100%; // DAPPNODE max-width: 1424px; padding: 24px 32px; diff --git a/shared/layout/header/components/navigation/styles.tsx b/shared/layout/header/components/navigation/styles.tsx index 7adab00c..55768d17 100644 --- a/shared/layout/header/components/navigation/styles.tsx +++ b/shared/layout/header/components/navigation/styles.tsx @@ -19,10 +19,10 @@ const mobileCss = css` bottom: 0; left: 0; right: 0; - padding: 8px; + padding: 2px; // DAPPNODE background-color: var(--lido-color-foreground); display: flex; - gap: 32px; + gap: 10px; // DAPPNODE justify-content: space-around; align-items: center; border-top: 1px solid var(--lido-color-border); diff --git a/shared/layout/header/components/navigation/use-nav-items.tsx b/shared/layout/header/components/navigation/use-nav-items.tsx index 2d5d8549..62eced8c 100644 --- a/shared/layout/header/components/navigation/use-nav-items.tsx +++ b/shared/layout/header/components/navigation/use-nav-items.tsx @@ -16,6 +16,10 @@ import { } from 'shared/counters'; import { ShowRule, useShowRule } from 'shared/hooks'; +//DAPPNODE +import { ReactComponent as AlertIcon } from 'assets/icons/bell.svg'; +import { ReactComponent as StarIcon } from 'assets/icons/star.svg'; + export type Route = { name: string; path: PATH; @@ -43,7 +47,8 @@ const routes: Route[] = [ path: PATH.KEYS, icon: , subPaths: [PATH.KEYS_SUBMIT, PATH.KEYS_REMOVE, PATH.KEYS_VIEW, PATH.CREATE], - showRules: ['IS_NODE_OPERATOR', 'CAN_CREATE'], + //DAPPNODE: remove 'CAN_CREATE' from showRules, + showRules: ['IS_NODE_OPERATOR'], suffix: , }, { @@ -76,6 +81,20 @@ const routes: Route[] = [ showRules: ['IS_SURVEYS_ACTIVE'], suffix: , }, + // DAPPNODE + { + name: 'Performance', + path: PATH.PERFORMANCE, + icon: , + showRules: ['IS_NODE_OPERATOR'], + }, + { + name: 'Notifications', + path: PATH.NOTIFICATIONS, + icon: , + showRules: ['IS_NODE_OPERATOR'], + suffix: , + }, ]; export const useNavItems = () => { diff --git a/shared/layout/header/styles.tsx b/shared/layout/header/styles.tsx index 466b591e..96e1437d 100644 --- a/shared/layout/header/styles.tsx +++ b/shared/layout/header/styles.tsx @@ -23,6 +23,7 @@ export const HeaderContentStyle = styled.div` export const HeaderStyle = styled((props: ContainerProps) => ( ))` + min-width: 100%; // DAPPNODE position: relative; padding-top: 18px; padding-bottom: 18px; diff --git a/shared/navigate/gates/gate-loaded.tsx b/shared/navigate/gates/gate-loaded.tsx index 9fc2bce2..110380e3 100644 --- a/shared/navigate/gates/gate-loaded.tsx +++ b/shared/navigate/gates/gate-loaded.tsx @@ -3,6 +3,7 @@ import { FC, PropsWithChildren, ReactNode } from 'react'; import { useAccount, useCsmPaused, useCsmPublicRelease } from 'shared/hooks'; import { useCsmEarlyAdoption } from 'shared/hooks/useCsmEarlyAdoption'; import { SplashPage } from '../splash'; +import { ECScanningPage } from 'dappnode/fallbacks/ec-scanning-events'; type Props = { fallback?: ReactNode; @@ -10,7 +11,6 @@ type Props = { }; export const GateLoaded: FC> = ({ - fallback = , additional, children, }) => { @@ -20,6 +20,9 @@ export const GateLoaded: FC> = ({ const { isListLoading, active } = useNodeOperatorContext(); const { initialLoading: isEaLoading } = useCsmEarlyAdoption(); + // DAPPNODE + const fallback = isListLoading ? : ; + const loading = isPublicReleaseLoading || isPausedLoading || diff --git a/shared/transaction-modal/tx-stages-parts/after-keys-upload.tsx b/shared/transaction-modal/tx-stages-parts/after-keys-upload.tsx index e98b1e9a..83173537 100644 --- a/shared/transaction-modal/tx-stages-parts/after-keys-upload.tsx +++ b/shared/transaction-modal/tx-stages-parts/after-keys-upload.tsx @@ -3,25 +3,24 @@ import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events'; import { PATH } from 'consts/urls'; import { FC } from 'react'; import { MatomoLink } from 'shared/components'; -import { useBeaconchainDashboardLink } from 'shared/hooks'; import { LocalLink } from 'shared/navigate'; import styled from 'styled-components'; import { NodeOperatorId } from 'types'; +import Link from 'next/link'; type Props = { nodeOperatorId?: NodeOperatorId; keys: string[]; }; -export const AfterKeysUpload: FC = ({ keys }) => { - const beaconchainDashboardLink = useBeaconchainDashboardLink(keys); - const { subscribeEvents, beaconchain } = getExternalLinks(); +export const AfterKeysUpload: FC = () => { + const { beaconchain } = getExternalLinks(); return ( What is next:
      -
    1. Wait for your keys to be deposited to through the protocol.
    2. +
    3. Wait for your keys to be deposited through the protocol.
    4. Once your keys become active (check the status on the{' '} = ({ keys }) => { > Keys tab - {beaconchain && ( - <> - , on{' '} - - beaconcha.in - - - )}{' '} - or subscribe to the{' '} + , on{' '} - CSM events notifications - - ) make sure your validators are producing attestations{' '} - {beaconchainDashboardLink && ( - <> - (you can use{' '} - - beaconcha.in dashboard - {' '} - to check) - - )} + beaconcha.in + {' '} + or subscribe to the + {/* DAPPNODE */} + CSM Telegram notifications) + make sure your validators are producing attestations.
    diff --git a/yarn.lock b/yarn.lock index a4187705..1024e510 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3789,6 +3789,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" @@ -3964,6 +4027,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" @@ -5460,6 +5531,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" @@ -5777,6 +5853,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" @@ -5846,6 +5993,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" @@ -6673,7 +6825,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== @@ -6780,6 +6932,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" @@ -7457,6 +7614,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" @@ -9889,6 +10051,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" @@ -9896,7 +10067,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== @@ -9969,6 +10140,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" @@ -11011,7 +11203,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== @@ -11595,6 +11787,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 c439bc53430ea6a744b49c4ed01719fecd1d67b6 Mon Sep 17 00:00:00 2001 From: pablomendezroyo <41727368+pablomendezroyo@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:39:42 +0100 Subject: [PATCH 32/42] Add missing envs and fix csp (#21) * fix: missing envs * fix: add miing trusted hosts --- Dockerfile | 6 ++++-- next.config.mjs | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4311ef82..2e9b1d1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile --non-interactive --ignore-scripts && yarn cache clean COPY . . -RUN NODE_NO_BUILD_DYNAMICS=true yarn typechain && yarn build +RUN NODE_NO_BUILD_DYNAMICS=true NODE_ENV=production yarn typechain && yarn build # public/runtime is used to inject runtime vars; it should exist and user node should have write access there for it RUN rm -rf /app/public/runtime && mkdir /app/public/runtime && chown node /app/public/runtime @@ -20,7 +20,9 @@ ARG BASE_PATH="" #ARG DEFAULT_CHAIN="1" ENV NEXT_TELEMETRY_DISABLED=1 \ - BASE_PATH=$BASE_PATH + BASE_PATH=$BASE_PATH \ + PORT=80 \ + NODE_ENV=production #SUPPORTED_CHAINS=$SUPPORTED_CHAINS \ #DEFAULT_CHAIN=$DEFAULT_CHAIN diff --git a/next.config.mjs b/next.config.mjs index 76296fcf..05dfbe74 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -234,7 +234,8 @@ export default withBundleAnalyzer({ clApiUrls_17000: process.env.CL_API_URLS_17000, clApiUrls_560048: process.env.CL_API_URLS_560048, - cspTrustedHosts: process.env.CSP_TRUSTED_HOSTS, + // DAPPNODE + cspTrustedHosts: 'https://*.lido.fi,http://*.dappnode', cspReportUri: process.env.CSP_REPORT_URI, cspReportOnly: process.env.CSP_REPORT_ONLY, From f896d32aca0cadeafe2b87297cf4ea11c5fad164 Mon Sep 17 00:00:00 2001 From: pablomendezroyo <41727368+pablomendezroyo@users.noreply.github.com> Date: Tue, 18 Feb 2025 18:30:55 +0100 Subject: [PATCH 33/42] Update CSP (#22) --- next.config.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/next.config.mjs b/next.config.mjs index 05dfbe74..11c5b409 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -185,6 +185,11 @@ export default withBundleAnalyzer({ // Apply these headers to all routes in your application. source: '/(.*)', headers: [ + // DAPPNODE + { + key: "Content-Security-Policy", + value: `connect-src 'self' https: wss: http: http://execution.holesky.dncore.dappnode:8545 http://execution.mainnet.dncore.dappnode:8545 http://lido-events.lido-csm-holesky.dappnode:8080 http://lido-events.lido-csm-mainnet.dappnode:8080 http://brain.web3signer-holesky.dappnode http://brain.web3signer.dappnode;`, + }, { key: 'X-DNS-Prefetch-Control', value: 'on', From b6612efe402f3ea5f70f38cf35214e92de42ba51 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Tue, 18 Feb 2025 18:50:13 +0100 Subject: [PATCH 34/42] fix: relocate node_env --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2e9b1d1d..a97fd91f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile --non-interactive --ignore-scripts && yarn cache clean COPY . . -RUN NODE_NO_BUILD_DYNAMICS=true NODE_ENV=production yarn typechain && yarn build +RUN NODE_NO_BUILD_DYNAMICS=true yarn typechain && NODE_ENV=production yarn build # public/runtime is used to inject runtime vars; it should exist and user node should have write access there for it RUN rm -rf /app/public/runtime && mkdir /app/public/runtime && chown node /app/public/runtime From 6437cae02cf6134b1d0b8f5c60905dfe0db2a31a Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Tue, 18 Feb 2025 19:58:13 +0100 Subject: [PATCH 35/42] feat: allow any http connection from internal network --- config/csp/index.ts | 1 + next.config.mjs | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/config/csp/index.ts b/config/csp/index.ts index 888fdb4e..05c6a9f1 100644 --- a/config/csp/index.ts +++ b/config/csp/index.ts @@ -37,6 +37,7 @@ export const contentSecurityPolicy: ContentSecurityPolicyOption = { connectSrc: [ "'self'", 'https:', + 'http:', 'wss:', ...(config.developmentMode ? ['ws:'] : []), // for HMR ], diff --git a/next.config.mjs b/next.config.mjs index 11c5b409..05dfbe74 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -185,11 +185,6 @@ export default withBundleAnalyzer({ // Apply these headers to all routes in your application. source: '/(.*)', headers: [ - // DAPPNODE - { - key: "Content-Security-Policy", - value: `connect-src 'self' https: wss: http: http://execution.holesky.dncore.dappnode:8545 http://execution.mainnet.dncore.dappnode:8545 http://lido-events.lido-csm-holesky.dappnode:8080 http://lido-events.lido-csm-mainnet.dappnode:8080 http://brain.web3signer-holesky.dappnode http://brain.web3signer.dappnode;`, - }, { key: 'X-DNS-Prefetch-Control', value: 'on', From bcd453204e79d6305391936a5475228fcc43d97e Mon Sep 17 00:00:00 2001 From: pablomendezroyo <41727368+pablomendezroyo@users.noreply.github.com> Date: Tue, 18 Feb 2025 22:32:09 +0100 Subject: [PATCH 36/42] feat: use dappnode urls for rpc and cc (#23) --- config/rpc/cl.ts | 21 ++++++++++++++++++--- config/rpc/index.ts | 29 ++++++++++++++++++++++++++--- dappnode/hooks/use-dappnode-urls.ts | 6 ++++++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/config/rpc/cl.ts b/config/rpc/cl.ts index 14f0e210..159952db 100644 --- a/config/rpc/cl.ts +++ b/config/rpc/cl.ts @@ -3,7 +3,7 @@ import invariant from 'tiny-invariant'; import { useSDK } from '@lido-sdk/react'; import { CHAINS } from 'consts/chains'; -import { API_ROUTES } from 'consts/api'; +//import { API_ROUTES } from 'consts/api'; // Don't use absolute import here! // code''' @@ -15,8 +15,23 @@ import { config } from '../get-config'; import { useUserConfig } from '../user-config'; export const getBackendApiPath = (chainId: string | number): string => { - const BASE_URL = typeof window === 'undefined' ? '' : window.location.origin; - return `${BASE_URL}/${API_ROUTES.CL}/${chainId}`; + // const BASE_URL = typeof window === 'undefined' ? '' : window.location.origin; + // return `${BASE_URL}/${API_ROUTES.CL}/${chainId}`; + + // DAPPNODE + if (typeof chainId === 'string') { + if (parseInt(chainId) === CHAINS.Mainnet) { + return 'http://beacon-chain.mainnet.dncore.dappnode:3500'; + } else { + return 'http://beacon-chain.holesky.dncore.dappnode:3500'; + } + } else { + if (chainId === CHAINS.Mainnet) { + return 'http://beacon-chain.mainnet.dncore.dappnode:3500'; + } else { + return 'http://beacon-chain.holesky.dncore.dappnode:3500'; + } + } }; export const useGetClApiUrlByChainId = () => { diff --git a/config/rpc/index.ts b/config/rpc/index.ts index 93fa8c8e..4895136d 100644 --- a/config/rpc/index.ts +++ b/config/rpc/index.ts @@ -3,7 +3,7 @@ import invariant from 'tiny-invariant'; import { useSDK } from '@lido-sdk/react'; import { CHAINS } from 'consts/chains'; -import { API_ROUTES } from 'consts/api'; +//import { API_ROUTES } from 'consts/api'; // Don't use absolute import here! // code''' @@ -15,8 +15,31 @@ import { config } from '../get-config'; import { useUserConfig } from '../user-config'; export const getBackendRPCPath = (chainId: string | number): string => { - const BASE_URL = typeof window === 'undefined' ? '' : window.location.origin; - return `${BASE_URL}/${API_ROUTES.RPC}?chainId=${chainId}`; + // const BASE_URL = typeof window === 'undefined' ? '' : window.location.origin; + // return `${BASE_URL}/${API_ROUTES.RPC}?chainId=${chainId}`; + + // DAPPNODE + if (typeof chainId === 'string') { + if (parseInt(chainId) === CHAINS.Mainnet) { + return ( + config.rpcUrls_17000 || 'http://execution.mainnet.dncore.dappnode:8545' + ); + } else { + return ( + config.rpcUrls_1 || 'http://execution.holesky.dncore.dappnode:8545' + ); + } + } else { + if (chainId === CHAINS.Mainnet) { + return ( + config.rpcUrls_17000 || 'http://execution.mainnet.dncore.dappnode:8545' + ); + } else { + return ( + config.rpcUrls_1 || 'http://execution.holesky.dncore.dappnode:8545' + ); + } + } }; export const useGetRpcUrlByChainId = () => { diff --git a/dappnode/hooks/use-dappnode-urls.ts b/dappnode/hooks/use-dappnode-urls.ts index a934a6aa..5059313c 100644 --- a/dappnode/hooks/use-dappnode-urls.ts +++ b/dappnode/hooks/use-dappnode-urls.ts @@ -10,6 +10,7 @@ interface DappnodeUrls { stakersUiUrl: string; backendUrl: string; ECApiUrl: string; + CCApiUrl: string; CCVersionApiUrl: string; CCStatusApiUrl: string; keysStatusUrl: string; @@ -34,6 +35,7 @@ const useDappnodeUrls = () => { ECApiUrl: publicRuntimeConfig.rpcUrls_1 || 'http://execution.mainnet.dncore.dappnode:8545', + CCApiUrl: 'http://beacon-chain.mainnet.dncore.dappnode:3500', CCVersionApiUrl: '/api/consensus-version-mainnet', CCStatusApiUrl: '/api/consensus-status-mainnet', keysStatusUrl: '/api/keys-status-mainnet', @@ -54,6 +56,7 @@ const useDappnodeUrls = () => { ECApiUrl: publicRuntimeConfig.rpcUrls_17000 || 'http://execution.holesky.dncore.dappnode:8545', + CCApiUrl: 'http://beacon-chain.holesky.dncore.dappnode:3500', CCVersionApiUrl: '/api/consensus-version-holesky', CCStatusApiUrl: '/api/consensus-status-holesky', keysStatusUrl: '/api/keys-status-holesky', @@ -82,6 +85,8 @@ const useDappnodeUrls = () => { urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.backendUrl || ''; const ECApiUrl = urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.ECApiUrl || ''; + const CCApiUrl = + urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.CCApiUrl || ''; const CCVersionApiUrl = urlsByChain[publicRuntimeConfig.defaultChain as CHAINS]?.CCVersionApiUrl || ''; @@ -108,6 +113,7 @@ const useDappnodeUrls = () => { stakersUiUrl, backendUrl, ECApiUrl, + CCApiUrl, CCVersionApiUrl, CCStatusApiUrl, keysStatusUrl, From 5915d5af71007e61b981ff4a524fb6e9ca4cce57 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Wed, 19 Feb 2025 10:05:36 +0100 Subject: [PATCH 37/42] fix: rpc --- config/rpc/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/rpc/index.ts b/config/rpc/index.ts index 4895136d..e6d7472e 100644 --- a/config/rpc/index.ts +++ b/config/rpc/index.ts @@ -22,21 +22,21 @@ export const getBackendRPCPath = (chainId: string | number): string => { if (typeof chainId === 'string') { if (parseInt(chainId) === CHAINS.Mainnet) { return ( - config.rpcUrls_17000 || 'http://execution.mainnet.dncore.dappnode:8545' + config.rpcUrls_1 || 'http://execution.mainnet.dncore.dappnode:8545' ); } else { return ( - config.rpcUrls_1 || 'http://execution.holesky.dncore.dappnode:8545' + config.rpcUrls_17000 || 'http://execution.holesky.dncore.dappnode:8545' ); } } else { if (chainId === CHAINS.Mainnet) { return ( - config.rpcUrls_17000 || 'http://execution.mainnet.dncore.dappnode:8545' + config.rpcUrls_1 || 'http://execution.mainnet.dncore.dappnode:8545' ); } else { return ( - config.rpcUrls_1 || 'http://execution.holesky.dncore.dappnode:8545' + config.rpcUrls_17000 || 'http://execution.holesky.dncore.dappnode:8545' ); } } From bc26d2f4c3923570d3f7e9c7d237ece64fb3c080 Mon Sep 17 00:00:00 2001 From: pablomendezroyo <41727368+pablomendezroyo@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:31:28 +0100 Subject: [PATCH 38/42] fix: move notifications and performance index to pages (#24) --- pages/notifications/index.ts | 3 --- {dappnode => pages}/notifications/index.tsx | 0 pages/performance/index.ts | 3 --- {dappnode => pages}/performance/index.tsx | 0 4 files changed, 6 deletions(-) delete mode 100644 pages/notifications/index.ts rename {dappnode => pages}/notifications/index.tsx (100%) delete mode 100644 pages/performance/index.ts rename {dappnode => pages}/performance/index.tsx (100%) diff --git a/pages/notifications/index.ts b/pages/notifications/index.ts deleted file mode 100644 index 8cac7c55..00000000 --- a/pages/notifications/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -//DAPPNODE -import Notifications from 'dappnode/notifications/index'; -export default Notifications; diff --git a/dappnode/notifications/index.tsx b/pages/notifications/index.tsx similarity index 100% rename from dappnode/notifications/index.tsx rename to pages/notifications/index.tsx diff --git a/pages/performance/index.ts b/pages/performance/index.ts deleted file mode 100644 index d037c8b4..00000000 --- a/pages/performance/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -//DAPPNODE -import Performance from 'dappnode/performance/index'; -export default Performance; diff --git a/dappnode/performance/index.tsx b/pages/performance/index.tsx similarity index 100% rename from dappnode/performance/index.tsx rename to pages/performance/index.tsx From dd96012d0d3f33da488b804f14f2ef81974af073 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Wed, 19 Feb 2025 11:15:44 +0100 Subject: [PATCH 39/42] fix: use getServerSideProps in notifications and performance --- pages/notifications/index.tsx | 3 ++- pages/performance/index.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pages/notifications/index.tsx b/pages/notifications/index.tsx index b88a3d17..349f99c9 100644 --- a/pages/notifications/index.tsx +++ b/pages/notifications/index.tsx @@ -14,4 +14,5 @@ const Page = () => ( export default Page; -export const getStaticProps = getProps(getFaqNotifications); +// It must be getServerSideProps instead of getStaticProps because we need to check the maintenance status on every request. +export const getServerSideProps = getProps(getFaqNotifications); diff --git a/pages/performance/index.tsx b/pages/performance/index.tsx index 5abbabd9..0574107f 100644 --- a/pages/performance/index.tsx +++ b/pages/performance/index.tsx @@ -14,4 +14,5 @@ const Page = () => ( export default Page; -export const getStaticProps = getProps(getFaqPerformance); +// It must be getServerSideProps instead of getStaticProps because we need to check the maintenance status on every request. +export const getServerSideProps = getProps(getFaqPerformance); From 208e3a53917d16d88656b6304e1bb41a582a14c1 Mon Sep 17 00:00:00 2001 From: Mateu Miralles <52827122+mateumiralles@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:17:10 +0100 Subject: [PATCH 40/42] fix: overall fixes (#25) * fix: overall fixes * fix: ux fixes --- config/csp/index.ts | 2 +- .../hooks/use-get-performance-by-range.ts | 10 ++++++--- .../notifications/notifications-types.tsx | 2 +- dappnode/performance/components/styles.ts | 5 ++++- dappnode/starter-pack/steps.tsx | 4 ++-- features/welcome/try-csm/try-csm.tsx | 21 ++++++------------- shared/layout/header/header.tsx | 4 ++-- 7 files changed, 23 insertions(+), 25 deletions(-) diff --git a/config/csp/index.ts b/config/csp/index.ts index 05c6a9f1..deeec052 100644 --- a/config/csp/index.ts +++ b/config/csp/index.ts @@ -37,7 +37,7 @@ export const contentSecurityPolicy: ContentSecurityPolicyOption = { connectSrc: [ "'self'", 'https:', - 'http:', + 'http:', // DAPPNODE 'wss:', ...(config.developmentMode ? ['ws:'] : []), // for HMR ], diff --git a/dappnode/hooks/use-get-performance-by-range.ts b/dappnode/hooks/use-get-performance-by-range.ts index cc875e7b..d6fc320a 100644 --- a/dappnode/hooks/use-get-performance-by-range.ts +++ b/dappnode/hooks/use-get-performance-by-range.ts @@ -76,13 +76,17 @@ export const useGetPerformanceByRange = (range: Range) => { Object.entries(thresholdsData) .map(([_, value]) => { const endFrame = value.frame[1].toString(); - const lidoThreshold = value.threshold * 100; // Convert to percentage + const lidoThreshold = (value.threshold * 100).toFixed(4); // Convert to percentage with max 4 decimals 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 + acc[validatorId] = parseFloat( + ( + (validatorPerf.included / validatorPerf.assigned) * + 100 + ).toFixed(4), + ); // Convert to percentage with max 4 decimals return acc; }, {} as Record, diff --git a/dappnode/notifications/notifications-types.tsx b/dappnode/notifications/notifications-types.tsx index 963ed188..2006a564 100644 --- a/dappnode/notifications/notifications-types.tsx +++ b/dappnode/notifications/notifications-types.tsx @@ -7,7 +7,7 @@ const avaliableNotifications = { { title: 'Validator requires exit 🚨', value: - 'Your validator is automatically requested to exit due to certain conditions.', + 'One of your validators has been requested to exit. It will be done automatically', }, { title: 'Validator failed to exit, manual exit required 🚪', diff --git a/dappnode/performance/components/styles.ts b/dappnode/performance/components/styles.ts index a39fe171..4b3c7bd6 100644 --- a/dappnode/performance/components/styles.ts +++ b/dappnode/performance/components/styles.ts @@ -129,7 +129,10 @@ export const Dropdown = styled.div` box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); z-index: 10; - div { + button { + all: unset; + display: block; + width: 100%; padding: 8px 16px; cursor: pointer; &:hover { diff --git a/dappnode/starter-pack/steps.tsx b/dappnode/starter-pack/steps.tsx index 3601b9db..5b352d7f 100644 --- a/dappnode/starter-pack/steps.tsx +++ b/dappnode/starter-pack/steps.tsx @@ -84,8 +84,8 @@ const Step1: FC = ({ step, title, setStep }: StepsProps) => (
  • {' '}

    - 2 {publicRuntimeConfig.defaultChain === 17000 && 'Holesky'} ETH - (stETH / wstETH equivalent) is required for the first validator{' '} + {publicRuntimeConfig.defaultChain === 17000 ? '2 Holesky' : '2.4'}{' '} + ETH (stETH / wstETH equivalent) is required for the first validator{' '}

  • diff --git a/features/welcome/try-csm/try-csm.tsx b/features/welcome/try-csm/try-csm.tsx index 19ab25ee..a90ba9d8 100644 --- a/features/welcome/try-csm/try-csm.tsx +++ b/features/welcome/try-csm/try-csm.tsx @@ -1,11 +1,8 @@ import { Button, Text } from '@lidofinance/lido-ui'; import { getConfig } from 'config'; import { CHAINS } from 'consts/chains'; -import { CSM_MAINNET_LINK, CSM_TESTNET_LINK } from 'consts/external-links'; 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'; @@ -33,14 +30,6 @@ export const TryCSM: FC = () => { Join CSM Testnet - - - CSM uses Hoodi as a testnet playground for those who want to try the @@ -55,14 +44,16 @@ export const TryCSM: FC = () => { Try CSM on Mainnet - - + ); diff --git a/shared/layout/header/header.tsx b/shared/layout/header/header.tsx index 83b94b29..9a05fbf7 100644 --- a/shared/layout/header/header.tsx +++ b/shared/layout/header/header.tsx @@ -7,7 +7,7 @@ import HeaderChain from './components/header-chain'; import HeaderEaMember from './components/header-ea-member'; import HeaderNodeOperator from './components/header-node-operator'; import { HeaderSettingsButton } from './components/header-settings-button'; -import HeaderTheme from './components/header-theme'; +// import HeaderTheme from './components/header-theme'; import HeaderWallet from './components/header-wallet'; import { Navigation } from './components/navigation/navigation'; import { HeaderActionsStyle, HeaderContentStyle, HeaderStyle } from './styles'; @@ -23,7 +23,7 @@ export const Header: FC = () => ( {config.ipfsMode && } - + {/* // DAPPNODE*/} From c2b35c74f9a9f1b71dd02770415de2af191a7c70 Mon Sep 17 00:00:00 2001 From: Mateu Miralles <52827122+mateumiralles@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:26:19 +0100 Subject: [PATCH 41/42] Retrieving processingStarted events from backend (#27) * fix: processingStarted call via backend * fix: relocating dapppnode hook * fix: remove comments --- dappnode/hooks/useLastRewardsFrame-api.ts | 50 +++++++++++++++++++++++ features/dashboard/bond/last-rewards.tsx | 2 +- shared/hooks/useLastRewardsReport.ts | 1 + 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 dappnode/hooks/useLastRewardsFrame-api.ts diff --git a/dappnode/hooks/useLastRewardsFrame-api.ts b/dappnode/hooks/useLastRewardsFrame-api.ts new file mode 100644 index 00000000..96efb235 --- /dev/null +++ b/dappnode/hooks/useLastRewardsFrame-api.ts @@ -0,0 +1,50 @@ +import { useLidoSWR } from '@lido-sdk/react'; +import { STRATEGY_CONSTANT } from 'consts/swr-strategies'; +import { fetchWithRetry } from 'dappnode/utils/fetchWithRetry'; +import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls'; + +interface Event { + RefSlot: number; + Hash: number[]; + Raw: { + address: string; + topics: string[]; + data: string; + blockNumber: string; + transactionHash: string; + transactionIndex: string; + blockHash: string; + logIndex: string; + removed: boolean; + }; +} + +export const useLastRewrdsTx = (config = STRATEGY_CONSTANT) => { + const { backendUrl } = useDappnodeUrls(); + + return useLidoSWR( + ['fee-oracle-report-tx'], + async () => { + const url = `${backendUrl}/api/v0/events_indexer/processing_started`; + const options = { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }; + const response = await fetchWithRetry(url, options, 5000); + if (!response.ok) { + throw new Error('Failed to fetch processing started events'); + } + const events: Event[] = await response.json(); + const txs = events + .sort( + (a, b) => + parseInt(a.Raw.blockNumber, 16) - parseInt(b.Raw.blockNumber, 16), + ) + .map((event) => { + return event.Raw.transactionHash; + }); + return txs[txs.length - 1]; + }, + config, + ); +}; diff --git a/features/dashboard/bond/last-rewards.tsx b/features/dashboard/bond/last-rewards.tsx index 6169e010..a7253537 100644 --- a/features/dashboard/bond/last-rewards.tsx +++ b/features/dashboard/bond/last-rewards.tsx @@ -19,7 +19,6 @@ import { import { FaqElement } from 'shared/components/faq/styles'; import { useLastOperatorRewards, - useLastRewrdsTx, useNodeOperatorInfo, useRewardsFrame, useSharesToSteth, @@ -34,6 +33,7 @@ import { RowHeader, RowTitle, } from './styles'; +import { useLastRewrdsTx } from 'dappnode/hooks/useLastRewardsFrame-api'; // DAPPNODE export const LastRewards: FC = () => { const { data: lastRewards, initialLoading: isLoading } = diff --git a/shared/hooks/useLastRewardsReport.ts b/shared/hooks/useLastRewardsReport.ts index 90bc4ba6..aa8c3f7e 100644 --- a/shared/hooks/useLastRewardsReport.ts +++ b/shared/hooks/useLastRewardsReport.ts @@ -104,6 +104,7 @@ export const useLastOperatorRewards = () => { ); }; +// DAPPNODE: Replaced by 'dappnode/hooks/useLastRewardsFrame-api' export const useLastRewrdsTx = (config = STRATEGY_CONSTANT) => { const feeOracle = useCSFeeOracleRPC(); const { deploymentBlockNumber } = getCsmConstants(); From d69ee2e29a1b3257d7cadb91d37b7558a1717175 Mon Sep 17 00:00:00 2001 From: Mateu Miralles <52827122+mateumiralles@users.noreply.github.com> Date: Thu, 20 Mar 2025 12:19:38 +0100 Subject: [PATCH 42/42] fix: mainnet widrawal address in starter pack (#28) --- dappnode/starter-pack/steps.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dappnode/starter-pack/steps.tsx b/dappnode/starter-pack/steps.tsx index 5b352d7f..2c28dec0 100644 --- a/dappnode/starter-pack/steps.tsx +++ b/dappnode/starter-pack/steps.tsx @@ -198,8 +198,8 @@ const Step2: FC = ({ step, title, setStep }: StepsProps) => {
    You must have a synced{' '} - {publicRuntimeConfig.defaultChain as CHAINS} Node and run MEV - Boost. + {CHAINS[publicRuntimeConfig.defaultChain as CHAINS]} Node and + run MEV Boost.
    ))} @@ -395,7 +395,8 @@ const Step4: FC = ({ step, title, setStep }: StepsProps) => {

    Set {withdrawalByAddres} as the withdrawal address while - generating the keystores. This is the Lido Withdrawal Vault on Holesky{' '} + generating the keystores. This is the Lido Withdrawal Vault on{' '} + {CHAINS[publicRuntimeConfig.defaultChain as CHAINS]}

    Prepare your deposit data (.json file) for submitting your keys in the