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.

Fetching the latest market ids
For Prompt Wars, this table is rendered in the /previous-rounds
URL. It will load this page component:
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()
:
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:
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:
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:
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
:
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:
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:
#!/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:
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:
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>
))}
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:
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:
Last updated