How to set up the MetaMask SDK

Learn how to set up a full-stack application using React, ethers.js and the MetaMask SDK.

Build a Full-stack dApp using MetaMask SDK

If you want an awesome wallet experience, here is a guide on how to switch the native RPC endpoint on your MetaMask wallet to an Alchemy RPC endpoint - this will let you see analytics on your own wallet activity! 🔥

1. Introduction

This tutorial will cover how to:

  • deploy a simple smart contract
  • set up a front-end UI
  • use the MetaMask SDK to establish a bridge connection between your front-end and a blockchain (Ethereum, Arbitrum, Sepolia, etc)

Feel free to write your own smart contract, otherwise we’ll use a simple Faucet.sol smart contract as the defacto database to our React front-end as an easy example of how to power a website using a smart contract - how cool! Once we deploy a smart contract to the Sepolia test network, we’ll use the powerful MetaMask SDK to establish a connection to our front-end. Here’s a visual of the application stack you’ll build in this tutorial:

diagram

Let’s go! 🏃‍♀️

2. Pre-Requisites

This tutorial requires the following are installed in order to run a successful local development environment:

  1. Install Node.js. You need version 14.5.0or higher.
  2. Install PNPM: To install PNPM, run the following command in Terminal:
bash
$npm install -g pnpm

Hint: If you’re building React-based apps, you might want to consider using PNPM instead of NPM for package management. PNPM is more space-efficient than NPM, which means it can save you disk space and reduce installation times. Additionally, PNPM is supported by most React-based apps, so you can use it with tools like Create React App without any issues. This tutorial will use PNPM because pnpm is cool and fast! 🔥

3. Build the front-end

Set up using npx create-react-app

  1. Open a terminal on your computer and cd into a directory of your choice

You’ll see cd a lot in this tutorial! cd is a terminal command that stands for Change Directory. 👀

  1. Run npx create-react-app my-dapp
  2. After installation, your terminal should show, the next steps: run cd my-dapp and then run npm start - this should start up your very own local development server!

Good looks! 🔥 You’ve just gotten the starter code React app up and running on http://localhost:3000/:

react

As the app says, let’s edit the App.js file in the /src folder and connect this website to a smart contract already deployed Sepolia test network.

Customize and Style Application

The smart contract we’ll integrate with is a Sepolia-based faucet smart contract. A faucet smart contract is cool because we can deposit some SepoliaETH to the smart contract and you can have your buddies withdraw to their own accounts - and we’ll faciliate this with our brand new application of course!

Let’s add some simple customizations and styling to our application:

  1. Copy-paste the following into your App.js file:
jsx
1import { useState } from "react";
2import "./App.css";
3
4function App() {
5 const [walletBalance, setWalletBalance] = useState(0);
6 const [faucetBalance, setFaucetBalance] = useState(0);
7 return (
8 <div className="container">
9 <h1 className="title">
10 My Faucet dApp! 🚰
11 <span className="subtitle">
12 This faucet allows you or your friends to withdraw .01 SepoliaETH per
13 account!
14 </span>
15 </h1>
16 <div className="balance-container">
17 <h2 className="balance-item">Faucet Balance: {faucetBalance}Ξ</h2>
18 <h2 className="balance-item">My Wallet Balance: {walletBalance} Ξ</h2>
19 </div>
20 <div className="button-container">
21 <button className="button">Withdraw .01 ETH</button>
22 <button className="button">Deposit .01 ETH</button>
23 </div>
24 </div>
25 );
26}
27
28export default App;

This code forms the basic structure of your user-facing application. Notice, we are setting up two buttons:

  • Withdraw .01 ETH
  • Deposit .01 ETH

But clicking them doesn’t do anything just yet! We’ll need to rig em up to the blockchain! ⚡️

  1. For css styling, copy-paste the following into your App.css file:
css
1.container {
2 display: flex;
3 flex-direction: column;
4 align-items: center;
5 margin: 2rem;
6}
7
8.title {
9 font-size: 3rem;
10 margin-bottom: 1rem;
11 text-align: center;
12}
13
14.subtitle {
15 display: block;
16 font-size: 1rem;
17 margin-top: 1rem;
18}
19
20.balance-container {
21 display: flex;
22 flex-direction: column;
23 align-items: center;
24 margin-bottom: 2rem;
25}
26
27.balance-item {
28 font-size: 2rem;
29 margin-right: 1rem;
30}
31
32.button-container {
33 display: flex;
34 justify-content: center;
35 overflow: hidden;
36 margin-top: 2rem;
37 margin-bottom: 2rem;
38}
39
40.button {
41 font-size: 1.5rem;
42 padding: 1rem 2rem;
43 background-color: #00bfff;
44 color: white;
45 border: none;
46 border-radius: 0.5rem;
47 cursor: pointer;
48 transition: background-color 0.3s ease-in-out;
49 margin-right: 1rem;
50}
51
52.button:last-child {
53 margin-right: 0;
54}
55
56.button:hover {
57 background-color: #0077b3;
58}
59
60.button:active {
61 background-color: #005580;
62}
63
64@media screen and (max-width: 768px) {
65 .title {
66 font-size: 2rem;
67 }
68
69 .balance-item {
70 font-size: 1.5rem;
71 }
72
73 .button {
74 font-size: 1rem;
75 padding: 0.5rem 1rem;
76 }
77}

Your app on http://localhost:3000 should now look like this:

react-1

The front-end looks decent enough! But wait… the buttons don’t work… Well, that’s because they’re not rigged to anything yet! In the next step, we’ll use the MetaMask SDK to programmatically connect this application to the Sepolia test network (or any chain you prefer!) and read/write to a faucet smart contract located at: https://sepolia.etherscan.io/address/0x9bdcbc868519cf2907ece4e9602346c3fc9e6c8e.

Calling all you styling and UX gurus! 📞 How can you make this UI/UX experience better for the user? 👀

4. Use the MetaMask SDK to Connect to a Blockchain

Now that a working front-end is built, let’s turn this application into a decentralized application by connecting it to a decentralized blockchain network, in our case to this faucet smart contract on the Sepolia network.

  1. In the terminal where your project is running, start a new tab and run pnpm i @metamask/sdk

You should see the @metamask/sdk get installed as a project dependency! (You can verify this by checking the dependencies in your project’s package.json file!) Your terminal might also show additional peer dependencies that need to be installed, like this:

terminal

You must install each needed dependency one-by-one! Just copy-paste each dependency name and pnpm install it, like this:

bash
$pnpm install "@babel/plugin-syntax-flow@^7.14.5"

If you get a zsh: no matches found: @babel/plugin-syntax-flow@^7.14.5, remember to wrap the package names in string quotations, this means your shell is treating the package as a file instead of a package. Wrap the package name it is complaining about, including @version numbers, in string quotations for the shell to know it is a package and no longer show an error like that. 👀

Keep doing this until your terminal no longer lists additional peer dependencies to install. 🏗️

  1. We’ll also use ethers.js, run pnpm i ethers

  2. Now, let’s make some changes to our React code and add a buuuunch of functionality that, thanks to the MetaMask SDK, we can easily port into our application. In your App.js, copy-paste the following:

jsx
1import MetaMaskSDK from "@metamask/sdk";
2import { ethers } from "ethers";
3import { useEffect, useState } from "react";
4import "./App.css";
5
6// MetaMask SDK initialization
7const MMSDK = new MetaMaskSDK();
8const ethereum = MMSDK.getProvider(); // You can also access via window.ethereum
9
10// This app will only work on Sepolia to protect users
11const SEPOLIA_CHAIN_ID = "0xAA36A7";
12
13// Faucet contract initialization
14const provider = new ethers.providers.Web3Provider(ethereum, "any");
15const FAUCET_CONTRACT_ADDRESS = "0x9BdCbC868519Cf2907ECE4E9602346c3fC9e6c8e";
16const abi = [
17 {
18 "inputs": [],
19 "name": "withdraw",
20 "outputs": [],
21 "stateMutability": "nonpayable",
22 "type": "function"
23 },
24];
25const faucetContract = new ethers.Contract(
26 FAUCET_CONTRACT_ADDRESS,
27 abi,
28 provider.getSigner()
29);
30
31function App() {
32 // useState hooks to keep track of changing figures
33 const [walletBalance, setWalletBalance] = useState(0);
34 const [faucetBalance, setFaucetBalance] = useState(0);
35 const [buttonText, setButtonText] = useState("Connect");
36 const [buttonDisabled, setButtonDisabled] = useState(false);
37 const [onRightNetwork, setOnRightNetwork] = useState(false);
38 // array populated by MetaMask onConnect
39 const [accounts, setAccounts] = useState([]);
40 const [isConnected, setIsConnected] = useState(
41 accounts && accounts.length > 0
42 );
43
44 // MetaMask event listeners
45 ethereum.on("chainChanged", handleNewNetwork);
46 ethereum.on("accountsChanged", (newAccounts) => {
47 handleNewAccounts(newAccounts);
48 updateBalances();
49 });
50
51 // any time accounts changes, toggle state in the Connect button
52 useEffect(() => {
53 setIsConnected(accounts && accounts.length > 0);
54 }, [accounts]);
55
56 useEffect(() => {
57 if (isConnected) {
58 setButtonText("Connected");
59 setButtonDisabled(true);
60 } else {
61 setButtonText("Connect");
62 setButtonDisabled(false);
63 }
64 }, [isConnected]);
65
66 // any time accounts changes, update the balances by checking blockchain again
67 useEffect(() => {
68 updateBalances();
69 }, [accounts]);
70
71 // helper function to protect users from using the wrong network
72 // disables the withdraw/deposit buttons if not on Sepolia
73 async function handleNewNetwork() {
74 const chainId = await ethereum.request({
75 method: "eth_chainId",
76 });
77 console.log(chainId);
78 if (chainId != 0xaa36a7) {
79 setOnRightNetwork(false);
80 } else {
81 setOnRightNetwork(true);
82 }
83 }
84
85 // uses MM SDK to query latest Faucet and user wallet balance
86 async function updateBalances() {
87 if (accounts.length > 0) {
88 const faucetBalance = await ethereum.request({
89 method: "eth_getBalance",
90 params: [FAUCET_CONTRACT_ADDRESS, "latest"],
91 });
92 const walletBalance = await ethereum.request({
93 method: "eth_getBalance",
94 params: [accounts[0], "latest"],
95 });
96 setFaucetBalance(Number(faucetBalance) / 10 ** 18);
97 setWalletBalance(Number(walletBalance) / 10 ** 18);
98 }
99 }
100
101 // handles user connecting to MetaMask
102 // will add Sepolia network if not already a network in MM
103 // will switch to Sepolia (bug, can't currently do this)
104 async function onClickConnect() {
105 try {
106 const newAccounts = await ethereum.request({
107 method: "eth_requestAccounts",
108 });
109 handleNewAccounts(newAccounts);
110 try {
111 await window.ethereum.request({
112 method: "wallet_switchEthereumChain",
113 params: [{ chainId: SEPOLIA_CHAIN_ID }], // Check networks.js for hexadecimal network ids
114 });
115 } catch (e) {
116 if (e.code === 4902) {
117 try {
118 await window.ethereum.request({
119 method: "wallet_addEthereumChain",
120 params: [
121 {
122 chainId: SEPOLIA_CHAIN_ID,
123 chainName: "Sepolia Test Network",
124 rpcUrls: [
125 "https://eth-sepolia.g.alchemy.com/v2/amBOEhqEklW5j3LzVerBSnqoV_Wtz-ws",
126 ],
127 nativeCurrency: {
128 name: "Sepolia ETH",
129 symbol: "SEPETH",
130 decimals: 18,
131 },
132 blockExplorerUrls: ["https://sepolia.etherscan.io/"],
133 },
134 ],
135 });
136 } catch (e) {
137 console.log(e);
138 }
139 }
140 }
141 setIsConnected(true);
142 handleNewNetwork();
143 console.log(onRightNetwork);
144 } catch (error) {
145 console.error(error);
146 }
147 }
148
149 // hook to change state
150 function handleNewAccounts(newAccounts) {
151 setAccounts(newAccounts);
152 }
153
154 // withdraws .01 ETH from the faucet
155 async function withdrawEther() {
156 console.log(ethereum);
157 // const provider = await alchemy.
158
159 const tx = await faucetContract.withdraw({
160 from: accounts[0],
161 });
162 await tx.wait();
163 updateBalances();
164 }
165
166 // deposits .01 ETH to the faucet
167 async function depositEther() {
168 const signer = await provider.getSigner();
169
170 const tx_params = {
171 from: accounts[0],
172 to: FAUCET_CONTRACT_ADDRESS,
173 value: "10000000000000000",
174 };
175
176 const tx = await signer.sendTransaction(tx_params);
177 await tx.wait();
178 updateBalances();
179 }
180
181 return (
182 <div className="container">
183 <div className="wallet-container">
184 <div className="wallet wallet-background">
185 <div className="balance-container">
186 <h1 className="address-item">My Wallet</h1>
187 <p className="balance-item">
188 <b>Address:</b> {accounts[0]}
189 </p>
190 <p className="balance-item">
191 <b>Balance:</b> {walletBalance} Ξ
192 </p>
193 <button
194 className="button"
195 onClick={onClickConnect}
196 disabled={buttonDisabled}
197 >
198 {buttonText}
199 </button>
200 </div>
201 </div>
202 </div>
203 <div className="faucet-container">
204 <h1 className="title">My Faucet dApp! 🚰</h1>
205 <p className="subtitle">
206 This faucet allows you or your friends to withdraw .01 SepoliaETH per
207 account!
208 </p>
209 <h2 className="balance-item">Faucet Balance: {faucetBalance}Ξ</h2>
210 <div className="button-container">
211 <button
212 onClick={withdrawEther}
213 className="button"
214 disabled={!isConnected || !onRightNetwork}
215 >
216 Withdraw .01 ETH
217 </button>
218 <button
219 onClick={depositEther}
220 className="button"
221 disabled={!isConnected || !onRightNetwork}
222 >
223 Deposit .01 ETH
224 </button>
225 </div>
226 </div>
227 <div className="footer">
228 {isConnected
229 ? "Connected to MetaMask ✅"
230 : "Not connected to MetaMask ❌"}
231 </div>
232 </div>
233 );
234}
235
236export default App;

Let’s do a quick breakdown of the important code components above, starting with:

The MetaMask SDK initialization:

js
1// MetaMask SDK initialization
2const MMSDK = new MetaMaskSDK();
3const ethereum = MMSDK.getProvider();

Easy… 2 lines of code! 💥

Creating a Contract Instance instance using ethers.js

js
1// Faucet contract initialization
2const provider = new ethers.providers.Web3Provider(ethereum, "any");
3const FAUCET_CONTRACT_ADDRESS = "0x9BdCbC868519Cf2907ECE4E9602346c3fC9e6c8e";
4const abi = `[
5 {
6 "inputs": [],
7 "name": "withdraw",
8 "outputs": [],
9 "stateMutability": "nonpayable",
10 "type": "function"
11 },
12]`;
13const faucetContract = new ethers.Contract(
14 FAUCET_CONTRACT_ADDRESS,
15 abi,
16 provider.getSigner()
17);

This code snippet:

  • initializes a provider by plugging in the ethereum object (fetched using the MM SDK) into the ethers Web3Provider
  • declares the FAUCET_CONTRACT_ADDRESS (can be found here), the abi of the Faucet’s withdraw method (necessary for our website to be able to communicate with the smart contract)
  • using all of the above, a faucetContract instance is created - this will be used to interact directly with the smart contract

Calling the Faucet’s withdraw function

js
1// withdraws .01 ETH from the faucet
2 async function withdrawEther() {
3 console.log(ethereum);
4 // const provider = await alchemy.
5
6 const tx = await faucetContract.withdraw({
7 from: accounts[0],
8 });
9 await tx.wait();
10 updateBalances();
11 }

Using the faucetContract instance, you can directly call the withdraw function.

Note: the abi we are using is a minimal version that does not contain the entire contract interface! 👀

Donating .01 ether to the Faucet

js
1// deposits .01 ETH to the faucet
2 async function depositEther() {
3 const signer = await provider.getSigner();
4
5 const tx_params = {
6 from: accounts[0],
7 to: FAUCET_CONTRACT_ADDRESS,
8 value: "10000000000000000",
9 };
10
11 const tx = await signer.sendTransaction(tx_params);
12 await tx.wait();
13 updateBalances();
14 }

Since the Faucet contract contains a receive function, we can send it some ether as if it was any regular account. ⬆️

5. Add Final Styling 🎨

Your application must look like a total mess right now! No worries, let’s add some basic styling to make it look a little better!

  1. In your App.css file, copy-paste the following code snippet:
css
1.container {
2 display: flex;
3 justify-content: center;
4 align-items: center;
5 height: 100vh;
6 font-family: sans-serif;
7 background-color: #fafafa;
8}
9
10.wallet-container {
11 background-color: #f5f5f5;
12 border-radius: 10px;
13 box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
14 padding: 20px;
15 margin-right: 20px;
16 min-width: 300px;
17}
18
19.wallet {
20 display: flex;
21 flex-direction: column;
22 align-items: center;
23}
24
25.wallet .account-address {
26 font-size: 20px;
27 margin: 0;
28}
29
30.wallet .balance-container {
31 display: flex;
32 flex-direction: column;
33 margin-top: 10px;
34 text-align: center;
35}
36
37.balance-item {
38 font-size: 20px;
39 margin-top: 10px;
40}
41
42.faucet-container {
43 display: flex;
44 flex-direction: column;
45 justify-content: center;
46 align-items: center;
47}
48
49.title {
50 font-size: 32px;
51 margin: 0;
52 margin-bottom: 10px;
53 text-align: center;
54}
55
56.subtitle {
57 font-size: 20px;
58 margin: 0;
59 margin-bottom: 20px;
60 text-align: center;
61}
62
63.balance-item {
64 font-size: 20px;
65 margin: 0;
66 margin-bottom: 10px;
67 text-align: center;
68}
69
70.button-container {
71 display: flex;
72 justify-content: center;
73 align-items: center;
74}
75
76.button {
77 background-color: #4285f4;
78 border: none;
79 border-radius: 5px;
80 color: white;
81 cursor: pointer;
82 font-size: 20px;
83 margin: 10px;
84 padding: 10px;
85 text-align: center;
86 text-decoration: none;
87 transition: all 0.2s ease-in-out;
88}
89
90.button:hover {
91 background-color: #ff9933;
92}
93
94.button:active {
95 background-color: #ff9933;
96 transform: translateY(2px);
97}
98
99.button[disabled] {
100 background-color: #87aff9;
101 cursor: not-allowed;
102}
103
104.connect-button-container {
105 display: flex;
106 justify-content: center;
107 align-items: center;
108 margin-top: 20px;
109}
110
111.footer {
112 position: absolute;
113 bottom: 20px;
114 left: 0;
115 right: 0;
116 text-align: center;
117 font-size: 16px;
118}

6. Final Application View

final-app

Woot woot!! You’ve just set up a full-stack decentralized application thanks to the help of the MetaMask SDK.

Feel free to withdraw or deposit to the faucet as much as you want, although please note an account can only ever withdraw once from the test faucet!

7. Final Challenges

If you want to continue improving your skills, here are some suggestions to make this dApp better:

  • What other powers can the MetaMask SDK provide your application?
  • Calling all front-end gurus! The styling needs some work!
  • Can you completely replace the smart contract used in this application? It shouldn’t be too difficult, everything is there as a template for you - just replace the important details such as CONTRACT_ADDRESS and abi.

Congrats on building a full-stack dapp!! Feel free to use this as a template for more dapp-building! 🔥