Web3 Twitter application on React.js + Solidity

Hello, in this article I will try to show in detail the process of creating a dApp application using Twitter as an example. In the first part, we will prepare a project, write a smart contract and deploy it on a private network. In the second part, we will write the frontend and set up interaction with the smart contract.

PS I don’t position myself as an expert in blockchain and web3, so I’m waiting for your criticism in the comments 🙂

DApps are digital applications or programs based on smart contracts that run on the blockchain rather than on centralized servers. They look and function like regular mobile apps and offer a wide range of services and features, from gaming to finance to social networking and more.

A few words about the project. If everything is clear with the client, simple react application with a couple of buttons and requests to the contract, then the backend is a smart contract written in solidity and deployed on a private network via truffle.

Regarding functionality: basic registration via metamask and password, writing posts (tweets) and viewing those already written (both your own and all users). You can add functionality for editing your tweets and the ability to like, but we’ll leave that for the second part.

Tools for creating an application:

Preparing the environment and installing dependencies

I will work in vs code, but this does not matter. First, install plugins for comfortable work: Simple React Snippets And Solidity.

You also need to download Ganache and put Truffle.

$ npm install -g truffle

We create a folder for the project and there are two folders in it: contracts And client.
The first will contain the smart contract and everything connected with it.
The second will contain a react application.

mkdir web3
cd web3

mkdir contracts
mkdir client

Now we initialize truffle in the contracts folder.

cd contracts
truffle init

After this, we receive a success message in the console and see new folders and a file in the contract folder truffle-config.js We will return to it later, but now we will start writing the smart contract itself in the folder contracts (created inside our contracts folder).

Folder after truffle initialization

Folder after truffle initialization

Creating a smart contract

In the directory web3/contracts/contracts create a file Twitter.sol it will contain all the logic of our server. I won’t explain too much about solidity, if you wrote it in js, you can easily figure it out, just in case there’s handy guide.

First, let's create two structures: User And Twitt.

struct User {
    address login;
    string password;
    string username;
    string avatar;
}

struct Twitt {
    User author;
    string text;
    uint likes;
    uint createdTime;
}
  • In structure User declare basic fields:
    – Login type address (user wallet address).
    – Password, nickname and avatar. These fields of the string type do not need any explanation, I think. We will store the avatar as a link to the picture, so as not to waste time uploading pictures to the server.

  • In structure Twitt it’s more complicated, here we declare the field author type of our structure Userlikes and the date the tweet was written will be stored in the type uintbecause this data cannot be less than 0.

Now let's create mapping for data storage. Concept mapping in Solidity it is similar HashMap in Java or dict in Python. The keys of all our mappings will be user addresses, and the value will differ depending on the mapping.

First mapping will store user accounts, where each user (address) can only have one account.
Second mapping will store the state of the user (in the system or not), here the key value will be boolean variable (true – in system / false – No).
Third mapping stores an array of user tweets.

Let's also declare a variable owner type address (contract owner) and variable usersCount to count the number of users, because we can't get the mapping size.

Hidden text
address owner;
    
mapping(address => User) accounts;
mapping(address => bool) isLogged;
mapping(address => Twitt[]) twitts;

uint usersCount = 0;

Next, we declare a couple of modifiers to check that the user is logged into the account and check that the user is the owner of the contract (in fact, the owner status is not currently used in the contract, but will be useful when expanding the contract).

We also create a constructor (it is called once when deploying a smart contract), in it we assign the owner of the contract to the address that this contract deployed (msg.sender).

msg.sender in solidity is the address of the user interacting with the smart contract, in the future we will use this function in almost all of our functions, I recommend that you familiarize yourself with it in more detail if you are not familiar.

In the modifier onlyLogged we check that the user calling the function with this modifier is logged in and is in a mapping with logged-in users, otherwise we cancel the function call with an error “You must log in to your account.”

modifier onlyLogged() {
    require(isLogged[msg.sender], "You must login in your account");
    _;
}

modifier onlyOwner() {
    require(msg.sender == owner, "Only for owner");
    _;
}

constructor() {
    owner = msg.sender;
}

Now let's move on to writing the contract functions themselves, starting with registration.

Function registration will accept username And password and check that the user calling the function does not have an account, that is, his address is not in the account mapping.

Inside the function, under a key equal to the address of the user who called the function, we create a new account, filling it with the passed parameters, leaving the link to the avatar empty.
After creating an account, we increase the variable that counts the number of accounts.

function Registration(string memory _username, string memory _password) public {
    require(accounts[msg.sender].login == address(0), "Account is already registered");

    accounts[msg.sender] = User({
        login: msg.sender,
        password: _password,
        username: _username,
        avatar: ""
    });

    usersCount++;
}

Function authorization only accepts a password, because login is the address of the account calling the function.

Inside the function, we check that the user has an account and compare the user’s password with the passed password (in solidity you cannot directly compare strings, so we convert them into bytes and compare their hash). If the passwords are equal, then in the mapping of authorized users we set the user to true (this will give him access to the remaining functionality).

function Login(string memory _password) public {
    require(accounts[msg.sender].login != address(0), "You don't have account!");
    require(keccak256(bytes(accounts[msg.sender].password)) == keccak256(bytes(_password)), "Wrong password");
    
    isLogged[msg.sender] = true;
}

In function exit from the account we simply change the user status to false in the mapping of authorized users.

function Logout() public onlyLogged {
    isLogged[msg.sender] = false;
}

In function getting user we register return because it will return the user's data at his address (login), which we pass as a parameter.

Memory reserved for variables defined within a function. They are only stored during a function call and are thus temporary variables that cannot be accessed outside of that function.

function GetUser(address _user) public view returns(User memory) {
    return accounts[_user];
}

In function writing a tweet add a modifier onlyLogged and transmit the text of the tweet. Inside using our method GetUser We get the data from the user calling the function and write it into a variable, which we will later transfer to a new object Twitt as the author of the tweet. In the date of creation of the tweet, we transmit the current time (on the client we will format it in the usual date time).

function AddTwitt(string memory _text) public onlyLogged {
    User memory _user = GetUser(msg.sender);

    twitts[msg.sender].push(Twitt({
        author: _user,
        text: _text,
        likes: 0,
        createdTime: block.timestamp
    }));
}

In function receiving tweets similar to the function GetUser We accept the user's address and return an array of his tweets.

In function registration checks We also check that in mapping accounts have user data, and return true or false .

function UserTwitts(address _user) external view onlyLogged returns(Twitt[] memory) {
    return twitts[_user];
}

function CheckRegistration(address _user) external view returns(bool) {
    return accounts[_user].login != address(0);
}

And the last function is avatar update, in it we get the user who called the function and write it to a variable with a modifier storage to save your changes.

Storage – This is where all state variables are stored. Since state can be changed within a contract (for example, inside a function), storage variables must be mutable. However, their location is permanent and they are stored on the blockchain.

function UpdateUser(string memory _avatar) public {
    User storage _user = accounts[msg.sender];

    _user.avatar = _avatar;
}

Full contract code:

Hidden text
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

contract Twitter {
    address owner;
    
    mapping(address => User) accounts;
    mapping(address => bool) isLogged;
    mapping(address => Twitt[]) twitts;
    
    uint usersCount = 0;

    struct User {
        address login;
        string password;
        string username;
        string avatar;
    }

    struct Twitt {
        User author;
        string text;
        uint likes;
        uint createdTime;
    }

    modifier onlyLogged() {
        require(isLogged[msg.sender], "You must login in your account");
        _;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Only for owner");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function Registration(string memory _username, string memory _password) public {
        require(accounts[msg.sender].login == address(0), "Account is already registered");

        accounts[msg.sender] = User({
            login: msg.sender,
            password: _password,
            username: _username,
            avatar: ""
        });

        usersCount++;
    }

    function Login(string memory _password) public {
        require(accounts[msg.sender].login != address(0), "You don't have account!");
        require(keccak256(bytes(accounts[msg.sender].password)) == keccak256(bytes(_password)), "Wrong password");
        
        isLogged[msg.sender] = true;
    }

    function Logout() public onlyLogged {
        isLogged[msg.sender] = false;
    }

    function AddTwitt(string memory _text) public onlyLogged {
        User memory _user = GetUser(msg.sender);

        twitts[msg.sender].push(Twitt({
            author: _user,
            text: _text,
            likes: 0,
            createdTime: block.timestamp
        }));
    }

    function UserTwitts(address _user) external view onlyLogged returns(Twitt[] memory) {
        return twitts[_user];
    }

    function CheckRegistration(address _user) external view returns(bool) {
        return accounts[_user].login != address(0);
    }

    function GetUser(address _user) public view returns(User memory) {
        return accounts[_user];
    }

    function UpdateUser(string memory _avatar) public {
        User storage _user = accounts[msg.sender];

        _user.avatar = _avatar;
    }
}

Deploying a smart contract on the network

Before deploying the contract, you need to prepare truffle-config and ganache.

Launch Ganache and create a new workspace. Further, all settings can be left as default.

After pressing the button “start” we see our accounts and some information about the network. We are interested in network id And rpc server we will enter them in the file truffle-config.js .

Go to the file truffle-config here we are looking development and remove comments, substituting our data from Ganace (screenshot above), also network_id You can leave it with the value “*” so that any id is suitable.

Go down below and change the compiler version from 0.8.21 on 0.8.2 . You can do without this, but then there is a chance of getting an error when deploying the contract.

Save the file and go to the folder migrations in it we create a file called 2_deploy_contracts.js which will deploy our contract and write simple code.

In fact, the contract is located in the directory “'../contracts/Twitter.sol'”, but we can specify the current directory.

const twitter = artifacts.require('./Twitter.sol');

module.exports = function(deployer) {
  deployer.deploy(twitter);
};

Now everything is ready for deployment. Open cmd in the root directory contracts and write a command for assembling and deploying the smart contract.

$ truffle migrate --network development

After this, the contract is successfully deployed and we see information about the transaction in the console, as well as the address of the smart contract; it needs to be saved, because We will use it when connecting a client.

In ganache we see a new block and if you open it, you can see that a smart contract was created in this block, and its address is also written.

At the end of the first part, we will add several accounts from ganache V metamaskto work with them on the client.

To do this, click on the key icon next to your account in ganache and copy its private key.

Open in browser metamask and add a new account, then select “Import account”, and insert the account’s private key into the field.

The account has been added, but the balance is 0, to link the balance Ganache And MetaMask you need to add a test network to MetaMask.

To add a network, go to settings and select “add network manually”.

Because I have already created networks, then the blockchain id is 1337, you will most likely have 5777. The name of the network and the currency symbol can be left as ETH, I will enter mine as an example.

After this, the network itself changes to a new one and offers to change the currency, we agree and see our balance as in Ganache.

We will also add several more accounts from Ganache.

At this point, the first part can be declared complete. Everything is completely ready to write the client part and work with the smart contract through the web application.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *