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( [ {