Skip to content
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
06e75ce
fix: hide SSV balance display for migrated operators with zero yearly…
axelrod-blox Feb 16, 2026
c22c95a
fix: align migration flow liquidation collateral calculation with sta…
axelrod-blox Feb 16, 2026
5cfeaf4
fix: remove incorrect /32 division from migration funding fee display
liorrutenberg Feb 16, 2026
c65de10
Merge pull request #1735 from ssvlabs/fix/migration-liquidation-colla…
IlyaVi Feb 16, 2026
8e27075
fix: change of labels for liquidation during migration
Chris-ssvlabs Feb 17, 2026
b2c4c05
Merge pull request #1737 from ssvlabs/fix/liquidation-tooltips
Chris-ssvlabs Feb 17, 2026
24962fb
fix: change of statuses used to determine deposited and not deposited…
Chris-ssvlabs Feb 17, 2026
337f687
Merge pull request #1738 from ssvlabs/fix/validator-breakdown-migration
Chris-ssvlabs Feb 17, 2026
5ebd1aa
fix: change the sizes of private operator icon to be responsive
Chris-ssvlabs Feb 17, 2026
c93f5f0
Merge pull request #1739 from ssvlabs/fix/private-op-lock-icon
Chris-ssvlabs Feb 17, 2026
24e5864
ci: changed contract addresses
sumbat-ssvlabs Feb 17, 2026
d1b22d9
Merge pull request #1740 from ssvlabs/feature/new-contracts
IlyaVi Feb 17, 2026
6d8c120
fix: use effective balance in ETH for migration liquidation collateral
Chris-ssvlabs Feb 17, 2026
b5d26af
fix: prefix (#1742)
sumbat-ssvlabs Feb 17, 2026
9723c79
Merge pull request #1741 from ssvlabs/fix/liq-col
Chris-ssvlabs Feb 17, 2026
6227525
fix: align liquidation collateral math with shared funding logic
Chris-ssvlabs Feb 18, 2026
44f6afc
Merge pull request #1743 from ssvlabs/fix/liq-col
Chris-ssvlabs Feb 18, 2026
9a29ba4
fix: funding calculation (#1744)
sumbat-ssvlabs Feb 18, 2026
f88d7db
Fex/merge hoodi stage (#1747)
sumbat-ssvlabs Feb 18, 2026
2003af5
fix: operator fee 11294034432 (#1745)
sumbat-ssvlabs Feb 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/build_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

pnpm run test:run
pnpm run lint-staged
8 changes: 4 additions & 4 deletions src/app/routes/create-cluster/initial-funding.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions src/app/routes/create-cluster/reactivate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions src/app/routes/dashboard/operators/operator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export const Operator: FC<ComponentPropsWithoutRef<"div">> = ({ ...props }) => {
const params = useOperatorPageParams();
const operatorId = BigInt(params.operatorId!);
const operator = useOperator(operatorId!);

const { feeEth, yearlyFeeEth, yearlyFeeSSV, balanceEth, balanceSSV } =
useOperatorEarningsAndFees(operatorId);

Expand Down Expand Up @@ -104,7 +103,7 @@ export const Operator: FC<ComponentPropsWithoutRef<"div">> = ({ ...props }) => {
</Text>
<div className="flex flex-col gap-4">
<BalanceDisplay amount={balanceEth} token="ETH" />
<BalanceDisplay amount={balanceSSV} token="SSV" />
{yearlyFeeSSV !== 0n && !operator.data.migrated && <BalanceDisplay amount={balanceSSV} token="SSV" />}
</div>
<Tooltip
asChild
Expand Down
4 changes: 2 additions & 2 deletions src/components/cluster/cluster-funding-summary.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -23,7 +23,7 @@ export const ClusterFundingSummary: ClusterFundingSummaryFC = ({
...props
}) => {
// const isBulk = validatorsAmount > 1;
const cost = useFundingCost({
const cost = useFundingCostETH({
operators,
fundingDays,
effectiveBalance,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export const MigrationEffectiveBalanceForm: FC<EffectiveBalanceFormProps> = ({
const confirmId = useId();

const balanceValue = form.watch("totalEffectiveBalance");
console.log("balanceValue:", balanceValue);

const balanceError = form.formState.errors.totalEffectiveBalance?.type;
const isLowBalance = balanceError === "too_small";
Expand Down
5 changes: 3 additions & 2 deletions src/components/operator/operator-avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@ export const OperatorAvatar = forwardRef<HTMLDivElement, FCProps>(
{...props}
>
{isPrivate && (
<div className="absolute flex items-center justify-center left-0 top-0 -m-2 bg-gray-50 text-gray-800 rounded-full size-7 border">
<MdOutlineLock className="size-4" />
<div className="absolute flex items-center justify-center left-0 top-0 bg-gray-50 text-gray-800 rounded-full border size-[60%] -translate-x-[20%] -translate-y-[20%]">
<MdOutlineLock className="size-[60%]" />
</div>
)}

{src ? (
<img
alt="Operator Avatar"
className={cn(variants({ size, variant }), "w-full h-full")}
src={src || "/images/operator_default_background/light.svg"}
/>
Expand Down
9 changes: 2 additions & 7 deletions src/components/wizard/switch-wizard-step-three.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,7 @@ export const SwitchWizardStepThree = ({
{formatEthValue(fundingSummary?.networkSubtotal)}
</Text>

<div className="flex gap-2 items-center">
<Text variant="body-2-medium">Liquidation collateral</Text>
<Tooltip content="Liquidation collateral information">
<FaCircleInfo className="size-3.5 text-gray-500" />
</Tooltip>
</div>
<Text variant="body-2-medium">Liquidation collateral</Text>
<Text variant="body-2-medium" className="text-right">
{formatEthValue(fundingSummary?.liquidationPerEth)}
</Text>
Expand Down Expand Up @@ -232,7 +227,7 @@ export const SwitchWizardStepThree = ({
<div className="flex items-start justify-between">
<div className="flex gap-2 items-center">
<Text variant="body-2-medium">Withdraw SSV</Text>
<Tooltip content="SSV withdrawal information">
<Tooltip content="The SSV amount withdrawn from this cluster to your wallet during this transaction.">
<FaCircleInfo className="size-3.5 text-gray-500" />
</Tooltip>
</div>
Expand Down
104 changes: 48 additions & 56 deletions src/components/wizard/switch-wizard-step-two.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +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 { 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";
Expand Down Expand Up @@ -65,13 +61,11 @@ export const SwitchWizardStepTwo = ({
navigateRoutePath,
navigateRouteOptions,
operators = [],
validatorsAmount = 1,
effectiveBalance,
currentRunwayDays = 0,
ssvBalance,
}: SwitchWizardStepTwoProps) => {
const rates = useRates();
const networkFees = useNetworkFee();
const hasSsvBalance = (ssvBalance ?? 0n) > 0n;

const form = useForm<z.infer<typeof schema>>({
Expand All @@ -95,63 +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 weiPerEth = 10n ** 18n;
const perValidatorBalance = 32n * weiPerEth;

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 operatorsCost = computeDailyAmount(operatorsFee, days);
const networkCost = computeDailyAmount(networkFee, days);
const liquidationCost = computeLiquidationCollateralCostPerValidator({
networkFee,
operatorsFee,
liquidationCollateralPeriod: liquidationThreshold,
minimumLiquidationCollateral,
effectiveBalance: BigInt(validatorsAmount || 1) * 32n,
});
const halfYearCostsQuery = useFundingCostETH({
fundingDays: periods["half-year"],
operators,
effectiveBalance: effectiveBalanceEth,
});

const operatorsPerEth = operatorsCost / 32n;
const networkPerEth = networkCost / 32n;
const liquidationPerEth = liquidationCost / 32n;
const yearCostsQuery = useFundingCostETH({
fundingDays: periods.year,
operators,
effectiveBalance: effectiveBalanceEth,
});

const customCostsQuery = useFundingCostETH({
fundingDays: values.custom,
operators,
effectiveBalance: effectiveBalanceEth,
});

const operatorsSubtotal =
(operatorsCost * effectiveBalanceWei) / perValidatorBalance;
const networkSubtotal =
(networkCost * effectiveBalanceWei) / perValidatorBalance;
const liquidationSubtotal =
(liquidationCost * effectiveBalanceWei) / perValidatorBalance;
const totalDeposit =
operatorsSubtotal + networkSubtotal + liquidationSubtotal;
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,
networkPerEth,
liquidationPerEth,
operatorsSubtotal,
networkSubtotal,
liquidationSubtotal,
totalDeposit,
};
};
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` : "-";
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/use-compute-funding-cost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ export const useComputeFundingCost = () => {
export type UseFundingCostArgs = {
operators: Pick<Operator, "eth_fee" | "fee">[];
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,
Expand All @@ -70,7 +71,7 @@ export const useFundingCost = ({
]),
queryFn: async () =>
computeFundingCost({
operatorsFee: sumOperatorsFee(operators),
operatorsFee: sumOperatorsFee(operators, "eth"),
fundingDays,
networkFee: ssvNetworkFee.data!,
liquidationCollateralPeriod: liquidationThresholdPeriod.data!,
Expand Down
9 changes: 4 additions & 5 deletions src/hooks/use-links.ts
Original file line number Diff line number Diff line change
@@ -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/`,
Expand Down
Loading