From 943add73e0e03ce237a889405bafe3e210c5dd80 Mon Sep 17 00:00:00 2001
From: valefar-on-discord
<124839138+valefar-on-discord@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:28:11 -0600
Subject: [PATCH 1/6] Show footer when not connected
---
src/App.tsx | 6 ++----
tsconfig.json | 2 +-
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/App.tsx b/src/App.tsx
index 1fe02cc..ccb2394 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -65,9 +65,6 @@ function AppContent() {
return null;
}
- const isDashboardNoWallet =
- location.pathname === "/dashboard" && !isConnected;
-
return (
@@ -80,7 +77,8 @@ function AppContent() {
- {!isDashboardNoWallet && }
+
+
diff --git a/tsconfig.json b/tsconfig.json
index 4d56b52..eff23d6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -17,7 +17,7 @@
"noUnusedLocals": true,
"jsx": "react-jsx",
"baseUrl": "./",
- "types": ["node", "vite/client", "vite-plugin-pwa/client"],
+ "types": ["node", "vite/client", "vite-plugin-pwa/client", "./src/vite-env.d.ts"],
"paths": {
"@/*": ["src/*"]
}
From afb3a419b21c4b099509039c0302adef0b3d69be Mon Sep 17 00:00:00 2001
From: valefar-on-discord
<124839138+valefar-on-discord@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:34:50 -0600
Subject: [PATCH 2/6] Adding ability to connect to offline wallet via address
---
.../ConnectWallet/ConnectWallet.tsx | 93 ++++++++++
src/components/ConnectWallet/index.ts | 1 +
src/config/OfflineConnector.tsx | 172 ++++++++++++++++++
src/config/appkit.tsx | 6 +
.../OfflineConnect/OfflineConnectModal.tsx | 123 +++++++++++++
src/modals/OfflineConnect/index.ts | 1 +
src/pages/Dashboard/Dashboard.tsx | 53 +-----
src/types/transaction.ts | 7 +
src/utils/offline/index.ts | 30 +++
9 files changed, 435 insertions(+), 51 deletions(-)
create mode 100644 src/components/ConnectWallet/ConnectWallet.tsx
create mode 100644 src/components/ConnectWallet/index.ts
create mode 100644 src/config/OfflineConnector.tsx
create mode 100644 src/modals/OfflineConnect/OfflineConnectModal.tsx
create mode 100644 src/modals/OfflineConnect/index.ts
create mode 100644 src/utils/offline/index.ts
diff --git a/src/components/ConnectWallet/ConnectWallet.tsx b/src/components/ConnectWallet/ConnectWallet.tsx
new file mode 100644
index 0000000..322cdc5
--- /dev/null
+++ b/src/components/ConnectWallet/ConnectWallet.tsx
@@ -0,0 +1,93 @@
+import { AccountBalanceWallet as WalletIcon } from "@mui/icons-material";
+import { Box, Typography } from "@mui/material";
+import { useState } from "react";
+import { useChainId, useConnect } from "wagmi";
+
+import { OfflineConnector } from "@/config/OfflineConnector";
+import { OfflineConnectModal } from "@/modals/OfflineConnect";
+
+export const ConnectWallet = () => {
+ const { connect } = useConnect();
+ const chainId = useChainId();
+
+ const [showOfflineConnect, setShowOfflineConnect] = useState(false);
+
+ const onOfflineConnectModalClose = (address: `0x${string}` | undefined) => {
+ if (address) {
+ connect({
+ connector: OfflineConnector({
+ address,
+ defaultChainId: chainId,
+ }),
+ });
+ }
+
+ setShowOfflineConnect(false);
+ };
+
+ return (
+
+
+
+
+
+
+ Connect Your Wallet
+
+
+ Connect your wallet to access your validator dashboard, manage your
+ stake, and monitor your Ethereum validators.
+
+
+
+
+
+ setShowOfflineConnect(true)}
+ >
+ or specify an address for offline wallets
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/ConnectWallet/index.ts b/src/components/ConnectWallet/index.ts
new file mode 100644
index 0000000..e79cc62
--- /dev/null
+++ b/src/components/ConnectWallet/index.ts
@@ -0,0 +1 @@
+export * from "./ConnectWallet";
diff --git a/src/config/OfflineConnector.tsx b/src/config/OfflineConnector.tsx
new file mode 100644
index 0000000..83cc9a7
--- /dev/null
+++ b/src/config/OfflineConnector.tsx
@@ -0,0 +1,172 @@
+import { createConnector } from "@wagmi/core";
+import {
+ Hex,
+ TransactionSerializable,
+ serializeTransaction,
+ keccak256,
+ type Chain,
+ PublicClient,
+} from "viem";
+import { createPublicClient, http } from "viem"; // For provider
+
+import { OfflineTransactionDetails } from "@/types";
+import { resolveUnsignedTx } from "@/utils/offline";
+
+type OfflineConnectorOptions = {
+ address?: `0x${string}`;
+ defaultChainId: number;
+};
+
+export function OfflineConnector(options: OfflineConnectorOptions) {
+ let address: `0x${string}` | undefined = options.address;
+ let chainId: number = options.defaultChainId;
+
+ const getProvider = (publicClient: PublicClient) => {
+ const provider = {
+ isWalletConnect: false,
+ request: async ({
+ method,
+ params,
+ }: {
+ method: string;
+ params?: unknown[];
+ }): Promise => {
+ switch (method) {
+ case "eth_requestAccounts": {
+ if (!address) throw new Error("Not connected");
+ return [address] as T;
+ }
+ case "eth_accounts": {
+ return address ? ([address] as T) : ([] as T);
+ }
+ case "eth_chainId": {
+ return `0x${chainId.toString(16)}` as T;
+ }
+ case "eth_sendTransaction": {
+ if (!address) throw new Error("Not connected");
+ if (!params?.[0]) throw new Error("No transaction provided");
+
+ let preparedTx = await publicClient.prepareTransactionRequest({
+ account: address,
+ ...(params[0] as any),
+ chainId,
+ });
+
+ const serialized = serializeTransaction(preparedTx);
+ const hash = keccak256(serialized as Hex);
+
+ const offlineData: OfflineTransactionDetails = {
+ hash,
+ transaction: preparedTx,
+ unsignedSerialized: serialized,
+ };
+
+ resolveUnsignedTx(offlineData);
+
+ return "0x01" as T;
+ }
+ case "eth_signTransaction": {
+ if (!address) throw new Error("Not connected");
+ if (!params?.[0]) throw new Error("No transaction provided");
+
+ const tx = {
+ ...params[0],
+ from: address,
+ chainId,
+ } as TransactionSerializable & { chainId: number };
+ const serialized = serializeTransaction(tx);
+ const hash = keccak256(serialized as Hex);
+
+ const offlineData: OfflineTransactionDetails = {
+ hash,
+ transaction: tx,
+ unsignedSerialized: serialized,
+ };
+
+ resolveUnsignedTx(offlineData);
+
+ return "0x01" as T;
+ }
+ default: {
+ throw new Error(`${method} not implemented in manual connector`);
+ }
+ }
+ },
+ } as any;
+
+ return provider;
+ };
+
+ return createConnector((config) => {
+ return {
+ id: "offline",
+ name: "Offline Connector",
+ type: "offline",
+ async setup() {},
+ async connect({
+ chainId: requestedChainId,
+ }: {
+ chainId?: number;
+ } = {}) {
+ const connectedChainId = requestedChainId || chainId;
+
+ if (!address) {
+ throw new Error("Address not specified for OfflineConnector");
+ }
+
+ const accounts = [address] as never;
+
+ config.emitter.emit("connect", { accounts, chainId: connectedChainId });
+
+ return { accounts, chainId: connectedChainId };
+ },
+ async disconnect() {
+ address = undefined;
+ config.emitter.emit("disconnect");
+ },
+ async getAccounts() {
+ if (!address) {
+ throw new Error("Not connected");
+ }
+
+ return [address];
+ },
+ async getChainId() {
+ return chainId;
+ },
+ async isAuthorized() {
+ return true;
+ },
+ async switchChain({ chainId: newChainId }) {
+ chainId = newChainId;
+ config.emitter.emit("change", { chainId });
+ return (
+ config.chains.find((chain) => chain.id === chainId) ??
+ ({
+ id: chainId,
+ name: "Unknown Chain",
+ nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
+ rpcUrls: { default: { http: [] }, public: { http: [] } },
+ } as Chain)
+ );
+ },
+ onAccountsChanged() {},
+ onChainChanged() {},
+ async onDisconnect() {},
+ async getProvider({
+ chainId: requestedChainId,
+ }: { chainId?: number } = {}) {
+ const chain = config.chains.find(
+ (c) => c.id === requestedChainId || chainId,
+ );
+ if (!chain) {
+ throw new Error(
+ "Chain not found when attempting to get the provider",
+ );
+ }
+ const publicClient = createPublicClient({ chain, transport: http() });
+ return getProvider(publicClient);
+ },
+ };
+ });
+}
diff --git a/src/config/appkit.tsx b/src/config/appkit.tsx
index cdc25aa..0844bad 100644
--- a/src/config/appkit.tsx
+++ b/src/config/appkit.tsx
@@ -21,6 +21,12 @@ if (
definedNetworks.push(mainnet);
}
+if (definedNetworks.length === 0) {
+ throw new Error(
+ "No networks defined. Please verify your environment variables set at least one network",
+ );
+}
+
export const networks: AppKitNetwork[] = definedNetworks;
export const wagmiAdapter = new WagmiAdapter({
diff --git a/src/modals/OfflineConnect/OfflineConnectModal.tsx b/src/modals/OfflineConnect/OfflineConnectModal.tsx
new file mode 100644
index 0000000..0691b60
--- /dev/null
+++ b/src/modals/OfflineConnect/OfflineConnectModal.tsx
@@ -0,0 +1,123 @@
+import { Close } from "@mui/icons-material";
+import { Box, Button, IconButton, TextField, Typography } from "@mui/material";
+import { useEffect, useMemo, useState } from "react";
+import { isAddress } from "viem";
+
+import { BaseDialog } from "../BaseDialog";
+
+interface OfflineConnectModalProps {
+ open: boolean;
+ onClose: (address: `0x${string}` | undefined) => void;
+}
+
+export const OfflineConnectModal = ({
+ open,
+ onClose,
+}: OfflineConnectModalProps) => {
+ const [address, setAddress] = useState("");
+ useEffect(() => {
+ if (!open) {
+ setAddress("");
+ }
+ }, [open]);
+
+ const validAddress = useMemo(() => {
+ return isAddress(address);
+ }, [address]);
+
+ return (
+ onClose(undefined)}>
+
+
+
+ Connect to Offline Wallet
+
+
+ onClose(undefined)}
+ >
+
+
+
+
+
+
+
+
+ Specify an address to an offline wallet. When attempting a
+ transaction, the unsigned transaction details will be provided to
+ you.
+
+
+ It will be your responsibility to sign the transaction in a secure
+ manner and then submit.
+
+
+
+ setAddress(e.target.value)}
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ color: "#ffffff",
+ backgroundColor: "#333743",
+ "& fieldset": {
+ borderColor: "#404040",
+ },
+ "&:hover fieldset": {
+ borderColor: "#606060",
+ },
+ "&.Mui-focused fieldset": {
+ borderColor: "#627EEA",
+ },
+ },
+ "& .MuiInputBase-input": {
+ color: "#ffffff",
+ padding: "8px 12px",
+ },
+ "& .MuiInputBase-input::placeholder": {
+ color: "#b3b3b3",
+ opacity: 1,
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/modals/OfflineConnect/index.ts b/src/modals/OfflineConnect/index.ts
new file mode 100644
index 0000000..90c2b39
--- /dev/null
+++ b/src/modals/OfflineConnect/index.ts
@@ -0,0 +1 @@
+export * from "./OfflineConnectModal";
diff --git a/src/pages/Dashboard/Dashboard.tsx b/src/pages/Dashboard/Dashboard.tsx
index cc1c863..69df7fb 100644
--- a/src/pages/Dashboard/Dashboard.tsx
+++ b/src/pages/Dashboard/Dashboard.tsx
@@ -1,8 +1,8 @@
-import { AccountBalanceWallet as WalletIcon } from "@mui/icons-material";
import { Box, Typography } from "@mui/material";
import React from "react";
import { useAccount } from "wagmi";
+import { ConnectWallet } from "@/components/ConnectWallet";
import { DashboardValidatorsTable } from "@/components/DashboardValidatorsTable";
import { Meta } from "@/components/Meta";
import { MetricCard } from "@/components/MetricCard";
@@ -17,56 +17,7 @@ const Dashboard: React.FC = () => {
<>
{!isConnected ? (
-
-
-
-
-
-
- Connect Your Wallet
-
-
- Connect your wallet to access your validator dashboard, manage
- your stake, and monitor your Ethereum validators.
-
-
-
-
-
-
+
) : (
<>
diff --git a/src/types/transaction.ts b/src/types/transaction.ts
index 3f3458d..78059ed 100644
--- a/src/types/transaction.ts
+++ b/src/types/transaction.ts
@@ -2,6 +2,7 @@ import {
WaitForTransactionReceiptErrorType,
SendTransactionErrorType,
} from "@wagmi/core";
+import { TransactionSerializable } from "viem";
import { Validator } from "./validator";
@@ -21,3 +22,9 @@ export interface Transaction {
signingError?: SendTransactionErrorType | null;
confirmationError?: WaitForTransactionReceiptErrorType | null;
}
+
+export interface OfflineTransactionDetails {
+ hash: `0x${string}`;
+ transaction: TransactionSerializable;
+ unsignedSerialized: `0x${string}`;
+}
diff --git a/src/utils/offline/index.ts b/src/utils/offline/index.ts
new file mode 100644
index 0000000..be4b2c6
--- /dev/null
+++ b/src/utils/offline/index.ts
@@ -0,0 +1,30 @@
+import type { OfflineTransactionDetails } from "@/types";
+
+let currentUnsignedTxPromise: Promise | null = null;
+let resolveCurrentUnsignedTxPromise:
+ | ((data: OfflineTransactionDetails) => void)
+ | null = null;
+
+export const getUnsignedTxPromise = (): Promise => {
+ if (!currentUnsignedTxPromise) {
+ currentUnsignedTxPromise = new Promise(
+ (resolve) => {
+ resolveCurrentUnsignedTxPromise = resolve;
+ },
+ );
+ }
+ return currentUnsignedTxPromise;
+};
+
+export const resetUnsignedTx = (): void => {
+ currentUnsignedTxPromise = null;
+ resolveCurrentUnsignedTxPromise = null;
+};
+
+// Export for connector (the resolver fn)
+export const resolveUnsignedTx = (data: OfflineTransactionDetails): void => {
+ if (resolveCurrentUnsignedTxPromise) {
+ resolveCurrentUnsignedTxPromise(data);
+ resolveCurrentUnsignedTxPromise = null; // One-time use
+ }
+};
From 8927a4305fb9b9322f79ddde26d502dd4e147677 Mon Sep 17 00:00:00 2001
From: valefar-on-discord
<124839138+valefar-on-discord@users.noreply.github.com>
Date: Thu, 12 Feb 2026 17:32:20 -0600
Subject: [PATCH 3/6] Add offline wallet support and offline tx submission
---
src/App.tsx | 12 +-
.../ConnectWallet/ConnectWallet.tsx | 19 +-
.../CopyToClipboard/CopyToCliboard.tsx | 42 ++++
src/components/CopyToClipboard/index.ts | 1 +
.../OfflineProgress/OfflineProgress.tsx | 185 ++++++++++++++++++
src/components/OfflineProgress/index.ts | 1 +
src/config/OfflineConnector.tsx | 8 +-
src/hooks/useConsolidate.ts | 22 ++-
src/hooks/useDeposit.ts | 2 +
src/hooks/useMulticall.ts | 23 ++-
src/hooks/useOfflineTransaction.ts | 98 ++++++++++
src/hooks/useWithdraw.ts | 22 ++-
.../Consolidate/ConsolidateProgressModal.tsx | 39 ++--
src/modals/Deposit/DepositProgressModal.tsx | 128 +++++++-----
src/modals/Exit/ExitProgressModal.tsx | 16 +-
.../OfflineConnect/OfflineConnectModal.tsx | 1 +
src/modals/OfflineMulti/OfflineMultiModal.tsx | 160 +++++++++++++++
src/modals/OfflineMulti/index.ts | 1 +
.../ProgressModal/ProgressModalConfirming.tsx | 24 +--
src/modals/TopUp/TopUpProgressModal.tsx | 93 +++++----
src/modals/Upgrade/UpgradeProgressModal.tsx | 16 +-
src/pages/Consolidate/Consolidate.tsx | 30 ++-
src/pages/Exit/Exit.tsx | 25 ++-
src/pages/PartialWithdraw/PartialWithdraw.tsx | 16 +-
src/pages/Upgrade/Upgrade.tsx | 29 ++-
src/polyfills.ts | 8 +-
src/types/consolidate.ts | 12 ++
src/types/index.ts | 1 +
src/types/transaction.ts | 2 +-
src/utils/deposit/index.ts | 2 +
src/utils/offline/index.ts | 2 +-
src/vite-env.d.ts | 4 +
vite.config.ts | 1 -
33 files changed, 886 insertions(+), 159 deletions(-)
create mode 100644 src/components/CopyToClipboard/CopyToCliboard.tsx
create mode 100644 src/components/CopyToClipboard/index.ts
create mode 100644 src/components/OfflineProgress/OfflineProgress.tsx
create mode 100644 src/components/OfflineProgress/index.ts
create mode 100644 src/hooks/useOfflineTransaction.ts
create mode 100644 src/modals/OfflineMulti/OfflineMultiModal.tsx
create mode 100644 src/modals/OfflineMulti/index.ts
create mode 100644 src/types/consolidate.ts
diff --git a/src/App.tsx b/src/App.tsx
index ccb2394..b9c00ef 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -26,9 +26,10 @@ const protectedRoutes = [
];
function AppContent() {
+ const { isConnected, isConnecting, isDisconnected } = useAccount();
const location = useLocation();
const navigate = useNavigate();
- const { isConnected, isConnecting } = useAccount();
+ const [hasConnected, setHasConnected] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const [redirectRoute, setRedirectRoute] = useState("");
@@ -39,6 +40,15 @@ function AppContent() {
return () => clearTimeout(timer);
}, []);
+ useEffect(() => {
+ if (isConnected && !hasConnected) {
+ setHasConnected(true);
+ } else if (hasConnected && isDisconnected) {
+ setHasConnected(false);
+ window.sessionStorage.removeItem("offline-address");
+ }
+ }, [isConnected, hasConnected, isDisconnected]);
+
useEffect(() => {
if (isConnected && protectedRoutes.includes(redirectRoute)) {
setRedirectRoute("");
diff --git a/src/components/ConnectWallet/ConnectWallet.tsx b/src/components/ConnectWallet/ConnectWallet.tsx
index 322cdc5..aae6ebb 100644
--- a/src/components/ConnectWallet/ConnectWallet.tsx
+++ b/src/components/ConnectWallet/ConnectWallet.tsx
@@ -1,6 +1,7 @@
import { AccountBalanceWallet as WalletIcon } from "@mui/icons-material";
import { Box, Typography } from "@mui/material";
-import { useState } from "react";
+import { useEffect, useState } from "react";
+import { isAddress } from "viem";
import { useChainId, useConnect } from "wagmi";
import { OfflineConnector } from "@/config/OfflineConnector";
@@ -12,6 +13,20 @@ export const ConnectWallet = () => {
const [showOfflineConnect, setShowOfflineConnect] = useState(false);
+ useEffect(() => {
+ const previousAddress = window.sessionStorage.getItem("offline-address");
+
+ if (!previousAddress) {
+ return;
+ }
+
+ if (isAddress(previousAddress)) {
+ onOfflineConnectModalClose(previousAddress as `0x${string}`);
+ } else {
+ window.sessionStorage.removeItem("offline-address");
+ }
+ }, []);
+
const onOfflineConnectModalClose = (address: `0x${string}` | undefined) => {
if (address) {
connect({
@@ -20,6 +35,8 @@ export const ConnectWallet = () => {
defaultChainId: chainId,
}),
});
+
+ window.sessionStorage.setItem("offline-address", address);
}
setShowOfflineConnect(false);
diff --git a/src/components/CopyToClipboard/CopyToCliboard.tsx b/src/components/CopyToClipboard/CopyToCliboard.tsx
new file mode 100644
index 0000000..88ece79
--- /dev/null
+++ b/src/components/CopyToClipboard/CopyToCliboard.tsx
@@ -0,0 +1,42 @@
+import { Box } from "@mui/material";
+import { ReactNode, useCallback, useState } from "react";
+
+interface CopyToClipboardProps {
+ children: ReactNode;
+ text: string;
+}
+
+export const CopyToClipboard = ({ children, text }: CopyToClipboardProps) => {
+ const [copySuccessful, setCopySuccessful] = useState(false);
+ const isSupported = !!navigator?.clipboard;
+
+ const handleCopy = useCallback(async () => {
+ if (!isSupported) {
+ return;
+ }
+
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopySuccessful(true);
+ const timer = setTimeout(() => setCopySuccessful(false), 1500);
+ return () => clearTimeout(timer);
+ } catch (e) {
+ console.error("Failed to copy: ", e);
+ }
+ }, [text, isSupported]);
+
+ return (
+
+ {children}
+
+ {copySuccessful && (
+
+ Copied Successfully
+
+ )}
+
+ );
+};
diff --git a/src/components/CopyToClipboard/index.ts b/src/components/CopyToClipboard/index.ts
new file mode 100644
index 0000000..ea27f5e
--- /dev/null
+++ b/src/components/CopyToClipboard/index.ts
@@ -0,0 +1 @@
+export * from "./CopyToCliboard";
diff --git a/src/components/OfflineProgress/OfflineProgress.tsx b/src/components/OfflineProgress/OfflineProgress.tsx
new file mode 100644
index 0000000..4ea9039
--- /dev/null
+++ b/src/components/OfflineProgress/OfflineProgress.tsx
@@ -0,0 +1,185 @@
+import {
+ Box,
+ Button,
+ CircularProgress,
+ TextField,
+ Typography,
+} from "@mui/material";
+import { useEffect, useMemo, useState } from "react";
+
+import { CopyToClipboard } from "@/components/CopyToClipboard";
+import { useOfflineTransaction } from "@/hooks/useOfflineTransaction";
+import {
+ ProgressModalConfirming,
+ ProgressModalSuccess,
+} from "@/modals/ProgressModal";
+import { OfflineTransactionDetails } from "@/types";
+
+interface OfflineProgressProps {
+ offlineData?: OfflineTransactionDetails;
+ onConfirmation: () => void;
+}
+
+export const OfflineProgress = ({
+ offlineData,
+ onConfirmation,
+}: OfflineProgressProps) => {
+ const [signedTx, setSignedTx] = useState<`0x${string}` | undefined>();
+ const [txDetails, setTxDetails] = useState("");
+ const [txError, setTxError] = useState();
+
+ const { confirmError, isConfirmed, reset, submitTransaction, txHash } =
+ useOfflineTransaction();
+
+ useMemo(() => {
+ if (offlineData) {
+ setTxDetails(JSON.stringify(offlineData.transaction, null, 2));
+ } else {
+ reset();
+ }
+ }, [offlineData, setTxDetails]);
+
+ const submitOfflineTransaction = async () => {
+ if (!signedTx || !offlineData) {
+ return;
+ }
+
+ setTxError(undefined);
+
+ try {
+ await submitTransaction(signedTx, offlineData);
+ } catch (e) {
+ setTxError(e as Error);
+ }
+ };
+
+ useEffect(() => {
+ if (isConfirmed && !!offlineData) {
+ onConfirmation();
+ }
+ }, [isConfirmed, offlineData, onConfirmation]);
+
+ return (
+
+
+ Sign the Unsigned Transaction Hash with your private key using a
+ secure method.
+
+
+ You may use the Signing Hash and Transaction Details to
+ verify the Unsigned Hash.
+
+
+ Please prioritize security to avoid exposing your key.
+
+
+ {offlineData ? (
+
+
+
+ Unsigned Transaction Hash
+
+
+
+ {offlineData.unsignedSerialized}
+
+
+
+
+
+ Signing Hash
+
+
+
+ {offlineData.signingHash}
+
+
+
+
+
+ Transaction Details
+
+
+
+ {txDetails}
+
+
+
+ {txHash ? (
+ <>
+
+
+ {isConfirmed && txHash && }
+ >
+ ) : (
+ <>
+
+
+ Signed Transaction
+
+ setSignedTx(e.target.value as `0x${string}`)}
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ color: "#ffffff",
+ backgroundColor: "#333743",
+ "& fieldset": {
+ borderColor: "#404040",
+ },
+ "&:hover fieldset": {
+ borderColor: "#606060",
+ },
+ "&.Mui-focused fieldset": {
+ borderColor: "#627EEA",
+ },
+ },
+ "& .MuiInputBase-input": {
+ color: "#ffffff",
+ padding: "8px 12px",
+ },
+ "& .MuiInputBase-input::placeholder": {
+ color: "#b3b3b3",
+ opacity: 1,
+ },
+ }}
+ />
+
+ {!!txError && (
+
+ {txError.message}
+
+ )}
+
+
+
+
+ >
+ )}
+
+ ) : (
+
+
+
+ Generating offline transaction...
+
+
+ )}
+
+ );
+};
diff --git a/src/components/OfflineProgress/index.ts b/src/components/OfflineProgress/index.ts
new file mode 100644
index 0000000..8d5ea69
--- /dev/null
+++ b/src/components/OfflineProgress/index.ts
@@ -0,0 +1 @@
+export * from "./OfflineProgress";
diff --git a/src/config/OfflineConnector.tsx b/src/config/OfflineConnector.tsx
index 83cc9a7..c9ab6b9 100644
--- a/src/config/OfflineConnector.tsx
+++ b/src/config/OfflineConnector.tsx
@@ -53,10 +53,10 @@ export function OfflineConnector(options: OfflineConnectorOptions) {
});
const serialized = serializeTransaction(preparedTx);
- const hash = keccak256(serialized as Hex);
+ const signingHash = keccak256(serialized as Hex);
const offlineData: OfflineTransactionDetails = {
- hash,
+ signingHash,
transaction: preparedTx,
unsignedSerialized: serialized,
};
@@ -75,10 +75,10 @@ export function OfflineConnector(options: OfflineConnectorOptions) {
chainId,
} as TransactionSerializable & { chainId: number };
const serialized = serializeTransaction(tx);
- const hash = keccak256(serialized as Hex);
+ const signingHash = keccak256(serialized as Hex);
const offlineData: OfflineTransactionDetails = {
- hash,
+ signingHash,
transaction: tx,
unsignedSerialized: serialized,
};
diff --git a/src/hooks/useConsolidate.ts b/src/hooks/useConsolidate.ts
index cf98343..ccfd13a 100644
--- a/src/hooks/useConsolidate.ts
+++ b/src/hooks/useConsolidate.ts
@@ -1,18 +1,25 @@
import { useMemo, useState } from "react";
import {
useChainId,
+ useConnections,
useSendTransaction,
useWaitForTransactionReceipt,
} from "wagmi";
+import { OfflineTransactionDetails } from "@/types";
import {
getContractAddress,
getConsolidationQueue,
generateConsolidateCalldata,
} from "@/utils/consolidate";
+import { getUnsignedTxPromise } from "@/utils/offline";
export const useConsolidate = () => {
const chainId = useChainId();
+ const [currentConnection] = useConnections();
+ const [offlineData, setOfflineData] = useState<
+ OfflineTransactionDetails | undefined
+ >();
const [txHash, setTxHash] = useState<`0x${string}` | undefined>();
const contractAddress = useMemo(() => getContractAddress(chainId), [chainId]);
@@ -37,6 +44,7 @@ export const useConsolidate = () => {
source: `0x${string}`,
target: `0x${string}`,
) => {
+ setOfflineData(undefined);
setTxHash(undefined);
const queue = await getConsolidationQueue(chainId);
@@ -54,16 +62,27 @@ export const useConsolidate = () => {
},
{
onSuccess: (hash) => {
- setTxHash(hash);
+ // Avoid setting the hash when using the offline connect to prevent polling for transaction confirmation
+ if (currentConnection?.connector?.id !== "offline") {
+ setTxHash(hash);
+ }
},
onError: (e) => {
console.log(e);
},
},
);
+
+ if (currentConnection?.connector?.id === "offline") {
+ const data = await getUnsignedTxPromise();
+ if (data) {
+ setOfflineData(data);
+ }
+ }
};
const reset = () => {
+ setOfflineData(undefined);
setTxHash(undefined);
resetSendTransaction();
};
@@ -76,6 +95,7 @@ export const useConsolidate = () => {
isPendingConfirmation: isPendingConfirmation && !!txHash,
isPendingSignature: isPendingSignature || !txHash,
isSendSuccess,
+ offlineData,
sendConsolidate,
txHash,
reset,
diff --git a/src/hooks/useDeposit.ts b/src/hooks/useDeposit.ts
index dff5666..98244cb 100644
--- a/src/hooks/useDeposit.ts
+++ b/src/hooks/useDeposit.ts
@@ -19,6 +19,7 @@ export const useDeposit = () => {
isPendingConfirmation,
isPendingSignature,
isSendSuccess,
+ offlineData,
reset: resetMulticall,
sendMulticall,
txHash,
@@ -57,6 +58,7 @@ export const useDeposit = () => {
isPendingConfirmation,
isPendingSignature,
isSendSuccess,
+ offlineData,
reset,
txHash,
writeDeposit,
diff --git a/src/hooks/useMulticall.ts b/src/hooks/useMulticall.ts
index e146017..24278fb 100644
--- a/src/hooks/useMulticall.ts
+++ b/src/hooks/useMulticall.ts
@@ -3,14 +3,20 @@ import {
useChainId,
useWriteContract,
useWaitForTransactionReceipt,
+ useConnections,
} from "wagmi";
import { multicallAbi } from "@/abi";
-import { MulticallData } from "@/types";
+import { MulticallData, OfflineTransactionDetails } from "@/types";
import { getContractAddress } from "@/utils/multicall";
+import { getUnsignedTxPromise } from "@/utils/offline";
export const useMulticall = () => {
const chainId = useChainId();
+ const [currentConnection] = useConnections();
+ const [offlineData, setOfflineData] = useState<
+ OfflineTransactionDetails | undefined
+ >();
const [txHash, setTxHash] = useState<`0x${string}` | undefined>();
const contractAddress = useMemo(() => getContractAddress(chainId), [chainId]);
@@ -32,6 +38,7 @@ export const useMulticall = () => {
});
const sendMulticall = async (calls: MulticallData[], totalValue: bigint) => {
+ setOfflineData(undefined);
setTxHash(undefined);
writeContract(
@@ -44,13 +51,24 @@ export const useMulticall = () => {
},
{
onSuccess: (hash) => {
- setTxHash(hash);
+ // Avoid setting the hash when using the offline connect to prevent polling for transaction confirmation
+ if (currentConnection?.connector?.id !== "offline") {
+ setTxHash(hash);
+ }
},
},
);
+
+ if (currentConnection?.connector?.id === "offline") {
+ const data = await getUnsignedTxPromise();
+ if (data) {
+ setOfflineData(data);
+ }
+ }
};
const reset = () => {
+ setOfflineData(undefined);
setTxHash(undefined);
resetWriteContract();
};
@@ -62,6 +80,7 @@ export const useMulticall = () => {
isPendingConfirmation: isPendingConfirmation && !!txHash,
isPendingSignature: isPendingSignature || !txHash,
isSendSuccess,
+ offlineData,
reset,
sendMulticall,
txHash,
diff --git a/src/hooks/useOfflineTransaction.ts b/src/hooks/useOfflineTransaction.ts
new file mode 100644
index 0000000..d40ff4e
--- /dev/null
+++ b/src/hooks/useOfflineTransaction.ts
@@ -0,0 +1,98 @@
+import { useState } from "react";
+import { parseTransaction, recoverTransactionAddress } from "viem";
+import { useAccount, useWaitForTransactionReceipt } from "wagmi";
+import { getPublicClient } from "wagmi/actions";
+
+import { wagmiAdapter } from "@/config/appkit";
+import { OfflineTransactionDetails } from "@/types";
+
+export const useOfflineTransaction = () => {
+ const { address } = useAccount();
+ const [txHash, setTxHash] = useState<`0x${string}` | undefined>();
+
+ const {
+ isPending: isPendingConfirmation,
+ isSuccess: isConfirmed,
+ error: confirmError,
+ } = useWaitForTransactionReceipt({
+ hash: txHash,
+ });
+
+ const submitTransaction = async (
+ hash: `0x${string}`,
+ offlineData: OfflineTransactionDetails,
+ ): Promise<`0x${string}`> => {
+ const config = wagmiAdapter.wagmiConfig;
+ const client = getPublicClient(config);
+ if (!client) {
+ throw new Error("Failed to get public client");
+ }
+
+ const recoveredAddress = await recoverTransactionAddress({
+ serializedTransaction: hash as any,
+ });
+
+ if (recoveredAddress.toLowerCase() !== address?.toLowerCase()) {
+ throw new Error(
+ "Address used to sign transaction does not match connected wallet",
+ );
+ }
+
+ const tx = parseTransaction(hash);
+
+ if (tx.data !== offlineData.transaction.data) {
+ throw new Error(
+ "Transaction data mismatch: The provided signed hash does not match the transaction requested",
+ );
+ }
+
+ if (tx.chainId !== offlineData.transaction.chainId) {
+ throw new Error(
+ "Chain mismatch: The provided signed hash chain ID does not match the transaction requested",
+ );
+ }
+
+ if (tx.nonce !== offlineData.transaction.nonce) {
+ throw new Error(
+ "Nonce mismatch: The provided signed hash nonce does not match the transaction requested",
+ );
+ }
+
+ // offlineData.transaction.value claims to be bigint but it is returned as a hex
+ if (
+ `0x${tx.value?.toString(16)}` !==
+ (offlineData.transaction.value as any as string)
+ ) {
+ throw new Error(
+ "Value mismatch: The provided signed value does not match the transaction requested",
+ );
+ }
+
+ if (tx.to?.toLowerCase() !== offlineData.transaction.to?.toLowerCase()) {
+ throw new Error(
+ "Destination mismatch: The provided signed value destination does not match the transaction requested",
+ );
+ }
+
+ const transactionHash = await client?.sendRawTransaction({
+ serializedTransaction: hash,
+ });
+
+ setTxHash(transactionHash);
+
+ return transactionHash;
+ };
+
+ const reset = () => {
+ setTxHash(undefined);
+ };
+
+ return {
+ confirmError,
+ isConfirmed,
+ isPendingConfirmation,
+ reset,
+ submitTransaction,
+ txHash,
+ };
+};
diff --git a/src/hooks/useWithdraw.ts b/src/hooks/useWithdraw.ts
index 78206a7..0276bc0 100644
--- a/src/hooks/useWithdraw.ts
+++ b/src/hooks/useWithdraw.ts
@@ -1,10 +1,13 @@
import { useMemo, useState } from "react";
import {
useChainId,
+ useConnections,
useSendTransaction,
useWaitForTransactionReceipt,
} from "wagmi";
+import { OfflineTransactionDetails } from "@/types";
+import { getUnsignedTxPromise } from "@/utils/offline";
import {
generateWithdrawalCalldata,
getContractAddress,
@@ -13,6 +16,10 @@ import {
export const useWithdraw = () => {
const chainId = useChainId();
+ const [currentConnection] = useConnections();
+ const [offlineData, setOfflineData] = useState<
+ OfflineTransactionDetails | undefined
+ >();
const [txHash, setTxHash] = useState<`0x${string}` | undefined>();
const contractAddress = useMemo(() => getContractAddress(chainId), [chainId]);
@@ -34,6 +41,7 @@ export const useWithdraw = () => {
});
const sendWithdraw = async (pubkey: `0x${string}`, amount: string) => {
+ setOfflineData(undefined);
setTxHash(undefined);
const queue = await getWithdrawalQueue(chainId);
@@ -51,16 +59,27 @@ export const useWithdraw = () => {
},
{
onSuccess: (hash) => {
- setTxHash(hash);
+ // Avoid setting the hash when using the offline connect to prevent polling for transaction confirmation
+ if (currentConnection?.connector?.id !== "offline") {
+ setTxHash(hash);
+ }
},
onError: (e) => {
console.log(e);
},
},
);
+
+ if (currentConnection?.connector?.id === "offline") {
+ const data = await getUnsignedTxPromise();
+ if (data) {
+ setOfflineData(data);
+ }
+ }
};
const reset = () => {
+ setOfflineData(undefined);
setTxHash(undefined);
resetSendTransaction();
};
@@ -73,6 +92,7 @@ export const useWithdraw = () => {
isPendingConfirmation: isPendingConfirmation && !!txHash,
isPendingSignature: isPendingSignature || !txHash,
isSendSuccess,
+ offlineData,
reset,
sendWithdraw,
txHash,
diff --git a/src/modals/Consolidate/ConsolidateProgressModal.tsx b/src/modals/Consolidate/ConsolidateProgressModal.tsx
index e23770d..2d06045 100644
--- a/src/modals/Consolidate/ConsolidateProgressModal.tsx
+++ b/src/modals/Consolidate/ConsolidateProgressModal.tsx
@@ -8,23 +8,21 @@ import { TransactionStatus } from "@/components/TransactionState";
import { useConsolidate } from "@/hooks/useConsolidate";
import { useTransactions } from "@/hooks/useTransactions";
import { ProgressModal } from "@/modals/ProgressModal";
-import { Transaction, TransactionState, Validator } from "@/types";
-
-interface ConsolidateTransaction extends Transaction {
- targetValidator: Validator;
- sourceValidator: Validator;
-}
+import {
+ ConsolidateEntry,
+ ConsolidateTransaction,
+ TransactionState,
+} from "@/types";
interface ConsolidateProgressModalProps {
open: boolean;
onClose: () => void;
- targetValidator: Validator;
- sourceValidators: Validator[];
+ consolidateEntries: ConsolidateEntry[];
}
export const ConsolidateProgressModal: React.FC<
ConsolidateProgressModalProps
-> = ({ open, onClose, targetValidator, sourceValidators }) => {
+> = ({ open, onClose, consolidateEntries }) => {
const { contractAddress, sendConsolidate, reset, ...consolidateProps } =
useConsolidate();
@@ -58,21 +56,16 @@ export const ConsolidateProgressModal: React.FC<
...consolidateProps,
});
- // Initialize transactions when validators change
useEffect(() => {
- if (sourceValidators.length > 0 && !!targetValidator) {
- const initialTransactions: ConsolidateTransaction[] =
- sourceValidators.map((validator) => ({
- validator,
- sourceValidator: validator,
- state: TransactionState.pending,
- targetValidator,
- }));
- setTransactions(initialTransactions);
- } else {
- setTransactions([]);
- }
- }, [targetValidator, sourceValidators]);
+ const txs = consolidateEntries.map((entry) => ({
+ validator: entry.sourceValidator,
+ sourceValidator: entry.sourceValidator,
+ state: TransactionState.pending,
+ targetValidator: entry.targetValidator,
+ }));
+
+ setTransactions(txs);
+ }, [consolidateEntries]);
const handleRowClick = (index: number, state: TransactionState) => {
// Only allow clicking on completed, error, or skip states
diff --git a/src/modals/Deposit/DepositProgressModal.tsx b/src/modals/Deposit/DepositProgressModal.tsx
index 23ed298..7e0da3d 100644
--- a/src/modals/Deposit/DepositProgressModal.tsx
+++ b/src/modals/Deposit/DepositProgressModal.tsx
@@ -1,8 +1,10 @@
import { Info } from "@mui/icons-material";
import { Box, Link, Typography } from "@mui/material";
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
+import { useConnections } from "wagmi";
+import { OfflineProgress } from "@/components/OfflineProgress";
import { WarningAlert } from "@/components/WarningAlert";
import { useDeposit } from "@/hooks/useDeposit";
import {
@@ -28,11 +30,13 @@ export const DepositProgressModal: React.FC = ({
open,
onClose,
}) => {
+ const [currentConnection] = useConnections();
const {
writeDeposit,
isConfirmed,
isPendingSignature,
confirmError,
+ offlineData,
reset,
sendError,
txHash,
@@ -40,6 +44,7 @@ export const DepositProgressModal: React.FC = ({
const navigate = useNavigate();
const [downloadUrl, setDownloadUrl] = useState("");
+ const [offlineSuccess, setOfflineSuccess] = useState(false);
useEffect(() => {
if (open && selectedDepositData.length > 0) {
@@ -74,72 +79,93 @@ export const DepositProgressModal: React.FC = ({
writeDeposit(selectedDepositData);
};
+ const onOfflineConfirmation = () => {
+ setOfflineSuccess(true);
+ };
+
const closeModal = () => {
- if (isConfirmed) {
+ if (isConfirmed || offlineSuccess) {
navigate("/dashboard");
}
onClose();
+ setDownloadUrl("");
+ setOfflineSuccess(false);
};
+ const isOffline = useMemo(() => {
+ return currentConnection?.connector?.id === "offline";
+ }, [currentConnection]);
+
return (
-
-
- Once the transaction is submitted and confirmed your deposit request
- will be processed by the Beacon Chain and then added to the activation
- queue.
-
-
-
-
-
-
-
- {!!downloadUrl && (
-
-
-
-
- You have not deposited all validators in the uploaded deposit
- JSON file.
-
-
- Click here to download the undeposited validators
-
+ {isOffline ? (
+
+ ) : (
+
+
+ Once the transaction is submitted and confirmed your deposit request
+ will be processed by the Beacon Chain and then added to the
+ activation queue.
+
+
+
+
+
+
+
+ {!!downloadUrl && (
+
+
+
+
+ You have not deposited all validators in the uploaded
+ deposit JSON file.
+
+
+ Click here to download the undeposited validators
+
+
-
- )}
+ )}
- {isConfirmed && txHash && }
+ {isConfirmed && txHash && }
- {isConfirmed && (
-
- It will take a few minutes for the new deposits to reach the
- Beacon Chain and be reflected in the dashboard.
-
- )}
+ {isConfirmed && (
+
+ It will take a few minutes for the new deposits to reach the
+ Beacon Chain and be reflected in the dashboard.
+
+ )}
+
-
+ )}
);
};
diff --git a/src/modals/Exit/ExitProgressModal.tsx b/src/modals/Exit/ExitProgressModal.tsx
index 2445499..15bbfd1 100644
--- a/src/modals/Exit/ExitProgressModal.tsx
+++ b/src/modals/Exit/ExitProgressModal.tsx
@@ -9,18 +9,18 @@ import { useTransactions } from "@/hooks/useTransactions";
import { useValidators } from "@/hooks/useValidators";
import { useWithdraw } from "@/hooks/useWithdraw";
import { ProgressModal } from "@/modals/ProgressModal";
-import { Transaction, TransactionState, Validator } from "@/types";
+import { Transaction, TransactionState, WithdrawalEntry } from "@/types";
interface ExitProgressModalProps {
open: boolean;
onClose: () => void;
- validators: Validator[];
+ withdrawals: WithdrawalEntry[];
}
export const ExitProgressModal: React.FC = ({
open,
onClose,
- validators,
+ withdrawals,
}) => {
const { refetch: refetchValidators } = useValidators();
const { contractAddress, sendWithdraw, reset, ...withdrawProps } =
@@ -56,10 +56,10 @@ export const ExitProgressModal: React.FC = ({
// Initialize transactions when validators change
useEffect(() => {
- if (validators.length > 0 && open) {
- const initialTransactions: Transaction[] = validators.map(
- (validator) => ({
- validator,
+ if (withdrawals.length > 0 && open) {
+ const initialTransactions: Transaction[] = withdrawals.map(
+ (withdrawal) => ({
+ validator: withdrawal.validator,
state: TransactionState.pending,
}),
);
@@ -67,7 +67,7 @@ export const ExitProgressModal: React.FC = ({
} else {
setTransactions([]);
}
- }, [open, validators]);
+ }, [open, withdrawals]);
const handleRowClick = (index: number, state: TransactionState) => {
// Only allow clicking on completed, error, or skip states
diff --git a/src/modals/OfflineConnect/OfflineConnectModal.tsx b/src/modals/OfflineConnect/OfflineConnectModal.tsx
index 0691b60..ca13781 100644
--- a/src/modals/OfflineConnect/OfflineConnectModal.tsx
+++ b/src/modals/OfflineConnect/OfflineConnectModal.tsx
@@ -66,6 +66,7 @@ export const OfflineConnectModal = ({
{
+ open: boolean;
+ onClose: () => void;
+ title: string;
+ transactions: T[];
+ type: "consolidate" | "withdraw";
+}
+
+export const OfflineMultiModal = ({
+ open,
+ onClose,
+ title,
+ transactions,
+ type,
+}: OfflineMultiModalProps) => {
+ const navigate = useNavigate();
+ const {
+ offlineData: offlineConsolidate,
+ reset: resetConsolidate,
+ sendConsolidate,
+ } = useConsolidate();
+ const {
+ offlineData: offlineWithdraw,
+ reset: resetWithdraw,
+ sendWithdraw,
+ } = useWithdraw();
+ const { refetch: refetchValidators } = useValidators();
+
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [transactionComplete, setTransactionComplete] =
+ useState(false);
+
+ const generateTransaction = (transaction: T) => {
+ if (type === "consolidate") {
+ const consolidateTransaction = transaction as ConsolidateEntry;
+ sendConsolidate(
+ consolidateTransaction.sourceValidator.pubkey,
+ consolidateTransaction.targetValidator.pubkey,
+ );
+ } else if (type === "withdraw") {
+ const withdrawTransaction = transaction as WithdrawalEntry;
+ sendWithdraw(
+ withdrawTransaction.validator.pubkey,
+ withdrawTransaction.withdrawalAmount,
+ );
+ }
+ };
+
+ useEffect(() => {
+ if (transactions[currentIndex]) {
+ generateTransaction(transactions[currentIndex]);
+ }
+ }, [currentIndex, transactions]);
+
+ const currentOfflineData = useMemo(() => {
+ if (type === "consolidate") {
+ return offlineConsolidate;
+ } else if (type === "withdraw") {
+ return offlineWithdraw;
+ }
+ }, [offlineConsolidate, offlineWithdraw, type]);
+
+ const onConfirmation = () => {
+ setTransactionComplete(true);
+ };
+
+ const onNextTransaction = () => {
+ resetConsolidate();
+ resetWithdraw();
+ setCurrentIndex((prev) => Math.min(transactions.length - 1, prev + 1));
+ setTransactionComplete(false);
+ };
+
+ const handleClose = () => {
+ onClose();
+ setCurrentIndex(0);
+ setTransactionComplete(false);
+ };
+
+ const onFinish = () => {
+ setCurrentIndex(0);
+ setTransactionComplete(false);
+
+ refetchValidators();
+ navigate("/dashboard");
+ };
+
+ return (
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+ {transactions.map((t, index) => (
+
+
+ Transaction {index + 1}/{transactions.length}
+
+
+
+
+
+ {index < transactions.length - 1 && (
+
+ )}
+ {index === transactions.length - 1 && (
+
+ )}
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/src/modals/OfflineMulti/index.ts b/src/modals/OfflineMulti/index.ts
new file mode 100644
index 0000000..9744dba
--- /dev/null
+++ b/src/modals/OfflineMulti/index.ts
@@ -0,0 +1 @@
+export * from "./OfflineMultiModal";
diff --git a/src/modals/ProgressModal/ProgressModalConfirming.tsx b/src/modals/ProgressModal/ProgressModalConfirming.tsx
index 52c9be2..e10c8a9 100644
--- a/src/modals/ProgressModal/ProgressModalConfirming.tsx
+++ b/src/modals/ProgressModal/ProgressModalConfirming.tsx
@@ -6,8 +6,8 @@ interface ConfirmingProps {
confirmationError: WaitForTransactionReceiptErrorType | null;
confirmedMessage: string;
confirmingMessage: string;
- isWaiting: boolean;
- onRetry: () => void;
+ isWaiting?: boolean;
+ onRetry?: () => void;
success: boolean;
waitingMessage: string;
}
@@ -16,7 +16,7 @@ export const ProgressModalConfirming = ({
confirmationError,
confirmedMessage,
confirmingMessage,
- isWaiting,
+ isWaiting = false,
onRetry,
success,
waitingMessage,
@@ -54,14 +54,16 @@ export const ProgressModalConfirming = ({
There was an error confirming the transaction
-
+ {!!onRetry && (
+
+ )}
{confirmationError.message}
diff --git a/src/modals/TopUp/TopUpProgressModal.tsx b/src/modals/TopUp/TopUpProgressModal.tsx
index 7702cf1..d8ab7d7 100644
--- a/src/modals/TopUp/TopUpProgressModal.tsx
+++ b/src/modals/TopUp/TopUpProgressModal.tsx
@@ -1,8 +1,11 @@
import { Box, Typography } from "@mui/material";
import BigNumber from "bignumber.js";
-import React, { useEffect } from "react";
+import { Buffer } from "buffer";
+import React, { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
+import { useConnections } from "wagmi";
+import { OfflineProgress } from "@/components/OfflineProgress";
import { useDeposit } from "@/hooks/useDeposit";
import {
ProgressModal,
@@ -24,17 +27,21 @@ export const TopUpProgressModal: React.FC = ({
open,
onClose,
}) => {
+ const [currentConnection] = useConnections();
const {
writeDeposit,
isConfirmed,
isPendingSignature,
confirmError,
+ offlineData,
reset,
sendError,
txHash,
} = useDeposit();
const navigate = useNavigate();
+ const [offlineSuccess, setOfflineSuccess] = useState(false);
+
useEffect(() => {
if (open && entries.length > 0) {
executeTransaction();
@@ -68,53 +75,69 @@ export const TopUpProgressModal: React.FC = ({
executeTransaction();
};
+ const onOfflineConfirmation = () => {
+ setOfflineSuccess(true);
+ };
+
const onCloseModal = () => {
- if (isConfirmed) {
+ if (isConfirmed || offlineSuccess) {
navigate("/dashboard");
}
onClose();
};
+ const isOffline = useMemo(() => {
+ return currentConnection?.connector?.id === "offline";
+ }, [currentConnection]);
+
return (
-
-
- Once the transaction is submitted and confirmed your deposit request
- will be processed by the Beacon Chain and then added to the activation
- queue.
-
-
-
-
-
-
-
- {isConfirmed && txHash && }
+ {isOffline ? (
+
+ ) : (
+
+
+ Once the transaction is submitted and confirmed your TopUp request
+ will be processed by the Beacon Chain and then added to the
+ activation queue.
+
+
+
+
+
+
+ {isConfirmed && txHash && }
+
-
+ )}
);
};
diff --git a/src/modals/Upgrade/UpgradeProgressModal.tsx b/src/modals/Upgrade/UpgradeProgressModal.tsx
index a73c4d0..eda34cc 100644
--- a/src/modals/Upgrade/UpgradeProgressModal.tsx
+++ b/src/modals/Upgrade/UpgradeProgressModal.tsx
@@ -8,18 +8,18 @@ import { TransactionStatus } from "@/components/TransactionState";
import { useConsolidate } from "@/hooks/useConsolidate";
import { useTransactions } from "@/hooks/useTransactions";
import { ProgressModal } from "@/modals/ProgressModal";
-import { Transaction, TransactionState, Validator } from "@/types";
+import { ConsolidateEntry, Transaction, TransactionState } from "@/types";
interface UpgradeProgressModalProps {
open: boolean;
onClose: () => void;
- validators: Validator[];
+ consolidateEntries: ConsolidateEntry[];
}
export const UpgradeProgressModal: React.FC = ({
open,
onClose,
- validators = [],
+ consolidateEntries,
}) => {
const { contractAddress, sendConsolidate, reset, ...consolidateProps } =
useConsolidate();
@@ -55,10 +55,10 @@ export const UpgradeProgressModal: React.FC = ({
// Initialize transactions when validators change
useEffect(() => {
- if (validators.length > 0) {
- const initialTransactions: Transaction[] = validators.map(
- (validator) => ({
- validator,
+ if (consolidateEntries.length > 0) {
+ const initialTransactions: Transaction[] = consolidateEntries.map(
+ (entry) => ({
+ validator: entry.sourceValidator,
state: TransactionState.pending,
}),
);
@@ -66,7 +66,7 @@ export const UpgradeProgressModal: React.FC = ({
} else {
setTransactions([]);
}
- }, [validators]);
+ }, [consolidateEntries]);
const handleRowClick = (index: number, state: TransactionState) => {
// Only allow clicking on completed, error, or skip states
diff --git a/src/pages/Consolidate/Consolidate.tsx b/src/pages/Consolidate/Consolidate.tsx
index ca43ee4..0c3e95f 100644
--- a/src/pages/Consolidate/Consolidate.tsx
+++ b/src/pages/Consolidate/Consolidate.tsx
@@ -1,5 +1,6 @@
import { Box, Typography, Button } from "@mui/material";
import React, { useState, useMemo, useEffect } from "react";
+import { useConnections } from "wagmi";
import { ConsolidationSourceValidatorsTable } from "@/components/ConsolidationSourceValidatorsTable";
import { FilterInput } from "@/components/FilterInput";
@@ -15,10 +16,13 @@ import {
ConsolidateBatchProgressModal,
ConsolidateProgressModal,
} from "@/modals/Consolidate";
+import { OfflineMultiModal } from "@/modals/OfflineMulti";
import { TargetValidatorSelectionModal } from "@/modals/TargetValidatorSelectionModal";
+import { ConsolidateEntry } from "@/types/consolidate";
import { Credentials, Validator, ValidatorStatus } from "@/types/validator";
const Consolidate: React.FC = () => {
+ const [currentConnection] = useConnections();
const { selectedValidator, setSelectedValidator } = useSelectedValidator();
const { allowSendMany } = useSendMany();
const { data: validatorData } = useValidators();
@@ -145,6 +149,21 @@ const Consolidate: React.FC = () => {
setShowProgressModal(false);
};
+ const consolidateEntries: ConsolidateEntry[] = useMemo(() => {
+ if (!targetValidator) {
+ return [];
+ }
+
+ return sourceValidators.map((s) => ({
+ sourceValidator: s,
+ targetValidator,
+ }));
+ }, [targetValidator, sourceValidators]);
+
+ const isOffline = useMemo(() => {
+ return currentConnection?.connector?.id === "offline";
+ }, [currentConnection]);
+
if (!targetValidator) {
return (
<>
@@ -314,12 +333,19 @@ const Consolidate: React.FC = () => {
targetValidator={targetValidator}
sourceValidators={sourceValidators}
/>
+ ) : isOffline ? (
+
) : (
)}
>
diff --git a/src/pages/Exit/Exit.tsx b/src/pages/Exit/Exit.tsx
index b833a11..ea863ba 100644
--- a/src/pages/Exit/Exit.tsx
+++ b/src/pages/Exit/Exit.tsx
@@ -1,6 +1,6 @@
import { Box, Typography, Button } from "@mui/material";
import React, { useState, useMemo } from "react";
-import { useAccount } from "wagmi";
+import { useAccount, useConnections } from "wagmi";
import { ExitValidatorTable } from "@/components/ExitValidatorTable";
import { Meta } from "@/components/Meta";
@@ -12,9 +12,11 @@ import {
ExitBatchProgressModal,
ExitProgressModal,
} from "@/modals/Exit";
+import { OfflineMultiModal } from "@/modals/OfflineMulti";
const Exit: React.FC = () => {
const { address } = useAccount();
+ const [currentConnection] = useConnections();
const { allowSendMany } = useSendMany();
const { data: validatorData } = useValidators();
const [selectedValidators, setSelectedValidators] = useState([]);
@@ -63,6 +65,17 @@ const Exit: React.FC = () => {
return balance.toFixed(4);
};
+ const withdrawalEntries = useMemo(() => {
+ return selectedValidatorData.map((validator) => ({
+ validator,
+ withdrawalAmount: "0",
+ }));
+ }, [selectedValidatorData]);
+
+ const isOffline = useMemo(() => {
+ return currentConnection?.connector?.id === "offline";
+ }, [currentConnection]);
+
return (
<>
@@ -132,11 +145,19 @@ const Exit: React.FC = () => {
onClose={handleCloseProgressModal}
validators={selectedValidatorData}
/>
+ ) : isOffline ? (
+
) : (
)}
diff --git a/src/pages/PartialWithdraw/PartialWithdraw.tsx b/src/pages/PartialWithdraw/PartialWithdraw.tsx
index 278d848..fea69bc 100644
--- a/src/pages/PartialWithdraw/PartialWithdraw.tsx
+++ b/src/pages/PartialWithdraw/PartialWithdraw.tsx
@@ -1,12 +1,13 @@
import { Box, Typography, Button } from "@mui/material";
import BigNumber from "bignumber.js";
import React, { useState, useMemo } from "react";
-import { useAccount } from "wagmi";
+import { useAccount, useConnections } from "wagmi";
import { Meta } from "@/components/Meta";
import { PartialWithdrawValidatorTable } from "@/components/PartialWithdrawValidatorTable";
import { useSendMany } from "@/hooks/useSendMany";
import { useValidators } from "@/hooks/useValidators";
+import { OfflineMultiModal } from "@/modals/OfflineMulti";
import {
PartialWithdrawBatchProgressModal,
PartialWithdrawConfirmModal,
@@ -17,6 +18,7 @@ import { WithdrawalEntry } from "@/types";
const PartialWithdraw: React.FC = () => {
const { address } = useAccount();
+ const [currentConnection] = useConnections();
const { allowSendMany } = useSendMany();
const { data: validatorData } = useValidators();
const [entries, setEntries] = useState([]);
@@ -64,6 +66,10 @@ const PartialWithdraw: React.FC = () => {
return balance.toFixed(4);
};
+ const isOffline = useMemo(() => {
+ return currentConnection?.connector?.id === "offline";
+ }, [currentConnection]);
+
return (
<>
@@ -131,6 +137,14 @@ const PartialWithdraw: React.FC = () => {
onClose={handleCloseProgressModal}
withdrawals={entries}
/>
+ ) : isOffline ? (
+
) : (
{
+ const [currentConnection] = useConnections();
const { allowSendMany } = useSendMany();
const { data: validatorData } = useValidators();
const [selectedPubkeys, setSelectedPubkeys] = useState([]);
@@ -53,6 +57,21 @@ const Upgrade: React.FC = () => {
selectedPubkeys.includes(v.pubkey),
);
+ const consolidateEntries: ConsolidateEntry[] = useMemo(() => {
+ if (!selectedValidatorObjects) {
+ return [];
+ }
+
+ return selectedValidatorObjects.map((s) => ({
+ sourceValidator: s,
+ targetValidator: s,
+ }));
+ }, [selectedValidatorObjects]);
+
+ const isOffline = useMemo(() => {
+ return currentConnection?.connector?.id === "offline";
+ }, [currentConnection]);
+
return (
<>
@@ -110,11 +129,19 @@ const Upgrade: React.FC = () => {
onClose={handleCloseProgressModal}
validators={selectedValidatorObjects}
/>
+ ) : isOffline ? (
+
) : (
)}
diff --git a/src/polyfills.ts b/src/polyfills.ts
index 7327b23..232c757 100644
--- a/src/polyfills.ts
+++ b/src/polyfills.ts
@@ -1,4 +1,4 @@
-import { Buffer } from "buffer";
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-(globalThis as any).Buffer = Buffer;
+// Required to stringify transaction objects
+BigInt.prototype.toJSON = function () {
+ return Number(this);
+};
diff --git a/src/types/consolidate.ts b/src/types/consolidate.ts
new file mode 100644
index 0000000..68c7619
--- /dev/null
+++ b/src/types/consolidate.ts
@@ -0,0 +1,12 @@
+import { Transaction } from "./transaction";
+import { Validator } from "./validator";
+
+export interface ConsolidateEntry {
+ sourceValidator: Validator;
+ targetValidator: Validator;
+}
+
+export interface ConsolidateTransaction extends Transaction {
+ targetValidator: Validator;
+ sourceValidator: Validator;
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index 0181d0b..1ba856d 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,3 +1,4 @@
+export * from "./consolidate";
export * from "./deposit";
export * from "./multicall";
export * from "./sendMany";
diff --git a/src/types/transaction.ts b/src/types/transaction.ts
index 78059ed..d966be1 100644
--- a/src/types/transaction.ts
+++ b/src/types/transaction.ts
@@ -24,7 +24,7 @@ export interface Transaction {
}
export interface OfflineTransactionDetails {
- hash: `0x${string}`;
+ signingHash: `0x${string}`;
transaction: TransactionSerializable;
unsignedSerialized: `0x${string}`;
}
diff --git a/src/utils/deposit/index.ts b/src/utils/deposit/index.ts
index 8af90b8..3df2913 100644
--- a/src/utils/deposit/index.ts
+++ b/src/utils/deposit/index.ts
@@ -1,3 +1,5 @@
+import { Buffer } from "buffer";
+
import {
getChainName,
getForkVersion,
diff --git a/src/utils/offline/index.ts b/src/utils/offline/index.ts
index be4b2c6..26ba521 100644
--- a/src/utils/offline/index.ts
+++ b/src/utils/offline/index.ts
@@ -25,6 +25,6 @@ export const resetUnsignedTx = (): void => {
export const resolveUnsignedTx = (data: OfflineTransactionDetails): void => {
if (resolveCurrentUnsignedTxPromise) {
resolveCurrentUnsignedTxPromise(data);
- resolveCurrentUnsignedTxPromise = null; // One-time use
+ resetUnsignedTx();
}
};
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index ab85839..53dc16d 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -20,3 +20,7 @@ interface Window {
) => void;
dataLayer?: any[];
}
+
+interface BigInt {
+ toJSON(): Number;
+}
diff --git a/vite.config.ts b/vite.config.ts
index 7e11f8e..47ebfd0 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -28,7 +28,6 @@ export default defineConfig(({ command, mode }) => {
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
- buffer: 'buffer',
},
},
build: {
From 34a31e9f8583ac83f347f55ee364c3acd200089d Mon Sep 17 00:00:00 2001
From: valefar-on-discord
<124839138+valefar-on-discord@users.noreply.github.com>
Date: Sat, 21 Feb 2026 08:06:07 -0600
Subject: [PATCH 4/6] Consolidating primary input usage into a single component
---
.../DashboardValidatorsTable.tsx | 2 +-
.../ExitValidatorTable/ExitValidatorTable.tsx | 4 +-
.../{FilterInput => Input}/FilterInput.tsx | 0
src/components/Input/Input.tsx | 43 +++++++++++++++++++
.../{FilterInput => Input}/index.ts | 1 +
.../OfflineProgress/OfflineProgress.tsx | 38 ++--------------
.../PartialWithdrawValidatorTable.tsx | 2 +-
.../TopUpValidatorTable.tsx | 2 +-
.../UpgradeValidatorsTable.tsx | 2 +-
src/modals/Exit/ExitConfirmModal.tsx | 31 ++-----------
.../OfflineConnect/OfflineConnectModal.tsx | 35 +++------------
.../TargetValidatorSelectionModal.tsx | 32 ++------------
src/pages/Consolidate/Consolidate.tsx | 2 +-
13 files changed, 66 insertions(+), 128 deletions(-)
rename src/components/{FilterInput => Input}/FilterInput.tsx (100%)
create mode 100644 src/components/Input/Input.tsx
rename src/components/{FilterInput => Input}/index.ts (55%)
diff --git a/src/components/DashboardValidatorsTable/DashboardValidatorsTable.tsx b/src/components/DashboardValidatorsTable/DashboardValidatorsTable.tsx
index fca3c35..bd5cdc5 100644
--- a/src/components/DashboardValidatorsTable/DashboardValidatorsTable.tsx
+++ b/src/components/DashboardValidatorsTable/DashboardValidatorsTable.tsx
@@ -24,7 +24,7 @@ import {
CustomTableRow,
} from "@/components/CustomTable";
import { ExplorerLink } from "@/components/ExplorerLink";
-import { FilterInput } from "@/components/FilterInput";
+import { FilterInput } from "@/components/Input";
import { PendingValidatorBalanceChange } from "@/components/PendingValidatorBalanceChange";
import { ValidatorMenu } from "@/components/ValidatorMenu";
import { ValidatorState } from "@/components/ValidatorState";
diff --git a/src/components/ExitValidatorTable/ExitValidatorTable.tsx b/src/components/ExitValidatorTable/ExitValidatorTable.tsx
index 1929a69..b06f94d 100644
--- a/src/components/ExitValidatorTable/ExitValidatorTable.tsx
+++ b/src/components/ExitValidatorTable/ExitValidatorTable.tsx
@@ -9,7 +9,7 @@ import {
TableBody,
TableSortLabel,
} from "@mui/material";
-import { useEffect, useMemo, useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
import {
CustomTableCell,
@@ -17,7 +17,7 @@ import {
CustomTableRow,
} from "@/components/CustomTable";
import { ExplorerLink } from "@/components/ExplorerLink";
-import { FilterInput } from "@/components/FilterInput";
+import { FilterInput } from "@/components/Input";
import { ValidatorsWrapper } from "@/components/ValidatorsWrapper";
import { useSelectedValidator } from "@/context/SelectedValidatorContext";
import { useValidators } from "@/hooks/useValidators";
diff --git a/src/components/FilterInput/FilterInput.tsx b/src/components/Input/FilterInput.tsx
similarity index 100%
rename from src/components/FilterInput/FilterInput.tsx
rename to src/components/Input/FilterInput.tsx
diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx
new file mode 100644
index 0000000..0ba588c
--- /dev/null
+++ b/src/components/Input/Input.tsx
@@ -0,0 +1,43 @@
+import { TextField } from "@mui/material";
+import { Dispatch, SetStateAction } from "react";
+
+interface InputParams {
+ placeholder: string;
+ value: T;
+ setValue: Dispatch>;
+}
+
+export const Input = ({ placeholder, value, setValue }: InputParams) => {
+ return (
+ setValue(e.target.value as T)}
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ color: "#ffffff",
+ backgroundColor: "#333743",
+ "& fieldset": {
+ borderColor: "#404040",
+ },
+ "&:hover fieldset": {
+ borderColor: "#606060",
+ },
+ "&.Mui-focused fieldset": {
+ borderColor: "#627EEA",
+ },
+ },
+ "& .MuiInputBase-input": {
+ color: "#ffffff",
+ padding: "8px 12px",
+ },
+ "& .MuiInputBase-input::placeholder": {
+ color: "#b3b3b3",
+ opacity: 1,
+ },
+ }}
+ />
+ );
+};
diff --git a/src/components/FilterInput/index.ts b/src/components/Input/index.ts
similarity index 55%
rename from src/components/FilterInput/index.ts
rename to src/components/Input/index.ts
index 9dc37ed..72ca428 100644
--- a/src/components/FilterInput/index.ts
+++ b/src/components/Input/index.ts
@@ -1 +1,2 @@
export * from "./FilterInput";
+export * from "./Input";
diff --git a/src/components/OfflineProgress/OfflineProgress.tsx b/src/components/OfflineProgress/OfflineProgress.tsx
index 4ea9039..5490bf7 100644
--- a/src/components/OfflineProgress/OfflineProgress.tsx
+++ b/src/components/OfflineProgress/OfflineProgress.tsx
@@ -1,13 +1,8 @@
-import {
- Box,
- Button,
- CircularProgress,
- TextField,
- Typography,
-} from "@mui/material";
+import { Box, Button, CircularProgress, Typography } from "@mui/material";
import { useEffect, useMemo, useState } from "react";
import { CopyToClipboard } from "@/components/CopyToClipboard";
+import { Input } from "@/components/Input";
import { useOfflineTransaction } from "@/hooks/useOfflineTransaction";
import {
ProgressModalConfirming,
@@ -123,35 +118,10 @@ export const OfflineProgress = ({
Signed Transaction
- setSignedTx(e.target.value as `0x${string}`)}
- sx={{
- "& .MuiOutlinedInput-root": {
- color: "#ffffff",
- backgroundColor: "#333743",
- "& fieldset": {
- borderColor: "#404040",
- },
- "&:hover fieldset": {
- borderColor: "#606060",
- },
- "&.Mui-focused fieldset": {
- borderColor: "#627EEA",
- },
- },
- "& .MuiInputBase-input": {
- color: "#ffffff",
- padding: "8px 12px",
- },
- "& .MuiInputBase-input::placeholder": {
- color: "#b3b3b3",
- opacity: 1,
- },
- }}
+ setValue={setSignedTx}
/>
{!!txError && (
diff --git a/src/components/PartialWithdrawValidatorTable/PartialWithdrawValidatorTable.tsx b/src/components/PartialWithdrawValidatorTable/PartialWithdrawValidatorTable.tsx
index d58618a..e53a506 100644
--- a/src/components/PartialWithdrawValidatorTable/PartialWithdrawValidatorTable.tsx
+++ b/src/components/PartialWithdrawValidatorTable/PartialWithdrawValidatorTable.tsx
@@ -20,7 +20,7 @@ import {
} from "@/components/CustomTable";
import { CustomTextField } from "@/components/CustomTextField";
import { ExplorerLink } from "@/components/ExplorerLink";
-import { FilterInput } from "@/components/FilterInput";
+import { FilterInput } from "@/components/Input";
import { ValidatorsWrapper } from "@/components/ValidatorsWrapper";
import { useSelectedValidator } from "@/context/SelectedValidatorContext";
import { useValidators } from "@/hooks/useValidators";
diff --git a/src/components/TopUpValidatorTable/TopUpValidatorTable.tsx b/src/components/TopUpValidatorTable/TopUpValidatorTable.tsx
index 0373f61..89b59c4 100644
--- a/src/components/TopUpValidatorTable/TopUpValidatorTable.tsx
+++ b/src/components/TopUpValidatorTable/TopUpValidatorTable.tsx
@@ -22,7 +22,7 @@ import {
} from "@/components/CustomTable";
import { CustomTextField } from "@/components/CustomTextField";
import { ExplorerLink } from "@/components/ExplorerLink";
-import { FilterInput } from "@/components/FilterInput";
+import { FilterInput } from "@/components/Input";
import { ValidatorsWrapper } from "@/components/ValidatorsWrapper";
import { useSelectedValidator } from "@/context/SelectedValidatorContext";
import { useValidator } from "@/hooks/useValidator";
diff --git a/src/components/UpgradeValidatorsTable/UpgradeValidatorsTable.tsx b/src/components/UpgradeValidatorsTable/UpgradeValidatorsTable.tsx
index 1ee8d9e..e8a15ff 100644
--- a/src/components/UpgradeValidatorsTable/UpgradeValidatorsTable.tsx
+++ b/src/components/UpgradeValidatorsTable/UpgradeValidatorsTable.tsx
@@ -17,7 +17,7 @@ import {
CustomTableRow,
} from "@/components/CustomTable";
import { ExplorerLink } from "@/components/ExplorerLink";
-import { FilterInput } from "@/components/FilterInput";
+import { FilterInput } from "@/components/Input";
import { ValidatorsWrapper } from "@/components/ValidatorsWrapper";
import { useSelectedValidator } from "@/context/SelectedValidatorContext";
import { useValidator } from "@/hooks/useValidator";
diff --git a/src/modals/Exit/ExitConfirmModal.tsx b/src/modals/Exit/ExitConfirmModal.tsx
index eb14abd..b6e6c9e 100644
--- a/src/modals/Exit/ExitConfirmModal.tsx
+++ b/src/modals/Exit/ExitConfirmModal.tsx
@@ -7,7 +7,6 @@ import {
Table,
TableBody,
TableRow,
- TextField,
IconButton,
} from "@mui/material";
import BigNumber from "bignumber.js";
@@ -18,6 +17,7 @@ import {
CustomModalTable,
CustomModalTableCell,
} from "@/components/CustomTable";
+import { Input } from "@/components/Input";
import { QueueWarning } from "@/components/QueueWarning";
import { WarningAlert } from "@/components/WarningAlert";
import { BaseDialog } from "@/modals/BaseDialog";
@@ -200,35 +200,10 @@ export const ExitConfirmModal: React.FC = ({
>
Type 'Unstake Funds' to confirm.
- setConfirmationText(e.target.value)}
- sx={{
- "& .MuiOutlinedInput-root": {
- color: "#ffffff",
- backgroundColor: "#333743",
- "& fieldset": {
- borderColor: "#404040",
- },
- "&:hover fieldset": {
- borderColor: "#606060",
- },
- "&.Mui-focused fieldset": {
- borderColor: "#627EEA",
- },
- },
- "& .MuiInputBase-input": {
- color: "#ffffff",
- padding: "8px 12px",
- },
- "& .MuiInputBase-input::placeholder": {
- color: "#b3b3b3",
- opacity: 1,
- },
- }}
+ setValue={setConfirmationText}
/>
diff --git a/src/modals/OfflineConnect/OfflineConnectModal.tsx b/src/modals/OfflineConnect/OfflineConnectModal.tsx
index ca13781..e083692 100644
--- a/src/modals/OfflineConnect/OfflineConnectModal.tsx
+++ b/src/modals/OfflineConnect/OfflineConnectModal.tsx
@@ -1,9 +1,10 @@
import { Close } from "@mui/icons-material";
-import { Box, Button, IconButton, TextField, Typography } from "@mui/material";
+import { Box, Button, IconButton, Typography } from "@mui/material";
import { useEffect, useMemo, useState } from "react";
import { isAddress } from "viem";
-import { BaseDialog } from "../BaseDialog";
+import { Input } from "@/components/Input";
+import { BaseDialog } from "@/modals/BaseDialog";
interface OfflineConnectModalProps {
open: boolean;
@@ -65,36 +66,10 @@ export const OfflineConnectModal = ({
- setAddress(e.target.value)}
- sx={{
- "& .MuiOutlinedInput-root": {
- color: "#ffffff",
- backgroundColor: "#333743",
- "& fieldset": {
- borderColor: "#404040",
- },
- "&:hover fieldset": {
- borderColor: "#606060",
- },
- "&.Mui-focused fieldset": {
- borderColor: "#627EEA",
- },
- },
- "& .MuiInputBase-input": {
- color: "#ffffff",
- padding: "8px 12px",
- },
- "& .MuiInputBase-input::placeholder": {
- color: "#b3b3b3",
- opacity: 1,
- },
- }}
+ setValue={setAddress}
/>
diff --git a/src/modals/TargetValidatorSelectionModal/TargetValidatorSelectionModal.tsx b/src/modals/TargetValidatorSelectionModal/TargetValidatorSelectionModal.tsx
index 980401c..5f04136 100644
--- a/src/modals/TargetValidatorSelectionModal/TargetValidatorSelectionModal.tsx
+++ b/src/modals/TargetValidatorSelectionModal/TargetValidatorSelectionModal.tsx
@@ -4,7 +4,6 @@ import {
Box,
Typography,
Button,
- TextField,
IconButton,
Tooltip,
} from "@mui/material";
@@ -12,6 +11,7 @@ import clsx from "clsx";
import React, { useState, useMemo } from "react";
import { ExplorerLink } from "@/components/ExplorerLink";
+import { Input } from "@/components/Input";
import { useValidator } from "@/hooks/useValidator";
import { BaseDialog } from "@/modals/BaseDialog";
import { Validator, Credentials, ValidatorStatus } from "@/types/validator";
@@ -215,36 +215,10 @@ export const TargetValidatorSelectionModal: React.FC<
Search Validators
- setSearchQuery(e.target.value)}
- sx={{
- mb: 3,
- "& .MuiOutlinedInput-root": {
- color: "#ffffff",
- backgroundColor: "#333743",
- "& fieldset": {
- borderColor: "#404040",
- },
- "&:hover fieldset": {
- borderColor: "#606060",
- },
- "&.Mui-focused fieldset": {
- borderColor: "#627EEA",
- },
- },
- "& .MuiInputBase-input": {
- color: "#ffffff",
- },
- "& .MuiInputBase-input::placeholder": {
- color: "#b3b3b3",
- opacity: 1,
- },
- }}
+ setValue={setSearchQuery}
/>
diff --git a/src/pages/Consolidate/Consolidate.tsx b/src/pages/Consolidate/Consolidate.tsx
index 0c3e95f..3e136ac 100644
--- a/src/pages/Consolidate/Consolidate.tsx
+++ b/src/pages/Consolidate/Consolidate.tsx
@@ -3,7 +3,7 @@ import React, { useState, useMemo, useEffect } from "react";
import { useConnections } from "wagmi";
import { ConsolidationSourceValidatorsTable } from "@/components/ConsolidationSourceValidatorsTable";
-import { FilterInput } from "@/components/FilterInput";
+import { FilterInput } from "@/components/Input";
import { Meta } from "@/components/Meta";
import { TargetValidatorDetails } from "@/components/TargetValidatorDetails";
import { ValidatorsWrapper } from "@/components/ValidatorsWrapper";
From 738a3cb338e9979fc53781dd0fb94818ce5b82aa Mon Sep 17 00:00:00 2001
From: valefar-on-discord
<124839138+valefar-on-discord@users.noreply.github.com>
Date: Sat, 21 Feb 2026 08:08:32 -0600
Subject: [PATCH 5/6] Clean up styling
---
src/components/OfflineProgress/OfflineProgress.tsx | 2 +-
src/modals/OfflineConnect/OfflineConnectModal.tsx | 12 +-----------
2 files changed, 2 insertions(+), 12 deletions(-)
diff --git a/src/components/OfflineProgress/OfflineProgress.tsx b/src/components/OfflineProgress/OfflineProgress.tsx
index 5490bf7..951e43c 100644
--- a/src/components/OfflineProgress/OfflineProgress.tsx
+++ b/src/components/OfflineProgress/OfflineProgress.tsx
@@ -110,7 +110,7 @@ export const OfflineProgress = ({
waitingMessage="Waiting for signature"
/>
- {isConfirmed && txHash && }
+ {isConfirmed && }
>
) : (
<>
diff --git a/src/modals/OfflineConnect/OfflineConnectModal.tsx b/src/modals/OfflineConnect/OfflineConnectModal.tsx
index e083692..fd85366 100644
--- a/src/modals/OfflineConnect/OfflineConnectModal.tsx
+++ b/src/modals/OfflineConnect/OfflineConnectModal.tsx
@@ -28,17 +28,7 @@ export const OfflineConnectModal = ({
return (
onClose(undefined)}>
-
+
Connect to Offline Wallet
From 8c431cd42fa7ab5abd0405852959e351b22cdc8e Mon Sep 17 00:00:00 2001
From: valefar-on-discord
<124839138+valefar-on-discord@users.noreply.github.com>
Date: Sat, 21 Feb 2026 08:19:31 -0600
Subject: [PATCH 6/6] Show only compounding when partial withdrawing
---
.../PartialWithdrawValidatorTable.tsx | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/components/PartialWithdrawValidatorTable/PartialWithdrawValidatorTable.tsx b/src/components/PartialWithdrawValidatorTable/PartialWithdrawValidatorTable.tsx
index e53a506..132a6b7 100644
--- a/src/components/PartialWithdrawValidatorTable/PartialWithdrawValidatorTable.tsx
+++ b/src/components/PartialWithdrawValidatorTable/PartialWithdrawValidatorTable.tsx
@@ -24,7 +24,12 @@ import { FilterInput } from "@/components/Input";
import { ValidatorsWrapper } from "@/components/ValidatorsWrapper";
import { useSelectedValidator } from "@/context/SelectedValidatorContext";
import { useValidators } from "@/hooks/useValidators";
-import { Validator, ValidatorStatus, WithdrawalEntry } from "@/types";
+import {
+ Credentials,
+ Validator,
+ ValidatorStatus,
+ WithdrawalEntry,
+} from "@/types";
import { enforceGweiPrecision } from "@/utils/number";
interface PartialWithdrawValidatorTableParams {
@@ -74,6 +79,7 @@ export const PartialWithdrawValidatorTable = ({
return validators
.filter(
(validator) =>
+ validator.credentials === Credentials.compounding &&
validator.status === ValidatorStatus.active_ongoing &&
(validator.pubkey.toLowerCase().includes(searchQuery.toLowerCase()) ||
validator.index.toString().includes(searchQuery)),