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/.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/.gitignore b/.gitignore index 0a71a893..c3d52525 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ yarn-error.log* .idea /public/runtime/ +.qodo 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/config/get-secret-config.ts b/config/get-secret-config.ts index ca193c78..c402182b 100644 --- a/config/get-secret-config.ts +++ b/config/get-secret-config.ts @@ -10,14 +10,14 @@ 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; - subgraphRequestTimeout: number; - rateLimit: number; rateLimitTimeFrame: number; } @@ -33,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 [ @@ -44,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, @@ -51,12 +55,11 @@ 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), - 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/config/rpc/index.ts b/config/rpc/index.ts index e6d7472e..6c9b7d8b 100644 --- a/config/rpc/index.ts +++ b/config/rpc/index.ts @@ -24,6 +24,10 @@ export const getBackendRPCPath = (chainId: string | number): string => { return ( config.rpcUrls_1 || 'http://execution.mainnet.dncore.dappnode:8545' ); + } else if (parseInt(chainId) === CHAINS.Hoodi) { + return ( + config.rpcUrls_560048 || 'http://execution.hoodi.dncore.dappnode:8545' + ); } else { return ( config.rpcUrls_17000 || 'http://execution.holesky.dncore.dappnode:8545' @@ -34,6 +38,10 @@ export const getBackendRPCPath = (chainId: string | number): string => { return ( config.rpcUrls_1 || 'http://execution.mainnet.dncore.dappnode:8545' ); + } else if (chainId === CHAINS.Hoodi) { + return ( + config.rpcUrls_560048 || 'http://execution.hoodi.dncore.dappnode:8545' + ); } else { return ( config.rpcUrls_17000 || 'http://execution.holesky.dncore.dappnode:8545' 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/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/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 fd1fbe9a..0b14118b 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'; @@ -12,12 +13,13 @@ type CsmContract = | 'CSFeeOracle' | 'CSModule' | 'CSVerifier' + | 'HashConsensus' | 'ExitBusOracle' | 'StakingRouter'; type CsmConstants = { contracts: Record; - deploymentBlockNumber: HexString; + deploymentBlockNumber?: HexString; stakingModuleId: number; withdrawalCredentials: Address; retentionPeriodMins: number; @@ -26,7 +28,7 @@ type CsmConstants = { slotsPerFrame: number; }; -export const CONSTANTS_BY_NETWORK: Partial> = { +export const CONSTANTS_BY_NETWORK: Record = { [CHAINS.Mainnet]: { contracts: { CSAccounting: '0x4d72BFF1BeaC69925F8Bd12526a39BAAb069e5Da', @@ -35,6 +37,7 @@ export const CONSTANTS_BY_NETWORK: Partial> = { CSFeeOracle: '0x4D4074628678Bd302921c20573EEa1ed38DdF7FB', CSModule: '0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F', CSVerifier: '0x3Dfc50f22aCA652a0a6F28a0F892ab62074b5583', + HashConsensus: '0x71093efF8D8599b5fA340D665Ad60fA7C80688e4', ExitBusOracle: '0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e', StakingRouter: '0xFdDf38947aFB03C621C71b06C9C70bce73f12999', }, @@ -46,6 +49,24 @@ export const CONSTANTS_BY_NETWORK: Partial> = { reportTimestamp: 1732282199, // DAPPNODE, epoch 326714 slotsPerFrame: 32 * 225 * 28, // 28 days }, + [CHAINS.Hoodi]: { + contracts: { + CSAccounting: '0xA54b90BA34C5f326BC1485054080994e38FB4C60', + CSEarlyAdoption: '0x3281b9E45518F462E594697f8fba1896a8B43939', + CSFeeDistributor: '0xaCd9820b0A2229a82dc1A0770307ce5522FF3582', + CSFeeOracle: '0xe7314f561B2e72f9543F1004e741bab6Fc51028B', + CSModule: '0x79CEf36D84743222f37765204Bec41E92a93E59d', + CSVerifier: '0x16D0f6068D211608e3703323314aa976a6492D09', + HashConsensus: '0x54f74a10e4397dDeF85C4854d9dfcA129D72C637', + ExitBusOracle: '0x8664d394C2B3278F26A1B44B967aEf99707eeAB2', + StakingRouter: '0xCc820558B39ee15C7C45B59390B503b83fb499A8', + }, + deploymentBlockNumber: '0x1374', + stakingModuleId: 4, + withdrawalCredentials: '0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2', + retentionPeriodMins: 80_640, // 8 weeks + slotsPerFrame: 32 * 225 * 1, // 1 days + }, [CHAINS.Holesky]: { contracts: { CSAccounting: '0xc093e53e8F4b55A223c18A2Da6fA00e60DD5EFE1', @@ -54,6 +75,7 @@ export const CONSTANTS_BY_NETWORK: Partial> = { CSFeeOracle: '0xaF57326C7d513085051b50912D51809ECC5d98Ee', CSModule: '0x4562c3e63c2e586cD1651B958C22F88135aCAd4f', CSVerifier: '0x6DcA479178E6Ae41CCEB72a88FfDaa3e10E83CB7', + HashConsensus: '0xbF38618Ea09B503c1dED867156A0ea276Ca1AE37', ExitBusOracle: '0xffDDF7025410412deaa05E3E1cE68FE53208afcb', StakingRouter: '0xd6EbF043D30A7fe46D1Db32BA90a0A51207FE229', }, @@ -68,9 +90,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`); } @@ -78,10 +100,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 92739b00..a393cb2c 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 = @@ -38,53 +38,70 @@ type ExternalLinksConstants = { surveyApi: string; }; -export const EXTERNAL_LINKS_BY_NETWORK: Partial< - 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: 'http://lido-events.lido-csm-mainnet.dappnode:8081', // DAPPNODE - 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: 'http://lido-events.lido-csm-mainnet.dappnode:8081', // DAPPNODE + 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: 'http://lido-events.lido-csm-holesky.dappnode:8081', // DAPPNODE - 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://lido-events.lido-csm-holesky.dappnode:8081', // DAPPNODE + surveyApi: '', + }, + // FIXME: links + [CHAINS.Hoodi]: { + earlyAdoptionTree: '', + rewardsTree: '', + earlyAdoptionSources: '', + earlyAdoptionAbout: '', + feedbackForm: 'https://forms.gle/ZBUqbykaZokJLf4M7', + stakeWidget: 'https://stake-hoodi.testnet.fi', + + feesMonitoring: '', + operatorsWidget: 'https://operators-hoodi.testnet.fi', + beaconchain: 'https://hoodi.beaconcha.in', + beaconchainDashboard: 'https://v2-beta-hoodi.beaconcha.in/dashboard', + ratedExplorer: '', + subscribeEvents: 'https://docs.lido.fi/staking-modules/csm/guides/events', + keysApi: 'http://lido-events.lido-csm-hoodi.dappnode:8081', // DAPPNODE + surveyApi: 'https://csm-surveys-api-testnet.up.railway.app', + }, + }; export const getExternalLinks = ( chainId: CHAINS | undefined = config.defaultChain, 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/consts/matomo-click-events.ts b/consts/matomo-click-events.ts index 312931b5..a2527e88 100644 --- a/consts/matomo-click-events.ts +++ b/consts/matomo-click-events.ts @@ -10,6 +10,7 @@ export const prefixed = (template: TemplateStringsArray, ...args: string[]) => { export const enum MATOMO_CLICK_EVENTS_TYPES { // Welcome connectWallet = 'connectWallet', + disconnectWallet = 'disconnectWallet', connectAsNodeOperator = 'connectAsNodeOperator', connectToBecomeNodeOperator = 'connectToBecomeNodeOperator', welcomeDetailedLink = 'welcomeDetailedLink', @@ -103,6 +104,11 @@ export const MATOMO_CLICK_EVENTS: Record< 'Push «Connect wallet» button', prefixed`connect_wallet`, ], + [MATOMO_CLICK_EVENTS_TYPES.disconnectWallet]: [ + MATOMO_APP_NAME, + 'Push «Disonnect» button', + prefixed`disconnect_wallet`, + ], [MATOMO_CLICK_EVENTS_TYPES.connectAsNodeOperator]: [ MATOMO_APP_NAME, 'Push «I am a Node Operator» on Welcome screen', 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/consts/urls.ts b/consts/urls.ts index 428f3f19..ae58bf6f 100644 --- a/consts/urls.ts +++ b/consts/urls.ts @@ -28,6 +28,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/dappnode/hooks/use-dappnode-urls.ts b/dappnode/hooks/use-dappnode-urls.ts index 5059313c..4ce8b581 100644 --- a/dappnode/hooks/use-dappnode-urls.ts +++ b/dappnode/hooks/use-dappnode-urls.ts @@ -40,7 +40,7 @@ const useDappnodeUrls = () => { CCStatusApiUrl: '/api/consensus-status-mainnet', keysStatusUrl: '/api/keys-status-mainnet', installerTabUrl: - 'http://my.dappnode/installer/dnp/lido-csm-holesky.dnp.dappnode.eth', + 'http://my.dappnode/installer/dnp/lido-csm-hoodi.dnp.dappnode.eth', MEVApiUrl: '/api/mev-status-mainnet', MEVPackageConfig: 'http://my.dappnode/packages/my/mev-boost.dnp.dappnode.eth/config', @@ -66,6 +66,27 @@ const useDappnodeUrls = () => { MEVPackageConfig: 'http://my.dappnode/packages/my/mev-boost-holesky.dnp.dappnode.eth/config', }, + [CHAINS.Hoodi]: { + brainUrl: 'http://brain.web3signer-hoodi.dappnode', + brainKeysUrl: '/api/brain-keys-hoodi', + brainLaunchpadUrl: '/api/brain-launchpad-hoodi', + signerUrl: 'http://web3signer.web3signer-hoodi.dappnode', + sentinelUrl: 'https://t.me/CSMSentinelHolesky_bot', // DEPRECATED? + stakersUiUrl: 'http://my.dappnode/stakers/hoodi', + backendUrl: 'http://lido-events.lido-csm-hoodi.dappnode:8080', + ECApiUrl: + publicRuntimeConfig.rpcUrls_560048 || + 'http://execution.hoodi.dncore.dappnode:8545', + CCApiUrl: 'http://beacon-chain.hoodi.dncore.dappnode:3500', + CCVersionApiUrl: '/api/consensus-version-hoodi', + CCStatusApiUrl: '/api/consensus-status-hoodi', + keysStatusUrl: '/api/keys-status-hoodi', + installerTabUrl: + 'http://my.dappnode/installer/dnp/lido-csm-mainnet.dnp.dappnode.eth', + MEVApiUrl: '/api/mev-status-hoodi', + MEVPackageConfig: + 'http://my.dappnode/packages/my/mev-boost-hoodi.dnp.dappnode.eth/config', + }, }; const brainUrl = diff --git a/dappnode/hooks/use-ec-sanity-check.ts b/dappnode/hooks/use-ec-sanity-check.ts index 5be07524..af7008de 100644 --- a/dappnode/hooks/use-ec-sanity-check.ts +++ b/dappnode/hooks/use-ec-sanity-check.ts @@ -15,6 +15,7 @@ export const useECSanityCheck = () => { () => ({ [CHAINS.Mainnet]: `0xf5330dbcf09885ed145c4435e356b5d8a10054751bb8009d3a2605d476ac173f`, [CHAINS.Holesky]: `0x1475719ecbb73b28bc531bb54b37695df1bf6b71c6d2bf1d28b4efa404867e26`, + [CHAINS.Hoodi]: `0xebc45a0fa30a3f9badbcc4448ea22cef1a5d18b97825802a70df31cecb59127d`, }), [], ); diff --git a/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts b/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts index 1703d7b7..ab5e7b8d 100644 --- a/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts +++ b/dappnode/hooks/use-node-operators-fetcher-from-events-api.ts @@ -66,9 +66,11 @@ const restoreEvents = ( return mergeRoles(prev, { id, manager: isUserAddress(e.newAddress), + rewards: false, }); case 'NodeOperatorRewardAddressChanged': return mergeRoles(prev, { + manager: false, id, rewards: isUserAddress(e.newAddress), }); diff --git a/env-dynamics.mjs b/env-dynamics.mjs index a7f1b165..c8396d1b 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 = 'd3f589c93f2a2de300741fcd71ed226b'; // DAPPNOPDE: 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/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/notifications-1.md b/faq/holesky/notifications-1.md similarity index 100% rename from faq/notifications-1.md rename to faq/holesky/notifications-1.md diff --git a/faq/notifications-2.md b/faq/holesky/notifications-2.md similarity index 100% rename from faq/notifications-2.md rename to faq/holesky/notifications-2.md diff --git a/faq/notifications-3.md b/faq/holesky/notifications-3.md similarity index 100% rename from faq/notifications-3.md rename to faq/holesky/notifications-3.md diff --git a/faq/performance-1.md b/faq/holesky/performance-1.md similarity index 100% rename from faq/performance-1.md rename to faq/holesky/performance-1.md diff --git a/faq/performance-2.md b/faq/holesky/performance-2.md similarity index 100% rename from faq/performance-2.md rename to faq/holesky/performance-2.md diff --git a/faq/performance-3.md b/faq/holesky/performance-3.md similarity index 100% rename from faq/performance-3.md rename to faq/holesky/performance-3.md diff --git a/faq/performance-4.md b/faq/holesky/performance-4.md similarity index 100% rename from faq/performance-4.md rename to faq/holesky/performance-4.md diff --git a/faq/performance-5.md b/faq/holesky/performance-5.md similarity index 100% rename from faq/performance-5.md rename to faq/holesky/performance-5.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..017eaffc --- /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.etherscan.io/address/0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2) on **Hoodi:** [`0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2`](https://hoodi.etherscan.io/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.etherscan.io/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..dd1013fa --- /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 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. + +The amount for the second and subsequent validators is 1.3 stETH + +For the Hoodi testnet, the values for the bond curve are the following: + +![curve.png](/assets/mainnet-curve-common.png) diff --git a/faq/hoodi/keys-3a.md b/faq/hoodi/keys-3a.md new file mode 100644 index 00000000..5c7af749 --- /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 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. + +The amount for the second and subsequent validators is 1.3 stETH + +For the Hoodi testnet, the values for the bond curve are the following: + +![curve.png](/assets/mainnet-curve-ea.png) diff --git a/faq/hoodi/keys-4.md b/faq/hoodi/keys-4.md new file mode 100644 index 00000000..663b9fd4 --- /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 Hoodi testnet, the values for the bond curve are the following: + +![curve.png](/assets/mainnet-curve-common.png) diff --git a/faq/hoodi/keys-4a.md b/faq/hoodi/keys-4a.md new file mode 100644 index 00000000..810697a1 --- /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 Hoodi testnet, the values for the bond curve are the following: + +![curve.png](/assets/mainnet-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-notifications-1.md b/faq/hoodi/notifications-1.md similarity index 100% rename from faq/testnet-notifications-1.md rename to faq/hoodi/notifications-1.md diff --git a/faq/testnet-notifications-2.md b/faq/hoodi/notifications-2.md similarity index 100% rename from faq/testnet-notifications-2.md rename to faq/hoodi/notifications-2.md diff --git a/faq/testnet-notifications-3.md b/faq/hoodi/notifications-3.md similarity index 100% rename from faq/testnet-notifications-3.md rename to faq/hoodi/notifications-3.md diff --git a/faq/testnet-performance-1.md b/faq/hoodi/performance-1.md similarity index 100% rename from faq/testnet-performance-1.md rename to faq/hoodi/performance-1.md diff --git a/faq/testnet-performance-2.md b/faq/hoodi/performance-2.md similarity index 100% rename from faq/testnet-performance-2.md rename to faq/hoodi/performance-2.md diff --git a/faq/hoodi/performance-3.md b/faq/hoodi/performance-3.md new file mode 100644 index 00000000..1b5c7d79 --- /dev/null +++ b/faq/hoodi/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/testnet-performance-4.md b/faq/hoodi/performance-4.md similarity index 100% rename from faq/testnet-performance-4.md rename to faq/hoodi/performance-4.md diff --git a/faq/testnet-performance-5.md b/faq/hoodi/performance-5.md similarity index 100% rename from faq/testnet-performance-5.md rename to faq/hoodi/performance-5.md 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/notifications-1.md b/faq/mainnet/notifications-1.md new file mode 100644 index 00000000..0b8df581 --- /dev/null +++ b/faq/mainnet/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/mainnet/notifications-2.md b/faq/mainnet/notifications-2.md new file mode 100644 index 00000000..d307dcbb --- /dev/null +++ b/faq/mainnet/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/mainnet/notifications-3.md b/faq/mainnet/notifications-3.md new file mode 100644 index 00000000..db5fa1da --- /dev/null +++ b/faq/mainnet/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/mainnet/performance-1.md b/faq/mainnet/performance-1.md new file mode 100644 index 00000000..5b60d56e --- /dev/null +++ b/faq/mainnet/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/mainnet/performance-2.md b/faq/mainnet/performance-2.md new file mode 100644 index 00000000..794b1e49 --- /dev/null +++ b/faq/mainnet/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/mainnet/performance-3.md b/faq/mainnet/performance-3.md new file mode 100644 index 00000000..1b5c7d79 --- /dev/null +++ b/faq/mainnet/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/mainnet/performance-4.md b/faq/mainnet/performance-4.md new file mode 100644 index 00000000..e3bcc0a6 --- /dev/null +++ b/faq/mainnet/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/mainnet/performance-5.md b/faq/mainnet/performance-5.md new file mode 100644 index 00000000..98eb5ad8 --- /dev/null +++ b/faq/mainnet/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/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/faq/testnet-performance-3.md b/faq/testnet-performance-3.md deleted file mode 100644 index 75d4a96a..00000000 --- a/faq/testnet-performance-3.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -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/features/accept-invite/accept-invite-form/context/use-accept-invite-submit.ts b/features/accept-invite/accept-invite-form/context/use-accept-invite-submit.ts index e742e68c..be799f8b 100644 --- a/features/accept-invite/accept-invite-form/context/use-accept-invite-submit.ts +++ b/features/accept-invite/accept-invite-form/context/use-accept-invite-submit.ts @@ -12,6 +12,8 @@ import { AcceptInviteFormInputType, AcceptInviteFormNetworkData, } from './types'; +import { useNavigate } from 'shared/navigate'; +import { PATH } from 'consts/urls'; // TODO: move to hooks type UseAcceptInviteOptions = { @@ -58,6 +60,7 @@ export const useAcceptInviteSubmit = ({ }: UseAcceptInviteOptions) => { const { txModalStages } = useTxModalStagesAcceptInvite(); const { append: appendNO } = useNodeOperatorContext(); + const n = useNavigate(); const getTx = useAcceptInviteTx(); const sendTx = useSendTx(); @@ -65,7 +68,7 @@ export const useAcceptInviteSubmit = ({ const acceptInvite = useCallback( async ( { invite }: AcceptInviteFormInputType, - { address }: AcceptInviteFormNetworkData, + { address, invites }: AcceptInviteFormNetworkData, ): Promise => { invariant(invite, 'Invite is not defined'); @@ -92,17 +95,20 @@ export const useAcceptInviteSubmit = ({ await onConfirm?.(); - txModalStages.success({ ...invite, address }, txHash); - // TODO: move to onConfirm - appendNO(invite); + appendNO({ id: invite.id, roles: [invite.role] }); + if (invites && invites.length <= 1) { + void n(PATH.HOME); + } + + txModalStages.success({ ...invite, address }, txHash); return true; } catch (error) { return handleTxError(error, txModalStages, onRetry); } }, - [getTx, txModalStages, onConfirm, appendNO, sendTx, onRetry], + [txModalStages, getTx, onConfirm, appendNO, n, sendTx, onRetry], ); return { 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-bond/add-bond-form/controls/info.tsx b/features/add-bond/add-bond-form/controls/info.tsx index 54bf12f1..1e275999 100644 --- a/features/add-bond/add-bond-form/controls/info.tsx +++ b/features/add-bond/add-bond-form/controls/info.tsx @@ -1,4 +1,4 @@ -import { BOND_EXCESS, BOND_INSUFFICIENT } from 'consts/text'; +import { BOND_INSUFFICIENT } from 'consts/text'; import { TOKENS } from 'consts/tokens'; import { FC } from 'react'; import { Latice, MatomoLink, Stack, TitledAmount } from 'shared/components'; @@ -13,11 +13,11 @@ export const Info: FC = () => { { {bond?.isInsufficient ? (

Your Node Operator has an Insufficient bond because of the penalty - applied. Now your Node Operator’s bond is less than required to - cover the Node Operator’s current validators. + applied. Now your Node Operator's bond is less than required + to cover the Node Operator's current validators.
Action required:
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/add-keys/add-keys/context/use-add-keys-submit.ts b/features/add-keys/add-keys/context/use-add-keys-submit.ts index 175ad2dc..08fd1f27 100644 --- a/features/add-keys/add-keys/context/use-add-keys-submit.ts +++ b/features/add-keys/add-keys/context/use-add-keys-submit.ts @@ -15,6 +15,8 @@ import { NodeOperatorId } from 'types'; import { addExtraWei, formatKeys, runWithTransactionLogger } from 'utils'; import { useTxModalStagesAddKeys } from '../hooks/use-tx-modal-stages-add-keys'; import { AddKeysFormInputType, AddKeysFormNetworkData } from './types'; +import { useNavigate } from 'shared/navigate'; +import { PATH } from 'consts/urls'; type AddKeysOptions = { onConfirm?: () => Promise | void; @@ -86,6 +88,7 @@ export const useAddKeysSubmit = ({ onConfirm, onRetry }: AddKeysOptions) => { const getPermitOrApprove = usePermitOrApprove(); const getTx = useAddKeysTx(); const sendTx = useSendTx(); + const n = useNavigate(); const { addCacheKeys } = useKeysCache(); @@ -138,14 +141,16 @@ export const useAddKeysSubmit = ({ onConfirm, onRetry }: AddKeysOptions) => { await onConfirm?.(); + // TODO: move to onConfirm + void addCacheKeys(depositData.map(({ pubkey }) => pubkey)); + + void n(PATH.KEYS_VIEW); + txModalStages.success( { keys: depositData.map((key) => key.pubkey) }, txHash, ); - // TODO: move to onConfirm - void addCacheKeys(depositData.map(({ pubkey }) => pubkey)); - return true; } catch (error) { return handleTxError(error, txModalStages, onRetry); @@ -157,6 +162,7 @@ export const useAddKeysSubmit = ({ onConfirm, onRetry }: AddKeysOptions) => { getTx, onConfirm, addCacheKeys, + n, sendTx, onRetry, ], diff --git a/features/change-role/change-role-form/controls/info.tsx b/features/change-role/change-role-form/controls/info.tsx index 87edf5ec..7ead48fc 100644 --- a/features/change-role/change-role-form/controls/info.tsx +++ b/features/change-role/change-role-form/controls/info.tsx @@ -1,9 +1,10 @@ import { FC, useCallback } from 'react'; import { useFormContext } from 'react-hook-form'; -import { Latice, TitledAddress, Warning } from 'shared/components'; +import { Latice, Stack, TitledAddress, Warning } from 'shared/components'; import { SubmitButtonHookForm } from 'shared/hook-form/controls'; import { ChangeRoleFormInputType, useChangeRoleFormData } from '../context'; import { useRole } from '../hooks/use-role'; +import { Text } from '@lidofinance/lido-ui'; export const Info: FC = () => { const role = useRole(); @@ -22,24 +23,42 @@ export const Info: FC = () => { title={`Current ${role} address`} address={currentAddress} /> - + + + {isPropose && ( + + Revoke + + )} + + } + address={proposedAddress} + /> + {proposedAddress && ( <> - - {isPropose && ( - - Revoke - - )} + + Action required + + +

    +
  1. Connect to CSM UI with the proposed address
  2. +
  3. + Go to Roles tab → Inbox requests to confirm the change +
  4. +
+ - } - address={proposedAddress} - /> + )} +
); diff --git a/features/change-role/change-role-form/hooks/use-tx-modal-stages-change-role.tsx b/features/change-role/change-role-form/hooks/use-tx-modal-stages-change-role.tsx index 45724103..ed14ad42 100644 --- a/features/change-role/change-role-form/hooks/use-tx-modal-stages-change-role.tsx +++ b/features/change-role/change-role-form/hooks/use-tx-modal-stages-change-role.tsx @@ -1,7 +1,10 @@ +import { Text } from '@lidofinance/lido-ui'; import { ROLES } from 'consts/roles'; import { capitalize } from 'lodash'; +import { Address } from 'shared/components'; import { getRoleTitle } from 'shared/node-operator'; import { + AfterAddressProposed, TransactionModalTransitStage, TxStagePending, TxStageSign, @@ -20,38 +23,78 @@ type Props = { isRevoke: boolean; }; -// TODO: show address with
component const getTexts = (props: Props) => { return props.isManagerReset || props.isRewardsChange ? { sign: { title: `You are changing ${getRoleTitle(props.role)} address`, - description: `New address ${props.address}`, + description: ( + <> + New ${getRoleTitle(props.role)} address is{' '} + +
+ + + ), }, success: { title: `${capitalize(getRoleTitle(props.role))} address has been changed`, - description: `New address ${props.address}`, + description: ( + <> + New ${getRoleTitle(props.role)} address is{' '} + +
+ + + ), }, } : props.isRevoke ? { sign: { title: `You are revoking request for ${getRoleTitle(props.role)} address change`, - description: `Address stays ${props.currentAddress}`, + description: ( + <> + Address stays{' '} + +
+ + + ), }, success: { title: `Proposed request for ${getRoleTitle(props.role)} address has been revoked`, - description: `Address stays ${props.currentAddress}`, + description: ( + <> + Address stays{' '} + +
+ + + ), }, } : { sign: { title: `You are proposing ${getRoleTitle(props.role)} address change`, - description: `Proposed address ${props.address}`, + description: ( + <> + Proposed address{' '} + +
+ + + ), }, success: { title: `New ${getRoleTitle(props.role)} address has been proposed`, - description: `To complete the address change, the owner of the new address must confirm the change`, + description: ( + <> +
+
+ + + ), }, }; }; diff --git a/features/claim-bond/claim-bond-form/claim-bond-form-info.tsx b/features/claim-bond/claim-bond-form/claim-bond-form-info.tsx index 3c5994f4..cba4552d 100644 --- a/features/claim-bond/claim-bond-form/claim-bond-form-info.tsx +++ b/features/claim-bond/claim-bond-form/claim-bond-form-info.tsx @@ -5,15 +5,15 @@ import { useWatch } from 'react-hook-form'; import { FormatToken } from 'shared/formatters'; import styled from 'styled-components'; import { ClaimBondFormInputType, useClaimBondFormData } from './context'; -import { useBondReceiveAmount } from './hooks/use-bond-receive-amount'; import { Address } from 'shared/components'; import { AddressContainerStyle, AddressStyle, } from 'shared/components/address/styles'; +import { useBondWillReceive } from 'shared/hooks'; export const ClaimBondFormInfo = () => { - const { rewardsAddress } = useClaimBondFormData(); + const { rewardsAddress, rewards } = useClaimBondFormData(); const [token, amount, claimRewards] = useWatch< ClaimBondFormInputType, ['token', 'amount', 'claimRewards'] @@ -21,7 +21,11 @@ export const ClaimBondFormInfo = () => { name: ['token', 'amount', 'claimRewards'], }); - const bondReceive = useBondReceiveAmount(); + const [bondReceive] = useBondWillReceive( + token, + amount, + claimRewards ? rewards?.available : undefined, + ); return ( @@ -32,6 +36,7 @@ export const ClaimBondFormInfo = () => {
) will receive } + help="The recipient of the claim is the Rewards address. You can change the Rewards address on the Roles tab" > @@ -45,6 +50,8 @@ export const ClaimBondFormInfo = () => { }; const AddressStyled = styled.div` + display: inline; + ${AddressContainerStyle} { display: inline-flex; } diff --git a/features/claim-bond/claim-bond-form/context/use-claim-bond-submit.ts b/features/claim-bond/claim-bond-form/context/use-claim-bond-submit.ts index d234a54b..172cb79b 100644 --- a/features/claim-bond/claim-bond-form/context/use-claim-bond-submit.ts +++ b/features/claim-bond/claim-bond-form/context/use-claim-bond-submit.ts @@ -4,12 +4,7 @@ import invariant from 'tiny-invariant'; import { Zero } from '@ethersproject/constants'; import { TOKENS } from 'consts/tokens'; -import { - useCSAccountingRPC, - useCSAccountingWeb3, - useCSModuleWeb3, - useSendTx, -} from 'shared/hooks'; +import { useCSAccountingWeb3, useCSModuleWeb3, useSendTx } from 'shared/hooks'; import { handleTxError } from 'shared/transaction-modal'; import { NodeOperatorId, RewardProof } from 'types'; import { runWithTransactionLogger } from 'utils'; @@ -88,7 +83,6 @@ export const useClaimBondSubmit = ({ onRetry, }: UseClaimBondOptions) => { const { txModalStages } = useTxModalStagesClaimBond(); - const CSAccounting = useCSAccountingRPC(); const getTx = useClaimBondTx(); const sendTx = useSendTx(); @@ -102,7 +96,12 @@ export const useClaimBondSubmit = ({ invariant(nodeOperatorId, 'NodeOperatorId is not defined'); try { - txModalStages.sign({ amount, token }); + txModalStages.sign({ + amount, + token, + claimRewards, + rewards: rewards?.available, + }); const tx = await getTx(token, { nodeOperatorId, @@ -115,23 +114,23 @@ export const useClaimBondSubmit = ({ () => sendTx(tx), ); - txModalStages.pending({ amount, token }, txHash); + txModalStages.pending( + { amount, token, claimRewards, rewards: rewards?.available }, + txHash, + ); await runWithTransactionLogger('ClaimBond block confirmation', waitTx); await onConfirm?.(); - // TODO: move to onConfirm - const { current } = await CSAccounting.getBondSummary(nodeOperatorId); - - txModalStages.success({ balance: current, amount, token }, txHash); + txModalStages.success({ amount, token }, txHash); return true; } catch (error) { return handleTxError(error, txModalStages, onRetry); } }, - [getTx, txModalStages, onConfirm, CSAccounting, sendTx, onRetry], + [getTx, txModalStages, onConfirm, sendTx, onRetry], ); return { diff --git a/features/claim-bond/claim-bond-form/controls/source-select.tsx b/features/claim-bond/claim-bond-form/controls/source-select.tsx index 63357616..81b41deb 100644 --- a/features/claim-bond/claim-bond-form/controls/source-select.tsx +++ b/features/claim-bond/claim-bond-form/controls/source-select.tsx @@ -10,10 +10,13 @@ import { Stack, TitledSelectableAmount, } from 'shared/components'; +import { useRewardsFrame } from 'shared/hooks'; +import { formatDate } from 'utils'; import { ClaimBondFormInputType, useClaimBondFormData } from '../context'; export const SourceSelect: FC = () => { const { bond, rewards, loading, maxValues } = useClaimBondFormData(); + const { data: rewardsFrame } = useRewardsFrame(); const { field } = useController({ name: 'claimRewards', @@ -23,6 +26,7 @@ export const SourceSelect: FC = () => { const { setValue } = useFormContext(); const availableToClaim = maxValues?.[TOKENS.STETH][Number(field.value)]; + const nextRewardsDate = formatDate(rewardsFrame?.nextRewards); useEffect(() => { if (bond?.isInsufficient) { @@ -54,7 +58,8 @@ export const SourceSelect: FC = () => { disabled={!rewards?.available.gt(0)} /> } - help="The rewards amount available to claim, obtained from all active validators of the Node Operator" + help={`The rewards amount available to claim, obtained from all active validators of the Node Operator. Next rewards distribution is expected on ${nextRewardsDate}`} + helpIcon="calendar" loading={loading.isRewardsLoading} amount={rewards?.available} token={TOKENS.STETH} @@ -70,9 +75,10 @@ export const SourceSelect: FC = () => { } help={ bond?.isInsufficient - ? 'Insufficient bond is the missing amount of stETH required to cover all operator’s keys. In case of a bond insufficient, "unbonded" validators are requested for exit by the protocol' - : 'The bond amount available to claim without having to exit validators' + ? 'Insufficient bond is the missing amount of stETH required to cover all operator’s keys' + : 'The bond amount available to claim without having to exit validators. Increases daily' } + helpIcon={bond?.isInsufficient ? undefined : 'calendar'} sign={bond?.isInsufficient ? 'minus' : 'plus'} loading={loading.isBondLoading} amount={bond?.delta} @@ -82,7 +88,7 @@ export const SourceSelect: FC = () => { } - help="Bond may be locked in the case of an MEV stealing event reported by a dedicated committee. This measure ensures that Node Operators are held accountable for any misbehavior or rule violations." + help="Bond may be locked in the case of an MEV stealing event reported by a dedicated committee. This measure ensures that Node Operators are held accountable for any misbehavior or rule violations" loading={loading.isBondLoading} amount={bond?.locked} token={TOKENS.ETH} diff --git a/features/claim-bond/claim-bond-form/controls/submit-button.tsx b/features/claim-bond/claim-bond-form/controls/submit-button.tsx index 82909fe4..23752044 100644 --- a/features/claim-bond/claim-bond-form/controls/submit-button.tsx +++ b/features/claim-bond/claim-bond-form/controls/submit-button.tsx @@ -1,22 +1,32 @@ +import { TOKENS } from 'consts/tokens'; +import { useWatch } from 'react-hook-form'; import { PausedButton, SubmitButtonHookForm } from 'shared/hook-form/controls'; import { ClaimBondFormInputType, useClaimBondFormData } from '../context'; -import { useWatch } from 'react-hook-form'; export const SubmitButton = () => { - const claimRewards = useWatch({ - name: 'claimRewards', + const [claimRewards, token, amount] = useWatch< + ClaimBondFormInputType, + ['claimRewards', 'token', 'amount'] + >({ + name: ['claimRewards', 'token', 'amount'], }); - const { isPaused } = useClaimBondFormData(); + const { isPaused, maxValues } = useClaimBondFormData(); if (isPaused) { return ; } - // TODO: disable - // TODO: nothing to claim + const isNothingToClaim = !!maxValues?.STETH[1]?.isZero(); + const isPullRewards = !!amount?.isZero() && claimRewards; + const text = isNothingToClaim + ? 'Nothing to claim' + : token === TOKENS.ETH + ? 'Request withdrawal to the Rewards Address' + : isPullRewards + ? 'Claim rewards to the Bond balance' + : 'Claim to the Rewards Address'; + return ( - - {claimRewards ? 'Claim Rewards and Bond' : 'Claim Bond'} - + {text} ); }; diff --git a/features/claim-bond/claim-bond-form/hooks/use-bond-receive-amount.ts b/features/claim-bond/claim-bond-form/hooks/use-bond-receive-amount.ts deleted file mode 100644 index abeccfd1..00000000 --- a/features/claim-bond/claim-bond-form/hooks/use-bond-receive-amount.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Zero } from '@ethersproject/constants'; -import { useWatch } from 'react-hook-form'; -import { ClaimBondFormInputType, useClaimBondFormData } from '../context'; -import { useStethAmount } from './use-steth-amount'; - -export const useBondReceiveAmount = () => { - const [token, amount] = useWatch< - ClaimBondFormInputType, - ['token', 'amount', 'claimRewards'] - >({ - name: ['token', 'amount', 'claimRewards'], - }); - - const { rewards } = useClaimBondFormData(); - - const stethAmount = useStethAmount(token, amount ?? Zero); - const bondReceive = rewards?.available.sub(stethAmount ?? Zero) ?? Zero; - - return bondReceive.lt(0) ? Zero : bondReceive; -}; diff --git a/features/claim-bond/claim-bond-form/hooks/use-tx-modal-stages-claim-bond.tsx b/features/claim-bond/claim-bond-form/hooks/use-tx-modal-stages-claim-bond.tsx index 97c2c312..2c2f52e1 100644 --- a/features/claim-bond/claim-bond-form/hooks/use-tx-modal-stages-claim-bond.tsx +++ b/features/claim-bond/claim-bond-form/hooks/use-tx-modal-stages-claim-bond.tsx @@ -1,28 +1,28 @@ import type { BigNumber } from 'ethers'; +import { getExternalLinks } from 'consts/external-links'; +import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events'; import { TOKENS } from 'consts/tokens'; +import { PATH } from 'consts/urls'; import { MatomoLink } from 'shared/components'; import { TxLinkEtherscan } from 'shared/components/tx-link-etherscan'; +import { LocalLink } from 'shared/navigate'; import { TransactionModalTransitStage, TxAmount, - TxStageOperationSucceedBalanceShown, - TxStageSignOperationAmount, + TxStageClaim, TxStageSuccess, getGeneralTransactionModalStages, useTransactionModalStage, } from 'shared/transaction-modal'; -import { getExternalLinks } from 'consts/external-links'; -import { LocalLink } from 'shared/navigate'; -import { PATH } from 'consts/urls'; -import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events'; -const STAGE_OPERATION_ARGS = { - operationText: 'Claiming Bond', +type Props = { + amount: BigNumber; + token: TOKENS; + claimRewards: boolean; + rewards?: BigNumber; }; - -type Props = { amount: BigNumber; token: TOKENS }; -type SuccessProps = { amount: BigNumber; balance: BigNumber; token: TOKENS }; +type SuccessProps = { amount: BigNumber; token: TOKENS }; const { stakeWidget } = getExternalLinks(); @@ -31,33 +31,12 @@ const getTxModalStagesClaimBond = ( ) => ({ ...getGeneralTransactionModalStages(transitStage), - sign: ({ amount, token }: Props) => - transitStage( - , - ), + sign: (props: Props) => transitStage(), - pending: ({ amount, token }: Props, txHash?: string) => - transitStage( - , - ), + pending: (props: Props, txHash?: string) => + transitStage(), - success: ({ amount, balance, token }: SuccessProps, txHash?: string) => + success: ({ amount, token }: SuccessProps, txHash?: string) => transitStage( token === TOKENS.ETH ? ( // TODO: matomo events @@ -88,11 +67,14 @@ const getTxModalStagesClaimBond = ( } /> ) : ( - + Transaction can be viewed on{' '} + . + + } /> ), { 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/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 2fcc9779..a8c36b14 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 @@ -6,7 +6,6 @@ import { useCallback } from 'react'; import { GatherPermitSignatureResult, useAddressCompare, - useAskHowDidYouLearnCsm, useCSModuleWeb3, useKeysCache, usePermitOrApprove, @@ -20,12 +19,16 @@ import { addressOrZero, formatKeys, getAddedNodeOperator, + packRoles, runWithTransactionLogger, } from 'utils'; 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 { PATH } from 'consts/urls'; +import { useNavigate } from 'shared/navigate'; +import { useOperatorCustomAddresses } from 'features/starter-pack/banner-operator-custom-addresses'; import useDappnodeUrls from 'dappnode/hooks/use-dappnode-urls'; type SubmitKeysOptions = { @@ -125,9 +128,10 @@ export const useSubmitKeysSubmit = ({ const sendTx = useSendTx(); const isUserOrZero = useAddressCompare(true); const { addCacheKeys } = useKeysCache(); + const n = useNavigate(); + const [, setOperatorCustomAddresses] = useOperatorCustomAddresses(); const confirmCustomAddresses = useConfirmCustomAddressesModal(); - const { ask } = useAskHowDidYouLearnCsm(); // DAPPNODE const { backendUrl } = useDappnodeUrls(); @@ -228,29 +232,36 @@ export const useSubmitKeysSubmit = ({ // DAPPNODE const nodeOperator = getAddedNodeOperator(receipt); + const roles = packRoles({ + rewards: isUserOrZero(nodeOperator?.rewards), + manager: isUserOrZero(nodeOperator?.manager), + }); + + void onConfirm?.(); + + // TODO: move to onConfirm + void addCacheKeys(depositData.map(({ pubkey }) => pubkey)); + + if (nodeOperator?.id) { + if (roles.length === 0) { + setOperatorCustomAddresses(nodeOperator.id); + void n(PATH.HOME); + } else { + appendNO({ + id: nodeOperator.id, + roles, + }); + } + } txModalStages.success( { nodeOperatorId: nodeOperator?.id, keys: depositData.map((key) => key.pubkey), + roles, }, txHash, ); - ask(); - - // TODO: move to onConfirm - void addCacheKeys(depositData.map(({ pubkey }) => pubkey)); - - // TODO: move to onConfirm - if (nodeOperator) { - appendNO({ - id: nodeOperator.id, - manager: isUserOrZero(nodeOperator.managerAddress), - rewards: isUserOrZero(nodeOperator.rewardsAddress), - }); - } - - await onConfirm?.(); return true; } catch (error) { @@ -262,13 +273,14 @@ export const useSubmitKeysSubmit = ({ getPermitOrApprove, txModalStages, getTx, + isUserOrZero, onConfirm, addCacheKeys, sendTx, + setOperatorCustomAddresses, + n, appendNO, - isUserOrZero, onRetry, - ask, scanEvents, // DAPPNODE ], ); 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 56c363c7..a7453f65 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 @@ -7,13 +7,18 @@ import { import { TOKENS } from 'consts/tokens'; import type { BigNumber } from 'ethers'; import { Plural } from 'shared/components'; -import { AfterKeysUpload, TxAmount } from 'shared/transaction-modal'; +import { + AfterCreateCustomNodeOperator, + AfterKeysUpload, + TxAmount, +} from 'shared/transaction-modal'; import { TxStagePending, TxStageSign, TxStageSuccess, } from 'shared/transaction-modal/tx-stages-basic'; import { NodeOperatorId } from 'types'; +import { ROLES } from 'consts/roles'; import { Button } from '@lidofinance/lido-ui'; import { PATH } from 'consts/urls'; @@ -23,7 +28,11 @@ type Props = { token: TOKENS; }; -type SuccessProps = { nodeOperatorId?: NodeOperatorId; keys: string[] }; +type SuccessProps = { + nodeOperatorId?: NodeOperatorId; + keys: string[]; + roles: ROLES[]; +}; const getTxModalStagesSubmitKeys = ( transitStage: TransactionModalTransitStage, @@ -69,7 +78,7 @@ const getTxModalStagesSubmitKeys = ( />, ), - success: ({ nodeOperatorId, keys }: SuccessProps, txHash?: string) => { + success: ({ nodeOperatorId, keys, roles }: SuccessProps, txHash?: string) => { return transitStage( {nodeOperatorId}

- + {roles.length > 0 ? ( + + ) : ( + + )}
- - - - - ); -}; diff --git a/shared/components/address/styles.tsx b/shared/components/address/styles.tsx index 7ee36ad5..7f62a0c3 100644 --- a/shared/components/address/styles.tsx +++ b/shared/components/address/styles.tsx @@ -15,6 +15,8 @@ export const AddressContainerStyle = styled(StackStyle).attrs({ $gap: 'xs', $align: 'center', })<{ $bold?: boolean }>` + display: inline-flex; + ${LinkStyled} { svg { display: inline-flex; 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/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] : '')} `; diff --git a/shared/components/external-icon-link/etherscan-address-link.tsx b/shared/components/external-icon-link/etherscan-address-link.tsx index 21fd787c..40bf10af 100644 --- a/shared/components/external-icon-link/etherscan-address-link.tsx +++ b/shared/components/external-icon-link/etherscan-address-link.tsx @@ -11,19 +11,16 @@ type Props = { 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/invite-content/invite-content.tsx b/shared/components/invite-content/invite-content.tsx index ba36eb87..5cc4bfa6 100644 --- a/shared/components/invite-content/invite-content.tsx +++ b/shared/components/invite-content/invite-content.tsx @@ -2,12 +2,46 @@ import { FC } from 'react'; import { DescriptorId, getRoleTitle } from 'shared/node-operator'; import { NodeOperatorInvite } from 'types'; import { Badge, InviteContentStyle } from './style'; +import { Tooltip } from '@lidofinance/lido-ui'; +import { ROLES } from 'consts/roles'; export const InviteContent: FC<{ invite: NodeOperatorInvite }> = ({ invite, }) => ( - {getRoleTitle(invite.role, true)} address role + + 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
  • +
+ + ) : ( + <> + The Rewards address is used for: +
    +
  • Claiming bond and rewards
  • +
  • Adding extra bond amount
  • +
  • Covering locked bond
  • +
  • Proposing a new Rewards address
  • +
  • + Resetting the Manager address to the current Rewards address +
  • +
+ + ) + } + > + {getRoleTitle(invite.role, true)} address role +
); diff --git a/shared/components/invite-content/style.ts b/shared/components/invite-content/style.ts index 4e5bcce4..f2e2dc9f 100644 --- a/shared/components/invite-content/style.ts +++ b/shared/components/invite-content/style.ts @@ -1,4 +1,3 @@ -import { BadgeStyle } from 'shared/node-operator/role-badge/styles'; import styled from 'styled-components'; export const InviteContentStyle = styled.div` @@ -11,6 +10,16 @@ export const InviteContentStyle = styled.div` color: var(--lido-color-text); `; -export const Badge = styled(BadgeStyle).attrs({ $background: 'dark' })` - text-transform: unset; +export const Badge = styled.span` + border-radius: ${({ theme }) => theme.borderRadiusesMap.md}px; + font-size: ${({ theme }) => theme.fontSizesMap.xs}px; + + display: inline-flex; + justify-content: center; + align-items: center; + padding: 2px 8px; + + background: var(--lido-color-background); + background: var(--lido-color-shadowLight); + color: var(--lido-color-textSecondary); `; 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; }; diff --git a/shared/components/titled-selectable-amount/titled-selectable-amount.tsx b/shared/components/titled-selectable-amount/titled-selectable-amount.tsx index b9f8d966..7372efdd 100644 --- a/shared/components/titled-selectable-amount/titled-selectable-amount.tsx +++ b/shared/components/titled-selectable-amount/titled-selectable-amount.tsx @@ -1,12 +1,13 @@ import { TOKENS } from 'consts/tokens'; import { BigNumber } from 'ethers'; -import { FC, ReactNode } from 'react'; +import { ComponentProps, FC, ReactNode } from 'react'; import { AmountWithPrice, IconTooltip, Stack } from 'shared/components'; import { TitledAmountStyle } from './style'; type Props = { title?: ReactNode; help?: string; + helpIcon?: ComponentProps['type']; warning?: boolean; loading?: boolean; amount?: BigNumber; @@ -17,6 +18,7 @@ type Props = { export const TitledSelectableAmount: FC = ({ title, help, + helpIcon, warning, ...props }) => { @@ -24,7 +26,7 @@ export const TitledSelectableAmount: FC = ({ {title} - + 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 e0072a6b..9e8f6e73 100644 --- a/shared/hooks/index.ts +++ b/shared/hooks/index.ts @@ -1,9 +1,9 @@ export * from './use-account'; export * from './use-address-compare'; export * from './use-approve'; -export * from './use-ask-how-did-you-learn-csm'; export * from './use-await-network-data'; export * from './use-awaiter'; +export * from './use-bond-will-receive'; export * from './use-can-create-node-operator'; export * from './use-compare-with-router-path'; export * from './use-confirm-modal'; @@ -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'; @@ -34,6 +35,8 @@ export * from './use-router-path'; export * from './use-send-tx'; export * from './use-show-rule'; export * from './use-sorted-keys'; +export * from './use-steth-amount'; +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'; @@ -59,7 +62,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'; @@ -75,6 +78,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/use-ask-how-did-you-learn-csm.tsx b/shared/hooks/use-ask-how-did-you-learn-csm.tsx deleted file mode 100644 index 539f8fa7..00000000 --- a/shared/hooks/use-ask-how-did-you-learn-csm.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useLocalStorage } from '@lido-sdk/react'; -import { useMemo } from 'react'; - -export const useAskHowDidYouLearnCsm = () => { - const [state, setState] = useLocalStorage< - 'ask' | 'answered' | 'closed' | undefined - >(`ask-how-learn-csm`, undefined); - - return useMemo( - () => ({ - canAsk: state === 'ask', - ask: () => !state && setState('ask'), - answer: () => setState('answered'), - rejectAnswer: () => state !== 'answered' && setState('closed'), - }), - [setState, state], - ); -}; diff --git a/shared/hooks/use-bond-will-receive.ts b/shared/hooks/use-bond-will-receive.ts new file mode 100644 index 00000000..665f2883 --- /dev/null +++ b/shared/hooks/use-bond-will-receive.ts @@ -0,0 +1,22 @@ +import { TOKENS } from 'consts/tokens'; +import { BigNumber } from 'ethers'; +import { useStethAmount } from './use-steth-amount'; +import { Zero } from '@ethersproject/constants'; + +export const useBondWillReceive = ( + token: TOKENS, + amount?: BigNumber, + rewards?: BigNumber, +) => { + const stethAmount = useStethAmount(token, amount); + + return [ + (amount && + stethAmount && + rewards && + rewards.gt(stethAmount) && + rewards.sub(stethAmount)) || + Zero, + !!(amount && stethAmount && rewards && stethAmount.gt(rewards)), + ] as const; +}; diff --git a/shared/hooks/use-external-links.ts b/shared/hooks/use-external-links.ts index ad8f572c..5d13f381 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 ) @@ -27,26 +29,27 @@ export const useBeaconchainDashboardLink = (directKeys?: string[]) => { export const useFeesMonitoningLink = () => { const nodeOperatorId = useNodeOperatorId(); const { stakingModuleId } = getCsmConstants(); + if (!links.feesMonitoring) return null; return `${links.feesMonitoring}/operatorInfo?stakingModuleIndex=${stakingModuleId}&operatorIndex=${nodeOperatorId}`; }; export const useOperatorPortalLink = () => { const nodeOperatorId = useNodeOperatorId(); const { stakingModuleId } = getCsmConstants(); + if (!links.operatorsWidget) return null; return `${links.operatorsWidget}/module/${stakingModuleId}/${nodeOperatorId}`; }; export const useRatedLink = () => { const nodeOperatorId = useNodeOperatorId(); - const network = defaultChain === CHAINS.Mainnet ? 'mainnet' : 'holesky'; + const network = defaultChain === CHAINS.Mainnet ? 'mainnet' : 'hoodi'; + if (!links.ratedExplorer) return null; 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'; - if (links.ethseerDashboard) { - return `${links.ethseerDashboard}/entity/csm_operator${nodeOperatorId}_lido?network=${network}`; - } - return null; + const network = defaultChain === CHAINS.Mainnet ? 'mainnet' : 'hoodi'; + if (!links.ethseerDashboard) return null; + return `${links.ethseerDashboard}/entity/csm_operator${nodeOperatorId}_lido?network=${network}`; }; 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-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-node-operators-fetcher-from-events.ts b/shared/hooks/use-node-operators-fetcher-from-events.ts index ada485a4..461cb8b7 100644 --- a/shared/hooks/use-node-operators-fetcher-from-events.ts +++ b/shared/hooks/use-node-operators-fetcher-from-events.ts @@ -21,7 +21,8 @@ type NodeOperatorRoleEvent = | NodeOperatorManagerAddressChangedEvent; const restoreEvents = (events: NodeOperatorRoleEvent[], address?: Address) => { - const isUserAddress = (value: string) => compareLowercase(address, value); + const isUserAddress = (value: string) => + compareLowercase(address, value) || null; return events .sort((a, b) => a.blockNumber - b.blockNumber) @@ -38,10 +39,12 @@ const restoreEvents = (events: NodeOperatorRoleEvent[], address?: Address) => { return mergeRoles(prev, { id, manager: isUserAddress(e.args[2]), + rewards: false, }); case 'NodeOperatorRewardAddressChanged': return mergeRoles(prev, { id, + manager: false, rewards: isUserAddress(e.args[2]), }); default: 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/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/features/claim-bond/claim-bond-form/hooks/use-steth-amount.ts b/shared/hooks/use-steth-amount.ts similarity index 69% rename from features/claim-bond/claim-bond-form/hooks/use-steth-amount.ts rename to shared/hooks/use-steth-amount.ts index 275118a2..02cdd08f 100644 --- a/features/claim-bond/claim-bond-form/hooks/use-steth-amount.ts +++ b/shared/hooks/use-steth-amount.ts @@ -1,8 +1,8 @@ +import { Zero } from '@ethersproject/constants'; import { TOKENS } from 'consts/tokens'; -import { BigNumber } from 'ethers'; import { useStethByWsteth } from 'shared/hooks'; -export const useStethAmount = (token: TOKENS, amount?: BigNumber) => { +export const useStethAmount = (token: TOKENS, amount = Zero) => { const { data: wstethToSteth } = useStethByWsteth( (token === TOKENS.WSTETH && amount) || undefined, ); diff --git a/shared/hooks/use-surveys-call.ts b/shared/hooks/use-surveys-call.ts new file mode 100644 index 00000000..3c16e02a --- /dev/null +++ b/shared/hooks/use-surveys-call.ts @@ -0,0 +1,11 @@ +import { getConfig } from 'config'; +import { CHAINS } from 'consts/chains'; +import { isBefore, parseISO } from 'date-fns'; + +const { defaultChain } = getConfig(); + +export const useSurveysCall = () => { + const endOfSurvey = parseISO('2025-04-01T00:00Z'); + const today = new Date(); + return defaultChain === CHAINS.Mainnet && isBefore(today, endOfSurvey); +}; diff --git a/shared/hooks/useCsmContracts.ts b/shared/hooks/useCsmContracts.ts index b06d9ba0..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'; @@ -19,6 +20,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'), @@ -48,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 04bbaba8..aa8c3f7e 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/useNodeOperatorKeys.ts b/shared/hooks/useNodeOperatorKeys.ts index 158be182..ee7e974f 100644 --- a/shared/hooks/useNodeOperatorKeys.ts +++ b/shared/hooks/useNodeOperatorKeys.ts @@ -17,7 +17,7 @@ export const useNodeOperatorKeys = ( contract: useCSModuleRPC(), method: 'getSigningKeys', params: [nodeOperatorId, startIndex, count], - shouldFetch: Boolean(nodeOperatorId && count), + shouldFetch: Boolean(nodeOperatorId), config, }); 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/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/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/shared/keys/validate/check-network-duplicates.ts b/shared/keys/validate/check-network-duplicates.ts index ccb9c59e..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: { @@ -18,12 +17,14 @@ 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(); + if (!keysApi) return; + + const response = await fetch(`${keysApi}/v1/keys/find`, { method: 'post', body: JSON.stringify({ pubkeys }), headers: { 'Content-Type': 'application/json' }, @@ -41,14 +42,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/shared/layout/header/components/navigation/use-nav-items.tsx b/shared/layout/header/components/navigation/use-nav-items.tsx index 03ef10b2..7538de41 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'; @@ -93,6 +94,7 @@ const routes: Route[] = [ path: PATH.SURVEYS, icon: , showRules: ['IS_SURVEYS_ACTIVE'], + suffix: , }, ]; diff --git a/shared/transaction-modal/tx-stages-basic/tx-stage-fail.tsx b/shared/transaction-modal/tx-stages-basic/tx-stage-fail.tsx index f4e389ed..4fba9dab 100644 --- a/shared/transaction-modal/tx-stages-basic/tx-stage-fail.tsx +++ b/shared/transaction-modal/tx-stages-basic/tx-stage-fail.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback, useState } from 'react'; +import { FC, ReactNode, useCallback, useState } from 'react'; import { Loader } from '@lidofinance/lido-ui'; import { TransactionModalContent } from 'shared/transaction-modal/transaction-modal-content'; @@ -11,11 +11,13 @@ type TxStageFailProps = { code?: ErrorCode; title?: string; onRetry?: React.MouseEventHandler; + error?: ReactNode; }; export const TxStageFail: FC = ({ code = ErrorCode.SOMETHING_WRONG, title = 'Transaction Failed', + error, onRetry, }) => { const [isLoading, setLoading] = useState(false); @@ -30,7 +32,7 @@ export const TxStageFail: FC = ({ } - description={ErrorMessages[code]} + description={error ?? ErrorMessages[code]} footerHint={ code !== ErrorCode.NOT_ENOUGH_ETHER && onRetry && diff --git a/shared/transaction-modal/tx-stages-composed/index.ts b/shared/transaction-modal/tx-stages-composed/index.ts index da3c79bc..c712c667 100644 --- a/shared/transaction-modal/tx-stages-composed/index.ts +++ b/shared/transaction-modal/tx-stages-composed/index.ts @@ -1,3 +1,4 @@ export * from './tx-stage-amount-operation'; export * from './tx-stage-keys-operation'; export * from './tx-stage-operation-succeed-balance-shown'; +export * from './tx-stage-claim'; diff --git a/shared/transaction-modal/tx-stages-composed/tx-stage-claim.tsx b/shared/transaction-modal/tx-stages-composed/tx-stage-claim.tsx new file mode 100644 index 00000000..d7d48198 --- /dev/null +++ b/shared/transaction-modal/tx-stages-composed/tx-stage-claim.tsx @@ -0,0 +1,66 @@ +import { TxStagePending } from '../tx-stages-basic/tx-stage-pending'; +import { TxStageSign } from '../tx-stages-basic/tx-stage-sign'; +import { TxAmount } from '../tx-stages-parts/tx-amount'; + +import { TOKENS } from 'consts/tokens'; +import type { BigNumber } from 'ethers'; +import { useBondWillReceive } from 'shared/hooks'; + +type TxStageClaimProps = { + claimRewards: boolean; + amount: BigNumber; + token: TOKENS; + rewards?: BigNumber; + isPending?: boolean; + txHash?: string; +}; + +export const TxStageClaim = ({ + claimRewards, + amount, + token, + rewards, + isPending, + txHash, +}: TxStageClaimProps) => { + const [bondReceive, amountBiggerRewards] = useBondWillReceive( + token, + amount, + claimRewards ? rewards : undefined, + ); + + const operationText = + token === 'ETH' ? 'requesting withdrawal of' : 'claiming'; + const sourceText = claimRewards + ? amountBiggerRewards + ? 'bond and rewards' + : 'rewards' + : 'bond'; + const Component = isPending ? TxStagePending : TxStageSign; + + return ( + + You are {operationText} {sourceText} + + } + description={ + <> +

+ Rewards Address will receive{' '} + . +

+ + {bondReceive.gt(0) && ( +

+ Bond balance will increase by{' '} + . +

+ )} + + } + /> + ); +}; diff --git a/shared/transaction-modal/tx-stages-parts/after-address-proposed.tsx b/shared/transaction-modal/tx-stages-parts/after-address-proposed.tsx new file mode 100644 index 00000000..881ea727 --- /dev/null +++ b/shared/transaction-modal/tx-stages-parts/after-address-proposed.tsx @@ -0,0 +1,35 @@ +import { Text } from '@lidofinance/lido-ui'; +import { FC } from 'react'; +import { Address } from 'shared/components'; +import styled from 'styled-components'; + +type Props = { + address: string; +}; + +export const AfterAddressProposed: FC = ({ address }) => { + return ( + + What is next: +
+
    +
  1. + Connect to CSM UI with the proposed address + +
    + +
  2. +
  3. Go to Roles tab → Inbox requests to confirm the change
  4. +
+
+ ); +}; + +const BlockStyled = styled.div` + text-align: left; + line-height: 24px; + + background-color: var(--lido-color-backgroundSecondary); + border-radius: ${({ theme }) => theme.borderRadiusesMap.lg}px; + padding: ${({ theme }) => theme.spaceMap.md}px; +`; diff --git a/shared/transaction-modal/tx-stages-parts/after-create-custom-node-operator.tsx b/shared/transaction-modal/tx-stages-parts/after-create-custom-node-operator.tsx new file mode 100644 index 00000000..e5daa7a4 --- /dev/null +++ b/shared/transaction-modal/tx-stages-parts/after-create-custom-node-operator.tsx @@ -0,0 +1,90 @@ +import { getExternalLinks } from 'consts/external-links'; +import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events'; +import { useModalActions } from 'providers/modal-provider'; +import { FC } from 'react'; +import { MatomoLink } from 'shared/components'; +import { useBeaconchainDashboardLink } from 'shared/hooks'; +import { Disconnect } from 'shared/wallet'; +import styled from 'styled-components'; +import { NodeOperatorId } from 'types'; + +type Props = { + nodeOperatorId?: NodeOperatorId; + keys: string[]; +}; + +export const AfterCreateCustomNodeOperator: FC = ({ keys }) => { + const beaconchainDashboardLink = useBeaconchainDashboardLink(keys); + const { subscribeEvents, beaconchain } = getExternalLinks(); + const { closeModal } = useModalActions(); + + return ( + <> + + What is next: +
+
    +
  1. + Connect to CSM UI with the address you specified as Reward/Manager + Address +
  2. +
  3. Wait for your keys to be deposited to through the protocol.
  4. +
  5. + Once your keys become active ( + {beaconchain && ( + <> + you can check their statuses on{' '} + + beaconcha.in + {' '} + or{' '} + + )} + subscribe to the{' '} + + CSM events notifications + + ) make sure your validators are producing attestations{' '} + {beaconchainDashboardLink && ( + <> + (you can use the{' '} + + beaconcha.in dashboard + {' '} + to check) + + )} +
  6. +
+
+
+ closeModal()} fullwidth> + Disconnect wallet + + + ); +}; + +const BlockStyled = styled.div` + text-align: left; + line-height: 24px; + + background-color: var(--lido-color-backgroundSecondary); + border-radius: ${({ theme }) => theme.borderRadiusesMap.lg}px; + padding: ${({ theme }) => theme.spaceMap.md}px; +`; 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 83173537..8983c5a9 100644 --- a/shared/transaction-modal/tx-stages-parts/after-keys-upload.tsx +++ b/shared/transaction-modal/tx-stages-parts/after-keys-upload.tsx @@ -22,20 +22,24 @@ export const AfterKeysUpload: FC = () => {
  1. Wait for your keys to be deposited through the protocol.
  2. - Once your keys become active (check the status on the{' '} + Once your keys become active (you can check their statuses on the{' '} Keys tab - , on{' '} - - beaconcha.in - {' '} + {beaconchain && ( + <> + , on{' '} + + beaconcha.in + + + )}{' '} or subscribe to the {/* DAPPNODE */} CSM Telegram notifications) diff --git a/shared/transaction-modal/tx-stages-parts/index.ts b/shared/transaction-modal/tx-stages-parts/index.ts index 62fece23..0079ee9f 100644 --- a/shared/transaction-modal/tx-stages-parts/index.ts +++ b/shared/transaction-modal/tx-stages-parts/index.ts @@ -1,3 +1,5 @@ +export * from './after-address-proposed'; export * from './after-keys-upload'; export * from './success-text'; export * from './tx-amount'; +export * from './after-create-custom-node-operator'; diff --git a/shared/wallet/disconnect/disconnect.tsx b/shared/wallet/disconnect/disconnect.tsx new file mode 100644 index 00000000..25cf52d4 --- /dev/null +++ b/shared/wallet/disconnect/disconnect.tsx @@ -0,0 +1,32 @@ +import { Button, ButtonProps } from '@lidofinance/lido-ui'; +import { FC, MouseEventHandler, PropsWithChildren, useCallback } from 'react'; +import { useDisconnect } from 'reef-knot/core-react'; + +import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events'; +import { trackMatomoEvent, WithMatomoEvent } from 'utils'; + +export const Disconnect: FC< + PropsWithChildren> +> = ({ + children, + matomoEvent = MATOMO_CLICK_EVENTS_TYPES.disconnectWallet, + onClick, + ...rest +}) => { + const { disconnect } = useDisconnect(); + + const handleClick: MouseEventHandler = useCallback( + (e) => { + trackMatomoEvent(matomoEvent); + disconnect?.(); + onClick?.(e); + }, + [disconnect, matomoEvent, onClick], + ); + + return ( + + ); +}; diff --git a/shared/wallet/index.ts b/shared/wallet/index.ts index 1084723c..150c2a03 100644 --- a/shared/wallet/index.ts +++ b/shared/wallet/index.ts @@ -1,4 +1,5 @@ export { Button } from './button/button'; export { Connect } from './connect/connect'; +export { Disconnect } from './disconnect/disconnect'; export * from './wallet-modal'; export * from './fallback'; diff --git a/shared/wallet/wallet-modal/styles.tsx b/shared/wallet/wallet-modal/styles.tsx index 93f1f2c1..cec0dd2c 100644 --- a/shared/wallet/wallet-modal/styles.tsx +++ b/shared/wallet/wallet-modal/styles.tsx @@ -1,5 +1,5 @@ -import { Button } from '@lidofinance/lido-ui'; import styled from 'styled-components'; +import { Disconnect } from '../disconnect/disconnect'; export const WalletModalContentStyle = styled.div` background-color: var(--lido-color-background); @@ -22,9 +22,7 @@ export const WalletModalConnectorStyle = styled.div` margin-right: auto; `; -export const WalletModalDisconnectStyle = styled((props) => ( -