Prompt Wars: NEAR Protocol Recipes
  • NEAR Protocol Recipes
    • NEAR Guest Wallet Widget
    • NEAR and NEP141 Balance Widget
    • Render a Data Table from Contract VIEW Methods
    • Link an Account to Image, Text and Time Metadata with On-Chain Storage
    • Create Time State Machines with Realtime Block Timestamp Comparisons
    • Handle NEP141 Transfer Calls and Track the Target Contract's Balance
    • Handle FunctionCall Transaction Errors
  • Project Setup
    • The User Interface
    • The Rust Contracts
Powered by GitBook
On this page
  • Fetching the latest market ids
  • Creating the game contracts
  • Fetching the game contracts IDs
  • Game metadata storage
  • Getting game metadata
  • Setting immutable storage values at contract initialization
  • Deploy the factory contract
  • Creating game contracts
  • Getting contract metadata from the client side
  • Iterate over game IDs, fetch the data and render the table
  • The Table Row
  • Getting game contract values for each row
  1. NEAR Protocol Recipes

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.

PreviousNEAR and NEP141 Balance WidgetNextLink an Account to Image, Text and Time Metadata with On-Chain Storage

Last updated 1 year ago

Getting Started

Follow the instructions to start the project in your local machine and try the recipes yourself. You can also 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:

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

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:

Fetching the game contracts IDs

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

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:

Getting game metadata

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

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:

Deploy the factory contract

Run sh build.sh to build the factory contract:

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:

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:

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

The Table Row

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

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:

Project Setup
clone or navigate the repo
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),
        )
    }
}
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],
}
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;
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)
    }
}
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()
    }
}
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;
};
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/
https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/contracts/prompt-wars/src/contract.rs#L42-L92
use near_sdk::collections::Vector;
use near_sdk::{
    collections::LookupMap, env, ext_contract, json_types::U128, log, near_bindgen, AccountId,
    Promise,
};
use num_format::ToFormattedString;
use shared::OutcomeId;
use std::default::Default;

use near_contract_standards::fungible_token::core::ext_ft_core;

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

#[ext_contract(ext_self)]
trait Callbacks {
    fn on_ft_transfer_callback(
        &mut self,
        amount_payable: WrappedBalance,
        outcome_id: OutcomeId,
    ) -> String;
    fn on_claim_fees_resolved_callback(
        &mut self,
        payee: AccountId,
        amount_paid: WrappedBalance,
    ) -> Option<Timestamp>;
    fn on_claim_balance_self_destruct_callback(
        &mut self,
        payee: AccountId,
        amount_payable: WrappedBalance,
    ) -> Option<Timestamp>;
}

#[ext_contract(ext_feed_parser)]
trait SwitchboardFeedParser {
    fn aggregator_read(&self, msg: String) -> Promise;
}

impl Default for Market {
    fn default() -> Self {
        env::panic_str("ERR_MARKET_NOT_INITIALIZED")
    }
}

#[near_bindgen]
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
            },
        }
    }

    #[private]
    #[payable]
    pub fn create_outcome_token(
        &mut self,
        sender_id: AccountId,
        amount: WrappedBalance,
        payload: CreateOutcomeTokenArgs,
    ) -> WrappedBalance {
        self.assert_is_open();
        self.assert_is_not_resolved();
        self.assert_price(amount);

        let outcome_id = sender_id;
        let prompt = payload.prompt;

        match self.outcome_tokens.get(&outcome_id) {
            Some(_token) => env::panic_str("ERR_CREATE_OUTCOME_TOKEN_outcome_id_EXIST"),
            None => {
                self.internal_create_outcome_token(outcome_id, prompt, amount);
            }
        }

        amount
    }

    #[payable]
    pub fn sell(&mut self) -> WrappedBalance {
        if self.is_expired_unresolved() {
            return self.internal_sell_unresolved();
        }

        self.assert_is_resolved();

        return self.internal_sell_resolved();
    }

    // During the resolution period, this method is called by the server (owner), setting the OutcomeTokenResult of each player
    pub fn reveal(
        &mut self,
        outcome_id: OutcomeId,
        result: OutcomeTokenResult,
        output_img_uri: String,
    ) {
        self.assert_only_owner();
        self.assert_is_reveal_window_open();
        self.assert_is_not_resolved();
        self.assert_is_valid_outcome(&outcome_id);
        self.assert_is_resolution_window_open();

        let mut outcome_token = self.outcome_tokens.get(&outcome_id).unwrap();
        outcome_token.set_result(result);
        outcome_token.set_output_img_uri(output_img_uri);

        self.outcome_tokens.insert(&outcome_id, &outcome_token);

        log!("reveal: outcome_id: {}, result: {}", outcome_id, result);
    }

    // Gets the players accounts,
    // gets their outcome token results if they revealed on time
    // sorts the results, closest to 0 is first, closest to 0 wins
    #[payable]
    pub fn resolve(&mut self) {
        self.assert_only_owner();
        self.assert_is_not_resolved();
        self.assert_is_resolution_window_open();

        if self.get_outcome_ids().is_empty() {
            self.resolution.resolved_at = Some(self.get_block_timestamp());
            log!("resolve, market ended with 0 participants");
            return;
        }

        let separator = "=".to_string();

        let mut results: Vector<String> = Vector::new(b"r");

        for player in self.get_outcome_ids().clone() {
            let outcome_token = self.outcome_tokens.get(&player).unwrap();

            if outcome_token.result.is_none() {
                log!(
                    "resolve — PLAYER_DID_NOT_REVEAL — outcome_id: {}, result: {}",
                    outcome_token.outcome_id,
                    outcome_token.result.unwrap_or(0.0)
                );
            } else {
                let result_str = outcome_token.result.unwrap().to_string();

                results.push(&(player.to_string() + &separator + &result_str));
            }
        }

        log!("resolve, unsorted results: {:?}", results.to_vec());

        let mut sort = results.to_vec();
        sort.sort_by(|a, b| {
            let result_a = a.split(&separator).last().unwrap();
            let result_b = b.split(&separator).last().unwrap();

            return result_a.partial_cmp(result_b).unwrap();
        });

        log!("resolve, sorted results: {:?}", sort);

        // @TODO what happens if 2 or more players have exactly the same result?
        let winner = &sort[0];
        let result = winner.split(&separator).next().unwrap();

        log!("resolve, result: {}", result);

        self.internal_set_resolution_result(AccountId::new_unchecked(result.to_string()));
    }

    pub fn claim_fees(&mut self) {
        self.assert_is_resolution_window_expired();
        self.assert_fees_not_claimed();

        let payee = self.management.dao_account_id.clone();

        let amount_payable = self.collateral_token.fee_balance;

        if amount_payable == 0 {
            self.fees.claimed_at = Some(self.get_block_timestamp());
            env::panic_str("ERR_CLAIM_FEES_AMOUNT_PAYABLE_IS_ZERO");
        }

        let ft_transfer_promise = ext_ft_core::ext(self.collateral_token.id.clone())
            .with_attached_deposit(FT_TRANSFER_BOND)
            .with_static_gas(GAS_FT_TRANSFER)
            .ft_transfer(payee.clone(), U128::from(amount_payable), None);

        let ft_transfer_callback_promise = ext_self::ext(env::current_account_id())
            .with_attached_deposit(0)
            .with_static_gas(GAS_FT_TRANSFER_CALLBACK)
            .on_claim_fees_resolved_callback(payee, amount_payable);

        ft_transfer_promise.then(ft_transfer_callback_promise);
    }

    pub fn self_destruct(&mut self) {
        self.assert_only_owner();
        self.assert_is_resolution_window_expired();

        if !self.is_self_destruct_window_expired() {
            env::panic_str("ERR_SELF_DESTRUCT_WINDOW_NOT_EXPIRED");
        }

        if self.get_outcome_ids().is_empty() {
            env::panic_str("ERR_NO_OUTCOME_IDS");
        }

        if !self.fees.claimed_at.is_some() || self.collateral_token.fee_balance > 0 {
            env::panic_str("ERR_SELF_DESTRUCT_FEES_UNCLAIMED");
        }

        let amount_payable = self.collateral_token.balance;

        if amount_payable == 0 {
            Promise::new(env::current_account_id())
                .delete_account(self.management.market_creator_account_id.clone());

            return;
        }

        let payee = self.management.dao_account_id.clone();

        let ft_transfer_promise = ext_ft_core::ext(self.collateral_token.id.clone())
            .with_attached_deposit(FT_TRANSFER_BOND)
            .with_static_gas(GAS_FT_TRANSFER)
            .ft_transfer(payee.clone(), U128::from(amount_payable), None);

        let ft_transfer_callback_promise = ext_self::ext(env::current_account_id())
            .with_attached_deposit(0)
            .with_static_gas(GAS_FT_TRANSFER_CALLBACK)
            .on_claim_balance_self_destruct_callback(payee, amount_payable);

        ft_transfer_promise.then(ft_transfer_callback_promise);
    }

    #[private]
    pub fn update_collateral_token_balance(&mut self, amount: WrappedBalance) -> WrappedBalance {
        self.collateral_token.balance = amount;

        log!(
            "update_collateral_token_balance: {}",
            amount.to_formatted_string(&FORMATTED_STRING_LOCALE)
        );

        self.collateral_token.balance
    }

    #[private]
    pub fn update_collateral_token_fee_balance(
        &mut self,
        amount: WrappedBalance,
    ) -> WrappedBalance {
        self.collateral_token.fee_balance += amount;

        log!(
            "update_collateral_token_fee_balance, in: {}, total: {}",
            amount.to_formatted_string(&FORMATTED_STRING_LOCALE),
            self.collateral_token
                .fee_balance
                .to_formatted_string(&FORMATTED_STRING_LOCALE)
        );

        self.collateral_token.fee_balance
    }
}

impl Market {
    // Mint a new token for the player
    fn internal_create_outcome_token(
        &mut self,
        outcome_id: OutcomeId,
        prompt: String,
        amount: WrappedBalance,
    ) -> WrappedBalance {
        let (amount_mintable, fee) = self.get_amount_mintable(amount);

        let outcome_token = OutcomeToken::new(&outcome_id, prompt, amount_mintable);

        self.players.push(&outcome_id);

        self.outcome_tokens.insert(&outcome_id, &outcome_token);

        self.update_collateral_token_balance(self.collateral_token.balance + amount_mintable);
        self.update_collateral_token_fee_balance(fee);

        self.outcome_tokens.insert(&outcome_id, &outcome_token);

        log!("CREATE_OUTCOME_TOKEN amount: {}, fee_ratio: {}, fee_result: {}, outcome_id: {}, sender_id: {}, ot_supply: {}, amount_mintable: {}, ct_balance: {}, fee_balance: {}",
            amount.to_formatted_string(&FORMATTED_STRING_LOCALE),
            self.fees.fee_ratio.to_formatted_string(&FORMATTED_STRING_LOCALE),
            fee.to_formatted_string(&FORMATTED_STRING_LOCALE),
            outcome_token.outcome_id,
            outcome_id,
            outcome_token.total_supply().to_formatted_string(&FORMATTED_STRING_LOCALE),
            amount_mintable.to_formatted_string(&FORMATTED_STRING_LOCALE),
            self.collateral_token.balance.to_formatted_string(&FORMATTED_STRING_LOCALE),
            self.collateral_token.fee_balance.to_formatted_string(&FORMATTED_STRING_LOCALE),
        );

        amount_mintable
    }

    fn internal_sell_unresolved(&mut self) -> WrappedBalance {
        let payee = env::signer_account_id();

        let (amount_payable, _weight) = self.get_amount_payable_unresolved(payee.clone());

        self.internal_transfer(payee, amount_payable)
    }

    fn internal_sell_resolved(&mut self) -> WrappedBalance {
        let payee = self.resolution.result.clone().unwrap();

        self.assert_is_winner(&payee);

        let (amount_payable, _weight) = self.get_amount_payable_resolved();

        self.internal_transfer(payee, amount_payable)
    }

    fn internal_transfer(
        &mut self,
        payee: AccountId,
        amount_payable: WrappedBalance,
    ) -> WrappedBalance {
        if amount_payable <= 0 {
            env::panic_str("ERR_CANT_SELL_A_LOSING_OUTCOME");
        }

        let outcome_token = self.get_outcome_token(&payee);

        log!(
            "TRANSFER amount: {},  account_id: {}, supply: {}, is_resolved: {}, ct_balance: {}, amount_payable: {}",
            amount_payable.to_formatted_string(&FORMATTED_STRING_LOCALE),
            payee,
            outcome_token.total_supply().to_formatted_string(&FORMATTED_STRING_LOCALE),
            self.is_resolved(),
            self.collateral_token.balance.to_formatted_string(&FORMATTED_STRING_LOCALE),
            amount_payable.to_formatted_string(&FORMATTED_STRING_LOCALE),
        );

        let ft_transfer_promise = ext_ft_core::ext(self.collateral_token.id.clone())
            .with_attached_deposit(FT_TRANSFER_BOND)
            .with_static_gas(GAS_FT_TRANSFER)
            .ft_transfer(payee.clone(), U128::from(amount_payable), None);

        let ft_transfer_callback_promise = ext_self::ext(env::current_account_id())
            .with_attached_deposit(0)
            .with_static_gas(GAS_FT_TRANSFER_CALLBACK)
            .on_ft_transfer_callback(amount_payable, outcome_token.outcome_id);

        ft_transfer_promise.then(ft_transfer_callback_promise);

        amount_payable
    }

    fn internal_set_resolution_result(&mut self, result: ResolutionResult) {
        self.resolution.result = Some(result);
        self.resolution.resolved_at = Some(self.get_block_timestamp());

        log!(
            "internal_set_resolution_result, result: {:?}",
            self.resolution.result.clone().unwrap()
        );
    }
}
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>
);
https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/ui/pulse/prompt-wars/previous-rounds-table-row/PreviousRoundsTableRow.tsx#L14
import clsx from "clsx";
import { useEffect } from "react";

import { Typography } from "ui/typography/Typography";
import { useNearPromptWarsMarketContractContext } from "context/near/prompt-wars-market-contract/useNearPromptWarsMarketContractContext";
import ipfs from "providers/ipfs";
import { useRoutes } from "hooks/useRoutes/useRoutes";
import date from "providers/date";

import styles from "./PreviousRoundsTableRow.module.scss";
import { PreviousRoundsTableRowProps } from "./PreviousRoundsTableRow.types";

export const PreviousRoundsTableRow: React.FC<PreviousRoundsTableRowProps> = ({ className, marketId }) => {
  const { fetchMarketContractValues, marketContractValues } = useNearPromptWarsMarketContractContext();

  const routes = useRoutes();

  useEffect(() => {
    fetchMarketContractValues();
  }, [marketId]);

  if (!marketContractValues) return null;

  return (
    <tr className={clsx(styles["previous-rounds-table-row"], className)}>
      <td>
        <img
          src={ipfs.asHttpsURL(marketContractValues?.market.image_uri)}
          className={clsx(styles["previous-rounds-table-row__thumbnail"])}
          alt="market thumbnail"
        />
      </td>
      <td>
        <Typography.Description flat>{marketContractValues?.resolution.result}</Typography.Description>
      </td>
      <td>
        <Typography.Description flat>{marketContractValues?.outcomeIds.length}</Typography.Description>
      </td>
      <td>
        <Typography.Description flat>
          {date.fromTimestampWithOffset(marketContractValues?.market.starts_at, 0)}
        </Typography.Description>
      </td>
      <td>
        <Typography.Description flat>
          {date.fromTimestampWithOffset(marketContractValues?.market.ends_at, 0)}
        </Typography.Description>
      </td>
      <td>
        {marketContractValues?.isResolved ? (
          <Typography.Description flat className={clsx(styles["previous-rounds-table-row__resolved"])}>
            resolved
          </Typography.Description>
        ) : (
          <Typography.Description flat>unresolved</Typography.Description>
        )}
      </td>
      <td>
        <Typography.Link
          className={clsx(styles["previous-rounds-table-row__link"])}
          href={routes.dashboard.promptWars.market({ marketId })}
        >
          See details
        </Typography.Link>
      </td>
    </tr>
  );
};
https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/providers/near/contracts/prompt-wars/contract.ts#L206
import { Contract } from "near-api-js";
import * as nearAPI from "near-api-js";
import { BN } from "bn.js";
import { FinalExecutionOutcome, Wallet } from "@near-wallet-selector/core";
import { TypedError } from "near-api-js/lib/providers";

import near from "providers/near";
import date from "providers/date";

import {
  AccountId,
  GetOutcomeTokenArgs,
  OutcomeId,
  OutcomeTokenResult,
  PromptWarsMarketContractMethods,
  PromptWarsMarketContractValues,
} from "./prompt-wars.types";
import { CHANGE_METHODS, VIEW_METHODS } from "./constants";

export class PromptWarsMarketContract {
  values: PromptWarsMarketContractValues | undefined;

  contract: Contract & PromptWarsMarketContractMethods;

  contractAddress: AccountId;

  constructor(contract: Contract & PromptWarsMarketContractMethods) {
    this.contract = contract;
    this.contractAddress = contract.contractId;
  }

  static async loadFromGuestConnection(contractAddress: AccountId) {
    try {
      const connection = await nearAPI.connect({
        keyStore: new nearAPI.keyStores.InMemoryKeyStore(),
        headers: {},
        ...near.getConfig(),
      });

      const account = await connection.account(near.getConfig().guestWalletId);
      const contractMethods = { viewMethods: VIEW_METHODS, changeMethods: CHANGE_METHODS };

      const contract = near.initContract<PromptWarsMarketContractMethods>(account, contractAddress, contractMethods);

      return new PromptWarsMarketContract(contract);
    } catch (error) {
      console.log(error);
      throw error;
    }
  }

  static async loadFromWalletConnection(
    connection: nearAPI.WalletConnection,
    contractAddress: string,
  ): Promise<[PromptWarsMarketContract, Contract & PromptWarsMarketContractMethods]> {
    const account = await connection.account();
    const contractMethods = { viewMethods: VIEW_METHODS, changeMethods: CHANGE_METHODS };

    const contract = near.initContract<PromptWarsMarketContractMethods>(account, contractAddress, contractMethods);

    return [new PromptWarsMarketContract(contract), contract];
  }

  static async sell(wallet: Wallet, contractAddress: AccountId) {
    try {
      const gas = new BN("300000000000000");
      const deposit = "0";

      const response = await wallet.signAndSendTransaction({
        receiverId: contractAddress,
        actions: [
          {
            type: "FunctionCall",
            params: {
              methodName: "sell",
              args: {},
              gas: gas.toString(),
              deposit,
            },
          },
        ],
      });

      near.unwrapFinalExecutionOutcome(response as FinalExecutionOutcome);
    } catch (error) {
      console.log(error);
      throw new Error("ERR_MARKET_CONTRACT_SELL");
    }
  }

  static async reveal(
    contractId: AccountId,
    outcome_id: OutcomeId,
    result: OutcomeTokenResult,
    output_img_uri: string,
  ) {
    console.log(`revealing Prompt Wars prompt result for  with account ${near.getConfig().serverWalletId}`);

    const connection = await near.getPrivateKeyConnection();
    const account = await connection.account(near.getConfig().serverWalletId);

    const methodName = "reveal";

    const gas = new BN("300000000000000");
    const attachedDeposit = new BN("0");

    const args = { outcome_id, result, output_img_uri };

    await account.functionCall({
      contractId,
      methodName,
      args,
      gas,
      attachedDeposit,
    });
  }

  static async resolve(contractId: AccountId) {
    console.log(`resolving Prompt Wars with account ${near.getConfig().serverWalletId}`);

    const connection = await near.getPrivateKeyConnection();
    const account = await connection.account(near.getConfig().serverWalletId);

    const methodName = "resolve";

    const gas = new BN("300000000000000");
    const attachedDeposit = new BN("0");

    const args = {};

    await account.functionCall({
      contractId,
      methodName,
      args,
      gas,
      attachedDeposit,
    });
  }

  static async sellResolved(contractId: AccountId) {
    console.log(`calling sell resolved Prompt Wars with account ${near.getConfig().serverWalletId}`);

    const connection = await near.getPrivateKeyConnection();
    const account = await connection.account(near.getConfig().serverWalletId);

    const methodName = "sell";

    const gas = new BN("300000000000000");
    const attachedDeposit = new BN("0");

    const args = {};

    await account.functionCall({
      contractId,
      methodName,
      args,
      gas,
      attachedDeposit,
    });
  }

  static async claimFees(contractId: AccountId) {
    console.log(`calling claim_fees Prompt Wars with account ${near.getConfig().serverWalletId}`);

    const connection = await near.getPrivateKeyConnection();
    const account = await connection.account(near.getConfig().serverWalletId);

    const methodName = "claim_fees";

    const gas = new BN("300000000000000");
    const attachedDeposit = new BN("0");

    const args = {};

    await account.functionCall({
      contractId,
      methodName,
      args,
      gas,
      attachedDeposit,
    });
  }

  static async selfDestruct(contractId: AccountId) {
    console.log(`calling self_destruct Prompt Wars with account ${near.getConfig().serverWalletId}`);

    const connection = await near.getPrivateKeyConnection();
    const account = await connection.account(near.getConfig().serverWalletId);

    const methodName = "self_destruct";

    const gas = new BN("300000000000000");
    const attachedDeposit = new BN("0");

    const args = {};

    await account.functionCall({
      contractId,
      methodName,
      args,
      gas,
      attachedDeposit,
    });
  }

  async get_market_data() {
    try {
      const result = await this.contract.get_market_data();

      return {
        ...result,
        starts_at: date.extractNanoseconds(result.starts_at),
        ends_at: date.extractNanoseconds(result.ends_at),
      };
    } catch (error) {
      console.log(error);
      throw new Error("ERR_PW_MARKET_CONTRACT_GET_MARKET_DATA");
    }
  }

  async get_resolution_data() {
    try {
      const result = await this.contract.get_resolution_data();

      return {
        ...result,
        window: date.extractNanoseconds(result.window),
        resolved_at: result.resolved_at ? date.extractNanoseconds(result.resolved_at) : undefined,
        reveal_window: date.extractNanoseconds(result.reveal_window),
      };
    } catch (error) {
      console.log(error);
      throw new Error("ERR_PW_MARKET_CONTRACT_GET_RESOLUTION_DATA");
    }
  }

  async get_fee_data() {
    try {
      const result = await this.contract.get_fee_data();

      return {
        ...result,
        claiming_window: result.claiming_window ? date.extractNanoseconds(result.claiming_window) : undefined,
      };
    } catch (error) {
      console.log(error);
      throw new Error("ERR_PW_MARKET_CONTRACT_GET_FEE_DATA");
    }
  }

  async get_management_data() {
    try {
      const result = await this.contract.get_management_data();

      return {
        ...result,
        self_destruct_window: date.extractNanoseconds(result.self_destruct_window),
      };
    } catch (error) {
      console.log(error);
      throw new Error("ERR_PW_MARKET_CONTRACT_GET_MANAGEMENT_DATA");
    }
  }

  async get_collateral_token_metadata() {
    try {
      const result = await this.contract.get_collateral_token_metadata();

      return result;
    } catch (error) {
      console.log(error);
      throw new Error("ERR_PW_MARKET_CONTRACT_GET_COLLATERAL_TOKEN_METADATA");
    }
  }

  async get_outcome_ids() {
    try {
      const result = await this.contract.get_outcome_ids();

      return result.sort((a, b) => a.localeCompare(b));
    } catch (error) {
      console.log(error);
      throw new Error("ERR_PW_MARKET_CONTRACT_GET_OUTCOME_IDS");
    }
  }

  async get_outcome_token(args: GetOutcomeTokenArgs) {
    try {
      const result = await this.contract.get_outcome_token(args);

      return result;
    } catch (error) {
      console.log(error);
      throw new Error("ERR_PW_MARKET_CONTRACT_GET_OUTCOME_TOKEN");
    }
  }

  async get_block_timestamp() {
    try {
      const result = await this.contract.get_block_timestamp();

      return date.extractNanoseconds(result);
    } catch (error) {
      console.log(error);
      throw new Error("ERR_PW_MARKET_CONTRACT_GET_BLOCK_TIMESTAMP");
    }
  }

  async is_resolved() {
    try {
      const result = await this.contract.is_resolved();

      return result;
    } catch (error) {
      console.log(error);
      throw new Error("ERR_PW_MARKET_CONTRACT_IS_RESOLVED");
    }
  }

  async is_open() {
    try {
      const result = await this.contract.is_open();

      return result;
    } catch (error) {
      console.log(error);
      throw new Error("ERR_PW_MARKET_CONTRACT_IS_OPEN");
    }
  }

  async is_over() {
    try {
      const result = await this.contract.is_over();

      return result;
    } catch (error) {
      console.log(error);

      if ((error as TypedError)?.type === "AccountDoesNotExist") {
        return true;
      }

      throw new Error("ERR_PW_MARKET_CONTRACT_IS_OVER");
    }
  }

  async is_reveal_window_expired() {
    try {
      const result = await this.contract.is_reveal_window_expired();

      return result;
    } catch (error) {
      console.log(error);

      if ((error as TypedError)?.type === "AccountDoesNotExist") {
        return true;
      }

      throw new Error("ERR_PW_MARKET_CONTRACT_IS_REVEAL_WINDOW_EXPIRED");
    }
  }

  async is_resolution_window_expired() {
    try {
      const result = await this.contract.is_resolution_window_expired();

      return result;
    } catch (error) {
      console.log(error);

      if ((error as TypedError)?.type === "AccountDoesNotExist") {
        return true;
      }

      throw new Error("ERR_PW_MARKET_CONTRACT_IS_RESOLUTION_WINDOW_EXPIRED");
    }
  }

  async is_self_destruct_window_expired() {
    try {
      const result = await this.contract.is_self_destruct_window_expired();

      return result;
    } catch (error) {
      console.log(error);

      if ((error as TypedError)?.type === "AccountDoesNotExist") {
        return true;
      }

      throw new Error("ERR_PW_MARKET_CONTRACT_IS_SELF_DESTRUCT_WINDOW_EXPIRED");
    }
  }

  async is_expired_unresolved() {
    try {
      const result = await this.contract.is_expired_unresolved();

      return result;
    } catch (error) {
      console.log(error);
      throw new Error("ERR_PW_MARKET_CONTRACT_IS_EXPIRED_UNRESOLVED");
    }
  }
}
https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/pages/api/prompt-wars/create.ts#L58
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 });
  }
}
https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/context/near/prompt-wars-market-contract/NearPromptWarsMarketContractContextController.tsx#L80
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>
  );
};