From Beginner to dApp Developer: Learn Solidity and Build a Decentralized Media App

In this post we will build our first decentralized social media app. We will call it EthSocial, but feel free to be more creative with the naming.
While building the dApp, we will learn about onchain storage, smart contracts, Solidity data types, and decentralized social media.

Our dApp will have the following capabilities:

  • create an account
  • make a post (with title and body)
  • read posts
  • check users

Before we jump into the actual building, let's first look at the WHY. Why decentralized social media? There are various advantages that decentralized social media offers. Here are some of the most important ones.

Portability: content is no longer controlled by a centralized server or company, it is accessible to everyone, everywhere. Pretend you are a creator on Twitter. You grew a follower base of over 1.5M people. However, recently you decided to expand to TikTok. With web2, you would need to grow your followers base from scratch. However, with web3, you no longer lose your followers, you can port them over to a different dApp.

Users own their data: no more centralized databases. Users own their data and are able to monetize their data how they like. No more Cambridge Analytica incidents because the platform no longer owns user data.

Censorship resistance: Creators can now be relieved from concerns about losing their content or audience due to the unpredictable actions of an individual platform's algorithms and policies.

Now that we had a look at the value of decentralized social media, let's start building.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

First, we define the SPDX-Licence -Identifier. This is important because it specifies under which license other people can use our code. In this example, we will use the open source "MIT" license.

The pragma is used to enable certain compiler features or checks. It comes annotated with a version, this allows use to get specific and ensure our byte code is reproducible even as the compiler changes.

contract EthSocial {
   struct UserStruct {
       string username;
       uint8 age;
       bool isUser;
   }
}

Next, we will create our smart contract. First, we need to define a user with some properties. To do so, we will use a struct. Structs are a data type that offer a way to define new custom types in Solidity that group together related data.
We will name our struct UserStruct, and it will contain three fields, a username of type string, the age of the user of type uint, and a bool, isUser, to keep track if the user is registered with our app or not.

In a similar fashion, we will define a struct for posts, we will call it Post. This time, it will have the following attributes: a title of type string, a body of type string, and a time attribute of type uint.

struct Post {
       string title;
       string body;
       uint time;
}

Now that we have defined our posts, it is time to keep track of who creates a post. To achieve this, we will use a mapping. Mappings in Solidity act like a hashmap or a dictionary in other programming languages. They are used to store data as a key-value pair. We will call our mapping posts, and it will keep track of what address has created each post. Similarly, we will also have a users mapping, to keep track of how addresses relate to user "accounts". In addition, we will also create a counter, userCount, to keep track of the total number of users that have signed up!

 mapping (address => Post[]) posts;
 mapping (address => UserStruct) users;
 uint256 userCount;

Now, it's time that we define a signup function. Our function, createUser will set the value for a user's info.
First we define an instance of type UserStruct, called user. Then, we will set the properties of user using the dot notation. Before returning true, indicating that a user was created, we increase the userCount.

  function createUser(address _userAddress, string memory _username, uint8 _age) public returns (bool success){
       UserStruct memory user;

       user.username = _username;
       user.age = _age;
       user.isUser = true;

       users[_userAddress] = user;
       userCount++;
       return true;
   }

Next we will create a getter to retrieve a particular value from our users mapping, in this case the "account" corresponding to an address.

function getUser(address _userAddress) public view returns (UserStruct memory){
       return users[_userAddress];
}

We will also create a function to keep track of the total number of users, we will call it getTotalUsers().

function getTotalUsers() public view returns (int) {
       return userCount;
}

Next, we will create a post. To do so, we will implement the createPost() function. The function defines a new instance of type Post, called newPost, and then it instantiates the values of it using the dot notation. Before returning true to indicate that a new post has been successfully created, the new post is added to the posts mapping using push().

  function createPost (
       address _userAddress,
       string memory _title,
       string memory _body
   )
   public returns (bool) {
       Post memory newPost;
       newPost.title = _title;
       newPost.body = _body;
       newPost.time = block.timestamp;

       posts[_userAddress].push(newPost);
       return true;
 }

Our dApp is almost ready. However, we still miss a way to retrieve the posts corresponding to a given address. To do so, we will implement the getPosts() method. The function takes the userAdddress and returns an array of type Post corresponding to the posts associated with the given address.

function getPosts(address _userAddress) public view returns (Post[] memory) {
       return posts[_userAddress];
}

Now we are ready to deploy our contract. We can compile our smart contract and deploy it. However, once we deploy it we might observe some unexpected behavior. We can call our getUser() function on an address that is yet to join the app. We do not want such behavior. To prevent this, we can use a require() statement in the getUser() function. This will return an error if we try to run getUser() for an address that has not "registered".

require(users[_userAddress].isUser == true, "User does not exist"); 

In a similar fashion, right now anyone can create a post using the createPost() function, even if they are not a user. To prevent this, we will use another require statement, this time in the createPost() function.

require(users[_userAddress].isUser == true, "User not registered");

The last thing we want to check, is if the user can only post in their name. To do so, we will use a third require statement to verify is the user is indeed the msg.sender.

require (_userAddress == msg.sender, "You can only post on your behalf");

That's all folks! I hope you found this short intro to Solidity helpful. If you are interested in more content like this you can find me on Twitter or join us at DevRel Uni.

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract SocialETH {

    struct UserStruct {
        string username;
        uint age;
        bool isUser;

    }

    struct Post {
     string title;
     string body;
     uint time;
    }

    mapping (address => Post[]) posts;
    mapping (address => UserStruct) users;
    uint userCount;

    function createUser (address _userAddress, 
string memory _username, 
uint _age) public returns 
(bool success){
        UserStruct memory user;

        user.age = _age;
        user.username = _username;
        user.isUser = true;

        users[_userAddress] = user;
        userCount ++;
        return true;
    }

    function getUser (address _userAddress) public view returns (UserStruct memory){
    require(users[_userAddress].isUser == true, "User does not exist");
    users[_userAddress];
    }

    function getTotalUsers () public view returns (uint) {
        return userCount;
    }

    function craetePost (
        address _userAddress,
        string memory _title,
        string memory _body
    ) 
    public returns (bool) {

        require(users[_userAddress].isUser == true, "User not registered");
        require (_userAddress == msg.sender, "You can only post on your behalf");
        Post memory newPost;

        newPost.title = _title;
        newPost.body = _body;
        newPost.time = block.timestamp; 

        posts[_userAddress].push(newPost);
        return true;
    }

    function getPosts(address _userAddress) public view returns (Post[] memory) {
        require(_userAddress == msg.sender,  "You can only post on your behalf");
      return posts[_userAddress];
    }
}