From 06e75ce54cfe760da81d4021bab191818b99c708 Mon Sep 17 00:00:00 2001 From: axelrod-ssvlabs <128966996+axelrod-blox@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:35:52 +0500 Subject: [PATCH 01/13] fix: hide SSV balance display for migrated operators with zero yearly fee (#1728) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.5 --- src/app/routes/dashboard/operators/operator.tsx | 3 +-- src/types/api.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/routes/dashboard/operators/operator.tsx b/src/app/routes/dashboard/operators/operator.tsx index 3d78c6a60..483a2304f 100644 --- a/src/app/routes/dashboard/operators/operator.tsx +++ b/src/app/routes/dashboard/operators/operator.tsx @@ -24,7 +24,6 @@ export const Operator: FC> = ({ ...props }) => { const params = useOperatorPageParams(); const operatorId = BigInt(params.operatorId!); const operator = useOperator(operatorId!); - const { feeEth, yearlyFeeEth, yearlyFeeSSV, balanceEth, balanceSSV } = useOperatorEarningsAndFees(operatorId); @@ -104,7 +103,7 @@ export const Operator: FC> = ({ ...props }) => {
- + {yearlyFeeSSV !== 0n && !operator.data.migrated && }
Date: Mon, 16 Feb 2026 22:10:21 +0500 Subject: [PATCH 02/13] =?UTF-8?q?fix:=20align=20migration=20flow=20liquida?= =?UTF-8?q?tion=20collateral=20calculation=20with=20sta=E2=80=A6=20(#1732)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: align migration flow liquidation collateral calculation with standard flow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 * fix: remove unused validatorsAmount param from switch wizard step two 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --------- Co-authored-by: Claude Sonnet 4.5 --- src/components/wizard/switch-wizard-step-two.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/wizard/switch-wizard-step-two.tsx b/src/components/wizard/switch-wizard-step-two.tsx index 327f57348..857c05a88 100644 --- a/src/components/wizard/switch-wizard-step-two.tsx +++ b/src/components/wizard/switch-wizard-step-two.tsx @@ -65,7 +65,6 @@ export const SwitchWizardStepTwo = ({ navigateRoutePath, navigateRouteOptions, operators = [], - validatorsAmount = 1, effectiveBalance, currentRunwayDays = 0, ssvBalance, @@ -115,24 +114,22 @@ export const SwitchWizardStepTwo = ({ const operatorsCost = computeDailyAmount(operatorsFee, days); const networkCost = computeDailyAmount(networkFee, days); + const validators = effectiveBalanceWei / perValidatorBalance || 1n; const liquidationCost = computeLiquidationCollateralCostPerValidator({ networkFee, operatorsFee, liquidationCollateralPeriod: liquidationThreshold, minimumLiquidationCollateral, - effectiveBalance: BigInt(validatorsAmount || 1) * 32n, + effectiveBalance: effectiveBalanceWei, }); const operatorsPerEth = operatorsCost / 32n; const networkPerEth = networkCost / 32n; const liquidationPerEth = liquidationCost / 32n; - const operatorsSubtotal = - (operatorsCost * effectiveBalanceWei) / perValidatorBalance; - const networkSubtotal = - (networkCost * effectiveBalanceWei) / perValidatorBalance; - const liquidationSubtotal = - (liquidationCost * effectiveBalanceWei) / perValidatorBalance; + const operatorsSubtotal = operatorsCost * validators; + const networkSubtotal = networkCost * validators; + const liquidationSubtotal = liquidationCost * validators; const totalDeposit = operatorsSubtotal + networkSubtotal + liquidationSubtotal; From 5cfeaf4fbb0e304132db5090736ffcd89adf09e8 Mon Sep 17 00:00:00 2001 From: Lior Rutenberg Date: Mon, 16 Feb 2026 20:54:02 +0200 Subject: [PATCH 03/13] fix: remove incorrect /32 division from migration funding fee display PR #1732 introduced a `/ 32n` division on the Fee column values (operatorsPerEth, networkPerEth, liquidationPerEth) causing them to display ~32x smaller than the registration flow. Also restore the subtotal formula to use `(cost * effectiveBalanceWei) / perValidatorBalance` to match the registration flow's `computeFundingCost` logic. Co-Authored-By: Claude Opus 4.6 --- src/components/wizard/switch-wizard-step-two.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/wizard/switch-wizard-step-two.tsx b/src/components/wizard/switch-wizard-step-two.tsx index 857c05a88..e8948e911 100644 --- a/src/components/wizard/switch-wizard-step-two.tsx +++ b/src/components/wizard/switch-wizard-step-two.tsx @@ -114,7 +114,6 @@ export const SwitchWizardStepTwo = ({ const operatorsCost = computeDailyAmount(operatorsFee, days); const networkCost = computeDailyAmount(networkFee, days); - const validators = effectiveBalanceWei / perValidatorBalance || 1n; const liquidationCost = computeLiquidationCollateralCostPerValidator({ networkFee, operatorsFee, @@ -123,13 +122,16 @@ export const SwitchWizardStepTwo = ({ effectiveBalance: effectiveBalanceWei, }); - const operatorsPerEth = operatorsCost / 32n; - const networkPerEth = networkCost / 32n; - const liquidationPerEth = liquidationCost / 32n; + const operatorsPerEth = operatorsCost; + const networkPerEth = networkCost; + const liquidationPerEth = liquidationCost; - const operatorsSubtotal = operatorsCost * validators; - const networkSubtotal = networkCost * validators; - const liquidationSubtotal = liquidationCost * validators; + const operatorsSubtotal = + (operatorsCost * effectiveBalanceWei) / perValidatorBalance; + const networkSubtotal = + (networkCost * effectiveBalanceWei) / perValidatorBalance; + const liquidationSubtotal = + (liquidationCost * effectiveBalanceWei) / perValidatorBalance; const totalDeposit = operatorsSubtotal + networkSubtotal + liquidationSubtotal; From 8e2707512b27f0776204af1472c93d454ba1971d Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 17 Feb 2026 12:26:30 +0100 Subject: [PATCH 04/13] fix: change of labels for liquidation during migration --- src/components/wizard/switch-wizard-step-three.tsx | 9 ++------- src/lib/utils/operator.ts | 1 + 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/wizard/switch-wizard-step-three.tsx b/src/components/wizard/switch-wizard-step-three.tsx index fc95c4d29..315bb409a 100644 --- a/src/components/wizard/switch-wizard-step-three.tsx +++ b/src/components/wizard/switch-wizard-step-three.tsx @@ -190,12 +190,7 @@ export const SwitchWizardStepThree = ({ {formatEthValue(fundingSummary?.networkSubtotal)} -
- Liquidation collateral - - - -
+ Liquidation collateral {formatEthValue(fundingSummary?.liquidationPerEth)} @@ -232,7 +227,7 @@ export const SwitchWizardStepThree = ({
Withdraw SSV - +
diff --git a/src/lib/utils/operator.ts b/src/lib/utils/operator.ts index c3d872f14..633da1977 100644 --- a/src/lib/utils/operator.ts +++ b/src/lib/utils/operator.ts @@ -204,6 +204,7 @@ export const createDefaultOperator = ( updated_at: 0, ...operator, effective_balance: operator.effective_balance ?? "0", + migrated: true, }); export type MainnetEvent = DecodeEventLogReturnType; From 24962fb6ea0b718cc03fe73b53017ddf94c13601 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 17 Feb 2026 12:47:30 +0100 Subject: [PATCH 05/13] fix: change of statuses used to determine deposited and not deposited validators --- .../existing-cluster-validators-breakdown.tsx | 11 ++--------- .../migration-effective-balance-form.tsx | 1 - 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/effective-balance/existing-cluster-validators-breakdown.tsx b/src/components/effective-balance/existing-cluster-validators-breakdown.tsx index 3866f4db9..2e6734470 100644 --- a/src/components/effective-balance/existing-cluster-validators-breakdown.tsx +++ b/src/components/effective-balance/existing-cluster-validators-breakdown.tsx @@ -46,18 +46,11 @@ export const ExistingClusterValidatorsBreakdown: FC< break; case "deposited": setFilters({ - status: [ - "active", - "exited", - "exiting", - "slashed", - "pending", - "inactive", - ], + status: ["active", "exiting", "slashed", "pending"], }); break; case "notDeposited": - setFilters({ status: ["notDeposited"] }); + setFilters({ status: ["notDeposited", "exited", "inactive"] }); break; } _setSelectedTab(value as TabKey); diff --git a/src/components/effective-balance/migration-effective-balance-form.tsx b/src/components/effective-balance/migration-effective-balance-form.tsx index 043194479..afe081056 100644 --- a/src/components/effective-balance/migration-effective-balance-form.tsx +++ b/src/components/effective-balance/migration-effective-balance-form.tsx @@ -70,7 +70,6 @@ export const MigrationEffectiveBalanceForm: FC = ({ const confirmId = useId(); const balanceValue = form.watch("totalEffectiveBalance"); - console.log("balanceValue:", balanceValue); const balanceError = form.formState.errors.totalEffectiveBalance?.type; const isLowBalance = balanceError === "too_small"; From 5ebd1aa1122122a2fcc72383731ce3568f465664 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 17 Feb 2026 14:05:01 +0100 Subject: [PATCH 06/13] fix: change the sizes of private operator icon to be responsive --- src/components/operator/operator-avatar.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/operator/operator-avatar.tsx b/src/components/operator/operator-avatar.tsx index 2e2b3d411..f02f9e78a 100644 --- a/src/components/operator/operator-avatar.tsx +++ b/src/components/operator/operator-avatar.tsx @@ -50,13 +50,14 @@ export const OperatorAvatar = forwardRef( {...props} > {isPrivate && ( -
- +
+
)} {src ? ( Operator Avatar From 24e5864e2d5446f50f2c423d937950e9614d8917 Mon Sep 17 00:00:00 2001 From: sumbat-ssvlabs Date: Tue, 17 Feb 2026 15:59:39 +0200 Subject: [PATCH 07/13] ci: changed contract addresses --- .github/workflows/build_deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml index aa4340f82..60ffe2d00 100644 --- a/.github/workflows/build_deploy.yml +++ b/.github/workflows/build_deploy.yml @@ -41,8 +41,8 @@ jobs: \"insufficientBalanceUrl\": \"https://faucet.stage.ssv.network\", \"googleTagSecret\": \"${{ secrets.STAGE_GOOGLE_TAG_SECRET }}\", \"tokenAddress\": \"0x746c33ccc28b1363c35c09badaf41b2ffa7e6d56\", - \"setterContractAddress\": \"0x0aaace4e8affc47c6834171c88d342a4abd8f105\", - \"getterContractAddress\": \"0x9143b8c25efa53f28de4cbefd0b6dfd66d43fea6\" + \"setterContractAddress\": \"0x384AC2c8AF4Df1faD7E20F15064B2C2917fAa7a3\", + \"getterContractAddress\": \"0x92c71f0A9823789f72BAEBB2BFE39e897bDd26bd\" } ] PROD_SSV_NETWORKS: > From 6d8c1208432bd1e3e25113551d8a87e43bc04b3a Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 17 Feb 2026 17:01:26 +0100 Subject: [PATCH 08/13] fix: use effective balance in ETH for migration liquidation collateral --- src/components/wizard/switch-wizard-step-two.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/wizard/switch-wizard-step-two.tsx b/src/components/wizard/switch-wizard-step-two.tsx index e8948e911..d9ecb34f4 100644 --- a/src/components/wizard/switch-wizard-step-two.tsx +++ b/src/components/wizard/switch-wizard-step-two.tsx @@ -99,9 +99,10 @@ export const SwitchWizardStepTwo = ({ 0n, ); + const weiPerEth = 10n ** 18n; const effectiveBalanceWei = effectiveBalance ?? 0n; + const effectiveBalanceEth = effectiveBalanceWei / weiPerEth; const ethRate = rates.data?.eth ?? 0; - const weiPerEth = 10n ** 18n; const perValidatorBalance = 32n * weiPerEth; const getCostsForDays = (days: number) => { @@ -119,7 +120,7 @@ export const SwitchWizardStepTwo = ({ operatorsFee, liquidationCollateralPeriod: liquidationThreshold, minimumLiquidationCollateral, - effectiveBalance: effectiveBalanceWei, + effectiveBalance: effectiveBalanceEth, }); const operatorsPerEth = operatorsCost; From b5d26af9308c82e5d15d6762b8f75b586aa0eddf Mon Sep 17 00:00:00 2001 From: sumbat-ssvlabs Date: Tue, 17 Feb 2026 18:06:06 +0200 Subject: [PATCH 09/13] fix: prefix (#1742) --- src/hooks/use-links.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/hooks/use-links.ts b/src/hooks/use-links.ts index b61c93504..914e8788b 100644 --- a/src/hooks/use-links.ts +++ b/src/hooks/use-links.ts @@ -1,20 +1,19 @@ import { useMemo } from "react"; import { useAccount } from "@/hooks/account/use-account"; -const isProduction = location.hostname === "app.ssv.network"; // TODO: determine production through build.yaml environment variable +// const isProduction = location.hostname === "app.ssv.network"; // TODO: determine production through build.yaml environment variable export const useLinks = () => { const { chain } = useAccount(); return useMemo(() => { const chainPrefix = chain?.testnet ? `${chain.name.toLowerCase()}.` : ""; - const envPrefix = isProduction ? "" : `.stage`; return { beaconcha: `https://${chainPrefix}beaconcha.in`, launchpad: `https://${chainPrefix}launchpad.ethereum.org`, - etherscan: `https://${envPrefix}etherscan.io`, + etherscan: `https://${chainPrefix}etherscan.io`, ssv: { - explorer: `https://explorer${envPrefix}.ssv.network/`, - stake: `https://stake${envPrefix}.ssv.network`, + explorer: `https://explorer.${chainPrefix}ssv.network/`, + stake: `https://stake.${chainPrefix}ssv.network`, docs: `https://docs.ssv.network`, forum: `https://forum.ssv.network/`, governanceForum: `https://forum.ssv.network/`, From 6227525ecae523bd9a69cd4c2d144f58fe8147f9 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 18 Feb 2026 10:24:58 +0100 Subject: [PATCH 10/13] fix: align liquidation collateral math with shared funding logic --- .../wizard/switch-wizard-step-two.tsx | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/src/components/wizard/switch-wizard-step-two.tsx b/src/components/wizard/switch-wizard-step-two.tsx index d9ecb34f4..5ceaa560d 100644 --- a/src/components/wizard/switch-wizard-step-two.tsx +++ b/src/components/wizard/switch-wizard-step-two.tsx @@ -22,10 +22,7 @@ import { Spacer } from "@/components/ui/spacer"; import type { Operator } from "@/types/api"; import { currencyFormatter, formatETH } from "@/lib/utils/number"; import { useRates } from "@/hooks/use-rates"; -import { - computeDailyAmount, - computeLiquidationCollateralCostPerValidator, -} from "@/lib/utils/keystore"; +import { computeFundingCost } from "@/lib/utils/keystore"; import { useNetworkFee } from "@/hooks/use-ssv-network-fee"; import { formatUnits } from "viem"; import type { NavigateOptions } from "react-router-dom"; @@ -103,7 +100,6 @@ export const SwitchWizardStepTwo = ({ const effectiveBalanceWei = effectiveBalance ?? 0n; const effectiveBalanceEth = effectiveBalanceWei / weiPerEth; const ethRate = rates.data?.eth ?? 0; - const perValidatorBalance = 32n * weiPerEth; const getCostsForDays = (days: number) => { if (!networkFees.isSuccess || days <= 0) return null; @@ -113,37 +109,23 @@ export const SwitchWizardStepTwo = ({ const minimumLiquidationCollateral = networkFees.minimumLiquidationCollateral.data ?? 0n; - const operatorsCost = computeDailyAmount(operatorsFee, days); - const networkCost = computeDailyAmount(networkFee, days); - const liquidationCost = computeLiquidationCollateralCostPerValidator({ - networkFee, + const cost = computeFundingCost({ + fundingDays: days, operatorsFee, + networkFee, liquidationCollateralPeriod: liquidationThreshold, minimumLiquidationCollateral, effectiveBalance: effectiveBalanceEth, }); - const operatorsPerEth = operatorsCost; - const networkPerEth = networkCost; - const liquidationPerEth = liquidationCost; - - const operatorsSubtotal = - (operatorsCost * effectiveBalanceWei) / perValidatorBalance; - const networkSubtotal = - (networkCost * effectiveBalanceWei) / perValidatorBalance; - const liquidationSubtotal = - (liquidationCost * effectiveBalanceWei) / perValidatorBalance; - const totalDeposit = - operatorsSubtotal + networkSubtotal + liquidationSubtotal; - return { - operatorsPerEth, - networkPerEth, - liquidationPerEth, - operatorsSubtotal, - networkSubtotal, - liquidationSubtotal, - totalDeposit, + operatorsPerEth: cost.perValidator.operatorsCost, + networkPerEth: cost.perValidator.networkCost, + liquidationPerEth: cost.perValidator.liquidationCollateral, + operatorsSubtotal: cost.subtotal.operatorsCost, + networkSubtotal: cost.subtotal.networkCost, + liquidationSubtotal: cost.subtotal.liquidationCollateral, + totalDeposit: cost.total, }; }; From 9a29ba4957a57d88bd0e36ef015d2dae8081a932 Mon Sep 17 00:00:00 2001 From: sumbat-ssvlabs Date: Wed, 18 Feb 2026 13:39:25 +0200 Subject: [PATCH 11/13] fix: funding calculation (#1744) --- .github/workflows/build_deploy.yml | 3 + .husky/pre-commit | 1 + .../routes/create-cluster/initial-funding.tsx | 8 +- src/app/routes/create-cluster/reactivate.tsx | 8 +- .../cluster/cluster-funding-summary.tsx | 4 +- .../wizard/switch-wizard-step-two.tsx | 86 +++++----- src/hooks/use-compute-funding-cost.ts | 5 +- src/lib/utils/__tests__/funding.test.ts | 147 ++++++++++++++++++ src/lib/utils/keystore.ts | 68 ++++---- 9 files changed, 252 insertions(+), 78 deletions(-) create mode 100644 src/lib/utils/__tests__/funding.test.ts diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml index 60ffe2d00..1a63406bc 100644 --- a/.github/workflows/build_deploy.yml +++ b/.github/workflows/build_deploy.yml @@ -87,6 +87,9 @@ jobs: - name: Run lint run: pnpm lint + - name: Run tests + run: pnpm run test:run + - name: Run semantic-release if: github.event_name == 'push' && (github.ref == 'refs/heads/stage' || github.ref == 'refs/heads/main') env: diff --git a/.husky/pre-commit b/.husky/pre-commit index 79fde575c..374e37fdd 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,5 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" +pnpm run test:run pnpm run lint-staged diff --git a/src/app/routes/create-cluster/initial-funding.tsx b/src/app/routes/create-cluster/initial-funding.tsx index 81b5150b3..32169b404 100644 --- a/src/app/routes/create-cluster/initial-funding.tsx +++ b/src/app/routes/create-cluster/initial-funding.tsx @@ -1,6 +1,6 @@ import { useComputeFundingCost, - useFundingCost, + useFundingCostETH, } from "@/hooks/use-compute-funding-cost"; import type { ComponentPropsWithoutRef, FC } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -88,19 +88,19 @@ export const InitialFunding: FCProps = ({ ...props }) => { days && days < globals.CLUSTER_VALIDITY_PERIOD_MINIMUM, ); - const customFundingCost = useFundingCost({ + const customFundingCost = useFundingCostETH({ fundingDays: values.custom, operators: operators.data ?? [], effectiveBalance, }); - const yearFundingCost = useFundingCost({ + const yearFundingCost = useFundingCostETH({ fundingDays: periods.year, operators: operators.data ?? [], effectiveBalance, }); - const halfYearFundingCost = useFundingCost({ + const halfYearFundingCost = useFundingCostETH({ fundingDays: periods["half-year"], operators: operators.data ?? [], effectiveBalance, diff --git a/src/app/routes/create-cluster/reactivate.tsx b/src/app/routes/create-cluster/reactivate.tsx index 46421a2a2..557ab4d40 100644 --- a/src/app/routes/create-cluster/reactivate.tsx +++ b/src/app/routes/create-cluster/reactivate.tsx @@ -27,7 +27,7 @@ import { useClusterPageParams } from "@/hooks/cluster/use-cluster-page-params"; import { useOperators } from "@/hooks/operator/use-operators"; import { useComputeFundingCost, - useFundingCost, + useFundingCostETH, } from "@/hooks/use-compute-funding-cost"; import { withTransactionModal } from "@/lib/contract-interactions/utils/useWaitForTransactionReceipt"; import { useReactivate } from "@/lib/contract-interactions/write/use-reactivate"; @@ -104,19 +104,19 @@ export const ReactivateCluster: FCProps = ({ ...props }) => { days && days < globals.CLUSTER_VALIDITY_PERIOD_MINIMUM, ); - const customFundingCost = useFundingCost({ + const customFundingCost = useFundingCostETH({ fundingDays: values.custom, operators: operators.data ?? [], effectiveBalance, }); - const yearFundingCost = useFundingCost({ + const yearFundingCost = useFundingCostETH({ fundingDays: periods.year, operators: operators.data ?? [], effectiveBalance, }); - const halfYearFundingCost = useFundingCost({ + const halfYearFundingCost = useFundingCostETH({ fundingDays: periods["half-year"], operators: operators.data ?? [], effectiveBalance, diff --git a/src/components/cluster/cluster-funding-summary.tsx b/src/components/cluster/cluster-funding-summary.tsx index 5001db2a5..67c1b816a 100644 --- a/src/components/cluster/cluster-funding-summary.tsx +++ b/src/components/cluster/cluster-funding-summary.tsx @@ -1,7 +1,7 @@ import type { FC, ComponentPropsWithoutRef } from "react"; import { cn } from "@/lib/utils/tw"; import type { UseFundingCostArgs } from "@/hooks/use-compute-funding-cost"; -import { useFundingCost } from "@/hooks/use-compute-funding-cost"; +import { useFundingCostETH } from "@/hooks/use-compute-funding-cost"; import { Text } from "@/components/ui/text"; import { formatSSV } from "@/lib/utils/number"; import { Divider } from "@/components/ui/divider"; @@ -23,7 +23,7 @@ export const ClusterFundingSummary: ClusterFundingSummaryFC = ({ ...props }) => { // const isBulk = validatorsAmount > 1; - const cost = useFundingCost({ + const cost = useFundingCostETH({ operators, fundingDays, effectiveBalance, diff --git a/src/components/wizard/switch-wizard-step-two.tsx b/src/components/wizard/switch-wizard-step-two.tsx index 5ceaa560d..1e48504e6 100644 --- a/src/components/wizard/switch-wizard-step-two.tsx +++ b/src/components/wizard/switch-wizard-step-two.tsx @@ -22,8 +22,7 @@ import { Spacer } from "@/components/ui/spacer"; import type { Operator } from "@/types/api"; import { currencyFormatter, formatETH } from "@/lib/utils/number"; import { useRates } from "@/hooks/use-rates"; -import { computeFundingCost } from "@/lib/utils/keystore"; -import { useNetworkFee } from "@/hooks/use-ssv-network-fee"; +import { useFundingCostETH } from "@/hooks/use-compute-funding-cost"; import { formatUnits } from "viem"; import type { NavigateOptions } from "react-router-dom"; import type { SwitchWizardStepThreeState } from "./switch-wizard-types"; @@ -67,7 +66,6 @@ export const SwitchWizardStepTwo = ({ ssvBalance, }: SwitchWizardStepTwoProps) => { const rates = useRates(); - const networkFees = useNetworkFee(); const hasSsvBalance = (ssvBalance ?? 0n) > 0n; const form = useForm>({ @@ -91,49 +89,61 @@ export const SwitchWizardStepTwo = ({ ? currentRunwayDays : periods[values.selected]; - const operatorsFee = operators.reduce( - (sum, operator) => sum + BigInt(operator.eth_fee || "0"), - 0n, - ); - const weiPerEth = 10n ** 18n; const effectiveBalanceWei = effectiveBalance ?? 0n; const effectiveBalanceEth = effectiveBalanceWei / weiPerEth; const ethRate = rates.data?.eth ?? 0; - const getCostsForDays = (days: number) => { - if (!networkFees.isSuccess || days <= 0) return null; - const networkFee = networkFees.ssvNetworkFee.data ?? 0n; - const liquidationThreshold = - networkFees.liquidationThresholdPeriod.data ?? 0n; - const minimumLiquidationCollateral = - networkFees.minimumLiquidationCollateral.data ?? 0n; + const currentCostsQuery = useFundingCostETH({ + fundingDays: currentRunwayDays, + operators, + effectiveBalance: effectiveBalanceEth, + }); - const cost = computeFundingCost({ - fundingDays: days, - operatorsFee, - networkFee, - liquidationCollateralPeriod: liquidationThreshold, - minimumLiquidationCollateral, - effectiveBalance: effectiveBalanceEth, - }); + const halfYearCostsQuery = useFundingCostETH({ + fundingDays: periods["half-year"], + operators, + effectiveBalance: effectiveBalanceEth, + }); + + const yearCostsQuery = useFundingCostETH({ + fundingDays: periods.year, + operators, + effectiveBalance: effectiveBalanceEth, + }); + + const customCostsQuery = useFundingCostETH({ + fundingDays: values.custom, + operators, + effectiveBalance: effectiveBalanceEth, + }); + + const mapQueryToCosts = (data: typeof halfYearCostsQuery.data) => + data + ? { + operatorsPerEth: data.perValidator.operatorsCost, + networkPerEth: data.perValidator.networkCost, + liquidationPerEth: data.perValidator.liquidationCollateral, + operatorsSubtotal: data.subtotal.operatorsCost, + networkSubtotal: data.subtotal.networkCost, + liquidationSubtotal: data.subtotal.liquidationCollateral, + totalDeposit: data.total, + } + : null; - return { - operatorsPerEth: cost.perValidator.operatorsCost, - networkPerEth: cost.perValidator.networkCost, - liquidationPerEth: cost.perValidator.liquidationCollateral, - operatorsSubtotal: cost.subtotal.operatorsCost, - networkSubtotal: cost.subtotal.networkCost, - liquidationSubtotal: cost.subtotal.liquidationCollateral, - totalDeposit: cost.total, - }; - }; + const currentCosts = mapQueryToCosts(currentCostsQuery.data); + const halfYearCosts = mapQueryToCosts(halfYearCostsQuery.data); + const yearCosts = mapQueryToCosts(yearCostsQuery.data); + const customCosts = mapQueryToCosts(customCostsQuery.data); - const selectedCosts = getCostsForDays(selectedDays); - const currentCosts = getCostsForDays(currentRunwayDays); - const halfYearCosts = getCostsForDays(periods["half-year"]); - const yearCosts = getCostsForDays(periods.year); - const customCosts = getCostsForDays(values.custom); + const selectedCosts = + values.selected === "current" + ? currentCosts + : values.selected === "half-year" + ? halfYearCosts + : values.selected === "year" + ? yearCosts + : customCosts; const formatEth = (value?: bigint) => value !== undefined ? `${formatETH(value)} ETH` : "-"; diff --git a/src/hooks/use-compute-funding-cost.ts b/src/hooks/use-compute-funding-cost.ts index 5f326f925..065dffc95 100644 --- a/src/hooks/use-compute-funding-cost.ts +++ b/src/hooks/use-compute-funding-cost.ts @@ -43,10 +43,11 @@ export const useComputeFundingCost = () => { export type UseFundingCostArgs = { operators: Pick[]; fundingDays: number; + /** Effective balance in ETH (human-readable). Examples: 32n (1 validator), 64n (2 validators) */ effectiveBalance: bigint; }; -export const useFundingCost = ({ +export const useFundingCostETH = ({ operators, fundingDays, effectiveBalance, @@ -70,7 +71,7 @@ export const useFundingCost = ({ ]), queryFn: async () => computeFundingCost({ - operatorsFee: sumOperatorsFee(operators), + operatorsFee: sumOperatorsFee(operators, "eth"), fundingDays, networkFee: ssvNetworkFee.data!, liquidationCollateralPeriod: liquidationThresholdPeriod.data!, diff --git a/src/lib/utils/__tests__/funding.test.ts b/src/lib/utils/__tests__/funding.test.ts new file mode 100644 index 000000000..078eeac80 --- /dev/null +++ b/src/lib/utils/__tests__/funding.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect } from "vitest"; +import { computeFundingCost } from "@/lib/utils/keystore"; +import { globals } from "@/config"; + +// Abbreviations: NF=networkFee, OF=operatorsFee, LCP=liquidationCollateralPeriod, +// MLC=minimumLiquidationCollateral, BPD=blocksPerDay + +describe("util:keystore", () => { + it("computeFundingCost calculates correctly for 1 validator, 1 day funding", () => { + // 1 validator (effectiveBalance=32), 1 day, 7160 BPD + // 1 NF, 1 OF → daily cost = fee × BPD = 7160 + // liquidation: (1 NF + 1 OF) × 2 LCP × 1 = 4, max(4, 10 MLC) = 10 + + const operatorsFee = 1n; + const networkFee = 1n; + + const liquidationCollateralPeriod = 2n; + const minimumLiquidationCollateral = 10n; + + const result = computeFundingCost({ + fundingDays: 1, + effectiveBalance: 32n, + networkFee, + operatorsFee, + liquidationCollateralPeriod, + minimumLiquidationCollateral, + }); + + const networkCost = networkFee * globals.BLOCKS_PER_DAY; + const operatorsCost = operatorsFee * globals.BLOCKS_PER_DAY; + + // 1 NF × 7160 BPD = 7160 + expect(result.perValidator.networkCost).toBe(networkCost); + expect(result.perValidator.operatorsCost).toBe(operatorsCost); + // (1+1) × 2 LCP × 1 = 4; max(4, 10 MLC) = 10 + expect(result.perValidator.liquidationCollateral).toBe( + minimumLiquidationCollateral, + ); + + // 1 validator → subtotal = perValidator + expect(result.subtotal.networkCost).toBe(networkCost); + expect(result.subtotal.operatorsCost).toBe(operatorsCost); + expect(result.subtotal.liquidationCollateral).toBe( + minimumLiquidationCollateral, + ); + + // total = network + operators + liquidation + expect(result.total).toBe(networkCost + operatorsCost + 10n); + }); + + describe("liquidation collateral", () => { + it("uses minimum when calculated collateral is below minimum", () => { + // (1nf+1opf) × 2 LCP × 1 = 4; max(4, 10 MLC) = 10 + const result = computeFundingCost({ + fundingDays: 1, + effectiveBalance: 32n, + networkFee: 1n, + operatorsFee: 1n, + liquidationCollateralPeriod: 2n, + minimumLiquidationCollateral: 10n, + }); + + // (1+1) × 2 LCP × 1 = 4 < 10 MLC → use 10 MLC + expect(result.perValidator.liquidationCollateral).toBe(10n); + // 1 validator × 10 + expect(result.subtotal.liquidationCollateral).toBe(10n); + }); + + it("uses calculated collateral when above minimum", () => { + // (10+10) × 5 LCP × 2 = 200 > 50 MLC → 200/2 = 100 + const result = computeFundingCost({ + fundingDays: 1, + effectiveBalance: 64n, // 2 validators + networkFee: 10n, + operatorsFee: 10n, + liquidationCollateralPeriod: 5n, + minimumLiquidationCollateral: 50n, + }); + + // (10+10) × 5 LCP × 2 = 200 > 50 MLC → 200/2 = 100 per validator + expect(result.perValidator.liquidationCollateral).toBe(100n); + // 2 validators × 100 + expect(result.subtotal.liquidationCollateral).toBe(200n); + }); + }); + + it("computeFundingCost uses scaled validators for fractional count (48 effectiveBalance = 1.5)", () => { + // 48/32 = 1.5 validators; validatorsScaled = 48×10⁶/32 = 1500000 + const result = computeFundingCost({ + fundingDays: 1, + effectiveBalance: 48n, + networkFee: 1n, + operatorsFee: 1n, + liquidationCollateralPeriod: 2n, + minimumLiquidationCollateral: 10n, + }); + + const networkCost = 1n * globals.BLOCKS_PER_DAY; + const operatorsCost = 1n * globals.BLOCKS_PER_DAY; + + // perValidator unchanged (base rate for 1 validator) + expect(result.perValidator.networkCost).toBe(networkCost); + expect(result.perValidator.operatorsCost).toBe(operatorsCost); + + // subtotal = cost × 1.5 (scaled math): (7160 × 1500000) / 10⁶ = 10740 + expect(result.subtotal.networkCost).toBe(10740n); + expect(result.subtotal.operatorsCost).toBe(10740n); + + // liquidation: (1+1)×2×1500000=6M < 10 MLC×scale → 10⁷/1500000 = 6 per validator + expect(result.perValidator.liquidationCollateral).toBe(6n); + // (6 × 1500000) / 10⁶ = 9 + expect(result.subtotal.liquidationCollateral).toBe(9n); + + expect(result.total).toBe(10740n + 10740n + 9n); + }); + + it("computeFundingCost scales network and operators cost by funding days", () => { + // 1 validator, 3 days → costs are 3× the 1-day amounts + // liquidation is one-time collateral, unchanged by days + const operatorsFee = 1n; + const networkFee = 1n; + const fundingDays = 3; + + const result = computeFundingCost({ + fundingDays, + effectiveBalance: 32n, + networkFee, + operatorsFee, + liquidationCollateralPeriod: 2n, + minimumLiquidationCollateral: 10n, + }); + + const networkCost = + networkFee * BigInt(fundingDays) * globals.BLOCKS_PER_DAY; + const operatorsCost = + operatorsFee * BigInt(fundingDays) * globals.BLOCKS_PER_DAY; + + // 1 NF × 3 days × 7160 BPD = 21480 + expect(result.perValidator.networkCost).toBe(networkCost); + expect(result.perValidator.operatorsCost).toBe(operatorsCost); + // liquidation one-time, not scaled by days → (1+1) × 2 LCP × 1 = 4, max(4, 10 MLC) = 10 + expect(result.perValidator.liquidationCollateral).toBe(10n); + + // 1 validator × (network + operators) + liquidation + expect(result.total).toBe(networkCost + operatorsCost + 10n); + }); +}); diff --git a/src/lib/utils/keystore.ts b/src/lib/utils/keystore.ts index d02c477d0..d01ed4820 100644 --- a/src/lib/utils/keystore.ts +++ b/src/lib/utils/keystore.ts @@ -1,19 +1,24 @@ import { globals } from "@/config"; import { bigintMax } from "./bigint"; import type { Prettify } from "@/types/ts-utils"; -import { calculateRunway } from "@/lib/utils/cluster"; +import { formatETH } from "@/lib/utils/number"; + +const SCALE = 10 ** 6; +const SCALE_N = BigInt(SCALE); export const computeDailyAmount = (value: bigint, days: number) => { - const scale = 10 ** 6; - const scaledDays = BigInt(days * scale); - return (value * scaledDays * BigInt(globals.BLOCKS_PER_DAY)) / BigInt(scale); + const scaledDays = BigInt(days * SCALE); + return (value * scaledDays * BigInt(globals.BLOCKS_PER_DAY)) / SCALE_N; }; type LiquidationCollateralCostArgs = { + /** Network fee per one block */ networkFee: bigint; + /** Operators fee per one block */ operatorsFee: bigint; liquidationCollateralPeriod: bigint; minimumLiquidationCollateral: bigint; + /** Effective balance in ETH (human-readable). Examples: 32n (1 validator), 64n (2 validators) */ effectiveBalance: bigint; }; @@ -24,49 +29,45 @@ export const computeLiquidationCollateralCostPerValidator = ({ minimumLiquidationCollateral, effectiveBalance, }: LiquidationCollateralCostArgs) => { - const validators = effectiveBalance / 32n || 1n; const total = - (operatorsFee + networkFee) * - liquidationCollateralPeriod * - BigInt(validators); + ((operatorsFee + networkFee) * + liquidationCollateralPeriod * + effectiveBalance) / + 32n; - return bigintMax(total, minimumLiquidationCollateral) / validators; + return ( + (bigintMax(total, minimumLiquidationCollateral) * 32n) / effectiveBalance + ); }; type ComputeFundingCostArgs = Prettify< { fundingDays: number; - effectiveBalance?: bigint; } & LiquidationCollateralCostArgs >; export const computeFundingCost = (args: ComputeFundingCostArgs) => { - const validators = args.effectiveBalance / 32n || 1n; + const effectiveBalance = args.effectiveBalance ?? 32n; const networkCost = computeDailyAmount(args.networkFee, args.fundingDays); const operatorsCost = computeDailyAmount(args.operatorsFee, args.fundingDays); - const liquidationCollateral = - computeLiquidationCollateralCostPerValidator(args); + const liquidationCollateral = computeLiquidationCollateralCostPerValidator({ + ...args, + effectiveBalance, + }); - // Subtotal = base cost × effective balance × validators - const networkCostSubtotal = networkCost * validators; - const operatorsCostSubtotal = operatorsCost * validators; - const liquidationCollateralSubtotal = liquidationCollateral * validators; + // Subtotal = base cost × validators (scaled then unscaled) + const networkCostSubtotal = (networkCost * effectiveBalance) / 32n; + const operatorsCostSubtotal = (operatorsCost * effectiveBalance) / 32n; + const liquidationCollateralSubtotal = + (liquidationCollateral * effectiveBalance) / 32n; const total = networkCostSubtotal + operatorsCostSubtotal + liquidationCollateralSubtotal; - const runway = calculateRunway({ - balance: total, - feesPerBlock: args.networkFee + args.operatorsFee, - validators, - liquidationThresholdBlocks: args.liquidationCollateralPeriod, - minimumLiquidationCollateral: args.minimumLiquidationCollateral, - }); - return { perValidator: { - networkCost, + networkCost: networkCost, operatorsCost, liquidationCollateral, }, @@ -76,7 +77,18 @@ export const computeFundingCost = (args: ComputeFundingCostArgs) => { liquidationCollateral: liquidationCollateralSubtotal, }, total, - runway, - effectiveBalance: args.effectiveBalance, + formatted: { + perValidator: { + networkCost: formatETH(networkCost), + operatorsCost: formatETH(operatorsCost), + liquidationCollateral: formatETH(liquidationCollateral), + }, + subtotal: { + networkCost: formatETH(networkCostSubtotal), + operatorsCost: formatETH(operatorsCostSubtotal), + liquidationCollateral: formatETH(liquidationCollateralSubtotal), + }, + total: formatETH(total), + }, }; }; From f88d7db658045dd698f5f509cc9f095a03377d4f Mon Sep 17 00:00:00 2001 From: sumbat-ssvlabs Date: Wed, 18 Feb 2026 14:24:07 +0200 Subject: [PATCH 12/13] Fex/merge hoodi stage (#1747) * fix: merged * ci: fix env --- .github/workflows/build_deploy.yml | 9 +- scripts/generate-hook-exports.cjs | 10 +- src/app/layouts/dashboard/navbar-dvt.tsx | 5 +- src/app/layouts/dashboard/navbar.tsx | 14 ++ .../create-cluster/additional-funding.tsx | 7 +- src/app/routes/create-cluster/preparation.tsx | 4 +- .../dashboard/clusters/cluster/cluster.tsx | 1 + .../cluster/estimated-operational-runway.tsx | 14 +- .../connect-wallet/connect-wallet-btn.tsx | 30 +--- .../network-switcher-hotfix.tsx | 169 ++++++++++++++++++ .../operator/operator-stat-card.tsx | 29 ++- .../clusters-table/clusters-table-row.tsx | 8 +- src/hooks/cluster/use-cluster-runway.ts | 34 ++-- src/hooks/use-links.ts | 5 +- src/hooks/use-ssv-network-details.ts | 32 ++-- src/lib/utils/cluster.ts | 17 +- src/lib/utils/env-checker.ts | 8 + src/wagmi/config.ts | 20 +-- 18 files changed, 303 insertions(+), 113 deletions(-) create mode 100644 src/components/connect-wallet/network-switcher-hotfix.tsx create mode 100644 src/lib/utils/env-checker.ts diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml index 1a63406bc..7aa666831 100644 --- a/.github/workflows/build_deploy.yml +++ b/.github/workflows/build_deploy.yml @@ -97,7 +97,7 @@ jobs: run: npx semantic-release - name: Configure AWS credentials - if: github.ref == 'refs/heads/pre-stage' || github.ref == 'refs/heads/stage' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/bapps-prod' + if: github.ref == 'refs/heads/pre-stage' || github.ref == 'refs/heads/stage' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/bapps-prod' || github.ref == 'refs/heads/temporary-hoodi-deploy' uses: aws-actions/configure-aws-credentials@50ac8dd1e1b10d09dac7b8727528b91bed831ac0 # v3 with: role-to-assume: ${{ secrets.SSV_WEB_AWS_IAM_ROLE }} @@ -124,10 +124,9 @@ jobs: run: | aws s3 cp ./build s3://${{ secrets.SSV_WEB_STAGE_AWS_S3_BUCKET }} --recursive # - # - name: Run prod webapp build - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/temporary-hoodi-deploy' run: > GAS_PRICE="${{ env.GAS_PRICE }}" GAS_LIMIT="${{ env.GAS_LIMIT }}" @@ -144,7 +143,7 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Upload files to S3 - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/temporary-hoodi-deploy' run: | aws s3 cp ./build s3://${{ secrets.SSV_WEB_PROD_HOODI_AWS_S3_BUCKET }} --recursive - # \ No newline at end of file + # diff --git a/scripts/generate-hook-exports.cjs b/scripts/generate-hook-exports.cjs index 2fff5fa53..d9124c735 100644 --- a/scripts/generate-hook-exports.cjs +++ b/scripts/generate-hook-exports.cjs @@ -197,8 +197,14 @@ function generate() { const outputPath = path.join(HOOKS_DIR, `${name}.ts`); const content = generateExportFile(name, hookNames); - fs.writeFileSync(outputPath, content, "utf-8"); - console.log(` Generated: ${outputPath}`); + const existing = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, "utf-8") : ""; + const normalize = (s) => s.replace(/\s/g, ""); + if (normalize(existing) === normalize(content)) { + console.log(` Unchanged: ${outputPath}`); + } else { + fs.writeFileSync(outputPath, content, "utf-8"); + console.log(` Generated: ${outputPath}`); + } } console.log("Done!"); diff --git a/src/app/layouts/dashboard/navbar-dvt.tsx b/src/app/layouts/dashboard/navbar-dvt.tsx index 46c5a22ae..5e66b289d 100644 --- a/src/app/layouts/dashboard/navbar-dvt.tsx +++ b/src/app/layouts/dashboard/navbar-dvt.tsx @@ -6,7 +6,6 @@ import { HiOutlineExternalLink, HiOutlineGlobeAlt } from "react-icons/hi"; import { TbDots } from "react-icons/tb"; import { ConnectWalletBtn } from "@/components/connect-wallet/connect-wallet-btn"; -import { NetworkSwitchBtn } from "@/components/connect-wallet/network-switch-btn"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -23,6 +22,7 @@ import { ThemeSwitcher } from "@/components/ui/theme-switcher"; import { Link } from "react-router-dom"; import { useLinks } from "@/hooks/use-links"; import { useAccountState } from "@/hooks/account/use-account-state"; +import { NetworkSwitcher } from "@/components/connect-wallet/network-switcher-hotfix"; export type NavbarProps = { // TODO: Add props or remove this type @@ -130,7 +130,8 @@ export const NavbarDVT: FCProps = ({ className, ...props }) => {
- + {/* */} +
diff --git a/src/app/layouts/dashboard/navbar.tsx b/src/app/layouts/dashboard/navbar.tsx index 2a7c57172..8a9e09267 100644 --- a/src/app/layouts/dashboard/navbar.tsx +++ b/src/app/layouts/dashboard/navbar.tsx @@ -180,6 +180,20 @@ export const Navbar: FCProps = ({ className, ...props }) => {
+
diff --git a/src/app/routes/create-cluster/additional-funding.tsx b/src/app/routes/create-cluster/additional-funding.tsx index 43f54d16a..2a7cfc369 100644 --- a/src/app/routes/create-cluster/additional-funding.tsx +++ b/src/app/routes/create-cluster/additional-funding.tsx @@ -42,7 +42,8 @@ export const AdditionalFunding: FC = () => { }); const context = useRegisterValidatorContext(); - const deltaValidators = BigInt(context.shares.length); + + const deltaEffectiveBalance = context.effectiveBalance; const form = useForm({ defaultValues: { depositAmount: context.depositAmount, topUp: true }, @@ -53,7 +54,7 @@ export const AdditionalFunding: FC = () => { const { data: clusterRunway } = useClusterRunway(params.clusterHash!, { deltaBalance: topUp ? depositAmount : 0n, - deltaValidators, + deltaEffectiveBalance, }); const submit = form.handleSubmit((data) => { @@ -80,7 +81,7 @@ export const AdditionalFunding: FC = () => { diff --git a/src/app/routes/create-cluster/preparation.tsx b/src/app/routes/create-cluster/preparation.tsx index aec2b0bce..56ddd7680 100644 --- a/src/app/routes/create-cluster/preparation.tsx +++ b/src/app/routes/create-cluster/preparation.tsx @@ -59,9 +59,7 @@ export const Preparation: FCProps = ({ className, ...props }) => {
- - SSV tokens to cover operational fees - + ETH to cover operational fees
diff --git a/src/app/routes/dashboard/clusters/cluster/cluster.tsx b/src/app/routes/dashboard/clusters/cluster/cluster.tsx index 5a2ca4468..d0cbc2e13 100644 --- a/src/app/routes/dashboard/clusters/cluster/cluster.tsx +++ b/src/app/routes/dashboard/clusters/cluster/cluster.tsx @@ -71,6 +71,7 @@ export const Cluster: FC = () => {
{cluster.data?.operators.map((operatorId) => ( )}
diff --git a/src/components/connect-wallet/connect-wallet-btn.tsx b/src/components/connect-wallet/connect-wallet-btn.tsx index fd19c7c11..39e4fb5af 100644 --- a/src/components/connect-wallet/connect-wallet-btn.tsx +++ b/src/components/connect-wallet/connect-wallet-btn.tsx @@ -4,7 +4,6 @@ import { textVariants } from "@/components/ui/text"; import { useAccount } from "@/hooks/account/use-account"; import { shortenAddress } from "@/lib/utils/strings"; import { ConnectButton } from "@rainbow-me/rainbowkit"; -import { ChevronDown } from "lucide-react"; import type { FC } from "react"; type WalletType = "ledger" | "trezor" | "walletconnect" | "metamask"; @@ -40,15 +39,9 @@ export const ConnectWalletBtn: FC = (props) => { return ( - {({ - chain, - openAccountModal, - openChainModal, - openConnectModal, - mounted, - }) => { + {({ chain, openAccountModal, openConnectModal, mounted }) => { const connected = mounted && account && chain; - if (!mounted) return null; + if (!mounted || chain?.unsupported) return null; if (!connected) { return ( @@ -64,25 +57,6 @@ export const ConnectWalletBtn: FC = (props) => { ); } - if (chain.unsupported) { - return ( - - ); - } - return ( + + + + + No results found + handleSelect("mainnet")} + className="flex items-center gap-2 p-3 py-2.5 border-none" + value="mainnet" + > + + + Ethereum + +
+ {isMainnetEnvironment ? ( + + ) : ( + + )} +
+
+ handleSelect("hoodi")} + className="flex items-center gap-2 p-3 py-2.5" + value="hoodi" + > + + + Hoodi + +
+ {isHoodiEnvironment ? ( + + ) : ( + + )} +
+
+
+
+
+ + ); + }} +
+ ); +}; + +NetworkSwitcher.displayName = "NetworkSwitcher"; diff --git a/src/components/operator/operator-stat-card.tsx b/src/components/operator/operator-stat-card.tsx index e5bf9409f..28c5fffd9 100644 --- a/src/components/operator/operator-stat-card.tsx +++ b/src/components/operator/operator-stat-card.tsx @@ -9,13 +9,19 @@ import { Text } from "@/components/ui/text"; import { FaCircleInfo } from "react-icons/fa6"; import { Tooltip } from "@/components/ui/tooltip"; import { useOperatorState } from "@/hooks/operator/use-operator-state"; -import { formatOperatorETHFee, percentageFormatter } from "@/lib/utils/number"; +import { + formatOperatorETHFee, + formatSSV, + percentageFormatter, +} from "@/lib/utils/number"; import { CircleX } from "lucide-react"; import { OperatorStatusBadge } from "@/components/operator/operator-status-badge"; import { FaEthereum } from "react-icons/fa"; +import { getYearlyFee } from "@/lib/utils/operator"; export type OperatorStatCardProps = { operatorId: OperatorID; + isClusterMigrated?: boolean; }; type OperatorStatCardFC = FC< @@ -26,10 +32,21 @@ type OperatorStatCardFC = FC< export const OperatorStatCard: OperatorStatCardFC = ({ operatorId, className, + isClusterMigrated = true, ...props }) => { const operatorState = useOperatorState(operatorId); + const fee = getYearlyFee( + isClusterMigrated + ? BigInt(operatorState.data?.operator?.eth_fee ?? 0n) + : BigInt(operatorState.data?.operator?.fee ?? 0n), + ); + + const displayFee = isClusterMigrated + ? formatOperatorETHFee(fee) + : formatSSV(fee); + if (operatorState.isLoading) return ( ); - const { operator, fee } = operatorState.data; + const { operator } = operatorState.data; return (
- + {isClusterMigrated ? ( + + ) : ( + SSV + )} - {formatOperatorETHFee(fee.yearly)} {/* ETH */} + {displayFee}
diff --git a/src/components/validator/clusters-table/clusters-table-row.tsx b/src/components/validator/clusters-table/clusters-table-row.tsx index 1678f33bc..e532a164a 100644 --- a/src/components/validator/clusters-table/clusters-table-row.tsx +++ b/src/components/validator/clusters-table/clusters-table-row.tsx @@ -42,6 +42,12 @@ export const ClustersTableRow: FCProps = ({ cluster, className, ...props }) => { const isLoadingRunway = !isLiquidated && runway.isLoading; const resolvedCluster = merge({}, cluster, apiCluster); + + const effectiveBalance = Math.max( + Number(resolvedCluster.effectiveBalance), + resolvedCluster.validatorCount * 32, + ); + const isMigrated = resolvedCluster.migrated; const isSsvCluster = !isMigrated; return ( @@ -95,7 +101,7 @@ export const ClustersTableRow: FCProps = ({ cluster, className, ...props }) => {
{" "} - {formatEffectiveBalance(BigInt(resolvedCluster.effectiveBalance))} + {formatEffectiveBalance(BigInt(effectiveBalance))}
diff --git a/src/hooks/cluster/use-cluster-runway.ts b/src/hooks/cluster/use-cluster-runway.ts index ef0327a2c..2daf10c2b 100644 --- a/src/hooks/cluster/use-cluster-runway.ts +++ b/src/hooks/cluster/use-cluster-runway.ts @@ -1,17 +1,17 @@ import { useCluster } from "@/hooks/cluster/use-cluster"; import { useClusterBalance } from "@/hooks/cluster/use-cluster-balance"; import { useClusterPageParams } from "@/hooks/cluster/use-cluster-page-params"; -import { useRegisterValidatorContext } from "@/guard/register-validator-guard.tsx"; import { bigintMax } from "@/lib/utils/bigint"; import { calculateRunway } from "@/lib/utils/cluster"; import { useNetworkFee, useNetworkFeeSSV } from "@/hooks/use-ssv-network-fee"; import { sumOperatorsFee } from "@/lib/utils/operator"; import { useOperators } from "@/hooks/operator/use-operators"; -const getDeltaValidators = (options: Options) => { - if ("deltaValidators" in options) return options.deltaValidators ?? 0n; +const getDeltaEffectiveBalance = (options: Options) => { + if ("deltaValidators" in options) + return (options.deltaValidators ?? 0n) * 32n; if ("deltaEffectiveBalance" in options) - return BigInt(options.deltaEffectiveBalance ?? 0) / 32n; + return BigInt(options.deltaEffectiveBalance ?? 0); return 0n; }; @@ -30,12 +30,10 @@ export const useClusterRunway = ( watch: false, }, ) => { - const { state } = useRegisterValidatorContext; - const params = useClusterPageParams(); const clusterHash = hash ?? params.clusterHash; - const deltaValidators = getDeltaValidators(opts); + const deltaEffectiveBalance = getDeltaEffectiveBalance(opts); const cluster = useCluster(clusterHash, { watch: opts.watch }); const balance = useClusterBalance(clusterHash!, { watch: opts.watch }); @@ -60,16 +58,12 @@ export const useClusterRunway = ( const feesPerBlock = operatorFees + networkFee; - const clusterEffectiveBalance = BigInt(cluster.data?.effectiveBalance ?? 0); - const minClusterEffectiveBalance = - BigInt(cluster.data?.validatorCount ?? 1) * 32n; - - const effectiveBalance = bigintMax( - clusterEffectiveBalance, - minClusterEffectiveBalance, - ); - - const validators = (effectiveBalance + state.effectiveBalance) / 32n; + const effectiveBalance = isETH + ? bigintMax( + BigInt(cluster.data?.effectiveBalance ?? 0), + BigInt(cluster.data?.validatorCount ?? 1) * 32n, + ) + : BigInt(cluster.data?.validatorCount ?? 1) * 32n; const isLoading = cluster.isLoading || @@ -79,10 +73,10 @@ export const useClusterRunway = ( ssvNetworkFee.isLoading; const runway = calculateRunway({ - balance: balance.data.eth || balance.data.ssv || 0n, + balance: (isETH ? balance.data.eth : balance.data.ssv) || 0n, feesPerBlock, - validators, - deltaValidators: deltaValidators, + effectiveBalance, + deltaEffectiveBalance, deltaBalance: opts.deltaBalance ?? 0n, liquidationThresholdBlocks, minimumLiquidationCollateral, diff --git a/src/hooks/use-links.ts b/src/hooks/use-links.ts index 914e8788b..8a5a23b53 100644 --- a/src/hooks/use-links.ts +++ b/src/hooks/use-links.ts @@ -1,19 +1,20 @@ import { useMemo } from "react"; import { useAccount } from "@/hooks/account/use-account"; -// const isProduction = location.hostname === "app.ssv.network"; // TODO: determine production through build.yaml environment variable +const isProduction = location.hostname === "app.ssv.network"; // TODO: determine production through build.yaml environment variable export const useLinks = () => { const { chain } = useAccount(); return useMemo(() => { const chainPrefix = chain?.testnet ? `${chain.name.toLowerCase()}.` : ""; + const envPrefix = isProduction ? "" : `stage.`; return { beaconcha: `https://${chainPrefix}beaconcha.in`, launchpad: `https://${chainPrefix}launchpad.ethereum.org`, etherscan: `https://${chainPrefix}etherscan.io`, ssv: { explorer: `https://explorer.${chainPrefix}ssv.network/`, - stake: `https://stake.${chainPrefix}ssv.network`, + stake: `https://stake.${envPrefix}ssv.network`, docs: `https://docs.ssv.network`, forum: `https://forum.ssv.network/`, governanceForum: `https://forum.ssv.network/`, diff --git a/src/hooks/use-ssv-network-details.ts b/src/hooks/use-ssv-network-details.ts index 3cef3867a..ce039e192 100644 --- a/src/hooks/use-ssv-network-details.ts +++ b/src/hooks/use-ssv-network-details.ts @@ -1,12 +1,13 @@ +import { isMainnetEnvironment } from "@/lib/utils/env-checker"; import { isAddress } from "viem"; -import { useChainId } from "wagmi"; import { z } from "zod"; -import { config, hoodi } from "@/wagmi/config"; -import { getAccount, getChainId } from "@wagmi/core"; -import { useAccount } from "@/hooks/account/use-account"; - -const networks = import.meta.env.VITE_SSV_NETWORKS; +// Get the network that matches the current environment for app.ssv.network or app.hoodi.ssv.network +// NETWORKS will be an array with only one network -> hoodi or mainnet +export const NETWORKS = import.meta.env.VITE_SSV_NETWORKS.filter( + (network) => + network.apiNetwork === (isMainnetEnvironment ? "mainnet" : "hoodi"), +); const networkSchema = z .array( @@ -25,13 +26,13 @@ const networkSchema = z ) .min(1); -if (!networks) { +if (!NETWORKS) { throw new Error( "VITE_SSV_NETWORKS is not defined in the environment variables", ); } -const parsed = networkSchema.safeParse(networks); +const parsed = networkSchema.safeParse(NETWORKS); if (!parsed.success) { throw new Error( @@ -44,19 +45,10 @@ Invalid network schema in VITE_SSV_NETWORKS environment variable: ); } -export const getSSVNetworkDetails = (chainId?: number) => { - const _chainId = chainId ?? getChainId(config); - const { isConnected } = getAccount(config); - return networks.find( - (network) => network.networkId === (isConnected ? _chainId : hoodi.id), - )!; +export const getSSVNetworkDetails = () => { + return NETWORKS[0]; }; export const useSSVNetworkDetails = () => { - const { isConnected } = useAccount(); - const chainId = useChainId(); - - return import.meta.env.VITE_SSV_NETWORKS.find( - (network) => network.networkId === (isConnected ? chainId : hoodi.id), - )!; + return NETWORKS[0]; }; diff --git a/src/lib/utils/cluster.ts b/src/lib/utils/cluster.ts index 46f93d5fa..cc41cdf5b 100644 --- a/src/lib/utils/cluster.ts +++ b/src/lib/utils/cluster.ts @@ -100,24 +100,29 @@ export const filterOutRemovedValidators = ( type CalculateRunwayParams = { balance: bigint; feesPerBlock: bigint; - validators: bigint; + effectiveBalance: bigint; deltaBalance?: bigint; - deltaValidators?: bigint; + deltaEffectiveBalance?: bigint; liquidationThresholdBlocks: bigint; minimumLiquidationCollateral: bigint; }; +const EB_PER_VALIDATOR = 32n; + export const calculateRunway = ({ balance, feesPerBlock, - validators, + effectiveBalance, deltaBalance = 0n, - deltaValidators = 0n, + deltaEffectiveBalance = 0n, liquidationThresholdBlocks, minimumLiquidationCollateral, }: CalculateRunwayParams) => { - const burnRateSnapshot = feesPerBlock * (validators || 1n); - const burnRateWithDelta = feesPerBlock * (validators + deltaValidators); + const snapshotEB = effectiveBalance || EB_PER_VALIDATOR; + const totalEB = effectiveBalance + deltaEffectiveBalance; + const burnRateSnapshot = (feesPerBlock * snapshotEB) / EB_PER_VALIDATOR; + const burnRateWithDelta = + (feesPerBlock * (totalEB || EB_PER_VALIDATOR)) / EB_PER_VALIDATOR; const collateralSnapshot = bigintMax( burnRateSnapshot * liquidationThresholdBlocks, diff --git a/src/lib/utils/env-checker.ts b/src/lib/utils/env-checker.ts new file mode 100644 index 000000000..7507ce921 --- /dev/null +++ b/src/lib/utils/env-checker.ts @@ -0,0 +1,8 @@ +const isDev = import.meta.env.DEV; +export const MAINNET_HOST = isDev ? "app.localhost:3000" : "app.ssv.network"; +export const HOODI_HOST = isDev + ? "app.hoodi.localhost:3000" + : "app.hoodi.ssv.network"; + +export const isMainnetEnvironment = document.location.host === MAINNET_HOST; +export const isHoodiEnvironment = document.location.host === HOODI_HOST; diff --git a/src/wagmi/config.ts b/src/wagmi/config.ts index b66fe2385..d28c7dbdb 100644 --- a/src/wagmi/config.ts +++ b/src/wagmi/config.ts @@ -1,7 +1,8 @@ -import { connectorsForWallets, type Chain } from "@rainbow-me/rainbowkit"; +import { isMainnetEnvironment } from "@/lib/utils/env-checker"; +import { type Chain, connectorsForWallets } from "@rainbow-me/rainbowkit"; import { - walletConnectWallet, coinbaseWallet, + walletConnectWallet, } from "@rainbow-me/rainbowkit/wallets"; import { createPublicClient, http } from "viem"; @@ -17,7 +18,6 @@ const mainnet: Chain = { export const hoodi = { id: 560048, name: "Hoodi", - network: "hoodi", nativeCurrency: { name: "Ethereum", symbol: "ETH", @@ -44,15 +44,13 @@ export const hoodi = { iconBackground: "none", iconUrl: "/images/networks/light.svg", testnet: true, -}; - -const chains = import.meta.env.VITE_SSV_NETWORKS.map((network) => - [mainnet, hoodi].find((chain) => chain.id === network.networkId), -).filter(Boolean) as [Chain, ...Chain[]]; -export const isChainSupported = (chainId: number) => { - return chains.some((chain) => chain.id === chainId); -}; +} satisfies Chain; +const chains = (isMainnetEnvironment ? [mainnet] : [hoodi]) satisfies [ + Chain, + ...Chain[], +]; +export type Chains = typeof chains; const connectors = connectorsForWallets( [ { From 2003af55393d4706ea94c45abca68f6b2e027401 Mon Sep 17 00:00:00 2001 From: sumbat-ssvlabs Date: Wed, 18 Feb 2026 14:27:18 +0200 Subject: [PATCH 13/13] fix: operator fee 11294034432 (#1745) --- .../routes/join/operator/set-operator-fee.tsx | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/app/routes/join/operator/set-operator-fee.tsx b/src/app/routes/join/operator/set-operator-fee.tsx index 5ea987d9a..51fdff06d 100644 --- a/src/app/routes/join/operator/set-operator-fee.tsx +++ b/src/app/routes/join/operator/set-operator-fee.tsx @@ -14,7 +14,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Text } from "@/components/ui/text"; import { BigNumberInput } from "@/components/ui/number-input"; import { formatUnits, parseEther } from "viem"; -import { globals } from "@/config"; import { Button } from "@/components/ui/button"; import { useNavigate } from "react-router-dom"; import { NavigateBackBtn } from "@/components/ui/navigate-back-btn"; @@ -22,17 +21,35 @@ import { useFocus } from "@/hooks/use-focus"; import { useRegisterOperatorContext } from "@/guard/register-operator-guards"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { useRates } from "@/hooks/use-rates"; -import { currencyFormatter } from "@/lib/utils/number"; +import { currencyFormatter, ms } from "@/lib/utils/number"; import { Tooltip } from "@/components/ui/tooltip"; import { FaCircleInfo } from "react-icons/fa6"; - -const minimumFee = - globals.BLOCKS_PER_YEAR * globals.MINIMUM_OPERATOR_FEE_PER_BLOCK; +import { + useGetMaximumOperatorFee, + useGetMinimumOperatorEthFee, +} from "@/lib/contract-interactions/hooks/getter"; +import { getYearlyFee } from "@/lib/utils/operator"; export const SetOperatorFee: FC> = () => { - const navigate = useNavigate(); const { isPrivate } = useRegisterOperatorContext(); + + const navigate = useNavigate(); const rates = useRates(); + + const { data: minFee = 0n } = useGetMinimumOperatorEthFee({ + staleTime: ms(1, "weeks"), + }); + const { data: maxFee = 13900000000n /* value from the contract */ } = + useGetMaximumOperatorFee({ + staleTime: ms(1, "weeks"), + }); + + const minYearlyFee = getYearlyFee(minFee); + const minYearlyFeeFormatted = getYearlyFee(minFee, { format: true }); + + const maxYearlyFee = getYearlyFee(maxFee); + const maxYearlyFeeFormatted = getYearlyFee(maxFee, { format: true }); + const ethRate = rates.data?.eth ?? 0; const form = useForm({ @@ -43,10 +60,10 @@ export const SetOperatorFee: FC> = () => { resolver: zodResolver( z.object({ yearlyFee: z.bigint().superRefine((value, ctx) => { - if (value > parseEther("200")) { + if (value > maxYearlyFee) { return ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Fee must be lower than 200 ETH", + message: `Fee must be lower than ${maxYearlyFeeFormatted}`, }); } if (isPrivate && value === parseEther("0")) return; @@ -57,10 +74,10 @@ export const SetOperatorFee: FC> = () => { message: `Fee cannot be set to 0 while operator status is set to public. To set the fee to 0, switch the operator status to private in the previous step.`, }); - if (value >= parseEther("0") && value < minimumFee) + if (value >= parseEther("0") && value < minYearlyFee) return ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Fee must be greater than ${formatUnits(minimumFee, 18)} ETH`, + message: `Fee must be greater than ${minYearlyFeeFormatted}`, }); }), }), @@ -137,7 +154,7 @@ export const SetOperatorFee: FC> = () => { id="register-operator-fee" value={field.value} onChange={field.onChange} - max={parseEther("200")} + max={maxYearlyFee} rightSlot={