Third Steps
So, you鈥檝e got the basics down. In the previous section, you developed a smart contract, and deployed it using Truffle. However, in the previous section, your smart contracts were deployed to a local development network 鈥 and that鈥檚 no fun, since only you can deploy things and interact with that local test network! We want friends! And access to other smart contracts that other people have deployed!
Therefore, in this section, we鈥檒l transition into utilizing a public Ethereum testnet, so you can join in on all of the action happening around the Ethereum ecosystem!
Let鈥檚 Get Started!
First, we鈥檙e going to talk about how you get access to these public Ethereum networks.
To access these networks, you need to connect to a node which is connected to the respective network. You can view each Ethereum network as its own little Ethereum world, and you can view an Ethereum node as your gateway or access point to each of those worlds! Because Ethereum is a distributed network, each Ethereum node stores the entire state of the network it is connected to (there are nodes that don鈥檛 need to store the full state, but don鈥檛 worry about that for now), and constantly communicates with the other nodes in the network to keep that state up-to-date! Therefore, to be able to read from and write to this state, we鈥檒l need to get access to one of these nodes.
You could very well host your own node using one of the many Ethereum clients currently available (Hyperledger Besu (Java client developed by ConsenSys), Geth (Go client), Parity (Rust client), etc.) 鈥 however, there is quite a bit of DevOps overhead that comes with hosting and maintaining your own Ethereum node 鈥 especially if you want to do it reliably! As such, we at ConsenSys have built Infura 鈥 a world class Ethereum infrastructure offering. Infura takes care of the whole 鈥榥ode management鈥 piece for you, providing you with instant, reliable, and scalable access to clusters of Ethereum nodes! You can think of Infura as 鈥淓thereum-nodes-as-a-Service鈥 馃檪
Getting Started With Infura
To get started with Infura, you鈥檒l want to register an account at infura.io. Don鈥檛 worry 鈥 it鈥檚 completely free to get started, and you won鈥檛 need to enter any sensitive information!
Once registered, you鈥檒l be directed to a page which looks like this:
As this page suggests, to get started, you鈥檒l select the first option 鈥淕et started and create your first project to access the Ethereum network!鈥
You can name your project whatever you鈥檇 like 鈥 we鈥檒l name ours 鈥渢est-project鈥.
Now, you鈥檒l be presented with the credentials you鈥檒l need to access the Infura nodes!
Keep this page open! We鈥檒l be coming back to it later 馃檪
The next thing we鈥檒l do is initialize a new Truffle project. If you need help installing Truffle, please refer to the previous section of this documentation.
To initialize a new Truffle project, create a new folder, and run the
truffle init
Next, you鈥檒l want to add the Truffle HD Wallet provider to your newly initialized project, so that you can sign your transactions before they are sent to the Infura nodes. Every state change that you make to Ethereum comes in the form of a transaction 鈥 whether it is deploying a contract, calling a function within a contract, or sending a token! Each transaction needs to be signed by an account 鈥 therefore, our application needs the ability to sign transactions so that it can make state changes to Ethereum!
Each transaction also costs ether. This transaction cost is referred to as the 鈥済as cost鈥. Therefore, in order to have our signed transactions be processed by the network once they are sent to the Infura nodes, we鈥檒l need to fund our account with some ether. We鈥檒l cover this a little later, but this is just another important reason why you鈥檒l need a wallet & wallet provider!
To add the Truffle HD Wallet provider to your newly initialized project type in your terminal:
npm install --save @truffle/hdwallet-provider
This may throw some warnings, but as long as it installs, you鈥檙e good to go!
Now we can create an Ethereum account for our application to use! Since our wallet provider is an HD (hierarchical deterministic) wallet, we can deterministically generate accounts using the same seed phrase, or mnemonic.
To create our account, we鈥檒l first need to start Ganache. Ganache is a Truffle product which allows us to easily create our own local dev network. To run ganache, simply type
ganache-cli
If you completed Step 2 of this guide, you should have Ganache / ganache-cli already installed 鈥 if you don鈥檛, you can install it using the npm command:
npm install -g ganache-cli
Or if you are using yarn聽
yarn global add ganache-cli
Next, we鈥檒l need to allow our app to talk to Ganache. Head over to your project directory and checkout the truffle-config.js file, simply uncomment (or add) the following lines under network:
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*" // Any network (default: none)
},
Nice! Now our app can talk to our Ganache development network running at 127.0.0.1:8545! Now, in a new terminal window (but still in your project folder), run the command
truffle console
聽to connect to your Ganache network. Don鈥檛 worry 鈥 we鈥檒l be connecting to a public network later! We just need to connect to Ganache right now to create our keys 馃檪
Note: If you are running into issues make sure in Ganache your RPC Server port number matches your truffle config file.聽In the default case, 8545 should work, otherwise change your config file to match Ganache.
Now enter the following commands in the Truffle console to create your wallet:
const HDWalletProvider = require('@truffle/hdwallet-provider');
This should result in a response of 鈥渦ndefined鈥
For your 12-word mnemonic, you can use a mnemonic generator such as this one聽if you鈥檇 like!
MAKE SURE YOU SAVE YOUR MNEMONIC (SEED) PHRASE! We鈥檒l need it later聽馃槂
Next, add the following command in your terminal (while still in truffle development):
const mnemonic = '12 words here';
const wallet = new HDWalletProvider(mnemonic, "http://localhost:8545");
Now, in your truffle console enter the command聽
wallet
If you scroll up, you should see a list of accounts, like this!
Despite that account being generated while we were connected to Ganache, we can use that same Ethereum account(s) across any Ethereum network (please note, however 鈥 although the same account can be used across any Ethereum network, assets / activities pertaining to that account are network-specific 鈥 for example, if I make a transaction on the Ethereum Mainnet, that transaction will only occur on the Ethereum Mainnet, and no other network). We are now going to stop interacting with Ganache (local dev network), and start using that account to interact with some public networks!!
Typically, the first thing you鈥檒l need to do when interacting with a public network is obtain some of that network鈥檚 ether. In our case, we鈥檒l be connecting to the Ropsten public test network, so we鈥檒l need to obtain some Ropsten ether (ETH)! Don鈥檛 worry 鈥 test net ETH is free and bountiful, and super easy to obtain聽馃憤
Time to acquire test ETH
To obtain some Ropsten ETH, head over to the Ropsten faucet. Paste in your account address, and viola! You鈥檝e received some Ropsten ETH and can start sending transactions (i.e. making state changes to) the Ropsten network!
For reference, the Ropsten test net is a public Ethereum test network, where you can test your code in an environment that closely mirrors that of the Ethereum mainnet. The main difference between the Ropsten test net (and the other public Ethereum test nets) is that in testnet-land, the ETH is plentiful and has no real-world value! When you start interacting with the Ethereum mainnet, the Ether you use to pay for your transactions (gas costs) will cost ACTUAL dollars 鈥 and so we need to make sure we are doing things right beforehand, so that we don鈥檛 lose our hard-earned cash / our precious mainnet ETH!
The Ropsten test net, along with most of the other public test networks, has many block explorers for you to view the activity happening on-chain (https://ropsten.etherscan.io/). To see your funded account, simply paste your account鈥檚 address into the explorer 鈥 and you can view all of the history associated with it:
Alright! Now that we鈥檝e got our wallet provider and an account funded with Ropsten ETH, we can head back into our project, and point it at the Infura nodes connected to the Ropsten test net.
The first thing we鈥檒l want to do is create a .env file to house our precious SECRETS! These secrets include our Infura API key (generated when we created our Infura account), and our mnemonic phrase.
At the root level of your project, simply create a new file 鈥.env鈥. Also, you鈥檒l need to install the dotenv NPM package by entering the the following command in the terminal
npm install --save dotenv
In this new .env file, you鈥檒l need two things:
INFURA_API_KEY=INSERT YOUR API KEY HERE (no quotations)
MNEMONIC=鈥漧ens whale fan wire bubble online seat expose stock number sentence winner鈥
INFURA_API_KEY is the Project ID from the project you previously created in infura:
And MNEMONIC is the 12-word seed phrase you previously used to generate your account.
Your file should now look like this:
Alright, we鈥檙e getting close!
NOTE: If you are going to push this to a Github repository, or make this project public in any way, MAKE SURE to have your .env file in .gitignore so that your secrets do not get exposed!聽
Now, we鈥檙e going to need to head over to the truffle-config.js file. In here, we鈥檒l need to add a few things to denote our provider (which is used to interact with the Infura (the Truffle HDWallet Provider we previously installed), and point our app at the Ropsten Infura nodes.
At the top of the file, add:
require("dotenv").config();
const HDWalletProvider = require("@truffle/hdwallet-provider");
Next, under 鈥渘etworks鈥, you鈥檒l want to add the following network:
ropsten: {
provider: () =>
new HDWalletProvider(
process.env.MNEMONIC,
`https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`
),
network_id: 3, // Ropsten's id
gas: 5500000, // Ropsten has a lower block limit than mainnet
confirmations: 2, // # of confs to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
}
聽
Now your truffle-config.js file should look something like this!
Side note:
If you are using Infura endpoints, the `from` parameter is required, as they don鈥檛 have a wallet. If you are using Ganache or Geth RPC endpoints, this is an optional parameter.
NOW WE ARE READY FOR THE MAGIC! It鈥檚 time to deploy a smart contract to ROPSTEN!
Setting Up a Smart Contract
Solidity setup
First, we鈥檒l want to create a smart contract to deploy! You can grab the smart contract that you developed in the previous section of this guide, build your own smart contract, or just use the following (extremely simple) sample contract:
pragma solidity >=0.5.8;
contract SimpleStorage {
uint256 storedData;
function set(uint256 x) public {
storedData = x;
}
function get() public view returns (uint256) {
return storedData;
}
}
This contract should be created as a 鈥.sol鈥 (Solidity) file in the 鈥渃ontracts鈥 folder of your project (in this scenario, we鈥檝e created the SimpleStorage.sol file, which is our SimpleStorage contract:
Migration setup
Next, we鈥檒l need to set up our migration file!
Migrations are JavaScript files that help you deploy contracts to the Ethereum network. These files are responsible for staging your deployment tasks, and they鈥檙e written under the assumption that your deployment needs will change over time. As your project evolves, you鈥檒l create new migration scripts to further this evolution on the blockchain. A history of previously run migrations is recorded on-chain through a special Migrations contract. You can find more information about them here.
Our migration file to deploy our contract will look like this:
const SimpleStorage = artifacts.require("SimpleStorage.sol");
module.exports = function(deployer) {
deployer.deploy(SimpleStorage);
};
Save this file in the 鈥渕igrations鈥 folder under the name 鈥2_deploy_contracts.js鈥.
Deploying Your First Public Contract
Time to migrate
Now you鈥檙e ACTUALLY ready for the MAGIC TO HAPPEN! Head back to the console, and type
truffle migrate --network ropsten
Boom!馃挘 Your code was deployed to the public Ropsten Ethereum Test Net!!!聽
What just happened was :
-
Your Solidity smart contract (in the 鈥渃ontracts鈥 folder) was compiled down to bytecode 鈥 the machine-readable code for the Ethereum Virtual Machine to use.
-
This bytecode, + some other data, was bundled together into a transaction.
-
That transaction was signed by your account.
-
That transaction was sent to the Infura node that is connect to Ropsten.
-
The transaction was propagated throughout the network, picked up by a Ropsten miner, and included in a Ropsten block.
-
Your smart contract is now LIVE on the Ropsten blockchain!
You can view your contract using Etherscan: https://ropsten.etherscan.io/ 鈥 simply paste in the contract鈥檚 address (should be in your terminal) to view it!
Amazing! We鈥檝e just deployed our very first smart contract to a PUBLIC Ethereum network!聽馃く
The process is the exact same for deploying to Ethereum mainnet, except you鈥檒l swap out the network in the truffle-config.js file for the Ethereum mainnet (and, of course, run the mainnet Truffle migration command instead of the Ropsten one)! We won鈥檛 walk you through this process here, because deploying to the Ethereum mainnet will cost you actual $鈥檚 鈥 but if you鈥檇 like assistance with this, hop on over to the ConsenSys Discord聽and we鈥檇 be more than happy to help!
Building a Web3 Frontend聽
Now that we鈥檝e deployed our contract to Ropsten, let鈥檚 build a simple user interface to interact with it!
Note: dApp 鈥渇ront-ends鈥 are just your everyday, regular old front-ends 鈥 as such, we can use all of our old tools we are familiar with (create-react-app, etc.) to spin up our front end, and then just add a few things to allow the front end to read from and write to Ethereum! This means, all of your old web dev skills are directly transferable to Ethereum-land / Web3!!
Spin up our React project聽
Alright, let鈥檚 get started.
First make sure you have a directory that contains all of the info we just made for our storage contract.聽 I鈥檝e named my folder 鈥渟torage-back鈥 and it contains the work we just completed to get our contract setup and deployed.聽
Now we are going to start by spinning up a react project, let鈥檚 call ours in this example 鈥渟torage-lab鈥
In our terminal let鈥檚 run the following to start our project聽
npx create-react-app storage-lab
Now that we鈥檝e got our new project boilerplate, let鈥檚 head into the project directory
cd storage-lab
Now that we are inside of our project we will now add the Web3 package, which will allow our project to interact with Ethereum! More on web3 here
npm install web3
Web3 is one of two major packages we can use, the other being ethers.js. For this example we will be using web3 but if you wanted to read more about ethers.js have a look here聽
For a detailed explanation of the two, take a look at this write up web3 vs ethers
Great! We are now almost ready to have our react project interacting with our contract!
First, let鈥檚 take our directory from earlier (for me it鈥檚 鈥渟torage-back鈥) which just contains the work we鈥檝e already done involving our smart contracts and now let鈥檚 add that to our new react project. This will live on the same level as our src, and now we should have everything we need together inside of our react REPO.
Next, we will need to set up our file containing our ABI information.
鈥淎BI?鈥
Glad you asked!聽
The Contract Application Binary Interface (ABI) is the standard way to interact with contracts within the Ethereum ecosystem, both from outside the blockchain and for contract-to-contract interaction. When we compiled our SimpleStorage contract in an earlier step, it created a JSON file for us. Check for yourself,聽 we have a SimpleStorage.json file inside of our build / contracts
An initial look at this file will reveal a lot of information, right now we only need to focus on the ABI in order to sync our contract up with the front end we are developing. This JSON contains the information we need to communicate our contract with our front end.
Our ABI is an array which holds objects. Looking at the file closer you can see each of those objects are actually each function our SimpleStorage contract contains.
You can quickly see
鈥渘ame鈥: 鈥渟et鈥
鈥渘ame鈥: 鈥済et鈥
both with a 鈥渢ype: 鈥渇unction鈥 both of the functions we declared when writing our smart contract!
Although Truffle obfuscates away the next few steps, we鈥檙e going to walk through a much more 鈥渕anual鈥 way of doing things so that you鈥檙e exposed to all of the basics 馃檪
First, go ahead and copy your abi information 鈥 we will need it in just a moment.聽
Let鈥檚 create a folder inside of our src named 鈥渁bi鈥.
Inside of our freshly made abi folder let鈥檚 now make a file named abi.js
Note: We don鈥檛 technically need to have this separation and could just add our abi.js to our src, but keeping our abi.js files contained helps with organization.
Now we will copy our abi array we grabbed earlier from the SimpleStorage.JSON file and add it to our newly made abi.js file. We will change the file a bit to allow our project to import the information into our App.js. Don鈥檛 forget since this is a .js file, we need to add an export so we have the ability to pull it into our app.js later. Let鈥檚 name the const the same as the contract, except with camelcase (see code below):
This will be the code we store in our abi.js file
export const simpleStorage = [
{
constant: false,
inputs: [
{
name: "x",
type: "uint256",
},
],
name: "set",
outputs: [],
payable: false,
stateMutability: "nonpayable",
type: "function",
},
{
constant: true,
inputs: [],
name: "get",
outputs: [
{
name: "",
type: "uint256",
},
],
payable: false,
stateMutability: "view",
type: "function",
},
];
Time to head to our App.js and import both web3 and our freshly made abi.js file.
We are also going to use hooks in this example (which is why we are also importing {useState} you can read more about useState here.
The top of our App.js file should now look like this:
import React, { useState } from "react";
import { simpleStorage } from "./abi/abi";
import Web3 from "web3";
import "./App.css";
We now need to make sure we have the ability for any arbitrary users to have the ability to connect and use our dApp , so long as they have a wallet provider!
The main wallet used in the Ethereum space for dApp interaction is MetaMask, introduced in Step 1.
If you don鈥檛 have MetaMask, visit metamask.io.聽
With MetaMask installed, we can access our wallet inside of our dapp with:
const web3 = new Web3(Web3.givenProvider);
鈥淲eb3.givenProvider鈥 will be set in a Ethereum supported browser.
(you can read more about why this is necessary here)
So now our code should look like this:
import React, { useState } from "react";
import { simpleStorage } from "./abi/abi";
import Web3 from "web3";
import "./App.css";
const web3 = new Web3(Web3.givenProvider);
Ok! So far we鈥檝e:
- Spun up a React project
- Installed Web3
- Added our folder containing our build + contract + migration to our React project
- Created an abi.js file holding the abi data we pulled from SimpleStorage.json
- Imported the data we need to interact with our contract
- Created a variable which allows our dApp to communicate with the user鈥檚 wallet
Again, although Truffle makes the next few steps unnecessary (we鈥檒l walk you through a much simpler version later), we鈥檒l add a bit more manual complexity to our dApp for educational purposes.
What we鈥檒l do now is create two new variables: one to store the address of the contract we deployed on Ropsten, and the other to match that contract to our ABI, so that our app knows how to talk to it!聽
To locate the contract address, navigate to the JSON file we were in earlier (which contains the ABI (SimpleStorage.json)), and scroll to the bottom. The address is in the 鈥渁ddress鈥 field here:
"compiler": {
"name": "solc",
"version": "0.5.8+commit.23d335f2.Emscripten.clang"
},
"networks": {
"3": {
"events": {},
"links": {},
"address": "0x24164F46A62a73de326E55fe46D1239d136851d8",
"transactionHash": "0x1f02006b451b9e85f70acdff15a01c6520e4beddfd93a20e88a9b702a607a7b0"
}
},
"schemaVersion": "3.0.16",
"updatedAt": "2020-06-30T20:45:38.686Z",
"devdoc": {
"methods": {}
},
"userdoc": {
"methods": {}
}
}
Alternatively, you could head over to https://ropsten.etherscan.io/ and look up the address of the account that deployed the contract! In Etherscan, clicking on the 鈥淐ontract Creation鈥 will expose the Contract Address itself.
Now we will take the copy of your contract鈥檚 address and create a new variable to store it.聽
Without this, we won鈥檛 have the ability to communicate with the contract and our dApp won鈥檛 work as intended.
You鈥檒l add this under our聽 const web3 = new Web3(Web3.givenProvider);
const contractAddress = "your contract address here";
Then we will create another new variable named 鈥渟torageContract鈥 which will contain both our contract address (so our app knows where the contract is), and the ABI (so our app knows how to interact with the contract).
const storageContract = new web3.eth.Contract(simpleStorage, contractAddress);
Our App.js should now look like this
import React, { useState } from "react";
import { simpleStorage } from "./abi/abi";
import Web3 from "web3";
import "./App.css";
const web3 = new Web3(Web3.givenProvider);
const contractAddress = "your contract address here";
const storageContract = new web3.eth.Contract(simpleStorage, contractAddress);
We now need to get our hooks to hold variables that will interact with our contract and front end.聽 We will do this by declaring the following within our app function:
import React, { useState } from "react";
import { simpleStorage } from "./abi/abi";
import Web3 from "web3";
import "./App.css";
const web3 = new Web3(Web3.givenProvider);
const contractAddress = "your contract address";
const storageContract = new web3.eth.Contract(simpleStorage, contractAddress);
function App() {
const [number, setUint] = useState(0);
const [getNumber, setGet] = useState("0");
Our first usage of useState(0) will hold the uint256 the user declares.
(the naming conventions of number, setUint, getNumber, setGet hopefully help show what is happening)
useState (鈥0鈥) value acts as a placeholder until we have confirmation on our signed action (our declared uint256)
setUint we will soon call on in our return (more on this later)
Time for our logic
Next we will add our numberSet and NumberGet logic (we add numberSet within our function App)
const numberSet = async (t) => {
t.preventDefault();
const accounts = await window.ethereum.enable();
const account = accounts[0];
const gas = await storageContract.methods.set(number).estimateGas();
const post = await storageContract.methods.set(number).send({
from: account,
gas,
});
};
const numberGet = async (t) => {
t.preventDefault();
const post = await storageContract.methods.get().call();
setGet(post);
};
We set a preventDefault (details on preventDefault found here)
We also use an async call on get for the contract (details on async found here)
Our hook setGet() stores a default value we initially view (鈥0鈥)
const accounts = await window.ethereum.enable();
makes sure we are calling our connected address via MetaMask.
const account = accounts[0];
Pulls in the connect account
You might be wondering whats going on with聽const gas = await storageContract.methods.set(number).estimateGas();
Our app needs permission to access user funds to pay for gas fees, any functions requesting ether regardless if it鈥檚 on testnet聽 or mainnet. This is where our connection to MetaMask comes in handy to sign for this usage to set our uint256 and pay for it (with test ETH).
So for any function that needs gas, you have to calculate the potential gas used.
The 鈥淪et鈥 function of our contract requires gas
鈥淕et鈥 does not.
(this is because 鈥淕et鈥 is viewing what has already been declared with 鈥淪et鈥)
const post is going to take the passed in uint256, confirm the transaction (post paying gas fee) from your MetaMask wallet on the Ropsten network.
Next we pass the functions parameters via methods.set() and with our declared address (users address) we then handle the Set function.
We create our smart contract transaction by passing in our function parameters to the smart contract聽 methods.set(), and estimated gas and user account address to .send().
const post = await storageContract.methods.set(number).send({
from: account,
gas,
});
This should be all of the logic we need to cover our numberSet.
Now we need our numberGet
const numberGet = async (t) => {
t.preventDefault();
const post = await storageContract.methods.get().call();
setGet(post);
};
Our const post retrieves our set number and setGet passes in the new value we declared
So our 鈥0鈥 will onClick refer to our numberGet and render our unint256!
聽So now your app.js should look like this
import React, { useState } from "react";
import { simpleStorage } from "./abi/abi";
import Web3 from "web3";
import "./App.css";
const web3 = new Web3(Web3.givenProvider);
const contractAddress = "your contract address";
const storageContract = new web3.eth.Contract(simpleStorage, contractAddress);
function App() {
const [number, setUint] = useState(0);
const [getNumber, setGet] = useState("0");
const numberSet = async (t) => {
t.preventDefault();
const accounts = await window.ethereum.enable();
const account = accounts[0];
const gas = await storageContract.methods.set(number).estimateGas();
const post = await storageContract.methods.set(number).send({
from: account,
gas,
});
};
const numberGet = async (t) => {
t.preventDefault();
const post = await storageContract.methods.get().call();
setGet(post);
};
Let鈥檚 create a very basic return to render so we can test if we can聽
- set a unint256 value,
- Pull up our metamask wallet and confirm the transaction
- Pay the gas cost
- then get the value (unint256) we stored once the transaction has completed.
Our return looks like this:聽
return (
<div className="main">
<div className="card">
<form className="form" onSubmit={numberSet}>
<label>
Set your uint256:
<input
className="input"
type="text"
name="name"
onChange={(t) => setUint(t.target.value)}
/>
</label>
<button className="button" type="submit" value="Confirm">
Confirm
</button>
</form>
<br />
<button className="button" onClick={numberGet} type="button">
Get your uint256
</button>
{getNumber}
</div>
</div>
);
}
export default App;
Some quick CSS
Let鈥檚 now head over to the App.css file, delete the boiler plate code and add this instead
.main {
text-align: center;
display: flex;
justify-content: center;
background-color: #f2f1f5;
height: 100vh;
}
.card {
min-height: 50vh;
width: 50vw;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.form {
height: 20vh;
width: 20vw;
display: flex;
justify-content: space-evenly;
flex-direction: column;
}
.button {
width: 20vw;
height: 5vh;
}
Now we are ready to test!
In your terminal run
yarn start
In our localhost:3000 we should look like this
聽
We should now be able to enter a unint256 value in our input field!
After we confirm our number in our dApp we then sign via MetaMask (Make sure your wallet is set to the Ropsten network)
We did it! 馃
We now have our smart contract connected to a front end and have the ability to manipulate the Set function (provided we have the test ETH to pay the gas fee for the transaction). Then we can call on the Get function and retrieved the stored uint265 value.
Pretty cool huh!?!
Extra Styling聽
Now it鈥檚 time to show how easy it can be to implement even more popular Web2 tech into our project.
We are going to be using MUI to add basic styling, if you develop with React already you might be familiar with material-ui. (Details found here) Material-UI or MUI for short is a very popular React framework that allows you to quickly spin up a project with a lot of styling cooked in provided you follow the naming conventions. It鈥檚 also very easy to manipulate if you鈥檙e looking to just use a foundation and customize from there.
*This will be a very short example of how to add MUI to a project with small additions to demonstrate how quickly you can incorporate our project as it stands with a Web2 tech.聽
Adding MUI
We will start by running the command (while still in our project directory in the terminal (if you鈥檙e app is still running, you鈥檒l need to close it (ctrl+c), or open a new tab)):
To install with npm:
npm install @material-ui/core
Or with yarn:
yarn add @material-ui/core
Now that we have MUI injected we will start with changing our styling. At the top of our app.js file we are going to import a few new things:
import { simpleStorage } from "./abi/abi";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import { makeStyles } from "@material-ui/core/styles";
The import of { makeStyles } allows us to manipulate the styling (in this case) our buttons and text field along with importing the default MUI styling.聽
We will now make a variable (above our function) which brings in the boilerplate styling from MUI
const useStyles = makeStyles((theme) => ({
root: {
"& > *": {
margin: theme.spacing(1),
},
},
}));
Now within our App function we will also add a variable named 鈥渃lasses鈥 pulling in the defined styles we just declared above.
function App() {
const classes = useStyles();
const [number, setUint] = useState(0);
const [getNumber, setGet] = useState("0");
We will now make adjustments within our return to replace some of our fields with what we鈥檝e just聽 imported.
return (
<div className={classes.root}>
<div className="main">
<div className="card">
<TextField
id="outlined-basic"
label="Set your uint256:"
onChange={(t) => setUint(t.target.value)}
variant="outlined"
/>
<form className="form" onSubmit={numberSet}>
<Button
variant="contained"
color="primary"
type="submit"
value="Confirm"
>
Confirm
</Button>
<br />
<Button
variant="contained"
color="secondary"
onClick={numberGet}
type="button"
>
Get your uint256
</Button>
{getNumber}
</form>
</div>
</div>
</div>
);
}
export default App;
Your code should now look like this
import React, { useState } from "react";
import { simpleStorage } from "./abi/abi";
import Web3 from "web3";
import "./App.css";
import { makeStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
const useStyles = makeStyles((theme) => ({
root: {
"& > *": {
margin: theme.spacing(1),
},
},
}));
const web3 = new Web3(Web3.givenProvider);
const contractAddress = "your contract address here";
const storageContract = new web3.eth.Contract(simpleStorage, contractAddress);
function App() {
const classes = useStyles();
const [number, setUint] = useState(0);
const [getNumber, setGet] = useState("0");
const numberSet = async (t) => {
t.preventDefault();
const accounts = await window.ethereum.enable();
const account = accounts[0];
const gas = await storageContract.methods.set(number).estimateGas();
const post = await storageContract.methods.set(number).send({
from: account,
gas,
});
};
const numberGet = async (t) => {
t.preventDefault();
const post = await storageContract.methods.get().call();
setGet(post);
};
return (
<div className={classes.root}>
<div className="main">
<div className="card">
<TextField
id="outlined-basic"
label="Set your uint256:"
onChange={(t) => setUint(t.target.value)}
variant="outlined"
/>
<form className="form" onSubmit={numberSet}>
<Button
variant="contained"
color="primary"
type="submit"
value="Confirm"
>
Confirm
</Button>
<br />
<Button
variant="contained"
color="secondary"
onClick={numberGet}
type="button"
>
Get your uint256
</Button>
{getNumber}
</form>
</div>
</div>
</div>
);
}
export default App;
Now if we take a look at our react project it should look like this!
Well done!
We still have all of the functionality of before and have now injected an easy to use framework to further customize our project however we want. Have a look at the MUI documentation to experiment with your own additions / modifications!
Bonus Round聽
It would be nice to show the users connect address within our dApp, wouldn鈥檛 it?
Well let鈥檚 make a very quick and basic component to do exactly that!
We will start by making a separate component that we can import back into our App.js file. It鈥檚 a good idea to separate our logic to not only keep our App.js easy to navigate, but also follow the practice of a component ideally only doing one thing. If it ends up growing, it should be decomposed into smaller subcomponents.
Component build out聽
We will make a new folder called components on the same level as our src and within that folder we will make a Nav.js file. Our project scaffolding should now look something like this
We will also make a Nav.css file within our components folder to import any styles we apply specifically to the Nav component.
Let鈥檚 open our Nav.js up and let鈥檚 import our React, Web3, and our empty .css file
import React from "react";
import Web3 from "web3";
import "./Nav.css"
Now we will make a class called Nav and we鈥檒l add just a few things within it to display our connected address. We will start by setting our state to read the account
class Nav extends React.Component {
state = { account: "" };
Still within our class we will load the account to read from by adding our async loadAccount logic
async loadAccount() {
const web3 = new Web3(Web3.givenProvider || "http://localhost:8080");
const network = await web3.eth.net.getNetworkType();
const accounts = await web3.eth.getAccounts();
this.setState({ account: accounts[0] });
}
We will next create a componentDidMount (which will be invoked immediately after the component mounts) In our case pulling in the loaded account. Read more here
componentDidMount() {
this.loadAccount();
}
Side note:
This can be done differently, instead of a class we can create a function and use hooks opposed to componentDidMount, but for the sake of this example we will stick to this method.
We will then create a render above our return, render is a method that is required when you are writing a React component using a class method. Within our return we add a class of address to our div (to give basic styling to later) along a p tag to show the connected address which we fetch using { this.state.account }
render() {
return (
<div>
Your connected address: {this.state.account}
</div>
);
}
}
export default Nav;
Our Nav.js file should now look like this
import React from "react";
import Web3 from "web3";
import "./Nav.css"
class Nav extends React.Component {
state = { account: "" };
async loadAccount() {
const web3 = new Web3(Web3.givenProvider || "http://localhost:8080");
const network = await web3.eth.net.getNetworkType();
const accounts = await web3.eth.getAccounts();
this.setState({ account: accounts[0] });
}
componentDidMount() {
this.loadAccount();
}
render() {
return (
<div>
Your connected address: {this.state.account}
</div>
);
}
}
export default Nav;
聽
Let鈥檚 head to the Nav.css file and add very basic styling
.address {
display: flex;
justify-content: center;
}
You technically could add this to the App.css file, keep in mind though pretty quickly that could get messy. Components should be reusable and to avoid as much friction as possible by compartmentalizing your work it can save you a headache down the road.
Now let鈥檚 head back to our App.js and import our newly made component and make sure we add it to our return to display it!
Our finished App.js file should look like this
import React, { useState } from "react";
import { simpleStorage } from "./abi/abi";
import Web3 from "web3";
import Nav from "./components/Nav.js";
import "./App.css";
import { makeStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
const useStyles = makeStyles((theme) => ({
root: {
"& > *": {
margin: theme.spacing(1),
},
},
}));
const web3 = new Web3(Web3.givenProvider);
const contractAddress = "your address here";
const storageContract = new web3.eth.Contract(simpleStorage, contractAddress);
function App() {
const classes = useStyles();
const [number, setUint] = useState(0);
const [getNumber, setGet] = useState("0");
const numberSet = async (t) => {
t.preventDefault();
const accounts = await window.ethereum.enable();
const account = accounts[0];
const gas = await storageContract.methods.set(number).estimateGas();
const post = await storageContract.methods.set(number).send({
from: account,
gas,
});
};
const numberGet = async (t) => {
t.preventDefault();
const post = await storageContract.methods.get().call();
setGet(post);
};
return (
<div className={classes.root}>
<Nav />
<div className="main">
<div className="card">
<TextField
id="outlined-basic"
label="Set your uint256:"
onChange={(t) => setUint(t.target.value)}
variant="outlined"
/>
<form className="form" onSubmit={numberSet}>
<Button
variant="contained"
color="primary"
type="submit"
value="Confirm"
>
Confirm
</Button>
<br />
<Button
variant="contained"
color="secondary"
onClick={numberGet}
type="button"
>
Get your uint256
</Button>
{getNumber}
</form>
</div>
</div>
</div>
);
}
export default App;
We should now see our connected address up top and still retain all of our functionality!
馃帀 We did it!聽馃帀
We now have a dApp that we鈥檝e built from the ground up. We pulled in our smart contract into a React project, wrote logic to make sure we have user functionality, created a component to render the connected address, and we even added a popular styling framework to our project.
Well done! This is just the start for your Web3 development adventures and you already have something to show that you鈥檝e not only created but also wrap your head around. Reach out to us in the Discord and share your project (especially if you鈥檝e made any modifications or additions) with us!