Emmanuel Ugwuoke
ScribbleTons

Follow

ScribbleTons

Follow
Write Your First DAPP in 10mins

Write Your First DAPP in 10mins

Part 1 of 3. This guide gets you started with Solidity and partly React

Emmanuel Ugwuoke's photo
Emmanuel Ugwuoke
ยทOct 26, 2021ยท

11 min read

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:

  1. Setting up the environment
  2. Thinking through the problem
  3. Writing our tests
  4. Write some more codes
  5. Testing
  6. 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 image.png 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

image.png 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.

image.png

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,

image.png

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.

ย 
Share this