Write Your First DAPP in 10mins
Part 1 of 3. This guide gets you started with Solidity and partly React
I am going to show you quickly how to get started with developing a decentralized application on the Ethereum blockchain and write tests for it.
Our goal: Build a social network application like Twitter on the Blockchain called EMeet.
Requirements:
- Users can post
- Users can tip an author
- Users can comment on posts
I feel very excited already, do you feel that?
If you do, let's get it done as promised 🥰.
We are going to break this down into these sections:
- Setting up the environment
- Thinking through the problem
- Writing our tests
- Write some more codes
- Testing
- Deploying our Dapp While explaining the concepts.
Setting Up Environment
If you have node.js installed on your system you can skip this section else, download it from here and install.
To check your installation, open your terminal, type win + R, and enter cmd on Windows.
// Type this in the cmd
node -v
Mine is V14.16.1
//In your terminal
cd desktop && mkdir myfirstdapp && cd myfirstdapp
//Let's install truffle. Truffle is a framework for developing, testing, and deploying Dapps
C:~\myfirstdapp> npm i -g truffle
When you are done with your installation
truffle init
//Initiate truffle configurations. You should see your project folders organized for you
C:~\myfirstdapp> truffle init
//View your project directories
C:~\myfirstdapp> dir
Directory of C:~\myfirstdapp 10/28/2021 06:05 PM <DIR> .
10/28/2021 06:05 PM <DIR> ..
10/28/2021 06:05 PM <DIR> contracts
10/28/2021 06:05 PM <DIR> migrations
10/28/2021 06:05 PM <DIR> test
10/26/1985 09:15 AM 4,900 truffle-config.js
What are these folders for?
Contract is to solidity what class is to other programming languages. The contracts/ folder contains the contracts we'll soon write in Solidity.
The migrations/ folder contains scriptable deployment files. These files containers instructions on how to deploy the smart contract.
The test/ folder contains test files for our various contracts.
PS. You can't mutate a deployed contract, you can only deploy a new one.
truffle-config.js is our truffle configuration file.
Thinking through the problem
You can use any IDE for this, I am using VS Code. Head over to your code editor.
Create a file in contracts/
folder named EMeet.sol
//~/contracts/EMeet.sol
//this tells the compiler what version to use when
//compiling your code
pragma solidity ^0.5.0;
// declaring EMeet contract
contract EMeet{
//Just like class, we can write our functions
//here and do whatever we want
/*
Users can create a new post
We use the keyword `public` to make states and functions available
outside the contract.
We need to receive the content sent from the user and add it to our contract
**/
function createPost(string memory _content) public{
//TODO
}
/**
Let's tip an author.
We will get the post id and tip its author.
The payable keyword enables a function or address to
transfer or receive Ether.
*/
function tipPost(uint _id) public payable {
//TODO
}
/**
Comment on a post.
We will need the id of the post and the content of the comment
to add the comment on the blockchain
*/
function commentOnPost(uint _id, string memory _content) public {
//TODO
}
}
Testing Application
Let's make sure our application is running by testing it.
create a new file in test/
emeet.js and add these codes
~/test/emeet.js
const EMeet = artifacts.require('./EMeet.sol');
contract("EMeet", (accounts) => {
let eMeet;
//this runs before the test described
before( async () => {
eMeet = await EMeet.deployed();
});
//check if our app is deployed
describe("Deployment", async () => {
it("deployes successfully", async () => {
const address = await eMeet.address;
assert.notEqual(address, 0x0);
assert.notEqual(address, '');
assert.notEqual(address, null);
assert.notEqual(address, undefined);
});
});
});
Truffle comes with some javascript testing libraries that are available globally to us, we just use them as seen above. The artifact.require
is a global method that we use when importing contract files.
To deploy our contract, create a file 2_deploy_contract.js in migrations/
folder and these lines.
2_~ tells the compiler the order of migrating files during deployment.
const EMeet = artifacts.require("EMeet");
module.exports = function(deployer) {
deployer.deploy(EMeet);
}
Now on your command line ~/myfirstdapp>
type truffle develop
.
You should see this
You notice you have 10 addresses to play with on truffle. In our test,
account
parameter in our contract is an array of these 10 addresses.
Type truffle(develop)> test
Congratulations, your application is deployed successfully!
Create Post
...
contract EMeet{
//state
uint public postCount;
//mapping is a key/value pair storage like hash table
// that points to the public variable name posts
mapping(uint => Post) public posts;
//Let's create our post and comment state type with struct
struct Post{
uint id;
string content;
uint tipAmount;
address payable author;
}
//let create events that frontend can interact with
event PostCreated(
uint indexed id,
string content,
uint tipAmount,
address payable indexed author
);
/*
Users can create a new post
We use the keyword `public` to make states and functions available
outside the contract.
We need to receive the content sent from the user and add it to our contract,
memory is temporal storage for our _content argument. It is deleted when the function finishes its execution.
**/
function createPost(string memory _content) public {
//let's ensure the content is not empty
require(bytes(_content).length > 0, "post content must not be empty");
//let's create a post an increment the id
postCount++;
posts[postCount] = Post(postCount, _content,0, msg.sender);
//emit the event
emit PostCreated(postCount, _content, 0, msg.sender);
}
...
Please don't feel overwhelmed. Don't forget to accept these concepts just as they are, especially developers coming from other languages.
uint and struct
are both types. The former is an unsigned integer and the latter is used to construct custom types like our Post
. We also have public
modifier that tells Solidity to create getters for the state or function, making it accessible in other contracts or anywhere. memory
is a runtime storage that gets created and cleared when the function is executed and after its execution respectively. The address
is a data type for a contract or account address on the blockchain. Having payable
marker attached to address
enables it to receive Ether. More details on types here. msg.sender
is a global variable that holds the address of the account that initiated the current transaction or called a function, in our case creating a post.
On the other hand, event and emit
as shown provides a means for other contracts and applications to interface with the Ethereum Virtual Machine logging facility. We declare the event we want and emit it from a function. We can index our event parameter (maximum of 3), more here. This is an expensive operation on the Blockchain but it's important for filtering through events in our frontend application.
Testing Create Post
Unlike other software development that we can redeploy, we can't do that on the blockchain. We have to test our application before deploying it.
...
//test our post function
describe("Post", async () => {
let post, postCount;
before(async () => {
post = await eMeet.createPost("My first post");
postCount = await eMeet.postCount();
})
it("post created", async () => {
let event = post.logs[0].args
assert.equal(postCount, 1);
assert.equal(event.id.toNumber(), postCount.toNumber(), "Post id matched")
assert.equal(event.content, "My first post", "content is correct")
assert.equal(event.author, deployer, "author's address matches deployer")
assert.equal(event.tipAmount, "0", "tip amount is zero")
});
});
...
Type truffle(develop)> test
like before.
We would do the same for our tip post and comment quickly below
EMeet.sol
pragma solidity ^0.5.0;
// declaring EMeet contract
contract EMeet{
uint public postCount;
uint public commentCount;
//mapping is a key/value pair storage like hash table
mapping(uint => Post) public posts;
mapping(uint => Comment) public comments;
//Let's create our post and comment state type with struct
struct Post{
uint id;
string content;
uint tipAmount;
address payable author;
}
struct Comment{
uint id;
uint postId;
string content;
address commenter;
}
//let create events that frontend can interact with
event PostCreated(
uint indexed id,
string content,
uint tipAmount,
address payable indexed author
);
event PostTipped(
uint id,
string content,
uint tipAmount,
address payable indexed author
);
event CommentPosted(
uint indexed id,
uint indexed postId,
string content,
address commenter
);
/*
Users can create a new post
We use the keyword `public` to make states and functions available
outside the contract.
We need to receive the content sent from the user and add it to our contract memory as a temporal storage for our _content argument. It is deleted when the function finishes its execution.
**/
function createPost(string memory _content) public {
//let's ensure the content is not empty
require(bytes(_content).length > 0, "post content must not be empty");
//let's create a post an increment the id
postCount++;
posts[postCount] = Post(postCount, _content,0, msg.sender);
//emit the event
emit PostCreated(postCount, _content, 0, msg.sender);
}
/**
Let's tip an author.
We will get the post id and tip its author.
The payable keyword enables a function or address to transfer or receive Eth.
*/
function tipPost(uint _id) public payable {
//Ensure the id is not invalid
require(_id > 0 && _id <= postCount, "Valid id required");
//fetch post
Post memory _post = posts[_id];
address payable _author = _post.author; //fetch the author
address(_author).transfer(msg.value); //transfer Ether to the author
//We increment the tip amount
_post.tipAmount += msg.value;
posts[_id] = _post;
// let's trigger a post tipped event
emit PostTipped(_id, _post.content, _post.tipAmount, _author );
}
/**
Comment on a post.
We will need the id of the post and the content of the comment
to add the comment on the blockchain
*/
function commentOnPost(uint _id, string memory _content) public {
require(bytes(_content).length > 0, "Content must not be empty");
commentCount++;
comments[commentCount] = Comment(commentCount, _id, _content, msg.sender);
emit CommentPosted(commentCount, _id, _content, msg.sender);
}
}
Then head over to our emeet.js
test file and add these lines.
const EMeet = artifacts.require('./EMeet.sol');
//accounts is restructured into [deployer, tipper , ...]
contract("EMeet", ([deployer, tipper]) => {
let eMeet;
//this runs before the test described
before( async () => {
eMeet = await EMeet.deployed();
});
//check if our app is deployed
describe("Deployment", async () => {
it("deployes successfully", async () => {
const address = await eMeet.address;
assert.notEqual(address, 0x0);
assert.notEqual(address, '');
assert.notEqual(address, null);
assert.notEqual(address, undefined);
});
});
//test our post function
describe("Post", async () => {
let post, postCount;
before(async () => {
post = await eMeet.createPost("My first post");
postCount = await eMeet.postCount();
})
it("post created", async () => {
let event = post.logs[0].args
assert.equal(postCount, 1);
assert.equal(event.id.toNumber(), postCount.toNumber(), "Post id matched")
assert.equal(event.content, "My first post", "content is correct")
assert.equal(event.author, deployer, "author's address matches deployer")
assert.equal(event.tipAmount, "0", "tip amount is zero")
});
it("Lists posts", async () => {
let post = await eMeet.posts(postCount);
assert.equal(post.id.toNumber(), postCount.toNumber(), "Id matches");
assert.equal(post.content, "My first post", "content matches");
assert.equal(post.tipAmount,"0", "tip amount is correct");
assert.equal(post.author, deployer, "Author is correct");
});
it("post tipped", async ()=>{
/**
To test if the author's account balance is incremented,
we declare two variables to hold the old and new balance in his account.
First, we tip the account with one Ether using the
web3 js library that enables us to interact with
the blockchain.
Read more on web3 Ethereum JS API https://web3js.readthedocs.io/
We are using the following utility methods from web3 below to access the user's account
and check the balance difference after the account is tipped.
*/
let newBalance, oldBalance;
oldBalance = await web3.eth.getBalance(deployer);
oldBalance = new web3.utils.BN(oldBalance);
let result = await eMeet.tipPost(postCount,{from: tipper, value: web3.utils.toWei("1", "Ether")});
let event = result.logs[0].args;
assert.equal(event.id.toNumber(), postCount.toNumber(), "Id matches");
assert.equal(event.content, "My first post", "content matches");
assert.equal(event.author, deployer, "author is deployer");
assert.notEqual(event.tipAmount,"0", "tipAmount has changed and increased");
//let's see how much the tipAmount is now
newBalance = await web3.eth.getBalance(deployer);
newBalance = new web3.utils.BN(newBalance);
let tip;
tip = web3.utils.toWei("1", "Ether");
tip = new web3.utils.BN(tip);
const expectedBal = oldBalance.add(tip);
assert.equal(newBalance.toString(), expectedBal.toString())
});
it("comment created", async ()=>{
let comment = await eMeet.commentOnPost(postCount, "I love your new app");
let commentId = await eMeet.commentCount();
let event = comment.logs[0].args;
assert.equal(event.id.toNumber(), commentId.toNumber(), "comment id matched");
assert.equal(event.postId.toNumber(), postCount.toNumber(), "Id matches post's");
assert.equal(event.content, "I love your new app", "Content matches");
assert.equal(event.commenter, deployer, "deployer did the commenting");
});
});
});
On your command line type truffle(develop)> test
like before,
Please stretch your legs and crack your knuckles, your application is working correctly, show your friends!
Deploying Application
On your command line type truffle(develop)> compile
to compile the contracts files.
After compiling your files, truffle(develop)> migrate
to deploy contracts to the Ethereum network.More on compiling and migration.
To verify if your contract was deployed (if you don't trust the test),
truffle(develop)> EMeet.deployed()
run this command and you will see lengthy output on your command line describing the contract you just deployed.
What is abi?
The new folder and files you discovered after compiling and migrating your contracts are the application binary interface files that describe your contract on the blockchain. Other application needs these files to interact with your application. Read more here on its specifications and design.
They are terms like transaction fees, gas, wei, ether, and rest I purposely skipped even though we used them. This page explains them very much in detail.
Hey, just before you leave this article, I have great news, you have developed a smart contract that allows users to create, tip, and comment on posts but the user needs to interact with your application on the blockchain.
We would use react to implement this user interfacing application that connects them to this contract.
😀 This article is long already, I will write part 2 on interacting with your new application from your command line, and another for the user interfacing application (we will use react).
Thank you for reading to this point!
Read part 2 of this article series here.
Follow me for more Dapp development tutorials, comments, and whatever I have to offer.