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.

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.

Fetching dates from a React hook

Let's examine where does Prompt Wars use dates from the on-chain storage, see fetchMarketContractValues:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/context/near/prompt-wars-market-contract/NearPromptWarsMarketContractContextController.tsx#L94-L120

          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(),

These values are then used in this component to render a Counter:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/ui/pulse/img-prompt-card/ImgPromptCard.tsx#L107
                    <Countdown date={market.ends_at} />

Setting the on-chain Timestamp storage

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.

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/contracts/prompt-wars/src/storage.rs#L10-L25
pub type Timestamp = i64;
pub type WrappedBalance = u128;
pub type OutcomeTokenResult = f32;
pub type ResolutionResult = OutcomeId;

#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)]
#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))]
#[serde(crate = "near_sdk::serde")]
pub struct MarketData {
    // The IPFS reference-image hash of the expected prompts
    pub image_uri: String,
    // Datetime nanos: the market is open
    pub starts_at: Timestamp,
    // Datetime nanos: the market is closed
    pub ends_at: Timestamp,
}

Comparing dates to determine time periods

Prompt Wars consists of 3 main time periods:

  1. The game is active when env::block_timestamp is between starts_at and ends_at

    1. By this time, users may submit prompts

  2. The game is in the REVEALING status when env::block_timestamp is greater than ends_at and less than resolution.window

    1. By this time, the game is comparing all prompts with the source image and setting the result of each player in its own OutcomeToken

  3. The game is in the RESOLUTION status when env::block_timestamp is between the resolution.window

    1. 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:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/contracts/prompt-wars/src/flags.rs
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()
    }
}

Automatic status checks

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:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/pages/api/prompt-wars/create.ts#L68-L94
    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);

Client-side interval status checks

A simple useEffect  will fetch and detect changes on the contract time flags and update the status accordingly:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/app/prompt-wars/PromptWars.tsx#L44-L52
  useEffect(() => {
    const interval = setInterval(() => {
      fetchMarketContractValues();
    }, 5000);

    return () => {
      clearInterval(interval);
    };
  }, [marketId]);

Last updated