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:
- Create and enter a directory on your machine where you see best fit.
mkdir SimpleStorage && cd SimpleStorage
- Create an
npm
project.
npm init -y
- Install Hardhat by running the following command in your terminal:
npm install --save-dev hardhat
- Initialize the Hardhat project by running the following command in your terminal:
npx hardhat init
- 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
- Create a new file called
HelloWorld.sol
in thecontracts
directory. This file will contain the smart contract code.
touch contracts/SimpleStorage.sol
- 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.
- First let's configure out
hardhat.config.js
file with the instructions from Hardhat Configuration. - Next, let's compile our code:.
npx hardhat compile
- 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
.
- 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
- 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
- Now, lets run our
deploy.js
script and deploy to to ourlocalhost
network, ran byhardhat node
:
npx hardhat run --network localhost scripts/deploy.js
- 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"
},
}
- 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:
- 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.**
- Grab the private key from the first account and import it into your TEST development wallet.
- click on MetaMask
- click on the multi-colored orb on the upper right hand side
- click on import account
- copy and paste the private key from the terminal into MetaMask.
- AGAIN DO NOT USE THIS WALLET OR ACCOUNT FOR PERSONAL USE.
- DO NOT ADD PERSONAL FUNDS TO THIS WALLET
- 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 runninghardhat 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.