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
  • Creating the Guest Wallet
  • Create a Guest Wallet Selector Module
  • Calling the API endpoint
  • Using the Guest Wallet Connection Instance
  1. NEAR Protocol Recipes

NEAR Guest Wallet Widget

Increase user engagement with this React component that sets up a NEAR guest wallet for your visitors on their first load.

PreviousNEAR Protocol RecipesNextNEAR and NEP141 Balance Widget

Last updated 1 year ago

Give your visitors a frictionless experience on performing on-chain transactions and using smart-contracts without the need of their own wallet. Perfect for beginners and for new product launches!

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.

Creating the Guest Wallet

This code needs to be executed on the server-side of your application, since creating new NEAR sub-accounts require you to provide the private keys of a parent account.

The clue is to return a JSON object that mimics the Wallet Selector localStorage format so that it would pick the expected guest-wallet when a user enters.

If the new account will interact with an NEP141 fungible token, you'll need to register the account and transfer the minimum NEAR to activate the account with the source NEP141 contract.

Create a Guest Wallet Selector Module

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

Calling the API endpoint

The next step is to GET the endpoint JSON object and parse it to set the localStorage properties, see line 188+ and how it waits for selector to exist first. Then call initGuestConnection to initialize a new NEAR Wallet Connection.

This new Wallet connection will be stored in the WalletStateContextController, so that it can be used in your components:

Using the Guest Wallet Connection Instance

Now that a React context state has a wallet instance, you can use it to display values on a widget component.

When you setup the NEAR Wallet Selector modules, you'll want to so that you have control on the ID and the way it connects your guest user.

See for full implementation.

See to learn how to use this React context pattern.

insert your own instance of a guest-wallet module
WalletStateContextController.tsx
Calling the API Endpoint
👌
Project Setup
clone or navigate the repo
https://github.com/aufacicenta/pulsemarkets/blob/prompt-wars/app/src/pages/api/prompt-wars/create-guest-account.ts
import { v4 as uuidv4 } from "uuid";
import { KeyPair } from "near-api-js/lib/utils/key_pair";
import { NextApiRequest, NextApiResponse } from "next";
import * as nearAPI from "near-api-js";
import { BN } from "bn.js";

import logger from "providers/logger";
import near from "providers/near";
import { FungibleTokenContract } from "providers/near/contracts/fungible-token/contract";
import pulse from "providers/pulse";

export default async function Fn(_request: NextApiRequest, response: NextApiResponse) {
  try {
    logger.info(`creating Prompt Wars guest account`);

    const accountId = near.getConfig().guestWalletId;

    const connection = await near.getPrivateKeyConnection(process.env.NEAR_OWNER_PRIVATE_KEY, accountId);

    const account = await connection.account(accountId);

    const id = `guest-${uuidv4().slice(0, 5)}.${accountId}`;

    const pk = KeyPair.fromRandom("ed25519");

    const publicKey = pk.getPublicKey();

    logger.info(publicKey.toString(), id, accountId);

    await account.createAccount(id, publicKey, new BN(nearAPI.utils.format.parseNearAmount("0.2") as string));

    await FungibleTokenContract.register(pulse.getDefaultCollateralToken().accountId, id);

    await FungibleTokenContract.staticFtTransferCall(pulse.getDefaultCollateralToken().accountId, "50000", id);

    response.status(200).json({
      promptwars_wallet_auth_key: { accountId: id, allKeys: [publicKey.toString()] },
      [`near-api-js:keystore:${id}:${near.getConfig().networkId}`]: pk.toString(),
      "near-wallet-selector:contract": { contractId: near.getConfig().factoryWalletId },
    });
  } 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/wallet-selector/NearWalletSelectorContextController.tsx#L189-L241
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/wallet-selector/WalletSelectorMobile.tsx#L39
import clsx from "clsx";
import { useTranslation } from "next-i18next";

import { Button } from "../button/Button";
import { useWalletStateContext } from "context/wallet/state/useWalletStateContext";
import { BalancePill } from "ui/pulse/sidebar/balance-pill/BalancePill";
import { Typography } from "ui/typography/Typography";
import { Icon } from "ui/icon/Icon";
import { useNearWalletSelectorContext } from "context/near/wallet-selector/useNearWalletSelectorContext";

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

export const WalletSelectorMobile: React.FC<WalletSelectorProps> = ({ className }) => {
  const wallet = useWalletStateContext();
  const nearWalletSelectorContext = useNearWalletSelectorContext();

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

  const handleOnConnectWalletClick = () => {
    if (!wallet.isConnected) {
      nearWalletSelectorContext.modal?.show();
    } else {
      nearWalletSelectorContext.signOut();
    }
  };

  return (
    <div className={clsx(styles["wallet-selector__mobile"], className)}>
      <Button
        size="xs"
        color={wallet.actions.isGettingGuestWallet ? "success" : "primary"}
        variant="outlined"
        onClick={handleOnConnectWalletClick}
        className={styles["wallet-selector__mobile--button"]}
        animate={wallet.actions.isGettingGuestWallet ? "pulse" : undefined}
        rightIcon={<Icon name={wallet.address ? "icon-power" : "icon-power-crossed"} />}
      >
        {wallet.isConnected ? (
          <Typography.Text inline truncate flat>
            {wallet.address}
          </Typography.Text>
        ) : (
          <>
            {wallet.actions.isGettingGuestWallet
              ? t("promptWars.walletSelector.isSettingGuestWallet")
              : t("promptWars.connectWallet")}
          </>
        )}
      </Button>

      {wallet.isConnected ? <BalancePill /> : null}

      {wallet.isConnected ? (
        <Typography.Description
          flat
          onClick={handleOnConnectWalletClick}
          className={styles["wallet-selector__mobile--logout"]}
        >
          {t("promptWars.disconnect")}
        </Typography.Description>
      ) : null}
    </div>
  );
};