Skip to content

Commit

Permalink
[ECO-2161] Update market page with new grace period dynamics (#237)
Browse files Browse the repository at this point in the history
Co-authored-by: Matt <[email protected]>
  • Loading branch information
CRBl69 and xbtmatt authored Oct 7, 2024
1 parent 54cc20f commit 4ba57a9
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
Expand All @@ -26,10 +31,22 @@ export const LiquidityButton = (props: GridProps) => {
</Link>
</Flex>
</StyledContentHeader>
) : (
) : canTrade ? (
<StyledContentHeader className="!p-0">
<AnimatedProgressBar geoblocked={props.geoblocked} data={props.data} />
</StyledContentHeader>
) : (
<StyledContentHeader>
<Flex width="100%" justifyContent="left">
<Text
textScale={isDesktop ? "pixelHeading3" : "pixelHeading4"}
color="lightGray"
textTransform="uppercase"
>
Grace period ends in {displayTimeLeft}
</Text>
</Flex>
</StyledContentHeader>
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,17 +27,20 @@ export const SwapButton = ({
setSubmit,
disabled,
geoblocked,
symbol,
}: {
inputAmount: bigint | number | string;
isSell: boolean;
marketAddress: AccountAddressString;
setSubmit: Dispatch<SetStateAction<(() => Promise<void>) | 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) {
Expand Down Expand Up @@ -80,10 +90,22 @@ export const SwapButton = ({
return (
<>
<ButtonWithConnectWalletFallback geoblocked={geoblocked}>
<Button disabled={disabled} onClick={handleClick} scale="lg">
{t("Swap")}
</Button>
<RewardsAnimation controls={controls} />
{canTrade ? (
<>
<Button disabled={disabled} onClick={handleClick} scale="lg">
{t("Swap")}
</Button>
<RewardsAnimation controls={controls} />
</>
) : (
<Popup className="max-w-[300px]" content={t(GRACE_PERIOD_MESSAGE)}>
<div>
<Button disabled={true} onClick={handleClick} scale="lg">
{t("Swap")}
</Button>
</div>
</Popup>
)}
</ButtonWithConnectWalletFallback>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ export default function SwapComponent({
// the user is connected.
disabled={!sufficientBalance && !isLoading && !!account}
geoblocked={geoblocked}
symbol={emojicoin}
/>
</Row>
</Column>
Expand Down
4 changes: 2 additions & 2 deletions src/typescript/frontend/src/context/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
<ThemeProvider theme={theme}>
<QueryClientProvider client={queryClient}>
Expand Down
115 changes: 115 additions & 0 deletions src/typescript/frontend/src/lib/hooks/queries/use-grace-period.ts
Original file line number Diff line number Diff line change
@@ -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<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);

// 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,
};
};

0 comments on commit 4ba57a9

Please sign in to comment.