The popularity of Blockchain technology is increasing day by day and so are the reasons to learn it.
This is the first article of the series Learning to Code Blockchain DApps from CryptoZombies in which I will be documenting my journey as I go through all the lessons present in the CryptoZombies curriculum.
Before getting into the lessons, Let's first understand what is CryptoZombies?
So, What is CryptoZombies?
CryptoZombies is a free resource for aspiring blockchain dApp developers in which users get to learn all the important concepts of dApp development and the Solidity programming language by building their own NFT-style Zombie game.
CryptoZombies is an interactive school that teaches you all things technical about blockchains. Learn to make smart contracts in Solidity by making your own crypto-collectibles game.
Why CryptoZombies?
When learning something new I generally try to find resources which are well structured and which walks you through the concepts in a progressive way, so that I am clear with the basics before jumping into more advanced concepts and after browsing through CryptoZombies curriculum I found out it follows a similar structure.
The best part about learning from CryptoZombies is that it's completely browser based and you don't have to install or configure anything.
So, after this little introduction of CryptoZombies let's try to understand the concepts explained in Lesson 1.
Lesson 1 - Making the Zombie Factory
Chapter 1: Lesson Overview
In this lesson, we are supposed to build a Zombie Factory
to make a lot of zombies for our army and in order to do that we have to,
- maintain a database of all zombies in our army.
- make a function for creating new zombies.
- make sure each zombie has a random and unique appearance.
But before we do that, We should first understand how zombie DNA works as the zombie's appearance is based on its Zombie DNA
.
Zombie DNA
is just a simple 16-digit integer, like:
8356281049284737
So, just like real DNA different parts of this number maps to different traits. The first 2 digits map to the zombie's head type, the second 2 digits to the zombie's eyes, etc. The zombie can have only 7 different types of heads and 8, 3 are the first two digits in the above Zombie DNA sample, to map that to the zombie's head type, we do
83 % 7 + 1 = 7
Which means this Zombie would have the 7th zombie head type (The Santa hat).
As we now have a better understanding of what Zombie DNA
is, let's see how we can build our own Zombie army.
Chapter 2: Contracts
Solidity's code is encapsulated in contracts and to start creating our Zombie army, we have to create a base Contract
called ZombieFactory
.
What are Contracts?
A contract is the fundamental building block of Ethereum applications — all variables and functions belong to a contract, and this will be the starting point of all your projects.
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
}
In the above code, the first line pragma solidity >=0.5.0 <0.6.0;
is known as a Version Pragma
.
What is a Version Pragma?
All solidity source code starts with a "version pragma". It simply tells the source code is written for which solidity version. This is to prevent issues with future compiler versions potentially introducing changes that would break the code.
Our contract uses solidity version which is greater than or equal to 0.5.0 and less than 0.6.0. Therefore,
pragma solidity >=0.5.0 <0.6.0;
Chapter 3: State Variables and Integeres
Now, as our Zombie DNA is going to be determined by a 16-digit number we will create a State Variable
named dnaDigits
and set it equal to 16
.
What are State Variables?
Variables which are permanently stored in contract storage is known as State Variables. These variables are permanently written to the Ethereum blockchain. Think of it as storing values in a database.
We will be using the uint
data type while defining our state variable dnaDigits
which means its value must not be negative.
Note:
uint
is a data type which stands for unsigned integer. There's also anint
data type for signed integers.
Chapter 4: Math Operations
To make sure our Zombie's DNA is only 16 characters we will make another uint
named dnaModulus
and set it equal to 10^dnaDigits
, since dnaDigits = 16
. That way we can later use the modulus operator %
to shorten an integer to 16 digits.
Note: Math in solidity is pretty straightforward and the operations are almost the same as in most programming languages.
Our solidity code should now look something like this, 👇
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
// This will be stored permanently in the blockchain
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits; // equal to 10^16
}
Chapter 5: Structs
Just like us humans, our zombies will also have multiple properties like name (a string)
, dna (a uint)
and to club these properties into one single data type solidity provides us with a tool known as structs
.
What are Structs?
Structs allow us to create more complicated data types that have multiple properties.
As we are going to want to create some zombies for our app, we will create a struct
named Zombie
with two properties name (a string)
and dna (a uint)
.
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
// This will be stored permanently in the blockchain
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits; // equal to 10^16
// Struct
struct Zombie {
string name;
uint dna;
}
}
Note: String is a data type which can store a sequence of characters. Ex:
string greeting = "Hello world!"
Chapter 6: Arrays
In order to store an army of zombies we will need a collection of zombies and in solidity, arrays
are used to create a collection of something.
What are Arrays?
Arrays are used to store a collection of something. In solidity, there are two types of arrays -
Fixed arrays
andDynamic arrays
.
Ex:
// Array with a fixed length of 2 uint elements:
uint[2] fixedArray;
// A dynamic Array - has no fixed size, can keep growing:
uint[] dynamicArray;
We can also declare an array as
public
which enables other contracts to read from this array, but can not write to this array.
Ex:
// Person struct
struct Person {
uint age;
string name;
}
// Dynamic Public Array named people which can store a lot of persons.
Person[] public people;
So, we will now create a public array of Zombie struct
, and name it zombies
.
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
// This will be stored permanently in the blockchain
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits; // equal to 10^16
// Zombie struct
struct Zombie {
string name;
uint dna;
}
// zombies array
Zombie[] public zombies;
}
Chapter 7: Function Declarations
Now, we will create a public function
named createZombie
for creating more zombies. It will take two parameters: _name (a string)
, and _dna (a uint)
.
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
// This will be stored permanently in the blockchain
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits; // equal to 10^16
// Zombie struct
struct Zombie {
string name;
uint dna;
}
// zombies array
Zombie[] public zombies;
// createZombie public function
function createZombie(string memory _name, uint _dna) public {
}
}
Note: It's convention (but not required) to start function parameter variable names with an underscore (_) in order to differentiate them from global variables.
For now the body of the function is empty. Note that we're specifying the function visibility as public
. We're also providing instructions about where the _name variable should be stored- in memory
. This is required for all reference types such as arrays, structs, mappings, and strings.
There are two ways in which you can pass an argument to a Solidity function:
- By value, which means that the Solidity compiler creates a new copy of the parameter's value and passes it to your function. This allows your function to modify the value without worrying that the value of the initial parameter gets changed.
- By reference, which means that your function is called with a reference to the original variable. Thus, if your function changes the value of the variable it receives, the value of the original variable gets changed.
Chapter 8: Working With Structs and Arrays
In the last chapter, we created an empty function named createZombie
with two parameters: _name (a string)
, and _dna (a uint)
. So, in this chapter we will try to fill the function so it can actually create a zombie
whose name and dna will come from the function arguments and add it to the zombies
array.
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
// This will be stored permanently in the blockchain
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits; // equal to 10^16
// Zombie struct
struct Zombie {
string name;
uint dna;
}
// zombies array
Zombie[] public zombies;
// createZombie public function
function createZombie(string memory _name, uint _dna) public {
// Create a new zombie and push it at the end of zombies array
zombies.push(Zombie(_name, _dna));
}
}
Note: array.push() adds something to the end of the array.
Chapter 9: Private / Public Functions
In solidity, functions are public
by default which means anyone or any contract can call our contract's function and execute its code this can make our contract vulnerable to attacks.
Our contract's createZombie
function is currently public
and anyone could call it and create a new Zombie in our contract which is not desirable. So, we will modify our createZombie
function and make it private. This will only allow other functions within our contract to call createZombie
function and add new zombies to the zombies
array.
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
// This will be stored permanently in the blockchain
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits; // equal to 10^16
// Zombie struct
struct Zombie {
string name;
uint dna;
}
// zombies array
Zombie[] public zombies;
// createZombie PRIVATE function
function _createZombie(string memory _name, uint _dna) private {
// Create a new zombie and push it at the end of zombies array
zombies.push(Zombie(_name, _dna));
}
}
Note:
- As with function parameters, it's convention to start private function names with an underscore (_).
- It is a good practice to mark your functions as private by default, and then only make those functions public which you want to expose to the world.
Chapter 10: More on Functions
In this chapter, we have to create a private view function
called _generateRandomDna
which takes a single string
parameter _str
and it should return a uint
. We will fill the function body in the next chapter.
A return
value in solidity functions is a value which gets returned when the function is called. In our case, a uint
will be returned.
A view
is a function modifier for a function that only views the data present in our smart and does not modify the data.
Note: Solidity also contains pure functions, which means the function does not access any data in the app and doesn't even read from the state of the app. Its return value depends only on its function parameters.
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
// This will be stored permanently in the blockchain
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits; // equal to 10^16
// Zombie struct
struct Zombie {
string name;
uint dna;
}
// zombies array
Zombie[] public zombies;
// createZombie PRIVATE function
function _createZombie(string memory _name, uint _dna) private {
// Create a new zombie and push it at the end of zombies array
zombies.push(Zombie(_name, _dna));
}
// create private view function _generateRandomDna
function _generateRandomDna(string memory _str) private view returns (uint) {
}
}
Chapter 11: Keccak256 and Typecasting
Keccak256
is a hash function in Ethereum which is a version of SHA3
. A hash function basically converts an input into a random 256-bit hexadecimal number and even if there is just a slight change in the input it will cause a large change in the hash. We will use it for pseudo-random number generation.
keccak256
expects a single parameter of type bytes. This means that we have to "pack" any parameters before calling keccak256
.
Typecasting
is a process to convert between data types. For Eg:
uint8 a = 5;
uint b = 6;
// throws an error because a * b returns a uint, not uint8:
uint8 c = a * b;
// we have to typecast b as a uint8 to make it work:
uint8 c = a * uint8(b);
Now, ln our _generateRandomDna
function we have to call the keccak256
hash function by passing abi.encodePacked(_str)
as its parameter to generate a pseudo-random hexadecimal, after that typecast it as a uint
, and finally store the result in a uint
called rand
. Also we want our DNA to be only 16 digits long so we will use the dnaModulus
variable that we created earlier in the lesson and in the end return the result.
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
// This will be stored permanently in the blockchain
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits; // equal to 10^16
// Zombie struct
struct Zombie {
string name;
uint dna;
}
// zombies array
Zombie[] public zombies;
// createZombie PRIVATE function
function _createZombie(string memory _name, uint _dna) private {
// Create a new zombie and push it at the end of zombies array
zombies.push(Zombie(_name, _dna));
}
// create private view function _generateRandomDna
function _generateRandomDna(string memory _str) private view returns (uint) {
// Keccak256 and Typecasting
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
}
Chapter 12: Putting It Together
To complete our random Zombie generator we will create a public
function named createRandomZombie
that takes one parameter named _name
(a string), then inside this function we will call _generateRandomDna
by passing _name
as its parameter and store it in a uint
named randDna
. Next, we will call the _createZombie
function and pass _name
and randDna
as its parameters for creating a new zombie.
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
// This will be stored permanently in the blockchain
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits; // equal to 10^16
// Zombie struct
struct Zombie {
string name;
uint dna;
}
// zombies array
Zombie[] public zombies;
// createZombie PRIVATE function
function _createZombie(string memory _name, uint _dna) private {
// Create a new zombie and push it at the end of zombies array
zombies.push(Zombie(_name, _dna));
}
// create private view function _generateRandomDna
function _generateRandomDna(string memory _str) private view returns (uint) {
// Keccak256 and Typecasting
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
// create public function createRandomZombie
function createRandomZombie(string memory _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
Chapter 13: Events
We want an event to let our front-end know every time a new zombie was created, so the app can display it.
Events are a way for a contract to communicate that something happened on the blockchain to our app front-end, which can be 'listening' for certain events and take action when they happen.
So, to do that we will declare an event called NewZombie
and pass zombieId
(a uint), name
(a string), dna
(a uint) as its parameters and then modify the _createZombie
function to fire the NewZombie
event after adding the new Zombie to our zombies
array.
We are also going to need the zombie's id and array.push()
returns a uint
of the new length of the array - and since the first item in an array has index 0, array.push() - 1
will be the index of the zombie we just added. So, we will store the result of zombies.push() - 1
in a uint
called id
, and then use this in the NewZombie event.
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
// declare our NewZombie event
event NewZombie(uint zombieId, string name, uint name);
// This will be stored permanently in the blockchain
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits; // equal to 10^16
// Zombie struct
struct Zombie {
string name;
uint dna;
}
// zombies array
Zombie[] public zombies;
// createZombie PRIVATE function
function _createZombie(string memory _name, uint _dna) private {
// Create a new zombie,
// push it at the end of zombies array and
// get the id of the zombie we just added.
uint id = zombies.push(Zombie(_name, _dna)) - 1;
// fire NewZombie event
emit NewZombie(id, _name, _dna);
}
// create private view function _generateRandomDna
function _generateRandomDna(string memory _str) private view returns (uint) {
// Keccak256 and Typecasting
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
// create public function createRandomZombie
function createRandomZombie(string memory _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
Chapter 14: Web3.js
We have completed our solidity
contract. Now we need to write a javascript frontend that interacts with the contract.
Ethereum has a Javascript library called Web3.js
. In later lessons we will go over in depth how to deploy a contract and set up Web3.js
. For now CryptoZombies have provided us with some sample code that will help us understand how Web3.js
would interact with our deployed contract.
// Here's how we would access our contract:
var abi = /* abi generated by the compiler */
var ZombieFactoryContract = web3.eth.contract(abi)
var contractAddress = /* our contract address on Ethereum after deploying */
var ZombieFactory = ZombieFactoryContract.at(contractAddress)
// `ZombieFactory` has access to our contract's public functions and events
// some sort of event listener to take the text input:
$("#ourButton").click(function(e) {
var name = $("#nameInput").val()
// Call our contract's `createRandomZombie` function:
ZombieFactory.createRandomZombie(name)
})
// Listen for the `NewZombie` event, and update the UI
var event = ZombieFactory.NewZombie(function(error, result) {
if (error) return
generateZombie(result.zombieId, result.name, result.dna)
})
// take the Zombie dna, and update our image
function generateZombie(id, name, dna) {
let dnaStr = String(dna)
// pad DNA with leading zeroes if it's less than 16 characters
while (dnaStr.length < 16)
dnaStr = "0" + dnaStr
let zombieDetails = {
// first 2 digits make up the head. We have 7 possible heads, so % 7
// to get a number 0 - 6, then add 1 to make it 1 - 7. Then we have 7
// image files named "head1.png" through "head7.png" we load based on
// this number:
headChoice: dnaStr.substring(0, 2) % 7 + 1,
// 2nd 2 digits make up the eyes, 11 variations:
eyeChoice: dnaStr.substring(2, 4) % 11 + 1,
// 6 variations of shirts:
shirtChoice: dnaStr.substring(4, 6) % 6 + 1,
// last 6 digits control color. Updated using CSS filter: hue-rotate
// which has 360 degrees:
skinColorChoice: parseInt(dnaStr.substring(6, 8) / 100 * 360),
eyeColorChoice: parseInt(dnaStr.substring(8, 10) / 100 * 360),
clothesColorChoice: parseInt(dnaStr.substring(10, 12) / 100 * 360),
zombieName: name,
zombieDescription: "A Level 1 CryptoZombie",
}
return zombieDetails
}
What this javascript code then does is take the values generated in zombieDetails
above, and use some browser-based javascript magic to swap out the images and apply CSS filters. For Eg:
Conclusion
We have now successfully completed Lesson 1 of CryptoZombies. In order to complete Lesson 1 of CryptoZombies we have learnt -
- How to write Ethereum smart contracts.
- Basics of Ethereum's smart contract programming language - Solidity
If you are interested in learning how to create your own blockchain dapps on Ethereum like me for FREE, CryptoZombies could be a good place to start.
Thank you for reading this article. I am learning more about web 3.0 and blockchain development, if you're interested in being a part of this learning journey feel free to follow me here or on twitter.
Top comments (0)