diff --git a/packages/synapse-interface/components/Maintenance/Maintenance.tsx b/packages/synapse-interface/components/Maintenance/Maintenance.tsx index fbe90417fd..108d1b68c8 100644 --- a/packages/synapse-interface/components/Maintenance/Maintenance.tsx +++ b/packages/synapse-interface/components/Maintenance/Maintenance.tsx @@ -1,12 +1,11 @@ import { MaintenanceBanner } from './components/MaintenanceBanner' import { MaintenanceWarningMessage } from './components/MaintenanceWarningMessage' -import { useMaintenanceCountdownProgress } from './components/useMaintenanceCountdownProgress' +import { useEventCountdownProgressBar } from './components/EventCountdownProgressBar' import { useBridgeState } from '@/slices/bridge/hooks' import { useSwapState } from '@/slices/swap/hooks' -import pausedChains from '@/public/pauses/v1/paused-chains.json' -import pausedBridgeModules from '@/public/pauses/v1/paused-bridge-modules.json' +import { useMaintanceState } from '@/slices/maintenance/hooks' +import { isChainIncluded } from '@/utils/isChainIncluded' -/** Pause Chain Activity */ interface ChainPause { id: string pausedFromChains: number[] @@ -14,144 +13,193 @@ interface ChainPause { pauseBridge: boolean pauseSwap: boolean startTimePauseChain: Date - endTimePauseChain: Date | null // If null, pause indefinitely + endTimePauseChain: Date | null startTimeBanner: Date - endTimeBanner: Date | null // If null, pause indefinitely - inputWarningMessage: JSX.Element + endTimeBanner: Date | null + inputWarningMessage: string bannerMessage: JSX.Element - progressBarMessage: JSX.Element + progressBarMessage: string disableBanner: boolean disableWarning: boolean disableCountdown: boolean } -const PAUSED_CHAINS: ChainPause[] = pausedChains.map((pause) => { +const useMaintenanceData = () => { + const { pausedChainsData, pausedModulesData } = useMaintanceState() + + const pausedChainsList: ChainPause[] = pausedChainsData + ? pausedChainsData?.map((pause: ChainPause) => { + return { + ...pause, + startTimeBanner: new Date(pause.startTimeBanner), + endTimeBanner: pause.endTimeBanner + ? new Date(pause.endTimeBanner) + : null, + startTimePauseChain: new Date(pause.startTimePauseChain), + endTimePauseChain: pause.endTimePauseChain + ? new Date(pause.endTimePauseChain) + : null, + bannerMessage:

{pause.bannerMessage}

, + inputWarningMessage: pause.inputWarningMessage, + progressBarMessage: pause.progressBarMessage, + } + }) + : [] + + const pausedModulesList: BridgeModulePause[] = pausedModulesData + ? pausedModulesData?.map((route: BridgeModulePause) => { + if (!isValidBridgeModule(route.bridgeModuleName)) { + throw new Error(`Invalid module type: ${route.bridgeModuleName}`) + } + + return { + ...route, + bridgeModuleName: route.bridgeModuleName as + | 'SynapseBridge' + | 'SynapseRFQ' + | 'SynapseCCTP' + | 'ALL', + } + }) + : [] + return { - ...pause, - startTimePauseChain: new Date(pause.startTimePauseChain), - endTimePauseChain: pause.endTimePauseChain - ? new Date(pause.endTimePauseChain) - : null, - startTimeBanner: new Date(pause.startTimeBanner), - endTimeBanner: pause.endTimeBanner ? new Date(pause.endTimeBanner) : null, - inputWarningMessage:

{pause.inputWarningMessage}

, - bannerMessage:

{pause.bannerMessage}

, - progressBarMessage:

{pause.progressBarMessage}

, + pausedChainsList, + pausedModulesList, } -}) - -export const MaintenanceBanners = () => { - return ( - <> - {PAUSED_CHAINS.map((event) => { - return ( - - ) - })} - - ) } -export const MaintenanceWarningMessages = ({ - type, -}: { - type: 'Bridge' | 'Swap' -}) => { +export const MaintenanceBanners = () => { + const { pausedChainsList } = useMaintenanceData() const { fromChainId: bridgeFromChainId, toChainId: bridgeToChainId } = useBridgeState() const { swapChainId } = useSwapState() - if (type === 'Bridge') { - return ( - <> - {PAUSED_CHAINS.map((event) => { - return ( - - ) - })} - - ) - } else if (type === 'Swap') { + const activeBanner = pausedChainsList.find( + (pausedChain) => + isChainIncluded(pausedChain?.pausedFromChains, [bridgeFromChainId]) || + isChainIncluded(pausedChain?.pausedToChains, [bridgeToChainId]) || + isChainIncluded(pausedChain?.pausedFromChains, [swapChainId]) || + isChainIncluded(pausedChain?.pausedToChains, [swapChainId]) + ) + + if (activeBanner) { return ( - <> - {PAUSED_CHAINS.map((event) => { - return ( - - ) - })} - + ) - } else { - return null } } -/** - * Hook that maps through PAUSED_CHAINS to apply the single event countdown progress logic to each. - * @returns A list of objects containing maintenance status and components for each paused chain. - */ -export const useMaintenanceCountdownProgresses = ({ - type, -}: { - type: 'Bridge' | 'Swap' -}) => { +export const useMaintenance = () => { + const { pausedChainsList, pausedModulesList } = useMaintenanceData() const { fromChainId: bridgeFromChainId, toChainId: bridgeToChainId } = useBridgeState() const { swapChainId } = useSwapState() - if (type === 'Bridge') { - return PAUSED_CHAINS.map((event) => { - return useMaintenanceCountdownProgress({ - fromChainId: bridgeFromChainId, - toChainId: bridgeToChainId, - startDate: event.startTimePauseChain, - endDate: event.endTimePauseChain, - pausedFromChains: event.pausedFromChains, - pausedToChains: event.pausedToChains, - progressBarMessage: event.progressBarMessage, - disabled: event.disableCountdown || !event.pauseBridge, - }) - }) - } else if (type === 'Swap') { - return PAUSED_CHAINS.map((event) => { - return useMaintenanceCountdownProgress({ - fromChainId: swapChainId, - toChainId: null, - startDate: event.startTimePauseChain, - endDate: event.endTimePauseChain, - pausedFromChains: event.pausedFromChains, - pausedToChains: event.pausedToChains, - progressBarMessage: event.progressBarMessage, - disabled: event.disableCountdown || !event.pauseSwap, - }) - }) + const activeBridgePause = pausedChainsList + .filter( + (pausedChain) => + isChainIncluded(pausedChain?.pausedFromChains, [bridgeFromChainId]) || + isChainIncluded(pausedChain?.pausedToChains, [bridgeToChainId]) + ) + .reduce((furthestPauseChain, currentChain) => { + const furthestDate = furthestPauseChain?.endTimePauseChain ?? null + const currentDate = currentChain.endTimePauseChain ?? null + const furthestFutureDate = getFurthestFutureDate( + furthestDate, + currentDate + ) + + return furthestFutureDate === furthestDate + ? furthestPauseChain + : currentChain + }, null) + + const activeSwapPause = pausedChainsList.find( + (pausedChain) => + isChainIncluded(pausedChain?.pausedFromChains, [swapChainId]) || + isChainIncluded(pausedChain?.pausedToChains, [swapChainId]) + ) + + const { + isPending: isBridgePaused, + EventCountdownProgressBar: BridgeEventCountdownProgressBar, + } = useEventCountdownProgressBar( + activeBridgePause?.progressBarMessage, + activeBridgePause?.startTimePauseChain, + activeBridgePause?.endTimePauseChain, + activeBridgePause?.disableCountdown + ) + + const { + isPending: isSwapPaused, + EventCountdownProgressBar: SwapEventCountdownProgressBar, + } = useEventCountdownProgressBar( + activeSwapPause?.progressBarMessage, + activeSwapPause?.startTimePauseChain, + activeSwapPause?.endTimePauseChain, + activeSwapPause?.disableCountdown + ) + + const BridgeMaintenanceProgressBar = () => BridgeEventCountdownProgressBar + const SwapMaintenanceProgressBar = () => SwapEventCountdownProgressBar + + const BridgeMaintenanceWarningMessage = () => ( + + ) + + const SwapMaintenanceWarningMessage = () => ( + + ) + + return { + isBridgePaused, + isSwapPaused, + pausedChainsList, + pausedModulesList, + BridgeMaintenanceProgressBar, + BridgeMaintenanceWarningMessage, + SwapMaintenanceProgressBar, + SwapMaintenanceWarningMessage, } } +const getFurthestFutureDate = ( + date1: Date | null, + date2: Date | null +): Date | null => { + if (date1 === null && date2 === null) return null + if (date1 === null) return date2 + if (date2 === null) return date1 + return date1 > date2 ? date1 : date2 +} + /** Pause Bridge Modules */ interface BridgeModulePause { chainId?: number // Will pause for all chains if undefined @@ -170,20 +218,3 @@ export function getBridgeModuleNames(module) { } return [module.bridgeModuleName] } - -export const PAUSED_MODULES: BridgeModulePause[] = pausedBridgeModules.map( - (route) => { - if (!isValidBridgeModule(route.bridgeModuleName)) { - throw new Error(`Invalid module type: ${route.bridgeModuleName}`) - } - - return { - ...route, - bridgeModuleName: route.bridgeModuleName as - | 'SynapseBridge' - | 'SynapseRFQ' - | 'SynapseCCTP' - | 'ALL', - } - } -) diff --git a/packages/synapse-interface/components/Maintenance/components/EventCountdownProgressBar.tsx b/packages/synapse-interface/components/Maintenance/components/EventCountdownProgressBar.tsx index e37089caa1..da75597604 100644 --- a/packages/synapse-interface/components/Maintenance/components/EventCountdownProgressBar.tsx +++ b/packages/synapse-interface/components/Maintenance/components/EventCountdownProgressBar.tsx @@ -13,7 +13,8 @@ import { useIntervalTimer } from '@/utils/hooks/useIntervalTimer' export const useEventCountdownProgressBar = ( eventLabel: string, startDate: Date, - endDate: Date | null + endDate: Date | null, + hideProgress?: boolean ): { isPending: boolean isComplete: boolean @@ -42,7 +43,7 @@ export const useEventCountdownProgressBar = ( return { isPending, isComplete, - EventCountdownProgressBar: ( + EventCountdownProgressBar: !hideProgress && ( -
-
{eventLabel}
+
+
{eventLabel}
{isIndefinite ? null :
{timeRemaining} remaining
}
@@ -100,8 +101,27 @@ export const getCountdownTimeStatus = ( startDate: Date, endDate: Date | null ) => { - const currentDate = new Date() + if (!startDate && !endDate) { + return { + currentDate: null, + currentTimeInSeconds: null, + startTimeInSeconds: null, + endTimeInSeconds: null, + totalTimeInSeconds: null, + totalTimeElapsedInSeconds: null, + totalTimeRemainingInSeconds: null, + totalTimeRemainingInMinutes: null, + daysRemaining: null, + hoursRemaining: null, + minutesRemaining: null, + secondsRemaining: null, + isStarted: false, + isComplete: false, + isPending: false, + } + } + const currentDate = new Date() const currentTimeInSeconds = Math.floor(currentDate.getTime() / 1000) const startTimeInSeconds = Math.floor(startDate.getTime() / 1000) @@ -164,9 +184,7 @@ export const getCountdownTimeStatus = ( const calculateTimeUntilTarget = (targetDate: Date) => { const currentDate = new Date() - const timeDifference = targetDate.getTime() - currentDate.getTime() - const isComplete = timeDifference <= 0 const daysRemaining = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) diff --git a/packages/synapse-interface/components/Maintenance/components/useMaintenanceCountdownProgress.tsx b/packages/synapse-interface/components/Maintenance/components/useMaintenanceCountdownProgress.tsx deleted file mode 100644 index 94e883c917..0000000000 --- a/packages/synapse-interface/components/Maintenance/components/useMaintenanceCountdownProgress.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useEventCountdownProgressBar } from './EventCountdownProgressBar' -import { isChainIncluded } from '@/utils/isChainIncluded' - -export const useMaintenanceCountdownProgress = ({ - fromChainId, - toChainId, - startDate, - endDate, - pausedFromChains, - pausedToChains, - progressBarMessage, - disabled = false, -}: { - fromChainId: number - toChainId: number - startDate: Date - endDate: Date | null - pausedFromChains: number[] - pausedToChains: number[] - progressBarMessage: any - disabled?: boolean -}) => { - const isCurrentChain = - isChainIncluded([fromChainId], pausedFromChains) || - isChainIncluded([toChainId], pausedToChains) - - const { - isPending: isMaintenancePending, - EventCountdownProgressBar: MaintenanceCountdownProgressBar, - } = useEventCountdownProgressBar(progressBarMessage, startDate, endDate) - - return { - isMaintenancePending, - isCurrentChainDisabled: isCurrentChain && isMaintenancePending, - MaintenanceCountdownProgressBar: - isCurrentChain && !disabled ? MaintenanceCountdownProgressBar : null, - } -} diff --git a/packages/synapse-interface/components/Maintenance/functions/fetchJsonData.ts b/packages/synapse-interface/components/Maintenance/functions/fetchJsonData.ts new file mode 100644 index 0000000000..ec8c05ea86 --- /dev/null +++ b/packages/synapse-interface/components/Maintenance/functions/fetchJsonData.ts @@ -0,0 +1,7 @@ +export const fetchJSONData = async (url: string): Promise => { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return response.json() +} diff --git a/packages/synapse-interface/components/Maintenance/functions/getSynapsePauseData.ts b/packages/synapse-interface/components/Maintenance/functions/getSynapsePauseData.ts new file mode 100644 index 0000000000..da144f1c90 --- /dev/null +++ b/packages/synapse-interface/components/Maintenance/functions/getSynapsePauseData.ts @@ -0,0 +1,39 @@ +import { useAppDispatch } from '@/store/hooks' +import { + setPausedChainsData, + setPausedModulesData, +} from '@/slices/maintenance/reducer' +import { fetchJSONData } from './fetchJsonData' + +export const PAUSED_CHAINS_URL = + 'https://raw.githubusercontent.com/synapsecns/sanguine/test/maintenance/packages/synapse-interface/public/pauses/v1/paused-chains.json' +export const PAUSED_MODULES_URL = + 'https://raw.githubusercontent.com/synapsecns/sanguine/test/maintenance/packages/synapse-interface/public/pauses/v1/paused-bridge-modules.json' + +let isFetching = false + +export const getSynapsePauseData = () => { + const dispatch = useAppDispatch() + + const fetchAndStoreData = async () => { + if (isFetching) { + return + } + try { + isFetching = true + const pausedChainsData = await fetchJSONData(PAUSED_CHAINS_URL) + const pausedModulesData = await fetchJSONData(PAUSED_MODULES_URL) + + dispatch(setPausedChainsData(pausedChainsData)) + dispatch(setPausedModulesData(pausedModulesData)) + } catch (error) { + console.error('Failed to fetch paused chains/modules: ', error) + } finally { + setTimeout(() => { + isFetching = false + }, 1000) + } + } + + return fetchAndStoreData +} diff --git a/packages/synapse-interface/components/layouts/LandingPageWrapper/index.tsx b/packages/synapse-interface/components/layouts/LandingPageWrapper/index.tsx index 7c033f624c..2873bd781c 100644 --- a/packages/synapse-interface/components/layouts/LandingPageWrapper/index.tsx +++ b/packages/synapse-interface/components/layouts/LandingPageWrapper/index.tsx @@ -25,7 +25,10 @@ import { NAVIGATION } from '@/constants/routes' import { MoreButton } from './MoreButton' import { PageFooter } from './PageFooter' import { joinClassNames } from '@/utils/joinClassNames' -import { MaintenanceBanners } from '@/components/Maintenance/Maintenance' +import { + MaintenanceBanners, + useMaintenance, +} from '@/components/Maintenance/Maintenance' import { AnnouncementBanner } from '@/components/Maintenance/components/AnnouncementBanner' const wrapperClassName = joinClassNames({ diff --git a/packages/synapse-interface/contexts/MaintenanceProvider.tsx b/packages/synapse-interface/contexts/MaintenanceProvider.tsx new file mode 100644 index 0000000000..b377d1feca --- /dev/null +++ b/packages/synapse-interface/contexts/MaintenanceProvider.tsx @@ -0,0 +1,18 @@ +import { createContext } from 'react' +import { getSynapsePauseData } from '@/components/Maintenance/functions/getSynapsePauseData' +import { useIntervalTimer } from '@/utils/hooks/useIntervalTimer' + +const MaintenanceContext = createContext(null) + +export const MaintenanceProvider = ({ children }) => { + const time = useIntervalTimer(60000) + const fetchMaintenanceData = getSynapsePauseData() + + fetchMaintenanceData() + + return ( + + {children} + + ) +} diff --git a/packages/synapse-interface/pages/_app.tsx b/packages/synapse-interface/pages/_app.tsx index fd4f1d8c57..77f4959753 100644 --- a/packages/synapse-interface/pages/_app.tsx +++ b/packages/synapse-interface/pages/_app.tsx @@ -13,6 +13,7 @@ import setupLogRocketReact from 'logrocket-react' import { SegmentAnalyticsProvider } from '@/contexts/SegmentAnalyticsProvider' import { UserProvider } from '@/contexts/UserProvider' +import { MaintenanceProvider } from '@/contexts/MaintenanceProvider' import { BackgroundListenerProvider } from '@/contexts/BackgroundListenerProvider' import CustomToaster from '@/components/toast' import { SynapseProvider } from '@/utils/providers/SynapseProvider' @@ -51,10 +52,12 @@ function App({ Component, pageProps }: AppProps) { - - - - + + + + + + diff --git a/packages/synapse-interface/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx index 46befb6311..458a528ca7 100644 --- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx +++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx @@ -1,4 +1,5 @@ import toast from 'react-hot-toast' +import { isEmpty } from 'lodash' import { useEffect, useRef, useState } from 'react' import { commify } from '@ethersproject/units' import { Address, zeroAddress, isAddress } from 'viem' @@ -63,14 +64,8 @@ import { RootState } from '@/store/store' import { getTimeMinutesFromNow } from '@/utils/time' import { isTransactionReceiptError } from '@/utils/isTransactionReceiptError' import { isTransactionUserRejectedError } from '@/utils/isTransactionUserRejectedError' -import { - MaintenanceWarningMessages, - useMaintenanceCountdownProgresses, -} from '@/components/Maintenance/Maintenance' -import { - PAUSED_MODULES, - getBridgeModuleNames, -} from '@/components/Maintenance/Maintenance' +import { useMaintenance } from '@/components/Maintenance/Maintenance' +import { getBridgeModuleNames } from '@/components/Maintenance/Maintenance' import { wagmiConfig } from '@/wagmiConfig' import { useStaleQuoteUpdater } from '@/utils/hooks/useStaleQuoteUpdater' @@ -99,6 +94,14 @@ const StateManagedBridge = () => { (state: RootState) => state.bridgeDisplay ) + const { + isBridgePaused, + pausedChainsList, + pausedModulesList, + BridgeMaintenanceProgressBar, + BridgeMaintenanceWarningMessage, + } = useMaintenance() + const [isApproved, setIsApproved] = useState(false) const dispatch = useAppDispatch() @@ -165,9 +168,11 @@ const StateManagedBridge = () => { ) const pausedBridgeModules = new Set( - PAUSED_MODULES.filter((module) => - module.chainId ? module.chainId === fromChainId : true - ).flatMap(getBridgeModuleNames) + pausedModulesList + .filter((module) => + module.chainId ? module.chainId === fromChainId : true + ) + .flatMap(getBridgeModuleNames) ) const activeQuotes = allQuotes.filter( @@ -534,13 +539,6 @@ const StateManagedBridge = () => { } } - const maintenanceCountdownProgressInstances = - useMaintenanceCountdownProgresses({ type: 'Bridge' }) - - const isBridgePaused = maintenanceCountdownProgressInstances.some( - (instance) => instance.isCurrentChainDisabled - ) - return (
@@ -572,9 +570,7 @@ const StateManagedBridge = () => {
- {maintenanceCountdownProgressInstances.map((instance) => ( - <>{instance.MaintenanceCountdownProgressBar} - ))} + {showSettingsSlideOver && (
@@ -594,7 +590,7 @@ const StateManagedBridge = () => { /> - + { const { address } = useAccount() @@ -62,6 +59,14 @@ const StateManagedSwap = () => { const { swapChainId, swapFromToken, swapToToken, swapFromValue, swapQuote } = useSwapState() + const { + isSwapPaused, + pausedChainsList, + pausedModulesList, + SwapMaintenanceProgressBar, + SwapMaintenanceWarningMessage, + } = useMaintenance() + const [isApproved, setIsApproved] = useState(false) const dispatch = useAppDispatch() @@ -345,13 +350,6 @@ const StateManagedSwap = () => { } } - const maintenanceCountdownProgressInstances = - useMaintenanceCountdownProgresses({ type: 'Swap' }) - - const isSwapPaused = maintenanceCountdownProgressInstances.some( - (instance) => instance.isCurrentChainDisabled - ) - return (
@@ -360,10 +358,7 @@ const StateManagedSwap = () => {
- {maintenanceCountdownProgressInstances.map((instance) => ( - <>{instance.MaintenanceCountdownProgressBar} - ))} - + { @@ -372,7 +367,7 @@ const StateManagedSwap = () => { }} /> - + { + return useAppSelector((state) => state.maintenance) +} diff --git a/packages/synapse-interface/slices/maintenance/reducer.ts b/packages/synapse-interface/slices/maintenance/reducer.ts new file mode 100644 index 0000000000..da6829001b --- /dev/null +++ b/packages/synapse-interface/slices/maintenance/reducer.ts @@ -0,0 +1,29 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +export interface MaintenanceState { + pausedChainsData: any + pausedModulesData: any +} + +const initialState: MaintenanceState = { + pausedChainsData: null, + pausedModulesData: null, +} + +export const maintenanceSlice = createSlice({ + name: 'maintenance', + initialState, + reducers: { + setPausedChainsData: (state, action: PayloadAction) => { + state.pausedChainsData = action.payload + }, + setPausedModulesData: (state, action: PayloadAction) => { + state.pausedModulesData = action.payload + }, + }, +}) + +export const { setPausedChainsData, setPausedModulesData } = + maintenanceSlice.actions + +export default maintenanceSlice.reducer diff --git a/packages/synapse-interface/store/reducer.ts b/packages/synapse-interface/store/reducer.ts index 6c41870f92..a76bdde417 100644 --- a/packages/synapse-interface/store/reducer.ts +++ b/packages/synapse-interface/store/reducer.ts @@ -8,6 +8,7 @@ import bridge from '@/slices/bridge/reducer' import portfolio from '@/slices/portfolio/reducer' import swap from '@/slices/swap/reducer' import transactions from '@/slices/transactions/reducer' +import maintenance from '@/slices/maintenance/reducer' import bridgeDisplay from '@/slices/bridgeDisplaySlice' import poolData from '@/slices/poolDataSlice' import poolDeposit from '@/slices/poolDepositSlice' @@ -37,6 +38,7 @@ export const appReducer = combineReducers({ bridge, portfolio, swap, + maintenance, bridgeDisplay, poolData, poolDeposit, diff --git a/packages/synapse-interface/utils/isChainIncluded.tsx b/packages/synapse-interface/utils/isChainIncluded.tsx index 6aa54f3bd3..aa942d6d5f 100644 --- a/packages/synapse-interface/utils/isChainIncluded.tsx +++ b/packages/synapse-interface/utils/isChainIncluded.tsx @@ -7,5 +7,5 @@ */ export const isChainIncluded = (chainList: number[], hasChains: number[]) => { - return hasChains.some((chainId) => chainList.includes(chainId)) + return hasChains?.some((chainId) => chainList.includes(chainId)) }