From 7a6447b404e3ec6005b474c53a3c071361f19e8d Mon Sep 17 00:00:00 2001 From: 0age <37939117+0age@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:47:13 -0800 Subject: [PATCH] propagate finalization thresholds throughout the FE correctly --- frontend/src/App.tsx | 126 ++++++++++-------- frontend/src/components/BalanceDisplay.tsx | 31 +---- frontend/src/components/DepositForm.tsx | 18 +++ .../src/components/FinalizationThreshold.tsx | 20 ++- frontend/src/components/HealthCheck.tsx | 104 ++++++++------- frontend/src/contexts/ChainConfigContext.tsx | 25 ++++ frontend/src/contexts/chain-config-context.ts | 10 ++ frontend/src/hooks/use-chain-config.ts | 8 ++ .../src/hooks/useFinalizationThreshold.ts | 52 -------- frontend/src/types/chain.ts | 7 + frontend/src/utils/formatting.ts | 25 ++++ 11 files changed, 235 insertions(+), 191 deletions(-) create mode 100644 frontend/src/contexts/ChainConfigContext.tsx create mode 100644 frontend/src/contexts/chain-config-context.ts create mode 100644 frontend/src/hooks/use-chain-config.ts delete mode 100644 frontend/src/hooks/useFinalizationThreshold.ts create mode 100644 frontend/src/types/chain.ts create mode 100644 frontend/src/utils/formatting.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2e82d9b..1a9212b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,6 +23,8 @@ import { BalanceDisplay } from './components/BalanceDisplay'; import HealthCheck from './components/HealthCheck'; import { DepositForm } from './components/DepositForm'; import { NotificationProvider } from './context/NotificationProvider'; +import { ChainConfigProvider } from './contexts/ChainConfigContext'; +import { ChainConfig } from './types/chain'; // Configure supported chains const projectId = 'YOUR_PROJECT_ID'; // Get from WalletConnect Cloud @@ -55,6 +57,7 @@ const customTheme = darkTheme({ function App() { const [sessionToken, setSessionToken] = useState(null); const [isHealthy, setIsHealthy] = useState(true); + const [chainConfig, setChainConfig] = useState(null); const queryClient = useMemo( () => @@ -74,71 +77,76 @@ function App() { -
-
-
-

- Sm - all - ocator - 🤏 -

-
+ -
-
-
- {/* Health Check Status */} -
- -
+
+
+
+ {/* Health Check Status */} +
+ +
- {/* Only show these components if the server is healthy */} - {isHealthy && ( - <> - {/* Deposit Form */} - {sessionToken && } + {/* Only show these components if the server is healthy */} + {isHealthy && ( + <> + {/* Deposit Form */} + {sessionToken && } - {/* Balance Display */} - {sessionToken && ( -
- -
- )} - - )} + {/* Balance Display */} + {sessionToken && ( +
+ +
+ )} + + )} +
-
-
-
+ + +
diff --git a/frontend/src/components/BalanceDisplay.tsx b/frontend/src/components/BalanceDisplay.tsx index 33b782f..2433aeb 100644 --- a/frontend/src/components/BalanceDisplay.tsx +++ b/frontend/src/components/BalanceDisplay.tsx @@ -2,7 +2,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useAccount, useChainId } from 'wagmi'; import { useBalances } from '../hooks/useBalances'; import { useResourceLocks } from '../hooks/useResourceLocks'; -import { useFinalizationThreshold } from '../hooks/useFinalizationThreshold'; import { formatUnits } from 'viem'; import { Transfer } from './Transfer'; import { InitiateForcedWithdrawalDialog } from './InitiateForcedWithdrawalDialog'; @@ -10,6 +9,7 @@ import { ForcedWithdrawalDialog } from './ForcedWithdrawalDialog'; import { useCompact } from '../hooks/useCompact'; import { useNotification } from '../hooks/useNotification'; import { FinalizationThreshold } from './FinalizationThreshold'; +import { formatTimeRemaining, formatResetPeriod } from '../utils/formatting'; interface BalanceDisplayProps { sessionToken: string | null; @@ -32,32 +32,6 @@ interface EthereumProvider { request: (args: { method: string; params: unknown[] }) => Promise; } -function formatTimeRemaining( - expiryTimestamp: number, - currentTime: number -): string { - const diff = expiryTimestamp - currentTime; - - if (diff <= 0) return 'Ready'; - - const days = Math.floor(diff / (24 * 60 * 60)); - const hours = Math.floor((diff % (24 * 60 * 60)) / (60 * 60)); - const minutes = Math.floor((diff % (60 * 60)) / 60); - const seconds = diff % 60; - - if (days > 0) return `${days}d ${hours}h ${minutes}m`; - if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`; - if (minutes > 0) return `${minutes}m ${seconds}s`; - return `${seconds}s`; -} - -export const formatResetPeriod = (seconds: number): string => { - if (seconds < 60) return `${seconds} seconds`; - if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`; - if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`; - return `${Math.floor(seconds / 86400)} days`; -}; - const chainNames: Record = { '1': 'Ethereum', '10': 'Optimism', @@ -446,7 +420,8 @@ export function BalanceDisplay({ {balance.resourceLock?.resetPeriod && balance.resourceLock.resetPeriod > 0 && ( - Reset Period: {formatResetPeriod(balance.resourceLock.resetPeriod)} + Reset Period:{' '} + {formatResetPeriod(balance.resourceLock.resetPeriod)} )} diff --git a/frontend/src/components/DepositForm.tsx b/frontend/src/components/DepositForm.tsx index ae08843..4f89d5c 100644 --- a/frontend/src/components/DepositForm.tsx +++ b/frontend/src/components/DepositForm.tsx @@ -5,6 +5,9 @@ import { useCompact } from '../hooks/useCompact'; import { useNotification } from '../hooks/useNotification'; import { useERC20 } from '../hooks/useERC20'; import { useAllocatorAPI } from '../hooks/useAllocatorAPI'; +import { useChainConfig } from '../hooks/use-chain-config'; +import { formatResetPeriod } from '../utils/formatting'; +import { ChainConfig } from '../types/chain'; // Chain name mapping const chainNames: Record = { @@ -18,6 +21,7 @@ export function DepositForm() { const { address, isConnected } = useAccount(); const { data: ethBalance } = useBalance({ address }); const chainId = useChainId(); + const { chainConfig } = useChainConfig(); const [amount, setAmount] = useState(''); const [tokenType, setTokenType] = useState('native'); const [tokenAddress, setTokenAddress] = useState(''); @@ -240,6 +244,20 @@ export function DepositForm() {

Deposit Ether or ERC20 tokens into a reusable resource lock.

+ {chainConfig && ( +

+ Deposits on {chainNames[chainId.toString()] || `Chain ${chainId}`}{' '} + will be considered finalized and available to allocate{' '} + {formatResetPeriod( + chainConfig.supportedChains.find( + (chain: ChainConfig['supportedChains'][0]) => + chain.chainId === chainId.toString() + )?.finalizationThresholdSeconds ?? + chainConfig.defaultFinalizationThresholdSeconds + )}{' '} + after a successful deposit transaction. +

+ )}
diff --git a/frontend/src/components/FinalizationThreshold.tsx b/frontend/src/components/FinalizationThreshold.tsx index 34d52d5..ceb8e88 100644 --- a/frontend/src/components/FinalizationThreshold.tsx +++ b/frontend/src/components/FinalizationThreshold.tsx @@ -1,18 +1,28 @@ -import { useFinalizationThreshold } from '../hooks/useFinalizationThreshold'; -import { formatResetPeriod } from './BalanceDisplay'; +import { useChainConfig } from '../hooks/use-chain-config'; +import { formatResetPeriod } from '../utils/formatting'; +import { ChainConfig } from '../types/chain'; interface FinalizationThresholdProps { chainId: number; } export function FinalizationThreshold({ chainId }: FinalizationThresholdProps) { - const { finalizationThreshold } = useFinalizationThreshold(chainId); + const { chainConfig } = useChainConfig(); - if (finalizationThreshold === null) return null; + if (!chainConfig) return null; + + const chainSpecific = chainConfig.supportedChains.find( + (chain: ChainConfig['supportedChains'][0]) => + chain.chainId === chainId.toString() + ); + + const threshold = + chainSpecific?.finalizationThresholdSeconds ?? + chainConfig.defaultFinalizationThresholdSeconds; return ( - Finalization: {formatResetPeriod(finalizationThreshold)} + Finalization: {formatResetPeriod(threshold)} ); } diff --git a/frontend/src/components/HealthCheck.tsx b/frontend/src/components/HealthCheck.tsx index 9259785..5ac9f3f 100644 --- a/frontend/src/components/HealthCheck.tsx +++ b/frontend/src/components/HealthCheck.tsx @@ -1,17 +1,24 @@ import React, { useEffect, useState } from 'react'; +import { ChainConfigProvider } from '../contexts/ChainConfigContext'; +import { ChainConfig } from '../types/chain'; interface HealthStatus { status: string; allocatorAddress: string; signingAddress: string; timestamp: string; + chainConfig: ChainConfig; } interface HealthCheckProps { onHealthStatusChange?: (isHealthy: boolean) => void; + onChainConfigUpdate?: (chainConfig: ChainConfig) => void; } -const HealthCheck: React.FC = ({ onHealthStatusChange }) => { +const HealthCheck: React.FC = ({ + onHealthStatusChange, + onChainConfigUpdate, +}) => { const [healthData, setHealthData] = useState(null); const [error, setError] = useState(null); @@ -24,6 +31,7 @@ const HealthCheck: React.FC = ({ onHealthStatusChange }) => { setHealthData(data); setError(null); onHealthStatusChange?.(data.status === 'healthy'); + onChainConfigUpdate?.(data.chainConfig); } catch (error) { console.error('Error fetching health status:', error); setError('Allocator server unavailable'); @@ -36,7 +44,7 @@ const HealthCheck: React.FC = ({ onHealthStatusChange }) => { // Cleanup interval on component unmount return () => clearInterval(intervalId); - }, [onHealthStatusChange]); + }, [onHealthStatusChange, onChainConfigUpdate]); if (error) { return ( @@ -50,15 +58,13 @@ const HealthCheck: React.FC = ({ onHealthStatusChange }) => { >
-
-

{error}

-
+

{error}

@@ -74,51 +80,55 @@ const HealthCheck: React.FC = ({ onHealthStatusChange }) => { } return ( -
- {/* Allocator Address and Status */} -
-
- Allocator: - - {healthData.allocatorAddress} - -
-
- Status: - - {healthData.status.charAt(0).toUpperCase() + - healthData.status.slice(1)} - + +
+ {/* Allocator Address and Status */} +
+
+ Allocator: + + {healthData.allocatorAddress} + +
+
+ Status: + + {healthData.status.charAt(0).toUpperCase() + + healthData.status.slice(1)} + +
-
- {/* Signer and Last Checked */} -
-
- Signer: - - {healthData.signingAddress} - -
-
- Last Checked: - - {new Date(healthData.timestamp).toLocaleTimeString(undefined, { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} - + {/* Signer and Last Checked */} +
+
+ Signer: + + {healthData.signingAddress} + +
+
+ Last Checked: + + {new Date(healthData.timestamp).toLocaleTimeString(undefined, { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} + +
-
+
); }; diff --git a/frontend/src/contexts/ChainConfigContext.tsx b/frontend/src/contexts/ChainConfigContext.tsx new file mode 100644 index 0000000..ca2daa1 --- /dev/null +++ b/frontend/src/contexts/ChainConfigContext.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { + ChainConfigContext, + ChainConfigContextType, +} from './chain-config-context'; + +interface ChainConfigProviderProps { + children: React.ReactNode; + value: ChainConfigContextType; +} + +export const ChainConfigProvider: React.FC = ({ + children, + value, +}) => { + return ( + + {children} + + ); +}; + +// Re-export the types and context for convenience +export type { ChainConfigContextType }; +export { ChainConfigContext }; diff --git a/frontend/src/contexts/chain-config-context.ts b/frontend/src/contexts/chain-config-context.ts new file mode 100644 index 0000000..b41c81d --- /dev/null +++ b/frontend/src/contexts/chain-config-context.ts @@ -0,0 +1,10 @@ +import { createContext } from 'react'; +import { ChainConfig } from '../types/chain'; + +export interface ChainConfigContextType { + chainConfig: ChainConfig | null; +} + +export const ChainConfigContext = createContext({ + chainConfig: null, +}); diff --git a/frontend/src/hooks/use-chain-config.ts b/frontend/src/hooks/use-chain-config.ts new file mode 100644 index 0000000..f8f4570 --- /dev/null +++ b/frontend/src/hooks/use-chain-config.ts @@ -0,0 +1,8 @@ +import { useContext } from 'react'; +import { + ChainConfigContext, + ChainConfigContextType, +} from '../contexts/chain-config-context'; + +export const useChainConfig = (): ChainConfigContextType => + useContext(ChainConfigContext); diff --git a/frontend/src/hooks/useFinalizationThreshold.ts b/frontend/src/hooks/useFinalizationThreshold.ts deleted file mode 100644 index 16ac060..0000000 --- a/frontend/src/hooks/useFinalizationThreshold.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useState, useEffect } from 'react'; - -interface ChainConfig { - defaultFinalizationThresholdSeconds: number; - supportedChains: Array<{ - chainId: string; - finalizationThresholdSeconds: number; - }>; -} - -export function useFinalizationThreshold(chainId: number) { - const [finalizationThreshold, setFinalizationThreshold] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchFinalizationThreshold = async () => { - try { - const response = await fetch('/health'); - if (!response.ok) { - throw new Error('Failed to fetch chain configuration'); - } - const data = await response.json(); - const chainConfig: ChainConfig = data.chainConfig; - - // Find the chain-specific threshold or use default - const chainSpecific = chainConfig.supportedChains.find( - chain => chain.chainId === chainId.toString() - ); - - setFinalizationThreshold( - chainSpecific?.finalizationThresholdSeconds ?? - chainConfig.defaultFinalizationThresholdSeconds - ); - setError(null); - } catch (err) { - setError( - err instanceof Error - ? err.message - : 'Failed to fetch finalization threshold' - ); - setFinalizationThreshold(null); - } finally { - setIsLoading(false); - } - }; - - fetchFinalizationThreshold(); - }, [chainId]); - - return { finalizationThreshold, isLoading, error }; -} diff --git a/frontend/src/types/chain.ts b/frontend/src/types/chain.ts new file mode 100644 index 0000000..075e448 --- /dev/null +++ b/frontend/src/types/chain.ts @@ -0,0 +1,7 @@ +export interface ChainConfig { + defaultFinalizationThresholdSeconds: number; + supportedChains: Array<{ + chainId: string; + finalizationThresholdSeconds: number; + }>; +} diff --git a/frontend/src/utils/formatting.ts b/frontend/src/utils/formatting.ts new file mode 100644 index 0000000..41c4676 --- /dev/null +++ b/frontend/src/utils/formatting.ts @@ -0,0 +1,25 @@ +export function formatTimeRemaining( + expiryTimestamp: number, + currentTime: number +): string { + const diff = expiryTimestamp - currentTime; + + if (diff <= 0) return 'Ready'; + + const days = Math.floor(diff / (24 * 60 * 60)); + const hours = Math.floor((diff % (24 * 60 * 60)) / (60 * 60)); + const minutes = Math.floor((diff % (60 * 60)) / 60); + const seconds = diff % 60; + + if (days > 0) return `${days}d ${hours}h ${minutes}m`; + if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`; + if (minutes > 0) return `${minutes}m ${seconds}s`; + return `${seconds}s`; +} + +export function formatResetPeriod(seconds: number): string { + if (seconds < 60) return `${seconds} seconds`; + if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`; + return `${Math.floor(seconds / 86400)} days`; +}