Skip to content

Commit

Permalink
Change useEffect to useEvent to change on swaps but maintain a …
Browse files Browse the repository at this point in the history
…stable reference to `props.data.symbol` and `queryClient`

Move grace period message to the top of the file

Put the `canTrade` logic into a `useMemo`

Add `numSwaps` to the `SwapButton` props so it re-renders properly when the # of swaps changes ready

Clean up and consolidate hook logic, decouple the fetch logic from display interval logic

Disable `useQuery` hook from fetching once the market is not in grace period
  • Loading branch information
xbtmatt committed Oct 7, 2024
1 parent cbf9ed0 commit 1e1722c
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import MainInfo from "./components/main-info/MainInfo";
import { useReliableSubscribe } from "@hooks/use-reliable-subscribe";
import { type BrokerEvent } from "@/broker/types";
import { marketToLatestBars } from "@/store/event/candlestick-bars";
import { useQueryClient } from "@tanstack/react-query";

const EVENT_TYPES: BrokerEvent[] = ["Chat", "PeriodicState", "Swap"];

Expand All @@ -34,12 +33,6 @@ const ClientEmojicoinPage = (props: EmojicoinProps) => {

useReliableSubscribe({ eventTypes: EVENT_TYPES });

const queryClient = useQueryClient();
const swaps = useEventStore((s) => s.getMarket(props.data.symbolEmojis)?.swapEvents ?? []);
useEffect(() => {
queryClient.invalidateQueries({queryKey: ["grace-period", props.data.symbol]});
}, [swaps]);

return (
<Box pt="85px">
<TextCarousel />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,18 @@ import { isInBondingCurve } from "utils/bonding-curve";
import { AnimatedProgressBar } from "./AnimatedProgressBar";
import Link from "next/link";
import { ROUTES } from "router/routes";
import { useGracePeriod } from "lib/hooks/queries/use-grace-period";
import { useCanTradeMarket } from "lib/hooks/queries/use-grace-period";
import { Text } from "components/text";
import { useEffect, useState } from "react";
import { useMatchBreakpoints } from "@hooks/index";
import { useQueryClient } from "@tanstack/react-query";

const timeLeft = (seconds: number) => {
if (seconds <= 0) return "0 s";
const remainder = seconds % 60;
const minutes = Math.floor(seconds / 60);
if (remainder === 0) {
return `${minutes} min`;
}
if (minutes === 0) {
return `${remainder} s`;
}
return `${minutes} min and ${remainder} s`;
};

const getNow = () => Math.floor(new Date().getTime() / 1000);

export const LiquidityButton = (props: GridProps) => {
const { isDesktop } = useMatchBreakpoints();
const { t } = translationFunction();
const [now, setNow] = useState(getNow());
const queryClient = useQueryClient();
const { isLoading, data } = useGracePeriod(props.data.symbol);

const isInGracePeriod = isLoading ? false : !data!.gracePeriodOver;
const registrationTime = Number((data?.flag?.marketRegistrationTime ?? 0n) / 1000000n);

useEffect(() => {
const id = setInterval(() => {
const n = getNow();
setNow(n);
if (60 * 5 - (n - registrationTime) < 0) {
queryClient.invalidateQueries({queryKey: ["grace-period", props.data.symbol]});
}
}, 200);
return () => clearInterval(id);
});
const { canTrade, displayTimeLeft } = useCanTradeMarket(props.data.symbol);

return (
<>
{!isInBondingCurve(props.data.state.state) && !isInGracePeriod ? (
{!isInBondingCurve(props.data.state.state) ? (
<StyledContentHeader>
<Flex width="100%" justifyContent="center">
<Link
Expand All @@ -64,7 +31,7 @@ export const LiquidityButton = (props: GridProps) => {
</Link>
</Flex>
</StyledContentHeader>
) : !isInGracePeriod ? (
) : canTrade ? (
<StyledContentHeader className="!p-0">
<AnimatedProgressBar geoblocked={props.geoblocked} data={props.data} />
</StyledContentHeader>
Expand All @@ -75,7 +42,9 @@ export const LiquidityButton = (props: GridProps) => {
textScale={isDesktop ? "pixelHeading3" : "pixelHeading4"}
color="lightGray"
textTransform="uppercase"
>Grace period ({timeLeft(60 * 5 - (now - registrationTime))} left)</Text>
>
Grace period ends in {displayTimeLeft}
</Text>
</Flex>
</StyledContentHeader>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ import { useAnimationControls } from "framer-motion";
import { RewardsAnimation } from "./RewardsAnimation";
import { toast } from "react-toastify";
import { CongratulationsToast } from "./CongratulationsToast";
import { useGracePeriod } from "lib/hooks/queries/use-grace-period";
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,
isSell,
Expand All @@ -35,13 +40,7 @@ export const SwapButton = ({
const { t } = translationFunction();
const { aptos, account, submit } = useAptos();
const controls = useAnimationControls();
const { isLoading, data } = useGracePeriod(symbol);

const isInGracePeriod = isLoading ? false : !data!.gracePeriodOver;
// If data not loaded yet, use user address as registrant address in order to not prevent the user from trying to trade.
const registrantAddress = data?.flag?.marketRegistrant ?? account?.address;

const canTrade = !isInGracePeriod || registrantAddress === account?.address;
const { canTrade } = useCanTradeMarket(symbol);

const handleClick = useCallback(async () => {
if (!account) {
Expand Down Expand Up @@ -99,7 +98,7 @@ export const SwapButton = ({
<RewardsAnimation controls={controls} />
</>
) : (
<Popup className="max-w-[300px]" content="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, of after the first trade, whichever comes first.">
<Popup className="max-w-[300px]" content={t(GRACE_PERIOD_MESSAGE)}>
<div>
<Button disabled={true} onClick={handleClick} scale="lg">
{t("Swap")}
Expand Down
117 changes: 107 additions & 10 deletions src/typescript/frontend/src/lib/hooks/queries/use-grace-period.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,115 @@
import { getRegistrationGracePeriodFlag } from "@sdk/markets";
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";

export const useGracePeriod = (symbol: string) => {
// -------------------------------------------------------------------------------------------------
//
// 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<ReturnType<typeof formattedTimeLeft>>();

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);

return useQuery({
// Include the seconds left in the query result to trigger re-renders upon each fetch.
const query = useQuery({
queryKey: ["grace-period", symbol],
queryFn: () => {
return getRegistrationGracePeriodFlag({
aptos,
symbol,
});
},
staleTime: 2000,
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,
};
};

0 comments on commit 1e1722c

Please sign in to comment.