Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECO-2161] Update market page with new grace period dynamics #237

Merged
merged 5 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
};
};
Loading