DEV Community

Cover image for Connecting to different web3 wallets using Wagmi.sh and ReactJS
Murat Can Yüksel
Murat Can Yüksel

Posted on

Connecting to different web3 wallets using Wagmi.sh and ReactJS

Hello everyone! In this post, I'll show you how to connect to different web3 wallets using Wagmi.sh. In the course of this tutorial, we'll write a basic smart contract using Solidity, create and deploy the project using Hardhat, interact with it using EthersJS on top of React, deploy the frontend to GhPages and hide our sensitive data with env variables.

So, first off, let me tell you about what is Wagmi.sh, what we'll use it for, and why would we need it anyway?

Introduction to Wagmi.sh and the project we're going to build

According to their website (https://wagmi.sh/) "wagmi is a collection of React Hooks containing everything you need to start working with Ethereum." What interests me initially is that we can connect to different web3 wallet providers easily using wagmi. Although there are other libraries that does the same thing, I've found wagmi to be really easy to use. They also have a library comparison page where they outline their pros and cons (https://wagmi.sh/docs/comparison). The biggest pro for me was that using wagmi with Ethers.js is a breeze. Wagmi supports the following wallet providers: Metamask, Coinbase Wallet, WalletConnect, and Injected.

Now, the project we're going to be a fairly simple one. When the app opens the first time, we will be presented with different buttons signifying their respective wallet providers. We can choose any one of them to connect to the app. Here's the first screen we see.

Image description

Once connected, we'll see an input field and a button that says "Wave". If we enter a name and hit the "Wave" button, the wallet provider will be triggered and we'll be asked to pay for the transaction. After the transaction is complete, the name we've entered will be displayed on the screen.

At any time we can disconnect from the chosen wallet provider and connect with another one.

We will use the Goerli test network for this application.

Part One: Creating the backend

Writing and Deploying the Smart contract with Hardhat

Let's start our application by creating an empty Hardhat project. We'll use Hardhat to deploy our smart contract into the Goerli test network. We'll use the solidity language to write our smart contract.

To start, let's create an empty folder. We'll keep both our smart contract and frontend in this folder for ease of use. Since I use Linux, I'll create a folder named wagmi-project by typing mkdir wagmi-project in the terminal. I'll cd into the folder and run the following command on the terminal to create a package.json file: yarn init -y (if you use npm, you'd enter npm init -y instead). Once that's done, I'll install hardhat with entering yarn add --dev hardhat in the terminal. Then, I'll start a new hardhat project by entering npx hardhat in the terminal and choose to create a new Javascript project. Then I'll add some dependencies with yarn add --dev @nomiclabs/hardhat-waffle@^2.0.0 ethereum-waffle@^3.0.0 chai@^4.2.0 @nomiclabs/hardhat-ethers@^2.0.0 ethers@^5.0.0. That's it for the hardhat setup. Now I'll just delete the folders inside contracts, scripts and test folders that we've just created.

Then I'll enter yarn add dotenv as I'll use environment variables later on to protect sensitive data such as my private wallet key.

After all that's done, I go to my hardhat.config.js file and change it's contents to the following:

require("@nomiclabs/hardhat-waffle");
require("dotenv").config();

module.exports = {
  solidity: "0.8.0",
  networks: {
    goerli: {
      url: `${process.env.ALCHEMY_GOERLI_URL}`,
      accounts: [`${process.env.MY_PRIVATE_KEY}`],
    },
  },
};

Enter fullscreen mode Exit fullscreen mode

As you can see, there are some things going on here. Apart from requiring waffle from hardhat and dotenv library for env variables, I also specify the Solidity compiler version, the network I'm going to use (goerli test network in our case), and pass my sensitive data that's been cached from unwanted attention by env variables. I'll show how to find, add, and hide those env variables in the next step.

Adding env variables to hide sensitive data

Alchemy API

To start with, I'll create an empty .env file. I will place this file in the wagmi-frontend folder I'll create soon. You can just create is somewhere else right now and move it to that forlder later on. Now to populate it, I'll need some external help from Alchemy. If you don't have an Alchemy account, please go to https://www.alchemy.com/ and create one (it's free). Now, since we have our Alchemy account, we'll go to our dashboard and create a new project by clicking the +create app button on the top right. While creating the app, it is of utmost importance to choose Goerli for our network, because we're not going to use the Ethereum Mainnet as we don't want to spend real money on practice. Now that we've created our application on Alchemy, we'll click the view key button on the right of the project's tab and copy-paste the API KEY and HTTPS sections. We will need both of them for our project.

Metamask private key

Now, for this step you need a metamask account with some fake money for Goerli test network in it. To continue with the tutorial, please install the Metamask extension on your browser, request some test money from a faucet like this one https://goerlifaucet.com/ (you can login and request money using your Alchemy account since you have one now)

NB: Please use a burner wallet. That is to say, never put real money in this wallet, never use it for production or any other purpose than practice. It is better just to forget about and never use this particular wallet after the tutorial.

Now, to get the private key, click on the three vertical dots on the right side of the metamask extension and choose account details. there, you'll see a button that'll direct you to your private key. Enter your password there and get the private key. Once you copy your private key, let's go to our .env file and paste all the info we've got so far there. It should look something like the following:

ALCHEMY_GOERLI_URL=https://eth-goerli.g.alchemy.com/somethingsomething
MY_PRIVATE_KEY= somethingsomethingmyprivatekey
ALCHEMY_ID=sthsthmyalchemyID
Enter fullscreen mode Exit fullscreen mode

Don't show the info in your .env file to anyone, and if you're going to use GitHub for your project, please check your .gitignore file includes .env files so that you won't post these vital info to the public. People even steal fake money, I've heard it.

With the .env file populated, we can now write our smart contract for deployment.

Writing the Smart Contract

Now that we've set up our project structure, we need a smart contract to deploy. Let's create a Wave.sol file in the contracts folder we've created by initiating a hardhat project and paste the following contract inside of it:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Wave {
    string public name = "Murat";

     function setName(string memory _name) external {

        name = _name;

    }
}
Enter fullscreen mode Exit fullscreen mode

What's going on here? First of all, .sol extension indicated that this is a Solidity file, i.e. the code written inside of this file is with Solidity language. In the first line I specify what kind of license I'm using (MIT in this case, open source), and the compiler version for Solidity. In this case, it means anything above 0.8.0 passes.

I start my contract with the contract keyword and give it a name of Wave. Inside of it I define a public string and give it an initial value of Murat, my name. You can give whatever you want obviously. Then, I write a function called setName and give it a parameter of type string which is saved in the memory. Then, I make it external so that it can be called from outside of the contract. All this function does is change the value of the public string taking the value that's put in the function parameter. Notice that the function parameter starts with an underline _name. This is not necessary, but is a convention used in the Solidity community. It helps us to discern between what is an actual variable and what's a parameter per se.

Do not worry too much about the Solidity code for now, this tutorial doesn't focus on writing Solidity smart contracts.

Now we're finished with the smart contract, let's tie it all together and deploy it to the test network!

Deploying the Smart Contract

In order for these steps to work, you should be using a supported version of nodeJS for hardhat. It is generally the LTS version.

Go to the root of the project, and enter npx hardhat compile in the terminal. If everything goes alright, you'll be prompted with Compiled 1 Solidity file successfully, if not, enter npx hardhat clean and redeploy until it's succesfull.

Now if you check your folder structre again, you'll see there's a new folder called artifacts. If you go to the contracts folder inside of that artifacts folder, you'll see a .json file with the name you've gave to your smart contract. We will use this file later on, it is our contract abi (application binary interface).

Now, let's go back and cd into our scripts folder and add a deploy.js file with the following code inside of it:

const hre = require("hardhat");

async function main() {
  // We get the contract to deploy
  const [owner] = await hre.ethers.getSigners();
  //Here make sure you enter the name of the .sol file you've created in the contracts folder.
  const WaveContractFactory = await hre.ethers.getContractFactory("Wave");
  const WaveContract = await WaveContractFactory.deploy();
  await WaveContract.deployed();

  console.log("WaveContract deployed to:", WaveContract.address);
  console.log("WaveContract owner address:", owner.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
Enter fullscreen mode Exit fullscreen mode

The above code will make sure our code is deployed. As I've mentioned in the comments, just make sure you've entered the name of the smart contract you've written previously.

Now, we write the following into the terminal npx hardhat run scripts/deploy.js --network goerli

If succcesful, you'll be prompted with something like:

WaveContract deployed to: somehweresomething
WaveContract owner address: somehweresomethingelse
Enter fullscreen mode Exit fullscreen mode

Save these addresses in somewhere safe and hidden, we will use them later on.

Congrats! You've just deployed a smart contract to the blockchain! You can even check your contract by going to https://goerli.etherscan.io/ and pasting the address that this contract was deployed!

Even for a simple contract, that was a lot of moving parts, so pat yourself on the back. Now, we'll get into the frontend stuff, i.e. how are we going to make the users interact with the contract we've just deployed.

Part Two: Creating the Frontend

Installing the dependencies

I'll use reactJS in this project, so let's go to our root folder and create a react project by entering npx create-react-app wagmi-frontend in the terminal.

After that's done our folder structure should be something like the following:

-Wagmi-project (root)
--artifacts
--cache
-contracts
-node_modules
-scripts
-test
-wagmi-frontend
-.gitignore
-hardhat-config.js
-package.json
-README.md
-yarn.lock
Enter fullscreen mode Exit fullscreen mode

Now, we'll cd into this new wagmi-frontend folder and install ethersJS as well as wagmi.sh. To install ethersJS library, I write the following command into the terminal yarn add ethers, and to install wagmi.sh I write the following yarn add wagmi ethers.

I also install buffer since node polyfills are not bundled with webpack anymore, to install this package is the easiest solution I've found to work with the error we'd see if we didn't have buffer. To install buffer, I write the following to command line yarn add buffer. Then, I'll go to my index.js file inside wagmi-frontend/src/ and require buffer as such window.Buffer = require("buffer/").Buffer; at the top of the component. Now my index.js file looks like this:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
window.Buffer = require("buffer/").Buffer;
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Enter fullscreen mode Exit fullscreen mode

Lastly, I'll install gh-pages as I want to deploy the project to Github Pages. To do that, I write the following command into the terminal yarn add gh-pages.

Now we're done with installing the dependencies, we can see how to use them to create a frontend to interact with our smart contract.

Writing the frontend

Since we're inside of our wagmi-frontend folder, our folder structure (excluding anything that's other than wagmi-frontend) should look like the following:

-node_modules
-public
-src
-.env
-.gitignore
-hardhat-config.js
-package.json
-README.md
-yarn.lock
Enter fullscreen mode Exit fullscreen mode

Now I'll add a couple of more folders and files into this wagmi-frontend folder. I'll add 2 folders named contracts and style, I'll also add a Profile.js file. Before proceeding any further, I'll create a Wave.json file in the contracts folder I've just created. To populate this file, I need to go back to my root folder (not the wagmi-frontend folder we're currently in, but the folder that contains it also). There, you'll remember we had an artifacts folder, I'll cd into it and find the contracts folder. Inside of the contracts folder, there's this Wave.json file that's been created by hardhat. I'll copy everthing inside of it.

After copying the contents of that Wave.json file, I'll go back to the wagmi-frontend folder, go inside src, then to contracts folder there and create another Wave.json file. I'll paste everything I've copied inside of this file. This file contains the abi of our smart contract.

In the style folder, I'll create a profile.css file and add the following contents inside of it (don't worry about this at all, it's just to make the buttons look a bit nicer. This is not a post about writing CSS after all.):

.btn {
  display: flex;
  flex-wrap: wrap;
  width: 150px;
  margin-bottom: 10px;
  padding: 5px;
}

Enter fullscreen mode Exit fullscreen mode

I'll also go into the App.css file in the src folder and add the following contents:

.App {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background: black;
  color: white;
  height: 100vh;
}

Enter fullscreen mode Exit fullscreen mode

Now I can go into App.js file, delete everything inside of it and write my own code.

App.js

I'll start by importing the dependencies for the application:

import { useState } from "react";
import { ethers, utils } from "ethers";
import abi from "./contracts/Wave.json";
import Profile from "./Profile";
import "./App.css";
import {
  WagmiConfig,
  createClient,
  defaultChains,
  configureChains,
} from "wagmi";

import { alchemyProvider } from "wagmi/providers/alchemy";
import { publicProvider } from "wagmi/providers/public";

import { CoinbaseWalletConnector } from "wagmi/connectors/coinbaseWallet";
import { InjectedConnector } from "wagmi/connectors/injected";
import { MetaMaskConnector } from "wagmi/connectors/metaMask";
import { WalletConnectConnector } from "wagmi/connectors/walletConnect";
Enter fullscreen mode Exit fullscreen mode

You see that I'll use useState hook from React, ethers library, the contract abi we've saved in Wave.json file, a Profile component we'll create later on, App.css file, dependencies for wagmi which includes providers alongside with wallet connectors. After that's done, I'll add the following lines of code just under it:


const alchemyId = process.env.ALCHEMY_ID;

// Configure chains & providers with the Alchemy provider.
// Two popular providers are Alchemy (alchemy.com) and Infura (infura.io)
const { chains, provider, webSocketProvider } = configureChains(defaultChains, [
  alchemyProvider({ alchemyId }),
  publicProvider(),
]);

// Set up client
const client = createClient({
  autoConnect: true,
  connectors: [
    new MetaMaskConnector({ chains }),
    new CoinbaseWalletConnector({
      chains,
      options: {
        appName: "wagmi",
      },
    }),
    new WalletConnectConnector({
      chains,
      options: {
        qrcode: true,
      },
    }),
    new InjectedConnector({
      chains,
      options: {
        name: "Injected",
        shimDisconnect: true,
      },
    }),
  ],
  provider,
  webSocketProvider,
});

Enter fullscreen mode Exit fullscreen mode

These parts can also be found on the wagmi documentation. What we're doing here is asically, we take our Alchemy Id we've saved in our .env file and then set up the client for different wallet options. You see that we have Metamask, Coinbase, WalletConnector and Injected here. After we specified these parts, we can start writing our main function, so let's do that.

const App = () => {
  const [isWalletConnected, setIsWalletConnected] = useState(false);
  const [userName, setUserName] = useState("");
  const [displayedUserName, setDisplayedUserName] = useState("initial name");
  const [error, setError] = useState(null);
  const contractAddress = "theaddressthatourcontracthasbeendeployedtocomeshere";
  const contractABI = abi.abi;
  }
Enter fullscreen mode Exit fullscreen mode

As you can see, I have some constants for changing the state, the contract address we've got when we deployed our contract to the (test) blockchain, and the contract abi that we've imported from our Wave.json file.

Next, I'll add the necessary functions. Let's look at what they're doing:


  const handleInput = (name) => {
    setUserName(name.target.value);
  };

  const handleChild = (status) => {
    console.log(status);
    status ? setIsWalletConnected(true) : setIsWalletConnected(false);
  };

  const handleClick = async (e) => {
    e.preventDefault();
    if (window.ethereum) {
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner();
      const waveContract = new ethers.Contract(
        contractAddress,
        contractABI,
        signer
      );

      const txn = await waveContract.setName(
        utils.formatBytes32String(userName)
      );
      console.log("setting user name");
      await txn.wait();
      console.log("user name set", txn.hash);
      setDisplayedUserName(userName);
    }
  };
Enter fullscreen mode Exit fullscreen mode

handleInput function is easy to understand. We'll take the user's input and set it to the state and display it in the UI.
handleChild function is a bit more complicated as it is linked to the child component (Profile.js) we'll create in the next steps. It'll make more sense retrospectively, but the gist is that I'll send this function to the child as props, check if any wallet is connected there, and send the boolean data from child back to this function (which is in the parent component). That's what I do in the ternary operator (status ? setIsWalletConnected(true) : setIsWalletConnected(false);). Actually, I don't even need to write this ternary operator, I can just do away with setIsWalletConnected(true) too but I want to be as explicit as possible so that I wouldn't look at the screen for hours and hours when I want to go back to this piece of code after some time.

Next, handleClick function. It is asynchronous, and it checks if there's a wallet in the browser, then by using ethersJS library, it deals with providers and signers. txn is the transaction, it converts the userName to Bytes32 so that EVM(Ethereum Virtual Machine) can understand it. Then it waits for the transaction to be mined and then it sets the userName.

Now let's look at the last bits of our App.js component:

  let userInteraction;
  if (isWalletConnected === true) {
    userInteraction = (
      <div>
        <input onChange={handleInput} type="text" />
        <button onClick={handleClick}>Wave</button>
        <p> {displayedUserName} waved!</p>
      </div>
    );
  }

  return (
    <div className="App">
      <h1>Wagmi Project</h1>

      {userInteraction}
      <WagmiConfig client={client}>
        <Profile handleChild={handleChild} />
      </WagmiConfig>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

First, I define an empty userInteraction variable. Then depending on the connection status of the wallet, I populate it with the appropriate UI. If the wallet is connected, I'll display the input field and the button. If the wallet is not connected, they won't be there.

In our return statement, I pass the above conditional statement, and invoke our Profile.js child component with handleChild passed as props as I've mentioned earlier.

Just for making sure everything is in place, let's see the whole App.js component:

import { useState, useEffect } from "react";
import { ethers, utils } from "ethers";
import abi from "./contracts/Wave.json";
import Profile from "./Profile";
import "./App.css";
import {
  WagmiConfig,
  createClient,
  defaultChains,
  configureChains,
} from "wagmi";

import { alchemyProvider } from "wagmi/providers/alchemy";
import { publicProvider } from "wagmi/providers/public";

import { CoinbaseWalletConnector } from "wagmi/connectors/coinbaseWallet";
import { InjectedConnector } from "wagmi/connectors/injected";
import { MetaMaskConnector } from "wagmi/connectors/metaMask";
import { WalletConnectConnector } from "wagmi/connectors/walletConnect";

const alchemyId = process.env.ALCHEMY_ID;

// Configure chains & providers with the Alchemy provider.
// Two popular providers are Alchemy (alchemy.com) and Infura (infura.io)
const { chains, provider, webSocketProvider } = configureChains(defaultChains, [
  alchemyProvider({ alchemyId }),
  publicProvider(),
]);

// Set up client
const client = createClient({
  autoConnect: true,
  connectors: [
    new MetaMaskConnector({ chains }),
    new CoinbaseWalletConnector({
      chains,
      options: {
        appName: "wagmi",
      },
    }),
    new WalletConnectConnector({
      chains,
      options: {
        qrcode: true,
      },
    }),
    new InjectedConnector({
      chains,
      options: {
        name: "Injected",
        shimDisconnect: true,
      },
    }),
  ],
  provider,
  webSocketProvider,
});

const App = () => {
  const [isWalletConnected, setIsWalletConnected] = useState(false);
  const [userName, setUserName] = useState("");
  const [displayedUserName, setDisplayedUserName] = useState("initial name");
  const [error, setError] = useState(null);
  const contractAddress = "0x17c5A76a5D6db7740821425aFa029B3494DeecaB";
  const contractABI = abi.abi;

  const handleInput = (name) => {
    setUserName(name.target.value);
  };

  const handleChild = (status) => {
    console.log(status);
    status ? setIsWalletConnected(true) : setIsWalletConnected(false);
  };

  const handleClick = async (e) => {
    e.preventDefault();
    if (window.ethereum) {
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner();
      const waveContract = new ethers.Contract(
        contractAddress,
        contractABI,
        signer
      );

      const txn = await waveContract.setName(
        utils.formatBytes32String(userName)
      );
      console.log("setting user name");
      await txn.wait();
      console.log("user name set", txn.hash);
      setDisplayedUserName(userName);
    }
  };

  let userInteraction;
  if (isWalletConnected === true) {
    userInteraction = (
      <div>
        <input onChange={handleInput} type="text" />
        <button onClick={handleClick}>Wave</button>
        <p> {displayedUserName} waved!</p>
      </div>
    );
  }

  return (
    <div className="App">
      <h1>Wagmi Project</h1>

      {userInteraction}
      <WagmiConfig client={client}>
        <Profile handleChild={handleChild} />
      </WagmiConfig>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

We're done with the App.js component. I know it is a lot, but we're left one more component, the child, Profile.js. After that we're all done.

Writing the child component

I start by importing whatever needs to be imported at the beginning as such:

import { React, useEffect } from "react";
import {
  useAccount,
  useConnect,
  useDisconnect,
  useEnsAvatar,
  useEnsName,
} from "wagmi";
import "./style/profile.css";
Enter fullscreen mode Exit fullscreen mode

Next comes our child component's main function with handleChild that's been passed from the parent as props:

const Profile = ({ handleChild }) => {
  const { address, connector, isConnected } = useAccount();
  const { data: ensAvatar } = useEnsAvatar({ addressOrName: address });
  const { data: ensName } = useEnsName({ address });
  const { connect, connectors, error, isLoading, pendingConnector } =
    useConnect();
  const { disconnect } = useDisconnect();

  useEffect(() => {
    console.log(isConnected);

    if (isConnected === true) {
      handleChild(true);
    }
  });
Enter fullscreen mode Exit fullscreen mode

You'll notice that I use destructured props with curly braces around them, I find this to be more readable.

Here we have our constants that can be found in wagmi.sh documentation that helps us to use its hooks.

In the useEffect hook, I check if the user is connected. If they are, I send the data to parent by invoking the handleChild function. Remember, it does set the state of the wallet as connected in the parent component. Next, I have a conditional statement that checks if the user is connected. f they are, I display the address of the wallet that's connected, and the name of it (Metamask or Coinbase for instance):


  if (isConnected) {
    return (
      <div>
        {/* <img src={ensAvatar} alt="ENS Avatar" /> */}
        <div>{ensName ? `${ensName} (${address})` : address}</div>
        <div>Connected to {connector.name}</div>
        <button onClick={disconnect}>Disconnect</button>
      </div>
    );
  }
Enter fullscreen mode Exit fullscreen mode

To wrap it up, I have my return statement. It's pretty self-explanatory:

  return (
    <div>
      {connectors.map((connector) => (
        <button
          className="btn"
          disabled={!connector.ready}
          key={connector.id}
          onClick={() => connect({ connector })}
        >
          {connector.name}
          {!connector.ready && " (unsupported)"}
          {isLoading &&
            connector.id === pendingConnector?.id &&
            " (connecting)"}
        </button>
      ))}

      {error && <div>{error.message}</div>}
    </div>
  );
};

export default Profile;
Enter fullscreen mode Exit fullscreen mode

That's all, actually. Now, if we went to our wagmi-frontend folder and enter yarn start (or npm run start if you're using npm), we can see that the app is running.

Closing thoughts

Congratulations! You've finished the tutorial. You can find the source code for this tutorial here=> https://github.com/muratcan-yuksel/wagmi-post . I know there were a lot of moving parts and things to do but I hope you've enjoyed it. If you have any questions, feel free to reach out to me on Twitter or Github.

Keep calm & happy hacking!

Top comments (0)