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
  • The WalletSelector Component
  • The NearWalletSelectorContextController
  • WalletStateContextController
  • CollateralTokenBalance
  1. NEAR Protocol Recipes

NEAR and NEP141 Balance Widget

A React component to display NEAR native and NEP141 tokens balance. Formatted with their corresponding decimals.

PreviousNEAR Guest Wallet WidgetNextRender a Data Table from Contract VIEW Methods

Last updated 1 year ago

This widget integrates with the original Wallet Selector.

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.

The WalletSelector Component

This React component may be inserted as a child of a NavBar component. You may need only the logic for your project, so follow along to learn how to initialize the NEAR wallet and integrate it with @near-wallet-selector/core

The NearWalletSelectorContextController

This component is designed to be a React context state and action manager. It should initialize the NEAR wallets modules on the first load. It will also set a selector state const so that you can load the wallets modal within UI components.

This context component works together with WalletStateContextController .

WalletStateContextController

This context component is used through the useWalletStateContext hook. It provides the connected wallet address, balance, and actions to sign in and out:

CollateralTokenBalance

You may also want to look at this component to understand how it fetches the balance of a given NEP141 token. Also displayed in the wallet widget:

Project Setup
clone or navigate the repo
https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/context/wallet/state/WalletStateContextController.tsx
import { ReactNode, useState } from "react";
import { NetworkId } from "@near-wallet-selector/core";

import { Address, Balance, Chain, Context, Explorer, IsConnected, StateActions } from "./WalletStateContext.types";
import { WalletStateContext } from "./WalletStateContext";

export const WalletStateContextController = ({ children }: { children: ReactNode }) => {
  const [network, setNetwork] = useState<NetworkId | undefined>("testnet");
  const [explorer, setExplorer] = useState<Explorer>(undefined);
  const [chain, setChain] = useState<Chain>(undefined);
  const [address, setAddress] = useState<Address>(undefined);
  const [balance, setBalance] = useState<Balance>("0.00");
  const [isConnected, setIsConnected] = useState<IsConnected>(false);
  const [context, setContext] = useState<Context>({
    connection: undefined,
    provider: undefined,
    guest: {
      address: "",
    },
  });
  const [actions, setActions] = useState<StateActions>({
    isGettingGuestWallet: false,
  });

  const reset = () => {
    setNetwork(undefined);
    setExplorer(undefined);
    setChain(undefined);
    setAddress(undefined);
    setBalance("0.00");
    setIsConnected(false);
    setActions({ isGettingGuestWallet: false });
  };

  const props = {
    setNetwork,
    setAddress,
    setExplorer,
    setChain,
    setBalance,
    setIsConnected,
    setContext,
    setActions,
    network,
    address,
    explorer,
    chain,
    balance,
    isConnected,
    reset,
    context,
    actions,
  };

  return <WalletStateContext.Provider value={props}>{children}</WalletStateContext.Provider>;
};
https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/ui/wallet-selector/WalletSelector.tsx
import clsx from "clsx";
import { useEffect, useState } from "react";
import { useTranslation } from "next-i18next";

import { Button } from "../button/Button";
import { Typography } from "ui/typography/Typography";
import { Icon } from "ui/icon/Icon";
import { useNearWalletSelectorContext } from "context/near/wallet-selector/useNearWalletSelectorContext";
import near from "providers/near";
import { useWalletStateContext } from "context/wallet/state/useWalletStateContext";
import pulse from "providers/pulse";
import { CollateralTokenBalance } from "ui/pulse/market-card/collateral-token-balance/CollateralTokenBalance";
import { useNearPromptWarsMarketContractContext } from "context/near/prompt-wars-market-contract/useNearPromptWarsMarketContractContext";

import { WalletSelectorProps } from "./WalletSelector.types";
import styles from "./WalletSelector.module.scss";

import "@near-wallet-selector/modal-ui/styles.css";

export const WalletSelector: React.FC<WalletSelectorProps> = ({ className }) => {
  const [isWidgetVisible, setIsWidgetVisible] = useState(false);

  const { marketId } = useNearPromptWarsMarketContractContext();

  const nearWalletSelectorContext = useNearWalletSelectorContext();

  const wallet = useWalletStateContext();

  useEffect(() => {
    if (!nearWalletSelectorContext.selector) {
      return;
    }

    nearWalletSelectorContext.initModal(near.getConfig().factoryWalletId);
  }, [nearWalletSelectorContext.selector]);

  const handleOnConnectWalletClick = () => {
    nearWalletSelectorContext.signOut();
    setIsWidgetVisible(false);
  };

  const handleOnDisplayWidgetClick = () => {
    if (wallet.isConnected) {
      setIsWidgetVisible(!isWidgetVisible);
    } else {
      nearWalletSelectorContext.modal?.show();
    }
  };

  const { t } = useTranslation(["prompt-wars"]);

  return (
    <div className={clsx(styles["wallet-selector"], className)}>
      <Button
        color={wallet.actions.isGettingGuestWallet ? "success" : "primary"}
        variant="outlined"
        onClick={handleOnDisplayWidgetClick}
        rightIcon={<Icon name={wallet.address ? "icon-power" : "icon-power-crossed"} />}
        className={styles["wallet-selector__button"]}
        animate={wallet.actions.isGettingGuestWallet ? "pulse" : undefined}
        size="s"
      >
        {wallet.isConnected ? (
          <Typography.Text inline truncate flat>
            {wallet.address}
          </Typography.Text>
        ) : (
          <>
            {wallet.actions.isGettingGuestWallet
              ? t("promptWars.walletSelector.isSettingGuestWallet")
              : t("promptWars.connectWallet")}
          </>
        )}
      </Button>
      {isWidgetVisible && (
        <>
          <div
            className={styles["wallet-selector__widget--backdrop"]}
            onClick={() => setIsWidgetVisible(!isWidgetVisible)}
            role="presentation"
          />
          <div className={styles["wallet-selector__widget"]}>
            <div className={styles["wallet-selector__balance"]}>
              <Typography.Description>{t("promptWars.walletSelector.nativeBalance")}</Typography.Description>
              <Typography.Text>{wallet.balance}</Typography.Text>
              <Typography.Description>
                {t("promptWars.balance")} <code>@{pulse.getDefaultCollateralToken().accountId}</code>
              </Typography.Description>
              <Typography.Text>
                <CollateralTokenBalance
                  accountId={wallet.address || marketId}
                  contractAddress={pulse.getDefaultCollateralToken().accountId}
                />
              </Typography.Text>
              <Typography.MiniDescription flat>
                <Typography.Anchor href={`${wallet.explorer}/accounts/${wallet.address}`} target="_blank">
                  {wallet.isConnected && wallet.address}
                </Typography.Anchor>
              </Typography.MiniDescription>
            </div>
            <div className={styles["wallet-selector__connect"]}>
              <Button size="xs" color="primary" onClick={handleOnConnectWalletClick}>
                {wallet.isConnected ? "Disconnect" : "Connect"}
              </Button>
            </div>
          </div>
        </>
      )}
    </div>
  );
};
https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/context/near/wallet-selector/NearWalletSelectorContextController.tsx
import React, { useEffect, useState } from "react";
import { BrowserWallet, NetworkId, setupWalletSelector, WalletSelector } from "@near-wallet-selector/core";
import { setupModal, WalletSelectorModal } from "@near-wallet-selector/modal-ui";
import { setupNearWallet } from "@near-wallet-selector/near-wallet";
import { setupNearFi } from "@near-wallet-selector/nearfi";
import { setupHereWallet } from "@near-wallet-selector/here-wallet";
import { setupMathWallet } from "@near-wallet-selector/math-wallet";
import { setupSender } from "@near-wallet-selector/sender";
import { setupNightly } from "@near-wallet-selector/nightly";
import { setupMeteorWallet } from "@near-wallet-selector/meteor-wallet";
import { setupLedger } from "@near-wallet-selector/ledger";
import { setupCoin98Wallet } from "@near-wallet-selector/coin98-wallet";
import { setupMyNearWallet } from "@near-wallet-selector/my-near-wallet";

import near from "providers/near";
import { useWalletStateContext } from "context/wallet/state/useWalletStateContext";
import { WalletSelectorChain } from "context/wallet-selector/WalletSelectorContext.types";
import { useRoutes } from "hooks/useRoutes/useRoutes";
import { setupGuestWallet } from "providers/near/wallet-selector/setupGuestWallet";
import { useLocalStorage } from "hooks/useLocalStorage/useLocalStorage";

import { NearWalletSelectorContextControllerProps } from "./NearWalletSelectorContext.types";
import { NearWalletSelectorContext } from "./NearWalletSelectorContext";

export const NearWalletSelectorContextController = ({ children }: NearWalletSelectorContextControllerProps) => {
  const [selector, setSelector] = useState<WalletSelector>();
  const [modal, setModal] = useState<WalletSelectorModal>();

  const walletStateContext = useWalletStateContext();

  const routes = useRoutes();

  const ls = useLocalStorage();

  const initGuestConnection = async (accountId: string) => {
    console.log(`initGuestConnection: ${accountId}`);

    try {
      const connection = await near.initWalletConnection();

      const { near: nearAPI, wallet: nearWalletConnection } = connection;

      const wallet = await selector?.wallet("guest-wallet");

      await (wallet as BrowserWallet)?.signIn({ contractId: near.getConfig().factoryWalletId });

      walletStateContext.setContext({
        wallet,
        connection: nearWalletConnection,
        provider: nearAPI,
        guest: {
          address: accountId,
        },
      });

      walletStateContext.setIsConnected(true);
      walletStateContext.setAddress(accountId);
      walletStateContext.setNetwork(near.getConfig().networkId as NetworkId);
      walletStateContext.setChain(WalletSelectorChain.near);
      walletStateContext.setExplorer(near.getConfig().explorerUrl);

      const accountBalance = await near.getAccountBalance(nearAPI, accountId);

      walletStateContext.setBalance(near.formatAccountBalance(accountBalance.available, 8));
    } catch (error) {
      console.log(error);
    }

    walletStateContext.setActions((prev) => ({
      ...prev,
      isGettingGuestWallet: false,
    }));
  };

  const onSignedIn = async (s: WalletSelector) => {
    try {
      const connection = await near.initWalletConnection();

      const { near: nearAPI, wallet: nearWalletConnection } = connection;

      walletStateContext.setContext({
        connection: nearWalletConnection,
        provider: nearAPI,
        guest: {
          address: near.getConfig().guestWalletId,
        },
      });

      const wallet = await s?.wallet();

      if (!wallet) {
        return;
      }

      const [account] = await wallet.getAccounts();

      walletStateContext.setContext({
        wallet,
        connection: nearWalletConnection,
        provider: nearAPI,
        guest: {
          address: account.accountId,
        },
      });

      walletStateContext.setIsConnected(s.isSignedIn());
      walletStateContext.setAddress(account.accountId);
      walletStateContext.setNetwork(near.getConfig().networkId as NetworkId);
      walletStateContext.setChain(WalletSelectorChain.near);
      walletStateContext.setExplorer(near.getConfig().explorerUrl);

      const accountBalance = await near.getAccountBalance(nearAPI, account.accountId);

      walletStateContext.setBalance(near.formatAccountBalance(accountBalance.available, 8));
    } catch (error) {
      console.log(error);
    }
  };

  const signOut = async () => {
    try {
      const wallet = await selector?.wallet()!;

      await wallet.signOut();
    } catch (error) {
      console.log(error);
    }

    walletStateContext.reset();
  };

  useEffect(() => {
    (async () => {
      try {
        const network = near.getConfig().networkId as NetworkId;

        const walletSelector = await setupWalletSelector({
          network,
          modules: [
            setupGuestWallet(),
            setupNearWallet(),
            setupMyNearWallet(),
            setupNearFi(),
            setupHereWallet(),
            setupMathWallet(),
            setupSender(),
            setupNightly(),
            setupMeteorWallet(),
            setupLedger(),
            setupCoin98Wallet(),
          ],
        });

        const onSignedInSub = walletSelector.on("signedIn", () => onSignedIn(walletSelector));

        setSelector(walletSelector);

        return () => {
          onSignedInSub.remove();
        };
      } catch (error) {
        console.log(error);
      }

      return undefined;
    })();
  }, []);

  useEffect(() => {
    if (!selector) {
      return;
    }

    (async () => {
      try {
        const wallet = await selector?.wallet();

        if (!wallet) {
          return;
        }

        onSignedIn(selector!);
      } catch (error) {
        console.log(error);
      }
    })();
  }, [selector]);

  useEffect(() => {
    if (!selector) {
      return;
    }

    (async () => {
      try {
        if (ls.get("near_app_wallet_auth_key") !== null) {
          console.log(`existing non-guest wallet connected`);

          return;
        }

        walletStateContext.setActions((prev) => ({
          ...prev,
          isGettingGuestWallet: true,
        }));

        const walletAuthKey = ls.get<{
          accountId: string;
          allKeys: Array<string>;
        }>("promptwars_wallet_auth_key");

        if (walletAuthKey !== null && /^guest-/i.test(walletAuthKey?.accountId)) {
          initGuestConnection(walletAuthKey.accountId);

          return;
        }

        const response = await fetch(routes.api.promptWars.createGuestAccount());
        const result = await response.json();

        ls.set("near-wallet-selector:selectedWalletId", JSON.stringify("guest-wallet"));
        ls.set("near-wallet-selector:recentlySignedInWallets", JSON.stringify(["guest-wallet"]));
        ls.set(Object.keys(result)[0], result[Object.keys(result)[0]]);
        ls.set(Object.keys(result)[1], result[Object.keys(result)[1]]);
        ls.set(Object.keys(result)[2], result[Object.keys(result)[2]]);

        console.log(result);

        const { accountId } = result.promptwars_wallet_auth_key;

        initGuestConnection(accountId);
      } catch (error) {
        console.log(error);

        walletStateContext.setActions((prev) => ({
          ...prev,
          isGettingGuestWallet: false,
        }));
      }
    })();
  }, [selector]);

  const initModal = (contractId: string) => {
    setModal(
      setupModal(selector!, {
        contractId,
      }),
    );
  };

  const props = {
    selector,
    modal,
    initModal,
    signOut,
  };

  return <NearWalletSelectorContext.Provider value={props}>{children}</NearWalletSelectorContext.Provider>;
};
https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/ui/pulse/market-card/collateral-token-balance/CollateralTokenBalance.tsx
import { useEffect } from "react";

import useNearFungibleTokenContract from "providers/near/contracts/fungible-token/useNearFungibleTokenContract";
import pulse from "providers/pulse";

import { CollateralTokenBalanceProps } from "./CollateralTokenBalance.types";

export const CollateralTokenBalance: React.FC<CollateralTokenBalanceProps> = ({ contractAddress, accountId }) => {
  const { getBalanceOf, balanceOf } = useNearFungibleTokenContract({
    contractAddress,
  });

  useEffect(() => {
    if (!accountId) {
      return;
    }

    getBalanceOf(accountId);
  }, [accountId, contractAddress]);

  const collateralToken = pulse.getCollateralTokenByAccountId(contractAddress);

  return (
    <>
      {collateralToken.symbol} {balanceOf}
    </>
  );
};