Decentralized Notes Application (DApp) with Solidity and Trufffle - Step by Step 📝

27/05/2022



What are we going to build?

We are going to create the typical notes app, but in a decentralized way, where each note is stored in the blockchain. Beyond the 0 practical utility that this application would have (since for the fee of each transaction, annotating each note would cost us approximately $10, it would be the most expensive notes application in the world :P), this will allow us to learn how to create a simple project with Truffle, create, deploy and test the smart contract, connect this smart contract with the front end of the application, use the metamask API, among other things. I hope you find it useful! 💪

Let's start!

To make this project we'll use Truffle as framework, although Hardhat can be used as well. We'll use Ganache to create a local blockchain to deploy and test our code.

We'll start the project with the command truffle init inside the folder we created for the project. This will create a small initial structure that we'll use to create the project.

We'll begin by creating a contract called TasksContract.sol that we'll use to manage the notes.

For this, what we'll do is to create a new data structure that represents each task, Solidity facilitates this to us with the struct data type. Each task will contain an id, a title, a description, a variable indicating whether it is done or not and a creation date.

We'll use the variable "createdAt" to store the date and time when the task was created. Despite it is a time variable, Solidity takes it as an integer (since from that integer it calculates the date and time, we will see it later).

We need a way to be able to access each task according to its id, in any other web2 language we would use an array, but in Ethereum traversing an array is very expensive, so we opt for the mapping datatype. Mapping is a "key-value" data type (dictionary style), where the key can be any data type and the value can be any other. In our case it will be the Task object we created earlier.

The way to denote the mapping is the following: 

mapping (KeyDataType => ValueDataType) visibility mappingName;

In our case:

mapping ( uint256 => Task ) public tasks;


Creating CRUD 🔨

We are going to create the different functions that allow us to interact with this data (create a task and mark it as done).

Create - Task Function ✏️

To create this function the only parameters we must pass to it will be the title of the task we want to create and the description, since the other parameters can be made to take values by default, generating a value for the id, false for the done variable and the current time for the createdAt parameter (which we'll obtain from the block where our contract is being executed).

Note: memory variables in Solidity can only be declared within methods and are usually used in method parameters. It's a short term variable that cannot be saved on the blockchain; it holds the value only during the execution of a function and its value is destroyed after execution.

The function will be public, because after we'll need call this fuction from de frontend.

uint256 taskCounter = 0;

function createTask(string memory _title, string memory _description) public {
        
    tasks[taskCounter] = Task(
       taskCounter,
       _title,
       _description,
       false,
       block.timestamp
    );
        
    taskCounter++;
}  


Deploying and Interacting with the Contract 🧪

It is advisable to test each function to make sure everything is working properly, because once the smart contract is deployed on the blockchain it is very difficult to update it. Two ways to test the contract functions are manually from the Truffle console (less recommended path) and automatically using another language such as JavaScript. We'll look at both.

The first thing we'll do is create the environtment for the test.  Open Ganache app and select Workspace Quickystart, this open the follow window (this it's possible to do in the terminal also): 


Ganache creates a private blockchain where we can deploy and test our dapps. Also, Ganache offers us 10 account with 100 test ethers (each one)  that we will use it for deploy our smart contract and interact with him. 

For connect our proyect in Truffle with Ganache and use its blockchain that offers us, we should config the file named "truffle_config.js" into the VS code. Into the part of networks, un-comment the lines of "development" and complete this with the correct data (in my case, Ganache tells me that the RPC SERVER is "http://127.0.0.1:7545", so I'd put 127.0.0.1 in host parameter and 7545 in the port parameter, the network_id parameter can be left at "*"). Then, in the compilers part we indicate the version of Solidity that we will use for the proyect (in my case 0.8.7).

Deploy Contract to the local Blockchain ⛓️

Now we need to create a file that we use for the contract deploy, into de migration folder, we'll create a file .js named "2_deploy_taskContract.js" (this is an example, you can use the name you want), in this file, we'll write the following: 

const TaskContract = artifacts.require("TasksContract");

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

The explanation of this code is here. Basically, at the beginning of the migration, we tell Truffle which contracts we'd like to interact with via the artifacts.require() method, in our case, our contract name is TasksContract. After that, we deploy the contract with the module exports.

To deploy the contract we'll use the comand truffle deploy, this command compile the contracts and deploy this. If we just want to compile the contracts, use the command truffle compile. This create a folder named "build", and inside it a file with the name TaskContract.json. This file is very important, it's the contract ABI and it's what we use to communicate with the methods and variables of our contract (for more information about the ABI of a contract check this page).

In the window of Ganache, we see that the first account doesn't have 100 ether like before (now we have 99.98 ether), this is because we spend 0.02 ethers in the contract deploy. Truffle takes the first account as default, unless otherwise stated.


In the terminal, we can see the transaction summary of the displayed contract:


Note: Every time we make a change to the smart contract and want to test it, we must deploy it again.

Interacting with the contract from Console Truffle

To access the console, type the command truffle console.

We need to declare a variable containing the information of the deployed contract. We do this as follows:

TasksContract = await TasksContract.depoyed()

We use this variable to interacting with the contract from the console. For example, TasksContract.address indicates the address where the contract is displayed.

To create a task from the console, we can use the public method named "createTask" that we defined earlier in the contract, this method receives as argument the title and description of the task:

await TasksContract.createTask("My first task", "I have to do something")

Important: Every time we call a public function or variable in the smart contract, we are interacting with this contract, every interaction with a smart contract is a transaction. So, truffle shows us the summary of this transaction in the console (such as gas used, transaction ID, etc.):


Then to see the task we can use the mapping tasks we defined earlier (named tasks). Since it's the first task and we define the counter as 0, we pass 0 as the key value of the mapping:

await TasksContract.tasks(0)

This shows us the information (title, description, done, etc.) of the task we just created. In this way we see if the function correctly fulfills its purpose 


Done task function ✔️

Now we'll create a function that changes the state of a task, from not done to done, and vice versa.

First we need the id of the task we want to change the status of. When we already identify the task within the mapping with its id, we change its state to the opposite of what it had, then we'll store the task with the new state. The code in Solidity should be something like:

function toggleDone(uint256 _id) public {
    Task memory _task = tasks[_id];
    _task.done = !_task.done;
    tasks[_id] = _task;

Note: When we change the state of the task, we are changing the state of the smart contract, this is why this action (change the task as completed) is a transaction and we have to pay a fee to do it (in case this project is deployed in a main network).


Testing Contract with JavaScript 🔬

We'll test the contract with JavaScript, this gives us the flexibility to test multiple functions without having to do it manually from the console. Truffle uses the Mocha testing framework and Chai for assertions to provide you with a solid framework from which to write your JavaScript tests (for more information about writing test in JS check here).

The first thing we do is create a .js file inside the test folder, in my case I named TaskContract_test.js. 

We start by declaring a variable that contains the contract information (as we did in the implementation file), that's the variable that we'll use to interact with the contract inside the file test.

const TaskContract = artifacts.require("TaskContract");

If you have experience with tests in applications with JS you know about describe() , this allows us to group a series of common tests common to something we define (for example a function), here the describe() function is called contract() and it works the same way, group tests of the same contract. 

When we interact with the contract in the Truffle console, we need to define a variable that contains the disployed contract information, this variable is the one we use to interact with the contract functions. We define this variable within the before() function, which is executed before each test (that is, what we are doing here is generating the contract deployment before each test):

const TaskContract = artifacts.require("TaskContract");
contract("TaskContract", () =>{  
    before(async() => {
        this.taskContract = await TaskContract.deployed();
    })
});

Note: We use "this" before the name variable to use it outside of the before function.

For each test inside the "contract()" function we use the "it" notation, where we indicate:

it("test name" ; function to do the test()=>{});

We start by testing if the contract deployment is successful, we can verify this by checking if the contract address is different from null, undefined, 0x0 or an empty string.

it("contract deployment successfully", async() => {
   address = await this.taskContract.address;
   assert.notEqual(address, null);
   assert.notEqual(address, undefined);
   assert.notEqual(address, 0x0);
   assert.notEqual(address, "");
});

To run the tests, we type the command truffle test in the terminal. If the contract was successfully deployed, we would see a green tick in the terminal next to the test name.

Ok, but now, how to test the creation of a task or the toggle done?

We could use events. Basically, we use events to signal that something has happened. In our case, we can emit an event when a task is created. For this case we'll create an event that contains all the parameters of the Task data structure that we created earlier:

event TaskCreated(
    uint256 id,
    string title,
    string description,
    bool done,
    uint256 createdAt
);

Note that the syntax is like a function, not a struct. 

Then, the moment a task is created, we emit this event with the emit keyword, passing the values of the created task as arguments:

function createTask(string memory _title, string memory _description) public {
        
   tasks[taskCounter] = Task(
        taskCounter,
        _title,
        _description,
        false,
        block.timestamp
    );
    emit TaskCreated(taskCounter, _title, _description, false, block.timestamp);
    taskCounter++;
}

The events are stored in the logs[] array of the transaction, If we deploy the contract again and create a new task, we can see in this transaction that the logs part contains an object:

In fact, we can see that the event is "Task Created", which is the event we just created. The results or values of this event are in the "args" (marked with the yellow arrow). 


CONTINUAR - FALTA TEST DE CREATED TASK Y TEST DE TOGGLE DONE


Frontend 🏠

Ok, we have the smart contract and we already tested that it works as it should. Now we need to create an interface so that people can interact with the notes app in their browser without necessarily having a technical understanding of what's going on (in terms of the smarts contracts).