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
.
- /.env.local
- /src/config.ts
...
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
:
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
.
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.
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.
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
.
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.
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.