Photo by Cytonn Photography on Unsplash
Blockchain Todo List With Solidity Part 2
Dipping into Solidity
In the last part of this series, we set up our little project to be ready to start developing a Smart Contract.
Hardhat Example
Before we dive into Solidity and how we develop the to-do list example we look at the basic example contract that Hardhat generated for us.
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract Greeter {
string private greeting;
constructor(string memory _greeting) {
console.log("Deploying a Greeter with greeting:", _greeting);
greeting = _greeting;
}
function greet() public view returns (string memory) {
return greeting;
}
function setGreeting(string memory _greeting) public {
console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
greeting = _greeting;
}
}
This contract saves a string on the Blockchain (the greeting) and clients can get this saved string by calling the greet function. The string is set upon construction (deployment) of the contract but can also be manipulated later by using the setGreeting-function. This contract already shows a lot of the stuff we're going to deal with in this article as well. Let's look further into the example code and see how to test such a contract:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Greeter", function () {
it("Should return the new greeting once it's changed", async function () {
const Greeter = await ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, world!");
await greeter.deployed();
expect(await greeter.greet()).to.equal("Hello, world!");
const setGreetingTx = await greeter.setGreeting("Hola, mundo!");
// wait until the transaction is mined
await setGreetingTx.wait();
expect(await greeter.greet()).to.equal("Hola, mundo!");
});
});
For anyone who ever did tests in JavaScript with Jest, this looks familiar. Hardhat gives us the tools we need to get a contract instance we can interact with.
const Greeter = await ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, world!");
await greeter.deployed();
After the deployed-function is finished we have the contract instance in the greeter variable. Now we can use chai to perform assertions on the contracts data and build out the test itself.
Testing the code of smart contracts is even more important because once the contract is deployed it can not be altered anymore. So what seems optional to some for normal programs is a must for every smart contract.
Last but not least we have the sample deploy script in the scripts directory:
const hre = require("hardhat");
async function main() {
const Greeter = await hre.ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, Hardhat!");
await greeter.deployed();
console.log("Greeter deployed to:", greeter.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
I stripped out the comments for readability in this blog post, but they contain useful information so go read it.
So the script itself looks similar to the test script. The asynchronous main function is performing the same steps for deploying and writing the address to the console.
If we run this script for example with:
hardhat run --network ganache scripts/sample-script.js
We get an output similar to this:
Greeter deployed to: 0x81EbFc63dD377D675Da74adA2A12168bc3e08FEc
This is the address on the Blockchain where this contract was deployed. We use this address later when we build our frontend to interact with the deployed contract.
Now we looked at the example we know what we need to do to build a contract for our todo list:
- Define the contract in Solidity
- Test the contract using Jest, Hardhat and Chai
- Deploy the contract using a script and Hardhat
Contract-Definition with Solidity
Solidity is a statically-typed curly-braces programming language designed for developing smart contracts that run on Ethereum.
This is the definition on the official Solidity homepage. Of course, I won't be able to teach the entire language in a few blog posts. A very fun and pretty complete tutorial on Solidity can be found on Cryptozombies.io. I will just go over the contract we need for the todo list.
So we start by defining the Solidity version this contract is using:
pragma solidity >=0.5.0;
This means this contract can be compiled with any Solidity-version higher than 0.5.0.
Next, we define the actual contract:
contract TodoList {
}
Data Types
Now we finally fill the contract with life. In Solidity, we begin with defining the data we want to store on the blockchain. Solidity knows five basic datatypes:
- int, uint : Integers (signed and unsigned)
- fixed, ufixed: Fixed-point decimals (signed and unsigned)
- string: Strings
- address, address payable: 20-byte address on the blockchain
- bool: Boolean
A deeper explanation about data types can be found here.
Complex types can be built using struct, like in C.
We will use struct to describe the data type of a Todo with an id, a name, and the status:
contract TodoList {
struct Todo {
uint256 id;
string name;
bool status;
}
}
To actually store the todos we use a so-called mapping. That data type maps key to a value. We use that to map the id of an Todo to the actual Todo-object. The syntax to define a mapping:
mapping (<key> => <value>) <modifier> <name>;
We will also use an unsigned integer to keep track of how many todos we did store already:
contract TodoList {
struct Todo {
uint256 id;
string name;
bool status;
}
uint256 public taskCount = 0;
mapping(uint256 => Todo) public todos;
}
The public-keyword makes both of our state variables available to everyone. To complete the contract we need a function to create a todo.
Functions
Functions in Solidity work like in any other language. We have a few extra things we can do with functions like guarding them with modifiers, require or using the payable-keyword. In this first dive into smart contracts we won't use those and just write some straightforward data manipulation functions:
function create(string memory _name) public {
todos[taskCount] = Todo(taskCount, _name, false);
taskCount++;
}
This function is used to create a new Todo, store it in our defined mapping todos and increment the taskCount-variable.
Note the memory keyword next to the parameter _name. The string data type is a so-called reference type which by default are stored in storage-mode. This means it's saved persistently on the blockchain which costs gas. We want to avoid that as much as we can. This mode is obviously unnecessary for a method parameter, so we explicitly use the memory-keyword to prevent that.
Note: In Solidity Version >= 0.6.9 there is also the calldata-mode usable in functions as well, which is to be preferred over memory.
You can find more on those modes and reference types in the Solidity Documentation here and here.
With that, the contract is almost complete. We add a function to toggle the status-variable for a given todo like this:
function toggleComplete(uint256 taskId) public {
todos[taskId].status = !todos[taskId].status;
}
uint data is by default value data so we don't need the memory keyword here.
Compiling
The final contract looks like this now:
pragma solidity >=0.5.0;
contract TodoList {
uint256 public taskCount = 0;
struct Todo {
uint256 id;
string name;
bool status;
}
mapping(uint256 => Todo) public todos;
function create(string memory _name) public {
todos[taskCount] = Todo(taskCount, _name, false);
taskCount++;
}
function toggleComplete(uint256 taskId) public {
todos[taskId].status = !todos[taskId].status;
}
}
All that's left to do is to compile. To do that we can use the script we defined while setting up the project
npm run compile
or hardhat directly
npx hardhat compile
We should see an output like
Solidity compilation finished successfully
If we inspect our project folder now we will find two new folders, named artifacts and cache. We ignore the cache folder since that is a hardhat internal to not compile everything on every run.
The artifacts folder contains our compiled contract. The important file lies under contracts/TodoList.sol/TodoList.json. This contains besides the compiled bytecode the so-called ABI (Application Binary Interface). Since a deployed contract is just bytecode for the EVM (Ethereum Virtual Machine) we need this interface to interpret return values from the contract. We will use the ABI when we develop the corresponding frontend.
Conclusion
And that's it for this easy smart contract. We saw how to write and compile an easy smart contract in Solidity.
In the next part of this series, we will test and deploy the contract before we build the frontend to actually use the deployed contract.