Skip to content
Last update: September 22, 2022

Step 4: Set up a MetaMask Wallet

Get API keys

You need a Web3Provider to help you connect and interact with the blockchain.

This tutorial uses:

  • Blocknative Onboard - An open-source JavaScript library to onboard users to Ethereum apps with wallet selection, connection, wallet checks, and real-time state updates.
  • Blocknative Notify - A UI component for transaction status notifications.
  • Infura - A provider of highly available APIs and developer tools to allow quick, reliable access to the Ethereum.

Sign up for the Blocknative API key here. On the Account Dashboard, create an API key with your choice of name or use/rename the Default Key.

Install Blocknative Onboard and Notify as dependencies:

yarn add bnc-onboard
yarn add bnc-notify

Follow these instructions to register a project and get your Infura Project ID.

Add your Blocknative and Infura keys to .env.local and export them in config.

...
REACT_APP_BNC_DAPP_ID=//put down your own BNC key
REACT_APP_INFURA_KEY=//put down your own Infura key
...
...
export const bncDappId = process.env.REACT_APP_BNC_DAPP_ID;
export const infuraKey = process.env.REACT_APP_INFURA_KEY;
...

Create WalletContext

You need to integrate the app with a wallet provider for people to purchase NFTs. You’ll set this up according to a reference implementation provided by Blocknative, which includes desired ways to initiate the library, as well as to select a wallet, check a wallet, log out of a wallet, and detect network changes.

Create a file called WalletContext:

/src/components/WalletContext.tsx
import React, { useState, useEffect, createContext } from "react";
import { ethers } from "ethers";
import { Web3Provider } from "@ethersproject/providers";
import Onboard from "bnc-onboard";
import { API } from "bnc-onboard/dist/src/interfaces";
import Notify from "bnc-notify";
import { bncDappId, infuraKey, networkId } from "../config";
import { getItem, removeItem, setItem } from "../utils/localStorage";

interface WalletContextValue {
    onboard: API | null;
    notify: any;
    web3Provider: Web3Provider | null;
    address: string | null;
    network: number | null;
    selectWallet(): any;
    checkWallet(): any;
    logoutWallet(): any;
    isRightNetwork(): boolean;
}

const ALLOWED_CHAIN_IDS = [1, 4, 147];

const chainId = networkId ? parseInt(networkId) : 4;

const wallets: any[] = [];

export const setupWallets = () => {
    if (infuraKey) {
        wallets.push({
            walletName: "walletConnect",
            infuraKey,
            preferred: true,
        });
    }
};

const WalletContext = createContext<WalletContextValue>({
    onboard: null,
    notify: null,
    web3Provider: null,
    address: null,
    network: null,
    selectWallet: () => {},
    checkWallet: () => {},
    logoutWallet: () => {},
    isRightNetwork: () => false,
});

const WalletProvider = ({ children }: any) => {
const [web3Provider, setWeb3Provider] = useState<Web3Provider | null>(null);
const [address, setAddress] = useState<string | null>(null);
const [network, setNetwork] = useState<number | null>(null);

// -----------------------------------------------------------------------------------
// Initialize onboard and notify library
// -----------------------------------------------------------------------------------
// note: we are not currently doing anything with a user's balance
// if we wanted to, there is a userWallet that we can use and keep updated
// via onbard.getState()

const [onboard, setOnboard] = useState<API | null>(null);
const [notify, setNotify] = useState<any>(null);

useEffect(() => {
    const onboard = Onboard({
        dappId: bncDappId,
        networkId: chainId,
        hideBranding: true,
        subscriptions: {
            wallet: (wallet) => {
                if (wallet.provider) {
                    const ethersProvider = new ethers.providers.Web3Provider(
                        wallet.provider
                    );
                    setWeb3Provider(ethersProvider);

                    // store user preference
                    setItem("selectedWallet", wallet.name);
                } else {
                    // logging out
                    setWeb3Provider(null);
                    removeItem("selectedWallet");
                }
            },
            address: (address) =>
                address ? setAddress(ethers.utils.getAddress(address)) : "",
            network: setNetwork,
        },
        walletSelect: {
            wallets: [
                { walletName: "metamask", preferred: true },
                ...(ALLOWED_CHAIN_IDS.includes(chainId) ? wallets : []),
            ],
        },
        walletCheck: [{ checkName: "connect" }, { checkName: "network" }],
    });

    const notify = Notify({
        dappId: bncDappId,
        networkId: chainId,
        darkMode: true,
    });

    setOnboard(onboard);
    setNotify(notify);
}, []);

// -----------------------------------------------------------------------------------
// If network changes, the getNetwork call with throw network change error with the
// previously connected network. This is what the provider is initialized with and
// is what we want.
// -----------------------------------------------------------------------------------
const getProviderNetwork = React.useCallback(async () => {
    try {
        return await web3Provider?.getNetwork();
    } catch (e: any) {
        if (e.network) {
            return e.network;
        }
        return null;
    }
}, [web3Provider]);

// -----------------------------------------------------------------------------------
// Wallet utility functions
// -----------------------------------------------------------------------------------
// note: call this before web3 txs and it will run through a series of checks that we
// can customize during initialization. Defaults to checking to make sure the wallet
// is connected.
// https://docs.blocknative.com/onboard#wallet-check-modules
const checkWallet = React.useCallback(async () => {
    return onboard?.walletCheck();
}, [onboard]);

const selectWallet = React.useCallback(async () => {
    const walletSelected = await onboard?.walletSelect();

    if (walletSelected) {
        await onboard?.walletCheck();
    }
}, [onboard]);

const logoutWallet = React.useCallback(async () => {
    setAddress(null);
    return onboard?.walletReset();
}, [onboard]);

// -----------------------------------------------------------------------------------
// Load the previous connected wallet so returning users have nice experience
// only happens once right after the onboard module is initialized
// -----------------------------------------------------------------------------------
useEffect(() => {
    const previouslySelectedWallet = getItem("selectedWallet");

    if (previouslySelectedWallet && onboard) {
        onboard.walletSelect(previouslySelectedWallet);
    }
}, [onboard]);

// -----------------------------------------------------------------------------------
// Force re-initialization of wallet on network change
// -----------------------------------------------------------------------------------
const reloadWalletOnNetworkChange = React.useCallback(async () => {
    if (!network) return;

    const providerNetwork = await getProviderNetwork();

    // get current wallet info so can auto log back in
    const userState = await onboard?.getState();
    const wallet = userState && userState.wallet;
    if (!wallet || !wallet.name) return;

    // if provider network is different, then the user has changed networks
    if (providerNetwork && providerNetwork.chainId !== network) {
        // reset wallet to trigger a full re-initialization on wallet select
        await onboard?.walletReset();

        // re-select the wallet
        // const walletSelected = await onboard?.walletSelect(wallet.name);
        await onboard?.walletSelect(wallet.name);
    }

    if (providerNetwork && providerNetwork.chainId !== chainId) {
        await onboard?.walletCheck();
    }
}, [onboard, getProviderNetwork, network]);

useEffect(() => {
    reloadWalletOnNetworkChange();
}, [reloadWalletOnNetworkChange]);

const isRightNetwork = () => network === chainId;

return (
    <WalletContext.Provider
        value={{
            onboard,
            web3Provider,
            address,
            network,
            selectWallet,
            checkWallet,
            logoutWallet,
            notify,
            isRightNetwork,
        }}
    >
        {children}
    </WalletContext.Provider>
);
};

const useWallet = () => {
const context = React.useContext(WalletContext);
if (context === undefined) {
    throw new Error("useWallet must be used within a WalletProvider");
}
return context;
};

export { WalletProvider, useWallet };

This implementation relies on utility methods to set and get items from local storage.

/src/utils/localStorage.ts
    export const getItem = (key: string) => localStorage.getItem(key);
    export const setItem = (key: string, value: any) => localStorage.setItem(key, value);
    export const removeItem = (key: string) => localStorage.removeItem(key);

Create wallet component

With this context, we can now set up a wallet component.

/src/componenets/Wallet.tsx
import { Box, Button, Typography } from "@mui/material";
import ExitToAppIcon from "@mui/icons-material/ExitToApp";
import EthAddress from "./EthAddress";
import { useWallet } from "./WalletContext";

const Wallet: React.FC = () => {
    const { selectWallet, logoutWallet, address } = useWallet();

    const logout = () => {
        console.log("logging out");
        logoutWallet();
    };

    return (
        <>
            {address ? (
                <>
                    <EthAddress address={address} networkId={4} />

                    <Box
                        onClick={logout}
                        display={["none", "block"]}
                        sx={{ cursor: "pointer" }}
                    >
                        <ExitToAppIcon />
                    </Box>
                </>
            ) : (
                <Button
                    variant="outlined"
                    onClick={selectWallet}
                    color="secondary"
                    sx={{
                        cursor: "pointer",
                        borderRadius: 5,}
                    }
                >

                    <Typography variant="button" sx={{ fontSize: "1rem" }}>
                        Connect Wallet
                    </Typography>
                </Button>
            )}
        </>
    );
};

export default Wallet;

Include this component in the Page template.

src/components/Page.tsx
import Wallet from "./Wallet";
    ...
    ...
        <>
            <Box display="flex" sx={{ p: 10, justifyContent: "flex-end" }}>
                <Wallet />
            </Box>
            <Container sx={{ py: 8 }}>{children}</Container>
        </>
    ...

In AppProviders, wrap the app with the WalletProvider.

/src/AppProviders.ts
    import { QueryClientProvider, QueryClient } from "react-query";
    import { setupWallets, WalletProvider } from "./components/WalletContext";
    import { ReactQueryDevtools } from "react-query/devtools";

    const queryClient = new QueryClient();
    setupWallets();

    const AppProviders = ({ children }: any) => (
        <QueryClientProvider client={queryClient}>
            <WalletProvider>{children}</WalletProvider>
            <ReactQueryDevtools initialIsOpen />
        </QueryClientProvider>
    );

    export default AppProviders;

If everything is set up correctly, a Connect Wallet button displays on top right corner of the app.

Selecting it triggers the Blocknative onboard library and prompts you to select a wallet. If you have a MetaMask account set up, this is the time to connect.

tutorial-1-connect-metamask

Your address displays in the same place if you’re connected to the Rinkeby Network. If not, MetaMask prompts you to change your network to Rinkeby. You may also select the log out icon to log out of your wallet.

tutorial-1-show-wallet-address

Back to top