From 4ba57a9fa38397cbb8780ebd4eba3b0b099f6585 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Mon, 7 Oct 2024 06:46:15 +0200 Subject: [PATCH] [ECO-2161] Update market page with new grace period dynamics (#237) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../trade-emojicoin/LiquidityButton.tsx | 19 ++- .../components/trade-emojicoin/SwapButton.tsx | 30 ++++- .../trade-emojicoin/SwapComponent.tsx | 1 + .../frontend/src/context/providers.tsx | 4 +- .../src/lib/hooks/queries/use-grace-period.ts | 115 ++++++++++++++++++ 5 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 src/typescript/frontend/src/lib/hooks/queries/use-grace-period.ts diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/LiquidityButton.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/LiquidityButton.tsx index cfc354834..120ce4ec5 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/LiquidityButton.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/LiquidityButton.tsx @@ -7,9 +7,14 @@ import { isInBondingCurve } from "utils/bonding-curve"; import { AnimatedProgressBar } from "./AnimatedProgressBar"; import Link from "next/link"; import { ROUTES } from "router/routes"; +import { useCanTradeMarket } from "lib/hooks/queries/use-grace-period"; +import { Text } from "components/text"; +import { useMatchBreakpoints } from "@hooks/index"; export const LiquidityButton = (props: GridProps) => { + const { isDesktop } = useMatchBreakpoints(); const { t } = translationFunction(); + const { canTrade, displayTimeLeft } = useCanTradeMarket(props.data.symbol); return ( <> @@ -26,10 +31,22 @@ export const LiquidityButton = (props: GridProps) => { - ) : ( + ) : canTrade ? ( + ) : ( + + + + Grace period ends in {displayTimeLeft} + + + )} ); diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx index 9c236794f..a9ff9d4d5 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx @@ -12,6 +12,13 @@ import { useAnimationControls } from "framer-motion"; import { RewardsAnimation } from "./RewardsAnimation"; import { toast } from "react-toastify"; import { CongratulationsToast } from "./CongratulationsToast"; +import { useCanTradeMarket } from "lib/hooks/queries/use-grace-period"; +import Popup from "components/popup"; + +const GRACE_PERIOD_MESSAGE = + "This market is in its grace period. During the grace period of a market, only the market " + + "creator can trade. The grace period ends 5 minutes after the market registration or after the first " + + "trade, whichever comes first."; export const SwapButton = ({ inputAmount, @@ -20,6 +27,7 @@ export const SwapButton = ({ setSubmit, disabled, geoblocked, + symbol, }: { inputAmount: bigint | number | string; isSell: boolean; @@ -27,10 +35,12 @@ export const SwapButton = ({ setSubmit: Dispatch Promise) | null>>; disabled?: boolean; geoblocked: boolean; + symbol: string; }) => { const { t } = translationFunction(); const { aptos, account, submit } = useAptos(); const controls = useAnimationControls(); + const { canTrade } = useCanTradeMarket(symbol); const handleClick = useCallback(async () => { if (!account) { @@ -80,10 +90,22 @@ export const SwapButton = ({ return ( <> - - + {canTrade ? ( + <> + + + + ) : ( + +
+ +
+
+ )}
); diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx index bff29f59a..65482e972 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx @@ -323,6 +323,7 @@ export default function SwapComponent({ // the user is connected. disabled={!sufficientBalance && !isLoading && !!account} geoblocked={geoblocked} + symbol={emojicoin} /> diff --git a/src/typescript/frontend/src/context/providers.tsx b/src/typescript/frontend/src/context/providers.tsx index 3ec590b4a..52bc737b1 100644 --- a/src/typescript/frontend/src/context/providers.tsx +++ b/src/typescript/frontend/src/context/providers.tsx @@ -31,6 +31,8 @@ import { isMobile, isTablet } from "react-device-detect"; enableMapSet(); +const queryClient = new QueryClient(); + const ThemedApp: React.FC<{ children: React.ReactNode; geoblocked: boolean }> = ({ children, geoblocked, @@ -43,8 +45,6 @@ const ThemedApp: React.FC<{ children: React.ReactNode; geoblocked: boolean }> = const wallets = useMemo(() => [new PontemWallet(), new RiseWallet(), new MartianWallet()], []); - const queryClient = new QueryClient(); - return ( diff --git a/src/typescript/frontend/src/lib/hooks/queries/use-grace-period.ts b/src/typescript/frontend/src/lib/hooks/queries/use-grace-period.ts new file mode 100644 index 000000000..344fb4dd8 --- /dev/null +++ b/src/typescript/frontend/src/lib/hooks/queries/use-grace-period.ts @@ -0,0 +1,115 @@ +import { getRegistrationGracePeriodFlag } from "@sdk/markets/utils"; +import { standardizeAddress } from "@sdk/utils/account-address"; +import { useQuery } from "@tanstack/react-query"; +import { useEventStore } from "context/event-store-context"; +import { useAptos } from "context/wallet-context/AptosContextProvider"; +import { useEffect, useMemo, useState } from "react"; + +// ------------------------------------------------------------------------------------------------- +// +// Utilities for calculating the number of seconds left. +// +// ------------------------------------------------------------------------------------------------- +const nowSeconds = () => Math.floor(new Date().getTime() / 1000); + +const calculateSecondsLeft = (marketRegistrationTime: bigint) => { + const registrationTime = marketRegistrationTime; + const asSeconds = Math.floor(Number(registrationTime / 1_000_000n)); + const plusFiveMinutes = asSeconds + 300; + return Math.max(plusFiveMinutes - nowSeconds(), 0); +}; + +const formattedTimeLeft = (secondsRemaining: number) => { + const remainder = secondsRemaining % 60; + const minutes = Math.floor(secondsRemaining / 60); + return `${minutes.toString().padStart(2, "0")}:${remainder.toString().padStart(2, "0")}` as const; +}; + +// ------------------------------------------------------------------------------------------------- +// +// Hook to force the component to re-render on an interval basis. +// +// ------------------------------------------------------------------------------------------------- +const useDisplayTimeLeft = (marketRegistrationTime?: bigint) => { + const [timeLeft, setTimeLeft] = useState>(); + + useEffect(() => { + const interval = setInterval(() => { + if (typeof marketRegistrationTime === "undefined") { + setTimeLeft(undefined); + return; + } + const secondsLeft = calculateSecondsLeft(marketRegistrationTime); + const formatted = formattedTimeLeft(secondsLeft); + setTimeLeft(formatted); + }, 100); + + return () => clearInterval(interval); + }, [marketRegistrationTime]); + + return timeLeft; +}; + +// ------------------------------------------------------------------------------------------------- +// +// `useQuery` hook that fetches the grace period status on an interval basis. +// +// ------------------------------------------------------------------------------------------------- +const useGracePeriod = (symbol: string, hasSwaps: boolean) => { + const { aptos } = useAptos(); + const [keepFetching, setKeepFetching] = useState(true); + + // Include the seconds left in the query result to trigger re-renders upon each fetch. + const query = useQuery({ + queryKey: ["grace-period", symbol], + refetchInterval: 2000, + refetchIntervalInBackground: true, + enabled: keepFetching, + queryFn: async () => getRegistrationGracePeriodFlag({ aptos, symbol }), + }); + + // Stop fetching once the market has clearly been registered. + useEffect(() => { + const notInGracePeriod = query.data?.gracePeriodOver || hasSwaps; + if (notInGracePeriod) { + setKeepFetching(false); + } + }, [query.data?.gracePeriodOver, hasSwaps]); + + return query; +}; + +// ------------------------------------------------------------------------------------------------- +// +// The actual hook to be used in a component to display the amount of seconds left. +// +// ------------------------------------------------------------------------------------------------- +export const useCanTradeMarket = (symbol: string) => { + const { account } = useAptos(); + const hasSwaps = useEventStore((s) => (s.markets.get(symbol)?.swapEvents.length ?? 0) > 0); + const { isLoading, data } = useGracePeriod(symbol, hasSwaps); + + const { canTrade, marketRegistrationTime } = useMemo(() => { + const notInGracePeriod = data?.gracePeriodOver; + const userAddress = account?.address && standardizeAddress(account.address); + // Assume the user is the market registrant while the query is fetching in order to prevent + // disallowing the actual registrant from trading while the query result is being fetched. + const userIsRegistrant = data?.flag?.marketRegistrant === userAddress; + return { + canTrade: isLoading || userIsRegistrant || notInGracePeriod || hasSwaps, + marketRegistrationTime: data?.flag?.marketRegistrationTime, + }; + }, [isLoading, data, account?.address, hasSwaps]); + + const displayTimeLeft = useDisplayTimeLeft(marketRegistrationTime); + + return typeof displayTimeLeft === "undefined" || canTrade + ? { + canTrade: true as const, + displayTimeLeft: undefined, + } + : { + canTrade: false as const, + displayTimeLeft, + }; +};