Render a Data Table from Contract VIEW Methods

Iterate over contract addresses and fetch their data to render a data table. Learn this useful React hook pattern to implement this in your own dApp.

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.

Fetching the latest market ids

For Prompt Wars, this table is rendered in the /previous-rounds URL. It will load this page component:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/pages/previous-rounds.tsx
import { GetServerSidePropsContext, NextPage } from "next";
import { i18n, useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Head from "next/head";

import { DashboardLayout } from "layouts/dashboard-layout/DashboardLayout";
import { AccountId } from "providers/near/contracts/market/market.types";
import pulse from "providers/pulse";
import { PreviousRoundsContainer } from "app/prompt-wars/previous-rounds/PreviousRoundsContainer";
import { PromptWarsMarketFactory } from "providers/near/contracts/prompt-wars-market-factory/contract";

const Index: NextPage<{ marketId: AccountId; markets: Array<AccountId> }> = ({ marketId, markets }) => {
  const { t } = useTranslation("head");

  return (
    <DashboardLayout marketId={marketId}>
      <Head>
        <title>{t("head.og.title")}</title>
        <meta name="description" content={t("head.og.description")} />
        <meta property="og:title" content={t("head.og.title")} />
        <meta property="og:description" content={t("head.og.description")} />
        <meta property="og:url" content="https://app.pulsemarkets.org/" />
      </Head>

      <PreviousRoundsContainer markets={markets} />
    </DashboardLayout>
  );
};

export const getServerSideProps = async ({ locale }: GetServerSidePropsContext) => {
  await i18n?.reloadResources();

  const marketId = await pulse.promptWars.getLatestMarketId();
  const markets = await PromptWarsMarketFactory.get_markets_list();
  markets?.reverse();

  return {
    props: {
      ...(await serverSideTranslations(locale!, ["common", "head", "prompt-wars"])),
      marketId,
      markets,
    },
  };
};

export default Index;

Let's dive into pulse.promptWars.getLatestMarketId():

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/providers/pulse/prompt-wars/getLatestMarketId.ts
import { MarketFactoryContract } from "providers/near/contracts/market-factory";

export default async () => {
  const marketFactory = await MarketFactoryContract.loadFromGuestConnection();
  const marketsList = await marketFactory.get_markets_list();

  if (!marketsList) {
    throw new Error("ERR_FAILED_TO_FETCH_MARKETS");
  }

  const latestMarketId = marketsList.pop();

  return latestMarketId;
};

As you can see, it is creating a guest connection to get data from a Rust NEAR contract get_markets_list VIEW method.

Creating the game contracts

This contract is a games factory implementation that stores only the game IDs after their contract accounts are created:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/contracts/market-factory/src/contract.rs
use near_sdk::{
    collections::Vector, env, json_types::Base64VecU8, near_bindgen, serde_json, serde_json::Value,
    AccountId, Promise,
};
use std::default::Default;

use crate::consts::*;
use crate::ext_self;
use crate::storage::*;

impl Default for MarketFactory {
    fn default() -> Self {
        Self {
            markets: Vector::new(b"d".to_vec()),
        }
    }
}

#[near_bindgen]
impl MarketFactory {
    #[payable]
    pub fn create_market(&mut self, name: AccountId, args: Base64VecU8) -> Promise {
        let market_account_id: AccountId = format!("{}.{}", name, env::current_account_id())
            .parse()
            .unwrap();

        let mut init_args: Value = serde_json::from_slice(&args.0.as_slice()).unwrap();

        init_args["management"]["market_creator_account_id"] =
            Value::String(env::signer_account_id().to_string());

        let collateral_token_account_id: AccountId = init_args["collateral_token"]["id"]
            .as_str()
            .unwrap()
            .parse()
            .unwrap();

        // @TODO if this promise fails, the funds (attached_deposit) are not returned to the signer
        let create_market_promise = Promise::new(market_account_id.clone())
            .create_account()
            .deploy_contract(MARKET_CODE.to_vec())
            .transfer(env::attached_deposit() - STORAGE_DEPOSIT_BOND)
            .function_call(
                "new".to_string(),
                init_args.to_string().into_bytes(),
                0,
                GAS_FOR_CREATE_MARKET,
            );

        let create_market_callback = ext_self::ext(env::current_account_id())
            .with_attached_deposit(0)
            .with_static_gas(GAS_FOR_CREATE_MARKET_CALLBACK)
            .on_create_market_callback(market_account_id, collateral_token_account_id);

        create_market_promise.then(create_market_callback)
    }
}

Fetching the game contracts IDs

The VIEW methods are in the same contract suite, but in this file:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/contracts/market-factory/src/views.rs
use near_sdk::{near_bindgen, AccountId};

use crate::storage::*;

#[near_bindgen]
impl MarketFactory {
    pub fn get_markets_list(&self) -> Vec<AccountId> {
        self.markets.to_vec()
    }

    pub fn get_markets_count(&self) -> u64 {
        self.markets.len()
    }

    pub fn get_markets(&self, from_index: u64, limit: u64) -> Vec<AccountId> {
        let elements = &self.markets;

        (from_index..std::cmp::min(from_index + limit, elements.len()))
            .filter_map(|index| elements.get(index))
            .collect()
    }
}

This way, we are able to get an array of game IDs which we'll use to fetch their storage metadata.

Game metadata storage

Pay attention to pub struct MarketData and its type definitions. We should have VIEW and CHANGE methods to modify some of these properties and get them later:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/contracts/prompt-wars/src/storage.rs
use near_sdk::{
    borsh::{self, BorshDeserialize, BorshSerialize},
    collections::{LookupMap, Vector},
    near_bindgen,
    serde::{Deserialize, Serialize},
    AccountId, BorshStorageKey,
};
use shared::OutcomeId;

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

#[near_bindgen]
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Market {
    // Market metadata
    pub market: MarketData,
    // NEP141 token metadata
    pub collateral_token: CollateralToken,
    // MArket resolution metadata
    pub resolution: Resolution,
    // Market management account ids metadata
    pub management: Management,
    // Keeps track of Outcomes prices and balances
    pub outcome_tokens: LookupMap<AccountId, OutcomeToken>,
    // Keeps track of the outcome_ids that have bet on the market
    pub players: Vector<AccountId>,
    // Market fees metadata
    pub fees: Fees,
}

#[derive(Serialize, Deserialize)]
pub enum SetPriceOptions {
    Increase,
    Decrease,
}

#[derive(BorshSerialize, BorshDeserialize, Serialize)]
pub struct OutcomeToken {
    // the account id of the outcome_token
    pub outcome_id: OutcomeId,
    // the outcome value, in this case, the prompt submitted to the competition
    pub prompt: String,
    // the outcome value, in this case, the prompt submitted to the competition
    pub output_img_uri: Option<String>,
    // store the result from the image comparison: percentage_diff or pixel_difference
    pub result: Option<OutcomeTokenResult>,
    // total supply of this outcome_token
    pub total_supply: WrappedBalance,
}

#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)]
pub struct CollateralToken {
    pub id: AccountId,
    pub balance: WrappedBalance,
    pub decimals: u8,
    pub fee_balance: WrappedBalance,
}

#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)]
pub struct Resolution {
    // Time to free up the market
    pub window: Timestamp,
    // Time after the market ends and before the resolution window starts
    pub reveal_window: Timestamp,
    // When the market is resolved, set only by fn resolve
    pub resolved_at: Option<Timestamp>,
    // When the market is resolved, set only by fn resolve
    pub result: Option<ResolutionResult>,
}

#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)]
pub struct Management {
    // Gets sent fees when claiming window is open
    pub dao_account_id: AccountId,
    // Gets back the storage deposit upon self-destruction
    pub market_creator_account_id: AccountId,
    // Set at initialization, the market may be destroyed by the creator to claim the storage deposit
    pub self_destruct_window: Timestamp,
    // Set at initialization, determines when to close bets
    pub buy_sell_threshold: f32,
}

#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)]
pub struct Fees {
    // Price to charge when creating an outcome token
    pub price: WrappedBalance,
    // Decimal fee to charge upon a bet
    pub fee_ratio: WrappedBalance,
    // When fees got sent to the DAO
    pub claimed_at: Option<Timestamp>,
}

#[derive(BorshStorageKey, BorshSerialize)]
pub enum StorageKeys {
    OutcomeTokens,
    StakingFees,
    MarketCreatorFees,
}

#[derive(Serialize, Deserialize)]
pub struct CreateOutcomeTokenArgs {
    // the outcome value, in this case, the prompt submitted to the competition
    pub prompt: String,
}

#[derive(Serialize, Deserialize)]
pub enum Payload {
    CreateOutcomeTokenArgs(CreateOutcomeTokenArgs),
}

#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)]
pub struct Ix {
    pub address: [u8; 32],
}

Getting game metadata

We add VIEW methods to set these properties, for example get_market_data:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/contracts/prompt-wars/src/views.rs
use crate::math;
use near_sdk::{env, log, near_bindgen, AccountId};
use num_format::ToFormattedString;
use shared::OutcomeId;

use crate::{storage::*, FORMATTED_STRING_LOCALE};

#[near_bindgen]
impl Market {
    pub fn get_market_data(&self) -> MarketData {
        self.market.clone()
    }

    pub fn get_resolution_data(&self) -> Resolution {
        self.resolution.clone()
    }

    pub fn get_fee_data(&self) -> Fees {
        self.fees.clone()
    }

    pub fn get_management_data(&self) -> Management {
        self.management.clone()
    }

    pub fn get_collateral_token_metadata(&self) -> CollateralToken {
        self.collateral_token.clone()
    }

    pub fn get_outcome_token(&self, outcome_id: &OutcomeId) -> OutcomeToken {
        match self.outcome_tokens.get(outcome_id) {
            Some(token) => token,
            None => env::panic_str("ERR_INVALID_OUTCOME_ID"),
        }
    }

    pub fn get_outcome_ids(&self) -> Vec<AccountId> {
        self.players.to_vec()
    }

    pub fn get_block_timestamp(&self) -> Timestamp {
        env::block_timestamp().try_into().unwrap()
    }

    pub fn resolved_at(&self) -> Timestamp {
        match self.resolution.resolved_at {
            Some(timestamp) => timestamp,
            None => env::panic_str("ERR_RESOLVED_AT"),
        }
    }

    pub fn balance_of(&self, outcome_id: &OutcomeId) -> WrappedBalance {
        self.get_outcome_token(outcome_id).get_balance_of()
    }

    pub fn get_amount_mintable(&self, amount: WrappedBalance) -> (WrappedBalance, WrappedBalance) {
        let fee = self.calc_percentage(amount, self.get_fee_data().fee_ratio);
        let amount_mintable = amount - fee;

        (amount_mintable, fee)
    }

    // The player gets his deposit back, minus fees
    pub fn get_amount_payable_unresolved(
        &self,
        outcome_id: OutcomeId,
    ) -> (WrappedBalance, WrappedBalance) {
        let amount = self.balance_of(&outcome_id);

        // This balance is already minus fees
        let collateral_token_balance = self.collateral_token.balance;

        let outcome_token_balance = self.balance_of(&outcome_id);

        if amount > outcome_token_balance {
            env::panic_str("ERR_GET_AMOUNT_PAYABLE_UNRESOLVED_INVALID_AMOUNT");
        }

        log!(
            "get_amount_payable_unresolved - EXPIRED_UNRESOLVED -- selling: {}, collateral_token_balance: {}, amount_payable: {}",
            amount.to_formatted_string(&FORMATTED_STRING_LOCALE),
            collateral_token_balance.to_formatted_string(&FORMATTED_STRING_LOCALE),
            amount.to_formatted_string(&FORMATTED_STRING_LOCALE)
        );

        (amount, 0)
    }

    // Winner takes all! This method calculates only after resolution
    pub fn get_amount_payable_resolved(&self) -> (WrappedBalance, WrappedBalance) {
        // This balance is already minus fees
        let collateral_token_balance = self.collateral_token.balance;

        // In this game, winner takes all
        let amount_payable = collateral_token_balance;

        // 100% of the bag baby!
        let weight = 1;

        log!(
            "get_amount_payable - RESOLVED -- ct_balance: {}, weight: {}, amount_payable: {}",
            collateral_token_balance.to_formatted_string(&FORMATTED_STRING_LOCALE),
            weight.to_formatted_string(&FORMATTED_STRING_LOCALE),
            amount_payable.to_formatted_string(&FORMATTED_STRING_LOCALE)
        );

        (amount_payable, weight)
    }

    pub fn get_precision_decimals(&self) -> WrappedBalance {
        let precision = format!(
            "{:0<p$}",
            10,
            p = self.collateral_token.decimals as usize + 1
        );

        precision.parse().unwrap()
    }

    pub fn calc_percentage(&self, amount: WrappedBalance, bps: WrappedBalance) -> WrappedBalance {
        math::complex_div_u128(
            self.get_precision_decimals(),
            math::complex_mul_u128(self.get_precision_decimals(), amount, bps),
            math::complex_mul_u128(1, self.get_precision_decimals(), 100),
        )
    }
}

Setting immutable storage values at contract initialization

Some of the storage values are set when the contract is initialized, meant to NOT be modified later:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/contracts/prompt-wars/src/contract.rs#L42-L92
impl Market {
    #[init]
    pub fn new(
        market: MarketData,
        management: Management,
        collateral_token: CollateralToken,
    ) -> Self {
        if env::state_exists() {
            env::panic_str("ERR_ALREADY_INITIALIZED");
        }

        let starts_at: Timestamp = env::block_timestamp().try_into().unwrap();
        let ends_at: Timestamp = starts_at + EVENT_PERIOD_NANOS;
        let reveal_window = ends_at + STAGE_PERIOD_NANOS;
        let resolution_window = reveal_window + STAGE_PERIOD_NANOS;

        // 7 days
        let self_destruct_window = resolution_window + 604_800_000_000_000;

        Self {
            market: MarketData {
                starts_at,
                ends_at,
                ..market
            },
            outcome_tokens: LookupMap::new(StorageKeys::OutcomeTokens),
            players: Vector::new(b"p"),
            resolution: Resolution {
                window: resolution_window,
                reveal_window,
                resolved_at: None,
                result: None,
            },
            fees: Fees {
                price: CREATE_OUTCOME_TOKEN_PRICE,
                fee_ratio: FEE_RATIO,
                claimed_at: None,
            },
            collateral_token: CollateralToken {
                balance: 0,
                fee_balance: 0,
                // @TODO collateral_token_decimals should be set by a cross-contract call to ft_metadata
                ..collateral_token
            },
            management: Management {
                self_destruct_window,
                buy_sell_threshold: BUY_SELL_THRESHOLD,
                ..management
            },
        }
    }

Deploy the factory contract

Run sh build.sh to build the factory contract:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/contracts/market-factory/build.sh
#!/bin/bash
RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release
wasm-opt -Oz --signext-lowering ./target/wasm32-unknown-unknown/release/market_factory.wasm -o ./target/wasm32-unknown-unknown/release/market_factory.wasm
cp ./target/wasm32-unknown-unknown/release/market_factory.wasm res/

and then deploy it:

near deploy --wasmFile target/wasm32-unknown-unknown/release/market_factory.wasm --accountId $NEAR_PROMPT_WARS_FACTORY_ACCOUNT_ID

Creating game contracts

Now that there's an on-chain factory contract, we should be able to create new games on the server side:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/pages/api/prompt-wars/create.ts#L58
    await PromptWarsMarketFactory.createMarket(id, promptWarsMarketArgs);

Getting contract metadata from the client side

When a new game is created, the factory contract should return the game IDs with get_markets_list as we explained earlier. Once you can get a game ID, you should also be able to get its metadata:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/providers/near/contracts/prompt-wars/contract.ts#L206
  async get_market_data() {

Iterate over game IDs, fetch the data and render the table

Finally, we should be able to render a table component with the contract values.

Notice how the table row component is wrapped with a context controller:

{markets.map((marketId) => (
          <NearPromptWarsMarketContractContextController marketId={marketId} key={marketId}>
            <PreviousRoundsTableRow marketId={marketId} />
          </NearPromptWarsMarketContractContextController>
        ))}
https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/ui/pulse/prompt-wars/previous-rounds-table/PreviousRoundsTable.tsx
import clsx from "clsx";

import { NearPromptWarsMarketContractContextController } from "context/near/prompt-wars-market-contract/NearPromptWarsMarketContractContextController";
import { PreviousRoundsTableRow } from "../previous-rounds-table-row/PreviousRoundsTableRow";
import { Typography } from "ui/typography/Typography";

import styles from "./PreviousRoundsTable.module.scss";
import { PreviousRoundsTableProps } from "./PreviousRoundsTable.types";

export const PreviousRoundsTable: React.FC<PreviousRoundsTableProps> = ({ className, markets }) => (
  <div className={clsx(styles["previous-rounds-table__responsive"], className)}>
    <table className={clsx(styles["previous-rounds-table"], className)}>
      <thead>
        <tr>
          <th>
            <Typography.Description>Image</Typography.Description>
          </th>
          <th>
            <Typography.Description>Winner</Typography.Description>
          </th>
          <th>
            <Typography.Description>No. of Players</Typography.Description>
          </th>
          <th>
            <Typography.Description>Started at</Typography.Description>
          </th>
          <th>
            <Typography.Description>Ended at</Typography.Description>
          </th>
          <th>
            <Typography.Description>Status</Typography.Description>
          </th>
          <th>{null}</th>
        </tr>
      </thead>
      <tbody>
        {markets.map((marketId) => (
          <NearPromptWarsMarketContractContextController marketId={marketId} key={marketId}>
            <PreviousRoundsTableRow marketId={marketId} />
          </NearPromptWarsMarketContractContextController>
        ))}
      </tbody>
    </table>
  </div>
);

The Table Row

useNearPromptWarsMarketContractContext hook will help us to organize the VIEW methods implementation better and render the data in the td elements:

https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/ui/pulse/prompt-wars/previous-rounds-table-row/PreviousRoundsTableRow.tsx#L14
  const { fetchMarketContractValues, marketContractValues } = useNearPromptWarsMarketContractContext();

Getting game contract values for each row

Lastly, this is the implementation of a Promise.all call to get all the on-chain values needed for the table and other game info components:

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

Last updated