10 min read

App - Simple Storage with React

Smart contracts in Ethereum are used to store data and execute code in a decentralized manner. In this blog post, we will walk through the creation of a simple storage smart contract that allows users to store and retrieve an integer value.

Project Setup

Setting up the Project

Now that we have a high level overview of the code, let's add our "Hello World" smart contract to a local project. Here are the steps to set up the project using Hardhat:

  1. Create and enter a directory on your machine where you see best fit.
mkdir SimpleStorage && cd SimpleStorage
  1. Create an npm project.
npm init -y
  1. Install Hardhat by running the following command in your terminal:
npm install --save-dev hardhat
  1. Initialize the Hardhat project by running the following command in your terminal:
npx hardhat init
  1. Choose create a JavaScript project and type y for each selection. Then delete the example files:
rm contracts/Lock.sol test/Lock.js scripts/deploy.js
  1. Create a new file called HelloWorld.sol in the contracts directory. This file will contain the smart contract code.
touch contracts/SimpleStorage.sol
  1. Let's add "Hello World" smart contract to contracts/HelloWorld.sol:
// SPDX-License-Identifier: MIT
// SimpleStorage.sol

pragma solidity 0.8.16;

contract SimpleStorage {
    uint256 storedData;

    function set(uint256 x) public {
        storedData = x;
    }

    function get() public view returns (uint256) {
        return storedData;
    }
}

Simple Storage Overview

To get started, we will define a contract called SimpleStorage that contains a state variable called storedData of type uint256. This will be used to store the integer value that we want to store and retrieve.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.16;

contract SimpleStorage {
    uint256 storedData;

    function set(uint256 x) public {
        storedData = x;
    }

    function get() public view returns (uint256) {
        return storedData;
    }
}

We will define a function called set that takes in an integer argument and sets the value of storedData to that integer.

function set(uint256 x) public {
    storedData = x;
}

The set function is marked as public, which means that it can be called by anyone on the Ethereum network. It takes in an integer argument called x and sets the value of storedData to that integer.

We will also define a function called get that returns the current value of storedData.

function get() public view returns (uint256) {
    return storedData;
}

The get function is marked as view, which means that it doesn't modify any state variables in the contract. It simply returns the current value of storedData.

Now that we have defined our smart contract, we can deploy it to the Ethereum network and interact with it using a web3 interface, such as Remix.

To set the value of storedData, we can call the set function and pass in an integer argument.

Testing

When developing smart contracts, it is important to write tests to ensure that the contract behaves as expected. Hardhat is a popular development environment for Ethereum that provides built-in support for testing smart contracts.

With Hardhat, we can easily create tests for our smart contracts and ensure that they behave as expected. By defining tests for the SimpleStorage.sol smart contract, we can ensure that the get and set functions work as intended and that the value of storedData is correctly stored and retrieved.

To get started, we will create a new file called SimpleStorage.test.js in the test directory. This file will contain the tests for our smart contract.

touch test/SimpleStorage.test.js

Now, add the following code the to the file.

const { expect } = require("chai");

describe("SimpleStorage contract", function () {
  let SimpleStorage;
  let simpleStorage;
  let owner;

  beforeEach(async function () {
    SimpleStorage = await ethers.getContractFactory("SimpleStorage");
    [owner] = await ethers.getSigners();
    simpleStorage = await SimpleStorage.deploy();
    await simpleStorage.deployed();
  });

  it("should set the value", async function () {
    const newValue = 42;
    await simpleStorage.set(newValue);
    const storedValue = await simpleStorage.get();
    expect(storedValue).to.equal(newValue);
  });

  it("should get the value", async function () {
    const initialValue = 10;
    await simpleStorage.set(initialValue);
    const storedValue = await simpleStorage.get();
    expect(storedValue).to.equal(initialValue);
  });

  it("should get the initial value", async function () {
    const storedValue = await simpleStorage.get();
    expect(storedValue).to.equal(0);
  });
});

Code Breakdown

First, we will import the necessary modules: ethers for interacting with the smart contract and uses expect function from the chai to write assertions about the behavior of the contract.

const { expect } = require("chai");
const { ethers } = require("hardhat");

Next, we will define a variable called simpleStorage that represents an instance of our smart contract. We will also define some sample values that we will use in our tests.

describe("SimpleStorage", function() {
//...

These lines declare three variables that we'll use throughout our tests. SimpleStorage will hold the contract factory that we'll use to deploy our contract, simpleStorage will hold an instance of our deployed contract, and owner will hold the first signer (i.e., account) that we get from ethers.getSigners().

let SimpleStorage;
let simpleStorage;
let owner;

This block of code is a Mocha beforeEach hook that runs before each test case in the suite. It sets up the environment for each test case by creating a new instance of the SimpleStorage contract, deploying it to the local network, and getting the owner account.

beforeEach(async function () {
  SimpleStorage = await ethers.getContractFactory("SimpleStorage");
  [owner] = await ethers.getSigners();
  simpleStorage = await SimpleStorage.deploy();
  await simpleStorage.deployed();
});

This is the first test case, which checks that the set function sets the stored value to a new value correctly. It uses the expect function to compare the returned value of the get function to the expected value.

it("should set the value", async function () {
  const newValue = 42;
  await simpleStorage.set(newValue);
  const storedValue = await simpleStorage.get();
  expect(storedValue).to.equal(newValue);
});

This is the second test case, which checks that the get function retrieves the current value correctly. It uses the expect function to compare the returned value of the get function to the expected initial value.

it("should get the value", async function () {
  const initialValue = 10;
  await simpleStorage.set(initialValue);
  const storedValue = await simpleStorage.get();
  expect(storedValue).to.equal(initialValue);
});

This is the third test case, which checks that the initial stored value is 0. It uses the expect function to compare the returned value of the get function to the expected initial value.

it("should get the initial value", async function () {
  const storedValue = await simpleStorage.get();
  expect(storedValue).to.equal(0);
});

Running the SimpleStorage Tests

Now, lets run our tests.

  1. First let's configure out hardhat.config.js file with the instructions from Hardhat Configuration.
  2. Next, let's compile our code:.
npx hardhat compile
  1. Then, let's test the code using the test command in Hardhat.
npx hardhat test

We should see the following:

  SimpleStorage contract
    ✔ should get the initial value
    ✔ should set the value
    ✔ should get the value
  3 passing (1s)

Deploying to local network

Note: that this code assumes you have installed Hardhat and it's dependencies, and that you have compiled the SimpleStorage contract with npx hardhat compile. You can run this script with the command npx hardhat run deploy.js.

  1. In order to deploy to any network, we must first create a deploy.js script in our /script directory. Then add the code above.
touch scripts/deploy.js
  1. Open a new terminal tab to start hardhat node:
    Note: Similar to running a local web server, keep this running to be able to interact with the contract. If the instance is closed, the local network will not remember your contract or its state, and you will have redeploy again.
npx hardhat node
  1. Now, lets run our deploy.js script and deploy to to our localhost network, ran by hardhat node:
npx hardhat run --network localhost scripts/deploy.js
  1. Since we may be running our deploy scripts often, let's create a script inside our package.json file to simplify things
{
  "name": "SimpleStorage",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "deploy-local": "npx hardhat run --network localhost scripts/deploy.js"
  },
  "devDependencies": {
    "@nomicfoundation/hardhat-toolbox": "^2.0.2",
    "@nomiclabs/hardhat-ethers": "^2.2.2",
    "ethers": "^5.7.2",
    "hardhat": "^2.13.0",
    "prettier": "2.8.4"
  },
}
  1. You should see the following output meaning that you contract deployment was successful:
    Note: the address (0x5FbDB...aa3) which the contract was deployed to may vary on your machine.
Deploying HelloWorld contract...
SimpleStorage contract deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3

Success! You have created, tested and deployed your first smart contract!

Front End

The SimpleStorage smart contract is a basic smart contract that allows a user to set and get a single value. In the previous blog post, we created and tested the SimpleStorage smart contract using Hardhat. Now, we will create a front-end application that allows users to interact with the smart contract.

Setting up the Environment

Before we begin, make sure you have the following installed the required tooling via Setting up Your Computer For Web3 Projects.

In addition to this, add this to your hardhat.config.js configuration file:

require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-ethers");

/** @type import('hardhat/config').HardhatUserConfig */

module.exports = {
solidity: "0.8.16", // or desired version of solidity
  networks: {
    localhost: {
      url: "http://127.0.0.1:8545"
    },
  },
  paths: {
    // gets around create-react-app's limitation on 
    // relative imports outside of src/ not being supported.
    // if you named your create-react-app project another name,
    // make sure to change the name of the path from front-end
    // to your projects name
    artifacts: "./front-end/src/artifacts"  
  }
};

The front end will live in the /src directory of the root folder /SimpleStorage. Let's create the /src directory via create-react-app.

This will create a new folder and package.json file for your project within /src for your front end project. Keep in mind to continue to use yarn for the entire project

npx create-react-app front-end

Next let's set up dotenv which can help keep our environment variables secret. This will be useful for our API keys which we will use to deploy our contracts to a test network. Again, do this within our front-end directory, not the root directory.

npm install env-cmd

Within our package.json file inside of our front-end directory, lets configure the npm start script to load our environment variables

"scripts": {
  "start": "env-cmd -f ../.env.address react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},

And let's create our .env.address file which will automatically get our updated deployed address we can use for our DApp. In the root of the ./SimpleStorage project create the .env.address file

touch .env.address

And lets modify our deploy.js file to output to our .env.address file

const fs = require('fs');
async function main() {
  const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
  const simpleStorage = await SimpleStorage.deploy();
  await simpleStorage.deployed();
  console.log("SimpleStorage contract deployed to:", simpleStorage.address);
  
  // .address.env file gets updated more regularly than other .env files
  // so better to keep as seperate file as to not overwrite values for main .env file
  const envConfigFile = '.env.address';
  
  const envConfig = `\nREACT_APP_CONTRACT_ADDRESS=${simpleStorage.address}`;
  fs.writeFileSync(envConfigFile, envConfig, { flag: 'w' });
}  

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Since we have a new package.json, lets install our ethers javascript library which will allow us to interact with Ethereum easily.

npm install ethers@5.4

Let's add add our one of our testnet's private key to MetaMask. To do so:

  1. Make sure the terminal is open and running hardhat node. If not run:
npx hardhat node

You should see the following accounts or something similar. Heed the warning NOT to place funds into these test accounts. This is why its VERY important to keep separate wallets for testing and personal use.

Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========
**WARNING: These accounts, and their private keys, are publicly known.**
**Any funds sent to them on Mainnet or any other live network WILL BE LOST.**

Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Account #1: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

Account #2: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a

... a bunch of accounts in-between

Account #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH)
Private Key: 0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e

**WARNING: These accounts, and their private keys, are publicly known.**
**Any funds sent to them on Mainnet or any other live network WILL BE LOST.**
  1. Grab the private key from the first account and import it into your TEST development wallet.
    1. click on MetaMask
    2. click on the multi-colored orb on the upper right hand side
    3. click on import account
    4. copy and paste the private key from the terminal into MetaMask.
    5. AGAIN DO NOT USE THIS WALLET OR ACCOUNT FOR PERSONAL USE.
    6. DO NOT ADD PERSONAL FUNDS TO THIS WALLET
    7. Hacker are known to set up scripts to drain wallets from test accounts who mistakenly add tokens to them. This is more common than you think. PLEASE label and segment your wallet use.
      Note: if you are having trouble with your wallet loading, try changing networks to clear MetaMask's cache, and changing back to local host. You may need to do this if you see this error in the terminal running hardhat node or if you see MetaMask's logo spinning and loading.
 Received invalid block tag 7. Latest block number is 0

Congratulations you now have ~1000 testnet ETH. They are worth absolutely nothing, but fun. This ETH will be used to interact with your test network.

Creating the SimpleStorage Front-End

Setting up the React Front End

Let's configure our React front end by removing the default code. In our App.js add the following:

import './App.css';
import SimpleStorage from './SimpleStorage.jsx';

function App() {
  return (
    <div className="App">
      <SimpleStorage/>   
    </div>
  );
}

export default App;

Now, let's create our SimpleStorage.jsx component in the same directory:

touch SimpleStorage.jsx

Let's open the SimpleStorage.jsx component and add the following code as the base. We will work to add methods to this code and explain it progressively.

import React, {useState} from 'react';  // useState lets variable remember what they are upon re-rendering

const SimpleStorage = () => {
  return (
    <div>
      <h1>SimpleStorage</h1>
      <h2>{"Getter and Setter interaction on a smart contract"}</h2>
    </div>
  );
}

export default SimpleStorage;

Conclusion

The Simple Storage contract and front-end example in this blog post demonstrate how to interact with a smart contract using a web interface and showcase the potential of blockchain technology in various industries. By following these steps and utilizing the tools and resources available, developers can create powerful and secure blockchain applications.

A simple storage smart contract can be used to store and retrieve data in a decentralized manner. By defining a state variable of type uint256 and using the set and get functions, we can easily store and retrieve an integer value on the Ethereum network.

Building a front-end for a Solidity smart contract using React is a great way to create a user-friendly and accessible application for interacting with blockchain technology. With React's powerful UI library and Solidity's flexible smart contract language, developers can create innovative and robust decentralized applications.

By following these steps and utilizing the tools and resources available, developers can create powerful and secure blockchain applications.

Feedback

Have feedback? Found something that needs to be updated, accurate, or explained?

Join our discord and share what can be improved.

Any and all feedback is welcomed.