Building a full-stack Decentralised Application on the Ethereum blockchain
Hi, welcome to my blog series on the basics of building a full-stack decentralised application to deploy on the Ethereum network.
What is a Decentralised Application, and how are we building it?
A Decentralised Application or Dapp for short is simply a program that has it’s back-end running on a peer-to-peer network like a blockchain. Dapp’s have been gaining popularity in recent years and if you’ve been around the crypto space for a while you’ve likely seen some iterations of Dapp’s such as Decentralised Exchanges like UniSwap & SushiSwap or the Ethereum Naming Service (ENS). In building our Dapp, we will first be building a smart contract using Solidity, which we will then test and deploy locally and remotely on the Rinkeby test network. Then, we’ll build a web application using React to serve as our front-end to allow users to interact with our smart contract.
What you need to know before jumping in
I’ve assumed very little technical knowledge in the writing of this blog post, and I’ll be giving you all of my code, so you can simply follow along if you’d like. I’ll also be pausing at major milestones and noting areas where extra steps can be taken for my experienced readers, but this is completely optional. This blog post requires you to have an understanding of basic programming concepts, but nothing too advanced. A knowledge of functions, variables and variable types will also help. Having worked with Node Package Manager (NPM) and Visual Studio Code will help you here too. Additionally, a basic understanding of cryptocurrency, specifically Ethereum and the MetaMask wallet, will make this blog much easier to follow.
Here are a few cryptocurrency resources I’d recommend having a read of before we begin:
The entirety of the code used in this blog post series can be found on my GitHub here:
- Smart contract: https://github.com/perryfardella/inbox-solidity-smart-contract
- Decentralised Application (Dapp): https://github.com/perryfardella/inbox-Dapp
This blog series will be broken down into five parts:
Part One: Building the smart contract
- Setting up a local Solidity developing environment.
- Introduction to solidity programming and how to use solidity to write a basic smart contract.
Part Two: Testing the smart contract
- Installing Waffle JavaScript testing library.
- Writing & executing basic unit tests on a smart contract.
Part Three: Deploying the smart contract to a local network
- How to create a local Ethereum network.
- Creating a script to deploy our smart contract.
- Deploying a smart contract to a local network.
Part Four: Deploying the smart contract to a live Ethereum network
- Creating a MetaMask wallet.
- Utilising an Ethereum (Eth) faucet.
- Utilising the Alchemy API to deploy our smart contract to a live Ethereum test network.
- Using Etherscan to view our deployed smart contract.
Part Five: Creating a front-end for the Decentralised Application
- Creating a React web application.
- Utilising the web3.js library.
- Integrating MetaMask wallets into our web application.
- Connecting the web application to a live smart contract.
Part One: Building the smart contract
So, first thing’s first, what is a Smart Contract?
So what is a smart contract and why are we building one? Put simply, a smart contract is a piece of code that sits on a blockchain network and executes whatever it’s function is. Smart contracts can also hold funds and react to funds sent to them, in more recent times they are also gaining the ability to react to events in the real world, through the use of ‘oracles’. Smart contracts help to give utility to a blockchain like Ethereum and it’s what sets them apart from more static blockchains like Bitcoin which doesn’t support smart contracts. Some of the most popular uses of smart contracts in recent years have been in the Decentralised Finance (DeFi) space with platforms like UniSwap and SushiSwap and for NFT projects such as CryptoKitties and Bored Ape Yacht Club.
What we’re building
The smart contract we’re going to build I have dubbed the ‘Inbox’, it’s a simple piece of code that will store a message on the blockchain. We will also add the functionality to allow users to read the message that our smart contract is storing, as well as interacting with our smart contract to change the message that it is storing.
System requirements
We’re going to be building our smart contract within the Visual Studio Code IDE, and will also require node.js to be installed. I'm using version 17.2.0 of node, although a slightly older version shouldn’t be an issue, anything from v14 onwards should suffice. In fact, it may save you from a webpack error I encountered and will detail below. So go ahead and install them if you don’t have them already and we can dive straight in.
Initialising the project
Now you’ve got VS code & node installed, we’re ready to get started. The first thing we need to do is open up Visual Studio Code, then click on the ‘plugins’ section on the left hand side of the screen and search for and install the ‘Solidity’ plugin. This plugin will be invaluable to us through this project as it will compile our solidity code, provide syntax highlighting and code snippets and many more quality of life changes.
Creating a project using NPM
A large portion of the tooling we will be using to create our solidity smart contract is JavaScript based, so we’ll use Node Package Manager (npm) to initialise and manage our project.
The first thing you need to do is create a folder where we’ll store our smart contract project files, let’s call that ‘inbox-solidity-project’. Open your folder within visual studio code and initialise a terminal within the directory (Terminal -> New Terminal). Within the terminal we then initialise a new npm project using the following code:
npm init -y
This command tells npm to initialise a new project within this directory, the ‘-y’ flag here tells npm to not ask us any further questions about the initialization as we don’t require any advanced configurations. This will generate a package.json file, which will serve as our main configuration file for the project and its content should resemble the following:
{
"name": "inbox-solidity-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Most of the fields in this file are self-explanatory, but don’t worry if any of this looks nonsensical to you, we’ll come back to this file in more detail later. The one thing we can do now is remove the “main” line here as we won’t need it. You could also add your name to the “author” field here and a project description to the “description” field if you want to add some more details to your project file.
Setting up a local Solidity development environment
We’ll now be installing Hardhat, which is a local Solidity development environment, it’s available as an npm package which makes it easy and quick to install. Hardhat brings a lot of great features to the table which we’ll be taking advantage of such as a task runner, automatic solidity compiler version management and a local test environment. We can install Hardhat using the following command in our terminal:
npx hardhat
You’ll then be presented with the following prompt:
Need to install the following packages:
hardhat
Ok to proceed? (y)
Press ‘enter’ (or ‘return’ if you’re on macOS) to proceed with the install.
You’ll then be presented with the following option menu:
👷 Welcome to Hardhat v2.7.1 👷
? What do you want to do? ...
❯ Create a basic sample project
Create an advanced sample project
Create an advanced sample project that uses TypeScript
Create an empty hardhat.config.js
Quit
Navigate to ‘Create an empty hardhat.config.js’ and press ‘enter’ to confirm. We’re choosing this option because we’ll be building our project from scratch and won’t require a sample project to build upon.
You should now have a ‘hardhat.config.js’ file in your project directory. This is the main configuration file for hardhat. Open the file up and it’s contents should resemble the following:
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.7.3",
};
The ‘solidity’ field here refers to the version of Solidity we will use to compile our project. Go ahead and change this number to ‘0.8.7’, the latest stable version of solidity at the time of writing this blog post, so we can use the most modern features Solidity offers in writing our smart contract.
So far we’ve created a config file for Hardhat, but it has not actually been installed locally and added as a development dependency to our project. By installing Hardhat locally to our project, it will help to prevent clashes occurring through updates and unexpected changes in new versions. We’ll need to do this by executing the following command in our terminal:
npm install --save-dev hardhat
Adding a build command
Currently we have only one command in our package.json file, which is the test command and when called using npm run test in our terminal it simply tells us that there are no tests specified. We'll get to fixing that later when we test our smart contract, but for now we need to add a build command to our project.
In our package.json file within the scripts object we need to add the following command:
"build": "hardhat compile"
By adding this to our project scripts, we can then use the command npm run build to tell hardhat to take our Solidity files and run them through the Solidity compiler.
Our scripts object within the package.json file should now look like this:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "hardhat compile"
},
We now have everything we need in our project to build and compile a Solidity smart contract, so let's start building one.
Building a smart contract with solidity
The smart contract we will be building is very straightforward, it’s purpose is to introduce you to basic Solidity programming concepts without completely overwhelming you. I’ll discuss some ways you can look at modifying the smart contract if you’re feeling confident and will cover more complex implementations in future blog posts.
The first thing we need to do is create a new folder in our root project directory called contracts, and then within that folder create a file called Inbox.sol, as you can probably guess the .sol extension here denotes a Solidity file. Once you’ve created your new file, go ahead and copy the following code into it:
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
// Contract declaration
contract Inbox {
// Storage variable declaration
string private message;
// Constructor declaration
constructor(string memory initialMessage) {
message = initialMessage;
}
// Declare setter function
function setMessage(string memory newMessage) public {
message = newMessage;
}
// Declare getter function
function getMessage() public view returns (string memory) {
return message;
}
}
This is our smart contract in its entirety. Go ahead and read over the code a few times and see if you can work out what’s going on. I’ve added some guiding comments to help. I’ll now break it down line-by-line so you can get a grasp of what we’ve written.
// SPDX-License-Identifier: MIT
This piece of code is our license header. It tells anyone using our software what kind of license it has. In this case, since my code will be open source, I’ll be using the MIT license. It’s best practice to include a license header, and you’ll likely get a warning from the compiler if you don’t include one. You may also remember seeing a license field in our package.json file so go ahead and update that to match whatever license you chose to use here for continuity purposes.
pragma solidity >=0.7.0 <0.9.0;
Here pragma is an instruction to our compiler which tells it that this particular source file requires a minimum version of 0.7.0 of the solidity compiler and will not work with any compiler version over 0.9.0. This of course can be changed to whatever version of the Solidity compiler you’d like your file to work with, you can be as specific as you like here and use a range like we have, or a specific value.
contract Inbox {
Here we are declaring a contract, much like a class in many other programming languages, a contract in Solidity can have methods and hold state.
string private message;
In Solidity, variable declarations have the format [type] [visibility modifier] [identifier]. We are declaring a variable here of the type string, visibility private and identifier message. You can read more about variable types in Solidity here, and about visibility modifiers here. By initialising our message variable as private here, we are saying it can only be accessed within our contract, and not externally. Another thing to note is that variables declared here outside of a function will be stored in ‘storage’ by default. I’ll discuss memory in Solidity a bit more below.
constructor(string memory initialMessage) {
message = initialMessage;
}
This is our constructor declaration. A constructor is a declaration that is invoked when we create an instance of our smart contract. In this instantiation we are using our constructor to take a parameter initialMessage and set our message variable’s value to the value of initialMessage. It’s important to note that we have to specify the variable type of the parameter we are providing. Here we’ve used string, to match the type of the variable whose value we are setting.
We’ve also provided the memory keyword here to the parameter to specify that it will be stored in memory, as opposed to in storage like our message variable. In Solidity, all parameters provided to functions are stored in memory. Solidity variables can be stored either in memory or in storage, which roughly translates to the concepts of locally and globally scoped variables in other languages, and can be thought of as the difference between storing data in a computer’s RAM as opposed to a harddrive. Variables stored in storage persist within the contract, whilst variables stored in memory are temporary and exist only within the context of the function that is using it. Here’s a great article explaining memory in Solidity in more depth.
function setMessage(string memory newMessage) public {
message = newMessage;
}
This is a setter function which changes the value of the stored message variable to be of the value of a provided parameter. This setter function is one which would require a transaction in the blockchain to use as it modifies the stored message property of the contract. In a more general sense it’s important to note that any function that writes to the blockchain will cost gas to be executed. The public keyword is used here as a visibility modifier the same way it is used with variables, the specific use of the public keyword here means the function can be called by anyone, from anywhere.
function getMessage() public view returns (string memory) {
return message;
}
This is a getter function which simply returns the value of the memory variable held in storage by the smart contract. It does not need a transaction to execute as it simply reads memory from the blockchain, as such it will require no gas fees to execute.The view keyword here marks the function as read-only which means there can be no mutating of data within the function.
Compiling the smart contract
Now we’ve built our first smart contract, we need to compile it. We can do this using our build script that we set up earlier. In your terminal, run the following command:
npm run build
You should then see the following output:
> inbox-solidity-project@1.0.0 build
> hardhat compile
Compiling 1 file with 0.8.7
Compilation finished successfully
Just like that, our smart contract has been built and compiled. You’ll probably notice there’s now two new folders in our project directory, artifacts and cache, feel free to poke around in both, but the more interesting will be the artifacts folders. The artifacts folder contains the outputs of the Solidity compiler. If you navigate to the contracts folder and then view the Inbox.json file, you’ll even be able to see the byte code of our smart contract. Congratulations, you’ve now successfully compiled and built your own smart contract. The next step from here is testing, so we can be sure it will behave the way we expect it to.
I’d also recommend that you have a play around with the smart contract. If you're feeling comfortable, can you add another variable and another set of getter and setter functions? What about a reverse string function that when called will reverse the letters in the message string? Don’t worry about breaking our smart contract, you can always just revert back to the code I’ve posted above.