DEV Community

Dustin Runnells
Dustin Runnells

Posted on

Web3 UI For Simple Smart Contract

Let's build a web frontend to a smart contract! This is a followup to my previous post about creating a simple smart contract with Solidity and Hardhat. The instructions here assume you are picking up up with the same contract that we just deployed to our Hardhat environment.

In the last post we created and tested a contract that will increment a counter stored in a state variable. Using the Hardhat console we called the incrementCount() and getCount() functions. In the real world, interfacing with a contract won't be through a development console. One way to create an application that calls these functions will be via Javascript (via the ethers.js library) in a web page - a Web3 application!

As stated in the previous post, interacting with web3 applications requires a browser that has a built in wallet. In this simple example we'll use Metamask. Metmask comes pre-configured for Etherium and maybe a few other EVM based blockchains, but not our simulated blockchain in the Hardhat environment. To get this all running, we are going to first setup Metmask, then create the HTML/Javascript needed to call our contract.

Metamask / Web3 Browser

  1. Install Metamask. I'll use the Chrome extension found here. If you are a Chrome user, this will let you now view and interact with web3 content.

    I won't walk you through the initial setup, but you will probably be prompted to import an existing private key or generate a new one and write down the recovery phrase. Do that.

  2. Next we'll add the Hardhat network to Metamask. Metamask supports any EVM you'd like, but it needs to be configured to do so. Usually this is just a matter of adding the chain ID and RPC URL. From inside Metamask (you might need to start it by clicking on your Chrome plugins and selecting it) you should see your public address in the top middle. To the left of your address there will be a dropdown that shows the current network. Click that to see what other networks are available:

    Image description

    Click "Add Custom Network". Fill in the Network Name with something like "Hardhat", the Network RPC URL with the IP address and Port of your Hardhat Node, probably something like this if you are running it locally:
    http://127.0.0.1:8545/

    Enter the Chain ID of 1337 and the symbol can just be ETH for now. Note that we are not dealing with real ETH on the real Ethereum network, but be very careful to stay on our Hardhat network if you have real ETH in your wallet.

    Now switch to the Hardhat Network that we just added in the Metamask plugin. In your terminal that is monitoring your running Hardhat node, you should see some activity as your wallet connects.

  3. Since your Metamask wallet doesn't currently have any (fake) ETH, let's send it some. Get your public address from Metamask (at the top of the Metamask window, under the wallet's name, click the copy button). From your terminal window that is running the Hardhat console, do:

    const [owner,  feeCollector, operator] = await 
    ethers.getSigners();
    await owner.sendTransaction({ to: "PasteYourMetamaskAddressHere", value: ethers.parseEther("0.1") });
    

    If you go back to Metamask, you should see that you now have some ETH in your Hardhat wallet! Now we are ready to do some web3 transactions on our Hardhat network.

Create a Web3 Webpage

  1. Let's create a simple webpage to view and increment our counter. I'm not going to use any heavy frameworks, just plain old HTML, Javascript and the ethers.js library. However, you won't be able to just point the browser to a .htm document, you'll need to be running a webserver somewhere for the Metamask plugin to work. Depending on your OS, you might be able to use a lightweight server like http-server or something locally.

    We'll need a few things from when we deployed our contract in the previous post. Refer back to the last post and grab the contract address and contract's ABI JSON array from the artifacts directory. We don't need the rest of the JSON from that file, just what is in the "abi" property, it should start with a [ and end with a ] and look something like this:

    [
        {
          "inputs": [],
          "stateMutability": "nonpayable",
          "type": "constructor"
        },
        {
          "inputs": [],
          "name": "getCount",
          "outputs": [
            {
              "internalType": "uint256",
              "name": "",
              "type": "uint256"
            }
          ],
          "stateMutability": "view",
          "type": "function"
        },
        {
          "inputs": [],
          "name": "incrementCount",
          "outputs": [],
          "stateMutability": "nonpayable",
          "type": "function"
        }
      ]
    
  2. Let's put this into some HTML and Javascript:

    <html>
        <head>
                <script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/5.2.0/ethers.umd.min.js" type="application/javascript"></script>
        </head>
    
        <body>
                <h2>The counter is at: <span id="counterValue"></span></h2>
                <br>
                <input type=button id="incrementBtn" value="Increment" onClick="incrementButtonClicked();">
    
    <script>
    const contractAbi = [ {
                "inputs": [],
                "stateMutability": "nonpayable",
                "type": "constructor"
        },
        {
                "inputs": [],
                "name": "getCount",
                "outputs": [
                        {
                                "internalType": "uint256",
                                "name": "",
                                "type": "uint256"
                        }
                ],
                "stateMutability": "view",
                "type": "function"
        },
        {
                "inputs": [],
                "name": "incrementCount",
                "outputs": [],
                "stateMutability": "nonpayable",
                "type": "function"
        } ]; // Replace with your contract's ABI
    
    var contractAddress = '0x5fbdb2315678afecb367f032d93f642f64180aa3'; // Replace with your contract's deployed address
    var signer;
    var contract;
    var contractWithSigner;
    
    connectMetaMask().then(function(provider) {
        signer = provider.getSigner();
        contract = new ethers.Contract(contractAddress, contractAbi, provider);
        contractWithSigner = contract.connect(signer);
        console.log(contractWithSigner);
        estimateGas(contractWithSigner);
        refreshCount();
    });
    
    async function connectMetaMask() {
        var provider = new ethers.providers.Web3Provider(window.ethereum);
        await provider.send("eth_requestAccounts", []);
        return provider;
    }
    
    async function getCountFromContract(contract) {
        console.log('Calling getCount...');
        var result = await contract.getCount();
        return result.toNumber();
    }
    
    async function incrementCount(contractWithSigner) {
        console.log('Calling incrementCount...');
        var result = await contractWithSigner.incrementCount();
        console.log('incrementCount RESULT: ');
        console.log(result);
        console.log('Waiting for transaction to be mined...');
        await result.wait(); // Wait for the transaction to be mined
        console.log('Mined.');
    }
    
    async function estimateGas(contractWithSigner) {
        console.log('Estimating Gas...');
        var estimatedGas = await contractWithSigner.estimateGas.incrementCount();
        console.log('ESTIMATED GAS RESULT:');
        console.log(estimatedGas.toNumber());
    }
    
    function refreshCount() {
        getCountFromContract(contractWithSigner).then(curCount => {
                console.log('getCount RESULT: ' + curCount);
                document.getElementById("counterValue").innerHTML = curCount;
        });
    }
    
    async function incrementButtonClicked() {
        await incrementCount(contractWithSigner);
        refreshCount();
    }
    
    </script>
    
        </body>
    </html>
    
  3. We should now be able to view our HTML document in a web browser with the Metamask plugin installed. I won't go through the Javascript, but if you are familiar with JavaScript and following the concepts and what we did in the Hardhat terminal previously, what is happening in the code should be fairly straight-forward. Metamask should prompt you that you are connecting to the site and you'll need to select the Hardnet network that we set up earlier. You should see something like this in the browser:

    Image description

  4. If all went well, you can click on the "Increment" button. Metamask will let you know that you are about to make a transaction and inform you of the gas fee. You can Confirm this transaction in Metamask and see the count increment on both the website and in the terminal where you have the hardhat node running!

  5. Congratulations, we are interacting with our contract through a web UI!

A few notes as you dive deeper into Hardhat and Metamask for development:

  • Each transaction has an nonce. When you reset your hardhat node, that nonce gets reset and you might loose sync with what Metamask thinks is a unique nonce. When that happens, Metmask has an option to set a custom nonce with the transaction, or you can reset Metamask's nonces in Settings->Advanced->Clear Activity Tab data.

  • You'll need to redeploy your smart contract every time you restart your Hardhat node.

  • If you are writing contracts that will keep track of users by their public address and want to experiment in the Hardhat console with transactions form different users, you can impersonate different addresses in the console that were displayed when you first started the Hardhat node with something like this before you connect to the contract:

    const signers = await ethers.getSigners();
    const newSigner = signers[1]; // Change the 1 to a different number that corolates with one of the pre-generated testing addresses
    const newMain = await main.connect(newSigner);
    
    newMain.setContractAddress("test","0xYourContractAddress");
    

Top comments (0)