Create Time State Machines with Realtime Block Timestamp Comparisons
This React component displays new status badges when a timestamp threshold is met. Useful for time-based games.
Last updated
This React component displays new status badges when a timestamp threshold is met. Useful for time-based games.
Last updated
Getting Started
Follow the Project Setup instructions to start the project in your local machine and try the recipes yourself. You can also clone or navigate the repo to get just what you need.
A few things to remember:
NEAR native block timestamps accuracy is in nanoseconds, that's 18 0's, 1000000000000000000
But Javascript dates only handle up to the microseconds, that's 12 0's, 1000000000000
NEAR native block timestamps are in UTC, or GMT-0
While your app Date libraries may print the date in the user's local time
NEAR native block timestamps return an u64
type
While the chrono
Rust crate returns i64
values
Knowing this will make your life easier when handling dates in smart-contracts. Let's begin.
Let's examine where does Prompt Wars use dates from the on-chain storage, see fetchMarketContractValues
:
These values are then used in this component to render a Counter
:
For example, for the Counter
component to render a countdown 5 minutes from the future, it uses ends_at
. Note that Timestamp
has an i64
(signed integer) as a type. While env::block_timestamp
returns an u64
(unsigned integer). The reason for this is that you want to prevent Rust overflow errors when doing time operations. A Rust overflow error happens when an unsigned integer breaks the -0 limit.
Prompt Wars consists of 3 main time periods:
The game is active when env::block_timestamp
is between starts_at
and ends_at
By this time, users may submit prompts
The game is in the REVEALING status when env::block_timestamp
is greater than ends_at
and less than resolution.window
By this time, the game is comparing all prompts with the source image and setting the result of each player in its own OutcomeToken
The game is in the RESOLUTION status when env::block_timestamp
is between the resolution.window
By this time, the game will set a winner by getting the result that's closest to 0
This time comparisons happen on-chain to prevent malicious manipulation and can be found here:
Lastly, in order for the UI to switch between statuses, a client-side interval checks for these flags in the UI side, but also a server-side cronjob will trigger smart-contract actions to reveal and resolve each game:
A simple useEffect
will fetch and detect changes on the contract time flags and update the status accordingly:
import React, { useState } from "react";
import { setTimeout } from "timers";
import { useRouter } from "next/router";
import { useToastContext } from "hooks/useToastContext/useToastContext";
import { Typography } from "ui/typography/Typography";
import { PromptWarsMarketContract } from "providers/near/contracts/prompt-wars/contract";
import {
GetOutcomeTokenArgs,
Prompt,
PromptWarsMarketContractStatus,
PromptWarsMarketContractValues,
} from "providers/near/contracts/prompt-wars/prompt-wars.types";
import { useWalletStateContext } from "context/wallet/state/useWalletStateContext";
import { FungibleTokenContract } from "providers/near/contracts/fungible-token/contract";
import currency from "providers/currency";
import { useRoutes } from "hooks/useRoutes/useRoutes";
import { ErrorCode } from "providers/near/error-codes";
import {
NearPromptWarsMarketContractContextContextActions,
NearPromptWarsMarketContractContextControllerProps,
} from "./NearPromptWarsMarketContractContextContext.types";
import { NearPromptWarsMarketContractContext } from "./NearPromptWarsMarketContractContext";
let marketContract: PromptWarsMarketContract;
export const NearPromptWarsMarketContractContextController = ({
children,
marketId,
}: NearPromptWarsMarketContractContextControllerProps) => {
const [marketContractValues, setMarketContractValues] = useState<PromptWarsMarketContractValues>();
const [actions, setActions] = useState<NearPromptWarsMarketContractContextContextActions>({
fetchMarketContractValues: {
isLoading: false,
},
ftTransferCall: {
success: false,
isLoading: false,
},
create: {
isLoading: false,
},
});
const routes = useRoutes();
const router = useRouter();
const toast = useToastContext();
const walletStateContext = useWalletStateContext();
const getMarketStatus = (values: PromptWarsMarketContractValues): PromptWarsMarketContractStatus => {
if (!values) {
return PromptWarsMarketContractStatus.LOADING;
}
if (values?.isOpen) {
return PromptWarsMarketContractStatus.OPEN;
}
if (values?.isOver && values.isResolved) {
return PromptWarsMarketContractStatus.RESOLVED;
}
if (values?.isOver && !values.isRevealWindowExpired) {
return PromptWarsMarketContractStatus.REVEALING;
}
if (values?.isOver && !values.isResolutionWindowExpired) {
return PromptWarsMarketContractStatus.RESOLVING;
}
if (values?.isOver && values.isExpiredUnresolved) {
return PromptWarsMarketContractStatus.UNRESOLVED;
}
return PromptWarsMarketContractStatus.CLOSED;
};
const fetchMarketContractValues = async () => {
setActions((prev) => ({
...prev,
fetchMarketContractValues: {
isLoading: true,
},
}));
try {
// Wait 1 second to allow flags to change
setTimeout(async () => {
try {
const contract = await PromptWarsMarketContract.loadFromGuestConnection(marketId);
const [
market,
resolution,
fees,
management,
collateralToken,
outcomeIds,
isResolved,
isOpen,
isOver,
isRevealWindowExpired,
isResolutionWindowExpired,
isExpiredUnresolved,
] = await Promise.all([
contract.get_market_data(),
contract.get_resolution_data(),
contract.get_fee_data(),
contract.get_management_data(),
contract.get_collateral_token_metadata(),
contract.get_outcome_ids(),
contract.is_resolved(),
contract.is_open(),
contract.is_over(),
contract.is_reveal_window_expired(),
contract.is_resolution_window_expired(),
contract.is_expired_unresolved(),
]);
const values: PromptWarsMarketContractValues = {
market,
resolution,
fees,
management,
collateralToken,
outcomeIds,
isResolved,
isOpen,
isOver,
isRevealWindowExpired,
isResolutionWindowExpired,
isExpiredUnresolved,
status: PromptWarsMarketContractStatus.LOADING,
};
const status = getMarketStatus(values);
values.status = status;
setMarketContractValues(values);
} catch (error) {
console.log(error);
}
}, 1000);
} catch {
toast.trigger({
variant: "error",
withTimeout: true,
title: "Failed to fetch market data",
children: <Typography.Text>Try refreshing the page, or check your internet connection.</Typography.Text>,
});
}
setActions((prev) => ({
...prev,
fetchMarketContractValues: {
isLoading: false,
},
}));
};
const assertWalletConnection = () => {
if (!walletStateContext.isConnected) {
toast.trigger({
variant: "info",
withTimeout: true,
title: "Wallet is not connected",
children: <Typography.Text>Check your internet connection, your NEAR balance and try again.</Typography.Text>,
});
throw new Error("ERR_USE_NEAR_MARKET_CONTRACT_INVALID_WALLET_CONNECTION");
}
};
const ftTransferCall = async (prompt: Prompt) => {
if (!marketContractValues) {
return;
}
try {
assertWalletConnection();
setActions((prev) => ({
...prev,
ftTransferCall: {
...prev.ftTransferCall,
isLoading: true,
},
}));
const amount = marketContractValues.fees.price.toString();
const msg = JSON.stringify({ CreateOutcomeTokenArgs: { prompt: JSON.stringify(prompt) } });
await FungibleTokenContract.ftTransferCall(
walletStateContext.context.wallet!,
marketContractValues.collateralToken.id!,
{
receiver_id: marketId,
amount,
msg,
},
);
setActions((prev) => ({
...prev,
ftTransferCall: {
...prev.ftTransferCall,
isLoading: false,
success: true,
},
}));
toast.trigger({
variant: "confirmation",
withTimeout: true,
title: "Your prompt was successfully submitted",
children: (
<Typography.Text>{`Transferred USDT ${currency.convert.toDecimalsPrecisionString(
amount,
marketContractValues.collateralToken.decimals!,
)} to ${marketId}`}</Typography.Text>
),
});
fetchMarketContractValues();
} catch (error) {
if ((error as Error).message === ErrorCode.ERR_FungibleTokenContract_ftTransferCall) {
toast.trigger({
variant: "error",
title: "Failed to make transfer call",
children: <Typography.Text>You already transferred to this match.</Typography.Text>,
});
} else {
toast.trigger({
variant: "error",
title: "Failed to make transfer call",
children: (
<Typography.Text>Check your internet connection, connect your wallet and try again.</Typography.Text>
),
});
}
setActions((prev) => ({
...prev,
ftTransferCall: {
...prev.ftTransferCall,
isLoading: false,
success: true,
},
}));
}
};
const sell = async () => {
try {
assertWalletConnection();
await PromptWarsMarketContract.sell(walletStateContext.context.wallet!, marketId);
toast.trigger({
variant: "confirmation",
withTimeout: false,
title: "Success",
children: <Typography.Text>Check your new wallet balance.</Typography.Text>,
});
} catch {
toast.trigger({
variant: "error",
withTimeout: true,
title: "Failed to call sell method",
children: (
<Typography.Text>Check your internet connection, your NEAR wallet connection and try again.</Typography.Text>
),
});
}
};
const getOutcomeToken = async (args: GetOutcomeTokenArgs) => {
try {
if (!marketContract) {
marketContract = await PromptWarsMarketContract.loadFromGuestConnection(marketId);
}
const outcomeToken = await marketContract.get_outcome_token(args);
return outcomeToken;
} catch (error) {
console.log(error);
}
return undefined;
};
const create = async () => {
setActions((prev) => ({
...prev,
create: {
isLoading: true,
},
}));
try {
const response = await fetch(routes.api.promptWars.create());
if (!response.ok) {
throw new Error("ERR_USE_NEAR_PROMPT_WARS_MARKET_CONTRACT_CREATE_FAILED");
}
router.push(routes.dashboard.promptWars.home());
} catch (error) {
console.log(error);
toast.trigger({
variant: "error",
withTimeout: false,
title: "Failed to create a new market",
children: <Typography.Text>The server must have run out of funds. Please try again later.</Typography.Text>,
});
}
setActions((prev) => ({
...prev,
create: {
isLoading: false,
},
}));
};
const props = {
fetchMarketContractValues,
marketContractValues,
ftTransferCall,
sell,
getOutcomeToken,
actions,
marketId,
create,
};
return (
<NearPromptWarsMarketContractContext.Provider value={props}>
{children}
</NearPromptWarsMarketContractContext.Provider>
);
};
import { v4 as uuidv4 } from "uuid";
import { NextApiRequest, NextApiResponse } from "next";
import logger from "providers/logger";
import { DeployPromptWarsMarketContractArgs } from "providers/near/contracts/prompt-wars-market-factory/prompt-wars-market-factory.types";
import pulse from "providers/pulse";
import { CollateralToken, Management, MarketData } from "providers/near/contracts/prompt-wars/prompt-wars.types";
import near from "providers/near";
import { PromptWarsMarketFactory } from "providers/near/contracts/prompt-wars-market-factory/contract";
import { PromptWarsMarketContract } from "providers/near/contracts/prompt-wars/contract";
import ipfs from "providers/ipfs";
import { routes } from "hooks/useRoutes/useRoutes";
export default async function Fn(_request: NextApiRequest, response: NextApiResponse) {
try {
const latestMarketId = await pulse.promptWars.getLatestMarketId();
if (await pulse.promptWars.isMarketActive(latestMarketId!)) {
throw new Error("ERR_LATEST_MARKET_IS_STILL_ACTIVE");
}
const image_uri = await ipfs.getFileAsIPFSUrl("https://source.unsplash.com/random/512x512");
const market: MarketData = {
image_uri,
// Set to 0, it will be set in the contract initialization
starts_at: 0,
// Set to 0, it will be set in the contract initialization
ends_at: 0,
};
const management: Management = {
dao_account_id: near.getConfig().marketDaoAccountId,
market_creator_account_id: near.getConfig().serverWalletId,
// Set to 0, it will be set in the contract initialization
self_destruct_window: 0,
// Set to 0, it will be set in the contract initialization
buy_sell_threshold: 0,
};
const { accountId, decimals } = pulse.getDefaultCollateralToken();
const collateral_token: CollateralToken = {
id: accountId,
decimals,
balance: 0,
fee_balance: 0,
};
const promptWarsMarketArgs: DeployPromptWarsMarketContractArgs = {
market,
management,
collateral_token,
};
const id = `pw-${uuidv4().slice(0, 4)}`;
await PromptWarsMarketFactory.createMarket(id, promptWarsMarketArgs);
logger.info({ promptWarsMarketArgs, id });
const marketId = `${id}.${near.getConfig().factoryWalletId}`;
const marketContract = await PromptWarsMarketContract.loadFromGuestConnection(marketId);
const marketData = await marketContract.get_market_data();
const resolution = await marketContract.get_resolution_data();
let ms = marketData.ends_at - marketData.starts_at;
const revealEndpoint = routes.api.promptWars.reveal();
logger.info(`setting timeout to call the reveal API endpoint ${revealEndpoint} for market ${marketId} in ${ms} ms`);
setTimeout(async () => {
try {
logger.info(`calling the reveal API endpoint ${revealEndpoint} for market ${marketId}`);
await fetch(revealEndpoint);
} catch (error) {
logger.error(error);
}
}, ms);
ms = resolution.reveal_window - marketData.starts_at;
const resolveEndpoint = routes.api.promptWars.resolve();
logger.info(
`setting timeout to call the resolution API endpoint ${resolveEndpoint} for market ${marketId} in ${ms} ms`,
);
setTimeout(async () => {
try {
logger.info(`calling resolution API endpoint ${resolveEndpoint} for market ${marketId}`);
await fetch(resolveEndpoint);
} catch (error) {
logger.error(error);
}
}, ms);
response.status(200).json({ promptWarsMarketArgs, id });
} catch (error) {
logger.error(error);
response.status(500).json({ error: (error as Error).message });
}
}
import clsx from "clsx";
import { useEffect, useState } from "react";
import { useTranslation } from "next-i18next";
import { MainPanel } from "ui/mainpanel/MainPanel";
import { PromptWarsLogo } from "ui/icons/PromptWarsLogo";
import { Grid } from "ui/grid/Grid";
import { Typography } from "ui/typography/Typography";
import { ImgPromptCard } from "ui/pulse/img-prompt-card/ImgPromptCard";
import { GenericLoader } from "ui/generic-loader/GenericLoader";
import { PromptInputCard } from "ui/pulse/prompt-input-card/PromptInputCard";
import { FaqsModal } from "ui/pulse/prompt-wars/faqs-modal/FaqsModal";
import { useNearPromptWarsMarketContractContext } from "context/near/prompt-wars-market-contract/useNearPromptWarsMarketContractContext";
import { useToastContext } from "hooks/useToastContext/useToastContext";
import { Prompt } from "providers/near/contracts/prompt-wars/prompt-wars.types";
import { ResultsModal } from "ui/pulse/prompt-wars/results-modal/ResultsModal";
import { ShareModal } from "ui/pulse/prompt-wars/share-modal/ShareModal";
import styles from "./PromptWars.module.scss";
import { PromptWarsProps } from "./PromptWars.types";
export const PromptWars: React.FC<PromptWarsProps> = ({ marketId, className }) => {
const [isShareModalVisible, displayShareModal] = useState(false);
const [isFAQsModalVisible, displayFAQsModal] = useState(false);
const [isResultsModalVisible, displayResultsModal] = useState(false);
const { marketContractValues, fetchMarketContractValues, ftTransferCall, sell, create, actions } =
useNearPromptWarsMarketContractContext();
const { t } = useTranslation(["prompt-wars"]);
const toast = useToastContext();
useEffect(() => {
fetchMarketContractValues();
}, [marketId]);
useEffect(() => {
if (actions.ftTransferCall.success) {
displayShareModal(true);
}
}, [actions.ftTransferCall.success]);
useEffect(() => {
const interval = setInterval(() => {
fetchMarketContractValues();
}, 5000);
return () => {
clearInterval(interval);
};
}, [marketId]);
if (!marketContractValues || actions.create.isLoading) {
// @TODO render PriceMarket skeleton template
return <GenericLoader />;
}
const onSubmit = async (prompt: Prompt) => {
if (marketContractValues.isOver) {
toast.trigger({
variant: "error",
title: t("promptWars.marketisover.title"),
children: <Typography.Text>{t("promptwars.marketisover.description")}</Typography.Text>,
});
return;
}
await ftTransferCall(prompt);
};
const onClaimDepositUnresolved = async () => {
await sell();
};
const onClickCloseShareModal = () => {
displayShareModal(false);
};
const onClickCloseFAQsModal = () => {
displayFAQsModal(false);
};
const onClickFAQsButton = () => {
displayFAQsModal(true);
};
const onClickCloseResultsModal = () => {
displayResultsModal(false);
};
const onClickSeeResults = () => {
displayResultsModal(true);
};
const onClickCreateNewGame = async () => {
await create();
};
return (
<>
<MainPanel.Container className={clsx(styles["prompt-wars"], className)}>
<Grid.Container>
<div className={styles["prompt-wars__title-row"]}>
<PromptWarsLogo className={styles["prompt-wars__logo"]} />
<div className={styles["prompt-wars__title-row--description"]}>
<Typography.Description flat>
{t("promptWars.description")}{" "}
<Typography.Anchor onClick={onClickFAQsButton} href="#">
{t("promptWars.faqs")}
</Typography.Anchor>
</Typography.Description>
</div>
</div>
<div className={styles["prompt-wars__game-row"]}>
<Grid.Row>
<Grid.Col lg={7} xs={12} className={styles["prompt-wars__game-row--col-left"]}>
<ImgPromptCard
marketId={marketId}
marketContractValues={marketContractValues}
datesElement={<></>}
onClaimDepositUnresolved={onClaimDepositUnresolved}
onClickSeeResults={onClickSeeResults}
onClickCreateNewGame={onClickCreateNewGame}
/>
</Grid.Col>
<Grid.Col lg={5} xs={12}>
<PromptInputCard
onSubmit={onSubmit}
onClickFAQsButton={onClickFAQsButton}
marketContractValues={marketContractValues}
/>
</Grid.Col>
</Grid.Row>
</div>
</Grid.Container>
</MainPanel.Container>
{isShareModalVisible && <ShareModal onClose={onClickCloseShareModal} />}
{isFAQsModalVisible && <FaqsModal onClose={onClickCloseFAQsModal} />}
{isResultsModalVisible && (
<ResultsModal onClose={onClickCloseResultsModal} marketContractValues={marketContractValues} />
)}
</>
);
};
use near_sdk::near_bindgen;
use crate::storage::*;
#[near_bindgen]
impl Market {
pub fn is_resolved(&self) -> bool {
let is_resolved_at_set = match self.resolution.resolved_at {
Some(_) => true,
None => false,
};
let is_resolution_result_set = match self.resolution.result {
Some(_) => true,
None => false,
};
is_resolved_at_set && is_resolution_result_set
}
pub fn is_open(&self) -> bool {
self.get_block_timestamp() <= self.market.ends_at
}
pub fn is_over(&self) -> bool {
self.get_block_timestamp() > self.market.ends_at
}
pub fn is_reveal_window_expired(&self) -> bool {
self.get_block_timestamp() > self.resolution.reveal_window
}
pub fn is_self_destruct_window_expired(&self) -> bool {
self.get_block_timestamp() > self.management.self_destruct_window
}
pub fn is_resolution_window_expired(&self) -> bool {
self.get_block_timestamp() > self.resolution.window
}
pub fn is_expired_unresolved(&self) -> bool {
self.is_resolution_window_expired() && !self.is_resolved()
}
}
import clsx from "clsx";
import Countdown from "react-countdown";
import { useTranslation } from "next-i18next";
import { Card } from "ui/card/Card";
import { Grid } from "ui/grid/Grid";
import { Typography } from "ui/typography/Typography";
import { Icon } from "ui/icon/Icon";
import near from "providers/near";
import currency from "providers/currency";
import { PromptWarsMarketContractStatus } from "providers/near/contracts/prompt-wars/prompt-wars.types";
import ipfs from "providers/ipfs";
import { useWalletStateContext } from "context/wallet/state/useWalletStateContext";
import { Button } from "ui/button/Button";
import { ImgPromptCardProps } from "./ImgPromptCard.types";
import styles from "./ImgPromptCard.module.scss";
export const ImgPromptCard: React.FC<ImgPromptCardProps> = ({
marketId,
marketContractValues,
className,
datesElement,
onClaimDepositUnresolved,
onClickSeeResults,
onClickCreateNewGame,
}) => {
const walletState = useWalletStateContext();
const { market, resolution, outcomeIds, collateralToken, status } = marketContractValues;
const { t } = useTranslation(["prompt-wars"]);
const getDatesElement = () => datesElement;
const getStatusElement = () => {
if (status === PromptWarsMarketContractStatus.REVEALING) {
return (
<>
<Typography.Text flat className={styles["img-prompt-card__status--text"]}>
{t(`promptWars.status.${status}`)} <span>(closest to 0 wins)</span>
</Typography.Text>
<Typography.MiniDescription onClick={onClickSeeResults}>
{t("promptWars.status.miniDescription.seeResults")}
</Typography.MiniDescription>
</>
);
}
if (status === PromptWarsMarketContractStatus.RESOLVING) {
return (
<>
<Typography.Text flat>{t(`promptWars.status.${status}`)}</Typography.Text>
<Typography.MiniDescription>
<Countdown date={resolution.window} />
</Typography.MiniDescription>
</>
);
}
if (status === PromptWarsMarketContractStatus.RESOLVED) {
return (
<>
<Typography.Text flat>
{t(`promptWars.status.${status}`)} ð
<br />
<span className={styles["img-prompt-card__status--winner"]}>{resolution?.result}</span>
</Typography.Text>
<Typography.MiniDescription onClick={onClickSeeResults}>See results</Typography.MiniDescription>
</>
);
}
if (status === PromptWarsMarketContractStatus.UNRESOLVED) {
return (
<>
<Typography.Text flat>{t(`promptWars.status.${status}`)}</Typography.Text>
<Typography.MiniDescription onClick={onClaimDepositUnresolved}>
{t("promptWars.status.miniDescription.claimBackDeposit")}
</Typography.MiniDescription>
</>
);
}
return <Typography.Text>{t(`promptWars.status.${status}`)}</Typography.Text>;
};
return (
<Card className={clsx(styles["img-prompt-card"], className)} withSpotlightEffect>
<Card.Content>
<Grid.Row>
<Grid.Col lg={7}>
<Card withSpotlightEffect className={styles["img-prompt-card__current-img-card"]}>
<Card.Content className={styles["img-prompt-card__current-img-card--box"]}>
<div className={styles["img-prompt-card__current-img-card--file"]}>
<img src={ipfs.asHttpsURL(market.image_uri)} alt="current" />
</div>
</Card.Content>
</Card>
</Grid.Col>
<Grid.Col lg={5}>
<div className={styles["img-prompt-card__right-column"]}>
<Card className={styles["img-prompt-card__countdown"]}>
<Card.Content className={styles["img-prompt-card__countdown--content"]}>
<Typography.Description>{t("promptWars.status.description.timeLeft")}</Typography.Description>
<Typography.Headline3 flat>
<Countdown date={market.ends_at} />
</Typography.Headline3>
{marketContractValues.isResolutionWindowExpired && (
<Button
variant="outlined"
color="success"
size="xs"
className={styles["img-prompt-card__countdown--button"]}
onClick={onClickCreateNewGame}
>
{t("promptWars.button.createNewGame")}
</Button>
)}
</Card.Content>
</Card>
<div className={styles["img-prompt-card__start-end-time"]}>
{getDatesElement()}
<Card className={styles["img-prompt-card__stats"]} withSpotlightEffect>
<Card.Content className={styles["img-prompt-card__stats--content"]}>
<Typography.Description>{t("promptWars.status.description.status")}</Typography.Description>
{getStatusElement()}
<Typography.Description>{t("promptWars.status.description.participants")}</Typography.Description>
<Typography.Text flat={outcomeIds.includes(walletState.address as string)}>
{outcomeIds.length}
</Typography.Text>
<Typography.MiniDescription>
{outcomeIds.includes(walletState.address as string)
? t("promptWars.status.description.youReIn")
: null}
</Typography.MiniDescription>
<Typography.Description>{t("promptWars.status.description.totalPriceBag")}</Typography.Description>
<Typography.Text flat>
{t("promptWars.description.usdt")}{" "}
{currency.convert.toDecimalsPrecisionString(collateralToken.balance, collateralToken.decimals)}
</Typography.Text>
</Card.Content>
</Card>
</div>
</div>
</Grid.Col>
</Grid.Row>
</Card.Content>
<Card.Actions>
<div className={styles["img-prompt-card__start-end-time--resolution"]}>
<Typography.Description flat>{t("promptWars.status.description.contract")}</Typography.Description>
<Typography.Anchor href={`${near.getConfig().explorerUrl}/accounts/${marketId}`} target="_blank">
{marketId} <Icon name="icon-launch" />
</Typography.Anchor>
</div>
</Card.Actions>
</Card>
);
};