diff --git a/packages/synapse-interface/components/Maintenance/AnnouncementBanner.tsx b/packages/synapse-interface/components/Maintenance/AnnouncementBanner.tsx index 42dc33dc97..377bcfadcf 100644 --- a/packages/synapse-interface/components/Maintenance/AnnouncementBanner.tsx +++ b/packages/synapse-interface/components/Maintenance/AnnouncementBanner.tsx @@ -1,7 +1,10 @@ import { useState, useEffect } from 'react' +import { getCountdownTimeStatus } from './EventCountdownProgressBar' /** - * Reusable Annoucement Banner with custom Start/End Time + * Reusable automated Announcement Banner with custom Start/End Time + * Will automatically appear after Start time + * Will automatically disappear after End time * @param bannerId: store in $MMDDYYYY-$BANNER_NAME format (e.g 03132024-ETH-DENCUN) * @param bannerContents: contents to display in banner * @param startDate: start date to show banner @@ -18,15 +21,9 @@ export const AnnouncementBanner = ({ startDate: Date endDate: Date }) => { + const { isStarted, isComplete } = getCountdownTimeStatus(startDate, endDate) const [hasMounted, setHasMounted] = useState(false) - const [showBanner, setShowBanner] = useState(false) - - const currentDate = new Date() - - const isStarted = - Math.floor(currentDate.getTime()) - Math.floor(startDate.getTime()) >= 0 - const isComplete = - Math.floor(currentDate.getTime()) - Math.floor(endDate.getTime()) >= 0 + const [showBanner, setShowBanner] = useState(true) useEffect(() => { setHasMounted(true) @@ -54,11 +51,11 @@ export const AnnouncementBanner = ({ } }, [showBanner, hasMounted]) - if (!showBanner || !hasMounted || isComplete) return null + if (!showBanner || !hasMounted || !isStarted || isComplete) return null return (
{ - return ( - -
- Synapse Bridge is upgrading ahead of the Ethereum Dencun upgrade - (March 13, 13:55 UTC, 9:55 EST). -
-
Will be back online shortly ahead of the network upgrade.
-
- } - startDate={ETH_DENCUN_BANNER_START} - endDate={ETH_DENCUN_END_DATE} - /> - ) -} diff --git a/packages/synapse-interface/components/Maintenance/EventCountdownProgressBar.tsx b/packages/synapse-interface/components/Maintenance/EventCountdownProgressBar.tsx index b4f60eb5ba..c2e9a9c41a 100644 --- a/packages/synapse-interface/components/Maintenance/EventCountdownProgressBar.tsx +++ b/packages/synapse-interface/components/Maintenance/EventCountdownProgressBar.tsx @@ -1,4 +1,3 @@ -import { useMemo } from 'react' import { LinearAnimatedProgressBar } from './LinearAnimatedProgressBar' import { useIntervalTimer } from '@/utils/hooks/useIntervalTimer' @@ -12,17 +11,14 @@ export const useEventCountdownProgressBar = ( EventCountdownProgressBar: JSX.Element } => { useIntervalTimer(60000) - const currentDate = new Date() - const currentTimeInSeconds = currentDate.getTime() / 1000 - const startTimeInSeconds = Math.floor(startDate.getTime() / 1000) - const endTimeInSeconds = Math.floor(endDate.getTime() / 1000) - const timeRemainingInSeconds = endTimeInSeconds - currentTimeInSeconds - const timeRemainingInMinutes = Math.ceil(timeRemainingInSeconds / 60) + const { totalTimeRemainingInMinutes, hoursRemaining, isComplete, isPending } = + getCountdownTimeStatus(startDate, endDate) - const isStarted = currentTimeInSeconds >= startTimeInSeconds - const isComplete = timeRemainingInSeconds <= 0 - const isPending = isStarted && !isComplete + const timeRemaining: string = + totalTimeRemainingInMinutes > 90 + ? `${hoursRemaining}h` + : `${totalTimeRemainingInMinutes}min` let status: 'idle' | 'pending' | 'complete' @@ -40,10 +36,10 @@ export const useEventCountdownProgressBar = ( EventCountdownProgressBar: ( ), } @@ -51,16 +47,16 @@ export const useEventCountdownProgressBar = ( export const EventCountdownProgressBar = ({ eventLabel, - startTime, - endTime, - status, + startDate, + endDate, timeRemaining, + status, }: { eventLabel: string - startTime: number - endTime: number + startDate: Date + endDate: Date + timeRemaining: string status: 'idle' | 'pending' | 'complete' - timeRemaining: number }) => { if (status === 'pending') { return ( @@ -73,14 +69,13 @@ export const EventCountdownProgressBar = ({ >
{eventLabel}
-
{timeRemaining}m remaining
+
{timeRemaining} remaining
@@ -89,3 +84,69 @@ export const EventCountdownProgressBar = ({ return null } } + +export const getCountdownTimeStatus = (startDate: Date, endDate: Date) => { + const currentDate = new Date() + + const { daysRemaining, hoursRemaining, minutesRemaining, secondsRemaining } = + calculateTimeUntilTarget(endDate) + + const currentTimeInSeconds = Math.floor(currentDate.getTime() / 1000) + + const startTimeInSeconds = Math.floor(startDate.getTime() / 1000) + const endTimeInSeconds = Math.floor(endDate.getTime() / 1000) + const totalTimeInSeconds = endTimeInSeconds - startTimeInSeconds + + const totalTimeElapsedInSeconds = currentTimeInSeconds - startTimeInSeconds + const totalTimeRemainingInSeconds = endTimeInSeconds - currentTimeInSeconds + const totalTimeRemainingInMinutes = Math.ceil( + totalTimeRemainingInSeconds / 60 + ) + + const isStarted = currentTimeInSeconds >= startTimeInSeconds + const isComplete = totalTimeRemainingInSeconds <= 0 + const isPending = isStarted && !isComplete + + return { + currentDate, + currentTimeInSeconds, + startTimeInSeconds, + endTimeInSeconds, + totalTimeInSeconds, + totalTimeElapsedInSeconds, + totalTimeRemainingInSeconds, + totalTimeRemainingInMinutes, + daysRemaining, + hoursRemaining, + minutesRemaining, + secondsRemaining, + isStarted, + isComplete, + isPending, + } +} + +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)) + const hoursRemaining = Math.floor( + (timeDifference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) + ) + const minutesRemaining = Math.floor( + (timeDifference % (1000 * 60 * 60)) / (1000 * 60) + ) + const secondsRemaining = Math.floor((timeDifference % (1000 * 60)) / 1000) + + return { + daysRemaining, + hoursRemaining, + minutesRemaining, + secondsRemaining, + isComplete, + } +} diff --git a/packages/synapse-interface/components/Maintenance/Events/EcotoneForkUpgrade.tsx b/packages/synapse-interface/components/Maintenance/Events/EcotoneForkUpgrade.tsx new file mode 100644 index 0000000000..c2dd3dd355 --- /dev/null +++ b/packages/synapse-interface/components/Maintenance/Events/EcotoneForkUpgrade.tsx @@ -0,0 +1,93 @@ +import { AnnouncementBanner } from '../AnnouncementBanner' +import { WarningMessage } from '../../Warning' +import { useBridgeState } from '@/slices/bridge/hooks' +import { OPTIMISM, BASE } from '@/constants/chains/master' +import { + useEventCountdownProgressBar, + getCountdownTimeStatus, +} from '../EventCountdownProgressBar' +import { useIntervalTimer } from '@/utils/hooks/useIntervalTimer' + +/** + * Start: 25 min prior to Ecotone Fork Upgrade Time @ (March 14, 00:00 UTC) + * End: 25 min after start of Ecotone Fork Upgrade Time + */ +export const ECOTONE_FORK_BANNERS_START = new Date( + Date.UTC(2024, 2, 13, 23, 20, 0) +) +export const ECOTONE_FORK_START_DATE = new Date( + Date.UTC(2024, 2, 13, 23, 35, 0) +) +export const ECOTONE_FORK_END_DATE = new Date(Date.UTC(2024, 2, 14, 0, 25, 0)) + +export const EcotoneForkUpgradeBanner = () => { + const { isComplete } = getCountdownTimeStatus( + ECOTONE_FORK_BANNERS_START, + ECOTONE_FORK_END_DATE + ) + + useIntervalTimer(60000, isComplete) + + return ( + +
+ Optimism + Base Bridging will be paused 10 minutes ahead of Ecotone + (March 14, 00:00 UTC, 20:00 EST). +
+
Will be back online shortly following the network upgrade.
+ + } + startDate={ECOTONE_FORK_BANNERS_START} + endDate={ECOTONE_FORK_END_DATE} + /> + ) +} + +export const EcotoneForkWarningMessage = () => { + const { fromChainId, toChainId } = useBridgeState() + + const isChainOptimism = [fromChainId, toChainId].includes(OPTIMISM.id) + const isChainBase = [fromChainId, toChainId].includes(BASE.id) + + if (isChainOptimism || isChainBase) { + return ( + +

+ Optimism Chain and Base Chain bridging are paused until the + Ecotone Fork upgrade completes. +

+ + } + /> + ) + } else return null +} + +export const useEcotoneForkCountdownProgress = () => { + const { fromChainId, toChainId } = useBridgeState() + + const isChainOptimism = [fromChainId, toChainId].includes(OPTIMISM.id) + const isChainBase = [fromChainId, toChainId].includes(BASE.id) + + const { + isPending: isEcotoneForkUpgradePending, + EventCountdownProgressBar: EcotoneForkCountdownProgressBar, + } = useEventCountdownProgressBar( + 'Ecotone Fork upgrade in progress', + ECOTONE_FORK_START_DATE, + ECOTONE_FORK_END_DATE + ) + + return { + isEcotoneForkUpgradePending, + isCurrentChainDisabled: + (isChainOptimism || isChainBase) && isEcotoneForkUpgradePending, + EcotoneForkCountdownProgressBar: + isChainOptimism || isChainBase ? EcotoneForkCountdownProgressBar : null, + } +} diff --git a/packages/synapse-interface/components/Maintenance/Events/MetisUpgrade.tsx b/packages/synapse-interface/components/Maintenance/Events/MetisUpgrade.tsx new file mode 100644 index 0000000000..20272264af --- /dev/null +++ b/packages/synapse-interface/components/Maintenance/Events/MetisUpgrade.tsx @@ -0,0 +1,90 @@ +import { AnnouncementBanner } from '../AnnouncementBanner' +import { WarningMessage } from '../../Warning' +import { useBridgeState } from '@/slices/bridge/hooks' +import { METIS } from '@/constants/chains/master' +import { useEventCountdownProgressBar } from '../EventCountdownProgressBar' +import { useIntervalTimer } from '@/utils/hooks/useIntervalTimer' +import { getCountdownTimeStatus } from '../EventCountdownProgressBar' + +/** + * Start: 30 min prior to Metis Chain Downtime @ (March 14, 02:00 UTC) + * End: 12 hours after start of Metis Chain Downtime + */ +export const METIS_DOWNTIME_BANNERS_START = new Date( + Date.UTC(2024, 2, 14, 1, 30, 0) +) +export const METIS_DOWNTIME_START_DATE = new Date( + Date.UTC(2024, 2, 14, 1, 45, 0) +) +export const METIS_DOWNTIME_END_DATE = new Date( + Date.UTC(2024, 2, 14, 13, 30, 0) +) + +export const MetisDowntimeBanner = () => { + const { isComplete } = getCountdownTimeStatus( + METIS_DOWNTIME_BANNERS_START, + METIS_DOWNTIME_END_DATE + ) + + useIntervalTimer(60000, isComplete) + + return ( + +
+ Metis Chain bridging will be paused 30 min ahead of the Metis + Upgrade (March 14, 02:00 UTC, 22:00 EST) +
+
and stay paused for ~12 hours.
+ + } + startDate={METIS_DOWNTIME_BANNERS_START} + endDate={METIS_DOWNTIME_END_DATE} + /> + ) +} + +export const MetisDowntimeWarningMessage = () => { + const { fromChainId, toChainId } = useBridgeState() + + const isChainMetis = [fromChainId, toChainId].includes(METIS.id) + + if (isChainMetis) { + return ( + +

+ Metis Chain bridging is paused until the Metis upgrade completes. +

+ + } + /> + ) + } else return null +} + +export const useMetisDowntimeCountdownProgress = () => { + const { fromChainId, toChainId } = useBridgeState() + + const isChainMetis = [fromChainId, toChainId].includes(METIS.id) + + const { + isPending: isMetisUpgradePending, + EventCountdownProgressBar: MetisUpgradeCountdownProgressBar, + } = useEventCountdownProgressBar( + 'Metis upgrade in progress', + METIS_DOWNTIME_START_DATE, + METIS_DOWNTIME_END_DATE + ) + + return { + isMetisUpgradePending, + isCurrentChainDisabled: isChainMetis && isMetisUpgradePending, + MetisUpgradeCountdownProgressBar: isChainMetis + ? MetisUpgradeCountdownProgressBar + : null, + } +} diff --git a/packages/synapse-interface/components/Maintenance/LinearAnimatedProgressBar.tsx b/packages/synapse-interface/components/Maintenance/LinearAnimatedProgressBar.tsx index d2c90fbe7b..955b932afc 100644 --- a/packages/synapse-interface/components/Maintenance/LinearAnimatedProgressBar.tsx +++ b/packages/synapse-interface/components/Maintenance/LinearAnimatedProgressBar.tsx @@ -1,36 +1,33 @@ import { memo } from 'react' -import { getTimeMinutesBeforeNow } from '@/utils/time' +import { getCountdownTimeStatus } from './EventCountdownProgressBar' /** * @param id unique identifier for progress bar instance * @param startTime start time in unix seconds * @param endTime end time in unix seconds - * @param status progress status */ export const LinearAnimatedProgressBar = memo( ({ id, - startTime, - endTime, - status, + startDate, + endDate, }: { id: string - startTime: number - endTime: number - status: 'idle' | 'pending' | 'complete' + startDate: Date + endDate: Date }) => { - const currentTime = Math.floor(getTimeMinutesBeforeNow(0)) - const elapsedTimeInSeconds = currentTime - startTime - const remainingTimeInSeconds = endTime - currentTime - const totalTimeInSeconds = endTime - startTime + const { + totalTimeInSeconds, + totalTimeElapsedInSeconds, + totalTimeRemainingInSeconds, + isComplete, + } = getCountdownTimeStatus(startDate, endDate) const percentElapsed = Math.floor( - (elapsedTimeInSeconds / totalTimeInSeconds) * 100 + (totalTimeElapsedInSeconds / totalTimeInSeconds) * 100 ) - const isComplete = status === 'complete' - - let duration = isComplete ? 0.5 : remainingTimeInSeconds + let duration = isComplete ? 0.5 : totalTimeRemainingInSeconds const synapsePurple = 'hsl(265deg 100% 75%)' const tailwindGreen400 = 'rgb(74 222 128)' @@ -40,7 +37,7 @@ export const LinearAnimatedProgressBar = memo( return ( { data-test-id="bridge-page" className="relative z-0 flex-1 h-full overflow-y-auto focus:outline-none" > - + +
diff --git a/packages/synapse-interface/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx index f23db2701e..9cb71d6685 100644 --- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx +++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx @@ -87,10 +87,21 @@ import { import { isTransactionReceiptError } from '@/utils/isTransactionReceiptError' import { SwitchButton } from '@/components/buttons/SwitchButton' import { useEventCountdownProgressBar } from '@/components/Maintenance/EventCountdownProgressBar' + +import { + METIS_DOWNTIME_START_DATE, + METIS_DOWNTIME_END_DATE, + MetisDowntimeWarningMessage, + useMetisDowntimeCountdownProgress, +} from '@/components/Maintenance/Events/MetisUpgrade' import { - ETH_DENCUN_START_DATE, - ETH_DENCUN_END_DATE, -} from '@/components/Maintenance/EthDencunUpgrade' + ECOTONE_FORK_START_DATE, + ECOTONE_FORK_END_DATE, + EcotoneForkWarningMessage, + useEcotoneForkCountdownProgress, +} from '@/components/Maintenance/Events/EcotoneForkUpgrade' + +import { OPTIMISM, BASE, METIS } from '@/constants/chains/master' const StateManagedBridge = () => { const { address } = useAccount() @@ -523,14 +534,19 @@ const StateManagedBridge = () => { const springClass = '-mt-4 fixed z-50 w-full h-full bg-opacity-50 bg-[#343036]' + /* Remove after upgrades */ const { - isPending: isUpgradePending, - EventCountdownProgressBar: EthDencunEventCountdownProgressBar, - } = useEventCountdownProgressBar( - 'Dencun upgrade in progress', - ETH_DENCUN_START_DATE, - ETH_DENCUN_END_DATE - ) + isEcotoneForkUpgradePending, + isCurrentChainDisabled: isEcotoneUpgradeChainsDisabled, + EcotoneForkCountdownProgressBar, + } = useEcotoneForkCountdownProgress() + + const { + isMetisUpgradePending, + isCurrentChainDisabled: isMetisUpgradeChainDisabled, + MetisUpgradeCountdownProgressBar, + } = useMetisDowntimeCountdownProgress() + /* Remove after upgrades */ return (
@@ -569,73 +585,74 @@ const StateManagedBridge = () => { transition-all duration-100 transform rounded-md `} > - {EthDencunEventCountdownProgressBar} -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - { - dispatch(setFromChainId(toChainId)) - dispatch(setFromToken(toToken)) - dispatch(setToChainId(fromChainId)) - dispatch(setToToken(fromToken)) - }} + {EcotoneForkCountdownProgressBar} + {MetisUpgradeCountdownProgressBar} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + { + dispatch(setFromChainId(toChainId)) + dispatch(setFromToken(toToken)) + dispatch(setToChainId(fromChainId)) + dispatch(setToToken(fromToken)) + }} + /> + + + + {/* Remove after upgrades */} + {isEcotoneForkUpgradePending && } + {isMetisUpgradePending && } + {/* Remove after upgrades */} + + + + + {showDestinationAddress && ( + + )} +
+ - - - - - - {showDestinationAddress && ( - - )} -
- -
diff --git a/packages/synapse-interface/utils/hooks/useIntervalTimer.ts b/packages/synapse-interface/utils/hooks/useIntervalTimer.ts index c394bff315..c84a5d07e9 100644 --- a/packages/synapse-interface/utils/hooks/useIntervalTimer.ts +++ b/packages/synapse-interface/utils/hooks/useIntervalTimer.ts @@ -6,24 +6,30 @@ import { getTimeMinutesFromNow } from '@/utils/time' * Hook for setting an interval based timer * * @param intervalInMs number, in ms (1000ms = 1s) + * @param isDisabled boolean, determines if we update at intervals * returns current time in minutes, unix */ -export const useIntervalTimer = (intervalInMs: number) => { +export const useIntervalTimer = ( + intervalInMs: number, + isDisabled?: boolean +) => { const [currentTime, setCurrentTime] = useState( getTimeMinutesFromNow(0) ) - /** Update time at set intervals */ + /** Update time at set intervals if not disabled */ useEffect(() => { - const interval = setInterval(() => { - const newCurrentTime = getTimeMinutesFromNow(0) - setCurrentTime(newCurrentTime) - }, intervalInMs) + if (!isDisabled) { + const interval = setInterval(() => { + const newCurrentTime = getTimeMinutesFromNow(0) + setCurrentTime(newCurrentTime) + }, intervalInMs) - return () => { - clearInterval(interval) // Clear the interval when the component unmounts + return () => { + clearInterval(interval) // Clear the interval when the component unmounts + } } - }, []) + }, [isDisabled]) return currentTime }